Files
odysseus/static/js/editor/tools/eyedropper.js
T
pewdiepie-archdaemonandClaude Opus 5.5 5fac693c1c feat(photo-editor): Ajax Image Editor client (Photoshop-parity tools)
Photoshop-parity editor client: layers, selection/masking, brushes,
smart filters, LUT import, Quick Edit, mobile gestures, workspaces,
batch operations, and the service-worker versioning (v978->v1006).

Source: vendor/photocraft (ad1cff44) photo-editor client files, merged
onto upstream/dev (a8c147b2). Conflict resolution documented in
pr_work/A_conflicts.md; editor files taken from vendor/photocraft
(upstream's editor is an earlier snapshot of the same feature).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 11:57:35 +00:00

82 lines
2.8 KiB
JavaScript

import { state } from '../state.js';
import { canvasCoords } from '../canvas-coords.js';
function hexByte(value) {
return Math.max(0, Math.min(255, value)).toString(16).padStart(2, '0');
}
// Average an N-by-N block centred on (cx, cy), clamped to the canvas edges.
// Photoshop's "Sample Size" option: 1 = Point Sample, 3/5/11/31/51 = N-by-N.
function averageBlock(ctx, cx, cy, size, width, height) {
const half = Math.floor(size / 2);
const x0 = Math.max(0, cx - half);
const y0 = Math.max(0, cy - half);
const x1 = Math.min(width - 1, cx + half);
const y1 = Math.min(height - 1, cy + half);
const w = x1 - x0 + 1;
const h = y1 - y0 + 1;
if (w <= 0 || h <= 0) return null;
const data = ctx.getImageData(x0, y0, w, h).data;
let r = 0, g = 0, b = 0, a = 0;
const n = data.length / 4;
for (let i = 0; i < data.length; i += 4) { r += data[i]; g += data[i + 1]; b += data[i + 2]; a += data[i + 3]; }
return { r: Math.round(r / n), g: Math.round(g / n), b: Math.round(b / n), a: a / n };
}
export function createEyedropperTool({ activeLayer, composite, syncColor, syncPreview, showToast }) {
function sample(e, refreshComposite = false) {
const point = canvasCoords(e, state.mainCanvas);
let canvas = state.mainCanvas;
let x = point.x;
let y = point.y;
if (state.eyedropperSample === 'layer') {
const layer = activeLayer();
if (!layer?.canvas) return null;
const off = state.layerOffsets.get(layer.id) || { x: 0, y: 0 };
canvas = layer.canvas;
x -= off.x;
y -= off.y;
} else if (refreshComposite) {
composite();
}
const ix = Math.floor(x);
const iy = Math.floor(y);
if (ix < 0 || iy < 0 || ix >= canvas.width || iy >= canvas.height) return null;
const ctx = canvas.getContext('2d');
const size = Math.max(1, Math.round(Number(state.eyedropperSampleSize) || 1));
let r, g, b, a;
if (size <= 1) {
const pixel = ctx.getImageData(ix, iy, 1, 1).data;
r = pixel[0]; g = pixel[1]; b = pixel[2]; a = pixel[3];
} else {
const avg = averageBlock(ctx, ix, iy, size, canvas.width, canvas.height);
if (!avg) return null;
r = avg.r; g = avg.g; b = avg.b; a = avg.a;
}
const rgb = [r, g, b];
if (!a) return { color: null, rgb, canvas, x, y };
return { color: `#${hexByte(r)}${hexByte(g)}${hexByte(b)}`, rgb, canvas, x, y };
}
return {
preview(e) {
const result = sample(e);
syncPreview?.(result?.color || null, result);
return result?.color || null;
},
clearPreview() {
syncPreview?.(null, null);
},
pick(e) {
const result = sample(e, true);
const color = result?.color;
if (!color) {
showToast?.('That pixel is transparent');
return;
}
state.color = color;
syncColor?.(state.color);
syncPreview?.(state.color, result);
},
};
}