Files
wehub-resource-sync ead81af521
Deploy to GitHub Pages / deploy (push) Failing after 0s
CI / go (push) Has been cancelled
CI / build (darwin, macos-14) (push) Has been cancelled
CI / build (windows, windows-2025) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:31:13 +08:00

177 lines
4.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ui
import (
"math"
"strings"
)
// pulseCoords holds per-dot distance-from-center and angle values for the
// current panel dimensions. Recomputed lazily on resize so the hot render
// loop can skip ~3360 sqrt and atan2 calls per frame (5 rows × 84 cols × 8
// dots) and read from flat arrays instead.
type pulseCoords struct {
width, height int
maxR float64
dist []float64
angle []float64
}
func (v *Visualizer) pulseCoords() *pulseCoords {
height := v.Rows
width := PanelWidth
if c := v.pulseCoordCache; c != nil && c.width == width && c.height == height {
return c
}
dotRows := height * 4
dotCols := width * 2
centerX := float64(dotCols) / 2.0
centerY := float64(dotRows) / 2.0
xScale := centerY / centerX
size := height * width * 8
c := &pulseCoords{
width: width,
height: height,
maxR: centerY - 1,
dist: make([]float64, size),
angle: make([]float64, size),
}
for row := range height {
for col := range width {
for dr := range 4 {
for dc := range 2 {
dx := (float64(col*2+dc) - centerX) * xScale
dy := float64(row*4+dr) - centerY
idx := pulseDotIndex(row, col, dr, dc, width)
c.dist[idx] = math.Sqrt(dx*dx + dy*dy)
a := math.Atan2(dy, dx)
if a < 0 {
a += 2 * math.Pi
}
c.angle[idx] = a
}
}
}
}
v.pulseCoordCache = c
return c
}
func pulseDotIndex(row, col, dr, dc, width int) int {
return ((row*width+col)*4+dr)*2 + dc
}
// renderPulse draws a pulsating ellipse using Braille dots that fills the
// display width. The radius at each angle blends per-band frequency energy
// with the overall level so the whole shape surges on every beat while still
// deforming per frequency. A clean shockwave ring radiates outward on
// transients. The interior is solid-filled with an anti-aliased edge and a
// green→yellow→red radial color gradient.
func (v *Visualizer) renderPulse(bands []float64) string {
coords := v.pulseCoords()
height := v.Rows
width := PanelWidth
bandCount := len(bands)
maxR := coords.maxR
var totalEnergy float64
for _, e := range bands {
totalEnergy += e
}
avgEnergy := totalEnergy / float64(bandCount)
// Shockwave: expanding ring that fades as it grows.
shockPhase := math.Mod(float64(v.frame)*0.10, 1.0)
shockR := maxR * (0.3 + 0.7*shockPhase)
shockStrength := avgEnergy * avgEnergy * (1.0 - shockPhase*shockPhase)
// Gentle breathing keeps the shape alive during silence.
breath := math.Sin(float64(v.frame)*0.05) * 0.02
// Per-frame rotation offset — added uniformly to every cached angle.
rotOffset := float64(v.frame) * (0.015 + avgEnergy*0.04)
twoPi := 2 * math.Pi
bandScale := float64(bandCount) / twoPi
lines := make([]string, height)
for row := range height {
var sb, run strings.Builder
tag := -1
for col := range width {
var braille rune = '\u2800'
var maxNorm float64
for dr := range 4 {
for dc := range 2 {
idx := pulseDotIndex(row, col, dr, dc, width)
dist := coords.dist[idx]
rotAngle := coords.angle[idx] + rotOffset
rotAngle -= math.Floor(rotAngle/twoPi) * twoPi
// Cosine-interpolated band mapping.
bandPos := rotAngle * bandScale
bandIdx := int(bandPos) % bandCount
nextBand := (bandIdx + 1) % bandCount
frac := bandPos - math.Floor(bandPos)
t := (1 - math.Cos(frac*math.Pi)) / 2
energy := bands[bandIdx]*(1-t) + bands[nextBand]*t
// Blend per-band with overall so the whole shape beats.
blended := energy*0.6 + avgEnergy*0.4
punch := blended * blended
r := maxR * (0.08 + breath + 0.92*punch)
// --- Solid fill ---
if r > 0.5 && dist <= r {
norm := dist / r
if norm > maxNorm {
maxNorm = norm
}
braille |= brailleBit[dr][dc]
} else if r > 0.5 && dist < r+1.5 {
// Anti-aliased edge.
edgeFade := 1.0 - (dist-r)/1.5
if scatterHash(bandIdx, row*4+dr, col*2+dc, v.frame) < edgeFade*0.7 {
braille |= brailleBit[dr][dc]
if maxNorm < 0.9 {
maxNorm = 0.9
}
}
}
// --- Shockwave ring ---
if shockStrength > 0.05 {
shockDist := math.Abs(dist - shockR)
shockThick := 0.6 + shockStrength*1.5
if shockDist < shockThick {
fade := 1.0 - shockDist/shockThick
if fade > 0.4 {
braille |= brailleBit[dr][dc]
if maxNorm < 0.65 {
maxNorm = 0.65
}
}
}
}
}
}
// Radial color gradient: green core → yellow → red edge.
newTag := specTag(maxNorm)
if newTag != tag {
flushStyleRun(&sb, &run, tag)
tag = newTag
}
run.WriteRune(braille)
}
flushStyleRun(&sb, &run, tag)
lines[row] = sb.String()
}
return strings.Join(lines, "\n")
}