Wijzigen voor document Tekenveld

Laatst gewijzigd door Jeeves de Butler op 23-10-2026 02:42

Van versie 7.1
gewijzigd door Jeeves de Butler
op 19-10-2026 01:22
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
Opmerking bij wijziging: Er is geen opmerking bij deze versie

Samenvatting

Details

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,45 +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 -html {
6 - --text: #202124;
7 - --brand-red: #e8920c;
8 - background: #ffffff;
9 -}
10 -
11 -body,
12 -#body,
13 -#contentcontainer,
14 -#contentcolumn,
15 -#mainContentArea,
16 -.main,
17 -#xwikicontent,
18 -.wb-home {
19 - background: transparent !important;
20 -}
21 -
22 -canvas.kb-tekenveld {
23 - position: fixed;
24 - inset: 0;
25 - z-index: -1;
26 - pointer-events: none;
27 - /* Naar het midden, waar het zoekveld en de tegels staan, vervaagt het veld. */
28 - -webkit-mask-image: radial-gradient(ellipse 48% 58% at 50% 55%, transparent 45%, #000 100%);
29 - mask-image: radial-gradient(ellipse 48% 58% at 50% 55%, transparent 45%, #000 100%);
30 -}
31 -
32 -/* Leesbaarheid: een zachte witte gloed achter de kop, het zoekveld en elke tegel. */
33 -.wb-zoek > h1,
34 -.wb-zoek > p,
35 -.wb-zoek > form,
36 -.wb-tegels > li > a {
37 - background: radial-gradient(closest-side, rgba(255, 255, 255, .97) 62%, rgba(255, 255, 255, 0) 100%);
38 -}
39 -
40 -.wb-home-onder {
41 - background: #ffffff;
42 - border-radius: 16px;
43 - box-shadow: 0 0 36px 28px #ffffff;
44 -}
45 -
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