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 { Sprite } from "@pixi/react";
import blocksData from "@/data/blocks/programmer-art/average_colors.json"; import blocksData from "@/data/blocks/programmer-art/average_colors.json";
import welcomeBlocksData from "@/data/welcome.json";
import { Texture } from "pixi.js"; import { Texture } from "pixi.js";
interface Props { interface Props {
blocks: Block[]; blocks: Block[];
setBlocks: React.Dispatch<React.SetStateAction<Block[]>>; setBlocks: React.Dispatch<React.SetStateAction<Block[]>>;
textures: Record<string, Texture>; 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) => { const findClosestBlock = (r: number, g: number, b: number, a: number) => {
let closestBlock = ""; let closestBlock = "";
let closestDistance = Infinity; let closestDistance = Infinity;
@ -28,19 +29,16 @@ function Blocks({ blocks, setBlocks, textures }: Props) {
}; };
useEffect(() => { useEffect(() => {
// TESTING: convert image to blocks if (image) {
const image = new Image();
image.src = "/bliss.png";
image.addEventListener("load", () => {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
if (ctx) { if (ctx) {
canvas.width = image.width; canvas.width = imageDimensions.width;
canvas.height = image.height; canvas.height = imageDimensions.height;
ctx.drawImage(image, 0, 0, image.width / 4, image.height / 4); 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[] = []; const newBlocks: Block[] = [];
for (let i = 0; i < imageData.data.length; i += 4) { for (let i = 0; i < imageData.data.length; i += 4) {
@ -58,10 +56,8 @@ function Blocks({ blocks, setBlocks, textures }: Props) {
setBlocks(newBlocks); setBlocks(newBlocks);
} }
}); }
}, [image, imageDimensions, setBlocks]);
setBlocks(welcomeBlocksData);
}, [textures]);
return ( return (
<> <>

View file

@ -2,9 +2,10 @@ import React, { useCallback, useContext, useEffect, useMemo, useRef, useState }
import { Container, Stage } from "@pixi/react"; import { Container, Stage } from "@pixi/react";
import * as PIXI from "pixi.js"; import * as PIXI from "pixi.js";
import { ImageContext } from "@/context/ImageContext";
import { SettingsContext } from "@/context/SettingsContext"; import { SettingsContext } from "@/context/SettingsContext";
import { ToolContext } from "@/context/ToolContext";
import { TexturesContext } from "@/context/TexturesContext"; import { TexturesContext } from "@/context/TexturesContext";
import { ToolContext } from "@/context/ToolContext";
import Blocks from "./Blocks"; import Blocks from "./Blocks";
import Grid from "./Grid"; import Grid from "./Grid";
@ -13,13 +14,16 @@ import CursorInformation from "./information/Cursor";
import CanvasInformation from "./information/Canvas"; import CanvasInformation from "./information/Canvas";
import CanvasBorder from "./CanvasBorder"; import CanvasBorder from "./CanvasBorder";
import welcomeBlocksData from "@/data/welcome.json";
// Set scale mode to NEAREST // Set scale mode to NEAREST
PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST; PIXI.settings.SCALE_MODE = PIXI.SCALE_MODES.NEAREST;
function Canvas() { function Canvas() {
const { image, imageDimensions } = useContext(ImageContext);
const { settings } = useContext(SettingsContext); const { settings } = useContext(SettingsContext);
const { tool, selectedBlock } = useContext(ToolContext);
const textures = useContext(TexturesContext); const textures = useContext(TexturesContext);
const { tool, selectedBlock } = useContext(ToolContext);
const stageContainerRef = useRef<HTMLDivElement>(null); const stageContainerRef = useRef<HTMLDivElement>(null);
const [stageSize, setStageSize] = useState<Dimension>({ width: 0, height: 0 }); const [stageSize, setStageSize] = useState<Dimension>({ width: 0, height: 0 });
@ -144,6 +148,8 @@ function Canvas() {
resizeCanvas(); resizeCanvas();
window.addEventListener("resize", resizeCanvas); window.addEventListener("resize", resizeCanvas);
setBlocks(welcomeBlocksData);
return () => window.removeEventListener("resize", resizeCanvas); return () => window.removeEventListener("resize", resizeCanvas);
}, []); }, []);
@ -158,7 +164,7 @@ function Canvas() {
onWheel={onWheel} onWheel={onWheel}
> >
<Container x={coords.x} y={coords.y} scale={scale}> <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} />} {settings.canvasBorder && <CanvasBorder canvasSize={canvasSize} />}
<Cursor mouseCoords={mouseCoords} /> <Cursor mouseCoords={mouseCoords} />
</Container> </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 { useDropzone } from "react-dropzone";
import { CircleAlertIcon, UploadIcon } from "lucide-react"; import { CircleAlertIcon, UploadIcon } from "lucide-react";
@ -8,7 +8,11 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; 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({ const { acceptedFiles, getRootProps, getInputProps } = useDropzone({
accept: { accept: {
"image/*": [".png", ".jpg", ".jpeg", ".bmp", ".webp", ".tiff"], "image/*": [".png", ".jpg", ".jpeg", ".bmp", ".webp", ".tiff"],
@ -17,6 +21,7 @@ function OpenImage() {
const [image, setImage] = useState<HTMLImageElement>(); const [image, setImage] = useState<HTMLImageElement>();
const [imageDimensions, setImageDimensions] = useState<Dimension>({ width: 0, height: 0 }); const [imageDimensions, setImageDimensions] = useState<Dimension>({ width: 0, height: 0 });
const [aspectRatio, setAspectRatio] = useState(1);
useEffect(() => { useEffect(() => {
if (acceptedFiles[0]) { if (acceptedFiles[0]) {
@ -24,6 +29,7 @@ function OpenImage() {
img.onload = () => { img.onload = () => {
setImage(img); setImage(img);
setImageDimensions({ width: img.width, height: img.height }); setImageDimensions({ width: img.width, height: img.height });
setAspectRatio(img.width / img.height);
}; };
img.src = URL.createObjectURL(acceptedFiles[0]); img.src = URL.createObjectURL(acceptedFiles[0]);
} }
@ -31,9 +37,8 @@ function OpenImage() {
const onDimensionChange = (e: React.ChangeEvent<HTMLInputElement>, isWidth: boolean) => { const onDimensionChange = (e: React.ChangeEvent<HTMLInputElement>, isWidth: boolean) => {
const newDimension = Number(e.target.value); const newDimension = Number(e.target.value);
const aspectRatio = imageDimensions.width / imageDimensions.height;
if (newDimension < 1 || newDimension > 10000) return; if (newDimension < 1 || newDimension > 10000) return;
setImageDimensions(() => { setImageDimensions(() => {
if (isWidth) { if (isWidth) {
return { width: newDimension, height: Math.round(newDimension / aspectRatio) }; return { width: newDimension, height: Math.round(newDimension / aspectRatio) };
@ -43,6 +48,14 @@ function OpenImage() {
}); });
}; };
const onSubmit = () => {
if (image) {
setContextImage(image);
setContextImageDimensions(imageDimensions);
close();
}
};
return ( return (
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
@ -106,7 +119,7 @@ function OpenImage() {
</div> </div>
)} )}
<Button type="submit" className="ml-auto"> <Button type="submit" className="ml-auto" onClick={onSubmit}>
Submit Submit
</Button> </Button>
</DialogFooter> </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)}> <Dialog open={open} onOpenChange={(value) => setOpen(value)}>
{LazyDialogContent && ( {LazyDialogContent && (
<Suspense fallback={<div className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">Loading dialog...</div>}> <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> </Suspense>
)} )}
</Dialog> </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 { SettingsProvider } from "../context/SettingsContext";
import { TexturesProvider } from "../context/TexturesContext"; import { TexturesProvider } from "../context/TexturesContext";
import { ToolProvider } from "../context/ToolContext"; import { ToolProvider } from "../context/ToolContext";
@ -8,6 +9,7 @@ import Canvas from "../components/Canvas";
function AppPage() { function AppPage() {
return ( return (
<ImageProvider>
<SettingsProvider> <SettingsProvider>
<TexturesProvider> <TexturesProvider>
<ToolProvider> <ToolProvider>
@ -19,6 +21,7 @@ function AppPage() {
</ToolProvider> </ToolProvider>
</TexturesProvider> </TexturesProvider>
</SettingsProvider> </SettingsProvider>
</ImageProvider>
); );
} }

4
src/types.d.ts vendored
View file

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