packages/core/src/lib/textures/texture-protocol.ts
This is the source snapshot used to build these API details. View this revision on GitHub.
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 version
Section titled “Documentation version”Documentation built with @pibbl/core 0.0.2, revision 2dccb19. ALPHA — NOT FOR PRODUCTION USE.