Skip to content

packages/core/src/lib/textures/texture-protocol.ts

Read as Markdown

This is the source snapshot used to build these API details. View this revision on GitHub.

Back to reference

1 import type { FillStyle, StrokeStyle } from '../types.js';
2 import type { Texture, TextureDefinition } from './texture.js';
3 import type { RenderingContext2D } from "../types.js";
4 
5 declare const textureBrand: unique symbol;
6 
7 /**
8  * How a texture raster is positioned and scaled when used as paint.
9  *
10  * @see {@link Texture}
11  * @see {@link TextureDefinition}
12  */
13 export interface TexturePlacement {
14   /** Selects the supported mapping, placement, or result policy. See {@link TexturePlacement}. */
15   readonly mode?: "repeat" | "stretch";
16   /** Width of one texture tile in logical paint coordinates. See {@link TexturePlacement}. */
17   readonly tileWidth?: number;
18   /** Height of one texture tile in logical paint coordinates. See {@link TexturePlacement}. */
19   readonly tileHeight?: number;
20   /** Horizontal offset applied to the texture or shadow. See {@link TexturePlacement}. */
21   readonly offsetX?: number;
22   /** Vertical offset applied to the texture or shadow. See {@link TexturePlacement}. */
23   readonly offsetY?: number;
24   /** Rotation applied to the containing geometry. See {@link TexturePlacement}. */
25   readonly rotation?: number;
26 }
27 
28 /**
29  * Opaque paint value produced by a mounted texture owner.
30  *
31  * @see {@link FillStyle}
32  * @see {@link StrokeStyle}
33  * @see {@link Texture}
34  */
35 export interface TexturePaint {
36   readonly [textureBrand]: true;
37 }
38 
39 /**
40  * A temporary raster view valid only during the borrowing callback.
41  *
42  * @see {@link TexturePlacement}
43  * @see {@link borrowTextureRaster}
44  */
45 export interface BorrowedTextureRaster {
46   /** Borrowed raster image; valid only within the borrowing callback. See {@link BorrowedTextureRaster}. */
47   readonly source: OffscreenCanvas;
48   /**
49    * Horizontal extent in the units of the containing geometry or surface. See
50    * {@link BorrowedTextureRaster}.
51    */
52   readonly width: number;
53   /**
54    * Vertical extent in the units of the containing geometry or surface. See
55    * {@link BorrowedTextureRaster}.
56    */
57   readonly height: number;
58   /**
59    * Revision used to detect changes to the underlying state or geometry. See
60    * {@link BorrowedTextureRaster}.
61    */
62   readonly revision: number;
63   /** Resource identity underlying the borrowed raster. See {@link BorrowedTextureRaster}. */
64   readonly resource: object;
65   /**
66    * Preferred placement relative to the anchor or containing geometry. See
67    * {@link TexturePlacement}.
68    */
69   readonly placement: Readonly<TexturePlacement>;
70 }
71 
72 interface TexturePaintCapability {
73   validate(): void;
74   resolvePaint(
75     ctx: RenderingContext2D,
76     bounds: { x: number; y: number; width: number; height: number },
77   ): string | CanvasGradient | CanvasPattern;
78   borrow<T>(callback: (raster: BorrowedTextureRaster) => T): T;
79 }
80 
81 const capabilities = new WeakMap<object, TexturePaintCapability>();
82 
83 /** @internal Registers a value-owned capability; there is no producer registry. */
84 export function registerTexturePaint(
85   value: TexturePaint,
86   capability: TexturePaintCapability,
87 ): void {
88   capabilities.set(value, capability);
89 }
90 
91 /**
92  * @internal Borrows only committed pixels for one synchronous consumer action.
93  *
94  * @param value - Texture paint descriptor to validate. See {@link TexturePaint}.
95  *
96  * @see {@link TexturePaint}
97  */
98 export function validateTexturePaint(value: TexturePaint): void {
99   const capability =
100     typeof value === "object" && value !== null ? capabilities.get(value) : undefined;
101   if (!capability) throw new TypeError("Expected a Pibbl texture paint value.");
102   capability.validate();
103 }
104 
105 /**
106  * Borrows a texture raster for synchronous use without transferring ownership to the caller.
107  *
108  * @param value - Texture paint whose raster is borrowed. See {@link TexturePaint}.
109  * @param callback - Synchronous callback using the raster while its lease is active. See
110  * {@link BorrowedTextureRaster} .
111  * @returns The callback's return value; the raster lease ends when the callback returns.
112  *
113  * @see {@link TexturePaint}
114  * @see {@link BorrowedTextureRaster}
115  */
116 export function borrowTextureRaster<T>(
117   value: TexturePaint,
118   callback: (raster: BorrowedTextureRaster) => T,
119 ): T {
120   const capability =
121     typeof value === "object" && value !== null ? capabilities.get(value) : undefined;
122   if (!capability) throw new TypeError("Expected a Pibbl texture paint value.");
123   return capability.borrow(callback);
124 }
125 
126 /**
127  * Internal paint resolution shared by Canvas built-ins.
128  *
129  * @param value - Native Canvas paint or Pibbl texture placement. See {@link TexturePaint}.
130  * @param ctx - Canvas context that will consume the resolved paint. See {@link RenderingContext2D}
131  * .
132  * @param bounds - Destination bounds in local logical coordinates.
133  * @returns Native Canvas paint ready to assign to the drawing context.
134  *
135  * @see {@link TexturePaint}
136  * @see {@link RenderingContext2D}
137  */
138 export function resolveTexturePaint(
139   value: string | CanvasGradient | CanvasPattern | TexturePaint,
140   ctx: RenderingContext2D,
141   bounds: { x: number; y: number; width: number; height: number },
142 ): string | CanvasGradient | CanvasPattern {
143   const capability =
144     typeof value === "object" && value !== null ? capabilities.get(value) : undefined;
145   return capability ? capability.resolvePaint(ctx, bounds) : value as string | CanvasGradient | CanvasPattern;
146 }
147 
148 /** Internal Rectangle-only fast path for the exact simple stretch-fill case. */
149 export function drawSimpleStretchedTexture(
150   value: string | CanvasGradient | CanvasPattern | TexturePaint | undefined,
151   ctx: RenderingContext2D,
152   bounds: { x: number; y: number; width: number; height: number },
153 ): boolean {
154   const capability = typeof value === "object" && value !== null ? capabilities.get(value) : undefined;
155   if (!capability || ctx.globalCompositeOperation !== "source-over" || ctx.filter !== "none" ||
156     ctx.shadowBlur !== 0 || ctx.shadowOffsetX !== 0 || ctx.shadowOffsetY !== 0 ||
157     ctx.shadowColor !== "rgba(0, 0, 0, 0)") return false;
158   // CanvasPattern and drawImage use different sampling rules when either is
159   // scaled, especially around transparent source pixels. Keep the
160   // allocation-free path to the exact 1:1, pixel-aligned case only.
161   const transform = ctx.getTransform();
162   if (!Number.isInteger(bounds.x) || !Number.isInteger(bounds.y) ||
163     !Number.isInteger(bounds.width) || !Number.isInteger(bounds.height) ||
164     bounds.width <= 0 || bounds.height <= 0 || transform.b !== 0 || transform.c !== 0 ||
165     transform.a !== 1 || transform.d !== 1 ||
166     !Number.isInteger(transform.e) || !Number.isInteger(transform.f)) return false;
167   capability.validate();
168   return capability.borrow(raster => {
169     const placement = raster.placement;
170     if (placement.mode !== "stretch" || placement.offsetX || placement.offsetY || placement.rotation) return false;
171     if (raster.width !== bounds.width || raster.height !== bounds.height) return false;
172     ctx.drawImage(raster.source, bounds.x, bounds.y, bounds.width, bounds.height);
173     return true;
174   });
175 }
176 

Documentation built with @pibbl/core 0.0.2, revision 272a94a. ALPHA — NOT FOR PRODUCTION USE.