canvasengine 2.0.0-beta.3 → 2.0.0-beta.5

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/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ declare module '*.ce' {
2
+ const content: import("./dist/index").ComponentFunction;
3
+ export default content;
4
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasengine",
3
- "version": "2.0.0-beta.3",
3
+ "version": "2.0.0-beta.5",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -37,7 +37,7 @@ interface SvgProps extends DisplayObjectProps {
37
37
  }
38
38
 
39
39
  class CanvasGraphics extends DisplayObject(PixiGraphics) {
40
- onInit(props: GraphicsProps) {
40
+ onInit(props) {
41
41
  super.onInit(props);
42
42
  if (props.draw) {
43
43
  effect(() => {
@@ -227,6 +227,24 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
227
227
  async onUpdate(props) {
228
228
  super.onUpdate(props);
229
229
 
230
+ const setTexture = async (image: string) => {
231
+ const onProgress = this.fullProps.loader?.onProgress;
232
+ const texture = await Assets.load(image, (progress) => {
233
+ if (onProgress) onProgress(progress);
234
+ if (progress == 1) {
235
+ const onComplete = this.fullProps.loader?.onComplete;
236
+ if (onComplete) {
237
+ // hack to memoize the texture
238
+ setTimeout(() => {
239
+ onComplete(texture);
240
+ });
241
+ }
242
+ }
243
+ });
244
+
245
+ return texture
246
+ }
247
+
230
248
  const sheet = props.sheet;
231
249
  if (sheet?.params) this.sheetParams = sheet?.params;
232
250
 
@@ -239,14 +257,13 @@ export class CanvasSprite extends DisplayObject(PixiSprite) {
239
257
 
240
258
  if (props.scaleMode) this.baseTexture.scaleMode = props.scaleMode;
241
259
  else if (props.image && this.fullProps.rectangle === undefined) {
242
- this.texture = await Assets.load(this.fullProps.image);
260
+ this.texture = await setTexture(this.fullProps.image);
243
261
  } else if (props.texture) {
244
262
  this.texture = props.texture;
245
263
  }
246
-
247
264
  if (props.rectangle !== undefined) {
248
265
  const { x, y, width, height } = props.rectangle?.value ?? props.rectangle;
249
- const texture = await Assets.load(this.fullProps.image);
266
+ const texture = await setTexture(this.fullProps.image);
250
267
  this.texture = new Texture({
251
268
  source: texture.source,
252
269
  frame: new Rectangle(x, y, width, height),
@@ -483,6 +500,10 @@ export interface SpritePropsWithSheet
483
500
  params?: any;
484
501
  onFinish?: () => void;
485
502
  };
503
+ loader?: {
504
+ onProgress?: (progress: number) => void;
505
+ onComplete?: (texture: Texture) => void;
506
+ };
486
507
  }
487
508
 
488
509
  export type SpritePropTypes = SpritePropsWithImage | SpritePropsWithSheet;
@@ -0,0 +1,110 @@
1
+ import { Texture } from "pixi.js";
2
+ import { h, mount } from "../engine/signal";
3
+ import { useDefineProps } from "../hooks/useProps";
4
+ import { Sprite } from "./Sprite";
5
+ import { effect, Signal, signal } from "@signe/reactive";
6
+
7
+ interface VideoProps {
8
+ source: string;
9
+ paused?: boolean;
10
+ loop?: boolean;
11
+ muted?: boolean;
12
+ loader?: {
13
+ onComplete?: (texture: Texture) => void;
14
+ onProgress?: (progress: number) => void;
15
+ };
16
+ }
17
+
18
+ export function Video(props: VideoProps) {
19
+ const eventsMap = {
20
+ audioprocess: null,
21
+ canplay: null,
22
+ canplaythrough: null,
23
+ complete: null,
24
+ durationchange: null,
25
+ emptied: null,
26
+ ended: null,
27
+ loadeddata: null,
28
+ loadedmetadata: null,
29
+ pause: null,
30
+ play: null,
31
+ playing: null,
32
+ progress: null,
33
+ ratechange: null,
34
+ seeked: null,
35
+ seeking: null,
36
+ stalled: null,
37
+ suspend: null,
38
+ timeupdate: null,
39
+ volumechange: null,
40
+ waiting: null
41
+ }
42
+
43
+ const video: Signal<HTMLVideoElement | null> = signal(null)
44
+ const defineProps = useDefineProps(props)
45
+ const { play, loop, muted } = defineProps({
46
+ play: {
47
+ type: Boolean,
48
+ default: true
49
+ },
50
+ loop: {
51
+ type: Boolean,
52
+ default: false
53
+ },
54
+ muted: {
55
+ type: Boolean,
56
+ default: false
57
+ }
58
+ })
59
+
60
+ effect(() => {
61
+ const _video = video()
62
+ const state = play()
63
+ if (_video && state !== undefined) {
64
+ if (state) {
65
+ _video.play()
66
+ } else {
67
+ _video.pause()
68
+ }
69
+ }
70
+ if (_video && loop()) {
71
+ _video.loop = loop()
72
+ }
73
+ if (_video && muted()) {
74
+ _video.muted = muted()
75
+ }
76
+ })
77
+
78
+ mount(() => {
79
+ return () => {
80
+ for (let event in eventsMap) {
81
+ if (eventsMap[event]) {
82
+ video().removeEventListener(event, eventsMap[event])
83
+ }
84
+ }
85
+ }
86
+ })
87
+
88
+ return h(Sprite, {
89
+ ...props,
90
+ image: props.source,
91
+ loader: {
92
+ onComplete: (texture) => {
93
+ const source = texture.source.resource
94
+ video.set(source)
95
+ if (props?.loader?.onComplete) {
96
+ props.loader.onComplete(texture)
97
+ }
98
+ for (let event in eventsMap) {
99
+ if (props[event]) {
100
+ const cb = (ev) => {
101
+ props[event](ev)
102
+ }
103
+ eventsMap[event] = cb
104
+ source.addEventListener(event, cb)
105
+ }
106
+ }
107
+ }
108
+ }
109
+ })
110
+ }
@@ -4,6 +4,7 @@ export { Graphics, Rect, Circle, Ellipse, Triangle, Svg as svg } from './Graphic
4
4
  export { Scene } from './Scene'
5
5
  export { ParticlesEmitter } from './ParticleEmitter'
6
6
  export { Sprite } from './Sprite'
7
+ export { Video } from './Video'
7
8
  export { Text } from './Text'
8
9
  export { TilingSprite } from './TilingSprite'
9
10
  export { Viewport } from './Viewport'
@@ -182,9 +182,24 @@ export function createComponent(tag: string, props?: Props): Element {
182
182
  }
183
183
 
184
184
  instance.onInit?.(element.props);
185
- instance.onUpdate?.(element.props);
186
185
 
187
- const onMount = (parent: Element, element: Element, index?: number) => {
186
+ const elementsListen = new Subject<any>()
187
+
188
+ if (props?.isRoot) {
189
+ element.allElements = elementsListen
190
+ element.props.context.rootElement = element;
191
+ element.componentInstance.onMount?.(element);
192
+ propagateContext(element);
193
+ }
194
+
195
+ if (props) {
196
+ for (let key in props) {
197
+ const directive = applyDirective(element, key);
198
+ if (directive) element.directives[key] = directive;
199
+ }
200
+ }
201
+
202
+ function onMount(parent: Element, element: Element, index?: number) {
188
203
  element.props.context = parent.props.context;
189
204
  element.parent = parent;
190
205
  element.componentInstance.onMount?.(element, index);
@@ -196,68 +211,79 @@ export function createComponent(tag: string, props?: Props): Element {
196
211
  });
197
212
  };
198
213
 
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;
214
+ async function propagateContext(element) {
215
+ if (element.props.attach) {
216
+ const isReactiveAttach = isSignal(element.propObservables?.attach)
217
+ if (!isReactiveAttach) {
218
+ element.props.children.push(element.props.attach)
206
219
  }
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
- });
220
+ else {
221
+ await new Promise((resolve) => {
222
+ let lastElement = null
223
+ element.propSubscriptions.push(element.propObservables.attach.observable.subscribe(async (args) => {
224
+ const value = args?.value ?? args
225
+ if (!value) {
226
+ throw new Error(`attach in ${element.tag} is undefined or null, add a component`)
240
227
  }
228
+ if (lastElement) {
229
+ destroyElement(lastElement)
230
+ }
231
+ lastElement = value
232
+ await createElement(element, value)
233
+ resolve(undefined)
234
+ }))
235
+ })
236
+ }
237
+ }
238
+ if (!element.props.children) {
239
+ return;
240
+ }
241
+ for (let child of element.props.children) {
242
+ if (!child) continue;
243
+ await createElement(element, child)
244
+ }
245
+ };
246
+
247
+ async function createElement(parent: Element, child: Element) {
248
+ if (isPromise(child)) {
249
+ child = await child;
250
+ }
251
+ if (child instanceof Observable) {
252
+ child.subscribe(
253
+ ({
254
+ elements: comp,
255
+ prev,
256
+ }: {
257
+ elements: Element[];
258
+ prev?: Element;
259
+ }) => {
260
+ // if prev, insert element after this
261
+ const components = comp.filter((c) => c !== null);
262
+ if (prev) {
263
+ components.forEach((c) => {
264
+ const index = parent.props.children.indexOf(prev.props.key);
265
+ onMount(parent, c, index + 1);
266
+ propagateContext(c);
267
+ });
268
+ return;
269
+ }
270
+ components.forEach((component) => {
271
+ if (!Array.isArray(component)) {
272
+ onMount(parent, component);
273
+ propagateContext(component);
274
+ } else {
275
+ component.forEach((comp) => {
276
+ onMount(parent, comp);
277
+ propagateContext(comp);
241
278
  });
242
- elementsListen.next(undefined)
243
279
  }
244
- );
245
- } else {
246
- onMount(element, child);
247
- await propagateContext(child);
280
+ });
281
+ elementsListen.next(undefined)
248
282
  }
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;
283
+ );
284
+ } else {
285
+ onMount(parent, child);
286
+ await propagateContext(child);
261
287
  }
262
288
  }
263
289
 
@@ -2,39 +2,95 @@ import { effect, signal } from "@signe/reactive";
2
2
 
3
3
  interface Listen<T = any> {
4
4
  config: T | undefined;
5
- seed: number;
5
+ seed: {
6
+ config: T | undefined;
7
+ value: number;
8
+ resolve: (value: any) => void;
9
+ };
6
10
  }
7
11
 
8
12
  interface Trigger<T = any> {
9
- start: () => void;
13
+ start: () => Promise<void>;
10
14
  listen: () => Listen<T> | undefined;
11
15
  }
12
16
 
17
+ /**
18
+ * Checks if the given argument is a Trigger object
19
+ * @param arg - The value to check
20
+ * @returns True if the argument is a Trigger object
21
+ */
13
22
  export function isTrigger(arg: any): arg is Trigger<any> {
14
23
  return arg?.start && arg?.listen;
15
24
  }
16
25
 
17
- export function trigger<T = any>(config?: T): Trigger<T> {
18
- const _signal = signal(0);
26
+ /**
27
+ * Creates a new trigger that can be used to pass data between components
28
+ * @param globalConfig - Optional configuration data to be passed when the trigger is activated
29
+ * @returns A Trigger object with start and listen methods
30
+ * @example
31
+ * ```ts
32
+ * const myTrigger = trigger()
33
+ *
34
+ * on(myTrigger, (data) => {
35
+ * console.log('Triggered with data:', data)
36
+ * })
37
+ *
38
+ * myTrigger.start({ message: 'Hello' })
39
+ * ```
40
+ */
41
+ export function trigger<T = any>(globalConfig?: T): Trigger<T> {
42
+ const _signal = signal({
43
+ config: globalConfig,
44
+ value: 0,
45
+ resolve: (value: any) => void 0,
46
+ });
19
47
  return {
20
- start: () => {
21
- _signal.set(Math.random());
48
+ start: (config?: T) => {
49
+ return new Promise((resolve: (value: any) => void) => {
50
+ _signal.set({
51
+ config: {
52
+ ...globalConfig,
53
+ ...config,
54
+ },
55
+ resolve,
56
+ value: Math.random(),
57
+ });
58
+ });
22
59
  },
23
60
  listen: (): Listen<T> | undefined => {
24
61
  return {
25
- config,
62
+ config: globalConfig,
26
63
  seed: _signal(),
27
64
  };
28
65
  },
29
66
  };
30
67
  }
31
68
 
32
- export function on(triggerSignal: any, callback: (config: any) => void) {
69
+ /**
70
+ * Subscribes to a trigger and executes a callback when the trigger is activated
71
+ * @param triggerSignal - The trigger to subscribe to
72
+ * @param callback - Function to execute when the trigger is activated
73
+ * @throws Error if triggerSignal is not a valid trigger
74
+ * @example
75
+ * ```ts
76
+ * const click = trigger()
77
+ *
78
+ * on(click, () => {
79
+ * console.log('Click triggered')
80
+ * })
81
+ * ```
82
+ */
83
+ export function on(triggerSignal: any, callback: (config: any) => void | Promise<void>) {
33
84
  if (!isTrigger(triggerSignal)) {
34
85
  throw new Error("In 'on(arg)' must have a trigger signal type");
35
86
  }
36
87
  effect(() => {
37
88
  const result = triggerSignal.listen();
38
- if (result?.seed) callback(result.config);
89
+ if (result?.seed.value) {
90
+ const ret = callback(result?.seed.config);
91
+ if (ret && typeof ret.then === 'function') {
92
+ ret.then(result?.seed.resolve);
93
+ }
94
+ }
39
95
  });
40
96
  }
@@ -1,17 +1,35 @@
1
1
  import { ObservablePoint } from "pixi.js"
2
2
 
3
+ /**
4
+ * Checks if code is running in a browser environment
5
+ * @returns {boolean} True if running in browser, false otherwise
6
+ */
3
7
  export function isBrowser(): boolean {
4
8
  return typeof window !== 'undefined'
5
9
  }
6
10
 
11
+ /**
12
+ * Returns current high-resolution timestamp
13
+ * @returns {number} Current time in milliseconds
14
+ */
7
15
  export function preciseNow(): number {
8
16
  return typeof performance !== 'undefined' ? performance.now() : Date.now()
9
17
  }
10
18
 
19
+ /**
20
+ * Converts frames per second to milliseconds
21
+ * @param {number} fps - Frames per second
22
+ * @returns {number} Milliseconds per frame
23
+ */
11
24
  export function fps2ms(fps: number): number {
12
25
  return 1000 / fps
13
26
  }
14
27
 
28
+ /**
29
+ * Checks if a value is a Promise
30
+ * @param {any} value - Value to check
31
+ * @returns {boolean} True if value is a Promise, false otherwise
32
+ */
15
33
  export function isPromise(value: any): boolean {
16
34
  return value && value instanceof Promise
17
35
  }
@@ -58,15 +76,38 @@ function deepEquals(a: any, b: any): boolean {
58
76
  return false;
59
77
  }
60
78
 
79
+ /**
80
+ * Checks if a value is a function
81
+ * @param {unknown} val - Value to check
82
+ * @returns {boolean} True if value is a function, false otherwise
83
+ */
61
84
  export function isFunction(val: unknown): boolean {
62
85
  return {}.toString.call(val) === '[object Function]'
63
86
  }
64
87
 
88
+ /**
89
+ * Checks if a value is a plain object
90
+ * @param {unknown} val - Value to check
91
+ * @returns {boolean} True if value is an object (not null and not array), false otherwise
92
+ */
65
93
  export function isObject(val: unknown): boolean {
66
94
  return typeof val == 'object' && val != null && !Array.isArray(val)
67
95
  }
68
96
 
69
- export function set(obj, path, value, onlyPlainObject = false) {
97
+ /**
98
+ * Sets a value in an object using a dot notation path
99
+ * @param {Record<string, any>} obj - Target object
100
+ * @param {string | string[]} path - Path to set value at (e.g., 'a.b.c' or ['a', 'b', 'c'])
101
+ * @param {any} value - Value to set
102
+ * @param {boolean} onlyPlainObject - If true, only creates plain objects in path
103
+ * @returns {Record<string, any>} Modified object
104
+ */
105
+ export function set(
106
+ obj: Record<string, any>,
107
+ path: string | string[],
108
+ value: any,
109
+ onlyPlainObject = false
110
+ ): Record<string, any> {
70
111
  if (Object(obj) !== obj) return obj;
71
112
 
72
113
  if (typeof path === 'string') {
@@ -79,8 +120,8 @@ export function set(obj, path, value, onlyPlainObject = false) {
79
120
  let current = obj;
80
121
  for (let i = 0; i < len - 1; i++) {
81
122
  let segment = path[i];
82
- let nextSegment = path[i + 1];
83
- let isNextNumeric = !isNaN(nextSegment) && isFinite(nextSegment);
123
+ let nextSegment: number | string = path[i + 1];
124
+ let isNextNumeric = !isNaN(Number(nextSegment)) && isFinite(Number(nextSegment));
84
125
 
85
126
  if (!current[segment] || typeof current[segment] !== 'object') {
86
127
  current[segment] = (isNextNumeric && !onlyPlainObject) ? [] : {};
@@ -94,7 +135,13 @@ export function set(obj, path, value, onlyPlainObject = false) {
94
135
  return obj;
95
136
  }
96
137
 
97
- export function get(obj, path) {
138
+ /**
139
+ * Gets a value from an object using a dot notation path
140
+ * @param {Record<string, any>} obj - Source object
141
+ * @param {string} path - Path to get value from (e.g., 'a.b.c')
142
+ * @returns {any} Value at path or undefined if not found
143
+ */
144
+ export function get(obj: Record<string, any>, path: string): any {
98
145
  const keys = path.split('.');
99
146
  let current = obj;
100
147
 
@@ -108,15 +155,31 @@ export function get(obj, path) {
108
155
  return current;
109
156
  }
110
157
 
111
- export function log(text) {
158
+ /**
159
+ * Logs a message to console
160
+ * @param {any} text - Message to log
161
+ */
162
+ export function log(text: any): void {
112
163
  console.log(text)
113
164
  }
114
165
 
115
- export function error(text) {
166
+ /**
167
+ * Logs an error message to console
168
+ * @param {any} text - Error message to log
169
+ */
170
+ export function error(text: any): void {
116
171
  console.error(text)
117
172
  }
118
173
 
119
- export function setObservablePoint(observablePoint: ObservablePoint, point: { x: number, y: number } | number | [number, number]): void {
174
+ /**
175
+ * Sets the position of an ObservablePoint using various input formats
176
+ * @param {ObservablePoint} observablePoint - The point to modify
177
+ * @param {Object | number | [number, number]} point - New position value
178
+ */
179
+ export function setObservablePoint(
180
+ observablePoint: ObservablePoint,
181
+ point: { x: number, y: number } | number | [number, number]
182
+ ): void {
120
183
  if (typeof point === 'number') {
121
184
  observablePoint.set(point);
122
185
  }
@@ -128,7 +191,20 @@ export function setObservablePoint(observablePoint: ObservablePoint, point: { x:
128
191
  }
129
192
  }
130
193
 
131
- export function calculateDistance(x1, y1, x2, y2) {
194
+ /**
195
+ * Calculates the Euclidean distance between two points
196
+ * @param {number} x1 - X coordinate of first point
197
+ * @param {number} y1 - Y coordinate of first point
198
+ * @param {number} x2 - X coordinate of second point
199
+ * @param {number} y2 - Y coordinate of second point
200
+ * @returns {number} Distance between the points
201
+ */
202
+ export function calculateDistance(
203
+ x1: number,
204
+ y1: number,
205
+ x2: number,
206
+ y2: number
207
+ ): number {
132
208
  const dx = x1 - x2;
133
209
  const dy = y1 - y2;
134
210
  return Math.sqrt(dx * dx + dy * dy);
package/src/index.ts CHANGED
@@ -11,4 +11,5 @@ export { useProps, useDefineProps } from './hooks/useProps'
11
11
  export * from './utils/Ease'
12
12
  export * from './utils/RadialGradient'
13
13
  export * from './components/DisplayObject'
14
- export { isObservable } from 'rxjs'
14
+ export { isObservable } from 'rxjs'
15
+ export * as Utils from './engine/utils'
@@ -1,5 +1,13 @@
1
1
  import { Texture, ImageSource, DOMAdapter, Matrix } from "pixi.js";
2
2
 
3
+ /**
4
+ * Creates a radial gradient texture that can be used in PixiJS.
5
+ * @example
6
+ * const gradient = new RadialGradient(size, size, 0, size, size, 0);
7
+ * gradient.addColorStop(0, "rgba(255, 255, 0, 1)");
8
+ * gradient.addColorStop(0.5, "rgba(255, 255, 0, 0.3)");
9
+ * gradient.addColorStop(0.8, "rgba(255, 255, 0, 0)");
10
+ */
3
11
  export class RadialGradient {
4
12
  private canvas: HTMLCanvasElement;
5
13
  private ctx: CanvasRenderingContext2D | null;
@@ -9,6 +17,16 @@ export class RadialGradient {
9
17
 
10
18
  public size = 600;
11
19
 
20
+ /**
21
+ * Creates a new RadialGradient instance
22
+ * @param x0 - The x-coordinate of the starting circle
23
+ * @param y0 - The y-coordinate of the starting circle
24
+ * @param x1 - The x-coordinate of the ending circle
25
+ * @param y1 - The y-coordinate of the ending circle
26
+ * @param x2 - The x-coordinate for gradient transformation
27
+ * @param y2 - The y-coordinate for gradient transformation
28
+ * @param focalPoint - The focal point of the gradient (0-1), defaults to 0
29
+ */
12
30
  constructor(
13
31
  private x0: number,
14
32
  private y0: number,
@@ -38,12 +56,23 @@ export class RadialGradient {
38
56
  }
39
57
  }
40
58
 
59
+ /**
60
+ * Adds a color stop to the gradient
61
+ * @param offset - The position of the color stop (0-1)
62
+ * @param color - The color value (any valid CSS color string)
63
+ */
41
64
  addColorStop(offset: number, color: string) {
42
65
  if (this.gradient) {
43
66
  this.gradient.addColorStop(offset, color);
44
67
  }
45
68
  }
46
69
 
70
+ /**
71
+ * Renders the gradient and returns the texture with its transformation matrix
72
+ * @param options - Render options
73
+ * @param options.translate - Optional translation coordinates
74
+ * @returns Object containing the texture and transformation matrix
75
+ */
47
76
  render({ translate }: { translate?: { x: number; y: number } } = {}) {
48
77
  const { x0, y0, x1, y1, x2, y2, focalPoint } = this;
49
78
  const defaultSize = this.size;