canvasengine 2.0.0-beta.20 → 2.0.0-beta.22

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.20",
3
+ "version": "2.0.0-beta.22",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -5,30 +5,10 @@ import {
5
5
  registerComponent,
6
6
  } from "../engine/reactive";
7
7
  import { ComponentInstance, DisplayObject } from "./DisplayObject";
8
- import { ComponentFunction } from "../engine/signal";
8
+ import { ComponentFunction, h } from "../engine/signal";
9
9
  import { DisplayObjectProps } from "./types/DisplayObject";
10
+ import { CanvasDOMElement, DOMElement } from "./DOMElement";
10
11
 
11
- interface DOMContainerProps extends DisplayObjectProps {
12
- element:
13
- | string
14
- | {
15
- value: HTMLElement;
16
- };
17
- textContent?: string;
18
- attrs?: Record<string, any> & {
19
- class?:
20
- | string
21
- | string[]
22
- | Record<string, boolean>
23
- | { items?: string[] }
24
- | { value?: string | string[] | Record<string, boolean> };
25
- style?:
26
- | string
27
- | Record<string, string | number>
28
- | { value?: string | Record<string, string | number> };
29
- };
30
- sortableChildren?: boolean;
31
- }
32
12
 
33
13
  /**
34
14
  * DOMContainer class for managing DOM elements within the canvas engine
@@ -127,96 +107,10 @@ const EVENTS = [
127
107
 
128
108
  export class CanvasDOMContainer extends DisplayObject(PixiDOMContainer) {
129
109
  disableLayout = true;
130
- private eventListeners: Map<string, (e: Event) => void> = new Map();
131
-
132
- onInit(props: DOMContainerProps) {
133
- super.onInit(props);
134
- if (props.element === undefined) {
135
- throw new Error("DOMContainer: element is required");
136
- }
137
- if (typeof props.element === "string") {
138
- this.element = document.createElement(props.element);
139
- } else {
140
- this.element = props.element.value;
141
- }
142
- for (const event of EVENTS) {
143
- if (props.attrs?.[event]) {
144
- const eventHandler = (e: Event) => {
145
- props.attrs[event]?.(e);
146
- };
147
- this.eventListeners.set(event, eventHandler);
148
- this.element.addEventListener(event, eventHandler, false);
149
- }
150
- }
151
- }
152
-
153
- onUpdate(props: DOMContainerProps) {
154
- super.onUpdate(props);
155
-
156
- for (const [key, value] of Object.entries(props.attrs || {})) {
157
- if (key === "class") {
158
- const classList = value.items || value.value || value;
159
-
160
- // Clear existing classes first
161
- this.element.className = "";
162
-
163
- if (typeof classList === "string") {
164
- // String: space-separated class names
165
- this.element.className = classList;
166
- } else if (Array.isArray(classList)) {
167
- // Array: array of class names
168
- this.element.classList.add(...classList);
169
- } else if (typeof classList === "object" && classList !== null) {
170
- // Object: { className: boolean }
171
- for (const [className, shouldAdd] of Object.entries(classList)) {
172
- if (shouldAdd) {
173
- this.element.classList.add(className);
174
- }
175
- }
176
- }
177
- } else if (key === "style") {
178
- const styleValue = value.items || value.value || value;
179
-
180
- if (typeof styleValue === "string") {
181
- // String: CSS style string
182
- this.element.setAttribute("style", styleValue);
183
- } else if (typeof styleValue === "object" && styleValue !== null) {
184
- // Object: { property: value }
185
- for (const [styleProp, styleVal] of Object.entries(styleValue)) {
186
- if (styleVal !== null && styleVal !== undefined) {
187
- (this.element.style as any)[styleProp] = styleVal;
188
- }
189
- }
190
- }
191
- } else if (!EVENTS.includes(key)) {
192
- this.element.setAttribute(key, value);
193
- }
194
- }
195
- if (props.textContent) {
196
- this.element.textContent = props.textContent;
197
- }
198
-
199
- if (props.sortableChildren !== undefined) {
200
- this.sortableChildren = props.sortableChildren;
201
- }
202
- }
203
-
204
- async onDestroy(
205
- parent: Element<ComponentInstance>,
206
- afterDestroy: () => void
207
- ): Promise<void> {
208
- // Remove all event listeners from the DOM element
209
- if (this.element) {
210
- for (const [event, handler] of this.eventListeners) {
211
- this.element.removeEventListener(event, handler, false);
212
- }
213
- this.eventListeners.clear();
214
- }
215
110
 
216
- const _afterDestroyCallback = async () => {
217
- afterDestroy();
218
- };
219
- await super.onDestroy(parent, _afterDestroyCallback);
111
+ onInit(props: any) {
112
+ const div = h(DOMElement, { element: "div" }, props.children) as unknown as Element<CanvasDOMElement>;
113
+ this.element = div.componentInstance.element;
220
114
  }
221
115
  }
222
116
 
@@ -224,6 +118,6 @@ export interface CanvasDOMContainer extends DisplayObjectProps {}
224
118
 
225
119
  registerComponent("DOMContainer", CanvasDOMContainer);
226
120
 
227
- export const DOMContainer: ComponentFunction<DOMContainerProps> = (props) => {
121
+ export const DOMContainer: ComponentFunction<any> = (props) => {
228
122
  return createComponent("DOMContainer", props);
229
123
  };
@@ -0,0 +1,421 @@
1
+ import { DOMContainer as PixiDOMContainer } from "pixi.js";
2
+ import {
3
+ createComponent,
4
+ Element,
5
+ registerComponent,
6
+ } from "../engine/reactive";
7
+ import { ComponentInstance, DisplayObject, OnHook } from "./DisplayObject";
8
+ import { ComponentFunction } from "../engine/signal";
9
+ import { DisplayObjectProps } from "./types/DisplayObject";
10
+ import { isObservable } from "../engine/utils";
11
+ import { isSignal } from "@signe/reactive";
12
+
13
+ interface DOMContainerProps extends DisplayObjectProps {
14
+ element:
15
+ | string
16
+ | {
17
+ value: HTMLElement;
18
+ };
19
+ textContent?: string;
20
+ attrs?: Record<string, any> & {
21
+ class?:
22
+ | string
23
+ | string[]
24
+ | Record<string, boolean>
25
+ | { items?: string[] }
26
+ | { value?: string | string[] | Record<string, boolean> };
27
+ style?:
28
+ | string
29
+ | Record<string, string | number>
30
+ | { value?: string | Record<string, string | number> };
31
+ };
32
+ onBeforeDestroy?: OnHook;
33
+ }
34
+
35
+ /**
36
+ * DOMContainer class for managing DOM elements within the canvas engine
37
+ *
38
+ * This class extends the DisplayObject functionality to handle DOM elements using
39
+ * PixiJS's native DOMContainer. It provides a bridge between the canvas rendering
40
+ * system and traditional DOM manipulation with proper transform hierarchy and visibility.
41
+ *
42
+ * The DOMContainer is especially useful for rendering standard DOM elements that handle
43
+ * user input, such as `<input>` or `<textarea>`. This is often simpler and more flexible
44
+ * than trying to implement text input directly in PixiJS.
45
+ *
46
+ * ## Form Elements with Reactive Signals
47
+ *
48
+ * For form elements (`input`, `textarea`, `select`), the component supports reactive
49
+ * two-way data binding using signals. When the `value` attribute is a signal, the
50
+ * component automatically:
51
+ * - Sets the initial value from the signal
52
+ * - Listens for `input` events and updates the signal with the new value
53
+ * - Updates the DOM element when the signal value changes programmatically
54
+ *
55
+ * ## Form Submission Handling
56
+ *
57
+ * When a `form` element has a `submit` event handler, the component automatically:
58
+ * - Prevents the default form submission behavior (stops propagation)
59
+ * - Collects all form data from input elements within the form
60
+ * - Passes both the event and the collected form data as parameters to the submit handler
61
+ * - Handles multiple values for the same field name (e.g., checkboxes with same name)
62
+ *
63
+ * @example
64
+ * ```typescript
65
+ * import { signal } from '@signe/reactive';
66
+ *
67
+ * // Basic usage with input element
68
+ * const element = document.createElement('input');
69
+ * element.type = 'text';
70
+ * element.placeholder = 'Enter text...';
71
+ *
72
+ * const domContainer = new DOMContainer({
73
+ * element,
74
+ * x: 100,
75
+ * y: 50,
76
+ * anchor: { x: 0.5, y: 0.5 }
77
+ * });
78
+ *
79
+ * // Reactive form input with signal
80
+ * const inputValue = signal('');
81
+ *
82
+ * const reactiveInput = new DOMContainer({
83
+ * element: 'input',
84
+ * attrs: {
85
+ * type: 'text',
86
+ * placeholder: 'Type something...',
87
+ * value: inputValue // Signal for two-way binding
88
+ * }
89
+ * });
90
+ *
91
+ * // The signal will automatically update when user types
92
+ * inputValue.subscribe(value => {
93
+ * console.log('Input value changed:', value);
94
+ * });
95
+ *
96
+ * // You can also update the input programmatically
97
+ * inputValue.set('New value');
98
+ *
99
+ * // Form submission with automatic data collection
100
+ * const loginForm = new DOMContainer({
101
+ * element: 'form',
102
+ * attrs: {
103
+ * submit: (event, formData) => {
104
+ * // event: the submit event (already prevented)
105
+ * // formData: object containing all form field values
106
+ * console.log('Form submitted with data:', formData);
107
+ * // Example formData: { username: 'john', password: 'secret', remember: 'on' }
108
+ * }
109
+ * },
110
+ * children: [
111
+ * new DOMContainer({
112
+ * element: 'input',
113
+ * attrs: { name: 'username', type: 'text', placeholder: 'Username' }
114
+ * }),
115
+ * new DOMContainer({
116
+ * element: 'input',
117
+ * attrs: { name: 'password', type: 'password', placeholder: 'Password' }
118
+ * }),
119
+ * new DOMContainer({
120
+ * element: 'input',
121
+ * attrs: { name: 'remember', type: 'checkbox', value: 'on' }
122
+ * }),
123
+ * new DOMContainer({
124
+ * element: 'button',
125
+ * attrs: { type: 'submit' },
126
+ * textContent: 'Login'
127
+ * })
128
+ * ]
129
+ * });
130
+ *
131
+ * // Using different class and style formats
132
+ * const containerWithClasses = new DOMContainer({
133
+ * element: 'div',
134
+ * attrs: {
135
+ * // String format: space-separated classes
136
+ * class: 'container primary-theme',
137
+ *
138
+ * // Array format: array of class names
139
+ * // class: ['container', 'primary-theme'],
140
+ *
141
+ * // Object format: conditional classes
142
+ * // class: {
143
+ * // 'container': true,
144
+ * // 'primary-theme': true,
145
+ * // 'disabled': false
146
+ * // }
147
+ *
148
+ * // String format: CSS style string
149
+ * style: 'background-color: red; padding: 10px;',
150
+ *
151
+ * // Object format: style properties
152
+ * // style: {
153
+ * // backgroundColor: 'red',
154
+ * // padding: '10px',
155
+ * // fontSize: 16
156
+ * // }
157
+ * }
158
+ * });
159
+ * ```
160
+ */
161
+ const EVENTS = [
162
+ "click",
163
+ "mouseover",
164
+ "mouseout",
165
+ "mouseenter",
166
+ "mouseleave",
167
+ "mousemove",
168
+ "mouseup",
169
+ "mousedown",
170
+ "touchstart",
171
+ "touchend",
172
+ "touchmove",
173
+ "touchcancel",
174
+ "wheel",
175
+ "scroll",
176
+ "resize",
177
+ "focus",
178
+ "blur",
179
+ "change",
180
+ "input",
181
+ "submit",
182
+ "reset",
183
+ "keydown",
184
+ "keyup",
185
+ "keypress",
186
+ "contextmenu",
187
+ "drag",
188
+ "dragend",
189
+ "dragenter",
190
+ "dragleave",
191
+ "dragover",
192
+ "drop",
193
+ "dragstart",
194
+ "select",
195
+ "selectstart",
196
+ "selectend",
197
+ "selectall",
198
+ "selectnone",
199
+ ];
200
+
201
+ export class CanvasDOMElement {
202
+ public element: HTMLElement;
203
+ private eventListeners: Map<string, (e: Event) => void> = new Map();
204
+ private onBeforeDestroy: OnHook | null = null;
205
+ private valueSignal: any = null;
206
+ private isFormElementType: boolean = false;
207
+
208
+ /**
209
+ * Checks if the element is a form element that supports the value attribute
210
+ * @param elementType - The element type string from props
211
+ * @returns true if the element is a form element with value support
212
+ */
213
+ private isFormElement(elementType: string): boolean {
214
+ const formElements = ["input", "textarea", "select"];
215
+ return formElements.includes(elementType.toLowerCase());
216
+ }
217
+
218
+ onInit(props: DOMContainerProps) {
219
+ if (typeof props.element === "string") {
220
+ this.element = document.createElement(props.element);
221
+ this.isFormElementType = this.isFormElement(props.element);
222
+ } else {
223
+ this.element = props.element.value;
224
+ this.isFormElementType = this.isFormElement(this.element.tagName);
225
+ }
226
+ if (props.onBeforeDestroy || props["on-before-destroy"]) {
227
+ this.onBeforeDestroy =
228
+ props.onBeforeDestroy || props["on-before-destroy"];
229
+ }
230
+
231
+ for (const event of EVENTS) {
232
+ if (props.attrs?.[event]) {
233
+ const eventHandler = (e: Event) => {
234
+ // Special handling for form submit events
235
+ if (event === "submit" && this.element.tagName.toLowerCase() === "form") {
236
+ e.preventDefault(); // Stop form submission propagation
237
+
238
+ // Collect all form data
239
+ const formData = new FormData(this.element as HTMLFormElement);
240
+ const formObject: Record<string, any> = {};
241
+
242
+ // Convert FormData to plain object
243
+ formData.forEach((value, key) => {
244
+ if (formObject[key]) {
245
+ // Handle multiple values for same key (like checkboxes)
246
+ if (Array.isArray(formObject[key])) {
247
+ formObject[key].push(value);
248
+ } else {
249
+ formObject[key] = [formObject[key], value];
250
+ }
251
+ } else {
252
+ formObject[key] = value;
253
+ }
254
+ });
255
+
256
+ // Call the event handler with event and form data
257
+ props.attrs[event]?.(e, formObject);
258
+ } else {
259
+ props.attrs[event]?.(e);
260
+ }
261
+ };
262
+ this.eventListeners.set(event, eventHandler);
263
+ this.element.addEventListener(event, eventHandler, false);
264
+ }
265
+ }
266
+ if (props.children) {
267
+ for (const child of props.children) {
268
+ if (isObservable(child)) {
269
+ child.subscribe(({ elements }) => {
270
+ for (const element of elements) {
271
+ this.element.appendChild(element.componentInstance.element);
272
+ }
273
+ });
274
+ } else {
275
+ this.element.appendChild(child.componentInstance.element);
276
+ }
277
+ }
278
+ }
279
+ this.onUpdate(props);
280
+ }
281
+
282
+ onMount(context: Element<CanvasDOMElement>) {
283
+ const props = context.propObservables;
284
+ const attrs = props.attrs as any;
285
+ // Handle form elements with signal value
286
+ if (
287
+ this.isFormElementType &&
288
+ attrs?.value &&
289
+ isSignal(attrs.value)
290
+ ) {
291
+ this.valueSignal = attrs.value;
292
+ // Set initial value from signal
293
+ (
294
+ this.element as
295
+ | HTMLInputElement
296
+ | HTMLTextAreaElement
297
+ | HTMLSelectElement
298
+ ).value = this.valueSignal();
299
+
300
+ // Listen for input events and update the signal
301
+ const inputHandler = (e: Event) => {
302
+ const target = e.target as
303
+ | HTMLInputElement
304
+ | HTMLTextAreaElement
305
+ | HTMLSelectElement;
306
+ this.valueSignal.set(target.value);
307
+ };
308
+
309
+ this.eventListeners.set("input", inputHandler);
310
+ this.element.addEventListener("input", inputHandler, false);
311
+ }
312
+ }
313
+
314
+ onUpdate(props: DOMContainerProps) {
315
+ if (!this.element) return;
316
+ for (const [key, value] of Object.entries(props.attrs || {})) {
317
+ if (key === "class") {
318
+ const classList = value.items || value.value || value;
319
+
320
+ // Clear existing classes first
321
+ this.element.className = "";
322
+
323
+ if (typeof classList === "string") {
324
+ // String: space-separated class names
325
+ this.element.className = classList;
326
+ } else if (Array.isArray(classList)) {
327
+ // Array: array of class names
328
+ this.element.classList.add(...classList);
329
+ } else if (typeof classList === "object" && classList !== null) {
330
+ // Object: { className: boolean }
331
+ for (const [className, shouldAdd] of Object.entries(classList)) {
332
+ if (shouldAdd) {
333
+ this.element.classList.add(className);
334
+ }
335
+ }
336
+ }
337
+ } else if (key === "style") {
338
+ const styleValue = value.items || value.value || value;
339
+
340
+ if (typeof styleValue === "string") {
341
+ // String: CSS style string
342
+ this.element.setAttribute("style", styleValue);
343
+ } else if (typeof styleValue === "object" && styleValue !== null) {
344
+ // Object: { property: value }
345
+ for (const [styleProp, styleVal] of Object.entries(styleValue)) {
346
+ if (styleVal !== null && styleVal !== undefined) {
347
+ (this.element.style as any)[styleProp] = styleVal;
348
+ }
349
+ }
350
+ }
351
+ } else if (key === "value" && this.isFormElementType) {
352
+ // Handle value attribute for form elements
353
+ if (isSignal(value)) {
354
+ // If it's a signal, the value is already handled in onInit
355
+ // Update the DOM element value if the signal value changed
356
+ const currentValue = (
357
+ this.element as
358
+ | HTMLInputElement
359
+ | HTMLTextAreaElement
360
+ | HTMLSelectElement
361
+ ).value;
362
+ const signalValue = value();
363
+ if (currentValue !== signalValue) {
364
+ (
365
+ this.element as
366
+ | HTMLInputElement
367
+ | HTMLTextAreaElement
368
+ | HTMLSelectElement
369
+ ).value = signalValue;
370
+ }
371
+ } else {
372
+ // If it's not a signal, set the value directly
373
+ (
374
+ this.element as
375
+ | HTMLInputElement
376
+ | HTMLTextAreaElement
377
+ | HTMLSelectElement
378
+ ).value = value;
379
+ }
380
+ } else if (!EVENTS.includes(key)) {
381
+ this.element.setAttribute(key, value);
382
+ }
383
+ }
384
+ if (props.textContent) {
385
+ this.element.textContent = props.textContent;
386
+ }
387
+ }
388
+
389
+ async onDestroy(
390
+ parent: Element<CanvasDOMElement>,
391
+ afterDestroy: () => void
392
+ ): Promise<void> {
393
+ // Remove all event listeners from the DOM element
394
+
395
+ if (this.element) {
396
+ if (this.onBeforeDestroy) {
397
+ await this.onBeforeDestroy();
398
+ }
399
+
400
+ for (const [event, handler] of this.eventListeners) {
401
+ this.element.removeEventListener(event, handler, false);
402
+ }
403
+
404
+ this.eventListeners.clear();
405
+
406
+ this.element.remove();
407
+
408
+ if (afterDestroy) {
409
+ afterDestroy();
410
+ }
411
+ }
412
+ }
413
+ }
414
+
415
+ export interface CanvasDOMElement extends DisplayObjectProps {}
416
+
417
+ registerComponent("DOMElement", CanvasDOMElement);
418
+
419
+ export const DOMElement: ComponentFunction<DOMContainerProps> = (props) => {
420
+ return createComponent("DOMElement", props);
421
+ };
@@ -96,7 +96,7 @@ export const EVENTS = [
96
96
  "wheelcapture",
97
97
  ];
98
98
 
99
- type OnHook = (() => void) | (() => Promise<void> | void);
99
+ export type OnHook = (() => void) | (() => Promise<void> | void);
100
100
 
101
101
  export function DisplayObject(extendClass) {
102
102
  return class DisplayObject extends extendClass {
@@ -11,4 +11,5 @@ export { TilingSprite } from './TilingSprite'
11
11
  export { Viewport } from './Viewport'
12
12
  export { NineSliceSprite } from './NineSliceSprite'
13
13
  export { type ComponentInstance } from './DisplayObject'
14
- export { DOMContainer } from './DOMContainer'
14
+ export { DOMContainer } from './DOMContainer'
15
+ export { DOMElement } from './DOMElement'
@@ -1,6 +1,7 @@
1
1
  import '@pixi/layout';
2
- import { Application } from "pixi.js";
2
+ import { Application, ApplicationOptions } from "pixi.js";
3
3
  import { ComponentFunction, h } from "./signal";
4
+ import { useProps } from '../hooks/useProps';
4
5
 
5
6
  /**
6
7
  * Bootstraps a canvas element and renders it to the DOM.
@@ -10,12 +11,13 @@ import { ComponentFunction, h } from "./signal";
10
11
  * @returns A Promise that resolves to the rendered canvas element.
11
12
  * @throws {Error} If the provided element is not a Canvas component.
12
13
  */
13
- export const bootstrapCanvas = async (rootElement: HTMLElement | null, canvas: ComponentFunction<any>) => {
14
+ export const bootstrapCanvas = async (rootElement: HTMLElement | null, canvas: ComponentFunction<any>, options?: ApplicationOptions) => {
14
15
 
15
16
  const app = new Application();
16
17
  await app.init({
17
18
  resizeTo: rootElement,
18
19
  autoStart: false,
20
+ ...(options ?? {})
19
21
  });
20
22
  const canvasElement = await h(canvas);
21
23
  if (canvasElement.tag != 'Canvas') {
@@ -23,6 +25,12 @@ export const bootstrapCanvas = async (rootElement: HTMLElement | null, canvas: C
23
25
  }
24
26
  (canvasElement as any).render(rootElement, app);
25
27
 
28
+ const { backgroundColor } = useProps(canvasElement.props, {
29
+ backgroundColor: 'black'
30
+ });
31
+
32
+ app.renderer.background.color = backgroundColor()
33
+
26
34
  return {
27
35
  canvasElement,
28
36
  app
@@ -7,6 +7,7 @@ import {
7
7
  from,
8
8
  map,
9
9
  of,
10
+ share,
10
11
  switchMap,
11
12
  } from "rxjs";
12
13
  import { ComponentInstance } from "../components/DisplayObject";
@@ -83,14 +84,26 @@ function destroyElement(element: Element | Element[]) {
83
84
  if (!element) {
84
85
  return;
85
86
  }
87
+ if (element.props?.children) {
88
+ for (let child of element.props.children) {
89
+ destroyElement(child)
90
+ }
91
+ }
86
92
  for (let name in element.directives) {
87
93
  element.directives[name].onDestroy?.(element);
88
94
  }
89
- element.componentInstance.onDestroy(element.parent as any, () => {
90
- element.propSubscriptions.forEach((sub) => sub.unsubscribe());
91
- element.effectSubscriptions.forEach((sub) => sub.unsubscribe());
92
- element.effectUnmounts.forEach((fn) => fn?.());
93
- });
95
+ if (element.componentInstance && element.componentInstance.onDestroy) {
96
+ element.componentInstance.onDestroy(element.parent as any, () => {
97
+ element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
98
+ element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
99
+ element.effectUnmounts?.forEach((fn) => fn?.());
100
+ });
101
+ } else {
102
+ // If componentInstance is undefined or doesn't have onDestroy, still clean up subscriptions
103
+ element.propSubscriptions?.forEach((sub) => sub.unsubscribe());
104
+ element.effectSubscriptions?.forEach((sub) => sub.unsubscribe());
105
+ element.effectUnmounts?.forEach((fn) => fn?.());
106
+ }
94
107
  }
95
108
 
96
109
  /**
@@ -489,7 +502,7 @@ export function cond(
489
502
  createElementFn: () => Element | Promise<Element>
490
503
  ): FlowObservable {
491
504
  let element: Element | null = null;
492
-
505
+
493
506
  if (isSignal(condition)) {
494
507
  const signalCondition = condition as WritableObjectSignal<boolean>;
495
508
  return new Observable<{elements: Element[], type?: "init" | "remove"}>(subscriber => {
@@ -522,7 +535,7 @@ export function cond(
522
535
  });
523
536
  }
524
537
  });
525
- });
538
+ }).pipe(share())
526
539
  } else {
527
540
  // Handle boolean case
528
541
  if (condition) {
@@ -1,4 +1,5 @@
1
1
  import { ObservablePoint } from "pixi.js"
2
+ import { Observable } from "rxjs"
2
3
 
3
4
  /**
4
5
  * Checks if code is running in a browser environment
@@ -101,6 +102,10 @@ export function isObject(val: unknown): boolean {
101
102
  return typeof val == 'object' && val != null && !Array.isArray(val) && val.constructor === Object
102
103
  }
103
104
 
105
+ export function isObservable(val: unknown): boolean {
106
+ return val instanceof Observable
107
+ }
108
+
104
109
  /**
105
110
  * Sets a value in an object using a dot notation path
106
111
  * @param {Record<string, any>} obj - Target object