Details & Tips
Technical implementation:
**Image loading and scaling:** The file input's `@change` event reads the selected file using `FileReader.readAsDataURL()`. A new `Image` object is created, and once its `onload` fires, the image is drawn onto the main canvas. The scaling logic preserves aspect ratio: `scale = Math.min(1, maxDisplayWidth / img.width)`. The canvas dimensions are set to `Math.round(img.width * scale)` × `Math.round(img.height * scale)`, and `ctx.drawImage(img, 0, 0, canvas.width, canvas.height)` renders the scaled image. This ensures the canvas bitmap matches what the user sees — there is no mismatch between the displayed pixel grid and the sampled coordinates.
**Pixel reading:** On each `@mousemove` event (when not locked), the cursor position relative to the canvas is computed using `canvas.getBoundingClientRect()` and `event.clientX`/`clientY`. The coordinates are floored to the nearest integer pixel. The call `canvas.getContext('2d').getImageData(x, y, 1, 1).data` returns a 4-element array `[R, G, B, A]`. Channels are already clamped to 0-255 by the canvas API, so no additional clamping is needed. The alpha channel is ignored (since images drawn with `drawImage` are always fully opaque on the canvas unless the source image had an alpha channel — in practice, JPEG files have no alpha, and PNG files with transparency will show the alpha; the tool reads the RGB as displayed on canvas, which for transparent areas would show the canvas's default black background).
**Zoom lens:** A separate small canvas (e.g., 150×150 pixels) acts as the zoom preview. When the mouse moves, the tool extracts a 15×15 pixel region centered on the cursor using `getImageData(cx-7, cy-7, 15, 15)`. The zoom canvas is cleared and the 15×15 block is rendered scaled up by 10× using `ctx.putImageData()` on an intermediate canvas first, then `ctx.drawImage()` with `imageSmoothingEnabled = false` to get blocky pixel-perfect scaling. A crosshair is drawn as two 1-pixel lines: one horizontal and one vertical through the centre, in an inverted colour (computed as `255 - R, 255 - G, 255 - B` of the centre pixel) for maximum visibility.
**HEX/RGB/HSL display:** The picked colour's RGB values are converted to a 6-digit hex string using `.toString(16).padStart(2, '0')` and to HSL using the standard RGB-to-HSL formulas (normalise to 0-1, compute max/min, derive hue from the dominant channel with offsets of 0, 2, or 4, lightness from (max+min)/2, saturation from the difference divided by (1 - |2L-1|)). All three formats are displayed simultaneously.
**Copy behaviour:** Clicking the Copy button next to the hex value triggers `navigator.clipboard.writeText(hexString)`. On success, the button text changes to "Copied!" and resets after 2 seconds via `setTimeout`.
**Lock/unlock:** A boolean `locked` flag in the Alpine state controls whether `@mousemove` updates the colour. Clicking the canvas toggles `locked`. When locked, the cursor on the canvas changes to `cursor: crosshair` (or a custom lock cursor) to indicate the locked state. The picked colour display shows a small lock icon (🔒 text or an SVG).
**Error handling:** If the selected file's MIME type does not start with `image/`, an inline error message is shown and no canvas drawing occurs. If the `Image.onerror` handler fires (e.g., corrupt file), an error is also shown. The error uses the standard inline error pattern: `<p x-show="error" x-cloak class="mt-3 rounded-lg border border-red-200 bg-red-50 px-4 py-2 text-sm text-red-600" x-text="error"></p>`.
**Performance:** Canvas `getImageData()` is fast for single-pixel reads. The zoom lens reads a 15×15 block — 225 pixels — which is well within the performance budget even on older hardware. No debouncing is needed on mouse move; Alpine's DOM updates are batched by the browser's microtask queue. The image is only read once during the file input phase; subsequent interactions are all canvas reads, not image re-decodes.
**Use cases:** Extracting brand colours from a competitor's screenshot, sampling a colour from a photo to use in CSS, building a mood board palette, checking if two images use the same shade, identifying the exact colour of a product in a catalogue image.