Digital inking on the web has historically suffered from input-to-display latency. When a user draws on a <canvas> element using a stylus or finger, the input pipeline passes through several processing layers: hardware digitizers fire interrupts, the operating system handles the incoming device events, the browser process receives them, and those events are queued onto the JavaScript main thread. By the time your pointermove handler executes, updates the 2D or WebGL context, and the browser schedules paint and composite steps, multiple display frames may have passed. This delay creates a visible gap where stroke trails drag behind the physical stylus tip, degrading the user experience for digital signatures, note-taking, and whiteboard tools.
The Ink API tackles this issue by bypassing the JavaScript event loop for immediate stroke feedback. Instead of waiting on the browser’s standard rendering pipeline, the API allows web applications to delegate transient stroke rendering directly to the underlying operating system’s low-latency compositor. Calling navigator.ink.requestPresenter() returns a DelegatedInkTrailPresenter object that renders hardware-accelerated ink trails between pointer events. Because these trails are temporary and automatically cleared as your canvas strokes commit to the screen, you retain complete authority over your document state and canvas data while delivering native-feeling input responsiveness.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ink API Demo</title>
<link href="style.css" rel="stylesheet" />
<script src="ink_API.js" defer></script>
</head>
<body>
<!-- HTML: Canvas container for inking demo -->
<canvas id="ink-canvas" width="800" height="500"></canvas>
</body>
</html>
/* CSS: Prevent default browser touch interactions from canceling drawing */
#ink-canvas {
touch-action: none;
/* Required to suppress default browser scroll and pinch gestures */
background-color: #ffffff;
border: 1px solid #cbd5e1;
border-radius: 4px;
cursor: crosshair;
display: block;
}
// FULLY COMMENTED CODE DEMO
window.addEventListener('DOMContentLoaded', async () => {
const canvas = document.querySelector('#ink-canvas');
if (!canvas) {
console.error('Canvas element not found.');
return;
}
const ctx = canvas.getContext('2d');
if (!ctx) {
console.error('2D context is not supported on this device.');
return;
}
// Define consistent stroke styling shared between the canvas and the Ink API
const strokeStyle = {
color: '#0f172a', // Slate 900
diameter: 4 // Pixel width of the stroke
};
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = strokeStyle.diameter;
ctx.strokeStyle = strokeStyle.color;
// Track active drawing stroke state
let isDrawing = false;
let lastX = 0;
let lastY = 0;
// Hold reference to the DelegatedInkTrailPresenter instance
let inkPresenter = null;
// Feature detection: Check for Ink API support before requesting a presenter
if (
'ink' in navigator &&
typeof navigator.ink.requestPresenter === 'function'
) {
try {
// Confine ink trail rendering to the canvas bounding box
inkPresenter = await navigator.ink.requestPresenter({
presentationArea: canvas
});
} catch (error) {
// Fallback: If presenter acquisition fails, standard canvas drawing continues seamlessly
console.warn(
'Ink API initialization failed; continuing with standard rendering.',
error
);
inkPresenter = null;
}
} else {
// Informational fallback for unsupported engines (Firefox, Safari, older Chrome versions)
console.info(
'Ink API is not supported in this browser environment. Using standard canvas pipeline.'
);
}
// Pointer Down: Initialize path tracking
canvas.addEventListener('pointerdown', (event) => {
// Restrict drawing to primary button interactions (stylus tip, touch, left click)
if (event.button !== 0) return;
isDrawing = true;
lastX = event.offsetX;
lastY = event.offsetY;
// Retain input focus even if the pointer moves outside the canvas boundary
canvas.setPointerCapture(event.pointerId);
});
// Pointer Move: Render persistent stroke and trigger predicted compositor trail
canvas.addEventListener('pointermove', (event) => {
if (!isDrawing) return;
const currentX = event.offsetX;
const currentY = event.offsetY;
// 1. Draw the permanent segment to the 2D canvas context
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.lineTo(currentX, currentY);
ctx.stroke();
lastX = currentX;
lastY = currentY;
// 2. Delegate low-latency trail rendering to the OS compositor
// updateInkTrailStartPoint requires a trusted PointerEvent
if (inkPresenter && event.isTrusted) {
inkPresenter.updateInkTrailStartPoint(event, {
color: strokeStyle.color,
diameter: strokeStyle.diameter
});
}
});
// Shared cleanup logic for pointer release and boundary interruptions
const stopDrawing = (event) => {
if (!isDrawing) return;
isDrawing = false;
if (canvas.hasPointerCapture(event.pointerId)) {
canvas.releasePointerCapture(event.pointerId);
}
};
// Pointer Up & Cancel: Teardown pointer state
canvas.addEventListener('pointerup', stopDrawing);
canvas.addEventListener('pointercancel', stopDrawing);
});





