neiam
/neiam-devices
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
moire: measure the pattern in units of the screen's shorter side
56cf58f · Gabriel Morell · 2026-09-29 23:17
Message
{commit_body(@commit)}
Files changed
modified
moire/shaders/field.frag
+18
−16
@@ -18,8 +18,9 @@ layout(std140, binding = 0) uniform buf {
| 18 | 18 | float qt_Opacity; |
| 19 | 19 | |
| 20 | 20 | vec2 size; // pixels |
| 21 | − // Positions and lengths below are in units of the height, so every | |
| 22 | − // aspect ratio shows the same pattern at the same apparent scale. | |
| 21 | + // Positions and lengths below are in units of the shorter side, so | |
| 22 | + // every aspect ratio, landscape or portrait, shows the same pattern | |
| 23 | + // at the same apparent scale. The screen spans 0 to size / shorter. | |
| 23 | 24 | vec2 ringA; // centres of the two ring families |
| 24 | 25 | vec2 ringB; |
| 25 | 26 | vec2 hub; // where the pattern is strongest |
@@ -110,13 +111,13 @@ float starLight(vec2 p, float px)
| 110 | 111 | // A band of light sweeping diagonally across the screen during the |
| 111 | 112 | // first 30% of each glint cycle. It lights only the rings and dots, as |
| 112 | 113 | // light catches fine grooves, and is zero between sweeps. |
| 113 | −float glintBand(vec2 p, float aspect) | |
| 114 | +float glintBand(vec2 p, vec2 extent) | |
| 114 | 115 | { |
| 115 | 116 | if (glintPhase >= 0.3) |
| 116 | 117 | return 0.0; |
| 117 | 118 | float s = glintPhase / 0.3; |
| 118 | 119 | vec2 dir = vec2(0.8, -0.6); |
| 119 | − float c0 = 0.0, c1 = aspect * dir.x, c2 = dir.y, c3 = aspect * dir.x + dir.y; | |
| 120 | + float c0 = 0.0, c1 = extent.x * dir.x, c2 = extent.y * dir.y, c3 = c1 + c2; | |
| 120 | 121 | float lo = min(min(c0, c1), min(c2, c3)); |
| 121 | 122 | float hi = max(max(c0, c1), max(c2, c3)); |
| 122 | 123 | const float width = 0.08; |
@@ -134,7 +135,7 @@ float glintBand(vec2 p, float aspect)
| 134 | 135 | // vivid. Returns the discs' blended colour and their combined coverage. |
| 135 | 136 | const int MAX_DISCS = 64; |
| 136 | 137 | |
| 137 | −vec4 bokehLight(vec2 p, float aspect, float px) | |
| 138 | +vec4 bokehLight(vec2 p, vec2 extent, float px) | |
| 138 | 139 | { |
| 139 | 140 | // More discs means smaller ones, so a crowd stays a scatter of |
| 140 | 141 | // lights rather than a haze. |
@@ -149,9 +150,9 @@ vec4 bokehLight(vec2 p, float aspect, float px)
| 149 | 150 | float climb = 1.0 + floor(hash(n + 1.0) * 3.0); // 1-3 laps a cycle |
| 150 | 151 | float sway = 1.0 + floor(hash(n + 2.0) * 2.0); |
| 151 | 152 | float life = fract(hash(n + 3.0) + climb * drift); // 0 at bottom, 1 at top |
| 152 | − vec2 c = vec2(hash(n + 4.0) * aspect | |
| 153 | + vec2 c = vec2(hash(n + 4.0) * extent.x | |
| 153 | 154 | + 0.04 * sin(6.2831853 * (sway * drift + hash(n + 5.0))), |
| 154 | − 1.15 - 1.3 * life); | |
| 155 | + extent.y + 0.15 - (extent.y + 0.3) * life); | |
| 155 | 156 | float radius = (0.035 + 0.075 * hash(n + 6.0)) * scale; |
| 156 | 157 | float d = length(p - c); |
| 157 | 158 | float disc = clamp((radius - d) / (2.5 * px) + 0.5, 0.0, 1.0); |
@@ -177,7 +178,7 @@ vec4 bokehLight(vec2 p, float aspect, float px)
| 177 | 178 | // As with the bokeh, whole-number laps keep the drift wrap seamless. |
| 178 | 179 | const int FLARES = 3; |
| 179 | 180 | |
| 180 | −vec4 foregroundLight(vec2 p, float aspect) | |
| 181 | +vec4 foregroundLight(vec2 p, vec2 extent) | |
| 181 | 182 | { |
| 182 | 183 | vec3 sum = vec3(0.0); |
| 183 | 184 | float total = 0.0; |
@@ -191,9 +192,9 @@ vec4 foregroundLight(vec2 p, float aspect)
| 191 | 192 | float envelope = sin(3.1415927 * window); |
| 192 | 193 | envelope *= envelope; // ease in and out |
| 193 | 194 | float fromRight = step(0.5, hash(n + 2.0)); |
| 194 | − float x = mix(-0.2, aspect + 0.2, hash(n + 3.0)) | |
| 195 | + float x = mix(-0.2, extent.x + 0.2, hash(n + 3.0)) | |
| 195 | 196 | + mix(0.35, -0.35, fromRight) * window; |
| 196 | − vec2 c = vec2(x, mix(0.15, 0.85, hash(n + 4.0)) - 0.12 * window); | |
| 197 | + vec2 c = vec2(x, mix(0.15, 0.85, hash(n + 4.0)) * extent.y - 0.12 * window); | |
| 197 | 198 | float radius = 0.22 + 0.2 * hash(n + 5.0); |
| 198 | 199 | float d = length(p - c); |
| 199 | 200 | // Far out of focus: a wide soft edge and only a hint of a rim. |
@@ -209,9 +210,10 @@ vec4 foregroundLight(vec2 p, float aspect)
| 209 | 210 | |
| 210 | 211 | void main() |
| 211 | 212 | { |
| 212 | − float px = 1.0 / size.y; | |
| 213 | − vec2 p = qt_TexCoord0 * size * px; | |
| 214 | − vec2 centre = vec2(0.5 * size.x * px, 0.5); | |
| 213 | + float px = 1.0 / min(size.x, size.y); | |
| 214 | + vec2 extent = size * px; | |
| 215 | + vec2 p = qt_TexCoord0 * extent; | |
| 216 | + vec2 centre = 0.5 * extent; | |
| 215 | 217 | |
| 216 | 218 | // Ground: base, darkening towards deep at the corners. |
| 217 | 219 | float v = smoothstep(0.35, 1.25, length(p - centre)); |
@@ -220,10 +222,10 @@ void main()
| 220 | 222 | if (stars > 0.0) |
| 221 | 223 | colour = mix(colour, ink.rgb, starLight(p, px) * 0.85 * stars); |
| 222 | 224 | |
| 223 | − float glint = glints > 0.0 ? glintBand(p, size.x * px) * glints : 0.0; | |
| 225 | + float glint = glints > 0.0 ? glintBand(p, extent) * glints : 0.0; | |
| 224 | 226 | |
| 225 | 227 | if (bokeh > 0.0) { |
| 226 | − vec4 light = bokehLight(p, size.x * px, px); | |
| 228 | + vec4 light = bokehLight(p, extent, px); | |
| 227 | 229 | colour = mix(colour, light.rgb, light.a * 0.3 * bokeh); |
| 228 | 230 | } |
| 229 | 231 |
@@ -261,7 +263,7 @@ void main()
| 261 | 263 | |
| 262 | 264 | // In front of everything, so drawn last. |
| 263 | 265 | if (flare > 0.0) { |
| 264 | − vec4 light = foregroundLight(p, size.x * px); | |
| 266 | + vec4 light = foregroundLight(p, extent); | |
| 265 | 267 | colour = mix(colour, light.rgb, light.a * 0.11 * flare); |
| 266 | 268 | } |
| 267 | 269 |
modified
moire/theme/contents/ui/Field.qml
+5
−3
@@ -52,7 +52,9 @@ Item {
| 52 | 52 | // random point in the motion so two screens don't mirror each other. |
| 53 | 53 | property real clock: Math.random() * 10000 |
| 54 | 54 | |
| 55 | − readonly property real aspect: height > 0 ? width / height : 16 / 9 | |
| 55 | + // The screen's size in the shader's units, the shorter side's length. | |
| 56 | + readonly property real extentX: height > 0 ? width / Math.min(width, height) : 16 / 9 | |
| 57 | + readonly property real extentY: width > 0 ? height / Math.min(width, height) : 1 | |
| 56 | 58 | |
| 57 | 59 | // How much of each layer a pattern shows. |
| 58 | 60 | function layers(name) { |
@@ -90,8 +92,8 @@ Item {
| 90 | 92 | // The hub, where the pattern is strongest, wanders around the |
| 91 | 93 | // right of centre; the ring centres circle it at a breathing |
| 92 | 94 | // distance, which walks the moire's beat around the rings. |
| 93 | − property point hub: Qt.point(0.62 * field.aspect + 0.05 * field.sweep(97, 0), | |
| 94 | − 0.46 + 0.04 * field.sweep(131, 0.3)) | |
| 95 | + property point hub: Qt.point(0.62 * field.extentX + 0.05 * field.sweep(97, 0), | |
| 96 | + 0.46 * field.extentY + 0.04 * field.sweep(131, 0.3)) | |
| 95 | 97 | readonly property real spread: 0.03 * (1 + 0.35 * field.sweep(71, 0.1)) |
| 96 | 98 | readonly property real turn: 2 * Math.PI * field.clock / 53 |
| 97 | 99 | property point ringA: Qt.point(hub.x - spread * Math.cos(turn) / 2, |
modified
?
+0
−0
Binary file — not rendered.
Parents: cd4c494