@oh-just-another/diagram 0.3.13 → 0.3.15

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../renderer-core/src/rendering/lod.ts", "../../renderer-core/src/rendering/shape-renderer.ts", "../../scene/src/constants.ts", "../../scene/src/text/style.ts", "../../scene/src/text/placeholder.ts", "../../scene/src/text/text-runs.ts", "../../scene/src/text/paragraphs.ts", "../../types/src/assert.ts", "../../math/src/vec2.ts", "../../math/src/matrix.ts", "../../math/src/bounds.ts", "../../math/src/polygon.ts", "../../scene/src/text/text-measure.ts", "../../scene/src/shapes/shape.ts", "../../scene/src/model/order.ts", "../../scene/src/query/queries.ts", "../../scene/src/shapes/brush-outline.ts", "../../scene/src/shapes/render-bounds.ts", "../../scene/src/model/viewport.ts", "../../../node_modules/.pnpm/@radix-ui+colors@3.0.0/node_modules/@radix-ui/colors/index.mjs", "../../tokens/src/colors.ts", "../../renderer-core/src/constants.ts", "../../renderer-core/src/text/text-editing.ts", "../../renderer-core/src/raster/animation-adapter.ts", "../../renderer-core/src/raster/image-source-guard.ts", "../../renderer-core/src/rendering/built-in-renderers.ts", "../../renderer-core/src/targets/dim-target.ts", "../../renderer-core/src/caches/shape-cache.ts", "../../renderer-core/src/caches/lru-cache.ts", "../../renderer-core/src/caches/shape-cache-bitmap.ts", "../../renderer-core/src/rendering/scene-renderer.ts", "../../fonts/src/index.ts", "../../renderer-canvas/src/canvas2d/image-source.ts", "../../renderer-canvas/src/canvas2d/canvas-target.ts", "../../renderer-canvas/src/offscreen/replay-codec.ts", "../../renderer-canvas/src/render-worker.ts"],
4
- "sourcesContent": ["import type { Bounds } from \"@oh-just-another/types\";\n\n/**\n * Level-of-detail thresholds for `renderScene` \u2014 decided PER ELEMENT from\n * its actual size on screen, not from the zoom level, so a huge shape or a\n * giant heading stays readable at 1 % while a sticky note degrades long\n * before that.\n *\n * - **placeholderMaxScreenPx** \u2192 a shape whose on-screen AABB (longer side,\n * world \u00D7 zoom) is below this many pixels is drawn as a flat fill at its\n * AABB and its renderer is skipped entirely.\n * - **minTextScreenPx** \u2192 text (standalone text shapes and embedded shape\n * labels) whose on-screen font size (`fontSize \u00D7 zoom`) is below this\n * many pixels is skipped \u2014 it could not be read anyway, and its\n * wrap + measure cost is the bulk of text rendering.\n *\n * Omit a threshold to disable that level.\n */\nexport interface LodOptions {\n readonly placeholderMaxScreenPx?: number;\n readonly minTextScreenPx?: number;\n}\n\n/** Longer side of `bounds` on screen at `zoom`, in CSS px. */\nexport const screenSizeOf = (bounds: Bounds, zoom: number): number =>\n Math.max(bounds.width, bounds.height) * zoom;\n\n/** `true` when text of `fontSize` (world units) is below the readable LOD floor at `zoom`. */\nexport const isTextBelowLod = (\n fontSize: number,\n zoom: number,\n lod: LodOptions | undefined,\n): boolean => lod?.minTextScreenPx !== undefined && fontSize * zoom < lod.minTextScreenPx;\n", "import type { ElementBase } from \"@oh-just-another/scene\";\nimport type { RenderTarget } from \"../targets/render-target.js\";\nimport type { AnimationClock } from \"../raster/animation-adapter.js\";\n\n/**\n * Optional draw context passed to an {@link ElementRenderer}. Carries the\n * current view `zoom` so a renderer can draw screen-constant features (e.g. a\n * 1px hairline border that does NOT scale with zoom): a local stroke width of\n * `1 / (zoom * shape.scale)` lands at one device pixel. Optional and additive \u2014\n * renderers that don't need it ignore the third argument, and callers that\n * can't supply it (preview / export at 1:1) may omit it.\n */\nexport interface ElementRenderContext {\n /** Current view scale (1.0 = 1:1). `world \u00D7 zoom = screen px`. */\n readonly zoom: number;\n /**\n * Per-instance animated-content playback clock. When set, the image\n * renderer samples animated sources at `clock(shape)` instead of the\n * process-global fallback ({@link setAnimationClock}) \u2014 so two editors on\n * one page can freeze / offset their GIFs independently. Omitted by headless\n * / preview paths, which fall back to the module clock.\n */\n readonly clock?: AnimationClock;\n /**\n * Static-export content switches. Omitted (interactive rendering) =\n * draw everything; export pipelines pass explicit flags (defaults in\n * `EXPORT_CONTENT_DEFAULTS`, overridable in the export UI) so hosts\n * can strip collaborative chrome \u2014 sticky reactions / tags / author \u2014\n * from PNG / SVG output.\n */\n readonly content?: {\n readonly stickyReactions?: boolean;\n readonly stickyTags?: boolean;\n readonly stickyAuthor?: boolean;\n /**\n * The \"+\" add-reaction button next to the pills \u2014 pure UI chrome:\n * drawn on the canvas so it tracks the shape 1:1 while dragging,\n * but excluded from static exports and read-only views.\n */\n readonly stickyAddButton?: boolean;\n };\n /**\n * Id of the element under the idle cursor, when the host tracks it.\n * Drives hover-only chrome (the sticky \"+\" add-reaction button).\n * Omitted by exports / headless paths \u2014 hover chrome never shows.\n */\n readonly hoveredElement?: string;\n /**\n * Draw a grey prompt inside EMPTY text elements (see\n * `pickTextPlaceholder`). Set by the interactive editor; omitted by\n * exports / headless / read-only paths so an empty text stays blank.\n */\n readonly textPlaceholders?: boolean;\n}\n\n/**\n * Draws a single shape onto `target`. The shape's `position` / `rotation` /\n * `scale` have already been applied to the target \u2014 implementations draw in\n * the shape's *local* coordinate space.\n *\n * Implementations should also apply style (fill / stroke / etc.) themselves;\n * the renderer-core does not push styles globally because some shapes (e.g.\n * `text`) extend the base `Style` with overlays.\n */\nexport type ElementRenderer<S extends ElementBase = ElementBase> = (\n shape: S,\n target: RenderTarget,\n ctx?: ElementRenderContext,\n) => void;\n\nconst registry = new Map<string, ElementRenderer>();\n\n/**\n * Register a renderer for a shape type. Plugins call this at module load.\n * The kernel ships renderers for every built-in shape from `@oh-just-another/scene`\n * \u2014 they are installed by `@oh-just-another/renderer-canvas` (and any other\n * backend) on import.\n */\nexport const registerElementRenderer = <S extends ElementBase>(\n type: S[\"type\"],\n renderer: ElementRenderer<S>,\n): void => {\n registry.set(type, renderer as ElementRenderer);\n};\n\n/** Look up a registered renderer. Returns `undefined` for unknown types. */\nexport const getElementRenderer = (type: string): ElementRenderer | undefined => registry.get(type);\n\n/** True if a renderer is registered for `type`. */\nexport const hasElementRenderer = (type: string): boolean => registry.has(type);\n", "/**\n * Tunable thresholds for the scene-level helpers (snap engine, hit-test\n * cheap-cull). Keep magic numbers here so hosts can re-tune the engine\n * without touching the algorithm code.\n */\n\n/**\n * Canvas paper colour when the scene's viewport carries no `background`.\n * Matches the light chrome token (`UI_SURFACE.light.canvas`) so a scene\n * without an explicit colour looks exactly as before the field existed.\n * Any CSS colour; keep it light unless the scene's own colours are authored\n * for dark paper.\n */\nexport const DEFAULT_CANVAS_BACKGROUND = \"#f5f5f5\";\n\n/**\n * Half-side of the bounding box used by `isProbeNearElement` to cheap-cull\n * snap candidates. Shapes farther than this from the probe (plus the\n * snap threshold cushion) are skipped without the full anchor walk.\n *\n * The default of 1000 world units covers any typical editor shape; bump\n * it if hosts work with very large diagrams where the cheap-cull starts\n * to over-prune real candidates.\n */\nexport const SNAP_PROBE_CULL_RADIUS = 1000;\n\n/**\n * Fixed grid spacing in world units: the step `renderGrid` paints and the\n * step snap-to-grid rounds to. Tune to change the grid density; range 4\u201364.\n */\nexport const DEFAULT_GRID_SPACING = 20;\n\n/**\n * Padding (world units) the elbow router inflates obstacle bboxes\n * by before searching. Larger values keep edges visibly clear of\n * shapes; smaller values let the router squeeze through tight\n * spaces. 20 px matches the grid spacing for diagrams that snap to\n * the grid.\n */\nexport const ELBOW_OBSTACLE_MARGIN = 20;\n\n/**\n * Epsilon used to decide whether an axis-aligned segment runs\n * *along* an obstacle boundary (allowed) or *through* it (blocked).\n * A degenerate small value catches floating-point fuzz from\n * `inflate` arithmetic without admitting real crossings.\n */\nexport const ELBOW_OBSTACLE_INTERIOR_EPSILON = 0.5;\n\n/**\n * Longest stub (world px) the orthogonal heuristic fallback in\n * `getLinkPath` adds before bending when an endpoint is anchored to a\n * named side. Bigger values push the first bend further from the shape;\n * reasonable range 24\u201364.\n */\nexport const ELBOW_STUB_MAX = 40;\n\n/**\n * Shortest stub (world px) for the same fallback \u2014 keeps the exit\n * visible even when the endpoints are nearly on top of each other.\n * Reasonable range 4\u201316.\n */\nexport const ELBOW_STUB_MIN = 8;\n\n/**\n * The stub scales as endpoint distance divided by this factor, clamped\n * to [ELBOW_STUB_MIN, ELBOW_STUB_MAX]. Larger divisors give shorter\n * stubs on mid-range spans. Reasonable range 2\u20138.\n */\nexport const ELBOW_STUB_DISTANCE_DIVISOR = 4;\n\n/**\n * Per-turn cost added in the elbow A* so the router minimises BENDS first,\n * distance second (lexicographic \u2014 far larger than any plausible canvas\n * distance). Keeps routes stable: small shape moves stay on the same\n * choice between equal-distance alternatives, and the path takes the\n * fewest corners.\n */\nexport const ELBOW_BEND_PENALTY = 100000;\n\n/**\n * Hysteresis band (world px) for the C-wrap side choice in `wrapRoute`.\n * When the connector must wrap around the union of its two bound shapes,\n * the over/under (or left/right) side is picked from the endpoints'\n * midpoint vs the union centre. Within this band of the centre the\n * previous side is kept (read from `edge.routedPoints`), so a small\n * back-and-forth drag doesn't thrash; the side only switches once the\n * midpoint moves this far past the centre. Larger = stickier.\n * Range: 8\u201348.\n */\nexport const ELBOW_WRAP_HYSTERESIS = 24;\n\n/**\n * Perf cap for \"avoid obstacles\" routing. The A* grid scales with the\n * number of obstacle corners, and the route is recomputed every frame\n * while a shape is dragged, so above this many scene shapes we skip\n * whole-scene avoidance and fall back to the cheap two-box elbow (the\n * link still keeps clear of its own ends). Range: 60\u2013300.\n */\nexport const ELBOW_AVOID_MAX_OBSTACLES = 150;\n\n/**\n * Length (world px) of the fixed, non-movable terminal segment an elbow\n * connector always leaves at each end before its first bend \u2014 the endpoint\n * is pushed out this far along its exit heading so the connector departs/\n * arrives perpendicular to the edge and there's buffer room to draw the\n * arrowhead. Must stay \u2265 ELBOW_OBSTACLE_MARGIN so the pushed-out point\n * sits outside the inflated obstacle the A* router avoids. Larger \u2192 more\n * breathing room before the first bend. Range: 16\u201340.\n */\nexport const ELBOW_TERMINAL_BUFFER = 30;\n\n/**\n * --- Self-loop connectors (a link whose both ends bind to the SAME element) ---\n *\n * A self-loop is routed OUTSIDE the element so it reads as a loop/arc instead of\n * a flat line on (or across) the shape.\n *\n * - `SELF_LOOP_SIZE` \u2014 how far (world px) the loop bows out past the element's\n * edge. Fixed for every element. Range: 24\u201380.\n * - `SELF_LOOP_SPREAD` \u2014 when both ends resolve to the SAME point (centre /\n * floating / same anchor) the two exit points are spread this far apart along\n * the edge so the loop has width. Clamped to a third of the side. Range: 12\u201340.\n * - `SELF_LOOP_CURVE_ARM_FACTOR` \u2014 control-arm length for a curved self-loop as a\n * multiple of `SELF_LOOP_SIZE`; larger = rounder, more pronounced arc. 2.2\n * gives a clean teardrop. Range: 1.5\u20133.\n */\nexport const SELF_LOOP_SIZE = 40;\nexport const SELF_LOOP_SPREAD = 24;\nexport const SELF_LOOP_CURVE_ARM_FACTOR = 2.2;\n\n/**\n * Clearance (world px) a candidate centred path (the \"thread\"/mid-S or the\n * C-wrap) must keep from a bound shape's interior before it counts as CROSSING\n * it. This is the threshold that decides thread-vs-wrap-vs-A*: a larger value\n * makes the router bail off the direct/centred path sooner (route stays further\n * from shapes), a smaller value lets it skim closer to an edge before detouring.\n * At 1 px it only rejects genuine interior crossings, allowing edge-grazing.\n * Range: 1\u20138.\n */\nexport const ELBOW_OBSTACLE_CLEARANCE = 1;\n\n/**\n * Parametric step used to sample each segment of a candidate path when testing\n * whether it crosses a shape (`pathCrossesObstacle`). Smaller = finer (catches\n * a narrow shape a coarse sampling would skip) at more cost; 0.1 samples 11\n * points per segment, enough for typical shape sizes. Range: 0.02\u20130.2.\n */\nexport const ELBOW_CROSS_SAMPLE_STEP = 0.1;\n\n/**\n * --- Curved (bezier) link geometry ---\n *\n * Shared by the renderer (draws cubic beziers), hit-testing and bounds\n * (flatten the same curve) so the visible curve and the clickable curve\n * agree. Lives in scene so lower layers own the geometry; renderer-core\n * imports it.\n *\n * - `CURVE_CATMULL_TENSION` \u2014 divisor for the Catmull-Rom tangents in the\n * spline\u2192bezier conversion (waypointed curves). 6 is canonical uniform\n * Catmull-Rom (control point = P + (Pnext \u2212 Pprev) / 6). Larger \u2192 tighter;\n * smaller \u2192 looser. Range: 4\u20138.\n * - `CURVE_END_TANGENT_RATIO` \u2014 for a no-waypoint span the cubic's control\n * arms leave/enter the endpoints along their edge normals with length =\n * this fraction of the endpoint distance, so the connector exits/enters\n * perpendicular to the element edge (flowchart look). Larger \u2192 rounder /\n * more pronounced. Range: 0.25\u20130.6.\n * - `CURVE_END_TANGENT_MAX_PX` \u2014 caps that control-arm length (world px) so a\n * long link doesn't over-bow. Range: 60\u2013160.\n * - `CURVE_FLATTEN_SEGMENTS` \u2014 samples per cubic when flattening the curve\n * for hit-testing / bounds. Higher = closer to the drawn curve. Range:\n * 8\u201324.\n */\nexport const CURVE_CATMULL_TENSION = 6;\nexport const CURVE_END_TANGENT_RATIO = 0.8;\nexport const CURVE_END_TANGENT_MAX_PX = 240;\nexport const CURVE_FLATTEN_SEGMENTS = 16;\n\n/**\n * --- Roundness (Style.roundness) ---\n *\n * Adaptive radius: pick a fixed radius for shapes bigger than the cutoff,\n * scale proportionally for smaller ones so the corner doesn't dominate.\n * 32 px / 0.25 looks rounded without becoming a capsule across the\n * realistic shape-size range.\n */\n\n/** Fixed pixel radius used by adaptive rounding for shapes \u2265 cutoff. */\nexport const ADAPTIVE_CORNER_RADIUS = 32;\n\n/**\n * Proportional radius (0..1 of the smaller side) used by adaptive\n * rounding for shapes below the cutoff, and the fall-through when\n * `Roundness.value` is omitted but the type is `round`.\n */\nexport const PROPORTIONAL_CORNER_RADIUS = 0.25;\n\n/**\n * --- Text bounds estimation ---\n *\n * The text bounder has no layout engine, so it approximates the box.\n * Renderers compute the precise layout (via `measureText`) during\n * draw / caret positioning; these factors only drive selection bbox\n * and resize-handle placement, where a rough estimate is fine.\n *\n * - `TEXT_APPROX_CHAR_WIDTH_FACTOR` \u2014 average glyph advance as a\n * fraction of font size (~0.6 for proportional Latin text).\n * - `TEXT_LINE_HEIGHT_FACTOR` \u2014 line height as a multiple of font\n * size. Must match the renderer's `DEFAULT_LINE_HEIGHT_FACTOR`.\n */\nexport const TEXT_APPROX_CHAR_WIDTH_FACTOR = 0.6;\nexport const TEXT_LINE_HEIGHT_FACTOR = 1.2;\n\n/**\n * --- Frame header (label strip) geometry ---\n *\n * The frame's name is drawn in a strip ABOVE the frame body (local y in\n * `[-FRAME_HEADER_HEIGHT, 0]`). Shared by the renderer (draws it), the\n * editor (header double-click \u2192 rename hit zone + render overflow) and\n * react-ui (positions the inline name editor) so all three agree.\n *\n * - `FRAME_HEADER_HEIGHT` \u2014 strip height (world px).\n * - `FRAME_HEADER_PADDING_X` \u2014 horizontal text inset, each side (world px).\n * - `FRAME_HEADER_FONT_SIZE` \u2014 label font size (world px).\n *\n * The strip width is dynamic: it hugs the label width but is capped at the\n * frame's own width (a too-long name is ellipsised) \u2014 computed in the\n * renderer, which can measure text.\n */\nexport const FRAME_HEADER_HEIGHT = 24;\nexport const FRAME_HEADER_PADDING_X = 8;\nexport const FRAME_HEADER_FONT_SIZE = 12;\n\n/**\n * --- Layout defaults ---\n *\n * Used by the built-in layout functions (`gridLayout`, `stackLayout`,\n * `wrapLayout`, `treeLayout`) when the caller's spec omits the value.\n *\n * - `DEFAULT_LAYOUT_GAP` \u2014 cell/sibling gap (world px) for grid, stack and\n * wrap layouts. Larger = more breathing room between shapes. Range: 8\u201348.\n * - `DEFAULT_TREE_RANK_SEP` \u2014 vertical distance (world px) between successive\n * depth levels in the tree layout. Larger = taller tree. Range: 40\u2013160.\n * - `DEFAULT_TREE_NODE_SEP` \u2014 horizontal distance (world px) between siblings\n * in the tree layout. Larger = wider tree. Range: 12\u201364.\n */\nexport const DEFAULT_LAYOUT_GAP = 16;\nexport const DEFAULT_TREE_RANK_SEP = 80;\nexport const DEFAULT_TREE_NODE_SEP = 24;\n\n/**\n * --- Outline sampling ---\n *\n * - `DEFAULT_OUTLINE_SAMPLES` \u2014 fixed density `findNearestOutlinePoint` walks\n * the outline at when resolving the nearest ratio to a world point. Good\n * enough for visual snap; bump it for sub-pixel accuracy at high zoom.\n * Range: 32\u2013256.\n * - `FLOATING_OUTLINE_SAMPLES` \u2014 segments the outline is sampled into when\n * intersecting it with the floating-endpoint ray. Smooth enough for\n * ellipses at high zoom without being a hot-loop cost (resolved once per\n * edge per frame). Range: 48\u2013256.\n */\nexport const DEFAULT_OUTLINE_SAMPLES = 64;\nexport const FLOATING_OUTLINE_SAMPLES = 96;\n\n/**\n * Fallback scene dimensions, in pixels, for a scene with no explicit\n * viewport size \u2014 a freshly imported document whose source carries no\n * canvas size, or an empty export region. Just needs to be non-degenerate.\n * Range: a few hundred to ~2000.\n */\nexport const FALLBACK_SCENE_WIDTH = 800;\nexport const FALLBACK_SCENE_HEIGHT = 600;\n\n/**\n * Max angular step (radians) between sampled points along a brush-outline round\n * join or cap arc. Smaller = smoother curves / more points; larger = coarser /\n * cheaper. ~0.35 rad (20\u00B0) keeps joins visually round without flooding the\n * polygon. Range: 0.2\u20130.6.\n */\nexport const BRUSH_OUTLINE_ARC_STEP = 0.35;\n\n/**\n * Miter limit for a brush-outline concave corner: when the miter point would run\n * more than this many half-widths from the vertex (a very sharp turn), fall back\n * to a bevel (two offset points) so the outline can't spike into a long spar.\n * Range: 1.5\u20134.\n */\nexport const BRUSH_OUTLINE_MITER_LIMIT = 2.5;\n\n/**\n * Default fractional position of a link label along its path (0 = source end,\n * 1 = target end). Used when `LinkLabel.position` is unset. Range: 0\u20131.\n */\nexport const LINK_LABEL_DEFAULT_POSITION = 0.5;\n\n/**\n * Default link-label font size (world px at zoom 1). Range: 10\u201316.\n */\nexport const LINK_LABEL_DEFAULT_FONT_SIZE = 12;\n\n/**\n * Max link-label line width before word-wrap kicks in (world px at zoom 1).\n * Wider = fewer, longer lines; narrower = taller pill. Range: 100\u2013240.\n */\nexport const LINK_LABEL_MAX_WIDTH = 160;\n\n/**\n * Inner padding of the label pill around the text block (world px at zoom 1).\n * Range: 2\u201310.\n */\nexport const LINK_LABEL_PAD_X = 6;\nexport const LINK_LABEL_PAD_Y = 3;\n\n/**\n * Line-height factor for multiline link labels (\u00D7 fontSize). Range: 1.1\u20131.5.\n */\nexport const LINK_LABEL_LINE_HEIGHT = 1.25;\n\n/**\n * Min arc-length distance (world px) the label anchor keeps from either path\n * end, so the pill never sits on an arrowhead. Applied as a clamp on the\n * fractional position; ignored when the whole path is shorter than twice this.\n * Range: 12\u201340.\n */\nexport const LINK_LABEL_END_CLEARANCE = 24;\n\n/**\n * Average glyph advance as a fraction of fontSize \u2014 the conservative width\n * estimate used where real text measurement is unavailable (hit-testing,\n * dirty-rect / culling bounds). Slightly generous on purpose: overestimating\n * keeps a label inside its computed bounds. Range: 0.55\u20130.7.\n */\nexport const LINK_LABEL_CHAR_WIDTH_FACTOR = 0.62;\n\n/**\n * Built-in polygon presets for image masks (`ImageMask.kind: \"polygon\"`),\n * as normalised (0..1) closed rings over the element box. Offered by the\n * mask picker UI; hosts may pass any other ring \u2014 the model accepts\n * arbitrary polygons. Point counts stay low: masks clip through the\n * render targets' clip API, and every vertex costs path work per frame.\n */\nexport const IMAGE_MASK_POLYGON_PRESETS: Readonly<\n Record<string, readonly { readonly x: number; readonly y: number }[]>\n> = {\n diamond: [\n { x: 0.5, y: 0 },\n { x: 1, y: 0.5 },\n { x: 0.5, y: 1 },\n { x: 0, y: 0.5 },\n ],\n triangle: [\n { x: 0.5, y: 0 },\n { x: 1, y: 1 },\n { x: 0, y: 1 },\n ],\n hexagon: [\n { x: 0.25, y: 0 },\n { x: 0.75, y: 0 },\n { x: 1, y: 0.5 },\n { x: 0.75, y: 1 },\n { x: 0.25, y: 1 },\n { x: 0, y: 0.5 },\n ],\n star: [\n { x: 0.5, y: 0 },\n { x: 0.618, y: 0.363 },\n { x: 1, y: 0.382 },\n { x: 0.691, y: 0.618 },\n { x: 0.809, y: 1 },\n { x: 0.5, y: 0.764 },\n { x: 0.191, y: 1 },\n { x: 0.309, y: 0.618 },\n { x: 0, y: 0.382 },\n { x: 0.382, y: 0.363 },\n ],\n};\n\n/**\n * Placeholder shown inside an EMPTY text element while it is being\n * written (interactive rendering only \u2014 never in exports). One entry is\n * picked per element, deterministically from its id, with these relative\n * weights (`weight` = chance ticket count): the plain prompts dominate, the\n * jokes are rare treats. Hosts may pass their own list to\n * `pickTextPlaceholder`. The text bounder sizes an empty text element by\n * its prompt, so the selection box wraps what is on screen.\n */\nexport interface TextPlaceholder {\n readonly text: string;\n /** Relative chance; integer \u2265 1. */\n readonly weight: number;\n}\n/**\n * Selection-outline (contour) sampling \u2014 how many polyline points stand in\n * for a curve when a shape's outline is walked (hit-testing, snap probes,\n * link end-points along the outline).\n * - `SELECTION_OUTLINE_ELLIPSE_SAMPLES` \u2014 points around a whole ellipse.\n * Range 24\u201396; fewer = faster, coarser hit areas.\n * - `SELECTION_OUTLINE_CURVE_SAMPLES` \u2014 points per Q/C path segment.\n * Range 4\u201324.\n * - `SELECTION_OUTLINE_CORNER_SAMPLES` \u2014 points per rounded-rect corner arc.\n * Range 2\u201312.\n */\nexport const SELECTION_OUTLINE_ELLIPSE_SAMPLES = 48;\nexport const SELECTION_OUTLINE_CURVE_SAMPLES = 10;\nexport const SELECTION_OUTLINE_CORNER_SAMPLES = 6;\n\n/**\n * Upper bound on parent-chain walks (`getAncestors` / nesting queries) so a\n * corrupted `parentId` cycle terminates instead of looping. Larger than any\n * sane nesting depth; range 16\u2013256.\n */\nexport const MAX_PARENT_DEPTH = 64;\n\n/**\n * Default `SpatialGrid` cell size in world units. Tuned for editor-scale\n * scenes with ~100\u2013400 px shapes: a shape touches 1\u20134 cells, range queries\n * visit few cells. Raise for very large shapes, lower for dense tiny ones.\n * Range 64\u20131024.\n */\nexport const SPATIAL_GRID_CELL_SIZE = 256;\n\n/**\n * Default hit tolerance (world units) for `findLinkAt` \u2014 how far from a\n * link's stroke a point still counts as \"on the link\". Range 2\u201312.\n */\nexport const LINK_HIT_THRESHOLD = 5;\n\nexport const TEXT_PLACEHOLDERS: readonly TextPlaceholder[] = [\n { text: \"Type something\", weight: 40 },\n { text: \"Place for text\", weight: 20 },\n { text: \"Start typing\u2026\", weight: 12 },\n { text: \"Your text here\", weight: 10 },\n { text: \"Add a note\", weight: 8 },\n { text: \"What's on your mind?\", weight: 6 },\n { text: \"Words go here\", weight: 5 },\n { text: \"Say it in a few words\", weight: 4 },\n { text: \"Lorem ipsum? No \u2014 your words.\", weight: 3 },\n { text: \"Blank is a state of mind\", weight: 2 },\n { text: \"Insert genius here\", weight: 2 },\n { text: \"The cursor is waiting patiently\", weight: 1 },\n];\n\n/**\n * Longest accessible name (screen-reader announcement) built from a text\n * body or a shape label; longer content is cut with an ellipsis so the\n * announcement stays actionable. Range 40\u2013160.\n */\nexport const ACCESSIBLE_NAME_MAX_CHARS = 80;\n", "import type { Color } from \"@oh-just-another/types\";\nimport { ADAPTIVE_CORNER_RADIUS, PROPORTIONAL_CORNER_RADIUS } from \"../constants.js\";\n\nexport type LineCap = \"butt\" | \"round\" | \"square\";\nexport type LineJoin = \"miter\" | \"round\" | \"bevel\";\n\n/**\n * Where the stroke sits relative to the shape's path.\n * `center` \u2014 half the stroke width inside the path, half outside.\n * Canvas2D / SVG default.\n * `inside` \u2014 stroke is fully inside the path (path = outer edge).\n * Useful when shape bounds must match the fill region\n * exactly (auto-layout / hit-tests).\n * `outside` \u2014 stroke is fully outside (path = inner edge).\n */\nexport type StrokeAlign = \"center\" | \"inside\" | \"outside\";\n\n/**\n * Corner-rounding spec for shapes that support it (rectangle, container,\n * box arrow, \u2026):\n * `sharp` \u2014 no rounding (sharp corners). Equivalent to omitting the\n * field; lets the value be set explicitly.\n * `round` \u2014 rounded corners. Without `value`, falls back to the\n * adaptive radius (fixed 32 px for big shapes, scales to\n * 25 % of the smaller side for shapes < 128 px so they\n * don't read as a capsule).\n */\nexport interface Roundness {\n readonly type: \"sharp\" | \"round\";\n /**\n * Override the rounded-corner radius in world units. Ignored when\n * `type === \"sharp\"`. When omitted on `round` shapes the renderer\n * applies the adaptive default (see {@link Style}).\n */\n readonly value?: number;\n}\n\n/**\n * Visual style for shapes and edges. Every field is optional so that scenes,\n * patches and partial updates stay compact; renderers fall back to library\n * defaults when a field is omitted.\n */\nexport interface Style {\n readonly fill?: Color;\n readonly stroke?: Color;\n readonly strokeWidth?: number;\n readonly opacity?: number;\n readonly dashArray?: readonly number[];\n readonly lineCap?: LineCap;\n readonly lineJoin?: LineJoin;\n /** Stroke alignment relative to the path. Defaults to `center`. */\n readonly strokeAlign?: StrokeAlign;\n /** Corner-rounding spec. Omitted = sharp corners. */\n readonly roundness?: Roundness;\n}\n\nexport type TextAlign = \"left\" | \"center\" | \"right\";\nexport type TextBaseline = \"top\" | \"middle\" | \"bottom\";\nexport type FontWeight = \"normal\" | \"bold\";\nexport type FontStyle = \"normal\" | \"italic\";\n\n/**\n * Text decorations (underline / strikethrough). Both can be on at once.\n * Rendered as thin line-rects under / through the text by the renderer,\n * so they work identically on Canvas2D and WebGL2.\n */\nexport interface TextDecoration {\n readonly underline?: boolean;\n readonly strikethrough?: boolean;\n}\n\n/**\n * Text-specific style overlay. Inherits all `Style` fields (fill = text color,\n * stroke = outline). Layout metrics live on the `TextElement` itself, not here.\n */\nexport interface TextStyle extends Style {\n readonly textAlign?: TextAlign;\n readonly textBaseline?: TextBaseline;\n /**\n * Marker-style background behind the glyphs (highlight colour). Painted\n * as a full line-height rect under the text, per styled run when runs are\n * present. Omitted = no highlight.\n */\n readonly highlight?: Color;\n /** Bold toggle. Default `\"normal\"`. */\n readonly fontWeight?: FontWeight;\n /** Italic toggle. Default `\"normal\"`. */\n readonly fontStyle?: FontStyle;\n /** Underline / strikethrough. Omitted = neither. */\n readonly textDecoration?: TextDecoration;\n}\n\n/**\n * How far a shape's stroke extends OUTSIDE its geometric contour, in world\n * units. Depends on stroke width and alignment: `outside` \u2192 the full width,\n * `center` \u2192 half, `inside` \u2192 none. No stroke \u2192 0. Used to place the\n * selection halo a constant distance beyond the shape's VISIBLE outer edge\n * (contour + this extent), regardless of border thickness / alignment.\n */\nexport const strokeOutsideExtent = (style: Style): number => {\n const hasStroke = style.stroke !== undefined && style.stroke !== \"transparent\";\n if (!hasStroke) return 0;\n const w = style.strokeWidth ?? 1;\n if (w <= 0) return 0;\n switch (style.strokeAlign ?? \"center\") {\n case \"outside\":\n return w;\n case \"inside\":\n return 0;\n default:\n return w / 2;\n }\n};\n\nexport const getCornerRadius = (\n roundness: Roundness | undefined,\n width: number,\n height: number,\n): number => {\n if (!roundness || roundness.type === \"sharp\") return 0;\n const smaller = Math.min(Math.abs(width), Math.abs(height));\n if (smaller <= 0) return 0;\n if (roundness.value !== undefined) {\n // Honour the override but clamp to half the smaller side so\n // the corner radii can't overlap on narrow shapes (would\n // produce a degenerate path).\n return Math.max(0, Math.min(roundness.value, smaller / 2));\n }\n // Adaptive default: proportional below the cutoff, fixed above.\n const cutoff = ADAPTIVE_CORNER_RADIUS / PROPORTIONAL_CORNER_RADIUS;\n if (smaller <= cutoff) return smaller * PROPORTIONAL_CORNER_RADIUS;\n return ADAPTIVE_CORNER_RADIUS;\n};\n", "import { TEXT_PLACEHOLDERS, type TextPlaceholder } from \"../constants.js\";\n\n/** FNV-1a 32-bit hash \u2014 stable across runs, cheap, good spread for short ids. */\nconst fnv1a = (s: string): number => {\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n h = Math.imul(h, 0x01000193) >>> 0;\n }\n return h;\n};\n\n/**\n * Pick the placeholder for an empty text element. Weighted by\n * `TextPlaceholder.weight`, and DETERMINISTIC in `seed` (the element id):\n * the same element always shows the same prompt \u2014 no reshuffling between\n * frames or re-renders \u2014 while different elements spread across the list.\n */\nexport const pickTextPlaceholder = (\n seed: string,\n placeholders: readonly TextPlaceholder[] = TEXT_PLACEHOLDERS,\n): string => {\n const total = placeholders.reduce((sum, p) => sum + Math.max(1, p.weight), 0);\n if (total <= 0 || placeholders.length === 0) return \"\";\n let ticket = fnv1a(seed) % total;\n for (const p of placeholders) {\n ticket -= Math.max(1, p.weight);\n if (ticket < 0) return p.text;\n }\n return placeholders[placeholders.length - 1]?.text ?? \"\";\n};\n", "import type { TextStyle } from \"./style.js\";\nimport type { TextElement } from \"../shapes/shape.js\";\n\n/**\n * A styled segment of a text block. `text` is the raw substring; `style`\n * is a PARTIAL overlay merged over the owning {@link TextElement}'s base\n * `style` (element style wins for fields the run omits). Omitting `style`\n * means \"inherit the element style verbatim\".\n *\n * Runs are an ADDITIVE overlay: the element's flat `text` stays the source\n * of truth and MUST equal `runs.map(r => r.text).join(\"\")`. A `TextElement`\n * with no `runs` (or an empty array) renders exactly as before this feature\n * existed \u2014 one uniform style \u2014 so plain-text scenes are untouched.\n */\nexport interface TextRun {\n readonly text: string;\n readonly style?: Partial<TextStyle>;\n}\n\n/** Concatenated raw text of a run list (the flat-text source of truth). */\nexport const runsToText = (runs: readonly TextRun[]): string => runs.map((r) => r.text).join(\"\");\n\n/**\n * Stable-ish key for a run style, used only to coalesce adjacent runs that\n * carry identical styling. Sorts top-level keys so key order doesn't defeat\n * the compare. A false \"different\" verdict only costs an extra (correct) run,\n * never wrong rendering, so a shallow canonicalisation is sufficient.\n */\nconst styleKey = (style: Partial<TextStyle> | undefined): string => {\n if (!style) return \"\";\n const entries = Object.entries(style as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return JSON.stringify(entries);\n};\n\n/**\n * Drop empty-text runs and coalesce adjacent runs with identical styling.\n * Returns a compact, canonical run list. An all-empty input yields `[]`.\n */\nexport const normalizeRuns = (runs: readonly TextRun[]): TextRun[] => {\n const out: TextRun[] = [];\n for (const run of runs) {\n if (run.text === \"\") continue;\n const last = out[out.length - 1];\n if (last !== undefined && styleKey(last.style) === styleKey(run.style)) {\n out[out.length - 1] = {\n text: last.text + run.text,\n ...(last.style !== undefined ? { style: last.style } : {}),\n };\n } else {\n out.push(run);\n }\n }\n return out;\n};\n\n/** The run list to start from: explicit `runs`, else one run spanning `text`. */\nconst baseRuns = (el: Pick<TextElement, \"text\" | \"runs\">): TextRun[] => {\n const runs = el.runs;\n if (runs !== undefined && runs.length > 0) return normalizeRuns(runs);\n return el.text === \"\" ? [] : [{ text: el.text }];\n};\n\n/**\n * The runs that fall inside the character range `[from, to)` of a text block,\n * clipped at the range edges. Used by the renderer to split each visual line\n * into per-style segments. Styles are preserved verbatim (still partial\n * overlays over the element style).\n */\nexport const sliceRuns = (\n el: Pick<TextElement, \"text\" | \"runs\">,\n from: number,\n to: number,\n): TextRun[] => {\n const lo = Math.min(from, to);\n const hi = Math.max(from, to);\n const out: TextRun[] = [];\n let pos = 0;\n for (const run of baseRuns(el)) {\n const rStart = pos;\n const rEnd = pos + run.text.length;\n pos = rEnd;\n const s = Math.max(rStart, lo);\n const e = Math.min(rEnd, hi);\n if (s >= e) continue;\n out.push({\n text: run.text.slice(s - rStart, e - rStart),\n ...(run.style !== undefined ? { style: run.style } : {}),\n });\n }\n return out;\n};\n\n/** Shallow-merge `patch` over `base`, pruning keys explicitly set to undefined. */\nconst mergeStyle = (\n base: Partial<TextStyle> | undefined,\n patch: Partial<TextStyle>,\n): Partial<TextStyle> | undefined => {\n const merged: Record<string, unknown> = { ...(base ?? {}), ...patch };\n const entries = Object.entries(merged).filter(([, v]) => v !== undefined);\n if (entries.length === 0) return undefined;\n const cleaned: Partial<TextStyle> = Object.fromEntries(entries);\n return cleaned;\n};\n\n/**\n * Pure operation: apply a partial {@link TextStyle} overlay to the character\n * range `[from, to)` of a text element, returning a NEW element. The flat\n * `text` is never touched (invariant preserved). Existing runs are split at\n * the range boundaries and the patch is merged into the overlapping portion;\n * adjacent runs with equal styling are coalesced.\n *\n * When the result collapses to a single unstyled run spanning the whole text,\n * `runs` is dropped entirely so the element reverts to a plain text block\n * (keeps scenes minimal and round-trips cleanly). An empty range is a no-op.\n */\nexport const applyStyleToRange = (\n el: TextElement,\n from: number,\n to: number,\n patch: Partial<TextStyle>,\n): TextElement => {\n const lo = Math.max(0, Math.min(from, to));\n const hi = Math.min(el.text.length, Math.max(from, to));\n if (lo >= hi) return el;\n\n const out: TextRun[] = [];\n let pos = 0;\n for (const run of baseRuns(el)) {\n const rStart = pos;\n const rEnd = pos + run.text.length;\n pos = rEnd;\n const midStart = Math.max(rStart, lo);\n const midEnd = Math.min(rEnd, hi);\n if (midStart >= midEnd) {\n out.push(run);\n continue;\n }\n if (rStart < midStart) {\n out.push({\n text: run.text.slice(0, midStart - rStart),\n ...(run.style !== undefined ? { style: run.style } : {}),\n });\n }\n const merged = mergeStyle(run.style, patch);\n out.push({\n text: run.text.slice(midStart - rStart, midEnd - rStart),\n ...(merged !== undefined ? { style: merged } : {}),\n });\n if (midEnd < rEnd) {\n out.push({\n text: run.text.slice(midEnd - rStart),\n ...(run.style !== undefined ? { style: run.style } : {}),\n });\n }\n }\n\n const normalized = normalizeRuns(out);\n const only = normalized[0];\n if (\n normalized.length === 1 &&\n only !== undefined &&\n (only.style === undefined || Object.keys(only.style).length === 0)\n ) {\n // Reverted to a single uniform style \u2192 shed the overlay entirely.\n const { runs: _drop, ...rest } = el;\n void _drop;\n return rest;\n }\n return { ...el, runs: normalized };\n};\n", "import type { TextParagraph } from \"../shapes/shape.js\";\n\n/**\n * Paragraph-attribute helpers for text lists. A \"paragraph\" is a\n * `\\n`-separated block of a text element's flat `text`; attributes\n * (`TextParagraph`) are stored in an array aligned by paragraph index.\n * These helpers keep that array consistent as the text is edited and\n * answer range queries for the toolbar.\n */\n\n/** Number of paragraphs in `text` (always \u2265 1; empty text = one empty paragraph). */\nexport const paragraphCount = (text: string): number => {\n let n = 1;\n for (const ch of text) if (ch === \"\\n\") n++;\n return n;\n};\n\n/**\n * Paragraph index range `[first, last]` (inclusive) covered by the source\n * offset range `[from, to]`. Offsets outside the text are clamped.\n */\nexport const paragraphRangeForOffsets = (\n text: string,\n from: number,\n to: number,\n): { readonly first: number; readonly last: number } => {\n const lo = Math.max(0, Math.min(from, to));\n const hi = Math.min(text.length, Math.max(from, to));\n // Paragraph index = newlines before the offset; `hi` may equal `text.length`\n // (caret at the very end), so the count runs over `[0, hi)` only.\n let idx = 0;\n let first = 0;\n for (let i = 0; i < hi; i++) {\n if (i === lo) first = idx;\n if (text[i] === \"\\n\") idx++;\n }\n if (lo >= hi) first = idx;\n return { first, last: idx };\n};\n\n/** Attrs for a paragraph index (missing / short array \u2192 plain). */\nexport const paragraphAt = (\n paragraphs: readonly TextParagraph[] | undefined,\n index: number,\n): TextParagraph => paragraphs?.[index] ?? {};\n\nconst isPlain = (p: TextParagraph): boolean => p.list === undefined && (p.indent ?? 0) === 0;\n\n/**\n * Canonical form: trailing plain paragraphs are dropped; an all-plain\n * array collapses to `undefined` so plain text stays byte-identical on\n * the wire.\n */\nexport const normalizeParagraphs = (\n paragraphs: readonly TextParagraph[],\n): readonly TextParagraph[] | undefined => {\n let end = paragraphs.length;\n while (end > 0 && isPlain(paragraphs[end - 1] ?? {})) end--;\n if (end === 0) return undefined;\n return paragraphs.slice(0, end);\n};\n\n/**\n * Re-align the paragraph-attribute array after a text change. Paragraphs\n * are matched by the longest common prefix and suffix of the old / new\n * paragraph lists; the edited middle keeps the first edited paragraph's\n * attrs and lets inserted paragraphs inherit them \u2014 so pressing Enter\n * inside a list item continues the list, and deleting a line drops its\n * attrs with it. Pure and heuristic by design: it has no caret input, so\n * pathological multi-paragraph pastes may inherit conservatively (plain).\n */\nexport const remapParagraphsForTextChange = (\n oldText: string,\n newText: string,\n paragraphs: readonly TextParagraph[] | undefined,\n): readonly TextParagraph[] | undefined => {\n if (paragraphs === undefined || paragraphs.length === 0) return undefined;\n if (oldText === newText) return paragraphs;\n const oldParas = oldText.split(\"\\n\");\n const newParas = newText.split(\"\\n\");\n if (oldParas.length === newParas.length) return paragraphs; // in-line edit \u2014 indices stable\n\n // Longest common prefix / suffix of the paragraph LISTS (exact match).\n let prefix = 0;\n while (\n prefix < oldParas.length &&\n prefix < newParas.length &&\n oldParas[prefix] === newParas[prefix]\n ) {\n prefix++;\n }\n let suffix = 0;\n while (\n suffix < oldParas.length - prefix &&\n suffix < newParas.length - prefix &&\n oldParas[oldParas.length - 1 - suffix] === newParas[newParas.length - 1 - suffix]\n ) {\n suffix++;\n }\n\n const out: TextParagraph[] = [];\n for (let i = 0; i < prefix; i++) out.push(paragraphAt(paragraphs, i));\n // The edited middle: inherit the first edited old paragraph's attrs\n // (falls back to the last prefix paragraph when the middle was empty \u2014\n // a pure insertion continues whatever precedes it).\n const inheritFrom = Math.min(prefix, oldParas.length - 1);\n const inherited = paragraphAt(paragraphs, inheritFrom);\n const newMiddle = newParas.length - prefix - suffix;\n for (let i = 0; i < newMiddle; i++) out.push(inherited);\n for (let i = suffix; i > 0; i--) out.push(paragraphAt(paragraphs, oldParas.length - i));\n return normalizeParagraphs(out);\n};\n\n/**\n * Derived list markers, one per paragraph: `\"\u2022\"` for bullets, `\"N.\"` for\n * numbered items (consecutive numbered paragraphs at the SAME indent\n * count up; any other paragraph kind resets the counter), `null` for\n * plain paragraphs.\n */\nexport const listMarkers = (\n paragraphs: readonly TextParagraph[] | undefined,\n count: number,\n): readonly (string | null)[] => {\n const out: (string | null)[] = [];\n const counters = new Map<number, number>();\n for (let i = 0; i < count; i++) {\n const p = paragraphAt(paragraphs, i);\n const level = p.indent ?? 0;\n if (p.list === \"numbered\") {\n const n = (counters.get(level) ?? 0) + 1;\n counters.set(level, n);\n // A deeper-or-equal reset boundary: nested lists restart when the\n // chain is interrupted at their own level (handled below).\n for (const key of [...counters.keys()]) if (key > level) counters.delete(key);\n out.push(`${String(n)}.`);\n } else {\n if (p.list === undefined) counters.clear();\n else for (const key of [...counters.keys()]) if (key >= level) counters.delete(key);\n out.push(p.list === \"bullet\" ? \"\u2022\" : null);\n }\n }\n return out;\n};\n", "/**\n * Return `v` when defined, otherwise throw. For narrowing values the caller\n * knows are present (in-range array access, resolved lookups) without scattering\n * non-null assertions.\n */\nexport const req = <T>(v: T | undefined): T => {\n if (v === undefined) throw new Error(\"required value is undefined\");\n return v;\n};\n", "import type { Vec2 } from \"@oh-just-another/types\";\n\nexport const ZERO: Vec2 = Object.freeze({ x: 0, y: 0 });\n\nexport const of = (x: number, y: number): Vec2 => ({ x, y });\n\nexport const add = (a: Vec2, b: Vec2): Vec2 => ({ x: a.x + b.x, y: a.y + b.y });\n\nexport const sub = (a: Vec2, b: Vec2): Vec2 => ({ x: a.x - b.x, y: a.y - b.y });\n\nexport const mul = (a: Vec2, scalar: number): Vec2 => ({ x: a.x * scalar, y: a.y * scalar });\n\nexport const div = (a: Vec2, scalar: number): Vec2 => ({ x: a.x / scalar, y: a.y / scalar });\n\nexport const negate = (a: Vec2): Vec2 => ({ x: -a.x, y: -a.y });\n\nexport const dot = (a: Vec2, b: Vec2): number => a.x * b.x + a.y * b.y;\n\n/** 2D pseudo-cross (z component of the 3D cross product). */\nexport const cross = (a: Vec2, b: Vec2): number => a.x * b.y - a.y * b.x;\n\nexport const lengthSq = (a: Vec2): number => a.x * a.x + a.y * a.y;\n\nexport const length = (a: Vec2): number => Math.sqrt(lengthSq(a));\n\nexport const distanceSq = (a: Vec2, b: Vec2): number => {\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n return dx * dx + dy * dy;\n};\n\nexport const distance = (a: Vec2, b: Vec2): number => Math.sqrt(distanceSq(a, b));\n\n/** Returns ZERO when input is the zero vector. */\nexport const normalize = (a: Vec2): Vec2 => {\n const len = length(a);\n if (len === 0) return ZERO;\n return { x: a.x / len, y: a.y / len };\n};\n\nexport const lerp = (a: Vec2, b: Vec2, t: number): Vec2 => ({\n x: a.x + (b.x - a.x) * t,\n y: a.y + (b.y - a.y) * t,\n});\n\n/** Midpoint of two points. */\nexport const midpoint = (a: Vec2, b: Vec2): Vec2 => ({\n x: (a.x + b.x) / 2,\n y: (a.y + b.y) / 2,\n});\n\n/** Angle of the vector from the positive x-axis, in radians (-\u03C0, \u03C0]. */\nexport const angle = (a: Vec2): number => Math.atan2(a.y, a.x);\n\n/** Rotate counterclockwise by `radians` around the origin. */\nexport const rotate = (a: Vec2, radians: number): Vec2 => {\n const c = Math.cos(radians);\n const s = Math.sin(radians);\n return { x: a.x * c - a.y * s, y: a.x * s + a.y * c };\n};\n\n/** Rotate `a` counterclockwise by `radians` around `pivot`. */\nexport const rotateAround = (a: Vec2, pivot: Vec2, radians: number): Vec2 => {\n const c = Math.cos(radians);\n const s = Math.sin(radians);\n const dx = a.x - pivot.x;\n const dy = a.y - pivot.y;\n return { x: pivot.x + (dx * c - dy * s), y: pivot.y + (dx * s + dy * c) };\n};\n\n/** Counterclockwise 90\u00B0 perpendicular. */\nexport const perp = (a: Vec2): Vec2 => ({ x: -a.y, y: a.x });\n\nexport const equals = (a: Vec2, b: Vec2, epsilon = 0): boolean => {\n if (epsilon === 0) return a.x === b.x && a.y === b.y;\n return Math.abs(a.x - b.x) <= epsilon && Math.abs(a.y - b.y) <= epsilon;\n};\n", "import type { Bounds, Transform, Vec2 } from \"@oh-just-another/types\";\n\nexport const IDENTITY: Transform = Object.freeze({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 });\n\nexport const of = (\n a: number,\n b: number,\n c: number,\n d: number,\n e: number,\n f: number,\n): Transform => ({ a, b, c, d, e, f });\n\nexport const translation = (tx: number, ty: number): Transform => ({\n a: 1,\n b: 0,\n c: 0,\n d: 1,\n e: tx,\n f: ty,\n});\n\nexport const scaling = (sx: number, sy: number = sx): Transform => ({\n a: sx,\n b: 0,\n c: 0,\n d: sy,\n e: 0,\n f: 0,\n});\n\nexport const rotation = (radians: number): Transform => {\n const cos = Math.cos(radians);\n const sin = Math.sin(radians);\n return { a: cos, b: sin, c: -sin, d: cos, e: 0, f: 0 };\n};\n\n/**\n * Matrix product `a \u00D7 b`. Composition is right-to-left: applying the result\n * to a point is equivalent to applying `b` first, then `a`.\n */\nexport const multiply = (a: Transform, b: Transform): Transform => ({\n a: a.a * b.a + a.c * b.b,\n b: a.b * b.a + a.d * b.b,\n c: a.a * b.c + a.c * b.d,\n d: a.b * b.c + a.d * b.d,\n e: a.a * b.e + a.c * b.f + a.e,\n f: a.b * b.e + a.d * b.f + a.f,\n});\n\nexport const inverse = (t: Transform): Transform => {\n const det = t.a * t.d - t.b * t.c;\n if (det === 0) throw new Error(\"Cannot invert singular matrix\");\n return {\n a: t.d / det,\n b: -t.b / det,\n c: -t.c / det,\n d: t.a / det,\n e: (t.c * t.f - t.d * t.e) / det,\n f: (t.b * t.e - t.a * t.f) / det,\n };\n};\n\nexport const applyToPoint = (t: Transform, p: Vec2): Vec2 => ({\n x: t.a * p.x + t.c * p.y + t.e,\n y: t.b * p.x + t.d * p.y + t.f,\n});\n\n/**\n * Axis-aligned bounding box of `b` after applying `t`. The result is the AABB\n * of the four transformed corners \u2014 tighter approaches exist for pure rotations\n * but this is correct for any affine transform.\n */\nexport const applyToBounds = (t: Transform, b: Bounds): Bounds => {\n const p1 = applyToPoint(t, { x: b.x, y: b.y });\n const p2 = applyToPoint(t, { x: b.x + b.width, y: b.y });\n const p3 = applyToPoint(t, { x: b.x, y: b.y + b.height });\n const p4 = applyToPoint(t, { x: b.x + b.width, y: b.y + b.height });\n const minX = Math.min(p1.x, p2.x, p3.x, p4.x);\n const minY = Math.min(p1.y, p2.y, p3.y, p4.y);\n const maxX = Math.max(p1.x, p2.x, p3.x, p4.x);\n const maxY = Math.max(p1.y, p2.y, p3.y, p4.y);\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n};\n\nexport interface DecomposedTransform {\n readonly translation: Vec2;\n /** Radians, range (-\u03C0, \u03C0]. */\n readonly rotation: number;\n readonly scale: Vec2;\n}\n\n/**\n * Extracts translate / rotate / scale (TRS) from a transform. Assumes the matrix\n * encodes only translate/rotate/uniform-or-axis-aligned-scale (no skew). For matrices\n * with skew the decomposition is approximate.\n */\nexport const decompose = (t: Transform): DecomposedTransform => {\n const sx = Math.sqrt(t.a * t.a + t.b * t.b);\n const sy = Math.sqrt(t.c * t.c + t.d * t.d);\n const det = t.a * t.d - t.b * t.c;\n const sySigned = det < 0 ? -sy : sy;\n return {\n translation: { x: t.e, y: t.f },\n rotation: Math.atan2(t.b, t.a),\n scale: { x: sx, y: sySigned },\n };\n};\n\nexport const equals = (a: Transform, b: Transform, epsilon = 0): boolean => {\n const fields: readonly (keyof Transform)[] = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"];\n if (epsilon === 0) return fields.every((k) => a[k] === b[k]);\n return fields.every((k) => Math.abs(a[k] - b[k]) <= epsilon);\n};\n", "import type { Bounds, Vec2 } from \"@oh-just-another/types\";\n\nexport const EMPTY: Bounds = Object.freeze({ x: 0, y: 0, width: 0, height: 0 });\n\nexport const of = (x: number, y: number, width: number, height: number): Bounds => ({\n x,\n y,\n width,\n height,\n});\n\nexport const fromPoints = (points: readonly Vec2[]): Bounds => {\n if (points.length === 0) return EMPTY;\n let minX = Infinity;\n let minY = Infinity;\n let maxXv = -Infinity;\n let maxYv = -Infinity;\n for (const p of points) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxXv) maxXv = p.x;\n if (p.y > maxYv) maxYv = p.y;\n }\n return { x: minX, y: minY, width: maxXv - minX, height: maxYv - minY };\n};\n\nexport const fromCenter = (center: Vec2, width: number, height: number): Bounds => ({\n x: center.x - width / 2,\n y: center.y - height / 2,\n width,\n height,\n});\n\nexport const centerOf = (b: Bounds): Vec2 => ({\n x: b.x + b.width / 2,\n y: b.y + b.height / 2,\n});\n\nexport const maxX = (b: Bounds): number => b.x + b.width;\nexport const maxY = (b: Bounds): number => b.y + b.height;\n\n/** True if width or height is non-positive. */\nexport const isEmpty = (b: Bounds): boolean => b.width <= 0 || b.height <= 0;\n\nexport const union = (a: Bounds, b: Bounds): Bounds => {\n if (isEmpty(a)) return b;\n if (isEmpty(b)) return a;\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n const xMax = Math.max(maxX(a), maxX(b));\n const yMax = Math.max(maxY(a), maxY(b));\n return { x, y, width: xMax - x, height: yMax - y };\n};\n\n/** Returns null if the intersection is empty. */\nexport const intersection = (a: Bounds, b: Bounds): Bounds | null => {\n const x = Math.max(a.x, b.x);\n const y = Math.max(a.y, b.y);\n const xMax = Math.min(maxX(a), maxX(b));\n const yMax = Math.min(maxY(a), maxY(b));\n if (xMax <= x || yMax <= y) return null;\n return { x, y, width: xMax - x, height: yMax - y };\n};\n\nexport const intersects = (a: Bounds, b: Bounds): boolean => intersection(a, b) !== null;\n\nexport const contains = (b: Bounds, point: Vec2): boolean =>\n point.x >= b.x && point.x <= maxX(b) && point.y >= b.y && point.y <= maxY(b);\n\nexport const containsBounds = (outer: Bounds, inner: Bounds): boolean =>\n inner.x >= outer.x &&\n inner.y >= outer.y &&\n maxX(inner) <= maxX(outer) &&\n maxY(inner) <= maxY(outer);\n\nexport const expand = (b: Bounds, padding: number): Bounds => ({\n x: b.x - padding,\n y: b.y - padding,\n width: b.width + 2 * padding,\n height: b.height + 2 * padding,\n});\n\n/** Flips negative width/height so that x/y is the top-left corner. */\nexport const normalize = (b: Bounds): Bounds => ({\n x: b.width < 0 ? b.x + b.width : b.x,\n y: b.height < 0 ? b.y + b.height : b.y,\n width: Math.abs(b.width),\n height: Math.abs(b.height),\n});\n\nexport const equals = (a: Bounds, b: Bounds, epsilon = 0): boolean => {\n if (epsilon === 0) {\n return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;\n }\n return (\n Math.abs(a.x - b.x) <= epsilon &&\n Math.abs(a.y - b.y) <= epsilon &&\n Math.abs(a.width - b.width) <= epsilon &&\n Math.abs(a.height - b.height) <= epsilon\n );\n};\n", "import { req, type Vec2 } from \"@oh-just-another/types\";\n\n/**\n * Offset a closed polygon's vertices along the bisector at each corner.\n * Positive `distance` moves vertices inward (toward the centroid), negative\n * moves outward; both winding orders are handled by projecting the bisector\n * onto the toward-centroid vector and flipping the sign as needed.\n *\n * Each vertex moves `distance / cos(angle/2)` along the bisector of its two\n * adjacent edges (miter offset of the polygon's stroked outline).\n *\n * The bisector clamps at very sharp angles (cos < 1e-6) to avoid pixel-spike\n * artefacts. Concave polygons whose centroid lies outside the polygon can flip\n * inward/outward sign on isolated vertices. Polygons with fewer than 3 vertices\n * are returned unchanged.\n */\nexport const offsetClosedPath = (points: readonly Vec2[], distance: number): Vec2[] => {\n if (points.length < 3 || distance === 0) return points.map((p) => ({ x: p.x, y: p.y }));\n\n // Centroid as an interior reference, used to disambiguate inward / outward\n // direction regardless of vertex winding order.\n let cx = 0;\n let cy = 0;\n for (const p of points) {\n cx += p.x;\n cy += p.y;\n }\n cx /= points.length;\n cy /= points.length;\n\n const n = points.length;\n // Edge unit normals \u2014 rotate each edge vector 90\u00B0.\n const nx = new Array<number>(n);\n const ny = new Array<number>(n);\n for (let i = 0; i < n; i++) {\n const a = req(points[i]);\n const b = req(points[(i + 1) % n]);\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len = Math.hypot(dx, dy) || 1;\n nx[i] = -dy / len;\n ny[i] = dx / len;\n }\n\n const out: Vec2[] = [];\n for (let i = 0; i < n; i++) {\n const prev = (i - 1 + n) % n;\n const n1x = req(nx[prev]);\n const n1y = req(ny[prev]);\n const n2x = req(nx[i]);\n const n2y = req(ny[i]);\n let bx = n1x + n2x;\n let by = n1y + n2y;\n const blen = Math.hypot(bx, by);\n if (blen < 1e-6) {\n // 180\u00B0 turn \u2014 bisector ill-defined. Use one of the normals.\n bx = n1x;\n by = n1y;\n } else {\n bx /= blen;\n by /= blen;\n }\n // Inward = toward centroid; check the bisector's component along\n // (centroid - vertex).\n const vertex = req(points[i]);\n const towardCx = cx - vertex.x;\n const towardCy = cy - vertex.y;\n const dot = bx * towardCx + by * towardCy;\n const sign = dot >= 0 ? 1 : -1;\n const cos = bx * n1x + by * n1y;\n const miterLen = cos > 1e-6 ? distance / cos : distance;\n out.push({\n x: vertex.x + sign * bx * miterLen,\n y: vertex.y + sign * by * miterLen,\n });\n }\n return out;\n};\n\n/**\n * Twice the signed polygon area via the shoelace formula. Positive =\n * counter-clockwise in y-up coordinates / clockwise in y-down. Use the sign\n * to detect winding order; abs/2 = polygon area.\n */\nexport const signedArea = (points: readonly Vec2[]): number => {\n let s = 0;\n for (let i = 0; i < points.length; i++) {\n const a = req(points[i]);\n const b = req(points[(i + 1) % points.length]);\n s += a.x * b.y - b.x * a.y;\n }\n return s / 2;\n};\n", "/**\n * Optional host-provided text measurer. The text bounder\n * (`getElementLocalBounds` for `TextElement`) is otherwise purely\n * geometric (`chars \u00D7 fontSize \u00D7 factor`), which can diverge a lot\n * from the actually-rendered width \u2014 the WebGL2 MSDF path draws with a\n * WASM-baked font whose glyph advances differ from any geometric\n * estimate, so the selection box would not hug the text.\n *\n * A host (the interaction layer) injects a measurer backed by the\n * renderer's `measureText` (which itself matches the active text\n * backend). When set, the bounder uses it for accurate width; when\n * absent (headless / tests), it falls back to the geometric estimate.\n *\n * Returns the measured width in world px, or `null` to defer to the\n * geometric estimate for that call. `opts` carries weight/style so the\n * measurer matches the *rendered* width \u2014 bold/italic change glyph\n * advances, so without it the bounds lag behind a bolded text and the\n * glyphs overflow the element box.\n */\nexport interface TextMeasureOpts {\n readonly bold?: boolean;\n readonly italic?: boolean;\n}\n\nexport type TextMeasurer = (\n text: string,\n fontFamily: string,\n fontSize: number,\n opts?: TextMeasureOpts,\n) => number | null;\n\nlet activeMeasurer: TextMeasurer | null = null;\n\n/** Install (or clear, with `null`) the active text measurer. */\nexport const setTextMeasurer = (measurer: TextMeasurer | null): void => {\n activeMeasurer = measurer;\n};\n\n/** The active text measurer, or `null` when none is installed. */\nexport const getTextMeasurer = (): TextMeasurer | null => activeMeasurer;\n", "import type { Bounds, FileId, LayerId, ElementId, Vec2 } from \"@oh-just-another/types\";\nimport type { FractionalIndex } from \"fractional-keys\";\nimport { bounds as B } from \"@oh-just-another/math\";\nimport type { AnchorRef } from \"../edges/edge.js\";\nimport type { Style, TextStyle } from \"../text/style.js\";\nimport type { TextRun } from \"../text/text-runs.js\";\nimport { TEXT_APPROX_CHAR_WIDTH_FACTOR, TEXT_LINE_HEIGHT_FACTOR } from \"../constants.js\";\nimport { getTextMeasurer } from \"../text/text-measure.js\";\nimport { pickTextPlaceholder } from \"../text/placeholder.js\";\n\n/**\n * Fields shared by every shape variant. `order` is a fractional-index string\n * used for z-ordering within the parent layer \u2014 insertions are O(1) and never\n * require renumbering neighbors, which keeps history small and is conflict-free\n * under concurrent edits.\n */\nexport interface ElementBase {\n readonly id: ElementId;\n readonly layerId: LayerId;\n /** Discriminator. Built-in shapes use the literal types declared below. */\n readonly type: string;\n /** Local-space origin. The shape is rotated/scaled around this point. */\n readonly position: Vec2;\n /** Rotation in radians, counter-clockwise. */\n readonly rotation: number;\n readonly scale: Vec2;\n /** Z-order key within `layerId`. */\n readonly order: FractionalIndex;\n readonly style: Style;\n /** Free-form metadata for plugins; the kernel never reads from here. */\n readonly metadata?: Readonly<Record<string, unknown>>;\n\n /**\n * Interactive-resize size constraints in local pixels. The editor clamps\n * the shape's width/height into [min, max] after every resize gesture.\n * Omitted = no constraint on that axis.\n */\n readonly minWidth?: number;\n readonly minHeight?: number;\n readonly maxWidth?: number;\n readonly maxHeight?: number;\n\n /**\n * If true, interactive resize is prevented from dragging through the\n * opposite edge \u2014 the shape cannot be mirrored by overshooting a handle.\n * `width` / `height` are clamped to a non-negative range (or `minWidth` /\n * `minHeight` if set). Defaults to `false`.\n */\n readonly noFlip?: boolean;\n\n /**\n * Custom named connection points on this shape, on top of the 9 standard\n * anchors (`top-left` / `top` / `top-right` / `right` / `bottom-right` /\n * `bottom` / `bottom-left` / `left` / `center`). Entries with a standard\n * name override the standard placement; new names add fresh ports.\n *\n * Values are `AnchorRef`s \u2014 `ratio` keeps the point proportional to the\n * shape's bounds, `absolute` pins it at a fixed pixel offset. Resolve\n * an anchor through `getAnchorLocal` / `getAnchorWorld`.\n */\n readonly anchors?: Readonly<Record<string, AnchorRef>>;\n\n /**\n * Optional parent shape id. When set, the shape is considered part of\n * the parent's group: hit-test and drag operations promote selection\n * to the parent (grouped), and `moveSelectionBy` translates every\n * descendant in lockstep. The kernel does not enforce a particular\n * shape type for parents \u2014 `GroupElement` (type `\"group\"`) is just the\n * default zero-render container; custom shape types can also act as\n * parents.\n */\n readonly parentId?: ElementId;\n\n /**\n * Frame membership \u2014 modern-style. Distinct from `parentId`\n * (which is for groups and containers). Children of a frame are\n * NOT nested in its `children` list; they're flat in the scene\n * but share `frameId === frame.id`. Move-by-drag of the frame\n * translates every shape with the matching frameId; export-by-\n * frame uses the frame's bounds as the crop region.\n */\n readonly frameId?: ElementId;\n\n /**\n * Per-shape lock flag. Locked shapes ignore all interactive gestures\n * (hit-test pretends they're not there for clicks / drags / resize),\n * but still render and remain serialisable. Propagates to\n * descendants: if any ancestor in the `parentId` chain is locked,\n * the shape is effectively locked. Use `isElementLocked(scene, shape)`\n * to consult the propagated state.\n *\n * Independent from `Layer.locked` \u2014 both gate interactions; either\n * one being true is enough to lock.\n */\n readonly locked?: boolean;\n\n /**\n * Per-shape visibility flag. Hidden shapes do not render and do not\n * receive interactions. Propagates to descendants like `locked`.\n * Use `isElementHidden(scene, shape)` to consult the propagated state.\n *\n * Independent from `Layer.visible` \u2014 either being false hides the\n * shape.\n */\n readonly hidden?: boolean;\n\n /**\n * Embedded text label \u2014 the shape's own text content, drawn inside its\n * bounds (wrapped to the width, aligned via `style.textAlign` /\n * `style.textBaseline`, `middle`+`center` by default). Shares the text\n * element's building blocks (styled runs, list paragraphs) as data;\n * layout is the renderer's job. Double-click opens the inline editor\n * on shapes that support it (see `canCarryLabel`).\n */\n readonly label?: ShapeLabel;\n\n /**\n * Element-level hyperlink. Any shape \u2014 text, image,\n * rectangle \u2014 can carry one. The host opens it on Cmd/Ctrl-click or via\n * the hover link-popup. Stored verbatim; the host MUST validate the\n * scheme before navigating (only `http`/`https`/`mailto` \u2014 never\n * `javascript:`). Per-fragment links inside text are a separate\n * rich-text feature.\n */\n readonly href?: string;\n}\n\nexport interface RectangleElement extends ElementBase {\n readonly type: \"rectangle\";\n readonly width: number;\n readonly height: number;\n}\n\nexport interface EllipseElement extends ElementBase {\n readonly type: \"ellipse\";\n readonly width: number;\n readonly height: number;\n}\n\nexport interface PolygonElement extends ElementBase {\n readonly type: \"polygon\";\n /** Closed polygon in local coordinates (origin = `position`). */\n readonly points: readonly Vec2[];\n}\n\nexport type PathCommand =\n | { readonly kind: \"M\"; readonly to: Vec2 }\n | { readonly kind: \"L\"; readonly to: Vec2 }\n | { readonly kind: \"Q\"; readonly control: Vec2; readonly to: Vec2 }\n | { readonly kind: \"C\"; readonly control1: Vec2; readonly control2: Vec2; readonly to: Vec2 }\n | { readonly kind: \"Z\" };\n\nexport interface PathElement extends ElementBase {\n readonly type: \"path\";\n /** Commands in local coordinates. */\n readonly commands: readonly PathCommand[];\n}\n\nexport interface TextElement extends ElementBase {\n readonly type: \"text\";\n readonly text: string;\n readonly fontFamily: string;\n readonly fontSize: number;\n /** Width budget for wrapping; `undefined` = single line. */\n readonly maxWidth?: number;\n readonly style: TextStyle;\n /**\n * Optional styled-run overlay for rich text. Each run styles a contiguous\n * substring; `runs.map(r => r.text).join(\"\")` MUST equal `text`, which\n * stays the flat source of truth. Omitted (or empty) = uniform styling\n * (renders exactly like a plain text block). See {@link TextRun}.\n */\n readonly runs?: readonly TextRun[];\n /**\n * Optional per-paragraph attributes (lists / nesting), aligned by index\n * with `text.split(\"\\n\")`. A shorter array leaves the trailing\n * paragraphs plain; omitted = every paragraph plain. Numbering for\n * `\"numbered\"` items is derived at render time (consecutive numbered\n * paragraphs at the same indent count up), never stored.\n */\n readonly paragraphs?: readonly TextParagraph[];\n}\n\n/**\n * Embedded text carried by a non-text shape (see `ElementBase.label`).\n * Field-for-field compatible with the text element's content model so\n * the text pipeline (runs, paragraphs, layout, inline editing) applies\n * unchanged.\n */\nexport interface ShapeLabel {\n readonly text: string;\n readonly fontFamily: string;\n readonly fontSize: number;\n /**\n * Auto-fit mode (sticky notes): the RENDERED font size is derived so\n * the text fills the shape body, scaling with the shape; `fontSize`\n * then only serves as the fallback / upper hint. Picking an explicit\n * size in the toolbar clears the flag.\n */\n readonly autoFit?: boolean;\n /** Optional style overlay; `textAlign`/`textBaseline` default to center/middle. */\n readonly style?: TextStyle;\n readonly runs?: readonly TextRun[];\n readonly paragraphs?: readonly TextParagraph[];\n}\n\n/**\n * Sticky note \u2014 a bounded card whose text lives in the shared embedded\n * `label` (double-click to edit). Background comes from `style.fill`;\n * `authorName` renders along the bottom edge when `showAuthor` is on.\n * Registered as a plugin-style type: not part of the built-in `Element`\n * union, handled through the renderer / bounder registries and the\n * custom-element wire schema.\n */\nexport interface StickyElement extends ElementBase {\n readonly type: \"sticky\";\n readonly width: number;\n readonly height: number;\n readonly authorName?: string;\n readonly showAuthor?: boolean;\n /** Free-form tags, rendered as small pills along the bottom edge. */\n readonly tags?: readonly string[];\n /**\n * Emoji reactions. Each glyph tracks WHO reacted (`users` \u2014 collab\n * user ids); the visible counter is `users.length`. A user's click on\n * a glyph they already reacted with removes their reaction (toggle),\n * anyone else's click adds theirs. The add button lives in the host\n * UI at the sticky's bottom-left corner.\n */\n readonly reactions?: readonly { readonly glyph: string; readonly users: readonly string[] }[];\n}\n\n/** True when the shape is a sticky note. */\nexport const isSticky = (shape: ElementBase): shape is StickyElement => shape.type === \"sticky\";\n\n/**\n * Emoji element \u2014 a single glyph drawn at `size` world units. The glyph\n * is replaced via the toolbar picker. Plugin-style type like `sticky`.\n */\nexport interface EmojiElement extends ElementBase {\n readonly type: \"emoji\";\n readonly glyph: string;\n readonly size: number;\n}\n\n/** True when the shape is an emoji element. */\nexport const isEmoji = (shape: ElementBase): shape is EmojiElement => shape.type === \"emoji\";\n\n/** Shape types whose body can host an embedded label. */\nconst LABELABLE_TYPES: ReadonlySet<string> = new Set([\n \"rectangle\",\n \"ellipse\",\n \"polygon\",\n \"block-arrow\",\n \"sticky\",\n]);\n\n/** True when the shape's type supports an embedded text label. */\nexport const canCarryLabel = (shape: ElementBase): boolean => LABELABLE_TYPES.has(shape.type);\n\n/**\n * Paragraph-level attributes for a {@link TextElement}. Both fields are\n * optional so plain paragraphs serialize as `{}` (or are omitted entirely\n * via a short array).\n */\nexport interface TextParagraph {\n /** List marker kind; omitted = plain paragraph. */\n readonly list?: \"bullet\" | \"numbered\";\n /** 0-based nesting level. Omitted = 0. */\n readonly indent?: number;\n}\n\n/**\n * Normalised crop rectangle for an {@link ImageElement}. All four\n * values are fractions in `[0, 1]` of the source image's intrinsic\n * dimensions: `{ x: 0, y: 0, width: 1, height: 1 }` shows the whole\n * image (equivalent to omitting `crop`). The cropped source region is\n * stretched to fill the element's `width` \u00D7 `height` box, so cropping\n * does not change the element's on-canvas footprint \u2014 only which part\n * of the bitmap is visible. Being normalised keeps the crop stable when\n * the backing file is swapped for a differently-sized copy.\n */\nexport interface ImageCrop {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Shape mask applied to an image element's BOX (after crop): pixels\n * outside the mask are clipped away by the renderer (`RenderTarget.clip`\n * \u2014 canvas2d clip / svg clipPath / webgl2 stencil). Coordinates are\n * normalised to the element box (0..1 on both axes), so the mask scales\n * with the shape. Additive: scenes and renderers that predate masks\n * ignore it and draw the full box.\n *\n * - `ellipse` \u2014 inscribed ellipse (circle on a square box).\n * - `round-rect` \u2014 rounded rectangle; `radius` is a fraction of the\n * SHORTER box side (0..0.5; 0.5 = capsule).\n * - `polygon` \u2014 arbitrary closed ring of normalised points (\u2265 3). The\n * built-in presets live in {@link IMAGE_MASK_POLYGON_PRESETS}.\n */\nexport type ImageMask =\n | { readonly kind: \"ellipse\" }\n | { readonly kind: \"round-rect\"; readonly radius: number }\n | { readonly kind: \"polygon\"; readonly points: readonly Vec2[] };\n\nexport interface ImageElement extends ElementBase {\n readonly type: \"image\";\n /**\n * URL or data-URI. Used for remote-host / SVG images that don't need\n * binary registration. Setting `fileId` instead points at a\n * `Scene.files` entry, which keeps scene.json small for large bitmaps.\n */\n readonly src: string;\n /**\n * Optional normalised source-crop rectangle. Omitted = whole image.\n * See {@link ImageCrop}. Additive: scenes and renderers that predate\n * cropping simply ignore it and draw the full bitmap.\n */\n readonly crop?: ImageCrop;\n /**\n * Optional shape mask clipping the drawn box. Omitted = no mask.\n * Independent of (and applied after) `crop`. See {@link ImageMask}.\n */\n readonly mask?: ImageMask;\n /**\n * Id of the `BinaryFile` in `Scene.files` that backs this image.\n * When present, hosts should resolve through the file registry\n * (creates an object-URL or ImageBitmap on demand); `src` stays\n * around as a fallback for the static renderer path.\n */\n readonly fileId?: FileId;\n readonly width: number;\n readonly height: number;\n /**\n * Accessible description of the image content. Surfaced as `<title>`\n * in SVG output and available to hosts for `aria` wiring. Omitted =\n * decorative / undescribed.\n */\n readonly alt?: string;\n /**\n * Animated-content hint (opt-in). When set, the\n * renderer's image path consults `getAnimationAdapter(kind)`\n * to fetch the current frame's image source instead of using\n * `src` directly. Hosts that don't register an adapter for the\n * kind get a static fallback (src as-is). The actual frame data\n * lives in `animationData` \u2014 opaque to the kernel, decoded by\n * the adapter.\n *\n * Built-in kinds: \"gif\" (host plugs `gifuct-js`), \"lottie\"\n * (host plugs `lottie-web`), \"video\" (host plugs an\n * `HTMLVideoElement`). No adapters ship in the kernel \u2014\n * registration is per-host.\n */\n readonly animationKind?: string;\n readonly animationData?: unknown;\n}\n\n/**\n * Composite shape backed by a rich template (`@oh-just-another/templates`). The\n * scene stores only the binding (`templateId` + `data`) plus a fixed box\n * size \u2014 layout, hit-test and rendering live in the templates package.\n *\n * The kernel ships a basic bounder (uses `width` \u00D7 `height`); the templates\n * package can re-register a tighter bounder that respects the layout engine.\n */\nexport interface TemplateElement extends ElementBase {\n readonly type: \"template\";\n readonly templateId: string;\n readonly data: Readonly<Record<string, unknown>>;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Variable-width brush stroke. Each `BrushPoint` carries its own width\n * (typically derived from `PointerEvent.pressure \u00D7 MAX_BRUSH_WIDTH`).\n * The renderer interpolates between consecutive widths along the path.\n *\n * Coordinates are local; the shape's `position` / `rotation` / `scale`\n * apply on top, same as any other shape variant.\n */\nexport interface BrushPoint {\n readonly x: number;\n readonly y: number;\n /** Stroke half-width in local pixels at this vertex. */\n readonly width: number;\n}\n\nexport interface BrushElement extends ElementBase {\n readonly type: \"brush\";\n readonly points: readonly BrushPoint[];\n /**\n * A closed stroke: its ends meet, so the renderer fills the area enclosed by\n * the centreline with `style.fill` (under the variable-width stroke body).\n * Set on commit only when a fill colour is chosen and the stroke loops back\n * on itself. Omitted (undefined) for ordinary open strokes.\n */\n readonly closed?: boolean;\n /**\n * Raw input pressure (0\u20131) per point, aligned 1:1 with `points`. The baked\n * `width` is derived from it (base width \u00D7 pressure curve \u00D7 taper), so a\n * stroke can be regenerated with a different base width / thinning without\n * re-capturing. Omitted on strokes committed before pressures were stored.\n */\n readonly pressures?: readonly number[];\n /**\n * True when `pressures` were synthesised from pointer speed (mouse / touch\n * without a real pressure channel) rather than read from the device. Lets a\n * regeneration pass know whether re-simulating is appropriate.\n */\n readonly simulatePressure?: boolean;\n /**\n * The brush base half-width (local px) the stroke was committed with \u2014 the\n * value the pressure curve scaled toward. Omitted on legacy strokes.\n */\n readonly baseWidth?: number;\n}\n\n/**\n * Container shape that holds children via the shared `parentId` link.\n * Rendered as a no-op (the group itself has no visual); the editor's\n * overlay highlights the union AABB of the children when selected.\n */\nexport interface GroupElement extends ElementBase {\n readonly type: \"group\";\n}\n\n/**\n * Frame element \u2014 modern-style visual container that groups\n * shapes via a separate `frameId` link (NOT `parentId`). Drawn as\n * a dashed rectangle with a header title; clicks pass through to\n * children. Move-by-drag translates every shape whose `frameId`\n * matches; the export pipeline can crop to the frame's bounds.\n *\n * Auto-numbering: the editor picks the next free \"Frame N\" on\n * create. Custom `name` overrides.\n */\nexport interface FrameElement extends ElementBase {\n readonly type: \"frame\";\n readonly width: number;\n readonly height: number;\n /** Visible header label. */\n readonly name?: string;\n}\n\n/**\n * Filled arrow drawn as a single shape (body rectangle + triangular\n * head, optionally with a triangular tail). Distinct from an Link:\n * edges connect anchors and re-route on shape move; a BlockArrowElement\n * is a free-standing element with a fixed silhouette like a block-arrow icon.\n */\nexport interface BlockArrowElement extends ElementBase {\n readonly type: \"block-arrow\";\n readonly width: number;\n readonly height: number;\n /**\n * Where the arrow points. Default `\"right\"`. Rotation is still\n * applied on top via `ElementBase.rotation` \u2014 this enum just picks\n * the head side in local coords so the user can quickly toggle\n * direction without typing a 90/180/270 deg angle.\n */\n readonly direction?: \"right\" | \"left\" | \"up\" | \"down\";\n /** Ratio of the head length over the total length (0..0.9). Default 0.4. */\n readonly headRatio?: number;\n /** Ratio of the body thickness over the perpendicular dimension. Default 0.5. */\n readonly bodyThickness?: number;\n}\n\nexport type BuiltinElement =\n | RectangleElement\n | EllipseElement\n | PolygonElement\n | PathElement\n | TextElement\n | ImageElement\n | TemplateElement\n | GroupElement\n | FrameElement\n | BlockArrowElement\n | BrushElement;\n\n/**\n * Open shape type. `Element` accepts any `ElementBase` extension, which lets plugins\n * register their own types without amending this union. The kernel treats\n * unknown shape types via the bounder registry \u2014 see `registerBounder`.\n */\nexport type Element = BuiltinElement | ElementBase;\n\n// --- type guards ---\n\nexport const isRectangle = (s: ElementBase): s is RectangleElement => s.type === \"rectangle\";\nexport const isEllipse = (s: ElementBase): s is EllipseElement => s.type === \"ellipse\";\nexport const isPolygon = (s: ElementBase): s is PolygonElement => s.type === \"polygon\";\nexport const isPath = (s: ElementBase): s is PathElement => s.type === \"path\";\nexport const isText = (s: ElementBase): s is TextElement => s.type === \"text\";\nexport const isImage = (s: ElementBase): s is ImageElement => s.type === \"image\";\nexport const isTemplate = (s: ElementBase): s is TemplateElement => s.type === \"template\";\nexport const isGroup = (s: ElementBase): s is GroupElement => s.type === \"group\";\nexport const isFrame = (s: ElementBase): s is FrameElement => s.type === \"frame\";\nexport const isBlockArrow = (s: ElementBase): s is BlockArrowElement => s.type === \"block-arrow\";\nexport const isBrush = (s: ElementBase): s is BrushElement => s.type === \"brush\";\n\n/**\n * The colour the variable-width brush BODY is painted with: the line colour\n * (`style.stroke`, set by the drawing panel), falling back to `style.fill` for\n * strokes authored before the stroke/fill split (their line lived in `fill`),\n * then to opaque black. Shared by the committed-stroke renderer and the live\n * overlay preview so the two never diverge.\n */\nexport const brushBodyColor = (style: Style): string => style.stroke ?? style.fill ?? \"#000\";\n\n// --- bounder registry ---\n\n/**\n * Computes the *local* bounds of a shape \u2014 its AABB in local coordinates,\n * before `position`/`rotation`/`scale` are applied. The world AABB lives in\n * `getElementWorldBounds`.\n */\nexport type ElementBounder<S extends ElementBase = ElementBase> = (shape: S) => Bounds;\n\nconst bounderRegistry = new Map<string, ElementBounder>();\n\n/**\n * Register a bounder for a custom shape type. Plugins call this once at module\n * load. The kernel ships bounders for every `BuiltinElement`.\n */\nexport const registerBounder = <S extends ElementBase>(\n type: S[\"type\"],\n bounder: ElementBounder<S>,\n): void => {\n bounderRegistry.set(type, bounder as ElementBounder);\n};\n\n/** Look up a registered bounder. Returns `undefined` for unknown shape types. */\nexport const getBounder = (type: string): ElementBounder | undefined => bounderRegistry.get(type);\n\n/**\n * Local AABB for any shape with a registered bounder. Throws on unknown types\n * \u2014 callers should either register a bounder or filter unknown shapes out.\n */\nexport const getElementLocalBounds = (shape: ElementBase): Bounds => {\n const bounder = bounderRegistry.get(shape.type);\n if (!bounder) {\n throw new Error(`No bounder registered for shape type: ${shape.type}`);\n }\n return bounder(shape);\n};\n\n/**\n * World-space AABB after `position`/`rotation`/`scale`. This is the conservative\n * AABB of the rotated/scaled local box, suitable for spatial-index keys.\n */\nexport const getElementWorldBounds = (shape: ElementBase): Bounds => {\n const local = getElementLocalBounds(shape);\n // Transform 4 corners then re-AABB.\n const corners: readonly Vec2[] = [\n { x: local.x, y: local.y },\n { x: local.x + local.width, y: local.y },\n { x: local.x, y: local.y + local.height },\n { x: local.x + local.width, y: local.y + local.height },\n ];\n const sin = Math.sin(shape.rotation);\n const cos = Math.cos(shape.rotation);\n const transformed = corners.map((p) => {\n const sx = p.x * shape.scale.x;\n const sy = p.y * shape.scale.y;\n return {\n x: shape.position.x + (sx * cos - sy * sin),\n y: shape.position.y + (sx * sin + sy * cos),\n };\n });\n return B.fromPoints(transformed);\n};\n\n// --- built-in bounders ---\n\nregisterBounder<RectangleElement>(\"rectangle\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\nregisterBounder<EllipseElement>(\"ellipse\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\nregisterBounder<PolygonElement>(\"polygon\", (s) => B.fromPoints(s.points));\n\nregisterBounder<StickyElement>(\"sticky\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\nregisterBounder<EmojiElement>(\"emoji\", (s) => ({ x: 0, y: 0, width: s.size, height: s.size }));\n\nregisterBounder<PathElement>(\"path\", (s) => {\n const points: Vec2[] = [];\n let cursor: Vec2 = { x: 0, y: 0 };\n for (const cmd of s.commands) {\n switch (cmd.kind) {\n case \"M\":\n case \"L\":\n points.push(cmd.to);\n cursor = cmd.to;\n break;\n case \"Q\":\n points.push(cmd.control, cmd.to);\n cursor = cmd.to;\n break;\n case \"C\":\n points.push(cmd.control1, cmd.control2, cmd.to);\n cursor = cmd.to;\n break;\n case \"Z\":\n // closes the subpath, no new points\n break;\n }\n }\n // suppress unused-var warning\n void cursor;\n return B.fromPoints(points);\n});\n\nregisterBounder<TextElement>(\"text\", (s) => {\n // Width comes from the host measurer when installed (matches the\n // actually-rendered glyph advances), falling back to a geometric\n // estimate (`chars \u00D7 fontSize \u00D7 factor`) headless / in tests. Height\n // is line count \u00D7 line-height; hard newlines honoured in both modes.\n const lineHeight = s.fontSize * TEXT_LINE_HEIGHT_FACTOR;\n // An empty element is sized by its placeholder prompt (the renderer draws\n // it while the text is being written), so the selection box and the\n // dirty rect cover exactly what is on screen; the box snaps to the real\n // text from the first keystroke.\n const paragraphs = (s.text === \"\" ? pickTextPlaceholder(s.id) : s.text).split(\"\\n\");\n const measurer = getTextMeasurer();\n // Pass weight/style so the measured width matches the rendered (bold /\n // italic) glyphs \u2014 otherwise the box wouldn't grow when text is bolded.\n const opts = {\n bold: s.style.fontWeight === \"bold\",\n italic: s.style.fontStyle === \"italic\",\n };\n const measureLine = (line: string): number => {\n if (measurer) {\n const w = measurer(line, s.fontFamily, s.fontSize, opts);\n if (w !== null) return w;\n }\n return line.length * s.fontSize * TEXT_APPROX_CHAR_WIDTH_FACTOR;\n };\n if (s.maxWidth === undefined) {\n // Auto-width: widest paragraph drives width, one visual line per\n // paragraph.\n let width = 0;\n for (const p of paragraphs) width = Math.max(width, measureLine(p));\n width = Math.max(width, s.fontSize * 0.5);\n return { x: 0, y: 0, width, height: Math.max(1, paragraphs.length) * lineHeight };\n }\n // Fixed-width: width is the budget; height \u2248 wrapped line count.\n let lines = 0;\n for (const p of paragraphs) {\n lines += Math.max(1, Math.ceil(measureLine(p) / s.maxWidth));\n }\n return { x: 0, y: 0, width: s.maxWidth, height: Math.max(1, lines) * lineHeight };\n});\n\nregisterBounder<ImageElement>(\"image\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\n// Built-in template bounder: uses the explicit `width` \u00D7 `height` box. The\n// templates package can re-register a tighter bounder driven by the layout\n// engine when an instance is auto-sized.\nregisterBounder<TemplateElement>(\"template\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\nregisterBounder<BrushElement>(\"brush\", (s) => {\n if (s.points.length === 0) return { x: 0, y: 0, width: 0, height: 0 };\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n for (const p of s.points) {\n if (p.x - p.width < minX) minX = p.x - p.width;\n if (p.y - p.width < minY) minY = p.y - p.width;\n if (p.x + p.width > maxX) maxX = p.x + p.width;\n if (p.y + p.width > maxY) maxY = p.y + p.width;\n }\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n});\n\n// Group shapes have no intrinsic geometry \u2014 their world AABB is empty.\n// Callers that need the union of descendants must walk `parentId` via\n// `getChildrenOf` and union the children's world bounds instead.\nregisterBounder<GroupElement>(\"group\", () => ({ x: 0, y: 0, width: 0, height: 0 }));\n\nregisterBounder<FrameElement>(\"frame\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\nregisterBounder<BlockArrowElement>(\"block-arrow\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n", "import type { FractionalIndex } from \"fractional-keys\";\n\n/** Compare by fractional `order`, ascending (bottom-to-top z-order). */\nexport const byOrderAsc = <T extends { readonly order: FractionalIndex }>(a: T, b: T): number =>\n a.order < b.order ? -1 : a.order > b.order ? 1 : 0;\n\n/** Compare by fractional `order`, descending (top-to-bottom). */\nexport const byOrderDesc = <T extends { readonly order: FractionalIndex }>(a: T, b: T): number =>\n byOrderAsc(b, a);\n", "import type { Bounds, LinkId, LayerId, ElementId, Vec2 } from \"@oh-just-another/types\";\nimport { bounds as B } from \"@oh-just-another/math\";\nimport type { Link } from \"../edges/edge.js\";\nimport type { Layer } from \"../model/layer.js\";\nimport type { Scene } from \"../model/scene.js\";\nimport {\n MAX_PARENT_DEPTH,\n SELECTION_OUTLINE_CORNER_SAMPLES,\n SELECTION_OUTLINE_CURVE_SAMPLES,\n SELECTION_OUTLINE_ELLIPSE_SAMPLES,\n SPATIAL_GRID_CELL_SIZE,\n} from \"../constants.js\";\nimport {\n getElementWorldBounds,\n getElementLocalBounds,\n isPolygon,\n isEllipse,\n isRectangle,\n isImage,\n isText,\n isPath,\n isGroup,\n type Element,\n type PathCommand,\n} from \"../shapes/shape.js\";\nimport { getCornerRadius } from \"../text/style.js\";\nimport { SpatialGrid } from \"./spatial.js\";\nimport { byOrderAsc } from \"../model/order.js\";\nimport { localToWorld } from \"../shapes/shape-transform.js\";\nimport { ellipseOutlinePoint } from \"../shapes/ellipse.js\";\n\n// --- direct lookups ---\n\nexport const getElement = (scene: Scene, id: ElementId): Element | undefined =>\n scene.elements.get(id);\n\nexport const getLink = (scene: Scene, id: LinkId): Link | undefined => scene.links.get(id);\n\nexport const getLayer = (scene: Scene, id: LayerId): Layer | undefined => scene.layers.get(id);\n\n// --- iteration in z-order ---\n\n/**\n * Layers sorted bottom-to-top by their `order` field. Stable for equal orders\n * (which should not happen in practice with fractional indices).\n */\nexport const getLayersInOrder = (scene: Scene): readonly Layer[] =>\n [...scene.layers.values()].sort(byOrderAsc);\n\n/** Shapes in `layerId`, sorted bottom-to-top by `order`. */\nexport const getElementsInLayer = (scene: Scene, layerId: LayerId): readonly Element[] =>\n [...scene.elements.values()].filter((s) => s.layerId === layerId).sort(byOrderAsc);\n\nexport const getLinksInLayer = (scene: Scene, layerId: LayerId): readonly Link[] =>\n [...scene.links.values()].filter((e) => e.layerId === layerId).sort(byOrderAsc);\n\n// --- selection outline (contour) ---\n\nconst rectLoop = (b: Bounds): Vec2[] => [\n { x: b.x, y: b.y },\n { x: b.x + b.width, y: b.y },\n { x: b.x + b.width, y: b.y + b.height },\n { x: b.x, y: b.y + b.height },\n];\n\n/** Rounded-rect outline as a polyline \u2014 straight edges + sampled corner arcs. */\nconst roundedRectLoop = (b: Bounds, r: number): Vec2[] => {\n const { x, y, width: w, height: h } = b;\n const arc = (cx: number, cy: number, from: number, to: number): Vec2[] => {\n const pts: Vec2[] = [];\n for (let i = 0; i <= SELECTION_OUTLINE_CORNER_SAMPLES; i++) {\n const a = from + (to - from) * (i / SELECTION_OUTLINE_CORNER_SAMPLES);\n pts.push({ x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) });\n }\n return pts;\n };\n const HALF_PI = Math.PI / 2;\n return [\n // top-left \u2192 top-right \u2192 bottom-right \u2192 bottom-left corners (clockwise).\n ...arc(x + r, y + r, Math.PI, Math.PI + HALF_PI),\n ...arc(x + w - r, y + r, -HALF_PI, 0),\n ...arc(x + w - r, y + h - r, 0, HALF_PI),\n ...arc(x + r, y + h - r, HALF_PI, Math.PI),\n ];\n};\n\nconst flattenPath = (commands: readonly PathCommand[]): Vec2[] => {\n const pts: Vec2[] = [];\n let cur: Vec2 = { x: 0, y: 0 };\n for (const c of commands) {\n if (c.kind === \"M\" || c.kind === \"L\") {\n cur = c.to;\n pts.push({ x: cur.x, y: cur.y });\n } else if (c.kind === \"Q\") {\n for (let i = 1; i <= SELECTION_OUTLINE_CURVE_SAMPLES; i++) {\n const t = i / SELECTION_OUTLINE_CURVE_SAMPLES;\n const u = 1 - t;\n pts.push({\n x: u * u * cur.x + 2 * u * t * c.control.x + t * t * c.to.x,\n y: u * u * cur.y + 2 * u * t * c.control.y + t * t * c.to.y,\n });\n }\n cur = c.to;\n } else if (c.kind === \"C\") {\n for (let i = 1; i <= SELECTION_OUTLINE_CURVE_SAMPLES; i++) {\n const t = i / SELECTION_OUTLINE_CURVE_SAMPLES;\n const u = 1 - t;\n pts.push({\n x:\n u ** 3 * cur.x +\n 3 * u * u * t * c.control1.x +\n 3 * u * t * t * c.control2.x +\n t ** 3 * c.to.x,\n y:\n u ** 3 * cur.y +\n 3 * u * u * t * c.control1.y +\n 3 * u * t * t * c.control2.y +\n t ** 3 * c.to.y,\n });\n }\n cur = c.to;\n }\n // \"Z\" closes implicitly \u2014 each loop is closed by the consumer.\n }\n return pts;\n};\n\n/**\n * Outline provider for a custom / composite element type \u2014 returns the\n * shape's contour as one or more LOCAL-space loops (pre transform). Lets a\n * plugin element made of several visually-disconnected figures (e.g. two\n * unconnected ellipses, no background) supply a multi-loop selection halo\n * instead of falling back to its bounding box. Registered by `shape.type`.\n */\nexport type ElementOutlineProvider = (shape: Element) => Vec2[][];\n\nconst outlineProviders = new Map<string, ElementOutlineProvider>();\n\n/** Register a multi-loop outline provider for a custom element `type`. */\nexport const registerElementOutline = (type: string, provider: ElementOutlineProvider): void => {\n outlineProviders.set(type, provider);\n};\n\n/** Local-space outline loop(s) for a single (non-group) shape, or `null`. */\nconst localOutlineLoops = (shape: Element): Vec2[][] | null => {\n if (isPolygon(shape)) return [shape.points.map((p) => ({ x: p.x, y: p.y }))];\n if (isEllipse(shape)) {\n const b = getElementLocalBounds(shape);\n const cx = b.x + b.width / 2;\n const cy = b.y + b.height / 2;\n const rx = b.width / 2;\n const ry = b.height / 2;\n const pts: Vec2[] = [];\n for (let i = 0; i < SELECTION_OUTLINE_ELLIPSE_SAMPLES; i++) {\n pts.push(ellipseOutlinePoint(cx, cy, rx, ry, i / SELECTION_OUTLINE_ELLIPSE_SAMPLES));\n }\n return [pts];\n }\n if (isRectangle(shape)) {\n const b = getElementLocalBounds(shape);\n const r = getCornerRadius(shape.style.roundness, b.width, b.height);\n return [r > 0 ? roundedRectLoop(b, r) : rectLoop(b)];\n }\n if (isImage(shape) || isText(shape)) {\n return [rectLoop(getElementLocalBounds(shape))];\n }\n if (isPath(shape)) return [flattenPath(shape.commands)];\n // group handled by the caller; brush / template / custom \u2192 bbox fallback.\n return null;\n};\n\n/**\n * World-space outline loop(s) tracing a shape's actual contour, for the\n * selection halo. polygon (star / diamond / hexagon) is exact; ellipse and\n * path are sampled; a group returns one loop per descendant (handles\n * visually-disconnected figures). Shapes without known geometry (composite\n * template, brush, custom) fall back to their world bounding box. Cheap\n * enough to recompute every frame \u2014 no baking needed.\n */\nexport const getElementOutline = (scene: Scene, shape: Element): Vec2[][] => {\n if (isGroup(shape)) {\n const loops: Vec2[][] = [];\n for (const child of getChildrenOf(scene, shape.id))\n loops.push(...getElementOutline(scene, child));\n return loops;\n }\n const local = localOutlineLoops(shape);\n if (local) return local.map((loop) => loop.map((p) => localToWorld(shape, p)));\n // Custom / composite type with a registered outline provider (multi-loop).\n const provider = outlineProviders.get(shape.type);\n if (provider) {\n const loops = provider(shape).filter((loop) => loop.length >= 2);\n if (loops.length > 0) return loops.map((loop) => loop.map((p) => localToWorld(shape, p)));\n }\n // Fallback: axis-aligned world bounding box.\n const b = getElementWorldBounds(shape);\n return [rectLoop(b)];\n};\n\n// --- group queries (parentId chain) ---\n\n/**\n * Direct children of `parentId` \u2014 every shape whose `parentId` equals\n * the argument, in z-order. Linear in scene size; for groups inside the\n * editor's hot path, cache the result by `(scene, parentId)`.\n */\nexport const getChildrenOf = (scene: Scene, parentId: ElementId): readonly Element[] => {\n const out: Element[] = [];\n for (const s of scene.elements.values()) {\n if (s.parentId === parentId) out.push(s);\n }\n out.sort(byOrderAsc);\n return out;\n};\n\n/**\n * `true` when the shape (or any of its ancestors via `parentId`) has\n * `locked: true`. Walks the parent chain bounded by\n * `MAX_PARENT_DEPTH` so the answer stays O(depth) for a freshly\n * grouped scene. Independent from `Layer.locked` \u2014 callers that need\n * the combined interactivity gate should `||` both flags.\n */\nexport const isElementLocked = (scene: Scene, shape: Element): boolean => {\n let current: Element | undefined = shape;\n for (let i = 0; current && i < MAX_PARENT_DEPTH; i++) {\n if (current.locked === true) return true;\n if (!current.parentId) return false;\n current = scene.elements.get(current.parentId);\n }\n return false;\n};\n\n/**\n * `true` when the shape (or any of its ancestors via `parentId`) has\n * `hidden: true`. Same propagation semantics as `isElementLocked`.\n */\nexport const isElementHidden = (scene: Scene, shape: Element): boolean => {\n let current: Element | undefined = shape;\n for (let i = 0; current && i < MAX_PARENT_DEPTH; i++) {\n if (current.hidden === true) return true;\n if (!current.parentId) return false;\n current = scene.elements.get(current.parentId);\n }\n return false;\n};\n\n/**\n * Walks the `parentId` chain starting from `elementId` and returns the\n * topmost ancestor (the root). Returns the shape itself when it has no\n * parent, or `undefined` when the shape (or any ancestor) is missing.\n * Cycle-safe \u2014 bails after `MAX_PARENT_DEPTH` hops.\n */\nexport const getRootSelf = (scene: Scene, elementId: ElementId): Element | undefined => {\n let current = scene.elements.get(elementId);\n for (let i = 0; current?.parentId && i < MAX_PARENT_DEPTH; i++) {\n const parent = scene.elements.get(current.parentId);\n if (!parent) break;\n current = parent;\n }\n return current;\n};\n\n/**\n * Every descendant of `parentId`, recursive, including the root itself.\n * Order: parent first, then a depth-first walk. Cycle-safe via the\n * `visited` set.\n */\nexport const getDescendantsOf = (scene: Scene, parentId: ElementId): readonly Element[] => {\n const root = scene.elements.get(parentId);\n if (!root) return [];\n const visited = new Set<ElementId>([parentId]);\n const out: Element[] = [root];\n const stack: ElementId[] = [parentId];\n while (stack.length > 0) {\n const cur = stack.pop();\n if (cur === undefined) break;\n for (const child of getChildrenOf(scene, cur)) {\n if (visited.has(child.id)) continue;\n visited.add(child.id);\n out.push(child);\n stack.push(child.id);\n }\n }\n return out;\n};\n\n// --- spatial queries (linear scan) ---\n\n/**\n * Shapes whose world AABB intersects `range`. Linear in the number of shapes.\n * For large scenes use `buildSpatialIndex` once and query the index.\n */\nexport const getElementsInBounds = (scene: Scene, range: Bounds): readonly Element[] => {\n const out: Element[] = [];\n for (const s of scene.elements.values()) {\n if (B.intersects(getElementWorldBounds(s), range)) out.push(s);\n }\n return out;\n};\n\n/**\n * Shapes whose world-AABB is at least `minCoverageRatio` covered by\n * `range`. `1` requires full containment (containment-style lasso);\n * `0.5` selects when at least half of the element sits inside the\n * box \u2014 friendlier than pure intersection because brushing past an\n * edge doesn't accidentally grab the shape.\n *\n * Always selects shapes that fully contain the lasso (small lasso\n * inside a big shape) \u2014 same affordance as bidirectional containment.\n * Zero-area shapes (groups, brushes-with-one-vertex) fall back to a\n * plain intersection test.\n */\nexport const getElementsCoveredByBounds = (\n scene: Scene,\n range: Bounds,\n minCoverageRatio = 0.5,\n): readonly Element[] => {\n const out: Element[] = [];\n for (const s of scene.elements.values()) {\n const b = getElementWorldBounds(s);\n if (!B.intersects(b, range)) continue;\n const area = b.width * b.height;\n if (area <= 0) {\n out.push(s);\n continue;\n }\n const ix = Math.max(b.x, range.x);\n const iy = Math.max(b.y, range.y);\n const ix2 = Math.min(b.x + b.width, range.x + range.width);\n const iy2 = Math.min(b.y + b.height, range.y + range.height);\n const iw = ix2 - ix;\n const ih = iy2 - iy;\n if (iw <= 0 || ih <= 0) continue;\n const coverage = (iw * ih) / area;\n if (coverage >= minCoverageRatio) {\n out.push(s);\n continue;\n }\n // Bidirectional: tiny lasso inside a big shape still picks it.\n const lassoArea = range.width * range.height;\n if (lassoArea > 0 && (iw * ih) / lassoArea >= minCoverageRatio) {\n out.push(s);\n }\n }\n return out;\n};\n\n/**\n * Topmost shape containing `point`. Iterates layers top-to-bottom, then shapes\n * within each layer top-to-bottom; returns the first hit. Hit-test here is the\n * conservative AABB test; renderer-specific shape-precise hit-tests belong\n * with the renderer.\n *\n * `accept` filters candidates: a shape it rejects is skipped and the scan\n * continues to the shapes beneath it (click-through), instead of shadowing\n * them the way a post-hoc filter on the topmost hit would.\n */\nexport const getElementAt = (\n scene: Scene,\n point: Vec2,\n accept?: (shape: Element) => boolean,\n): Element | undefined => {\n const layers = getLayersInOrder(scene);\n for (let i = layers.length - 1; i >= 0; i--) {\n const layer = layers[i];\n if (!layer?.visible) continue;\n const shapes = getElementsInLayer(scene, layer.id);\n for (let j = shapes.length - 1; j >= 0; j--) {\n const s = shapes[j];\n if (s === undefined) continue;\n if (!B.contains(getElementWorldBounds(s), point)) continue;\n if (accept && !accept(s)) continue;\n return s;\n }\n }\n return undefined;\n};\n\n// --- spatial index helpers ---\n\n/**\n * Build a `SpatialGrid` from the current scene. Re-build (or update\n * incrementally) when shapes change \u2014 the grid is not auto-synced with the\n * scene. The default cell size is tuned for typical editor scenes; pass an\n * explicit value if your shapes are much larger or smaller.\n */\nexport const buildSpatialIndex = (\n scene: Scene,\n cellSize: number = SPATIAL_GRID_CELL_SIZE,\n): SpatialGrid => {\n const grid = new SpatialGrid(cellSize);\n for (const shape of scene.elements.values()) {\n grid.insert(shape.id, getElementWorldBounds(shape));\n }\n return grid;\n};\n\n/**\n * Range query backed by the index. Returns shapes (not just ids) whose AABB\n * actually intersects `range`. The grid pre-filters by cell overlap; this\n * function does the precise AABB filter.\n */\nexport const queryByIndex = (\n scene: Scene,\n grid: SpatialGrid,\n range: Bounds,\n): readonly Element[] => {\n const candidates = grid.query(range);\n const out: Element[] = [];\n for (const id of candidates) {\n const shape = scene.elements.get(id);\n if (!shape) continue;\n if (B.intersects(getElementWorldBounds(shape), range)) out.push(shape);\n }\n return out;\n};\n\n/**\n * Point hit-test backed by a SpatialGrid. Equivalent to `getElementAt` but\n * pre-filters candidates through `grid.query` \u2014 O(k) where k is the\n * shapes overlapping the point's cell. Walks layers top-to-bottom for\n * stable z-order; within a layer picks the highest-`order` shape that\n * actually contains the point. `accept` skips rejected shapes and keeps\n * scanning beneath them (same click-through contract as `getElementAt`).\n */\nexport const getElementAtIndexed = (\n scene: Scene,\n grid: SpatialGrid,\n point: Vec2,\n accept?: (shape: Element) => boolean,\n): Element | undefined => {\n const pointRange: Bounds = { x: point.x, y: point.y, width: 0, height: 0 };\n const candidates = grid.query(pointRange);\n if (candidates.size === 0) return undefined;\n let best: Element | undefined;\n let bestLayerOrder = \"\";\n let bestElementOrder = \"\";\n let bestSet = false;\n for (const id of candidates) {\n const shape = scene.elements.get(id);\n if (!shape) continue;\n if (!B.contains(getElementWorldBounds(shape), point)) continue;\n if (accept && !accept(shape)) continue;\n const layer = scene.layers.get(shape.layerId);\n if (!layer?.visible) continue;\n const layerOrder = layer.order as string;\n if (\n !bestSet ||\n layerOrder > bestLayerOrder ||\n (layerOrder === bestLayerOrder && shape.order > bestElementOrder)\n ) {\n best = shape;\n bestLayerOrder = layerOrder;\n bestElementOrder = shape.order;\n bestSet = true;\n }\n }\n return best;\n};\n", "import { vec2 } from \"@oh-just-another/math\";\nimport { req, type Vec2 } from \"@oh-just-another/types\";\nimport type { BrushPoint } from \"./shape.js\";\nimport { BRUSH_OUTLINE_ARC_STEP, BRUSH_OUTLINE_MITER_LIMIT } from \"../constants.js\";\n\n/**\n * The single closed outline polygon of a variable-width brush stroke: the left\n * offset side forward, a round end cap, the right offset side back, a round start\n * cap. Filling this ONE simple polygon (nonzero winding) paints every pixel\n * exactly once \u2014 unlike per-segment quads + joint discs, whose overlaps\n * double-blend at `opacity < 1` (dark blotches at the joins).\n *\n * Round joins/caps are approximated by arc points at {@link BRUSH_OUTLINE_ARC_STEP}\n * spacing. Convex corners round outward with an arc; concave corners take the\n * miter (offset-line intersection), clamped to {@link BRUSH_OUTLINE_MITER_LIMIT}\n * half-widths (bevel beyond that) so the polygon stays simple \u2014 the WebGL2 earcut\n * fill needs a non-self-intersecting boundary.\n *\n * `points` carry per-vertex half-width. Returns `[]` for `< 2` points (callers\n * draw a single dot as an ellipse). Output is a closed loop in the same local\n * space as `points` (no duplicated closing point; the caller closes the path).\n */\nexport const brushOutline = (points: readonly BrushPoint[]): Vec2[] => {\n const n = points.length;\n if (n < 2) return [];\n const pos = (i: number): Vec2 => {\n const q = req(points[i]);\n return { x: q.x, y: q.y };\n };\n const halfWidth = (i: number): number => req(points[i]).width;\n\n // Per-segment unit direction and LEFT normal (perp = (-dy, dx)).\n const dir: Vec2[] = [];\n const nrm: Vec2[] = [];\n for (let i = 0; i < n - 1; i++) {\n const raw = vec2.sub(pos(i + 1), pos(i));\n const d = vec2.lengthSq(raw) > 0 ? vec2.normalize(raw) : { x: 1, y: 0 };\n dir.push(d);\n nrm.push(vec2.perp(d));\n }\n\n const left: Vec2[] = [];\n const right: Vec2[] = [];\n for (let i = 0; i < n; i++) {\n const w = halfWidth(i);\n const c = pos(i);\n if (i === 0) {\n const nn = req(nrm[0]);\n left.push(vec2.add(c, vec2.mul(nn, w)));\n right.push(vec2.sub(c, vec2.mul(nn, w)));\n continue;\n }\n if (i === n - 1) {\n const nn = req(nrm[n - 2]);\n left.push(vec2.add(c, vec2.mul(nn, w)));\n right.push(vec2.sub(c, vec2.mul(nn, w)));\n continue;\n }\n const nPrev = req(nrm[i - 1]);\n const nNext = req(nrm[i]);\n const turn = vec2.cross(req(dir[i - 1]), req(dir[i]));\n if (Math.abs(turn) < 1e-9) {\n // Collinear \u2014 one offset point per side is enough.\n left.push(vec2.add(c, vec2.mul(nPrev, w)));\n right.push(vec2.sub(c, vec2.mul(nPrev, w)));\n continue;\n }\n // turn > 0: left turn \u2192 left side concave (miter), right side convex (arc).\n // turn < 0: right turn \u2192 left side convex (arc), right side concave (miter).\n if (turn > 0) {\n left.push(...miterSide(c, nPrev, nNext, w, 1));\n right.push(...arcSide(c, nPrev, nNext, w, -1));\n } else {\n left.push(...arcSide(c, nPrev, nNext, w, 1));\n right.push(...miterSide(c, nPrev, nNext, w, -1));\n }\n }\n\n // Round caps: half-turn arcs from the +normal offset to the -normal offset,\n // bulging past the endpoint along the stroke direction (+dir at the end,\n // -dir at the start). The \u2212\u03C0 sweep passes through that direction.\n const endCap = capArc(pos(n - 1), req(nrm[n - 2]), halfWidth(n - 1), false);\n const startCap = capArc(pos(0), req(nrm[0]), halfWidth(0), true);\n\n const outline: Vec2[] = [];\n for (const p of left) outline.push(p);\n for (const p of endCap) outline.push(p);\n for (let i = right.length - 1; i >= 0; i--) outline.push(req(right[i]));\n for (const p of startCap) outline.push(p);\n return outline;\n};\n\n/**\n * Concave corner: the miter point where the two offset lines meet, on the\n * `sign` side (+1 = left/+normal, -1 = right/-normal). Beyond the miter limit\n * (a very sharp turn) fall back to a bevel (the two segment-offset points) so the\n * outline can't grow a long spike.\n */\nconst miterSide = (c: Vec2, nPrev: Vec2, nNext: Vec2, w: number, sign: number): Vec2[] => {\n const m = vec2.normalize(vec2.add(nPrev, nNext));\n const cos = vec2.dot(m, nPrev); // cos of half the turn angle\n if (cos > 1e-3 && 1 / cos <= BRUSH_OUTLINE_MITER_LIMIT) {\n return [vec2.add(c, vec2.mul(m, sign * (w / cos)))];\n }\n return [vec2.add(c, vec2.mul(nPrev, sign * w)), vec2.add(c, vec2.mul(nNext, sign * w))];\n};\n\n/**\n * Convex corner: an arc of radius `w` around `c` from the previous offset\n * direction to the next, sampled the short way. `sign` picks the side (+1 =\n * +normal, -1 = -normal).\n */\nconst arcSide = (c: Vec2, nPrev: Vec2, nNext: Vec2, w: number, sign: number): Vec2[] =>\n arc(c, w, vec2.angle(vec2.mul(nPrev, sign)), vec2.angle(vec2.mul(nNext, sign)));\n\n/**\n * A round cap: the half-turn arc of radius `w` from the +normal offset to the\n * -normal offset. `fromOpposite` starts at the -normal side (for the START cap,\n * which the outline reaches from the reversed right side); the \u2212\u03C0 sweep makes it\n * bulge along the stroke direction rather than back across the body.\n */\nconst capArc = (c: Vec2, nrm: Vec2, w: number, fromOpposite: boolean): Vec2[] => {\n const a0 = vec2.angle(nrm) + (fromOpposite ? Math.PI : 0);\n return arc(c, w, a0, a0 - Math.PI);\n};\n\n/**\n * Sample a circular arc of `radius` around `c` from angle `a0` to `a1` inclusive,\n * taking the shorter signed sweep, at {@link BRUSH_OUTLINE_ARC_STEP} spacing.\n * A `\u00B1\u03C0` sweep (a cap) keeps its given sign so the half-circle bulges the right\n * way.\n */\nconst arc = (c: Vec2, radius: number, a0: number, a1: number): Vec2[] => {\n let delta = a1 - a0;\n while (delta > Math.PI + 1e-9) delta -= 2 * Math.PI;\n while (delta < -Math.PI - 1e-9) delta += 2 * Math.PI;\n const steps = Math.max(1, Math.ceil(Math.abs(delta) / BRUSH_OUTLINE_ARC_STEP));\n const out: Vec2[] = [];\n for (let k = 0; k <= steps; k++) {\n const a = a0 + (delta * k) / steps;\n out.push({ x: c.x + radius * Math.cos(a), y: c.y + radius * Math.sin(a) });\n }\n return out;\n};\n", "import type { Bounds } from \"@oh-just-another/types\";\nimport { getElementWorldBounds, type ElementBase } from \"./shape.js\";\n\n/**\n * Some elements PAINT beyond their geometric bounds \u2014 a frame draws its\n * header strip above the rectangle, a confetti box throws particles past\n * its edges. The dirty-rect / tile invalidation must clear that overspill\n * too, otherwise deleting (or moving without a full repaint) leaves a\n * \"ghost\" of the overpainted region.\n *\n * `RenderOverflow` is the per-side extra paint margin (world units) an\n * element type bleeds past `getElementWorldBounds`. Providers are keyed by\n * element `type` and may inspect the shape (e.g. only confetti-tagged\n * rectangles overflow). All sides default to 0.\n */\nexport interface RenderOverflow {\n readonly top?: number;\n readonly right?: number;\n readonly bottom?: number;\n readonly left?: number;\n}\n\ntype RenderOverflowProvider = (shape: ElementBase) => RenderOverflow;\n\nconst providers = new Map<string, RenderOverflowProvider>();\n\n/**\n * Register how far an element type paints past its bounds. The renderer\n * that draws the overspill owns this (it knows the header height /\n * particle spread). Idempotent per type \u2014 last registration wins.\n */\nexport const registerRenderOverflow = (type: string, fn: RenderOverflowProvider): void => {\n providers.set(type, fn);\n};\n\n/**\n * World bounds expanded by the element type's registered paint overflow \u2014\n * the region that must be invalidated/cleared when the element changes or\n * is removed. Falls back to the plain world bounds when no overflow is\n * registered (the common case).\n */\nexport const getElementRenderBounds = (shape: ElementBase): Bounds => {\n const b = getElementWorldBounds(shape);\n const fn = providers.get(shape.type);\n if (!fn) return b;\n const o = fn(shape);\n const top = o.top ?? 0;\n const right = o.right ?? 0;\n const bottom = o.bottom ?? 0;\n const left = o.left ?? 0;\n if (top === 0 && right === 0 && bottom === 0 && left === 0) return b;\n return {\n x: b.x - left,\n y: b.y - top,\n width: b.width + left + right,\n height: b.height + top + bottom,\n };\n};\n", "import type { Transform, Vec2 } from \"@oh-just-another/types\";\nimport { matrix } from \"@oh-just-another/math\";\nimport { DEFAULT_GRID_SPACING, DEFAULT_CANVAS_BACKGROUND } from \"../constants.js\";\n\n/**\n * How the background grid is painted.\n * `\"lines\"` \u2014 ruled grid lines (default).\n * `\"dots\"` \u2014 a dot at every grid intersection.\n *\n * Snap behaviour is identical between styles; only the paint differs.\n */\nexport type GridStyle = \"lines\" | \"dots\";\n\n/**\n * Camera over the world. Stored as pan/zoom/rotation rather than a raw matrix\n * because every UI control (zoom-to-fit, hotkeys, pinch) wants these axes\n * directly; the matrix form is derivable.\n */\nexport interface Viewport {\n /** World coordinate at viewport (0, 0) before rotation. */\n readonly pan: Vec2;\n /** Uniform scale. 1 = native; 2 = zoomed in 2\u00D7. */\n readonly zoom: number;\n /** Rotation in radians, counter-clockwise. */\n readonly rotation: number;\n readonly size: { readonly width: number; readonly height: number };\n /**\n * Whether the background grid is drawn for this scene. Spacing is fixed\n * at {@link DEFAULT_GRID_SPACING}; this flag controls only visibility.\n */\n readonly gridEnabled: boolean;\n /** How the grid is painted. Renderers fall back to lines when unset. */\n readonly gridStyle?: GridStyle;\n /**\n * Programmatic snap opt-out. `undefined` is treated as ON\n * (see {@link isSnapToGridEnabled}). Snapping also requires the grid to be\n * enabled \u2014 snapping to a hidden grid is confusing \u2014 so this flag only\n * matters while `gridEnabled` is true.\n */\n readonly snapToGrid?: boolean;\n /**\n * Saved \"start view\" camera \u2014 where the document opens (and where\n * \"go to start view\" jumps). Absent until the author sets one.\n */\n readonly startView?: StartView;\n /**\n * Canvas paper colour (any CSS colour) behind the grid and the shapes.\n * Absent = {@link DEFAULT_CANVAS_BACKGROUND}. Part of the document: it\n * serialises with the scene and reaches \"with background\" exports.\n */\n readonly background?: string;\n}\n\n/** The canvas paper colour of `viewport`, defaulted. */\nexport const canvasBackgroundOf = (viewport: Viewport): string =>\n viewport.background ?? DEFAULT_CANVAS_BACKGROUND;\n\n/** A saved camera pose: pan + zoom (rotation is not part of a start view). */\nexport interface StartView {\n readonly pan: Vec2;\n readonly zoom: number;\n}\n\n/** World-unit spacing snap-to-grid rounds to (the fixed grid spacing). */\nexport const resolveSnapSpacing = (): number => DEFAULT_GRID_SPACING;\n\n/**\n * Whether snap-to-grid is enabled for this viewport. `undefined` counts as\n * ON \u2014 the product default.\n */\nexport const isSnapToGridEnabled = (viewport: Viewport): boolean => viewport.snapToGrid ?? true;\n\nexport const DEFAULT_VIEWPORT: Viewport = Object.freeze({\n pan: { x: 0, y: 0 },\n zoom: 1,\n rotation: 0,\n size: { width: 0, height: 0 },\n // Grid off by default; hosts enable it per scene via `gridEnabled`.\n gridEnabled: false,\n});\n\n/** World \u2192 screen transform. */\nexport const getWorldToScreen = (viewport: Viewport): Transform => {\n // Order: world point \u2192 translate by -pan \u2192 rotate \u2192 scale.\n const translate = matrix.translation(-viewport.pan.x, -viewport.pan.y);\n const rotate = matrix.rotation(viewport.rotation);\n const scale = matrix.scaling(viewport.zoom);\n return matrix.multiply(scale, matrix.multiply(rotate, translate));\n};\n\n/** Screen \u2192 world transform (inverse of `getWorldToScreen`). */\nexport const getScreenToWorld = (viewport: Viewport): Transform =>\n matrix.inverse(getWorldToScreen(viewport));\n\n/**\n * Pan the camera by a screen-space delta. Most useful for drag handlers that\n * report pixel deltas; the delta is divided by `zoom` so panning by 1 screen\n * pixel moves the world by 1 / zoom world units.\n */\nexport const panBy = (viewport: Viewport, deltaScreen: Vec2): Viewport => ({\n ...viewport,\n pan: {\n x: viewport.pan.x - deltaScreen.x / viewport.zoom,\n y: viewport.pan.y - deltaScreen.y / viewport.zoom,\n },\n});\n\n/**\n * Multiplicative zoom around a world-space anchor. The anchor stays under the\n * same screen pixel, which is what users expect from mouse-wheel zoom.\n */\nexport const zoomAt = (viewport: Viewport, factor: number, anchorWorld: Vec2): Viewport => {\n const newZoom = viewport.zoom * factor;\n // Adjust pan so that anchorWorld maps to the same screen point.\n return {\n ...viewport,\n zoom: newZoom,\n pan: {\n x: anchorWorld.x - (anchorWorld.x - viewport.pan.x) / factor,\n y: anchorWorld.y - (anchorWorld.y - viewport.pan.y) / factor,\n },\n };\n};\n\nexport const resize = (viewport: Viewport, width: number, height: number): Viewport => ({\n ...viewport,\n size: { width, height },\n});\n", "const grayDark = {\n gray1: \"#111111\",\n gray2: \"#191919\",\n gray3: \"#222222\",\n gray4: \"#2a2a2a\",\n gray5: \"#313131\",\n gray6: \"#3a3a3a\",\n gray7: \"#484848\",\n gray8: \"#606060\",\n gray9: \"#6e6e6e\",\n gray10: \"#7b7b7b\",\n gray11: \"#b4b4b4\",\n gray12: \"#eeeeee\",\n};\nconst grayDarkA = {\n grayA1: \"#00000000\",\n grayA2: \"#ffffff09\",\n grayA3: \"#ffffff12\",\n grayA4: \"#ffffff1b\",\n grayA5: \"#ffffff22\",\n grayA6: \"#ffffff2c\",\n grayA7: \"#ffffff3b\",\n grayA8: \"#ffffff55\",\n grayA9: \"#ffffff64\",\n grayA10: \"#ffffff72\",\n grayA11: \"#ffffffaf\",\n grayA12: \"#ffffffed\",\n};\nconst grayDarkP3 = {\n gray1: \"color(display-p3 0.067 0.067 0.067)\",\n gray2: \"color(display-p3 0.098 0.098 0.098)\",\n gray3: \"color(display-p3 0.135 0.135 0.135)\",\n gray4: \"color(display-p3 0.163 0.163 0.163)\",\n gray5: \"color(display-p3 0.192 0.192 0.192)\",\n gray6: \"color(display-p3 0.228 0.228 0.228)\",\n gray7: \"color(display-p3 0.283 0.283 0.283)\",\n gray8: \"color(display-p3 0.375 0.375 0.375)\",\n gray9: \"color(display-p3 0.431 0.431 0.431)\",\n gray10: \"color(display-p3 0.484 0.484 0.484)\",\n gray11: \"color(display-p3 0.706 0.706 0.706)\",\n gray12: \"color(display-p3 0.933 0.933 0.933)\",\n};\nconst grayDarkP3A = {\n grayA1: \"color(display-p3 0 0 0 / 0)\",\n grayA2: \"color(display-p3 1 1 1 / 0.034)\",\n grayA3: \"color(display-p3 1 1 1 / 0.071)\",\n grayA4: \"color(display-p3 1 1 1 / 0.105)\",\n grayA5: \"color(display-p3 1 1 1 / 0.134)\",\n grayA6: \"color(display-p3 1 1 1 / 0.172)\",\n grayA7: \"color(display-p3 1 1 1 / 0.231)\",\n grayA8: \"color(display-p3 1 1 1 / 0.332)\",\n grayA9: \"color(display-p3 1 1 1 / 0.391)\",\n grayA10: \"color(display-p3 1 1 1 / 0.445)\",\n grayA11: \"color(display-p3 1 1 1 / 0.685)\",\n grayA12: \"color(display-p3 1 1 1 / 0.929)\",\n};\nconst mauveDark = {\n mauve1: \"#121113\",\n mauve2: \"#1a191b\",\n mauve3: \"#232225\",\n mauve4: \"#2b292d\",\n mauve5: \"#323035\",\n mauve6: \"#3c393f\",\n mauve7: \"#49474e\",\n mauve8: \"#625f69\",\n mauve9: \"#6f6d78\",\n mauve10: \"#7c7a85\",\n mauve11: \"#b5b2bc\",\n mauve12: \"#eeeef0\",\n};\nconst mauveDarkA = {\n mauveA1: \"#00000000\",\n mauveA2: \"#f5f4f609\",\n mauveA3: \"#ebeaf814\",\n mauveA4: \"#eee5f81d\",\n mauveA5: \"#efe6fe25\",\n mauveA6: \"#f1e6fd30\",\n mauveA7: \"#eee9ff40\",\n mauveA8: \"#eee7ff5d\",\n mauveA9: \"#eae6fd6e\",\n mauveA10: \"#ece9fd7c\",\n mauveA11: \"#f5f1ffb7\",\n mauveA12: \"#fdfdffef\",\n};\nconst mauveDarkP3 = {\n mauve1: \"color(display-p3 0.07 0.067 0.074)\",\n mauve2: \"color(display-p3 0.101 0.098 0.105)\",\n mauve3: \"color(display-p3 0.138 0.134 0.144)\",\n mauve4: \"color(display-p3 0.167 0.161 0.175)\",\n mauve5: \"color(display-p3 0.196 0.189 0.206)\",\n mauve6: \"color(display-p3 0.232 0.225 0.245)\",\n mauve7: \"color(display-p3 0.286 0.277 0.302)\",\n mauve8: \"color(display-p3 0.383 0.373 0.408)\",\n mauve9: \"color(display-p3 0.434 0.428 0.467)\",\n mauve10: \"color(display-p3 0.487 0.48 0.519)\",\n mauve11: \"color(display-p3 0.707 0.7 0.735)\",\n mauve12: \"color(display-p3 0.933 0.933 0.94)\",\n};\nconst mauveDarkP3A = {\n mauveA1: \"color(display-p3 0 0 0 / 0)\",\n mauveA2: \"color(display-p3 0.996 0.992 1 / 0.034)\",\n mauveA3: \"color(display-p3 0.937 0.933 0.992 / 0.077)\",\n mauveA4: \"color(display-p3 0.957 0.918 0.996 / 0.111)\",\n mauveA5: \"color(display-p3 0.937 0.906 0.996 / 0.145)\",\n mauveA6: \"color(display-p3 0.953 0.925 0.996 / 0.183)\",\n mauveA7: \"color(display-p3 0.945 0.929 1 / 0.246)\",\n mauveA8: \"color(display-p3 0.937 0.918 1 / 0.361)\",\n mauveA9: \"color(display-p3 0.933 0.918 1 / 0.424)\",\n mauveA10: \"color(display-p3 0.941 0.925 1 / 0.479)\",\n mauveA11: \"color(display-p3 0.965 0.961 1 / 0.712)\",\n mauveA12: \"color(display-p3 0.992 0.992 1 / 0.937)\",\n};\nconst slateDark = {\n slate1: \"#111113\",\n slate2: \"#18191b\",\n slate3: \"#212225\",\n slate4: \"#272a2d\",\n slate5: \"#2e3135\",\n slate6: \"#363a3f\",\n slate7: \"#43484e\",\n slate8: \"#5a6169\",\n slate9: \"#696e77\",\n slate10: \"#777b84\",\n slate11: \"#b0b4ba\",\n slate12: \"#edeef0\",\n};\nconst slateDarkA = {\n slateA1: \"#00000000\",\n slateA2: \"#d8f4f609\",\n slateA3: \"#ddeaf814\",\n slateA4: \"#d3edf81d\",\n slateA5: \"#d9edfe25\",\n slateA6: \"#d6ebfd30\",\n slateA7: \"#d9edff40\",\n slateA8: \"#d9edff5d\",\n slateA9: \"#dfebfd6d\",\n slateA10: \"#e5edfd7b\",\n slateA11: \"#f1f7feb5\",\n slateA12: \"#fcfdffef\",\n};\nconst slateDarkP3 = {\n slate1: \"color(display-p3 0.067 0.067 0.074)\",\n slate2: \"color(display-p3 0.095 0.098 0.105)\",\n slate3: \"color(display-p3 0.13 0.135 0.145)\",\n slate4: \"color(display-p3 0.156 0.163 0.176)\",\n slate5: \"color(display-p3 0.183 0.191 0.206)\",\n slate6: \"color(display-p3 0.215 0.226 0.244)\",\n slate7: \"color(display-p3 0.265 0.28 0.302)\",\n slate8: \"color(display-p3 0.357 0.381 0.409)\",\n slate9: \"color(display-p3 0.415 0.431 0.463)\",\n slate10: \"color(display-p3 0.469 0.483 0.514)\",\n slate11: \"color(display-p3 0.692 0.704 0.728)\",\n slate12: \"color(display-p3 0.93 0.933 0.94)\",\n};\nconst slateDarkP3A = {\n slateA1: \"color(display-p3 0 0 0 / 0)\",\n slateA2: \"color(display-p3 0.875 0.992 1 / 0.034)\",\n slateA3: \"color(display-p3 0.882 0.933 0.992 / 0.077)\",\n slateA4: \"color(display-p3 0.882 0.953 0.996 / 0.111)\",\n slateA5: \"color(display-p3 0.878 0.929 0.996 / 0.145)\",\n slateA6: \"color(display-p3 0.882 0.949 0.996 / 0.183)\",\n slateA7: \"color(display-p3 0.882 0.929 1 / 0.246)\",\n slateA8: \"color(display-p3 0.871 0.937 1 / 0.361)\",\n slateA9: \"color(display-p3 0.898 0.937 1 / 0.42)\",\n slateA10: \"color(display-p3 0.918 0.945 1 / 0.475)\",\n slateA11: \"color(display-p3 0.949 0.969 0.996 / 0.708)\",\n slateA12: \"color(display-p3 0.988 0.992 1 / 0.937)\",\n};\nconst sageDark = {\n sage1: \"#101211\",\n sage2: \"#171918\",\n sage3: \"#202221\",\n sage4: \"#272a29\",\n sage5: \"#2e3130\",\n sage6: \"#373b39\",\n sage7: \"#444947\",\n sage8: \"#5b625f\",\n sage9: \"#63706b\",\n sage10: \"#717d79\",\n sage11: \"#adb5b2\",\n sage12: \"#eceeed\",\n};\nconst sageDarkA = {\n sageA1: \"#00000000\",\n sageA2: \"#f0f2f108\",\n sageA3: \"#f3f5f412\",\n sageA4: \"#f2fefd1a\",\n sageA5: \"#f1fbfa22\",\n sageA6: \"#edfbf42d\",\n sageA7: \"#edfcf73c\",\n sageA8: \"#ebfdf657\",\n sageA9: \"#dffdf266\",\n sageA10: \"#e5fdf674\",\n sageA11: \"#f4fefbb0\",\n sageA12: \"#fdfffeed\",\n};\nconst sageDarkP3 = {\n sage1: \"color(display-p3 0.064 0.07 0.067)\",\n sage2: \"color(display-p3 0.092 0.098 0.094)\",\n sage3: \"color(display-p3 0.128 0.135 0.131)\",\n sage4: \"color(display-p3 0.155 0.164 0.159)\",\n sage5: \"color(display-p3 0.183 0.193 0.188)\",\n sage6: \"color(display-p3 0.218 0.23 0.224)\",\n sage7: \"color(display-p3 0.269 0.285 0.277)\",\n sage8: \"color(display-p3 0.362 0.382 0.373)\",\n sage9: \"color(display-p3 0.398 0.438 0.421)\",\n sage10: \"color(display-p3 0.453 0.49 0.474)\",\n sage11: \"color(display-p3 0.685 0.709 0.697)\",\n sage12: \"color(display-p3 0.927 0.933 0.93)\",\n};\nconst sageDarkP3A = {\n sageA1: \"color(display-p3 0 0 0 / 0)\",\n sageA2: \"color(display-p3 0.976 0.988 0.984 / 0.03)\",\n sageA3: \"color(display-p3 0.992 0.945 0.941 / 0.072)\",\n sageA4: \"color(display-p3 0.988 0.996 0.992 / 0.102)\",\n sageA5: \"color(display-p3 0.992 1 0.996 / 0.131)\",\n sageA6: \"color(display-p3 0.973 1 0.976 / 0.173)\",\n sageA7: \"color(display-p3 0.957 1 0.976 / 0.233)\",\n sageA8: \"color(display-p3 0.957 1 0.984 / 0.334)\",\n sageA9: \"color(display-p3 0.902 1 0.957 / 0.397)\",\n sageA10: \"color(display-p3 0.929 1 0.973 / 0.452)\",\n sageA11: \"color(display-p3 0.969 1 0.988 / 0.688)\",\n sageA12: \"color(display-p3 0.992 1 0.996 / 0.929)\",\n};\nconst oliveDark = {\n olive1: \"#111210\",\n olive2: \"#181917\",\n olive3: \"#212220\",\n olive4: \"#282a27\",\n olive5: \"#2f312e\",\n olive6: \"#383a36\",\n olive7: \"#454843\",\n olive8: \"#5c625b\",\n olive9: \"#687066\",\n olive10: \"#767d74\",\n olive11: \"#afb5ad\",\n olive12: \"#eceeec\",\n};\nconst oliveDarkA = {\n oliveA1: \"#00000000\",\n oliveA2: \"#f1f2f008\",\n oliveA3: \"#f4f5f312\",\n oliveA4: \"#f3fef21a\",\n oliveA5: \"#f2fbf122\",\n oliveA6: \"#f4faed2c\",\n oliveA7: \"#f2fced3b\",\n oliveA8: \"#edfdeb57\",\n oliveA9: \"#ebfde766\",\n oliveA10: \"#f0fdec74\",\n oliveA11: \"#f6fef4b0\",\n oliveA12: \"#fdfffded\",\n};\nconst oliveDarkP3 = {\n olive1: \"color(display-p3 0.067 0.07 0.063)\",\n olive2: \"color(display-p3 0.095 0.098 0.091)\",\n olive3: \"color(display-p3 0.131 0.135 0.126)\",\n olive4: \"color(display-p3 0.158 0.163 0.153)\",\n olive5: \"color(display-p3 0.186 0.192 0.18)\",\n olive6: \"color(display-p3 0.221 0.229 0.215)\",\n olive7: \"color(display-p3 0.273 0.284 0.266)\",\n olive8: \"color(display-p3 0.365 0.382 0.359)\",\n olive9: \"color(display-p3 0.414 0.438 0.404)\",\n olive10: \"color(display-p3 0.467 0.49 0.458)\",\n olive11: \"color(display-p3 0.69 0.709 0.682)\",\n olive12: \"color(display-p3 0.927 0.933 0.926)\",\n};\nconst oliveDarkP3A = {\n oliveA1: \"color(display-p3 0 0 0 / 0)\",\n oliveA2: \"color(display-p3 0.984 0.988 0.976 / 0.03)\",\n oliveA3: \"color(display-p3 0.992 0.996 0.988 / 0.068)\",\n oliveA4: \"color(display-p3 0.953 0.996 0.949 / 0.102)\",\n oliveA5: \"color(display-p3 0.969 1 0.965 / 0.131)\",\n oliveA6: \"color(display-p3 0.973 1 0.969 / 0.169)\",\n oliveA7: \"color(display-p3 0.98 1 0.961 / 0.228)\",\n oliveA8: \"color(display-p3 0.961 1 0.957 / 0.334)\",\n oliveA9: \"color(display-p3 0.949 1 0.922 / 0.397)\",\n oliveA10: \"color(display-p3 0.953 1 0.941 / 0.452)\",\n oliveA11: \"color(display-p3 0.976 1 0.965 / 0.688)\",\n oliveA12: \"color(display-p3 0.992 1 0.992 / 0.929)\",\n};\nconst sandDark = {\n sand1: \"#111110\",\n sand2: \"#191918\",\n sand3: \"#222221\",\n sand4: \"#2a2a28\",\n sand5: \"#31312e\",\n sand6: \"#3b3a37\",\n sand7: \"#494844\",\n sand8: \"#62605b\",\n sand9: \"#6f6d66\",\n sand10: \"#7c7b74\",\n sand11: \"#b5b3ad\",\n sand12: \"#eeeeec\",\n};\nconst sandDarkA = {\n sandA1: \"#00000000\",\n sandA2: \"#f4f4f309\",\n sandA3: \"#f6f6f513\",\n sandA4: \"#fefef31b\",\n sandA5: \"#fbfbeb23\",\n sandA6: \"#fffaed2d\",\n sandA7: \"#fffbed3c\",\n sandA8: \"#fff9eb57\",\n sandA9: \"#fffae965\",\n sandA10: \"#fffdee73\",\n sandA11: \"#fffcf4b0\",\n sandA12: \"#fffffded\",\n};\nconst sandDarkP3 = {\n sand1: \"color(display-p3 0.067 0.067 0.063)\",\n sand2: \"color(display-p3 0.098 0.098 0.094)\",\n sand3: \"color(display-p3 0.135 0.135 0.129)\",\n sand4: \"color(display-p3 0.164 0.163 0.156)\",\n sand5: \"color(display-p3 0.193 0.192 0.183)\",\n sand6: \"color(display-p3 0.23 0.229 0.217)\",\n sand7: \"color(display-p3 0.285 0.282 0.267)\",\n sand8: \"color(display-p3 0.384 0.378 0.357)\",\n sand9: \"color(display-p3 0.434 0.428 0.403)\",\n sand10: \"color(display-p3 0.487 0.481 0.456)\",\n sand11: \"color(display-p3 0.707 0.703 0.68)\",\n sand12: \"color(display-p3 0.933 0.933 0.926)\",\n};\nconst sandDarkP3A = {\n sandA1: \"color(display-p3 0 0 0 / 0)\",\n sandA2: \"color(display-p3 0.992 0.992 0.988 / 0.034)\",\n sandA3: \"color(display-p3 0.996 0.996 0.992 / 0.072)\",\n sandA4: \"color(display-p3 0.992 0.992 0.953 / 0.106)\",\n sandA5: \"color(display-p3 1 1 0.965 / 0.135)\",\n sandA6: \"color(display-p3 1 0.976 0.929 / 0.177)\",\n sandA7: \"color(display-p3 1 0.984 0.929 / 0.236)\",\n sandA8: \"color(display-p3 1 0.976 0.925 / 0.341)\",\n sandA9: \"color(display-p3 1 0.98 0.925 / 0.395)\",\n sandA10: \"color(display-p3 1 0.992 0.933 / 0.45)\",\n sandA11: \"color(display-p3 1 0.996 0.961 / 0.685)\",\n sandA12: \"color(display-p3 1 1 0.992 / 0.929)\",\n};\nconst tomatoDark = {\n tomato1: \"#181111\",\n tomato2: \"#1f1513\",\n tomato3: \"#391714\",\n tomato4: \"#4e1511\",\n tomato5: \"#5e1c16\",\n tomato6: \"#6e2920\",\n tomato7: \"#853a2d\",\n tomato8: \"#ac4d39\",\n tomato9: \"#e54d2e\",\n tomato10: \"#ec6142\",\n tomato11: \"#ff977d\",\n tomato12: \"#fbd3cb\",\n};\nconst tomatoDarkA = {\n tomatoA1: \"#f1121208\",\n tomatoA2: \"#ff55330f\",\n tomatoA3: \"#ff35232b\",\n tomatoA4: \"#fd201142\",\n tomatoA5: \"#fe332153\",\n tomatoA6: \"#ff4f3864\",\n tomatoA7: \"#fd644a7d\",\n tomatoA8: \"#fe6d4ea7\",\n tomatoA9: \"#fe5431e4\",\n tomatoA10: \"#ff6847eb\",\n tomatoA11: \"#ff977d\",\n tomatoA12: \"#ffd6cefb\",\n};\nconst tomatoDarkP3 = {\n tomato1: \"color(display-p3 0.09 0.068 0.067)\",\n tomato2: \"color(display-p3 0.115 0.084 0.076)\",\n tomato3: \"color(display-p3 0.205 0.097 0.083)\",\n tomato4: \"color(display-p3 0.282 0.099 0.077)\",\n tomato5: \"color(display-p3 0.339 0.129 0.101)\",\n tomato6: \"color(display-p3 0.398 0.179 0.141)\",\n tomato7: \"color(display-p3 0.487 0.245 0.194)\",\n tomato8: \"color(display-p3 0.629 0.322 0.248)\",\n tomato9: \"color(display-p3 0.831 0.345 0.231)\",\n tomato10: \"color(display-p3 0.862 0.415 0.298)\",\n tomato11: \"color(display-p3 1 0.585 0.455)\",\n tomato12: \"color(display-p3 0.959 0.833 0.802)\",\n};\nconst tomatoDarkP3A = {\n tomatoA1: \"color(display-p3 0.973 0.071 0.071 / 0.026)\",\n tomatoA2: \"color(display-p3 0.992 0.376 0.224 / 0.051)\",\n tomatoA3: \"color(display-p3 0.996 0.282 0.176 / 0.148)\",\n tomatoA4: \"color(display-p3 1 0.204 0.118 / 0.232)\",\n tomatoA5: \"color(display-p3 1 0.286 0.192 / 0.29)\",\n tomatoA6: \"color(display-p3 1 0.392 0.278 / 0.353)\",\n tomatoA7: \"color(display-p3 1 0.459 0.349 / 0.45)\",\n tomatoA8: \"color(display-p3 1 0.49 0.369 / 0.601)\",\n tomatoA9: \"color(display-p3 1 0.408 0.267 / 0.82)\",\n tomatoA10: \"color(display-p3 1 0.478 0.341 / 0.853)\",\n tomatoA11: \"color(display-p3 1 0.585 0.455)\",\n tomatoA12: \"color(display-p3 0.959 0.833 0.802)\",\n};\nconst redDark = {\n red1: \"#191111\",\n red2: \"#201314\",\n red3: \"#3b1219\",\n red4: \"#500f1c\",\n red5: \"#611623\",\n red6: \"#72232d\",\n red7: \"#8c333a\",\n red8: \"#b54548\",\n red9: \"#e5484d\",\n red10: \"#ec5d5e\",\n red11: \"#ff9592\",\n red12: \"#ffd1d9\",\n};\nconst redDarkA = {\n redA1: \"#f4121209\",\n redA2: \"#f22f3e11\",\n redA3: \"#ff173f2d\",\n redA4: \"#fe0a3b44\",\n redA5: \"#ff204756\",\n redA6: \"#ff3e5668\",\n redA7: \"#ff536184\",\n redA8: \"#ff5d61b0\",\n redA9: \"#fe4e54e4\",\n redA10: \"#ff6465eb\",\n redA11: \"#ff9592\",\n redA12: \"#ffd1d9\",\n};\nconst redDarkP3 = {\n red1: \"color(display-p3 0.093 0.068 0.067)\",\n red2: \"color(display-p3 0.118 0.077 0.079)\",\n red3: \"color(display-p3 0.211 0.081 0.099)\",\n red4: \"color(display-p3 0.287 0.079 0.113)\",\n red5: \"color(display-p3 0.348 0.11 0.142)\",\n red6: \"color(display-p3 0.414 0.16 0.183)\",\n red7: \"color(display-p3 0.508 0.224 0.236)\",\n red8: \"color(display-p3 0.659 0.298 0.297)\",\n red9: \"color(display-p3 0.83 0.329 0.324)\",\n red10: \"color(display-p3 0.861 0.403 0.387)\",\n red11: \"color(display-p3 1 0.57 0.55)\",\n red12: \"color(display-p3 0.971 0.826 0.852)\",\n};\nconst redDarkP3A = {\n redA1: \"color(display-p3 0.984 0.071 0.071 / 0.03)\",\n redA2: \"color(display-p3 0.996 0.282 0.282 / 0.055)\",\n redA3: \"color(display-p3 1 0.169 0.271 / 0.156)\",\n redA4: \"color(display-p3 1 0.118 0.267 / 0.236)\",\n redA5: \"color(display-p3 1 0.212 0.314 / 0.303)\",\n redA6: \"color(display-p3 1 0.318 0.38 / 0.374)\",\n redA7: \"color(display-p3 1 0.4 0.424 / 0.475)\",\n redA8: \"color(display-p3 1 0.431 0.431 / 0.635)\",\n redA9: \"color(display-p3 1 0.388 0.384 / 0.82)\",\n redA10: \"color(display-p3 1 0.463 0.447 / 0.853)\",\n redA11: \"color(display-p3 1 0.57 0.55)\",\n redA12: \"color(display-p3 0.971 0.826 0.852)\",\n};\nconst rubyDark = {\n ruby1: \"#191113\",\n ruby2: \"#1e1517\",\n ruby3: \"#3a141e\",\n ruby4: \"#4e1325\",\n ruby5: \"#5e1a2e\",\n ruby6: \"#6f2539\",\n ruby7: \"#883447\",\n ruby8: \"#b3445a\",\n ruby9: \"#e54666\",\n ruby10: \"#ec5a72\",\n ruby11: \"#ff949d\",\n ruby12: \"#fed2e1\",\n};\nconst rubyDarkA = {\n rubyA1: \"#f4124a09\",\n rubyA2: \"#fe5a7f0e\",\n rubyA3: \"#ff235d2c\",\n rubyA4: \"#fd195e42\",\n rubyA5: \"#fe2d6b53\",\n rubyA6: \"#ff447665\",\n rubyA7: \"#ff577d80\",\n rubyA8: \"#ff5c7cae\",\n rubyA9: \"#fe4c70e4\",\n rubyA10: \"#ff617beb\",\n rubyA11: \"#ff949d\",\n rubyA12: \"#ffd3e2fe\",\n};\nconst rubyDarkP3 = {\n ruby1: \"color(display-p3 0.093 0.068 0.074)\",\n ruby2: \"color(display-p3 0.113 0.083 0.089)\",\n ruby3: \"color(display-p3 0.208 0.088 0.117)\",\n ruby4: \"color(display-p3 0.279 0.092 0.147)\",\n ruby5: \"color(display-p3 0.337 0.12 0.18)\",\n ruby6: \"color(display-p3 0.401 0.166 0.223)\",\n ruby7: \"color(display-p3 0.495 0.224 0.281)\",\n ruby8: \"color(display-p3 0.652 0.295 0.359)\",\n ruby9: \"color(display-p3 0.83 0.323 0.408)\",\n ruby10: \"color(display-p3 0.857 0.392 0.455)\",\n ruby11: \"color(display-p3 1 0.57 0.59)\",\n ruby12: \"color(display-p3 0.968 0.83 0.88)\",\n};\nconst rubyDarkP3A = {\n rubyA1: \"color(display-p3 0.984 0.071 0.329 / 0.03)\",\n rubyA2: \"color(display-p3 0.992 0.376 0.529 / 0.051)\",\n rubyA3: \"color(display-p3 0.996 0.196 0.404 / 0.152)\",\n rubyA4: \"color(display-p3 1 0.173 0.416 / 0.227)\",\n rubyA5: \"color(display-p3 1 0.259 0.459 / 0.29)\",\n rubyA6: \"color(display-p3 1 0.341 0.506 / 0.358)\",\n rubyA7: \"color(display-p3 1 0.412 0.541 / 0.458)\",\n rubyA8: \"color(display-p3 1 0.431 0.537 / 0.627)\",\n rubyA9: \"color(display-p3 1 0.376 0.482 / 0.82)\",\n rubyA10: \"color(display-p3 1 0.447 0.522 / 0.849)\",\n rubyA11: \"color(display-p3 1 0.57 0.59)\",\n rubyA12: \"color(display-p3 0.968 0.83 0.88)\",\n};\nconst crimsonDark = {\n crimson1: \"#191114\",\n crimson2: \"#201318\",\n crimson3: \"#381525\",\n crimson4: \"#4d122f\",\n crimson5: \"#5c1839\",\n crimson6: \"#6d2545\",\n crimson7: \"#873356\",\n crimson8: \"#b0436e\",\n crimson9: \"#e93d82\",\n crimson10: \"#ee518a\",\n crimson11: \"#ff92ad\",\n crimson12: \"#fdd3e8\",\n};\nconst crimsonDarkA = {\n crimsonA1: \"#f4126709\",\n crimsonA2: \"#f22f7a11\",\n crimsonA3: \"#fe2a8b2a\",\n crimsonA4: \"#fd158741\",\n crimsonA5: \"#fd278f51\",\n crimsonA6: \"#fe459763\",\n crimsonA7: \"#fd559b7f\",\n crimsonA8: \"#fe5b9bab\",\n crimsonA9: \"#fe418de8\",\n crimsonA10: \"#ff5693ed\",\n crimsonA11: \"#ff92ad\",\n crimsonA12: \"#ffd5eafd\",\n};\nconst crimsonDarkP3 = {\n crimson1: \"color(display-p3 0.093 0.068 0.078)\",\n crimson2: \"color(display-p3 0.117 0.078 0.095)\",\n crimson3: \"color(display-p3 0.203 0.091 0.143)\",\n crimson4: \"color(display-p3 0.277 0.087 0.182)\",\n crimson5: \"color(display-p3 0.332 0.115 0.22)\",\n crimson6: \"color(display-p3 0.394 0.162 0.268)\",\n crimson7: \"color(display-p3 0.489 0.222 0.336)\",\n crimson8: \"color(display-p3 0.638 0.289 0.429)\",\n crimson9: \"color(display-p3 0.843 0.298 0.507)\",\n crimson10: \"color(display-p3 0.864 0.364 0.539)\",\n crimson11: \"color(display-p3 1 0.56 0.66)\",\n crimson12: \"color(display-p3 0.966 0.834 0.906)\",\n};\nconst crimsonDarkP3A = {\n crimsonA1: \"color(display-p3 0.984 0.071 0.463 / 0.03)\",\n crimsonA2: \"color(display-p3 0.996 0.282 0.569 / 0.055)\",\n crimsonA3: \"color(display-p3 0.996 0.227 0.573 / 0.148)\",\n crimsonA4: \"color(display-p3 1 0.157 0.569 / 0.227)\",\n crimsonA5: \"color(display-p3 1 0.231 0.604 / 0.286)\",\n crimsonA6: \"color(display-p3 1 0.337 0.643 / 0.349)\",\n crimsonA7: \"color(display-p3 1 0.416 0.663 / 0.454)\",\n crimsonA8: \"color(display-p3 0.996 0.427 0.651 / 0.614)\",\n crimsonA9: \"color(display-p3 1 0.345 0.596 / 0.832)\",\n crimsonA10: \"color(display-p3 1 0.42 0.62 / 0.853)\",\n crimsonA11: \"color(display-p3 1 0.56 0.66)\",\n crimsonA12: \"color(display-p3 0.966 0.834 0.906)\",\n};\nconst pinkDark = {\n pink1: \"#191117\",\n pink2: \"#21121d\",\n pink3: \"#37172f\",\n pink4: \"#4b143d\",\n pink5: \"#591c47\",\n pink6: \"#692955\",\n pink7: \"#833869\",\n pink8: \"#a84885\",\n pink9: \"#d6409f\",\n pink10: \"#de51a8\",\n pink11: \"#ff8dcc\",\n pink12: \"#fdd1ea\",\n};\nconst pinkDarkA = {\n pinkA1: \"#f412bc09\",\n pinkA2: \"#f420bb12\",\n pinkA3: \"#fe37cc29\",\n pinkA4: \"#fc1ec43f\",\n pinkA5: \"#fd35c24e\",\n pinkA6: \"#fd51c75f\",\n pinkA7: \"#fd62c87b\",\n pinkA8: \"#ff68c8a2\",\n pinkA9: \"#fe49bcd4\",\n pinkA10: \"#ff5cc0dc\",\n pinkA11: \"#ff8dcc\",\n pinkA12: \"#ffd3ecfd\",\n};\nconst pinkDarkP3 = {\n pink1: \"color(display-p3 0.093 0.068 0.089)\",\n pink2: \"color(display-p3 0.121 0.073 0.11)\",\n pink3: \"color(display-p3 0.198 0.098 0.179)\",\n pink4: \"color(display-p3 0.271 0.095 0.231)\",\n pink5: \"color(display-p3 0.32 0.127 0.273)\",\n pink6: \"color(display-p3 0.382 0.177 0.326)\",\n pink7: \"color(display-p3 0.477 0.238 0.405)\",\n pink8: \"color(display-p3 0.612 0.304 0.51)\",\n pink9: \"color(display-p3 0.775 0.297 0.61)\",\n pink10: \"color(display-p3 0.808 0.356 0.645)\",\n pink11: \"color(display-p3 1 0.535 0.78)\",\n pink12: \"color(display-p3 0.964 0.826 0.912)\",\n};\nconst pinkDarkP3A = {\n pinkA1: \"color(display-p3 0.984 0.071 0.855 / 0.03)\",\n pinkA2: \"color(display-p3 1 0.2 0.8 / 0.059)\",\n pinkA3: \"color(display-p3 1 0.294 0.886 / 0.139)\",\n pinkA4: \"color(display-p3 1 0.192 0.82 / 0.219)\",\n pinkA5: \"color(display-p3 1 0.282 0.827 / 0.274)\",\n pinkA6: \"color(display-p3 1 0.396 0.835 / 0.337)\",\n pinkA7: \"color(display-p3 1 0.459 0.831 / 0.442)\",\n pinkA8: \"color(display-p3 1 0.478 0.827 / 0.585)\",\n pinkA9: \"color(display-p3 1 0.373 0.784 / 0.761)\",\n pinkA10: \"color(display-p3 1 0.435 0.792 / 0.795)\",\n pinkA11: \"color(display-p3 1 0.535 0.78)\",\n pinkA12: \"color(display-p3 0.964 0.826 0.912)\",\n};\nconst plumDark = {\n plum1: \"#181118\",\n plum2: \"#201320\",\n plum3: \"#351a35\",\n plum4: \"#451d47\",\n plum5: \"#512454\",\n plum6: \"#5e3061\",\n plum7: \"#734079\",\n plum8: \"#92549c\",\n plum9: \"#ab4aba\",\n plum10: \"#b658c4\",\n plum11: \"#e796f3\",\n plum12: \"#f4d4f4\",\n};\nconst plumDarkA = {\n plumA1: \"#f112f108\",\n plumA2: \"#f22ff211\",\n plumA3: \"#fd4cfd27\",\n plumA4: \"#f646ff3a\",\n plumA5: \"#f455ff48\",\n plumA6: \"#f66dff56\",\n plumA7: \"#f07cfd70\",\n plumA8: \"#ee84ff95\",\n plumA9: \"#e961feb6\",\n plumA10: \"#ed70ffc0\",\n plumA11: \"#f19cfef3\",\n plumA12: \"#feddfef4\",\n};\nconst plumDarkP3 = {\n plum1: \"color(display-p3 0.09 0.068 0.092)\",\n plum2: \"color(display-p3 0.118 0.077 0.121)\",\n plum3: \"color(display-p3 0.192 0.105 0.202)\",\n plum4: \"color(display-p3 0.25 0.121 0.271)\",\n plum5: \"color(display-p3 0.293 0.152 0.319)\",\n plum6: \"color(display-p3 0.343 0.198 0.372)\",\n plum7: \"color(display-p3 0.424 0.262 0.461)\",\n plum8: \"color(display-p3 0.54 0.341 0.595)\",\n plum9: \"color(display-p3 0.624 0.313 0.708)\",\n plum10: \"color(display-p3 0.666 0.365 0.748)\",\n plum11: \"color(display-p3 0.86 0.602 0.933)\",\n plum12: \"color(display-p3 0.936 0.836 0.949)\",\n};\nconst plumDarkP3A = {\n plumA1: \"color(display-p3 0.973 0.071 0.973 / 0.026)\",\n plumA2: \"color(display-p3 0.933 0.267 1 / 0.059)\",\n plumA3: \"color(display-p3 0.918 0.333 0.996 / 0.148)\",\n plumA4: \"color(display-p3 0.91 0.318 1 / 0.219)\",\n plumA5: \"color(display-p3 0.914 0.388 1 / 0.269)\",\n plumA6: \"color(display-p3 0.906 0.463 1 / 0.328)\",\n plumA7: \"color(display-p3 0.906 0.529 1 / 0.425)\",\n plumA8: \"color(display-p3 0.906 0.553 1 / 0.568)\",\n plumA9: \"color(display-p3 0.875 0.427 1 / 0.69)\",\n plumA10: \"color(display-p3 0.886 0.471 0.996 / 0.732)\",\n plumA11: \"color(display-p3 0.86 0.602 0.933)\",\n plumA12: \"color(display-p3 0.936 0.836 0.949)\",\n};\nconst purpleDark = {\n purple1: \"#18111b\",\n purple2: \"#1e1523\",\n purple3: \"#301c3b\",\n purple4: \"#3d224e\",\n purple5: \"#48295c\",\n purple6: \"#54346b\",\n purple7: \"#664282\",\n purple8: \"#8457aa\",\n purple9: \"#8e4ec6\",\n purple10: \"#9a5cd0\",\n purple11: \"#d19dff\",\n purple12: \"#ecd9fa\",\n};\nconst purpleDarkA = {\n purpleA1: \"#b412f90b\",\n purpleA2: \"#b744f714\",\n purpleA3: \"#c150ff2d\",\n purpleA4: \"#bb53fd42\",\n purpleA5: \"#be5cfd51\",\n purpleA6: \"#c16dfd61\",\n purpleA7: \"#c378fd7a\",\n purpleA8: \"#c47effa4\",\n purpleA9: \"#b661ffc2\",\n purpleA10: \"#bc6fffcd\",\n purpleA11: \"#d19dff\",\n purpleA12: \"#f1ddfffa\",\n};\nconst purpleDarkP3 = {\n purple1: \"color(display-p3 0.09 0.068 0.103)\",\n purple2: \"color(display-p3 0.113 0.082 0.134)\",\n purple3: \"color(display-p3 0.175 0.112 0.224)\",\n purple4: \"color(display-p3 0.224 0.137 0.297)\",\n purple5: \"color(display-p3 0.264 0.167 0.349)\",\n purple6: \"color(display-p3 0.311 0.208 0.406)\",\n purple7: \"color(display-p3 0.381 0.266 0.496)\",\n purple8: \"color(display-p3 0.49 0.349 0.649)\",\n purple9: \"color(display-p3 0.523 0.318 0.751)\",\n purple10: \"color(display-p3 0.57 0.373 0.791)\",\n purple11: \"color(display-p3 0.8 0.62 1)\",\n purple12: \"color(display-p3 0.913 0.854 0.971)\",\n};\nconst purpleDarkP3A = {\n purpleA1: \"color(display-p3 0.686 0.071 0.996 / 0.038)\",\n purpleA2: \"color(display-p3 0.722 0.286 0.996 / 0.072)\",\n purpleA3: \"color(display-p3 0.718 0.349 0.996 / 0.169)\",\n purpleA4: \"color(display-p3 0.702 0.353 1 / 0.248)\",\n purpleA5: \"color(display-p3 0.718 0.404 1 / 0.303)\",\n purpleA6: \"color(display-p3 0.733 0.455 1 / 0.366)\",\n purpleA7: \"color(display-p3 0.753 0.506 1 / 0.458)\",\n purpleA8: \"color(display-p3 0.749 0.522 1 / 0.622)\",\n purpleA9: \"color(display-p3 0.686 0.408 1 / 0.736)\",\n purpleA10: \"color(display-p3 0.71 0.459 1 / 0.778)\",\n purpleA11: \"color(display-p3 0.8 0.62 1)\",\n purpleA12: \"color(display-p3 0.913 0.854 0.971)\",\n};\nconst violetDark = {\n violet1: \"#14121f\",\n violet2: \"#1b1525\",\n violet3: \"#291f43\",\n violet4: \"#33255b\",\n violet5: \"#3c2e69\",\n violet6: \"#473876\",\n violet7: \"#56468b\",\n violet8: \"#6958ad\",\n violet9: \"#6e56cf\",\n violet10: \"#7d66d9\",\n violet11: \"#baa7ff\",\n violet12: \"#e2ddfe\",\n};\nconst violetDarkA = {\n violetA1: \"#4422ff0f\",\n violetA2: \"#853ff916\",\n violetA3: \"#8354fe36\",\n violetA4: \"#7d51fd50\",\n violetA5: \"#845ffd5f\",\n violetA6: \"#8f6cfd6d\",\n violetA7: \"#9879ff83\",\n violetA8: \"#977dfea8\",\n violetA9: \"#8668ffcc\",\n violetA10: \"#9176fed7\",\n violetA11: \"#baa7ff\",\n violetA12: \"#e3defffe\",\n};\nconst violetDarkP3 = {\n violet1: \"color(display-p3 0.077 0.071 0.118)\",\n violet2: \"color(display-p3 0.101 0.084 0.141)\",\n violet3: \"color(display-p3 0.154 0.123 0.256)\",\n violet4: \"color(display-p3 0.191 0.148 0.345)\",\n violet5: \"color(display-p3 0.226 0.182 0.396)\",\n violet6: \"color(display-p3 0.269 0.223 0.449)\",\n violet7: \"color(display-p3 0.326 0.277 0.53)\",\n violet8: \"color(display-p3 0.399 0.346 0.656)\",\n violet9: \"color(display-p3 0.417 0.341 0.784)\",\n violet10: \"color(display-p3 0.477 0.402 0.823)\",\n violet11: \"color(display-p3 0.72 0.65 1)\",\n violet12: \"color(display-p3 0.883 0.867 0.986)\",\n};\nconst violetDarkP3A = {\n violetA1: \"color(display-p3 0.282 0.141 0.996 / 0.055)\",\n violetA2: \"color(display-p3 0.51 0.263 1 / 0.08)\",\n violetA3: \"color(display-p3 0.494 0.337 0.996 / 0.202)\",\n violetA4: \"color(display-p3 0.49 0.345 1 / 0.299)\",\n violetA5: \"color(display-p3 0.525 0.392 1 / 0.353)\",\n violetA6: \"color(display-p3 0.569 0.455 1 / 0.408)\",\n violetA7: \"color(display-p3 0.588 0.494 1 / 0.496)\",\n violetA8: \"color(display-p3 0.596 0.51 1 / 0.631)\",\n violetA9: \"color(display-p3 0.522 0.424 1 / 0.769)\",\n violetA10: \"color(display-p3 0.576 0.482 1 / 0.811)\",\n violetA11: \"color(display-p3 0.72 0.65 1)\",\n violetA12: \"color(display-p3 0.883 0.867 0.986)\",\n};\nconst irisDark = {\n iris1: \"#13131e\",\n iris2: \"#171625\",\n iris3: \"#202248\",\n iris4: \"#262a65\",\n iris5: \"#303374\",\n iris6: \"#3d3e82\",\n iris7: \"#4a4a95\",\n iris8: \"#5958b1\",\n iris9: \"#5b5bd6\",\n iris10: \"#6e6ade\",\n iris11: \"#b1a9ff\",\n iris12: \"#e0dffe\",\n};\nconst irisDarkA = {\n irisA1: \"#3636fe0e\",\n irisA2: \"#564bf916\",\n irisA3: \"#525bff3b\",\n irisA4: \"#4d58ff5a\",\n irisA5: \"#5b62fd6b\",\n irisA6: \"#6d6ffd7a\",\n irisA7: \"#7777fe8e\",\n irisA8: \"#7b7afeac\",\n irisA9: \"#6a6afed4\",\n irisA10: \"#7d79ffdc\",\n irisA11: \"#b1a9ff\",\n irisA12: \"#e1e0fffe\",\n};\nconst irisDarkP3 = {\n iris1: \"color(display-p3 0.075 0.075 0.114)\",\n iris2: \"color(display-p3 0.089 0.086 0.14)\",\n iris3: \"color(display-p3 0.128 0.134 0.272)\",\n iris4: \"color(display-p3 0.153 0.165 0.382)\",\n iris5: \"color(display-p3 0.192 0.201 0.44)\",\n iris6: \"color(display-p3 0.239 0.241 0.491)\",\n iris7: \"color(display-p3 0.291 0.289 0.565)\",\n iris8: \"color(display-p3 0.35 0.345 0.673)\",\n iris9: \"color(display-p3 0.357 0.357 0.81)\",\n iris10: \"color(display-p3 0.428 0.416 0.843)\",\n iris11: \"color(display-p3 0.685 0.662 1)\",\n iris12: \"color(display-p3 0.878 0.875 0.986)\",\n};\nconst irisDarkP3A = {\n irisA1: \"color(display-p3 0.224 0.224 0.992 / 0.051)\",\n irisA2: \"color(display-p3 0.361 0.314 1 / 0.08)\",\n irisA3: \"color(display-p3 0.357 0.373 1 / 0.219)\",\n irisA4: \"color(display-p3 0.325 0.361 1 / 0.337)\",\n irisA5: \"color(display-p3 0.38 0.4 1 / 0.4)\",\n irisA6: \"color(display-p3 0.447 0.447 1 / 0.454)\",\n irisA7: \"color(display-p3 0.486 0.486 1 / 0.534)\",\n irisA8: \"color(display-p3 0.502 0.494 1 / 0.652)\",\n irisA9: \"color(display-p3 0.431 0.431 1 / 0.799)\",\n irisA10: \"color(display-p3 0.502 0.486 1 / 0.832)\",\n irisA11: \"color(display-p3 0.685 0.662 1)\",\n irisA12: \"color(display-p3 0.878 0.875 0.986)\",\n};\nconst indigoDark = {\n indigo1: \"#11131f\",\n indigo2: \"#141726\",\n indigo3: \"#182449\",\n indigo4: \"#1d2e62\",\n indigo5: \"#253974\",\n indigo6: \"#304384\",\n indigo7: \"#3a4f97\",\n indigo8: \"#435db1\",\n indigo9: \"#3e63dd\",\n indigo10: \"#5472e4\",\n indigo11: \"#9eb1ff\",\n indigo12: \"#d6e1ff\",\n};\nconst indigoDarkA = {\n indigoA1: \"#1133ff0f\",\n indigoA2: \"#3354fa17\",\n indigoA3: \"#2f62ff3c\",\n indigoA4: \"#3566ff57\",\n indigoA5: \"#4171fd6b\",\n indigoA6: \"#5178fd7c\",\n indigoA7: \"#5a7fff90\",\n indigoA8: \"#5b81feac\",\n indigoA9: \"#4671ffdb\",\n indigoA10: \"#5c7efee3\",\n indigoA11: \"#9eb1ff\",\n indigoA12: \"#d6e1ff\",\n};\nconst indigoDarkP3 = {\n indigo1: \"color(display-p3 0.068 0.074 0.118)\",\n indigo2: \"color(display-p3 0.081 0.089 0.144)\",\n indigo3: \"color(display-p3 0.105 0.141 0.275)\",\n indigo4: \"color(display-p3 0.129 0.18 0.369)\",\n indigo5: \"color(display-p3 0.163 0.22 0.439)\",\n indigo6: \"color(display-p3 0.203 0.262 0.5)\",\n indigo7: \"color(display-p3 0.245 0.309 0.575)\",\n indigo8: \"color(display-p3 0.285 0.362 0.674)\",\n indigo9: \"color(display-p3 0.276 0.384 0.837)\",\n indigo10: \"color(display-p3 0.354 0.445 0.866)\",\n indigo11: \"color(display-p3 0.63 0.69 1)\",\n indigo12: \"color(display-p3 0.848 0.881 0.99)\",\n};\nconst indigoDarkP3A = {\n indigoA1: \"color(display-p3 0.071 0.212 0.996 / 0.055)\",\n indigoA2: \"color(display-p3 0.251 0.345 0.988 / 0.085)\",\n indigoA3: \"color(display-p3 0.243 0.404 1 / 0.223)\",\n indigoA4: \"color(display-p3 0.263 0.42 1 / 0.324)\",\n indigoA5: \"color(display-p3 0.314 0.451 1 / 0.4)\",\n indigoA6: \"color(display-p3 0.361 0.49 1 / 0.467)\",\n indigoA7: \"color(display-p3 0.388 0.51 1 / 0.547)\",\n indigoA8: \"color(display-p3 0.404 0.518 1 / 0.652)\",\n indigoA9: \"color(display-p3 0.318 0.451 1 / 0.824)\",\n indigoA10: \"color(display-p3 0.404 0.506 1 / 0.858)\",\n indigoA11: \"color(display-p3 0.63 0.69 1)\",\n indigoA12: \"color(display-p3 0.848 0.881 0.99)\",\n};\nconst blueDark = {\n blue1: \"#0d1520\",\n blue2: \"#111927\",\n blue3: \"#0d2847\",\n blue4: \"#003362\",\n blue5: \"#004074\",\n blue6: \"#104d87\",\n blue7: \"#205d9e\",\n blue8: \"#2870bd\",\n blue9: \"#0090ff\",\n blue10: \"#3b9eff\",\n blue11: \"#70b8ff\",\n blue12: \"#c2e6ff\",\n};\nconst blueDarkA = {\n blueA1: \"#004df211\",\n blueA2: \"#1166fb18\",\n blueA3: \"#0077ff3a\",\n blueA4: \"#0075ff57\",\n blueA5: \"#0081fd6b\",\n blueA6: \"#0f89fd7f\",\n blueA7: \"#2a91fe98\",\n blueA8: \"#3094feb9\",\n blueA9: \"#0090ff\",\n blueA10: \"#3b9eff\",\n blueA11: \"#70b8ff\",\n blueA12: \"#c2e6ff\",\n};\nconst blueDarkP3 = {\n blue1: \"color(display-p3 0.057 0.081 0.122)\",\n blue2: \"color(display-p3 0.072 0.098 0.147)\",\n blue3: \"color(display-p3 0.078 0.154 0.27)\",\n blue4: \"color(display-p3 0.033 0.197 0.37)\",\n blue5: \"color(display-p3 0.08 0.245 0.441)\",\n blue6: \"color(display-p3 0.14 0.298 0.511)\",\n blue7: \"color(display-p3 0.195 0.361 0.6)\",\n blue8: \"color(display-p3 0.239 0.434 0.72)\",\n blue9: \"color(display-p3 0.247 0.556 0.969)\",\n blue10: \"color(display-p3 0.344 0.612 0.973)\",\n blue11: \"color(display-p3 0.49 0.72 1)\",\n blue12: \"color(display-p3 0.788 0.898 0.99)\",\n};\nconst blueDarkP3A = {\n blueA1: \"color(display-p3 0 0.333 1 / 0.059)\",\n blueA2: \"color(display-p3 0.114 0.435 0.988 / 0.085)\",\n blueA3: \"color(display-p3 0.122 0.463 1 / 0.219)\",\n blueA4: \"color(display-p3 0 0.467 1 / 0.324)\",\n blueA5: \"color(display-p3 0.098 0.51 1 / 0.4)\",\n blueA6: \"color(display-p3 0.224 0.557 1 / 0.475)\",\n blueA7: \"color(display-p3 0.294 0.584 1 / 0.572)\",\n blueA8: \"color(display-p3 0.314 0.592 1 / 0.702)\",\n blueA9: \"color(display-p3 0.251 0.573 0.996 / 0.967)\",\n blueA10: \"color(display-p3 0.357 0.631 1 / 0.971)\",\n blueA11: \"color(display-p3 0.49 0.72 1)\",\n blueA12: \"color(display-p3 0.788 0.898 0.99)\",\n};\nconst cyanDark = {\n cyan1: \"#0b161a\",\n cyan2: \"#101b20\",\n cyan3: \"#082c36\",\n cyan4: \"#003848\",\n cyan5: \"#004558\",\n cyan6: \"#045468\",\n cyan7: \"#12677e\",\n cyan8: \"#11809c\",\n cyan9: \"#00a2c7\",\n cyan10: \"#23afd0\",\n cyan11: \"#4ccce6\",\n cyan12: \"#b6ecf7\",\n};\nconst cyanDarkA = {\n cyanA1: \"#0091f70a\",\n cyanA2: \"#02a7f211\",\n cyanA3: \"#00befd28\",\n cyanA4: \"#00baff3b\",\n cyanA5: \"#00befd4d\",\n cyanA6: \"#00c7fd5e\",\n cyanA7: \"#14cdff75\",\n cyanA8: \"#11cfff95\",\n cyanA9: \"#00cfffc3\",\n cyanA10: \"#28d6ffcd\",\n cyanA11: \"#52e1fee5\",\n cyanA12: \"#bbf3fef7\",\n};\nconst cyanDarkP3 = {\n cyan1: \"color(display-p3 0.053 0.085 0.098)\",\n cyan2: \"color(display-p3 0.072 0.105 0.122)\",\n cyan3: \"color(display-p3 0.073 0.168 0.209)\",\n cyan4: \"color(display-p3 0.063 0.216 0.277)\",\n cyan5: \"color(display-p3 0.091 0.267 0.336)\",\n cyan6: \"color(display-p3 0.137 0.324 0.4)\",\n cyan7: \"color(display-p3 0.186 0.398 0.484)\",\n cyan8: \"color(display-p3 0.23 0.496 0.6)\",\n cyan9: \"color(display-p3 0.282 0.627 0.765)\",\n cyan10: \"color(display-p3 0.331 0.675 0.801)\",\n cyan11: \"color(display-p3 0.446 0.79 0.887)\",\n cyan12: \"color(display-p3 0.757 0.919 0.962)\",\n};\nconst cyanDarkP3A = {\n cyanA1: \"color(display-p3 0 0.647 0.992 / 0.034)\",\n cyanA2: \"color(display-p3 0.133 0.733 1 / 0.059)\",\n cyanA3: \"color(display-p3 0.122 0.741 0.996 / 0.152)\",\n cyanA4: \"color(display-p3 0.051 0.725 1 / 0.227)\",\n cyanA5: \"color(display-p3 0.149 0.757 1 / 0.29)\",\n cyanA6: \"color(display-p3 0.267 0.792 1 / 0.358)\",\n cyanA7: \"color(display-p3 0.333 0.808 1 / 0.446)\",\n cyanA8: \"color(display-p3 0.357 0.816 1 / 0.572)\",\n cyanA9: \"color(display-p3 0.357 0.82 1 / 0.748)\",\n cyanA10: \"color(display-p3 0.4 0.839 1 / 0.786)\",\n cyanA11: \"color(display-p3 0.446 0.79 0.887)\",\n cyanA12: \"color(display-p3 0.757 0.919 0.962)\",\n};\nconst tealDark = {\n teal1: \"#0d1514\",\n teal2: \"#111c1b\",\n teal3: \"#0d2d2a\",\n teal4: \"#023b37\",\n teal5: \"#084843\",\n teal6: \"#145750\",\n teal7: \"#1c6961\",\n teal8: \"#207e73\",\n teal9: \"#12a594\",\n teal10: \"#0eb39e\",\n teal11: \"#0bd8b6\",\n teal12: \"#adf0dd\",\n};\nconst tealDarkA = {\n tealA1: \"#00deab05\",\n tealA2: \"#12fbe60c\",\n tealA3: \"#00ffe61e\",\n tealA4: \"#00ffe92d\",\n tealA5: \"#00ffea3b\",\n tealA6: \"#1cffe84b\",\n tealA7: \"#2efde85f\",\n tealA8: \"#32ffe775\",\n tealA9: \"#13ffe49f\",\n tealA10: \"#0dffe0ae\",\n tealA11: \"#0afed5d6\",\n tealA12: \"#b8ffebef\",\n};\nconst tealDarkP3 = {\n teal1: \"color(display-p3 0.059 0.083 0.079)\",\n teal2: \"color(display-p3 0.075 0.11 0.107)\",\n teal3: \"color(display-p3 0.087 0.175 0.165)\",\n teal4: \"color(display-p3 0.087 0.227 0.214)\",\n teal5: \"color(display-p3 0.12 0.277 0.261)\",\n teal6: \"color(display-p3 0.162 0.335 0.314)\",\n teal7: \"color(display-p3 0.205 0.406 0.379)\",\n teal8: \"color(display-p3 0.245 0.489 0.453)\",\n teal9: \"color(display-p3 0.297 0.637 0.581)\",\n teal10: \"color(display-p3 0.319 0.69 0.62)\",\n teal11: \"color(display-p3 0.388 0.835 0.719)\",\n teal12: \"color(display-p3 0.734 0.934 0.87)\",\n};\nconst tealDarkP3A = {\n tealA1: \"color(display-p3 0 0.992 0.761 / 0.017)\",\n tealA2: \"color(display-p3 0.235 0.988 0.902 / 0.047)\",\n tealA3: \"color(display-p3 0.235 1 0.898 / 0.118)\",\n tealA4: \"color(display-p3 0.18 0.996 0.929 / 0.173)\",\n tealA5: \"color(display-p3 0.31 1 0.933 / 0.227)\",\n tealA6: \"color(display-p3 0.396 1 0.933 / 0.286)\",\n tealA7: \"color(display-p3 0.443 1 0.925 / 0.366)\",\n tealA8: \"color(display-p3 0.459 1 0.925 / 0.454)\",\n tealA9: \"color(display-p3 0.443 0.996 0.906 / 0.61)\",\n tealA10: \"color(display-p3 0.439 0.996 0.89 / 0.669)\",\n tealA11: \"color(display-p3 0.388 0.835 0.719)\",\n tealA12: \"color(display-p3 0.734 0.934 0.87)\",\n};\nconst jadeDark = {\n jade1: \"#0d1512\",\n jade2: \"#121c18\",\n jade3: \"#0f2e22\",\n jade4: \"#0b3b2c\",\n jade5: \"#114837\",\n jade6: \"#1b5745\",\n jade7: \"#246854\",\n jade8: \"#2a7e68\",\n jade9: \"#29a383\",\n jade10: \"#27b08b\",\n jade11: \"#1fd8a4\",\n jade12: \"#adf0d4\",\n};\nconst jadeDarkA = {\n jadeA1: \"#00de4505\",\n jadeA2: \"#27fba60c\",\n jadeA3: \"#02f99920\",\n jadeA4: \"#00ffaa2d\",\n jadeA5: \"#11ffb63b\",\n jadeA6: \"#34ffc24b\",\n jadeA7: \"#45fdc75e\",\n jadeA8: \"#48ffcf75\",\n jadeA9: \"#38feca9d\",\n jadeA10: \"#31fec7ab\",\n jadeA11: \"#21fec0d6\",\n jadeA12: \"#b8ffe1ef\",\n};\nconst jadeDarkP3 = {\n jade1: \"color(display-p3 0.059 0.083 0.071)\",\n jade2: \"color(display-p3 0.078 0.11 0.094)\",\n jade3: \"color(display-p3 0.091 0.176 0.138)\",\n jade4: \"color(display-p3 0.102 0.228 0.177)\",\n jade5: \"color(display-p3 0.133 0.279 0.221)\",\n jade6: \"color(display-p3 0.174 0.334 0.273)\",\n jade7: \"color(display-p3 0.219 0.402 0.335)\",\n jade8: \"color(display-p3 0.263 0.488 0.411)\",\n jade9: \"color(display-p3 0.319 0.63 0.521)\",\n jade10: \"color(display-p3 0.338 0.68 0.555)\",\n jade11: \"color(display-p3 0.4 0.835 0.656)\",\n jade12: \"color(display-p3 0.734 0.934 0.838)\",\n};\nconst jadeDarkP3A = {\n jadeA1: \"color(display-p3 0 0.992 0.298 / 0.017)\",\n jadeA2: \"color(display-p3 0.318 0.988 0.651 / 0.047)\",\n jadeA3: \"color(display-p3 0.267 1 0.667 / 0.118)\",\n jadeA4: \"color(display-p3 0.275 0.996 0.702 / 0.173)\",\n jadeA5: \"color(display-p3 0.361 1 0.741 / 0.227)\",\n jadeA6: \"color(display-p3 0.439 1 0.796 / 0.286)\",\n jadeA7: \"color(display-p3 0.49 1 0.804 / 0.362)\",\n jadeA8: \"color(display-p3 0.506 1 0.835 / 0.45)\",\n jadeA9: \"color(display-p3 0.478 0.996 0.816 / 0.606)\",\n jadeA10: \"color(display-p3 0.478 1 0.816 / 0.656)\",\n jadeA11: \"color(display-p3 0.4 0.835 0.656)\",\n jadeA12: \"color(display-p3 0.734 0.934 0.838)\",\n};\nconst greenDark = {\n green1: \"#0e1512\",\n green2: \"#121b17\",\n green3: \"#132d21\",\n green4: \"#113b29\",\n green5: \"#174933\",\n green6: \"#20573e\",\n green7: \"#28684a\",\n green8: \"#2f7c57\",\n green9: \"#30a46c\",\n green10: \"#33b074\",\n green11: \"#3dd68c\",\n green12: \"#b1f1cb\",\n};\nconst greenDarkA = {\n greenA1: \"#00de4505\",\n greenA2: \"#29f99d0b\",\n greenA3: \"#22ff991e\",\n greenA4: \"#11ff992d\",\n greenA5: \"#2bffa23c\",\n greenA6: \"#44ffaa4b\",\n greenA7: \"#50fdac5e\",\n greenA8: \"#54ffad73\",\n greenA9: \"#44ffa49e\",\n greenA10: \"#43fea4ab\",\n greenA11: \"#46fea5d4\",\n greenA12: \"#bbffd7f0\",\n};\nconst greenDarkP3 = {\n green1: \"color(display-p3 0.062 0.083 0.071)\",\n green2: \"color(display-p3 0.079 0.106 0.09)\",\n green3: \"color(display-p3 0.1 0.173 0.133)\",\n green4: \"color(display-p3 0.115 0.229 0.166)\",\n green5: \"color(display-p3 0.147 0.282 0.206)\",\n green6: \"color(display-p3 0.185 0.338 0.25)\",\n green7: \"color(display-p3 0.227 0.403 0.298)\",\n green8: \"color(display-p3 0.27 0.479 0.351)\",\n green9: \"color(display-p3 0.332 0.634 0.442)\",\n green10: \"color(display-p3 0.357 0.682 0.474)\",\n green11: \"color(display-p3 0.434 0.828 0.573)\",\n green12: \"color(display-p3 0.747 0.938 0.807)\",\n};\nconst greenDarkP3A = {\n greenA1: \"color(display-p3 0 0.992 0.298 / 0.017)\",\n greenA2: \"color(display-p3 0.341 0.98 0.616 / 0.043)\",\n greenA3: \"color(display-p3 0.376 0.996 0.655 / 0.114)\",\n greenA4: \"color(display-p3 0.341 0.996 0.635 / 0.173)\",\n greenA5: \"color(display-p3 0.408 1 0.678 / 0.232)\",\n greenA6: \"color(display-p3 0.475 1 0.706 / 0.29)\",\n greenA7: \"color(display-p3 0.514 1 0.706 / 0.362)\",\n greenA8: \"color(display-p3 0.529 1 0.718 / 0.442)\",\n greenA9: \"color(display-p3 0.502 0.996 0.682 / 0.61)\",\n greenA10: \"color(display-p3 0.506 1 0.682 / 0.66)\",\n greenA11: \"color(display-p3 0.434 0.828 0.573)\",\n greenA12: \"color(display-p3 0.747 0.938 0.807)\",\n};\nconst grassDark = {\n grass1: \"#0e1511\",\n grass2: \"#141a15\",\n grass3: \"#1b2a1e\",\n grass4: \"#1d3a24\",\n grass5: \"#25482d\",\n grass6: \"#2d5736\",\n grass7: \"#366740\",\n grass8: \"#3e7949\",\n grass9: \"#46a758\",\n grass10: \"#53b365\",\n grass11: \"#71d083\",\n grass12: \"#c2f0c2\",\n};\nconst grassDarkA = {\n grassA1: \"#00de1205\",\n grassA2: \"#5ef7780a\",\n grassA3: \"#70fe8c1b\",\n grassA4: \"#57ff802c\",\n grassA5: \"#68ff8b3b\",\n grassA6: \"#71ff8f4b\",\n grassA7: \"#77fd925d\",\n grassA8: \"#77fd9070\",\n grassA9: \"#65ff82a1\",\n grassA10: \"#72ff8dae\",\n grassA11: \"#89ff9fcd\",\n grassA12: \"#ceffceef\",\n};\nconst grassDarkP3 = {\n grass1: \"color(display-p3 0.062 0.083 0.067)\",\n grass2: \"color(display-p3 0.083 0.103 0.085)\",\n grass3: \"color(display-p3 0.118 0.163 0.122)\",\n grass4: \"color(display-p3 0.142 0.225 0.15)\",\n grass5: \"color(display-p3 0.178 0.279 0.186)\",\n grass6: \"color(display-p3 0.217 0.337 0.224)\",\n grass7: \"color(display-p3 0.258 0.4 0.264)\",\n grass8: \"color(display-p3 0.302 0.47 0.305)\",\n grass9: \"color(display-p3 0.38 0.647 0.378)\",\n grass10: \"color(display-p3 0.426 0.694 0.426)\",\n grass11: \"color(display-p3 0.535 0.807 0.542)\",\n grass12: \"color(display-p3 0.797 0.936 0.776)\",\n};\nconst grassDarkP3A = {\n grassA1: \"color(display-p3 0 0.992 0.071 / 0.017)\",\n grassA2: \"color(display-p3 0.482 0.996 0.584 / 0.038)\",\n grassA3: \"color(display-p3 0.549 0.992 0.588 / 0.106)\",\n grassA4: \"color(display-p3 0.51 0.996 0.557 / 0.169)\",\n grassA5: \"color(display-p3 0.553 1 0.588 / 0.227)\",\n grassA6: \"color(display-p3 0.584 1 0.608 / 0.29)\",\n grassA7: \"color(display-p3 0.604 1 0.616 / 0.358)\",\n grassA8: \"color(display-p3 0.608 1 0.62 / 0.433)\",\n grassA9: \"color(display-p3 0.573 1 0.569 / 0.622)\",\n grassA10: \"color(display-p3 0.6 0.996 0.6 / 0.673)\",\n grassA11: \"color(display-p3 0.535 0.807 0.542)\",\n grassA12: \"color(display-p3 0.797 0.936 0.776)\",\n};\nconst brownDark = {\n brown1: \"#12110f\",\n brown2: \"#1c1816\",\n brown3: \"#28211d\",\n brown4: \"#322922\",\n brown5: \"#3e3128\",\n brown6: \"#4d3c2f\",\n brown7: \"#614a39\",\n brown8: \"#7c5f46\",\n brown9: \"#ad7f58\",\n brown10: \"#b88c67\",\n brown11: \"#dbb594\",\n brown12: \"#f2e1ca\",\n};\nconst brownDarkA = {\n brownA1: \"#91110002\",\n brownA2: \"#fba67c0c\",\n brownA3: \"#fcb58c19\",\n brownA4: \"#fbbb8a24\",\n brownA5: \"#fcb88931\",\n brownA6: \"#fdba8741\",\n brownA7: \"#ffbb8856\",\n brownA8: \"#ffbe8773\",\n brownA9: \"#feb87da8\",\n brownA10: \"#ffc18cb3\",\n brownA11: \"#fed1aad9\",\n brownA12: \"#feecd4f2\",\n};\nconst brownDarkP3 = {\n brown1: \"color(display-p3 0.071 0.067 0.059)\",\n brown2: \"color(display-p3 0.107 0.095 0.087)\",\n brown3: \"color(display-p3 0.151 0.13 0.115)\",\n brown4: \"color(display-p3 0.191 0.161 0.138)\",\n brown5: \"color(display-p3 0.235 0.194 0.162)\",\n brown6: \"color(display-p3 0.291 0.237 0.192)\",\n brown7: \"color(display-p3 0.365 0.295 0.232)\",\n brown8: \"color(display-p3 0.469 0.377 0.287)\",\n brown9: \"color(display-p3 0.651 0.505 0.368)\",\n brown10: \"color(display-p3 0.697 0.557 0.423)\",\n brown11: \"color(display-p3 0.835 0.715 0.597)\",\n brown12: \"color(display-p3 0.938 0.885 0.802)\",\n};\nconst brownDarkP3A = {\n brownA1: \"color(display-p3 0.855 0.071 0 / 0.005)\",\n brownA2: \"color(display-p3 0.98 0.706 0.525 / 0.043)\",\n brownA3: \"color(display-p3 0.996 0.745 0.576 / 0.093)\",\n brownA4: \"color(display-p3 1 0.765 0.592 / 0.135)\",\n brownA5: \"color(display-p3 1 0.761 0.588 / 0.181)\",\n brownA6: \"color(display-p3 1 0.773 0.592 / 0.24)\",\n brownA7: \"color(display-p3 0.996 0.776 0.58 / 0.32)\",\n brownA8: \"color(display-p3 1 0.78 0.573 / 0.433)\",\n brownA9: \"color(display-p3 1 0.769 0.549 / 0.627)\",\n brownA10: \"color(display-p3 1 0.792 0.596 / 0.677)\",\n brownA11: \"color(display-p3 0.835 0.715 0.597)\",\n brownA12: \"color(display-p3 0.938 0.885 0.802)\",\n};\nconst bronzeDark = {\n bronze1: \"#141110\",\n bronze2: \"#1c1917\",\n bronze3: \"#262220\",\n bronze4: \"#302a27\",\n bronze5: \"#3b3330\",\n bronze6: \"#493e3a\",\n bronze7: \"#5a4c47\",\n bronze8: \"#6f5f58\",\n bronze9: \"#a18072\",\n bronze10: \"#ae8c7e\",\n bronze11: \"#d4b3a5\",\n bronze12: \"#ede0d9\",\n};\nconst bronzeDarkA = {\n bronzeA1: \"#d1110004\",\n bronzeA2: \"#fbbc910c\",\n bronzeA3: \"#faceb817\",\n bronzeA4: \"#facdb622\",\n bronzeA5: \"#ffd2c12d\",\n bronzeA6: \"#ffd1c03c\",\n bronzeA7: \"#fdd0c04f\",\n bronzeA8: \"#ffd6c565\",\n bronzeA9: \"#fec7b09b\",\n bronzeA10: \"#fecab5a9\",\n bronzeA11: \"#ffd7c6d1\",\n bronzeA12: \"#fff1e9ec\",\n};\nconst bronzeDarkP3 = {\n bronze1: \"color(display-p3 0.076 0.067 0.063)\",\n bronze2: \"color(display-p3 0.106 0.097 0.093)\",\n bronze3: \"color(display-p3 0.147 0.132 0.125)\",\n bronze4: \"color(display-p3 0.185 0.166 0.156)\",\n bronze5: \"color(display-p3 0.227 0.202 0.19)\",\n bronze6: \"color(display-p3 0.278 0.246 0.23)\",\n bronze7: \"color(display-p3 0.343 0.302 0.281)\",\n bronze8: \"color(display-p3 0.426 0.374 0.347)\",\n bronze9: \"color(display-p3 0.611 0.507 0.455)\",\n bronze10: \"color(display-p3 0.66 0.556 0.504)\",\n bronze11: \"color(display-p3 0.81 0.707 0.655)\",\n bronze12: \"color(display-p3 0.921 0.88 0.854)\",\n};\nconst bronzeDarkP3A = {\n bronzeA1: \"color(display-p3 0.941 0.067 0 / 0.009)\",\n bronzeA2: \"color(display-p3 0.98 0.8 0.706 / 0.043)\",\n bronzeA3: \"color(display-p3 0.988 0.851 0.761 / 0.085)\",\n bronzeA4: \"color(display-p3 0.996 0.839 0.78 / 0.127)\",\n bronzeA5: \"color(display-p3 0.996 0.863 0.773 / 0.173)\",\n bronzeA6: \"color(display-p3 1 0.863 0.796 / 0.227)\",\n bronzeA7: \"color(display-p3 1 0.867 0.8 / 0.295)\",\n bronzeA8: \"color(display-p3 1 0.859 0.788 / 0.387)\",\n bronzeA9: \"color(display-p3 1 0.82 0.733 / 0.585)\",\n bronzeA10: \"color(display-p3 1 0.839 0.761 / 0.635)\",\n bronzeA11: \"color(display-p3 0.81 0.707 0.655)\",\n bronzeA12: \"color(display-p3 0.921 0.88 0.854)\",\n};\nconst goldDark = {\n gold1: \"#121211\",\n gold2: \"#1b1a17\",\n gold3: \"#24231f\",\n gold4: \"#2d2b26\",\n gold5: \"#38352e\",\n gold6: \"#444039\",\n gold7: \"#544f46\",\n gold8: \"#696256\",\n gold9: \"#978365\",\n gold10: \"#a39073\",\n gold11: \"#cbb99f\",\n gold12: \"#e8e2d9\",\n};\nconst goldDarkA = {\n goldA1: \"#91911102\",\n goldA2: \"#f9e29d0b\",\n goldA3: \"#f8ecbb15\",\n goldA4: \"#ffeec41e\",\n goldA5: \"#feecc22a\",\n goldA6: \"#feebcb37\",\n goldA7: \"#ffedcd48\",\n goldA8: \"#fdeaca5f\",\n goldA9: \"#ffdba690\",\n goldA10: \"#fedfb09d\",\n goldA11: \"#fee7c6c8\",\n goldA12: \"#fef7ede7\",\n};\nconst goldDarkP3 = {\n gold1: \"color(display-p3 0.071 0.071 0.067)\",\n gold2: \"color(display-p3 0.104 0.101 0.09)\",\n gold3: \"color(display-p3 0.141 0.136 0.122)\",\n gold4: \"color(display-p3 0.177 0.17 0.152)\",\n gold5: \"color(display-p3 0.217 0.207 0.185)\",\n gold6: \"color(display-p3 0.265 0.252 0.225)\",\n gold7: \"color(display-p3 0.327 0.31 0.277)\",\n gold8: \"color(display-p3 0.407 0.384 0.342)\",\n gold9: \"color(display-p3 0.579 0.517 0.41)\",\n gold10: \"color(display-p3 0.628 0.566 0.463)\",\n gold11: \"color(display-p3 0.784 0.728 0.635)\",\n gold12: \"color(display-p3 0.906 0.887 0.855)\",\n};\nconst goldDarkP3A = {\n goldA1: \"color(display-p3 0.855 0.855 0.071 / 0.005)\",\n goldA2: \"color(display-p3 0.98 0.89 0.616 / 0.043)\",\n goldA3: \"color(display-p3 1 0.949 0.753 / 0.08)\",\n goldA4: \"color(display-p3 1 0.933 0.8 / 0.118)\",\n goldA5: \"color(display-p3 1 0.949 0.804 / 0.16)\",\n goldA6: \"color(display-p3 1 0.925 0.8 / 0.215)\",\n goldA7: \"color(display-p3 1 0.945 0.831 / 0.278)\",\n goldA8: \"color(display-p3 1 0.937 0.82 / 0.366)\",\n goldA9: \"color(display-p3 0.996 0.882 0.69 / 0.551)\",\n goldA10: \"color(display-p3 1 0.894 0.725 / 0.601)\",\n goldA11: \"color(display-p3 0.784 0.728 0.635)\",\n goldA12: \"color(display-p3 0.906 0.887 0.855)\",\n};\nconst skyDark = {\n sky1: \"#0d141f\",\n sky2: \"#111a27\",\n sky3: \"#112840\",\n sky4: \"#113555\",\n sky5: \"#154467\",\n sky6: \"#1b537b\",\n sky7: \"#1f6692\",\n sky8: \"#197cae\",\n sky9: \"#7ce2fe\",\n sky10: \"#a8eeff\",\n sky11: \"#75c7f0\",\n sky12: \"#c2f3ff\",\n};\nconst skyDarkA = {\n skyA1: \"#0044ff0f\",\n skyA2: \"#1171fb18\",\n skyA3: \"#1184fc33\",\n skyA4: \"#128fff49\",\n skyA5: \"#1c9dfd5d\",\n skyA6: \"#28a5ff72\",\n skyA7: \"#2badfe8b\",\n skyA8: \"#1db2fea9\",\n skyA9: \"#7ce3fffe\",\n skyA10: \"#a8eeff\",\n skyA11: \"#7cd3ffef\",\n skyA12: \"#c2f3ff\",\n};\nconst skyDarkP3 = {\n sky1: \"color(display-p3 0.056 0.078 0.116)\",\n sky2: \"color(display-p3 0.075 0.101 0.149)\",\n sky3: \"color(display-p3 0.089 0.154 0.244)\",\n sky4: \"color(display-p3 0.106 0.207 0.323)\",\n sky5: \"color(display-p3 0.135 0.261 0.394)\",\n sky6: \"color(display-p3 0.17 0.322 0.469)\",\n sky7: \"color(display-p3 0.205 0.394 0.557)\",\n sky8: \"color(display-p3 0.232 0.48 0.665)\",\n sky9: \"color(display-p3 0.585 0.877 0.983)\",\n sky10: \"color(display-p3 0.718 0.925 0.991)\",\n sky11: \"color(display-p3 0.536 0.772 0.924)\",\n sky12: \"color(display-p3 0.799 0.947 0.993)\",\n};\nconst skyDarkP3A = {\n skyA1: \"color(display-p3 0 0.282 0.996 / 0.055)\",\n skyA2: \"color(display-p3 0.157 0.467 0.992 / 0.089)\",\n skyA3: \"color(display-p3 0.192 0.522 0.996 / 0.19)\",\n skyA4: \"color(display-p3 0.212 0.584 1 / 0.274)\",\n skyA5: \"color(display-p3 0.259 0.631 1 / 0.349)\",\n skyA6: \"color(display-p3 0.302 0.655 1 / 0.433)\",\n skyA7: \"color(display-p3 0.329 0.686 1 / 0.526)\",\n skyA8: \"color(display-p3 0.325 0.71 1 / 0.643)\",\n skyA9: \"color(display-p3 0.592 0.894 1 / 0.984)\",\n skyA10: \"color(display-p3 0.722 0.933 1 / 0.992)\",\n skyA11: \"color(display-p3 0.536 0.772 0.924)\",\n skyA12: \"color(display-p3 0.799 0.947 0.993)\",\n};\nconst mintDark = {\n mint1: \"#0e1515\",\n mint2: \"#0f1b1b\",\n mint3: \"#092c2b\",\n mint4: \"#003a38\",\n mint5: \"#004744\",\n mint6: \"#105650\",\n mint7: \"#1e685f\",\n mint8: \"#277f70\",\n mint9: \"#86ead4\",\n mint10: \"#a8f5e5\",\n mint11: \"#58d5ba\",\n mint12: \"#c4f5e1\",\n};\nconst mintDarkA = {\n mintA1: \"#00dede05\",\n mintA2: \"#00f9f90b\",\n mintA3: \"#00fff61d\",\n mintA4: \"#00fff42c\",\n mintA5: \"#00fff23a\",\n mintA6: \"#0effeb4a\",\n mintA7: \"#34fde55e\",\n mintA8: \"#41ffdf76\",\n mintA9: \"#92ffe7e9\",\n mintA10: \"#aefeedf5\",\n mintA11: \"#67ffded2\",\n mintA12: \"#cbfee9f5\",\n};\nconst mintDarkP3 = {\n mint1: \"color(display-p3 0.059 0.082 0.081)\",\n mint2: \"color(display-p3 0.068 0.104 0.105)\",\n mint3: \"color(display-p3 0.077 0.17 0.168)\",\n mint4: \"color(display-p3 0.068 0.224 0.22)\",\n mint5: \"color(display-p3 0.104 0.275 0.264)\",\n mint6: \"color(display-p3 0.154 0.332 0.313)\",\n mint7: \"color(display-p3 0.207 0.403 0.373)\",\n mint8: \"color(display-p3 0.258 0.49 0.441)\",\n mint9: \"color(display-p3 0.62 0.908 0.834)\",\n mint10: \"color(display-p3 0.725 0.954 0.898)\",\n mint11: \"color(display-p3 0.482 0.825 0.733)\",\n mint12: \"color(display-p3 0.807 0.955 0.887)\",\n};\nconst mintDarkP3A = {\n mintA1: \"color(display-p3 0 0.992 0.992 / 0.017)\",\n mintA2: \"color(display-p3 0.071 0.98 0.98 / 0.043)\",\n mintA3: \"color(display-p3 0.176 0.996 0.996 / 0.11)\",\n mintA4: \"color(display-p3 0.071 0.996 0.973 / 0.169)\",\n mintA5: \"color(display-p3 0.243 1 0.949 / 0.223)\",\n mintA6: \"color(display-p3 0.369 1 0.933 / 0.286)\",\n mintA7: \"color(display-p3 0.459 1 0.914 / 0.362)\",\n mintA8: \"color(display-p3 0.49 1 0.89 / 0.454)\",\n mintA9: \"color(display-p3 0.678 0.996 0.914 / 0.904)\",\n mintA10: \"color(display-p3 0.761 1 0.941 / 0.95)\",\n mintA11: \"color(display-p3 0.482 0.825 0.733)\",\n mintA12: \"color(display-p3 0.807 0.955 0.887)\",\n};\nconst limeDark = {\n lime1: \"#11130c\",\n lime2: \"#151a10\",\n lime3: \"#1f2917\",\n lime4: \"#29371d\",\n lime5: \"#334423\",\n lime6: \"#3d522a\",\n lime7: \"#496231\",\n lime8: \"#577538\",\n lime9: \"#bdee63\",\n lime10: \"#d4ff70\",\n lime11: \"#bde56c\",\n lime12: \"#e3f7ba\",\n};\nconst limeDarkA = {\n limeA1: \"#11bb0003\",\n limeA2: \"#78f7000a\",\n limeA3: \"#9bfd4c1a\",\n limeA4: \"#a7fe5c29\",\n limeA5: \"#affe6537\",\n limeA6: \"#b2fe6d46\",\n limeA7: \"#b6ff6f57\",\n limeA8: \"#b6fd6d6c\",\n limeA9: \"#caff69ed\",\n limeA10: \"#d4ff70\",\n limeA11: \"#d1fe77e4\",\n limeA12: \"#e9febff7\",\n};\nconst limeDarkP3 = {\n lime1: \"color(display-p3 0.067 0.073 0.048)\",\n lime2: \"color(display-p3 0.086 0.1 0.067)\",\n lime3: \"color(display-p3 0.13 0.16 0.099)\",\n lime4: \"color(display-p3 0.172 0.214 0.126)\",\n lime5: \"color(display-p3 0.213 0.266 0.153)\",\n lime6: \"color(display-p3 0.257 0.321 0.182)\",\n lime7: \"color(display-p3 0.307 0.383 0.215)\",\n lime8: \"color(display-p3 0.365 0.456 0.25)\",\n lime9: \"color(display-p3 0.78 0.928 0.466)\",\n lime10: \"color(display-p3 0.865 0.995 0.519)\",\n lime11: \"color(display-p3 0.771 0.893 0.485)\",\n lime12: \"color(display-p3 0.905 0.966 0.753)\",\n};\nconst limeDarkP3A = {\n limeA1: \"color(display-p3 0.067 0.941 0 / 0.009)\",\n limeA2: \"color(display-p3 0.584 0.996 0.071 / 0.038)\",\n limeA3: \"color(display-p3 0.69 1 0.38 / 0.101)\",\n limeA4: \"color(display-p3 0.729 1 0.435 / 0.16)\",\n limeA5: \"color(display-p3 0.745 1 0.471 / 0.215)\",\n limeA6: \"color(display-p3 0.769 1 0.482 / 0.274)\",\n limeA7: \"color(display-p3 0.769 1 0.506 / 0.341)\",\n limeA8: \"color(display-p3 0.784 1 0.51 / 0.416)\",\n limeA9: \"color(display-p3 0.839 1 0.502 / 0.925)\",\n limeA10: \"color(display-p3 0.871 1 0.522 / 0.996)\",\n limeA11: \"color(display-p3 0.771 0.893 0.485)\",\n limeA12: \"color(display-p3 0.905 0.966 0.753)\",\n};\nconst yellowDark = {\n yellow1: \"#14120b\",\n yellow2: \"#1b180f\",\n yellow3: \"#2d2305\",\n yellow4: \"#362b00\",\n yellow5: \"#433500\",\n yellow6: \"#524202\",\n yellow7: \"#665417\",\n yellow8: \"#836a21\",\n yellow9: \"#ffe629\",\n yellow10: \"#ffff57\",\n yellow11: \"#f5e147\",\n yellow12: \"#f6eeb4\",\n};\nconst yellowDarkA = {\n yellowA1: \"#d1510004\",\n yellowA2: \"#f9b4000b\",\n yellowA3: \"#ffaa001e\",\n yellowA4: \"#fdb70028\",\n yellowA5: \"#febb0036\",\n yellowA6: \"#fec40046\",\n yellowA7: \"#fdcb225c\",\n yellowA8: \"#fdca327b\",\n yellowA9: \"#ffe629\",\n yellowA10: \"#ffff57\",\n yellowA11: \"#fee949f5\",\n yellowA12: \"#fef6baf6\",\n};\nconst yellowDarkP3 = {\n yellow1: \"color(display-p3 0.078 0.069 0.047)\",\n yellow2: \"color(display-p3 0.103 0.094 0.063)\",\n yellow3: \"color(display-p3 0.168 0.137 0.039)\",\n yellow4: \"color(display-p3 0.209 0.169 0)\",\n yellow5: \"color(display-p3 0.255 0.209 0)\",\n yellow6: \"color(display-p3 0.31 0.261 0.07)\",\n yellow7: \"color(display-p3 0.389 0.331 0.135)\",\n yellow8: \"color(display-p3 0.497 0.42 0.182)\",\n yellow9: \"color(display-p3 1 0.92 0.22)\",\n yellow10: \"color(display-p3 1 1 0.456)\",\n yellow11: \"color(display-p3 0.948 0.885 0.392)\",\n yellow12: \"color(display-p3 0.959 0.934 0.731)\",\n};\nconst yellowDarkP3A = {\n yellowA1: \"color(display-p3 0.973 0.369 0 / 0.013)\",\n yellowA2: \"color(display-p3 0.996 0.792 0 / 0.038)\",\n yellowA3: \"color(display-p3 0.996 0.71 0 / 0.11)\",\n yellowA4: \"color(display-p3 0.996 0.741 0 / 0.152)\",\n yellowA5: \"color(display-p3 0.996 0.765 0 / 0.202)\",\n yellowA6: \"color(display-p3 0.996 0.816 0.082 / 0.261)\",\n yellowA7: \"color(display-p3 1 0.831 0.263 / 0.345)\",\n yellowA8: \"color(display-p3 1 0.831 0.314 / 0.463)\",\n yellowA9: \"color(display-p3 1 0.922 0.22)\",\n yellowA10: \"color(display-p3 1 1 0.455)\",\n yellowA11: \"color(display-p3 0.948 0.885 0.392)\",\n yellowA12: \"color(display-p3 0.959 0.934 0.731)\",\n};\nconst amberDark = {\n amber1: \"#16120c\",\n amber2: \"#1d180f\",\n amber3: \"#302008\",\n amber4: \"#3f2700\",\n amber5: \"#4d3000\",\n amber6: \"#5c3d05\",\n amber7: \"#714f19\",\n amber8: \"#8f6424\",\n amber9: \"#ffc53d\",\n amber10: \"#ffd60a\",\n amber11: \"#ffca16\",\n amber12: \"#ffe7b3\",\n};\nconst amberDarkA = {\n amberA1: \"#e63c0006\",\n amberA2: \"#fd9b000d\",\n amberA3: \"#fa820022\",\n amberA4: \"#fc820032\",\n amberA5: \"#fd8b0041\",\n amberA6: \"#fd9b0051\",\n amberA7: \"#ffab2567\",\n amberA8: \"#ffae3587\",\n amberA9: \"#ffc53d\",\n amberA10: \"#ffd60a\",\n amberA11: \"#ffca16\",\n amberA12: \"#ffe7b3\",\n};\nconst amberDarkP3 = {\n amber1: \"color(display-p3 0.082 0.07 0.05)\",\n amber2: \"color(display-p3 0.111 0.094 0.064)\",\n amber3: \"color(display-p3 0.178 0.128 0.049)\",\n amber4: \"color(display-p3 0.239 0.156 0)\",\n amber5: \"color(display-p3 0.29 0.193 0)\",\n amber6: \"color(display-p3 0.344 0.245 0.076)\",\n amber7: \"color(display-p3 0.422 0.314 0.141)\",\n amber8: \"color(display-p3 0.535 0.399 0.189)\",\n amber9: \"color(display-p3 1 0.77 0.26)\",\n amber10: \"color(display-p3 1 0.87 0.15)\",\n amber11: \"color(display-p3 1 0.8 0.29)\",\n amber12: \"color(display-p3 0.984 0.909 0.726)\",\n};\nconst amberDarkP3A = {\n amberA1: \"color(display-p3 0.992 0.298 0 / 0.017)\",\n amberA2: \"color(display-p3 0.988 0.651 0 / 0.047)\",\n amberA3: \"color(display-p3 1 0.6 0 / 0.118)\",\n amberA4: \"color(display-p3 1 0.557 0 / 0.185)\",\n amberA5: \"color(display-p3 1 0.592 0 / 0.24)\",\n amberA6: \"color(display-p3 1 0.659 0.094 / 0.299)\",\n amberA7: \"color(display-p3 1 0.714 0.263 / 0.383)\",\n amberA8: \"color(display-p3 0.996 0.729 0.306 / 0.5)\",\n amberA9: \"color(display-p3 1 0.769 0.259)\",\n amberA10: \"color(display-p3 1 0.871 0.149)\",\n amberA11: \"color(display-p3 1 0.8 0.29)\",\n amberA12: \"color(display-p3 0.984 0.909 0.726)\",\n};\nconst orangeDark = {\n orange1: \"#17120e\",\n orange2: \"#1e160f\",\n orange3: \"#331e0b\",\n orange4: \"#462100\",\n orange5: \"#562800\",\n orange6: \"#66350c\",\n orange7: \"#7e451d\",\n orange8: \"#a35829\",\n orange9: \"#f76b15\",\n orange10: \"#ff801f\",\n orange11: \"#ffa057\",\n orange12: \"#ffe0c2\",\n};\nconst orangeDarkA = {\n orangeA1: \"#ec360007\",\n orangeA2: \"#fe6d000e\",\n orangeA3: \"#fb6a0025\",\n orangeA4: \"#ff590039\",\n orangeA5: \"#ff61004a\",\n orangeA6: \"#fd75045c\",\n orangeA7: \"#ff832c75\",\n orangeA8: \"#fe84389d\",\n orangeA9: \"#fe6d15f7\",\n orangeA10: \"#ff801f\",\n orangeA11: \"#ffa057\",\n orangeA12: \"#ffe0c2\",\n};\nconst orangeDarkP3 = {\n orange1: \"color(display-p3 0.088 0.07 0.057)\",\n orange2: \"color(display-p3 0.113 0.089 0.061)\",\n orange3: \"color(display-p3 0.189 0.12 0.056)\",\n orange4: \"color(display-p3 0.262 0.132 0)\",\n orange5: \"color(display-p3 0.315 0.168 0.016)\",\n orange6: \"color(display-p3 0.376 0.219 0.088)\",\n orange7: \"color(display-p3 0.465 0.283 0.147)\",\n orange8: \"color(display-p3 0.601 0.359 0.201)\",\n orange9: \"color(display-p3 0.9 0.45 0.2)\",\n orange10: \"color(display-p3 0.98 0.51 0.23)\",\n orange11: \"color(display-p3 1 0.63 0.38)\",\n orange12: \"color(display-p3 0.98 0.883 0.775)\",\n};\nconst orangeDarkP3A = {\n orangeA1: \"color(display-p3 0.961 0.247 0 / 0.022)\",\n orangeA2: \"color(display-p3 0.992 0.529 0 / 0.051)\",\n orangeA3: \"color(display-p3 0.996 0.486 0 / 0.131)\",\n orangeA4: \"color(display-p3 0.996 0.384 0 / 0.211)\",\n orangeA5: \"color(display-p3 1 0.455 0 / 0.265)\",\n orangeA6: \"color(display-p3 1 0.529 0.129 / 0.332)\",\n orangeA7: \"color(display-p3 1 0.569 0.251 / 0.429)\",\n orangeA8: \"color(display-p3 1 0.584 0.302 / 0.572)\",\n orangeA9: \"color(display-p3 1 0.494 0.216 / 0.895)\",\n orangeA10: \"color(display-p3 1 0.522 0.235 / 0.979)\",\n orangeA11: \"color(display-p3 1 0.63 0.38)\",\n orangeA12: \"color(display-p3 0.98 0.883 0.775)\",\n};\n\nconst gray = {\n gray1: \"#fcfcfc\",\n gray2: \"#f9f9f9\",\n gray3: \"#f0f0f0\",\n gray4: \"#e8e8e8\",\n gray5: \"#e0e0e0\",\n gray6: \"#d9d9d9\",\n gray7: \"#cecece\",\n gray8: \"#bbbbbb\",\n gray9: \"#8d8d8d\",\n gray10: \"#838383\",\n gray11: \"#646464\",\n gray12: \"#202020\",\n};\nconst grayA = {\n grayA1: \"#00000003\",\n grayA2: \"#00000006\",\n grayA3: \"#0000000f\",\n grayA4: \"#00000017\",\n grayA5: \"#0000001f\",\n grayA6: \"#00000026\",\n grayA7: \"#00000031\",\n grayA8: \"#00000044\",\n grayA9: \"#00000072\",\n grayA10: \"#0000007c\",\n grayA11: \"#0000009b\",\n grayA12: \"#000000df\",\n};\nconst grayP3 = {\n gray1: \"color(display-p3 0.988 0.988 0.988)\",\n gray2: \"color(display-p3 0.975 0.975 0.975)\",\n gray3: \"color(display-p3 0.939 0.939 0.939)\",\n gray4: \"color(display-p3 0.908 0.908 0.908)\",\n gray5: \"color(display-p3 0.88 0.88 0.88)\",\n gray6: \"color(display-p3 0.849 0.849 0.849)\",\n gray7: \"color(display-p3 0.807 0.807 0.807)\",\n gray8: \"color(display-p3 0.732 0.732 0.732)\",\n gray9: \"color(display-p3 0.553 0.553 0.553)\",\n gray10: \"color(display-p3 0.512 0.512 0.512)\",\n gray11: \"color(display-p3 0.392 0.392 0.392)\",\n gray12: \"color(display-p3 0.125 0.125 0.125)\",\n};\nconst grayP3A = {\n grayA1: \"color(display-p3 0 0 0 / 0.012)\",\n grayA2: \"color(display-p3 0 0 0 / 0.024)\",\n grayA3: \"color(display-p3 0 0 0 / 0.063)\",\n grayA4: \"color(display-p3 0 0 0 / 0.09)\",\n grayA5: \"color(display-p3 0 0 0 / 0.122)\",\n grayA6: \"color(display-p3 0 0 0 / 0.153)\",\n grayA7: \"color(display-p3 0 0 0 / 0.192)\",\n grayA8: \"color(display-p3 0 0 0 / 0.267)\",\n grayA9: \"color(display-p3 0 0 0 / 0.447)\",\n grayA10: \"color(display-p3 0 0 0 / 0.486)\",\n grayA11: \"color(display-p3 0 0 0 / 0.608)\",\n grayA12: \"color(display-p3 0 0 0 / 0.875)\",\n};\nconst mauve = {\n mauve1: \"#fdfcfd\",\n mauve2: \"#faf9fb\",\n mauve3: \"#f2eff3\",\n mauve4: \"#eae7ec\",\n mauve5: \"#e3dfe6\",\n mauve6: \"#dbd8e0\",\n mauve7: \"#d0cdd7\",\n mauve8: \"#bcbac7\",\n mauve9: \"#8e8c99\",\n mauve10: \"#84828e\",\n mauve11: \"#65636d\",\n mauve12: \"#211f26\",\n};\nconst mauveA = {\n mauveA1: \"#55005503\",\n mauveA2: \"#2b005506\",\n mauveA3: \"#30004010\",\n mauveA4: \"#20003618\",\n mauveA5: \"#20003820\",\n mauveA6: \"#14003527\",\n mauveA7: \"#10003332\",\n mauveA8: \"#08003145\",\n mauveA9: \"#05001d73\",\n mauveA10: \"#0500197d\",\n mauveA11: \"#0400119c\",\n mauveA12: \"#020008e0\",\n};\nconst mauveP3 = {\n mauve1: \"color(display-p3 0.991 0.988 0.992)\",\n mauve2: \"color(display-p3 0.98 0.976 0.984)\",\n mauve3: \"color(display-p3 0.946 0.938 0.952)\",\n mauve4: \"color(display-p3 0.915 0.906 0.925)\",\n mauve5: \"color(display-p3 0.886 0.876 0.901)\",\n mauve6: \"color(display-p3 0.856 0.846 0.875)\",\n mauve7: \"color(display-p3 0.814 0.804 0.84)\",\n mauve8: \"color(display-p3 0.735 0.728 0.777)\",\n mauve9: \"color(display-p3 0.555 0.549 0.596)\",\n mauve10: \"color(display-p3 0.514 0.508 0.552)\",\n mauve11: \"color(display-p3 0.395 0.388 0.424)\",\n mauve12: \"color(display-p3 0.128 0.122 0.147)\",\n};\nconst mauveP3A = {\n mauveA1: \"color(display-p3 0.349 0.024 0.349 / 0.012)\",\n mauveA2: \"color(display-p3 0.184 0.024 0.349 / 0.024)\",\n mauveA3: \"color(display-p3 0.129 0.008 0.255 / 0.063)\",\n mauveA4: \"color(display-p3 0.094 0.012 0.216 / 0.095)\",\n mauveA5: \"color(display-p3 0.098 0.008 0.224 / 0.126)\",\n mauveA6: \"color(display-p3 0.055 0.004 0.18 / 0.153)\",\n mauveA7: \"color(display-p3 0.067 0.008 0.184 / 0.197)\",\n mauveA8: \"color(display-p3 0.02 0.004 0.176 / 0.271)\",\n mauveA9: \"color(display-p3 0.02 0.004 0.106 / 0.451)\",\n mauveA10: \"color(display-p3 0.012 0.004 0.09 / 0.491)\",\n mauveA11: \"color(display-p3 0.016 0 0.059 / 0.612)\",\n mauveA12: \"color(display-p3 0.008 0 0.027 / 0.879)\",\n};\nconst slate = {\n slate1: \"#fcfcfd\",\n slate2: \"#f9f9fb\",\n slate3: \"#f0f0f3\",\n slate4: \"#e8e8ec\",\n slate5: \"#e0e1e6\",\n slate6: \"#d9d9e0\",\n slate7: \"#cdced6\",\n slate8: \"#b9bbc6\",\n slate9: \"#8b8d98\",\n slate10: \"#80838d\",\n slate11: \"#60646c\",\n slate12: \"#1c2024\",\n};\nconst slateA = {\n slateA1: \"#00005503\",\n slateA2: \"#00005506\",\n slateA3: \"#0000330f\",\n slateA4: \"#00002d17\",\n slateA5: \"#0009321f\",\n slateA6: \"#00002f26\",\n slateA7: \"#00062e32\",\n slateA8: \"#00083046\",\n slateA9: \"#00051d74\",\n slateA10: \"#00071b7f\",\n slateA11: \"#0007149f\",\n slateA12: \"#000509e3\",\n};\nconst slateP3 = {\n slate1: \"color(display-p3 0.988 0.988 0.992)\",\n slate2: \"color(display-p3 0.976 0.976 0.984)\",\n slate3: \"color(display-p3 0.94 0.941 0.953)\",\n slate4: \"color(display-p3 0.908 0.909 0.925)\",\n slate5: \"color(display-p3 0.88 0.881 0.901)\",\n slate6: \"color(display-p3 0.85 0.852 0.876)\",\n slate7: \"color(display-p3 0.805 0.808 0.838)\",\n slate8: \"color(display-p3 0.727 0.733 0.773)\",\n slate9: \"color(display-p3 0.547 0.553 0.592)\",\n slate10: \"color(display-p3 0.503 0.512 0.549)\",\n slate11: \"color(display-p3 0.379 0.392 0.421)\",\n slate12: \"color(display-p3 0.113 0.125 0.14)\",\n};\nconst slateP3A = {\n slateA1: \"color(display-p3 0.024 0.024 0.349 / 0.012)\",\n slateA2: \"color(display-p3 0.024 0.024 0.349 / 0.024)\",\n slateA3: \"color(display-p3 0.004 0.004 0.204 / 0.059)\",\n slateA4: \"color(display-p3 0.012 0.012 0.184 / 0.091)\",\n slateA5: \"color(display-p3 0.004 0.039 0.2 / 0.122)\",\n slateA6: \"color(display-p3 0.008 0.008 0.165 / 0.15)\",\n slateA7: \"color(display-p3 0.008 0.027 0.184 / 0.197)\",\n slateA8: \"color(display-p3 0.004 0.031 0.176 / 0.275)\",\n slateA9: \"color(display-p3 0.004 0.02 0.106 / 0.455)\",\n slateA10: \"color(display-p3 0.004 0.027 0.098 / 0.499)\",\n slateA11: \"color(display-p3 0 0.02 0.063 / 0.62)\",\n slateA12: \"color(display-p3 0 0.012 0.031 / 0.887)\",\n};\nconst sage = {\n sage1: \"#fbfdfc\",\n sage2: \"#f7f9f8\",\n sage3: \"#eef1f0\",\n sage4: \"#e6e9e8\",\n sage5: \"#dfe2e0\",\n sage6: \"#d7dad9\",\n sage7: \"#cbcfcd\",\n sage8: \"#b8bcba\",\n sage9: \"#868e8b\",\n sage10: \"#7c8481\",\n sage11: \"#5f6563\",\n sage12: \"#1a211e\",\n};\nconst sageA = {\n sageA1: \"#00804004\",\n sageA2: \"#00402008\",\n sageA3: \"#002d1e11\",\n sageA4: \"#001f1519\",\n sageA5: \"#00180820\",\n sageA6: \"#00140d28\",\n sageA7: \"#00140a34\",\n sageA8: \"#000f0847\",\n sageA9: \"#00110b79\",\n sageA10: \"#00100a83\",\n sageA11: \"#000a07a0\",\n sageA12: \"#000805e5\",\n};\nconst sageP3 = {\n sage1: \"color(display-p3 0.986 0.992 0.988)\",\n sage2: \"color(display-p3 0.97 0.977 0.974)\",\n sage3: \"color(display-p3 0.935 0.944 0.94)\",\n sage4: \"color(display-p3 0.904 0.913 0.909)\",\n sage5: \"color(display-p3 0.875 0.885 0.88)\",\n sage6: \"color(display-p3 0.844 0.854 0.849)\",\n sage7: \"color(display-p3 0.8 0.811 0.806)\",\n sage8: \"color(display-p3 0.725 0.738 0.732)\",\n sage9: \"color(display-p3 0.531 0.556 0.546)\",\n sage10: \"color(display-p3 0.492 0.515 0.506)\",\n sage11: \"color(display-p3 0.377 0.395 0.389)\",\n sage12: \"color(display-p3 0.107 0.129 0.118)\",\n};\nconst sageP3A = {\n sageA1: \"color(display-p3 0.024 0.514 0.267 / 0.016)\",\n sageA2: \"color(display-p3 0.02 0.267 0.145 / 0.032)\",\n sageA3: \"color(display-p3 0.008 0.184 0.125 / 0.067)\",\n sageA4: \"color(display-p3 0.012 0.094 0.051 / 0.095)\",\n sageA5: \"color(display-p3 0.008 0.098 0.035 / 0.126)\",\n sageA6: \"color(display-p3 0.004 0.078 0.027 / 0.157)\",\n sageA7: \"color(display-p3 0 0.059 0.039 / 0.2)\",\n sageA8: \"color(display-p3 0.004 0.047 0.031 / 0.275)\",\n sageA9: \"color(display-p3 0.004 0.059 0.035 / 0.471)\",\n sageA10: \"color(display-p3 0 0.047 0.031 / 0.51)\",\n sageA11: \"color(display-p3 0 0.031 0.02 / 0.624)\",\n sageA12: \"color(display-p3 0 0.027 0.012 / 0.895)\",\n};\nconst olive = {\n olive1: \"#fcfdfc\",\n olive2: \"#f8faf8\",\n olive3: \"#eff1ef\",\n olive4: \"#e7e9e7\",\n olive5: \"#dfe2df\",\n olive6: \"#d7dad7\",\n olive7: \"#cccfcc\",\n olive8: \"#b9bcb8\",\n olive9: \"#898e87\",\n olive10: \"#7f847d\",\n olive11: \"#60655f\",\n olive12: \"#1d211c\",\n};\nconst oliveA = {\n oliveA1: \"#00550003\",\n oliveA2: \"#00490007\",\n oliveA3: \"#00200010\",\n oliveA4: \"#00160018\",\n oliveA5: \"#00180020\",\n oliveA6: \"#00140028\",\n oliveA7: \"#000f0033\",\n oliveA8: \"#040f0047\",\n oliveA9: \"#050f0078\",\n oliveA10: \"#040e0082\",\n oliveA11: \"#020a00a0\",\n oliveA12: \"#010600e3\",\n};\nconst oliveP3 = {\n olive1: \"color(display-p3 0.989 0.992 0.989)\",\n olive2: \"color(display-p3 0.974 0.98 0.973)\",\n olive3: \"color(display-p3 0.939 0.945 0.937)\",\n olive4: \"color(display-p3 0.907 0.914 0.905)\",\n olive5: \"color(display-p3 0.878 0.885 0.875)\",\n olive6: \"color(display-p3 0.846 0.855 0.843)\",\n olive7: \"color(display-p3 0.803 0.812 0.8)\",\n olive8: \"color(display-p3 0.727 0.738 0.723)\",\n olive9: \"color(display-p3 0.541 0.556 0.532)\",\n olive10: \"color(display-p3 0.5 0.515 0.491)\",\n olive11: \"color(display-p3 0.38 0.395 0.374)\",\n olive12: \"color(display-p3 0.117 0.129 0.111)\",\n};\nconst oliveP3A = {\n oliveA1: \"color(display-p3 0.024 0.349 0.024 / 0.012)\",\n oliveA2: \"color(display-p3 0.024 0.302 0.024 / 0.028)\",\n oliveA3: \"color(display-p3 0.008 0.129 0.008 / 0.063)\",\n oliveA4: \"color(display-p3 0.012 0.094 0.012 / 0.095)\",\n oliveA5: \"color(display-p3 0.035 0.098 0.008 / 0.126)\",\n oliveA6: \"color(display-p3 0.027 0.078 0.004 / 0.157)\",\n oliveA7: \"color(display-p3 0.02 0.059 0 / 0.2)\",\n oliveA8: \"color(display-p3 0.02 0.059 0.004 / 0.279)\",\n oliveA9: \"color(display-p3 0.02 0.051 0.004 / 0.467)\",\n oliveA10: \"color(display-p3 0.024 0.047 0 / 0.51)\",\n oliveA11: \"color(display-p3 0.012 0.039 0 / 0.628)\",\n oliveA12: \"color(display-p3 0.008 0.024 0 / 0.891)\",\n};\nconst sand = {\n sand1: \"#fdfdfc\",\n sand2: \"#f9f9f8\",\n sand3: \"#f1f0ef\",\n sand4: \"#e9e8e6\",\n sand5: \"#e2e1de\",\n sand6: \"#dad9d6\",\n sand7: \"#cfceca\",\n sand8: \"#bcbbb5\",\n sand9: \"#8d8d86\",\n sand10: \"#82827c\",\n sand11: \"#63635e\",\n sand12: \"#21201c\",\n};\nconst sandA = {\n sandA1: \"#55550003\",\n sandA2: \"#25250007\",\n sandA3: \"#20100010\",\n sandA4: \"#1f150019\",\n sandA5: \"#1f180021\",\n sandA6: \"#19130029\",\n sandA7: \"#19140035\",\n sandA8: \"#1915014a\",\n sandA9: \"#0f0f0079\",\n sandA10: \"#0c0c0083\",\n sandA11: \"#080800a1\",\n sandA12: \"#060500e3\",\n};\nconst sandP3 = {\n sand1: \"color(display-p3 0.992 0.992 0.989)\",\n sand2: \"color(display-p3 0.977 0.977 0.973)\",\n sand3: \"color(display-p3 0.943 0.942 0.936)\",\n sand4: \"color(display-p3 0.913 0.912 0.903)\",\n sand5: \"color(display-p3 0.885 0.883 0.873)\",\n sand6: \"color(display-p3 0.854 0.852 0.839)\",\n sand7: \"color(display-p3 0.813 0.81 0.794)\",\n sand8: \"color(display-p3 0.738 0.734 0.713)\",\n sand9: \"color(display-p3 0.553 0.553 0.528)\",\n sand10: \"color(display-p3 0.511 0.511 0.488)\",\n sand11: \"color(display-p3 0.388 0.388 0.37)\",\n sand12: \"color(display-p3 0.129 0.126 0.111)\",\n};\nconst sandP3A = {\n sandA1: \"color(display-p3 0.349 0.349 0.024 / 0.012)\",\n sandA2: \"color(display-p3 0.161 0.161 0.024 / 0.028)\",\n sandA3: \"color(display-p3 0.067 0.067 0.008 / 0.063)\",\n sandA4: \"color(display-p3 0.129 0.129 0.012 / 0.099)\",\n sandA5: \"color(display-p3 0.098 0.067 0.008 / 0.126)\",\n sandA6: \"color(display-p3 0.102 0.075 0.004 / 0.161)\",\n sandA7: \"color(display-p3 0.098 0.098 0.004 / 0.208)\",\n sandA8: \"color(display-p3 0.086 0.075 0.004 / 0.287)\",\n sandA9: \"color(display-p3 0.051 0.051 0.004 / 0.471)\",\n sandA10: \"color(display-p3 0.047 0.047 0 / 0.514)\",\n sandA11: \"color(display-p3 0.031 0.031 0 / 0.632)\",\n sandA12: \"color(display-p3 0.024 0.02 0 / 0.891)\",\n};\nconst tomato = {\n tomato1: \"#fffcfc\",\n tomato2: \"#fff8f7\",\n tomato3: \"#feebe7\",\n tomato4: \"#ffdcd3\",\n tomato5: \"#ffcdc2\",\n tomato6: \"#fdbdaf\",\n tomato7: \"#f5a898\",\n tomato8: \"#ec8e7b\",\n tomato9: \"#e54d2e\",\n tomato10: \"#dd4425\",\n tomato11: \"#d13415\",\n tomato12: \"#5c271f\",\n};\nconst tomatoA = {\n tomatoA1: \"#ff000003\",\n tomatoA2: \"#ff200008\",\n tomatoA3: \"#f52b0018\",\n tomatoA4: \"#ff35002c\",\n tomatoA5: \"#ff2e003d\",\n tomatoA6: \"#f92d0050\",\n tomatoA7: \"#e7280067\",\n tomatoA8: \"#db250084\",\n tomatoA9: \"#df2600d1\",\n tomatoA10: \"#d72400da\",\n tomatoA11: \"#cd2200ea\",\n tomatoA12: \"#460900e0\",\n};\nconst tomatoP3 = {\n tomato1: \"color(display-p3 0.998 0.989 0.988)\",\n tomato2: \"color(display-p3 0.994 0.974 0.969)\",\n tomato3: \"color(display-p3 0.985 0.924 0.909)\",\n tomato4: \"color(display-p3 0.996 0.868 0.835)\",\n tomato5: \"color(display-p3 0.98 0.812 0.77)\",\n tomato6: \"color(display-p3 0.953 0.75 0.698)\",\n tomato7: \"color(display-p3 0.917 0.673 0.611)\",\n tomato8: \"color(display-p3 0.875 0.575 0.502)\",\n tomato9: \"color(display-p3 0.831 0.345 0.231)\",\n tomato10: \"color(display-p3 0.802 0.313 0.2)\",\n tomato11: \"color(display-p3 0.755 0.259 0.152)\",\n tomato12: \"color(display-p3 0.335 0.165 0.132)\",\n};\nconst tomatoP3A = {\n tomatoA1: \"color(display-p3 0.675 0.024 0.024 / 0.012)\",\n tomatoA2: \"color(display-p3 0.757 0.145 0.02 / 0.032)\",\n tomatoA3: \"color(display-p3 0.831 0.184 0.012 / 0.091)\",\n tomatoA4: \"color(display-p3 0.976 0.192 0.004 / 0.165)\",\n tomatoA5: \"color(display-p3 0.918 0.192 0.004 / 0.232)\",\n tomatoA6: \"color(display-p3 0.847 0.173 0.004 / 0.302)\",\n tomatoA7: \"color(display-p3 0.788 0.165 0.004 / 0.389)\",\n tomatoA8: \"color(display-p3 0.749 0.153 0.004 / 0.499)\",\n tomatoA9: \"color(display-p3 0.78 0.149 0 / 0.769)\",\n tomatoA10: \"color(display-p3 0.757 0.141 0 / 0.8)\",\n tomatoA11: \"color(display-p3 0.755 0.259 0.152)\",\n tomatoA12: \"color(display-p3 0.335 0.165 0.132)\",\n};\nconst red = {\n red1: \"#fffcfc\",\n red2: \"#fff7f7\",\n red3: \"#feebec\",\n red4: \"#ffdbdc\",\n red5: \"#ffcdce\",\n red6: \"#fdbdbe\",\n red7: \"#f4a9aa\",\n red8: \"#eb8e90\",\n red9: \"#e5484d\",\n red10: \"#dc3e42\",\n red11: \"#ce2c31\",\n red12: \"#641723\",\n};\nconst redA = {\n redA1: \"#ff000003\",\n redA2: \"#ff000008\",\n redA3: \"#f3000d14\",\n redA4: \"#ff000824\",\n redA5: \"#ff000632\",\n redA6: \"#f8000442\",\n redA7: \"#df000356\",\n redA8: \"#d2000571\",\n redA9: \"#db0007b7\",\n redA10: \"#d10005c1\",\n redA11: \"#c40006d3\",\n redA12: \"#55000de8\",\n};\nconst redP3 = {\n red1: \"color(display-p3 0.998 0.989 0.988)\",\n red2: \"color(display-p3 0.995 0.971 0.971)\",\n red3: \"color(display-p3 0.985 0.925 0.925)\",\n red4: \"color(display-p3 0.999 0.866 0.866)\",\n red5: \"color(display-p3 0.984 0.812 0.811)\",\n red6: \"color(display-p3 0.955 0.751 0.749)\",\n red7: \"color(display-p3 0.915 0.675 0.672)\",\n red8: \"color(display-p3 0.872 0.575 0.572)\",\n red9: \"color(display-p3 0.83 0.329 0.324)\",\n red10: \"color(display-p3 0.798 0.294 0.285)\",\n red11: \"color(display-p3 0.744 0.234 0.222)\",\n red12: \"color(display-p3 0.36 0.115 0.143)\",\n};\nconst redP3A = {\n redA1: \"color(display-p3 0.675 0.024 0.024 / 0.012)\",\n redA2: \"color(display-p3 0.863 0.024 0.024 / 0.028)\",\n redA3: \"color(display-p3 0.792 0.008 0.008 / 0.075)\",\n redA4: \"color(display-p3 1 0.008 0.008 / 0.134)\",\n redA5: \"color(display-p3 0.918 0.008 0.008 / 0.189)\",\n redA6: \"color(display-p3 0.831 0.02 0.004 / 0.251)\",\n redA7: \"color(display-p3 0.741 0.016 0.004 / 0.33)\",\n redA8: \"color(display-p3 0.698 0.012 0.004 / 0.428)\",\n redA9: \"color(display-p3 0.749 0.008 0 / 0.675)\",\n redA10: \"color(display-p3 0.714 0.012 0 / 0.714)\",\n redA11: \"color(display-p3 0.744 0.234 0.222)\",\n redA12: \"color(display-p3 0.36 0.115 0.143)\",\n};\nconst ruby = {\n ruby1: \"#fffcfd\",\n ruby2: \"#fff7f8\",\n ruby3: \"#feeaed\",\n ruby4: \"#ffdce1\",\n ruby5: \"#ffced6\",\n ruby6: \"#f8bfc8\",\n ruby7: \"#efacb8\",\n ruby8: \"#e592a3\",\n ruby9: \"#e54666\",\n ruby10: \"#dc3b5d\",\n ruby11: \"#ca244d\",\n ruby12: \"#64172b\",\n};\nconst rubyA = {\n rubyA1: \"#ff005503\",\n rubyA2: \"#ff002008\",\n rubyA3: \"#f3002515\",\n rubyA4: \"#ff002523\",\n rubyA5: \"#ff002a31\",\n rubyA6: \"#e4002440\",\n rubyA7: \"#ce002553\",\n rubyA8: \"#c300286d\",\n rubyA9: \"#db002cb9\",\n rubyA10: \"#d2002cc4\",\n rubyA11: \"#c10030db\",\n rubyA12: \"#550016e8\",\n};\nconst rubyP3 = {\n ruby1: \"color(display-p3 0.998 0.989 0.992)\",\n ruby2: \"color(display-p3 0.995 0.971 0.974)\",\n ruby3: \"color(display-p3 0.983 0.92 0.928)\",\n ruby4: \"color(display-p3 0.987 0.869 0.885)\",\n ruby5: \"color(display-p3 0.968 0.817 0.839)\",\n ruby6: \"color(display-p3 0.937 0.758 0.786)\",\n ruby7: \"color(display-p3 0.897 0.685 0.721)\",\n ruby8: \"color(display-p3 0.851 0.588 0.639)\",\n ruby9: \"color(display-p3 0.83 0.323 0.408)\",\n ruby10: \"color(display-p3 0.795 0.286 0.375)\",\n ruby11: \"color(display-p3 0.728 0.211 0.311)\",\n ruby12: \"color(display-p3 0.36 0.115 0.171)\",\n};\nconst rubyP3A = {\n rubyA1: \"color(display-p3 0.675 0.024 0.349 / 0.012)\",\n rubyA2: \"color(display-p3 0.863 0.024 0.024 / 0.028)\",\n rubyA3: \"color(display-p3 0.804 0.008 0.11 / 0.079)\",\n rubyA4: \"color(display-p3 0.91 0.008 0.125 / 0.13)\",\n rubyA5: \"color(display-p3 0.831 0.004 0.133 / 0.185)\",\n rubyA6: \"color(display-p3 0.745 0.004 0.118 / 0.244)\",\n rubyA7: \"color(display-p3 0.678 0.004 0.114 / 0.314)\",\n rubyA8: \"color(display-p3 0.639 0.004 0.125 / 0.412)\",\n rubyA9: \"color(display-p3 0.753 0 0.129 / 0.679)\",\n rubyA10: \"color(display-p3 0.714 0 0.125 / 0.714)\",\n rubyA11: \"color(display-p3 0.728 0.211 0.311)\",\n rubyA12: \"color(display-p3 0.36 0.115 0.171)\",\n};\nconst crimson = {\n crimson1: \"#fffcfd\",\n crimson2: \"#fef7f9\",\n crimson3: \"#ffe9f0\",\n crimson4: \"#fedce7\",\n crimson5: \"#facedd\",\n crimson6: \"#f3bed1\",\n crimson7: \"#eaacc3\",\n crimson8: \"#e093b2\",\n crimson9: \"#e93d82\",\n crimson10: \"#df3478\",\n crimson11: \"#cb1d63\",\n crimson12: \"#621639\",\n};\nconst crimsonA = {\n crimsonA1: \"#ff005503\",\n crimsonA2: \"#e0004008\",\n crimsonA3: \"#ff005216\",\n crimsonA4: \"#f8005123\",\n crimsonA5: \"#e5004f31\",\n crimsonA6: \"#d0004b41\",\n crimsonA7: \"#bf004753\",\n crimsonA8: \"#b6004a6c\",\n crimsonA9: \"#e2005bc2\",\n crimsonA10: \"#d70056cb\",\n crimsonA11: \"#c4004fe2\",\n crimsonA12: \"#530026e9\",\n};\nconst crimsonP3 = {\n crimson1: \"color(display-p3 0.998 0.989 0.992)\",\n crimson2: \"color(display-p3 0.991 0.969 0.976)\",\n crimson3: \"color(display-p3 0.987 0.917 0.941)\",\n crimson4: \"color(display-p3 0.975 0.866 0.904)\",\n crimson5: \"color(display-p3 0.953 0.813 0.864)\",\n crimson6: \"color(display-p3 0.921 0.755 0.817)\",\n crimson7: \"color(display-p3 0.88 0.683 0.761)\",\n crimson8: \"color(display-p3 0.834 0.592 0.694)\",\n crimson9: \"color(display-p3 0.843 0.298 0.507)\",\n crimson10: \"color(display-p3 0.807 0.266 0.468)\",\n crimson11: \"color(display-p3 0.731 0.195 0.388)\",\n crimson12: \"color(display-p3 0.352 0.111 0.221)\",\n};\nconst crimsonP3A = {\n crimsonA1: \"color(display-p3 0.675 0.024 0.349 / 0.012)\",\n crimsonA2: \"color(display-p3 0.757 0.02 0.267 / 0.032)\",\n crimsonA3: \"color(display-p3 0.859 0.008 0.294 / 0.083)\",\n crimsonA4: \"color(display-p3 0.827 0.008 0.298 / 0.134)\",\n crimsonA5: \"color(display-p3 0.753 0.008 0.275 / 0.189)\",\n crimsonA6: \"color(display-p3 0.682 0.004 0.247 / 0.244)\",\n crimsonA7: \"color(display-p3 0.62 0.004 0.251 / 0.318)\",\n crimsonA8: \"color(display-p3 0.6 0.004 0.251 / 0.408)\",\n crimsonA9: \"color(display-p3 0.776 0 0.298 / 0.702)\",\n crimsonA10: \"color(display-p3 0.737 0 0.275 / 0.734)\",\n crimsonA11: \"color(display-p3 0.731 0.195 0.388)\",\n crimsonA12: \"color(display-p3 0.352 0.111 0.221)\",\n};\nconst pink = {\n pink1: \"#fffcfe\",\n pink2: \"#fef7fb\",\n pink3: \"#fee9f5\",\n pink4: \"#fbdcef\",\n pink5: \"#f6cee7\",\n pink6: \"#efbfdd\",\n pink7: \"#e7acd0\",\n pink8: \"#dd93c2\",\n pink9: \"#d6409f\",\n pink10: \"#cf3897\",\n pink11: \"#c2298a\",\n pink12: \"#651249\",\n};\nconst pinkA = {\n pinkA1: \"#ff00aa03\",\n pinkA2: \"#e0008008\",\n pinkA3: \"#f4008c16\",\n pinkA4: \"#e2008b23\",\n pinkA5: \"#d1008331\",\n pinkA6: \"#c0007840\",\n pinkA7: \"#b6006f53\",\n pinkA8: \"#af006f6c\",\n pinkA9: \"#c8007fbf\",\n pinkA10: \"#c2007ac7\",\n pinkA11: \"#b60074d6\",\n pinkA12: \"#59003bed\",\n};\nconst pinkP3 = {\n pink1: \"color(display-p3 0.998 0.989 0.996)\",\n pink2: \"color(display-p3 0.992 0.97 0.985)\",\n pink3: \"color(display-p3 0.981 0.917 0.96)\",\n pink4: \"color(display-p3 0.963 0.867 0.932)\",\n pink5: \"color(display-p3 0.939 0.815 0.899)\",\n pink6: \"color(display-p3 0.907 0.756 0.859)\",\n pink7: \"color(display-p3 0.869 0.683 0.81)\",\n pink8: \"color(display-p3 0.825 0.59 0.751)\",\n pink9: \"color(display-p3 0.775 0.297 0.61)\",\n pink10: \"color(display-p3 0.748 0.27 0.581)\",\n pink11: \"color(display-p3 0.698 0.219 0.528)\",\n pink12: \"color(display-p3 0.363 0.101 0.279)\",\n};\nconst pinkP3A = {\n pinkA1: \"color(display-p3 0.675 0.024 0.675 / 0.012)\",\n pinkA2: \"color(display-p3 0.757 0.02 0.51 / 0.032)\",\n pinkA3: \"color(display-p3 0.765 0.008 0.529 / 0.083)\",\n pinkA4: \"color(display-p3 0.737 0.008 0.506 / 0.134)\",\n pinkA5: \"color(display-p3 0.663 0.004 0.451 / 0.185)\",\n pinkA6: \"color(display-p3 0.616 0.004 0.424 / 0.244)\",\n pinkA7: \"color(display-p3 0.596 0.004 0.412 / 0.318)\",\n pinkA8: \"color(display-p3 0.573 0.004 0.404 / 0.412)\",\n pinkA9: \"color(display-p3 0.682 0 0.447 / 0.702)\",\n pinkA10: \"color(display-p3 0.655 0 0.424 / 0.73)\",\n pinkA11: \"color(display-p3 0.698 0.219 0.528)\",\n pinkA12: \"color(display-p3 0.363 0.101 0.279)\",\n};\nconst plum = {\n plum1: \"#fefcff\",\n plum2: \"#fdf7fd\",\n plum3: \"#fbebfb\",\n plum4: \"#f7def8\",\n plum5: \"#f2d1f3\",\n plum6: \"#e9c2ec\",\n plum7: \"#deade3\",\n plum8: \"#cf91d8\",\n plum9: \"#ab4aba\",\n plum10: \"#a144af\",\n plum11: \"#953ea3\",\n plum12: \"#53195d\",\n};\nconst plumA = {\n plumA1: \"#aa00ff03\",\n plumA2: \"#c000c008\",\n plumA3: \"#cc00cc14\",\n plumA4: \"#c200c921\",\n plumA5: \"#b700bd2e\",\n plumA6: \"#a400b03d\",\n plumA7: \"#9900a852\",\n plumA8: \"#9000a56e\",\n plumA9: \"#89009eb5\",\n plumA10: \"#7f0092bb\",\n plumA11: \"#730086c1\",\n plumA12: \"#40004be6\",\n};\nconst plumP3 = {\n plum1: \"color(display-p3 0.995 0.988 0.999)\",\n plum2: \"color(display-p3 0.988 0.971 0.99)\",\n plum3: \"color(display-p3 0.973 0.923 0.98)\",\n plum4: \"color(display-p3 0.953 0.875 0.966)\",\n plum5: \"color(display-p3 0.926 0.825 0.945)\",\n plum6: \"color(display-p3 0.89 0.765 0.916)\",\n plum7: \"color(display-p3 0.84 0.686 0.877)\",\n plum8: \"color(display-p3 0.775 0.58 0.832)\",\n plum9: \"color(display-p3 0.624 0.313 0.708)\",\n plum10: \"color(display-p3 0.587 0.29 0.667)\",\n plum11: \"color(display-p3 0.543 0.263 0.619)\",\n plum12: \"color(display-p3 0.299 0.114 0.352)\",\n};\nconst plumP3A = {\n plumA1: \"color(display-p3 0.675 0.024 1 / 0.012)\",\n plumA2: \"color(display-p3 0.58 0.024 0.58 / 0.028)\",\n plumA3: \"color(display-p3 0.655 0.008 0.753 / 0.079)\",\n plumA4: \"color(display-p3 0.627 0.008 0.722 / 0.126)\",\n plumA5: \"color(display-p3 0.58 0.004 0.69 / 0.177)\",\n plumA6: \"color(display-p3 0.537 0.004 0.655 / 0.236)\",\n plumA7: \"color(display-p3 0.49 0.004 0.616 / 0.314)\",\n plumA8: \"color(display-p3 0.471 0.004 0.6 / 0.42)\",\n plumA9: \"color(display-p3 0.451 0 0.576 / 0.687)\",\n plumA10: \"color(display-p3 0.42 0 0.529 / 0.71)\",\n plumA11: \"color(display-p3 0.543 0.263 0.619)\",\n plumA12: \"color(display-p3 0.299 0.114 0.352)\",\n};\nconst purple = {\n purple1: \"#fefcfe\",\n purple2: \"#fbf7fe\",\n purple3: \"#f7edfe\",\n purple4: \"#f2e2fc\",\n purple5: \"#ead5f9\",\n purple6: \"#e0c4f4\",\n purple7: \"#d1afec\",\n purple8: \"#be93e4\",\n purple9: \"#8e4ec6\",\n purple10: \"#8347b9\",\n purple11: \"#8145b5\",\n purple12: \"#402060\",\n};\nconst purpleA = {\n purpleA1: \"#aa00aa03\",\n purpleA2: \"#8000e008\",\n purpleA3: \"#8e00f112\",\n purpleA4: \"#8d00e51d\",\n purpleA5: \"#8000db2a\",\n purpleA6: \"#7a01d03b\",\n purpleA7: \"#6d00c350\",\n purpleA8: \"#6600c06c\",\n purpleA9: \"#5c00adb1\",\n purpleA10: \"#53009eb8\",\n purpleA11: \"#52009aba\",\n purpleA12: \"#250049df\",\n};\nconst purpleP3 = {\n purple1: \"color(display-p3 0.995 0.988 0.996)\",\n purple2: \"color(display-p3 0.983 0.971 0.993)\",\n purple3: \"color(display-p3 0.963 0.931 0.989)\",\n purple4: \"color(display-p3 0.937 0.888 0.981)\",\n purple5: \"color(display-p3 0.904 0.837 0.966)\",\n purple6: \"color(display-p3 0.86 0.774 0.942)\",\n purple7: \"color(display-p3 0.799 0.69 0.91)\",\n purple8: \"color(display-p3 0.719 0.583 0.874)\",\n purple9: \"color(display-p3 0.523 0.318 0.751)\",\n purple10: \"color(display-p3 0.483 0.289 0.7)\",\n purple11: \"color(display-p3 0.473 0.281 0.687)\",\n purple12: \"color(display-p3 0.234 0.132 0.363)\",\n};\nconst purpleP3A = {\n purpleA1: \"color(display-p3 0.675 0.024 0.675 / 0.012)\",\n purpleA2: \"color(display-p3 0.443 0.024 0.722 / 0.028)\",\n purpleA3: \"color(display-p3 0.506 0.008 0.835 / 0.071)\",\n purpleA4: \"color(display-p3 0.451 0.004 0.831 / 0.114)\",\n purpleA5: \"color(display-p3 0.431 0.004 0.788 / 0.165)\",\n purpleA6: \"color(display-p3 0.384 0.004 0.745 / 0.228)\",\n purpleA7: \"color(display-p3 0.357 0.004 0.71 / 0.31)\",\n purpleA8: \"color(display-p3 0.322 0.004 0.702 / 0.416)\",\n purpleA9: \"color(display-p3 0.298 0 0.639 / 0.683)\",\n purpleA10: \"color(display-p3 0.271 0 0.58 / 0.71)\",\n purpleA11: \"color(display-p3 0.473 0.281 0.687)\",\n purpleA12: \"color(display-p3 0.234 0.132 0.363)\",\n};\nconst violet = {\n violet1: \"#fdfcfe\",\n violet2: \"#faf8ff\",\n violet3: \"#f4f0fe\",\n violet4: \"#ebe4ff\",\n violet5: \"#e1d9ff\",\n violet6: \"#d4cafe\",\n violet7: \"#c2b5f5\",\n violet8: \"#aa99ec\",\n violet9: \"#6e56cf\",\n violet10: \"#654dc4\",\n violet11: \"#6550b9\",\n violet12: \"#2f265f\",\n};\nconst violetA = {\n violetA1: \"#5500aa03\",\n violetA2: \"#4900ff07\",\n violetA3: \"#4400ee0f\",\n violetA4: \"#4300ff1b\",\n violetA5: \"#3600ff26\",\n violetA6: \"#3100fb35\",\n violetA7: \"#2d01dd4a\",\n violetA8: \"#2b00d066\",\n violetA9: \"#2400b7a9\",\n violetA10: \"#2300abb2\",\n violetA11: \"#1f0099af\",\n violetA12: \"#0b0043d9\",\n};\nconst violetP3 = {\n violet1: \"color(display-p3 0.991 0.988 0.995)\",\n violet2: \"color(display-p3 0.978 0.974 0.998)\",\n violet3: \"color(display-p3 0.953 0.943 0.993)\",\n violet4: \"color(display-p3 0.916 0.897 1)\",\n violet5: \"color(display-p3 0.876 0.851 1)\",\n violet6: \"color(display-p3 0.825 0.793 0.981)\",\n violet7: \"color(display-p3 0.752 0.712 0.943)\",\n violet8: \"color(display-p3 0.654 0.602 0.902)\",\n violet9: \"color(display-p3 0.417 0.341 0.784)\",\n violet10: \"color(display-p3 0.381 0.306 0.741)\",\n violet11: \"color(display-p3 0.383 0.317 0.702)\",\n violet12: \"color(display-p3 0.179 0.15 0.359)\",\n};\nconst violetP3A = {\n violetA1: \"color(display-p3 0.349 0.024 0.675 / 0.012)\",\n violetA2: \"color(display-p3 0.161 0.024 0.863 / 0.028)\",\n violetA3: \"color(display-p3 0.204 0.004 0.871 / 0.059)\",\n violetA4: \"color(display-p3 0.196 0.004 1 / 0.102)\",\n violetA5: \"color(display-p3 0.165 0.008 1 / 0.15)\",\n violetA6: \"color(display-p3 0.153 0.004 0.906 / 0.208)\",\n violetA7: \"color(display-p3 0.141 0.004 0.796 / 0.287)\",\n violetA8: \"color(display-p3 0.133 0.004 0.753 / 0.397)\",\n violetA9: \"color(display-p3 0.114 0 0.675 / 0.659)\",\n violetA10: \"color(display-p3 0.11 0 0.627 / 0.695)\",\n violetA11: \"color(display-p3 0.383 0.317 0.702)\",\n violetA12: \"color(display-p3 0.179 0.15 0.359)\",\n};\nconst iris = {\n iris1: \"#fdfdff\",\n iris2: \"#f8f8ff\",\n iris3: \"#f0f1fe\",\n iris4: \"#e6e7ff\",\n iris5: \"#dadcff\",\n iris6: \"#cbcdff\",\n iris7: \"#b8baf8\",\n iris8: \"#9b9ef0\",\n iris9: \"#5b5bd6\",\n iris10: \"#5151cd\",\n iris11: \"#5753c6\",\n iris12: \"#272962\",\n};\nconst irisA = {\n irisA1: \"#0000ff02\",\n irisA2: \"#0000ff07\",\n irisA3: \"#0011ee0f\",\n irisA4: \"#000bff19\",\n irisA5: \"#000eff25\",\n irisA6: \"#000aff34\",\n irisA7: \"#0008e647\",\n irisA8: \"#0008d964\",\n irisA9: \"#0000c0a4\",\n irisA10: \"#0000b6ae\",\n irisA11: \"#0600abac\",\n irisA12: \"#000246d8\",\n};\nconst irisP3 = {\n iris1: \"color(display-p3 0.992 0.992 0.999)\",\n iris2: \"color(display-p3 0.972 0.973 0.998)\",\n iris3: \"color(display-p3 0.943 0.945 0.992)\",\n iris4: \"color(display-p3 0.902 0.906 1)\",\n iris5: \"color(display-p3 0.857 0.861 1)\",\n iris6: \"color(display-p3 0.799 0.805 0.987)\",\n iris7: \"color(display-p3 0.721 0.727 0.955)\",\n iris8: \"color(display-p3 0.61 0.619 0.918)\",\n iris9: \"color(display-p3 0.357 0.357 0.81)\",\n iris10: \"color(display-p3 0.318 0.318 0.774)\",\n iris11: \"color(display-p3 0.337 0.326 0.748)\",\n iris12: \"color(display-p3 0.154 0.161 0.371)\",\n};\nconst irisP3A = {\n irisA1: \"color(display-p3 0.02 0.02 1 / 0.008)\",\n irisA2: \"color(display-p3 0.024 0.024 0.863 / 0.028)\",\n irisA3: \"color(display-p3 0.004 0.071 0.871 / 0.059)\",\n irisA4: \"color(display-p3 0.012 0.051 1 / 0.099)\",\n irisA5: \"color(display-p3 0.008 0.035 1 / 0.142)\",\n irisA6: \"color(display-p3 0 0.02 0.941 / 0.2)\",\n irisA7: \"color(display-p3 0.004 0.02 0.847 / 0.279)\",\n irisA8: \"color(display-p3 0.004 0.024 0.788 / 0.389)\",\n irisA9: \"color(display-p3 0 0 0.706 / 0.644)\",\n irisA10: \"color(display-p3 0 0 0.667 / 0.683)\",\n irisA11: \"color(display-p3 0.337 0.326 0.748)\",\n irisA12: \"color(display-p3 0.154 0.161 0.371)\",\n};\nconst indigo = {\n indigo1: \"#fdfdfe\",\n indigo2: \"#f7f9ff\",\n indigo3: \"#edf2fe\",\n indigo4: \"#e1e9ff\",\n indigo5: \"#d2deff\",\n indigo6: \"#c1d0ff\",\n indigo7: \"#abbdf9\",\n indigo8: \"#8da4ef\",\n indigo9: \"#3e63dd\",\n indigo10: \"#3358d4\",\n indigo11: \"#3a5bc7\",\n indigo12: \"#1f2d5c\",\n};\nconst indigoA = {\n indigoA1: \"#00008002\",\n indigoA2: \"#0040ff08\",\n indigoA3: \"#0047f112\",\n indigoA4: \"#0044ff1e\",\n indigoA5: \"#0044ff2d\",\n indigoA6: \"#003eff3e\",\n indigoA7: \"#0037ed54\",\n indigoA8: \"#0034dc72\",\n indigoA9: \"#0031d2c1\",\n indigoA10: \"#002ec9cc\",\n indigoA11: \"#002bb7c5\",\n indigoA12: \"#001046e0\",\n};\nconst indigoP3 = {\n indigo1: \"color(display-p3 0.992 0.992 0.996)\",\n indigo2: \"color(display-p3 0.971 0.977 0.998)\",\n indigo3: \"color(display-p3 0.933 0.948 0.992)\",\n indigo4: \"color(display-p3 0.885 0.914 1)\",\n indigo5: \"color(display-p3 0.831 0.87 1)\",\n indigo6: \"color(display-p3 0.767 0.814 0.995)\",\n indigo7: \"color(display-p3 0.685 0.74 0.957)\",\n indigo8: \"color(display-p3 0.569 0.639 0.916)\",\n indigo9: \"color(display-p3 0.276 0.384 0.837)\",\n indigo10: \"color(display-p3 0.234 0.343 0.801)\",\n indigo11: \"color(display-p3 0.256 0.354 0.755)\",\n indigo12: \"color(display-p3 0.133 0.175 0.348)\",\n};\nconst indigoP3A = {\n indigoA1: \"color(display-p3 0.02 0.02 0.51 / 0.008)\",\n indigoA2: \"color(display-p3 0.024 0.161 0.863 / 0.028)\",\n indigoA3: \"color(display-p3 0.008 0.239 0.886 / 0.067)\",\n indigoA4: \"color(display-p3 0.004 0.247 1 / 0.114)\",\n indigoA5: \"color(display-p3 0.004 0.235 1 / 0.169)\",\n indigoA6: \"color(display-p3 0.004 0.208 0.984 / 0.232)\",\n indigoA7: \"color(display-p3 0.004 0.176 0.863 / 0.314)\",\n indigoA8: \"color(display-p3 0.004 0.165 0.812 / 0.432)\",\n indigoA9: \"color(display-p3 0 0.153 0.773 / 0.726)\",\n indigoA10: \"color(display-p3 0 0.137 0.737 / 0.765)\",\n indigoA11: \"color(display-p3 0.256 0.354 0.755)\",\n indigoA12: \"color(display-p3 0.133 0.175 0.348)\",\n};\nconst blue = {\n blue1: \"#fbfdff\",\n blue2: \"#f4faff\",\n blue3: \"#e6f4fe\",\n blue4: \"#d5efff\",\n blue5: \"#c2e5ff\",\n blue6: \"#acd8fc\",\n blue7: \"#8ec8f6\",\n blue8: \"#5eb1ef\",\n blue9: \"#0090ff\",\n blue10: \"#0588f0\",\n blue11: \"#0d74ce\",\n blue12: \"#113264\",\n};\nconst blueA = {\n blueA1: \"#0080ff04\",\n blueA2: \"#008cff0b\",\n blueA3: \"#008ff519\",\n blueA4: \"#009eff2a\",\n blueA5: \"#0093ff3d\",\n blueA6: \"#0088f653\",\n blueA7: \"#0083eb71\",\n blueA8: \"#0084e6a1\",\n blueA9: \"#0090ff\",\n blueA10: \"#0086f0fa\",\n blueA11: \"#006dcbf2\",\n blueA12: \"#002359ee\",\n};\nconst blueP3 = {\n blue1: \"color(display-p3 0.986 0.992 0.999)\",\n blue2: \"color(display-p3 0.96 0.979 0.998)\",\n blue3: \"color(display-p3 0.912 0.956 0.991)\",\n blue4: \"color(display-p3 0.853 0.932 1)\",\n blue5: \"color(display-p3 0.788 0.894 0.998)\",\n blue6: \"color(display-p3 0.709 0.843 0.976)\",\n blue7: \"color(display-p3 0.606 0.777 0.947)\",\n blue8: \"color(display-p3 0.451 0.688 0.917)\",\n blue9: \"color(display-p3 0.247 0.556 0.969)\",\n blue10: \"color(display-p3 0.234 0.523 0.912)\",\n blue11: \"color(display-p3 0.15 0.44 0.84)\",\n blue12: \"color(display-p3 0.102 0.193 0.379)\",\n};\nconst blueP3A = {\n blueA1: \"color(display-p3 0.024 0.514 1 / 0.016)\",\n blueA2: \"color(display-p3 0.024 0.514 0.906 / 0.04)\",\n blueA3: \"color(display-p3 0.012 0.506 0.914 / 0.087)\",\n blueA4: \"color(display-p3 0.008 0.545 1 / 0.146)\",\n blueA5: \"color(display-p3 0.004 0.502 0.984 / 0.212)\",\n blueA6: \"color(display-p3 0.004 0.463 0.922 / 0.291)\",\n blueA7: \"color(display-p3 0.004 0.431 0.863 / 0.393)\",\n blueA8: \"color(display-p3 0 0.427 0.851 / 0.55)\",\n blueA9: \"color(display-p3 0 0.412 0.961 / 0.753)\",\n blueA10: \"color(display-p3 0 0.376 0.886 / 0.765)\",\n blueA11: \"color(display-p3 0.15 0.44 0.84)\",\n blueA12: \"color(display-p3 0.102 0.193 0.379)\",\n};\nconst cyan = {\n cyan1: \"#fafdfe\",\n cyan2: \"#f2fafb\",\n cyan3: \"#def7f9\",\n cyan4: \"#caf1f6\",\n cyan5: \"#b5e9f0\",\n cyan6: \"#9ddde7\",\n cyan7: \"#7dcedc\",\n cyan8: \"#3db9cf\",\n cyan9: \"#00a2c7\",\n cyan10: \"#0797b9\",\n cyan11: \"#107d98\",\n cyan12: \"#0d3c48\",\n};\nconst cyanA = {\n cyanA1: \"#0099cc05\",\n cyanA2: \"#009db10d\",\n cyanA3: \"#00c2d121\",\n cyanA4: \"#00bcd435\",\n cyanA5: \"#01b4cc4a\",\n cyanA6: \"#00a7c162\",\n cyanA7: \"#009fbb82\",\n cyanA8: \"#00a3c0c2\",\n cyanA9: \"#00a2c7\",\n cyanA10: \"#0094b7f8\",\n cyanA11: \"#007491ef\",\n cyanA12: \"#00323ef2\",\n};\nconst cyanP3 = {\n cyan1: \"color(display-p3 0.982 0.992 0.996)\",\n cyan2: \"color(display-p3 0.955 0.981 0.984)\",\n cyan3: \"color(display-p3 0.888 0.965 0.975)\",\n cyan4: \"color(display-p3 0.821 0.941 0.959)\",\n cyan5: \"color(display-p3 0.751 0.907 0.935)\",\n cyan6: \"color(display-p3 0.671 0.862 0.9)\",\n cyan7: \"color(display-p3 0.564 0.8 0.854)\",\n cyan8: \"color(display-p3 0.388 0.715 0.798)\",\n cyan9: \"color(display-p3 0.282 0.627 0.765)\",\n cyan10: \"color(display-p3 0.264 0.583 0.71)\",\n cyan11: \"color(display-p3 0.08 0.48 0.63)\",\n cyan12: \"color(display-p3 0.108 0.232 0.277)\",\n};\nconst cyanP3A = {\n cyanA1: \"color(display-p3 0.02 0.608 0.804 / 0.02)\",\n cyanA2: \"color(display-p3 0.02 0.557 0.647 / 0.044)\",\n cyanA3: \"color(display-p3 0.004 0.694 0.796 / 0.114)\",\n cyanA4: \"color(display-p3 0.004 0.678 0.784 / 0.181)\",\n cyanA5: \"color(display-p3 0.004 0.624 0.733 / 0.248)\",\n cyanA6: \"color(display-p3 0.004 0.584 0.706 / 0.33)\",\n cyanA7: \"color(display-p3 0.004 0.541 0.667 / 0.436)\",\n cyanA8: \"color(display-p3 0 0.533 0.667 / 0.612)\",\n cyanA9: \"color(display-p3 0 0.482 0.675 / 0.718)\",\n cyanA10: \"color(display-p3 0 0.435 0.608 / 0.738)\",\n cyanA11: \"color(display-p3 0.08 0.48 0.63)\",\n cyanA12: \"color(display-p3 0.108 0.232 0.277)\",\n};\nconst teal = {\n teal1: \"#fafefd\",\n teal2: \"#f3fbf9\",\n teal3: \"#e0f8f3\",\n teal4: \"#ccf3ea\",\n teal5: \"#b8eae0\",\n teal6: \"#a1ded2\",\n teal7: \"#83cdc1\",\n teal8: \"#53b9ab\",\n teal9: \"#12a594\",\n teal10: \"#0d9b8a\",\n teal11: \"#008573\",\n teal12: \"#0d3d38\",\n};\nconst tealA = {\n tealA1: \"#00cc9905\",\n tealA2: \"#00aa800c\",\n tealA3: \"#00c69d1f\",\n tealA4: \"#00c39633\",\n tealA5: \"#00b49047\",\n tealA6: \"#00a6855e\",\n tealA7: \"#0099807c\",\n tealA8: \"#009783ac\",\n tealA9: \"#009e8ced\",\n tealA10: \"#009684f2\",\n tealA11: \"#008573\",\n tealA12: \"#00332df2\",\n};\nconst tealP3 = {\n teal1: \"color(display-p3 0.983 0.996 0.992)\",\n teal2: \"color(display-p3 0.958 0.983 0.976)\",\n teal3: \"color(display-p3 0.895 0.971 0.952)\",\n teal4: \"color(display-p3 0.831 0.949 0.92)\",\n teal5: \"color(display-p3 0.761 0.914 0.878)\",\n teal6: \"color(display-p3 0.682 0.864 0.825)\",\n teal7: \"color(display-p3 0.581 0.798 0.756)\",\n teal8: \"color(display-p3 0.433 0.716 0.671)\",\n teal9: \"color(display-p3 0.297 0.637 0.581)\",\n teal10: \"color(display-p3 0.275 0.599 0.542)\",\n teal11: \"color(display-p3 0.08 0.5 0.43)\",\n teal12: \"color(display-p3 0.11 0.235 0.219)\",\n};\nconst tealP3A = {\n tealA1: \"color(display-p3 0.024 0.757 0.514 / 0.016)\",\n tealA2: \"color(display-p3 0.02 0.647 0.467 / 0.044)\",\n tealA3: \"color(display-p3 0.004 0.741 0.557 / 0.106)\",\n tealA4: \"color(display-p3 0.004 0.702 0.537 / 0.169)\",\n tealA5: \"color(display-p3 0.004 0.643 0.494 / 0.24)\",\n tealA6: \"color(display-p3 0.004 0.569 0.447 / 0.318)\",\n tealA7: \"color(display-p3 0.004 0.518 0.424 / 0.42)\",\n tealA8: \"color(display-p3 0 0.506 0.424 / 0.569)\",\n tealA9: \"color(display-p3 0 0.482 0.404 / 0.702)\",\n tealA10: \"color(display-p3 0 0.451 0.369 / 0.726)\",\n tealA11: \"color(display-p3 0.08 0.5 0.43)\",\n tealA12: \"color(display-p3 0.11 0.235 0.219)\",\n};\nconst jade = {\n jade1: \"#fbfefd\",\n jade2: \"#f4fbf7\",\n jade3: \"#e6f7ed\",\n jade4: \"#d6f1e3\",\n jade5: \"#c3e9d7\",\n jade6: \"#acdec8\",\n jade7: \"#8bceb6\",\n jade8: \"#56ba9f\",\n jade9: \"#29a383\",\n jade10: \"#26997b\",\n jade11: \"#208368\",\n jade12: \"#1d3b31\",\n};\nconst jadeA = {\n jadeA1: \"#00c08004\",\n jadeA2: \"#00a3460b\",\n jadeA3: \"#00ae4819\",\n jadeA4: \"#00a85129\",\n jadeA5: \"#00a2553c\",\n jadeA6: \"#009a5753\",\n jadeA7: \"#00945f74\",\n jadeA8: \"#00976ea9\",\n jadeA9: \"#00916bd6\",\n jadeA10: \"#008764d9\",\n jadeA11: \"#007152df\",\n jadeA12: \"#002217e2\",\n};\nconst jadeP3 = {\n jade1: \"color(display-p3 0.986 0.996 0.992)\",\n jade2: \"color(display-p3 0.962 0.983 0.969)\",\n jade3: \"color(display-p3 0.912 0.965 0.932)\",\n jade4: \"color(display-p3 0.858 0.941 0.893)\",\n jade5: \"color(display-p3 0.795 0.909 0.847)\",\n jade6: \"color(display-p3 0.715 0.864 0.791)\",\n jade7: \"color(display-p3 0.603 0.802 0.718)\",\n jade8: \"color(display-p3 0.44 0.72 0.629)\",\n jade9: \"color(display-p3 0.319 0.63 0.521)\",\n jade10: \"color(display-p3 0.299 0.592 0.488)\",\n jade11: \"color(display-p3 0.15 0.5 0.37)\",\n jade12: \"color(display-p3 0.142 0.229 0.194)\",\n};\nconst jadeP3A = {\n jadeA1: \"color(display-p3 0.024 0.757 0.514 / 0.016)\",\n jadeA2: \"color(display-p3 0.024 0.612 0.22 / 0.04)\",\n jadeA3: \"color(display-p3 0.012 0.596 0.235 / 0.087)\",\n jadeA4: \"color(display-p3 0.008 0.588 0.255 / 0.142)\",\n jadeA5: \"color(display-p3 0.004 0.561 0.251 / 0.204)\",\n jadeA6: \"color(display-p3 0.004 0.525 0.278 / 0.287)\",\n jadeA7: \"color(display-p3 0.004 0.506 0.29 / 0.397)\",\n jadeA8: \"color(display-p3 0 0.506 0.337 / 0.561)\",\n jadeA9: \"color(display-p3 0 0.459 0.298 / 0.683)\",\n jadeA10: \"color(display-p3 0 0.42 0.271 / 0.702)\",\n jadeA11: \"color(display-p3 0.15 0.5 0.37)\",\n jadeA12: \"color(display-p3 0.142 0.229 0.194)\",\n};\nconst green = {\n green1: \"#fbfefc\",\n green2: \"#f4fbf6\",\n green3: \"#e6f6eb\",\n green4: \"#d6f1df\",\n green5: \"#c4e8d1\",\n green6: \"#adddc0\",\n green7: \"#8eceaa\",\n green8: \"#5bb98b\",\n green9: \"#30a46c\",\n green10: \"#2b9a66\",\n green11: \"#218358\",\n green12: \"#193b2d\",\n};\nconst greenA = {\n greenA1: \"#00c04004\",\n greenA2: \"#00a32f0b\",\n greenA3: \"#00a43319\",\n greenA4: \"#00a83829\",\n greenA5: \"#019c393b\",\n greenA6: \"#00963c52\",\n greenA7: \"#00914071\",\n greenA8: \"#00924ba4\",\n greenA9: \"#008f4acf\",\n greenA10: \"#008647d4\",\n greenA11: \"#00713fde\",\n greenA12: \"#002616e6\",\n};\nconst greenP3 = {\n green1: \"color(display-p3 0.986 0.996 0.989)\",\n green2: \"color(display-p3 0.963 0.983 0.967)\",\n green3: \"color(display-p3 0.913 0.964 0.925)\",\n green4: \"color(display-p3 0.859 0.94 0.879)\",\n green5: \"color(display-p3 0.796 0.907 0.826)\",\n green6: \"color(display-p3 0.718 0.863 0.761)\",\n green7: \"color(display-p3 0.61 0.801 0.675)\",\n green8: \"color(display-p3 0.451 0.715 0.559)\",\n green9: \"color(display-p3 0.332 0.634 0.442)\",\n green10: \"color(display-p3 0.308 0.595 0.417)\",\n green11: \"color(display-p3 0.19 0.5 0.32)\",\n green12: \"color(display-p3 0.132 0.228 0.18)\",\n};\nconst greenP3A = {\n greenA1: \"color(display-p3 0.024 0.757 0.267 / 0.016)\",\n greenA2: \"color(display-p3 0.024 0.565 0.129 / 0.036)\",\n greenA3: \"color(display-p3 0.012 0.596 0.145 / 0.087)\",\n greenA4: \"color(display-p3 0.008 0.588 0.145 / 0.142)\",\n greenA5: \"color(display-p3 0.004 0.541 0.157 / 0.204)\",\n greenA6: \"color(display-p3 0.004 0.518 0.157 / 0.283)\",\n greenA7: \"color(display-p3 0.004 0.486 0.165 / 0.389)\",\n greenA8: \"color(display-p3 0 0.478 0.2 / 0.55)\",\n greenA9: \"color(display-p3 0 0.455 0.165 / 0.667)\",\n greenA10: \"color(display-p3 0 0.416 0.153 / 0.691)\",\n greenA11: \"color(display-p3 0.19 0.5 0.32)\",\n greenA12: \"color(display-p3 0.132 0.228 0.18)\",\n};\nconst grass = {\n grass1: \"#fbfefb\",\n grass2: \"#f5fbf5\",\n grass3: \"#e9f6e9\",\n grass4: \"#daf1db\",\n grass5: \"#c9e8ca\",\n grass6: \"#b2ddb5\",\n grass7: \"#94ce9a\",\n grass8: \"#65ba74\",\n grass9: \"#46a758\",\n grass10: \"#3e9b4f\",\n grass11: \"#2a7e3b\",\n grass12: \"#203c25\",\n};\nconst grassA = {\n grassA1: \"#00c00004\",\n grassA2: \"#0099000a\",\n grassA3: \"#00970016\",\n grassA4: \"#009f0725\",\n grassA5: \"#00930536\",\n grassA6: \"#008f0a4d\",\n grassA7: \"#018b0f6b\",\n grassA8: \"#008d199a\",\n grassA9: \"#008619b9\",\n grassA10: \"#007b17c1\",\n grassA11: \"#006514d5\",\n grassA12: \"#002006df\",\n};\nconst grassP3 = {\n grass1: \"color(display-p3 0.986 0.996 0.985)\",\n grass2: \"color(display-p3 0.966 0.983 0.964)\",\n grass3: \"color(display-p3 0.923 0.965 0.917)\",\n grass4: \"color(display-p3 0.872 0.94 0.865)\",\n grass5: \"color(display-p3 0.811 0.908 0.802)\",\n grass6: \"color(display-p3 0.733 0.864 0.724)\",\n grass7: \"color(display-p3 0.628 0.803 0.622)\",\n grass8: \"color(display-p3 0.477 0.72 0.482)\",\n grass9: \"color(display-p3 0.38 0.647 0.378)\",\n grass10: \"color(display-p3 0.344 0.598 0.342)\",\n grass11: \"color(display-p3 0.263 0.488 0.261)\",\n grass12: \"color(display-p3 0.151 0.233 0.153)\",\n};\nconst grassP3A = {\n grassA1: \"color(display-p3 0.024 0.757 0.024 / 0.016)\",\n grassA2: \"color(display-p3 0.024 0.565 0.024 / 0.036)\",\n grassA3: \"color(display-p3 0.059 0.576 0.008 / 0.083)\",\n grassA4: \"color(display-p3 0.035 0.565 0.008 / 0.134)\",\n grassA5: \"color(display-p3 0.047 0.545 0.008 / 0.197)\",\n grassA6: \"color(display-p3 0.031 0.502 0.004 / 0.275)\",\n grassA7: \"color(display-p3 0.012 0.482 0.004 / 0.377)\",\n grassA8: \"color(display-p3 0 0.467 0.008 / 0.522)\",\n grassA9: \"color(display-p3 0.008 0.435 0 / 0.624)\",\n grassA10: \"color(display-p3 0.008 0.388 0 / 0.659)\",\n grassA11: \"color(display-p3 0.263 0.488 0.261)\",\n grassA12: \"color(display-p3 0.151 0.233 0.153)\",\n};\nconst brown = {\n brown1: \"#fefdfc\",\n brown2: \"#fcf9f6\",\n brown3: \"#f6eee7\",\n brown4: \"#f0e4d9\",\n brown5: \"#ebdaca\",\n brown6: \"#e4cdb7\",\n brown7: \"#dcbc9f\",\n brown8: \"#cea37e\",\n brown9: \"#ad7f58\",\n brown10: \"#a07553\",\n brown11: \"#815e46\",\n brown12: \"#3e332e\",\n};\nconst brownA = {\n brownA1: \"#aa550003\",\n brownA2: \"#aa550009\",\n brownA3: \"#a04b0018\",\n brownA4: \"#9b4a0026\",\n brownA5: \"#9f4d0035\",\n brownA6: \"#a04e0048\",\n brownA7: \"#a34e0060\",\n brownA8: \"#9f4a0081\",\n brownA9: \"#823c00a7\",\n brownA10: \"#723300ac\",\n brownA11: \"#522100b9\",\n brownA12: \"#140600d1\",\n};\nconst brownP3 = {\n brown1: \"color(display-p3 0.995 0.992 0.989)\",\n brown2: \"color(display-p3 0.987 0.976 0.964)\",\n brown3: \"color(display-p3 0.959 0.936 0.909)\",\n brown4: \"color(display-p3 0.934 0.897 0.855)\",\n brown5: \"color(display-p3 0.909 0.856 0.798)\",\n brown6: \"color(display-p3 0.88 0.808 0.73)\",\n brown7: \"color(display-p3 0.841 0.742 0.639)\",\n brown8: \"color(display-p3 0.782 0.647 0.514)\",\n brown9: \"color(display-p3 0.651 0.505 0.368)\",\n brown10: \"color(display-p3 0.601 0.465 0.344)\",\n brown11: \"color(display-p3 0.485 0.374 0.288)\",\n brown12: \"color(display-p3 0.236 0.202 0.183)\",\n};\nconst brownP3A = {\n brownA1: \"color(display-p3 0.675 0.349 0.024 / 0.012)\",\n brownA2: \"color(display-p3 0.675 0.349 0.024 / 0.036)\",\n brownA3: \"color(display-p3 0.573 0.314 0.012 / 0.091)\",\n brownA4: \"color(display-p3 0.545 0.302 0.008 / 0.146)\",\n brownA5: \"color(display-p3 0.561 0.29 0.004 / 0.204)\",\n brownA6: \"color(display-p3 0.553 0.294 0.004 / 0.271)\",\n brownA7: \"color(display-p3 0.557 0.286 0.004 / 0.361)\",\n brownA8: \"color(display-p3 0.549 0.275 0.004 / 0.487)\",\n brownA9: \"color(display-p3 0.447 0.22 0 / 0.632)\",\n brownA10: \"color(display-p3 0.388 0.188 0 / 0.655)\",\n brownA11: \"color(display-p3 0.485 0.374 0.288)\",\n brownA12: \"color(display-p3 0.236 0.202 0.183)\",\n};\nconst bronze = {\n bronze1: \"#fdfcfc\",\n bronze2: \"#fdf7f5\",\n bronze3: \"#f6edea\",\n bronze4: \"#efe4df\",\n bronze5: \"#e7d9d3\",\n bronze6: \"#dfcdc5\",\n bronze7: \"#d3bcb3\",\n bronze8: \"#c2a499\",\n bronze9: \"#a18072\",\n bronze10: \"#957468\",\n bronze11: \"#7d5e54\",\n bronze12: \"#43302b\",\n};\nconst bronzeA = {\n bronzeA1: \"#55000003\",\n bronzeA2: \"#cc33000a\",\n bronzeA3: \"#92250015\",\n bronzeA4: \"#80280020\",\n bronzeA5: \"#7423002c\",\n bronzeA6: \"#7324003a\",\n bronzeA7: \"#6c1f004c\",\n bronzeA8: \"#671c0066\",\n bronzeA9: \"#551a008d\",\n bronzeA10: \"#4c150097\",\n bronzeA11: \"#3d0f00ab\",\n bronzeA12: \"#1d0600d4\",\n};\nconst bronzeP3 = {\n bronze1: \"color(display-p3 0.991 0.988 0.988)\",\n bronze2: \"color(display-p3 0.989 0.97 0.961)\",\n bronze3: \"color(display-p3 0.958 0.932 0.919)\",\n bronze4: \"color(display-p3 0.929 0.894 0.877)\",\n bronze5: \"color(display-p3 0.898 0.853 0.832)\",\n bronze6: \"color(display-p3 0.861 0.805 0.778)\",\n bronze7: \"color(display-p3 0.812 0.739 0.706)\",\n bronze8: \"color(display-p3 0.741 0.647 0.606)\",\n bronze9: \"color(display-p3 0.611 0.507 0.455)\",\n bronze10: \"color(display-p3 0.563 0.461 0.414)\",\n bronze11: \"color(display-p3 0.471 0.373 0.336)\",\n bronze12: \"color(display-p3 0.251 0.191 0.172)\",\n};\nconst bronzeP3A = {\n bronzeA1: \"color(display-p3 0.349 0.024 0.024 / 0.012)\",\n bronzeA2: \"color(display-p3 0.71 0.22 0.024 / 0.04)\",\n bronzeA3: \"color(display-p3 0.482 0.2 0.008 / 0.083)\",\n bronzeA4: \"color(display-p3 0.424 0.133 0.004 / 0.122)\",\n bronzeA5: \"color(display-p3 0.4 0.145 0.004 / 0.169)\",\n bronzeA6: \"color(display-p3 0.388 0.125 0.004 / 0.224)\",\n bronzeA7: \"color(display-p3 0.365 0.11 0.004 / 0.295)\",\n bronzeA8: \"color(display-p3 0.341 0.102 0.004 / 0.393)\",\n bronzeA9: \"color(display-p3 0.29 0.094 0 / 0.546)\",\n bronzeA10: \"color(display-p3 0.255 0.082 0 / 0.585)\",\n bronzeA11: \"color(display-p3 0.471 0.373 0.336)\",\n bronzeA12: \"color(display-p3 0.251 0.191 0.172)\",\n};\nconst gold = {\n gold1: \"#fdfdfc\",\n gold2: \"#faf9f2\",\n gold3: \"#f2f0e7\",\n gold4: \"#eae6db\",\n gold5: \"#e1dccf\",\n gold6: \"#d8d0bf\",\n gold7: \"#cbc0aa\",\n gold8: \"#b9a88d\",\n gold9: \"#978365\",\n gold10: \"#8c7a5e\",\n gold11: \"#71624b\",\n gold12: \"#3b352b\",\n};\nconst goldA = {\n goldA1: \"#55550003\",\n goldA2: \"#9d8a000d\",\n goldA3: \"#75600018\",\n goldA4: \"#6b4e0024\",\n goldA5: \"#60460030\",\n goldA6: \"#64440040\",\n goldA7: \"#63420055\",\n goldA8: \"#633d0072\",\n goldA9: \"#5332009a\",\n goldA10: \"#492d00a1\",\n goldA11: \"#362100b4\",\n goldA12: \"#130c00d4\",\n};\nconst goldP3 = {\n gold1: \"color(display-p3 0.992 0.992 0.989)\",\n gold2: \"color(display-p3 0.98 0.976 0.953)\",\n gold3: \"color(display-p3 0.947 0.94 0.909)\",\n gold4: \"color(display-p3 0.914 0.904 0.865)\",\n gold5: \"color(display-p3 0.88 0.865 0.816)\",\n gold6: \"color(display-p3 0.84 0.818 0.756)\",\n gold7: \"color(display-p3 0.788 0.753 0.677)\",\n gold8: \"color(display-p3 0.715 0.66 0.565)\",\n gold9: \"color(display-p3 0.579 0.517 0.41)\",\n gold10: \"color(display-p3 0.538 0.479 0.38)\",\n gold11: \"color(display-p3 0.433 0.386 0.305)\",\n gold12: \"color(display-p3 0.227 0.209 0.173)\",\n};\nconst goldP3A = {\n goldA1: \"color(display-p3 0.349 0.349 0.024 / 0.012)\",\n goldA2: \"color(display-p3 0.592 0.514 0.024 / 0.048)\",\n goldA3: \"color(display-p3 0.4 0.357 0.012 / 0.091)\",\n goldA4: \"color(display-p3 0.357 0.298 0.008 / 0.134)\",\n goldA5: \"color(display-p3 0.345 0.282 0.004 / 0.185)\",\n goldA6: \"color(display-p3 0.341 0.263 0.004 / 0.244)\",\n goldA7: \"color(display-p3 0.345 0.235 0.004 / 0.322)\",\n goldA8: \"color(display-p3 0.345 0.22 0.004 / 0.436)\",\n goldA9: \"color(display-p3 0.286 0.18 0 / 0.589)\",\n goldA10: \"color(display-p3 0.255 0.161 0 / 0.62)\",\n goldA11: \"color(display-p3 0.433 0.386 0.305)\",\n goldA12: \"color(display-p3 0.227 0.209 0.173)\",\n};\nconst sky = {\n sky1: \"#f9feff\",\n sky2: \"#f1fafd\",\n sky3: \"#e1f6fd\",\n sky4: \"#d1f0fa\",\n sky5: \"#bee7f5\",\n sky6: \"#a9daed\",\n sky7: \"#8dcae3\",\n sky8: \"#60b3d7\",\n sky9: \"#7ce2fe\",\n sky10: \"#74daf8\",\n sky11: \"#00749e\",\n sky12: \"#1d3e56\",\n};\nconst skyA = {\n skyA1: \"#00d5ff06\",\n skyA2: \"#00a4db0e\",\n skyA3: \"#00b3ee1e\",\n skyA4: \"#00ace42e\",\n skyA5: \"#00a1d841\",\n skyA6: \"#0092ca56\",\n skyA7: \"#0089c172\",\n skyA8: \"#0085bf9f\",\n skyA9: \"#00c7fe83\",\n skyA10: \"#00bcf38b\",\n skyA11: \"#00749e\",\n skyA12: \"#002540e2\",\n};\nconst skyP3 = {\n sky1: \"color(display-p3 0.98 0.995 0.999)\",\n sky2: \"color(display-p3 0.953 0.98 0.99)\",\n sky3: \"color(display-p3 0.899 0.963 0.989)\",\n sky4: \"color(display-p3 0.842 0.937 0.977)\",\n sky5: \"color(display-p3 0.777 0.9 0.954)\",\n sky6: \"color(display-p3 0.701 0.851 0.921)\",\n sky7: \"color(display-p3 0.604 0.785 0.879)\",\n sky8: \"color(display-p3 0.457 0.696 0.829)\",\n sky9: \"color(display-p3 0.585 0.877 0.983)\",\n sky10: \"color(display-p3 0.555 0.845 0.959)\",\n sky11: \"color(display-p3 0.193 0.448 0.605)\",\n sky12: \"color(display-p3 0.145 0.241 0.329)\",\n};\nconst skyP3A = {\n skyA1: \"color(display-p3 0.02 0.804 1 / 0.02)\",\n skyA2: \"color(display-p3 0.024 0.592 0.757 / 0.048)\",\n skyA3: \"color(display-p3 0.004 0.655 0.886 / 0.102)\",\n skyA4: \"color(display-p3 0.004 0.604 0.851 / 0.157)\",\n skyA5: \"color(display-p3 0.004 0.565 0.792 / 0.224)\",\n skyA6: \"color(display-p3 0.004 0.502 0.737 / 0.299)\",\n skyA7: \"color(display-p3 0.004 0.459 0.694 / 0.397)\",\n skyA8: \"color(display-p3 0 0.435 0.682 / 0.542)\",\n skyA9: \"color(display-p3 0.004 0.71 0.965 / 0.416)\",\n skyA10: \"color(display-p3 0.004 0.647 0.914 / 0.444)\",\n skyA11: \"color(display-p3 0.193 0.448 0.605)\",\n skyA12: \"color(display-p3 0.145 0.241 0.329)\",\n};\nconst mint = {\n mint1: \"#f9fefd\",\n mint2: \"#f2fbf9\",\n mint3: \"#ddf9f2\",\n mint4: \"#c8f4e9\",\n mint5: \"#b3ecde\",\n mint6: \"#9ce0d0\",\n mint7: \"#7ecfbd\",\n mint8: \"#4cbba5\",\n mint9: \"#86ead4\",\n mint10: \"#7de0cb\",\n mint11: \"#027864\",\n mint12: \"#16433c\",\n};\nconst mintA = {\n mintA1: \"#00d5aa06\",\n mintA2: \"#00b18a0d\",\n mintA3: \"#00d29e22\",\n mintA4: \"#00cc9937\",\n mintA5: \"#00c0914c\",\n mintA6: \"#00b08663\",\n mintA7: \"#00a17d81\",\n mintA8: \"#009e7fb3\",\n mintA9: \"#00d3a579\",\n mintA10: \"#00c39982\",\n mintA11: \"#007763fd\",\n mintA12: \"#00312ae9\",\n};\nconst mintP3 = {\n mint1: \"color(display-p3 0.98 0.995 0.992)\",\n mint2: \"color(display-p3 0.957 0.985 0.977)\",\n mint3: \"color(display-p3 0.888 0.972 0.95)\",\n mint4: \"color(display-p3 0.819 0.951 0.916)\",\n mint5: \"color(display-p3 0.747 0.918 0.873)\",\n mint6: \"color(display-p3 0.668 0.87 0.818)\",\n mint7: \"color(display-p3 0.567 0.805 0.744)\",\n mint8: \"color(display-p3 0.42 0.724 0.649)\",\n mint9: \"color(display-p3 0.62 0.908 0.834)\",\n mint10: \"color(display-p3 0.585 0.871 0.797)\",\n mint11: \"color(display-p3 0.203 0.463 0.397)\",\n mint12: \"color(display-p3 0.136 0.259 0.236)\",\n};\nconst mintP3A = {\n mintA1: \"color(display-p3 0.02 0.804 0.608 / 0.02)\",\n mintA2: \"color(display-p3 0.02 0.647 0.467 / 0.044)\",\n mintA3: \"color(display-p3 0.004 0.761 0.553 / 0.114)\",\n mintA4: \"color(display-p3 0.004 0.741 0.545 / 0.181)\",\n mintA5: \"color(display-p3 0.004 0.678 0.51 / 0.255)\",\n mintA6: \"color(display-p3 0.004 0.616 0.463 / 0.334)\",\n mintA7: \"color(display-p3 0.004 0.549 0.412 / 0.432)\",\n mintA8: \"color(display-p3 0 0.529 0.392 / 0.581)\",\n mintA9: \"color(display-p3 0.004 0.765 0.569 / 0.381)\",\n mintA10: \"color(display-p3 0.004 0.69 0.51 / 0.416)\",\n mintA11: \"color(display-p3 0.203 0.463 0.397)\",\n mintA12: \"color(display-p3 0.136 0.259 0.236)\",\n};\nconst lime = {\n lime1: \"#fcfdfa\",\n lime2: \"#f8faf3\",\n lime3: \"#eef6d6\",\n lime4: \"#e2f0bd\",\n lime5: \"#d3e7a6\",\n lime6: \"#c2da91\",\n lime7: \"#abc978\",\n lime8: \"#8db654\",\n lime9: \"#bdee63\",\n lime10: \"#b0e64c\",\n lime11: \"#5c7c2f\",\n lime12: \"#37401c\",\n};\nconst limeA = {\n limeA1: \"#66990005\",\n limeA2: \"#6b95000c\",\n limeA3: \"#96c80029\",\n limeA4: \"#8fc60042\",\n limeA5: \"#81bb0059\",\n limeA6: \"#72aa006e\",\n limeA7: \"#61990087\",\n limeA8: \"#559200ab\",\n limeA9: \"#93e4009c\",\n limeA10: \"#8fdc00b3\",\n limeA11: \"#375f00d0\",\n limeA12: \"#1e2900e3\",\n};\nconst limeP3 = {\n lime1: \"color(display-p3 0.989 0.992 0.981)\",\n lime2: \"color(display-p3 0.975 0.98 0.954)\",\n lime3: \"color(display-p3 0.939 0.965 0.851)\",\n lime4: \"color(display-p3 0.896 0.94 0.76)\",\n lime5: \"color(display-p3 0.843 0.903 0.678)\",\n lime6: \"color(display-p3 0.778 0.852 0.599)\",\n lime7: \"color(display-p3 0.694 0.784 0.508)\",\n lime8: \"color(display-p3 0.585 0.707 0.378)\",\n lime9: \"color(display-p3 0.78 0.928 0.466)\",\n lime10: \"color(display-p3 0.734 0.896 0.397)\",\n lime11: \"color(display-p3 0.386 0.482 0.227)\",\n lime12: \"color(display-p3 0.222 0.25 0.128)\",\n};\nconst limeP3A = {\n limeA1: \"color(display-p3 0.412 0.608 0.02 / 0.02)\",\n limeA2: \"color(display-p3 0.514 0.592 0.024 / 0.048)\",\n limeA3: \"color(display-p3 0.584 0.765 0.008 / 0.15)\",\n limeA4: \"color(display-p3 0.561 0.757 0.004 / 0.24)\",\n limeA5: \"color(display-p3 0.514 0.698 0.004 / 0.322)\",\n limeA6: \"color(display-p3 0.443 0.627 0 / 0.4)\",\n limeA7: \"color(display-p3 0.376 0.561 0.004 / 0.491)\",\n limeA8: \"color(display-p3 0.333 0.529 0 / 0.624)\",\n limeA9: \"color(display-p3 0.588 0.867 0 / 0.534)\",\n limeA10: \"color(display-p3 0.561 0.827 0 / 0.604)\",\n limeA11: \"color(display-p3 0.386 0.482 0.227)\",\n limeA12: \"color(display-p3 0.222 0.25 0.128)\",\n};\nconst yellow = {\n yellow1: \"#fdfdf9\",\n yellow2: \"#fefce9\",\n yellow3: \"#fffab8\",\n yellow4: \"#fff394\",\n yellow5: \"#ffe770\",\n yellow6: \"#f3d768\",\n yellow7: \"#e4c767\",\n yellow8: \"#d5ae39\",\n yellow9: \"#ffe629\",\n yellow10: \"#ffdc00\",\n yellow11: \"#9e6c00\",\n yellow12: \"#473b1f\",\n};\nconst yellowA = {\n yellowA1: \"#aaaa0006\",\n yellowA2: \"#f4dd0016\",\n yellowA3: \"#ffee0047\",\n yellowA4: \"#ffe3016b\",\n yellowA5: \"#ffd5008f\",\n yellowA6: \"#ebbc0097\",\n yellowA7: \"#d2a10098\",\n yellowA8: \"#c99700c6\",\n yellowA9: \"#ffe100d6\",\n yellowA10: \"#ffdc00\",\n yellowA11: \"#9e6c00\",\n yellowA12: \"#2e2000e0\",\n};\nconst yellowP3 = {\n yellow1: \"color(display-p3 0.992 0.992 0.978)\",\n yellow2: \"color(display-p3 0.995 0.99 0.922)\",\n yellow3: \"color(display-p3 0.997 0.982 0.749)\",\n yellow4: \"color(display-p3 0.992 0.953 0.627)\",\n yellow5: \"color(display-p3 0.984 0.91 0.51)\",\n yellow6: \"color(display-p3 0.934 0.847 0.474)\",\n yellow7: \"color(display-p3 0.876 0.785 0.46)\",\n yellow8: \"color(display-p3 0.811 0.689 0.313)\",\n yellow9: \"color(display-p3 1 0.92 0.22)\",\n yellow10: \"color(display-p3 0.977 0.868 0.291)\",\n yellow11: \"color(display-p3 0.6 0.44 0)\",\n yellow12: \"color(display-p3 0.271 0.233 0.137)\",\n};\nconst yellowP3A = {\n yellowA1: \"color(display-p3 0.675 0.675 0.024 / 0.024)\",\n yellowA2: \"color(display-p3 0.953 0.855 0.008 / 0.079)\",\n yellowA3: \"color(display-p3 0.988 0.925 0.004 / 0.251)\",\n yellowA4: \"color(display-p3 0.98 0.875 0.004 / 0.373)\",\n yellowA5: \"color(display-p3 0.969 0.816 0.004 / 0.491)\",\n yellowA6: \"color(display-p3 0.875 0.71 0 / 0.526)\",\n yellowA7: \"color(display-p3 0.769 0.604 0 / 0.542)\",\n yellowA8: \"color(display-p3 0.725 0.549 0 / 0.687)\",\n yellowA9: \"color(display-p3 1 0.898 0 / 0.781)\",\n yellowA10: \"color(display-p3 0.969 0.812 0 / 0.71)\",\n yellowA11: \"color(display-p3 0.6 0.44 0)\",\n yellowA12: \"color(display-p3 0.271 0.233 0.137)\",\n};\nconst amber = {\n amber1: \"#fefdfb\",\n amber2: \"#fefbe9\",\n amber3: \"#fff7c2\",\n amber4: \"#ffee9c\",\n amber5: \"#fbe577\",\n amber6: \"#f3d673\",\n amber7: \"#e9c162\",\n amber8: \"#e2a336\",\n amber9: \"#ffc53d\",\n amber10: \"#ffba18\",\n amber11: \"#ab6400\",\n amber12: \"#4f3422\",\n};\nconst amberA = {\n amberA1: \"#c0800004\",\n amberA2: \"#f4d10016\",\n amberA3: \"#ffde003d\",\n amberA4: \"#ffd40063\",\n amberA5: \"#f8cf0088\",\n amberA6: \"#eab5008c\",\n amberA7: \"#dc9b009d\",\n amberA8: \"#da8a00c9\",\n amberA9: \"#ffb300c2\",\n amberA10: \"#ffb300e7\",\n amberA11: \"#ab6400\",\n amberA12: \"#341500dd\",\n};\nconst amberP3 = {\n amber1: \"color(display-p3 0.995 0.992 0.985)\",\n amber2: \"color(display-p3 0.994 0.986 0.921)\",\n amber3: \"color(display-p3 0.994 0.969 0.782)\",\n amber4: \"color(display-p3 0.989 0.937 0.65)\",\n amber5: \"color(display-p3 0.97 0.902 0.527)\",\n amber6: \"color(display-p3 0.936 0.844 0.506)\",\n amber7: \"color(display-p3 0.89 0.762 0.443)\",\n amber8: \"color(display-p3 0.85 0.65 0.3)\",\n amber9: \"color(display-p3 1 0.77 0.26)\",\n amber10: \"color(display-p3 0.959 0.741 0.274)\",\n amber11: \"color(display-p3 0.64 0.4 0)\",\n amber12: \"color(display-p3 0.294 0.208 0.145)\",\n};\nconst amberP3A = {\n amberA1: \"color(display-p3 0.757 0.514 0.024 / 0.016)\",\n amberA2: \"color(display-p3 0.902 0.804 0.008 / 0.079)\",\n amberA3: \"color(display-p3 0.965 0.859 0.004 / 0.22)\",\n amberA4: \"color(display-p3 0.969 0.82 0.004 / 0.35)\",\n amberA5: \"color(display-p3 0.933 0.796 0.004 / 0.475)\",\n amberA6: \"color(display-p3 0.875 0.682 0.004 / 0.495)\",\n amberA7: \"color(display-p3 0.804 0.573 0 / 0.557)\",\n amberA8: \"color(display-p3 0.788 0.502 0 / 0.699)\",\n amberA9: \"color(display-p3 1 0.686 0 / 0.742)\",\n amberA10: \"color(display-p3 0.945 0.643 0 / 0.726)\",\n amberA11: \"color(display-p3 0.64 0.4 0)\",\n amberA12: \"color(display-p3 0.294 0.208 0.145)\",\n};\nconst orange = {\n orange1: \"#fefcfb\",\n orange2: \"#fff7ed\",\n orange3: \"#ffefd6\",\n orange4: \"#ffdfb5\",\n orange5: \"#ffd19a\",\n orange6: \"#ffc182\",\n orange7: \"#f5ae73\",\n orange8: \"#ec9455\",\n orange9: \"#f76b15\",\n orange10: \"#ef5f00\",\n orange11: \"#cc4e00\",\n orange12: \"#582d1d\",\n};\nconst orangeA = {\n orangeA1: \"#c0400004\",\n orangeA2: \"#ff8e0012\",\n orangeA3: \"#ff9c0029\",\n orangeA4: \"#ff91014a\",\n orangeA5: \"#ff8b0065\",\n orangeA6: \"#ff81007d\",\n orangeA7: \"#ed6c008c\",\n orangeA8: \"#e35f00aa\",\n orangeA9: \"#f65e00ea\",\n orangeA10: \"#ef5f00\",\n orangeA11: \"#cc4e00\",\n orangeA12: \"#431200e2\",\n};\nconst orangeP3 = {\n orange1: \"color(display-p3 0.995 0.988 0.985)\",\n orange2: \"color(display-p3 0.994 0.968 0.934)\",\n orange3: \"color(display-p3 0.989 0.938 0.85)\",\n orange4: \"color(display-p3 1 0.874 0.687)\",\n orange5: \"color(display-p3 1 0.821 0.583)\",\n orange6: \"color(display-p3 0.975 0.767 0.545)\",\n orange7: \"color(display-p3 0.919 0.693 0.486)\",\n orange8: \"color(display-p3 0.877 0.597 0.379)\",\n orange9: \"color(display-p3 0.9 0.45 0.2)\",\n orange10: \"color(display-p3 0.87 0.409 0.164)\",\n orange11: \"color(display-p3 0.76 0.34 0)\",\n orange12: \"color(display-p3 0.323 0.185 0.127)\",\n};\nconst orangeP3A = {\n orangeA1: \"color(display-p3 0.757 0.267 0.024 / 0.016)\",\n orangeA2: \"color(display-p3 0.886 0.533 0.008 / 0.067)\",\n orangeA3: \"color(display-p3 0.922 0.584 0.008 / 0.15)\",\n orangeA4: \"color(display-p3 1 0.604 0.004 / 0.314)\",\n orangeA5: \"color(display-p3 1 0.569 0.004 / 0.416)\",\n orangeA6: \"color(display-p3 0.949 0.494 0.004 / 0.455)\",\n orangeA7: \"color(display-p3 0.839 0.408 0 / 0.514)\",\n orangeA8: \"color(display-p3 0.804 0.349 0 / 0.62)\",\n orangeA9: \"color(display-p3 0.878 0.314 0 / 0.8)\",\n orangeA10: \"color(display-p3 0.843 0.29 0 / 0.836)\",\n orangeA11: \"color(display-p3 0.76 0.34 0)\",\n orangeA12: \"color(display-p3 0.323 0.185 0.127)\",\n};\n\nconst blackA = {\n blackA1: \"rgba(0, 0, 0, 0.05)\",\n blackA2: \"rgba(0, 0, 0, 0.1)\",\n blackA3: \"rgba(0, 0, 0, 0.15)\",\n blackA4: \"rgba(0, 0, 0, 0.2)\",\n blackA5: \"rgba(0, 0, 0, 0.3)\",\n blackA6: \"rgba(0, 0, 0, 0.4)\",\n blackA7: \"rgba(0, 0, 0, 0.5)\",\n blackA8: \"rgba(0, 0, 0, 0.6)\",\n blackA9: \"rgba(0, 0, 0, 0.7)\",\n blackA10: \"rgba(0, 0, 0, 0.8)\",\n blackA11: \"rgba(0, 0, 0, 0.9)\",\n blackA12: \"rgba(0, 0, 0, 0.95)\",\n};\nconst blackP3A = {\n blackA1: \"color(display-p3 0 0 0 / 0.05)\",\n blackA2: \"color(display-p3 0 0 0 / 0.1)\",\n blackA3: \"color(display-p3 0 0 0 / 0.15)\",\n blackA4: \"color(display-p3 0 0 0 / 0.2)\",\n blackA5: \"color(display-p3 0 0 0 / 0.3)\",\n blackA6: \"color(display-p3 0 0 0 / 0.4)\",\n blackA7: \"color(display-p3 0 0 0 / 0.5)\",\n blackA8: \"color(display-p3 0 0 0 / 0.6)\",\n blackA9: \"color(display-p3 0 0 0 / 0.7)\",\n blackA10: \"color(display-p3 0 0 0 / 0.8)\",\n blackA11: \"color(display-p3 0 0 0 / 0.9)\",\n blackA12: \"color(display-p3 0 0 0 / 0.95)\",\n};\n\nconst whiteA = {\n whiteA1: \"rgba(255, 255, 255, 0.05)\",\n whiteA2: \"rgba(255, 255, 255, 0.1)\",\n whiteA3: \"rgba(255, 255, 255, 0.15)\",\n whiteA4: \"rgba(255, 255, 255, 0.2)\",\n whiteA5: \"rgba(255, 255, 255, 0.3)\",\n whiteA6: \"rgba(255, 255, 255, 0.4)\",\n whiteA7: \"rgba(255, 255, 255, 0.5)\",\n whiteA8: \"rgba(255, 255, 255, 0.6)\",\n whiteA9: \"rgba(255, 255, 255, 0.7)\",\n whiteA10: \"rgba(255, 255, 255, 0.8)\",\n whiteA11: \"rgba(255, 255, 255, 0.9)\",\n whiteA12: \"rgba(255, 255, 255, 0.95)\",\n};\nconst whiteP3A = {\n whiteA1: \"color(display-p3 1 1 1 / 0.05)\",\n whiteA2: \"color(display-p3 1 1 1 / 0.1)\",\n whiteA3: \"color(display-p3 1 1 1 / 0.15)\",\n whiteA4: \"color(display-p3 1 1 1 / 0.2)\",\n whiteA5: \"color(display-p3 1 1 1 / 0.3)\",\n whiteA6: \"color(display-p3 1 1 1 / 0.4)\",\n whiteA7: \"color(display-p3 1 1 1 / 0.5)\",\n whiteA8: \"color(display-p3 1 1 1 / 0.6)\",\n whiteA9: \"color(display-p3 1 1 1 / 0.7)\",\n whiteA10: \"color(display-p3 1 1 1 / 0.8)\",\n whiteA11: \"color(display-p3 1 1 1 / 0.9)\",\n whiteA12: \"color(display-p3 1 1 1 / 0.95)\",\n};\n\nexport { amber, amberA, amberDark, amberDarkA, amberDarkP3, amberDarkP3A, amberP3, amberP3A, blackA, blackP3A, blue, blueA, blueDark, blueDarkA, blueDarkP3, blueDarkP3A, blueP3, blueP3A, bronze, bronzeA, bronzeDark, bronzeDarkA, bronzeDarkP3, bronzeDarkP3A, bronzeP3, bronzeP3A, brown, brownA, brownDark, brownDarkA, brownDarkP3, brownDarkP3A, brownP3, brownP3A, crimson, crimsonA, crimsonDark, crimsonDarkA, crimsonDarkP3, crimsonDarkP3A, crimsonP3, crimsonP3A, cyan, cyanA, cyanDark, cyanDarkA, cyanDarkP3, cyanDarkP3A, cyanP3, cyanP3A, gold, goldA, goldDark, goldDarkA, goldDarkP3, goldDarkP3A, goldP3, goldP3A, grass, grassA, grassDark, grassDarkA, grassDarkP3, grassDarkP3A, grassP3, grassP3A, gray, grayA, grayDark, grayDarkA, grayDarkP3, grayDarkP3A, grayP3, grayP3A, green, greenA, greenDark, greenDarkA, greenDarkP3, greenDarkP3A, greenP3, greenP3A, indigo, indigoA, indigoDark, indigoDarkA, indigoDarkP3, indigoDarkP3A, indigoP3, indigoP3A, iris, irisA, irisDark, irisDarkA, irisDarkP3, irisDarkP3A, irisP3, irisP3A, jade, jadeA, jadeDark, jadeDarkA, jadeDarkP3, jadeDarkP3A, jadeP3, jadeP3A, lime, limeA, limeDark, limeDarkA, limeDarkP3, limeDarkP3A, limeP3, limeP3A, mauve, mauveA, mauveDark, mauveDarkA, mauveDarkP3, mauveDarkP3A, mauveP3, mauveP3A, mint, mintA, mintDark, mintDarkA, mintDarkP3, mintDarkP3A, mintP3, mintP3A, olive, oliveA, oliveDark, oliveDarkA, oliveDarkP3, oliveDarkP3A, oliveP3, oliveP3A, orange, orangeA, orangeDark, orangeDarkA, orangeDarkP3, orangeDarkP3A, orangeP3, orangeP3A, pink, pinkA, pinkDark, pinkDarkA, pinkDarkP3, pinkDarkP3A, pinkP3, pinkP3A, plum, plumA, plumDark, plumDarkA, plumDarkP3, plumDarkP3A, plumP3, plumP3A, purple, purpleA, purpleDark, purpleDarkA, purpleDarkP3, purpleDarkP3A, purpleP3, purpleP3A, red, redA, redDark, redDarkA, redDarkP3, redDarkP3A, redP3, redP3A, ruby, rubyA, rubyDark, rubyDarkA, rubyDarkP3, rubyDarkP3A, rubyP3, rubyP3A, sage, sageA, sageDark, sageDarkA, sageDarkP3, sageDarkP3A, sageP3, sageP3A, sand, sandA, sandDark, sandDarkA, sandDarkP3, sandDarkP3A, sandP3, sandP3A, sky, skyA, skyDark, skyDarkA, skyDarkP3, skyDarkP3A, skyP3, skyP3A, slate, slateA, slateDark, slateDarkA, slateDarkP3, slateDarkP3A, slateP3, slateP3A, teal, tealA, tealDark, tealDarkA, tealDarkP3, tealDarkP3A, tealP3, tealP3A, tomato, tomatoA, tomatoDark, tomatoDarkA, tomatoDarkP3, tomatoDarkP3A, tomatoP3, tomatoP3A, violet, violetA, violetDark, violetDarkA, violetDarkP3, violetDarkP3A, violetP3, violetP3A, whiteA, whiteP3A, yellow, yellowA, yellowDark, yellowDarkA, yellowDarkP3, yellowDarkP3A, yellowP3, yellowP3A };\n", "/**\n * Curated colour tokens \u2014 single source of truth for every hex\n * the project ships.\n *\n * The underlying scales follow a consistent 12-step semantic model:\n * step 1-2 app background / subtle background\n * step 3-5 UI element backgrounds (hover / active)\n * step 6-8 borders / separators / hovered borders\n * step 9 \"solid\" \u2014 the main brand-ish colour, used for marks /\n * text on a light surface; stays the same hex on both\n * light and dark themes\n * step 10-12 hovered solid / low- and high-contrast text\n *\n * Stable subsets are picked here so the rest of the codebase imports\n * named tokens (`UI.accent.solid`, `ELEMENT_FILL.iris.light`, \u2026)\n * instead of raw hex strings. To re-skin the editor, change the\n * mapping below in one place \u2014 every package picks it up.\n */\nimport {\n amber,\n amberDark,\n cyan,\n cyanDark,\n grass,\n grassDark,\n gray,\n grayDark,\n iris,\n irisDark,\n plum,\n plumDark,\n tomato,\n tomatoDark,\n} from \"@radix-ui/colors\";\n\n/** Hue families exposed to the rest of the codebase. */\nexport const HUES = [\"tomato\", \"amber\", \"grass\", \"cyan\", \"iris\", \"plum\", \"gray\"] as const;\nexport type Hue = (typeof HUES)[number];\n\n/**\n * Per-hue paired tones for shape fills + strokes. Fills use the\n * \"subtle\" step-4 (pastel on light, deep-tinted on dark). Strokes /\n * solids use step-9, which is layout-consistent across themes.\n */\nexport interface HueTones {\n /** Subtle fill \u2014 step-4 (pastel on light, deep-tinted on dark). */\n readonly fill: string;\n /** Solid stroke / mark \u2014 step-9 (same hex in both themes). */\n readonly solid: string;\n /** Hovered solid \u2014 step-10. */\n readonly solidHover: string;\n /** Low-contrast text on subtle fill \u2014 step-11. */\n readonly textLow: string;\n /** High-contrast text \u2014 step-12. */\n readonly textHigh: string;\n}\n\n/** Read a required scale step, failing loudly if the token is absent. */\nconst step = (s: Record<string, string>, key: string): string => {\n const v = s[key];\n if (v === undefined) throw new Error(`Missing color token: ${key}`);\n return v;\n};\n\nconst hueLight = (s: Record<string, string>, name: Hue): HueTones => ({\n fill: step(s, `${name}4`),\n solid: step(s, `${name}9`),\n solidHover: step(s, `${name}10`),\n textLow: step(s, `${name}11`),\n textHigh: step(s, `${name}12`),\n});\n\n/** Lookup `{ hue \u2192 tones }` for a given theme. */\nexport const HUE_TONES = {\n light: {\n tomato: hueLight(tomato, \"tomato\"),\n amber: hueLight(amber, \"amber\"),\n grass: hueLight(grass, \"grass\"),\n cyan: hueLight(cyan, \"cyan\"),\n iris: hueLight(iris, \"iris\"),\n plum: hueLight(plum, \"plum\"),\n gray: hueLight(gray, \"gray\"),\n },\n dark: {\n tomato: hueLight(tomatoDark, \"tomato\"),\n amber: hueLight(amberDark, \"amber\"),\n grass: hueLight(grassDark, \"grass\"),\n cyan: hueLight(cyanDark, \"cyan\"),\n iris: hueLight(irisDark, \"iris\"),\n plum: hueLight(plumDark, \"plum\"),\n gray: hueLight(grayDark, \"gray\"),\n },\n} as const satisfies Record<\"light\" | \"dark\", Record<Hue, HueTones>>;\n\n/**\n * Per-hue step-2 backgrounds \u2014 the \"almost-pure tint\" row used by\n * the canvas palette picker. Step-2 is the \"subtle app background\" \u2014\n * paper-like in light mode, deep-near-black in dark.\n * Exposed separately from `HUE_TONES` because shape fills (step-4)\n * and canvas backgrounds (step-2) have different aesthetic\n * intents \u2014 same hue, very different role.\n */\nexport const CANVAS_TONES = {\n light: {\n tomato: tomato.tomato2,\n amber: amber.amber2,\n grass: grass.grass2,\n cyan: cyan.cyan2,\n iris: iris.iris2,\n plum: plum.plum2,\n gray: gray.gray2,\n },\n dark: {\n tomato: tomatoDark.tomato2,\n amber: amberDark.amber2,\n grass: grassDark.grass2,\n cyan: cyanDark.cyan2,\n iris: irisDark.iris2,\n plum: plumDark.plum2,\n gray: grayDark.gray2,\n },\n} as const satisfies Record<\"light\" | \"dark\", Record<Hue, string>>;\n\n// ---------------------------------------------------------------------------\n// UI surface tokens for chrome (toolbar, panels, modals, tooltips).\n// Intentionally curated, not a full scale: every UI surface picks\n// from a small fixed set so themes stay coherent.\n// ---------------------------------------------------------------------------\n\nexport interface UISurface {\n /** Canvas / page background. */\n readonly canvas: string;\n /** Floating UI background (top bar, panels, popovers) \u2014 opaque. */\n readonly bg: string;\n /** Same as `bg`; kept for hosts that distinguished the two. */\n readonly bgSolid: string;\n /** Subtle border around floating chrome. */\n readonly border: string;\n /** Body text on the bg. */\n readonly text: string;\n /** Secondary / placeholder text. */\n readonly textMuted: string;\n /** Hover tint inside button-groups / flat buttons. */\n readonly hoverOverlay: string;\n}\n\nexport interface UIAccent {\n /** Primary accent \u2014 focus rings, links. */\n readonly accent: string;\n /** Hovered accent. */\n readonly accentHover: string;\n /** Selected / active background (tonal, not saturated). */\n readonly selectedBg: string;\n /** Foreground colour on top of `selectedBg`. */\n readonly selectedFg: string;\n /** Danger / destructive (delete, leave). */\n readonly danger: string;\n}\n\nexport const UI_SURFACE = {\n light: {\n canvas: \"#f5f5f5\",\n bg: \"#ffffff\",\n bgSolid: \"#ffffff\",\n border: \"rgba(0, 0, 0, 0.08)\",\n text: \"#1a1a1a\",\n textMuted: \"#6b6b6b\",\n hoverOverlay: \"rgba(0, 0, 0, 0.05)\",\n },\n dark: {\n // The canvas is deliberately NOT themed: user content (raw hex colors in\n // the scene) is authored against light paper, so dark mode darkens the\n // chrome only. Keep in sync with the light value above.\n canvas: \"#f5f5f5\",\n bg: \"#252525\",\n bgSolid: \"#252525\",\n border: \"rgba(255, 255, 255, 0.08)\",\n text: \"#e8e8e8\",\n textMuted: \"#9a9a9a\",\n hoverOverlay: \"rgba(255, 255, 255, 0.06)\",\n },\n} as const satisfies Record<\"light\" | \"dark\", UISurface>;\n\nexport const UI_ACCENT = {\n light: {\n accent: iris.iris9,\n accentHover: iris.iris10,\n selectedBg: iris.iris4,\n selectedFg: iris.iris11,\n danger: tomato.tomato9,\n },\n dark: {\n accent: irisDark.iris9,\n accentHover: irisDark.iris10,\n selectedBg: irisDark.iris4,\n selectedFg: irisDark.iris11,\n danger: tomatoDark.tomato9,\n },\n} as const satisfies Record<\"light\" | \"dark\", UIAccent>;\n\n// ---------------------------------------------------------------------------\n// Renderer tokens \u2014 colours baked into renderer output (grid,\n// default shape styles for newly-created shapes). Theme-agnostic\n// because the renderer doesn't have a theme context; the canvas\n// content is the user's document, not the chrome around it. Picked\n// to read on both light and dark canvases.\n// ---------------------------------------------------------------------------\n\n/**\n * Grid colour \u2014 neutral gray. Step-6 reads as a calm grid line\n * on a paper-white canvas and stays visible on a near-black one.\n * Single hex for both themes.\n */\nexport const GRID_COLOR = gray.gray6;\n\n/**\n * Dot-grid colour \u2014 deliberately darker than {@link GRID_COLOR}.\n * A lone dot covers far less area than a ruled line, so at the\n * line colour (step-6) the dots read as a faint, low-contrast\n * haze on a gray canvas. Step-9 (\"solid\") gives each dot enough\n * weight to be a legible anchor without turning the field busy.\n */\nexport const GRID_DOT_COLOR = gray.gray9;\n\n/**\n * Default shape styles applied when a user draws a new shape\n * with the toolbar. The user can override anything via the\n * property panel afterwards.\n *\n * Fills use light-theme step-3 (very subtle pastel) so they read\n * cleanly on a paper-white canvas; strokes use step-9 (solid\n * brand colour). Sticky note uses amber for that classic\n * yellow paper feel.\n */\nexport interface DefaultElementStyle {\n readonly fill: string;\n readonly stroke: string;\n readonly strokeWidth: number;\n}\n\nexport const DEFAULT_ELEMENT_STYLES = {\n rectangle: {\n fill: iris.iris3,\n stroke: iris.iris9,\n strokeWidth: 2,\n },\n ellipse: {\n fill: tomato.tomato3,\n stroke: tomato.tomato9,\n strokeWidth: 2,\n },\n flowchart: {\n fill: grass.grass3,\n stroke: grass.grass9,\n strokeWidth: 2,\n },\n sticky: {\n fill: amber.amber3,\n stroke: amber.amber9,\n strokeWidth: 1,\n },\n} as const satisfies Record<string, DefaultElementStyle>;\n\n/**\n * Default style for a freshly-created edge \u2014 neutral dark gray\n * line so it reads on most canvas backgrounds without competing\n * with the connected shapes' brand colours. step-12 of `gray`\n * gives ink-like contrast on paper-white.\n */\nexport const DEFAULT_EDGE_STYLE = {\n stroke: gray.gray12,\n strokeWidth: 1.5,\n} as const;\n\n/**\n * Semantic colours for the scene-diff overlay (`<DiffPanel>`):\n * `added` (green), `removed` (red), `modified` (amber). Picked\n * from step-9 of grass / tomato / amber so the three markers\n * stay legible side by side on a paper-white background.\n */\nexport const DIFF_COLORS = {\n added: grass.grass9,\n removed: tomato.tomato9,\n modified: amber.amber9,\n} as const satisfies Record<\"added\" | \"removed\" | \"modified\", string>;\n", "/**\n * Tunable constants for the renderer core. All \"magic numbers\" used by\n * `renderScene` / `renderLinks` / `renderGrid` live here so there is one\n * place to tweak performance / visual behaviour.\n */\n\nimport { GRID_COLOR, GRID_DOT_COLOR, UI_SURFACE } from \"@oh-just-another/tokens\";\nimport type { LodOptions } from \"./rendering/scene-renderer.js\";\n\n/**\n * Level-of-detail floors, in ON-SCREEN pixels \u2014 decided per element from\n * what actually lands on screen, so the zoom level alone never degrades a\n * shape that is still large or a heading that is still readable.\n *\n * - `LOD_PLACEHOLDER_MAX_SCREEN_PX` \u2014 a shape whose longer side is below\n * this on screen is a flat AABB fill (no detail is visible at that size\n * anyway; saves ~10\u00D7 renderer cost per shape). Range: 4\u201316.\n * - `LOD_MIN_TEXT_SCREEN_PX` \u2014 text whose font size on screen is below this\n * is skipped (glyphs are unreadable below ~6 px; skipping the\n * wrap + measure is the bulk of text cost). Range: 4\u20138.\n *\n * Hosts override per-render by passing `RenderSceneOptions.lod`.\n */\nexport const LOD_PLACEHOLDER_MAX_SCREEN_PX = 8;\nexport const LOD_MIN_TEXT_SCREEN_PX = 6;\nexport const DEFAULT_LOD: LodOptions = {\n placeholderMaxScreenPx: LOD_PLACEHOLDER_MAX_SCREEN_PX,\n minTextScreenPx: LOD_MIN_TEXT_SCREEN_PX,\n};\n\n/**\n * Neutral grey for the empty-text placeholder prompt (`TEXT_PLACEHOLDERS`\n * in `@oh-just-another/scene`) \u2014 the muted text tone of the light UI (the\n * canvas is always light).\n */\nexport const TEXT_PLACEHOLDER_COLOR = UI_SURFACE.light.textMuted;\n\n/**\n * Grey colour used for placeholder fills when LOD switches to the\n * cheapest path. A mid-tone neutral that blends with most scene\n * palettes; override via `RenderSceneOptions.placeholderFill`.\n */\nexport const DEFAULT_PLACEHOLDER_FILL = \"#bbb\";\n\n/**\n * Viewport-rect inflation factor applied by hosts when computing the\n * world-space culling rect. 0.05 = 5% padding on each side \u2014 enough\n * to avoid flicker during a one-frame pan without keeping much\n * off-screen geometry alive in the renderer.\n */\nexport const VIEWPORT_CULL_PADDING_RATIO = 0.05;\n\n/**\n * Text-decoration geometry (underline / strikethrough), as fractions of\n * font size, measured from the line's top (the renderer draws text with\n * a top baseline).\n *\n * - `TEXT_DECORATION_THICKNESS` \u2014 line thickness \u2248 6% of font size\n * (clamped to \u22651 px in the renderer).\n * - `TEXT_UNDERLINE_OFFSET` \u2014 underline top, ~92% down (just below the\n * glyph baseline).\n * - `TEXT_STRIKETHROUGH_OFFSET` \u2014 strikethrough centre, ~50% (x-height).\n */\nexport const TEXT_DECORATION_THICKNESS = 0.06;\n\n/**\n * List layout metrics, in em (\u00D7 font size):\n * - `LIST_INDENT_EM` \u2014 horizontal shift per nesting level; list paragraphs\n * get one extra level for the marker slot. Reasonable range 1.2\u20131.8.\n * - `LIST_MARKER_GAP_EM` \u2014 gap between the marker's right edge and the\n * item text. Reasonable range 0.3\u20130.6.\n */\nexport const LIST_INDENT_EM = 1.4;\nexport const LIST_MARKER_GAP_EM = 0.4;\n\n/**\n * Inset between a shape's bounds and its embedded label text, in em\n * (\u00D7 label font size). Reasonable range 0.3\u20131.0.\n */\nexport const LABEL_PADDING_EM = 0.5;\n\n/**\n * Auto-fit font-size bounds (world px) for `ShapeLabel.autoFit` \u2014 the\n * binary search picks the largest size in this range whose layout fits\n * the shape body. Reasonable ranges: min 8\u201314, max 48\u201396.\n */\nexport const LABEL_AUTOFIT_MIN_PX = 10;\nexport const LABEL_AUTOFIT_MAX_PX = 64;\n\n/**\n * Sticky-note chrome:\n * - `STICKY_DEFAULT_FILL` \u2014 card colour when `style.fill` is omitted.\n * - `STICKY_CORNER_RADIUS` \u2014 corner rounding in world units.\n * - `STICKY_AUTHOR_FONT_SIZE` \u2014 author-name strip font size.\n * - `STICKY_AUTHOR_COLOR` \u2014 author-name text colour.\n */\nexport const STICKY_DEFAULT_FILL = \"#fff9b1\";\nexport const STICKY_CORNER_RADIUS = 4;\nexport const STICKY_AUTHOR_FONT_SIZE = 10;\nexport const STICKY_AUTHOR_COLOR = \"#8a8a6f\";\n\n/**\n * Sticky skeuomorphism (paper look):\n * - `STICKY_SHADOW_COLOR` / `STICKY_SHADOW_OFFSET_Y` \u2014 soft drop shadow\n * under the card (offset in world units, 2\u20136 reasonable).\n * - `STICKY_TAG_*` \u2014 tag pill metrics along the bottom edge.\n */\nexport const STICKY_SHADOW_COLOR = \"rgba(0, 0, 0, 0.18)\";\nexport const STICKY_SHADOW_OFFSET_Y = 4;\nexport const STICKY_TAG_FONT_SIZE = 9;\nexport const STICKY_TAG_PAD_X = 5;\nexport const STICKY_TAG_HEIGHT = 14;\nexport const STICKY_TAG_GAP = 4;\nexport const STICKY_TAG_BG = \"rgba(0, 0, 0, 0.08)\";\nexport const STICKY_TAG_COLOR = \"#555\";\n\n/**\n * Sticky reaction pills (bottom-left row, drawn by the renderer so they\n * reach PNG / SVG exports; the DOM layer only provides click zones).\n */\nexport const STICKY_REACTION_FONT_SIZE = 10;\nexport const STICKY_REACTION_HEIGHT = 16;\nexport const STICKY_REACTION_PAD_X = 6;\nexport const STICKY_REACTION_GAP = 4;\nexport const STICKY_REACTION_BG = \"rgba(255, 255, 255, 0.85)\";\nexport const STICKY_REACTION_COLOR = \"#333\";\n/** Accent for the canvas-drawn \"+\" add-reaction button (iris 9). */\nexport const STICKY_REACTION_ADD_COLOR = \"#5b5bd6\";\n/**\n * Reaction pills keep a CONSTANT on-screen size: their world size is\n * `base / zoom`. Once the sticky's shorter side is narrower than this\n * many screen pixels the reaction chrome (pills AND the \"+\" button) is\n * HIDDEN entirely \u2014 constant-size pills would swallow a small card. A\n * screen-size gate (like the text / placeholder LOD), so a large note keeps\n * its reactions at a zoom where a small one already hides them. Also bounds\n * the worst-case pill world size for render-overflow estimates. Range\n * 40\u2013160 (80 = the medium 160 px preset at 50 % zoom).\n */\nexport const STICKY_REACTION_MIN_SCREEN_PX = 80;\n\n/**\n * What static exports (PNG / SVG) include by default. The export UI can\n * override per run; interactive rendering ignores these and draws\n * everything.\n */\nexport const EXPORT_CONTENT_DEFAULTS = {\n stickyReactions: true,\n stickyTags: true,\n stickyAuthor: true,\n // UI chrome, not content \u2014 never wanted in a static image.\n stickyAddButton: false,\n} as const;\nexport const TEXT_UNDERLINE_OFFSET = 0.92;\nexport const TEXT_STRIKETHROUGH_OFFSET = 0.5;\n\n/**\n * Corner radius (world px) for the rounded bends of an elbow (orthogonal)\n * connector and of a straight connector broken by user waypoints. Each\n * corner is replaced by a quadratic arc of this radius, clamped to half the\n * shorter adjacent segment so short segments don't overshoot. 0 disables\n * rounding (sharp corners). Range: 0\u201316.\n */\nexport const LINK_CORNER_RADIUS = 10;\n\n// --- Grid -------------------------------------------------------------------\n//\n// Lines and dots are tuned independently: a ruled line covers far more\n// pixels than a lone dot, so the dot grid needs a darker colour, a\n// slightly fatter mark, and a denser ladder to read as clearly as the\n// line grid at the same zoom.\n\n/** Stroke colour for the ruled (`\"lines\"`) grid. Neutral step-6 gray. */\nexport const GRID_LINE_COLOR = GRID_COLOR;\n\n/** Fill colour for the dotted (`\"dots\"`) grid \u2014 darker step-9 gray so the dots stay legible on a gray canvas. */\nexport const GRID_DOT_FILL = GRID_DOT_COLOR;\n\n/** On-screen stroke width (px) of a grid line. Divided by zoom at the use site so the line stays 1 px regardless of view scale. */\nexport const GRID_LINE_WIDTH_PX = 1.0;\n\n/**\n * Dot radius (screen px) for `gridStyle === \"dots\"`. Constant across\n * zoom (divided by `zoom` at the use site). Reads as a crisp anchor on\n * a gray surface. Range: 1.0\u20132.0.\n */\nexport const GRID_DOT_RADIUS_PX = 1;\n\n/**\n * Below this on-screen spacing (px) a grid level paints nothing \u2014\n * denser rendering reads as a flat haze. Only used by the fixed-ladder\n * path (`options.levels`); the default dynamic ladder uses the fade\n * bands below instead.\n */\nexport const GRID_MIN_SCREEN_SPACING_PX = 4;\n\n// --- Dynamic (infinite) grid ladder -----------------------------------------\n//\n// The default grid is a SELF-SIMILAR, zoom-relative ladder: instead of a\n// fixed set of world steps it renders a handful of rungs anchored to the\n// current zoom, each rung `GRID_LEVEL_SUBDIV`\u00D7 the previous. As you zoom\n// the rungs slide \u2014 a finer rung fades in and a coarser one fades out \u2014\n// so new lines / dots keep appearing at EVERY zoom, not just at the\n// hand-picked thresholds of a fixed ladder. Rungs finer than `gridSize`\n// are purely visual (snap-to-grid still rounds to `gridSize`).\n\n/** Ratio between adjacent rungs. 4 keeps the 64/16/4/1 cadence. */\nexport const GRID_LEVEL_SUBDIV = 4;\n\n/**\n * How many self-similar rungs to paint at once (finest first). 3 keeps a\n * stable fully-opaque coarse tier while the finest rung fades in/out.\n */\nexport const GRID_LEVEL_RUNGS = 3;\n\n/**\n * Line grid fade band (on-screen px). A rung is invisible at/below\n * `FROM`, ramps to full opacity by `FULL`, and stays full above. Tuned\n * so at 100 % (gridSize 20) the 20 px rung reads faint and the 80 px rung\n * is solid, while subdividing forever.\n */\nexport const GRID_LINE_FADE_FROM_PX = 12;\nexport const GRID_LINE_FADE_FULL_PX = 56;\n\n/**\n * Dot grid fade band. Lower / tighter than lines so the base `gridSize`\n * dot lattice is fully solid at 100 % (the denser dot field) yet still\n * subdivides on zoom-in.\n */\nexport const GRID_DOT_FADE_FROM_PX = 10;\nexport const GRID_DOT_FADE_FULL_PX = 20;\n\n// --- Block-arrow shape (BlockArrowElement) ----------------------------------\n\n/**\n * Fraction of the shape's length given to the arrow head when\n * `BlockArrowElement.headRatio` is omitted. 0.4 = head spans the last 40 %,\n * body the first 60 %. Clamped to `ARROWHEAD_RATIO_MIN`..`ARROWHEAD_RATIO_MAX`.\n */\nexport const ARROWHEAD_HEAD_RATIO = 0.4;\n\n/**\n * Fraction of the shape's cross-axis filled by the body when\n * `BlockArrowElement.bodyThickness` is omitted. 0.5 = body half as thick as\n * the box. Clamped to `ARROWHEAD_RATIO_MIN`..`ARROWHEAD_RATIO_MAX`.\n */\nexport const ARROWHEAD_BODY_THICKNESS = 0.5;\n\n/**\n * Lower clamp for the block-arrow head/body ratios so a degenerate input can't\n * collapse the head or body to nothing. Range: 0\u2013`ARROWHEAD_RATIO_MAX`.\n */\nexport const ARROWHEAD_RATIO_MIN = 0.1;\n\n/**\n * Upper clamp for the block-arrow head/body ratios so the head/body can't eat\n * the whole box. Range: `ARROWHEAD_RATIO_MIN`\u20131.\n */\nexport const ARROWHEAD_RATIO_MAX = 0.9;\n\n// --- Frame chrome colours ---------------------------------------------------\n\n/** Outline colour of a frame when no explicit style overrides it. Neutral gray. */\nexport const FRAME_STROKE_COLOR = \"#888\";\n\n/** Default body fill of a frame when `style.fill` is omitted. White. */\nexport const FRAME_FILL_COLOR = \"#ffffff\";\n\n/** Background fill of the frame's header strip. Near-black. */\nexport const FRAME_HEADER_BG_COLOR = \"#222\";\n\n/** Text colour of the frame's header label. Light gray for contrast on the dark strip. */\nexport const FRAME_HEADER_TEXT_COLOR = \"#ddd\";\n\n// --- Edge / link rendering defaults -----------------------------------------\n\n/**\n * Length (world px) of a block-arrow edge's head triangle when\n * `Link.blockArrow.headLength` is omitted. The body terminates this far before\n * the endpoint so the head fills the gap. Range: ~8\u201340.\n */\nexport const BLOCK_ARROW_HEAD_LENGTH = 18;\n\n/**\n * Body thickness (world px) of a block-arrow edge when\n * `Link.blockArrow.bodyThickness` is omitted. Offset half this on each side of\n * the routed path. Range: ~4\u201332.\n */\nexport const BLOCK_ARROW_BODY_THICKNESS = 12;\n\n/** Fallback fill for a block-arrow edge when neither `style.fill` nor `style.stroke` is set. Mid gray. */\nexport const BLOCK_ARROW_FILL_COLOR = \"#444\";\n\n/** Fallback stroke for a block-arrow edge when `style.stroke` is omitted. Near-black. */\nexport const BLOCK_ARROW_STROKE_COLOR = \"#222\";\n\n/**\n * Arrowhead size (world px) when `LinkArrowheads.size` is omitted. Drives the\n * wing/length scale of every arrowhead style. Range: ~6\u201324.\n */\nexport const ARROWHEAD_SIZE = 10;\n\n/** Fallback stroke colour for an edge / its arrowheads when `style.stroke` is omitted. Black. */\nexport const EDGE_STROKE_COLOR = \"#000\";\n\n/** Fallback text colour of a link label when `LinkLabel.fill` is omitted. Near-black. */\nexport const LABEL_FILL_COLOR = \"#222\";\n\n/** Fallback pill-background colour of a link label when `LinkLabel.background` is omitted. White. */\nexport const LABEL_BG_COLOR = \"#fff\";\n\n/**\n * Corner radius of the label pill behind a link caption (world px at zoom 1).\n * 0 = square. Range: 0\u20138 (clamped visually by the pill height).\n */\nexport const LINK_LABEL_RADIUS = 4;\n", "import type { TextAlign } from \"../targets/render-target.js\";\nimport type { TextParagraph } from \"@oh-just-another/scene\";\nimport { req, type Vec2 } from \"@oh-just-another/types\";\nimport { LIST_INDENT_EM } from \"../constants.js\";\n\n/**\n * Caret-aware text layout. Unlike {@link wrapText} (which collapses\n * whitespace and is only good enough for *drawing*), this keeps every\n * line as an exact substring of the source plus its `[start, end)`\n * character offsets \u2014 so a caret index maps unambiguously to a line +\n * column. Measurement is injected as a `measure(s) => width` callback\n * so the same geometry can be computed against either backend's font\n * metrics (Canvas2D `measureText` or WebGL2 MSDF advances).\n *\n * Convention for `\\n`: a hard newline at source index `k` ends the\n * current line at `end === k` (caret at `k` = end of line) and the next\n * line starts at `start === k + 1` (caret at `k + 1` = start of next\n * line). The `\\n` itself never holds a caret.\n */\nexport interface LaidOutLine {\n /** Exact source substring for this visual line (no whitespace collapsing). */\n readonly text: string;\n /** Source offset where the line begins (inclusive). */\n readonly start: number;\n /** Source offset where the line ends (exclusive; excludes a trailing `\\n`). */\n readonly end: number;\n /** Measured width of `text` in CSS px. */\n readonly width: number;\n /** List indent offset in CSS px (0 for plain paragraphs). */\n readonly indentX: number;\n /** Index of the source paragraph this line belongs to. */\n readonly para: number;\n /** True on the paragraph's first visual line (where the marker draws). */\n readonly paraFirst: boolean;\n}\n\nexport interface EditableTextLayout {\n readonly lines: readonly LaidOutLine[];\n readonly lineHeight: number;\n /** Width the lines are aligned within (maxWidth, or the widest line). */\n readonly blockWidth: number;\n}\n\nexport type MeasureText = (text: string) => number;\n\nexport interface LayoutTextOptions {\n readonly fontSize: number;\n /** Wrap budget in CSS px. `undefined` \u2192 no wrap (split on `\\n` only). */\n readonly maxWidth?: number;\n /** Line-height multiplier. Default 1.2 (matches the text renderer). */\n readonly lineHeightFactor?: number;\n /**\n * Per-paragraph list attributes (aligned by paragraph index). List\n * paragraphs are indented by `(indent + 1) \u00D7 LIST_INDENT_EM \u00D7 fontSize`\n * and their wrap budget shrinks accordingly; plain paragraphs with a\n * bare `indent` shift without the marker slot.\n */\n readonly paragraphs?: readonly TextParagraph[];\n}\n\n/** Default multiplier from font size to line height (matches `drawText`). */\nexport const DEFAULT_LINE_HEIGHT_FACTOR = 1.2;\n\nconst wrapParagraph = (\n para: string,\n base: number,\n maxWidth: number,\n measure: MeasureText,\n out: LaidOutLine[],\n paraIndex: number,\n indentX: number,\n): void => {\n const lineBase = { indentX, para: paraIndex };\n const first = (): boolean => out.length === 0 || req(out[out.length - 1]).para !== paraIndex;\n if (para === \"\") {\n out.push({ text: \"\", start: base, end: base, width: 0, ...lineBase, paraFirst: first() });\n return;\n }\n // Word spans (non-whitespace runs) with offsets relative to `para`.\n const words: { s: number; e: number }[] = [];\n const re = /\\S+/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(para)) !== null) words.push({ s: m.index, e: m.index + m[0].length });\n if (words.length === 0) {\n // Whitespace-only paragraph \u2014 keep it as one line so offsets survive.\n out.push({\n text: para,\n start: base,\n end: base + para.length,\n width: measure(para),\n ...lineBase,\n paraFirst: first(),\n });\n return;\n }\n\n // `white-space: pre-wrap` + `overflow-wrap: break-word` (standard):\n // preserve whitespace, wrap at word boundaries, and break a word that\n // is itself wider than the line so narrowing the block always reflows\n // (a single long word can't overflow forever). Lines are gapless\n // slices of the source \u2014 inter-word whitespace at a soft break stays\n // on the preceding line \u2014 so every character maps to exactly one line\n // (clean caret offsets). First line starts at 0 (keeps leading\n // whitespace); the last extends to the paragraph end (trailing ws).\n let lineStart = 0;\n const push = (start: number, end: number): void => {\n const text = para.slice(start, end);\n out.push({\n text,\n start: base + start,\n end: base + end,\n width: measure(text),\n ...lineBase,\n paraFirst: first(),\n });\n };\n let i = 0;\n while (i < words.length) {\n const w = req(words[i]);\n if (measure(para.slice(lineStart, w.e)) <= maxWidth) {\n i++; // word fits on the current line \u2014 keep it, try the next\n continue;\n }\n if (w.s > lineStart) {\n // Content precedes this word on the line \u2192 break before it. The\n // whitespace up to its start stays on the current line (pre-wrap).\n push(lineStart, w.s);\n lineStart = w.s;\n continue; // retry this word on the fresh line\n }\n // The word starts the line and alone overflows \u2192 break it by chars,\n // keeping at least one char per line so we always make progress.\n let e = w.s + 1;\n while (e < w.e && measure(para.slice(lineStart, e + 1)) <= maxWidth) e++;\n if (e >= w.e) {\n // Whole (remaining) word consumed \u2014 leave it on the current line\n // and advance. Guards termination when the measurer is degenerate\n // (e.g. a constant stub that never reports \"fits\").\n i++;\n continue;\n }\n push(lineStart, e);\n lineStart = e;\n w.s = e; // remainder of the word continues on the next line\n }\n // Last line keeps everything through the end of the paragraph.\n push(lineStart, para.length);\n};\n\n/**\n * Lay out `text` into visual lines with exact source offsets. Always\n * returns at least one (possibly empty) line.\n */\nexport const layoutText = (\n text: string,\n measure: MeasureText,\n options: LayoutTextOptions,\n): EditableTextLayout => {\n const lineHeight = options.fontSize * (options.lineHeightFactor ?? DEFAULT_LINE_HEIGHT_FACTOR);\n const lines: LaidOutLine[] = [];\n let paraStart = 0;\n let paraIndex = 0;\n for (let i = 0; i <= text.length; i++) {\n if (i === text.length || text[i] === \"\\n\") {\n const para = text.slice(paraStart, i);\n const attrs = options.paragraphs?.[paraIndex];\n const levels = (attrs?.indent ?? 0) + (attrs?.list !== undefined ? 1 : 0);\n const indentX = levels * LIST_INDENT_EM * options.fontSize;\n if (options.maxWidth === undefined) {\n lines.push({\n text: para,\n start: paraStart,\n end: i,\n width: measure(para),\n indentX,\n para: paraIndex,\n paraFirst: true,\n });\n } else {\n // Keep at least one em of budget so a huge indent can't wedge the\n // wrapper into zero-width lines.\n const budget = Math.max(options.fontSize, options.maxWidth - indentX);\n wrapParagraph(para, paraStart, budget, measure, lines, paraIndex, indentX);\n }\n paraStart = i + 1;\n paraIndex++;\n }\n }\n if (lines.length === 0) {\n lines.push({ text: \"\", start: 0, end: 0, width: 0, indentX: 0, para: 0, paraFirst: true });\n }\n let widest = 0;\n for (const l of lines) widest = Math.max(widest, l.width + l.indentX);\n const blockWidth = options.maxWidth ?? widest;\n return { lines, lineHeight, blockWidth };\n};\n\n/** Left edge (local x) where a line's glyphs start, given the align. */\nconst lineLeftX = (lineWidth: number, blockWidth: number, align: TextAlign): number => {\n if (align === \"center\") return blockWidth / 2 - lineWidth / 2;\n if (align === \"right\") return blockWidth - lineWidth;\n return 0;\n};\n\n/**\n * Left edge of a laid-out line INCLUDING its list indent: alignment is\n * computed within the space remaining after the indent, then shifted by\n * it. The single source of glyph-left-x for the renderer, caret,\n * click-to-caret and selection rects \u2014 keep them in lockstep.\n */\nexport const lineLeft = (line: LaidOutLine, blockWidth: number, align: TextAlign): number =>\n lineLeftX(line.width, blockWidth - line.indentX, align) + line.indentX;\n\n/** Index of the line a caret offset falls on (handles boundaries). */\nconst lineIndexForCaret = (layout: EditableTextLayout, caret: number): number => {\n const { lines } = layout;\n for (let i = 0; i < lines.length; i++) {\n const l = req(lines[i]);\n // Caret belongs to this line when it's within [start, end]; the\n // upper bound is inclusive so end-of-line resolves here, while\n // start-of-next-line (end + 1 for a hard `\\n`) resolves to the\n // next line on the following iteration.\n if (caret <= l.end) return i;\n }\n return lines.length - 1;\n};\n\nexport interface CaretGeometry {\n /** Local x of the caret bar. */\n readonly x: number;\n /** Local y of the caret top (baseline-top line origin). */\n readonly y: number;\n /** Caret height (\u2248 font size). */\n readonly height: number;\n /** Index of the line the caret sits on. */\n readonly line: number;\n}\n\n/**\n * Local-space geometry of the caret for a given source `caret` offset.\n * `align` must match the renderer's `textAlign`.\n */\nexport const caretGeometry = (\n layout: EditableTextLayout,\n caret: number,\n measure: MeasureText,\n fontSize: number,\n align: TextAlign,\n): CaretGeometry => {\n const i = lineIndexForCaret(layout, caret);\n const line = req(layout.lines[i]);\n const col = Math.max(0, Math.min(caret, line.end) - line.start);\n const prefixWidth = col === 0 ? 0 : measure(line.text.slice(0, col));\n const left = lineLeft(line, layout.blockWidth, align);\n return { x: left + prefixWidth, y: i * layout.lineHeight, height: fontSize, line: i };\n};\n\n/**\n * Map a local-space point to the nearest source caret offset. Used for\n * click-to-place-caret and drag-to-select.\n */\nexport const pointToCaretIndex = (\n layout: EditableTextLayout,\n point: Vec2,\n measure: MeasureText,\n align: TextAlign,\n): number => {\n const { lines, lineHeight } = layout;\n const i = Math.max(0, Math.min(lines.length - 1, Math.floor(point.y / lineHeight)));\n const line = req(lines[i]);\n const left = lineLeft(line, layout.blockWidth, align);\n // Walk columns, picking the boundary whose x is closest to point.x.\n let best = 0;\n let bestDist = Math.abs(left - point.x);\n for (let col = 1; col <= line.text.length; col++) {\n const x = left + measure(line.text.slice(0, col));\n const d = Math.abs(x - point.x);\n if (d < bestDist) {\n bestDist = d;\n best = col;\n }\n }\n return line.start + best;\n};\n\nexport interface SelectionRect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Local-space highlight rectangles covering the source range `[from, to)`\n * (order-independent), one per visual line it spans.\n */\nexport const selectionRects = (\n layout: EditableTextLayout,\n from: number,\n to: number,\n measure: MeasureText,\n align: TextAlign,\n): readonly SelectionRect[] => {\n const lo = Math.min(from, to);\n const hi = Math.max(from, to);\n if (lo === hi) return [];\n const rects: SelectionRect[] = [];\n for (let i = 0; i < layout.lines.length; i++) {\n const line = req(layout.lines[i]);\n const a = Math.max(lo, line.start);\n const b = Math.min(hi, line.end);\n if (a > b) continue;\n if (a === b && !(lo <= line.start && hi > line.end)) {\n // Empty intersection on this line, unless the selection spans the\n // hard break past it (then show a thin trailing marker).\n if (!(hi > line.end && lo <= line.end)) continue;\n }\n const left = lineLeft(line, layout.blockWidth, align);\n const xa = left + (a === line.start ? 0 : measure(line.text.slice(0, a - line.start)));\n const xb = left + (b === line.start ? 0 : measure(line.text.slice(0, b - line.start)));\n // A line whose break is inside the selection gets a small trailing\n // sliver so multi-line selections read continuously.\n const trailing = hi > line.end ? layout.lineHeight * 0.25 : 0;\n rects.push({\n x: xa,\n y: i * layout.lineHeight,\n width: Math.max(0, xb - xa) + trailing,\n height: layout.lineHeight,\n });\n }\n return rects;\n};\n", "/**\n * Animated-content adapter registry. The kernel doesn't decode GIF /\n * Lottie / video itself: it exposes an `AnimatedSourceAdapter` interface\n * and a process-global registry indexed by `kind` (\"gif\", \"lottie\",\n * \"video\", \"<your-format>\"). Hosts plug their decoder of choice:\n *\n * registerAnimationAdapter({\n * kind: \"gif\",\n * getFrameAt(data, timestampMs) { ... return ImageBitmap }\n * });\n *\n * An `ImageElement` carrying `animationKind` + `animationData` resolves\n * its source through the registry; without those fields the static `src`\n * is used. The kernel only answers the stateless \"what should this frame\n * look like?\" question \u2014 live playback ticking is the host's job.\n */\n\nexport interface AnimatedSourceAdapter<Data = unknown> {\n readonly kind: string;\n /**\n * Return the image source the renderer should draw at\n * `timestampMs` (typically `performance.now()`). The returned\n * value is opaque \u2014 it gets passed straight to `target.drawImage\n * (image, ...)`. Backends accept different types: Canvas2D wants\n * a `CanvasImageSource`, headless SVG wants a string URL. The\n * adapter's `kind` is paired with the renderer the host actually\n * uses, so there's no ambiguity in practice.\n *\n * Implementations are stateless w.r.t. the registry; they may\n * cache decoded frames internally (the `data` payload is the\n * natural cache key).\n */\n getFrameAt(data: Data, timestampMs: number): unknown;\n /**\n * Optional \u2014 total animation duration in ms. The animation tick\n * uses this to schedule the next frame; an unset value means\n * \"keep ticking forever\" (endless lottie loops or streamed video).\n */\n totalDurationMs?(data: Data): number;\n}\n\nconst registry = new Map<string, AnimatedSourceAdapter>();\n\nexport const registerAnimationAdapter = <D>(adapter: AnimatedSourceAdapter<D>): void => {\n registry.set(adapter.kind, adapter);\n};\n\nexport const unregisterAnimationAdapter = (kind: string): void => {\n registry.delete(kind);\n};\n\nexport const getAnimationAdapter = (kind: string): AnimatedSourceAdapter | undefined =>\n registry.get(kind);\n\nexport const listAnimationKinds = (): readonly string[] => [...registry.keys()];\n\n/**\n * Content-ready notification. Adapters decode lazily and often\n * asynchronously (e.g. the GIF adapter's `createImageBitmap`): the\n * first `getFrameAt` returns `null` while the decode is in flight. For\n * a *playing* shape the host's animation tick re-renders on the next\n * rAF and picks up the frames \u2014 but a **paused** shape (reduced-motion,\n * auto-stopped, frozen) has no tick, so without a nudge it would stay\n * blank forever once decoded. Adapters call\n * {@link notifyAnimationContentReady} when a decode completes; the host\n * (editor) subscribes via {@link onAnimationContentReady} and schedules\n * one more render so the now-decoded (possibly paused) frame paints.\n */\nconst contentListeners = new Set<() => void>();\n\nexport const onAnimationContentReady = (fn: () => void): (() => void) => {\n contentListeners.add(fn);\n return () => contentListeners.delete(fn);\n};\n\nexport const notifyAnimationContentReady = (): void => {\n for (const fn of contentListeners) {\n try {\n fn();\n } catch {\n /* a listener throwing must not break sibling listeners / decode */\n }\n }\n};\n\n/**\n * Pluggable playback clock. Returns the playback position (ms) the\n * animation adapter should be sampled at for a given shape \u2014 letting\n * a host pause / freeze / offset individual animated shapes without\n * the renderer knowing about playback state.\n *\n * Default: wall-clock `performance.now()` for every shape (every GIF\n * plays, in lock-step with real time). A host (the editor) overrides\n * this via {@link setAnimationClock} to consult its per-shape\n * playback map \u2014 returning a frozen value for paused shapes, an\n * offset for shapes started later, etc.\n *\n * Preferred channel: pass a per-instance clock through the render context\n * (`RenderSceneOptions.clock` \u2192 {@link ElementRenderContext.clock}). Each\n * `Editor` threads its own clock that way, so two editors on one page no\n * longer fight over a shared module global. This module-level clock remains a\n * process-global **fallback** for paths that can't thread a context \u2014 headless\n * `renderScene` (SVG / worker / PNG export) and the tile compositor \u2014 where\n * the wall-clock default (or a single host override) is sufficient.\n */\nexport type AnimationClock = (shape: { readonly id?: unknown }) => number;\n\nlet animationClock: AnimationClock = () =>\n typeof performance !== \"undefined\" ? performance.now() : 0;\n\n/**\n * Install the process-global fallback playback clock. Prefer threading a\n * per-instance clock via `RenderSceneOptions.clock`; this setter only affects\n * render paths that don't carry a render context (headless renderers, the tile\n * compositor). Idempotent \u2014 last write wins.\n */\nexport const setAnimationClock = (clock: AnimationClock): void => {\n animationClock = clock;\n};\n\n/** Restore the default wall-clock playback (used in tests / teardown). */\nexport const resetAnimationClock = (): void => {\n animationClock = () => (typeof performance !== \"undefined\" ? performance.now() : 0);\n};\n\n/**\n * Resolve an image source for an `ImageElement`. When the shape has\n * an `animationKind` and a matching adapter is registered, the\n * adapter's `getFrameAt(animationData, t)` result is returned, where\n * `t` comes from the pluggable {@link setAnimationClock} (default\n * wall-clock). Otherwise \u2014 and as a fallback when the adapter throws \u2014\n * falls back to the static `src`. The renderer hands the result to\n * `target.drawImage` without further interpretation.\n */\nexport const resolveImageSource = (\n shape: {\n readonly id?: unknown;\n readonly src: string;\n readonly animationKind?: string;\n readonly animationData?: unknown;\n },\n timestampMs: number = animationClock(shape),\n): unknown => {\n if (!shape.animationKind) return shape.src;\n const adapter = registry.get(shape.animationKind);\n if (!adapter) return shape.src;\n try {\n return adapter.getFrameAt(shape.animationData, timestampMs);\n } catch {\n return shape.src;\n }\n};\n", "/**\n * Runtime guard: is `value` an actual drawable image source that\n * `ctx.drawImage` / `gl.texImage2D` will accept?\n *\n * Needed because a deserialized scene can carry a **garbage**\n * `metadata.image`: a live `<img>` DOM element serialises to `{}`\n * via `JSON.stringify`, so a scene restored from localStorage has\n * `metadata.image === {}` \u2014 a truthy object that passes a naive\n * `typeof === \"object\"` check but throws inside `drawImage`\n * (\"provided value is not of type \u2026\") / `texImage2D` (\"overload\n * resolution failed\").\n *\n * The check is environment-safe: each constructor is probed for\n * existence first (workers / SSR / older browsers may lack some),\n * so it never throws on a missing global. A bare `{}` matches none\n * of them and is rejected.\n *\n * Single implementation for the whole repo \u2014 element renderers\n * (renderer-core), backends (renderer-canvas) and scene rehydration\n * (state) all import it from here.\n */\nconst DRAWABLE_CTOR_NAMES = [\n \"HTMLImageElement\",\n \"HTMLCanvasElement\",\n \"HTMLVideoElement\",\n \"ImageBitmap\",\n \"OffscreenCanvas\",\n \"SVGImageElement\",\n \"VideoFrame\",\n] as const;\n\nexport const isDrawableImageSource = (value: unknown): value is CanvasImageSource => {\n if (typeof value !== \"object\" || value === null) return false;\n const g = globalThis as Record<string, unknown>;\n for (const name of DRAWABLE_CTOR_NAMES) {\n const ctor = g[name];\n if (\n typeof ctor === \"function\" &&\n value instanceof (ctor as new (...args: never[]) => unknown)\n ) {\n return true;\n }\n }\n return false;\n};\n", "import { polygon as polygonMath } from \"@oh-just-another/math\";\nimport {\n getCornerRadius,\n getElementLocalBounds,\n registerRenderOverflow,\n FRAME_HEADER_HEIGHT,\n FRAME_HEADER_PADDING_X,\n FRAME_HEADER_FONT_SIZE,\n type BlockArrowElement,\n type ElementBase,\n type EmojiElement,\n type StickyElement,\n type BrushElement,\n type EllipseElement,\n type FrameElement,\n type GroupElement,\n type ImageElement,\n type ImageMask,\n type PathElement,\n type PolygonElement,\n type RectangleElement,\n type Style,\n type TextElement,\n type TextRun,\n type TextStyle,\n sliceRuns,\n listMarkers,\n paragraphCount,\n brushBodyColor,\n brushOutline,\n pickTextPlaceholder,\n} from \"@oh-just-another/scene\";\nimport { registerElementRenderer, type ElementRenderer } from \"./shape-renderer.js\";\nimport type { RenderTarget } from \"../targets/render-target.js\";\nimport { isTextBelowLod, type LodOptions } from \"./lod.js\";\nimport {\n DEFAULT_LINE_HEIGHT_FACTOR,\n layoutText,\n lineLeft,\n type EditableTextLayout,\n} from \"../text/text-editing.js\";\nimport { resolveImageSource } from \"../raster/animation-adapter.js\";\nimport { isDrawableImageSource } from \"../raster/image-source-guard.js\";\nimport {\n LABEL_PADDING_EM,\n LABEL_AUTOFIT_MIN_PX,\n LABEL_AUTOFIT_MAX_PX,\n STICKY_DEFAULT_FILL,\n STICKY_CORNER_RADIUS,\n STICKY_AUTHOR_FONT_SIZE,\n STICKY_AUTHOR_COLOR,\n STICKY_SHADOW_COLOR,\n STICKY_SHADOW_OFFSET_Y,\n STICKY_TAG_FONT_SIZE,\n STICKY_TAG_PAD_X,\n STICKY_TAG_HEIGHT,\n STICKY_TAG_GAP,\n STICKY_TAG_BG,\n STICKY_TAG_COLOR,\n STICKY_REACTION_FONT_SIZE,\n STICKY_REACTION_HEIGHT,\n STICKY_REACTION_PAD_X,\n STICKY_REACTION_GAP,\n STICKY_REACTION_BG,\n STICKY_REACTION_ADD_COLOR,\n STICKY_REACTION_MIN_SCREEN_PX,\n STICKY_REACTION_COLOR,\n LIST_MARKER_GAP_EM,\n TEXT_DECORATION_THICKNESS,\n TEXT_UNDERLINE_OFFSET,\n TEXT_STRIKETHROUGH_OFFSET,\n ARROWHEAD_HEAD_RATIO,\n ARROWHEAD_BODY_THICKNESS,\n ARROWHEAD_RATIO_MIN,\n ARROWHEAD_RATIO_MAX,\n FRAME_STROKE_COLOR,\n FRAME_FILL_COLOR,\n FRAME_HEADER_BG_COLOR,\n FRAME_HEADER_TEXT_COLOR,\n TEXT_PLACEHOLDER_COLOR,\n} from \"../constants.js\";\nimport { req, type Vec2 } from \"@oh-just-another/types\";\n\n/**\n * Applies common style fields to a target. Returns whether any fill or stroke\n * was configured \u2014 shape renderers use the result to decide which paint call\n * to issue.\n */\nconst applyStyle = (style: Style, target: RenderTarget): { fill: boolean; stroke: boolean } => {\n const hasFill = style.fill !== undefined && style.fill !== \"transparent\";\n const hasStroke =\n style.stroke !== undefined && style.stroke !== \"transparent\" && (style.strokeWidth ?? 1) > 0;\n\n if (hasFill) target.setFill(style.fill);\n if (hasStroke) {\n target.setStroke(style.stroke);\n target.setStrokeWidth(style.strokeWidth ?? 1);\n if (style.lineCap) target.setLineCap(style.lineCap);\n if (style.lineJoin) target.setLineJoin(style.lineJoin);\n if (style.dashArray) target.setDashArray(style.dashArray);\n }\n if (style.opacity !== undefined) target.setOpacity(style.opacity);\n\n return { fill: hasFill, stroke: hasStroke };\n};\n\nconst drawRectangle: ElementRenderer<RectangleElement> = (shape, target) => {\n const { fill, stroke } = applyStyle(shape.style, target);\n if (!fill && !stroke) return;\n const r = getCornerRadius(shape.style.roundness, shape.width, shape.height);\n // Fill path \u2014 always uses the original shape geometry.\n if (fill) {\n target.beginPath();\n if (r > 0) {\n buildRoundedRectPath(target, 0, 0, shape.width, shape.height, r);\n } else {\n target.rect(0, 0, shape.width, shape.height);\n }\n target.fill();\n }\n // Stroke path \u2014 offset by `strokeAlign` so the stroke sits inside\n // / centred-on / outside the fill region. The default (omitted /\n // `center`) reuses the fill geometry. Implemented at this layer so\n // every backend (Canvas2D, WebGL2, SVG) honours strokeAlign without\n // backend-specific work \u2014 the math is purely on the rect bounds.\n if (stroke) {\n const offset = strokeAlignOffset(shape.style);\n const sx = offset;\n const sy = offset;\n const sw = shape.width - 2 * offset;\n const sh = shape.height - 2 * offset;\n if (sw <= 0 || sh <= 0) return; // degenerate offset \u2014 skip\n const sr = r > 0 ? Math.max(0, r - offset) : 0;\n target.beginPath();\n if (sr > 0) {\n buildRoundedRectPath(target, sx, sy, sw, sh, sr);\n } else {\n target.rect(sx, sy, sw, sh);\n }\n target.stroke();\n }\n};\n\n/**\n * Translate `Style.strokeAlign` into a path-offset distance in world\n * units. The rendered stroke geometry shifts by `\u00B1half-width` along\n * the inward / outward normal:\n * center \u2192 0 (path centred \u2014 Canvas2D / SVG default).\n * inside \u2192 +half-width (path moves inward so the stroke's outer\n * edge sits on the original fill boundary).\n * outside \u2192 -half-width (path moves outward so the stroke's inner\n * edge sits on the boundary).\n *\n * Only used by axis-aligned primitives (rectangle, container) where\n * \"inward\" reduces to \"subtract from bbox\".\n */\nconst strokeAlignOffset = (style: Style): number => {\n const align = style.strokeAlign ?? \"center\";\n if (align === \"center\") return 0;\n const half = (style.strokeWidth ?? 1) / 2;\n return align === \"inside\" ? half : -half;\n};\n\n/**\n * Build a rounded-rect path via the standard \"4 corners with\n * quadratic Bezier arcs\" pattern \u2014 same shape every backend\n * understands without a special `roundRect()` API:\n *\n * \u250C\u2500\u2500\u2500arc\u2500\u2500\u2500\u2510\n * \u2502 \u2502\n * arc arc\n * \u2502 \u2502\n * \u2514\u2500\u2500\u2500arc\u2500\u2500\u2500\u2518\n *\n * Quadratic control points sit at each corner of the rect; the\n * curve goes from `r` units along one side to `r` units along the\n * adjacent side.\n *\n * Radius `r` is pre-clamped by `getCornerRadius` to half the\n * smaller side, so no overlap-handling is needed here.\n */\nexport const buildRoundedRectPath = (\n target: RenderTarget,\n x: number,\n y: number,\n w: number,\n h: number,\n r: number,\n): void => {\n target.moveTo(x + r, y);\n target.lineTo(x + w - r, y);\n target.quadraticCurveTo(x + w, y, x + w, y + r);\n target.lineTo(x + w, y + h - r);\n target.quadraticCurveTo(x + w, y + h, x + w - r, y + h);\n target.lineTo(x + r, y + h);\n target.quadraticCurveTo(x, y + h, x, y + h - r);\n target.lineTo(x, y + r);\n target.quadraticCurveTo(x, y, x + r, y);\n target.closePath();\n};\n\nconst drawEllipse: ElementRenderer<EllipseElement> = (shape, target) => {\n const { fill, stroke } = applyStyle(shape.style, target);\n if (!fill && !stroke) return;\n const rx = shape.width / 2;\n const ry = shape.height / 2;\n if (fill) {\n target.beginPath();\n target.ellipse(rx, ry, rx, ry);\n target.fill();\n }\n if (stroke) {\n // Inset / outset radii by `strokeAlignOffset` so the stroke\n // sits inside / centred-on / outside the fill ellipse. Centre\n // stays the same; radii shift uniformly. Degenerate (radius \u2264 0)\n // skips the pass.\n const offset = strokeAlignOffset(shape.style);\n const srx = rx - offset;\n const sry = ry - offset;\n if (srx <= 0 || sry <= 0) return;\n target.beginPath();\n target.ellipse(rx, ry, srx, sry);\n target.stroke();\n }\n};\n\nconst drawPolygon: ElementRenderer<PolygonElement> = (shape, target) => {\n if (shape.points.length < 2) return;\n const { fill, stroke } = applyStyle(shape.style, target);\n if (!fill && !stroke) return;\n if (fill) {\n target.beginPath();\n polygonPath(target, shape.points);\n target.fill();\n }\n if (stroke) {\n const offset = strokeAlignOffset(shape.style);\n const pts = offset !== 0 ? polygonMath.offsetClosedPath(shape.points, offset) : shape.points;\n target.beginPath();\n polygonPath(target, pts);\n target.stroke();\n }\n};\n\n/** Emit a closed polygon outline as `moveTo` + `lineTo`s + `closePath`. */\nconst polygonPath = (target: RenderTarget, pts: readonly Vec2[]): void => {\n const first = pts[0];\n if (first === undefined) return;\n target.moveTo(first.x, first.y);\n for (let i = 1; i < pts.length; i++) {\n const p = pts[i];\n if (p === undefined) continue;\n target.lineTo(p.x, p.y);\n }\n target.closePath();\n};\n\nconst drawPath: ElementRenderer<PathElement> = (shape, target) => {\n if (shape.commands.length === 0) return;\n const { fill, stroke } = applyStyle(shape.style, target);\n if (!fill && !stroke) return;\n target.beginPath();\n for (const cmd of shape.commands) {\n switch (cmd.kind) {\n case \"M\":\n target.moveTo(cmd.to.x, cmd.to.y);\n break;\n case \"L\":\n target.lineTo(cmd.to.x, cmd.to.y);\n break;\n case \"Q\":\n target.quadraticCurveTo(cmd.control.x, cmd.control.y, cmd.to.x, cmd.to.y);\n break;\n case \"C\":\n target.bezierCurveTo(\n cmd.control1.x,\n cmd.control1.y,\n cmd.control2.x,\n cmd.control2.y,\n cmd.to.x,\n cmd.to.y,\n );\n break;\n case \"Z\":\n target.closePath();\n break;\n }\n }\n if (fill) target.fill();\n if (stroke) target.stroke();\n};\n\n/**\n * Rich-text path: draw a text element whose glyphs carry per-run styling\n * (bold / italic / colour / decoration). Each visual line is split into\n * style segments (via `sliceRuns` against the line's source offsets) and each\n * segment is painted with its own font + fill at an accumulated x offset \u2014\n * so it renders identically on Canvas2D, WebGL2 and SVG through the shared\n * `RenderTarget`. Line breaking uses the ELEMENT's base font metrics (matches\n * the plain-text path); per-run weight only affects glyph paint + segment\n * widths, an acceptable etap-1 approximation for wrapping.\n */\n/**\n * Sticky note: a rounded card filled with `style.fill` (default sticky\n * yellow); the text itself is the shared embedded label, drawn by the\n * scene renderer's label pass. The author name renders along the bottom\n * edge when `showAuthor` is set.\n */\n/**\n * The zoom at which `shape`'s shorter side spans exactly\n * `STICKY_REACTION_MIN_SCREEN_PX` on screen \u2014 the reaction chrome's\n * visibility threshold for that sticky.\n */\nconst stickyReactionMinZoom = (shape: StickyElement): number =>\n STICKY_REACTION_MIN_SCREEN_PX / Math.max(1, Math.min(shape.width, shape.height));\n\n/**\n * Whether `shape` is large enough on screen at `zoom` for its reaction\n * chrome (pills and the \"+\" button) to be drawn and clickable.\n */\nexport const stickyReactionChromeVisible = (shape: StickyElement, zoom: number): boolean =>\n Math.min(shape.width, shape.height) * zoom >= STICKY_REACTION_MIN_SCREEN_PX;\n\n/**\n * The pill scale factor keeping reaction chrome at a CONSTANT on-screen\n * size: world size = base / zoom, with the divisor clamped at the sticky's\n * visibility threshold so a zoomed-out board gets shrinking (not\n * card-swallowing) pills.\n */\nconst stickyReactionScale = (shape: StickyElement, zoom: number): number =>\n 1 / Math.max(zoom > 0 ? zoom : 1, stickyReactionMinZoom(shape));\n\ninterface StickyReactionRect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\ninterface StickyReactionPill extends StickyReactionRect {\n readonly glyph: string;\n readonly label: string;\n}\n\n/**\n * Layout of a sticky's reaction pills + the \"+\" add button under its\n * bottom edge, in the shape's LOCAL space. Pills flow left-to-right and\n * WRAP onto new rows (inline-block style) when they'd overrun the card\n * width \u2014 every reaction is always laid out, none are dropped. ONE\n * implementation shared by the canvas renderer and the DOM click-zone\n * overlay so the hit areas always match the painted pills.\n *\n * `measure` must be bound to `STICKY_REACTION_FONT_SIZE`-sized system-ui\n * text (base px); `zoom` is the current view scale \u2014 pill sizes are\n * divided by it so they stay visually constant (clamped at the sticky's\n * {@link stickyReactionChromeVisible} threshold).\n */\nexport const stickyReactionLayout = (\n shape: StickyElement,\n measure: (s: string) => number,\n zoom = 1,\n): { readonly pills: readonly StickyReactionPill[]; readonly add: StickyReactionRect } => {\n const k = stickyReactionScale(shape, zoom);\n const gap = STICKY_REACTION_GAP * k;\n const h = STICKY_REACTION_HEIGHT * k;\n const x0 = STICKY_CORNER_RADIUS + 2;\n const pills: StickyReactionPill[] = [];\n let x = x0;\n let y = shape.height + gap;\n for (const reaction of shape.reactions ?? []) {\n const users = (reaction as { users?: readonly string[]; count?: number }).users;\n const count = users?.length ?? (reaction as { count?: number }).count ?? 0;\n const label = `${reaction.glyph} ${String(count)}`;\n const width = (measure(label) + 2 * STICKY_REACTION_PAD_X) * k;\n if (x > x0 && x + width > shape.width) {\n x = x0;\n y += h + gap;\n }\n pills.push({ glyph: reaction.glyph, label, x, y, width, height: h });\n x += width + gap;\n }\n if (x > x0 && x + h > shape.width) {\n x = x0;\n y += h + gap;\n }\n return { pills, add: { x, y, width: h, height: h } };\n};\n\n/** Pills half of {@link stickyReactionLayout} (click-zone overlay helper). */\nexport const stickyReactionPillRects = (\n shape: StickyElement,\n measure: (s: string) => number,\n zoom = 1,\n): readonly StickyReactionPill[] => stickyReactionLayout(shape, measure, zoom).pills;\n\n/** \"+\" button half of {@link stickyReactionLayout} (click-zone overlay helper). */\nexport const stickyReactionAddRect = (\n shape: StickyElement,\n measure: (s: string) => number,\n zoom = 1,\n): StickyReactionRect => stickyReactionLayout(shape, measure, zoom).add;\n\nconst drawSticky: ElementRenderer<StickyElement> = (shape, target, ctx) => {\n const fill = shape.style.fill ?? STICKY_DEFAULT_FILL;\n if (shape.style.opacity !== undefined) target.setOpacity(shape.style.opacity);\n const w = shape.width;\n const h = shape.height;\n const r = STICKY_CORNER_RADIUS;\n\n // Soft drop shadow under the card, offset downwards.\n target.setFill(STICKY_SHADOW_COLOR);\n target.beginPath();\n buildRoundedRectPath(target, 1, STICKY_SHADOW_OFFSET_Y, w - 2, h - 2, r);\n target.fill();\n\n // The card body \u2014 a plain rounded sheet over its drop shadow.\n target.setFill(fill);\n target.beginPath();\n buildRoundedRectPath(target, 0, 0, w, h, r);\n target.fill();\n\n // Tag pills along the bottom edge.\n if (ctx?.content?.stickyTags !== false && shape.tags !== undefined && shape.tags.length > 0) {\n target.setFont(\"system-ui, sans-serif\", STICKY_TAG_FONT_SIZE, {});\n target.setTextAlign(\"left\");\n target.setTextBaseline(\"top\");\n let x = r + 2;\n const y = h - STICKY_TAG_HEIGHT - 3;\n for (const tag of shape.tags) {\n const tw = target.measureText(tag).width + 2 * STICKY_TAG_PAD_X;\n if (x + tw > w - r) break;\n target.setFill(STICKY_TAG_BG);\n target.beginPath();\n buildRoundedRectPath(target, x, y, tw, STICKY_TAG_HEIGHT, STICKY_TAG_HEIGHT / 2);\n target.fill();\n target.setFill(STICKY_TAG_COLOR);\n target.fillText(\n tag,\n x + STICKY_TAG_PAD_X,\n y + (STICKY_TAG_HEIGHT - STICKY_TAG_FONT_SIZE) / 2,\n );\n x += tw + STICKY_TAG_GAP;\n }\n }\n\n if (\n ctx?.content?.stickyAuthor !== false &&\n shape.showAuthor === true &&\n shape.authorName !== undefined &&\n shape.authorName !== \"\"\n ) {\n target.setFont(\"system-ui, sans-serif\", STICKY_AUTHOR_FONT_SIZE, {});\n target.setTextAlign(\"left\");\n target.setTextBaseline(\"top\");\n target.setFill(STICKY_AUTHOR_COLOR);\n const authorY =\n shape.tags !== undefined && shape.tags.length > 0\n ? h - STICKY_TAG_HEIGHT - STICKY_AUTHOR_FONT_SIZE - 8\n : h - STICKY_AUTHOR_FONT_SIZE - 4;\n target.fillText(shape.authorName, r + 2, authorY);\n }\n\n // Reaction pills under the bottom-left edge \u2014 canvas is the single\n // visual source (exports included); the DOM overlay only overlays\n // transparent click zones on the same rects.\n const zoom = ctx?.zoom ?? 1;\n const k = stickyReactionScale(shape, zoom);\n // Once the card is small on screen the whole reaction chrome is hidden \u2014\n // constant on-screen pills would swallow it.\n const chromeVisible = stickyReactionChromeVisible(shape, zoom);\n const drawReactions = chromeVisible && ctx?.content?.stickyReactions !== false;\n // \"+\" add-reaction button \u2014 UI chrome drawn on the canvas so it tracks\n // the shape 1:1 while dragging. Shown only for the HOVERED sticky in\n // interactive renders; exports and read-only views switch it off.\n const drawAdd =\n chromeVisible && ctx?.content?.stickyAddButton !== false && ctx?.hoveredElement === shape.id;\n if (drawReactions || drawAdd) {\n // Measure at the BASE font size (the layout contract). Text is also\n // DRAWN at the base size inside a `scale(k)` transform: a fractional\n // per-frame font size would defeat the backend's string-bitmap cache\n // during smooth zoom (a fresh rasterisation per pill per frame).\n target.setFont(\"system-ui, sans-serif\", STICKY_REACTION_FONT_SIZE, {});\n target.setTextAlign(\"left\");\n target.setTextBaseline(\"top\");\n const layout = stickyReactionLayout(shape, (t) => target.measureText(t).width, zoom);\n if (drawReactions) {\n for (const pill of layout.pills) {\n target.setFill(STICKY_REACTION_BG);\n target.beginPath();\n buildRoundedRectPath(target, pill.x, pill.y, pill.width, pill.height, pill.height / 2);\n target.fill();\n target.setFill(STICKY_REACTION_COLOR);\n target.save();\n target.translate(pill.x, pill.y);\n target.scale(k, k);\n target.fillText(\n pill.label,\n STICKY_REACTION_PAD_X,\n (STICKY_REACTION_HEIGHT - STICKY_REACTION_FONT_SIZE) / 2,\n );\n target.restore();\n }\n }\n if (drawAdd) {\n const add = layout.add;\n target.setFill(STICKY_REACTION_BG);\n target.beginPath();\n buildRoundedRectPath(target, add.x, add.y, add.width, add.height, add.height / 2);\n target.fill();\n // Vector \"+\" cross as two filled bars \u2014 crisper than a glyph at any\n // zoom, and immune to per-backend multi-subpath stroke quirks.\n const cx = add.x + add.width / 2;\n const cy = add.y + add.height / 2;\n const arm = add.height * 0.22;\n const bar = 1.4 * k;\n target.setFill(STICKY_REACTION_ADD_COLOR);\n target.beginPath();\n target.rect(cx - arm, cy - bar / 2, arm * 2, bar);\n target.fill();\n target.beginPath();\n target.rect(cx - bar / 2, cy - arm, bar, arm * 2);\n target.fill();\n }\n }\n};\n\n/** Emoji element: one glyph filling the element's square via the text path. */\nconst drawEmoji: ElementRenderer<EmojiElement> = (shape, target) => {\n if (shape.style.opacity !== undefined) target.setOpacity(shape.style.opacity);\n target.setFont(\"system-ui, sans-serif\", shape.size, {});\n target.setTextAlign(\"left\");\n target.setTextBaseline(\"top\");\n target.setFill(shape.style.fill ?? \"#000\");\n target.fillText(shape.glyph, 0, 0);\n};\n\n/**\n * Geometry of an embedded shape label: the synthetic text element the\n * text renderer can draw, plus the local-space offset where it starts.\n * Shared by the renderer and the inline-edit caret path (state) so the\n * glyphs and the caret can't drift apart.\n */\n/**\n * Auto-fit font sizing for `ShapeLabel.autoFit` (sticky notes): the\n * largest size in [`LABEL_AUTOFIT_MIN_PX`, `LABEL_AUTOFIT_MAX_PX`]\n * whose wrapped layout fits the padded shape body, found by binary\n * search over `layoutText`. Memoized \u2014 the measure callback varies by\n * backend, so the cache key folds in a coarse measure fingerprint.\n */\nconst autoFitCache = new Map<string, number>();\n\nconst autoFitFontSize = (\n text: string,\n boxW: number,\n boxH: number,\n measure: (s: string) => number,\n baseSize: number,\n paragraphs: TextElement[\"paragraphs\"],\n): number => {\n // The measure callback is bound to `baseSize`; normalise so the cache\n // key (and the search) are stable across backends and base sizes.\n const fingerprint = Math.round((measure(\"Mg \u0432\u043E\u0434\u043E\u0451\u043C\") / baseSize) * 1000);\n const key = `${text}|${String(Math.round(boxW))}x${String(Math.round(boxH))}|${String(fingerprint)}`;\n const cached = autoFitCache.get(key);\n if (cached !== undefined) return cached;\n\n const fits = (size: number): boolean => {\n const pad = LABEL_PADDING_EM * size;\n const maxWidth = boxW - 2 * pad;\n if (maxWidth < size) return false;\n // Rescale the base-size measurement to the candidate size so the\n // wrap decisions inside layoutText are internally consistent.\n const scaled = (t: string): number => (measure(t) * size) / baseSize;\n const layout = layoutText(text, scaled, {\n fontSize: size,\n maxWidth,\n ...(paragraphs !== undefined ? { paragraphs } : {}),\n });\n return layout.lines.length * layout.lineHeight <= boxH - 2 * pad;\n };\n let lo = LABEL_AUTOFIT_MIN_PX;\n let hi = LABEL_AUTOFIT_MAX_PX;\n while (hi - lo > 1) {\n const mid = Math.floor((lo + hi) / 2);\n if (fits(mid)) lo = mid;\n else hi = mid;\n }\n if (autoFitCache.size > 512) autoFitCache.clear();\n autoFitCache.set(key, lo);\n return lo;\n};\n\nexport const shapeLabelLayout = (\n shape: ElementBase,\n measure: (s: string) => number,\n): {\n readonly synthetic: TextElement;\n readonly offsetX: number;\n readonly offsetY: number;\n /** Visible window in layout-space Y (lines outside are not painted). */\n readonly windowTop: number;\n readonly windowBottom: number;\n} | null => {\n const label = shape.label;\n if (label === undefined) return null;\n const bounds = getElementLocalBounds(shape);\n const fontSize =\n label.autoFit === true && label.text !== \"\"\n ? autoFitFontSize(\n label.text,\n bounds.width,\n bounds.height,\n measure,\n label.fontSize,\n label.paragraphs,\n )\n : label.fontSize;\n const pad = LABEL_PADDING_EM * fontSize;\n const maxWidth = Math.max(fontSize, bounds.width - 2 * pad);\n // `measure` arrives bound to the label's BASE font size; when auto-fit\n // picked a different size, rescale so wrap decisions match the glyphs\n // that will actually be drawn.\n const scaledMeasure =\n fontSize === label.fontSize\n ? measure\n : (t: string): number => (measure(t) * fontSize) / label.fontSize;\n // Block-level vertical alignment is applied via `offsetY` below; the\n // synthetic's glyph baseline stays \"top\" so drawn glyphs, the caret and\n // selection rects all share top-anchored line coordinates.\n const valign = label.style?.textBaseline ?? \"middle\";\n const style: TextStyle = {\n textAlign: \"center\",\n ...label.style,\n textBaseline: \"top\",\n };\n const layout = layoutText(label.text, scaledMeasure, {\n fontSize,\n maxWidth,\n ...(label.paragraphs !== undefined ? { paragraphs: label.paragraphs } : {}),\n });\n // Text never escapes the shape body: only the lines inside the padded\n // window are painted (the flat text keeps the rest for editing). While\n // the inline editor is open the transient `metadata.labelScrollLines`\n // scrolls that window so the caret stays visible.\n const innerHeight = Math.max(0, bounds.height - 2 * pad);\n const clipLines = Math.max(0, Math.floor(innerHeight / layout.lineHeight));\n const rawScroll = shape.metadata?.labelScrollLines;\n const maxScroll = Math.max(0, layout.lines.length - clipLines);\n const scroll = Math.max(\n 0,\n Math.min(maxScroll, typeof rawScroll === \"number\" ? Math.floor(rawScroll) : 0),\n );\n const synthetic = {\n id: shape.id,\n layerId: shape.layerId,\n type: \"text\",\n position: { x: 0, y: 0 },\n rotation: 0,\n scale: { x: 1, y: 1 },\n order: shape.order,\n style,\n text: label.text,\n fontFamily: label.fontFamily,\n fontSize,\n maxWidth,\n clipStart: scroll,\n clipLines,\n ...(label.runs !== undefined ? { runs: label.runs } : {}),\n ...(label.paragraphs !== undefined ? { paragraphs: label.paragraphs } : {}),\n } as TextElement;\n const textH = Math.min(layout.lines.length - scroll, clipLines) * layout.lineHeight;\n const windowAnchor =\n valign === \"top\"\n ? bounds.y + pad\n : valign === \"bottom\"\n ? bounds.y + bounds.height - textH - pad\n : bounds.y + Math.max(pad, (bounds.height - textH) / 2);\n // Lines keep their absolute layout Y (line \u00D7 lineHeight); shifting the\n // whole block up by the scroll puts the visible window at the anchor.\n const offsetY = windowAnchor - scroll * layout.lineHeight;\n return {\n synthetic,\n offsetX: bounds.x + pad,\n offsetY,\n windowTop: scroll * layout.lineHeight,\n windowBottom: (scroll + clipLines) * layout.lineHeight,\n };\n};\n\n/**\n * Draw a shape's embedded label inside its local bounds. Reuses the text\n * renderer wholesale (wrap, runs, lists, highlight); vertical alignment\n * places the whole block, `textAlign` centres lines within the padded\n * width. Called by the scene renderer after the shape body.\n */\nexport const drawShapeLabel = (\n shape: ElementBase,\n target: RenderTarget,\n /** Readable-text LOD: skip the label when its resolved font size is below the floor on screen. */\n lod?: { readonly zoom: number; readonly lod: LodOptions },\n): void => {\n const label = shape.label;\n if (label === undefined || label.text === \"\") return;\n // Fast path on the base size: an auto-fit label can only grow from it.\n if (lod && !label.autoFit && isTextBelowLod(label.fontSize, lod.zoom, lod.lod)) return;\n // Measure with the label's base font \u2014 same metrics drawText wraps with.\n const weight = label.style?.fontWeight;\n const fontStyle = label.style?.fontStyle;\n target.setFont(label.fontFamily, label.fontSize, {\n ...(weight ? { weight } : {}),\n ...(fontStyle ? { style: fontStyle } : {}),\n });\n const placed = shapeLabelLayout(shape, (s) => target.measureText(s).width);\n if (!placed) return;\n if (lod && isTextBelowLod(placed.synthetic.fontSize, lod.zoom, lod.lod)) return;\n target.save();\n target.translate(placed.offsetX, placed.offsetY);\n drawText(placed.synthetic, target);\n target.restore();\n};\n\n/**\n * Internal draw hint carried by label synthetics: paint at most this many\n * visual lines so the text never escapes the shape body. Never serialized.\n */\nconst clipWindowOf = (\n shape: TextElement,\n): { readonly start: number; readonly end: number } | undefined => {\n const hint = shape as { readonly clipStart?: number; readonly clipLines?: number };\n if (hint.clipLines === undefined) return undefined;\n const start = hint.clipStart ?? 0;\n return { start, end: start + hint.clipLines };\n};\n\n/**\n * Draw the derived list markers (\"\u2022\" / \"1.\") for every paragraph's first\n * visual line, right-aligned into the indent slot the layout reserved.\n * Uses the element's base font + fill; leaves the fill set to `color`.\n */\nconst drawListMarkersForLayout = (\n shape: TextElement,\n layout: EditableTextLayout,\n target: RenderTarget,\n color: string,\n): void => {\n if (shape.paragraphs === undefined) return;\n const align = shape.style.textAlign ?? \"left\";\n const markers = listMarkers(shape.paragraphs, paragraphCount(shape.text));\n const gap = LIST_MARKER_GAP_EM * shape.fontSize;\n target.setFill(color);\n const markerClip = clipWindowOf(shape);\n layout.lines.forEach((line, i) => {\n if (markerClip !== undefined && (i < markerClip.start || i >= markerClip.end)) return;\n if (!line.paraFirst) return;\n const marker = markers[line.para];\n if (marker == null) return;\n const w = target.measureText(marker).width;\n const left = lineLeft(line, layout.blockWidth, align);\n target.fillText(marker, left - gap - w, i * layout.lineHeight);\n });\n};\n\nconst drawStyledText = (shape: TextElement, target: RenderTarget): void => {\n const align = shape.style.textAlign ?? \"left\";\n const fontSize = shape.fontSize;\n target.setTextAlign(\"left\");\n target.setTextBaseline(shape.style.textBaseline ?? \"top\");\n\n // Apply the resolved font for a run: run overlay wins, element style is\n // the fallback for any field the run omits.\n const setSegFont = (st: TextRun[\"style\"]): void => {\n const weight = st?.fontWeight ?? shape.style.fontWeight;\n const style = st?.fontStyle ?? shape.style.fontStyle;\n target.setFont(shape.fontFamily, fontSize, {\n ...(weight ? { weight } : {}),\n ...(style ? { style } : {}),\n });\n };\n\n // Base-font line breaking \u2014 same metrics the plain path wraps with.\n setSegFont(undefined);\n const layout = layoutText(shape.text, (s) => target.measureText(s).width, {\n fontSize,\n ...(shape.maxWidth !== undefined ? { maxWidth: shape.maxWidth } : {}),\n ...(shape.paragraphs !== undefined ? { paragraphs: shape.paragraphs } : {}),\n });\n\n interface Seg {\n readonly text: string;\n readonly style: TextStyle | undefined;\n readonly width: number;\n }\n const perLine = layout.lines.map((line) => {\n const segs: Seg[] = sliceRuns(shape, line.start, line.end).map((r) => {\n setSegFont(r.style);\n return { text: r.text, style: r.style, width: target.measureText(r.text).width };\n });\n const total = segs.reduce((a, s) => a + s.width, 0);\n return { segs, total };\n });\n\n // Alignment box: fixed budget, or the widest STYLED line so bold text\n // stays self-consistently aligned.\n const blockWidth =\n shape.maxWidth ??\n perLine.reduce((m, l, i) => Math.max(m, l.total + req(layout.lines[i]).indentX), 0);\n const thickness = Math.max(1, fontSize * TEXT_DECORATION_THICKNESS);\n\n const styledClip = clipWindowOf(shape);\n perLine.forEach((line, i) => {\n if (styledClip !== undefined && (i < styledClip.start || i >= styledClip.end)) return;\n const top = i * layout.lineHeight;\n const indentX = req(layout.lines[i]).indentX;\n let x =\n indentX +\n (align === \"center\"\n ? (blockWidth - indentX) / 2 - line.total / 2\n : align === \"right\"\n ? blockWidth - indentX - line.total\n : 0);\n for (const seg of line.segs) {\n const color = seg.style?.fill ?? shape.style.fill ?? \"#000\";\n const opacity = seg.style?.opacity ?? shape.style.opacity;\n setSegFont(seg.style);\n if (opacity !== undefined) target.setOpacity(opacity);\n // Highlight first \u2014 a full line-height rect under the glyphs, so the\n // text paints on top of its own marker stripe.\n const highlight = seg.style?.highlight ?? shape.style.highlight;\n if (seg.width > 0 && highlight !== undefined && highlight !== \"transparent\") {\n target.setFill(highlight);\n target.beginPath();\n target.rect(x, top, seg.width, layout.lineHeight);\n target.fill();\n }\n target.setFill(color);\n target.fillText(seg.text, x, top);\n\n const deco = seg.style?.textDecoration ?? shape.style.textDecoration;\n if (seg.width > 0 && (deco?.underline || deco?.strikethrough)) {\n if (deco.underline) {\n target.beginPath();\n target.rect(x, top + fontSize * TEXT_UNDERLINE_OFFSET, seg.width, thickness);\n target.fill();\n }\n if (deco.strikethrough) {\n target.beginPath();\n target.rect(\n x,\n top + fontSize * TEXT_STRIKETHROUGH_OFFSET - thickness / 2,\n seg.width,\n thickness,\n );\n target.fill();\n }\n }\n x += seg.width;\n }\n });\n // Markers use the element's base font/colour, after the segments so the\n // font state is deterministic.\n setSegFont(undefined);\n drawListMarkersForLayout(shape, layout, target, shape.style.fill ?? \"#000\");\n};\n\nconst drawText: ElementRenderer<TextElement> = (shape, target, ctx) => {\n // Empty text while writing: draw the element's placeholder prompt in the\n // neutral grey, with the element's own font / alignment so the caret and\n // the prompt line up. Interactive rendering only (`ctx.textPlaceholders`).\n if (shape.text === \"\" && ctx?.textPlaceholders === true) {\n const { runs: _runs, ...plain } = shape;\n drawText(\n {\n ...plain,\n text: pickTextPlaceholder(shape.id),\n style: { ...shape.style, fill: TEXT_PLACEHOLDER_COLOR },\n },\n target,\n );\n return;\n }\n // Rich text (styled runs) takes a dedicated path; plain text keeps the\n // original single-style path byte-for-byte (golden-SVG compatible).\n if (shape.runs !== undefined && shape.runs.length > 0) {\n drawStyledText(shape, target);\n return;\n }\n const align = shape.style.textAlign ?? \"left\";\n const weight = shape.style.fontWeight;\n const fontStyle = shape.style.fontStyle;\n target.setFont(shape.fontFamily, shape.fontSize, {\n ...(weight ? { weight } : {}),\n ...(fontStyle ? { style: fontStyle } : {}),\n });\n // Lines are positioned manually (per-line x below) so the caret\n // geometry computed from the same `layoutText` lines up exactly, so\n // the target always draws left-anchored.\n target.setTextAlign(\"left\");\n target.setTextBaseline(shape.style.textBaseline ?? \"top\");\n\n // Color: use fill if specified, otherwise default to black.\n const color = shape.style.fill ?? \"#000\";\n target.setFill(color);\n if (shape.style.opacity !== undefined) target.setOpacity(shape.style.opacity);\n\n // Resolve per-line geometry once (x = align offset, top = i \u00D7\n // lineHeight). Single-line text without list attrs skips the wrap engine.\n const fontSize = shape.fontSize;\n let lines: { text: string; x: number; width: number; top: number }[];\n if (\n shape.maxWidth === undefined &&\n !shape.text.includes(\"\\n\") &&\n shape.paragraphs === undefined\n ) {\n lines = [{ text: shape.text, x: 0, width: target.measureText(shape.text).width, top: 0 }];\n } else {\n // Measure with the target's own `measureText` so wrapping matches\n // exactly what this backend draws.\n const measure = (s: string) => target.measureText(s).width;\n const layout = layoutText(shape.text, measure, {\n fontSize,\n ...(shape.maxWidth !== undefined ? { maxWidth: shape.maxWidth } : {}),\n ...(shape.paragraphs !== undefined ? { paragraphs: shape.paragraphs } : {}),\n });\n lines = layout.lines.map((line, i) => ({\n text: line.text,\n x: lineLeft(line, layout.blockWidth, align),\n width: line.width,\n top: i * layout.lineHeight,\n }));\n const clip = clipWindowOf(shape);\n if (clip !== undefined) lines = lines.filter((_, i) => i >= clip.start && i < clip.end);\n drawListMarkersForLayout(shape, layout, target, color);\n }\n\n // Highlight stripes under the glyphs (marker-style), then the text on top.\n const highlight = shape.style.highlight;\n if (highlight !== undefined && highlight !== \"transparent\") {\n const lineHeight = fontSize * DEFAULT_LINE_HEIGHT_FACTOR;\n target.setFill(highlight);\n for (const l of lines) {\n if (l.width <= 0) continue;\n target.beginPath();\n target.rect(l.x, l.top, l.width, lineHeight);\n target.fill();\n }\n target.setFill(color);\n }\n for (const l of lines) target.fillText(l.text, l.x, l.top);\n\n // Underline / strikethrough \u2014 thin filled rects per line, same on\n // Canvas2D and WebGL2 (uses the current text fill colour).\n const deco = shape.style.textDecoration;\n if (deco?.underline || deco?.strikethrough) {\n const thickness = Math.max(1, fontSize * TEXT_DECORATION_THICKNESS);\n for (const l of lines) {\n if (l.width <= 0) continue;\n if (deco.underline) {\n target.beginPath();\n target.rect(l.x, l.top + fontSize * TEXT_UNDERLINE_OFFSET, l.width, thickness);\n target.fill();\n }\n if (deco.strikethrough) {\n target.beginPath();\n target.rect(\n l.x,\n l.top + fontSize * TEXT_STRIKETHROUGH_OFFSET - thickness / 2,\n l.width,\n thickness,\n );\n target.fill();\n }\n }\n }\n};\n\n/**\n * Variable-width brush stroke. Each segment between two `BrushPoint`s\n * is drawn as a quad (two triangles) \u2014 its width interpolates from\n * `p.width` at the head to `q.width` at the tail. Renders\n * pressure-sensitive ink that gets thicker / thinner along the path\n * without needing per-segment `setStrokeWidth` calls (which most 2D\n * APIs treat as a single line width).\n */\nconst drawBrush: ElementRenderer<BrushElement> = (shape, target) => {\n const pts = shape.points;\n if (pts.length === 0) return;\n // Honour the stroke's opacity (drawBrush paints fills directly, so it can't\n // rely on the shared `applyStyle` the other renderers use). Set once up front\n // so both the enclosed-area fill and the body get it; the scene renderer resets\n // opacity to 1 between shapes.\n if (shape.style.opacity !== undefined) target.setOpacity(shape.style.opacity);\n // Closed stroke with a fill colour: paint the enclosed area FIRST (under the\n // stroke body) as a polygon through the centreline points. Needs \u22653 points to\n // enclose an area. Open strokes skip this entirely and are unchanged.\n if (shape.closed === true && shape.style.fill !== undefined && pts.length >= 3) {\n target.setFill(shape.style.fill);\n target.setStroke(null);\n target.beginPath();\n const start = req(pts[0]);\n target.moveTo(start.x, start.y);\n for (let i = 1; i < pts.length; i++) {\n const p = req(pts[i]);\n target.lineTo(p.x, p.y);\n }\n target.closePath();\n target.fill();\n }\n // The variable-width stroke body is painted with the shared brush-body colour\n // (the same resolution the live preview uses \u2014 see `brushBodyColor`).\n const paint = brushBodyColor(shape.style);\n target.setFill(paint);\n target.setStroke(null);\n // Single dot for one-point strokes \u2014 degenerate quad would be invisible.\n if (pts.length === 1) {\n const p = req(pts[0]);\n target.beginPath();\n target.ellipse(p.x, p.y, p.width, p.width);\n target.fill();\n return;\n }\n // Body as ONE closed outline polygon, filled once. Per-segment quads + joint\n // discs (the old approach) overlap, so at `opacity < 1` the joins double-blend\n // into dark blotches; a single fill paints every pixel exactly once.\n const outline = brushOutline(pts);\n if (outline.length >= 3) {\n target.beginPath();\n const first = req(outline[0]);\n target.moveTo(first.x, first.y);\n for (let i = 1; i < outline.length; i++) {\n const p = req(outline[i]);\n target.lineTo(p.x, p.y);\n }\n target.closePath();\n target.fill();\n }\n};\n\nconst drawImage: ElementRenderer<ImageElement> = (shape, target, ctx) => {\n // Priority: for an animated source prefer the per-frame image the\n // registered adapter returns; otherwise a preloaded handle in\n // `metadata.image`; otherwise the static `src` fallback.\n // `resolveImageSource` returns `null` while an async decode is still\n // in flight, which the backend's drawImage guard skips.\n // Sample at the per-instance clock when the caller threaded one via the\n // render context; `undefined` defers to `resolveImageSource`'s process-global\n // fallback clock (headless / preview paths).\n const t = ctx?.clock?.(shape);\n const handle = shape.animationKind\n ? resolveImageSource(shape, t)\n : (shape.metadata?.image ?? resolveImageSource(shape, t));\n // A non-drawable handle with a `fileId` is a TRANSIENT state, not a\n // problem: the first paint after a scene restore runs before async\n // rehydration re-attaches a live handle from `Scene.files`. Skip the\n // frame silently \u2014 rehydration repaints when it lands, and reports its\n // own failure if the bytes are missing or won't decode. Only handles\n // with no rehydration source fall through to the backend, which warns\n // (the image really will stay blank).\n if (!isDrawableImageSource(handle)) {\n if (handle == null || shape.fileId) return;\n }\n // `dynamic` \u2192 backends that cache the upload (WebGL2) re-upload the\n // current frame. GIF / video sources flag `metadata.animated`, and\n // any adapter-driven source is dynamic by definition.\n const dynamic = shape.metadata?.animated === true || shape.animationKind !== undefined;\n const mask = shape.mask;\n if (mask) {\n target.save();\n target.beginPath();\n buildImageMaskPath(target, mask, shape.width, shape.height);\n target.clip();\n }\n target.drawImage(handle, 0, 0, shape.width, shape.height, dynamic, shape.crop, shape.alt);\n if (mask) target.restore();\n};\n\n/**\n * Build an {@link ImageMask}'s path in the shape's LOCAL space\n * (normalised mask coordinates \u00D7 the element box). Exported so overlays\n * (crop/mask preview) can trace the same outline the renderer clips by.\n */\nexport const buildImageMaskPath = (\n target: RenderTarget,\n mask: ImageMask,\n width: number,\n height: number,\n): void => {\n switch (mask.kind) {\n case \"ellipse\":\n target.ellipse(width / 2, height / 2, width / 2, height / 2);\n return;\n case \"round-rect\": {\n const r = Math.max(0, Math.min(0.5, mask.radius)) * Math.min(width, height);\n buildRoundedRectPath(target, 0, 0, width, height, r);\n return;\n }\n case \"polygon\": {\n const pts = mask.points;\n if (pts.length < 3) return;\n const first = req(pts[0]);\n target.moveTo(first.x * width, first.y * height);\n for (let i = 1; i < pts.length; i++) {\n const p = req(pts[i]);\n target.lineTo(p.x * width, p.y * height);\n }\n target.closePath();\n return;\n }\n }\n};\n\n/**\n * Registers renderers for every `BuiltinElement` type.\n */\nexport const installBuiltinRenderers = (): void => {\n registerElementRenderer<RectangleElement>(\"rectangle\", drawRectangle);\n registerElementRenderer<EllipseElement>(\"ellipse\", drawEllipse);\n registerElementRenderer<PolygonElement>(\"polygon\", drawPolygon);\n registerElementRenderer<PathElement>(\"path\", drawPath);\n registerElementRenderer<TextElement>(\"text\", drawText);\n registerElementRenderer<ImageElement>(\"image\", drawImage);\n // Group shapes are invisible containers \u2014 the shape itself paints nothing.\n registerElementRenderer<GroupElement>(\"group\", () => {\n /* intentional no-op: group shapes are invisible containers and paint nothing */\n });\n registerElementRenderer<FrameElement>(\"frame\", drawFrame);\n // The frame paints its header strip ABOVE the rectangle, so its dirty\n // region must extend up by the header height \u2014 otherwise deleting a\n // frame leaves the header behind.\n registerRenderOverflow(\"frame\", () => ({ top: FRAME_HEADER_HEIGHT }));\n registerElementRenderer<BlockArrowElement>(\"block-arrow\", drawBlockArrow);\n registerElementRenderer<BrushElement>(\"brush\", drawBrush);\n registerElementRenderer<StickyElement>(\"sticky\", drawSticky);\n // The sticky's drop shadow paints below its box \u2014 extend the dirty\n // region so moving/deleting it doesn't leave the shadow behind.\n registerRenderOverflow(\"sticky\", (shape) => {\n // Worst-case invalidation bound for the reaction rows: every pill on\n // its own row (+ the \"+\" button row), at the largest world size the\n // visibility clamp allows (1 / the sticky's threshold zoom). Overflow\n // providers have no zoom access, so this over-approximates \u2014 costs\n // only redraw area, never leaves ghosts.\n const s = shape as StickyElement;\n const n = (s.reactions?.length ?? 0) + 1;\n const kMax = 1 / stickyReactionMinZoom(s);\n return {\n bottom:\n STICKY_SHADOW_OFFSET_Y + (STICKY_REACTION_GAP + STICKY_REACTION_HEIGHT) * kMax * n + 2,\n right: (STICKY_REACTION_GAP + STICKY_REACTION_HEIGHT) * kMax + 2,\n };\n });\n registerElementRenderer<EmojiElement>(\"emoji\", drawEmoji);\n};\n\n/**\n * Block-arrow silhouette: a rectangle body whose tip is replaced\n * by a triangle, oriented by `direction`. Path is closed and filled\n * with `style.fill`; stroke applies to the outline.\n *\n * right \u2192 \u250C\u2500\u2500\u2500\u2500\u2510\u25B6\n * \u2502 body \u2502\n * \u2514\u2500\u2500\u2500\u2500\u2518\n *\n * up \u2191 \u25B2\n * \u250C\u2500\u2500\u2510\n * \u2502 \u2502\n * \u2514\u2500\u2500\u2518\n */\nconst drawBlockArrow: ElementRenderer<BlockArrowElement> = (shape, target) => {\n const { fill, stroke } = applyStyle(shape.style, target);\n const direction = shape.direction ?? \"right\";\n const headRatio = Math.max(\n ARROWHEAD_RATIO_MIN,\n Math.min(ARROWHEAD_RATIO_MAX, shape.headRatio ?? ARROWHEAD_HEAD_RATIO),\n );\n const bodyT = Math.max(\n ARROWHEAD_RATIO_MIN,\n Math.min(ARROWHEAD_RATIO_MAX, shape.bodyThickness ?? ARROWHEAD_BODY_THICKNESS),\n );\n const w = shape.width;\n const h = shape.height;\n // Compute the local path for a `right`-pointing arrow inside\n // [0, w] \u00D7 [0, h], then rotate the resulting points if the\n // direction is different. Keeps the drawing primitives in one\n // place.\n const headW = w * headRatio;\n const bodyW = w - headW;\n const bodyHalfH = (h * bodyT) / 2;\n const cy = h / 2;\n let points: readonly [number, number][] = [\n [0, cy - bodyHalfH],\n [bodyW, cy - bodyHalfH],\n [bodyW, 0],\n [w, cy],\n [bodyW, h],\n [bodyW, cy + bodyHalfH],\n [0, cy + bodyHalfH],\n ];\n if (direction !== \"right\") {\n points = points.map(([x, y]) => rotateLocal([x, y], direction, w, h));\n }\n const ptObjs = points.map(([x, y]) => ({ x, y }));\n if (fill) {\n target.beginPath();\n polygonPath(target, ptObjs);\n target.fill();\n }\n if (stroke) {\n const offset = strokeAlignOffset(shape.style);\n const sPts = offset !== 0 ? polygonMath.offsetClosedPath(ptObjs, offset) : ptObjs;\n target.beginPath();\n polygonPath(target, sPts);\n target.stroke();\n }\n};\n\nconst rotateLocal = (\n [x, y]: readonly [number, number],\n direction: \"left\" | \"up\" | \"down\",\n w: number,\n h: number,\n): [number, number] => {\n switch (direction) {\n case \"left\":\n return [w - x, y];\n case \"up\":\n // Rotate 90\u00B0 CCW around the box centre, then translate so the\n // result still fits inside [0, w] \u00D7 [0, h].\n return [y * (w / h), h - x * (h / w)];\n case \"down\":\n return [(h - y) * (w / h), x * (h / w)];\n }\n};\n\nconst FRAME_HEADER_ELLIPSIS = \"\u2026\";\n\n/**\n * Trim `text` with a trailing ellipsis until it fits `maxWidth` (in the\n * font already set on `target`). Returns the full text when it fits, the\n * longest prefix + \"\u2026\" otherwise, or just \"\u2026\" when even one char can't\n * fit. Binary-searches the prefix length to keep `measureText` calls ~log.\n */\nconst ellipsizeToWidth = (text: string, maxWidth: number, target: RenderTarget): string => {\n if (maxWidth <= 0) return \"\";\n if (target.measureText(text).width <= maxWidth) return text;\n let lo = 0;\n let hi = text.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n const w = target.measureText(text.slice(0, mid) + FRAME_HEADER_ELLIPSIS).width;\n if (w <= maxWidth) lo = mid;\n else hi = mid - 1;\n }\n return lo > 0 ? text.slice(0, lo) + FRAME_HEADER_ELLIPSIS : FRAME_HEADER_ELLIPSIS;\n};\n\nconst drawFrame: ElementRenderer<FrameElement> = (shape, target, ctx) => {\n // Body \u2014 solid fill + thin solid outline. Frames sit at the bottom\n // z-order, so the fill backs their members without covering them.\n // Honours an explicit `style.fill`, else default white.\n target.setFill(shape.style.fill ?? FRAME_FILL_COLOR);\n target.setStroke(null);\n target.setDashArray(null);\n target.beginPath();\n target.rect(0, 0, shape.width, shape.height);\n target.fill();\n // Outline on top of the fill \u2014 a 1px SCREEN-constant hairline (doesn't scale\n // with zoom). The renderer draws in local coords where 1 unit = zoom \u00D7 scale\n // device px, so divide to keep the stroke at one device pixel. Falls back to\n // 1 world-px when no zoom context is supplied (preview / export at 1:1).\n const screenScale = (ctx?.zoom ?? 1) * (shape.scale.x || 1);\n target.setFill(null);\n target.setStroke(FRAME_STROKE_COLOR);\n target.setStrokeWidth(1 / (screenScale || 1));\n target.setDashArray(null);\n target.beginPath();\n target.rect(0, 0, shape.width, shape.height);\n target.stroke();\n\n // Header label: the strip hugs the text width but never exceeds the\n // frame's right edge; a name too long for the frame is ellipsised.\n const name = shape.name ?? \"Frame\";\n target.setFont(\"system-ui, sans-serif\", FRAME_HEADER_FONT_SIZE);\n const avail = shape.width - FRAME_HEADER_PADDING_X * 2;\n const fits = target.measureText(name).width <= avail;\n // Fits \u2192 the strip hugs the text. Too long \u2192 ellipsise the text and\n // stretch the strip to the frame's full width (the label runs to the\n // right edge).\n const label = fits ? name : ellipsizeToWidth(name, avail, target);\n const headerWidth = fits\n ? Math.min(target.measureText(name).width + FRAME_HEADER_PADDING_X * 2, shape.width)\n : shape.width;\n\n // Header label background \u2014 stretches to fit the (possibly truncated) text.\n target.setFill(FRAME_HEADER_BG_COLOR);\n target.beginPath();\n target.rect(0, -FRAME_HEADER_HEIGHT, headerWidth, FRAME_HEADER_HEIGHT);\n target.fill();\n\n // Header label text.\n target.setFill(FRAME_HEADER_TEXT_COLOR);\n target.setTextBaseline(\"middle\");\n target.setTextAlign(\"left\");\n target.fillText(label, FRAME_HEADER_PADDING_X, -FRAME_HEADER_HEIGHT / 2);\n};\n", "import type { RenderTarget } from \"./render-target.js\";\n\n/**\n * Wrap a {@link RenderTarget} so every `setOpacity(a)` becomes\n * `setOpacity(a * factor)`, leaving all other calls untouched.\n *\n * Isolation / eraser dim works by lowering the alpha for a subset of shapes.\n * The scene renderer sets that alpha *before* the shape renderer runs, but a\n * renderer that applies the shape's own `style.opacity` calls `setOpacity`\n * absolutely \u2014 overwriting the dim, so a shape carrying an explicit opacity\n * would never dim (the eraser's \"about to delete\" fade silently vanished).\n * Routing the renderer through this wrapper multiplies the two instead: a\n * plain shape stays at `factor`, and a shape with `opacity` renders at\n * `opacity * factor` \u2014 dimmed *and* semi-transparent, as expected.\n *\n * One wrapper is allocated per dimmed pass (constant `factor`) and reused for\n * every dimmed shape; method lookups are memoised so the hot per-shape draw\n * loop allocates nothing.\n */\nexport const createDimTarget = (inner: RenderTarget, factor: number): RenderTarget => {\n const cache = new Map<PropertyKey, unknown>();\n const scaledSetOpacity = (a: number): void => {\n inner.setOpacity(a * factor);\n };\n const handler: ProxyHandler<RenderTarget> = {\n get(target, prop) {\n if (prop === \"setOpacity\") return scaledSetOpacity;\n if (cache.has(prop)) return cache.get(prop);\n const value: unknown = Reflect.get(target, prop);\n const resolved =\n typeof value === \"function\" ? (value as (...a: unknown[]) => unknown).bind(target) : value;\n cache.set(prop, resolved);\n return resolved;\n },\n };\n return new Proxy(inner, handler);\n};\n", "import type { Bounds, ElementId } from \"@oh-just-another/types\";\nimport { getElementWorldBounds, type Scene, type ElementBase } from \"@oh-just-another/scene\";\n\n/**\n * Per-shape memo with object-identity invalidation. Cached value sticks\n * until the underlying shape reference changes \u2014 and because every scene\n * op (`updateElement` / `moveElement` / ...) returns a new shape object, the\n * cache invalidates automatically without the caller threading versions\n * through.\n *\n * Caches survive across frames; pair with `prune(scene)` after large\n * deletions if memory matters. For ephemeral, single-render memos use\n * a fresh `ElementCache` instance (cheap to construct).\n */\nexport class ElementCache<T> {\n private readonly entries = new Map<ElementId, { readonly ref: ElementBase; value: T }>();\n\n get(shape: ElementBase): T | undefined {\n const entry = this.entries.get(shape.id);\n if (!entry) return undefined;\n if (entry.ref !== shape) {\n this.entries.delete(shape.id);\n return undefined;\n }\n return entry.value;\n }\n\n set(shape: ElementBase, value: T): T {\n this.entries.set(shape.id, { ref: shape, value });\n return value;\n }\n\n /**\n * Lazy memo. Returns the cached value if `shape` is the same reference\n * as the one we cached against; otherwise runs `compute`, stores the\n * result, and returns it.\n */\n getOrCompute(shape: ElementBase, compute: (s: ElementBase) => T): T {\n const cached = this.get(shape);\n if (cached !== undefined) return cached;\n return this.set(shape, compute(shape));\n }\n\n invalidate(id: ElementId): void {\n this.entries.delete(id);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n /** Drop entries whose shape is no longer in the scene. */\n prune(scene: Scene): void {\n for (const id of this.entries.keys()) {\n if (!scene.elements.has(id)) this.entries.delete(id);\n }\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\n/**\n * Shared module-level cache for world-space bounds. Used by `renderScene`\n * for viewport culling and reusable from outside (hit-test, overlay) so\n * a single computation amortizes across passes.\n *\n * `getElementWorldBounds` is pure \u2014 same shape ref \u2192 same bounds \u2014 so a\n * by-identity cache is sound.\n */\nexport const sharedBoundsCache: ElementCache<Bounds> = new ElementCache<Bounds>();\n\nexport const cachedWorldBounds = (cache: ElementCache<Bounds>, shape: ElementBase): Bounds =>\n cache.getOrCompute(shape, getElementWorldBounds);\n", "/** Insertion-ordered LRU map: get() marks recency; set() evicts oldest past `cap` (by entry count). */\nexport class LruCache<K, V> {\n private readonly map = new Map<K, V>();\n constructor(\n private readonly cap: number,\n private readonly onEvict?: (key: K, value: V) => void,\n ) {}\n get(key: K): V | undefined {\n const v = this.map.get(key);\n if (v === undefined) return undefined;\n this.map.delete(key);\n this.map.set(key, v);\n return v;\n }\n has(key: K): boolean {\n return this.map.has(key);\n }\n set(key: K, value: V): void {\n this.map.delete(key);\n this.map.set(key, value);\n while (this.map.size > this.cap) {\n const oldest = this.map.keys().next().value as K;\n const ev = this.map.get(oldest);\n this.map.delete(oldest);\n if (ev !== undefined) this.onEvict?.(oldest, ev);\n }\n }\n delete(key: K): boolean {\n return this.map.delete(key);\n }\n clear(): void {\n this.map.clear();\n }\n get size(): number {\n return this.map.size;\n }\n keys(): IterableIterator<K> {\n return this.map.keys();\n }\n values(): IterableIterator<V> {\n return this.map.values();\n }\n}\n", "import type { ElementBase } from \"@oh-just-another/scene\";\nimport { LruCache } from \"./lru-cache.js\";\n\n/**\n * Per-shape rasterised cache. Keyed by the shape's identity reference \u2014\n * since scene mutations always replace the shape object (`apply(scene,\n * patch)` produces fresh references), a cache hit is guaranteed to reflect\n * the exact rendered output of the cached version. Pan / zoom invalidation\n * is the host's job: keep zoom in a small \"bucket\" (e.g. round to 0.1) and\n * include it in the key.\n *\n * LRU-by-insertion-order with a count cap. Hosts can replace with their own\n * cache by implementing the same `get` / `set` / `delete` surface.\n */\n\nexport interface ElementBitmapCache<V = unknown> {\n get(shape: ElementBase, zoomBucket: number): V | undefined;\n set(shape: ElementBase, zoomBucket: number, value: V): void;\n delete(shape: ElementBase, zoomBucket: number): void;\n clear(): void;\n readonly size: number;\n}\n\nconst keyFor = (shape: ElementBase, zoomBucket: number): string => `${shape.id}@${zoomBucket}`;\n\ninterface Entry<V> {\n readonly shapeRef: ElementBase;\n readonly value: V;\n}\n\n/**\n * In-memory LRU cache. Operates on shape identity (reference) \u2014\n * a stale shape reference for the same id is a miss because the\n * cached entry's `shapeRef !== shape`. That is the invalidation\n * mechanism \u2014 no version field needed.\n */\nexport class InMemoryElementBitmapCache<V> implements ElementBitmapCache<V> {\n private readonly entries: LruCache<string, Entry<V>>;\n\n constructor(cap = 512) {\n this.entries = new LruCache(cap);\n }\n\n get size(): number {\n return this.entries.size;\n }\n\n get(shape: ElementBase, zoomBucket: number): V | undefined {\n const key = keyFor(shape, zoomBucket);\n const e = this.entries.get(key);\n if (!e) return undefined;\n if (e.shapeRef !== shape) {\n // Reference changed \u2192 stale; evict so the slot is free.\n this.entries.delete(key);\n return undefined;\n }\n return e.value;\n }\n\n set(shape: ElementBase, zoomBucket: number, value: V): void {\n this.entries.set(keyFor(shape, zoomBucket), { shapeRef: shape, value });\n }\n\n delete(shape: ElementBase, zoomBucket: number): void {\n this.entries.delete(keyFor(shape, zoomBucket));\n }\n\n clear(): void {\n this.entries.clear();\n }\n}\n\n/**\n * Quantise a continuous zoom value to a bucket. Buckets within a\n * power-of-two range share a cache entry so small zoom adjustments\n * don't blow the cache. `bucket = 2 ^ round(log2(zoom))`.\n */\nexport const zoomBucket = (zoom: number): number => {\n if (zoom <= 0) return 1;\n return 2 ** Math.round(Math.log2(zoom));\n};\n", "import {\n getLayersInOrder,\n getElementsInLayer,\n getWorldToScreen,\n isText,\n type Scene,\n type ElementBase,\n type SpatialGrid,\n} from \"@oh-just-another/scene\";\nimport type { Bounds, LayerId, ElementId } from \"@oh-just-another/types\";\nimport { bounds as B, matrix } from \"@oh-just-another/math\";\nimport type { RenderTarget } from \"../targets/render-target.js\";\nimport type { AnimationClock } from \"../raster/animation-adapter.js\";\nimport { getElementRenderer, type ElementRenderContext } from \"./shape-renderer.js\";\nimport { drawShapeLabel } from \"./built-in-renderers.js\";\nimport { createDimTarget } from \"../targets/dim-target.js\";\nimport { cachedWorldBounds, ElementCache } from \"../caches/shape-cache.js\";\nimport { DEFAULT_PLACEHOLDER_FILL } from \"../constants.js\";\nimport type { LayerCompositeCache } from \"../caches/layer-cache-composite.js\";\nimport { zoomBucket as bucketFor } from \"../caches/shape-cache-bitmap.js\";\nimport { isTextBelowLod, screenSizeOf, type LodOptions } from \"./lod.js\";\n\nexport type { LodOptions } from \"./lod.js\";\n\nexport interface RenderSceneOptions {\n /** Skip clearing the target before drawing. Default: false. */\n readonly skipClear?: boolean;\n /** Called for shapes whose `type` has no registered renderer. Default: ignore. */\n readonly onUnknownElement?: (shape: ElementBase) => void;\n /**\n * World-space viewport bounds. When provided, shapes whose AABB does\n * not intersect this rectangle are skipped (viewport culling). Pass\n * a slightly inflated rect to avoid pop-in during pan.\n */\n readonly viewport?: Bounds;\n /**\n * Persistent bounds cache. When omitted a fresh per-render cache is\n * created \u2014 fine for hot paths because lookups inside one frame still\n * amortize. Pass a long-lived cache from `Editor` to share work across\n * frames, hit-test, and overlay.\n */\n readonly boundsCache?: ElementCache<Bounds>;\n /**\n * Pre-built spatial index. When provided together with `viewport`, the\n * renderer picks candidate shapes from the index and skips full layer\n * scans \u2014 pays off around ~10k shapes.\n */\n readonly spatialIndex?: SpatialGrid;\n /**\n * On-screen size thresholds for cheaper render paths. See {@link LodOptions}.\n */\n readonly lod?: LodOptions;\n /**\n * Placeholder fill colour. Defaults to `#bbb`. Pick something close\n * to the average shape colour so the transition is unobtrusive.\n */\n readonly placeholderFill?: string;\n /**\n * Optional dirty rectangle in **world** coords. When set:\n * \u2022 the renderer clears only the corresponding screen region;\n * \u2022 shapes whose world AABB doesn't intersect the dirty rect are\n * skipped entirely.\n * Combined with shape-identity tracking by the host this drops most\n * of the per-frame work for \"single shape moves on otherwise static\n * scene\".\n */\n readonly dirtyWorld?: Bounds;\n /**\n * Shapes to render with reduced alpha (modern-style group isolation).\n * For each shape whose `id` appears in this set, the renderer sets\n * `globalAlpha = dimOpacity` for the per-shape draw pass before\n * dispatching to the registered renderer.\n *\n * Caveat: shapes whose own `style.opacity` is explicitly set will\n * have their renderer call `setOpacity` again and override the\n * dim \u2014 the dim affects only the common case where shapes don't\n * carry an explicit opacity. Acceptable for the isolation UX\n * because outsiders are usually plain opaque shapes.\n */\n readonly dimElements?: ReadonlySet<ElementId>;\n /**\n * Alpha to use for `dimElements`. Default 1 (no-op). Hosts using the\n * isolation feature should pass their `ISOLATION_DIM_OPACITY`\n * constant.\n */\n readonly dimOpacity?: number;\n /**\n * Element ids that should NOT render this pass. The host computes\n * which shapes are effectively hidden (e.g. via group hide\n * propagation) and forwards the set here.\n */\n readonly hideElements?: ReadonlySet<ElementId>;\n /**\n * Per-layer composite bitmap cache. When supplied along with\n * `compositeLayerBitmap`, unchanged layers (i.e. not present in\n * `dirtyLayerIds`) are drawn from a single cached `drawImage` call\n * instead of walking every shape.\n *\n * Pass `dirtyLayerIds` so the renderer knows which layers to\n * re-rasterise. Without it the cache is treated as cold every\n * frame (defensive \u2014 better stale work than a stale visual).\n */\n readonly layerCompositeCache?: LayerCompositeCache;\n readonly dirtyLayerIds?: ReadonlySet<LayerId>;\n /**\n * Host-side layer rasteriser. Receives the layer id, the active\n * zoom bucket, and the scene; returns the bitmap to cache or\n * `null` to opt out. The kernel doesn't ship one \u2014 OffscreenCanvas\n * creation is the backend's job.\n */\n readonly compositeLayerBitmap?: (layerId: LayerId, zoomBucket: number, scene: Scene) => unknown;\n /**\n * Per-instance animated-content playback clock, forwarded to each shape\n * renderer via {@link ElementRenderContext.clock}. Lets the caller (an\n * `Editor`) drive per-shape GIF playback without mutating the process-global\n * {@link setAnimationClock}. Omit to fall back to the module clock.\n */\n readonly clock?: AnimationClock;\n /**\n * Static-export content switches, forwarded to element renderers via\n * the render context (see `ElementRenderContext.content`). Omit for\n * interactive rendering.\n */\n readonly content?: ElementRenderContext[\"content\"];\n /**\n * Hovered element id, forwarded to `ElementRenderContext.hoveredElement`\n * (hover-only chrome like the sticky \"+\" button). Omit when untracked.\n */\n readonly hoveredElement?: string;\n /** Forwarded to `ElementRenderContext.textPlaceholders` (grey prompt in empty text). */\n readonly textPlaceholders?: boolean;\n}\n\n/**\n * Renders the `main` z-stack of a scene onto a single target.\n *\n * Order of operations:\n * 1. Optionally clear the surface.\n * 2. Apply the scene's world-to-screen transform.\n * 3. For each visible layer (bottom \u2192 top): for each shape (bottom \u2192 top):\n * save state, push the shape's local TRS, invoke its registered renderer.\n *\n * This function does not draw edges, selection handles, or grids \u2014 those\n * either live on different layers (`background` / `overlay`) or are added by\n * higher-level packages.\n */\nexport const renderScene = (\n scene: Scene,\n target: RenderTarget,\n options: RenderSceneOptions = {},\n): void => {\n const w2s = getWorldToScreen(scene.viewport);\n const dirtyWorld = options.dirtyWorld;\n if (!options.skipClear) {\n if (dirtyWorld) {\n // Project the dirty rect to screen pixels, inflate by a few\n // pixels to cover anti-aliased stroke fuzz.\n const corners = [\n matrix.applyToPoint(w2s, { x: dirtyWorld.x, y: dirtyWorld.y }),\n matrix.applyToPoint(w2s, {\n x: dirtyWorld.x + dirtyWorld.width,\n y: dirtyWorld.y + dirtyWorld.height,\n }),\n ];\n const screen = B.expand(B.fromPoints(corners), 2);\n target.clear(screen);\n } else {\n target.clear();\n }\n }\n\n target.save();\n target.setTransform(w2s);\n\n const boundsCache = options.boundsCache ?? new ElementCache<Bounds>();\n const viewport = options.viewport;\n // Spatial-index candidate set: when present, restricts the per-layer\n // walk to shapes the index considers possibly-visible. Without it the\n // per-shape AABB check on a cached bounds is still cheap (~50ns), so\n // the index is only worth the build cost for very large scenes.\n let candidates: ReadonlySet<ElementId> | null = null;\n if (viewport && options.spatialIndex) {\n candidates = options.spatialIndex.query(viewport);\n }\n\n const zoom = scene.viewport.zoom;\n const clock = options.clock;\n // Reused per-shape render context. `clock` is per-instance when the caller\n // (Editor) threads one; omitted otherwise so the image renderer falls back\n // to the process-global animation clock.\n const ctx: ElementRenderContext = {\n zoom,\n ...(clock ? { clock } : {}),\n ...(options.content ? { content: options.content } : {}),\n ...(options.hoveredElement !== undefined ? { hoveredElement: options.hoveredElement } : {}),\n ...(options.textPlaceholders === true ? { textPlaceholders: true } : {}),\n };\n const lod = options.lod;\n const placeholderMax = lod?.placeholderMaxScreenPx;\n const placeholderFill = options.placeholderFill ?? DEFAULT_PLACEHOLDER_FILL;\n\n const layerCache = options.layerCompositeCache;\n const dirtyLayers = options.dirtyLayerIds;\n const compositeLayerBitmap = options.compositeLayerBitmap;\n const zoomBucket = bucketFor(zoom);\n const layerBoundsFor = (layerId: LayerId): Bounds | null => {\n let acc: Bounds | null = null;\n for (const shape of getElementsInLayer(scene, layerId)) {\n const bb = cachedWorldBounds(boundsCache, shape);\n acc = acc ? B.union(acc, bb) : bb;\n }\n return acc;\n };\n\n // Dim (isolation / eraser preview) scales the alpha of `dimElements`. Route\n // those shapes through a wrapper that multiplies `setOpacity` by `dimOpacity`\n // \u2014 so a shape carrying its own `style.opacity` renders dimmed too, instead\n // of overwriting the dim back to full. Built once (constant factor), reused.\n const dimOpacity = options.dimOpacity;\n const dimTarget =\n options.dimElements !== undefined && dimOpacity !== undefined\n ? createDimTarget(target, dimOpacity)\n : null;\n\n for (const layer of getLayersInOrder(scene)) {\n if (!layer.visible) continue;\n\n // Per-layer composite cache fast path. Only fires when the host\n // plugged a cache + a layer rasteriser; the kernel ships no default\n // rasteriser (OffscreenCanvas creation is the backend's job). Drop\n // dirty layers from the cache so the bitmap isn't re-used after a\n // mutation.\n if (layerCache && compositeLayerBitmap) {\n if (dirtyLayers?.has(layer.id)) layerCache.invalidateLayer(layer.id);\n let bitmap = layerCache.get(layer.id, zoomBucket);\n if (bitmap === undefined) {\n const fresh = compositeLayerBitmap(layer.id, zoomBucket, scene);\n if (fresh !== null) {\n layerCache.set(layer.id, zoomBucket, fresh);\n bitmap = fresh;\n }\n }\n if (bitmap !== undefined) {\n const bb = layerBoundsFor(layer.id);\n if (bb) target.drawImage(bitmap, bb.x, bb.y, bb.width, bb.height);\n continue;\n }\n }\n\n for (const shape of getElementsInLayer(scene, layer.id)) {\n if (options.hideElements?.has(shape.id)) continue;\n if (candidates && !candidates.has(shape.id)) continue;\n if (viewport) {\n const bb = cachedWorldBounds(boundsCache, shape);\n if (!B.intersects(bb, viewport)) continue;\n }\n if (dirtyWorld) {\n const bb = cachedWorldBounds(boundsCache, shape);\n if (!B.intersects(bb, dirtyWorld)) continue;\n }\n\n // LOD is per element, from what actually lands on screen: unreadable\n // text is skipped, a shape too small to show detail becomes a flat\n // fill \u2014 regardless of the zoom level itself.\n if (isText(shape) && isTextBelowLod(shape.fontSize, zoom, lod)) continue;\n\n if (\n placeholderMax !== undefined &&\n screenSizeOf(cachedWorldBounds(boundsCache, shape), zoom) < placeholderMax\n ) {\n // Draw the AABB directly in world coords \u2014 skip the renderer\n // entirely. The shape's TRS is folded into the cached bounds.\n const bb = cachedWorldBounds(boundsCache, shape);\n target.setFill(placeholderFill);\n target.setStrokeWidth(0);\n target.beginPath();\n target.rect(bb.x, bb.y, bb.width, bb.height);\n target.fill();\n continue;\n }\n\n const renderer = getElementRenderer(shape.type);\n if (!renderer) {\n options.onUnknownElement?.(shape);\n continue;\n }\n\n target.save();\n // Isolation / eraser dim \u2014 draw through the scaling wrapper so the\n // shape's own `style.opacity` multiplies with `dimOpacity` instead of\n // overwriting it (see RenderSceneOptions.dimElements / createDimTarget).\n // The wrapper's base alpha is `dimOpacity` (setOpacity(1) \u2192 dimOpacity),\n // so a shape that never sets its own opacity still dims.\n const dimmed = dimTarget !== null && options.dimElements?.has(shape.id) === true;\n const draw = dimmed ? dimTarget : target;\n if (dimmed) draw.setOpacity(1);\n draw.translate(shape.position.x, shape.position.y);\n if (shape.rotation !== 0) draw.rotate(shape.rotation);\n if (shape.scale.x !== 1 || shape.scale.y !== 1) {\n draw.scale(shape.scale.x, shape.scale.y);\n }\n renderer(shape, draw, ctx);\n // Embedded label \u2014 drawn in the shape's local space, after its\n // body so the text sits on top. Subject to the same readable-text\n // LOD floor as standalone text (checked on the resolved font size).\n if (shape.label !== undefined && !isText(shape)) {\n drawShapeLabel(shape, draw, lod?.minTextScreenPx !== undefined ? { zoom, lod } : undefined);\n }\n target.restore();\n }\n }\n\n target.restore();\n};\n", "/**\n * The fonts the editor ships and draws with \u2014 Roboto (sans), PT Serif\n * (serif) and Roboto Mono (mono). Bundling them means every render backend\n * (Canvas2D, WebGL2/MSDF, the offscreen worker) measures and draws the same\n * glyphs, instead of WebGL2 using the embedded font while Canvas2D falls back\n * to whatever the OS resolves for the requested family.\n */\n\n/** The three bundled font families. */\nexport const FONT_SANS = \"Roboto\";\nexport const FONT_SERIF = \"PT Serif\";\nexport const FONT_MONO = \"Roboto Mono\";\n\n/**\n * Map a CSS font-family stack to the bundled family that backs it. Mirrors\n * the resolution the WASM shaper uses, so Canvas2D and WebGL2 pick the same\n * face: `mono` wins, then `sans` (so `sans-serif` stays sans), then a\n * serif-ish keyword, else sans.\n */\nexport const resolveBundledFamily = (cssFamily: string): string => {\n const f = cssFamily.toLowerCase();\n if (f.includes(\"mono\")) return FONT_MONO;\n if (f.includes(\"sans\")) return FONT_SANS;\n if (f.includes(\"serif\") || f.includes(\"slab\") || f.includes(\"georgia\") || f.includes(\"times\")) {\n return FONT_SERIF;\n }\n return FONT_SANS;\n};\n\ninterface FaceSpec {\n readonly family: string;\n readonly weight: \"400\" | \"700\";\n readonly style: \"normal\" | \"italic\";\n /** Built with a static `new URL(...)` literal so bundlers emit the asset. */\n readonly url: URL;\n}\n\n// Each `new URL` must be a static literal \u2014 a dynamic path (template string)\n// isn't seen by bundler asset pipelines and would 404.\nconst FACES: readonly FaceSpec[] = [\n {\n family: FONT_SANS,\n weight: \"400\",\n style: \"normal\",\n url: new URL(\"../fonts/Roboto-Regular.woff2\", import.meta.url),\n },\n {\n family: FONT_SANS,\n weight: \"700\",\n style: \"normal\",\n url: new URL(\"../fonts/Roboto-Bold.woff2\", import.meta.url),\n },\n {\n family: FONT_SANS,\n weight: \"400\",\n style: \"italic\",\n url: new URL(\"../fonts/Roboto-Italic.woff2\", import.meta.url),\n },\n {\n family: FONT_SANS,\n weight: \"700\",\n style: \"italic\",\n url: new URL(\"../fonts/Roboto-BoldItalic.woff2\", import.meta.url),\n },\n {\n family: FONT_SERIF,\n weight: \"400\",\n style: \"normal\",\n url: new URL(\"../fonts/PTSerif-Regular.woff2\", import.meta.url),\n },\n {\n family: FONT_SERIF,\n weight: \"700\",\n style: \"normal\",\n url: new URL(\"../fonts/PTSerif-Bold.woff2\", import.meta.url),\n },\n {\n family: FONT_SERIF,\n weight: \"400\",\n style: \"italic\",\n url: new URL(\"../fonts/PTSerif-Italic.woff2\", import.meta.url),\n },\n {\n family: FONT_SERIF,\n weight: \"700\",\n style: \"italic\",\n url: new URL(\"../fonts/PTSerif-BoldItalic.woff2\", import.meta.url),\n },\n {\n family: FONT_MONO,\n weight: \"400\",\n style: \"normal\",\n url: new URL(\"../fonts/RobotoMono-Regular.woff2\", import.meta.url),\n },\n {\n family: FONT_MONO,\n weight: \"700\",\n style: \"normal\",\n url: new URL(\"../fonts/RobotoMono-Bold.woff2\", import.meta.url),\n },\n {\n family: FONT_MONO,\n weight: \"400\",\n style: \"italic\",\n url: new URL(\"../fonts/RobotoMono-Italic.woff2\", import.meta.url),\n },\n {\n family: FONT_MONO,\n weight: \"700\",\n style: \"italic\",\n url: new URL(\"../fonts/RobotoMono-BoldItalic.woff2\", import.meta.url),\n },\n];\n\nexport interface FontScope {\n readonly fonts?: {\n add(font: FontFace): void;\n has(font: FontFace): boolean;\n };\n}\n\n/**\n * Load and register the bundled fonts into a scope's font set \u2014 pass the\n * `window` on the main thread and the worker's `self` inside a render worker\n * (both expose `.fonts`). Idempotent and resolves once every face is ready,\n * so callers can render crisp text after it settles. A no-op where the\n * `FontFace` API is unavailable (older runtimes / SSR).\n */\nexport const registerBundledFonts = async (\n scope: FontScope = globalThis as FontScope,\n): Promise<void> => {\n const set = scope.fonts;\n if (!set || typeof FontFace === \"undefined\") return;\n // `allSettled` so one missing face doesn't block the rest from loading.\n await Promise.allSettled(\n FACES.map(async (f) => {\n const face = new FontFace(f.family, `url(${f.url.href})`, {\n weight: f.weight,\n style: f.style,\n });\n await face.load();\n set.add(face);\n }),\n );\n};\n", "// Single implementation lives in renderer-core (element renderers need it\n// too, to distinguish \"rehydration pending\" from \"permanently broken\");\n// re-exported here for this package's backends.\nexport { isDrawableImageSource } from \"@oh-just-another/renderer-core\";\n\n/**\n * Intrinsic pixel size of a drawable image source, or `null` when it can't be\n * determined. Handles the differing width/height accessors of the DOM image\n * types (`naturalWidth` for `<img>`, `videoWidth` for `<video>`, plain\n * `width`/`height` for bitmaps / canvases). Needed to turn a normalised crop\n * (fractions) into a pixel source rectangle for `ctx.drawImage`.\n */\nexport const intrinsicImageSize = (\n source: CanvasImageSource,\n): { readonly width: number; readonly height: number } | null => {\n const s = source as {\n naturalWidth?: number;\n naturalHeight?: number;\n videoWidth?: number;\n videoHeight?: number;\n width?: number | { baseVal?: unknown };\n height?: number | { baseVal?: unknown };\n };\n if (typeof s.naturalWidth === \"number\" && s.naturalWidth > 0) {\n return { width: s.naturalWidth, height: s.naturalHeight ?? s.naturalWidth };\n }\n if (typeof s.videoWidth === \"number\" && s.videoWidth > 0) {\n return { width: s.videoWidth, height: s.videoHeight ?? s.videoWidth };\n }\n if (typeof s.width === \"number\" && s.width > 0 && typeof s.height === \"number\") {\n return { width: s.width, height: s.height };\n }\n return null;\n};\n\n/**\n * Warn (once per distinct kind) when an image draw is skipped because\n * the handle isn't drawable. Throttled by a module-level `Set` so a\n * per-frame render loop doesn't spam the console \u2014 but the host still\n * sees that an image failed to render and the likely cause.\n */\nconst warnedImageKinds = new Set<string>();\n\nexport const warnSkippedImage = (value: unknown): void => {\n if (typeof console === \"undefined\") return;\n const kind =\n typeof value === \"string\"\n ? value.startsWith(\"blob:\")\n ? \"dead-blob-url\"\n : \"string-src\"\n : value === null || value === undefined\n ? \"empty\"\n : \"stale-object\"; // e.g. a {} from a serialised <img>\n if (warnedImageKinds.has(kind)) return;\n warnedImageKinds.add(kind);\n\n console.warn(\n `[renderer] skipped a non-drawable image source (kind: ${kind}). ` +\n \"The shape's image handle isn't a live HTMLImageElement / canvas / \" +\n \"bitmap and it has no Scene.files bytes to rehydrate from (shapes \" +\n \"with a fileId are skipped silently while rehydration is in flight) \u2014 \" +\n \"the image will stay blank.\",\n );\n};\n", "import type { Bounds, Transform } from \"@oh-just-another/types\";\nimport type {\n FillRule,\n LineCap,\n LineJoin,\n RenderTarget,\n TextAlign,\n TextBaseline,\n} from \"@oh-just-another/renderer-core\";\nimport { resolveBundledFamily } from \"@oh-just-another/fonts\";\nimport { intrinsicImageSize, isDrawableImageSource, warnSkippedImage } from \"./image-source.js\";\n\n/**\n * Wraps a `CanvasRenderingContext2D` (or compatible OffscreenCanvas context)\n * as a backend-agnostic `RenderTarget`. Coordinates passed to the target are\n * in CSS pixels; the device-pixel scaling is applied once at construction by\n * the device-pixel-ratio (DPR) helper, so all draw calls see CSS units.\n *\n * `size` reports the CSS-pixel size that draw calls operate in. The underlying\n * canvas bitmap may be larger (DPR \u00D7 size) but that is transparent here.\n */\nexport class Canvas2DTarget implements RenderTarget {\n private readonly ctx: CanvasRenderingContext2D;\n private _width: number;\n private _height: number;\n /**\n * Device-pixel-ratio the canvas bitmap is scaled by (see `setupHiDpi`).\n * `setTransform` / `resetTransform` take a transform that maps world \u2192\n * CSS pixels; they pre-multiply by `scale(dpr)` so the result lands in\n * the DPR-scaled device buffer.\n */\n private dpr: number;\n\n /**\n * `width` / `height` are CSS-pixel dimensions. `dpr` must match the value\n * `setupHiDpi` used to scale the bitmap (default 1). The constructor assumes\n * the caller has already configured the canvas bitmap + context transform.\n */\n constructor(ctx: CanvasRenderingContext2D, width: number, height: number, dpr = 1) {\n this.ctx = ctx;\n this._width = width;\n this._height = height;\n this.dpr = dpr;\n }\n\n get size(): { readonly width: number; readonly height: number } {\n return { width: this._width, height: this._height };\n }\n\n /** Mutator for callers that resize the canvas. `dpr` updates the device\n * scale when the canvas moves to a different-density display. */\n resize(width: number, height: number, dpr?: number): void {\n this._width = width;\n this._height = height;\n if (dpr !== undefined) this.dpr = dpr;\n }\n\n // --- Style ---\n\n setFill(color: string | null): void {\n this.ctx.fillStyle = color ?? \"transparent\";\n }\n setStroke(color: string | null): void {\n this.ctx.strokeStyle = color ?? \"transparent\";\n }\n setStrokeWidth(width: number): void {\n this.ctx.lineWidth = width;\n }\n setOpacity(alpha: number): void {\n this.ctx.globalAlpha = alpha;\n }\n setLineCap(cap: LineCap): void {\n this.ctx.lineCap = cap;\n }\n setLineJoin(join: LineJoin): void {\n this.ctx.lineJoin = join;\n }\n setDashArray(dash: readonly number[] | null): void {\n this.ctx.setLineDash(dash ? [...dash] : []);\n }\n setFont(\n fontFamily: string,\n fontSize: number,\n options?: { weight?: \"normal\" | \"bold\"; style?: \"normal\" | \"italic\" },\n ): void {\n // CSS font shorthand order: `<style> <weight> <size> <family>`. Draw with\n // the bundled face that backs the requested family (matching the WebGL2\n // MSDF path), falling back to the original stack until it has loaded.\n const style = options?.style === \"italic\" ? \"italic \" : \"\";\n const weight = options?.weight === \"bold\" ? \"bold \" : \"\";\n this.ctx.font = `${style}${weight}${fontSize}px \"${resolveBundledFamily(fontFamily)}\", ${fontFamily}`;\n }\n setTextAlign(align: TextAlign): void {\n this.ctx.textAlign = align === \"center\" ? \"center\" : align;\n }\n setTextBaseline(baseline: TextBaseline): void {\n this.ctx.textBaseline =\n baseline === \"middle\" ? \"middle\" : baseline === \"top\" ? \"top\" : \"bottom\";\n }\n\n // --- State stack ---\n\n save(): void {\n this.ctx.save();\n }\n restore(): void {\n this.ctx.restore();\n }\n\n // --- Transform ---\n\n translate(dx: number, dy: number): void {\n this.ctx.translate(dx, dy);\n }\n rotate(radians: number): void {\n this.ctx.rotate(radians);\n }\n scale(sx: number, sy: number): void {\n this.ctx.scale(sx, sy);\n }\n setTransform(t: Transform): void {\n // Compose with the DPR base: device = scale(dpr) \u00B7 t. `t` maps world \u2192\n // CSS px; the bitmap is dpr\u00D7 bigger, so every coordinate scales by dpr.\n const d = this.dpr;\n this.ctx.setTransform(d * t.a, d * t.b, d * t.c, d * t.d, d * t.e, d * t.f);\n }\n resetTransform(): void {\n // Reset to the DPR base (NOT raw identity) so CSS-px draws stay scaled.\n this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);\n }\n\n // --- Path primitives ---\n\n beginPath(): void {\n this.ctx.beginPath();\n }\n closePath(): void {\n this.ctx.closePath();\n }\n moveTo(x: number, y: number): void {\n this.ctx.moveTo(x, y);\n }\n lineTo(x: number, y: number): void {\n this.ctx.lineTo(x, y);\n }\n quadraticCurveTo(cx: number, cy: number, x: number, y: number): void {\n this.ctx.quadraticCurveTo(cx, cy, x, y);\n }\n bezierCurveTo(c1x: number, c1y: number, c2x: number, c2y: number, x: number, y: number): void {\n this.ctx.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);\n }\n rect(x: number, y: number, width: number, height: number): void {\n this.ctx.rect(x, y, width, height);\n }\n ellipse(cx: number, cy: number, rx: number, ry: number): void {\n this.ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);\n }\n\n // --- Fill / stroke ---\n\n fill(rule?: FillRule): void {\n this.ctx.fill(rule);\n }\n stroke(): void {\n this.ctx.stroke();\n }\n clip(rule?: FillRule): void {\n this.ctx.clip(rule);\n }\n\n // --- Text ---\n\n fillText(text: string, x: number, y: number, maxWidth?: number): void {\n if (maxWidth !== undefined) this.ctx.fillText(text, x, y, maxWidth);\n else this.ctx.fillText(text, x, y);\n }\n measureText(text: string): { width: number } {\n const m = this.ctx.measureText(text);\n return { width: m.width };\n }\n\n // --- Images ---\n\n drawImage(\n image: unknown,\n dx: number,\n dy: number,\n dw: number,\n dh: number,\n _dynamic?: boolean,\n crop?: {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n },\n ): void {\n // `_dynamic` ignored \u2014 Canvas2D reads the source element live on\n // every drawImage, so animated GIF / video frames are picked up\n // automatically as long as the host re-renders.\n void _dynamic;\n // Guard against non-drawable handles. A restored scene carries\n // either a string `src` (dead blob: URL) OR a `metadata.image`\n // that serialised to `{}` (a live `<img>` becomes an empty object\n // through JSON). Both throw inside `ctx.drawImage`. Skip rather\n // than crash the whole render pass, and surface it once so hosts\n // know an image didn't render (and why).\n if (!isDrawableImageSource(image)) {\n warnSkippedImage(image);\n return;\n }\n // Cropped draw: map the normalised source rect to pixels and use the\n // 9-argument form. Falls back to the whole image when the intrinsic\n // size is unknown (crop can't be resolved).\n if (crop && (crop.x !== 0 || crop.y !== 0 || crop.width !== 1 || crop.height !== 1)) {\n const size = intrinsicImageSize(image);\n if (size) {\n this.ctx.drawImage(\n image,\n crop.x * size.width,\n crop.y * size.height,\n crop.width * size.width,\n crop.height * size.height,\n dx,\n dy,\n dw,\n dh,\n );\n return;\n }\n }\n this.ctx.drawImage(image, dx, dy, dw, dh);\n }\n\n // --- Surface control ---\n\n clear(bounds?: Bounds): void {\n // A `clear()` always opens a fresh dirty pass \u2014 the host took\n // responsibility for the cleared region, anything we accumulate\n // from here is the new frame's coverage.\n this.dirtyRect = null;\n if (bounds) {\n this.ctx.clearRect(bounds.x, bounds.y, bounds.width, bounds.height);\n } else {\n // Clear the entire CSS-space area, regardless of current transform.\n this.ctx.save();\n this.ctx.setTransform(1, 0, 0, 1, 0, 0);\n this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);\n this.ctx.restore();\n }\n }\n\n // --- Per-pass dirty accumulator ---\n\n /**\n * Screen-space union of every `markDirty(bounds)` call since the\n * last `clear()`. Hosts can read it via `getDirtyRect()` to size\n * the next clear precisely \u2014 covers anti-aliased stroke fuzz and\n * shape renderers that paint a few px beyond their geometric bbox.\n */\n private dirtyRect: Bounds | null = null;\n\n markDirty(bounds: Bounds): void {\n if (!this.dirtyRect) {\n this.dirtyRect = bounds;\n return;\n }\n const minX = Math.min(this.dirtyRect.x, bounds.x);\n const minY = Math.min(this.dirtyRect.y, bounds.y);\n const maxX = Math.max(this.dirtyRect.x + this.dirtyRect.width, bounds.x + bounds.width);\n const maxY = Math.max(this.dirtyRect.y + this.dirtyRect.height, bounds.y + bounds.height);\n this.dirtyRect = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n }\n\n /** Read the accumulated dirty rect for this pass. `null` when nothing painted. */\n getDirtyRect(): Bounds | null {\n return this.dirtyRect;\n }\n}\n", "import type { Bounds } from \"@oh-just-another/types\";\nimport {\n LruCache,\n type FillRule,\n type LineCap,\n type LineJoin,\n type RenderTarget,\n type TextAlign,\n type TextBaseline,\n} from \"@oh-just-another/renderer-core\";\nimport { OFFSCREEN_IMAGE_CACHE_CAP } from \"../constants.js\";\nimport type { RenderCommand } from \"./recording-target.js\";\n\n/**\n * Packed-frame codec for the offscreen backend's per-frame worker hop.\n *\n * `structuredClone`-ing an array of {@link RenderCommand} objects costs\n * ~1.6 ms for a ~4.5k-command frame (see `tests/offscreen-transfer.bench.ts`)\n * \u2014 every object, key, and string is walked and copied. This codec flattens\n * the stream into one transferable `ArrayBuffer` of Float64 words (opcode +\n * args per command) plus a per-frame deduplicated string table, so\n * `postMessage` transfers the numeric bulk for free and only clones a small\n * string array. `ImageBitmap` payloads (`defineImage`) travel in a side\n * array \u2014 see {@link packReplayFrame}.\n */\n\n/**\n * Opcodes for the packed numeric stream \u2014 one per {@link RenderCommand}\n * variant except `defineImage`, which travels in the side bitmap array and\n * emits nothing here. Wire format only: values are arbitrary but must stay\n * in sync between {@link packReplayFrame} and {@link replayPackedFrame}\n * (same-package protocol; both sides always ship together).\n */\nconst OP_SET_FILL = 0;\nconst OP_SET_STROKE = 1;\nconst OP_SET_STROKE_WIDTH = 2;\nconst OP_SET_OPACITY = 3;\nconst OP_SET_LINE_CAP = 4;\nconst OP_SET_LINE_JOIN = 5;\nconst OP_SET_DASH_ARRAY = 6;\nconst OP_SET_FONT = 7;\nconst OP_SET_TEXT_ALIGN = 8;\nconst OP_SET_TEXT_BASELINE = 9;\nconst OP_SAVE = 10;\nconst OP_RESTORE = 11;\nconst OP_TRANSLATE = 12;\nconst OP_ROTATE = 13;\nconst OP_SCALE = 14;\nconst OP_SET_TRANSFORM = 15;\nconst OP_RESET_TRANSFORM = 16;\nconst OP_BEGIN_PATH = 17;\nconst OP_CLOSE_PATH = 18;\nconst OP_MOVE_TO = 19;\nconst OP_LINE_TO = 20;\nconst OP_QUADRATIC_CURVE_TO = 21;\nconst OP_BEZIER_CURVE_TO = 22;\nconst OP_RECT = 23;\nconst OP_ELLIPSE = 24;\nconst OP_FILL = 25;\nconst OP_STROKE = 26;\nconst OP_FILL_TEXT = 27;\nconst OP_CLEAR = 28;\nconst OP_MARK_DIRTY = 29;\nconst OP_RESIZE = 30;\nconst OP_DRAW_IMAGE = 31;\nconst OP_CLIP = 32;\n\n/**\n * String-table index sentinel for a `null` color (`setFill` / `setStroke`\n * accept `Color | null`). Real indices are >= 0.\n */\nconst NULL_STRING_INDEX = -1;\n\n/** `setDashArray(null)` marker written in place of the dash length. */\nconst NULL_DASH_LENGTH = -1;\n\n/**\n * Enum wire codes. Encode side uses the `*_CODE` records, decode side the\n * positional arrays \u2014 index === code. Order is wire format; append only.\n */\nconst LINE_CAPS: readonly LineCap[] = [\"butt\", \"round\", \"square\"];\nconst LINE_CAP_CODE: Record<LineCap, number> = { butt: 0, round: 1, square: 2 };\nconst LINE_JOINS: readonly LineJoin[] = [\"miter\", \"round\", \"bevel\"];\nconst LINE_JOIN_CODE: Record<LineJoin, number> = { miter: 0, round: 1, bevel: 2 };\nconst TEXT_ALIGNS: readonly TextAlign[] = [\"left\", \"center\", \"right\"];\nconst TEXT_ALIGN_CODE: Record<TextAlign, number> = { left: 0, center: 1, right: 2 };\nconst TEXT_BASELINES: readonly TextBaseline[] = [\"top\", \"middle\", \"bottom\"];\nconst TEXT_BASELINE_CODE: Record<TextBaseline, number> = { top: 0, middle: 1, bottom: 2 };\n\n/**\n * `fill(rule?)` wire codes: 0 = no rule argument, 1 = \"nonzero\",\n * 2 = \"evenodd\".\n */\nconst FILL_RULE_CODE: Record<FillRule, number> = { nonzero: 1, evenodd: 2 };\n\n/**\n * `setFont` options wire codes. 0 = key absent from the options object;\n * 1 / 2 = the two allowed values. A separate leading flag word (0 / 1)\n * distinguishes \"no options argument at all\" from an empty options object.\n */\nconst FONT_WEIGHT_CODE: Record<\"normal\" | \"bold\", number> = { normal: 1, bold: 2 };\nconst FONT_STYLE_CODE: Record<\"normal\" | \"italic\", number> = { normal: 1, italic: 2 };\n\n/** Optional-argument presence flags (`fillText` maxWidth, `clear` bounds). */\nconst ABSENT = 0;\nconst PRESENT = 1;\n\n/**\n * Initial capacity (in Float64 words) of the packed stream, as a multiple\n * of the command count. Most commands fit in opcode + \u22646 args; the writer\n * doubles on overflow, so this only tunes how often the first frames\n * reallocate.\n */\nconst PACK_WORDS_PER_COMMAND = 4;\n\n/** Floor for the writer's initial capacity so tiny frames don't thrash. */\nconst PACK_MIN_CAPACITY = 64;\n\n/** One `defineImage` payload carried alongside the numeric stream. */\nexport interface PackedFrameBitmap {\n readonly id: number;\n readonly bitmap: ImageBitmap;\n}\n\n/**\n * Result of {@link packReplayFrame}: `buffer` is the transferable numeric\n * stream, `strings` the per-frame deduplicated string table it indexes\n * into, `bitmaps` the `defineImage` payloads (worker registers them BEFORE\n * replaying the stream).\n */\nexport interface PackedReplayFrame {\n readonly buffer: ArrayBuffer;\n readonly strings: readonly string[];\n readonly bitmaps: readonly PackedFrameBitmap[];\n}\n\n/**\n * postMessage shape the offscreen surface posts per changed layer and the\n * render worker consumes. `buffer` goes in the transfer list; `strings`\n * are cloned (cheap \u2014 deduplicated); `bitmaps` are CLONED, never\n * transferred \u2014 see {@link packReplayFrame}.\n */\nexport interface PackedReplayMessage {\n readonly type: \"replay\";\n readonly buffer: ArrayBuffer;\n readonly strings: readonly string[];\n readonly bitmaps: readonly PackedFrameBitmap[];\n}\n\n/**\n * Flatten a flushed {@link RenderCommand} buffer into a transferable packed\n * frame: one Float64 word per opcode / argument, strings deduplicated into\n * a side table, enums and presence flags as small ints.\n *\n * `defineImage` commands emit nothing into the numeric stream; their\n * `{ id, bitmap }` payloads are collected into `bitmaps` instead. The\n * caller must post them WITHOUT a transfer-list entry so `postMessage`\n * clones the pixels: the recorder's intern LRU still owns the source\n * bitmap and will keep drawing it on later frames (GIF / video), so\n * transferring (detaching) it would break the main thread's copy.\n */\nexport const packReplayFrame = (commands: readonly RenderCommand[]): PackedReplayFrame => {\n let words = new Float64Array(\n Math.max(PACK_MIN_CAPACITY, commands.length * PACK_WORDS_PER_COMMAND),\n );\n let used = 0;\n const push = (v: number): void => {\n if (used === words.length) {\n const grown = new Float64Array(words.length * 2);\n grown.set(words);\n words = grown;\n }\n words[used++] = v;\n };\n\n const strings: string[] = [];\n const stringIndex = new Map<string, number>();\n /** Dedup a string through the per-frame table, returning its index. */\n const intern = (s: string): number => {\n let idx = stringIndex.get(s);\n if (idx === undefined) {\n idx = strings.length;\n strings.push(s);\n stringIndex.set(s, idx);\n }\n return idx;\n };\n\n const bitmaps: PackedFrameBitmap[] = [];\n\n for (const cmd of commands) {\n switch (cmd.k) {\n case \"setFill\":\n push(OP_SET_FILL);\n push(cmd.color === null ? NULL_STRING_INDEX : intern(cmd.color));\n break;\n case \"setStroke\":\n push(OP_SET_STROKE);\n push(cmd.color === null ? NULL_STRING_INDEX : intern(cmd.color));\n break;\n case \"setStrokeWidth\":\n push(OP_SET_STROKE_WIDTH);\n push(cmd.w);\n break;\n case \"setOpacity\":\n push(OP_SET_OPACITY);\n push(cmd.a);\n break;\n case \"setLineCap\":\n push(OP_SET_LINE_CAP);\n push(LINE_CAP_CODE[cmd.cap]);\n break;\n case \"setLineJoin\":\n push(OP_SET_LINE_JOIN);\n push(LINE_JOIN_CODE[cmd.join]);\n break;\n case \"setDashArray\":\n push(OP_SET_DASH_ARRAY);\n if (cmd.dash === null) {\n push(NULL_DASH_LENGTH);\n } else {\n push(cmd.dash.length);\n for (const d of cmd.dash) push(d);\n }\n break;\n case \"setFont\":\n push(OP_SET_FONT);\n push(intern(cmd.family));\n push(cmd.size);\n if (cmd.options === undefined) {\n push(ABSENT);\n } else {\n push(PRESENT);\n push(cmd.options.weight === undefined ? 0 : FONT_WEIGHT_CODE[cmd.options.weight]);\n push(cmd.options.style === undefined ? 0 : FONT_STYLE_CODE[cmd.options.style]);\n }\n break;\n case \"setTextAlign\":\n push(OP_SET_TEXT_ALIGN);\n push(TEXT_ALIGN_CODE[cmd.align]);\n break;\n case \"setTextBaseline\":\n push(OP_SET_TEXT_BASELINE);\n push(TEXT_BASELINE_CODE[cmd.baseline]);\n break;\n case \"save\":\n push(OP_SAVE);\n break;\n case \"restore\":\n push(OP_RESTORE);\n break;\n case \"translate\":\n push(OP_TRANSLATE);\n push(cmd.x);\n push(cmd.y);\n break;\n case \"rotate\":\n push(OP_ROTATE);\n push(cmd.r);\n break;\n case \"scale\":\n push(OP_SCALE);\n push(cmd.sx);\n push(cmd.sy);\n break;\n case \"setTransform\":\n push(OP_SET_TRANSFORM);\n push(cmd.t.a);\n push(cmd.t.b);\n push(cmd.t.c);\n push(cmd.t.d);\n push(cmd.t.e);\n push(cmd.t.f);\n break;\n case \"resetTransform\":\n push(OP_RESET_TRANSFORM);\n break;\n case \"beginPath\":\n push(OP_BEGIN_PATH);\n break;\n case \"closePath\":\n push(OP_CLOSE_PATH);\n break;\n case \"moveTo\":\n push(OP_MOVE_TO);\n push(cmd.x);\n push(cmd.y);\n break;\n case \"lineTo\":\n push(OP_LINE_TO);\n push(cmd.x);\n push(cmd.y);\n break;\n case \"quadraticCurveTo\":\n push(OP_QUADRATIC_CURVE_TO);\n push(cmd.cx);\n push(cmd.cy);\n push(cmd.x);\n push(cmd.y);\n break;\n case \"bezierCurveTo\":\n push(OP_BEZIER_CURVE_TO);\n push(cmd.c1x);\n push(cmd.c1y);\n push(cmd.c2x);\n push(cmd.c2y);\n push(cmd.x);\n push(cmd.y);\n break;\n case \"rect\":\n push(OP_RECT);\n push(cmd.x);\n push(cmd.y);\n push(cmd.w);\n push(cmd.h);\n break;\n case \"ellipse\":\n push(OP_ELLIPSE);\n push(cmd.cx);\n push(cmd.cy);\n push(cmd.rx);\n push(cmd.ry);\n break;\n case \"fill\":\n push(OP_FILL);\n push(cmd.rule === undefined ? ABSENT : FILL_RULE_CODE[cmd.rule]);\n break;\n case \"clip\":\n push(OP_CLIP);\n push(cmd.rule === undefined ? ABSENT : FILL_RULE_CODE[cmd.rule]);\n break;\n case \"stroke\":\n push(OP_STROKE);\n break;\n case \"fillText\":\n push(OP_FILL_TEXT);\n push(intern(cmd.text));\n push(cmd.x);\n push(cmd.y);\n if (cmd.maxWidth === undefined) {\n push(ABSENT);\n } else {\n push(PRESENT);\n push(cmd.maxWidth);\n }\n break;\n case \"clear\":\n push(OP_CLEAR);\n if (cmd.bounds === undefined) {\n push(ABSENT);\n } else {\n push(PRESENT);\n push(cmd.bounds.x);\n push(cmd.bounds.y);\n push(cmd.bounds.width);\n push(cmd.bounds.height);\n }\n break;\n case \"markDirty\":\n push(OP_MARK_DIRTY);\n push(cmd.bounds.x);\n push(cmd.bounds.y);\n push(cmd.bounds.width);\n push(cmd.bounds.height);\n break;\n case \"resize\":\n push(OP_RESIZE);\n push(cmd.w);\n push(cmd.h);\n break;\n case \"defineImage\":\n // Not packed: the bitmap travels beside the numeric stream. The\n // worker registers all side bitmaps before replaying, so the\n // stream's `drawImage` id references always resolve.\n bitmaps.push({ id: cmd.id, bitmap: cmd.bitmap });\n break;\n case \"drawImage\":\n push(OP_DRAW_IMAGE);\n push(cmd.id);\n push(cmd.dx);\n push(cmd.dy);\n push(cmd.dw);\n push(cmd.dh);\n break;\n }\n }\n\n // Exact-size copy so the transferred buffer carries no slack capacity.\n return { buffer: words.slice(0, used).buffer, strings, bitmaps };\n};\n\n/**\n * Decode a packed frame and dispatch each command straight onto `target`\n * in one pass \u2014 no intermediate {@link RenderCommand} objects.\n *\n * `images` is the worker's persistent id \u2192 bitmap LRU (mirrors the\n * recorder's same-capacity intern LRU): the caller must have registered\n * the frame's side bitmaps into it BEFORE calling. Semantics match\n * {@link replayCommands}: `resize` is a no-op (the worker owns the canvas\n * size via its own `resize` message) and a `drawImage` whose id misses the\n * cache is skipped rather than thrown.\n */\nexport const replayPackedFrame = (\n target: RenderTarget,\n buffer: ArrayBuffer,\n strings: readonly string[],\n images: LruCache<number, ImageBitmap> = new LruCache(OFFSCREEN_IMAGE_CACHE_CAP),\n): void => {\n const words = new Float64Array(buffer);\n let i = 0;\n const next = (): number => {\n const v = words[i++];\n if (v === undefined) throw new Error(\"replayPackedFrame: truncated stream\");\n return v;\n };\n const str = (idx: number): string => {\n const s = strings[idx];\n if (s === undefined) throw new Error(`replayPackedFrame: bad string index ${String(idx)}`);\n return s;\n };\n const at = <T>(table: readonly T[], code: number): T => {\n const v = table[code];\n if (v === undefined) throw new Error(`replayPackedFrame: bad enum code ${String(code)}`);\n return v;\n };\n\n while (i < words.length) {\n const op = next();\n switch (op) {\n case OP_SET_FILL: {\n const idx = next();\n target.setFill(idx === NULL_STRING_INDEX ? null : str(idx));\n break;\n }\n case OP_SET_STROKE: {\n const idx = next();\n target.setStroke(idx === NULL_STRING_INDEX ? null : str(idx));\n break;\n }\n case OP_SET_STROKE_WIDTH:\n target.setStrokeWidth(next());\n break;\n case OP_SET_OPACITY:\n target.setOpacity(next());\n break;\n case OP_SET_LINE_CAP:\n target.setLineCap(at(LINE_CAPS, next()));\n break;\n case OP_SET_LINE_JOIN:\n target.setLineJoin(at(LINE_JOINS, next()));\n break;\n case OP_SET_DASH_ARRAY: {\n const n = next();\n if (n === NULL_DASH_LENGTH) {\n target.setDashArray(null);\n } else {\n const dash: number[] = [];\n for (let d = 0; d < n; d++) dash.push(next());\n target.setDashArray(dash);\n }\n break;\n }\n case OP_SET_FONT: {\n const family = str(next());\n const size = next();\n if (next() === ABSENT) {\n target.setFont(family, size);\n break;\n }\n const weight = next();\n const style = next();\n const options: { weight?: \"normal\" | \"bold\"; style?: \"normal\" | \"italic\" } = {};\n if (weight === FONT_WEIGHT_CODE.normal) options.weight = \"normal\";\n else if (weight === FONT_WEIGHT_CODE.bold) options.weight = \"bold\";\n if (style === FONT_STYLE_CODE.normal) options.style = \"normal\";\n else if (style === FONT_STYLE_CODE.italic) options.style = \"italic\";\n target.setFont(family, size, options);\n break;\n }\n case OP_SET_TEXT_ALIGN:\n target.setTextAlign(at(TEXT_ALIGNS, next()));\n break;\n case OP_SET_TEXT_BASELINE:\n target.setTextBaseline(at(TEXT_BASELINES, next()));\n break;\n case OP_SAVE:\n target.save();\n break;\n case OP_RESTORE:\n target.restore();\n break;\n case OP_TRANSLATE:\n target.translate(next(), next());\n break;\n case OP_ROTATE:\n target.rotate(next());\n break;\n case OP_SCALE:\n target.scale(next(), next());\n break;\n case OP_SET_TRANSFORM:\n target.setTransform({\n a: next(),\n b: next(),\n c: next(),\n d: next(),\n e: next(),\n f: next(),\n });\n break;\n case OP_RESET_TRANSFORM:\n target.resetTransform();\n break;\n case OP_BEGIN_PATH:\n target.beginPath();\n break;\n case OP_CLOSE_PATH:\n target.closePath();\n break;\n case OP_MOVE_TO:\n target.moveTo(next(), next());\n break;\n case OP_LINE_TO:\n target.lineTo(next(), next());\n break;\n case OP_QUADRATIC_CURVE_TO:\n target.quadraticCurveTo(next(), next(), next(), next());\n break;\n case OP_BEZIER_CURVE_TO:\n target.bezierCurveTo(next(), next(), next(), next(), next(), next());\n break;\n case OP_RECT:\n target.rect(next(), next(), next(), next());\n break;\n case OP_ELLIPSE:\n target.ellipse(next(), next(), next(), next());\n break;\n case OP_FILL: {\n const code = next();\n if (code === FILL_RULE_CODE.nonzero) target.fill(\"nonzero\");\n else if (code === FILL_RULE_CODE.evenodd) target.fill(\"evenodd\");\n else target.fill();\n break;\n }\n case OP_STROKE:\n target.stroke();\n break;\n case OP_CLIP: {\n const code = next();\n if (code === FILL_RULE_CODE.nonzero) target.clip(\"nonzero\");\n else if (code === FILL_RULE_CODE.evenodd) target.clip(\"evenodd\");\n else target.clip();\n break;\n }\n case OP_FILL_TEXT: {\n const text = str(next());\n const x = next();\n const y = next();\n if (next() === PRESENT) target.fillText(text, x, y, next());\n else target.fillText(text, x, y);\n break;\n }\n case OP_CLEAR:\n if (next() === PRESENT) {\n const bounds: Bounds = { x: next(), y: next(), width: next(), height: next() };\n target.clear(bounds);\n } else {\n target.clear();\n }\n break;\n case OP_MARK_DIRTY:\n target.markDirty?.({ x: next(), y: next(), width: next(), height: next() });\n break;\n case OP_RESIZE:\n // No-op for replay \u2014 the worker owns the canvas size and resizes\n // via its own `resize` message, not via the command stream. Still\n // consume the args to stay in sync with the stream.\n next();\n next();\n break;\n case OP_DRAW_IMAGE: {\n // `get` bumps recency so the worker LRU evicts in lockstep with\n // the recorder's. A miss means an out-of-sync stream \u2014 skip\n // rather than throw (matches the non-drawable skip on record).\n const id = next();\n const dx = next();\n const dy = next();\n const dw = next();\n const dh = next();\n const bitmap = images.get(id);\n if (bitmap) target.drawImage(bitmap, dx, dy, dw, dh);\n break;\n }\n default:\n throw new Error(`replayPackedFrame: unknown opcode ${String(op)}`);\n }\n }\n};\n", "/// <reference lib=\"webworker\" />\nimport { LruCache, installBuiltinRenderers, renderScene } from \"@oh-just-another/renderer-core\";\nimport type { Scene } from \"@oh-just-another/scene\";\nimport type { WorkerRenderMessage, WorkerRenderResponse } from \"@oh-just-another/renderer-core\";\nimport { registerBundledFonts, type FontScope } from \"@oh-just-another/fonts\";\nimport { Canvas2DTarget } from \"./canvas2d/canvas-target.js\";\nimport { replayPackedFrame, type PackedReplayMessage } from \"./offscreen/replay-codec.js\";\nimport { OFFSCREEN_IMAGE_CACHE_CAP } from \"./constants.js\";\n\n/**\n * OffscreenCanvas render worker.\n *\n * Hosts spawn this with `new Worker(new URL(\"./render-worker.ts\",\n * import.meta.url), { type: \"module\" })`. The main thread transfers a\n * canvas via `transferCanvasToWorker(canvas, worker, { width, height,\n * dpr })`, then posts `snapshot` messages with full scenes.\n *\n * One worker owns one OffscreenCanvas \u2014 typically one per layer in a\n * `LayerWorkerPool`, so layers can be rasterised in parallel and then\n * composited on the main thread.\n */\n\ninterface WorkerState {\n canvas: OffscreenCanvas | null;\n target: Canvas2DTarget | null;\n dpr: number;\n /**\n * Bitmaps shipped by the main thread's RecordingTarget, keyed by the\n * id it assigned. Persists across `replay` messages and mirrors the\n * recorder's same-capacity LRU. Evicted clones are closed to release\n * their memory promptly (these are worker-owned copies, distinct from\n * the main thread's source bitmaps).\n */\n readonly images: LruCache<number, ImageBitmap>;\n}\n\nconst state: WorkerState = {\n canvas: null,\n target: null,\n dpr: 1,\n images: new LruCache<number, ImageBitmap>(OFFSCREEN_IMAGE_CACHE_CAP, (_id, bitmap) => {\n bitmap.close();\n }),\n};\n\nlet renderersInstalled = false;\n\nconst ensureRenderers = (): void => {\n if (renderersInstalled) return;\n installBuiltinRenderers();\n renderersInstalled = true;\n};\n\nconst post = (msg: WorkerRenderResponse, transfer?: Transferable[]): void => {\n if (transfer && transfer.length > 0) {\n (self as unknown as DedicatedWorkerGlobalScope).postMessage(msg, transfer);\n } else {\n (self as unknown as DedicatedWorkerGlobalScope).postMessage(msg);\n }\n};\n\nconst init = (canvas: OffscreenCanvas, width: number, height: number, dpr: number): void => {\n // Load the bundled fonts into the worker's font set so its Canvas2D target\n // draws the same faces as the main thread. Fire-and-forget \u2014 replays after\n // it resolves pick up the loaded fonts.\n void registerBundledFonts(self as unknown as FontScope);\n state.canvas = canvas;\n state.dpr = dpr;\n // Resize the bitmap to dpr-scaled pixels \u2014 the host's CSS size is\n // (width, height); render into the bigger buffer and let the\n // composite step downsample as needed.\n canvas.width = Math.max(1, Math.round(width * dpr));\n canvas.height = Math.max(1, Math.round(height * dpr));\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) throw new Error(\"OffscreenCanvas 2D context unavailable\");\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n state.target = new Canvas2DTarget(ctx as unknown as CanvasRenderingContext2D, width, height, dpr);\n post({ type: \"ready\" });\n};\n\nconst resize = (width: number, height: number): void => {\n if (!state.canvas || !state.target) return;\n state.canvas.width = Math.max(1, Math.round(width * state.dpr));\n state.canvas.height = Math.max(1, Math.round(height * state.dpr));\n const ctx = state.canvas.getContext(\"2d\");\n if (!ctx) return;\n ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);\n state.target.resize(width, height, state.dpr);\n};\n\nconst snapshot = (scene: Scene): void => {\n if (!state.canvas || !state.target) {\n post({ type: \"error\", message: \"Worker not initialised\" });\n return;\n }\n ensureRenderers();\n const ctx = state.canvas.getContext(\"2d\");\n if (ctx === null) {\n post({ type: \"error\", message: \"Worker not initialised\" });\n return;\n }\n ctx.save();\n ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);\n ctx.clearRect(0, 0, state.target.size.width, state.target.size.height);\n renderScene(scene, state.target);\n ctx.restore();\n const bitmap = state.canvas.transferToImageBitmap();\n post({ type: \"frame-done\", bitmap }, [bitmap]);\n};\n\n/**\n * Replay a packed RecordingTarget command stream onto the owned\n * OffscreenCanvas. Used by the LayeredSurface \"offscreen\" backend: the\n * main thread captures every RenderTarget call into a buffer, packs it\n * via `packReplayFrame`, and ships it here per frame (numeric stream in\n * the transfer list, bitmaps cloned alongside); the worker replays.\n */\nconst replay = (msg: PackedReplayMessage): void => {\n if (!state.target) {\n post({ type: \"error\", message: \"Worker not initialised\" });\n return;\n }\n // Register this frame's bitmaps BEFORE replaying so the stream's\n // drawImage id references resolve. These are worker-owned clones \u2014\n // the LRU's evict hook closes them.\n for (const { id, bitmap } of msg.bitmaps) {\n // A re-defined id (re-captured video frame) replaces the stored clone;\n // close the old one \u2014 LruCache.set does not fire onEvict on overwrite.\n const prev = state.images.get(id);\n if (prev && prev !== bitmap) prev.close();\n state.images.set(id, bitmap);\n }\n replayPackedFrame(state.target, msg.buffer, msg.strings, state.images);\n};\n\ntype InboundMessage = WorkerRenderMessage | PackedReplayMessage;\n\n(self as unknown as DedicatedWorkerGlobalScope).addEventListener(\n \"message\",\n (ev: MessageEvent<InboundMessage>) => {\n const msg = ev.data;\n try {\n switch (msg.type) {\n case \"init\":\n init(msg.canvas as OffscreenCanvas, msg.width, msg.height, msg.dpr);\n break;\n case \"resize\":\n resize(msg.width, msg.height);\n break;\n case \"snapshot\":\n // A dpr update only takes effect on the next resize; snapshot\n // honours whatever transform init established.\n snapshot(msg.scene);\n break;\n case \"replay\":\n replay(msg);\n break;\n case \"frame\":\n // Patch-stream frames are not implemented; the protocol is\n // reserved. Reply with an error so callers don't hang on the\n // awaited response.\n post({ type: \"error\", message: \"patch-stream frames not implemented\" });\n break;\n }\n } catch (err) {\n post({\n type: \"error\",\n message: err instanceof Error ? err.message : String(err),\n });\n }\n },\n);\n"],
4
+ "sourcesContent": ["import type { Bounds } from \"@oh-just-another/types\";\n\n/**\n * Level-of-detail thresholds for `renderScene` \u2014 decided PER ELEMENT from\n * its actual size on screen, not from the zoom level, so a huge shape or a\n * giant heading stays readable at 1 % while a sticky note degrades long\n * before that.\n *\n * - **placeholderMaxScreenPx** \u2192 a shape whose on-screen AABB (longer side,\n * world \u00D7 zoom) is below this many pixels is drawn as a flat fill at its\n * AABB and its renderer is skipped entirely.\n * - **minTextScreenPx** \u2192 text (standalone text shapes and embedded shape\n * labels) whose on-screen font size (`fontSize \u00D7 zoom`) is below this\n * many pixels is skipped \u2014 it could not be read anyway, and its\n * wrap + measure cost is the bulk of text rendering.\n *\n * Omit a threshold to disable that level.\n */\nexport interface LodOptions {\n readonly placeholderMaxScreenPx?: number;\n readonly minTextScreenPx?: number;\n}\n\n/** Longer side of `bounds` on screen at `zoom`, in CSS px. */\nexport const screenSizeOf = (bounds: Bounds, zoom: number): number =>\n Math.max(bounds.width, bounds.height) * zoom;\n\n/** `true` when text of `fontSize` (world units) is below the readable LOD floor at `zoom`. */\nexport const isTextBelowLod = (\n fontSize: number,\n zoom: number,\n lod: LodOptions | undefined,\n): boolean => lod?.minTextScreenPx !== undefined && fontSize * zoom < lod.minTextScreenPx;\n", "import type { ElementBase } from \"@oh-just-another/scene\";\nimport type { RenderTarget } from \"../targets/render-target.js\";\nimport type { AnimationClock } from \"../raster/animation-adapter.js\";\n\n/**\n * Optional draw context passed to an {@link ElementRenderer}. Carries the\n * current view `zoom` so a renderer can draw screen-constant features (e.g. a\n * 1px hairline border that does NOT scale with zoom): a local stroke width of\n * `1 / (zoom * shape.scale)` lands at one device pixel. Optional and additive \u2014\n * renderers that don't need it ignore the third argument, and callers that\n * can't supply it (preview / export at 1:1) may omit it.\n */\nexport interface ElementRenderContext {\n /** Current view scale (1.0 = 1:1). `world \u00D7 zoom = screen px`. */\n readonly zoom: number;\n /**\n * Per-instance animated-content playback clock. When set, the image\n * renderer samples animated sources at `clock(shape)` instead of the\n * process-global fallback ({@link setAnimationClock}) \u2014 so two editors on\n * one page can freeze / offset their GIFs independently. Omitted by headless\n * / preview paths, which fall back to the module clock.\n */\n readonly clock?: AnimationClock;\n /**\n * Static-export content switches. Omitted (interactive rendering) =\n * draw everything; export pipelines pass explicit flags (defaults in\n * `EXPORT_CONTENT_DEFAULTS`, overridable in the export UI) so hosts\n * can strip collaborative chrome \u2014 sticky reactions / tags / author \u2014\n * from PNG / SVG output.\n */\n readonly content?: {\n readonly stickyReactions?: boolean;\n readonly stickyTags?: boolean;\n readonly stickyAuthor?: boolean;\n /**\n * The \"+\" add-reaction button next to the pills \u2014 pure UI chrome:\n * drawn on the canvas so it tracks the shape 1:1 while dragging,\n * but excluded from static exports and read-only views.\n */\n readonly stickyAddButton?: boolean;\n };\n /**\n * Id of the element under the idle cursor, when the host tracks it.\n * Drives hover-only chrome (the sticky \"+\" add-reaction button).\n * Omitted by exports / headless paths \u2014 hover chrome never shows.\n */\n readonly hoveredElement?: string;\n /**\n * Draw a grey prompt inside EMPTY text elements (see\n * `pickTextPlaceholder`). Set by the interactive editor; omitted by\n * exports / headless / read-only paths so an empty text stays blank.\n */\n readonly textPlaceholders?: boolean;\n}\n\n/**\n * Draws a single shape onto `target`. The shape's `position` / `rotation` /\n * `scale` have already been applied to the target \u2014 implementations draw in\n * the shape's *local* coordinate space.\n *\n * Implementations should also apply style (fill / stroke / etc.) themselves;\n * the renderer-core does not push styles globally because some shapes (e.g.\n * `text`) extend the base `Style` with overlays.\n */\nexport type ElementRenderer<S extends ElementBase = ElementBase> = (\n shape: S,\n target: RenderTarget,\n ctx?: ElementRenderContext,\n) => void;\n\nconst registry = new Map<string, ElementRenderer>();\n\n/**\n * Register a renderer for a shape type. Plugins call this at module load.\n * The kernel ships renderers for every built-in shape from `@oh-just-another/scene`\n * \u2014 they are installed by `@oh-just-another/renderer-canvas` (and any other\n * backend) on import.\n */\nexport const registerElementRenderer = <S extends ElementBase>(\n type: S[\"type\"],\n renderer: ElementRenderer<S>,\n): void => {\n registry.set(type, renderer as ElementRenderer);\n};\n\n/** Look up a registered renderer. Returns `undefined` for unknown types. */\nexport const getElementRenderer = (type: string): ElementRenderer | undefined => registry.get(type);\n\n/** True if a renderer is registered for `type`. */\nexport const hasElementRenderer = (type: string): boolean => registry.has(type);\n", "/**\n * Tunable thresholds for the scene-level helpers (snap engine, hit-test\n * cheap-cull). Keep magic numbers here so hosts can re-tune the engine\n * without touching the algorithm code.\n */\n\n/**\n * Canvas paper colour when the scene's viewport carries no `background`.\n * Matches the light chrome token (`UI_SURFACE.light.canvas`) so a scene\n * without an explicit colour looks exactly as before the field existed.\n * Any CSS colour; keep it light unless the scene's own colours are authored\n * for dark paper.\n */\nexport const DEFAULT_CANVAS_BACKGROUND = \"#f5f5f5\";\n\n/**\n * Half-side of the bounding box used by `isProbeNearElement` to cheap-cull\n * snap candidates. Shapes farther than this from the probe (plus the\n * snap threshold cushion) are skipped without the full anchor walk.\n *\n * The default of 1000 world units covers any typical editor shape; bump\n * it if hosts work with very large diagrams where the cheap-cull starts\n * to over-prune real candidates.\n */\nexport const SNAP_PROBE_CULL_RADIUS = 1000;\n\n/**\n * Fixed grid spacing in world units: the step `renderGrid` paints and the\n * step snap-to-grid rounds to. Tune to change the grid density; range 4\u201364.\n */\nexport const DEFAULT_GRID_SPACING = 20;\n\n/**\n * Padding (world units) the elbow router inflates obstacle bboxes\n * by before searching. Larger values keep edges visibly clear of\n * shapes; smaller values let the router squeeze through tight\n * spaces. 20 px matches the grid spacing for diagrams that snap to\n * the grid.\n */\nexport const ELBOW_OBSTACLE_MARGIN = 20;\n\n/**\n * Epsilon used to decide whether an axis-aligned segment runs\n * *along* an obstacle boundary (allowed) or *through* it (blocked).\n * A degenerate small value catches floating-point fuzz from\n * `inflate` arithmetic without admitting real crossings.\n */\nexport const ELBOW_OBSTACLE_INTERIOR_EPSILON = 0.5;\n\n/**\n * Longest stub (world px) the orthogonal heuristic fallback in\n * `getLinkPath` adds before bending when an endpoint is anchored to a\n * named side. Bigger values push the first bend further from the shape;\n * reasonable range 24\u201364.\n */\nexport const ELBOW_STUB_MAX = 40;\n\n/**\n * Shortest stub (world px) for the same fallback \u2014 keeps the exit\n * visible even when the endpoints are nearly on top of each other.\n * Reasonable range 4\u201316.\n */\nexport const ELBOW_STUB_MIN = 8;\n\n/**\n * The stub scales as endpoint distance divided by this factor, clamped\n * to [ELBOW_STUB_MIN, ELBOW_STUB_MAX]. Larger divisors give shorter\n * stubs on mid-range spans. Reasonable range 2\u20138.\n */\nexport const ELBOW_STUB_DISTANCE_DIVISOR = 4;\n\n/**\n * Per-turn cost added in the elbow A* so the router minimises BENDS first,\n * distance second (lexicographic \u2014 far larger than any plausible canvas\n * distance). Keeps routes stable: small shape moves stay on the same\n * choice between equal-distance alternatives, and the path takes the\n * fewest corners.\n */\nexport const ELBOW_BEND_PENALTY = 100000;\n\n/**\n * Hysteresis band (world px) for the C-wrap side choice in `wrapRoute`.\n * When the connector must wrap around the union of its two bound shapes,\n * the over/under (or left/right) side is picked from the endpoints'\n * midpoint vs the union centre. Within this band of the centre the\n * previous side is kept (read from `edge.routedPoints`), so a small\n * back-and-forth drag doesn't thrash; the side only switches once the\n * midpoint moves this far past the centre. Larger = stickier.\n * Range: 8\u201348.\n */\nexport const ELBOW_WRAP_HYSTERESIS = 24;\n\n/**\n * Perf cap for \"avoid obstacles\" routing. The A* grid scales with the\n * number of obstacle corners, and the route is recomputed every frame\n * while a shape is dragged, so above this many scene shapes we skip\n * whole-scene avoidance and fall back to the cheap two-box elbow (the\n * link still keeps clear of its own ends). Range: 60\u2013300.\n */\nexport const ELBOW_AVOID_MAX_OBSTACLES = 150;\n\n/**\n * Length (world px) of the fixed, non-movable terminal segment an elbow\n * connector always leaves at each end before its first bend \u2014 the endpoint\n * is pushed out this far along its exit heading so the connector departs/\n * arrives perpendicular to the edge and there's buffer room to draw the\n * arrowhead. Must stay \u2265 ELBOW_OBSTACLE_MARGIN so the pushed-out point\n * sits outside the inflated obstacle the A* router avoids. Larger \u2192 more\n * breathing room before the first bend. Range: 16\u201340.\n */\nexport const ELBOW_TERMINAL_BUFFER = 30;\n\n/**\n * --- Self-loop connectors (a link whose both ends bind to the SAME element) ---\n *\n * A self-loop is routed OUTSIDE the element so it reads as a loop/arc instead of\n * a flat line on (or across) the shape.\n *\n * - `SELF_LOOP_SIZE` \u2014 how far (world px) the loop bows out past the element's\n * edge. Fixed for every element. Range: 24\u201380.\n * - `SELF_LOOP_SPREAD` \u2014 when both ends resolve to the SAME point (centre /\n * floating / same anchor) the two exit points are spread this far apart along\n * the edge so the loop has width. Clamped to a third of the side. Range: 12\u201340.\n * - `SELF_LOOP_CURVE_ARM_FACTOR` \u2014 control-arm length for a curved self-loop as a\n * multiple of `SELF_LOOP_SIZE`; larger = rounder, more pronounced arc. 2.2\n * gives a clean teardrop. Range: 1.5\u20133.\n */\nexport const SELF_LOOP_SIZE = 40;\nexport const SELF_LOOP_SPREAD = 24;\nexport const SELF_LOOP_CURVE_ARM_FACTOR = 2.2;\n\n/**\n * Clearance (world px) a candidate centred path (the \"thread\"/mid-S or the\n * C-wrap) must keep from a bound shape's interior before it counts as CROSSING\n * it. This is the threshold that decides thread-vs-wrap-vs-A*: a larger value\n * makes the router bail off the direct/centred path sooner (route stays further\n * from shapes), a smaller value lets it skim closer to an edge before detouring.\n * At 1 px it only rejects genuine interior crossings, allowing edge-grazing.\n * Range: 1\u20138.\n */\nexport const ELBOW_OBSTACLE_CLEARANCE = 1;\n\n/**\n * Parametric step used to sample each segment of a candidate path when testing\n * whether it crosses a shape (`pathCrossesObstacle`). Smaller = finer (catches\n * a narrow shape a coarse sampling would skip) at more cost; 0.1 samples 11\n * points per segment, enough for typical shape sizes. Range: 0.02\u20130.2.\n */\nexport const ELBOW_CROSS_SAMPLE_STEP = 0.1;\n\n/**\n * --- Curved (bezier) link geometry ---\n *\n * Shared by the renderer (draws cubic beziers), hit-testing and bounds\n * (flatten the same curve) so the visible curve and the clickable curve\n * agree. Lives in scene so lower layers own the geometry; renderer-core\n * imports it.\n *\n * - `CURVE_CATMULL_TENSION` \u2014 divisor for the Catmull-Rom tangents in the\n * spline\u2192bezier conversion (waypointed curves). 6 is canonical uniform\n * Catmull-Rom (control point = P + (Pnext \u2212 Pprev) / 6). Larger \u2192 tighter;\n * smaller \u2192 looser. Range: 4\u20138.\n * - `CURVE_END_TANGENT_RATIO` \u2014 for a no-waypoint span the cubic's control\n * arms leave/enter the endpoints along their edge normals with length =\n * this fraction of the endpoint distance, so the connector exits/enters\n * perpendicular to the element edge (flowchart look). Larger \u2192 rounder /\n * more pronounced. Range: 0.25\u20130.6.\n * - `CURVE_END_TANGENT_MAX_PX` \u2014 caps that control-arm length (world px) so a\n * long link doesn't over-bow. Range: 60\u2013160.\n * - `CURVE_FLATTEN_SEGMENTS` \u2014 samples per cubic when flattening the curve\n * for hit-testing / bounds. Higher = closer to the drawn curve. Range:\n * 8\u201324.\n */\nexport const CURVE_CATMULL_TENSION = 6;\nexport const CURVE_END_TANGENT_RATIO = 0.8;\nexport const CURVE_END_TANGENT_MAX_PX = 240;\nexport const CURVE_FLATTEN_SEGMENTS = 16;\n\n/**\n * --- Roundness (Style.roundness) ---\n *\n * Adaptive radius: pick a fixed radius for shapes bigger than the cutoff,\n * scale proportionally for smaller ones so the corner doesn't dominate.\n * 32 px / 0.25 looks rounded without becoming a capsule across the\n * realistic shape-size range.\n */\n\n/** Fixed pixel radius used by adaptive rounding for shapes \u2265 cutoff. */\nexport const ADAPTIVE_CORNER_RADIUS = 32;\n\n/**\n * Proportional radius (0..1 of the smaller side) used by adaptive\n * rounding for shapes below the cutoff, and the fall-through when\n * `Roundness.value` is omitted but the type is `round`.\n */\nexport const PROPORTIONAL_CORNER_RADIUS = 0.25;\n\n/**\n * --- Text bounds estimation ---\n *\n * The text bounder has no layout engine, so it approximates the box.\n * Renderers compute the precise layout (via `measureText`) during\n * draw / caret positioning; these factors only drive selection bbox\n * and resize-handle placement, where a rough estimate is fine.\n *\n * - `TEXT_APPROX_CHAR_WIDTH_FACTOR` \u2014 average glyph advance as a\n * fraction of font size (~0.6 for proportional Latin text).\n * - `TEXT_LINE_HEIGHT_FACTOR` \u2014 line height as a multiple of font\n * size. Must match the renderer's `DEFAULT_LINE_HEIGHT_FACTOR`.\n */\nexport const TEXT_APPROX_CHAR_WIDTH_FACTOR = 0.6;\nexport const TEXT_LINE_HEIGHT_FACTOR = 1.2;\n\n/**\n * --- Frame header (label strip) geometry ---\n *\n * The frame's name is drawn in a strip ABOVE the frame body (local y in\n * `[-FRAME_HEADER_HEIGHT, 0]`). Shared by the renderer (draws it), the\n * editor (header double-click \u2192 rename hit zone + render overflow) and\n * react-ui (positions the inline name editor) so all three agree.\n *\n * - `FRAME_HEADER_HEIGHT` \u2014 strip height (world px).\n * - `FRAME_HEADER_PADDING_X` \u2014 horizontal text inset, each side (world px).\n * - `FRAME_HEADER_FONT_SIZE` \u2014 label font size (world px).\n *\n * The strip width is dynamic: it hugs the label width but is capped at the\n * frame's own width (a too-long name is ellipsised) \u2014 computed in the\n * renderer, which can measure text.\n */\nexport const FRAME_HEADER_HEIGHT = 24;\nexport const FRAME_HEADER_PADDING_X = 8;\nexport const FRAME_HEADER_FONT_SIZE = 12;\n\n/**\n * --- Layout defaults ---\n *\n * Used by the built-in layout functions (`gridLayout`, `stackLayout`,\n * `wrapLayout`, `treeLayout`) when the caller's spec omits the value.\n *\n * - `DEFAULT_LAYOUT_GAP` \u2014 cell/sibling gap (world px) for grid, stack and\n * wrap layouts. Larger = more breathing room between shapes. Range: 8\u201348.\n * - `DEFAULT_TREE_RANK_SEP` \u2014 vertical distance (world px) between successive\n * depth levels in the tree layout. Larger = taller tree. Range: 40\u2013160.\n * - `DEFAULT_TREE_NODE_SEP` \u2014 horizontal distance (world px) between siblings\n * in the tree layout. Larger = wider tree. Range: 12\u201364.\n */\nexport const DEFAULT_LAYOUT_GAP = 16;\nexport const DEFAULT_TREE_RANK_SEP = 80;\nexport const DEFAULT_TREE_NODE_SEP = 24;\n\n/**\n * --- Outline sampling ---\n *\n * - `DEFAULT_OUTLINE_SAMPLES` \u2014 fixed density `findNearestOutlinePoint` walks\n * the outline at when resolving the nearest ratio to a world point. Good\n * enough for visual snap; bump it for sub-pixel accuracy at high zoom.\n * Range: 32\u2013256.\n * - `FLOATING_OUTLINE_SAMPLES` \u2014 segments the outline is sampled into when\n * intersecting it with the floating-endpoint ray. Smooth enough for\n * ellipses at high zoom without being a hot-loop cost (resolved once per\n * edge per frame). Range: 48\u2013256.\n */\nexport const DEFAULT_OUTLINE_SAMPLES = 64;\nexport const FLOATING_OUTLINE_SAMPLES = 96;\n\n/**\n * Fallback scene dimensions, in pixels, for a scene with no explicit\n * viewport size \u2014 a freshly imported document whose source carries no\n * canvas size, or an empty export region. Just needs to be non-degenerate.\n * Range: a few hundred to ~2000.\n */\nexport const FALLBACK_SCENE_WIDTH = 800;\nexport const FALLBACK_SCENE_HEIGHT = 600;\n\n/**\n * Max angular step (radians) between sampled points along a brush-outline round\n * join or cap arc. Smaller = smoother curves / more points; larger = coarser /\n * cheaper. ~0.35 rad (20\u00B0) keeps joins visually round without flooding the\n * polygon. Range: 0.2\u20130.6.\n */\nexport const BRUSH_OUTLINE_ARC_STEP = 0.35;\n\n/**\n * Miter limit for a brush-outline concave corner: when the miter point would run\n * more than this many half-widths from the vertex (a very sharp turn), fall back\n * to a bevel (two offset points) so the outline can't spike into a long spar.\n * Range: 1.5\u20134.\n */\nexport const BRUSH_OUTLINE_MITER_LIMIT = 2.5;\n\n/**\n * Default fractional position of a link label along its path (0 = source end,\n * 1 = target end). Used when `LinkLabel.position` is unset. Range: 0\u20131.\n */\nexport const LINK_LABEL_DEFAULT_POSITION = 0.5;\n\n/**\n * Default link-label font size (world px at zoom 1). Range: 10\u201316.\n */\nexport const LINK_LABEL_DEFAULT_FONT_SIZE = 12;\n\n/**\n * Max link-label line width before word-wrap kicks in (world px at zoom 1).\n * Wider = fewer, longer lines; narrower = taller pill. Range: 100\u2013240.\n */\nexport const LINK_LABEL_MAX_WIDTH = 160;\n\n/**\n * Inner padding of the label pill around the text block (world px at zoom 1).\n * Range: 2\u201310.\n */\nexport const LINK_LABEL_PAD_X = 6;\nexport const LINK_LABEL_PAD_Y = 3;\n\n/**\n * Line-height factor for multiline link labels (\u00D7 fontSize). Range: 1.1\u20131.5.\n */\nexport const LINK_LABEL_LINE_HEIGHT = 1.25;\n\n/**\n * Min arc-length distance (world px) the label anchor keeps from either path\n * end, so the pill never sits on an arrowhead. Applied as a clamp on the\n * fractional position; ignored when the whole path is shorter than twice this.\n * Range: 12\u201340.\n */\nexport const LINK_LABEL_END_CLEARANCE = 24;\n\n/**\n * Average glyph advance as a fraction of fontSize \u2014 the conservative width\n * estimate used where real text measurement is unavailable (hit-testing,\n * dirty-rect / culling bounds). Slightly generous on purpose: overestimating\n * keeps a label inside its computed bounds. Range: 0.55\u20130.7.\n */\nexport const LINK_LABEL_CHAR_WIDTH_FACTOR = 0.62;\n\n/**\n * Built-in polygon presets for image masks (`ImageMask.kind: \"polygon\"`),\n * as normalised (0..1) closed rings over the element box. Offered by the\n * mask picker UI; hosts may pass any other ring \u2014 the model accepts\n * arbitrary polygons. Point counts stay low: masks clip through the\n * render targets' clip API, and every vertex costs path work per frame.\n */\nexport const IMAGE_MASK_POLYGON_PRESETS: Readonly<\n Record<string, readonly { readonly x: number; readonly y: number }[]>\n> = {\n diamond: [\n { x: 0.5, y: 0 },\n { x: 1, y: 0.5 },\n { x: 0.5, y: 1 },\n { x: 0, y: 0.5 },\n ],\n triangle: [\n { x: 0.5, y: 0 },\n { x: 1, y: 1 },\n { x: 0, y: 1 },\n ],\n hexagon: [\n { x: 0.25, y: 0 },\n { x: 0.75, y: 0 },\n { x: 1, y: 0.5 },\n { x: 0.75, y: 1 },\n { x: 0.25, y: 1 },\n { x: 0, y: 0.5 },\n ],\n star: [\n { x: 0.5, y: 0 },\n { x: 0.618, y: 0.363 },\n { x: 1, y: 0.382 },\n { x: 0.691, y: 0.618 },\n { x: 0.809, y: 1 },\n { x: 0.5, y: 0.764 },\n { x: 0.191, y: 1 },\n { x: 0.309, y: 0.618 },\n { x: 0, y: 0.382 },\n { x: 0.382, y: 0.363 },\n ],\n};\n\n/**\n * Placeholder shown inside an EMPTY text element while it is being\n * written (interactive rendering only \u2014 never in exports). One entry is\n * picked per element, deterministically from its id, with these relative\n * weights (`weight` = chance ticket count): the plain prompts dominate, the\n * jokes are rare treats. Hosts may pass their own list to\n * `pickTextPlaceholder`. The text bounder sizes an empty text element by\n * its prompt, so the selection box wraps what is on screen.\n */\nexport interface TextPlaceholder {\n readonly text: string;\n /** Relative chance; integer \u2265 1. */\n readonly weight: number;\n}\n/**\n * Selection-outline (contour) sampling \u2014 how many polyline points stand in\n * for a curve when a shape's outline is walked (hit-testing, snap probes,\n * link end-points along the outline).\n * - `SELECTION_OUTLINE_ELLIPSE_SAMPLES` \u2014 points around a whole ellipse.\n * Range 24\u201396; fewer = faster, coarser hit areas.\n * - `SELECTION_OUTLINE_CURVE_SAMPLES` \u2014 points per Q/C path segment.\n * Range 4\u201324.\n * - `SELECTION_OUTLINE_CORNER_SAMPLES` \u2014 points per rounded-rect corner arc.\n * Range 2\u201312.\n */\nexport const SELECTION_OUTLINE_ELLIPSE_SAMPLES = 48;\nexport const SELECTION_OUTLINE_CURVE_SAMPLES = 10;\nexport const SELECTION_OUTLINE_CORNER_SAMPLES = 6;\n\n/**\n * Upper bound on parent-chain walks (`getAncestors` / nesting queries) so a\n * corrupted `parentId` cycle terminates instead of looping. Larger than any\n * sane nesting depth; range 16\u2013256.\n */\nexport const MAX_PARENT_DEPTH = 64;\n\n/**\n * Default `SpatialGrid` cell size in world units. Tuned for editor-scale\n * scenes with ~100\u2013400 px shapes: a shape touches 1\u20134 cells, range queries\n * visit few cells. Raise for very large shapes, lower for dense tiny ones.\n * Range 64\u20131024.\n */\nexport const SPATIAL_GRID_CELL_SIZE = 256;\n\n/**\n * Default hit tolerance (world units) for `findLinkAt` \u2014 how far from a\n * link's stroke a point still counts as \"on the link\". Range 2\u201312.\n */\nexport const LINK_HIT_THRESHOLD = 5;\n\nexport const TEXT_PLACEHOLDERS: readonly TextPlaceholder[] = [\n { text: \"Type something\", weight: 40 },\n { text: \"Place for text\", weight: 20 },\n { text: \"Start typing\u2026\", weight: 12 },\n { text: \"Your text here\", weight: 10 },\n { text: \"Add a note\", weight: 8 },\n { text: \"What's on your mind?\", weight: 6 },\n { text: \"Words go here\", weight: 5 },\n { text: \"Say it in a few words\", weight: 4 },\n { text: \"Lorem ipsum? No \u2014 your words.\", weight: 3 },\n { text: \"Blank is a state of mind\", weight: 2 },\n { text: \"Insert genius here\", weight: 2 },\n { text: \"The cursor is waiting patiently\", weight: 1 },\n];\n\n/**\n * Longest accessible name (screen-reader announcement) built from a text\n * body or a shape label; longer content is cut with an ellipsis so the\n * announcement stays actionable. Range 40\u2013160.\n */\nexport const ACCESSIBLE_NAME_MAX_CHARS = 80;\n", "import type { Color } from \"@oh-just-another/types\";\nimport { ADAPTIVE_CORNER_RADIUS, PROPORTIONAL_CORNER_RADIUS } from \"../constants.js\";\n\nexport type LineCap = \"butt\" | \"round\" | \"square\";\nexport type LineJoin = \"miter\" | \"round\" | \"bevel\";\n\n/**\n * Where the stroke sits relative to the shape's path.\n * `center` \u2014 half the stroke width inside the path, half outside.\n * Canvas2D / SVG default.\n * `inside` \u2014 stroke is fully inside the path (path = outer edge).\n * Useful when shape bounds must match the fill region\n * exactly (auto-layout / hit-tests).\n * `outside` \u2014 stroke is fully outside (path = inner edge).\n */\nexport type StrokeAlign = \"center\" | \"inside\" | \"outside\";\n\n/**\n * Corner-rounding spec for shapes that support it (rectangle, container,\n * box arrow, \u2026):\n * `sharp` \u2014 no rounding (sharp corners). Equivalent to omitting the\n * field; lets the value be set explicitly.\n * `round` \u2014 rounded corners. Without `value`, falls back to the\n * adaptive radius (fixed 32 px for big shapes, scales to\n * 25 % of the smaller side for shapes < 128 px so they\n * don't read as a capsule).\n */\nexport interface Roundness {\n readonly type: \"sharp\" | \"round\";\n /**\n * Override the rounded-corner radius in world units. Ignored when\n * `type === \"sharp\"`. When omitted on `round` shapes the renderer\n * applies the adaptive default (see {@link Style}).\n */\n readonly value?: number;\n}\n\n/**\n * Visual style for shapes and edges. Every field is optional so that scenes,\n * patches and partial updates stay compact; renderers fall back to library\n * defaults when a field is omitted.\n */\nexport interface Style {\n readonly fill?: Color;\n readonly stroke?: Color;\n readonly strokeWidth?: number;\n readonly opacity?: number;\n readonly dashArray?: readonly number[];\n readonly lineCap?: LineCap;\n readonly lineJoin?: LineJoin;\n /** Stroke alignment relative to the path. Defaults to `center`. */\n readonly strokeAlign?: StrokeAlign;\n /** Corner-rounding spec. Omitted = sharp corners. */\n readonly roundness?: Roundness;\n}\n\nexport type TextAlign = \"left\" | \"center\" | \"right\";\nexport type TextBaseline = \"top\" | \"middle\" | \"bottom\";\nexport type FontWeight = \"normal\" | \"bold\";\nexport type FontStyle = \"normal\" | \"italic\";\n\n/**\n * Text decorations (underline / strikethrough). Both can be on at once.\n * Rendered as thin line-rects under / through the text by the renderer,\n * so they work identically on Canvas2D and WebGL2.\n */\nexport interface TextDecoration {\n readonly underline?: boolean;\n readonly strikethrough?: boolean;\n}\n\n/**\n * Text-specific style overlay. Inherits all `Style` fields (fill = text color,\n * stroke = outline). Layout metrics live on the `TextElement` itself, not here.\n */\nexport interface TextStyle extends Style {\n readonly textAlign?: TextAlign;\n readonly textBaseline?: TextBaseline;\n /**\n * Marker-style background behind the glyphs (highlight colour). Painted\n * as a full line-height rect under the text, per styled run when runs are\n * present. Omitted = no highlight.\n */\n readonly highlight?: Color;\n /** Bold toggle. Default `\"normal\"`. */\n readonly fontWeight?: FontWeight;\n /** Italic toggle. Default `\"normal\"`. */\n readonly fontStyle?: FontStyle;\n /** Underline / strikethrough. Omitted = neither. */\n readonly textDecoration?: TextDecoration;\n}\n\n/**\n * How far a shape's stroke extends OUTSIDE its geometric contour, in world\n * units. Depends on stroke width and alignment: `outside` \u2192 the full width,\n * `center` \u2192 half, `inside` \u2192 none. No stroke \u2192 0. Used to place the\n * selection halo a constant distance beyond the shape's VISIBLE outer edge\n * (contour + this extent), regardless of border thickness / alignment.\n */\nexport const strokeOutsideExtent = (style: Style): number => {\n const hasStroke = style.stroke !== undefined && style.stroke !== \"transparent\";\n if (!hasStroke) return 0;\n const w = style.strokeWidth ?? 1;\n if (w <= 0) return 0;\n switch (style.strokeAlign ?? \"center\") {\n case \"outside\":\n return w;\n case \"inside\":\n return 0;\n default:\n return w / 2;\n }\n};\n\nexport const getCornerRadius = (\n roundness: Roundness | undefined,\n width: number,\n height: number,\n): number => {\n if (!roundness || roundness.type === \"sharp\") return 0;\n const smaller = Math.min(Math.abs(width), Math.abs(height));\n if (smaller <= 0) return 0;\n if (roundness.value !== undefined) {\n // Honour the override but clamp to half the smaller side so\n // the corner radii can't overlap on narrow shapes (would\n // produce a degenerate path).\n return Math.max(0, Math.min(roundness.value, smaller / 2));\n }\n // Adaptive default: proportional below the cutoff, fixed above.\n const cutoff = ADAPTIVE_CORNER_RADIUS / PROPORTIONAL_CORNER_RADIUS;\n if (smaller <= cutoff) return smaller * PROPORTIONAL_CORNER_RADIUS;\n return ADAPTIVE_CORNER_RADIUS;\n};\n", "import { TEXT_PLACEHOLDERS, type TextPlaceholder } from \"../constants.js\";\n\n/** FNV-1a 32-bit hash \u2014 stable across runs, cheap, good spread for short ids. */\nconst fnv1a = (s: string): number => {\n let h = 0x811c9dc5;\n for (let i = 0; i < s.length; i++) {\n h ^= s.charCodeAt(i);\n h = Math.imul(h, 0x01000193) >>> 0;\n }\n return h;\n};\n\n/**\n * Pick the placeholder for an empty text element. Weighted by\n * `TextPlaceholder.weight`, and DETERMINISTIC in `seed` (the element id):\n * the same element always shows the same prompt \u2014 no reshuffling between\n * frames or re-renders \u2014 while different elements spread across the list.\n */\nexport const pickTextPlaceholder = (\n seed: string,\n placeholders: readonly TextPlaceholder[] = TEXT_PLACEHOLDERS,\n): string => {\n const total = placeholders.reduce((sum, p) => sum + Math.max(1, p.weight), 0);\n if (total <= 0 || placeholders.length === 0) return \"\";\n let ticket = fnv1a(seed) % total;\n for (const p of placeholders) {\n ticket -= Math.max(1, p.weight);\n if (ticket < 0) return p.text;\n }\n return placeholders[placeholders.length - 1]?.text ?? \"\";\n};\n", "import type { TextStyle } from \"./style.js\";\nimport type { TextElement } from \"../shapes/shape.js\";\n\n/**\n * A styled segment of a text block. `text` is the raw substring; `style`\n * is a PARTIAL overlay merged over the owning {@link TextElement}'s base\n * `style` (element style wins for fields the run omits). Omitting `style`\n * means \"inherit the element style verbatim\".\n *\n * Runs are an ADDITIVE overlay: the element's flat `text` stays the source\n * of truth and MUST equal `runs.map(r => r.text).join(\"\")`. A `TextElement`\n * with no `runs` (or an empty array) renders exactly as before this feature\n * existed \u2014 one uniform style \u2014 so plain-text scenes are untouched.\n */\nexport interface TextRun {\n readonly text: string;\n readonly style?: Partial<TextStyle>;\n}\n\n/** Concatenated raw text of a run list (the flat-text source of truth). */\nexport const runsToText = (runs: readonly TextRun[]): string => runs.map((r) => r.text).join(\"\");\n\n/**\n * Stable-ish key for a run style, used only to coalesce adjacent runs that\n * carry identical styling. Sorts top-level keys so key order doesn't defeat\n * the compare. A false \"different\" verdict only costs an extra (correct) run,\n * never wrong rendering, so a shallow canonicalisation is sufficient.\n */\nconst styleKey = (style: Partial<TextStyle> | undefined): string => {\n if (!style) return \"\";\n const entries = Object.entries(style as Record<string, unknown>)\n .filter(([, v]) => v !== undefined)\n .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n return JSON.stringify(entries);\n};\n\n/**\n * Drop empty-text runs and coalesce adjacent runs with identical styling.\n * Returns a compact, canonical run list. An all-empty input yields `[]`.\n */\nexport const normalizeRuns = (runs: readonly TextRun[]): TextRun[] => {\n const out: TextRun[] = [];\n for (const run of runs) {\n if (run.text === \"\") continue;\n const last = out[out.length - 1];\n if (last !== undefined && styleKey(last.style) === styleKey(run.style)) {\n out[out.length - 1] = {\n text: last.text + run.text,\n ...(last.style !== undefined ? { style: last.style } : {}),\n };\n } else {\n out.push(run);\n }\n }\n return out;\n};\n\n/** The run list to start from: explicit `runs`, else one run spanning `text`. */\nconst baseRuns = (el: Pick<TextElement, \"text\" | \"runs\">): TextRun[] => {\n const runs = el.runs;\n if (runs !== undefined && runs.length > 0) return normalizeRuns(runs);\n return el.text === \"\" ? [] : [{ text: el.text }];\n};\n\n/**\n * The runs that fall inside the character range `[from, to)` of a text block,\n * clipped at the range edges. Used by the renderer to split each visual line\n * into per-style segments. Styles are preserved verbatim (still partial\n * overlays over the element style).\n */\nexport const sliceRuns = (\n el: Pick<TextElement, \"text\" | \"runs\">,\n from: number,\n to: number,\n): TextRun[] => {\n const lo = Math.min(from, to);\n const hi = Math.max(from, to);\n const out: TextRun[] = [];\n let pos = 0;\n for (const run of baseRuns(el)) {\n const rStart = pos;\n const rEnd = pos + run.text.length;\n pos = rEnd;\n const s = Math.max(rStart, lo);\n const e = Math.min(rEnd, hi);\n if (s >= e) continue;\n out.push({\n text: run.text.slice(s - rStart, e - rStart),\n ...(run.style !== undefined ? { style: run.style } : {}),\n });\n }\n return out;\n};\n\n/** Shallow-merge `patch` over `base`, pruning keys explicitly set to undefined. */\nconst mergeStyle = (\n base: Partial<TextStyle> | undefined,\n patch: Partial<TextStyle>,\n): Partial<TextStyle> | undefined => {\n const merged: Record<string, unknown> = { ...(base ?? {}), ...patch };\n const entries = Object.entries(merged).filter(([, v]) => v !== undefined);\n if (entries.length === 0) return undefined;\n const cleaned: Partial<TextStyle> = Object.fromEntries(entries);\n return cleaned;\n};\n\n/**\n * Pure operation: apply a partial {@link TextStyle} overlay to the character\n * range `[from, to)` of a text element, returning a NEW element. The flat\n * `text` is never touched (invariant preserved). Existing runs are split at\n * the range boundaries and the patch is merged into the overlapping portion;\n * adjacent runs with equal styling are coalesced.\n *\n * When the result collapses to a single unstyled run spanning the whole text,\n * `runs` is dropped entirely so the element reverts to a plain text block\n * (keeps scenes minimal and round-trips cleanly). An empty range is a no-op.\n */\nexport const applyStyleToRange = (\n el: TextElement,\n from: number,\n to: number,\n patch: Partial<TextStyle>,\n): TextElement => {\n const lo = Math.max(0, Math.min(from, to));\n const hi = Math.min(el.text.length, Math.max(from, to));\n if (lo >= hi) return el;\n\n const out: TextRun[] = [];\n let pos = 0;\n for (const run of baseRuns(el)) {\n const rStart = pos;\n const rEnd = pos + run.text.length;\n pos = rEnd;\n const midStart = Math.max(rStart, lo);\n const midEnd = Math.min(rEnd, hi);\n if (midStart >= midEnd) {\n out.push(run);\n continue;\n }\n if (rStart < midStart) {\n out.push({\n text: run.text.slice(0, midStart - rStart),\n ...(run.style !== undefined ? { style: run.style } : {}),\n });\n }\n const merged = mergeStyle(run.style, patch);\n out.push({\n text: run.text.slice(midStart - rStart, midEnd - rStart),\n ...(merged !== undefined ? { style: merged } : {}),\n });\n if (midEnd < rEnd) {\n out.push({\n text: run.text.slice(midEnd - rStart),\n ...(run.style !== undefined ? { style: run.style } : {}),\n });\n }\n }\n\n const normalized = normalizeRuns(out);\n const only = normalized[0];\n if (\n normalized.length === 1 &&\n only !== undefined &&\n (only.style === undefined || Object.keys(only.style).length === 0)\n ) {\n // Reverted to a single uniform style \u2192 shed the overlay entirely.\n const { runs: _drop, ...rest } = el;\n void _drop;\n return rest;\n }\n return { ...el, runs: normalized };\n};\n", "import type { TextParagraph } from \"../shapes/shape.js\";\n\n/**\n * Paragraph-attribute helpers for text lists. A \"paragraph\" is a\n * `\\n`-separated block of a text element's flat `text`; attributes\n * (`TextParagraph`) are stored in an array aligned by paragraph index.\n * These helpers keep that array consistent as the text is edited and\n * answer range queries for the toolbar.\n */\n\n/** Number of paragraphs in `text` (always \u2265 1; empty text = one empty paragraph). */\nexport const paragraphCount = (text: string): number => {\n let n = 1;\n for (const ch of text) if (ch === \"\\n\") n++;\n return n;\n};\n\n/**\n * Paragraph index range `[first, last]` (inclusive) covered by the source\n * offset range `[from, to]`. Offsets outside the text are clamped.\n */\nexport const paragraphRangeForOffsets = (\n text: string,\n from: number,\n to: number,\n): { readonly first: number; readonly last: number } => {\n const lo = Math.max(0, Math.min(from, to));\n const hi = Math.min(text.length, Math.max(from, to));\n // Paragraph index = newlines before the offset; `hi` may equal `text.length`\n // (caret at the very end), so the count runs over `[0, hi)` only.\n let idx = 0;\n let first = 0;\n for (let i = 0; i < hi; i++) {\n if (i === lo) first = idx;\n if (text[i] === \"\\n\") idx++;\n }\n if (lo >= hi) first = idx;\n return { first, last: idx };\n};\n\n/** Attrs for a paragraph index (missing / short array \u2192 plain). */\nexport const paragraphAt = (\n paragraphs: readonly TextParagraph[] | undefined,\n index: number,\n): TextParagraph => paragraphs?.[index] ?? {};\n\nconst isPlain = (p: TextParagraph): boolean => p.list === undefined && (p.indent ?? 0) === 0;\n\n/**\n * Canonical form: trailing plain paragraphs are dropped; an all-plain\n * array collapses to `undefined` so plain text stays byte-identical on\n * the wire.\n */\nexport const normalizeParagraphs = (\n paragraphs: readonly TextParagraph[],\n): readonly TextParagraph[] | undefined => {\n let end = paragraphs.length;\n while (end > 0 && isPlain(paragraphs[end - 1] ?? {})) end--;\n if (end === 0) return undefined;\n return paragraphs.slice(0, end);\n};\n\n/**\n * Re-align the paragraph-attribute array after a text change. Paragraphs\n * are matched by the longest common prefix and suffix of the old / new\n * paragraph lists; the edited middle keeps the first edited paragraph's\n * attrs and lets inserted paragraphs inherit them \u2014 so pressing Enter\n * inside a list item continues the list, and deleting a line drops its\n * attrs with it. Pure and heuristic by design: it has no caret input, so\n * pathological multi-paragraph pastes may inherit conservatively (plain).\n */\nexport const remapParagraphsForTextChange = (\n oldText: string,\n newText: string,\n paragraphs: readonly TextParagraph[] | undefined,\n): readonly TextParagraph[] | undefined => {\n if (paragraphs === undefined || paragraphs.length === 0) return undefined;\n if (oldText === newText) return paragraphs;\n const oldParas = oldText.split(\"\\n\");\n const newParas = newText.split(\"\\n\");\n if (oldParas.length === newParas.length) return paragraphs; // in-line edit \u2014 indices stable\n\n // Longest common prefix / suffix of the paragraph LISTS (exact match).\n let prefix = 0;\n while (\n prefix < oldParas.length &&\n prefix < newParas.length &&\n oldParas[prefix] === newParas[prefix]\n ) {\n prefix++;\n }\n let suffix = 0;\n while (\n suffix < oldParas.length - prefix &&\n suffix < newParas.length - prefix &&\n oldParas[oldParas.length - 1 - suffix] === newParas[newParas.length - 1 - suffix]\n ) {\n suffix++;\n }\n\n const out: TextParagraph[] = [];\n for (let i = 0; i < prefix; i++) out.push(paragraphAt(paragraphs, i));\n // The edited middle: inherit the first edited old paragraph's attrs\n // (falls back to the last prefix paragraph when the middle was empty \u2014\n // a pure insertion continues whatever precedes it).\n const inheritFrom = Math.min(prefix, oldParas.length - 1);\n const inherited = paragraphAt(paragraphs, inheritFrom);\n const newMiddle = newParas.length - prefix - suffix;\n for (let i = 0; i < newMiddle; i++) out.push(inherited);\n for (let i = suffix; i > 0; i--) out.push(paragraphAt(paragraphs, oldParas.length - i));\n return normalizeParagraphs(out);\n};\n\n/**\n * Derived list markers, one per paragraph: `\"\u2022\"` for bullets, `\"N.\"` for\n * numbered items (consecutive numbered paragraphs at the SAME indent\n * count up; any other paragraph kind resets the counter), `null` for\n * plain paragraphs.\n */\nexport const listMarkers = (\n paragraphs: readonly TextParagraph[] | undefined,\n count: number,\n): readonly (string | null)[] => {\n const out: (string | null)[] = [];\n const counters = new Map<number, number>();\n for (let i = 0; i < count; i++) {\n const p = paragraphAt(paragraphs, i);\n const level = p.indent ?? 0;\n if (p.list === \"numbered\") {\n const n = (counters.get(level) ?? 0) + 1;\n counters.set(level, n);\n // A deeper-or-equal reset boundary: nested lists restart when the\n // chain is interrupted at their own level (handled below).\n for (const key of [...counters.keys()]) if (key > level) counters.delete(key);\n out.push(`${String(n)}.`);\n } else {\n if (p.list === undefined) counters.clear();\n else for (const key of [...counters.keys()]) if (key >= level) counters.delete(key);\n out.push(p.list === \"bullet\" ? \"\u2022\" : null);\n }\n }\n return out;\n};\n", "/**\n * Return `v` when defined, otherwise throw. For narrowing values the caller\n * knows are present (in-range array access, resolved lookups) without scattering\n * non-null assertions.\n */\nexport const req = <T>(v: T | undefined): T => {\n if (v === undefined) throw new Error(\"required value is undefined\");\n return v;\n};\n", "import type { Vec2 } from \"@oh-just-another/types\";\n\nexport const ZERO: Vec2 = Object.freeze({ x: 0, y: 0 });\n\nexport const of = (x: number, y: number): Vec2 => ({ x, y });\n\nexport const add = (a: Vec2, b: Vec2): Vec2 => ({ x: a.x + b.x, y: a.y + b.y });\n\nexport const sub = (a: Vec2, b: Vec2): Vec2 => ({ x: a.x - b.x, y: a.y - b.y });\n\nexport const mul = (a: Vec2, scalar: number): Vec2 => ({ x: a.x * scalar, y: a.y * scalar });\n\nexport const div = (a: Vec2, scalar: number): Vec2 => ({ x: a.x / scalar, y: a.y / scalar });\n\nexport const negate = (a: Vec2): Vec2 => ({ x: -a.x, y: -a.y });\n\nexport const dot = (a: Vec2, b: Vec2): number => a.x * b.x + a.y * b.y;\n\n/** 2D pseudo-cross (z component of the 3D cross product). */\nexport const cross = (a: Vec2, b: Vec2): number => a.x * b.y - a.y * b.x;\n\nexport const lengthSq = (a: Vec2): number => a.x * a.x + a.y * a.y;\n\nexport const length = (a: Vec2): number => Math.sqrt(lengthSq(a));\n\nexport const distanceSq = (a: Vec2, b: Vec2): number => {\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n return dx * dx + dy * dy;\n};\n\nexport const distance = (a: Vec2, b: Vec2): number => Math.sqrt(distanceSq(a, b));\n\n/** Returns ZERO when input is the zero vector. */\nexport const normalize = (a: Vec2): Vec2 => {\n const len = length(a);\n if (len === 0) return ZERO;\n return { x: a.x / len, y: a.y / len };\n};\n\nexport const lerp = (a: Vec2, b: Vec2, t: number): Vec2 => ({\n x: a.x + (b.x - a.x) * t,\n y: a.y + (b.y - a.y) * t,\n});\n\n/** Midpoint of two points. */\nexport const midpoint = (a: Vec2, b: Vec2): Vec2 => ({\n x: (a.x + b.x) / 2,\n y: (a.y + b.y) / 2,\n});\n\n/** Angle of the vector from the positive x-axis, in radians (-\u03C0, \u03C0]. */\nexport const angle = (a: Vec2): number => Math.atan2(a.y, a.x);\n\n/** Rotate counterclockwise by `radians` around the origin. */\nexport const rotate = (a: Vec2, radians: number): Vec2 => {\n const c = Math.cos(radians);\n const s = Math.sin(radians);\n return { x: a.x * c - a.y * s, y: a.x * s + a.y * c };\n};\n\n/** Rotate `a` counterclockwise by `radians` around `pivot`. */\nexport const rotateAround = (a: Vec2, pivot: Vec2, radians: number): Vec2 => {\n const c = Math.cos(radians);\n const s = Math.sin(radians);\n const dx = a.x - pivot.x;\n const dy = a.y - pivot.y;\n return { x: pivot.x + (dx * c - dy * s), y: pivot.y + (dx * s + dy * c) };\n};\n\n/** Counterclockwise 90\u00B0 perpendicular. */\nexport const perp = (a: Vec2): Vec2 => ({ x: -a.y, y: a.x });\n\nexport const equals = (a: Vec2, b: Vec2, epsilon = 0): boolean => {\n if (epsilon === 0) return a.x === b.x && a.y === b.y;\n return Math.abs(a.x - b.x) <= epsilon && Math.abs(a.y - b.y) <= epsilon;\n};\n", "import type { Bounds, Transform, Vec2 } from \"@oh-just-another/types\";\n\nexport const IDENTITY: Transform = Object.freeze({ a: 1, b: 0, c: 0, d: 1, e: 0, f: 0 });\n\nexport const of = (\n a: number,\n b: number,\n c: number,\n d: number,\n e: number,\n f: number,\n): Transform => ({ a, b, c, d, e, f });\n\nexport const translation = (tx: number, ty: number): Transform => ({\n a: 1,\n b: 0,\n c: 0,\n d: 1,\n e: tx,\n f: ty,\n});\n\nexport const scaling = (sx: number, sy: number = sx): Transform => ({\n a: sx,\n b: 0,\n c: 0,\n d: sy,\n e: 0,\n f: 0,\n});\n\nexport const rotation = (radians: number): Transform => {\n const cos = Math.cos(radians);\n const sin = Math.sin(radians);\n return { a: cos, b: sin, c: -sin, d: cos, e: 0, f: 0 };\n};\n\n/**\n * Matrix product `a \u00D7 b`. Composition is right-to-left: applying the result\n * to a point is equivalent to applying `b` first, then `a`.\n */\nexport const multiply = (a: Transform, b: Transform): Transform => ({\n a: a.a * b.a + a.c * b.b,\n b: a.b * b.a + a.d * b.b,\n c: a.a * b.c + a.c * b.d,\n d: a.b * b.c + a.d * b.d,\n e: a.a * b.e + a.c * b.f + a.e,\n f: a.b * b.e + a.d * b.f + a.f,\n});\n\nexport const inverse = (t: Transform): Transform => {\n const det = t.a * t.d - t.b * t.c;\n if (det === 0) throw new Error(\"Cannot invert singular matrix\");\n return {\n a: t.d / det,\n b: -t.b / det,\n c: -t.c / det,\n d: t.a / det,\n e: (t.c * t.f - t.d * t.e) / det,\n f: (t.b * t.e - t.a * t.f) / det,\n };\n};\n\nexport const applyToPoint = (t: Transform, p: Vec2): Vec2 => ({\n x: t.a * p.x + t.c * p.y + t.e,\n y: t.b * p.x + t.d * p.y + t.f,\n});\n\n/**\n * Axis-aligned bounding box of `b` after applying `t`. The result is the AABB\n * of the four transformed corners \u2014 tighter approaches exist for pure rotations\n * but this is correct for any affine transform.\n */\nexport const applyToBounds = (t: Transform, b: Bounds): Bounds => {\n const p1 = applyToPoint(t, { x: b.x, y: b.y });\n const p2 = applyToPoint(t, { x: b.x + b.width, y: b.y });\n const p3 = applyToPoint(t, { x: b.x, y: b.y + b.height });\n const p4 = applyToPoint(t, { x: b.x + b.width, y: b.y + b.height });\n const minX = Math.min(p1.x, p2.x, p3.x, p4.x);\n const minY = Math.min(p1.y, p2.y, p3.y, p4.y);\n const maxX = Math.max(p1.x, p2.x, p3.x, p4.x);\n const maxY = Math.max(p1.y, p2.y, p3.y, p4.y);\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n};\n\nexport interface DecomposedTransform {\n readonly translation: Vec2;\n /** Radians, range (-\u03C0, \u03C0]. */\n readonly rotation: number;\n readonly scale: Vec2;\n}\n\n/**\n * Extracts translate / rotate / scale (TRS) from a transform. Assumes the matrix\n * encodes only translate/rotate/uniform-or-axis-aligned-scale (no skew). For matrices\n * with skew the decomposition is approximate.\n */\nexport const decompose = (t: Transform): DecomposedTransform => {\n const sx = Math.sqrt(t.a * t.a + t.b * t.b);\n const sy = Math.sqrt(t.c * t.c + t.d * t.d);\n const det = t.a * t.d - t.b * t.c;\n const sySigned = det < 0 ? -sy : sy;\n return {\n translation: { x: t.e, y: t.f },\n rotation: Math.atan2(t.b, t.a),\n scale: { x: sx, y: sySigned },\n };\n};\n\nexport const equals = (a: Transform, b: Transform, epsilon = 0): boolean => {\n const fields: readonly (keyof Transform)[] = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"];\n if (epsilon === 0) return fields.every((k) => a[k] === b[k]);\n return fields.every((k) => Math.abs(a[k] - b[k]) <= epsilon);\n};\n", "import type { Bounds, Vec2 } from \"@oh-just-another/types\";\n\nexport const EMPTY: Bounds = Object.freeze({ x: 0, y: 0, width: 0, height: 0 });\n\nexport const of = (x: number, y: number, width: number, height: number): Bounds => ({\n x,\n y,\n width,\n height,\n});\n\nexport const fromPoints = (points: readonly Vec2[]): Bounds => {\n if (points.length === 0) return EMPTY;\n let minX = Infinity;\n let minY = Infinity;\n let maxXv = -Infinity;\n let maxYv = -Infinity;\n for (const p of points) {\n if (p.x < minX) minX = p.x;\n if (p.y < minY) minY = p.y;\n if (p.x > maxXv) maxXv = p.x;\n if (p.y > maxYv) maxYv = p.y;\n }\n return { x: minX, y: minY, width: maxXv - minX, height: maxYv - minY };\n};\n\nexport const fromCenter = (center: Vec2, width: number, height: number): Bounds => ({\n x: center.x - width / 2,\n y: center.y - height / 2,\n width,\n height,\n});\n\nexport const centerOf = (b: Bounds): Vec2 => ({\n x: b.x + b.width / 2,\n y: b.y + b.height / 2,\n});\n\nexport const maxX = (b: Bounds): number => b.x + b.width;\nexport const maxY = (b: Bounds): number => b.y + b.height;\n\n/** True if width or height is non-positive. */\nexport const isEmpty = (b: Bounds): boolean => b.width <= 0 || b.height <= 0;\n\nexport const union = (a: Bounds, b: Bounds): Bounds => {\n if (isEmpty(a)) return b;\n if (isEmpty(b)) return a;\n const x = Math.min(a.x, b.x);\n const y = Math.min(a.y, b.y);\n const xMax = Math.max(maxX(a), maxX(b));\n const yMax = Math.max(maxY(a), maxY(b));\n return { x, y, width: xMax - x, height: yMax - y };\n};\n\n/** Returns null if the intersection is empty. */\nexport const intersection = (a: Bounds, b: Bounds): Bounds | null => {\n const x = Math.max(a.x, b.x);\n const y = Math.max(a.y, b.y);\n const xMax = Math.min(maxX(a), maxX(b));\n const yMax = Math.min(maxY(a), maxY(b));\n if (xMax <= x || yMax <= y) return null;\n return { x, y, width: xMax - x, height: yMax - y };\n};\n\nexport const intersects = (a: Bounds, b: Bounds): boolean => intersection(a, b) !== null;\n\nexport const contains = (b: Bounds, point: Vec2): boolean =>\n point.x >= b.x && point.x <= maxX(b) && point.y >= b.y && point.y <= maxY(b);\n\nexport const containsBounds = (outer: Bounds, inner: Bounds): boolean =>\n inner.x >= outer.x &&\n inner.y >= outer.y &&\n maxX(inner) <= maxX(outer) &&\n maxY(inner) <= maxY(outer);\n\nexport const expand = (b: Bounds, padding: number): Bounds => ({\n x: b.x - padding,\n y: b.y - padding,\n width: b.width + 2 * padding,\n height: b.height + 2 * padding,\n});\n\n/** Flips negative width/height so that x/y is the top-left corner. */\nexport const normalize = (b: Bounds): Bounds => ({\n x: b.width < 0 ? b.x + b.width : b.x,\n y: b.height < 0 ? b.y + b.height : b.y,\n width: Math.abs(b.width),\n height: Math.abs(b.height),\n});\n\nexport const equals = (a: Bounds, b: Bounds, epsilon = 0): boolean => {\n if (epsilon === 0) {\n return a.x === b.x && a.y === b.y && a.width === b.width && a.height === b.height;\n }\n return (\n Math.abs(a.x - b.x) <= epsilon &&\n Math.abs(a.y - b.y) <= epsilon &&\n Math.abs(a.width - b.width) <= epsilon &&\n Math.abs(a.height - b.height) <= epsilon\n );\n};\n", "import { req, type Vec2 } from \"@oh-just-another/types\";\n\n/**\n * Offset a closed polygon's vertices along the bisector at each corner.\n * Positive `distance` moves vertices inward (toward the centroid), negative\n * moves outward; both winding orders are handled by projecting the bisector\n * onto the toward-centroid vector and flipping the sign as needed.\n *\n * Each vertex moves `distance / cos(angle/2)` along the bisector of its two\n * adjacent edges (miter offset of the polygon's stroked outline).\n *\n * The bisector clamps at very sharp angles (cos < 1e-6) to avoid pixel-spike\n * artefacts. Concave polygons whose centroid lies outside the polygon can flip\n * inward/outward sign on isolated vertices. Polygons with fewer than 3 vertices\n * are returned unchanged.\n */\nexport const offsetClosedPath = (points: readonly Vec2[], distance: number): Vec2[] => {\n if (points.length < 3 || distance === 0) return points.map((p) => ({ x: p.x, y: p.y }));\n\n // Centroid as an interior reference, used to disambiguate inward / outward\n // direction regardless of vertex winding order.\n let cx = 0;\n let cy = 0;\n for (const p of points) {\n cx += p.x;\n cy += p.y;\n }\n cx /= points.length;\n cy /= points.length;\n\n const n = points.length;\n // Edge unit normals \u2014 rotate each edge vector 90\u00B0.\n const nx = new Array<number>(n);\n const ny = new Array<number>(n);\n for (let i = 0; i < n; i++) {\n const a = req(points[i]);\n const b = req(points[(i + 1) % n]);\n const dx = b.x - a.x;\n const dy = b.y - a.y;\n const len = Math.hypot(dx, dy) || 1;\n nx[i] = -dy / len;\n ny[i] = dx / len;\n }\n\n const out: Vec2[] = [];\n for (let i = 0; i < n; i++) {\n const prev = (i - 1 + n) % n;\n const n1x = req(nx[prev]);\n const n1y = req(ny[prev]);\n const n2x = req(nx[i]);\n const n2y = req(ny[i]);\n let bx = n1x + n2x;\n let by = n1y + n2y;\n const blen = Math.hypot(bx, by);\n if (blen < 1e-6) {\n // 180\u00B0 turn \u2014 bisector ill-defined. Use one of the normals.\n bx = n1x;\n by = n1y;\n } else {\n bx /= blen;\n by /= blen;\n }\n // Inward = toward centroid; check the bisector's component along\n // (centroid - vertex).\n const vertex = req(points[i]);\n const towardCx = cx - vertex.x;\n const towardCy = cy - vertex.y;\n const dot = bx * towardCx + by * towardCy;\n const sign = dot >= 0 ? 1 : -1;\n const cos = bx * n1x + by * n1y;\n const miterLen = cos > 1e-6 ? distance / cos : distance;\n out.push({\n x: vertex.x + sign * bx * miterLen,\n y: vertex.y + sign * by * miterLen,\n });\n }\n return out;\n};\n\n/**\n * Twice the signed polygon area via the shoelace formula. Positive =\n * counter-clockwise in y-up coordinates / clockwise in y-down. Use the sign\n * to detect winding order; abs/2 = polygon area.\n */\nexport const signedArea = (points: readonly Vec2[]): number => {\n let s = 0;\n for (let i = 0; i < points.length; i++) {\n const a = req(points[i]);\n const b = req(points[(i + 1) % points.length]);\n s += a.x * b.y - b.x * a.y;\n }\n return s / 2;\n};\n", "/**\n * Optional host-provided text measurer. The text bounder\n * (`getElementLocalBounds` for `TextElement`) is otherwise purely\n * geometric (`chars \u00D7 fontSize \u00D7 factor`), which can diverge a lot\n * from the actually-rendered width \u2014 the WebGL2 MSDF path draws with a\n * WASM-baked font whose glyph advances differ from any geometric\n * estimate, so the selection box would not hug the text.\n *\n * A host (the interaction layer) injects a measurer backed by the\n * renderer's `measureText` (which itself matches the active text\n * backend). When set, the bounder uses it for accurate width; when\n * absent (headless / tests), it falls back to the geometric estimate.\n *\n * Returns the measured width in world px, or `null` to defer to the\n * geometric estimate for that call. `opts` carries weight/style so the\n * measurer matches the *rendered* width \u2014 bold/italic change glyph\n * advances, so without it the bounds lag behind a bolded text and the\n * glyphs overflow the element box.\n */\nexport interface TextMeasureOpts {\n readonly bold?: boolean;\n readonly italic?: boolean;\n}\n\nexport type TextMeasurer = (\n text: string,\n fontFamily: string,\n fontSize: number,\n opts?: TextMeasureOpts,\n) => number | null;\n\nlet activeMeasurer: TextMeasurer | null = null;\n\n/** Install (or clear, with `null`) the active text measurer. */\nexport const setTextMeasurer = (measurer: TextMeasurer | null): void => {\n activeMeasurer = measurer;\n};\n\n/** The active text measurer, or `null` when none is installed. */\nexport const getTextMeasurer = (): TextMeasurer | null => activeMeasurer;\n", "import type { Bounds, FileId, LayerId, ElementId, Vec2 } from \"@oh-just-another/types\";\nimport type { FractionalIndex } from \"fractional-keys\";\nimport { bounds as B } from \"@oh-just-another/math\";\nimport type { AnchorRef } from \"../edges/edge.js\";\nimport type { Style, TextStyle } from \"../text/style.js\";\nimport type { TextRun } from \"../text/text-runs.js\";\nimport { TEXT_APPROX_CHAR_WIDTH_FACTOR, TEXT_LINE_HEIGHT_FACTOR } from \"../constants.js\";\nimport { getTextMeasurer } from \"../text/text-measure.js\";\nimport { pickTextPlaceholder } from \"../text/placeholder.js\";\n\n/**\n * Fields shared by every shape variant. `order` is a fractional-index string\n * used for z-ordering within the parent layer \u2014 insertions are O(1) and never\n * require renumbering neighbors, which keeps history small and is conflict-free\n * under concurrent edits.\n */\nexport interface ElementBase {\n readonly id: ElementId;\n readonly layerId: LayerId;\n /** Discriminator. Built-in shapes use the literal types declared below. */\n readonly type: string;\n /** Local-space origin. The shape is rotated/scaled around this point. */\n readonly position: Vec2;\n /** Rotation in radians, counter-clockwise. */\n readonly rotation: number;\n readonly scale: Vec2;\n /** Z-order key within `layerId`. */\n readonly order: FractionalIndex;\n readonly style: Style;\n /** Free-form metadata for plugins; the kernel never reads from here. */\n readonly metadata?: Readonly<Record<string, unknown>>;\n\n /**\n * Interactive-resize size constraints in local pixels. The editor clamps\n * the shape's width/height into [min, max] after every resize gesture.\n * Omitted = no constraint on that axis.\n */\n readonly minWidth?: number;\n readonly minHeight?: number;\n readonly maxWidth?: number;\n readonly maxHeight?: number;\n\n /**\n * If true, interactive resize is prevented from dragging through the\n * opposite edge \u2014 the shape cannot be mirrored by overshooting a handle.\n * `width` / `height` are clamped to a non-negative range (or `minWidth` /\n * `minHeight` if set). Defaults to `false`.\n */\n readonly noFlip?: boolean;\n\n /**\n * Custom named connection points on this shape, on top of the 9 standard\n * anchors (`top-left` / `top` / `top-right` / `right` / `bottom-right` /\n * `bottom` / `bottom-left` / `left` / `center`). Entries with a standard\n * name override the standard placement; new names add fresh ports.\n *\n * Values are `AnchorRef`s \u2014 `ratio` keeps the point proportional to the\n * shape's bounds, `absolute` pins it at a fixed pixel offset. Resolve\n * an anchor through `getAnchorLocal` / `getAnchorWorld`.\n */\n readonly anchors?: Readonly<Record<string, AnchorRef>>;\n\n /**\n * Optional parent shape id. When set, the shape is considered part of\n * the parent's group: hit-test and drag operations promote selection\n * to the parent (grouped), and `moveSelectionBy` translates every\n * descendant in lockstep. The kernel does not enforce a particular\n * shape type for parents \u2014 `GroupElement` (type `\"group\"`) is just the\n * default zero-render container; custom shape types can also act as\n * parents.\n */\n readonly parentId?: ElementId;\n\n /**\n * Frame membership \u2014 modern-style. Distinct from `parentId`\n * (which is for groups and containers). Children of a frame are\n * NOT nested in its `children` list; they're flat in the scene\n * but share `frameId === frame.id`. Move-by-drag of the frame\n * translates every shape with the matching frameId; export-by-\n * frame uses the frame's bounds as the crop region.\n */\n readonly frameId?: ElementId;\n\n /**\n * Per-shape lock flag. Locked shapes ignore all interactive gestures\n * (hit-test pretends they're not there for clicks / drags / resize),\n * but still render and remain serialisable. Propagates to\n * descendants: if any ancestor in the `parentId` chain is locked,\n * the shape is effectively locked. Use `isElementLocked(scene, shape)`\n * to consult the propagated state.\n *\n * Independent from `Layer.locked` \u2014 both gate interactions; either\n * one being true is enough to lock.\n */\n readonly locked?: boolean;\n\n /**\n * Per-shape visibility flag. Hidden shapes do not render and do not\n * receive interactions. Propagates to descendants like `locked`.\n * Use `isElementHidden(scene, shape)` to consult the propagated state.\n *\n * Independent from `Layer.visible` \u2014 either being false hides the\n * shape.\n */\n readonly hidden?: boolean;\n\n /**\n * Embedded text label \u2014 the shape's own text content, drawn inside its\n * bounds (wrapped to the width, aligned via `style.textAlign` /\n * `style.textBaseline`, `middle`+`center` by default). Shares the text\n * element's building blocks (styled runs, list paragraphs) as data;\n * layout is the renderer's job. Double-click opens the inline editor\n * on shapes that support it (see `canCarryLabel`).\n */\n readonly label?: ShapeLabel;\n\n /**\n * Element-level hyperlink. Any shape \u2014 text, image,\n * rectangle \u2014 can carry one. The host opens it on Cmd/Ctrl-click or via\n * the hover link-popup. Stored verbatim; the host MUST validate the\n * scheme before navigating (only `http`/`https`/`mailto` \u2014 never\n * `javascript:`). Per-fragment links inside text are a separate\n * rich-text feature.\n */\n readonly href?: string;\n}\n\nexport interface RectangleElement extends ElementBase {\n readonly type: \"rectangle\";\n readonly width: number;\n readonly height: number;\n}\n\nexport interface EllipseElement extends ElementBase {\n readonly type: \"ellipse\";\n readonly width: number;\n readonly height: number;\n}\n\nexport interface PolygonElement extends ElementBase {\n readonly type: \"polygon\";\n /** Closed polygon in local coordinates (origin = `position`). */\n readonly points: readonly Vec2[];\n}\n\nexport type PathCommand =\n | { readonly kind: \"M\"; readonly to: Vec2 }\n | { readonly kind: \"L\"; readonly to: Vec2 }\n | { readonly kind: \"Q\"; readonly control: Vec2; readonly to: Vec2 }\n | { readonly kind: \"C\"; readonly control1: Vec2; readonly control2: Vec2; readonly to: Vec2 }\n | { readonly kind: \"Z\" };\n\nexport interface PathElement extends ElementBase {\n readonly type: \"path\";\n /** Commands in local coordinates. */\n readonly commands: readonly PathCommand[];\n}\n\nexport interface TextElement extends ElementBase {\n readonly type: \"text\";\n readonly text: string;\n readonly fontFamily: string;\n readonly fontSize: number;\n /** Width budget for wrapping; `undefined` = single line. */\n readonly maxWidth?: number;\n readonly style: TextStyle;\n /**\n * Optional styled-run overlay for rich text. Each run styles a contiguous\n * substring; `runs.map(r => r.text).join(\"\")` MUST equal `text`, which\n * stays the flat source of truth. Omitted (or empty) = uniform styling\n * (renders exactly like a plain text block). See {@link TextRun}.\n */\n readonly runs?: readonly TextRun[];\n /**\n * Optional per-paragraph attributes (lists / nesting), aligned by index\n * with `text.split(\"\\n\")`. A shorter array leaves the trailing\n * paragraphs plain; omitted = every paragraph plain. Numbering for\n * `\"numbered\"` items is derived at render time (consecutive numbered\n * paragraphs at the same indent count up), never stored.\n */\n readonly paragraphs?: readonly TextParagraph[];\n}\n\n/**\n * Embedded text carried by a non-text shape (see `ElementBase.label`).\n * Field-for-field compatible with the text element's content model so\n * the text pipeline (runs, paragraphs, layout, inline editing) applies\n * unchanged.\n */\nexport interface ShapeLabel {\n readonly text: string;\n readonly fontFamily: string;\n readonly fontSize: number;\n /**\n * Auto-fit mode (sticky notes): the RENDERED font size is derived so\n * the text fills the shape body, scaling with the shape; `fontSize`\n * then only serves as the fallback / upper hint. Picking an explicit\n * size in the toolbar clears the flag.\n */\n readonly autoFit?: boolean;\n /** Optional style overlay; `textAlign`/`textBaseline` default to center/middle. */\n readonly style?: TextStyle;\n readonly runs?: readonly TextRun[];\n readonly paragraphs?: readonly TextParagraph[];\n}\n\n/**\n * Sticky note \u2014 a bounded card whose text lives in the shared embedded\n * `label` (double-click to edit). Background comes from `style.fill`;\n * `authorName` renders along the bottom edge when `showAuthor` is on.\n * Registered as a plugin-style type: not part of the built-in `Element`\n * union, handled through the renderer / bounder registries and the\n * custom-element wire schema.\n */\nexport interface StickyElement extends ElementBase {\n readonly type: \"sticky\";\n readonly width: number;\n readonly height: number;\n readonly authorName?: string;\n readonly showAuthor?: boolean;\n /** Free-form tags, rendered as small pills along the bottom edge. */\n readonly tags?: readonly string[];\n /**\n * Emoji reactions. Each glyph tracks WHO reacted (`users` \u2014 collab\n * user ids); the visible counter is `users.length`. A user's click on\n * a glyph they already reacted with removes their reaction (toggle),\n * anyone else's click adds theirs. The add button lives in the host\n * UI at the sticky's bottom-left corner.\n */\n readonly reactions?: readonly { readonly glyph: string; readonly users: readonly string[] }[];\n}\n\n/** True when the shape is a sticky note. */\nexport const isSticky = (shape: ElementBase): shape is StickyElement => shape.type === \"sticky\";\n\n/**\n * Emoji element \u2014 a single glyph drawn at `size` world units. The glyph\n * is replaced via the toolbar picker. Plugin-style type like `sticky`.\n */\nexport interface EmojiElement extends ElementBase {\n readonly type: \"emoji\";\n readonly glyph: string;\n readonly size: number;\n}\n\n/** True when the shape is an emoji element. */\nexport const isEmoji = (shape: ElementBase): shape is EmojiElement => shape.type === \"emoji\";\n\n/** Shape types whose body can host an embedded label. */\nconst LABELABLE_TYPES: ReadonlySet<string> = new Set([\n \"rectangle\",\n \"ellipse\",\n \"polygon\",\n \"block-arrow\",\n \"sticky\",\n]);\n\n/** True when the shape's type supports an embedded text label. */\nexport const canCarryLabel = (shape: ElementBase): boolean => LABELABLE_TYPES.has(shape.type);\n\n/**\n * Paragraph-level attributes for a {@link TextElement}. Both fields are\n * optional so plain paragraphs serialize as `{}` (or are omitted entirely\n * via a short array).\n */\nexport interface TextParagraph {\n /** List marker kind; omitted = plain paragraph. */\n readonly list?: \"bullet\" | \"numbered\";\n /** 0-based nesting level. Omitted = 0. */\n readonly indent?: number;\n}\n\n/**\n * Normalised crop rectangle for an {@link ImageElement}. All four\n * values are fractions in `[0, 1]` of the source image's intrinsic\n * dimensions: `{ x: 0, y: 0, width: 1, height: 1 }` shows the whole\n * image (equivalent to omitting `crop`). The cropped source region is\n * stretched to fill the element's `width` \u00D7 `height` box, so cropping\n * does not change the element's on-canvas footprint \u2014 only which part\n * of the bitmap is visible. Being normalised keeps the crop stable when\n * the backing file is swapped for a differently-sized copy.\n */\nexport interface ImageCrop {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Shape mask applied to an image element's BOX (after crop): pixels\n * outside the mask are clipped away by the renderer (`RenderTarget.clip`\n * \u2014 canvas2d clip / svg clipPath / webgl2 stencil). Coordinates are\n * normalised to the element box (0..1 on both axes), so the mask scales\n * with the shape. Additive: scenes and renderers that predate masks\n * ignore it and draw the full box.\n *\n * - `ellipse` \u2014 inscribed ellipse (circle on a square box).\n * - `round-rect` \u2014 rounded rectangle; `radius` is a fraction of the\n * SHORTER box side (0..0.5; 0.5 = capsule).\n * - `polygon` \u2014 arbitrary closed ring of normalised points (\u2265 3). The\n * built-in presets live in {@link IMAGE_MASK_POLYGON_PRESETS}.\n */\nexport type ImageMask =\n | { readonly kind: \"ellipse\" }\n | { readonly kind: \"round-rect\"; readonly radius: number }\n | { readonly kind: \"polygon\"; readonly points: readonly Vec2[] };\n\nexport interface ImageElement extends ElementBase {\n readonly type: \"image\";\n /**\n * URL or data-URI. Used for remote-host / SVG images that don't need\n * binary registration. Setting `fileId` instead points at a\n * `Scene.files` entry, which keeps scene.json small for large bitmaps.\n */\n readonly src: string;\n /**\n * Optional normalised source-crop rectangle. Omitted = whole image.\n * See {@link ImageCrop}. Additive: scenes and renderers that predate\n * cropping simply ignore it and draw the full bitmap.\n */\n readonly crop?: ImageCrop;\n /**\n * Optional shape mask clipping the drawn box. Omitted = no mask.\n * Independent of (and applied after) `crop`. See {@link ImageMask}.\n */\n readonly mask?: ImageMask;\n /**\n * Id of the `BinaryFile` in `Scene.files` that backs this image.\n * When present, hosts should resolve through the file registry\n * (creates an object-URL or ImageBitmap on demand); `src` stays\n * around as a fallback for the static renderer path.\n */\n readonly fileId?: FileId;\n readonly width: number;\n readonly height: number;\n /**\n * Accessible description of the image content. Surfaced as `<title>`\n * in SVG output and available to hosts for `aria` wiring. Omitted =\n * decorative / undescribed.\n */\n readonly alt?: string;\n /**\n * Animated-content hint (opt-in). When set, the\n * renderer's image path consults `getAnimationAdapter(kind)`\n * to fetch the current frame's image source instead of using\n * `src` directly. Hosts that don't register an adapter for the\n * kind get a static fallback (src as-is). The actual frame data\n * lives in `animationData` \u2014 opaque to the kernel, decoded by\n * the adapter.\n *\n * Built-in kinds: \"gif\" (host plugs `gifuct-js`), \"lottie\"\n * (host plugs `lottie-web`), \"video\" (host plugs an\n * `HTMLVideoElement`). No adapters ship in the kernel \u2014\n * registration is per-host.\n */\n readonly animationKind?: string;\n readonly animationData?: unknown;\n}\n\n/**\n * Composite shape backed by a rich template (`@oh-just-another/templates`). The\n * scene stores only the binding (`templateId` + `data`) plus a fixed box\n * size \u2014 layout, hit-test and rendering live in the templates package.\n *\n * The kernel ships a basic bounder (uses `width` \u00D7 `height`); the templates\n * package can re-register a tighter bounder that respects the layout engine.\n */\nexport interface TemplateElement extends ElementBase {\n readonly type: \"template\";\n readonly templateId: string;\n readonly data: Readonly<Record<string, unknown>>;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Variable-width brush stroke. Each `BrushPoint` carries its own width\n * (typically derived from `PointerEvent.pressure \u00D7 MAX_BRUSH_WIDTH`).\n * The renderer interpolates between consecutive widths along the path.\n *\n * Coordinates are local; the shape's `position` / `rotation` / `scale`\n * apply on top, same as any other shape variant.\n */\nexport interface BrushPoint {\n readonly x: number;\n readonly y: number;\n /** Stroke half-width in local pixels at this vertex. */\n readonly width: number;\n}\n\nexport interface BrushElement extends ElementBase {\n readonly type: \"brush\";\n readonly points: readonly BrushPoint[];\n /**\n * A closed stroke: its ends meet, so the renderer fills the area enclosed by\n * the centreline with `style.fill` (under the variable-width stroke body).\n * Set on commit only when a fill colour is chosen and the stroke loops back\n * on itself. Omitted (undefined) for ordinary open strokes.\n */\n readonly closed?: boolean;\n /**\n * Raw input pressure (0\u20131) per point, aligned 1:1 with `points`. The baked\n * `width` is derived from it (base width \u00D7 pressure curve \u00D7 taper), so a\n * stroke can be regenerated with a different base width / thinning without\n * re-capturing. Omitted on strokes committed before pressures were stored.\n */\n readonly pressures?: readonly number[];\n /**\n * True when `pressures` were synthesised from pointer speed (mouse / touch\n * without a real pressure channel) rather than read from the device. Lets a\n * regeneration pass know whether re-simulating is appropriate.\n */\n readonly simulatePressure?: boolean;\n /**\n * The brush base half-width (local px) the stroke was committed with \u2014 the\n * value the pressure curve scaled toward. Omitted on legacy strokes.\n */\n readonly baseWidth?: number;\n}\n\n/**\n * Container shape that holds children via the shared `parentId` link.\n * Rendered as a no-op (the group itself has no visual); the editor's\n * overlay highlights the union AABB of the children when selected.\n */\nexport interface GroupElement extends ElementBase {\n readonly type: \"group\";\n}\n\n/**\n * Frame element \u2014 modern-style visual container that groups\n * shapes via a separate `frameId` link (NOT `parentId`). Drawn as\n * a dashed rectangle with a header title; clicks pass through to\n * children. Move-by-drag translates every shape whose `frameId`\n * matches; the export pipeline can crop to the frame's bounds.\n *\n * Auto-numbering: the editor picks the next free \"Frame N\" on\n * create. Custom `name` overrides.\n */\nexport interface FrameElement extends ElementBase {\n readonly type: \"frame\";\n readonly width: number;\n readonly height: number;\n /** Visible header label. */\n readonly name?: string;\n}\n\n/**\n * Filled arrow drawn as a single shape (body rectangle + triangular\n * head, optionally with a triangular tail). Distinct from an Link:\n * edges connect anchors and re-route on shape move; a BlockArrowElement\n * is a free-standing element with a fixed silhouette like a block-arrow icon.\n */\nexport interface BlockArrowElement extends ElementBase {\n readonly type: \"block-arrow\";\n readonly width: number;\n readonly height: number;\n /**\n * Where the arrow points. Default `\"right\"`. Rotation is still\n * applied on top via `ElementBase.rotation` \u2014 this enum just picks\n * the head side in local coords so the user can quickly toggle\n * direction without typing a 90/180/270 deg angle.\n */\n readonly direction?: \"right\" | \"left\" | \"up\" | \"down\";\n /** Ratio of the head length over the total length (0..0.9). Default 0.4. */\n readonly headRatio?: number;\n /** Ratio of the body thickness over the perpendicular dimension. Default 0.5. */\n readonly bodyThickness?: number;\n}\n\nexport type BuiltinElement =\n | RectangleElement\n | EllipseElement\n | PolygonElement\n | PathElement\n | TextElement\n | ImageElement\n | TemplateElement\n | GroupElement\n | FrameElement\n | BlockArrowElement\n | BrushElement;\n\n/**\n * Open shape type. `Element` accepts any `ElementBase` extension, which lets plugins\n * register their own types without amending this union. The kernel treats\n * unknown shape types via the bounder registry \u2014 see `registerBounder`.\n */\nexport type Element = BuiltinElement | ElementBase;\n\n// --- type guards ---\n\nexport const isRectangle = (s: ElementBase): s is RectangleElement => s.type === \"rectangle\";\nexport const isEllipse = (s: ElementBase): s is EllipseElement => s.type === \"ellipse\";\nexport const isPolygon = (s: ElementBase): s is PolygonElement => s.type === \"polygon\";\nexport const isPath = (s: ElementBase): s is PathElement => s.type === \"path\";\nexport const isText = (s: ElementBase): s is TextElement => s.type === \"text\";\nexport const isImage = (s: ElementBase): s is ImageElement => s.type === \"image\";\nexport const isTemplate = (s: ElementBase): s is TemplateElement => s.type === \"template\";\nexport const isGroup = (s: ElementBase): s is GroupElement => s.type === \"group\";\nexport const isFrame = (s: ElementBase): s is FrameElement => s.type === \"frame\";\nexport const isBlockArrow = (s: ElementBase): s is BlockArrowElement => s.type === \"block-arrow\";\nexport const isBrush = (s: ElementBase): s is BrushElement => s.type === \"brush\";\n\n/**\n * The colour the variable-width brush BODY is painted with: the line colour\n * (`style.stroke`, set by the drawing panel), falling back to `style.fill` for\n * strokes authored before the stroke/fill split (their line lived in `fill`),\n * then to opaque black. Shared by the committed-stroke renderer and the live\n * overlay preview so the two never diverge.\n */\nexport const brushBodyColor = (style: Style): string => style.stroke ?? style.fill ?? \"#000\";\n\n// --- bounder registry ---\n\n/**\n * Computes the *local* bounds of a shape \u2014 its AABB in local coordinates,\n * before `position`/`rotation`/`scale` are applied. The world AABB lives in\n * `getElementWorldBounds`.\n */\nexport type ElementBounder<S extends ElementBase = ElementBase> = (shape: S) => Bounds;\n\nconst bounderRegistry = new Map<string, ElementBounder>();\n\n/**\n * Register a bounder for a custom shape type. Plugins call this once at module\n * load. The kernel ships bounders for every `BuiltinElement`.\n */\nexport const registerBounder = <S extends ElementBase>(\n type: S[\"type\"],\n bounder: ElementBounder<S>,\n): void => {\n bounderRegistry.set(type, bounder as ElementBounder);\n};\n\n/** Look up a registered bounder. Returns `undefined` for unknown shape types. */\nexport const getBounder = (type: string): ElementBounder | undefined => bounderRegistry.get(type);\n\n/**\n * Local AABB for any shape with a registered bounder. Throws on unknown types\n * \u2014 callers should either register a bounder or filter unknown shapes out.\n */\nexport const getElementLocalBounds = (shape: ElementBase): Bounds => {\n const bounder = bounderRegistry.get(shape.type);\n if (!bounder) {\n throw new Error(`No bounder registered for shape type: ${shape.type}`);\n }\n return bounder(shape);\n};\n\n/**\n * World-space AABB after `position`/`rotation`/`scale`. This is the conservative\n * AABB of the rotated/scaled local box, suitable for spatial-index keys.\n */\nexport const getElementWorldBounds = (shape: ElementBase): Bounds => {\n const local = getElementLocalBounds(shape);\n // Transform 4 corners then re-AABB.\n const corners: readonly Vec2[] = [\n { x: local.x, y: local.y },\n { x: local.x + local.width, y: local.y },\n { x: local.x, y: local.y + local.height },\n { x: local.x + local.width, y: local.y + local.height },\n ];\n const sin = Math.sin(shape.rotation);\n const cos = Math.cos(shape.rotation);\n const transformed = corners.map((p) => {\n const sx = p.x * shape.scale.x;\n const sy = p.y * shape.scale.y;\n return {\n x: shape.position.x + (sx * cos - sy * sin),\n y: shape.position.y + (sx * sin + sy * cos),\n };\n });\n return B.fromPoints(transformed);\n};\n\n// --- built-in bounders ---\n\nregisterBounder<RectangleElement>(\"rectangle\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\nregisterBounder<EllipseElement>(\"ellipse\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\nregisterBounder<PolygonElement>(\"polygon\", (s) => B.fromPoints(s.points));\n\nregisterBounder<StickyElement>(\"sticky\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\nregisterBounder<EmojiElement>(\"emoji\", (s) => ({ x: 0, y: 0, width: s.size, height: s.size }));\n\nregisterBounder<PathElement>(\"path\", (s) => {\n const points: Vec2[] = [];\n let cursor: Vec2 = { x: 0, y: 0 };\n for (const cmd of s.commands) {\n switch (cmd.kind) {\n case \"M\":\n case \"L\":\n points.push(cmd.to);\n cursor = cmd.to;\n break;\n case \"Q\":\n points.push(cmd.control, cmd.to);\n cursor = cmd.to;\n break;\n case \"C\":\n points.push(cmd.control1, cmd.control2, cmd.to);\n cursor = cmd.to;\n break;\n case \"Z\":\n // closes the subpath, no new points\n break;\n }\n }\n // suppress unused-var warning\n void cursor;\n return B.fromPoints(points);\n});\n\nregisterBounder<TextElement>(\"text\", (s) => {\n // Width comes from the host measurer when installed (matches the\n // actually-rendered glyph advances), falling back to a geometric\n // estimate (`chars \u00D7 fontSize \u00D7 factor`) headless / in tests. Height\n // is line count \u00D7 line-height; hard newlines honoured in both modes.\n const lineHeight = s.fontSize * TEXT_LINE_HEIGHT_FACTOR;\n // An empty element is sized by its placeholder prompt (the renderer draws\n // it while the text is being written), so the selection box and the\n // dirty rect cover exactly what is on screen; the box snaps to the real\n // text from the first keystroke.\n const paragraphs = (s.text === \"\" ? pickTextPlaceholder(s.id) : s.text).split(\"\\n\");\n const measurer = getTextMeasurer();\n // Pass weight/style so the measured width matches the rendered (bold /\n // italic) glyphs \u2014 otherwise the box wouldn't grow when text is bolded.\n const opts = {\n bold: s.style.fontWeight === \"bold\",\n italic: s.style.fontStyle === \"italic\",\n };\n const measureLine = (line: string): number => {\n if (measurer) {\n const w = measurer(line, s.fontFamily, s.fontSize, opts);\n if (w !== null) return w;\n }\n return line.length * s.fontSize * TEXT_APPROX_CHAR_WIDTH_FACTOR;\n };\n if (s.maxWidth === undefined) {\n // Auto-width: widest paragraph drives width, one visual line per\n // paragraph.\n let width = 0;\n for (const p of paragraphs) width = Math.max(width, measureLine(p));\n width = Math.max(width, s.fontSize * 0.5);\n return { x: 0, y: 0, width, height: Math.max(1, paragraphs.length) * lineHeight };\n }\n // Fixed-width: width is the budget; height \u2248 wrapped line count.\n let lines = 0;\n for (const p of paragraphs) {\n lines += Math.max(1, Math.ceil(measureLine(p) / s.maxWidth));\n }\n return { x: 0, y: 0, width: s.maxWidth, height: Math.max(1, lines) * lineHeight };\n});\n\nregisterBounder<ImageElement>(\"image\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\n// Built-in template bounder: uses the explicit `width` \u00D7 `height` box. The\n// templates package can re-register a tighter bounder driven by the layout\n// engine when an instance is auto-sized.\nregisterBounder<TemplateElement>(\"template\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\nregisterBounder<BrushElement>(\"brush\", (s) => {\n if (s.points.length === 0) return { x: 0, y: 0, width: 0, height: 0 };\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n for (const p of s.points) {\n if (p.x - p.width < minX) minX = p.x - p.width;\n if (p.y - p.width < minY) minY = p.y - p.width;\n if (p.x + p.width > maxX) maxX = p.x + p.width;\n if (p.y + p.width > maxY) maxY = p.y + p.width;\n }\n return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n});\n\n// Group shapes have no intrinsic geometry \u2014 their world AABB is empty.\n// Callers that need the union of descendants must walk `parentId` via\n// `getChildrenOf` and union the children's world bounds instead.\nregisterBounder<GroupElement>(\"group\", () => ({ x: 0, y: 0, width: 0, height: 0 }));\n\nregisterBounder<FrameElement>(\"frame\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n\nregisterBounder<BlockArrowElement>(\"block-arrow\", (s) => ({\n x: 0,\n y: 0,\n width: s.width,\n height: s.height,\n}));\n", "import type { FractionalIndex } from \"fractional-keys\";\n\n/** Compare by fractional `order`, ascending (bottom-to-top z-order). */\nexport const byOrderAsc = <T extends { readonly order: FractionalIndex }>(a: T, b: T): number =>\n a.order < b.order ? -1 : a.order > b.order ? 1 : 0;\n\n/** Compare by fractional `order`, descending (top-to-bottom). */\nexport const byOrderDesc = <T extends { readonly order: FractionalIndex }>(a: T, b: T): number =>\n byOrderAsc(b, a);\n", "import type { Bounds, FileId, LinkId, LayerId, ElementId, Vec2 } from \"@oh-just-another/types\";\nimport { bounds as B } from \"@oh-just-another/math\";\nimport type { Link } from \"../edges/edge.js\";\nimport type { Layer } from \"../model/layer.js\";\nimport type { Scene } from \"../model/scene.js\";\nimport {\n MAX_PARENT_DEPTH,\n SELECTION_OUTLINE_CORNER_SAMPLES,\n SELECTION_OUTLINE_CURVE_SAMPLES,\n SELECTION_OUTLINE_ELLIPSE_SAMPLES,\n SPATIAL_GRID_CELL_SIZE,\n} from \"../constants.js\";\nimport {\n getElementWorldBounds,\n getElementLocalBounds,\n isPolygon,\n isEllipse,\n isRectangle,\n isImage,\n isText,\n isPath,\n isGroup,\n type Element,\n type PathCommand,\n} from \"../shapes/shape.js\";\nimport { getCornerRadius } from \"../text/style.js\";\nimport { SpatialGrid } from \"./spatial.js\";\nimport { byOrderAsc } from \"../model/order.js\";\nimport { localToWorld } from \"../shapes/shape-transform.js\";\nimport { ellipseOutlinePoint } from \"../shapes/ellipse.js\";\n\n// --- direct lookups ---\n\nexport const getElement = (scene: Scene, id: ElementId): Element | undefined =>\n scene.elements.get(id);\n\nexport const getLink = (scene: Scene, id: LinkId): Link | undefined => scene.links.get(id);\n\nexport const getLayer = (scene: Scene, id: LayerId): Layer | undefined => scene.layers.get(id);\n\n// --- iteration in z-order ---\n\n/**\n * Layers sorted bottom-to-top by their `order` field. Stable for equal orders\n * (which should not happen in practice with fractional indices).\n */\nexport const getLayersInOrder = (scene: Scene): readonly Layer[] =>\n [...scene.layers.values()].sort(byOrderAsc);\n\n/** Shapes in `layerId`, sorted bottom-to-top by `order`. */\nexport const getElementsInLayer = (scene: Scene, layerId: LayerId): readonly Element[] =>\n [...scene.elements.values()].filter((s) => s.layerId === layerId).sort(byOrderAsc);\n\nexport const getLinksInLayer = (scene: Scene, layerId: LayerId): readonly Link[] =>\n [...scene.links.values()].filter((e) => e.layerId === layerId).sort(byOrderAsc);\n\n// --- selection outline (contour) ---\n\nconst rectLoop = (b: Bounds): Vec2[] => [\n { x: b.x, y: b.y },\n { x: b.x + b.width, y: b.y },\n { x: b.x + b.width, y: b.y + b.height },\n { x: b.x, y: b.y + b.height },\n];\n\n/** Rounded-rect outline as a polyline \u2014 straight edges + sampled corner arcs. */\nconst roundedRectLoop = (b: Bounds, r: number): Vec2[] => {\n const { x, y, width: w, height: h } = b;\n const arc = (cx: number, cy: number, from: number, to: number): Vec2[] => {\n const pts: Vec2[] = [];\n for (let i = 0; i <= SELECTION_OUTLINE_CORNER_SAMPLES; i++) {\n const a = from + (to - from) * (i / SELECTION_OUTLINE_CORNER_SAMPLES);\n pts.push({ x: cx + r * Math.cos(a), y: cy + r * Math.sin(a) });\n }\n return pts;\n };\n const HALF_PI = Math.PI / 2;\n return [\n // top-left \u2192 top-right \u2192 bottom-right \u2192 bottom-left corners (clockwise).\n ...arc(x + r, y + r, Math.PI, Math.PI + HALF_PI),\n ...arc(x + w - r, y + r, -HALF_PI, 0),\n ...arc(x + w - r, y + h - r, 0, HALF_PI),\n ...arc(x + r, y + h - r, HALF_PI, Math.PI),\n ];\n};\n\nconst flattenPath = (commands: readonly PathCommand[]): Vec2[] => {\n const pts: Vec2[] = [];\n let cur: Vec2 = { x: 0, y: 0 };\n for (const c of commands) {\n if (c.kind === \"M\" || c.kind === \"L\") {\n cur = c.to;\n pts.push({ x: cur.x, y: cur.y });\n } else if (c.kind === \"Q\") {\n for (let i = 1; i <= SELECTION_OUTLINE_CURVE_SAMPLES; i++) {\n const t = i / SELECTION_OUTLINE_CURVE_SAMPLES;\n const u = 1 - t;\n pts.push({\n x: u * u * cur.x + 2 * u * t * c.control.x + t * t * c.to.x,\n y: u * u * cur.y + 2 * u * t * c.control.y + t * t * c.to.y,\n });\n }\n cur = c.to;\n } else if (c.kind === \"C\") {\n for (let i = 1; i <= SELECTION_OUTLINE_CURVE_SAMPLES; i++) {\n const t = i / SELECTION_OUTLINE_CURVE_SAMPLES;\n const u = 1 - t;\n pts.push({\n x:\n u ** 3 * cur.x +\n 3 * u * u * t * c.control1.x +\n 3 * u * t * t * c.control2.x +\n t ** 3 * c.to.x,\n y:\n u ** 3 * cur.y +\n 3 * u * u * t * c.control1.y +\n 3 * u * t * t * c.control2.y +\n t ** 3 * c.to.y,\n });\n }\n cur = c.to;\n }\n // \"Z\" closes implicitly \u2014 each loop is closed by the consumer.\n }\n return pts;\n};\n\n/**\n * Outline provider for a custom / composite element type \u2014 returns the\n * shape's contour as one or more LOCAL-space loops (pre transform). Lets a\n * plugin element made of several visually-disconnected figures (e.g. two\n * unconnected ellipses, no background) supply a multi-loop selection halo\n * instead of falling back to its bounding box. Registered by `shape.type`.\n */\nexport type ElementOutlineProvider = (shape: Element) => Vec2[][];\n\nconst outlineProviders = new Map<string, ElementOutlineProvider>();\n\n/** Register a multi-loop outline provider for a custom element `type`. */\nexport const registerElementOutline = (type: string, provider: ElementOutlineProvider): void => {\n outlineProviders.set(type, provider);\n};\n\n/** Local-space outline loop(s) for a single (non-group) shape, or `null`. */\nconst localOutlineLoops = (shape: Element): Vec2[][] | null => {\n if (isPolygon(shape)) return [shape.points.map((p) => ({ x: p.x, y: p.y }))];\n if (isEllipse(shape)) {\n const b = getElementLocalBounds(shape);\n const cx = b.x + b.width / 2;\n const cy = b.y + b.height / 2;\n const rx = b.width / 2;\n const ry = b.height / 2;\n const pts: Vec2[] = [];\n for (let i = 0; i < SELECTION_OUTLINE_ELLIPSE_SAMPLES; i++) {\n pts.push(ellipseOutlinePoint(cx, cy, rx, ry, i / SELECTION_OUTLINE_ELLIPSE_SAMPLES));\n }\n return [pts];\n }\n if (isRectangle(shape)) {\n const b = getElementLocalBounds(shape);\n const r = getCornerRadius(shape.style.roundness, b.width, b.height);\n return [r > 0 ? roundedRectLoop(b, r) : rectLoop(b)];\n }\n if (isImage(shape) || isText(shape)) {\n return [rectLoop(getElementLocalBounds(shape))];\n }\n if (isPath(shape)) return [flattenPath(shape.commands)];\n // group handled by the caller; brush / template / custom \u2192 bbox fallback.\n return null;\n};\n\n/**\n * World-space outline loop(s) tracing a shape's actual contour, for the\n * selection halo. polygon (star / diamond / hexagon) is exact; ellipse and\n * path are sampled; a group returns one loop per descendant (handles\n * visually-disconnected figures). Shapes without known geometry (composite\n * template, brush, custom) fall back to their world bounding box. Cheap\n * enough to recompute every frame \u2014 no baking needed.\n */\nexport const getElementOutline = (scene: Scene, shape: Element): Vec2[][] => {\n if (isGroup(shape)) {\n const loops: Vec2[][] = [];\n for (const child of getChildrenOf(scene, shape.id))\n loops.push(...getElementOutline(scene, child));\n return loops;\n }\n const local = localOutlineLoops(shape);\n if (local) return local.map((loop) => loop.map((p) => localToWorld(shape, p)));\n // Custom / composite type with a registered outline provider (multi-loop).\n const provider = outlineProviders.get(shape.type);\n if (provider) {\n const loops = provider(shape).filter((loop) => loop.length >= 2);\n if (loops.length > 0) return loops.map((loop) => loop.map((p) => localToWorld(shape, p)));\n }\n // Fallback: axis-aligned world bounding box.\n const b = getElementWorldBounds(shape);\n return [rectLoop(b)];\n};\n\n// --- group queries (parentId chain) ---\n\n/**\n * Direct children of `parentId` \u2014 every shape whose `parentId` equals\n * the argument, in z-order. Linear in scene size; for groups inside the\n * editor's hot path, cache the result by `(scene, parentId)`.\n */\nexport const getChildrenOf = (scene: Scene, parentId: ElementId): readonly Element[] => {\n const out: Element[] = [];\n for (const s of scene.elements.values()) {\n if (s.parentId === parentId) out.push(s);\n }\n out.sort(byOrderAsc);\n return out;\n};\n\n/**\n * `true` when the shape (or any of its ancestors via `parentId`) has\n * `locked: true`. Walks the parent chain bounded by\n * `MAX_PARENT_DEPTH` so the answer stays O(depth) for a freshly\n * grouped scene. Independent from `Layer.locked` \u2014 callers that need\n * the combined interactivity gate should `||` both flags.\n */\nexport const isElementLocked = (scene: Scene, shape: Element): boolean => {\n let current: Element | undefined = shape;\n for (let i = 0; current && i < MAX_PARENT_DEPTH; i++) {\n if (current.locked === true) return true;\n if (!current.parentId) return false;\n current = scene.elements.get(current.parentId);\n }\n return false;\n};\n\n/**\n * `true` when the shape (or any of its ancestors via `parentId`) has\n * `hidden: true`. Same propagation semantics as `isElementLocked`.\n */\nexport const isElementHidden = (scene: Scene, shape: Element): boolean => {\n let current: Element | undefined = shape;\n for (let i = 0; current && i < MAX_PARENT_DEPTH; i++) {\n if (current.hidden === true) return true;\n if (!current.parentId) return false;\n current = scene.elements.get(current.parentId);\n }\n return false;\n};\n\n/**\n * Walks the `parentId` chain starting from `elementId` and returns the\n * topmost ancestor (the root). Returns the shape itself when it has no\n * parent, or `undefined` when the shape (or any ancestor) is missing.\n * Cycle-safe \u2014 bails after `MAX_PARENT_DEPTH` hops.\n */\nexport const getRootSelf = (scene: Scene, elementId: ElementId): Element | undefined => {\n let current = scene.elements.get(elementId);\n for (let i = 0; current?.parentId && i < MAX_PARENT_DEPTH; i++) {\n const parent = scene.elements.get(current.parentId);\n if (!parent) break;\n current = parent;\n }\n return current;\n};\n\n/**\n * Every descendant of `parentId`, recursive, including the root itself.\n * Order: parent first, then a depth-first walk. Cycle-safe via the\n * `visited` set.\n */\nexport const getDescendantsOf = (scene: Scene, parentId: ElementId): readonly Element[] => {\n const root = scene.elements.get(parentId);\n if (!root) return [];\n const visited = new Set<ElementId>([parentId]);\n const out: Element[] = [root];\n const stack: ElementId[] = [parentId];\n while (stack.length > 0) {\n const cur = stack.pop();\n if (cur === undefined) break;\n for (const child of getChildrenOf(scene, cur)) {\n if (visited.has(child.id)) continue;\n visited.add(child.id);\n out.push(child);\n stack.push(child.id);\n }\n }\n return out;\n};\n\n// --- spatial queries (linear scan) ---\n\n/**\n * Shapes whose world AABB intersects `range`. Linear in the number of shapes.\n * For large scenes use `buildSpatialIndex` once and query the index.\n */\nexport const getElementsInBounds = (scene: Scene, range: Bounds): readonly Element[] => {\n const out: Element[] = [];\n for (const s of scene.elements.values()) {\n if (B.intersects(getElementWorldBounds(s), range)) out.push(s);\n }\n return out;\n};\n\n/**\n * Shapes whose world-AABB is at least `minCoverageRatio` covered by\n * `range`. `1` requires full containment (containment-style lasso);\n * `0.5` selects when at least half of the element sits inside the\n * box \u2014 friendlier than pure intersection because brushing past an\n * edge doesn't accidentally grab the shape.\n *\n * Always selects shapes that fully contain the lasso (small lasso\n * inside a big shape) \u2014 same affordance as bidirectional containment.\n * Zero-area shapes (groups, brushes-with-one-vertex) fall back to a\n * plain intersection test.\n */\nexport const getElementsCoveredByBounds = (\n scene: Scene,\n range: Bounds,\n minCoverageRatio = 0.5,\n): readonly Element[] => {\n const out: Element[] = [];\n for (const s of scene.elements.values()) {\n const b = getElementWorldBounds(s);\n if (!B.intersects(b, range)) continue;\n const area = b.width * b.height;\n if (area <= 0) {\n out.push(s);\n continue;\n }\n const ix = Math.max(b.x, range.x);\n const iy = Math.max(b.y, range.y);\n const ix2 = Math.min(b.x + b.width, range.x + range.width);\n const iy2 = Math.min(b.y + b.height, range.y + range.height);\n const iw = ix2 - ix;\n const ih = iy2 - iy;\n if (iw <= 0 || ih <= 0) continue;\n const coverage = (iw * ih) / area;\n if (coverage >= minCoverageRatio) {\n out.push(s);\n continue;\n }\n // Bidirectional: tiny lasso inside a big shape still picks it.\n const lassoArea = range.width * range.height;\n if (lassoArea > 0 && (iw * ih) / lassoArea >= minCoverageRatio) {\n out.push(s);\n }\n }\n return out;\n};\n\n/**\n * Topmost shape containing `point`. Iterates layers top-to-bottom, then shapes\n * within each layer top-to-bottom; returns the first hit. Hit-test here is the\n * conservative AABB test; renderer-specific shape-precise hit-tests belong\n * with the renderer.\n *\n * `accept` filters candidates: a shape it rejects is skipped and the scan\n * continues to the shapes beneath it (click-through), instead of shadowing\n * them the way a post-hoc filter on the topmost hit would.\n */\nexport const getElementAt = (\n scene: Scene,\n point: Vec2,\n accept?: (shape: Element) => boolean,\n): Element | undefined => {\n const layers = getLayersInOrder(scene);\n for (let i = layers.length - 1; i >= 0; i--) {\n const layer = layers[i];\n if (!layer?.visible) continue;\n const shapes = getElementsInLayer(scene, layer.id);\n for (let j = shapes.length - 1; j >= 0; j--) {\n const s = shapes[j];\n if (s === undefined) continue;\n if (!B.contains(getElementWorldBounds(s), point)) continue;\n if (accept && !accept(s)) continue;\n return s;\n }\n }\n return undefined;\n};\n\n// --- spatial index helpers ---\n\n/**\n * Build a `SpatialGrid` from the current scene. Re-build (or update\n * incrementally) when shapes change \u2014 the grid is not auto-synced with the\n * scene. The default cell size is tuned for typical editor scenes; pass an\n * explicit value if your shapes are much larger or smaller.\n */\nexport const buildSpatialIndex = (\n scene: Scene,\n cellSize: number = SPATIAL_GRID_CELL_SIZE,\n): SpatialGrid => {\n const grid = new SpatialGrid(cellSize);\n for (const shape of scene.elements.values()) {\n grid.insert(shape.id, getElementWorldBounds(shape));\n }\n return grid;\n};\n\n/**\n * Range query backed by the index. Returns shapes (not just ids) whose AABB\n * actually intersects `range`. The grid pre-filters by cell overlap; this\n * function does the precise AABB filter.\n */\nexport const queryByIndex = (\n scene: Scene,\n grid: SpatialGrid,\n range: Bounds,\n): readonly Element[] => {\n const candidates = grid.query(range);\n const out: Element[] = [];\n for (const id of candidates) {\n const shape = scene.elements.get(id);\n if (!shape) continue;\n if (B.intersects(getElementWorldBounds(shape), range)) out.push(shape);\n }\n return out;\n};\n\n/**\n * Point hit-test backed by a SpatialGrid. Equivalent to `getElementAt` but\n * pre-filters candidates through `grid.query` \u2014 O(k) where k is the\n * shapes overlapping the point's cell. Walks layers top-to-bottom for\n * stable z-order; within a layer picks the highest-`order` shape that\n * actually contains the point. `accept` skips rejected shapes and keeps\n * scanning beneath them (same click-through contract as `getElementAt`).\n */\nexport const getElementAtIndexed = (\n scene: Scene,\n grid: SpatialGrid,\n point: Vec2,\n accept?: (shape: Element) => boolean,\n): Element | undefined => {\n const pointRange: Bounds = { x: point.x, y: point.y, width: 0, height: 0 };\n const candidates = grid.query(pointRange);\n if (candidates.size === 0) return undefined;\n let best: Element | undefined;\n let bestLayerOrder = \"\";\n let bestElementOrder = \"\";\n let bestSet = false;\n for (const id of candidates) {\n const shape = scene.elements.get(id);\n if (!shape) continue;\n if (!B.contains(getElementWorldBounds(shape), point)) continue;\n if (accept && !accept(shape)) continue;\n const layer = scene.layers.get(shape.layerId);\n if (!layer?.visible) continue;\n const layerOrder = layer.order as string;\n if (\n !bestSet ||\n layerOrder > bestLayerOrder ||\n (layerOrder === bestLayerOrder && shape.order > bestElementOrder)\n ) {\n best = shape;\n bestLayerOrder = layerOrder;\n bestElementOrder = shape.order;\n bestSet = true;\n }\n }\n return best;\n};\n\n/**\n * Every `FileId` the scene's elements still point at. The binary registry\n * is shared \u2014 several shapes may reference one entry \u2014 so a file is only\n * unused when NO element references it.\n */\nexport const referencedFileIds = (scene: Scene): ReadonlySet<FileId> => {\n const out = new Set<FileId>();\n for (const el of scene.elements.values()) {\n const id = (el as { readonly fileId?: FileId }).fileId;\n if (id !== undefined) out.add(id);\n }\n return out;\n};\n\n/**\n * File entries no element references any more \u2014 the bytes a host can drop\n * from its store. Deleting a shape leaves its file behind on purpose (undo\n * must be able to bring the pixels back), so callers pair this with an\n * undoable patch or run it when history no longer reaches the file.\n */\nexport const unreferencedFileIds = (scene: Scene): readonly FileId[] => {\n const used = referencedFileIds(scene);\n return [...scene.files.keys()].filter((id) => !used.has(id));\n};\n", "import { vec2 } from \"@oh-just-another/math\";\nimport { req, type Vec2 } from \"@oh-just-another/types\";\nimport type { BrushPoint } from \"./shape.js\";\nimport { BRUSH_OUTLINE_ARC_STEP, BRUSH_OUTLINE_MITER_LIMIT } from \"../constants.js\";\n\n/**\n * The single closed outline polygon of a variable-width brush stroke: the left\n * offset side forward, a round end cap, the right offset side back, a round start\n * cap. Filling this ONE simple polygon (nonzero winding) paints every pixel\n * exactly once \u2014 unlike per-segment quads + joint discs, whose overlaps\n * double-blend at `opacity < 1` (dark blotches at the joins).\n *\n * Round joins/caps are approximated by arc points at {@link BRUSH_OUTLINE_ARC_STEP}\n * spacing. Convex corners round outward with an arc; concave corners take the\n * miter (offset-line intersection), clamped to {@link BRUSH_OUTLINE_MITER_LIMIT}\n * half-widths (bevel beyond that) so the polygon stays simple \u2014 the WebGL2 earcut\n * fill needs a non-self-intersecting boundary.\n *\n * `points` carry per-vertex half-width. Returns `[]` for `< 2` points (callers\n * draw a single dot as an ellipse). Output is a closed loop in the same local\n * space as `points` (no duplicated closing point; the caller closes the path).\n */\nexport const brushOutline = (points: readonly BrushPoint[]): Vec2[] => {\n const n = points.length;\n if (n < 2) return [];\n const pos = (i: number): Vec2 => {\n const q = req(points[i]);\n return { x: q.x, y: q.y };\n };\n const halfWidth = (i: number): number => req(points[i]).width;\n\n // Per-segment unit direction and LEFT normal (perp = (-dy, dx)).\n const dir: Vec2[] = [];\n const nrm: Vec2[] = [];\n for (let i = 0; i < n - 1; i++) {\n const raw = vec2.sub(pos(i + 1), pos(i));\n const d = vec2.lengthSq(raw) > 0 ? vec2.normalize(raw) : { x: 1, y: 0 };\n dir.push(d);\n nrm.push(vec2.perp(d));\n }\n\n const left: Vec2[] = [];\n const right: Vec2[] = [];\n for (let i = 0; i < n; i++) {\n const w = halfWidth(i);\n const c = pos(i);\n if (i === 0) {\n const nn = req(nrm[0]);\n left.push(vec2.add(c, vec2.mul(nn, w)));\n right.push(vec2.sub(c, vec2.mul(nn, w)));\n continue;\n }\n if (i === n - 1) {\n const nn = req(nrm[n - 2]);\n left.push(vec2.add(c, vec2.mul(nn, w)));\n right.push(vec2.sub(c, vec2.mul(nn, w)));\n continue;\n }\n const nPrev = req(nrm[i - 1]);\n const nNext = req(nrm[i]);\n const turn = vec2.cross(req(dir[i - 1]), req(dir[i]));\n if (Math.abs(turn) < 1e-9) {\n // Collinear \u2014 one offset point per side is enough.\n left.push(vec2.add(c, vec2.mul(nPrev, w)));\n right.push(vec2.sub(c, vec2.mul(nPrev, w)));\n continue;\n }\n // turn > 0: left turn \u2192 left side concave (miter), right side convex (arc).\n // turn < 0: right turn \u2192 left side convex (arc), right side concave (miter).\n if (turn > 0) {\n left.push(...miterSide(c, nPrev, nNext, w, 1));\n right.push(...arcSide(c, nPrev, nNext, w, -1));\n } else {\n left.push(...arcSide(c, nPrev, nNext, w, 1));\n right.push(...miterSide(c, nPrev, nNext, w, -1));\n }\n }\n\n // Round caps: half-turn arcs from the +normal offset to the -normal offset,\n // bulging past the endpoint along the stroke direction (+dir at the end,\n // -dir at the start). The \u2212\u03C0 sweep passes through that direction.\n const endCap = capArc(pos(n - 1), req(nrm[n - 2]), halfWidth(n - 1), false);\n const startCap = capArc(pos(0), req(nrm[0]), halfWidth(0), true);\n\n const outline: Vec2[] = [];\n for (const p of left) outline.push(p);\n for (const p of endCap) outline.push(p);\n for (let i = right.length - 1; i >= 0; i--) outline.push(req(right[i]));\n for (const p of startCap) outline.push(p);\n return outline;\n};\n\n/**\n * Concave corner: the miter point where the two offset lines meet, on the\n * `sign` side (+1 = left/+normal, -1 = right/-normal). Beyond the miter limit\n * (a very sharp turn) fall back to a bevel (the two segment-offset points) so the\n * outline can't grow a long spike.\n */\nconst miterSide = (c: Vec2, nPrev: Vec2, nNext: Vec2, w: number, sign: number): Vec2[] => {\n const m = vec2.normalize(vec2.add(nPrev, nNext));\n const cos = vec2.dot(m, nPrev); // cos of half the turn angle\n if (cos > 1e-3 && 1 / cos <= BRUSH_OUTLINE_MITER_LIMIT) {\n return [vec2.add(c, vec2.mul(m, sign * (w / cos)))];\n }\n return [vec2.add(c, vec2.mul(nPrev, sign * w)), vec2.add(c, vec2.mul(nNext, sign * w))];\n};\n\n/**\n * Convex corner: an arc of radius `w` around `c` from the previous offset\n * direction to the next, sampled the short way. `sign` picks the side (+1 =\n * +normal, -1 = -normal).\n */\nconst arcSide = (c: Vec2, nPrev: Vec2, nNext: Vec2, w: number, sign: number): Vec2[] =>\n arc(c, w, vec2.angle(vec2.mul(nPrev, sign)), vec2.angle(vec2.mul(nNext, sign)));\n\n/**\n * A round cap: the half-turn arc of radius `w` from the +normal offset to the\n * -normal offset. `fromOpposite` starts at the -normal side (for the START cap,\n * which the outline reaches from the reversed right side); the \u2212\u03C0 sweep makes it\n * bulge along the stroke direction rather than back across the body.\n */\nconst capArc = (c: Vec2, nrm: Vec2, w: number, fromOpposite: boolean): Vec2[] => {\n const a0 = vec2.angle(nrm) + (fromOpposite ? Math.PI : 0);\n return arc(c, w, a0, a0 - Math.PI);\n};\n\n/**\n * Sample a circular arc of `radius` around `c` from angle `a0` to `a1` inclusive,\n * taking the shorter signed sweep, at {@link BRUSH_OUTLINE_ARC_STEP} spacing.\n * A `\u00B1\u03C0` sweep (a cap) keeps its given sign so the half-circle bulges the right\n * way.\n */\nconst arc = (c: Vec2, radius: number, a0: number, a1: number): Vec2[] => {\n let delta = a1 - a0;\n while (delta > Math.PI + 1e-9) delta -= 2 * Math.PI;\n while (delta < -Math.PI - 1e-9) delta += 2 * Math.PI;\n const steps = Math.max(1, Math.ceil(Math.abs(delta) / BRUSH_OUTLINE_ARC_STEP));\n const out: Vec2[] = [];\n for (let k = 0; k <= steps; k++) {\n const a = a0 + (delta * k) / steps;\n out.push({ x: c.x + radius * Math.cos(a), y: c.y + radius * Math.sin(a) });\n }\n return out;\n};\n", "import type { Bounds } from \"@oh-just-another/types\";\nimport { getElementWorldBounds, type ElementBase } from \"./shape.js\";\n\n/**\n * Some elements PAINT beyond their geometric bounds \u2014 a frame draws its\n * header strip above the rectangle, a confetti box throws particles past\n * its edges. The dirty-rect / tile invalidation must clear that overspill\n * too, otherwise deleting (or moving without a full repaint) leaves a\n * \"ghost\" of the overpainted region.\n *\n * `RenderOverflow` is the per-side extra paint margin (world units) an\n * element type bleeds past `getElementWorldBounds`. Providers are keyed by\n * element `type` and may inspect the shape (e.g. only confetti-tagged\n * rectangles overflow). All sides default to 0.\n */\nexport interface RenderOverflow {\n readonly top?: number;\n readonly right?: number;\n readonly bottom?: number;\n readonly left?: number;\n}\n\ntype RenderOverflowProvider = (shape: ElementBase) => RenderOverflow;\n\nconst providers = new Map<string, RenderOverflowProvider>();\n\n/**\n * Register how far an element type paints past its bounds. The renderer\n * that draws the overspill owns this (it knows the header height /\n * particle spread). Idempotent per type \u2014 last registration wins.\n */\nexport const registerRenderOverflow = (type: string, fn: RenderOverflowProvider): void => {\n providers.set(type, fn);\n};\n\n/**\n * World bounds expanded by the element type's registered paint overflow \u2014\n * the region that must be invalidated/cleared when the element changes or\n * is removed. Falls back to the plain world bounds when no overflow is\n * registered (the common case).\n */\nexport const getElementRenderBounds = (shape: ElementBase): Bounds => {\n const b = getElementWorldBounds(shape);\n const fn = providers.get(shape.type);\n if (!fn) return b;\n const o = fn(shape);\n const top = o.top ?? 0;\n const right = o.right ?? 0;\n const bottom = o.bottom ?? 0;\n const left = o.left ?? 0;\n if (top === 0 && right === 0 && bottom === 0 && left === 0) return b;\n return {\n x: b.x - left,\n y: b.y - top,\n width: b.width + left + right,\n height: b.height + top + bottom,\n };\n};\n", "import type { Transform, Vec2 } from \"@oh-just-another/types\";\nimport { matrix } from \"@oh-just-another/math\";\nimport { DEFAULT_GRID_SPACING, DEFAULT_CANVAS_BACKGROUND } from \"../constants.js\";\n\n/**\n * How the background grid is painted.\n * `\"lines\"` \u2014 ruled grid lines (default).\n * `\"dots\"` \u2014 a dot at every grid intersection.\n *\n * Snap behaviour is identical between styles; only the paint differs.\n */\nexport type GridStyle = \"lines\" | \"dots\";\n\n/**\n * Camera over the world. Stored as pan/zoom/rotation rather than a raw matrix\n * because every UI control (zoom-to-fit, hotkeys, pinch) wants these axes\n * directly; the matrix form is derivable.\n */\nexport interface Viewport {\n /** World coordinate at viewport (0, 0) before rotation. */\n readonly pan: Vec2;\n /** Uniform scale. 1 = native; 2 = zoomed in 2\u00D7. */\n readonly zoom: number;\n /** Rotation in radians, counter-clockwise. */\n readonly rotation: number;\n readonly size: { readonly width: number; readonly height: number };\n /**\n * Whether the background grid is drawn for this scene. Spacing is fixed\n * at {@link DEFAULT_GRID_SPACING}; this flag controls only visibility.\n */\n readonly gridEnabled: boolean;\n /** How the grid is painted. Renderers fall back to lines when unset. */\n readonly gridStyle?: GridStyle;\n /**\n * Programmatic snap opt-out. `undefined` is treated as ON\n * (see {@link isSnapToGridEnabled}). Snapping also requires the grid to be\n * enabled \u2014 snapping to a hidden grid is confusing \u2014 so this flag only\n * matters while `gridEnabled` is true.\n */\n readonly snapToGrid?: boolean;\n /**\n * Saved \"start view\" camera \u2014 where the document opens (and where\n * \"go to start view\" jumps). Absent until the author sets one.\n */\n readonly startView?: StartView;\n /**\n * Canvas paper colour (any CSS colour) behind the grid and the shapes.\n * Absent = {@link DEFAULT_CANVAS_BACKGROUND}. Part of the document: it\n * serialises with the scene and reaches \"with background\" exports.\n */\n readonly background?: string;\n}\n\n/** The canvas paper colour of `viewport`, defaulted. */\nexport const canvasBackgroundOf = (viewport: Viewport): string =>\n viewport.background ?? DEFAULT_CANVAS_BACKGROUND;\n\n/** A saved camera pose: pan + zoom (rotation is not part of a start view). */\nexport interface StartView {\n readonly pan: Vec2;\n readonly zoom: number;\n}\n\n/** World-unit spacing snap-to-grid rounds to (the fixed grid spacing). */\nexport const resolveSnapSpacing = (): number => DEFAULT_GRID_SPACING;\n\n/**\n * Whether snap-to-grid is enabled for this viewport. `undefined` counts as\n * ON \u2014 the product default.\n */\nexport const isSnapToGridEnabled = (viewport: Viewport): boolean => viewport.snapToGrid ?? true;\n\nexport const DEFAULT_VIEWPORT: Viewport = Object.freeze({\n pan: { x: 0, y: 0 },\n zoom: 1,\n rotation: 0,\n size: { width: 0, height: 0 },\n // Grid off by default; hosts enable it per scene via `gridEnabled`.\n gridEnabled: false,\n});\n\n/** World \u2192 screen transform. */\nexport const getWorldToScreen = (viewport: Viewport): Transform => {\n // Order: world point \u2192 translate by -pan \u2192 rotate \u2192 scale.\n const translate = matrix.translation(-viewport.pan.x, -viewport.pan.y);\n const rotate = matrix.rotation(viewport.rotation);\n const scale = matrix.scaling(viewport.zoom);\n return matrix.multiply(scale, matrix.multiply(rotate, translate));\n};\n\n/** Screen \u2192 world transform (inverse of `getWorldToScreen`). */\nexport const getScreenToWorld = (viewport: Viewport): Transform =>\n matrix.inverse(getWorldToScreen(viewport));\n\n/**\n * Pan the camera by a screen-space delta. Most useful for drag handlers that\n * report pixel deltas; the delta is divided by `zoom` so panning by 1 screen\n * pixel moves the world by 1 / zoom world units.\n */\nexport const panBy = (viewport: Viewport, deltaScreen: Vec2): Viewport => ({\n ...viewport,\n pan: {\n x: viewport.pan.x - deltaScreen.x / viewport.zoom,\n y: viewport.pan.y - deltaScreen.y / viewport.zoom,\n },\n});\n\n/**\n * Multiplicative zoom around a world-space anchor. The anchor stays under the\n * same screen pixel, which is what users expect from mouse-wheel zoom.\n */\nexport const zoomAt = (viewport: Viewport, factor: number, anchorWorld: Vec2): Viewport => {\n const newZoom = viewport.zoom * factor;\n // Adjust pan so that anchorWorld maps to the same screen point.\n return {\n ...viewport,\n zoom: newZoom,\n pan: {\n x: anchorWorld.x - (anchorWorld.x - viewport.pan.x) / factor,\n y: anchorWorld.y - (anchorWorld.y - viewport.pan.y) / factor,\n },\n };\n};\n\nexport const resize = (viewport: Viewport, width: number, height: number): Viewport => ({\n ...viewport,\n size: { width, height },\n});\n", "const grayDark = {\n gray1: \"#111111\",\n gray2: \"#191919\",\n gray3: \"#222222\",\n gray4: \"#2a2a2a\",\n gray5: \"#313131\",\n gray6: \"#3a3a3a\",\n gray7: \"#484848\",\n gray8: \"#606060\",\n gray9: \"#6e6e6e\",\n gray10: \"#7b7b7b\",\n gray11: \"#b4b4b4\",\n gray12: \"#eeeeee\",\n};\nconst grayDarkA = {\n grayA1: \"#00000000\",\n grayA2: \"#ffffff09\",\n grayA3: \"#ffffff12\",\n grayA4: \"#ffffff1b\",\n grayA5: \"#ffffff22\",\n grayA6: \"#ffffff2c\",\n grayA7: \"#ffffff3b\",\n grayA8: \"#ffffff55\",\n grayA9: \"#ffffff64\",\n grayA10: \"#ffffff72\",\n grayA11: \"#ffffffaf\",\n grayA12: \"#ffffffed\",\n};\nconst grayDarkP3 = {\n gray1: \"color(display-p3 0.067 0.067 0.067)\",\n gray2: \"color(display-p3 0.098 0.098 0.098)\",\n gray3: \"color(display-p3 0.135 0.135 0.135)\",\n gray4: \"color(display-p3 0.163 0.163 0.163)\",\n gray5: \"color(display-p3 0.192 0.192 0.192)\",\n gray6: \"color(display-p3 0.228 0.228 0.228)\",\n gray7: \"color(display-p3 0.283 0.283 0.283)\",\n gray8: \"color(display-p3 0.375 0.375 0.375)\",\n gray9: \"color(display-p3 0.431 0.431 0.431)\",\n gray10: \"color(display-p3 0.484 0.484 0.484)\",\n gray11: \"color(display-p3 0.706 0.706 0.706)\",\n gray12: \"color(display-p3 0.933 0.933 0.933)\",\n};\nconst grayDarkP3A = {\n grayA1: \"color(display-p3 0 0 0 / 0)\",\n grayA2: \"color(display-p3 1 1 1 / 0.034)\",\n grayA3: \"color(display-p3 1 1 1 / 0.071)\",\n grayA4: \"color(display-p3 1 1 1 / 0.105)\",\n grayA5: \"color(display-p3 1 1 1 / 0.134)\",\n grayA6: \"color(display-p3 1 1 1 / 0.172)\",\n grayA7: \"color(display-p3 1 1 1 / 0.231)\",\n grayA8: \"color(display-p3 1 1 1 / 0.332)\",\n grayA9: \"color(display-p3 1 1 1 / 0.391)\",\n grayA10: \"color(display-p3 1 1 1 / 0.445)\",\n grayA11: \"color(display-p3 1 1 1 / 0.685)\",\n grayA12: \"color(display-p3 1 1 1 / 0.929)\",\n};\nconst mauveDark = {\n mauve1: \"#121113\",\n mauve2: \"#1a191b\",\n mauve3: \"#232225\",\n mauve4: \"#2b292d\",\n mauve5: \"#323035\",\n mauve6: \"#3c393f\",\n mauve7: \"#49474e\",\n mauve8: \"#625f69\",\n mauve9: \"#6f6d78\",\n mauve10: \"#7c7a85\",\n mauve11: \"#b5b2bc\",\n mauve12: \"#eeeef0\",\n};\nconst mauveDarkA = {\n mauveA1: \"#00000000\",\n mauveA2: \"#f5f4f609\",\n mauveA3: \"#ebeaf814\",\n mauveA4: \"#eee5f81d\",\n mauveA5: \"#efe6fe25\",\n mauveA6: \"#f1e6fd30\",\n mauveA7: \"#eee9ff40\",\n mauveA8: \"#eee7ff5d\",\n mauveA9: \"#eae6fd6e\",\n mauveA10: \"#ece9fd7c\",\n mauveA11: \"#f5f1ffb7\",\n mauveA12: \"#fdfdffef\",\n};\nconst mauveDarkP3 = {\n mauve1: \"color(display-p3 0.07 0.067 0.074)\",\n mauve2: \"color(display-p3 0.101 0.098 0.105)\",\n mauve3: \"color(display-p3 0.138 0.134 0.144)\",\n mauve4: \"color(display-p3 0.167 0.161 0.175)\",\n mauve5: \"color(display-p3 0.196 0.189 0.206)\",\n mauve6: \"color(display-p3 0.232 0.225 0.245)\",\n mauve7: \"color(display-p3 0.286 0.277 0.302)\",\n mauve8: \"color(display-p3 0.383 0.373 0.408)\",\n mauve9: \"color(display-p3 0.434 0.428 0.467)\",\n mauve10: \"color(display-p3 0.487 0.48 0.519)\",\n mauve11: \"color(display-p3 0.707 0.7 0.735)\",\n mauve12: \"color(display-p3 0.933 0.933 0.94)\",\n};\nconst mauveDarkP3A = {\n mauveA1: \"color(display-p3 0 0 0 / 0)\",\n mauveA2: \"color(display-p3 0.996 0.992 1 / 0.034)\",\n mauveA3: \"color(display-p3 0.937 0.933 0.992 / 0.077)\",\n mauveA4: \"color(display-p3 0.957 0.918 0.996 / 0.111)\",\n mauveA5: \"color(display-p3 0.937 0.906 0.996 / 0.145)\",\n mauveA6: \"color(display-p3 0.953 0.925 0.996 / 0.183)\",\n mauveA7: \"color(display-p3 0.945 0.929 1 / 0.246)\",\n mauveA8: \"color(display-p3 0.937 0.918 1 / 0.361)\",\n mauveA9: \"color(display-p3 0.933 0.918 1 / 0.424)\",\n mauveA10: \"color(display-p3 0.941 0.925 1 / 0.479)\",\n mauveA11: \"color(display-p3 0.965 0.961 1 / 0.712)\",\n mauveA12: \"color(display-p3 0.992 0.992 1 / 0.937)\",\n};\nconst slateDark = {\n slate1: \"#111113\",\n slate2: \"#18191b\",\n slate3: \"#212225\",\n slate4: \"#272a2d\",\n slate5: \"#2e3135\",\n slate6: \"#363a3f\",\n slate7: \"#43484e\",\n slate8: \"#5a6169\",\n slate9: \"#696e77\",\n slate10: \"#777b84\",\n slate11: \"#b0b4ba\",\n slate12: \"#edeef0\",\n};\nconst slateDarkA = {\n slateA1: \"#00000000\",\n slateA2: \"#d8f4f609\",\n slateA3: \"#ddeaf814\",\n slateA4: \"#d3edf81d\",\n slateA5: \"#d9edfe25\",\n slateA6: \"#d6ebfd30\",\n slateA7: \"#d9edff40\",\n slateA8: \"#d9edff5d\",\n slateA9: \"#dfebfd6d\",\n slateA10: \"#e5edfd7b\",\n slateA11: \"#f1f7feb5\",\n slateA12: \"#fcfdffef\",\n};\nconst slateDarkP3 = {\n slate1: \"color(display-p3 0.067 0.067 0.074)\",\n slate2: \"color(display-p3 0.095 0.098 0.105)\",\n slate3: \"color(display-p3 0.13 0.135 0.145)\",\n slate4: \"color(display-p3 0.156 0.163 0.176)\",\n slate5: \"color(display-p3 0.183 0.191 0.206)\",\n slate6: \"color(display-p3 0.215 0.226 0.244)\",\n slate7: \"color(display-p3 0.265 0.28 0.302)\",\n slate8: \"color(display-p3 0.357 0.381 0.409)\",\n slate9: \"color(display-p3 0.415 0.431 0.463)\",\n slate10: \"color(display-p3 0.469 0.483 0.514)\",\n slate11: \"color(display-p3 0.692 0.704 0.728)\",\n slate12: \"color(display-p3 0.93 0.933 0.94)\",\n};\nconst slateDarkP3A = {\n slateA1: \"color(display-p3 0 0 0 / 0)\",\n slateA2: \"color(display-p3 0.875 0.992 1 / 0.034)\",\n slateA3: \"color(display-p3 0.882 0.933 0.992 / 0.077)\",\n slateA4: \"color(display-p3 0.882 0.953 0.996 / 0.111)\",\n slateA5: \"color(display-p3 0.878 0.929 0.996 / 0.145)\",\n slateA6: \"color(display-p3 0.882 0.949 0.996 / 0.183)\",\n slateA7: \"color(display-p3 0.882 0.929 1 / 0.246)\",\n slateA8: \"color(display-p3 0.871 0.937 1 / 0.361)\",\n slateA9: \"color(display-p3 0.898 0.937 1 / 0.42)\",\n slateA10: \"color(display-p3 0.918 0.945 1 / 0.475)\",\n slateA11: \"color(display-p3 0.949 0.969 0.996 / 0.708)\",\n slateA12: \"color(display-p3 0.988 0.992 1 / 0.937)\",\n};\nconst sageDark = {\n sage1: \"#101211\",\n sage2: \"#171918\",\n sage3: \"#202221\",\n sage4: \"#272a29\",\n sage5: \"#2e3130\",\n sage6: \"#373b39\",\n sage7: \"#444947\",\n sage8: \"#5b625f\",\n sage9: \"#63706b\",\n sage10: \"#717d79\",\n sage11: \"#adb5b2\",\n sage12: \"#eceeed\",\n};\nconst sageDarkA = {\n sageA1: \"#00000000\",\n sageA2: \"#f0f2f108\",\n sageA3: \"#f3f5f412\",\n sageA4: \"#f2fefd1a\",\n sageA5: \"#f1fbfa22\",\n sageA6: \"#edfbf42d\",\n sageA7: \"#edfcf73c\",\n sageA8: \"#ebfdf657\",\n sageA9: \"#dffdf266\",\n sageA10: \"#e5fdf674\",\n sageA11: \"#f4fefbb0\",\n sageA12: \"#fdfffeed\",\n};\nconst sageDarkP3 = {\n sage1: \"color(display-p3 0.064 0.07 0.067)\",\n sage2: \"color(display-p3 0.092 0.098 0.094)\",\n sage3: \"color(display-p3 0.128 0.135 0.131)\",\n sage4: \"color(display-p3 0.155 0.164 0.159)\",\n sage5: \"color(display-p3 0.183 0.193 0.188)\",\n sage6: \"color(display-p3 0.218 0.23 0.224)\",\n sage7: \"color(display-p3 0.269 0.285 0.277)\",\n sage8: \"color(display-p3 0.362 0.382 0.373)\",\n sage9: \"color(display-p3 0.398 0.438 0.421)\",\n sage10: \"color(display-p3 0.453 0.49 0.474)\",\n sage11: \"color(display-p3 0.685 0.709 0.697)\",\n sage12: \"color(display-p3 0.927 0.933 0.93)\",\n};\nconst sageDarkP3A = {\n sageA1: \"color(display-p3 0 0 0 / 0)\",\n sageA2: \"color(display-p3 0.976 0.988 0.984 / 0.03)\",\n sageA3: \"color(display-p3 0.992 0.945 0.941 / 0.072)\",\n sageA4: \"color(display-p3 0.988 0.996 0.992 / 0.102)\",\n sageA5: \"color(display-p3 0.992 1 0.996 / 0.131)\",\n sageA6: \"color(display-p3 0.973 1 0.976 / 0.173)\",\n sageA7: \"color(display-p3 0.957 1 0.976 / 0.233)\",\n sageA8: \"color(display-p3 0.957 1 0.984 / 0.334)\",\n sageA9: \"color(display-p3 0.902 1 0.957 / 0.397)\",\n sageA10: \"color(display-p3 0.929 1 0.973 / 0.452)\",\n sageA11: \"color(display-p3 0.969 1 0.988 / 0.688)\",\n sageA12: \"color(display-p3 0.992 1 0.996 / 0.929)\",\n};\nconst oliveDark = {\n olive1: \"#111210\",\n olive2: \"#181917\",\n olive3: \"#212220\",\n olive4: \"#282a27\",\n olive5: \"#2f312e\",\n olive6: \"#383a36\",\n olive7: \"#454843\",\n olive8: \"#5c625b\",\n olive9: \"#687066\",\n olive10: \"#767d74\",\n olive11: \"#afb5ad\",\n olive12: \"#eceeec\",\n};\nconst oliveDarkA = {\n oliveA1: \"#00000000\",\n oliveA2: \"#f1f2f008\",\n oliveA3: \"#f4f5f312\",\n oliveA4: \"#f3fef21a\",\n oliveA5: \"#f2fbf122\",\n oliveA6: \"#f4faed2c\",\n oliveA7: \"#f2fced3b\",\n oliveA8: \"#edfdeb57\",\n oliveA9: \"#ebfde766\",\n oliveA10: \"#f0fdec74\",\n oliveA11: \"#f6fef4b0\",\n oliveA12: \"#fdfffded\",\n};\nconst oliveDarkP3 = {\n olive1: \"color(display-p3 0.067 0.07 0.063)\",\n olive2: \"color(display-p3 0.095 0.098 0.091)\",\n olive3: \"color(display-p3 0.131 0.135 0.126)\",\n olive4: \"color(display-p3 0.158 0.163 0.153)\",\n olive5: \"color(display-p3 0.186 0.192 0.18)\",\n olive6: \"color(display-p3 0.221 0.229 0.215)\",\n olive7: \"color(display-p3 0.273 0.284 0.266)\",\n olive8: \"color(display-p3 0.365 0.382 0.359)\",\n olive9: \"color(display-p3 0.414 0.438 0.404)\",\n olive10: \"color(display-p3 0.467 0.49 0.458)\",\n olive11: \"color(display-p3 0.69 0.709 0.682)\",\n olive12: \"color(display-p3 0.927 0.933 0.926)\",\n};\nconst oliveDarkP3A = {\n oliveA1: \"color(display-p3 0 0 0 / 0)\",\n oliveA2: \"color(display-p3 0.984 0.988 0.976 / 0.03)\",\n oliveA3: \"color(display-p3 0.992 0.996 0.988 / 0.068)\",\n oliveA4: \"color(display-p3 0.953 0.996 0.949 / 0.102)\",\n oliveA5: \"color(display-p3 0.969 1 0.965 / 0.131)\",\n oliveA6: \"color(display-p3 0.973 1 0.969 / 0.169)\",\n oliveA7: \"color(display-p3 0.98 1 0.961 / 0.228)\",\n oliveA8: \"color(display-p3 0.961 1 0.957 / 0.334)\",\n oliveA9: \"color(display-p3 0.949 1 0.922 / 0.397)\",\n oliveA10: \"color(display-p3 0.953 1 0.941 / 0.452)\",\n oliveA11: \"color(display-p3 0.976 1 0.965 / 0.688)\",\n oliveA12: \"color(display-p3 0.992 1 0.992 / 0.929)\",\n};\nconst sandDark = {\n sand1: \"#111110\",\n sand2: \"#191918\",\n sand3: \"#222221\",\n sand4: \"#2a2a28\",\n sand5: \"#31312e\",\n sand6: \"#3b3a37\",\n sand7: \"#494844\",\n sand8: \"#62605b\",\n sand9: \"#6f6d66\",\n sand10: \"#7c7b74\",\n sand11: \"#b5b3ad\",\n sand12: \"#eeeeec\",\n};\nconst sandDarkA = {\n sandA1: \"#00000000\",\n sandA2: \"#f4f4f309\",\n sandA3: \"#f6f6f513\",\n sandA4: \"#fefef31b\",\n sandA5: \"#fbfbeb23\",\n sandA6: \"#fffaed2d\",\n sandA7: \"#fffbed3c\",\n sandA8: \"#fff9eb57\",\n sandA9: \"#fffae965\",\n sandA10: \"#fffdee73\",\n sandA11: \"#fffcf4b0\",\n sandA12: \"#fffffded\",\n};\nconst sandDarkP3 = {\n sand1: \"color(display-p3 0.067 0.067 0.063)\",\n sand2: \"color(display-p3 0.098 0.098 0.094)\",\n sand3: \"color(display-p3 0.135 0.135 0.129)\",\n sand4: \"color(display-p3 0.164 0.163 0.156)\",\n sand5: \"color(display-p3 0.193 0.192 0.183)\",\n sand6: \"color(display-p3 0.23 0.229 0.217)\",\n sand7: \"color(display-p3 0.285 0.282 0.267)\",\n sand8: \"color(display-p3 0.384 0.378 0.357)\",\n sand9: \"color(display-p3 0.434 0.428 0.403)\",\n sand10: \"color(display-p3 0.487 0.481 0.456)\",\n sand11: \"color(display-p3 0.707 0.703 0.68)\",\n sand12: \"color(display-p3 0.933 0.933 0.926)\",\n};\nconst sandDarkP3A = {\n sandA1: \"color(display-p3 0 0 0 / 0)\",\n sandA2: \"color(display-p3 0.992 0.992 0.988 / 0.034)\",\n sandA3: \"color(display-p3 0.996 0.996 0.992 / 0.072)\",\n sandA4: \"color(display-p3 0.992 0.992 0.953 / 0.106)\",\n sandA5: \"color(display-p3 1 1 0.965 / 0.135)\",\n sandA6: \"color(display-p3 1 0.976 0.929 / 0.177)\",\n sandA7: \"color(display-p3 1 0.984 0.929 / 0.236)\",\n sandA8: \"color(display-p3 1 0.976 0.925 / 0.341)\",\n sandA9: \"color(display-p3 1 0.98 0.925 / 0.395)\",\n sandA10: \"color(display-p3 1 0.992 0.933 / 0.45)\",\n sandA11: \"color(display-p3 1 0.996 0.961 / 0.685)\",\n sandA12: \"color(display-p3 1 1 0.992 / 0.929)\",\n};\nconst tomatoDark = {\n tomato1: \"#181111\",\n tomato2: \"#1f1513\",\n tomato3: \"#391714\",\n tomato4: \"#4e1511\",\n tomato5: \"#5e1c16\",\n tomato6: \"#6e2920\",\n tomato7: \"#853a2d\",\n tomato8: \"#ac4d39\",\n tomato9: \"#e54d2e\",\n tomato10: \"#ec6142\",\n tomato11: \"#ff977d\",\n tomato12: \"#fbd3cb\",\n};\nconst tomatoDarkA = {\n tomatoA1: \"#f1121208\",\n tomatoA2: \"#ff55330f\",\n tomatoA3: \"#ff35232b\",\n tomatoA4: \"#fd201142\",\n tomatoA5: \"#fe332153\",\n tomatoA6: \"#ff4f3864\",\n tomatoA7: \"#fd644a7d\",\n tomatoA8: \"#fe6d4ea7\",\n tomatoA9: \"#fe5431e4\",\n tomatoA10: \"#ff6847eb\",\n tomatoA11: \"#ff977d\",\n tomatoA12: \"#ffd6cefb\",\n};\nconst tomatoDarkP3 = {\n tomato1: \"color(display-p3 0.09 0.068 0.067)\",\n tomato2: \"color(display-p3 0.115 0.084 0.076)\",\n tomato3: \"color(display-p3 0.205 0.097 0.083)\",\n tomato4: \"color(display-p3 0.282 0.099 0.077)\",\n tomato5: \"color(display-p3 0.339 0.129 0.101)\",\n tomato6: \"color(display-p3 0.398 0.179 0.141)\",\n tomato7: \"color(display-p3 0.487 0.245 0.194)\",\n tomato8: \"color(display-p3 0.629 0.322 0.248)\",\n tomato9: \"color(display-p3 0.831 0.345 0.231)\",\n tomato10: \"color(display-p3 0.862 0.415 0.298)\",\n tomato11: \"color(display-p3 1 0.585 0.455)\",\n tomato12: \"color(display-p3 0.959 0.833 0.802)\",\n};\nconst tomatoDarkP3A = {\n tomatoA1: \"color(display-p3 0.973 0.071 0.071 / 0.026)\",\n tomatoA2: \"color(display-p3 0.992 0.376 0.224 / 0.051)\",\n tomatoA3: \"color(display-p3 0.996 0.282 0.176 / 0.148)\",\n tomatoA4: \"color(display-p3 1 0.204 0.118 / 0.232)\",\n tomatoA5: \"color(display-p3 1 0.286 0.192 / 0.29)\",\n tomatoA6: \"color(display-p3 1 0.392 0.278 / 0.353)\",\n tomatoA7: \"color(display-p3 1 0.459 0.349 / 0.45)\",\n tomatoA8: \"color(display-p3 1 0.49 0.369 / 0.601)\",\n tomatoA9: \"color(display-p3 1 0.408 0.267 / 0.82)\",\n tomatoA10: \"color(display-p3 1 0.478 0.341 / 0.853)\",\n tomatoA11: \"color(display-p3 1 0.585 0.455)\",\n tomatoA12: \"color(display-p3 0.959 0.833 0.802)\",\n};\nconst redDark = {\n red1: \"#191111\",\n red2: \"#201314\",\n red3: \"#3b1219\",\n red4: \"#500f1c\",\n red5: \"#611623\",\n red6: \"#72232d\",\n red7: \"#8c333a\",\n red8: \"#b54548\",\n red9: \"#e5484d\",\n red10: \"#ec5d5e\",\n red11: \"#ff9592\",\n red12: \"#ffd1d9\",\n};\nconst redDarkA = {\n redA1: \"#f4121209\",\n redA2: \"#f22f3e11\",\n redA3: \"#ff173f2d\",\n redA4: \"#fe0a3b44\",\n redA5: \"#ff204756\",\n redA6: \"#ff3e5668\",\n redA7: \"#ff536184\",\n redA8: \"#ff5d61b0\",\n redA9: \"#fe4e54e4\",\n redA10: \"#ff6465eb\",\n redA11: \"#ff9592\",\n redA12: \"#ffd1d9\",\n};\nconst redDarkP3 = {\n red1: \"color(display-p3 0.093 0.068 0.067)\",\n red2: \"color(display-p3 0.118 0.077 0.079)\",\n red3: \"color(display-p3 0.211 0.081 0.099)\",\n red4: \"color(display-p3 0.287 0.079 0.113)\",\n red5: \"color(display-p3 0.348 0.11 0.142)\",\n red6: \"color(display-p3 0.414 0.16 0.183)\",\n red7: \"color(display-p3 0.508 0.224 0.236)\",\n red8: \"color(display-p3 0.659 0.298 0.297)\",\n red9: \"color(display-p3 0.83 0.329 0.324)\",\n red10: \"color(display-p3 0.861 0.403 0.387)\",\n red11: \"color(display-p3 1 0.57 0.55)\",\n red12: \"color(display-p3 0.971 0.826 0.852)\",\n};\nconst redDarkP3A = {\n redA1: \"color(display-p3 0.984 0.071 0.071 / 0.03)\",\n redA2: \"color(display-p3 0.996 0.282 0.282 / 0.055)\",\n redA3: \"color(display-p3 1 0.169 0.271 / 0.156)\",\n redA4: \"color(display-p3 1 0.118 0.267 / 0.236)\",\n redA5: \"color(display-p3 1 0.212 0.314 / 0.303)\",\n redA6: \"color(display-p3 1 0.318 0.38 / 0.374)\",\n redA7: \"color(display-p3 1 0.4 0.424 / 0.475)\",\n redA8: \"color(display-p3 1 0.431 0.431 / 0.635)\",\n redA9: \"color(display-p3 1 0.388 0.384 / 0.82)\",\n redA10: \"color(display-p3 1 0.463 0.447 / 0.853)\",\n redA11: \"color(display-p3 1 0.57 0.55)\",\n redA12: \"color(display-p3 0.971 0.826 0.852)\",\n};\nconst rubyDark = {\n ruby1: \"#191113\",\n ruby2: \"#1e1517\",\n ruby3: \"#3a141e\",\n ruby4: \"#4e1325\",\n ruby5: \"#5e1a2e\",\n ruby6: \"#6f2539\",\n ruby7: \"#883447\",\n ruby8: \"#b3445a\",\n ruby9: \"#e54666\",\n ruby10: \"#ec5a72\",\n ruby11: \"#ff949d\",\n ruby12: \"#fed2e1\",\n};\nconst rubyDarkA = {\n rubyA1: \"#f4124a09\",\n rubyA2: \"#fe5a7f0e\",\n rubyA3: \"#ff235d2c\",\n rubyA4: \"#fd195e42\",\n rubyA5: \"#fe2d6b53\",\n rubyA6: \"#ff447665\",\n rubyA7: \"#ff577d80\",\n rubyA8: \"#ff5c7cae\",\n rubyA9: \"#fe4c70e4\",\n rubyA10: \"#ff617beb\",\n rubyA11: \"#ff949d\",\n rubyA12: \"#ffd3e2fe\",\n};\nconst rubyDarkP3 = {\n ruby1: \"color(display-p3 0.093 0.068 0.074)\",\n ruby2: \"color(display-p3 0.113 0.083 0.089)\",\n ruby3: \"color(display-p3 0.208 0.088 0.117)\",\n ruby4: \"color(display-p3 0.279 0.092 0.147)\",\n ruby5: \"color(display-p3 0.337 0.12 0.18)\",\n ruby6: \"color(display-p3 0.401 0.166 0.223)\",\n ruby7: \"color(display-p3 0.495 0.224 0.281)\",\n ruby8: \"color(display-p3 0.652 0.295 0.359)\",\n ruby9: \"color(display-p3 0.83 0.323 0.408)\",\n ruby10: \"color(display-p3 0.857 0.392 0.455)\",\n ruby11: \"color(display-p3 1 0.57 0.59)\",\n ruby12: \"color(display-p3 0.968 0.83 0.88)\",\n};\nconst rubyDarkP3A = {\n rubyA1: \"color(display-p3 0.984 0.071 0.329 / 0.03)\",\n rubyA2: \"color(display-p3 0.992 0.376 0.529 / 0.051)\",\n rubyA3: \"color(display-p3 0.996 0.196 0.404 / 0.152)\",\n rubyA4: \"color(display-p3 1 0.173 0.416 / 0.227)\",\n rubyA5: \"color(display-p3 1 0.259 0.459 / 0.29)\",\n rubyA6: \"color(display-p3 1 0.341 0.506 / 0.358)\",\n rubyA7: \"color(display-p3 1 0.412 0.541 / 0.458)\",\n rubyA8: \"color(display-p3 1 0.431 0.537 / 0.627)\",\n rubyA9: \"color(display-p3 1 0.376 0.482 / 0.82)\",\n rubyA10: \"color(display-p3 1 0.447 0.522 / 0.849)\",\n rubyA11: \"color(display-p3 1 0.57 0.59)\",\n rubyA12: \"color(display-p3 0.968 0.83 0.88)\",\n};\nconst crimsonDark = {\n crimson1: \"#191114\",\n crimson2: \"#201318\",\n crimson3: \"#381525\",\n crimson4: \"#4d122f\",\n crimson5: \"#5c1839\",\n crimson6: \"#6d2545\",\n crimson7: \"#873356\",\n crimson8: \"#b0436e\",\n crimson9: \"#e93d82\",\n crimson10: \"#ee518a\",\n crimson11: \"#ff92ad\",\n crimson12: \"#fdd3e8\",\n};\nconst crimsonDarkA = {\n crimsonA1: \"#f4126709\",\n crimsonA2: \"#f22f7a11\",\n crimsonA3: \"#fe2a8b2a\",\n crimsonA4: \"#fd158741\",\n crimsonA5: \"#fd278f51\",\n crimsonA6: \"#fe459763\",\n crimsonA7: \"#fd559b7f\",\n crimsonA8: \"#fe5b9bab\",\n crimsonA9: \"#fe418de8\",\n crimsonA10: \"#ff5693ed\",\n crimsonA11: \"#ff92ad\",\n crimsonA12: \"#ffd5eafd\",\n};\nconst crimsonDarkP3 = {\n crimson1: \"color(display-p3 0.093 0.068 0.078)\",\n crimson2: \"color(display-p3 0.117 0.078 0.095)\",\n crimson3: \"color(display-p3 0.203 0.091 0.143)\",\n crimson4: \"color(display-p3 0.277 0.087 0.182)\",\n crimson5: \"color(display-p3 0.332 0.115 0.22)\",\n crimson6: \"color(display-p3 0.394 0.162 0.268)\",\n crimson7: \"color(display-p3 0.489 0.222 0.336)\",\n crimson8: \"color(display-p3 0.638 0.289 0.429)\",\n crimson9: \"color(display-p3 0.843 0.298 0.507)\",\n crimson10: \"color(display-p3 0.864 0.364 0.539)\",\n crimson11: \"color(display-p3 1 0.56 0.66)\",\n crimson12: \"color(display-p3 0.966 0.834 0.906)\",\n};\nconst crimsonDarkP3A = {\n crimsonA1: \"color(display-p3 0.984 0.071 0.463 / 0.03)\",\n crimsonA2: \"color(display-p3 0.996 0.282 0.569 / 0.055)\",\n crimsonA3: \"color(display-p3 0.996 0.227 0.573 / 0.148)\",\n crimsonA4: \"color(display-p3 1 0.157 0.569 / 0.227)\",\n crimsonA5: \"color(display-p3 1 0.231 0.604 / 0.286)\",\n crimsonA6: \"color(display-p3 1 0.337 0.643 / 0.349)\",\n crimsonA7: \"color(display-p3 1 0.416 0.663 / 0.454)\",\n crimsonA8: \"color(display-p3 0.996 0.427 0.651 / 0.614)\",\n crimsonA9: \"color(display-p3 1 0.345 0.596 / 0.832)\",\n crimsonA10: \"color(display-p3 1 0.42 0.62 / 0.853)\",\n crimsonA11: \"color(display-p3 1 0.56 0.66)\",\n crimsonA12: \"color(display-p3 0.966 0.834 0.906)\",\n};\nconst pinkDark = {\n pink1: \"#191117\",\n pink2: \"#21121d\",\n pink3: \"#37172f\",\n pink4: \"#4b143d\",\n pink5: \"#591c47\",\n pink6: \"#692955\",\n pink7: \"#833869\",\n pink8: \"#a84885\",\n pink9: \"#d6409f\",\n pink10: \"#de51a8\",\n pink11: \"#ff8dcc\",\n pink12: \"#fdd1ea\",\n};\nconst pinkDarkA = {\n pinkA1: \"#f412bc09\",\n pinkA2: \"#f420bb12\",\n pinkA3: \"#fe37cc29\",\n pinkA4: \"#fc1ec43f\",\n pinkA5: \"#fd35c24e\",\n pinkA6: \"#fd51c75f\",\n pinkA7: \"#fd62c87b\",\n pinkA8: \"#ff68c8a2\",\n pinkA9: \"#fe49bcd4\",\n pinkA10: \"#ff5cc0dc\",\n pinkA11: \"#ff8dcc\",\n pinkA12: \"#ffd3ecfd\",\n};\nconst pinkDarkP3 = {\n pink1: \"color(display-p3 0.093 0.068 0.089)\",\n pink2: \"color(display-p3 0.121 0.073 0.11)\",\n pink3: \"color(display-p3 0.198 0.098 0.179)\",\n pink4: \"color(display-p3 0.271 0.095 0.231)\",\n pink5: \"color(display-p3 0.32 0.127 0.273)\",\n pink6: \"color(display-p3 0.382 0.177 0.326)\",\n pink7: \"color(display-p3 0.477 0.238 0.405)\",\n pink8: \"color(display-p3 0.612 0.304 0.51)\",\n pink9: \"color(display-p3 0.775 0.297 0.61)\",\n pink10: \"color(display-p3 0.808 0.356 0.645)\",\n pink11: \"color(display-p3 1 0.535 0.78)\",\n pink12: \"color(display-p3 0.964 0.826 0.912)\",\n};\nconst pinkDarkP3A = {\n pinkA1: \"color(display-p3 0.984 0.071 0.855 / 0.03)\",\n pinkA2: \"color(display-p3 1 0.2 0.8 / 0.059)\",\n pinkA3: \"color(display-p3 1 0.294 0.886 / 0.139)\",\n pinkA4: \"color(display-p3 1 0.192 0.82 / 0.219)\",\n pinkA5: \"color(display-p3 1 0.282 0.827 / 0.274)\",\n pinkA6: \"color(display-p3 1 0.396 0.835 / 0.337)\",\n pinkA7: \"color(display-p3 1 0.459 0.831 / 0.442)\",\n pinkA8: \"color(display-p3 1 0.478 0.827 / 0.585)\",\n pinkA9: \"color(display-p3 1 0.373 0.784 / 0.761)\",\n pinkA10: \"color(display-p3 1 0.435 0.792 / 0.795)\",\n pinkA11: \"color(display-p3 1 0.535 0.78)\",\n pinkA12: \"color(display-p3 0.964 0.826 0.912)\",\n};\nconst plumDark = {\n plum1: \"#181118\",\n plum2: \"#201320\",\n plum3: \"#351a35\",\n plum4: \"#451d47\",\n plum5: \"#512454\",\n plum6: \"#5e3061\",\n plum7: \"#734079\",\n plum8: \"#92549c\",\n plum9: \"#ab4aba\",\n plum10: \"#b658c4\",\n plum11: \"#e796f3\",\n plum12: \"#f4d4f4\",\n};\nconst plumDarkA = {\n plumA1: \"#f112f108\",\n plumA2: \"#f22ff211\",\n plumA3: \"#fd4cfd27\",\n plumA4: \"#f646ff3a\",\n plumA5: \"#f455ff48\",\n plumA6: \"#f66dff56\",\n plumA7: \"#f07cfd70\",\n plumA8: \"#ee84ff95\",\n plumA9: \"#e961feb6\",\n plumA10: \"#ed70ffc0\",\n plumA11: \"#f19cfef3\",\n plumA12: \"#feddfef4\",\n};\nconst plumDarkP3 = {\n plum1: \"color(display-p3 0.09 0.068 0.092)\",\n plum2: \"color(display-p3 0.118 0.077 0.121)\",\n plum3: \"color(display-p3 0.192 0.105 0.202)\",\n plum4: \"color(display-p3 0.25 0.121 0.271)\",\n plum5: \"color(display-p3 0.293 0.152 0.319)\",\n plum6: \"color(display-p3 0.343 0.198 0.372)\",\n plum7: \"color(display-p3 0.424 0.262 0.461)\",\n plum8: \"color(display-p3 0.54 0.341 0.595)\",\n plum9: \"color(display-p3 0.624 0.313 0.708)\",\n plum10: \"color(display-p3 0.666 0.365 0.748)\",\n plum11: \"color(display-p3 0.86 0.602 0.933)\",\n plum12: \"color(display-p3 0.936 0.836 0.949)\",\n};\nconst plumDarkP3A = {\n plumA1: \"color(display-p3 0.973 0.071 0.973 / 0.026)\",\n plumA2: \"color(display-p3 0.933 0.267 1 / 0.059)\",\n plumA3: \"color(display-p3 0.918 0.333 0.996 / 0.148)\",\n plumA4: \"color(display-p3 0.91 0.318 1 / 0.219)\",\n plumA5: \"color(display-p3 0.914 0.388 1 / 0.269)\",\n plumA6: \"color(display-p3 0.906 0.463 1 / 0.328)\",\n plumA7: \"color(display-p3 0.906 0.529 1 / 0.425)\",\n plumA8: \"color(display-p3 0.906 0.553 1 / 0.568)\",\n plumA9: \"color(display-p3 0.875 0.427 1 / 0.69)\",\n plumA10: \"color(display-p3 0.886 0.471 0.996 / 0.732)\",\n plumA11: \"color(display-p3 0.86 0.602 0.933)\",\n plumA12: \"color(display-p3 0.936 0.836 0.949)\",\n};\nconst purpleDark = {\n purple1: \"#18111b\",\n purple2: \"#1e1523\",\n purple3: \"#301c3b\",\n purple4: \"#3d224e\",\n purple5: \"#48295c\",\n purple6: \"#54346b\",\n purple7: \"#664282\",\n purple8: \"#8457aa\",\n purple9: \"#8e4ec6\",\n purple10: \"#9a5cd0\",\n purple11: \"#d19dff\",\n purple12: \"#ecd9fa\",\n};\nconst purpleDarkA = {\n purpleA1: \"#b412f90b\",\n purpleA2: \"#b744f714\",\n purpleA3: \"#c150ff2d\",\n purpleA4: \"#bb53fd42\",\n purpleA5: \"#be5cfd51\",\n purpleA6: \"#c16dfd61\",\n purpleA7: \"#c378fd7a\",\n purpleA8: \"#c47effa4\",\n purpleA9: \"#b661ffc2\",\n purpleA10: \"#bc6fffcd\",\n purpleA11: \"#d19dff\",\n purpleA12: \"#f1ddfffa\",\n};\nconst purpleDarkP3 = {\n purple1: \"color(display-p3 0.09 0.068 0.103)\",\n purple2: \"color(display-p3 0.113 0.082 0.134)\",\n purple3: \"color(display-p3 0.175 0.112 0.224)\",\n purple4: \"color(display-p3 0.224 0.137 0.297)\",\n purple5: \"color(display-p3 0.264 0.167 0.349)\",\n purple6: \"color(display-p3 0.311 0.208 0.406)\",\n purple7: \"color(display-p3 0.381 0.266 0.496)\",\n purple8: \"color(display-p3 0.49 0.349 0.649)\",\n purple9: \"color(display-p3 0.523 0.318 0.751)\",\n purple10: \"color(display-p3 0.57 0.373 0.791)\",\n purple11: \"color(display-p3 0.8 0.62 1)\",\n purple12: \"color(display-p3 0.913 0.854 0.971)\",\n};\nconst purpleDarkP3A = {\n purpleA1: \"color(display-p3 0.686 0.071 0.996 / 0.038)\",\n purpleA2: \"color(display-p3 0.722 0.286 0.996 / 0.072)\",\n purpleA3: \"color(display-p3 0.718 0.349 0.996 / 0.169)\",\n purpleA4: \"color(display-p3 0.702 0.353 1 / 0.248)\",\n purpleA5: \"color(display-p3 0.718 0.404 1 / 0.303)\",\n purpleA6: \"color(display-p3 0.733 0.455 1 / 0.366)\",\n purpleA7: \"color(display-p3 0.753 0.506 1 / 0.458)\",\n purpleA8: \"color(display-p3 0.749 0.522 1 / 0.622)\",\n purpleA9: \"color(display-p3 0.686 0.408 1 / 0.736)\",\n purpleA10: \"color(display-p3 0.71 0.459 1 / 0.778)\",\n purpleA11: \"color(display-p3 0.8 0.62 1)\",\n purpleA12: \"color(display-p3 0.913 0.854 0.971)\",\n};\nconst violetDark = {\n violet1: \"#14121f\",\n violet2: \"#1b1525\",\n violet3: \"#291f43\",\n violet4: \"#33255b\",\n violet5: \"#3c2e69\",\n violet6: \"#473876\",\n violet7: \"#56468b\",\n violet8: \"#6958ad\",\n violet9: \"#6e56cf\",\n violet10: \"#7d66d9\",\n violet11: \"#baa7ff\",\n violet12: \"#e2ddfe\",\n};\nconst violetDarkA = {\n violetA1: \"#4422ff0f\",\n violetA2: \"#853ff916\",\n violetA3: \"#8354fe36\",\n violetA4: \"#7d51fd50\",\n violetA5: \"#845ffd5f\",\n violetA6: \"#8f6cfd6d\",\n violetA7: \"#9879ff83\",\n violetA8: \"#977dfea8\",\n violetA9: \"#8668ffcc\",\n violetA10: \"#9176fed7\",\n violetA11: \"#baa7ff\",\n violetA12: \"#e3defffe\",\n};\nconst violetDarkP3 = {\n violet1: \"color(display-p3 0.077 0.071 0.118)\",\n violet2: \"color(display-p3 0.101 0.084 0.141)\",\n violet3: \"color(display-p3 0.154 0.123 0.256)\",\n violet4: \"color(display-p3 0.191 0.148 0.345)\",\n violet5: \"color(display-p3 0.226 0.182 0.396)\",\n violet6: \"color(display-p3 0.269 0.223 0.449)\",\n violet7: \"color(display-p3 0.326 0.277 0.53)\",\n violet8: \"color(display-p3 0.399 0.346 0.656)\",\n violet9: \"color(display-p3 0.417 0.341 0.784)\",\n violet10: \"color(display-p3 0.477 0.402 0.823)\",\n violet11: \"color(display-p3 0.72 0.65 1)\",\n violet12: \"color(display-p3 0.883 0.867 0.986)\",\n};\nconst violetDarkP3A = {\n violetA1: \"color(display-p3 0.282 0.141 0.996 / 0.055)\",\n violetA2: \"color(display-p3 0.51 0.263 1 / 0.08)\",\n violetA3: \"color(display-p3 0.494 0.337 0.996 / 0.202)\",\n violetA4: \"color(display-p3 0.49 0.345 1 / 0.299)\",\n violetA5: \"color(display-p3 0.525 0.392 1 / 0.353)\",\n violetA6: \"color(display-p3 0.569 0.455 1 / 0.408)\",\n violetA7: \"color(display-p3 0.588 0.494 1 / 0.496)\",\n violetA8: \"color(display-p3 0.596 0.51 1 / 0.631)\",\n violetA9: \"color(display-p3 0.522 0.424 1 / 0.769)\",\n violetA10: \"color(display-p3 0.576 0.482 1 / 0.811)\",\n violetA11: \"color(display-p3 0.72 0.65 1)\",\n violetA12: \"color(display-p3 0.883 0.867 0.986)\",\n};\nconst irisDark = {\n iris1: \"#13131e\",\n iris2: \"#171625\",\n iris3: \"#202248\",\n iris4: \"#262a65\",\n iris5: \"#303374\",\n iris6: \"#3d3e82\",\n iris7: \"#4a4a95\",\n iris8: \"#5958b1\",\n iris9: \"#5b5bd6\",\n iris10: \"#6e6ade\",\n iris11: \"#b1a9ff\",\n iris12: \"#e0dffe\",\n};\nconst irisDarkA = {\n irisA1: \"#3636fe0e\",\n irisA2: \"#564bf916\",\n irisA3: \"#525bff3b\",\n irisA4: \"#4d58ff5a\",\n irisA5: \"#5b62fd6b\",\n irisA6: \"#6d6ffd7a\",\n irisA7: \"#7777fe8e\",\n irisA8: \"#7b7afeac\",\n irisA9: \"#6a6afed4\",\n irisA10: \"#7d79ffdc\",\n irisA11: \"#b1a9ff\",\n irisA12: \"#e1e0fffe\",\n};\nconst irisDarkP3 = {\n iris1: \"color(display-p3 0.075 0.075 0.114)\",\n iris2: \"color(display-p3 0.089 0.086 0.14)\",\n iris3: \"color(display-p3 0.128 0.134 0.272)\",\n iris4: \"color(display-p3 0.153 0.165 0.382)\",\n iris5: \"color(display-p3 0.192 0.201 0.44)\",\n iris6: \"color(display-p3 0.239 0.241 0.491)\",\n iris7: \"color(display-p3 0.291 0.289 0.565)\",\n iris8: \"color(display-p3 0.35 0.345 0.673)\",\n iris9: \"color(display-p3 0.357 0.357 0.81)\",\n iris10: \"color(display-p3 0.428 0.416 0.843)\",\n iris11: \"color(display-p3 0.685 0.662 1)\",\n iris12: \"color(display-p3 0.878 0.875 0.986)\",\n};\nconst irisDarkP3A = {\n irisA1: \"color(display-p3 0.224 0.224 0.992 / 0.051)\",\n irisA2: \"color(display-p3 0.361 0.314 1 / 0.08)\",\n irisA3: \"color(display-p3 0.357 0.373 1 / 0.219)\",\n irisA4: \"color(display-p3 0.325 0.361 1 / 0.337)\",\n irisA5: \"color(display-p3 0.38 0.4 1 / 0.4)\",\n irisA6: \"color(display-p3 0.447 0.447 1 / 0.454)\",\n irisA7: \"color(display-p3 0.486 0.486 1 / 0.534)\",\n irisA8: \"color(display-p3 0.502 0.494 1 / 0.652)\",\n irisA9: \"color(display-p3 0.431 0.431 1 / 0.799)\",\n irisA10: \"color(display-p3 0.502 0.486 1 / 0.832)\",\n irisA11: \"color(display-p3 0.685 0.662 1)\",\n irisA12: \"color(display-p3 0.878 0.875 0.986)\",\n};\nconst indigoDark = {\n indigo1: \"#11131f\",\n indigo2: \"#141726\",\n indigo3: \"#182449\",\n indigo4: \"#1d2e62\",\n indigo5: \"#253974\",\n indigo6: \"#304384\",\n indigo7: \"#3a4f97\",\n indigo8: \"#435db1\",\n indigo9: \"#3e63dd\",\n indigo10: \"#5472e4\",\n indigo11: \"#9eb1ff\",\n indigo12: \"#d6e1ff\",\n};\nconst indigoDarkA = {\n indigoA1: \"#1133ff0f\",\n indigoA2: \"#3354fa17\",\n indigoA3: \"#2f62ff3c\",\n indigoA4: \"#3566ff57\",\n indigoA5: \"#4171fd6b\",\n indigoA6: \"#5178fd7c\",\n indigoA7: \"#5a7fff90\",\n indigoA8: \"#5b81feac\",\n indigoA9: \"#4671ffdb\",\n indigoA10: \"#5c7efee3\",\n indigoA11: \"#9eb1ff\",\n indigoA12: \"#d6e1ff\",\n};\nconst indigoDarkP3 = {\n indigo1: \"color(display-p3 0.068 0.074 0.118)\",\n indigo2: \"color(display-p3 0.081 0.089 0.144)\",\n indigo3: \"color(display-p3 0.105 0.141 0.275)\",\n indigo4: \"color(display-p3 0.129 0.18 0.369)\",\n indigo5: \"color(display-p3 0.163 0.22 0.439)\",\n indigo6: \"color(display-p3 0.203 0.262 0.5)\",\n indigo7: \"color(display-p3 0.245 0.309 0.575)\",\n indigo8: \"color(display-p3 0.285 0.362 0.674)\",\n indigo9: \"color(display-p3 0.276 0.384 0.837)\",\n indigo10: \"color(display-p3 0.354 0.445 0.866)\",\n indigo11: \"color(display-p3 0.63 0.69 1)\",\n indigo12: \"color(display-p3 0.848 0.881 0.99)\",\n};\nconst indigoDarkP3A = {\n indigoA1: \"color(display-p3 0.071 0.212 0.996 / 0.055)\",\n indigoA2: \"color(display-p3 0.251 0.345 0.988 / 0.085)\",\n indigoA3: \"color(display-p3 0.243 0.404 1 / 0.223)\",\n indigoA4: \"color(display-p3 0.263 0.42 1 / 0.324)\",\n indigoA5: \"color(display-p3 0.314 0.451 1 / 0.4)\",\n indigoA6: \"color(display-p3 0.361 0.49 1 / 0.467)\",\n indigoA7: \"color(display-p3 0.388 0.51 1 / 0.547)\",\n indigoA8: \"color(display-p3 0.404 0.518 1 / 0.652)\",\n indigoA9: \"color(display-p3 0.318 0.451 1 / 0.824)\",\n indigoA10: \"color(display-p3 0.404 0.506 1 / 0.858)\",\n indigoA11: \"color(display-p3 0.63 0.69 1)\",\n indigoA12: \"color(display-p3 0.848 0.881 0.99)\",\n};\nconst blueDark = {\n blue1: \"#0d1520\",\n blue2: \"#111927\",\n blue3: \"#0d2847\",\n blue4: \"#003362\",\n blue5: \"#004074\",\n blue6: \"#104d87\",\n blue7: \"#205d9e\",\n blue8: \"#2870bd\",\n blue9: \"#0090ff\",\n blue10: \"#3b9eff\",\n blue11: \"#70b8ff\",\n blue12: \"#c2e6ff\",\n};\nconst blueDarkA = {\n blueA1: \"#004df211\",\n blueA2: \"#1166fb18\",\n blueA3: \"#0077ff3a\",\n blueA4: \"#0075ff57\",\n blueA5: \"#0081fd6b\",\n blueA6: \"#0f89fd7f\",\n blueA7: \"#2a91fe98\",\n blueA8: \"#3094feb9\",\n blueA9: \"#0090ff\",\n blueA10: \"#3b9eff\",\n blueA11: \"#70b8ff\",\n blueA12: \"#c2e6ff\",\n};\nconst blueDarkP3 = {\n blue1: \"color(display-p3 0.057 0.081 0.122)\",\n blue2: \"color(display-p3 0.072 0.098 0.147)\",\n blue3: \"color(display-p3 0.078 0.154 0.27)\",\n blue4: \"color(display-p3 0.033 0.197 0.37)\",\n blue5: \"color(display-p3 0.08 0.245 0.441)\",\n blue6: \"color(display-p3 0.14 0.298 0.511)\",\n blue7: \"color(display-p3 0.195 0.361 0.6)\",\n blue8: \"color(display-p3 0.239 0.434 0.72)\",\n blue9: \"color(display-p3 0.247 0.556 0.969)\",\n blue10: \"color(display-p3 0.344 0.612 0.973)\",\n blue11: \"color(display-p3 0.49 0.72 1)\",\n blue12: \"color(display-p3 0.788 0.898 0.99)\",\n};\nconst blueDarkP3A = {\n blueA1: \"color(display-p3 0 0.333 1 / 0.059)\",\n blueA2: \"color(display-p3 0.114 0.435 0.988 / 0.085)\",\n blueA3: \"color(display-p3 0.122 0.463 1 / 0.219)\",\n blueA4: \"color(display-p3 0 0.467 1 / 0.324)\",\n blueA5: \"color(display-p3 0.098 0.51 1 / 0.4)\",\n blueA6: \"color(display-p3 0.224 0.557 1 / 0.475)\",\n blueA7: \"color(display-p3 0.294 0.584 1 / 0.572)\",\n blueA8: \"color(display-p3 0.314 0.592 1 / 0.702)\",\n blueA9: \"color(display-p3 0.251 0.573 0.996 / 0.967)\",\n blueA10: \"color(display-p3 0.357 0.631 1 / 0.971)\",\n blueA11: \"color(display-p3 0.49 0.72 1)\",\n blueA12: \"color(display-p3 0.788 0.898 0.99)\",\n};\nconst cyanDark = {\n cyan1: \"#0b161a\",\n cyan2: \"#101b20\",\n cyan3: \"#082c36\",\n cyan4: \"#003848\",\n cyan5: \"#004558\",\n cyan6: \"#045468\",\n cyan7: \"#12677e\",\n cyan8: \"#11809c\",\n cyan9: \"#00a2c7\",\n cyan10: \"#23afd0\",\n cyan11: \"#4ccce6\",\n cyan12: \"#b6ecf7\",\n};\nconst cyanDarkA = {\n cyanA1: \"#0091f70a\",\n cyanA2: \"#02a7f211\",\n cyanA3: \"#00befd28\",\n cyanA4: \"#00baff3b\",\n cyanA5: \"#00befd4d\",\n cyanA6: \"#00c7fd5e\",\n cyanA7: \"#14cdff75\",\n cyanA8: \"#11cfff95\",\n cyanA9: \"#00cfffc3\",\n cyanA10: \"#28d6ffcd\",\n cyanA11: \"#52e1fee5\",\n cyanA12: \"#bbf3fef7\",\n};\nconst cyanDarkP3 = {\n cyan1: \"color(display-p3 0.053 0.085 0.098)\",\n cyan2: \"color(display-p3 0.072 0.105 0.122)\",\n cyan3: \"color(display-p3 0.073 0.168 0.209)\",\n cyan4: \"color(display-p3 0.063 0.216 0.277)\",\n cyan5: \"color(display-p3 0.091 0.267 0.336)\",\n cyan6: \"color(display-p3 0.137 0.324 0.4)\",\n cyan7: \"color(display-p3 0.186 0.398 0.484)\",\n cyan8: \"color(display-p3 0.23 0.496 0.6)\",\n cyan9: \"color(display-p3 0.282 0.627 0.765)\",\n cyan10: \"color(display-p3 0.331 0.675 0.801)\",\n cyan11: \"color(display-p3 0.446 0.79 0.887)\",\n cyan12: \"color(display-p3 0.757 0.919 0.962)\",\n};\nconst cyanDarkP3A = {\n cyanA1: \"color(display-p3 0 0.647 0.992 / 0.034)\",\n cyanA2: \"color(display-p3 0.133 0.733 1 / 0.059)\",\n cyanA3: \"color(display-p3 0.122 0.741 0.996 / 0.152)\",\n cyanA4: \"color(display-p3 0.051 0.725 1 / 0.227)\",\n cyanA5: \"color(display-p3 0.149 0.757 1 / 0.29)\",\n cyanA6: \"color(display-p3 0.267 0.792 1 / 0.358)\",\n cyanA7: \"color(display-p3 0.333 0.808 1 / 0.446)\",\n cyanA8: \"color(display-p3 0.357 0.816 1 / 0.572)\",\n cyanA9: \"color(display-p3 0.357 0.82 1 / 0.748)\",\n cyanA10: \"color(display-p3 0.4 0.839 1 / 0.786)\",\n cyanA11: \"color(display-p3 0.446 0.79 0.887)\",\n cyanA12: \"color(display-p3 0.757 0.919 0.962)\",\n};\nconst tealDark = {\n teal1: \"#0d1514\",\n teal2: \"#111c1b\",\n teal3: \"#0d2d2a\",\n teal4: \"#023b37\",\n teal5: \"#084843\",\n teal6: \"#145750\",\n teal7: \"#1c6961\",\n teal8: \"#207e73\",\n teal9: \"#12a594\",\n teal10: \"#0eb39e\",\n teal11: \"#0bd8b6\",\n teal12: \"#adf0dd\",\n};\nconst tealDarkA = {\n tealA1: \"#00deab05\",\n tealA2: \"#12fbe60c\",\n tealA3: \"#00ffe61e\",\n tealA4: \"#00ffe92d\",\n tealA5: \"#00ffea3b\",\n tealA6: \"#1cffe84b\",\n tealA7: \"#2efde85f\",\n tealA8: \"#32ffe775\",\n tealA9: \"#13ffe49f\",\n tealA10: \"#0dffe0ae\",\n tealA11: \"#0afed5d6\",\n tealA12: \"#b8ffebef\",\n};\nconst tealDarkP3 = {\n teal1: \"color(display-p3 0.059 0.083 0.079)\",\n teal2: \"color(display-p3 0.075 0.11 0.107)\",\n teal3: \"color(display-p3 0.087 0.175 0.165)\",\n teal4: \"color(display-p3 0.087 0.227 0.214)\",\n teal5: \"color(display-p3 0.12 0.277 0.261)\",\n teal6: \"color(display-p3 0.162 0.335 0.314)\",\n teal7: \"color(display-p3 0.205 0.406 0.379)\",\n teal8: \"color(display-p3 0.245 0.489 0.453)\",\n teal9: \"color(display-p3 0.297 0.637 0.581)\",\n teal10: \"color(display-p3 0.319 0.69 0.62)\",\n teal11: \"color(display-p3 0.388 0.835 0.719)\",\n teal12: \"color(display-p3 0.734 0.934 0.87)\",\n};\nconst tealDarkP3A = {\n tealA1: \"color(display-p3 0 0.992 0.761 / 0.017)\",\n tealA2: \"color(display-p3 0.235 0.988 0.902 / 0.047)\",\n tealA3: \"color(display-p3 0.235 1 0.898 / 0.118)\",\n tealA4: \"color(display-p3 0.18 0.996 0.929 / 0.173)\",\n tealA5: \"color(display-p3 0.31 1 0.933 / 0.227)\",\n tealA6: \"color(display-p3 0.396 1 0.933 / 0.286)\",\n tealA7: \"color(display-p3 0.443 1 0.925 / 0.366)\",\n tealA8: \"color(display-p3 0.459 1 0.925 / 0.454)\",\n tealA9: \"color(display-p3 0.443 0.996 0.906 / 0.61)\",\n tealA10: \"color(display-p3 0.439 0.996 0.89 / 0.669)\",\n tealA11: \"color(display-p3 0.388 0.835 0.719)\",\n tealA12: \"color(display-p3 0.734 0.934 0.87)\",\n};\nconst jadeDark = {\n jade1: \"#0d1512\",\n jade2: \"#121c18\",\n jade3: \"#0f2e22\",\n jade4: \"#0b3b2c\",\n jade5: \"#114837\",\n jade6: \"#1b5745\",\n jade7: \"#246854\",\n jade8: \"#2a7e68\",\n jade9: \"#29a383\",\n jade10: \"#27b08b\",\n jade11: \"#1fd8a4\",\n jade12: \"#adf0d4\",\n};\nconst jadeDarkA = {\n jadeA1: \"#00de4505\",\n jadeA2: \"#27fba60c\",\n jadeA3: \"#02f99920\",\n jadeA4: \"#00ffaa2d\",\n jadeA5: \"#11ffb63b\",\n jadeA6: \"#34ffc24b\",\n jadeA7: \"#45fdc75e\",\n jadeA8: \"#48ffcf75\",\n jadeA9: \"#38feca9d\",\n jadeA10: \"#31fec7ab\",\n jadeA11: \"#21fec0d6\",\n jadeA12: \"#b8ffe1ef\",\n};\nconst jadeDarkP3 = {\n jade1: \"color(display-p3 0.059 0.083 0.071)\",\n jade2: \"color(display-p3 0.078 0.11 0.094)\",\n jade3: \"color(display-p3 0.091 0.176 0.138)\",\n jade4: \"color(display-p3 0.102 0.228 0.177)\",\n jade5: \"color(display-p3 0.133 0.279 0.221)\",\n jade6: \"color(display-p3 0.174 0.334 0.273)\",\n jade7: \"color(display-p3 0.219 0.402 0.335)\",\n jade8: \"color(display-p3 0.263 0.488 0.411)\",\n jade9: \"color(display-p3 0.319 0.63 0.521)\",\n jade10: \"color(display-p3 0.338 0.68 0.555)\",\n jade11: \"color(display-p3 0.4 0.835 0.656)\",\n jade12: \"color(display-p3 0.734 0.934 0.838)\",\n};\nconst jadeDarkP3A = {\n jadeA1: \"color(display-p3 0 0.992 0.298 / 0.017)\",\n jadeA2: \"color(display-p3 0.318 0.988 0.651 / 0.047)\",\n jadeA3: \"color(display-p3 0.267 1 0.667 / 0.118)\",\n jadeA4: \"color(display-p3 0.275 0.996 0.702 / 0.173)\",\n jadeA5: \"color(display-p3 0.361 1 0.741 / 0.227)\",\n jadeA6: \"color(display-p3 0.439 1 0.796 / 0.286)\",\n jadeA7: \"color(display-p3 0.49 1 0.804 / 0.362)\",\n jadeA8: \"color(display-p3 0.506 1 0.835 / 0.45)\",\n jadeA9: \"color(display-p3 0.478 0.996 0.816 / 0.606)\",\n jadeA10: \"color(display-p3 0.478 1 0.816 / 0.656)\",\n jadeA11: \"color(display-p3 0.4 0.835 0.656)\",\n jadeA12: \"color(display-p3 0.734 0.934 0.838)\",\n};\nconst greenDark = {\n green1: \"#0e1512\",\n green2: \"#121b17\",\n green3: \"#132d21\",\n green4: \"#113b29\",\n green5: \"#174933\",\n green6: \"#20573e\",\n green7: \"#28684a\",\n green8: \"#2f7c57\",\n green9: \"#30a46c\",\n green10: \"#33b074\",\n green11: \"#3dd68c\",\n green12: \"#b1f1cb\",\n};\nconst greenDarkA = {\n greenA1: \"#00de4505\",\n greenA2: \"#29f99d0b\",\n greenA3: \"#22ff991e\",\n greenA4: \"#11ff992d\",\n greenA5: \"#2bffa23c\",\n greenA6: \"#44ffaa4b\",\n greenA7: \"#50fdac5e\",\n greenA8: \"#54ffad73\",\n greenA9: \"#44ffa49e\",\n greenA10: \"#43fea4ab\",\n greenA11: \"#46fea5d4\",\n greenA12: \"#bbffd7f0\",\n};\nconst greenDarkP3 = {\n green1: \"color(display-p3 0.062 0.083 0.071)\",\n green2: \"color(display-p3 0.079 0.106 0.09)\",\n green3: \"color(display-p3 0.1 0.173 0.133)\",\n green4: \"color(display-p3 0.115 0.229 0.166)\",\n green5: \"color(display-p3 0.147 0.282 0.206)\",\n green6: \"color(display-p3 0.185 0.338 0.25)\",\n green7: \"color(display-p3 0.227 0.403 0.298)\",\n green8: \"color(display-p3 0.27 0.479 0.351)\",\n green9: \"color(display-p3 0.332 0.634 0.442)\",\n green10: \"color(display-p3 0.357 0.682 0.474)\",\n green11: \"color(display-p3 0.434 0.828 0.573)\",\n green12: \"color(display-p3 0.747 0.938 0.807)\",\n};\nconst greenDarkP3A = {\n greenA1: \"color(display-p3 0 0.992 0.298 / 0.017)\",\n greenA2: \"color(display-p3 0.341 0.98 0.616 / 0.043)\",\n greenA3: \"color(display-p3 0.376 0.996 0.655 / 0.114)\",\n greenA4: \"color(display-p3 0.341 0.996 0.635 / 0.173)\",\n greenA5: \"color(display-p3 0.408 1 0.678 / 0.232)\",\n greenA6: \"color(display-p3 0.475 1 0.706 / 0.29)\",\n greenA7: \"color(display-p3 0.514 1 0.706 / 0.362)\",\n greenA8: \"color(display-p3 0.529 1 0.718 / 0.442)\",\n greenA9: \"color(display-p3 0.502 0.996 0.682 / 0.61)\",\n greenA10: \"color(display-p3 0.506 1 0.682 / 0.66)\",\n greenA11: \"color(display-p3 0.434 0.828 0.573)\",\n greenA12: \"color(display-p3 0.747 0.938 0.807)\",\n};\nconst grassDark = {\n grass1: \"#0e1511\",\n grass2: \"#141a15\",\n grass3: \"#1b2a1e\",\n grass4: \"#1d3a24\",\n grass5: \"#25482d\",\n grass6: \"#2d5736\",\n grass7: \"#366740\",\n grass8: \"#3e7949\",\n grass9: \"#46a758\",\n grass10: \"#53b365\",\n grass11: \"#71d083\",\n grass12: \"#c2f0c2\",\n};\nconst grassDarkA = {\n grassA1: \"#00de1205\",\n grassA2: \"#5ef7780a\",\n grassA3: \"#70fe8c1b\",\n grassA4: \"#57ff802c\",\n grassA5: \"#68ff8b3b\",\n grassA6: \"#71ff8f4b\",\n grassA7: \"#77fd925d\",\n grassA8: \"#77fd9070\",\n grassA9: \"#65ff82a1\",\n grassA10: \"#72ff8dae\",\n grassA11: \"#89ff9fcd\",\n grassA12: \"#ceffceef\",\n};\nconst grassDarkP3 = {\n grass1: \"color(display-p3 0.062 0.083 0.067)\",\n grass2: \"color(display-p3 0.083 0.103 0.085)\",\n grass3: \"color(display-p3 0.118 0.163 0.122)\",\n grass4: \"color(display-p3 0.142 0.225 0.15)\",\n grass5: \"color(display-p3 0.178 0.279 0.186)\",\n grass6: \"color(display-p3 0.217 0.337 0.224)\",\n grass7: \"color(display-p3 0.258 0.4 0.264)\",\n grass8: \"color(display-p3 0.302 0.47 0.305)\",\n grass9: \"color(display-p3 0.38 0.647 0.378)\",\n grass10: \"color(display-p3 0.426 0.694 0.426)\",\n grass11: \"color(display-p3 0.535 0.807 0.542)\",\n grass12: \"color(display-p3 0.797 0.936 0.776)\",\n};\nconst grassDarkP3A = {\n grassA1: \"color(display-p3 0 0.992 0.071 / 0.017)\",\n grassA2: \"color(display-p3 0.482 0.996 0.584 / 0.038)\",\n grassA3: \"color(display-p3 0.549 0.992 0.588 / 0.106)\",\n grassA4: \"color(display-p3 0.51 0.996 0.557 / 0.169)\",\n grassA5: \"color(display-p3 0.553 1 0.588 / 0.227)\",\n grassA6: \"color(display-p3 0.584 1 0.608 / 0.29)\",\n grassA7: \"color(display-p3 0.604 1 0.616 / 0.358)\",\n grassA8: \"color(display-p3 0.608 1 0.62 / 0.433)\",\n grassA9: \"color(display-p3 0.573 1 0.569 / 0.622)\",\n grassA10: \"color(display-p3 0.6 0.996 0.6 / 0.673)\",\n grassA11: \"color(display-p3 0.535 0.807 0.542)\",\n grassA12: \"color(display-p3 0.797 0.936 0.776)\",\n};\nconst brownDark = {\n brown1: \"#12110f\",\n brown2: \"#1c1816\",\n brown3: \"#28211d\",\n brown4: \"#322922\",\n brown5: \"#3e3128\",\n brown6: \"#4d3c2f\",\n brown7: \"#614a39\",\n brown8: \"#7c5f46\",\n brown9: \"#ad7f58\",\n brown10: \"#b88c67\",\n brown11: \"#dbb594\",\n brown12: \"#f2e1ca\",\n};\nconst brownDarkA = {\n brownA1: \"#91110002\",\n brownA2: \"#fba67c0c\",\n brownA3: \"#fcb58c19\",\n brownA4: \"#fbbb8a24\",\n brownA5: \"#fcb88931\",\n brownA6: \"#fdba8741\",\n brownA7: \"#ffbb8856\",\n brownA8: \"#ffbe8773\",\n brownA9: \"#feb87da8\",\n brownA10: \"#ffc18cb3\",\n brownA11: \"#fed1aad9\",\n brownA12: \"#feecd4f2\",\n};\nconst brownDarkP3 = {\n brown1: \"color(display-p3 0.071 0.067 0.059)\",\n brown2: \"color(display-p3 0.107 0.095 0.087)\",\n brown3: \"color(display-p3 0.151 0.13 0.115)\",\n brown4: \"color(display-p3 0.191 0.161 0.138)\",\n brown5: \"color(display-p3 0.235 0.194 0.162)\",\n brown6: \"color(display-p3 0.291 0.237 0.192)\",\n brown7: \"color(display-p3 0.365 0.295 0.232)\",\n brown8: \"color(display-p3 0.469 0.377 0.287)\",\n brown9: \"color(display-p3 0.651 0.505 0.368)\",\n brown10: \"color(display-p3 0.697 0.557 0.423)\",\n brown11: \"color(display-p3 0.835 0.715 0.597)\",\n brown12: \"color(display-p3 0.938 0.885 0.802)\",\n};\nconst brownDarkP3A = {\n brownA1: \"color(display-p3 0.855 0.071 0 / 0.005)\",\n brownA2: \"color(display-p3 0.98 0.706 0.525 / 0.043)\",\n brownA3: \"color(display-p3 0.996 0.745 0.576 / 0.093)\",\n brownA4: \"color(display-p3 1 0.765 0.592 / 0.135)\",\n brownA5: \"color(display-p3 1 0.761 0.588 / 0.181)\",\n brownA6: \"color(display-p3 1 0.773 0.592 / 0.24)\",\n brownA7: \"color(display-p3 0.996 0.776 0.58 / 0.32)\",\n brownA8: \"color(display-p3 1 0.78 0.573 / 0.433)\",\n brownA9: \"color(display-p3 1 0.769 0.549 / 0.627)\",\n brownA10: \"color(display-p3 1 0.792 0.596 / 0.677)\",\n brownA11: \"color(display-p3 0.835 0.715 0.597)\",\n brownA12: \"color(display-p3 0.938 0.885 0.802)\",\n};\nconst bronzeDark = {\n bronze1: \"#141110\",\n bronze2: \"#1c1917\",\n bronze3: \"#262220\",\n bronze4: \"#302a27\",\n bronze5: \"#3b3330\",\n bronze6: \"#493e3a\",\n bronze7: \"#5a4c47\",\n bronze8: \"#6f5f58\",\n bronze9: \"#a18072\",\n bronze10: \"#ae8c7e\",\n bronze11: \"#d4b3a5\",\n bronze12: \"#ede0d9\",\n};\nconst bronzeDarkA = {\n bronzeA1: \"#d1110004\",\n bronzeA2: \"#fbbc910c\",\n bronzeA3: \"#faceb817\",\n bronzeA4: \"#facdb622\",\n bronzeA5: \"#ffd2c12d\",\n bronzeA6: \"#ffd1c03c\",\n bronzeA7: \"#fdd0c04f\",\n bronzeA8: \"#ffd6c565\",\n bronzeA9: \"#fec7b09b\",\n bronzeA10: \"#fecab5a9\",\n bronzeA11: \"#ffd7c6d1\",\n bronzeA12: \"#fff1e9ec\",\n};\nconst bronzeDarkP3 = {\n bronze1: \"color(display-p3 0.076 0.067 0.063)\",\n bronze2: \"color(display-p3 0.106 0.097 0.093)\",\n bronze3: \"color(display-p3 0.147 0.132 0.125)\",\n bronze4: \"color(display-p3 0.185 0.166 0.156)\",\n bronze5: \"color(display-p3 0.227 0.202 0.19)\",\n bronze6: \"color(display-p3 0.278 0.246 0.23)\",\n bronze7: \"color(display-p3 0.343 0.302 0.281)\",\n bronze8: \"color(display-p3 0.426 0.374 0.347)\",\n bronze9: \"color(display-p3 0.611 0.507 0.455)\",\n bronze10: \"color(display-p3 0.66 0.556 0.504)\",\n bronze11: \"color(display-p3 0.81 0.707 0.655)\",\n bronze12: \"color(display-p3 0.921 0.88 0.854)\",\n};\nconst bronzeDarkP3A = {\n bronzeA1: \"color(display-p3 0.941 0.067 0 / 0.009)\",\n bronzeA2: \"color(display-p3 0.98 0.8 0.706 / 0.043)\",\n bronzeA3: \"color(display-p3 0.988 0.851 0.761 / 0.085)\",\n bronzeA4: \"color(display-p3 0.996 0.839 0.78 / 0.127)\",\n bronzeA5: \"color(display-p3 0.996 0.863 0.773 / 0.173)\",\n bronzeA6: \"color(display-p3 1 0.863 0.796 / 0.227)\",\n bronzeA7: \"color(display-p3 1 0.867 0.8 / 0.295)\",\n bronzeA8: \"color(display-p3 1 0.859 0.788 / 0.387)\",\n bronzeA9: \"color(display-p3 1 0.82 0.733 / 0.585)\",\n bronzeA10: \"color(display-p3 1 0.839 0.761 / 0.635)\",\n bronzeA11: \"color(display-p3 0.81 0.707 0.655)\",\n bronzeA12: \"color(display-p3 0.921 0.88 0.854)\",\n};\nconst goldDark = {\n gold1: \"#121211\",\n gold2: \"#1b1a17\",\n gold3: \"#24231f\",\n gold4: \"#2d2b26\",\n gold5: \"#38352e\",\n gold6: \"#444039\",\n gold7: \"#544f46\",\n gold8: \"#696256\",\n gold9: \"#978365\",\n gold10: \"#a39073\",\n gold11: \"#cbb99f\",\n gold12: \"#e8e2d9\",\n};\nconst goldDarkA = {\n goldA1: \"#91911102\",\n goldA2: \"#f9e29d0b\",\n goldA3: \"#f8ecbb15\",\n goldA4: \"#ffeec41e\",\n goldA5: \"#feecc22a\",\n goldA6: \"#feebcb37\",\n goldA7: \"#ffedcd48\",\n goldA8: \"#fdeaca5f\",\n goldA9: \"#ffdba690\",\n goldA10: \"#fedfb09d\",\n goldA11: \"#fee7c6c8\",\n goldA12: \"#fef7ede7\",\n};\nconst goldDarkP3 = {\n gold1: \"color(display-p3 0.071 0.071 0.067)\",\n gold2: \"color(display-p3 0.104 0.101 0.09)\",\n gold3: \"color(display-p3 0.141 0.136 0.122)\",\n gold4: \"color(display-p3 0.177 0.17 0.152)\",\n gold5: \"color(display-p3 0.217 0.207 0.185)\",\n gold6: \"color(display-p3 0.265 0.252 0.225)\",\n gold7: \"color(display-p3 0.327 0.31 0.277)\",\n gold8: \"color(display-p3 0.407 0.384 0.342)\",\n gold9: \"color(display-p3 0.579 0.517 0.41)\",\n gold10: \"color(display-p3 0.628 0.566 0.463)\",\n gold11: \"color(display-p3 0.784 0.728 0.635)\",\n gold12: \"color(display-p3 0.906 0.887 0.855)\",\n};\nconst goldDarkP3A = {\n goldA1: \"color(display-p3 0.855 0.855 0.071 / 0.005)\",\n goldA2: \"color(display-p3 0.98 0.89 0.616 / 0.043)\",\n goldA3: \"color(display-p3 1 0.949 0.753 / 0.08)\",\n goldA4: \"color(display-p3 1 0.933 0.8 / 0.118)\",\n goldA5: \"color(display-p3 1 0.949 0.804 / 0.16)\",\n goldA6: \"color(display-p3 1 0.925 0.8 / 0.215)\",\n goldA7: \"color(display-p3 1 0.945 0.831 / 0.278)\",\n goldA8: \"color(display-p3 1 0.937 0.82 / 0.366)\",\n goldA9: \"color(display-p3 0.996 0.882 0.69 / 0.551)\",\n goldA10: \"color(display-p3 1 0.894 0.725 / 0.601)\",\n goldA11: \"color(display-p3 0.784 0.728 0.635)\",\n goldA12: \"color(display-p3 0.906 0.887 0.855)\",\n};\nconst skyDark = {\n sky1: \"#0d141f\",\n sky2: \"#111a27\",\n sky3: \"#112840\",\n sky4: \"#113555\",\n sky5: \"#154467\",\n sky6: \"#1b537b\",\n sky7: \"#1f6692\",\n sky8: \"#197cae\",\n sky9: \"#7ce2fe\",\n sky10: \"#a8eeff\",\n sky11: \"#75c7f0\",\n sky12: \"#c2f3ff\",\n};\nconst skyDarkA = {\n skyA1: \"#0044ff0f\",\n skyA2: \"#1171fb18\",\n skyA3: \"#1184fc33\",\n skyA4: \"#128fff49\",\n skyA5: \"#1c9dfd5d\",\n skyA6: \"#28a5ff72\",\n skyA7: \"#2badfe8b\",\n skyA8: \"#1db2fea9\",\n skyA9: \"#7ce3fffe\",\n skyA10: \"#a8eeff\",\n skyA11: \"#7cd3ffef\",\n skyA12: \"#c2f3ff\",\n};\nconst skyDarkP3 = {\n sky1: \"color(display-p3 0.056 0.078 0.116)\",\n sky2: \"color(display-p3 0.075 0.101 0.149)\",\n sky3: \"color(display-p3 0.089 0.154 0.244)\",\n sky4: \"color(display-p3 0.106 0.207 0.323)\",\n sky5: \"color(display-p3 0.135 0.261 0.394)\",\n sky6: \"color(display-p3 0.17 0.322 0.469)\",\n sky7: \"color(display-p3 0.205 0.394 0.557)\",\n sky8: \"color(display-p3 0.232 0.48 0.665)\",\n sky9: \"color(display-p3 0.585 0.877 0.983)\",\n sky10: \"color(display-p3 0.718 0.925 0.991)\",\n sky11: \"color(display-p3 0.536 0.772 0.924)\",\n sky12: \"color(display-p3 0.799 0.947 0.993)\",\n};\nconst skyDarkP3A = {\n skyA1: \"color(display-p3 0 0.282 0.996 / 0.055)\",\n skyA2: \"color(display-p3 0.157 0.467 0.992 / 0.089)\",\n skyA3: \"color(display-p3 0.192 0.522 0.996 / 0.19)\",\n skyA4: \"color(display-p3 0.212 0.584 1 / 0.274)\",\n skyA5: \"color(display-p3 0.259 0.631 1 / 0.349)\",\n skyA6: \"color(display-p3 0.302 0.655 1 / 0.433)\",\n skyA7: \"color(display-p3 0.329 0.686 1 / 0.526)\",\n skyA8: \"color(display-p3 0.325 0.71 1 / 0.643)\",\n skyA9: \"color(display-p3 0.592 0.894 1 / 0.984)\",\n skyA10: \"color(display-p3 0.722 0.933 1 / 0.992)\",\n skyA11: \"color(display-p3 0.536 0.772 0.924)\",\n skyA12: \"color(display-p3 0.799 0.947 0.993)\",\n};\nconst mintDark = {\n mint1: \"#0e1515\",\n mint2: \"#0f1b1b\",\n mint3: \"#092c2b\",\n mint4: \"#003a38\",\n mint5: \"#004744\",\n mint6: \"#105650\",\n mint7: \"#1e685f\",\n mint8: \"#277f70\",\n mint9: \"#86ead4\",\n mint10: \"#a8f5e5\",\n mint11: \"#58d5ba\",\n mint12: \"#c4f5e1\",\n};\nconst mintDarkA = {\n mintA1: \"#00dede05\",\n mintA2: \"#00f9f90b\",\n mintA3: \"#00fff61d\",\n mintA4: \"#00fff42c\",\n mintA5: \"#00fff23a\",\n mintA6: \"#0effeb4a\",\n mintA7: \"#34fde55e\",\n mintA8: \"#41ffdf76\",\n mintA9: \"#92ffe7e9\",\n mintA10: \"#aefeedf5\",\n mintA11: \"#67ffded2\",\n mintA12: \"#cbfee9f5\",\n};\nconst mintDarkP3 = {\n mint1: \"color(display-p3 0.059 0.082 0.081)\",\n mint2: \"color(display-p3 0.068 0.104 0.105)\",\n mint3: \"color(display-p3 0.077 0.17 0.168)\",\n mint4: \"color(display-p3 0.068 0.224 0.22)\",\n mint5: \"color(display-p3 0.104 0.275 0.264)\",\n mint6: \"color(display-p3 0.154 0.332 0.313)\",\n mint7: \"color(display-p3 0.207 0.403 0.373)\",\n mint8: \"color(display-p3 0.258 0.49 0.441)\",\n mint9: \"color(display-p3 0.62 0.908 0.834)\",\n mint10: \"color(display-p3 0.725 0.954 0.898)\",\n mint11: \"color(display-p3 0.482 0.825 0.733)\",\n mint12: \"color(display-p3 0.807 0.955 0.887)\",\n};\nconst mintDarkP3A = {\n mintA1: \"color(display-p3 0 0.992 0.992 / 0.017)\",\n mintA2: \"color(display-p3 0.071 0.98 0.98 / 0.043)\",\n mintA3: \"color(display-p3 0.176 0.996 0.996 / 0.11)\",\n mintA4: \"color(display-p3 0.071 0.996 0.973 / 0.169)\",\n mintA5: \"color(display-p3 0.243 1 0.949 / 0.223)\",\n mintA6: \"color(display-p3 0.369 1 0.933 / 0.286)\",\n mintA7: \"color(display-p3 0.459 1 0.914 / 0.362)\",\n mintA8: \"color(display-p3 0.49 1 0.89 / 0.454)\",\n mintA9: \"color(display-p3 0.678 0.996 0.914 / 0.904)\",\n mintA10: \"color(display-p3 0.761 1 0.941 / 0.95)\",\n mintA11: \"color(display-p3 0.482 0.825 0.733)\",\n mintA12: \"color(display-p3 0.807 0.955 0.887)\",\n};\nconst limeDark = {\n lime1: \"#11130c\",\n lime2: \"#151a10\",\n lime3: \"#1f2917\",\n lime4: \"#29371d\",\n lime5: \"#334423\",\n lime6: \"#3d522a\",\n lime7: \"#496231\",\n lime8: \"#577538\",\n lime9: \"#bdee63\",\n lime10: \"#d4ff70\",\n lime11: \"#bde56c\",\n lime12: \"#e3f7ba\",\n};\nconst limeDarkA = {\n limeA1: \"#11bb0003\",\n limeA2: \"#78f7000a\",\n limeA3: \"#9bfd4c1a\",\n limeA4: \"#a7fe5c29\",\n limeA5: \"#affe6537\",\n limeA6: \"#b2fe6d46\",\n limeA7: \"#b6ff6f57\",\n limeA8: \"#b6fd6d6c\",\n limeA9: \"#caff69ed\",\n limeA10: \"#d4ff70\",\n limeA11: \"#d1fe77e4\",\n limeA12: \"#e9febff7\",\n};\nconst limeDarkP3 = {\n lime1: \"color(display-p3 0.067 0.073 0.048)\",\n lime2: \"color(display-p3 0.086 0.1 0.067)\",\n lime3: \"color(display-p3 0.13 0.16 0.099)\",\n lime4: \"color(display-p3 0.172 0.214 0.126)\",\n lime5: \"color(display-p3 0.213 0.266 0.153)\",\n lime6: \"color(display-p3 0.257 0.321 0.182)\",\n lime7: \"color(display-p3 0.307 0.383 0.215)\",\n lime8: \"color(display-p3 0.365 0.456 0.25)\",\n lime9: \"color(display-p3 0.78 0.928 0.466)\",\n lime10: \"color(display-p3 0.865 0.995 0.519)\",\n lime11: \"color(display-p3 0.771 0.893 0.485)\",\n lime12: \"color(display-p3 0.905 0.966 0.753)\",\n};\nconst limeDarkP3A = {\n limeA1: \"color(display-p3 0.067 0.941 0 / 0.009)\",\n limeA2: \"color(display-p3 0.584 0.996 0.071 / 0.038)\",\n limeA3: \"color(display-p3 0.69 1 0.38 / 0.101)\",\n limeA4: \"color(display-p3 0.729 1 0.435 / 0.16)\",\n limeA5: \"color(display-p3 0.745 1 0.471 / 0.215)\",\n limeA6: \"color(display-p3 0.769 1 0.482 / 0.274)\",\n limeA7: \"color(display-p3 0.769 1 0.506 / 0.341)\",\n limeA8: \"color(display-p3 0.784 1 0.51 / 0.416)\",\n limeA9: \"color(display-p3 0.839 1 0.502 / 0.925)\",\n limeA10: \"color(display-p3 0.871 1 0.522 / 0.996)\",\n limeA11: \"color(display-p3 0.771 0.893 0.485)\",\n limeA12: \"color(display-p3 0.905 0.966 0.753)\",\n};\nconst yellowDark = {\n yellow1: \"#14120b\",\n yellow2: \"#1b180f\",\n yellow3: \"#2d2305\",\n yellow4: \"#362b00\",\n yellow5: \"#433500\",\n yellow6: \"#524202\",\n yellow7: \"#665417\",\n yellow8: \"#836a21\",\n yellow9: \"#ffe629\",\n yellow10: \"#ffff57\",\n yellow11: \"#f5e147\",\n yellow12: \"#f6eeb4\",\n};\nconst yellowDarkA = {\n yellowA1: \"#d1510004\",\n yellowA2: \"#f9b4000b\",\n yellowA3: \"#ffaa001e\",\n yellowA4: \"#fdb70028\",\n yellowA5: \"#febb0036\",\n yellowA6: \"#fec40046\",\n yellowA7: \"#fdcb225c\",\n yellowA8: \"#fdca327b\",\n yellowA9: \"#ffe629\",\n yellowA10: \"#ffff57\",\n yellowA11: \"#fee949f5\",\n yellowA12: \"#fef6baf6\",\n};\nconst yellowDarkP3 = {\n yellow1: \"color(display-p3 0.078 0.069 0.047)\",\n yellow2: \"color(display-p3 0.103 0.094 0.063)\",\n yellow3: \"color(display-p3 0.168 0.137 0.039)\",\n yellow4: \"color(display-p3 0.209 0.169 0)\",\n yellow5: \"color(display-p3 0.255 0.209 0)\",\n yellow6: \"color(display-p3 0.31 0.261 0.07)\",\n yellow7: \"color(display-p3 0.389 0.331 0.135)\",\n yellow8: \"color(display-p3 0.497 0.42 0.182)\",\n yellow9: \"color(display-p3 1 0.92 0.22)\",\n yellow10: \"color(display-p3 1 1 0.456)\",\n yellow11: \"color(display-p3 0.948 0.885 0.392)\",\n yellow12: \"color(display-p3 0.959 0.934 0.731)\",\n};\nconst yellowDarkP3A = {\n yellowA1: \"color(display-p3 0.973 0.369 0 / 0.013)\",\n yellowA2: \"color(display-p3 0.996 0.792 0 / 0.038)\",\n yellowA3: \"color(display-p3 0.996 0.71 0 / 0.11)\",\n yellowA4: \"color(display-p3 0.996 0.741 0 / 0.152)\",\n yellowA5: \"color(display-p3 0.996 0.765 0 / 0.202)\",\n yellowA6: \"color(display-p3 0.996 0.816 0.082 / 0.261)\",\n yellowA7: \"color(display-p3 1 0.831 0.263 / 0.345)\",\n yellowA8: \"color(display-p3 1 0.831 0.314 / 0.463)\",\n yellowA9: \"color(display-p3 1 0.922 0.22)\",\n yellowA10: \"color(display-p3 1 1 0.455)\",\n yellowA11: \"color(display-p3 0.948 0.885 0.392)\",\n yellowA12: \"color(display-p3 0.959 0.934 0.731)\",\n};\nconst amberDark = {\n amber1: \"#16120c\",\n amber2: \"#1d180f\",\n amber3: \"#302008\",\n amber4: \"#3f2700\",\n amber5: \"#4d3000\",\n amber6: \"#5c3d05\",\n amber7: \"#714f19\",\n amber8: \"#8f6424\",\n amber9: \"#ffc53d\",\n amber10: \"#ffd60a\",\n amber11: \"#ffca16\",\n amber12: \"#ffe7b3\",\n};\nconst amberDarkA = {\n amberA1: \"#e63c0006\",\n amberA2: \"#fd9b000d\",\n amberA3: \"#fa820022\",\n amberA4: \"#fc820032\",\n amberA5: \"#fd8b0041\",\n amberA6: \"#fd9b0051\",\n amberA7: \"#ffab2567\",\n amberA8: \"#ffae3587\",\n amberA9: \"#ffc53d\",\n amberA10: \"#ffd60a\",\n amberA11: \"#ffca16\",\n amberA12: \"#ffe7b3\",\n};\nconst amberDarkP3 = {\n amber1: \"color(display-p3 0.082 0.07 0.05)\",\n amber2: \"color(display-p3 0.111 0.094 0.064)\",\n amber3: \"color(display-p3 0.178 0.128 0.049)\",\n amber4: \"color(display-p3 0.239 0.156 0)\",\n amber5: \"color(display-p3 0.29 0.193 0)\",\n amber6: \"color(display-p3 0.344 0.245 0.076)\",\n amber7: \"color(display-p3 0.422 0.314 0.141)\",\n amber8: \"color(display-p3 0.535 0.399 0.189)\",\n amber9: \"color(display-p3 1 0.77 0.26)\",\n amber10: \"color(display-p3 1 0.87 0.15)\",\n amber11: \"color(display-p3 1 0.8 0.29)\",\n amber12: \"color(display-p3 0.984 0.909 0.726)\",\n};\nconst amberDarkP3A = {\n amberA1: \"color(display-p3 0.992 0.298 0 / 0.017)\",\n amberA2: \"color(display-p3 0.988 0.651 0 / 0.047)\",\n amberA3: \"color(display-p3 1 0.6 0 / 0.118)\",\n amberA4: \"color(display-p3 1 0.557 0 / 0.185)\",\n amberA5: \"color(display-p3 1 0.592 0 / 0.24)\",\n amberA6: \"color(display-p3 1 0.659 0.094 / 0.299)\",\n amberA7: \"color(display-p3 1 0.714 0.263 / 0.383)\",\n amberA8: \"color(display-p3 0.996 0.729 0.306 / 0.5)\",\n amberA9: \"color(display-p3 1 0.769 0.259)\",\n amberA10: \"color(display-p3 1 0.871 0.149)\",\n amberA11: \"color(display-p3 1 0.8 0.29)\",\n amberA12: \"color(display-p3 0.984 0.909 0.726)\",\n};\nconst orangeDark = {\n orange1: \"#17120e\",\n orange2: \"#1e160f\",\n orange3: \"#331e0b\",\n orange4: \"#462100\",\n orange5: \"#562800\",\n orange6: \"#66350c\",\n orange7: \"#7e451d\",\n orange8: \"#a35829\",\n orange9: \"#f76b15\",\n orange10: \"#ff801f\",\n orange11: \"#ffa057\",\n orange12: \"#ffe0c2\",\n};\nconst orangeDarkA = {\n orangeA1: \"#ec360007\",\n orangeA2: \"#fe6d000e\",\n orangeA3: \"#fb6a0025\",\n orangeA4: \"#ff590039\",\n orangeA5: \"#ff61004a\",\n orangeA6: \"#fd75045c\",\n orangeA7: \"#ff832c75\",\n orangeA8: \"#fe84389d\",\n orangeA9: \"#fe6d15f7\",\n orangeA10: \"#ff801f\",\n orangeA11: \"#ffa057\",\n orangeA12: \"#ffe0c2\",\n};\nconst orangeDarkP3 = {\n orange1: \"color(display-p3 0.088 0.07 0.057)\",\n orange2: \"color(display-p3 0.113 0.089 0.061)\",\n orange3: \"color(display-p3 0.189 0.12 0.056)\",\n orange4: \"color(display-p3 0.262 0.132 0)\",\n orange5: \"color(display-p3 0.315 0.168 0.016)\",\n orange6: \"color(display-p3 0.376 0.219 0.088)\",\n orange7: \"color(display-p3 0.465 0.283 0.147)\",\n orange8: \"color(display-p3 0.601 0.359 0.201)\",\n orange9: \"color(display-p3 0.9 0.45 0.2)\",\n orange10: \"color(display-p3 0.98 0.51 0.23)\",\n orange11: \"color(display-p3 1 0.63 0.38)\",\n orange12: \"color(display-p3 0.98 0.883 0.775)\",\n};\nconst orangeDarkP3A = {\n orangeA1: \"color(display-p3 0.961 0.247 0 / 0.022)\",\n orangeA2: \"color(display-p3 0.992 0.529 0 / 0.051)\",\n orangeA3: \"color(display-p3 0.996 0.486 0 / 0.131)\",\n orangeA4: \"color(display-p3 0.996 0.384 0 / 0.211)\",\n orangeA5: \"color(display-p3 1 0.455 0 / 0.265)\",\n orangeA6: \"color(display-p3 1 0.529 0.129 / 0.332)\",\n orangeA7: \"color(display-p3 1 0.569 0.251 / 0.429)\",\n orangeA8: \"color(display-p3 1 0.584 0.302 / 0.572)\",\n orangeA9: \"color(display-p3 1 0.494 0.216 / 0.895)\",\n orangeA10: \"color(display-p3 1 0.522 0.235 / 0.979)\",\n orangeA11: \"color(display-p3 1 0.63 0.38)\",\n orangeA12: \"color(display-p3 0.98 0.883 0.775)\",\n};\n\nconst gray = {\n gray1: \"#fcfcfc\",\n gray2: \"#f9f9f9\",\n gray3: \"#f0f0f0\",\n gray4: \"#e8e8e8\",\n gray5: \"#e0e0e0\",\n gray6: \"#d9d9d9\",\n gray7: \"#cecece\",\n gray8: \"#bbbbbb\",\n gray9: \"#8d8d8d\",\n gray10: \"#838383\",\n gray11: \"#646464\",\n gray12: \"#202020\",\n};\nconst grayA = {\n grayA1: \"#00000003\",\n grayA2: \"#00000006\",\n grayA3: \"#0000000f\",\n grayA4: \"#00000017\",\n grayA5: \"#0000001f\",\n grayA6: \"#00000026\",\n grayA7: \"#00000031\",\n grayA8: \"#00000044\",\n grayA9: \"#00000072\",\n grayA10: \"#0000007c\",\n grayA11: \"#0000009b\",\n grayA12: \"#000000df\",\n};\nconst grayP3 = {\n gray1: \"color(display-p3 0.988 0.988 0.988)\",\n gray2: \"color(display-p3 0.975 0.975 0.975)\",\n gray3: \"color(display-p3 0.939 0.939 0.939)\",\n gray4: \"color(display-p3 0.908 0.908 0.908)\",\n gray5: \"color(display-p3 0.88 0.88 0.88)\",\n gray6: \"color(display-p3 0.849 0.849 0.849)\",\n gray7: \"color(display-p3 0.807 0.807 0.807)\",\n gray8: \"color(display-p3 0.732 0.732 0.732)\",\n gray9: \"color(display-p3 0.553 0.553 0.553)\",\n gray10: \"color(display-p3 0.512 0.512 0.512)\",\n gray11: \"color(display-p3 0.392 0.392 0.392)\",\n gray12: \"color(display-p3 0.125 0.125 0.125)\",\n};\nconst grayP3A = {\n grayA1: \"color(display-p3 0 0 0 / 0.012)\",\n grayA2: \"color(display-p3 0 0 0 / 0.024)\",\n grayA3: \"color(display-p3 0 0 0 / 0.063)\",\n grayA4: \"color(display-p3 0 0 0 / 0.09)\",\n grayA5: \"color(display-p3 0 0 0 / 0.122)\",\n grayA6: \"color(display-p3 0 0 0 / 0.153)\",\n grayA7: \"color(display-p3 0 0 0 / 0.192)\",\n grayA8: \"color(display-p3 0 0 0 / 0.267)\",\n grayA9: \"color(display-p3 0 0 0 / 0.447)\",\n grayA10: \"color(display-p3 0 0 0 / 0.486)\",\n grayA11: \"color(display-p3 0 0 0 / 0.608)\",\n grayA12: \"color(display-p3 0 0 0 / 0.875)\",\n};\nconst mauve = {\n mauve1: \"#fdfcfd\",\n mauve2: \"#faf9fb\",\n mauve3: \"#f2eff3\",\n mauve4: \"#eae7ec\",\n mauve5: \"#e3dfe6\",\n mauve6: \"#dbd8e0\",\n mauve7: \"#d0cdd7\",\n mauve8: \"#bcbac7\",\n mauve9: \"#8e8c99\",\n mauve10: \"#84828e\",\n mauve11: \"#65636d\",\n mauve12: \"#211f26\",\n};\nconst mauveA = {\n mauveA1: \"#55005503\",\n mauveA2: \"#2b005506\",\n mauveA3: \"#30004010\",\n mauveA4: \"#20003618\",\n mauveA5: \"#20003820\",\n mauveA6: \"#14003527\",\n mauveA7: \"#10003332\",\n mauveA8: \"#08003145\",\n mauveA9: \"#05001d73\",\n mauveA10: \"#0500197d\",\n mauveA11: \"#0400119c\",\n mauveA12: \"#020008e0\",\n};\nconst mauveP3 = {\n mauve1: \"color(display-p3 0.991 0.988 0.992)\",\n mauve2: \"color(display-p3 0.98 0.976 0.984)\",\n mauve3: \"color(display-p3 0.946 0.938 0.952)\",\n mauve4: \"color(display-p3 0.915 0.906 0.925)\",\n mauve5: \"color(display-p3 0.886 0.876 0.901)\",\n mauve6: \"color(display-p3 0.856 0.846 0.875)\",\n mauve7: \"color(display-p3 0.814 0.804 0.84)\",\n mauve8: \"color(display-p3 0.735 0.728 0.777)\",\n mauve9: \"color(display-p3 0.555 0.549 0.596)\",\n mauve10: \"color(display-p3 0.514 0.508 0.552)\",\n mauve11: \"color(display-p3 0.395 0.388 0.424)\",\n mauve12: \"color(display-p3 0.128 0.122 0.147)\",\n};\nconst mauveP3A = {\n mauveA1: \"color(display-p3 0.349 0.024 0.349 / 0.012)\",\n mauveA2: \"color(display-p3 0.184 0.024 0.349 / 0.024)\",\n mauveA3: \"color(display-p3 0.129 0.008 0.255 / 0.063)\",\n mauveA4: \"color(display-p3 0.094 0.012 0.216 / 0.095)\",\n mauveA5: \"color(display-p3 0.098 0.008 0.224 / 0.126)\",\n mauveA6: \"color(display-p3 0.055 0.004 0.18 / 0.153)\",\n mauveA7: \"color(display-p3 0.067 0.008 0.184 / 0.197)\",\n mauveA8: \"color(display-p3 0.02 0.004 0.176 / 0.271)\",\n mauveA9: \"color(display-p3 0.02 0.004 0.106 / 0.451)\",\n mauveA10: \"color(display-p3 0.012 0.004 0.09 / 0.491)\",\n mauveA11: \"color(display-p3 0.016 0 0.059 / 0.612)\",\n mauveA12: \"color(display-p3 0.008 0 0.027 / 0.879)\",\n};\nconst slate = {\n slate1: \"#fcfcfd\",\n slate2: \"#f9f9fb\",\n slate3: \"#f0f0f3\",\n slate4: \"#e8e8ec\",\n slate5: \"#e0e1e6\",\n slate6: \"#d9d9e0\",\n slate7: \"#cdced6\",\n slate8: \"#b9bbc6\",\n slate9: \"#8b8d98\",\n slate10: \"#80838d\",\n slate11: \"#60646c\",\n slate12: \"#1c2024\",\n};\nconst slateA = {\n slateA1: \"#00005503\",\n slateA2: \"#00005506\",\n slateA3: \"#0000330f\",\n slateA4: \"#00002d17\",\n slateA5: \"#0009321f\",\n slateA6: \"#00002f26\",\n slateA7: \"#00062e32\",\n slateA8: \"#00083046\",\n slateA9: \"#00051d74\",\n slateA10: \"#00071b7f\",\n slateA11: \"#0007149f\",\n slateA12: \"#000509e3\",\n};\nconst slateP3 = {\n slate1: \"color(display-p3 0.988 0.988 0.992)\",\n slate2: \"color(display-p3 0.976 0.976 0.984)\",\n slate3: \"color(display-p3 0.94 0.941 0.953)\",\n slate4: \"color(display-p3 0.908 0.909 0.925)\",\n slate5: \"color(display-p3 0.88 0.881 0.901)\",\n slate6: \"color(display-p3 0.85 0.852 0.876)\",\n slate7: \"color(display-p3 0.805 0.808 0.838)\",\n slate8: \"color(display-p3 0.727 0.733 0.773)\",\n slate9: \"color(display-p3 0.547 0.553 0.592)\",\n slate10: \"color(display-p3 0.503 0.512 0.549)\",\n slate11: \"color(display-p3 0.379 0.392 0.421)\",\n slate12: \"color(display-p3 0.113 0.125 0.14)\",\n};\nconst slateP3A = {\n slateA1: \"color(display-p3 0.024 0.024 0.349 / 0.012)\",\n slateA2: \"color(display-p3 0.024 0.024 0.349 / 0.024)\",\n slateA3: \"color(display-p3 0.004 0.004 0.204 / 0.059)\",\n slateA4: \"color(display-p3 0.012 0.012 0.184 / 0.091)\",\n slateA5: \"color(display-p3 0.004 0.039 0.2 / 0.122)\",\n slateA6: \"color(display-p3 0.008 0.008 0.165 / 0.15)\",\n slateA7: \"color(display-p3 0.008 0.027 0.184 / 0.197)\",\n slateA8: \"color(display-p3 0.004 0.031 0.176 / 0.275)\",\n slateA9: \"color(display-p3 0.004 0.02 0.106 / 0.455)\",\n slateA10: \"color(display-p3 0.004 0.027 0.098 / 0.499)\",\n slateA11: \"color(display-p3 0 0.02 0.063 / 0.62)\",\n slateA12: \"color(display-p3 0 0.012 0.031 / 0.887)\",\n};\nconst sage = {\n sage1: \"#fbfdfc\",\n sage2: \"#f7f9f8\",\n sage3: \"#eef1f0\",\n sage4: \"#e6e9e8\",\n sage5: \"#dfe2e0\",\n sage6: \"#d7dad9\",\n sage7: \"#cbcfcd\",\n sage8: \"#b8bcba\",\n sage9: \"#868e8b\",\n sage10: \"#7c8481\",\n sage11: \"#5f6563\",\n sage12: \"#1a211e\",\n};\nconst sageA = {\n sageA1: \"#00804004\",\n sageA2: \"#00402008\",\n sageA3: \"#002d1e11\",\n sageA4: \"#001f1519\",\n sageA5: \"#00180820\",\n sageA6: \"#00140d28\",\n sageA7: \"#00140a34\",\n sageA8: \"#000f0847\",\n sageA9: \"#00110b79\",\n sageA10: \"#00100a83\",\n sageA11: \"#000a07a0\",\n sageA12: \"#000805e5\",\n};\nconst sageP3 = {\n sage1: \"color(display-p3 0.986 0.992 0.988)\",\n sage2: \"color(display-p3 0.97 0.977 0.974)\",\n sage3: \"color(display-p3 0.935 0.944 0.94)\",\n sage4: \"color(display-p3 0.904 0.913 0.909)\",\n sage5: \"color(display-p3 0.875 0.885 0.88)\",\n sage6: \"color(display-p3 0.844 0.854 0.849)\",\n sage7: \"color(display-p3 0.8 0.811 0.806)\",\n sage8: \"color(display-p3 0.725 0.738 0.732)\",\n sage9: \"color(display-p3 0.531 0.556 0.546)\",\n sage10: \"color(display-p3 0.492 0.515 0.506)\",\n sage11: \"color(display-p3 0.377 0.395 0.389)\",\n sage12: \"color(display-p3 0.107 0.129 0.118)\",\n};\nconst sageP3A = {\n sageA1: \"color(display-p3 0.024 0.514 0.267 / 0.016)\",\n sageA2: \"color(display-p3 0.02 0.267 0.145 / 0.032)\",\n sageA3: \"color(display-p3 0.008 0.184 0.125 / 0.067)\",\n sageA4: \"color(display-p3 0.012 0.094 0.051 / 0.095)\",\n sageA5: \"color(display-p3 0.008 0.098 0.035 / 0.126)\",\n sageA6: \"color(display-p3 0.004 0.078 0.027 / 0.157)\",\n sageA7: \"color(display-p3 0 0.059 0.039 / 0.2)\",\n sageA8: \"color(display-p3 0.004 0.047 0.031 / 0.275)\",\n sageA9: \"color(display-p3 0.004 0.059 0.035 / 0.471)\",\n sageA10: \"color(display-p3 0 0.047 0.031 / 0.51)\",\n sageA11: \"color(display-p3 0 0.031 0.02 / 0.624)\",\n sageA12: \"color(display-p3 0 0.027 0.012 / 0.895)\",\n};\nconst olive = {\n olive1: \"#fcfdfc\",\n olive2: \"#f8faf8\",\n olive3: \"#eff1ef\",\n olive4: \"#e7e9e7\",\n olive5: \"#dfe2df\",\n olive6: \"#d7dad7\",\n olive7: \"#cccfcc\",\n olive8: \"#b9bcb8\",\n olive9: \"#898e87\",\n olive10: \"#7f847d\",\n olive11: \"#60655f\",\n olive12: \"#1d211c\",\n};\nconst oliveA = {\n oliveA1: \"#00550003\",\n oliveA2: \"#00490007\",\n oliveA3: \"#00200010\",\n oliveA4: \"#00160018\",\n oliveA5: \"#00180020\",\n oliveA6: \"#00140028\",\n oliveA7: \"#000f0033\",\n oliveA8: \"#040f0047\",\n oliveA9: \"#050f0078\",\n oliveA10: \"#040e0082\",\n oliveA11: \"#020a00a0\",\n oliveA12: \"#010600e3\",\n};\nconst oliveP3 = {\n olive1: \"color(display-p3 0.989 0.992 0.989)\",\n olive2: \"color(display-p3 0.974 0.98 0.973)\",\n olive3: \"color(display-p3 0.939 0.945 0.937)\",\n olive4: \"color(display-p3 0.907 0.914 0.905)\",\n olive5: \"color(display-p3 0.878 0.885 0.875)\",\n olive6: \"color(display-p3 0.846 0.855 0.843)\",\n olive7: \"color(display-p3 0.803 0.812 0.8)\",\n olive8: \"color(display-p3 0.727 0.738 0.723)\",\n olive9: \"color(display-p3 0.541 0.556 0.532)\",\n olive10: \"color(display-p3 0.5 0.515 0.491)\",\n olive11: \"color(display-p3 0.38 0.395 0.374)\",\n olive12: \"color(display-p3 0.117 0.129 0.111)\",\n};\nconst oliveP3A = {\n oliveA1: \"color(display-p3 0.024 0.349 0.024 / 0.012)\",\n oliveA2: \"color(display-p3 0.024 0.302 0.024 / 0.028)\",\n oliveA3: \"color(display-p3 0.008 0.129 0.008 / 0.063)\",\n oliveA4: \"color(display-p3 0.012 0.094 0.012 / 0.095)\",\n oliveA5: \"color(display-p3 0.035 0.098 0.008 / 0.126)\",\n oliveA6: \"color(display-p3 0.027 0.078 0.004 / 0.157)\",\n oliveA7: \"color(display-p3 0.02 0.059 0 / 0.2)\",\n oliveA8: \"color(display-p3 0.02 0.059 0.004 / 0.279)\",\n oliveA9: \"color(display-p3 0.02 0.051 0.004 / 0.467)\",\n oliveA10: \"color(display-p3 0.024 0.047 0 / 0.51)\",\n oliveA11: \"color(display-p3 0.012 0.039 0 / 0.628)\",\n oliveA12: \"color(display-p3 0.008 0.024 0 / 0.891)\",\n};\nconst sand = {\n sand1: \"#fdfdfc\",\n sand2: \"#f9f9f8\",\n sand3: \"#f1f0ef\",\n sand4: \"#e9e8e6\",\n sand5: \"#e2e1de\",\n sand6: \"#dad9d6\",\n sand7: \"#cfceca\",\n sand8: \"#bcbbb5\",\n sand9: \"#8d8d86\",\n sand10: \"#82827c\",\n sand11: \"#63635e\",\n sand12: \"#21201c\",\n};\nconst sandA = {\n sandA1: \"#55550003\",\n sandA2: \"#25250007\",\n sandA3: \"#20100010\",\n sandA4: \"#1f150019\",\n sandA5: \"#1f180021\",\n sandA6: \"#19130029\",\n sandA7: \"#19140035\",\n sandA8: \"#1915014a\",\n sandA9: \"#0f0f0079\",\n sandA10: \"#0c0c0083\",\n sandA11: \"#080800a1\",\n sandA12: \"#060500e3\",\n};\nconst sandP3 = {\n sand1: \"color(display-p3 0.992 0.992 0.989)\",\n sand2: \"color(display-p3 0.977 0.977 0.973)\",\n sand3: \"color(display-p3 0.943 0.942 0.936)\",\n sand4: \"color(display-p3 0.913 0.912 0.903)\",\n sand5: \"color(display-p3 0.885 0.883 0.873)\",\n sand6: \"color(display-p3 0.854 0.852 0.839)\",\n sand7: \"color(display-p3 0.813 0.81 0.794)\",\n sand8: \"color(display-p3 0.738 0.734 0.713)\",\n sand9: \"color(display-p3 0.553 0.553 0.528)\",\n sand10: \"color(display-p3 0.511 0.511 0.488)\",\n sand11: \"color(display-p3 0.388 0.388 0.37)\",\n sand12: \"color(display-p3 0.129 0.126 0.111)\",\n};\nconst sandP3A = {\n sandA1: \"color(display-p3 0.349 0.349 0.024 / 0.012)\",\n sandA2: \"color(display-p3 0.161 0.161 0.024 / 0.028)\",\n sandA3: \"color(display-p3 0.067 0.067 0.008 / 0.063)\",\n sandA4: \"color(display-p3 0.129 0.129 0.012 / 0.099)\",\n sandA5: \"color(display-p3 0.098 0.067 0.008 / 0.126)\",\n sandA6: \"color(display-p3 0.102 0.075 0.004 / 0.161)\",\n sandA7: \"color(display-p3 0.098 0.098 0.004 / 0.208)\",\n sandA8: \"color(display-p3 0.086 0.075 0.004 / 0.287)\",\n sandA9: \"color(display-p3 0.051 0.051 0.004 / 0.471)\",\n sandA10: \"color(display-p3 0.047 0.047 0 / 0.514)\",\n sandA11: \"color(display-p3 0.031 0.031 0 / 0.632)\",\n sandA12: \"color(display-p3 0.024 0.02 0 / 0.891)\",\n};\nconst tomato = {\n tomato1: \"#fffcfc\",\n tomato2: \"#fff8f7\",\n tomato3: \"#feebe7\",\n tomato4: \"#ffdcd3\",\n tomato5: \"#ffcdc2\",\n tomato6: \"#fdbdaf\",\n tomato7: \"#f5a898\",\n tomato8: \"#ec8e7b\",\n tomato9: \"#e54d2e\",\n tomato10: \"#dd4425\",\n tomato11: \"#d13415\",\n tomato12: \"#5c271f\",\n};\nconst tomatoA = {\n tomatoA1: \"#ff000003\",\n tomatoA2: \"#ff200008\",\n tomatoA3: \"#f52b0018\",\n tomatoA4: \"#ff35002c\",\n tomatoA5: \"#ff2e003d\",\n tomatoA6: \"#f92d0050\",\n tomatoA7: \"#e7280067\",\n tomatoA8: \"#db250084\",\n tomatoA9: \"#df2600d1\",\n tomatoA10: \"#d72400da\",\n tomatoA11: \"#cd2200ea\",\n tomatoA12: \"#460900e0\",\n};\nconst tomatoP3 = {\n tomato1: \"color(display-p3 0.998 0.989 0.988)\",\n tomato2: \"color(display-p3 0.994 0.974 0.969)\",\n tomato3: \"color(display-p3 0.985 0.924 0.909)\",\n tomato4: \"color(display-p3 0.996 0.868 0.835)\",\n tomato5: \"color(display-p3 0.98 0.812 0.77)\",\n tomato6: \"color(display-p3 0.953 0.75 0.698)\",\n tomato7: \"color(display-p3 0.917 0.673 0.611)\",\n tomato8: \"color(display-p3 0.875 0.575 0.502)\",\n tomato9: \"color(display-p3 0.831 0.345 0.231)\",\n tomato10: \"color(display-p3 0.802 0.313 0.2)\",\n tomato11: \"color(display-p3 0.755 0.259 0.152)\",\n tomato12: \"color(display-p3 0.335 0.165 0.132)\",\n};\nconst tomatoP3A = {\n tomatoA1: \"color(display-p3 0.675 0.024 0.024 / 0.012)\",\n tomatoA2: \"color(display-p3 0.757 0.145 0.02 / 0.032)\",\n tomatoA3: \"color(display-p3 0.831 0.184 0.012 / 0.091)\",\n tomatoA4: \"color(display-p3 0.976 0.192 0.004 / 0.165)\",\n tomatoA5: \"color(display-p3 0.918 0.192 0.004 / 0.232)\",\n tomatoA6: \"color(display-p3 0.847 0.173 0.004 / 0.302)\",\n tomatoA7: \"color(display-p3 0.788 0.165 0.004 / 0.389)\",\n tomatoA8: \"color(display-p3 0.749 0.153 0.004 / 0.499)\",\n tomatoA9: \"color(display-p3 0.78 0.149 0 / 0.769)\",\n tomatoA10: \"color(display-p3 0.757 0.141 0 / 0.8)\",\n tomatoA11: \"color(display-p3 0.755 0.259 0.152)\",\n tomatoA12: \"color(display-p3 0.335 0.165 0.132)\",\n};\nconst red = {\n red1: \"#fffcfc\",\n red2: \"#fff7f7\",\n red3: \"#feebec\",\n red4: \"#ffdbdc\",\n red5: \"#ffcdce\",\n red6: \"#fdbdbe\",\n red7: \"#f4a9aa\",\n red8: \"#eb8e90\",\n red9: \"#e5484d\",\n red10: \"#dc3e42\",\n red11: \"#ce2c31\",\n red12: \"#641723\",\n};\nconst redA = {\n redA1: \"#ff000003\",\n redA2: \"#ff000008\",\n redA3: \"#f3000d14\",\n redA4: \"#ff000824\",\n redA5: \"#ff000632\",\n redA6: \"#f8000442\",\n redA7: \"#df000356\",\n redA8: \"#d2000571\",\n redA9: \"#db0007b7\",\n redA10: \"#d10005c1\",\n redA11: \"#c40006d3\",\n redA12: \"#55000de8\",\n};\nconst redP3 = {\n red1: \"color(display-p3 0.998 0.989 0.988)\",\n red2: \"color(display-p3 0.995 0.971 0.971)\",\n red3: \"color(display-p3 0.985 0.925 0.925)\",\n red4: \"color(display-p3 0.999 0.866 0.866)\",\n red5: \"color(display-p3 0.984 0.812 0.811)\",\n red6: \"color(display-p3 0.955 0.751 0.749)\",\n red7: \"color(display-p3 0.915 0.675 0.672)\",\n red8: \"color(display-p3 0.872 0.575 0.572)\",\n red9: \"color(display-p3 0.83 0.329 0.324)\",\n red10: \"color(display-p3 0.798 0.294 0.285)\",\n red11: \"color(display-p3 0.744 0.234 0.222)\",\n red12: \"color(display-p3 0.36 0.115 0.143)\",\n};\nconst redP3A = {\n redA1: \"color(display-p3 0.675 0.024 0.024 / 0.012)\",\n redA2: \"color(display-p3 0.863 0.024 0.024 / 0.028)\",\n redA3: \"color(display-p3 0.792 0.008 0.008 / 0.075)\",\n redA4: \"color(display-p3 1 0.008 0.008 / 0.134)\",\n redA5: \"color(display-p3 0.918 0.008 0.008 / 0.189)\",\n redA6: \"color(display-p3 0.831 0.02 0.004 / 0.251)\",\n redA7: \"color(display-p3 0.741 0.016 0.004 / 0.33)\",\n redA8: \"color(display-p3 0.698 0.012 0.004 / 0.428)\",\n redA9: \"color(display-p3 0.749 0.008 0 / 0.675)\",\n redA10: \"color(display-p3 0.714 0.012 0 / 0.714)\",\n redA11: \"color(display-p3 0.744 0.234 0.222)\",\n redA12: \"color(display-p3 0.36 0.115 0.143)\",\n};\nconst ruby = {\n ruby1: \"#fffcfd\",\n ruby2: \"#fff7f8\",\n ruby3: \"#feeaed\",\n ruby4: \"#ffdce1\",\n ruby5: \"#ffced6\",\n ruby6: \"#f8bfc8\",\n ruby7: \"#efacb8\",\n ruby8: \"#e592a3\",\n ruby9: \"#e54666\",\n ruby10: \"#dc3b5d\",\n ruby11: \"#ca244d\",\n ruby12: \"#64172b\",\n};\nconst rubyA = {\n rubyA1: \"#ff005503\",\n rubyA2: \"#ff002008\",\n rubyA3: \"#f3002515\",\n rubyA4: \"#ff002523\",\n rubyA5: \"#ff002a31\",\n rubyA6: \"#e4002440\",\n rubyA7: \"#ce002553\",\n rubyA8: \"#c300286d\",\n rubyA9: \"#db002cb9\",\n rubyA10: \"#d2002cc4\",\n rubyA11: \"#c10030db\",\n rubyA12: \"#550016e8\",\n};\nconst rubyP3 = {\n ruby1: \"color(display-p3 0.998 0.989 0.992)\",\n ruby2: \"color(display-p3 0.995 0.971 0.974)\",\n ruby3: \"color(display-p3 0.983 0.92 0.928)\",\n ruby4: \"color(display-p3 0.987 0.869 0.885)\",\n ruby5: \"color(display-p3 0.968 0.817 0.839)\",\n ruby6: \"color(display-p3 0.937 0.758 0.786)\",\n ruby7: \"color(display-p3 0.897 0.685 0.721)\",\n ruby8: \"color(display-p3 0.851 0.588 0.639)\",\n ruby9: \"color(display-p3 0.83 0.323 0.408)\",\n ruby10: \"color(display-p3 0.795 0.286 0.375)\",\n ruby11: \"color(display-p3 0.728 0.211 0.311)\",\n ruby12: \"color(display-p3 0.36 0.115 0.171)\",\n};\nconst rubyP3A = {\n rubyA1: \"color(display-p3 0.675 0.024 0.349 / 0.012)\",\n rubyA2: \"color(display-p3 0.863 0.024 0.024 / 0.028)\",\n rubyA3: \"color(display-p3 0.804 0.008 0.11 / 0.079)\",\n rubyA4: \"color(display-p3 0.91 0.008 0.125 / 0.13)\",\n rubyA5: \"color(display-p3 0.831 0.004 0.133 / 0.185)\",\n rubyA6: \"color(display-p3 0.745 0.004 0.118 / 0.244)\",\n rubyA7: \"color(display-p3 0.678 0.004 0.114 / 0.314)\",\n rubyA8: \"color(display-p3 0.639 0.004 0.125 / 0.412)\",\n rubyA9: \"color(display-p3 0.753 0 0.129 / 0.679)\",\n rubyA10: \"color(display-p3 0.714 0 0.125 / 0.714)\",\n rubyA11: \"color(display-p3 0.728 0.211 0.311)\",\n rubyA12: \"color(display-p3 0.36 0.115 0.171)\",\n};\nconst crimson = {\n crimson1: \"#fffcfd\",\n crimson2: \"#fef7f9\",\n crimson3: \"#ffe9f0\",\n crimson4: \"#fedce7\",\n crimson5: \"#facedd\",\n crimson6: \"#f3bed1\",\n crimson7: \"#eaacc3\",\n crimson8: \"#e093b2\",\n crimson9: \"#e93d82\",\n crimson10: \"#df3478\",\n crimson11: \"#cb1d63\",\n crimson12: \"#621639\",\n};\nconst crimsonA = {\n crimsonA1: \"#ff005503\",\n crimsonA2: \"#e0004008\",\n crimsonA3: \"#ff005216\",\n crimsonA4: \"#f8005123\",\n crimsonA5: \"#e5004f31\",\n crimsonA6: \"#d0004b41\",\n crimsonA7: \"#bf004753\",\n crimsonA8: \"#b6004a6c\",\n crimsonA9: \"#e2005bc2\",\n crimsonA10: \"#d70056cb\",\n crimsonA11: \"#c4004fe2\",\n crimsonA12: \"#530026e9\",\n};\nconst crimsonP3 = {\n crimson1: \"color(display-p3 0.998 0.989 0.992)\",\n crimson2: \"color(display-p3 0.991 0.969 0.976)\",\n crimson3: \"color(display-p3 0.987 0.917 0.941)\",\n crimson4: \"color(display-p3 0.975 0.866 0.904)\",\n crimson5: \"color(display-p3 0.953 0.813 0.864)\",\n crimson6: \"color(display-p3 0.921 0.755 0.817)\",\n crimson7: \"color(display-p3 0.88 0.683 0.761)\",\n crimson8: \"color(display-p3 0.834 0.592 0.694)\",\n crimson9: \"color(display-p3 0.843 0.298 0.507)\",\n crimson10: \"color(display-p3 0.807 0.266 0.468)\",\n crimson11: \"color(display-p3 0.731 0.195 0.388)\",\n crimson12: \"color(display-p3 0.352 0.111 0.221)\",\n};\nconst crimsonP3A = {\n crimsonA1: \"color(display-p3 0.675 0.024 0.349 / 0.012)\",\n crimsonA2: \"color(display-p3 0.757 0.02 0.267 / 0.032)\",\n crimsonA3: \"color(display-p3 0.859 0.008 0.294 / 0.083)\",\n crimsonA4: \"color(display-p3 0.827 0.008 0.298 / 0.134)\",\n crimsonA5: \"color(display-p3 0.753 0.008 0.275 / 0.189)\",\n crimsonA6: \"color(display-p3 0.682 0.004 0.247 / 0.244)\",\n crimsonA7: \"color(display-p3 0.62 0.004 0.251 / 0.318)\",\n crimsonA8: \"color(display-p3 0.6 0.004 0.251 / 0.408)\",\n crimsonA9: \"color(display-p3 0.776 0 0.298 / 0.702)\",\n crimsonA10: \"color(display-p3 0.737 0 0.275 / 0.734)\",\n crimsonA11: \"color(display-p3 0.731 0.195 0.388)\",\n crimsonA12: \"color(display-p3 0.352 0.111 0.221)\",\n};\nconst pink = {\n pink1: \"#fffcfe\",\n pink2: \"#fef7fb\",\n pink3: \"#fee9f5\",\n pink4: \"#fbdcef\",\n pink5: \"#f6cee7\",\n pink6: \"#efbfdd\",\n pink7: \"#e7acd0\",\n pink8: \"#dd93c2\",\n pink9: \"#d6409f\",\n pink10: \"#cf3897\",\n pink11: \"#c2298a\",\n pink12: \"#651249\",\n};\nconst pinkA = {\n pinkA1: \"#ff00aa03\",\n pinkA2: \"#e0008008\",\n pinkA3: \"#f4008c16\",\n pinkA4: \"#e2008b23\",\n pinkA5: \"#d1008331\",\n pinkA6: \"#c0007840\",\n pinkA7: \"#b6006f53\",\n pinkA8: \"#af006f6c\",\n pinkA9: \"#c8007fbf\",\n pinkA10: \"#c2007ac7\",\n pinkA11: \"#b60074d6\",\n pinkA12: \"#59003bed\",\n};\nconst pinkP3 = {\n pink1: \"color(display-p3 0.998 0.989 0.996)\",\n pink2: \"color(display-p3 0.992 0.97 0.985)\",\n pink3: \"color(display-p3 0.981 0.917 0.96)\",\n pink4: \"color(display-p3 0.963 0.867 0.932)\",\n pink5: \"color(display-p3 0.939 0.815 0.899)\",\n pink6: \"color(display-p3 0.907 0.756 0.859)\",\n pink7: \"color(display-p3 0.869 0.683 0.81)\",\n pink8: \"color(display-p3 0.825 0.59 0.751)\",\n pink9: \"color(display-p3 0.775 0.297 0.61)\",\n pink10: \"color(display-p3 0.748 0.27 0.581)\",\n pink11: \"color(display-p3 0.698 0.219 0.528)\",\n pink12: \"color(display-p3 0.363 0.101 0.279)\",\n};\nconst pinkP3A = {\n pinkA1: \"color(display-p3 0.675 0.024 0.675 / 0.012)\",\n pinkA2: \"color(display-p3 0.757 0.02 0.51 / 0.032)\",\n pinkA3: \"color(display-p3 0.765 0.008 0.529 / 0.083)\",\n pinkA4: \"color(display-p3 0.737 0.008 0.506 / 0.134)\",\n pinkA5: \"color(display-p3 0.663 0.004 0.451 / 0.185)\",\n pinkA6: \"color(display-p3 0.616 0.004 0.424 / 0.244)\",\n pinkA7: \"color(display-p3 0.596 0.004 0.412 / 0.318)\",\n pinkA8: \"color(display-p3 0.573 0.004 0.404 / 0.412)\",\n pinkA9: \"color(display-p3 0.682 0 0.447 / 0.702)\",\n pinkA10: \"color(display-p3 0.655 0 0.424 / 0.73)\",\n pinkA11: \"color(display-p3 0.698 0.219 0.528)\",\n pinkA12: \"color(display-p3 0.363 0.101 0.279)\",\n};\nconst plum = {\n plum1: \"#fefcff\",\n plum2: \"#fdf7fd\",\n plum3: \"#fbebfb\",\n plum4: \"#f7def8\",\n plum5: \"#f2d1f3\",\n plum6: \"#e9c2ec\",\n plum7: \"#deade3\",\n plum8: \"#cf91d8\",\n plum9: \"#ab4aba\",\n plum10: \"#a144af\",\n plum11: \"#953ea3\",\n plum12: \"#53195d\",\n};\nconst plumA = {\n plumA1: \"#aa00ff03\",\n plumA2: \"#c000c008\",\n plumA3: \"#cc00cc14\",\n plumA4: \"#c200c921\",\n plumA5: \"#b700bd2e\",\n plumA6: \"#a400b03d\",\n plumA7: \"#9900a852\",\n plumA8: \"#9000a56e\",\n plumA9: \"#89009eb5\",\n plumA10: \"#7f0092bb\",\n plumA11: \"#730086c1\",\n plumA12: \"#40004be6\",\n};\nconst plumP3 = {\n plum1: \"color(display-p3 0.995 0.988 0.999)\",\n plum2: \"color(display-p3 0.988 0.971 0.99)\",\n plum3: \"color(display-p3 0.973 0.923 0.98)\",\n plum4: \"color(display-p3 0.953 0.875 0.966)\",\n plum5: \"color(display-p3 0.926 0.825 0.945)\",\n plum6: \"color(display-p3 0.89 0.765 0.916)\",\n plum7: \"color(display-p3 0.84 0.686 0.877)\",\n plum8: \"color(display-p3 0.775 0.58 0.832)\",\n plum9: \"color(display-p3 0.624 0.313 0.708)\",\n plum10: \"color(display-p3 0.587 0.29 0.667)\",\n plum11: \"color(display-p3 0.543 0.263 0.619)\",\n plum12: \"color(display-p3 0.299 0.114 0.352)\",\n};\nconst plumP3A = {\n plumA1: \"color(display-p3 0.675 0.024 1 / 0.012)\",\n plumA2: \"color(display-p3 0.58 0.024 0.58 / 0.028)\",\n plumA3: \"color(display-p3 0.655 0.008 0.753 / 0.079)\",\n plumA4: \"color(display-p3 0.627 0.008 0.722 / 0.126)\",\n plumA5: \"color(display-p3 0.58 0.004 0.69 / 0.177)\",\n plumA6: \"color(display-p3 0.537 0.004 0.655 / 0.236)\",\n plumA7: \"color(display-p3 0.49 0.004 0.616 / 0.314)\",\n plumA8: \"color(display-p3 0.471 0.004 0.6 / 0.42)\",\n plumA9: \"color(display-p3 0.451 0 0.576 / 0.687)\",\n plumA10: \"color(display-p3 0.42 0 0.529 / 0.71)\",\n plumA11: \"color(display-p3 0.543 0.263 0.619)\",\n plumA12: \"color(display-p3 0.299 0.114 0.352)\",\n};\nconst purple = {\n purple1: \"#fefcfe\",\n purple2: \"#fbf7fe\",\n purple3: \"#f7edfe\",\n purple4: \"#f2e2fc\",\n purple5: \"#ead5f9\",\n purple6: \"#e0c4f4\",\n purple7: \"#d1afec\",\n purple8: \"#be93e4\",\n purple9: \"#8e4ec6\",\n purple10: \"#8347b9\",\n purple11: \"#8145b5\",\n purple12: \"#402060\",\n};\nconst purpleA = {\n purpleA1: \"#aa00aa03\",\n purpleA2: \"#8000e008\",\n purpleA3: \"#8e00f112\",\n purpleA4: \"#8d00e51d\",\n purpleA5: \"#8000db2a\",\n purpleA6: \"#7a01d03b\",\n purpleA7: \"#6d00c350\",\n purpleA8: \"#6600c06c\",\n purpleA9: \"#5c00adb1\",\n purpleA10: \"#53009eb8\",\n purpleA11: \"#52009aba\",\n purpleA12: \"#250049df\",\n};\nconst purpleP3 = {\n purple1: \"color(display-p3 0.995 0.988 0.996)\",\n purple2: \"color(display-p3 0.983 0.971 0.993)\",\n purple3: \"color(display-p3 0.963 0.931 0.989)\",\n purple4: \"color(display-p3 0.937 0.888 0.981)\",\n purple5: \"color(display-p3 0.904 0.837 0.966)\",\n purple6: \"color(display-p3 0.86 0.774 0.942)\",\n purple7: \"color(display-p3 0.799 0.69 0.91)\",\n purple8: \"color(display-p3 0.719 0.583 0.874)\",\n purple9: \"color(display-p3 0.523 0.318 0.751)\",\n purple10: \"color(display-p3 0.483 0.289 0.7)\",\n purple11: \"color(display-p3 0.473 0.281 0.687)\",\n purple12: \"color(display-p3 0.234 0.132 0.363)\",\n};\nconst purpleP3A = {\n purpleA1: \"color(display-p3 0.675 0.024 0.675 / 0.012)\",\n purpleA2: \"color(display-p3 0.443 0.024 0.722 / 0.028)\",\n purpleA3: \"color(display-p3 0.506 0.008 0.835 / 0.071)\",\n purpleA4: \"color(display-p3 0.451 0.004 0.831 / 0.114)\",\n purpleA5: \"color(display-p3 0.431 0.004 0.788 / 0.165)\",\n purpleA6: \"color(display-p3 0.384 0.004 0.745 / 0.228)\",\n purpleA7: \"color(display-p3 0.357 0.004 0.71 / 0.31)\",\n purpleA8: \"color(display-p3 0.322 0.004 0.702 / 0.416)\",\n purpleA9: \"color(display-p3 0.298 0 0.639 / 0.683)\",\n purpleA10: \"color(display-p3 0.271 0 0.58 / 0.71)\",\n purpleA11: \"color(display-p3 0.473 0.281 0.687)\",\n purpleA12: \"color(display-p3 0.234 0.132 0.363)\",\n};\nconst violet = {\n violet1: \"#fdfcfe\",\n violet2: \"#faf8ff\",\n violet3: \"#f4f0fe\",\n violet4: \"#ebe4ff\",\n violet5: \"#e1d9ff\",\n violet6: \"#d4cafe\",\n violet7: \"#c2b5f5\",\n violet8: \"#aa99ec\",\n violet9: \"#6e56cf\",\n violet10: \"#654dc4\",\n violet11: \"#6550b9\",\n violet12: \"#2f265f\",\n};\nconst violetA = {\n violetA1: \"#5500aa03\",\n violetA2: \"#4900ff07\",\n violetA3: \"#4400ee0f\",\n violetA4: \"#4300ff1b\",\n violetA5: \"#3600ff26\",\n violetA6: \"#3100fb35\",\n violetA7: \"#2d01dd4a\",\n violetA8: \"#2b00d066\",\n violetA9: \"#2400b7a9\",\n violetA10: \"#2300abb2\",\n violetA11: \"#1f0099af\",\n violetA12: \"#0b0043d9\",\n};\nconst violetP3 = {\n violet1: \"color(display-p3 0.991 0.988 0.995)\",\n violet2: \"color(display-p3 0.978 0.974 0.998)\",\n violet3: \"color(display-p3 0.953 0.943 0.993)\",\n violet4: \"color(display-p3 0.916 0.897 1)\",\n violet5: \"color(display-p3 0.876 0.851 1)\",\n violet6: \"color(display-p3 0.825 0.793 0.981)\",\n violet7: \"color(display-p3 0.752 0.712 0.943)\",\n violet8: \"color(display-p3 0.654 0.602 0.902)\",\n violet9: \"color(display-p3 0.417 0.341 0.784)\",\n violet10: \"color(display-p3 0.381 0.306 0.741)\",\n violet11: \"color(display-p3 0.383 0.317 0.702)\",\n violet12: \"color(display-p3 0.179 0.15 0.359)\",\n};\nconst violetP3A = {\n violetA1: \"color(display-p3 0.349 0.024 0.675 / 0.012)\",\n violetA2: \"color(display-p3 0.161 0.024 0.863 / 0.028)\",\n violetA3: \"color(display-p3 0.204 0.004 0.871 / 0.059)\",\n violetA4: \"color(display-p3 0.196 0.004 1 / 0.102)\",\n violetA5: \"color(display-p3 0.165 0.008 1 / 0.15)\",\n violetA6: \"color(display-p3 0.153 0.004 0.906 / 0.208)\",\n violetA7: \"color(display-p3 0.141 0.004 0.796 / 0.287)\",\n violetA8: \"color(display-p3 0.133 0.004 0.753 / 0.397)\",\n violetA9: \"color(display-p3 0.114 0 0.675 / 0.659)\",\n violetA10: \"color(display-p3 0.11 0 0.627 / 0.695)\",\n violetA11: \"color(display-p3 0.383 0.317 0.702)\",\n violetA12: \"color(display-p3 0.179 0.15 0.359)\",\n};\nconst iris = {\n iris1: \"#fdfdff\",\n iris2: \"#f8f8ff\",\n iris3: \"#f0f1fe\",\n iris4: \"#e6e7ff\",\n iris5: \"#dadcff\",\n iris6: \"#cbcdff\",\n iris7: \"#b8baf8\",\n iris8: \"#9b9ef0\",\n iris9: \"#5b5bd6\",\n iris10: \"#5151cd\",\n iris11: \"#5753c6\",\n iris12: \"#272962\",\n};\nconst irisA = {\n irisA1: \"#0000ff02\",\n irisA2: \"#0000ff07\",\n irisA3: \"#0011ee0f\",\n irisA4: \"#000bff19\",\n irisA5: \"#000eff25\",\n irisA6: \"#000aff34\",\n irisA7: \"#0008e647\",\n irisA8: \"#0008d964\",\n irisA9: \"#0000c0a4\",\n irisA10: \"#0000b6ae\",\n irisA11: \"#0600abac\",\n irisA12: \"#000246d8\",\n};\nconst irisP3 = {\n iris1: \"color(display-p3 0.992 0.992 0.999)\",\n iris2: \"color(display-p3 0.972 0.973 0.998)\",\n iris3: \"color(display-p3 0.943 0.945 0.992)\",\n iris4: \"color(display-p3 0.902 0.906 1)\",\n iris5: \"color(display-p3 0.857 0.861 1)\",\n iris6: \"color(display-p3 0.799 0.805 0.987)\",\n iris7: \"color(display-p3 0.721 0.727 0.955)\",\n iris8: \"color(display-p3 0.61 0.619 0.918)\",\n iris9: \"color(display-p3 0.357 0.357 0.81)\",\n iris10: \"color(display-p3 0.318 0.318 0.774)\",\n iris11: \"color(display-p3 0.337 0.326 0.748)\",\n iris12: \"color(display-p3 0.154 0.161 0.371)\",\n};\nconst irisP3A = {\n irisA1: \"color(display-p3 0.02 0.02 1 / 0.008)\",\n irisA2: \"color(display-p3 0.024 0.024 0.863 / 0.028)\",\n irisA3: \"color(display-p3 0.004 0.071 0.871 / 0.059)\",\n irisA4: \"color(display-p3 0.012 0.051 1 / 0.099)\",\n irisA5: \"color(display-p3 0.008 0.035 1 / 0.142)\",\n irisA6: \"color(display-p3 0 0.02 0.941 / 0.2)\",\n irisA7: \"color(display-p3 0.004 0.02 0.847 / 0.279)\",\n irisA8: \"color(display-p3 0.004 0.024 0.788 / 0.389)\",\n irisA9: \"color(display-p3 0 0 0.706 / 0.644)\",\n irisA10: \"color(display-p3 0 0 0.667 / 0.683)\",\n irisA11: \"color(display-p3 0.337 0.326 0.748)\",\n irisA12: \"color(display-p3 0.154 0.161 0.371)\",\n};\nconst indigo = {\n indigo1: \"#fdfdfe\",\n indigo2: \"#f7f9ff\",\n indigo3: \"#edf2fe\",\n indigo4: \"#e1e9ff\",\n indigo5: \"#d2deff\",\n indigo6: \"#c1d0ff\",\n indigo7: \"#abbdf9\",\n indigo8: \"#8da4ef\",\n indigo9: \"#3e63dd\",\n indigo10: \"#3358d4\",\n indigo11: \"#3a5bc7\",\n indigo12: \"#1f2d5c\",\n};\nconst indigoA = {\n indigoA1: \"#00008002\",\n indigoA2: \"#0040ff08\",\n indigoA3: \"#0047f112\",\n indigoA4: \"#0044ff1e\",\n indigoA5: \"#0044ff2d\",\n indigoA6: \"#003eff3e\",\n indigoA7: \"#0037ed54\",\n indigoA8: \"#0034dc72\",\n indigoA9: \"#0031d2c1\",\n indigoA10: \"#002ec9cc\",\n indigoA11: \"#002bb7c5\",\n indigoA12: \"#001046e0\",\n};\nconst indigoP3 = {\n indigo1: \"color(display-p3 0.992 0.992 0.996)\",\n indigo2: \"color(display-p3 0.971 0.977 0.998)\",\n indigo3: \"color(display-p3 0.933 0.948 0.992)\",\n indigo4: \"color(display-p3 0.885 0.914 1)\",\n indigo5: \"color(display-p3 0.831 0.87 1)\",\n indigo6: \"color(display-p3 0.767 0.814 0.995)\",\n indigo7: \"color(display-p3 0.685 0.74 0.957)\",\n indigo8: \"color(display-p3 0.569 0.639 0.916)\",\n indigo9: \"color(display-p3 0.276 0.384 0.837)\",\n indigo10: \"color(display-p3 0.234 0.343 0.801)\",\n indigo11: \"color(display-p3 0.256 0.354 0.755)\",\n indigo12: \"color(display-p3 0.133 0.175 0.348)\",\n};\nconst indigoP3A = {\n indigoA1: \"color(display-p3 0.02 0.02 0.51 / 0.008)\",\n indigoA2: \"color(display-p3 0.024 0.161 0.863 / 0.028)\",\n indigoA3: \"color(display-p3 0.008 0.239 0.886 / 0.067)\",\n indigoA4: \"color(display-p3 0.004 0.247 1 / 0.114)\",\n indigoA5: \"color(display-p3 0.004 0.235 1 / 0.169)\",\n indigoA6: \"color(display-p3 0.004 0.208 0.984 / 0.232)\",\n indigoA7: \"color(display-p3 0.004 0.176 0.863 / 0.314)\",\n indigoA8: \"color(display-p3 0.004 0.165 0.812 / 0.432)\",\n indigoA9: \"color(display-p3 0 0.153 0.773 / 0.726)\",\n indigoA10: \"color(display-p3 0 0.137 0.737 / 0.765)\",\n indigoA11: \"color(display-p3 0.256 0.354 0.755)\",\n indigoA12: \"color(display-p3 0.133 0.175 0.348)\",\n};\nconst blue = {\n blue1: \"#fbfdff\",\n blue2: \"#f4faff\",\n blue3: \"#e6f4fe\",\n blue4: \"#d5efff\",\n blue5: \"#c2e5ff\",\n blue6: \"#acd8fc\",\n blue7: \"#8ec8f6\",\n blue8: \"#5eb1ef\",\n blue9: \"#0090ff\",\n blue10: \"#0588f0\",\n blue11: \"#0d74ce\",\n blue12: \"#113264\",\n};\nconst blueA = {\n blueA1: \"#0080ff04\",\n blueA2: \"#008cff0b\",\n blueA3: \"#008ff519\",\n blueA4: \"#009eff2a\",\n blueA5: \"#0093ff3d\",\n blueA6: \"#0088f653\",\n blueA7: \"#0083eb71\",\n blueA8: \"#0084e6a1\",\n blueA9: \"#0090ff\",\n blueA10: \"#0086f0fa\",\n blueA11: \"#006dcbf2\",\n blueA12: \"#002359ee\",\n};\nconst blueP3 = {\n blue1: \"color(display-p3 0.986 0.992 0.999)\",\n blue2: \"color(display-p3 0.96 0.979 0.998)\",\n blue3: \"color(display-p3 0.912 0.956 0.991)\",\n blue4: \"color(display-p3 0.853 0.932 1)\",\n blue5: \"color(display-p3 0.788 0.894 0.998)\",\n blue6: \"color(display-p3 0.709 0.843 0.976)\",\n blue7: \"color(display-p3 0.606 0.777 0.947)\",\n blue8: \"color(display-p3 0.451 0.688 0.917)\",\n blue9: \"color(display-p3 0.247 0.556 0.969)\",\n blue10: \"color(display-p3 0.234 0.523 0.912)\",\n blue11: \"color(display-p3 0.15 0.44 0.84)\",\n blue12: \"color(display-p3 0.102 0.193 0.379)\",\n};\nconst blueP3A = {\n blueA1: \"color(display-p3 0.024 0.514 1 / 0.016)\",\n blueA2: \"color(display-p3 0.024 0.514 0.906 / 0.04)\",\n blueA3: \"color(display-p3 0.012 0.506 0.914 / 0.087)\",\n blueA4: \"color(display-p3 0.008 0.545 1 / 0.146)\",\n blueA5: \"color(display-p3 0.004 0.502 0.984 / 0.212)\",\n blueA6: \"color(display-p3 0.004 0.463 0.922 / 0.291)\",\n blueA7: \"color(display-p3 0.004 0.431 0.863 / 0.393)\",\n blueA8: \"color(display-p3 0 0.427 0.851 / 0.55)\",\n blueA9: \"color(display-p3 0 0.412 0.961 / 0.753)\",\n blueA10: \"color(display-p3 0 0.376 0.886 / 0.765)\",\n blueA11: \"color(display-p3 0.15 0.44 0.84)\",\n blueA12: \"color(display-p3 0.102 0.193 0.379)\",\n};\nconst cyan = {\n cyan1: \"#fafdfe\",\n cyan2: \"#f2fafb\",\n cyan3: \"#def7f9\",\n cyan4: \"#caf1f6\",\n cyan5: \"#b5e9f0\",\n cyan6: \"#9ddde7\",\n cyan7: \"#7dcedc\",\n cyan8: \"#3db9cf\",\n cyan9: \"#00a2c7\",\n cyan10: \"#0797b9\",\n cyan11: \"#107d98\",\n cyan12: \"#0d3c48\",\n};\nconst cyanA = {\n cyanA1: \"#0099cc05\",\n cyanA2: \"#009db10d\",\n cyanA3: \"#00c2d121\",\n cyanA4: \"#00bcd435\",\n cyanA5: \"#01b4cc4a\",\n cyanA6: \"#00a7c162\",\n cyanA7: \"#009fbb82\",\n cyanA8: \"#00a3c0c2\",\n cyanA9: \"#00a2c7\",\n cyanA10: \"#0094b7f8\",\n cyanA11: \"#007491ef\",\n cyanA12: \"#00323ef2\",\n};\nconst cyanP3 = {\n cyan1: \"color(display-p3 0.982 0.992 0.996)\",\n cyan2: \"color(display-p3 0.955 0.981 0.984)\",\n cyan3: \"color(display-p3 0.888 0.965 0.975)\",\n cyan4: \"color(display-p3 0.821 0.941 0.959)\",\n cyan5: \"color(display-p3 0.751 0.907 0.935)\",\n cyan6: \"color(display-p3 0.671 0.862 0.9)\",\n cyan7: \"color(display-p3 0.564 0.8 0.854)\",\n cyan8: \"color(display-p3 0.388 0.715 0.798)\",\n cyan9: \"color(display-p3 0.282 0.627 0.765)\",\n cyan10: \"color(display-p3 0.264 0.583 0.71)\",\n cyan11: \"color(display-p3 0.08 0.48 0.63)\",\n cyan12: \"color(display-p3 0.108 0.232 0.277)\",\n};\nconst cyanP3A = {\n cyanA1: \"color(display-p3 0.02 0.608 0.804 / 0.02)\",\n cyanA2: \"color(display-p3 0.02 0.557 0.647 / 0.044)\",\n cyanA3: \"color(display-p3 0.004 0.694 0.796 / 0.114)\",\n cyanA4: \"color(display-p3 0.004 0.678 0.784 / 0.181)\",\n cyanA5: \"color(display-p3 0.004 0.624 0.733 / 0.248)\",\n cyanA6: \"color(display-p3 0.004 0.584 0.706 / 0.33)\",\n cyanA7: \"color(display-p3 0.004 0.541 0.667 / 0.436)\",\n cyanA8: \"color(display-p3 0 0.533 0.667 / 0.612)\",\n cyanA9: \"color(display-p3 0 0.482 0.675 / 0.718)\",\n cyanA10: \"color(display-p3 0 0.435 0.608 / 0.738)\",\n cyanA11: \"color(display-p3 0.08 0.48 0.63)\",\n cyanA12: \"color(display-p3 0.108 0.232 0.277)\",\n};\nconst teal = {\n teal1: \"#fafefd\",\n teal2: \"#f3fbf9\",\n teal3: \"#e0f8f3\",\n teal4: \"#ccf3ea\",\n teal5: \"#b8eae0\",\n teal6: \"#a1ded2\",\n teal7: \"#83cdc1\",\n teal8: \"#53b9ab\",\n teal9: \"#12a594\",\n teal10: \"#0d9b8a\",\n teal11: \"#008573\",\n teal12: \"#0d3d38\",\n};\nconst tealA = {\n tealA1: \"#00cc9905\",\n tealA2: \"#00aa800c\",\n tealA3: \"#00c69d1f\",\n tealA4: \"#00c39633\",\n tealA5: \"#00b49047\",\n tealA6: \"#00a6855e\",\n tealA7: \"#0099807c\",\n tealA8: \"#009783ac\",\n tealA9: \"#009e8ced\",\n tealA10: \"#009684f2\",\n tealA11: \"#008573\",\n tealA12: \"#00332df2\",\n};\nconst tealP3 = {\n teal1: \"color(display-p3 0.983 0.996 0.992)\",\n teal2: \"color(display-p3 0.958 0.983 0.976)\",\n teal3: \"color(display-p3 0.895 0.971 0.952)\",\n teal4: \"color(display-p3 0.831 0.949 0.92)\",\n teal5: \"color(display-p3 0.761 0.914 0.878)\",\n teal6: \"color(display-p3 0.682 0.864 0.825)\",\n teal7: \"color(display-p3 0.581 0.798 0.756)\",\n teal8: \"color(display-p3 0.433 0.716 0.671)\",\n teal9: \"color(display-p3 0.297 0.637 0.581)\",\n teal10: \"color(display-p3 0.275 0.599 0.542)\",\n teal11: \"color(display-p3 0.08 0.5 0.43)\",\n teal12: \"color(display-p3 0.11 0.235 0.219)\",\n};\nconst tealP3A = {\n tealA1: \"color(display-p3 0.024 0.757 0.514 / 0.016)\",\n tealA2: \"color(display-p3 0.02 0.647 0.467 / 0.044)\",\n tealA3: \"color(display-p3 0.004 0.741 0.557 / 0.106)\",\n tealA4: \"color(display-p3 0.004 0.702 0.537 / 0.169)\",\n tealA5: \"color(display-p3 0.004 0.643 0.494 / 0.24)\",\n tealA6: \"color(display-p3 0.004 0.569 0.447 / 0.318)\",\n tealA7: \"color(display-p3 0.004 0.518 0.424 / 0.42)\",\n tealA8: \"color(display-p3 0 0.506 0.424 / 0.569)\",\n tealA9: \"color(display-p3 0 0.482 0.404 / 0.702)\",\n tealA10: \"color(display-p3 0 0.451 0.369 / 0.726)\",\n tealA11: \"color(display-p3 0.08 0.5 0.43)\",\n tealA12: \"color(display-p3 0.11 0.235 0.219)\",\n};\nconst jade = {\n jade1: \"#fbfefd\",\n jade2: \"#f4fbf7\",\n jade3: \"#e6f7ed\",\n jade4: \"#d6f1e3\",\n jade5: \"#c3e9d7\",\n jade6: \"#acdec8\",\n jade7: \"#8bceb6\",\n jade8: \"#56ba9f\",\n jade9: \"#29a383\",\n jade10: \"#26997b\",\n jade11: \"#208368\",\n jade12: \"#1d3b31\",\n};\nconst jadeA = {\n jadeA1: \"#00c08004\",\n jadeA2: \"#00a3460b\",\n jadeA3: \"#00ae4819\",\n jadeA4: \"#00a85129\",\n jadeA5: \"#00a2553c\",\n jadeA6: \"#009a5753\",\n jadeA7: \"#00945f74\",\n jadeA8: \"#00976ea9\",\n jadeA9: \"#00916bd6\",\n jadeA10: \"#008764d9\",\n jadeA11: \"#007152df\",\n jadeA12: \"#002217e2\",\n};\nconst jadeP3 = {\n jade1: \"color(display-p3 0.986 0.996 0.992)\",\n jade2: \"color(display-p3 0.962 0.983 0.969)\",\n jade3: \"color(display-p3 0.912 0.965 0.932)\",\n jade4: \"color(display-p3 0.858 0.941 0.893)\",\n jade5: \"color(display-p3 0.795 0.909 0.847)\",\n jade6: \"color(display-p3 0.715 0.864 0.791)\",\n jade7: \"color(display-p3 0.603 0.802 0.718)\",\n jade8: \"color(display-p3 0.44 0.72 0.629)\",\n jade9: \"color(display-p3 0.319 0.63 0.521)\",\n jade10: \"color(display-p3 0.299 0.592 0.488)\",\n jade11: \"color(display-p3 0.15 0.5 0.37)\",\n jade12: \"color(display-p3 0.142 0.229 0.194)\",\n};\nconst jadeP3A = {\n jadeA1: \"color(display-p3 0.024 0.757 0.514 / 0.016)\",\n jadeA2: \"color(display-p3 0.024 0.612 0.22 / 0.04)\",\n jadeA3: \"color(display-p3 0.012 0.596 0.235 / 0.087)\",\n jadeA4: \"color(display-p3 0.008 0.588 0.255 / 0.142)\",\n jadeA5: \"color(display-p3 0.004 0.561 0.251 / 0.204)\",\n jadeA6: \"color(display-p3 0.004 0.525 0.278 / 0.287)\",\n jadeA7: \"color(display-p3 0.004 0.506 0.29 / 0.397)\",\n jadeA8: \"color(display-p3 0 0.506 0.337 / 0.561)\",\n jadeA9: \"color(display-p3 0 0.459 0.298 / 0.683)\",\n jadeA10: \"color(display-p3 0 0.42 0.271 / 0.702)\",\n jadeA11: \"color(display-p3 0.15 0.5 0.37)\",\n jadeA12: \"color(display-p3 0.142 0.229 0.194)\",\n};\nconst green = {\n green1: \"#fbfefc\",\n green2: \"#f4fbf6\",\n green3: \"#e6f6eb\",\n green4: \"#d6f1df\",\n green5: \"#c4e8d1\",\n green6: \"#adddc0\",\n green7: \"#8eceaa\",\n green8: \"#5bb98b\",\n green9: \"#30a46c\",\n green10: \"#2b9a66\",\n green11: \"#218358\",\n green12: \"#193b2d\",\n};\nconst greenA = {\n greenA1: \"#00c04004\",\n greenA2: \"#00a32f0b\",\n greenA3: \"#00a43319\",\n greenA4: \"#00a83829\",\n greenA5: \"#019c393b\",\n greenA6: \"#00963c52\",\n greenA7: \"#00914071\",\n greenA8: \"#00924ba4\",\n greenA9: \"#008f4acf\",\n greenA10: \"#008647d4\",\n greenA11: \"#00713fde\",\n greenA12: \"#002616e6\",\n};\nconst greenP3 = {\n green1: \"color(display-p3 0.986 0.996 0.989)\",\n green2: \"color(display-p3 0.963 0.983 0.967)\",\n green3: \"color(display-p3 0.913 0.964 0.925)\",\n green4: \"color(display-p3 0.859 0.94 0.879)\",\n green5: \"color(display-p3 0.796 0.907 0.826)\",\n green6: \"color(display-p3 0.718 0.863 0.761)\",\n green7: \"color(display-p3 0.61 0.801 0.675)\",\n green8: \"color(display-p3 0.451 0.715 0.559)\",\n green9: \"color(display-p3 0.332 0.634 0.442)\",\n green10: \"color(display-p3 0.308 0.595 0.417)\",\n green11: \"color(display-p3 0.19 0.5 0.32)\",\n green12: \"color(display-p3 0.132 0.228 0.18)\",\n};\nconst greenP3A = {\n greenA1: \"color(display-p3 0.024 0.757 0.267 / 0.016)\",\n greenA2: \"color(display-p3 0.024 0.565 0.129 / 0.036)\",\n greenA3: \"color(display-p3 0.012 0.596 0.145 / 0.087)\",\n greenA4: \"color(display-p3 0.008 0.588 0.145 / 0.142)\",\n greenA5: \"color(display-p3 0.004 0.541 0.157 / 0.204)\",\n greenA6: \"color(display-p3 0.004 0.518 0.157 / 0.283)\",\n greenA7: \"color(display-p3 0.004 0.486 0.165 / 0.389)\",\n greenA8: \"color(display-p3 0 0.478 0.2 / 0.55)\",\n greenA9: \"color(display-p3 0 0.455 0.165 / 0.667)\",\n greenA10: \"color(display-p3 0 0.416 0.153 / 0.691)\",\n greenA11: \"color(display-p3 0.19 0.5 0.32)\",\n greenA12: \"color(display-p3 0.132 0.228 0.18)\",\n};\nconst grass = {\n grass1: \"#fbfefb\",\n grass2: \"#f5fbf5\",\n grass3: \"#e9f6e9\",\n grass4: \"#daf1db\",\n grass5: \"#c9e8ca\",\n grass6: \"#b2ddb5\",\n grass7: \"#94ce9a\",\n grass8: \"#65ba74\",\n grass9: \"#46a758\",\n grass10: \"#3e9b4f\",\n grass11: \"#2a7e3b\",\n grass12: \"#203c25\",\n};\nconst grassA = {\n grassA1: \"#00c00004\",\n grassA2: \"#0099000a\",\n grassA3: \"#00970016\",\n grassA4: \"#009f0725\",\n grassA5: \"#00930536\",\n grassA6: \"#008f0a4d\",\n grassA7: \"#018b0f6b\",\n grassA8: \"#008d199a\",\n grassA9: \"#008619b9\",\n grassA10: \"#007b17c1\",\n grassA11: \"#006514d5\",\n grassA12: \"#002006df\",\n};\nconst grassP3 = {\n grass1: \"color(display-p3 0.986 0.996 0.985)\",\n grass2: \"color(display-p3 0.966 0.983 0.964)\",\n grass3: \"color(display-p3 0.923 0.965 0.917)\",\n grass4: \"color(display-p3 0.872 0.94 0.865)\",\n grass5: \"color(display-p3 0.811 0.908 0.802)\",\n grass6: \"color(display-p3 0.733 0.864 0.724)\",\n grass7: \"color(display-p3 0.628 0.803 0.622)\",\n grass8: \"color(display-p3 0.477 0.72 0.482)\",\n grass9: \"color(display-p3 0.38 0.647 0.378)\",\n grass10: \"color(display-p3 0.344 0.598 0.342)\",\n grass11: \"color(display-p3 0.263 0.488 0.261)\",\n grass12: \"color(display-p3 0.151 0.233 0.153)\",\n};\nconst grassP3A = {\n grassA1: \"color(display-p3 0.024 0.757 0.024 / 0.016)\",\n grassA2: \"color(display-p3 0.024 0.565 0.024 / 0.036)\",\n grassA3: \"color(display-p3 0.059 0.576 0.008 / 0.083)\",\n grassA4: \"color(display-p3 0.035 0.565 0.008 / 0.134)\",\n grassA5: \"color(display-p3 0.047 0.545 0.008 / 0.197)\",\n grassA6: \"color(display-p3 0.031 0.502 0.004 / 0.275)\",\n grassA7: \"color(display-p3 0.012 0.482 0.004 / 0.377)\",\n grassA8: \"color(display-p3 0 0.467 0.008 / 0.522)\",\n grassA9: \"color(display-p3 0.008 0.435 0 / 0.624)\",\n grassA10: \"color(display-p3 0.008 0.388 0 / 0.659)\",\n grassA11: \"color(display-p3 0.263 0.488 0.261)\",\n grassA12: \"color(display-p3 0.151 0.233 0.153)\",\n};\nconst brown = {\n brown1: \"#fefdfc\",\n brown2: \"#fcf9f6\",\n brown3: \"#f6eee7\",\n brown4: \"#f0e4d9\",\n brown5: \"#ebdaca\",\n brown6: \"#e4cdb7\",\n brown7: \"#dcbc9f\",\n brown8: \"#cea37e\",\n brown9: \"#ad7f58\",\n brown10: \"#a07553\",\n brown11: \"#815e46\",\n brown12: \"#3e332e\",\n};\nconst brownA = {\n brownA1: \"#aa550003\",\n brownA2: \"#aa550009\",\n brownA3: \"#a04b0018\",\n brownA4: \"#9b4a0026\",\n brownA5: \"#9f4d0035\",\n brownA6: \"#a04e0048\",\n brownA7: \"#a34e0060\",\n brownA8: \"#9f4a0081\",\n brownA9: \"#823c00a7\",\n brownA10: \"#723300ac\",\n brownA11: \"#522100b9\",\n brownA12: \"#140600d1\",\n};\nconst brownP3 = {\n brown1: \"color(display-p3 0.995 0.992 0.989)\",\n brown2: \"color(display-p3 0.987 0.976 0.964)\",\n brown3: \"color(display-p3 0.959 0.936 0.909)\",\n brown4: \"color(display-p3 0.934 0.897 0.855)\",\n brown5: \"color(display-p3 0.909 0.856 0.798)\",\n brown6: \"color(display-p3 0.88 0.808 0.73)\",\n brown7: \"color(display-p3 0.841 0.742 0.639)\",\n brown8: \"color(display-p3 0.782 0.647 0.514)\",\n brown9: \"color(display-p3 0.651 0.505 0.368)\",\n brown10: \"color(display-p3 0.601 0.465 0.344)\",\n brown11: \"color(display-p3 0.485 0.374 0.288)\",\n brown12: \"color(display-p3 0.236 0.202 0.183)\",\n};\nconst brownP3A = {\n brownA1: \"color(display-p3 0.675 0.349 0.024 / 0.012)\",\n brownA2: \"color(display-p3 0.675 0.349 0.024 / 0.036)\",\n brownA3: \"color(display-p3 0.573 0.314 0.012 / 0.091)\",\n brownA4: \"color(display-p3 0.545 0.302 0.008 / 0.146)\",\n brownA5: \"color(display-p3 0.561 0.29 0.004 / 0.204)\",\n brownA6: \"color(display-p3 0.553 0.294 0.004 / 0.271)\",\n brownA7: \"color(display-p3 0.557 0.286 0.004 / 0.361)\",\n brownA8: \"color(display-p3 0.549 0.275 0.004 / 0.487)\",\n brownA9: \"color(display-p3 0.447 0.22 0 / 0.632)\",\n brownA10: \"color(display-p3 0.388 0.188 0 / 0.655)\",\n brownA11: \"color(display-p3 0.485 0.374 0.288)\",\n brownA12: \"color(display-p3 0.236 0.202 0.183)\",\n};\nconst bronze = {\n bronze1: \"#fdfcfc\",\n bronze2: \"#fdf7f5\",\n bronze3: \"#f6edea\",\n bronze4: \"#efe4df\",\n bronze5: \"#e7d9d3\",\n bronze6: \"#dfcdc5\",\n bronze7: \"#d3bcb3\",\n bronze8: \"#c2a499\",\n bronze9: \"#a18072\",\n bronze10: \"#957468\",\n bronze11: \"#7d5e54\",\n bronze12: \"#43302b\",\n};\nconst bronzeA = {\n bronzeA1: \"#55000003\",\n bronzeA2: \"#cc33000a\",\n bronzeA3: \"#92250015\",\n bronzeA4: \"#80280020\",\n bronzeA5: \"#7423002c\",\n bronzeA6: \"#7324003a\",\n bronzeA7: \"#6c1f004c\",\n bronzeA8: \"#671c0066\",\n bronzeA9: \"#551a008d\",\n bronzeA10: \"#4c150097\",\n bronzeA11: \"#3d0f00ab\",\n bronzeA12: \"#1d0600d4\",\n};\nconst bronzeP3 = {\n bronze1: \"color(display-p3 0.991 0.988 0.988)\",\n bronze2: \"color(display-p3 0.989 0.97 0.961)\",\n bronze3: \"color(display-p3 0.958 0.932 0.919)\",\n bronze4: \"color(display-p3 0.929 0.894 0.877)\",\n bronze5: \"color(display-p3 0.898 0.853 0.832)\",\n bronze6: \"color(display-p3 0.861 0.805 0.778)\",\n bronze7: \"color(display-p3 0.812 0.739 0.706)\",\n bronze8: \"color(display-p3 0.741 0.647 0.606)\",\n bronze9: \"color(display-p3 0.611 0.507 0.455)\",\n bronze10: \"color(display-p3 0.563 0.461 0.414)\",\n bronze11: \"color(display-p3 0.471 0.373 0.336)\",\n bronze12: \"color(display-p3 0.251 0.191 0.172)\",\n};\nconst bronzeP3A = {\n bronzeA1: \"color(display-p3 0.349 0.024 0.024 / 0.012)\",\n bronzeA2: \"color(display-p3 0.71 0.22 0.024 / 0.04)\",\n bronzeA3: \"color(display-p3 0.482 0.2 0.008 / 0.083)\",\n bronzeA4: \"color(display-p3 0.424 0.133 0.004 / 0.122)\",\n bronzeA5: \"color(display-p3 0.4 0.145 0.004 / 0.169)\",\n bronzeA6: \"color(display-p3 0.388 0.125 0.004 / 0.224)\",\n bronzeA7: \"color(display-p3 0.365 0.11 0.004 / 0.295)\",\n bronzeA8: \"color(display-p3 0.341 0.102 0.004 / 0.393)\",\n bronzeA9: \"color(display-p3 0.29 0.094 0 / 0.546)\",\n bronzeA10: \"color(display-p3 0.255 0.082 0 / 0.585)\",\n bronzeA11: \"color(display-p3 0.471 0.373 0.336)\",\n bronzeA12: \"color(display-p3 0.251 0.191 0.172)\",\n};\nconst gold = {\n gold1: \"#fdfdfc\",\n gold2: \"#faf9f2\",\n gold3: \"#f2f0e7\",\n gold4: \"#eae6db\",\n gold5: \"#e1dccf\",\n gold6: \"#d8d0bf\",\n gold7: \"#cbc0aa\",\n gold8: \"#b9a88d\",\n gold9: \"#978365\",\n gold10: \"#8c7a5e\",\n gold11: \"#71624b\",\n gold12: \"#3b352b\",\n};\nconst goldA = {\n goldA1: \"#55550003\",\n goldA2: \"#9d8a000d\",\n goldA3: \"#75600018\",\n goldA4: \"#6b4e0024\",\n goldA5: \"#60460030\",\n goldA6: \"#64440040\",\n goldA7: \"#63420055\",\n goldA8: \"#633d0072\",\n goldA9: \"#5332009a\",\n goldA10: \"#492d00a1\",\n goldA11: \"#362100b4\",\n goldA12: \"#130c00d4\",\n};\nconst goldP3 = {\n gold1: \"color(display-p3 0.992 0.992 0.989)\",\n gold2: \"color(display-p3 0.98 0.976 0.953)\",\n gold3: \"color(display-p3 0.947 0.94 0.909)\",\n gold4: \"color(display-p3 0.914 0.904 0.865)\",\n gold5: \"color(display-p3 0.88 0.865 0.816)\",\n gold6: \"color(display-p3 0.84 0.818 0.756)\",\n gold7: \"color(display-p3 0.788 0.753 0.677)\",\n gold8: \"color(display-p3 0.715 0.66 0.565)\",\n gold9: \"color(display-p3 0.579 0.517 0.41)\",\n gold10: \"color(display-p3 0.538 0.479 0.38)\",\n gold11: \"color(display-p3 0.433 0.386 0.305)\",\n gold12: \"color(display-p3 0.227 0.209 0.173)\",\n};\nconst goldP3A = {\n goldA1: \"color(display-p3 0.349 0.349 0.024 / 0.012)\",\n goldA2: \"color(display-p3 0.592 0.514 0.024 / 0.048)\",\n goldA3: \"color(display-p3 0.4 0.357 0.012 / 0.091)\",\n goldA4: \"color(display-p3 0.357 0.298 0.008 / 0.134)\",\n goldA5: \"color(display-p3 0.345 0.282 0.004 / 0.185)\",\n goldA6: \"color(display-p3 0.341 0.263 0.004 / 0.244)\",\n goldA7: \"color(display-p3 0.345 0.235 0.004 / 0.322)\",\n goldA8: \"color(display-p3 0.345 0.22 0.004 / 0.436)\",\n goldA9: \"color(display-p3 0.286 0.18 0 / 0.589)\",\n goldA10: \"color(display-p3 0.255 0.161 0 / 0.62)\",\n goldA11: \"color(display-p3 0.433 0.386 0.305)\",\n goldA12: \"color(display-p3 0.227 0.209 0.173)\",\n};\nconst sky = {\n sky1: \"#f9feff\",\n sky2: \"#f1fafd\",\n sky3: \"#e1f6fd\",\n sky4: \"#d1f0fa\",\n sky5: \"#bee7f5\",\n sky6: \"#a9daed\",\n sky7: \"#8dcae3\",\n sky8: \"#60b3d7\",\n sky9: \"#7ce2fe\",\n sky10: \"#74daf8\",\n sky11: \"#00749e\",\n sky12: \"#1d3e56\",\n};\nconst skyA = {\n skyA1: \"#00d5ff06\",\n skyA2: \"#00a4db0e\",\n skyA3: \"#00b3ee1e\",\n skyA4: \"#00ace42e\",\n skyA5: \"#00a1d841\",\n skyA6: \"#0092ca56\",\n skyA7: \"#0089c172\",\n skyA8: \"#0085bf9f\",\n skyA9: \"#00c7fe83\",\n skyA10: \"#00bcf38b\",\n skyA11: \"#00749e\",\n skyA12: \"#002540e2\",\n};\nconst skyP3 = {\n sky1: \"color(display-p3 0.98 0.995 0.999)\",\n sky2: \"color(display-p3 0.953 0.98 0.99)\",\n sky3: \"color(display-p3 0.899 0.963 0.989)\",\n sky4: \"color(display-p3 0.842 0.937 0.977)\",\n sky5: \"color(display-p3 0.777 0.9 0.954)\",\n sky6: \"color(display-p3 0.701 0.851 0.921)\",\n sky7: \"color(display-p3 0.604 0.785 0.879)\",\n sky8: \"color(display-p3 0.457 0.696 0.829)\",\n sky9: \"color(display-p3 0.585 0.877 0.983)\",\n sky10: \"color(display-p3 0.555 0.845 0.959)\",\n sky11: \"color(display-p3 0.193 0.448 0.605)\",\n sky12: \"color(display-p3 0.145 0.241 0.329)\",\n};\nconst skyP3A = {\n skyA1: \"color(display-p3 0.02 0.804 1 / 0.02)\",\n skyA2: \"color(display-p3 0.024 0.592 0.757 / 0.048)\",\n skyA3: \"color(display-p3 0.004 0.655 0.886 / 0.102)\",\n skyA4: \"color(display-p3 0.004 0.604 0.851 / 0.157)\",\n skyA5: \"color(display-p3 0.004 0.565 0.792 / 0.224)\",\n skyA6: \"color(display-p3 0.004 0.502 0.737 / 0.299)\",\n skyA7: \"color(display-p3 0.004 0.459 0.694 / 0.397)\",\n skyA8: \"color(display-p3 0 0.435 0.682 / 0.542)\",\n skyA9: \"color(display-p3 0.004 0.71 0.965 / 0.416)\",\n skyA10: \"color(display-p3 0.004 0.647 0.914 / 0.444)\",\n skyA11: \"color(display-p3 0.193 0.448 0.605)\",\n skyA12: \"color(display-p3 0.145 0.241 0.329)\",\n};\nconst mint = {\n mint1: \"#f9fefd\",\n mint2: \"#f2fbf9\",\n mint3: \"#ddf9f2\",\n mint4: \"#c8f4e9\",\n mint5: \"#b3ecde\",\n mint6: \"#9ce0d0\",\n mint7: \"#7ecfbd\",\n mint8: \"#4cbba5\",\n mint9: \"#86ead4\",\n mint10: \"#7de0cb\",\n mint11: \"#027864\",\n mint12: \"#16433c\",\n};\nconst mintA = {\n mintA1: \"#00d5aa06\",\n mintA2: \"#00b18a0d\",\n mintA3: \"#00d29e22\",\n mintA4: \"#00cc9937\",\n mintA5: \"#00c0914c\",\n mintA6: \"#00b08663\",\n mintA7: \"#00a17d81\",\n mintA8: \"#009e7fb3\",\n mintA9: \"#00d3a579\",\n mintA10: \"#00c39982\",\n mintA11: \"#007763fd\",\n mintA12: \"#00312ae9\",\n};\nconst mintP3 = {\n mint1: \"color(display-p3 0.98 0.995 0.992)\",\n mint2: \"color(display-p3 0.957 0.985 0.977)\",\n mint3: \"color(display-p3 0.888 0.972 0.95)\",\n mint4: \"color(display-p3 0.819 0.951 0.916)\",\n mint5: \"color(display-p3 0.747 0.918 0.873)\",\n mint6: \"color(display-p3 0.668 0.87 0.818)\",\n mint7: \"color(display-p3 0.567 0.805 0.744)\",\n mint8: \"color(display-p3 0.42 0.724 0.649)\",\n mint9: \"color(display-p3 0.62 0.908 0.834)\",\n mint10: \"color(display-p3 0.585 0.871 0.797)\",\n mint11: \"color(display-p3 0.203 0.463 0.397)\",\n mint12: \"color(display-p3 0.136 0.259 0.236)\",\n};\nconst mintP3A = {\n mintA1: \"color(display-p3 0.02 0.804 0.608 / 0.02)\",\n mintA2: \"color(display-p3 0.02 0.647 0.467 / 0.044)\",\n mintA3: \"color(display-p3 0.004 0.761 0.553 / 0.114)\",\n mintA4: \"color(display-p3 0.004 0.741 0.545 / 0.181)\",\n mintA5: \"color(display-p3 0.004 0.678 0.51 / 0.255)\",\n mintA6: \"color(display-p3 0.004 0.616 0.463 / 0.334)\",\n mintA7: \"color(display-p3 0.004 0.549 0.412 / 0.432)\",\n mintA8: \"color(display-p3 0 0.529 0.392 / 0.581)\",\n mintA9: \"color(display-p3 0.004 0.765 0.569 / 0.381)\",\n mintA10: \"color(display-p3 0.004 0.69 0.51 / 0.416)\",\n mintA11: \"color(display-p3 0.203 0.463 0.397)\",\n mintA12: \"color(display-p3 0.136 0.259 0.236)\",\n};\nconst lime = {\n lime1: \"#fcfdfa\",\n lime2: \"#f8faf3\",\n lime3: \"#eef6d6\",\n lime4: \"#e2f0bd\",\n lime5: \"#d3e7a6\",\n lime6: \"#c2da91\",\n lime7: \"#abc978\",\n lime8: \"#8db654\",\n lime9: \"#bdee63\",\n lime10: \"#b0e64c\",\n lime11: \"#5c7c2f\",\n lime12: \"#37401c\",\n};\nconst limeA = {\n limeA1: \"#66990005\",\n limeA2: \"#6b95000c\",\n limeA3: \"#96c80029\",\n limeA4: \"#8fc60042\",\n limeA5: \"#81bb0059\",\n limeA6: \"#72aa006e\",\n limeA7: \"#61990087\",\n limeA8: \"#559200ab\",\n limeA9: \"#93e4009c\",\n limeA10: \"#8fdc00b3\",\n limeA11: \"#375f00d0\",\n limeA12: \"#1e2900e3\",\n};\nconst limeP3 = {\n lime1: \"color(display-p3 0.989 0.992 0.981)\",\n lime2: \"color(display-p3 0.975 0.98 0.954)\",\n lime3: \"color(display-p3 0.939 0.965 0.851)\",\n lime4: \"color(display-p3 0.896 0.94 0.76)\",\n lime5: \"color(display-p3 0.843 0.903 0.678)\",\n lime6: \"color(display-p3 0.778 0.852 0.599)\",\n lime7: \"color(display-p3 0.694 0.784 0.508)\",\n lime8: \"color(display-p3 0.585 0.707 0.378)\",\n lime9: \"color(display-p3 0.78 0.928 0.466)\",\n lime10: \"color(display-p3 0.734 0.896 0.397)\",\n lime11: \"color(display-p3 0.386 0.482 0.227)\",\n lime12: \"color(display-p3 0.222 0.25 0.128)\",\n};\nconst limeP3A = {\n limeA1: \"color(display-p3 0.412 0.608 0.02 / 0.02)\",\n limeA2: \"color(display-p3 0.514 0.592 0.024 / 0.048)\",\n limeA3: \"color(display-p3 0.584 0.765 0.008 / 0.15)\",\n limeA4: \"color(display-p3 0.561 0.757 0.004 / 0.24)\",\n limeA5: \"color(display-p3 0.514 0.698 0.004 / 0.322)\",\n limeA6: \"color(display-p3 0.443 0.627 0 / 0.4)\",\n limeA7: \"color(display-p3 0.376 0.561 0.004 / 0.491)\",\n limeA8: \"color(display-p3 0.333 0.529 0 / 0.624)\",\n limeA9: \"color(display-p3 0.588 0.867 0 / 0.534)\",\n limeA10: \"color(display-p3 0.561 0.827 0 / 0.604)\",\n limeA11: \"color(display-p3 0.386 0.482 0.227)\",\n limeA12: \"color(display-p3 0.222 0.25 0.128)\",\n};\nconst yellow = {\n yellow1: \"#fdfdf9\",\n yellow2: \"#fefce9\",\n yellow3: \"#fffab8\",\n yellow4: \"#fff394\",\n yellow5: \"#ffe770\",\n yellow6: \"#f3d768\",\n yellow7: \"#e4c767\",\n yellow8: \"#d5ae39\",\n yellow9: \"#ffe629\",\n yellow10: \"#ffdc00\",\n yellow11: \"#9e6c00\",\n yellow12: \"#473b1f\",\n};\nconst yellowA = {\n yellowA1: \"#aaaa0006\",\n yellowA2: \"#f4dd0016\",\n yellowA3: \"#ffee0047\",\n yellowA4: \"#ffe3016b\",\n yellowA5: \"#ffd5008f\",\n yellowA6: \"#ebbc0097\",\n yellowA7: \"#d2a10098\",\n yellowA8: \"#c99700c6\",\n yellowA9: \"#ffe100d6\",\n yellowA10: \"#ffdc00\",\n yellowA11: \"#9e6c00\",\n yellowA12: \"#2e2000e0\",\n};\nconst yellowP3 = {\n yellow1: \"color(display-p3 0.992 0.992 0.978)\",\n yellow2: \"color(display-p3 0.995 0.99 0.922)\",\n yellow3: \"color(display-p3 0.997 0.982 0.749)\",\n yellow4: \"color(display-p3 0.992 0.953 0.627)\",\n yellow5: \"color(display-p3 0.984 0.91 0.51)\",\n yellow6: \"color(display-p3 0.934 0.847 0.474)\",\n yellow7: \"color(display-p3 0.876 0.785 0.46)\",\n yellow8: \"color(display-p3 0.811 0.689 0.313)\",\n yellow9: \"color(display-p3 1 0.92 0.22)\",\n yellow10: \"color(display-p3 0.977 0.868 0.291)\",\n yellow11: \"color(display-p3 0.6 0.44 0)\",\n yellow12: \"color(display-p3 0.271 0.233 0.137)\",\n};\nconst yellowP3A = {\n yellowA1: \"color(display-p3 0.675 0.675 0.024 / 0.024)\",\n yellowA2: \"color(display-p3 0.953 0.855 0.008 / 0.079)\",\n yellowA3: \"color(display-p3 0.988 0.925 0.004 / 0.251)\",\n yellowA4: \"color(display-p3 0.98 0.875 0.004 / 0.373)\",\n yellowA5: \"color(display-p3 0.969 0.816 0.004 / 0.491)\",\n yellowA6: \"color(display-p3 0.875 0.71 0 / 0.526)\",\n yellowA7: \"color(display-p3 0.769 0.604 0 / 0.542)\",\n yellowA8: \"color(display-p3 0.725 0.549 0 / 0.687)\",\n yellowA9: \"color(display-p3 1 0.898 0 / 0.781)\",\n yellowA10: \"color(display-p3 0.969 0.812 0 / 0.71)\",\n yellowA11: \"color(display-p3 0.6 0.44 0)\",\n yellowA12: \"color(display-p3 0.271 0.233 0.137)\",\n};\nconst amber = {\n amber1: \"#fefdfb\",\n amber2: \"#fefbe9\",\n amber3: \"#fff7c2\",\n amber4: \"#ffee9c\",\n amber5: \"#fbe577\",\n amber6: \"#f3d673\",\n amber7: \"#e9c162\",\n amber8: \"#e2a336\",\n amber9: \"#ffc53d\",\n amber10: \"#ffba18\",\n amber11: \"#ab6400\",\n amber12: \"#4f3422\",\n};\nconst amberA = {\n amberA1: \"#c0800004\",\n amberA2: \"#f4d10016\",\n amberA3: \"#ffde003d\",\n amberA4: \"#ffd40063\",\n amberA5: \"#f8cf0088\",\n amberA6: \"#eab5008c\",\n amberA7: \"#dc9b009d\",\n amberA8: \"#da8a00c9\",\n amberA9: \"#ffb300c2\",\n amberA10: \"#ffb300e7\",\n amberA11: \"#ab6400\",\n amberA12: \"#341500dd\",\n};\nconst amberP3 = {\n amber1: \"color(display-p3 0.995 0.992 0.985)\",\n amber2: \"color(display-p3 0.994 0.986 0.921)\",\n amber3: \"color(display-p3 0.994 0.969 0.782)\",\n amber4: \"color(display-p3 0.989 0.937 0.65)\",\n amber5: \"color(display-p3 0.97 0.902 0.527)\",\n amber6: \"color(display-p3 0.936 0.844 0.506)\",\n amber7: \"color(display-p3 0.89 0.762 0.443)\",\n amber8: \"color(display-p3 0.85 0.65 0.3)\",\n amber9: \"color(display-p3 1 0.77 0.26)\",\n amber10: \"color(display-p3 0.959 0.741 0.274)\",\n amber11: \"color(display-p3 0.64 0.4 0)\",\n amber12: \"color(display-p3 0.294 0.208 0.145)\",\n};\nconst amberP3A = {\n amberA1: \"color(display-p3 0.757 0.514 0.024 / 0.016)\",\n amberA2: \"color(display-p3 0.902 0.804 0.008 / 0.079)\",\n amberA3: \"color(display-p3 0.965 0.859 0.004 / 0.22)\",\n amberA4: \"color(display-p3 0.969 0.82 0.004 / 0.35)\",\n amberA5: \"color(display-p3 0.933 0.796 0.004 / 0.475)\",\n amberA6: \"color(display-p3 0.875 0.682 0.004 / 0.495)\",\n amberA7: \"color(display-p3 0.804 0.573 0 / 0.557)\",\n amberA8: \"color(display-p3 0.788 0.502 0 / 0.699)\",\n amberA9: \"color(display-p3 1 0.686 0 / 0.742)\",\n amberA10: \"color(display-p3 0.945 0.643 0 / 0.726)\",\n amberA11: \"color(display-p3 0.64 0.4 0)\",\n amberA12: \"color(display-p3 0.294 0.208 0.145)\",\n};\nconst orange = {\n orange1: \"#fefcfb\",\n orange2: \"#fff7ed\",\n orange3: \"#ffefd6\",\n orange4: \"#ffdfb5\",\n orange5: \"#ffd19a\",\n orange6: \"#ffc182\",\n orange7: \"#f5ae73\",\n orange8: \"#ec9455\",\n orange9: \"#f76b15\",\n orange10: \"#ef5f00\",\n orange11: \"#cc4e00\",\n orange12: \"#582d1d\",\n};\nconst orangeA = {\n orangeA1: \"#c0400004\",\n orangeA2: \"#ff8e0012\",\n orangeA3: \"#ff9c0029\",\n orangeA4: \"#ff91014a\",\n orangeA5: \"#ff8b0065\",\n orangeA6: \"#ff81007d\",\n orangeA7: \"#ed6c008c\",\n orangeA8: \"#e35f00aa\",\n orangeA9: \"#f65e00ea\",\n orangeA10: \"#ef5f00\",\n orangeA11: \"#cc4e00\",\n orangeA12: \"#431200e2\",\n};\nconst orangeP3 = {\n orange1: \"color(display-p3 0.995 0.988 0.985)\",\n orange2: \"color(display-p3 0.994 0.968 0.934)\",\n orange3: \"color(display-p3 0.989 0.938 0.85)\",\n orange4: \"color(display-p3 1 0.874 0.687)\",\n orange5: \"color(display-p3 1 0.821 0.583)\",\n orange6: \"color(display-p3 0.975 0.767 0.545)\",\n orange7: \"color(display-p3 0.919 0.693 0.486)\",\n orange8: \"color(display-p3 0.877 0.597 0.379)\",\n orange9: \"color(display-p3 0.9 0.45 0.2)\",\n orange10: \"color(display-p3 0.87 0.409 0.164)\",\n orange11: \"color(display-p3 0.76 0.34 0)\",\n orange12: \"color(display-p3 0.323 0.185 0.127)\",\n};\nconst orangeP3A = {\n orangeA1: \"color(display-p3 0.757 0.267 0.024 / 0.016)\",\n orangeA2: \"color(display-p3 0.886 0.533 0.008 / 0.067)\",\n orangeA3: \"color(display-p3 0.922 0.584 0.008 / 0.15)\",\n orangeA4: \"color(display-p3 1 0.604 0.004 / 0.314)\",\n orangeA5: \"color(display-p3 1 0.569 0.004 / 0.416)\",\n orangeA6: \"color(display-p3 0.949 0.494 0.004 / 0.455)\",\n orangeA7: \"color(display-p3 0.839 0.408 0 / 0.514)\",\n orangeA8: \"color(display-p3 0.804 0.349 0 / 0.62)\",\n orangeA9: \"color(display-p3 0.878 0.314 0 / 0.8)\",\n orangeA10: \"color(display-p3 0.843 0.29 0 / 0.836)\",\n orangeA11: \"color(display-p3 0.76 0.34 0)\",\n orangeA12: \"color(display-p3 0.323 0.185 0.127)\",\n};\n\nconst blackA = {\n blackA1: \"rgba(0, 0, 0, 0.05)\",\n blackA2: \"rgba(0, 0, 0, 0.1)\",\n blackA3: \"rgba(0, 0, 0, 0.15)\",\n blackA4: \"rgba(0, 0, 0, 0.2)\",\n blackA5: \"rgba(0, 0, 0, 0.3)\",\n blackA6: \"rgba(0, 0, 0, 0.4)\",\n blackA7: \"rgba(0, 0, 0, 0.5)\",\n blackA8: \"rgba(0, 0, 0, 0.6)\",\n blackA9: \"rgba(0, 0, 0, 0.7)\",\n blackA10: \"rgba(0, 0, 0, 0.8)\",\n blackA11: \"rgba(0, 0, 0, 0.9)\",\n blackA12: \"rgba(0, 0, 0, 0.95)\",\n};\nconst blackP3A = {\n blackA1: \"color(display-p3 0 0 0 / 0.05)\",\n blackA2: \"color(display-p3 0 0 0 / 0.1)\",\n blackA3: \"color(display-p3 0 0 0 / 0.15)\",\n blackA4: \"color(display-p3 0 0 0 / 0.2)\",\n blackA5: \"color(display-p3 0 0 0 / 0.3)\",\n blackA6: \"color(display-p3 0 0 0 / 0.4)\",\n blackA7: \"color(display-p3 0 0 0 / 0.5)\",\n blackA8: \"color(display-p3 0 0 0 / 0.6)\",\n blackA9: \"color(display-p3 0 0 0 / 0.7)\",\n blackA10: \"color(display-p3 0 0 0 / 0.8)\",\n blackA11: \"color(display-p3 0 0 0 / 0.9)\",\n blackA12: \"color(display-p3 0 0 0 / 0.95)\",\n};\n\nconst whiteA = {\n whiteA1: \"rgba(255, 255, 255, 0.05)\",\n whiteA2: \"rgba(255, 255, 255, 0.1)\",\n whiteA3: \"rgba(255, 255, 255, 0.15)\",\n whiteA4: \"rgba(255, 255, 255, 0.2)\",\n whiteA5: \"rgba(255, 255, 255, 0.3)\",\n whiteA6: \"rgba(255, 255, 255, 0.4)\",\n whiteA7: \"rgba(255, 255, 255, 0.5)\",\n whiteA8: \"rgba(255, 255, 255, 0.6)\",\n whiteA9: \"rgba(255, 255, 255, 0.7)\",\n whiteA10: \"rgba(255, 255, 255, 0.8)\",\n whiteA11: \"rgba(255, 255, 255, 0.9)\",\n whiteA12: \"rgba(255, 255, 255, 0.95)\",\n};\nconst whiteP3A = {\n whiteA1: \"color(display-p3 1 1 1 / 0.05)\",\n whiteA2: \"color(display-p3 1 1 1 / 0.1)\",\n whiteA3: \"color(display-p3 1 1 1 / 0.15)\",\n whiteA4: \"color(display-p3 1 1 1 / 0.2)\",\n whiteA5: \"color(display-p3 1 1 1 / 0.3)\",\n whiteA6: \"color(display-p3 1 1 1 / 0.4)\",\n whiteA7: \"color(display-p3 1 1 1 / 0.5)\",\n whiteA8: \"color(display-p3 1 1 1 / 0.6)\",\n whiteA9: \"color(display-p3 1 1 1 / 0.7)\",\n whiteA10: \"color(display-p3 1 1 1 / 0.8)\",\n whiteA11: \"color(display-p3 1 1 1 / 0.9)\",\n whiteA12: \"color(display-p3 1 1 1 / 0.95)\",\n};\n\nexport { amber, amberA, amberDark, amberDarkA, amberDarkP3, amberDarkP3A, amberP3, amberP3A, blackA, blackP3A, blue, blueA, blueDark, blueDarkA, blueDarkP3, blueDarkP3A, blueP3, blueP3A, bronze, bronzeA, bronzeDark, bronzeDarkA, bronzeDarkP3, bronzeDarkP3A, bronzeP3, bronzeP3A, brown, brownA, brownDark, brownDarkA, brownDarkP3, brownDarkP3A, brownP3, brownP3A, crimson, crimsonA, crimsonDark, crimsonDarkA, crimsonDarkP3, crimsonDarkP3A, crimsonP3, crimsonP3A, cyan, cyanA, cyanDark, cyanDarkA, cyanDarkP3, cyanDarkP3A, cyanP3, cyanP3A, gold, goldA, goldDark, goldDarkA, goldDarkP3, goldDarkP3A, goldP3, goldP3A, grass, grassA, grassDark, grassDarkA, grassDarkP3, grassDarkP3A, grassP3, grassP3A, gray, grayA, grayDark, grayDarkA, grayDarkP3, grayDarkP3A, grayP3, grayP3A, green, greenA, greenDark, greenDarkA, greenDarkP3, greenDarkP3A, greenP3, greenP3A, indigo, indigoA, indigoDark, indigoDarkA, indigoDarkP3, indigoDarkP3A, indigoP3, indigoP3A, iris, irisA, irisDark, irisDarkA, irisDarkP3, irisDarkP3A, irisP3, irisP3A, jade, jadeA, jadeDark, jadeDarkA, jadeDarkP3, jadeDarkP3A, jadeP3, jadeP3A, lime, limeA, limeDark, limeDarkA, limeDarkP3, limeDarkP3A, limeP3, limeP3A, mauve, mauveA, mauveDark, mauveDarkA, mauveDarkP3, mauveDarkP3A, mauveP3, mauveP3A, mint, mintA, mintDark, mintDarkA, mintDarkP3, mintDarkP3A, mintP3, mintP3A, olive, oliveA, oliveDark, oliveDarkA, oliveDarkP3, oliveDarkP3A, oliveP3, oliveP3A, orange, orangeA, orangeDark, orangeDarkA, orangeDarkP3, orangeDarkP3A, orangeP3, orangeP3A, pink, pinkA, pinkDark, pinkDarkA, pinkDarkP3, pinkDarkP3A, pinkP3, pinkP3A, plum, plumA, plumDark, plumDarkA, plumDarkP3, plumDarkP3A, plumP3, plumP3A, purple, purpleA, purpleDark, purpleDarkA, purpleDarkP3, purpleDarkP3A, purpleP3, purpleP3A, red, redA, redDark, redDarkA, redDarkP3, redDarkP3A, redP3, redP3A, ruby, rubyA, rubyDark, rubyDarkA, rubyDarkP3, rubyDarkP3A, rubyP3, rubyP3A, sage, sageA, sageDark, sageDarkA, sageDarkP3, sageDarkP3A, sageP3, sageP3A, sand, sandA, sandDark, sandDarkA, sandDarkP3, sandDarkP3A, sandP3, sandP3A, sky, skyA, skyDark, skyDarkA, skyDarkP3, skyDarkP3A, skyP3, skyP3A, slate, slateA, slateDark, slateDarkA, slateDarkP3, slateDarkP3A, slateP3, slateP3A, teal, tealA, tealDark, tealDarkA, tealDarkP3, tealDarkP3A, tealP3, tealP3A, tomato, tomatoA, tomatoDark, tomatoDarkA, tomatoDarkP3, tomatoDarkP3A, tomatoP3, tomatoP3A, violet, violetA, violetDark, violetDarkA, violetDarkP3, violetDarkP3A, violetP3, violetP3A, whiteA, whiteP3A, yellow, yellowA, yellowDark, yellowDarkA, yellowDarkP3, yellowDarkP3A, yellowP3, yellowP3A };\n", "/**\n * Curated colour tokens \u2014 single source of truth for every hex\n * the project ships.\n *\n * The underlying scales follow a consistent 12-step semantic model:\n * step 1-2 app background / subtle background\n * step 3-5 UI element backgrounds (hover / active)\n * step 6-8 borders / separators / hovered borders\n * step 9 \"solid\" \u2014 the main brand-ish colour, used for marks /\n * text on a light surface; stays the same hex on both\n * light and dark themes\n * step 10-12 hovered solid / low- and high-contrast text\n *\n * Stable subsets are picked here so the rest of the codebase imports\n * named tokens (`UI.accent.solid`, `ELEMENT_FILL.iris.light`, \u2026)\n * instead of raw hex strings. To re-skin the editor, change the\n * mapping below in one place \u2014 every package picks it up.\n */\nimport {\n amber,\n amberDark,\n cyan,\n cyanDark,\n grass,\n grassDark,\n gray,\n grayDark,\n iris,\n irisDark,\n plum,\n plumDark,\n tomato,\n tomatoDark,\n} from \"@radix-ui/colors\";\n\n/** Hue families exposed to the rest of the codebase. */\nexport const HUES = [\"tomato\", \"amber\", \"grass\", \"cyan\", \"iris\", \"plum\", \"gray\"] as const;\nexport type Hue = (typeof HUES)[number];\n\n/**\n * Per-hue paired tones for shape fills + strokes. Fills use the\n * \"subtle\" step-4 (pastel on light, deep-tinted on dark). Strokes /\n * solids use step-9, which is layout-consistent across themes.\n */\nexport interface HueTones {\n /** Subtle fill \u2014 step-4 (pastel on light, deep-tinted on dark). */\n readonly fill: string;\n /** Solid stroke / mark \u2014 step-9 (same hex in both themes). */\n readonly solid: string;\n /** Hovered solid \u2014 step-10. */\n readonly solidHover: string;\n /** Low-contrast text on subtle fill \u2014 step-11. */\n readonly textLow: string;\n /** High-contrast text \u2014 step-12. */\n readonly textHigh: string;\n}\n\n/** Read a required scale step, failing loudly if the token is absent. */\nconst step = (s: Record<string, string>, key: string): string => {\n const v = s[key];\n if (v === undefined) throw new Error(`Missing color token: ${key}`);\n return v;\n};\n\nconst hueLight = (s: Record<string, string>, name: Hue): HueTones => ({\n fill: step(s, `${name}4`),\n solid: step(s, `${name}9`),\n solidHover: step(s, `${name}10`),\n textLow: step(s, `${name}11`),\n textHigh: step(s, `${name}12`),\n});\n\n/** Lookup `{ hue \u2192 tones }` for a given theme. */\nexport const HUE_TONES = {\n light: {\n tomato: hueLight(tomato, \"tomato\"),\n amber: hueLight(amber, \"amber\"),\n grass: hueLight(grass, \"grass\"),\n cyan: hueLight(cyan, \"cyan\"),\n iris: hueLight(iris, \"iris\"),\n plum: hueLight(plum, \"plum\"),\n gray: hueLight(gray, \"gray\"),\n },\n dark: {\n tomato: hueLight(tomatoDark, \"tomato\"),\n amber: hueLight(amberDark, \"amber\"),\n grass: hueLight(grassDark, \"grass\"),\n cyan: hueLight(cyanDark, \"cyan\"),\n iris: hueLight(irisDark, \"iris\"),\n plum: hueLight(plumDark, \"plum\"),\n gray: hueLight(grayDark, \"gray\"),\n },\n} as const satisfies Record<\"light\" | \"dark\", Record<Hue, HueTones>>;\n\n/**\n * Per-hue step-2 backgrounds \u2014 the \"almost-pure tint\" row used by\n * the canvas palette picker. Step-2 is the \"subtle app background\" \u2014\n * paper-like in light mode, deep-near-black in dark.\n * Exposed separately from `HUE_TONES` because shape fills (step-4)\n * and canvas backgrounds (step-2) have different aesthetic\n * intents \u2014 same hue, very different role.\n */\nexport const CANVAS_TONES = {\n light: {\n tomato: tomato.tomato2,\n amber: amber.amber2,\n grass: grass.grass2,\n cyan: cyan.cyan2,\n iris: iris.iris2,\n plum: plum.plum2,\n gray: gray.gray2,\n },\n dark: {\n tomato: tomatoDark.tomato2,\n amber: amberDark.amber2,\n grass: grassDark.grass2,\n cyan: cyanDark.cyan2,\n iris: irisDark.iris2,\n plum: plumDark.plum2,\n gray: grayDark.gray2,\n },\n} as const satisfies Record<\"light\" | \"dark\", Record<Hue, string>>;\n\n// ---------------------------------------------------------------------------\n// UI surface tokens for chrome (toolbar, panels, modals, tooltips).\n// Intentionally curated, not a full scale: every UI surface picks\n// from a small fixed set so themes stay coherent.\n// ---------------------------------------------------------------------------\n\nexport interface UISurface {\n /** Canvas / page background. */\n readonly canvas: string;\n /** Floating UI background (top bar, panels, popovers) \u2014 opaque. */\n readonly bg: string;\n /** Same as `bg`; kept for hosts that distinguished the two. */\n readonly bgSolid: string;\n /** Subtle border around floating chrome. */\n readonly border: string;\n /** Body text on the bg. */\n readonly text: string;\n /** Secondary / placeholder text. */\n readonly textMuted: string;\n /** Hover tint inside button-groups / flat buttons. */\n readonly hoverOverlay: string;\n}\n\nexport interface UIAccent {\n /** Primary accent \u2014 focus rings, links. */\n readonly accent: string;\n /** Hovered accent. */\n readonly accentHover: string;\n /** Selected / active background (tonal, not saturated). */\n readonly selectedBg: string;\n /** Foreground colour on top of `selectedBg`. */\n readonly selectedFg: string;\n /** Danger / destructive (delete, leave). */\n readonly danger: string;\n}\n\nexport const UI_SURFACE = {\n light: {\n canvas: \"#f5f5f5\",\n bg: \"#ffffff\",\n bgSolid: \"#ffffff\",\n border: \"rgba(0, 0, 0, 0.08)\",\n text: \"#1a1a1a\",\n textMuted: \"#6b6b6b\",\n hoverOverlay: \"rgba(0, 0, 0, 0.05)\",\n },\n dark: {\n // The canvas is deliberately NOT themed: user content (raw hex colors in\n // the scene) is authored against light paper, so dark mode darkens the\n // chrome only. Keep in sync with the light value above.\n canvas: \"#f5f5f5\",\n bg: \"#252525\",\n bgSolid: \"#252525\",\n border: \"rgba(255, 255, 255, 0.08)\",\n text: \"#e8e8e8\",\n textMuted: \"#9a9a9a\",\n hoverOverlay: \"rgba(255, 255, 255, 0.06)\",\n },\n} as const satisfies Record<\"light\" | \"dark\", UISurface>;\n\nexport const UI_ACCENT = {\n light: {\n accent: iris.iris9,\n accentHover: iris.iris10,\n selectedBg: iris.iris4,\n selectedFg: iris.iris11,\n danger: tomato.tomato9,\n },\n dark: {\n accent: irisDark.iris9,\n accentHover: irisDark.iris10,\n selectedBg: irisDark.iris4,\n selectedFg: irisDark.iris11,\n danger: tomatoDark.tomato9,\n },\n} as const satisfies Record<\"light\" | \"dark\", UIAccent>;\n\n// ---------------------------------------------------------------------------\n// Renderer tokens \u2014 colours baked into renderer output (grid,\n// default shape styles for newly-created shapes). Theme-agnostic\n// because the renderer doesn't have a theme context; the canvas\n// content is the user's document, not the chrome around it. Picked\n// to read on both light and dark canvases.\n// ---------------------------------------------------------------------------\n\n/**\n * Grid colour \u2014 neutral gray. Step-6 reads as a calm grid line\n * on a paper-white canvas and stays visible on a near-black one.\n * Single hex for both themes.\n */\nexport const GRID_COLOR = gray.gray6;\n\n/**\n * Dot-grid colour \u2014 deliberately darker than {@link GRID_COLOR}.\n * A lone dot covers far less area than a ruled line, so at the\n * line colour (step-6) the dots read as a faint, low-contrast\n * haze on a gray canvas. Step-9 (\"solid\") gives each dot enough\n * weight to be a legible anchor without turning the field busy.\n */\nexport const GRID_DOT_COLOR = gray.gray9;\n\n/**\n * Default shape styles applied when a user draws a new shape\n * with the toolbar. The user can override anything via the\n * property panel afterwards.\n *\n * Fills use light-theme step-3 (very subtle pastel) so they read\n * cleanly on a paper-white canvas; strokes use step-9 (solid\n * brand colour). Sticky note uses amber for that classic\n * yellow paper feel.\n */\nexport interface DefaultElementStyle {\n readonly fill: string;\n readonly stroke: string;\n readonly strokeWidth: number;\n}\n\nexport const DEFAULT_ELEMENT_STYLES = {\n rectangle: {\n fill: iris.iris3,\n stroke: iris.iris9,\n strokeWidth: 2,\n },\n ellipse: {\n fill: tomato.tomato3,\n stroke: tomato.tomato9,\n strokeWidth: 2,\n },\n flowchart: {\n fill: grass.grass3,\n stroke: grass.grass9,\n strokeWidth: 2,\n },\n sticky: {\n fill: amber.amber3,\n stroke: amber.amber9,\n strokeWidth: 1,\n },\n} as const satisfies Record<string, DefaultElementStyle>;\n\n/**\n * Default style for a freshly-created edge \u2014 neutral dark gray\n * line so it reads on most canvas backgrounds without competing\n * with the connected shapes' brand colours. step-12 of `gray`\n * gives ink-like contrast on paper-white.\n */\nexport const DEFAULT_EDGE_STYLE = {\n stroke: gray.gray12,\n strokeWidth: 1.5,\n} as const;\n\n/**\n * Semantic colours for the scene-diff overlay (`<DiffPanel>`):\n * `added` (green), `removed` (red), `modified` (amber). Picked\n * from step-9 of grass / tomato / amber so the three markers\n * stay legible side by side on a paper-white background.\n */\nexport const DIFF_COLORS = {\n added: grass.grass9,\n removed: tomato.tomato9,\n modified: amber.amber9,\n} as const satisfies Record<\"added\" | \"removed\" | \"modified\", string>;\n", "/**\n * Tunable constants for the renderer core. All \"magic numbers\" used by\n * `renderScene` / `renderLinks` / `renderGrid` live here so there is one\n * place to tweak performance / visual behaviour.\n */\n\nimport { GRID_COLOR, GRID_DOT_COLOR, UI_SURFACE } from \"@oh-just-another/tokens\";\nimport type { LodOptions } from \"./rendering/scene-renderer.js\";\n\n/**\n * Level-of-detail floors, in ON-SCREEN pixels \u2014 decided per element from\n * what actually lands on screen, so the zoom level alone never degrades a\n * shape that is still large or a heading that is still readable.\n *\n * - `LOD_PLACEHOLDER_MAX_SCREEN_PX` \u2014 a shape whose longer side is below\n * this on screen is a flat AABB fill (no detail is visible at that size\n * anyway; saves ~10\u00D7 renderer cost per shape). Range: 4\u201316.\n * - `LOD_MIN_TEXT_SCREEN_PX` \u2014 text whose font size on screen is below this\n * is skipped (glyphs are unreadable below ~6 px; skipping the\n * wrap + measure is the bulk of text cost). Range: 4\u20138.\n *\n * Hosts override per-render by passing `RenderSceneOptions.lod`.\n */\nexport const LOD_PLACEHOLDER_MAX_SCREEN_PX = 8;\nexport const LOD_MIN_TEXT_SCREEN_PX = 6;\nexport const DEFAULT_LOD: LodOptions = {\n placeholderMaxScreenPx: LOD_PLACEHOLDER_MAX_SCREEN_PX,\n minTextScreenPx: LOD_MIN_TEXT_SCREEN_PX,\n};\n\n/**\n * Neutral grey for the empty-text placeholder prompt (`TEXT_PLACEHOLDERS`\n * in `@oh-just-another/scene`) \u2014 the muted text tone of the light UI (the\n * canvas is always light).\n */\nexport const TEXT_PLACEHOLDER_COLOR = UI_SURFACE.light.textMuted;\n\n/**\n * Grey colour used for placeholder fills when LOD switches to the\n * cheapest path. A mid-tone neutral that blends with most scene\n * palettes; override via `RenderSceneOptions.placeholderFill`.\n */\nexport const DEFAULT_PLACEHOLDER_FILL = \"#bbb\";\n\n/**\n * Viewport-rect inflation factor applied by hosts when computing the\n * world-space culling rect. 0.05 = 5% padding on each side \u2014 enough\n * to avoid flicker during a one-frame pan without keeping much\n * off-screen geometry alive in the renderer.\n */\nexport const VIEWPORT_CULL_PADDING_RATIO = 0.05;\n\n/**\n * Text-decoration geometry (underline / strikethrough), as fractions of\n * font size, measured from the line's top (the renderer draws text with\n * a top baseline).\n *\n * - `TEXT_DECORATION_THICKNESS` \u2014 line thickness \u2248 6% of font size\n * (clamped to \u22651 px in the renderer).\n * - `TEXT_UNDERLINE_OFFSET` \u2014 underline top, ~92% down (just below the\n * glyph baseline).\n * - `TEXT_STRIKETHROUGH_OFFSET` \u2014 strikethrough centre, ~50% (x-height).\n */\nexport const TEXT_DECORATION_THICKNESS = 0.06;\n\n/**\n * List layout metrics, in em (\u00D7 font size):\n * - `LIST_INDENT_EM` \u2014 horizontal shift per nesting level; list paragraphs\n * get one extra level for the marker slot. Reasonable range 1.2\u20131.8.\n * - `LIST_MARKER_GAP_EM` \u2014 gap between the marker's right edge and the\n * item text. Reasonable range 0.3\u20130.6.\n */\nexport const LIST_INDENT_EM = 1.4;\nexport const LIST_MARKER_GAP_EM = 0.4;\n\n/**\n * Inset between a shape's bounds and its embedded label text, in em\n * (\u00D7 label font size). Reasonable range 0.3\u20131.0.\n */\nexport const LABEL_PADDING_EM = 0.5;\n\n/**\n * Auto-fit font-size bounds (world px) for `ShapeLabel.autoFit` \u2014 the\n * binary search picks the largest size in this range whose layout fits\n * the shape body. Reasonable ranges: min 8\u201314, max 48\u201396.\n */\nexport const LABEL_AUTOFIT_MIN_PX = 10;\nexport const LABEL_AUTOFIT_MAX_PX = 64;\n\n/**\n * Sticky-note chrome:\n * - `STICKY_DEFAULT_FILL` \u2014 card colour when `style.fill` is omitted.\n * - `STICKY_CORNER_RADIUS` \u2014 corner rounding in world units.\n * - `STICKY_AUTHOR_FONT_SIZE` \u2014 author-name strip font size.\n * - `STICKY_AUTHOR_COLOR` \u2014 author-name text colour.\n */\nexport const STICKY_DEFAULT_FILL = \"#fff9b1\";\nexport const STICKY_CORNER_RADIUS = 4;\nexport const STICKY_AUTHOR_FONT_SIZE = 10;\nexport const STICKY_AUTHOR_COLOR = \"#8a8a6f\";\n\n/**\n * Sticky skeuomorphism (paper look):\n * - `STICKY_SHADOW_COLOR` / `STICKY_SHADOW_OFFSET_Y` \u2014 soft drop shadow\n * under the card (offset in world units, 2\u20136 reasonable).\n * - `STICKY_TAG_*` \u2014 tag pill metrics along the bottom edge.\n */\nexport const STICKY_SHADOW_COLOR = \"rgba(0, 0, 0, 0.18)\";\nexport const STICKY_SHADOW_OFFSET_Y = 4;\nexport const STICKY_TAG_FONT_SIZE = 9;\nexport const STICKY_TAG_PAD_X = 5;\nexport const STICKY_TAG_HEIGHT = 14;\nexport const STICKY_TAG_GAP = 4;\nexport const STICKY_TAG_BG = \"rgba(0, 0, 0, 0.08)\";\nexport const STICKY_TAG_COLOR = \"#555\";\n\n/**\n * Sticky reaction pills (bottom-left row, drawn by the renderer so they\n * reach PNG / SVG exports; the DOM layer only provides click zones).\n */\nexport const STICKY_REACTION_FONT_SIZE = 10;\nexport const STICKY_REACTION_HEIGHT = 16;\nexport const STICKY_REACTION_PAD_X = 6;\nexport const STICKY_REACTION_GAP = 4;\nexport const STICKY_REACTION_BG = \"rgba(255, 255, 255, 0.85)\";\nexport const STICKY_REACTION_COLOR = \"#333\";\n/** Accent for the canvas-drawn \"+\" add-reaction button (iris 9). */\nexport const STICKY_REACTION_ADD_COLOR = \"#5b5bd6\";\n/**\n * Reaction pills keep a CONSTANT on-screen size: their world size is\n * `base / zoom`. Once the sticky's shorter side is narrower than this\n * many screen pixels the reaction chrome (pills AND the \"+\" button) is\n * HIDDEN entirely \u2014 constant-size pills would swallow a small card. A\n * screen-size gate (like the text / placeholder LOD), so a large note keeps\n * its reactions at a zoom where a small one already hides them. Also bounds\n * the worst-case pill world size for render-overflow estimates. Range\n * 40\u2013160 (80 = the medium 160 px preset at 50 % zoom).\n */\nexport const STICKY_REACTION_MIN_SCREEN_PX = 80;\n\n/**\n * What static exports (PNG / SVG) include by default. The export UI can\n * override per run; interactive rendering ignores these and draws\n * everything.\n */\nexport const EXPORT_CONTENT_DEFAULTS = {\n stickyReactions: true,\n stickyTags: true,\n stickyAuthor: true,\n // UI chrome, not content \u2014 never wanted in a static image.\n stickyAddButton: false,\n} as const;\nexport const TEXT_UNDERLINE_OFFSET = 0.92;\nexport const TEXT_STRIKETHROUGH_OFFSET = 0.5;\n\n/**\n * Corner radius (world px) for the rounded bends of an elbow (orthogonal)\n * connector and of a straight connector broken by user waypoints. Each\n * corner is replaced by a quadratic arc of this radius, clamped to half the\n * shorter adjacent segment so short segments don't overshoot. 0 disables\n * rounding (sharp corners). Range: 0\u201316.\n */\nexport const LINK_CORNER_RADIUS = 10;\n\n// --- Grid -------------------------------------------------------------------\n//\n// Lines and dots are tuned independently: a ruled line covers far more\n// pixels than a lone dot, so the dot grid needs a darker colour, a\n// slightly fatter mark, and a denser ladder to read as clearly as the\n// line grid at the same zoom.\n\n/** Stroke colour for the ruled (`\"lines\"`) grid. Neutral step-6 gray. */\nexport const GRID_LINE_COLOR = GRID_COLOR;\n\n/** Fill colour for the dotted (`\"dots\"`) grid \u2014 darker step-9 gray so the dots stay legible on a gray canvas. */\nexport const GRID_DOT_FILL = GRID_DOT_COLOR;\n\n/** On-screen stroke width (px) of a grid line. Divided by zoom at the use site so the line stays 1 px regardless of view scale. */\nexport const GRID_LINE_WIDTH_PX = 1.0;\n\n/**\n * Dot radius (screen px) for `gridStyle === \"dots\"`. Constant across\n * zoom (divided by `zoom` at the use site). Reads as a crisp anchor on\n * a gray surface. Range: 1.0\u20132.0.\n */\nexport const GRID_DOT_RADIUS_PX = 1;\n\n/**\n * Below this on-screen spacing (px) a grid level paints nothing \u2014\n * denser rendering reads as a flat haze. Only used by the fixed-ladder\n * path (`options.levels`); the default dynamic ladder uses the fade\n * bands below instead.\n */\nexport const GRID_MIN_SCREEN_SPACING_PX = 4;\n\n// --- Dynamic (infinite) grid ladder -----------------------------------------\n//\n// The default grid is a SELF-SIMILAR, zoom-relative ladder: instead of a\n// fixed set of world steps it renders a handful of rungs anchored to the\n// current zoom, each rung `GRID_LEVEL_SUBDIV`\u00D7 the previous. As you zoom\n// the rungs slide \u2014 a finer rung fades in and a coarser one fades out \u2014\n// so new lines / dots keep appearing at EVERY zoom, not just at the\n// hand-picked thresholds of a fixed ladder. Rungs finer than `gridSize`\n// are purely visual (snap-to-grid still rounds to `gridSize`).\n\n/** Ratio between adjacent rungs. 4 keeps the 64/16/4/1 cadence. */\nexport const GRID_LEVEL_SUBDIV = 4;\n\n/**\n * How many self-similar rungs to paint at once (finest first). 3 keeps a\n * stable fully-opaque coarse tier while the finest rung fades in/out.\n */\nexport const GRID_LEVEL_RUNGS = 3;\n\n/**\n * Line grid fade band (on-screen px). A rung is invisible at/below\n * `FROM`, ramps to full opacity by `FULL`, and stays full above. Tuned\n * so at 100 % (gridSize 20) the 20 px rung reads faint and the 80 px rung\n * is solid, while subdividing forever.\n */\nexport const GRID_LINE_FADE_FROM_PX = 12;\nexport const GRID_LINE_FADE_FULL_PX = 56;\n\n/**\n * Dot grid fade band. Lower / tighter than lines so the base `gridSize`\n * dot lattice is fully solid at 100 % (the denser dot field) yet still\n * subdivides on zoom-in.\n */\nexport const GRID_DOT_FADE_FROM_PX = 10;\nexport const GRID_DOT_FADE_FULL_PX = 20;\n\n// --- Block-arrow shape (BlockArrowElement) ----------------------------------\n\n/**\n * Fraction of the shape's length given to the arrow head when\n * `BlockArrowElement.headRatio` is omitted. 0.4 = head spans the last 40 %,\n * body the first 60 %. Clamped to `ARROWHEAD_RATIO_MIN`..`ARROWHEAD_RATIO_MAX`.\n */\nexport const ARROWHEAD_HEAD_RATIO = 0.4;\n\n/**\n * Fraction of the shape's cross-axis filled by the body when\n * `BlockArrowElement.bodyThickness` is omitted. 0.5 = body half as thick as\n * the box. Clamped to `ARROWHEAD_RATIO_MIN`..`ARROWHEAD_RATIO_MAX`.\n */\nexport const ARROWHEAD_BODY_THICKNESS = 0.5;\n\n/**\n * Lower clamp for the block-arrow head/body ratios so a degenerate input can't\n * collapse the head or body to nothing. Range: 0\u2013`ARROWHEAD_RATIO_MAX`.\n */\nexport const ARROWHEAD_RATIO_MIN = 0.1;\n\n/**\n * Upper clamp for the block-arrow head/body ratios so the head/body can't eat\n * the whole box. Range: `ARROWHEAD_RATIO_MIN`\u20131.\n */\nexport const ARROWHEAD_RATIO_MAX = 0.9;\n\n// --- Frame chrome colours ---------------------------------------------------\n\n/** Outline colour of a frame when no explicit style overrides it. Neutral gray. */\nexport const FRAME_STROKE_COLOR = \"#888\";\n\n/** Default body fill of a frame when `style.fill` is omitted. White. */\nexport const FRAME_FILL_COLOR = \"#ffffff\";\n\n/** Background fill of the frame's header strip. Near-black. */\nexport const FRAME_HEADER_BG_COLOR = \"#222\";\n\n/** Text colour of the frame's header label. Light gray for contrast on the dark strip. */\nexport const FRAME_HEADER_TEXT_COLOR = \"#ddd\";\n\n// --- Edge / link rendering defaults -----------------------------------------\n\n/**\n * Length (world px) of a block-arrow edge's head triangle when\n * `Link.blockArrow.headLength` is omitted. The body terminates this far before\n * the endpoint so the head fills the gap. Range: ~8\u201340.\n */\nexport const BLOCK_ARROW_HEAD_LENGTH = 18;\n\n/**\n * Body thickness (world px) of a block-arrow edge when\n * `Link.blockArrow.bodyThickness` is omitted. Offset half this on each side of\n * the routed path. Range: ~4\u201332.\n */\nexport const BLOCK_ARROW_BODY_THICKNESS = 12;\n\n/** Fallback fill for a block-arrow edge when neither `style.fill` nor `style.stroke` is set. Mid gray. */\nexport const BLOCK_ARROW_FILL_COLOR = \"#444\";\n\n/** Fallback stroke for a block-arrow edge when `style.stroke` is omitted. Near-black. */\nexport const BLOCK_ARROW_STROKE_COLOR = \"#222\";\n\n/**\n * Arrowhead size (world px) when `LinkArrowheads.size` is omitted. Drives the\n * wing/length scale of every arrowhead style. Range: ~6\u201324.\n */\nexport const ARROWHEAD_SIZE = 10;\n\n/** Fallback stroke colour for an edge / its arrowheads when `style.stroke` is omitted. Black. */\nexport const EDGE_STROKE_COLOR = \"#000\";\n\n/** Fallback text colour of a link label when `LinkLabel.fill` is omitted. Near-black. */\nexport const LABEL_FILL_COLOR = \"#222\";\n\n/** Fallback pill-background colour of a link label when `LinkLabel.background` is omitted. White. */\nexport const LABEL_BG_COLOR = \"#fff\";\n\n/**\n * Corner radius of the label pill behind a link caption (world px at zoom 1).\n * 0 = square. Range: 0\u20138 (clamped visually by the pill height).\n */\nexport const LINK_LABEL_RADIUS = 4;\n", "import type { TextAlign } from \"../targets/render-target.js\";\nimport type { TextParagraph } from \"@oh-just-another/scene\";\nimport { req, type Vec2 } from \"@oh-just-another/types\";\nimport { LIST_INDENT_EM } from \"../constants.js\";\n\n/**\n * Caret-aware text layout. Unlike {@link wrapText} (which collapses\n * whitespace and is only good enough for *drawing*), this keeps every\n * line as an exact substring of the source plus its `[start, end)`\n * character offsets \u2014 so a caret index maps unambiguously to a line +\n * column. Measurement is injected as a `measure(s) => width` callback\n * so the same geometry can be computed against either backend's font\n * metrics (Canvas2D `measureText` or WebGL2 MSDF advances).\n *\n * Convention for `\\n`: a hard newline at source index `k` ends the\n * current line at `end === k` (caret at `k` = end of line) and the next\n * line starts at `start === k + 1` (caret at `k + 1` = start of next\n * line). The `\\n` itself never holds a caret.\n */\nexport interface LaidOutLine {\n /** Exact source substring for this visual line (no whitespace collapsing). */\n readonly text: string;\n /** Source offset where the line begins (inclusive). */\n readonly start: number;\n /** Source offset where the line ends (exclusive; excludes a trailing `\\n`). */\n readonly end: number;\n /** Measured width of `text` in CSS px. */\n readonly width: number;\n /** List indent offset in CSS px (0 for plain paragraphs). */\n readonly indentX: number;\n /** Index of the source paragraph this line belongs to. */\n readonly para: number;\n /** True on the paragraph's first visual line (where the marker draws). */\n readonly paraFirst: boolean;\n}\n\nexport interface EditableTextLayout {\n readonly lines: readonly LaidOutLine[];\n readonly lineHeight: number;\n /** Width the lines are aligned within (maxWidth, or the widest line). */\n readonly blockWidth: number;\n}\n\nexport type MeasureText = (text: string) => number;\n\nexport interface LayoutTextOptions {\n readonly fontSize: number;\n /** Wrap budget in CSS px. `undefined` \u2192 no wrap (split on `\\n` only). */\n readonly maxWidth?: number;\n /** Line-height multiplier. Default 1.2 (matches the text renderer). */\n readonly lineHeightFactor?: number;\n /**\n * Per-paragraph list attributes (aligned by paragraph index). List\n * paragraphs are indented by `(indent + 1) \u00D7 LIST_INDENT_EM \u00D7 fontSize`\n * and their wrap budget shrinks accordingly; plain paragraphs with a\n * bare `indent` shift without the marker slot.\n */\n readonly paragraphs?: readonly TextParagraph[];\n}\n\n/** Default multiplier from font size to line height (matches `drawText`). */\nexport const DEFAULT_LINE_HEIGHT_FACTOR = 1.2;\n\nconst wrapParagraph = (\n para: string,\n base: number,\n maxWidth: number,\n measure: MeasureText,\n out: LaidOutLine[],\n paraIndex: number,\n indentX: number,\n): void => {\n const lineBase = { indentX, para: paraIndex };\n const first = (): boolean => out.length === 0 || req(out[out.length - 1]).para !== paraIndex;\n if (para === \"\") {\n out.push({ text: \"\", start: base, end: base, width: 0, ...lineBase, paraFirst: first() });\n return;\n }\n // Word spans (non-whitespace runs) with offsets relative to `para`.\n const words: { s: number; e: number }[] = [];\n const re = /\\S+/g;\n let m: RegExpExecArray | null;\n while ((m = re.exec(para)) !== null) words.push({ s: m.index, e: m.index + m[0].length });\n if (words.length === 0) {\n // Whitespace-only paragraph \u2014 keep it as one line so offsets survive.\n out.push({\n text: para,\n start: base,\n end: base + para.length,\n width: measure(para),\n ...lineBase,\n paraFirst: first(),\n });\n return;\n }\n\n // `white-space: pre-wrap` + `overflow-wrap: break-word` (standard):\n // preserve whitespace, wrap at word boundaries, and break a word that\n // is itself wider than the line so narrowing the block always reflows\n // (a single long word can't overflow forever). Lines are gapless\n // slices of the source \u2014 inter-word whitespace at a soft break stays\n // on the preceding line \u2014 so every character maps to exactly one line\n // (clean caret offsets). First line starts at 0 (keeps leading\n // whitespace); the last extends to the paragraph end (trailing ws).\n let lineStart = 0;\n const push = (start: number, end: number): void => {\n const text = para.slice(start, end);\n out.push({\n text,\n start: base + start,\n end: base + end,\n width: measure(text),\n ...lineBase,\n paraFirst: first(),\n });\n };\n let i = 0;\n while (i < words.length) {\n const w = req(words[i]);\n if (measure(para.slice(lineStart, w.e)) <= maxWidth) {\n i++; // word fits on the current line \u2014 keep it, try the next\n continue;\n }\n if (w.s > lineStart) {\n // Content precedes this word on the line \u2192 break before it. The\n // whitespace up to its start stays on the current line (pre-wrap).\n push(lineStart, w.s);\n lineStart = w.s;\n continue; // retry this word on the fresh line\n }\n // The word starts the line and alone overflows \u2192 break it by chars,\n // keeping at least one char per line so we always make progress.\n let e = w.s + 1;\n while (e < w.e && measure(para.slice(lineStart, e + 1)) <= maxWidth) e++;\n if (e >= w.e) {\n // Whole (remaining) word consumed \u2014 leave it on the current line\n // and advance. Guards termination when the measurer is degenerate\n // (e.g. a constant stub that never reports \"fits\").\n i++;\n continue;\n }\n push(lineStart, e);\n lineStart = e;\n w.s = e; // remainder of the word continues on the next line\n }\n // Last line keeps everything through the end of the paragraph.\n push(lineStart, para.length);\n};\n\n/**\n * Lay out `text` into visual lines with exact source offsets. Always\n * returns at least one (possibly empty) line.\n */\nexport const layoutText = (\n text: string,\n measure: MeasureText,\n options: LayoutTextOptions,\n): EditableTextLayout => {\n const lineHeight = options.fontSize * (options.lineHeightFactor ?? DEFAULT_LINE_HEIGHT_FACTOR);\n const lines: LaidOutLine[] = [];\n let paraStart = 0;\n let paraIndex = 0;\n for (let i = 0; i <= text.length; i++) {\n if (i === text.length || text[i] === \"\\n\") {\n const para = text.slice(paraStart, i);\n const attrs = options.paragraphs?.[paraIndex];\n const levels = (attrs?.indent ?? 0) + (attrs?.list !== undefined ? 1 : 0);\n const indentX = levels * LIST_INDENT_EM * options.fontSize;\n if (options.maxWidth === undefined) {\n lines.push({\n text: para,\n start: paraStart,\n end: i,\n width: measure(para),\n indentX,\n para: paraIndex,\n paraFirst: true,\n });\n } else {\n // Keep at least one em of budget so a huge indent can't wedge the\n // wrapper into zero-width lines.\n const budget = Math.max(options.fontSize, options.maxWidth - indentX);\n wrapParagraph(para, paraStart, budget, measure, lines, paraIndex, indentX);\n }\n paraStart = i + 1;\n paraIndex++;\n }\n }\n if (lines.length === 0) {\n lines.push({ text: \"\", start: 0, end: 0, width: 0, indentX: 0, para: 0, paraFirst: true });\n }\n let widest = 0;\n for (const l of lines) widest = Math.max(widest, l.width + l.indentX);\n const blockWidth = options.maxWidth ?? widest;\n return { lines, lineHeight, blockWidth };\n};\n\n/** Left edge (local x) where a line's glyphs start, given the align. */\nconst lineLeftX = (lineWidth: number, blockWidth: number, align: TextAlign): number => {\n if (align === \"center\") return blockWidth / 2 - lineWidth / 2;\n if (align === \"right\") return blockWidth - lineWidth;\n return 0;\n};\n\n/**\n * Left edge of a laid-out line INCLUDING its list indent: alignment is\n * computed within the space remaining after the indent, then shifted by\n * it. The single source of glyph-left-x for the renderer, caret,\n * click-to-caret and selection rects \u2014 keep them in lockstep.\n */\nexport const lineLeft = (line: LaidOutLine, blockWidth: number, align: TextAlign): number =>\n lineLeftX(line.width, blockWidth - line.indentX, align) + line.indentX;\n\n/** Index of the line a caret offset falls on (handles boundaries). */\nconst lineIndexForCaret = (layout: EditableTextLayout, caret: number): number => {\n const { lines } = layout;\n for (let i = 0; i < lines.length; i++) {\n const l = req(lines[i]);\n // Caret belongs to this line when it's within [start, end]; the\n // upper bound is inclusive so end-of-line resolves here, while\n // start-of-next-line (end + 1 for a hard `\\n`) resolves to the\n // next line on the following iteration.\n if (caret <= l.end) return i;\n }\n return lines.length - 1;\n};\n\nexport interface CaretGeometry {\n /** Local x of the caret bar. */\n readonly x: number;\n /** Local y of the caret top (baseline-top line origin). */\n readonly y: number;\n /** Caret height (\u2248 font size). */\n readonly height: number;\n /** Index of the line the caret sits on. */\n readonly line: number;\n}\n\n/**\n * Local-space geometry of the caret for a given source `caret` offset.\n * `align` must match the renderer's `textAlign`.\n */\nexport const caretGeometry = (\n layout: EditableTextLayout,\n caret: number,\n measure: MeasureText,\n fontSize: number,\n align: TextAlign,\n): CaretGeometry => {\n const i = lineIndexForCaret(layout, caret);\n const line = req(layout.lines[i]);\n const col = Math.max(0, Math.min(caret, line.end) - line.start);\n const prefixWidth = col === 0 ? 0 : measure(line.text.slice(0, col));\n const left = lineLeft(line, layout.blockWidth, align);\n return { x: left + prefixWidth, y: i * layout.lineHeight, height: fontSize, line: i };\n};\n\n/**\n * Map a local-space point to the nearest source caret offset. Used for\n * click-to-place-caret and drag-to-select.\n */\nexport const pointToCaretIndex = (\n layout: EditableTextLayout,\n point: Vec2,\n measure: MeasureText,\n align: TextAlign,\n): number => {\n const { lines, lineHeight } = layout;\n const i = Math.max(0, Math.min(lines.length - 1, Math.floor(point.y / lineHeight)));\n const line = req(lines[i]);\n const left = lineLeft(line, layout.blockWidth, align);\n // Walk columns, picking the boundary whose x is closest to point.x.\n let best = 0;\n let bestDist = Math.abs(left - point.x);\n for (let col = 1; col <= line.text.length; col++) {\n const x = left + measure(line.text.slice(0, col));\n const d = Math.abs(x - point.x);\n if (d < bestDist) {\n bestDist = d;\n best = col;\n }\n }\n return line.start + best;\n};\n\nexport interface SelectionRect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\n/**\n * Local-space highlight rectangles covering the source range `[from, to)`\n * (order-independent), one per visual line it spans.\n */\nexport const selectionRects = (\n layout: EditableTextLayout,\n from: number,\n to: number,\n measure: MeasureText,\n align: TextAlign,\n): readonly SelectionRect[] => {\n const lo = Math.min(from, to);\n const hi = Math.max(from, to);\n if (lo === hi) return [];\n const rects: SelectionRect[] = [];\n for (let i = 0; i < layout.lines.length; i++) {\n const line = req(layout.lines[i]);\n const a = Math.max(lo, line.start);\n const b = Math.min(hi, line.end);\n if (a > b) continue;\n if (a === b && !(lo <= line.start && hi > line.end)) {\n // Empty intersection on this line, unless the selection spans the\n // hard break past it (then show a thin trailing marker).\n if (!(hi > line.end && lo <= line.end)) continue;\n }\n const left = lineLeft(line, layout.blockWidth, align);\n const xa = left + (a === line.start ? 0 : measure(line.text.slice(0, a - line.start)));\n const xb = left + (b === line.start ? 0 : measure(line.text.slice(0, b - line.start)));\n // A line whose break is inside the selection gets a small trailing\n // sliver so multi-line selections read continuously.\n const trailing = hi > line.end ? layout.lineHeight * 0.25 : 0;\n rects.push({\n x: xa,\n y: i * layout.lineHeight,\n width: Math.max(0, xb - xa) + trailing,\n height: layout.lineHeight,\n });\n }\n return rects;\n};\n", "/**\n * Animated-content adapter registry. The kernel doesn't decode GIF /\n * Lottie / video itself: it exposes an `AnimatedSourceAdapter` interface\n * and a process-global registry indexed by `kind` (\"gif\", \"lottie\",\n * \"video\", \"<your-format>\"). Hosts plug their decoder of choice:\n *\n * registerAnimationAdapter({\n * kind: \"gif\",\n * getFrameAt(data, timestampMs) { ... return ImageBitmap }\n * });\n *\n * An `ImageElement` carrying `animationKind` + `animationData` resolves\n * its source through the registry; without those fields the static `src`\n * is used. The kernel only answers the stateless \"what should this frame\n * look like?\" question \u2014 live playback ticking is the host's job.\n */\n\nexport interface AnimatedSourceAdapter<Data = unknown> {\n readonly kind: string;\n /**\n * Return the image source the renderer should draw at\n * `timestampMs` (typically `performance.now()`). The returned\n * value is opaque \u2014 it gets passed straight to `target.drawImage\n * (image, ...)`. Backends accept different types: Canvas2D wants\n * a `CanvasImageSource`, headless SVG wants a string URL. The\n * adapter's `kind` is paired with the renderer the host actually\n * uses, so there's no ambiguity in practice.\n *\n * Implementations are stateless w.r.t. the registry; they may\n * cache decoded frames internally (the `data` payload is the\n * natural cache key).\n */\n getFrameAt(data: Data, timestampMs: number): unknown;\n /**\n * Optional \u2014 total animation duration in ms. The animation tick\n * uses this to schedule the next frame; an unset value means\n * \"keep ticking forever\" (endless lottie loops or streamed video).\n */\n totalDurationMs?(data: Data): number;\n}\n\nconst registry = new Map<string, AnimatedSourceAdapter>();\n\nexport const registerAnimationAdapter = <D>(adapter: AnimatedSourceAdapter<D>): void => {\n registry.set(adapter.kind, adapter);\n};\n\nexport const unregisterAnimationAdapter = (kind: string): void => {\n registry.delete(kind);\n};\n\nexport const getAnimationAdapter = (kind: string): AnimatedSourceAdapter | undefined =>\n registry.get(kind);\n\nexport const listAnimationKinds = (): readonly string[] => [...registry.keys()];\n\n/**\n * Content-ready notification. Adapters decode lazily and often\n * asynchronously (e.g. the GIF adapter's `createImageBitmap`): the\n * first `getFrameAt` returns `null` while the decode is in flight. For\n * a *playing* shape the host's animation tick re-renders on the next\n * rAF and picks up the frames \u2014 but a **paused** shape (reduced-motion,\n * auto-stopped, frozen) has no tick, so without a nudge it would stay\n * blank forever once decoded. Adapters call\n * {@link notifyAnimationContentReady} when a decode completes; the host\n * (editor) subscribes via {@link onAnimationContentReady} and schedules\n * one more render so the now-decoded (possibly paused) frame paints.\n */\nconst contentListeners = new Set<() => void>();\n\nexport const onAnimationContentReady = (fn: () => void): (() => void) => {\n contentListeners.add(fn);\n return () => contentListeners.delete(fn);\n};\n\nexport const notifyAnimationContentReady = (): void => {\n for (const fn of contentListeners) {\n try {\n fn();\n } catch {\n /* a listener throwing must not break sibling listeners / decode */\n }\n }\n};\n\n/**\n * Pluggable playback clock. Returns the playback position (ms) the\n * animation adapter should be sampled at for a given shape \u2014 letting\n * a host pause / freeze / offset individual animated shapes without\n * the renderer knowing about playback state.\n *\n * Default: wall-clock `performance.now()` for every shape (every GIF\n * plays, in lock-step with real time). A host (the editor) overrides\n * this via {@link setAnimationClock} to consult its per-shape\n * playback map \u2014 returning a frozen value for paused shapes, an\n * offset for shapes started later, etc.\n *\n * Preferred channel: pass a per-instance clock through the render context\n * (`RenderSceneOptions.clock` \u2192 {@link ElementRenderContext.clock}). Each\n * `Editor` threads its own clock that way, so two editors on one page no\n * longer fight over a shared module global. This module-level clock remains a\n * process-global **fallback** for paths that can't thread a context \u2014 headless\n * `renderScene` (SVG / worker / PNG export) and the tile compositor \u2014 where\n * the wall-clock default (or a single host override) is sufficient.\n */\nexport type AnimationClock = (shape: { readonly id?: unknown }) => number;\n\nlet animationClock: AnimationClock = () =>\n typeof performance !== \"undefined\" ? performance.now() : 0;\n\n/**\n * Install the process-global fallback playback clock. Prefer threading a\n * per-instance clock via `RenderSceneOptions.clock`; this setter only affects\n * render paths that don't carry a render context (headless renderers, the tile\n * compositor). Idempotent \u2014 last write wins.\n */\nexport const setAnimationClock = (clock: AnimationClock): void => {\n animationClock = clock;\n};\n\n/** Restore the default wall-clock playback (used in tests / teardown). */\nexport const resetAnimationClock = (): void => {\n animationClock = () => (typeof performance !== \"undefined\" ? performance.now() : 0);\n};\n\n/**\n * Resolve an image source for an `ImageElement`. When the shape has\n * an `animationKind` and a matching adapter is registered, the\n * adapter's `getFrameAt(animationData, t)` result is returned, where\n * `t` comes from the pluggable {@link setAnimationClock} (default\n * wall-clock). Otherwise \u2014 and as a fallback when the adapter throws \u2014\n * falls back to the static `src`. The renderer hands the result to\n * `target.drawImage` without further interpretation.\n */\nexport const resolveImageSource = (\n shape: {\n readonly id?: unknown;\n readonly src: string;\n readonly animationKind?: string;\n readonly animationData?: unknown;\n },\n timestampMs: number = animationClock(shape),\n): unknown => {\n if (!shape.animationKind) return shape.src;\n const adapter = registry.get(shape.animationKind);\n if (!adapter) return shape.src;\n try {\n return adapter.getFrameAt(shape.animationData, timestampMs);\n } catch {\n return shape.src;\n }\n};\n", "/**\n * Runtime guard: is `value` an actual drawable image source that\n * `ctx.drawImage` / `gl.texImage2D` will accept?\n *\n * Needed because a deserialized scene can carry a **garbage**\n * `metadata.image`: a live `<img>` DOM element serialises to `{}`\n * via `JSON.stringify`, so a scene restored from localStorage has\n * `metadata.image === {}` \u2014 a truthy object that passes a naive\n * `typeof === \"object\"` check but throws inside `drawImage`\n * (\"provided value is not of type \u2026\") / `texImage2D` (\"overload\n * resolution failed\").\n *\n * The check is environment-safe: each constructor is probed for\n * existence first (workers / SSR / older browsers may lack some),\n * so it never throws on a missing global. A bare `{}` matches none\n * of them and is rejected.\n *\n * Single implementation for the whole repo \u2014 element renderers\n * (renderer-core), backends (renderer-canvas) and scene rehydration\n * (state) all import it from here.\n */\nconst DRAWABLE_CTOR_NAMES = [\n \"HTMLImageElement\",\n \"HTMLCanvasElement\",\n \"HTMLVideoElement\",\n \"ImageBitmap\",\n \"OffscreenCanvas\",\n \"SVGImageElement\",\n \"VideoFrame\",\n] as const;\n\nexport const isDrawableImageSource = (value: unknown): value is CanvasImageSource => {\n if (typeof value !== \"object\" || value === null) return false;\n const g = globalThis as Record<string, unknown>;\n for (const name of DRAWABLE_CTOR_NAMES) {\n const ctor = g[name];\n if (\n typeof ctor === \"function\" &&\n value instanceof (ctor as new (...args: never[]) => unknown)\n ) {\n return true;\n }\n }\n return false;\n};\n", "import { polygon as polygonMath } from \"@oh-just-another/math\";\nimport {\n getCornerRadius,\n getElementLocalBounds,\n registerRenderOverflow,\n FRAME_HEADER_HEIGHT,\n FRAME_HEADER_PADDING_X,\n FRAME_HEADER_FONT_SIZE,\n type BlockArrowElement,\n type ElementBase,\n type EmojiElement,\n type StickyElement,\n type BrushElement,\n type EllipseElement,\n type FrameElement,\n type GroupElement,\n type ImageElement,\n type ImageMask,\n type PathElement,\n type PolygonElement,\n type RectangleElement,\n type Style,\n type TextElement,\n type TextRun,\n type TextStyle,\n sliceRuns,\n listMarkers,\n paragraphCount,\n brushBodyColor,\n brushOutline,\n pickTextPlaceholder,\n} from \"@oh-just-another/scene\";\nimport { registerElementRenderer, type ElementRenderer } from \"./shape-renderer.js\";\nimport type { RenderTarget } from \"../targets/render-target.js\";\nimport { isTextBelowLod, type LodOptions } from \"./lod.js\";\nimport {\n DEFAULT_LINE_HEIGHT_FACTOR,\n layoutText,\n lineLeft,\n type EditableTextLayout,\n} from \"../text/text-editing.js\";\nimport { resolveImageSource } from \"../raster/animation-adapter.js\";\nimport { isDrawableImageSource } from \"../raster/image-source-guard.js\";\nimport {\n LABEL_PADDING_EM,\n LABEL_AUTOFIT_MIN_PX,\n LABEL_AUTOFIT_MAX_PX,\n STICKY_DEFAULT_FILL,\n STICKY_CORNER_RADIUS,\n STICKY_AUTHOR_FONT_SIZE,\n STICKY_AUTHOR_COLOR,\n STICKY_SHADOW_COLOR,\n STICKY_SHADOW_OFFSET_Y,\n STICKY_TAG_FONT_SIZE,\n STICKY_TAG_PAD_X,\n STICKY_TAG_HEIGHT,\n STICKY_TAG_GAP,\n STICKY_TAG_BG,\n STICKY_TAG_COLOR,\n STICKY_REACTION_FONT_SIZE,\n STICKY_REACTION_HEIGHT,\n STICKY_REACTION_PAD_X,\n STICKY_REACTION_GAP,\n STICKY_REACTION_BG,\n STICKY_REACTION_ADD_COLOR,\n STICKY_REACTION_MIN_SCREEN_PX,\n STICKY_REACTION_COLOR,\n LIST_MARKER_GAP_EM,\n TEXT_DECORATION_THICKNESS,\n TEXT_UNDERLINE_OFFSET,\n TEXT_STRIKETHROUGH_OFFSET,\n ARROWHEAD_HEAD_RATIO,\n ARROWHEAD_BODY_THICKNESS,\n ARROWHEAD_RATIO_MIN,\n ARROWHEAD_RATIO_MAX,\n FRAME_STROKE_COLOR,\n FRAME_FILL_COLOR,\n FRAME_HEADER_BG_COLOR,\n FRAME_HEADER_TEXT_COLOR,\n TEXT_PLACEHOLDER_COLOR,\n} from \"../constants.js\";\nimport { req, type Vec2 } from \"@oh-just-another/types\";\n\n/**\n * Applies common style fields to a target. Returns whether any fill or stroke\n * was configured \u2014 shape renderers use the result to decide which paint call\n * to issue.\n */\nconst applyStyle = (style: Style, target: RenderTarget): { fill: boolean; stroke: boolean } => {\n const hasFill = style.fill !== undefined && style.fill !== \"transparent\";\n const hasStroke =\n style.stroke !== undefined && style.stroke !== \"transparent\" && (style.strokeWidth ?? 1) > 0;\n\n if (hasFill) target.setFill(style.fill);\n if (hasStroke) {\n target.setStroke(style.stroke);\n target.setStrokeWidth(style.strokeWidth ?? 1);\n if (style.lineCap) target.setLineCap(style.lineCap);\n if (style.lineJoin) target.setLineJoin(style.lineJoin);\n if (style.dashArray) target.setDashArray(style.dashArray);\n }\n if (style.opacity !== undefined) target.setOpacity(style.opacity);\n\n return { fill: hasFill, stroke: hasStroke };\n};\n\nconst drawRectangle: ElementRenderer<RectangleElement> = (shape, target) => {\n const { fill, stroke } = applyStyle(shape.style, target);\n if (!fill && !stroke) return;\n const r = getCornerRadius(shape.style.roundness, shape.width, shape.height);\n // Fill path \u2014 always uses the original shape geometry.\n if (fill) {\n target.beginPath();\n if (r > 0) {\n buildRoundedRectPath(target, 0, 0, shape.width, shape.height, r);\n } else {\n target.rect(0, 0, shape.width, shape.height);\n }\n target.fill();\n }\n // Stroke path \u2014 offset by `strokeAlign` so the stroke sits inside\n // / centred-on / outside the fill region. The default (omitted /\n // `center`) reuses the fill geometry. Implemented at this layer so\n // every backend (Canvas2D, WebGL2, SVG) honours strokeAlign without\n // backend-specific work \u2014 the math is purely on the rect bounds.\n if (stroke) {\n const offset = strokeAlignOffset(shape.style);\n const sx = offset;\n const sy = offset;\n const sw = shape.width - 2 * offset;\n const sh = shape.height - 2 * offset;\n if (sw <= 0 || sh <= 0) return; // degenerate offset \u2014 skip\n const sr = r > 0 ? Math.max(0, r - offset) : 0;\n target.beginPath();\n if (sr > 0) {\n buildRoundedRectPath(target, sx, sy, sw, sh, sr);\n } else {\n target.rect(sx, sy, sw, sh);\n }\n target.stroke();\n }\n};\n\n/**\n * Translate `Style.strokeAlign` into a path-offset distance in world\n * units. The rendered stroke geometry shifts by `\u00B1half-width` along\n * the inward / outward normal:\n * center \u2192 0 (path centred \u2014 Canvas2D / SVG default).\n * inside \u2192 +half-width (path moves inward so the stroke's outer\n * edge sits on the original fill boundary).\n * outside \u2192 -half-width (path moves outward so the stroke's inner\n * edge sits on the boundary).\n *\n * Only used by axis-aligned primitives (rectangle, container) where\n * \"inward\" reduces to \"subtract from bbox\".\n */\nconst strokeAlignOffset = (style: Style): number => {\n const align = style.strokeAlign ?? \"center\";\n if (align === \"center\") return 0;\n const half = (style.strokeWidth ?? 1) / 2;\n return align === \"inside\" ? half : -half;\n};\n\n/**\n * Build a rounded-rect path via the standard \"4 corners with\n * quadratic Bezier arcs\" pattern \u2014 same shape every backend\n * understands without a special `roundRect()` API:\n *\n * \u250C\u2500\u2500\u2500arc\u2500\u2500\u2500\u2510\n * \u2502 \u2502\n * arc arc\n * \u2502 \u2502\n * \u2514\u2500\u2500\u2500arc\u2500\u2500\u2500\u2518\n *\n * Quadratic control points sit at each corner of the rect; the\n * curve goes from `r` units along one side to `r` units along the\n * adjacent side.\n *\n * Radius `r` is pre-clamped by `getCornerRadius` to half the\n * smaller side, so no overlap-handling is needed here.\n */\nexport const buildRoundedRectPath = (\n target: RenderTarget,\n x: number,\n y: number,\n w: number,\n h: number,\n r: number,\n): void => {\n target.moveTo(x + r, y);\n target.lineTo(x + w - r, y);\n target.quadraticCurveTo(x + w, y, x + w, y + r);\n target.lineTo(x + w, y + h - r);\n target.quadraticCurveTo(x + w, y + h, x + w - r, y + h);\n target.lineTo(x + r, y + h);\n target.quadraticCurveTo(x, y + h, x, y + h - r);\n target.lineTo(x, y + r);\n target.quadraticCurveTo(x, y, x + r, y);\n target.closePath();\n};\n\nconst drawEllipse: ElementRenderer<EllipseElement> = (shape, target) => {\n const { fill, stroke } = applyStyle(shape.style, target);\n if (!fill && !stroke) return;\n const rx = shape.width / 2;\n const ry = shape.height / 2;\n if (fill) {\n target.beginPath();\n target.ellipse(rx, ry, rx, ry);\n target.fill();\n }\n if (stroke) {\n // Inset / outset radii by `strokeAlignOffset` so the stroke\n // sits inside / centred-on / outside the fill ellipse. Centre\n // stays the same; radii shift uniformly. Degenerate (radius \u2264 0)\n // skips the pass.\n const offset = strokeAlignOffset(shape.style);\n const srx = rx - offset;\n const sry = ry - offset;\n if (srx <= 0 || sry <= 0) return;\n target.beginPath();\n target.ellipse(rx, ry, srx, sry);\n target.stroke();\n }\n};\n\nconst drawPolygon: ElementRenderer<PolygonElement> = (shape, target) => {\n if (shape.points.length < 2) return;\n const { fill, stroke } = applyStyle(shape.style, target);\n if (!fill && !stroke) return;\n if (fill) {\n target.beginPath();\n polygonPath(target, shape.points);\n target.fill();\n }\n if (stroke) {\n const offset = strokeAlignOffset(shape.style);\n const pts = offset !== 0 ? polygonMath.offsetClosedPath(shape.points, offset) : shape.points;\n target.beginPath();\n polygonPath(target, pts);\n target.stroke();\n }\n};\n\n/** Emit a closed polygon outline as `moveTo` + `lineTo`s + `closePath`. */\nconst polygonPath = (target: RenderTarget, pts: readonly Vec2[]): void => {\n const first = pts[0];\n if (first === undefined) return;\n target.moveTo(first.x, first.y);\n for (let i = 1; i < pts.length; i++) {\n const p = pts[i];\n if (p === undefined) continue;\n target.lineTo(p.x, p.y);\n }\n target.closePath();\n};\n\nconst drawPath: ElementRenderer<PathElement> = (shape, target) => {\n if (shape.commands.length === 0) return;\n const { fill, stroke } = applyStyle(shape.style, target);\n if (!fill && !stroke) return;\n target.beginPath();\n for (const cmd of shape.commands) {\n switch (cmd.kind) {\n case \"M\":\n target.moveTo(cmd.to.x, cmd.to.y);\n break;\n case \"L\":\n target.lineTo(cmd.to.x, cmd.to.y);\n break;\n case \"Q\":\n target.quadraticCurveTo(cmd.control.x, cmd.control.y, cmd.to.x, cmd.to.y);\n break;\n case \"C\":\n target.bezierCurveTo(\n cmd.control1.x,\n cmd.control1.y,\n cmd.control2.x,\n cmd.control2.y,\n cmd.to.x,\n cmd.to.y,\n );\n break;\n case \"Z\":\n target.closePath();\n break;\n }\n }\n if (fill) target.fill();\n if (stroke) target.stroke();\n};\n\n/**\n * Rich-text path: draw a text element whose glyphs carry per-run styling\n * (bold / italic / colour / decoration). Each visual line is split into\n * style segments (via `sliceRuns` against the line's source offsets) and each\n * segment is painted with its own font + fill at an accumulated x offset \u2014\n * so it renders identically on Canvas2D, WebGL2 and SVG through the shared\n * `RenderTarget`. Line breaking uses the ELEMENT's base font metrics (matches\n * the plain-text path); per-run weight only affects glyph paint + segment\n * widths, an acceptable etap-1 approximation for wrapping.\n */\n/**\n * Sticky note: a rounded card filled with `style.fill` (default sticky\n * yellow); the text itself is the shared embedded label, drawn by the\n * scene renderer's label pass. The author name renders along the bottom\n * edge when `showAuthor` is set.\n */\n/**\n * The zoom at which `shape`'s shorter side spans exactly\n * `STICKY_REACTION_MIN_SCREEN_PX` on screen \u2014 the reaction chrome's\n * visibility threshold for that sticky.\n */\nconst stickyReactionMinZoom = (shape: StickyElement): number =>\n STICKY_REACTION_MIN_SCREEN_PX / Math.max(1, Math.min(shape.width, shape.height));\n\n/**\n * Whether `shape` is large enough on screen at `zoom` for its reaction\n * chrome (pills and the \"+\" button) to be drawn and clickable.\n */\nexport const stickyReactionChromeVisible = (shape: StickyElement, zoom: number): boolean =>\n Math.min(shape.width, shape.height) * zoom >= STICKY_REACTION_MIN_SCREEN_PX;\n\n/**\n * The pill scale factor keeping reaction chrome at a CONSTANT on-screen\n * size: world size = base / zoom, with the divisor clamped at the sticky's\n * visibility threshold so a zoomed-out board gets shrinking (not\n * card-swallowing) pills.\n */\nconst stickyReactionScale = (shape: StickyElement, zoom: number): number =>\n 1 / Math.max(zoom > 0 ? zoom : 1, stickyReactionMinZoom(shape));\n\ninterface StickyReactionRect {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n}\n\ninterface StickyReactionPill extends StickyReactionRect {\n readonly glyph: string;\n readonly label: string;\n}\n\n/**\n * Layout of a sticky's reaction pills + the \"+\" add button under its\n * bottom edge, in the shape's LOCAL space. Pills flow left-to-right and\n * WRAP onto new rows (inline-block style) when they'd overrun the card\n * width \u2014 every reaction is always laid out, none are dropped. ONE\n * implementation shared by the canvas renderer and the DOM click-zone\n * overlay so the hit areas always match the painted pills.\n *\n * `measure` must be bound to `STICKY_REACTION_FONT_SIZE`-sized system-ui\n * text (base px); `zoom` is the current view scale \u2014 pill sizes are\n * divided by it so they stay visually constant (clamped at the sticky's\n * {@link stickyReactionChromeVisible} threshold).\n */\nexport const stickyReactionLayout = (\n shape: StickyElement,\n measure: (s: string) => number,\n zoom = 1,\n): { readonly pills: readonly StickyReactionPill[]; readonly add: StickyReactionRect } => {\n const k = stickyReactionScale(shape, zoom);\n const gap = STICKY_REACTION_GAP * k;\n const h = STICKY_REACTION_HEIGHT * k;\n const x0 = STICKY_CORNER_RADIUS + 2;\n const pills: StickyReactionPill[] = [];\n let x = x0;\n let y = shape.height + gap;\n for (const reaction of shape.reactions ?? []) {\n const users = (reaction as { users?: readonly string[]; count?: number }).users;\n const count = users?.length ?? (reaction as { count?: number }).count ?? 0;\n const label = `${reaction.glyph} ${String(count)}`;\n const width = (measure(label) + 2 * STICKY_REACTION_PAD_X) * k;\n if (x > x0 && x + width > shape.width) {\n x = x0;\n y += h + gap;\n }\n pills.push({ glyph: reaction.glyph, label, x, y, width, height: h });\n x += width + gap;\n }\n if (x > x0 && x + h > shape.width) {\n x = x0;\n y += h + gap;\n }\n return { pills, add: { x, y, width: h, height: h } };\n};\n\n/** Pills half of {@link stickyReactionLayout} (click-zone overlay helper). */\nexport const stickyReactionPillRects = (\n shape: StickyElement,\n measure: (s: string) => number,\n zoom = 1,\n): readonly StickyReactionPill[] => stickyReactionLayout(shape, measure, zoom).pills;\n\n/** \"+\" button half of {@link stickyReactionLayout} (click-zone overlay helper). */\nexport const stickyReactionAddRect = (\n shape: StickyElement,\n measure: (s: string) => number,\n zoom = 1,\n): StickyReactionRect => stickyReactionLayout(shape, measure, zoom).add;\n\nconst drawSticky: ElementRenderer<StickyElement> = (shape, target, ctx) => {\n const fill = shape.style.fill ?? STICKY_DEFAULT_FILL;\n if (shape.style.opacity !== undefined) target.setOpacity(shape.style.opacity);\n const w = shape.width;\n const h = shape.height;\n const r = STICKY_CORNER_RADIUS;\n\n // Soft drop shadow under the card, offset downwards.\n target.setFill(STICKY_SHADOW_COLOR);\n target.beginPath();\n buildRoundedRectPath(target, 1, STICKY_SHADOW_OFFSET_Y, w - 2, h - 2, r);\n target.fill();\n\n // The card body \u2014 a plain rounded sheet over its drop shadow.\n target.setFill(fill);\n target.beginPath();\n buildRoundedRectPath(target, 0, 0, w, h, r);\n target.fill();\n\n // Tag pills along the bottom edge.\n if (ctx?.content?.stickyTags !== false && shape.tags !== undefined && shape.tags.length > 0) {\n target.setFont(\"system-ui, sans-serif\", STICKY_TAG_FONT_SIZE, {});\n target.setTextAlign(\"left\");\n target.setTextBaseline(\"top\");\n let x = r + 2;\n const y = h - STICKY_TAG_HEIGHT - 3;\n for (const tag of shape.tags) {\n const tw = target.measureText(tag).width + 2 * STICKY_TAG_PAD_X;\n if (x + tw > w - r) break;\n target.setFill(STICKY_TAG_BG);\n target.beginPath();\n buildRoundedRectPath(target, x, y, tw, STICKY_TAG_HEIGHT, STICKY_TAG_HEIGHT / 2);\n target.fill();\n target.setFill(STICKY_TAG_COLOR);\n target.fillText(\n tag,\n x + STICKY_TAG_PAD_X,\n y + (STICKY_TAG_HEIGHT - STICKY_TAG_FONT_SIZE) / 2,\n );\n x += tw + STICKY_TAG_GAP;\n }\n }\n\n if (\n ctx?.content?.stickyAuthor !== false &&\n shape.showAuthor === true &&\n shape.authorName !== undefined &&\n shape.authorName !== \"\"\n ) {\n target.setFont(\"system-ui, sans-serif\", STICKY_AUTHOR_FONT_SIZE, {});\n target.setTextAlign(\"left\");\n target.setTextBaseline(\"top\");\n target.setFill(STICKY_AUTHOR_COLOR);\n const authorY =\n shape.tags !== undefined && shape.tags.length > 0\n ? h - STICKY_TAG_HEIGHT - STICKY_AUTHOR_FONT_SIZE - 8\n : h - STICKY_AUTHOR_FONT_SIZE - 4;\n target.fillText(shape.authorName, r + 2, authorY);\n }\n\n // Reaction pills under the bottom-left edge \u2014 canvas is the single\n // visual source (exports included); the DOM overlay only overlays\n // transparent click zones on the same rects.\n const zoom = ctx?.zoom ?? 1;\n const k = stickyReactionScale(shape, zoom);\n // Once the card is small on screen the whole reaction chrome is hidden \u2014\n // constant on-screen pills would swallow it.\n const chromeVisible = stickyReactionChromeVisible(shape, zoom);\n const drawReactions = chromeVisible && ctx?.content?.stickyReactions !== false;\n // \"+\" add-reaction button \u2014 UI chrome drawn on the canvas so it tracks\n // the shape 1:1 while dragging. Shown only for the HOVERED sticky in\n // interactive renders; exports and read-only views switch it off.\n const drawAdd =\n chromeVisible && ctx?.content?.stickyAddButton !== false && ctx?.hoveredElement === shape.id;\n if (drawReactions || drawAdd) {\n // Measure at the BASE font size (the layout contract). Text is also\n // DRAWN at the base size inside a `scale(k)` transform: a fractional\n // per-frame font size would defeat the backend's string-bitmap cache\n // during smooth zoom (a fresh rasterisation per pill per frame).\n target.setFont(\"system-ui, sans-serif\", STICKY_REACTION_FONT_SIZE, {});\n target.setTextAlign(\"left\");\n target.setTextBaseline(\"top\");\n const layout = stickyReactionLayout(shape, (t) => target.measureText(t).width, zoom);\n if (drawReactions) {\n for (const pill of layout.pills) {\n target.setFill(STICKY_REACTION_BG);\n target.beginPath();\n buildRoundedRectPath(target, pill.x, pill.y, pill.width, pill.height, pill.height / 2);\n target.fill();\n target.setFill(STICKY_REACTION_COLOR);\n target.save();\n target.translate(pill.x, pill.y);\n target.scale(k, k);\n target.fillText(\n pill.label,\n STICKY_REACTION_PAD_X,\n (STICKY_REACTION_HEIGHT - STICKY_REACTION_FONT_SIZE) / 2,\n );\n target.restore();\n }\n }\n if (drawAdd) {\n const add = layout.add;\n target.setFill(STICKY_REACTION_BG);\n target.beginPath();\n buildRoundedRectPath(target, add.x, add.y, add.width, add.height, add.height / 2);\n target.fill();\n // Vector \"+\" cross as two filled bars \u2014 crisper than a glyph at any\n // zoom, and immune to per-backend multi-subpath stroke quirks.\n const cx = add.x + add.width / 2;\n const cy = add.y + add.height / 2;\n const arm = add.height * 0.22;\n const bar = 1.4 * k;\n target.setFill(STICKY_REACTION_ADD_COLOR);\n target.beginPath();\n target.rect(cx - arm, cy - bar / 2, arm * 2, bar);\n target.fill();\n target.beginPath();\n target.rect(cx - bar / 2, cy - arm, bar, arm * 2);\n target.fill();\n }\n }\n};\n\n/** Emoji element: one glyph filling the element's square via the text path. */\nconst drawEmoji: ElementRenderer<EmojiElement> = (shape, target) => {\n if (shape.style.opacity !== undefined) target.setOpacity(shape.style.opacity);\n target.setFont(\"system-ui, sans-serif\", shape.size, {});\n target.setTextAlign(\"left\");\n target.setTextBaseline(\"top\");\n target.setFill(shape.style.fill ?? \"#000\");\n target.fillText(shape.glyph, 0, 0);\n};\n\n/**\n * Geometry of an embedded shape label: the synthetic text element the\n * text renderer can draw, plus the local-space offset where it starts.\n * Shared by the renderer and the inline-edit caret path (state) so the\n * glyphs and the caret can't drift apart.\n */\n/**\n * Auto-fit font sizing for `ShapeLabel.autoFit` (sticky notes): the\n * largest size in [`LABEL_AUTOFIT_MIN_PX`, `LABEL_AUTOFIT_MAX_PX`]\n * whose wrapped layout fits the padded shape body, found by binary\n * search over `layoutText`. Memoized \u2014 the measure callback varies by\n * backend, so the cache key folds in a coarse measure fingerprint.\n */\nconst autoFitCache = new Map<string, number>();\n\nconst autoFitFontSize = (\n text: string,\n boxW: number,\n boxH: number,\n measure: (s: string) => number,\n baseSize: number,\n paragraphs: TextElement[\"paragraphs\"],\n): number => {\n // The measure callback is bound to `baseSize`; normalise so the cache\n // key (and the search) are stable across backends and base sizes.\n const fingerprint = Math.round((measure(\"Mg \u0432\u043E\u0434\u043E\u0451\u043C\") / baseSize) * 1000);\n const key = `${text}|${String(Math.round(boxW))}x${String(Math.round(boxH))}|${String(fingerprint)}`;\n const cached = autoFitCache.get(key);\n if (cached !== undefined) return cached;\n\n const fits = (size: number): boolean => {\n const pad = LABEL_PADDING_EM * size;\n const maxWidth = boxW - 2 * pad;\n if (maxWidth < size) return false;\n // Rescale the base-size measurement to the candidate size so the\n // wrap decisions inside layoutText are internally consistent.\n const scaled = (t: string): number => (measure(t) * size) / baseSize;\n const layout = layoutText(text, scaled, {\n fontSize: size,\n maxWidth,\n ...(paragraphs !== undefined ? { paragraphs } : {}),\n });\n return layout.lines.length * layout.lineHeight <= boxH - 2 * pad;\n };\n let lo = LABEL_AUTOFIT_MIN_PX;\n let hi = LABEL_AUTOFIT_MAX_PX;\n while (hi - lo > 1) {\n const mid = Math.floor((lo + hi) / 2);\n if (fits(mid)) lo = mid;\n else hi = mid;\n }\n if (autoFitCache.size > 512) autoFitCache.clear();\n autoFitCache.set(key, lo);\n return lo;\n};\n\nexport const shapeLabelLayout = (\n shape: ElementBase,\n measure: (s: string) => number,\n): {\n readonly synthetic: TextElement;\n readonly offsetX: number;\n readonly offsetY: number;\n /** Visible window in layout-space Y (lines outside are not painted). */\n readonly windowTop: number;\n readonly windowBottom: number;\n} | null => {\n const label = shape.label;\n if (label === undefined) return null;\n const bounds = getElementLocalBounds(shape);\n const fontSize =\n label.autoFit === true && label.text !== \"\"\n ? autoFitFontSize(\n label.text,\n bounds.width,\n bounds.height,\n measure,\n label.fontSize,\n label.paragraphs,\n )\n : label.fontSize;\n const pad = LABEL_PADDING_EM * fontSize;\n const maxWidth = Math.max(fontSize, bounds.width - 2 * pad);\n // `measure` arrives bound to the label's BASE font size; when auto-fit\n // picked a different size, rescale so wrap decisions match the glyphs\n // that will actually be drawn.\n const scaledMeasure =\n fontSize === label.fontSize\n ? measure\n : (t: string): number => (measure(t) * fontSize) / label.fontSize;\n // Block-level vertical alignment is applied via `offsetY` below; the\n // synthetic's glyph baseline stays \"top\" so drawn glyphs, the caret and\n // selection rects all share top-anchored line coordinates.\n const valign = label.style?.textBaseline ?? \"middle\";\n const style: TextStyle = {\n textAlign: \"center\",\n ...label.style,\n textBaseline: \"top\",\n };\n const layout = layoutText(label.text, scaledMeasure, {\n fontSize,\n maxWidth,\n ...(label.paragraphs !== undefined ? { paragraphs: label.paragraphs } : {}),\n });\n // Text never escapes the shape body: only the lines inside the padded\n // window are painted (the flat text keeps the rest for editing). While\n // the inline editor is open the transient `metadata.labelScrollLines`\n // scrolls that window so the caret stays visible.\n const innerHeight = Math.max(0, bounds.height - 2 * pad);\n const clipLines = Math.max(0, Math.floor(innerHeight / layout.lineHeight));\n const rawScroll = shape.metadata?.labelScrollLines;\n const maxScroll = Math.max(0, layout.lines.length - clipLines);\n const scroll = Math.max(\n 0,\n Math.min(maxScroll, typeof rawScroll === \"number\" ? Math.floor(rawScroll) : 0),\n );\n const synthetic = {\n id: shape.id,\n layerId: shape.layerId,\n type: \"text\",\n position: { x: 0, y: 0 },\n rotation: 0,\n scale: { x: 1, y: 1 },\n order: shape.order,\n style,\n text: label.text,\n fontFamily: label.fontFamily,\n fontSize,\n maxWidth,\n clipStart: scroll,\n clipLines,\n ...(label.runs !== undefined ? { runs: label.runs } : {}),\n ...(label.paragraphs !== undefined ? { paragraphs: label.paragraphs } : {}),\n } as TextElement;\n const textH = Math.min(layout.lines.length - scroll, clipLines) * layout.lineHeight;\n const windowAnchor =\n valign === \"top\"\n ? bounds.y + pad\n : valign === \"bottom\"\n ? bounds.y + bounds.height - textH - pad\n : bounds.y + Math.max(pad, (bounds.height - textH) / 2);\n // Lines keep their absolute layout Y (line \u00D7 lineHeight); shifting the\n // whole block up by the scroll puts the visible window at the anchor.\n const offsetY = windowAnchor - scroll * layout.lineHeight;\n return {\n synthetic,\n offsetX: bounds.x + pad,\n offsetY,\n windowTop: scroll * layout.lineHeight,\n windowBottom: (scroll + clipLines) * layout.lineHeight,\n };\n};\n\n/**\n * Draw a shape's embedded label inside its local bounds. Reuses the text\n * renderer wholesale (wrap, runs, lists, highlight); vertical alignment\n * places the whole block, `textAlign` centres lines within the padded\n * width. Called by the scene renderer after the shape body.\n */\nexport const drawShapeLabel = (\n shape: ElementBase,\n target: RenderTarget,\n /** Readable-text LOD: skip the label when its resolved font size is below the floor on screen. */\n lod?: { readonly zoom: number; readonly lod: LodOptions },\n): void => {\n const label = shape.label;\n if (label === undefined || label.text === \"\") return;\n // Fast path on the base size: an auto-fit label can only grow from it.\n if (lod && !label.autoFit && isTextBelowLod(label.fontSize, lod.zoom, lod.lod)) return;\n // Measure with the label's base font \u2014 same metrics drawText wraps with.\n const weight = label.style?.fontWeight;\n const fontStyle = label.style?.fontStyle;\n target.setFont(label.fontFamily, label.fontSize, {\n ...(weight ? { weight } : {}),\n ...(fontStyle ? { style: fontStyle } : {}),\n });\n const placed = shapeLabelLayout(shape, (s) => target.measureText(s).width);\n if (!placed) return;\n if (lod && isTextBelowLod(placed.synthetic.fontSize, lod.zoom, lod.lod)) return;\n target.save();\n target.translate(placed.offsetX, placed.offsetY);\n drawText(placed.synthetic, target);\n target.restore();\n};\n\n/**\n * Internal draw hint carried by label synthetics: paint at most this many\n * visual lines so the text never escapes the shape body. Never serialized.\n */\nconst clipWindowOf = (\n shape: TextElement,\n): { readonly start: number; readonly end: number } | undefined => {\n const hint = shape as { readonly clipStart?: number; readonly clipLines?: number };\n if (hint.clipLines === undefined) return undefined;\n const start = hint.clipStart ?? 0;\n return { start, end: start + hint.clipLines };\n};\n\n/**\n * Draw the derived list markers (\"\u2022\" / \"1.\") for every paragraph's first\n * visual line, right-aligned into the indent slot the layout reserved.\n * Uses the element's base font + fill; leaves the fill set to `color`.\n */\nconst drawListMarkersForLayout = (\n shape: TextElement,\n layout: EditableTextLayout,\n target: RenderTarget,\n color: string,\n): void => {\n if (shape.paragraphs === undefined) return;\n const align = shape.style.textAlign ?? \"left\";\n const markers = listMarkers(shape.paragraphs, paragraphCount(shape.text));\n const gap = LIST_MARKER_GAP_EM * shape.fontSize;\n target.setFill(color);\n const markerClip = clipWindowOf(shape);\n layout.lines.forEach((line, i) => {\n if (markerClip !== undefined && (i < markerClip.start || i >= markerClip.end)) return;\n if (!line.paraFirst) return;\n const marker = markers[line.para];\n if (marker == null) return;\n const w = target.measureText(marker).width;\n const left = lineLeft(line, layout.blockWidth, align);\n target.fillText(marker, left - gap - w, i * layout.lineHeight);\n });\n};\n\nconst drawStyledText = (shape: TextElement, target: RenderTarget): void => {\n const align = shape.style.textAlign ?? \"left\";\n const fontSize = shape.fontSize;\n target.setTextAlign(\"left\");\n target.setTextBaseline(shape.style.textBaseline ?? \"top\");\n\n // Apply the resolved font for a run: run overlay wins, element style is\n // the fallback for any field the run omits.\n const setSegFont = (st: TextRun[\"style\"]): void => {\n const weight = st?.fontWeight ?? shape.style.fontWeight;\n const style = st?.fontStyle ?? shape.style.fontStyle;\n target.setFont(shape.fontFamily, fontSize, {\n ...(weight ? { weight } : {}),\n ...(style ? { style } : {}),\n });\n };\n\n // Base-font line breaking \u2014 same metrics the plain path wraps with.\n setSegFont(undefined);\n const layout = layoutText(shape.text, (s) => target.measureText(s).width, {\n fontSize,\n ...(shape.maxWidth !== undefined ? { maxWidth: shape.maxWidth } : {}),\n ...(shape.paragraphs !== undefined ? { paragraphs: shape.paragraphs } : {}),\n });\n\n interface Seg {\n readonly text: string;\n readonly style: TextStyle | undefined;\n readonly width: number;\n }\n const perLine = layout.lines.map((line) => {\n const segs: Seg[] = sliceRuns(shape, line.start, line.end).map((r) => {\n setSegFont(r.style);\n return { text: r.text, style: r.style, width: target.measureText(r.text).width };\n });\n const total = segs.reduce((a, s) => a + s.width, 0);\n return { segs, total };\n });\n\n // Alignment box: fixed budget, or the widest STYLED line so bold text\n // stays self-consistently aligned.\n const blockWidth =\n shape.maxWidth ??\n perLine.reduce((m, l, i) => Math.max(m, l.total + req(layout.lines[i]).indentX), 0);\n const thickness = Math.max(1, fontSize * TEXT_DECORATION_THICKNESS);\n\n const styledClip = clipWindowOf(shape);\n perLine.forEach((line, i) => {\n if (styledClip !== undefined && (i < styledClip.start || i >= styledClip.end)) return;\n const top = i * layout.lineHeight;\n const indentX = req(layout.lines[i]).indentX;\n let x =\n indentX +\n (align === \"center\"\n ? (blockWidth - indentX) / 2 - line.total / 2\n : align === \"right\"\n ? blockWidth - indentX - line.total\n : 0);\n for (const seg of line.segs) {\n const color = seg.style?.fill ?? shape.style.fill ?? \"#000\";\n const opacity = seg.style?.opacity ?? shape.style.opacity;\n setSegFont(seg.style);\n if (opacity !== undefined) target.setOpacity(opacity);\n // Highlight first \u2014 a full line-height rect under the glyphs, so the\n // text paints on top of its own marker stripe.\n const highlight = seg.style?.highlight ?? shape.style.highlight;\n if (seg.width > 0 && highlight !== undefined && highlight !== \"transparent\") {\n target.setFill(highlight);\n target.beginPath();\n target.rect(x, top, seg.width, layout.lineHeight);\n target.fill();\n }\n target.setFill(color);\n target.fillText(seg.text, x, top);\n\n const deco = seg.style?.textDecoration ?? shape.style.textDecoration;\n if (seg.width > 0 && (deco?.underline || deco?.strikethrough)) {\n if (deco.underline) {\n target.beginPath();\n target.rect(x, top + fontSize * TEXT_UNDERLINE_OFFSET, seg.width, thickness);\n target.fill();\n }\n if (deco.strikethrough) {\n target.beginPath();\n target.rect(\n x,\n top + fontSize * TEXT_STRIKETHROUGH_OFFSET - thickness / 2,\n seg.width,\n thickness,\n );\n target.fill();\n }\n }\n x += seg.width;\n }\n });\n // Markers use the element's base font/colour, after the segments so the\n // font state is deterministic.\n setSegFont(undefined);\n drawListMarkersForLayout(shape, layout, target, shape.style.fill ?? \"#000\");\n};\n\nconst drawText: ElementRenderer<TextElement> = (shape, target, ctx) => {\n // Empty text while writing: draw the element's placeholder prompt in the\n // neutral grey, with the element's own font / alignment so the caret and\n // the prompt line up. Interactive rendering only (`ctx.textPlaceholders`).\n if (shape.text === \"\" && ctx?.textPlaceholders === true) {\n const { runs: _runs, ...plain } = shape;\n drawText(\n {\n ...plain,\n text: pickTextPlaceholder(shape.id),\n style: { ...shape.style, fill: TEXT_PLACEHOLDER_COLOR },\n },\n target,\n );\n return;\n }\n // Rich text (styled runs) takes a dedicated path; plain text keeps the\n // original single-style path byte-for-byte (golden-SVG compatible).\n if (shape.runs !== undefined && shape.runs.length > 0) {\n drawStyledText(shape, target);\n return;\n }\n const align = shape.style.textAlign ?? \"left\";\n const weight = shape.style.fontWeight;\n const fontStyle = shape.style.fontStyle;\n target.setFont(shape.fontFamily, shape.fontSize, {\n ...(weight ? { weight } : {}),\n ...(fontStyle ? { style: fontStyle } : {}),\n });\n // Lines are positioned manually (per-line x below) so the caret\n // geometry computed from the same `layoutText` lines up exactly, so\n // the target always draws left-anchored.\n target.setTextAlign(\"left\");\n target.setTextBaseline(shape.style.textBaseline ?? \"top\");\n\n // Color: use fill if specified, otherwise default to black.\n const color = shape.style.fill ?? \"#000\";\n target.setFill(color);\n if (shape.style.opacity !== undefined) target.setOpacity(shape.style.opacity);\n\n // Resolve per-line geometry once (x = align offset, top = i \u00D7\n // lineHeight). Single-line text without list attrs skips the wrap engine.\n const fontSize = shape.fontSize;\n let lines: { text: string; x: number; width: number; top: number }[];\n if (\n shape.maxWidth === undefined &&\n !shape.text.includes(\"\\n\") &&\n shape.paragraphs === undefined\n ) {\n lines = [{ text: shape.text, x: 0, width: target.measureText(shape.text).width, top: 0 }];\n } else {\n // Measure with the target's own `measureText` so wrapping matches\n // exactly what this backend draws.\n const measure = (s: string) => target.measureText(s).width;\n const layout = layoutText(shape.text, measure, {\n fontSize,\n ...(shape.maxWidth !== undefined ? { maxWidth: shape.maxWidth } : {}),\n ...(shape.paragraphs !== undefined ? { paragraphs: shape.paragraphs } : {}),\n });\n lines = layout.lines.map((line, i) => ({\n text: line.text,\n x: lineLeft(line, layout.blockWidth, align),\n width: line.width,\n top: i * layout.lineHeight,\n }));\n const clip = clipWindowOf(shape);\n if (clip !== undefined) lines = lines.filter((_, i) => i >= clip.start && i < clip.end);\n drawListMarkersForLayout(shape, layout, target, color);\n }\n\n // Highlight stripes under the glyphs (marker-style), then the text on top.\n const highlight = shape.style.highlight;\n if (highlight !== undefined && highlight !== \"transparent\") {\n const lineHeight = fontSize * DEFAULT_LINE_HEIGHT_FACTOR;\n target.setFill(highlight);\n for (const l of lines) {\n if (l.width <= 0) continue;\n target.beginPath();\n target.rect(l.x, l.top, l.width, lineHeight);\n target.fill();\n }\n target.setFill(color);\n }\n for (const l of lines) target.fillText(l.text, l.x, l.top);\n\n // Underline / strikethrough \u2014 thin filled rects per line, same on\n // Canvas2D and WebGL2 (uses the current text fill colour).\n const deco = shape.style.textDecoration;\n if (deco?.underline || deco?.strikethrough) {\n const thickness = Math.max(1, fontSize * TEXT_DECORATION_THICKNESS);\n for (const l of lines) {\n if (l.width <= 0) continue;\n if (deco.underline) {\n target.beginPath();\n target.rect(l.x, l.top + fontSize * TEXT_UNDERLINE_OFFSET, l.width, thickness);\n target.fill();\n }\n if (deco.strikethrough) {\n target.beginPath();\n target.rect(\n l.x,\n l.top + fontSize * TEXT_STRIKETHROUGH_OFFSET - thickness / 2,\n l.width,\n thickness,\n );\n target.fill();\n }\n }\n }\n};\n\n/**\n * Variable-width brush stroke. Each segment between two `BrushPoint`s\n * is drawn as a quad (two triangles) \u2014 its width interpolates from\n * `p.width` at the head to `q.width` at the tail. Renders\n * pressure-sensitive ink that gets thicker / thinner along the path\n * without needing per-segment `setStrokeWidth` calls (which most 2D\n * APIs treat as a single line width).\n */\nconst drawBrush: ElementRenderer<BrushElement> = (shape, target) => {\n const pts = shape.points;\n if (pts.length === 0) return;\n // Honour the stroke's opacity (drawBrush paints fills directly, so it can't\n // rely on the shared `applyStyle` the other renderers use). Set once up front\n // so both the enclosed-area fill and the body get it; the scene renderer resets\n // opacity to 1 between shapes.\n if (shape.style.opacity !== undefined) target.setOpacity(shape.style.opacity);\n // Closed stroke with a fill colour: paint the enclosed area FIRST (under the\n // stroke body) as a polygon through the centreline points. Needs \u22653 points to\n // enclose an area. Open strokes skip this entirely and are unchanged.\n if (shape.closed === true && shape.style.fill !== undefined && pts.length >= 3) {\n target.setFill(shape.style.fill);\n target.setStroke(null);\n target.beginPath();\n const start = req(pts[0]);\n target.moveTo(start.x, start.y);\n for (let i = 1; i < pts.length; i++) {\n const p = req(pts[i]);\n target.lineTo(p.x, p.y);\n }\n target.closePath();\n target.fill();\n }\n // The variable-width stroke body is painted with the shared brush-body colour\n // (the same resolution the live preview uses \u2014 see `brushBodyColor`).\n const paint = brushBodyColor(shape.style);\n target.setFill(paint);\n target.setStroke(null);\n // Single dot for one-point strokes \u2014 degenerate quad would be invisible.\n if (pts.length === 1) {\n const p = req(pts[0]);\n target.beginPath();\n target.ellipse(p.x, p.y, p.width, p.width);\n target.fill();\n return;\n }\n // Body as ONE closed outline polygon, filled once. Per-segment quads + joint\n // discs (the old approach) overlap, so at `opacity < 1` the joins double-blend\n // into dark blotches; a single fill paints every pixel exactly once.\n const outline = brushOutline(pts);\n if (outline.length >= 3) {\n target.beginPath();\n const first = req(outline[0]);\n target.moveTo(first.x, first.y);\n for (let i = 1; i < outline.length; i++) {\n const p = req(outline[i]);\n target.lineTo(p.x, p.y);\n }\n target.closePath();\n target.fill();\n }\n};\n\nconst drawImage: ElementRenderer<ImageElement> = (shape, target, ctx) => {\n // Priority: for an animated source prefer the per-frame image the\n // registered adapter returns; otherwise a preloaded handle in\n // `metadata.image`; otherwise the static `src` fallback.\n // `resolveImageSource` returns `null` while an async decode is still\n // in flight, which the backend's drawImage guard skips.\n // Sample at the per-instance clock when the caller threaded one via the\n // render context; `undefined` defers to `resolveImageSource`'s process-global\n // fallback clock (headless / preview paths).\n const t = ctx?.clock?.(shape);\n const handle = shape.animationKind\n ? resolveImageSource(shape, t)\n : (shape.metadata?.image ?? resolveImageSource(shape, t));\n // A non-drawable handle with a `fileId` is a TRANSIENT state, not a\n // problem: the first paint after a scene restore runs before async\n // rehydration re-attaches a live handle from `Scene.files`. Skip the\n // frame silently \u2014 rehydration repaints when it lands, and reports its\n // own failure if the bytes are missing or won't decode. Only handles\n // with no rehydration source fall through to the backend, which warns\n // (the image really will stay blank).\n if (!isDrawableImageSource(handle)) {\n if (handle == null || shape.fileId) return;\n }\n // `dynamic` \u2192 backends that cache the upload (WebGL2) re-upload the\n // current frame. GIF / video sources flag `metadata.animated`, and\n // any adapter-driven source is dynamic by definition.\n const dynamic = shape.metadata?.animated === true || shape.animationKind !== undefined;\n const mask = shape.mask;\n if (mask) {\n target.save();\n target.beginPath();\n buildImageMaskPath(target, mask, shape.width, shape.height);\n target.clip();\n }\n target.drawImage(handle, 0, 0, shape.width, shape.height, dynamic, shape.crop, shape.alt);\n if (mask) target.restore();\n};\n\n/**\n * Build an {@link ImageMask}'s path in the shape's LOCAL space\n * (normalised mask coordinates \u00D7 the element box). Exported so overlays\n * (crop/mask preview) can trace the same outline the renderer clips by.\n */\nexport const buildImageMaskPath = (\n target: RenderTarget,\n mask: ImageMask,\n width: number,\n height: number,\n): void => {\n switch (mask.kind) {\n case \"ellipse\":\n target.ellipse(width / 2, height / 2, width / 2, height / 2);\n return;\n case \"round-rect\": {\n const r = Math.max(0, Math.min(0.5, mask.radius)) * Math.min(width, height);\n buildRoundedRectPath(target, 0, 0, width, height, r);\n return;\n }\n case \"polygon\": {\n const pts = mask.points;\n if (pts.length < 3) return;\n const first = req(pts[0]);\n target.moveTo(first.x * width, first.y * height);\n for (let i = 1; i < pts.length; i++) {\n const p = req(pts[i]);\n target.lineTo(p.x * width, p.y * height);\n }\n target.closePath();\n return;\n }\n }\n};\n\n/**\n * Registers renderers for every `BuiltinElement` type.\n */\nexport const installBuiltinRenderers = (): void => {\n registerElementRenderer<RectangleElement>(\"rectangle\", drawRectangle);\n registerElementRenderer<EllipseElement>(\"ellipse\", drawEllipse);\n registerElementRenderer<PolygonElement>(\"polygon\", drawPolygon);\n registerElementRenderer<PathElement>(\"path\", drawPath);\n registerElementRenderer<TextElement>(\"text\", drawText);\n registerElementRenderer<ImageElement>(\"image\", drawImage);\n // Group shapes are invisible containers \u2014 the shape itself paints nothing.\n registerElementRenderer<GroupElement>(\"group\", () => {\n /* intentional no-op: group shapes are invisible containers and paint nothing */\n });\n registerElementRenderer<FrameElement>(\"frame\", drawFrame);\n // The frame paints its header strip ABOVE the rectangle, so its dirty\n // region must extend up by the header height \u2014 otherwise deleting a\n // frame leaves the header behind.\n registerRenderOverflow(\"frame\", () => ({ top: FRAME_HEADER_HEIGHT }));\n registerElementRenderer<BlockArrowElement>(\"block-arrow\", drawBlockArrow);\n registerElementRenderer<BrushElement>(\"brush\", drawBrush);\n registerElementRenderer<StickyElement>(\"sticky\", drawSticky);\n // The sticky's drop shadow paints below its box \u2014 extend the dirty\n // region so moving/deleting it doesn't leave the shadow behind.\n registerRenderOverflow(\"sticky\", (shape) => {\n // Worst-case invalidation bound for the reaction rows: every pill on\n // its own row (+ the \"+\" button row), at the largest world size the\n // visibility clamp allows (1 / the sticky's threshold zoom). Overflow\n // providers have no zoom access, so this over-approximates \u2014 costs\n // only redraw area, never leaves ghosts.\n const s = shape as StickyElement;\n const n = (s.reactions?.length ?? 0) + 1;\n const kMax = 1 / stickyReactionMinZoom(s);\n return {\n bottom:\n STICKY_SHADOW_OFFSET_Y + (STICKY_REACTION_GAP + STICKY_REACTION_HEIGHT) * kMax * n + 2,\n right: (STICKY_REACTION_GAP + STICKY_REACTION_HEIGHT) * kMax + 2,\n };\n });\n registerElementRenderer<EmojiElement>(\"emoji\", drawEmoji);\n};\n\n/**\n * Block-arrow silhouette: a rectangle body whose tip is replaced\n * by a triangle, oriented by `direction`. Path is closed and filled\n * with `style.fill`; stroke applies to the outline.\n *\n * right \u2192 \u250C\u2500\u2500\u2500\u2500\u2510\u25B6\n * \u2502 body \u2502\n * \u2514\u2500\u2500\u2500\u2500\u2518\n *\n * up \u2191 \u25B2\n * \u250C\u2500\u2500\u2510\n * \u2502 \u2502\n * \u2514\u2500\u2500\u2518\n */\nconst drawBlockArrow: ElementRenderer<BlockArrowElement> = (shape, target) => {\n const { fill, stroke } = applyStyle(shape.style, target);\n const direction = shape.direction ?? \"right\";\n const headRatio = Math.max(\n ARROWHEAD_RATIO_MIN,\n Math.min(ARROWHEAD_RATIO_MAX, shape.headRatio ?? ARROWHEAD_HEAD_RATIO),\n );\n const bodyT = Math.max(\n ARROWHEAD_RATIO_MIN,\n Math.min(ARROWHEAD_RATIO_MAX, shape.bodyThickness ?? ARROWHEAD_BODY_THICKNESS),\n );\n const w = shape.width;\n const h = shape.height;\n // Compute the local path for a `right`-pointing arrow inside\n // [0, w] \u00D7 [0, h], then rotate the resulting points if the\n // direction is different. Keeps the drawing primitives in one\n // place.\n const headW = w * headRatio;\n const bodyW = w - headW;\n const bodyHalfH = (h * bodyT) / 2;\n const cy = h / 2;\n let points: readonly [number, number][] = [\n [0, cy - bodyHalfH],\n [bodyW, cy - bodyHalfH],\n [bodyW, 0],\n [w, cy],\n [bodyW, h],\n [bodyW, cy + bodyHalfH],\n [0, cy + bodyHalfH],\n ];\n if (direction !== \"right\") {\n points = points.map(([x, y]) => rotateLocal([x, y], direction, w, h));\n }\n const ptObjs = points.map(([x, y]) => ({ x, y }));\n if (fill) {\n target.beginPath();\n polygonPath(target, ptObjs);\n target.fill();\n }\n if (stroke) {\n const offset = strokeAlignOffset(shape.style);\n const sPts = offset !== 0 ? polygonMath.offsetClosedPath(ptObjs, offset) : ptObjs;\n target.beginPath();\n polygonPath(target, sPts);\n target.stroke();\n }\n};\n\nconst rotateLocal = (\n [x, y]: readonly [number, number],\n direction: \"left\" | \"up\" | \"down\",\n w: number,\n h: number,\n): [number, number] => {\n switch (direction) {\n case \"left\":\n return [w - x, y];\n case \"up\":\n // Rotate 90\u00B0 CCW around the box centre, then translate so the\n // result still fits inside [0, w] \u00D7 [0, h].\n return [y * (w / h), h - x * (h / w)];\n case \"down\":\n return [(h - y) * (w / h), x * (h / w)];\n }\n};\n\nconst FRAME_HEADER_ELLIPSIS = \"\u2026\";\n\n/**\n * Trim `text` with a trailing ellipsis until it fits `maxWidth` (in the\n * font already set on `target`). Returns the full text when it fits, the\n * longest prefix + \"\u2026\" otherwise, or just \"\u2026\" when even one char can't\n * fit. Binary-searches the prefix length to keep `measureText` calls ~log.\n */\nconst ellipsizeToWidth = (text: string, maxWidth: number, target: RenderTarget): string => {\n if (maxWidth <= 0) return \"\";\n if (target.measureText(text).width <= maxWidth) return text;\n let lo = 0;\n let hi = text.length;\n while (lo < hi) {\n const mid = Math.ceil((lo + hi) / 2);\n const w = target.measureText(text.slice(0, mid) + FRAME_HEADER_ELLIPSIS).width;\n if (w <= maxWidth) lo = mid;\n else hi = mid - 1;\n }\n return lo > 0 ? text.slice(0, lo) + FRAME_HEADER_ELLIPSIS : FRAME_HEADER_ELLIPSIS;\n};\n\nconst drawFrame: ElementRenderer<FrameElement> = (shape, target, ctx) => {\n // Body \u2014 solid fill + thin solid outline. Frames sit at the bottom\n // z-order, so the fill backs their members without covering them.\n // Honours an explicit `style.fill`, else default white.\n target.setFill(shape.style.fill ?? FRAME_FILL_COLOR);\n target.setStroke(null);\n target.setDashArray(null);\n target.beginPath();\n target.rect(0, 0, shape.width, shape.height);\n target.fill();\n // Outline on top of the fill \u2014 a 1px SCREEN-constant hairline (doesn't scale\n // with zoom). The renderer draws in local coords where 1 unit = zoom \u00D7 scale\n // device px, so divide to keep the stroke at one device pixel. Falls back to\n // 1 world-px when no zoom context is supplied (preview / export at 1:1).\n const screenScale = (ctx?.zoom ?? 1) * (shape.scale.x || 1);\n target.setFill(null);\n target.setStroke(FRAME_STROKE_COLOR);\n target.setStrokeWidth(1 / (screenScale || 1));\n target.setDashArray(null);\n target.beginPath();\n target.rect(0, 0, shape.width, shape.height);\n target.stroke();\n\n // Header label: the strip hugs the text width but never exceeds the\n // frame's right edge; a name too long for the frame is ellipsised.\n const name = shape.name ?? \"Frame\";\n target.setFont(\"system-ui, sans-serif\", FRAME_HEADER_FONT_SIZE);\n const avail = shape.width - FRAME_HEADER_PADDING_X * 2;\n const fits = target.measureText(name).width <= avail;\n // Fits \u2192 the strip hugs the text. Too long \u2192 ellipsise the text and\n // stretch the strip to the frame's full width (the label runs to the\n // right edge).\n const label = fits ? name : ellipsizeToWidth(name, avail, target);\n const headerWidth = fits\n ? Math.min(target.measureText(name).width + FRAME_HEADER_PADDING_X * 2, shape.width)\n : shape.width;\n\n // Header label background \u2014 stretches to fit the (possibly truncated) text.\n target.setFill(FRAME_HEADER_BG_COLOR);\n target.beginPath();\n target.rect(0, -FRAME_HEADER_HEIGHT, headerWidth, FRAME_HEADER_HEIGHT);\n target.fill();\n\n // Header label text.\n target.setFill(FRAME_HEADER_TEXT_COLOR);\n target.setTextBaseline(\"middle\");\n target.setTextAlign(\"left\");\n target.fillText(label, FRAME_HEADER_PADDING_X, -FRAME_HEADER_HEIGHT / 2);\n};\n", "import type { RenderTarget } from \"./render-target.js\";\n\n/**\n * Wrap a {@link RenderTarget} so every `setOpacity(a)` becomes\n * `setOpacity(a * factor)`, leaving all other calls untouched.\n *\n * Isolation / eraser dim works by lowering the alpha for a subset of shapes.\n * The scene renderer sets that alpha *before* the shape renderer runs, but a\n * renderer that applies the shape's own `style.opacity` calls `setOpacity`\n * absolutely \u2014 overwriting the dim, so a shape carrying an explicit opacity\n * would never dim (the eraser's \"about to delete\" fade silently vanished).\n * Routing the renderer through this wrapper multiplies the two instead: a\n * plain shape stays at `factor`, and a shape with `opacity` renders at\n * `opacity * factor` \u2014 dimmed *and* semi-transparent, as expected.\n *\n * One wrapper is allocated per dimmed pass (constant `factor`) and reused for\n * every dimmed shape; method lookups are memoised so the hot per-shape draw\n * loop allocates nothing.\n */\nexport const createDimTarget = (inner: RenderTarget, factor: number): RenderTarget => {\n const cache = new Map<PropertyKey, unknown>();\n const scaledSetOpacity = (a: number): void => {\n inner.setOpacity(a * factor);\n };\n const handler: ProxyHandler<RenderTarget> = {\n get(target, prop) {\n if (prop === \"setOpacity\") return scaledSetOpacity;\n if (cache.has(prop)) return cache.get(prop);\n const value: unknown = Reflect.get(target, prop);\n const resolved =\n typeof value === \"function\" ? (value as (...a: unknown[]) => unknown).bind(target) : value;\n cache.set(prop, resolved);\n return resolved;\n },\n };\n return new Proxy(inner, handler);\n};\n", "import type { Bounds, ElementId } from \"@oh-just-another/types\";\nimport { getElementWorldBounds, type Scene, type ElementBase } from \"@oh-just-another/scene\";\n\n/**\n * Per-shape memo with object-identity invalidation. Cached value sticks\n * until the underlying shape reference changes \u2014 and because every scene\n * op (`updateElement` / `moveElement` / ...) returns a new shape object, the\n * cache invalidates automatically without the caller threading versions\n * through.\n *\n * Caches survive across frames; pair with `prune(scene)` after large\n * deletions if memory matters. For ephemeral, single-render memos use\n * a fresh `ElementCache` instance (cheap to construct).\n */\nexport class ElementCache<T> {\n private readonly entries = new Map<ElementId, { readonly ref: ElementBase; value: T }>();\n\n get(shape: ElementBase): T | undefined {\n const entry = this.entries.get(shape.id);\n if (!entry) return undefined;\n if (entry.ref !== shape) {\n this.entries.delete(shape.id);\n return undefined;\n }\n return entry.value;\n }\n\n set(shape: ElementBase, value: T): T {\n this.entries.set(shape.id, { ref: shape, value });\n return value;\n }\n\n /**\n * Lazy memo. Returns the cached value if `shape` is the same reference\n * as the one we cached against; otherwise runs `compute`, stores the\n * result, and returns it.\n */\n getOrCompute(shape: ElementBase, compute: (s: ElementBase) => T): T {\n const cached = this.get(shape);\n if (cached !== undefined) return cached;\n return this.set(shape, compute(shape));\n }\n\n invalidate(id: ElementId): void {\n this.entries.delete(id);\n }\n\n clear(): void {\n this.entries.clear();\n }\n\n /** Drop entries whose shape is no longer in the scene. */\n prune(scene: Scene): void {\n for (const id of this.entries.keys()) {\n if (!scene.elements.has(id)) this.entries.delete(id);\n }\n }\n\n get size(): number {\n return this.entries.size;\n }\n}\n\n/**\n * Shared module-level cache for world-space bounds. Used by `renderScene`\n * for viewport culling and reusable from outside (hit-test, overlay) so\n * a single computation amortizes across passes.\n *\n * `getElementWorldBounds` is pure \u2014 same shape ref \u2192 same bounds \u2014 so a\n * by-identity cache is sound.\n */\nexport const sharedBoundsCache: ElementCache<Bounds> = new ElementCache<Bounds>();\n\nexport const cachedWorldBounds = (cache: ElementCache<Bounds>, shape: ElementBase): Bounds =>\n cache.getOrCompute(shape, getElementWorldBounds);\n", "/** Insertion-ordered LRU map: get() marks recency; set() evicts oldest past `cap` (by entry count). */\nexport class LruCache<K, V> {\n private readonly map = new Map<K, V>();\n constructor(\n private readonly cap: number,\n private readonly onEvict?: (key: K, value: V) => void,\n ) {}\n get(key: K): V | undefined {\n const v = this.map.get(key);\n if (v === undefined) return undefined;\n this.map.delete(key);\n this.map.set(key, v);\n return v;\n }\n has(key: K): boolean {\n return this.map.has(key);\n }\n set(key: K, value: V): void {\n this.map.delete(key);\n this.map.set(key, value);\n while (this.map.size > this.cap) {\n const oldest = this.map.keys().next().value as K;\n const ev = this.map.get(oldest);\n this.map.delete(oldest);\n if (ev !== undefined) this.onEvict?.(oldest, ev);\n }\n }\n delete(key: K): boolean {\n return this.map.delete(key);\n }\n clear(): void {\n this.map.clear();\n }\n get size(): number {\n return this.map.size;\n }\n keys(): IterableIterator<K> {\n return this.map.keys();\n }\n values(): IterableIterator<V> {\n return this.map.values();\n }\n}\n", "import type { ElementBase } from \"@oh-just-another/scene\";\nimport { LruCache } from \"./lru-cache.js\";\n\n/**\n * Per-shape rasterised cache. Keyed by the shape's identity reference \u2014\n * since scene mutations always replace the shape object (`apply(scene,\n * patch)` produces fresh references), a cache hit is guaranteed to reflect\n * the exact rendered output of the cached version. Pan / zoom invalidation\n * is the host's job: keep zoom in a small \"bucket\" (e.g. round to 0.1) and\n * include it in the key.\n *\n * LRU-by-insertion-order with a count cap. Hosts can replace with their own\n * cache by implementing the same `get` / `set` / `delete` surface.\n */\n\nexport interface ElementBitmapCache<V = unknown> {\n get(shape: ElementBase, zoomBucket: number): V | undefined;\n set(shape: ElementBase, zoomBucket: number, value: V): void;\n delete(shape: ElementBase, zoomBucket: number): void;\n clear(): void;\n readonly size: number;\n}\n\nconst keyFor = (shape: ElementBase, zoomBucket: number): string => `${shape.id}@${zoomBucket}`;\n\ninterface Entry<V> {\n readonly shapeRef: ElementBase;\n readonly value: V;\n}\n\n/**\n * In-memory LRU cache. Operates on shape identity (reference) \u2014\n * a stale shape reference for the same id is a miss because the\n * cached entry's `shapeRef !== shape`. That is the invalidation\n * mechanism \u2014 no version field needed.\n */\nexport class InMemoryElementBitmapCache<V> implements ElementBitmapCache<V> {\n private readonly entries: LruCache<string, Entry<V>>;\n\n constructor(cap = 512) {\n this.entries = new LruCache(cap);\n }\n\n get size(): number {\n return this.entries.size;\n }\n\n get(shape: ElementBase, zoomBucket: number): V | undefined {\n const key = keyFor(shape, zoomBucket);\n const e = this.entries.get(key);\n if (!e) return undefined;\n if (e.shapeRef !== shape) {\n // Reference changed \u2192 stale; evict so the slot is free.\n this.entries.delete(key);\n return undefined;\n }\n return e.value;\n }\n\n set(shape: ElementBase, zoomBucket: number, value: V): void {\n this.entries.set(keyFor(shape, zoomBucket), { shapeRef: shape, value });\n }\n\n delete(shape: ElementBase, zoomBucket: number): void {\n this.entries.delete(keyFor(shape, zoomBucket));\n }\n\n clear(): void {\n this.entries.clear();\n }\n}\n\n/**\n * Quantise a continuous zoom value to a bucket. Buckets within a\n * power-of-two range share a cache entry so small zoom adjustments\n * don't blow the cache. `bucket = 2 ^ round(log2(zoom))`.\n */\nexport const zoomBucket = (zoom: number): number => {\n if (zoom <= 0) return 1;\n return 2 ** Math.round(Math.log2(zoom));\n};\n", "import {\n getLayersInOrder,\n getElementsInLayer,\n getWorldToScreen,\n isText,\n type Scene,\n type ElementBase,\n type SpatialGrid,\n} from \"@oh-just-another/scene\";\nimport type { Bounds, LayerId, ElementId } from \"@oh-just-another/types\";\nimport { bounds as B, matrix } from \"@oh-just-another/math\";\nimport type { RenderTarget } from \"../targets/render-target.js\";\nimport type { AnimationClock } from \"../raster/animation-adapter.js\";\nimport { getElementRenderer, type ElementRenderContext } from \"./shape-renderer.js\";\nimport { drawShapeLabel } from \"./built-in-renderers.js\";\nimport { createDimTarget } from \"../targets/dim-target.js\";\nimport { cachedWorldBounds, ElementCache } from \"../caches/shape-cache.js\";\nimport { DEFAULT_PLACEHOLDER_FILL } from \"../constants.js\";\nimport type { LayerCompositeCache } from \"../caches/layer-cache-composite.js\";\nimport { zoomBucket as bucketFor } from \"../caches/shape-cache-bitmap.js\";\nimport { isTextBelowLod, screenSizeOf, type LodOptions } from \"./lod.js\";\n\nexport type { LodOptions } from \"./lod.js\";\n\nexport interface RenderSceneOptions {\n /** Skip clearing the target before drawing. Default: false. */\n readonly skipClear?: boolean;\n /** Called for shapes whose `type` has no registered renderer. Default: ignore. */\n readonly onUnknownElement?: (shape: ElementBase) => void;\n /**\n * World-space viewport bounds. When provided, shapes whose AABB does\n * not intersect this rectangle are skipped (viewport culling). Pass\n * a slightly inflated rect to avoid pop-in during pan.\n */\n readonly viewport?: Bounds;\n /**\n * Persistent bounds cache. When omitted a fresh per-render cache is\n * created \u2014 fine for hot paths because lookups inside one frame still\n * amortize. Pass a long-lived cache from `Editor` to share work across\n * frames, hit-test, and overlay.\n */\n readonly boundsCache?: ElementCache<Bounds>;\n /**\n * Pre-built spatial index. When provided together with `viewport`, the\n * renderer picks candidate shapes from the index and skips full layer\n * scans \u2014 pays off around ~10k shapes.\n */\n readonly spatialIndex?: SpatialGrid;\n /**\n * On-screen size thresholds for cheaper render paths. See {@link LodOptions}.\n */\n readonly lod?: LodOptions;\n /**\n * Placeholder fill colour. Defaults to `#bbb`. Pick something close\n * to the average shape colour so the transition is unobtrusive.\n */\n readonly placeholderFill?: string;\n /**\n * Optional dirty rectangle in **world** coords. When set:\n * \u2022 the renderer clears only the corresponding screen region;\n * \u2022 shapes whose world AABB doesn't intersect the dirty rect are\n * skipped entirely.\n * Combined with shape-identity tracking by the host this drops most\n * of the per-frame work for \"single shape moves on otherwise static\n * scene\".\n */\n readonly dirtyWorld?: Bounds;\n /**\n * Shapes to render with reduced alpha (modern-style group isolation).\n * For each shape whose `id` appears in this set, the renderer sets\n * `globalAlpha = dimOpacity` for the per-shape draw pass before\n * dispatching to the registered renderer.\n *\n * Caveat: shapes whose own `style.opacity` is explicitly set will\n * have their renderer call `setOpacity` again and override the\n * dim \u2014 the dim affects only the common case where shapes don't\n * carry an explicit opacity. Acceptable for the isolation UX\n * because outsiders are usually plain opaque shapes.\n */\n readonly dimElements?: ReadonlySet<ElementId>;\n /**\n * Alpha to use for `dimElements`. Default 1 (no-op). Hosts using the\n * isolation feature should pass their `ISOLATION_DIM_OPACITY`\n * constant.\n */\n readonly dimOpacity?: number;\n /**\n * Element ids that should NOT render this pass. The host computes\n * which shapes are effectively hidden (e.g. via group hide\n * propagation) and forwards the set here.\n */\n readonly hideElements?: ReadonlySet<ElementId>;\n /**\n * Per-layer composite bitmap cache. When supplied along with\n * `compositeLayerBitmap`, unchanged layers (i.e. not present in\n * `dirtyLayerIds`) are drawn from a single cached `drawImage` call\n * instead of walking every shape.\n *\n * Pass `dirtyLayerIds` so the renderer knows which layers to\n * re-rasterise. Without it the cache is treated as cold every\n * frame (defensive \u2014 better stale work than a stale visual).\n */\n readonly layerCompositeCache?: LayerCompositeCache;\n readonly dirtyLayerIds?: ReadonlySet<LayerId>;\n /**\n * Host-side layer rasteriser. Receives the layer id, the active\n * zoom bucket, and the scene; returns the bitmap to cache or\n * `null` to opt out. The kernel doesn't ship one \u2014 OffscreenCanvas\n * creation is the backend's job.\n */\n readonly compositeLayerBitmap?: (layerId: LayerId, zoomBucket: number, scene: Scene) => unknown;\n /**\n * Per-instance animated-content playback clock, forwarded to each shape\n * renderer via {@link ElementRenderContext.clock}. Lets the caller (an\n * `Editor`) drive per-shape GIF playback without mutating the process-global\n * {@link setAnimationClock}. Omit to fall back to the module clock.\n */\n readonly clock?: AnimationClock;\n /**\n * Static-export content switches, forwarded to element renderers via\n * the render context (see `ElementRenderContext.content`). Omit for\n * interactive rendering.\n */\n readonly content?: ElementRenderContext[\"content\"];\n /**\n * Hovered element id, forwarded to `ElementRenderContext.hoveredElement`\n * (hover-only chrome like the sticky \"+\" button). Omit when untracked.\n */\n readonly hoveredElement?: string;\n /** Forwarded to `ElementRenderContext.textPlaceholders` (grey prompt in empty text). */\n readonly textPlaceholders?: boolean;\n}\n\n/**\n * Renders the `main` z-stack of a scene onto a single target.\n *\n * Order of operations:\n * 1. Optionally clear the surface.\n * 2. Apply the scene's world-to-screen transform.\n * 3. For each visible layer (bottom \u2192 top): for each shape (bottom \u2192 top):\n * save state, push the shape's local TRS, invoke its registered renderer.\n *\n * This function does not draw edges, selection handles, or grids \u2014 those\n * either live on different layers (`background` / `overlay`) or are added by\n * higher-level packages.\n */\nexport const renderScene = (\n scene: Scene,\n target: RenderTarget,\n options: RenderSceneOptions = {},\n): void => {\n const w2s = getWorldToScreen(scene.viewport);\n const dirtyWorld = options.dirtyWorld;\n if (!options.skipClear) {\n if (dirtyWorld) {\n // Project the dirty rect to screen pixels, inflate by a few\n // pixels to cover anti-aliased stroke fuzz.\n const corners = [\n matrix.applyToPoint(w2s, { x: dirtyWorld.x, y: dirtyWorld.y }),\n matrix.applyToPoint(w2s, {\n x: dirtyWorld.x + dirtyWorld.width,\n y: dirtyWorld.y + dirtyWorld.height,\n }),\n ];\n const screen = B.expand(B.fromPoints(corners), 2);\n target.clear(screen);\n } else {\n target.clear();\n }\n }\n\n target.save();\n target.setTransform(w2s);\n\n const boundsCache = options.boundsCache ?? new ElementCache<Bounds>();\n const viewport = options.viewport;\n // Spatial-index candidate set: when present, restricts the per-layer\n // walk to shapes the index considers possibly-visible. Without it the\n // per-shape AABB check on a cached bounds is still cheap (~50ns), so\n // the index is only worth the build cost for very large scenes.\n let candidates: ReadonlySet<ElementId> | null = null;\n if (viewport && options.spatialIndex) {\n candidates = options.spatialIndex.query(viewport);\n }\n\n const zoom = scene.viewport.zoom;\n const clock = options.clock;\n // Reused per-shape render context. `clock` is per-instance when the caller\n // (Editor) threads one; omitted otherwise so the image renderer falls back\n // to the process-global animation clock.\n const ctx: ElementRenderContext = {\n zoom,\n ...(clock ? { clock } : {}),\n ...(options.content ? { content: options.content } : {}),\n ...(options.hoveredElement !== undefined ? { hoveredElement: options.hoveredElement } : {}),\n ...(options.textPlaceholders === true ? { textPlaceholders: true } : {}),\n };\n const lod = options.lod;\n const placeholderMax = lod?.placeholderMaxScreenPx;\n const placeholderFill = options.placeholderFill ?? DEFAULT_PLACEHOLDER_FILL;\n\n const layerCache = options.layerCompositeCache;\n const dirtyLayers = options.dirtyLayerIds;\n const compositeLayerBitmap = options.compositeLayerBitmap;\n const zoomBucket = bucketFor(zoom);\n const layerBoundsFor = (layerId: LayerId): Bounds | null => {\n let acc: Bounds | null = null;\n for (const shape of getElementsInLayer(scene, layerId)) {\n const bb = cachedWorldBounds(boundsCache, shape);\n acc = acc ? B.union(acc, bb) : bb;\n }\n return acc;\n };\n\n // Dim (isolation / eraser preview) scales the alpha of `dimElements`. Route\n // those shapes through a wrapper that multiplies `setOpacity` by `dimOpacity`\n // \u2014 so a shape carrying its own `style.opacity` renders dimmed too, instead\n // of overwriting the dim back to full. Built once (constant factor), reused.\n const dimOpacity = options.dimOpacity;\n const dimTarget =\n options.dimElements !== undefined && dimOpacity !== undefined\n ? createDimTarget(target, dimOpacity)\n : null;\n\n for (const layer of getLayersInOrder(scene)) {\n if (!layer.visible) continue;\n\n // Per-layer composite cache fast path. Only fires when the host\n // plugged a cache + a layer rasteriser; the kernel ships no default\n // rasteriser (OffscreenCanvas creation is the backend's job). Drop\n // dirty layers from the cache so the bitmap isn't re-used after a\n // mutation.\n if (layerCache && compositeLayerBitmap) {\n if (dirtyLayers?.has(layer.id)) layerCache.invalidateLayer(layer.id);\n let bitmap = layerCache.get(layer.id, zoomBucket);\n if (bitmap === undefined) {\n const fresh = compositeLayerBitmap(layer.id, zoomBucket, scene);\n if (fresh !== null) {\n layerCache.set(layer.id, zoomBucket, fresh);\n bitmap = fresh;\n }\n }\n if (bitmap !== undefined) {\n const bb = layerBoundsFor(layer.id);\n if (bb) target.drawImage(bitmap, bb.x, bb.y, bb.width, bb.height);\n continue;\n }\n }\n\n for (const shape of getElementsInLayer(scene, layer.id)) {\n if (options.hideElements?.has(shape.id)) continue;\n if (candidates && !candidates.has(shape.id)) continue;\n if (viewport) {\n const bb = cachedWorldBounds(boundsCache, shape);\n if (!B.intersects(bb, viewport)) continue;\n }\n if (dirtyWorld) {\n const bb = cachedWorldBounds(boundsCache, shape);\n if (!B.intersects(bb, dirtyWorld)) continue;\n }\n\n // LOD is per element, from what actually lands on screen: unreadable\n // text is skipped, a shape too small to show detail becomes a flat\n // fill \u2014 regardless of the zoom level itself.\n if (isText(shape) && isTextBelowLod(shape.fontSize, zoom, lod)) continue;\n\n if (\n placeholderMax !== undefined &&\n screenSizeOf(cachedWorldBounds(boundsCache, shape), zoom) < placeholderMax\n ) {\n // Draw the AABB directly in world coords \u2014 skip the renderer\n // entirely. The shape's TRS is folded into the cached bounds.\n const bb = cachedWorldBounds(boundsCache, shape);\n target.setFill(placeholderFill);\n target.setStrokeWidth(0);\n target.beginPath();\n target.rect(bb.x, bb.y, bb.width, bb.height);\n target.fill();\n continue;\n }\n\n const renderer = getElementRenderer(shape.type);\n if (!renderer) {\n options.onUnknownElement?.(shape);\n continue;\n }\n\n target.save();\n // Isolation / eraser dim \u2014 draw through the scaling wrapper so the\n // shape's own `style.opacity` multiplies with `dimOpacity` instead of\n // overwriting it (see RenderSceneOptions.dimElements / createDimTarget).\n // The wrapper's base alpha is `dimOpacity` (setOpacity(1) \u2192 dimOpacity),\n // so a shape that never sets its own opacity still dims.\n const dimmed = dimTarget !== null && options.dimElements?.has(shape.id) === true;\n const draw = dimmed ? dimTarget : target;\n if (dimmed) draw.setOpacity(1);\n draw.translate(shape.position.x, shape.position.y);\n if (shape.rotation !== 0) draw.rotate(shape.rotation);\n if (shape.scale.x !== 1 || shape.scale.y !== 1) {\n draw.scale(shape.scale.x, shape.scale.y);\n }\n renderer(shape, draw, ctx);\n // Embedded label \u2014 drawn in the shape's local space, after its\n // body so the text sits on top. Subject to the same readable-text\n // LOD floor as standalone text (checked on the resolved font size).\n if (shape.label !== undefined && !isText(shape)) {\n drawShapeLabel(shape, draw, lod?.minTextScreenPx !== undefined ? { zoom, lod } : undefined);\n }\n target.restore();\n }\n }\n\n target.restore();\n};\n", "/**\n * The fonts the editor ships and draws with \u2014 Roboto (sans), PT Serif\n * (serif) and Roboto Mono (mono). Bundling them means every render backend\n * (Canvas2D, WebGL2/MSDF, the offscreen worker) measures and draws the same\n * glyphs, instead of WebGL2 using the embedded font while Canvas2D falls back\n * to whatever the OS resolves for the requested family.\n */\n\n/** The three bundled font families. */\nexport const FONT_SANS = \"Roboto\";\nexport const FONT_SERIF = \"PT Serif\";\nexport const FONT_MONO = \"Roboto Mono\";\n\n/**\n * Map a CSS font-family stack to the bundled family that backs it. Mirrors\n * the resolution the WASM shaper uses, so Canvas2D and WebGL2 pick the same\n * face: `mono` wins, then `sans` (so `sans-serif` stays sans), then a\n * serif-ish keyword, else sans.\n */\nexport const resolveBundledFamily = (cssFamily: string): string => {\n const f = cssFamily.toLowerCase();\n if (f.includes(\"mono\")) return FONT_MONO;\n if (f.includes(\"sans\")) return FONT_SANS;\n if (f.includes(\"serif\") || f.includes(\"slab\") || f.includes(\"georgia\") || f.includes(\"times\")) {\n return FONT_SERIF;\n }\n return FONT_SANS;\n};\n\ninterface FaceSpec {\n readonly family: string;\n readonly weight: \"400\" | \"700\";\n readonly style: \"normal\" | \"italic\";\n /** Built with a static `new URL(...)` literal so bundlers emit the asset. */\n readonly url: URL;\n}\n\n// Each `new URL` must be a static literal \u2014 a dynamic path (template string)\n// isn't seen by bundler asset pipelines and would 404.\nconst FACES: readonly FaceSpec[] = [\n {\n family: FONT_SANS,\n weight: \"400\",\n style: \"normal\",\n url: new URL(\"../fonts/Roboto-Regular.woff2\", import.meta.url),\n },\n {\n family: FONT_SANS,\n weight: \"700\",\n style: \"normal\",\n url: new URL(\"../fonts/Roboto-Bold.woff2\", import.meta.url),\n },\n {\n family: FONT_SANS,\n weight: \"400\",\n style: \"italic\",\n url: new URL(\"../fonts/Roboto-Italic.woff2\", import.meta.url),\n },\n {\n family: FONT_SANS,\n weight: \"700\",\n style: \"italic\",\n url: new URL(\"../fonts/Roboto-BoldItalic.woff2\", import.meta.url),\n },\n {\n family: FONT_SERIF,\n weight: \"400\",\n style: \"normal\",\n url: new URL(\"../fonts/PTSerif-Regular.woff2\", import.meta.url),\n },\n {\n family: FONT_SERIF,\n weight: \"700\",\n style: \"normal\",\n url: new URL(\"../fonts/PTSerif-Bold.woff2\", import.meta.url),\n },\n {\n family: FONT_SERIF,\n weight: \"400\",\n style: \"italic\",\n url: new URL(\"../fonts/PTSerif-Italic.woff2\", import.meta.url),\n },\n {\n family: FONT_SERIF,\n weight: \"700\",\n style: \"italic\",\n url: new URL(\"../fonts/PTSerif-BoldItalic.woff2\", import.meta.url),\n },\n {\n family: FONT_MONO,\n weight: \"400\",\n style: \"normal\",\n url: new URL(\"../fonts/RobotoMono-Regular.woff2\", import.meta.url),\n },\n {\n family: FONT_MONO,\n weight: \"700\",\n style: \"normal\",\n url: new URL(\"../fonts/RobotoMono-Bold.woff2\", import.meta.url),\n },\n {\n family: FONT_MONO,\n weight: \"400\",\n style: \"italic\",\n url: new URL(\"../fonts/RobotoMono-Italic.woff2\", import.meta.url),\n },\n {\n family: FONT_MONO,\n weight: \"700\",\n style: \"italic\",\n url: new URL(\"../fonts/RobotoMono-BoldItalic.woff2\", import.meta.url),\n },\n];\n\nexport interface FontScope {\n readonly fonts?: {\n add(font: FontFace): void;\n has(font: FontFace): boolean;\n };\n}\n\n/**\n * Load and register the bundled fonts into a scope's font set \u2014 pass the\n * `window` on the main thread and the worker's `self` inside a render worker\n * (both expose `.fonts`). Idempotent and resolves once every face is ready,\n * so callers can render crisp text after it settles. A no-op where the\n * `FontFace` API is unavailable (older runtimes / SSR).\n */\nexport const registerBundledFonts = async (\n scope: FontScope = globalThis as FontScope,\n): Promise<void> => {\n const set = scope.fonts;\n if (!set || typeof FontFace === \"undefined\") return;\n // `allSettled` so one missing face doesn't block the rest from loading.\n await Promise.allSettled(\n FACES.map(async (f) => {\n const face = new FontFace(f.family, `url(${f.url.href})`, {\n weight: f.weight,\n style: f.style,\n });\n await face.load();\n set.add(face);\n }),\n );\n};\n", "// Single implementation lives in renderer-core (element renderers need it\n// too, to distinguish \"rehydration pending\" from \"permanently broken\");\n// re-exported here for this package's backends.\nexport { isDrawableImageSource } from \"@oh-just-another/renderer-core\";\n\n/**\n * Intrinsic pixel size of a drawable image source, or `null` when it can't be\n * determined. Handles the differing width/height accessors of the DOM image\n * types (`naturalWidth` for `<img>`, `videoWidth` for `<video>`, plain\n * `width`/`height` for bitmaps / canvases). Needed to turn a normalised crop\n * (fractions) into a pixel source rectangle for `ctx.drawImage`.\n */\nexport const intrinsicImageSize = (\n source: CanvasImageSource,\n): { readonly width: number; readonly height: number } | null => {\n const s = source as {\n naturalWidth?: number;\n naturalHeight?: number;\n videoWidth?: number;\n videoHeight?: number;\n width?: number | { baseVal?: unknown };\n height?: number | { baseVal?: unknown };\n };\n if (typeof s.naturalWidth === \"number\" && s.naturalWidth > 0) {\n return { width: s.naturalWidth, height: s.naturalHeight ?? s.naturalWidth };\n }\n if (typeof s.videoWidth === \"number\" && s.videoWidth > 0) {\n return { width: s.videoWidth, height: s.videoHeight ?? s.videoWidth };\n }\n if (typeof s.width === \"number\" && s.width > 0 && typeof s.height === \"number\") {\n return { width: s.width, height: s.height };\n }\n return null;\n};\n\n/**\n * Warn (once per distinct kind) when an image draw is skipped because\n * the handle isn't drawable. Throttled by a module-level `Set` so a\n * per-frame render loop doesn't spam the console \u2014 but the host still\n * sees that an image failed to render and the likely cause.\n */\nconst warnedImageKinds = new Set<string>();\n\nexport const warnSkippedImage = (value: unknown): void => {\n if (typeof console === \"undefined\") return;\n const kind =\n typeof value === \"string\"\n ? value.startsWith(\"blob:\")\n ? \"dead-blob-url\"\n : \"string-src\"\n : value === null || value === undefined\n ? \"empty\"\n : \"stale-object\"; // e.g. a {} from a serialised <img>\n if (warnedImageKinds.has(kind)) return;\n warnedImageKinds.add(kind);\n\n console.warn(\n `[renderer] skipped a non-drawable image source (kind: ${kind}). ` +\n \"The shape's image handle isn't a live HTMLImageElement / canvas / \" +\n \"bitmap and it has no Scene.files bytes to rehydrate from (shapes \" +\n \"with a fileId are skipped silently while rehydration is in flight) \u2014 \" +\n \"the image will stay blank.\",\n );\n};\n", "import type { Bounds, Transform } from \"@oh-just-another/types\";\nimport type {\n FillRule,\n LineCap,\n LineJoin,\n RenderTarget,\n TextAlign,\n TextBaseline,\n} from \"@oh-just-another/renderer-core\";\nimport { resolveBundledFamily } from \"@oh-just-another/fonts\";\nimport { intrinsicImageSize, isDrawableImageSource, warnSkippedImage } from \"./image-source.js\";\n\n/**\n * Wraps a `CanvasRenderingContext2D` (or compatible OffscreenCanvas context)\n * as a backend-agnostic `RenderTarget`. Coordinates passed to the target are\n * in CSS pixels; the device-pixel scaling is applied once at construction by\n * the device-pixel-ratio (DPR) helper, so all draw calls see CSS units.\n *\n * `size` reports the CSS-pixel size that draw calls operate in. The underlying\n * canvas bitmap may be larger (DPR \u00D7 size) but that is transparent here.\n */\nexport class Canvas2DTarget implements RenderTarget {\n private readonly ctx: CanvasRenderingContext2D;\n private _width: number;\n private _height: number;\n /**\n * Device-pixel-ratio the canvas bitmap is scaled by (see `setupHiDpi`).\n * `setTransform` / `resetTransform` take a transform that maps world \u2192\n * CSS pixels; they pre-multiply by `scale(dpr)` so the result lands in\n * the DPR-scaled device buffer.\n */\n private dpr: number;\n\n /**\n * `width` / `height` are CSS-pixel dimensions. `dpr` must match the value\n * `setupHiDpi` used to scale the bitmap (default 1). The constructor assumes\n * the caller has already configured the canvas bitmap + context transform.\n */\n constructor(ctx: CanvasRenderingContext2D, width: number, height: number, dpr = 1) {\n this.ctx = ctx;\n this._width = width;\n this._height = height;\n this.dpr = dpr;\n }\n\n get size(): { readonly width: number; readonly height: number } {\n return { width: this._width, height: this._height };\n }\n\n /** Mutator for callers that resize the canvas. `dpr` updates the device\n * scale when the canvas moves to a different-density display. */\n resize(width: number, height: number, dpr?: number): void {\n this._width = width;\n this._height = height;\n if (dpr !== undefined) this.dpr = dpr;\n }\n\n // --- Style ---\n\n setFill(color: string | null): void {\n this.ctx.fillStyle = color ?? \"transparent\";\n }\n setStroke(color: string | null): void {\n this.ctx.strokeStyle = color ?? \"transparent\";\n }\n setStrokeWidth(width: number): void {\n this.ctx.lineWidth = width;\n }\n setOpacity(alpha: number): void {\n this.ctx.globalAlpha = alpha;\n }\n setLineCap(cap: LineCap): void {\n this.ctx.lineCap = cap;\n }\n setLineJoin(join: LineJoin): void {\n this.ctx.lineJoin = join;\n }\n setDashArray(dash: readonly number[] | null): void {\n this.ctx.setLineDash(dash ? [...dash] : []);\n }\n setFont(\n fontFamily: string,\n fontSize: number,\n options?: { weight?: \"normal\" | \"bold\"; style?: \"normal\" | \"italic\" },\n ): void {\n // CSS font shorthand order: `<style> <weight> <size> <family>`. Draw with\n // the bundled face that backs the requested family (matching the WebGL2\n // MSDF path), falling back to the original stack until it has loaded.\n const style = options?.style === \"italic\" ? \"italic \" : \"\";\n const weight = options?.weight === \"bold\" ? \"bold \" : \"\";\n this.ctx.font = `${style}${weight}${fontSize}px \"${resolveBundledFamily(fontFamily)}\", ${fontFamily}`;\n }\n setTextAlign(align: TextAlign): void {\n this.ctx.textAlign = align === \"center\" ? \"center\" : align;\n }\n setTextBaseline(baseline: TextBaseline): void {\n this.ctx.textBaseline =\n baseline === \"middle\" ? \"middle\" : baseline === \"top\" ? \"top\" : \"bottom\";\n }\n\n // --- State stack ---\n\n save(): void {\n this.ctx.save();\n }\n restore(): void {\n this.ctx.restore();\n }\n\n // --- Transform ---\n\n translate(dx: number, dy: number): void {\n this.ctx.translate(dx, dy);\n }\n rotate(radians: number): void {\n this.ctx.rotate(radians);\n }\n scale(sx: number, sy: number): void {\n this.ctx.scale(sx, sy);\n }\n setTransform(t: Transform): void {\n // Compose with the DPR base: device = scale(dpr) \u00B7 t. `t` maps world \u2192\n // CSS px; the bitmap is dpr\u00D7 bigger, so every coordinate scales by dpr.\n const d = this.dpr;\n this.ctx.setTransform(d * t.a, d * t.b, d * t.c, d * t.d, d * t.e, d * t.f);\n }\n resetTransform(): void {\n // Reset to the DPR base (NOT raw identity) so CSS-px draws stay scaled.\n this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);\n }\n\n // --- Path primitives ---\n\n beginPath(): void {\n this.ctx.beginPath();\n }\n closePath(): void {\n this.ctx.closePath();\n }\n moveTo(x: number, y: number): void {\n this.ctx.moveTo(x, y);\n }\n lineTo(x: number, y: number): void {\n this.ctx.lineTo(x, y);\n }\n quadraticCurveTo(cx: number, cy: number, x: number, y: number): void {\n this.ctx.quadraticCurveTo(cx, cy, x, y);\n }\n bezierCurveTo(c1x: number, c1y: number, c2x: number, c2y: number, x: number, y: number): void {\n this.ctx.bezierCurveTo(c1x, c1y, c2x, c2y, x, y);\n }\n rect(x: number, y: number, width: number, height: number): void {\n this.ctx.rect(x, y, width, height);\n }\n ellipse(cx: number, cy: number, rx: number, ry: number): void {\n this.ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);\n }\n\n // --- Fill / stroke ---\n\n fill(rule?: FillRule): void {\n this.ctx.fill(rule);\n }\n stroke(): void {\n this.ctx.stroke();\n }\n clip(rule?: FillRule): void {\n this.ctx.clip(rule);\n }\n\n // --- Text ---\n\n fillText(text: string, x: number, y: number, maxWidth?: number): void {\n if (maxWidth !== undefined) this.ctx.fillText(text, x, y, maxWidth);\n else this.ctx.fillText(text, x, y);\n }\n measureText(text: string): { width: number } {\n const m = this.ctx.measureText(text);\n return { width: m.width };\n }\n\n // --- Images ---\n\n drawImage(\n image: unknown,\n dx: number,\n dy: number,\n dw: number,\n dh: number,\n _dynamic?: boolean,\n crop?: {\n readonly x: number;\n readonly y: number;\n readonly width: number;\n readonly height: number;\n },\n ): void {\n // `_dynamic` ignored \u2014 Canvas2D reads the source element live on\n // every drawImage, so animated GIF / video frames are picked up\n // automatically as long as the host re-renders.\n void _dynamic;\n // Guard against non-drawable handles. A restored scene carries\n // either a string `src` (dead blob: URL) OR a `metadata.image`\n // that serialised to `{}` (a live `<img>` becomes an empty object\n // through JSON). Both throw inside `ctx.drawImage`. Skip rather\n // than crash the whole render pass, and surface it once so hosts\n // know an image didn't render (and why).\n if (!isDrawableImageSource(image)) {\n warnSkippedImage(image);\n return;\n }\n // Cropped draw: map the normalised source rect to pixels and use the\n // 9-argument form. Falls back to the whole image when the intrinsic\n // size is unknown (crop can't be resolved).\n if (crop && (crop.x !== 0 || crop.y !== 0 || crop.width !== 1 || crop.height !== 1)) {\n const size = intrinsicImageSize(image);\n if (size) {\n this.ctx.drawImage(\n image,\n crop.x * size.width,\n crop.y * size.height,\n crop.width * size.width,\n crop.height * size.height,\n dx,\n dy,\n dw,\n dh,\n );\n return;\n }\n }\n this.ctx.drawImage(image, dx, dy, dw, dh);\n }\n\n // --- Surface control ---\n\n clear(bounds?: Bounds): void {\n // A `clear()` always opens a fresh dirty pass \u2014 the host took\n // responsibility for the cleared region, anything we accumulate\n // from here is the new frame's coverage.\n this.dirtyRect = null;\n if (bounds) {\n this.ctx.clearRect(bounds.x, bounds.y, bounds.width, bounds.height);\n } else {\n // Clear the entire CSS-space area, regardless of current transform.\n this.ctx.save();\n this.ctx.setTransform(1, 0, 0, 1, 0, 0);\n this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);\n this.ctx.restore();\n }\n }\n\n // --- Per-pass dirty accumulator ---\n\n /**\n * Screen-space union of every `markDirty(bounds)` call since the\n * last `clear()`. Hosts can read it via `getDirtyRect()` to size\n * the next clear precisely \u2014 covers anti-aliased stroke fuzz and\n * shape renderers that paint a few px beyond their geometric bbox.\n */\n private dirtyRect: Bounds | null = null;\n\n markDirty(bounds: Bounds): void {\n if (!this.dirtyRect) {\n this.dirtyRect = bounds;\n return;\n }\n const minX = Math.min(this.dirtyRect.x, bounds.x);\n const minY = Math.min(this.dirtyRect.y, bounds.y);\n const maxX = Math.max(this.dirtyRect.x + this.dirtyRect.width, bounds.x + bounds.width);\n const maxY = Math.max(this.dirtyRect.y + this.dirtyRect.height, bounds.y + bounds.height);\n this.dirtyRect = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };\n }\n\n /** Read the accumulated dirty rect for this pass. `null` when nothing painted. */\n getDirtyRect(): Bounds | null {\n return this.dirtyRect;\n }\n}\n", "import type { Bounds } from \"@oh-just-another/types\";\nimport {\n LruCache,\n type FillRule,\n type LineCap,\n type LineJoin,\n type RenderTarget,\n type TextAlign,\n type TextBaseline,\n} from \"@oh-just-another/renderer-core\";\nimport { OFFSCREEN_IMAGE_CACHE_CAP } from \"../constants.js\";\nimport type { RenderCommand } from \"./recording-target.js\";\n\n/**\n * Packed-frame codec for the offscreen backend's per-frame worker hop.\n *\n * `structuredClone`-ing an array of {@link RenderCommand} objects costs\n * ~1.6 ms for a ~4.5k-command frame (see `tests/offscreen-transfer.bench.ts`)\n * \u2014 every object, key, and string is walked and copied. This codec flattens\n * the stream into one transferable `ArrayBuffer` of Float64 words (opcode +\n * args per command) plus a per-frame deduplicated string table, so\n * `postMessage` transfers the numeric bulk for free and only clones a small\n * string array. `ImageBitmap` payloads (`defineImage`) travel in a side\n * array \u2014 see {@link packReplayFrame}.\n */\n\n/**\n * Opcodes for the packed numeric stream \u2014 one per {@link RenderCommand}\n * variant except `defineImage`, which travels in the side bitmap array and\n * emits nothing here. Wire format only: values are arbitrary but must stay\n * in sync between {@link packReplayFrame} and {@link replayPackedFrame}\n * (same-package protocol; both sides always ship together).\n */\nconst OP_SET_FILL = 0;\nconst OP_SET_STROKE = 1;\nconst OP_SET_STROKE_WIDTH = 2;\nconst OP_SET_OPACITY = 3;\nconst OP_SET_LINE_CAP = 4;\nconst OP_SET_LINE_JOIN = 5;\nconst OP_SET_DASH_ARRAY = 6;\nconst OP_SET_FONT = 7;\nconst OP_SET_TEXT_ALIGN = 8;\nconst OP_SET_TEXT_BASELINE = 9;\nconst OP_SAVE = 10;\nconst OP_RESTORE = 11;\nconst OP_TRANSLATE = 12;\nconst OP_ROTATE = 13;\nconst OP_SCALE = 14;\nconst OP_SET_TRANSFORM = 15;\nconst OP_RESET_TRANSFORM = 16;\nconst OP_BEGIN_PATH = 17;\nconst OP_CLOSE_PATH = 18;\nconst OP_MOVE_TO = 19;\nconst OP_LINE_TO = 20;\nconst OP_QUADRATIC_CURVE_TO = 21;\nconst OP_BEZIER_CURVE_TO = 22;\nconst OP_RECT = 23;\nconst OP_ELLIPSE = 24;\nconst OP_FILL = 25;\nconst OP_STROKE = 26;\nconst OP_FILL_TEXT = 27;\nconst OP_CLEAR = 28;\nconst OP_MARK_DIRTY = 29;\nconst OP_RESIZE = 30;\nconst OP_DRAW_IMAGE = 31;\nconst OP_CLIP = 32;\n\n/**\n * String-table index sentinel for a `null` color (`setFill` / `setStroke`\n * accept `Color | null`). Real indices are >= 0.\n */\nconst NULL_STRING_INDEX = -1;\n\n/** `setDashArray(null)` marker written in place of the dash length. */\nconst NULL_DASH_LENGTH = -1;\n\n/**\n * Enum wire codes. Encode side uses the `*_CODE` records, decode side the\n * positional arrays \u2014 index === code. Order is wire format; append only.\n */\nconst LINE_CAPS: readonly LineCap[] = [\"butt\", \"round\", \"square\"];\nconst LINE_CAP_CODE: Record<LineCap, number> = { butt: 0, round: 1, square: 2 };\nconst LINE_JOINS: readonly LineJoin[] = [\"miter\", \"round\", \"bevel\"];\nconst LINE_JOIN_CODE: Record<LineJoin, number> = { miter: 0, round: 1, bevel: 2 };\nconst TEXT_ALIGNS: readonly TextAlign[] = [\"left\", \"center\", \"right\"];\nconst TEXT_ALIGN_CODE: Record<TextAlign, number> = { left: 0, center: 1, right: 2 };\nconst TEXT_BASELINES: readonly TextBaseline[] = [\"top\", \"middle\", \"bottom\"];\nconst TEXT_BASELINE_CODE: Record<TextBaseline, number> = { top: 0, middle: 1, bottom: 2 };\n\n/**\n * `fill(rule?)` wire codes: 0 = no rule argument, 1 = \"nonzero\",\n * 2 = \"evenodd\".\n */\nconst FILL_RULE_CODE: Record<FillRule, number> = { nonzero: 1, evenodd: 2 };\n\n/**\n * `setFont` options wire codes. 0 = key absent from the options object;\n * 1 / 2 = the two allowed values. A separate leading flag word (0 / 1)\n * distinguishes \"no options argument at all\" from an empty options object.\n */\nconst FONT_WEIGHT_CODE: Record<\"normal\" | \"bold\", number> = { normal: 1, bold: 2 };\nconst FONT_STYLE_CODE: Record<\"normal\" | \"italic\", number> = { normal: 1, italic: 2 };\n\n/** Optional-argument presence flags (`fillText` maxWidth, `clear` bounds). */\nconst ABSENT = 0;\nconst PRESENT = 1;\n\n/**\n * Initial capacity (in Float64 words) of the packed stream, as a multiple\n * of the command count. Most commands fit in opcode + \u22646 args; the writer\n * doubles on overflow, so this only tunes how often the first frames\n * reallocate.\n */\nconst PACK_WORDS_PER_COMMAND = 4;\n\n/** Floor for the writer's initial capacity so tiny frames don't thrash. */\nconst PACK_MIN_CAPACITY = 64;\n\n/** One `defineImage` payload carried alongside the numeric stream. */\nexport interface PackedFrameBitmap {\n readonly id: number;\n readonly bitmap: ImageBitmap;\n}\n\n/**\n * Result of {@link packReplayFrame}: `buffer` is the transferable numeric\n * stream, `strings` the per-frame deduplicated string table it indexes\n * into, `bitmaps` the `defineImage` payloads (worker registers them BEFORE\n * replaying the stream).\n */\nexport interface PackedReplayFrame {\n readonly buffer: ArrayBuffer;\n readonly strings: readonly string[];\n readonly bitmaps: readonly PackedFrameBitmap[];\n}\n\n/**\n * postMessage shape the offscreen surface posts per changed layer and the\n * render worker consumes. `buffer` goes in the transfer list; `strings`\n * are cloned (cheap \u2014 deduplicated); `bitmaps` are CLONED, never\n * transferred \u2014 see {@link packReplayFrame}.\n */\nexport interface PackedReplayMessage {\n readonly type: \"replay\";\n readonly buffer: ArrayBuffer;\n readonly strings: readonly string[];\n readonly bitmaps: readonly PackedFrameBitmap[];\n}\n\n/**\n * Flatten a flushed {@link RenderCommand} buffer into a transferable packed\n * frame: one Float64 word per opcode / argument, strings deduplicated into\n * a side table, enums and presence flags as small ints.\n *\n * `defineImage` commands emit nothing into the numeric stream; their\n * `{ id, bitmap }` payloads are collected into `bitmaps` instead. The\n * caller must post them WITHOUT a transfer-list entry so `postMessage`\n * clones the pixels: the recorder's intern LRU still owns the source\n * bitmap and will keep drawing it on later frames (GIF / video), so\n * transferring (detaching) it would break the main thread's copy.\n */\nexport const packReplayFrame = (commands: readonly RenderCommand[]): PackedReplayFrame => {\n let words = new Float64Array(\n Math.max(PACK_MIN_CAPACITY, commands.length * PACK_WORDS_PER_COMMAND),\n );\n let used = 0;\n const push = (v: number): void => {\n if (used === words.length) {\n const grown = new Float64Array(words.length * 2);\n grown.set(words);\n words = grown;\n }\n words[used++] = v;\n };\n\n const strings: string[] = [];\n const stringIndex = new Map<string, number>();\n /** Dedup a string through the per-frame table, returning its index. */\n const intern = (s: string): number => {\n let idx = stringIndex.get(s);\n if (idx === undefined) {\n idx = strings.length;\n strings.push(s);\n stringIndex.set(s, idx);\n }\n return idx;\n };\n\n const bitmaps: PackedFrameBitmap[] = [];\n\n for (const cmd of commands) {\n switch (cmd.k) {\n case \"setFill\":\n push(OP_SET_FILL);\n push(cmd.color === null ? NULL_STRING_INDEX : intern(cmd.color));\n break;\n case \"setStroke\":\n push(OP_SET_STROKE);\n push(cmd.color === null ? NULL_STRING_INDEX : intern(cmd.color));\n break;\n case \"setStrokeWidth\":\n push(OP_SET_STROKE_WIDTH);\n push(cmd.w);\n break;\n case \"setOpacity\":\n push(OP_SET_OPACITY);\n push(cmd.a);\n break;\n case \"setLineCap\":\n push(OP_SET_LINE_CAP);\n push(LINE_CAP_CODE[cmd.cap]);\n break;\n case \"setLineJoin\":\n push(OP_SET_LINE_JOIN);\n push(LINE_JOIN_CODE[cmd.join]);\n break;\n case \"setDashArray\":\n push(OP_SET_DASH_ARRAY);\n if (cmd.dash === null) {\n push(NULL_DASH_LENGTH);\n } else {\n push(cmd.dash.length);\n for (const d of cmd.dash) push(d);\n }\n break;\n case \"setFont\":\n push(OP_SET_FONT);\n push(intern(cmd.family));\n push(cmd.size);\n if (cmd.options === undefined) {\n push(ABSENT);\n } else {\n push(PRESENT);\n push(cmd.options.weight === undefined ? 0 : FONT_WEIGHT_CODE[cmd.options.weight]);\n push(cmd.options.style === undefined ? 0 : FONT_STYLE_CODE[cmd.options.style]);\n }\n break;\n case \"setTextAlign\":\n push(OP_SET_TEXT_ALIGN);\n push(TEXT_ALIGN_CODE[cmd.align]);\n break;\n case \"setTextBaseline\":\n push(OP_SET_TEXT_BASELINE);\n push(TEXT_BASELINE_CODE[cmd.baseline]);\n break;\n case \"save\":\n push(OP_SAVE);\n break;\n case \"restore\":\n push(OP_RESTORE);\n break;\n case \"translate\":\n push(OP_TRANSLATE);\n push(cmd.x);\n push(cmd.y);\n break;\n case \"rotate\":\n push(OP_ROTATE);\n push(cmd.r);\n break;\n case \"scale\":\n push(OP_SCALE);\n push(cmd.sx);\n push(cmd.sy);\n break;\n case \"setTransform\":\n push(OP_SET_TRANSFORM);\n push(cmd.t.a);\n push(cmd.t.b);\n push(cmd.t.c);\n push(cmd.t.d);\n push(cmd.t.e);\n push(cmd.t.f);\n break;\n case \"resetTransform\":\n push(OP_RESET_TRANSFORM);\n break;\n case \"beginPath\":\n push(OP_BEGIN_PATH);\n break;\n case \"closePath\":\n push(OP_CLOSE_PATH);\n break;\n case \"moveTo\":\n push(OP_MOVE_TO);\n push(cmd.x);\n push(cmd.y);\n break;\n case \"lineTo\":\n push(OP_LINE_TO);\n push(cmd.x);\n push(cmd.y);\n break;\n case \"quadraticCurveTo\":\n push(OP_QUADRATIC_CURVE_TO);\n push(cmd.cx);\n push(cmd.cy);\n push(cmd.x);\n push(cmd.y);\n break;\n case \"bezierCurveTo\":\n push(OP_BEZIER_CURVE_TO);\n push(cmd.c1x);\n push(cmd.c1y);\n push(cmd.c2x);\n push(cmd.c2y);\n push(cmd.x);\n push(cmd.y);\n break;\n case \"rect\":\n push(OP_RECT);\n push(cmd.x);\n push(cmd.y);\n push(cmd.w);\n push(cmd.h);\n break;\n case \"ellipse\":\n push(OP_ELLIPSE);\n push(cmd.cx);\n push(cmd.cy);\n push(cmd.rx);\n push(cmd.ry);\n break;\n case \"fill\":\n push(OP_FILL);\n push(cmd.rule === undefined ? ABSENT : FILL_RULE_CODE[cmd.rule]);\n break;\n case \"clip\":\n push(OP_CLIP);\n push(cmd.rule === undefined ? ABSENT : FILL_RULE_CODE[cmd.rule]);\n break;\n case \"stroke\":\n push(OP_STROKE);\n break;\n case \"fillText\":\n push(OP_FILL_TEXT);\n push(intern(cmd.text));\n push(cmd.x);\n push(cmd.y);\n if (cmd.maxWidth === undefined) {\n push(ABSENT);\n } else {\n push(PRESENT);\n push(cmd.maxWidth);\n }\n break;\n case \"clear\":\n push(OP_CLEAR);\n if (cmd.bounds === undefined) {\n push(ABSENT);\n } else {\n push(PRESENT);\n push(cmd.bounds.x);\n push(cmd.bounds.y);\n push(cmd.bounds.width);\n push(cmd.bounds.height);\n }\n break;\n case \"markDirty\":\n push(OP_MARK_DIRTY);\n push(cmd.bounds.x);\n push(cmd.bounds.y);\n push(cmd.bounds.width);\n push(cmd.bounds.height);\n break;\n case \"resize\":\n push(OP_RESIZE);\n push(cmd.w);\n push(cmd.h);\n break;\n case \"defineImage\":\n // Not packed: the bitmap travels beside the numeric stream. The\n // worker registers all side bitmaps before replaying, so the\n // stream's `drawImage` id references always resolve.\n bitmaps.push({ id: cmd.id, bitmap: cmd.bitmap });\n break;\n case \"drawImage\":\n push(OP_DRAW_IMAGE);\n push(cmd.id);\n push(cmd.dx);\n push(cmd.dy);\n push(cmd.dw);\n push(cmd.dh);\n break;\n }\n }\n\n // Exact-size copy so the transferred buffer carries no slack capacity.\n return { buffer: words.slice(0, used).buffer, strings, bitmaps };\n};\n\n/**\n * Decode a packed frame and dispatch each command straight onto `target`\n * in one pass \u2014 no intermediate {@link RenderCommand} objects.\n *\n * `images` is the worker's persistent id \u2192 bitmap LRU (mirrors the\n * recorder's same-capacity intern LRU): the caller must have registered\n * the frame's side bitmaps into it BEFORE calling. Semantics match\n * {@link replayCommands}: `resize` is a no-op (the worker owns the canvas\n * size via its own `resize` message) and a `drawImage` whose id misses the\n * cache is skipped rather than thrown.\n */\nexport const replayPackedFrame = (\n target: RenderTarget,\n buffer: ArrayBuffer,\n strings: readonly string[],\n images: LruCache<number, ImageBitmap> = new LruCache(OFFSCREEN_IMAGE_CACHE_CAP),\n): void => {\n const words = new Float64Array(buffer);\n let i = 0;\n const next = (): number => {\n const v = words[i++];\n if (v === undefined) throw new Error(\"replayPackedFrame: truncated stream\");\n return v;\n };\n const str = (idx: number): string => {\n const s = strings[idx];\n if (s === undefined) throw new Error(`replayPackedFrame: bad string index ${String(idx)}`);\n return s;\n };\n const at = <T>(table: readonly T[], code: number): T => {\n const v = table[code];\n if (v === undefined) throw new Error(`replayPackedFrame: bad enum code ${String(code)}`);\n return v;\n };\n\n while (i < words.length) {\n const op = next();\n switch (op) {\n case OP_SET_FILL: {\n const idx = next();\n target.setFill(idx === NULL_STRING_INDEX ? null : str(idx));\n break;\n }\n case OP_SET_STROKE: {\n const idx = next();\n target.setStroke(idx === NULL_STRING_INDEX ? null : str(idx));\n break;\n }\n case OP_SET_STROKE_WIDTH:\n target.setStrokeWidth(next());\n break;\n case OP_SET_OPACITY:\n target.setOpacity(next());\n break;\n case OP_SET_LINE_CAP:\n target.setLineCap(at(LINE_CAPS, next()));\n break;\n case OP_SET_LINE_JOIN:\n target.setLineJoin(at(LINE_JOINS, next()));\n break;\n case OP_SET_DASH_ARRAY: {\n const n = next();\n if (n === NULL_DASH_LENGTH) {\n target.setDashArray(null);\n } else {\n const dash: number[] = [];\n for (let d = 0; d < n; d++) dash.push(next());\n target.setDashArray(dash);\n }\n break;\n }\n case OP_SET_FONT: {\n const family = str(next());\n const size = next();\n if (next() === ABSENT) {\n target.setFont(family, size);\n break;\n }\n const weight = next();\n const style = next();\n const options: { weight?: \"normal\" | \"bold\"; style?: \"normal\" | \"italic\" } = {};\n if (weight === FONT_WEIGHT_CODE.normal) options.weight = \"normal\";\n else if (weight === FONT_WEIGHT_CODE.bold) options.weight = \"bold\";\n if (style === FONT_STYLE_CODE.normal) options.style = \"normal\";\n else if (style === FONT_STYLE_CODE.italic) options.style = \"italic\";\n target.setFont(family, size, options);\n break;\n }\n case OP_SET_TEXT_ALIGN:\n target.setTextAlign(at(TEXT_ALIGNS, next()));\n break;\n case OP_SET_TEXT_BASELINE:\n target.setTextBaseline(at(TEXT_BASELINES, next()));\n break;\n case OP_SAVE:\n target.save();\n break;\n case OP_RESTORE:\n target.restore();\n break;\n case OP_TRANSLATE:\n target.translate(next(), next());\n break;\n case OP_ROTATE:\n target.rotate(next());\n break;\n case OP_SCALE:\n target.scale(next(), next());\n break;\n case OP_SET_TRANSFORM:\n target.setTransform({\n a: next(),\n b: next(),\n c: next(),\n d: next(),\n e: next(),\n f: next(),\n });\n break;\n case OP_RESET_TRANSFORM:\n target.resetTransform();\n break;\n case OP_BEGIN_PATH:\n target.beginPath();\n break;\n case OP_CLOSE_PATH:\n target.closePath();\n break;\n case OP_MOVE_TO:\n target.moveTo(next(), next());\n break;\n case OP_LINE_TO:\n target.lineTo(next(), next());\n break;\n case OP_QUADRATIC_CURVE_TO:\n target.quadraticCurveTo(next(), next(), next(), next());\n break;\n case OP_BEZIER_CURVE_TO:\n target.bezierCurveTo(next(), next(), next(), next(), next(), next());\n break;\n case OP_RECT:\n target.rect(next(), next(), next(), next());\n break;\n case OP_ELLIPSE:\n target.ellipse(next(), next(), next(), next());\n break;\n case OP_FILL: {\n const code = next();\n if (code === FILL_RULE_CODE.nonzero) target.fill(\"nonzero\");\n else if (code === FILL_RULE_CODE.evenodd) target.fill(\"evenodd\");\n else target.fill();\n break;\n }\n case OP_STROKE:\n target.stroke();\n break;\n case OP_CLIP: {\n const code = next();\n if (code === FILL_RULE_CODE.nonzero) target.clip(\"nonzero\");\n else if (code === FILL_RULE_CODE.evenodd) target.clip(\"evenodd\");\n else target.clip();\n break;\n }\n case OP_FILL_TEXT: {\n const text = str(next());\n const x = next();\n const y = next();\n if (next() === PRESENT) target.fillText(text, x, y, next());\n else target.fillText(text, x, y);\n break;\n }\n case OP_CLEAR:\n if (next() === PRESENT) {\n const bounds: Bounds = { x: next(), y: next(), width: next(), height: next() };\n target.clear(bounds);\n } else {\n target.clear();\n }\n break;\n case OP_MARK_DIRTY:\n target.markDirty?.({ x: next(), y: next(), width: next(), height: next() });\n break;\n case OP_RESIZE:\n // No-op for replay \u2014 the worker owns the canvas size and resizes\n // via its own `resize` message, not via the command stream. Still\n // consume the args to stay in sync with the stream.\n next();\n next();\n break;\n case OP_DRAW_IMAGE: {\n // `get` bumps recency so the worker LRU evicts in lockstep with\n // the recorder's. A miss means an out-of-sync stream \u2014 skip\n // rather than throw (matches the non-drawable skip on record).\n const id = next();\n const dx = next();\n const dy = next();\n const dw = next();\n const dh = next();\n const bitmap = images.get(id);\n if (bitmap) target.drawImage(bitmap, dx, dy, dw, dh);\n break;\n }\n default:\n throw new Error(`replayPackedFrame: unknown opcode ${String(op)}`);\n }\n }\n};\n", "/// <reference lib=\"webworker\" />\nimport { LruCache, installBuiltinRenderers, renderScene } from \"@oh-just-another/renderer-core\";\nimport type { Scene } from \"@oh-just-another/scene\";\nimport type { WorkerRenderMessage, WorkerRenderResponse } from \"@oh-just-another/renderer-core\";\nimport { registerBundledFonts, type FontScope } from \"@oh-just-another/fonts\";\nimport { Canvas2DTarget } from \"./canvas2d/canvas-target.js\";\nimport { replayPackedFrame, type PackedReplayMessage } from \"./offscreen/replay-codec.js\";\nimport { OFFSCREEN_IMAGE_CACHE_CAP } from \"./constants.js\";\n\n/**\n * OffscreenCanvas render worker.\n *\n * Hosts spawn this with `new Worker(new URL(\"./render-worker.ts\",\n * import.meta.url), { type: \"module\" })`. The main thread transfers a\n * canvas via `transferCanvasToWorker(canvas, worker, { width, height,\n * dpr })`, then posts `snapshot` messages with full scenes.\n *\n * One worker owns one OffscreenCanvas \u2014 typically one per layer in a\n * `LayerWorkerPool`, so layers can be rasterised in parallel and then\n * composited on the main thread.\n */\n\ninterface WorkerState {\n canvas: OffscreenCanvas | null;\n target: Canvas2DTarget | null;\n dpr: number;\n /**\n * Bitmaps shipped by the main thread's RecordingTarget, keyed by the\n * id it assigned. Persists across `replay` messages and mirrors the\n * recorder's same-capacity LRU. Evicted clones are closed to release\n * their memory promptly (these are worker-owned copies, distinct from\n * the main thread's source bitmaps).\n */\n readonly images: LruCache<number, ImageBitmap>;\n}\n\nconst state: WorkerState = {\n canvas: null,\n target: null,\n dpr: 1,\n images: new LruCache<number, ImageBitmap>(OFFSCREEN_IMAGE_CACHE_CAP, (_id, bitmap) => {\n bitmap.close();\n }),\n};\n\nlet renderersInstalled = false;\n\nconst ensureRenderers = (): void => {\n if (renderersInstalled) return;\n installBuiltinRenderers();\n renderersInstalled = true;\n};\n\nconst post = (msg: WorkerRenderResponse, transfer?: Transferable[]): void => {\n if (transfer && transfer.length > 0) {\n (self as unknown as DedicatedWorkerGlobalScope).postMessage(msg, transfer);\n } else {\n (self as unknown as DedicatedWorkerGlobalScope).postMessage(msg);\n }\n};\n\nconst init = (canvas: OffscreenCanvas, width: number, height: number, dpr: number): void => {\n // Load the bundled fonts into the worker's font set so its Canvas2D target\n // draws the same faces as the main thread. Fire-and-forget \u2014 replays after\n // it resolves pick up the loaded fonts.\n void registerBundledFonts(self as unknown as FontScope);\n state.canvas = canvas;\n state.dpr = dpr;\n // Resize the bitmap to dpr-scaled pixels \u2014 the host's CSS size is\n // (width, height); render into the bigger buffer and let the\n // composite step downsample as needed.\n canvas.width = Math.max(1, Math.round(width * dpr));\n canvas.height = Math.max(1, Math.round(height * dpr));\n const ctx = canvas.getContext(\"2d\");\n if (!ctx) throw new Error(\"OffscreenCanvas 2D context unavailable\");\n ctx.setTransform(dpr, 0, 0, dpr, 0, 0);\n state.target = new Canvas2DTarget(ctx as unknown as CanvasRenderingContext2D, width, height, dpr);\n post({ type: \"ready\" });\n};\n\nconst resize = (width: number, height: number): void => {\n if (!state.canvas || !state.target) return;\n state.canvas.width = Math.max(1, Math.round(width * state.dpr));\n state.canvas.height = Math.max(1, Math.round(height * state.dpr));\n const ctx = state.canvas.getContext(\"2d\");\n if (!ctx) return;\n ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);\n state.target.resize(width, height, state.dpr);\n};\n\nconst snapshot = (scene: Scene): void => {\n if (!state.canvas || !state.target) {\n post({ type: \"error\", message: \"Worker not initialised\" });\n return;\n }\n ensureRenderers();\n const ctx = state.canvas.getContext(\"2d\");\n if (ctx === null) {\n post({ type: \"error\", message: \"Worker not initialised\" });\n return;\n }\n ctx.save();\n ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);\n ctx.clearRect(0, 0, state.target.size.width, state.target.size.height);\n renderScene(scene, state.target);\n ctx.restore();\n const bitmap = state.canvas.transferToImageBitmap();\n post({ type: \"frame-done\", bitmap }, [bitmap]);\n};\n\n/**\n * Replay a packed RecordingTarget command stream onto the owned\n * OffscreenCanvas. Used by the LayeredSurface \"offscreen\" backend: the\n * main thread captures every RenderTarget call into a buffer, packs it\n * via `packReplayFrame`, and ships it here per frame (numeric stream in\n * the transfer list, bitmaps cloned alongside); the worker replays.\n */\nconst replay = (msg: PackedReplayMessage): void => {\n if (!state.target) {\n post({ type: \"error\", message: \"Worker not initialised\" });\n return;\n }\n // Register this frame's bitmaps BEFORE replaying so the stream's\n // drawImage id references resolve. These are worker-owned clones \u2014\n // the LRU's evict hook closes them.\n for (const { id, bitmap } of msg.bitmaps) {\n // A re-defined id (re-captured video frame) replaces the stored clone;\n // close the old one \u2014 LruCache.set does not fire onEvict on overwrite.\n const prev = state.images.get(id);\n if (prev && prev !== bitmap) prev.close();\n state.images.set(id, bitmap);\n }\n replayPackedFrame(state.target, msg.buffer, msg.strings, state.images);\n};\n\ntype InboundMessage = WorkerRenderMessage | PackedReplayMessage;\n\n(self as unknown as DedicatedWorkerGlobalScope).addEventListener(\n \"message\",\n (ev: MessageEvent<InboundMessage>) => {\n const msg = ev.data;\n try {\n switch (msg.type) {\n case \"init\":\n init(msg.canvas as OffscreenCanvas, msg.width, msg.height, msg.dpr);\n break;\n case \"resize\":\n resize(msg.width, msg.height);\n break;\n case \"snapshot\":\n // A dpr update only takes effect on the next resize; snapshot\n // honours whatever transform init established.\n snapshot(msg.scene);\n break;\n case \"replay\":\n replay(msg);\n break;\n case \"frame\":\n // Patch-stream frames are not implemented; the protocol is\n // reserved. Reply with an error so callers don't hang on the\n // awaited response.\n post({ type: \"error\", message: \"patch-stream frames not implemented\" });\n break;\n }\n } catch (err) {\n post({\n type: \"error\",\n message: err instanceof Error ? err.message : String(err),\n });\n }\n },\n);\n"],
5
5
  "mappings": "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",
6
6
  "names": ["screenSizeOf", "bounds", "zoom", "isTextBelowLod", "fontSize", "lod", "registry", "registerElementRenderer", "type", "renderer", "getElementRenderer", "TEXT_PLACEHOLDERS", "getCornerRadius", "roundness", "width", "height", "smaller", "cutoff", "fnv1a", "s", "h", "i", "pickTextPlaceholder", "seed", "placeholders", "TEXT_PLACEHOLDERS", "total", "sum", "p", "ticket", "styleKey", "style", "entries", "v", "a", "b", "normalizeRuns", "runs", "out", "run", "last", "baseRuns", "el", "sliceRuns", "from", "to", "lo", "hi", "pos", "rStart", "rEnd", "s", "e", "paragraphCount", "text", "n", "ch", "paragraphAt", "paragraphs", "index", "listMarkers", "paragraphs", "count", "out", "counters", "i", "p", "paragraphAt", "level", "n", "key", "req", "v", "vec2_exports", "__export", "ZERO", "add", "angle", "cross", "distance", "distanceSq", "div", "dot", "equals", "length", "lengthSq", "lerp", "midpoint", "mul", "negate", "normalize", "of", "perp", "rotate", "rotateAround", "sub", "x", "y", "a", "b", "scalar", "dx", "dy", "len", "t", "radians", "c", "s", "pivot", "epsilon", "matrix_exports", "__export", "IDENTITY", "applyToBounds", "applyToPoint", "decompose", "equals", "inverse", "multiply", "of", "rotation", "scaling", "translation", "a", "b", "c", "d", "e", "f", "tx", "ty", "sx", "sy", "radians", "cos", "sin", "t", "det", "p", "p1", "p2", "p3", "p4", "minX", "minY", "maxX", "maxY", "sySigned", "epsilon", "fields", "k", "bounds_exports", "__export", "EMPTY", "centerOf", "contains", "containsBounds", "equals", "expand", "fromCenter", "fromPoints", "intersection", "intersects", "isEmpty", "maxX", "maxY", "normalize", "of", "union", "x", "y", "width", "height", "points", "minX", "minY", "maxXv", "maxYv", "p", "center", "b", "a", "xMax", "yMax", "point", "outer", "inner", "padding", "epsilon", "polygon_exports", "__export", "offsetClosedPath", "signedArea", "offsetClosedPath", "points", "distance", "p", "cx", "cy", "n", "nx", "ny", "i", "a", "req", "b", "dx", "dy", "len", "out", "prev", "n1x", "n1y", "n2x", "n2y", "bx", "by", "blen", "vertex", "towardCx", "towardCy", "sign", "cos", "miterLen", "signedArea", "s", "activeMeasurer", "getTextMeasurer", "activeMeasurer", "isText", "s", "brushBodyColor", "style", "bounderRegistry", "registerBounder", "type", "bounder", "getElementLocalBounds", "shape", "bounder", "bounderRegistry", "getElementWorldBounds", "local", "corners", "sin", "cos", "transformed", "p", "sx", "sy", "bounds_exports", "registerBounder", "s", "points", "cursor", "cmd", "lineHeight", "paragraphs", "pickTextPlaceholder", "measurer", "getTextMeasurer", "opts", "measureLine", "line", "w", "width", "lines", "minX", "minY", "maxX", "maxY", "byOrderAsc", "a", "b", "getLayersInOrder", "scene", "byOrderAsc", "getElementsInLayer", "layerId", "s", "brushOutline", "points", "n", "pos", "i", "q", "req", "halfWidth", "dir", "nrm", "raw", "vec2_exports", "d", "left", "right", "w", "c", "nn", "nPrev", "nNext", "turn", "miterSide", "arcSide", "endCap", "capArc", "startCap", "outline", "sign", "m", "cos", "arc", "fromOpposite", "a0", "radius", "a1", "delta", "steps", "out", "k", "a", "providers", "registerRenderOverflow", "type", "fn", "DEFAULT_VIEWPORT", "getWorldToScreen", "viewport", "translate", "matrix_exports", "rotate", "scale", "grayDark", "tomatoDark", "plumDark", "irisDark", "cyanDark", "grassDark", "amberDark", "gray", "tomato", "plum", "iris", "cyan", "grass", "amber", "step", "s", "key", "v", "hueLight", "name", "HUE_TONES", "tomato", "amber", "grass", "cyan", "iris", "plum", "gray", "tomatoDark", "amberDark", "grassDark", "cyanDark", "irisDark", "plumDark", "grayDark", "CANVAS_TONES", "UI_SURFACE", "UI_ACCENT", "GRID_COLOR", "GRID_DOT_COLOR", "DEFAULT_ELEMENT_STYLES", "DEFAULT_EDGE_STYLE", "DIFF_COLORS", "TEXT_PLACEHOLDER_COLOR", "UI_SURFACE", "DEFAULT_PLACEHOLDER_FILL", "TEXT_DECORATION_THICKNESS", "LIST_INDENT_EM", "LIST_MARKER_GAP_EM", "LABEL_PADDING_EM", "LABEL_AUTOFIT_MIN_PX", "LABEL_AUTOFIT_MAX_PX", "STICKY_DEFAULT_FILL", "STICKY_CORNER_RADIUS", "STICKY_AUTHOR_FONT_SIZE", "STICKY_AUTHOR_COLOR", "STICKY_SHADOW_COLOR", "STICKY_SHADOW_OFFSET_Y", "STICKY_TAG_FONT_SIZE", "STICKY_TAG_PAD_X", "STICKY_TAG_HEIGHT", "STICKY_TAG_GAP", "STICKY_TAG_BG", "STICKY_TAG_COLOR", "STICKY_REACTION_FONT_SIZE", "STICKY_REACTION_HEIGHT", "STICKY_REACTION_PAD_X", "STICKY_REACTION_GAP", "STICKY_REACTION_BG", "STICKY_REACTION_COLOR", "STICKY_REACTION_ADD_COLOR", "STICKY_REACTION_MIN_SCREEN_PX", "TEXT_UNDERLINE_OFFSET", "TEXT_STRIKETHROUGH_OFFSET", "ARROWHEAD_HEAD_RATIO", "ARROWHEAD_BODY_THICKNESS", "ARROWHEAD_RATIO_MIN", "ARROWHEAD_RATIO_MAX", "FRAME_STROKE_COLOR", "FRAME_FILL_COLOR", "FRAME_HEADER_BG_COLOR", "FRAME_HEADER_TEXT_COLOR", "DEFAULT_LINE_HEIGHT_FACTOR", "wrapParagraph", "para", "base", "maxWidth", "measure", "out", "paraIndex", "indentX", "lineBase", "first", "req", "words", "re", "m", "lineStart", "push", "start", "end", "text", "i", "w", "e", "layoutText", "options", "lineHeight", "lines", "paraStart", "attrs", "LIST_INDENT_EM", "budget", "widest", "l", "blockWidth", "lineLeftX", "lineWidth", "align", "lineLeft", "line", "registry", "animationClock", "resolveImageSource", "shape", "timestampMs", "animationClock", "adapter", "registry", "DRAWABLE_CTOR_NAMES", "isDrawableImageSource", "value", "g", "name", "ctor", "applyStyle", "style", "target", "hasFill", "hasStroke", "drawRectangle", "shape", "fill", "stroke", "r", "getCornerRadius", "buildRoundedRectPath", "offset", "strokeAlignOffset", "sx", "sy", "sw", "sh", "sr", "align", "half", "x", "y", "w", "h", "drawEllipse", "rx", "ry", "srx", "sry", "drawPolygon", "polygonPath", "pts", "polygon_exports", "first", "i", "p", "drawPath", "cmd", "stickyReactionMinZoom", "STICKY_REACTION_MIN_SCREEN_PX", "stickyReactionChromeVisible", "zoom", "stickyReactionScale", "stickyReactionLayout", "measure", "k", "gap", "STICKY_REACTION_GAP", "STICKY_REACTION_HEIGHT", "x0", "STICKY_CORNER_RADIUS", "pills", "reaction", "count", "label", "width", "STICKY_REACTION_PAD_X", "drawSticky", "shape", "target", "ctx", "fill", "STICKY_DEFAULT_FILL", "w", "h", "r", "STICKY_CORNER_RADIUS", "STICKY_SHADOW_COLOR", "buildRoundedRectPath", "STICKY_SHADOW_OFFSET_Y", "STICKY_TAG_FONT_SIZE", "x", "STICKY_TAG_HEIGHT", "tag", "tw", "STICKY_TAG_PAD_X", "STICKY_TAG_BG", "STICKY_TAG_COLOR", "STICKY_TAG_GAP", "STICKY_AUTHOR_FONT_SIZE", "STICKY_AUTHOR_COLOR", "authorY", "zoom", "k", "stickyReactionScale", "chromeVisible", "stickyReactionChromeVisible", "drawReactions", "drawAdd", "STICKY_REACTION_FONT_SIZE", "layout", "stickyReactionLayout", "t", "pill", "STICKY_REACTION_BG", "STICKY_REACTION_COLOR", "STICKY_REACTION_PAD_X", "STICKY_REACTION_HEIGHT", "add", "cx", "cy", "arm", "bar", "STICKY_REACTION_ADD_COLOR", "drawEmoji", "autoFitCache", "autoFitFontSize", "text", "boxW", "boxH", "measure", "baseSize", "paragraphs", "fingerprint", "key", "cached", "fits", "size", "pad", "LABEL_PADDING_EM", "maxWidth", "layoutText", "lo", "LABEL_AUTOFIT_MIN_PX", "hi", "LABEL_AUTOFIT_MAX_PX", "mid", "shapeLabelLayout", "label", "bounds", "getElementLocalBounds", "fontSize", "scaledMeasure", "valign", "style", "innerHeight", "clipLines", "rawScroll", "maxScroll", "scroll", "synthetic", "textH", "offsetY", "drawShapeLabel", "lod", "isTextBelowLod", "weight", "fontStyle", "placed", "s", "drawText", "clipWindowOf", "hint", "start", "drawListMarkersForLayout", "color", "align", "markers", "listMarkers", "paragraphCount", "gap", "LIST_MARKER_GAP_EM", "markerClip", "line", "i", "marker", "left", "lineLeft", "drawStyledText", "setSegFont", "st", "perLine", "segs", "sliceRuns", "total", "a", "blockWidth", "m", "l", "req", "thickness", "TEXT_DECORATION_THICKNESS", "styledClip", "top", "indentX", "seg", "opacity", "highlight", "deco", "TEXT_UNDERLINE_OFFSET", "TEXT_STRIKETHROUGH_OFFSET", "_runs", "plain", "pickTextPlaceholder", "TEXT_PLACEHOLDER_COLOR", "lines", "clip", "_", "lineHeight", "DEFAULT_LINE_HEIGHT_FACTOR", "drawBrush", "pts", "p", "paint", "brushBodyColor", "outline", "brushOutline", "first", "drawImage", "handle", "resolveImageSource", "isDrawableImageSource", "dynamic", "mask", "buildImageMaskPath", "width", "height", "installBuiltinRenderers", "registerElementRenderer", "drawRectangle", "drawEllipse", "drawPolygon", "drawPath", "drawFrame", "registerRenderOverflow", "drawBlockArrow", "n", "kMax", "stickyReactionMinZoom", "STICKY_REACTION_GAP", "stroke", "applyStyle", "direction", "headRatio", "ARROWHEAD_RATIO_MIN", "ARROWHEAD_RATIO_MAX", "ARROWHEAD_HEAD_RATIO", "bodyT", "ARROWHEAD_BODY_THICKNESS", "headW", "bodyW", "bodyHalfH", "points", "y", "rotateLocal", "ptObjs", "polygonPath", "offset", "strokeAlignOffset", "sPts", "polygon_exports", "FRAME_HEADER_ELLIPSIS", "ellipsizeToWidth", "FRAME_FILL_COLOR", "screenScale", "FRAME_STROKE_COLOR", "name", "avail", "headerWidth", "FRAME_HEADER_BG_COLOR", "FRAME_HEADER_TEXT_COLOR", "createDimTarget", "inner", "factor", "cache", "scaledSetOpacity", "a", "handler", "target", "prop", "value", "resolved", "ElementCache", "shape", "entry", "value", "compute", "cached", "id", "scene", "sharedBoundsCache", "cachedWorldBounds", "cache", "getElementWorldBounds", "LruCache", "cap", "onEvict", "key", "v", "value", "oldest", "ev", "zoomBucket", "zoom", "renderScene", "scene", "target", "options", "w2s", "getWorldToScreen", "dirtyWorld", "corners", "matrix_exports", "screen", "bounds_exports", "boundsCache", "ElementCache", "viewport", "candidates", "zoom", "clock", "ctx", "lod", "placeholderMax", "placeholderFill", "DEFAULT_PLACEHOLDER_FILL", "layerCache", "dirtyLayers", "compositeLayerBitmap", "zoomBucket", "layerBoundsFor", "layerId", "acc", "shape", "getElementsInLayer", "bb", "cachedWorldBounds", "dimOpacity", "dimTarget", "createDimTarget", "layer", "getLayersInOrder", "bitmap", "fresh", "isText", "isTextBelowLod", "screenSizeOf", "renderer", "getElementRenderer", "dimmed", "draw", "drawShapeLabel", "FONT_SANS", "FONT_SERIF", "FONT_MONO", "resolveBundledFamily", "cssFamily", "f", "FACES", "registerBundledFonts", "scope", "set", "face", "intrinsicImageSize", "source", "s", "warnedImageKinds", "warnSkippedImage", "value", "kind", "Canvas2DTarget", "ctx", "width", "height", "dpr", "color", "alpha", "cap", "join", "dash", "fontFamily", "fontSize", "options", "style", "weight", "resolveBundledFamily", "align", "baseline", "dx", "dy", "radians", "sx", "sy", "t", "d", "x", "y", "cx", "cy", "c1x", "c1y", "c2x", "c2y", "rx", "ry", "rule", "text", "maxWidth", "image", "dw", "dh", "_dynamic", "crop", "isDrawableImageSource", "warnSkippedImage", "size", "intrinsicImageSize", "bounds", "minX", "minY", "maxX", "maxY", "OP_SET_FILL", "OP_SET_STROKE", "OP_SET_STROKE_WIDTH", "OP_SET_OPACITY", "OP_SET_LINE_CAP", "OP_SET_LINE_JOIN", "OP_SET_DASH_ARRAY", "OP_SET_FONT", "OP_SET_TEXT_ALIGN", "OP_SET_TEXT_BASELINE", "OP_SAVE", "OP_RESTORE", "OP_TRANSLATE", "OP_ROTATE", "OP_SCALE", "OP_SET_TRANSFORM", "OP_RESET_TRANSFORM", "OP_BEGIN_PATH", "OP_CLOSE_PATH", "OP_MOVE_TO", "OP_LINE_TO", "OP_QUADRATIC_CURVE_TO", "OP_BEZIER_CURVE_TO", "OP_RECT", "OP_ELLIPSE", "OP_FILL", "OP_STROKE", "OP_FILL_TEXT", "OP_CLEAR", "OP_MARK_DIRTY", "OP_RESIZE", "OP_DRAW_IMAGE", "OP_CLIP", "NULL_STRING_INDEX", "NULL_DASH_LENGTH", "LINE_CAPS", "LINE_JOINS", "TEXT_ALIGNS", "TEXT_BASELINES", "FILL_RULE_CODE", "FONT_WEIGHT_CODE", "FONT_STYLE_CODE", "ABSENT", "PRESENT", "replayPackedFrame", "target", "buffer", "strings", "images", "LruCache", "words", "i", "next", "v", "str", "idx", "s", "at", "table", "code", "op", "OP_SET_FILL", "NULL_STRING_INDEX", "OP_SET_STROKE", "OP_SET_STROKE_WIDTH", "OP_SET_OPACITY", "OP_SET_LINE_CAP", "LINE_CAPS", "OP_SET_LINE_JOIN", "LINE_JOINS", "OP_SET_DASH_ARRAY", "n", "NULL_DASH_LENGTH", "dash", "d", "OP_SET_FONT", "family", "size", "ABSENT", "weight", "style", "options", "FONT_WEIGHT_CODE", "FONT_STYLE_CODE", "OP_SET_TEXT_ALIGN", "TEXT_ALIGNS", "OP_SET_TEXT_BASELINE", "TEXT_BASELINES", "OP_SAVE", "OP_RESTORE", "OP_TRANSLATE", "OP_ROTATE", "OP_SCALE", "OP_SET_TRANSFORM", "OP_RESET_TRANSFORM", "OP_BEGIN_PATH", "OP_CLOSE_PATH", "OP_MOVE_TO", "OP_LINE_TO", "OP_QUADRATIC_CURVE_TO", "OP_BEZIER_CURVE_TO", "OP_RECT", "OP_ELLIPSE", "OP_FILL", "FILL_RULE_CODE", "OP_STROKE", "OP_CLIP", "OP_FILL_TEXT", "text", "x", "y", "PRESENT", "OP_CLEAR", "bounds", "OP_MARK_DIRTY", "OP_RESIZE", "OP_DRAW_IMAGE", "id", "dx", "dy", "dw", "dh", "bitmap", "state", "LruCache", "_id", "bitmap", "renderersInstalled", "ensureRenderers", "installBuiltinRenderers", "post", "msg", "transfer", "init", "canvas", "width", "height", "dpr", "registerBundledFonts", "ctx", "Canvas2DTarget", "resize", "snapshot", "scene", "renderScene", "replay", "id", "prev", "replayPackedFrame", "ev", "err"]
7
7
  }