canvasengine 2.0.0-rc.2 → 2.0.0-rc.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasengine",
3
- "version": "2.0.0-rc.2",
3
+ "version": "2.0.0-rc.3",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1,4 +1,4 @@
1
- import { Container as PixiContainer } from "pixi.js";
1
+ import { Container as PixiContainer, type ContainerChild } from "pixi.js";
2
2
  import { createComponent, registerComponent } from "../engine/reactive";
3
3
  import { DisplayObject } from "./DisplayObject";
4
4
  import { ComponentFunction } from "../engine/signal";
@@ -6,8 +6,16 @@ import { DisplayObjectProps } from "./types/DisplayObject";
6
6
  import { setObservablePoint } from "../engine/utils";
7
7
  import { isPercent } from "../utils/functions";
8
8
 
9
- interface ContainerProps extends DisplayObjectProps {
9
+ export interface ContainerProps extends DisplayObjectProps {
10
10
  sortableChildren?: boolean;
11
+ /**
12
+ * Native PixiJS display objects to add to this container when it mounts.
13
+ *
14
+ * This is an escape hatch for rendering PixiJS objects directly inside the
15
+ * CanvasEngine scene graph. CanvasEngine does not manage these children's
16
+ * props or lifecycle; destroy or update them manually when needed.
17
+ */
18
+ pixiChildren?: ContainerChild[];
11
19
  }
12
20
 
13
21
  export class CanvasContainer extends DisplayObject(PixiContainer) {
@@ -343,6 +343,14 @@ export class CanvasDOMContainer extends DisplayObject(PixiDOMContainer) {
343
343
  }
344
344
 
345
345
  async onMount(element: Element<any>, index?: number) {
346
+ const parentTag = element.parent?.tag;
347
+ if (parentTag === "DOMContainer" || parentTag === "DOMElement") {
348
+ this.onUpdate(element.props);
349
+ this.syncCanvasSizeEffect();
350
+ this.applyElementSize();
351
+ return;
352
+ }
353
+
346
354
  await super.onMount(element, index);
347
355
  this.syncCanvasSizeEffect();
348
356
  this.applyElementSize();
@@ -88,6 +88,9 @@ class CanvasGraphics extends DisplayObject(PixiGraphics) {
88
88
  */
89
89
  async onMount(element: Element<any>, index?: number): Promise<void> {
90
90
  await super.onMount(element, index);
91
+ if (this.destroyed || !this.parent) {
92
+ return;
93
+ }
91
94
  const { props, propObservables } = element;
92
95
 
93
96
  // Use original signals from propObservables if available, otherwise create new ones
@@ -111,6 +114,9 @@ class CanvasGraphics extends DisplayObject(PixiGraphics) {
111
114
  if (typeof w == 'string' || typeof h == 'string') {
112
115
  return
113
116
  }
117
+ if (this.destroyed || !this.parent) {
118
+ return
119
+ }
114
120
  this.clear();
115
121
  props.draw?.(this, w, h, a);
116
122
  this.subjectInit.next(this)
@@ -1,9 +1,8 @@
1
- import { Effect, effect } from "@signe/reactive";
1
+ import { isSignal } from "@signe/reactive";
2
2
  import { Mesh as PixiMesh, Geometry, Shader, Texture, Assets, BLEND_MODES } 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
- import { useProps } from "../hooks/useProps";
7
6
  import { SignalOrPrimitive } from "./types";
8
7
  import { ComponentFunction } from "../engine/signal";
9
8
 
@@ -13,19 +12,31 @@ import { ComponentFunction } from "../engine/signal";
13
12
  */
14
13
  interface MeshProps extends DisplayObjectProps {
15
14
  /** The geometry defining the mesh structure (vertices, indices, UVs, etc.) */
16
- geometry?: Geometry;
15
+ geometry?: SignalOrPrimitive<Geometry>;
17
16
  /** The shader to render the mesh with */
18
- shader?: Shader;
17
+ shader?: SignalOrPrimitive<Shader>;
19
18
  /** The texture to apply to the mesh */
20
- texture?: Texture | string;
19
+ texture?: SignalOrPrimitive<Texture | string>;
21
20
  /** The image URL to load as texture */
22
- image?: string;
21
+ image?: SignalOrPrimitive<string>;
23
22
  /** The tint color to apply to the mesh */
24
23
  tint?: SignalOrPrimitive<number>;
25
24
  /** Whether to round pixels for sharper rendering */
26
25
  roundPixels?: SignalOrPrimitive<boolean>;
27
26
  }
28
27
 
28
+ const resolveProp = <T>(value: SignalOrPrimitive<T> | undefined): T | undefined => {
29
+ return isSignal(value as any) ? (value as any)() : value as T | undefined;
30
+ };
31
+
32
+ const isValidGeometry = (value: Geometry | undefined): value is Geometry => {
33
+ if (!value) return false;
34
+ if (value instanceof Geometry) return true;
35
+
36
+ const geometry = value as any;
37
+ return typeof geometry.on === 'function' && typeof geometry.off === 'function';
38
+ };
39
+
29
40
  /**
30
41
  * Canvas Mesh component class that extends DisplayObject with PixiMesh functionality.
31
42
  * This component allows rendering of custom 3D meshes with shaders and textures.
@@ -87,17 +98,19 @@ class CanvasMesh extends DisplayObject(PixiMesh) {
87
98
  super.onInit(props);
88
99
 
89
100
  // Set initial geometry if provided
90
- if (props.geometry) {
101
+ const geometry = resolveProp(props.geometry);
102
+ if (isValidGeometry(geometry)) {
91
103
  try {
92
- this.geometry = props.geometry;
104
+ this.geometry = geometry;
93
105
  } catch (error) {
94
106
  console.warn('Failed to set geometry:', error);
95
107
  }
96
108
  }
97
109
 
98
110
  // Set initial shader if provided
99
- if (props.shader) {
100
- this.shader = props.shader;
111
+ const shader = resolveProp(props.shader);
112
+ if (shader) {
113
+ this.shader = shader;
101
114
  }
102
115
  }
103
116
 
@@ -119,28 +132,32 @@ class CanvasMesh extends DisplayObject(PixiMesh) {
119
132
  super.onUpdate(props);
120
133
 
121
134
  // Handle geometry updates
122
- if (props.geometry) {
135
+ const geometry = resolveProp(props.geometry);
136
+ if (isValidGeometry(geometry)) {
123
137
  try {
124
- this.geometry = props.geometry;
138
+ this.geometry = geometry;
125
139
  } catch (error) {
126
140
  console.warn('Failed to update geometry:', error);
127
141
  }
128
142
  }
129
143
 
130
144
  // Handle shader/material updates
131
- if (props.shader) {
132
- this.shader = props.shader;
145
+ const shader = resolveProp(props.shader);
146
+ if (shader) {
147
+ this.shader = shader;
133
148
  }
134
149
 
135
150
  // Handle texture updates
136
- if (props.texture) {
137
- if (typeof props.texture === 'string') {
138
- this.texture = await Assets.load(props.texture);
151
+ const texture = resolveProp(props.texture);
152
+ const image = resolveProp(props.image);
153
+ if (texture) {
154
+ if (typeof texture === 'string') {
155
+ this.texture = await Assets.load(texture);
139
156
  } else {
140
- this.texture = props.texture;
157
+ this.texture = texture;
141
158
  }
142
- } else if (props.image) {
143
- this.texture = await Assets.load(props.image);
159
+ } else if (image) {
160
+ this.texture = await Assets.load(image);
144
161
  }
145
162
 
146
163
  // Handle tint updates
@@ -219,4 +236,4 @@ export const Mesh: ComponentFunction<MeshProps> = (props) => {
219
236
  export { CanvasMesh };
220
237
 
221
238
  // Export the props interface for TypeScript users
222
- export type { MeshProps };
239
+ export type { MeshProps };
@@ -181,10 +181,10 @@ export class CanvasViewport extends DisplayObject(Container) {
181
181
 
182
182
  private updateMask() {
183
183
  if (!this.#mask) return
184
- this.#mask.clear()
185
- this.#mask.beginFill(0xffffff)
186
- this.#mask.drawRect(0, 0, this.viewport.screenWidth, this.viewport.screenHeight)
187
- this.#mask.endFill()
184
+ this.#mask
185
+ .clear()
186
+ .rect(0, 0, this.viewport.screenWidth, this.viewport.screenHeight)
187
+ .fill(0xffffff)
188
188
  }
189
189
 
190
190
  /**
@@ -1,5 +1,5 @@
1
1
  export { Canvas } from './Canvas'
2
- export { Container } from './Container'
2
+ export { Container, type ContainerProps } from './Container'
3
3
  export { Graphics, Rect, Circle, Ellipse, Triangle, Svg } from './Graphic'
4
4
  export { Mesh } from './Mesh'
5
5
  export { Scene } from './Scene'
@@ -65,6 +65,9 @@ export interface LoopOptions<T> {
65
65
  }
66
66
 
67
67
  const components: { [key: string]: any } = {};
68
+ const HOT_COMPONENT_PROPS = "__canvasEngineHotProps";
69
+ const HOT_COMPONENT_UPDATE_PROPS = "__canvasEngineUpdateHotProps";
70
+ const DEFINE_PROPS_SIGNALS = "__canvasEngineDefinePropsSignals";
68
71
 
69
72
  export const isElement = (value: any): value is Element => {
70
73
  return (
@@ -114,6 +117,24 @@ const DOM_UNSUPPORTED_TAGS = new Set([
114
117
  "FocusContainer",
115
118
  ]);
116
119
 
120
+ const readSignalValue = (value: any) => isSignal(value) ? value() : value;
121
+
122
+ const patchDefinePropsSignals = (target: Element, source: Element) => {
123
+ const targetSignals = (target as any)[DEFINE_PROPS_SIGNALS];
124
+ const sourceSignals = (source as any)[DEFINE_PROPS_SIGNALS];
125
+
126
+ if (!targetSignals || !sourceSignals) {
127
+ return;
128
+ }
129
+
130
+ Object.entries(sourceSignals as Record<string, any>).forEach(([key, sourceSignal]) => {
131
+ const targetSignal = targetSignals[key];
132
+ if (targetSignal && typeof targetSignal.set === "function") {
133
+ targetSignal.set(readSignalValue(sourceSignal));
134
+ }
135
+ });
136
+ };
137
+
117
138
  const hasDomAncestor = (element: Element | null): boolean => {
118
139
  let current = element;
119
140
  while (current) {
@@ -700,6 +721,78 @@ export function createComponent(tag: string, props?: Props): Element {
700
721
  return getNextGroupIndex();
701
722
  };
702
723
 
724
+ const collectMountedInstances = (
725
+ element: Element,
726
+ instances: any[],
727
+ childIndex: Map<any, number>,
728
+ seen = new Set<Element>()
729
+ ) => {
730
+ if (!element || seen.has(element)) return;
731
+ seen.add(element);
732
+
733
+ const instance = element.componentInstance as any;
734
+ if (childIndex.has(instance)) {
735
+ instances.push(instance);
736
+ return;
737
+ }
738
+
739
+ const nestedGroups = ((element as any).__childGroups ?? [])
740
+ .slice()
741
+ .sort((a, b) => a.order - b.order);
742
+ for (const group of nestedGroups) {
743
+ for (const mounted of group.mounted.values()) {
744
+ collectMountedInstances(mounted, instances, childIndex, seen);
745
+ }
746
+ }
747
+ };
748
+
749
+ const reorderMountedChildGroups = () => {
750
+ if (childGroups.length < 2) return;
751
+
752
+ const parentInstance = parent.componentInstance as any;
753
+ const children = parentInstance?.children;
754
+ if (!children || typeof parentInstance.addChildAt !== "function") return;
755
+
756
+ const childIndex = new Map<any, number>();
757
+ children.forEach((child, index) => {
758
+ childIndex.set(child, index);
759
+ });
760
+
761
+ const orderedInstances: any[] = [];
762
+ const orderedGroups = childGroups
763
+ .slice()
764
+ .sort((a, b) => a.order - b.order);
765
+
766
+ for (const group of orderedGroups) {
767
+ for (const mounted of group.mounted.values()) {
768
+ collectMountedInstances(mounted, orderedInstances, childIndex);
769
+ }
770
+ }
771
+
772
+ const mountedIndices = orderedInstances
773
+ .map((instance) => childIndex.get(instance))
774
+ .filter((index): index is number => index !== undefined);
775
+ if (!mountedIndices.length) return;
776
+
777
+ let targetIndex = Math.min(...mountedIndices);
778
+ for (const instance of orderedInstances) {
779
+ if (children[targetIndex] !== instance) {
780
+ parentInstance.addChildAt(instance, targetIndex);
781
+ }
782
+ targetIndex++;
783
+ }
784
+ };
785
+
786
+ const mountElementAtDeclaredOrder = (
787
+ element: Element,
788
+ sourceIndex: number,
789
+ orderedSources: any[]
790
+ ) => {
791
+ const mountResult = onMount(parent, element, getInsertIndex(sourceIndex, orderedSources));
792
+ void Promise.resolve(mountResult).then(reorderMountedChildGroups);
793
+ return mountResult;
794
+ };
795
+
703
796
  if (child instanceof Observable) {
704
797
  const mountedFlowElements = childGroup.mounted;
705
798
  const flowEffectSubscriptions = ((child as any).effectSubscriptions ?? []) as Subscription[];
@@ -759,7 +852,7 @@ export function createComponent(tag: string, props?: Props): Element {
759
852
  const routed = routeDomComponent(parent, element);
760
853
  applyFlowEffects(routed);
761
854
  mountedFlowElements.set(element, routed);
762
- onMount(parent, routed, getInsertIndex(sourceIndex, orderedSources));
855
+ mountElementAtDeclaredOrder(routed, sourceIndex, orderedSources);
763
856
  propagateContext(routed);
764
857
  };
765
858
 
@@ -840,7 +933,7 @@ export function createComponent(tag: string, props?: Props): Element {
840
933
  const routed = routeDomComponent(parent, value);
841
934
  applyFlowEffects(routed);
842
935
  childGroup.mounted.set(value, routed);
843
- onMount(parent, routed, getInsertIndex(0, [value]));
936
+ mountElementAtDeclaredOrder(routed, 0, [value]);
844
937
  propagateContext(routed);
845
938
  } else if (Array.isArray(value)) {
846
939
  // Handle array of elements (which can also be observables)
@@ -867,7 +960,7 @@ export function createComponent(tag: string, props?: Props): Element {
867
960
  } else if (isElement(child)) {
868
961
  const routed = routeDomComponent(parent, child);
869
962
  childGroup.mounted.set(child, routed);
870
- onMount(parent, routed, getInsertIndex(0, [child]));
963
+ mountElementAtDeclaredOrder(routed, 0, [child]);
871
964
  await propagateContext(routed);
872
965
  }
873
966
  }
@@ -933,14 +1026,36 @@ export function loop<T>(
933
1026
  element.effectUnmounts?.forEach((fn) => fn?.());
934
1027
  };
935
1028
 
1029
+ const updateTrackedHotChildren = (targetChildren: any, sourceChildren: any) => {
1030
+ const targetList = Array.isArray(targetChildren) ? targetChildren : [targetChildren];
1031
+ const sourceList = Array.isArray(sourceChildren) ? sourceChildren : [sourceChildren];
1032
+ let updated = false;
1033
+
1034
+ targetList.forEach((targetChild, index) => {
1035
+ const sourceChild = sourceList[index];
1036
+ const updateProps = targetChild?.[HOT_COMPONENT_UPDATE_PROPS];
1037
+ const nextProps = sourceChild?.[HOT_COMPONENT_PROPS];
1038
+
1039
+ if (typeof updateProps === "function" && nextProps !== undefined) {
1040
+ updateProps(nextProps);
1041
+ updated = true;
1042
+ }
1043
+ });
1044
+
1045
+ return updated;
1046
+ };
1047
+
936
1048
  const patchTrackedElement = (target: Element, source: Element) => {
937
1049
  const nextProps = { ...source.props };
938
1050
  const nextPropObservables = source.propObservables;
1051
+ const updatedHotChildren = updateTrackedHotChildren(target.props.children, source.props.children);
1052
+
1053
+ patchDefinePropsSignals(target, source);
939
1054
 
940
1055
  if (target.props.context) {
941
1056
  nextProps.context = target.props.context;
942
1057
  }
943
- if (target.props.children && !source.props.children) {
1058
+ if (updatedHotChildren || (target.props.children && !source.props.children)) {
944
1059
  nextProps.children = target.props.children;
945
1060
  }
946
1061
 
@@ -22,7 +22,12 @@ type HotComponentRecord = {
22
22
  wrapper?: ComponentFunction<any>;
23
23
  };
24
24
 
25
+ const HOT_COMPONENT_PROPS = "__canvasEngineHotProps";
26
+ const HOT_COMPONENT_UPDATE_PROPS = "__canvasEngineUpdateHotProps";
27
+ const DEFINE_PROPS_SIGNALS = "__canvasEngineDefinePropsSignals";
28
+
25
29
  export let currentSubscriptionsTracker: ((subscription: Subscription) => void) | null = null;
30
+ export let currentDefinePropsTracker: ((signals: Record<string, any>) => void) | null = null;
26
31
  export let mountTracker: MountFunction | null = null;
27
32
 
28
33
  const getHotComponentRegistry = (): Map<string, HotComponentRecord> => {
@@ -163,11 +168,19 @@ function createTrackedComponent<C extends ComponentFunction<any>>(
163
168
  ): ReturnType<C> {
164
169
  const allSubscriptions = new Set<Subscription>();
165
170
  const allMounts = new Set<MountCallback>();
171
+ let allDefinePropSignals: Record<string, any> | null = null;
166
172
 
167
173
  currentSubscriptionsTracker = (subscription) => {
168
174
  allSubscriptions.add(subscription);
169
175
  };
170
176
 
177
+ currentDefinePropsTracker = (signals) => {
178
+ allDefinePropSignals = {
179
+ ...(allDefinePropSignals ?? {}),
180
+ ...signals,
181
+ };
182
+ };
183
+
171
184
  mountTracker = (fn: any) => {
172
185
  allMounts.add(fn);
173
186
  };
@@ -177,6 +190,7 @@ function createTrackedComponent<C extends ComponentFunction<any>>(
177
190
  component = componentFunction(props) as ReturnType<C>;
178
191
  } finally {
179
192
  currentSubscriptionsTracker = null;
193
+ currentDefinePropsTracker = null;
180
194
  mountTracker = null;
181
195
  }
182
196
 
@@ -190,6 +204,9 @@ function createTrackedComponent<C extends ComponentFunction<any>>(
190
204
  ...Array.from(allMounts),
191
205
  ...((element as any).effectMounts ?? [])
192
206
  ];
207
+ if (allDefinePropSignals) {
208
+ (element as any)[DEFINE_PROPS_SIGNALS] = allDefinePropSignals;
209
+ }
193
210
  };
194
211
 
195
212
  if (component instanceof Promise) {
@@ -208,6 +225,9 @@ function createTrackedComponent<C extends ComponentFunction<any>>(
208
225
  ...Array.from(allMounts),
209
226
  ...((component as any).effectMounts ?? [])
210
227
  ];
228
+ if (allDefinePropSignals) {
229
+ (component as any)[DEFINE_PROPS_SIGNALS] = allDefinePropSignals;
230
+ }
211
231
  } else {
212
232
  applyTrackedEffects(component as Element);
213
233
  }
@@ -235,14 +255,55 @@ export function createHotComponent<P>(
235
255
 
236
256
  if (!record.wrapper) {
237
257
  record.wrapper = ((props: P) => {
238
- return new Observable<HotFlowResult>((subscriber) => {
258
+ let currentProps = props;
259
+
260
+ const observable = new Observable<HotFlowResult>((subscriber) => {
239
261
  let disposed = false;
240
262
  let currentElement: Element | null = null;
241
263
 
242
- const emit = () => {
243
- const rendered = createTrackedComponent(record!.component, props);
264
+ const patchElement = (target: Element, source: Element) => {
265
+ if (target.tag !== source.tag) {
266
+ return false;
267
+ }
268
+
269
+ const nextProps = { ...source.props };
270
+ if (target.props.context) {
271
+ nextProps.context = target.props.context;
272
+ }
273
+ if (target.props.children && !source.props.children) {
274
+ nextProps.children = target.props.children;
275
+ }
276
+
277
+ target.props = nextProps;
278
+ target.propObservables = source.propObservables;
279
+ target.componentInstance.onUpdate?.(nextProps);
280
+ Object.entries(target.directives).forEach(([name, directive]) => {
281
+ if (name in nextProps) {
282
+ directive.onUpdate?.(nextProps[name], target);
283
+ }
284
+ });
285
+
286
+ source.propSubscriptions?.forEach((sub) => sub.unsubscribe());
287
+ source.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
288
+ source.effectUnmounts?.forEach((fn) => fn?.());
289
+
290
+ return true;
291
+ };
292
+
293
+ const emit = (preserveCurrentElement = false) => {
294
+ const rendered = createTrackedComponent(record!.component, currentProps);
244
295
  const next = (element: Element | null | undefined) => {
245
296
  if (!disposed) {
297
+ if (
298
+ preserveCurrentElement &&
299
+ currentElement &&
300
+ element &&
301
+ patchElement(currentElement, element)
302
+ ) {
303
+ subscriber.next({ elements: [currentElement] });
304
+ return;
305
+ }
306
+
246
307
  subscriber.next({ elements: element ? [element] : [] });
247
308
  if (currentElement && currentElement !== element) {
248
309
  destroyElement(currentElement);
@@ -259,7 +320,12 @@ export function createHotComponent<P>(
259
320
  };
260
321
 
261
322
  emit();
262
- const subscription = record!.updates.subscribe(emit);
323
+ (observable as any)[HOT_COMPONENT_UPDATE_PROPS] = (nextProps: P) => {
324
+ currentProps = nextProps;
325
+ emit(true);
326
+ };
327
+
328
+ const subscription = record!.updates.subscribe(() => emit());
263
329
 
264
330
  return () => {
265
331
  disposed = true;
@@ -270,6 +336,10 @@ export function createHotComponent<P>(
270
336
  subscription.unsubscribe();
271
337
  };
272
338
  }) as any;
339
+
340
+ (observable as any)[HOT_COMPONENT_PROPS] = props;
341
+
342
+ return observable;
273
343
  }) as ComponentFunction<any>;
274
344
  }
275
345
 
@@ -1,5 +1,6 @@
1
1
  import { isSignal, signal } from "@signe/reactive"
2
2
  import { isPrimitive } from "../engine/reactive"
3
+ import { currentDefinePropsTracker } from "../engine/signal"
3
4
 
4
5
  /**
5
6
  * Converts props into reactive signals if they are primitive values.
@@ -122,10 +123,13 @@ export const useDefineProps = (props: any) => {
122
123
  validatedProps[key] = toPropSignal(validatedValue)
123
124
  }
124
125
 
125
- return {
126
+ const definedProps = {
126
127
  ...definePropSignals(rawProps),
127
128
  ...validatedProps
128
129
  }
130
+ currentDefinePropsTracker?.(definedProps)
131
+
132
+ return definedProps
129
133
  }
130
134
  }
131
135