mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-16 03:42:20 +02:00
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>
82 lines
2.8 KiB
JavaScript
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);
|
|
},
|
|
};
|
|
}
|