mirror of
https://github.com/elwrcl/copnix.git
synced 2026-10-31 06:18:03 +03:00
ghostty glsl
This commit is contained in:
7 files changed
+567
No files matched your search
@@ -22,3 +22,5 @@
|
|||||||
!*.png
|
!*.png
|
||||||
!*.yml
|
!*.yml
|
||||||
!*.json
|
!*.json
|
||||||
|
!*.glsl
|
||||||
|
!*.py
|
||||||
@@ -28,6 +28,8 @@ in
|
|||||||
imports = [
|
imports = [
|
||||||
config.flake.homeModules.home-base
|
config.flake.homeModules.home-base
|
||||||
config.flake.homeModules.home-ghostty
|
config.flake.homeModules.home-ghostty
|
||||||
|
config.flake.homeModules.home-ghostty-shaders
|
||||||
|
config.flake.homeModules.home-degauss-sound
|
||||||
config.flake.homeModules.home-nushell
|
config.flake.homeModules.home-nushell
|
||||||
config.flake.homeModules.home-nushell-bash-compat
|
config.flake.homeModules.home-nushell-bash-compat
|
||||||
config.flake.homeModules.home-nushell-completions
|
config.flake.homeModules.home-nushell-completions
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
///cursor-smear.glsl
|
||||||
|
|
||||||
|
const float DURATION = 0.45;
|
||||||
|
const float TRAIL_OPACITY = 0.65;
|
||||||
|
void processEdge(vec2 p, vec2 a, vec2 b, inout float minDist, inout float inside) {
|
||||||
|
vec2 edge = b - a;
|
||||||
|
vec2 pa = p - a;
|
||||||
|
float lenSq = dot(edge, edge);
|
||||||
|
float invLenSq = 1.0 / lenSq;
|
||||||
|
|
||||||
|
float t = clamp(dot(pa, edge) * invLenSq, 0.0, 1.0);
|
||||||
|
vec2 diff = pa - edge * t;
|
||||||
|
minDist = min(minDist, dot(diff, diff));
|
||||||
|
|
||||||
|
float crossZ = edge.x * pa.y - edge.y * pa.x;
|
||||||
|
inside = min(inside, step(0.0, crossZ));
|
||||||
|
}
|
||||||
|
|
||||||
|
// signed distance field for hexagon (negative inside, positive outside)
|
||||||
|
float sdHexagon(in vec2 p, in vec2 v0, in vec2 v1, in vec2 v2, in vec2 v3, in vec2 v4, in vec2 v5) {
|
||||||
|
float minDist = 1e20;
|
||||||
|
float inside = 1.0;
|
||||||
|
|
||||||
|
processEdge(p, v0, v1, minDist, inside);
|
||||||
|
processEdge(p, v1, v2, minDist, inside);
|
||||||
|
processEdge(p, v2, v3, minDist, inside);
|
||||||
|
processEdge(p, v3, v4, minDist, inside);
|
||||||
|
processEdge(p, v4, v5, minDist, inside);
|
||||||
|
processEdge(p, v5, v0, minDist, inside);
|
||||||
|
|
||||||
|
float dist = sqrt(max(minDist, 0.0));
|
||||||
|
return mix(dist, -dist, inside);
|
||||||
|
}
|
||||||
|
|
||||||
|
// signed distance field for rectangle (negative inside, positive outside)
|
||||||
|
float sdRectangle(in vec2 p, in vec2 center, in vec2 halfSize) {
|
||||||
|
vec2 d = abs(p - center) - halfSize;
|
||||||
|
return length(max(d, 0.0)) + min(max(d.x, d.y), 0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// represents cursor as a quad with four corners
|
||||||
|
struct Quad {
|
||||||
|
vec2 topLeft;
|
||||||
|
vec2 topRight;
|
||||||
|
vec2 bottomLeft;
|
||||||
|
vec2 bottomRight;
|
||||||
|
};
|
||||||
|
|
||||||
|
// construct quad from top-left position and size
|
||||||
|
Quad getQuad(vec2 pos, vec2 size) {
|
||||||
|
Quad q;
|
||||||
|
q.topLeft = pos;
|
||||||
|
q.topRight = pos + vec2(size.x, 0.0);
|
||||||
|
q.bottomLeft = pos - vec2(0.0, size.y);
|
||||||
|
q.bottomRight = pos + vec2(size.x, -size.y);
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
|
||||||
|
// selecting 3 corners from quad based on movement direction
|
||||||
|
// sel.x: 0=left, 1=right | sel.y: 0=top, 1=bottom
|
||||||
|
void selectTrailCorners(Quad q, vec2 sel, out vec2 p1, out vec2 p2, out vec2 p3) {
|
||||||
|
p1 = mix(mix(q.topRight, q.topLeft, sel.x),
|
||||||
|
mix(q.bottomRight, q.bottomLeft, sel.x),
|
||||||
|
sel.y);
|
||||||
|
|
||||||
|
p2 = mix(mix(q.topLeft, q.bottomLeft, sel.x),
|
||||||
|
mix(q.topRight, q.bottomRight, sel.x),
|
||||||
|
sel.y);
|
||||||
|
p3 = mix(mix(q.bottomRight, q.topRight, sel.x),
|
||||||
|
mix(q.bottomLeft, q.topLeft, sel.x),
|
||||||
|
sel.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// select 4 corners from quad based on movement direction
|
||||||
|
void selectCorners(Quad q, vec2 sel, out vec2 p1, out vec2 p2, out vec2 p3, out vec2 p4) {
|
||||||
|
selectTrailCorners(q, sel, p1, p2, p3);
|
||||||
|
|
||||||
|
p4 = mix(mix(q.bottomLeft, q.bottomRight, sel.x),
|
||||||
|
mix(q.topLeft, q.topRight, sel.x),
|
||||||
|
sel.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
// cubic ease-out function for smooth animation (expects clamped input)
|
||||||
|
float easeClamped(float x) {
|
||||||
|
float t = 1.0 - x;
|
||||||
|
return 1.0 - t * t * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||||
|
vec2 uv = fragCoord / iResolution.xy;
|
||||||
|
vec4 background = texture(iChannel0, uv);
|
||||||
|
fragColor = background;
|
||||||
|
|
||||||
|
// an unfocused split still reports cursor changes, and its frames arrive far
|
||||||
|
// apart, so a trail there is both wrong and jumpy.
|
||||||
|
if (iFocus == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculate animation progress with easing
|
||||||
|
float baseProgress = clamp((iTime - iTimeCursorChange) / DURATION, 0.0, 1.0);
|
||||||
|
|
||||||
|
// skip further work when animation is complete
|
||||||
|
if (baseProgress >= 1.0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// precompute reused values
|
||||||
|
float invResY = 1.0 / iResolution.y;
|
||||||
|
float scale = 2.0 * invResY;
|
||||||
|
float aaWidth = scale;
|
||||||
|
vec2 normOffset = iResolution.xy * invResY;
|
||||||
|
|
||||||
|
// normalize cursor positions and sizes to screen-independent coordinates
|
||||||
|
vec2 currentPos = iCurrentCursor.xy * scale - normOffset;
|
||||||
|
vec2 previousPos = iPreviousCursor.xy * scale - normOffset;
|
||||||
|
vec2 currentSize = iCurrentCursor.zw * scale;
|
||||||
|
vec2 previousSize = iPreviousCursor.zw * scale;
|
||||||
|
|
||||||
|
// determine movement direction and construct cursor quads
|
||||||
|
vec2 deltaPos = currentPos - previousPos;
|
||||||
|
Quad currentCursor = getQuad(currentPos, currentSize);
|
||||||
|
Quad previousCursor = getQuad(previousPos, previousSize);
|
||||||
|
vec2 selector = step(vec2(0.0), deltaPos);
|
||||||
|
|
||||||
|
// select corners based on movement direction
|
||||||
|
vec2 currP1, currP2, currP3, currP4;
|
||||||
|
vec2 prevP1, prevP2, prevP3;
|
||||||
|
selectCorners(currentCursor, selector, currP1, currP2, currP3, currP4);
|
||||||
|
selectTrailCorners(previousCursor, selector, prevP1, prevP2, prevP3);
|
||||||
|
|
||||||
|
float easedProgress = easeClamped(baseProgress);
|
||||||
|
float stretchedProgress = min(baseProgress * 2.0, 1.0);
|
||||||
|
float easedProgressDouble = easeClamped(stretchedProgress);
|
||||||
|
|
||||||
|
// create trailing effect by moving diagonal point slower
|
||||||
|
vec2 trailP1 = mix(prevP1, currP1, easedProgress);
|
||||||
|
vec2 trailP2 = mix(prevP2, currP2, easedProgressDouble);
|
||||||
|
vec2 trailP3 = mix(prevP3, currP3, easedProgressDouble);
|
||||||
|
|
||||||
|
// compute hexagon SDF and convert to alpha with antialiasing
|
||||||
|
vec2 normCoord = fragCoord * scale - normOffset;
|
||||||
|
float sdfHex = sdHexagon(normCoord, trailP1, trailP2, currP2, currP4, currP3, trailP3);
|
||||||
|
float alpha = 1.0 - smoothstep(-aaWidth, aaWidth, sdfHex);
|
||||||
|
|
||||||
|
// computing current cursor SDF
|
||||||
|
vec2 halfCurrentSize = currentSize * 0.5;
|
||||||
|
vec2 currentCenter = currentPos + vec2(halfCurrentSize.x, -halfCurrentSize.y);
|
||||||
|
float sdfCurrentCursor = sdRectangle(normCoord, currentCenter, halfCurrentSize);
|
||||||
|
|
||||||
|
// calculating line length (distance between cursor centers)
|
||||||
|
vec2 previousCenter = previousPos + vec2(previousSize.x * 0.5, -previousSize.y * 0.5);
|
||||||
|
// max() because a cursor change that does not move it (a recolour, say)
|
||||||
|
// gives a zero-length line, and smoothstep with equal edges is undefined.
|
||||||
|
float lineLength = max(distance(currentCenter, previousCenter), 1e-4);
|
||||||
|
|
||||||
|
// cursor already carries the palette accent, so the trail needs no color
|
||||||
|
// of its own and never fights the theme.
|
||||||
|
vec3 trailColor = iCurrentCursorColor.rgb;
|
||||||
|
|
||||||
|
// fade along the length, squared so the tail
|
||||||
|
float distFromCursor = distance(normCoord, currentCenter);
|
||||||
|
float lengthFade = 1.0 - smoothstep(0.0, lineLength, distFromCursor);
|
||||||
|
float timeFade = 1.0 - baseProgress * baseProgress;
|
||||||
|
float strength = TRAIL_OPACITY * lengthFade * lengthFade * timeFade;
|
||||||
|
|
||||||
|
// blend trail color with background
|
||||||
|
vec3 originalColor = fragColor.rgb;
|
||||||
|
fragColor.rgb = mix(fragColor.rgb, trailColor, alpha * strength);
|
||||||
|
|
||||||
|
// remove trail where it overlaps with current cursor
|
||||||
|
fragColor.rgb = mix(fragColor.rgb, originalColor, step(sdfCurrentCursor, 0.0));
|
||||||
|
}
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
degauss-sound "thwoonngg" every time.
|
||||||
|
|
||||||
|
degauss-sound # play it
|
||||||
|
degauss-sound -o out.wav # write a file instead
|
||||||
|
degauss-sound --seed 42 # reproducible, for tuning
|
||||||
|
degauss-sound --duration 1.5 # match your shader's DURATION
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import array
|
||||||
|
import math
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import wave
|
||||||
|
|
||||||
|
SR = 48000
|
||||||
|
TABLE_BITS = 12
|
||||||
|
TABLE_SIZE = 1 << TABLE_BITS
|
||||||
|
TABLE_MASK = TABLE_SIZE - 1
|
||||||
|
SINE = [math.sin(2.0 * math.pi * i / TABLE_SIZE) for i in range(TABLE_SIZE)]
|
||||||
|
|
||||||
|
|
||||||
|
def synth(duration, rng):
|
||||||
|
n = int(SR * duration)
|
||||||
|
out = array.array("d", bytes(8 * n))
|
||||||
|
|
||||||
|
# randomized tube personality
|
||||||
|
f0 = rng.uniform(46.0, 74.0)
|
||||||
|
glide = rng.uniform(3.0, 14.0)
|
||||||
|
h2 = rng.uniform(0.30, 0.60)
|
||||||
|
h3 = rng.uniform(0.12, 0.32)
|
||||||
|
h4 = rng.uniform(0.00, 0.09)
|
||||||
|
h5 = rng.uniform(0.03, 0.15)
|
||||||
|
|
||||||
|
warble_hz = rng.uniform(5.5, 10.5)
|
||||||
|
warble_depth = rng.uniform(0.28, 0.55)
|
||||||
|
wander_hz = rng.uniform(1.3, 3.4)
|
||||||
|
wander_depth = rng.uniform(0.4, 1.1)
|
||||||
|
|
||||||
|
decay = rng.uniform(2.1, 3.4)
|
||||||
|
rattle_amt = rng.uniform(0.025, 0.095)
|
||||||
|
rattle_decay = rng.uniform(6.0, 14.0)
|
||||||
|
thump_f = rng.uniform(33.0, 54.0)
|
||||||
|
thump_amt = rng.uniform(0.30, 0.70)
|
||||||
|
thump_decay = rng.uniform(11.0, 22.0)
|
||||||
|
second_kick = rng.random() < 0.35
|
||||||
|
kick_at = rng.uniform(0.22, 0.45)
|
||||||
|
kick_amt = rng.uniform(0.25, 0.55)
|
||||||
|
|
||||||
|
# phase accumulators
|
||||||
|
scale = TABLE_SIZE / SR
|
||||||
|
ph = 0.0
|
||||||
|
ph_warble = 0.0
|
||||||
|
ph_wander = 0.0
|
||||||
|
ph_thump = 0.0
|
||||||
|
dt = 1.0 / SR
|
||||||
|
inv_dur = 1.0 / duration
|
||||||
|
|
||||||
|
peak = 0.0
|
||||||
|
for i in range(n):
|
||||||
|
t = i * dt
|
||||||
|
k = t * inv_dur
|
||||||
|
|
||||||
|
# coil tone, gliding down
|
||||||
|
f = f0 - glide * k
|
||||||
|
ph += f * scale
|
||||||
|
p = int(ph)
|
||||||
|
tone = SINE[p & TABLE_MASK]
|
||||||
|
tone += h2 * SINE[(p << 1) & TABLE_MASK]
|
||||||
|
tone += h3 * SINE[(p * 3) & TABLE_MASK]
|
||||||
|
tone += h4 * SINE[(p << 2) & TABLE_MASK]
|
||||||
|
tone += h5 * SINE[(p * 5) & TABLE_MASK]
|
||||||
|
|
||||||
|
# warble, itself slowly wandering
|
||||||
|
ph_wander += wander_hz * scale
|
||||||
|
ph_warble += (warble_hz + wander_depth * SINE[int(ph_wander) & TABLE_MASK]) * scale
|
||||||
|
warble = 1.0 - warble_depth + warble_depth * SINE[int(ph_warble) & TABLE_MASK]
|
||||||
|
|
||||||
|
# envelope: near-instant attack, exponential decay
|
||||||
|
env = math.exp(-decay * t) * (1.0 - math.exp(-220.0 * t))
|
||||||
|
if second_kick:
|
||||||
|
d = t - kick_at
|
||||||
|
if d > 0.0:
|
||||||
|
env += kick_amt * math.exp(-decay * 1.6 * d) * (1.0 - math.exp(-160.0 * d))
|
||||||
|
|
||||||
|
# opening clunk
|
||||||
|
ph_thump += thump_f * scale
|
||||||
|
thump = thump_amt * SINE[int(ph_thump) & TABLE_MASK] * math.exp(-thump_decay * t)
|
||||||
|
|
||||||
|
# shadow-mask rattle
|
||||||
|
rattle = rattle_amt * (rng.random() * 2.0 - 1.0) * math.exp(-rattle_decay * t)
|
||||||
|
|
||||||
|
s = tone * warble * env + thump + rattle
|
||||||
|
out[i] = s
|
||||||
|
a = s if s >= 0.0 else -s
|
||||||
|
if a > peak:
|
||||||
|
peak = a
|
||||||
|
|
||||||
|
# normalize, then fade the tail so it never clicks
|
||||||
|
gain = 0.85 / peak if peak > 0.0 else 0.0
|
||||||
|
fade = int(SR * 0.04)
|
||||||
|
pcm = array.array("h", bytes(2 * n))
|
||||||
|
for i in range(n):
|
||||||
|
s = out[i] * gain
|
||||||
|
if i >= n - fade:
|
||||||
|
s *= (n - i) / fade
|
||||||
|
v = int(s * 32767.0)
|
||||||
|
pcm[i] = 32767 if v > 32767 else (-32768 if v < -32768 else v)
|
||||||
|
return pcm
|
||||||
|
|
||||||
|
|
||||||
|
def write_wav(path, pcm):
|
||||||
|
with wave.open(path, "wb") as w:
|
||||||
|
w.setnchannels(1)
|
||||||
|
w.setsampwidth(2)
|
||||||
|
w.setframerate(SR)
|
||||||
|
w.writeframes(pcm.tobytes())
|
||||||
|
|
||||||
|
|
||||||
|
def play(path, volume):
|
||||||
|
for cmd in (["pw-play", "--volume=%s" % volume, path],
|
||||||
|
["paplay", path],
|
||||||
|
["aplay", "-q", path]):
|
||||||
|
try:
|
||||||
|
return subprocess.run(cmd, stderr=subprocess.DEVNULL).returncode
|
||||||
|
except FileNotFoundError:
|
||||||
|
continue
|
||||||
|
print("degauss-sound: no player found (pw-play/paplay/aplay)", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("-o", "--out", help="write a wav here instead of playing")
|
||||||
|
ap.add_argument("-d", "--duration", type=float, default=1.5,
|
||||||
|
help="seconds; match your shader's DURATION (default 1.5)")
|
||||||
|
ap.add_argument("-s", "--seed", type=int, help="reproducible output")
|
||||||
|
ap.add_argument("-v", "--volume", default="0.5", help="pw-play volume")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
rng = random.Random(args.seed)
|
||||||
|
pcm = synth(args.duration, rng)
|
||||||
|
|
||||||
|
if args.out:
|
||||||
|
write_wav(args.out, pcm)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
tmpdir = os.environ.get("XDG_RUNTIME_DIR") or tempfile.gettempdir()
|
||||||
|
fd, path = tempfile.mkstemp(suffix=".wav", dir=tmpdir)
|
||||||
|
os.close(fd)
|
||||||
|
try:
|
||||||
|
write_wav(path, pcm)
|
||||||
|
return play(path, args.volume)
|
||||||
|
finally:
|
||||||
|
os.unlink(path)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
///degauss.glsl
|
||||||
|
const float BOOT_DELAY = 0.45;
|
||||||
|
|
||||||
|
// How hard the coil hits.
|
||||||
|
const float WOBBLE_AMP = 0.055;
|
||||||
|
const float WAVE_FREQ = 9.0;
|
||||||
|
const float WAVE_SPEED = 3.2;
|
||||||
|
const float WAVE_GLIDE = 0.35;
|
||||||
|
const float WARBLE_HZ = 7.0;
|
||||||
|
const float WARBLE_DEPTH = 0.25;
|
||||||
|
const float CHROMA_AMP = 0.016;
|
||||||
|
const float FLASH_AMP = 0.55;
|
||||||
|
const float JITTER_AMP = 0.006;
|
||||||
|
const float SQUEEZE_AMP = 0.075;
|
||||||
|
const float BAND_SWEEPS = 2.0;
|
||||||
|
const bool JUMP_DEGAUSS = false;
|
||||||
|
const float CURSOR_JUMP = 0.60;
|
||||||
|
|
||||||
|
#define TAU 6.28318530718
|
||||||
|
float envelope(float t) {
|
||||||
|
if (t <= 0.0 || t >= DURATION) return 0.0;
|
||||||
|
float k = t / DURATION;
|
||||||
|
float attack = 1.0 - exp(-45.0 * t);
|
||||||
|
float ring = exp(-2.3 * k);
|
||||||
|
float tail = 1.0 - k * k * k * k;
|
||||||
|
return attack * ring * tail;
|
||||||
|
}
|
||||||
|
float coilPhase(float t) {
|
||||||
|
float k = clamp(t / DURATION, 0.0, 1.0);
|
||||||
|
return WAVE_SPEED * t * (1.0 - 0.5 * WAVE_GLIDE * k);
|
||||||
|
}
|
||||||
|
|
||||||
|
float hash11(float x) {
|
||||||
|
return fract(sin(x * 78.233) * 43758.5453);
|
||||||
|
}
|
||||||
|
vec4 sampleTerm(vec2 uv) {
|
||||||
|
vec2 halfTexel = 0.5 / iResolution.xy;
|
||||||
|
return texture(iChannel0, clamp(uv, halfTexel, 1.0 - halfTexel));
|
||||||
|
}
|
||||||
|
|
||||||
|
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
||||||
|
vec2 uv = fragCoord / iResolution.xy;
|
||||||
|
|
||||||
|
float amp = 0.0;
|
||||||
|
float t = 0.0;
|
||||||
|
|
||||||
|
float boot = iTime - BOOT_DELAY;
|
||||||
|
float bootAmp = envelope(boot);
|
||||||
|
if (bootAmp > amp) { amp = bootAmp; t = boot; }
|
||||||
|
|
||||||
|
if (JUMP_DEGAUSS) {
|
||||||
|
float jump = abs(iCurrentCursor.y - iPreviousCursor.y);
|
||||||
|
float elapsed = iTime - iTimeCursorChange;
|
||||||
|
if (jump > CURSOR_JUMP * iResolution.y) {
|
||||||
|
float e = envelope(elapsed);
|
||||||
|
if (e > amp) { amp = e; t = elapsed; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amp <= 0.0001) {
|
||||||
|
fragColor = texture(iChannel0, uv);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// coil's own tremble, so the picture pulses along with the warble in the
|
||||||
|
// sound instead of sliding smoothly under it.
|
||||||
|
amp *= 1.0 - WARBLE_DEPTH + WARBLE_DEPTH * sin(t * TAU * WARBLE_HZ);
|
||||||
|
|
||||||
|
float ph = coilPhase(t);
|
||||||
|
|
||||||
|
// magnetic distortion
|
||||||
|
vec2 p = uv - 0.5;
|
||||||
|
|
||||||
|
float wave = sin(p.y * WAVE_FREQ * TAU - ph * TAU);
|
||||||
|
float wave2 = sin(p.y * WAVE_FREQ * 2.3 * TAU + ph * 0.62 * TAU);
|
||||||
|
float warp = wave * 0.7 + wave2 * 0.3;
|
||||||
|
|
||||||
|
// vertical breathing
|
||||||
|
p.y /= 1.0 + amp * SQUEEZE_AMP * sin(t * TAU * 3.0);
|
||||||
|
p *= 1.0 - amp * 0.04 * sin(t * TAU * 2.0);
|
||||||
|
|
||||||
|
// per-scanline jitter
|
||||||
|
float line = floor(uv.y * iResolution.y);
|
||||||
|
float jitter = (hash11(line + floor(t * 90.0)) - 0.5) * 2.0;
|
||||||
|
|
||||||
|
p.x += amp * (WOBBLE_AMP * warp + JITTER_AMP * jitter);
|
||||||
|
|
||||||
|
vec2 duv = p + 0.5;
|
||||||
|
|
||||||
|
// misconvergence
|
||||||
|
float ca = amp * CHROMA_AMP * (0.5 + abs(warp));
|
||||||
|
vec4 cr = sampleTerm(duv + vec2(ca, 0.0));
|
||||||
|
vec4 cg = sampleTerm(duv);
|
||||||
|
vec4 cb = sampleTerm(duv - vec2(ca, 0.0));
|
||||||
|
|
||||||
|
vec4 col = vec4(cr.r, cg.g, cb.b, cg.a);
|
||||||
|
|
||||||
|
// coil pulse sweeping down the tube
|
||||||
|
float bandY = 1.0 - fract(t / DURATION * BAND_SWEEPS);
|
||||||
|
float band = exp(-pow((uv.y - bandY) * 9.0, 2.0));
|
||||||
|
|
||||||
|
// background-opacity to solid for the length of the flash.
|
||||||
|
float glow = amp * FLASH_AMP * (0.25 + 0.75 * band);
|
||||||
|
col.rgb += glow * (iForegroundColor * 0.22 + col.rgb * 0.9);
|
||||||
|
|
||||||
|
fragColor = col;
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
flake.homeModules.home-degauss-sound =
|
||||||
|
{ pkgs, lib, ... }:
|
||||||
|
let
|
||||||
|
duration = "1.5";
|
||||||
|
variants = 8;
|
||||||
|
generatorSource = lib.concatStringsSep "\n" (
|
||||||
|
lib.drop 1 (lib.splitString "\n" (builtins.readFile ./degauss-sound.py))
|
||||||
|
);
|
||||||
|
|
||||||
|
generator = pkgs.writers.writePython3Bin "degauss-gen" {
|
||||||
|
flakeIgnore = [
|
||||||
|
"E501"
|
||||||
|
"W291"
|
||||||
|
];
|
||||||
|
} generatorSource;
|
||||||
|
|
||||||
|
samples = pkgs.runCommandLocal "degauss-samples" { nativeBuildInputs = [ generator ]; } ''
|
||||||
|
mkdir -p $out
|
||||||
|
for i in $(seq 1 ${toString variants}); do
|
||||||
|
degauss-gen --seed "$i" --duration ${duration} --out "$out/degauss-$i.wav"
|
||||||
|
done
|
||||||
|
'';
|
||||||
|
|
||||||
|
degaussSound = pkgs.writeShellApplication {
|
||||||
|
name = "degauss-sound";
|
||||||
|
runtimeInputs = [ pkgs.pipewire ];
|
||||||
|
text = ''
|
||||||
|
n=$(( (RANDOM % ${toString variants}) + 1 ))
|
||||||
|
exec pw-play --volume="''${DEGAUSS_VOLUME:-0.35}" "${samples}/degauss-$n.wav"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
home.packages = [ degaussSound ];
|
||||||
|
programs.nushell.extraConfig = lib.mkBefore ''
|
||||||
|
if $nu.is-interactive and ($env.TERM_PROGRAM? == "ghostty") and ($env.ZELLIJ? | is-empty) {
|
||||||
|
job spawn { ^${lib.getExe degaussSound} } | ignore
|
||||||
|
}
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{ ... }:
|
||||||
|
{
|
||||||
|
flake.homeModules.home-ghostty-shaders =
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
let
|
||||||
|
preamble = pkgs.writeText "ghostty-shader-preamble.glsl" ''
|
||||||
|
#version 330 core
|
||||||
|
#define CURSORSTYLE_BLOCK 0
|
||||||
|
#define CURSORSTYLE_BLOCK_HOLLOW 1
|
||||||
|
#define CURSORSTYLE_BAR 2
|
||||||
|
#define CURSORSTYLE_UNDERLINE 3
|
||||||
|
#define CURSORSTYLE_LOCK 4
|
||||||
|
uniform vec3 iResolution;
|
||||||
|
uniform float iTime;
|
||||||
|
uniform float iTimeDelta;
|
||||||
|
uniform float iFrameRate;
|
||||||
|
uniform int iFrame;
|
||||||
|
uniform float iChannelTime[4];
|
||||||
|
uniform vec3 iChannelResolution[4];
|
||||||
|
uniform vec4 iMouse;
|
||||||
|
uniform vec4 iDate;
|
||||||
|
uniform float iSampleRate;
|
||||||
|
uniform sampler2D iChannel0;
|
||||||
|
uniform vec4 iCurrentCursor;
|
||||||
|
uniform vec4 iPreviousCursor;
|
||||||
|
uniform vec4 iCurrentCursorColor;
|
||||||
|
uniform vec4 iPreviousCursorColor;
|
||||||
|
uniform vec4 iCurrentCursorStyle;
|
||||||
|
uniform vec4 iPreviousCursorStyle;
|
||||||
|
uniform vec4 iCursorVisible;
|
||||||
|
uniform float iTimeCursorChange;
|
||||||
|
uniform float iTimeFocus;
|
||||||
|
uniform int iFocus;
|
||||||
|
uniform vec3 iPalette[256];
|
||||||
|
uniform vec3 iBackgroundColor;
|
||||||
|
uniform vec3 iForegroundColor;
|
||||||
|
uniform vec3 iCursorColor;
|
||||||
|
uniform vec3 iCursorText;
|
||||||
|
uniform vec3 iSelectionBackgroundColor;
|
||||||
|
uniform vec3 iSelectionForegroundColor;
|
||||||
|
out vec4 _ghosttyFragColor;
|
||||||
|
#line 1
|
||||||
|
'';
|
||||||
|
|
||||||
|
entryPoint = pkgs.writeText "ghostty-shader-entry.glsl" ''
|
||||||
|
void main() { mainImage(_ghosttyFragColor, gl_FragCoord.xy); }
|
||||||
|
'';
|
||||||
|
|
||||||
|
compiled =
|
||||||
|
name: src:
|
||||||
|
pkgs.runCommandLocal "ghostty-shader-${name}.glsl"
|
||||||
|
{
|
||||||
|
nativeBuildInputs = [ pkgs.glslang ];
|
||||||
|
}
|
||||||
|
''
|
||||||
|
cat ${preamble} ${src} ${entryPoint} > wrapped.frag
|
||||||
|
glslangValidator -S frag wrapped.frag
|
||||||
|
cp ${src} $out
|
||||||
|
'';
|
||||||
|
|
||||||
|
shaderDir = "${config.xdg.configHome}/ghostty/shaders";
|
||||||
|
in
|
||||||
|
{
|
||||||
|
xdg.configFile."ghostty/shaders/cursor-smear.glsl".source =
|
||||||
|
compiled "cursor-smear" ./cursor-smear.glsl;
|
||||||
|
xdg.configFile."ghostty/shaders/degauss.glsl".source = compiled "degauss" ./degauss.glsl;
|
||||||
|
|
||||||
|
programs.ghostty.settings = {
|
||||||
|
custom-shader = [
|
||||||
|
"${shaderDir}/cursor-smear.glsl"
|
||||||
|
"${shaderDir}/degauss.glsl"
|
||||||
|
];
|
||||||
|
custom-shader-animation = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in new issue
Block a user