feat: Puente Mayúscula-Minúscula + TTS SpeechSynthesis + E2E fixes
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useRef, useCallback, useEffect } from "react"
|
||||
import { getStroke } from "perfect-freehand"
|
||||
|
||||
interface TrazoLetraProps {
|
||||
letra: string
|
||||
color?: string
|
||||
onComplete?: () => void
|
||||
promptLevel?: number
|
||||
}
|
||||
|
||||
export default function TrazoLetra({
|
||||
letra,
|
||||
color = "#8CB8A0",
|
||||
onComplete,
|
||||
promptLevel = 0,
|
||||
}: TrazoLetraProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null)
|
||||
const [isDrawing, setIsDrawing] = useState(false)
|
||||
const points = useRef<number[][]>([])
|
||||
const [showLetter, setShowLetter] = useState(promptLevel < 2)
|
||||
|
||||
useEffect(() => {
|
||||
if (promptLevel < 2) {
|
||||
drawLetterGuide(letra)
|
||||
}
|
||||
}, [letra, promptLevel])
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
canvas.style.touchAction = "none"
|
||||
}, [])
|
||||
|
||||
const drawLetterGuide = (l: string) => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (!ctx) return
|
||||
const w = canvas.width
|
||||
const h = canvas.height
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
|
||||
ctx.font = `${h * 0.6}px "Fredoka", sans-serif`
|
||||
ctx.textAlign = "center"
|
||||
ctx.textBaseline = "middle"
|
||||
ctx.globalAlpha = 0.15
|
||||
ctx.fillStyle = "#2D3436"
|
||||
ctx.fillText(l.toUpperCase(), w / 2, h / 2)
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
|
||||
const startDrawing = useCallback((e: React.PointerEvent) => {
|
||||
setIsDrawing(true)
|
||||
points.current = [[e.clientX, e.clientY, e.pressure || 0.5]]
|
||||
const canvas = canvasRef.current
|
||||
if (canvas) canvas.setPointerCapture(e.pointerId)
|
||||
}, [])
|
||||
|
||||
const draw = useCallback((e: React.PointerEvent) => {
|
||||
if (!isDrawing) return
|
||||
points.current.push([e.clientX, e.clientY, e.pressure || 0.5])
|
||||
renderStroke()
|
||||
}, [isDrawing])
|
||||
|
||||
const endDrawing = useCallback(() => {
|
||||
setIsDrawing(false)
|
||||
if (points.current.length > 5 && onComplete) {
|
||||
onComplete()
|
||||
}
|
||||
points.current = []
|
||||
}, [onComplete])
|
||||
|
||||
const renderStroke = () => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
const ctx = canvas.getContext("2d")
|
||||
if (!ctx) return
|
||||
|
||||
const stroke = getStroke(points.current, {
|
||||
size: 12,
|
||||
thinning: 0.5,
|
||||
smoothing: 0.5,
|
||||
streamline: 0.5,
|
||||
})
|
||||
|
||||
if (stroke.length < 2) return
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
if (showLetter) drawLetterGuide(letra)
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(stroke[0][0], stroke[0][1])
|
||||
for (let i = 1; i < stroke.length; i++) {
|
||||
ctx.lineTo(stroke[i][0], stroke[i][1])
|
||||
}
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = 3
|
||||
ctx.lineCap = "round"
|
||||
ctx.lineJoin = "round"
|
||||
ctx.fillStyle = color
|
||||
ctx.fill()
|
||||
|
||||
if (promptLevel < 3) {
|
||||
drawDirectionArrow(ctx, letra)
|
||||
}
|
||||
}
|
||||
|
||||
const drawDirectionArrow = (ctx: CanvasRenderingContext2D, l: string) => {
|
||||
const w = ctx.canvas.width
|
||||
const h = ctx.canvas.height
|
||||
ctx.save()
|
||||
ctx.globalAlpha = 0.3
|
||||
ctx.strokeStyle = "#8CB8A0"
|
||||
ctx.lineWidth = 2
|
||||
ctx.fillStyle = "#8CB8A0"
|
||||
|
||||
const arrowPaths: Record<string, { sx: number; sy: number; ex: number; ey: number }> = {
|
||||
a: { sx: w * 0.3, sy: h * 0.7, ex: w * 0.7, ey: h * 0.3 },
|
||||
e: { sx: w * 0.3, sy: h * 0.3, ex: w * 0.7, ey: h * 0.7 },
|
||||
i: { sx: w * 0.5, sy: h * 0.2, ex: w * 0.5, ey: h * 0.8 },
|
||||
o: { sx: w * 0.3, sy: h * 0.3, ex: w * 0.6, ey: h * 0.6 },
|
||||
u: { sx: w * 0.3, sy: h * 0.3, ex: w * 0.7, ey: h * 0.5 },
|
||||
}
|
||||
|
||||
const path = arrowPaths[l.toLowerCase()]
|
||||
if (path) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(path.sx, path.sy)
|
||||
ctx.lineTo(path.ex, path.ey)
|
||||
ctx.stroke()
|
||||
|
||||
const angle = Math.atan2(path.ey - path.sy, path.ex - path.sx)
|
||||
const sz = 6
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(path.ex, path.ey)
|
||||
ctx.lineTo(path.ex - sz * Math.cos(angle - Math.PI / 6), path.ey - sz * Math.sin(angle - Math.PI / 6))
|
||||
ctx.lineTo(path.ex - sz * Math.cos(angle + Math.PI / 6), path.ey - sz * Math.sin(angle + Math.PI / 6))
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
width={300}
|
||||
height={300}
|
||||
className="w-[250px] h-[250px] touch-none rounded-2xl bg-white shadow-md"
|
||||
onPointerDown={startDrawing}
|
||||
onPointerMove={draw}
|
||||
onPointerUp={endDrawing}
|
||||
onPointerCancel={endDrawing}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user