GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

JavaScript Piano: 9 Scenes, Keys to Strings

In a javascript piano, when a key goes down, how should the string shake? 03 sizes each overtone by where the hammer lands, 06 makes strings with matching

From a Practice Room Screen to a Concert Stage — Nine Scenes Where Keys, Hammers and Strings Move Together

Auto-plays · click a tile to jump to its section · all nine in one zip

A browser piano of this kind is a web page where you press drawn keys with a mouse, a finger or the computer keyboard, and it shows the pressed key and the shaking string as pictures. Left alone, the nine scenes play preset notes on their own in silence. Nothing is heard until the "Sound on" button is pressed, and from then on the JavaScript piano sound is four sine waves stacked for each note. The first touch on a key or on the sound button drops the demo mode, and after 2.5 seconds with no human input the demo notes pick up again.

This runs the opposite way from the audio visualizer, which splits incoming sound into bars. There the sound comes first and the picture follows it; here the key depth, the hammer angle and the string shape are computed first, and sound follows that computation only once it is switched on.

Practice Keyboards and Lesson Material

For lesson material on chord names, the chord circle in 05 and the staff in 08 are the closest fit. C–E–G, C–E–A and C–F–A are triangles with the same three side lengths on the circle, so each covers 37.7% of it and only the side it leans toward changes, which lets a single picture show that a major and a minor chord share one frame. In 08 the played note flies up from its key and lands on a line or space of the staff, so someone who has just started reading music gets to pair each key position with a staff height.

A Showcase Screen and a Small Phone

On the first screen of a landing page, the falling bars in 04 or the perspective stage in 09 catch the eye. Stretch the board to 88 keys, though, and phone width causes trouble at once; how 07 copes with it is worked through with numbers in the section on where things break.

JavaScript Piano: 9 Scenes — Spring Keys, a Hammer Lever, String Overtones, a Piano Roll, a Chord Circle, Sympathetic Strings, a Glissando Sweep, Notes on a Staff and an Autoplay Stage

Every scene was captured at 480×300 as 24 shots across 2 seconds, and the table below adds up the 23 differences between neighboring shots. In each of the nine scenes, all 23 of those comparisons showed a change. The fingertip sweeping over 88 keys in 07 covered the widest area at 49.8%, and 06, where only the matching strings light up, the narrowest at 10.4%.

Changed Area and Intensity for Each Scene

Scene Cumulative area Moved frames Mean intensity
01 Spring-back 3D keys 19.0% 23/23 45.4
02 Hammer cross-section 15.6% 23/23 69.9
03 String overtones 16.6% 23/23 77.7
04 Piano roll 44.7% 23/23 56.0
05 Chord circle 23.3% 23/23 67.8
06 Sympathetic strings 10.4% 23/23 31.1
07 Glissando 49.8% 23/23 130.8
08 Notes on a staff 25.6% 23/23 65.5
09 Perspective stage 22.4% 23/23 40.2

Why Does a JavaScript Piano Keyboard Look Like a Row of Flat Rectangles, and What Gives It Depth?

Build the piano keys CSS-only, as white and navy boxes that swap color on press, and a viewer can tell that a key went down but not how deep it went or how hard it was hit. The keys in this set carry depth in place of a color change. In 01 the front end of a key tilts around its back end and drops 16 units, and a spring returns that depth when the key is let go.

Tilt and Shade Build the Depth

Pressed all the way, a white key tilts 6.12° and a black key 10.02°. A black key is only 92 from its pivot, against 150 for a white key, so the same 16-unit drop tilts it more steeply. When a neighboring white key's front end sits higher than yours, a shade triangle as wide as that height gap times 1.6 falls across your top face, and a fully pressed key beside a resting neighbor gets a shade 25.6 wide.

Where Loudness Becomes Visible

With a mouse or a finger, loudness climbs from 0.45 to 1 the lower on the key you press. 01 adds loudness × 14 of extra speed at the moment of the press, so a key pressed at 0.8 reaches the key bed in 80 ms. A plain color swap has nowhere to show that difference.

013D Keys That Spring Back from a Press

Thirteen keys are viewed from a height of 250, looking down by 0.7 radians, so their front ends land at screen y 228.5 and their back ends at y 141.9, while a graph at the top left keeps 1.92 seconds of press depth for the last key played. The stepSpring function below pulls the position toward its target with stiffness 420 and damping 16, and past the floor at 1 it flips the speed and keeps 0.3 of it. With a damping ratio of 0.39, a released key first crosses its rest position at 96 ms, overshoots upward by 23.9% of its travel and stays within 2% of rest from 384 ms on.

3D Keys That Spring Back from a Press
Open in a new tab

01 · 3D Keys That Spring Back from a Press — Measured area 19.043% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

KEY_SPRING_STIFFNESSKEY_TRAVELSHADOW_SLOPE
    function stepSpring(s, k, c, dt) {
        const a = -k * (s.x - s.target) - c * s.v;
        s.v += a * dt;
        s.x += s.v * dt;
        if (s.x > 1) {
            s.x = 1;
            s.v = -s.v * KEYBED_BOUNCE;
        }
    }

02A Hammer Cross-Section That Strikes the String and Rebounds

While the key's front end travels 12 px, a lever ratio of 4.412, that is (back arm 120 ÷ front arm 170) × (shank 150 ÷ butt arm 24), drives the hammer head along its arc at 4.4 times the key speed, and once let off at 0.27 radians the hammer flies under gravity alone. In the snippet below it rebounds with its angular speed multiplied by −0.45 on reaching the string angle 0.4334, and if it turns back short of the string the panel shows "Too soft — falls back before the string"; a struck key travels all the way down even when released early, so the split depends on loudness and not on how long the key is held, with 0.406 and below missing and 0.407 and above landing. The demo hits at 0.95 for a 2.36 rad/s rebound, plays 0.3 once the hammer has settled, which peaks at 0.3566 and shows that message once per flight, and closes with 0.65, while a human press has its loudness squared, so pressing roughly the top 34% of a key button falls short of the string, though with sound on the note still plays at any loudness.

A Hammer Cross-Section That Strikes the String and Rebounds
Open in a new tab

02 · A Hammer Cross-Section That Strikes the String and Rebounds — Measured area 15.556% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LEVER_RATIOTHETA_LETOFFRESTITUTION
        const wasRising = omega > 0;
        omega -= GRAVITY / SHANK_LEN * Math.cos(theta) * dt;
        theta += omega * dt;
        apex = Math.max(apex, theta);
        if (theta >= THETA_STRING && omega > 0) {
            theta = THETA_STRING;
            const hitW = omega;
            omega = -RESTITUTION * omega;
            hitThisFlight = true;
            eventText = HIT_LABEL.replace('{w}', hitW.toFixed(1)).replace('{r}', Math.abs(omega).toFixed(1));
            eventAt = clock;
            stringAmp = Math.min(34, hitW * SHANK_LEN * STRING_GAIN);
            stringAt = clock;
        }
        // One verdict per flight — so "Too soft" does not show again when the hammer bounces off the check and comes back down
        if (wasRising && omega <= 0 && !hitThisFlight) {
            eventText = MISS_LABEL;
            eventAt = clock;
            hitThisFlight = true;
        }

03A String Whose Overtones Depend on Where It Was Struck

When the hammer strikes 1/8 of the way along the string, overtone n has the size sin(nπ × 0.125) ÷ n, so overtone 1 is 0.3827, overtone 2 is 0.3536 and overtone 4 is 0.25, while overtones 8 and 16, which have a node at that point, fall to 0 and the panel reads "size 0 (node at strike point): 8 · 16". Decay time shortens from 2600 ms for overtone 1 to 536 ms for overtone 8 and 281 ms for overtone 16, so after 600 ms overtone 1 keeps 0.794 of its size and overtone 8 keeps 0.327, and the string tidies into a single bow. The shape leans toward the strike point, with mirrored points on the left and right differing by 45.71 px at 120 ms and by 24.91 px at 600 ms.

A String Whose Overtones Depend on Where It Was Struck
Open in a new tab

03 · A String Whose Overtones Depend on Where It Was Struck — Measured area 16.561% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

STRIKE_POINTDECAY_BASE_MSpartialSizes
    function partialSizes(p) {
        const out = [];
        for (let n = 1; n <= PARTIALS; n += 1) {
            out.push(Math.sin(n * Math.PI * p) / n);
        }
        return out;
    }
    function decayMs(n) { return DECAY_BASE_MS / (1 + DECAY_SLOPE * (n - 1)); }

04Piano Roll Bars Falling Toward the Keys

At tempo 112 one beat lasts 535.71 ms, and with a fall speed of 150 px per second a one-beat bar is 80.36 px tall, so each bar appears at the top 1.507 seconds before its note starts. The step code below compares the lower end of every bar at the previous step and the current one, pressing the key when that end crosses the key line at y 226 and releasing it when the upper end crosses, so a bar that jumps over the line within one 16 ms step is still caught. At contact seven sparks fly off and a 150 px column of light rises over the pressed key.

Piano Roll Bars Falling Toward the Keys
Open in a new tab

04 · Piano Roll Bars Falling Toward the Keys — Measured area 44.665% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

TEMPO_BPMFALL_PX_PER_SECbarBottom
        songPrev = songMs;
        songMs = clock - LEAD_IN_MS;
        // Press when a bar's lower end crosses the key line, release when its upper end crosses — compare the previous and current laps
        const lap = Math.floor(songMs / LOOP_MS);
        for (let k = lap - 1; k <= lap; k += 1) {
            for (let i = 0; i < SONG.length; i += 1) {
                const n = SONG[i];
                const st = n[0] * BEAT_MS + k * LOOP_MS;
                const b0 = barBottom(st, songPrev);
                const b1 = barBottom(st, songMs);
                const len = barLengthPx(n[2]);
                if (b0 - len < KEY_LINE_Y && b1 - len >= KEY_LINE_Y) {
                    release(n[1], true);
                }
                if (b0 < KEY_LINE_Y && b1 >= KEY_LINE_Y && autoAllowed()) {
                    press(n[1], n[3], true);
                }
            }
        }

05A Chord Circle That Connects the Sounding Notes

The twelve notes sit 30° apart, clockwise from C at twelve o'clock, the sounding notes are joined into a polygon, and polygonArea gets its area from a sum of cross products, the same cross products that weight the average giving the center of mass. C–E–G, C–E–A and C–F–A each cover 37.7% of the circle with their center of mass 16.6 px from the middle, leaning toward F, C and A in turn, while D–F–G–B covers 59.4%. When the chord changes, the corners travel on a spring with stiffness 170 and damping 11, overshooting by 28.95 px on the way from the C spot to the A spot and staying within ±0.5 px of it from 880 ms on.

A Chord Circle That Connects the Sounding Notes
Open in a new tab

05 · A Chord Circle That Connects the Sounding Notes — Measured area 23.285% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

VERTEX_STIFFNESSpolygonAreapolygonCentroid
    function pcAngle(pc) { return -Math.PI / 2 + pc * TAU / 12; }
    function pcPoint(pc) { return [CIRCLE_X + CIRCLE_R * Math.cos(pcAngle(pc)), CIRCLE_Y + CIRCLE_R * Math.sin(pcAngle(pc))]; }
    // Shoelace formula — walk the corners in order; half the sum of x·y cross products is the area, and the average weighted by the same cross products is the center of mass
    function polygonArea(ps) {
        let a = 0;
        for (let i = 0; i < ps.length; i += 1) {
            const p = ps[i];
            const q = ps[(i + 1) % ps.length];
            a += p[0] * q[1] - q[0] * p[1];
        }
        return a / 2;
    }

06Sympathetic Strings That Ring Along with Matching Overtones

Under a bank of 24 strings, one per semitone from C3 to B4, striking C2 measures the gap in cents between each string and overtones 1 to 8, divides the resonance curve 1 ÷ (1 + (cents ÷ 30)²) by the overtone number and keeps the largest result. That gives C3 0.5 from overtone 2 at 0¢, G3 0.332 from overtone 3 at −2¢, C4 0.25, and E4 0.166 from overtone 5 at 13.7¢, and the 7 strings above 0.04 glow. A string ringing along fills in slowly with a 260 ms time constant and fades with an 1,800 ms one.

Sympathetic Strings That Ring Along with Matching Overtones
Open in a new tab

06 · Sympathetic Strings That Ring Along with Matching Overtones — Measured area 10.364% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

RESONANCE_WIDTHcentsBetweenresponseOf
    function centsBetween(f1, f2) { return 1200 * Math.log(f1 / f2) / Math.LN2; }
    function resonanceCurve(cents) { const k = cents / RESONANCE_WIDTH; return 1 / (1 + k * k); }
    // How much one string shakes from the struck note — for each overtone take (1/n) × the resonance curve and keep the largest
    function responseOf(stringMidi, srcMidi) {
        const fs = freqOf(stringMidi);
        const f0 = freqOf(srcMidi);
        let best = { amp: 0, n: 1, cents: 0 };
        for (let n = 1; n <= OVERTONES; n += 1) {
            const c = centsBetween(fs, f0 * n);
            const a = resonanceCurve(c) / n;
            if (a > best.amp) {
                best = { amp: a, n: n, cents: c };
            }
        }
        return best;
    }

07A Glissando Where Sweep Speed Becomes Loudness

The speed of a fingertip crossing 88 keys is followed by closing 0.15 of the gap each step and divided by 30 to give a loudness from 0.15 to 1, and each time the fingertip passes a white key boundary that key gets a speed of loudness × 10, so the press spreads down a chain of keys tied to their neighbors by springs. A key pushed once peaks at 0.182, the key two over reaches 0.082 and the one five over 0.029, and a single demo lap presses keys 43 times. The keyWidthFor function below sets the white key width to the larger of board width ÷ 52 and 22 CSS px, and on a narrow screen only the visible window slides toward the fingertip.

A Glissando Where Sweep Speed Becomes Loudness
Open in a new tab

07 · A Glissando Where Sweep Speed Becomes Loudness — Measured area 49.794% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

KEY_MIN_WIDTHSPEED_LERPK_COUPLE
    // Key width that fits the board — the width that shows every key when the screen allows, the minimum once that gets narrower (converted to board units)
    function keyWidthFor(boardW, cssW, totalWhite, minCss) {
        const minBoard = minCss * boardW / Math.max(1, cssW);
        return Math.max(boardW / totalWhite, minBoard);
    }
    function easeInOut(x) { const c = clamp(x, 0, 1); return c < 0.5 ? 2 * c * c : 1 - 2 * (1 - c) * (1 - c); }

08Notes That Fly onto Their Place on the Staff

The quotient of a note number by 12, times 7, plus its letter slot among C, D, E, F, G, A and B (0 to 6) gives a step count, and the gap from 37, the step of E (64) on the bottom line, times 7 px sets the height, so C (60) sits at y 146, E at y 132 and high C (72) at y 97. A black-key note takes the white-key slot below it, with a sharp drawn in lines 17 px to its left, so of the eight demo notes only F♯ carries one. Each note flies for 560 ms on a parabola 70 high, then a landing spring pushes it 5.74 px down, and from 528 ms on it stays within ±0.5 px of its place.

Notes That Fly onto Their Place on the Staff
Open in a new tab

08 · Notes That Fly onto Their Place on the Staff — Measured area 25.614% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

LETTER_INDEXdiatonicStepLAND_STIFFNESS
    // Letter order — note number mod 12 to the C·D·E·F·G·A·B slot (0~6). A semitone (black key) takes the white-key slot below + a sharp
    const LETTER_INDEX = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6];
    const SLOT_X0 = 96;
    const SLOT_GAP = 46;
    const SLOTS = 8;
    // Flight — a straight line from key to slot over FLIGHT_MS plus a parabola of height ARC_HEIGHT; on landing, a landing spring
    const FLIGHT_MS = 560;
    const ARC_HEIGHT = 70;
    const LAND_STIFFNESS = 260;
    const LAND_DAMPING = 10;
    const LAND_KICK = 160;
    const BEAT_MS = 260;
    const LOOP_MS = 12 * BEAT_MS;
    const CLEAR_MS = 10.4 * BEAT_MS;
    // Demo melody — C E D F♯ G B A C (eight notes written for this demo)
    const MELODY = [60, 64, 62, 66, 67, 71, 69, 72];
    const SEQ = MELODY.map(function (m, i) { return [0.8 + i, m, 0.7, 0.8]; });
    const STEP_LABEL = 'position = octave × 7 + letter index';
    let keys = [];
    let notes = [];
    let nextSlot = 0;
    let fade = 1;
    function diatonicStep(m) { return Math.floor(m / 12) * 7 + LETTER_INDEX[pcOf(m)]; }
    const BOTTOM_STEP = diatonicStep(64);
    function staffY(m) { return STAFF_BOTTOM_Y - (diatonicStep(m) - BOTTOM_STEP) * HALF_SPACE; }

09A Piano That Plays Itself on a Perspective Stage

Twenty-five keys are laid out with perspective projection, putting their front ends at y 280, their back ends at y 213.6 and the far end of the lane (depth 900) at y 89.7, and the time mapping from 04 moves from height to depth, so bars come toward the keys from the far end 1.99 seconds before their start. A key goes down when a bar's front end passes the depth of the key's back end, and the same stiffness-420 spring used in 01 lowers only the front end, by 6. The lid mirrors those bars across the horizon, squashed to 0.26 and dimmed by 0.75 × e^(−distance ÷ 70), so a reflection 70 away from the horizon shines at 0.276.

A Piano That Plays Itself on a Perspective Stage
Open in a new tab

09 · A Piano That Plays Itself on a Perspective Stage — Measured area 22.363% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

barFrontZZ_SPEEDLID_FALLOFF
    function projX(x, z) { return VANISH_X + x * Z0 / (z + Z0); }
    function projY(h, z) { return HORIZON_Y + (DEPTH_K - h * Z0) / (z + Z0); }
    function keyOf(m) {
        for (let i = 0; i < keys9.length; i += 1) {
            if (keys9[i].midi === m) {
                return keys9[i];
            }
        }
        return null;
    }
    // Depth of a bar's front end — farther than the keys' back end (KEY_LEN) by the seconds left until its start × Z_SPEED
    function barFrontZ(startMs, nowMs) { return KEY_LEN + (startMs - nowMs) / 1000 * Z_SPEED; }

Depth from a Spring, Tone from Overtones — Key Damping, Lever Ratios, Strike-Point Overtone Sizes and Semitone-Ratio Resonance

The motion in these nine scenes comes from equations solved at every 16 ms step rather than from keyframes. The nine stylesheets contain zero @keyframes, and the screen clock advances only in fixed 16 ms steps, so a given moment draws the same picture every run.

The Single Bounce Left by a 0.39 Damping Ratio

Keys in 01 move by acceleration = −420 × (position − target) − 16 × speed. The damping ratio is 16 ÷ (2 × √420), about 0.39, which is below 1, so a released key passes its rest position once before coming back. That overshoot is what makes a key feel like it pops back up, and in the top-left graph it appears as a small peak above the rest line.

A Lever Scaled 4.4 Times, Then Gravity

Once let off, the hammer in 02 feels only an angular acceleration of −(3000 ÷ 150) × cos θ, or −20·cos θ rad/s². At loudness 1 it leaves at 6.22 rad/s, meets the string at 5.68 and rebounds at 2.56, at 0.65 it leaves at 4.04 and lands at 3.14, and at 0.45 it leaves at 2.79 and reaches the string at only 1.17 before rebounding at 0.53. With sound on, the tone plays at any loudness, because the shared press code starts the synth first, so a miss changes the picture and not whether a note sounds. The simulation runs at 0.3 times screen time, which keeps the arc up to the string visible.

Strike Point p and the Overtone Nodes

The overtone size sin(nπp) ÷ n drops to 0 at every multiple of k when p is 1/k: 0.125 silences 8 and 16, and 0.5 silences all the even ones. The bar panel treats anything under 0.02 as 0 and draws it as an empty circle, while overtone 15 sits at −0.0255 and is therefore not zero. How two waves add up on their own is covered in the wave interference post; here one strike point sets the size of all sixteen overtones together.

The Twelfth Root of 2 and Cents

In 06 a string's pitch is 440 × 2^((note number − 69) ÷ 12) Hz, so one semitone is a factor of the twelfth root of 2, or 100 cents. Whole-number overtones do not land exactly on that ladder: overtone 3 sits −1.96¢ from the equal-temperament note, overtone 5 sits 13.69¢ off and overtone 7 sits 31.17¢ off. That 31.2¢ gap is why the A♯ string, reached through overtone 7, receives only 0.069 at a 30¢ resonance width.

Where a Browser Piano Breaks: an 88-Key Board, Ten Notes at Once, and Playback That Starts Before Sound Is On

An 88-Key Board

Divide a 320 px screen evenly among 88 keys (52 of them white) and white keys come out 6.15 px wide and black keys 3.57 px, too thin to pick out with a finger and with no room for a note name. 07 keeps a white key from shrinking below 22 CSS px, so 14.5 white keys show at 320 px and 21.8 at 480 px, while at 1280 px all 52 fit at 24.62 px each. When the screen width changes, resize rebuilds the board and its buttons, and when Tab moves focus to a key button outside the window, the window shifts toward that key for 3 seconds. The outer box uses overflow: clip, so the box itself does not scroll sideways while that happens.

Ten Notes at Once

Make four oscillators and their gain nodes for every note with no cap, and the nodes pile up each time a palm lands across the keys. Each note here needs five gains, one for the whole note and one per oscillator, so the nine scenes set MAX_VOICES to 6 and, when a new note arrives while 6 or more are still sounding, let go of the oldest first.

    const MAX_VOICES = 6;
    const voices = [];
    function allocVoice(pool, midi, startMs, auto) {
        const freed = [];
        for (let i = pool.length - 1; i >= 0; i -= 1) {
            if (pool[i].midi === midi) {
                freed.push(pool[i]);
                pool.splice(i, 1);
            }
        }
        while (pool.length >= MAX_VOICES) {
            const old = pool.shift();
            if (old) {
                freed.push(old);
            }
        }
        pool.push({ midi: midi, startMs: startMs, auto: auto, nodes: null, gain: null });
        return freed;
    }

Press the ten notes 60 to 69 one after another and 64 to 69 remain, while 60 to 63 lose their sound, their aria-pressed state and their pressed glow together. The pressed glow reads from the same list, so the drawing load stays capped at six. The chord circle in 05 has 4 corners, so with 5 or more distinct notes sounding, only the first four by note number get joined.

Playback That Starts Before Sound Is On

Create the AudioContext from the Web Audio API while the page loads and, with no user action yet, sound is likely blocked so the first note may not play, and the screen clock and the audio clock run separately, so picture and sound can drift apart. The nine scenes create the AudioContext in a single line inside the click handler of the "Sound on" button. Over the 2.5-second demo it is created 0 times, pressing "Sound on" makes it 1, pressing again to switch off leaves it at 1, and switching back on reuses that first one.

    soundBtn.addEventListener('click', function () {
        if (!audio) {
            audio = new AudioContext();
            master = audio.createGain();
            master.gain.value = MASTER_GAIN;
            master.connect(audio.destination);
        }
        soundOn = !soundOn;
        soundBtn.setAttribute('aria-pressed', soundOn ? 'true' : 'false');
        soundBtn.textContent = soundOn ? SOUND_OFF_LABEL : SOUND_ON_LABEL;
        if (soundOn) {
            syncClocks();
            audio.resume().then(syncClocks);
        }
        else {
            for (let i = 0; i < voices.length; i += 1) {
                synthStop(voices[i], clock);
            }
            screenTarget = clock;
        }
        wake();
    }, { signal: off.signal });

At the moment sound turns on, syncClocks writes down an audio time and a screen time as one pair, and each note's screen time is turned into audio time when the note is scheduled. With a pair of 12.5 seconds and 3,000 ms, screen time 3,500 ms becomes audio time 13 seconds, and 4,321 ms converted to audio time and back comes out as 4321 again. How far the two clocks drift apart before sound is on could not be measured in a silent capture, while the fact that they are re-aligned when sound turns on comes straight from the code. The nine vanilla scenes carrying these three guards, plus their React versions in tsx, are packed into one archive that opens with the password hqre6xd3.

A Softer Key, a Strike at Mid-String, a Narrower Resonance — Three One-Value Changes to the Tone

Name Changed value Feel
Soften the key spring 01 KEY_SPRING_STIFFNESS 420 → 240 The damping ratio rises from 0.39 to 0.516, the first pass over rest slows from 96 ms to 160 ms and the bounce shrinks from 23.9% to 12.4%. Settling barely moves, from 384 ms to 352 ms, and a press reaches the key bed later, at 112 ms rather than 80 ms
Strike at mid-string 03 STRIKE_POINT 0.125 → 0.5 Zeroed overtones grow from two (8 and 16) to all eight even ones, and overtone 1 rises from 0.383 to 1. The lopsided string becomes a symmetric bow, its left-right gap falling from 45.7 px to 0
Narrow the resonance 06 RESONANCE_WIDTH 30 → 10 With C2 struck, strings at 0.1 or more drop from 5 to 4, the E string falls from 0.166 to 0.07 and the A♯ string from 0.069 to 0.013, so glowing strings go from 7 to 5

Accessibility Without Sound: Announced Note Names, Computer-Key Playing and a Still Keyboard Under Reduced Motion

A Live Line That Reads Out Note Names

A hidden aria-live="polite" line beside the canvas writes the notes a person plays right away, joining names that arrive within 250 ms with " · ", and gathers demo notes into one line per 2-second bar in the form "Autoplay: C · E · G". Key buttons are named with their octave number, as in "C4", and aria-pressed turns true while a key is held down. The line follows the polite rule of aria-live, so a new line can cut off one still being read, which is why the demo notes are grouped by bar.

Playing the Keys from a Computer Keyboard

Each key drawn on the canvas is covered by a transparent <button>, so Tab moves between keys and Enter or Space presses one, and on the computer keyboard A S D F G H J K play the white keys while W E T Y U play the black keys. The focus ring stacks a 4 px cream band inside a 2 px navy outline, so it stays visible over both the ivory keys and the navy ones.

The Single Frame Kept Under Reduced Motion

With reduced motion turned on in the operating system, the nine scenes stop their animation loop and draw one final frame (prefers-reduced-motion). 01 keeps C, E and G pressed, 02 shows the hammer resting on its check above a flat string, 05 the C–E–G triangle, and 08 a staff with five notes in place. Pressing a key in that state changes only the pressed color and the note name, with no shaking, flight or bounce.

Questions People Ask Before Putting a Virtual Piano Keyboard on a Lesson Page or Practice Site

Can these keys be played with a finger on a phone?

Key buttons respond to pointerdown and set touch-action: none, so a finger can play them, and sliding into the next key while still pressing presses that key too, which gives a sweep. If you need all 88 keys, bring along the 22 px white key minimum and the following window from 07 so a single key can still be picked at 320 px.

How does it make sound without any audio files?

No sample files or outside addresses are involved: four sine waves at 1, 2, 3 and 4 times the pitch are stacked at sizes 1, 0.42, 0.18 and 0.08, rise in 0.006 seconds and fade exponentially over 1.6 seconds. For a screen that cuts and edits the waveform of recorded audio, the audio waveform scenes are the better match, and the synthesized tone here appears best suited to screens where the picture leads or to short practice notes.

Where do I change the tune in the javascript piano roll and the stage?

In 04 and 09 each line of the SONG array reads [start beat, note number, length in beats, loudness], and TEMPO_BPM alone sets the tempo. Change only those and bar lengths and press times follow on the same clock, and in 09 the bars reflected on the lid change along with them.

Enter the archive password

The password is inside this article. You will find it as you read.