项目文件夹

文件
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

197 行
5.2 KiB
Go

此文件含有不可见的 Unicode 字符
此文件含有人类无法区分的不可见的 Unicode 字符,但可以由计算机进行不同的处理。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
package ui
import (
"strings"
"time"
)
// flameDriver renders a fire effect using the classic doom-fire propagation:
// a heat field is fed at the bottom row from the spectrum, then each frame
// every cell inherits its neighbour-below's heat with a small lateral wind
// jitter and a random decay. The result is a continuous, lapping flame
// instead of a row of independent columns. Bass thickens the heat source so
// loud passages feed taller flames; quiet passages settle into a low,
// flickering bed of coals.
type flameDriver struct {
heat []float64
dotRows, dotCols int
rng uint64
frame uint64
}
func newFlameDriver() visModeDriver {
return &flameDriver{rng: 0xF1A3C0DE0BADCAFE}
}
func (*flameDriver) AnalysisSpec(*Visualizer) VisAnalysisSpec {
return spectrumAnalysisSpec(DefaultSpectrumBands)
}
func (d *flameDriver) ensure(rows, cols int) {
if d.dotRows == rows && d.dotCols == cols && len(d.heat) == rows*cols {
return
}
d.heat = make([]float64, rows*cols)
d.dotRows = rows
d.dotCols = cols
}
func (d *flameDriver) Tick(v *Visualizer, ctx VisTickContext) {
defaultDriverTick(v, ctx, d.AnalysisSpec(v))
if ctx.OverlayActive {
return
}
dotRows := v.Rows * 4
dotCols := PanelWidth * 2
if dotRows < 4 || dotCols < 4 {
return
}
d.ensure(dotRows, dotCols)
d.frame = v.frame
bands := v.SmoothedBands()
bandCount := len(bands)
// Source (bottom) row: per-column heat seeded from a smooth spectrum sample
// plus a small per-column sparkle so the base shimmers even on quiet input.
if bandCount > 0 {
last := float64(bandCount - 1)
for x := 0; x < dotCols; x++ {
pos := float64(x) / float64(max(1, dotCols-1)) * last
src := sampleBandLinear(bands, pos)
d.rng = d.rng*6364136223846793005 + 1442695040888963407
sparkle := float64((d.rng>>33)%100) / 100.0 * 0.18
// Always keep the base mildly lit so a bed of embers is visible.
base := 0.30 + 0.70*src + sparkle
if base > 1.05 {
base = 1.05
}
d.heat[x] = base
}
} else {
for x := 0; x < dotCols; x++ {
d.rng = d.rng*6364136223846793005 + 1442695040888963407
d.heat[x] = 0.30 + float64((d.rng>>33)%100)/100.0*0.20
}
}
// Propagate heat upward. Process top→down so we always read row y-1 before
// any later iteration overwrites it. Each cell inherits from a horizontally
// jittered neighbour below (the "wind") and loses a randomised amount of
// heat — that randomness is what gives the flame its wispy texture.
for y := dotRows - 1; y >= 1; y-- {
// Heat decays faster toward the top so flames taper. Tuned light so
// flames can climb most of the panel on a strong source.
heightFrac := float64(y) / float64(max(1, dotRows-1))
decayBase := 0.010 + 0.028*heightFrac
for x := 0; x < dotCols; x++ {
d.rng = d.rng*6364136223846793005 + 1442695040888963407
r := d.rng >> 33
offset := int(r%3) - 1 // -1, 0, +1
r >>= 2
decayJitter := float64(r%100) / 100.0 * 0.018
sourceX := x + offset
if sourceX < 0 {
sourceX = 0
} else if sourceX >= dotCols {
sourceX = dotCols - 1
}
next := d.heat[(y-1)*dotCols+sourceX] - decayBase - decayJitter
if next < 0 {
next = 0
}
d.heat[y*dotCols+x] = next
}
}
}
func (*flameDriver) TickInterval(_ *Visualizer, ctx VisTickContext) time.Duration {
return defaultDriverTickInterval(ctx)
}
func (d *flameDriver) OnEnter(v *Visualizer) {
if v == nil {
d.heat = nil
d.dotRows = 0
d.dotCols = 0
return
}
d.ensure(v.Rows*4, PanelWidth*2)
for i := range d.heat {
d.heat[i] = 0
}
}
func (*flameDriver) OnLeave(*Visualizer) {}
func (d *flameDriver) Render(v *Visualizer) string {
height := v.Rows
dotRows := height * 4
dotCols := PanelWidth * 2
if dotRows < 4 || dotCols < 4 {
return strings.Repeat("\n", max(0, height-1))
}
if d.dotRows != dotRows || d.dotCols != dotCols || len(d.heat) != dotRows*dotCols {
d.ensure(dotRows, dotCols)
}
lines := make([]string, height)
for row := 0; row < height; row++ {
var sb, run strings.Builder
tag := -1
for col := 0; col < PanelWidth; col++ {
var braille rune = ''
cellTag := -1
for dr := 0; dr < 4; dr++ {
for dc := 0; dc < 2; dc++ {
y := row*4 + dr
x := col*2 + dc
// Panel y=0 is top; heat buffer y=0 is bottom (the source).
heatY := dotRows - 1 - y
h := d.heat[heatY*dotCols+x]
// Wispy tips: at low heat, only stochastically light the dot
// so the upper edge of the flame has a soft, broken silhouette
// instead of a hard cutoff.
if h < 0.10 {
continue
}
if h < 0.25 && scatterHash(0, y, x, d.frame) > h*4 {
continue
}
// Tier mapping:
// hottest → yellow (mid spectrum tier)
// body → red (high tier)
// tips → red, stippled above
var t int
switch {
case h >= 0.55:
t = 1 // yellow core
default:
t = 2 // red body / tips
}
braille |= brailleBit[dr][dc]
if t > cellTag {
cellTag = t
}
}
}
if cellTag < 0 {
cellTag = 0
}
if cellTag != tag {
flushStyleRun(&sb, &run, tag)
tag = cellTag
}
run.WriteRune(braille)
}
flushStyleRun(&sb, &run, tag)
lines[row] = sb.String()
}
return strings.Join(lines, "\n")
}