Morphing a formation

The sphere of beads turns into a doughnut shape, starting at the top and rippling down, while the beads brighten from dark to bright purple. A morph blends each entity's two positions with mix(a, b, ramp(t, from, to, ease)), and giving each entity its own lag in from and to turns one blend into a wave.

This pattern builds on A camera push and a fly-through: the scene and its camera push are that pattern's, with beadsAt replaced. Each bead has a position on the sphere, a position on the torus and a lag that runs from the top of the sphere to the bottom, so the sphere becomes a torus from the top down.

<div id="stage" data-composition-id="space"     data-start="0" data-duration="5" data-width="1280" data-height="720" data-fps="30">
    <div class="layer" data-component="space" data-start="0" data-duration="5">
        <canvas width="1280" height="720"></canvas>
    </div>
</div>
import * as THREE from 'three';

const { scene, ramp, mix, ease } = wildflower.kinetic;
wildflower.three.use(THREE);

const inOut = ease('power3.inOut');

const BEADS = 600;
const golden = Math.PI * (3 - Math.sqrt(5));
const RING = 2.4, TUBE = 0.6;
const SHAPES = Array.from({ length: BEADS }, (_, i) => {
    const y = 1 - 2 * (i + 0.5) / BEADS, r = Math.sqrt(1 - y * y), around = i * golden, tube = 2 * Math.acos(y);
    return {
        sphere: [Math.cos(around) * r * 2.2, y * 2.2, Math.sin(around) * r * 2.2],
        torus: [(RING + TUBE * Math.cos(tube)) * Math.cos(around), TUBE * Math.sin(tube), (RING + TUBE * Math.cos(tube)) * Math.sin(around)],
        lag: (1 - y) / 2 * 0.3,   // the morph runs from the top down
    };
});

// A color between two hex numbers.
function mixHex(a, b, p) {
    const ch = (s) => [(s >> 16) & 255, (s >> 8) & 255, s & 255];
    const [ar, ag, ab] = ch(a), [br, bg, bb] = ch(b);
    return (Math.round(mix(ar, br, p)) << 16) | (Math.round(mix(ag, bg, p)) << 8) | Math.round(mix(ab, bb, p));
}

// The beads at t: each blends from its sphere position to its torus position, its lag behind the top.
function beadsAt(t) {
    return SHAPES.map((b, i) => {
        const m = ramp(t, 1.6 + b.lag, 2.35 + b.lag, inOut);
        return {
            id: i,
            x: mix(b.sphere[0], b.torus[0], m),
            y: mix(b.sphere[1], b.torus[1], m),
            z: mix(b.sphere[2], b.torus[2], m),
            scale: 0.06,
            colour: mixHex(0x521a78, 0xaa3de6, m),
        };
    });
}

const camera = new THREE.PerspectiveCamera(40, 1280 / 720, 0.05, 100);

scene('space', {
    computed: {
        beads() { return beadsAt(this.local); },
    },
    init() {
        const renderer = new THREE.WebGLRenderer({ canvas: this.element.querySelector('canvas'), antialias: true, preserveDrawingBuffer: true });
        renderer.setSize(1280, 720, false);
        const world = new THREE.Scene();
        const mesh = new THREE.InstancedMesh(new THREE.SphereGeometry(1, 20, 14), new THREE.MeshBasicMaterial(), BEADS);
        world.add(mesh);

        const view = wildflower.three.view('space-view', { renderer, scene: world, camera, render: 'change' });
        view.instanced(() => this.beads, mesh, { position: ['x', 'y', 'z'], scale: 'scale', color: 'colour', pickable: false });
        renderer.compile(world, camera);
    },
    seek(local) {
        camera.position.set(0, 0, mix(9.5, 5.9, ramp(local, 0, 4.6, inOut)));
        camera.lookAt(0, 0, 0);
    },
});
html, body { margin: 0; background: #0a0b10; }

#stage { position: relative; overflow: hidden; background: #07060d; }

.layer { position: absolute; inset: 0; }

canvas { display: block; width: 1280px; height: 720px; }

[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }
Morphing a formation

Color blends through mixHex, which mixes the red, green and blue channels of two hex numbers, because three.js takes a color as a number and mix() returns a color as a string.

The space scene of the Showreel demo morphs its beads from a sphere to a torus to a logo this way.