canvasengine 2.0.0-beta.4 → 2.0.0-beta.6

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.4",
3
+ "version": "2.0.0-beta.6",
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
+ src: 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.src,
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'
@@ -8,6 +8,7 @@ export type Size = number | `${number}%`
8
8
  export type EdgeSize = SignalOrPrimitive<Size | [Size, Size] | [Size, Size, Size, Size]>
9
9
 
10
10
  export interface DisplayObjectProps {
11
+ attach?: any;
11
12
  ref?: string;
12
13
  x?: SignalOrPrimitive<number>;
13
14
  y?: SignalOrPrimitive<number>;
@@ -1,4 +1,4 @@
1
- import { Signal, WritableArraySignal, isSignal } from "@signe/reactive";
1
+ import { Signal, WritableArraySignal, WritableObjectSignal, isSignal } from "@signe/reactive";
2
2
  import {
3
3
  Observable,
4
4
  Subject,
@@ -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,85 +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.attach) {
205
- const isReactiveAttach = isSignal(element.propObservables?.attach)
206
- if (!isReactiveAttach) {
207
- element.props.children.push(element.props.attach)
208
- }
209
- else {
210
- let lastElement = null
211
- element.propObservables.attach.observable.subscribe(({ value, type }) => {
212
- if (type != "init") {
213
- destroyElement(lastElement)
214
- }
215
- lastElement = value
216
- onMount(element, value);
217
- propagateContext(value);
218
- })
219
- }
220
- }
221
- if (!element.props.children) {
222
- 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)
223
219
  }
224
- for (let child of element.props.children) {
225
- if (!child) continue;
226
- if (isPromise(child)) {
227
- child = await child;
228
- }
229
- if (child instanceof Observable) {
230
- child.subscribe(
231
- ({
232
- elements: comp,
233
- prev,
234
- }: {
235
- elements: Element[];
236
- prev?: Element;
237
- }) => {
238
- // if prev, insert element after this
239
- const components = comp.filter((c) => c !== null);
240
- if (prev) {
241
- components.forEach((c) => {
242
- const index = element.props.children.indexOf(prev.props.key);
243
- onMount(element, c, index + 1);
244
- propagateContext(c);
245
- });
246
- return;
247
- }
248
- components.forEach((component) => {
249
- if (!Array.isArray(component)) {
250
- onMount(element, component);
251
- propagateContext(component);
252
- } else {
253
- component.forEach((comp) => {
254
- onMount(element, comp);
255
- propagateContext(comp);
256
- });
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`)
257
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);
258
278
  });
259
- elementsListen.next(undefined)
260
279
  }
261
- );
262
- } else {
263
- onMount(element, child);
264
- await propagateContext(child);
280
+ });
281
+ elementsListen.next(undefined)
265
282
  }
266
- }
267
- };
268
- element.allElements = elementsListen
269
- element.props.context.rootElement = element;
270
- element.componentInstance.onMount?.(element);
271
- propagateContext(element);
272
- }
273
-
274
- if (props) {
275
- for (let key in props) {
276
- const directive = applyDirective(element, key);
277
- if (directive) element.directives[key] = directive;
283
+ );
284
+ } else {
285
+ onMount(parent, child);
286
+ await propagateContext(child);
278
287
  }
279
288
  }
280
289
 
@@ -360,37 +369,71 @@ export function loop<T = any>(
360
369
  });
361
370
  }
362
371
 
372
+ /**
373
+ * Conditionally creates and destroys elements based on a condition signal.
374
+ *
375
+ * @param {Signal<boolean> | boolean} condition - A signal or boolean that determines whether to create an element.
376
+ * @param {Function} createElementFn - A function that returns an element or a promise that resolves to an element.
377
+ * @returns {Observable} An observable that emits the created or destroyed element.
378
+ */
363
379
  export function cond(
364
- condition: Signal,
380
+ condition: Signal<boolean> | boolean,
365
381
  createElementFn: () => Element | Promise<Element>
366
382
  ): FlowObservable {
367
383
  let element: Element | null = null;
368
- return (condition.observable as Observable<boolean>).pipe(
369
- switchMap((bool) => {
370
- if (bool) {
371
- let _el = createElementFn();
372
- if (isPromise(_el)) {
373
- return from(_el as Promise<Element>).pipe(
374
- map((el) => {
375
- element = _el as Element;
376
- return {
384
+
385
+ if (isSignal(condition)) {
386
+ const signalCondition = condition as WritableObjectSignal<boolean>;
387
+ return new Observable<{elements: Element[], type?: "init" | "remove"}>(subscriber => {
388
+ return signalCondition.observable.subscribe(bool => {
389
+ if (bool) {
390
+ let _el = createElementFn();
391
+ if (isPromise(_el)) {
392
+ from(_el as Promise<Element>).subscribe(el => {
393
+ element = el;
394
+ subscriber.next({
377
395
  type: "init",
378
396
  elements: [el],
379
- };
380
- })
381
- );
397
+ });
398
+ });
399
+ } else {
400
+ element = _el as Element;
401
+ subscriber.next({
402
+ type: "init",
403
+ elements: [element],
404
+ });
405
+ }
406
+ } else if (element) {
407
+ destroyElement(element);
408
+ subscriber.next({
409
+ elements: [],
410
+ });
411
+ } else {
412
+ subscriber.next({
413
+ elements: [],
414
+ });
382
415
  }
383
- element = _el as Element;
384
- return of({
385
- type: "init",
386
- elements: [element],
387
- });
388
- } else if (element) {
389
- destroyElement(element);
416
+ });
417
+ });
418
+ } else {
419
+ // Handle boolean case
420
+ if (condition) {
421
+ let _el = createElementFn();
422
+ if (isPromise(_el)) {
423
+ return from(_el as Promise<Element>).pipe(
424
+ map((el) => ({
425
+ type: "init",
426
+ elements: [el],
427
+ }))
428
+ );
390
429
  }
391
430
  return of({
392
- elements: [],
431
+ type: "init",
432
+ elements: [_el as Element],
393
433
  });
394
- })
395
- );
434
+ }
435
+ return of({
436
+ elements: [],
437
+ });
438
+ }
396
439
  }
@@ -0,0 +1,11 @@
1
+ import { bootstrapCanvas, Canvas, ComponentInstance, Element, h } from "canvasengine";
2
+
3
+ export class TestBed {
4
+ static async createComponent(component: any, props: any = {}, children: any = []): Promise<Element<ComponentInstance>> {
5
+ const comp = () => h(Canvas, {
6
+ tickStart: false
7
+ }, h(component, props, children))
8
+ const canvas = await bootstrapCanvas(document.getElementById('root'), comp)
9
+ return canvas.props.children?.[0]
10
+ }
11
+ }