@vertz/ui-canvas 0.1.0 → 0.2.0
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.
- package/dist/index.d.ts +214 -13
- package/dist/index.js +396 -16
- package/package.json +10 -8
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { DisposeFn, Signal } from "@vertz/ui";
|
|
2
|
+
import { Application, Container as Container2 } from "pixi.js";
|
|
3
3
|
interface CanvasOptions {
|
|
4
4
|
width: number;
|
|
5
5
|
height: number;
|
|
6
6
|
backgroundColor?: number;
|
|
7
7
|
}
|
|
8
8
|
interface CanvasState {
|
|
9
|
+
canvas: HTMLCanvasElement;
|
|
9
10
|
app: Application;
|
|
10
|
-
|
|
11
|
+
stage: Container2;
|
|
12
|
+
dispose: DisposeFn;
|
|
11
13
|
}
|
|
12
14
|
/**
|
|
13
15
|
* Binds a Vertz signal to a PixiJS display object property reactively.
|
|
@@ -43,15 +45,9 @@ declare function createReactiveSprite(options: {
|
|
|
43
45
|
/**
|
|
44
46
|
* Renders a PixiJS canvas to the specified container element.
|
|
45
47
|
* Returns the canvas element and a dispose function for cleanup.
|
|
48
|
+
* Async because PixiJS v8 requires `app.init()` for initialization.
|
|
46
49
|
*/
|
|
47
|
-
declare function render(container: HTMLElement, options: CanvasOptions):
|
|
48
|
-
canvas: HTMLCanvasElement;
|
|
49
|
-
dispose: DisposeFn;
|
|
50
|
-
};
|
|
51
|
-
/**
|
|
52
|
-
* Destroy a PixiJS application and remove its canvas from the DOM.
|
|
53
|
-
*/
|
|
54
|
-
declare function destroy(app: Application, container: HTMLElement): void;
|
|
50
|
+
declare function render(container: HTMLElement, options: CanvasOptions): Promise<CanvasState>;
|
|
55
51
|
/**
|
|
56
52
|
* Canvas renderer for Vertz with PixiJS integration.
|
|
57
53
|
* Provides reactive primitives for canvas rendering.
|
|
@@ -60,6 +56,211 @@ declare const Canvas: {
|
|
|
60
56
|
render: typeof render;
|
|
61
57
|
bindSignal: typeof bindSignal;
|
|
62
58
|
createReactiveSprite: typeof createReactiveSprite;
|
|
63
|
-
destroy: typeof destroy;
|
|
64
59
|
};
|
|
65
|
-
|
|
60
|
+
import { Container as Container3 } from "pixi.js";
|
|
61
|
+
type DisposeFn2 = () => void;
|
|
62
|
+
/**
|
|
63
|
+
* Conditionally renders a canvas display object based on a reactive condition.
|
|
64
|
+
* When the condition is true, the factory creates a display object and adds it to parent.
|
|
65
|
+
* When false, the display object is removed and destroyed.
|
|
66
|
+
* An optional fallback factory is shown when the condition is false.
|
|
67
|
+
*
|
|
68
|
+
* Each branch is rendered in its own disposal scope so that effects created
|
|
69
|
+
* inside branches are properly cleaned up when the condition changes.
|
|
70
|
+
*
|
|
71
|
+
* Canvas equivalent of @vertz/ui's DOM `__conditional()`.
|
|
72
|
+
*
|
|
73
|
+
* @param parent - The parent Container.
|
|
74
|
+
* @param condition - Accessor returning a boolean.
|
|
75
|
+
* @param factory - Creates the display object when condition is true.
|
|
76
|
+
* @param fallbackFactory - Optional: creates a display object when condition is false.
|
|
77
|
+
* @returns A dispose function that removes and destroys the current display object.
|
|
78
|
+
*/
|
|
79
|
+
declare function canvasConditional(parent: Container3, condition: () => boolean, factory: () => Container3, fallbackFactory?: () => Container3): DisposeFn2;
|
|
80
|
+
import { Context } from "@vertz/ui";
|
|
81
|
+
import { Container as Container4 } from "pixi.js";
|
|
82
|
+
/**
|
|
83
|
+
* Context that provides the current PixiJS Container (typically the stage)
|
|
84
|
+
* to canvas children. Canvas intrinsic elements use this to know which
|
|
85
|
+
* container they should add themselves to.
|
|
86
|
+
*/
|
|
87
|
+
declare const CanvasRenderContext: Context<Container4 | null>;
|
|
88
|
+
interface CanvasLayerProps {
|
|
89
|
+
width: number;
|
|
90
|
+
height: number;
|
|
91
|
+
background?: number;
|
|
92
|
+
debug?: boolean;
|
|
93
|
+
children?: unknown;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Bridge component that embeds a PixiJS canvas inside the DOM tree.
|
|
97
|
+
* Creates a PixiJS Application, provides the stage via CanvasRenderContext,
|
|
98
|
+
* and processes canvas children.
|
|
99
|
+
*
|
|
100
|
+
* Nesting CanvasLayer inside another CanvasLayer is forbidden and throws.
|
|
101
|
+
*/
|
|
102
|
+
declare function CanvasLayer(props: CanvasLayerProps): HTMLDivElement;
|
|
103
|
+
import { Container as Container5 } from "pixi.js";
|
|
104
|
+
type DisposeFn3 = () => void;
|
|
105
|
+
/**
|
|
106
|
+
* Reactively manages a list of canvas children inside a parent Container.
|
|
107
|
+
* When the items signal changes, children are added/removed/reordered.
|
|
108
|
+
* Each item gets its own disposal scope for cleanup.
|
|
109
|
+
*
|
|
110
|
+
* Canvas equivalent of @vertz/ui's DOM `__list()`.
|
|
111
|
+
*
|
|
112
|
+
* @param parent - The parent Container to manage children on.
|
|
113
|
+
* @param items - Accessor returning the current array of items.
|
|
114
|
+
* @param renderFn - Factory that creates a display object for an item.
|
|
115
|
+
* @param keyFn - Extracts a unique key from an item for identity tracking.
|
|
116
|
+
* @returns A dispose function that removes and destroys all managed children.
|
|
117
|
+
*/
|
|
118
|
+
declare function canvasList<T>(parent: Container5, items: () => T[], renderFn: (item: T) => Container5, keyFn: (item: T) => string | number): DisposeFn3;
|
|
119
|
+
import { Container as Container6 } from "pixi.js";
|
|
120
|
+
/**
|
|
121
|
+
* Creates a debug overlay that draws wireframe bounding boxes and labels
|
|
122
|
+
* for all display objects in the stage. Toggle via the `debug` prop on CanvasLayer.
|
|
123
|
+
*/
|
|
124
|
+
declare function createDebugOverlay(stage: Container6): {
|
|
125
|
+
overlay: Container6;
|
|
126
|
+
update: () => void;
|
|
127
|
+
destroy: () => void;
|
|
128
|
+
};
|
|
129
|
+
import { Container as Container7 } from "pixi.js";
|
|
130
|
+
/** Check if a tag name is a known canvas intrinsic element. */
|
|
131
|
+
declare function isCanvasIntrinsic(tag: string): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* Create a PixiJS display object from a canvas intrinsic tag and props.
|
|
134
|
+
* Handles static props, reactive (accessor) props, event binding,
|
|
135
|
+
* Graphics draw callbacks, children processing, ref escape hatch,
|
|
136
|
+
* and disposal cleanup.
|
|
137
|
+
*/
|
|
138
|
+
declare function jsxCanvas(tag: string, props: Record<string, unknown>): Container7;
|
|
139
|
+
import { Container as Container8 } from "pixi.js";
|
|
140
|
+
/** Reactive prop: can be a static value or accessor function. */
|
|
141
|
+
type MaybeAccessor<T> = T | (() => T);
|
|
142
|
+
/**
|
|
143
|
+
* Unwrap a MaybeAccessor to its underlying value.
|
|
144
|
+
* If the value is a function, call it to get the result.
|
|
145
|
+
* If it's a static value, return it directly.
|
|
146
|
+
*/
|
|
147
|
+
declare function unwrap<T>(value: MaybeAccessor<T>): T;
|
|
148
|
+
interface CircleProps {
|
|
149
|
+
x?: MaybeAccessor<number>;
|
|
150
|
+
y?: MaybeAccessor<number>;
|
|
151
|
+
radius: MaybeAccessor<number>;
|
|
152
|
+
fill: MaybeAccessor<number>;
|
|
153
|
+
}
|
|
154
|
+
interface RectProps {
|
|
155
|
+
x?: MaybeAccessor<number>;
|
|
156
|
+
y?: MaybeAccessor<number>;
|
|
157
|
+
width: MaybeAccessor<number>;
|
|
158
|
+
height: MaybeAccessor<number>;
|
|
159
|
+
fill: MaybeAccessor<number>;
|
|
160
|
+
stroke?: MaybeAccessor<number>;
|
|
161
|
+
strokeWidth?: MaybeAccessor<number>;
|
|
162
|
+
}
|
|
163
|
+
interface LineProps {
|
|
164
|
+
x?: MaybeAccessor<number>;
|
|
165
|
+
y?: MaybeAccessor<number>;
|
|
166
|
+
from: {
|
|
167
|
+
x: number;
|
|
168
|
+
y: number;
|
|
169
|
+
};
|
|
170
|
+
to: {
|
|
171
|
+
x: number;
|
|
172
|
+
y: number;
|
|
173
|
+
};
|
|
174
|
+
stroke: MaybeAccessor<number>;
|
|
175
|
+
strokeWidth?: MaybeAccessor<number>;
|
|
176
|
+
}
|
|
177
|
+
interface EllipseProps {
|
|
178
|
+
x?: MaybeAccessor<number>;
|
|
179
|
+
y?: MaybeAccessor<number>;
|
|
180
|
+
halfWidth: MaybeAccessor<number>;
|
|
181
|
+
halfHeight: MaybeAccessor<number>;
|
|
182
|
+
fill: MaybeAccessor<number>;
|
|
183
|
+
}
|
|
184
|
+
/** Create a circle shape as a Graphics element. */
|
|
185
|
+
declare function Circle(props: CircleProps): Container8;
|
|
186
|
+
/** Create a rectangle shape as a Graphics element. */
|
|
187
|
+
declare function Rect(props: RectProps): Container8;
|
|
188
|
+
/** Create a line shape as a Graphics element. */
|
|
189
|
+
declare function Line(props: LineProps): Container8;
|
|
190
|
+
/** Create an ellipse shape as a Graphics element. */
|
|
191
|
+
declare function Ellipse(props: EllipseProps): Container8;
|
|
192
|
+
import { Sprite as Sprite2 } from "pixi.js";
|
|
193
|
+
/**
|
|
194
|
+
* Load a texture asynchronously and assign it to a Sprite.
|
|
195
|
+
* The sprite is hidden until the texture loads successfully.
|
|
196
|
+
*
|
|
197
|
+
* - On success: sets `sprite.texture` and makes the sprite visible.
|
|
198
|
+
* - On failure: logs a warning, sprite remains invisible.
|
|
199
|
+
* - If the sprite is destroyed before the texture loads, the assignment is skipped.
|
|
200
|
+
*
|
|
201
|
+
* @param sprite - The Sprite to load the texture into.
|
|
202
|
+
* @param path - The texture asset path (resolved by PixiJS Assets).
|
|
203
|
+
* @returns A promise that resolves when loading completes (success or failure).
|
|
204
|
+
*/
|
|
205
|
+
declare function loadSpriteTexture(sprite: Sprite2, path: string): Promise<void>;
|
|
206
|
+
import { FederatedPointerEvent, FederatedWheelEvent, Container as PIXIContainer, TextStyleOptions } from "pixi.js";
|
|
207
|
+
import { Graphics as Graphics_2qg1pn } from "pixi.js";
|
|
208
|
+
/** Common transform props shared by all canvas elements. */
|
|
209
|
+
interface CanvasTransformProps {
|
|
210
|
+
x?: MaybeAccessor<number>;
|
|
211
|
+
y?: MaybeAccessor<number>;
|
|
212
|
+
rotation?: MaybeAccessor<number>;
|
|
213
|
+
alpha?: MaybeAccessor<number>;
|
|
214
|
+
scale?: MaybeAccessor<number>;
|
|
215
|
+
visible?: MaybeAccessor<boolean>;
|
|
216
|
+
ref?: (obj: PIXIContainer) => void;
|
|
217
|
+
}
|
|
218
|
+
/** Pointer event handlers for interactive canvas elements. */
|
|
219
|
+
interface CanvasEventProps {
|
|
220
|
+
onPointerDown?: (e: FederatedPointerEvent) => void;
|
|
221
|
+
onPointerUp?: (e: FederatedPointerEvent) => void;
|
|
222
|
+
onPointerMove?: (e: FederatedPointerEvent) => void;
|
|
223
|
+
onPointerOver?: (e: FederatedPointerEvent) => void;
|
|
224
|
+
onPointerOut?: (e: FederatedPointerEvent) => void;
|
|
225
|
+
onPointerEnter?: (e: FederatedPointerEvent) => void;
|
|
226
|
+
onPointerLeave?: (e: FederatedPointerEvent) => void;
|
|
227
|
+
onClick?: (e: FederatedPointerEvent) => void;
|
|
228
|
+
onRightClick?: (e: FederatedPointerEvent) => void;
|
|
229
|
+
onWheel?: (e: FederatedWheelEvent) => void;
|
|
230
|
+
interactive?: boolean;
|
|
231
|
+
eventMode?: "static" | "passive" | "dynamic" | "auto" | "none";
|
|
232
|
+
}
|
|
233
|
+
/** The type of a function passed to Graphics.draw */
|
|
234
|
+
type DrawFn = (g: Graphics_2qg1pn) => void;
|
|
235
|
+
/** A value that can be a child of a canvas container. */
|
|
236
|
+
type CanvasChild = PIXIContainer | null | undefined | false;
|
|
237
|
+
interface GraphicsProps extends CanvasTransformProps, CanvasEventProps {
|
|
238
|
+
draw: DrawFn;
|
|
239
|
+
children?: never;
|
|
240
|
+
}
|
|
241
|
+
interface ContainerProps extends CanvasTransformProps, CanvasEventProps {
|
|
242
|
+
children?: CanvasChild | CanvasChild[];
|
|
243
|
+
}
|
|
244
|
+
interface SpriteProps extends CanvasTransformProps, CanvasEventProps {
|
|
245
|
+
texture: MaybeAccessor<string>;
|
|
246
|
+
anchor?: MaybeAccessor<number>;
|
|
247
|
+
width?: MaybeAccessor<number>;
|
|
248
|
+
height?: MaybeAccessor<number>;
|
|
249
|
+
}
|
|
250
|
+
interface TextProps extends CanvasTransformProps, CanvasEventProps {
|
|
251
|
+
text: MaybeAccessor<string>;
|
|
252
|
+
style?: Partial<TextStyleOptions>;
|
|
253
|
+
}
|
|
254
|
+
/** Reserved for Phase 3 accessibility. */
|
|
255
|
+
interface AccessibilityProps {
|
|
256
|
+
label?: string;
|
|
257
|
+
role?: string;
|
|
258
|
+
}
|
|
259
|
+
/** Map of canvas intrinsic tag names to their prop types. */
|
|
260
|
+
interface CanvasIntrinsicElements {
|
|
261
|
+
Graphics: GraphicsProps;
|
|
262
|
+
Container: ContainerProps;
|
|
263
|
+
Sprite: SpriteProps;
|
|
264
|
+
Text: TextProps;
|
|
265
|
+
}
|
|
266
|
+
export { unwrap, render, loadSpriteTexture, jsxCanvas, isCanvasIntrinsic, createReactiveSprite, createDebugOverlay, canvasList, canvasConditional, bindSignal, TextProps, SpriteProps, RectProps, Rect, MaybeAccessor, LineProps, Line, GraphicsProps, EllipseProps, Ellipse, DrawFn, ContainerProps, CircleProps, Circle, CanvasTransformProps, CanvasState, CanvasRenderContext, CanvasOptions, CanvasLayerProps, CanvasLayer, CanvasIntrinsicElements, CanvasEventProps, CanvasChild, Canvas, AccessibilityProps };
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
// src/canvas.ts
|
|
2
|
+
import { domEffect } from "@vertz/ui/internals";
|
|
2
3
|
import { Application } from "pixi.js";
|
|
3
|
-
import { effect } from "@vertz/ui";
|
|
4
4
|
function bindSignal(sig, displayObject, property, transform) {
|
|
5
5
|
const update = () => {
|
|
6
6
|
const value = transform ? transform(sig.value) : sig.value;
|
|
7
7
|
displayObject[property] = value;
|
|
8
8
|
};
|
|
9
9
|
update();
|
|
10
|
-
const disposeEffect =
|
|
11
|
-
sig.value;
|
|
10
|
+
const disposeEffect = domEffect(() => {
|
|
12
11
|
update();
|
|
13
12
|
});
|
|
14
13
|
return disposeEffect;
|
|
@@ -35,41 +34,422 @@ function createReactiveSprite(options, displayObject) {
|
|
|
35
34
|
}
|
|
36
35
|
return {
|
|
37
36
|
displayObject,
|
|
38
|
-
dispose: () =>
|
|
37
|
+
dispose: () => {
|
|
38
|
+
for (const fn of cleanups)
|
|
39
|
+
fn();
|
|
40
|
+
}
|
|
39
41
|
};
|
|
40
42
|
}
|
|
41
|
-
function render(container, options) {
|
|
42
|
-
const app = new Application
|
|
43
|
+
async function render(container, options) {
|
|
44
|
+
const app = new Application;
|
|
45
|
+
await app.init({
|
|
43
46
|
width: options.width,
|
|
44
47
|
height: options.height,
|
|
45
|
-
|
|
48
|
+
background: options.backgroundColor ?? 0
|
|
46
49
|
});
|
|
47
|
-
container.appendChild(app.
|
|
50
|
+
container.appendChild(app.canvas);
|
|
48
51
|
const dispose = () => {
|
|
49
52
|
destroy(app, container);
|
|
50
53
|
};
|
|
51
54
|
return {
|
|
52
|
-
canvas: app.
|
|
55
|
+
canvas: app.canvas,
|
|
56
|
+
app,
|
|
57
|
+
stage: app.stage,
|
|
53
58
|
dispose
|
|
54
59
|
};
|
|
55
60
|
}
|
|
56
61
|
function destroy(app, container) {
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
container.removeChild(
|
|
62
|
+
const canvas = app.canvas;
|
|
63
|
+
if (canvas && container.contains(canvas)) {
|
|
64
|
+
container.removeChild(canvas);
|
|
60
65
|
}
|
|
61
|
-
app.destroy(true, { children: true
|
|
66
|
+
app.destroy(true, { children: true });
|
|
62
67
|
}
|
|
63
68
|
var Canvas = {
|
|
64
69
|
render,
|
|
65
70
|
bindSignal,
|
|
66
|
-
createReactiveSprite
|
|
67
|
-
destroy
|
|
71
|
+
createReactiveSprite
|
|
68
72
|
};
|
|
73
|
+
// src/canvas-conditional.ts
|
|
74
|
+
import { domEffect as domEffect2, popScope, pushScope, runCleanups } from "@vertz/ui/internals";
|
|
75
|
+
function canvasConditional(parent, condition, factory, fallbackFactory) {
|
|
76
|
+
let current = null;
|
|
77
|
+
let branchCleanups = [];
|
|
78
|
+
let disposed = false;
|
|
79
|
+
function removeCurrent() {
|
|
80
|
+
if (current) {
|
|
81
|
+
parent.removeChild(current);
|
|
82
|
+
current = null;
|
|
83
|
+
}
|
|
84
|
+
runCleanups(branchCleanups);
|
|
85
|
+
}
|
|
86
|
+
const disposeEffect = domEffect2(() => {
|
|
87
|
+
if (disposed)
|
|
88
|
+
return;
|
|
89
|
+
const shouldShow = condition();
|
|
90
|
+
removeCurrent();
|
|
91
|
+
if (shouldShow) {
|
|
92
|
+
const scope = pushScope();
|
|
93
|
+
current = factory();
|
|
94
|
+
popScope();
|
|
95
|
+
branchCleanups = scope;
|
|
96
|
+
parent.addChild(current);
|
|
97
|
+
} else if (fallbackFactory) {
|
|
98
|
+
const scope = pushScope();
|
|
99
|
+
current = fallbackFactory();
|
|
100
|
+
popScope();
|
|
101
|
+
branchCleanups = scope;
|
|
102
|
+
parent.addChild(current);
|
|
103
|
+
} else {
|
|
104
|
+
branchCleanups = [];
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return () => {
|
|
108
|
+
disposed = true;
|
|
109
|
+
disposeEffect();
|
|
110
|
+
removeCurrent();
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// src/canvas-layer.ts
|
|
114
|
+
import { createContext, useContext } from "@vertz/ui";
|
|
115
|
+
import { onCleanup } from "@vertz/ui/internals";
|
|
116
|
+
import { Application as Application2, Container as Container2 } from "pixi.js";
|
|
117
|
+
|
|
118
|
+
// src/debug-overlay.ts
|
|
119
|
+
import { Container, Graphics, Text } from "pixi.js";
|
|
120
|
+
function createDebugOverlay(stage) {
|
|
121
|
+
const overlay = new Container;
|
|
122
|
+
overlay.label = "__debug_overlay";
|
|
123
|
+
function update() {
|
|
124
|
+
overlay.removeChildren();
|
|
125
|
+
drawDebugRecursive(stage, overlay);
|
|
126
|
+
}
|
|
127
|
+
function drawDebugRecursive(node, debugLayer) {
|
|
128
|
+
for (const child of node.children) {
|
|
129
|
+
if (child === overlay)
|
|
130
|
+
continue;
|
|
131
|
+
if (child instanceof Container) {
|
|
132
|
+
const bounds = child.getBounds();
|
|
133
|
+
const box = new Graphics;
|
|
134
|
+
box.rect(bounds.x, bounds.y, bounds.width, bounds.height);
|
|
135
|
+
box.stroke({ width: 1, color: 65280, alpha: 0.5 });
|
|
136
|
+
debugLayer.addChild(box);
|
|
137
|
+
if (child.label) {
|
|
138
|
+
const label = new Text({
|
|
139
|
+
text: child.label,
|
|
140
|
+
style: { fontSize: 10, fill: 65280 }
|
|
141
|
+
});
|
|
142
|
+
label.x = bounds.x;
|
|
143
|
+
label.y = bounds.y - 12;
|
|
144
|
+
debugLayer.addChild(label);
|
|
145
|
+
}
|
|
146
|
+
if (child.children.length > 0) {
|
|
147
|
+
drawDebugRecursive(child, debugLayer);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
overlay,
|
|
154
|
+
update,
|
|
155
|
+
destroy: () => {
|
|
156
|
+
overlay.removeChildren();
|
|
157
|
+
overlay.destroy({ children: true });
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/canvas-layer.ts
|
|
163
|
+
var CanvasRenderContext = createContext(null);
|
|
164
|
+
function CanvasLayer(props) {
|
|
165
|
+
const parentCtx = useContext(CanvasRenderContext);
|
|
166
|
+
if (parentCtx) {
|
|
167
|
+
throw new Error("<CanvasLayer> cannot be nested inside another <CanvasLayer>. Use <Container> to group canvas elements.");
|
|
168
|
+
}
|
|
169
|
+
const div = document.createElement("div");
|
|
170
|
+
const app = new Application2;
|
|
171
|
+
let destroyed = false;
|
|
172
|
+
onCleanup(() => {
|
|
173
|
+
destroyed = true;
|
|
174
|
+
app.destroy(true, { children: true });
|
|
175
|
+
});
|
|
176
|
+
app.init({
|
|
177
|
+
width: props.width,
|
|
178
|
+
height: props.height,
|
|
179
|
+
background: props.background ?? 0
|
|
180
|
+
}).then(() => {
|
|
181
|
+
if (destroyed)
|
|
182
|
+
return;
|
|
183
|
+
div.appendChild(app.canvas);
|
|
184
|
+
CanvasRenderContext.Provider(app.stage, () => {
|
|
185
|
+
if (props.children != null) {
|
|
186
|
+
const children = Array.isArray(props.children) ? props.children : [props.children];
|
|
187
|
+
for (const child of children) {
|
|
188
|
+
if (child instanceof Container2) {
|
|
189
|
+
app.stage.addChild(child);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
if (props.debug) {
|
|
195
|
+
const debug = createDebugOverlay(app.stage);
|
|
196
|
+
app.stage.addChild(debug.overlay);
|
|
197
|
+
debug.update();
|
|
198
|
+
}
|
|
199
|
+
}).catch((error) => {
|
|
200
|
+
if (destroyed)
|
|
201
|
+
return;
|
|
202
|
+
console.error("[ui-canvas] CanvasLayer failed to initialize:", error);
|
|
203
|
+
});
|
|
204
|
+
return div;
|
|
205
|
+
}
|
|
206
|
+
// src/canvas-list.ts
|
|
207
|
+
import { domEffect as domEffect3, popScope as popScope2, pushScope as pushScope2, runCleanups as runCleanups2 } from "@vertz/ui/internals";
|
|
208
|
+
function canvasList(parent, items, renderFn, keyFn) {
|
|
209
|
+
const itemMap = new Map;
|
|
210
|
+
let disposed = false;
|
|
211
|
+
const disposeEffect = domEffect3(() => {
|
|
212
|
+
if (disposed)
|
|
213
|
+
return;
|
|
214
|
+
const currentItems = items();
|
|
215
|
+
const currentKeys = new Set(currentItems.map(keyFn));
|
|
216
|
+
for (const [key, entry] of itemMap) {
|
|
217
|
+
if (!currentKeys.has(key)) {
|
|
218
|
+
parent.removeChild(entry.displayObject);
|
|
219
|
+
runCleanups2(entry.scope);
|
|
220
|
+
itemMap.delete(key);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const item of currentItems) {
|
|
224
|
+
const key = keyFn(item);
|
|
225
|
+
if (!itemMap.has(key)) {
|
|
226
|
+
const scope = pushScope2();
|
|
227
|
+
const displayObject = renderFn(item);
|
|
228
|
+
popScope2();
|
|
229
|
+
parent.addChild(displayObject);
|
|
230
|
+
itemMap.set(key, { displayObject, scope });
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
for (let i = 0;i < currentItems.length; i++) {
|
|
234
|
+
const key = keyFn(currentItems[i]);
|
|
235
|
+
const entry = itemMap.get(key);
|
|
236
|
+
if (entry && parent.getChildIndex(entry.displayObject) !== i) {
|
|
237
|
+
parent.setChildIndex(entry.displayObject, i);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
return () => {
|
|
242
|
+
disposed = true;
|
|
243
|
+
disposeEffect();
|
|
244
|
+
for (const [, entry] of itemMap) {
|
|
245
|
+
parent.removeChild(entry.displayObject);
|
|
246
|
+
runCleanups2(entry.scope);
|
|
247
|
+
}
|
|
248
|
+
itemMap.clear();
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
// src/jsx-canvas.ts
|
|
252
|
+
import { _tryOnCleanup, domEffect as domEffect4 } from "@vertz/ui/internals";
|
|
253
|
+
import { Container as Container3, Graphics as Graphics2, Sprite, Text as Text2 } from "pixi.js";
|
|
254
|
+
|
|
255
|
+
// src/sprite-loading.ts
|
|
256
|
+
import { Assets } from "pixi.js";
|
|
257
|
+
async function loadSpriteTexture(sprite, path) {
|
|
258
|
+
sprite.visible = false;
|
|
259
|
+
try {
|
|
260
|
+
const texture = await Assets.load(path);
|
|
261
|
+
if (sprite.destroyed)
|
|
262
|
+
return;
|
|
263
|
+
sprite.texture = texture;
|
|
264
|
+
sprite.visible = true;
|
|
265
|
+
} catch (error) {
|
|
266
|
+
console.warn(`[ui-canvas] Failed to load texture "${path}": ${error instanceof Error ? error.message : String(error)}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// src/jsx-canvas.ts
|
|
271
|
+
var CANVAS_INTRINSICS = new Set(["Graphics", "Container", "Sprite", "Text"]);
|
|
272
|
+
function isCanvasIntrinsic(tag) {
|
|
273
|
+
return CANVAS_INTRINSICS.has(tag);
|
|
274
|
+
}
|
|
275
|
+
function jsxCanvas(tag, props) {
|
|
276
|
+
const displayObject = createDisplayObject(tag);
|
|
277
|
+
let hasEventProps = false;
|
|
278
|
+
_tryOnCleanup(() => {
|
|
279
|
+
displayObject.destroy({ children: true });
|
|
280
|
+
});
|
|
281
|
+
if (displayObject instanceof Text2) {
|
|
282
|
+
if ("text" in props) {
|
|
283
|
+
const textValue = props.text;
|
|
284
|
+
if (typeof textValue === "function") {
|
|
285
|
+
domEffect4(() => {
|
|
286
|
+
displayObject.text = textValue();
|
|
287
|
+
});
|
|
288
|
+
} else if (textValue !== undefined) {
|
|
289
|
+
displayObject.text = textValue;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if ("style" in props && props.style !== undefined) {
|
|
293
|
+
displayObject.style = props.style;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
if (displayObject instanceof Sprite && "texture" in props) {
|
|
297
|
+
const textureValue = props.texture;
|
|
298
|
+
if (typeof textureValue === "string") {
|
|
299
|
+
loadSpriteTexture(displayObject, textureValue);
|
|
300
|
+
} else if (typeof textureValue === "function") {
|
|
301
|
+
domEffect4(() => {
|
|
302
|
+
const url = textureValue();
|
|
303
|
+
if (typeof url === "string") {
|
|
304
|
+
loadSpriteTexture(displayObject, url);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const [key, value] of Object.entries(props)) {
|
|
310
|
+
if (key === "children" || key === "ref" || key === "interactive")
|
|
311
|
+
continue;
|
|
312
|
+
if (displayObject instanceof Text2 && (key === "text" || key === "style"))
|
|
313
|
+
continue;
|
|
314
|
+
if (displayObject instanceof Sprite && key === "texture")
|
|
315
|
+
continue;
|
|
316
|
+
if (key === "draw" && displayObject instanceof Graphics2) {
|
|
317
|
+
domEffect4(() => {
|
|
318
|
+
displayObject.clear();
|
|
319
|
+
value(displayObject);
|
|
320
|
+
});
|
|
321
|
+
} else if (key === "eventMode") {
|
|
322
|
+
displayObject.eventMode = value;
|
|
323
|
+
} else if (key.startsWith("on") && typeof value === "function") {
|
|
324
|
+
const event = key.slice(2).toLowerCase();
|
|
325
|
+
const handler = value;
|
|
326
|
+
displayObject.on(event, handler);
|
|
327
|
+
_tryOnCleanup(() => displayObject.off(event, handler));
|
|
328
|
+
hasEventProps = true;
|
|
329
|
+
} else if (typeof value === "function") {
|
|
330
|
+
domEffect4(() => {
|
|
331
|
+
displayObject[key] = value();
|
|
332
|
+
});
|
|
333
|
+
} else if (value !== undefined) {
|
|
334
|
+
displayObject[key] = value;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (hasEventProps && props.interactive !== false && !("eventMode" in props)) {
|
|
338
|
+
displayObject.eventMode = "static";
|
|
339
|
+
}
|
|
340
|
+
if (props.ref && typeof props.ref === "function") {
|
|
341
|
+
props.ref(displayObject);
|
|
342
|
+
}
|
|
343
|
+
applyCanvasChildren(displayObject, props.children);
|
|
344
|
+
return displayObject;
|
|
345
|
+
}
|
|
346
|
+
function applyCanvasChildren(parent, children) {
|
|
347
|
+
if (children == null || children === false)
|
|
348
|
+
return;
|
|
349
|
+
if (Array.isArray(children)) {
|
|
350
|
+
for (const child of children) {
|
|
351
|
+
applyCanvasChildren(parent, child);
|
|
352
|
+
}
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
if (children instanceof Container3) {
|
|
356
|
+
parent.addChild(children);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function createDisplayObject(tag) {
|
|
361
|
+
switch (tag) {
|
|
362
|
+
case "Graphics":
|
|
363
|
+
return new Graphics2;
|
|
364
|
+
case "Container":
|
|
365
|
+
return new Container3;
|
|
366
|
+
case "Sprite":
|
|
367
|
+
return new Sprite;
|
|
368
|
+
case "Text":
|
|
369
|
+
return new Text2;
|
|
370
|
+
default:
|
|
371
|
+
throw new Error(`Unknown canvas element: <${tag}>`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// src/unwrap.ts
|
|
375
|
+
function unwrap(value) {
|
|
376
|
+
return typeof value === "function" ? value() : value;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// src/shapes.ts
|
|
380
|
+
function Circle(props) {
|
|
381
|
+
return jsxCanvas("Graphics", {
|
|
382
|
+
x: props.x,
|
|
383
|
+
y: props.y,
|
|
384
|
+
draw: (g) => {
|
|
385
|
+
const r = unwrap(props.radius);
|
|
386
|
+
const color = unwrap(props.fill);
|
|
387
|
+
g.circle(0, 0, r);
|
|
388
|
+
g.fill(color);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
function Rect(props) {
|
|
393
|
+
return jsxCanvas("Graphics", {
|
|
394
|
+
x: props.x,
|
|
395
|
+
y: props.y,
|
|
396
|
+
draw: (g) => {
|
|
397
|
+
const w = unwrap(props.width);
|
|
398
|
+
const h = unwrap(props.height);
|
|
399
|
+
const color = unwrap(props.fill);
|
|
400
|
+
g.rect(0, 0, w, h);
|
|
401
|
+
g.fill(color);
|
|
402
|
+
if (props.stroke !== undefined) {
|
|
403
|
+
g.stroke({
|
|
404
|
+
color: unwrap(props.stroke),
|
|
405
|
+
width: unwrap(props.strokeWidth ?? 1)
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
function Line(props) {
|
|
412
|
+
return jsxCanvas("Graphics", {
|
|
413
|
+
x: props.x,
|
|
414
|
+
y: props.y,
|
|
415
|
+
draw: (g) => {
|
|
416
|
+
const color = unwrap(props.stroke);
|
|
417
|
+
const width = unwrap(props.strokeWidth ?? 1);
|
|
418
|
+
g.moveTo(props.from.x, props.from.y);
|
|
419
|
+
g.lineTo(props.to.x, props.to.y);
|
|
420
|
+
g.stroke({ color, width });
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
function Ellipse(props) {
|
|
425
|
+
return jsxCanvas("Graphics", {
|
|
426
|
+
x: props.x,
|
|
427
|
+
y: props.y,
|
|
428
|
+
draw: (g) => {
|
|
429
|
+
const rx = unwrap(props.halfWidth);
|
|
430
|
+
const ry = unwrap(props.halfHeight);
|
|
431
|
+
const color = unwrap(props.fill);
|
|
432
|
+
g.ellipse(0, 0, rx, ry);
|
|
433
|
+
g.fill(color);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
}
|
|
69
437
|
export {
|
|
438
|
+
unwrap,
|
|
70
439
|
render,
|
|
71
|
-
|
|
440
|
+
loadSpriteTexture,
|
|
441
|
+
jsxCanvas,
|
|
442
|
+
isCanvasIntrinsic,
|
|
72
443
|
createReactiveSprite,
|
|
444
|
+
createDebugOverlay,
|
|
445
|
+
canvasList,
|
|
446
|
+
canvasConditional,
|
|
73
447
|
bindSignal,
|
|
448
|
+
Rect,
|
|
449
|
+
Line,
|
|
450
|
+
Ellipse,
|
|
451
|
+
Circle,
|
|
452
|
+
CanvasRenderContext,
|
|
453
|
+
CanvasLayer,
|
|
74
454
|
Canvas
|
|
75
455
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertz/ui-canvas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Vertz Canvas renderer with PixiJS and signals",
|
|
@@ -26,20 +26,22 @@
|
|
|
26
26
|
],
|
|
27
27
|
"scripts": {
|
|
28
28
|
"build": "bunup",
|
|
29
|
-
"test": "
|
|
30
|
-
"test:watch": "
|
|
29
|
+
"test": "bun test",
|
|
30
|
+
"test:watch": "bun test --watch",
|
|
31
31
|
"typecheck": "tsc --noEmit"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@vertz/ui": "workspace:*",
|
|
35
34
|
"pixi.js": "^8.0.0"
|
|
36
35
|
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"@vertz/ui": "workspace:*"
|
|
38
|
+
},
|
|
37
39
|
"devDependencies": {
|
|
38
|
-
"@
|
|
40
|
+
"@happy-dom/global-registrator": "^20.7.0",
|
|
41
|
+
"@vertz/ui": "workspace:*",
|
|
39
42
|
"bunup": "latest",
|
|
40
|
-
"happy-dom": "^
|
|
41
|
-
"typescript": "^5.7.0"
|
|
42
|
-
"vitest": "^4.0.18"
|
|
43
|
+
"happy-dom": "^20.7.0",
|
|
44
|
+
"typescript": "^5.7.0"
|
|
43
45
|
},
|
|
44
46
|
"engines": {
|
|
45
47
|
"node": ">=22"
|