canvasengine 2.0.0-beta.60 → 2.0.0-beta.62

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.
@@ -0,0 +1,461 @@
1
+ import { effect, isSignal } from "@signe/reactive";
2
+ import { Container, Graphics, Rectangle, Sprite as PixiSprite } from "pixi.js";
3
+ import { OutlineFilter } from "pixi-filters/outline";
4
+ import { Directive, registerDirective } from "../engine/directive";
5
+ import { isElement } from "../engine/reactive";
6
+ import type { Element } from "../engine/reactive";
7
+ import type { SignalOrPrimitive } from "../components/types";
8
+ import type { Subscription } from "rxjs";
9
+
10
+ type MaybeSignal<T> = SignalOrPrimitive<T> | T;
11
+
12
+ const valueOf = <T>(value: MaybeSignal<T> | undefined, fallback: T): T => {
13
+ if (value === undefined) return fallback;
14
+ return isSignal(value as any) ? (value as any)() : (value as T);
15
+ };
16
+
17
+ const rawValueOf = <T>(value: MaybeSignal<T> | undefined): T | undefined => {
18
+ if (value === undefined) return undefined;
19
+ return isSignal(value as any) ? (value as any)() : (value as T);
20
+ };
21
+
22
+ const asArray = <T>(value: T | T[] | undefined): T[] => {
23
+ if (value === undefined) return [];
24
+ return Array.isArray(value) ? value : [value];
25
+ };
26
+
27
+ const getFilters = (instance: Container): any[] => {
28
+ const filters = (instance as any).filters;
29
+ if (!filters) return [];
30
+ return Array.isArray(filters) ? [...filters] : [filters];
31
+ };
32
+
33
+ const setFilters = (instance: Container, filters: any[]) => {
34
+ (instance as any).filters = filters.length > 0 ? filters : [];
35
+ };
36
+
37
+ const getElementInstance = (value: unknown): Container | null => {
38
+ const resolved = rawValueOf(value as any);
39
+ if (isElement(resolved)) {
40
+ return (resolved.componentInstance as unknown as Container) ?? null;
41
+ }
42
+ if (resolved instanceof Container) return resolved;
43
+ if (resolved && typeof resolved === "object" && typeof (resolved as any).getBounds === "function") {
44
+ return resolved as Container;
45
+ }
46
+ return null;
47
+ };
48
+
49
+ const normalizeBounds = (bounds: any): Rectangle => {
50
+ return new Rectangle(bounds?.x ?? 0, bounds?.y ?? 0, bounds?.width ?? 0, bounds?.height ?? 0);
51
+ };
52
+
53
+ const getWorldBounds = (instance: any, mode: "bounds" | "hitbox" = "bounds"): Rectangle => {
54
+ const bounds = normalizeBounds(
55
+ typeof instance.getBounds === "function"
56
+ ? instance.getBounds()
57
+ : { x: instance.x ?? 0, y: instance.y ?? 0, width: instance.width ?? 0, height: instance.height ?? 0 }
58
+ );
59
+
60
+ if (mode !== "hitbox" || !instance.hitbox) {
61
+ return bounds;
62
+ }
63
+
64
+ const hitbox = instance.hitbox;
65
+ return new Rectangle(
66
+ bounds.x + Math.max(0, (bounds.width - hitbox.w) / 2),
67
+ bounds.y + Math.max(0, bounds.height - hitbox.h),
68
+ hitbox.w,
69
+ hitbox.h
70
+ );
71
+ };
72
+
73
+ const intersectRect = (a: Rectangle, b: Rectangle): Rectangle | null => {
74
+ const x = Math.max(a.x, b.x);
75
+ const y = Math.max(a.y, b.y);
76
+ const right = Math.min(a.x + a.width, b.x + b.width);
77
+ const bottom = Math.min(a.y + a.height, b.y + b.height);
78
+ const width = right - x;
79
+ const height = bottom - y;
80
+ if (width <= 0 || height <= 0) return null;
81
+ return new Rectangle(x, y, width, height);
82
+ };
83
+
84
+ const drawFilledRect = (graphics: Graphics, rect: Rectangle) => {
85
+ graphics.rect(rect.x, rect.y, rect.width, rect.height);
86
+ graphics.fill(0xffffff);
87
+ };
88
+
89
+ const addMaskToParent = (instance: Container, mask: Graphics) => {
90
+ if (mask.parent === instance.parent) return;
91
+ if (mask.parent) {
92
+ mask.parent.removeChild(mask);
93
+ }
94
+ instance.parent?.addChild(mask);
95
+ };
96
+
97
+ const setMask = (instance: any, mask: any, inverse = false) => {
98
+ if (!instance) return;
99
+ if (typeof instance.setMask === "function") {
100
+ if (mask == null) {
101
+ instance.mask = null;
102
+ }
103
+ instance.setMask({ mask: mask ?? null, inverse });
104
+ } else {
105
+ instance.mask = mask ?? null;
106
+ }
107
+ };
108
+
109
+ const clearMask = (instance: any, fallbackMask: any) => {
110
+ setMask(instance, fallbackMask ?? null);
111
+ };
112
+
113
+ const copyTransform = (target: any, source: any) => {
114
+ const position = { x: source.x ?? source.position?.x ?? 0, y: source.y ?? source.position?.y ?? 0 };
115
+ if (target.position?.copyFrom) {
116
+ target.position.copyFrom(position);
117
+ } else {
118
+ target.x = position.x;
119
+ target.y = position.y;
120
+ }
121
+ target.scale?.copyFrom?.(source.scale ?? { x: 1, y: 1 });
122
+ target.pivot?.copyFrom?.(source.pivot ?? { x: 0, y: 0 });
123
+ target.skew?.copyFrom?.(source.skew ?? { x: 0, y: 0 });
124
+ target.rotation = source.rotation ?? 0;
125
+ };
126
+
127
+ const drawDisplayRect = (graphics: Graphics, instance: any, rect: Rectangle) => {
128
+ copyTransform(graphics, instance);
129
+
130
+ const textureWidth = instance.texture?.orig?.width ?? instance.texture?.width ?? instance.width ?? rect.width;
131
+ const textureHeight = instance.texture?.orig?.height ?? instance.texture?.height ?? instance.height ?? rect.height;
132
+ const displayWidth = instance.width || textureWidth || 1;
133
+ const displayHeight = instance.height || textureHeight || 1;
134
+ const anchorX = instance.anchor?.x ?? 0;
135
+ const anchorY = instance.anchor?.y ?? 0;
136
+
137
+ drawFilledRect(
138
+ graphics,
139
+ new Rectangle(
140
+ -anchorX * textureWidth + (rect.x * textureWidth) / displayWidth,
141
+ -anchorY * textureHeight + (rect.y * textureHeight) / displayHeight,
142
+ (rect.width * textureWidth) / displayWidth,
143
+ (rect.height * textureHeight) / displayHeight
144
+ )
145
+ );
146
+ };
147
+
148
+ export type OutlineProps = {
149
+ enabled?: SignalOrPrimitive<boolean>;
150
+ color?: SignalOrPrimitive<number>;
151
+ thickness?: SignalOrPrimitive<number>;
152
+ quality?: SignalOrPrimitive<number>;
153
+ alpha?: SignalOrPrimitive<number>;
154
+ };
155
+
156
+ export type ClipShape =
157
+ | {
158
+ type: "rect";
159
+ x: SignalOrPrimitive<number>;
160
+ y: SignalOrPrimitive<number>;
161
+ width: SignalOrPrimitive<number>;
162
+ height: SignalOrPrimitive<number>;
163
+ };
164
+
165
+ export type ClipProps = {
166
+ enabled?: SignalOrPrimitive<boolean>;
167
+ mode?: SignalOrPrimitive<"keep" | "hide">;
168
+ shape: ClipShape;
169
+ };
170
+
171
+ export type OcclusionProps = {
172
+ enabled?: SignalOrPrimitive<boolean>;
173
+ obstacles: SignalOrPrimitive<Element | Element[] | Container | Container[]>;
174
+ bounds?: SignalOrPrimitive<"bounds" | "hitbox">;
175
+ padding?: SignalOrPrimitive<number>;
176
+ alpha?: SignalOrPrimitive<number>;
177
+ zIndex?: SignalOrPrimitive<number>;
178
+ };
179
+
180
+ export class Outline extends Directive {
181
+ private elementRef: Element<Container> | null = null;
182
+ private filter: OutlineFilter | null = null;
183
+ private updateEffect: ReturnType<typeof effect> | null = null;
184
+
185
+ onInit(element: Element<Container>) {
186
+ this.elementRef = element;
187
+ }
188
+
189
+ onMount() {
190
+ this.updateEffect = effect(() => {
191
+ this.apply();
192
+ });
193
+ }
194
+
195
+ onUpdate() {
196
+ this.apply();
197
+ }
198
+
199
+ onDestroy() {
200
+ this.removeFilter();
201
+ this.updateEffect?.subscription.unsubscribe();
202
+ this.updateEffect = null;
203
+ this.elementRef = null;
204
+ }
205
+
206
+ private get props(): OutlineProps {
207
+ const props = rawValueOf(this.elementRef?.props.outline);
208
+ if (props === true) return {};
209
+ if (props === false) return { enabled: false };
210
+ return (props as OutlineProps | undefined) ?? {};
211
+ }
212
+
213
+ private apply() {
214
+ const instance = this.elementRef?.componentInstance;
215
+ if (!instance) return;
216
+
217
+ const props = this.props;
218
+ const enabled = valueOf(props.enabled, true);
219
+
220
+ if (!enabled) {
221
+ this.removeFilter();
222
+ return;
223
+ }
224
+
225
+ if (!this.filter) {
226
+ this.filter = new OutlineFilter({
227
+ thickness: valueOf(props.thickness, 1),
228
+ color: valueOf(props.color, 0xffffff),
229
+ quality: valueOf(props.quality, 0.1),
230
+ alpha: valueOf(props.alpha, 1),
231
+ });
232
+ setFilters(instance, [...getFilters(instance), this.filter]);
233
+ }
234
+
235
+ this.filter.thickness = valueOf(props.thickness, 1);
236
+ this.filter.color = valueOf(props.color, 0xffffff);
237
+ this.filter.quality = valueOf(props.quality, 0.1);
238
+ this.filter.alpha = valueOf(props.alpha, 1);
239
+ }
240
+
241
+ private removeFilter() {
242
+ const instance = this.elementRef?.componentInstance;
243
+ if (!instance || !this.filter) return;
244
+ setFilters(instance, getFilters(instance).filter((filter) => filter !== this.filter));
245
+ this.filter = null;
246
+ }
247
+ }
248
+
249
+ export class Clip extends Directive {
250
+ private elementRef: Element<Container> | null = null;
251
+ private maskGraphics: Graphics | null = null;
252
+ private previousMask: any = null;
253
+ private tickSubscription: Subscription | null = null;
254
+ private updateEffect: ReturnType<typeof effect> | null = null;
255
+
256
+ onInit(element: Element<Container>) {
257
+ this.elementRef = element;
258
+ }
259
+
260
+ onMount(element: Element<Container>) {
261
+ this.tickSubscription = element.props.context?.tick?.observable?.subscribe(() => {
262
+ this.apply();
263
+ }) ?? null;
264
+ this.updateEffect = effect(() => {
265
+ this.apply();
266
+ });
267
+ }
268
+
269
+ onUpdate() {
270
+ this.apply();
271
+ }
272
+
273
+ onDestroy() {
274
+ const instance = this.elementRef?.componentInstance as any;
275
+ if (instance && this.maskGraphics) {
276
+ clearMask(instance, this.previousMask);
277
+ }
278
+ if (this.maskGraphics?.parent) {
279
+ this.maskGraphics.parent.removeChild(this.maskGraphics);
280
+ }
281
+ this.maskGraphics?.destroy();
282
+ this.maskGraphics = null;
283
+ this.previousMask = null;
284
+ this.tickSubscription?.unsubscribe();
285
+ this.tickSubscription = null;
286
+ this.updateEffect?.subscription.unsubscribe();
287
+ this.updateEffect = null;
288
+ this.elementRef = null;
289
+ }
290
+
291
+ private get props(): ClipProps | null {
292
+ return (rawValueOf(this.elementRef?.props.clip) as ClipProps | undefined) ?? null;
293
+ }
294
+
295
+ private apply() {
296
+ const instance = this.elementRef?.componentInstance as any;
297
+ const props = this.props;
298
+ if (!instance || !instance.parent || !props?.shape) return;
299
+
300
+ if (!valueOf(props.enabled, true)) {
301
+ if (this.maskGraphics) {
302
+ clearMask(instance, this.previousMask);
303
+ this.maskGraphics.clear();
304
+ }
305
+ return;
306
+ }
307
+
308
+ if (!this.maskGraphics) {
309
+ this.maskGraphics = new Graphics();
310
+ this.previousMask = instance.mask ?? null;
311
+ }
312
+
313
+ addMaskToParent(instance, this.maskGraphics);
314
+ this.maskGraphics.clear();
315
+
316
+ const shape = props.shape;
317
+ if (shape.type === "rect") {
318
+ const rect = new Rectangle(
319
+ valueOf(shape.x, 0),
320
+ valueOf(shape.y, 0),
321
+ valueOf(shape.width, 0),
322
+ valueOf(shape.height, 0)
323
+ );
324
+ drawDisplayRect(this.maskGraphics, instance, rect);
325
+ }
326
+
327
+ const inverse = valueOf(props.mode, "keep") === "hide";
328
+ setMask(instance, this.maskGraphics, inverse);
329
+ }
330
+ }
331
+
332
+ export class Occlusion extends Directive {
333
+ private elementRef: Element<Container> | null = null;
334
+ private maskGraphics: Graphics | null = null;
335
+ private ghostSprite: PixiSprite | null = null;
336
+ private tickSubscription: Subscription | null = null;
337
+ private updateEffect: ReturnType<typeof effect> | null = null;
338
+
339
+ onInit(element: Element<Container>) {
340
+ this.elementRef = element;
341
+ }
342
+
343
+ onMount(element: Element<Container>) {
344
+ this.tickSubscription = element.props.context?.tick?.observable?.subscribe(() => {
345
+ this.apply();
346
+ }) ?? null;
347
+ this.updateEffect = effect(() => {
348
+ this.apply();
349
+ });
350
+ }
351
+
352
+ onUpdate() {
353
+ this.apply();
354
+ }
355
+
356
+ onDestroy() {
357
+ clearMask(this.ghostSprite, null);
358
+ if (this.ghostSprite?.parent) {
359
+ this.ghostSprite.parent.removeChild(this.ghostSprite);
360
+ }
361
+ this.ghostSprite?.destroy();
362
+ if (this.maskGraphics?.parent) {
363
+ this.maskGraphics.parent.removeChild(this.maskGraphics);
364
+ }
365
+ this.maskGraphics?.destroy();
366
+ this.maskGraphics = null;
367
+ this.ghostSprite = null;
368
+ this.tickSubscription?.unsubscribe();
369
+ this.tickSubscription = null;
370
+ this.updateEffect?.subscription.unsubscribe();
371
+ this.updateEffect = null;
372
+ this.elementRef = null;
373
+ }
374
+
375
+ private get props(): OcclusionProps | null {
376
+ return (rawValueOf(this.elementRef?.props.occlusion) as OcclusionProps | undefined) ?? null;
377
+ }
378
+
379
+ private apply() {
380
+ const instance = this.elementRef?.componentInstance as any;
381
+ const props = this.props;
382
+ if (!instance || !props) return;
383
+
384
+ if (!valueOf(props.enabled, true)) {
385
+ if (this.maskGraphics) {
386
+ this.maskGraphics.clear();
387
+ }
388
+ if (this.ghostSprite) {
389
+ this.ghostSprite.visible = false;
390
+ }
391
+ return;
392
+ }
393
+
394
+ if (!this.maskGraphics || !this.ghostSprite) {
395
+ if (!instance.texture) return;
396
+ this.maskGraphics = new Graphics();
397
+ this.ghostSprite = new PixiSprite(instance.texture);
398
+ }
399
+
400
+ addMaskToParent(instance, this.maskGraphics);
401
+ if (this.ghostSprite.parent !== instance.parent) {
402
+ if (this.ghostSprite.parent) {
403
+ this.ghostSprite.parent.removeChild(this.ghostSprite);
404
+ }
405
+ instance.parent?.addChild(this.ghostSprite);
406
+ }
407
+
408
+ this.maskGraphics.clear();
409
+ this.ghostSprite.texture = instance.texture;
410
+ this.ghostSprite.width = instance.width;
411
+ this.ghostSprite.height = instance.height;
412
+ this.ghostSprite.visible = false;
413
+ this.ghostSprite.alpha = valueOf(props.alpha, 0.35);
414
+ copyTransform(this.ghostSprite, instance);
415
+ if (instance.anchor && this.ghostSprite.anchor) {
416
+ this.ghostSprite.anchor.copyFrom(instance.anchor);
417
+ }
418
+
419
+ const boundsMode = valueOf(props.bounds, "bounds");
420
+ const padding = valueOf(props.padding, 0);
421
+ const targetBounds = getWorldBounds(instance, boundsMode);
422
+ let hasIntersection = false;
423
+ let maxObstacleZIndex = instance.zIndex ?? 0;
424
+
425
+ for (const obstacle of asArray(rawValueOf(props.obstacles) as any)) {
426
+ const obstacleInstance = getElementInstance(obstacle);
427
+ if (!obstacleInstance) continue;
428
+
429
+ maxObstacleZIndex = Math.max(maxObstacleZIndex, (obstacleInstance as any).zIndex ?? 0);
430
+ const obstacleBounds = getWorldBounds(obstacleInstance, boundsMode);
431
+ const intersection = intersectRect(targetBounds, obstacleBounds);
432
+ if (!intersection) continue;
433
+
434
+ hasIntersection = true;
435
+ const parentLocalTopLeft = instance.parent?.toLocal?.({ x: intersection.x - padding, y: intersection.y - padding })
436
+ ?? { x: intersection.x - padding, y: intersection.y - padding };
437
+ drawFilledRect(
438
+ this.maskGraphics,
439
+ new Rectangle(
440
+ parentLocalTopLeft.x,
441
+ parentLocalTopLeft.y,
442
+ intersection.width + padding * 2,
443
+ intersection.height + padding * 2
444
+ )
445
+ );
446
+ }
447
+
448
+ if (hasIntersection) {
449
+ this.ghostSprite.visible = true;
450
+ this.ghostSprite.zIndex = valueOf(props.zIndex, maxObstacleZIndex + 1);
451
+ setMask(this.ghostSprite, this.maskGraphics, false);
452
+ } else {
453
+ this.ghostSprite.visible = false;
454
+ clearMask(this.ghostSprite, null);
455
+ }
456
+ }
457
+ }
458
+
459
+ registerDirective("outline", Outline);
460
+ registerDirective("clip", Clip);
461
+ registerDirective("occlusion", Occlusion);
@@ -12,3 +12,4 @@ export * from './Transition'
12
12
  export * from './Shake'
13
13
  export * from './Flash'
14
14
  export * from './FogVisibility'
15
+ export * from './SpriteEffects'
@@ -1,4 +1,5 @@
1
1
  import { Application, ApplicationOptions } from "pixi.js";
2
+ import { Observable, Subscription } from "rxjs";
2
3
  import { ComponentFunction, h } from "./signal";
3
4
  import { useProps } from '../hooks/useProps';
4
5
  import { registerAllComponents, registerComponent } from './reactive';
@@ -33,6 +34,12 @@ export interface BootstrapOptions extends ApplicationOptions {
33
34
  enableLayout?: boolean; // true by default
34
35
  }
35
36
 
37
+ type BootstrapResult = {
38
+ canvasElement: any;
39
+ app: Application;
40
+ hmrSubscription?: Subscription;
41
+ };
42
+
36
43
  /**
37
44
  * Bootstraps a canvas element and renders it to the DOM.
38
45
  *
@@ -61,7 +68,7 @@ export interface BootstrapOptions extends ApplicationOptions {
61
68
  * });
62
69
  * ```
63
70
  */
64
- export const bootstrapCanvas = async (rootElement: HTMLElement | null, canvas: ComponentFunction<any>, options?: BootstrapOptions) => {
71
+ export const bootstrapCanvas = async (rootElement: HTMLElement | null, canvas: ComponentFunction<any>, options?: BootstrapOptions): Promise<BootstrapResult> => {
65
72
  // Extract component registration options
66
73
  const { components, autoRegister, enableLayout, ...appOptions } = options ?? {};
67
74
  if (enableLayout !== false) {
@@ -90,20 +97,57 @@ export const bootstrapCanvas = async (rootElement: HTMLElement | null, canvas: C
90
97
  antialias: true,
91
98
  ...appOptions
92
99
  });
93
- const canvasElement = await h(canvas);
94
- if (canvasElement.tag != 'Canvas') {
95
- throw new Error('Canvas is required');
96
- }
97
- (canvasElement as any).render(rootElement, app);
98
100
 
99
- const { backgroundColor } = useProps(canvasElement.props, {
100
- backgroundColor: 'black'
101
- });
101
+ const renderCanvasElement = (canvasElement: any) => {
102
+ if (canvasElement.tag != 'Canvas') {
103
+ throw new Error('Canvas is required');
104
+ }
105
+ canvasElement.render(rootElement, app);
106
+
107
+ const { backgroundColor } = useProps(canvasElement.props, {
108
+ backgroundColor: 'black'
109
+ });
102
110
 
103
- app.renderer.background.color = backgroundColor()
111
+ app.renderer.background.color = backgroundColor()
104
112
 
105
- return {
106
- canvasElement,
107
- app
113
+ return {
114
+ canvasElement,
115
+ app
116
+ };
108
117
  };
118
+
119
+ const canvasElement = h(canvas) as any;
120
+
121
+ if (canvasElement instanceof Observable) {
122
+ return new Promise<BootstrapResult>((resolve, reject) => {
123
+ let resolved = false;
124
+ let hmrSubscription: Subscription;
125
+
126
+ hmrSubscription = canvasElement.subscribe({
127
+ next(value: any) {
128
+ try {
129
+ const nextCanvasElement = value?.elements?.[0] ?? value;
130
+ if (!nextCanvasElement) return;
131
+
132
+ const result = renderCanvasElement(nextCanvasElement);
133
+
134
+ if (!resolved) {
135
+ resolved = true;
136
+ Promise.resolve().then(() => {
137
+ resolve({
138
+ ...result,
139
+ hmrSubscription
140
+ });
141
+ });
142
+ }
143
+ } catch (error) {
144
+ reject(error);
145
+ }
146
+ },
147
+ error: reject
148
+ });
149
+ });
150
+ }
151
+
152
+ return renderCanvasElement(await canvasElement);
109
153
  };
@@ -294,7 +294,7 @@ function handleAnimatedSignalsFreeze(element: Element, shouldPause: boolean) {
294
294
  Object.values(element.propObservables).forEach(processValue);
295
295
  }
296
296
 
297
- function destroyElement(element: Element | Element[]) {
297
+ export function destroyElement(element: Element | Element[]) {
298
298
  if (Array.isArray(element)) {
299
299
  element.forEach((e) => destroyElement(e));
300
300
  return;
@@ -697,6 +697,19 @@ export function createComponent(tag: string, props?: Props): Element {
697
697
 
698
698
  if (child instanceof Observable) {
699
699
  const mountedFlowElements = childGroup.mounted;
700
+ const flowEffectSubscriptions = ((child as any).effectSubscriptions ?? []) as Subscription[];
701
+ const flowEffectMounts = ((child as any).effectMounts ?? []) as Array<(element: Element) => any>;
702
+
703
+ const applyFlowEffects = (element: Element) => {
704
+ if (!flowEffectMounts.length) {
705
+ return;
706
+ }
707
+
708
+ element.effectMounts = [
709
+ ...flowEffectMounts,
710
+ ...(element.effectMounts ?? []),
711
+ ];
712
+ };
700
713
 
701
714
  const createFragmentOwner = (): Element => ({
702
715
  tag: 'fragment',
@@ -724,6 +737,7 @@ export function createComponent(tag: string, props?: Props): Element {
724
737
  }
725
738
 
726
739
  const routed = routeDomComponent(parent, element);
740
+ applyFlowEffects(routed);
727
741
  mountedFlowElements.set(element, routed);
728
742
  onMount(parent, routed, getInsertIndex(sourceIndex, orderedSources));
729
743
  propagateContext(routed);
@@ -801,6 +815,7 @@ export function createComponent(tag: string, props?: Props): Element {
801
815
  } else if (isElement(value)) {
802
816
  // Handle direct Element emission
803
817
  const routed = routeDomComponent(parent, value);
818
+ applyFlowEffects(routed);
804
819
  childGroup.mounted.set(value, routed);
805
820
  onMount(parent, routed, getInsertIndex(0, [value]));
806
821
  propagateContext(routed);
@@ -821,6 +836,7 @@ export function createComponent(tag: string, props?: Props): Element {
821
836
  destroyElement(mounted);
822
837
  });
823
838
  mountedFlowElements.clear();
839
+ flowEffectSubscriptions.forEach((sub) => sub.unsubscribe());
824
840
  });
825
841
 
826
842
  // Store subscription for cleanup