moire: optional bokeh drifting behind the pattern

ca16122 · Gabriel Morell · 2026-09-29 20:55

13 files +76 -2

Files changed

modified README.md
+4 −0
@@ -65,6 +65,10 @@ latitude and longitude. Between phases it blends by the sun's
65 65 elevation, turning the hue so the transitions pass through the purples
66 66 of a real twilight rather than grey.
67 67
68 +Bokeh (off by default) adds soft out-of-focus discs in the accent and
69 +primary colours, drifting upward behind the pattern and fading in and
70 +out as they rise.
71 +
68 72 A checkbox writes "NEIAM" in the bottom right corner, in the theme's
69 73 accent colour, in B612 Mono Bold at 12% of the screen height; the package pulls
70 74 in the font (`fonts-b612` / `firstyear-b612-fonts`).
modified moire/shaders/field.frag
+51 −2
@@ -1,5 +1,6 @@
1 −// The moire wallpaper's whole picture: ground, halftone dots and moire
2 −// rings, each fading out into the ground away from a drifting hub.
1 +// The moire wallpaper's whole picture: ground, bokeh, halftone dots and
2 +// moire rings, the patterns fading out into the ground away from a
3 +// drifting hub.
3 4 //
4 5 // Everything that moves is computed in Field.qml, in double precision,
5 6 // and arrives here as positions and phases; the shader holds no clock,
@@ -28,6 +29,8 @@ layout(std140, binding = 0) uniform buf {
28 29 float cell; // halftone grid spacing
29 30 float angle; // halftone grid rotation, radians
30 31 float wave; // halftone wave phase, radians, kept in [0, 2pi)
32 + float bokeh; // 0-1, how much of the bokeh to show
33 + float drift; // bokeh phase, 0-1 over its whole cycle
31 34
32 35 vec4 base; // the ground
33 36 vec4 deep; // the ground at the edges
@@ -49,6 +52,45 @@ float rings(vec2 p, vec2 centre, float px)
49 52 return clamp(1.0 - (d - halfWidth) / px, 0.0, 1.0);
50 53 }
51 54
55 +// A repeatable pseudo-random number in [0, 1) per disc and purpose.
56 +float hash(float n)
57 +{
58 + return fract(sin(n * 127.1 + 311.7) * 43758.5453);
59 +}
60 +
61 +// Out-of-focus lights drifting upward behind the pattern, as if the
62 +// moire were in focus in front of them. Each disc moves at a whole
63 +// multiple of drift, so when drift wraps from 1 back to 0 every disc
64 +// is exactly where it started; each fades in rising from the bottom
65 +// and out before the top, so none ever appears or vanishes abruptly.
66 +const int DISCS = 14;
67 +
68 +float bokehLight(vec2 p, float aspect, float px, out float tint)
69 +{
70 + float total = 0.0;
71 + tint = 0.0;
72 + for (int i = 0; i < DISCS; i++) {
73 + float n = float(i);
74 + float climb = 1.0 + floor(hash(n + 1.0) * 3.0); // 1-3 laps a cycle
75 + float sway = 1.0 + floor(hash(n + 2.0) * 2.0);
76 + float life = fract(hash(n + 3.0) + climb * drift); // 0 at bottom, 1 at top
77 + vec2 c = vec2(hash(n + 4.0) * aspect
78 + + 0.04 * sin(6.2831853 * (sway * drift + hash(n + 5.0))),
79 + 1.15 - 1.3 * life);
80 + float radius = 0.035 + 0.075 * hash(n + 6.0);
81 + float d = length(p - c);
82 + float disc = clamp((radius - d) / (2.5 * px) + 0.5, 0.0, 1.0);
83 + // A lens renders a defocused point with a slightly brighter rim.
84 + float rim = smoothstep(radius * 0.7, radius, d) * disc;
85 + float shape = disc * 0.65 + rim * 0.35;
86 + float strength = shape * sin(3.1415927 * life) * (0.45 + 0.55 * hash(n + 7.0));
87 + total += strength;
88 + tint += strength * step(0.5, hash(n + 8.0));
89 + }
90 + tint = total > 0.0 ? tint / total : 0.0;
91 + return min(total, 1.0);
92 +}
93 +
52 94 void main()
53 95 {
54 96 float px = 1.0 / size.y;
@@ -59,6 +101,13 @@ void main()
59 101 float v = smoothstep(0.35, 1.25, length(p - centre));
60 102 vec3 colour = mix(base.rgb, deep.rgb, v);
61 103
104 + // Bokeh sits behind everything else: it is the out-of-focus scene.
105 + if (bokeh > 0.0) {
106 + float tint;
107 + float light = bokehLight(p, size.x * px, px, tint);
108 + colour = mix(colour, mix(ink.rgb, tone.rgb, tint), light * 0.14 * bokeh);
109 + }
110 +
62 111 // Halftone: dots on a rotated grid, each sized by the fade and a
63 112 // travelling wave sampled at its cell's centre, so a dot is round
64 113 // and uniform rather than cut by the field. Area, not radius, is
modified moire/theme/contents/config/main.xml
+4 −0
@@ -18,6 +18,10 @@
18 18 <label>Frames per second while animating</label>
19 19 <default>30</default>
20 20 </entry>
21 + <entry name="Bokeh" type="Bool">
22 + <label>Soft out-of-focus lights drifting up behind the pattern</label>
23 + <default>false</default>
24 + </entry>
21 25 <entry name="Wordmark" type="Bool">
22 26 <label>Write NEIAM in the bottom right corner</label>
23 27 <default>false</default>
modified moire/theme/contents/ui/Field.qml
+8 −0
@@ -18,6 +18,8 @@ Item {
18 18 property int frameRate: 30
19 19 // "NEIAM" in the bottom right, in the ink colour.
20 20 property bool wordmark: false
21 + // Soft out-of-focus discs drifting up behind the pattern.
22 + property bool bokeh: false
21 23
22 24 property color base: "#661e1e"
23 25 property color deep: "#470005"
@@ -64,6 +66,12 @@ Item {
64 66 property real angle: 0.26 + 0.03 * field.sweep(211, 0.2)
65 67 property real wave: (2 * Math.PI * field.clock / 16) % (2 * Math.PI)
66 68
69 + // One bokeh cycle is ten minutes; the fastest disc climbs the
70 + // screen three times in it.
71 + property real drift: (field.clock / 600) % 1
72 + property real bokeh: field.bokeh ? 1 : 0
73 + Behavior on bokeh { NumberAnimation { duration: 1500 } }
74 +
67 75 // Amounts ease so a pattern change fades rather than cuts.
68 76 readonly property real alternate: 0.5 + 0.5 * field.sweep(360, 0)
69 77 property real moire: field.pattern === "halftone" ? 0
modified moire/theme/contents/ui/config.qml
+8 −0
@@ -15,6 +15,7 @@ Kirigami.FormLayout {
15 15 property double cfg_Speed
16 16 property int cfg_FrameRate
17 17 property bool cfg_Wordmark
18 + property bool cfg_Bokeh
18 19 property string cfg_ThemeMode
19 20 property string cfg_Theme
20 21 property string cfg_NightTheme
@@ -85,6 +86,13 @@ Kirigami.FormLayout {
85 86 enabled: root.cfg_Speed > 0
86 87 }
87 88
89 + QQC2.CheckBox {
90 + Kirigami.FormData.label: "Bokeh:"
91 + text: "Drifting out-of-focus lights"
92 + checked: root.cfg_Bokeh
93 + onToggled: root.cfg_Bokeh = checked
94 + }
95 +
88 96 QQC2.CheckBox {
89 97 Kirigami.FormData.label: "Wordmark:"
90 98 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 speed: root.cfg.Speed
33 33 frameRate: root.cfg.FrameRate
34 34 wordmark: root.cfg.Wordmark
35 + bokeh: root.cfg.Bokeh
35 36 base: root.scheme.base
36 37 deep: root.scheme.deep
37 38 ink: root.scheme.ink

Parents: eb8ebcc