Add a Highlighter tool (translucent freehand marker) #196
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_whiteboard#196
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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
crates/hero_whiteboard_admin/static/web/js/whiteboard/tools.js(currentTool === 'draw'in mousedown/move/up around lines 587, 717, 799;drawColor/drawWidthmodule state at lines 15-16;setDrawColor/setDrawWidth/getDrawColor/getDrawWidthexports ~1598-1603).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 astype: 'drawing'(objects.js:2197); it has no opacity / blend / lineCap options today.type === 'drawing'branches incrates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js(~356 serialize, ~758/770 apply viaapplyRemoteDrawingShape).crates/hero_whiteboard_admin/templates/web/board.html(data-tool="draw"at :190,#sub-drawpanel at :255). Keyboard shortcuts/cursors inshortcuts.jsandtools.jssetToolCursor.Requirements
highlightertool: 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.multiply-style composite (globalCompositeOperation) and round line cap/join,drawingobject type with an explicit marker (e.g.data.highlighter = trueplus the opacity/composite/width in style/data) so it is distinguishable on reload.drawingplumbing.drawingobjects with no highlighter marker keep rendering exactly as today (opaque pen).drawingstyle/data payload is free-form JSON). Confirm during planning.Notes / decisions for the spec
data.highlighterboolean plus the existing color (alpha can ride in the 8-digit hex color added in #195) and a storedstrokeWidth, vs. storing explicitopacity+globalCompositeOperationinstyle. Whichever must round-trip through thedrawingbranches ofserializeForServer/applySyncUpdate/applyRemoteDrawingShapeandcreateDrawing.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.applyRemoteDrawingShapepreserve the highlighter marker/opacity after a cut.shortcuts.js(noteDis Draw,EEraser;Ctrl+Dis duplicate).touch crates/hero_whiteboard_admin/src/assets.rsbeforecargo 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—createDrawinghighlighter variant (opacity/composite/lineCap) + the drawing readers.crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js— round-trip the highlighter marker in thedrawingserialize/apply branches.crates/hero_whiteboard_admin/templates/web/board.html— toolbar button +#sub-highlighterpanel.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.static/web/css/whiteboard.cssfor the new sub-toolbar/button.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
drawingobject type with a marker flag, so eraser, history, sync, undo/redo, selection, and reload reuse the existingdrawingplumbing with no server/schema change.Requirements
drawingobject type with a highlighter marker. No new object type, no new sync/history/eraser code path, no server/schema change.Files to Modify/Create
templates/web/board.html- highlighter toolbar button;#sub-highlighterpanel (translucent swatches + thickness slider).static/web/js/whiteboard/tools.js-highlighterstate + mousedown/move/up reusing the draw flow; pass marker tocreateDrawing; cursor; setters/getters.static/web/js/whiteboard/objects.js- thread ahighlighterflag through_rebuildDrawingLines,createDrawing,getDrawingStyle,updateDrawingSegments,applyLocalDrawingPreview,applyRemoteDrawingShape.static/web/js/whiteboard/sync.js- serialize the marker in thedrawingbranch; apply path already covered viaapplyRemoteDrawingShape.static/web/js/whiteboard/app.js- pass marker from persistedstyleintocreateDrawingincase 'drawing'.static/web/js/whiteboard/toolbar.js- registerhighlighter: 'sub-highlighter'; wire swatches/thickness.static/web/js/whiteboard/shortcuts.js-msingle-key shortcut.crates/hero_whiteboard_admin/src/assets.rs- no edit;touchonly 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), andapplyRemoteDrawingShape(remote/segment-cut) all rebuild through it and already thread astyleobject. Carryinghighlighterinside thatstyleobject makes every eraser/sync/undo path preserve the marker with minimal edits. Translucency rides in the existingstrokecolor 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): whenstyle.highlighter === truesetglobalCompositeOperation('multiply')(keep round cap/join, already round) and stamp the marker on the node vialine.setAttr('highlighter', true). Falsy flag → build exactly as today (default source-over) — backwards compatible.createDrawing(~2126-2206): readopts.highlighter; passhighlighter:!!opts.highlighterinto the_rebuildDrawingLinesstyle arg (~2177); addhighlighter:!!opts.highlighterto the registeredobjects[id]entry (~2197).getDrawingStyle(~2226-2235): includehighlighter: line ? !!line.getAttr('highlighter') : false(feeds the eraser precision-cut snapshot).updateDrawingSegments(~2237-2279): addhighlighter: existingLine ? !!existingLine.getAttr('highlighter') : falseto 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.jsvar highlighterColor = '#f5d90a80'; var highlighterWidth = 16;.else if (currentTool === 'highlighter')building the preview Konva.Line like draw (~588-598) butstroke:highlighterColor,strokeWidth:highlighterWidth,globalCompositeOperation:'multiply', round cap/join,listening:false.draw && drawLine): extend condition to also run forhighlighter(same body, samedrawLine).highlighter; at thecreateDrawingcall (~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 shareddrawLine; defensiveendActiveGesturealready covers a leaked line;allowDragexcludes non-select/connector tools so dragging is disabled like draw — no change.setHighlighterColor/Width,getHighlighterColor/Width.Dependencies: Step 1.
Step 3: Sync serialize in sync.js
Files:
crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.jstype === 'drawing'(~356-374): afterstyle.stroke/strokeWidth(~372-373) addstyle.highlighter = !!firstLine.getAttr('highlighter');.type === 'drawing'(~758-772): no change — it passesstyle||{}toapplyRemoteDrawingShapewhich 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.jscreateObjectFromDatacase 'drawing'(~496-512): addhighlighter: !!(style && style.highlighter)to thecreateDrawingoptions (~504-510). Serves both first-load and live WSobject.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.htmldata-tool="highlighter" title="Highlighter (M)"with a marker/highlighter Bootstrap icon (fallback to an available icon ifbi-highlighteris not in the pinned set — verify).#sub-highlighterpanel modeled on#sub-draw(~254-273):#highlighter-colorstranslucent 8-digit swatches (e.g.#f5d90a80,#a3e63580,#67c2f380,#f9737380,#c084fc80), firstclass="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.jsSUB_PANELS(~4-10): addhighlighter: 'sub-highlighter',.#highlighter-colors .sub-color→WhiteboardTools.setHighlighterColor(data-color)(mirror draw ~56-63).#highlighter-width/#highlighter-width-val→WhiteboardTools.setHighlighterWidth(...)(mirror ~66-74)..tool-btnloop +showSubToolbar/setActivealready handle anydata-toolonceSUB_PANELSis 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.jscase '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
Mselects it.drawingobject with the highlighter marker (no new object type).Notes
style.highlighterboolean + alpha in the existingstroke8-digit#rrggbbaa; blend/cap derived at render, not stored. Fewest changes (all rebuild paths already threadstylethrough 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: nohighlighterkey.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.objectenumeration — rejected.M(Marker): unused as single key, not in any modifier branch;D/E/Ctrl+D/Havoided.getDrawingStyle(snapshot for precision-cut),updateDrawingSegments+applyLocalDrawingPreview(readexistingLineattr),applyRemoteDrawingShape(styleToUseprefers incomingstyle.highlighter, falls back to node attr). All call_rebuildDrawingLineswhich re-applies multiply/cap and re-stamps the attr.#f5d90a80(~50% yellow), width 16, slider 6-40; other swatches also 8-digit translucent.touch crates/hero_whiteboard_admin/src/assets.rs,cargo build --release -p hero_whiteboard_admin, then verify the served asset changed before testing.Test Results
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.
Implementation Summary
JS/template-only. The Highlighter reuses the existing
drawingobject 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 astyle.highlighterboolean. Backwards compatible: existing drawings have no marker and render exactly as before.Changes
objects.js —
_rebuildDrawingLinessets 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
Tests