Pārlūkot izejas kodu

pixelated images initial experiment

Preet Shihn 4 gadi atpakaļ
vecāks
revīzija
1156ef6b96
2 mainītis faili ar 126 papildinājumiem un 0 dzēšanām
  1. 34 0
      src/components/App.tsx
  2. 92 0
      src/data/pixelated-image.ts

+ 34 - 0
src/components/App.tsx

@@ -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, 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,

+ 92 - 0
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<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,
+  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<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: x + col * cellSize,
+            y: y + row * cellSize,
+            width: cellSize,
+            height: cellSize,
+          });
+          shapes.push(rectangle);
+        }
+      }
+    }
+    return shapes;
+  } finally {
+    URL.revokeObjectURL(url);
+  }
+};