{
  "schemaVersion": 1,
  "id": "rolling-counter",
  "version": "1.0.0",
  "status": "published",
  "kind": "text",
  "title": "Rolling Number Counter",
  "summary": "A number that counts up like a mechanical odometer, every digit rolling into place.",
  "description": "Show off a big number the satisfying way: digits roll like a car's odometer from a starting value to your target, slowing down as they arrive. Add a prefix like $ or a suffix like + or 'users', choose the colour and how long it counts. Great for milestones, stats and results.",
  "role": "Foreground number layer (Canvas 2D).",
  "collections": [
    "kinetic-text",
    "product-reveals",
    "satisfying"
  ],
  "tags": [
    "counter",
    "number",
    "odometer",
    "count up",
    "stats",
    "milestone",
    "digits",
    "rolling",
    "text",
    "2d",
    "canvas"
  ],
  "synonyms": [
    "number animation",
    "count up",
    "counting numbers",
    "animated number",
    "stat reveal",
    "rolling digits",
    "slot machine numbers",
    "ticker"
  ],
  "useCases": [
    "Milestone announcement (users, revenue, downloads)",
    "Stats in a launch video",
    "Countdown-style reveals"
  ],
  "featured": 11,
  "publishedAt": "2026-10-02",
  "implementation": {
    "target": "canvas2d",
    "runtime": "browser",
    "language": "javascript",
    "module": "esm",
    "entry": "src/rolling-counter.js",
    "exports": [
      {
        "name": "createRollingCounter",
        "signature": "createRollingCounter({ from?, to?, delay?, duration?, ease?, prefix?, suffix?, separator?, color?, fontFamily?, weight?, size?, x?, y? }) => { draw(ctx, info, width, height), valueAt(t) }",
        "description": "Canvas 2D odometer driven by info.time (seconds)."
      }
    ],
    "reusable": [
      "src/rolling-counter.js"
    ],
    "demo": "demo.js",
    "harness": "index.html"
  },
  "dependencies": [
    {
      "id": "apw-runtime",
      "version": "1.0.0",
      "scope": "demo",
      "reason": "Demo harness. src/rolling-counter.js has no dependencies."
    }
  ],
  "params": [
    {
      "key": "from",
      "label": "Start at",
      "type": "integer",
      "min": 0,
      "max": 999999999,
      "step": 1,
      "default": 0
    },
    {
      "key": "to",
      "label": "Count to",
      "type": "integer",
      "min": 0,
      "max": 999999999,
      "step": 1,
      "default": 1250000
    },
    {
      "key": "countSeconds",
      "label": "Counting time (s)",
      "type": "number",
      "min": 0.5,
      "max": 10,
      "step": 0.1,
      "default": 3
    },
    {
      "key": "ease",
      "label": "Feel",
      "type": "select",
      "default": "outExpo",
      "options": [
        {
          "value": "outExpo",
          "label": "Fast then slow"
        },
        {
          "value": "outCubic",
          "label": "Gentle slow-down"
        },
        {
          "value": "inOutCubic",
          "label": "Slow, fast, slow"
        },
        {
          "value": "linear",
          "label": "Steady"
        }
      ]
    },
    {
      "key": "prefix",
      "label": "Before the number",
      "type": "text",
      "default": "$",
      "maxLength": 6,
      "sensitive": true
    },
    {
      "key": "suffix",
      "label": "After the number",
      "type": "text",
      "default": "",
      "maxLength": 12,
      "sensitive": true
    },
    {
      "key": "separator",
      "label": "Thousands commas",
      "type": "boolean",
      "default": true
    },
    {
      "key": "color",
      "label": "Colour",
      "type": "color",
      "default": "#ffffff"
    },
    {
      "key": "background",
      "label": "Background",
      "type": "color",
      "default": "#0c0b12"
    }
  ],
  "presets": [
    {
      "id": "default",
      "label": "Revenue",
      "params": {}
    },
    {
      "id": "users",
      "label": "Users",
      "params": {
        "to": 50000,
        "prefix": "",
        "suffix": "+",
        "ease": "outCubic"
      }
    },
    {
      "id": "small",
      "label": "Small number",
      "params": {
        "to": 99,
        "prefix": "",
        "suffix": "%",
        "countSeconds": 2
      }
    }
  ],
  "output": {
    "width": 1080,
    "height": 1080,
    "fps": 30,
    "durationSeconds": 5,
    "posterSeconds": 4.5,
    "alpha": false,
    "motion": "procedural",
    "loop": {
      "seamless": false,
      "method": "Counts once and holds the final number."
    },
    "seed": null
  },
  "composition": {
    "inputs": "Canvas 2D context, info.time (seconds), width, height.",
    "outputs": "Draws the number on the canvas. Does not clear it.",
    "coordinates": "Centre point as fractions of width/height; size as a fraction of the shorter side.",
    "lighting": "None.",
    "timing": "Pure function of time; valueAt(t) gives the exact value at any moment.",
    "compatibleWith": [
      "gradient-background",
      "spring-motion",
      "kinetic-title",
      "particle-tunnel"
    ],
    "notes": [
      "Prefix and suffix are treated as private and are not stored in shared packs unless you choose to include them."
    ]
  },
  "reuse": [
    "Combine with Soft Moving Gradient and a spring pop-in label (see Minimal Animated Number Reveal).",
    "Use valueAt(t) alone to drive a progress bar or a 3D bar chart.",
    "Place several counters (x/y options) for a stats grid."
  ],
  "integration": {
    "newProject": "Create the counter once; each frame draw your background then counter.draw(ctx, { time: frame / fps }, width, height).",
    "existingProject": "Copy src/rolling-counter.js and call draw from your existing frame loop where the number should appear."
  },
  "performance": {
    "notes": "Two fillText calls per digit per frame.",
    "limits": "Up to 15 digits."
  },
  "limitations": [
    "Whole numbers only (no decimals)."
  ],
  "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. Uses the viewer's system font.",
  "changelog": [
    {
      "version": "1.0.0",
      "date": "2026-10-02",
      "notes": "Initial release."
    }
  ]
}
