feat: open images
This commit is contained in:
parent
98e6354a6e
commit
bebf8aaba4
9 changed files with 121 additions and 34 deletions
|
|
@ -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 (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
22
src/components/ui/input.tsx
Normal file
22
src/components/ui/input.tsx
Normal 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 }
|
||||
24
src/components/ui/label.tsx
Normal file
24
src/components/ui/label.tsx
Normal 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 }
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
19
src/context/ImageContext.tsx
Normal file
19
src/context/ImageContext.tsx
Normal 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>;
|
||||
};
|
||||
|
|
@ -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
4
src/types.d.ts
vendored
|
|
@ -27,3 +27,7 @@ interface Settings {
|
|||
grid: boolean;
|
||||
canvasBorder: boolean;
|
||||
}
|
||||
|
||||
interface DialogProps {
|
||||
close: () => void;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue