{"id":"spring-motion","version":"1.0.0","title":"Squash & Stretch Spring Motion","summary":"Bouncy, jelly-like motion: things crouch, jump, stretch, land with a squash and wobble.","kind":"motion","target":"js","collections":["satisfying","bouncing-falling","kinetic-text"],"tags":["spring","bounce","squash","stretch","jelly","wobble","elastic","cartoon","hop","jump","motion","easing","playful"],"loopable":true,"motion":"procedural","durationSeconds":4,"status":"published","media":{"poster":"https://aipixelwizard.com/media/spring-motion/1.0.0/poster.webp","preview":"https://aipixelwizard.com/media/spring-motion/1.0.0/preview.mp4","sample":"https://aipixelwizard.com/media/spring-motion/1.0.0/sample.mp4","sampleBytes":138980,"sampleNote":"Pre-rendered with the default settings. Customised settings must be exported locally (npm run render)."},"urls":{"page":"https://aipixelwizard.com/visuals/spring-motion","manifest":"https://aipixelwizard.com/api/v1/visuals/spring-motion/versions/1.0.0","agentMd":"https://aipixelwizard.com/registry/spring-motion/1.0.0/agent.md","files":"https://aipixelwizard.com/registry/spring-motion/1.0.0/","demo":"https://aipixelwizard.com/registry/spring-motion/1.0.0/index.html"},"description":"The classic cartoon animation principles as ready-made motion: anything can hop, stretch as it flies, squash when it lands and wobble back into shape. There is also an exact spring (for overshoot-and-settle moves like a pop-in) and a squash helper that keeps the shape's volume believable. Use it to make logos, icons, characters or buttons feel playful.","role":"Motion source: spring values and squash/stretch scales over time.","useCases":["Make a logo or icon hop","Pop-in animations for text or stickers","Playful mascot or character loops"],"synonyms":["bouncy","jiggle","springy","elastic animation","overshoot","pop in","jumping characters","squishy","cartoon bounce"],"digest":"86255fdc273d65cd967a729287c7ac6cd7bfccfabf9ddb54145bd470ce7a973b","implementation":{"target":"js","runtime":"any","language":"javascript","module":"esm","entry":"src/spring-motion.js","exports":[{"name":"spring","signature":"spring(t, { from?, to?, velocity?, stiffness?, damping?, mass? }) => number","description":"Exact damped spring position at time t (seconds)."},{"name":"squashStretch","signature":"squashStretch(stretch) => [scaleX, scaleY]","description":"Volume-preserving scale pair."},{"name":"hop","signature":"hop(time, { period?, height?, squash?, airtime?, wobble?, offset? }) => { y, scaleX, scaleY, phase, inAir }","description":"Periodic hop cycle with anticipation, stretch, landing squash and wobble."},{"name":"cyclePhase","signature":"cyclePhase(progress, cycles?) => number","description":"Loop phase helper."}],"reusable":["src/spring-motion.js"],"demo":"demo.js","harness":"index.html"},"dependencies":[{"id":"apw-runtime","version":"1.0.0","scope":"demo","reason":"Demo harness and colour helpers. src/spring-motion.js has no dependencies."}],"uses":[],"usedBy":[{"id":"number-reveal","version":"1.0.0"},{"id":"number-reveal","version":"1.1.0"}],"params":[{"key":"hops","label":"Hops per clip","type":"integer","min":1,"max":8,"step":1,"default":4},{"key":"jump","label":"Jump height","type":"number","min":0.2,"max":1.6,"step":0.05,"default":1},{"key":"squash","label":"Squishiness","type":"number","min":0,"max":0.6,"step":0.02,"default":0.35},{"key":"wobble","label":"Wobble","help":"Lower numbers wobble longer after landing.","type":"number","min":3,"max":30,"step":0.5,"default":9},{"key":"stagger","label":"Hop out of step","type":"boolean","default":true},{"key":"colorA","label":"Colour 1","type":"color","default":"#ff6fb5"},{"key":"colorB","label":"Colour 2","type":"color","default":"#ffd166"},{"key":"colorC","label":"Colour 3","type":"color","default":"#5ee6c3"},{"key":"background","label":"Background","type":"color","default":"#14111f"}],"presets":[{"id":"default","label":"Jelly trio","params":{}},{"id":"stiff","label":"Stiff & quick","params":{"hops":8,"squash":0.18,"wobble":24,"jump":0.6}},{"id":"gooey","label":"Extra gooey","params":{"hops":3,"squash":0.56,"wobble":4,"stagger":false}}],"output":{"width":1080,"height":1080,"fps":30,"durationSeconds":4,"posterSeconds":0.55,"alpha":false,"motion":"procedural","loop":{"seamless":true,"cycleSeconds":4,"method":"The hop is periodic and the clip holds a whole number of hops."},"seed":null},"composition":{"inputs":"Time in seconds (or progress) plus options.","outputs":"Numbers: an offset (y) and scales (scaleX, scaleY). Apply them to any object.","coordinates":"y is height above the ground in your units. Scale around the object's bottom centre so it squashes onto the floor.","lighting":"None.","timing":"Closed-form math: no state, frames can be computed in any order.","compatibleWith":["kinetic-title","rolling-counter","bouncing-balls"],"notes":["Apply scale with the pivot at the bottom of the object, not its centre."]},"reuse":["Pop a number or label in with spring(t, { from: 0, to: 1 }) as its scale (see Minimal Animated Number Reveal).","Make a logo or app icon hop in a product video.","Use squashStretch on a Three.js mesh's scale for 3D jelly characters."],"integration":{"newProject":"Each frame compute const s = hop(frame / fps, { period: 1 }) and draw your object at y = groundY − s.y with scale (s.scaleX, s.scaleY) around its bottom centre.","existingProject":"Copy src/spring-motion.js. Feed your existing time variable into hop() or spring() and apply the result to the existing object's transform."},"performance":{"notes":"A few exp/sin calls per object per frame.","limits":"None."},"limitations":["The hop is stylised animation, not physics. Use Bouncing Balls for real collisions."],"license":{"spdx":"MIT","notice":"Copyright (c) 2026 AI Pixel Wizard. Released under the MIT License (see LICENSE in this folder).","thirdParty":[]},"rights":{"approvedForPublicRelease":true},"provenance":"Original code written for AI Pixel Wizard in October 2026.","changelog":[{"version":"1.0.0","date":"2026-10-02","notes":"Initial release."}],"verification":{"testedOn":"2026-10-02","inputs":{"spring-motion@1.0.0":"86255fdc273d65cd967a729287c7ac6cd7bfccfabf9ddb54145bd470ce7a973b","apw-runtime@1.0.0":"5be859cdac74823ba0bcc32c6daf06688a871104d24db5d4ceaffffd84d7c40d"},"environment":"Chrome 151.0.7922.137 headless on Linux 7.0.0-31-generic (x64), Node 22.14.0","renderer":"Canvas 2D","width":1080,"height":1080,"fps":30,"frames":120,"renderSeconds":7.4,"loopCheck":{"checked":true,"seamless":true,"maxDifference":0,"note":"Frame 0 vs end state (progress 1): mean |Δ| 0.0000, max |Δ| 0 of 255."},"determinism":{"checked":true,"identical":true},"errors":[],"media":{"poster":"/media/spring-motion/1.0.0/poster.webp","preview":"/media/spring-motion/1.0.0/preview.mp4","sample":"/media/spring-motion/1.0.0/sample.mp4","sampleBytes":138980}},"files":[{"path":"LICENSE","size":1072,"sha256":"8a5f3a96f07e11522ec4bdd0a583356f99bad1245920f9709cee09b995a45153","contentType":"text/plain; charset=utf-8","url":"https://aipixelwizard.com/registry/spring-motion/1.0.0/LICENSE"},{"path":"asset.json","size":6635,"sha256":"f4a0d2619f8f9b2f9dd7c0b3dfba95b0422adaad51dec93bb3b8e8b3dffbe739","contentType":"application/json; charset=utf-8","url":"https://aipixelwizard.com/registry/spring-motion/1.0.0/asset.json"},{"path":"demo.js","size":2690,"sha256":"c06f4d3655a0370f69bc599a4cad74445d40eadcd7c38aed0fceae4ce6ebc011","contentType":"text/javascript; charset=utf-8","url":"https://aipixelwizard.com/registry/spring-motion/1.0.0/demo.js"},{"path":"index.html","size":548,"sha256":"9bc07e1ad1714f5f71c0ae1f91588528b6ef4675481bea9768dde2ff7d4d45a9","contentType":"text/html; charset=utf-8","url":"https://aipixelwizard.com/registry/spring-motion/1.0.0/index.html"},{"path":"src/spring-motion.js","size":4898,"sha256":"491c870a293db34e355334a512a99015a366cdc830dd51a3351e6bd166670004","contentType":"text/javascript; charset=utf-8","url":"https://aipixelwizard.com/registry/spring-motion/1.0.0/src/spring-motion.js"}],"warnings":[]}