- Apple Pencil hover cursor: previews brush size/shape before contact - Tilt shading: tilted pen strokes widen and reduce thinning via tiltX/Y - Two-finger undo in draw mode (non-stylus two-finger tap) - Copy & open Claude buttons: copies study/podcast prompt then opens claude.ai/new - Inline ink canvas in study notes (DrawCanvas embedded in Notes tab) - Full-text search across all project notes with 300ms debounce - Reading plan: set a book + week goal, mark chapters read from the reader Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
67 lines
2.3 KiB
JavaScript
67 lines
2.3 KiB
JavaScript
import { getStroke } from 'perfect-freehand';
|
|
|
|
export function pathFromStroke(points) {
|
|
if (!points.length) return '';
|
|
const d = [`M ${points[0][0].toFixed(2)} ${points[0][1].toFixed(2)}`];
|
|
for (let i = 1; i < points.length - 1; i++) {
|
|
const mx = ((points[i][0] + points[i + 1][0]) / 2).toFixed(2);
|
|
const my = ((points[i][1] + points[i + 1][1]) / 2).toFixed(2);
|
|
d.push(`Q ${points[i][0].toFixed(2)} ${points[i][1].toFixed(2)} ${mx} ${my}`);
|
|
}
|
|
d.push('Z');
|
|
return d.join(' ');
|
|
}
|
|
|
|
// Renders saved strokes + optional active stroke onto a canvas element.
|
|
// Coordinates are stored as fractions [0,1] of the draw canvas dimensions.
|
|
// size is stored as a fraction of canvas height (e.g. 0.012 ≈ 8px on a 600px canvas).
|
|
export function renderInkToCanvas(
|
|
canvas,
|
|
strokes,
|
|
activeStroke = [],
|
|
activeTool = 'pen',
|
|
activeColor = '#0f172a',
|
|
activeSize = 0.012,
|
|
) {
|
|
const dpr = window.devicePixelRatio || 1;
|
|
const w = canvas.clientWidth;
|
|
const h = canvas.clientHeight;
|
|
if (!w || !h) return;
|
|
|
|
canvas.width = Math.round(w * dpr);
|
|
canvas.height = Math.round(h * dpr);
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.scale(dpr, dpr);
|
|
|
|
function strokeOpts(tool, baseSize, points) {
|
|
const avgTilt = points.length
|
|
? points.reduce((s, p) => s + (p[3] ?? 0), 0) / points.length
|
|
: 0;
|
|
return {
|
|
// Tilt widens the stroke and reduces pressure-thinning (pencil-shading feel)
|
|
size: tool === 'pen' ? baseSize * (1 + avgTilt * 2.5) : baseSize,
|
|
thinning: tool === 'highlighter' ? 0 : 0.5 * (1 - (tool === 'pen' ? avgTilt * 0.8 : 0)),
|
|
smoothing: 0.5,
|
|
streamline: 0.4,
|
|
};
|
|
}
|
|
|
|
for (const s of strokes) {
|
|
const pts = s.points.map(([x, y, p]) => [x * w, y * h, p]);
|
|
const outline = getStroke(pts, strokeOpts(s.tool, s.size * h, s.points));
|
|
ctx.globalAlpha = s.tool === 'highlighter' ? 0.35 : 1;
|
|
ctx.fillStyle = s.color;
|
|
ctx.fill(new Path2D(pathFromStroke(outline)));
|
|
}
|
|
|
|
if (activeStroke.length > 1 && activeTool !== 'eraser') {
|
|
const pts = activeStroke.map(([x, y, p]) => [x * w, y * h, p]);
|
|
const outline = getStroke(pts, strokeOpts(activeTool, activeSize * h, activeStroke));
|
|
ctx.globalAlpha = activeTool === 'highlighter' ? 0.35 : 1;
|
|
ctx.fillStyle = activeColor;
|
|
ctx.fill(new Path2D(pathFromStroke(outline)));
|
|
}
|
|
|
|
ctx.globalAlpha = 1;
|
|
}
|