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