Add opacity (alpha) control to all color selections #195
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#195
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 an opacity (alpha) control to every color selection on the whiteboard so users can make fills, strokes, and other colored elements semi-transparent. Today every color picker only chooses an opaque color; there is no way to set transparency, and opacity is neither persisted nor synced.
Current state
_buildColorTrigger/_buildColorPopoverincrates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.jsand are reused for: shape fill, shape stroke, sticky color, text color, frame background, frame border, document background, document border, connector stroke, and kanban column color.serializeForServerincrates/hero_whiteboard_admin/static/web/js/whiteboard/sync.jswritesstyle.fill/style.stroke/style.strokeWidth/style.color/style.bgColor/style.borderColor— none carry an alpha component.applySyncUpdateapplies those same fields back; there is no opacity path..opacity(); objects are always fully opaque.Requirements
_buildColorTrigger/ popover instance) gains an opacity control (e.g. a 0–100% slider in the color popover) that applies to that specific color.styleJSON, serialized byserializeForServer, and restored on board load.applySyncUpdateon other clients.snapshotBefore/commitUpdate) like any other style edit.Notes / decisions for the implementation spec
rgba()string vs. keep the hex color and add a sibling*_opacityvalue instyle. Either must round-trip throughserializeForServer/applySyncUpdateand the per-type style readers inobjects.js.touch crates/hero_whiteboard_admin/src/assets.rsbeforecargo build --release -p hero_whiteboard_admin) and verify the served JS reflects the change before testing.Affected files (expected)
crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js— opacity UI in the color popover/trigger; wire to the per-type apply handlers.crates/hero_whiteboard_admin/static/web/js/whiteboard/sync.js— serialize/apply opacity inserializeForServer/applySyncUpdate.crates/hero_whiteboard_admin/static/web/js/whiteboard/objects.js— per-type style readers/writers honor the alpha.connectors.js(connector stroke style payload) and the kanban column color path.Implementation Spec for Issue #195
Objective
Add an opacity (alpha) control to every color picker so users can make any fill, stroke, text, background, border, or connector color semi-transparent. The alpha renders live, persists in the existing
stylepayload, restores on reload, syncs over WebSocket, and round-trips through undo/redo — with zero server/schema changes and full backwards compatibility (objects saved without alpha render fully opaque).Requirements
_buildColorPopover), inherited by all ~10 call sites — no per-call-site duplication.#rrggbbaa; no newstylekeys; noserializeForServer/applySyncUpdate/app.js/connectors.jsschema changes..fill()/.stroke()accept 8-digit hex directly; value flows through every existing read/write path untouched.rgb()/named colors treated as alpha=1;transparentstays the no-fill sentinel._splitColor/_composeColor/_clampAlpha) lives inselection_toolbar.js.<input type=color>(6-digit only) keeps editing RGB; the slider edits alpha independently; both callonPickwith the recombined value..sub-colorpreset swatches stay opaque presets (popover slider is the single source of alpha).themes.js _parseColorguarded so an 8-digit hex doesn't break text-contrast luma.Files to Modify/Create
crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js- color/alpha helpers; opacity slider in_buildColorPopover; thread alpha through_buildColorTrigger/activeColorSetter;_normalizeHexignores alpha.crates/hero_whiteboard_admin/static/web/js/whiteboard/themes.js-_parseColoraccepts 8-digit hex (drops alpha).crates/hero_whiteboard_admin/src/assets.rs- no code change; must betouched before the release rebuild so rust-embed re-embeds the changed JS.No other files require changes:
sync.js,objects.js,app.js,connectors.js,board.htmlalready pass the color string through verbatim to Konva.fill()/.stroke()which accept 8-digit hex.Implementation Plan
Step 1: Shared color/alpha helpers in selection_toolbar.js
Files:
crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js_normalizeHex(~:463) add_splitColor(c)→{hex6,'#rrggbb', alpha:0..1, special:'transparent'|null}handlingtransparent,#rgb,#rrggbb,#rrggbbaa,rgb(),rgba(), unknown→opaque black; reuse the existing#rgb→#rrggbbexpansion._composeColor(hex6, alpha)→ returnhex6when alpha≥1 (opaque stays 6-digit, byte-identical to today), elsehex6 + 2-hex(round(alpha*255))._clampAlpha(n)→ clamp 0..1._normalizeHex(~:463-481) so an 8-digit#rrggbbaais truncated to its first 6 hex digits for the native<input type=color>.Dependencies: none
Step 2: Opacity slider in
_buildColorPopoverFiles:
crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js_buildColorPopover(:493-552), compute:549) append an opacity row: range 0..100 (percent), initialvar split=_splitColor(currentValue)at top; after the custom-color row (round(split.alpha*100), live numeric label.baseHex6=split.hex6andisTransparent=split.special==='transparent'.input:onPick(_composeColor(baseHex6, alpha))(skip when transparent).baseHex6=_splitColor(c).hex6; isTransparent=false; onPick(_composeColor(baseHex6,currentAlpha))(preserve current alpha); keep_closeTopPopover().baseHex6=native.value; isTransparent=false; onPick(_composeColor(baseHex6,currentAlpha)).onPick('transparent'); grey/disable the slider while transparent active.Dependencies: Step 1
Step 3: Thread alpha through
_buildColorTrigger/activeColorSetterFiles:
crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js_applySwatchVisual(~:483-491) already sets--swatch-color; 8-digit hex renders via CSS — verify the.swatch-discisn't overlaid opaque (cosmetic only; out of scope unless trivial)._buildColorTrigger(:575-614) passes full value into the popover and:310-314) still work with 6-digit input (alpha 1 → opaque).activeColorSetter/setColor(Dependencies: Steps 1-2 (parallel with Step 4)
Step 4:
themes.js _parseColortolerates 8-digit hexFiles:
crates/hero_whiteboard_admin/static/web/js/whiteboard/themes.js_parseColor(~:376-394) rejects hex length != 3/6 (:383). Add: if length 8, drop the last 2 chars before parsing r/g/b (alpha irrelevant to luma). This is the only color parser that breaks on 8-digit input; it feeds text-contrast only.Dependencies: none (parallel with Steps 1-3)
Step 5: Verify no other 6-digit-only color parsing (read-only)
sync.js serializeForServer/applySyncUpdate,app.js createObjectFromData,objects.jsper-type readers,connectors.js(createConnector/persistStyle/applyUpdate/loadFromSync) all pass the color string verbatim to Konva. Onlythemes.js _parseColorandselection_toolbar.js _normalizeHexinspect hex length. Spot-checkkanban.jscolumn color usage during implementation (expected pass-through).Dependencies: none
Acceptance Criteria
stylecolor field (8-digit hex), no new keys, no server/schema change.rgb()/named/none) render fully opaque.transparent(no-fill) still works and is not corrupted by the slider.Notes
#rrggbbaain the existing single color field. Every persistence/sync/render path already carries the color as one string applied via Konva.fill()/.stroke()(which accept 8-digit hex), so this needs zero changes to serialize/apply/objects/app/connectors and undo round-trips for free. Opaque colors stay 6-digit (_composeColorreturnshex6when alpha≥1) so old/new opaque objects are byte-identical and paletteis-selectedmatching still works.transparentremains untouched.selection_toolbar.js(only module that constructs/deconstructs alpha) — no new util file /<script>include.baseStrokeand kanban column color pass 8-digit through unchanged; preset swatches stay opaque.themes.js _parseColor(text contrast), fixed in Step 4;_normalizeHex(native-picker prep) handled in Step 1.touch crates/hero_whiteboard_admin/src/assets.rsbeforecargo build --release -p hero_whiteboard_admin, and verify the served JS changed (fetch + grep the new helper) before testing — otherwise a stale embedded copy is served.Test Results
cargo test --workspace --lib: all 4 lib test binaries (server/admin, app, examples, sdk) compiled and ran clean, 0 tests, all results "ok", no failures
node --check selection_toolbar.js, themes.js: ok
Note: #195 is a JS-only change (per-color opacity via 8-digit hex through existing style fields). No JS unit harness exists in this repo; the Rust suite is the regression gate and the opacity UI/persistence/sync/undo is verified manually in-browser.
Implementation Summary
JS-only. Alpha is encoded as 8-digit hex (#rrggbbaa) inside the existing color string; opaque colors stay 6-digit so old objects are byte-identical and all persistence/sync/undo paths round-trip unchanged. No server or schema changes.
Changes
crates/hero_whiteboard_admin/static/web/js/whiteboard/selection_toolbar.js
crates/hero_whiteboard_admin/static/web/js/whiteboard/themes.js
Unchanged (verified): sync.js serializeForServer/applySyncUpdate, app.js createObjectFromData, objects.js per-type readers, and connectors.js pass the color string through verbatim to Konva .fill()/.stroke(), which accept 8-digit hex — so persistence, WebSocket sync, and undo/redo carry the alpha for free.
Behavior after change
Tests