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/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)} + /> { it("Annotator should render correctly", () => { diff --git a/src/components/Board/Board.tsx b/src/components/Board/Board.tsx index 8427b9a..a095d91 100644 --- a/src/components/Board/Board.tsx +++ b/src/components/Board/Board.tsx @@ -1,47 +1,354 @@ -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"; +import { CanvasObject } from "./types"; +import styled from "styled-components"; export type BoardProps = { - text?: string; primary?: boolean; - disabled?: boolean; - size?: "small" | "medium" | "large"; - onClick?: MouseEventHandler; + 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; }; -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"; - } - - return ( - - ); +export type BoardActions = { + toggleDragging: (value?: boolean) => void; + resetZoom: () => void; }; +type CanvasAnnotationState = { + selection?: boolean; + lastPosX: number; + lastPosY: number; + isDragging?: boolean; +}; + +const StyledCanvas = styled.div` + width: 100%; + height: 100%; +`; + +const Board = React.forwardRef( + ( + { + primary = true, + imageSrc, + initialStatus, + items, + onToggleDragging, + onResetZoom, + onZoomChange, + onLoadedImage, + }, + ref, + ) => { + // Set board actions + React.useImperativeHandle(ref, () => ({ + toggleDragging() { + 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, + ); + + 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( + initialStatus?.draggingEnabled || false, + ); + + 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(); + onLoadedImage?.({ width: img.width ?? 0, height: img.height ?? 0 }); + }, + { 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, + onLoadedImage, + onZoomChange, + ]); + + // Update zoom parent value + useEffect(() => { + onZoomChange?.(Math.round(currentZoom)); + }, [currentZoom, onZoomChange]); + + // 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(); + // 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 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 6f4410b..d0cf05c 100644 --- a/src/components/Board/__docs__/Board.mdx +++ b/src/components/Board/__docs__/Board.mdx @@ -10,20 +10,21 @@ Button component with different props. #### Example - + ## Usage ```ts -import {Board} from "react-image-annotator"; +import Board {BoardActions} from "react-image-annotator"; const Example = () => { + const ref = React.createRef(); return ( console.log("Clicked")} primary + ref={ref} + imageSrc={"holder-min.jpg"} + items={[]} /> ); }; diff --git a/src/components/Board/__docs__/Board.stories.tsx b/src/components/Board/__docs__/Board.stories.tsx index f860979..ae99c73 100644 --- a/src/components/Board/__docs__/Board.stories.tsx +++ b/src/components/Board/__docs__/Board.stories.tsx @@ -1,29 +1,310 @@ import type { Meta, StoryObj } from "@storybook/react"; -import ExampleMain from "./ExampleMain"; +import Example from "./Example"; +import { CanvasObject } from "../types"; -const meta: Meta = { +const meta: Meta = { title: "Board", - component: ExampleMain, + component: Example, }; +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, + }, + ], + }, +]; + export default meta; -type Story = StoryObj; +type Story = StoryObj; -export const Primary: Story = { +export const Main: 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"), + imageSrc: "holder-min.jpg", + items: ITEMS, }, }; diff --git a/src/components/Board/__docs__/Example.tsx b/src/components/Board/__docs__/Example.tsx index 228ae31..7a94a6c 100644 --- a/src/components/Board/__docs__/Example.tsx +++ b/src/components/Board/__docs__/Example.tsx @@ -1,30 +1,55 @@ -import React, { FC } from "react"; -import Board, { BoardProps } from "../Board"; +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(); -const Example: FC = ({ - disabled = false, - onClick = () => {}, - primary = true, - size = "small", - text = "Button", -}) => { return ( -
- -
+ <> + + + + + + Current zoom: {currentZoom} + +
+ setToggleStatus(s)} + onZoomChange={(v) => setCurrentZoom(v)} + /> +
+ ); }; 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/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/Board/types.tsx b/src/components/Board/types.tsx new file mode 100644 index 0000000..a315322 --- /dev/null +++ b/src/components/Board/types.tsx @@ -0,0 +1,7 @@ +export type CanvasObject = { + id: string; + category: string; + color: string; + value: string; + coords: { x: number; y: number }[]; +}; 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", () => { 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";