Details & Tips
Full algorithm walkthrough:
**1. Image loading and scaling:** Same as the other canvas tools — FileReader → Image object → drawImage onto offscreen canvas, capped at 900px max width.
**2. Dominant colour extraction (same as palette extractor):**
- Sample stride: 6 pixels in both dimensions.
- Alpha threshold: skip pixels with A < 128.
- Binning: each channel reduced to 4 bits (`R >> 4`, etc.), bin index = `(r4 << 8) | (g4 << 4) | b4`.
- Accumulate counts and channel sums per bin.
- Sort bins by count descending, take top 5.
- Compute average colour per top bin: `Math.round(sum / count)` per channel.
- Output: 5 colours as `{ hex, r, g, b, count }` objects.
**3. Role assignment logic:**
- Sort the 5 extracted colours by `count` descending.
- Primary = colours[0], Secondary = colours[1].
- Accent: compute HSL saturation for each of the 5. Pick the one with the highest S value (using the standard RGB-to-HSL saturation formula on the average colour of each bin).
- Background: compute HSL lightness for each of the 5. Pick the one with highest L. If max L < 85, create a derived background: take the average hue of the 5 colours, set S = 5%, L = 95%, then convert back to RGB → hex. This produces a very light, warm/cool tinted off-white that harmonises with the palette.
- Text: compute HSL lightness for each of the 5. Pick the one with lowest L. If min L > 25, create a derived text colour: take the average hue, set S = 3%, L = 10%, producing a near-black with a subtle tint matching the palette.
**4. Variant generation (tints and shades in HSL):**
For each of Primary, Secondary, and Accent:
- Convert to HSL.
- **Light variant:** L_light = Math.min(100, L_base + 30). This shifts lightness ~30 points toward white. Convert back to RGB → hex.
- **Dark variant:** L_dark = Math.max(0, L_base - 30). Shifts lightness ~30 points toward black. Convert back to RGB → hex.
If the base colour is already very light (L > 85), the Light variant clamps at 100 (white). If very dark (L < 15), the Dark variant clamps at 0 (black).
- **Neutral:** Average the hues of all 5 extracted colours. Set S = 8%, L = 65% (a mid-grey with a subtle warm/cool tint from the palette).
**5. Display grid:** 12 swatches in a 4-column grid (3 columns on small screens). Each swatch card contains:
- A colour square (approx 48×48px with rounded corners).
- A label: "Primary", "Secondary", "Accent", "Background", "Text", "Primary Light", "Primary Dark", "Secondary Light", "Secondary Dark", "Accent Light", "Accent Dark", "Neutral".
- The hex code in monospace text below.
- Click handler: copy hex to clipboard.
**6. Preview card:** A dedicated section below or beside the swatch grid. It renders a small card element styled inline with CSS variables derived from the scheme:
- Card background: Background colour.
- Card border: a slightly darker shade (Neutral colour at reduced opacity).
- Title text: Text colour, bold font.
- Body text: Text colour at ~70% opacity.
- Button: Primary colour background, Text colour (or white if Text is dark) for label, rounded corners.
The preview card is purely visual — it is a styled `<div>`, not a canvas, using inline `style` attributes bound to Alpine data.
**Edge cases:**
- Image with fewer than 5 distinct colours: the palette extraction returns whatever non-zero bins exist. The tool displays only as many swatches as there are colours, and the preview card adapts.
- All colours similar lightness: Background and Text assignments may use derived colours. This is correct — the tool should not force a mid-tone as background when the user's image is genuinely all mid-tones.
- Highly saturated image: Accent and Primary might be the same colour. In this case, Accent defaults to the colour with second-highest saturation.
- Solid black or white image: the tool produces a grayscale scheme.
**Performance:** The extraction uses the same fast binning algorithm as the palette extractor. The 12-colour grid and preview card are pure DOM rendering — no canvas work beyond the initial extraction. Total processing time is well under 100ms.