canvasengine 2.0.0-beta.2 → 2.0.0-beta.21

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.
Files changed (41) hide show
  1. package/dist/index.d.ts +1289 -0
  2. package/dist/index.js +4248 -0
  3. package/dist/index.js.map +1 -0
  4. package/index.d.ts +4 -0
  5. package/package.json +5 -12
  6. package/src/components/Canvas.ts +53 -45
  7. package/src/components/Container.ts +2 -2
  8. package/src/components/DOMContainer.ts +123 -0
  9. package/src/components/DOMElement.ts +421 -0
  10. package/src/components/DisplayObject.ts +263 -189
  11. package/src/components/Graphic.ts +213 -36
  12. package/src/components/Mesh.ts +222 -0
  13. package/src/components/NineSliceSprite.ts +4 -1
  14. package/src/components/ParticleEmitter.ts +12 -8
  15. package/src/components/Sprite.ts +77 -14
  16. package/src/components/Text.ts +34 -14
  17. package/src/components/Video.ts +110 -0
  18. package/src/components/Viewport.ts +59 -43
  19. package/src/components/index.ts +6 -4
  20. package/src/components/types/DisplayObject.ts +30 -0
  21. package/src/directives/Drag.ts +357 -52
  22. package/src/directives/KeyboardControls.ts +3 -1
  23. package/src/directives/Sound.ts +94 -31
  24. package/src/directives/ViewportFollow.ts +35 -7
  25. package/src/engine/animation.ts +41 -5
  26. package/src/engine/bootstrap.ts +22 -3
  27. package/src/engine/directive.ts +2 -2
  28. package/src/engine/reactive.ts +337 -168
  29. package/src/engine/trigger.ts +65 -9
  30. package/src/engine/utils.ts +97 -9
  31. package/src/hooks/useProps.ts +1 -1
  32. package/src/index.ts +5 -1
  33. package/src/utils/RadialGradient.ts +29 -0
  34. package/src/utils/functions.ts +7 -0
  35. package/testing/index.ts +12 -0
  36. package/src/components/DrawMap/index.ts +0 -65
  37. package/src/components/Tilemap/Tile.ts +0 -79
  38. package/src/components/Tilemap/TileGroup.ts +0 -207
  39. package/src/components/Tilemap/TileLayer.ts +0 -163
  40. package/src/components/Tilemap/TileSet.ts +0 -41
  41. package/src/components/Tilemap/index.ts +0 -80
@@ -1,4 +1,4 @@
1
- import { Signal, WritableArraySignal, isSignal } from "@signe/reactive";
1
+ import { ArrayChange, ObjectChange, Signal, WritableArraySignal, WritableObjectSignal, isComputed, isSignal, signal } from "@signe/reactive";
2
2
  import {
3
3
  Observable,
4
4
  Subject,
@@ -7,6 +7,7 @@ import {
7
7
  from,
8
8
  map,
9
9
  of,
10
+ share,
10
11
  switchMap,
11
12
  } from "rxjs";
12
13
  import { ComponentInstance } from "../components/DisplayObject";
@@ -17,18 +18,6 @@ export interface Props {
17
18
  [key: string]: any;
18
19
  }
19
20
 
20
- export type ArrayChange<T> = {
21
- type: "add" | "remove" | "update" | "init" | "reset";
22
- index?: number;
23
- items: T[];
24
- };
25
-
26
- type ElementObservable<T> = Observable<
27
- ArrayChange<T> & {
28
- value: Element | Element[];
29
- }
30
- >;
31
-
32
21
  type NestedSignalObjects = {
33
22
  [Key in string]: NestedSignalObjects | Signal<any>;
34
23
  };
@@ -53,10 +42,13 @@ export interface Element<T = ComponentInstance> {
53
42
  allElements: Subject<void>;
54
43
  }
55
44
 
56
- type FlowObservable = Observable<{
45
+ type FlowResult = {
57
46
  elements: Element[];
58
47
  prev?: Element;
59
- }>;
48
+ fullElements?: Element[];
49
+ };
50
+
51
+ type FlowObservable = Observable<FlowResult>;
60
52
 
61
53
  const components: { [key: string]: any } = {};
62
54
 
@@ -92,13 +84,14 @@ function destroyElement(element: Element | Element[]) {
92
84
  if (!element) {
93
85
  return;
94
86
  }
95
- element.propSubscriptions.forEach((sub) => sub.unsubscribe());
96
- element.effectSubscriptions.forEach((sub) => sub.unsubscribe());
97
87
  for (let name in element.directives) {
98
- element.directives[name].onDestroy?.();
88
+ element.directives[name].onDestroy?.(element);
99
89
  }
100
- element.componentInstance.onDestroy?.(element.parent as any);
101
- element.effectUnmounts.forEach((fn) => fn?.());
90
+ element.componentInstance.onDestroy(element.parent as any, () => {
91
+ element.propSubscriptions.forEach((sub) => sub.unsubscribe());
92
+ element.effectSubscriptions.forEach((sub) => sub.unsubscribe());
93
+ element.effectUnmounts.forEach((fn) => fn?.());
94
+ });
102
95
  }
103
96
 
104
97
  /**
@@ -155,7 +148,7 @@ export function createComponent(tag: string, props?: Props): Element {
155
148
  _value.observable.subscribe((value) => {
156
149
  _set(path, key, value);
157
150
  if (element.directives[key]) {
158
- element.directives[key].onUpdate?.(value);
151
+ element.directives[key].onUpdate?.(value, element);
159
152
  }
160
153
  if (key == "tick") {
161
154
  return
@@ -182,9 +175,24 @@ export function createComponent(tag: string, props?: Props): Element {
182
175
  }
183
176
 
184
177
  instance.onInit?.(element.props);
185
- instance.onUpdate?.(element.props);
186
178
 
187
- const onMount = (parent: Element, element: Element, index?: number) => {
179
+ const elementsListen = new Subject<any>()
180
+
181
+ if (props?.isRoot) {
182
+ element.allElements = elementsListen
183
+ element.props.context.rootElement = element;
184
+ element.componentInstance.onMount?.(element);
185
+ propagateContext(element);
186
+ }
187
+
188
+ if (props) {
189
+ for (let key in props) {
190
+ const directive = applyDirective(element, key);
191
+ if (directive) element.directives[key] = directive;
192
+ }
193
+ }
194
+
195
+ function onMount(parent: Element, element: Element, index?: number) {
188
196
  element.props.context = parent.props.context;
189
197
  element.parent = parent;
190
198
  element.componentInstance.onMount?.(element, index);
@@ -196,68 +204,79 @@ export function createComponent(tag: string, props?: Props): Element {
196
204
  });
197
205
  };
198
206
 
199
- const elementsListen = new Subject<any>()
200
-
201
- if (props?.isRoot) {
202
- // propagate recrusively context in all children
203
- const propagateContext = async (element) => {
204
- if (!element.props.children) {
205
- return;
207
+ async function propagateContext(element) {
208
+ if (element.props.attach) {
209
+ const isReactiveAttach = isSignal(element.propObservables?.attach)
210
+ if (!isReactiveAttach) {
211
+ element.props.children.push(element.props.attach)
206
212
  }
207
- for (let child of element.props.children) {
208
- if (!child) continue;
209
- if (isPromise(child)) {
210
- child = await child;
211
- }
212
- if (child instanceof Observable) {
213
- child.subscribe(
214
- ({
215
- elements: comp,
216
- prev,
217
- }: {
218
- elements: Element[];
219
- prev?: Element;
220
- }) => {
221
- // if prev, insert element after this
222
- const components = comp.filter((c) => c !== null);
223
- if (prev) {
224
- components.forEach((c) => {
225
- const index = element.props.children.indexOf(prev.props.key);
226
- onMount(element, c, index + 1);
227
- propagateContext(c);
228
- });
229
- return;
230
- }
231
- components.forEach((component) => {
232
- if (!Array.isArray(component)) {
233
- onMount(element, component);
234
- propagateContext(component);
235
- } else {
236
- component.forEach((comp) => {
237
- onMount(element, comp);
238
- propagateContext(comp);
239
- });
213
+ else {
214
+ await new Promise((resolve) => {
215
+ let lastElement = null
216
+ element.propSubscriptions.push(element.propObservables.attach.observable.subscribe(async (args) => {
217
+ const value = args?.value ?? args
218
+ if (!value) {
219
+ throw new Error(`attach in ${element.tag} is undefined or null, add a component`)
220
+ }
221
+ if (lastElement) {
222
+ destroyElement(lastElement)
240
223
  }
224
+ lastElement = value
225
+ await createElement(element, value)
226
+ resolve(undefined)
227
+ }))
228
+ })
229
+ }
230
+ }
231
+ if (!element.props.children) {
232
+ return;
233
+ }
234
+ for (let child of element.props.children) {
235
+ if (!child) continue;
236
+ await createElement(element, child)
237
+ }
238
+ };
239
+
240
+ async function createElement(parent: Element, child: Element) {
241
+ if (isPromise(child)) {
242
+ child = await child;
243
+ }
244
+ if (child instanceof Observable) {
245
+ child.subscribe(
246
+ ({
247
+ elements: comp,
248
+ prev,
249
+ }: {
250
+ elements: Element[];
251
+ prev?: Element;
252
+ }) => {
253
+ // if prev, insert element after this
254
+ const components = comp.filter((c) => c !== null);
255
+ if (prev) {
256
+ components.forEach((c) => {
257
+ const index = parent.props.children.indexOf(prev.props.key);
258
+ onMount(parent, c, index + 1);
259
+ propagateContext(c);
260
+ });
261
+ return;
262
+ }
263
+ components.forEach((component) => {
264
+ if (!Array.isArray(component)) {
265
+ onMount(parent, component);
266
+ propagateContext(component);
267
+ } else {
268
+ component.forEach((comp) => {
269
+ onMount(parent, comp);
270
+ propagateContext(comp);
241
271
  });
242
- elementsListen.next(undefined)
243
272
  }
244
- );
245
- } else {
246
- onMount(element, child);
247
- await propagateContext(child);
273
+ });
274
+ elementsListen.next(undefined)
248
275
  }
249
- }
250
- };
251
- element.allElements = elementsListen
252
- element.props.context.rootElement = element;
253
- element.componentInstance.onMount?.(element);
254
- propagateContext(element);
255
- }
256
-
257
- if (props) {
258
- for (let key in props) {
259
- const directive = applyDirective(element, key);
260
- if (directive) element.directives[key] = directive;
276
+ );
277
+ } else {
278
+ onMount(parent, child);
279
+ await propagateContext(child);
261
280
  }
262
281
  }
263
282
 
@@ -266,114 +285,264 @@ export function createComponent(tag: string, props?: Props): Element {
266
285
  }
267
286
 
268
287
  /**
269
- * Observes a BehaviorSubject containing an array of items and dynamically creates child elements for each item.
288
+ * Observes a BehaviorSubject containing an array or object of items and dynamically creates child elements for each item.
270
289
  *
271
- * @param {BehaviorSubject<Array>} itemsSubject - A BehaviorSubject that emits an array of items.
290
+ * @param {WritableArraySignal<T> | WritableObjectSignal<T>} itemsSubject - A signal that emits an array or object of items.
272
291
  * @param {Function} createElementFn - A function that takes an item and returns an element representation.
273
292
  * @returns {Observable} An observable that emits the list of created child elements.
274
293
  */
275
- export function loop<T = any>(
276
- itemsSubject: WritableArraySignal<T>,
277
- createElementFn: (item: any, index: number) => Element | Promise<Element>
294
+ export function loop<T>(
295
+ itemsSubject: any,
296
+ createElementFn: (item: T, index: number | string) => Element | null
278
297
  ): FlowObservable {
279
- let elements: Element[] = [];
280
298
 
281
- const addAt = (items, insertIndex: number) => {
282
- return items.map((item, index) => {
283
- const element = createElementFn(item, insertIndex + index);
284
- elements.splice(insertIndex + index, 0, element as Element);
285
- return element;
286
- });
287
- };
299
+ if (isComputed(itemsSubject) && itemsSubject.dependencies.size == 0) {
300
+ itemsSubject = signal(itemsSubject());
301
+ }
302
+ else if (!isSignal(itemsSubject)) {
303
+ itemsSubject = signal(itemsSubject);
304
+ }
288
305
 
289
306
  return defer(() => {
290
- let initialItems = [...itemsSubject._subject.items];
291
- let init = true;
292
- return itemsSubject.observable.pipe(
293
- map((event: ArrayChange<T>) => {
294
- const { type, items, index } = event;
295
- if (init) {
296
- if (elements.length > 0) {
297
- return {
298
- elements: elements,
299
- fullElements: elements,
300
- };
301
- }
302
- const newElements = addAt(initialItems, 0);
303
- initialItems = [];
304
- init = false;
305
- return {
306
- elements: newElements,
307
- fullElements: elements,
308
- };
309
- } else if (type == "reset") {
310
- if (elements.length != 0) {
311
- elements.forEach((element) => {
312
- destroyElement(element);
307
+ let elements: Element[] = [];
308
+ let elementMap = new Map<string | number, Element>();
309
+ let isFirstSubscription = true;
310
+
311
+ const isArraySignal = (signal: any): signal is WritableArraySignal<T[]> =>
312
+ Array.isArray(signal());
313
+
314
+ return new Observable<FlowResult>(subscriber => {
315
+ const subscription = isArraySignal(itemsSubject)
316
+ ? itemsSubject.observable.subscribe(change => {
317
+ if (isFirstSubscription) {
318
+ isFirstSubscription = false;
319
+ elements.forEach(el => el.destroy());
320
+ elements = [];
321
+ elementMap.clear();
322
+
323
+ const items = itemsSubject();
324
+ if (items) {
325
+ items.forEach((item, index) => {
326
+ const element = createElementFn(item, index);
327
+ if (element) {
328
+ elements.push(element);
329
+ elementMap.set(index, element);
330
+ }
331
+ });
332
+ }
333
+ subscriber.next({
334
+ elements: [...elements]
335
+ });
336
+ return;
337
+ }
338
+
339
+ if (change.type === 'init' || change.type === 'reset') {
340
+ elements.forEach(el => el.destroy());
341
+ elements = [];
342
+ elementMap.clear();
343
+
344
+ const items = itemsSubject();
345
+ if (items) {
346
+ items.forEach((item, index) => {
347
+ const element = createElementFn(item, index);
348
+ if (element) {
349
+ elements.push(element);
350
+ elementMap.set(index, element);
351
+ }
352
+ });
353
+ }
354
+ } else if (change.type === 'add' && change.index !== undefined) {
355
+ const newElements = change.items.map((item, i) => {
356
+ const element = createElementFn(item as T, change.index! + i);
357
+ if (element) {
358
+ elementMap.set(change.index! + i, element);
359
+ }
360
+ return element;
361
+ }).filter((el): el is Element => el !== null);
362
+
363
+ elements.splice(change.index, 0, ...newElements);
364
+ } else if (change.type === 'remove' && change.index !== undefined) {
365
+ const removed = elements.splice(change.index, 1);
366
+ removed.forEach(el => {
367
+ el.destroy();
368
+ elementMap.delete(change.index!);
369
+ });
370
+ } else if (change.type === 'update' && change.index !== undefined && change.items.length === 1) {
371
+ const index = change.index;
372
+ const newItem = change.items[0];
373
+
374
+ // Check if the previous item at this index was effectively undefined or non-existent
375
+ if (index >= elements.length || elements[index] === undefined || !elementMap.has(index)) {
376
+ // Treat as add operation
377
+ const newElement = createElementFn(newItem as T, index);
378
+ if (newElement) {
379
+ elements.splice(index, 0, newElement); // Insert at the correct index
380
+ elementMap.set(index, newElement);
381
+ // Adjust indices in elementMap for subsequent elements might be needed if map relied on exact indices
382
+ // This simple implementation assumes keys are stable or createElementFn handles context correctly
383
+ } else {
384
+ console.warn(`Element creation returned null for index ${index} during add-like update.`);
385
+ }
386
+ } else {
387
+ // Treat as a standard update operation
388
+ const oldElement = elements[index];
389
+ oldElement.destroy();
390
+ const newElement = createElementFn(newItem as T, index);
391
+ if (newElement) {
392
+ elements[index] = newElement;
393
+ elementMap.set(index, newElement);
394
+ } else {
395
+ // Handle case where new element creation returns null
396
+ elements.splice(index, 1);
397
+ elementMap.delete(index);
398
+ }
399
+ }
400
+ }
401
+
402
+ subscriber.next({
403
+ elements: [...elements] // Create a new array to ensure change detection
313
404
  });
314
- elements = [];
315
- }
316
- const newElements = addAt(items, 0);
317
- return {
318
- elements: newElements,
319
- fullElements: elements,
320
- };
321
- } else if (type == "add" && index != undefined) {
322
- const lastElement = elements[index - 1];
323
- const newElements = addAt(items, index);
324
- return {
325
- prev: lastElement,
326
- elements: newElements,
327
- fullElements: elements,
328
- };
329
- } else if (index != undefined && type == "remove") {
330
- const currentElement = elements[index];
331
- destroyElement(currentElement);
332
- elements.splice(index, 1);
333
- return {
334
- elements: [],
335
- };
336
- }
337
- return {
338
- elements: [],
339
- fullElements: elements,
340
- };
341
- })
342
- );
405
+ })
406
+ : (itemsSubject as WritableObjectSignal<T>).observable.subscribe(change => {
407
+ const key = change.key as string | number
408
+ if (isFirstSubscription) {
409
+ isFirstSubscription = false;
410
+ elements.forEach(el => el.destroy());
411
+ elements = [];
412
+ elementMap.clear();
413
+
414
+ const items = (itemsSubject as WritableObjectSignal<T>)();
415
+ if (items) {
416
+ Object.entries(items).forEach(([key, value]) => {
417
+ const element = createElementFn(value, key);
418
+ if (element) {
419
+ elements.push(element);
420
+ elementMap.set(key, element);
421
+ }
422
+ });
423
+ }
424
+ subscriber.next({
425
+ elements: [...elements]
426
+ });
427
+ return;
428
+ }
429
+
430
+ if (change.type === 'init' || change.type === 'reset') {
431
+ elements.forEach(el => el.destroy());
432
+ elements = [];
433
+ elementMap.clear();
434
+
435
+ const items = (itemsSubject as WritableObjectSignal<T>)();
436
+ if (items) {
437
+ Object.entries(items).forEach(([key, value]) => {
438
+ const element = createElementFn(value, key);
439
+ if (element) {
440
+ elements.push(element);
441
+ elementMap.set(key, element);
442
+ }
443
+ });
444
+ }
445
+ } else if (change.type === 'add' && change.key && change.value !== undefined) {
446
+ const element = createElementFn(change.value as T, key);
447
+ if (element) {
448
+ elements.push(element);
449
+ elementMap.set(key, element);
450
+ }
451
+ } else if (change.type === 'remove' && change.key) {
452
+ const index = elements.findIndex(el => elementMap.get(key) === el);
453
+ if (index !== -1) {
454
+ const [removed] = elements.splice(index, 1);
455
+ removed.destroy();
456
+ elementMap.delete(key);
457
+ }
458
+ } else if (change.type === 'update' && change.key && change.value !== undefined) {
459
+ const index = elements.findIndex(el => elementMap.get(key) === el);
460
+ if (index !== -1) {
461
+ const oldElement = elements[index];
462
+ oldElement.destroy();
463
+ const newElement = createElementFn(change.value as T, key);
464
+ if (newElement) {
465
+ elements[index] = newElement;
466
+ elementMap.set(key, newElement);
467
+ }
468
+ }
469
+ }
470
+
471
+ subscriber.next({
472
+ elements: [...elements] // Create a new array to ensure change detection
473
+ });
474
+ });
475
+
476
+ return subscription;
477
+ });
343
478
  });
344
479
  }
345
480
 
481
+ /**
482
+ * Conditionally creates and destroys elements based on a condition signal.
483
+ *
484
+ * @param {Signal<boolean> | boolean} condition - A signal or boolean that determines whether to create an element.
485
+ * @param {Function} createElementFn - A function that returns an element or a promise that resolves to an element.
486
+ * @returns {Observable} An observable that emits the created or destroyed element.
487
+ */
346
488
  export function cond(
347
- condition: Signal,
489
+ condition: Signal<boolean> | boolean,
348
490
  createElementFn: () => Element | Promise<Element>
349
491
  ): FlowObservable {
350
492
  let element: Element | null = null;
351
- return (condition.observable as Observable<boolean>).pipe(
352
- switchMap((bool) => {
353
- if (bool) {
354
- let _el = createElementFn();
355
- if (isPromise(_el)) {
356
- return from(_el as Promise<Element>).pipe(
357
- map((el) => {
358
- element = _el as Element;
359
- return {
493
+
494
+ if (isSignal(condition)) {
495
+ const signalCondition = condition as WritableObjectSignal<boolean>;
496
+ return new Observable<{elements: Element[], type?: "init" | "remove"}>(subscriber => {
497
+ return signalCondition.observable.subscribe(bool => {
498
+ if (bool) {
499
+ let _el = createElementFn();
500
+ if (isPromise(_el)) {
501
+ from(_el as Promise<Element>).subscribe(el => {
502
+ element = el;
503
+ subscriber.next({
360
504
  type: "init",
361
505
  elements: [el],
362
- };
363
- })
364
- );
506
+ });
507
+ });
508
+ } else {
509
+ element = _el as Element;
510
+ subscriber.next({
511
+ type: "init",
512
+ elements: [element],
513
+ });
514
+ }
515
+ } else if (element) {
516
+ destroyElement(element);
517
+ subscriber.next({
518
+ elements: [],
519
+ });
520
+ } else {
521
+ subscriber.next({
522
+ elements: [],
523
+ });
365
524
  }
366
- element = _el as Element;
367
- return of({
368
- type: "init",
369
- elements: [element],
370
- });
371
- } else if (element) {
372
- destroyElement(element);
525
+ });
526
+ }).pipe(share())
527
+ } else {
528
+ // Handle boolean case
529
+ if (condition) {
530
+ let _el = createElementFn();
531
+ if (isPromise(_el)) {
532
+ return from(_el as Promise<Element>).pipe(
533
+ map((el) => ({
534
+ type: "init",
535
+ elements: [el],
536
+ }))
537
+ );
373
538
  }
374
539
  return of({
375
- elements: [],
540
+ type: "init",
541
+ elements: [_el as Element],
376
542
  });
377
- })
378
- );
543
+ }
544
+ return of({
545
+ elements: [],
546
+ });
547
+ }
379
548
  }