Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 91 additions & 3 deletions src/lib/Toolbar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@
let { onundo, onclear, onexit }: Props = $props();

// --- Draggable toolbar state ---
// Start centered vertically on the right edge of the screen.
// Start centered vertically on the left edge of the screen.
let position = $state<Point>({ x: 0, y: 0 });
let positioned = $state(false);
let dragging = $state(false);
let dragOffset: Point = { x: 0, y: 0 };
let toolbarEl: HTMLElement | null = null;

// --- Auto-fade state ---
let visible = $state(true);
Expand All @@ -34,8 +35,8 @@
// Place toolbar at right-center on first layout.
function initPosition(): void {
if (positioned) return;
const tbW = 52; // approx toolbar width
const tbH = 380; // approx toolbar height
// Measure actual toolbar height after layout; fall back to estimate
const tbH = toolbarEl?.offsetHeight ?? 560;
position = {
x: 24,
y: Math.max(16, Math.round((window.innerHeight - tbH) / 2))
Expand All @@ -46,6 +47,14 @@
// Start auto-fade + initial position on mount
$effect(() => {
initPosition();
// Re-measure after first paint in case layout wasn't ready
requestAnimationFrame(() => {
if (toolbarEl && positioned) {
const tbH = toolbarEl.offsetHeight;
const centeredY = Math.max(16, Math.round((window.innerHeight - tbH) / 2));
position = { ...position, y: centeredY };
}
});
resetFade();
return () => {
if (fadeTimer) clearTimeout(fadeTimer);
Expand Down Expand Up @@ -96,12 +105,25 @@
{ id: 'eraser', icon: '⌫', label: 'Eraser' }
];

const shapes: { id: Tool; icon: string; label: string }[] = [
{ id: 'line', icon: '╱', label: 'Line' },
{ id: 'arrow', icon: '→', label: 'Arrow' },
{ id: 'rectangle', icon: '▭', label: 'Rectangle' },
{ id: 'ellipse', icon: '◯', label: 'Ellipse' }
];

function changeThickness(delta: number): void {
currentThickness.update((t) => Math.min(20, Math.max(2, t + delta)));
resetFade();
}

const colors = ['#ef4444', '#facc15', '#22c55e', '#ffffff'];
</script>

<svelte:window onpointermove={onPointerMove} />

<div
bind:this={toolbarEl}
class="toolbar"
class:visible
class:dragging
Expand Down Expand Up @@ -131,6 +153,50 @@
<!-- Divider -->
<div class="divider"></div>

<!-- Shape tools -->
{#each shapes as shape (shape.id)}
<button
data-btn
class="btn tool-btn"
class:active={$currentTool === shape.id}
onclick={() => selectTool(shape.id)}
aria-label={shape.label}
aria-pressed={$currentTool === shape.id}
title={shape.label}
>
{shape.icon}
</button>
{/each}

<!-- Divider -->
<div class="divider"></div>

<!-- Brush size control -->
<div class="size-control" data-btn>
<button
data-btn
class="btn size-btn"
onclick={() => changeThickness(-2)}
aria-label="Decrease brush size"
title="Thinner (−)"
>
</button>
<span class="size-readout" title="Brush size">{$currentThickness}</span>
<button
data-btn
class="btn size-btn"
onclick={() => changeThickness(2)}
aria-label="Increase brush size"
title="Thicker (+)"
>
+
</button>
</div>

<!-- Divider -->
<div class="divider"></div>

<!-- Color presets -->
{#each colors as c (c)}
<button
Expand Down Expand Up @@ -267,4 +333,26 @@
background: rgba(255, 80, 80, 0.2);
color: #ff6464;
}

.size-control {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}

.size-readout {
font-size: 9px;
color: rgba(255, 255, 255, 0.6);
font-variant-numeric: tabular-nums;
line-height: 1;
min-height: 12px;
pointer-events: none;
}

.size-btn {
font-size: 16px;
font-weight: 600;
line-height: 1;
}
</style>
2 changes: 1 addition & 1 deletion src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type Tool = 'pen' | 'highlighter' | 'eraser';
export type Tool = 'pen' | 'highlighter' | 'eraser' | 'line' | 'arrow' | 'rectangle' | 'ellipse';

export interface Point {
x: number;
Expand Down
93 changes: 76 additions & 17 deletions src/routes/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
let isDrawing = $state(false);
let currentPoints: Point[] = [];
let dpr = 1;
let pdown = $state(0);

// Store subscription values for template reactivity
let drawModeOn = $state(false);
Expand Down Expand Up @@ -48,13 +47,17 @@
}

// ─── Drawing ───
const SHAPE_TOOLS: Tool[] = ['line', 'arrow', 'rectangle', 'ellipse'];

function isShape(t: Tool): boolean {
return SHAPE_TOOLS.includes(t);
}

function getPoint(e: PointerEvent): Point {
return { x: e.clientX, y: e.clientY };
}

function onPointerDown(e: PointerEvent): void {
pdown++;
console.log('[DrawOver] pointerdown', { drawModeOn, button: e.button, type: e.pointerType });
if (!drawModeOn) return;
if (e.button !== 0 && e.pointerType === 'mouse') return;
e.preventDefault();
Expand All @@ -68,8 +71,13 @@
if (!isDrawing) return;
e.preventDefault();

currentPoints.push(getPoint(e));
// Draw incrementally: render the last segment(s) for performance
const pt = getPoint(e);
if (isShape(tool)) {
// Shapes only need start + current point
currentPoints = [currentPoints[0], pt];
} else {
currentPoints.push(pt);
}
renderIncremental();
}

Expand All @@ -81,6 +89,11 @@
canvas.releasePointerCapture?.(e.pointerId);

if (currentPoints.length < 2) {
// Shapes need 2 points; ignore tiny drags
if (isShape(tool)) {
currentPoints = [];
return;
}
// Treat as a dot — create a minimal stroke
const p = currentPoints[0];
currentPoints = [
Expand Down Expand Up @@ -115,7 +128,53 @@
}
}

function drawArrowHead(fromX: number, fromY: number, toX: number, toY: number): void {
const headLen = Math.max(12, ctx.lineWidth * 2.5);
const angle = Math.atan2(toY - fromY, toX - fromX);
ctx.beginPath();
ctx.moveTo(toX, toY);
ctx.lineTo(toX - headLen * Math.cos(angle - Math.PI / 6), toY - headLen * Math.sin(angle - Math.PI / 6));
ctx.moveTo(toX, toY);
ctx.lineTo(toX - headLen * Math.cos(angle + Math.PI / 6), toY - headLen * Math.sin(angle + Math.PI / 6));
ctx.stroke();
}

function drawShape(s: Stroke): void {
if (s.points.length < 2) return;
const a = s.points[0];
const b = s.points[s.points.length - 1];

ctx.beginPath();
if (s.tool === 'line') {
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
} else if (s.tool === 'arrow') {
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
drawArrowHead(a.x, a.y, b.x, b.y);
} else if (s.tool === 'rectangle') {
ctx.strokeRect(a.x, a.y, b.x - a.x, b.y - a.y);
} else if (s.tool === 'ellipse') {
const cx = (a.x + b.x) / 2;
const cy = (a.y + b.y) / 2;
const rx = Math.abs(b.x - a.x) / 2;
const ry = Math.abs(b.y - a.y) / 2;
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
ctx.stroke();
}
}

function drawStroke(s: Stroke): void {
if (isShape(s.tool)) {
ctx.save();
setStrokeStyle(s);
drawShape(s);
ctx.restore();
return;
}

if (s.points.length < 2) return;

ctx.save();
Expand Down Expand Up @@ -176,7 +235,18 @@

const pts = currentPoints;

if (pts.length === 2) {
if (isShape(tool)) {
// Live shape preview from the 2 current points
const previewStroke: Stroke = {
id: 'preview',
points: pts,
color,
width: thickness,
tool,
opacity: 1
};
drawShape(previewStroke);
} else if (pts.length === 2) {
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
ctx.lineTo(pts[1].x, pts[1].y);
Expand Down Expand Up @@ -294,17 +364,6 @@
<svelte:window onresize={onResize} />

<main class:draw-mode={drawModeOn}>
<div class="debug" style="position:fixed;top:50px;left:8px;z-index:99999;font:12px monospace;background:rgba(0,0,0,0.7);color:#0f0;padding:4px 8px;border-radius:4px;pointer-events:none;">
mode:{drawModeOn} tool:{tool} cls:{drawModeOn ? 'CAPTURE' : 'pass'} pdown:{pdown} strokes:{strokes.length}
</div>

<button
onclick={toggleDrawMode}
style="position:fixed;bottom:24px;left:24px;z-index:99999;padding:8px 14px;background:#3b82f6;color:white;border:none;border-radius:8px;font:14px sans-serif;cursor:pointer;"
>
{drawModeOn ? '🔴 STOP Draw' : '✏️ Start Draw'}
</button>

<canvas
bind:this={canvas}
class:capture={drawModeOn}
Expand Down
Loading