{"rowid": 234, "title": "An Introduction to CSS 3-D Transforms", "contents": "Ladies and gentlemen, it is the second decade of the third millennium and we are still kicking around the same 2-D interface we got three decades ago. Sure, Apple debuted a few apps for OSX 10.7 that have a couple more 3-D flourishes, and Microsoft has had that Flip 3D for a while. But c\u2019mon \u2013 2011 is right around the corner. That\u2019s Twenty Eleven, folks. Where is our 3-D virtual reality? By now, we should be zipping around the Metaverse on super-sonic motorbikes.\n\nGranted, the capability of rendering complex 3-D environments has been present for years. On the web, there are already several solutions: Flash; three.js in ; and, eventually, WebGL. Finally, we meagre front-end developers have our own three-dimensional jewel: CSS 3-D transforms!\n\nRationale\n\nLike a beautiful jewel, 3-D transforms can be dazzling, a true spectacle to behold. But before we start tacking 3-D diamonds and rubies to our compositions like Liberace\u2018s tailor, we owe it to our users to ask how they can benefit from this awesome feature. \n\nAn entire application should not take advantage of 3-D transforms. CSS was built to style documents, not generate explorable environments. I fail to find a benefit to completing a web form that can be accessed by swivelling my viewport to the Sign-Up Room (although there have been proposals to make the web just that). Nevertheless, there are plenty of opportunities to use 3-D transforms in between interactions with the interface, via transitions.\n\nTake, for instance, the Weather App on the iPhone. The application uses two views: a details view; and an options view. Switching between these two views is done with a 3-D flip transition. This informs the user that the interface has two \u2013 and only two \u2013 views, as they can exist only on either side of the same plane.\n\n Flipping from details view to options view via a 3-D transition\n\nAlso, consider slide shows. When you\u2019re looking at the last slide, what cues tip you off that advancing will restart the cycle at the first slide? A better paradigm might be achieved with a 3-D transform, placing the slides side-by-side in a circle (carousel) in three-dimensional space; in that arrangement, the last slide obviously comes before the first.\n\n3-D transforms are more than just eye candy. We can also use them to solve dilemmas and make our applications more intuitive. \n\nCurrent support\n\nThe CSS 3D Transforms module has been out in the wild for over a year now. Currently, only Safari supports the specification \u2013 which includes Safari on Mac OS X and Mobile Safari on iOS. \n\nThe support roadmap for other browsers varies. The Mozilla team has taken some initial steps towards implementing the module. Mike Taylor tells me that the Opera team is keeping a close eye on CSS transforms, and is waiting until the specification is fleshed out. And our best friend Internet Explorer still needs to catch up to 2-D transforms before we can talk about the 3-D variety.\n\nTo make matters more perplexing, Safari\u2019s WebKit cousin Chrome currently accepts 3-D transform declarations, but renders them in 2-D space. Chrome team member Paul Irish, says that 3-D transforms are on the horizon, perhaps in one of the next 8.0 releases.\n\nThis all adds up to a bit of a challenge for those of us excited by 3-D transforms. I\u2019ll give it to you straight: missing the dimension of depth can make degradation a bit ungraceful. Unless the transform is relatively simple and holds up in non-3D-supporting browsers, you\u2019ll most likely have to design another solution. But what\u2019s another hurdle in a steeplechase? We web folk have had our mettle tested for years. We\u2019re prepared to devise multiple solutions.\n\nHere\u2019s the part of the article where I mention Modernizr, and you brush over it because you\u2019ve read this part of an article hundreds of times before. But seriously, it\u2019s the best way to test for CSS 3-D transform support. Use it.\n\nEven with these difficulties mounting up, trying out 3-D transforms today is the right move. The CSS 3-D transforms module was developed by the same team at Apple that produced the CSS 2D Transforms and Animation modules. Both specifications have since been adopted by Mozilla and Opera. Transforming in three-dimensions now will guarantee you\u2019ll be ahead of the game when the other browsers catch up.\n\nThe choice is yours. You can make excuses and pooh-pooh 3-D transforms because they\u2019re too hard and only snobby Apple fans will see them today. Or, with a tip of the fedora to Mr Andy Clarke, you can get hard-boiled and start designing with the best features out there right this instant.\n\nSo, I bid you, in the words of the eternal Optimus Prime\u2026\n\n\n\tTransform and roll out.\n\n\nLet\u2019s get coding.\n\nPerspective\n\nTo activate 3-D space, an element needs perspective. This can be applied in two ways: using the transform property, with the perspective as a functional notation:\n\n-webkit-transform: perspective(600);\n\nor using the perspective property: \n\n-webkit-perspective: 600;\n\nSee example: Perspective 1.\n\n\n\n The red element on the left uses transform: perspective() functional notation; the blue element on the right uses the perspective property\n\n\n\nThese two formats both trigger a 3-D space, but there is a difference. The first, functional notation is convenient for directly applying a 3-D transform on a single element (in the previous example, I use it in conjunction with a rotateY transform). But when used on multiple elements, the transformed elements don\u2019t line up as expected. If you use the same transform across elements with different positions, each element will have its own vanishing point. To remedy this, use the perspective property on a parent element, so each child shares the same 3-D space.\n\nSee Example: Perspective 2.\n\n\n\n Each red box on the left has its own vanishing point within the parent container; the blue boxes on the right share the vanishing point of the parent container\n\n\n\nThe value of perspective determines the intensity of the 3-D effect. Think of it as a distance from the viewer to the object. The greater the value, the further the distance, so the less intense the visual effect. perspective: 2000; yields a subtle 3-D effect, as if we were viewing an object from far away. perspective: 100; produces a tremendous 3-D effect, like a tiny insect viewing a massive object.\n\nBy default, the vanishing point for a 3-D space is positioned at its centre. You can change the position of the vanishing point with perspective-origin property.\n\n-webkit-perspective-origin: 25% 75%;\n\nSee Example: Perspective 3.\n\n\n\n\n\n\n\n3-D transform functions\n\nAs a web designer, you\u2019re probably well acquainted with working in two dimensions, X and Y, positioning items horizontally and vertically. With a 3-D space initialised with perspective, we can now transform elements in all three glorious spatial dimensions, including the third Z dimension, depth. \n\n3-D transforms use the same transform property used for 2-D transforms. If you\u2019re familiar with 2-D transforms, you\u2019ll find the basic 3D transform functions fairly similar. \n\n\n\trotateX(angle)\n\trotateY(angle)\n\trotateZ(angle)\n\ttranslateZ(tz)\n\tscaleZ(sz)\n\n\nWhereas translateX() positions an element along the horizontal X-axis, translateZ() positions it along the Z-axis, which runs front to back in 3-D space. Positive values position the element closer to the viewer, negative values further away.\n\nThe rotate functions rotate the element around the corresponding axis. This is somewhat counter-intuitive at first, as you might imagine that rotateX will spin an object left to right. Instead, using rotateX(45deg) rotates an element around the horizontal X-axis, so the top of the element angles back and away, and the bottom gets closer to the viewer.\n\nSee Example: Transforms 1.\n\n\n\n3-D rotate() and translate() functions around each axis\n\n\n\nThere are also several shorthand transform functions that require values for all three dimensions:\n\n\n\ttranslate3d(tx,ty,tz)\n\tscale3d(sx,sy,sz)\n\trotate3d(rx,ry,rz,angle)\n\n\nPro-tip: These foo3d() transform functions also have the benefit of triggering hardware acceleration in Safari. Dean Jackson, CSS 3-D transform spec author and main WebKit dude, writes (to Thomas Fuchs):\n\n\n\tIn essence, any transform that has a 3D operation as one of its functions will trigger hardware compositing, even when the actual transform is 2D, or not doing anything at all (such as translate3d(0,0,0)). Note this is just current behaviour, and could change in the future (which is why we don\u2019t document or encourage it). But it is very helpful in some situations and can significantly improve redraw performance.\n\n\nFor the sake of simplicity, my demos will use the basic transform functions, but if you\u2019re writing production-ready CSS for iOS or Safari-only, make sure to use the foo3d() functions to get the best rendering performance.\n\nCard flip\n\nWe now have all the tools to start making 3-D objects. Let\u2019s get started with something simple: flipping a card.\n\nHere\u2019s the basic markup we\u2019ll need:\n\n
\n
\n
1
\n
2
\n
\n
\n\nThe .container will house the 3-D space. The #card acts as a wrapper for the 3-D object. Each face of the card has a separate element: .front; and .back. Even for such a simple object, I recommend using this same pattern for any 3-D transform. Keeping the 3-D space element and the object element(s) separate establishes a pattern that is simple to understand and easier to style.\n\nWe\u2019re ready for some 3-D stylin\u2019. First, apply the necessary perspective to the parent 3-D space, along with any size or positioning styles.\n\n.container { \n width: 200px;\n height: 260px;\n position: relative;\n -webkit-perspective: 800;\n}\n\nNow the #card element can be transformed in its parent\u2019s 3-D space. We\u2019re combining absolute and relative positioning so the 3-D object is removed from the flow of the document. We\u2019ll also add width: 100%; and height: 100%;. This ensures the object\u2019s transform-origin will occur in the centre of .container. More on transform-origin later. \n\nLet\u2019s add a CSS3 transition so users can see the transform take effect. \n\n#card {\n width: 100%;\n height: 100%;\n position: absolute;\n -webkit-transform-style: preserve-3d;\n -webkit-transition: -webkit-transform 1s;\n}\n\nThe .container\u2019s perspective only applies to direct descendant children, in this case #card. In order for subsequent children to inherit a parent\u2019s perspective, and live in the same 3-D space, the parent can pass along its perspective with transform-style: preserve-3d. Without 3-D transform-style, the faces of the card would be flattened with its parents and the back face\u2019s rotation would be nullified. \n\nTo position the faces in 3-D space, we\u2019ll need to reset their positions in 2-D with position: absolute. In order to hide the reverse sides of the faces when they are faced away from the viewer, we use backface-visibility: hidden. \n\n#card figure {\n display: block;\n position: absolute;\n width: 100%;\n height: 100%;\n -webkit-backface-visibility: hidden;\n}\n\nTo flip the .back face, we add a basic 3-D transform of rotateY(180deg). \n\n#card .front {\n background: red;\n}\n#card .back {\n background: blue;\n -webkit-transform: rotateY(180deg);\n}\n\nWith the faces in place, the #card requires a corresponding style for when it is flipped.\n\n#card.flipped {\n -webkit-transform: rotateY(180deg);\n}\n\nNow we have a working 3-D object. To flip the card, we can toggle the flipped class. When .flipped, the #card will rotate 180 degrees, thus exposing the .back face.\n\nSee Example: Card 1.\n\n\n\nFlipping a card in three dimensions\n\n\n\nSlide-flip\n\nTake another look at the Weather App 3-D transition. You\u2019ll notice that it\u2019s not quite the same effect as our previous demo. If you follow the right edge of the card, you\u2019ll find that its corners stay within the container. Instead of pivoting from the horizontal centre, it pivots on that right edge. But the transition is not just a rotation \u2013 the edge moves horizontally from right to left. We can reproduce this transition just by modifying a couple of lines of CSS from our original card flip demo.\n\nThe pivot point for the rotation occurs at the right side of the card. By default, the transform-origin of an element is at its horizontal and vertical centre (50% 50% or center center). Let\u2019s change it to the right side:\n\n#card { -webkit-transform-origin: right center; }\n\nThat flip now needs some horizontal movement with translateX. We\u2019ll set the rotation to -180deg so it flips right side out.\n\n#card.flipped {\n -webkit-transform: translateX(-100%) rotateY(-180deg);\n}\n\nSee Example: Card 2.\n\n\n\nCreating a slide-flip from the right edge of the card\n\n\n\nCube\n\nCreating 3-D card objects is a good way to get started with 3-D transforms. But once you\u2019ve mastered them, you\u2019ll be hungry to push it further and create some true 3-D objects: prisms. We\u2019ll start out by making a cube.\n\nThe markup for the cube is similar to the card. This time, however, we need six child elements for all six faces of the cube:\n\n
\n
\n
1
\n
2
\n
3
\n
4
\n
5
\n
6
\n
\n
\n\nBasic position and size styles set the six faces on top of one another in the container.\n\n.container {\n width: 200px;\n height: 200px;\n position: relative;\n -webkit-perspective: 1000;\n}\n#cube {\n width: 100%;\n height: 100%;\n position: absolute;\n -webkit-transform-style: preserve-3d;\n}\n#cube figure {\n width: 196px;\n height: 196px;\n display: block;\n position: absolute;\n border: 2px solid black;\n}\n\nWith the card, we only had to rotate its back face. The cube, however, requires that five of the six faces to be rotated. Faces 1 and 2 will be the front and back. Faces 3 and 4 will be the sides. Faces 5 and 6 will be the top and bottom.\n\n#cube .front { -webkit-transform: rotateY(0deg); }\n#cube .back { -webkit-transform: rotateX(180deg); }\n#cube .right { -webkit-transform: rotateY(90deg); }\n#cube .left { -webkit-transform: rotateY(-90deg); }\n#cube .top { -webkit-transform: rotateX(90deg); }\n#cube .bottom { -webkit-transform: rotateX(-90deg); }\n\nWe could remove the first #cube .front style declaration, as this transform has no effect, but let\u2019s leave it in to keep our code consistent.\n\nNow each face is rotated, and only the front face is visible. The four side faces are all perpendicular to the viewer, so they appear invisible. To push them out to their appropriate sides, they need to be translated out from the centre of their positions. Each side of the cube is 200 pixels wide. From the cube\u2019s centre they\u2019ll need to be translated out half that distance, 100px.\n\n#cube .front { -webkit-transform: rotateY(0deg) translateZ(100px); }\n#cube .back { -webkit-transform: rotateX(180deg) translateZ(100px); }\n#cube .right { -webkit-transform: rotateY(90deg) translateZ(100px); }\n#cube .left { -webkit-transform: rotateY(-90deg) translateZ(100px); }\n#cube .top { -webkit-transform: rotateX(90deg) translateZ(100px); }\n#cube .bottom { -webkit-transform: rotateX(-90deg) translateZ(100px); }\n\nNote here that the translateZ function comes after the rotate. The order of transform functions is important. Take a moment and soak this up. Each face is first rotated towards its position, then translated outward in a separate vector.\n\nWe have a working cube, but we\u2019re not done yet.\n\nReturning to the Z-axis origin\n\nFor the sake of our users, our 3-D transforms should not distort the interface when the active panel is at its resting position. But once we start pushing elements off their Z-axis origin, distortion is inevitable. \n\nIn order to keep 3-D transforms snappy, Safari composites the element, then applies the transform. Consequently, anti-aliasing on text will remain whatever it was before the transform was applied. When transformed forward in 3-D space, significant pixelation can occur. \n\nSee Example: Transforms 2.\n\n\n\n\n\n\n\nLooking back at the Perspective 3 demo, note that no matter how small the perspective value is, or wherever the transform-origin may be, the panel number 1 always returns to its original position, as if all those funky 3-D transforms didn\u2019t even matter.\n\nTo resolve the distortion and restore pixel perfection to our #cube, we can push the 3-D object back, so that the front face will be positioned back to the Z-axis origin.\n\n#cube { -webkit-transform: translateZ(-100px); }\n\nSee Example: Cube 1.\n\n\n\nRestoring the front face to the original position on the Z-axis\n\n\n\nRotating the cube\n\nTo expose any face of the cube, we\u2019ll need a style that rotates the cube to expose any face. The transform values are the opposite of those for the corresponding face. We toggle the necessary class on the #box to apply the appropriate transform.\n\n#cube.show-front { -webkit-transform: translateZ(-100px) rotateY(0deg); }\n#cube.show-back { -webkit-transform: translateZ(-100px) rotateX(-180deg); }\n#cube.show-right { -webkit-transform: translateZ(-100px) rotateY(-90deg); }\n#cube.show-left { -webkit-transform: translateZ(-100px) rotateY(90deg); }\n#cube.show-top { -webkit-transform: translateZ(-100px) rotateX(-90deg); }\n#cube.show-bottom { -webkit-transform: translateZ(-100px) rotateX(90deg); }\n\nNotice how the order of the transform functions has reversed. First, we push the object back with translateZ, then we rotate it.\n\nFinishing up, we can add a transition to animate the rotation between states. \n\n#cube { -webkit-transition: -webkit-transform 1s; }\n\nSee Example: Cube 2.\n\n\n\nRotating the cube with a CSS transition\n\n\n\nRectangular prism\n\nCubes are easy enough to generate, as we only have to worry about one measurement. But how would we handle a non-regular rectangular prism? Let\u2019s try to make one that\u2019s 300 pixels wide, 200 pixels high, and 100 pixels deep. \n\nThe markup remains the same as the #cube, but we\u2019ll switch the cube id for #box. The container styles remain mostly the same:\n\n.container {\n width: 300px;\n height: 200px;\n position: relative;\n -webkit-perspective: 1000;\n}\n#box {\n width: 100%;\n height: 100%;\n position: absolute;\n -webkit-transform-style: preserve-3d;\n}\n\nNow to position the faces. Each set of faces will need their own sizes. The smaller faces (left, right, top and bottom) need to be positioned in the centre of the container, where they can be easily rotated and then shifted outward. The thinner left and right faces get positioned left: 100px ((300\u2009\u2212\u2009100)\u2009\u00f7\u20092), The stouter top and bottom faces get positioned top: 50px ((200\u2009\u2212\u2009100)\u2009\u00f7\u20092).\n\n#box figure {\n display: block;\n position: absolute;\n border: 2px solid black;\n}\n#box .front,\n#box .back {\n width: 296px;\n height: 196px;\n}\n#box .right,\n#box .left {\n width: 96px;\n height: 196px;\n left: 100px;\n}\n#box .top,\n#box .bottom {\n width: 296px;\n height: 96px;\n top: 50px;\n}\n\nThe rotate values can all remain the same as the cube example, but for this rectangular prism, the translate values do differ. The front and back faces are each shifted out 50 pixels since the #box is 100 pixels deep. The translate value for the left and right faces is 150 pixels for their 300 pixels width. Top and bottom panels take 100 pixels for their 200 pixels height:\n\n#box .front { -webkit-transform: rotateY(0deg) translateZ(50px); }\n#box .back { -webkit-transform: rotateX(180deg) translateZ(50px); }\n#box .right { -webkit-transform: rotateY(90deg) translateZ(150px); }\n#box .left { -webkit-transform: rotateY(-90deg) translateZ(150px); }\n#box .top { -webkit-transform: rotateX(90deg) translateZ(100px); }\n#box .bottom { -webkit-transform: rotateX(-90deg) translateZ(100px); }\n\nSee Example: Box 1.\n\n\n\n\n\n\n\nJust like the cube example, to expose a face, the #box needs to have a style to reverse that face\u2019s transform. Both the translateZ and rotate values are the opposites of the corresponding face.\n\n#box.show-front { -webkit-transform: translateZ(-50px) rotateY(0deg); }\n#box.show-back { -webkit-transform: translateZ(-50px) rotateX(-180deg); }\n#box.show-right { -webkit-transform: translateZ(-150px) rotateY(-90deg); }\n#box.show-left { -webkit-transform: translateZ(-150px) rotateY(90deg); }\n#box.show-top { -webkit-transform: translateZ(-100px) rotateX(-90deg); }\n#box.show-bottom { -webkit-transform: translateZ(-100px) rotateX(90deg); }\n\nSee Example: Box 2.\n\n\n\nRotating the rectangular box with a CSS transition\n\n\n\nCarousel\n\nFront-end developers have a myriad of choices when it comes to content carousels. Now that we have 3-D capabilities in our browsers, why not take a shot at creating an actual 3-D carousel?\n\nThe markup for this demo takes the same form as the box, cube and card. Let\u2019s make it interesting and have a carousel with nine panels.\n\n
\n
\n
1
\n
2
\n
3
\n
4
\n
5
\n
6
\n
7
\n
8
\n
9
\n
\n
\n\nNow, apply basic layout styles. Let\u2019s give each panel of the #carousel 20 pixel gaps between one another, done here with left: 10px; and top: 10px;. The effective width of each panel is 210 pixels.\n\n.container {\n width: 210px;\n height: 140px;\n position: relative;\n -webkit-perspective: 1000;\n}\n#carousel {\n width: 100%;\n height: 100%;\n position: absolute;\n -webkit-transform-style: preserve-3d;\n}\n#carousel figure {\n display: block;\n position: absolute;\n width: 186px;\n height: 116px;\n left: 10px;\n top: 10px;\n border: 2px solid black;\n}\n\nNext up: rotating the faces. This #carousel has nine panels. If each panel gets an equal distribution on the carousel, each panel would be rotated forty degrees from its neighbour (360\u2009\u00f7\u20099).\n\n#carousel figure:nth-child(1) { -webkit-transform: rotateY(0deg); }\n#carousel figure:nth-child(2) { -webkit-transform: rotateY(40deg); }\n#carousel figure:nth-child(3) { -webkit-transform: rotateY(80deg); }\n#carousel figure:nth-child(4) { -webkit-transform: rotateY(120deg); }\n#carousel figure:nth-child(5) { -webkit-transform: rotateY(160deg); }\n#carousel figure:nth-child(6) { -webkit-transform: rotateY(200deg); }\n#carousel figure:nth-child(7) { -webkit-transform: rotateY(240deg); }\n#carousel figure:nth-child(8) { -webkit-transform: rotateY(280deg); }\n#carousel figure:nth-child(9) { -webkit-transform: rotateY(320deg); }\n\nNow, the outward shift. Back when we were creating the cube and box, the translate value was simple to calculate, as it was equal to one half the width, height or depth of the object. With this carousel, there is no size we can automatically use as a reference. We\u2019ll have to calculate the distance of the shift by other means.\n\n\n\nDrawing a diagram of the carousel, we can see that we know only two things: the width of each panel is 210 pixels; and the each panel is rotated forty degrees from the next. If we split one of these segments down its centre, we get a right-angled triangle, perfect for some trigonometry.\n\nWe can determine the length of r in this diagram with a basic tangent equation:\n\n\n\nThere you have it: the panels need to be translated 288 pixels in 3-D space. \n\n#carousel figure:nth-child(1) { -webkit-transform: rotateY(0deg) translateZ(288px); }\n#carousel figure:nth-child(2) { -webkit-transform: rotateY(40deg) translateZ(288px); }\n#carousel figure:nth-child(3) { -webkit-transform: rotateY(80deg) translateZ(288px); }\n#carousel figure:nth-child(4) { -webkit-transform: rotateY(120deg) translateZ(288px); }\n#carousel figure:nth-child(5) { -webkit-transform: rotateY(160deg) translateZ(288px); }\n#carousel figure:nth-child(6) { -webkit-transform: rotateY(200deg) translateZ(288px); }\n#carousel figure:nth-child(7) { -webkit-transform: rotateY(240deg) translateZ(288px); }\n#carousel figure:nth-child(8) { -webkit-transform: rotateY(280deg) translateZ(288px); }\n#carousel figure:nth-child(9) { -webkit-transform: rotateY(320deg) translateZ(288px); }\n\nIf we decide to change the width of the panel or the number of panels, we only need to plug in those two variables into our equation to get the appropriate translateZ value. In JavaScript terms, that equation would be:\n\nvar tz = Math.round( ( panelSize / 2 ) / \n Math.tan( ( ( Math.PI * 2 ) / numberOfPanels ) / 2 ) );\n// or simplified to\nvar tz = Math.round( ( panelSize / 2 ) / \n Math.tan( Math.PI / numberOfPanels ) );\n\nJust like our previous 3-D objects, to show any one panel we need only apply the reverse transform on the carousel. Here\u2019s the style to show the fifth panel:\n\n-webkit-transform: translateZ(-288px) rotateY(-160deg);\n\nSee Example: Carousel 1.\n\n\n\n\n\n\n\nBy now, you probably have two thoughts: \n\n\n\tRewriting transform styles for each panel looks tedious.\n\tWhy bother doing high school maths? Aren\u2019t robots supposed to be doing all this work for us?\n\n\nAnd you\u2019re absolutely right. The repetitive nature of 3-D objects lends itself to scripting. We can offload all the monotonous transform styles to our dynamic script, which, if done correctly, will be more flexible than the hard-coded version.\n\nSee Example: Carousel 2.\n\nConclusion\n\n3-D transforms change the way we think about the blank canvas of web design. Better yet, they change the canvas itself, trading in the flat surface for voluminous depth.\n\nMy hope is that you took at least one peak at a demo and were intrigued. We web designers, who have rejoiced for border-radius, box-shadow and background gradients, now have an incredible tool at our disposal in 3-D transforms. They deserve just the same enthusiasm, research and experimentation we have seen on other CSS3 features. Now is the perfect time to take the plunge and start thinking about how to use three dimensions to elevate our craft. I\u2019m breathless waiting for what\u2019s to come. \n\nSee you on the flip side.", "year": "2010", "author": "David DeSandro", "author_slug": "daviddesandro", "published": "2010-12-14T00:00:00+00:00", "url": "https://24ways.org/2010/intro-to-css-3d-transforms/", "topic": "code"}