Launch Title
The code of the Launch Title demo, 2 files.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Launch Title | WildflowerJS Demo</title>
<script defer src="/js/analytics.js"></script>
<!-- The framework, then Kinetic with its preview bar, then the scenes.
Kinetic runs the clock and the render itself. -->
<script defer src="/js/dist/wildflower.lite.min.js"></script>
<script defer src="/js/extensions/kinetic.wf.min.js" data-preview></script>
<script defer src="app.js"></script>
<style>
* { box-sizing: border-box; }
html, body { margin: 0; background: #0a0b10; color: #e6e6f0; font-family: system-ui, sans-serif; }
.demo-toolbar { display: flex; justify-content: space-between; align-items: center; padding: 8px 20px; background: #12141c; border-bottom: 1px solid rgba(255,255,255,0.08); }
.demo-toolbar a { display: flex; align-items: center; gap: 10px; text-decoration: none; color: #5fa6a6; font-size: 1rem; }
.demo-toolbar .logo { height: 32px; }
.demo-toolbar .brand-wf { font-weight: 520; }
.demo-toolbar .brand-js { font-weight: 700; }
.demo-toolbar .demo-info { font-size: 11px; color: rgba(255,255,255,0.6); text-align: right; }
.demo-toolbar .demo-info strong { color: rgba(255,255,255,0.9); }
/* The composition is 1280 by 720. With data-preview, Kinetic scales
the stage down to the window; the stage's layout stays 1280 by
720, so a render captures the layout the preview shows. */
/* Hidden until Kinetic marks the root ready: before that, the stage
holds unbound markup. */
#stage:not([data-kinetic-ready]) { visibility: hidden; }
.frame { padding: 24px; }
#stage { position: relative; width: 1280px; height: 720px; overflow: hidden; background: #0b0f1a; }
.layer { position: absolute; inset: 0; }
.bg { position: absolute; inset: 0; }
.dust { position: absolute; left: 0; top: 0; border-radius: 50%; background: #fff; }
.card { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
.title { margin: 0; font-size: 136px; font-weight: 800; letter-spacing: 0.02em; line-height: 1; color: #fff; display: flex; }
.ch { display: inline-block; white-space: pre; will-change: transform, opacity; }
.rule { height: 4px; border-radius: 2px; margin: 28px 0 22px; }
.sub { margin: 0; font-size: 34px; font-weight: 400; color: #c9c9dc; min-height: 1.3em; letter-spacing: 0.01em; }
.about { max-width: 1280px; margin: 0 auto; padding: 0 24px 96px; color: #a9adc2; font-size: 14px; line-height: 1.55; }
.about p { margin: 0 0 10px; }
.about b { color: #e6e6f0; font-weight: 600; }
.about code { color: #e6e6f0; font-size: 13px; }
</style>
</head>
<body>
<div class="demo-toolbar">
<a href="/demos/">
<img class="logo" src="/assets/wildflower_96.png" alt="WildflowerJS">
<span><span class="brand-wf">Wildflower</span><span class="brand-js">JS</span> Demos</span>
</a>
<div class="demo-info"><strong>Launch Title</strong>: a title card</div>
</div>
<div class="frame">
<div id="stage" data-composition-id="launch" data-start="0" data-duration="6" data-width="1280" data-height="720" data-fps="30"
data-composition-variables='[{"id":"title","type":"string","label":"Title","default":"WILDFLOWER"},{"id":"subtitle","type":"string","label":"Subtitle","default":"Every frame is a function of time."},{"id":"accent","type":"color","label":"Accent","default":"#9d7bff"}]'>
<div class="layer" data-component="launch-dust" data-start="0" data-duration="6">
<div data-pool="dust" data-key="id"><template><i class="dust" data-bind-style="style"></i></template></div>
</div>
<div class="layer" data-component="launch-title" data-start="0.4" data-duration="5.6">
<div class="bg" data-bind-style="bg"></div>
<div class="card" data-bind-style="card">
<h1 class="title" data-list="letters" data-key="key"><template><span class="ch" data-bind="text" data-bind-style="style"></span></template></h1>
<div class="rule" data-bind-style="rule"></div>
<p class="sub" data-bind="typed"></p>
</div>
</div>
</div>
</div>
<div class="about">
<p>Press play, or scrub. Every letter and every speck of dust is computed from the time.</p>
</div>
</body>
</html>
app.js
// Launch title: a composition where every frame is a function of time, in
// two scenes (the dust, the title). Kinetic scales the 1280 x 720 stage to
// the window, and in a render, or with ?embed, shows the stage alone.
const { scene, split, styleAt, stagger, typewriter, spring, ramp, random, noise } = wildflower.kinetic;
// ---- The dust: 240 motes, placed once, styled on every seek ---------------
const DUST = 240;
const SEED = 7;
scene('launch-dust', {
pools: { dust: {} },
init() {
const rnd = random(SEED);
for (let i = 0; i < DUST; i++) {
this.pools.dust.add({
id: i,
x: rnd() * 1280,
y: 120 + rnd() * 600,
size: 1.5 + rnd() * 3,
delay: rnd() * 1.6,
style: {},
});
}
},
seek(local) {
const fade = 1 - ramp(local, 5.4, 6.0);
this.pools.dust.forEach((d) => {
const up = spring(local - d.delay, { damping: 22 });
const drift = noise(d.id * 0.37 + local * 0.12, SEED) * 36;
this.pools.dust.update(d.id, {
style: {
width: d.size + 'px',
height: d.size + 'px',
...styleAt(local, {
opacity: Math.min(1, up) * 0.65 * fade,
translateX: d.x + drift,
translateY: d.y - up * 48,
}),
},
});
});
},
});
// ---- The title -----------------------------------------------------------
// Each letter fades in over 0.35 s, rises from 48 px below with a spring,
// and settles its scale over 0.6 s. styleAt() reads it at each letter's own
// offset.
const RISE = {
opacity: { at: [0, 0.35], to: [0, 1] },
translateY: { at: [0, 0.6], to: [48, 0], ease: 'spring' },
scale: { at: [0, 0.6], to: [0.9, 1], ease: 'power2.out' },
};
scene('launch-title', {
inputs: {
title: { type: String, default: 'WILDFLOWER' },
subtitle: { type: String, default: 'Every frame is a function of time.' },
accent: { type: String, default: '#9d7bff' },
},
edges: { enter: 0.3, exit: 0.6 },
keyframes: {
bg: { background: { at: [0, 2.6, 5.6], to: ['#0b0f1a', '#181030', '#0b0f1a'] } },
ruleWidth: { at: [1.0, 1.7], to: [0, 480], ease: 'power2.out', unit: 'px' },
},
computed: {
letters() {
const rows = split(this.inputs.title, 'letter');
return rows.map((r) => ({
key: r.key,
text: r.text,
style: r.space ? {} : styleAt(this.local - 0.2 - stagger(r.i, rows.length, { each: 0.06 }), RISE),
}));
},
rule() { return { width: this.ruleWidth, background: this.inputs.accent }; },
typed() { return typewriter(this.local - 1.5, this.inputs.subtitle, 28); },
card() { return { opacity: String(this.presence) }; },
},
});