PL
games/satisfying-bounce / AGENTS.md · commit 8bdd6e4
# Satisfying BounceVertical "satisfying" videos and animated optical illusions (270 x 480 drawn in HD, x4 = 1080 x 1920 for TikTok / Shorts / Reels), fifteenscenes, each with its own scale or melody. Every bounce, landing or beat is a note (`Recorder`, mixed offline by `Mixer`). English and French(`config/lang.yaml`, `--lang`).| template | scene | what | sound ||---|---|---|---|| `zoom` | `ZoomRingsScene` | rings with openings, the ball escapes ring after ring, the camera glides out (`ZoomView`), rings regenerate outside: infinite zoom | C major pentatonic marimba, a bell chord at each passage || `gap` | `GapCircleScene` | ball in a circle closed on 3/4; each exit makes the next opening narrower | Ode to Joy (public domain) || `rings` | `ConcentricScene` | seven rings, each breaks after 3 hits, from the inside | pentatonic, a bell when a ring breaks || `multiply` | `MultiplyScene` | every wall hit makes a ball, 10 at most (the oldest fades) | the opening of Fur Elise || `marble` | `MarbleScene` | marbles down zig-zag ramps | one pentatonic note per ramp (marimba) || `plinko` | `PlinkoScene` | balls through pegs, peg = note by column | pentatonic marimba, bell at the bottom || `dominos` | `DominoScene` | a spiral of dominoes falls, then stands up again (12 s loop) | rising notes, then soft falling ones || `pendulum` | `PendulumScene` | pendulum wave, one cycle = 24 s, exact loop | one pentatonic note per pendulum || `stairs` | `StairsScene` | a shape hops down ever bigger steps, the camera zooms so its screen size is constant: it "grows" without growing | Lydian, each landing lower || `tunnel` | `TunnelScene` | endless tunnel of twisted squares (6 s loop) | D minor pentatonic phrase of 12 notes || `spiral` | `SpiralScene` | rotating spiral of dots, rippling sizes (4 s loop) | whole-tone bells || `checker` | `WaveGridScene` | checkerboard that undulates (4 s loop) | Hirajoshi marimba || `ripple` | `DotRippleScene` | dots swelling in rings from the middle: 3D look (4 s loop) | Lydian bells || `cats` | `CatBounceScene` | four round cats show up one by one (every 0.9 s, pop-in + soft meow) then bounce and meow (synthesized meows + purr) | `meow` voice, pentatonic pitches || `rain` | `RainPondScene` | ASMR pond, slow drops and rings | `drop` voice, minor pentatonic, pad |- `VideoScene`: base (hook caption, counter, closing caption, fade in/out unless `LOOPS`, `Fx`, `ZoomView`, `note()`, `play()`, `pad()`, ball and  ring drawing). Params: `seed`, `seconds`, `lang`, `title`, `melody`, `captions`. Texts are Lang keys (English), French in `config/lang.yaml`.- `src/Render/`: `Gfx` (HD shapes: arcs, rings, ellipses, polygons, glow; nothing snapped to the view grid), `ZoomView` (camera: a centre and a  scale, zooms in log space with an ease, the physics never sees it), `Fx` (sparks and shock waves in screen space).- `src/Audio/Scale.php`; `src/Lib/Shorts/Voice.php` (pluck, bell, marimba, sine, pad, drop, **meow**, **purr**: generated, no recording; phase is  integrated so glides never click), `Mixer` (low-pass, echo, fades, same peak for every video), `Recorder` (live notes use the same voices; while  filming nothing is played).- `src/Lib/Physics2d` is the copy of `jumphp/physics` (+ `removeBall`/`removeRing` forget their "left this ring" marks: PHP reuses object ids).- Zoom without touching the physics: bodies keep world coordinates; the scene scales the ball's radius, speed and gravity by 1/zoom so they  stay the same ON SCREEN, and `rebase()` multiplies the world by the zoom when the camera rests, so numbers stay small however far it goes.  Illusion scenes use whole numbers of periods (`PERIOD`), so the last frame equals the first.## What I learned about why these videos work (heuristic: sources are mostly blogs of simulation tools, no independent study)- The sound is the content: a bounce that plays the next note of a tune (or a scale) makes sight and sound match; a minimum spacing between  notes (80-120 ms) keeps busy moments from smearing; pentatonic scales never clash when notes overlap; soft attacks and tails, no clicks.- A hook in the first second: already moving, stakes visible, one short line that names the payoff ("wait for the escape"). 15-30 s, finished  rather than long.- Tension climbs on a curve: openings that narrow, rings that get closer, near misses, then a clearly bigger payoff (burst, bell chord, zoom).- Loops: the last frame equals the first (periodic motion, whole periods) or the end resembles the start; fade through black otherwise.- Clarity at thumbnail size: dark background, bright saturated shapes, one palette, few moving things (multiply was cut from 44 to 10 balls  so every bounce can be followed), high frame rate (60), safe margins for the platforms' interface.- Juice: squash and stretch, trails, halos, a soft flash and a shock wave on each hit, a little camera punch on the beat.- Optical illusions work from relative size, repetition and phase (rings of different size and colour, twisted squares, travelling waves).## Back to the menuEvery `VideoScene` has Escape / `back` and a small "<- MENU" button at the top left (`MENU_BUTTON`, mouse click or touch tap, looked at before thetap-to-restart; not drawn nor active while filming). `exit()` silences the voices (`Recorder::stopAll()`); scenes are reused, `enter()` resets all.## Make the videos```bashtools/make-videos.sh [template...]          # every template, English + French, in parallel (JOBS=6), then the light versions of the sitebin/jumphp video:make satisfying-bounce --template zoom --lang fr --seconds 28 --seed 3# dist/video/satisfying-bounce-<template>-<seed>-<lang>.mp4 (+ .png cover); site/videos/<template>-<lang>.mp4 (540 x 960) + site/images/<template>-<lang>.jpgpython3 tools/site/build.py                 # site/index.html (en), site/fr/index.html (fr), sitemap.xml, from ONE listpython3 tools/site/images.py                # site/images/og.png and assets/thumbnail.png from the covers```HD: `project.yaml hd: auto`. The headless picture used to be fixed at 2x the view (540 x 960); `JUMPHP_HEADLESS_SCALE` (engine/src/Renderer/Sdl.php,set by `tools/video-make-commands.sh` from `video.scale`) makes it 4x, so the film is drawn natively at 1080 x 1920 (sharp text, round circles).`video:make --lang fr`, `--tmpfs /tmp` and a check that every frame was written are local patches of `tools/video-make-commands.sh` (to bringback to JumPHP). Same seed, same video.Tests: `bin/jumphp test satisfying-bounce` (tests/BounceTest, ZoomTest, ScenesTest; long simulations step the scene without drawing, so thefilmed tests stay short).## Played live, on a phone (performance)The film (`video:make`, `JUMPHP_VIDEO_OUT`) is drawn exactly as before; only a game played live adapts (`JumPHP\Shorts\Quality::$live`).- **What made it slow** (measured, `tests/` bench in the history of this change): with `hd: auto` every disc / ellipse / polygon / line of thickness  was one `fillRect` or `drawLine` per real pixel (up to 25 000 rects and 38 000 lines a frame at 4x), and the translucent ones went through  Renderer's 256-entry cache of 1x1 textures, flushed over and over by trails of changing colours (300 textures created per frame in  `multiply` and `marble`). Sounds were generated in PHP at the first bounce of each note (a meow 31 ms, a bell 11 ms on the desktop).- **Now (live)**: `Sprites` (src/Lib/Shorts) draws discs / ellipses / halos / rotated rectangles from textures made once (GD -> texture, bounded  caches, colours rounded, few alpha levels); `Gfx::arc` is a chain of rectangles; `Fx` sparks are sprites. A frame is 30 to 1500 SDL calls.  `Quality::begin()` caps hd at 3 on a phone; `Quality::frame()` goes one step lighter (hd 2, fewer sparks, then no halos) when the real frame  rate stays under ~45 fps for a second (never back up). `JUMPHP_QUALITY=0|1|2` (env, or `define`) fixes the level.- **Sound live**: `Recorder` makes voices at 22 050 Hz, a few ms per frame at most (`pump()`, called at the end of `VideoScene::update()`), the notes  a scene plays are queued at `enter()` (`App\Audio\Warm`, made by `tools/warm-notes.sh`); a note whose voice is late by more than 0.18 s is skipped.  The physics gets at most 0.04 s per frame live (a slow frame slows the picture instead of making the next one slower).