PointerEventと<canvas>と<dialog>とPreactでお絵かきUserScript
使うもの
Preact
DOM構築を楽にする
PointerEvent
筆圧検知もいれたい
<canvas>
UIを中央にあわせるのがめんどいので、これを使う
使い分けがわからない
code:script.ts
import "./main.tsx";
code:main.tsx
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
/** @jsx h */
import { h, render } from "../preact/mod.tsx";
import { App } from "./App.tsx";
const app = document.createElement("div");
const shadowRoot = app.attachShadow({ mode: "open" });
document.body.append(app);
render(<App />, shadowRoot);
code:App.tsx
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
/** @jsx h */
/** @jsxFrag Fragment */
import { h, Fragment } from "../preact/mod.tsx";
import { useState, useEffect, useCallback, useMemo, useRef } from "../preact/hooks.tsx";
import { useDialog } from "../JSXと%3Cdialog%3Eで組んだmodal_window/useDialog.ts";
import { getContext } from "./getContext.ts";
import { colorForTouch } from "./colorForTouch.ts";
import { relative, PointerPos } from "./relative.ts";
export const App: FunctionComponent<{}> = () => {
const { ref, open, close } = useDialog();
const stopPropagation = useCallback((e: Event) => e.stopPropagation(), []);
const pointers = useRef<Map<number, PointerPos>>(new Map());
const handleDown = useCallback(
(event: PointerEvent) => {
const canvas = event.currentTarget;
const ctx = getContext(canvas);
const id = event.pointerId;
// <canvs>の外に出ても、ボタンを離したりdeviceを取り外したりしない限り描画を継続させる
canvas.setPointerCapture(id);
// draw a circle at the start
const start = relative(event);
ctx.beginPath();
ctx.arc(start.x, start.y, 4, 0, 2 * Math.PI, false); // a circle at the start
const color = colorForTouch(id);
ctx.fillStyle = color;
ctx.fill();
pointers.current.set(id, start);
},
[]
);
const handleMove = useCallback(
(event: PointerEvent) => {
const prev = pointers.current.get(event.pointerId);
if (!prev) return;
const ctx = getContext(event.currentTarget);
// draw a path
ctx.beginPath();
ctx.moveTo(prev.x, prev.y);
const next = relative(event);
ctx.lineTo(next.x, next.y);
ctx.lineWidth = 4;
ctx.strokeStyle = color;
ctx.stroke();
pointers.current.set(event.pointerId, next);
},
[]
);
const handleUp = useCallback(
(event: PointerEvent) => {
const prev = pointers.current.get(event.pointerId);
if (!prev) return;
pointers.current.delete(event.pointerId);
if (event.type !== "pointerup") {
canvas.releasePointerCapture(id);
return;
}
// and a square at the end
const ctx = getContext(event.currentTarget);
ctx.lineWidth = 4;
ctx.fillStyle = color;
ctx.beginPath();
const end = relative(event);
ctx.moveTo(prev.x, prev.y);
ctx.lineTo(end.x, end.y);
ctx.fillRect(end.x - 4, end.y - 4, 8, 8);
},
[]
);
return (<>
<style>{`
dialog {
&::backdrop{
background-color:#000c;
}
flex-direction: column;
align-items: center;
row-gap: 10px;
padding: 10px;
background: unset;
margin-top: unset;
margin-bottom: unset;
border: unset;
height: unset;
display: flex;
}
* {
background-color: var(--dropdown-menu-bg, #fff); border: 1px solid rgba(0,0,0,.2);
border-radius: 6px;
}
}
@media (min-width: 768px) {
dialog {
padding: 30px 0;
}
}
`}</style>
<dialog ref={ref} onClick={stopPropagation}>
<canvas
ref={initialize}
width={600}
height={300}
onPointerDown={handleDown}
onPointerMove={handleMove}
onPointerUp={handleUp}
onPointerEnd={handleUp}
onPointerOut={handleUp}
onPointerCancel={handleUp}
/>
</dialog>
<>);
};
code:getContext.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
export const getContext = (canvas: HTMLCanvasElement): CanvasRenderingContext2D => {
const ctx = canvas.getContext("2d");
if (!ctx) throw new Error("2D context is not supported.");
return ctx;
}
code:color.ts
export const colorForTouch = (
pointerId: number,
): #${string} => {
const r = (pointerId % 16).toString(16);
const g = (Math.floor(pointerId / 3) % 16).toString(16);
const b = (Math.floor(pointerId / 7) % 16).toString(16);
return #${r}${g}${b} as const;
};
code:relative.ts
/// <reference no-default-lib="true" />
/// <reference lib="esnext" />
/// <reference lib="dom" />
export interface PointerPos {
x: number;
y: number;
}
export const relative = (event: PointerEvent): PointerPos => {
const rect = event.currentTarget.getBoundingClientRect();
return {
x: Math.round(event.clientX - rect.left),
y: Math.round(event.clientY - rect.top),
};
};