From 1a6bcaa08d21ae388dd99589290ae9acc03232e3 Mon Sep 17 00:00:00 2001 From: Kresna Date: Fri, 31 Jul 2026 23:13:06 +0700 Subject: [PATCH 1/2] docs(watermark): scale slider design spec Co-Authored-By: Claude Opus 4.8 (1M context) --- ...026-07-31-watermark-scale-slider-design.md | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-31-watermark-scale-slider-design.md diff --git a/docs/superpowers/specs/2026-07-31-watermark-scale-slider-design.md b/docs/superpowers/specs/2026-07-31-watermark-scale-slider-design.md new file mode 100644 index 0000000..b0f93f1 --- /dev/null +++ b/docs/superpowers/specs/2026-07-31-watermark-scale-slider-design.md @@ -0,0 +1,72 @@ +# Watermark Scale Slider — Design + +**Date:** 2026-07-31 +**Tool:** Image → Watermark (`/tools/image-watermark`) +**Type:** Improvement (UX control) + +## Problem + +The Watermark tool exposes size as three preset buttons (Small `1/16`, Medium `1/10`, +Large `1/6`). Users want continuous control over the watermark size, where a larger +value produces both a bigger font **and** a wider gap between tiled repetitions. + +Note: color (font color picker) and transparency (opacity slider) **already exist** in +the tool — this change is scoped to the size control only. + +## Goal + +Replace the three Size buttons with a continuous **Scale** slider, matching the visual +style of the existing Opacity slider, with a live readout. + +## Design + +### UI (island: `src/islands/image/ImageWatermark.tsx`) + +- Remove the `SIZES` preset buttons block. +- Add a range slider labelled **Scale** with a live `NN%` readout, styled like the + existing Opacity range input (`type="range"`, `accent-accent`). +- State: `const [scale, setScale] = useState(30)` (percent, 1–100). Default 30%. +- The slider value (percent) maps to the library's `fontScale` fraction: + + ``` + const MIN_FS = 1 / 24; // ≈ 0.0417 (narrow) + const MAX_FS = 1 / 4; // 0.25 (big) + fontScale = MIN_FS + (scale / 100) * (MAX_FS - MIN_FS); + ``` + + 30% → ≈ 0.104 (close to the old "Medium" default, so behavior is familiar). + +### Library (`src/tools/image/canvas.lib.ts`) + +No signature change. `watermarkImage` already accepts a continuous `fontScale`, and the +tiled layout already derives its gap from font size: + +``` +const stepX = textWidth + fontSize * 2; +const stepY = fontSize * 4; +``` + +So a larger `fontScale` already yields a bigger font **and** proportionally wider gaps — +"smaller = narrow, bigger = big font & wider gap" is satisfied by the existing math. + +We add a **helper + test** to lock the mapping so the contract can't silently regress: + +- Export a pure `scaleToFontScale(percent: number): number` from `canvas.lib.ts` + implementing the mapping above (clamped to 1–100). This keeps the magic numbers in + the tested lib rather than the island. + +## Testing + +`src/tools/image/canvas.lib.test.ts` (extend): + +- `scaleToFontScale(1)` ≈ `1/24`; `scaleToFontScale(100)` === `0.25`. +- Monotonic: `scaleToFontScale(20) < scaleToFontScale(80)`. +- Clamps: `scaleToFontScale(0)` === `scaleToFontScale(1)`; `scaleToFontScale(150)` === `scaleToFontScale(100)`. + +Island is covered by build + manual smoke (slider moves, watermark grows, tiled gap widens). + +## Out of scope + +- Color and opacity controls (already shipped). +- Layout options (Diagonal / Tiled / Corner) — unchanged. +- No change to output format handling. From 21a87ad9b8b4fa1334bb24c8669240fa07a5315b Mon Sep 17 00:00:00 2001 From: Kresna Date: Fri, 31 Jul 2026 23:15:26 +0700 Subject: [PATCH 2/2] feat(watermark): continuous scale slider replacing size presets Swap the three Small/Medium/Large size buttons for a continuous Scale slider (1-100%), mapped to fontScale 1/24..1/4 via a new tested scaleToFontScale() helper. Larger scale yields a bigger font and, because the tiled layout derives its gap from font size, proportionally wider gaps. Color and opacity controls are unchanged (already shipped). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/islands/image/ImageWatermark.tsx | 41 +++++++++++----------------- src/tools/image/canvas.lib.test.ts | 17 +++++++++++- src/tools/image/canvas.lib.ts | 12 ++++++++ 3 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/islands/image/ImageWatermark.tsx b/src/islands/image/ImageWatermark.tsx index 7582694..be6a7b6 100644 --- a/src/islands/image/ImageWatermark.tsx +++ b/src/islands/image/ImageWatermark.tsx @@ -3,7 +3,7 @@ import { Dropzone } from '@/components/ui/Dropzone'; import { Button } from '@/components/ui/Button'; import { Alert } from '@/components/ui/Alert'; import { ImageResult } from '@/components/ui/ImageResult'; -import { watermarkImage, keepFormat, type WatermarkLayout } from '@/tools/image/canvas.lib'; +import { watermarkImage, keepFormat, scaleToFontScale, type WatermarkLayout } from '@/tools/image/canvas.lib'; import { usePasteImage } from '@/hooks/usePasteImage'; const LAYOUTS: { value: WatermarkLayout; label: string }[] = [ @@ -12,17 +12,11 @@ const LAYOUTS: { value: WatermarkLayout; label: string }[] = [ { value: 'bottom-right', label: 'Corner' }, ]; -const SIZES = [ - { value: 1 / 16, label: 'Small' }, - { value: 1 / 10, label: 'Medium' }, - { value: 1 / 6, label: 'Large' }, -]; - export default function ImageWatermark() { const [file, setFile] = useState(null); const [text, setText] = useState('© GoodWebTools'); const [layout, setLayout] = useState('diagonal'); - const [fontScale, setFontScale] = useState(1 / 10); + const [scale, setScale] = useState(30); const [opacity, setOpacity] = useState(60); const [color, setColor] = useState('#808080'); const [result, setResult] = useState(null); @@ -50,7 +44,7 @@ export default function ImageWatermark() { const { blob } = await watermarkImage(file, { text: text.trim(), layout, - fontScale, + fontScale: scaleToFontScale(scale), opacity: opacity / 100, color, }); @@ -102,23 +96,20 @@ export default function ImageWatermark() { -
- - Size +
+ setScale(Number(e.target.value))} + className="w-full accent-accent" + /> +