From 1156ef6b96c32799000e291b67d5ac5df248c66b Mon Sep 17 00:00:00 2001 From: Preet Shihn Date: Thu, 4 Feb 2021 23:27:35 -0800 Subject: [PATCH] pixelated images initial experiment --- src/components/App.tsx | 34 ++++++++++++++ src/data/pixelated-image.ts | 92 +++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) create mode 100644 src/data/pixelated-image.ts diff --git a/src/components/App.tsx b/src/components/App.tsx index fd6759fbe..a71fd78c8 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -3453,11 +3453,40 @@ class App extends React.Component { } }; + private handleCanvasImageDrop = async ( + event: React.DragEvent, + file: File, + ) => { + try { + const shapes = await ( + await import( + /* webpackChunkName: "pixelated-image" */ "../data/pixelated-image" + ) + ).pixelateImage(file, 20, 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 ( event: React.DragEvent, ) => { + let imageFile: File | null = null; try { const file = event.dataTransfer.files[0]; + if (file?.type.indexOf("image/") === 0) { + imageFile = file; + } if (file?.type === "image/png" || file?.type === "image/svg+xml") { const { elements, appState } = await loadFromBlob(file, this.state); this.syncActionResult({ @@ -3469,8 +3498,13 @@ class App extends React.Component { commitToHistory: true, }); return; + } else if (imageFile) { + return await this.handleCanvasImageDrop(event, imageFile); } } catch (error) { + if (imageFile) { + return await this.handleCanvasImageDrop(event, imageFile); + } return this.setState({ isLoading: false, errorMessage: error.message, diff --git a/src/data/pixelated-image.ts b/src/data/pixelated-image.ts new file mode 100644 index 000000000..46161e892 --- /dev/null +++ b/src/data/pixelated-image.ts @@ -0,0 +1,92 @@ +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 => { + const image = new Image(); + return new Promise((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, + x: number, + y: number, +) => { + const url = URL.createObjectURL(blob); + try { + const image = await loadImage(url); + + // initialize canvas for pixelation + const { width, height } = image; + const canvasWidth = Math.floor(width / cellSize); + const canvasHeight = Math.floor(height / cellSize); + 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[] = []; + + 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: x + col * cellSize, + y: y + row * cellSize, + width: cellSize, + height: cellSize, + }); + shapes.push(rectangle); + } + } + } + return shapes; + } finally { + URL.revokeObjectURL(url); + } +};