feat: open images

This commit is contained in:
trafficlunar 2024-12-14 22:57:50 +00:00
parent 98e6354a6e
commit bebf8aaba4
9 changed files with 121 additions and 34 deletions

View file

@ -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<React.SetStateAction<Block[]>>;
textures: Record<string, Texture>;
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 (
<>

View file

@ -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<HTMLDivElement>(null);
const [stageSize, setStageSize] = useState<Dimension>({ 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}
>
<Container x={coords.x} y={coords.y} scale={scale}>
<Blocks blocks={blocks} setBlocks={setBlocks} textures={textures} />
<Blocks blocks={blocks} setBlocks={setBlocks} textures={textures} image={image} imageDimensions={imageDimensions} />
{settings.canvasBorder && <CanvasBorder canvasSize={canvasSize} />}
<Cursor mouseCoords={mouseCoords} />
</Container>

View file

@ -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<HTMLImageElement>();
const [imageDimensions, setImageDimensions] = useState<Dimension>({ 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<HTMLInputElement>, 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 (
<DialogContent>
<DialogHeader>
@ -106,7 +119,7 @@ function OpenImage() {
</div>
)}
<Button type="submit" className="ml-auto">
<Button type="submit" className="ml-auto" onClick={onSubmit}>
Submit
</Button>
</DialogFooter>

View file

@ -0,0 +1,22 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-zinc-200 bg-white px-3 py-2 text-base ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-zinc-950 placeholder:text-zinc-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-zinc-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:border-zinc-800 dark:bg-zinc-950 dark:ring-offset-zinc-950 dark:file:text-zinc-50 dark:placeholder:text-zinc-400 dark:focus-visible:ring-zinc-300",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View file

@ -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<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View file

@ -23,7 +23,7 @@ export const DialogProvider = ({ children }: Props) => {
<Dialog open={open} onOpenChange={(value) => setOpen(value)}>
{LazyDialogContent && (
<Suspense fallback={<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">Loading dialog...</div>}>
<LazyDialogContent />
<LazyDialogContent close={() => setOpen(false)} />
</Suspense>
)}
</Dialog>

View file

@ -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<HTMLImageElement>();
const [imageDimensions, setImageDimensions] = useState<Dimension>({ width: 0, height: 0 });
return <ImageContext.Provider value={{ image, imageDimensions, setImage, setImageDimensions }}>{children}</ImageContext.Provider>;
};

View file

@ -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 (
<SettingsProvider>
<TexturesProvider>
<ToolProvider>
<main className="h-screen grid grid-rows-[2.5rem_1fr] grid-cols-[2.5rem_1fr]">
<Menubar />
<Toolbar />
<Canvas />
</main>
</ToolProvider>
</TexturesProvider>
</SettingsProvider>
<ImageProvider>
<SettingsProvider>
<TexturesProvider>
<ToolProvider>
<main className="h-screen grid grid-rows-[2.5rem_1fr] grid-cols-[2.5rem_1fr]">
<Menubar />
<Toolbar />
<Canvas />
</main>
</ToolProvider>
</TexturesProvider>
</SettingsProvider>
</ImageProvider>
);
}

4
src/types.d.ts vendored
View file

@ -27,3 +27,7 @@ interface Settings {
grid: boolean;
canvasBorder: boolean;
}
interface DialogProps {
close: () => void;
}