{"_id":"@a-company/atelier-math","name":"@a-company/atelier-math","dist-tags":{"latest":"0.25.1"},"versions":{"0.25.1":{"name":"@a-company/atelier-math","version":"0.25.1","publishConfig":{"access":"public"},"description":"Pure interpolation engine — easing, lerp, spring, color","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"devDependencies":{"tsup":"^8.4.0","typescript":"^5.7.0","vitest":"^3.0.0"},"scripts":{"build":"tsup","typecheck":"tsc --noEmit","test":"vitest run","clean":"rm -rf dist"},"_id":"@a-company/atelier-math@0.25.1","_integrity":"sha512-Uwwd2etOxLLVNVpTryB7fDlZaGscQsfpQBg/0clCMpLcKMl+7FnO/4hD48CixJ92yTMvUV0Z+DbX/+S/iuTM7A==","_resolved":"/private/var/folders/gq/nt2kdpsj39dc8vpq6cfzx8gw0000gn/T/78135ee3fe67ea35f2fd38d53c151fe4/a-company-atelier-math-0.25.1.tgz","_from":"file:a-company-atelier-math-0.25.1.tgz","_nodeVersion":"24.12.0","_npmVersion":"11.7.0","dist":{"integrity":"sha512-Uwwd2etOxLLVNVpTryB7fDlZaGscQsfpQBg/0clCMpLcKMl+7FnO/4hD48CixJ92yTMvUV0Z+DbX/+S/iuTM7A==","shasum":"dd28da7a0a35c9d5a8261d94d66535c9a81cd087","tarball":"https://registry.npmjs.org/@a-company/atelier-math/-/atelier-math-0.25.1.tgz","fileCount":8,"unpackedSize":79677,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQD+UUlFnGYIZohGUbSidO9VhO3CEhacTUK5IhNH/k4mrwIhAIZa7wlRJjKu81mCdYnP3Cw+rQiq5NHVOn+TiIMpxOC8"}]},"_npmUser":{"name":"ascend42","email":"ascend@a-company.org"},"directories":{},"maintainers":[{"name":"ascend42","email":"ascend@a-company.org"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/atelier-math_0.25.1_1771887797827_0.8120998978188954"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-23T23:03:17.768Z","0.25.1":"2026-02-23T23:03:17.990Z","modified":"2026-02-23T23:03:18.180Z"},"maintainers":[{"name":"ascend42","email":"ascend@a-company.org"}],"description":"Pure interpolation engine — easing, lerp, spring, color","readme":"---\ntitle: \"@atelier/math\"\nscope: Pure interpolation engine — easing, spring physics, lerp, color interpolation\npackages: [\"@atelier/math\"]\nrelated: [\"docs/format-spec.md\", \"packages/types/README.md\", \"packages/core/README.md\"]\n---\n\n# @atelier/math\n\nPure interpolation engine for the Atelier animation system. Provides easing curves, spring physics, linear interpolation, and color space conversion -- all as pure, stateless, side-effect-free functions.\n\n| | |\n|---|---|\n| **Version** | 0.1.0 |\n| **Dependencies** | Zero (no external or `@atelier/*` dependencies) |\n| **Build** | tsup (ESM + CJS + DTS) |\n| **Source** | `packages/math/src/` |\n| **Test** | `vitest run` |\n\n## Installation\n\n```bash\npnpm add @atelier/math\n```\n\n## Exports\n\n```typescript\n// Easing\nexport { linear, cubicBezier, easeIn, easeOut, easeInOut, step } from \"./easing.js\";\nexport { spring } from \"./spring.js\";\nexport type { SpringConfig } from \"./spring.js\";\n\n// Interpolation\nexport { lerp, clamp, lerpArray, remap } from \"./lerp.js\";\n\n// Color\nexport { hexToRgba, rgbaToHex, lerpRgba, rgbaToHsla, hslaToRgba, lerpHsla } from \"./color.js\";\nexport type { RGBA, HSLA } from \"./color.js\";\n```\n\n---\n\n## Modules\n\n### 1. easing.ts -- Easing Functions\n\nSource: `packages/math/src/easing.ts`\n\nAll easing functions accept a normalized progress value `t` in the range `[0, 1]` and return a mapped value.\n\n#### `linear(t: number): number`\n\nIdentity easing. Returns `t` unchanged -- no acceleration, no deceleration.\n\n```typescript\nlinear(0.5); // 0.5\n```\n\n#### `cubicBezier(x1, y1, x2, y2): (t: number) => number`\n\nCSS-compatible cubic bezier curve. Returns a closure that maps progress `t` to an eased value.\n\nInternally uses binary search (20 iterations, tolerance `1e-6`) to solve the parametric `t` from the x-axis bezier polynomial, then evaluates the y-axis polynomial at that parameter.\n\nThe bezier polynomial for a single axis is:\n\n```\nB(t) = 3(1-t)^2 * t * p1  +  3(1-t) * t^2 * p2  +  t^3\n```\n\nBoundary clamping: returns `0` for `t <= 0` and `1` for `t >= 1`.\n\n```typescript\nconst ease = cubicBezier(0.25, 0.1, 0.25, 1.0);\nease(0.5); // ~0.802\n```\n\n#### Presets\n\nThree standard CSS easing presets, pre-built with `cubicBezier`:\n\n| Name | Control Points | CSS Equivalent |\n|------|---------------|----------------|\n| `easeIn` | `(0.42, 0, 1, 1)` | `ease-in` |\n| `easeOut` | `(0, 0, 0.58, 1)` | `ease-out` |\n| `easeInOut` | `(0.42, 0, 0.58, 1)` | `ease-in-out` |\n\n```typescript\neaseIn(0.5);    // slow start\neaseOut(0.5);   // slow end\neaseInOut(0.5); // slow start and end\n```\n\n#### `step(steps: number, position?: \"start\" | \"end\"): (t: number) => number`\n\nDiscrete step easing that jumps between `steps` evenly-spaced values. Returns a closure.\n\nThe `position` parameter controls when the jump occurs within each step interval:\n\n- `\"end\"` (default) -- the value holds at the previous level until the next step boundary.\n- `\"start\"` -- the value jumps to the next level at the beginning of each step interval.\n\nBoundary behavior:\n- `t <= 0`: returns `1 / steps` for `\"start\"`, `0` for `\"end\"`.\n- `t >= 1`: always returns `1`.\n\n```typescript\nconst step4 = step(4);\nstep4(0.0);  // 0\nstep4(0.25); // 0.25\nstep4(0.5);  // 0.5\nstep4(0.99); // 0.75\nstep4(1.0);  // 1\n\nconst step4Start = step(4, \"start\");\nstep4Start(0.0);  // 0.25\nstep4Start(0.25); // 0.5\n```\n\n---\n\n### 2. spring.ts -- Spring Physics\n\nSource: `packages/math/src/spring.ts`\n\n#### `spring(config?: SpringConfig): (t: number) => number`\n\nCreates a spring easing function based on damped harmonic oscillator physics. The spring always transitions from `0` to `1` but may overshoot depending on the configuration. Returns a closure that maps normalized `t` in `[0, 1]` to the spring's displacement.\n\n```typescript\ninterface SpringConfig {\n  mass?: number;      // default: 1\n  stiffness?: number; // default: 100\n  damping?: number;   // default: 10\n  velocity?: number;  // default: 0\n}\n```\n\n#### Physics model\n\nThe behavior is determined by the **damping ratio** and **natural frequency**:\n\n```\nw0   = sqrt(stiffness / mass)          -- natural frequency\nzeta = damping / (2 * sqrt(stiffness * mass))  -- damping ratio\n```\n\nThree regimes:\n\n| Regime | Condition | Behavior |\n|--------|-----------|----------|\n| **Underdamped** | `zeta < 1` | Oscillates around the target before settling. Damped frequency: `wd = w0 * sqrt(1 - zeta^2)`. |\n| **Critically damped** | `zeta = 1` | Fastest approach to target without any oscillation. |\n| **Overdamped** | `zeta > 1` | Approaches the target slowly, no oscillation. |\n\n#### Duration auto-estimation\n\nThe spring's settle time (within 0.1% of target) is automatically estimated:\n\n- **Underdamped**: `ln(1000) / (zeta * w0)`\n- **Overdamped / critically damped**: `10 / (zeta * w0)`\n\nThe input `t` in `[0, 1]` is scaled to this estimated duration internally.\n\n```typescript\n// Bouncy spring\nconst bouncy = spring({ stiffness: 200, damping: 8 });\nbouncy(0.3); // may overshoot 1.0\n\n// Stiff, no-bounce spring\nconst stiff = spring({ stiffness: 300, damping: 30 });\nstiff(0.5); // approaches 1.0 smoothly\n\n// Heavy, slow spring\nconst heavy = spring({ mass: 5, stiffness: 100, damping: 15 });\nheavy(0.5); // slower rise\n```\n\n---\n\n### 3. lerp.ts -- Linear Interpolation\n\nSource: `packages/math/src/lerp.ts`\n\n#### `lerp(a: number, b: number, t: number): number`\n\nLinear interpolation between two numbers.\n\n```\nlerp(a, b, t) = a + (b - a) * t\n```\n\n```typescript\nlerp(0, 100, 0.5);  // 50\nlerp(10, 20, 0.25); // 12.5\n```\n\n#### `clamp(value: number, min: number, max: number): number`\n\nConstrains a value to the range `[min, max]`.\n\n```typescript\nclamp(150, 0, 100); // 100\nclamp(-5, 0, 100);  // 0\nclamp(50, 0, 100);  // 50\n```\n\n#### `lerpArray(a: number[], b: number[], t: number): number[]`\n\nElement-wise linear interpolation between two arrays of equal length.\n\n```typescript\nlerpArray([0, 0, 0], [10, 20, 30], 0.5); // [5, 10, 15]\n```\n\n#### `remap(value: number, inMin: number, inMax: number, outMin: number, outMax: number): number`\n\nRe-maps a value from one range to another. Internally computes the normalized position within the input range, then applies `lerp` to the output range.\n\n```\nremap(value, inMin, inMax, outMin, outMax)\n  = lerp(outMin, outMax, (value - inMin) / (inMax - inMin))\n```\n\n```typescript\nremap(50, 0, 100, 0, 1);    // 0.5\nremap(0.5, 0, 1, -100, 100); // 0\nremap(75, 0, 100, 200, 400); // 350\n```\n\n---\n\n### 4. color.ts -- Color Interpolation\n\nSource: `packages/math/src/color.ts`\n\n#### Types\n\n```typescript\ninterface RGBA {\n  r: number; // 0-255\n  g: number; // 0-255\n  b: number; // 0-255\n  a: number; // 0-1\n}\n\ninterface HSLA {\n  h: number; // 0-360 (degrees)\n  s: number; // 0-100 (percent)\n  l: number; // 0-100 (percent)\n  a: number; // 0-1\n}\n```\n\n#### `hexToRgba(hex: string): RGBA`\n\nParses a hex color string into an RGBA object. Supports four formats:\n\n| Format | Example | Expansion |\n|--------|---------|-----------|\n| `#RGB` | `#f00` | `#ff0000ff` |\n| `#RGBA` | `#f00f` | `#ff0000ff` |\n| `#RRGGBB` | `#ff0000` | `#ff0000ff` |\n| `#RRGGBBAA` | `#ff000080` | as-is |\n\n```typescript\nhexToRgba(\"#ff0000\");  // { r: 255, g: 0, b: 0, a: 1 }\nhexToRgba(\"#f00\");     // { r: 255, g: 0, b: 0, a: 1 }\nhexToRgba(\"#ff000080\"); // { r: 255, g: 0, b: 0, a: ~0.502 }\n```\n\n#### `rgbaToHex(color: RGBA): string`\n\nConverts an RGBA object back to a hex string. Values are rounded and clamped to `[0, 255]`. The alpha component is omitted from the output when it is fully opaque (`ff`).\n\n```typescript\nrgbaToHex({ r: 255, g: 0, b: 0, a: 1 });   // \"#ff0000\"\nrgbaToHex({ r: 255, g: 0, b: 0, a: 0.5 }); // \"#ff000080\"\n```\n\n#### `lerpRgba(a: RGBA, b: RGBA, t: number): RGBA`\n\nChannel-wise linear interpolation between two RGBA colors. Each channel (r, g, b, a) is interpolated independently.\n\n```typescript\nconst red  = { r: 255, g: 0, b: 0, a: 1 };\nconst blue = { r: 0, g: 0, b: 255, a: 1 };\nlerpRgba(red, blue, 0.5); // { r: 127.5, g: 0, b: 127.5, a: 1 }\n```\n\n#### `rgbaToHsla(color: RGBA): HSLA`\n\nStandard RGB-to-HSL conversion. Normalizes RGB channels to `[0, 1]`, computes hue from the dominant channel, and returns saturation and lightness as percentages.\n\n```typescript\nrgbaToHsla({ r: 255, g: 0, b: 0, a: 1 }); // { h: 0, s: 100, l: 50, a: 1 }\n```\n\n#### `hslaToRgba(color: HSLA): RGBA`\n\nStandard HSL-to-RGB conversion. Returns RGBA with `r`, `g`, `b` rounded to integers in `[0, 255]`.\n\n```typescript\nhslaToRgba({ h: 0, s: 100, l: 50, a: 1 }); // { r: 255, g: 0, b: 0, a: 1 }\n```\n\n#### `lerpHsla(a: HSLA, b: HSLA, t: number): HSLA`\n\nInterpolation between two HSLA colors with **shortest hue path**. If the hue difference exceeds 180 degrees, the interpolation wraps around the color wheel to take the shorter arc. This prevents unexpected color transitions -- for example, interpolating from red (0 degrees) to blue (240 degrees) will travel through magenta/purple rather than through green and cyan.\n\nSaturation, lightness, and alpha are interpolated linearly. The resulting hue is normalized to `[0, 360)`.\n\n```typescript\nconst red  = { h: 0, s: 100, l: 50, a: 1 };\nconst blue = { h: 240, s: 100, l: 50, a: 1 };\n\n// Shortest path: 0 -> 360 -> 300 -> 240 (through magenta)\nlerpHsla(red, blue, 0.5); // { h: 300, s: 100, l: 50, a: 1 }\n```\n\n---\n\n## Usage Examples\n\n### Easing functions with lerp\n\n```typescript\nimport { easeInOut, lerp } from \"@atelier/math\";\n\n// Animate a value from 100 to 500 with easeInOut\nfunction animate(progress: number): number {\n  const eased = easeInOut(progress);\n  return lerp(100, 500, eased);\n}\n\nanimate(0);   // 100\nanimate(0.5); // ~300\nanimate(1);   // 500\n```\n\n### Spring configuration\n\n```typescript\nimport { spring, lerp } from \"@atelier/math\";\n\n// Bouncy entrance animation\nconst bounce = spring({ stiffness: 180, damping: 12 });\n\nfunction springPosition(progress: number): number {\n  return lerp(0, 200, bounce(progress));\n}\n\n// The position will overshoot 200 before settling\nspringPosition(0.3); // may exceed 200 briefly\nspringPosition(1.0); // 200\n```\n\n### Color interpolation between hex values\n\n```typescript\nimport { hexToRgba, lerpRgba, rgbaToHex, rgbaToHsla, lerpHsla, hslaToRgba } from \"@atelier/math\";\n\nconst sunrise = hexToRgba(\"#ff6b35\");\nconst sunset  = hexToRgba(\"#9b59b6\");\n\n// RGB interpolation (direct channel blend)\nconst midRgb = lerpRgba(sunrise, sunset, 0.5);\nrgbaToHex(midRgb); // blended color in hex\n\n// HSL interpolation (perceptually smoother, shortest hue path)\nconst sunriseHsl = rgbaToHsla(sunrise);\nconst sunsetHsl  = rgbaToHsla(sunset);\nconst midHsl     = lerpHsla(sunriseHsl, sunsetHsl, 0.5);\nconst midRgba    = hslaToRgba(midHsl);\nrgbaToHex(midRgba); // perceptually blended color\n```\n\n### Range remapping\n\n```typescript\nimport { remap, clamp } from \"@atelier/math\";\n\n// Convert a mouse position (0-800px) to an opacity (0-1)\nfunction mouseToOpacity(mouseX: number): number {\n  return clamp(remap(mouseX, 0, 800, 0, 1), 0, 1);\n}\n\nmouseToOpacity(400); // 0.5\nmouseToOpacity(800); // 1\nmouseToOpacity(900); // 1 (clamped)\n```\n\n---\n\n## Design Principles\n\n- **Pure functions only.** No internal state, no side effects, no mutation. Given the same inputs, every function always returns the same output.\n- **Zero dependencies.** The package depends on nothing -- not even other `@atelier/*` packages. It can be used standalone in any JavaScript/TypeScript project.\n- **Normalized time.** All easing and spring functions accept `t` in `[0, 1]` and handle boundary clamping internally.\n- **CSS compatibility.** The `cubicBezier` implementation and presets match CSS transition timing functions exactly.\n","readmeFilename":"README.md","_rev":"1-0ac1c275599d243a30839aa5e5a2fd65"}