neiam
/neiam-devices
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
moire: optional large foreground bokeh drifting past now and then
8f734ab · Gabriel Morell · 2026-09-29 21:17
Files changed
modified
README.md
+3
−1
@@ -57,7 +57,9 @@ that draws its patterns live: the moire, a halftone field, both, or
| 57 | 57 | bokeh (soft out-of-focus discs drifting upward and fading in and out |
| 58 | 58 | as they rise, 4 to 64 of them, each its own shade from the primary |
| 59 | 59 | towards the accent and from muted to vivid). It can also fade back and |
| 60 | −forth between any two of those every few minutes. | |
| 60 | +forth between any two of those every few minutes. Optionally, with any | |
| 61 | +pattern, a few large defocused discs drift past in front now and then, | |
| 62 | +like a light passing close to the lens. | |
| 61 | 63 | Everything drifts slowly and never visibly repeats. |
| 62 | 64 | |
| 63 | 65 | Its colours come from the waxx themes (dark, light, her, afterdark, |
modified
moire/shaders/field.frag
+44
−0
@@ -31,6 +31,7 @@ layout(std140, binding = 0) uniform buf {
| 31 | 31 | float wave; // halftone wave phase, radians, kept in [0, 2pi) |
| 32 | 32 | float bokeh; // 0-1, how much of the bokeh to show |
| 33 | 33 | float lights; // how many bokeh discs, up to MAX_DISCS |
| 34 | + float flare; // 0-1, how much of the large foreground discs to show | |
| 34 | 35 | float drift; // bokeh phase, 0-1 over its whole cycle |
| 35 | 36 | |
| 36 | 37 | vec4 base; // the ground |
@@ -105,6 +106,43 @@ vec4 bokehLight(vec2 p, float aspect, float px)
| 105 | 106 | return vec4(total > 0.0 ? sum / total : vec3(0.0), min(total, 1.0)); |
| 106 | 107 | } |
| 107 | 108 | |
| 109 | +// A few large, very defocused discs in front of everything, like a | |
| 110 | +// light passing close to the lens. Each is visible for about a third | |
| 111 | +// of its cycle, fading in and out as it slides across, and absent the | |
| 112 | +// rest of the time, so they come by every so often rather than always. | |
| 113 | +// As with the bokeh, whole-number laps keep the drift wrap seamless. | |
| 114 | +const int FLARES = 3; | |
| 115 | + | |
| 116 | +vec4 foregroundLight(vec2 p, float aspect) | |
| 117 | +{ | |
| 118 | + vec3 sum = vec3(0.0); | |
| 119 | + float total = 0.0; | |
| 120 | + for (int i = 0; i < FLARES; i++) { | |
| 121 | + float n = float(i) + 40.0; | |
| 122 | + float laps = 1.0 + floor(hash(n) * 2.0); // 1-2 visits a cycle | |
| 123 | + float life = fract(hash(n + 1.0) + laps * drift); | |
| 124 | + float window = life / 0.35; // visible for the first 35% | |
| 125 | + if (window >= 1.0) | |
| 126 | + continue; | |
| 127 | + float envelope = sin(3.1415927 * window); | |
| 128 | + envelope *= envelope; // ease in and out | |
| 129 | + float fromRight = step(0.5, hash(n + 2.0)); | |
| 130 | + float x = mix(-0.2, aspect + 0.2, hash(n + 3.0)) | |
| 131 | + + mix(0.35, -0.35, fromRight) * window; | |
| 132 | + vec2 c = vec2(x, mix(0.15, 0.85, hash(n + 4.0)) - 0.12 * window); | |
| 133 | + float radius = 0.22 + 0.2 * hash(n + 5.0); | |
| 134 | + float d = length(p - c); | |
| 135 | + // Far out of focus: a wide soft edge and only a hint of a rim. | |
| 136 | + float disc = 1.0 - smoothstep(radius * 0.72, radius, d); | |
| 137 | + float rim = smoothstep(radius * 0.6, radius * 0.88, d) * disc; | |
| 138 | + float strength = (disc * 0.8 + rim * 0.2) * envelope; | |
| 139 | + | |
| 140 | + sum += mix(tone.rgb, ink.rgb, 0.6 + 0.4 * hash(n + 6.0)) * strength; | |
| 141 | + total += strength; | |
| 142 | + } | |
| 143 | + return vec4(total > 0.0 ? sum / total : vec3(0.0), min(total, 1.0)); | |
| 144 | +} | |
| 145 | + | |
| 108 | 146 | void main() |
| 109 | 147 | { |
| 110 | 148 | float px = 1.0 / size.y; |
@@ -147,5 +185,11 @@ void main()
| 147 | 185 | colour = mix(colour, ink.rgb, light * 0.6 * fade(p) * moire); |
| 148 | 186 | } |
| 149 | 187 | |
| 188 | + // In front of everything, so drawn last. | |
| 189 | + if (flare > 0.0) { | |
| 190 | + vec4 light = foregroundLight(p, size.x * px); | |
| 191 | + colour = mix(colour, light.rgb, light.a * 0.11 * flare); | |
| 192 | + } | |
| 193 | + | |
| 150 | 194 | fragColor = vec4(colour, 1.0) * qt_Opacity; |
| 151 | 195 | } |
modified
moire/theme/contents/config/main.xml
+4
−0
@@ -31,6 +31,10 @@
| 31 | 31 | <label>Frames per second while animating</label> |
| 32 | 32 | <default>30</default> |
| 33 | 33 | </entry> |
| 34 | + <entry name="ForegroundBokeh" type="Bool"> | |
| 35 | + <label>Now and then, large defocused lights drifting past in front</label> | |
| 36 | + <default>false</default> | |
| 37 | + </entry> | |
| 34 | 38 | <entry name="Wordmark" type="Bool"> |
| 35 | 39 | <label>Write NEIAM in the bottom right corner</label> |
| 36 | 40 | <default>false</default> |
modified
moire/theme/contents/ui/Field.qml
+4
−0
@@ -17,6 +17,8 @@ Item {
| 17 | 17 | property string fadeFrom: "moire" |
| 18 | 18 | property string fadeTo: "halftone" |
| 19 | 19 | property int bokehLights: 14 |
| 20 | + // Now and then, a few large defocused discs drifting past in front. | |
| 21 | + property bool foreground: false | |
| 20 | 22 | // Multiplies how fast everything drifts; 0 holds the picture still. |
| 21 | 23 | property real speed: 1 |
| 22 | 24 | property int frameRate: 30 |
@@ -94,6 +96,8 @@ Item {
| 94 | 96 | property real halftone: field.amount("halftone") |
| 95 | 97 | property real bokeh: field.amount("bokeh") |
| 96 | 98 | property real lights: field.bokehLights |
| 99 | + property real flare: field.foreground ? 1 : 0 | |
| 100 | + Behavior on flare { NumberAnimation { duration: 1500 } } | |
| 97 | 101 | Behavior on moire { enabled: field.pattern !== "alternate"; NumberAnimation { duration: 1500 } } |
| 98 | 102 | Behavior on halftone { enabled: field.pattern !== "alternate"; NumberAnimation { duration: 1500 } } |
| 99 | 103 | Behavior on bokeh { enabled: field.pattern !== "alternate"; NumberAnimation { duration: 1500 } } |
modified
moire/theme/contents/ui/config.qml
+8
−0
@@ -18,6 +18,7 @@ Kirigami.FormLayout {
| 18 | 18 | property double cfg_Speed |
| 19 | 19 | property int cfg_FrameRate |
| 20 | 20 | property bool cfg_Wordmark |
| 21 | + property bool cfg_ForegroundBokeh | |
| 21 | 22 | property string cfg_ThemeMode |
| 22 | 23 | property string cfg_Theme |
| 23 | 24 | property string cfg_NightTheme |
@@ -127,6 +128,13 @@ Kirigami.FormLayout {
| 127 | 128 | enabled: root.cfg_Speed > 0 |
| 128 | 129 | } |
| 129 | 130 | |
| 131 | + QQC2.CheckBox { | |
| 132 | + Kirigami.FormData.label: "Foreground:" | |
| 133 | + text: "Large lights drifting past now and then" | |
| 134 | + checked: root.cfg_ForegroundBokeh | |
| 135 | + onToggled: root.cfg_ForegroundBokeh = checked | |
| 136 | + } | |
| 137 | + | |
| 130 | 138 | QQC2.CheckBox { |
| 131 | 139 | Kirigami.FormData.label: "Wordmark:" |
| 132 | 140 | text: "Write NEIAM in the corner" |
modified
?
+0
−0
Binary file — not rendered.
modified
moire/theme/contents/ui/main.qml
+1
−0
@@ -32,6 +32,7 @@ WallpaperItem {
| 32 | 32 | fadeFrom: root.cfg.FadeFrom |
| 33 | 33 | fadeTo: root.cfg.FadeTo |
| 34 | 34 | bokehLights: root.cfg.BokehLights |
| 35 | + foreground: root.cfg.ForegroundBokeh | |
| 35 | 36 | speed: root.cfg.Speed |
| 36 | 37 | frameRate: root.cfg.FrameRate |
| 37 | 38 | wordmark: root.cfg.Wordmark |
Parents: bd286fb