mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-10 21:25:24 +03:00
working multi polygons
This commit is contained in:
parent
7556d08e1c
commit
8610b02ddb
@ -1,197 +0,0 @@
|
|||||||
import React, { useMemo, useRef, useState, useEffect } from "react";
|
|
||||||
import PolygonDrawer from "./PolygonDrawer";
|
|
||||||
import { Stage, Layer, Image } from "react-konva";
|
|
||||||
import Konva from "konva";
|
|
||||||
import type { KonvaEventObject } from "konva/lib/Node";
|
|
||||||
import { Button } from "../ui/button";
|
|
||||||
|
|
||||||
const videoSource = "./space_landscape.jpg";
|
|
||||||
|
|
||||||
const wrapperStyle: React.CSSProperties = {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
marginTop: 20,
|
|
||||||
backgroundColor: "aliceblue",
|
|
||||||
};
|
|
||||||
|
|
||||||
const columnStyle: React.CSSProperties = {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
marginTop: 20,
|
|
||||||
backgroundColor: "aliceblue",
|
|
||||||
};
|
|
||||||
|
|
||||||
export function DebugCanvas() {
|
|
||||||
const [image, setImage] = useState<HTMLImageElement | undefined>();
|
|
||||||
const imageRef = useRef<Konva.Image | null>(null);
|
|
||||||
const dataRef = useRef<HTMLDivElement | null>(null);
|
|
||||||
const stageRef = useRef<Konva.Stage>(null);
|
|
||||||
const [points, setPoints] = useState<number[][]>([]);
|
|
||||||
const [size, setSize] = useState<{ width: number; height: number }>({
|
|
||||||
width: 0,
|
|
||||||
height: 0,
|
|
||||||
});
|
|
||||||
const [flattenedPoints, setFlattenedPoints] = useState<number[]>([]);
|
|
||||||
const [position, setPosition] = useState([0, 0]);
|
|
||||||
const [isMouseOverPoint, setMouseOverPoint] = useState(false);
|
|
||||||
const [isPolyComplete, setPolyComplete] = useState(false);
|
|
||||||
|
|
||||||
const videoElement = useMemo(() => {
|
|
||||||
const element = new window.Image();
|
|
||||||
element.width = 650;
|
|
||||||
element.height = 302;
|
|
||||||
element.src = videoSource;
|
|
||||||
return element;
|
|
||||||
}, []); // No dependency needed here since videoSource is a constant
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const onload = function () {
|
|
||||||
setSize({
|
|
||||||
width: videoElement.width,
|
|
||||||
height: videoElement.height,
|
|
||||||
});
|
|
||||||
setImage(videoElement);
|
|
||||||
if (imageRef.current) imageRef.current = videoElement;
|
|
||||||
};
|
|
||||||
videoElement.addEventListener("load", onload);
|
|
||||||
return () => {
|
|
||||||
videoElement.removeEventListener("load", onload);
|
|
||||||
};
|
|
||||||
}, [videoElement]);
|
|
||||||
|
|
||||||
const getMousePos = (stage: Konva.Stage) => {
|
|
||||||
return [stage.getPointerPosition()!.x, stage.getPointerPosition()!.y];
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseDown = (e: KonvaEventObject<MouseEvent>) => {
|
|
||||||
if (isPolyComplete) return;
|
|
||||||
const stage = e.target.getStage()!;
|
|
||||||
const mousePos = getMousePos(stage);
|
|
||||||
if (isMouseOverPoint && points.length >= 3) {
|
|
||||||
setPolyComplete(true);
|
|
||||||
} else {
|
|
||||||
setPoints([...points, mousePos]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseMove = (e: KonvaEventObject<MouseEvent>) => {
|
|
||||||
const stage = e.target.getStage()!;
|
|
||||||
const mousePos = getMousePos(stage);
|
|
||||||
setPosition(mousePos);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseOverStartPoint = (e: KonvaEventObject<MouseEvent>) => {
|
|
||||||
if (isPolyComplete || points.length < 3) return;
|
|
||||||
e.currentTarget.scale({ x: 3, y: 3 });
|
|
||||||
setMouseOverPoint(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseOutStartPoint = (e: KonvaEventObject<MouseEvent>) => {
|
|
||||||
e.currentTarget.scale({ x: 1, y: 1 });
|
|
||||||
setMouseOverPoint(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePointDragMove = (e: KonvaEventObject<MouseEvent>) => {
|
|
||||||
const stage = e.target.getStage();
|
|
||||||
if (stage) {
|
|
||||||
const index = e.target.index - 1;
|
|
||||||
const pos = [e.target._lastPos!.x, e.target._lastPos!.y];
|
|
||||||
if (pos[0] < 0) pos[0] = 0;
|
|
||||||
if (pos[1] < 0) pos[1] = 0;
|
|
||||||
if (pos[0] > stage.width()) pos[0] = stage.width();
|
|
||||||
if (pos[1] > stage.height()) pos[1] = stage.height();
|
|
||||||
setPoints([...points.slice(0, index), pos, ...points.slice(index + 1)]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFlattenedPoints(
|
|
||||||
points
|
|
||||||
.concat(isPolyComplete ? [] : position)
|
|
||||||
.reduce((a, b) => a.concat(b), []),
|
|
||||||
);
|
|
||||||
}, [points, isPolyComplete, position]);
|
|
||||||
|
|
||||||
const undo = () => {
|
|
||||||
setPoints(points.slice(0, -1));
|
|
||||||
setPolyComplete(false);
|
|
||||||
setPosition(points[points.length - 1]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const reset = () => {
|
|
||||||
setPoints([]);
|
|
||||||
setPolyComplete(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleGroupDragEnd = (e: KonvaEventObject<MouseEvent>) => {
|
|
||||||
if (e.target.name() === "polygon") {
|
|
||||||
const result: number[][] = [];
|
|
||||||
const copyPoints = [...points];
|
|
||||||
copyPoints.map((point) =>
|
|
||||||
result.push([point[0] + e.target.x(), point[1] + e.target.y()]),
|
|
||||||
);
|
|
||||||
e.target.position({ x: 0, y: 0 });
|
|
||||||
setPoints(result);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div style={wrapperStyle}>
|
|
||||||
<div style={columnStyle}>
|
|
||||||
<Stage
|
|
||||||
ref={stageRef}
|
|
||||||
width={size.width || 650}
|
|
||||||
height={size.height || 302}
|
|
||||||
onMouseMove={handleMouseMove}
|
|
||||||
onMouseDown={handleMouseDown}
|
|
||||||
>
|
|
||||||
<Layer>
|
|
||||||
<Image
|
|
||||||
ref={imageRef}
|
|
||||||
image={image}
|
|
||||||
x={0}
|
|
||||||
y={0}
|
|
||||||
width={size.width}
|
|
||||||
height={size.height}
|
|
||||||
/>
|
|
||||||
<PolygonDrawer
|
|
||||||
points={points}
|
|
||||||
flattenedPoints={flattenedPoints}
|
|
||||||
handlePointDragMove={handlePointDragMove}
|
|
||||||
handleGroupDragEnd={handleGroupDragEnd}
|
|
||||||
handleMouseOverStartPoint={handleMouseOverStartPoint}
|
|
||||||
handleMouseOutStartPoint={handleMouseOutStartPoint}
|
|
||||||
isFinished={isPolyComplete}
|
|
||||||
/>
|
|
||||||
</Layer>
|
|
||||||
</Stage>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button name="Undo" onClick={undo} />
|
|
||||||
<Button name="Reset" onClick={reset} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
ref={dataRef}
|
|
||||||
style={{
|
|
||||||
width: 375,
|
|
||||||
height: 302,
|
|
||||||
boxShadow: ".5px .5px 5px .4em rgba(0,0,0,.1)",
|
|
||||||
marginTop: 20,
|
|
||||||
color: "black",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<pre style={{ whiteSpace: "pre-wrap" }}>{JSON.stringify(points)}</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DebugCanvas;
|
|
||||||
@ -8,10 +8,10 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import DebugCameraImage from "@/components/camera/DebugCameraImage";
|
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
|
import AutoUpdatingCameraImage from "@/components/camera/AutoUpdatingCameraImage";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useMemo, useState } from "react";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { Slider } from "@/components/ui/slider";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@ -105,7 +105,11 @@ export default function MotionTuner() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<DebugCameraImage cameraConfig={cameraConfig} className="w-[50%]" />
|
<AutoUpdatingCameraImage
|
||||||
|
camera={cameraConfig.name}
|
||||||
|
searchParams={new URLSearchParams([["motion", "1"]])}
|
||||||
|
className="w-[50%]"
|
||||||
|
/>
|
||||||
<div className="flex flex-col justify-evenly w-full">
|
<div className="flex flex-col justify-evenly w-full">
|
||||||
<div className="flex flex-row mb-5">
|
<div className="flex flex-row mb-5">
|
||||||
<Slider
|
<Slider
|
||||||
|
|||||||
277
web/src/components/settings/PolygonCanvas.tsx
Normal file
277
web/src/components/settings/PolygonCanvas.tsx
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
import React, { useMemo, useRef, useState, useEffect } from "react";
|
||||||
|
import PolygonDrawer from "./PolygonDrawer";
|
||||||
|
import { Stage, Layer, Image } from "react-konva";
|
||||||
|
import Konva from "konva";
|
||||||
|
import type { KonvaEventObject } from "konva/lib/Node";
|
||||||
|
import { Polygon } from "@/types/canvas";
|
||||||
|
import { useApiHost } from "@/api";
|
||||||
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
|
|
||||||
|
type PolygonCanvasProps = {
|
||||||
|
camera: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
polygons: Polygon[];
|
||||||
|
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
|
||||||
|
activePolygonIndex: number | null;
|
||||||
|
setActivePolygonIndex: React.Dispatch<React.SetStateAction<number | null>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PolygonCanvas({
|
||||||
|
camera,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
polygons,
|
||||||
|
setPolygons,
|
||||||
|
activePolygonIndex,
|
||||||
|
setActivePolygonIndex,
|
||||||
|
}: PolygonCanvasProps) {
|
||||||
|
const [image, setImage] = useState<HTMLImageElement | undefined>();
|
||||||
|
const imageRef = useRef<Konva.Image | null>(null);
|
||||||
|
// const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const stageRef = useRef<Konva.Stage>(null);
|
||||||
|
// const [points, setPoints] = useState<number[][]>([]);
|
||||||
|
// const [activePolygonIndex, setActivePolygonIndex] = useState<number | null>(
|
||||||
|
// null,
|
||||||
|
// );
|
||||||
|
// const [size, setSize] = useState<{ width: number; height: number }>({
|
||||||
|
// width: width,
|
||||||
|
// height: height,
|
||||||
|
// });
|
||||||
|
const apiHost = useApiHost();
|
||||||
|
// const [position, setPosition] = useState([0, 0]);
|
||||||
|
// const [{ width: windowWidth }] = useResizeObserver(window);
|
||||||
|
|
||||||
|
const videoElement = useMemo(() => {
|
||||||
|
if (camera && width && height) {
|
||||||
|
// console.log("width:", containerRef.current.clientWidth);
|
||||||
|
// console.log("width:", containerRef.current.clientHeight);
|
||||||
|
const element = new window.Image();
|
||||||
|
element.width = width; //containerRef.current.clientWidth;
|
||||||
|
element.height = height; //containerRef.current.clientHeight;
|
||||||
|
element.src = `${apiHost}api/${camera}/latest.jpg`;
|
||||||
|
// setSize({
|
||||||
|
// width: width,
|
||||||
|
// height: height,
|
||||||
|
// });
|
||||||
|
return element;
|
||||||
|
}
|
||||||
|
}, [camera, width, height, apiHost]);
|
||||||
|
|
||||||
|
// const imageScale = scaledWidth / 720;
|
||||||
|
// console.log("window width", windowWidth);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!videoElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const onload = function () {
|
||||||
|
setImage(videoElement);
|
||||||
|
// if (!imageRef.current) imageRef.current = videoElement;
|
||||||
|
console.log(videoElement, Date.now());
|
||||||
|
};
|
||||||
|
videoElement.addEventListener("load", onload);
|
||||||
|
return () => {
|
||||||
|
videoElement.removeEventListener("load", onload);
|
||||||
|
};
|
||||||
|
}, [videoElement]);
|
||||||
|
|
||||||
|
// use Konva.Animation to redraw a layer
|
||||||
|
// useEffect(() => {
|
||||||
|
// //videoElement.play();
|
||||||
|
// if (!videoElement && !imageRef && !imageRef.current) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const layer = imageRef.current?.getLayer();
|
||||||
|
// console.log("layer", layer);
|
||||||
|
|
||||||
|
// const anim = new Konva.Animation(() => {}, layer);
|
||||||
|
// anim.start();
|
||||||
|
|
||||||
|
// return () => {
|
||||||
|
// anim.stop();
|
||||||
|
// };
|
||||||
|
// }, [videoElement]);
|
||||||
|
|
||||||
|
const getMousePos = (stage: Konva.Stage) => {
|
||||||
|
return [stage.getPointerPosition()!.x, stage.getPointerPosition()!.y];
|
||||||
|
};
|
||||||
|
|
||||||
|
const isMouseOverPoint = (polygon: Polygon, mousePos: number[]) => {
|
||||||
|
if (!polygon || !polygon.points) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const [firstPoint] = polygon.points;
|
||||||
|
console.log("first", firstPoint);
|
||||||
|
const distance = Math.hypot(
|
||||||
|
mousePos[0] - firstPoint[0],
|
||||||
|
mousePos[1] - firstPoint[1],
|
||||||
|
);
|
||||||
|
return distance < 15;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDown = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (activePolygonIndex === null || !polygons) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.log("mouse down polygons", polygons);
|
||||||
|
console.log(activePolygonIndex);
|
||||||
|
|
||||||
|
if (!polygons[activePolygonIndex].points.length) {
|
||||||
|
// Start a new polygon
|
||||||
|
const stage = e.target.getStage()!;
|
||||||
|
const mousePos = getMousePos(stage);
|
||||||
|
setPolygons([
|
||||||
|
...polygons,
|
||||||
|
{
|
||||||
|
name: "foo",
|
||||||
|
points: [mousePos],
|
||||||
|
isFinished: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
setActivePolygonIndex(polygons.length);
|
||||||
|
} else {
|
||||||
|
const updatedPolygons = [...polygons];
|
||||||
|
const activePolygon = updatedPolygons[activePolygonIndex];
|
||||||
|
const stage = e.target.getStage()!;
|
||||||
|
const mousePos = getMousePos(stage);
|
||||||
|
|
||||||
|
if (
|
||||||
|
isMouseOverPoint(activePolygon, mousePos) &&
|
||||||
|
activePolygon.points.length >= 3
|
||||||
|
) {
|
||||||
|
// Close the polygon
|
||||||
|
updatedPolygons[activePolygonIndex] = {
|
||||||
|
...activePolygon,
|
||||||
|
isFinished: true,
|
||||||
|
};
|
||||||
|
setPolygons(updatedPolygons);
|
||||||
|
// setActivePolygonIndex(null);
|
||||||
|
} else {
|
||||||
|
if (!activePolygon.isFinished) {
|
||||||
|
// Add a new point to the active polygon
|
||||||
|
updatedPolygons[activePolygonIndex] = {
|
||||||
|
...activePolygon,
|
||||||
|
points: [...activePolygon.points, mousePos],
|
||||||
|
};
|
||||||
|
setPolygons(updatedPolygons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseMove = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
const stage = e.target.getStage()!;
|
||||||
|
const mousePos = getMousePos(stage);
|
||||||
|
// setPosition(mousePos);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseOverStartPoint = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (activePolygonIndex !== null && polygons) {
|
||||||
|
const activePolygon = polygons[activePolygonIndex];
|
||||||
|
if (!activePolygon.isFinished && activePolygon.points.length >= 3) {
|
||||||
|
e.currentTarget.scale({ x: 2, y: 2 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseOutStartPoint = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
console.log("active index:", activePolygonIndex);
|
||||||
|
e.currentTarget.scale({ x: 1, y: 1 });
|
||||||
|
if (activePolygonIndex !== null && polygons) {
|
||||||
|
const activePolygon = polygons[activePolygonIndex];
|
||||||
|
console.log(activePolygon);
|
||||||
|
if (
|
||||||
|
(!activePolygon.isFinished && activePolygon.points.length >= 3) ||
|
||||||
|
activePolygon.isFinished
|
||||||
|
) {
|
||||||
|
console.log(e.currentTarget);
|
||||||
|
e.currentTarget.scale({ x: 1, y: 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePointDragMove = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (activePolygonIndex !== null && polygons) {
|
||||||
|
const updatedPolygons = [...polygons];
|
||||||
|
const activePolygon = updatedPolygons[activePolygonIndex];
|
||||||
|
const stage = e.target.getStage();
|
||||||
|
if (stage) {
|
||||||
|
const index = e.target.index - 1;
|
||||||
|
const pos = [e.target._lastPos!.x, e.target._lastPos!.y];
|
||||||
|
if (pos[0] < 0) pos[0] = 0;
|
||||||
|
if (pos[1] < 0) pos[1] = 0;
|
||||||
|
if (pos[0] > stage.width()) pos[0] = stage.width();
|
||||||
|
if (pos[1] > stage.height()) pos[1] = stage.height();
|
||||||
|
updatedPolygons[activePolygonIndex] = {
|
||||||
|
...activePolygon,
|
||||||
|
points: [
|
||||||
|
...activePolygon.points.slice(0, index),
|
||||||
|
pos,
|
||||||
|
...activePolygon.points.slice(index + 1),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
setPolygons(updatedPolygons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenPoints = (points: number[][]): number[] => {
|
||||||
|
return points.reduce((acc, point) => [...acc, ...point], []);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGroupDragEnd = (e: KonvaEventObject<MouseEvent>) => {
|
||||||
|
if (activePolygonIndex !== null && e.target.name() === "polygon") {
|
||||||
|
const updatedPolygons = [...polygons];
|
||||||
|
const activePolygon = updatedPolygons[activePolygonIndex];
|
||||||
|
const result: number[][] = [];
|
||||||
|
activePolygon.points.map((point: number[]) =>
|
||||||
|
result.push([point[0] + e.target.x(), point[1] + e.target.y()]),
|
||||||
|
);
|
||||||
|
e.target.position({ x: 0, y: 0 });
|
||||||
|
updatedPolygons[activePolygonIndex] = {
|
||||||
|
...activePolygon,
|
||||||
|
points: result,
|
||||||
|
};
|
||||||
|
setPolygons(updatedPolygons);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stage
|
||||||
|
ref={stageRef}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
onMouseMove={handleMouseMove}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
>
|
||||||
|
<Layer>
|
||||||
|
<Image
|
||||||
|
ref={imageRef}
|
||||||
|
image={image}
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
/>
|
||||||
|
{polygons &&
|
||||||
|
polygons.map((polygon, index) => (
|
||||||
|
<PolygonDrawer
|
||||||
|
key={index}
|
||||||
|
points={polygon.points}
|
||||||
|
flattenedPoints={flattenPoints(polygon.points)}
|
||||||
|
isActive={index === activePolygonIndex}
|
||||||
|
isFinished={polygon.isFinished}
|
||||||
|
handlePointDragMove={handlePointDragMove}
|
||||||
|
handleGroupDragEnd={handleGroupDragEnd}
|
||||||
|
handleMouseOverStartPoint={handleMouseOverStartPoint}
|
||||||
|
handleMouseOutStartPoint={handleMouseOutStartPoint}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Layer>
|
||||||
|
</Stage>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PolygonCanvas;
|
||||||
79
web/src/components/settings/PolygonControls.tsx
Normal file
79
web/src/components/settings/PolygonControls.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
import { Button } from "../ui/button";
|
||||||
|
import { Polygon } from "@/types/canvas";
|
||||||
|
|
||||||
|
type PolygonCanvasProps = {
|
||||||
|
camera: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
polygons: Polygon[];
|
||||||
|
setPolygons: React.Dispatch<React.SetStateAction<Polygon[]>>;
|
||||||
|
activePolygonIndex: number | null;
|
||||||
|
setActivePolygonIndex: React.Dispatch<React.SetStateAction<number | null>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function PolygonControls({
|
||||||
|
polygons,
|
||||||
|
setPolygons,
|
||||||
|
activePolygonIndex,
|
||||||
|
setActivePolygonIndex,
|
||||||
|
}: PolygonCanvasProps) {
|
||||||
|
const undo = () => {
|
||||||
|
if (activePolygonIndex !== null && polygons) {
|
||||||
|
const updatedPolygons = [...polygons];
|
||||||
|
const activePolygon = updatedPolygons[activePolygonIndex];
|
||||||
|
if (activePolygon.points.length > 0) {
|
||||||
|
updatedPolygons[activePolygonIndex] = {
|
||||||
|
...activePolygon,
|
||||||
|
points: activePolygon.points.slice(0, -1),
|
||||||
|
isFinished: false,
|
||||||
|
};
|
||||||
|
setPolygons(updatedPolygons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
if (activePolygonIndex !== null) {
|
||||||
|
const updatedPolygons = [...polygons];
|
||||||
|
updatedPolygons[activePolygonIndex] = {
|
||||||
|
points: [],
|
||||||
|
isFinished: false,
|
||||||
|
name: "new",
|
||||||
|
};
|
||||||
|
setPolygons(updatedPolygons);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNewPolygon = () => {
|
||||||
|
setPolygons([
|
||||||
|
...(polygons || []),
|
||||||
|
{
|
||||||
|
points: [],
|
||||||
|
isFinished: false,
|
||||||
|
name: "new",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
console.log(polygons.length);
|
||||||
|
console.log(polygons);
|
||||||
|
console.log("active index", polygons.length);
|
||||||
|
setActivePolygonIndex(polygons.length);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="flex justify-between items-center my-5">
|
||||||
|
<Button className="mr-5" variant="secondary" onClick={undo}>
|
||||||
|
Undo
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" onClick={reset}>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" onClick={handleNewPolygon}>
|
||||||
|
New Polygon
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PolygonControls;
|
||||||
@ -8,6 +8,7 @@ import { Vector2d } from "konva/lib/types";
|
|||||||
type PolygonDrawerProps = {
|
type PolygonDrawerProps = {
|
||||||
points: number[][];
|
points: number[][];
|
||||||
flattenedPoints: number[];
|
flattenedPoints: number[];
|
||||||
|
isActive: boolean;
|
||||||
isFinished: boolean;
|
isFinished: boolean;
|
||||||
handlePointDragMove: (e: KonvaEventObject<MouseEvent>) => void;
|
handlePointDragMove: (e: KonvaEventObject<MouseEvent>) => void;
|
||||||
handleGroupDragEnd: (e: KonvaEventObject<MouseEvent>) => void;
|
handleGroupDragEnd: (e: KonvaEventObject<MouseEvent>) => void;
|
||||||
@ -18,6 +19,7 @@ type PolygonDrawerProps = {
|
|||||||
export default function PolygonDrawer({
|
export default function PolygonDrawer({
|
||||||
points,
|
points,
|
||||||
flattenedPoints,
|
flattenedPoints,
|
||||||
|
isActive,
|
||||||
isFinished,
|
isFinished,
|
||||||
handlePointDragMove,
|
handlePointDragMove,
|
||||||
handleGroupDragEnd,
|
handleGroupDragEnd,
|
||||||
@ -25,11 +27,9 @@ export default function PolygonDrawer({
|
|||||||
handleMouseOutStartPoint,
|
handleMouseOutStartPoint,
|
||||||
}: PolygonDrawerProps) {
|
}: PolygonDrawerProps) {
|
||||||
const vertexRadius = 6;
|
const vertexRadius = 6;
|
||||||
|
|
||||||
const [stage, setStage] = useState<Konva.Stage>();
|
const [stage, setStage] = useState<Konva.Stage>();
|
||||||
|
const [minMaxX, setMinMaxX] = useState([0, 0]);
|
||||||
const [minMaxX, setMinMaxX] = useState<[number, number]>([0, 0]); //min and max in x axis
|
const [minMaxY, setMinMaxY] = useState([0, 0]);
|
||||||
const [minMaxY, setMinMaxY] = useState<[number, number]>([0, 0]); //min and max in y axis
|
|
||||||
|
|
||||||
const handleGroupMouseOver = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
const handleGroupMouseOver = (e: Konva.KonvaEventObject<MouseEvent>) => {
|
||||||
if (!isFinished) return;
|
if (!isFinished) return;
|
||||||
@ -53,39 +53,40 @@ export default function PolygonDrawer({
|
|||||||
if (!stage) {
|
if (!stage) {
|
||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { x, y } = pos;
|
let { x, y } = pos;
|
||||||
const sw = stage.width();
|
const sw = stage.width();
|
||||||
const sh = stage.height();
|
const sh = stage.height();
|
||||||
|
|
||||||
if (minMaxY[0] + y < 0) y = -1 * minMaxY[0];
|
if (minMaxY[0] + y < 0) y = -1 * minMaxY[0];
|
||||||
if (minMaxX[0] + x < 0) x = -1 * minMaxX[0];
|
if (minMaxX[0] + x < 0) x = -1 * minMaxX[0];
|
||||||
if (minMaxY[1] + y > sh) y = sh - minMaxY[1];
|
if (minMaxY[1] + y > sh) y = sh - minMaxY[1];
|
||||||
if (minMaxX[1] + x > sw) x = sw - minMaxX[1];
|
if (minMaxX[1] + x > sw) x = sw - minMaxX[1];
|
||||||
|
|
||||||
return { x, y };
|
return { x, y };
|
||||||
};
|
};
|
||||||
|
|
||||||
// const flattenedPointsAsNumber = useMemo(
|
|
||||||
// () => flattenedPoints.flatMap((point) => [point.x, point.y]),
|
|
||||||
// [flattenedPoints],
|
|
||||||
// );
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Group
|
<Group
|
||||||
name="polygon"
|
name="polygon"
|
||||||
draggable={isFinished}
|
draggable={isActive && isFinished}
|
||||||
onDragStart={handleGroupDragStart}
|
onDragStart={isActive ? handleGroupDragStart : undefined}
|
||||||
onDragEnd={handleGroupDragEnd}
|
onDragEnd={isActive ? handleGroupDragEnd : undefined}
|
||||||
dragBoundFunc={groupDragBound}
|
dragBoundFunc={isActive ? groupDragBound : undefined}
|
||||||
onMouseOver={handleGroupMouseOver}
|
onMouseOver={isActive ? handleGroupMouseOver : undefined}
|
||||||
onMouseOut={handleGroupMouseOut}
|
onMouseOut={isActive ? handleGroupMouseOut : undefined}
|
||||||
>
|
>
|
||||||
<Line
|
<Line
|
||||||
points={flattenedPoints}
|
points={flattenedPoints}
|
||||||
stroke="#00F1FF"
|
stroke="#aa0000"
|
||||||
strokeWidth={3}
|
strokeWidth={3}
|
||||||
closed={isFinished}
|
closed={isFinished}
|
||||||
fill="rgb(140,30,255,0.5)"
|
fill="rgb(220,0,0,0.5)"
|
||||||
/>
|
/>
|
||||||
{points.map((point, index) => {
|
{points.map((point, index) => {
|
||||||
|
if (!isActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const x = point[0] - vertexRadius / 2;
|
const x = point[0] - vertexRadius / 2;
|
||||||
const y = point[1] - vertexRadius / 2;
|
const y = point[1] - vertexRadius / 2;
|
||||||
const startPointAttr =
|
const startPointAttr =
|
||||||
@ -96,17 +97,18 @@ export default function PolygonDrawer({
|
|||||||
onMouseOut: handleMouseOutStartPoint,
|
onMouseOut: handleMouseOutStartPoint,
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Circle
|
<Circle
|
||||||
key={index}
|
key={index}
|
||||||
x={x}
|
x={x}
|
||||||
y={y}
|
y={y}
|
||||||
radius={vertexRadius}
|
radius={vertexRadius}
|
||||||
fill="#FF019A"
|
fill="#dd0000"
|
||||||
stroke="#00F1FF"
|
stroke="#cccccc"
|
||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
draggable
|
draggable={isActive}
|
||||||
onDragMove={handlePointDragMove}
|
onDragMove={isActive ? handlePointDragMove : undefined}
|
||||||
dragBoundFunc={(pos) => {
|
dragBoundFunc={(pos) => {
|
||||||
if (stage) {
|
if (stage) {
|
||||||
return dragBoundFunc(
|
return dragBoundFunc(
|
||||||
@ -116,7 +118,7 @@ export default function PolygonDrawer({
|
|||||||
pos,
|
pos,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
return pos; // Return original pos if stage is not defined
|
return pos;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
{...startPointAttr}
|
{...startPointAttr}
|
||||||
|
|||||||
@ -8,18 +8,43 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import DebugCameraImage from "@/components/camera/DebugCameraImage";
|
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import { useCallback, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Slider } from "@/components/ui/slider";
|
import { PolygonCanvas } from "./PolygonCanvas";
|
||||||
import { Label } from "@/components/ui/label";
|
import { useApiHost } from "@/api";
|
||||||
import { useMotionContourArea, useMotionThreshold } from "@/api/ws";
|
import { Polygon } from "@/types/canvas";
|
||||||
import DebugCanvas from "./DebugCanvas";
|
import { interpolatePoints } from "@/utils/canvasUtil";
|
||||||
|
import AutoUpdatingCameraImage from "../camera/AutoUpdatingCameraImage";
|
||||||
|
import { isDesktop } from "react-device-detect";
|
||||||
|
import PolygonControls from "./PolygonControls";
|
||||||
|
import { Skeleton } from "../ui/skeleton";
|
||||||
|
import { useResizeObserver } from "@/hooks/resize-observer";
|
||||||
|
|
||||||
|
const parseCoordinates = (coordinatesString: string) => {
|
||||||
|
const coordinates = coordinatesString.split(",");
|
||||||
|
const points = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < coordinates.length; i += 2) {
|
||||||
|
const x = parseInt(coordinates[i], 10);
|
||||||
|
const y = parseInt(coordinates[i + 1], 10);
|
||||||
|
points.push([x, y]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return points;
|
||||||
|
};
|
||||||
|
|
||||||
export default function SettingsZones() {
|
export default function SettingsZones() {
|
||||||
const { data: config } = useSWR<FrigateConfig>("config");
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
const [zonePolygons, setZonePolygons] = useState<Polygon[]>([]);
|
||||||
|
const [activePolygonIndex, setActivePolygonIndex] = useState<number | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
const imgRef = useRef<HTMLImageElement | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const apiHost = useApiHost();
|
||||||
|
// const videoSource = `${apiHost}api/ptzcam/latest.jpg`;
|
||||||
|
|
||||||
const cameras = useMemo(() => {
|
const cameras = useMemo(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
@ -39,6 +64,119 @@ export default function SettingsZones() {
|
|||||||
}
|
}
|
||||||
}, [config, selectedCamera]);
|
}, [config, selectedCamera]);
|
||||||
|
|
||||||
|
const cameraAspect = useMemo(() => {
|
||||||
|
if (!cameraConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const aspectRatio = cameraConfig.detect.width / cameraConfig.detect.height;
|
||||||
|
console.log("aspect", aspectRatio);
|
||||||
|
|
||||||
|
if (!aspectRatio) {
|
||||||
|
return "normal";
|
||||||
|
} else if (aspectRatio > 2) {
|
||||||
|
return "wide";
|
||||||
|
} else if (aspectRatio < 16 / 9) {
|
||||||
|
return "tall";
|
||||||
|
} else {
|
||||||
|
return "normal";
|
||||||
|
}
|
||||||
|
}, [cameraConfig]);
|
||||||
|
|
||||||
|
const grow = useMemo(() => {
|
||||||
|
if (cameraAspect == "wide") {
|
||||||
|
return "aspect-wide";
|
||||||
|
} else if (cameraAspect == "tall") {
|
||||||
|
if (isDesktop) {
|
||||||
|
return "size-full aspect-tall";
|
||||||
|
} else {
|
||||||
|
return "size-full";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "aspect-video";
|
||||||
|
}
|
||||||
|
}, [cameraAspect]);
|
||||||
|
|
||||||
|
const [{ width: containerWidth, height: containerHeight }] =
|
||||||
|
useResizeObserver(containerRef);
|
||||||
|
|
||||||
|
// Add scrollbar width (when visible) to the available observer width to eliminate screen juddering.
|
||||||
|
// https://github.com/blakeblackshear/frigate/issues/1657
|
||||||
|
let scrollBarWidth = 0;
|
||||||
|
if (window.innerWidth && document.body.offsetWidth) {
|
||||||
|
scrollBarWidth = window.innerWidth - document.body.offsetWidth;
|
||||||
|
}
|
||||||
|
const availableWidth = scrollBarWidth
|
||||||
|
? containerWidth + scrollBarWidth
|
||||||
|
: containerWidth;
|
||||||
|
|
||||||
|
const { width, height } = cameraConfig
|
||||||
|
? cameraConfig.detect
|
||||||
|
: { width: 1, height: 1 };
|
||||||
|
const aspectRatio = width / height;
|
||||||
|
|
||||||
|
const stretch = true;
|
||||||
|
const fitAspect = 1.8;
|
||||||
|
const scaledHeight = useMemo(() => {
|
||||||
|
const scaledHeight =
|
||||||
|
aspectRatio < (fitAspect ?? 0)
|
||||||
|
? Math.floor(containerHeight)
|
||||||
|
: Math.floor(availableWidth / aspectRatio);
|
||||||
|
const finalHeight = stretch ? scaledHeight : Math.min(scaledHeight, height);
|
||||||
|
|
||||||
|
if (finalHeight > 0) {
|
||||||
|
return finalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 100;
|
||||||
|
}, [
|
||||||
|
availableWidth,
|
||||||
|
aspectRatio,
|
||||||
|
containerHeight,
|
||||||
|
fitAspect,
|
||||||
|
height,
|
||||||
|
stretch,
|
||||||
|
]);
|
||||||
|
const scaledWidth = useMemo(
|
||||||
|
() => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth),
|
||||||
|
[scaledHeight, aspectRatio, scrollBarWidth],
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cameraConfig && containerRef.current) {
|
||||||
|
setZonePolygons(
|
||||||
|
Object.entries(cameraConfig.zones).map(([name, zoneData]) => ({
|
||||||
|
name,
|
||||||
|
points: interpolatePoints(
|
||||||
|
parseCoordinates(zoneData.coordinates),
|
||||||
|
cameraConfig.detect.width,
|
||||||
|
cameraConfig.detect.height,
|
||||||
|
scaledWidth,
|
||||||
|
scaledHeight,
|
||||||
|
),
|
||||||
|
isFinished: true,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [cameraConfig, containerRef, scaledWidth, scaledHeight]);
|
||||||
|
|
||||||
|
// const image = useMemo(() => {
|
||||||
|
// if (cameraConfig && containerRef && containerRef.current) {
|
||||||
|
// console.log("width:", containerRef.current.clientWidth);
|
||||||
|
// const element = new window.Image();
|
||||||
|
// element.width = containerRef.current.clientWidth;
|
||||||
|
// element.height = containerRef.current.clientHeight;
|
||||||
|
// element.src = `${apiHost}api/${cameraConfig.name}/latest.jpg`;
|
||||||
|
// return element;
|
||||||
|
// }
|
||||||
|
// }, [cameraConfig, apiHost, containerRef]);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (image) {
|
||||||
|
// imgRef.current = image;
|
||||||
|
// }
|
||||||
|
// }, [image]);
|
||||||
|
|
||||||
if (!cameraConfig && !selectedCamera) {
|
if (!cameraConfig && !selectedCamera) {
|
||||||
return <ActivityIndicator />;
|
return <ActivityIndicator />;
|
||||||
}
|
}
|
||||||
@ -46,7 +184,12 @@ export default function SettingsZones() {
|
|||||||
// console.log("selected camera", selectedCamera);
|
// console.log("selected camera", selectedCamera);
|
||||||
// console.log("threshold", motionThreshold);
|
// console.log("threshold", motionThreshold);
|
||||||
// console.log("contour area", motionContourArea);
|
// console.log("contour area", motionContourArea);
|
||||||
// console.log(cameraConfig);
|
// console.log("zone polygons", zonePolygons);
|
||||||
|
|
||||||
|
// console.log("width:", containerRef.current.clientWidth);
|
||||||
|
// const element = new window.Image();
|
||||||
|
// element.width = containerRef.current.clientWidth;
|
||||||
|
// element.height = containerRef.current.clientHeight;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -72,8 +215,59 @@ export default function SettingsZones() {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<DebugCameraImage cameraConfig={cameraConfig} className="w-[50%]" />
|
|
||||||
<DebugCanvas />
|
{cameraConfig && (
|
||||||
|
<div className="flex flex-row justify-evenly">
|
||||||
|
<div
|
||||||
|
className={`flex flex-col justify-center items-center w-[50%] ${grow}`}
|
||||||
|
>
|
||||||
|
<div ref={containerRef} className="size-full">
|
||||||
|
{cameraConfig ? (
|
||||||
|
<PolygonCanvas
|
||||||
|
camera={cameraConfig.name}
|
||||||
|
width={scaledWidth}
|
||||||
|
height={scaledHeight}
|
||||||
|
polygons={zonePolygons}
|
||||||
|
setPolygons={setZonePolygons}
|
||||||
|
activePolygonIndex={activePolygonIndex}
|
||||||
|
setActivePolygonIndex={setActivePolygonIndex}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Skeleton className="w-full h-full" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-[30%]">
|
||||||
|
<PolygonControls
|
||||||
|
camera={cameraConfig.name}
|
||||||
|
width={scaledWidth}
|
||||||
|
height={scaledHeight}
|
||||||
|
polygons={zonePolygons}
|
||||||
|
setPolygons={setZonePolygons}
|
||||||
|
activePolygonIndex={activePolygonIndex}
|
||||||
|
setActivePolygonIndex={setActivePolygonIndex}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col justify-center items-center m-auto w-[30%] bg-secondary">
|
||||||
|
<pre style={{ whiteSpace: "pre-wrap" }}>
|
||||||
|
{JSON.stringify(
|
||||||
|
zonePolygons &&
|
||||||
|
zonePolygons.map((polygon) =>
|
||||||
|
interpolatePoints(
|
||||||
|
polygon.points,
|
||||||
|
scaledWidth,
|
||||||
|
scaledHeight,
|
||||||
|
cameraConfig.detect.width,
|
||||||
|
cameraConfig.detect.height,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
)}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -47,7 +47,7 @@ function General() {
|
|||||||
export default function Settings() {
|
export default function Settings() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tabs defaultValue="account" className="w-auto">
|
<Tabs defaultValue="general" className="w-auto">
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
<TabsTrigger value="objects">Objects</TabsTrigger>
|
<TabsTrigger value="objects">Objects</TabsTrigger>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
export type Point = {
|
export type Polygon = {
|
||||||
x: number;
|
name: string;
|
||||||
y: number;
|
points: number[][];
|
||||||
|
isFinished: boolean;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
export const getAveragePoint = (points: number[]): number => {
|
import { Vector2d } from "konva/lib/types";
|
||||||
|
|
||||||
|
export const getAveragePoint = (points: number[]): Vector2d => {
|
||||||
let totalX = 0;
|
let totalX = 0;
|
||||||
let totalY = 0;
|
let totalY = 0;
|
||||||
for (let i = 0; i < points.length; i += 2) {
|
for (let i = 0; i < points.length; i += 2) {
|
||||||
@ -22,8 +24,8 @@ export const dragBoundFunc = (
|
|||||||
stageWidth: number,
|
stageWidth: number,
|
||||||
stageHeight: number,
|
stageHeight: number,
|
||||||
vertexRadius: number,
|
vertexRadius: number,
|
||||||
pos: Point,
|
pos: Vector2d,
|
||||||
): Point => {
|
): Vector2d => {
|
||||||
let x = pos.x;
|
let x = pos.x;
|
||||||
let y = pos.y;
|
let y = pos.y;
|
||||||
if (pos.x + vertexRadius > stageWidth) x = stageWidth;
|
if (pos.x + vertexRadius > stageWidth) x = stageWidth;
|
||||||
@ -43,3 +45,21 @@ export const minMax = (points: number[]): [number, number] => {
|
|||||||
[undefined, undefined],
|
[undefined, undefined],
|
||||||
) as [number, number];
|
) as [number, number];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const interpolatePoints = (
|
||||||
|
points: number[][],
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
newWidth: number,
|
||||||
|
newHeight: number,
|
||||||
|
): number[][] => {
|
||||||
|
const newPoints: number[][] = [];
|
||||||
|
|
||||||
|
for (const [x, y] of points) {
|
||||||
|
const newX = (x * newWidth) / width;
|
||||||
|
const newY = (y * newHeight) / height;
|
||||||
|
newPoints.push([Math.floor(newX), Math.floor(newY)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return newPoints;
|
||||||
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user