Wijzigen voor document Tekenveld
Laatst gewijzigd door Jeeves de Butler op 23-10-2026 02:42
Van versie 9.1
gewijzigd door Jeeves de Butler
op 19-10-2026 01:24
op 19-10-2026 01:24
Opmerking bij wijziging:
Er is geen opmerking bij deze versie
Naar versie 1.1
gewijzigd door Jeeves de Butler
op 16-10-2026 12:29
op 16-10-2026 12:29
Opmerking bij wijziging:
Er is geen opmerking bij deze versie
Samenvatting
-
Paginaeigenschappen (1 gewijzigd, 0 toegevoegd, 0 verwijderd)
-
Objecten (0 gewijzigd, 0 toegevoegd, 5 verwijderd)
Details
- Paginaeigenschappen
-
- Inhoud
-
... ... @@ -1,4 +1,4 @@ 1 -Tekenveld op de startpagina van de Kennisbank: een bijna onzichtbaar veld van letters en cijfers achter de inhoud, met een muisspoor, hartslagringen, af en toe een rodeECG-lijn en een smiley bij een klik op de lege achtergrond.Vijf snelle klikken op de lege achtergrond geven een explosie; daarna komt het veld in een minuut terug.Het is hetzelfde veld als in Ember, in een rustige variant: het vervaagt naar het midden en ligt stil na een minuut zonder muis.1 +Tekenveld op de startpagina van de Kennisbank: een bijna onzichtbaar veld van letters en cijfers achter de inhoud, met een muisspoor, hartslagringen, af en toe een ECG-lijn en een smiley bij een klik op de lege achtergrond. Het is hetzelfde veld als in Ember, in een rustige variant: het vervaagt naar het midden en ligt stil na een minuut zonder muis. 2 2 3 3 Het object //JavaScriptExtension// op deze pagina is de engine van Ember plus een korte opstart; het object //StyleSheetExtension// zet het canvas achter de inhoud. Beide staan op //onDemand// en worden alleen geladen door [[Startpagina>>doc:Wardenburg.Code.Startpagina]]. 4 4
- XWiki.JavaScriptExtension[0]
-
- cache
-
... ... @@ -1,1 +1,0 @@ 1 -long - code
-
... ... @@ -1,1391 +1,0 @@ 1 -/* Tekenveld voor de Kennisbank. Gemaakt met maak-tekenveld.js uit de skill xwiki-wardenburg-admin. 2 - Engine: Ember src/lib/glyphField.js, commit 83ea536. Niet hier wijzigen: pas de bron aan en maak opnieuw. */ 3 -(function () { 4 -"use strict"; 5 -// /src/lib/glyphField.js 6 - 7 -/* Het tekenveld (Home, het Dashboard, de lijstpagina's, inloggen en de foutpagina's; welke pagina's 8 - het krijgen staat bij de route, zie src/components/RouteBackground.jsx): een raster van willekeurige letters en cijfers in een monospace 9 - letter, bijna onzichtbaar, met langzaam drijvende dichtere wolken. Waar de muis langs 10 - gaat, worden de tekens donkerder, kleuren ze naar het merkrood en wisselen ze snel; dat 11 - spoor dooft in een paar seconden weer uit. Daaronder klopt een rustige hartslag: twee 12 - hartvormige ringen die vanuit de muis uitdijen en uitdoven. Af en toe schiet er een rode 13 - hartmonitorlijn over het scherm, en een klik op de achtergrond laat een smiley opspringen. 14 - 15 - Bewust zonder React: de engine tekent op een canvas en leest het thema uit de CSS-tokens 16 - (--text en --brand-red), zodat hij licht en donker vanzelf volgt. 17 - 18 - Zuinig met CPU en GPU, want het veld staat open zolang de pagina openstaat: 19 - - de tekens staan eenmalig in een atlas en worden per cel gekopieerd (drawImage) in 20 - plaats van steeds opnieuw gezet (fillText); 21 - - het rustende veld staat op een eigen laag die maar vier keer per seconde opnieuw wordt 22 - getekend; per frame gaat die laag in één keer over het canvas; 23 - - per frame worden alleen de cellen onder het spoor en de hartslag bijgewerkt, binnen de 24 - kleinste rechthoek waarin nog iets ligt; 25 - - op een verborgen tabblad tekent het veld niet, en bij prefers-reduced-motion staat er 26 - één stilstaand beeld zonder spoor. */ 27 - 28 -const GLYPHS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; 29 - 30 -/* Het thema bepaalt welke tekens het veld gebruikt. Nu alleen de standaardset; een seizoensset 31 - (sneeuwvlokken rond kerst, paaseitjes rond Pasen) komt hier later bij als eigen thema, zonder dat 32 - een pagina iets hoeft te weten. Elk teken krijgt één cel; hooguit 256 tekens (Uint8Array). */ 33 -const GLYPH_THEMES = Object.freeze({ 34 - standaard: Object.freeze({ label: "Standaard", glyphs: Object.freeze(Array.from(GLYPHS)) }), 35 -}); 36 - 37 -const DEFAULT_GLYPH_THEME = "standaard"; 38 - 39 -/* Intensiteit per pagina. "vol" is het veld van Home zoals het is; "lijst" heeft in rust minder 40 - tekens (de wolken liggen hoger, zodat er minder cellen boven de drempel komen), maar elk teken is 41 - even duidelijk en het muisspoor, de hartslag, de hartslaglijn, de smileys en de explosie blijven 42 - precies gelijk. Gemeten: in rust staan er op "lijst" ongeveer 70% van de tekens van "vol". */ 43 -const GLYPH_INTENSITIES = Object.freeze({ 44 - vol: Object.freeze({}), 45 - lijst: Object.freeze({ ambientLift: -0.05 }), 46 -}); 47 - 48 -const DEFAULT_GLYPH_INTENSITY = "vol"; 49 - 50 -/* De opties voor createGlyphField bij een intensiteit en een thema. Een onbekende naam is een 51 - programmeerfout; die wordt gemeld en valt terug op de standaard, zodat de pagina niet stukgaat. */ 52 -function glyphFieldSettings({ intensity = DEFAULT_GLYPH_INTENSITY, theme = DEFAULT_GLYPH_THEME } = {}) { 53 - let intensityOptions = GLYPH_INTENSITIES[intensity]; 54 - if (!intensityOptions) { 55 - console.error(`tekenveld: onbekende intensiteit "${intensity}"; ${DEFAULT_GLYPH_INTENSITY} gebruikt`); 56 - intensityOptions = GLYPH_INTENSITIES[DEFAULT_GLYPH_INTENSITY]; 57 - } 58 - let themeOptions = GLYPH_THEMES[theme]; 59 - if (!themeOptions) { 60 - console.error(`tekenveld: onbekend thema "${theme}"; ${DEFAULT_GLYPH_THEME} gebruikt`); 61 - themeOptions = GLYPH_THEMES[DEFAULT_GLYPH_THEME]; 62 - } 63 - return { ...intensityOptions, glyphs: themeOptions.glyphs }; 64 -} 65 - 66 -const DEFAULTS = { 67 - // De tekens van het veld; zie GLYPH_THEMES. 68 - glyphs: GLYPH_THEMES.standaard.glyphs, 69 - fontSize: 12, 70 - lineHeight: 15, 71 - fontFamily: 'ui-monospace, "Cascadia Mono", "SF Mono", Consolas, "Liberation Mono", monospace', 72 - // Zichtbaarheid van de wolken in het rustende veld; donker oogt sneller sterker. 73 - ambientMax: { light: 0.1, dark: 0.075 }, 74 - // Hoe hoog de wolken liggen: lager betekent minder tekens in rust (zie GLYPH_INTENSITIES). 75 - ambientLift: 0.05, 76 - // Zichtbaarheid onder het spoor van de muis. 77 - trailNeutral: { light: 0.38, dark: 0.3 }, 78 - trailAccent: { light: 0.4, dark: 0.45 }, 79 - brushRadius: 70, 80 - trailSeconds: 2.2, 81 - ambientIntervalMs: 250, 82 - maxFps: 30, 83 - // Bepaalt of een klik op de achtergrond viel (en dus een smiley geeft); zonder deze 84 - // functie telt elke klik. 85 - isBackground: null, 86 - // Geeft de rechthoeken (getBoundingClientRect) van de inhoud; de hartslaglijn kiest bij 87 - // voorkeur een strook waar die niet staan. 88 - obstacles: null, 89 - // Hartslag: elke slag twee hartvormige ringen vlak na elkaar (lub-dub) die vanaf de muis 90 - // uitdijen en onderweg uitdoven. In rust een slag per heartPeriod; tijdens bewegen een 91 - // slag per heartSpacing pixels, maar niet vaker dan per heartMinGap seconden. 92 - heartPeriod: 4, 93 - heartDubDelay: 0.2, 94 - // De tweede slag (dub) is een zachte naklank van de eerste, geen tweede volle ring. 95 - heartDubWeight: 0.3, 96 - heartSpacing: 420, 97 - heartMinGap: 1.4, 98 - heartMaxRings: 12, 99 - // Hartslaglijn: af en toe trekt er op een willekeurige hoogte een rode lijn in de vorm van 100 - // een hartmonitor van links naar rechts over het scherm; het spoor dooft vanzelf uit. 101 - ecgEverySeconds: [10, 24], 102 - ecgFirstSeconds: [3, 7], 103 - ecgCrossSeconds: 0.7, 104 - ecgBeatWidth: 165, 105 - ecgAmplitude: 105, 106 - ecgRadius: 10, 107 - // De hartslaglijn en de smileys hebben een eigen, kort spoor; ze moeten flitsen, niet 108 - // blijven hangen zoals het spoor van de muis. 109 - streakSeconds: 0.3, 110 - streakNeutral: { light: 0.1, dark: 0.08 }, 111 - streakAccent: { light: 1, dark: 1 }, 112 - // Smileys: een klik op de achtergrond laat er een opspringen; spammen mag. 113 - smileyRadius: 145, 114 - smileySeconds: 1.5, 115 - smileyMax: 40, 116 - // Variatie in de hartslaglijn; een op de zes lijnen is wild. 117 - ecgWildChance: 0.17, 118 - // Vijf snelle klikken: een atoomexplosie. De letters vallen van het scherm; na 15 s mogen 119 - // de smileys weer, na 30 s komt de hartslaglijn terug, na 45 s de hartslag en het 120 - // muisspoor, na 60 s het hele veld. 121 - nukeClicks: 5, 122 - nukeClickWindow: 1.6, 123 - nukeFallAfter: 2.4, 124 - nukeSmileyAfter: 15, 125 - nukeEcgAfter: 30, 126 - nukeHeartAfter: 45, 127 - nukeNormalAfter: 60, 128 - ecgStrength: 1, 129 - heartDuration: 1.9, 130 - heartReach: 0.42, 131 - heartThickness: 40, 132 - heartStrength: 0.6, 133 - heartSeconds: 0.15, 134 - // Hoe rood een verse ring is ten opzichte van het muisspoor. 135 - heartRed: 1.3, 136 -}; 137 - 138 -const MIN_ALPHA = 0.01; 139 - 140 -/* Straal van een hart per hoek, vanuit het midden van het hart en met de punt naar 141 - beneden; de grootste straal is 1. Gemaakt uit de bekende parametrische hartkromme. */ 142 -const HEART_BINS = 360; 143 -const HEART_RADIUS = (() => { 144 - const points = []; 145 - for (let i = 0; i < 2000; i += 1) { 146 - const t = (i / 2000) * Math.PI * 2; 147 - const x = 16 * Math.sin(t) ** 3; 148 - const y = -(13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t)); 149 - points.push([x, y]); 150 - } 151 - const ys = points.map((p) => p[1]); 152 - const midY = (Math.min(...ys) + Math.max(...ys)) / 2; 153 - const table = new Float32Array(HEART_BINS); 154 - for (const [x, y] of points) { 155 - const angle = Math.atan2(y - midY, x); 156 - const bin = Math.floor(((angle + Math.PI) / (Math.PI * 2)) * HEART_BINS) % HEART_BINS; 157 - table[bin] = Math.max(table[bin], Math.hypot(x, y - midY)); 158 - } 159 - for (let i = 0; i < HEART_BINS; i += 1) { 160 - if (!table[i]) table[i] = table[(i + HEART_BINS - 1) % HEART_BINS]; 161 - } 162 - const max = Math.max(...table); 163 - for (let i = 0; i < HEART_BINS; i += 1) table[i] /= max; 164 - return table; 165 -})(); 166 -const HEART_MIN = Math.min(...HEART_RADIUS); 167 - 168 -function randomBetween([min, max]) { 169 - return min + Math.random() * (max - min); 170 -} 171 - 172 -function bump(u, center, width, height) { 173 - const d = (u - center) / width; 174 - return height * Math.exp(-d * d); 175 -} 176 - 177 -/* Een slag van een hartmonitor over een fase u van 0 tot 1: ronde P-golf, de scherpe QRS-piek 178 - met een diepe S, een licht oplopend ST-stuk, een brede T-golf en een kleine U-golf. Omhoog 179 - is positief. */ 180 -function ecgShape(u) { 181 - return ( 182 - bump(u, 0.18, 0.05, 0.16) + 183 - bump(u, 0.36, 0.012, -0.14) + 184 - bump(u, 0.4, 0.013, 1) + 185 - bump(u, 0.44, 0.014, -0.34) + 186 - bump(u, 0.5, 0.05, 0.04) + 187 - bump(u, 0.66, 0.075, 0.32) + 188 - bump(u, 0.82, 0.035, 0.05) 189 - ); 190 -} 191 - 192 -/* Smileys in genormaliseerde coördinaten: het midden is (0, 0), de rand van het gezicht 193 - ligt op straal 1 en y loopt naar beneden. Elke vorm geeft per punt 1 (teken) of 0. */ 194 -function inDot(x, y, cx, cy, radius) { 195 - return Math.hypot(x - cx, y - cy) < radius; 196 -} 197 - 198 -function inSmile(x, y) { 199 - return y > 0.12 && Math.abs(Math.hypot(x, y + 0.05) - 0.56) < 0.09; 200 -} 201 - 202 -function inHeartEye(x, y, cx, cy) { 203 - const dx = (x - cx) / 0.2; 204 - const dy = (y - cy) / 0.2; 205 - return Math.hypot(dx, dy) < heartRadius(dx, dy); 206 -} 207 - 208 -const SMILEYS = [ 209 - // blij 210 - (x, y) => inDot(x, y, -0.33, -0.28, 0.13) || inDot(x, y, 0.33, -0.28, 0.13) || inSmile(x, y), 211 - // grote lach 212 - (x, y) => 213 - inDot(x, y, -0.33, -0.3, 0.13) || 214 - inDot(x, y, 0.33, -0.3, 0.13) || 215 - // open mond: de rand van een D, met een gat erin 216 - (y > 0.06 && Math.hypot(x, y - 0.05) < 0.62 && !(y > 0.2 && Math.hypot(x, y - 0.05) < 0.44)), 217 - // knipoog 218 - (x, y) => 219 - (Math.abs(y + 0.28) < 0.09 && Math.abs(x + 0.33) < 0.17) || inDot(x, y, 0.33, -0.28, 0.13) || inSmile(x, y), 220 - // tong uit 221 - (x, y) => 222 - inDot(x, y, -0.33, -0.28, 0.13) || 223 - inDot(x, y, 0.33, -0.28, 0.13) || 224 - (Math.abs(y - 0.28) < 0.08 && Math.abs(x) < 0.42) || 225 - (y > 0.28 && inDot(x, y, 0.14, 0.46, 0.21)), 226 - // verliefd 227 - (x, y) => inHeartEye(x, y, -0.33, -0.26) || inHeartEye(x, y, 0.33, -0.26) || inSmile(x, y), 228 - // verbaasd 229 - (x, y) => 230 - inDot(x, y, -0.33, -0.3, 0.13) || 231 - inDot(x, y, 0.33, -0.3, 0.13) || 232 - Math.abs(Math.hypot(x, y - 0.4) - 0.19) < 0.08, 233 -]; 234 - 235 -function smileyAt(kind, x, y) { 236 - const r = Math.hypot(x, y); 237 - // De rand is minstens één rasterregel dik, anders valt hij onderaan tussen de regels door. 238 - if (Math.abs(r - 0.9) < 0.12) return true; 239 - if (r > 0.78) return false; 240 - return SMILEYS[kind](x, y); 241 -} 242 -function hash(value) { 243 - const x = Math.sin(value * 12.9898) * 43758.5453; 244 - return x - Math.floor(x); 245 -} 246 - 247 -function heartRadius(dx, dy) { 248 - const bin = Math.floor(((Math.atan2(dy, dx) + Math.PI) / (Math.PI * 2)) * HEART_BINS) % HEART_BINS; 249 - return HEART_RADIUS[bin]; 250 -} 251 - 252 -function clamp01(value) { 253 - return value < 0 ? 0 : value > 1 ? 1 : value; 254 -} 255 - 256 -function readTheme() { 257 - const root = document.documentElement; 258 - const styles = getComputedStyle(root); 259 - const text = styles.getPropertyValue("--text").trim() || "#111827"; 260 - const accent = styles.getPropertyValue("--brand-red").trim() || "#e62b27"; 261 - // Een eigen kleur voor de hartslaglijn (ECG); zonder --glyph-line is dat het accent, zoals in Ember. 262 - const line = styles.getPropertyValue("--glyph-line").trim() || accent; 263 - // :root zonder attribuut is in Ember donker; zie layout.css. 264 - const mode = root.getAttribute("data-appearance") === "light" ? "light" : "dark"; 265 - return { text, accent, line, mode }; 266 -} 267 - 268 -/* De tekens van een thema: een niet-lege lijst van hooguit 256 tekens (de index staat in een Uint8Array). */ 269 -function checkGlyphs(glyphs) { 270 - const list = Array.from(glyphs || []); 271 - if (!list.length || list.length > 256) throw new Error("tekenveld: een thema heeft 1 tot 256 tekens"); 272 - return list; 273 -} 274 - 275 -/* Wat geen achtergrond is, staat met data-glyph-block op het element: een blok, een tegel, een kop. 276 - Een klik daarop geeft geen smiley, en de hartslaglijn loopt bij voorkeur waar die niet staan. De 277 - pagina markeert zelf; het veld kent geen vaste selectors van een pagina. */ 278 -const GLYPH_BLOCK_SELECTOR = "[data-glyph-block]"; 279 - 280 -/* De rechthoeken van alle gemarkeerde blokken die zichtbaar zijn; voor de optie obstacles. */ 281 -function glyphBlockRects(root = document) { 282 - return Array.from(root.querySelectorAll(GLYPH_BLOCK_SELECTOR), (element) => element.getBoundingClientRect()) 283 - .filter((rect) => rect.width > 0 && rect.height > 0); 284 -} 285 - 286 -/* Start het veld op een canvas. Geeft { destroy, configure } terug: configure past de intensiteit of 287 - het thema aan terwijl het veld doorloopt (een andere pagina met hetzelfde veld), zonder opnieuw te 288 - beginnen. */ 289 -function createGlyphField(canvas, overrides = {}) { 290 - const options = { ...DEFAULTS, ...overrides }; 291 - let glyphSet = checkGlyphs(options.glyphs); 292 - const randomGlyph = () => (Math.random() * glyphSet.length) | 0; 293 - const ctx = canvas.getContext("2d"); 294 - const layer = document.createElement("canvas"); 295 - const layerCtx = layer.getContext("2d"); 296 - const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)"); 297 - 298 - let dpr = 1; 299 - let cellW = 7; 300 - let cellH = options.lineHeight; 301 - let tileW = 7; 302 - let tileH = 15; 303 - let cols = 0; 304 - let rows = 0; 305 - let glyphs = new Uint8Array(0); 306 - let ambient = new Float32Array(0); 307 - let trail = new Float32Array(0); 308 - let pulse = new Float32Array(0); 309 - let streak = new Float32Array(0); 310 - // Het spoor van de hartslaglijn apart van dat van de smileys, zodat de lijn een eigen kleur kan hebben. 311 - let lineTrail = new Float32Array(0); 312 - let quiet = new Float32Array(0); 313 - let neutralAtlas = null; 314 - let accentAtlas = null; 315 - let lineAtlas = null; 316 - let yellowAtlas = null; 317 - let orangeAtlas = null; 318 - let smokeAtlas = null; 319 - let theme = readTheme(); 320 - 321 - // Rechthoek (in cellen) waarbinnen nog spoor ligt; leeg als minC > maxC. 322 - let minC = 1; 323 - let maxC = 0; 324 - let minR = 1; 325 - let maxR = 0; 326 - 327 - let pointer = null; 328 - let lastPointer = null; 329 - let heartHome = null; 330 - let rings = []; 331 - let lastBeatAt = -Infinity; 332 - let lastBeatSpot = null; 333 - let ecg = null; 334 - let smileys = []; 335 - let calm = 1; 336 - let nuke = null; 337 - let clickTimes = []; 338 - let fallers = null; 339 - let nextEcgAt = performance.now() + randomBetween(options.ecgFirstSeconds) * 1000; 340 - let frameId = 0; 341 - let lastFrame = 0; 342 - let lastAmbient = -Infinity; 343 - let running = false; 344 - let destroyed = false; 345 - 346 - function buildAtlas(color) { 347 - const atlas = document.createElement("canvas"); 348 - atlas.width = tileW * glyphSet.length; 349 - atlas.height = tileH; 350 - const a = atlas.getContext("2d"); 351 - a.font = `${options.fontSize * dpr}px ${options.fontFamily}`; 352 - a.textBaseline = "middle"; 353 - a.textAlign = "center"; 354 - a.fillStyle = color; 355 - for (let i = 0; i < glyphSet.length; i += 1) { 356 - a.fillText(glyphSet[i], i * tileW + tileW / 2, tileH / 2 + dpr * 0.5); 357 - } 358 - return atlas; 359 - } 360 - 361 - function buildAtlases() { 362 - neutralAtlas = buildAtlas(theme.text); 363 - accentAtlas = buildAtlas(theme.accent); 364 - lineAtlas = theme.line === theme.accent ? accentAtlas : buildAtlas(theme.line); 365 - // Vuurkleuren voor de explosie. 366 - yellowAtlas = buildAtlas(theme.mode === "light" ? "#eab308" : "#facc15"); 367 - orangeAtlas = buildAtlas("#f97316"); 368 - // Rook: zwart op licht; op donker donkergrijs, anders zie je hem niet. 369 - smokeAtlas = buildAtlas(theme.mode === "light" ? "#0b0b0f" : "#52525b"); 370 - } 371 - 372 - function resize() { 373 - dpr = Math.min(window.devicePixelRatio || 1, 2); 374 - const width = window.innerWidth; 375 - const height = window.innerHeight; 376 - 377 - canvas.width = layer.width = Math.round(width * dpr); 378 - canvas.height = layer.height = Math.round(height * dpr); 379 - canvas.style.width = `${width}px`; 380 - canvas.style.height = `${height}px`; 381 - 382 - ctx.font = `${options.fontSize}px ${options.fontFamily}`; 383 - cellW = Math.max(6, Math.round(ctx.measureText("M").width * 10) / 10); 384 - cellH = options.lineHeight; 385 - tileW = Math.ceil(cellW * dpr); 386 - tileH = Math.ceil(cellH * dpr); 387 - 388 - cols = Math.ceil(width / cellW) + 1; 389 - rows = Math.ceil(height / cellH) + 1; 390 - glyphs = new Uint8Array(cols * rows); 391 - for (let i = 0; i < glyphs.length; i += 1) glyphs[i] = randomGlyph(); 392 - ambient = new Float32Array(cols * rows); 393 - trail = new Float32Array(cols * rows); 394 - pulse = new Float32Array(cols * rows); 395 - streak = new Float32Array(cols * rows); 396 - lineTrail = new Float32Array(cols * rows); 397 - quiet = new Float32Array(cols * rows); 398 - minC = minR = 1; 399 - maxC = maxR = 0; 400 - 401 - buildAtlases(); 402 - lastAmbient = -Infinity; 403 - render(performance.now(), 0); 404 - } 405 - 406 - function applyTheme() { 407 - const next = readTheme(); 408 - if (next.text === theme.text && next.accent === theme.accent && next.line === theme.line && next.mode === theme.mode) return; 409 - theme = next; 410 - buildAtlases(); 411 - lastAmbient = -Infinity; 412 - render(performance.now(), 0); 413 - } 414 - 415 - function cellX(c) { 416 - return Math.round(c * cellW * dpr); 417 - } 418 - 419 - function cellY(r) { 420 - return Math.round(r * cellH * dpr); 421 - } 422 - 423 - // Het rustende veld: twee trage golven per as en een diagonale band. Alleen de toppen 424 - // worden zichtbaar, zodat er losse wolken ontstaan en geen egaal behang. 425 - function drawAmbient(time, elapsed) { 426 - const t = time / 1000; 427 - const max = options.ambientMax[theme.mode]; 428 - const colWave = new Float32Array(cols); 429 - const rowWave = new Float32Array(rows); 430 - for (let c = 0; c < cols; c += 1) { 431 - colWave[c] = Math.sin(c * 0.045 + t * 0.21) + 0.6 * Math.sin(c * 0.013 - t * 0.09); 432 - } 433 - for (let r = 0; r < rows; r += 1) { 434 - rowWave[r] = Math.sin(r * 0.11 - t * 0.17) + 0.6 * Math.sin(r * 0.031 + t * 0.07); 435 - } 436 - 437 - // Tekens wisselen ook in rust, heel af en toe. 438 - const rerollChance = elapsed > 0 ? Math.min(0.05 * elapsed, 0.2) : 0; 439 - 440 - layerCtx.setTransform(1, 0, 0, 1, 0, 0); 441 - layerCtx.clearRect(0, 0, layer.width, layer.height); 442 - let currentAlpha = -1; 443 - 444 - for (let r = 0; r < rows; r += 1) { 445 - const y = cellY(r); 446 - const rw = rowWave[r]; 447 - for (let c = 0; c < cols; c += 1) { 448 - const index = r * cols + c; 449 - const wave = (colWave[c] * rw + Math.sin((c + r * 2.2) * 0.035 + t * 0.12)) / 3.2; 450 - const cloud = clamp01((wave + options.ambientLift) * 2.2); 451 - const alpha = Math.round(max * cloud * cloud * 128) / 128; 452 - ambient[index] = alpha; 453 - if (rerollChance && Math.random() < rerollChance) glyphs[index] = randomGlyph(); 454 - if (alpha < MIN_ALPHA) continue; 455 - if (alpha !== currentAlpha) { 456 - layerCtx.globalAlpha = alpha; 457 - currentAlpha = alpha; 458 - } 459 - layerCtx.drawImage(neutralAtlas, glyphs[index] * tileW, 0, tileW, tileH, cellX(c), y, tileW, tileH); 460 - } 461 - } 462 - layerCtx.globalAlpha = 1; 463 - } 464 - 465 - // Een zachte kwast op de plek van de muis; het spoor neemt het hoogste van oud en nieuw. 466 - // flat: een harde kern zonder zachte rand, voor een scherpe lijn in plaats van een vlek. 467 - function stamp(px, py, radius = options.brushRadius, strength = 1, flat = false, target = trail) { 468 - const c0 = Math.max(0, Math.floor((px - radius) / cellW)); 469 - const c1 = Math.min(cols - 1, Math.ceil((px + radius) / cellW)); 470 - const r0 = Math.max(0, Math.floor((py - radius) / cellH)); 471 - const r1 = Math.min(rows - 1, Math.ceil((py + radius) / cellH)); 472 - if (c0 > c1 || r0 > r1) return; 473 - const inv = 1 / (radius * radius); 474 - 475 - for (let r = r0; r <= r1; r += 1) { 476 - const dy = r * cellH + cellH / 2 - py; 477 - for (let c = c0; c <= c1; c += 1) { 478 - const dx = c * cellW + cellW / 2 - px; 479 - const d = (dx * dx + dy * dy) * inv; 480 - if (d >= 1) continue; 481 - // Rafelige rand: niet elke cel aan de buitenkant doet even hard mee. 482 - const edge = flat ? (d < 0.6 ? 1 : (1 - d) / 0.4) : (1 - d) * (1 - d) * (0.75 + Math.random() * 0.25); 483 - const value = edge * strength; 484 - const index = r * cols + c; 485 - if (value > target[index]) target[index] = value; 486 - } 487 - } 488 - 489 - extendBox(c0, c1, r0, r1); 490 - } 491 - 492 - function extendBox(c0, c1, r0, r1) { 493 - if (minC > maxC) { 494 - minC = c0; 495 - maxC = c1; 496 - minR = r0; 497 - maxR = r1; 498 - } else { 499 - minC = Math.min(minC, c0); 500 - maxC = Math.max(maxC, c1); 501 - minR = Math.min(minR, r0); 502 - maxR = Math.max(maxR, r1); 503 - } 504 - } 505 - 506 - // Een hartvormige ring met de grootste straal `size` rond (cx, cy). Per rij worden alleen 507 - // de kolommen bekeken die tussen de kleinste en grootste straal van de ring vallen. 508 - function stampRing(cx, cy, size, strength) { 509 - const half = options.heartThickness / 2; 510 - const outer = size + half; 511 - const inner = Math.max(0, size * HEART_MIN - half); 512 - const r0 = Math.max(0, Math.floor((cy - outer) / cellH)); 513 - const r1 = Math.min(rows - 1, Math.ceil((cy + outer) / cellH)); 514 - let touched = false; 515 - 516 - for (let r = r0; r <= r1; r += 1) { 517 - const dy = r * cellH + cellH / 2 - cy; 518 - const outerSpan = outer * outer - dy * dy; 519 - if (outerSpan <= 0) continue; 520 - const reach = Math.sqrt(outerSpan); 521 - const innerSpan = inner * inner - dy * dy; 522 - const gap = innerSpan > 0 ? Math.sqrt(innerSpan) : 0; 523 - const c0 = Math.max(0, Math.floor((cx - reach) / cellW)); 524 - const c1 = Math.min(cols - 1, Math.ceil((cx + reach) / cellW)); 525 - 526 - for (let c = c0; c <= c1; c += 1) { 527 - const dx = c * cellW + cellW / 2 - cx; 528 - if (dx > -gap && dx < gap) { 529 - c = Math.max(c, Math.floor((cx + gap) / cellW) - 1); 530 - continue; 531 - } 532 - const distance = Math.sqrt(dx * dx + dy * dy); 533 - const offset = Math.abs(distance - size * heartRadius(dx, dy)) / half; 534 - if (offset >= 1) continue; 535 - const value = (1 - offset * offset) * strength; 536 - const index = r * cols + c; 537 - if (value > pulse[index]) pulse[index] = value; 538 - touched = true; 539 - } 540 - } 541 - 542 - if (touched) { 543 - extendBox( 544 - Math.max(0, Math.floor((cx - outer) / cellW)), 545 - Math.min(cols - 1, Math.ceil((cx + outer) / cellW)), 546 - r0, 547 - r1 548 - ); 549 - } 550 - } 551 - 552 - // Een slag: twee ringen vanaf dezelfde plek, de tweede iets later en veel zwakker (lub-dub). 553 - function spawnBeat(now, x, y) { 554 - rings.push({ x, y, start: now, weight: 1 }); 555 - rings.push({ x, y, start: now + options.heartDubDelay * 1000, weight: options.heartDubWeight }); 556 - if (rings.length > options.heartMaxRings) rings.splice(0, rings.length - options.heartMaxRings); 557 - lastBeatAt = now; 558 - lastBeatSpot = { x, y }; 559 - } 560 - 561 - /* De hartslag komt uit de muis. Staat de muis stil, dan klopt hij in een rustig ritme op 562 - die plek; beweegt hij, dan komt er een slag bij zodra hij een stuk verder is, zodat er 563 - hartjes langs het pad liggen. Buiten het venster klopt er niets. */ 564 - /* De hartslaglijn loopt los van de muis. De baan wordt per 2 px gevolgd, anders valt de 565 - smalle piek tussen twee frames door; een stempel volgt zodra de lijn een stukje verder 566 - is. */ 567 - /* Hoogte voor de hartslaglijn. Een lichte voorkeur voor stroken waar weinig inhoud staat: 568 - uit vijf willekeurige hoogtes wint meestal de vrijste, maar een op de vier keer gewoon 569 - een willekeurige, zodat de lijn overal kan verschijnen. */ 570 - function pickLineY(height) { 571 - const candidates = []; 572 - for (let i = 0; i < 5; i += 1) candidates.push(height * (0.15 + Math.random() * 0.75)); 573 - const rects = options.obstacles ? options.obstacles() : []; 574 - if (!rects.length || Math.random() < 0.25) return candidates[0]; 575 - 576 - const width = window.innerWidth; 577 - const top = options.ecgAmplitude; 578 - const bottom = options.ecgAmplitude * 0.4; 579 - let best = candidates[0]; 580 - let bestCover = Infinity; 581 - for (const y of candidates) { 582 - let cover = 0; 583 - for (const rect of rects) { 584 - if (rect.bottom < y - top || rect.top > y + bottom) continue; 585 - cover += Math.max(0, Math.min(rect.right, width) - Math.max(rect.left, 0)); 586 - } 587 - if (cover < bestCover) { 588 - bestCover = cover; 589 - best = y; 590 - } 591 - } 592 - return best; 593 - } 594 - 595 - // Elke slag een eigen grootte, vast per slag zodat hij niet trilt tussen frames. 596 - function beatScale(line, beat) { 597 - const roll = hash(line.seed + beat * 7.31); 598 - let scale = 0.82 + roll * 0.36; 599 - if (line.wild && hash(line.seed - beat * 3.17) > 0.62) scale *= 1.6 + roll * 0.5; 600 - return scale; 601 - } 602 - 603 - function heartLine(now) { 604 - if (nuke && now - nuke.start < options.nukeEcgAfter * 1000) return; 605 - if (!ecg) { 606 - if (now < nextEcgAt) return; 607 - const height = window.innerHeight; 608 - ecg = { 609 - start: now, 610 - baseY: pickLineY(height), 611 - lastX: -options.ecgRadius, 612 - stampX: -Infinity, 613 - stampY: 0, 614 - offset: Math.random(), 615 - seed: Math.random() * 1000, 616 - // Elke lijn een eigen hoogte en tempo; zo nu en dan een wilde met uitschieters. 617 - amplitude: options.ecgAmplitude * (0.8 + Math.random() * 0.4), 618 - beatWidth: options.ecgBeatWidth * (0.8 + Math.random() * 0.45), 619 - wild: Math.random() < options.ecgWildChance, 620 - }; 621 - } 622 - 623 - const width = window.innerWidth; 624 - const travel = width + options.ecgRadius * 2; 625 - const progress = (now - ecg.start) / (options.ecgCrossSeconds * 1000); 626 - const targetX = -options.ecgRadius + travel * Math.min(progress, 1); 627 - 628 - for (let x = ecg.lastX; x <= targetX; x += 2) { 629 - // Een echte lijn ligt nooit stil: de basislijn deint zacht mee (ademhaling). 630 - const sway = ecg.wild ? 2 : 1; 631 - const wander = (Math.sin(x * 0.006 + ecg.offset * 6) * 0.1 + Math.sin(x * 0.017 + ecg.offset * 3) * 0.04) * sway; 632 - const phase = x / ecg.beatWidth + ecg.offset; 633 - const y = ecg.baseY - (ecgShape(phase % 1) * beatScale(ecg, Math.floor(phase)) + wander) * ecg.amplitude; 634 - if (Math.hypot(x - ecg.stampX, y - ecg.stampY) >= options.ecgRadius * 0.3) { 635 - stamp(x, y, options.ecgRadius, options.ecgStrength, true, lineTrail); 636 - ecg.stampX = x; 637 - ecg.stampY = y; 638 - } 639 - ecg.lastX = x + 2; 640 - } 641 - 642 - if (progress >= 1) { 643 - ecg = null; 644 - nextEcgAt = now + randomBetween(options.ecgEverySeconds) * 1000; 645 - } 646 - } 647 - 648 - function spawnSmiley(x, y) { 649 - let kind = (Math.random() * SMILEYS.length) | 0; 650 - // Niet twee keer achter elkaar dezelfde. 651 - if (smileys.length && smileys[smileys.length - 1].kind === kind) kind = (kind + 1) % SMILEYS.length; 652 - smileys.push({ x, y, kind, start: performance.now(), tilt: (Math.random() - 0.5) * 0.5 }); 653 - if (smileys.length > options.smileyMax) smileys.splice(0, smileys.length - options.smileyMax); 654 - } 655 - 656 - 657 - /* Een smiley springt op (even groter dan zijn eindmaat), zweeft een stukje omhoog en 658 - dooft uit. Hij wordt elk frame opnieuw in het korte spoor gezet; als hij weg is, flitst 659 - hij in een paar tienden van een seconde uit. */ 660 - function drawSmileys(now) { 661 - let keep = 0; 662 - for (const smiley of smileys) { 663 - const age = (now - smiley.start) / 1000; 664 - if (age >= options.smileySeconds) continue; 665 - smileys[keep] = smiley; 666 - keep += 1; 667 - 668 - const pop = Math.min(age / 0.22, 1); 669 - const scale = pop < 1 ? 0.3 + 0.9 * Math.sin((pop * Math.PI) / 2) + 0.1 * Math.sin(pop * Math.PI) : 1; 670 - const fade = Math.min(1, (options.smileySeconds - age) / 0.45); 671 - const radius = options.smileyRadius * scale; 672 - const cx = smiley.x; 673 - const cy = smiley.y - age * 40; 674 - const cos = Math.cos(smiley.tilt); 675 - const sin = Math.sin(smiley.tilt); 676 - // Stille cirkel: binnen 1,1 keer de straal helemaal stil, tot 1,5 keer zacht aflopend. 677 - const reach = radius * 1.5; 678 - 679 - const c0 = Math.max(0, Math.floor((cx - reach) / cellW)); 680 - const c1 = Math.min(cols - 1, Math.ceil((cx + reach) / cellW)); 681 - const r0 = Math.max(0, Math.floor((cy - reach) / cellH)); 682 - const r1 = Math.min(rows - 1, Math.ceil((cy + reach) / cellH)); 683 - if (c0 > c1 || r0 > r1) continue; 684 - 685 - for (let r = r0; r <= r1; r += 1) { 686 - const py = (r * cellH + cellH / 2 - cy) / radius; 687 - for (let c = c0; c <= c1; c += 1) { 688 - const px = (c * cellW + cellW / 2 - cx) / radius; 689 - const x = px * cos + py * sin; 690 - const y = -px * sin + py * cos; 691 - const index = r * cols + c; 692 - const d2 = x * x + y * y; 693 - if (d2 < 2.25) { 694 - const still = fade * (d2 < 1.21 ? 1 : (2.25 - d2) / 1.04); 695 - if (still > quiet[index]) quiet[index] = still; 696 - } 697 - if (!smileyAt(smiley.kind, x, y)) continue; 698 - if (fade > streak[index]) streak[index] = fade; 699 - } 700 - } 701 - extendBox(c0, c1, r0, r1); 702 - } 703 - smileys.length = keep; 704 - } 705 - 706 - function onPointerDown(event) { 707 - if (reducedMotion.matches || event.button !== 0) return; 708 - if (options.isBackground && !options.isBackground(event)) return; 709 - if (nuke) { 710 - // In de nasleep geen nieuwe explosie; na nukeSmileyAfter mogen de smileys wel weer. 711 - if (nukeAge(performance.now()) >= options.nukeSmileyAfter) spawnSmiley(event.clientX, event.clientY); 712 - return; 713 - } 714 - 715 - const now = performance.now(); 716 - clickTimes = clickTimes.filter((time) => now - time < options.nukeClickWindow * 1000); 717 - clickTimes.push(now); 718 - if (clickTimes.length >= options.nukeClicks) { 719 - clickTimes = []; 720 - startNuke(now, event.clientX, event.clientY); 721 - return; 722 - } 723 - spawnSmiley(event.clientX, event.clientY); 724 - } 725 - 726 - function startNuke(now, x, y) { 727 - nuke = { start: now, x, y, puffs: [], puffClock: 0 }; 728 - smileys = []; 729 - rings = []; 730 - ecg = null; 731 - pointer = null; 732 - lastPointer = null; 733 - trail.fill(0); 734 - pulse.fill(0); 735 - nextEcgAt = now + options.nukeEcgAfter * 1000; 736 - } 737 - 738 - // Hoe ver de nasleep is, in seconden; Infinity als er geen explosie is geweest. 739 - function nukeAge(now) { 740 - return nuke ? (now - nuke.start) / 1000 : Infinity; 741 - } 742 - 743 - function heartbeat(now) { 744 - if (nukeAge(now) < options.nukeHeartAfter) return; 745 - if (heartHome) { 746 - const sinceBeat = now - lastBeatAt; 747 - const moved = lastBeatSpot 748 - ? Math.hypot(heartHome.x - lastBeatSpot.x, heartHome.y - lastBeatSpot.y) 749 - : Infinity; 750 - if ( 751 - sinceBeat >= options.heartPeriod * 1000 || 752 - (moved >= options.heartSpacing && sinceBeat >= options.heartMinGap * 1000) 753 - ) { 754 - spawnBeat(now, heartHome.x, heartHome.y); 755 - } 756 - } 757 - 758 - const reach = Math.max(window.innerWidth, window.innerHeight) * options.heartReach; 759 - let keep = 0; 760 - for (const ring of rings) { 761 - const age = (now - ring.start) / 1000; 762 - if (age >= options.heartDuration) continue; 763 - rings[keep] = ring; 764 - keep += 1; 765 - if (age < 0) continue; 766 - const progress = age / options.heartDuration; 767 - const size = reach * (1 - (1 - progress) * (1 - progress)); 768 - const strength = options.heartStrength * ring.weight * (1 - progress) ** 1.3; 769 - // Een bijna uitgedoofde ring is groot en kost veel cellen voor weinig beeld. 770 - if (size > 4 && strength > 0.06) stampRing(ring.x, ring.y, size, strength); 771 - } 772 - rings.length = keep; 773 - } 774 - 775 - function paintPointer() { 776 - const from = lastPointer || pointer; 777 - const dx = pointer.x - from.x; 778 - const dy = pointer.y - from.y; 779 - const step = options.brushRadius * 0.35; 780 - const steps = Math.max(1, Math.ceil(Math.hypot(dx, dy) / step)); 781 - for (let s = 1; s <= steps; s += 1) { 782 - stamp(from.x + (dx * s) / steps, from.y + (dy * s) / steps); 783 - } 784 - lastPointer = pointer; 785 - pointer = null; 786 - } 787 - 788 - // Het spoor: alleen de cellen binnen de rechthoek met spoor. De cel wordt eerst leeg 789 - // gemaakt, zodat het teken uit de rustlaag er niet dubbel onder staat. 790 - function drawTrail(dt) { 791 - if (minC > maxC) return; 792 - const decay = dt > 0 ? Math.exp(-dt / options.trailSeconds) : 1; 793 - const pulseDecay = dt > 0 ? Math.exp(-dt / options.heartSeconds) : 1; 794 - const streakDecay = dt > 0 ? Math.exp(-dt / options.streakSeconds) : 1; 795 - const neutralMax = options.trailNeutral[theme.mode]; 796 - const accentMax = options.trailAccent[theme.mode]; 797 - const streakNeutral = options.streakNeutral[theme.mode]; 798 - const streakAccent = options.streakAccent[theme.mode]; 799 - let nextMinC = cols; 800 - let nextMaxC = -1; 801 - let nextMinR = rows; 802 - let nextMaxR = -1; 803 - 804 - for (let r = minR; r <= maxR; r += 1) { 805 - const y = cellY(r); 806 - for (let c = minC; c <= maxC; c += 1) { 807 - const index = r * cols + c; 808 - let heat = trail[index]; 809 - let beat = pulse[index]; 810 - let flash = streak[index]; 811 - let pulseLine = lineTrail[index]; 812 - let hush = quiet[index]; 813 - if (heat <= 0 && beat <= 0 && flash <= 0 && pulseLine <= 0 && hush <= 0) continue; 814 - 815 - if (heat > 0) { 816 - heat *= decay; 817 - if (heat < 0.01) heat = 0; 818 - trail[index] = heat; 819 - } 820 - if (beat > 0) { 821 - beat *= pulseDecay; 822 - if (beat < 0.04) beat = 0; 823 - pulse[index] = beat; 824 - } 825 - if (flash > 0) { 826 - flash *= streakDecay; 827 - if (flash < 0.03) flash = 0; 828 - streak[index] = flash; 829 - } 830 - if (pulseLine > 0) { 831 - pulseLine *= streakDecay; 832 - if (pulseLine < 0.03) pulseLine = 0; 833 - lineTrail[index] = pulseLine; 834 - } 835 - if (hush > 0) { 836 - hush *= streakDecay; 837 - if (hush < 0.03) hush = 0; 838 - quiet[index] = hush; 839 - } 840 - if (heat === 0 && beat === 0 && flash === 0 && pulseLine === 0 && hush === 0) continue; 841 - 842 - if (c < nextMinC) nextMinC = c; 843 - if (c > nextMaxC) nextMaxC = c; 844 - if (r < nextMinR) nextMinR = r; 845 - if (r > nextMaxR) nextMaxR = r; 846 - 847 - // Onder het spoor en de hartslag wisselen de tekens snel. 848 - if (dt > 0 && Math.random() < (heat * 9 + beat * 5 + (flash + pulseLine) * 6) * dt) glyphs[index] = randomGlyph(); 849 - 850 - // Een verse ring is roder dan het spoor; verder weg is hij al zwakker en daarmee 851 - // vanzelf grijzer. 852 - // In de stille cirkel van een smiley (hush) en tijdens de nasleep van een explosie 853 - // (calm) wijken het spoor en de ringen; de smiley en de hartslaglijn blijven. 854 - heat *= calm * (1 - hush); 855 - beat *= calm * (1 - hush); 856 - const lift = heat > beat ? heat : beat; 857 - const ring = Math.min(1, beat * options.heartRed); 858 - const glow = heat > ring ? heat : ring; 859 - const x = cellX(c); 860 - const sx = glyphs[index] * tileW; 861 - // Achter een smiley wordt het rustende veld stilgezet (hush), zodat het gezicht 862 - // niet in de wolken verdwijnt. De smileys (flash) en de hartslaglijn (pulseLine) zijn vooral rood; de lijn in --glyph-line. 863 - const neutral = ambient[index] * calm * (1 - lift) * (1 - hush) + neutralMax * lift + streakNeutral * Math.max(flash, pulseLine); 864 - const red = Math.max(glow * glow * accentMax, flash * streakAccent); 865 - const lineRed = pulseLine * streakAccent; 866 - ctx.clearRect(x, y, tileW, tileH); 867 - if (neutral >= MIN_ALPHA) { 868 - ctx.globalAlpha = neutral; 869 - ctx.drawImage(neutralAtlas, sx, 0, tileW, tileH, x, y, tileW, tileH); 870 - } 871 - if (red > 0.01) { 872 - ctx.globalAlpha = Math.min(1, red); 873 - ctx.drawImage(accentAtlas, sx, 0, tileW, tileH, x, y, tileW, tileH); 874 - } 875 - if (lineRed > 0.01) { 876 - ctx.globalAlpha = Math.min(1, lineRed); 877 - ctx.drawImage(lineAtlas, sx, 0, tileW, tileH, x, y, tileW, tileH); 878 - } 879 - } 880 - } 881 - 882 - ctx.globalAlpha = 1; 883 - minC = nextMinC; 884 - maxC = nextMaxC; 885 - minR = nextMinR; 886 - maxR = nextMaxR; 887 - } 888 - 889 - /* Zwarte rook die uit de kop van de paddenstoel spuwt en naar de bovenkant van het scherm 890 - klimt. Losse wolkjes die opstijgen, opzwellen en onderweg dunner worden. */ 891 - function drawSmoke(t, dt) { 892 - const size = Math.min(1.7, Math.max(0.7, Math.min(window.innerWidth, window.innerHeight) / 650)); 893 - const capY = nuke.y - 300 * size * easeOut(t / 2.2); 894 - const capRx = (50 + 190 * easeOut(t / 1.4)) * size; 895 - 896 - // Spuwen tussen 0,5 en 3,2 seconden, zo'n vijfentwintig wolkjes per seconde. Alle rook is 897 - // weg rond 5,5 seconden, als de letters gevallen zijn. 898 - if (dt > 0 && t > 0.5 && t < 3.2) { 899 - nuke.puffClock += dt * 25; 900 - while (nuke.puffClock >= 1) { 901 - nuke.puffClock -= 1; 902 - const startY = capY - Math.random() * 40 * size; 903 - nuke.puffs.push({ 904 - x: nuke.x + (Math.random() - 0.5) * capRx * 0.9, 905 - y: startY, 906 - // Snel genoeg om in een paar seconden de bovenkant te halen. 907 - vy: -(startY + 120) / (1.1 + Math.random() * 0.8), 908 - vx: (Math.random() - 0.5) * 50 * size, 909 - radius: (14 + Math.random() * 18) * size, 910 - born: t, 911 - seed: Math.random() * 10, 912 - }); 913 - } 914 - } 915 - 916 - const life = 2.4; 917 - const allFade = t < 4.2 ? 1 : Math.max(0, 1 - (t - 4.2) / 1.3); 918 - let keep = 0; 919 - for (const puff of nuke.puffs) { 920 - const age = t - puff.born; 921 - if (age >= life) continue; 922 - nuke.puffs[keep] = puff; 923 - keep += 1; 924 - 925 - const px = puff.x + puff.vx * age; 926 - const py = puff.y + puff.vy * age * (1 - age / (life * 2.4)); 927 - const radius = puff.radius * (1 + age * 0.85); 928 - const strength = 0.75 * (1 - age / life) * Math.min(1, age / 0.15) * allFade; 929 - if (py + radius < 0 || strength <= 0.01) continue; 930 - 931 - const c0 = Math.max(0, Math.floor((px - radius) / cellW)); 932 - const c1 = Math.min(cols - 1, Math.ceil((px + radius) / cellW)); 933 - const r0 = Math.max(0, Math.floor((py - radius) / cellH)); 934 - const r1 = Math.min(rows - 1, Math.ceil((py + radius) / cellH)); 935 - for (let r = r0; r <= r1; r += 1) { 936 - const dy = r * cellH + cellH / 2 - py; 937 - for (let c = c0; c <= c1; c += 1) { 938 - const dx = c * cellW + cellW / 2 - px; 939 - const angle = Math.atan2(dy, dx); 940 - const edge = 1 + 0.18 * Math.sin(angle * 5 + puff.seed + t * 2); 941 - const d = Math.sqrt(dx * dx + dy * dy) / (radius * edge); 942 - if (d >= 1) continue; 943 - ctx.globalAlpha = strength * (1 - d * d); 944 - ctx.drawImage(smokeAtlas, randomGlyph() * tileW, 0, tileW, tileH, cellX(c), cellY(r), tileW, tileH); 945 - } 946 - } 947 - } 948 - nuke.puffs.length = keep; 949 - } 950 - 951 - // 1 als het veld er gewoon staat; 0 tijdens de nasleep van een explosie. 952 - function fieldPresence(now) { 953 - const age = nukeAge(now); 954 - if (age < options.nukeFallAfter) return 1; 955 - if (age < options.nukeNormalAfter) return 0; 956 - return Math.min(1, (age - options.nukeNormalAfter) / 2); 957 - } 958 - 959 - function easeOut(value) { 960 - const v = Math.min(1, Math.max(0, value)); 961 - return 1 - (1 - v) ** 3; 962 - } 963 - 964 - /* De explosie, rechtstreeks op het canvas: een rode flits over het hele veld, een 965 - schokgolf die over het scherm rolt, een paddenstoelwolk van kokende letters, het veld 966 - dat schudt, en daarna vallen alle letters van het scherm. */ 967 - function drawNuke(now, dt) { 968 - const t = nukeAge(now); 969 - const accentAlpha = theme.mode === "light" ? 0.85 : 0.95; 970 - 971 - // Schudden: alleen het canvas, de inhoud van de pagina blijft staan. 972 - if (t < 1.3) { 973 - const shake = 12 * (1 - t / 1.3) ** 2; 974 - canvas.style.transform = `translate(${(Math.random() - 0.5) * 2 * shake}px, ${(Math.random() - 0.5) * 2 * shake}px)`; 975 - } else if (canvas.style.transform) { 976 - canvas.style.transform = ""; 977 - } 978 - 979 - // Flits: het hele veld licht even rood op. 980 - if (t < 0.7) { 981 - const flashAtlas = t < 0.2 ? yellowAtlas : t < 0.42 ? orangeAtlas : accentAtlas; 982 - ctx.globalAlpha = 0.6 * (1 - t / 0.7) ** 2; 983 - for (let r = 0; r < rows; r += 1) { 984 - const y = cellY(r); 985 - for (let c = 0; c < cols; c += 1) { 986 - ctx.drawImage(flashAtlas, glyphs[r * cols + c] * tileW, 0, tileW, tileH, cellX(c), y, tileW, tileH); 987 - } 988 - } 989 - } 990 - 991 - // Schokgolf: een ring die in anderhalve seconde voorbij de randen van het scherm rolt. 992 - if (t < 1.8) { 993 - const progress = t / 1.8; 994 - const reach = Math.hypot(window.innerWidth, window.innerHeight); 995 - const radius = reach * easeOut(progress); 996 - const band = 30 + 90 * (1 - progress); 997 - const strength = 0.95 * (1 - progress); 998 - const r0 = Math.max(0, Math.floor((nuke.y - radius - band) / cellH)); 999 - const r1 = Math.min(rows - 1, Math.ceil((nuke.y + radius + band) / cellH)); 1000 - for (let r = r0; r <= r1; r += 1) { 1001 - const dy = r * cellH + cellH / 2 - nuke.y; 1002 - const outer = (radius + band) ** 2 - dy * dy; 1003 - if (outer <= 0) continue; 1004 - const reachX = Math.sqrt(outer); 1005 - const c0 = Math.max(0, Math.floor((nuke.x - reachX) / cellW)); 1006 - const c1 = Math.min(cols - 1, Math.ceil((nuke.x + reachX) / cellW)); 1007 - for (let c = c0; c <= c1; c += 1) { 1008 - const dx = c * cellW + cellW / 2 - nuke.x; 1009 - const off = Math.abs(Math.sqrt(dx * dx + dy * dy) - radius) / band; 1010 - if (off >= 1) continue; 1011 - const front = Math.sqrt(dx * dx + dy * dy) - radius; 1012 - const waveAtlas = front > band * 0.3 ? yellowAtlas : front > -band * 0.3 ? orangeAtlas : accentAtlas; 1013 - ctx.globalAlpha = strength * (1 - off); 1014 - ctx.drawImage(waveAtlas, randomGlyph() * tileW, 0, tileW, tileH, cellX(c), cellY(r), tileW, tileH); 1015 - } 1016 - } 1017 - } 1018 - 1019 - // Paddenstoelwolk: een kokende kop die opstijgt, een steel en een wolk over de grond. 1020 - if (t < 4.2) { 1021 - const grow = easeOut(t / 1.4); 1022 - const fade = t < 2.8 ? 1 : Math.max(0, 1 - (t - 2.8) / 1.4); 1023 - // Groot op een groot scherm, passend op een klein scherm. 1024 - const size = Math.min(1.7, Math.max(0.7, Math.min(window.innerWidth, window.innerHeight) / 650)); 1025 - const rise = 300 * size * easeOut(t / 2.2); 1026 - const capY = nuke.y - rise; 1027 - const capRx = (50 + 190 * grow) * size; 1028 - const capRy = (35 + 85 * grow) * size; 1029 - const baseRx = (70 + 230 * grow) * size; 1030 - const baseRy = (22 + 34 * grow) * size; 1031 - const spanX = Math.max(capRx * 1.25, baseRx); 1032 - const c0 = Math.max(0, Math.floor((nuke.x - spanX) / cellW)); 1033 - const c1 = Math.min(cols - 1, Math.ceil((nuke.x + spanX) / cellW)); 1034 - const r0 = Math.max(0, Math.floor((capY - capRy * 1.25) / cellH)); 1035 - const r1 = Math.min(rows - 1, Math.ceil((nuke.y + baseRy) / cellH)); 1036 - 1037 - for (let r = r0; r <= r1; r += 1) { 1038 - const py = r * cellH + cellH / 2; 1039 - for (let c = c0; c <= c1; c += 1) { 1040 - const dx = c * cellW + cellW / 2 - nuke.x; 1041 - let core = 0; 1042 - 1043 - const capDy = py - capY; 1044 - const angle = Math.atan2(capDy, dx); 1045 - const edge = 1 + 0.14 * Math.sin(angle * 7 + t * 2.5) + 0.08 * Math.sin(angle * 13 - t * 4); 1046 - const capD = Math.sqrt((dx / capRx) ** 2 + (capDy / capRy) ** 2) / edge; 1047 - if (capD < 1) core = Math.max(core, 1 - capD * 0.6); 1048 - 1049 - if (py > capY && py < nuke.y) { 1050 - const along = (py - capY) / Math.max(rise, 1); 1051 - const half = (18 + 14 * grow + 30 * grow * along * along) * size; 1052 - if (Math.abs(dx) < half) core = Math.max(core, 0.75); 1053 - } 1054 - 1055 - const baseD = Math.sqrt((dx / baseRx) ** 2 + ((py - nuke.y) / baseRy) ** 2); 1056 - if (baseD < 1 + 0.12 * Math.sin(dx * 0.05 + t * 3)) core = Math.max(core, 0.85 - baseD * 0.4); 1057 - 1058 - if (core <= 0) continue; 1059 - const x = cellX(c); 1060 - const y = cellY(r); 1061 - const glyph = randomGlyph() * tileW; 1062 - // Heet in de kern; naarmate de wolk afkoelt schuift alles richting rood. 1063 - const heatLevel = core - Math.max(0, t - 1.2) * 0.12 + (Math.random() - 0.5) * 0.12; 1064 - const fireAtlas = heatLevel > 0.8 ? yellowAtlas : heatLevel > 0.58 ? orangeAtlas : accentAtlas; 1065 - ctx.globalAlpha = Math.min(1, accentAlpha * core * fade); 1066 - ctx.drawImage(fireAtlas, glyph, 0, tileW, tileH, x, y, tileW, tileH); 1067 - ctx.globalAlpha = 0.35 * core * core * fade; 1068 - ctx.drawImage(neutralAtlas, glyph, 0, tileW, tileH, x, y, tileW, tileH); 1069 - } 1070 - } 1071 - } 1072 - 1073 - drawSmoke(t, dt); 1074 - 1075 - // De letters vallen: elk teken uit het rustende veld krijgt een eigen moment en valt 1076 - // dan met zwaartekracht van het scherm. 1077 - if (t >= options.nukeFallAfter && !fallers) { 1078 - const list = []; 1079 - for (let i = 0; i < ambient.length; i += 1) if (ambient[i] >= 0.012) list.push(i); 1080 - fallers = { 1081 - index: Int32Array.from(list), 1082 - y: new Float32Array(list.length), 1083 - vy: new Float32Array(list.length), 1084 - delay: Float32Array.from(list, () => Math.random() * 1.8), 1085 - }; 1086 - } 1087 - if (fallers && t < options.nukeNormalAfter) { 1088 - const since = t - options.nukeFallAfter; 1089 - const floor = canvas.height; 1090 - for (let i = 0; i < fallers.index.length; i += 1) { 1091 - const index = fallers.index[i]; 1092 - if (since > fallers.delay[i] && dt > 0) { 1093 - fallers.vy[i] += 1600 * dpr * dt; 1094 - fallers.y[i] += fallers.vy[i] * dt; 1095 - } 1096 - const r = (index / cols) | 0; 1097 - const y = cellY(r) + fallers.y[i]; 1098 - if (y > floor) continue; 1099 - // Tijdens het vallen iets duidelijker, anders zie je ze bijna niet gaan. 1100 - const boost = since > fallers.delay[i] ? 3.5 : 1; 1101 - ctx.globalAlpha = Math.min(0.5, ambient[index] * boost); 1102 - ctx.drawImage(neutralAtlas, glyphs[index] * tileW, 0, tileW, tileH, cellX(index % cols), y, tileW, tileH); 1103 - } 1104 - } 1105 - 1106 - ctx.globalAlpha = 1; 1107 - if (t >= options.nukeNormalAfter + 2) { 1108 - nuke = null; 1109 - fallers = null; 1110 - canvas.style.transform = ""; 1111 - } 1112 - } 1113 - 1114 - function render(now, dt) { 1115 - // Een venster zonder afmeting (geminimaliseerd of verborgen bij het laden): niets tekenen. 1116 - // drawImage van een leeg canvas gooit een fout, en die fout brak hier createGlyphField af. 1117 - // Bij de eerste resize gaat het vanzelf verder. 1118 - if (!neutralAtlas || !cols || !canvas.width || !canvas.height) return; 1119 - const hadTrail = minC <= maxC; 1120 - const ambientDue = now - lastAmbient >= options.ambientIntervalMs; 1121 - 1122 - // Los aangeroepen (thema, formaat) terwijl er niets veranderd is: niets tekenen. 1123 - if (!hadTrail && !ambientDue && dt === 0) return; 1124 - 1125 - if (ambientDue) { 1126 - const elapsed = Number.isFinite(lastAmbient) ? (now - lastAmbient) / 1000 : 0; 1127 - drawAmbient(now, elapsed); 1128 - lastAmbient = now; 1129 - } 1130 - 1131 - ctx.setTransform(1, 0, 0, 1, 0, 0); 1132 - ctx.clearRect(0, 0, canvas.width, canvas.height); 1133 - // Na een explosie ligt het veld stil tot nukeNormalAfter en komt het daarna terug. Een 1134 - // smiley maakt alleen een cirkel om zichzelf stil (quiet), niet het hele veld. 1135 - calm = fieldPresence(now); 1136 - if (calm > 0.01) { 1137 - ctx.globalAlpha = calm; 1138 - ctx.drawImage(layer, 0, 0); 1139 - ctx.globalAlpha = 1; 1140 - } 1141 - if (dt > 0) { 1142 - heartbeat(now); 1143 - heartLine(now); 1144 - drawSmileys(now); 1145 - } 1146 - drawTrail(dt); 1147 - if (nuke) drawNuke(now, dt); 1148 - } 1149 - 1150 - function loop(now) { 1151 - if (!running) return; 1152 - frameId = requestAnimationFrame(loop); 1153 - if (pointer && nukeAge(now) >= options.nukeHeartAfter) paintPointer(); 1154 - else if (nukeAge(now) < options.nukeHeartAfter) pointer = null; 1155 - // Dertig beelden per seconde is genoeg voor wisselende tekens; dat halveert het werk 1156 - // op een 60 Hz-scherm en meer op een sneller scherm. 1157 - if (lastFrame && now - lastFrame < 1000 / options.maxFps - 2) return; 1158 - const dt = lastFrame ? Math.min((now - lastFrame) / 1000, 0.25) : 0; 1159 - lastFrame = now; 1160 - render(now, dt); 1161 - } 1162 - 1163 - function start() { 1164 - if (running || destroyed || reducedMotion.matches || document.hidden) return; 1165 - running = true; 1166 - lastFrame = 0; 1167 - frameId = requestAnimationFrame(loop); 1168 - } 1169 - 1170 - function stop() { 1171 - running = false; 1172 - cancelAnimationFrame(frameId); 1173 - } 1174 - 1175 - function onPointerMove(event) { 1176 - if (reducedMotion.matches) return; 1177 - const next = { x: event.clientX, y: event.clientY }; 1178 - // Na het verlaten van het venster geen streep trekken vanaf de oude plek. 1179 - if (!lastPointer) lastPointer = next; 1180 - pointer = next; 1181 - heartHome = next; 1182 - } 1183 - 1184 - function onPointerLeave() { 1185 - lastPointer = null; 1186 - pointer = null; 1187 - heartHome = null; 1188 - lastBeatSpot = null; 1189 - } 1190 - 1191 - function onVisibility() { 1192 - if (document.hidden) stop(); 1193 - else start(); 1194 - } 1195 - 1196 - function onMotionChange() { 1197 - if (reducedMotion.matches) { 1198 - stop(); 1199 - trail.fill(0); 1200 - pulse.fill(0); 1201 - rings = []; 1202 - smileys = []; 1203 - nuke = null; 1204 - fallers = null; 1205 - canvas.style.transform = ""; 1206 - streak.fill(0); 1207 - lineTrail.fill(0); 1208 - quiet.fill(0); 1209 - minC = minR = 1; 1210 - maxC = maxR = 0; 1211 - lastAmbient = -Infinity; 1212 - render(performance.now(), 0); 1213 - } else { 1214 - start(); 1215 - } 1216 - } 1217 - 1218 - let resizeTimer = 0; 1219 - function onResize() { 1220 - window.clearTimeout(resizeTimer); 1221 - resizeTimer = window.setTimeout(resize, 120); 1222 - } 1223 - 1224 - // Het thema wisselt via data-appearance op <html>; zie layout.css. 1225 - const themeObserver = new MutationObserver(applyTheme); 1226 - themeObserver.observe(document.documentElement, { 1227 - attributes: true, 1228 - attributeFilter: ["data-appearance", "class", "style"], 1229 - }); 1230 - 1231 - window.addEventListener("pointermove", onPointerMove, { passive: true }); 1232 - window.addEventListener("pointerdown", onPointerDown, { passive: true }); 1233 - document.documentElement.addEventListener("pointerleave", onPointerLeave); 1234 - window.addEventListener("blur", onPointerLeave); 1235 - window.addEventListener("resize", onResize); 1236 - document.addEventListener("visibilitychange", onVisibility); 1237 - reducedMotion.addEventListener("change", onMotionChange); 1238 - 1239 - /* Andere intensiteit of ander thema zonder het veld opnieuw te starten. Een nieuw thema zet nieuwe 1240 - tekens in elke cel; een andere intensiteit tekent alleen de rustlaag opnieuw. */ 1241 - function configure(next = {}) { 1242 - if (destroyed) return; 1243 - Object.assign(options, next); 1244 - if (next.glyphs) { 1245 - const nextSet = checkGlyphs(next.glyphs); 1246 - const changed = nextSet.length !== glyphSet.length || nextSet.some((glyph, index) => glyph !== glyphSet[index]); 1247 - if (changed) { 1248 - glyphSet = nextSet; 1249 - for (let i = 0; i < glyphs.length; i += 1) glyphs[i] = randomGlyph(); 1250 - buildAtlases(); 1251 - } 1252 - } 1253 - lastAmbient = -Infinity; 1254 - render(performance.now(), 0); 1255 - } 1256 - 1257 - resize(); 1258 - start(); 1259 - 1260 - return { destroy, configure }; 1261 - 1262 - function destroy() { 1263 - destroyed = true; 1264 - stop(); 1265 - window.clearTimeout(resizeTimer); 1266 - themeObserver.disconnect(); 1267 - window.removeEventListener("pointermove", onPointerMove); 1268 - window.removeEventListener("pointerdown", onPointerDown); 1269 - document.documentElement.removeEventListener("pointerleave", onPointerLeave); 1270 - window.removeEventListener("blur", onPointerLeave); 1271 - window.removeEventListener("resize", onResize); 1272 - document.removeEventListener("visibilitychange", onVisibility); 1273 - reducedMotion.removeEventListener("change", onMotionChange); 1274 - } 1275 -} 1276 - 1277 -// Opstart van het tekenveld op de startpagina van de Kennisbank (Wardenburg.Code.Tekenveld). 1278 -// maak-tekenveld.js zet dit achter de engine van Ember; samen vormen ze de JavaScriptExtension. 1279 -// De engine zelf (createGlyphField, glyphFieldSettings) komt ongewijzigd uit Ember. 1280 - 1281 -// Waar een klik iets doet of waar inhoud staat: geen achtergrond, dus geen smiley. 1282 -const KB_NIET_ACHTERGROND = [ 1283 - "a", 1284 - "button", 1285 - "input", 1286 - "select", 1287 - "textarea", 1288 - "label", 1289 - "form", 1290 - "summary", 1291 - "[role='button']", 1292 - "[role='dialog']", 1293 - ".navbar", 1294 - "#leftPanels", 1295 - "#rightPanels", 1296 - ".document-header", 1297 - ".wb-zoek > *", 1298 - ".wb-tegels li", 1299 - ".wb-home-onder", 1300 -].join(","); 1301 - 1302 -// De blokken van de startpagina; de hartslaglijn loopt bij voorkeur waar die niet staan. 1303 -const KB_BLOKKEN = ".wb-zoek > *, .wb-tegels li, .wb-home-onder, .navbar"; 1304 - 1305 -// Na zoveel tijd zonder muis of aanraking ligt het veld stil (scheelt rekenwerk en 1306 -// beeldverkeer op de terminalserver); bij de eerste beweging loopt het weer. 1307 -const KB_STILSTAND_MS = 60000; 1308 - 1309 -// De explosie van de engine: vijf klikken op de lege achtergrond binnen 1,6 seconde, met een 1310 -// nasleep van een minuut. Dezelfde getallen als de standaard van de engine; hier alleen om 1311 -// tijdens de explosie het vervagen naar het midden weg te halen en het veld wakker te houden. 1312 -const KB_BOEM_KLIKKEN = 5; 1313 -const KB_BOEM_VENSTER_MS = 1600; 1314 -const KB_BOEM_DUUR_MS = 62000; 1315 - 1316 -function kbTekenveld() { 1317 - // Alleen op de startpagina; de extensie wordt ook alleen daar geladen. 1318 - if (!document.querySelector(".wb-home")) return; 1319 - 1320 - // De Kennisbank heeft alleen een lichte stand; de engine leest die van <html>. 1321 - document.documentElement.setAttribute("data-appearance", "light"); 1322 - 1323 - const canvas = document.createElement("canvas"); 1324 - canvas.className = "kb-tekenveld"; 1325 - canvas.setAttribute("aria-hidden", "true"); 1326 - document.body.prepend(canvas); 1327 - 1328 - const isAchtergrond = (event) => event.target instanceof Element && !event.target.closest(KB_NIET_ACHTERGROND); 1329 - const opties = { 1330 - ...glyphFieldSettings({ intensity: "lijst" }), 1331 - isBackground: isAchtergrond, 1332 - obstacles: () => Array.from(document.querySelectorAll(KB_BLOKKEN), (element) => element.getBoundingClientRect()), 1333 - }; 1334 - 1335 - let veld = null; 1336 - let rustTimer = 0; 1337 - let klikken = []; 1338 - let boemTot = 0; 1339 - 1340 - function slaap() { 1341 - // Tijdens een explosie niet: de nasleep duurt een minuut, ook zonder muis. 1342 - if (Date.now() < boemTot) { 1343 - rustTimer = window.setTimeout(slaap, boemTot - Date.now()); 1344 - return; 1345 - } 1346 - // Het laatste beeld blijft staan; alleen de animatie stopt. 1347 - veld.destroy(); 1348 - veld = null; 1349 - } 1350 - 1351 - // De klik die een slapend veld wekt, ziet de engine zelf niet (hij luistert pas daarna); 1352 - // die telt hier dus ook niet mee, anders lopen de twee tellers uiteen. 1353 - let wekklik = null; 1354 - 1355 - function wakker(event) { 1356 - if (!veld) { 1357 - veld = createGlyphField(canvas, opties); 1358 - wekklik = event || null; 1359 - } 1360 - window.clearTimeout(rustTimer); 1361 - rustTimer = window.setTimeout(slaap, KB_STILSTAND_MS); 1362 - } 1363 - 1364 - function telKlik(event) { 1365 - const nu = Date.now(); 1366 - if (event === wekklik || event.button !== 0 || nu < boemTot || !isAchtergrond(event)) return; 1367 - klikken = klikken.filter((tijd) => nu - tijd < KB_BOEM_VENSTER_MS); 1368 - klikken.push(nu); 1369 - if (klikken.length < KB_BOEM_KLIKKEN) return; 1370 - klikken = []; 1371 - boemTot = nu + KB_BOEM_DUUR_MS; 1372 - // Tijdens de explosie het hele scherm, niet alleen de randen. 1373 - canvas.classList.add("kb-tekenveld--boem"); 1374 - window.setTimeout(() => canvas.classList.remove("kb-tekenveld--boem"), KB_BOEM_DUUR_MS); 1375 - } 1376 - 1377 - window.addEventListener("pointermove", wakker, { passive: true }); 1378 - // Eerst wakker maken, zodat de engine de klik ook krijgt als het veld sliep. 1379 - window.addEventListener("pointerdown", wakker, { passive: true }); 1380 - window.addEventListener("pointerdown", telKlik, { passive: true }); 1381 - wakker(); 1382 -} 1383 - 1384 -if (document.readyState === "loading") { 1385 - document.addEventListener("DOMContentLoaded", kbTekenveld); 1386 -} else { 1387 - kbTekenveld(); 1388 -} 1389 - 1390 -})(); 1391 - - name
-
... ... @@ -1,1 +1,0 @@ 1 -Wardenburg tekenveld - parse
-
... ... @@ -1,1 +1,0 @@ 1 -Nee - use
-
... ... @@ -1,1 +1,0 @@ 1 -onDemand
- XWiki.StyleSheetExtension[0]
-
- Caching policy
-
... ... @@ -1,1 +1,0 @@ 1 -long - Code
-
... ... @@ -1,54 +1,0 @@ 1 -/* Tekenveld op de startpagina van de Kennisbank (Wardenburg.Code.Tekenveld); alleen geladen op 2 - de startpagina. Het canvas ligt achter de inhoud; de vlakken eromheen worden doorzichtig. */ 3 - 4 -/* Kleuren voor de engine: de tekstkleur en het donkere oranje (brand-warning) uit het kleurthema; 5 - de hartslaglijn in het rood van het thema (brand-danger). */ 6 -html { 7 - --text: #202124; 8 - --brand-red: #e8920c; 9 - --glyph-line: #d0021b; 10 - background: #ffffff; 11 -} 12 - 13 -body, 14 -#body, 15 -#contentcontainer, 16 -#contentcolumn, 17 -#mainContentArea, 18 -.main, 19 -#xwikicontent, 20 -.wb-home { 21 - background: transparent !important; 22 -} 23 - 24 -canvas.kb-tekenveld { 25 - position: fixed; 26 - inset: 0; 27 - z-index: -1; 28 - pointer-events: none; 29 - /* Naar het midden, waar het zoekveld en de tegels staan, vervaagt het veld tot 40%: het zwakke 30 - grijze veld is daar vrijwel weg, de rode hartslaglijn komt er nog doorheen. */ 31 - -webkit-mask-image: radial-gradient(ellipse 48% 58% at 50% 55%, rgba(0, 0, 0, .4) 45%, #000 100%); 32 - mask-image: radial-gradient(ellipse 48% 58% at 50% 55%, rgba(0, 0, 0, .4) 45%, #000 100%); 33 -} 34 - 35 -/* Tijdens de explosie (vijf snelle klikken) het hele scherm. */ 36 -canvas.kb-tekenveld.kb-tekenveld--boem { 37 - -webkit-mask-image: none; 38 - mask-image: none; 39 -} 40 - 41 -/* Leesbaarheid: een zachte witte gloed achter de kop, het zoekveld en elke tegel. */ 42 -.wb-zoek > h1, 43 -.wb-zoek > p, 44 -.wb-zoek > form, 45 -.wb-tegels > li > a { 46 - background: radial-gradient(closest-side, rgba(255, 255, 255, .97) 62%, rgba(255, 255, 255, 0) 100%); 47 -} 48 - 49 -.wb-home-onder { 50 - background: #ffffff; 51 - border-radius: 16px; 52 - box-shadow: 0 0 36px 28px #ffffff; 53 -} 54 - - Content Type
-
... ... @@ -1,1 +1,0 @@ 1 -CSS - Naam
-
... ... @@ -1,1 +1,0 @@ 1 -Wardenburg tekenveld - Inhoud verwerken
-
... ... @@ -1,1 +1,0 @@ 1 -Nee - Gebruik deze uitbreiding
-
... ... @@ -1,1 +1,0 @@ 1 -onDemand
- XWiki.XWikiRights[0]
-
- allow
-
... ... @@ -1,1 +1,0 @@ 1 -Sta toe - levels
-
... ... @@ -1,1 +1,0 @@ 1 -view - users
-
... ... @@ -1,1 +1,0 @@ 1 -XWiki.XWikiGuest
- XWiki.XWikiRights[1]
-
- allow
-
... ... @@ -1,1 +1,0 @@ 1 -Sta toe - groups
-
... ... @@ -1,1 +1,0 @@ 1 -XWiki.XWikiAllGroup - levels
-
... ... @@ -1,1 +1,0 @@ 1 -view
- XWiki.XWikiRights[2]
-
- allow
-
... ... @@ -1,1 +1,0 @@ 1 -Sta toe - groups
-
... ... @@ -1,1 +1,0 @@ 1 -XWiki.XWikiAdminGroup - levels
-
... ... @@ -1,1 +1,0 @@ 1 -view,edit,comment,script