canvasengine 2.0.0-beta.6 → 2.0.0-beta.8

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-beta.6",
3
+ "version": "2.0.0-beta.8",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -170,7 +170,7 @@ export function DisplayObject(extendClass) {
170
170
  effect(() => {
171
171
  setter(parentSize() * (parseInt(size) / 100));
172
172
  if (this.isFlex) {
173
- this.#applyFlexLayout();
173
+ this.applyFlexLayout();
174
174
  }
175
175
  });
176
176
  } else {
@@ -188,7 +188,7 @@ export function DisplayObject(extendClass) {
188
188
  );
189
189
  }
190
190
 
191
- #applyFlexLayout() {
191
+ applyFlexLayout() {
192
192
  this.calculateLayout();
193
193
  for (let child of this.children) {
194
194
  const { left, top } = child.node.getComputedLayout();
@@ -201,7 +201,7 @@ export function DisplayObject(extendClass) {
201
201
  if (!this.parent) return;
202
202
  if (props.flexDirection || props.justifyContent) {
203
203
  this.isFlex = true;
204
- this.#applyFlexLayout();
204
+ this.applyFlexLayout();
205
205
  }
206
206
  }
207
207
 
@@ -65,8 +65,8 @@ class CanvasText extends DisplayObject(PixiText) {
65
65
  this.typewriterOptions = props.typewriter;
66
66
  }
67
67
  }
68
- if (props.text) {
69
- this.text = props.text;
68
+ if (props.text !== undefined) {
69
+ this.text = ''+props.text;
70
70
  }
71
71
  if (props.text !== undefined && props.text !== this.fullText && this.fullProps.typewriter) {
72
72
  this.text = "";
@@ -1,10 +1,11 @@
1
1
  import { effect, signal, type WritableSignal } from "@signe/reactive";
2
2
  import { animate as animatePopmotion } from "popmotion";
3
3
 
4
- interface AnimateOptions<T> {
4
+ export interface AnimateOptions<T> {
5
5
  duration?: number;
6
6
  ease?: (t: number) => number;
7
7
  onUpdate?: (value: T) => void;
8
+ onComplete?: () => void;
8
9
  }
9
10
 
10
11
  export interface AnimatedState<T> {
@@ -15,7 +16,7 @@ export interface AnimatedState<T> {
15
16
 
16
17
  export interface AnimatedSignal<T> extends Omit<WritableSignal<T>, 'set'> {
17
18
  (): T;
18
- set: (newValue: T) => void;
19
+ set: (newValue: T, options?: AnimateOptions<T>) => Promise<void>;
19
20
  animatedState: WritableSignal<AnimatedState<T>>;
20
21
  update: (updater: (value: T) => T) => void;
21
22
  }
@@ -60,7 +61,8 @@ export function animatedSignal<T>(initialValue: T, options: AnimateOptions<T> =
60
61
 
61
62
  function animatedSignal(): AnimatedState<T>;
62
63
  function animatedSignal(newValue: T): void;
63
- function animatedSignal(newValue?: T): AnimatedState<T> | void {
64
+ function animatedSignal(newValue: T, animationConfig: AnimateOptions<T>): void;
65
+ function animatedSignal(newValue?: T, animationConfig: AnimateOptions<T> = {}): AnimatedState<T> | void {
64
66
  if (newValue === undefined) {
65
67
  return privateSignal();
66
68
  }
@@ -82,6 +84,7 @@ export function animatedSignal<T>(initialValue: T, options: AnimateOptions<T> =
82
84
  // TODO
83
85
  duration: 20,
84
86
  ...options,
87
+ ...animationConfig,
85
88
  from: prevState.current,
86
89
  to: newValue,
87
90
  onUpdate: (value) => {
@@ -105,9 +108,42 @@ export function animatedSignal<T>(initialValue: T, options: AnimateOptions<T> =
105
108
  fn.update = (updater: (value: T) => any) => {
106
109
  animatedSignal(updater(privateSignal().current));
107
110
  }
108
- fn.set = (newValue: T) => {
109
- animatedSignal(newValue);
111
+ fn.set = async (newValue: T, animationConfig: AnimateOptions<T> = {}) => {
112
+ return new Promise<void>((resolve) => {
113
+ animatedSignal(newValue, {
114
+ ...animationConfig,
115
+ onComplete: resolve
116
+ });
117
+ })
110
118
  }
111
119
 
112
120
  return fn as any
121
+ }
122
+
123
+ /**
124
+ * Executes a sequence of animations. If an array is provided as an element in the sequence,
125
+ * those animations will be executed in parallel.
126
+ *
127
+ * @param sequence Array of animation functions or arrays of animation functions for parallel execution
128
+ * @returns Promise that resolves when all animations are complete
129
+ * @example
130
+ * ```ts
131
+ * await animatedSequence([
132
+ * () => value1.set(10),
133
+ * [
134
+ * () => value2.set(20),
135
+ * () => value3.set(30)
136
+ * ],
137
+ * () => value1.set(0)
138
+ * ])
139
+ * ```
140
+ */
141
+ export async function animatedSequence(sequence: ((() => Promise<void>) | (() => Promise<void>)[])[]) {
142
+ for (const item of sequence) {
143
+ if (Array.isArray(item)) {
144
+ await Promise.all(item.map(fn => fn()));
145
+ } else {
146
+ await item();
147
+ }
148
+ }
113
149
  }
@@ -23,8 +23,15 @@ export type ArrayChange<T> = {
23
23
  items: T[];
24
24
  };
25
25
 
26
+ export type ObjectChange<T> = {
27
+ type: "add" | "remove" | "update" | "init" | "reset";
28
+ key?: string;
29
+ value?: T;
30
+ items: T[];
31
+ };
32
+
26
33
  type ElementObservable<T> = Observable<
27
- ArrayChange<T> & {
34
+ (ArrayChange<T> | ObjectChange<T>) & {
28
35
  value: Element | Element[];
29
36
  }
30
37
  >;
@@ -53,10 +60,13 @@ export interface Element<T = ComponentInstance> {
53
60
  allElements: Subject<void>;
54
61
  }
55
62
 
56
- type FlowObservable = Observable<{
63
+ type FlowResult = {
57
64
  elements: Element[];
58
65
  prev?: Element;
59
- }>;
66
+ fullElements?: Element[];
67
+ };
68
+
69
+ type FlowObservable = Observable<FlowResult>;
60
70
 
61
71
  const components: { [key: string]: any } = {};
62
72
 
@@ -292,32 +302,58 @@ export function createComponent(tag: string, props?: Props): Element {
292
302
  }
293
303
 
294
304
  /**
295
- * Observes a BehaviorSubject containing an array of items and dynamically creates child elements for each item.
305
+ * Observes a BehaviorSubject containing an array or object of items and dynamically creates child elements for each item.
296
306
  *
297
- * @param {BehaviorSubject<Array>} itemsSubject - A BehaviorSubject that emits an array of items.
307
+ * @param {WritableArraySignal<T> | WritableObjectSignal<T>} itemsSubject - A signal that emits an array or object of items.
298
308
  * @param {Function} createElementFn - A function that takes an item and returns an element representation.
299
309
  * @returns {Observable} An observable that emits the list of created child elements.
300
310
  */
301
- export function loop<T = any>(
302
- itemsSubject: WritableArraySignal<T>,
303
- createElementFn: (item: any, index: number) => Element | Promise<Element>
311
+ export function loop<T>(
312
+ itemsSubject: WritableArraySignal<T[]> | WritableObjectSignal<T>,
313
+ createElementFn: (item: T, index: number | string) => Element | null
304
314
  ): FlowObservable {
305
315
  let elements: Element[] = [];
306
316
 
307
- const addAt = (items, insertIndex: number) => {
317
+ const isArraySignal = '_subject' in itemsSubject && 'items' in (itemsSubject as any)._subject;
318
+
319
+ const addAt = (items: T[], insertIndex: number | string, keys?: string[]): Element[] => {
308
320
  return items.map((item, index) => {
309
- const element = createElementFn(item, insertIndex + index);
310
- elements.splice(insertIndex + index, 0, element as Element);
321
+ const key = keys ? keys[index] : (typeof insertIndex === 'number' ? insertIndex + index : insertIndex);
322
+ const element = createElementFn(item, key);
323
+ if (typeof insertIndex === 'number') {
324
+ elements.splice(insertIndex + index, 0, element);
325
+ } else {
326
+ elements.push(element);
327
+ }
311
328
  return element;
312
329
  });
313
330
  };
314
331
 
332
+ const getInitialItems = () => {
333
+ if (isArraySignal) {
334
+ return {
335
+ items: (itemsSubject as any)._subject.items as T[],
336
+ keys: undefined as string[] | undefined
337
+ };
338
+ } else {
339
+ const entries = Object.entries((itemsSubject as any)._subject.value.value) as [string, T][];
340
+ return {
341
+ items: entries.map(([_, value]) => value),
342
+ keys: entries.map(([key]) => key)
343
+ };
344
+ }
345
+ };
346
+
315
347
  return defer(() => {
316
- let initialItems = [...itemsSubject._subject.items];
348
+ const { items, keys } = getInitialItems();
349
+ let initialItems = [...items];
350
+ let initialKeys = keys ? [...keys] : undefined;
317
351
  let init = true;
318
- return itemsSubject.observable.pipe(
319
- map((event: ArrayChange<T>) => {
320
- const { type, items, index } = event;
352
+ return (itemsSubject.observable as Observable<ArrayChange<T> | ObjectChange<T>>).pipe(
353
+ map((event: ArrayChange<T> | ObjectChange<T>): FlowResult => {
354
+ const { type, items } = event;
355
+ const index = 'index' in event ? event.index : (event as ObjectChange<T>).key;
356
+
321
357
  if (init) {
322
358
  if (elements.length > 0) {
323
359
  return {
@@ -325,8 +361,9 @@ export function loop<T = any>(
325
361
  fullElements: elements,
326
362
  };
327
363
  }
328
- const newElements = addAt(initialItems, 0);
364
+ const newElements = addAt(initialItems, 0, initialKeys);
329
365
  initialItems = [];
366
+ initialKeys = undefined;
330
367
  init = false;
331
368
  return {
332
369
  elements: newElements,
@@ -339,25 +376,64 @@ export function loop<T = any>(
339
376
  });
340
377
  elements = [];
341
378
  }
342
- const newElements = addAt(items, 0);
379
+ if (!isArraySignal) {
380
+ const entries = Object.entries((itemsSubject as any)._subject.value.value) as [string, T][];
381
+ const newElements = addAt(
382
+ entries.map(([_, value]) => value),
383
+ 0,
384
+ entries.map(([key]) => key)
385
+ );
386
+ return {
387
+ elements: newElements,
388
+ fullElements: elements,
389
+ };
390
+ }
391
+ const newElements = addAt(items as T[], 0);
343
392
  return {
344
393
  elements: newElements,
345
394
  fullElements: elements,
346
395
  };
347
396
  } else if (type == "add" && index != undefined) {
348
- const lastElement = elements[index - 1];
349
- const newElements = addAt(items, index);
397
+ const lastElement = typeof index === 'number' ? elements[index - 1] : elements[elements.length - 1];
398
+ let newElements: Element[];
399
+ if (!isArraySignal && typeof index === 'string') {
400
+ // For object updates, create a single element with the new value
401
+ const value = (event as ObjectChange<T>).value;
402
+ if (value !== undefined) {
403
+ newElements = [createElementFn(value, index)];
404
+ elements.push(newElements[0]);
405
+ } else {
406
+ newElements = [];
407
+ }
408
+ } else {
409
+ // For array updates, use addAt with the items array
410
+ newElements = addAt(items as T[], index);
411
+ }
350
412
  return {
351
413
  prev: lastElement,
352
414
  elements: newElements,
353
415
  fullElements: elements,
354
416
  };
355
- } else if (index != undefined && type == "remove") {
356
- const currentElement = elements[index];
357
- destroyElement(currentElement);
358
- elements.splice(index, 1);
417
+ } else if (type == "remove") {
418
+ if (!isArraySignal && typeof index === 'string') {
419
+ // For object property deletion
420
+ const elementIndex = elements.findIndex(el => {
421
+ return el.props.text === index || el.props.key === index;
422
+ });
423
+ if (elementIndex !== -1) {
424
+ const currentElement = elements[elementIndex];
425
+ destroyElement(currentElement);
426
+ elements.splice(elementIndex, 1);
427
+ }
428
+ } else if (typeof index === 'number') {
429
+ // For array element deletion
430
+ const currentElement = elements[index];
431
+ destroyElement(currentElement);
432
+ elements.splice(index, 1);
433
+ }
359
434
  return {
360
435
  elements: [],
436
+ fullElements: elements,
361
437
  };
362
438
  }
363
439
  return {