Tutorials – Codrops: Recent Episodes

None

Fueling web creativity since 2009

View Details

Learn how to bring the Mac Mini animation to life using Rive’s powerful no-code tools, with tips on nested artboards, state machines, and faux 3D effects.

View Details

Explore the world of shaders with this easy-to-follow guide to creating a custom ASCII art animation using WebGL, Perlin noise, and GLSL.

View Details

Learn how to create an organic distortion effect for text using JavaScript and CSS for a unique, fluid animation.

View Details

Get started with AI agents in this practical tutorial and learn to build an AI-powered newsletter aggregator using React and KaibanJS.

View Details

Learn how to create an interactive shader background effect using React Three Fiber and Drei in four simple steps.

View Details

Learn how to create a full 3D hand controller with depth, using @mediapipe/hands and Three.js.

View Details

Learn how to create a custom jitter shader in React-Three-Fiber, inspired by the visual style of PS1-era games, to add a retro aesthetic to 3D models.

View Details

Learn how to apply a pixel/grid displacement to a texture in Three.js using shaders and GPGPU with a subtle RGB shift effect on cursor move.

View Details

A simple yet powerful approach to Web Component server-rendering, declarative behaviors, and JavaScript islands.

View Details

Build a minimal 3D web application to showcase designs on a laptop and phone in an interactive environment using Three.js and threepipe.

View Details

Learn how to create distortion and grain effects on scroll using Three.js shaders, syncing HTML images with WebGL.

View Details

An introduction to Raymarching using the power of Signed Distance Fields (SDFs) and simple lighting to create a liquid shape effect.

View Details

Learn how to create an animated, displaced sphere using custom shaders with Three.js and React Three Fiber.

View Details

Learn how to create an interesting progressive blur effect using WebGL, OGL, and GLSL shaders.

View Details

Learn how to create a custom tool for printing Riso posters using Three.js.

View Details

Learn how to create a glowing SVG text animation with a marquee effect using only HTML and CSS.

View Details

An introduction on harnessing the power of Signed Distance Fields (SDFs) to draw shapes in WebGL and create interactive effects, such as lens blur.

View Details

A quick tutorial on how to create a beautiful distorted text ring effect in React Three Fiber.

View Details

Learn how to use WebGPU compute shaders to create reaction-diffusion patterns.

View Details

Exploring how to generate an engaging bulge effect on text using React Three Fiber.

View Details

A brief tutorial that guides you through creating a 3D visual effect using a 2D optical illusion with CSS and JavaScript.

View Details

Learn how to effortlessly implement smooth CSS view-transitions with Velvette, a useful library designed to tackle common challenges and enhance user experiences in web applications.

View Details

With just CSS you can add scroll animations that toggle shadows on navbars, reveal images, add scrollytelling, link up carousel elements and much more.

View Details

Learn how to build a particles music visualizer in Three.js, with techniques covering audio synchronization and 3D visual effects, inspired by ARKx's work for Coala Music's website.

View Details

Explore the fundamentals of Three.js Instancing in this tutorial, learning how to optimize GPU performance and efficiently render large numbers of objects.

View Details

Learn how to create a responsive WebGL layout powered by CSS and React Three Fiber.

View Details

A beginner-friendly guide that walks you through the use of the Browser View Transitions API with Astro for a smoother navigation experience.

View Details

Learn how to create a fun bulge effect in WebGL using the OGL library and shaders.

View Details

Learn how to code a similar hover animation to the one seen on the website of Quai Network.

View Details

A recreation of the hover effect seen on the customer grid on the Evervault website.

View Details

Learn how to create an interactive 3D carousel using WebGL, React Three Fiber, and GSAP with this step-by-step tutorial.

View Details

Learn how to code stunning animations with Theatre.js in this beginner-friendly tutorial.

View Details

In this detailed tutorial you will learn how to turn 3D models into voxel art with Three.js.

View Details

Learn how to code a vibrant Cyberpunk scene using Three.js, complete with post-processing and dynamic lighting, no shader expertise needed!

View Details

This tutorial will show you how to animate a camera flying through a 3D scene as the user scrolls using Theatre.js and React Three Fiber.

View Details

Learn how to craft audio reactive shaders with Three.js and Shaderpark.

View Details

In this tutorial, you'll learn how to recreate a captivating motion type effect using SVG and GreenSock.

View Details

This tutorial will guide you through the process of creating a 3D dice roller using Three.js and Cannon-es.

View Details

Infinite scrolling is a web design technique that allows users to scroll through a never-ending list of content by automatically loading new items as the user reaches the bottom of the page. Instead of having to click through to a new page to see more content, the content is automatically loaded and appended to the bottom of the page as the user scrolls. This can create a seamless and engaging browsing experience for users, as they can easily access a large amount of content without having to wait for new pages to load. Forget about reaching the footer, though!

Looping the scroll of a page refers to the process of automatically taking users back to the top of the page once they reach the end of the scroll. This means that they will be able to continuously scroll through the same content over and over again, rather than being presented with new content as they scroll down the page.

In this article, we will show some examples creative loop scrolling, and then reimplement the effect seem on Bureau DAM. We will be using Lenis by Studio Freight to implement the looping effect and GSAP for the animations.

What is loop scrolling?When your content is limited, you can do creative things with loop scrolling, which is basically infinite scrolling with repeating content. A great example for such a creative use is the effect seen on Bureau DAM:

Bureau DAM employs a fun looping scroll effectSome great ideas how looping the scroll can be used in a creative way and add value to a website:

  • It can create an immersive experience for users. By continuously scrolling through the same content, users can feel more immersed in the website and engaged with the content. This can be particularly useful for websites that want to create a strong emotional connection with their audience.
  • It can be used to create a game or interactive experience. By adding interactive elements to the content that is being looped, developers can create a game or interactive experience for users. For example, a website could use looping the scroll to create a scrolling game or to allow users to interact with the content in a novel way.
  • It can be used to create a “time loop” effect. By looping the scroll in a particular way, developers can create the illusion of a “time loop,” where the content appears to repeat itself in a continuous loop. This can be particularly effective for websites that want to convey a sense of continuity or timelessness.

A simple exampleLet’s create a simple example. We’ll set up a grid of 6 images that repeat on scroll. How can we achieve this? It’s simple: we need to repeat the content in a way that when reaching the end, we can simply reset the scroll to the top without anybody noticing! We’ve previously explored this concept in our CSS-only marquee effect. In another demo we also show how to play with this kind of infinite animation.

So, for our first example we have the following markup:

```

``` Our styles will create a 3×3 grid:

.grid {display: grid;grid-template-columns: repeat(3, 1fr);gap: 5vh;}.grid__item {height: 47.5vh; /* 50vh minus half of the gap */background-size: cover;background-position: 50% 20%;}.grid__item:nth-child(3n-2) {border-radius: 0 2rem 2rem 0;}.grid__item:nth-child(3n) {border-radius: 2rem 0 0 2rem;}.grid__item:nth-child(3n-1) {border-radius: 2rem;} Lenis comes with a handy option for making the scroll infinite. In our script we’ll make sure to repeat the visible grid items (in this case it’s 6):

const lenis = new Lenis({ smooth: true, infinite: true,});function raf(time) { lenis.raf(time); requestAnimationFrame(raf);}requestAnimationFrame(raf);// repeat first six items by cloning them and appending them to the .gridconst repeatItems = (parentEl, total = 0) => { const items = [...parentEl.children]; for (let i = 0; i <= total-1; ++i) { var cln = items[i].cloneNode(true); parentEl.appendChild(cln); }};repeatItems(document.querySelector('.grid'), 6); The result is a grid that repeats on scroll:

A simple example of loop scrolling. View the demoPlaying with animationsWhile we scroll, we can add some fancy animations to our grid items. Paired with switching the transform origin in the right time, we can get something playful like this:

Scale animation while scrolling. View the demoWe can also play with a 3D animation and an additional filter effect:

Adding a filter effect and perspective. View the demoThe Bureau DAM exampleNow, let’s see how we can remake the Bureau DAM animation. As we don’t have a grid here, things get a bit simpler. Just like them, we’ll use an SVG for the typography element, as we want to stretch it over the screen:

```

An infinite scrolling demo based on Bureau DAM

``` In our CSS we set a couple of styles that will make sure the items are stretched to full screen:

.grid { display: flex; flex-direction: column; gap: 5vh;}.grid__item { height: 100vh; place-items: center; display: grid;}.grid__item-inner {display: grid;gap: 1rem;place-items: center;text-align: center;}.grid__item--stack {display: grid;gap: 2rem;grid-template-rows: 1fr auto;}.grid__item-logo {padding: 8rem 1rem 0;}.grid__item-img {background-size: cover; background-position: 50% 50%;height: 70vh;aspect-ratio: 1.5;}.grid__item-text {margin: 0;} A proper setting for the transform origins ensures that we get the right visual effect:

gsap.registerPlugin(ScrollTrigger);// repeat first three items by cloning them and appending them to the .gridconst repeatItems = (parentEl, total = 0) => { const items = [...parentEl.children]; for (let i = 0; i <= total-1; ++i) { var cln = items[i].cloneNode(true); parentEl.appendChild(cln); }};const lenis = new Lenis({ smooth: true, infinite: true});function raf(time) { lenis.raf(time); requestAnimationFrame(raf);}imagesLoaded( document.querySelectorAll('.grid__item'), { background: true }, () => { document.body.classList.remove('loading'); repeatItems(document.querySelector('.grid'), 1); const items = [...document.querySelectorAll('.grid__item')]; // first item const firtsItem = items[0]; gsap.set(firtsItem, {transformOrigin: '50% 100%'}) gsap.to(firtsItem, { ease: 'none', startAt: {scaleY: 1}, scaleY: 0, scrollTrigger: { trigger: firtsItem, start: 'center center', end: 'bottom top', scrub: true, fastScrollEnd: true, onLeave: () => { gsap.set(firtsItem, {scaleY: 1,}) }, } }); // last item const lastItem = items[2]; gsap.set(lastItem, {transformOrigin: '50% 0%', scaleY: 0}) gsap.to(lastItem, { ease: 'none', startAt: {scaleY: 0}, scaleY: 1, scrollTrigger: { trigger: lastItem, start: 'top bottom', end: 'bottom top', scrub: true, fastScrollEnd: true, onLeaveBack: () => { gsap.set(lastItem, {scaleY: 1}) } } }); // in between let ft; let st; const middleItem = items[1]; ft = gsap.timeline() .to(middleItem, { ease: 'none', onStart: () => { if (st) st.kill() }, startAt: {scale: 0}, scale: 1, scrollTrigger: { trigger: middleItem, start: 'top bottom', end: 'center center', scrub: true, onEnter: () => gsap.set(middleItem, {transformOrigin: '50% 0%'}), onEnterBack: () => gsap.set(middleItem, {transformOrigin: '50% 0%'}), onLeave: () => gsap.set(middleItem, {transformOrigin: '50% 100%'}), onLeaveBack: () => gsap.set(middleItem, {transformOrigin: '50% 100%'}), }, }); st = gsap.timeline() .to(middleItem, { ease: 'none', onStart: () => { if (ft) ft.kill() }, startAt: {scale: 1}, scale: 0, scrollTrigger: { trigger: middleItem, start: 'center center', end: 'bottom top', scrub: true, onEnter: () => gsap.set(middleItem, {transformOrigin: '50% 100%'}), onEnterBack: () => gsap.set(middleItem, {transformOrigin: '50% 100%'}), onLeave: () => gsap.set(middleItem, {transformOrigin: '50% 0%'}), onLeaveBack: () => gsap.set(middleItem, {transformOrigin: '50% 0%'}), }, }); requestAnimationFrame(raf); const refresh = () => { ScrollTrigger.clearScrollMemory(); window.history.scrollRestoration = 'manual'; ScrollTrigger.refresh(true); } refresh(); window.addEventListener('resize', refresh);}); The result is a squashy and squeezy sequence of joy:

Check out the demoAnd that’s it! Hope you had some fun and that you got some inspiration for your next projects.

View Details

Today we’ll walk through the creation of a 3D packaging box that folds and unfolds on scroll. We’ll be using Three.js and GSAP for this.

We won’t use any textures or shaders to set it up. Instead, we’ll discover some ways to manipulate the Three.js BufferGeometry.

This is what we will be creating:

Scroll-driven animationWe’ll be using GSAP ScrollTrigger, a handy plugin for scroll-driven animations. It’s a great tool with a good documentation and an active community so I’ll only touch the basics here.

Let’s set up a minimal example. The HTML page contains:

  1. a full-screen <canvas> element with some styles that will make it cover the browser window
  2. a <div class=”page”> element behind the <canvas>. The .page element a larger height than the window so we have a scrollable element to track.

On the <canvas> we render a 3D scene with a box element that rotates on scroll.

To rotate the box, we use the GSAP timeline which allows an intuitive way to describe the transition of the box.rotation.x property.

gsap.timeline({}) .to(box.rotation, { duration: 1, // <- takes 1 second to complete x: .5 * Math.PI, ease: 'power1.out' }, 0) // <- starts at zero second (immediately) The x value of the box.rotation is changing from 0 (or any other value that was set before defining the timeline) to 90 degrees. The transition starts immediately. It has a duration of one second and power1.out easing so the rotation slows down at the end.

Once we add the scrollTrigger to the timeline, we start tracking the scroll position of the .page element (see properties trigger, start, end). Setting the scrub property to true makes the transition not only start on scroll but actually binds the transition progress to the scroll progress.

gsap.timeline({ scrollTrigger: { trigger: '.page', start: '0% 0%', end: '100% 100%', scrub: true, markers: true // to debug start and end properties },}) .to(box.rotation, { duration: 1, x: .5 * Math.PI, ease: 'power1.out' }, 0) Now box.rotation.x is calculated as a function of the scroll progress, not as a function of time. But the easing and timing parameters still matter. Power1.out easing still makes the rotation slower at the end (check out ease visualiser tool and try other options to see the difference). Start and duration values don’t mean seconds anymore but they still define the sequence of the transitions within the timeline.

For example, in the following timeline the last transition is finished at 2.3 + 0.7 = 3.

gsap.timeline({ scrollTrigger: { // ... },}) .to(box.rotation, { duration: 1, x: .5 * Math.PI, ease: 'power1.out' }, 0) .to(box.rotation, { duration: 0.5, x: 0, ease: 'power2.inOut' }, 1) .to(box.rotation, { duration: 0.7, // <- duration of the last transition x: - Math.PI, ease: 'none' }, 2.3) // <- start of the last transition We take the total duration of the animation as 3. Considering that, the first rotation starts once the scroll starts and takes ⅓ of the page height to complete. The second rotation starts without any delay and ends right in the middle of the scroll (1.5 of 3). The last rotation starts after a delay and ends when we scroll to the end of the page. That’s how we can construct the sequences of transitions bound to the scroll.

To get further with this tutorial, we don’t need more than some basic understanding of GSAP timing and easing. Let me just mention a few tips about the usage of GSAP ScrollTrigger, specifically for a Three.js scene.

Tip #1: Separating 3D scene and scroll animationI found it useful to introduce an additional variable params = { angle: 0 } to hold animated parameters. Instead of directly changing rotation.x in the timeline, we animate the properties of the “proxy” object, and then use it for the 3D scene (see the updateSceneOnScroll() function under tip #2). This way, we keep scroll-related stuff separate from 3D code. Plus, it makes it easier to use the same animated parameter for multiple 3D transforms; more about that a bit further on.

Tip #2: Render scene only when needed Maybe the most common way to render a Three.js scene is calling the render function within the window.requestAnimationFrame() loop. It’s good to remember that we don’t need it, if the scene is static except for the GSAP animation. Instead, the line renderer.render(scene, camera) can be simply added to to the onUpdate callback so the scene is redrawing only when needed, during the transition.

// No need to render the scene all the time// function animate() {// requestAnimationFrame(animate);// // update objects(s) transforms here// renderer.render(scene, camera);// }let params = { angle: 0 }; // <- "proxy" object// Three.js functionsfunction updateSceneOnScroll() { box.rotation.x = angle.v; renderer.render(scene, camera);}// GSAP functionsfunction createScrollAnimation() { gsap.timeline({ scrollTrigger: { // ... onUpdate: updateSceneOnScroll }, }) .to(angle, { duration: 1, v: .5 * Math.PI, ease: 'power1.out' })} Tip #3: Three.js methods to use with onUpdate callback Various properties of Three.js objects (.quaternion, .position, .scale, etc) can be animated with GSAP in the same way as we did for rotation. But not all the Three.js methods would work.

Some of them are aimed to assign the value to the property (.setRotationFromAxisAngle(), .setRotationFromQuaternion(), .applyMatrix4(), etc.) which works perfectly for GSAP timelines.

But other methods add the value to the property. For example, .rotateX(.1) would increase the rotation by 0.1 radians every time it’s called. So in case box.rotateX(angle.v) is placed to the onUpdate callback, the angle value will be added to the box rotation every frame and the 3D box will get a bit crazy on scroll. Same with .rotateOnAxis, .translateX, .translateY and other similar methods – they work for animations in the window.requestAnimationFrame() loop but not as much for today’s GSAP setup.

View the minimal scroll sandbox here.Note: This Three.js scene and other demos below contain some additional elements like axes lines and titles. They have no effect on the scroll animation and can be excluded from the code easily. Feel free to remove the addAxesAndOrbitControls() function, everything related to axisTitles and orbits, and <div> classed ui-controls to get a truly minimal setup.

Now that we know how to rotate the 3D object on scroll, let’s see how to create the package box.

Box structureThe box is composed of 4 x 3 = 12 meshes:

We want to control the position and rotation of those meshes to define the following:

  • unfolded state
  • folded state
  • closed state

For starters, let’s say our box doesn’t have flaps so all we have is two width-sides and two length-sides. The Three.js scene with 4 planes would look like this:

let box = { params: { width: 27, length: 80, depth: 45 }, els: { group: new THREE.Group(), backHalf: { width: new THREE.Mesh(), length: new THREE.Mesh(), }, frontHalf: { width: new THREE.Mesh(), length: new THREE.Mesh(), } }};scene.add(box.els.group);setGeometryHierarchy();createBoxElements();function setGeometryHierarchy() { // for now, the box is a group with 4 child meshes box.els.group.add(box.els.frontHalf.width, box.els.frontHalf.length, box.els.backHalf.width, box.els.backHalf.length);}function createBoxElements() { for (let halfIdx = 0; halfIdx < 2; halfIdx++) { for (let sideIdx = 0; sideIdx < 2; sideIdx++) { const half = halfIdx ? 'frontHalf' : 'backHalf'; const side = sideIdx ? 'width' : 'length'; const sideWidth = side === 'width' ? box.params.width : box.params.length; box.els[half][side].geometry = new THREE.PlaneGeometry( sideWidth, box.params.depth ); } }} All 4 sides are by default centered in the (0, 0, 0) point and lying in the XY-plane:

Folding animationTo define the unfolded state, it’s sufficient to:

  • move panels along X-axis aside from center so they don’t overlap

Transforming it to the folded state means

  • rotating width-sides to 90 deg around Y-axis
  • moving length-sides to the opposite directions along Z-axis
  • moving length-sides along X-axis to keep the box centered

Aside of box.params.width, box.params.length and box.params.depth, the only parameter needed to define these states is the opening angle. So the box.animated.openingAngle parameter is added to be animated on scroll from 0 to 90 degrees.

let box = { params: { // ... }, els: { // ... }, animated: { openingAngle: 0 }};function createFoldingAnimation() { gsap.timeline({ scrollTrigger: { trigger: '.page', start: '0% 0%', end: '100% 100%', scrub: true, }, onUpdate: updatePanelsTransform }) .to(box.animated, { duration: 1, openingAngle: .5 * Math.PI, ease: 'power1.inOut' })} Using box.animated.openingAngle, the position and rotation of sides can be calculated

function updatePanelsTransform() { // place width-sides aside of length-sides (not animated) box.els.frontHalf.width.position.x = .5 * box.params.length; box.els.backHalf.width.position.x = -.5 * box.params.length; // rotate width-sides from 0 to 90 deg box.els.frontHalf.width.rotation.y = box.animated.openingAngle; box.els.backHalf.width.rotation.y = box.animated.openingAngle; // move length-sides to keep the closed box centered const cos = Math.cos(box.animated.openingAngle); // animates from 1 to 0 box.els.frontHalf.length.position.x = -.5 * cos * box.params.width; box.els.backHalf.length.position.x = .5 * cos * box.params.width; // move length-sides to define box inner space const sin = Math.sin(box.animated.openingAngle); // animates from 0 to 1 box.els.frontHalf.length.position.z = .5 * sin * box.params.width; box.els.backHalf.length.position.z = -.5 * sin * box.params.width;} View the sandbox here.Nice! Let’s think about the flaps. We want them to move together with the sides and then to rotate around their own edge to close the box.

To move the flaps together with the sides we simply add them as the children of the side meshes. This way, flaps inherit all the transforms we apply to the sides. An additional position.y transition will place them on top or bottom of the side panel.

let box = { params: { // ... }, els: { group: new THREE.Group(), backHalf: { width: { top: new THREE.Mesh(), side: new THREE.Mesh(), bottom: new THREE.Mesh(), }, length: { top: new THREE.Mesh(), side: new THREE.Mesh(), bottom: new THREE.Mesh(), }, }, frontHalf: { width: { top: new THREE.Mesh(), side: new THREE.Mesh(), bottom: new THREE.Mesh(), }, length: { top: new THREE.Mesh(), side: new THREE.Mesh(), bottom: new THREE.Mesh(), }, } }, animated: { openingAngle: .02 * Math.PI }};scene.add(box.els.group);setGeometryHierarchy();createBoxElements();function setGeometryHierarchy() { // as before box.els.group.add(box.els.frontHalf.width.side, box.els.frontHalf.length.side, box.els.backHalf.width.side, box.els.backHalf.length.side); // add flaps box.els.frontHalf.width.side.add(box.els.frontHalf.width.top, box.els.frontHalf.width.bottom); box.els.frontHalf.length.side.add(box.els.frontHalf.length.top, box.els.frontHalf.length.bottom); box.els.backHalf.width.side.add(box.els.backHalf.width.top, box.els.backHalf.width.bottom); box.els.backHalf.length.side.add(box.els.backHalf.length.top, box.els.backHalf.length.bottom);}function createBoxElements() { for (let halfIdx = 0; halfIdx < 2; halfIdx++) { for (let sideIdx = 0; sideIdx < 2; sideIdx++) { // ... const flapWidth = sideWidth - 2 * box.params.flapGap; const flapHeight = .5 * box.params.width - .75 * box.params.flapGap; // ... const flapPlaneGeometry = new THREE.PlaneGeometry( flapWidth, flapHeight ); box.els[half][side].top.geometry = flapPlaneGeometry; box.els[half][side].bottom.geometry = flapPlaneGeometry; box.els[half][side].top.position.y = .5 * box.params.depth + .5 * flapHeight; box.els[half][side].bottom.position.y = -.5 * box.params.depth -.5 * flapHeight; } }} The flaps rotation is a bit more tricky.

Changing the pivot point of Three.js meshLet’s get back to the first example with a Three.js object rotating around the X axis.

There’re many ways to set the rotation of a 3D object: Euler angle, quaternion, lookAt() function, transform matrices and so on. Regardless of the way angle and axis of rotation are set, the pivot point (transform origin) will be at the center of the mesh.

Say we animate rotation.x for the 4 boxes that are placed around the scene:

const boxGeometry = new THREE.BoxGeometry(boxSize[0], boxSize[1], boxSize[2]);const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);const numberOfBoxes = 4;for (let i = 0; i < numberOfBoxes; i++) { boxes[i] = boxMesh.clone(); boxes[i].position.x = (i - .5 * numberOfBoxes) * (boxSize[0] + 2); scene.add(boxes[i]);}boxes[1].position.y = .5 * boxSize[1];boxes[2].rotation.y = .5 * Math.PI;boxes[3].position.y = - boxSize[1]; See the sandbox here.For them to rotate around the bottom edge, we need to move the pivot point to -.5 x box size. There are couple of ways to do this:

  • wrap mesh with additional Object3D
  • transform geometry of mesh
  • assign pivot point with additional transform matrix
  • could be some other tricks

If you’re curious why Three.js doesn’t provide origin positioning as a native method, check out this discussion.

Option #1: Wrapping mesh with additional Object3DFor the first option, we add the original box mesh as a child of new Object3D. We treat the parent object as a box so we apply transforms (rotation.x) to it, exactly as before. But we also translate the mesh to half of its size. The mesh moves up in the local space but the origin of the parent object stays in the same point.

const boxGeometry = new THREE.BoxGeometry(boxSize[0], boxSize[1], boxSize[2]);const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);const numberOfBoxes = 4;for (let i = 0; i < numberOfBoxes; i++) { boxes[i] = new THREE.Object3D(); const mesh = boxMesh.clone(); mesh.position.y = .5 * boxSize[1]; boxes[i].add(mesh); boxes[i].position.x = (i - .5 * numberOfBoxes) * (boxSize[0] + 2); scene.add(boxes[i]);}boxes[1].position.y = .5 * boxSize[1];boxes[2].rotation.y = .5 * Math.PI;boxes[3].position.y = - boxSize[1]; See the sandbox here.Option #2: Translating the geometry of MeshWith the second option, we move up the geometry of the mesh. In Three.js, we can apply a transform not only to the objects but also to their geometry.

const boxGeometry = new THREE.BoxGeometry(boxSize[0], boxSize[1], boxSize[2]);boxGeometry.translate(0, .5 * boxSize[1], 0);const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);const numberOfBoxes = 4;for (let i = 0; i < numberOfBoxes; i++) { boxes[i] = boxMesh.clone(); boxes[i].position.x = (i - .5 * numberOfBoxes) * (boxSize[0] + 2); scene.add(boxes[i]);}boxes[1].position.y = .5 * boxSize[1];boxes[2].rotation.y = .5 * Math.PI;boxes[3].position.y = - boxSize[1]; See the sandbox here.The idea and result are the same: we move the mesh up ½ of its height but the origin point is staying at the same coordinates. That’s why rotation.x transform makes the box rotate around its bottom side.

*Option #3:* Assign pivot point with additional transform matrixI find this way less suitable for today’s project but the idea behind it is pretty simple. We take both, pivot point position and desired transform as matrixes. Instead of simply applying the desired transform to the box, we apply the inverted pivot point position first, then do rotation.x as the box is centered at the moment, and then apply the point position.

object.matrix = inverse(pivot.matrix) * someTranformationMatrix * pivot.matrix You can find a nice implementation of this method here.

I’m using geometry translation (option #2) to move the origin of the flaps. Before getting back to the box, let’s see what we can achieve if the very same rotating boxes are added to the scene in hierarchical order and placed one on top of another.

const boxGeometry = new THREE.BoxGeometry(boxSize[0], boxSize[1], boxSize[2]);boxGeometry.translate(0, .5 * boxSize[1], 0);const boxMesh = new THREE.Mesh(boxGeometry, boxMaterial);const numberOfBoxes = 4;for (let i = 0; i < numberOfBoxes; i++) { boxes[i] = boxMesh.clone(); if (i === 0) { scene.add(boxes[i]); } else { boxes[i - 1].add(boxes[i]); boxes[i].position.y = boxSize[1]; }} We still animate rotation.x of each box from 0 to 90 degrees, so the first mesh rotates to 90 degrees, the second one does the same 90 degrees plus its own 90 degrees rotation, the third does 90+90+90 degrees, etc.

See the sandbox here.A very easy and quite useful trick.

Animating the flapsBack to the flaps. Flaps are made from translated geometry and added to the scene as children of the side meshes. We set their position.y property once and animate their rotation.x property on scroll.

function setGeometryHierarchy() { box.els.group.add(box.els.frontHalf.width.side, box.els.frontHalf.length.side, box.els.backHalf.width.side, box.els.backHalf.length.side); box.els.frontHalf.width.side.add(box.els.frontHalf.width.top, box.els.frontHalf.width.bottom); box.els.frontHalf.length.side.add(box.els.frontHalf.length.top, box.els.frontHalf.length.bottom); box.els.backHalf.width.side.add(box.els.backHalf.width.top, box.els.backHalf.width.bottom); box.els.backHalf.length.side.add(box.els.backHalf.length.top, box.els.backHalf.length.bottom);}function createBoxElements() { for (let halfIdx = 0; halfIdx < 2; halfIdx++) { for (let sideIdx = 0; sideIdx < 2; sideIdx++) { // ... const topGeometry = flapPlaneGeometry.clone(); topGeometry.translate(0, .5 * flapHeight, 0); const bottomGeometry = flapPlaneGeometry.clone(); bottomGeometry.translate(0, -.5 * flapHeight, 0); box.els[half][side].top.position.y = .5 * box.params.depth; box.els[half][side].bottom.position.y = -.5 * box.params.depth; } }} The animation of each flap has an individual timing and easing within the gsap.timeline so we store the flap angles separately.

let box = { // ... animated: { openingAngle: .02 * Math.PI, flapAngles: { backHalf: { width: { top: 0, bottom: 0 }, length: { top: 0, bottom: 0 }, }, frontHalf: { width: { top: 0, bottom: 0 }, length: { top: 0, bottom: 0 }, } } }}function createFoldingAnimation() { gsap.timeline({ scrollTrigger: { // ... }, onUpdate: updatePanelsTransform }) .to(box.animated, { duration: 1, openingAngle: .5 * Math.PI, ease: 'power1.inOut' }) .to([ box.animated.flapAngles.backHalf.width, box.animated.flapAngles.frontHalf.width ], { duration: .6, bottom: .6 * Math.PI, ease: 'back.in(3)' }, .9) .to(box.animated.flapAngles.backHalf.length, { duration: .7, bottom: .5 * Math.PI, ease: 'back.in(2)' }, 1.1) .to(box.animated.flapAngles.frontHalf.length, { duration: .8, bottom: .49 * Math.PI, ease: 'back.in(3)' }, 1.4) .to([box.animated.flapAngles.backHalf.width, box.animated.flapAngles.frontHalf.width], { duration: .6, top: .6 * Math.PI, ease: 'back.in(3)' }, 1.4) .to(box.animated.flapAngles.backHalf.length, { duration: .7, top: .5 * Math.PI, ease: 'back.in(3)' }, 1.7) .to(box.animated.flapAngles.frontHalf.length, { duration: .9, top: .49 * Math.PI, ease: 'back.in(4)' }, 1.8)}function updatePanelsTransform() { // ... folding / unfolding box.els.frontHalf.width.top.rotation.x = -box.animated.flapAngles.frontHalf.width.top; box.els.frontHalf.length.top.rotation.x = -box.animated.flapAngles.frontHalf.length.top; box.els.frontHalf.width.bottom.rotation.x = box.animated.flapAngles.frontHalf.width.bottom; box.els.frontHalf.length.bottom.rotation.x = box.animated.flapAngles.frontHalf.length.bottom; box.els.backHalf.width.top.rotation.x = box.animated.flapAngles.backHalf.width.top; box.els.backHalf.length.top.rotation.x = box.animated.flapAngles.backHalf.length.top; box.els.backHalf.width.bottom.rotation.x = -box.animated.flapAngles.backHalf.width.bottom; box.els.backHalf.length.bottom.rotation.x = -box.animated.flapAngles.backHalf.length.bottom;} See the sandbox here.With all this, we finish the animation part! Let’s now work on the look of our box.

Lights and colors This part is as simple as replacing multi-color wireframes with a single color MeshStandardMaterial and adding a few lights.

const ambientLight = new THREE.AmbientLight(0xffffff, .5);scene.add(ambientLight);lightHolder = new THREE.Group();const topLight = new THREE.PointLight(0xffffff, .5);topLight.position.set(-30, 300, 0);lightHolder.add(topLight);const sideLight = new THREE.PointLight(0xffffff, .7);sideLight.position.set(50, 0, 150);lightHolder.add(sideLight);scene.add(lightHolder);const material = new THREE.MeshStandardMaterial({ color: new THREE.Color(0x9C8D7B), side: THREE.DoubleSide});box.els.group.traverse(c => { if (c.isMesh) c.material = material;}); Tip: Object rotation effect with OrbitControlsOrbitControls make the camera orbit around the central point (left preview). To demonstrate a 3D object, it’s better to give users a feeling that they rotate the object itself, not the camera around it (right preview). To do so, we keep the lights position static relative to camera.

It can be done by wrapping lights in an additional lightHolder object. The pivot point of the parent object is (0, 0, 0). We also know that the camera rotates around (0, 0, 0). It means we can simply apply the camera’s rotation to the lightHolder to keep the lights static relative to the camera.

function render() { // ... lightHolder.quaternion.copy(camera.quaternion); renderer.render(scene, camera);} See the sandbox here.Layered panelsSo far, our sides and flaps were done as a simple PlaneGeomery. Let’s replace it with “real” corrugated cardboard material ‐ two covers and a fluted layer between them.

First step is replacing a single plane with 3 planes merged into one. To do so, we need to place 3 clones of PlaneGeometry one behind another and translate the front and back levels along the Z axis by half of the total cardboard thickness.

There’re many ways to move the layers, starting from the geometry.translate(0, 0, .5 * thickness) method we used to change the pivot point. But considering other transforms we’re about to apply to the cardboard geometry, we better go through the geometry.attributes.position array and add the offset to the z-coordinates directly:

fconst baseGeometry = new THREE.PlaneGeometry( params.width, params.height,);const geometriesToMerge = [ getLayerGeometry(- .5 * params.thickness), getLayerGeometry(0), getLayerGeometry(.5 * params.thickness)];function getLayerGeometry(offset) { const layerGeometry = baseGeometry.clone(); const positionAttr = layerGeometry.attributes.position; for (let i = 0; i < positionAttr.count; i++) { const x = positionAttr.getX(i); const y = positionAttr.getY(i) const z = positionAttr.getZ(i) + offset; positionAttr.setXYZ(i, x, y, z); } return layerGeometry;} For merging the geometries we use the mergeBufferGeometries method. It’s pretty straightforward, just don’t forget to import the BufferGeometryUtils module into your project.

See the sandbox here.Wavy fluteTo turn a mid layer into the flute, we apply the sine wave to the plane. In fact, it’s the same z-coordinate offset, just calculated as Sine function of the x-attribute instead of a constant value.

function getLayerGeometry() { const baseGeometry = new THREE.PlaneGeometry( params.width, params.height, params.widthSegments, 1 ); const offset = (v) => .5 * params.thickness * Math.sin(params.fluteFreq * v); const layerGeometry = baseGeometry.clone(); const positionAttr = layerGeometry.attributes.position; for (let i = 0; i < positionAttr.count; i++) { const x = positionAttr.getX(i); const y = positionAttr.getY(i) const z = positionAttr.getZ(i) + offset(x); positionAttr.setXYZ(i, x, y, z); } layerGeometry.computeVertexNormals(); return layerGeometry;} The z-offset is not the only change we need here. By default, PlaneGeometry is constructed from two triangles. As it has only one width segment and one height segment, there’re only corner vertices. To apply the sine(x) wave, we need enough vertices along the x axis – enough resolution, you can say.

Also, don’t forget to update the normals after changing the geometry. It doesn’t happen automatically.

See the sandbox here.I apply the wave with an amplitude equal to the cardboard thickness to the middle layer, and the same wave with a little amplitude to the front and back layers, just to give some texture to the box.

The surfaces and cuts look pretty cool. But we don’t want to see the wavy layer on the folding lines. At the same time, I want those lines to be visible before the folding happens:

To achieve this, we can “press” the cardboard on the selected edges of each panel.

We can do so by applying another modifier to the z-coordinate. This time it’s a power function of the x or y attribute (depending on the side we’re “pressing”).

function getLayerGeometry() { const baseGeometry = new THREE.PlaneGeometry( params.width, params.height, params.widthSegments, params.heightSegments // to apply folding we need sufficient number of segments on each side ); const offset = (v) => .5 * params.thickness * Math.sin(params.fluteFreq * v); const layerGeometry = baseGeometry.clone(); const positionAttr = layerGeometry.attributes.position; for (let i = 0; i < positionAttr.count; i++) { const x = positionAttr.getX(i); const y = positionAttr.getY(i) let z = positionAttr.getZ(i) + offset(x); // add wave z = applyFolds(x, y, z); // add folds positionAttr.setXYZ(i, x, y, z); } layerGeometry.computeVertexNormals(); return layerGeometry;}function applyFolds(x, y, z) { const folds = [ params.topFold, params.rightFold, params.bottomFold, params.leftFold ]; const size = [ params.width, params.height ]; let modifier = (c, size) => (1. - Math.pow(c / (.5 * size), params.foldingPow)); // top edge: Z -> 0 when y -> plane height, // bottom edge: Z -> 0 when y -> 0, // right edge: Z -> 0 when x -> plane width, // left edge: Z -> 0 when x -> 0 if ((x > 0 && folds[1]) || (x < 0 && folds[3])) { z *= modifier(x, size[0]); } if ((y > 0 && folds[0]) || (y < 0 && folds[2])) { z *= modifier(y, size[1]); } return z;} See the sandbox here.The folding modifier is applied to all 4 edges of the box sides, to the bottom edges of the top flaps, and to the top edges of bottom flaps.

With this the box itself is finished.

There is room for optimization, and for some extra features, of course. For example, we can easily remove the flute level from the side panels as it’s never visible anyway. Let me also quickly describe how to add zooming buttons and a side image to our gorgeous box.

ZoomingThe default behaviour of OrbitControls is zooming the scene by scroll. It means that our scroll-driven animation is in conflict with it, so we set orbit.enableZoom property to false.

We still can have zooming on the scene by changing the camera.zoom property. We can use the same GSAP animation as before, just note that animating the camera’s property doesn’t automatically update the camera’s projection. According to the documentation, updateProjectionMatrix() must be called after any change of the camera parameters so we have to call it on every frame of the transition:

// ...// changing the zoomLevel variable with buttonsgsap.to(camera, { duration: .2, zoom: zoomLevel, onUpdate: () => { camera.updateProjectionMatrix(); }}) Side imageThe image, or even a clickable link, can be added on the box side. It can be done with an additional plane mesh with a texture on it. It should be just moving together with the selected side of the box:

function updatePanelsTransform() { // ... // for copyright mesh to be placed on the front length side of the box copyright.position.copy(box.els.frontHalf.length.side.position); copyright.position.x += .5 * box.params.length - .5 * box.params.copyrightSize[0]; copyright.position.y -= .5 * (box.params.depth - box.params.copyrightSize[1]); copyright.position.z += box.params.thickness;} As for the texture, we can import an image/video file, or use a canvas element we create programmatically. In the final demo I use a canvas with a transparent background, and two lines of text with an underline. Turning the canvas into a Three.js texture makes me able to map it on the plane:

function createCopyright() { // create canvas const canvas = document.createElement('canvas'); canvas.width = box.params.copyrightSize[0] * 10; canvas.height = box.params.copyrightSize[1] * 10; const planeGeometry = new THREE.PlaneGeometry(box.params.copyrightSize[0], box.params.copyrightSize[1]); const ctx = canvas.getContext('2d'); ctx.clearRect(0, 0, canvas.width, canvas.width); ctx.fillStyle = '#000000'; ctx.font = '22px sans-serif'; ctx.textAlign = 'end'; ctx.fillText('ksenia-k.com', canvas.width - 30, 30); ctx.fillText('codepen.io/ksenia-k', canvas.width - 30, 70); ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(canvas.width - 160, 35); ctx.lineTo(canvas.width - 30, 35); ctx.stroke(); ctx.beginPath(); ctx.moveTo(canvas.width - 228, 77); ctx.lineTo(canvas.width - 30, 77); ctx.stroke(); // create texture const texture = new THREE.CanvasTexture(canvas); // create mesh mapped with texture copyright = new THREE.Mesh(planeGeometry, new THREE.MeshBasicMaterial({ map: texture, transparent: true, opacity: .5 })); scene.add(copyright);} To make the text lines clickable, we do the following:

  • use Raycaster and mousemove event to track if the intersection between cursor ray and plane, change the cursor appearance if the mesh is hovered
  • if a click happened while the mesh is hovered, check the uv coordinate of intersection
  • if the uv coordinate is on the top half of the mesh (uv.y > .5) we open the first link, if uv coordinate is below .5, we go to the second link

The raycaster code is available in the full demo.

Thank you for scrolling this far!
Hope this tutorial can be useful for your Three.js projects ♡

View Details

A tutorial where you'll learn how to create a pencil effect with a sketchy look using Three.js post-processing.

View Details

In this tutorial we will go over implementing the Game of Life in Three.js using ping pong buffering and off-screen renderbuffers.

View Details

A detailed tutorial on how to create typable 3D text with Three.js.

View Details

Today we are looking under the hood of a page transition based on Vitalii Burhonskyi's Dribbble shot.

The post How to Create a Cover Page Transition appeared first on Codrops.

View Details

A short tutorial on how to animate SVG paths while smooth scrolling a page.

The post How to Animate SVG Shapes on Scroll appeared first on Codrops.

View Details

A look into the making of a mini-city full of post effects and micro-interactions using Three.js.

The post Case Study: Windland — An Immersive Three.js Experience appeared first on Codrops.

View Details

A tutorial that explores some hands-on changes of "The Aviator" game to make it more fun and engaging.

The post How to Add More Fun to a Game: Extending “The Aviator” appeared first on Codrops.

View Details

Learn how to build an interactive line graph using the D3 JavaScript library and CSS custom properties to create different color schemes.

The post Building an Interactive Sparkline Graph with D3 appeared first on Codrops.

View Details

Learn two ways of applying a creative grain effect to 3D elements in Three.js.

The post Creating a Risograph Grain Light Effect in Three.js appeared first on Codrops.

View Details

Learn how to create and use native web components with the Minze JavaScript framework.

The post Creating Native Web Components appeared first on Codrops.

View Details

Learn how to code creative animations using SVG paths and the getPointAtLength() function.

The post Animate Anything Along an SVG Path appeared first on Codrops.

View Details

Learn how to create a scroll based animation in WebGL with Three.js.

The post Crafting Scroll Based Animations in Three.js appeared first on Codrops.

View Details

Learn how to build a scrollable and draggable horizontal timeline using GSAP's ScrollTrigger and Draggable plugins.

The post Building a Scrollable and Draggable Timeline with GSAP appeared first on Codrops.

View Details

Learn how to enhance your Three.js scenes with postprocessing via framebuffers.

The post Adding a Persistence Effect to Three.js Scenes appeared first on Codrops.

View Details

Learn to create beautiful, inspiring, and unique color palettes/combinations, all from the comfort of your favorite text editor!

The post Coloring With Code — A Programmatic Approach To Design appeared first on Codrops.

View Details

In a recent release of Three.js (r129 and beyond) some fabulous new features to MeshPhysicalMaterial were merged. The new features allow us to create convincing transparent, glass-like and plastic-like materials that refract and diffuse the content behind them, and are as easy-to-use as adding a couple of material properties!

About this article This article explores some advanced properties of materials. While the results are very technically impressive, the new features that enable them are simple to use! Some experience with three and an intermediate understanding of the concept of “materials” in 3D graphics is ideal. Code examples are written for brevity, so it’s best to dive into the sandbox code (provided with each screenshot) if you’re interested in the gritty implementation details.

The physics of optics, light, reflection and refraction are not discussed in-depth here. This article approaches these effects through an aesthetic lens: aiming for convincing and visually pleasing results, even if they are not scientifically accurate.

Rather than introducing new concepts, this is primarily a walkthrough of features that exist within three and its MeshPhysicalMaterial class. I’d like to gush and shower praise upon the contributors and maintainers of three. It continues to be a core pillar of 3D in the browser. It has a vibrant community and extremely talented contributors who continue to push the boundaries of what’s possible on a humble web page.

Prior Art Creating transparent materials, especially with texture and diffusion, has for a long time required deep technical expertise and creative problem solving. Some projects have achieved an impressive and convincing effect in WebGL through bespoke techniques:

A screenshot from Make Me Pulse’s 2018 Wishes, showcasing frosted glass materials in WebGL The Jam3 FWA 100 project, showcasing glass-like orbs Jesper Vos published an incredible tutorial here on Codrops: Real-time Multiside Refraction in Three Steps, which includes some great insights into the science and simulation of refraction.

In addition, these excellent technical examples provided the inspiration for writing this article, and further exploring what’s possible with these new features.

Three.js three is an open-source javascript library for rendering 3D graphics in the browser. It provides a friendly API and abstractions that make working with WebGL more palatable and expressive. three has been around since 2010, is extremely well battle-tested, and is the de-facto standard for rendering 3D content on the internet. See the list of case studies on the home page, docs, examples, or source.

MeshPhysicalMaterial MeshPhysicalMaterial is a relatively recent Physically-Based Rendering (PBR) built-in material for three. It’s an evolution and extension of the already impressive MeshStandardMaterial, providing additional features to pump the photo-realism.

This visual fidelity comes at a cost, from the docs: As a result of these complex shading features, MeshPhysicalMaterial has a higher performance cost, per pixel, than other Three.js materials. Most effects are disabled by default, and add cost as they are enabled.

Beyond the properties offered in MeshStandardMaterial, it introduces some new ones:

Transmission transmission is the key to transparent glass-like and plastic-like effects. Traditionally when we adjust the opacity of an element to make it transparent, its visual presence is diluted as a whole. The object appears ghostly, uniformly transparent, and not realistic as a see-through object. In the real-world, transparent objects reflect light and show glare. They have a physical presence even though they may be perfectly clear.

Reflectivity properties MeshPhysicalMaterial includes some properties that estimate refraction through the transmissible object: thickness, ior (Index-of-refraction) and reflectivity. We’ll mostly ignore ior and reflectivity (which changes ior too, but is mapped to a 0-1 range) as the defaults work great!

thickness is the magic here, as we’ll see shortly.

Clearcoat Like a layer of lacquer, clearcoat provides an additional thin reflective layer on the surface of objects. Previously this would require a second version of the object, with a separate material, and with different parameters.

Other There are some other additional properties on MeshPhysicalMaterial like sheen and attenuationTint which I won’t be touching on in this article.

We can expect to see more and more features added to this material in future releases.


First steps First things first, let’s create a scene and pop something in it! We’ll start with an Icosahedron because hey, they just look cool.

I’m skipping the basic scene setup stuff here, I recommend diving into the sandbox source or three docs if you’re unfamiliar with this.

const geometry = new THREE.IcosahedronGeometry(1, 0); const material = new THREE.MeshNormalMaterial(); const mesh = new THREE.Mesh(geometry, material) scene.add(mesh);

View on CodeSandbox

Looks like an Icosahedron! Let’s apply our MeshPhysicalMaterial:

const material = new THREE.MeshPhysicalMaterial({ metalness: 0, roughness: 0 });

The options metalness and roughness are the two primary handles with PBR materials (they are on MeshStandardMaterial too). They can be used to set the stage for how our material responds to lighting and environment. Having both set at zero describes something like “A non-metallic object with a highly polished surface”.

View on CodeSandbox

Doesn’t look like much! Physically-based materials need light to reflect, so let’s add some light:

const light = new THREE.DirectionalLight(0xfff0dd, 1); light.position.set(0, 5, 10); scene.add(light);

View on CodeSandbox

Cool, there it is again… Now let’s make it transparent!

Call the Glazier The transmission option is responsible for applying our transparency. It makes the “fill” or “body” of the object transparent, while leaving all lighting and reflections on the surface in-tact.

Note that we’re not using the opacity option, which applies a uniform transparency to the material as a whole. We also don’t need to include the transparent option on the material for it to appear transparent through transmission.

const material = new THREE.MeshPhysicalMaterial({ roughness: 0, transmission: 1, // Add transparency });

View on CodeSandbox

I think that’s transparent, we can see the background colour through it. Let’s pop something else behind it to be sure. We’ll add a textured plane as our “backdrop”:

const bgTexture = new THREE.TextureLoader().load("src/texture.jpg"); const bgGeometry = new THREE.PlaneGeometry(5, 5); const bgMaterial = new THREE.MeshBasicMaterial({ map: bgTexture }); const bgMesh = new THREE.Mesh(bgGeometry, bgMaterial); bgMesh.position.set(0, 0, -1); scene.add(bgMesh);

View on CodeSandbox

It’s transparent! It’s lacking something though. There’s nothing but a tiny flicker of movement on the corners of our geometry; as if our material is made from the most delicate and fragile of super-thin, super-clear glass.

Now here’s the magic part!

const material = new THREE.MeshPhysicalMaterial({ roughness: 0, transmission: 1, thickness: 0.5, // Add refraction! });

View on CodeSandbox

By adding a single option: thickness to our material, we’ve now been given the gift of refraction through our object! Our background plane, which is a completely separate object, simply sitting behind our Icosahedron in the scene, now gets refracted.

This is incredible! Previous methods of achieving this required much more work and intense technical understanding. This has immediately democratised refractive materials in WebGL.

The effect is especially impressive when viewed in motion, and from an angle:

Swooping around our glass object to see it refracting the rest of the scene Have a play by dragging around in this sandbox:

Diverse objects While the sharp facets of our Icosahedron show a nice “cut-gem” style of refraction, we rarely see such precisely cut glass objects at any size other than tiny. This effect is greatly enhanced when geometries with smoother edges are used.
Let’s increase the detail level of our Icosahedron to form a sphere:

const geometry = new THREE.IcosahedronGeometry(1, 15);

View on CodeSandbox

This shows some optical distortion in addition to the refraction based on the shape of the geometry!


Hot tip: with all of the PolyhedronGeometry types in three, any detail level above zero is rendered as a sphere, rather than a faceted polyhedron as far as transmission is concerned.


You may notice that the distorted content is a little pixelated, this is due to the material upscaling what’s transmitted through it to perform the distortion. We can mitigate this a bit with some other effects which we’ll cover later.

Let’s explore adding some texture to our glass material:

const material = new THREE.MeshPhysicalMaterial({ roughness: 0.7, transmission: 1, thickness: 1 });

The roughness option on our transmissible material provides us with a “frosting” level, making light that passes through the material more diffuse.

View in CodeSandbox

This becomes immediately recognisable as a frosted glass object, with a fine powdery texture.

Notes on roughness: * The middle of the roughness range can display some quite noticeably pixelated transmitted content (at the time of writing). In my experience the best results are found in the low (0-0.15) and higher (0.65+) ends of the range. This can also be quite successfully mitigated with some of the things we’ll add shortly. * The distance of the transmissible object from the camera affects how roughness is rendered. It’s best to tweak the roughness parameter once you’ve established your scene


Hot tip: Using a small amount of roughness (0.05 – 0.15) can help soften aliasing on the transmitted content at the cost of a bit of sharpness.


For the rest of our examples we’ll include two additional geometries for reference: a RoundedBoxGeometry and a 3D model of a dragon (loaded as a GLTF, but only used for the geometry):

View on CodeSandbox

Through the lens While the transmission effect is already appealing, there’s so much more we can do to make this appear truer-to-life.

The next thing we’ll do is add an environment map. It’s recommended that you always include an envMap when using MeshPhysicalMaterial, as per the docs: For best results, always specify an environment map when using this material.

Highly reflective objects show reflections, and glare, and glimpses of their surrounding environment reflected off their surface. It’s unusual for a shiny object to be perfectly unreflective; as they have been in our examples so far.

We’ll use a high quality High Dynamic Range Image (HDRI) environment map. I’ve chosen this one for its bright fluorescent overhead lighting:

const hdrEquirect = new THREE.RGBELoader().load( "src/empty_warehouse_01_2k.hdr", () => { hdrEquirect.mapping = THREE.EquirectangularReflectionMapping; } ); const material = new THREE.MeshPhysicalMaterial({ ... envMap: hdrEquirect });

View on CodeSandbox

NICE! Now that looks more realistic. The objects glint and shimmer in our bright environment; much more like the lighting challenges faced by a photographer of shiny things.

This is where our rounded geometries really shine too. Their smoother curves and edges catch light differently, really amplifying the effect of a highly polished surface.


Hot tip: Adding an envMap texture of some sort helps to resolve some rendering artifacts of this material. This is why it’s always recommended to include one (beyond the fact that it looks great!).


If you adjust the roughness level upward, you’ll notice that the reflections are diffused by the rougher frosted texture of the surface; however, we may want an object that’s semi-transparent while still having a shiny surface.

The clearcoat options allow us to include an additional reflective layer on the surface of our object (think lacquered wood, powder coatings, or plastic films). In the case of our transparent objects, we can make them from semi-transparent glass or plastic which still has a polished and reflective surface.

View on CodeSandbox

Adjusting the clearcoatRoughness option adjusts how highly polished the surface is; visually spanning the range from highly-polished frosted glass through to semi-gloss and matte frosted plastics. This effect is pretty convincing! You can almost feel the tack and texture of these objects.

So far we’ve been exploring objects with perfectly smooth surfaces. To really bring some texture to them, we can add a normal map:

const textureLoader = new THREE.TextureLoader(); const normalMapTexture = textureLoader.load("src/normal.jpg"); normalMapTexture.wrapS = THREE.RepeatWrapping; normalMapTexture.wrapT = THREE.RepeatWrapping; const material = new THREE.MeshPhysicalMaterial({ ... normalMap: normalMapTexture, clearcoatNormalMap: normalMapTexture, });

View on CodeSandbox

The interplay between the normalMap and clearcoatNormalMap is interesting. By setting the normalMap we affect the transmission through the object, adding a textured frosting that refracts light differently. By setting the clearcoatNormalMap we affect the finish on the surface of the object.


Hot tip: The additional texture added by the normalMap greatly reduces the visible pixelation on the transmitted content, effectively solving this issue for us.


As a final touch, we’ll add a post-processing pass to apply bloom to our scene. Bloom adds that extra little bit of photographic appeal by simulating volumetric glare from the bright overhead lighting bathing our objects.

I’ll leave information around implementation post-processing within three to the docs and examples. In this sandbox I’ve included the UnrealBloomPass.

Bloom always looks good. There we have it! Convincingly transparent, textured and reflective 3D objects, rendered in real-time, and without much effort. This deserves to be celebrated, what an empowering experience it is working with MeshPhysicalMaterial.

Drippin ice Just for fun, let’s crank the dial on this by rendering many of our transparent objects using three‘s InstancedMesh (link).

View on CodeSandbox

OOOUF! YES.
Instances can’t be seen through each other, which is a general limitation of transmission on MeshPhysicalMaterial (current as of r133); but in my opinion the effect is still very cool.

Explore for yourself Finally, here’s our dragon model with a bunch of material options enabled in the GUI:

View on CodeSandbox

Have a play, check out metalness, play around with color to explore colourful tinted glass, tweak the ior to change our glass into crystal!

Sign-off I’ve really only scratched the surface of what can be achieved with MeshPhysicalMaterial. There are even more options available within this material, sheen, roughnessMap, transmissionMap, attenuationTint and all sorts of other things provide inroads to many more effects. Dig deep into the docs and source if you’re interested!

This is an enabler, given the creative vision for a transparent object you can use these tools to work towards a convincing result. Transparent materials in three are here, you can start using them in your projects today.

Attributions * Environment map: Empty Warehouse 01 HDRI by Sergej Majboroda, via Poly Haven * 3D model: Dragon GLB by Stanford University and Morgan McGuire’s Computer Graphics Archive, via KhronosGroup * Normal map: Packed Dirt normal by Dim, via opengameart.org * Sandboxes: Hosted on CodeSandbox and running in canvas-sketch by @MattDesl.

© 2021 Kelly Milligan

The post Creating the Effect of Transparent Glass and Plastic in Three.js appeared first on Codrops.

View Details

We recently released version 3 of CodyFrame, a CSS framework Sebastiano (my partner at CodyHouse) and I have been working on for the last three years.

In this article, I want to show you the steps that brought us to this last version, the key features of the framework, and how CodyFrame and the library of components that come with it – almost 400 – can be used to create web pages in no time.

Here’s a preview of what we’ll build:

Live demo:

https://ambercreative.co/codrops/

Click here to skip the ‘framework story’ and go straight to the tutorial.

Why we built CodyFrame and how it evolved 7 years ago, we started CodyHouse as a collection of web design experiments. We did so to promote our web design agency. There weren’t many websites back then that were sharing similar content (Codrops was one of our main inspirations).

Our library grew a lot in popularity, but we soon realised how difficult it was to include multiple experiments in the same project: they had disconnected CSS code, which required extracting the common style (e.g., buttons, typography) to avoid code repetition and style inconsistency.

This is why we decided to build CodyFrame and a new library of connected components.

The original idea was to create a super light CSS framework that would include only a set of global styles (e.g., colors, form elements, and buttons) that we could reuse each time a new component was created.

As the library kept growing, we started facing a few problems: by including layout decisions into the components (e.g., margins and paddings), it was difficult to create layout variations.

Our components are built using the BEM naming convention, so even the smallest change required a component variation:

```

```

We decided to include a few utility classes into the framework, and it turned out to be super useful not only to dry the components code but also to make them easier to customise.

```

```

We’ve since then included a lot of utility classes and developed a system to build a scalable CSS architecture based on BEM and utility classes.

CodyFrame v2 In v1, each global file contained both the base rules and the custom style:

``` .btn { / base style / display: inline-flex; text-decoration: none;

/* custom style */  
background-color: var(--color-primary);
color: var(--color-white);

} ```

One flaw of this version was the upgrade process: when a new version of the framework was released, the changes to the base code would be mixed up with the custom code the user had already modified.

To fix this issue, in CodyFrame v2 we decided to split the framework into two parts:

  1. Basic style – essential CSS rules and utility classes;
  2. Custom style – SCSS templates to create your bespoke style.

css/ ├── base/ │ ├── _accessibility.scss │ ├── _buttons.scss │ ├── _colors.scss │ ├── _forms.scss │ ├── _icons.scss │ ├── _spacing.scss │ ├── _typography.scss │ └── _util.scss └── custom-style/ ├── _buttons.scss ├── _colors.scss ├── _forms.scss ├── _icons.scss ├── _spacing.scss └── _typography.scss

Each time a new version of the framework is released, our users can replace the ‘base’ folder with the new one without affecting their bespoke style!

CSS Custom Properties Since the beginning, we have been using CSS custom properties to handle variables in CodyFrame.

CSS variables are great for code maintenance: you define the variable in one place and use it everywhere in your code. If you need to modify its value later, you only need to update one line of code, and it will propagate through the entire codebase.

But we picked them (mostly) for a different reason: unlike SASS variables, you can update the value of a CSS variable using a class or media queries.

``` .icon { --size: 1em; height: var(--size); width: var(--size); }

.icon--sm { --size: 16px; } ```

This difference opens a world of possibilities. We’ve built a system where spacing and typography are controlled by a few CSS variables, with almost no need for media queries.

Why not using an existing framework Building a framework is a learning process. It taught us a lot about code maintainability, and it freed us from chasing other frameworks updates.

Sure, it takes time to convince developers we’re worth a try. But, in the long term, it’s a win-win deal for both us and our members.

CodyFrame v2 turned out to be very stable: it has been out for more than 2 years, with more than 40 minor releases.

CodyFrame v3 was launched a few days ago to include the new SASS Modules with some important tweaks to the color and spacing systems.

Our framework is now downloaded almost 10’000 times/month, and I consider this a great achievement.

Plans for the future We just released CodyFrame v3, and we are still keeping an eye out for issue reports.

We plan to keep shipping new components and templates until we run out of ideas. We’re also looking into native web components, that would make our components compatible with all the popular JS frameworks.

CodyFrame in action Now the fun part! Let’s build something with CodyFrame and the components!

Here’s the final result:

https://ambercreative.co/codrops/

To keep the tutorial short and sweet, we’ll include CodyFrame via CDN:

```


```

If you want to create your custom style (e.g., modify buttons, typography, …), you can download the SCSS templates included in the framework and modify them using our global editors, as explained in our Documentation.

As the main navigation of the page, we’ll use the Main Header components from our UI library.

Copy the HTML, CSS and JS code using the buttons in the top-right corner of the page and include them in your project.

We can customise the HTML structure of this header: let’s delete the code for the ‘Download’ button item and the divider (.header__item--divider) and let’s remove the aria-current="page" attribute from the ‘Resources’ link – that attribute is used to style the current page link. In our case, it is the home page, so we won’t need to set it for any of the header links:

```

```

For more info on the Main Header component, you can check its Info page.

For the hero section, we’ll be using some of the CodyFrame utility classes.

```

Lorem ipsum dolor sit amet consectetur adipisicing elit

Lorem ipsum dolor ...

```

The .container and .max-width-adaptive-md classes are used to set the width of the hero element. The flex classes (.flex/.items-center/.gap-sm) are used to align the action buttons and create a gap.

To complete the hero section, we’ll need to 1) animate the headline with rotating words, 2) create the offset effect of the paragraph element, and 3) create the hover effect for the ‘Learn more’ link.

For the offset effect, we can use the grid utility classes: 1) the .col class (and its responsive modifier .col-8@md) to change the element width at a specific breakpoint, and 2) the .offset class to offset it.

```

Lorem ipsum dolor sit amet consectetur adipisicing elit

Lorem ipsum dolor ...

```

To animate the headline words, we can use the Animated Headline component (the ‘clip’ variation).

Let’s copy the CSS and JS code of the component in our project. Then use the HTML to replace the <h1> element of our hero section:

```

We designdevelopcreate digital experiences

Lorem ipsum dolor ...

```

Finally, for the link hover effect, let’s use the Link Effects component. Import the CSS code, then copy the HTML and use it to replace the original <a> element:

<a class="link-fx-1 color-contrast-higher" href="#0"> <span>Learn more</span> <svg class="icon" viewBox="0 0 32 32" aria-hidden="true"> <g fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round"><!-- ... --></g> </svg> </a>

For the next section, let’s use the Sticky Hero component – the ‘scale’ variation – with some minor customisations.

First, we’ll copy the HTML, CSS and JS code of the component in our project.

In the HTML, we can update the content of the .sticky-hero__content to include a <blockquote> element:

```

One belongs to New York instantly, one belongs to it as much in five minutes as in five years.

— Tom Wolfe

```

Now let’s create the gallery section. The HTML will include a heading element and images:

```

Selected Works

Image Preview
Image Preview
Image Preview
Image Preview

```

On mobile, we want the elements to be one below the other, while on a bigger screen, they can be positioned to create a grid. Again, we’ll make use of the grid utility classes:

```

Selected Works

Image Preview
Image Preview
Image Preview
Image Preview

```

You can play with the .col-{number} and .offset-{number} classes to create a different grid layout!

To animate the gallery images as they enter the viewport, let’s use the Reveal Effects component.

Of the different animations, we’ll use the rotate-up. Make sure to include the CSS and JS of the component, then add the .reveal-fx.reveal-fx--rotate-up classes, as explained in the Info page:

```

Image Preview

```

For the next section, we are going to create a single-column layout on mobile, and a two-columns on bigger screens:

```

```

Now we can add a heading element to the first column and some text elements to the second column.

```

Lorem, ipsum dolor sit amet consectetur adipisicing elit.

Lorem ipsum dolor sit amet...

Lorem ipsum dolor sit amet, consectetur adipisicing elit...

Read more

```

The .text-component class is used to stylise blocks containing typography, taking care of vertical rhythm and styling inline elements.

To complete this section, let’s add a hover effect to the link at the bottom of the second column, as we did for the link in the top hero section! The CSS of the Link Effects component has already been included, we only need to copy the new HTML and replace the original link:

```

Lorem ipsum dolor sit amet...

Lorem ipsum dolor sit amet, consectetur adipisicing elit...

Read more

```

Our page is almost complete! We still have two sections + the footer
For those sections, we can use three components from the UI library:

  1. The Stacking Cards component – the ‘bg-images’ variation;
  2. The Newsletter component;
  3. The Main Footer component.

And here’s the complete page:

https://ambercreative.co/codrops/

CodyFrame comes with a built-in dark theme! To activate dark more, add the data-theme="dark" to the body element, no additional changes are required!

```

```

Here’s a preview of the same page in dark mode:

https://ambercreative.co/codrops/dark-theme.html

You can get the complete code on GitHub.

Hope you enjoyed this article! We’re always open to feedback and suggestions! Share your thoughts in the comments or message us on Twitter.

The post The Process of Building a CSS Framework appeared first on Codrops.

View Details

A dialog is a component of web interfaces providing important information and requiring user interaction in response. The user’s attention should be focused exclusively on the active dialog element, both visually and interactively. Information and user actions presented by the dialog should be phrased simply and unambiguously. So, a dialog is interruptive by nature and should be used sparingly.

Usage Imagine a user browsing a web application from a mobile phone. The application needs the user’s decision about its settings immediately in order to keep functioning properly – like enabling location services in order to give directions on a map. This could be a use case for a dialog:

  1. The dialog pops up. Only the dialog is interactive, lying over the rest of the content.
  2. The dialog’s header explains the required actions in short. The dialog’s body may contain more detailed information.
  3. One or more interactive elements provide possible user actions in order to find a solution.

Structure A modal dialog consists of a container, title, description, buttons and a backdrop. If the user’s flow browsing the application must be interrupted anyway, the least we can do is to present the user a concise and well structured, clear dialog to attract their focus and quickly make an action in order to continue browsing.

Basic structure of a modal dialog It’s essential to phrase a clear and unambiguous message in the title, so the reader can understand it at a glance. Here is one example:

  • Not such a good title: “Do you want to proceed?”
  • Better, because direct and clear: “Allow access to the file system?”

Of course, further information can be put in the body of the dialog itself, but the gist should be comprehensible by reading the title and button texts only.

Behavior A dialog always needs to suit a notable purpose: Getting the user to make a choice in order to finish a task or to keep the application functioning properly (like enabling location services for navigation).

Should the dialog close by clicking the backdrop? Well, I only asked myself this question after trying to implement that behavior with the native dialog element. As it turns out, it’s far easier with ordinary divs to achieve.

Without the native dialog element, your markup would look something like this:

```

```

And the corresponding CSS

.backdrop { position: fixed; top: 0; left: 0; right: 0; bottom: 0; }

Here we have an ordinary div stretching out over the full viewport. We can easily grab that div.backdrop with JavaScript and implement our “close-modal-on-click-behavior”.

``` const backdrop = document.querySelector(".backdrop"); const dialog = document.getElementById("dialog");

backdrop.addEventListener("click", function() { dialog.style.display = none; }); ```

So, why can’t we do exactly this with the native dialog element? The native dialog element comes with a pseudo-element called ::backdrop when invoked with dialog.showModal(). As the name suggests, it is not part of the DOM, and so we cannot access it using JavaScript…

How can we add an event listener on an element, which is essentially not part of the DOM? Well, there are workarounds, like detecting a click outside of the active dialog, but that’s a completely different story.

And once I’ve come to understand, that it is not that easy, I’ve revisitied the initially posed question: Is it worthwhile to close the dialog on click outside?

No, it is not. Keep in mind, that we wanted the user to make a decision. We interrupted the user’s flow of browsing the application. We phrased the message clearly and directly, so that it’ll be comprehensible at a glance. And then we allow the user to dismiss everything we have carefully put together with a single click?! I don’t think so.

Implementation When we implement a dialog, the following requirements must be observed carefully:

  • Focus the first interactive element inside the modal once the dialog opens (*)
  • Trap focus inside the modal dialog
  • Provide at least one button that closes the dialog
  • Prevent interaction with the rest of the page (*)
  • Close the dialog when the user presses ESC (*)
  • Return focus to the element, that opened the dialog in the first place

Requirements marked with (*) are handled by the native dialog element out of the box, when opened as a modal.

So in order to get all the benefits listed above, we’re going to invoke the dialog using the method showModal provided to us by the native dialog JavaScript API.

// Open dialog as a modal const dialog = querySelector("dialog"); dialog.showModal();

Example HTML structure

```

<dialog aria-labelledby="dialog_title" aria-describedby="dialog_description"

Illustration of Location Services

Use location services?

In order to give directional instructions, we kindly ask you to turn on the location services.

```

Because we’re using the native dialog element here, we do not need to use role="dialog", modal="true" or similar for an accessible implementation.

Based on this simple HTML structure, which is taken from the example CodePen shown at the end of this article, we can now go ahead and implement the requirements listed above. Once the reader clicks the “Open Dialog” button, the first interactive element inside the dialog will receive focus by default.

Return focus to last active element after closing the dialog The HTML of a modal dialog can be placed nearly anywhere in the page’s markup. So, when the reader opens the modal, the user agent jumps to the dialog’s markup, like using a portal. Once the reader closes the dialog again, the focus needs to be returned back to the element that the reader was interacting with before opening the dialog. The portal to and from the dialog should go two-way, otherwise the reader will get lost.

``` const dialog = document.querySelector("dialog"); const openDialogBtn = document.getElementById("open_dialog"); const closeDialogBtn = document.getElementById("close_dialog");

const openDialog = () => { dialog.showModal(); };

const closeDialog = () => { dialog.close();

// Returns focus back to the button // that opened the dialog openDialogBtn.focus(); };

openDialogBtn.addEventListener("click", openDialog); closeDialogBtn.addEventListener("click", closeDialog);

// If the buttons of the dialog are contained inside a

// Use event.preventDefault() const closeDialog = (event) => { event.preventDefault(); dialog.close(); openDialogBtn.focus(); }; ```

Trap focus inside the dialog while open A focus trap is often a horror regarding UX – in case of a modal dialog it serves an essential purpose: Keeping the reader’s focus on the dialog, helping to prevent interaction with the background.

Based on the same markup and existing JS above, we can add the focus trap to our script.

``` const trapFocus = (e) => { if (e.key === "Tab") { const tabForwards = !e.shiftKey && document.activeElement === lastElement; const tabBackwards = e.shiftKey && document.activeElement === firstElement;

if (tabForwards) {
  // only TAB is pressed, not SHIFT simultaneously
  // Prevent default behavior of keydown on TAB (i.e. focus next element)
  e.preventDefault();
  firstElement.focus();
} else if (tabBackwards) {
  // TAB and SHIFT are pressed simultaneously
  e.preventDefault();
  lastElement.focus();
}

} };

// Attach trapFocus function to dialog on keydown // Updated openDialog const openDialog = () => { dialog.showModal(); dialog.addEventListener("keydown", trapFocus); };

// Remove trapFocus once dialog closes // Updated closeDialog const closeDialog = () => { dialog.removeEventListener("keydown", trapFocus); dialog.close(); openDialogBtn.focus(); }; ```

Disable closing the dialog on ESC Just in case you want to disable the built-in functionality of closing the dialog once the user has pressed the ESC key, you can listen for the keydown event when the dialog opens and prevent its default behavior. Please remember to remove the event listener after the modal has closed.

Here is the code to make it happen:

``` // Inside the function that calls dialog.showModal() const dialog = document.querySelector("dialog");

const openDialog = () => { // ... dialog.addEventListener("keydown", (e) => { if (e.key === "Escape") { e.preventDefault(); } }); }; ```

Styles for the dialog element The user agents provide some default styles for the dialog element. To override these and apply our own styles, we can use this tiny CSS reset.

dialog { padding: 0; border: none !important; /* !important used here to override polyfill CSS, if loaded */ }

Admittedly, there are more default user agent styles, which center the dialog inside the viewport and prevent overflowing content. We’ll leave these default styles untouched, because in our case they are desirable.

CSS ::backdrop pseudo-element Perhaps the coolest thing about the native dialog element is, that it gives us a nice ::backdrop pseudo-element right out of the box. The serves several purposes for us:

  • Overlay to prevent interaction with the background
  • Easily style the surroundings of the dialog while open

Accessibility aspects of a dialog element To ensure accessibility of your modal dialog you’ve already got a great deal covered by simply using the native HTML dialog element as a modal – i.e. invoked with dialog.showModal(). Thus, the first interactive element will receive focus, once the dialog opens. Additionally, interaction with other content on the page will be blocked while the dialog is active. Plus, the dialog closes with a keystroke on ESC. Everything coming “for free” along with the native dialog element.

In contrast to using a generic div as a wrapper instead of the semantically correct dialog element, you do not have to put role="dialog" accompanied by aria-modal="true.

Apart from these benefits the native dialog element has to offer, we need to make sure the following aspects are implemented:

  • Put a label on the dialog element – e.g. <dialog aria-label="Use location services?"> or use aria-labelledby if you want to reference the ID of another element inside the dialog’s body, which presents the title anyway
  • If the dialog message requires additional information, which may already be visible in the dialog’s body, you can optionally reference this text with aria-describedby or phrase a description just for screen readers inside an aria-description
  • Return focus to the element, which opened the dialog in the first place, if the dialog has been triggered by a click interaction. This is to ensure that the user can continue browsing the site or application from the same point of regard where they left off before the dialog popped up.

Polyfill for the native dialog element Sadly, the native HTML dialog element still lacks browser support here and there. As of this writing, Chrome, Edge and Opera support it, Firefox hides support behind a flag. No support from Safari and IE. The support coverage is around 75% globally. Reference browser support

On the bright side, the dialog element is easily polyfilled with this dialog polyfill from GoogleChrome.

In order to load the polyfill only on those browsers not supporting the dialog element, we check if dialog.showModal is not a function.

``` const dialog = document.querySelector("dialog");

if (typeof dialog.showModal !== "function") { // Load polyfill script const polyfill = document.createElement("script"); polyfill.type = "text/javascript"; polyfill.src = "dist/dialog-polyfill.js"; // example path document.body.append(polyfill);

// Register polyfill on dialog element once the script has loaded polyfill.onload = () => { dialogPolyfill.registerDialog(dialog); };

// Load polyfill CSS styles const polyfillStyles = document.createElement("link");

polyfillStyles.rel = "stylesheet"; polyfillStyles.href = "dialog-polyfill.css"; document.head.append(polyfillStyles); } ```

Example of a styling a native dialog element Here is a CodePen showing off an accessible, polyfilled modal dialog. It implements the requirements listed above regarding accessibility, managing focus and polyfill on-demand. The style is based on Giovanni Piemontese’s Auto Layout Dialogs – Figma UI Kit.

See the Pen

Accessible Material Dialog by Christian Kozalla (@ckozalla)

on CodePen.0

Apart from CodePen, you can view the source code of the example here on GitHub. A live example of that native dialog is hosted here.

Wrapping up In this tutorial discussed the structure and purpose of dialogs regarding user-experience, especially for modal dialogs. We’ve compiled a list of requirements for creating user-friendly dialogs. Naturally, we’ve gone in-depth on the native dialog HTML element and the benefits we gain from using it. We’ve extended its functionality by building a focus trap and managing focus around the life-cycle of the native dialog altogether.

We’ve seen how to implement an accessible modal dialog based on the requirements we set before. Our implementation will be polyfilled only when necessary.

Finally, I’ve noticed during my research about the native dialog element, that its reputation in the community has changed alot over the years. It may have been welcomed with an open mind, but today’s opinions are predominantly criticizing the dialog element while simultaneously advising to rather rely on libraries.

Nevertheless, I’m sure the native dialog element has proven to be a suitable basis for implementing modal dialogs in this tutorial. I definitely had some fun!

Thanks for reading, I hope you enjoyed it!

Related Another tutorial that might be interesting to you is this one by Osvaldas Valutis, where you’ll learn how to style and customize the upload button (file inputs).

The post How to Implement and Style the Dialog Element appeared first on Codrops.

View Details

Three.js is a JavaScript library for drawing in 3D with WebGL. It enables us to add 3D objects to a scene, and manipulate things like position and lighting. If you’re a developer used to working with the DOM and styling elements with CSS, Three.js and WebGL can seem like a whole new world, and perhaps a little intimidating! This article is for developers who are comfortable with JavaScript but relatively new to Three.js. Our goal is to walk through building something simple but effective with Three.js — a 3D animated figure — to get a handle on the basic principles, and demonstrate that a little knowledge can take you a long way!

Setting the scene In web development we’re accustomed to styling DOM elements, which we can inspect and debug in our browser developer tools. In WebGL, everything is rendered in a single <canvas> element. Much like a video, everything is simply pixels changing color, so there’s nothing to inspect. If you inspected a webpage rendered entirely with WebGL, all you would see is a <canvas> element. We can use libraries like Three.js to draw on the canvas with JavaScript.

Basic principles First we’re going to set up the scene. If you’re already comfortable with this you can skip over this part and jump straight to the section where we start creating our 3D character.

We can think of our Three.js scene as a 3D space in which we can place a camera, and an object for it to look at.

We can picture our scene as a giant cube, with objects placed at the center. In actual fact, it extends infinitely, but there is a limit to how much we can see. First of all we need to create the scene. In our HTML we just need a <canvas> element:

```

```

Now we can create the scene with a camera, and render it on our canvas in Three.js:

``` const canvas = document.querySelector('[data-canvas]')

// Create the scene const scene = new THREE.Scene()

// Create the camera const camera = new THREE.PerspectiveCamera(75, sizes.width / sizes.height, 0.1, 1000) scene.add(camera)

// Create the renderer const renderer = new THREE.WebGLRenderer({ canvas })

// Render the scene renderer.setSize(window.innerWidth, window.innerHeight) renderer.render(scene, camera) ```

For brevity, we won’t go into the precise details of everything we’re doing here. The documentation has much more detail about creating a scene and the various camera attributes. However, the first thing we’ll do is move the position of our camera. By default, anything we add to the scene is going to be placed at co-ordinates (0, 0, 0) — that is, if we imagine the scene itself as a cube, our camera will be placed right in the center. Let’s place our camera a little further out, so that our camera can look at any objects placed in the center of the scene.

Moving the camera away from the center allows us to see the objects placed in the center of the scene. We can do this by setting the z position of the camera:

camera.position.z = 5

We won’t see anything yet, as we haven’t added any objects to the scene. Let’s add a cube to the scene, which will form the basis of our figure.

3D shapes Objects in Three.js are known as meshes. In order to create a mesh, we need two things: a geometry and a material. Geometries are 3D shapes. Three.js has a selection of geometries to choose from, which can be manipulated in different ways. For the purpose of this tutorial — to see what interesting scenes we can make with just some basic principles — we’re going to limit ourselves to only two geometries: cubes and spheres.

Let’s add a cube to our scene. First we’ll define the geometry and material. Using Three.js BoxGeometry, we pass in parameters for the x, y and z dimensions.

// Create a new BoxGeometry with dimensions 1 x 1 x 1 const geometry = new THREE.BoxGeometry(1, 1, 1)

For the material we’ll choose MeshLambertMaterial, which reacts to light and shade but is more performant than some other materials.

// Create a new material with a white color const material = new THREE.MeshLambertMaterial({ color: 0xffffff })

Then we create the mesh by combining the geometry and material, and add it to the scene:

const mesh = new THREE.Mesh(geometry, material) scene.add(mesh)

Unfortunately we still won’t see anything! That’s because the material we’re using depends on light in order to be seen. Let’s add a directional light, which will shine down from above. We’ll pass in two arguments: 0xffffff for the color (white), and the intensity, which we’ll set to 1.

const lightDirectional = new THREE.DirectionalLight(0xffffff, 1) scene.add(lightDirectional)

By default, the light points down from above If you’ve followed all the steps so far, you still won’t see anything! That’s because the light is pointing directly down at our cube, so the front face is in shadow. If we move the z position of the light towards the camera and off-center, we should now see our cube.

``` const lightDirectional = new THREE.DirectionalLight(0xffffff, 1) scene.add(lightDirectional)

// Move the light source towards us and off-center lightDirectional.position.x = 5 lightDirectional.position.y = 5 lightDirectional.position.z = 5 ```

Moving the light gives us a better view We can alternatively set the position on the x, y and z axis simultaneously by calling set():

lightDirectional.position.set(5, 5, 5)

We’re looking at our cube straight on, so only one face can be seen. If we give it a little bit of rotation, we can see the other faces. To rotate an object, we need to give it a rotation angle in radians. I don’t know about you, but I don’t find radians very easy to visualize, so I prefer to use a JS function to convert from degrees:

``` const degreesToRadians = (degrees) => { return degrees * (Math.PI / 180) }

mesh.rotation.x = degreesToRadians(30) mesh.rotation.y = degreesToRadians(30) ```

We can also add some ambient light (light that comes from all directions) with a color tint, which softens the effect slightly end ensures the face of the cube turned away from the light isn’t completely hidden in shadow:

const lightAmbient = new THREE.AmbientLight(0x9eaeff, 0.2) scene.add(lightAmbient)

Now that we have our basic scene set up, we can start to create our 3D character. To help you get started I’ve created a boilerplate which includes all the set-up work we’ve just been through, so that you can jump straight to the next part if you wish.

Creating a class The first thing we’ll do is create a class for our figure. This will make it easy to add any number of figures to our scene by instantiating the class. We’ll give it some default parameters, which we’ll use later on to position our character in the scene.

class Figure { constructor(params) { this.params = { x: 0, y: 0, z: 0, ry: 0, ...params } } }

Groups In our class constructor, let’s create a Three.js group and add it to our scene. Creating a group allows us to manipulate several geometries as one. We’re going to add the different elements of our figure (head, body, arms, etc.) to this group. Then we can position, scale or rotate the figure anywhere in our scene without having to concern ourselves with individually positioning those parts individually every time.

``` class Figure { constructor(params) { this.params = { x: 0, y: 0, z: 0, ry: 0, ...params }

    this.group = new THREE.Group()
    scene.add(this.group)
}

} ```

Creating the body parts Next let’s write a function to render the body of our figure. It’ll be much the same as the way we created a cube earlier, except, we’ll make it a little taller by increasing the size on the y axis. (While we’re at it, we can remove the lines of code where we created the cube earlier, to start with a clear scene.) We already have the material defined in our codebase, and don’t need to define it within the class itself.

Instead of adding the body to the scene, we instead add it to the group we created.

``` const material = new THREE.MeshLambertMaterial({ color: 0xffffff })

class Figure { constructor(params) { this.params = { x: 0, y: 0, z: 0, ry: 0, ...params }

    this.group = new THREE.Group()
    scene.add(this.group)
}

createBody() {
    const geometry = new THREE.BoxGeometry(1, 1.5, 1)
    const body = new THREE.Mesh(geometry, material)
    this.group.add(body)
}

} ```

We’ll also write a class method to initialize the figure. So far it will call only the createBody() method, but we’ll add others shortly. (This and all subsequent methods will be written inside our class declaration, unless otherwise specified.)

``` createBody() { const geometry = new THREE.BoxGeometry(1, 1.5, 1) const body = new THREE.Mesh(geometry, material) this.group.add(body) }

init() { this.createBody() } ```

Adding the figure to the scene At this point we’ll want to render our figure in our scene, to check that everything’s working. We can do that by instantiating the class.

const figure = new Figure() figure.init()

Next we’ll write a similar method to create the head of our character. We’ll make this a cube, slightly larger than the width of the body. We’ll also need to adjust the position so it’s just above the body, and call the function in our init() method:

``` createHead() { const geometry = new THREE.BoxGeometry(1.4, 1.4, 1.4) const head = new THREE.Mesh(geometry, material) this.group.add(head)

// Position it above the body
head.position.y = 1.65

}

init() { this.createBody() this.createHead() } ```

You should now see a narrower cuboid (the body) rendered below the first cube (the head).

Adding the arms Now we’re going to give our character some arms. Here’s where things get slightly more complex. We’ll add another method to our class called createArms(). Again, we’ll define a geometry and a mesh. The arms will be long, thin cuboids, so we’ll pass in our desired dimensions for these.

As we need two arms, we’ll create them in a for loop.

``` createArms() { for(let i = 0; i < 2; i++) { const geometry = new THREE.BoxGeometry(0.25, 1, 0.25) const arm = new THREE.Mesh(geometry, material)

    this.group.add(arm)
}

} ```

We don’t need to create the geometry in the for loop, as it will be the same for each arm.

Don’t forget to call the function in our init() method:

init() { this.createBody() this.createHead() this.createArms() }

We’ll also need to position each arm either side of the body. I find it helpful here to create a variable m (for multiplier). This helps us position the left arm in the opposite direction on the x axis, with minimal code. (We’ll also use it rotate the arms in a moment too.)

``` createArms() { for(let i = 0; i < 2; i++) { const geometry = new THREE.BoxGeometry(0.25, 1, 0.25) const arm = new THREE.Mesh(geometry, material) const m = i % 2 === 0 ? 1 : -1

    this.group.add(arm)

    arm.position.x = m * 0.8
    arm.position.y = 0.1
}

} ```

Additionally, we can rotate the arms in our for loop, so they stick out at a more natural angle (as natural as a cube person can be!):

arm.rotation.z = degreesToRadians(30 * m)

If our figure is placed in the center, the arm on the left will be positioned at the negative equivalent of the x-axis position of the arm on the right Pivoting When we rotate the arms you might notice that they rotate from a point of origin in the center. It can be hard to see with a static demo, but try moving the slider in this example.

See the Pen

ThreeJS figure arm pivot example (default pivot from center) by Michelle Barker (@michellebarker)

on CodePen.0

We can see that the arms don’t move naturally, at an angle from the shoulder, but instead the entire arm rotates from the center. In CSS we would simply set the transform-origin. Three.js doesn’t have this option, so we need to do things slightly differently.

The figure on the right has arms that rotate from the top, for a more natural effect Our steps are as follows for each arm:

  1. Create a new Three.js group.
  2. Position the group at the “shoulder” of our figure (or the point from which we want to rotate).
  3. Create a new mesh for the arm and position it relative to the group.
  4. Rotate the group (instead of the arm).

Let’s update our createArms() function to follow these steps. First we’ll create the group for each arm, add the arm mesh to the group, and position the group roughly where we want it:

``` createArms() { const geometry = new THREE.BoxGeometry(0.25, 1, 0.25)

for(let i = 0; i < 2; i++) {
    const arm = new THREE.Mesh(geometry, material)
    const m = i % 2 === 0 ? 1 : -1

    // Create group for each arm
    const armGroup = new THREE.Group()

    // Add the arm to the group
    armGroup.add(arm)

    // Add the arm group to the figure
    this.group.add(armGroup)

    // Position the arm group
    armGroup.position.x = m * 0.8
    armGroup.position.y = 0.1
}

} ```

To assist us with visualizing this, we can add one of Three.js’s built-in helpers to our figure. This creates a wireframe showing the bounding box of an object. It’s useful to help us position the arm, and once we’re done we can remove it.

// Inside the `for` loop: const box = new THREE.BoxHelper(armGroup, 0xffff00) this.group.add(box)

To set the transform origin to the top of the arm rather than the center, we then need to move the arm (within the group) downwards by half of its height. Let’s create a variable for height, which we can use when creating the geometry:

``` createArms() { // Set the variable const height = 1 const geometry = new THREE.BoxGeometry(0.25, height, 0.25)

for(let i = 0; i < 2; i++) {
    const armGroup = new THREE.Group()
    const arm = new THREE.Mesh(geometry, material)

    const m = i % 2 === 0 ? 1 : -1

    armGroup.add(arm)
    this.group.add(armGroup)

    // Translate the arm (not the group) downwards by half the height
    arm.position.y = height * -0.5

    armGroup.position.x = m * 0.8
    armGroup.position.y = 0.6

    // Helper
    const box = new THREE.BoxHelper(armGroup, 0xffff00)
    this.group.add(box)
}

} ```

Then we can rotate the arm group.

// In the `for` loop armGroup.rotation.z = degreesToRadians(30 * m)

In this demo, we can see that the arms are (correctly) being rotated from the top, for a more realistic effect. (The yellow is the bounding box.)

See the Pen

ThreeJS figure arm pivot example (using group) by Michelle Barker (@michellebarker)

on CodePen.0

Eyes Next we’re going to give our figure some eyes, for which we’ll use the Sphere geometry in Three.js. We’ll need to pass in three parameters: the radius of the sphere, and the number of segments for the width and height respectively (defaults shown here).

const geometry = new THREE.SphereGeometry(1, 32, 16)

As our eyes are going to be quite small, we can probably get away with fewer segments, which is better for performance (fewer calculations needed).

Let’s create a new group for the eyes. This is optional, but it helps keep things neat. If we need to reposition the eyes later on, we only need to reposition the group, rather than both eyes individually. Once again, let’s create the eyes in a for loop and add them to the group. As we want the eyes to be a different color from the body, we can define a new material:

``` createEyes() { const eyes = new THREE.Group() const geometry = new THREE.SphereGeometry(0.15, 12, 8)

// Define the eye material
const material = new THREE.MeshLambertMaterial({ color: 0x44445c })

for(let i = 0; i < 2; i++) {
    const eye = new THREE.Mesh(geometry, material)
    const m = i % 2 === 0 ? 1 : -1

    // Add the eye to the group
    eyes.add(eye)

    // Position the eye
    eye.position.x = 0.36 * m
}

} ```

We could add the eye group directly to the figure. However, if we decide we want to move the head later on, it would be better if the eyes moved with it, rather than being positioned entirely independently! For that, we need to modify our createHead() method to create another group, comprising both the main cube of the head, and the eyes:

``` createHead() { // Create a new group for the head this.head = new THREE.Group()

// Create the main cube of the head and add to the group
const geometry = new THREE.BoxGeometry(1.4, 1.4, 1.4)
const headMain = new THREE.Mesh(geometry, material)
this.head.add(headMain)

// Add the head group to the figure
this.group.add(this.head)

// Position the head group
this.head.position.y = 1.65

// Add the eyes by calling the function we already made
this.createEyes()

} ```

In the createEyes() method we then need to add the eye group to the head group, and position them to our liking. We’ll need to position them forwards on the z axis, so they’re not hidden inside the cube of the head:

``` // in createEyes() this.head.add(eyes)

// Move the eyes forwards by half of the head depth - it might be a good idea to create a variable to do this! eyes.position.z = 0.7 ```

Legs Lastly, let’s give our figure some legs. We can create these in much the same way as the eyes. As they should be positioned relative to the body, we can create a new group for the body in the same way that we did with the head, then add the legs to it:

``` createLegs() { const legs = new THREE.Group() const geometry = new THREE.BoxGeometry(0.25, 0.4, 0.25)

for(let i = 0; i < 2; i++) {
    const leg = new THREE.Mesh(geometry, material)
    const m = i % 2 === 0 ? 1 : -1

    legs.add(leg)
    leg.position.x = m * 0.22
}

this.group.add(legs)
legs.position.y = -1.15

this.body.add(legs)

} ```

Positioning in the scene If we go back to our constructor, we can position our figure group according to the parameters:

``` class Figure { constructor(params) { this.params = { x: 0, y: 0, z: 0, ry: 0, ...params }

    this.group.position.x = this.params.x
    this.group.position.y = this.params.y
    this.group.position.z = this.params.z
    this.group.rotation.y = this.params.ry
}

} ```

Now, passing in different parameters enables us to position it accordingly. For example, we can give it a bit of rotation, and adjust its x and y position:

const figure = new Figure({ x: -4, y: -2, ry: degreesToRadians(35) }) figure.init()

Alternatively, if we want to center the figure within the scene, we can use the Three.js Box3 function, which computes the bounding box of the figure group. This line will center the figure horizontally and vertically:

new THREE.Box3().setFromObject(figure.group).getCenter(figure.group.position).multiplyScalar(-1)

Making it generative At the moment our figure is all one color, which doesn’t look particularly interesting. We can add a bit more color, and take the extra step of making it generative, so we get a new color combination every time we refresh the page! To do this we’re going to use a function to randomly generate a number between a minimum and a maximum. This is one I’ve borrowed from George Francis, which allows us to specify whether we want an integer or a floating point value (default is an integer).

``` const random = (min, max, float = false) => { const val = Math.random() * (max - min) + min

if (float) { return val }

return Math.floor(val) } ```

Let’s define some variables for the head and body in our class constructor. Using the random() function, we’ll generate a value for each one between 0 and 360:

class Figure { constructor(params) { this.headHue = random(0, 360) this.bodyHue = random(0, 360) } }

I like to use HSL when manipulating colors, as it gives us a fine degree of control over the hue, saturation and lightness. We’re going to define the material for the head and body, generating different colors for each by using template literals to pass the random hue values to the hsl color function. Here I’m adjusting the saturation and lightness values, so the body will be a vibrant color (high saturation) while the head will be more muted:

``` class Figure { constructor(params) { this.headHue = random(0, 360) this.bodyHue = random(0, 360)

    this.headMaterial = new THREE.MeshLambertMaterial({ color: `hsl(${this.headHue}, 30%, 50%` })
    this.bodyMaterial = new THREE.MeshLambertMaterial({ color: `hsl(${this.bodyHue}, 85%, 50%)` })
}

} ```

Our generated hues range from 0 to 360, a full cycle of the color wheel. If we want to narrow the range (for a limited color palette), we could select a lower range between the minimum and maximum. For example, a range between 0 and 60 would select hues in the red, orange and yellow end of the spectrum, excluding greens, blues and purples.

We could similarly generate values for the lightness and saturation if we choose to.

Now we just need to replace any reference to material with this.headMaterial or this.bodyMaterial to apply our generative colors. I’ve chosen to use the head hue for the head, arms and legs.

See the Pen

ThreeJS figure (generative) by Michelle Barker (@michellebarker)

on CodePen.0

We could use generative parameters for much more than just the colors. In this demo I’m generating random values for the size of the head and body, the length of the arms and legs, and the size and position of the eyes.

See the Pen

ThreeJS figure random generated by Michelle Barker (@michellebarker)

on CodePen.0

Animation Part of the fun of working with 3D is having our objects move in a three-dimensional space and behave like objects in the real world. We can add a bit of animation to our 3D figure using the Greensock animation library (GSAP).

GSAP is more commonly used to animate elements in the DOM. As we’re not animating DOM elements in this case, it requires a different approach. GSAP doesn’t require an element to animate — it can animate JavaScript objects. As one post in the GSAP forum puts it, GSAP is just “changing numbers really fast”.

We’ll let GSAP do the work of changing the parameters of our figure, then re-render our figure on each frame. To do this, we can use GSAP’s ticker method, which uses requestAnimationFrame. First, let’s animate the ry value (our figure’s rotation on the y axis). We’ll set it to repeat infinitely, and the duration to 20 seconds:

gsap.to(figure.params, { ry: degreesToRadians(360), repeat: -1, duration: 20 })

We won’t see any change just yet, as we aren’t re-rendering our scene. Let’s now trigger a re-render on every frame:

``` gsap.ticker.add(() => { // Update the rotation value figure.group.rotation.y = this.params.ry

// Render the scene
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.render(scene, camera)

}) ```

Now we should see the figure rotating on its y axis in the center of the scene. Let’s give him a little bounce action too, by moving him up and down and rotating the arms. First of all we’ll set his starting position on the y axis to be a little further down, so he’s not bouncing off screen. We’ll set yoyo: true on our tween, so that the animation repeats in reverse (so our figure will bounce up and down):

``` // Set the starting position gsap.set(figure.params, { y: -1.5 })

// Tween the y axis position and arm rotation gsap.to(figure.params, { y: 0, armRotation: degreesToRadians(90), repeat: -1, yoyo: true, duration: 0.5 }) ```

As we need to update a few things, let’s create a method called bounce() on our Figure class, which will handle the animation. We can use it to update the values for the rotation and position, then call it within our ticker, to keep things neat:

``` / In the Figure class: / bounce() { this.group.rotation.y = this.params.ry this.group.position.y = this.params.y }

/ Outside of the class / gsap.ticker.add(() => { figure.bounce()

// Render the scene
renderer.setSize(window.innerWidth, window.innerHeight)
renderer.render(scene, camera)

}) ```

To make the arms move, we need to do a little more work. In our class constructor, let’s define a variable for the arms, which will be an empty array:

class Figure { constructor(params) { this.arms = [] } }

In our createArms() method, in addition to our code, we’ll push each arm group to the array:

``` createArms() { const height = 0.85

for(let i = 0; i < 2; i++) {
    /* Other code for creating the arms.. */

    // Push to the array
    this.arms.push(armGroup)
}

} ```

Now we can add the arm rotation to our bounce() method, ensuring we rotate them in opposite directions:

``` bounce() { // Rotate the figure this.group.rotation.y = this.params.ry

// Bounce up and down
this.group.position.y = this.params.y

// Move the arms
this.arms.forEach((arm, index) => {
    const m = index % 2 === 0 ? 1 : -1
    arm.rotation.z = this.params.armRotation * m
})

} ```

Now we should see our little figure bouncing, as if on a trampoline!

See the Pen

ThreeJS figure with GSAP by Michelle Barker (@michellebarker)

on CodePen.0

Wrapping up There’s much, much more to Three.js, but we’ve seen that it doesn’t take too much to get started building something fun with just the basic building blocks, and sometimes limitation breeds creativity! If you’re interested in exploring further, I recommend the following resources.

Resources * Three.js Journey video course by Bruno Simon (includes transcripts and starter projects every lesson) * Three.js Fundamentals * Three.js documentation * A Generative SVG Starter Kit by George Francis — not Three.js-related, but covers some of the principles of generative art, and some handy utility functions * Greensock documentation

The post Creating 3D Characters in Three.js appeared first on Codrops.

View Details

One day I got lost in the Three.js documentation and I came across something called “MeshSurfaceSampler“. After reading the little information on the page, I opened the provided demo and was blown away!

What exactly does this class do? In short, it’s a tool you attach to a Mesh (any 3D object) then you can call it at any time to get a random point along the surface of your object.

The function works in two steps:

  1. Pick a random face from the geometry
  2. Pick a random point on that face

In this tutorial we will see how you can get started with the MeshSurfaceSampler class and explore some nice effects we can build with it.

If you are the kind of person who wants to dig right away with the demos, please do! I’ve added comments in each CodePen to help you understand the process.

This tutorial assumes basic familiarity with Three.js

Creating a scene The first step in (almost) any WebGL project is to first setup a basic scene with a cube.
In this step I will not go into much detail, you can check the comments in the code if needed.

We are aiming to render a scene with a wireframe cube that spins. This way we know our setup is ready.

Don’t forget to also load OrbitControls as it is not included in Three.js package.

``` // Create an empty scene, needed for the renderer const scene = new THREE.Scene(); // Create a camera and translate it const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); camera.position.set(1, 1, 2);

// Create a WebGL renderer and enable the antialias effect const renderer = new THREE.WebGLRenderer({ antialias: true }); // Define the size and append the in our document renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement);

// Add OrbitControls to allow the user to move in the scene const controls = new THREE.OrbitControls(camera, renderer.domElement);

// Create a cube with basic geometry & material const geometry = new THREE.BoxGeometry(1, 1, 1); const material = new THREE.MeshBasicMaterial({ color: 0x66ccff, wireframe: true }); const cube = new THREE.Mesh(geometry, material); scene.add(cube);

/// Render the scene on each frame function render () {
// Rotate the cube a little on each frame cube.rotation.y += 0.01;

renderer.render(scene, camera); } renderer.setAnimationLoop(render); ```

See the Pen by Louis Hoebregts (@Mamboleoo) on CodePen.

Creating a sampler For this step we will create a new sampler and use it to generate 300 spheres on the surface of our cube.

Note that MeshSurfaceSampler is not built-in with Three.js. You can find it in the official repository, in the ‘examples’ folder.

Once you have added the file in your imported scripts, we can initiate a sampler for our cube.

const sampler = new THREE.MeshSurfaceSampler(cube).build();

This needs to be done only once in our code. If you want to get random coordinates on multiple meshes, you will need to store a new sampler for each object.

Because we will be displaying hundreds of the same geometry, we can use the InstancedMesh class to achieve better performance. Juste like a regular Mesh, we define the geometry (SphereGeometry for the demo) and a material (MeshBasicMaterial). After to have those two, you can pass them to a new InstancedMesh and define how many objects you need (300 in this case).

const sphereGeometry = new THREE.SphereGeometry(0.05, 6, 6); const sphereMaterial = new THREE.MeshBasicMaterial({ color: 0xffa0e6 }); const spheres = new THREE.InstancedMesh(sphereGeometry, sphereMaterial, 300); scene.add(spheres);

Now that our sampler is ready to be used, we can create a loop to define a random position and scale for each of our spheres.

Before we loop, we need two dummy variables for this step:

  • tempPosition is a 3D Vector that our sampler will update with the random coordinates
  • tempObject is a 3D Object used to define the position and scale of a sphere and generate a matrix from it

Inside the loop, we start by sampling a random point on the surface of our cube and store it into tempPosition.
Those coordinates are then applied to our tempObject.
We also define a random scale for the dummy object so that not every sphere will look the same.
Because we need the Matrix of the dummy object, we ask Three.js to update it.
Finally we add the updated Matrix of the object into our InstancedMesh’s own Matrix at the index of the sphere we want to move.

const tempPosition = new THREE.Vector3(); const tempObject = new THREE.Object3D(); for (let i = 0; i < 300; i++) { sampler.sample(tempPosition); tempObject.position.set(tempPosition.x, tempPosition.y, tempPosition.z); tempObject.scale.setScalar(Math.random() * 0.5 + 0.5); tempObject.updateMatrix(); spheres.setMatrixAt(i, tempObject.matrix); }

See the Pen #1 Surface Sampling by Louis Hoebregts (@Mamboleoo) on CodePen.

Amazing isn’t it? With only a few steps we already have a working scene with random meshes along a surface.

Phew, let’s just take a breath before we move to more creative demos

Playing with particles Because everybody loves particles (I know you do), let’s see how we can generate thousands of them to create the feeling of volume only from tiny dots. For this demo, we will be using a Torus knot instead of a cube.

This demo will work with a very similar logic as for the spheres before:

  • Sample 15000 coordinates and store them in an array
  • Create a geometry from the coordinates and a material for Points
  • Combine the geometry and material into a Points object
  • Add them to the scene

``` / Sample the coordinates / const vertices = []; const tempPosition = new THREE.Vector3(); for (let i = 0; i < 15000; i ++) { sampler.sample(tempPosition); vertices.push(tempPosition.x, tempPosition.y, tempPosition.z); }

/ Create a geometry from the coordinates / const pointsGeometry = new THREE.BufferGeometry(); pointsGeometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));

/ Create a material / const pointsMaterial = new THREE.PointsMaterial({ color: 0xff61d5, size: 0.03 }); / Create a Points object / const points = new THREE.Points(pointsGeometry, pointsMaterial);

/ Add the points into the scene / scene.add(points);
```

Here is the result, a 3D Torus knot only made from particles
Try adding more particles or play with another geometry!

See the Pen #3 Surface Sampling by Louis Hoebregts (@Mamboleoo) on CodePen.

If you check the code of the demo, you will notice that I don’t add the torus knot into the scene anymore. MeshSurfaceSampler requires a Mesh, but it doesn’t even have to be rendered in your scene!

Using a 3D Model So far we have only been playing with native geometries from Three.js. It was a good start but we can take a step further by using our code with a 3D model!

There are many websites that provide free or paid models online. For this demo I will use this elephant from poly.pizza.

See the Pen #4 Surface Sampling by Louis Hoebregts (@Mamboleoo) on CodePen.

1 Loading the .obj file

Three.js doesn’t have built-in loaders for OBJ models but there are many loaders available on the official repository.

Once the file is loaded, we will update its material with wireframe activated and reduce the opacity so we can see easily through.

``` / Create global variable we will need for later / let elephant = null; let sampler = null; / Load the .obj file / new THREE.OBJLoader().load( "path/to/the/model.obj", (obj) => { / The loaded object with my file being a group, I need to pick its first child / elephant = obj.children[0]; / Update the material of the object / elephant.material = new THREE.MeshBasicMaterial({ wireframe: true, color: 0x000000, transparent: true, opacity: 0.05 }); / Add the elephant in the scene / scene.add(obj);

/* Create a surface sampler from the loaded model */
sampler = new THREE.MeshSurfaceSampler(elephant).build();

/* Start the rendering loop */ 
renderer.setAnimationLoop(render);

} );
```

2 Setup the Points object

Before sampling points along our elephant we need to setup a Points object to store all our points.

This is very similar to what we did in the previous demo, except that this time we will define a custom color for each point. We are also using a texture of a circle to make our particles rounded instead of the default square.

/* Used to store each particle coordinates & color */ const vertices = []; const colors = []; /* The geometry of the points */ const sparklesGeometry = new THREE.BufferGeometry(); /* The material of the points */ const sparklesMaterial = new THREE.PointsMaterial({ size: 3, alphaTest: 0.2, map: new THREE.TextureLoader().load("path/to/texture.png"), vertexColors: true // Let Three.js knows that each point has a different color }); /* Create a Points object */ const points = new THREE.Points(sparklesGeometry, sparklesMaterial); /* Add the points into the scene */ scene.add(points);

3 Sample a point on each frame

It is time to generate the particles on our model! But you know what? It works the same way as on a native geometry

Since you already know how to do that, you can check the code below and notice the differences:

  • On each frame, we add a new point
  • Once the point is sampled, we update the position attribute of the geometry
  • We pick a color from an array of colors and add it to the color attribute of the geometry

``` / Define the colors we want / const palette = [new THREE.Color("#FAAD80"), new THREE.Color("#FF6767"), new THREE.Color("#FF3D68"), new THREE.Color("#A73489")]; / Vector to sample a random point / const tempPosition = new THREE.Vector3();

function addPoint() { / Sample a new point / sampler.sample(tempPosition); / Push the point coordinates / vertices.push(tempPosition.x, tempPosition.y, tempPosition.z); / Update the position attribute with the new coordinates / sparklesGeometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3) );

/ Get a random color from the palette / const color = palette[Math.floor(Math.random() * palette.length)]; / Push the picked color / colors.push(color.r, color.g, color.b); / Update the color attribute with the new colors / sparklesGeometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3)); }

function render(a) { / If there are less than 10,000 points, add a new one/ if (vertices.length < 30000) { addPoint(); } renderer.render(scene, camera); }
```

Animate a growing path A cool effect we can create using the MeshSurfaceSampler class is to create a line that will randomly grow along the surface of our mesh. Here are the steps to generate the effect:

  1. Create an array to store the coordinates of the vertices of the line
  2. Pick a random point on the surface to start and add it to your array
  3. Pick another random point and check its distance from the previous point
    1. If the distance is short enough, go to step 4
    2. If the distance is too far, repeat step 3 until you find a point close enough
  4. Add the coordinates of the new point in the array
  5. Update the line geometry and render it
  6. Repeat steps 3-5 to make the line grow on each frame

The key here is the step 3 where we will pick random points until we find one that is close enough. This way we won’t have two points across the mesh. This could work for a simple object (like a sphere or a cube) as all the lines will stay inside the object. But think about our elephant, what if we have a point connected from the trunk to one of the back legs. You will end up with lines where there should be ’empty’ spaces.

Check the demo below to see the line coming to life!

See the Pen #5 Surface Sampling by Louis Hoebregts (@Mamboleoo) on CodePen.

For this animation, I’m creating a class Path as I find it a cleaner way if we want to create multiple lines. The first step is to setup the constructor of that Path. Similar to what we have done before, each path will require 4 properties:

  1. An array to store the vertices of the line
  2. The final geometry of the line
  3. A material specific for Line objects
  4. A Line object combining the geometry and the material
  5. The previous point Vector

``` / Vector to sample the new point / const tempPosition = new THREE.Vector3(); class Path { constructor () { / The array with all the vertices of the line / this.vertices = []; / The geometry of the line / this.geometry = new THREE.BufferGeometry(); / The material of the line / this.material = new THREE.LineBasicMaterial({color: 0x14b1ff}); / The Line object combining the geometry & the material / this.line = new THREE.Line(this.geometry, this.material);

/* Sample the first point of the line */
sampler.sample(tempPosition);
/* Store the sampled point so we can use it to calculate the distance */
this.previousPoint = tempPosition.clone();

} }
```

The second step is to create a function we can call on each frame to add a new vertex at the end of our line. Within that function we will execute a loop to find the next point for the path.
When that next point is found, we can store it in the vertices array and in the previousPoint variable.
Finally, we need to update the line geometry with the updated vertices array.

``` class Path { constructor () {...} update () { / Variable used to exit the while loop when we find a point / let pointFound = false; / Loop while we haven't found a point / while (!pointFound) { / Sample a random point / sampler.sample(tempPosition); / If the new point is less 30 units from the previous point / if (tempPosition.distanceTo(this.previousPoint) < 30) { / Add the new point in the vertices array / this.vertices.push(tempPosition.x, tempPosition.y, tempPosition.z); / Store the new point vector / this.previousPoint = tempPosition.clone(); / Exit the loop / pointFound = true; } } / Update the geometry / this.geometry.setAttribute("position", new THREE.Float32BufferAttribute(this.vertices, 3)); } }

function render() { / Stop the progression once we have reached 10,000 points / if (path.vertices.length < 30000) { / Make the line grow / path.update(); } renderer.render(scene, camera); }
```

The value of how short the distance between the previous point and the new one depends on your 3D model. If you have a very small object, that distance could be ‘1’, with the elephant model we are using ’30’.

Now what? Now that you know how to use MeshSurfaceSampler with particles and lines, it is your turn to create funky demos with it!
What about animating multiple lines together or starting a line from each leg of the elephant, or even popping particles from each new point of the line. The sky is the limit

See the Pen #6 Surface Sampling by Louis Hoebregts (@Mamboleoo) on CodePen.

This article does not show all the available features from MeshSurfaceSampler. There is still the weight property that allows you to have more or less chance to have a point on some faces. When we sample a point, we could also use the normal or the color of that point for other creative ideas. This could be part of a future article one day…

Until next time, I hope you learned something today and that you can’t wait to use that new knowledge!

If you have questions, let me know on Twitter.

The post Surface Sampling in Three.js appeared first on Codrops.