mirror of
https://github.com/r2r90/canvas-label.git
synced 2025-12-18 12:35:49 +00:00
Started snapping objects
This commit is contained in:
@@ -60,7 +60,9 @@ const Canvas = () => {
|
|||||||
<div className="relative flex h-full w-full flex-col items-center">
|
<div className="relative flex h-full w-full flex-col items-center">
|
||||||
<Toolbar />
|
<Toolbar />
|
||||||
<div
|
<div
|
||||||
className={"flex h-full w-full items-center justify-center"}
|
className={
|
||||||
|
"vertical-line horizontal-line flex h-full w-full items-center justify-center"
|
||||||
|
}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
if (e.target !== e.currentTarget) {
|
if (e.target !== e.currentTarget) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { deleteStageItem } from "@/store/app.slice";
|
import { deleteStageItem } from "@/store/app.slice";
|
||||||
import { useAppDispatch } from "@/hooks";
|
import { useAppDispatch } from "@/hooks";
|
||||||
import { BsTrash3 } from "react-icons/bs";
|
import { IconButton } from "@radix-ui/themes";
|
||||||
|
import { Trash2 } from "lucide-react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedItemId: string;
|
selectedItemId: string;
|
||||||
@@ -14,12 +14,8 @@ export const DeleteShapeButton = ({ selectedItemId }: Props) => {
|
|||||||
dispatch(deleteStageItem(selectedItemId));
|
dispatch(deleteStageItem(selectedItemId));
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Button
|
<IconButton onClick={() => deleteTextHandler(selectedItemId)}>
|
||||||
variant="destructive"
|
<Trash2 size={18} />
|
||||||
color="cyan"
|
</IconButton>
|
||||||
onClick={() => deleteTextHandler(selectedItemId)}
|
|
||||||
>
|
|
||||||
<BsTrash3 />
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,14 +6,15 @@ import {
|
|||||||
type StageItem,
|
type StageItem,
|
||||||
StageItemType,
|
StageItemType,
|
||||||
} from "@/store/app.slice";
|
} from "@/store/app.slice";
|
||||||
import { useAppDispatch } from "@/hooks";
|
import { useAppDispatch, useAppSelector } from "@/hooks";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { HiOutlineLockClosed, HiOutlineLockOpen } from "react-icons/hi2";
|
|
||||||
import { IconButton } from "@radix-ui/themes";
|
import { IconButton } from "@radix-ui/themes";
|
||||||
import { GripVertical } from "lucide-react";
|
import { Eye, GripVertical, Lock, Trash2, Unlock } from "lucide-react";
|
||||||
|
import { DeleteShapeButton } from "@/components/delete-shape-button";
|
||||||
|
import VisibleShapeToggle from "@/components/visible-shape-toggle";
|
||||||
|
|
||||||
export default function LayerItem({ item }: { item: StageItem }) {
|
export default function LayerItem({ item }: { item: StageItem }) {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
const selectedItemId = useAppSelector((state) => state.app.selectedItemId);
|
||||||
const { attributes, listeners, setNodeRef, transform, transition } =
|
const { attributes, listeners, setNodeRef, transform, transition } =
|
||||||
useSortable({
|
useSortable({
|
||||||
id: item.id,
|
id: item.id,
|
||||||
@@ -33,12 +34,12 @@ export default function LayerItem({ item }: { item: StageItem }) {
|
|||||||
style={style}
|
style={style}
|
||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
className="m-2 flex items-center justify-between rounded-md border p-2 text-black"
|
className="flex items-center justify-between rounded-md border bg-yellow-50 p-2 text-black"
|
||||||
>
|
>
|
||||||
<IconButton {...listeners}>
|
<IconButton {...listeners}>
|
||||||
<GripVertical />
|
<GripVertical size={18} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<span className="">{item.type}</span>
|
<span className="text-sm">{item.type}</span>
|
||||||
|
|
||||||
{item.type === StageItemType.Text ? (
|
{item.type === StageItemType.Text ? (
|
||||||
item.params.text < 5 ? (
|
item.params.text < 5 ? (
|
||||||
@@ -53,9 +54,14 @@ export default function LayerItem({ item }: { item: StageItem }) {
|
|||||||
src={item.params.imageUrl}
|
src={item.params.imageUrl}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<Button onClick={handleBlockItemClicked}>
|
|
||||||
{item.isBlocked ? <HiOutlineLockClosed /> : <HiOutlineLockOpen />}
|
<DeleteShapeButton selectedItemId={selectedItemId} />
|
||||||
</Button>
|
|
||||||
|
<VisibleShapeToggle />
|
||||||
|
|
||||||
|
<span onClick={() => handleBlockItemClicked()}>
|
||||||
|
{item.isBlocked ? <Lock size={18} /> : <Unlock size={18} />}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { type ImageConfig } from "konva/lib/shapes/Image";
|
import { type ImageConfig } from "konva/lib/shapes/Image";
|
||||||
import { useRef, useEffect, type ElementRef } from "react";
|
import { type ElementRef, useEffect, useRef } from "react";
|
||||||
import { Transformer, Image } from "react-konva";
|
import { Image, Transformer } from "react-konva";
|
||||||
import useImage from "use-image";
|
import useImage from "use-image";
|
||||||
|
|
||||||
type TransformableImageConfig = Omit<ImageConfig, "image"> & {
|
type TransformableImageConfig = Omit<ImageConfig, "image"> & {
|
||||||
@@ -37,10 +37,26 @@ export const TransformableImage = ({
|
|||||||
trRef.current?.getLayer()?.batchDraw();
|
trRef.current?.getLayer()?.batchDraw();
|
||||||
}
|
}
|
||||||
}, [isSelected]);
|
}, [isSelected]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Image
|
<Image
|
||||||
|
onDragMove={(e) => {
|
||||||
|
console.log(e);
|
||||||
|
const positionX = e.target.x() + e.target.width() / 2;
|
||||||
|
const positionY = e.target.y() + e.target.height() / 2;
|
||||||
|
const canvasXCenter = (e.target.getStage()?.width() ?? 0) / 2;
|
||||||
|
const canvasYCenter = (e.target.getStage()?.height() ?? 0) / 2;
|
||||||
|
const isInTheXCenter =
|
||||||
|
Math.abs(Math.round(canvasXCenter) - Math.round(positionX)) <= 5;
|
||||||
|
const isInTheYCenter =
|
||||||
|
Math.abs(Math.round(canvasYCenter) - Math.round(positionY)) <= 5;
|
||||||
|
|
||||||
|
document.body.classList.toggle("show-vertical-line", isInTheXCenter);
|
||||||
|
document.body.classList.toggle(
|
||||||
|
"show-horizontal-line",
|
||||||
|
isInTheYCenter,
|
||||||
|
);
|
||||||
|
}}
|
||||||
id={id}
|
id={id}
|
||||||
alt={"canvas image"}
|
alt={"canvas image"}
|
||||||
onClick={onSelect}
|
onClick={onSelect}
|
||||||
@@ -53,10 +69,16 @@ export const TransformableImage = ({
|
|||||||
if (isBlocked) {
|
if (isBlocked) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const positionX = e.target.x() + e.target.width() / 2;
|
||||||
|
const canvasXCenter = (e.target.getStage()?.width() ?? 0) / 2;
|
||||||
|
const isInTheCenter =
|
||||||
|
Math.abs(Math.round(canvasXCenter) - Math.round(positionX)) <= 5;
|
||||||
onChange({
|
onChange({
|
||||||
...imageProps,
|
...imageProps,
|
||||||
image,
|
image,
|
||||||
x: e.target.x(),
|
x: isInTheCenter
|
||||||
|
? canvasXCenter - e.target.width() / 2
|
||||||
|
: e.target.x(),
|
||||||
y: e.target.y(),
|
y: e.target.y(),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -80,6 +102,8 @@ export const TransformableImage = ({
|
|||||||
height: Math.max(node.height() * scaleY),
|
height: Math.max(node.height() * scaleY),
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
x={imageProps.x}
|
||||||
|
y={imageProps.y}
|
||||||
/>
|
/>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<Transformer
|
<Transformer
|
||||||
|
|||||||
5
src/components/visible-shape-toggle.tsx
Normal file
5
src/components/visible-shape-toggle.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { Eye } from "lucide-react";
|
||||||
|
|
||||||
|
export default function VisibleShapeToggle() {
|
||||||
|
return <Eye size={18} />;
|
||||||
|
}
|
||||||
15
src/index.css
Normal file
15
src/index.css
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
.show-horizontal-line .horizontal-line::before {
|
||||||
|
content: '';
|
||||||
|
border-bottom: 1px solid red;
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
z-index:9999
|
||||||
|
}
|
||||||
|
.show-vertical-line .vertical-line::after {
|
||||||
|
content: '';
|
||||||
|
border-right: 1px dotted #8a8888;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 50%
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ import { type AppType } from "next/app";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
|
import "../index.css";
|
||||||
|
|
||||||
import { Layout } from "@/components/layout/layout";
|
import { Layout } from "@/components/layout/layout";
|
||||||
import { Provider } from "react-redux";
|
import { Provider } from "react-redux";
|
||||||
import { store } from "@/store/store";
|
import { store } from "@/store/store";
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
|
||||||
const Canvas = dynamic(() => import("../components/canvas"), { ssr: false });
|
const Canvas = dynamic(() => import("../components/canvas"), { ssr: false });
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
|||||||
Reference in New Issue
Block a user