From e16b249fa182a2b190e0463f39e23914b83a7b9b Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Tue, 2 Apr 2024 10:43:44 +0100 Subject: [PATCH 01/10] Board component - Fixed tests error --- setupTests.ts | 1 + src/components/Annotator/__test__/Annotator.test.tsx | 1 + src/components/Board/__test__/Board.test.tsx | 1 + src/components/Button/__test__/Button.test.tsx | 1 + src/components/Header/__test__/Header.test.tsx | 1 + src/components/Menu/__test__/Menu.test.tsx | 1 + src/components/Toolbar/__test__/Toolbar.test.tsx | 1 + src/components/ToolbarItem/__test__/ToolbarItem.test.tsx | 1 + 8 files changed, 8 insertions(+) diff --git a/setupTests.ts b/setupTests.ts index b752187..bf2155f 100644 --- a/setupTests.ts +++ b/setupTests.ts @@ -1,6 +1,7 @@ import { expect } from "vitest"; import * as matchers from "@testing-library/jest-dom/matchers"; import { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers"; + declare module "vitest" { // eslint-disable-next-line @typescript-eslint/no-explicit-any interface Assertion diff --git a/src/components/Annotator/__test__/Annotator.test.tsx b/src/components/Annotator/__test__/Annotator.test.tsx index ad6e78a..3081331 100644 --- a/src/components/Annotator/__test__/Annotator.test.tsx +++ b/src/components/Annotator/__test__/Annotator.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { describe, expect, it } from "vitest"; import { render, screen } from "@testing-library/react"; import Annotator from "../Annotator"; +import "@testing-library/jest-dom"; // This needs to be here for now. describe("Annotator component", () => { it("Annotator should render correctly", () => { diff --git a/src/components/Board/__test__/Board.test.tsx b/src/components/Board/__test__/Board.test.tsx index 61fcc65..09cbb7b 100644 --- a/src/components/Board/__test__/Board.test.tsx +++ b/src/components/Board/__test__/Board.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { describe, expect, it } from "vitest"; import { render, screen } from "@testing-library/react"; import Board from "../Board"; +import "@testing-library/jest-dom"; // This needs to be here for now. describe("Button component", () => { it("Button should render correctly", () => { diff --git a/src/components/Button/__test__/Button.test.tsx b/src/components/Button/__test__/Button.test.tsx index 686b9c8..88af64e 100644 --- a/src/components/Button/__test__/Button.test.tsx +++ b/src/components/Button/__test__/Button.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { describe, expect, it } from "vitest"; import { render, screen } from "@testing-library/react"; import Button from "../Button"; +import "@testing-library/jest-dom"; // This needs to be here for now. describe("Button component", () => { it("Button should render correctly", () => { diff --git a/src/components/Header/__test__/Header.test.tsx b/src/components/Header/__test__/Header.test.tsx index 423e454..06a255c 100644 --- a/src/components/Header/__test__/Header.test.tsx +++ b/src/components/Header/__test__/Header.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { describe, expect, it } from "vitest"; import { render, screen } from "@testing-library/react"; import Header from "../Header"; +import "@testing-library/jest-dom"; // This needs to be here for now. describe("Header component", () => { it("Header should render correctly", () => { diff --git a/src/components/Menu/__test__/Menu.test.tsx b/src/components/Menu/__test__/Menu.test.tsx index ca420f1..e233f6d 100644 --- a/src/components/Menu/__test__/Menu.test.tsx +++ b/src/components/Menu/__test__/Menu.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { describe, expect, it } from "vitest"; import { render, screen } from "@testing-library/react"; import Menu from "../Menu"; +import "@testing-library/jest-dom"; // This needs to be here for now. describe("Menu component", () => { it("Menu should render correctly", () => { diff --git a/src/components/Toolbar/__test__/Toolbar.test.tsx b/src/components/Toolbar/__test__/Toolbar.test.tsx index af81dee..d85600b 100644 --- a/src/components/Toolbar/__test__/Toolbar.test.tsx +++ b/src/components/Toolbar/__test__/Toolbar.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { describe, expect, it } from "vitest"; import { render, screen } from "@testing-library/react"; import Toolbar from "../Toolbar"; +import "@testing-library/jest-dom"; // This needs to be here for now. describe("Toolbar component", () => { it("Toolbar should render correctly", () => { diff --git a/src/components/ToolbarItem/__test__/ToolbarItem.test.tsx b/src/components/ToolbarItem/__test__/ToolbarItem.test.tsx index 4e7d6e2..83d8fb4 100644 --- a/src/components/ToolbarItem/__test__/ToolbarItem.test.tsx +++ b/src/components/ToolbarItem/__test__/ToolbarItem.test.tsx @@ -2,6 +2,7 @@ import React from "react"; import { describe, expect, it } from "vitest"; import { render, screen } from "@testing-library/react"; import ToolbarItem from "../ToolbarItem"; +import "@testing-library/jest-dom"; // This needs to be here for now. describe("ToolbarItem component", () => { it("ToolbarItem should render correctly", () => { From 652ded6d01f7af8931bf7e9416cef6d30585b287 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Tue, 2 Apr 2024 11:11:57 +0100 Subject: [PATCH 02/10] Removed 'ExampleMain' --- src/components/Board/Board.tsx | 264 +++++++++++++++--- src/components/Board/__docs__/Board.mdx | 3 - .../Board/__docs__/Board.stories.tsx | 21 +- src/components/Board/__docs__/Example.tsx | 16 +- src/components/Board/__docs__/ExampleMain.tsx | 224 --------------- tsconfig.json | 1 + 6 files changed, 235 insertions(+), 294 deletions(-) delete mode 100644 src/components/Board/__docs__/ExampleMain.tsx diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 8427b9a..05d5bbd 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -1,46 +1,238 @@ -import React, { MouseEventHandler } from "react"; +import React, { useEffect, useState } from "react"; +import { fabric } from "fabric"; +import { FabricJSCanvas, useFabricJSEditor } from "fabricjs-react"; +import tokens from "../../tokens"; + +const WIDTH = 800; +const HEIGHT = 500; export type BoardProps = { - text?: string; primary?: boolean; - disabled?: boolean; - size?: "small" | "medium" | "large"; - onClick?: MouseEventHandler; }; -const Board: React.FC = ({ - size, - primary, - disabled, - text, - onClick, - ...props -}) => { - // Determine button color and background color based on primary prop - const buttonColor = primary ? "text-white" : "text-black"; - const bgColor = primary ? "bg-red-600" : "bg-gray-300"; - - // Determine padding based on size prop - let paddingClass = ""; - if (size === "small") { - paddingClass = "py-1 px-6"; - } else if (size === "medium") { - paddingClass = "py-2 px-8"; - } else { - paddingClass = "py-3 px-8"; - } +type CanvasAnnotationState = { + selection?: boolean; + lastPosX: number; + lastPosY: number; + isDragging?: boolean; +}; + +const Board: React.FC = ({ primary = true }) => { + const { editor, onReady } = useFabricJSEditor(); + const [currentZoom, setCurrentZoom] = useState(100); + const [scaleRatio, setScaleRation] = useState(100); + const [imageSize, setImageSize] = useState({ + width: 0, + height: 0, + }); + + const [draggingEnabled, setDraggingEnabled] = useState(false); + + useEffect(() => { + if (!editor || !fabric) { + return; + } + + // Background color of canvas + editor.canvas.backgroundColor = primary + ? tokens.primary.backgroundColor + : tokens.secondary.backgroundColor; + + editor.canvas.setWidth(WIDTH); + editor.canvas.setHeight(HEIGHT); + + // Change the cursor + editor.canvas.defaultCursor = draggingEnabled ? "pointer" : "default"; + + fabric.Image.fromURL( + "holder-min.jpg", + (img) => { + const { canvas } = editor; + const scaleRatio = Math.min( + (canvas.width ?? 1) / (img.width ?? 1), + (canvas.height ?? 1) / (img.height ?? 1), + ); + setImageSize({ width: img.width ?? 0, height: img.height ?? 0 }); + + setScaleRation(scaleRatio); + canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), { + scaleX: scaleRatio, + scaleY: scaleRatio, + left: (canvas.width ?? 1) / 2, + top: (canvas.height ?? 1) / 2, + originX: "middle", + originY: "middle", + }); + canvas!.renderAll(); + }, + { selectable: false }, + ); + + editor.canvas.on( + "mouse:wheel", + function (this: CanvasAnnotationState, opt) { + const delta = opt.e.deltaY; + let zoom = editor.canvas.getZoom(); + zoom *= 0.999 ** delta; + if (zoom > 20) zoom = 20; + if (zoom < 0.01) zoom = 0.01; + editor.canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom); + setCurrentZoom(zoom * 100); + opt.e.preventDefault(); + opt.e.stopPropagation(); + }, + ); + + editor.canvas.on("mouse:down", function (this: CanvasAnnotationState, opt) { + const evt = opt.e; + this.isDragging = draggingEnabled; + this.selection = false; + this.lastPosX = evt.clientX; + this.lastPosY = evt.clientY; + + opt.e.preventDefault(); + opt.e.stopPropagation(); + }); + + editor.canvas.on("mouse:move", function (this: CanvasAnnotationState, opt) { + if (this.isDragging) { + const e = opt.e; + const vpt = editor.canvas.viewportTransform; + if (vpt) { + vpt[4] += e.clientX - this.lastPosX; + vpt[5] += e.clientY - this.lastPosY; + editor.canvas.requestRenderAll(); + this.lastPosX = e.clientX; + this.lastPosY = e.clientY; + } + } + + opt.e.preventDefault(); + opt.e.stopPropagation(); + }); + + editor.canvas.on("mouse:up", function (this: CanvasAnnotationState, opt) { + this.isDragging = false; + this.selection = true; + + opt.e.preventDefault(); + opt.e.stopPropagation(); + }); + + editor.canvas.renderAll(); + }, [primary, draggingEnabled, editor]); + + // TODO: Make sure this makes sense.. + const resetZoom = () => { + editor?.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); + }; + + const getActiveObjects = () => { + console.log(editor?.canvas.getActiveObjects()); + }; + + const addRandom = () => { + // Calculate the coordinates relative to the image + // (x1, y1) = (133, 460) + const rect = new fabric.Rect({ + scaleX: scaleRatio, + scaleY: scaleRatio, + left: WIDTH / 2 - (imageSize.width * scaleRatio) / 2 + 133 * scaleRatio, // Specify the left coordinate relative to image + top: HEIGHT / 2 - (imageSize.height * scaleRatio) / 2 + 460 * scaleRatio, + width: 73, + height: 33, + fill: undefined, + stroke: "red", + strokeWidth: 1, + selectable: true, + }); + editor?.canvas.add(rect); + }; + + const onAddRectangle = () => { + // editor?.addRectangle(); + const rect = new fabric.Rect({ + left: 0, + top: 0, + originX: "left", + originY: "top", + width: 100, + height: 100, + fill: "rgba(255,127,39,1)", + selectable: true, + visible: false, + }); + + const renderIcon = ( + ctx: CanvasRenderingContext2D, + left: number, + top: number, + styleOverride: unknown, + fabricObject: fabric.Object, + ) => { + const deleteIcon = + "data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg version='1.1' id='Ebene_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='595.275px' height='595.275px' viewBox='200 215 230 470' xml:space='preserve'%3E%3Ccircle style='fill:%23F44336;' cx='299.76' cy='439.067' r='218.516'/%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18'/%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179'/%3E%3C/g%3E%3C/svg%3E"; + + const img = document.createElement("img"); + img.src = deleteIcon; + const size = 24; + ctx.save(); + ctx.translate(left, top); + if (fabricObject.angle) + ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle)); + ctx.drawImage(img, -size / 2, -size / 2, size, size); + ctx.restore(); + }; + + const onDelete = () => { + editor?.deleteSelected(); + return true; + }; + + rect.controls = { + onDelete: new fabric.Control({ + x: 0.5, + y: -0.5, + offsetY: 16, + cursorStyle: "pointer", + mouseUpHandler: () => onDelete(), + render: renderIcon, + }), + }; + + editor?.canvas.add(rect); + // setAction({ primitive: "rectangle", operation: "add" }); + }; + + const draggingState = () => { + setDraggingEnabled(!draggingEnabled); + }; + + const getVisible = () => { + console.log(editor?.canvas.getObjects()?.[0].visible); + }; return ( - +
+ + + + + + +
+ +
Zoom: {Math.round(currentZoom)}%
+
+
); }; diff --git a/src/components/Board/__docs__/Board.mdx b/src/components/Board/__docs__/Board.mdx index 6f4410b..506f91f 100644 --- a/src/components/Board/__docs__/Board.mdx +++ b/src/components/Board/__docs__/Board.mdx @@ -20,9 +20,6 @@ import {Board} from "react-image-annotator"; const Example = () => { return ( console.log("Clicked")} primary /> ); diff --git a/src/components/Board/__docs__/Board.stories.tsx b/src/components/Board/__docs__/Board.stories.tsx index f860979..7c01ac4 100644 --- a/src/components/Board/__docs__/Board.stories.tsx +++ b/src/components/Board/__docs__/Board.stories.tsx @@ -1,29 +1,16 @@ import type { Meta, StoryObj } from "@storybook/react"; -import ExampleMain from "./ExampleMain"; +import Example from "./Example"; -const meta: Meta = { +const meta: Meta = { title: "Board", - component: ExampleMain, + component: Example, }; export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Primary: Story = { args: { - text: "Board", primary: true, - disabled: false, - size: "small", - onClick: () => console.log("Board"), - }, -}; -export const Secondary: Story = { - args: { - text: "Board", - primary: false, - disabled: false, - size: "small", - onClick: () => console.log("Board"), }, }; diff --git a/src/components/Board/__docs__/Example.tsx b/src/components/Board/__docs__/Example.tsx index 228ae31..1c2fff1 100644 --- a/src/components/Board/__docs__/Example.tsx +++ b/src/components/Board/__docs__/Example.tsx @@ -1,13 +1,7 @@ import React, { FC } from "react"; import Board, { BoardProps } from "../Board"; -const Example: FC = ({ - disabled = false, - onClick = () => {}, - primary = true, - size = "small", - text = "Button", -}) => { +const Example: FC = ({ primary = true }) => { return (
= ({ height: "100%", }} > - +
); }; diff --git a/src/components/Board/__docs__/ExampleMain.tsx b/src/components/Board/__docs__/ExampleMain.tsx deleted file mode 100644 index 779abec..0000000 --- a/src/components/Board/__docs__/ExampleMain.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import React, { FC, useEffect, useState } from "react"; -import { fabric } from "fabric"; -import { BoardProps } from "../Board"; -import { FabricJSCanvas, useFabricJSEditor } from "fabricjs-react"; - -const WIDTH = 800; -const HEIGHT = 500; - -const ExampleMain: FC = ({ - // eslint-disable-next-line @typescript-eslint/no-unused-vars - disabled = false, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - onClick = () => {}, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - primary = true, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - size = "small", - // eslint-disable-next-line @typescript-eslint/no-unused-vars - text = "Button", -}) => { - const { editor, onReady } = useFabricJSEditor(); - const [currentZoom, setCurrentZoom] = useState(100); - const [scaleRatio, setScaleRation] = useState(100); - const [imageSize, setImageSize] = useState({ - width: 0, - height: 0, - }); - - const [draggingEnabled, setDraggingEnabled] = useState(false); - - useEffect(() => { - if (!editor || !fabric) { - return; - } - - editor.canvas.setWidth(WIDTH); - editor.canvas.setHeight(HEIGHT); - - // Change the cursor - editor.canvas.defaultCursor = draggingEnabled ? "pointer" : "default"; - - fabric.Image.fromURL( - "holder-min.jpg", - (img) => { - const { canvas } = editor; - const scaleRatio = Math.min( - (canvas.width ?? 1) / (img.width ?? 1), - (canvas.height ?? 1) / (img.height ?? 1), - ); - setImageSize({ width: img.width ?? 0, height: img.height ?? 0 }); - - setScaleRation(scaleRatio); - canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), { - scaleX: scaleRatio, - scaleY: scaleRatio, - left: (canvas.width ?? 1) / 2, - top: (canvas.height ?? 1) / 2, - originX: "middle", - originY: "middle", - }); - canvas!.renderAll(); - }, - { selectable: false }, - ); - - editor.canvas.on("mouse:wheel", function (opt) { - const delta = opt.e.deltaY; - let zoom = editor.canvas.getZoom(); - zoom *= 0.999 ** delta; - if (zoom > 20) zoom = 20; - if (zoom < 0.01) zoom = 0.01; - editor.canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom); - setCurrentZoom(zoom * 100); - opt.e.preventDefault(); - opt.e.stopPropagation(); - }); - - editor.canvas.on("mouse:down", function (opt) { - const evt = opt.e; - this.isDragging = draggingEnabled; - this.selection = false; - this.lastPosX = evt.clientX; - this.lastPosY = evt.clientY; - - opt.e.preventDefault(); - opt.e.stopPropagation(); - }); - - editor.canvas.on("mouse:move", function (opt) { - if (this.isDragging) { - const e = opt.e; - const vpt = editor.canvas.viewportTransform; - if (vpt) { - vpt[4] += e.clientX - this.lastPosX; - vpt[5] += e.clientY - this.lastPosY; - editor.canvas.requestRenderAll(); - this.lastPosX = e.clientX; - this.lastPosY = e.clientY; - } - } - - opt.e.preventDefault(); - opt.e.stopPropagation(); - }); - - editor.canvas.on("mouse:up", function (opt) { - this.isDragging = false; - this.selection = true; - - opt.e.preventDefault(); - opt.e.stopPropagation(); - }); - - editor.canvas.renderAll(); - }, [draggingEnabled, editor]); - - // TODO: Make sure this makes sense.. - const resetZoom = () => { - editor?.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); - }; - - const getActiveObjects = () => { - console.log(editor?.canvas.getActiveObjects()); - }; - - const addRandom = () => { - // Calculate the coordinates relative to the image - // (x1, y1) = (133, 460) - const rect = new fabric.Rect({ - scaleX: scaleRatio, - scaleY: scaleRatio, - left: WIDTH / 2 - (imageSize.width * scaleRatio) / 2 + 133 * scaleRatio, // Specify the left coordinate relative to image - top: HEIGHT / 2 - (imageSize.height * scaleRatio) / 2 + 460 * scaleRatio, - width: 73, - height: 33, - fill: undefined, - stroke: "red", - strokeWidth: 1, - selectable: true, - }); - editor?.canvas.add(rect); - }; - - const onAddRectangle = () => { - // editor?.addRectangle(); - const rect = new fabric.Rect({ - left: 0, - top: 0, - originX: "left", - originY: "top", - width: 100, - height: 100, - fill: "rgba(255,127,39,1)", - selectable: true, - visible: false, - }); - - const renderIcon = (ctx, left, top, styleOverride, fabricObject) => { - const deleteIcon = - "data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg version='1.1' id='Ebene_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='595.275px' height='595.275px' viewBox='200 215 230 470' xml:space='preserve'%3E%3Ccircle style='fill:%23F44336;' cx='299.76' cy='439.067' r='218.516'/%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18'/%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179'/%3E%3C/g%3E%3C/svg%3E"; - - const img = document.createElement("img"); - img.src = deleteIcon; - const size = 24; - ctx.save(); - ctx.translate(left, top); - ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle)); - ctx.drawImage(img, -size / 2, -size / 2, size, size); - ctx.restore(); - }; - - const onDelete = () => { - editor?.deleteSelected(); - return true; - }; - - rect.controls = { - onDelete: new fabric.Control({ - x: 0.5, - y: -0.5, - offsetY: 16, - cursorStyle: "pointer", - mouseUpHandler: () => onDelete(), - render: renderIcon, - }), - }; - - editor?.canvas.add(rect); - // setAction({ primitive: "rectangle", operation: "add" }); - }; - - const draggingState = () => { - setDraggingEnabled(!draggingEnabled); - }; - - const getVisible = () => { - console.log(editor?.canvas.getObjects()?.[0].visible); - }; - - return ( -
- - - - - - -
- -
Zoom: {Math.round(currentZoom)}%
-
-
- ); -}; - -export default ExampleMain; diff --git a/tsconfig.json b/tsconfig.json index 1efc5b9..9ce70a3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,6 +20,7 @@ "noUnusedParameters": true, // Flags unused function parameters. "noFallthroughCasesInSwitch": true, // Requires handling all cases in a switch statement. "declaration": true, // Generates declaration files for TypeScript. + }, "include": ["src"], // Specifies the directory to include when searching for TypeScript files. "exclude": [ From d3d5c2fdee1c419af75663e02a3f1ec79adc2f3b Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Tue, 2 Apr 2024 11:12:18 +0100 Subject: [PATCH 03/10] Removed extra space from tsconfig --- tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 9ce70a3..1efc5b9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -20,7 +20,6 @@ "noUnusedParameters": true, // Flags unused function parameters. "noFallthroughCasesInSwitch": true, // Requires handling all cases in a switch statement. "declaration": true, // Generates declaration files for TypeScript. - }, "include": ["src"], // Specifies the directory to include when searching for TypeScript files. "exclude": [ From f41cae1b363beb0b26522f47b13fbf0da72231ce Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Tue, 2 Apr 2024 11:14:47 +0100 Subject: [PATCH 04/10] Enabled visibility of addRectangle --- src/components/Board/Board.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 05d5bbd..79b1249 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -160,7 +160,7 @@ const Board: React.FC = ({ primary = true }) => { height: 100, fill: "rgba(255,127,39,1)", selectable: true, - visible: false, + visible: true, }); const renderIcon = ( From b0fb08087076672aa5ac1393720df2551e98a787 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Tue, 2 Apr 2024 15:37:46 +0100 Subject: [PATCH 05/10] Actions on board working --- src/components/Board/Board.tsx | 304 +++++++++++------- src/components/Board/__docs__/Board.mdx | 6 +- .../Board/__docs__/Board.stories.tsx | 20 +- src/components/Board/__docs__/Example.tsx | 33 +- src/components/Board/hooks.tsx | 16 + src/components/Board/index.ts | 1 + src/components/Board/types.tsx | 7 + src/components/Tooltip/Tooltip.tsx | 79 +++++ src/components/Tooltip/__docs__/Example.tsx | 19 ++ src/components/Tooltip/__docs__/Tooltip.mdx | 37 +++ .../Tooltip/__docs__/Tooltip.stories.tsx | 16 + .../Tooltip/__test__/Tooltip.test.tsx | 13 + src/components/Tooltip/index.ts | 1 + 13 files changed, 422 insertions(+), 130 deletions(-) create mode 100644 src/components/Board/hooks.tsx create mode 100644 src/components/Board/types.tsx create mode 100644 src/components/Tooltip/Tooltip.tsx create mode 100644 src/components/Tooltip/__docs__/Example.tsx create mode 100644 src/components/Tooltip/__docs__/Tooltip.mdx create mode 100644 src/components/Tooltip/__docs__/Tooltip.stories.tsx create mode 100644 src/components/Tooltip/__test__/Tooltip.test.tsx create mode 100644 src/components/Tooltip/index.ts diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 79b1249..a50fd2c 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -2,12 +2,18 @@ import React, { useEffect, useState } from "react"; import { fabric } from "fabric"; import { FabricJSCanvas, useFabricJSEditor } from "fabricjs-react"; import tokens from "../../tokens"; - -const WIDTH = 800; -const HEIGHT = 500; +import { CanvasObject } from "./types"; +import styled from "styled-components"; export type BoardProps = { primary?: boolean; + items: CanvasObject[]; + imageSrc: string; + onLoaded: (actions: BoardActions) => void; +}; + +export type BoardActions = { + alert: () => void; }; type CanvasAnnotationState = { @@ -17,8 +23,26 @@ type CanvasAnnotationState = { isDragging?: boolean; }; -const Board: React.FC = ({ primary = true }) => { +const StyledCanvas = styled.div` + width: 100%; + height: 100%; +`; + +// const Board = React.forwardRef( +// ({ primary = true, imageSrc }, ref) => { +// // Set board actions +// useImperativeHandle(ref, () => ({ +// alert() { +// console.log("getAlert from Child"); +// }, +// })); +const Board: React.FC = ({ + primary = true, + imageSrc, + onLoaded, +}) => { const { editor, onReady } = useFabricJSEditor(); + const [currentZoom, setCurrentZoom] = useState(100); const [scaleRatio, setScaleRation] = useState(100); const [imageSize, setImageSize] = useState({ @@ -28,8 +52,20 @@ const Board: React.FC = ({ primary = true }) => { const [draggingEnabled, setDraggingEnabled] = useState(false); + const boardActions = React.useRef({ + alert: () => console.log("HERE! - ALERT!"), + }); + + // Set available actions for parent useEffect(() => { - if (!editor || !fabric) { + onLoaded(boardActions.current); + }, [onLoaded]); + + useEffect(() => { + const parentCanvasElement = document.getElementById( + "react-annotator-canvas", + ); + if (!editor || !fabric || !parentCanvasElement) { return; } @@ -38,14 +74,15 @@ const Board: React.FC = ({ primary = true }) => { ? tokens.primary.backgroundColor : tokens.secondary.backgroundColor; - editor.canvas.setWidth(WIDTH); - editor.canvas.setHeight(HEIGHT); + // Set FabricJS canvas width and height + editor.canvas.setWidth(parentCanvasElement.clientWidth); + editor.canvas.setHeight(parentCanvasElement.clientHeight); // Change the cursor editor.canvas.defaultCursor = draggingEnabled ? "pointer" : "default"; fabric.Image.fromURL( - "holder-min.jpg", + imageSrc, (img) => { const { canvas } = editor; const scaleRatio = Math.min( @@ -119,120 +156,153 @@ const Board: React.FC = ({ primary = true }) => { opt.e.stopPropagation(); }); + // Selected Objects + editor.canvas.on( + "selection:created", + function (this: CanvasAnnotationState, opt) { + console.log("SELECTED! ", opt.selected?.[0]); + + opt.e.preventDefault(); + opt.e.stopPropagation(); + }, + ); + + // editor.canvas.on("mouse:over", function (this: CanvasAnnotationState, opt) { + // opt.target?.set("fill", "green"); + // editor.canvas.renderAll(); + + // opt.e.preventDefault(); + // opt.e.stopPropagation(); + // }); + + // editor.canvas.on("mouse:out", function (this: CanvasAnnotationState, opt) { + // opt.target?.set("fill", "rgba(255,127,39,1)"); + // editor.canvas.renderAll(); + + // opt.e.preventDefault(); + // opt.e.stopPropagation(); + // }); + editor.canvas.renderAll(); - }, [primary, draggingEnabled, editor]); + }, [primary, draggingEnabled, editor, imageSrc]); // TODO: Make sure this makes sense.. - const resetZoom = () => { - editor?.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); - }; - - const getActiveObjects = () => { - console.log(editor?.canvas.getActiveObjects()); - }; - - const addRandom = () => { - // Calculate the coordinates relative to the image - // (x1, y1) = (133, 460) - const rect = new fabric.Rect({ - scaleX: scaleRatio, - scaleY: scaleRatio, - left: WIDTH / 2 - (imageSize.width * scaleRatio) / 2 + 133 * scaleRatio, // Specify the left coordinate relative to image - top: HEIGHT / 2 - (imageSize.height * scaleRatio) / 2 + 460 * scaleRatio, - width: 73, - height: 33, - fill: undefined, - stroke: "red", - strokeWidth: 1, - selectable: true, - }); - editor?.canvas.add(rect); - }; - - const onAddRectangle = () => { - // editor?.addRectangle(); - const rect = new fabric.Rect({ - left: 0, - top: 0, - originX: "left", - originY: "top", - width: 100, - height: 100, - fill: "rgba(255,127,39,1)", - selectable: true, - visible: true, - }); - - const renderIcon = ( - ctx: CanvasRenderingContext2D, - left: number, - top: number, - styleOverride: unknown, - fabricObject: fabric.Object, - ) => { - const deleteIcon = - "data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg version='1.1' id='Ebene_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='595.275px' height='595.275px' viewBox='200 215 230 470' xml:space='preserve'%3E%3Ccircle style='fill:%23F44336;' cx='299.76' cy='439.067' r='218.516'/%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18'/%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179'/%3E%3C/g%3E%3C/svg%3E"; - - const img = document.createElement("img"); - img.src = deleteIcon; - const size = 24; - ctx.save(); - ctx.translate(left, top); - if (fabricObject.angle) - ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle)); - ctx.drawImage(img, -size / 2, -size / 2, size, size); - ctx.restore(); - }; - - const onDelete = () => { - editor?.deleteSelected(); - return true; - }; - - rect.controls = { - onDelete: new fabric.Control({ - x: 0.5, - y: -0.5, - offsetY: 16, - cursorStyle: "pointer", - mouseUpHandler: () => onDelete(), - render: renderIcon, - }), - }; - - editor?.canvas.add(rect); - // setAction({ primitive: "rectangle", operation: "add" }); - }; - - const draggingState = () => { - setDraggingEnabled(!draggingEnabled); - }; - - const getVisible = () => { - console.log(editor?.canvas.getObjects()?.[0].visible); - }; + // const resetZoom = () => { + // editor?.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); + // }; + + // const getActiveObjects = () => { + // console.log(editor?.canvas.getActiveObjects()); + // }; + + // const addRandom = () => { + // // Calculate the coordinates relative to the image + // // (x1, y1) = (133, 460) + // const rect = new fabric.Rect({ + // scaleX: scaleRatio, + // scaleY: scaleRatio, + // left: WIDTH / 2 - (imageSize.width * scaleRatio) / 2 + 133 * scaleRatio, // Specify the left coordinate relative to image + // top: HEIGHT / 2 - (imageSize.height * scaleRatio) / 2 + 460 * scaleRatio, + // width: 73, + // height: 33, + // fill: undefined, + // stroke: "red", + // strokeWidth: 1, + // selectable: true, + // }); + // editor?.canvas.add(rect); + // }; + + // const onAddRectangle = () => { + // // editor?.addRectangle(); + // const rect = new fabric.Rect({ + // name: "MERDAS", + // left: 0, + // top: 0, + // originX: "left", + // originY: "top", + // width: 100, + // height: 100, + // fill: "rgba(255,127,39,1)", + // selectable: true, + // visible: true, + // }); + + // const renderIcon = ( + // ctx: CanvasRenderingContext2D, + // left: number, + // top: number, + // styleOverride: unknown, + // fabricObject: fabric.Object, + // ) => { + // const deleteIcon = + // "data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg version='1.1' id='Ebene_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='595.275px' height='595.275px' viewBox='200 215 230 470' xml:space='preserve'%3E%3Ccircle style='fill:%23F44336;' cx='299.76' cy='439.067' r='218.516'/%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18'/%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179'/%3E%3C/g%3E%3C/svg%3E"; + + // const img = document.createElement("img"); + // img.src = deleteIcon; + // const size = 24; + // ctx.save(); + // ctx.translate(left, top); + // if (fabricObject.angle) + // ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle)); + // ctx.drawImage(img, -size / 2, -size / 2, size, size); + // ctx.restore(); + // }; + + // const onDelete = () => { + // editor?.deleteSelected(); + // return true; + // }; + + // rect.controls = { + // onDelete: new fabric.Control({ + // x: 0.5, + // y: -0.5, + // offsetY: 16, + // cursorStyle: "pointer", + // mouseUpHandler: () => onDelete(), + // render: renderIcon, + // }), + // }; + + // editor?.canvas.add(rect); + // // setAction({ primitive: "rectangle", operation: "add" }); + // }; + + // const draggingState = () => { + // setDraggingEnabled(!draggingEnabled); + // }; + + // const getVisible = () => { + // console.log(editor?.canvas.getObjects()?.[0].visible); + // }; return ( -
- - - - - - -
- -
Zoom: {Math.round(currentZoom)}%
-
-
+ <> + + + + + //
+ // + // + // + // + // + // + //
+ // + //
Zoom: {Math.round(currentZoom)}%
+ //
+ //
); }; diff --git a/src/components/Board/__docs__/Board.mdx b/src/components/Board/__docs__/Board.mdx index 506f91f..e04adad 100644 --- a/src/components/Board/__docs__/Board.mdx +++ b/src/components/Board/__docs__/Board.mdx @@ -10,7 +10,7 @@ Button component with different props. #### Example - + ## Usage @@ -18,9 +18,13 @@ Button component with different props. import {Board} from "react-image-annotator"; const Example = () => { + const { onLoaded } = useBoardActions(); return ( ); }; diff --git a/src/components/Board/__docs__/Board.stories.tsx b/src/components/Board/__docs__/Board.stories.tsx index 7c01ac4..300f741 100644 --- a/src/components/Board/__docs__/Board.stories.tsx +++ b/src/components/Board/__docs__/Board.stories.tsx @@ -1,16 +1,34 @@ import type { Meta, StoryObj } from "@storybook/react"; import Example from "./Example"; +import { CanvasObject } from "../types"; const meta: Meta = { title: "Board", component: Example, }; +const ITEMS: CanvasObject[] = [ + { + id: "1", + category: "category1", + color: "green", + value: "⌀42", + coords: [ + [55, 382], + [128, 382], + [128, 415], + [55, 415], + ], + }, +]; +console.log(ITEMS); export default meta; type Story = StoryObj; -export const Primary: Story = { +export const Main: Story = { args: { primary: true, + imageSrc: "holder-min.jpg", + items: ITEMS, }, }; diff --git a/src/components/Board/__docs__/Example.tsx b/src/components/Board/__docs__/Example.tsx index 1c2fff1..750fdf5 100644 --- a/src/components/Board/__docs__/Example.tsx +++ b/src/components/Board/__docs__/Example.tsx @@ -1,18 +1,29 @@ import React, { FC } from "react"; import Board, { BoardProps } from "../Board"; +import useBoardActions from "../hooks"; -const Example: FC = ({ primary = true }) => { +const Example: FC = ({ primary = true, items, imageSrc }) => { + const { actions, onLoaded } = useBoardActions(); return ( -
- -
+ <> +
+ +
+ + ); }; diff --git a/src/components/Board/hooks.tsx b/src/components/Board/hooks.tsx new file mode 100644 index 0000000..b3e93d8 --- /dev/null +++ b/src/components/Board/hooks.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { BoardActions } from "./Board"; + +const useBoardActions = () => { + const [actions, setActions] = React.useState({ + alert: () => console.error("Missing Implementation!"), + }); + + const onLoaded = (actions: BoardActions) => { + setActions(actions); + }; + + return { actions, onLoaded }; +}; + +export default useBoardActions; diff --git a/src/components/Board/index.ts b/src/components/Board/index.ts index ccc93b8..1b7ab1d 100644 --- a/src/components/Board/index.ts +++ b/src/components/Board/index.ts @@ -1 +1,2 @@ export { default as Board } from "./Board"; +export * from "./hooks"; diff --git a/src/components/Board/types.tsx b/src/components/Board/types.tsx new file mode 100644 index 0000000..b76146b --- /dev/null +++ b/src/components/Board/types.tsx @@ -0,0 +1,7 @@ +export type CanvasObject = { + id: string; + category: string; + color: string; + value: string; + coords: number[][]; +}; diff --git a/src/components/Tooltip/Tooltip.tsx b/src/components/Tooltip/Tooltip.tsx new file mode 100644 index 0000000..c21627e --- /dev/null +++ b/src/components/Tooltip/Tooltip.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import styled from "styled-components"; + +export type TooltipProps = { + primary?: boolean; +}; + +const TooltipText = styled.div` + background: rgba(28, 56, 151, 0.9); + color: #fff; + width: 200px; + text-align: center; + line-height: 44px; + border-radius: 3px; + cursor: pointer; +`; +const TooltipBox = styled.div` + position: absolute; + top: calc(100% + 10px); + left: 30px; + visibility: hidden; + color: transparent; + background-color: transparent; + width: 150px; + padding: 5px 5px; + border-radius: 4px; + transition: + visibility 0.5s, + color 0.5s, + background-color 0.5s, + width 0.5s, + padding 0.5s ease-in-out; + &:before { + content: ""; + width: 0; + height: 0; + left: 40px; + top: -10px; + position: absolute; + border: 10px solid transparent; + transform: rotate(135deg); + transition: border 0.3s ease-in-out; + } +`; +const TooltipCard = styled.div` + position: relative; + & ${TooltipText}:hover + ${TooltipBox} { + visibility: visible; + color: #fff; + background-color: rgba(0, 0, 0, 0.8); + width: 230px; + padding: 8px 8px; + &:before { + border-color: transparent transparent rgba(0, 0, 0, 0.8) + rgba(0, 0, 0, 0.8); + } + } +`; + +const Tooltip: React.FC = () => { + return ( + <> + + +

Hover :D

+
+ +

First item

+

Second item

+
+
+

+ Some content that is right below Hover :D +

+ + ); +}; + +export default Tooltip; diff --git a/src/components/Tooltip/__docs__/Example.tsx b/src/components/Tooltip/__docs__/Example.tsx new file mode 100644 index 0000000..348eb4b --- /dev/null +++ b/src/components/Tooltip/__docs__/Example.tsx @@ -0,0 +1,19 @@ +import React, { FC } from "react"; +import Tooltip, { TooltipProps } from "../Tooltip"; + +const Example: FC = ({ primary = true }) => { + return ( +
+ +
+ ); +}; + +export default Example; diff --git a/src/components/Tooltip/__docs__/Tooltip.mdx b/src/components/Tooltip/__docs__/Tooltip.mdx new file mode 100644 index 0000000..deb0dd7 --- /dev/null +++ b/src/components/Tooltip/__docs__/Tooltip.mdx @@ -0,0 +1,37 @@ +import { Canvas, Meta } from "@storybook/blocks"; +import Example from "./Example.tsx"; +import * as Tooltip from "./Tooltip.stories.tsx"; + + + +# Tooltip + +Button component with different props. + +#### Example + + + +## Usage + +```ts +import {Tooltip} from "react-image-annotator"; + +const Example = () => { + return ( + + ); +}; + +export default Example; +``` + +#### Arguments + +- **text** _`() => void`_ - A string that represents the text content of the button. +- **primary** - A boolean indicating whether the button should have a primary styling or not. Typically, a primary button stands out as the main action in a user interface. +- **disabled** - A boolean indicating whether the button should be disabled or not. When disabled, the button cannot be clicked or interacted with. +- **size** - A string with one of three possible values: "small," "medium," or "large." It defines the size or dimensions of the button. +- **onClick** - A function that is called when the button is clicked. It receives a MouseEventHandler for handling the click event on the button element. \ No newline at end of file diff --git a/src/components/Tooltip/__docs__/Tooltip.stories.tsx b/src/components/Tooltip/__docs__/Tooltip.stories.tsx new file mode 100644 index 0000000..ab7c5da --- /dev/null +++ b/src/components/Tooltip/__docs__/Tooltip.stories.tsx @@ -0,0 +1,16 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import Example from "./Example"; + +const meta: Meta = { + title: "Tooltip", + component: Example, +}; + +export default meta; +type Story = StoryObj; + +export const Primary: Story = { + args: { + primary: true, + }, +}; diff --git a/src/components/Tooltip/__test__/Tooltip.test.tsx b/src/components/Tooltip/__test__/Tooltip.test.tsx new file mode 100644 index 0000000..7faf0f6 --- /dev/null +++ b/src/components/Tooltip/__test__/Tooltip.test.tsx @@ -0,0 +1,13 @@ +import React from "react"; +import { describe, expect, it } from "vitest"; +import { render, screen } from "@testing-library/react"; +import Tooltip from "../Tooltip"; +import "@testing-library/jest-dom"; // This needs to be here for now. + +describe("Tooltip component", () => { + it("Tooltip should render correctly", () => { + render(); + const tooltip = screen.getByRole("tooltip"); + expect(tooltip).toBeInTheDocument(); + }); +}); diff --git a/src/components/Tooltip/index.ts b/src/components/Tooltip/index.ts new file mode 100644 index 0000000..6769490 --- /dev/null +++ b/src/components/Tooltip/index.ts @@ -0,0 +1 @@ +export { default as Tooltip } from "./Tooltip"; From 906b7a5f2b213544c6cfe68882b3c254a9fe063a Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Tue, 2 Apr 2024 16:13:32 +0100 Subject: [PATCH 06/10] Prepare usage of useImperativeHandle --- src/components/Board/Board.tsx | 6 ++++++ src/components/Board/__docs__/Example.tsx | 2 ++ src/components/Board/hooks.tsx | 3 ++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index a50fd2c..672d62c 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -14,6 +14,7 @@ export type BoardProps = { export type BoardActions = { alert: () => void; + setDragging: () => void; }; type CanvasAnnotationState = { @@ -54,6 +55,11 @@ const Board: React.FC = ({ const boardActions = React.useRef({ alert: () => console.log("HERE! - ALERT!"), + setDragging: () => { + const vv = !draggingEnabled; + console.log(`New value: ${vv}`); + setDraggingEnabled(vv); + }, }); // Set available actions for parent diff --git a/src/components/Board/__docs__/Example.tsx b/src/components/Board/__docs__/Example.tsx index 750fdf5..18491d2 100644 --- a/src/components/Board/__docs__/Example.tsx +++ b/src/components/Board/__docs__/Example.tsx @@ -4,6 +4,7 @@ import useBoardActions from "../hooks"; const Example: FC = ({ primary = true, items, imageSrc }) => { const { actions, onLoaded } = useBoardActions(); + return ( <>
= ({ primary = true, items, imageSrc }) => { />
+ ); }; diff --git a/src/components/Board/hooks.tsx b/src/components/Board/hooks.tsx index b3e93d8..10fbd71 100644 --- a/src/components/Board/hooks.tsx +++ b/src/components/Board/hooks.tsx @@ -3,7 +3,8 @@ import { BoardActions } from "./Board"; const useBoardActions = () => { const [actions, setActions] = React.useState({ - alert: () => console.error("Missing Implementation!"), + alert: () => console.error("Missing Implementation! - alert"), + setDragging: () => console.error("Missing Implementation! - setDragging"), }); const onLoaded = (actions: BoardActions) => { From 4868a15965a356f9b693edc3a40f240ef15102af Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Tue, 2 Apr 2024 19:32:22 +0100 Subject: [PATCH 07/10] Using ref to interact with component from parent --- src/components/Board/Board.tsx | 546 +++++++++--------- src/components/Board/__docs__/Board.mdx | 6 +- .../Board/__docs__/Board.stories.tsx | 2 +- src/components/Board/__docs__/Example.tsx | 15 +- src/components/Board/hooks.tsx | 17 - 5 files changed, 278 insertions(+), 308 deletions(-) delete mode 100644 src/components/Board/hooks.tsx diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 672d62c..bb20839 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -9,12 +9,10 @@ export type BoardProps = { primary?: boolean; items: CanvasObject[]; imageSrc: string; - onLoaded: (actions: BoardActions) => void; }; export type BoardActions = { - alert: () => void; - setDragging: () => void; + toggleDragging: (value?: boolean) => void; }; type CanvasAnnotationState = { @@ -29,287 +27,283 @@ const StyledCanvas = styled.div` height: 100%; `; -// const Board = React.forwardRef( -// ({ primary = true, imageSrc }, ref) => { -// // Set board actions -// useImperativeHandle(ref, () => ({ -// alert() { -// console.log("getAlert from Child"); -// }, -// })); -const Board: React.FC = ({ - primary = true, - imageSrc, - onLoaded, -}) => { - const { editor, onReady } = useFabricJSEditor(); - - const [currentZoom, setCurrentZoom] = useState(100); - const [scaleRatio, setScaleRation] = useState(100); - const [imageSize, setImageSize] = useState({ - width: 0, - height: 0, - }); - - const [draggingEnabled, setDraggingEnabled] = useState(false); - - const boardActions = React.useRef({ - alert: () => console.log("HERE! - ALERT!"), - setDragging: () => { - const vv = !draggingEnabled; - console.log(`New value: ${vv}`); - setDraggingEnabled(vv); - }, - }); - - // Set available actions for parent - useEffect(() => { - onLoaded(boardActions.current); - }, [onLoaded]); - - useEffect(() => { - const parentCanvasElement = document.getElementById( - "react-annotator-canvas", - ); - if (!editor || !fabric || !parentCanvasElement) { - return; - } - - // Background color of canvas - editor.canvas.backgroundColor = primary - ? tokens.primary.backgroundColor - : tokens.secondary.backgroundColor; - - // Set FabricJS canvas width and height - editor.canvas.setWidth(parentCanvasElement.clientWidth); - editor.canvas.setHeight(parentCanvasElement.clientHeight); - - // Change the cursor - editor.canvas.defaultCursor = draggingEnabled ? "pointer" : "default"; - - fabric.Image.fromURL( - imageSrc, - (img) => { - const { canvas } = editor; - const scaleRatio = Math.min( - (canvas.width ?? 1) / (img.width ?? 1), - (canvas.height ?? 1) / (img.height ?? 1), - ); - setImageSize({ width: img.width ?? 0, height: img.height ?? 0 }); - - setScaleRation(scaleRatio); - canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), { - scaleX: scaleRatio, - scaleY: scaleRatio, - left: (canvas.width ?? 1) / 2, - top: (canvas.height ?? 1) / 2, - originX: "middle", - originY: "middle", - }); - canvas!.renderAll(); +const Board = React.forwardRef( + ({ primary = true, imageSrc }, ref) => { + // Set board actions + React.useImperativeHandle(ref, () => ({ + toggleDragging() { + console.log("dragging!"); }, - { selectable: false }, - ); - - editor.canvas.on( - "mouse:wheel", - function (this: CanvasAnnotationState, opt) { - const delta = opt.e.deltaY; - let zoom = editor.canvas.getZoom(); - zoom *= 0.999 ** delta; - if (zoom > 20) zoom = 20; - if (zoom < 0.01) zoom = 0.01; - editor.canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom); - setCurrentZoom(zoom * 100); - opt.e.preventDefault(); - opt.e.stopPropagation(); - }, - ); - - editor.canvas.on("mouse:down", function (this: CanvasAnnotationState, opt) { - const evt = opt.e; - this.isDragging = draggingEnabled; - this.selection = false; - this.lastPosX = evt.clientX; - this.lastPosY = evt.clientY; - - opt.e.preventDefault(); - opt.e.stopPropagation(); + })); + const { editor, onReady } = useFabricJSEditor(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [currentZoom, setCurrentZoom] = useState(100); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [scaleRatio, setScaleRation] = useState(100); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [imageSize, setImageSize] = useState({ + width: 0, + height: 0, }); - editor.canvas.on("mouse:move", function (this: CanvasAnnotationState, opt) { - if (this.isDragging) { - const e = opt.e; - const vpt = editor.canvas.viewportTransform; - if (vpt) { - vpt[4] += e.clientX - this.lastPosX; - vpt[5] += e.clientY - this.lastPosY; - editor.canvas.requestRenderAll(); - this.lastPosX = e.clientX; - this.lastPosY = e.clientY; - } - } - - opt.e.preventDefault(); - opt.e.stopPropagation(); - }); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [draggingEnabled, setDraggingEnabled] = useState(false); - editor.canvas.on("mouse:up", function (this: CanvasAnnotationState, opt) { - this.isDragging = false; - this.selection = true; - - opt.e.preventDefault(); - opt.e.stopPropagation(); - }); + useEffect(() => { + const parentCanvasElement = document.getElementById( + "react-annotator-canvas", + ); + if (!editor || !fabric || !parentCanvasElement) { + return; + } - // Selected Objects - editor.canvas.on( - "selection:created", - function (this: CanvasAnnotationState, opt) { - console.log("SELECTED! ", opt.selected?.[0]); + // Background color of canvas + editor.canvas.backgroundColor = primary + ? tokens.primary.backgroundColor + : tokens.secondary.backgroundColor; + + // Set FabricJS canvas width and height + editor.canvas.setWidth(parentCanvasElement.clientWidth); + editor.canvas.setHeight(parentCanvasElement.clientHeight); + + // Change the cursor + editor.canvas.defaultCursor = draggingEnabled ? "pointer" : "default"; + + fabric.Image.fromURL( + imageSrc, + (img) => { + const { canvas } = editor; + const scaleRatio = Math.min( + (canvas.width ?? 1) / (img.width ?? 1), + (canvas.height ?? 1) / (img.height ?? 1), + ); + setImageSize({ width: img.width ?? 0, height: img.height ?? 0 }); + + setScaleRation(scaleRatio); + canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas), { + scaleX: scaleRatio, + scaleY: scaleRatio, + left: (canvas.width ?? 1) / 2, + top: (canvas.height ?? 1) / 2, + originX: "middle", + originY: "middle", + }); + canvas!.renderAll(); + }, + { selectable: false }, + ); + + editor.canvas.on( + "mouse:wheel", + function (this: CanvasAnnotationState, opt) { + const delta = opt.e.deltaY; + let zoom = editor.canvas.getZoom(); + zoom *= 0.999 ** delta; + if (zoom > 20) zoom = 20; + if (zoom < 0.01) zoom = 0.01; + editor.canvas.zoomToPoint( + { x: opt.e.offsetX, y: opt.e.offsetY }, + zoom, + ); + setCurrentZoom(zoom * 100); + opt.e.preventDefault(); + opt.e.stopPropagation(); + }, + ); + + editor.canvas.on( + "mouse:down", + function (this: CanvasAnnotationState, opt) { + const evt = opt.e; + this.isDragging = draggingEnabled; + this.selection = false; + this.lastPosX = evt.clientX; + this.lastPosY = evt.clientY; + + opt.e.preventDefault(); + opt.e.stopPropagation(); + }, + ); + + editor.canvas.on( + "mouse:move", + function (this: CanvasAnnotationState, opt) { + if (this.isDragging) { + const e = opt.e; + const vpt = editor.canvas.viewportTransform; + if (vpt) { + vpt[4] += e.clientX - this.lastPosX; + vpt[5] += e.clientY - this.lastPosY; + editor.canvas.requestRenderAll(); + this.lastPosX = e.clientX; + this.lastPosY = e.clientY; + } + } + + opt.e.preventDefault(); + opt.e.stopPropagation(); + }, + ); + + editor.canvas.on("mouse:up", function (this: CanvasAnnotationState, opt) { + this.isDragging = false; + this.selection = true; opt.e.preventDefault(); opt.e.stopPropagation(); - }, + }); + + // Selected Objects + editor.canvas.on( + "selection:created", + function (this: CanvasAnnotationState, opt) { + console.log("SELECTED! ", opt.selected?.[0]); + + opt.e.preventDefault(); + opt.e.stopPropagation(); + }, + ); + + // editor.canvas.on("mouse:over", function (this: CanvasAnnotationState, opt) { + // opt.target?.set("fill", "green"); + // editor.canvas.renderAll(); + + // opt.e.preventDefault(); + // opt.e.stopPropagation(); + // }); + + // editor.canvas.on("mouse:out", function (this: CanvasAnnotationState, opt) { + // opt.target?.set("fill", "rgba(255,127,39,1)"); + // editor.canvas.renderAll(); + + // opt.e.preventDefault(); + // opt.e.stopPropagation(); + // }); + + editor.canvas.renderAll(); + }, [primary, draggingEnabled, editor, imageSrc]); + + // TODO: Make sure this makes sense.. + // const resetZoom = () => { + // editor?.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); + // }; + + // const getActiveObjects = () => { + // console.log(editor?.canvas.getActiveObjects()); + // }; + + // const addRandom = () => { + // // Calculate the coordinates relative to the image + // // (x1, y1) = (133, 460) + // const rect = new fabric.Rect({ + // scaleX: scaleRatio, + // scaleY: scaleRatio, + // left: WIDTH / 2 - (imageSize.width * scaleRatio) / 2 + 133 * scaleRatio, // Specify the left coordinate relative to image + // top: HEIGHT / 2 - (imageSize.height * scaleRatio) / 2 + 460 * scaleRatio, + // width: 73, + // height: 33, + // fill: undefined, + // stroke: "red", + // strokeWidth: 1, + // selectable: true, + // }); + // editor?.canvas.add(rect); + // }; + + // const onAddRectangle = () => { + // // editor?.addRectangle(); + // const rect = new fabric.Rect({ + // name: "MERDAS", + // left: 0, + // top: 0, + // originX: "left", + // originY: "top", + // width: 100, + // height: 100, + // fill: "rgba(255,127,39,1)", + // selectable: true, + // visible: true, + // }); + + // const renderIcon = ( + // ctx: CanvasRenderingContext2D, + // left: number, + // top: number, + // styleOverride: unknown, + // fabricObject: fabric.Object, + // ) => { + // const deleteIcon = + // "data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg version='1.1' id='Ebene_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='595.275px' height='595.275px' viewBox='200 215 230 470' xml:space='preserve'%3E%3Ccircle style='fill:%23F44336;' cx='299.76' cy='439.067' r='218.516'/%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18'/%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179'/%3E%3C/g%3E%3C/svg%3E"; + + // const img = document.createElement("img"); + // img.src = deleteIcon; + // const size = 24; + // ctx.save(); + // ctx.translate(left, top); + // if (fabricObject.angle) + // ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle)); + // ctx.drawImage(img, -size / 2, -size / 2, size, size); + // ctx.restore(); + // }; + + // const onDelete = () => { + // editor?.deleteSelected(); + // return true; + // }; + + // rect.controls = { + // onDelete: new fabric.Control({ + // x: 0.5, + // y: -0.5, + // offsetY: 16, + // cursorStyle: "pointer", + // mouseUpHandler: () => onDelete(), + // render: renderIcon, + // }), + // }; + + // editor?.canvas.add(rect); + // // setAction({ primitive: "rectangle", operation: "add" }); + // }; + + // const draggingState = () => { + // setDraggingEnabled(!draggingEnabled); + // }; + + // const getVisible = () => { + // console.log(editor?.canvas.getObjects()?.[0].visible); + // }; + + return ( + <> + + + + + //
+ // + // + // + // + // + // + //
+ // + //
Zoom: {Math.round(currentZoom)}%
+ //
+ //
); + }, +); - // editor.canvas.on("mouse:over", function (this: CanvasAnnotationState, opt) { - // opt.target?.set("fill", "green"); - // editor.canvas.renderAll(); - - // opt.e.preventDefault(); - // opt.e.stopPropagation(); - // }); - - // editor.canvas.on("mouse:out", function (this: CanvasAnnotationState, opt) { - // opt.target?.set("fill", "rgba(255,127,39,1)"); - // editor.canvas.renderAll(); - - // opt.e.preventDefault(); - // opt.e.stopPropagation(); - // }); - - editor.canvas.renderAll(); - }, [primary, draggingEnabled, editor, imageSrc]); - - // TODO: Make sure this makes sense.. - // const resetZoom = () => { - // editor?.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); - // }; - - // const getActiveObjects = () => { - // console.log(editor?.canvas.getActiveObjects()); - // }; - - // const addRandom = () => { - // // Calculate the coordinates relative to the image - // // (x1, y1) = (133, 460) - // const rect = new fabric.Rect({ - // scaleX: scaleRatio, - // scaleY: scaleRatio, - // left: WIDTH / 2 - (imageSize.width * scaleRatio) / 2 + 133 * scaleRatio, // Specify the left coordinate relative to image - // top: HEIGHT / 2 - (imageSize.height * scaleRatio) / 2 + 460 * scaleRatio, - // width: 73, - // height: 33, - // fill: undefined, - // stroke: "red", - // strokeWidth: 1, - // selectable: true, - // }); - // editor?.canvas.add(rect); - // }; - - // const onAddRectangle = () => { - // // editor?.addRectangle(); - // const rect = new fabric.Rect({ - // name: "MERDAS", - // left: 0, - // top: 0, - // originX: "left", - // originY: "top", - // width: 100, - // height: 100, - // fill: "rgba(255,127,39,1)", - // selectable: true, - // visible: true, - // }); - - // const renderIcon = ( - // ctx: CanvasRenderingContext2D, - // left: number, - // top: number, - // styleOverride: unknown, - // fabricObject: fabric.Object, - // ) => { - // const deleteIcon = - // "data:image/svg+xml,%3C%3Fxml version='1.0' encoding='utf-8'%3F%3E%3C!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'%3E%3Csvg version='1.1' id='Ebene_1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' x='0px' y='0px' width='595.275px' height='595.275px' viewBox='200 215 230 470' xml:space='preserve'%3E%3Ccircle style='fill:%23F44336;' cx='299.76' cy='439.067' r='218.516'/%3E%3Cg%3E%3Crect x='267.162' y='307.978' transform='matrix(0.7071 -0.7071 0.7071 0.7071 -222.6202 340.6915)' style='fill:white;' width='65.545' height='262.18'/%3E%3Crect x='266.988' y='308.153' transform='matrix(0.7071 0.7071 -0.7071 0.7071 398.3889 -83.3116)' style='fill:white;' width='65.544' height='262.179'/%3E%3C/g%3E%3C/svg%3E"; - - // const img = document.createElement("img"); - // img.src = deleteIcon; - // const size = 24; - // ctx.save(); - // ctx.translate(left, top); - // if (fabricObject.angle) - // ctx.rotate(fabric.util.degreesToRadians(fabricObject.angle)); - // ctx.drawImage(img, -size / 2, -size / 2, size, size); - // ctx.restore(); - // }; - - // const onDelete = () => { - // editor?.deleteSelected(); - // return true; - // }; - - // rect.controls = { - // onDelete: new fabric.Control({ - // x: 0.5, - // y: -0.5, - // offsetY: 16, - // cursorStyle: "pointer", - // mouseUpHandler: () => onDelete(), - // render: renderIcon, - // }), - // }; - - // editor?.canvas.add(rect); - // // setAction({ primitive: "rectangle", operation: "add" }); - // }; - - // const draggingState = () => { - // setDraggingEnabled(!draggingEnabled); - // }; - - // const getVisible = () => { - // console.log(editor?.canvas.getObjects()?.[0].visible); - // }; - - return ( - <> - - - - - //
- // - // - // - // - // - // - //
- // - //
Zoom: {Math.round(currentZoom)}%
- //
- //
- ); -}; - +Board.displayName = "Board"; export default Board; diff --git a/src/components/Board/__docs__/Board.mdx b/src/components/Board/__docs__/Board.mdx index e04adad..d0cf05c 100644 --- a/src/components/Board/__docs__/Board.mdx +++ b/src/components/Board/__docs__/Board.mdx @@ -15,15 +15,15 @@ Button component with different props. ## Usage ```ts -import {Board} from "react-image-annotator"; +import Board {BoardActions} from "react-image-annotator"; const Example = () => { - const { onLoaded } = useBoardActions(); + const ref = React.createRef(); return ( ); diff --git a/src/components/Board/__docs__/Board.stories.tsx b/src/components/Board/__docs__/Board.stories.tsx index 300f741..73af7a2 100644 --- a/src/components/Board/__docs__/Board.stories.tsx +++ b/src/components/Board/__docs__/Board.stories.tsx @@ -21,7 +21,7 @@ const ITEMS: CanvasObject[] = [ ], }, ]; -console.log(ITEMS); + export default meta; type Story = StoryObj; diff --git a/src/components/Board/__docs__/Example.tsx b/src/components/Board/__docs__/Example.tsx index 18491d2..f4a2b16 100644 --- a/src/components/Board/__docs__/Example.tsx +++ b/src/components/Board/__docs__/Example.tsx @@ -1,9 +1,8 @@ import React, { FC } from "react"; -import Board, { BoardProps } from "../Board"; -import useBoardActions from "../hooks"; +import Board, { BoardActions, BoardProps } from "../Board"; const Example: FC = ({ primary = true, items, imageSrc }) => { - const { actions, onLoaded } = useBoardActions(); + const ref = React.createRef(); return ( <> @@ -16,15 +15,9 @@ const Example: FC = ({ primary = true, items, imageSrc }) => { height: "500px", }} > - + - - + ); }; diff --git a/src/components/Board/hooks.tsx b/src/components/Board/hooks.tsx deleted file mode 100644 index 10fbd71..0000000 --- a/src/components/Board/hooks.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import React from "react"; -import { BoardActions } from "./Board"; - -const useBoardActions = () => { - const [actions, setActions] = React.useState({ - alert: () => console.error("Missing Implementation! - alert"), - setDragging: () => console.error("Missing Implementation! - setDragging"), - }); - - const onLoaded = (actions: BoardActions) => { - setActions(actions); - }; - - return { actions, onLoaded }; -}; - -export default useBoardActions; From 56c53597ab593b27565b5f4a42c346e1f5cafcc7 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Wed, 3 Apr 2024 10:22:24 +0100 Subject: [PATCH 08/10] Working on board component --- src/components/Board/Board.tsx | 76 ++++++++++++++++++----- src/components/Board/__docs__/Example.tsx | 37 ++++++++++- 2 files changed, 93 insertions(+), 20 deletions(-) diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index bb20839..875a626 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -9,10 +9,26 @@ export type BoardProps = { primary?: boolean; items: CanvasObject[]; imageSrc: string; + initialStatus?: { + draggingEnabled?: boolean; + currentZoom?: number; + scaleRatio?: number; + }; + onResetZoom?: () => void; + onZoomChange?: (currentZoom: number) => void; + onToggleDragging?: (currentStatus: boolean) => void; + onLoadedImage?: ({ + width, + height, + }: { + width: number; + height: number; + }) => void; }; export type BoardActions = { toggleDragging: (value?: boolean) => void; + resetZoom: () => void; }; type CanvasAnnotationState = { @@ -28,27 +44,49 @@ const StyledCanvas = styled.div` `; const Board = React.forwardRef( - ({ primary = true, imageSrc }, ref) => { + ( + { + primary = true, + imageSrc, + initialStatus, + onToggleDragging, + onResetZoom, + onZoomChange, + onLoadedImage, + }, + ref, + ) => { // Set board actions React.useImperativeHandle(ref, () => ({ toggleDragging() { - console.log("dragging!"); + const newStatus = !draggingEnabled; + setDraggingEnabled(newStatus); + onToggleDragging?.(newStatus); + }, + resetZoom() { + editor?.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); + onResetZoom?.(); }, })); const { editor, onReady } = useFabricJSEditor(); + const [currentZoom, setCurrentZoom] = useState( + initialStatus?.currentZoom || 100, + ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [currentZoom, setCurrentZoom] = useState(100); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [scaleRatio, setScaleRation] = useState(100); + const [scaleRatio, setScaleRation] = useState( + initialStatus?.scaleRatio || 100, + ); // eslint-disable-next-line @typescript-eslint/no-unused-vars const [imageSize, setImageSize] = useState({ width: 0, height: 0, }); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [draggingEnabled, setDraggingEnabled] = useState(false); + const [draggingEnabled, setDraggingEnabled] = useState( + initialStatus?.draggingEnabled || false, + ); useEffect(() => { const parentCanvasElement = document.getElementById( @@ -90,6 +128,7 @@ const Board = React.forwardRef( originY: "middle", }); canvas!.renderAll(); + onLoadedImage?.({ width: img.width ?? 0, height: img.height ?? 0 }); }, { selectable: false }, ); @@ -182,12 +221,19 @@ const Board = React.forwardRef( // }); editor.canvas.renderAll(); - }, [primary, draggingEnabled, editor, imageSrc]); - - // TODO: Make sure this makes sense.. - // const resetZoom = () => { - // editor?.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); - // }; + }, [ + primary, + draggingEnabled, + editor, + imageSrc, + onLoadedImage, + onZoomChange, + ]); + + // Update zoom parent value + useEffect(() => { + onZoomChange?.(Math.round(currentZoom)); + }, [currentZoom, onZoomChange]); // const getActiveObjects = () => { // console.log(editor?.canvas.getActiveObjects()); @@ -267,10 +313,6 @@ const Board = React.forwardRef( // // setAction({ primitive: "rectangle", operation: "add" }); // }; - // const draggingState = () => { - // setDraggingEnabled(!draggingEnabled); - // }; - // const getVisible = () => { // console.log(editor?.canvas.getObjects()?.[0].visible); // }; diff --git a/src/components/Board/__docs__/Example.tsx b/src/components/Board/__docs__/Example.tsx index f4a2b16..7a94a6c 100644 --- a/src/components/Board/__docs__/Example.tsx +++ b/src/components/Board/__docs__/Example.tsx @@ -1,11 +1,35 @@ -import React, { FC } from "react"; +import React, { FC, useState } from "react"; import Board, { BoardActions, BoardProps } from "../Board"; +import styled from "styled-components"; + +const StyledDiv = styled.div` + display: flex; + gap: 10px; +`; + +const StyledP = styled.p` + display: flex; + border: 1px solid black; + padding: 3px; +`; const Example: FC = ({ primary = true, items, imageSrc }) => { const ref = React.createRef(); + const [toggleStatus, setToggleStatus] = useState(false); + const [currentZoom, setCurrentZoom] = useState(); + return ( <> + + + + + + Current zoom: {currentZoom} +
= ({ primary = true, items, imageSrc }) => { alignItems: "center", width: "800px", height: "500px", + border: "1px solid black", }} > - + setToggleStatus(s)} + onZoomChange={(v) => setCurrentZoom(v)} + />
- ); }; From a22f826f3093f28ac267822637f0c7155fa2295e Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Wed, 3 Apr 2024 11:21:48 +0100 Subject: [PATCH 09/10] Add polygons correctly --- src/components/Board/Board.tsx | 50 +-- .../Board/__docs__/Board.stories.tsx | 284 +++++++++++++++++- src/components/Board/types.tsx | 2 +- 3 files changed, 308 insertions(+), 28 deletions(-) diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 875a626..afd4973 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -49,6 +49,7 @@ const Board = React.forwardRef( primary = true, imageSrc, initialStatus, + items, onToggleDragging, onResetZoom, onZoomChange, @@ -65,6 +66,7 @@ const Board = React.forwardRef( }, resetZoom() { editor?.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); + addItems(); onResetZoom?.(); }, })); @@ -74,7 +76,6 @@ const Board = React.forwardRef( initialStatus?.currentZoom || 100, ); - // eslint-disable-next-line @typescript-eslint/no-unused-vars const [scaleRatio, setScaleRation] = useState( initialStatus?.scaleRatio || 100, ); @@ -197,7 +198,7 @@ const Board = React.forwardRef( editor.canvas.on( "selection:created", function (this: CanvasAnnotationState, opt) { - console.log("SELECTED! ", opt.selected?.[0]); + // console.log("SELECTED! ", opt.selected?.[0]); opt.e.preventDefault(); opt.e.stopPropagation(); @@ -235,27 +236,30 @@ const Board = React.forwardRef( onZoomChange?.(Math.round(currentZoom)); }, [currentZoom, onZoomChange]); - // const getActiveObjects = () => { - // console.log(editor?.canvas.getActiveObjects()); - // }; - - // const addRandom = () => { - // // Calculate the coordinates relative to the image - // // (x1, y1) = (133, 460) - // const rect = new fabric.Rect({ - // scaleX: scaleRatio, - // scaleY: scaleRatio, - // left: WIDTH / 2 - (imageSize.width * scaleRatio) / 2 + 133 * scaleRatio, // Specify the left coordinate relative to image - // top: HEIGHT / 2 - (imageSize.height * scaleRatio) / 2 + 460 * scaleRatio, - // width: 73, - // height: 33, - // fill: undefined, - // stroke: "red", - // strokeWidth: 1, - // selectable: true, - // }); - // editor?.canvas.add(rect); - // }; + // Load Initial items + useEffect(() => { + const toScaledCoord = (coord: { x: number; y: number }) => { + const height = editor?.canvas.getHeight() ?? 1; + const width = editor?.canvas.getWidth() ?? 1; + const x = + width / 2 - (imageSize.width * scaleRatio) / 2 + coord.x * scaleRatio; + const y = + height / 2 - + (imageSize.height * scaleRatio) / 2 + + coord.y * scaleRatio; + + return { x, y }; + }; + + for (const item of items) { + const polygon = new fabric.Polygon(item.coords.map(toScaledCoord), { + fill: undefined, + stroke: "red", + strokeWidth: 0.3, + }); + editor?.canvas.add(polygon); + } + }, [editor?.canvas, imageSize.height, imageSize.width, items, scaleRatio]); // const onAddRectangle = () => { // // editor?.addRectangle(); diff --git a/src/components/Board/__docs__/Board.stories.tsx b/src/components/Board/__docs__/Board.stories.tsx index 73af7a2..ae99c73 100644 --- a/src/components/Board/__docs__/Board.stories.tsx +++ b/src/components/Board/__docs__/Board.stories.tsx @@ -14,10 +14,286 @@ const ITEMS: CanvasObject[] = [ color: "green", value: "⌀42", coords: [ - [55, 382], - [128, 382], - [128, 415], - [55, 415], + { + x: 133, + y: 460, + }, + { + x: 206, + y: 460, + }, + { + x: 206, + y: 493, + }, + { + x: 133, + y: 493, + }, + ], + }, + { + id: "2", + category: "category2", + color: "green", + value: "38", + coords: [ + { + x: 150, + y: 1064, + }, + { + x: 182, + y: 1064, + }, + { + x: 182, + y: 1111, + }, + { + x: 150, + y: 1111, + }, + ], + }, + { + id: "3", + category: "category3", + color: "green", + value: "9", + coords: [ + { + x: 235, + y: 1207, + }, + { + x: 266, + y: 1207, + }, + { + x: 266, + y: 1226, + }, + { + x: 235, + y: 1226, + }, + ], + }, + { + id: "4", + category: "category4", + color: "green", + value: "⌀38", + coords: [ + { + x: 481, + y: 1375, + }, + { + x: 556, + y: 1375, + }, + { + x: 556, + y: 1407, + }, + { + x: 481, + y: 1407, + }, + ], + }, + { + id: "5", + category: "category5", + color: "green", + value: "(25.5)", + coords: [ + { + x: 1370, + y: 1405, + }, + { + x: 1473, + y: 1406, + }, + { + x: 1472, + y: 1444, + }, + { + x: 1369, + y: 1442, + }, + ], + }, + { + id: "6", + category: "category6", + color: "green", + value: "1x45°", + coords: [ + { + x: 732, + y: 1277, + }, + { + x: 735, + y: 1381, + }, + { + x: 702, + y: 1382, + }, + { + x: 699, + y: 1278, + }, + ], + }, + { + id: "7", + category: "category7", + color: "green", + value: "1x45°", + coords: [ + { + x: 740, + y: 795, + }, + { + x: 738, + y: 895, + }, + { + x: 705, + y: 894, + }, + { + x: 707, + y: 794, + }, + ], + }, + { + id: "8", + category: "category8", + color: "green", + value: "16", + coords: [ + { + x: 943, + y: 1024, + }, + { + x: 945, + y: 976, + }, + { + x: 979, + y: 978, + }, + { + x: 977, + y: 1025, + }, + ], + }, + { + id: "9", + category: "category9", + color: "green", + value: "45.0°", + coords: [ + { + x: 821, + y: 1093, + }, + { + x: 855, + y: 1182, + }, + { + x: 817, + y: 1197, + }, + { + x: 783, + y: 1108, + }, + ], + }, + { + id: "10", + category: "category10", + color: "green", + value: "⌀21.5±0.1", + coords: [ + { + x: 943, + y: 1341, + }, + { + x: 1122, + y: 1343, + }, + { + x: 1121, + y: 1380, + }, + { + x: 942, + y: 1378, + }, + ], + }, + { + id: "11", + category: "category11", + color: "green", + value: "60.0°", + coords: [ + { + x: 925, + y: 1556, + }, + { + x: 963, + y: 1538, + }, + { + x: 1004, + y: 1627, + }, + { + x: 966, + y: 1644, + }, + ], + }, + { + id: "12", + category: "category12", + color: "green", + value: "⌀38H12", + coords: [ + { + x: 1317, + y: 749, + }, + { + x: 1317, + y: 782, + }, + { + x: 1163, + y: 782, + }, + { + x: 1163, + y: 749, + }, ], }, ]; diff --git a/src/components/Board/types.tsx b/src/components/Board/types.tsx index b76146b..a315322 100644 --- a/src/components/Board/types.tsx +++ b/src/components/Board/types.tsx @@ -3,5 +3,5 @@ export type CanvasObject = { category: string; color: string; value: string; - coords: number[][]; + coords: { x: number; y: number }[]; }; From 418047c777f50e5a54be9a343bdf030ad416ee15 Mon Sep 17 00:00:00 2001 From: Daniel Reis Date: Wed, 3 Apr 2024 11:49:37 +0100 Subject: [PATCH 10/10] Demo Board --- src/components/Annotator/Annotator.tsx | 323 +++++++++++++++++++++++-- src/components/Board/Board.tsx | 1 - src/components/Board/index.ts | 1 - 3 files changed, 305 insertions(+), 20 deletions(-) diff --git a/src/components/Annotator/Annotator.tsx b/src/components/Annotator/Annotator.tsx index b5567d3..33624f6 100644 --- a/src/components/Annotator/Annotator.tsx +++ b/src/components/Annotator/Annotator.tsx @@ -1,15 +1,308 @@ -import React, { useEffect, useRef } from "react"; +import React from "react"; import styled from "styled-components"; import { Header } from "../Header"; import { Toolbar } from "../Toolbar"; import { Menu } from "../Menu"; -import { FabricJSCanvas, useFabricJSEditor } from "fabricjs-react"; +import { Board } from "../Board"; +import { BoardActions } from "../Board/Board"; +import { CanvasObject } from "../Board/types"; export type AnnotatorProps = { id?: string; primary?: boolean; }; +const ITEMS: CanvasObject[] = [ + { + id: "1", + category: "category1", + color: "green", + value: "⌀42", + coords: [ + { + x: 133, + y: 460, + }, + { + x: 206, + y: 460, + }, + { + x: 206, + y: 493, + }, + { + x: 133, + y: 493, + }, + ], + }, + { + id: "2", + category: "category2", + color: "green", + value: "38", + coords: [ + { + x: 150, + y: 1064, + }, + { + x: 182, + y: 1064, + }, + { + x: 182, + y: 1111, + }, + { + x: 150, + y: 1111, + }, + ], + }, + { + id: "3", + category: "category3", + color: "green", + value: "9", + coords: [ + { + x: 235, + y: 1207, + }, + { + x: 266, + y: 1207, + }, + { + x: 266, + y: 1226, + }, + { + x: 235, + y: 1226, + }, + ], + }, + { + id: "4", + category: "category4", + color: "green", + value: "⌀38", + coords: [ + { + x: 481, + y: 1375, + }, + { + x: 556, + y: 1375, + }, + { + x: 556, + y: 1407, + }, + { + x: 481, + y: 1407, + }, + ], + }, + { + id: "5", + category: "category5", + color: "green", + value: "(25.5)", + coords: [ + { + x: 1370, + y: 1405, + }, + { + x: 1473, + y: 1406, + }, + { + x: 1472, + y: 1444, + }, + { + x: 1369, + y: 1442, + }, + ], + }, + { + id: "6", + category: "category6", + color: "green", + value: "1x45°", + coords: [ + { + x: 732, + y: 1277, + }, + { + x: 735, + y: 1381, + }, + { + x: 702, + y: 1382, + }, + { + x: 699, + y: 1278, + }, + ], + }, + { + id: "7", + category: "category7", + color: "green", + value: "1x45°", + coords: [ + { + x: 740, + y: 795, + }, + { + x: 738, + y: 895, + }, + { + x: 705, + y: 894, + }, + { + x: 707, + y: 794, + }, + ], + }, + { + id: "8", + category: "category8", + color: "green", + value: "16", + coords: [ + { + x: 943, + y: 1024, + }, + { + x: 945, + y: 976, + }, + { + x: 979, + y: 978, + }, + { + x: 977, + y: 1025, + }, + ], + }, + { + id: "9", + category: "category9", + color: "green", + value: "45.0°", + coords: [ + { + x: 821, + y: 1093, + }, + { + x: 855, + y: 1182, + }, + { + x: 817, + y: 1197, + }, + { + x: 783, + y: 1108, + }, + ], + }, + { + id: "10", + category: "category10", + color: "green", + value: "⌀21.5±0.1", + coords: [ + { + x: 943, + y: 1341, + }, + { + x: 1122, + y: 1343, + }, + { + x: 1121, + y: 1380, + }, + { + x: 942, + y: 1378, + }, + ], + }, + { + id: "11", + category: "category11", + color: "green", + value: "60.0°", + coords: [ + { + x: 925, + y: 1556, + }, + { + x: 963, + y: 1538, + }, + { + x: 1004, + y: 1627, + }, + { + x: 966, + y: 1644, + }, + ], + }, + { + id: "12", + category: "category12", + color: "green", + value: "⌀38H12", + coords: [ + { + x: 1317, + y: 749, + }, + { + x: 1317, + y: 782, + }, + { + x: 1163, + y: 782, + }, + { + x: 1163, + y: 749, + }, + ], + }, +]; + const Container = styled.div` display: flex; flex-direction: column; @@ -52,20 +345,7 @@ const InnerContent = styled.div` `; const Annotator: React.FC = ({ id, primary }) => { - const ref = useRef(null); - const { editor, onReady } = useFabricJSEditor(); - - useEffect(() => { - if (!editor || !ref.current) { - return; - } - console.log("width", ref.current ? ref.current.offsetWidth : 0); - console.log("height", ref.current ? ref.current.offsetHeight : 0); - - editor.canvas.setWidth(ref.current.offsetWidth); - editor.canvas.setHeight(ref.current.offsetHeight); - }, [ref, editor]); - + const ref = React.createRef(); return ( @@ -101,8 +381,15 @@ const Annotator: React.FC = ({ id, primary }) => { ]} /> - - + + setToggleStatus(s)} + // onZoomChange={(v) => setCurrentZoom(v)} + /> ( }, resetZoom() { editor?.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); - addItems(); onResetZoom?.(); }, })); diff --git a/src/components/Board/index.ts b/src/components/Board/index.ts index 1b7ab1d..ccc93b8 100644 --- a/src/components/Board/index.ts +++ b/src/components/Board/index.ts @@ -1,2 +1 @@ export { default as Board } from "./Board"; -export * from "./hooks";