-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpostprocess.go
More file actions
295 lines (264 loc) · 7.86 KB
/
postprocess.go
File metadata and controls
295 lines (264 loc) · 7.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
package glyph
import (
"math"
"sync"
"time"
)
// resolveColor16 updates Color16 cells in the buffer to use the detected
// terminal palette RGB values. Called once before effects run so all
// colour math operates on the terminal's actual colours.
func resolveColor16(buf *Buffer, w, h int) {
for y := range h {
base := y * buf.width
for x := range w {
c := &buf.cells[base+x]
if c.Style.FG.Mode == Color16 {
rgb := basic16RGB[c.Style.FG.Index&0xF]
c.Style.FG.R, c.Style.FG.G, c.Style.FG.B = rgb[0], rgb[1], rgb[2]
}
if c.Style.BG.Mode == Color16 {
rgb := basic16RGB[c.Style.BG.Index&0xF]
c.Style.BG.R, c.Style.BG.G, c.Style.BG.B = rgb[0], rgb[1], rgb[2]
}
}
}
}
// Effect transforms the buffer after rendering, before flush.
// Like a GPU shader pass. Receives the full cell buffer and frame context.
// Mutate cells in-place. Chain multiple passes for layered effects.
type Effect interface {
Apply(buf *Buffer, ctx PostContext)
}
// effectCompilable is implemented by effects that have dynamic properties (e.g. animated strength).
// The template compiler calls this during ScreenEffectNode compilation to wire tween evaluators.
// Returns a new Effect with dynamic pointers wired in (effects are value types).
type effectCompilable interface {
compileEffect(t *Template) Effect
}
// funcEffect adapts a bare function to the Effect interface.
// Returned by EachCell and used internally by blend/quantize wrappers.
type funcEffect func(*Buffer, PostContext)
func (f funcEffect) Apply(buf *Buffer, ctx PostContext) { f(buf, ctx) }
// ScreenEffectNode is a declarative node that registers one or more full-screen
// post-processing effects. Place it anywhere in the view tree. It takes zero
// layout space and applies to the entire screen. Works with If() for reactive
// toggling. Accepts multiple effects in a single node.
type ScreenEffectNode struct {
Effects []Effect
}
// ScreenEffect creates a declarative full-screen post-processing node.
// Accepts multiple effects — they apply in order, left to right.
func ScreenEffect(pp ...Effect) ScreenEffectNode {
return ScreenEffectNode{Effects: pp}
}
// PostContext provides frame metadata to post-processing passes.
type PostContext struct {
Width int
Height int
Frame uint64
Delta time.Duration // time since last frame
Time time.Duration // total elapsed since first render
// terminal's default FG/BG detected via OSC 10/11 at startup.
// Mode == ColorRGB when detected, ColorDefault when unknown.
DefaultFG Color
DefaultBG Color
}
// EachCell wraps a per-cell transform into a Effect.
// The fragment shader equivalent. You define the per-cell logic,
// iteration is handled for you. Splits work across four quadrants
// using a persistent worker pool. Zero allocations in the hot path.
func EachCell(fn func(x, y int, cell Cell, ctx PostContext) Cell) Effect {
return funcEffect(func(buf *Buffer, ctx PostContext) {
qp := getCellPool()
midX, midY := ctx.Width/2, ctx.Height/2
// set shared work (safe: written before wakeup send, read after recv)
qp.fn = fn
qp.buf = buf
qp.ctx = ctx
qp.quads = [4][4]int{
{0, midX, 0, midY},
{midX, ctx.Width, 0, midY},
{0, midX, midY, ctx.Height},
{midX, ctx.Width, midY, ctx.Height},
}
for i := range 3 {
qp.wakeup[i] <- struct{}{}
}
// run 4th quadrant on caller
q := qp.quads[3]
for y := q[2]; y < q[3]; y++ {
base := y * buf.width
for x := q[0]; x < q[1]; x++ {
idx := base + x
buf.cells[idx] = fn(x, y, buf.cells[idx], ctx)
}
}
for i := range 3 {
<-qp.done[i]
}
})
}
type cellPool struct {
fn func(x, y int, cell Cell, ctx PostContext) Cell
buf *Buffer
ctx PostContext
quads [4][4]int
wakeup [3]chan struct{}
done [3]chan struct{}
}
var cellPoolInstance struct {
once sync.Once
pool *cellPool
}
func getCellPool() *cellPool {
cellPoolInstance.once.Do(func() {
p := &cellPool{}
for i := range 3 {
p.wakeup[i] = make(chan struct{}, 1)
p.done[i] = make(chan struct{}, 1)
go func(idx int) {
for range p.wakeup[idx] {
q := p.quads[idx]
buf, fn, ctx := p.buf, p.fn, p.ctx
for y := q[2]; y < q[3]; y++ {
base := y * buf.width
for x := q[0]; x < q[1]; x++ {
i := base + x
buf.cells[i] = fn(x, y, buf.cells[i], ctx)
}
}
p.done[idx] <- struct{}{}
}
}(i)
}
cellPoolInstance.pool = p
})
return cellPoolInstance.pool
}
// ---------------------------------------------------------------------------
// Blend modes
// ---------------------------------------------------------------------------
// BlendMode controls how two colours are combined during post-processing.
// Use with WithBlend to wrap any Effect effect.
type BlendMode int
const (
BlendNormal BlendMode = iota
BlendMultiply // darkens: a * b / 255
BlendScreen // lightens: 255 - (255-a)(255-b)/255
BlendOverlay // multiply if dark, screen if light
BlendAdd // clipped addition
BlendSoftLight // gentle contrast
BlendColorDodge // dramatic brighten
BlendColorBurn // dramatic darken
)
// BlendColor combines two RGB colours using the specified blend mode.
func BlendColor(base, top Color, mode BlendMode) Color {
if base.Mode == ColorDefault || top.Mode == ColorDefault {
return top
}
return RGB(
blendChannel(base.R, top.R, mode),
blendChannel(base.G, top.G, mode),
blendChannel(base.B, top.B, mode),
)
}
func blendChannel(a, b uint8, mode BlendMode) uint8 {
fa, fb := float64(a)/255, float64(b)/255
var r float64
switch mode {
case BlendMultiply:
r = fa * fb
case BlendScreen:
r = 1 - (1-fa)*(1-fb)
case BlendOverlay:
if fa < 0.5 {
r = 2 * fa * fb
} else {
r = 1 - 2*(1-fa)*(1-fb)
}
case BlendAdd:
r = fa + fb
case BlendSoftLight:
if fb < 0.5 {
r = fa - (1-2*fb)*fa*(1-fa)
} else {
r = fa + (2*fb-1)*(softLightG(fa)-fa)
}
case BlendColorDodge:
if fb >= 1 {
r = 1
} else {
r = fa / (1 - fb)
}
case BlendColorBurn:
if fb <= 0 {
r = 0
} else {
r = 1 - (1-fa)/fb
}
default:
r = fb
}
if r < 0 {
r = 0
} else if r > 1 {
r = 1
}
return uint8(r * 255)
}
func softLightG(a float64) float64 {
if a <= 0.25 {
return ((16*a-12)*a + 4) * a
}
return math.Sqrt(a)
}
// blendEffect wraps an inner Effect with a blend mode. Snapshots the
// buffer before the effect runs, then blends the output with the original.
type blendEffect struct {
mode BlendMode
inner Effect
}
func (b blendEffect) Apply(buf *Buffer, ctx PostContext) {
w, h := ctx.Width, ctx.Height
// snapshot original FG+BG
type colorPair struct{ fg, bg Color }
snap := make([]colorPair, w*h)
for y := range h {
bufBase := y * buf.width
snapBase := y * w
for x := range w {
c := buf.cells[bufBase+x]
snap[snapBase+x] = colorPair{c.Style.FG, c.Style.BG}
}
}
// run the effect
b.inner.Apply(buf, ctx)
// blend result with snapshot
for y := range h {
bufBase := y * buf.width
snapBase := y * w
for x := range w {
c := &buf.cells[bufBase+x]
orig := snap[snapBase+x]
c.Style.FG = BlendColor(orig.fg, c.Style.FG, b.mode)
c.Style.BG = BlendColor(orig.bg, c.Style.BG, b.mode)
}
}
}
// WithBlend wraps any Effect with a blend mode. Snapshots the buffer
// before the effect runs, then blends the effect's output with the original
// using the specified mode. Works with any effect, plasma through multiply,
// fire through screen, etc.
func WithBlend(mode BlendMode, pp Effect) Effect {
return blendEffect{mode: mode, inner: pp}
}
// WithQuantize wraps a Effect with output quantization.
// Runs pp first, then snaps all resulting RGB values to the nearest multiple of step.
// Use step=32 to reduce bytes/frame by ~40-50% with acceptable banding at typical terminal sizes.
func WithQuantize(step uint8, pp Effect) Effect {
q := SEQuantize(step)
return funcEffect(func(buf *Buffer, ctx PostContext) {
pp.Apply(buf, ctx)
q.Apply(buf, ctx)
})
}