Daylily
The code of the Daylily 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>Daylily | 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, the live finish 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. */
#stage:not([data-kinetic-ready]) { visibility: hidden; }
.frame { padding: 24px; }
#stage { position: relative; width: 1280px; height: 720px; overflow: hidden; background: #14121c; }
.layer { position: absolute; inset: 0; }
.bg { background: radial-gradient(ellipse at 50% 60%, #2a2238 0%, #14121c 70%); }
/* The title: letters swept in, each row an inline block so its transform takes. */
.title { position: absolute; left: 0; right: 0; top: 232px; margin: 0; text-align: center; font-size: 150px; font-weight: 800; letter-spacing: 0.04em; line-height: 1; color: #f2efe6; }
.sub { position: absolute; left: 0; right: 0; top: 404px; margin: 0; text-align: center; font-size: 34px; font-weight: 400; letter-spacing: 0.08em; color: #c9c3b8; }
.ch { display: inline-block; white-space: pre; will-change: transform, opacity; }
.ch i { display: inline-block; font-style: normal; }
/* The fan: fourteen petals, each the one before it turned a little more, about the fan's base. */
.fan { position: absolute; left: 640px; top: 600px; }
.petal { position: absolute; left: -16px; top: -230px; width: 32px; height: 230px; border-radius: 16px 16px 6px 6px; background: linear-gradient(#f5c542, #e0702f); }
/* The photo, revealed through a luma matte that a track slides across. */
.photo { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: 50% 42%;
mask-image: linear-gradient(to right, #fff 44%, #000 56%); mask-mode: luminance;
mask-size: 200% 100%; mask-repeat: no-repeat; -webkit-mask-image: linear-gradient(to right, #fff 44%, #000 56%); -webkit-mask-size: 200% 100%; -webkit-mask-repeat: no-repeat; }
.caption { position: absolute; left: 80px; bottom: 72px; margin: 0; font-size: 44px; font-weight: 300; letter-spacing: 0.12em; color: #f2efe6; }
/* The card through four points, and the adjustment layer that slides in behind it. */
.adjust { position: absolute; top: 0; bottom: 0; left: 100%; right: 0; backdrop-filter: grayscale(1) contrast(1.25); }
.card { position: absolute; left: 0; top: 0; padding: 18px 26px; border-radius: 8px; background: #f2efe6; color: #14121c; font-size: 30px; font-weight: 700; letter-spacing: 0.06em; box-shadow: 0 18px 40px rgba(0,0,0,0.45); }
.card small { display: block; font-size: 16px; font-weight: 400; letter-spacing: 0.1em; color: #6b6558; }
/* Paper over everything by multiply, and a light leak by screen, the paper a generated texture. */
.paper, .leak { position: absolute; inset: 0; pointer-events: none; }
.paper { mix-blend-mode: multiply;
background: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='400' height='400'><filter id='p'><feTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' seed='7'/><feColorMatrix values='0 0 0 0 0.9 0 0 0 0 0.87 0 0 0 0 0.8 0 0 0 0.3 0.7'/></filter><rect width='400' height='400' fill='white'/><rect width='400' height='400' filter='url(%23p)'/></svg>");
background-size: 400px 400px; }
/* The leak is larger than the stage, so its edge never shows as it drifts. */
.leak { inset: -320px; mix-blend-mode: screen; background: radial-gradient(circle at 34% 36%, rgba(255, 150, 70, 0.8), rgba(255, 110, 50, 0) 40%); }
.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>Daylily</strong>: a title study</div>
</div>
<div class="frame">
<div id="stage" data-composition-id="daylily" data-start="0" data-duration="15" data-width="1280" data-height="720" data-fps="30">
<div class="layer bg"></div>
<div class="layer" data-component="daylily-photo" data-start="8" data-duration="5.6">
<img class="photo" src="/media/daylily.jpg" alt="" data-bind-style="matte">
<p class="caption" data-list="caption" data-key="key"><template><span class="ch" data-bind="text" data-bind-style="style"></span></template></p>
</div>
<div class="layer" data-component="daylily-title" data-start="0" data-duration="5">
<h1 class="title" data-list="letters" data-key="key"><template><span class="ch" data-bind="text" data-bind-style="style"></span></template></h1>
<p class="sub" data-list="sub" data-key="key"><template><span class="ch" data-bind-style="style"><i data-bind="text" data-bind-style="wave"></i></span></template></p>
</div>
<div class="layer" data-component="daylily-fan" data-start="4.4" data-duration="4.6">
<div class="fan" data-list="petals" data-key="key"><template><div class="petal" data-bind-style="style"></div></template></div>
</div>
<div class="layer" data-component="daylily-card" data-start="11.4" data-duration="3.6">
<div class="adjust" data-bind-style="adjust"></div>
<div class="card" data-bind-style="card">DAYLILY <small>Hemerocallis, in the garden</small></div>
</div>
<div class="layer" data-component="daylily-paper" data-start="0" data-duration="15">
<div class="paper"></div>
<div class="leak" data-bind-style="leak"></div>
</div>
</div>
</div>
<div class="about">
<p>Press play, or scrub. The grain, vignette and dust are Kinetic's finish, set once in <code>config()</code>.</p>
</div>
</body>
</html>
app.js
// Daylily: a title study in five scenes. The finish is on, so the live page
// shows the grain, the vignette and the dust and the file carries the bloom
// as well; the shutter is on, so the fast moves blur as a camera's would.
// Every value is a function of the time.
const { scene, split, styleAt, sweep, repeat, ramp, interpolate } = wildflower.kinetic;
wildflower.kinetic.config({
shutter: 180,
finish: {
grain: 0.22,
vignette: 0.32,
bloom: { amount: 0.25, threshold: 0.86, radius: 40 },
dust: 0.25,
grade: { lift: 0.03, saturation: 0.95 },
},
});
// A letter's style from its weight under a sweep: the weight is the time
// of these one-second tracks, so 0 is hidden and below, 1 is in place.
const RISE = {
opacity: { at: [0, 1], to: [0, 1] },
translateY: { at: [0, 1], to: [40, 0] },
blur: { at: [0, 1], to: [6, 0] },
};
// The wave through the second line: a lift and a warm colour that pass.
const WAVE = {
translateY: { at: [0, 1], to: [0, -14] },
color: { at: [0, 1], to: ['#c9c3b8', '#f5c542'] },
};
// Letters swept in by one window and out by another: the weight is the
// smaller of the way in and what is left of the way out.
const weight = (i, n, pin, pout, width) => Math.min(sweep(i, n, pin, { width, ease: 'power2.out' }), 1 - sweep(i, n, pout, { width: 0.6, from: 'last' }));
// ---- The title -----------------------------------------------------------
scene('daylily-title', {
computed: {
letters() {
const rows = split('DAYLILY');
const pin = ramp(this.local, 0.3, 1.5, 'power1.inOut');
const pout = ramp(this.local, 4.0, 4.6, 'power2.in');
return rows.map((r) => ({ key: r.key, text: r.text, style: styleAt(weight(r.i, rows.length, pin, pout, 0.45), RISE) }));
},
sub() {
const rows = split('a title study');
const pin = ramp(this.local, 1.2, 2.2, 'power1.inOut');
const wave = ramp(this.local, 2.5, 3.7);
const pout = ramp(this.local, 4.0, 4.5, 'power2.in');
return rows.map((r) => ({
key: r.key,
text: r.text,
style: r.space ? {} : styleAt(weight(r.i, rows.length, pin, pout, 0.35), RISE),
wave: r.space ? {} : styleAt(sweep(r.i, rows.length, wave, { width: 0.5, shape: 'triangle', ease: 'power1.inOut' }), WAVE),
}));
},
},
});
// ---- The fan -------------------------------------------------------------
// Fourteen petals, each the one before it turned `open` degrees more about
// the fan's base; the offset of -7 puts the original in the middle, so the
// fan opens both ways. It settles open with a little overshoot and closes
// faster than it opened.
scene('daylily-fan', {
computed: {
petals() {
const open = interpolate(this.local, [0.2, 1.7, 3.4, 4.1], [0, 11, 11, 0], { ease: ['back.out', 'linear', 'power2.in'] });
const alpha = interpolate(this.local, [0, 0.4, 3.7, 4.2], [0, 1, 1, 0]);
return repeat(14, { rotate: open, scale: 0.975, opacity: [alpha, alpha * 0.35], offset: -7, origin: '50% 100%' });
},
},
});
// ---- The photo -----------------------------------------------------------
// The matte's gradient is twice the photo's width; at 100% its dark half
// covers the photo, at 0% its bright half does, and the edge between them
// sweeps across.
scene('daylily-photo', {
keyframes: {
matte: {
maskPosition: { at: [0.3, 1.9], to: [100, 0], unit: '%', ease: 'power2.inOut' },
opacity: { at: [4.9, 5.5], to: [1, 0], ease: 'power2.in' },
},
},
computed: {
caption() {
const rows = split('Hemerocallis');
const pin = ramp(this.local, 1.7, 2.9, 'power1.inOut');
const pout = ramp(this.local, 4.6, 5.1, 'power2.in');
return rows.map((r) => ({ key: r.key, text: r.text, style: styleAt(weight(r.i, rows.length, pin, pout, 0.4), RISE) }));
},
},
});
// ---- The card ------------------------------------------------------------
// Through four points on one smooth track: the card passes each without
// stopping and comes to rest at the last. The adjustment layer slides in
// from the right behind it and grades half the frame.
scene('daylily-card', {
keyframes: {
card: {
translateX: { at: [0.3, 1.2, 2.1, 3.0], to: [160, 540, 760, 980], ease: 'smooth' },
translateY: { at: [0.3, 1.2, 2.1, 3.0], to: [470, 200, 460, 280], ease: 'smooth' },
rotate: { at: [0.3, 3.0], to: [-6, 5], ease: 'smooth' },
opacity: { at: [0, 0.35, 3.1, 3.5], to: [0, 1, 1, 0] },
},
adjust: {
left: { at: [0.4, 2.0], to: [100, 50], unit: '%', ease: 'power2.inOut' },
opacity: { at: [3.1, 3.5], to: [1, 0] },
},
},
});
// ---- Paper and a light leak ----------------------------------------------
scene('daylily-paper', {
keyframes: {
leak: {
translateX: { at: [0, 15], to: [-260, 260] },
translateY: { at: [0, 15], to: [60, -60] },
opacity: { at: [0, 1.5, 13.5, 15], to: [0, 0.9, 0.9, 0] },
},
},
});