eyedropper tool (I) — sample colors via the native picker

A new ⊙ button in the tool palette and the I keyboard shortcut open
the browser's window.EyeDropper picker so the user can sample any
pixel anywhere on screen (not just our canvas) and apply the
sampled hex to a smart target:

- Selected shape layer → fill color (Shift = stroke color).
- Selected text layer  → text color.
- No layer selected    → pen / shape tool's default fill (or
  stroke), so the next thing drawn picks up the sampled hue.

The eyedropper is a transient action rather than a persistent tool —
invoking it doesn't change the active tool, so users can sample
mid-edit without losing their cursor. Falls back to a hint toast in
browsers that don't yet support window.EyeDropper (Safari).
This commit is contained in:
thePR0M3TH3AN committed 2026-06-02 08:41:06 -04:00
1 parent 0cb028c3f1
commit cb130b82cd
1 file changed
+51
+51
View File
@@ -4042,6 +4042,7 @@
<button type="button" class="toolPaletteBtn" id="tool-pencil" data-tool="pencil" title="Pencil tool (B)" aria-label="Pencil tool">✎</button>
<button type="button" class="toolPaletteBtn" id="tool-shape" data-tool="shape" title="Shape tool (S)" aria-label="Shape tool">▢</button>
<button type="button" class="toolPaletteBtn" id="tool-text" data-tool="text" title="Text tool (T)" aria-label="Text tool">T</button>
<button type="button" class="toolPaletteBtn" id="tool-eyedropper" data-tool="eyedropper" title="Eyedropper (I) — sample a color from anywhere on screen" aria-label="Eyedropper tool">⊙</button>
<div class="pixelToolWrap">
<button type="button" class="toolPaletteBtn" id="tool-pixel" title="Pixel art layer" aria-haspopup="true" aria-expanded="false" aria-label="Pixel art layer">▦</button>
<div id="pixelToolMenu" class="pixelToolMenu">
@@ -9534,6 +9535,50 @@
render();
}
// Eyedropper: invoke the browser's native EyeDropper picker so
// the user can sample any pixel anywhere on screen (not just our
// canvas). Applies the picked hex to the most-relevant target:
// shape layer → fill (or stroke if Shift was held)
// text layer → text color
// nothing → the pen / shape tool's default fill colour so
// the next thing drawn picks up the sampled hue.
// Not supported in Safari yet — show a hint toast there.
async function invokeEyedropper(toStroke) {
if (typeof window.EyeDropper !== 'function') {
toastError?.('Eyedropper needs Chrome, Edge, or Firefox 124+. Safari support is still pending.');
return;
}
let hex = '';
try {
const res = await new window.EyeDropper().open();
hex = (res && res.sRGBHex) || '';
} catch (err) {
// User pressed Escape — silent abort.
return;
}
if (!hex) return;
const layer = selectedLayer?.();
if (layer && layer.type === 'shape') {
pushHistory('eyedropper');
if (toStroke) layer.stroke = { type: 'none', color: '#000000', width: 2, dash: 'solid', ...(layer.stroke || {}), type: 'solid', color: hex };
else layer.fill = { ...(layer.fill || {}), type: 'solid', color: hex };
render();
} else if (layer && layer.type === 'text') {
pushHistory('eyedropper');
layer.color = hex;
render();
} else {
// No layer selected — stash the color on the active drawing
// tool's defaults so the next stroke / fill uses it.
if (toStroke) _penToolOpts.penStrokeColor = hex;
else _penToolOpts.penFillColor = hex;
_shapeToolOpts.fillColor = toStroke ? _shapeToolOpts.fillColor : hex;
_shapeToolOpts.strokeColor = toStroke ? hex : _shapeToolOpts.strokeColor;
syncToolOptionInputs?.();
toast?.(`Sampled ${hex} — applied to current ${toStroke ? 'stroke' : 'fill'} color.`);
}
}
function syncPenOverlay() {
if (_penOverlay && _penOverlay.parentNode) _penOverlay.parentNode.removeChild(_penOverlay);
_penOverlay = null;
@@ -30053,6 +30098,11 @@
for (const t of EDITOR_TOOLS) {
on(`tool-${t}`, 'click', () => setEditorTool(t));
}
// Eyedropper is a transient action, not a persistent tool — its
// button immediately opens the native picker and the previous
// tool stays active. Shift-click samples into the stroke color
// instead of the fill.
on('tool-eyedropper', 'click', (e) => invokeEyedropper(!!e.shiftKey));
on('penFinishBtn', 'click', () => { finishPenPath(false); setEditorTool('select'); });
on('penCancelBtn', 'click', () => setEditorTool('select'));
// Path actions side-panel buttons. Each grabs the currently
@@ -30191,6 +30241,7 @@
if (k === 'b') { setEditorTool('pencil'); return; }
if (k === 's') { setEditorTool('shape'); return; }
if (k === 't') { setEditorTool('text'); return; }
if (k === 'i') { invokeEyedropper(e.shiftKey); return; }
if (k === 'enter' && _penDraft && _penDraft.tool === 'pen') { e.preventDefault(); finishPenPath(false); return; }
if (!_penDraft) {
if (k === 'delete' || k === 'backspace') {