UNPKG

@saran-ign/video-annotation-tool

Version:

[![npm version](https://img.shields.io/npm/v/@saran-ign/video-annotation-tool.svg)](https://www.npmjs.com/package/@saran-ign/video-annotation-tool) [![npm downloads](https://img.shields.io/npm/dm/@saran-ign/video-annotation-tool.svg)](https://www.npmjs.co

446 lines (445 loc) 25.3 kB
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; // @ts-nocheck import { useState, useEffect, useRef, useCallback, forwardRef, useImperativeHandle, } from "react"; import { Stage, Layer, Rect, Transformer, Circle, Line } from "react-konva"; import { throttle } from "lodash"; import generateId from "../utils/generateId"; import Player from "../VideoPlayer/Player"; import TransparentVideoController from "../VideoPlayerController/TransparentVideoplayerController"; import useVideoController from "../VideoPlayerController/UseVideoPlayerControllerHook"; import { useCanvas } from "../contexts/CanvasProvider"; import { LineShape } from "../shapes/Line"; import { CircleShape } from "../shapes/Circle"; import { Rectangle } from "../shapes/Rectangle"; const Canvas = forwardRef(({ children, url, videoId, selectedShapeTool, hideAnnotations, lockEdit, annotationColor = "#000000", opacity = 1, strokeWidth = 2, selectedAnnotationData, videoControls, videoTimeAnnotation, showVideoDuration, video_ref, }, ref) => { var _a, _b, _c, _d, _e; const { shapes, setShapes, isDrawing, setIsDrawing, newShape, setNewShape, selectedShapeId, setSelectedShapeId, rectPosititon, setRectPosition, videoRefVal, setVideoRefVal, dimensions, setDimensions, history, setHistory, redoStack, setRedoStack, undo, redo, deleteShape, } = useCanvas(); // REF STATES const shapeRef = useRef({}); const transformerRef = useRef(null); const stageRef = useRef(null); const canvasParentRef = useRef(null); const layerRef = useRef(null); const videoRef = video_ref !== null && video_ref !== void 0 ? video_ref : useRef(null); // HOOK VALUES const { currentTime, setCurrentTime, isFullScreen } = useVideoController(videoRefVal, canvasParentRef); const [canvasParentWidth, setcanvasParentWidth] = useState(((_a = canvasParentRef === null || canvasParentRef === void 0 ? void 0 : canvasParentRef.current) === null || _a === void 0 ? void 0 : _a.offsetWidth) || 0); const [canvasParentHeight, setcanvasParentHeight] = useState(((_b = canvasParentRef === null || canvasParentRef === void 0 ? void 0 : canvasParentRef.current) === null || _b === void 0 ? void 0 : _b.offsetHeight) || 0); useEffect(() => { if (videoRef) { setVideoRefVal(videoRef); } return () => { setVideoRefVal(null); }; }, [videoRef]); // =================== Exported Handlers =================================== useEffect(() => { if (typeof selectedAnnotationData === "function") { if (selectedShapeId) { const selectedData = shapes.find((shape) => shape.id === selectedShapeId); selectedAnnotationData(selectedData || null); } else { selectedAnnotationData(null); } } }, [selectedShapeId, shapes, selectedAnnotationData]); useImperativeHandle(ref, () => ({ undo, redo, deleteShape, })); // ============================================================================ const handleMouseDown = useCallback((e) => { if (isFullScreen) return; const cursor = window.getComputedStyle(document.body).cursor; if (cursor === "nwse-resize") return; if (selectedShapeTool !== "rectangle" && selectedShapeTool !== "circle" && selectedShapeTool !== "line") { console.warn("Kindly Select appropriate tool which can only include line rectangle and circle"); return; } const stage = e.target.getStage(); if (!stage) return; const { x, y } = stage.getPointerPosition(); const startTime = currentTime; let shapeProperties; switch (selectedShapeTool) { case "rectangle": shapeProperties = { type: "rectangle", x, y, width: 4, height: 4, startTime, endTime: startTime + 0.5, scaleX: 1, scaleY: 1, screenHeight: canvasParentHeight, screenWidth: canvasParentWidth, strokeWidth: strokeWidth || 2, opacity: opacity, }; break; case "circle": shapeProperties = { type: "circle", x, y, radius: 4, startTime, endTime: startTime + 0.5, scaleX: 1, scaleY: 1, screenHeight: canvasParentHeight, screenWidth: canvasParentWidth, strokeWidth: strokeWidth || 2, opacity: opacity, }; break; case "line": shapeProperties = { type: "line", x, y, points: [0, 0, 100, 0, 100, 100], startTime, endTime: startTime + 0.5, scaleX: 1, scaleY: 1, screenHeight: canvasParentHeight, screenWidth: canvasParentWidth, strokeWidth: strokeWidth || 2, opacity: opacity, }; break; default: return {}; } setNewShape({ id: generateId(), color: annotationColor, label: "", data: {}, properties: shapeProperties, }); setIsDrawing(true); }, [ currentTime, isFullScreen, annotationColor, selectedShapeTool, strokeWidth, opacity, canvasParentHeight, canvasParentWidth, ]); const handleMouseMove = throttle((e) => { if (isFullScreen) return; if (!isDrawing || !newShape) return; const stage = e.target.getStage(); if (!stage) return; const { x, y } = stage.getPointerPosition(); if (x === newShape.properties.x && y === newShape.properties.y) return; let updatedShape; switch (newShape.properties.type) { case "rectangle": const width = x - newShape.properties.x; const height = y - newShape.properties.y; updatedShape = Object.assign(Object.assign({}, newShape), { properties: Object.assign(Object.assign({}, newShape.properties), { width, height }) }); break; case "circle": const radius = Math.sqrt(Math.pow(x - newShape.properties.x, 2) + Math.pow(y - newShape.properties.y, 2)); updatedShape = Object.assign(Object.assign({}, newShape), { properties: Object.assign(Object.assign({}, newShape.properties), { radius }) }); break; case "line": const points = [ 0, 0, x - newShape.properties.x, y - newShape.properties.y, ]; updatedShape = Object.assign(Object.assign({}, newShape), { properties: Object.assign(Object.assign({}, newShape.properties), { points }) }); break; default: return; } setNewShape(updatedShape); }, 100); const handleMouseUp = useCallback(() => { if (newShape) { setHistory((prevHistory) => [...prevHistory, shapes]); setRedoStack([]); setShapes((prevShapes) => [...prevShapes, newShape]); setIsDrawing(false); setSelectedShapeId(newShape.id); setNewShape(null); } }, [newShape, shapes]); const handleSelectShape = useCallback((shapeId, e) => { e.cancelBubble = true; setSelectedShapeId(shapeId); }, []); const handleStageClick = (e) => { if (isFullScreen) return; if (e.target === e.target.getStage() || e.target.className === "Image") { setSelectedShapeId(null); } }; const handleDragStart = (e) => { setHistory((prevHistory) => [...prevHistory, shapes]); setRedoStack([]); e.target.getStage().container().style.cursor = "move"; }; const handleDragEnd = useCallback((e, shapeId) => { const { x, y } = rectPosititon; if (x !== null && y !== null) { setShapes((prevShapes) => prevShapes.map((shape) => shape.id === shapeId ? Object.assign(Object.assign({}, shape), { properties: Object.assign(Object.assign({}, shape.properties), { x, y }) }) : shape)); } e.target.getStage().container().style.cursor = "default"; }, [rectPosititon]); const handleTransformStart = useCallback(() => { document.body.style.cursor = "nwse-resize"; setHistory((prevHistory) => [...prevHistory, shapes]); setRedoStack([]); }, [shapes]); const handleTransformEnd = useCallback((e, shapeId) => { document.body.style.cursor = "auto"; const node = e.target; const scaleX = node.scaleX(); const scaleY = node.scaleY(); node.scaleX(1); node.scaleY(1); if (!isFullScreen) { setShapes((prevShapes) => prevShapes.map((shape) => { if (shape.id !== shapeId) return shape; const { type } = shape.properties; let updatedProperties; switch (type) { case "rectangle": updatedProperties = Object.assign(Object.assign({}, shape.properties), { x: node.x(), y: node.y(), width: node.width() * scaleX, height: node.height() * scaleY }); break; case "circle": updatedProperties = Object.assign(Object.assign({}, shape.properties), { x: node.x(), y: node.y(), radius: node.radius() * scaleX }); break; case "line": updatedProperties = Object.assign(Object.assign({}, shape.properties), { x: node.x(), y: node.y(), points: node .points() .map((point, index) => index % 2 === 0 ? point * scaleX : point * scaleY) }); break; default: return shape; } return Object.assign(Object.assign({}, shape), { properties: updatedProperties }); })); } }, [isFullScreen]); useEffect(() => { var _a, _b; setcanvasParentHeight(((_a = canvasParentRef === null || canvasParentRef === void 0 ? void 0 : canvasParentRef.current) === null || _a === void 0 ? void 0 : _a.offsetHeight) || 0); setcanvasParentWidth(((_b = canvasParentRef === null || canvasParentRef === void 0 ? void 0 : canvasParentRef.current) === null || _b === void 0 ? void 0 : _b.offsetWidth) || 0); }, [ (_c = canvasParentRef === null || canvasParentRef === void 0 ? void 0 : canvasParentRef.current) === null || _c === void 0 ? void 0 : _c.offsetHeight, (_d = canvasParentRef === null || canvasParentRef === void 0 ? void 0 : canvasParentRef.current) === null || _d === void 0 ? void 0 : _d.offsetWidth, ]); useEffect(() => { const handleKeyDown = (e) => { if ((e.ctrlKey || e.metaKey) && e.key === "z") { undo(); } if ((e.ctrlKey || e.metaKey) && e.key === "y") { redo(); } if (e.key === "Delete") { deleteShape(); } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [undo, redo, deleteShape]); useEffect(() => { var _a, _b, _c, _d; if (selectedShapeId !== null && shapeRef.current[selectedShapeId]) { (_a = transformerRef.current) === null || _a === void 0 ? void 0 : _a.nodes([shapeRef.current[selectedShapeId]]); (_c = (_b = transformerRef.current) === null || _b === void 0 ? void 0 : _b.getLayer()) === null || _c === void 0 ? void 0 : _c.batchDraw(); } else { (_d = transformerRef.current) === null || _d === void 0 ? void 0 : _d.nodes([]); } }, [selectedShapeId]); useEffect(() => { const handleTimeUpdate = () => { var _a; return setCurrentTime(((_a = videoRef === null || videoRef === void 0 ? void 0 : videoRef.current) === null || _a === void 0 ? void 0 : _a.currentTime) || 0); }; const video = videoRef.current; video === null || video === void 0 ? void 0 : video.addEventListener("timeupdate", handleTimeUpdate); return () => video === null || video === void 0 ? void 0 : video.removeEventListener("timeupdate", handleTimeUpdate); }, [videoRef]); const isVisible = useCallback((shapeId) => { var _a, _b; const shape = shapes.find((shape) => shape.id === shapeId); if (videoTimeAnnotation) { return (currentTime >= ((_a = shape === null || shape === void 0 ? void 0 : shape.properties) === null || _a === void 0 ? void 0 : _a.startTime) && currentTime <= ((_b = shape === null || shape === void 0 ? void 0 : shape.properties) === null || _b === void 0 ? void 0 : _b.endTime)); } else { return true; } }, [currentTime, shapes]); useEffect(() => { var _a; if (!((_a = videoRef.current) === null || _a === void 0 ? void 0 : _a.paused) || lockEdit || !isVisible(selectedShapeId || "")) { setSelectedShapeId(null); } }, [ (_e = videoRef.current) === null || _e === void 0 ? void 0 : _e.paused, lockEdit, currentTime, isVisible, selectedShapeId, ]); const dragBoundFunc = (pos) => { if (!selectedShapeId || !shapeRef.current[selectedShapeId]) { return pos; } const newX = Math.max(0, Math.min(pos.x, dimensions.width - shapeRef.current[selectedShapeId].width())); const newY = Math.max(0, Math.min(pos.y, dimensions.height - shapeRef.current[selectedShapeId].height())); return { x: newX, y: newY }; }; const handleDragMove = (e) => { if (!selectedShapeId || !shapeRef.current[selectedShapeId]) return; const newX = Math.max(0, Math.min(e.target.x(), dimensions.width - shapeRef.current[selectedShapeId].width())); const newY = Math.max(0, Math.min(e.target.y(), dimensions.height - shapeRef.current[selectedShapeId].height())); setRectPosition({ x: newX, y: newY }); }; const [imagedim, setImgDim] = useState(dimensions); useEffect(() => { const updateSize = () => { if (canvasParentRef.current) { const { offsetWidth, offsetHeight } = canvasParentRef.current; setDimensions({ width: offsetWidth, height: offsetHeight, }); setImgDim({ width: offsetWidth, height: offsetHeight, }); } }; updateSize(); window.addEventListener("resize", updateSize); return () => window.removeEventListener("resize", updateSize); }, []); const handleMouseEnterInStage = (e) => { if (!selectedShapeTool) { e.target.getStage().container().style.cursor = "pointer"; } else { e.target.getStage().container().style.cursor = "crosshair"; } }; return (_jsxs(_Fragment, { children: [_jsxs("div", { ref: canvasParentRef, style: { maxWidth: window.innerWidth, aspectRatio: "16/9", minHeight: 300, minWidth: 500, position: "relative", }, children: [_jsx(Player, { url: url, videoId: videoId, videoControls: videoControls, ref: videoRef, dimensions: imagedim, parentref: canvasParentRef }), _jsx(Stage, { ref: stageRef, width: dimensions.width, height: dimensions.height, style: { backgroundColor: "black", display: hideAnnotations ? "none" : "block", }, onMouseEnter: handleMouseEnterInStage, onMouseLeave: (e) => (e.target.getStage().container().style.cursor = "default"), onMouseDown: !lockEdit && !isFullScreen && selectedShapeTool ? handleMouseDown : undefined, onMouseMove: !lockEdit && !isFullScreen && selectedShapeTool ? handleMouseMove : undefined, onMouseUp: !lockEdit && !isFullScreen && selectedShapeTool ? handleMouseUp : undefined, onClick: !isFullScreen ? (e) => handleStageClick(e) : undefined, children: _jsxs(Layer, { ref: layerRef, children: [shapes .filter((shape) => !videoTimeAnnotation || // If false, show all shapes (videoTimeAnnotation && // If true, filter by time currentTime >= shape.properties.startTime && currentTime <= shape.properties.endTime)) .map((shape) => { var _a, _b, _c, _d, _e, _f; switch (shape.properties.type) { case "rectangle": return (_jsx(Rectangle, Object.assign({ ref: (ref) => { if (ref) { shapeRef.current[shape.id] = ref; } } }, shape, { scaleX: (_a = stageRef.current) === null || _a === void 0 ? void 0 : _a.scaleX(), scaleY: (_b = stageRef.current) === null || _b === void 0 ? void 0 : _b.scaleY(), onMouseEnter: handleMouseEnterInStage, draggable: !selectedShapeTool && !isFullScreen && !lockEdit && shape.id === selectedShapeId, onClick: !lockEdit && !isFullScreen && !selectedShapeTool ? (e) => handleSelectShape(shape.id, e) : undefined, onDragEnd: selectedShapeId ? (e) => handleDragEnd(e, shape.id) : undefined, onDragStart: selectedShapeId ? handleDragStart : undefined, onDragMove: selectedShapeId ? handleDragMove : undefined, dragBoundFunc: dragBoundFunc, onTransformEnd: selectedShapeId ? (e) => handleTransformEnd(e, shape.id) : undefined, onTransformStart: selectedShapeId ? handleTransformStart : undefined, currentHeight: canvasParentHeight, currentWidth: canvasParentWidth }), shape.id)); case "circle": return (_jsx(CircleShape, Object.assign({ ref: (ref) => { if (ref) { shapeRef.current[shape.id] = ref; } } }, shape, { scaleX: (_c = stageRef.current) === null || _c === void 0 ? void 0 : _c.scaleX(), scaleY: (_d = stageRef.current) === null || _d === void 0 ? void 0 : _d.scaleY(), onMouseEnter: handleMouseEnterInStage, draggable: !selectedShapeTool && !isFullScreen && !lockEdit && shape.id === selectedShapeId, onClick: !lockEdit && !isFullScreen && !selectedShapeTool ? (e) => handleSelectShape(shape.id, e) : undefined, onDragEnd: selectedShapeId ? (e) => handleDragEnd(e, shape.id) : undefined, onDragStart: selectedShapeId ? handleDragStart : undefined, onDragMove: selectedShapeId ? handleDragMove : undefined, dragBoundFunc: dragBoundFunc, onTransformEnd: selectedShapeId ? (e) => handleTransformEnd(e, shape.id) : undefined, onTransformStart: selectedShapeId ? handleTransformStart : undefined, currentHeight: canvasParentHeight, currentWidth: canvasParentWidth }), shape.id)); case "line": return (_jsx(LineShape, Object.assign({ ref: (ref) => { if (ref) { shapeRef.current[shape.id] = ref; } } }, shape, { scaleX: (_e = stageRef.current) === null || _e === void 0 ? void 0 : _e.scaleX(), scaleY: (_f = stageRef.current) === null || _f === void 0 ? void 0 : _f.scaleY(), onMouseEnter: handleMouseEnterInStage, draggable: !selectedShapeTool && !isFullScreen && !lockEdit && shape.id === selectedShapeId, onClick: !lockEdit && !isFullScreen && !selectedShapeTool ? (e) => handleSelectShape(shape.id, e) : undefined, onDragEnd: selectedShapeId ? (e) => handleDragEnd(e, shape.id) : undefined, onDragStart: selectedShapeId ? handleDragStart : undefined, onDragMove: selectedShapeId ? handleDragMove : undefined, dragBoundFunc: dragBoundFunc, onTransformEnd: selectedShapeId ? (e) => handleTransformEnd(e, shape.id) : undefined, onTransformStart: selectedShapeId ? handleTransformStart : undefined, currentHeight: canvasParentHeight, currentWidth: canvasParentWidth }), shape.id)); default: return null; } }), newShape && (_jsx(_Fragment, { children: (() => { var _a, _b, _c, _d, _e, _f; switch (newShape.properties.type) { case "rectangle": return (_jsx(Rect, { x: (_a = newShape.properties) === null || _a === void 0 ? void 0 : _a.x, y: (_b = newShape.properties) === null || _b === void 0 ? void 0 : _b.y, width: newShape.properties.width, height: newShape.properties.height, stroke: "violet", opacity: 0.8 })); case "circle": return (_jsx(Circle, { x: (_c = newShape.properties) === null || _c === void 0 ? void 0 : _c.x, y: (_d = newShape.properties) === null || _d === void 0 ? void 0 : _d.y, radius: newShape.properties.radius, stroke: "violet", opacity: 0.8 })); case "line": return (_jsx(Line, { x: (_e = newShape.properties) === null || _e === void 0 ? void 0 : _e.x, y: (_f = newShape.properties) === null || _f === void 0 ? void 0 : _f.y, points: newShape.properties.points, stroke: "violet", opacity: 0.8 })); default: return null; } })() })), _jsx(Transformer, { ref: transformerRef, keepRatio: false, rotateEnabled: false, anchorSize: 7, anchorCornerRadius: 10 })] }) })] }), _jsx(TransparentVideoController, { playerRef: videoRef, dimensions: dimensions, canvasParentRef: canvasParentRef, showVideoDuration: showVideoDuration })] })); }); export default Canvas;