{"_id":"@a-company/atelier-canvas","_rev":"2-0bdcc3ebf8e12d86aaddce847a9131b8","name":"@a-company/atelier-canvas","dist-tags":{"latest":"0.26.0"},"versions":{"0.25.1":{"name":"@a-company/atelier-canvas","version":"0.25.1","_id":"@a-company/atelier-canvas@0.25.1","maintainers":[{"name":"ascend42","email":"ascend@a-company.org"}],"dist":{"shasum":"2fc2ce8066e2c1a8de2c1fc4a4185257dba98c07","tarball":"https://registry.npmjs.org/@a-company/atelier-canvas/-/atelier-canvas-0.25.1.tgz","fileCount":8,"integrity":"sha512-+3jS17w2Rq+1YeTqvPHqhUjG7DTOxHy7fTvkkba5C53d+QOmgS+dFLmSQqzMha2FK8Ulg/nxV78V/bxvkBqReQ==","signatures":[{"sig":"MEYCIQCYnj+QGk/2M2FZIb+3bE6p0CBnvx889GZUYTULKdyATQIhAL7E4bnk/5rN0gg4grW1W6lVEXuWj/vWMaLLk1yZ3giw","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":187473},"main":"./dist/index.cjs","type":"module","_from":"file:a-company-atelier-canvas-0.25.1.tgz","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"test":"vitest run","build":"tsup","clean":"rm -rf dist","typecheck":"tsc --noEmit"},"_npmUser":{"name":"ascend42","email":"ascend@a-company.org"},"_resolved":"/private/var/folders/gq/nt2kdpsj39dc8vpq6cfzx8gw0000gn/T/f5c44987a9ad2923809e5bcb4ce3c5d2/a-company-atelier-canvas-0.25.1.tgz","_integrity":"sha512-+3jS17w2Rq+1YeTqvPHqhUjG7DTOxHy7fTvkkba5C53d+QOmgS+dFLmSQqzMha2FK8Ulg/nxV78V/bxvkBqReQ==","_npmVersion":"11.7.0","description":"Canvas 2D renderer and playback controller","directories":{},"_nodeVersion":"24.12.0","dependencies":{"@a-company/atelier-core":"0.25.1","@a-company/atelier-math":"0.25.1","@a-company/atelier-types":"0.25.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.4.0","vitest":"^3.0.0","typescript":"^5.7.0"},"_npmOperationalInternal":{"tmp":"tmp/atelier-canvas_0.25.1_1771887816544_0.5193348800855824","host":"s3://npm-registry-packages-npm-production"}},"0.26.0":{"name":"@a-company/atelier-canvas","version":"0.26.0","publishConfig":{"access":"public"},"description":"Canvas 2D renderer and playback controller","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"}},"dependencies":{"@a-company/atelier-types":"0.26.0","@a-company/atelier-core":"0.26.0","@a-company/atelier-math":"0.25.1"},"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-canvas@0.26.0","_integrity":"sha512-8UlKTzpNREgFyWk6zZvmuAF+FvTg+Q0sgdIONs7oHXAZxaeRNDPqvTU7hPTMCuIw9l/PVYVk8zVFEKcjnA6dfg==","_resolved":"/private/var/folders/gq/nt2kdpsj39dc8vpq6cfzx8gw0000gn/T/9e30b6121bd3b42344f792d64604bbf9/a-company-atelier-canvas-0.26.0.tgz","_from":"file:a-company-atelier-canvas-0.26.0.tgz","_nodeVersion":"24.12.0","_npmVersion":"11.7.0","dist":{"integrity":"sha512-8UlKTzpNREgFyWk6zZvmuAF+FvTg+Q0sgdIONs7oHXAZxaeRNDPqvTU7hPTMCuIw9l/PVYVk8zVFEKcjnA6dfg==","shasum":"915dd873e957be79d05050030fa084f2f3fd1370","tarball":"https://registry.npmjs.org/@a-company/atelier-canvas/-/atelier-canvas-0.26.0.tgz","fileCount":8,"unpackedSize":197161,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCCUJTZkByCaafUFnYm7MKnpe95x62+9xr8Z5Z+3/nK8gIgGtRxcgoJimHpWLGgVXmOLd8H1TFAvqQhQuaDFVS6nyo="}]},"_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-canvas_0.26.0_1778200997671_0.8742859720226173"},"_hasShrinkwrap":false}},"time":{"created":"2026-02-23T23:03:36.473Z","modified":"2026-05-08T00:43:17.958Z","0.25.1":"2026-02-23T23:03:36.712Z","0.26.0":"2026-05-08T00:43:17.846Z"},"description":"Canvas 2D renderer and playback controller","maintainers":[{"name":"ascend42","email":"ascend@a-company.org"}],"readme":"---\ntitle: \"@atelier/canvas\"\nscope: Canvas 2D renderer — renderFrame, shape/text renderers, RenderContext, styles\npackages: [\"@atelier/canvas\"]\nrelated: [\"docs/rendering-pipeline.md\", \"docs/architecture.md\", \"packages/core/README.md\"]\n---\n\n# @atelier/canvas\n\nCanvas 2D renderer and playback controller for Atelier animation documents. Takes a resolved frame from `@atelier/core` and draws it to any Canvas 2D-compatible context -- browser `CanvasRenderingContext2D`, `node-canvas`, or any object that implements the `RenderContext` interface.\n\n## Package Info\n\n| Field | Value |\n|-------|-------|\n| **Name** | `@atelier/canvas` |\n| **Version** | `0.1.0` |\n| **Description** | Canvas 2D renderer and playback controller |\n| **Dependencies** | `@atelier/types` (workspace), `@atelier/core` (workspace) |\n| **Build** | tsup (ESM + CJS + DTS, with sourcemaps) |\n| **Source** | `packages/canvas/src/` |\n| **Entry** | `src/index.ts` |\n\n## Installation\n\n```bash\npnpm add @atelier/canvas\n```\n\nThis package requires its workspace siblings `@atelier/types` and `@atelier/core`.\n\n## Exports\n\n```typescript\n// Main render entry\nexport { renderFrame } from \"./render-frame.js\";\n\n// Types\nexport type { RenderContext, GradientLike } from \"./canvas-types.js\";\nexport type { EffectiveLayer } from \"./apply-properties.js\";\n\n// Utilities (useful for custom renderers)\nexport { buildEffectiveLayer } from \"./apply-properties.js\";\nexport { colorToCSS, applyFill, applyStroke } from \"./styles.js\";\nexport { renderShape } from \"./renderers/shape-renderer.js\";\nexport { renderText } from \"./renderers/text-renderer.js\";\n```\n\n## Usage\n\n```typescript\nimport { resolveFrame } from \"@atelier/core\";\nimport { renderFrame } from \"@atelier/canvas\";\n\nconst canvas = document.getElementById(\"canvas\") as HTMLCanvasElement;\nconst ctx = canvas.getContext(\"2d\")!;\nconst resolved = resolveFrame(doc, \"intro\", frameNumber);\nrenderFrame(ctx, resolved, doc);\n```\n\nFor a playback loop:\n\n```typescript\nimport { resolveFrame } from \"@atelier/core\";\nimport { renderFrame } from \"@atelier/canvas\";\n\nfunction animate(doc: AtelierDocument, sceneName: string) {\n  const canvas = document.getElementById(\"canvas\") as HTMLCanvasElement;\n  const ctx = canvas.getContext(\"2d\")!;\n  let frame = 0;\n\n  function tick() {\n    const resolved = resolveFrame(doc, sceneName, frame);\n    renderFrame(ctx, resolved, doc);\n    frame++;\n    requestAnimationFrame(tick);\n  }\n\n  requestAnimationFrame(tick);\n}\n```\n\n## Modules\n\nThe package contains five modules, each with a single responsibility.\n\n---\n\n### 1. render-frame.ts -- Main Entry\n\n**File:** `packages/canvas/src/render-frame.ts`\n\n```typescript\nfunction renderFrame(\n  ctx: RenderContext,\n  resolvedFrame: ResolvedFrame,\n  doc: AtelierDocument,\n): void;\n```\n\nThe top-level render function. Clears the canvas and draws all visible layers for a single resolved frame.\n\n**Algorithm:**\n\n1. **Clear canvas** with `doc.canvas.background` (falls back to `\"transparent\"` if unset).\n2. **Iterate layers** in order (painters algorithm -- first layer in the array = backmost).\n3. **Skip invisible layers**: layers with `visible === false` are skipped entirely.\n4. **Build effective layer**: call `buildEffectiveLayer()` to merge computed animation properties over layer defaults.\n5. **Skip fully transparent layers**: layers with `opacity <= 0` are skipped after property computation.\n6. **For each visible layer:**\n   - `ctx.save()` -- push state\n   - Apply `globalAlpha` (opacity)\n   - `translate(x, y)` to layer position\n   - Apply anchor-relative rotation and scale: translate to anchor point, rotate (degrees to radians), scale, translate back\n   - Dispatch to type-specific renderer based on `layer.visual.type`:\n     - `\"shape\"` calls `renderShape()`\n     - `\"text\"` calls `renderText()`\n     - `\"image\"`, `\"group\"`, `\"ref\"` -- deferred (not yet implemented)\n   - `ctx.restore()` -- pop state\n\n---\n\n### 2. canvas-types.ts -- RenderContext Interface\n\n**File:** `packages/canvas/src/canvas-types.ts`\n\n```typescript\ninterface RenderContext {\n  // State\n  save(): void;\n  restore(): void;\n\n  // Transform\n  translate(x: number, y: number): void;\n  rotate(angle: number): void;\n  scale(x: number, y: number): void;\n\n  // Rect\n  fillRect(x: number, y: number, w: number, h: number): void;\n  strokeRect(x: number, y: number, w: number, h: number): void;\n\n  // Path\n  beginPath(): void;\n  closePath(): void;\n  moveTo(x: number, y: number): void;\n  lineTo(x: number, y: number): void;\n  bezierCurveTo(\n    cp1x: number, cp1y: number,\n    cp2x: number, cp2y: number,\n    x: number, y: number,\n  ): void;\n  fill(): void;\n  stroke(): void;\n\n  // Ellipse\n  ellipse(\n    x: number, y: number,\n    radiusX: number, radiusY: number,\n    rotation: number, startAngle: number, endAngle: number,\n  ): void;\n\n  // Rounded rect (optional -- fallback to fillRect/strokeRect if absent)\n  roundRect?(x: number, y: number, w: number, h: number, radii: number | number[]): void;\n\n  // Text\n  fillText(text: string, x: number, y: number): void;\n  strokeText(text: string, x: number, y: number): void;\n\n  // Style properties\n  fillStyle: string | object;\n  strokeStyle: string | object;\n  lineWidth: number;\n  lineCap: string;\n  lineJoin: string;\n  globalAlpha: number;\n  font: string;\n  textAlign: string;\n  textBaseline: string;\n\n  // Line dash\n  setLineDash(segments: number[]): void;\n\n  // Gradient factories\n  createLinearGradient(x0: number, y0: number, x1: number, y1: number): GradientLike;\n  createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): GradientLike;\n\n  // Canvas dimensions\n  canvas: { width: number; height: number };\n}\n\ninterface GradientLike {\n  addColorStop(offset: number, color: string): void;\n}\n```\n\nThis is a **minimal subset** of the Canvas 2D API. It includes only the methods and properties that the Atelier renderer actually uses, which means:\n\n- It is compatible with browser `CanvasRenderingContext2D` out of the box.\n- It is compatible with `node-canvas` (server-side rendering).\n- It avoids any dependency on the DOM `lib` typings, keeping the package environment-agnostic.\n- Custom implementations can be provided for testing or alternative render targets.\n\nThe `roundRect` method is marked optional (`roundRect?`) because it is not available in all environments. When absent, the shape renderer falls back to `fillRect`/`strokeRect`.\n\n---\n\n### 3. apply-properties.ts -- Effective Layer Computation\n\n**File:** `packages/canvas/src/apply-properties.ts`\n\n```typescript\ninterface EffectiveLayer {\n  layer: Layer;          // Original layer reference\n  x: number;             // Effective X position (px)\n  y: number;             // Effective Y position (px)\n  width: number;         // Effective width (px)\n  height: number;        // Effective height (px)\n  opacity: number;       // Effective opacity (0..1)\n  rotation: number;      // Effective rotation (degrees)\n  scaleX: number;        // Effective horizontal scale\n  scaleY: number;        // Effective vertical scale\n  anchorX: number;       // Anchor point X (0..1, fraction of width)\n  anchorY: number;       // Anchor point Y (0..1, fraction of height)\n}\n\nfunction buildEffectiveLayer(\n  resolved: ResolvedLayer,\n  parentWidth: number,\n  parentHeight: number,\n): EffectiveLayer;\n```\n\nMerges computed animation properties (produced by the delta resolver in `@atelier/core`) over the layer's default values. This is where animation values take effect at render time.\n\n**Property resolution priority:** `computedProperties[prop] ?? layer.defaultValue`\n\n**Computed property keys:** `frame.x`, `frame.y`, `bounds.width`, `bounds.height`, `opacity`, `rotation`, `scale.x`, `scale.y`, `anchorPoint.x`, `anchorPoint.y`\n\n**Unit resolution:** String values ending in `%` are resolved to pixels against the parent dimension (e.g., `\"50%\"` with a parent width of 800 becomes 400). Numeric values pass through unchanged.\n\n---\n\n### 4. renderers/shape-renderer.ts -- Shape Rendering\n\n**File:** `packages/canvas/src/renderers/shape-renderer.ts`\n\n```typescript\nfunction renderShape(ctx: RenderContext, eff: EffectiveLayer): void;\n```\n\nDispatches to internal rendering functions based on `shape.type`:\n\n**Rect:**\n- If `cornerRadius` is set and `ctx.roundRect` is available, uses `roundRect()` for rounded corners.\n- Otherwise falls back to `fillRect()`/`strokeRect()`.\n- Applies fill first, then stroke.\n\n**Ellipse:**\n- Uses `ctx.ellipse()` centered within the layer bounds (`width/2, height/2` as center, `width/2, height/2` as radii).\n- Full arc from 0 to 2pi.\n- Applies fill first, then stroke.\n\n**Path:**\n- Requires at least 2 points; returns early otherwise.\n- Iterates `PathPoint[]`. Uses `bezierCurveTo()` when both `prev.out` and `curr.in` control points exist. Uses `lineTo()` for straight segments.\n- Control points are relative offsets from their parent point.\n- Closes the path if `closed: true`.\n- Applies fill first, then stroke.\n\nAll shape types apply fill via `applyFill()` and stroke via `applyStroke()` from the styles module.\n\n---\n\n### 5. renderers/text-renderer.ts -- Text Rendering\n\n**File:** `packages/canvas/src/renderers/text-renderer.ts`\n\n```typescript\nfunction renderText(ctx: RenderContext, eff: EffectiveLayer): void;\n```\n\nRenders a text layer using the Canvas 2D text API.\n\n**Steps:**\n\n1. **Build font string:** `\"${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}\"` (defaults: `fontStyle = \"normal\"`, `fontWeight = \"normal\"`).\n2. **Set alignment:** `textAlign` from style (defaults to `\"left\"`), `textBaseline` always set to `\"top\"`.\n3. **Set color:** converts `style.color` to CSS via `colorToCSS()` and assigns to `fillStyle`.\n4. **Compute text X position** based on alignment:\n   - `\"left\"` -- `x = 0` (text flows right from the left edge of bounds)\n   - `\"center\"` -- `x = width / 2` (text centers within bounds)\n   - `\"right\"` -- `x = width` (text flows left from the right edge of bounds)\n5. **Draw:** calls `ctx.fillText(content, textX, 0)`.\n\n---\n\n### styles.ts -- Color and Fill Utilities\n\n**File:** `packages/canvas/src/styles.ts`\n\n```typescript\nfunction colorToCSS(color: Color): string;\nfunction applyFill(ctx: RenderContext, fill: Fill, width: number, height: number): void;\nfunction applyStroke(ctx: RenderContext, stroke: Stroke): void;\n```\n\n**`colorToCSS(color)`:**\nConverts an Atelier `Color` value to a CSS color string.\n- Hex strings (e.g., `\"#ff0000\"`) pass through unchanged.\n- `RGBAColor` objects produce `rgba(r, g, b, a)` (values are rounded to integers for r/g/b).\n- `HSLAColor` objects produce `hsla(h, s%, l%, a)`.\n- Falls back to `\"#000000\"` for unrecognized formats.\n\n**`applyFill(ctx, fill, width, height)`:**\nSets `ctx.fillStyle` based on the fill type:\n- **`\"solid\"`** -- sets `fillStyle` to `colorToCSS(fill.color)`.\n- **`\"linear-gradient\"`** -- computes start/end points from the gradient angle using trigonometry. The gradient line runs through the center of the bounding box. Creates a `CanvasGradient` via `createLinearGradient()` and adds all color stops.\n- **`\"radial-gradient\"`** -- resolves center (`x`, `y`) and radius from the fill definition. Percentage units resolve against `width`/`height` (radius resolves against the larger dimension). Creates a gradient from center with `r0=0` to center with `r1=radius`.\n\n**`applyStroke(ctx, stroke)`:**\nConfigures the context for stroking:\n- Sets `strokeStyle` from `stroke.color` (via `colorToCSS`).\n- Sets `lineWidth` from `stroke.width`.\n- Optionally sets `lineCap`, `lineJoin`, and dash pattern via `setLineDash()`.\n\n## Architecture\n\n```\n@atelier/types    @atelier/core\n      |                |\n      v                v\n  [Layer, Fill,   [ResolvedFrame,\n   Stroke, Color,  ResolvedLayer]\n   ShapeVisual,         |\n   TextVisual]          |\n      |                 |\n      +--------+--------+\n               |\n        @atelier/canvas\n               |\n     +---------+---------+\n     |         |         |\n render-   apply-    styles.ts\n frame.ts  properties.ts  |\n     |         |         |\n     +----+----+---------+\n          |\n    +-----+------+\n    |            |\n shape-      text-\n renderer.ts renderer.ts\n```\n\n`renderFrame` is the orchestrator. It receives a `ResolvedFrame` (with all animation deltas already applied by `@atelier/core`), builds effective layer values via `buildEffectiveLayer`, and dispatches each layer to the appropriate renderer. The renderers use `applyFill` and `applyStroke` from `styles.ts` to configure the canvas context.\n\n## Building\n\n```bash\npnpm run build        # Build ESM + CJS + DTS via tsup\npnpm run typecheck    # Type-check without emitting\npnpm run test         # Run tests via vitest\npnpm run clean        # Remove dist/\n```\n\n## Design Decisions\n\n**Environment-agnostic RenderContext:** The package defines its own `RenderContext` interface instead of depending on DOM typings. This allows the same rendering code to run in browsers, Node.js (via `node-canvas`), and test environments with mock contexts.\n\n**Optional `roundRect`:** Since `roundRect` is not universally available (it was added to the Canvas spec relatively recently), the shape renderer checks for its existence at runtime and falls back gracefully.\n\n**Painters algorithm (back-to-front):** Layers are rendered in array order, with the first layer at the back. This matches the convention used throughout the Atelier document model.\n\n**Anchor-relative transforms:** Rotation and scale are applied around the layer's anchor point, not its origin. The renderer translates to the anchor, applies transforms, then translates back -- a standard technique for anchor-based transformations.\n\n**Deferred renderers:** Image, group, and ref layer types are recognized in the dispatch switch but not yet implemented. They require async asset loading and recursive rendering, respectively, and will be added in a future milestone.\n","readmeFilename":"README.md"}