diff --git a/src/components/Blocks.tsx b/src/components/Blocks.tsx index 441d131..5b71645 100644 --- a/src/components/Blocks.tsx +++ b/src/components/Blocks.tsx @@ -2,16 +2,17 @@ import { useEffect } from "react"; import { Sprite } from "@pixi/react"; import blocksData from "@/data/blocks/programmer-art/average_colors.json"; -import welcomeBlocksData from "@/data/welcome.json"; import { Texture } from "pixi.js"; interface Props { blocks: Block[]; setBlocks: React.Dispatch>; textures: Record; + image: HTMLImageElement | undefined; + imageDimensions: Dimension; } -function Blocks({ blocks, setBlocks, textures }: Props) { +function Blocks({ blocks, setBlocks, textures, image, imageDimensions }: Props) { const findClosestBlock = (r: number, g: number, b: number, a: number) => { let closestBlock = ""; let closestDistance = Infinity; @@ -28,19 +29,16 @@ function Blocks({ blocks, setBlocks, textures }: Props) { }; useEffect(() => { - // TESTING: convert image to blocks - const image = new Image(); - image.src = "/bliss.png"; - image.addEventListener("load", () => { + if (image) { const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); if (ctx) { - canvas.width = image.width; - canvas.height = image.height; - ctx.drawImage(image, 0, 0, image.width / 4, image.height / 4); + canvas.width = imageDimensions.width; + canvas.height = imageDimensions.height; + ctx.drawImage(image, 0, 0, imageDimensions.width, imageDimensions.height); - const imageData = ctx.getImageData(0, 0, image.width / 4, image.height / 4); + const imageData = ctx.getImageData(0, 0, imageDimensions.width, imageDimensions.height); const newBlocks: Block[] = []; for (let i = 0; i < imageData.data.length; i += 4) { @@ -58,10 +56,8 @@ function Blocks({ blocks, setBlocks, textures }: Props) { setBlocks(newBlocks); } - }); - - setBlocks(welcomeBlocksData); - }, [textures]); + } + }, [image, imageDimensions, setBlocks]); return ( <> diff --git a/src/components/Canvas.tsx b/src/components/Canvas.tsx index 6b04c23..8b5520c 100644 --- a/src/components/Canvas.tsx +++ b/src/components/Canvas.tsx @@ -2,9 +2,10 @@ import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } import { Container, Stage } from "@pixi/react"; import * as PIXI from "pixi.js"; +import { ImageContext } from "@/context/ImageContext"; import { SettingsContext } from "@/context/SettingsContext"; -import { ToolContext } from "@/context/ToolContext"; import { TexturesContext } from "@/context/TexturesContext"; +import { ToolContext } from "@/context/ToolContext"; import Blocks from "./Blocks"; import Grid from "./Grid"; @@ -13,13 +14,16 @@ import CursorInformation from "./information/Cursor"; import CanvasInformation from "./information/Canvas"; import CanvasBorder from "./CanvasBorder"; +import welcomeBlocksData from "@/data/welcome.json"; + // Set scale mode to NEAREST PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; function Canvas() { + const { image, imageDimensions } = useContext(ImageContext); const { settings } = useContext(SettingsContext); - const { tool, selectedBlock } = useContext(ToolContext); const textures = useContext(TexturesContext); + const { tool, selectedBlock } = useContext(ToolContext); const stageContainerRef = useRef(null); const [stageSize, setStageSize] = useState({ width: 0, height: 0 }); @@ -144,6 +148,8 @@ function Canvas() { resizeCanvas(); window.addEventListener("resize", resizeCanvas); + + setBlocks(welcomeBlocksData); return () => window.removeEventListener("resize", resizeCanvas); }, []); @@ -158,7 +164,7 @@ function Canvas() { onWheel={onWheel} > - + {settings.canvasBorder && } diff --git a/src/components/dialogs/OpenImage.tsx b/src/components/dialogs/OpenImage.tsx index 07e4c75..e3b60c1 100644 --- a/src/components/dialogs/OpenImage.tsx +++ b/src/components/dialogs/OpenImage.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useState } from "react"; +import { useContext, useEffect, useState } from "react"; import { useDropzone } from "react-dropzone"; import { CircleAlertIcon, UploadIcon } from "lucide-react"; @@ -8,7 +8,11 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; -function OpenImage() { +import { ImageContext } from "@/context/ImageContext"; + +function OpenImage({ close }: DialogProps) { + const { setImage: setContextImage, setImageDimensions: setContextImageDimensions } = useContext(ImageContext); + const { acceptedFiles, getRootProps, getInputProps } = useDropzone({ accept: { "image/*": [".png", ".jpg", ".jpeg", ".bmp", ".webp", ".tiff"], @@ -17,6 +21,7 @@ function OpenImage() { const [image, setImage] = useState(); const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 }); + const [aspectRatio, setAspectRatio] = useState(1); useEffect(() => { if (acceptedFiles[0]) { @@ -24,6 +29,7 @@ function OpenImage() { img.onload = () => { setImage(img); setImageDimensions({ width: img.width, height: img.height }); + setAspectRatio(img.width / img.height); }; img.src = URL.createObjectURL(acceptedFiles[0]); } @@ -31,9 +37,8 @@ function OpenImage() { const onDimensionChange = (e: React.ChangeEvent, isWidth: boolean) => { const newDimension = Number(e.target.value); - const aspectRatio = imageDimensions.width / imageDimensions.height; - if (newDimension < 1 || newDimension > 10000) return; + setImageDimensions(() => { if (isWidth) { return { width: newDimension, height: Math.round(newDimension / aspectRatio) }; @@ -43,6 +48,14 @@ function OpenImage() { }); }; + const onSubmit = () => { + if (image) { + setContextImage(image); + setContextImageDimensions(imageDimensions); + close(); + } + }; + return ( @@ -106,7 +119,7 @@ function OpenImage() { )} - diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..ff2b6c9 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Input = React.forwardRef>( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..683faa7 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,24 @@ +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/src/context/DialogContext.tsx b/src/context/DialogContext.tsx index 6137091..e7a0119 100644 --- a/src/context/DialogContext.tsx +++ b/src/context/DialogContext.tsx @@ -23,7 +23,7 @@ export const DialogProvider = ({ children }: Props) => { setOpen(value)}> {LazyDialogContent && ( Loading dialog...}> - + setOpen(false)} /> )} diff --git a/src/context/ImageContext.tsx b/src/context/ImageContext.tsx new file mode 100644 index 0000000..3db1033 --- /dev/null +++ b/src/context/ImageContext.tsx @@ -0,0 +1,19 @@ +import { createContext, ReactNode, useState } from "react"; + +interface Props { + children: ReactNode; +} + +export const ImageContext = createContext({ + image: new Image() as HTMLImageElement | undefined, + imageDimensions: { width: 0, height: 0 } as Dimension, + setImage: (image: HTMLImageElement) => {}, + setImageDimensions: (dimension: Dimension) => {}, +}); + +export const ImageProvider = ({ children }: Props) => { + const [image, setImage] = useState(); + const [imageDimensions, setImageDimensions] = useState({ width: 0, height: 0 }); + + return {children}; +}; diff --git a/src/pages/AppPage.tsx b/src/pages/AppPage.tsx index 8815897..f9eb662 100644 --- a/src/pages/AppPage.tsx +++ b/src/pages/AppPage.tsx @@ -1,3 +1,4 @@ +import { ImageProvider } from "@/context/ImageContext"; import { SettingsProvider } from "../context/SettingsContext"; import { TexturesProvider } from "../context/TexturesContext"; import { ToolProvider } from "../context/ToolContext"; @@ -8,17 +9,19 @@ import Canvas from "../components/Canvas"; function AppPage() { return ( - - - -
- - - -
-
-
-
+ + + + +
+ + + +
+
+
+
+
); } diff --git a/src/types.d.ts b/src/types.d.ts index 1af5325..a05b2d1 100644 --- a/src/types.d.ts +++ b/src/types.d.ts @@ -27,3 +27,7 @@ interface Settings { grid: boolean; canvasBorder: boolean; } + +interface DialogProps { + close: () => void; +}