Kokkoja: first public assembly
Assembled from the development repository by tools/make_release.sh. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
95c80d04c9
39 files changed
+9905
No files matched your search
@@ -0,0 +1,170 @@
|
||||
// @ts-check
|
||||
/**
|
||||
* Turns pointer events (pen, mouse or finger) on the page into strokes and
|
||||
* erasures.
|
||||
*/
|
||||
|
||||
import { rounded } from './page_model.js';
|
||||
|
||||
/** Bit in PointerEvent.buttons for the eraser end of a pen. */
|
||||
const ERASER_BUTTON = 32;
|
||||
|
||||
/**
|
||||
* Pressure recorded for a mouse or a finger, which report none. It is the
|
||||
* pressure at which a stroke is drawn at its nominal width.
|
||||
*/
|
||||
const NOMINAL_PRESSURE = 0.4;
|
||||
|
||||
/**
|
||||
* @typedef {object} PenTarget
|
||||
* @property {(clientX: number, clientY: number) => [number, number]} toPage
|
||||
* Convert a window position to page units.
|
||||
* @property {() => 'pen' | 'eraser' | 'text'} tool The tool selected in the toolbar.
|
||||
* @property {() => boolean} canWrite False while there is no page to write on.
|
||||
* @property {() => void} beginStroke
|
||||
* @property {(points: number[]) => void} addPoints
|
||||
* @property {() => void} endStroke
|
||||
* @property {(x0: number, y0: number, x1: number, y1: number) => void} eraseAlong
|
||||
* @property {(x: number, y: number, reach: number) => void} [eraseEnded]
|
||||
* The eraser went down at (x, y) and has come up again, having moved
|
||||
* no farther than `reach` from there.
|
||||
* @property {(x0: number, y0: number, x1: number, y1: number) => void} placeText
|
||||
* With the text tool, the pointer went down at (x0, y0) and up at (x1, y1).
|
||||
* @property {(x0: number, y0: number, x1: number) => void} [previewText]
|
||||
* With the text tool, the pointer is down at (x0, y0) and now at x1.
|
||||
|
||||
* @property {(report: PenReport) => void} [onReport]
|
||||
*/
|
||||
|
||||
/**
|
||||
* What the input device is delivering, for judging pen support.
|
||||
* @typedef {object} PenReport
|
||||
* @property {string} pointerType 'pen', 'mouse' or 'touch'.
|
||||
* @property {number} pressure Latest pressure.
|
||||
* @property {number} pointsPerSecond
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {HTMLElement} surface The element that receives the pointer.
|
||||
* @param {PenTarget} target
|
||||
*/
|
||||
export function attachPenInput(surface, target) {
|
||||
/** @type {number | null} */
|
||||
let activePointer = null;
|
||||
let erasing = false;
|
||||
let placingText = false;
|
||||
let textStartX = 0;
|
||||
let textStartY = 0;
|
||||
let eraseStartX = 0;
|
||||
let eraseStartY = 0;
|
||||
let eraseReach = 0;
|
||||
let penSeen = false;
|
||||
let lastX = NaN;
|
||||
let lastY = NaN;
|
||||
let strokeStart = 0;
|
||||
let strokePoints = 0;
|
||||
|
||||
/**
|
||||
* @param {PointerEvent} event
|
||||
* @returns {PointerEvent[]} Every position the pointer reported since
|
||||
* the previous event, not only the latest.
|
||||
*/
|
||||
function allEvents(event) {
|
||||
const coalesced = event.getCoalescedEvents ? event.getCoalescedEvents() : [];
|
||||
return coalesced.length ? coalesced : [event];
|
||||
}
|
||||
|
||||
/** @param {PointerEvent} event */
|
||||
function draw(event) {
|
||||
/** @type {number[]} */
|
||||
const points = [];
|
||||
let pressure = 0;
|
||||
for (const e of allEvents(event)) {
|
||||
const [x, y] = target.toPage(e.clientX, e.clientY);
|
||||
const px = rounded(x, 10);
|
||||
const py = rounded(y, 10);
|
||||
if (px === lastX && py === lastY) continue;
|
||||
pressure = e.pointerType === 'pen' ? rounded(e.pressure, 1000) : NOMINAL_PRESSURE;
|
||||
points.push(px, py, pressure);
|
||||
lastX = px;
|
||||
lastY = py;
|
||||
}
|
||||
if (points.length === 0) return;
|
||||
target.addPoints(points);
|
||||
strokePoints += points.length / 3;
|
||||
const seconds = (performance.now() - strokeStart) / 1000;
|
||||
target.onReport?.({
|
||||
pointerType: event.pointerType, pressure,
|
||||
pointsPerSecond: seconds > 0.2 ? Math.round(strokePoints / seconds) : 0,
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {PointerEvent} event */
|
||||
function erase(event) {
|
||||
for (const e of allEvents(event)) {
|
||||
const [x, y] = target.toPage(e.clientX, e.clientY);
|
||||
target.eraseAlong(Number.isNaN(lastX) ? x : lastX, Number.isNaN(lastY) ? y : lastY, x, y);
|
||||
eraseReach = Math.max(eraseReach, Math.hypot(x - eraseStartX, y - eraseStartY));
|
||||
lastX = x;
|
||||
lastY = y;
|
||||
}
|
||||
}
|
||||
|
||||
surface.addEventListener('pointerdown', (event) => {
|
||||
if (activePointer !== null || !target.canWrite() || event.target !== surface) return;
|
||||
if (event.pointerType === 'pen') penSeen = true;
|
||||
// Once a pen has been used, a touch is taken to be the resting hand.
|
||||
if (event.pointerType === 'touch' && penSeen) return;
|
||||
if (event.pointerType === 'mouse' && event.button !== 0) return;
|
||||
event.preventDefault();
|
||||
surface.setPointerCapture(event.pointerId);
|
||||
activePointer = event.pointerId;
|
||||
const tool = target.tool();
|
||||
erasing = tool === 'eraser' || (event.buttons & ERASER_BUTTON) !== 0;
|
||||
placingText = tool === 'text' && !erasing;
|
||||
lastX = NaN;
|
||||
lastY = NaN;
|
||||
strokeStart = performance.now();
|
||||
strokePoints = 0;
|
||||
if (placingText) {
|
||||
[textStartX, textStartY] = target.toPage(event.clientX, event.clientY);
|
||||
} else if (erasing) {
|
||||
[eraseStartX, eraseStartY] = target.toPage(event.clientX, event.clientY);
|
||||
eraseReach = 0;
|
||||
erase(event);
|
||||
} else {
|
||||
target.beginStroke();
|
||||
draw(event);
|
||||
}
|
||||
});
|
||||
|
||||
surface.addEventListener('pointermove', (event) => {
|
||||
if (event.pointerId !== activePointer) return;
|
||||
if (placingText) {
|
||||
const [x] = target.toPage(event.clientX, event.clientY);
|
||||
target.previewText?.(textStartX, textStartY, x);
|
||||
return;
|
||||
}
|
||||
if (erasing) erase(event);
|
||||
else draw(event);
|
||||
});
|
||||
|
||||
/** @param {PointerEvent} event */
|
||||
function finish(event) {
|
||||
if (event.pointerId !== activePointer) return;
|
||||
activePointer = null;
|
||||
if (placingText) {
|
||||
const [x, y] = target.toPage(event.clientX, event.clientY);
|
||||
if (event.type === 'pointerup') target.placeText(textStartX, textStartY, x, y);
|
||||
} else if (erasing) {
|
||||
if (event.type === 'pointerup') target.eraseEnded?.(eraseStartX, eraseStartY, eraseReach);
|
||||
} else {
|
||||
target.endStroke();
|
||||
}
|
||||
}
|
||||
surface.addEventListener('pointerup', finish);
|
||||
surface.addEventListener('pointercancel', finish);
|
||||
|
||||
// The side button of a pen asks for a context menu.
|
||||
surface.addEventListener('contextmenu', (event) => event.preventDefault());
|
||||
}
|
||||
Reference in new issue
Block a user