neiam
/neiam-devices
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
moire: bokeh count and per-disc colour; fade between any two patterns
bd286fb · Gabriel Morell · 2026-09-29 21:11
Message
{commit_body(@commit)}
Files changed
modified
README.md
+5
−3
@@ -53,9 +53,11 @@ adds to its size.
| 53 | 53 | |
| 54 | 54 | `plasma-wallpaper-neiam-moire` is a Plasma wallpaper plugin ("neiam |
| 55 | 55 | moire" under *Wallpaper type*, for the desktop or the lock screen) |
| 56 | −that draws the moire live, with a halftone field, both, or fading | |
| 57 | −between the two, or bokeh: soft out-of-focus discs in the accent and | |
| 58 | −primary colours, drifting upward and fading in and out as they rise. | |
| 56 | +that draws its patterns live: the moire, a halftone field, both, or | |
| 57 | +bokeh (soft out-of-focus discs drifting upward and fading in and out | |
| 58 | +as they rise, 4 to 64 of them, each its own shade from the primary | |
| 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. | |
| 59 | 61 | Everything drifts slowly and never visibly repeats. |
| 60 | 62 | |
| 61 | 63 | Its colours come from the waxx themes (dark, light, her, afterdark, |
modified
moire/shaders/field.frag
+25
−11
@@ -30,6 +30,7 @@ layout(std140, binding = 0) uniform buf {
| 30 | 30 | float angle; // halftone grid rotation, radians |
| 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 | + float lights; // how many bokeh discs, up to MAX_DISCS | |
| 33 | 34 | float drift; // bokeh phase, 0-1 over its whole cycle |
| 34 | 35 | |
| 35 | 36 | vec4 base; // the ground |
@@ -62,13 +63,23 @@ float hash(float n)
| 62 | 63 | // multiple of drift, so when drift wraps from 1 back to 0 every disc |
| 63 | 64 | // is exactly where it started; each fades in rising from the bottom |
| 64 | 65 | // and out before the top, so none ever appears or vanishes abruptly. |
| 65 | −const int DISCS = 14; | |
| 66 | +// | |
| 67 | +// Every disc has its own colour: somewhere from the primary to the | |
| 68 | +// accent, weighted towards the accent, and somewhere from muted to | |
| 69 | +// vivid. Returns the discs' blended colour and their combined coverage. | |
| 70 | +const int MAX_DISCS = 64; | |
| 66 | 71 | |
| 67 | −float bokehLight(vec2 p, float aspect, float px, out float tint) | |
| 72 | +vec4 bokehLight(vec2 p, float aspect, float px) | |
| 68 | 73 | { |
| 74 | + // More discs means smaller ones, so a crowd stays a scatter of | |
| 75 | + // lights rather than a haze. | |
| 76 | + float scale = clamp(pow(14.0 / max(lights, 1.0), 0.3), 0.6, 1.2); | |
| 77 | + | |
| 78 | + vec3 sum = vec3(0.0); | |
| 69 | 79 | float total = 0.0; |
| 70 | − tint = 0.0; | |
| 71 | − for (int i = 0; i < DISCS; i++) { | |
| 80 | + for (int i = 0; i < MAX_DISCS; i++) { | |
| 81 | + if (float(i) >= lights) | |
| 82 | + break; | |
| 72 | 83 | float n = float(i); |
| 73 | 84 | float climb = 1.0 + floor(hash(n + 1.0) * 3.0); // 1-3 laps a cycle |
| 74 | 85 | float sway = 1.0 + floor(hash(n + 2.0) * 2.0); |
@@ -76,18 +87,22 @@ float bokehLight(vec2 p, float aspect, float px, out float tint)
| 76 | 87 | vec2 c = vec2(hash(n + 4.0) * aspect |
| 77 | 88 | + 0.04 * sin(6.2831853 * (sway * drift + hash(n + 5.0))), |
| 78 | 89 | 1.15 - 1.3 * life); |
| 79 | − float radius = 0.035 + 0.075 * hash(n + 6.0); | |
| 90 | + float radius = (0.035 + 0.075 * hash(n + 6.0)) * scale; | |
| 80 | 91 | float d = length(p - c); |
| 81 | 92 | float disc = clamp((radius - d) / (2.5 * px) + 0.5, 0.0, 1.0); |
| 82 | 93 | // A lens renders a defocused point with a slightly brighter rim. |
| 83 | 94 | float rim = smoothstep(radius * 0.7, radius, d) * disc; |
| 84 | 95 | float shape = disc * 0.65 + rim * 0.35; |
| 85 | 96 | float strength = shape * sin(3.1415927 * life) * (0.45 + 0.55 * hash(n + 7.0)); |
| 97 | + | |
| 98 | + vec3 colour = mix(tone.rgb, ink.rgb, sqrt(hash(n + 8.0))); | |
| 99 | + float grey = dot(colour, vec3(0.2126, 0.7152, 0.0722)); | |
| 100 | + colour = clamp(mix(vec3(grey), colour, 0.3 + 0.9 * hash(n + 9.0)), 0.0, 1.0); | |
| 101 | + | |
| 102 | + sum += colour * strength; | |
| 86 | 103 | total += strength; |
| 87 | − tint += strength * step(0.5, hash(n + 8.0)); | |
| 88 | 104 | } |
| 89 | − tint = total > 0.0 ? tint / total : 0.0; | |
| 90 | − return min(total, 1.0); | |
| 105 | + return vec4(total > 0.0 ? sum / total : vec3(0.0), min(total, 1.0)); | |
| 91 | 106 | } |
| 92 | 107 | |
| 93 | 108 | void main() |
@@ -101,9 +116,8 @@ void main()
| 101 | 116 | vec3 colour = mix(base.rgb, deep.rgb, v); |
| 102 | 117 | |
| 103 | 118 | if (bokeh > 0.0) { |
| 104 | − float tint; | |
| 105 | − float light = bokehLight(p, size.x * px, px, tint); | |
| 106 | − colour = mix(colour, mix(ink.rgb, tone.rgb, tint), light * 0.3 * bokeh); | |
| 119 | + vec4 light = bokehLight(p, size.x * px, px); | |
| 120 | + colour = mix(colour, light.rgb, light.a * 0.3 * bokeh); | |
| 107 | 121 | } |
| 108 | 122 | |
| 109 | 123 | // Halftone: dots on a rotated grid, each sized by the fade and a |
modified
moire/theme/contents/config/main.xml
+14
−1
@@ -7,9 +7,22 @@
| 7 | 7 | |
| 8 | 8 | <group name="General"> |
| 9 | 9 | <entry name="Pattern" type="String"> |
| 10 | − <label>moire, halftone, both, alternate, or bokeh</label> | |
| 10 | + <label>moire, halftone, both, bokeh, or alternate</label> | |
| 11 | 11 | <default>both</default> |
| 12 | 12 | </entry> |
| 13 | + <entry name="FadeFrom" type="String"> | |
| 14 | + <label>With Pattern alternate, the pattern faded from: moire, halftone, both, or bokeh</label> | |
| 15 | + <default>moire</default> | |
| 16 | + </entry> | |
| 17 | + <entry name="FadeTo" type="String"> | |
| 18 | + <default>halftone</default> | |
| 19 | + </entry> | |
| 20 | + <entry name="BokehLights" type="Int"> | |
| 21 | + <label>How many bokeh discs</label> | |
| 22 | + <default>14</default> | |
| 23 | + <min>4</min> | |
| 24 | + <max>64</max> | |
| 25 | + </entry> | |
| 13 | 26 | <entry name="Speed" type="Double"> |
| 14 | 27 | <label>How fast the pattern drifts; 0 holds it still</label> |
| 15 | 28 | <default>1</default> |
modified
moire/theme/contents/ui/Field.qml
+29
−10
@@ -10,9 +10,13 @@ import QtQuick 2.15
| 10 | 10 | Item { |
| 11 | 11 | id: field |
| 12 | 12 | |
| 13 | − // "moire", "halftone", "both" (moire over halftone), "alternate" | |
| 14 | − // (fading between those two every few minutes), or "bokeh". | |
| 13 | + // "moire", "halftone", "both" (moire over halftone), "bokeh", or | |
| 14 | + // "alternate": fading between fadeFrom and fadeTo, which are any of | |
| 15 | + // the others, every few minutes. | |
| 15 | 16 | property string pattern: "both" |
| 17 | + property string fadeFrom: "moire" | |
| 18 | + property string fadeTo: "halftone" | |
| 19 | + property int bokehLights: 14 | |
| 16 | 20 | // Multiplies how fast everything drifts; 0 holds the picture still. |
| 17 | 21 | property real speed: 1 |
| 18 | 22 | property int frameRate: 30 |
@@ -36,6 +40,22 @@ Item {
| 36 | 40 | |
| 37 | 41 | readonly property real aspect: height > 0 ? width / height : 16 / 9 |
| 38 | 42 | |
| 43 | + // How much of each layer a pattern shows. | |
| 44 | + function layers(name) { | |
| 45 | + return { | |
| 46 | + moire: name === "moire" || name === "both" ? 1 : 0, | |
| 47 | + halftone: name === "halftone" || name === "both" ? 1 : 0, | |
| 48 | + bokeh: name === "bokeh" ? 1 : 0, | |
| 49 | + } | |
| 50 | + } | |
| 51 | + | |
| 52 | + function amount(layer) { | |
| 53 | + if (pattern !== "alternate") | |
| 54 | + return layers(pattern)[layer] | |
| 55 | + var t = 0.5 + 0.5 * sweep(360, 0) | |
| 56 | + return layers(fadeFrom)[layer] * (1 - t) + layers(fadeTo)[layer] * t | |
| 57 | + } | |
| 58 | + | |
| 39 | 59 | function sweep(period, phase) { |
| 40 | 60 | return Math.sin(2 * Math.PI * (clock / period + phase)) |
| 41 | 61 | } |
@@ -68,16 +88,15 @@ Item {
| 68 | 88 | // screen three times in it. |
| 69 | 89 | property real drift: (field.clock / 600) % 1 |
| 70 | 90 | |
| 71 | − // Amounts ease so a pattern change fades rather than cuts. | |
| 72 | − readonly property real alternate: 0.5 + 0.5 * field.sweep(360, 0) | |
| 73 | − property real moire: field.pattern === "alternate" ? alternate | |
| 74 | − : field.pattern === "moire" || field.pattern === "both" ? 1 : 0 | |
| 75 | − property real halftone: field.pattern === "alternate" ? 1 - alternate | |
| 76 | − : field.pattern === "halftone" || field.pattern === "both" ? 1 : 0 | |
| 77 | − property real bokeh: field.pattern === "bokeh" ? 1 : 0 | |
| 91 | + // Amounts ease so a pattern change fades rather than cuts; while | |
| 92 | + // alternating they are already continuous, so easing would lag. | |
| 93 | + property real moire: field.amount("moire") | |
| 94 | + property real halftone: field.amount("halftone") | |
| 95 | + property real bokeh: field.amount("bokeh") | |
| 96 | + property real lights: field.bokehLights | |
| 78 | 97 | Behavior on moire { enabled: field.pattern !== "alternate"; NumberAnimation { duration: 1500 } } |
| 79 | 98 | Behavior on halftone { enabled: field.pattern !== "alternate"; NumberAnimation { duration: 1500 } } |
| 80 | − Behavior on bokeh { NumberAnimation { duration: 1500 } } | |
| 99 | + Behavior on bokeh { enabled: field.pattern !== "alternate"; NumberAnimation { duration: 1500 } } | |
| 81 | 100 | |
| 82 | 101 | property color base: field.base |
| 83 | 102 | property color deep: field.deep |
modified
moire/theme/contents/ui/config.qml
+49
−8
@@ -12,6 +12,9 @@ Kirigami.FormLayout {
| 12 | 12 | property alias formLayout: root |
| 13 | 13 | |
| 14 | 14 | property string cfg_Pattern |
| 15 | + property string cfg_FadeFrom | |
| 16 | + property string cfg_FadeTo | |
| 17 | + property int cfg_BokehLights | |
| 15 | 18 | property double cfg_Speed |
| 16 | 19 | property int cfg_FrameRate |
| 17 | 20 | property bool cfg_Wordmark |
@@ -27,13 +30,29 @@ Kirigami.FormLayout {
| 27 | 30 | property string cfg_SunriseTime |
| 28 | 31 | property string cfg_SunsetTime |
| 29 | 32 | |
| 30 | − readonly property var patterns: [ | |
| 33 | + // The patterns a fade can go between; Pattern adds the fade itself. | |
| 34 | + readonly property var layers: [ | |
| 31 | 35 | { value: "both", text: "Moire and halftone" }, |
| 32 | 36 | { value: "moire", text: "Moire" }, |
| 33 | 37 | { value: "halftone", text: "Halftone" }, |
| 34 | − { value: "alternate", text: "Fade between moire and halftone" }, | |
| 35 | 38 | { value: "bokeh", text: "Bokeh" }, |
| 36 | 39 | ] |
| 40 | + readonly property var patterns: layers.concat([{ value: "alternate", text: "Fade between two…" }]) | |
| 41 | + | |
| 42 | + readonly property bool fading: cfg_Pattern === "alternate" | |
| 43 | + readonly property bool showsBokeh: cfg_Pattern === "bokeh" | |
| 44 | + || (fading && (cfg_FadeFrom === "bokeh" || cfg_FadeTo === "bokeh")) | |
| 45 | + | |
| 46 | + component PatternBox: QQC2.ComboBox { | |
| 47 | + property var choices | |
| 48 | + property string value | |
| 49 | + signal picked(string value) | |
| 50 | + implicitContentWidthPolicy: QQC2.ComboBox.WidestText | |
| 51 | + model: choices | |
| 52 | + textRole: "text" | |
| 53 | + currentIndex: Math.max(0, choices.findIndex(p => p.value === value)) | |
| 54 | + onActivated: index => picked(choices[index].value) | |
| 55 | + } | |
| 37 | 56 | |
| 38 | 57 | component ThemeBox: QQC2.ComboBox { |
| 39 | 58 | property string value |
@@ -58,13 +77,35 @@ Kirigami.FormLayout {
| 58 | 77 | |
| 59 | 78 | // ------------------------------------------------------------ pattern |
| 60 | 79 | |
| 61 | − QQC2.ComboBox { | |
| 80 | + PatternBox { | |
| 62 | 81 | Kirigami.FormData.label: "Pattern:" |
| 63 | − implicitContentWidthPolicy: QQC2.ComboBox.WidestText | |
| 64 | − model: root.patterns | |
| 65 | − textRole: "text" | |
| 66 | − currentIndex: Math.max(0, root.patterns.findIndex(p => p.value === root.cfg_Pattern)) | |
| 67 | − onActivated: index => root.cfg_Pattern = root.patterns[index].value | |
| 82 | + choices: root.patterns | |
| 83 | + value: root.cfg_Pattern | |
| 84 | + onPicked: v => root.cfg_Pattern = v | |
| 85 | + } | |
| 86 | + | |
| 87 | + PatternBox { | |
| 88 | + Kirigami.FormData.label: "From:" | |
| 89 | + visible: root.fading | |
| 90 | + choices: root.layers | |
| 91 | + value: root.cfg_FadeFrom | |
| 92 | + onPicked: v => root.cfg_FadeFrom = v | |
| 93 | + } | |
| 94 | + | |
| 95 | + PatternBox { | |
| 96 | + Kirigami.FormData.label: "To:" | |
| 97 | + visible: root.fading | |
| 98 | + choices: root.layers | |
| 99 | + value: root.cfg_FadeTo | |
| 100 | + onPicked: v => root.cfg_FadeTo = v | |
| 101 | + } | |
| 102 | + | |
| 103 | + QQC2.SpinBox { | |
| 104 | + Kirigami.FormData.label: "Bokeh lights:" | |
| 105 | + visible: root.showsBokeh | |
| 106 | + from: 4; to: 64 | |
| 107 | + value: root.cfg_BokehLights | |
| 108 | + onValueModified: root.cfg_BokehLights = value | |
| 68 | 109 | } |
| 69 | 110 | |
| 70 | 111 | RowLayout { |
modified
?
+0
−0
Binary file — not rendered.
modified
moire/theme/contents/ui/main.qml
+3
−0
@@ -29,6 +29,9 @@ WallpaperItem {
| 29 | 29 | Field { |
| 30 | 30 | anchors.fill: parent |
| 31 | 31 | pattern: root.cfg.Pattern |
| 32 | + fadeFrom: root.cfg.FadeFrom | |
| 33 | + fadeTo: root.cfg.FadeTo | |
| 34 | + bokehLights: root.cfg.BokehLights | |
| 32 | 35 | speed: root.cfg.Speed |
| 33 | 36 | frameRate: root.cfg.FrameRate |
| 34 | 37 | wordmark: root.cfg.Wordmark |
Parents: c3df760