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