Add a Highlighter tool (translucent freehand marker) #196

Open
opened 2026-05-18 07:12:54 +00:00 by AhmedHanafy725 · 3 comments
Member

Summary

Add a Highlighter tool to the whiteboard: a freehand marker that lays down a thick, semi-transparent stroke which tints the canvas without obscuring the content underneath (text stays readable through it). It is a sibling of the existing Draw tool, not a replacement.

Current state

  • The Draw tool lives in crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js (currentTool === 'draw' in mousedown/move/up around lines 587, 717, 799; drawColor/drawWidth module state at lines 15-16; setDrawColor/setDrawWidth/getDrawColor/getDrawWidth exports ~1598-1603).
  • On mouseup it calls WhiteboardObjects.createDrawing(pts, { stroke, strokeWidth }) (tools.js:807).
  • createDrawing (crates/hero_whiteboard_admin/static/web/js/whiteboard/objects.js:2126) builds a Konva line group registered as type: 'drawing' (objects.js:2197); it has no opacity / blend / lineCap options today.
  • Drawings serialize through the type === 'drawing' branches in crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js (~356 serialize, ~758/770 apply via applyRemoteDrawingShape).
  • Toolbar buttons and per-tool sub-toolbars are in crates/hero_whiteboard_admin/templates/web/board.html (data-tool="draw" at :190, #sub-draw panel at :255). Keyboard shortcuts/cursors in shortcuts.js and tools.js setToolCursor.

Requirements

  • A new highlighter tool: toolbar button, keyboard shortcut, tool cursor, and a sub-toolbar (color + thickness) mirroring the Draw sub-toolbar. Default to a thick stroke and a translucent warm color.
  • Drawing with it produces a freehand stroke that is:
    • semi-transparent (alpha well below 1),
    • noticeably thicker than the pen default,
    • blended so overlapping passes/content remain legible — use a multiply-style composite (globalCompositeOperation) and round line cap/join,
    • rendered so it does not hide objects/text beneath it.
  • Persisted as the existing drawing object type with an explicit marker (e.g. data.highlighter = true plus the opacity/composite/width in style/data) so it is distinguishable on reload.
  • Full parity with pen strokes for: multi-user WebSocket sync, board reload, undo/redo, eraser (both erase-all and precision modes), selection/transform, lock, and delete. No separate code paths for these — reuse the drawing plumbing.
  • Round-trips correctly: reload and remote clients render the highlighter with the same translucency/blend, not as an opaque pen stroke.
  • Backwards compatible: existing drawing objects with no highlighter marker keep rendering exactly as today (opaque pen).
  • JS/template/CSS only expected; no server or DB schema change (the drawing style/data payload is free-form JSON). Confirm during planning.

Notes / decisions for the spec

  • Decide the persistence shape: a data.highlighter boolean plus the existing color (alpha can ride in the 8-digit hex color added in #195) and a stored strokeWidth, vs. storing explicit opacity + globalCompositeOperation in style. Whichever must round-trip through the drawing branches of serializeForServer / applySyncUpdate / applyRemoteDrawingShape and createDrawing.
  • Z-order/legibility approach to settle: rely on globalCompositeOperation: 'multiply' (keeps underlying text visible, simplest, no extra Konva layer) vs. a dedicated behind-objects layer. Recommend the composite approach unless it breaks eraser hit-testing.
  • Confirm the eraser precision/segment-cut path and applyRemoteDrawingShape preserve the highlighter marker/opacity after a cut.
  • Pick a shortcut that doesn't collide with existing single-key tools/shortcuts in shortcuts.js (note D is Draw, E Eraser; Ctrl+D is duplicate).
  • Deploy reminder: assets are embedded at compile time via rust-embed — after editing JS/CSS/templates, touch crates/hero_whiteboard_admin/src/assets.rs before cargo build --release -p hero_whiteboard_admin, and verify the served asset changed before testing.

Affected files (expected)

  • crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js — highlighter tool state, mouse handlers, cursor.
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/objects.jscreateDrawing highlighter variant (opacity/composite/lineCap) + the drawing readers.
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js — round-trip the highlighter marker in the drawing serialize/apply branches.
  • crates/hero_whiteboard_admin/templates/web/board.html — toolbar button + #sub-highlighter panel.
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/shortcuts.js — shortcut + tool activation.
  • crates/hero_whiteboard_admin/static/web/js/whiteboard/toolbar.js — sub-toolbar wiring.
  • Possibly static/web/css/whiteboard.css for the new sub-toolbar/button.
## Summary Add a Highlighter tool to the whiteboard: a freehand marker that lays down a thick, semi-transparent stroke which tints the canvas without obscuring the content underneath (text stays readable through it). It is a sibling of the existing Draw tool, not a replacement. ## Current state - The Draw tool lives in `crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js` (`currentTool === 'draw'` in mousedown/move/up around lines 587, 717, 799; `drawColor`/`drawWidth` module state at lines 15-16; `setDrawColor`/`setDrawWidth`/`getDrawColor`/`getDrawWidth` exports ~1598-1603). - On mouseup it calls `WhiteboardObjects.createDrawing(pts, { stroke, strokeWidth })` (`tools.js:807`). - `createDrawing` (`crates/hero_whiteboard_admin/static/web/js/whiteboard/objects.js:2126`) builds a Konva line group registered as `type: 'drawing'` (`objects.js:2197`); it has no opacity / blend / lineCap options today. - Drawings serialize through the `type === 'drawing'` branches in `crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js` (~356 serialize, ~758/770 apply via `applyRemoteDrawingShape`). - Toolbar buttons and per-tool sub-toolbars are in `crates/hero_whiteboard_admin/templates/web/board.html` (`data-tool="draw"` at :190, `#sub-draw` panel at :255). Keyboard shortcuts/cursors in `shortcuts.js` and `tools.js` `setToolCursor`. ## Requirements - A new `highlighter` tool: toolbar button, keyboard shortcut, tool cursor, and a sub-toolbar (color + thickness) mirroring the Draw sub-toolbar. Default to a thick stroke and a translucent warm color. - Drawing with it produces a freehand stroke that is: - semi-transparent (alpha well below 1), - noticeably thicker than the pen default, - blended so overlapping passes/content remain legible — use a `multiply`-style composite (`globalCompositeOperation`) and round line cap/join, - rendered so it does not hide objects/text beneath it. - Persisted as the existing `drawing` object type with an explicit marker (e.g. `data.highlighter = true` plus the opacity/composite/width in style/data) so it is distinguishable on reload. - Full parity with pen strokes for: multi-user WebSocket sync, board reload, undo/redo, eraser (both erase-all and precision modes), selection/transform, lock, and delete. No separate code paths for these — reuse the `drawing` plumbing. - Round-trips correctly: reload and remote clients render the highlighter with the same translucency/blend, not as an opaque pen stroke. - Backwards compatible: existing `drawing` objects with no highlighter marker keep rendering exactly as today (opaque pen). - JS/template/CSS only expected; no server or DB schema change (the `drawing` style/data payload is free-form JSON). Confirm during planning. ## Notes / decisions for the spec - Decide the persistence shape: a `data.highlighter` boolean plus the existing color (alpha can ride in the 8-digit hex color added in #195) and a stored `strokeWidth`, vs. storing explicit `opacity` + `globalCompositeOperation` in `style`. Whichever must round-trip through the `drawing` branches of `serializeForServer` / `applySyncUpdate` / `applyRemoteDrawingShape` and `createDrawing`. - Z-order/legibility approach to settle: rely on `globalCompositeOperation: 'multiply'` (keeps underlying text visible, simplest, no extra Konva layer) vs. a dedicated behind-objects layer. Recommend the composite approach unless it breaks eraser hit-testing. - Confirm the eraser precision/segment-cut path and `applyRemoteDrawingShape` preserve the highlighter marker/opacity after a cut. - Pick a shortcut that doesn't collide with existing single-key tools/shortcuts in `shortcuts.js` (note `D` is Draw, `E` Eraser; `Ctrl+D` is duplicate). - Deploy reminder: assets are embedded at compile time via rust-embed — after editing JS/CSS/templates, `touch crates/hero_whiteboard_admin/src/assets.rs` before `cargo build --release -p hero_whiteboard_admin`, and verify the served asset changed before testing. ## Affected files (expected) - `crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js` — highlighter tool state, mouse handlers, cursor. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/objects.js` — `createDrawing` highlighter variant (opacity/composite/lineCap) + the drawing readers. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js` — round-trip the highlighter marker in the `drawing` serialize/apply branches. - `crates/hero_whiteboard_admin/templates/web/board.html` — toolbar button + `#sub-highlighter` panel. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/shortcuts.js` — shortcut + tool activation. - `crates/hero_whiteboard_admin/static/web/js/whiteboard/toolbar.js` — sub-toolbar wiring. - Possibly `static/web/css/whiteboard.css` for the new sub-toolbar/button.
Author
Member

Implementation Spec for Issue #196

Objective

Add a Highlighter tool: a freehand marker that lays a thick, semi-transparent, multiply-blended, round-capped stroke that tints the canvas without obscuring content underneath. Sibling of the Draw tool, persisted as the existing drawing object type with a marker flag, so eraser, history, sync, undo/redo, selection, and reload reuse the existing drawing plumbing with no server/schema change.

Requirements

  • New toolbar button + keyboard shortcut + crosshair cursor + sub-toolbar (color + thickness) with a translucent default.
  • Stroke is thick, semi-transparent, multiply-blended, round-capped; underlying text/objects stay readable.
  • Persisted as the existing drawing object type with a highlighter marker. No new object type, no new sync/history/eraser code path, no server/schema change.
  • Round-trips through reload, multi-user WebSocket sync, undo/redo, eraser erase-all, and eraser precision/segment-cut.
  • Backwards compatible: existing drawings without the marker render exactly as today (opaque pen, source-over).

Files to Modify/Create

  • templates/web/board.html - highlighter toolbar button; #sub-highlighter panel (translucent swatches + thickness slider).
  • static/web/js/whiteboard/tools.js - highlighter state + mousedown/move/up reusing the draw flow; pass marker to createDrawing; cursor; setters/getters.
  • static/web/js/whiteboard/objects.js - thread a highlighter flag through _rebuildDrawingLines, createDrawing, getDrawingStyle, updateDrawingSegments, applyLocalDrawingPreview, applyRemoteDrawingShape.
  • static/web/js/whiteboard/sync.js - serialize the marker in the drawing branch; apply path already covered via applyRemoteDrawingShape.
  • static/web/js/whiteboard/app.js - pass marker from persisted style into createDrawing in case 'drawing'.
  • static/web/js/whiteboard/toolbar.js - register highlighter: 'sub-highlighter'; wire swatches/thickness.
  • static/web/js/whiteboard/shortcuts.js - m single-key shortcut.
  • crates/hero_whiteboard_admin/src/assets.rs - no edit; touch only to bust the rust-embed cache before rebuild.

Implementation Plan

Central design: _rebuildDrawingLines (objects.js:2108) is the ONLY Konva.Line factory for drawings; createDrawing, updateDrawingSegments (eraser commit), applyLocalDrawingPreview (eraser drag preview), and applyRemoteDrawingShape (remote/segment-cut) all rebuild through it and already thread a style object. Carrying highlighter inside that style object makes every eraser/sync/undo path preserve the marker with minimal edits. Translucency rides in the existing stroke color as 8-digit #rrggbbaa (the mechanism added in #195; Konva .stroke() renders it natively). Multiply blend + round cap are derived from the flag at render time, not stored.

Step 1: Persistence/render core in objects.js

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/objects.js

  • _rebuildDrawingLines (~2108-2124): when style.highlighter === true set globalCompositeOperation('multiply') (keep round cap/join, already round) and stamp the marker on the node via line.setAttr('highlighter', true). Falsy flag → build exactly as today (default source-over) — backwards compatible.
  • createDrawing (~2126-2206): read opts.highlighter; pass highlighter:!!opts.highlighter into the _rebuildDrawingLines style arg (~2177); add highlighter:!!opts.highlighter to the registered objects[id] entry (~2197).
  • getDrawingStyle (~2226-2235): include highlighter: line ? !!line.getAttr('highlighter') : false (feeds the eraser precision-cut snapshot).
  • updateDrawingSegments (~2237-2279): add highlighter: existingLine ? !!existingLine.getAttr('highlighter') : false to the rebuilt style (~2265-2268).
  • applyLocalDrawingPreview (~2282-2305): same addition to the style (~2294-2297).
  • applyRemoteDrawingShape (~2307-2340): styleToUse (~2334-2337) → highlighter: (style && style.highlighter != null) ? !!style.highlighter : (existingLine ? !!existingLine.getAttr('highlighter') : false).
    Dependencies: none (foundation; do first).

Step 2: Tool flow in tools.js

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js

  • State (~15-16): var highlighterColor = '#f5d90a80'; var highlighterWidth = 16;.
  • mousedown (~587 draw branch): add else if (currentTool === 'highlighter') building the preview Konva.Line like draw (~588-598) but stroke:highlighterColor, strokeWidth:highlighterWidth, globalCompositeOperation:'multiply', round cap/join, listening:false.
  • mousemove (~717 draw && drawLine): extend condition to also run for highlighter (same body, same drawLine).
  • mouseup (~799-816): extend condition to highlighter; at the createDrawing call (~807) pass { stroke, strokeWidth: sw, highlighter: currentTool === 'highlighter' }.
  • setToolCursor (~454-465): final else already returns 'crosshair' for non-pan/select/eraser → highlighter gets crosshair automatically (verify only).
  • setTool/setObjectsDraggable (~467-508): highlighter reuses the shared drawLine; defensive endActiveGesture already covers a leaked line; allowDrag excludes non-select/connector tools so dragging is disabled like draw — no change.
  • Exports (~1598-1606): add setHighlighterColor/Width, getHighlighterColor/Width.
    Dependencies: Step 1.

Step 3: Sync serialize in sync.js

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js

  • Serialize type === 'drawing' (~356-374): after style.stroke/strokeWidth (~372-373) add style.highlighter = !!firstLine.getAttr('highlighter');.
  • Apply type === 'drawing' (~758-772): no change — it passes style||{} to applyRemoteDrawingShape which Step 1 made marker-aware.
    Dependencies: Step 1.

Step 4: Reload / WS-create loader in app.js

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/app.js

  • createObjectFromData case 'drawing' (~496-512): add highlighter: !!(style && style.highlighter) to the createDrawing options (~504-510). Serves both first-load and live WS object.created. Old rows lack the key → opaque pen, unchanged.
    Dependencies: Steps 1, 3.

Step 5: Toolbar button + sub-toolbar markup

Files: crates/hero_whiteboard_admin/templates/web/board.html

  • Tool button after Draw (~190-192): data-tool="highlighter" title="Highlighter (M)" with a marker/highlighter Bootstrap icon (fallback to an available icon if bi-highlighter is not in the pinned set — verify).
  • #sub-highlighter panel modeled on #sub-draw (~254-273): #highlighter-colors translucent 8-digit swatches (e.g. #f5d90a80, #a3e63580, #67c2f380, #f9737380, #c084fc80), first class="sub-color active"; thickness <input type="range" id="highlighter-width" min="6" max="40" value="16"> + #highlighter-width-val.
    Dependencies: none (markup; parallelizable).

Step 6: Toolbar wiring in toolbar.js

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/toolbar.js

  • SUB_PANELS (~4-10): add highlighter: 'sub-highlighter',.
  • Swatch handler over #highlighter-colors .sub-colorWhiteboardTools.setHighlighterColor(data-color) (mirror draw ~56-63).
  • Thickness handler for #highlighter-width/#highlighter-width-valWhiteboardTools.setHighlighterWidth(...) (mirror ~66-74).
  • Generic .tool-btn loop + showSubToolbar/setActive already handle any data-tool once SUB_PANELS is extended.
    Dependencies: Step 2 (setters), Step 5 (DOM ids).

Step 7: Keyboard shortcut in shortcuts.js

Files: crates/hero_whiteboard_admin/static/web/js/whiteboard/shortcuts.js

  • Single-key tool switch (~174-214): add case 'm': WhiteboardTools.setTool('highlighter'); WhiteboardToolbar.setActive('highlighter'); break;.
    Dependencies: Step 2. Parallelizable with 5/6.

Recommended order: 1 → (2, 3, 5 in parallel) → (4, 6, 7).

Acceptance Criteria

  • Highlighter toolbar button present, activates the tool, shows its sub-toolbar; M selects it.
  • Cursor is a crosshair while the tool is active.
  • Sub-toolbar has color swatches + thickness slider; default color translucent, default width thick.
  • Stroke is thick, semi-transparent, multiply-blended, round-capped; content under it stays readable.
  • Persisted as a drawing object with the highlighter marker (no new object type).
  • After reload the stroke renders translucent/multiply (not opaque pen).
  • In a second session, a peer's highlighter stroke renders translucent/multiply via WebSocket.
  • Undo removes it; redo restores it with the highlighter look intact.
  • Eraser erase-all deletes it; precision/segment-cut splits it and every surviving segment keeps the look.
  • Selection, transform/resize, lock, delete work like a pen drawing.
  • Existing pre-feature drawings unchanged (opaque, source-over, no regression).
  • No server/schema/RPC change; no new sync/history/eraser code path.

Notes

  • Persistence: style.highlighter boolean + alpha in the existing stroke 8-digit #rrggbbaa; blend/cap derived at render, not stored. Fewest changes (all rebuild paths already thread style through one factory) and backwards compatible (missing key ⇒ today's pen).
    Example: {"type":"drawing","style":{"stroke":"#f5d90a80","strokeWidth":16,"highlighter":true},"data":{"segments":[[x,y,...]]}}. Normal pen: no highlighter key.
  • Z-order/blend: multiply + round cap on the existing object layer; no new layer. Eraser hit-tests AABB/segment geometry + group .object/getClientRect (blend/opacity not read) → erase-all + precision-cut still work; transformer/selection use bounding box; minimap renders via its own 2D ctx (ignores blend/opacity). A separate layer would break eraser .object enumeration — rejected.
  • Shortcut M (Marker): unused as single key, not in any modifier branch; D/E/Ctrl+D/H avoided.
  • Eraser-cut preservation points: getDrawingStyle (snapshot for precision-cut), updateDrawingSegments + applyLocalDrawingPreview (read existingLine attr), applyRemoteDrawingShape (styleToUse prefers incoming style.highlighter, falls back to node attr). All call _rebuildDrawingLines which re-applies multiply/cap and re-stamps the attr.
  • Defaults: color #f5d90a80 (~50% yellow), width 16, slider 6-40; other swatches also 8-digit translucent.
  • Deploy: assets embed at compile time via rust-embed — after edits touch crates/hero_whiteboard_admin/src/assets.rs, cargo build --release -p hero_whiteboard_admin, then verify the served asset changed before testing.
## Implementation Spec for Issue #196 ### Objective Add a Highlighter tool: a freehand marker that lays a thick, semi-transparent, multiply-blended, round-capped stroke that tints the canvas without obscuring content underneath. Sibling of the Draw tool, persisted as the existing `drawing` object type with a marker flag, so eraser, history, sync, undo/redo, selection, and reload reuse the existing `drawing` plumbing with no server/schema change. ### Requirements - New toolbar button + keyboard shortcut + crosshair cursor + sub-toolbar (color + thickness) with a translucent default. - Stroke is thick, semi-transparent, multiply-blended, round-capped; underlying text/objects stay readable. - Persisted as the existing `drawing` object type with a highlighter marker. No new object type, no new sync/history/eraser code path, no server/schema change. - Round-trips through reload, multi-user WebSocket sync, undo/redo, eraser erase-all, and eraser precision/segment-cut. - Backwards compatible: existing drawings without the marker render exactly as today (opaque pen, source-over). ### Files to Modify/Create - `templates/web/board.html` - highlighter toolbar button; `#sub-highlighter` panel (translucent swatches + thickness slider). - `static/web/js/whiteboard/tools.js` - `highlighter` state + mousedown/move/up reusing the draw flow; pass marker to `createDrawing`; cursor; setters/getters. - `static/web/js/whiteboard/objects.js` - thread a `highlighter` flag through `_rebuildDrawingLines`, `createDrawing`, `getDrawingStyle`, `updateDrawingSegments`, `applyLocalDrawingPreview`, `applyRemoteDrawingShape`. - `static/web/js/whiteboard/sync.js` - serialize the marker in the `drawing` branch; apply path already covered via `applyRemoteDrawingShape`. - `static/web/js/whiteboard/app.js` - pass marker from persisted `style` into `createDrawing` in `case 'drawing'`. - `static/web/js/whiteboard/toolbar.js` - register `highlighter: 'sub-highlighter'`; wire swatches/thickness. - `static/web/js/whiteboard/shortcuts.js` - `m` single-key shortcut. - `crates/hero_whiteboard_admin/src/assets.rs` - no edit; `touch` only to bust the rust-embed cache before rebuild. ### Implementation Plan Central design: `_rebuildDrawingLines` (objects.js:2108) is the ONLY Konva.Line factory for drawings; `createDrawing`, `updateDrawingSegments` (eraser commit), `applyLocalDrawingPreview` (eraser drag preview), and `applyRemoteDrawingShape` (remote/segment-cut) all rebuild through it and already thread a `style` object. Carrying `highlighter` inside that `style` object makes every eraser/sync/undo path preserve the marker with minimal edits. Translucency rides in the existing `stroke` color as 8-digit `#rrggbbaa` (the mechanism added in #195; Konva `.stroke()` renders it natively). Multiply blend + round cap are derived from the flag at render time, not stored. #### Step 1: Persistence/render core in objects.js Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/objects.js` - `_rebuildDrawingLines` (~2108-2124): when `style.highlighter === true` set `globalCompositeOperation('multiply')` (keep round cap/join, already round) and stamp the marker on the node via `line.setAttr('highlighter', true)`. Falsy flag → build exactly as today (default source-over) — backwards compatible. - `createDrawing` (~2126-2206): read `opts.highlighter`; pass `highlighter:!!opts.highlighter` into the `_rebuildDrawingLines` style arg (~2177); add `highlighter:!!opts.highlighter` to the registered `objects[id]` entry (~2197). - `getDrawingStyle` (~2226-2235): include `highlighter: line ? !!line.getAttr('highlighter') : false` (feeds the eraser precision-cut snapshot). - `updateDrawingSegments` (~2237-2279): add `highlighter: existingLine ? !!existingLine.getAttr('highlighter') : false` to the rebuilt style (~2265-2268). - `applyLocalDrawingPreview` (~2282-2305): same addition to the style (~2294-2297). - `applyRemoteDrawingShape` (~2307-2340): `styleToUse` (~2334-2337) → `highlighter: (style && style.highlighter != null) ? !!style.highlighter : (existingLine ? !!existingLine.getAttr('highlighter') : false)`. Dependencies: none (foundation; do first). #### Step 2: Tool flow in tools.js Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js` - State (~15-16): `var highlighterColor = '#f5d90a80'; var highlighterWidth = 16;`. - mousedown (~587 draw branch): add `else if (currentTool === 'highlighter')` building the preview Konva.Line like draw (~588-598) but `stroke:highlighterColor`, `strokeWidth:highlighterWidth`, `globalCompositeOperation:'multiply'`, round cap/join, `listening:false`. - mousemove (~717 `draw && drawLine`): extend condition to also run for `highlighter` (same body, same `drawLine`). - mouseup (~799-816): extend condition to `highlighter`; at the `createDrawing` call (~807) pass `{ stroke, strokeWidth: sw, highlighter: currentTool === 'highlighter' }`. - `setToolCursor` (~454-465): final else already returns `'crosshair'` for non-pan/select/eraser → highlighter gets crosshair automatically (verify only). - `setTool`/`setObjectsDraggable` (~467-508): highlighter reuses the shared `drawLine`; defensive `endActiveGesture` already covers a leaked line; `allowDrag` excludes non-select/connector tools so dragging is disabled like draw — no change. - Exports (~1598-1606): add `setHighlighterColor/Width`, `getHighlighterColor/Width`. Dependencies: Step 1. #### Step 3: Sync serialize in sync.js Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js` - Serialize `type === 'drawing'` (~356-374): after `style.stroke`/`strokeWidth` (~372-373) add `style.highlighter = !!firstLine.getAttr('highlighter');`. - Apply `type === 'drawing'` (~758-772): no change — it passes `style||{}` to `applyRemoteDrawingShape` which Step 1 made marker-aware. Dependencies: Step 1. #### Step 4: Reload / WS-create loader in app.js Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/app.js` - `createObjectFromData` `case 'drawing'` (~496-512): add `highlighter: !!(style && style.highlighter)` to the `createDrawing` options (~504-510). Serves both first-load and live WS `object.created`. Old rows lack the key → opaque pen, unchanged. Dependencies: Steps 1, 3. #### Step 5: Toolbar button + sub-toolbar markup Files: `crates/hero_whiteboard_admin/templates/web/board.html` - Tool button after Draw (~190-192): `data-tool="highlighter" title="Highlighter (M)"` with a marker/highlighter Bootstrap icon (fallback to an available icon if `bi-highlighter` is not in the pinned set — verify). - `#sub-highlighter` panel modeled on `#sub-draw` (~254-273): `#highlighter-colors` translucent 8-digit swatches (e.g. `#f5d90a80`, `#a3e63580`, `#67c2f380`, `#f9737380`, `#c084fc80`), first `class="sub-color active"`; thickness `<input type="range" id="highlighter-width" min="6" max="40" value="16">` + `#highlighter-width-val`. Dependencies: none (markup; parallelizable). #### Step 6: Toolbar wiring in toolbar.js Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/toolbar.js` - `SUB_PANELS` (~4-10): add `highlighter: 'sub-highlighter',`. - Swatch handler over `#highlighter-colors .sub-color` → `WhiteboardTools.setHighlighterColor(data-color)` (mirror draw ~56-63). - Thickness handler for `#highlighter-width`/`#highlighter-width-val` → `WhiteboardTools.setHighlighterWidth(...)` (mirror ~66-74). - Generic `.tool-btn` loop + `showSubToolbar`/`setActive` already handle any `data-tool` once `SUB_PANELS` is extended. Dependencies: Step 2 (setters), Step 5 (DOM ids). #### Step 7: Keyboard shortcut in shortcuts.js Files: `crates/hero_whiteboard_admin/static/web/js/whiteboard/shortcuts.js` - Single-key tool switch (~174-214): add `case 'm': WhiteboardTools.setTool('highlighter'); WhiteboardToolbar.setActive('highlighter'); break;`. Dependencies: Step 2. Parallelizable with 5/6. Recommended order: 1 → (2, 3, 5 in parallel) → (4, 6, 7). ### Acceptance Criteria - [ ] Highlighter toolbar button present, activates the tool, shows its sub-toolbar; `M` selects it. - [ ] Cursor is a crosshair while the tool is active. - [ ] Sub-toolbar has color swatches + thickness slider; default color translucent, default width thick. - [ ] Stroke is thick, semi-transparent, multiply-blended, round-capped; content under it stays readable. - [ ] Persisted as a `drawing` object with the highlighter marker (no new object type). - [ ] After reload the stroke renders translucent/multiply (not opaque pen). - [ ] In a second session, a peer's highlighter stroke renders translucent/multiply via WebSocket. - [ ] Undo removes it; redo restores it with the highlighter look intact. - [ ] Eraser erase-all deletes it; precision/segment-cut splits it and every surviving segment keeps the look. - [ ] Selection, transform/resize, lock, delete work like a pen drawing. - [ ] Existing pre-feature drawings unchanged (opaque, source-over, no regression). - [ ] No server/schema/RPC change; no new sync/history/eraser code path. ### Notes - Persistence: `style.highlighter` boolean + alpha in the existing `stroke` 8-digit `#rrggbbaa`; blend/cap derived at render, not stored. Fewest changes (all rebuild paths already thread `style` through one factory) and backwards compatible (missing key ⇒ today's pen). Example: `{"type":"drawing","style":{"stroke":"#f5d90a80","strokeWidth":16,"highlighter":true},"data":{"segments":[[x,y,...]]}}`. Normal pen: no `highlighter` key. - Z-order/blend: `multiply` + round cap on the existing object layer; no new layer. Eraser hit-tests AABB/segment geometry + group `.object`/`getClientRect` (blend/opacity not read) → erase-all + precision-cut still work; transformer/selection use bounding box; minimap renders via its own 2D ctx (ignores blend/opacity). A separate layer would break eraser `.object` enumeration — rejected. - Shortcut `M` (Marker): unused as single key, not in any modifier branch; `D`/`E`/`Ctrl+D`/`H` avoided. - Eraser-cut preservation points: `getDrawingStyle` (snapshot for precision-cut), `updateDrawingSegments` + `applyLocalDrawingPreview` (read `existingLine` attr), `applyRemoteDrawingShape` (`styleToUse` prefers incoming `style.highlighter`, falls back to node attr). All call `_rebuildDrawingLines` which re-applies multiply/cap and re-stamps the attr. - Defaults: color `#f5d90a80` (~50% yellow), width 16, slider 6-40; other swatches also 8-digit translucent. - Deploy: assets embed at compile time via rust-embed — after edits `touch crates/hero_whiteboard_admin/src/assets.rs`, `cargo build --release -p hero_whiteboard_admin`, then verify the served asset changed before testing.
Author
Member

Test Results

  • Total: 0
  • Passed: 0
  • Failed: 0

cargo test --workspace --lib: ok - all 4 lib crates built and ran clean (0 passed; 0 failed; 0 ignored); no Rust regression
node --check (objects, tools, sync, app, toolbar, shortcuts): ok - all 6 files parsed successfully

Note: #196 is a JS/template-only change (Highlighter tool reuses the existing drawing object type). No JS unit harness exists in this repo; the Rust suite is the regression gate and the highlighter tool/persistence/sync/eraser/undo is verified manually in-browser.

## Test Results - Total: 0 - Passed: 0 - Failed: 0 cargo test --workspace --lib: ok - all 4 lib crates built and ran clean (0 passed; 0 failed; 0 ignored); no Rust regression node --check (objects, tools, sync, app, toolbar, shortcuts): ok - all 6 files parsed successfully Note: #196 is a JS/template-only change (Highlighter tool reuses the existing drawing object type). No JS unit harness exists in this repo; the Rust suite is the regression gate and the highlighter tool/persistence/sync/eraser/undo is verified manually in-browser.
Author
Member

Implementation Summary

JS/template-only. The Highlighter reuses the existing drawing object type entirely — no new object type, no new sync/history/eraser code path, no server or schema change. Translucency rides in the existing stroke color as an 8-digit hex; the multiply blend and round cap are derived at render time from a style.highlighter boolean. Backwards compatible: existing drawings have no marker and render exactly as before.

Changes

objects.js — _rebuildDrawingLines sets globalCompositeOperation 'multiply' when style.highlighter is true and stamps line.setAttr('highlighter', bool); the falsy path is byte-identical to before. The marker is threaded through createDrawing (opts + registered entry), getDrawingStyle, updateDrawingSegments, applyLocalDrawingPreview, and applyRemoteDrawingShape so every eraser/sync/cut rebuild preserves it (all rebuilds go through the single _rebuildDrawingLines factory).

tools.js — added highlighterColor (#f5d90a80) / highlighterWidth (16) state, a highlighter mousedown branch (multiply preview line on the shared drawLine), extended mousemove/mouseup to also handle the highlighter tool, passes highlighter:true into createDrawing, and added setHighlighterColor/Width + getHighlighterColor/Width. Cursor is crosshair via the existing default.

sync.js — serializeForServer drawing branch now writes style.highlighter from firstLine.getAttr('highlighter'); the apply path needed no change (it routes through applyRemoteDrawingShape which is now marker-aware).

app.js — createObjectFromData case 'drawing' passes highlighter from the persisted style into createDrawing; this single path serves both first-load and live WebSocket create.

board.html — Highlighter toolbar button (M) next to Draw, and a #sub-highlighter sub-toolbar (six translucent 8-digit color swatches + a 6-40 thickness slider, default 16).

toolbar.js — registered highlighter -> sub-highlighter in SUB_PANELS and wired the swatch/thickness handlers to the new tool setters (mirrors the draw handlers).

shortcuts.js — single-key 'm' selects the highlighter tool.

Behavior after change

  • Highlighter tool button + M shortcut + crosshair cursor + its own color/thickness sub-toolbar.
  • Strokes are thick, semi-transparent, multiply-blended, round-capped; underlying text/objects stay readable.
  • Persisted as a drawing with the highlighter marker; reload and remote peers render it translucent (not opaque pen).
  • Undo/redo, eraser erase-all, eraser precision/segment-cut, selection, transform, lock, and delete all work via the existing drawing plumbing and preserve the highlighter look on every surviving segment.
  • Pre-existing drawings are unchanged.

Tests

  • cargo test --workspace --lib: green, no Rust regression (JS/template-only; no JS unit harness in repo).
  • node --check on all 6 changed JS files: ok.
  • Highlighter draw / persistence / sync / eraser-cut / undo verified manually in-browser after a forced-embed rebuild and redeploy.
## Implementation Summary JS/template-only. The Highlighter reuses the existing `drawing` object type entirely — no new object type, no new sync/history/eraser code path, no server or schema change. Translucency rides in the existing stroke color as an 8-digit hex; the multiply blend and round cap are derived at render time from a `style.highlighter` boolean. Backwards compatible: existing drawings have no marker and render exactly as before. ### Changes objects.js — `_rebuildDrawingLines` sets globalCompositeOperation 'multiply' when style.highlighter is true and stamps line.setAttr('highlighter', bool); the falsy path is byte-identical to before. The marker is threaded through createDrawing (opts + registered entry), getDrawingStyle, updateDrawingSegments, applyLocalDrawingPreview, and applyRemoteDrawingShape so every eraser/sync/cut rebuild preserves it (all rebuilds go through the single _rebuildDrawingLines factory). tools.js — added highlighterColor (#f5d90a80) / highlighterWidth (16) state, a highlighter mousedown branch (multiply preview line on the shared drawLine), extended mousemove/mouseup to also handle the highlighter tool, passes highlighter:true into createDrawing, and added setHighlighterColor/Width + getHighlighterColor/Width. Cursor is crosshair via the existing default. sync.js — serializeForServer drawing branch now writes style.highlighter from firstLine.getAttr('highlighter'); the apply path needed no change (it routes through applyRemoteDrawingShape which is now marker-aware). app.js — createObjectFromData case 'drawing' passes highlighter from the persisted style into createDrawing; this single path serves both first-load and live WebSocket create. board.html — Highlighter toolbar button (M) next to Draw, and a #sub-highlighter sub-toolbar (six translucent 8-digit color swatches + a 6-40 thickness slider, default 16). toolbar.js — registered highlighter -> sub-highlighter in SUB_PANELS and wired the swatch/thickness handlers to the new tool setters (mirrors the draw handlers). shortcuts.js — single-key 'm' selects the highlighter tool. ### Behavior after change - Highlighter tool button + M shortcut + crosshair cursor + its own color/thickness sub-toolbar. - Strokes are thick, semi-transparent, multiply-blended, round-capped; underlying text/objects stay readable. - Persisted as a drawing with the highlighter marker; reload and remote peers render it translucent (not opaque pen). - Undo/redo, eraser erase-all, eraser precision/segment-cut, selection, transform, lock, and delete all work via the existing drawing plumbing and preserve the highlighter look on every surviving segment. - Pre-existing drawings are unchanged. ### Tests - cargo test --workspace --lib: green, no Rust regression (JS/template-only; no JS unit harness in repo). - node --check on all 6 changed JS files: ok. - Highlighter draw / persistence / sync / eraser-cut / undo verified manually in-browser after a forced-embed rebuild and redeploy.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lhumina_code/hero_whiteboard#196
No description provided.