1/** 2 * CitrusOS theme transition â CRT scanlines + chromatic glitch (in-panel; taskbar stable). 3 * Visual patterns based on https://deloughry.co.uk/field-notes/building-glitch-effects-with-css/ 4 */ 5(function (global) { 6 'use strict'; 7 8 var TOTAL_MS = 820; 9 var SWAP_MS = 410; 10 var VEIL_ID = 'citrusos-theme-glitch-veil'; 11 var ROOT_CLASS = 'citrusos-theme-transition'; 12 var HOST_CLASS = 'citrusos-theme-glitch-host'; 13 var OVERLAY_CLASS = 'citrusos-theme-glitch-overlay'; 14 var BLOCK_CLASS = 'citrusos-glitch-block'; 15 var BLOCK_COUNT = 6; 16 var running = false; 17 var activeTimers = []; 18 19 var LAYER_BASE = { 20 desktop: 0, 21 widgets: 95, 22 windows: 190 23 }; 24 25 function prefersReducedMotion() { 26 try { 27 return global.matchMedia && global.matchMedia('(prefers-reduced-motion: reduce)').matches; 28 } catch (e) { 29 return false; 30 } 31 } 32 33 function clearTimers() { 34 for (var i = 0; i < activeTimers.length; i++) { 35 global.clearTimeout(activeTimers[i]); 36 } 37 activeTimers = []; 38 } 39 40 function schedule(fn, ms) { 41 var id = global.setTimeout(fn, ms); 42 activeTimers.push(id); 43 return id; 44 } 45 46 function ensureVeil() { 47 var veil = document.getElementById(VEIL_ID); 48 if (veil) return veil; 49 50 veil = document.createElement('div'); 51 veil.id = VEIL_ID; 52 veil.className = 'citrusos-theme-glitch-veil'; 53 veil.setAttribute('aria-hidden', 'true'); 54 document.body.appendChild(veil); 55 return veil; 56 } 57 58 function createOverlay(options) { 59 options = options || {}; 60 var overlay = document.createElement('div'); 61 overlay.className = OVERLAY_CLASS; 62 overlay.setAttribute('aria-hidden', 'true'); 63 64 if (options.desktop) { 65 overlay.classList.add(OVERLAY_CLASS + '--desktop'); 66 } else { 67 for (var b = 0; b < BLOCK_COUNT; b++) { 68 var block = document.createElement('span'); 69 block.className = BLOCK_CLASS + ' ' + BLOCK_CLASS + '--' + (b + 1); 70 overlay.appendChild(block); 71 } 72 } 73 74 return overlay; 75 } 76 77 function mountGlitchHost(host, delayMs, bucket, options) { 78 if (!host) return; 79 80 host.classList.add(HOST_CLASS); 81 if (options && options.desktop) { 82 host.classList.add(HOST_CLASS + '--desktop'); 83 } 84 host.style.setProperty('--citrusos-theme-glitch-delay', delayMs + 'ms'); 85 86 var overlay = createOverlay(options); 87 host.appendChild(overlay); 88 89 bucket.push({ 90 host: host, 91 overlay: overlay 92 }); 93 } 94 95 function mountDesktopGlitches(bucket) { 96 mountGlitchHost( 97 document.getElementById('citrusos-desktop'), 98 LAYER_BASE.desktop, 99 bucket, 100 { desktop: true } 101 ); 102 } 103 104 function mountWidgetGlitches(bucket) { 105 var widgets = document.querySelectorAll('#citrusos-widgets .citrus-widget__body'); 106 for (var i = 0; i < widgets.length; i++) { 107 mountGlitchHost(widgets[i], LAYER_BASE.widgets + i * 36, bucket); 108 } 109 } 110 111 function mountWindowGlitches(bucket) { 112 var bodies = document.querySelectorAll('#citrusos-windows .citrus-window__body'); 113 for (var i = 0; i < bodies.length; i++) { 114 mountGlitchHost(bodies[i], LAYER_BASE.windows + i * 42, bucket); 115 } 116 117 var fullscreen = document.querySelector('#citrusos-fullscreen:not([hidden]) .citrus-fullscreen__content'); 118 if (fullscreen) { 119 mountGlitchHost(fullscreen, LAYER_BASE.windows + bodies.length * 42, bucket); 120 } 121 } 122 123 function collectGlitchMounts() { 124 var mounted = []; 125 mountDesktopGlitches(mounted); 126 mountWidgetGlitches(mounted); 127 mountWindowGlitches(mounted); 128 return mounted; 129 } 130 131 function clearGlitchMounts(mounted) { 132 for (var i = 0; i < mounted.length; i++) { 133 var entry = mounted[i]; 134 if (!entry || !entry.host) continue; 135 136 entry.host.classList.remove(HOST_CLASS); 137 entry.host.classList.remove(HOST_CLASS + '--desktop'); 138 entry.host.style.removeProperty('--citrusos-theme-glitch-delay'); 139 140 if (entry.overlay && entry.overlay.parentNode) { 141 entry.overlay.parentNode.removeChild(entry.overlay); 142 } 143 } 144 } 145 146 function finishTransition(root, veil, mounted) { 147 if (veil) veil.classList.remove('is-active'); 148 clearGlitchMounts(mounted); 149 if (root) root.classList.remove(ROOT_CLASS); 150 running = false; 151 clearTimers(); 152 } 153 154 function play(theme, applyFn) { 155 if (typeof applyFn !== 'function') return; 156 157 var body = document.body; 158 if (!body || !body.classList.contains('page-citrusos')) { 159 applyFn(); 160 return; 161 } 162 163 if (prefersReducedMotion() || running) { 164 applyFn(); 165 return; 166 } 167 168 running = true; 169 clearTimers(); 170 171 var root = document.documentElement; 172 var veil = ensureVeil(); 173 var mounted = collectGlitchMounts(); 174 175 root.classList.add(ROOT_CLASS); 176 veil.classList.add('is-active'); 177 178 schedule(function () { 179 try { 180 applyFn(); 181 } catch (e) { 182 finishTransition(root, veil, mounted); 183 throw e; 184 } 185 }, SWAP_MS); 186 187 schedule(function () { 188 finishTransition(root, veil, mounted); 189 }, TOTAL_MS); 190 } 191 192 global.CitrusThemeTransition = { 193 play: play, 194 TOTAL_MS: TOTAL_MS, 195 SWAP_MS: SWAP_MS 196 }; 197})(typeof window !== 'undefined' ? window : this);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.