Fix notes
This commit is contained in:
@@ -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
|
||||||
|
Reference in New Issue
Block a user