canvasengine 2.0.0-rc.1 → 2.0.0-rc.2

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.
@@ -50,7 +50,7 @@ export declare class RadialGradient {
50
50
  y: number;
51
51
  };
52
52
  }): {
53
- texture: Texture<import('pixi.js').TextureSource<any>> | null;
53
+ texture: Texture<import('pixi.js').TextureSource<any>>;
54
54
  matrix: Matrix;
55
55
  };
56
56
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasengine",
3
- "version": "2.0.0-rc.1",
3
+ "version": "2.0.0-rc.2",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -515,7 +515,11 @@ export class CanvasDOMSprite extends CanvasDOMElement {
515
515
  if (resolvedSheet?.definition !== undefined) {
516
516
  const resolvedDefinition = this.resolveSheetDefinition(resolvedSheet.definition);
517
517
  if (resolvedDefinition instanceof Promise) {
518
- void this.setSheetDefinition(resolvedDefinition);
518
+ void resolvedDefinition.then((definition) => {
519
+ if (definition) {
520
+ void this.setSheetDefinition(definition);
521
+ }
522
+ });
519
523
  } else if (resolvedDefinition && resolvedDefinition !== this.sheetDefinition) {
520
524
  void this.setSheetDefinition(resolvedDefinition);
521
525
  }
@@ -570,7 +574,7 @@ export class CanvasDOMSprite extends CanvasDOMElement {
570
574
  }
571
575
  }
572
576
 
573
- private resolveValue<T>(value: T | Signal<T> | { value?: T } | undefined): T | undefined {
577
+ private resolveValue<T = any>(value: any): T | undefined {
574
578
  if (value === undefined) return undefined;
575
579
  const resolved = isSignal(value as any) ? (value as any)() : value;
576
580
  if (resolved && typeof resolved === "object" && "value" in (resolved as any)) {
@@ -687,9 +691,9 @@ export class CanvasDOMSprite extends CanvasDOMElement {
687
691
  ].some((value) => value !== undefined);
688
692
 
689
693
  if (hasTransformProps) {
690
- let x = this.resolveValue(props.x) ?? 0;
691
- let y = this.resolveValue(props.y) ?? 0;
692
- const roundPixels = this.resolveValue(props.roundPixels);
694
+ let x = this.resolveValue<number>(props.x) ?? 0;
695
+ let y = this.resolveValue<number>(props.y) ?? 0;
696
+ const roundPixels = this.resolveValue<boolean>(props.roundPixels);
693
697
  if (roundPixels) {
694
698
  x = Math.round(x);
695
699
  y = Math.round(y);
@@ -698,8 +702,8 @@ export class CanvasDOMSprite extends CanvasDOMElement {
698
702
  const scale = this.resolvePoint(props.scale) ?? { x: 1, y: 1 };
699
703
  const skew = this.resolvePoint(props.skew);
700
704
 
701
- const angle = this.resolveValue(props.angle);
702
- const rotation = this.resolveValue(props.rotation);
705
+ const angle = this.resolveValue<number>(props.angle);
706
+ const rotation = this.resolveValue<number>(props.rotation);
703
707
  const rotationDeg = angle !== undefined
704
708
  ? angle
705
709
  : rotation !== undefined
@@ -100,7 +100,7 @@ export const EVENTS = [
100
100
 
101
101
  export type OnHook = (() => void) | (() => Promise<void> | void);
102
102
 
103
- export function DisplayObject(extendClass) {
103
+ export function DisplayObject(extendClass): any {
104
104
  return class DisplayObject extends extendClass {
105
105
  #canvasContext: {
106
106
  [key: string]: any;
@@ -55,10 +55,15 @@ type FlowResult = {
55
55
  elements: Element[];
56
56
  prev?: Element;
57
57
  fullElements?: Element[];
58
+ reorder?: boolean;
58
59
  };
59
60
 
60
61
  type FlowObservable = Observable<FlowResult>;
61
62
 
63
+ export interface LoopOptions<T> {
64
+ track?: (item: T, index: number | string) => string | number;
65
+ }
66
+
62
67
  const components: { [key: string]: any } = {};
63
68
 
64
69
  export const isElement = (value: any): value is Element => {
@@ -698,7 +703,7 @@ export function createComponent(tag: string, props?: Props): Element {
698
703
  if (child instanceof Observable) {
699
704
  const mountedFlowElements = childGroup.mounted;
700
705
  const flowEffectSubscriptions = ((child as any).effectSubscriptions ?? []) as Subscription[];
701
- const flowEffectMounts = ((child as any).effectMounts ?? []) as Array<(element: Element) => any>;
706
+ const flowEffectMounts = ((child as any).effectMounts ?? []) as Array<(element?: Element) => any>;
702
707
 
703
708
  const applyFlowEffects = (element: Element) => {
704
709
  if (!flowEffectMounts.length) {
@@ -730,9 +735,24 @@ export function createComponent(tag: string, props?: Props): Element {
730
735
  const mountFlowElement = (
731
736
  element: Element,
732
737
  sourceIndex: number,
733
- orderedSources: any[]
738
+ orderedSources: any[],
739
+ shouldReorder = false
734
740
  ) => {
735
- if (mountedFlowElements.has(element)) {
741
+ const mounted = mountedFlowElements.get(element);
742
+ if (mounted) {
743
+ if (shouldReorder) {
744
+ const insertIndex = getInsertIndex(sourceIndex, orderedSources);
745
+ const parentInstance = mounted.parent?.componentInstance as any;
746
+ const childInstance = mounted.componentInstance as any;
747
+ if (
748
+ insertIndex !== undefined &&
749
+ parentInstance &&
750
+ typeof parentInstance.addChildAt === "function" &&
751
+ parentInstance.children?.includes(childInstance)
752
+ ) {
753
+ parentInstance.addChildAt(childInstance, insertIndex);
754
+ }
755
+ }
736
756
  return;
737
757
  }
738
758
 
@@ -759,7 +779,8 @@ export function createComponent(tag: string, props?: Props): Element {
759
779
  component: any,
760
780
  nextElements: Set<any>,
761
781
  index: number,
762
- orderedSources: any[]
782
+ orderedSources: any[],
783
+ shouldReorder = false
763
784
  ) => {
764
785
  if (component instanceof Observable) {
765
786
  nextElements.add(component);
@@ -772,7 +793,7 @@ export function createComponent(tag: string, props?: Props): Element {
772
793
  }
773
794
  if (Array.isArray(component)) {
774
795
  component.forEach((comp) =>
775
- processFlowComponent(comp, nextElements, index, orderedSources)
796
+ processFlowComponent(comp, nextElements, index, orderedSources, shouldReorder)
776
797
  );
777
798
  return;
778
799
  }
@@ -781,7 +802,7 @@ export function createComponent(tag: string, props?: Props): Element {
781
802
  }
782
803
 
783
804
  nextElements.add(component);
784
- mountFlowElement(component, index, orderedSources);
805
+ mountFlowElement(component, index, orderedSources, shouldReorder);
785
806
  };
786
807
 
787
808
  // Subscribe to the observable and handle the emitted values
@@ -793,9 +814,11 @@ export function createComponent(tag: string, props?: Props): Element {
793
814
  const {
794
815
  elements: comp,
795
816
  prev,
817
+ reorder,
796
818
  }: {
797
819
  elements: Element[];
798
820
  prev?: Element;
821
+ reorder?: boolean;
799
822
  } = value;
800
823
 
801
824
  const components = comp.filter((c) => c !== null);
@@ -809,7 +832,7 @@ export function createComponent(tag: string, props?: Props): Element {
809
832
  return;
810
833
  }
811
834
  components.forEach((component, index) => {
812
- processFlowComponent(component, nextElements, index, components);
835
+ processFlowComponent(component, nextElements, index, components, reorder);
813
836
  });
814
837
  syncFlowElements(nextElements);
815
838
  } else if (isElement(value)) {
@@ -862,7 +885,8 @@ export function createComponent(tag: string, props?: Props): Element {
862
885
  */
863
886
  export function loop<T>(
864
887
  itemsSubject: any,
865
- createElementFn: (item: T, index: number | string) => Element | null
888
+ createElementFn: (item: T, index: number | string) => Element | null,
889
+ options: LoopOptions<T> = {}
866
890
  ): FlowObservable {
867
891
 
868
892
  if (isComputed(itemsSubject) && itemsSubject.dependencies.size == 0) {
@@ -876,6 +900,8 @@ export function loop<T>(
876
900
  let elements: Element[] = [];
877
901
  let elementMap = new Map<string | number, Element>();
878
902
  let isFirstSubscription = true;
903
+ const getTrackKey = (item: T, index: number | string) =>
904
+ options.track ? options.track(item, index) : index;
879
905
 
880
906
  const ensureElement = (itemResult: any): Element | null => {
881
907
  if (!itemResult) return null;
@@ -900,27 +926,111 @@ export function loop<T>(
900
926
  const isArraySignal = (signal: any): signal is WritableArraySignal<T[]> =>
901
927
  Array.isArray(signal());
902
928
 
929
+ const cleanupUntrackedElement = (element: Element | null) => {
930
+ if (!element) return;
931
+ element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
932
+ element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
933
+ element.effectUnmounts?.forEach((fn) => fn?.());
934
+ };
935
+
936
+ const patchTrackedElement = (target: Element, source: Element) => {
937
+ const nextProps = { ...source.props };
938
+ const nextPropObservables = source.propObservables;
939
+
940
+ if (target.props.context) {
941
+ nextProps.context = target.props.context;
942
+ }
943
+ if (target.props.children && !source.props.children) {
944
+ nextProps.children = target.props.children;
945
+ }
946
+
947
+ target.props = nextProps;
948
+ target.propObservables = nextPropObservables;
949
+ target.componentInstance.onUpdate?.(nextProps);
950
+ Object.entries(target.directives).forEach(([name, directive]) => {
951
+ if (name in nextProps) {
952
+ directive.onUpdate?.(nextProps[name], target);
953
+ }
954
+ });
955
+
956
+ cleanupUntrackedElement(source);
957
+ };
958
+
959
+ const removeElementFromMap = (element: Element) => {
960
+ for (const [key, mappedElement] of elementMap.entries()) {
961
+ if (mappedElement === element) {
962
+ elementMap.delete(key);
963
+ return;
964
+ }
965
+ }
966
+ };
967
+
968
+ const rebuildArrayElements = (items: T[] | undefined | null) => {
969
+ if (!options.track) {
970
+ elements.forEach(el => destroyElement(el));
971
+ elements = [];
972
+ elementMap.clear();
973
+
974
+ if (items) {
975
+ items.forEach((item, index) => {
976
+ const element = ensureElement(createElementFn(item, index));
977
+ if (element) {
978
+ elements.push(element);
979
+ elementMap.set(index, element);
980
+ }
981
+ });
982
+ }
983
+ return;
984
+ }
985
+
986
+ const previousMap = elementMap;
987
+ const nextElements: Element[] = [];
988
+ const nextMap = new Map<string | number, Element>();
989
+ const usedElements = new Set<Element>();
990
+
991
+ if (items) {
992
+ items.forEach((item, index) => {
993
+ const key = getTrackKey(item, index);
994
+ const existing = previousMap.get(key);
995
+ const nextElement = ensureElement(createElementFn(item, index));
996
+
997
+ if (existing) {
998
+ if (nextElement) {
999
+ patchTrackedElement(existing, nextElement);
1000
+ }
1001
+ nextElements.push(existing);
1002
+ nextMap.set(key, existing);
1003
+ usedElements.add(existing);
1004
+ return;
1005
+ }
1006
+
1007
+ if (nextElement) {
1008
+ nextElements.push(nextElement);
1009
+ nextMap.set(key, nextElement);
1010
+ usedElements.add(nextElement);
1011
+ }
1012
+ });
1013
+ }
1014
+
1015
+ elements.forEach((element) => {
1016
+ if (!usedElements.has(element)) {
1017
+ destroyElement(element);
1018
+ }
1019
+ });
1020
+
1021
+ elements = nextElements;
1022
+ elementMap = nextMap;
1023
+ };
1024
+
903
1025
  return new Observable<FlowResult>(subscriber => {
904
1026
  const subscription = isArraySignal(itemsSubject)
905
1027
  ? itemsSubject.observable.subscribe(change => {
906
1028
  if (isFirstSubscription) {
907
1029
  isFirstSubscription = false;
908
- elements.forEach(el => el.destroy());
909
- elements = [];
910
- elementMap.clear();
911
-
912
- const items = itemsSubject();
913
- if (items) {
914
- items.forEach((item, index) => {
915
- const element = ensureElement(createElementFn(item, index));
916
- if (element) {
917
- elements.push(element);
918
- elementMap.set(index, element);
919
- }
920
- });
921
- }
1030
+ rebuildArrayElements(itemsSubject());
922
1031
  subscriber.next({
923
- elements: [...elements]
1032
+ elements: [...elements],
1033
+ reorder: Boolean(options.track)
924
1034
  });
925
1035
  return;
926
1036
  }
@@ -930,25 +1040,13 @@ export function loop<T>(
930
1040
  const isDirectArrayChange = Array.isArray(change) || (change && typeof change === 'object' && !('type' in change));
931
1041
 
932
1042
  if (change.type === 'init' || change.type === 'reset' || isDirectArrayChange) {
933
- elements.forEach(el => destroyElement(el));
934
- elements = [];
935
- elementMap.clear();
936
-
937
- const items = itemsSubject();
938
- if (items) {
939
- items.forEach((item, index) => {
940
- const element = ensureElement(createElementFn(item, index));
941
- if (element) {
942
- elements.push(element);
943
- elementMap.set(index, element);
944
- }
945
- });
946
- }
1043
+ rebuildArrayElements(itemsSubject());
947
1044
  } else if (change.type === 'add' && change.index !== undefined) {
948
1045
  const newElements = change.items.map((item, i) => {
949
- const element = ensureElement(createElementFn(item as T, change.index! + i));
1046
+ const index = change.index! + i;
1047
+ const element = ensureElement(createElementFn(item as T, index));
950
1048
  if (element) {
951
- elementMap.set(change.index! + i, element);
1049
+ elementMap.set(getTrackKey(item as T, index), element);
952
1050
  }
953
1051
  return element;
954
1052
  }).filter((el): el is Element => el !== null);
@@ -958,19 +1056,20 @@ export function loop<T>(
958
1056
  const removed = elements.splice(change.index, 1);
959
1057
  removed.forEach(el => {
960
1058
  destroyElement(el)
961
- elementMap.delete(change.index!);
1059
+ removeElementFromMap(el);
962
1060
  });
963
1061
  } else if (change.type === 'update' && change.index !== undefined && change.items.length === 1) {
964
1062
  const index = change.index;
965
1063
  const newItem = change.items[0];
1064
+ const key = getTrackKey(newItem as T, index);
966
1065
 
967
1066
  // Check if the previous item at this index was effectively undefined or non-existent
968
- if (index >= elements.length || elements[index] === undefined || !elementMap.has(index)) {
1067
+ if (index >= elements.length || elements[index] === undefined || !elementMap.has(key)) {
969
1068
  // Treat as add operation
970
1069
  const newElement = ensureElement(createElementFn(newItem as T, index));
971
1070
  if (newElement) {
972
1071
  elements.splice(index, 0, newElement); // Insert at the correct index
973
- elementMap.set(index, newElement);
1072
+ elementMap.set(key, newElement);
974
1073
  // Adjust indices in elementMap for subsequent elements might be needed if map relied on exact indices
975
1074
  // This simple implementation assumes keys are stable or createElementFn handles context correctly
976
1075
  } else {
@@ -978,22 +1077,28 @@ export function loop<T>(
978
1077
  }
979
1078
  } else {
980
1079
  // Treat as a standard update operation
981
- const oldElement = elements[index];
982
- destroyElement(oldElement)
1080
+ const oldElement = elementMap.get(key) ?? elements[index];
983
1081
  const newElement = ensureElement(createElementFn(newItem as T, index));
984
- if (newElement) {
1082
+ if (options.track && oldElement && newElement) {
1083
+ patchTrackedElement(oldElement, newElement);
1084
+ elements[index] = oldElement;
1085
+ elementMap.set(key, oldElement);
1086
+ } else if (newElement) {
1087
+ destroyElement(oldElement)
985
1088
  elements[index] = newElement;
986
- elementMap.set(index, newElement);
1089
+ elementMap.set(key, newElement);
987
1090
  } else {
988
1091
  // Handle case where new element creation returns null
1092
+ destroyElement(oldElement)
989
1093
  elements.splice(index, 1);
990
- elementMap.delete(index);
1094
+ elementMap.delete(key);
991
1095
  }
992
1096
  }
993
1097
  }
994
1098
 
995
1099
  subscriber.next({
996
- elements: [...elements] // Create a new array to ensure change detection
1100
+ elements: [...elements], // Create a new array to ensure change detection
1101
+ reorder: Boolean(options.track)
997
1102
  });
998
1103
  })
999
1104
  : (itemsSubject as WritableObjectSignal<T>).observable.subscribe(change => {
@@ -1007,7 +1112,7 @@ export function loop<T>(
1007
1112
  const items = (itemsSubject as WritableObjectSignal<T>)();
1008
1113
  if (items) {
1009
1114
  Object.entries(items).forEach(([key, value]) => {
1010
- const element = ensureElement(createElementFn(value, key));
1115
+ const element = ensureElement(createElementFn(value as T, key));
1011
1116
  if (element) {
1012
1117
  elements.push(element);
1013
1118
  elementMap.set(key, element);
@@ -1028,7 +1133,7 @@ export function loop<T>(
1028
1133
  const items = (itemsSubject as WritableObjectSignal<T>)();
1029
1134
  if (items) {
1030
1135
  Object.entries(items).forEach(([key, value]) => {
1031
- const element = ensureElement(createElementFn(value, key));
1136
+ const element = ensureElement(createElementFn(value as T, key));
1032
1137
  if (element) {
1033
1138
  elements.push(element);
1034
1139
  elementMap.set(key, element);
@@ -46,6 +46,16 @@ type PropSchema = {
46
46
  [key: string]: PropType | PropType[] | PropConfig;
47
47
  }
48
48
 
49
+ const toPropSignal = (value: any) => isSignal(value) ? value : signal(value)
50
+
51
+ const definePropSignals = (props: any): any => {
52
+ const obj: any = {}
53
+ for (let key in props) {
54
+ obj[key] = toPropSignal(props[key])
55
+ }
56
+ return obj
57
+ }
58
+
49
59
  /**
50
60
  * Validates and defines properties based on a schema.
51
61
  *
@@ -109,13 +119,11 @@ export const useDefineProps = (props: any) => {
109
119
  }
110
120
  }
111
121
 
112
- validatedProps[key] = isSignal(validatedValue)
113
- ? validatedValue
114
- : signal(validatedValue)
122
+ validatedProps[key] = toPropSignal(validatedValue)
115
123
  }
116
124
 
117
125
  return {
118
- ...useProps(rawProps),
126
+ ...definePropSignals(rawProps),
119
127
  ...validatedProps
120
128
  }
121
129
  }
@@ -152,4 +160,4 @@ function validateType(key: string, value: any, types: any[]) {
152
160
  `Expected ${types.map(t => t.name).join(' or ')}`
153
161
  )
154
162
  }
155
- }
163
+ }
package/tsconfig.json CHANGED
@@ -6,7 +6,11 @@
6
6
  "emitDeclarationOnly": false,
7
7
  "declarationMap": true,
8
8
  "rootDir": "src",
9
- "module": "ESNext"
9
+ "module": "ESNext",
10
+ "strict": false,
11
+ "noImplicitAny": false,
12
+ "strictNullChecks": false,
13
+ "strictPropertyInitialization": false
10
14
  },
11
15
  "include": [
12
16
  "src/**/*"