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 { 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 (
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
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)}>
|
<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>
|
||||||
|
|
|
||||||
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 { 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,17 +9,19 @@ import Canvas from "../components/Canvas";
|
||||||
|
|
||||||
function AppPage() {
|
function AppPage() {
|
||||||
return (
|
return (
|
||||||
<SettingsProvider>
|
<ImageProvider>
|
||||||
<TexturesProvider>
|
<SettingsProvider>
|
||||||
<ToolProvider>
|
<TexturesProvider>
|
||||||
<main className="h-screen grid grid-rows-[2.5rem_1fr] grid-cols-[2.5rem_1fr]">
|
<ToolProvider>
|
||||||
<Menubar />
|
<main className="h-screen grid grid-rows-[2.5rem_1fr] grid-cols-[2.5rem_1fr]">
|
||||||
<Toolbar />
|
<Menubar />
|
||||||
<Canvas />
|
<Toolbar />
|
||||||
</main>
|
<Canvas />
|
||||||
</ToolProvider>
|
</main>
|
||||||
</TexturesProvider>
|
</ToolProvider>
|
||||||
</SettingsProvider>
|
</TexturesProvider>
|
||||||
|
</SettingsProvider>
|
||||||
|
</ImageProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
4
src/types.d.ts
vendored
4
src/types.d.ts
vendored
|
|
@ -27,3 +27,7 @@ interface Settings {
|
||||||
grid: boolean;
|
grid: boolean;
|
||||||
canvasBorder: boolean;
|
canvasBorder: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DialogProps {
|
||||||
|
close: () => void;
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue