@saran-ign/video-annotation-tool
Version:
[](https://www.npmjs.com/package/@saran-ign/video-annotation-tool) [](https://www.npmjs.co
446 lines (445 loc) • 25.3 kB
JavaScript
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;