Skip to content

packages/core/src/lib/components/img.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 { resolveTexturePaint } from '../textures/texture-protocol.js';
2 import { definePrimitive } from '../define-primitive.js';
3 import type {
4   PibblEventFocusOptions,
5   PibblEventHandlers,
6   PibblEventParticipationOptions,
7 } from '../events/types.js';
8 import { useRectPath } from '../hooks/use-rect-path.js';
9 import { useReaction } from '../hooks/use-reaction.js';
10 import { useSignal } from '../hooks/use-signal.js';
11 import type {
12   PibblPhysics2DPropExtension,
13   MeasureInput,
14   MeasureResult,
15   RenderingContext2D,
16   StyleResolutionContext,
17 } from '../types.js';
18 import { resolveLength } from '../style/resolve-length.js';
19 import {
20   resolveOptionalLength,
21   stripLayoutParticipation,
22 } from '../style/resolve-built-in.js';
23 import { registerOpaqueStyleProperties } from '../style/resolve-dispatch.js';
24 import type { ImgStyle, ResolvedImgStyle } from '../style/types.js';
25 import { adjustRectangleToPixelGrid } from '../util/pixel-grid-adjustments.js';
26 import { wireCommonEvents } from '../util/wire-common-events.js';
27 
28 /**
29  * Authored inputs for Image, including the declared data and presentation options.
30  *
31  * @see {@link PibblEventHandlers}
32  * @see {@link PibblEventParticipationOptions}
33  * @see {@link PibblPhysics2DPropExtension}
34  * @see {@link Image}
35  */
36 export interface ImageProps extends PibblEventHandlers, PibblEventFocusOptions, PibblEventParticipationOptions, PibblPhysics2DPropExtension {}
37 
38 interface LoadedImage {
39   src: string;
40   element: HTMLImageElement;
41 }
42 
43 function renderImage(
44   props: ImageProps,
45   style: Readonly<ResolvedImgStyle>,
46   ctx: RenderingContext2D,
47 ) {
48   let {
49     src,
50     x = 0,
51     y = 0,
52     width,
53     height,
54     stroke,
55     strokeWidth,
56     opacity,
57     cursor,
58     alignToPixelGrid,
59   } = style;
60 
61   if (opacity !== undefined) {
62     ctx.globalAlpha = ctx.globalAlpha * opacity;
63   }
64 
65   const loadedImage = useSignal<LoadedImage | null>(null);
66   const loadedImageValue = loadedImage.get();
67   const image =
68     loadedImageValue && loadedImageValue.src === src ?
69       loadedImageValue.element : null;
70 
71   useReaction(() => {
72     let active = true;
73     const BrowserImage = globalThis.Image;
74     const element = new BrowserImage();
75     const settle = () => {
76       active = false;
77       element.onload = null;
78       element.onerror = null;
79     };
80 
81     element.onload = () => {
82       if (!active) return;
83       settle();
84       loadedImage.set({ src, element });
85     };
86     element.onerror = () => {
87       if (!active) return;
88       settle();
89       loadedImage.set(null);
90     };
91     element.src = src;
92 
93     return () => {
94       active = false;
95       element.onload = null;
96       element.onerror = null;
97       element.removeAttribute('src');
98     };
99   }, [src]);
100 
101   width = width ?? image?.naturalWidth ?? image?.width ?? 0;
102   height = height ?? image?.naturalHeight ?? image?.height ?? 0;
103 
104   if (alignToPixelGrid) {
105     ({ x, y, width, height } = adjustRectangleToPixelGrid(
106       strokeWidth ?? 0,
107       x,
108       y,
109       width,
110       height,
111       alignToPixelGrid,
112     ));
113   }
114 
115   const imagePath = useRectPath(x, y, width, height);
116 
117   if (image) {
118     ctx.drawImage(image, x, y, width, height);
119   }
120 
121   if (stroke && strokeWidth) {
122     ctx.strokeStyle = resolveTexturePaint(stroke, ctx, { x, y, width, height });
123     ctx.lineWidth = strokeWidth;
124     ctx.stroke(imagePath);
125   }
126 
127   wireCommonEvents(
128     props,
129     imagePath,
130     true,
131     strokeWidth ?? 0,
132     cursor,
133     { x, y, width, height },
134   );
135 }
136 
137 /**
138  * Describes a Canvas image primitive for JSX or createElement authoring.
139  *
140  * @param props - Authored component inputs, supplied through JSX or createElement. See the linked
141  * props and style types.
142  * @throws When called directly; Pibbl mounts this component through JSX or createElement.
143  *
144  * @see {@link ImageProps}
145  * @see {@link ImgStyle}
146  */
147 export const Image = definePrimitive<
148   ImageProps,
149   ImgStyle,
150   ImgStyle,
151   ResolvedImgStyle
152 >(renderImage, {
153   resolveStyle: resolveImageStyle,
154   measure: measureImage,
155 });
156 registerOpaqueStyleProperties(Image, ['src', 'stroke', 'cursor']);
157 
158 function resolveImageStyle(
159   style: Readonly<ImgStyle>,
160   context: Readonly<StyleResolutionContext>,
161 ): ResolvedImgStyle {
162   return {
163     ...stripLayoutParticipation(style),
164     x: 0,
165     y: 0,
166     width: resolveOptionalLength(
167       style.width,
168       context.percentageBasis.width,
169       'width',
170       context,
171     ),
172     height: resolveOptionalLength(
173       style.height,
174       context.percentageBasis.height,
175       'height',
176       context,
177     ),
178   } as ResolvedImgStyle;
179 }
180 
181 function measureImage({
182   style,
183   constraints,
184 }: MeasureInput<ImageProps, ImgStyle>): MeasureResult {
185   if (
186     style.width === undefined || style.width === 'auto' ||
187     style.height === undefined || style.height === 'auto'
188   ) {
189     return {
190       status: 'unsupported',
191       reason: 'Image natural dimensions are unavailable during pure measurement',
192     };
193   }
194   const context = {
195     component: 'Image',
196     algorithm: 'measurement' as const,
197     constraints,
198   };
199   return {
200     status: 'measured',
201     size: {
202       width: resolveLength(
203         style.width,
204         constraints.maxWidth,
205         'width',
206         context,
207       )!,
208       height: resolveLength(
209         style.height,
210         constraints.maxHeight,
211         'height',
212         context,
213       )!,
214     },
215   };
216 }
217 

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