feat: support image background editor [wip]
parent
8b5657e1ce
commit
3c83a322b6
@ -0,0 +1,75 @@
|
||||
import { getSelectedElements, isSomeElementSelected } from "../scene";
|
||||
import { ToolButton } from "../components/ToolButton";
|
||||
import { backgroundIcon } from "../components/icons";
|
||||
import { register } from "./register";
|
||||
import { getNonDeletedElements } from "../element";
|
||||
import { isInitializedImageElement } from "../element/typeChecks";
|
||||
import Scene from "../scene/Scene";
|
||||
|
||||
export const actionEditImageAlpha = register({
|
||||
name: "editImageAlpha",
|
||||
perform: async (elements, appState, _, app) => {
|
||||
if (appState.editingImageElement) {
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
editingImageElement: null,
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
}
|
||||
|
||||
const selectedElements = getSelectedElements(elements, appState);
|
||||
const selectedElement = selectedElements[0];
|
||||
if (
|
||||
selectedElements.length === 1 &&
|
||||
isInitializedImageElement(selectedElement)
|
||||
) {
|
||||
const imgData = app.imageCache.get(selectedElement.fileId);
|
||||
if (!imgData) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const image = await imgData.image;
|
||||
const { width, height } = image;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.height = height;
|
||||
canvas.width = width;
|
||||
const context = canvas.getContext("2d")!;
|
||||
|
||||
context.drawImage(image, 0, 0, width, height);
|
||||
|
||||
const imageData = context.getImageData(0, 0, width, height);
|
||||
|
||||
Scene.mapElementToScene(selectedElement.id, app.scene);
|
||||
|
||||
return {
|
||||
appState: {
|
||||
...appState,
|
||||
editingImageElement: {
|
||||
editorType: "alpha",
|
||||
elementId: selectedElement.id,
|
||||
origImageData: imageData,
|
||||
imageData,
|
||||
pointerDownState: { screenX: 0, screenY: 0, sampledPixel: null },
|
||||
},
|
||||
},
|
||||
commitToHistory: false,
|
||||
};
|
||||
}
|
||||
return false;
|
||||
},
|
||||
PanelComponent: ({ elements, appState, updateData }) => (
|
||||
<ToolButton
|
||||
type="button"
|
||||
icon={backgroundIcon}
|
||||
label="Edit Image Alpha"
|
||||
className={appState.editingImageElement ? "active" : ""}
|
||||
title={"Edit image alpha"}
|
||||
aria-label={"Edit image alpha"}
|
||||
onClick={() => updateData(null)}
|
||||
visible={isSomeElementSelected(getNonDeletedElements(elements), appState)}
|
||||
/>
|
||||
),
|
||||
});
|
@ -0,0 +1,112 @@
|
||||
import { distance2d } from "../math";
|
||||
import Scene from "../scene/Scene";
|
||||
import {
|
||||
ExcalidrawImageElement,
|
||||
InitializedExcalidrawImageElement,
|
||||
} from "./types";
|
||||
|
||||
export type EditingImageElement = {
|
||||
editorType: "alpha";
|
||||
elementId: ExcalidrawImageElement["id"];
|
||||
origImageData: Readonly<ImageData>;
|
||||
imageData: ImageData;
|
||||
pointerDownState: {
|
||||
screenX: number;
|
||||
screenY: number;
|
||||
sampledPixel: readonly [number, number, number, number] | null;
|
||||
};
|
||||
};
|
||||
|
||||
const getElement = (id: EditingImageElement["elementId"]) => {
|
||||
const element = Scene.getScene(id)?.getNonDeletedElement(id);
|
||||
if (element) {
|
||||
return element as InitializedExcalidrawImageElement;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export class ImageEditor {
|
||||
static handlePointerDown(
|
||||
editingElement: EditingImageElement,
|
||||
scenePointer: { x: number; y: number },
|
||||
) {
|
||||
const imageElement = getElement(editingElement.elementId);
|
||||
|
||||
if (imageElement) {
|
||||
if (
|
||||
scenePointer.x >= imageElement.x &&
|
||||
scenePointer.x <= imageElement.x + imageElement.width &&
|
||||
scenePointer.y >= imageElement.y &&
|
||||
scenePointer.y <= imageElement.y + imageElement.height
|
||||
) {
|
||||
editingElement.pointerDownState.screenX = scenePointer.x;
|
||||
editingElement.pointerDownState.screenY = scenePointer.y;
|
||||
|
||||
const { width, height, data } = editingElement.origImageData;
|
||||
|
||||
const imageOffsetX = Math.round(
|
||||
(scenePointer.x - imageElement.x) * (width / imageElement.width),
|
||||
);
|
||||
const imageOffsetY = Math.round(
|
||||
(scenePointer.y - imageElement.y) * (height / imageElement.height),
|
||||
);
|
||||
|
||||
const sampledPixel = [
|
||||
data[(imageOffsetY * width + imageOffsetX) * 4 + 0],
|
||||
data[(imageOffsetY * width + imageOffsetX) * 4 + 1],
|
||||
data[(imageOffsetY * width + imageOffsetX) * 4 + 2],
|
||||
data[(imageOffsetY * width + imageOffsetX) * 4 + 3],
|
||||
] as const;
|
||||
|
||||
editingElement.pointerDownState.sampledPixel = sampledPixel;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static handlePointerMove(
|
||||
editingElement: EditingImageElement,
|
||||
scenePointer: { x: number; y: number },
|
||||
) {
|
||||
const { sampledPixel } = editingElement.pointerDownState;
|
||||
if (sampledPixel) {
|
||||
const { screenX, screenY } = editingElement.pointerDownState;
|
||||
const distance = distance2d(
|
||||
scenePointer.x,
|
||||
scenePointer.y,
|
||||
screenX,
|
||||
screenY,
|
||||
);
|
||||
|
||||
const { width, height, data } = editingElement.origImageData;
|
||||
const newImageData = new ImageData(width, height);
|
||||
|
||||
for (let x = 0; x < width; ++x) {
|
||||
for (let y = 0; y < height; ++y) {
|
||||
if (
|
||||
Math.abs(sampledPixel[0] - data[(y * width + x) * 4 + 0]) +
|
||||
Math.abs(sampledPixel[1] - data[(y * width + x) * 4 + 1]) +
|
||||
Math.abs(sampledPixel[2] - data[(y * width + x) * 4 + 2]) <
|
||||
distance
|
||||
) {
|
||||
newImageData.data[(y * width + x) * 4 + 0] = 0;
|
||||
newImageData.data[(y * width + x) * 4 + 1] = 255;
|
||||
newImageData.data[(y * width + x) * 4 + 2] = 0;
|
||||
newImageData.data[(y * width + x) * 4 + 3] = 0;
|
||||
} else {
|
||||
for (let p = 0; p < 4; ++p) {
|
||||
newImageData.data[(y * width + x) * 4 + p] =
|
||||
data[(y * width + x) * 4 + p];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newImageData;
|
||||
}
|
||||
}
|
||||
|
||||
static handlePointerUp(editingElement: EditingImageElement) {
|
||||
editingElement.pointerDownState.sampledPixel = null;
|
||||
editingElement.origImageData = editingElement.imageData;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue