## 3D Typing Effects with Three.js

From our sponsor: Sell access to courses, classes, and community with Squarespace.

In this tutorial we’ll explore various animated WebGL text typing effects. We will mostly be using Three.js but not the whole tutorial relies on the specific features of this library.
But who doesn’t love Three.js though ❤️

This tutorial is aimed at developers who are familiar with the basic concepts of WebGL.

The main idea is to create a JavaScript template that takes a keyboard input and draws the text on the screen in some fancy way. The effects we will build today are all about composing a text shape with a big number of repeating objects. We will cover the following steps:

• Sampling text on Canvas (generating 2D coordinates)
• Setting up the scene and placing the Canvas element
• Generating particles in 3D space
• Turning particles to an instanced mesh
• Replacing a static string with some user input
• Basic animation
• Typing-related animation
• Generating the visuals: clouds, bubbles, flowers, eyeballs

## Text sampling

In the following we will fill a text shape with some particles.

First, let’s think about what a 3D text shape is. In general, a text mesh is nothing but a 2D shape being extruded. So we don’t need to sample the 3rd coordinate – we can just use `X/Y` coordinates with `Z` being randomly generated within the text depth (although we’re not about to use the `Z` coordinate much today).

One of the ways to generate 2D coordinates inside the shape is with Canvas sampling. So let’s create a element, apply some font-related styles to it and make sure the size of is big enough for the text to fit (extra space is okay).

``````// Settings
const fontName = 'Verdana';
const textureFontSize = 100;

// String to show
let string = 'Some text' + '\n' + 'to sample' + '\n' + 'with Canvas';

// Create canvas to sample the text
const textCanvas = document.createElement('canvas');
const textCtx = textCanvas.getContext('2d');
document.body.appendChild(textCanvas);

// ---------------------------------------------------------------

sampleCoordinates();

// ---------------------------------------------------------------

function sampleCoordinates()

// Parse text
const lines = string.split(`\n`);
const linesMaxLength = [...lines].sort((a, b) => b.length - a.length)[0].length;
const wTexture = textureFontSize * .7 * linesMaxLength;
const hTexture = lines.length * textureFontSize;

// ...

``````

With the Canvas API you can set all the font styling pretty much like in CSS. Custom fonts can be used as well, but I’m using good old Verdana today.

Once the style is set, we draw the text (or any other graphics!) on the

``````function sampleCoordinates()

// Parse text
// ...

// Draw text
const linesNumber = lines.length;
textCanvas.width = wTexture;
textCanvas.height = hTexture;
textCtx.font = '100 ' + textureFontSize + 'px ' + fontName;
textCtx.fillStyle = '#2a9d8f';
textCtx.clearRect(0, 0, textCanvas.width, textCanvas.height);
for (let i = 0; i < linesNumber; i++)
textCtx.fillText(lines[i], 0, (i + .8) * hTexture / linesNumber);

// ...

``````

… to be able to get imageData from it.

The `ImageData` object contains a one-dimensional array with RGBA data for every pixel. Knowing the size of the `canvas`, we can go through the array and check if the given `X/Y` coordinate matches the color of text or the color of the background.

Since our `canvas` doesn’t have anything but colored text on the unset (transparent black) background, we can check any of the four RGBA bytes with against a condition as simple as “bigger than zero”.

``````function sampleCoordinates() {
// Parse text
// ...
// Draw text
// ...
// Sample coordinates
textureCoordinates = [];
const samplingStep = 4;
if (wTexture > 0) {
const imageData = textCtx.getImageData(0, 0, textCanvas.width, textCanvas.height);
for (let i = 0; i < textCanvas.height; i += samplingStep)
for (let j = 0; j < textCanvas.width; j += samplingStep)
// Checking if R-channel is not zero since the background RGBA is (0,0,0,0)
if (imageData.data[(j + i * textCanvas.width) * 4] > 0)
textureCoordinates.push(
x: j,
y: i
)

}
}
``````

There’re lots of things you can do with the sampling function: change the sampling step, add some randomness, apply an outline stroke to the text, and more. Below we’ll keep using only the simplest sampling. To check the result we can add a second and draw the dot for each of sampled `textureCoordinates`.

It works 🙂

## The Three.js scene

Let’s set up a basic Three.js scene and place a `Plane` object on it. We can use the text sampling Canvas from the previous step as a color map for the `Plane`.

## Generating the particles

We can generate 3D coordinates with the very same sampling function. `X/Y` are gathered from the Canvas and for the `Z` coordinate we can take a random number.

The easiest way to visualize this set of coordinates would be a particle system known as `THREE.Points`.

``````function createParticles()
const geometry = new THREE.BufferGeometry();
const material = new THREE.PointsMaterial(
color: 0xff0000,
size: 2
);
const vertices = [];
for (let i = 0; i < textureCoordinates.length; i ++)
vertices.push(textureCoordinates[i].x, textureCoordinates[i].y, 5 * Math.random());

geometry.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
const particles = new THREE.Points(geometry, material);
scene.add(particles);

``````

Somehow it works ¯\_(ツ)_/¯

Obviously, we need to flip the `Y` coordinate for each particle and center the whole text.

To do both, we need to know the bounding box of our text. There are various ways to measure the box using the canvas API or Three.js functions. But as a temporary solution, we just take max X and Y coordinates as width and height of the text.

``````function refreshText()
sampleCoordinates();

// Gather with and height of the bounding box
const maxX = textureCoordinates.map(v => v.x).sort((a, b) => (b - a))[0];
const maxY = textureCoordinates.map(v => v.y).sort((a, b) => (b - a))[0];
stringBox.wScene = maxX;
stringBox.hScene = maxY;

createParticles();

``````

For each point, the `Y` coordinate becomes `boxTotalHeight - Y`.

Shifting the whole particles system by half-width and half-height of the box solves the centering issue.

``````function createParticles()

// ...
for (let i = 0; i < textureCoordinates.length; i ++)
// Turning Y coordinate to stringBox.hScene - Y
vertices.push(textureCoordinates[i].x, stringBox.hScene - textureCoordinates[i].y, 5 * Math.random());

// ...

// Centralizing the text
particles.position.x = -.5 * stringBox.wScene;
particles.position.y = -.5 * stringBox.hScene;

``````

Until now, we were using pixel coordinates gathered from `text canvas` directly on the 3D scene. But let’s say we need the 3D text to have the height equal to 10 units. If we set 10 as a font size, the `canvas` resolution would be too low to make a proper sampling. To avoid it (and to be more flexible with the particles density), we can add an additional scaling factor: the value we’d multiply the canvas coordinates with before using them in 3D space.

``````// Settings
// ...
const textureFontSize = 30;
const fontScaleFactor = .3;

// ...

function refreshText()

// ...

textureCoordinates = textureCoordinates.map(c =>
return  x: c.x * fontScaleFactor, y: c.y * fontScaleFactor
);

// ...

``````

At this point, we can also remove the `Plane` object. We keep using the `canvas` to draw the text and sample coordinates but we don’t need to turn it to a texture and put it on the scene.

## Switching to instanced mesh

Of course there are many cool things we can do with `THREE.Points` but our next step is turning the particles into `THREE.InstancedMesh`.

The main limitation of `THREE.Points` is the particle size. `THREE.PointsMaterial` is based on WebGL `gl_PointSize`, which can be rendered with a maximum pixel size of around 50 to 100, depending on your video card. So even if we need our particles to be as simple as planes, we sometimes can’t use `THREE.Points` due to this limitation. You may think about `THREE.Sprite` as an alternative, but (surprisingly) instanced mesh gives us much better performance on the big (10k+) number of particles.

Plus, if we want to use 3D shapes as particles, `THREE.InstancedMesh` is the only choice.

There is a well-known approach to work with `THREE.InstancedMesh`:

1. Create an instanced mesh with a known number of instances. In our case, the number of instances is the length of our coordinates array.
``````function createInstancedMesh()
instancedMesh = new THREE.InstancedMesh(particleGeometry, particleMaterial, textureCoordinates.length);
scene.add(instancedMesh);

// centralize it in the same way as before
instancedMesh.position.x = -.5 * stringBox.wScene;
instancedMesh.position.y = -.5 * stringBox.hScene;
``````
1. Add the geometry and material to be used for each instance. I use a doughnut shape known as `THREE.TorusGeometry` and `THREE.MeshNormalMaterial`.
``````function init()
// Create scene and text canvas
// ...

// Instanced geometry and material
particleGeometry = new THREE.TorusGeometry(.1, .05, 16, 50);
particleMaterial = new THREE.MeshNormalMaterial( );

// ...
``````
1. Create a dummy object that helps us generate a 4×4 transform matrix for each particle. It doesn’t need to be a part of the scene.
``````function init()
// Create scene, text canvas, instanced geometry and material
// ...

dummy = new THREE.Object3D();
``````
1. Apply the transform matrix to each instance with the `.setMatrixAt` method
``````function updateParticlesMatrices()
let idx = 0;
textureCoordinates.forEach(p =>

// we apply samples coordinates like before + some random rotation
dummy.rotation.set(2 * Math.random(), 2 * Math.random(), 2 * Math.random());
dummy.position.set(p.x, stringBox.hScene - p.y, Math.random());

dummy.updateMatrix();
instancedMesh.setMatrixAt(idx, dummy.matrix);

idx ++;
)
instancedMesh.instanceMatrix.needsUpdate = true;
``````

## Listening to the keyboard

So far, the `string` value was hard-coded. We want it to be dynamic and contain the user input.

There are many ways to listen to the keyboard: working directly with keyup/keydown events, using the HTML input element as a proxy, etc. I ended up with a

``` ```
element that has a contenteditable attribute set. Compared to an or a ```</code>, it’s more painful to parse the multi-line string from an editable <code></p> <div></code>. But it’s much easier to get an accurate pixel values for the cursor position and the text bounding box.</p> <p>I won’t go too much into details here. The main idea is to keep the editable <code></p> <div></code> focused all the time so that we keep track of whatever the user types there.</p> <div style="font-size: .8em;"> <pre class="wp-block-prismatic-blocks"><code class="language-markup"><div id="text-input" contenteditable="true" onblur="this.focus()" autofocus></div></code></pre> </div> <p>Using the <code>keyup event</code> we parse the <code>string</code> and get the width and height of <code>stringBox</code> from the <code>contenteditable </p> <div></code>, and then refresh the instanced mesh.</p> <div style="font-size: .8em; max-height: 400px; overflow-y: scroll;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">document.addEventListener('keyup', () => handleInput(); refreshText(); );</code></pre> </div> <p>While parsing, we replace the inner tags with new lines (this part is specific for <code></p> <div contenteditable></code>), and do a few things for usability like disabling empty new lines above and below the text. </p> <p>Please note that <code></p> <div contenteditable></code> and text <code>canvas</code> should have the same CSS properties (font, font size, etc). With the same styles applied, the text is rendered in the very same way on both elements. With that in place, we can take the pixel values from <code></p> <div contenteditable></code> (text width, height, cursor position) and use them for the <code>canvas</code>.</p> <div style="font-size: .8em; max-height: 400px; overflow-y: scroll;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">const textInputEl = document.querySelector('#text-input'); textInputEl.style.fontSize = textureFontSize + 'px'; textInputEl.style.font = '100 ' + textureFontSize + 'px ' + fontName; textInputEl.style.lineHeight = 1.1 * textureFontSize + 'px'; // ... function handleInput() { if (isNewLine(textInputEl.firstChild)) textInputEl.firstChild.remove(); if (isNewLine(textInputEl.lastChild)) if (isNewLine(textInputEl.lastChild.previousSibling)) textInputEl.lastChild.remove(); string = textInputEl.innerHTML .replaceAll("<p>", "\n") .replaceAll("</p>", "") .replaceAll("<div>", "\n") .replaceAll("</div>", "") .replaceAll("<br>", "") .replaceAll("<br/>", "") .replaceAll(" ", " "); stringBox.wTexture = textInputEl.clientWidth; stringBox.wScene = stringBox.wTexture * fontScaleFactor; stringBox.hTexture = textInputEl.clientHeight; stringBox.hScene = stringBox.hTexture * fontScaleFactor; function isNewLine(el) { if (el) if (el.tagName) if (el.tagName.toUpperCase() === 'DIV' return false } }</code></pre> </div> <p>Once we have the <code>string</code> and the <code>stringBox</code>, we update the instanced mesh.</p> <div style="font-size: .8em; max-height: 400px; overflow-y: scroll;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">function refreshText() sampleCoordinates(); textureCoordinates = textureCoordinates.map(c => return x: c.x * fontScaleFactor, y: c.y * fontScaleFactor ); // This part can be removed as we take text size from editable <div> // const sortedX = textureCoordinates.map(v => v.x).sort((a, b) => (b - a))[0]; // const sortedY = textureCoordinates.map(v => v.y).sort((a, b) => (b - a))[0]; // stringBox.wScene = sortedX; // stringBox.hScene = sortedY;</s> recreateInstancedMesh(); updateParticlesMatrices(); </code></pre> </div> <p>Coordinate sampling is the same as before with one difference: we now can create <code>canvas</code> with the exact text size, no extra space to sample.</p> <div style="font-size: .8em; max-height: 400px; overflow-y: scroll;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">function sampleCoordinates() const lines = string.split(`\n`); // This part can be removed as we take text size from editable <div> // const linesMaxLength = [...lines].sort((a, b) => b.length - a.length)[0].length; // stringBox.wTexture = textureFontSize * .7 * linesMaxLength; // stringBox.hTexture = lines.length * textureFontSize; textCanvas.width = stringBox.wTexture; textCanvas.height = stringBox.hTexture; // ... </code></pre> </div> <p>We can’t increase the number of instances for the existing mesh. So the mesh should be recreated every time the text is updated. Although text centering and instances transform is done exactly like before.</p> <div style="font-size: .8em; max-height: 400px; overflow-y: scroll;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">// function createInstancedMesh() { function recreateInstancedMesh() // Now we need to remove the old Mesh and create a new one every refreshText() call scene.remove(instancedMesh); instancedMesh = new THREE.InstancedMesh(particleGeometry, particleMaterial, textureCoordinates.length); // ... function updateParticlesMatrices() // same as before //... </code></pre> </div> <p>Since our text is dynamic and it can get pretty long, let’s make sure the instanced mesh fits the screen:</p> <div style="font-size: .8em;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">function refreshText() // ... makeTextFitScreen(); function makeTextFitScreen() const fov = camera.fov * (Math.PI / 180); const fovH = 2 * Math.atan(Math.tan(fov / 2) * camera.aspect); const dx = Math.abs(.55 * stringBox.wScene / Math.tan(.5 * fovH)); const dy = Math.abs(.55 * stringBox.hScene / Math.tan(.5 * fov)); const factor = Math.max(dx, dy) / camera.position.length(); if (factor > 1) camera.position.x *= factor; camera.position.y *= factor; camera.position.z *= factor; </code></pre> </div> <p><iframe src="https://codesandbox.io/embed/06-demo--typing-effects-with-webgl-tutorial-s3kr31?runonclick=0&codemirror=1&fontsize=12&view=preview" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="06_demo--Typing-Effects-with-WebGL-Tutorial" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe></p> <p>One more thing to add is a caret (text cursor). It can be a simple 3D box with a size matching the font size.</p> <div style="font-size: .8em; max-height: 400px; overflow-y: scroll;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">function init() // ... const cursorGeometry = new THREE.BoxGeometry(.3, 4.5, .03); cursorGeometry.translate(.5, -2.7, 0) const cursorMaterial = new THREE.MeshNormalMaterial( transparent: true, ); cursorMesh = new THREE.Mesh(cursorGeometry, cursorMaterial); scene.add(cursorMesh); </code></pre> </div> <p>We gather the position of the caret from our editable <code></p> <div></code> in pixels and multiply it by <code>fontScaleFactor</code>, like we do with the bounding box width and height.</p> <div style="font-size: .8em; max-height: 400px; overflow-y: scroll;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">function handleInput() // ... stringBox.caretPosScene = getCaretCoordinates().map(c => c * fontScaleFactor); function getCaretCoordinates() const range = window.getSelection().getRangeAt(0); const needsToWorkAroundNewlineBug = (range.startContainer.nodeName.toLowerCase() === 'div' && range.startOffset === 0); if (needsToWorkAroundNewlineBug) return [ range.startContainer.offsetLeft, range.startContainer.offsetTop ] else const rects = range.getClientRects(); if (rects[0]) return [rects[0].left, rects[0].top] else // since getClientRects() gets buggy in FF document.execCommand('selectAll', false, null); return [ 0, 0 ] </code></pre> </div> <p>The cursor just needs same centering as our <code>instanced mesh</code> has, and voilà, the 3D caret position is the same as in the the input div.</p> <div style="font-size: .8em; max-height: 400px; overflow-y: scroll;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">function refreshText() // ... updateCursorPosition(); function updateCursorPosition() cursorMesh.position.x = -.5 * stringBox.wScene + stringBox.caretPosScene[0]; cursorMesh.position.y = .5 * stringBox.hScene - stringBox.caretPosScene[1]; </code></pre> </div> <p>The only thing left is to make the cursor blink when the page (and hence the input element) is focused. The <code>roundPulse</code> function generates the rounded pulse between 0 and 1 from <code>THREE.Clock.getElapsedTime()</code>. We need to update the cursor opacity all the time, so the <code>updateCursorOpacity</code> call goes to the main <code>render</code> loop.</p> <div style="font-size: .8em; max-height: 400px; overflow-y: scroll;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">function render() // ... updateCursorOpacity(); // ... let roundPulse = function updateCursorOpacity() if (document.hasFocus() && document.activeElement === textInputEl) cursorMesh.material.opacity = roundPulse(2 * clock.getElapsedTime()); else cursorMesh.material.opacity = 0; </code></pre> </div> <p><iframe src="https://codesandbox.io/embed/07-demo--typing-effects-with-webgl-tutorial-13q5z0?runonclick=0&codemirror=1&fontsize=12&view=preview" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="07_demo--Typing-Effects-with-WebGL-Tutorial" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe></p> <h2>Basic animation</h2> <p>Instead of setting the instances transform just on the text update, we can also animate this transform. </p> <p>To do this, we add an additional array of <code>Particle</code> objects to store the parameters for each instance. We still need the <code>textureCoordinates</code> array to store the 2D coordinates in pixels, but now we remap them to the <code>particles</code> array. And obviously, the particles transform update should happen in the main <code>render</code> loop now.</p> <div style="font-size: .8em; max-height: 400px; overflow-y: scroll;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">// ... let textureCoordinates = []; let particles = []; function refreshText() // ... // textureCoordinates are only pixel coordinates, particles is array of data objects particles = textureCoordinates.map(c => new Particle([c.x * fontScaleFactor, c.y * fontScaleFactor]) ); // We call it in the render() loop now // updateParticlesMatrices(); // ... </code></pre> </div> <p>Each <code>Particle</code> object contains a list of properties and a <code>grow()</code> function that updates some of those properties.</p> <p>For starters, we define position, rotation and scale. Position would be static for each particle, scale would increase from zero to one when the particle is created, and rotation would be animated all the time.</p> <div style="font-size: .8em; max-height: 400px; overflow-y: scroll;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">function Particle([x, y]) this.x = x; this.y = y; this.z = 0; this.rotationX = Math.random() * 2 * Math.PI; this.rotationY = Math.random() * 2 * Math.PI; this.rotationZ = Math.random() * 2 * Math.PI; this.scale = 0; this.deltaRotation = .2 * (Math.random() - .5); this.deltaScale = .01 + .2 * Math.random(); this.grow = function () this.rotationX += this.deltaRotation; this.rotationY += this.deltaRotation; this.rotationZ += this.deltaRotation; if (this.scale < 1) this.scale += this.deltaScale; // ... function updateParticlesMatrices() { let idx = 0; // textureCoordinates.forEach(p => particles.forEach(p => // update the particles data p.grow(); // dummy.rotation.set(2 * Math.random(), 2 * Math.random(), 2 * Math.random()); dummy.rotation.set(p.rotationX, p.rotationY, p.rotationZ); dummy.scale.set(p.scale, p.scale, p.scale); dummy.position.set(p.x, stringBox.hScene - p.y, p.z); dummy.updateMatrix(); instancedMesh.setMatrixAt(idx, dummy.matrix); idx ++; ) instancedMesh.instanceMatrix.needsUpdate = true; </code></pre> </div> <p><iframe src="https://codesandbox.io/embed/08-demo--typing-effects-with-webgl-tutorial-26skky?runonclick=0&codemirror=1&fontsize=12&view=preview" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="08_demo--Typing-Effects-with-WebGL-Tutorial" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe></p> <h2>Typing animation</h2> <p>We already have a nice template by now. But every time the text is updated we recreate all the instances for all the symbols. So every time the text is changed we reset all the properties and animations of all the particles.</p> <p>Instead, we need to keep the properties and animations for “old” particles. To do so, we need to know if each particle should be recreated or not. </p> <p>In other words, for each sampled coordinate we need to check if <code>Particle</code> already exists or not. If we found a <code>Particle</code> object with the same <code>X/Y</code> coordinates, we keep it along with all its properties. If there is no existing <code>Particle</code> for the sampled coordinate, we call <code>new Particle()</code> like we did before. </p> <p>We evolve the sampling function so we don’t only gather the X/Y values and refill <code>textureCoordinates</code> array but also do the following:</p> <ol> <li>Turn one-dimensional array <code>imageData</code> to two-dimensional <code>imageMask</code> array</li> <li>Go through the existing <code>textureCoordinates</code> array and compare its elements to the <code>imageMask</code>. If coordinate exists, add <code>old</code> property to the coordinate, otherwise add <code>toDelete</code> property.</li> <li>All the sampled coordinates that were not found in the <code>textureCoordinates</code>, we handle as new coordinate that has X and Y values and <code>old</code> or <code>toDelete</code> properties set to <code>false</code></li> </ol> <p>It would make sense to simply delete old coordinates that were not found in the new <code>imageMask</code>. But we use a special <code>toDelete</code> property instead to play a fade-out animation for deleted particles first, and actually delete the Particle data only in the next step.</p> <div style="font-size: .8em; max-height: 400px; overflow-y: scroll;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">function sampleCoordinates() { // Draw text // ... // Sample coordinates if (stringBox.wTexture > 0) { // Image data to 2d array const imageData = textCtx.getImageData(0, 0, textCanvas.width, textCanvas.height); const imageMask = Array.from(Array(textCanvas.height), () => new Array(textCanvas.width)); for (let i = 0; i < textCanvas.height; i++) for (let j = 0; j < textCanvas.width; j++) imageMask[i][j] = imageData.data[(j + i * textCanvas.width) * 4] > 0; if (textureCoordinates.length !== 0) { // Clean up: delete coordinates and particles which disappeared on the prev step // We need to keep same indexes for coordinates and particles to reuse old particles properly textureCoordinates = textureCoordinates.filter(c => !c.toDelete); particles = particles.filter(c => !c.toDelete); // Go through existing coordinates (old to keep, toDelete for fade-out animation) textureCoordinates.forEach(c => if (imageMask[c.y]) if (imageMask[c.y][c.x]) c.old = true; if (!c.toDelete) imageMask[c.y][c.x] = false; else c.toDelete = true; else c.toDelete = true; ); } // Add new coordinates for (let i = 0; i < textCanvas.height; i++) for (let j = 0; j < textCanvas.width; j++) if (imageMask[i][j]) textureCoordinates.push( x: j, y: i, old: false, toDelete: false ) } else textureCoordinates = []; }</code></pre> </div> <p>With <code>old</code> and <code>toDelete</code> properties, mapping texture coordinates to the particles becomes conditional:</p> <div style="font-size: .8em; max-height: 400px; overflow-y: scroll;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">function refreshText() // ... // particles = textureCoordinates.map(c => // new Particle([c.x * fontScaleFactor, c.y * fontScaleFactor]) // ); particles = textureCoordinates.map((c, cIdx) => const x = c.x * fontScaleFactor; const y = c.y * fontScaleFactor; let p = (c.old && particles[cIdx]) ? particles[cIdx] : new Particle([x, y]); if (c.toDelete) p.toDelete = true; p.scale = 1; return p; ); // ... </code></pre> </div> <p>The <code>grow()</code> call would not only increase the size of the particle when it’s created. We would also decrease it if the particle meant to be deleted.</p> <div style="font-size: .8em; max-height: 400px; overflow-y: scroll;"> <pre class="wp-block-prismatic-blocks"><code class="language-javascript">function Particle([x, y]) // ... this.toDelete = false; this.grow = function () // ... if (this.scale < 1) this.scale += this.deltaScale; if (this.toDelete) this.scale -= this.deltaScale; if (this.scale <= 0) this.scale = 0; </code></pre> </div> <p>The template is now ready and we can use it to create various effects with only little changes.</p> <p><iframe src="https://codesandbox.io/embed/09-demo--typing-effects-with-webgl-tutorial-j3jbuy?runonclick=0&codemirror=1&fontsize=12&view=preview" style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;" title="09_demo--Typing-Effects-with-WebGL-Tutorial" sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"></iframe></p> <h2>Bubbles effect 🫧</h2> <p class="codepen" data-height="500" data-theme-id="dark" data-default-tab="result" data-slug-hash="LYmBgmr" data-user="ksenia-k" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a rel="nofollow noopener" target="_blank" href="https://codepen.io/ksenia-k/pen/LYmBgmr"><br /> Bubble Typer Three.js – Demo #2 </a> by Ksenia Kondrashova (<a rel="nofollow noopener" target="_blank" href="https://codepen.io/ksenia-k">@ksenia-k</a>)<br /> on <a rel="nofollow noopener" target="_blank" href="https://codepen.io">CodePen</a>.</span> </p> <p>Here is the full list of changes I made to make these bubbles based on the template:</p> <ol> <li>Change <code>TorusGeometry</code> to <code>IcosahedronGeometry</code> so each instance is a sphere</li> <li>Replace <code>MeshNormalMaterial</code> with <code>ShaderMaterial</code>. You can check out the GLSL code in the sandbox above but the shader essentially does this: <ul> <li>mix white color and randomized gradient (taken from normal vector), and use the result as sphere color</li> <li>applies transparency in a way to make less transparent outline and more transparent middle of the sphere if you look from the camera position</li> </ul> </li> <li>Adjust <code>textureFontSize</code> and <code>fontScaleFactor</code> values to change the density of the particles</li> <li>Evolve the <code>Particle</code> object so that <ul> <li>bubble position is a bit randomized comparing to the sampled coordinates</li> <li>maximum size of the bubble is defined by randomized <code>maxScale</code> property</li> <li>no rotation</li> <li>bubbles size is randomized as the scale limit is <code>maxScale</code> property, not 1</li> <li>bubble grows all the time, bursts, and then grows again. So the scale increase happens not only when <code>Particle</code> is created but all the time. Once the scale reaches the <code>maxScale</code> value, we reset the scale to zero</li> <li>some bubbles would get <code>isFlying</code> property so they move up from the initial position</li> </ul> </li> <li>Change color of page background and cursor</li> </ol> <h2>Clouds effect ☁️</h2> <p>You don’t need to do much for having clouds, too:</p> <ol> <li>Use <code>PlaneGeometry</code> for instance shape</li> <li>Use <code>MeshBasicMaterial</code> and apply the following image as an alpha map<img decoding="async" style="display: block; width: 100px;" src="https://i7x7p5b7.stackpathcdn.com/codrops/wp-content/uploads/2022/11/smoke.png" alt="smoke 3D Typing Effects with Three.js" title="3D Typing Effects with Three.js 5"></li> <li>Adjust <code>textureFontSize</code> and <code>fontScaleFactor</code> to change the density of the particles</li> <li>Evolve the <code>Particle</code> object so that <ul> <li>particle position is a bit randomized compared to the sampled coordinates</li> <li>size of the particle is defined by randomized <code>maxScale</code> property</li> <li>only rotation around Z axis is needed</li> <li>particle size (scale) is pulsating all the time</li> </ul> </li> <li>Additional transform <code>.copy(camera.quaternion)</code> should be applied for each instance. This way the particle is always facing towards the camera; rotate the cloudy text to see the result 🙂</li> <li>Change color of page background and cursor</li> </ol> <p class="codepen" data-height="400" data-theme-id="dark" data-default-tab="result" data-slug-hash="vYrKWOB" data-user="ksenia-k" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a rel="nofollow noopener" target="_blank" href="https://codepen.io/ksenia-k/pen/vYrKWOB"><br /> Clouds Typer Three.js – Demo #1</a> by Ksenia Kondrashova (<a rel="nofollow noopener" target="_blank" href="https://codepen.io/ksenia-k">@ksenia-k</a>)<br /> on <a rel="nofollow noopener" target="_blank" href="https://codepen.io">CodePen</a>.</span> </p> <h2>Flowers effect 🌸</h2> <p>Flowers are actually quite similar to clouds. The main difference is about having two instanced meshes and two materials. One is mapped as flower texture, another one as a leaf</p> <p><img decoding="async" style="display: inline-block; width: 100px;" src="https://i7x7p5b7.stackpathcdn.com/codrops/wp-content/uploads/2022/11/flower.png" alt="flower 3D Typing Effects with Three.js" title="3D Typing Effects with Three.js 6"><br /> <img decoding="async" style="display: inline-block; width: 100px;" src="https://i7x7p5b7.stackpathcdn.com/codrops/wp-content/uploads/2022/11/leaf.png" alt="leaf 3D Typing Effects with Three.js" title="3D Typing Effects with Three.js 7"></p> <p>Also, all the particles must have a new <code>color</code> property. We apply colors to the <code>instanced mesh</code> with the <code>setColorAt</code> method every time we recreate the meshes. </p> <p>With a few small changes like particles density, scaling speed, rotation speed, and the color of the background and cursor, we have this:</p> <p class="codepen" data-height="400" data-theme-id="dark" data-default-tab="result" data-slug-hash="WNyxXpO" data-user="ksenia-k" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a rel="nofollow noopener" target="_blank" href="https://codepen.io/ksenia-k/pen/WNyxXpO"><br /> Flower Typer Three.js – Demo #3</a> by Ksenia Kondrashova (<a rel="nofollow noopener" target="_blank" href="https://codepen.io/ksenia-k">@ksenia-k</a>)<br /> on <a rel="nofollow noopener" target="_blank" href="https://codepen.io">CodePen</a>.</span> </p> <h2>Eyes effect 👀</h2> <p>We can go further and load a <code>glb</code> model and use it as an instance! I took this nice looking eye from <a rel="nofollow noopener" target="_blank" href="https://www.turbosquid.com/3d-models/3d-16-colors-of-realistic-eye-demo-free-model-1765448">turbosquid.com</a></p> <p><img decoding="async" loading="lazy" style="display: inline-block" src="https://i7x7p5b7.stackpathcdn.com/codrops/wp-content/uploads/2022/11/tscreenshot000.jpeg" alt="tscreenshot000 3D Typing Effects with Three.js" width="199" height="159" title="3D Typing Effects with Three.js 8"></p> <p>Instead of applying a random rotation, we can make the eyeballs follow the mouse position! To do so, we need an additional transparent plane in front of the instanced mesh, <code>THREE.Raycaster()</code> and the mouse position tracker. We are listening to the <code>mousemove</code> event, set ray from mouse to the plane, and make the <code>dummy</code> object look at the intersection point.</p> <p>Don’t forget to add some lights to see the imported model. And as we have lights, let’s make the instanced mesh cast the shadow to the plane behind the text.</p> <p>Together with some other small changes like sampling density, <code>grow()</code> function parameters, cursor and background style, we get this:</p> <p class="codepen" data-height="400" data-theme-id="dark" data-default-tab="result" data-slug-hash="KKeMyLx" data-user="ksenia-k" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;"> <span>See the Pen <a rel="nofollow noopener" target="_blank" href="https://codepen.io/ksenia-k/pen/KKeMyLx"><br /> Eyes Typer Three.js – Demo #4</a> by Ksenia Kondrashova (<a rel="nofollow noopener" target="_blank" href="https://codepen.io/ksenia-k">@ksenia-k</a>)<br /> on <a rel="nofollow noopener" target="_blank" href="https://codepen.io">CodePen</a>.</span> </p> <p>And that’s it! I hope this tutorial was interesting and that it gave you some inspiration. Feel free to use this template to create more fun things!</p> <div class="ct-post-nav"> <div class="ct-post-prev"> How to Make Search Your Site’s Greatest Asset </div> </p></div> <p> <!-- ct-post-nav --></p></div> <div class="post-views content-post post-563 entry-meta"> <span class="post-views-icon dashicons dashicons-chart-bar"></span> <span class="post-views-label">Post Views:</span> <span class="post-views-count">32</span> </div><div class="addtoany_share_save_container addtoany_content addtoany_content_bottom"><div class="a2a_kit a2a_kit_size_32 addtoany_list" data-a2a-url="https://biharigraphic.com/3d-typing-effects-with-three-js/" data-a2a-title="3D Typing Effects with Three.js"><a class="a2a_button_facebook" href="https://www.addtoany.com/add_to/facebook?linkurl=https%3A%2F%2Fbiharigraphic.com%2F3d-typing-effects-with-three-js%2F&linkname=3D%20Typing%20Effects%20with%20Three.js" title="Facebook" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_whatsapp" href="https://www.addtoany.com/add_to/whatsapp?linkurl=https%3A%2F%2Fbiharigraphic.com%2F3d-typing-effects-with-three-js%2F&linkname=3D%20Typing%20Effects%20with%20Three.js" title="WhatsApp" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_twitter" href="https://www.addtoany.com/add_to/twitter?linkurl=https%3A%2F%2Fbiharigraphic.com%2F3d-typing-effects-with-three-js%2F&linkname=3D%20Typing%20Effects%20with%20Three.js" title="Twitter" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_linkedin" href="https://www.addtoany.com/add_to/linkedin?linkurl=https%3A%2F%2Fbiharigraphic.com%2F3d-typing-effects-with-three-js%2F&linkname=3D%20Typing%20Effects%20with%20Three.js" title="LinkedIn" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_telegram" href="https://www.addtoany.com/add_to/telegram?linkurl=https%3A%2F%2Fbiharigraphic.com%2F3d-typing-effects-with-three-js%2F&linkname=3D%20Typing%20Effects%20with%20Three.js" title="Telegram" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_facebook_messenger" href="https://www.addtoany.com/add_to/facebook_messenger?linkurl=https%3A%2F%2Fbiharigraphic.com%2F3d-typing-effects-with-three-js%2F&linkname=3D%20Typing%20Effects%20with%20Three.js" title="Messenger" rel="nofollow noopener" target="_blank"></a><a class="a2a_button_email" href="https://www.addtoany.com/add_to/email?linkurl=https%3A%2F%2Fbiharigraphic.com%2F3d-typing-effects-with-three-js%2F&linkname=3D%20Typing%20Effects%20with%20Three.js" title="Email" rel="nofollow noopener" target="_blank"></a><a class="a2a_dd addtoany_share_save addtoany_share" href="https://www.addtoany.com/share"></a></div></div> </div><!-- .entry --> <div class="post-tags clr"> </div> <section id="related-posts" class="clr"> <h3 class="theme-heading related-posts-title"> <span class="text">You Might Also Like</span> </h3> <div class="oceanwp-row clr"> <article class="related-post clr col span_1_of_3 col-1 post-1683 post type-post status-publish format-standard has-post-thumbnail hentry category-computer-science entry has-media owp-thumbs-layout-horizontal owp-btn-normal owp-tabs-layout-horizontal has-no-thumbnails has-product-nav"> <figure class="related-post-media clr"> <a href="https://biharigraphic.com/how-i-reduced-flutter-integration-suite-result-time-by-almost-50-by-akanksha-dec-2022/" class="related-thumb"> <img fifu-featured="1" width="300" height="300" src="https://miro.medium.com/max/1200/1*z6SM-_656-vOSdEeb6Jcvw.png" class="attachment-medium size-medium wp-post-image" alt="" title="" title="How I Reduced Flutter Integration Suite Result Time by Almost 50% | by Akanksha | Dec, 2022 9" decoding="async" loading="lazy" itemprop="image"> </a> </figure> <h3 class="related-post-title"> <a href="https://biharigraphic.com/how-i-reduced-flutter-integration-suite-result-time-by-almost-50-by-akanksha-dec-2022/" rel="bookmark">How I Reduced Flutter Integration Suite Result Time by Almost 50% | by Akanksha | Dec, 2022</a> </h3><!-- .related-post-title --> <time class="published" datetime="2022-12-05T15:04:55+00:00"><i class=" icon-clock" aria-hidden="true" role="img"></i>December 5, 2022</time> </article><!-- .related-post --> <article class="related-post clr col span_1_of_3 col-2 post-930 post type-post status-publish format-standard has-post-thumbnail hentry category-computer-science entry has-media owp-thumbs-layout-horizontal owp-btn-normal owp-tabs-layout-horizontal has-no-thumbnails has-product-nav"> <figure class="related-post-media clr"> <a href="https://biharigraphic.com/simple-reasons-i-was-promoted-to-a-senior-engineer-by-scott-pickthorn-nov-2022/" class="related-thumb"> <img fifu-featured="1" width="300" height="300" src="https://miro.medium.com/max/1200/0*XFs6XUW1huh9PDKT" class="attachment-medium size-medium wp-post-image" alt="" title="" title="Simple Reasons I Was Promoted to a Senior Engineer | by Scott Pickthorn | Nov, 2022 10" decoding="async" loading="lazy" itemprop="image"> </a> </figure> <h3 class="related-post-title"> <a href="https://biharigraphic.com/simple-reasons-i-was-promoted-to-a-senior-engineer-by-scott-pickthorn-nov-2022/" rel="bookmark">Simple Reasons I Was Promoted to a Senior Engineer | by Scott Pickthorn | Nov, 2022</a> </h3><!-- .related-post-title --> <time class="published" datetime="2022-11-16T16:47:07+00:00"><i class=" icon-clock" aria-hidden="true" role="img"></i>November 16, 2022</time> </article><!-- .related-post --> <article class="related-post clr col span_1_of_3 col-3 post-1166 post type-post status-publish format-standard has-post-thumbnail hentry category-computer-science entry has-media owp-thumbs-layout-horizontal owp-btn-normal owp-tabs-layout-horizontal has-no-thumbnails has-product-nav"> <figure class="related-post-media clr"> <a href="https://biharigraphic.com/understanding-java-virtual-threads-by-the-bored-dev-nov-2022/" class="related-thumb"> <img fifu-featured="1" width="300" height="300" src="https://miro.medium.com/max/1200/0*-7pWoqFgLYhXckti" class="attachment-medium size-medium wp-post-image" alt="" title="" title="Understanding Java Virtual Threads | by The Bored Dev | Nov, 2022 11" decoding="async" loading="lazy" itemprop="image"> </a> </figure> <h3 class="related-post-title"> <a href="https://biharigraphic.com/understanding-java-virtual-threads-by-the-bored-dev-nov-2022/" rel="bookmark">Understanding Java Virtual Threads | by The Bored Dev | Nov, 2022</a> </h3><!-- .related-post-title --> <time class="published" datetime="2022-11-21T21:18:25+00:00"><i class=" icon-clock" aria-hidden="true" role="img"></i>November 21, 2022</time> </article><!-- .related-post --> </div><!-- .oceanwp-row --> </section><!-- .related-posts --> <section id="comments" class="comments-area clr has-comments"> <div id="respond" class="comment-respond"> <h3 id="reply-title" class="comment-reply-title">Leave a Reply <small><a rel="nofollow" id="cancel-comment-reply-link" href="/3d-typing-effects-with-three-js/#respond" style="display:none;">Cancel reply</a></small></h3><form action="https://biharigraphic.com/wp-comments-post.php" method="post" id="commentform" class="comment-form" novalidate><div class="comment-textarea"><label for="comment" class="screen-reader-text">Comment</label><textarea name="comment" id="comment" cols="39" rows="4" tabindex="0" class="textarea-comment" placeholder="Your comment here...">```
`Enter your name or username to comment`
``` Enter your email address to comment Enter your website URL (optional) Save my name, email, and website in this browser for the next time I comment. ```
``` ```
``` (adsbygoogle = window.adsbygoogle || []).push({}); ```
``` ```
``` About Us Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis us nostrud exercitation amet, consectetur elit aboris nisi. ```
``` ```