Compare commits
2 Commits
master
...
devolved-i
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b20fe944e7 | ||
![]() |
1156ef6b96 |
@ -3453,11 +3453,40 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private handleCanvasImageDrop = async (
|
||||||
|
event: React.DragEvent<HTMLCanvasElement>,
|
||||||
|
file: File,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const shapes = await (
|
||||||
|
await import(
|
||||||
|
/* webpackChunkName: "pixelated-image" */ "../data/pixelated-image"
|
||||||
|
)
|
||||||
|
).pixelateImage(file, 20, 1200, event.clientX, event.clientY);
|
||||||
|
|
||||||
|
const nextElements = [
|
||||||
|
...this.scene.getElementsIncludingDeleted(),
|
||||||
|
...shapes,
|
||||||
|
];
|
||||||
|
|
||||||
|
this.scene.replaceAllElements(nextElements);
|
||||||
|
} catch (error) {
|
||||||
|
return this.setState({
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: error.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private handleCanvasOnDrop = async (
|
private handleCanvasOnDrop = async (
|
||||||
event: React.DragEvent<HTMLCanvasElement>,
|
event: React.DragEvent<HTMLCanvasElement>,
|
||||||
) => {
|
) => {
|
||||||
|
let imageFile: File | null = null;
|
||||||
try {
|
try {
|
||||||
const file = event.dataTransfer.files[0];
|
const file = event.dataTransfer.files[0];
|
||||||
|
if (file?.type.indexOf("image/") === 0) {
|
||||||
|
imageFile = file;
|
||||||
|
}
|
||||||
if (file?.type === "image/png" || file?.type === "image/svg+xml") {
|
if (file?.type === "image/png" || file?.type === "image/svg+xml") {
|
||||||
const { elements, appState } = await loadFromBlob(file, this.state);
|
const { elements, appState } = await loadFromBlob(file, this.state);
|
||||||
this.syncActionResult({
|
this.syncActionResult({
|
||||||
@ -3469,8 +3498,13 @@ class App extends React.Component<ExcalidrawProps, AppState> {
|
|||||||
commitToHistory: true,
|
commitToHistory: true,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
} else if (imageFile) {
|
||||||
|
return await this.handleCanvasImageDrop(event, imageFile);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (imageFile) {
|
||||||
|
return await this.handleCanvasImageDrop(event, imageFile);
|
||||||
|
}
|
||||||
return this.setState({
|
return this.setState({
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
errorMessage: error.message,
|
errorMessage: error.message,
|
||||||
|
106
src/data/pixelated-image.ts
Normal file
106
src/data/pixelated-image.ts
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import { ExcalidrawGenericElement, NonDeleted } from "../element/types";
|
||||||
|
import { newElement } from "../element";
|
||||||
|
import { DEFAULT_FONT_FAMILY, DEFAULT_FONT_SIZE } from "../constants";
|
||||||
|
import { randomId } from "../random";
|
||||||
|
|
||||||
|
const loadImage = async (url: string): Promise<HTMLImageElement> => {
|
||||||
|
const image = new Image();
|
||||||
|
return new Promise<HTMLImageElement>((resolve, reject) => {
|
||||||
|
image.onload = () => resolve(image);
|
||||||
|
image.onerror = (err) =>
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
`Failed to load image: ${err ? err.toString : "unknown error"}`,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
image.onabort = () =>
|
||||||
|
reject(new Error(`Failed to load image: image load aborted`));
|
||||||
|
image.src = url;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const commonProps = {
|
||||||
|
fillStyle: "solid",
|
||||||
|
fontFamily: DEFAULT_FONT_FAMILY,
|
||||||
|
fontSize: DEFAULT_FONT_SIZE,
|
||||||
|
opacity: 100,
|
||||||
|
roughness: 1,
|
||||||
|
strokeColor: "transparent",
|
||||||
|
strokeSharpness: "sharp",
|
||||||
|
strokeStyle: "solid",
|
||||||
|
strokeWidth: 1,
|
||||||
|
verticalAlign: "middle",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const pixelateImage = async (
|
||||||
|
blob: Blob,
|
||||||
|
cellSize: number,
|
||||||
|
suggestedMaxShapeCount: number,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
) => {
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
try {
|
||||||
|
const image = await loadImage(url);
|
||||||
|
|
||||||
|
// initialize canvas for pixelation
|
||||||
|
const { width, height } = image;
|
||||||
|
let canvasWidth = Math.floor(width / cellSize);
|
||||||
|
let canvasHeight = Math.floor(height / cellSize);
|
||||||
|
const shapeCount = canvasHeight * canvasWidth;
|
||||||
|
if (shapeCount > suggestedMaxShapeCount) {
|
||||||
|
canvasWidth = Math.floor(
|
||||||
|
canvasWidth * (suggestedMaxShapeCount / shapeCount),
|
||||||
|
);
|
||||||
|
canvasHeight = Math.floor(
|
||||||
|
canvasHeight * (suggestedMaxShapeCount / shapeCount),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const xOffset = x - (canvasWidth * cellSize) / 2;
|
||||||
|
const yOffset = y - (canvasHeight * cellSize) / 2;
|
||||||
|
|
||||||
|
const canvas =
|
||||||
|
"OffscreenCanvas" in window
|
||||||
|
? new OffscreenCanvas(canvasWidth, canvasHeight)
|
||||||
|
: document.createElement("canvas");
|
||||||
|
canvas.width = canvasWidth;
|
||||||
|
canvas.height = canvasHeight;
|
||||||
|
|
||||||
|
// Draw image on canvas
|
||||||
|
const ctx = canvas.getContext("2d")!;
|
||||||
|
ctx.drawImage(image, 0, 0, width, height, 0, 0, canvasWidth, canvasHeight);
|
||||||
|
const imageData = ctx.getImageData(0, 0, canvasWidth, canvasHeight);
|
||||||
|
const buffer = imageData.data;
|
||||||
|
|
||||||
|
const groupId = randomId();
|
||||||
|
const shapes: NonDeleted<ExcalidrawGenericElement>[] = [];
|
||||||
|
|
||||||
|
for (let row = 0; row < canvasHeight; row++) {
|
||||||
|
for (let col = 0; col < canvasWidth; col++) {
|
||||||
|
const offset = row * canvasWidth * 4 + col * 4;
|
||||||
|
const r = buffer[offset];
|
||||||
|
const g = buffer[offset + 1];
|
||||||
|
const b = buffer[offset + 2];
|
||||||
|
const alpha = buffer[offset + 3];
|
||||||
|
if (alpha) {
|
||||||
|
const color = `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||||
|
const rectangle = newElement({
|
||||||
|
backgroundColor: color,
|
||||||
|
groupIds: [groupId],
|
||||||
|
...commonProps,
|
||||||
|
type: "rectangle",
|
||||||
|
x: xOffset + col * cellSize,
|
||||||
|
y: yOffset + row * cellSize,
|
||||||
|
width: cellSize,
|
||||||
|
height: cellSize,
|
||||||
|
});
|
||||||
|
shapes.push(rectangle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shapes;
|
||||||
|
} finally {
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user