@snowcone-app/canvas 0.31.0 → 0.32.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.
- package/ERROR_CONTRACT.md +11 -0
- package/STYLING_CONTRACT.md +3 -3
- package/dist/{CanvasRenderer-hpJ05JGO.js → CanvasRenderer-1j-twIbs.js} +443 -431
- package/dist/{CanvasRenderer-hpJ05JGO.js.map → CanvasRenderer-1j-twIbs.js.map} +1 -1
- package/dist/{CanvasStateV1-jLOibojY.js → CanvasStateV1-BmC1JfPv.js} +810 -802
- package/dist/{CanvasStateV1-jLOibojY.js.map → CanvasStateV1-BmC1JfPv.js.map} +1 -1
- package/dist/ContextualToolbars-BKU8IoAM.js +6474 -0
- package/dist/ContextualToolbars-BKU8IoAM.js.map +1 -0
- package/dist/{EditorCanvasImpl-P2GHPNA3.js → EditorCanvasImpl-4voOsj2k.js} +7 -7
- package/dist/{EditorCanvasImpl-P2GHPNA3.js.map → EditorCanvasImpl-4voOsj2k.js.map} +1 -1
- package/dist/{ExportManager-Bkzzyxgw.js → ExportManager-Ci81K6xA.js} +662 -518
- package/dist/ExportManager-Ci81K6xA.js.map +1 -0
- package/dist/{FontAnalyzer-BYhwxDhe.js → FontAnalyzer-BrKY6XYn.js} +1256 -1146
- package/dist/FontAnalyzer-BrKY6XYn.js.map +1 -0
- package/dist/{ImagePanel-CEr0_FYy.js → ImagePanel-ntJBepby.js} +6 -6
- package/dist/{ImagePanel-CEr0_FYy.js.map → ImagePanel-ntJBepby.js.map} +1 -1
- package/dist/{SnowconeCanvas-Fa0isFpO.js → SnowconeCanvas-CgQdiBXv.js} +10 -10
- package/dist/{SnowconeCanvas-Fa0isFpO.js.map → SnowconeCanvas-CgQdiBXv.js.map} +1 -1
- package/dist/advanced.mjs +14 -14
- package/dist/components/ImageToolbar.d.ts +8 -0
- package/dist/components/ShapeToolbar.d.ts +8 -0
- package/dist/components/TextToolbar.d.ts +5 -1
- package/dist/components/toolbars/EmbeddedToolbarLayout.d.ts +4 -0
- package/dist/components/toolbars/GroupElementToolbar.d.ts +8 -0
- package/dist/components/toolbars/SecondaryPanels.d.ts +24 -1
- package/dist/components/toolbars/index.d.ts +2 -2
- package/dist/core/BaseElement.d.ts +2 -1
- package/dist/core/shape-fill.d.ts +31 -0
- package/dist/{custom-icons-Co4rcW0S.js → custom-icons-DTWtkJ9N.js} +2 -2
- package/dist/{custom-icons-Co4rcW0S.js.map → custom-icons-DTWtkJ9N.js.map} +1 -1
- package/dist/{design-builder-CBG2hKc8.js → design-builder-BpbYUYtK.js} +55 -55
- package/dist/{design-builder-CBG2hKc8.js.map → design-builder-BpbYUYtK.js.map} +1 -1
- package/dist/{embed-D4TIzO7g.js → embed-C69x0jHc.js} +10 -10
- package/dist/{embed-D4TIzO7g.js.map → embed-C69x0jHc.js.map} +1 -1
- package/dist/embed.mjs +7 -7
- package/dist/errors/editorError.d.ts +7 -0
- package/dist/{guards-DgZuwVgu.js → guards-BhaQgJrE.js} +2 -2
- package/dist/{guards-DgZuwVgu.js.map → guards-BhaQgJrE.js.map} +1 -1
- package/dist/hooks/elementPatch.d.ts +8 -3
- package/dist/index.mjs +9 -9
- package/dist/internals.mjs +123 -123
- package/dist/internals.mjs.map +1 -1
- package/dist/rendering/LayerEffectsRenderer.d.ts +24 -0
- package/dist/rendering/renderer-types.d.ts +3 -1
- package/dist/state/CanvasStateV1.d.ts +17 -0
- package/dist/style.css +1 -1
- package/dist/{template-fields-CvFcr5zy.js → template-fields-D-2oLhCi.js} +5 -5
- package/dist/{template-fields-CvFcr5zy.js.map → template-fields-D-2oLhCi.js.map} +1 -1
- package/dist/testing.mjs +3 -3
- package/dist/types/index.d.ts +84 -1
- package/dist/types/public.d.ts +3 -2
- package/dist/{useCommands-bPXY9rCh.js → useCommands-DwnZHUg8.js} +435 -418
- package/dist/useCommands-DwnZHUg8.js.map +1 -0
- package/dist/{usePieceGuideBundle-Bj98nPk0.js → usePieceGuideBundle-CnSmYrmM.js} +2 -2
- package/dist/{usePieceGuideBundle-Bj98nPk0.js.map → usePieceGuideBundle-CnSmYrmM.js.map} +1 -1
- package/dist/utils/featureApplied.d.ts +4 -0
- package/dist/{validation-CsTlSvJJ.js → validation-DyUOjRGa.js} +11 -11
- package/dist/{validation-CsTlSvJJ.js.map → validation-DyUOjRGa.js.map} +1 -1
- package/dist/workers/export-worker.bundle.string.d.ts +1 -1
- package/example/package.json +1 -1
- package/package.json +1 -1
- package/dist/ContextualToolbars-DRbuUE7W.js +0 -6061
- package/dist/ContextualToolbars-DRbuUE7W.js.map +0 -1
- package/dist/ExportManager-Bkzzyxgw.js.map +0 -1
- package/dist/FontAnalyzer-BYhwxDhe.js.map +0 -1
- package/dist/useCommands-bPXY9rCh.js.map +0 -1
package/dist/internals.mjs.map
CHANGED
|
@@ -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';\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\n ctx.globalAlpha = elementOpacity * fillOpacity;\n ctx.fillStyle = transformData.fillColor || '#3b82f6';\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","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,24 @@
|
|
|
1
|
+
import { ShadowConfig } from '../types/index.js';
|
|
2
|
+
type LayerEffectsContext = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
|
3
|
+
/** The slice of an element config that layer effects read. */
|
|
4
|
+
export interface LayerEffectsConfig {
|
|
5
|
+
shadow?: ShadowConfig;
|
|
6
|
+
/** BlendMode (or raw string off serialized JSON) — only standard modes apply here */
|
|
7
|
+
blendMode?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* True when the element needs the offscreen raster pass — an enabled shadow
|
|
11
|
+
* and/or a standard blend mode. knockout/clip/'normal'/undefined return false.
|
|
12
|
+
*/
|
|
13
|
+
export declare function hasLayerEffects(config: LayerEffectsConfig | undefined | null): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Render `paint` with the element's layer effects (drop shadow + standard
|
|
16
|
+
* blend mode) applied around it.
|
|
17
|
+
*
|
|
18
|
+
* No effects → `paint(ctx)` directly (zero-cost path, no offscreen).
|
|
19
|
+
* With effects → `paint` runs against an offscreen raster that is then
|
|
20
|
+
* composited onto `ctx`; all shadow/composite state is scoped with
|
|
21
|
+
* save/restore so nothing leaks onto later passes (masks, selection chrome).
|
|
22
|
+
*/
|
|
23
|
+
export declare function renderWithLayerEffects(ctx: LayerEffectsContext, config: LayerEffectsConfig | undefined | null, paint: (targetCtx: LayerEffectsContext) => void): void;
|
|
24
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AnyTransformData, TextAlign, StrokeConfig, OpenTypeFeatures, TextSpan, GlyphOverride, ImageAdjustments } from '../types/index.js';
|
|
1
|
+
import { AnyTransformData, TextAlign, StrokeConfig, OpenTypeFeatures, TextSpan, GlyphOverride, ImageAdjustments, ShapeFill } from '../types/index.js';
|
|
2
2
|
export type RenderContext = CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
|
|
3
3
|
export interface SerializedTextElement {
|
|
4
4
|
id: string;
|
|
@@ -89,6 +89,8 @@ export interface SerializedShapeElement {
|
|
|
89
89
|
innerRadius?: number;
|
|
90
90
|
fillColor?: string;
|
|
91
91
|
fillOpacity?: number;
|
|
92
|
+
/** Rich fill (solid or gradient) — wins over `fillColor` when present. */
|
|
93
|
+
fill?: ShapeFill;
|
|
92
94
|
};
|
|
93
95
|
stroke?: StrokeConfig;
|
|
94
96
|
knockoutParts?: {
|
|
@@ -174,6 +174,23 @@ declare const AnyTransformDataSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
174
174
|
innerRadius: z.ZodOptional<z.ZodNumber>;
|
|
175
175
|
fillColor: z.ZodOptional<z.ZodString>;
|
|
176
176
|
fillOpacity: z.ZodOptional<z.ZodNumber>;
|
|
177
|
+
fill: z.ZodOptional<z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
178
|
+
type: z.ZodLiteral<"solid">;
|
|
179
|
+
color: z.ZodString;
|
|
180
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
181
|
+
type: z.ZodLiteral<"linear">;
|
|
182
|
+
angle: z.ZodNumber;
|
|
183
|
+
stops: z.ZodArray<z.ZodObject<{
|
|
184
|
+
offset: z.ZodNumber;
|
|
185
|
+
color: z.ZodString;
|
|
186
|
+
}, z.core.$strip>>;
|
|
187
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
188
|
+
type: z.ZodLiteral<"radial">;
|
|
189
|
+
stops: z.ZodArray<z.ZodObject<{
|
|
190
|
+
offset: z.ZodNumber;
|
|
191
|
+
color: z.ZodString;
|
|
192
|
+
}, z.core.$strip>>;
|
|
193
|
+
}, z.core.$strip>], "type">>;
|
|
177
194
|
}, z.core.$strip>, z.ZodObject<{
|
|
178
195
|
type: z.ZodLiteral<"path">;
|
|
179
196
|
points: z.ZodArray<z.ZodObject<{
|