// Отрисовка кнопки «Моя волна» на Canvas 2D — порт прототипа docs/Разработка дизайна приложения/wave.js // (варианты orb и liquid; «Кольца» не реализуем — docs/design.md). Физика и цвета — в @pipu/core (wave-anim). import { mix, rgb, rgba, type Rgb, type WaveState } from '@pipu/core'; export { rgb, step, type Rgb, type WaveState } from '@pipu/core'; export function drawOrb(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number, s: WaveState, t: number) { const [c1, c2, c3] = s.colors.map(rgb) as [Rgb, Rgb, Rgb]; const glow = ctx.createRadialGradient(cx, cy, r * 0.6, cx, cy, r * 1.65); glow.addColorStop(0, rgba(c1, 0.32 + s.pulse * 0.35 + s.hover * 0.12)); glow.addColorStop(0.5, rgba(c2, 0.1 + s.pulse * 0.12)); glow.addColorStop(1, rgba(c1, 0)); ctx.fillStyle = glow; ctx.beginPath(); ctx.arc(cx, cy, r * 1.65, 0, Math.PI * 2); ctx.fill(); ctx.save(); ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI * 2); ctx.clip(); ctx.fillStyle = s.dark ? '#060B1C' : '#0B1533'; ctx.fillRect(cx - r, cy - r, r * 2, r * 2); ctx.globalCompositeOperation = 'lighter'; const blobs = [c1, c2, c3, c1, c2]; blobs.forEach((c, i) => { const a = t * (0.25 + i * 0.07) + i * 1.7; const bx = cx + Math.cos(a) * r * (0.35 + 0.12 * Math.sin(t * 0.3 + i)) + s.mx * r * 0.18 * s.hover; const by = cy + Math.sin(a * 1.3) * r * 0.38 + s.my * r * 0.18 * s.hover; const br = r * (0.75 + 0.15 * Math.sin(t * 0.7 + i) + s.pulse * 0.2); const g = ctx.createRadialGradient(bx, by, 0, bx, by, br); g.addColorStop(0, rgba(c, 0.55)); g.addColorStop(1, rgba(c, 0)); ctx.fillStyle = g; ctx.fillRect(cx - r, cy - r, r * 2, r * 2); }); const lines = 9; for (let l = 0; l < lines; l++) { const yy = cy - r * 0.75 + (l / (lines - 1)) * r * 1.5; const amp = r * (0.03 + 0.05 * s.amp + 0.09 * s.pulse + 0.03 * s.hover); ctx.beginPath(); for (let x = -r; x <= r; x += 4) { const y = yy + Math.sin((x / r) * (3 + l * 0.4) + t * (1.2 + l * 0.15) + l) * amp * Math.cos((x / r) * 1.4); if (x === -r) ctx.moveTo(cx + x, y); else ctx.lineTo(cx + x, y); } ctx.strokeStyle = `rgba(255,255,255,${0.05 + 0.05 * s.amp + (l % 3 === 0 ? 0.05 : 0)})`; ctx.lineWidth = 1.5; ctx.stroke(); } ctx.globalCompositeOperation = 'source-over'; const hl = ctx.createRadialGradient(cx - r * 0.35, cy - r * 0.45, 0, cx - r * 0.35, cy - r * 0.45, r * 0.9); hl.addColorStop(0, 'rgba(255,255,255,0.18)'); hl.addColorStop(1, 'rgba(255,255,255,0)'); ctx.fillStyle = hl; ctx.fillRect(cx - r, cy - r, r * 2, r * 2); ctx.restore(); ctx.beginPath(); ctx.arc(cx, cy, r - 0.5, 0, Math.PI * 2); ctx.strokeStyle = rgba(mix(c1, [255, 255, 255], 0.4), 0.35 + s.pulse * 0.3); ctx.lineWidth = 1; ctx.stroke(); } export function drawLiquid(ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number, s: WaveState, t: number) { const cols = s.colors.map(rgb) as [Rgb, Rgb, Rgb]; const glow = ctx.createRadialGradient(cx, cy, r * 0.4, cx, cy, r * 1.6); glow.addColorStop(0, rgba(cols[0], 0.3 + s.pulse * 0.3)); glow.addColorStop(1, rgba(cols[0], 0)); ctx.fillStyle = glow; ctx.beginPath(); ctx.arc(cx, cy, r * 1.6, 0, Math.PI * 2); ctx.fill(); ctx.globalCompositeOperation = s.dark ? 'lighter' : 'source-over'; for (let L = 0; L < 3; L++) { ctx.beginPath(); const P = 140; for (let j = 0; j <= P; j++) { const th = (j / P) * Math.PI * 2; const w = 1 + 0.06 * Math.sin(3 * th + t * (0.9 + L * 0.2) + L * 2) + 0.04 * Math.sin(5 * th - t * (1.3 + L * 0.1) + L) + 0.03 * Math.sin(7 * th + t * 1.7) * (0.5 + s.amp) + 0.08 * s.pulse * Math.sin(2 * th + L) + 0.16 * s.hover * Math.pow(Math.max(0, Math.cos(th - s.ang)), 3); const rr = r * (0.86 - L * 0.07) * w; const x = cx + Math.cos(th) * rr; const y = cy + Math.sin(th) * rr; if (j) ctx.lineTo(x, y); else ctx.moveTo(x, y); } const ox = Math.cos(t * 0.4 + L * 2) * r * 0.3; const oy = Math.sin(t * 0.5 + L) * r * 0.3; const g = ctx.createRadialGradient(cx + ox, cy + oy, 0, cx, cy, r * 1.1); g.addColorStop(0, rgba(mix(cols[L]!, [255, 255, 255], 0.25), 0.85)); g.addColorStop(1, rgba(cols[L]!, 0.25)); ctx.fillStyle = g; ctx.globalAlpha = s.dark ? 0.6 : 0.75; ctx.fill(); } ctx.globalAlpha = 1; ctx.globalCompositeOperation = 'source-over'; }