Skip to content

Commit

Permalink
Merge pull request #15 from dnass/layer-events
Browse files Browse the repository at this point in the history
Add layer hit detection and event handling
  • Loading branch information
dnass authored Feb 4, 2023
2 parents 3497d91 + 853ec67 commit 6f1ba87
Show file tree
Hide file tree
Showing 14 changed files with 533 additions and 144 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.9.0

- Add layer-level hit detection and event handling.

## 0.8.1

- Remove SvelteKit `browser` check.
Expand Down
51 changes: 37 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Reactive canvas rendering with Svelte.
</Canvas>
```

If you use typescript, add the Render type to your reactive statement:
If you use TypeScript, add the Render type to your reactive statement:

```ts
import { ..., type Render } from "svelte-canvas";
Expand All @@ -50,16 +50,17 @@ More examples:

`Canvas` is the top-level element. It's a Svelte wrapper around an [HTML `<canvas>` element](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/canvas).

#### Parameters
#### Props

| parameter | default | description |
| ------------ | ------------------------- | ------------------------------------------------------------------------------------------------------- |
| `width` | 640 | Canvas width |
| `height` | 640 | Canvas height |
| `pixelRatio` | `window.devicePixelRatio` | Canvas [pixel ratio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#Examples) |
| `style` | `null` | A CSS style string which will be applied to the canvas element |
| `class` | `null` | A class string which will be applied to the canvas element |
| `autoclear` | `true` | If `true`, will use `context.clearRect` to clear the canvas at the start of each render cycle |
| prop | default | description |
| ------------- | ------------------------- | ------------------------------------------------------------------------------------------------------- |
| `width` | 640 | Canvas width |
| `height` | 640 | Canvas height |
| `pixelRatio` | `window.devicePixelRatio` | Canvas [pixel ratio](https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#Examples) |
| `style` | `null` | A CSS style string which will be applied to the canvas element |
| `class` | `null` | A class string which will be applied to the canvas element |
| `autoclear` | `true` | If `true`, will use `context.clearRect` to clear the canvas at the start of each render cycle |
| `layerEvents` | `false` | If `true`, enables event handlers on child `Layer` components |

#### Methods

Expand All @@ -69,10 +70,6 @@ More examples:
| `getContext` | Returns the canvas's 2D rendering context |
| `redraw` | Forces a re-render of the canvas |

#### Events

All DOM events on the `<canvas>` element are forwarded to the `Canvas` component, so [handling an event](https://svelte.dev/docs#Element_directives) is as simple as `<Canvas on:click={handleClick}>`.

### Layer

`Layer` is a layer to be rendered onto the canvas. It takes two props, `setup` and `render` Both take functions with a single argument that receives an object with the properties `context`, `width`, and `height`. `context` is the [2D rendering context](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D) of the parent canvas. `width` and `height` are the canvas's dimensions.
Expand All @@ -81,6 +78,32 @@ All DOM events on the `<canvas>` element are forwarded to the `Canvas` component

Declaring your `render` function [reactively](https://svelte.dev/docs#3_$_marks_a_statement_as_reactive) allows `svelte-canvas` to re-render anytime the values that the function depends on change.

### Event handling

All DOM events on the `<canvas>` element are forwarded to the `Canvas` component, so [handling an event](https://svelte.dev/docs#Element_directives) is as simple as `<Canvas on:click={handleClick}>`.

Individual `Layer` instances can also handle events that fall within their bounds when the `layerEvents` prop on the parent `Canvas` component is `true`. Event handlers registered with the `on:` directive receive a `CustomEvent` with properties `detail.x` and `detail.y` representing the coordinates of the event relative to the parent canvas, as well as `detail.originalEvent`, which contains the original `canvas` DOM event.

```ts
<script>
import { Canvas, Layer, type CanvasLayerEvent } from 'svelte-canvas';

const render = ({ context }) => {
...
};

const handleClick = (e: CanvasLayerEvent) => console.log(e.detail.x, e.detail.y)
</script>

<Canvas width={640} height={640}>
<Layer {render} on:click={handleClick} />
</Canvas>
```

Supported event types are `'click' | 'contextmenu' | 'dblclick' | 'mousedown' | 'mouseenter' | 'mouseleave' | 'mousemove' | 'mouseup' | 'wheel' | 'touchcancel' | 'touchend' | 'touchmove' | 'touchstart' | 'pointerenter' | 'pointerleave' | 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel'`

This is an experimental feature that may have a negative performance impact.

### t

`t` is a [readable store](https://svelte.dev/docs#readable) that provides the time in milliseconds since initialization. Subscribing to `t` within your render function lets you easily create animations.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "svelte-canvas",
"version": "0.8.1",
"version": "0.9.0",
"scripts": {
"dev": "vite dev",
"build": "svelte-kit sync && svelte-package",
Expand Down
76 changes: 62 additions & 14 deletions src/lib/components/Canvas.svelte
Original file line number Diff line number Diff line change
@@ -1,39 +1,40 @@
<script context="module" lang="ts">
import RenderManager from '../util/renderManager';
import LayerManager from '../util/LayerManager';
import { getContext as getCTX } from 'svelte';
export const KEY = Symbol();
interface TypedContext {
register: RenderManager['register'];
unregister: RenderManager['unregister'];
redraw: RenderManager['redraw'];
register: LayerManager['register'];
unregister: LayerManager['unregister'];
redraw: LayerManager['redraw'];
}
export const getTypedContext = (): TypedContext => getCTX(KEY);
</script>

<script lang="ts">
import { createContextProxy, type ContextProxy } from '../util/contextProxy';
import { onMount, onDestroy, setContext } from 'svelte';
export let width = 640,
height = 640,
pixelRatio: number | null = null,
style = '',
autoclear = true;
autoclear = true,
layerEvents = false;
/** Class field. Only works for global classes. */
let clazz = '';
export { clazz as class, redraw, getCanvas, getContext };
let canvas: HTMLCanvasElement;
let context: CanvasRenderingContext2D | null = null;
let context: CanvasRenderingContext2D | ContextProxy | null = null;
let animationLoop: number;
let layerRef: HTMLDivElement;
let layerObserver: MutationObserver;
const manager = new RenderManager();
const manager = new LayerManager();
function redraw() {
manager.redraw();
Expand All @@ -57,7 +58,7 @@
function draw() {
manager.render({
context: context!,
context: <CanvasRenderingContext2D>context!,
width,
height,
pixelRatio: pixelRatio!,
Expand All @@ -73,7 +74,14 @@
});
onMount(() => {
context = canvas.getContext('2d')!;
const ctx = canvas.getContext('2d')!;
if (layerEvents) {
context = createContextProxy(ctx);
context._renderingLayerId = manager.getRenderingLayerId;
} else {
context = ctx;
}
layerObserver = new MutationObserver(getLayerSequence);
layerObserver.observe(layerRef, { childList: true });
Expand All @@ -97,10 +105,49 @@
layerObserver.disconnect();
});
const handleLayerMouseMove = (e: MouseEvent) => {
const { offsetX: x, offsetY: y } = e;
const id = (<ContextProxy>context)._getLayerIdAtPixel(x, y);
manager.setActiveLayer(id, e);
manager.dispatchLayerEvent(e);
};
const handleLayerTouchStart = (e: TouchEvent) => {
const { clientX: x, clientY: y } = e.changedTouches[0];
const { left, top } = canvas.getBoundingClientRect();
const id = (<ContextProxy>context)._getLayerIdAtPixel(x - left, y - top);
manager.setActiveLayer(id, e);
manager.dispatchLayerEvent(e);
};
const handleLayerEvent = (e: MouseEvent | TouchEvent) => {
if (window.TouchEvent && e instanceof TouchEvent) e.preventDefault();
manager.dispatchLayerEvent(e);
};
$: width, height, pixelRatio, autoclear, manager.resize();
</script>

<canvas
on:touchstart|preventDefault={layerEvents ? handleLayerTouchStart : null}
on:mousemove={layerEvents ? handleLayerMouseMove : null}
on:pointermove={layerEvents ? handleLayerMouseMove : null}
on:click={layerEvents ? handleLayerEvent : null}
on:contextmenu={layerEvents ? handleLayerEvent : null}
on:dblclick={layerEvents ? handleLayerEvent : null}
on:mousedown={layerEvents ? handleLayerEvent : null}
on:mouseenter={layerEvents ? handleLayerEvent : null}
on:mouseleave={layerEvents ? handleLayerEvent : null}
on:mouseup={layerEvents ? handleLayerEvent : null}
on:wheel={layerEvents ? handleLayerEvent : null}
on:touchcancel|preventDefault={layerEvents ? handleLayerEvent : null}
on:touchend|preventDefault={layerEvents ? handleLayerEvent : null}
on:touchmove|preventDefault={layerEvents ? handleLayerEvent : null}
on:pointerenter={layerEvents ? handleLayerEvent : null}
on:pointerleave={layerEvents ? handleLayerEvent : null}
on:pointerdown={layerEvents ? handleLayerEvent : null}
on:pointerup={layerEvents ? handleLayerEvent : null}
on:pointercancel={layerEvents ? handleLayerEvent : null}
on:focus
on:blur
on:fullscreenchange
Expand Down Expand Up @@ -146,15 +193,16 @@
on:pointerleave
on:gotpointercapture
on:lostpointercapture
style="display: block; width: {width}px; height: {height}px;{style
? ` ${style}`
: ''}"
width={width * (pixelRatio ?? 1)}
height={height * (pixelRatio ?? 1)}
class={clazz}
style:display="block"
style:width="{width}px"
style:height="{height}px"
{style}
bind:this={canvas}
/>

<div style="display: none;" bind:this={layerRef}>
<div style:display="none" bind:this={layerRef}>
<slot />
</div>
7 changes: 5 additions & 2 deletions src/lib/components/Layer.svelte
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { onDestroy, createEventDispatcher } from 'svelte';
import { getTypedContext } from './Canvas.svelte';
import type { Render } from './render';
import type { LayerEvents } from './layerEvent';
const { register, unregister, redraw } = getTypedContext();
const dispatcher = createEventDispatcher<LayerEvents>();
export let setup: Render | undefined = undefined;
export let render: Render = () => undefined;
const layerId = register({ setup, render });
const layerId = register({ setup, render, dispatcher });
onDestroy(() => unregister(layerId));
Expand Down
38 changes: 38 additions & 0 deletions src/lib/components/layerEvent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { createEventDispatcher } from 'svelte';

export type Events =
| 'click'
| 'contextmenu'
| 'dblclick'
| 'mousedown'
| 'mouseenter'
| 'mouseleave'
| 'mousemove'
| 'mouseup'
| 'wheel'
| 'touchcancel'
| 'touchend'
| 'touchmove'
| 'touchstart'
| 'pointerenter'
| 'pointerleave'
| 'pointerdown'
| 'pointermove'
| 'pointerup'
| 'pointercancel';

export type LayerEventDetail = {
x: number;
y: number;
originalEvent: MouseEvent | TouchEvent;
};

export type LayerEvents = {
[E in Events]: LayerEventDetail;
};

export type CanvasLayerEvent = CustomEvent<LayerEventDetail>;

export type LayerEventDispatcher = ReturnType<
typeof createEventDispatcher<LayerEvents>
>;
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { default as Canvas } from './components/Canvas.svelte';
export { default as Layer } from './components/Layer.svelte';
export { timer as t } from './components/timer';
export type { Render } from './components/render';
export type { CanvasLayerEvent } from './components/layerEvent';
Loading

0 comments on commit 6f1ba87

Please sign in to comment.