diff --git a/dashboard/src/renderer/src/pages/Notes.jsx b/dashboard/src/renderer/src/pages/Notes.jsx index 72aff29..80e4d8b 100644 --- a/dashboard/src/renderer/src/pages/Notes.jsx +++ b/dashboard/src/renderer/src/pages/Notes.jsx @@ -1,4 +1,4 @@ -import { useRef, useEffect, useState, useCallback } from 'react' +import { useRef, useEffect, useState, useCallback, useMemo } from 'react' import { FiEdit3, FiTrash, FiChevronDown } from 'react-icons/fi' import { TbEraser } from 'react-icons/tb' import './Notes.sass' @@ -8,12 +8,16 @@ const Notes = () => { const ctxRef = useRef(null) const rectRef = useRef(null) const saveTimeoutRef = useRef(null) + const lastDrawTimeRef = useRef(0) + const pathPointsRef = useRef([]) + const animationFrameRef = useRef(null) const [isDrawing, setIsDrawing] = useState(false) const [tool, setTool] = useState('pen') // 'pen' or 'eraser' const [penColor, setPenColor] = useState('#2d3748') const [showColorPicker, setShowColorPicker] = useState(false) - const colors = [ + // Memoize colors array to prevent recreation on every render + const colors = useMemo(() => [ '#2d3748', // Dark gray '#000000', // Black '#e53e3e', // Red @@ -24,7 +28,7 @@ const Notes = () => { '#dd6b20', // Orange '#e91e63', // Pink '#00acc1' // Cyan - ] + ], []) const loadCanvasFromStorage = useCallback(() => { const canvas = canvasRef.current @@ -91,18 +95,34 @@ const Notes = () => { if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current) } + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + } } }, [loadCanvasFromStorage]) - // Debounced save function to prevent excessive localStorage writes + // Optimized save function with compression and size check const debouncedSave = useCallback(() => { if (saveTimeoutRef.current) { clearTimeout(saveTimeoutRef.current) } saveTimeoutRef.current = setTimeout(() => { const canvas = canvasRef.current - const dataURL = canvas.toDataURL() - localStorage.setItem('notes-canvas', dataURL) + if (!canvas) return + + try { + // Use JPEG for better compression on large canvases + const dataURL = canvas.width * canvas.height > 500000 + ? canvas.toDataURL('image/jpeg', 0.8) + : canvas.toDataURL('image/png') + + // Check if data is too large for localStorage (usually 5-10MB limit) + if (dataURL.length < 4900000) { // ~5MB safety margin + localStorage.setItem('notes-canvas', dataURL) + } + } catch (error) { + console.warn('Failed to save canvas:', error) + } }, 500) // Save 500ms after user stops drawing }, []) @@ -110,6 +130,34 @@ const Notes = () => { debouncedSave() }, [debouncedSave]) + // Optimized drawing with path smoothing and throttling + const drawSmoothPath = useCallback(() => { + const ctx = ctxRef.current + const points = pathPointsRef.current + + if (!ctx || points.length < 2) return + + // Use quadratic curves for smoother lines + ctx.beginPath() + ctx.moveTo(points[0].x, points[0].y) + + for (let i = 1; i < points.length - 1; i++) { + const currentPoint = points[i] + const nextPoint = points[i + 1] + const cpx = (currentPoint.x + nextPoint.x) / 2 + const cpy = (currentPoint.y + nextPoint.y) / 2 + ctx.quadraticCurveTo(currentPoint.x, currentPoint.y, cpx, cpy) + } + + // Draw the last point + if (points.length > 1) { + const lastPoint = points[points.length - 1] + ctx.lineTo(lastPoint.x, lastPoint.y) + } + + ctx.stroke() + }, []) + const startDrawing = useCallback((e) => { setIsDrawing(true) const ctx = ctxRef.current @@ -120,6 +168,9 @@ const Notes = () => { const x = e.clientX - rect.left const y = e.clientY - rect.top + // Reset path points for new stroke + pathPointsRef.current = [{ x, y }] + // Set tool properties once at the start of drawing if (tool === 'pen') { ctx.globalCompositeOperation = 'source-over' @@ -130,29 +181,81 @@ const Notes = () => { ctx.lineWidth = 40 } + // Draw initial dot using stroke for consistency ctx.beginPath() ctx.moveTo(x, y) + ctx.lineTo(x + 0.1, y + 0.1) // Small offset to ensure a visible dot + ctx.stroke() }, [tool, penColor]) + // Throttled drawing with requestAnimationFrame for smooth performance const draw = useCallback((e) => { if (!isDrawing) return - const ctx = ctxRef.current - const rect = rectRef.current || canvasRef.current.getBoundingClientRect() + const now = performance.now() + const timeDiff = now - lastDrawTimeRef.current + // Throttle to ~60fps for performance + if (timeDiff < 16) return + + const rect = rectRef.current || canvasRef.current.getBoundingClientRect() const x = e.clientX - rect.left const y = e.clientY - rect.top - // Tool properties are already set in startDrawing - ctx.lineTo(x, y) - ctx.stroke() + // Add point to path + pathPointsRef.current.push({ x, y }) + + // Limit path points to prevent memory issues + if (pathPointsRef.current.length > 50) { + pathPointsRef.current = pathPointsRef.current.slice(-25) + } + + // Cancel previous animation frame + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + } + + // Schedule drawing on next frame + animationFrameRef.current = requestAnimationFrame(() => { + const ctx = ctxRef.current + if (!ctx) return + + // Simple line drawing for real-time feedback + const points = pathPointsRef.current + if (points.length >= 2) { + const lastTwo = points.slice(-2) + ctx.beginPath() + ctx.moveTo(lastTwo[0].x, lastTwo[0].y) + ctx.lineTo(lastTwo[1].x, lastTwo[1].y) + ctx.stroke() + } + }) + + lastDrawTimeRef.current = now }, [isDrawing]) const stopDrawing = useCallback(() => { + if (!isDrawing) return + setIsDrawing(false) + + // Cancel any pending animation frame + if (animationFrameRef.current) { + cancelAnimationFrame(animationFrameRef.current) + animationFrameRef.current = null + } + + // Draw final smooth path if we have enough points + if (pathPointsRef.current.length > 2) { + drawSmoothPath() + } + + // Clear path points + pathPointsRef.current = [] + // Save canvas state after drawing (debounced) saveCanvasToStorage() - }, [saveCanvasToStorage]) + }, [isDrawing, drawSmoothPath, saveCanvasToStorage]) // Optimized touch event handlers const handleTouchStart = useCallback((e) => { @@ -218,46 +321,58 @@ const Notes = () => { rectRef.current = canvasRef.current.getBoundingClientRect() }, []) + // Memoized color picker to prevent recreation + const colorPicker = useMemo(() => ( + showColorPicker && ( +
+
+ {colors.map((color) => ( +
+
+ ) + ), [showColorPicker, colors, penColor]) + + // Memoized toolbar buttons to prevent unnecessary re-renders + const toolbarButtons = useMemo(() => ( + <> + + + + ), [tool, penColor, showColorPicker]) + return (
{/* Toolbar */}
- - - {showColorPicker && ( -
-
- {colors.map((color) => ( -
-
- )} + {toolbarButtons} + {colorPicker}