A landing on a whole frame

A ball hangs in the air, drops to a line on the floor, and bounces twice more, each bounce shorter, squashing flat on every landing. A render seeks to whole frames (a frame number divided by the frame rate), so a contact between two frames is never drawn. The frames either side show it above the floor and already rising.

<div id="stage" data-composition-id="main"     data-start="0" data-duration="2" data-width="1280" data-height="720" data-fps="30">
    <div class="layer" data-component="ball" data-start="0" data-duration="2">
        <div style="position: absolute; left: 0; top: 470px; width: 1280px; height: 2px; background: #888"></div>
        <div style="position: absolute; left: -22px; top: -44px; width: 44px; height: 44px; border-radius: 50%; background: #aa3de6; transform-origin: 50% 100%" data-bind-style="ball"></div>
    </div>
</div>
const { scene, styleAt } = wildflower.kinetic;

const FPS = 30;         // the root's data-fps="30"; the arcs below are worked out before any scene runs, so this.fps is not there yet
const FLOOR = 470;      // px, the line the ball lands on
const HANG = 128;       // px, the ball's bottom edge before it is let go
const DROP = 9;         // let go at frame 9
const FALL = 10;        // and falls for 10 frames
const ARCS = [6, 3];    // then bounces of 6 and 3 frames
const G = 2 * (FLOOR - HANG) / ((FALL / FPS) ** 2);   // px/s²

const LANDS = [DROP + FALL];   // frames, each landing
const LAUNCH = [];             // px/s, each bounce's launch speed
for (const n of ARCS) {
    LAUNCH.push(G * (n / FPS) / 2);                    // an arc of n frames lasts 2 * speed / G
    LANDS.push(LANDS[LANDS.length - 1] + n);
}

// The ball's bottom edge at frame f: held, then falling, then bouncing.
function ballY(f) {
    if (f <= DROP) return HANG;
    if (f < LANDS[0]) { const u = (f - DROP) / FPS; return HANG + 0.5 * G * u * u; }
    for (let k = 0; k < ARCS.length; k++) {
        if (f < LANDS[k + 1]) { const u = (f - LANDS[k]) / FPS; return FLOOR - (LAUNCH[k] * u - 0.5 * G * u * u); }
    }
    return FLOOR;
}

scene('ball', {
    computed: {
        ball() {
            const f = this.clipFrame;
            let squash = 0;
            for (const at of LANDS) if (f >= at) squash += 0.4 * Math.max(0, 1 - (f - at) / (0.09 * FPS));
            return styleAt(this.local, { translateX: 640, translateY: ballY(f), scaleX: 1 + 0.8 * squash, scaleY: 1 - squash });
        },
    },
});
html, body { margin: 0; background: #0a0b10; }

#stage { position: relative; overflow: hidden; background: #0c0c10; }

.layer { position: absolute; inset: 0; }

[data-composition-id]:not([data-kinetic-ready]) { visibility: hidden; }
A landing on a whole frame

Here the ball is let go at frame 9, falls for 10 frames and then bounces for 6 and 3, so every landing is a whole frame. Each arc's launch speed is computed from its length, so the arc ends exactly on the floor.

The scene counts in frames with this.clipFrame, the whole-number frame within the clip, so f >= LANDS[0] is true from the landing frame on, with no tolerance for the rounding that a time in seconds such as 19 / 30 carries. The squash is a function of the frames since the landing, so it is largest on the contact frame.

The opening scene of the Showreel demo times its bouncing ball this way.