UNPKG

tldraw

Version:

A tiny little drawing editor.

8 lines (7 loc) 8.03 kB
{ "version": 3, "sources": ["../../../../src/lib/shapes/image/ImageAlphaCache.ts"], "sourcesContent": ["import { Image, VecLike } from '@tldraw/editor'\n\n/** Mime types of image formats that support transparency / alpha channel. */\nexport const TRANSPARENT_IMAGE_MIMETYPES: readonly string[] = [\n\t'image/png',\n\t'image/webp',\n\t'image/gif',\n\t'image/avif',\n]\n\n/** Alpha channel data for an image, downsampled for efficient hit testing. */\nexport interface AlphaData {\n\twidth: number\n\theight: number\n\t/** Row-major alpha values (0\u2013255) */\n\talphas: Uint8Array\n}\n\n/** Shared config for image geometries that support alpha hit testing. */\nexport interface ImageAlphaGeometryConfig {\n\talphaDataGetter(): AlphaData | null\n\tcrop: { topLeft: { x: number; y: number }; bottomRight: { x: number; y: number } } | null\n\tflipX: boolean\n\tflipY: boolean\n}\n\n/**\n * Map a point in shape space to normalized [0,1] image coordinates, accounting for crop and flip.\n * @internal\n */\nfunction mapToImageCoords(\n\tconfig: ImageAlphaGeometryConfig,\n\tpoint: VecLike,\n\tbounds: { minX: number; minY: number; w: number; h: number }\n): { nx: number; ny: number } {\n\t// Normalize point to [0,1] within the shape bounds, clamped for edge-margin hits\n\tlet nx = Math.max(0, Math.min(1, (point.x - bounds.minX) / bounds.w))\n\tlet ny = Math.max(0, Math.min(1, (point.y - bounds.minY) / bounds.h))\n\n\t// Map from cropped shape space to full image space\n\tif (config.crop) {\n\t\tconst { topLeft, bottomRight } = config.crop\n\t\tnx = topLeft.x + nx * (bottomRight.x - topLeft.x)\n\t\tny = topLeft.y + ny * (bottomRight.y - topLeft.y)\n\t}\n\n\t// Account for flips\n\tif (config.flipX) nx = 1 - nx\n\tif (config.flipY) ny = 1 - ny\n\n\treturn { nx, ny }\n}\n\n/** Returns true if the point maps to a transparent pixel. Returns false if alpha data isn't loaded yet. */\nexport function isImagePointTransparent(\n\tconfig: ImageAlphaGeometryConfig,\n\tpoint: VecLike,\n\tbounds: { minX: number; minY: number; w: number; h: number }\n): boolean {\n\tconst data = config.alphaDataGetter()\n\tif (!data) return false\n\tconst { nx, ny } = mapToImageCoords(config, point, bounds)\n\treturn isPointTransparent(data, nx, ny)\n}\n\nconst MAX_SIZE = 256\n\nconst alphaCache = new Map<string, AlphaData>()\nconst pending = new Set<string>()\nlet offscreenCanvas: OffscreenCanvas | null = null\n\nfunction getOffscreenCanvas(w: number, h: number): OffscreenCanvas {\n\tif (!offscreenCanvas) {\n\t\toffscreenCanvas = new OffscreenCanvas(w, h)\n\t} else {\n\t\toffscreenCanvas.width = w\n\t\toffscreenCanvas.height = h\n\t}\n\treturn offscreenCanvas\n}\n\nfunction extractAlphas(ctx: OffscreenCanvasRenderingContext2D, w: number, h: number): Uint8Array {\n\tconst imageData = ctx.getImageData(0, 0, w, h)\n\tconst pixels = new Uint32Array(imageData.data.buffer)\n\tconst alphas = new Uint8Array(w * h)\n\tfor (let i = 0; i < alphas.length; i++) {\n\t\talphas[i] = pixels[i] >>> 24\n\t}\n\treturn alphas\n}\n\n/**\n * Start loading alpha data for a given image URL. No-op if already loaded or loading.\n *\n * @param url - The URL to fetch the image from (may be a resolved/optimized CDN URL).\n * @param cacheKey - The key to store/lookup the alpha data under. Defaults to `url`.\n * Pass `asset.props.src` here so that `getAlphaData(asset.props.src)` in getGeometry\n * finds data that was preloaded from a resolved URL.\n */\nexport function preloadAlphaData(url: string, cacheKey?: string): void {\n\tconst key = cacheKey ?? url\n\tif (alphaCache.has(key) || pending.has(key)) return\n\tpending.add(key)\n\n\tconst img = Image()\n\timg.crossOrigin = 'anonymous'\n\timg.onload = async () => {\n\t\tpending.delete(key)\n\t\tconst { width: origW, height: origH } = img\n\t\tif (origW === 0 || origH === 0) return\n\n\t\tconst scale = Math.min(1, MAX_SIZE / Math.max(origW, origH))\n\t\tconst w = Math.max(1, Math.round(origW * scale))\n\t\tconst h = Math.max(1, Math.round(origH * scale))\n\n\t\tlet bitmap: ImageBitmap | null = null\n\t\ttry {\n\t\t\t// Resize off the main thread via createImageBitmap\n\t\t\tbitmap = await createImageBitmap(img, {\n\t\t\t\tresizeWidth: w,\n\t\t\t\tresizeHeight: h,\n\t\t\t\tresizeQuality: 'low',\n\t\t\t})\n\t\t} catch {\n\t\t\t// Fallback handled below\n\t\t}\n\n\t\t// Canvas operations are synchronous from here \u2014 no interleaving from\n\t\t// concurrent preloads that could resize the shared OffscreenCanvas.\n\t\tconst canvas = getOffscreenCanvas(w, h)\n\t\tconst ctx = canvas.getContext('2d')\n\t\tif (!ctx) return\n\n\t\tif (bitmap) {\n\t\t\tctx.drawImage(bitmap, 0, 0)\n\t\t\tbitmap.close()\n\t\t} else {\n\t\t\tctx.drawImage(img, 0, 0, w, h)\n\t\t}\n\n\t\talphaCache.set(key, { width: w, height: h, alphas: extractAlphas(ctx, w, h) })\n\t}\n\timg.onerror = () => {\n\t\tpending.delete(key)\n\t}\n\timg.src = url\n}\n\n/** Get cached alpha data for a URL, or null if not yet loaded. */\nexport function getAlphaData(src: string): AlphaData | null {\n\treturn alphaCache.get(src) ?? null\n}\n\n/**\n * Check whether a point in normalized [0,1] coordinates falls on a transparent pixel.\n * Returns true if the pixel's alpha is below the threshold.\n */\nexport function isPointTransparent(\n\tdata: AlphaData,\n\tnx: number,\n\tny: number,\n\tthreshold = 10\n): boolean {\n\tconst ix = Math.min(Math.floor(nx * data.width), data.width - 1)\n\tconst iy = Math.min(Math.floor(ny * data.height), data.height - 1)\n\treturn data.alphas[iy * data.width + ix] < threshold\n}\n"], "mappings": "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", "names": [] }