{
  "schemaVersion": 1,
  "id": "spring-motion",
  "version": "1.0.0",
  "status": "published",
  "kind": "motion",
  "title": "Squash & Stretch Spring Motion",
  "summary": "Bouncy, jelly-like motion: things crouch, jump, stretch, land with a squash and wobble.",
  "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.",
  "collections": [
    "satisfying",
    "bouncing-falling",
    "kinetic-text"
  ],
  "tags": [
    "spring",
    "bounce",
    "squash",
    "stretch",
    "jelly",
    "wobble",
    "elastic",
    "cartoon",
    "hop",
    "jump",
    "motion",
    "easing",
    "playful"
  ],
  "synonyms": [
    "bouncy",
    "jiggle",
    "springy",
    "elastic animation",
    "overshoot",
    "pop in",
    "jumping characters",
    "squishy",
    "cartoon bounce"
  ],
  "useCases": [
    "Make a logo or icon hop",
    "Pop-in animations for text or stickers",
    "Playful mascot or character loops"
  ],
  "featured": 9,
  "publishedAt": "2026-10-02",
  "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."
    }
  ],
  "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": []
  },
  "provenance": "Original code written for AI Pixel Wizard in October 2026.",
  "changelog": [
    {
      "version": "1.0.0",
      "date": "2026-10-02",
      "notes": "Initial release."
    }
  ]
}
