@pixerate/editor 0.113.0 → 0.115.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/THIRD_PARTY_NOTICES.md +8 -0
- package/dist/animation-editor/index.cjs +60 -54
- package/dist/animation-editor/index.cjs.map +1 -1
- package/dist/animation-editor/index.d.cts +3 -3
- package/dist/animation-editor/index.d.ts +3 -3
- package/dist/animation-editor/index.js +14 -8
- package/dist/animation-editor/index.js.map +1 -1
- package/dist/builtin-CY97ppKM.d.cts +354 -0
- package/dist/builtin-CY97ppKM.d.ts +354 -0
- package/dist/canvas/index.cjs +3 -3
- package/dist/canvas/index.cjs.map +1 -1
- package/dist/canvas/index.js +9 -9
- package/dist/chunk-26PGXHY7.cjs +753 -0
- package/dist/chunk-26PGXHY7.cjs.map +1 -0
- package/dist/chunk-2OJACTPR.cjs +929 -0
- package/dist/chunk-2OJACTPR.cjs.map +1 -0
- package/dist/chunk-3UFGNHSP.js +929 -0
- package/dist/chunk-3UFGNHSP.js.map +1 -0
- package/dist/chunk-643B2HKC.cjs +534 -0
- package/dist/chunk-643B2HKC.cjs.map +1 -0
- package/dist/chunk-6FCKOS2B.js +534 -0
- package/dist/chunk-6FCKOS2B.js.map +1 -0
- package/dist/chunk-BRLYOPLT.js +2574 -0
- package/dist/chunk-BRLYOPLT.js.map +1 -0
- package/dist/{chunk-LQEEDHOP.js → chunk-C6C5PDKU.js} +759 -761
- package/dist/chunk-C6C5PDKU.js.map +1 -0
- package/dist/{chunk-4KLT7ZYS.cjs → chunk-DFQEX3AM.cjs} +29 -29
- package/dist/{chunk-4KLT7ZYS.cjs.map → chunk-DFQEX3AM.cjs.map} +1 -1
- package/dist/chunk-G5NMLUVF.cjs +2574 -0
- package/dist/chunk-G5NMLUVF.cjs.map +1 -0
- package/dist/{chunk-YGIMK4NX.cjs → chunk-I7FQBWMQ.cjs} +813 -842
- package/dist/chunk-I7FQBWMQ.cjs.map +1 -0
- package/dist/{chunk-LPYOIVLA.js → chunk-LKCYANMW.js} +3 -3
- package/dist/{chunk-W3GXSUSQ.cjs → chunk-LPLCWZTM.cjs} +5087 -1283
- package/dist/chunk-LPLCWZTM.cjs.map +1 -0
- package/dist/chunk-MMVHBKMM.js +753 -0
- package/dist/chunk-MMVHBKMM.js.map +1 -0
- package/dist/{chunk-POGBUYWE.cjs → chunk-ST7E3NEE.cjs} +857 -859
- package/dist/chunk-ST7E3NEE.cjs.map +1 -0
- package/dist/{chunk-7VOHOPLW.cjs → chunk-TCKIXI46.cjs} +59 -59
- package/dist/{chunk-7VOHOPLW.cjs.map → chunk-TCKIXI46.cjs.map} +1 -1
- package/dist/{chunk-XIOMMGPD.cjs → chunk-TFOPHZ3U.cjs} +1 -1
- package/dist/chunk-TFOPHZ3U.cjs.map +1 -0
- package/dist/{chunk-Z4BC6M74.js → chunk-UBIXVXPR.js} +2 -2
- package/dist/{chunk-BLWCAQCW.js → chunk-VXR7XWMF.js} +4574 -770
- package/dist/chunk-VXR7XWMF.js.map +1 -0
- package/dist/{chunk-CZOXTEMN.js → chunk-XICA4PVO.js} +392 -421
- package/dist/chunk-XICA4PVO.js.map +1 -0
- package/dist/{chunk-HNS7UHCM.js → chunk-XK5LZKBD.js} +1 -1
- package/dist/chunk-XK5LZKBD.js.map +1 -0
- package/dist/cms/index.cjs +146 -0
- package/dist/cms/index.cjs.map +1 -0
- package/dist/cms/index.d.cts +51 -0
- package/dist/cms/index.d.ts +51 -0
- package/dist/cms/index.js +146 -0
- package/dist/cms/index.js.map +1 -0
- package/dist/{export-CEXEzqBf.d.cts → export-BJE8T0YA.d.cts} +279 -191
- package/dist/{export-DIkoK8-r.d.ts → export-Con3UKkm.d.ts} +279 -191
- package/dist/image-editor/index.cjs +1848 -1218
- package/dist/image-editor/index.cjs.map +1 -1
- package/dist/image-editor/index.d.cts +85 -93
- package/dist/image-editor/index.d.ts +85 -93
- package/dist/image-editor/index.js +871 -241
- package/dist/image-editor/index.js.map +1 -1
- package/dist/opentype-BZEOSOX3.d.ts +188 -0
- package/dist/opentype-Dpe75vL5.d.cts +188 -0
- package/dist/paint/index.cjs +4 -4
- package/dist/paint/index.d.cts +2 -2
- package/dist/paint/index.d.ts +2 -2
- package/dist/paint/index.js +3 -3
- package/dist/paint-editor/index.cjs +147 -145
- package/dist/paint-editor/index.cjs.map +1 -1
- package/dist/paint-editor/index.d.cts +9 -8
- package/dist/paint-editor/index.d.ts +9 -8
- package/dist/paint-editor/index.js +7 -5
- package/dist/paint-editor/index.js.map +1 -1
- package/dist/raster/index.cjs +53 -3
- package/dist/raster/index.cjs.map +1 -1
- package/dist/raster/index.d.cts +62 -91
- package/dist/raster/index.d.ts +62 -91
- package/dist/raster/index.js +59 -9
- package/dist/{render-BEriFuqK.d.cts → render-B1K_OoLy.d.cts} +87 -2
- package/dist/{render-tjzKaik-.d.ts → render-CrmHOgpo.d.ts} +87 -2
- package/dist/{retouch-CEIKR22W.d.cts → retouch-Bxkuc0KH.d.cts} +1 -1
- package/dist/{retouch-BXmeSCaB.d.ts → retouch-Do6tBs2H.d.ts} +1 -1
- package/dist/{surface-C8RRg_Bh.d.cts → surface-Cn30xIV6.d.cts} +6 -1
- package/dist/{surface-C8RRg_Bh.d.ts → surface-Cn30xIV6.d.ts} +6 -1
- package/dist/{svg-D_cfkhlB.d.cts → svg-BnRj0q8i.d.cts} +16 -4
- package/dist/{svg-wIaKLrU0.d.ts → svg-CmmBFfGT.d.ts} +16 -4
- package/dist/{types-DtL_qLFz.d.ts → types-1SUQKDAw.d.ts} +273 -4
- package/dist/{types-DyQq-WPY.d.cts → types-yINF17Ta.d.cts} +273 -4
- package/dist/vector/index.cjs +12170 -7343
- package/dist/vector/index.cjs.map +1 -1
- package/dist/vector/index.d.cts +1579 -16
- package/dist/vector/index.d.ts +1579 -16
- package/dist/vector/index.js +6303 -1476
- package/dist/vector/index.js.map +1 -1
- package/dist/warp-B39VsOtd.d.cts +348 -0
- package/dist/warp-DMN30aY6.d.ts +348 -0
- package/dist/zlib-dxQdgHyN.d.cts +95 -0
- package/dist/zlib-dxQdgHyN.d.ts +95 -0
- package/package.json +9 -2
- package/skills/pixerate-image-editor/SKILL.md +13 -36
- package/skills/pixerate-image-editor/references/color-management.md +42 -0
- package/skills/pixerate-image-editor/references/photoshop-files.md +41 -0
- package/skills/pixerate-paint-editor/SKILL.md +136 -4
- package/skills/pixerate-vector/SKILL.md +72 -2
- package/dist/chunk-BLWCAQCW.js.map +0 -1
- package/dist/chunk-CZOXTEMN.js.map +0 -1
- package/dist/chunk-HNS7UHCM.js.map +0 -1
- package/dist/chunk-JLYX4V57.cjs +0 -2001
- package/dist/chunk-JLYX4V57.cjs.map +0 -1
- package/dist/chunk-KB2YBCVS.js +0 -2001
- package/dist/chunk-KB2YBCVS.js.map +0 -1
- package/dist/chunk-LQEEDHOP.js.map +0 -1
- package/dist/chunk-POGBUYWE.cjs.map +0 -1
- package/dist/chunk-W3GXSUSQ.cjs.map +0 -1
- package/dist/chunk-XIOMMGPD.cjs.map +0 -1
- package/dist/chunk-YGIMK4NX.cjs.map +0 -1
- package/dist/homography-CZ-tFjay.d.cts +0 -50
- package/dist/homography-Ctfjoc1b.d.ts +0 -50
- package/dist/warp-CWHZzE3x.d.ts +0 -118
- package/dist/warp-D5jz_tN7.d.cts +0 -118
- /package/dist/{chunk-LPYOIVLA.js.map → chunk-LKCYANMW.js.map} +0 -0
- /package/dist/{chunk-Z4BC6M74.js.map → chunk-UBIXVXPR.js.map} +0 -0
|
@@ -1116,4 +1116,4 @@ function blendImageData(backdrop, source, mode, alpha = 1) {
|
|
|
1116
1116
|
|
|
1117
1117
|
|
|
1118
1118
|
exports.TILE = TILE; exports.TILE_SHIFT = TILE_SHIFT; exports.tileKey = tileKey; exports.tileCoord = tileCoord; exports.tileRect = tileRect; exports.TiledSurface = TiledSurface; exports.uniformTile = uniformTile; exports.packRgba = packRgba; exports.finishTile = finishTile; exports.emptySurface = emptySurface; exports.solidSurface = solidSurface; exports.surfaceFromPixels = surfaceFromPixels; exports.tilesInRect = tilesInRect; exports.readRegion = readRegion; exports.readPixel = readPixel; exports.contentBounds = contentBounds; exports.SurfaceDraft = SurfaceDraft; exports.writeRegion = writeRegion; exports.fillRect = fillRect; exports.translateSurface = translateSurface; exports.surfaceBytes = surfaceBytes; exports.collectTiles = collectTiles; exports.surfacesEqual = surfacesEqual; exports.PIGMENT_BANDS = PIGMENT_BANDS; exports.PIGMENT_LAMBDA_MIN = PIGMENT_LAMBDA_MIN; exports.PIGMENT_LAMBDA_STEP = PIGMENT_LAMBDA_STEP; exports.PIGMENT_WAVELENGTHS = PIGMENT_WAVELENGTHS; exports.rgbToReflectance = rgbToReflectance; exports.reflectanceToRgb = reflectanceToRgb; exports.createPigment = createPigment; exports.setPigmentColor = setPigmentColor; exports.mixPigments = mixPigments; exports.lerpPigment = lerpPigment; exports.lerpPigmentInto = lerpPigmentInto; exports.BLEND_MODES = BLEND_MODES; exports.isBlendMode = isBlendMode; exports.PAINTERLY_BLEND_MODES = PAINTERLY_BLEND_MODES; exports.isPainterlyBlendMode = isPainterlyBlendMode; exports.standardBlendMode = standardBlendMode; exports.normalizeBlendMode = normalizeBlendMode; exports.canvasCompositeOperation = canvasCompositeOperation; exports.isSeparableBlendMode = isSeparableBlendMode; exports.blendChannel = blendChannel; exports.blendLum = blendLum; exports.setLum = setLum; exports.blendRgb = blendRgb; exports.SHADOW_AMBIENT = SHADOW_AMBIENT; exports.mixPigmentRgb = mixPigmentRgb; exports.pigment8 = pigment8; exports.mixPigment8 = mixPigment8; exports.blendPixel = blendPixel; exports.blendImageData = blendImageData;
|
|
1119
|
-
//# sourceMappingURL=chunk-
|
|
1119
|
+
//# sourceMappingURL=chunk-TFOPHZ3U.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/home/runner/work/editor/editor/packages/core/dist/chunk-TFOPHZ3U.cjs","../src/raster/surface.ts","../src/raster/pigment.ts","../src/raster/blend.ts"],"names":["lin","weights","clamp01"],"mappings":"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ACA,EAAA;AACD,IAAA;AACC,IAAA;AACD,IAAA;AACC,IAAA;AACD,IAAA;AACD,IAAA;AACD,IAAA;AACA,IAAA;AACC,IAAA;AACD,MAAA;AACG,MAAA;AACD,QAAA;AACF,QAAA;AACA,QAAA;AACA,QAAA;AACN,MAAA;AACF,IAAA;AACF,EAAA;AACS,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACX,EAAA;AACY,EAAA;AACd;AAGS;AACI,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACC,EAAA;AACD,EAAA;AACA,EAAA;AACJ,EAAA;AACM,IAAA;AACJ,IAAA;AACA,IAAA;AACT,EAAA;AACF;AAEM;AAQG;AACI,EAAA;AACC,EAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACD,IAAA;AACI,IAAA;AACb,EAAA;AACY,EAAA;AACA,EAAA;AACA,EAAA;AACF,EAAA;AACJ,EAAA;AACO,EAAA;AACL,IAAA;AACA,IAAA;AACR,EAAA;AACU,EAAA;AACZ;AAES;AACM,EAAA;AACD,IAAA;AACC,IAAA;AACP,IAAA;AACN,EAAA;AACF;AAO4B;AACK;AAOxB;AACI,EAAA;AACD,EAAA;AACA,EAAA;AACE,EAAA;AACN,EAAA;AACO,EAAA;AACF,IAAA;AAEA,MAAA;AACE,MAAA;AACP,QAAA;AACI,QAAA;AACJ,QAAA;AACF,MAAA;AACF,IAAA;AACF,EAAA;AACQ,EAAA;AACV;AAGiB;AACA;AACF;AAGN;AACG,EAAA;AACA,EAAA;AACC,EAAA;AACC,EAAA;AACDA,EAAAA;AACC,EAAA;AACA,EAAA;AACL,EAAA;AACT;AAMgB;AACJ,EAAA;AACA,EAAA;AACC,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACA,EAAA;AACD,EAAA;AACD,EAAA;AACI,IAAA;AACC,IAAA;AACD,IAAA;AACL,IAAA;AACF,IAAA;AACE,IAAA;AACG,IAAA;AACX,EAAA;AACO,EAAA;AACT;AAGgB;AACJ,EAAA;AACF,EAAA;AACA,EAAA;AACA,EAAA;AACK,EAAA;AACD,IAAA;AACF,IAAA;AACG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,IAAA;AACU,IAAA;AACZ,EAAA;AACY,EAAA;AACd;AAOyC;AAMzB;AACL,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACF,IAAA;AACT,EAAA;AACU,EAAA;AACC,IAAA;AACA,IAAA;AACA,IAAA;AACF,IAAA;AACT,EAAA;AACa,EAAA;AACF,EAAA;AACX,EAAA;AACA,EAAA;AACA,EAAA;AACO,EAAA;AACT;AAGM;AAEA;AAMU;AACP,EAAA;AACT;AAES;AACM,EAAA;AACL,EAAA;AACA,EAAA;AACF,IAAA;AACA,IAAA;AACJ,IAAA;AACF,EAAA;AACO,EAAA;AACT;AAMgB;AACL,EAAA;AACE,IAAA;AACA,IAAA;AACA,IAAA;AACT,IAAA;AACF,EAAA;AACA,EAAA;AACF;AAUgB;AACH,EAAA;AACC,EAAA;AACD,EAAA;AACA,EAAA;AACE,EAAA;AACM,EAAA;AACN,EAAA;AACX,IAAA;AACO,IAAA;AACT,EAAA;AACU,EAAA;AACG,EAAA;AACA,IAAA;AACb,EAAA;AACO,EAAA;AACT;AAMgB;AACJ,EAAA;AACA,EAAA;AACE,EAAA;AACA,EAAA;AACA,EAAA;AACN,EAAA;AACO,EAAA;AACA,IAAA;AACP,IAAA;AACI,MAAA;AACA,MAAA;AACA,MAAA;AACN,MAAA;AACO,MAAA;AACA,MAAA;AACA,MAAA;AACA,MAAA;AACP,MAAA;AACF,IAAA;AACS,IAAA;AACC,MAAA;AACA,MAAA;AACV,IAAA;AACU,IAAA;AACH,IAAA;AACI,IAAA;AACA,IAAA;AACA,IAAA;AACb,EAAA;AACF;AH6Ze;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","file":"/home/runner/work/editor/editor/packages/core/dist/chunk-TFOPHZ3U.cjs","sourcesContent":[null,"// Portions ported from photocraft/crates/raster/src/lib.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Tiled, sparse, copy-on-write pixel storage (photocraft's `Surface`): 256 px square tiles in a\n * map keyed by tile coordinate, a default value for every missing tile, and shared tiles.\n *\n * - Pixels are 8-bit, straight alpha: RGBA for layers (4 channels, the same as `ImageData`), one\n * byte for masks.\n * - A surface is never mutated. A write returns a new surface that shares every tile it did not\n * touch, so undo history and caches can keep old surfaces by reference.\n * - A tile whose pixels all hold one value is a *uniform* tile; one instance per value is shared,\n * so a solid fill or a \"hide all\" mask costs one tile however large it is.\n * - Coordinates are document pixels and may be negative or past the canvas (layers can extend\n * beyond it, as PSD layer rects do).\n *\n * The module has no DOM dependency, so it runs in Node, tests and workers.\n */\n\n/** A pixel rectangle (`x`, `y` of the top-left corner, `width`, `height`). */\nexport interface RasterRect {\n x: number;\n y: number;\n width: number;\n height: number;\n}\ntype CropBox = RasterRect;\n\nexport const TILE = 256;\nexport const TILE_SHIFT = 8;\nconst TILE_PIXELS = TILE * TILE;\nconst KEY_OFFSET = 1 << 15;\nconst KEY_SPAN = 1 << 16;\n\n/** A tile coordinate packed into one number (tx, ty in −32768..32767). */\nexport type TileKey = number;\n\nexport function tileKey(tx: number, ty: number): TileKey {\n return (ty + KEY_OFFSET) * KEY_SPAN + (tx + KEY_OFFSET);\n}\n\nexport function tileCoord(key: TileKey): { tx: number; ty: number } {\n return { tx: (key % KEY_SPAN) - KEY_OFFSET, ty: Math.floor(key / KEY_SPAN) - KEY_OFFSET };\n}\n\n/** A tile's pixel rectangle in document space. */\nexport function tileRect(tx: number, ty: number): CropBox {\n return { x: tx * TILE, y: ty * TILE, width: TILE, height: TILE };\n}\n\n/** One tile: TILE × TILE pixels of `channels` bytes. Never mutate `data` after it is shared. */\nexport interface Tile {\n readonly data: Uint8ClampedArray;\n /** The packed value of every pixel when the tile is uniform, else `null`. */\n readonly uniform: number | null;\n /**\n * High bit depth samples (see `./deep`): 16-bit sRGB-encoded or 32-bit linear float, of which\n * `data` is the 8-bit rendering. Missing: the tile is 8-bit.\n */\n readonly deep?: Uint16Array | Float32Array;\n}\n\nlet nextSurfaceId = 1;\n\n/**\n * An immutable tiled surface. `channels` is 4 (RGBA) for layers and 1 for masks. Missing tiles\n * hold `defaultValue` in every byte (0: transparent black for layers; 0 or 255 for masks).\n */\nexport class TiledSurface {\n /** Unique per instance: a cache key that changes whenever the pixels can have changed. */\n readonly id = nextSurfaceId++;\n private boundsCache: CropBox | null | undefined;\n\n constructor(\n readonly channels: 1 | 4,\n readonly tiles: ReadonlyMap<TileKey, Tile>,\n readonly defaultValue = 0\n ) {}\n\n /** Bounds of the stored (non-default) tiles, tile-aligned; `null` when there are none. */\n get bounds(): CropBox | null {\n if (this.boundsCache === undefined) this.boundsCache = tileUnion(this.tiles.keys());\n return this.boundsCache;\n }\n\n get tileCount(): number {\n return this.tiles.size;\n }\n\n getTile(tx: number, ty: number): Tile | undefined {\n return this.tiles.get(tileKey(tx, ty));\n }\n}\n\nexport type PixelSurface = TiledSurface;\nexport type MaskSurface = TiledSurface;\n\nfunction tileUnion(keys: Iterable<TileKey>): CropBox | null {\n let x0 = Infinity;\n let y0 = Infinity;\n let x1 = -Infinity;\n let y1 = -Infinity;\n for (const key of keys) {\n const { tx, ty } = tileCoord(key);\n if (tx < x0) x0 = tx;\n if (ty < y0) y0 = ty;\n if (tx > x1) x1 = tx;\n if (ty > y1) y1 = ty;\n }\n if (x0 === Infinity) return null;\n return { x: x0 * TILE, y: y0 * TILE, width: (x1 - x0 + 1) * TILE, height: (y1 - y0 + 1) * TILE };\n}\n\n// ---------------------------------------------------------------------------\n// Uniform tiles\n// ---------------------------------------------------------------------------\n\nconst uniformTiles = new Map<string, Tile>();\nconst UNIFORM_LIMIT = 64;\n\n/** Packs one pixel (`channels` bytes from `data` at `offset`) into a number. */\nfunction packPixel(data: ArrayLike<number>, offset: number, channels: number): number {\n return channels === 1\n ? data[offset]\n : ((data[offset] << 24) | (data[offset + 1] << 16) | (data[offset + 2] << 8) | data[offset + 3]) >>> 0;\n}\n\n/** The shared tile whose every pixel is `value` (packed RGBA, or one byte for masks). */\nexport function uniformTile(channels: 1 | 4, value: number): Tile {\n const key = `${channels}:${value}`;\n let tile = uniformTiles.get(key);\n if (tile) return tile;\n const data = new Uint8ClampedArray(TILE_PIXELS * channels);\n if (channels === 1) data.fill(value);\n else if (value !== 0) {\n const r = value >>> 24;\n const g = (value >>> 16) & 255;\n const b = (value >>> 8) & 255;\n const a = value & 255;\n for (let i = 0; i < data.length; i += 4) {\n data[i] = r;\n data[i + 1] = g;\n data[i + 2] = b;\n data[i + 3] = a;\n }\n }\n tile = { data, uniform: value };\n if (uniformTiles.size >= UNIFORM_LIMIT) uniformTiles.delete(uniformTiles.keys().next().value as string);\n uniformTiles.set(key, tile);\n return tile;\n}\n\n/** Packs an RGBA colour (0..255 each) the way uniform tiles store it. */\nexport function packRgba(r: number, g: number, b: number, a: number): number {\n return ((r << 24) | (g << 16) | (b << 8) | a) >>> 0;\n}\n\nfunction defaultPacked(channels: 1 | 4, defaultValue: number): number {\n return channels === 1 ? defaultValue : packRgba(defaultValue, defaultValue, defaultValue, defaultValue);\n}\n\n/** The packed value when every pixel of `data` is the same, else `null`. */\nfunction uniformValue(data: Uint8ClampedArray, channels: 1 | 4): number | null {\n if (channels === 1) {\n const v = data[0];\n for (let i = 1; i < data.length; i++) if (data[i] !== v) return null;\n return v;\n }\n const words = new Uint32Array(data.buffer, data.byteOffset, data.length >> 2);\n const w = words[0];\n for (let i = 1; i < words.length; i++) if (words[i] !== w) return null;\n return packPixel(data, 0, 4);\n}\n\n/**\n * Turns freshly written tile data into a stored tile: `null` when it equals the surface default\n * (the tile is dropped), the shared uniform tile when it is uniform, else a tile owning `data`.\n */\nexport function finishTile(data: Uint8ClampedArray, channels: 1 | 4, defaultValue: number): Tile | null {\n const u = uniformValue(data, channels);\n if (u === null) return { data, uniform: null };\n if (u === defaultPacked(channels, defaultValue)) return null;\n return uniformTile(channels, u);\n}\n\n// ---------------------------------------------------------------------------\n// Construction\n// ---------------------------------------------------------------------------\n\n/** An empty surface (every pixel is `defaultValue`). */\nexport function emptySurface(channels: 1 | 4 = 4, defaultValue = 0): TiledSurface {\n return new TiledSurface(channels, new Map(), defaultValue);\n}\n\n/** A surface filled with one value over `rect` (packed RGBA for layers, a byte for masks). */\nexport function solidSurface(channels: 1 | 4, rect: CropBox, value: number, defaultValue = 0): TiledSurface {\n const bytes = new Uint8ClampedArray(channels);\n if (channels === 1) bytes[0] = value;\n else {\n bytes[0] = value >>> 24;\n bytes[1] = (value >>> 16) & 255;\n bytes[2] = (value >>> 8) & 255;\n bytes[3] = value & 255;\n }\n return fillRect(emptySurface(channels, defaultValue), rect, bytes);\n}\n\n/**\n * A surface holding `width × height` interleaved pixels placed at (`x`, `y`) in document space.\n * Tiles equal to the default are left out; uniform tiles are shared.\n */\nexport function surfaceFromPixels(\n pixels: { width: number; height: number; data: ArrayLike<number> },\n channels: 1 | 4 = 4,\n x = 0,\n y = 0,\n defaultValue = 0\n): TiledSurface {\n return writeRegion(emptySurface(channels, defaultValue), { x, y, width: pixels.width, height: pixels.height }, pixels.data);\n}\n\n// ---------------------------------------------------------------------------\n// Reading\n// ---------------------------------------------------------------------------\n\n/** Tile coordinates covering `rect`, row by row. */\nexport function tilesInRect(rect: CropBox): Array<{ tx: number; ty: number }> {\n const out: Array<{ tx: number; ty: number }> = [];\n if (rect.width <= 0 || rect.height <= 0) return out;\n const tx0 = Math.floor(rect.x / TILE);\n const ty0 = Math.floor(rect.y / TILE);\n const tx1 = Math.floor((rect.x + rect.width - 1) / TILE);\n const ty1 = Math.floor((rect.y + rect.height - 1) / TILE);\n for (let ty = ty0; ty <= ty1; ty++) for (let tx = tx0; tx <= tx1; tx++) out.push({ tx, ty });\n return out;\n}\n\n/**\n * Copies `rect` of the surface into `out` (or a new buffer), `rect.width × rect.height`\n * interleaved pixels; missing tiles read as the default.\n */\nexport function readRegion(surface: TiledSurface, rect: CropBox, out?: Uint8ClampedArray): Uint8ClampedArray {\n const ch = surface.channels;\n const w = rect.width;\n const h = rect.height;\n const dst = out ?? new Uint8ClampedArray(w * h * ch);\n if (surface.defaultValue !== 0) dst.fill(surface.defaultValue);\n else if (out) dst.fill(0);\n for (const { tx, ty } of tilesInRect(rect)) {\n const tile = surface.getTile(tx, ty);\n if (!tile) continue;\n const ox = tx * TILE;\n const oy = ty * TILE;\n const x0 = Math.max(rect.x, ox);\n const y0 = Math.max(rect.y, oy);\n const x1 = Math.min(rect.x + w, ox + TILE);\n const y1 = Math.min(rect.y + h, oy + TILE);\n const span = (x1 - x0) * ch;\n for (let yy = y0; yy < y1; yy++) {\n const s = ((yy - oy) * TILE + (x0 - ox)) * ch;\n const d = ((yy - rect.y) * w + (x0 - rect.x)) * ch;\n dst.set(tile.data.subarray(s, s + span), d);\n }\n }\n return dst;\n}\n\n/** One pixel's bytes (RGBA or the mask byte). */\nexport function readPixel(surface: TiledSurface, x: number, y: number): number[] {\n const tile = surface.getTile(Math.floor(x / TILE), Math.floor(y / TILE));\n const ch = surface.channels;\n if (!tile) return new Array(ch).fill(surface.defaultValue);\n const i = (((y & (TILE - 1)) * TILE) + (x & (TILE - 1))) * ch;\n return Array.from(tile.data.subarray(i, i + ch));\n}\n\n/**\n * Exact bounds of the pixels that differ from the default (for a layer: non-transparent), or\n * `null`. Scans the stored tiles; uniform tiles count whole.\n */\nexport function contentBounds(surface: TiledSurface): CropBox | null {\n const ch = surface.channels;\n const def = surface.defaultValue;\n let x0 = Infinity;\n let y0 = Infinity;\n let x1 = -Infinity;\n let y1 = -Infinity;\n for (const [key, tile] of surface.tiles) {\n const { tx, ty } = tileCoord(key);\n const ox = tx * TILE;\n const oy = ty * TILE;\n if (tile.uniform !== null) {\n if (ox < x0) x0 = ox;\n if (oy < y0) y0 = oy;\n if (ox + TILE - 1 > x1) x1 = ox + TILE - 1;\n if (oy + TILE - 1 > y1) y1 = oy + TILE - 1;\n continue;\n }\n const d = tile.data;\n for (let y = 0; y < TILE; y++) {\n let first = -1;\n let last = -1;\n const row = y * TILE * ch;\n for (let x = 0; x < TILE; x++) {\n const i = row + x * ch;\n let differs = false;\n for (let c = 0; c < ch; c++) if (d[i + c] !== def) differs = true;\n if (differs) {\n if (first < 0) first = x;\n last = x;\n }\n }\n if (first < 0) continue;\n if (ox + first < x0) x0 = ox + first;\n if (ox + last > x1) x1 = ox + last;\n if (oy + y < y0) y0 = oy + y;\n if (oy + y > y1) y1 = oy + y;\n }\n }\n if (x0 === Infinity) return null;\n return { x: x0, y: y0, width: x1 - x0 + 1, height: y1 - y0 + 1 };\n}\n\n// ---------------------------------------------------------------------------\n// Writing (copy-on-write)\n// ---------------------------------------------------------------------------\n\n/**\n * A private, mutable copy of the tiles a write touches. `tile(tx, ty)` copies the tile on first\n * use; `commit()` returns a new surface sharing every untouched tile. Use it for strokes:\n * edit through the draft while the pointer moves and commit once at pointer-up.\n */\nexport class SurfaceDraft {\n private readonly copies = new Map<TileKey, Uint8ClampedArray>();\n\n constructor(readonly base: TiledSurface) {}\n\n get channels(): 1 | 4 {\n return this.base.channels;\n }\n\n /** The writable data of tile (tx, ty), copied from the base on first use. */\n tile(tx: number, ty: number): Uint8ClampedArray {\n const key = tileKey(tx, ty);\n let data = this.copies.get(key);\n if (data) return data;\n const src = this.base.tiles.get(key);\n if (src) data = new Uint8ClampedArray(src.data);\n else {\n data = new Uint8ClampedArray(TILE_PIXELS * this.base.channels);\n if (this.base.defaultValue !== 0) data.fill(this.base.defaultValue);\n }\n this.copies.set(key, data);\n return data;\n }\n\n /** Replaces a whole tile with `data` (owned by the draft from now on). */\n replace(tx: number, ty: number, data: Uint8ClampedArray): void {\n this.copies.set(tileKey(tx, ty), data);\n }\n\n /** The current data of a tile without copying it (base tile, draft copy or `null` for default). */\n peek(tx: number, ty: number): Uint8ClampedArray | null {\n const key = tileKey(tx, ty);\n return this.copies.get(key) ?? this.base.tiles.get(key)?.data ?? null;\n }\n\n /** Keys of the tiles changed so far. */\n get touched(): TileKey[] {\n return [...this.copies.keys()];\n }\n\n /** A new surface with the draft's tiles; the draft must not be used afterwards. */\n commit(): TiledSurface {\n if (!this.copies.size) return this.base;\n const tiles = new Map(this.base.tiles);\n const { channels, defaultValue } = this.base;\n for (const [key, data] of this.copies) {\n const tile = finishTile(data, channels, defaultValue);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n }\n return new TiledSurface(channels, tiles, defaultValue);\n }\n}\n\n/** Writes `rect.width × rect.height` interleaved pixels at `rect`; returns the new surface. */\nexport function writeRegion(surface: TiledSurface, rect: CropBox, data: ArrayLike<number>): TiledSurface {\n const ch = surface.channels;\n const w = rect.width;\n const h = rect.height;\n if (w <= 0 || h <= 0) return surface;\n if (data.length < w * h * ch) throw new Error(`writeRegion: expected ${w * h * ch} bytes, got ${data.length}`);\n const draft = new SurfaceDraft(surface);\n const src = data as Uint8ClampedArray;\n const canSubarray = typeof (src as Uint8ClampedArray).subarray === 'function';\n for (const { tx, ty } of tilesInRect(rect)) {\n const ox = tx * TILE;\n const oy = ty * TILE;\n const x0 = Math.max(rect.x, ox);\n const y0 = Math.max(rect.y, oy);\n const x1 = Math.min(rect.x + w, ox + TILE);\n const y1 = Math.min(rect.y + h, oy + TILE);\n const full = x0 === ox && y0 === oy && x1 === ox + TILE && y1 === oy + TILE;\n const t = full ? new Uint8ClampedArray(TILE_PIXELS * ch) : draft.tile(tx, ty);\n const span = (x1 - x0) * ch;\n for (let yy = y0; yy < y1; yy++) {\n const s = ((yy - rect.y) * w + (x0 - rect.x)) * ch;\n const d = ((yy - oy) * TILE + (x0 - ox)) * ch;\n if (canSubarray) t.set(src.subarray(s, s + span), d);\n else for (let k = 0; k < span; k++) t[d + k] = src[s + k];\n }\n if (full) draft.replace(tx, ty, t);\n }\n return draft.commit();\n}\n\n/** Fills `rect` with one pixel value (`bytes`, `channels` long). */\nexport function fillRect(surface: TiledSurface, rect: CropBox, bytes: ArrayLike<number>): TiledSurface {\n const ch = surface.channels;\n if (rect.width <= 0 || rect.height <= 0) return surface;\n const tiles = new Map(surface.tiles);\n const draft = new SurfaceDraft(surface);\n const value = packPixel(bytes, 0, ch);\n const isDefault = value === defaultPacked(ch, surface.defaultValue);\n for (const { tx, ty } of tilesInRect(rect)) {\n const ox = tx * TILE;\n const oy = ty * TILE;\n const x0 = Math.max(rect.x, ox);\n const y0 = Math.max(rect.y, oy);\n const x1 = Math.min(rect.x + rect.width, ox + TILE);\n const y1 = Math.min(rect.y + rect.height, oy + TILE);\n const key = tileKey(tx, ty);\n if (x0 === ox && y0 === oy && x1 === ox + TILE && y1 === oy + TILE) {\n if (isDefault) tiles.delete(key);\n else tiles.set(key, uniformTile(ch, value));\n continue;\n }\n const t = draft.tile(tx, ty);\n for (let yy = y0; yy < y1; yy++) {\n let d = ((yy - oy) * TILE + (x0 - ox)) * ch;\n for (let xx = x0; xx < x1; xx++, d += ch) for (let c = 0; c < ch; c++) t[d + c] = bytes[c];\n }\n const tile = finishTile(t, ch, surface.defaultValue);\n if (tile) tiles.set(key, tile);\n else tiles.delete(key);\n }\n return new TiledSurface(ch, tiles, surface.defaultValue);\n}\n\n/** A copy of the surface moved by an integer offset (re-tiles; layers normally move by `offset`). */\nexport function translateSurface(surface: TiledSurface, dx: number, dy: number): TiledSurface {\n dx = Math.round(dx);\n dy = Math.round(dy);\n if (!dx && !dy) return surface;\n if (dx % TILE === 0 && dy % TILE === 0) {\n const tiles = new Map<TileKey, Tile>();\n for (const [key, tile] of surface.tiles) {\n const { tx, ty } = tileCoord(key);\n tiles.set(tileKey(tx + dx / TILE, ty + dy / TILE), tile);\n }\n return new TiledSurface(surface.channels, tiles, surface.defaultValue);\n }\n const b = surface.bounds;\n if (!b) return surface;\n const data = readRegion(surface, b);\n return writeRegion(emptySurface(surface.channels, surface.defaultValue), { ...b, x: b.x + dx, y: b.y + dy }, data);\n}\n\n// ---------------------------------------------------------------------------\n// Memory\n// ---------------------------------------------------------------------------\n\n/** Bytes held by a surface's non-uniform tiles (uniform tiles are shared and nearly free). */\nexport function surfaceBytes(surface: TiledSurface): number {\n let n = 0;\n for (const tile of surface.tiles.values()) if (tile.uniform === null) n += tile.data.byteLength;\n return n;\n}\n\n/** Adds every non-uniform tile of `surface` to `into` (for counting bytes shared across states). */\nexport function collectTiles(surface: TiledSurface, into: Set<Tile>): void {\n for (const tile of surface.tiles.values()) if (tile.uniform === null) into.add(tile);\n}\n\n/** True when two surfaces hold the same pixels (cheap when they share tiles). */\nexport function surfacesEqual(a: TiledSurface, b: TiledSurface): boolean {\n if (a === b) return true;\n if (a.channels !== b.channels || a.defaultValue !== b.defaultValue || a.tiles.size !== b.tiles.size) return false;\n for (const [key, ta] of a.tiles) {\n const tb = b.tiles.get(key);\n if (!tb) return false;\n if (ta === tb) continue;\n if (ta.uniform !== null || tb.uniform !== null) {\n if (ta.uniform !== tb.uniform) return false;\n continue;\n }\n const x = ta.data;\n const y = tb.data;\n for (let i = 0; i < x.length; i++) if (x[i] !== y[i]) return false;\n }\n return true;\n}\n","/**\n * Pigment-like colour mixing: blue and yellow make green, red and white make pink, as paint does.\n *\n * Each sRGB colour is lifted to a smooth reflectance spectrum, the spectra are mixed with the\n * Kubelka-Munk two-flux model, and the mixed spectrum is turned back into sRGB. Written from the\n * published literature; no third-party code or data:\n *\n * 1. **Spectral uplift.** A colour's reflectance is a sigmoid of a quadratic in wavelength,\n * `R(λ) = s(c₀λ² + c₁λ + c₂)` with `s(z) = ½ + z / 2√(1 + z²)` (Jakob & Hanika, \"A\n * Low-Dimensional Function Space for Efficient Spectral Upsampling\", Eurographics 2019). The\n * three coefficients are solved per colour with Newton's method so the spectrum reproduces the\n * colour. Spectra are precomputed lazily on a 17³ lattice over sRGB and interpolated\n * trilinearly in *linear* RGB: colour is linear in reflectance, so an interpolated spectrum\n * still reproduces its colour exactly while staying smooth and inside 0..1.\n * 2. **Mixing.** Each spectrum becomes `K/S = (1 − R)² / 2R` per band. Colours mix with the\n * two-constant form `K/S = Σ cᵢSᵢ(K/S)ᵢ / Σ cᵢSᵢ`, where `cᵢ` is the weight times the colour's\n * tinting strength and `Sᵢ` its scattering, taken from its luminance (light paints such as\n * titanium white scatter strongly, dark ones barely). Back to reflectance with\n * `R = 1 + K/S − √((K/S)² + 2·K/S)`.\n * 3. **Back to sRGB.** The spectrum is weighted by the CIE 1931 2° colour-matching functions\n * (the analytic multi-lobe fit of Wyman, Sloan & Shirley, JCGT 2013) under a 6504 K Planckian\n * illuminant, converted to linear sRGB and normalised so a perfect white reflector is exactly\n * white. Analytic curves keep the CIE's own (CC BY-SA) tables out of the package.\n *\n * Everything is plain TypeScript on typed arrays with no DOM: it runs in Node and Workers and is\n * deterministic. Colours are straight sRGB triples in 0..1, as in `@pixerate/editor/raster`.\n */\n\n/** An sRGB colour, each channel 0..1 (gamma-encoded, as in CSS). */\nexport type PigmentRgb = readonly [number, number, number];\n\n/** Number of spectral bands. */\nexport const PIGMENT_BANDS = 36;\n/** First band's wavelength in nm; bands are 10 nm apart (380..730 nm). */\nexport const PIGMENT_LAMBDA_MIN = 380;\nexport const PIGMENT_LAMBDA_STEP = 10;\n\n/** Band centre wavelengths in nm. */\nexport const PIGMENT_WAVELENGTHS: Float32Array = Float32Array.from({ length: PIGMENT_BANDS }, (_, i) => PIGMENT_LAMBDA_MIN + i * PIGMENT_LAMBDA_STEP);\n\nconst N = PIGMENT_BANDS;\n/** Lattice nodes per axis of the spectrum table. */\nconst LUT_NODES = 17;\n/** Reflectance floor for K/S, so a black's K/S stays finite (≈ 5000). */\nconst R_MIN = 1e-4;\n/** Scattering floor, so a black still counts in a mix. */\nconst SCATTER_MIN = 0.01;\n\n// ---------------------------------------------------------------------------\n// sRGB\n// ---------------------------------------------------------------------------\n\nfunction toLinear(v: number): number {\n return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);\n}\n\nfunction toEncoded(v: number): number {\n if (!(v > 0)) return 0;\n if (v >= 1) return 1;\n return v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;\n}\n\nfunction clamp01(v: number): number {\n return v > 0 ? (v < 1 ? v : 1) : 0;\n}\n\n// ---------------------------------------------------------------------------\n// Reflectance → linear sRGB weights\n// ---------------------------------------------------------------------------\n\n/** Asymmetric Gaussian lobe of the Wyman et al. fit. */\nfunction lobe(l: number, mu: number, s1: number, s2: number): number {\n const t = (l - mu) / (l < mu ? s1 : s2);\n return Math.exp(-0.5 * t * t);\n}\n\n/** Relative spectral power of a black body (Planck's law; the constant factor cancels). */\nfunction planck(nm: number, kelvin: number): number {\n const m = nm * 1e-9;\n return 1 / (m ** 5 * (Math.exp(1.438776877e-2 / (m * kelvin)) - 1));\n}\n\nlet weights: Float64Array | null = null;\n\n/**\n * Row-major 3 × N matrix taking a reflectance spectrum to linear sRGB. Each row sums to 1, so a\n * flat reflectance `r` gives the grey `(r, r, r)`.\n */\nfunction rgbWeights(): Float64Array {\n if (weights) return weights;\n const w = new Float64Array(3 * N);\n // XYZ (D65 white) → linear sRGB, IEC 61966-2-1.\n const m = [3.2404542, -1.5371385, -0.4985314, -0.969266, 1.8760108, 0.041556, 0.0556434, -0.2040259, 1.0572252];\n for (let b = 0; b < N; b++) {\n const l = PIGMENT_LAMBDA_MIN + b * PIGMENT_LAMBDA_STEP;\n const e = planck(l, 6504);\n const x = e * (1.056 * lobe(l, 599.8, 37.9, 31.0) + 0.362 * lobe(l, 442.0, 16.0, 26.7) - 0.065 * lobe(l, 501.1, 20.4, 26.2));\n const y = e * (0.821 * lobe(l, 568.8, 46.9, 40.5) + 0.286 * lobe(l, 530.9, 16.3, 31.1));\n const z = e * (1.217 * lobe(l, 437.0, 11.8, 36.0) + 0.681 * lobe(l, 459.0, 26.0, 13.8));\n for (let c = 0; c < 3; c++) w[c * N + b] = m[c * 3] * x + m[c * 3 + 1] * y + m[c * 3 + 2] * z;\n }\n for (let c = 0; c < 3; c++) {\n let sum = 0;\n for (let b = 0; b < N; b++) sum += w[c * N + b];\n for (let b = 0; b < N; b++) w[c * N + b] /= sum;\n }\n return (weights = w);\n}\n\n// ---------------------------------------------------------------------------\n// Sigmoid-polynomial uplift\n// ---------------------------------------------------------------------------\n\n/**\n * Solves the sigmoid-polynomial coefficients whose spectrum has the linear sRGB colour\n * `(r, g, b)`, starting from `coeffs` (updated in place). Wavelength is normalised to 0..1 over\n * the bands. Damped Newton: a step is halved until the residual drops.\n */\nfunction fitCoefficients(r: number, g: number, b: number, coeffs: Float64Array): number {\n const w = rgbWeights();\n const target = [r, g, b];\n const res = new Float64Array(3);\n const jac = new Float64Array(9);\n const trial = new Float64Array(3);\n let err = residual(coeffs, target, w, res, jac);\n for (let it = 0; it < 60 && err > 1e-10; it++) {\n const step = solve3(jac, res);\n if (!step) break;\n let scale = 1;\n let next = Infinity;\n for (let k = 0; k < 12; k++) {\n for (let i = 0; i < 3; i++) trial[i] = coeffs[i] - scale * step[i];\n next = residual(trial, target, w, null, null);\n if (next < err) break;\n scale *= 0.5;\n }\n if (!(next < err)) break;\n coeffs.set(trial);\n err = residual(coeffs, target, w, res, jac);\n }\n return err;\n}\n\n/** Squared colour error of a coefficient set; fills the residual and Jacobian when asked. */\nfunction residual(c: Float64Array, target: number[], w: Float64Array, res: Float64Array | null, jac: Float64Array | null): number {\n let r0 = -target[0];\n let r1 = -target[1];\n let r2 = -target[2];\n if (jac) jac.fill(0);\n for (let b = 0; b < N; b++) {\n const x = b / (N - 1);\n const z = (c[0] * x + c[1]) * x + c[2];\n const q = 1 + z * z;\n const sq = Math.sqrt(q);\n const s = 0.5 + z / (2 * sq);\n r0 += w[b] * s;\n r1 += w[N + b] * s;\n r2 += w[2 * N + b] * s;\n if (jac) {\n const ds = 1 / (2 * q * sq);\n for (let k = 0; k < 3; k++) {\n const wd = w[k * N + b] * ds;\n jac[k * 3] += wd * x * x;\n jac[k * 3 + 1] += wd * x;\n jac[k * 3 + 2] += wd;\n }\n }\n }\n if (res) {\n res[0] = r0;\n res[1] = r1;\n res[2] = r2;\n }\n return r0 * r0 + r1 * r1 + r2 * r2;\n}\n\n/** Solves the 3 × 3 system `a·x = v` (Cramer's rule), or null when singular. */\nfunction solve3(a: Float64Array, v: Float64Array): number[] | null {\n const [a0, a1, a2, a3, a4, a5, a6, a7, a8] = a;\n const c0 = a4 * a8 - a5 * a7;\n const c1 = a5 * a6 - a3 * a8;\n const c2 = a3 * a7 - a4 * a6;\n const det = a0 * c0 + a1 * c1 + a2 * c2;\n if (!(Math.abs(det) > 1e-300)) return null;\n const [v0, v1, v2] = v;\n return [\n (v0 * c0 + a1 * (a5 * v2 - v1 * a8) + a2 * (v1 * a7 - a4 * v2)) / det,\n (a0 * (v1 * a8 - a5 * v2) + v0 * c1 + a2 * (a3 * v2 - v1 * a6)) / det,\n (a0 * (a4 * v2 - v1 * a7) + a1 * (v1 * a6 - a3 * v2) + v0 * c2) / det,\n ];\n}\n\nconst candidate = new Float64Array(3);\n\n/**\n * Fits a lattice node, starting from `coeffs` (its neighbour's solution). Where that stalls, as it\n * can next to very saturated or very dark nodes, it also tries a flat grey start and a dimming\n * continuation (solve the colour brighter, then dim it in small steps, each starting from the last,\n * as Jakob & Hanika do along their brightness axis), and keeps the closest fit.\n */\nfunction fitNode(r: number, g: number, b: number, coeffs: Float64Array): void {\n let best = fitCoefficients(r, g, b, coeffs);\n if (best <= 1e-12) return;\n candidate.fill(0);\n let err = fitCoefficients(r, g, b, candidate);\n if (err < best) {\n best = err;\n coeffs.set(candidate);\n }\n if (best <= 1e-12) return;\n const max = Math.max(r, g, b);\n if (!(max > 0) || max >= 0.5) return;\n candidate.fill(0);\n const steps = 32;\n for (let i = 0; i <= steps; i++) {\n const scale = (0.5 * Math.pow(max / 0.5, i / steps)) / max;\n err = fitCoefficients(r * scale, g * scale, b * scale, candidate);\n }\n if (err < best) coeffs.set(candidate);\n}\n\nfunction sigmoidSpectrum(c: Float64Array, out: Float32Array, offset: number): void {\n for (let b = 0; b < N; b++) {\n const x = b / (N - 1);\n const z = (c[0] * x + c[1]) * x + c[2];\n out[offset + b] = 0.5 + z / (2 * Math.sqrt(1 + z * z));\n }\n}\n\n// ---------------------------------------------------------------------------\n// Spectrum table\n// ---------------------------------------------------------------------------\n\n/** Linear value of each lattice node: evenly spaced in encoded sRGB, so darks get more nodes. */\nconst NODES: Float64Array = Float64Array.from({ length: LUT_NODES }, (_, i) => toLinear(i / (LUT_NODES - 1)));\nlet table: Float32Array | null = null;\n\n/**\n * The spectrum of every lattice node, `[(r·n + g)·n + b]·N`, solved on first use (a few tens of\n * milliseconds). Each node starts from its neighbour's solution, so Newton converges in a few\n * steps.\n */\nfunction spectrumTable(): Float32Array {\n if (table) return table;\n const n = LUT_NODES;\n const t = new Float32Array(n * n * n * N);\n const row = new Float64Array(3);\n const coeffs = new Float64Array(3);\n for (let r = 0; r < n; r++) {\n for (let g = 0; g < n; g++) {\n // Warm start each row from the first node of the previous row.\n coeffs.set(row);\n for (let b = 0; b < n; b++) {\n fitNode(NODES[r], NODES[g], NODES[b], coeffs);\n if (b === 0) row.set(coeffs);\n sigmoidSpectrum(coeffs, t, ((r * n + g) * n + b) * N);\n }\n }\n }\n return (table = t);\n}\n\n/** Lattice cell, fraction and linear value per channel from the last `locate` calls. */\nconst cell = new Int32Array(3);\nconst frac = new Float64Array(3);\nconst linear = new Float64Array(3);\n\n/** Finds an encoded channel's lattice cell; the fraction is linear in linear RGB. */\nfunction locate(channel: number, encoded: number): void {\n const v = clamp01(encoded);\n const i = Math.min(LUT_NODES - 2, Math.floor(v * (LUT_NODES - 1)));\n const lo = NODES[i];\n const lin = toLinear(v);\n const f = (lin - lo) / (NODES[i + 1] - lo);\n cell[channel] = i;\n frac[channel] = f < 0 ? 0 : f > 1 ? 1 : f;\n linear[channel] = lin;\n}\n\n/**\n * The smooth reflectance spectrum (0..1 per band, `PIGMENT_BANDS` bands from 380 nm in 10 nm\n * steps) of an sRGB colour. `reflectanceToRgb` of the result gives the colour back.\n */\nexport function rgbToReflectance(r: number, g: number, b: number, out: Float32Array = new Float32Array(N)): Float32Array {\n const t = spectrumTable();\n const n = LUT_NODES;\n locate(0, r);\n locate(1, g);\n locate(2, b);\n const [ri, gi, bi] = cell;\n const [rf, gf, bf] = frac;\n out.fill(0);\n for (let corner = 0; corner < 8; corner++) {\n const dr = corner >> 2;\n const dg = (corner >> 1) & 1;\n const db = corner & 1;\n const weight = (dr ? rf : 1 - rf) * (dg ? gf : 1 - gf) * (db ? bf : 1 - bf);\n if (weight === 0) continue;\n const base = (((ri + dr) * n + gi + dg) * n + bi + db) * N;\n for (let k = 0; k < N; k++) out[k] += weight * t[base + k];\n }\n return out;\n}\n\n/** The sRGB colour (0..1, clipped to the gamut) of a reflectance spectrum. */\nexport function reflectanceToRgb(reflectance: ArrayLike<number>, out: number[] = [0, 0, 0]): number[] {\n const w = rgbWeights();\n let r = 0;\n let g = 0;\n let b = 0;\n for (let k = 0; k < N; k++) {\n const v = reflectance[k];\n r += w[k] * v;\n g += w[N + k] * v;\n b += w[2 * N + k] * v;\n }\n out[0] = toEncoded(r);\n out[1] = toEncoded(g);\n out[2] = toEncoded(b);\n return out;\n}\n\n// ---------------------------------------------------------------------------\n// Pigments and mixing\n// ---------------------------------------------------------------------------\n\n/**\n * A colour prepared for mixing: its K/S spectrum and its weight in a mix. Make one per brush or\n * palette colour with `createPigment` and reuse it (`setPigmentColor`) on hot paths.\n */\nexport interface Pigment {\n /** The sRGB colour it was made from (0..1). */\n readonly rgb: [number, number, number];\n /** Tinting strength (default 1): how strongly it colours a mix, relative to its weight. */\n strength: number;\n /** K/S per band. */\n readonly ks: Float32Array;\n /** Scattering from the colour's luminance (floored), before strength. */\n scatter: number;\n}\n\nexport interface PigmentOptions {\n /**\n * Tinting strength, > 0 (default 1). A strength of 4 counts each part as four, so a little of\n * a strong dark pigment tints a lot of white. A colour mixed only with itself is unchanged\n * whatever its strength.\n */\n strength?: number;\n}\n\nconst spectrumScratch = new Float32Array(N);\n\n/** Prepares a colour for mixing. */\nexport function createPigment(color: PigmentRgb, options: PigmentOptions = {}): Pigment {\n const pigment: Pigment = { rgb: [0, 0, 0], strength: 1, ks: new Float32Array(N), scatter: 1 };\n return setPigmentColor(pigment, color[0], color[1], color[2], options.strength);\n}\n\n/** Re-targets a pigment to another colour without allocating (the per-pixel fast path). */\nexport function setPigmentColor(pigment: Pigment, r: number, g: number, b: number, strength = pigment.strength): Pigment {\n const R = rgbToReflectance(r, g, b, spectrumScratch);\n const ks = pigment.ks;\n for (let k = 0; k < N; k++) {\n const v = R[k] > R_MIN ? R[k] : R_MIN;\n ks[k] = ((1 - v) * (1 - v)) / (2 * v);\n }\n pigment.rgb[0] = clamp01(r);\n pigment.rgb[1] = clamp01(g);\n pigment.rgb[2] = clamp01(b);\n pigment.strength = Number.isFinite(strength) && strength > 0 ? strength : 1;\n // `rgbToReflectance` left each channel's linear value in `linear`.\n const y = 0.2126 * linear[0] + 0.7152 * linear[1] + 0.0722 * linear[2];\n pigment.scatter = y > SCATTER_MIN ? y : SCATTER_MIN;\n return pigment;\n}\n\n/** Mixed K/S → reflectance → sRGB, written to `out[offset..offset + 2]`. */\nfunction ksToRgb(ks: Float32Array | Float64Array, out: { [i: number]: number }, offset: number): void {\n const w = rgbWeights();\n let r = 0;\n let g = 0;\n let b = 0;\n for (let k = 0; k < N; k++) {\n const q = ks[k];\n const v = 1 + q - Math.sqrt(q * q + 2 * q);\n r += w[k] * v;\n g += w[N + k] * v;\n b += w[2 * N + k] * v;\n }\n out[offset] = toEncoded(r);\n out[offset + 1] = toEncoded(g);\n out[offset + 2] = toEncoded(b);\n}\n\nconst mixScratch = new Float64Array(N);\n\n/**\n * Mixes colours like paint. `weights` are parts by amount (any non-negative scale; at least one\n * positive). Colours may be sRGB triples (0..1) or prepared pigments (which carry a tinting\n * strength). Returns the mixed sRGB colour (0..1).\n */\nexport function mixPigments(colors: readonly (PigmentRgb | Pigment)[], weights: readonly number[]): [number, number, number] {\n if (colors.length === 0) throw new Error('mixPigments needs at least one colour.');\n if (weights.length !== colors.length) throw new Error(`mixPigments got ${colors.length} colours but ${weights.length} weights; pass one weight per colour.`);\n const pigments = colors.map((c) => ('ks' in c ? c : createPigment(c)));\n let total = 0;\n for (let i = 0; i < pigments.length; i++) {\n const w = weights[i];\n if (!(Number.isFinite(w) && w >= 0)) throw new Error(`Weight ${i} is ${w}; weights must be finite and ≥ 0.`);\n total += w * pigments[i].strength * pigments[i].scatter;\n }\n if (!(total > 0)) throw new Error('mixPigments needs at least one positive weight.');\n mixScratch.fill(0);\n for (let i = 0; i < pigments.length; i++) {\n const p = pigments[i];\n const c = (weights[i] * p.strength * p.scatter) / total;\n if (c === 0) continue;\n for (let k = 0; k < N; k++) mixScratch[k] += c * p.ks[k];\n }\n const out: [number, number, number] = [0, 0, 0];\n ksToRgb(mixScratch, out, 0);\n return out;\n}\n\n/** Mixes `a` towards `b` like paint: `t = 0` is `a`, `t = 1` is `b`. */\nexport function lerpPigment(a: PigmentRgb | Pigment, b: PigmentRgb | Pigment, t: number): [number, number, number] {\n const u = clamp01(t);\n return mixPigments([a, b], [1 - u, u]);\n}\n\n/**\n * The per-dab fast path: mixes two prepared pigments (`t` of `b` into `a`) and writes the sRGB\n * result to `out[offset..offset + 2]`, without allocating. Pair it with `setPigmentColor` for the\n * colour under the brush.\n */\nexport function lerpPigmentInto(a: Pigment, b: Pigment, t: number, out: { [i: number]: number }, offset = 0): void {\n const u = t > 0 ? (t < 1 ? t : 1) : 0;\n const wa = (1 - u) * a.strength * a.scatter;\n const wb = u * b.strength * b.scatter;\n const ca = wa / (wa + wb);\n const cb = 1 - ca;\n const ka = a.ks;\n const kb = b.ks;\n for (let k = 0; k < N; k++) mixScratch[k] = ca * ka[k] + cb * kb[k];\n ksToRgb(mixScratch, out, offset);\n}\n","// Portions ported from photocraft-main/crates/color/src/blend.rs (MIT OR Apache-2.0, © ArtCraft Team and contributors). See THIRD_PARTY_NOTICES.md.\n\n/**\n * Photoshop blend modes on straight (non-premultiplied) 0..1 colours.\n *\n * Formulas follow the W3C Compositing / ISO 32000 definitions, with Photoshop's variants where\n * they differ (Soft Light, Vivid / Linear / Pin Light, Hard Mix, Linear Burn / Dodge, Subtract,\n * Divide, Darker / Lighter Color), and the hardened `ClipColor` for the non-separable modes.\n *\n * Plus `pigment`, which is not a `B(Cb, Cs)` mode: it replaces the alpha interpolation\n * `(1 − α)·Cb + α·B` with a Kubelka-Munk paint mix of the two colours (see `lerpPigment`).\n *\n * Plus four painterly modes of our own design, which are ordinary `B(Cb, Cs)` modes, so every\n * caller of `blendRgb` / `blendChannel` (the compositor, adjustment layers, effects, brushes) gets\n * them for free: `watercolor` (transparent washes, multiplied per wavelength), `tint` (the source's\n * colour on the backdrop's lightness, gently at the extremes), `highlight` (light added with a\n * soft shoulder) and `shadow` (light taken away, with ambient fill). See `painterlyRgb`.\n */\n\nimport { createPigment, lerpPigmentInto, reflectanceToRgb, rgbToReflectance, setPigmentColor, type Pigment } from './pigment';\n\nexport type BlendMode =\n | 'normal'\n | 'darken'\n | 'multiply'\n | 'color-burn'\n | 'linear-burn'\n | 'darker-color'\n | 'lighten'\n | 'screen'\n | 'color-dodge'\n | 'linear-dodge'\n | 'lighter-color'\n | 'overlay'\n | 'soft-light'\n | 'hard-light'\n | 'vivid-light'\n | 'linear-light'\n | 'pin-light'\n | 'hard-mix'\n | 'difference'\n | 'exclusion'\n | 'subtract'\n | 'divide'\n | 'hue'\n | 'saturation'\n | 'color'\n | 'luminosity'\n | 'pigment'\n | 'watercolor'\n | 'tint'\n | 'highlight'\n | 'shadow';\n\nexport interface BlendModeInfo {\n id: BlendMode;\n label: string;\n /** Canvas `globalCompositeOperation` with the same maths, when there is one. */\n canvas: GlobalCompositeOperation | null;\n /** True for the painterly modes (`watercolor`, `tint`, `highlight`, `shadow`), which no other app has. */\n painterly?: boolean;\n /** The nearest standard (Photoshop) mode, for formats that lack this one (PSD). Missing: it is standard. */\n standard?: BlendMode;\n /** One sentence on what the mode does (painterly and pigment modes). */\n description?: string;\n}\n\n/** Every blend mode, in Photoshop's menu order. */\nexport const BLEND_MODES: readonly BlendModeInfo[] = [\n { id: 'normal', label: 'Normal', canvas: 'source-over' },\n { id: 'darken', label: 'Darken', canvas: 'darken' },\n { id: 'multiply', label: 'Multiply', canvas: 'multiply' },\n { id: 'color-burn', label: 'Color Burn', canvas: 'color-burn' },\n { id: 'linear-burn', label: 'Linear Burn', canvas: null },\n { id: 'darker-color', label: 'Darker Color', canvas: null },\n { id: 'lighten', label: 'Lighten', canvas: 'lighten' },\n { id: 'screen', label: 'Screen', canvas: 'screen' },\n { id: 'color-dodge', label: 'Color Dodge', canvas: 'color-dodge' },\n { id: 'linear-dodge', label: 'Linear Dodge (Add)', canvas: null },\n { id: 'lighter-color', label: 'Lighter Color', canvas: null },\n { id: 'overlay', label: 'Overlay', canvas: 'overlay' },\n // Canvas soft-light is the W3C formula, not Photoshop's: use the scalar path.\n { id: 'soft-light', label: 'Soft Light', canvas: null },\n { id: 'hard-light', label: 'Hard Light', canvas: 'hard-light' },\n { id: 'vivid-light', label: 'Vivid Light', canvas: null },\n { id: 'linear-light', label: 'Linear Light', canvas: null },\n { id: 'pin-light', label: 'Pin Light', canvas: null },\n { id: 'hard-mix', label: 'Hard Mix', canvas: null },\n { id: 'difference', label: 'Difference', canvas: 'difference' },\n { id: 'exclusion', label: 'Exclusion', canvas: 'exclusion' },\n { id: 'subtract', label: 'Subtract', canvas: null },\n { id: 'divide', label: 'Divide', canvas: null },\n { id: 'hue', label: 'Hue', canvas: 'hue' },\n { id: 'saturation', label: 'Saturation', canvas: 'saturation' },\n { id: 'color', label: 'Color', canvas: 'color' },\n { id: 'luminosity', label: 'Luminosity', canvas: 'luminosity' },\n // Not in Photoshop: mixes like paint (blue + yellow = green). No canvas or PSD equivalent.\n {\n id: 'pigment',\n label: 'Pigment',\n canvas: null,\n standard: 'normal',\n description: 'Mixes the layer into what is below like paint, by its opacity: blue and yellow make green.',\n },\n // Painterly modes (ours; see `painterlyRgb`). No canvas equivalent; PSD gets `standard`.\n {\n id: 'watercolor',\n label: 'Watercolor',\n canvas: null,\n painterly: true,\n standard: 'multiply',\n description: 'A transparent wash: the colours filter each other wavelength by wavelength, so layers darken like glazes and blue over yellow gives green. White is clear.',\n },\n {\n id: 'tint',\n label: 'Tint',\n canvas: null,\n painterly: true,\n standard: 'color',\n description: \"Tints what is below with the layer's colour and keeps its lightness; mid-tones take the full tint, highlights and darks stay clean.\",\n },\n {\n id: 'highlight',\n label: 'Highlight',\n canvas: null,\n painterly: true,\n standard: 'screen',\n description: 'Adds the layer as coloured light that rolls off softly towards white instead of clipping. Black adds nothing.',\n },\n {\n id: 'shadow',\n label: 'Shadow',\n canvas: null,\n painterly: true,\n standard: 'multiply',\n description: \"Shades what is below as if the layer blocked the light, coloured by the layer, with ambient light so it never crushes to black. White does nothing.\",\n },\n];\n\nconst BLEND_INFO = new Map<string, BlendModeInfo>(BLEND_MODES.map((m) => [m.id, m]));\n\nexport function isBlendMode(value: unknown): value is BlendMode {\n return typeof value === 'string' && BLEND_INFO.has(value);\n}\n\n/** The painterly modes, in menu order. */\nexport const PAINTERLY_BLEND_MODES: readonly BlendMode[] = BLEND_MODES.filter((m) => m.painterly).map((m) => m.id);\n\n/** Whether a mode is one of the painterly modes (`watercolor`, `tint`, `highlight`, `shadow`). */\nexport function isPainterlyBlendMode(mode: unknown): boolean {\n return typeof mode === 'string' && Boolean(BLEND_INFO.get(mode)?.painterly);\n}\n\n/**\n * The nearest standard (Photoshop) mode: the mode itself when it is standard, `normal` for\n * `pigment`, and each painterly mode's closest relative (watercolour and shadow → multiply,\n * tint → color, highlight → screen). For writing formats that lack our modes, such as PSD.\n */\nexport function standardBlendMode(mode: BlendMode): BlendMode {\n return BLEND_INFO.get(mode)?.standard ?? mode;\n}\n\n/** A stored blend mode, or `normal` for a missing / unknown value. */\nexport function normalizeBlendMode(value: unknown): BlendMode {\n return isBlendMode(value) ? value : 'normal';\n}\n\n/**\n * The canvas composite operation that implements `mode` exactly, or `null` when the canvas has\n * none (draw through {@link blendImageData} instead).\n */\nexport function canvasCompositeOperation(mode: BlendMode | undefined): GlobalCompositeOperation | null {\n return BLEND_INFO.get(mode ?? 'normal')?.canvas ?? null;\n}\n\n/**\n * Whether a mode works channel by channel. The non-separable ones work on whole colours (hue,\n * saturation, colour, luminosity, darker / lighter colour, pigment).\n */\nexport function isSeparableBlendMode(mode: BlendMode): boolean {\n return !(\n mode === 'pigment' ||\n mode === 'watercolor' ||\n mode === 'tint' ||\n mode === 'hue' ||\n mode === 'saturation' ||\n mode === 'color' ||\n mode === 'luminosity' ||\n mode === 'darker-color' ||\n mode === 'lighter-color'\n );\n}\n\nfunction colorBurn(cb: number, cs: number): number {\n if (cb >= 1) return 1;\n if (cs <= 0) return 0;\n return 1 - Math.min(1, (1 - cb) / cs);\n}\n\nfunction colorDodge(cb: number, cs: number): number {\n if (cb <= 0) return 0;\n if (cs >= 1) return 1;\n return Math.min(1, cb / (1 - cs));\n}\n\nfunction hardLight(cb: number, cs: number): number {\n if (cs <= 0.5) return cb * 2 * cs;\n const s = 2 * cs - 1;\n return cb + s - cb * s;\n}\n\n/** Photoshop's Soft Light (differs from W3C in the upper half). */\nfunction softLight(cb: number, cs: number): number {\n return cs <= 0.5\n ? 2 * cb * cs + cb * cb * (1 - 2 * cs)\n : 2 * cb * (1 - cs) + Math.sqrt(Math.max(0, cb)) * (2 * cs - 1);\n}\n\nfunction vividLightHardMix(cb: number, cs: number): number {\n const EDGE = 1e-4;\n if (cs <= 0.5) {\n if (cb >= 1 - EDGE) return 1;\n if (cs <= 0) return 0;\n return 1 - Math.min(1, (1 - cb) / (2 * cs));\n }\n if (cb <= EDGE) return 0;\n if (cs >= 1) return 1;\n return Math.min(1, cb / (2 * (1 - cs)));\n}\n\n/** Separable blend function `B(cb, cs)` of one channel. Non-separable modes return `cs`. */\nexport function blendChannel(mode: BlendMode, cb: number, cs: number): number {\n switch (mode) {\n case 'normal':\n return cs;\n case 'darken':\n return Math.min(cb, cs);\n case 'multiply':\n return cb * cs;\n case 'color-burn':\n return colorBurn(cb, cs);\n case 'linear-burn':\n return Math.max(0, cb + cs - 1);\n case 'lighten':\n return Math.max(cb, cs);\n case 'screen':\n return cb + cs - cb * cs;\n case 'color-dodge':\n return colorDodge(cb, cs);\n case 'linear-dodge':\n return Math.min(1, cb + cs);\n case 'overlay':\n return hardLight(cs, cb);\n case 'soft-light':\n return softLight(cb, cs);\n case 'hard-light':\n return hardLight(cb, cs);\n case 'vivid-light':\n if (cs <= 0) return 0;\n if (cs >= 1) return 1;\n return cs <= 0.5 ? colorBurn(cb, 2 * cs) : colorDodge(cb, 2 * (cs - 0.5));\n case 'linear-light':\n return Math.min(1, Math.max(0, cb + 2 * cs - 1));\n case 'pin-light':\n return cs <= 0.5 ? Math.min(cb, 2 * cs) : Math.max(cb, 2 * cs - 1);\n case 'hard-mix':\n return vividLightHardMix(cb, cs) >= 0.5 - 1e-6 ? 1 : 0;\n case 'difference':\n return Math.abs(cb - cs);\n case 'exclusion':\n return cb + cs - 2 * cb * cs;\n case 'subtract':\n return Math.max(0, cb - cs);\n case 'divide':\n if (cs <= 0) return cb <= 0 ? 0 : 1;\n return Math.min(1, cb / cs);\n case 'highlight':\n return highlight(cb, cs);\n case 'shadow':\n return shadow(cb, cs);\n default:\n return cs;\n }\n}\n\nexport type Rgb = [number, number, number];\nexport type Rgba = [number, number, number, number];\n\n/** Luminosity of the non-separable modes (W3C / ISO 32000 weights). */\nexport function blendLum(c: readonly number[]): number {\n return 0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2];\n}\n\n/**\n * ClipColor, hardened: when the luminosity itself is out of range no colour of that luminosity\n * is in gamut, so the result is its grey (the spec formula turned such colours inside out and,\n * for greys, divided by rounding noise).\n */\nfunction clipColor(c: Rgb): Rgb {\n const l = blendLum(c);\n const n = Math.min(c[0], c[1], c[2]);\n const x = Math.max(c[0], c[1], c[2]);\n let out: Rgb = [c[0], c[1], c[2]];\n if (n < 0) {\n const d = l - n;\n out = out.map((v) => (l > 0 && d > 0 ? l + ((v - l) * l) / d : l)) as Rgb;\n }\n if (x > 1) {\n const d = x - l;\n out = out.map((v) => (l < 1 && d > 0 ? l + ((v - l) * (1 - l)) / d : l)) as Rgb;\n }\n return out;\n}\n\n/** `c` with its luminosity moved to `l`, clipped back into gamut towards grey (SetLum + ClipColor). */\nexport function setLum(c: readonly number[], l: number): Rgb {\n const d = l - blendLum(c);\n return clipColor([c[0] + d, c[1] + d, c[2] + d]);\n}\n\nfunction sat(c: readonly number[]): number {\n return Math.max(c[0], c[1], c[2]) - Math.min(c[0], c[1], c[2]);\n}\n\nfunction setSat(c: readonly number[], s: number): Rgb {\n const idx = [0, 1, 2].sort((a, b) => c[a] - c[b]);\n const [imin, imid, imax] = idx;\n const out: Rgb = [0, 0, 0];\n if (c[imax] > c[imin]) {\n out[imid] = ((c[imid] - c[imin]) * s) / (c[imax] - c[imin]);\n out[imax] = s;\n }\n out[imin] = 0;\n return out;\n}\n\n/**\n * `B(Cb, Cs)` of a whole colour, including the non-separable modes. `pigment` has no `B` and\n * returns `Cs`; composite it with {@link blendPixel} or {@link mixPigmentRgb}.\n */\nexport function blendRgb(mode: BlendMode, cb: readonly number[], cs: readonly number[]): Rgb {\n switch (mode) {\n case 'hue':\n return setLum(setSat(cs, sat(cb)), blendLum(cb));\n case 'saturation':\n return setLum(setSat(cb, sat(cs)), blendLum(cb));\n case 'color':\n return setLum(cs, blendLum(cb));\n case 'luminosity':\n return setLum(cb, blendLum(cs));\n case 'darker-color':\n return blendLum(cs) < blendLum(cb) ? [cs[0], cs[1], cs[2]] : [cb[0], cb[1], cb[2]];\n case 'lighter-color':\n return blendLum(cs) > blendLum(cb) ? [cs[0], cs[1], cs[2]] : [cb[0], cb[1], cb[2]];\n case 'watercolor':\n return watercolor(cb, cs);\n case 'tint':\n return tint(cb, cs);\n default:\n return [blendChannel(mode, cb[0], cs[0]), blendChannel(mode, cb[1], cs[1]), blendChannel(mode, cb[2], cs[2])];\n }\n}\n\n// ---------------------------------------------------------------------------\n// Painterly modes\n// ---------------------------------------------------------------------------\n//\n// Our own designs, written from how each effect should look. All four are true `B(Cb, Cs)`\n// functions (colours 0..1, gamma-encoded), so the W3C general formula composites them like any\n// other mode, opacity and alpha included. Light is added or removed in *linear* light, where it\n// adds up physically; `lin` / `enc` are the sRGB transfer functions.\n//\n// - watercolor: R(λ) = Rb(λ)·Rs(λ) over the pigment model's reflectance spectra (non-separable).\n// - tint: Oklab: L = Lb, ab = ab_s·√(4·Lb·(1 − Lb)), chroma cut to the gamut.\n// - highlight: per channel, x = lb + (1 − lb)·(1 − e^(−ls / (1 − lb))) (separable).\n// - shadow: per channel, x = lb·(A + (1 − A)·ls), with ambient A = 0.2 (separable).\n\nconst clamp01 = (v: number): number => (v > 0 ? (v < 1 ? v : 1) : 0);\n\nfunction lin(v: number): number {\n const c = clamp01(v);\n return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);\n}\n\nfunction enc(v: number): number {\n if (!(v > 0)) return 0;\n if (v >= 1) return 1;\n return v <= 0.0031308 ? 12.92 * v : 1.055 * Math.pow(v, 1 / 2.4) - 0.055;\n}\n\n/**\n * Highlight: the source is coloured light added to the backdrop in linear light. Plain addition\n * (linear dodge) clips to flat white; here the added light `ls` fills the headroom `1 − lb`\n * exponentially, `x = lb + (1 − lb)·(1 − e^(−ls / (1 − lb)))`. A little light adds almost exactly\n * `ls`, a lot approaches white without ever clipping, black adds nothing, and the backdrop's\n * gradations survive inside the highlight.\n */\nfunction highlight(cb: number, cs: number): number {\n const lb = lin(cb);\n const ls = lin(cs);\n if (ls <= 0) return clamp01(cb);\n const room = 1 - lb;\n if (room <= 1e-9) return 1;\n return enc(lb + room * (1 - Math.exp(-ls / room)));\n}\n\n/** Shadow's ambient light: the share of light that still reaches a fully shaded area. */\nexport const SHADOW_AMBIENT = 0.2;\n\n/**\n * Shadow: the source says how much light still reaches each channel (white: all of it, black:\n * none), in linear light, plus ambient light `A` that fills every shadow:\n * `x = lb·(A + (1 − A)·ls)`. A black shadow keeps a fifth of the light, so stacked shading\n * deepens gradually and darks never crush to black; a coloured source tints the shade (a blue\n * one cools it, as painters shade). White changes nothing.\n */\nfunction shadow(cb: number, cs: number): number {\n if (cs >= 1) return clamp01(cb);\n return enc(lin(cb) * (SHADOW_AMBIENT + (1 - SHADOW_AMBIENT) * lin(cs)));\n}\n\n/** Reflectance spectra of 8-bit colours (key `r << 16 | g << 8 | b`), for `watercolor`. */\nconst spectrumCache = new Map<number, Float32Array>();\n/** About 2.4 MB of spectra; the cache starts over when it fills. */\nconst SPECTRUM_CACHE_MAX = 16384;\nconst washScratch = new Float32Array(64);\n\nfunction spectrum8(r: number, g: number, b: number): Float32Array {\n const key = (r << 16) | (g << 8) | b;\n let s = spectrumCache.get(key);\n if (!s) {\n if (spectrumCache.size >= SPECTRUM_CACHE_MAX) spectrumCache.clear();\n s = rgbToReflectance(r / 255, g / 255, b / 255);\n spectrumCache.set(key, s);\n }\n return s;\n}\n\n/**\n * Watercolor: the source is a transparent wash over the backdrop. A wash absorbs light by\n * wavelength and the paper (or paint) under it reflects what passes, so the reflectances multiply\n * band by band, `R(λ) = Rb(λ)·Rs(λ)` (Beer–Lambert through the wash), using each colour's smooth\n * reflectance spectrum from the pigment model, then back to sRGB. Multiply in RGB treats the three\n * channels as the only wavelengths, so a blue glaze over yellow goes black; spectra overlap in\n * the greens, so here it gives the deep green a real glaze does. White is clear (exactly: it\n * returns the backdrop) and each wash darkens a little more, as glazes do. Colours are quantised\n * to 8 bits and their spectra cached.\n */\nfunction watercolor(cb: readonly number[], cs: readonly number[]): Rgb {\n const q = (v: number) => Math.round(clamp01(v) * 255);\n const b0 = q(cb[0]), b1 = q(cb[1]), b2 = q(cb[2]);\n const s0 = q(cs[0]), s1 = q(cs[1]), s2 = q(cs[2]);\n if (s0 === 255 && s1 === 255 && s2 === 255) return [clamp01(cb[0]), clamp01(cb[1]), clamp01(cb[2])];\n if (b0 === 255 && b1 === 255 && b2 === 255) return [clamp01(cs[0]), clamp01(cs[1]), clamp01(cs[2])];\n if ((b0 | b1 | b2) === 0 || (s0 | s1 | s2) === 0) return [0, 0, 0];\n const rb = spectrum8(b0, b1, b2);\n const rs = spectrum8(s0, s1, s2);\n const n = rb.length;\n for (let k = 0; k < n; k++) washScratch[k] = rb[k] * rs[k];\n const out = reflectanceToRgb(washScratch.subarray(0, n), [0, 0, 0]);\n return [out[0], out[1], out[2]];\n}\n\n/** Linear sRGB → Oklab (Björn Ottosson, 2020), written to `out`. */\nfunction toOklab(r: number, g: number, b: number, out: number[]): void {\n const l = Math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b);\n const m = Math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b);\n const s = Math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b);\n out[0] = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;\n out[1] = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;\n out[2] = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;\n}\n\n/** Oklab → linear sRGB (unclipped), written to `out`. */\nfunction fromOklab(L: number, a: number, b: number, out: number[]): void {\n const l = (L + 0.3963377774 * a + 0.2158037573 * b) ** 3;\n const m = (L - 0.1055613458 * a - 0.0638541728 * b) ** 3;\n const s = (L - 0.0894841775 * a - 1.291485548 * b) ** 3;\n out[0] = 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s;\n out[1] = -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s;\n out[2] = -0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s;\n}\n\nconst labB = [0, 0, 0];\nconst labS = [0, 0, 0];\nconst rgbScratch = [0, 0, 0];\n\nconst inGamut = (c: number[]) => c[0] >= -1e-6 && c[0] <= 1 + 1e-6 && c[1] >= -1e-6 && c[1] <= 1 + 1e-6 && c[2] >= -1e-6 && c[2] <= 1 + 1e-6;\n\n/**\n * Tint: the backdrop takes the source's colour and keeps its own lightness, in Oklab (a\n * perceptual space, so the lightness really stays put, unlike `color`'s RGB-weighted luminosity):\n * `L = Lb` and `ab = ab_s·w` with `w = √(4·Lb·(1 − Lb))`. Mid-tones take the source's full\n * colour; towards white and black less of it, so a tint over a drawing leaves the paper white\n * and the blacks black. The backdrop's own hue is replaced, not averaged (averaging opposite hues\n * would grey them). Out-of-gamut results lose chroma (bisection at constant L and hue), not\n * lightness. A grey source neutralises.\n */\nfunction tint(cb: readonly number[], cs: readonly number[]): Rgb {\n toOklab(lin(cb[0]), lin(cb[1]), lin(cb[2]), labB);\n toOklab(lin(cs[0]), lin(cs[1]), lin(cs[2]), labS);\n const L = labB[0];\n const w = Math.sqrt(Math.min(1, Math.max(0, 4 * L * (1 - L))));\n const a = labS[1] * w;\n const b = labS[2] * w;\n fromOklab(L, a, b, rgbScratch);\n if (!inGamut(rgbScratch)) {\n let lo = 0;\n let hi = 1;\n for (let i = 0; i < 14; i++) {\n const mid = (lo + hi) / 2;\n fromOklab(L, a * mid, b * mid, rgbScratch);\n if (inGamut(rgbScratch)) lo = mid;\n else hi = mid;\n }\n fromOklab(L, a * lo, b * lo, rgbScratch);\n }\n return [enc(rgbScratch[0]), enc(rgbScratch[1]), enc(rgbScratch[2])];\n}\n\n// ---------------------------------------------------------------------------\n// Pigment\n// ---------------------------------------------------------------------------\n\n/** Scratch pigments, made on first use (building one solves the spectrum table). */\nlet scratch: [Pigment, Pigment] | null = null;\n\n/**\n * The `pigment` mode's colour: `t` of `cs` mixed into `cb` like paint (`lerpPigment`), written to\n * `out[0..2]`. `t ≥ 1` (or equal colours) is `cs` exactly. Colours are 0..1.\n */\nexport function mixPigmentRgb(cb: readonly number[], cs: readonly number[], t: number, out: number[] = [0, 0, 0]): number[] {\n if (t >= 1 || (cb[0] === cs[0] && cb[1] === cs[1] && cb[2] === cs[2])) {\n out[0] = cs[0];\n out[1] = cs[1];\n out[2] = cs[2];\n return out;\n }\n if (!(t > 0)) {\n out[0] = cb[0];\n out[1] = cb[1];\n out[2] = cb[2];\n return out;\n }\n scratch ??= [createPigment([0, 0, 0]), createPigment([0, 0, 0])];\n const [a, b] = scratch;\n setPigmentColor(a, cb[0], cb[1], cb[2], 1);\n setPigmentColor(b, cs[0], cs[1], cs[2], 1);\n lerpPigmentInto(a, b, t, out, 0);\n return out;\n}\n\n/** K/S of 8-bit colours (key `r << 16 | g << 8 | b`), shared by every 8-bit pigment composite. */\nconst pigmentCache = new Map<number, Pigment>();\n/** About 3 MB of spectra; the cache starts over when it fills (a photo can hold millions of colours). */\nconst PIGMENT_CACHE_MAX = 16384;\n\n/**\n * The prepared pigment (K/S spectrum) of an 8-bit colour, from the cache every 8-bit pigment\n * composite shares. Read-only: do not retarget it with `setPigmentColor`.\n */\nexport function pigment8(r: number, g: number, b: number): Pigment {\n return cachedPigment(r, g, b);\n}\n\nfunction cachedPigment(r: number, g: number, b: number): Pigment {\n const key = (r << 16) | (g << 8) | b;\n let p = pigmentCache.get(key);\n if (!p) {\n if (pigmentCache.size >= PIGMENT_CACHE_MAX) pigmentCache.clear();\n p = createPigment([r / 255, g / 255, b / 255]);\n pigmentCache.set(key, p);\n }\n return p;\n}\n\n/**\n * {@link mixPigmentRgb} on 8-bit channels, with each colour's spectrum cached: writes the 0..1\n * result to `out[0..2]`. The compositor's and {@link blendImageData}'s per-pixel path.\n */\nexport function mixPigment8(b0: number, b1: number, b2: number, s0: number, s1: number, s2: number, t: number, out: number[]): void {\n if (t >= 1 || (b0 === s0 && b1 === s1 && b2 === s2)) {\n out[0] = s0 / 255;\n out[1] = s1 / 255;\n out[2] = s2 / 255;\n return;\n }\n lerpPigmentInto(cachedPigment(b0, b1, b2), cachedPigment(s0, s1, s2), t, out, 0);\n}\n\n/**\n * Composites a straight-alpha `source` over a straight-alpha `backdrop` (both RGBA in 0..1) with a\n * blend mode and an extra `alpha` (opacity), using the W3C general formula:\n * `αo = αs + αb(1 − αs)`, `αo·Co = (1 − αs)·αb·Cb + (1 − αb)·αs·Cs + αs·αb·B(Cb, Cs)`.\n *\n * `pigment` mixes the colours like paint instead, by amount: `Co = lerpPigment(Cb, Cs, αs / αo)`\n * (over an opaque backdrop that is `lerpPigment(Cb, Cs, αs)`), with the same `αo`.\n */\nexport function blendPixel(mode: BlendMode, backdrop: readonly number[], source: readonly number[], alpha = 1): Rgba {\n const ab = backdrop[3] ?? 1;\n const as = (source[3] ?? 1) * alpha;\n if (!(as > 0)) return [backdrop[0], backdrop[1], backdrop[2], ab];\n const ao = as + ab * (1 - as);\n if (ao <= 0) return [0, 0, 0, 0];\n const out: Rgba = [0, 0, 0, ao];\n if (mode === 'pigment') {\n mixPigmentRgb(backdrop, source, as / ao, out);\n return out;\n }\n const b = blendRgb(mode, backdrop, source);\n for (let i = 0; i < 3; i++) {\n out[i] = ((1 - as) * ab * backdrop[i] + (1 - ab) * as * source[i] + as * ab * b[i]) / ao;\n }\n return out;\n}\n\n/**\n * Blends `source` into `backdrop` in place (8-bit straight RGBA of the same size) with\n * {@link blendPixel}. The scalar fallback for modes the canvas cannot composite.\n */\nexport function blendImageData(backdrop: ImageData, source: ImageData, mode: BlendMode, alpha = 1): void {\n const d = backdrop.data;\n const s = source.data;\n const len = Math.min(d.length, s.length);\n const cb = [0, 0, 0, 0];\n const cs = [0, 0, 0, 0];\n const pigment = mode === 'pigment';\n for (let i = 0; i < len; i += 4) {\n if (s[i + 3] === 0) continue;\n if (pigment) {\n const as = (s[i + 3] / 255) * alpha;\n if (!(as > 0)) continue;\n const ao = as + (d[i + 3] / 255) * (1 - as);\n mixPigment8(d[i], d[i + 1], d[i + 2], s[i], s[i + 1], s[i + 2], as / ao, cb);\n d[i] = Math.round(cb[0] * 255);\n d[i + 1] = Math.round(cb[1] * 255);\n d[i + 2] = Math.round(cb[2] * 255);\n d[i + 3] = Math.round(ao * 255);\n continue;\n }\n for (let k = 0; k < 4; k++) {\n cb[k] = d[i + k] / 255;\n cs[k] = s[i + k] / 255;\n }\n const o = blendPixel(mode, cb, cs, alpha);\n d[i] = Math.round(o[0] * 255);\n d[i + 1] = Math.round(o[1] * 255);\n d[i + 2] = Math.round(o[2] * 255);\n d[i + 3] = Math.round(o[3] * 255);\n }\n}\n"]}
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
setPigmentColor,
|
|
11
11
|
tileCoord,
|
|
12
12
|
tileKey
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-XK5LZKBD.js";
|
|
14
14
|
|
|
15
15
|
// src/paint/brush.ts
|
|
16
16
|
var DEFAULT_BRUSH = {
|
|
@@ -1133,4 +1133,4 @@ export {
|
|
|
1133
1133
|
SmudgeTip,
|
|
1134
1134
|
createPigmentTools
|
|
1135
1135
|
};
|
|
1136
|
-
//# sourceMappingURL=chunk-
|
|
1136
|
+
//# sourceMappingURL=chunk-UBIXVXPR.js.map
|