Daylily

The code of the Daylily demo, 2 files.

Run the demo All demos

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