Composition
A composition is just a value, so a let-bound compose [...] can be placed
inside another compose — with its own transform, and its animations sampled
independently in each spot. That includes whole examples: each of the four
cells here is the kind of scene shown elsewhere in this gallery.
This example arranges four sub-compositions in a 2×2 grid (each scaled and
translated into a quadrant): an icon orbit, the viking sprite cycle, the
bouncing earth, and the spinning homepage glyph — 2D and 3D layers freely
mixed. The lava lamp sits in the lower centre (a raymarched metaballs field
is a full-frame effect, so it’s placed by its blob coordinates rather than a
cell transform). A full-scene particles layer then sparkles over the whole
grid, and the title example — the word cmotion, each letter spinning in — is
dropped in the centre as the last layer, so it composites on top of everything
else.
runner "0.0.1";
use std.shapes.*;
use std.anim.*;
use std.mesh3d.*;
use std.text;
use std.lighting.*;
use std.scene3d.*;
scene composition(duration: Duration = 8s) -> Frame {
let bg = rect(width: 1920px, height: 1080px, fill: #0b0f1a);
// Cell A — the Icons example: a globe with six icons orbiting it,
// spaced 60° apart via wave phase (x a quarter-turn ahead of y).
let spin = 8s;
let cellA = compose [
icon("globe", size: 220px, color: #38bdf8),
icon("smartphone", size: 110px, color: #e2e8f0).translate(x: wave(amplitude: 300px, period: spin, phase: 90deg), y: wave(amplitude: 300px, period: spin, phase: 0deg)),
icon("satellite", size: 110px, color: #a3e635).translate(x: wave(amplitude: 300px, period: spin, phase: 150deg), y: wave(amplitude: 300px, period: spin, phase: 60deg)),
icon("dollar-sign", size: 110px, color: #fbbf24).translate(x: wave(amplitude: 300px, period: spin, phase: 210deg), y: wave(amplitude: 300px, period: spin, phase: 120deg)),
icon("sword", size: 110px, color: #f87171).translate(x: wave(amplitude: 300px, period: spin, phase: 270deg), y: wave(amplitude: 300px, period: spin, phase: 180deg)),
icon("ice-cream-cone", size: 110px, color: #f9a8d4).translate(x: wave(amplitude: 300px, period: spin, phase: 330deg), y: wave(amplitude: 300px, period: spin, phase: 240deg)),
icon("users", size: 110px, color: #60a5fa).translate(x: wave(amplitude: 300px, period: spin, phase: 390deg), y: wave(amplitude: 300px, period: spin, phase: 300deg)),
];
// Cell B — the viking sprite, reused. Like the standalone example, step
// through all 16 cells (idle / walk / attack / death), hold the death
// frame, then fade out and loop — so the full sheet plays, not just walk.
let cycle = animate { 0s => 0, 0.7s => 0, 3.5s => 15, 5s => 15 };
let fade = animate { 0s => 0, 0.4s => 1, 5s => 1, 6s => 0, 8s => 0 };
let cellB = compose [
sprite(image("/img/viking-transparent.png"), width: 700px, height: 700px, cols: 4, rows: 4, frame: cycle, anchor: center, opacity: fade),
];
// Cell C — the bouncing-ball example reused (earth sphere, bounce + squash).
let spinB = animate { 0s => 0deg, 6s => 360deg } with { repeat: forever };
let bounceY = bounce(height: 520px, period: 1.2s, floor: -300px);
let squashB = on_event(bounceY.impacts, decay: 0.18s, peak: 0.35);
let ball = sphere(r: 190px)
.material(fill: image("/img/earth_4k.jpg").as_texture(projection: equirectangular))
.rotate(y: spinB).pivot(bottom).squash(factor: squashB);
let cellC = compose [
render3d(ball.translate(y: bounceY.position),
lights: [ ambient(0.35), directional(from: vec3(2, 3, 4), intensity: 1.0) ]),
];
// Cell D — the homepage glyph reused (extruded "C", spin + hue cycle).
// Every period divides the 8s scene loop so the cell wraps seamlessly:
// one full y-turn over 8s, the x-wave over 8s, hue over 4s, pulse over 1s.
let rotG = animate { 0s => 0deg, 8s => 360deg } with { repeat: forever };
let hueG = animate { 0s => 280deg, 4s => 640deg } with { repeat: forever };
let pulseG = animate { 0s => 1.00, 500ms => 1.06, 1s => 1.00 } with { easing: easing.out_cubic, repeat: forever };
let glyph = extrude(text.glyph("C", font: "Inter Bold"), depth: 16px)
.material(fill: oklch(0.78, 0.20, hueG), metalness: 0.25, roughness: 0.35,
emissive: oklch(0.65, 0.18, hueG), emissive_intensity: 0.6)
.rotate(x: wave(amplitude: 8.6deg, period: 8s), y: rotG).scale(pulseG);
let cellD = compose [
render3d(glyph, lights: [ ambient(0.35), directional(from: vec3(3, 4, 5), intensity: 1.6), directional(from: vec3(-4, -2, -3), intensity: 0.9) ]),
];
// Centre piece — the Title example reused: the word "cmotion", each letter
// its own colour, spinning in on a 0.3s stagger; the "i" hops. No tile bg.
let r0 = animate { 0s => 0deg, 2.0s => 360deg, 8s => 360deg } with { easing: easing.in_out_cubic };
let r1 = animate { 0s => 0deg, 0.3s => 0deg, 2.3s => 360deg, 8s => 360deg } with { easing: easing.in_out_cubic };
let r2 = animate { 0s => 0deg, 0.6s => 0deg, 2.6s => 360deg, 8s => 360deg } with { easing: easing.in_out_cubic };
let r3 = animate { 0s => 0deg, 0.9s => 0deg, 2.9s => 360deg, 8s => 360deg } with { easing: easing.in_out_cubic };
let r5 = animate { 0s => 0deg, 1.5s => 0deg, 3.5s => 360deg, 8s => 360deg } with { easing: easing.in_out_cubic };
let r6 = animate { 0s => 0deg, 1.8s => 0deg, 3.8s => 360deg, 8s => 360deg } with { easing: easing.in_out_cubic };
let ijump = animate { 0s => 30px, 1.3s => 250px, 2.1s => 30px, 2.8s => 165px, 3.5s => 30px, 8s => 30px } with { easing: easing.in_out_cubic };
let t0 = extrude(text.glyph("c", size: 430px), depth: 43px).material(fill: oklch(0.70, 0.20, 25)).rotate(y: r0).translate(x: -567px);
let t1 = extrude(text.glyph("m", size: 430px), depth: 43px).material(fill: oklch(0.83, 0.16, 92)).rotate(y: r1).translate(x: -313px);
let t2 = extrude(text.glyph("o", size: 430px), depth: 43px).material(fill: oklch(0.70, 0.19, 150)).rotate(y: r2).translate(x: -59px);
let t3 = extrude(text.glyph("t", size: 430px), depth: 43px).material(fill: oklch(0.66, 0.16, 245)).rotate(y: r3).translate(x: 108px);
let t4 = extrude(text.glyph("i", size: 430px), depth: 43px).material(fill: oklch(0.70, 0.23, 350)).translate(x: 225px, y: ijump);
let t5 = extrude(text.glyph("o", size: 430px), depth: 43px).material(fill: oklch(0.74, 0.20, 55)).rotate(y: r5).translate(x: 368px);
let t6 = extrude(text.glyph("n", size: 430px), depth: 43px).material(fill: oklch(0.72, 0.17, 195)).rotate(y: r6).translate(x: 570px);
let title = render3d(compose [t0, t1, t2, t3, t4, t5, t6],
lights: [ ambient(0.45), directional(from: vec3(2, 4, 6), intensity: 1.6), directional(from: vec3(-4, -2, 4), intensity: 0.8) ]);
// Lava lamp, centre-bottom — the metaballs example reused. A raymarched SDF
// field is a full-frame effect (its blobs live in world space, the quad fills
// the frame and misses discard), so it's placed by its blob coordinates
// rather than a cell transform: a compact cluster bobbing in the lower centre,
// below the title and between the two bottom cells. 4s bob loops within 8s.
let lava = metaballs([
blob(at: vec3( 4px, animate { 0s => -317px, 4s => -359px, 8s => -317px } with { easing: easing.in_out_cubic }, 0px), radius: 105px),
blob(at: vec3(-118px, animate { 0s => -267px, 4s => -217px, 8s => -267px } with { easing: easing.in_out_cubic }, 0px), radius: 67px),
blob(at: vec3( 126px, animate { 0s => -334px, 4s => -284px, 8s => -334px } with { easing: easing.in_out_cubic }, 0px), radius: 71px),
blob(at: vec3( -67px, animate { 0s => -431px, 4s => -376px, 8s => -431px } with { easing: easing.in_out_cubic }, 0px), radius: 63px),
blob(at: vec3( 97px, animate { 0s => -448px, 4s => -393px, 8s => -448px } with { easing: easing.in_out_cubic }, 0px), radius: 61px),
blob(at: vec3(-197px, animate { 0s => -313px, 4s => -368px, 8s => -313px } with { easing: easing.in_out_cubic }, 0px), radius: 50px),
blob(at: vec3( 210px, animate { 0s => -368px, 4s => -301px, 8s => -368px } with { easing: easing.in_out_cubic }, 0px), radius: 48px),
], smoothing: 38px).material(roughness: 0.18);
// Reuse the four cells in a 2×2 grid, drop the lava lamp in the lower centre,
// sparkle over the whole scene, then the title on top of every other layer.
compose [
bg,
cellA.scale(0.46).translate(x: -480px, y: 264px),
cellB.scale(0.46).translate(x: 480px, y: 264px),
cellC.scale(0.46).translate(x: -480px, y: -264px),
cellD.scale(0.46).translate(x: 480px, y: -264px),
lava,
particles(kind: magic_sparks, count: 220),
title.scale(0.50),
]
}