Skip to content

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.

rendering…
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),
  ]
}