canvasengine 2.0.0-beta.30 → 2.0.0-beta.32

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { h as a } from "./index-C-iY-lCt.js";
2
- import { A as r, B as n, w as o, C as c, m as l, l as p, D as S, v as m, a5 as u, a4 as g, a2 as b, n as d, G as C, c as j, M as T, N as E, O, P as f, a3 as v, R as w, o as h, p as D, S as P, q as k, r as x, T as y, b as A, V as H, t as M, a1 as V, a0 as B, _ as G, d as N, J as R, H as q, K as U, e as z, W as I, $ as J, f as K, g as L, x as Q, j as W, i as X, y as Y, k as Z, X as _, I as $, Q as F, L as aa, Z as sa, z as ea, s as ta, U as ia, Y as ra, a as na, u as oa } from "./index-C-iY-lCt.js";
1
+ import { h as a } from "./index-19a35G23.js";
2
+ import { A as r, B as n, w as o, C as c, m as l, l as p, D as S, v as m, a5 as u, a4 as g, a2 as b, n as d, G as C, c as j, M as T, N as E, O, P as f, a3 as v, R as w, o as h, p as D, S as P, q as k, r as x, T as y, b as A, V as H, t as M, a1 as V, a0 as B, _ as G, d as N, J as R, H as q, K as U, e as z, W as I, $ as J, f as K, g as L, x as Q, j as W, i as X, y as Y, k as Z, X as _, I as $, Q as F, L as aa, Z as sa, z as ea, s as ta, U as ia, Y as ra, a as na, u as oa } from "./index-19a35G23.js";
3
3
  const e = a.Howler;
4
4
  export {
5
5
  r as ArraySubject,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvasengine",
3
- "version": "2.0.0-beta.30",
3
+ "version": "2.0.0-beta.32",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -4,6 +4,7 @@ import { DisplayObject, ComponentInstance } from "./DisplayObject";
4
4
  import { DisplayObjectProps } from "./types/DisplayObject";
5
5
  import { Signal } from "@signe/reactive";
6
6
  import { on, isTrigger } from "../engine/trigger";
7
+ import { Howl } from "howler";
7
8
 
8
9
  enum TextEffect {
9
10
  Typewriter = "typewriter",
@@ -20,6 +21,11 @@ export interface TextProps extends DisplayObjectProps {
20
21
  start?: () => void;
21
22
  onComplete?: () => void;
22
23
  skip?: () => void;
24
+ sound?: {
25
+ src: string;
26
+ volume?: number;
27
+ rate?: number;
28
+ };
23
29
  };
24
30
  context?: any; // Ensure context is available, ideally typed from a base prop or injected
25
31
  }
@@ -32,6 +38,9 @@ class CanvasText extends DisplayObject(PixiText) {
32
38
  private _wordWrapWidth: number = 0;
33
39
  private typewriterOptions: any = {};
34
40
  private skipSignal?: () => void;
41
+ private typewriterSound?: Howl;
42
+ private lastSoundTime: number = 0;
43
+ private soundDuration: number = 0; // Duration of the sound in milliseconds
35
44
 
36
45
  /**
37
46
  * Called when the component is mounted to the scene graph.
@@ -56,7 +65,13 @@ class CanvasText extends DisplayObject(PixiText) {
56
65
  this.skipTypewriter();
57
66
  });
58
67
  }
68
+ // Initialize typewriter sound if configured
69
+ if (this.typewriterOptions.sound) {
70
+ this.initializeTypewriterSound();
71
+ }
59
72
  }
73
+ // Update layout after initializing typewriter
74
+ this.updateLayout();
60
75
  }
61
76
  this.subscriptionTick = tick.observable.subscribe(() => {
62
77
  if (props.typewriter) {
@@ -70,6 +85,10 @@ class CanvasText extends DisplayObject(PixiText) {
70
85
  if (props.typewriter) {
71
86
  if (props.typewriter) {
72
87
  this.typewriterOptions = props.typewriter;
88
+ // Reinitialize sound if sound configuration changed
89
+ if (props.typewriter.sound) {
90
+ this.initializeTypewriterSound();
91
+ }
73
92
  }
74
93
  }
75
94
  if (props.text !== undefined) {
@@ -79,6 +98,8 @@ class CanvasText extends DisplayObject(PixiText) {
79
98
  this.text = "";
80
99
  this.currentIndex = 0;
81
100
  this.fullText = props.text;
101
+ // Update layout after resetting typewriter
102
+ this.updateLayout();
82
103
  }
83
104
  if (props.style) {
84
105
  for (const key in props.style) {
@@ -97,6 +118,59 @@ class CanvasText extends DisplayObject(PixiText) {
97
118
  if (props.fontFamily) {
98
119
  this.style.fontFamily = props.fontFamily;
99
120
  }
121
+
122
+ // Use the centralized layout update method
123
+ this.updateLayout();
124
+ }
125
+
126
+ get onCompleteCallback() {
127
+ return this.typewriterOptions.onComplete;
128
+ }
129
+
130
+ /**
131
+ * Initializes the typewriter sound effect using Howler.
132
+ * Creates a Howl instance with the configured sound settings.
133
+ * Calculates the sound duration to prevent overlapping sounds.
134
+ */
135
+ private initializeTypewriterSound() {
136
+ if (!this.typewriterOptions.sound?.src) return;
137
+
138
+ this.typewriterSound = new Howl({
139
+ src: [this.typewriterOptions.sound.src],
140
+ volume: this.typewriterOptions.sound.volume ?? 0.5,
141
+ rate: this.typewriterOptions.sound.rate ?? 1.0,
142
+ preload: true,
143
+ onload: () => {
144
+ // Calculate sound duration in milliseconds
145
+ if (this.typewriterSound) {
146
+ const duration = this.typewriterSound.duration();
147
+ const rate = this.typewriterOptions.sound?.rate ?? 1.0;
148
+ this.soundDuration = (duration / rate) * 1000;
149
+ }
150
+ }
151
+ });
152
+ }
153
+
154
+ /**
155
+ * Plays the typewriter sound with duration-based cooldown to prevent overlapping sounds.
156
+ * @param {number} currentTime - The current timestamp to check against sound duration.
157
+ */
158
+ private playTypewriterSound(currentTime: number) {
159
+ if (!this.typewriterSound || !this.typewriterOptions.sound) return;
160
+
161
+ // Check if enough time has passed since the last sound play
162
+ // Use the actual sound duration to prevent overlap
163
+ if (this.soundDuration > 0 && currentTime - this.lastSoundTime < this.soundDuration) return;
164
+
165
+ this.typewriterSound.play();
166
+ this.lastSoundTime = currentTime;
167
+ }
168
+
169
+ /**
170
+ * Updates the layout properties of the text component.
171
+ * This method ensures consistent width, height and word wrap behavior.
172
+ */
173
+ private updateLayout() {
100
174
  if (this._wordWrapWidth) {
101
175
  this.setWidth(this._wordWrapWidth);
102
176
  } else {
@@ -105,10 +179,6 @@ class CanvasText extends DisplayObject(PixiText) {
105
179
  this.setHeight(this.height);
106
180
  }
107
181
 
108
- get onCompleteCallback() {
109
- return this.typewriterOptions.onComplete;
110
- }
111
-
112
182
  private typewriterEffect() {
113
183
  if (this.currentIndex < this.fullText.length) {
114
184
  const nextIndex = Math.min(
@@ -118,6 +188,14 @@ class CanvasText extends DisplayObject(PixiText) {
118
188
  this.text = this.fullText.slice(0, nextIndex);
119
189
  this.currentIndex = nextIndex;
120
190
 
191
+ // Play typewriter sound if configured
192
+ if (this.typewriterOptions.sound) {
193
+ this.playTypewriterSound(Date.now());
194
+ }
195
+
196
+ // Update layout after text change to maintain proper word wrap and dimensions
197
+ this.updateLayout();
198
+
121
199
  // Check if typewriter effect is complete
122
200
  if (
123
201
  this.currentIndex === this.fullText.length &&
@@ -135,11 +213,14 @@ class CanvasText extends DisplayObject(PixiText) {
135
213
  }
136
214
  this.text = this.fullText;
137
215
  this.currentIndex = this.fullText.length;
216
+
217
+ // Update layout after setting full text to maintain proper word wrap and dimensions
218
+ this.updateLayout();
138
219
  }
139
220
 
140
221
  /**
141
222
  * Called when the component is about to be destroyed.
142
- * Unsubscribes from the tick observable.
223
+ * Unsubscribes from the tick observable and cleans up sound resources.
143
224
  * @param {Element<any>} parent - The parent element.
144
225
  * @param {() => void} [afterDestroy] - An optional callback function to be executed after the component's own destruction logic.
145
226
  */
@@ -148,6 +229,12 @@ class CanvasText extends DisplayObject(PixiText) {
148
229
  if (this.subscriptionTick) {
149
230
  this.subscriptionTick.unsubscribe();
150
231
  }
232
+ // Clean up typewriter sound
233
+ if (this.typewriterSound) {
234
+ this.typewriterSound.stop();
235
+ this.typewriterSound.unload();
236
+ this.typewriterSound = undefined;
237
+ }
151
238
  if (afterDestroy) {
152
239
  afterDestroy();
153
240
  }
@@ -254,44 +254,116 @@ export function createComponent(tag: string, props?: Props): Element {
254
254
  }
255
255
  };
256
256
 
257
- async function createElement(parent: Element, child: Element) {
257
+ /**
258
+ * Creates and mounts a child element to a parent element.
259
+ * Handles different types of children: Elements, Promises resolving to Elements, and Observables.
260
+ *
261
+ * @description This function is designed to handle reactive child components that can be:
262
+ * - Direct Element instances
263
+ * - Promises that resolve to Elements (for async components)
264
+ * - Observables that emit Elements, arrays of Elements, or FlowObservable results
265
+ * - Nested observables within arrays or FlowObservable results (handled recursively)
266
+ *
267
+ * For Observables, it subscribes to the stream and automatically mounts/unmounts elements
268
+ * as they are emitted. The function handles nested observables recursively, ensuring that
269
+ * observables within arrays or FlowObservable results are also properly subscribed to.
270
+ * All subscriptions are stored in the parent's effectSubscriptions for automatic cleanup.
271
+ *
272
+ * @param {Element} parent - The parent element to mount the child to
273
+ * @param {Element | Observable<any> | Promise<Element>} child - The child to create and mount
274
+ *
275
+ * @example
276
+ * ```typescript
277
+ * // Direct element
278
+ * await createElement(parent, childElement);
279
+ *
280
+ * // Observable of elements (from cond, loop, etc.)
281
+ * await createElement(parent, cond(signal(visible), () => h(Container)));
282
+ *
283
+ * // Observable that emits arrays containing other observables
284
+ * await createElement(parent, observableOfObservables);
285
+ *
286
+ * // Promise resolving to element
287
+ * await createElement(parent, import('./MyComponent').then(mod => h(mod.default)));
288
+ * ```
289
+ */
290
+ async function createElement(parent: Element, child: Element | Observable<any> | Promise<Element>) {
258
291
  if (isPromise(child)) {
259
292
  child = await child;
260
293
  }
261
294
  if (child instanceof Observable) {
262
- child.subscribe(
263
- ({
264
- elements: comp,
265
- prev,
266
- }: {
267
- elements: Element[];
268
- prev?: Element;
269
- }) => {
270
- // if prev, insert element after this
271
- const components = comp.filter((c) => c !== null);
272
- if (prev) {
273
- components.forEach((c) => {
274
- const index = parent.props.children.indexOf(prev.props.key);
275
- onMount(parent, c, index + 1);
276
- propagateContext(c);
277
- });
278
- return;
279
- }
280
- components.forEach((component) => {
281
- if (!Array.isArray(component)) {
282
- onMount(parent, component);
283
- propagateContext(component);
284
- } else {
285
- component.forEach((comp) => {
286
- onMount(parent, comp);
287
- propagateContext(comp);
288
- });
289
- }
290
- });
291
- elementsListen.next(undefined)
295
+ // Subscribe to the observable and handle the emitted values
296
+ const subscription = child.subscribe(
297
+ (value: any) => {
298
+ // Handle different types of observable emissions
299
+ if (value && typeof value === 'object' && 'elements' in value) {
300
+ // Handle FlowObservable result (from loop, cond, etc.)
301
+ const {
302
+ elements: comp,
303
+ prev,
304
+ }: {
305
+ elements: Element[];
306
+ prev?: Element;
307
+ } = value;
308
+
309
+ const components = comp.filter((c) => c !== null);
310
+ if (prev) {
311
+ components.forEach(async (c) => {
312
+ const index = parent.props.children.indexOf(prev.props.key);
313
+ if (c instanceof Observable) {
314
+ // Handle observable component recursively
315
+ await createElement(parent, c);
316
+ } else if (isElement(c)) {
317
+ onMount(parent, c, index + 1);
318
+ propagateContext(c);
319
+ }
320
+ });
321
+ return;
322
+ }
323
+ components.forEach(async (component) => {
324
+ if (!Array.isArray(component)) {
325
+ if (component instanceof Observable) {
326
+ // Handle observable component recursively
327
+ await createElement(parent, component);
328
+ } else if (isElement(component)) {
329
+ onMount(parent, component);
330
+ propagateContext(component);
331
+ }
332
+ } else {
333
+ component.forEach(async (comp) => {
334
+ if (comp instanceof Observable) {
335
+ // Handle observable component recursively
336
+ await createElement(parent, comp);
337
+ } else if (isElement(comp)) {
338
+ onMount(parent, comp);
339
+ propagateContext(comp);
340
+ }
341
+ });
342
+ }
343
+ });
344
+ } else if (isElement(value)) {
345
+ // Handle direct Element emission
346
+ onMount(parent, value);
347
+ propagateContext(value);
348
+ } else if (Array.isArray(value)) {
349
+ // Handle array of elements (which can also be observables)
350
+ value.forEach(async (element) => {
351
+ if (element instanceof Observable) {
352
+ // Handle observable element recursively
353
+ await createElement(parent, element);
354
+ } else if (isElement(element)) {
355
+ onMount(parent, element);
356
+ propagateContext(element);
357
+ }
358
+ });
359
+ }
360
+ elementsListen.next(undefined);
292
361
  }
293
362
  );
294
- } else {
363
+
364
+ // Store subscription for cleanup
365
+ parent.effectSubscriptions.push(subscription);
366
+ } else if (isElement(child)) {
295
367
  onMount(parent, child);
296
368
  await propagateContext(child);
297
369
  }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ Observable,
2
3
  Subscription
3
4
  } from "rxjs";
4
5
  import type { Element } from "./reactive";
@@ -124,6 +125,9 @@ export function h<C extends ComponentFunction<any>>(
124
125
  else if ('tag' in componentFunction) {
125
126
  component = componentFunction
126
127
  }
128
+ else if (componentFunction instanceof Observable) {
129
+ component = componentFunction as any
130
+ }
127
131
  else {
128
132
  component = componentFunction({ ...props, children }) as Element;
129
133
  }