UNPKG

svelte-canvas

Version:

Reactive canvas rendering with Svelte.

138 lines (137 loc) 4.97 kB
import { onDestroy, setContext, untrack } from 'svelte'; import { SvelteMap } from 'svelte/reactivity'; import { dispatchLayerEvent, getEventCoords, SUPPORTED_EVENTS } from './events'; import { warn } from './console'; import { REGISTER_KEY } from './registerLayer'; class LayerManager { #setups = new SvelteMap(); #renderers = new SvelteMap(); #eventHandlers = new SvelteMap(); #currentLayerId = 1; #startTime = Date.now(); #frame = $state(0); #context; #config; #autoplayLoop; #layerObserver; #layerSequence = $state([]); #activeLayerId = $state(0); #activeLayerEventHandlers = $derived(this.#eventHandlers.get(this.#activeLayerId)); constructor(config) { this.#config = config; setContext(REGISTER_KEY, this.register.bind(this)); } init(context, layerRef) { this.#context = context; this.#observeLayerSequence(layerRef); $effect(() => this.#autoplay()); $effect(() => this.#handleResize()); $effect(() => (this.#frame, this.#render())); onDestroy(() => this.#destroy()); } redraw() { this.#frame++; } register({ setup, render, ...eventHandlers }) { const layerId = this.#getLayerId(); if (setup) { this.#setups.set(layerId, setup); } this.#renderers.set(layerId, render); if (Object.keys(eventHandlers).length) { if (this.#config.layerEvents) { this.#eventHandlers.set(layerId, eventHandlers); } else { warn('Canvas must have layerEvents={true} in order to use layer-level event handlers'); } } onDestroy(() => this.#unregister(layerId)); return layerId; } #getLayerId() { return this.#currentLayerId++; } #unregister(layerId) { this.#renderers.delete(layerId); this.#eventHandlers.delete(layerId); } #handleResize() { const { onresize, width, height, pixelRatio } = this.#config; onresize?.({ width, height, pixelRatio }); } #observeLayerSequence(layerRef) { const getLayerSequence = () => { const layers = [...layerRef.children]; this.#layerSequence = layers.map((layer) => +layer.dataset.layerId); }; this.#layerObserver = new MutationObserver(() => getLayerSequence()); this.#layerObserver.observe(layerRef, { childList: true }); getLayerSequence(); } #autoplay() { if (this.#config.autoplay) { untrack(() => this.redraw()); this.#autoplayLoop = requestAnimationFrame(() => this.#autoplay()); } else { cancelAnimationFrame(this.#autoplayLoop); } } #render() { const context = this.#context; const { width, height, pixelRatio, autoclear } = this.#config; const time = Date.now() - this.#startTime; const renderProps = { context, width, height, time }; context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0); if (autoclear) { context.clearRect(0, 0, width, height); } for (const [layerId, setup] of this.#setups) { setup(renderProps); this.#setups.delete(layerId); } for (const layerId of this.#layerSequence) { this.#context.setCurrentLayerId?.(layerId); this.#renderers.get(layerId)?.(renderProps); } } createEventHandlers() { const handleCanvasEvent = (e) => { const type = `on${e.type}`; const handler = this.#config.handlers[type]; handler?.(e); }; const handleLayerEvent = (e) => { const { pixelRatio } = this.#config; const { x, y } = getEventCoords(e); if (['touchstart', 'pointermove'].includes(e.type)) { const id = this.#context.getLayerIdAt(x * pixelRatio, y * pixelRatio); if (this.#activeLayerId !== id) { dispatchLayerEvent(e, 'leave'); this.#activeLayerId = id; dispatchLayerEvent(e, 'enter'); } } if (!this.#activeLayerEventHandlers) return; const type = `on${e.type.replace('layer.', '')}`; const handler = this.#activeLayerEventHandlers[type]; handler?.({ x, y, originalEvent: e }); }; const handleEvent = (e) => { handleCanvasEvent(e); if (this.#config.layerEvents) { handleLayerEvent(e); } }; return SUPPORTED_EVENTS.reduce((acc, type) => ({ ...acc, [type]: handleEvent }), {}); } #destroy() { if (typeof window === 'undefined') return; this.#layerObserver?.disconnect(); cancelAnimationFrame(this.#autoplayLoop); } } export default LayerManager;