Помогите исправить проблему с определением вершин при рисовании на Canvas
Моя задача от руки нарисовать некий график, а затем определить где были самые высшие точки и самые нижние. Все работает, но если рисовать очень медленно, если же рисовать быстро, то определяется не правильно. Помогите исправить, спасибо!
import { createRef, MouseEvent, useEffect, useRef, useState } from "react";
type Point = { x: number; y: number; };
const CANVAS_SIZE: [ width: number, height: number ] = [ 980, 600 ];
const LINE_STROKE =
{
width: 5,
color: "#FFFFFF"
} as const;
function findPeakIndex(points: Point[])
{
let maxIndex: null | number = null;
let maxValue = -Infinity;
for (let i = 0; i < points.length; i++)
{
if (points[ i ].y > maxValue)
{
maxIndex = i;
maxValue = points[ i ].y;
}
}
return maxIndex;
}
function findValleyIndex(paths: Point[])
{
let minIndex: number | null = null;
let minValue = Infinity;
for (let i = 0; i < paths.length; i++)
{
if (paths[ i ].y < minValue)
{
minIndex = i;
minValue = paths[ i ].y;
}
}
return minIndex;
}
function findPeaksAndValleys(path: Point[])
{
// Smooth path with moving average filter
const smoothPath = path.map((point) => point.y);
const smoothWindowSize = 20;
for (let i = 0; i < smoothPath.length; i++)
{
const windowStart = Math.max(0, i - smoothWindowSize);
const windowEnd = Math.min(smoothPath.length - 1, i + smoothWindowSize);
const window = smoothPath.slice(windowStart, windowEnd + 1);
const windowSum = window.reduce((a, b) => a + b, 0);
smoothPath[ i ] = windowSum / window.length;
}
// Find peaks and valleys based on filtered path
const peaks: Point[] = [];
let peakStart: null | number = null;
let peakEnd: null | number = null;
let peakMax = -Infinity;
let peakThreshold = 0.05; // Initial threshold for peak detection
for (let i = 0; i < smoothPath.length; i++)
{
const value = smoothPath[ i ];
if (value > peakMax)
{
peakMax = value;
peakEnd = i;
}
else if (peakStart !== null && peakEnd !== null)
{
const peakIndex = findPeakIndex(path.slice(peakStart, peakEnd + 1));
if (peakIndex !== null)
{
peaks.push(path[ peakStart + peakIndex ]);
}
peakStart = null;
peakEnd = null;
peakMax = -Infinity;
peakThreshold = 0.05; // Reset threshold for peak detection
}
if (peakStart === null || value > peakMax - peakThreshold * peakMax)
{
peakStart = i;
peakMax = value;
}
else if (value < peakMax - peakThreshold * peakMax)
{
peakThreshold = 0.01; // Decrease threshold for peak detection
}
}
const valleys: Point[] = [];
let valleyStart: null | number = null;
let valleyEnd: null | number = null;
let valleyMin = Infinity;
let valleyThreshold = 0.05; // Initial threshold for valley detection
for (let i = 0; i < smoothPath.length; i++)
{
const value = smoothPath[ i ];
if (value < valleyMin)
{
valleyMin = value;
valleyEnd = i;
}
else if (valleyStart !== null && valleyEnd !== null)
{
const valleyIndex = findValleyIndex(path.slice(valleyStart, valleyEnd + 1));
if (valleyIndex !== null)
{
valleys.push(path[ valleyStart + valleyIndex ]);
}
valleyStart = null;
valleyEnd = null;
valleyMin = Infinity;
valleyThreshold = 0.05; // Reset threshold for valley detection
}
if (valleyStart === null || value < valleyMin + valleyThreshold * valleyMin)
{
valleyStart = i;
valleyMin = value;
}
else if (value > valleyMin + valleyThreshold * valleyMin)
{
valleyThreshold = 0.01; // Decrease threshold for valley detection
}
}
return { peaks, valleys };
}
export default function App() {
const loadRef = useRef<boolean>(false);
const canvasRef = createRef<HTMLCanvasElement>();
const contextRef = useRef<CanvasRenderingContext2D>();
const [ isDrawing, setIsDrawing ] = useState<boolean>(false);
const [ path, setPath ] = useState<Point[]>([]);
// useEffect(() => console.info(path), [ path ]);
// Effects
useEffect(() =>
{
function prepareCanvas()
{
const canvas = canvasRef.current!;
canvas.width = CANVAS_SIZE[ 0 ];
canvas.height = CANVAS_SIZE[ 1 ];
canvas.style.width = CANVAS_SIZE[ 0 ] + "px";
canvas.style.height = CANVAS_SIZE[ 1 ] + "px";
const ctx = canvas.getContext("2d")!;
ctx.lineCap = "round";
ctx.shadowColor = "rgba(126, 196, 255, 0.102)";
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 5;
ctx.shadowBlur = 10;
contextRef.current = ctx;
}
if (loadRef.current === false)
{
loadRef.current = true;
prepareCanvas();
}
}, []);
// Handles
function clearCanvas()
{
contextRef.current!.clearRect(0, 0, canvasRef.current!.width, canvasRef.current!.height);
}
function startDrawing({ nativeEvent }: MouseEvent<HTMLCanvasElement>)
{
if (!contextRef.current)
{
return;
}
clearCanvas();
const { offsetX: x, offsetY: y } = nativeEvent;
const ctx = contextRef.current;
ctx.strokeStyle = LINE_STROKE.color;
ctx.lineWidth = LINE_STROKE.width;
ctx.beginPath();
ctx.moveTo(x, y);
setIsDrawing(true);
setPath([ { x, y } ]);
}
function finishDrawing()
{
if (!contextRef.current)
{
return;
}
setIsDrawing(false);
const ctx = contextRef.current;
const { peaks, valleys } = findPeaksAndValleys(path);
ctx.closePath();
// Draw incursions
const circleRadius = 10;
// Peaks
ctx.fillStyle = "red";
peaks.forEach((peak) =>
{
ctx.beginPath();
ctx.arc(peak.x, peak.y, circleRadius, 0, 2 * Math.PI);
ctx.fill();
});
// Valleys
ctx.fillStyle = "blue";
valleys.forEach((valley) =>
{
ctx.beginPath();
ctx.arc(valley.x, valley.y, circleRadius, 0, 2 * Math.PI);
ctx.fill();
});
}
function onDraw({ nativeEvent }: MouseEvent<HTMLCanvasElement>)
{
if (!contextRef.current || !isDrawing)
{
return;
}
const { offsetX: x, offsetY: y } = nativeEvent;
const ctx = contextRef.current;
ctx.lineTo(x, y);
ctx.stroke();
setPath((prevPath) => [ ...prevPath, { x, y } ]);
}
// Render
return (
<canvas
ref={canvasRef}
onMouseDown={startDrawing}
onMouseUp={finishDrawing}
onMouseLeave={finishDrawing}
onMouseMove={onDraw}
style={{ backgroundColor: "#FFF", borderRadius: "16px", border: "1px solid black" }}
/>
);
}
Проблема при быстром рисовании:
Правильное отображение (если рисовать медленно):
Источник: Stack Overflow на русском