canvasengine 2.0.0-beta.19 → 2.0.0-beta.20
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 +118 -9
- package/dist/index.js +491 -102
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/DOMContainer.ts +229 -0
- package/src/components/DisplayObject.ts +36 -21
- package/src/components/Graphic.ts +184 -20
- package/src/components/Mesh.ts +222 -0
- package/src/components/Sprite.ts +39 -6
- package/src/components/Text.ts +10 -6
- package/src/components/index.ts +4 -2
- package/src/directives/Sound.ts +92 -30
- package/src/engine/bootstrap.ts +0 -1
- package/src/engine/utils.ts +10 -3
package/package.json
CHANGED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { DOMContainer as PixiDOMContainer } from "pixi.js";
|
|
2
|
+
import {
|
|
3
|
+
createComponent,
|
|
4
|
+
Element,
|
|
5
|
+
registerComponent,
|
|
6
|
+
} from "../engine/reactive";
|
|
7
|
+
import { ComponentInstance, DisplayObject } from "./DisplayObject";
|
|
8
|
+
import { ComponentFunction } from "../engine/signal";
|
|
9
|
+
import { DisplayObjectProps } from "./types/DisplayObject";
|
|
10
|
+
|
|
11
|
+
interface DOMContainerProps extends DisplayObjectProps {
|
|
12
|
+
element:
|
|
13
|
+
| string
|
|
14
|
+
| {
|
|
15
|
+
value: HTMLElement;
|
|
16
|
+
};
|
|
17
|
+
textContent?: string;
|
|
18
|
+
attrs?: Record<string, any> & {
|
|
19
|
+
class?:
|
|
20
|
+
| string
|
|
21
|
+
| string[]
|
|
22
|
+
| Record<string, boolean>
|
|
23
|
+
| { items?: string[] }
|
|
24
|
+
| { value?: string | string[] | Record<string, boolean> };
|
|
25
|
+
style?:
|
|
26
|
+
| string
|
|
27
|
+
| Record<string, string | number>
|
|
28
|
+
| { value?: string | Record<string, string | number> };
|
|
29
|
+
};
|
|
30
|
+
sortableChildren?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* DOMContainer class for managing DOM elements within the canvas engine
|
|
35
|
+
*
|
|
36
|
+
* This class extends the DisplayObject functionality to handle DOM elements using
|
|
37
|
+
* PixiJS's native DOMContainer. It provides a bridge between the canvas rendering
|
|
38
|
+
* system and traditional DOM manipulation with proper transform hierarchy and visibility.
|
|
39
|
+
*
|
|
40
|
+
* The DOMContainer is especially useful for rendering standard DOM elements that handle
|
|
41
|
+
* user input, such as `<input>` or `<textarea>`. This is often simpler and more flexible
|
|
42
|
+
* than trying to implement text input directly in PixiJS.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* // Basic usage with input element
|
|
47
|
+
* const element = document.createElement('input');
|
|
48
|
+
* element.type = 'text';
|
|
49
|
+
* element.placeholder = 'Enter text...';
|
|
50
|
+
*
|
|
51
|
+
* const domContainer = new DOMContainer({
|
|
52
|
+
* element,
|
|
53
|
+
* x: 100,
|
|
54
|
+
* y: 50,
|
|
55
|
+
* anchor: { x: 0.5, y: 0.5 }
|
|
56
|
+
* });
|
|
57
|
+
*
|
|
58
|
+
* // Using different class and style formats
|
|
59
|
+
* const containerWithClasses = new DOMContainer({
|
|
60
|
+
* element: 'div',
|
|
61
|
+
* attrs: {
|
|
62
|
+
* // String format: space-separated classes
|
|
63
|
+
* class: 'container primary-theme',
|
|
64
|
+
*
|
|
65
|
+
* // Array format: array of class names
|
|
66
|
+
* // class: ['container', 'primary-theme'],
|
|
67
|
+
*
|
|
68
|
+
* // Object format: conditional classes
|
|
69
|
+
* // class: {
|
|
70
|
+
* // 'container': true,
|
|
71
|
+
* // 'primary-theme': true,
|
|
72
|
+
* // 'disabled': false
|
|
73
|
+
* // }
|
|
74
|
+
*
|
|
75
|
+
* // String format: CSS style string
|
|
76
|
+
* style: 'background-color: red; padding: 10px;',
|
|
77
|
+
*
|
|
78
|
+
* // Object format: style properties
|
|
79
|
+
* // style: {
|
|
80
|
+
* // backgroundColor: 'red',
|
|
81
|
+
* // padding: '10px',
|
|
82
|
+
* // fontSize: 16
|
|
83
|
+
* // }
|
|
84
|
+
* }
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
const EVENTS = [
|
|
89
|
+
"click",
|
|
90
|
+
"mouseover",
|
|
91
|
+
"mouseout",
|
|
92
|
+
"mouseenter",
|
|
93
|
+
"mouseleave",
|
|
94
|
+
"mousemove",
|
|
95
|
+
"mouseup",
|
|
96
|
+
"mousedown",
|
|
97
|
+
"touchstart",
|
|
98
|
+
"touchend",
|
|
99
|
+
"touchmove",
|
|
100
|
+
"touchcancel",
|
|
101
|
+
"wheel",
|
|
102
|
+
"scroll",
|
|
103
|
+
"resize",
|
|
104
|
+
"focus",
|
|
105
|
+
"blur",
|
|
106
|
+
"change",
|
|
107
|
+
"input",
|
|
108
|
+
"submit",
|
|
109
|
+
"reset",
|
|
110
|
+
"keydown",
|
|
111
|
+
"keyup",
|
|
112
|
+
"keypress",
|
|
113
|
+
"contextmenu",
|
|
114
|
+
"drag",
|
|
115
|
+
"dragend",
|
|
116
|
+
"dragenter",
|
|
117
|
+
"dragleave",
|
|
118
|
+
"dragover",
|
|
119
|
+
"drop",
|
|
120
|
+
"dragstart",
|
|
121
|
+
"select",
|
|
122
|
+
"selectstart",
|
|
123
|
+
"selectend",
|
|
124
|
+
"selectall",
|
|
125
|
+
"selectnone",
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
export class CanvasDOMContainer extends DisplayObject(PixiDOMContainer) {
|
|
129
|
+
disableLayout = true;
|
|
130
|
+
private eventListeners: Map<string, (e: Event) => void> = new Map();
|
|
131
|
+
|
|
132
|
+
onInit(props: DOMContainerProps) {
|
|
133
|
+
super.onInit(props);
|
|
134
|
+
if (props.element === undefined) {
|
|
135
|
+
throw new Error("DOMContainer: element is required");
|
|
136
|
+
}
|
|
137
|
+
if (typeof props.element === "string") {
|
|
138
|
+
this.element = document.createElement(props.element);
|
|
139
|
+
} else {
|
|
140
|
+
this.element = props.element.value;
|
|
141
|
+
}
|
|
142
|
+
for (const event of EVENTS) {
|
|
143
|
+
if (props.attrs?.[event]) {
|
|
144
|
+
const eventHandler = (e: Event) => {
|
|
145
|
+
props.attrs[event]?.(e);
|
|
146
|
+
};
|
|
147
|
+
this.eventListeners.set(event, eventHandler);
|
|
148
|
+
this.element.addEventListener(event, eventHandler, false);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
onUpdate(props: DOMContainerProps) {
|
|
154
|
+
super.onUpdate(props);
|
|
155
|
+
|
|
156
|
+
for (const [key, value] of Object.entries(props.attrs || {})) {
|
|
157
|
+
if (key === "class") {
|
|
158
|
+
const classList = value.items || value.value || value;
|
|
159
|
+
|
|
160
|
+
// Clear existing classes first
|
|
161
|
+
this.element.className = "";
|
|
162
|
+
|
|
163
|
+
if (typeof classList === "string") {
|
|
164
|
+
// String: space-separated class names
|
|
165
|
+
this.element.className = classList;
|
|
166
|
+
} else if (Array.isArray(classList)) {
|
|
167
|
+
// Array: array of class names
|
|
168
|
+
this.element.classList.add(...classList);
|
|
169
|
+
} else if (typeof classList === "object" && classList !== null) {
|
|
170
|
+
// Object: { className: boolean }
|
|
171
|
+
for (const [className, shouldAdd] of Object.entries(classList)) {
|
|
172
|
+
if (shouldAdd) {
|
|
173
|
+
this.element.classList.add(className);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} else if (key === "style") {
|
|
178
|
+
const styleValue = value.items || value.value || value;
|
|
179
|
+
|
|
180
|
+
if (typeof styleValue === "string") {
|
|
181
|
+
// String: CSS style string
|
|
182
|
+
this.element.setAttribute("style", styleValue);
|
|
183
|
+
} else if (typeof styleValue === "object" && styleValue !== null) {
|
|
184
|
+
// Object: { property: value }
|
|
185
|
+
for (const [styleProp, styleVal] of Object.entries(styleValue)) {
|
|
186
|
+
if (styleVal !== null && styleVal !== undefined) {
|
|
187
|
+
(this.element.style as any)[styleProp] = styleVal;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
} else if (!EVENTS.includes(key)) {
|
|
192
|
+
this.element.setAttribute(key, value);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (props.textContent) {
|
|
196
|
+
this.element.textContent = props.textContent;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (props.sortableChildren !== undefined) {
|
|
200
|
+
this.sortableChildren = props.sortableChildren;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async onDestroy(
|
|
205
|
+
parent: Element<ComponentInstance>,
|
|
206
|
+
afterDestroy: () => void
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
// Remove all event listeners from the DOM element
|
|
209
|
+
if (this.element) {
|
|
210
|
+
for (const [event, handler] of this.eventListeners) {
|
|
211
|
+
this.element.removeEventListener(event, handler, false);
|
|
212
|
+
}
|
|
213
|
+
this.eventListeners.clear();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const _afterDestroyCallback = async () => {
|
|
217
|
+
afterDestroy();
|
|
218
|
+
};
|
|
219
|
+
await super.onDestroy(parent, _afterDestroyCallback);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface CanvasDOMContainer extends DisplayObjectProps {}
|
|
224
|
+
|
|
225
|
+
registerComponent("DOMContainer", CanvasDOMContainer);
|
|
226
|
+
|
|
227
|
+
export const DOMContainer: ComponentFunction<DOMContainerProps> = (props) => {
|
|
228
|
+
return createComponent("DOMContainer", props);
|
|
229
|
+
};
|
|
@@ -9,9 +9,10 @@ import type {
|
|
|
9
9
|
TransformOrigin,
|
|
10
10
|
} from "./types/DisplayObject";
|
|
11
11
|
import { signal } from "@signe/reactive";
|
|
12
|
-
import { DropShadowFilter } from "pixi-filters";
|
|
13
12
|
import { BlurFilter, ObservablePoint } from "pixi.js";
|
|
13
|
+
import * as FILTERS from "pixi-filters";
|
|
14
14
|
import { isPercent } from "../utils/functions";
|
|
15
|
+
import { BehaviorSubject, filter, Subject } from "rxjs";
|
|
15
16
|
|
|
16
17
|
export interface ComponentInstance extends PixiMixins.ContainerOptions {
|
|
17
18
|
id?: string;
|
|
@@ -113,12 +114,19 @@ export function DisplayObject(extendClass) {
|
|
|
113
114
|
layout = null;
|
|
114
115
|
onBeforeDestroy: OnHook | null = null;
|
|
115
116
|
onAfterMount: OnHook | null = null;
|
|
117
|
+
subjectInit = new BehaviorSubject(null);
|
|
118
|
+
disableLayout: boolean = false;
|
|
116
119
|
|
|
117
120
|
get deltaRatio() {
|
|
118
121
|
return this.#canvasContext?.scheduler?.tick.value.deltaRatio;
|
|
119
122
|
}
|
|
120
123
|
|
|
121
|
-
|
|
124
|
+
get parentIsFlex() {
|
|
125
|
+
if (this.disableLayout) return false;
|
|
126
|
+
return this.parent?.isFlex;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onInit(props: Props) {
|
|
122
130
|
this._id = props.id;
|
|
123
131
|
for (let event of EVENTS) {
|
|
124
132
|
if (props[event] && !this.overrideProps.includes(event)) {
|
|
@@ -146,14 +154,20 @@ export function DisplayObject(extendClass) {
|
|
|
146
154
|
this.layout = {};
|
|
147
155
|
this.isFlex = true;
|
|
148
156
|
}
|
|
157
|
+
|
|
158
|
+
this.subjectInit.next(this);
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
async onMount({ parent, props }: Element<DisplayObject>, index?: number) {
|
|
152
162
|
this.#canvasContext = props.context;
|
|
153
163
|
if (parent) {
|
|
154
164
|
const instance = parent.componentInstance as DisplayObject;
|
|
155
|
-
if (instance.isFlex && !this.layout) {
|
|
156
|
-
|
|
165
|
+
if (instance.isFlex && !this.layout && !this.disableLayout) {
|
|
166
|
+
try {
|
|
167
|
+
this.layout = {};
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.warn('Failed to set layout:', error);
|
|
170
|
+
}
|
|
157
171
|
}
|
|
158
172
|
if (index === undefined) {
|
|
159
173
|
instance.addChild(this);
|
|
@@ -168,7 +182,7 @@ export function DisplayObject(extendClass) {
|
|
|
168
182
|
}
|
|
169
183
|
}
|
|
170
184
|
|
|
171
|
-
onUpdate(props) {
|
|
185
|
+
onUpdate(props: Props) {
|
|
172
186
|
this.fullProps = {
|
|
173
187
|
...this.fullProps,
|
|
174
188
|
...props,
|
|
@@ -229,23 +243,24 @@ export function DisplayObject(extendClass) {
|
|
|
229
243
|
if (props.filters) this.filters = props.filters;
|
|
230
244
|
if (props.maskOf) {
|
|
231
245
|
if (isElement(props.maskOf)) {
|
|
232
|
-
props.maskOf.componentInstance.mask = this;
|
|
246
|
+
props.maskOf.componentInstance.mask = this as any;
|
|
233
247
|
}
|
|
234
248
|
}
|
|
235
249
|
if (props.blendMode) this.blendMode = props.blendMode;
|
|
236
250
|
if (props.filterArea) this.filterArea = props.filterArea;
|
|
237
251
|
const currentFilters = this.filters || [];
|
|
238
252
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
253
|
+
// TODO: Fix DropShadowFilter import issue
|
|
254
|
+
// if (props.shadow) {
|
|
255
|
+
// let dropShadowFilter = currentFilters.find(
|
|
256
|
+
// (filter) => filter instanceof FILTERS.DropShadowFilter
|
|
257
|
+
// );
|
|
258
|
+
// if (!dropShadowFilter) {
|
|
259
|
+
// dropShadowFilter = new FILTERS.DropShadowFilter();
|
|
260
|
+
// currentFilters.push(dropShadowFilter);
|
|
261
|
+
// }
|
|
262
|
+
// Object.assign(dropShadowFilter, props.shadow);
|
|
263
|
+
// }
|
|
249
264
|
|
|
250
265
|
if (props.blur) {
|
|
251
266
|
let blurFilter = currentFilters.find(
|
|
@@ -272,7 +287,7 @@ export function DisplayObject(extendClass) {
|
|
|
272
287
|
await this.onBeforeDestroy();
|
|
273
288
|
}
|
|
274
289
|
super.destroy();
|
|
275
|
-
if (
|
|
290
|
+
if (afterDestroy) afterDestroy();
|
|
276
291
|
}
|
|
277
292
|
|
|
278
293
|
setFlexDirection(direction: FlexDirection) {
|
|
@@ -328,7 +343,7 @@ export function DisplayObject(extendClass) {
|
|
|
328
343
|
|
|
329
344
|
setX(x: number) {
|
|
330
345
|
x = x + this.getWidth() * this._anchorPoints.x;
|
|
331
|
-
if (!this.
|
|
346
|
+
if (!this.parentIsFlex) {
|
|
332
347
|
this.x = x;
|
|
333
348
|
} else {
|
|
334
349
|
this.x = x;
|
|
@@ -338,7 +353,7 @@ export function DisplayObject(extendClass) {
|
|
|
338
353
|
|
|
339
354
|
setY(y: number) {
|
|
340
355
|
y = y + this.getHeight() * this._anchorPoints.y;
|
|
341
|
-
if (!this.
|
|
356
|
+
if (!this.parentIsFlex) {
|
|
342
357
|
this.y = y;
|
|
343
358
|
} else {
|
|
344
359
|
this.y = y;
|
|
@@ -416,7 +431,7 @@ export function DisplayObject(extendClass) {
|
|
|
416
431
|
|
|
417
432
|
setWidth(width: number) {
|
|
418
433
|
this.displayWidth.set(width);
|
|
419
|
-
if (!this.
|
|
434
|
+
if (!this.parentIsFlex) {
|
|
420
435
|
this.width = width;
|
|
421
436
|
} else {
|
|
422
437
|
this.layout = { width };
|
|
@@ -425,7 +440,7 @@ export function DisplayObject(extendClass) {
|
|
|
425
440
|
|
|
426
441
|
setHeight(height: number) {
|
|
427
442
|
this.displayHeight.set(height);
|
|
428
|
-
if (!this.
|
|
443
|
+
if (!this.parentIsFlex) {
|
|
429
444
|
this.height = height;
|
|
430
445
|
} else {
|
|
431
446
|
this.layout = { height };
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { Effect, effect, Signal } from "@signe/reactive";
|
|
2
|
-
import { Graphics as PixiGraphics } from "pixi.js";
|
|
1
|
+
import { Effect, effect, isSignal, signal, Signal, WritableSignal } from "@signe/reactive";
|
|
2
|
+
import { Assets, Graphics as PixiGraphics } from "pixi.js";
|
|
3
3
|
import { createComponent, Element, registerComponent } from "../engine/reactive";
|
|
4
4
|
import { ComponentInstance, DisplayObject } from "./DisplayObject";
|
|
5
5
|
import { DisplayObjectProps } from "./types/DisplayObject";
|
|
6
6
|
import { useProps } from "../hooks/useProps";
|
|
7
7
|
import { SignalOrPrimitive } from "./types";
|
|
8
|
+
import { isPercent } from "../utils/functions";
|
|
8
9
|
|
|
9
10
|
interface GraphicsProps extends DisplayObjectProps {
|
|
10
|
-
draw?: (graphics: PixiGraphics) => void;
|
|
11
|
+
draw?: (graphics: PixiGraphics, width: number, height: number) => void;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
interface RectProps extends DisplayObjectProps {
|
|
@@ -29,19 +30,135 @@ interface TriangleProps extends DisplayObjectProps {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
interface SvgProps extends DisplayObjectProps {
|
|
32
|
-
|
|
33
|
+
/** SVG content as string (legacy prop) */
|
|
34
|
+
svg?: string;
|
|
35
|
+
/** URL source of the SVG file to load */
|
|
36
|
+
src?: string;
|
|
37
|
+
/** Direct SVG content as string */
|
|
38
|
+
content?: string;
|
|
33
39
|
}
|
|
34
40
|
|
|
35
41
|
class CanvasGraphics extends DisplayObject(PixiGraphics) {
|
|
36
42
|
clearEffect: Effect;
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
width: WritableSignal<number>;
|
|
44
|
+
height: WritableSignal<number>;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Initializes the graphics component with reactive width and height handling.
|
|
48
|
+
*
|
|
49
|
+
* This method handles different types of width and height props:
|
|
50
|
+
* - **Numbers**: Direct pixel values
|
|
51
|
+
* - **Strings with %**: Percentage values that trigger flex layout and use layout box dimensions
|
|
52
|
+
* - **Signals**: Reactive values that update automatically
|
|
53
|
+
*
|
|
54
|
+
* When percentage values are detected, the component:
|
|
55
|
+
* 1. Sets `display: 'flex'` to enable layout calculations
|
|
56
|
+
* 2. Listens to layout events to get computed dimensions
|
|
57
|
+
* 3. Updates internal width/height signals with layout box values
|
|
58
|
+
*
|
|
59
|
+
* The draw function receives the reactive width and height signals as parameters.
|
|
60
|
+
*
|
|
61
|
+
* @param props - Component properties including width, height, and draw function
|
|
62
|
+
* @example
|
|
63
|
+
* ```typescript
|
|
64
|
+
* // With pixel values
|
|
65
|
+
* Graphics({ width: 100, height: 50, draw: (g, w, h) => g.rect(0, 0, w(), h()) });
|
|
66
|
+
*
|
|
67
|
+
* // With percentage values (uses layout box)
|
|
68
|
+
* Graphics({ width: "50%", height: "100%", draw: (g, w, h) => g.rect(0, 0, w(), h()) });
|
|
69
|
+
*
|
|
70
|
+
* // With signals
|
|
71
|
+
* const width = signal(100);
|
|
72
|
+
* Graphics({ width, height: 50, draw: (g, w, h) => g.rect(0, 0, w(), h()) });
|
|
73
|
+
* ```
|
|
74
|
+
*/
|
|
75
|
+
async onInit(props) {
|
|
76
|
+
await super.onInit(props);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Called when the component is mounted to the scene graph.
|
|
81
|
+
* Creates the reactive effect for drawing using the original signals from propObservables.
|
|
82
|
+
* @param {Element<DisplayObject>} element - The element being mounted with props and propObservables.
|
|
83
|
+
* @param {number} [index] - The index of the component among its siblings.
|
|
84
|
+
*/
|
|
85
|
+
async onMount(element: Element<any>, index?: number): Promise<void> {
|
|
86
|
+
await super.onMount(element, index);
|
|
87
|
+
const { props, propObservables } = element;
|
|
88
|
+
|
|
89
|
+
// Use original signals from propObservables if available, otherwise create new ones
|
|
90
|
+
const width = (isSignal(propObservables?.width) ? propObservables.width : signal(props.width || 0)) as WritableSignal<number>;
|
|
91
|
+
const height = (isSignal(propObservables?.height) ? propObservables.height : signal(props.height || 0)) as WritableSignal<number>;
|
|
92
|
+
|
|
93
|
+
// Store as class properties for access in other methods
|
|
94
|
+
this.width = width;
|
|
95
|
+
this.height = height;
|
|
96
|
+
|
|
97
|
+
// Check if width or height are percentages to set display flex
|
|
98
|
+
const isWidthPercentage = isPercent(width());
|
|
99
|
+
const isHeightPercentage = isPercent(height());
|
|
100
|
+
|
|
39
101
|
if (props.draw) {
|
|
40
102
|
this.clearEffect = effect(() => {
|
|
103
|
+
const w = width();
|
|
104
|
+
const h = height();
|
|
105
|
+
if (typeof w == 'string' || typeof h == 'string') {
|
|
106
|
+
return
|
|
107
|
+
}
|
|
108
|
+
if (w == 0 || h == 0) {
|
|
109
|
+
return
|
|
110
|
+
}
|
|
41
111
|
this.clear();
|
|
42
|
-
props.draw?.(this);
|
|
112
|
+
props.draw?.(this, w, h);
|
|
113
|
+
this.subjectInit.next(this)
|
|
43
114
|
});
|
|
44
115
|
}
|
|
116
|
+
|
|
117
|
+
this.on('layout', (event) => {
|
|
118
|
+
const layoutBox = event.computedLayout;
|
|
119
|
+
// Update width if it's a percentage
|
|
120
|
+
if (isWidthPercentage && isSignal(width)) {
|
|
121
|
+
width.set(layoutBox.width);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Update height if it's a percentage
|
|
125
|
+
if (isHeightPercentage && isSignal(height)) {
|
|
126
|
+
height.set(layoutBox.height);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Called when component props are updated.
|
|
133
|
+
* Updates the internal width and height signals when props change.
|
|
134
|
+
* @param props - Updated properties
|
|
135
|
+
*/
|
|
136
|
+
onUpdate(props: any) {
|
|
137
|
+
super.onUpdate(props);
|
|
138
|
+
|
|
139
|
+
// Update width signal if width prop changed
|
|
140
|
+
if (props.width !== undefined && this.width) {
|
|
141
|
+
if (isSignal(props.width)) {
|
|
142
|
+
// If the new prop is a signal, we need to replace our local signal
|
|
143
|
+
// This shouldn't happen in normal usage, but handle it just in case
|
|
144
|
+
this.width = props.width;
|
|
145
|
+
} else {
|
|
146
|
+
// Update our local signal with the new value
|
|
147
|
+
this.width.set(props.width);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Update height signal if height prop changed
|
|
152
|
+
if (props.height !== undefined && this.height) {
|
|
153
|
+
if (isSignal(props.height)) {
|
|
154
|
+
// If the new prop is a signal, we need to replace our local signal
|
|
155
|
+
// This shouldn't happen in normal usage, but handle it just in case
|
|
156
|
+
this.height = props.height;
|
|
157
|
+
} else {
|
|
158
|
+
// Update our local signal with the new value
|
|
159
|
+
this.height.set(props.height);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
45
162
|
}
|
|
46
163
|
|
|
47
164
|
/**
|
|
@@ -70,16 +187,17 @@ export function Graphics(props: GraphicsProps) {
|
|
|
70
187
|
}
|
|
71
188
|
|
|
72
189
|
export function Rect(props: RectProps) {
|
|
73
|
-
const {
|
|
190
|
+
const { color, borderRadius, border } = useProps(props, {
|
|
74
191
|
borderRadius: null,
|
|
75
192
|
border: null
|
|
76
193
|
})
|
|
194
|
+
|
|
77
195
|
return Graphics({
|
|
78
|
-
draw: (g) => {
|
|
196
|
+
draw: (g, width, height) => {
|
|
79
197
|
if (borderRadius()) {
|
|
80
|
-
g.roundRect(0, 0, width
|
|
198
|
+
g.roundRect(0, 0, width, height, borderRadius());
|
|
81
199
|
} else {
|
|
82
|
-
g.rect(0, 0, width
|
|
200
|
+
g.rect(0, 0, width, height);
|
|
83
201
|
}
|
|
84
202
|
if (border) {
|
|
85
203
|
g.stroke(border);
|
|
@@ -95,8 +213,8 @@ function drawShape(g: PixiGraphics, shape: 'circle' | 'ellipse', props: {
|
|
|
95
213
|
color: Signal<string>;
|
|
96
214
|
border: Signal<number>;
|
|
97
215
|
} | {
|
|
98
|
-
width:
|
|
99
|
-
height:
|
|
216
|
+
width: WritableSignal<number>;
|
|
217
|
+
height: WritableSignal<number>;
|
|
100
218
|
color: Signal<string>;
|
|
101
219
|
border: Signal<number>;
|
|
102
220
|
}) {
|
|
@@ -127,7 +245,7 @@ export function Ellipse(props: EllipseProps) {
|
|
|
127
245
|
border: null
|
|
128
246
|
})
|
|
129
247
|
return Graphics({
|
|
130
|
-
draw: (g) => drawShape(g, 'ellipse', { width, height, color, border }),
|
|
248
|
+
draw: (g, gWidth, gHeight) => drawShape(g, 'ellipse', { width: signal(gWidth), height: signal(gHeight), color, border }),
|
|
131
249
|
...props
|
|
132
250
|
})
|
|
133
251
|
}
|
|
@@ -138,11 +256,11 @@ export function Triangle(props: TriangleProps) {
|
|
|
138
256
|
color: '#000'
|
|
139
257
|
})
|
|
140
258
|
return Graphics({
|
|
141
|
-
draw: (g) => {
|
|
142
|
-
g.moveTo(0,
|
|
143
|
-
g.lineTo(
|
|
144
|
-
g.lineTo(
|
|
145
|
-
g.lineTo(0,
|
|
259
|
+
draw: (g, gWidth, gHeight) => {
|
|
260
|
+
g.moveTo(0, gHeight);
|
|
261
|
+
g.lineTo(gWidth / 2, 0);
|
|
262
|
+
g.lineTo(gWidth, gHeight);
|
|
263
|
+
g.lineTo(0, gHeight);
|
|
146
264
|
g.fill(color());
|
|
147
265
|
if (border) {
|
|
148
266
|
g.stroke(border);
|
|
@@ -152,9 +270,55 @@ export function Triangle(props: TriangleProps) {
|
|
|
152
270
|
})
|
|
153
271
|
}
|
|
154
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Creates an SVG component that can render SVG graphics from URL, content, or legacy svg prop.
|
|
275
|
+
*
|
|
276
|
+
* This component provides three ways to display SVG graphics:
|
|
277
|
+
* - **src**: Load SVG from a URL using Assets.load with parseAsGraphicsContext option
|
|
278
|
+
* - **content**: Render SVG directly from string content using Graphics.svg() method
|
|
279
|
+
* - **svg**: Legacy prop for SVG content (for backward compatibility)
|
|
280
|
+
*
|
|
281
|
+
* @param props - Component properties including src, content, or svg
|
|
282
|
+
* @returns A reactive SVG component
|
|
283
|
+
* @example
|
|
284
|
+
* ```typescript
|
|
285
|
+
* // Load from URL
|
|
286
|
+
* const svgFromUrl = Svg({ src: "/assets/logo.svg" });
|
|
287
|
+
*
|
|
288
|
+
* // Direct content
|
|
289
|
+
* const svgFromContent = Svg({
|
|
290
|
+
* content: `<svg viewBox="0 0 100 100">
|
|
291
|
+
* <circle cx="50" cy="50" r="40" fill="blue"/>
|
|
292
|
+
* </svg>`
|
|
293
|
+
* });
|
|
294
|
+
*
|
|
295
|
+
* // Legacy usage
|
|
296
|
+
* const svgLegacy = Svg({ svg: "<svg>...</svg>" });
|
|
297
|
+
* ```
|
|
298
|
+
*/
|
|
155
299
|
export function Svg(props: SvgProps) {
|
|
156
300
|
return Graphics({
|
|
157
|
-
draw: (g) =>
|
|
301
|
+
draw: async (g) => {
|
|
302
|
+
if (props.src) {
|
|
303
|
+
// Load SVG from source URL with graphics context parsing
|
|
304
|
+
const svgData = await Assets.load({
|
|
305
|
+
src: props.src,
|
|
306
|
+
data: {
|
|
307
|
+
parseAsGraphicsContext: true,
|
|
308
|
+
},
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
// Apply the loaded graphics context
|
|
312
|
+
const graphics = new PixiGraphics(svgData);
|
|
313
|
+
g.context = graphics.context;
|
|
314
|
+
} else if (props.content) {
|
|
315
|
+
// Render SVG directly from content string
|
|
316
|
+
g.svg(props.content);
|
|
317
|
+
} else if (props.svg) {
|
|
318
|
+
// Legacy prop support
|
|
319
|
+
g.svg(props.svg);
|
|
320
|
+
}
|
|
321
|
+
},
|
|
158
322
|
...props
|
|
159
323
|
})
|
|
160
324
|
}
|