1
0

Fix notes

This commit is contained in:
2025-07-18 19:48:31 +02:00
parent dfa54ae883
commit a15956a960

View File

@@ -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 { FiEdit3, FiTrash, FiChevronDown } from 'react-icons/fi'
import { TbEraser } from 'react-icons/tb' import { TbEraser } from 'react-icons/tb'
import './Notes.sass' import './Notes.sass'
@@ -8,12 +8,16 @@ const Notes = () => {
const ctxRef = useRef(null) const ctxRef = useRef(null)
const rectRef = useRef(null) const rectRef = useRef(null)
const saveTimeoutRef = useRef(null) const saveTimeoutRef = useRef(null)
const lastDrawTimeRef = useRef(0)
const pathPointsRef = useRef([])
const animationFrameRef = useRef(null)
const [isDrawing, setIsDrawing] = useState(false) const [isDrawing, setIsDrawing] = useState(false)
const [tool, setTool] = useState('pen') // 'pen' or 'eraser' const [tool, setTool] = useState('pen') // 'pen' or 'eraser'
const [penColor, setPenColor] = useState('#2d3748') const [penColor, setPenColor] = useState('#2d3748')
const [showColorPicker, setShowColorPicker] = useState(false) const [showColorPicker, setShowColorPicker] = useState(false)
const colors = [ // Memoize colors array to prevent recreation on every render
const colors = useMemo(() => [
'#2d3748', // Dark gray '#2d3748', // Dark gray
'#000000', // Black '#000000', // Black
'#e53e3e', // Red '#e53e3e', // Red
@@ -24,7 +28,7 @@ const Notes = () => {
'#dd6b20', // Orange '#dd6b20', // Orange
'#e91e63', // Pink '#e91e63', // Pink
'#00acc1' // Cyan '#00acc1' // Cyan
] ], [])
const loadCanvasFromStorage = useCallback(() => { const loadCanvasFromStorage = useCallback(() => {
const canvas = canvasRef.current const canvas = canvasRef.current
@@ -91,18 +95,34 @@ const Notes = () => {
if (saveTimeoutRef.current) { if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
} }
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current)
}
} }
}, [loadCanvasFromStorage]) }, [loadCanvasFromStorage])
// Debounced save function to prevent excessive localStorage writes // Optimized save function with compression and size check
const debouncedSave = useCallback(() => { const debouncedSave = useCallback(() => {
if (saveTimeoutRef.current) { if (saveTimeoutRef.current) {
clearTimeout(saveTimeoutRef.current) clearTimeout(saveTimeoutRef.current)
} }
saveTimeoutRef.current = setTimeout(() => { saveTimeoutRef.current = setTimeout(() => {
const canvas = canvasRef.current const canvas = canvasRef.current
const dataURL = canvas.toDataURL() if (!canvas) return
localStorage.setItem('notes-canvas', dataURL)
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 }, 500) // Save 500ms after user stops drawing
}, []) }, [])
@@ -110,6 +130,34 @@ const Notes = () => {
debouncedSave() debouncedSave()
}, [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) => { const startDrawing = useCallback((e) => {
setIsDrawing(true) setIsDrawing(true)
const ctx = ctxRef.current const ctx = ctxRef.current
@@ -120,6 +168,9 @@ const Notes = () => {
const x = e.clientX - rect.left const x = e.clientX - rect.left
const y = e.clientY - rect.top 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 // Set tool properties once at the start of drawing
if (tool === 'pen') { if (tool === 'pen') {
ctx.globalCompositeOperation = 'source-over' ctx.globalCompositeOperation = 'source-over'
@@ -130,29 +181,81 @@ const Notes = () => {
ctx.lineWidth = 40 ctx.lineWidth = 40
} }
// Draw initial dot using stroke for consistency
ctx.beginPath() ctx.beginPath()
ctx.moveTo(x, y) ctx.moveTo(x, y)
ctx.lineTo(x + 0.1, y + 0.1) // Small offset to ensure a visible dot
ctx.stroke()
}, [tool, penColor]) }, [tool, penColor])
// Throttled drawing with requestAnimationFrame for smooth performance
const draw = useCallback((e) => { const draw = useCallback((e) => {
if (!isDrawing) return if (!isDrawing) return
const ctx = ctxRef.current const now = performance.now()
const rect = rectRef.current || canvasRef.current.getBoundingClientRect() 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 x = e.clientX - rect.left
const y = e.clientY - rect.top const y = e.clientY - rect.top
// Tool properties are already set in startDrawing // Add point to path
ctx.lineTo(x, y) pathPointsRef.current.push({ x, y })
ctx.stroke()
// 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]) }, [isDrawing])
const stopDrawing = useCallback(() => { const stopDrawing = useCallback(() => {
if (!isDrawing) return
setIsDrawing(false) 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) // Save canvas state after drawing (debounced)
saveCanvasToStorage() saveCanvasToStorage()
}, [saveCanvasToStorage]) }, [isDrawing, drawSmoothPath, saveCanvasToStorage])
// Optimized touch event handlers // Optimized touch event handlers
const handleTouchStart = useCallback((e) => { const handleTouchStart = useCallback((e) => {
@@ -218,46 +321,58 @@ const Notes = () => {
rectRef.current = canvasRef.current.getBoundingClientRect() rectRef.current = canvasRef.current.getBoundingClientRect()
}, []) }, [])
// Memoized color picker to prevent recreation
const colorPicker = useMemo(() => (
showColorPicker && (
<div className="color-picker-popover">
<div className="color-grid">
{colors.map((color) => (
<button
key={color}
className={`color-option ${penColor === color ? 'active' : ''}`}
style={{ backgroundColor: color }}
onClick={() => {
setPenColor(color)
setShowColorPicker(false)
}}
title={`Farbe: ${color}`}
/>
))}
</div>
</div>
)
), [showColorPicker, colors, penColor])
// Memoized toolbar buttons to prevent unnecessary re-renders
const toolbarButtons = useMemo(() => (
<>
<button
className={`tool-button ${tool === 'pen' ? 'active' : ''}`}
onClick={() => setTool('pen')}
title="Stift"
style={{ backgroundColor: tool === 'pen' ? penColor + '20' : undefined }}
>
<FiEdit3 style={{ color: tool === 'pen' ? penColor : undefined }} />
</button>
<button
className="color-picker-button"
onClick={() => setShowColorPicker(!showColorPicker)}
title="Farbe wählen"
style={{ backgroundColor: penColor }}
>
<FiChevronDown />
</button>
</>
), [tool, penColor, showColorPicker])
return ( return (
<div className="notes-page"> <div className="notes-page">
<div className="notes-container"> <div className="notes-container">
{/* Toolbar */} {/* Toolbar */}
<div className="notes-toolbar"> <div className="notes-toolbar">
<div className="pen-tool-group"> <div className="pen-tool-group">
<button {toolbarButtons}
className={`tool-button ${tool === 'pen' ? 'active' : ''}`} {colorPicker}
onClick={() => setTool('pen')}
title="Stift"
style={{ backgroundColor: tool === 'pen' ? penColor + '20' : undefined }}
>
<FiEdit3 style={{ color: tool === 'pen' ? penColor : undefined }} />
</button>
<button
className="color-picker-button"
onClick={() => setShowColorPicker(!showColorPicker)}
title="Farbe wählen"
style={{ backgroundColor: penColor }}
>
<FiChevronDown />
</button>
{showColorPicker && (
<div className="color-picker-popover">
<div className="color-grid">
{colors.map((color) => (
<button
key={color}
className={`color-option ${penColor === color ? 'active' : ''}`}
style={{ backgroundColor: color }}
onClick={() => {
setPenColor(color)
setShowColorPicker(false)
}}
title={`Farbe: ${color}`}
/>
))}
</div>
</div>
)}
</div> </div>
<button <button