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