@snowcone-app/canvas 0.37.1 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/ERROR_CONTRACT.md +39 -8
  2. package/STYLING_CONTRACT.md +24 -2
  3. package/dist/{CanvasRenderer-CTN4j-uO.js → CanvasRenderer-COO5UXtZ.js} +704 -669
  4. package/dist/CanvasRenderer-COO5UXtZ.js.map +1 -0
  5. package/dist/{CanvasStateV1-DLE5d1-d.js → CanvasStateV1-CJJff7Ci.js} +319 -318
  6. package/dist/{CanvasStateV1-DLE5d1-d.js.map → CanvasStateV1-CJJff7Ci.js.map} +1 -1
  7. package/dist/{ContextualToolbars-CG5Gmv_i.js → ContextualToolbars-C62As-xY.js} +202 -202
  8. package/dist/{ContextualToolbars-CG5Gmv_i.js.map → ContextualToolbars-C62As-xY.js.map} +1 -1
  9. package/dist/EditorCanvasImpl-C_72UKu3.js +144 -0
  10. package/dist/EditorCanvasImpl-C_72UKu3.js.map +1 -0
  11. package/dist/{ExportManager-CDX5_up8.js → ExportManager-BbBfO22S.js} +45 -45
  12. package/dist/{ExportManager-CDX5_up8.js.map → ExportManager-BbBfO22S.js.map} +1 -1
  13. package/dist/{FontAnalyzer-CEYA_6fY.js → FontAnalyzer-DWHS41ba.js} +128 -128
  14. package/dist/FontAnalyzer-DWHS41ba.js.map +1 -0
  15. package/dist/{ImagePanel-DLxIDKV-.js → ImagePanel-BaX0UE-L.js} +57 -57
  16. package/dist/{ImagePanel-DLxIDKV-.js.map → ImagePanel-BaX0UE-L.js.map} +1 -1
  17. package/dist/SnowconeCanvas-C-YbGCcQ.js +1112 -0
  18. package/dist/SnowconeCanvas-C-YbGCcQ.js.map +1 -0
  19. package/dist/advanced.mjs +427 -429
  20. package/dist/advanced.mjs.map +1 -1
  21. package/dist/api/placement.d.ts +3 -0
  22. package/dist/components/VisualGuideOverlay.d.ts +4 -1
  23. package/dist/components/editor/EditorPlacements.d.ts +19 -6
  24. package/dist/components/editor/RootInternals.d.ts +7 -0
  25. package/dist/components/editor/designStateStore.d.ts +18 -0
  26. package/dist/components/editor/index.d.ts +3 -1
  27. package/dist/components/editor/placementLinks.d.ts +52 -0
  28. package/dist/components/editor/usePlacements.d.ts +28 -4
  29. package/dist/components/embed-realtime/context.d.ts +20 -4
  30. package/dist/components/embed-realtime/usePieceGuideBundle.d.ts +3 -0
  31. package/dist/components/embed-realtime/validateEmbed.d.ts +6 -3
  32. package/dist/{custom-icons-D51KIAfz.js → custom-icons-o1U13Vcg.js} +14 -14
  33. package/dist/{custom-icons-D51KIAfz.js.map → custom-icons-o1U13Vcg.js.map} +1 -1
  34. package/dist/{design-builder-CqgTpp9B.js → design-builder-CoCtjIwj.js} +86 -86
  35. package/dist/{design-builder-CqgTpp9B.js.map → design-builder-CoCtjIwj.js.map} +1 -1
  36. package/dist/{embed-CLz3CTcX.js → embed-DtlgX9iG.js} +843 -729
  37. package/dist/embed-DtlgX9iG.js.map +1 -0
  38. package/dist/embed.mjs +28 -28
  39. package/dist/errors/editorError.d.ts +7 -2
  40. package/dist/{guards-DldCuuJq.js → guards-DMmhw_Ow.js} +4 -4
  41. package/dist/{guards-DldCuuJq.js.map → guards-DMmhw_Ow.js.map} +1 -1
  42. package/dist/index.mjs +21 -21
  43. package/dist/internals.mjs +108 -108
  44. package/dist/internals.mjs.map +1 -1
  45. package/dist/placement-Br5jI_-u.js +90 -0
  46. package/dist/placement-Br5jI_-u.js.map +1 -0
  47. package/dist/placement.d.ts +2 -0
  48. package/dist/placement.mjs +5 -0
  49. package/dist/placement.mjs.map +1 -0
  50. package/dist/rendering/PieceGuideRenderer.d.ts +4 -1
  51. package/dist/state/CanvasStateV1.d.ts +3 -3
  52. package/dist/template-fields-Dltc4Ztb.js +556 -0
  53. package/dist/template-fields-Dltc4Ztb.js.map +1 -0
  54. package/dist/testing.mjs +24 -24
  55. package/dist/types/index.d.ts +2 -1
  56. package/dist/{useCommands-CQi4Pyd2.js → useCommands-x8J7_Mzy.js} +262 -262
  57. package/dist/{useCommands-CQi4Pyd2.js.map → useCommands-x8J7_Mzy.js.map} +1 -1
  58. package/dist/usePieceGuideBundle-Cab2gRnn.js +68 -0
  59. package/dist/usePieceGuideBundle-Cab2gRnn.js.map +1 -0
  60. package/dist/utils/pieceRotation.d.ts +35 -0
  61. package/dist/{validation-THw5W6N_.js → validation-Dh9sVEb9.js} +1182 -1184
  62. package/dist/validation-Dh9sVEb9.js.map +1 -0
  63. package/example/package.json +1 -1
  64. package/package.json +7 -2
  65. package/dist/CanvasRenderer-CTN4j-uO.js.map +0 -1
  66. package/dist/EditorCanvasImpl-BRdFWZA5.js +0 -81
  67. package/dist/EditorCanvasImpl-BRdFWZA5.js.map +0 -1
  68. package/dist/FontAnalyzer-CEYA_6fY.js.map +0 -1
  69. package/dist/SnowconeCanvas-BaFR8QXs.js +0 -1196
  70. package/dist/SnowconeCanvas-BaFR8QXs.js.map +0 -1
  71. package/dist/embed-CLz3CTcX.js.map +0 -1
  72. package/dist/template-fields-D8uOutD4.js +0 -505
  73. package/dist/template-fields-D8uOutD4.js.map +0 -1
  74. package/dist/usePieceGuideBundle-BUQKb0Ja.js +0 -61
  75. package/dist/usePieceGuideBundle-BUQKb0Ja.js.map +0 -1
  76. package/dist/validation-THw5W6N_.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"internals.mjs","sources":["../src/rendering/image-adjustments.ts","../src/rendering/image-renderer.ts","../src/rendering/shape-renderer.ts"],"sourcesContent":["/**\n * Image adjustments (brightness / contrast / saturation) — the SINGLE pixel\n * pipeline shared by the live editor renderer and the export worker.\n *\n * This is the HEAVY side of the seam in `src/core/image-adjustments.ts`\n * (D9 element/renderer split): importing this module registers the\n * adjusted-source provider that `ImageElement.render()` looks up at paint\n * time. `rendering/element-painters.ts` imports it, so every paint driver\n * (CanvasRenderer, ExportManager, the export worker) carries it; the\n * no-canvas state graph never does.\n *\n * ## Why a pixel pass and not `ctx.filter`\n *\n * `CanvasRenderingContext2D.filter` is NOT available on Safari / iOS Safari:\n * caniuse (mdn-api_canvasrenderingcontext2d_filter, checked 2026-06) lists\n * Safari ≤ 17.6 \"Not supported\" and Safari 18.0 → 26.x \"Disabled by default\"\n * (same for iOS). iOS Safari is a hard requirement for this package, so the\n * pixel pass must exist regardless — and once it exists, using it EVERYWHERE\n * (instead of a ctx.filter fast path on Chromium/Firefox) guarantees the live\n * canvas, the client export worker, and the server renderer produce identical\n * pixels with one implementation. Browser-native filter pipelines differ in\n * intermediate precision, so a hybrid risks live-vs-export drift for zero\n * functional gain.\n *\n * ## Performance contract\n *\n * The pass runs ONCE per (source, adjustments) pair and the result is cached\n * (see {@link getAdjustedImageSource}); the 60fps render loop only ever pays\n * one extra `drawImage` of the cached canvas. Per-frame `getImageData` never\n * happens.\n *\n * ## Math\n *\n * Equivalent to the CSS Filter Effects spec functions, applied in the fixed\n * order `brightness() contrast() saturate()` on non-premultiplied sRGB:\n *\n * brightness(b): v' = v·b\n * contrast(c): v' = v·c + 127.5·(1 − c)\n * saturate(s): feColorMatrix type=\"saturate\" (luma 0.213 / 0.715 / 0.072)\n *\n * Alpha is untouched. Results clamp to [0, 255] via Uint8ClampedArray.\n *\n * Worker-safe: no DOM access at module level; the canvas factory prefers\n * `OffscreenCanvas` and falls back to `document.createElement('canvas')`.\n */\n\nimport type { ImageAdjustments } from '../types/index.js';\nimport {\n isNeutralAdjustments,\n adjustmentsCacheKey,\n registerAdjustedImageSourceProvider,\n type AdjustableImageSource,\n} from '../core/image-adjustments.js';\nimport { createLogger } from '../utils/logger.js';\n\n// Re-export the model-side helpers so heavy-side consumers (serializers,\n// worker renderer, tests) have a single import surface.\nexport {\n NEUTRAL_ADJUSTMENTS,\n isNeutralAdjustments,\n normalizeAdjustments,\n adjustmentsCacheKey,\n type AdjustableImageSource,\n} from '../core/image-adjustments.js';\n\nconst logger = createLogger('image-adjustments');\n\nconst EPSILON = 0.001;\n\n/**\n * Apply adjustments to raw RGBA pixels IN PLACE.\n *\n * Pure (no canvas, no DOM) — safe in workers and directly unit-testable.\n * Brightness and contrast are fused into a single 256-entry LUT; saturation\n * applies the feColorMatrix \"saturate\" matrix per pixel (skipped when ~1).\n */\nexport function applyAdjustmentsToPixels(data: Uint8ClampedArray, adjustments: ImageAdjustments): void {\n const { brightness, contrast, saturation } = adjustments;\n\n // Fused brightness+contrast transfer: v' = (v·b)·c + 127.5·(1 − c)\n const slope = brightness * contrast;\n const intercept = 127.5 * (1 - contrast);\n const applyLut = Math.abs(slope - 1) > EPSILON || Math.abs(intercept) > EPSILON;\n const lut = new Uint8ClampedArray(256);\n if (applyLut) {\n for (let v = 0; v < 256; v++) lut[v] = v * slope + intercept; // clamps on write\n }\n\n const applySaturation = Math.abs(saturation - 1) > EPSILON;\n // feColorMatrix type=\"saturate\" coefficients (Filter Effects spec).\n const s = saturation;\n const rr = 0.213 + 0.787 * s;\n const rg = 0.715 - 0.715 * s;\n const rb = 0.072 - 0.072 * s;\n const gr = 0.213 - 0.213 * s;\n const gg = 0.715 + 0.285 * s;\n const gb = 0.072 - 0.072 * s;\n const br = 0.213 - 0.213 * s;\n const bg = 0.715 - 0.715 * s;\n const bb = 0.072 + 0.928 * s;\n\n for (let i = 0; i < data.length; i += 4) {\n let r = data[i];\n let g = data[i + 1];\n let b = data[i + 2];\n\n if (applyLut) {\n r = lut[r];\n g = lut[g];\n b = lut[b];\n }\n\n if (applySaturation) {\n const r2 = rr * r + rg * g + rb * b;\n const g2 = gr * r + gg * g + gb * b;\n const b2 = br * r + bg * g + bb * b;\n r = r2;\n g = g2;\n b = b2;\n }\n\n data[i] = r;\n data[i + 1] = g;\n data[i + 2] = b;\n // alpha (i + 3) untouched\n }\n}\n\n// ---------------------------------------------------------------------------\n// Adjusted-source cache\n// ---------------------------------------------------------------------------\n\n/**\n * Per-source LRU of adjusted variants. WeakMap keyed by the SOURCE OBJECT so\n * entries vanish with the source (e.g. when ImageCache evicts an image or the\n * worker clears a bitmap). The inner map is bounded — slider drags produce a\n * stream of distinct adjustment values and must not accumulate full-size\n * canvases.\n */\nconst adjustedSourceCache = new WeakMap<object, Map<string, AdjustableImageSource>>();\n\n/** Max adjusted variants kept per source (oldest evicted first). */\nconst MAX_VARIANTS_PER_SOURCE = 4;\n\nfunction sourcePixelWidth(source: AdjustableImageSource): number {\n const natural = (source as HTMLImageElement).naturalWidth;\n return typeof natural === 'number' && natural > 0 ? natural : source.width;\n}\n\nfunction sourcePixelHeight(source: AdjustableImageSource): number {\n const natural = (source as HTMLImageElement).naturalHeight;\n return typeof natural === 'number' && natural > 0 ? natural : source.height;\n}\n\n/** Create a 2D canvas in whatever environment we're in (worker or DOM). */\nfunction createCanvas(width: number, height: number): HTMLCanvasElement | OffscreenCanvas {\n if (typeof OffscreenCanvas !== 'undefined') {\n return new OffscreenCanvas(width, height);\n }\n if (typeof document !== 'undefined') {\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n return canvas;\n }\n throw new Error('image-adjustments: no canvas implementation available (no OffscreenCanvas, no DOM)');\n}\n\n/** Run the pixel pass once, producing an adjusted copy of the source. */\nfunction renderAdjustedSource(\n source: AdjustableImageSource,\n adjustments: ImageAdjustments\n): AdjustableImageSource {\n const width = sourcePixelWidth(source);\n const height = sourcePixelHeight(source);\n if (width <= 0 || height <= 0) {\n throw new Error('image-adjustments: source has no pixels (width/height is 0)');\n }\n\n const canvas = createCanvas(width, height);\n const ctx = canvas.getContext('2d') as\n | CanvasRenderingContext2D\n | OffscreenCanvasRenderingContext2D\n | null;\n if (!ctx) {\n throw new Error('image-adjustments: could not acquire a 2d context');\n }\n\n ctx.drawImage(source, 0, 0, width, height);\n // Throws SecurityError on a tainted canvas — all canvas-bound images load\n // with crossOrigin=\"anonymous\" (CLAUDE.md gotcha #13), so taint here means\n // a host bug; the catch in getAdjustedImageSource degrades + caches.\n const imageData = ctx.getImageData(0, 0, width, height);\n applyAdjustmentsToPixels(imageData.data, adjustments);\n ctx.putImageData(imageData, 0, 0);\n return canvas as AdjustableImageSource;\n}\n\n/**\n * Get a drawable source with the adjustments baked in. Neutral adjustments\n * return the original source unchanged.\n *\n * Cached per (source object, adjustments value): the pixel pass runs once per\n * distinct pair; every subsequent call — i.e. every render frame — is a map\n * lookup. FAILED passes are cached too (as the original source) so a broken\n * environment degrades to \"no adjustment applied\" without retrying the\n * expensive pass every frame (the gotcha-#28 retry-loop lesson).\n */\nexport function getAdjustedImageSource(\n source: AdjustableImageSource,\n adjustments: ImageAdjustments | undefined | null\n): AdjustableImageSource {\n if (isNeutralAdjustments(adjustments)) return source;\n\n const key = adjustmentsCacheKey(adjustments);\n let variants = adjustedSourceCache.get(source);\n const cached = variants?.get(key);\n if (cached) {\n // Refresh LRU position.\n variants!.delete(key);\n variants!.set(key, cached);\n return cached;\n }\n\n let result: AdjustableImageSource;\n try {\n result = renderAdjustedSource(source, adjustments!);\n } catch (error) {\n logger.error('adjustment pixel pass failed — rendering UNADJUSTED source:', error);\n result = source;\n }\n\n if (!variants) {\n variants = new Map();\n adjustedSourceCache.set(source, variants);\n }\n variants.set(key, result);\n while (variants.size > MAX_VARIANTS_PER_SOURCE) {\n const oldest = variants.keys().next().value as string;\n variants.delete(oldest);\n }\n\n return result;\n}\n\n/** Test-only: drop every cached variant for a source. */\nexport function clearAdjustedSourceCacheFor(source: object): void {\n adjustedSourceCache.delete(source);\n}\n\n// Register as THE adjusted-source provider for the element model\n// (core/image-adjustments.ts seam) at module evaluation.\nregisterAdjustedImageSourceProvider(getAdjustedImageSource);\n","/**\n * Image Renderer - Image element rendering with crop, flip, border radius, and stroke.\n *\n * Works in both main thread and Web Worker (via OffscreenCanvas).\n */\n\nimport type { ImageElementConfig } from '../types/index.js';\nimport { renderImageStroke } from './StrokeRenderer.js';\nimport { getAdjustedImageSource, type AdjustableImageSource } from './image-adjustments.js';\nimport type { RenderContext, SerializedImageElement } from './renderer-types.js';\n\n/**\n * Convert a server-render image element's crop rectangle from FRACTIONS (0–1)\n * to source pixels, using the fetched bitmap's real dimensions.\n *\n * The server-render serializers (`serializeStateForServer`,\n * `flattenImageElement`) store crop as fractions and stamp\n * `needsCropPixelConversion` because they have no bitmap at serialize time. The\n * export worker holds the bitmap and calls this right before drawing. Without\n * it, `drawImage` reads e.g. `cropWidth=1` as a 1px source rect and stretches a\n * single corner pixel across the whole element — the layer renders as a solid\n * black/transparent block. Mirrors the client-side conversion in\n * `element-serializer` (which runs at serialize time because the client DOES\n * have the bitmap). No-op unless the flag is set and a crop is present, so the\n * already-pixel client path is untouched and the conversion can't run twice.\n */\nexport function resolveServerCrop(\n el: SerializedImageElement,\n bitmap: { width: number; height: number }\n): SerializedImageElement {\n if (!el.needsCropPixelConversion || el.cropWidth === undefined) return el;\n return {\n ...el,\n cropX: (el.cropX ?? 0) * bitmap.width,\n cropY: (el.cropY ?? 0) * bitmap.height,\n cropWidth: el.cropWidth * bitmap.width,\n cropHeight: (el.cropHeight ?? 1) * bitmap.height,\n needsCropPixelConversion: false,\n };\n}\n\n/**\n * Render an image element using ImageBitmap\n * Works in both main thread and worker\n */\nexport function renderImageElement(\n ctx: RenderContext,\n elementData: SerializedImageElement,\n imageBitmap: ImageBitmap\n): void {\n // Brightness/contrast/saturation: bake into a cached copy of the source\n // bitmap FIRST (no-op when neutral/absent) so crop, flip and border-radius\n // below composite adjusted pixels — same order as the live ImageElement\n // renderer. Crop source rects stay valid: the adjusted copy has identical\n // pixel dimensions.\n const source = getAdjustedImageSource(\n imageBitmap as AdjustableImageSource,\n elementData.adjustments\n );\n\n ctx.save();\n\n // Translate to element position\n ctx.translate(elementData.x, elementData.y);\n\n // Apply rotation (negative for clockwise)\n if (elementData.rotation) {\n ctx.rotate((-elementData.rotation * Math.PI) / 180);\n }\n\n // Apply flip transformations\n let scaleX = 1;\n let scaleY = 1;\n if (elementData.flipHorizontal) scaleX = -1;\n if (elementData.flipVertical) scaleY = -1;\n if (scaleX !== 1 || scaleY !== 1) {\n ctx.scale(scaleX, scaleY);\n }\n\n // Apply opacity if specified\n if (elementData.opacity !== undefined && elementData.opacity !== 1) {\n ctx.globalAlpha = elementData.opacity;\n }\n\n // Apply border radius if specified (borderRadius is stored as percentage 0-100)\n if (elementData.borderRadius && elementData.borderRadius > 0) {\n const radius = Math.min(\n (elementData.borderRadius / 100) * Math.min(elementData.width, elementData.height),\n elementData.width / 2,\n elementData.height / 2\n );\n\n // Create clipping path for rounded corners\n ctx.beginPath();\n const x = -elementData.width / 2;\n const y = -elementData.height / 2;\n const w = elementData.width;\n const h = elementData.height;\n\n ctx.moveTo(x + radius, y);\n ctx.lineTo(x + w - radius, y);\n ctx.arcTo(x + w, y, x + w, y + radius, radius);\n ctx.lineTo(x + w, y + h - radius);\n ctx.arcTo(x + w, y + h, x + w - radius, y + h, radius);\n ctx.lineTo(x + radius, y + h);\n ctx.arcTo(x, y + h, x, y + h - radius, radius);\n ctx.lineTo(x, y + radius);\n ctx.arcTo(x, y, x + radius, y, radius);\n ctx.closePath();\n ctx.clip();\n }\n\n // Draw the image\n if (elementData.cropX !== undefined && elementData.cropWidth !== undefined) {\n // Draw with crop\n ctx.drawImage(\n source,\n elementData.cropX,\n elementData.cropY || 0,\n elementData.cropWidth,\n elementData.cropHeight || imageBitmap.height,\n -elementData.width / 2,\n -elementData.height / 2,\n elementData.width,\n elementData.height\n );\n } else {\n // Draw without crop (centered)\n ctx.drawImage(source, -elementData.width / 2, -elementData.height / 2, elementData.width, elementData.height);\n }\n\n ctx.restore();\n\n // Render stroke if enabled (after restore to apply fresh transforms)\n if (elementData.stroke?.enabled) {\n const hasKnockoutStroke = elementData.knockoutParts?.stroke === true;\n\n const imageConfig: ImageElementConfig = {\n transformType: 'image',\n x: elementData.x,\n y: elementData.y,\n rotation: elementData.rotation,\n stroke: elementData.stroke,\n knockoutParts: elementData.knockoutParts,\n transformData: {\n type: 'image',\n width: elementData.width,\n height: elementData.height,\n borderRadius: elementData.borderRadius || 0,\n },\n };\n\n if (hasKnockoutStroke) {\n // For knockout stroke, use offscreen canvas approach\n const canvas = ctx.canvas;\n let offscreen: HTMLCanvasElement | OffscreenCanvas;\n\n if (typeof OffscreenCanvas !== 'undefined') {\n offscreen = new OffscreenCanvas(canvas.width, canvas.height);\n } else if (typeof document !== 'undefined') {\n offscreen = document.createElement('canvas');\n offscreen.width = canvas.width;\n offscreen.height = canvas.height;\n } else {\n renderImageStroke(ctx, imageConfig);\n return;\n }\n\n const offCtx = offscreen.getContext('2d');\n if (!offCtx) {\n renderImageStroke(ctx, imageConfig);\n return;\n }\n\n // Render the stroke to the offscreen canvas with knockout flag\n renderImageStroke(offCtx, imageConfig, { isKnockout: true });\n\n // Composite with destination-out\n ctx.save();\n ctx.globalCompositeOperation = 'destination-out';\n ctx.drawImage(offscreen as CanvasImageSource, 0, 0);\n ctx.restore();\n } else {\n // Normal stroke\n renderImageStroke(ctx, imageConfig);\n }\n }\n}\n","/**\n * Shape Renderer - Shape element rendering (rectangle, circle, ellipse, triangle, etc.)\n *\n * Works in both main thread and Web Worker (via OffscreenCanvas).\n *\n * Opacity strategy: When a shape has both element-level opacity AND a stroke,\n * we render fill+stroke at full opacity on a temporary canvas, then composite\n * the result at the element's opacity. This prevents fill/stroke overlap from\n * compounding (e.g., 50% fill + 50% stroke overlap ≠ 75% opaque).\n */\n\nimport type { RenderContext, SerializedShapeElement } from './renderer-types.js';\nimport { createShapeFillStyle } from '../core/shape-fill.js';\n\ntype CanvasLike = HTMLCanvasElement | OffscreenCanvas;\n\nfunction createTempCanvas(width: number, height: number): CanvasLike {\n if (typeof OffscreenCanvas !== 'undefined') {\n return new OffscreenCanvas(width, height);\n }\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n return canvas;\n}\n\n/**\n * Render a shape element\n * Works in both main thread and worker\n */\nexport function renderShapeElement(ctx: RenderContext, elementData: SerializedShapeElement): void {\n const elementOpacity = elementData.opacity ?? 1;\n const hasStroke = !!elementData.stroke?.enabled;\n const needsOffscreen = elementOpacity < 1 && hasStroke;\n\n if (needsOffscreen) {\n // Render fill+stroke at full opacity on a temp canvas, then composite\n renderShapeWithOffscreen(ctx, elementData, elementOpacity);\n } else {\n // No compounding risk — render directly\n renderShapeDirect(ctx, elementData, elementOpacity);\n }\n}\n\n/**\n * Render shape via temporary canvas to avoid fill/stroke opacity compounding.\n */\nfunction renderShapeWithOffscreen(\n ctx: RenderContext,\n elementData: SerializedShapeElement,\n elementOpacity: number\n): void {\n const td = elementData.transformData;\n const strokeWidth = elementData.stroke?.width || 2;\n // Pad temp canvas to fit the stroke (strokes extend beyond the shape bounds)\n const padding = strokeWidth + 2;\n const offW = Math.ceil(td.width + padding * 2);\n const offH = Math.ceil(td.height + padding * 2);\n\n const offCanvas = createTempCanvas(offW, offH);\n const offCtx = offCanvas.getContext('2d') as RenderContext;\n if (!offCtx) {\n // Fallback to direct render\n renderShapeDirect(ctx, elementData, elementOpacity);\n return;\n }\n\n // Render at center of temp canvas with full opacity (elementOpacity = 1)\n const tempData: SerializedShapeElement = {\n ...elementData,\n x: offW / 2,\n y: offH / 2,\n opacity: 1,\n };\n renderShapeDirect(offCtx, tempData, 1);\n\n // Composite temp canvas onto main canvas at element opacity\n ctx.save();\n\n // Position: elementData.(x,y) is the shape center, temp canvas center is (offW/2, offH/2)\n // We need to account for rotation around the element center\n ctx.translate(elementData.x, elementData.y);\n if (elementData.rotation) {\n ctx.rotate((-elementData.rotation * Math.PI) / 180);\n }\n\n ctx.globalAlpha = elementOpacity;\n ctx.drawImage(offCanvas, -offW / 2, -offH / 2);\n ctx.restore();\n}\n\n/**\n * Render shape directly onto ctx (no offscreen). Used when there's no\n * compounding risk, or as the inner render for the offscreen path.\n */\nfunction renderShapeDirect(\n ctx: RenderContext,\n elementData: SerializedShapeElement,\n elementOpacity: number\n): void {\n const transformData = elementData.transformData;\n const fillOpacity = transformData.fillOpacity ?? 1;\n\n ctx.save();\n ctx.translate(elementData.x, elementData.y);\n\n if (elementData.rotation) {\n ctx.rotate((-elementData.rotation * Math.PI) / 180);\n }\n\n // Fill — gradients are computed in local coords (post-translate/rotate),\n // so live render and worker export agree at every rotation.\n ctx.globalAlpha = elementOpacity * fillOpacity;\n ctx.fillStyle = createShapeFillStyle(ctx, transformData);\n ctx.beginPath();\n traceShapePath(ctx, transformData);\n ctx.fill();\n ctx.restore();\n\n // Stroke\n if (elementData.stroke?.enabled) {\n ctx.save();\n ctx.translate(elementData.x, elementData.y);\n if (elementData.rotation) {\n ctx.rotate((-elementData.rotation * Math.PI) / 180);\n }\n\n const stroke = elementData.stroke;\n ctx.globalAlpha = elementOpacity * (stroke.opacity ?? 1);\n ctx.strokeStyle = stroke.color || '#000000';\n ctx.lineWidth = stroke.width || 2;\n ctx.lineCap = stroke.lineCap || 'round';\n ctx.lineJoin = stroke.lineJoin || 'round';\n\n ctx.beginPath();\n traceShapePath(ctx, transformData);\n ctx.stroke();\n ctx.restore();\n }\n}\n\n/**\n * Trace the shape path (without fill/stroke) — reusable for both fill and stroke passes.\n */\nfunction traceShapePath(\n ctx: RenderContext,\n transformData: SerializedShapeElement['transformData']\n): void {\n const { shapeType, width, height } = transformData;\n\n switch (shapeType) {\n case 'rectangle': {\n const borderRadius = transformData.borderRadius || 0;\n const x = -width / 2;\n const y = -height / 2;\n\n if (borderRadius > 0) {\n const radius = Math.min((borderRadius / 100) * Math.min(width, height), width / 2, height / 2);\n ctx.roundRect(x, y, width, height, radius);\n } else {\n ctx.rect(x, y, width, height);\n }\n break;\n }\n\n case 'circle': {\n const radius = Math.min(width, height) / 2;\n ctx.arc(0, 0, radius, 0, Math.PI * 2);\n break;\n }\n\n case 'ellipse': {\n ctx.ellipse(0, 0, width / 2, height / 2, 0, 0, Math.PI * 2);\n break;\n }\n\n case 'triangle': {\n const halfWidth = width / 2;\n const halfHeight = height / 2;\n ctx.moveTo(0, -halfHeight);\n ctx.lineTo(halfWidth, halfHeight);\n ctx.lineTo(-halfWidth, halfHeight);\n ctx.closePath();\n break;\n }\n\n case 'polygon': {\n const sides = transformData.sides || 5;\n const radius = Math.min(width, height) / 2;\n for (let i = 0; i < sides; i++) {\n const angle = (i * 2 * Math.PI) / sides - Math.PI / 2;\n const px = radius * Math.cos(angle);\n const py = radius * Math.sin(angle);\n if (i === 0) {\n ctx.moveTo(px, py);\n } else {\n ctx.lineTo(px, py);\n }\n }\n ctx.closePath();\n break;\n }\n\n case 'star': {\n const points = transformData.points || 5;\n const outerRadius = Math.min(width, height) / 2;\n const innerRadius = outerRadius * (transformData.innerRadius || 0.4);\n for (let i = 0; i < points * 2; i++) {\n const angle = (i * Math.PI) / points - Math.PI / 2;\n const r = i % 2 === 0 ? outerRadius : innerRadius;\n const px = r * Math.cos(angle);\n const py = r * Math.sin(angle);\n if (i === 0) {\n ctx.moveTo(px, py);\n } else {\n ctx.lineTo(px, py);\n }\n }\n ctx.closePath();\n break;\n }\n\n case 'line': {\n const halfWidth = width / 2;\n const halfHeight = height / 2;\n ctx.rect(-halfWidth, -halfHeight, width, height);\n break;\n }\n\n default:\n ctx.rect(-width / 2, -height / 2, width, height);\n }\n}\n"],"names":["logger","createLogger","EPSILON","applyAdjustmentsToPixels","data","adjustments","brightness","contrast","saturation","slope","intercept","applyLut","lut","v","applySaturation","s","rr","rg","rb","gr","gg","gb","br","bg","bb","i","r","g","b","r2","g2","b2","adjustedSourceCache","MAX_VARIANTS_PER_SOURCE","sourcePixelWidth","source","natural","sourcePixelHeight","createCanvas","width","height","canvas","renderAdjustedSource","ctx","imageData","getAdjustedImageSource","isNeutralAdjustments","key","adjustmentsCacheKey","variants","cached","result","error","oldest","registerAdjustedImageSourceProvider","resolveServerCrop","el","bitmap","renderImageElement","elementData","imageBitmap","scaleX","scaleY","radius","x","y","w","h","_a","hasKnockoutStroke","_b","imageConfig","offscreen","renderImageStroke","offCtx","createTempCanvas","renderShapeElement","elementOpacity","hasStroke","renderShapeWithOffscreen","renderShapeDirect","td","padding","offW","offH","offCanvas","tempData","transformData","fillOpacity","createShapeFillStyle","traceShapePath","stroke","shapeType","borderRadius","halfWidth","halfHeight","sides","angle","px","py","points","outerRadius","innerRadius"],"mappings":"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"}
1
+ {"version":3,"file":"internals.mjs","sources":["../src/rendering/image-adjustments.ts","../src/rendering/image-renderer.ts","../src/rendering/shape-renderer.ts"],"sourcesContent":["/**\n * Image adjustments (brightness / contrast / saturation) — the SINGLE pixel\n * pipeline shared by the live editor renderer and the export worker.\n *\n * This is the HEAVY side of the seam in `src/core/image-adjustments.ts`\n * (D9 element/renderer split): importing this module registers the\n * adjusted-source provider that `ImageElement.render()` looks up at paint\n * time. `rendering/element-painters.ts` imports it, so every paint driver\n * (CanvasRenderer, ExportManager, the export worker) carries it; the\n * no-canvas state graph never does.\n *\n * ## Why a pixel pass and not `ctx.filter`\n *\n * `CanvasRenderingContext2D.filter` is NOT available on Safari / iOS Safari:\n * caniuse (mdn-api_canvasrenderingcontext2d_filter, checked 2026-06) lists\n * Safari ≤ 17.6 \"Not supported\" and Safari 18.0 → 26.x \"Disabled by default\"\n * (same for iOS). iOS Safari is a hard requirement for this package, so the\n * pixel pass must exist regardless — and once it exists, using it EVERYWHERE\n * (instead of a ctx.filter fast path on Chromium/Firefox) guarantees the live\n * canvas, the client export worker, and the server renderer produce identical\n * pixels with one implementation. Browser-native filter pipelines differ in\n * intermediate precision, so a hybrid risks live-vs-export drift for zero\n * functional gain.\n *\n * ## Performance contract\n *\n * The pass runs ONCE per (source, adjustments) pair and the result is cached\n * (see {@link getAdjustedImageSource}); the 60fps render loop only ever pays\n * one extra `drawImage` of the cached canvas. Per-frame `getImageData` never\n * happens.\n *\n * ## Math\n *\n * Equivalent to the CSS Filter Effects spec functions, applied in the fixed\n * order `brightness() contrast() saturate()` on non-premultiplied sRGB:\n *\n * brightness(b): v' = v·b\n * contrast(c): v' = v·c + 127.5·(1 − c)\n * saturate(s): feColorMatrix type=\"saturate\" (luma 0.213 / 0.715 / 0.072)\n *\n * Alpha is untouched. Results clamp to [0, 255] via Uint8ClampedArray.\n *\n * Worker-safe: no DOM access at module level; the canvas factory prefers\n * `OffscreenCanvas` and falls back to `document.createElement('canvas')`.\n */\n\nimport type { ImageAdjustments } from '../types/index.js';\nimport {\n isNeutralAdjustments,\n adjustmentsCacheKey,\n registerAdjustedImageSourceProvider,\n type AdjustableImageSource,\n} from '../core/image-adjustments.js';\nimport { createLogger } from '../utils/logger.js';\n\n// Re-export the model-side helpers so heavy-side consumers (serializers,\n// worker renderer, tests) have a single import surface.\nexport {\n NEUTRAL_ADJUSTMENTS,\n isNeutralAdjustments,\n normalizeAdjustments,\n adjustmentsCacheKey,\n type AdjustableImageSource,\n} from '../core/image-adjustments.js';\n\nconst logger = createLogger('image-adjustments');\n\nconst EPSILON = 0.001;\n\n/**\n * Apply adjustments to raw RGBA pixels IN PLACE.\n *\n * Pure (no canvas, no DOM) — safe in workers and directly unit-testable.\n * Brightness and contrast are fused into a single 256-entry LUT; saturation\n * applies the feColorMatrix \"saturate\" matrix per pixel (skipped when ~1).\n */\nexport function applyAdjustmentsToPixels(data: Uint8ClampedArray, adjustments: ImageAdjustments): void {\n const { brightness, contrast, saturation } = adjustments;\n\n // Fused brightness+contrast transfer: v' = (v·b)·c + 127.5·(1 − c)\n const slope = brightness * contrast;\n const intercept = 127.5 * (1 - contrast);\n const applyLut = Math.abs(slope - 1) > EPSILON || Math.abs(intercept) > EPSILON;\n const lut = new Uint8ClampedArray(256);\n if (applyLut) {\n for (let v = 0; v < 256; v++) lut[v] = v * slope + intercept; // clamps on write\n }\n\n const applySaturation = Math.abs(saturation - 1) > EPSILON;\n // feColorMatrix type=\"saturate\" coefficients (Filter Effects spec).\n const s = saturation;\n const rr = 0.213 + 0.787 * s;\n const rg = 0.715 - 0.715 * s;\n const rb = 0.072 - 0.072 * s;\n const gr = 0.213 - 0.213 * s;\n const gg = 0.715 + 0.285 * s;\n const gb = 0.072 - 0.072 * s;\n const br = 0.213 - 0.213 * s;\n const bg = 0.715 - 0.715 * s;\n const bb = 0.072 + 0.928 * s;\n\n for (let i = 0; i < data.length; i += 4) {\n let r = data[i];\n let g = data[i + 1];\n let b = data[i + 2];\n\n if (applyLut) {\n r = lut[r];\n g = lut[g];\n b = lut[b];\n }\n\n if (applySaturation) {\n const r2 = rr * r + rg * g + rb * b;\n const g2 = gr * r + gg * g + gb * b;\n const b2 = br * r + bg * g + bb * b;\n r = r2;\n g = g2;\n b = b2;\n }\n\n data[i] = r;\n data[i + 1] = g;\n data[i + 2] = b;\n // alpha (i + 3) untouched\n }\n}\n\n// ---------------------------------------------------------------------------\n// Adjusted-source cache\n// ---------------------------------------------------------------------------\n\n/**\n * Per-source LRU of adjusted variants. WeakMap keyed by the SOURCE OBJECT so\n * entries vanish with the source (e.g. when ImageCache evicts an image or the\n * worker clears a bitmap). The inner map is bounded — slider drags produce a\n * stream of distinct adjustment values and must not accumulate full-size\n * canvases.\n */\nconst adjustedSourceCache = new WeakMap<object, Map<string, AdjustableImageSource>>();\n\n/** Max adjusted variants kept per source (oldest evicted first). */\nconst MAX_VARIANTS_PER_SOURCE = 4;\n\nfunction sourcePixelWidth(source: AdjustableImageSource): number {\n const natural = (source as HTMLImageElement).naturalWidth;\n return typeof natural === 'number' && natural > 0 ? natural : source.width;\n}\n\nfunction sourcePixelHeight(source: AdjustableImageSource): number {\n const natural = (source as HTMLImageElement).naturalHeight;\n return typeof natural === 'number' && natural > 0 ? natural : source.height;\n}\n\n/** Create a 2D canvas in whatever environment we're in (worker or DOM). */\nfunction createCanvas(width: number, height: number): HTMLCanvasElement | OffscreenCanvas {\n if (typeof OffscreenCanvas !== 'undefined') {\n return new OffscreenCanvas(width, height);\n }\n if (typeof document !== 'undefined') {\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n return canvas;\n }\n throw new Error('image-adjustments: no canvas implementation available (no OffscreenCanvas, no DOM)');\n}\n\n/** Run the pixel pass once, producing an adjusted copy of the source. */\nfunction renderAdjustedSource(\n source: AdjustableImageSource,\n adjustments: ImageAdjustments\n): AdjustableImageSource {\n const width = sourcePixelWidth(source);\n const height = sourcePixelHeight(source);\n if (width <= 0 || height <= 0) {\n throw new Error('image-adjustments: source has no pixels (width/height is 0)');\n }\n\n const canvas = createCanvas(width, height);\n const ctx = canvas.getContext('2d') as\n | CanvasRenderingContext2D\n | OffscreenCanvasRenderingContext2D\n | null;\n if (!ctx) {\n throw new Error('image-adjustments: could not acquire a 2d context');\n }\n\n ctx.drawImage(source, 0, 0, width, height);\n // Throws SecurityError on a tainted canvas — all canvas-bound images load\n // with crossOrigin=\"anonymous\" (CLAUDE.md gotcha #13), so taint here means\n // a host bug; the catch in getAdjustedImageSource degrades + caches.\n const imageData = ctx.getImageData(0, 0, width, height);\n applyAdjustmentsToPixels(imageData.data, adjustments);\n ctx.putImageData(imageData, 0, 0);\n return canvas as AdjustableImageSource;\n}\n\n/**\n * Get a drawable source with the adjustments baked in. Neutral adjustments\n * return the original source unchanged.\n *\n * Cached per (source object, adjustments value): the pixel pass runs once per\n * distinct pair; every subsequent call — i.e. every render frame — is a map\n * lookup. FAILED passes are cached too (as the original source) so a broken\n * environment degrades to \"no adjustment applied\" without retrying the\n * expensive pass every frame (the gotcha-#28 retry-loop lesson).\n */\nexport function getAdjustedImageSource(\n source: AdjustableImageSource,\n adjustments: ImageAdjustments | undefined | null\n): AdjustableImageSource {\n if (isNeutralAdjustments(adjustments)) return source;\n\n const key = adjustmentsCacheKey(adjustments);\n let variants = adjustedSourceCache.get(source);\n const cached = variants?.get(key);\n if (cached) {\n // Refresh LRU position.\n variants!.delete(key);\n variants!.set(key, cached);\n return cached;\n }\n\n let result: AdjustableImageSource;\n try {\n result = renderAdjustedSource(source, adjustments!);\n } catch (error) {\n logger.error('adjustment pixel pass failed — rendering UNADJUSTED source:', error);\n result = source;\n }\n\n if (!variants) {\n variants = new Map();\n adjustedSourceCache.set(source, variants);\n }\n variants.set(key, result);\n while (variants.size > MAX_VARIANTS_PER_SOURCE) {\n const oldest = variants.keys().next().value as string;\n variants.delete(oldest);\n }\n\n return result;\n}\n\n/** Test-only: drop every cached variant for a source. */\nexport function clearAdjustedSourceCacheFor(source: object): void {\n adjustedSourceCache.delete(source);\n}\n\n// Register as THE adjusted-source provider for the element model\n// (core/image-adjustments.ts seam) at module evaluation.\nregisterAdjustedImageSourceProvider(getAdjustedImageSource);\n","/**\n * Image Renderer - Image element rendering with crop, flip, border radius, and stroke.\n *\n * Works in both main thread and Web Worker (via OffscreenCanvas).\n */\n\nimport type { ImageElementConfig } from '../types/index.js';\nimport { renderImageStroke } from './StrokeRenderer.js';\nimport { getAdjustedImageSource, type AdjustableImageSource } from './image-adjustments.js';\nimport type { RenderContext, SerializedImageElement } from './renderer-types.js';\n\n/**\n * Convert a server-render image element's crop rectangle from FRACTIONS (0–1)\n * to source pixels, using the fetched bitmap's real dimensions.\n *\n * The server-render serializers (`serializeStateForServer`,\n * `flattenImageElement`) store crop as fractions and stamp\n * `needsCropPixelConversion` because they have no bitmap at serialize time. The\n * export worker holds the bitmap and calls this right before drawing. Without\n * it, `drawImage` reads e.g. `cropWidth=1` as a 1px source rect and stretches a\n * single corner pixel across the whole element — the layer renders as a solid\n * black/transparent block. Mirrors the client-side conversion in\n * `element-serializer` (which runs at serialize time because the client DOES\n * have the bitmap). No-op unless the flag is set and a crop is present, so the\n * already-pixel client path is untouched and the conversion can't run twice.\n */\nexport function resolveServerCrop(\n el: SerializedImageElement,\n bitmap: { width: number; height: number }\n): SerializedImageElement {\n if (!el.needsCropPixelConversion || el.cropWidth === undefined) return el;\n return {\n ...el,\n cropX: (el.cropX ?? 0) * bitmap.width,\n cropY: (el.cropY ?? 0) * bitmap.height,\n cropWidth: el.cropWidth * bitmap.width,\n cropHeight: (el.cropHeight ?? 1) * bitmap.height,\n needsCropPixelConversion: false,\n };\n}\n\n/**\n * Render an image element using ImageBitmap\n * Works in both main thread and worker\n */\nexport function renderImageElement(\n ctx: RenderContext,\n elementData: SerializedImageElement,\n imageBitmap: ImageBitmap\n): void {\n // Brightness/contrast/saturation: bake into a cached copy of the source\n // bitmap FIRST (no-op when neutral/absent) so crop, flip and border-radius\n // below composite adjusted pixels — same order as the live ImageElement\n // renderer. Crop source rects stay valid: the adjusted copy has identical\n // pixel dimensions.\n const source = getAdjustedImageSource(\n imageBitmap as AdjustableImageSource,\n elementData.adjustments\n );\n\n ctx.save();\n\n // Translate to element position\n ctx.translate(elementData.x, elementData.y);\n\n // Apply rotation (negative for clockwise)\n if (elementData.rotation) {\n ctx.rotate((-elementData.rotation * Math.PI) / 180);\n }\n\n // Apply flip transformations\n let scaleX = 1;\n let scaleY = 1;\n if (elementData.flipHorizontal) scaleX = -1;\n if (elementData.flipVertical) scaleY = -1;\n if (scaleX !== 1 || scaleY !== 1) {\n ctx.scale(scaleX, scaleY);\n }\n\n // Apply opacity if specified\n if (elementData.opacity !== undefined && elementData.opacity !== 1) {\n ctx.globalAlpha = elementData.opacity;\n }\n\n // Apply border radius if specified (borderRadius is stored as percentage 0-100)\n if (elementData.borderRadius && elementData.borderRadius > 0) {\n const radius = Math.min(\n (elementData.borderRadius / 100) * Math.min(elementData.width, elementData.height),\n elementData.width / 2,\n elementData.height / 2\n );\n\n // Create clipping path for rounded corners\n ctx.beginPath();\n const x = -elementData.width / 2;\n const y = -elementData.height / 2;\n const w = elementData.width;\n const h = elementData.height;\n\n ctx.moveTo(x + radius, y);\n ctx.lineTo(x + w - radius, y);\n ctx.arcTo(x + w, y, x + w, y + radius, radius);\n ctx.lineTo(x + w, y + h - radius);\n ctx.arcTo(x + w, y + h, x + w - radius, y + h, radius);\n ctx.lineTo(x + radius, y + h);\n ctx.arcTo(x, y + h, x, y + h - radius, radius);\n ctx.lineTo(x, y + radius);\n ctx.arcTo(x, y, x + radius, y, radius);\n ctx.closePath();\n ctx.clip();\n }\n\n // Draw the image\n if (elementData.cropX !== undefined && elementData.cropWidth !== undefined) {\n // Draw with crop\n ctx.drawImage(\n source,\n elementData.cropX,\n elementData.cropY || 0,\n elementData.cropWidth,\n elementData.cropHeight || imageBitmap.height,\n -elementData.width / 2,\n -elementData.height / 2,\n elementData.width,\n elementData.height\n );\n } else {\n // Draw without crop (centered)\n ctx.drawImage(source, -elementData.width / 2, -elementData.height / 2, elementData.width, elementData.height);\n }\n\n ctx.restore();\n\n // Render stroke if enabled (after restore to apply fresh transforms)\n if (elementData.stroke?.enabled) {\n const hasKnockoutStroke = elementData.knockoutParts?.stroke === true;\n\n const imageConfig: ImageElementConfig = {\n transformType: 'image',\n x: elementData.x,\n y: elementData.y,\n rotation: elementData.rotation,\n stroke: elementData.stroke,\n knockoutParts: elementData.knockoutParts,\n transformData: {\n type: 'image',\n width: elementData.width,\n height: elementData.height,\n borderRadius: elementData.borderRadius || 0,\n },\n };\n\n if (hasKnockoutStroke) {\n // For knockout stroke, use offscreen canvas approach\n const canvas = ctx.canvas;\n let offscreen: HTMLCanvasElement | OffscreenCanvas;\n\n if (typeof OffscreenCanvas !== 'undefined') {\n offscreen = new OffscreenCanvas(canvas.width, canvas.height);\n } else if (typeof document !== 'undefined') {\n offscreen = document.createElement('canvas');\n offscreen.width = canvas.width;\n offscreen.height = canvas.height;\n } else {\n renderImageStroke(ctx, imageConfig);\n return;\n }\n\n const offCtx = offscreen.getContext('2d');\n if (!offCtx) {\n renderImageStroke(ctx, imageConfig);\n return;\n }\n\n // Render the stroke to the offscreen canvas with knockout flag\n renderImageStroke(offCtx, imageConfig, { isKnockout: true });\n\n // Composite with destination-out\n ctx.save();\n ctx.globalCompositeOperation = 'destination-out';\n ctx.drawImage(offscreen as CanvasImageSource, 0, 0);\n ctx.restore();\n } else {\n // Normal stroke\n renderImageStroke(ctx, imageConfig);\n }\n }\n}\n","/**\n * Shape Renderer - Shape element rendering (rectangle, circle, ellipse, triangle, etc.)\n *\n * Works in both main thread and Web Worker (via OffscreenCanvas).\n *\n * Opacity strategy: When a shape has both element-level opacity AND a stroke,\n * we render fill+stroke at full opacity on a temporary canvas, then composite\n * the result at the element's opacity. This prevents fill/stroke overlap from\n * compounding (e.g., 50% fill + 50% stroke overlap ≠ 75% opaque).\n */\n\nimport type { RenderContext, SerializedShapeElement } from './renderer-types.js';\nimport { createShapeFillStyle } from '../core/shape-fill.js';\n\ntype CanvasLike = HTMLCanvasElement | OffscreenCanvas;\n\nfunction createTempCanvas(width: number, height: number): CanvasLike {\n if (typeof OffscreenCanvas !== 'undefined') {\n return new OffscreenCanvas(width, height);\n }\n const canvas = document.createElement('canvas');\n canvas.width = width;\n canvas.height = height;\n return canvas;\n}\n\n/**\n * Render a shape element\n * Works in both main thread and worker\n */\nexport function renderShapeElement(ctx: RenderContext, elementData: SerializedShapeElement): void {\n const elementOpacity = elementData.opacity ?? 1;\n const hasStroke = !!elementData.stroke?.enabled;\n const needsOffscreen = elementOpacity < 1 && hasStroke;\n\n if (needsOffscreen) {\n // Render fill+stroke at full opacity on a temp canvas, then composite\n renderShapeWithOffscreen(ctx, elementData, elementOpacity);\n } else {\n // No compounding risk — render directly\n renderShapeDirect(ctx, elementData, elementOpacity);\n }\n}\n\n/**\n * Render shape via temporary canvas to avoid fill/stroke opacity compounding.\n */\nfunction renderShapeWithOffscreen(\n ctx: RenderContext,\n elementData: SerializedShapeElement,\n elementOpacity: number\n): void {\n const td = elementData.transformData;\n const strokeWidth = elementData.stroke?.width || 2;\n // Pad temp canvas to fit the stroke (strokes extend beyond the shape bounds)\n const padding = strokeWidth + 2;\n const offW = Math.ceil(td.width + padding * 2);\n const offH = Math.ceil(td.height + padding * 2);\n\n const offCanvas = createTempCanvas(offW, offH);\n const offCtx = offCanvas.getContext('2d') as RenderContext;\n if (!offCtx) {\n // Fallback to direct render\n renderShapeDirect(ctx, elementData, elementOpacity);\n return;\n }\n\n // Render at center of temp canvas with full opacity (elementOpacity = 1)\n const tempData: SerializedShapeElement = {\n ...elementData,\n x: offW / 2,\n y: offH / 2,\n opacity: 1,\n };\n renderShapeDirect(offCtx, tempData, 1);\n\n // Composite temp canvas onto main canvas at element opacity\n ctx.save();\n\n // Position: elementData.(x,y) is the shape center, temp canvas center is (offW/2, offH/2)\n // We need to account for rotation around the element center\n ctx.translate(elementData.x, elementData.y);\n if (elementData.rotation) {\n ctx.rotate((-elementData.rotation * Math.PI) / 180);\n }\n\n ctx.globalAlpha = elementOpacity;\n ctx.drawImage(offCanvas, -offW / 2, -offH / 2);\n ctx.restore();\n}\n\n/**\n * Render shape directly onto ctx (no offscreen). Used when there's no\n * compounding risk, or as the inner render for the offscreen path.\n */\nfunction renderShapeDirect(\n ctx: RenderContext,\n elementData: SerializedShapeElement,\n elementOpacity: number\n): void {\n const transformData = elementData.transformData;\n const fillOpacity = transformData.fillOpacity ?? 1;\n\n ctx.save();\n ctx.translate(elementData.x, elementData.y);\n\n if (elementData.rotation) {\n ctx.rotate((-elementData.rotation * Math.PI) / 180);\n }\n\n // Fill — gradients are computed in local coords (post-translate/rotate),\n // so live render and worker export agree at every rotation.\n ctx.globalAlpha = elementOpacity * fillOpacity;\n ctx.fillStyle = createShapeFillStyle(ctx, transformData);\n ctx.beginPath();\n traceShapePath(ctx, transformData);\n ctx.fill();\n ctx.restore();\n\n // Stroke\n if (elementData.stroke?.enabled) {\n ctx.save();\n ctx.translate(elementData.x, elementData.y);\n if (elementData.rotation) {\n ctx.rotate((-elementData.rotation * Math.PI) / 180);\n }\n\n const stroke = elementData.stroke;\n ctx.globalAlpha = elementOpacity * (stroke.opacity ?? 1);\n ctx.strokeStyle = stroke.color || '#000000';\n ctx.lineWidth = stroke.width || 2;\n ctx.lineCap = stroke.lineCap || 'round';\n ctx.lineJoin = stroke.lineJoin || 'round';\n\n ctx.beginPath();\n traceShapePath(ctx, transformData);\n ctx.stroke();\n ctx.restore();\n }\n}\n\n/**\n * Trace the shape path (without fill/stroke) — reusable for both fill and stroke passes.\n */\nfunction traceShapePath(\n ctx: RenderContext,\n transformData: SerializedShapeElement['transformData']\n): void {\n const { shapeType, width, height } = transformData;\n\n switch (shapeType) {\n case 'rectangle': {\n const borderRadius = transformData.borderRadius || 0;\n const x = -width / 2;\n const y = -height / 2;\n\n if (borderRadius > 0) {\n const radius = Math.min((borderRadius / 100) * Math.min(width, height), width / 2, height / 2);\n ctx.roundRect(x, y, width, height, radius);\n } else {\n ctx.rect(x, y, width, height);\n }\n break;\n }\n\n case 'circle': {\n const radius = Math.min(width, height) / 2;\n ctx.arc(0, 0, radius, 0, Math.PI * 2);\n break;\n }\n\n case 'ellipse': {\n ctx.ellipse(0, 0, width / 2, height / 2, 0, 0, Math.PI * 2);\n break;\n }\n\n case 'triangle': {\n const halfWidth = width / 2;\n const halfHeight = height / 2;\n ctx.moveTo(0, -halfHeight);\n ctx.lineTo(halfWidth, halfHeight);\n ctx.lineTo(-halfWidth, halfHeight);\n ctx.closePath();\n break;\n }\n\n case 'polygon': {\n const sides = transformData.sides || 5;\n const radius = Math.min(width, height) / 2;\n for (let i = 0; i < sides; i++) {\n const angle = (i * 2 * Math.PI) / sides - Math.PI / 2;\n const px = radius * Math.cos(angle);\n const py = radius * Math.sin(angle);\n if (i === 0) {\n ctx.moveTo(px, py);\n } else {\n ctx.lineTo(px, py);\n }\n }\n ctx.closePath();\n break;\n }\n\n case 'star': {\n const points = transformData.points || 5;\n const outerRadius = Math.min(width, height) / 2;\n const innerRadius = outerRadius * (transformData.innerRadius || 0.4);\n for (let i = 0; i < points * 2; i++) {\n const angle = (i * Math.PI) / points - Math.PI / 2;\n const r = i % 2 === 0 ? outerRadius : innerRadius;\n const px = r * Math.cos(angle);\n const py = r * Math.sin(angle);\n if (i === 0) {\n ctx.moveTo(px, py);\n } else {\n ctx.lineTo(px, py);\n }\n }\n ctx.closePath();\n break;\n }\n\n case 'line': {\n const halfWidth = width / 2;\n const halfHeight = height / 2;\n ctx.rect(-halfWidth, -halfHeight, width, height);\n break;\n }\n\n default:\n ctx.rect(-width / 2, -height / 2, width, height);\n }\n}\n"],"names":["logger","createLogger","EPSILON","applyAdjustmentsToPixels","data","adjustments","brightness","contrast","saturation","slope","intercept","applyLut","lut","v","applySaturation","s","rr","rg","rb","gr","gg","gb","br","bg","bb","i","r","g","b","r2","g2","b2","adjustedSourceCache","MAX_VARIANTS_PER_SOURCE","sourcePixelWidth","source","natural","sourcePixelHeight","createCanvas","width","height","canvas","renderAdjustedSource","ctx","imageData","getAdjustedImageSource","isNeutralAdjustments","key","adjustmentsCacheKey","variants","cached","result","error","oldest","registerAdjustedImageSourceProvider","resolveServerCrop","el","bitmap","renderImageElement","elementData","imageBitmap","scaleX","scaleY","radius","x","y","w","h","_a","hasKnockoutStroke","_b","imageConfig","offscreen","renderImageStroke","offCtx","createTempCanvas","renderShapeElement","elementOpacity","hasStroke","renderShapeWithOffscreen","renderShapeDirect","td","padding","offW","offH","offCanvas","tempData","transformData","fillOpacity","createShapeFillStyle","traceShapePath","stroke","shapeType","borderRadius","halfWidth","halfHeight","sides","angle","px","py","points","outerRadius","innerRadius"],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ const X = {
2
+ // Vertical alignments (map to top/center/bottom center)
3
+ "far-top": "t",
4
+ top: "t",
5
+ center: "c",
6
+ bottom: "b",
7
+ "far-bottom": "b",
8
+ // Horizontal alignments (map to left/center/right middle)
9
+ "far-left": "l",
10
+ left: "l",
11
+ right: "r",
12
+ "far-right": "r",
13
+ // Direct 9-point values
14
+ tl: "tl",
15
+ t: "t",
16
+ tr: "tr",
17
+ l: "l",
18
+ c: "c",
19
+ r: "r",
20
+ bl: "bl",
21
+ b: "b",
22
+ br: "br"
23
+ };
24
+ function Y(a) {
25
+ return a && X[a] || "c";
26
+ }
27
+ function S(a, e) {
28
+ const r = e.scaleMode || "cover", i = (r === "contain" ? e.marginTop : 0) || 0, A = (r === "contain" ? e.marginRight : 0) || 0, R = (r === "contain" ? e.marginBottom : 0) || 0, f = (r === "contain" ? e.marginLeft : 0) || 0, h = e.width - f - A, l = e.height - i - R, b = h / a.width, g = l / a.height, d = r === "contain" ? Math.min(b, g) : Math.max(b, g), u = e.scale || 1, s = d * u, k = a.width * s, m = a.height * s, w = e.align || "c";
29
+ let t = 0, o = 0;
30
+ const c = k - h, n = m - l;
31
+ switch (w) {
32
+ // Top row
33
+ case "tl":
34
+ t = 0, o = 0;
35
+ break;
36
+ case "t":
37
+ t = -c / 2, o = 0;
38
+ break;
39
+ case "tr":
40
+ t = -c, o = 0;
41
+ break;
42
+ // Middle row
43
+ case "l":
44
+ t = 0, o = -n / 2;
45
+ break;
46
+ case "c":
47
+ // Center (default)
48
+ default:
49
+ t = -c / 2, o = -n / 2;
50
+ break;
51
+ case "r":
52
+ t = -c, o = -n / 2;
53
+ break;
54
+ // Bottom row
55
+ case "bl":
56
+ t = 0, o = -n;
57
+ break;
58
+ case "b":
59
+ t = -c / 2, o = -n;
60
+ break;
61
+ case "br":
62
+ t = -c, o = -n;
63
+ break;
64
+ }
65
+ t += f, o += i;
66
+ const M = e.offsetX || 0, v = e.offsetY || 0, x = c, y = n;
67
+ return t += M * x, o += v * y, t = Math.round(t), o = Math.round(o), {
68
+ // Final scale to apply to artwork
69
+ scale: s,
70
+ // Final artwork dimensions after scaling
71
+ width: Math.round(k),
72
+ height: Math.round(m),
73
+ // Position to draw artwork (relative to artboard origin)
74
+ x: t,
75
+ y: o,
76
+ // Debug info
77
+ debug: {
78
+ coverScale: d,
79
+ userScale: u,
80
+ overflow: { x: c, y: n },
81
+ alignment: w,
82
+ appliedOffset: { x: M * x, y: v * y }
83
+ }
84
+ };
85
+ }
86
+ export {
87
+ S as c,
88
+ Y as g
89
+ };
90
+ //# sourceMappingURL=placement-Br5jI_-u.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"placement-Br5jI_-u.js","sources":["../src/utils/ArtworkPlacement.ts"],"sourcesContent":["/**\n * ArtworkPlacement - Utility for calculating artwork position and scale\n *\n * Uses \"cover\" logic (like CSS object-fit: cover) to ensure artwork fills\n * the entire artboard while maintaining aspect ratio.\n */\n\n/**\n * 9-point alignment options for positioning artwork\n */\nexport type Alignment = 'tl' | 't' | 'tr' | 'l' | 'c' | 'r' | 'bl' | 'b' | 'br';\n\n/**\n * Scale mode for artwork placement\n * - \"cover\" (default): fills entire area, may crop (like CSS object-fit: cover)\n * - \"contain\": fits entirely inside area, may have empty space (like CSS object-fit: contain)\n */\nexport type ScaleMode = 'cover' | 'contain';\n\n/**\n * Placement configuration for artwork positioning\n */\nexport interface PlacementConfig {\n /** Artboard width in pixels */\n width: number;\n /** Artboard height in pixels */\n height: number;\n /** User scale multiplier (default: 1) */\n scale?: number;\n /** 9-point alignment (default: \"c\" for center) */\n align?: Alignment;\n /** X offset in range -1 to 1 (default: 0) */\n offsetX?: number;\n /** Y offset in range -1 to 1 (default: 0) */\n offsetY?: number;\n /** Scale mode: \"cover\" fills area (may crop), \"contain\" fits inside (may have space). Default: \"cover\" */\n scaleMode?: ScaleMode;\n /** Top margin in pixels (reduces available area in contain mode) */\n marginTop?: number;\n /** Right margin in pixels (reduces available area in contain mode) */\n marginRight?: number;\n /** Bottom margin in pixels (reduces available area in contain mode) */\n marginBottom?: number;\n /** Left margin in pixels (reduces available area in contain mode) */\n marginLeft?: number;\n}\n\n/**\n * Result of artwork placement calculation\n */\nexport interface ArtworkPlacementResult {\n /** Final scale to apply to artwork */\n scale: number;\n /** Final artwork dimensions after scaling */\n width: number;\n height: number;\n /** Position to draw artwork (relative to artboard origin) */\n x: number;\n y: number;\n /** Debug information */\n debug: {\n coverScale: number;\n userScale: number;\n overflow: { x: number; y: number };\n alignment: string;\n appliedOffset: { x: number; y: number };\n };\n}\n\n/**\n * Map string alignment values to 9-point grid\n */\nexport const alignmentMap: Record<string, Alignment> = {\n // Vertical alignments (map to top/center/bottom center)\n 'far-top': 't',\n 'top': 't',\n 'center': 'c',\n 'bottom': 'b',\n 'far-bottom': 'b',\n // Horizontal alignments (map to left/center/right middle)\n 'far-left': 'l',\n 'left': 'l',\n 'right': 'r',\n 'far-right': 'r',\n // Direct 9-point values\n 'tl': 'tl',\n 't': 't',\n 'tr': 'tr',\n 'l': 'l',\n 'c': 'c',\n 'r': 'r',\n 'bl': 'bl',\n 'b': 'b',\n 'br': 'br',\n};\n\n/**\n * Get Alignment from string (supports various naming conventions)\n */\nexport function getAlignment(value: string | Alignment | undefined): Alignment {\n if (!value) return 'c';\n return alignmentMap[value] || 'c';\n}\n\n/**\n * Calculates how to size and position artwork on an artboard\n * Uses \"cover\" logic (like CSS object-fit: cover) to ensure artwork fills the entire area\n *\n * @param artwork - The artwork dimensions\n * @param placement - The placement configuration\n * @returns Positioning and scaling information\n *\n * @example\n * const result = calculateArtworkPlacement(\n * { width: 1000, height: 1000 }, // Square artwork\n * { width: 500, height: 800 } // Tall artboard\n * );\n * // Result: artwork scaled to 800x800, positioned at x: -150, y: 0\n */\nexport function calculateArtworkPlacement(\n artwork: { width: number; height: number },\n placement: PlacementConfig\n): ArtworkPlacementResult {\n // Step 1: Calculate base scale based on scale mode\n const mode = placement.scaleMode || 'cover';\n\n // Margins reduce available area (only in contain mode)\n const mTop = (mode === 'contain' ? placement.marginTop : 0) || 0;\n const mRight = (mode === 'contain' ? placement.marginRight : 0) || 0;\n const mBottom = (mode === 'contain' ? placement.marginBottom : 0) || 0;\n const mLeft = (mode === 'contain' ? placement.marginLeft : 0) || 0;\n\n const effectiveWidth = placement.width - mLeft - mRight;\n const effectiveHeight = placement.height - mTop - mBottom;\n\n const scaleX = effectiveWidth / artwork.width;\n const scaleY = effectiveHeight / artwork.height;\n\n // \"cover\": use LARGER scale so artwork fills entire area (may crop)\n // \"contain\": use SMALLER scale so artwork fits entirely inside (may have space)\n const coverScale = mode === 'contain'\n ? Math.min(scaleX, scaleY)\n : Math.max(scaleX, scaleY);\n\n // Step 2: Apply user's custom scale multiplier\n const userScale = placement.scale || 1;\n const finalScale = coverScale * userScale;\n\n // Step 3: Calculate final artwork dimensions after scaling\n const scaledArtworkWidth = artwork.width * finalScale;\n const scaledArtworkHeight = artwork.height * finalScale;\n\n // Step 4: Calculate base position using alignment\n const alignment = placement.align || 'c'; // default to center\n let x = 0;\n let y = 0;\n\n // Calculate the \"overflow\" relative to effective area\n // In contain mode with margins, overflow is relative to the margin-bounded area\n const overflowX = scaledArtworkWidth - effectiveWidth;\n const overflowY = scaledArtworkHeight - effectiveHeight;\n\n // Apply 9-point alignment\n switch (alignment) {\n // Top row\n case 'tl': // Top-Left\n x = 0;\n y = 0;\n break;\n case 't': // Top-Center\n x = -overflowX / 2;\n y = 0;\n break;\n case 'tr': // Top-Right\n x = -overflowX;\n y = 0;\n break;\n\n // Middle row\n case 'l': // Middle-Left\n x = 0;\n y = -overflowY / 2;\n break;\n case 'c': // Center (default)\n default:\n x = -overflowX / 2;\n y = -overflowY / 2;\n break;\n case 'r': // Middle-Right\n x = -overflowX;\n y = -overflowY / 2;\n break;\n\n // Bottom row\n case 'bl': // Bottom-Left\n x = 0;\n y = -overflowY;\n break;\n case 'b': // Bottom-Center\n x = -overflowX / 2;\n y = -overflowY;\n break;\n case 'br': // Bottom-Right\n x = -overflowX;\n y = -overflowY;\n break;\n }\n\n // Step 5: Apply margin offsets (shift positions into margin-bounded area)\n x += mLeft;\n y += mTop;\n\n // Step 6: Apply user offsets\n // Offsets are in range -1 to 1, so convert to pixel values\n const offsetX = placement.offsetX || 0;\n const offsetY = placement.offsetY || 0;\n\n // The \"range\" is how far we can move the artwork\n const xRange = overflowX;\n const yRange = overflowY;\n\n x += offsetX * xRange;\n y += offsetY * yRange;\n\n // Round to whole pixels\n x = Math.round(x);\n y = Math.round(y);\n\n return {\n // Final scale to apply to artwork\n scale: finalScale,\n\n // Final artwork dimensions after scaling\n width: Math.round(scaledArtworkWidth),\n height: Math.round(scaledArtworkHeight),\n\n // Position to draw artwork (relative to artboard origin)\n x,\n y,\n\n // Debug info\n debug: {\n coverScale,\n userScale,\n overflow: { x: overflowX, y: overflowY },\n alignment,\n appliedOffset: { x: offsetX * xRange, y: offsetY * yRange },\n },\n };\n}\n"],"names":["alignmentMap","getAlignment","value","calculateArtworkPlacement","artwork","placement","mode","mTop","mRight","mBottom","mLeft","effectiveWidth","effectiveHeight","scaleX","scaleY","coverScale","userScale","finalScale","scaledArtworkWidth","scaledArtworkHeight","alignment","x","y","overflowX","overflowY","offsetX","offsetY","xRange","yRange"],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export * from './api/placement'
2
+ export {}
@@ -0,0 +1,5 @@
1
+ import { c as e } from "./placement-Br5jI_-u.js";
2
+ export {
3
+ e as calculateArtworkPlacement
4
+ };
5
+ //# sourceMappingURL=placement.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"placement.mjs","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
@@ -17,8 +17,11 @@ export interface PieceGuideRendererPiece {
17
17
  * legacy centered-inset fallback; otherwise omit.
18
18
  */
19
19
  safeAreaInsetPx?: number;
20
- /** Clockwise rotation in degrees about the piece center. Default 0. */
20
+ /** Clockwise rotation in degrees about the piece center (any multiple of 45). Default 0. */
21
21
  rotation?: number;
22
+ /** Natural (unrotated) size; required when `rotation` isn't a right angle (utils/pieceRotation.ts). */
23
+ naturalWidth?: number;
24
+ naturalHeight?: number;
22
25
  }
23
26
  export interface PieceGuideRendererStyle {
24
27
  /**
@@ -238,7 +238,7 @@ declare const ClipShapeSchema: z.ZodUnion<readonly [z.ZodLiteral<"rectangle">, z
238
238
  d: z.ZodString;
239
239
  x: z.ZodNumber;
240
240
  y: z.ZodNumber;
241
- rotation: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<90>, z.ZodLiteral<180>, z.ZodLiteral<270>]>>;
241
+ rotation: z.ZodOptional<z.ZodNumber>;
242
242
  baseWidth: z.ZodOptional<z.ZodNumber>;
243
243
  baseHeight: z.ZodOptional<z.ZodNumber>;
244
244
  }, z.core.$strip>>;
@@ -270,7 +270,7 @@ declare const SerializedArtboardSchema: z.ZodObject<{
270
270
  d: z.ZodString;
271
271
  x: z.ZodNumber;
272
272
  y: z.ZodNumber;
273
- rotation: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<90>, z.ZodLiteral<180>, z.ZodLiteral<270>]>>;
273
+ rotation: z.ZodOptional<z.ZodNumber>;
274
274
  baseWidth: z.ZodOptional<z.ZodNumber>;
275
275
  baseHeight: z.ZodOptional<z.ZodNumber>;
276
276
  }, z.core.$strip>>;
@@ -336,7 +336,7 @@ declare const CanvasStateV1Schema: z.ZodObject<{
336
336
  d: z.ZodString;
337
337
  x: z.ZodNumber;
338
338
  y: z.ZodNumber;
339
- rotation: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<90>, z.ZodLiteral<180>, z.ZodLiteral<270>]>>;
339
+ rotation: z.ZodOptional<z.ZodNumber>;
340
340
  baseWidth: z.ZodOptional<z.ZodNumber>;
341
341
  baseHeight: z.ZodOptional<z.ZodNumber>;
342
342
  }, z.core.$strip>>;