Wijzigen voor document Tekenveld
Laatst gewijzigd door Jeeves de Butler op 23-10-2026 02:42
Van versie 15.1
gewijzigd door Jeeves de Butler
op 23-10-2026 02:42
op 23-10-2026 02:42
Opmerking bij wijziging:
Er is geen opmerking bij deze versie
Naar 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
Samenvatting
-
Paginaeigenschappen (1 gewijzigd, 0 toegevoegd, 0 verwijderd)
-
Objecten (2 gewijzigd, 0 toegevoegd, 0 verwijderd)
Details
- Paginaeigenschappen
-
- Inhoud
-
... ... @@ -1,6 +1,6 @@ 1 -Tekenveld op de startpagina en de inlogpaginavan de Kennisbank: een bijna onzichtbaar veld van letters en cijfers achter de inhoud, met een muisspoor, hartslagringen, af en toe een rode ECG-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.Rond feestdagen en herdenkingen (Sint-Maarten, Sinterklaas, Kerst, Nieuwjaar, Pasen, Koningsdag, 4 en 5 mei) neemt een seizoensthema het over, met de kalender uit Ember; ?tekenveld=kerst in de adresbalk toont een thema meteen.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 rode ECG-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. 2 2 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]] en, op de inlogpagina, door de lader [[TekenveldInloggen>>doc:Wardenburg.Code.TekenveldInloggen]].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 5 -Niet hier wijzigen: de bron staat in de skill xwiki-wardenburg-admin (map bron\tekenveld) en wordt gemaakt met maak-tekenveld.js. Uitzetten: opdestartpagina de twee regels met //Wardenburg.Code.Tekenveld//weghalen;opdeinlogpaginadepaginaTekenveldInloggenverwijderen. Zie het [[register van eigen aanpassingen>>doc:Wardenburg.Code.WebHome]].5 +Niet hier wijzigen: de bron staat in de skill xwiki-wardenburg-admin (map bron\tekenveld) en wordt gemaakt met maak-tekenveld.js. Uitzetten: de twee regels met //Wardenburg.Code.Tekenveld// van de startpagina weghalen. Zie het [[register van eigen aanpassingen>>doc:Wardenburg.Code.WebHome]]. 6 6
- XWiki.JavaScriptExtension[0]
-
- code
-
... ... @@ -1,6 +1,5 @@ 1 1 /* Tekenveld voor de Kennisbank. Gemaakt met maak-tekenveld.js uit de skill xwiki-wardenburg-admin. 2 - Engine en seizoenen: Ember src/lib/glyphField.js en src/lib/glyphSeasons.js, commit fdeea1a. 3 - Niet hier wijzigen: pas de bron aan en maak opnieuw. */ 2 + Engine: Ember src/lib/glyphField.js, commit 83ea536. Niet hier wijzigen: pas de bron aan en maak opnieuw. */ 4 4 (function () { 5 5 "use strict"; 6 6 // /src/lib/glyphField.js ... ... @@ -84,10 +84,6 @@ 84 84 // Bepaalt of een klik op de achtergrond viel (en dus een smiley geeft); zonder deze 85 85 // functie telt elke klik. 86 86 isBackground: null, 87 - // Seizoensthema als gegevens (zie src/lib/glyphSeasons.js), of null voor het gewone veld. 88 - season: null, 89 - // Vanaf deze hoogte (px) begint het decor, zodat een slinger niet onder de topbalk verdwijnt. 90 - decorTop: 0, 91 91 // Geeft de rechthoeken (getBoundingClientRect) van de inhoud; de hartslaglijn kiest bij 92 92 // voorkeur een strook waar die niet staan. 93 93 obstacles: null, ... ... @@ -116,9 +116,7 @@ 116 116 streakAccent: { light: 1, dark: 1 }, 117 117 // Smileys: een klik op de achtergrond laat er een opspringen; spammen mag. 118 118 smileyRadius: 145, 119 - // Een smiley of klikvorm zweeft smileyRise px per seconde omhoog en blijft smileySeconds staan. 120 - smileySeconds: 3, 121 - smileyRise: 55, 114 + smileySeconds: 1.5, 122 122 smileyMax: 40, 123 123 // Variatie in de hartslaglijn; een op de zes lijnen is wild. 124 124 ecgWildChance: 0.17, ... ... @@ -206,19 +206,10 @@ 206 206 return y > 0.12 && Math.abs(Math.hypot(x, y + 0.05) - 0.56) < 0.09; 207 207 } 208 208 209 -/* Een hartje als oog: de klassieke hartvorm (x^2 + y^2 - 1)^3 - x^2 y^3 <= 0, met een diepe inkeping 210 - bovenin en een punt onderaan. Alleen de rand, met een lege binnenkant: zo blijft het een hart en 211 - geen vlek. */ 212 -function heartValue(dx, dy) { 213 - const x = dx * 1.15; 214 - const y = -dy * 1.15 + 0.15; 215 - return (x * x + y * y - 1) ** 3 - x * x * y ** 3; 216 -} 217 - 218 218 function inHeartEye(x, y, cx, cy) { 219 - const dx = (x - cx) / 0. 33;220 - const dy = (y - cy) / 0. 33;221 - return heartValue(dx, dy) <=0 &&heartValue(dx/ 0.62,(dy+ 0.06)/ 0.62) > 0;203 + const dx = (x - cx) / 0.2; 204 + const dy = (y - cy) / 0.2; 205 + return Math.hypot(dx, dy) < heartRadius(dx, dy); 222 222 } 223 223 224 224 const SMILEYS = [ ... ... @@ -240,7 +240,7 @@ 240 240 (Math.abs(y - 0.28) < 0.08 && Math.abs(x) < 0.42) || 241 241 (y > 0.28 && inDot(x, y, 0.14, 0.46, 0.21)), 242 242 // verliefd 243 - (x, y) => inHeartEye(x, y, -0.3 6, -0.16) || inHeartEye(x, y, 0.36, -0.16) ||(y > 0.24 &&inSmile(x, y)),227 + (x, y) => inHeartEye(x, y, -0.33, -0.26) || inHeartEye(x, y, 0.33, -0.26) || inSmile(x, y), 244 244 // verbaasd 245 245 (x, y) => 246 246 inDot(x, y, -0.33, -0.3, 0.13) || ... ... @@ -248,8 +248,6 @@ 248 248 Math.abs(Math.hypot(x, y - 0.4) - 0.19) < 0.08, 249 249 ]; 250 250 251 -const SMILEY_VERLIEFD = 4; 252 - 253 253 function smileyAt(kind, x, y) { 254 254 const r = Math.hypot(x, y); 255 255 // De rand is minstens ƩƩn rasterregel dik, anders valt hij onderaan tussen de regels door. ... ... @@ -271,16 +271,6 @@ 271 271 return value < 0 ? 0 : value > 1 ? 1 : value; 272 272 } 273 273 274 -// Geen seizoen: alle seizoensonderdelen staan uit. 275 -const NO_SEASON = Object.freeze({}); 276 -// Tekens die een cel goed vullen; voor figuren en vormen (zie denseGlyph). 277 -const DENSE_GLYPHS = "MWNHBQR8DK0X"; 278 -// Schittering als de glazen klinken. 279 -const CLINK_SYMBOLS = ["\u2726", "\u2727", "\u2605", "*"]; 280 -// Sneeuwvlokjes voor het decor "sneeuw"; de browser valt voor ontbrekende tekens terug op een symbolenletter. 281 -const SNOW_SYMBOLS = ["\u2744", "\u2745", "\u2746", "\u273b", "\u273c", "*"]; 282 -const GOLD_BALL = "#c99a2e"; 283 - 284 284 function readTheme() { 285 285 const root = document.documentElement; 286 286 const styles = getComputedStyle(root); ... ... @@ -318,20 +318,6 @@ 318 318 const options = { ...DEFAULTS, ...overrides }; 319 319 let glyphSet = checkGlyphs(options.glyphs); 320 320 const randomGlyph = () => (Math.random() * glyphSet.length) | 0; 321 - // Figuren, vlag en klikvormen gebruiken vaste, dichte tekens (M, W, 8, ...), zodat de vorm vol en 322 - // strak leest; per cel vast via een hash, zodat niets flikkert. 323 - let denseIndices = []; 324 - function refreshDense() { 325 - denseIndices = []; 326 - glyphSet.forEach((glyph, index) => { 327 - if (DENSE_GLYPHS.includes(glyph)) denseIndices.push(index); 328 - }); 329 - } 330 - refreshDense(); 331 - function denseGlyph(c, r) { 332 - if (!denseIndices.length) return glyphs[r * cols + c]; 333 - return denseIndices[Math.floor(hash(c * 131.7 + r * 7.31) * denseIndices.length)]; 334 - } 335 335 const ctx = canvas.getContext("2d"); 336 336 const layer = document.createElement("canvas"); 337 337 const layerCtx = layer.getContext("2d"); ... ... @@ -376,15 +376,8 @@ 376 376 let smileys = []; 377 377 let calm = 1; 378 378 let nuke = null; 379 - // Proosten op Nieuwjaar (vijf snelle klikken): vuurwerkshow en twee klinkende glazen. 380 - let proost = null; 381 381 let clickTimes = []; 382 382 let fallers = null; 383 - // Tijd tussen twee frames voor het vuurwerk, gezet door drawDecor. 384 - let fxDt = 0; 385 - // De doorkomer van een seizoen (een figuur in plaats van de ECG-lijn) en de atlassen per kleur. 386 - let figure = null; 387 - const paletteAtlases = new Map(); 388 388 let nextEcgAt = performance.now() + randomBetween(options.ecgFirstSeconds) * 1000; 389 389 let frameId = 0; 390 390 let lastFrame = 0; ... ... @@ -392,10 +392,9 @@ 392 392 let running = false; 393 393 let destroyed = false; 394 394 395 - // set: de tekens in de atlas; standaard die van het veld, voor symbolen (sneeuwvlokken) een eigen lijst. 396 - function buildAtlas(color, set = glyphSet) { 346 + function buildAtlas(color) { 397 397 const atlas = document.createElement("canvas"); 398 - atlas.width = tileW * set.length;348 + atlas.width = tileW * glyphSet.length; 399 399 atlas.height = tileH; 400 400 const a = atlas.getContext("2d"); 401 401 a.font = `${options.fontSize * dpr}px ${options.fontFamily}`; ... ... @@ -402,8 +402,8 @@ 402 402 a.textBaseline = "middle"; 403 403 a.textAlign = "center"; 404 404 a.fillStyle = color; 405 - for (let i = 0; i < set.length; i += 1) {406 - a.fillText( set[i], i * tileW + tileW / 2, tileH / 2 + dpr * 0.5);355 + for (let i = 0; i < glyphSet.length; i += 1) { 356 + a.fillText(glyphSet[i], i * tileW + tileW / 2, tileH / 2 + dpr * 0.5); 407 407 } 408 408 return atlas; 409 409 } ... ... @@ -410,13 +410,8 @@ 410 410 411 411 function buildAtlases() { 412 412 neutralAtlas = buildAtlas(theme.text); 413 - // Een seizoen kan het accent en de lijnkleur zetten; anders gelden die van de pagina. 414 - const s = season(); 415 - const accent = s.accent || theme.accent; 416 - const line = s.line || (s.accent ? s.accent : theme.line); 417 - accentAtlas = buildAtlas(accent); 418 - lineAtlas = line === accent ? accentAtlas : buildAtlas(line); 419 - paletteAtlases.clear(); 363 + accentAtlas = buildAtlas(theme.accent); 364 + lineAtlas = theme.line === theme.accent ? accentAtlas : buildAtlas(theme.line); 420 420 // Vuurkleuren voor de explosie. 421 421 yellowAtlas = buildAtlas(theme.mode === "light" ? "#eab308" : "#facc15"); 422 422 orangeAtlas = buildAtlas("#f97316"); ... ... @@ -454,8 +454,6 @@ 454 454 maxC = maxR = 0; 455 455 456 456 buildAtlases(); 457 - // Sneeuw, lampions en eieren opnieuw verdelen over de nieuwe breedte. 458 - resetSeasonFx(); 459 459 lastAmbient = -Infinity; 460 460 render(performance.now(), 0); 461 461 } ... ... @@ -624,17 +624,15 @@ 624 624 /* Hoogte voor de hartslaglijn. Een lichte voorkeur voor stroken waar weinig inhoud staat: 625 625 uit vijf willekeurige hoogtes wint meestal de vrijste, maar een op de vier keer gewoon 626 626 een willekeurige, zodat de lijn overal kan verschijnen. */ 627 - // above en below: hoeveel ruimte boven en onder de lijn vrij moet zijn; randomShare: hoe vaak een 628 - // willekeurige hoogte wint (de ECG-lijn mag overal, een figuur liever niet achter een kaart). 629 - function pickLineY(height, above = options.ecgAmplitude, below = options.ecgAmplitude * 0.4, randomShare = 0.25) { 570 + function pickLineY(height) { 630 630 const candidates = []; 631 - for (let i = 0; i < 9; i += 1) candidates.push(height * (0.15 + Math.random() * 0.8));572 + for (let i = 0; i < 5; i += 1) candidates.push(height * (0.15 + Math.random() * 0.75)); 632 632 const rects = options.obstacles ? options.obstacles() : []; 633 - if (!rects.length || Math.random() < randomShare) return candidates[0];574 + if (!rects.length || Math.random() < 0.25) return candidates[0]; 634 634 635 635 const width = window.innerWidth; 636 - const top = above;637 - const bottom = below;577 + const top = options.ecgAmplitude; 578 + const bottom = options.ecgAmplitude * 0.4; 638 638 let best = candidates[0]; 639 639 let bestCover = Infinity; 640 640 for (const y of candidates) { ... ... @@ -661,17 +661,6 @@ 661 661 662 662 function heartLine(now) { 663 663 if (nuke && now - nuke.start < options.nukeEcgAfter * 1000) return; 664 - const s = season(); 665 - if (s.quiet) return; 666 - // Een seizoen met een doorkomer: die figuur in plaats van de lijn. 667 - if (s.traverser && s.traverser.kind === "vuurwerk") { 668 - runFireworks(now, s.traverser); 669 - return; 670 - } 671 - if (s.traverser) { 672 - runFigure(now, s.traverser); 673 - return; 674 - } 675 675 if (!ecg) { 676 676 if (now < nextEcgAt) return; 677 677 const height = window.innerHeight; ... ... @@ -716,24 +716,10 @@ 716 716 } 717 717 718 718 function spawnSmiley(x, y) { 719 - const art = season().clickArt; 720 - if (art && art.burst) { 721 - spawnBurst(x, y); 722 - return; 723 - } 724 - if (art) { 725 - // Een seizoen geeft zijn eigen vorm bij een klik, in plaats van een smiley. 726 - const shape = art.shapes[(Math.random() * art.shapes.length) | 0]; 727 - smileys.push({ x, y, art: shape, cellScale: art.cellScale || 1, start: performance.now(), tilt: 0 }); 728 - if (smileys.length > options.smileyMax) smileys.splice(0, smileys.length - options.smileyMax); 729 - return; 730 - } 731 731 let kind = (Math.random() * SMILEYS.length) | 0; 732 732 // Niet twee keer achter elkaar dezelfde. 733 733 if (smileys.length && smileys[smileys.length - 1].kind === kind) kind = (kind + 1) % SMILEYS.length; 734 - // De verliefde smiley is groter, anders zijn de hartjes niet te zien. 735 - const size = kind === SMILEY_VERLIEFD ? 1.3 : 1; 736 - smileys.push({ x, y, kind, size, start: performance.now(), tilt: (Math.random() - 0.5) * 0.5 }); 652 + smileys.push({ x, y, kind, start: performance.now(), tilt: (Math.random() - 0.5) * 0.5 }); 737 737 if (smileys.length > options.smileyMax) smileys.splice(0, smileys.length - options.smileyMax); 738 738 } 739 739 ... ... @@ -752,13 +752,9 @@ 752 752 const pop = Math.min(age / 0.22, 1); 753 753 const scale = pop < 1 ? 0.3 + 0.9 * Math.sin((pop * Math.PI) / 2) + 0.1 * Math.sin(pop * Math.PI) : 1; 754 754 const fade = Math.min(1, (options.smileySeconds - age) / 0.45); 671 + const radius = options.smileyRadius * scale; 755 755 const cx = smiley.x; 756 - const cy = smiley.y - age * options.smileyRise; 757 - if (smiley.art) { 758 - drawArtShape(smiley, cx, cy, scale, fade); 759 - continue; 760 - } 761 - const radius = options.smileyRadius * scale * (smiley.size || 1); 673 + const cy = smiley.y - age * 40; 762 762 const cos = Math.cos(smiley.tilt); 763 763 const sin = Math.sin(smiley.tilt); 764 764 // Stille cirkel: binnen 1,1 keer de straal helemaal stil, tot 1,5 keer zacht aflopend. ... ... @@ -782,12 +782,7 @@ 782 782 const still = fade * (d2 < 1.21 ? 1 : (2.25 - d2) / 1.04); 783 783 if (still > quiet[index]) quiet[index] = still; 784 784 } 785 - if (!smileyAt(smiley.kind, x, y)) { 786 - // De smiley zweeft omhoog; wat hij net verliet dooft meteen uit, anders smeert hij uit en lopen 787 - // open vormen (de holle hartjesogen) dicht. 788 - if (d2 < 1.21) streak[index] *= 0.3; 789 - continue; 790 - } 697 + if (!smileyAt(smiley.kind, x, y)) continue; 791 791 if (fade > streak[index]) streak[index] = fade; 792 792 } 793 793 } ... ... @@ -799,14 +799,6 @@ 799 799 function onPointerDown(event) { 800 800 if (reducedMotion.matches || event.button !== 0) return; 801 801 if (options.isBackground && !options.isBackground(event)) return; 802 - // Op een stille dag (4 mei) doet een klik niets. 803 - if (season().quiet) return; 804 - // Pasen: een klik op een verstopt ei laat het openspringen. 805 - const egg = findEgg(event.clientX, event.clientY); 806 - if (egg) { 807 - egg.found = performance.now(); 808 - return; 809 - } 810 810 if (nuke) { 811 811 // In de nasleep geen nieuwe explosie; na nukeSmileyAfter mogen de smileys wel weer. 812 812 if (nukeAge(performance.now()) >= options.nukeSmileyAfter) spawnSmiley(event.clientX, event.clientY); ... ... @@ -816,17 +816,8 @@ 816 816 const now = performance.now(); 817 817 clickTimes = clickTimes.filter((time) => now - time < options.nukeClickWindow * 1000); 818 818 clickTimes.push(now); 819 - // Tijdens een seizoen geen explosie, tenzij het seizoen nuke: true zet. 820 - const s = season(); 821 - // Een seizoen kan vijf snelle klikken een eigen feest geven (Nieuwjaar: proosten). 822 - if (s.fiveClicks === "proost" && clickTimes.length >= options.nukeClicks) { 718 + if (clickTimes.length >= options.nukeClicks) { 823 823 clickTimes = []; 824 - if (!proost) startProost(now); 825 - return; 826 - } 827 - const nukeAllowed = !s.noNuke && (!options.season || s.nuke); 828 - if (nukeAllowed && clickTimes.length >= options.nukeClicks) { 829 - clickTimes = []; 830 830 startNuke(now, event.clientX, event.clientY); 831 831 return; 832 832 } ... ... @@ -852,9 +852,6 @@ 852 852 853 853 function heartbeat(now) { 854 854 if (nukeAge(now) < options.nukeHeartAfter) return; 855 - // Tijdens een seizoen kloppen de hartjesringen niet, tenzij het seizoen heart: true zet. 856 - const s = season(); 857 - if (s.quiet || (options.season && !s.heart)) return; 858 858 if (heartHome) { 859 859 const sinceBeat = now - lastBeatAt; 860 860 const moved = lastBeatSpot ... ... @@ -1061,897 +1061,6 @@ 1061 1061 nuke.puffs.length = keep; 1062 1062 } 1063 1063 1064 - /* ---- Seizoenen (zie src/lib/glyphSeasons.js) ---- 1065 - Alles hieronder volgt de gegevens van options.season; zonder seizoen doet het niets. */ 1066 - 1067 - function season() { 1068 - return options.season || NO_SEASON; 1069 - } 1070 - 1071 - // Een kleur uit een palette: "#rrggbb", "text" (de tekstkleur), met een optionele zichtbaarheid 1072 - // erachter ("text:0.5"). De atlas per kleur wordt een keer gemaakt. 1073 - function paletteAtlas(spec) { 1074 - let entry = paletteAtlases.get(spec); 1075 - if (!entry) { 1076 - const [color, alpha] = String(spec).split(":"); 1077 - const resolved = color === "text" ? theme.text : color; 1078 - entry = { atlas: buildAtlas(resolved), alpha: alpha === undefined ? 1 : Number(alpha) }; 1079 - paletteAtlases.set(spec, entry); 1080 - } 1081 - return entry; 1082 - } 1083 - 1084 - // Een cel van het veld vervangen door een teken in een kleur; het teken van het veld onder die cel 1085 - // gaat eerst weg, zodat er niets dubbel staat. 1086 - function drawCell(c, r, entry, strength = 1, dense = false) { 1087 - if (c < 0 || r < 0 || c >= cols || r >= rows) return; 1088 - const x = cellX(c); 1089 - const y = cellY(r); 1090 - const glyph = dense ? denseGlyph(c, r) : glyphs[r * cols + c]; 1091 - ctx.clearRect(x, y, tileW, tileH); 1092 - ctx.globalAlpha = Math.min(1, entry.alpha * strength * calm); 1093 - ctx.drawImage(entry.atlas, glyph * tileW, 0, tileW, tileH, x, y, tileW, tileH); 1094 - } 1095 - 1096 - // Een atlas met symbolen die niet in het veld zitten (sneeuwvlokken), in een kleur uit een palette. 1097 - function symbolAtlas(spec, symbols) { 1098 - const key = `${spec}|${symbols.join("")}`; 1099 - let entry = paletteAtlases.get(key); 1100 - if (!entry) { 1101 - const [color, alpha] = String(spec).split(":"); 1102 - entry = { atlas: buildAtlas(color === "text" ? theme.text : color, symbols), alpha: alpha === undefined ? 1 : Number(alpha) }; 1103 - paletteAtlases.set(key, entry); 1104 - } 1105 - return entry; 1106 - } 1107 - 1108 - function drawSymbol(c, r, entry, index, strength = 1) { 1109 - if (c < 0 || r < 0 || c >= cols || r >= rows) return; 1110 - const x = cellX(c); 1111 - const y = cellY(r); 1112 - ctx.clearRect(x, y, tileW, tileH); 1113 - ctx.globalAlpha = Math.min(1, entry.alpha * strength * calm); 1114 - ctx.drawImage(entry.atlas, index * tileW, 0, tileW, tileH, x, y, tileW, tileH); 1115 - } 1116 - 1117 - /* Een figuur verkleinen met een factor step (groter dan 1): elke doelcel neemt het vaakst voorkomende 1118 - teken uit zijn blok broncellen; een blok met iets erin blijft gevuld, zodat dunne lijnen niet 1119 - wegvallen. Per figuur en factor een keer berekend. */ 1120 - const scaledFrames = new WeakMap(); 1121 - function scaleFrames(frames, step) { 1122 - let perStep = scaledFrames.get(frames); 1123 - if (!perStep) { 1124 - perStep = new Map(); 1125 - scaledFrames.set(frames, perStep); 1126 - } 1127 - const key = step.toFixed(2); 1128 - let out = perStep.get(key); 1129 - if (out) return out; 1130 - out = frames.map((frame) => { 1131 - const srcW = artWidth(frame); 1132 - const srcH = frame.length; 1133 - const dstW = Math.max(1, Math.round(srcW / step)); 1134 - const dstH = Math.max(1, Math.round(srcH / step)); 1135 - const rows = []; 1136 - for (let r = 0; r < dstH; r += 1) { 1137 - let row = ""; 1138 - const r0 = Math.floor(r * step); 1139 - const r1 = Math.max(r0 + 1, Math.floor((r + 1) * step)); 1140 - for (let c = 0; c < dstW; c += 1) { 1141 - const c0 = Math.floor(c * step); 1142 - const c1 = Math.max(c0 + 1, Math.floor((c + 1) * step)); 1143 - const counts = new Map(); 1144 - for (let yy = r0; yy < r1 && yy < srcH; yy += 1) { 1145 - for (let xx = c0; xx < c1; xx += 1) { 1146 - const ch = frame[yy][xx]; 1147 - if (ch && ch !== " ") counts.set(ch, (counts.get(ch) || 0) + 1); 1148 - } 1149 - } 1150 - let best = " "; 1151 - let bestCount = 0; 1152 - for (const [ch, count] of counts) { 1153 - if (count > bestCount) { 1154 - best = ch; 1155 - bestCount = count; 1156 - } 1157 - } 1158 - row += best; 1159 - } 1160 - rows.push(row); 1161 - } 1162 - return rows; 1163 - }); 1164 - perStep.set(key, out); 1165 - return out; 1166 - } 1167 - 1168 - function artWidth(picture) { 1169 - return picture.reduce((max, row) => Math.max(max, row.length), 0); 1170 - } 1171 - 1172 - // De doorkomer: een figuur die van links naar rechts over het scherm gaat, op dezelfde plekken en 1173 - // tijden als de ECG-lijn. Hier alleen de plaats en de stand; drawFigure tekent hem. 1174 - /* Stroken over de hele breedte waar geen inhoud staat, van boven (onder het decor) naar beneden. Een 1175 - blok inhoud bezet zijn hoogte over de hele breedte, want een figuur trekt van links naar rechts. */ 1176 - function freeBands(height) { 1177 - const rects = (options.obstacles ? options.obstacles() : []) 1178 - .filter((r) => r.bottom > options.decorTop && r.top < height && r.width > 0 && r.height > 0) 1179 - .map((r) => [Math.max(r.top, options.decorTop), Math.min(r.bottom, height)]) 1180 - .sort((a, b) => a[0] - b[0]); 1181 - const bands = []; 1182 - let y = options.decorTop; 1183 - for (const [top, bottom] of rects) { 1184 - if (top > y) bands.push({ top: y, bottom: top }); 1185 - y = Math.max(y, bottom); 1186 - } 1187 - if (y < height) bands.push({ top: y, bottom: height }); 1188 - return bands; 1189 - } 1190 - 1191 - function runFigure(now, part) { 1192 - if (!figure) { 1193 - if (now < nextEcgAt) return; 1194 - const height = window.innerHeight; 1195 - let frames = part.frames; 1196 - /* De figuur gaat over de hele breedte, dus hij moet in een strook zonder inhoud passen. Lopende 1197 - figuren (ground) nemen de strook langs de onderrand, vliegende de hoogste vrije strook. Past de 1198 - figuur (met zijn golf) er niet in, dan wordt hij verkleind, zodat hij helemaal in beeld komt. */ 1199 - const bands = freeBands(height); 1200 - const band = part.ground 1201 - ? bands.find((b) => b.bottom >= height - 2) || bands[bands.length - 1] 1202 - : bands.reduce((best, b) => (!best || b.bottom - b.top > best.bottom - best.top ? b : best), null); 1203 - let wave = part.wave || 0; 1204 - if (band) { 1205 - const room = band.bottom - band.top - 12; 1206 - const need = frames[0].length * cellH + 2 * wave; 1207 - if (room > 8 * cellH && need > room) { 1208 - frames = scaleFrames(frames, need / room); 1209 - wave = wave * (room / need); 1210 - } 1211 - } 1212 - const heightPx = frames[0].length * cellH; 1213 - const widthPx = Math.max(...frames.map(artWidth)) * cellW; 1214 - const baseY = band ? band.bottom - 6 - wave : height - 6; 1215 - figure = { start: now, baseY, widthPx, heightPx, frames, wave, x: -widthPx, lift: 0, frame: 0 }; 1216 - } 1217 - 1218 - const elapsed = now - figure.start; 1219 - const progress = elapsed / (part.crossSeconds * 1000); 1220 - if (progress >= 1) { 1221 - figure = null; 1222 - nextEcgAt = now + randomBetween(part.everySeconds || options.ecgEverySeconds) * 1000; 1223 - return; 1224 - } 1225 - figure.x = -figure.widthPx + (window.innerWidth + figure.widthPx) * progress; 1226 - figure.frame = Math.floor(elapsed / part.frameMs) % figure.frames.length; 1227 - /* Spoor: sterren die achter de figuur ontstaan en in een golf blijven hangen, zachtjes twinkelen en 1228 - uitdoven (de kerstman). De golf volgt de tijd, zodat het spoor als een lint achter de slee kronkelt. */ 1229 - if (part.trail) { 1230 - const t = part.trail; 1231 - const since = figure.lastTrail ? (now - figure.lastTrail) / 1000 : 0; 1232 - figure.lastTrail = now; 1233 - let due = (figure.trailDebt || 0) + since * t.perSecond; 1234 - const fromX = figure.x + figure.widthPx * (t.at ? t.at[0] : 0); 1235 - const fromY = figure.baseY - figure.lift - figure.heightPx * (t.at ? t.at[1] : 0.5); 1236 - const wave = Math.sin(now / (t.waveMs || 380)) * (t.waveAmp || 26); 1237 - while (due >= 1) { 1238 - due -= 1; 1239 - sparks.push({ 1240 - x: fromX + (Math.random() - 0.5) * 10, 1241 - y: fromY + wave + (Math.random() - 0.5) * (t.spread || 14), 1242 - vx: -(10 + Math.random() * 20), 1243 - vy: (Math.random() - 0.5) * 8, 1244 - g: 0, 1245 - born: now, 1246 - life: (t.life || 2.2) * (0.7 + Math.random() * 0.6), 1247 - color: t.colors[(Math.random() * t.colors.length) | 0], 1248 - symbols: t.symbols, 1249 - symbol: (Math.random() * t.symbols.length) | 0, 1250 - twinkle: Math.random() * Math.PI * 2, 1251 - }); 1252 - } 1253 - figure.trailDebt = due; 1254 - if (sparks.length > 900) sparks.splice(0, sparks.length - 900); 1255 - } 1256 - // Strooien: deeltjes die achter de figuur uit vliegen en omlaag tuimelen (pepernoten). 1257 - if (part.scatter) { 1258 - const since = figure.lastScatter ? (now - figure.lastScatter) / 1000 : 0; 1259 - figure.lastScatter = now; 1260 - let due = (figure.scatterDebt || 0) + since * part.scatter.perSecond; 1261 - const fromX = figure.x + figure.widthPx * (part.scatter.at ? part.scatter.at[0] : 0.5); 1262 - const fromY = figure.baseY - figure.heightPx * (part.scatter.at ? part.scatter.at[1] : 0.5); 1263 - while (due >= 1) { 1264 - due -= 1; 1265 - const colors = part.scatter.colors; 1266 - sparks.push({ 1267 - x: fromX + (Math.random() - 0.5) * 20, 1268 - y: fromY, 1269 - vx: -(40 + Math.random() * 110), 1270 - vy: -(90 + Math.random() * 120), 1271 - g: 340, 1272 - dense: true, 1273 - born: now, 1274 - life: 1.8 + Math.random() * 0.8, 1275 - color: colors[(Math.random() * colors.length) | 0], 1276 - }); 1277 - } 1278 - figure.scatterDebt = due; 1279 - if (sparks.length > 900) sparks.splice(0, sparks.length - 900); 1280 - } 1281 - const bob = (part.bob || 0) * Math.abs(Math.sin((elapsed / part.frameMs) * Math.PI)); 1282 - const wave = (figure.wave || 0) * Math.sin((elapsed / ((part.waveSeconds || 1) * 1000)) * Math.PI * 2); 1283 - figure.lift = bob + wave; 1284 - } 1285 - 1286 - function drawFigure() { 1287 - const part = season().traverser; 1288 - if (!figure || !part) return; 1289 - const picture = figure.frames[figure.frame]; 1290 - const c0 = Math.round(figure.x / cellW); 1291 - const r0 = Math.round((figure.baseY - figure.heightPx - figure.lift) / cellH); 1292 - for (let i = 0; i < picture.length; i += 1) { 1293 - const row = picture[i]; 1294 - for (let j = 0; j < row.length; j += 1) { 1295 - const ch = row[j]; 1296 - if (ch === " ") continue; 1297 - drawCell(c0 + j, r0 + i, paletteAtlas(part.palette[ch]), 1, true); 1298 - } 1299 - } 1300 - ctx.globalAlpha = 1; 1301 - } 1302 - 1303 - // Een klikvorm van tekst-art: zelfde opspringen en uitdoven als een smiley. Elke letter van de vorm 1304 - // beslaat cellScale bij cellScale cellen; de vorm komt in het korte spoor, in de accentkleur. 1305 - function drawArtShape(shape, cx, cy, scale, fade) { 1306 - const picture = shape.art; 1307 - const unitW = cellW * shape.cellScale * scale; 1308 - const unitH = cellH * shape.cellScale * scale; 1309 - const widthPx = artWidth(picture) * unitW; 1310 - const heightPx = picture.length * unitH; 1311 - const left = cx - widthPx / 2; 1312 - const top = cy - heightPx / 2; 1313 - // Stille cirkel om de vorm, net als bij een smiley. 1314 - const radius = Math.max(widthPx, heightPx) * 0.62; 1315 - const reach = radius * 1.4; 1316 - const c0 = Math.max(0, Math.floor((cx - reach) / cellW)); 1317 - const c1 = Math.min(cols - 1, Math.ceil((cx + reach) / cellW)); 1318 - const r0 = Math.max(0, Math.floor((cy - reach) / cellH)); 1319 - const r1 = Math.min(rows - 1, Math.ceil((cy + reach) / cellH)); 1320 - if (c0 > c1 || r0 > r1) return; 1321 - 1322 - for (let r = r0; r <= r1; r += 1) { 1323 - const py = r * cellH + cellH / 2; 1324 - for (let c = c0; c <= c1; c += 1) { 1325 - const px = c * cellW + cellW / 2; 1326 - const index = r * cols + c; 1327 - const d2 = ((px - cx) ** 2 + (py - cy) ** 2) / (radius * radius); 1328 - if (d2 < 1.96) { 1329 - const still = fade * (d2 < 1 ? 1 : (1.96 - d2) / 0.96); 1330 - if (still > quiet[index]) quiet[index] = still; 1331 - } 1332 - } 1333 - } 1334 - extendBox(c0, c1, r0, r1); 1335 - // Het tekenen zelf gebeurt na het spoor (drawArtShapes), in de kleuren van de palette. 1336 - shape.place = { left, top, unitW, unitH, fade }; 1337 - } 1338 - 1339 - function drawArtShapes() { 1340 - const art = season().clickArt; 1341 - if (!art) return; 1342 - for (const shape of smileys) { 1343 - if (!shape.art || !shape.place) continue; 1344 - const { left, top, unitW, unitH, fade } = shape.place; 1345 - const picture = shape.art; 1346 - const c0 = Math.max(0, Math.floor(left / cellW)); 1347 - const c1 = Math.min(cols - 1, Math.ceil((left + artWidth(picture) * unitW) / cellW)); 1348 - const r0 = Math.max(0, Math.floor(top / cellH)); 1349 - const r1 = Math.min(rows - 1, Math.ceil((top + picture.length * unitH) / cellH)); 1350 - for (let r = r0; r <= r1; r += 1) { 1351 - const ay = Math.floor((r * cellH + cellH / 2 - top) / unitH); 1352 - const row = picture[ay]; 1353 - if (!row) continue; 1354 - for (let c = c0; c <= c1; c += 1) { 1355 - const ch = row[Math.floor((c * cellW + cellW / 2 - left) / unitW)]; 1356 - if (!ch || ch === " ") continue; 1357 - drawCell(c, r, paletteAtlas(art.palette[ch] || "text"), fade, true); 1358 - } 1359 - } 1360 - } 1361 - ctx.globalAlpha = 1; 1362 - } 1363 - 1364 - /* ---- Bewegend decor en vuurwerk van een seizoen ---- 1365 - Deeltjes in pixels; ze worden per frame op het raster gezet. fxTime houdt de tijd tussen frames bij, 1366 - zodat de snelheid niet van de framerate afhangt. */ 1367 - 1368 - let fxTime = 0; 1369 - let flakes = null; 1370 - let lanterns = null; 1371 - let eggs = null; 1372 - let rockets = []; 1373 - let sparks = []; 1374 - 1375 - function resetSeasonFx() { 1376 - proost = null; 1377 - nextShowerAt = 0; 1378 - flakes = null; 1379 - lanterns = null; 1380 - eggs = null; 1381 - rockets = []; 1382 - sparks = []; 1383 - } 1384 - 1385 - function fxStep(now) { 1386 - const dt = fxTime ? Math.min((now - fxTime) / 1000, 0.1) : 0; 1387 - fxTime = now; 1388 - return dt; 1389 - } 1390 - 1391 - // Sneeuw: tekens die zacht naar beneden dwarrelen; wat onder uit beeld valt, begint weer bovenaan. 1392 - function drawSnow(now, item, dt) { 1393 - const width = window.innerWidth; 1394 - const height = window.innerHeight; 1395 - const symbols = item.symbols || SNOW_SYMBOLS; 1396 - if (!flakes) { 1397 - // Een deel van de vlokken is een echt sneeuwvlokje, de rest een letter uit het veld. 1398 - const share = item.symbolShare === undefined ? 0.5 : item.symbolShare; 1399 - flakes = Array.from({ length: item.count || 60 }, () => ({ 1400 - x: Math.random() * width, 1401 - y: options.decorTop + Math.random() * (height - options.decorTop), 1402 - speed: 18 + Math.random() * 26, 1403 - drift: Math.random() * Math.PI * 2, 1404 - symbol: Math.random() < share ? (Math.random() * symbols.length) | 0 : -1, 1405 - })); 1406 - } 1407 - const flake = paletteAtlas(item.color || "text:0.4"); 1408 - const symbolEntry = symbolAtlas(item.symbolColor || "text:0.6", symbols); 1409 - for (const f of flakes) { 1410 - f.y += f.speed * dt; 1411 - f.x += Math.sin(now / 1400 + f.drift) * 10 * dt; 1412 - if (f.y > height) { 1413 - f.y = options.decorTop; 1414 - f.x = Math.random() * width; 1415 - } 1416 - if (f.symbol >= 0) drawSymbol(Math.floor(f.x / cellW), Math.floor(f.y / cellH), symbolEntry, f.symbol); 1417 - else drawCell(Math.floor(f.x / cellW), Math.floor(f.y / cellH), flake); 1418 - } 1419 - } 1420 - 1421 - // Lampionnetjes die van onder naar boven zweven, licht heen en weer, en zacht gloeien. 1422 - function drawLanterns(now, item, dt) { 1423 - const width = window.innerWidth; 1424 - const height = window.innerHeight; 1425 - const picture = item.art; 1426 - const heightPx = picture.length * cellH; 1427 - if (!lanterns) { 1428 - lanterns = Array.from({ length: item.count || 6 }, (_, i) => ({ 1429 - x: ((i + 0.5) / (item.count || 6)) * width + (Math.random() - 0.5) * 80, 1430 - // Bij de start al over de hele hoogte verdeeld, niet allemaal onder het scherm. 1431 - y: options.decorTop + Math.random() * (height - options.decorTop + heightPx), 1432 - speed: 22 + Math.random() * 16, 1433 - sway: Math.random() * Math.PI * 2, 1434 - })); 1435 - } 1436 - for (const l of lanterns) { 1437 - l.y -= l.speed * dt; 1438 - if (l.y < options.decorTop - heightPx) { 1439 - l.y = height + heightPx; 1440 - l.x = Math.random() * width; 1441 - } 1442 - const x = l.x + Math.sin(now / 1800 + l.sway) * 18; 1443 - const glow = 0.75 + 0.25 * Math.sin(now / 300 + l.sway * 3); 1444 - const c0 = Math.round(x / cellW); 1445 - const r0 = Math.round(l.y / cellH); 1446 - picture.forEach((row, i) => { 1447 - for (let j = 0; j < row.length; j += 1) { 1448 - const ch = row[j]; 1449 - if (ch === " ") continue; 1450 - drawCell(c0 + j, r0 + i, paletteAtlas(item.palette[ch]), ch === "K" ? 1 : glow, true); 1451 - } 1452 - }); 1453 - } 1454 - } 1455 - 1456 - // Eieren verstopt in het veld: half zichtbaar op vrije plekken. Een klik erop (findEgg) laat het 1457 - // openspringen; daarna ligt het even later ergens anders. 1458 - function placeEgg(egg, item) { 1459 - const width = window.innerWidth; 1460 - const height = window.innerHeight; 1461 - const rects = options.obstacles ? options.obstacles() : []; 1462 - const w = artWidth(item.art) * cellW; 1463 - const h = item.art.length * cellH; 1464 - for (let tries = 0; tries < 40; tries += 1) { 1465 - const x = 10 + Math.random() * (width - w - 20); 1466 - const y = options.decorTop + 20 + Math.random() * (height - h - options.decorTop - 30); 1467 - const hit = rects.some((r) => x < r.right && x + w > r.left && y < r.bottom && y + h > r.top); 1468 - if (!hit || tries === 39) { 1469 - egg.x = x; 1470 - egg.y = y; 1471 - break; 1472 - } 1473 - } 1474 - egg.color = item.colors[(Math.random() * item.colors.length) | 0]; 1475 - egg.stripe = item.colors[(Math.random() * item.colors.length) | 0]; 1476 - egg.found = 0; 1477 - } 1478 - 1479 - function drawEggs(now, item) { 1480 - if (!eggs) { 1481 - eggs = Array.from({ length: item.count || 5 }, () => { 1482 - const egg = {}; 1483 - placeEgg(egg, item); 1484 - return egg; 1485 - }); 1486 - } 1487 - for (const egg of eggs) { 1488 - let strength = 0.45; 1489 - let lift = 0; 1490 - if (egg.found) { 1491 - const age = (now - egg.found) / 1000; 1492 - if (age > 4) { 1493 - placeEgg(egg, item); 1494 - } else { 1495 - // Gevonden: helder, een sprongetje, en daarna weg. 1496 - strength = age < 1.4 ? 1 : 0; 1497 - lift = age < 0.6 ? Math.sin((age / 0.6) * Math.PI) * 14 : 0; 1498 - } 1499 - } 1500 - if (strength <= 0) continue; 1501 - const c0 = Math.round(egg.x / cellW); 1502 - const r0 = Math.round((egg.y - lift) / cellH); 1503 - item.art.forEach((row, i) => { 1504 - for (let j = 0; j < row.length; j += 1) { 1505 - const ch = row[j]; 1506 - if (ch === " ") continue; 1507 - drawCell(c0 + j, r0 + i, paletteAtlas(ch === "D" ? egg.stripe : egg.color), strength, true); 1508 - } 1509 - }); 1510 - } 1511 - } 1512 - 1513 - function findEgg(x, y) { 1514 - if (!eggs) return null; 1515 - const decor = season().decor; 1516 - const item = (Array.isArray(decor) ? decor : [decor]).find((d) => d && d.kind === "eieren"); 1517 - if (!item) return null; 1518 - const w = artWidth(item.art) * cellW; 1519 - const h = item.art.length * cellH; 1520 - return eggs.find((egg) => !egg.found && x >= egg.x - 10 && x <= egg.x + w + 10 && y >= egg.y - 10 && y <= egg.y + h + 10) || null; 1521 - } 1522 - 1523 - const FIREWORK_COLORS = ["#e0b000", "#c8102e", "#3b82c4", "#2f7d32", "#f97316", "#c9a7eb"]; 1524 - 1525 - /* Buien: af en toe spat ergens op een vrije plek een handvol deeltjes uit elkaar die omlaag tuimelen 1526 - (pepernoten). */ 1527 - let nextShowerAt = 0; 1528 - function burstShower(now, item) { 1529 - if (!nextShowerAt) nextShowerAt = now + randomBetween(item.firstSeconds || [1, 3]) * 1000; 1530 - if (now < nextShowerAt) return; 1531 - nextShowerAt = now + randomBetween(item.everySeconds || [4, 9]) * 1000; 1532 - const width = window.innerWidth; 1533 - const height = window.innerHeight; 1534 - // Een vrije plek: niet achter de inhoud (met marge), anders zie je de bui niet. 1535 - const rects = options.obstacles ? options.obstacles() : []; 1536 - const margin = 60; 1537 - let x = width * 0.12; 1538 - let y = height * 0.75; 1539 - for (let tries = 0; tries < 40; tries += 1) { 1540 - const px = width * (0.05 + Math.random() * 0.9); 1541 - const py = options.decorTop + 80 + Math.random() * (height - options.decorTop - 160); 1542 - const hit = rects.some((r) => px > r.left - margin && px < r.right + margin && py > r.top - margin && py < r.bottom + margin); 1543 - if (!hit) { 1544 - x = px; 1545 - y = py; 1546 - break; 1547 - } 1548 - } 1549 - const count = item.count || 24; 1550 - for (let i = 0; i < count; i += 1) { 1551 - const angle = Math.random() * Math.PI * 2; 1552 - const speed = 60 + Math.random() * 190; 1553 - // Grote en kleine noten door elkaar: een cel, twee cellen breed, of twee bij twee. 1554 - const roll = Math.random(); 1555 - sparks.push({ 1556 - x, 1557 - y, 1558 - vx: Math.cos(angle) * speed, 1559 - vy: Math.sin(angle) * speed - 80, 1560 - g: 300, 1561 - dense: true, 1562 - w: roll < 0.3 ? 1 : 2, 1563 - h: roll > 0.75 ? 2 : 1, 1564 - born: now, 1565 - life: 2.4 + Math.random() * 1.2, 1566 - color: item.colors[(Math.random() * item.colors.length) | 0], 1567 - }); 1568 - } 1569 - if (sparks.length > 900) sparks.splice(0, sparks.length - 900); 1570 - } 1571 - 1572 - // Een vuurwerkbol: een ring van vonken die uitwaaieren, vallen en uitdoven. 1573 - function spawnBurst(x, y) { 1574 - const color = FIREWORK_COLORS[(Math.random() * FIREWORK_COLORS.length) | 0]; 1575 - const second = FIREWORK_COLORS[(Math.random() * FIREWORK_COLORS.length) | 0]; 1576 - const count = 46; 1577 - for (let i = 0; i < count; i += 1) { 1578 - const angle = (i / count) * Math.PI * 2 + Math.random() * 0.1; 1579 - const speed = 120 + Math.random() * 90; 1580 - sparks.push({ x, y, vx: Math.cos(angle) * speed, vy: Math.sin(angle) * speed, born: performance.now(), life: 1.3 + Math.random() * 0.5, color: i % 3 ? color : second }); 1581 - } 1582 - if (sparks.length > 900) sparks.splice(0, sparks.length - 900); 1583 - } 1584 - 1585 - // Vuurpijlen als doorkomer: af en toe een of twee pijlen die opstijgen en openspatten. 1586 - /* ---- Proosten op Nieuwjaar (vijf snelle klikken) ---- 1587 - Zes seconden een vuurwerkshow over de hele achtergrond. In de grootste vrije strook schuiven twee 1588 - kristallen glazen naar elkaar toe, kantelen en klinken; op dat moment schittert het en ploffen er twee 1589 - extra bollen. Daarna dooft alles uit. */ 1590 - const PROOST_SHOW = 6; 1591 - const PROOST_IN = 1.1; 1592 - const PROOST_TILT = 1.7; 1593 - const PROOST_OUT = 4.2; 1594 - const PROOST_END = 5; 1595 - 1596 - function mirrorFrames(frames) { 1597 - const width = Math.max(...frames.map(artWidth)); 1598 - return frames.map((frame) => frame.map((row) => row.padEnd(width).split("").reverse().join("").replace(/\s+$/, ""))); 1599 - } 1600 - 1601 - function startProost(now) { 1602 - const art = season().proostArt; 1603 - if (!art) return; 1604 - const height = window.innerHeight; 1605 - const bands = freeBands(height); 1606 - const band = bands.reduce((best, b) => (!best || b.bottom - b.top > best.bottom - best.top ? b : best), null) || { top: options.decorTop, bottom: height }; 1607 - let frames = art.frames; 1608 - const room = band.bottom - band.top - 20; 1609 - const need = frames[0].length * cellH; 1610 - if (room > 8 * cellH && need > room) frames = scaleFrames(frames, need / room); 1611 - proost = { 1612 - start: now, 1613 - left: frames, 1614 - right: mirrorFrames(frames), 1615 - palette: art.palette, 1616 - centerX: window.innerWidth / 2, 1617 - bottomY: band.bottom - 10, 1618 - nextRocket: now, 1619 - clinked: false, 1620 - }; 1621 - } 1622 - 1623 - function drawProost(now) { 1624 - const t = (now - proost.start) / 1000; 1625 - const width = window.innerWidth; 1626 - const height = window.innerHeight; 1627 - if (t < PROOST_SHOW) { 1628 - while (now >= proost.nextRocket) { 1629 - launchRocket(width, height, options.decorTop + 30 + Math.random() * height * 0.5); 1630 - proost.nextRocket += 140 + Math.random() * 200; 1631 - } 1632 - } 1633 - if (!proost.clinked && t >= PROOST_TILT) { 1634 - proost.clinked = true; 1635 - const x = proost.centerX; 1636 - const y = proost.bottomY - proost.left[0].length * cellH * 0.82; 1637 - spawnBurst(x - 120, y - 80); 1638 - spawnBurst(x + 120, y - 90); 1639 - // Schittering op het punt waar de glazen elkaar raken. 1640 - for (let i = 0; i < 26; i += 1) { 1641 - const angle = Math.random() * Math.PI * 2; 1642 - const speed = 60 + Math.random() * 140; 1643 - sparks.push({ 1644 - x, 1645 - y, 1646 - vx: Math.cos(angle) * speed, 1647 - vy: Math.sin(angle) * speed, 1648 - g: 1, 1649 - born: now, 1650 - life: 1 + Math.random() * 0.8, 1651 - color: Math.random() < 0.5 ? "#f6d86b" : "#cfe6f5", 1652 - symbols: CLINK_SYMBOLS, 1653 - symbol: (Math.random() * CLINK_SYMBOLS.length) | 0, 1654 - twinkle: Math.random() * Math.PI * 2, 1655 - }); 1656 - } 1657 - } 1658 - if (t > Math.max(PROOST_SHOW, PROOST_END) + 0.5) proost = null; 1659 - } 1660 - 1661 - function drawGlasses(now) { 1662 - const t = (now - proost.start) / 1000; 1663 - if (t >= PROOST_END) return; 1664 - // Stand: rechtop tijdens het binnenschuiven, dan kantelen tot ze klinken. 1665 - const frameCount = proost.left.length; 1666 - const tilt = t < PROOST_IN ? 0 : Math.min(1, (t - PROOST_IN) / (PROOST_TILT - PROOST_IN)); 1667 - const frame = Math.min(frameCount - 1, Math.round(tilt * (frameCount - 1))); 1668 - const slide = t < PROOST_IN ? 1 - easeOut(t / PROOST_IN) : 0; 1669 - const fade = t < PROOST_OUT ? 1 : Math.max(0, 1 - (t - PROOST_OUT) / (PROOST_END - PROOST_OUT)); 1670 - // Na het klinken deinen ze nog even mee. 1671 - const bob = t > PROOST_TILT ? Math.sin((t - PROOST_TILT) * 6) * 3 * Math.max(0, 1 - (t - PROOST_TILT) / 1.5) : 0; 1672 - const left = proost.left[frame]; 1673 - const right = proost.right[frame]; 1674 - const leftW = artWidth(left); 1675 - const centerCol = Math.round(proost.centerX / cellW); 1676 - const offset = Math.round(slide * (window.innerWidth * 0.35) / cellW); 1677 - const r0 = Math.round((proost.bottomY - left.length * cellH + bob) / cellH); 1678 - // Het veld rond de glazen stil, zodat het kristal goed leest. 1679 - const c0 = centerCol - leftW - offset - 2; 1680 - const c1 = centerCol + leftW + offset + 2; 1681 - for (let r = Math.max(0, r0 - 1); r <= Math.min(rows - 1, r0 + left.length); r += 1) { 1682 - for (let c = Math.max(0, c0); c <= Math.min(cols - 1, c1); c += 1) { 1683 - const index = r * cols + c; 1684 - if (fade > quiet[index]) quiet[index] = fade; 1685 - } 1686 - } 1687 - extendBox(Math.max(0, c0), Math.min(cols - 1, c1), Math.max(0, r0 - 1), Math.min(rows - 1, r0 + left.length)); 1688 - const paint = (picture, startCol) => { 1689 - picture.forEach((row, i) => { 1690 - for (let j = 0; j < row.length; j += 1) { 1691 - const ch = row[j]; 1692 - if (ch === " ") continue; 1693 - drawCell(startCol + j, r0 + i, paletteAtlas(proost.palette[ch]), fade, true); 1694 - } 1695 - }); 1696 - }; 1697 - paint(left, centerCol - leftW - offset); 1698 - paint(right, centerCol + offset); 1699 - ctx.globalAlpha = 1; 1700 - } 1701 - function launchRocket(width, height, top) { 1702 - rockets.push({ 1703 - x: width * (0.06 + Math.random() * 0.88), 1704 - y: height, 1705 - top: top === undefined ? options.decorTop + 40 + Math.random() * height * 0.4 : top, 1706 - speed: 380 + Math.random() * 160, 1707 - }); 1708 - } 1709 - 1710 - function launchSidewaysRocket(width, height) { 1711 - const fromLeft = Math.random() < 0.5; 1712 - rockets.push({ 1713 - sideways: true, 1714 - dir: fromLeft ? 1 : -1, 1715 - x: fromLeft ? -10 : width + 10, 1716 - // In de grootste vrije strook, zodat je hem ziet slingeren en ontploffen. 1717 - baseY: (() => { 1718 - const band = freeBands(height).reduce((best, b) => (!best || b.bottom - b.top > best.bottom - best.top ? b : best), null); 1719 - if (!band || band.bottom - band.top < 120) return options.decorTop + 60 + Math.random() * height * 0.45; 1720 - return band.top + 40 + Math.random() * (band.bottom - band.top - 120); 1721 - })(), 1722 - y: 0, 1723 - age: 0, 1724 - speed: 260 + Math.random() * 120, 1725 - endX: width * (fromLeft ? 0.4 + Math.random() * 0.4 : 0.2 + Math.random() * 0.4), 1726 - wobble: 14 + Math.random() * 14, 1727 - }); 1728 - } 1729 - 1730 - function runFireworks(now, part) { 1731 - if (now >= nextEcgAt) { 1732 - const width = window.innerWidth; 1733 - const height = window.innerHeight; 1734 - const n = 1 + (Math.random() < 0.5 ? 1 : 0) + (Math.random() < 0.2 ? 1 : 0); 1735 - for (let i = 0; i < n; i += 1) launchRocket(width, height); 1736 - // Af en toe gaat er een mis: een pijl die horizontaal en slingerend over het scherm schiet. 1737 - if (Math.random() < (part.sideways || 0)) launchSidewaysRocket(width, height); 1738 - nextEcgAt = now + randomBetween(part.everySeconds) * 1000; 1739 - } 1740 - } 1741 - 1742 - function drawFireworks(now, dt) { 1743 - const trailEntry = paletteAtlas("#e0b000"); 1744 - rockets = rockets.filter((rocket) => { 1745 - if (rocket.sideways) { 1746 - // Slingerend en een beetje zakkend; aan het eind ontploft hij alsnog. 1747 - rocket.age += dt; 1748 - rocket.x += rocket.dir * rocket.speed * dt; 1749 - rocket.y = rocket.baseY + Math.sin(rocket.age * 11) * rocket.wobble + rocket.age * rocket.age * 30; 1750 - if ((rocket.dir > 0 && rocket.x >= rocket.endX) || (rocket.dir < 0 && rocket.x <= rocket.endX)) { 1751 - spawnBurst(rocket.x, rocket.y); 1752 - return false; 1753 - } 1754 - const c = Math.floor(rocket.x / cellW); 1755 - const r = Math.floor(rocket.y / cellH); 1756 - drawCell(c, r, trailEntry); 1757 - drawCell(c - rocket.dir, r, trailEntry, 0.7); 1758 - drawCell(c - 2 * rocket.dir, r, trailEntry, 0.45); 1759 - drawCell(c - 3 * rocket.dir, r, trailEntry, 0.25); 1760 - return true; 1761 - } 1762 - rocket.y -= rocket.speed * dt; 1763 - if (rocket.y <= rocket.top) { 1764 - spawnBurst(rocket.x, rocket.y); 1765 - return false; 1766 - } 1767 - const c = Math.floor(rocket.x / cellW); 1768 - const r = Math.floor(rocket.y / cellH); 1769 - drawCell(c, r, trailEntry); 1770 - drawCell(c, r + 1, trailEntry, 0.6); 1771 - drawCell(c, r + 2, trailEntry, 0.3); 1772 - return true; 1773 - }); 1774 - sparks = sparks.filter((spark) => { 1775 - const age = (now - spark.born) / 1000; 1776 - if (age >= spark.life) return false; 1777 - spark.vy += (spark.g || 90) * dt; 1778 - spark.vx *= 1 - 0.8 * dt; 1779 - spark.x += spark.vx * dt; 1780 - spark.y += spark.vy * dt; 1781 - const c = Math.floor(spark.x / cellW); 1782 - const r = Math.floor(spark.y / cellH); 1783 - const alpha = 1 - age / spark.life; 1784 - if (spark.symbols) { 1785 - // Een ster uit het spoor: een symbool dat zacht twinkelt terwijl het uitdooft. 1786 - const glow = 0.65 + 0.35 * Math.sin(now / 140 + spark.twinkle); 1787 - drawSymbol(c, r, symbolAtlas(spark.color, spark.symbols), spark.symbol, alpha * glow); 1788 - return true; 1789 - } 1790 - const entry = paletteAtlas(spark.color); 1791 - for (let i = 0; i < (spark.w || 1); i += 1) { 1792 - for (let j = 0; j < (spark.h || 1); j += 1) drawCell(c + i, r + j, entry, alpha, !!spark.dense); 1793 - } 1794 - return true; 1795 - }); 1796 - ctx.globalAlpha = 1; 1797 - } 1798 - function drawDecor(now) { 1799 - const dt = fxStep(now); 1800 - // De tijdstap voor alle deeltjes (vuurwerk, buien), ook zonder decor. 1801 - fxDt = dt; 1802 - const decor = season().decor; 1803 - if (!decor) return; 1804 - for (const item of Array.isArray(decor) ? decor : [decor]) { 1805 - if (item.kind === "slinger") drawGarland(now, item); 1806 - else if (item.kind === "vlag") drawFlag(now, item); 1807 - else if (item.kind === "sneeuw") drawSnow(now, item, dt); 1808 - else if (item.kind === "lampions") drawLanterns(now, item, dt); 1809 - else if (item.kind === "eieren") drawEggs(now, item); 1810 - else if (item.kind === "buien") burstShower(now, item); 1811 - else if (item.kind === "figuur") drawStill(now, item); 1812 - } 1813 - ctx.globalAlpha = 1; 1814 - } 1815 - 1816 - // Een slinger bovenin: bogen over de hele breedte, met vlaggetjes of lampjes eraan; hij deint zacht. 1817 - function drawGarland(now, item) { 1818 - const width = window.innerWidth; 1819 - const swag = Math.min(320, Math.max(180, width / 5)); 1820 - const string = paletteAtlas("text:0.35"); 1821 - const colors = item.colors || ["text:0.6"]; 1822 - let n = 0; 1823 - for (let k = 0; k * swag < width; k += 1) { 1824 - const x0 = k * swag; 1825 - const sag = 26 + 4 * Math.sin(now / 1300 + k * 1.7); 1826 - const first = Math.floor(x0 / cellW); 1827 - const last = Math.floor((x0 + swag) / cellW); 1828 - for (let c = first; c < last; c += 1) { 1829 - const u = (c * cellW + cellW / 2 - x0) / swag; 1830 - const r = Math.floor((options.decorTop + 6 + sag * 4 * u * (1 - u)) / cellH); 1831 - drawCell(c, r, string); 1832 - if ((c - first) % 3 !== 1 || u < 0.08 || u > 0.92) continue; 1833 - const entry = paletteAtlas(colors[n % colors.length]); 1834 - n += 1; 1835 - if (item.style === "lampjes") { 1836 - drawCell(c, r + 1, entry, 0.6 + 0.4 * Math.sin(now / 400 + c), true); 1837 - } else { 1838 - drawCell(c, r + 1, entry, 1, true); 1839 - drawCell(c + 1, r + 1, entry, 1, true); 1840 - drawCell(c, r + 2, entry, 1, true); 1841 - } 1842 - } 1843 - } 1844 - } 1845 - 1846 - /* De vrijste plek voor iets van w bij h pixels: van een raster van mogelijke plekken die met de minste 1847 - overlap met de inhoud; bij gelijke overlap liever rechts en hoger. Elke twee seconden opnieuw, zodat 1848 - iets mee schuift als de pagina verandert, maar niet elke frame. */ 1849 - let spotCache = null; 1850 - function freeSpot(now, w, h) { 1851 - if (spotCache && spotCache.w === w && spotCache.h === h && now - spotCache.at < 2000) return spotCache; 1852 - const width = window.innerWidth; 1853 - const height = window.innerHeight; 1854 - const rects = options.obstacles ? options.obstacles() : []; 1855 - const minY = options.decorTop + 8; 1856 - let best = null; 1857 - for (let gy = 0; gy <= 6; gy += 1) { 1858 - const y = minY + ((height - h - minY - 8) * gy) / 6; 1859 - for (let gx = 0; gx <= 8; gx += 1) { 1860 - const x = 8 + ((width - w - 16) * (8 - gx)) / 8; 1861 - let cover = 0; 1862 - for (const rect of rects) { 1863 - const ox = Math.min(rect.right, x + w) - Math.max(rect.left, x); 1864 - const oy = Math.min(rect.bottom, y + h) - Math.max(rect.top, y); 1865 - if (ox > 0 && oy > 0) cover += ox * oy; 1866 - } 1867 - if (!best || cover < best.cover - 1) best = { x, y, cover }; 1868 - } 1869 - } 1870 - spotCache = { ...(best || { x: width - w - 8, y: minY }), w, h, at: now }; 1871 - return spotCache; 1872 - } 1873 - 1874 - /* Een stilstaande figuur van tekst-art (de kroon op Koningsdag) op de vrijste plek, met een zachte 1875 - glinstering: steeds lichten een paar cellen op, per cel op een eigen ritme. */ 1876 - function drawStill(now, item) { 1877 - const picture = item.art; 1878 - const w = artWidth(picture); 1879 - const spot = freeSpot(now, (w + 4) * cellW, (picture.length + 2) * cellH); 1880 - const c0 = Math.floor(spot.x / cellW) + 2; 1881 - const r0 = Math.floor(spot.y / cellH) + 1; 1882 - const sparkle = item.sparkle ? symbolAtlas(item.sparkle.color, item.sparkle.symbols) : null; 1883 - picture.forEach((row, i) => { 1884 - for (let j = 0; j < row.length; j += 1) { 1885 - const ch = row[j]; 1886 - if (ch === " ") continue; 1887 - const phase = hash(i * 31.7 + j * 7.1) * Math.PI * 2; 1888 - const shine = Math.sin(now / 420 + phase); 1889 - const glitter = item.glitter && shine > 0.92; 1890 - if (glitter && sparkle) { 1891 - drawSymbol(c0 + j, r0 + i, sparkle, (hash(i + j * 3.3) * item.sparkle.symbols.length) | 0, 1); 1892 - continue; 1893 - } 1894 - drawCell(c0 + j, r0 + i, paletteAtlas(item.palette[ch]), item.glitter ? 0.82 + 0.18 * shine : 1, true); 1895 - } 1896 - }); 1897 - ctx.globalAlpha = 1; 1898 - } 1899 - 1900 - // De Nederlandse vlag aan een mast, op de vrijste plek, in de tekens van het veld. Hij wappert rustig: een 1901 - // golf die naar het vrije einde toe groter wordt, met wat schaduw in de dalen. Halfstok op 4 mei. 1902 - function drawFlag(now, item) { 1903 - let flagCols = Math.min(44, Math.max(26, Math.floor(cols * 0.18))); 1904 - let flagRows = Math.max(12, Math.round((flagCols * cellW * 0.66) / cellH / 3) * 3); 1905 - // Een lange mast (ruim twee keer de vlag), zodat goed te zien is of de vlag in top of halfstok hangt. 1906 - // Past de mast niet in de hoogste vrije strook, dan worden vlag en mast samen kleiner. 1907 - const room = freeBands(window.innerHeight).reduce((max, b) => Math.max(max, b.bottom - b.top), 0); 1908 - const roomRows = Math.floor(room / cellH) - 3; 1909 - if (roomRows > 0 && flagRows * 2.1 > roomRows) { 1910 - flagRows = Math.max(6, Math.floor(roomRows / 2.1 / 3) * 3); 1911 - flagCols = Math.max(14, Math.round((flagRows * cellH * 3) / (cellW * 0.66) / 3)); 1912 - } 1913 - const poleRows = Math.round(flagRows * 2.1); 1914 - // De wimpel is langer dan de vlag; de plek moet voor allebei breed genoeg zijn. 1915 - const reachCols = item.wimpel ? Math.round(flagCols * 1.25) : flagCols; 1916 - // Rechts van de vlag blijft wat ruimte, zodat hij niet tegen de rand van het venster wappert. 1917 - const spot = freeSpot(now, (reachCols + 12) * cellW, (poleRows + 2) * cellH); 1918 - const poleCol = Math.floor(spot.x / cellW); 1919 - const poleTop = Math.max(Math.ceil((options.decorTop + 24) / cellH), Math.floor(spot.y / cellH) + 1); 1920 - // Halfstok: de vlag een derde van de mast omlaag; in top direct onder de knop, of onder de wimpel. 1921 - const pennantRows = item.wimpel ? 3 : 0; 1922 - const flagTop = item.halfstok ? poleTop + Math.round(poleRows * 0.3) : poleTop + 1 + pennantRows; 1923 - const stripe = flagRows / 3; 1924 - const red = paletteAtlas("#ae1c28"); 1925 - const white = paletteAtlas("text:0.22"); 1926 - const blue = paletteAtlas("#21468b"); 1927 - const pole = paletteAtlas("text:0.5"); 1928 - 1929 - drawCell(poleCol, poleTop - 1, paletteAtlas(GOLD_BALL)); 1930 - for (let r = 0; r < poleRows; r += 1) drawCell(poleCol, poleTop + r, pole); 1931 - 1932 - // De oranje wimpel boven de vlag (5 mei): lang en smal, naar het einde toe dunner. 1933 - if (item.wimpel) { 1934 - const orange = paletteAtlas("#f97316"); 1935 - const length = Math.round(flagCols * 1.25); 1936 - for (let j = 0; j < length; j += 1) { 1937 - const phase = j * 0.22 - now / 650; 1938 - const wave = Math.round(Math.sin(phase) * 1.4 * (j / length)); 1939 - const thick = Math.max(1, Math.round(2 * (1 - j / length))); 1940 - for (let i = 0; i < thick; i += 1) drawCell(poleCol + 1 + j, poleTop + i + wave, orange, 0.72 + 0.28 * Math.cos(phase), true); 1941 - } 1942 - } 1943 - 1944 - for (let j = 0; j < flagCols; j += 1) { 1945 - const phase = j * 0.22 - now / 650; 1946 - const free = j / flagCols; 1947 - const wave = Math.round(Math.sin(phase) * 1.6 * free); 1948 - const shade = 0.72 + 0.28 * Math.cos(phase); 1949 - for (let i = 0; i < flagRows; i += 1) { 1950 - const entry = i < stripe ? red : i < stripe * 2 ? white : blue; 1951 - drawCell(poleCol + 1 + j, flagTop + i + wave, entry, shade, true); 1952 - } 1953 - } 1954 - } 1955 1955 // 1 als het veld er gewoon staat; 0 tijdens de nasleep van een explosie. 1956 1956 function fieldPresence(now) { 1957 1957 const age = nukeAge(now); ... ... @@ -2142,7 +2142,6 @@ 2142 2142 ctx.drawImage(layer, 0, 0); 2143 2143 ctx.globalAlpha = 1; 2144 2144 } 2145 - drawDecor(now); 2146 2146 if (dt > 0) { 2147 2147 heartbeat(now); 2148 2148 heartLine(now); ... ... @@ -2149,11 +2149,6 @@ 2149 2149 drawSmileys(now); 2150 2150 } 2151 2151 drawTrail(dt); 2152 - drawArtShapes(); 2153 - drawFigure(); 2154 - if (proost) drawProost(now); 2155 - if (rockets.length || sparks.length) drawFireworks(now, fxDt); 2156 - if (proost) drawGlasses(now); 2157 2157 if (nuke) drawNuke(now, dt); 2158 2158 } 2159 2159 ... ... @@ -2160,9 +2160,6 @@ 2160 2160 function loop(now) { 2161 2161 if (!running) return; 2162 2162 frameId = requestAnimationFrame(loop); 2163 - // Op een stille dag en tijdens een seizoen geen muisspoor (tenzij het seizoen trail: true zet): 2164 - // zo komt de figuur beter tot zijn recht. 2165 - if (season().quiet || (options.season && !season().trail)) pointer = null; 2166 2166 if (pointer && nukeAge(now) >= options.nukeHeartAfter) paintPointer(); 2167 2167 else if (nukeAge(now) < options.nukeHeartAfter) pointer = null; 2168 2168 // Dertig beelden per seconde is genoeg voor wisselende tekens; dat halveert het werk ... ... @@ -2254,21 +2254,11 @@ 2254 2254 function configure(next = {}) { 2255 2255 if (destroyed) return; 2256 2256 Object.assign(options, next); 2257 - if ("season" in next) { 2258 - // Ander seizoen: andere kleuren, en de doorkomer, het decor en de klikvormen beginnen opnieuw. 2259 - figure = null; 2260 - resetSeasonFx(); 2261 - smileys = []; 2262 - const first = (options.season && options.season.traverser && options.season.traverser.firstSeconds) || options.ecgFirstSeconds; 2263 - nextEcgAt = performance.now() + randomBetween(first) * 1000; 2264 - buildAtlases(); 2265 - } 2266 2266 if (next.glyphs) { 2267 2267 const nextSet = checkGlyphs(next.glyphs); 2268 2268 const changed = nextSet.length !== glyphSet.length || nextSet.some((glyph, index) => glyph !== glyphSet[index]); 2269 2269 if (changed) { 2270 2270 glyphSet = nextSet; 2271 - refreshDense(); 2272 2272 for (let i = 0; i < glyphs.length; i += 1) glyphs[i] = randomGlyph(); 2273 2273 buildAtlases(); 2274 2274 } ... ... @@ -2297,1132 +2297,10 @@ 2297 2297 } 2298 2298 } 2299 2299 2300 -// /src/lib/glyphSeasons.js 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. 2301 2301 2302 -/* Seizoensthema's van het tekenveld: welke periode welk thema heeft, en wat dat thema is. Dit bestand is 2303 - puur (geen browser, geen engine), zodat de datumregels zonder browser te testen zijn 2304 - (tests/glyphSeasons.test.mjs). De engine (src/lib/glyphField.js) kent geen thema's bij naam; hij krijgt 2305 - een seizoen als gegevens (optie season) en tekent wat erin staat: 2306 - 2307 - - accent en line: de kleur van het muisspoor, de ringen en de lijn; 2308 - - traverser: iets wat af en toe over het scherm gaat, in plaats van de ECG-lijn: een figuur van 2309 - tekst-art (frames; ground: over de onderrand in plaats van door een vrije strook; trail: een golvend 2310 - spoor van symbolen erachter), of vuurpijlen (kind: "vuurwerk"); 2311 - - decor: wat blijft staan of steeds terugkomt: een stilstaande figuur (kind "figuur", optioneel 2312 - glinsterend, zoals de kroon), de vlag (halfstok op 4 mei, met de oranje wimpel op 2313 - 5 mei), sneeuw, lampionnetjes die opstijgen, eieren die verstopt liggen, of buien (een handvol 2314 - deeltjes die ergens uit elkaar spat en valt, zoals pepernoten); een slinger bovenin kan 2315 - ook (kind "slinger"), maar leest op celniveau slecht en wordt nu nergens gebruikt; 2316 - - clickArt: wat een klik op de achtergrond geeft, in plaats van een smiley (burst: vuurwerk); 2317 - - quiet: alleen het rustige veld en het decor, zonder spoor, hartslag, lijn, smileys of explosie. 2318 - 2319 - Tijdens elk seizoen staan de hartjesringen, de hartslaglijn, de explosie en het muisspoor uit; het 2320 - feest is de passieve animatie. Een seizoen dat iets toch wil, zet heart: true, nuke: true of trail: true. 2321 - De klikvormen blijven. 2322 - 2323 - Een nieuw thema is een blok gegevens hieronder, geen code. Tekst-art: elke letter is ƩƩn cel van het 2324 - veld; een spatie is leeg, en de letter bepaalt de kleur via de palette ("text" is de tekstkleur van het 2325 - thema, met een optionele zichtbaarheid erachter, bijvoorbeeld "text:0.5"). De cellen zijn ongeveer twee 2326 - keer zo hoog als breed, net als letters in een editor. De grote figuren zijn gemaakt met 2327 - scripts/glyph-sprites.mjs; daar staan ze als vormen, hier als tekst. 2328 - 2329 - Een thema wordt pas vanzelf actief als het klaar is (klaar: true). Met ?tekenveld=<id> in de adresbalk 2330 - is elk thema meteen te zien, ook buiten zijn periode; ?tekenveld=geen zet de thema's uit. */ 2331 - 2332 -/* Paaszondag volgens de gregoriaanse kalender (anonieme algoritme van Meeus/Jones/Butcher). */ 2333 -function easterSunday(year) { 2334 - const a = year % 19; 2335 - const b = Math.floor(year / 100); 2336 - const c = year % 100; 2337 - const d = Math.floor(b / 4); 2338 - const e = b % 4; 2339 - const f = Math.floor((b + 8) / 25); 2340 - const g = Math.floor((b - f + 1) / 3); 2341 - const h = (19 * a + b - d - g + 15) % 30; 2342 - const i = Math.floor(c / 4); 2343 - const k = c % 4; 2344 - const l = (32 + 2 * e + 2 * i - h - k) % 7; 2345 - const m = Math.floor((a + 11 * h + 22 * l) / 451); 2346 - const month = Math.floor((h + l - 7 * m + 114) / 31); 2347 - const day = ((h + l - 7 * m + 114) % 31) + 1; 2348 - return new Date(year, month - 1, day); 2349 -} 2350 - 2351 -/* Koningsdag: 27 april, of 26 april als de 27e op een zondag valt. */ 2352 -function kingsDay(year) { 2353 - const day = new Date(year, 3, 27); 2354 - return day.getDay() === 0 ? new Date(year, 3, 26) : day; 2355 -} 2356 - 2357 -function dayNumber(date) { 2358 - return Math.floor(Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) / 86400000); 2359 -} 2360 - 2361 -function monthDay(text, year) { 2362 - const [month, day] = text.split("-").map(Number); 2363 - return new Date(year, month - 1, day); 2364 -} 2365 - 2366 -/* Valt de datum in de periode van de regel? Regels: 2367 - - { from: "MM-DD", to: "MM-DD" }: vaste dagen, beide ingesloten; mag over de jaarwisseling heen; 2368 - - { easter: [vanaf, tot] }: dagen ten opzichte van Paaszondag, beide ingesloten; 2369 - - { kingsDay: true }: Koningsdag met de zondagregel. */ 2370 -function inPeriod(date, rule) { 2371 - const today = dayNumber(date); 2372 - const year = date.getFullYear(); 2373 - if (rule.kingsDay) return today === dayNumber(kingsDay(year)); 2374 - if (rule.easter) { 2375 - const easter = dayNumber(easterSunday(year)); 2376 - return today >= easter + rule.easter[0] && today <= easter + rule.easter[1]; 2377 - } 2378 - const from = dayNumber(monthDay(rule.from, year)); 2379 - const to = dayNumber(monthDay(rule.to, year)); 2380 - // Over de jaarwisseling: van "12-27" tot "01-14". 2381 - if (from > to) return today >= from || today <= to; 2382 - return today >= from && today <= to; 2383 -} 2384 - 2385 -const RED = "#c8102e"; 2386 -const GOLD = "#c99a2e"; 2387 -const GREEN = "#2f7d32"; 2388 -const ORANGE = "#f97316"; 2389 -const NL_RED = "#ae1c28"; 2390 -const NL_BLUE = "#21468b"; 2391 -const BROWN = "#8b5a2b"; 2392 -const YELLOW = "#e0b000"; 2393 - 2394 -/* De figuren, gemaakt met scripts/glyph-sprites.mjs (node scripts/glyph-sprites.mjs <naam> js). */ 2395 -const SINT_0 = [ 2396 - " R GGGGGGG", 2397 - " RGR GG GG", 2398 - " RRRGRRR G GGGG F FH", 2399 - " RRRRGRRRR G MMMMFH HFH", 2400 - " RRRRRGRRRR G MMMMMMHHHHHFH", 2401 - " RRRRRGRRRR G MMMMMMMHHHHHH", 2402 - " GGGGGGGGGGGGG G MMMMMMHHHHHHHHHHH", 2403 - " SSSSSKS G MMMMM HHHHHHHHHHHHHH", 2404 - " SSSSSSS G MMMMM HHHHHHHHHHHHHHHHH", 2405 - " WWWWWWWWW G MMMMMHHHHHHHHHHHHHHHHHHHHH", 2406 - " RRRRWWWWWWWWW G MMMMMHHHHHHHHHHHHHHHHHHHHKH", 2407 - " RRRRRRWWWWWWWWWR G MMMMMHHHHHHHHHHH HHHHKLLH", 2408 - " RRRRRRRRRWWWWWRRRRRRRR GMMMM HHHHHHHHHHH LLLLL", 2409 - " RRRRRRRRRRRWWWRRRRRRRRRWWGMMMHHHHHHHHHHHLLLL", 2410 - " HHHHHRRRRRRRRRRRWWWRRRRR RWLLLLLHHHHHHHHLLL", 2411 - " HHHHHHHHRRRRRRRRRRRRWWWRRRRR MMMGMHHHHHHHHHHH", 2412 - " MMMMMHHHHHHHHHRRRRRRRRRRRRWWWRRRRRRRRRRMGHHHHHHHHHH", 2413 - " MMMMMMMHHHHHHHHHRRRRRRRRRRRRRWWWRRRRRRRRRRHGHHHHHHHHHH", 2414 - " MMMMMMMMHHHHHHHHHHHHHHHRRRRRRRRRRRRRRRRRRRRRHGHHHHHHHHHHH", 2415 - " MMMMMM HHHHHHHHHHHHHHHGGGGGRRRRRRRGGGGGGGGGHGHHHHHHHHHHH", 2416 - " MMMMM HHHHHHHHHHHHHHHHGHHGRRRRRRRHGHHGHHGHHHHHHHHHHHHHH", 2417 - " MMMM HHHHHHHHHHHHHHHHHHHRRRRRRRHHHHHHHHHHHHHHHHHHHHHH", 2418 - " MMMM HHHHHHHHHHHHHHHHHHRRRRRRRHHHHHHHHHHHHHHHHHHHHH", 2419 - " MMMM HHHHHHHHHHHHHHHHHRRRRRRRHHHHHHHHHHHHHHHHHHHH", 2420 - " MMM HHHHHHFFF HHKKKKKHHHHHH HHHHHHHHHFF", 2421 - " MMM HHHHHFFF KKKKK HHHHFFFF", 2422 - " MMM HHHHFF GGGGGGG HHHHHFFF", 2423 - " MMMM HHHHH HHHHHFF", 2424 - " MMM HHHHH HHHHHF", 2425 - " MMM HHHH HHHHHH", 2426 - " M KKK HKKK", 2427 - "", 2428 -]; 2429 -const SINT_1 = [ 2430 - " R GGGGGGG", 2431 - " RGR GG GG", 2432 - " RRRGRRR G GGGG F FH", 2433 - " RRRRGRRRR G MMMFH HFH", 2434 - " RRRRRGRRRR G MMMMMMHHHHHFH", 2435 - " RRRRRGRRRR G MMMMMMHHHHHH", 2436 - " GGGGGGGGGGGGG G MMMMMMHHHHHHHHHHH", 2437 - " SSSSSKS G MMMMMMHHHHHHHHHHHHHH", 2438 - " SSSSSSS G MMMMM HHHHHHHHHHHHHHHHH", 2439 - " WWWWWWWWW G MMMMMHHHHHHHHHHHHHHHHHHHHH", 2440 - " RRRRWWWWWWWWW G MMMM HHHHHHHHHHHHHHHHHHHHKH", 2441 - " RRRRRRWWWWWWWWWR G MMMM HHHHHHHHHHH HHHHKLLH", 2442 - " RRRRRRRRRWWWWWRRRRRRRR GMMMM HHHHHHHHHHH LLLLL", 2443 - " RRRRRRRRRRRRWWWRRRRRRRRRWWGMMMHHHHHHHHHHHLLLL", 2444 - " HHHHRRRRRRRRRRRRWWWRRRRR RWLLLLLHHHHHHHHLLL", 2445 - " HHHHHHHRRRRRRRRRRRRRWWWRRRRR MMMGMHHHHHHHHHHH", 2446 - " MMMMHHHHHHHHRRRRRRRRRRRRRWWWRRRRRRRRRRMGHHHHHHHHHH", 2447 - " MMMMMMHHHHHHHHRRRRRRRRRRRRRRWWWRRRRRRRRRRHGHHHHHHHHHH", 2448 - " MMMMMMMHHHHHHHHHHHHHHHRRRRRRRRRRRRRRRRRRRRRHGHHHHHHHHHHH", 2449 - " MMMMMMM HHHHHHHHHHHHHHHGGGGGRRRRRRRGGGGGGGGGHGHHHHHHHHHHH", 2450 - " MMMMMM HHHHHHHHHHHHHHHHGHHGRRRRRRRHGHHGHHGHHHHHHHHHHHHHH", 2451 - " MMMM HHHHHHHHHHHHHHHHHHHRRRRRRRHHHHHHHHHHHHHHHHHHHHHH", 2452 - " MMMM HHHHHHHHHHHHHHHHHHRRRRRRRHHHHHHHHHHHHHHHHHHHHH", 2453 - " MMMM HHHHHHHHHHHHHHHHHRRRRRRRHHHHHHHHHHHHHHHHHHHH", 2454 - " MMMM HHHHHHHFFF HHKKKKKHHHHHH HHHHHHHHHF", 2455 - " MMM HHHHHFFFF KKKKK HHHHFFF", 2456 - " MMM HHHH FFFF GGGGGGG HHHHFFF", 2457 - " MMMM KKK FFFFF HHHHHF", 2458 - " MMMMM FFFFF HHHH", 2459 - " MMMM KFFF KKHHHH", 2460 - " M KKK KKK", 2461 - "", 2462 -]; 2463 -const SINT_2 = [ 2464 - " R GGGGGGG", 2465 - " RGR GG GG", 2466 - " RRRGRRR G GGGG F FH", 2467 - " RRRRGRRRR G MMMMFH HFH", 2468 - " RRRRRGRRRR G MMMMMMHHHHHFH", 2469 - " RRRRRGRRRR G MMMMMMMHHHHHH", 2470 - " GGGGGGGGGGGGG G MMMMMMHHHHHHHHHHH", 2471 - " SSSSSKS G MMMMM HHHHHHHHHHHHHH", 2472 - " SSSSSSS G MMMMM HHHHHHHHHHHHHHHHH", 2473 - " WWWWWWWWW G MMMMMHHHHHHHHHHHHHHHHHHHHH", 2474 - " RRRRWWWWWWWWW G MMMMMHHHHHHHHHHHHHHHHHHHHKH", 2475 - " RRRRRRWWWWWWWWWR G MMMMMHHHHHHHHHHH HHHHKLLH", 2476 - " RRRRRRRRRWWWWWRRRRRRRR GMMMM HHHHHHHHHHH LLLLL", 2477 - " RRRRRRRRRRRWWWRRRRRRRRRWWGMMMHHHHHHHHHHHLLLL", 2478 - " HHHHHRRRRRRRRRRRWWWRRRRR RWLLLLLHHHHHHHHLLL", 2479 - " HHHHHHHHRRRRRRRRRRRRWWWRRRRR MMMGMHHHHHHHHHHH", 2480 - " MMMMMHHHHHHHHHRRRRRRRRRRRRWWWRRRRRRRRRRMGHHHHHHHHHH", 2481 - " MMMMMMMHHHHHHHHHRRRRRRRRRRRRRWWWRRRRRRRRRRHGHHHHHHHHHH", 2482 - " MMMMMMMMHHHHHHHHHHHHHHHRRRRRRRRRRRRRRRRRRRRRHGHHHHHHHHHHH", 2483 - " MMMMMM HHHHHHHHHHHHHHHGGGGGRRRRRRRGGGGGGGGGHGHHHHHHHHHHH", 2484 - " MMMMM HHHHHHHHHHHHHHHHGHHGRRRRRRRHGHHGHHGHHHHHHHHHHHHHH", 2485 - " MMMM HHHHHHHHHHHHHHHHHHHRRRRRRRHHHHHHHHHHHHHHHHHHHHHH", 2486 - " MMMM HHHHHHHHHHHHHHHHHHRRRRRRRHHHHHHHHHHHHHHHHHHHHH", 2487 - " MMMM HHHHHHHHHHHHHHHHHRRRRRRRHHHHHHHHHHHHHHHHHHHH", 2488 - " MMM HHHHHHHHFFFF HHKKKKKHHHHHH HHHHHHHHHF", 2489 - " MMM HHHHH FFFF KKKKK HHHHH FFFF", 2490 - " MMM HHHHH FFF GGGGGGG HHHHH FFFF", 2491 - " MMMM HHHHH FFF HHHH KKK", 2492 - " MMM HHHHH FFF HHHH", 2493 - " MMM HHHH FFF KKK", 2494 - " M KKK KKK", 2495 - "", 2496 -]; 2497 -const SINT_3 = [ 2498 - " R GGGGGGG", 2499 - " RGR GG GG", 2500 - " RRRGRRR G GGGG F FH", 2501 - " RRRRGRRRR G MMMMFH HFH", 2502 - " RRRRRGRRRR G MMMMMMMHHHHHFH", 2503 - " RRRRRGRRRR G MMMMMMMHHHHHH", 2504 - " GGGGGGGGGGGGG G MMMMM HHHHHHHHHHH", 2505 - " SSSSSKS G MMMMM HHHHHHHHHHHHHH", 2506 - " SSSSSSS G MMMMM HHHHHHHHHHHHHHHHH", 2507 - " WWWWWWWWW G MMMMMHHHHHHHHHHHHHHHHHHHHH", 2508 - " RRRRWWWWWWWWW G MMMMMHHHHHHHHHHHHHHHHHHHHKH", 2509 - " RRRRRWWWWWWWWWR G MMMMMHHHHHHHHHHH HHHHKLLH", 2510 - " RRRRRRRRWWWWWRRRRRRRR GMMMM HHHHHHHHHHH LLLLL", 2511 - " RRRRRRRRRRWWWRRRRRRRRRWWGMMMHHHHHHHHHHHLLLL", 2512 - " HHHHH RRRRRRRRRRWWWRRRRR RWLLLLLHHHHHHHHLLL", 2513 - " HHHHHHHHHRRRRRRRRRRRWWWRRRRR MMMG HHHHHHHHHHH", 2514 - " MMMMMMHHHHHHHHHHRRRRRRRRRRRWWWRRRRRRRRRRMGHHHHHHHHHH", 2515 - " MMMMMMMMMHHHHHHHHHHRRRRRRRRRRRRWWWRRRRRRRRRRHGHHHHHHHHHH", 2516 - " MMMMMMMMMHHHHHHHHHHHHHHHRRRRRRRRRRRRRRRRRRRRRHGHHHHHHHHHHH", 2517 - " MMMMM HHHHHHHHHHHHHHHGGGGGRRRRRRRGGGGGGGGGHGHHHHHHHHHHH", 2518 - " MMMMM HHHHHHHHHHHHHHHHGHHGRRRRRRRHGHHGHHGHHHHHHHHHHHHHH", 2519 - " MMMM HHHHHHHHHHHHHHHHHHHRRRRRRRHHHHHHHHHHHHHHHHHHHHHH", 2520 - " MMMM HHHHHHHHHHHHHHHHHHRRRRRRRHHHHHHHHHHHHHHHHHHHHH", 2521 - " MMM HHHHHHHHHHHHHHHHHRRRRRRRHHHHHHHHHHHHHHHHHHHH", 2522 - " MMM HHHHHHFFFFF HHKKKKKHHHHHH HHHHHHHHHFF", 2523 - " MMM HHHH FFFFF KKKKK HHHHH FFFF", 2524 - " MMM HHHHH FFFF GGGGGGG HHHHH FFFF", 2525 - " MMM HHH FFFFF KKK FFFF", 2526 - " MMM HHHH FFFFF FFFF", 2527 - " MMM HHH FFFK FFFF", 2528 - " M KKK KKK KKK", 2529 - "", 2530 -]; 2531 -const DUIF_0 = [ 2532 - " WWWWWW", 2533 - " WWWWWWWWW", 2534 - " WWWWWWWW", 2535 - " WWWWWWW", 2536 - " WWWWWWW", 2537 - " WWWWWWWW WW", 2538 - " WWWWWWW WWWWWWWWW WWWWKW", 2539 - " WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWOOOG", 2540 - " WWWWWWWWWWWWWWWWWWWWWWWWWWWW GGGGG", 2541 - " WWWWWWWW WWWWWWWWWWWWW GGG", 2542 - " WWW GG", 2543 - "", 2544 - "", 2545 - "", 2546 -]; 2547 -const DUIF_1 = [ 2548 - "", 2549 - "", 2550 - "", 2551 - " WWWWWW", 2552 - " WWWWWWW", 2553 - " WWWWWWWW WW", 2554 - " WWWWWWW WWWWWWWWWW WWWWKW", 2555 - " WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWOOOG", 2556 - " WWWWWWWWWWWWWWWWWWWWWWWWWWWW GGGGG", 2557 - " WWWWWWWW WWWWWWWWWWWWW GGG", 2558 - " WWW GG", 2559 - "", 2560 - "", 2561 - "", 2562 -]; 2563 -const DUIF_2 = [ 2564 - "", 2565 - "", 2566 - "", 2567 - "", 2568 - "", 2569 - " WW", 2570 - " WWWWWWW WWWWWWWWW WWWWKW", 2571 - " WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWOOOG", 2572 - " WWWWWWWWWWWWWWWWWWWWWWWWWWWW GGGGG", 2573 - " WWWWWWWW WWWWWWWWWWWWW GGG", 2574 - " WWW WWWWWWWW GG", 2575 - " WWWWWWWW", 2576 - " WWWWWWWWW", 2577 - " WWWWWWWWWW", 2578 -]; 2579 -const DUIF_3 = [ 2580 - "", 2581 - "", 2582 - "", 2583 - "", 2584 - "", 2585 - " WW", 2586 - " WWWWWWW WWWWWWWWW WWWWKW", 2587 - " WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWOOOG", 2588 - " WWWWWWWWWWWWWWWWWWWWWWWWWWWW GGGGG", 2589 - " WWWWWWWW WWWWWWWWWWWWW GGG", 2590 - " WWW WWWWWWWWW GG", 2591 - " WWWWWWW", 2592 - " WWWWW", 2593 - "", 2594 -]; 2595 -const KERSTMAN_0 = [ 2596 - "", 2597 - " WWWW", 2598 - " WWRRRRR", 2599 - " RRRRRRRR", 2600 - " RRRRRRRR KK KK KKKK KK KK KKKK", 2601 - " RRRRRRRR KKK K KK KKK K KK", 2602 - " RRRRRRRRR KKK KK KKK KKK KK KK", 2603 - " RRRRRRRRRR KK KK KKK KK KK KKK", 2604 - " WWWWWWWWWWWWW WWWW KKKKK K KK KK KKKKK K KK KK", 2605 - " WWWWWWWWWWWWW RRWWWW KK KK KKKK KK KK KKKK", 2606 - " RRGRR SSSSSSSSSSRRRRRRR KKK KK KKK KK", 2607 - " GGGG RRRRR WWWWWWWWWWRRRRRRR KK KK KK KK", 2608 - " GGGGKKRRRRR RWWWWWWWWRRRRRRR KK KK KK KK", 2609 - " KGGGGKKKKKKKK RRRRWWWWWWWRRRRRR BBKBKKB BBKBKKB", 2610 - " KKKKKKKKKKKKKKK RRRRRRRWWWWWWWWRR BBBBBKBBBB BBBBBKBBBB", 2611 - " KKKKKKKKKKKKKKK RRRRRRRRRRRRRRRRR BBBBBBBBBBBK BBBBBBBBBBBN", 2612 - " KKKKKKKKKKKKKKK RRRRRRRRGGGRRRRRRRR RR BBBBB BBB BBBBB BBB", 2613 - " KKKKKKKKKKK KKKKKKKGGGKKKKKKK RRRRRLLLL RRR BBBBB RRR BBBBB", 2614 - " KKKKK RRRRRRRRRRRRRRR RRRRRR LLLLLLLLLLL W BBBBBRRRBBBBBBBB W BBBBBRRRBBBBBBBB", 2615 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR LLLLL LLLLLLLLBBLLLLRRRBBBBBBB BBBBBBLLRRBBBBBBB", 2616 - " RRRGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGRRR LLLLLLLL LLLLLBBRRRBBBBBB BBLLBBRRRBBBBBB", 2617 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR DBBBBRRLLLLLBBLLLLLLLLLLL DBBBBRRRBBBBBB", 2618 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR GGG DDDBBBB DDBBB DDDBBBB DDBBB", 2619 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR GG DDD BBBB DD BBB DDD BBBB DD BBB", 2620 - " G GG GGG KK BBBBDDD BBBB KK BBBBDDD BBBB", 2621 - " GG G GGG BBB DD BBBB BBB DD BBBB", 2622 - " GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG KK KK KK KK", 2623 - " GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", 2624 - "", 2625 - "", 2626 - "", 2627 - "", 2628 - "", 2629 - "", 2630 - "", 2631 - "", 2632 -]; 2633 -const KERSTMAN_1 = [ 2634 - "", 2635 - " WW", 2636 - " WWWWRRR", 2637 - " RRRRRRRR", 2638 - " RRRRRRRR KK KK KKKK KK KK KKKK", 2639 - " RRRRRRRR KKK K KK KKK K KK", 2640 - " RRRRRRRRR KKK KK KKK KKK KK KK", 2641 - " RRRRRRRRRR KK KK KKK KK KK KKK", 2642 - " WWWWWWWWWWWWW WW KKKKK K KK KK KKKKK K KK KK", 2643 - " WWWWWWWWWWWWW RWWWW KK KK KKKK KK KK KKKK", 2644 - " RRGRR SSSSSSSSSS RRRRWWR KKK KK KKK KK", 2645 - " GGGG RRRRR WWWWWWWWWWWRRRRRR KK KK KK KK", 2646 - " GGGGKKRRRRR RWWWWWWWWRRRRRRR KK KK KK KK", 2647 - " KGGGGKKKKKKKK RRRRWWWWWWWRRRRRR BBKBKKB BBKBKKB", 2648 - " KKKKKKKKKKKKKKK RRRRRRRWWWWWWWWRR BBBBBKBBBB BBBBBKBBBB", 2649 - " KKKKKKKKKKKKKKK RRRRRRRRRRRRRRRRR BBBBBBBBBBBK BBBBBBBBBBBN", 2650 - " KKKKKKKKKKKKKKK RRRRRRRRGGGRRRRRRRR RR BBBBB BBB BBBBB BBB", 2651 - " KKKKKKKKKKK KKKKKKKGGGKKKKKKK RRRRRLLLL RRR BBBBB RRR BBBBB", 2652 - " KKKKK RRRRRRRRRRRRRRR RRRRRR LLLLLLLLL W BBBBBRRRBBBBBBBB W BBBBBRRRBBBBBBBB", 2653 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR LLLL LLLLLLL BBBBLLLRRRBBBBBBB BBBBBBLLRRBBBBBBB", 2654 - " RRRGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGRRR LLLLLLLLLLLLLLBBBBBRRRBBBBBB BBBLBBRRRBBBBBB", 2655 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR DBBBLLLLBBBBBB LLLLLDBBBBRRRBBBBBB", 2656 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR GGG DDDBBB DDDBLLLLLLLLLL DDDBBB DDDBBB", 2657 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR GG DDDBBB DD BBB DDDBBB DD BBB", 2658 - " G GG GGG DDDD KK DD BBB DDDD KK DD BBB", 2659 - " GG G GGG DDD DD BB DDD DD BB", 2660 - " GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG KK KK", 2661 - " GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", 2662 - "", 2663 - "", 2664 - "", 2665 - "", 2666 - "", 2667 - "", 2668 - "", 2669 - "", 2670 -]; 2671 -const KERSTMAN_2 = [ 2672 - "", 2673 - " WWWW", 2674 - " WWRRRRR", 2675 - " RRRRRRRR", 2676 - " RRRRRRRR KK KK KKKK KK KK KKKK", 2677 - " RRRRRRRR KKK K KK KKK K KK", 2678 - " RRRRRRRRR KKK KK KKK KKK KK KK", 2679 - " RRRRRRRRRR KK KK KKK KK KK KKK", 2680 - " WWWWWWWWWWWWW WWWW KKKKK K KK KK KKKKK K KK KK", 2681 - " WWWWWWWWWWWWW RRWWWW KK KK KKKK KK KK KKKK", 2682 - " RRGRR SSSSSSSSSSRRRRRRR KKK KK KKK KK", 2683 - " GGGG RRRRR WWWWWWWWWWRRRRRRR KK KK KK KK", 2684 - " GGGGKKRRRRR RWWWWWWWWRRRRRRR KK KK KK KK", 2685 - " KGGGGKKKKKKKK RRRRWWWWWWWRRRRRR BBKBKKB BBKBKKB", 2686 - " KKKKKKKKKKKKKKK RRRRRRRWWWWWWWWRR BBBBBKBBBB BBBBBKBBBB", 2687 - " KKKKKKKKKKKKKKK RRRRRRRRRRRRRRRRR BBBBBBBBBBBK BBBBBBBBBBBN", 2688 - " KKKKKKKKKKKKKKK RRRRRRRRGGGRRRRRRRR RR BBBBB BBB BBBBB BBB", 2689 - " KKKKKKKKKKK KKKKKKKGGGKKKKKKK RRRRRLLLL RRR BBBBB RRR BBBBB", 2690 - " KKKKK RRRRRRRRRRRRRRR RRRRRR LLLLLLLLLLL W BBBBBRRRBBBBBBBB W BBBBBRRRBBBBBBBB", 2691 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR LLLLL LLLLLLLLBBLLLLRRRBBBBBBB BBBBBBLLRRBBBBBBB", 2692 - " RRRGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGRRR LLLLLLLL LLLLLBBRRRBBBBBB BBLLBBRRRBBBBBB", 2693 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR DBBBBRRLLLLLBBLLLLLLLLLLL DBBBBRRRBBBBBB", 2694 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR GGG BBB DBBB BBB DBBB", 2695 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR GG BBBD DBB BBBD DBB", 2696 - " G GG GGG BBBBDD BBK BBBBDD BBK", 2697 - " GG G GGG BBB DD BB BBB DD BB", 2698 - " GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", 2699 - " GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", 2700 - "", 2701 - "", 2702 - "", 2703 - "", 2704 - "", 2705 - "", 2706 - "", 2707 - "", 2708 -]; 2709 -const KERSTMAN_3 = [ 2710 - " WW", 2711 - " WWWWR", 2712 - " RRRRR", 2713 - " RRRRRRR", 2714 - " RRRRRRR KK KK KKKK KK KK KKKK", 2715 - " RRRRRRRR KKK K KK KKK K KK", 2716 - " RRRRRRRRR KKK KK KKK KKK KK KK", 2717 - " RRRRRRRRRR KK KK KKK KK KK KKK", 2718 - " WWWWWWWWWWWWW WWWW KKKKK K KK KK KKKKK K KK KK", 2719 - " WWWWWWWWWWWWW RRWWWW KK KK KKKK KK KK KKKK", 2720 - " RRGRR SSSSSSSSSSRRRRRRR KKK KK KKK KK", 2721 - " GGGG RRRRR WWWWWWWWWWRRRRRR KK KK KK KK", 2722 - " GGGGKKRRRRR RWWWWWWWWRRRRRR KK KK KK KK", 2723 - " KGGGGKKKKKKKK RRRRWWWWWWWRRRRRR BBKBKKB BBKBKKB", 2724 - " KKKKKKKKKKKKKKK RRRRRRRWWWWWWWWRR BBBBBKBBBB BBBBBKBBBB", 2725 - " KKKKKKKKKKKKKKK RRRRRRRRRRRRRRRRR BBBBBBBBBBBK BBBBBBBBBBBN", 2726 - " KKKKKKKKKKKKKKK RRRRRRRRGGGRRRRRRRR RR BBBBB BBB BBBBB BBB", 2727 - " KKKKKKKKKKK KKKKKKKGGGKKKKKKK RRRRRLLLLL RRR BBBBB RRR BBBBB", 2728 - " KKKKK RRRRRRRRRRRRRRR RRRRRR LLLLLLLLLLLL W BBBBBRRRBBBBBBBB W BBBBBRRRBBBBBBBB", 2729 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR LLLLLLLLLLLLLLLLLLLBRRRBBBBBBB BBBBBBLLRRBBBBBBB", 2730 - " RRRGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGRRR BBLLLLLRRBBBBBB LLBLLBBBRRRBBBBBB", 2731 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR BBBBRRRBBBBLLLLLLLLLLL BBBBRRRBBBBBB", 2732 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR GGG DDBBB DBB DDBBB DBB", 2733 - " RRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRRR GG DBBB BBB DBBB BBB", 2734 - " G GG GGG BBDD KKDD BBDD KKDD", 2735 - " GG G GGG BBDDD DDD BBDDD DDD", 2736 - " GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG KK KK", 2737 - " GGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGGG", 2738 - "", 2739 - "", 2740 - "", 2741 - "", 2742 - "", 2743 - "", 2744 - "", 2745 - "", 2746 -]; 2747 -const PAASHAAS_0 = [ 2748 - "", 2749 - "", 2750 - "", 2751 - "", 2752 - "", 2753 - "", 2754 - "", 2755 - "", 2756 - "", 2757 - "", 2758 - "", 2759 - "", 2760 - "", 2761 - " FF HH", 2762 - " FFF HHHH", 2763 - " FFFFF HHHHH", 2764 - " FFFFF HHPPH", 2765 - " FFPPF HHPPH", 2766 - " FPPFF HHPPH", 2767 - " FPPPF HHPPH", 2768 - " FFPPF HHPPH", 2769 - " FPPF HHPPH", 2770 - " FPPF HPPH", 2771 - " FFPP HPPH", 2772 - " FPPFHPPH", 2773 - " FPPFHPPH", 2774 - " FFFFHHHH", 2775 - " HHHHHHHHH", 2776 - " HHHHHHHHHHH", 2777 - " H HHHHHHHHHHHHH", 2778 - " HHHHHHHHHHHHHHHHHHHHHHHHKKK", 2779 - " WWWHHHHHHHHHHHHHHHHHHHHHHHHH", 2780 - " WWWWWWHHHHHHHHHHHHHHHHHH GGG", 2781 - " WWWWWHHHHHHHHHHHHHHHHHHH GGGBGGG", 2782 - " HHHHHHHHHHHHHHHHHHHHH GGRGGGYGG", 2783 - " HHHHHHHHHHHHHHHHHHHHH GGGGGGGGG", 2784 - " HHHHHHHHHHHHHHHHHHHH GGGGGGG", 2785 - " HHHHHHHHHHHHHHHHH HHHHHHHH", 2786 - " HHHHHHHHHHHHH HHHHHHH", 2787 - " H", 2788 - "", 2789 - "", 2790 - "", 2791 - "", 2792 -]; 2793 -const PAASHAAS_1 = [ 2794 - "", 2795 - "", 2796 - "", 2797 - "", 2798 - "", 2799 - "", 2800 - "", 2801 - "", 2802 - " H", 2803 - " FF HH", 2804 - " FFFF HHHHH", 2805 - " FFFFF HHHHH", 2806 - " FFPPF HHPPH", 2807 - " FPPF HHPPH", 2808 - " FPPFF HHPPH", 2809 - " FFPPF HHPPH", 2810 - " FPPF HHPPH", 2811 - " FPPF HPPH", 2812 - " FPPF HPPH", 2813 - " FFPP HPPH", 2814 - " FPPFHPPH", 2815 - " FPPFHPPH", 2816 - " FFFFHHHH", 2817 - " HHHHHHHHHHH", 2818 - " HHHHHHHHKHHHH", 2819 - " HHHHHHHHHHH HHHHHHHHHHHP", 2820 - " HHHHHHHHHHHHHHHHHHHHHHHHHH", 2821 - " WWWWHHHHHHHHHHHHHHHHHHH H", 2822 - " WWWWWWWHHHHHHHHHHHHHHHHH HHHHGGGG", 2823 - " WWWHHHHHHHHHHHHHHHHHHHHHHHHGGHB GG", 2824 - " HHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGG", 2825 - " HHHHHHHHHHHHHHHHHHH GGGGGGGGG", 2826 - " HHHHHHHHHHHHHHHHH GGGGG", 2827 - " HHHHHHH H", 2828 - " HHHHHHHH", 2829 - " HHHHHHH", 2830 - " HHH", 2831 - "", 2832 - "", 2833 - "", 2834 - "", 2835 - "", 2836 - "", 2837 - "", 2838 -]; 2839 -const PAASHAAS_2 = [ 2840 - "", 2841 - "", 2842 - "", 2843 - " FF HH", 2844 - " FFF HHHH", 2845 - " FFFFF HHHHH", 2846 - " FFFFF HHPPH", 2847 - " FFPPF HHPPH", 2848 - " FPPFF HHPPH", 2849 - " FPPPF HHPPH", 2850 - " FFPPF HHPPH", 2851 - " FPPF HHPPH", 2852 - " FPPF HPPH", 2853 - " FFPP HPPH", 2854 - " FPPFHPPH", 2855 - " FPPFHPPH", 2856 - " FFFFHHHH", 2857 - " HHHHHHHHH", 2858 - " HHHHHHHHHHH", 2859 - " H HHHHHHHHHHHHH", 2860 - " HHHHHHHHHHHHHHHHHHHHHHHHKKK", 2861 - " WWWHHHHHHHHHHHHHHHHHHHHHHHHH", 2862 - " WWWWWWHHHHHHHHHHHHHHHHHH GGG", 2863 - " WWWWWHHHHHHHHHHHHHHHHHHH GGGBGGG", 2864 - " HHHHHHHHHHHHHHHHHHHHH GGRGGGYGG", 2865 - " HHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGGGG", 2866 - " HHHHHHHHHHHHHHHHHHHHHHHHHGGGGGGG", 2867 - " HHHHHHHHHHHHHHHHHHHH", 2868 - "HHHHHHHHHHH", 2869 - "HHHHH", 2870 - "", 2871 - "", 2872 - "", 2873 - "", 2874 - "", 2875 - "", 2876 - "", 2877 - "", 2878 - "", 2879 - "", 2880 - "", 2881 - "", 2882 - "", 2883 - "", 2884 -]; 2885 -const PAASHAAS_3 = [ 2886 - "", 2887 - "", 2888 - "", 2889 - "", 2890 - "", 2891 - "", 2892 - "", 2893 - "", 2894 - "", 2895 - " H", 2896 - " FF HH", 2897 - " FFFF HHHHH", 2898 - " FFFFF HHHHH", 2899 - " FFPPF HHPPH", 2900 - " FPPF HHPPH", 2901 - " FPPFF HHPPH", 2902 - " FFPPF HHPPH", 2903 - " FPPF HHPPH", 2904 - " FPPF HPPH", 2905 - " FPPF HPPH", 2906 - " FFPP HPPH", 2907 - " FPPFHPPH", 2908 - " FPPFHPPH", 2909 - " FFFFHHHH", 2910 - " HHHHHHHHHHH", 2911 - " HHHHHHHHKHHHH", 2912 - " HHHHHHHHHHH HHHHHHHHHHHP", 2913 - " HHHHHHHHHHHHHHHHHHHHHHHHHH", 2914 - " WWWWHHHHHHHHHHHHHHHHHHH H", 2915 - " WWWWWWWHHHHHHHHHHHHHHHHH GGGGG", 2916 - " WWWHHHHHHHHHHHHHHHHHHHH GG B GG", 2917 - " HHHHHHHHHHHHHHHHHHHHH GGGGGGGGG", 2918 - " HHHHHHHHHHHHHHHHHHH GGGGGGGGG", 2919 - " HHHHHHHHHHHHHHH GGGGG", 2920 - " HHHHHH H HHHHHHHHH", 2921 - " HHHH HHHHHHHHHHH", 2922 - " HHHH HHHHH", 2923 - " HHHHHH", 2924 - " HHH", 2925 - "", 2926 - "", 2927 - "", 2928 - "", 2929 - "", 2930 -]; 2931 -const BOOTJE_0 = [ 2932 - "", 2933 - " OOOOOOOOOOOK", 2934 - " RRRRRRRRRRRRK", 2935 - " RRRRRRRRRRRRKWW", 2936 - " WWWWWWWWWWWWKWWWWW", 2937 - " WWWWWWWWWWWWKWWWWWWW", 2938 - " BBBBBBBBBBBBKWWWWWWWWWW", 2939 - " KWWWWWWWWWWWWW", 2940 - " KWWWWWWWWWWWWWWW", 2941 - " KWWWWWWWWWWWWWWW", 2942 - " KWWWWWWWWWWWWWWWW", 2943 - " KWWWWWWWWWWWWWWWW", 2944 - " KWWWWWWWWWWWWWWWW", 2945 - " K", 2946 - " OOOOOOOOOOOOOOOOOOOOOOOOOOKOOOOOOOOOOOOOOOOOOOOOOOO", 2947 - " OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", 2948 - " WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWO", 2949 - " OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", 2950 - " OOOOOWWWOOOOOWWWOOOOOWWWOOOOOWWWOOOOOOOOO", 2951 - " OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO GG", 2952 - "G GGGGGGGG GGGGGGGG GGGGGGGG GGGGGGG", 2953 - "GGGGGGGG GGGGGGGG GGGGGGGG GGGGGGGG", 2954 - " GG GG GG GG", 2955 - "", 2956 -]; 2957 -const BOOTJE_1 = [ 2958 - "", 2959 - " OOOOOOOOOOOOOK", 2960 - " K", 2961 - " RRRRRRRRRRRRKWW", 2962 - " RRRRRRRRRRRRKWWWWW", 2963 - " WWWWWWWWWWWWKWWWWWWW", 2964 - " BBBBBBBBBBBBKWWWWWWWWWW", 2965 - " BBBBBBBBBBBBKWWWWWWWWWWWWW", 2966 - " KWWWWWWWWWWWWWWW", 2967 - " KWWWWWWWWWWWWWWW", 2968 - " KWWWWWWWWWWWWWWWW", 2969 - " KWWWWWWWWWWWWWWWW", 2970 - " KWWWWWWWWWWWWWWWW", 2971 - " K", 2972 - " OOOOOOOOOOOOOOOOOOOOOOOOOOKOOOOOOOOOOOOOOOOOOOOOOOO", 2973 - " OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", 2974 - " WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWO", 2975 - " OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", 2976 - " OOOOOWWWOOOOOWWWOOOOOWWWOOOOOWWWOOOOOOOOO", 2977 - " GGOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO GG", 2978 - " GGGGGGGG GGGGGGGG GGGGGGGG GGGGGGGG", 2979 - "GGGGG GGGGGGGG GGGGGGGG GGGGGGGG GGG", 2980 - "GG GG GG GG", 2981 - "", 2982 -]; 2983 -const KERSTBOOM = [ 2984 - " Y", 2985 - " G", 2986 - " GGG", 2987 - " GGGGG", 2988 - " GGRGGGG", 2989 - " GGGGGGGGG", 2990 - " GGGGGGGRGGG", 2991 - " GGGGGGGGGGG", 2992 - " GGGGGGGGGGGGG", 2993 - " GGGGGGGGGGGGGGG", 2994 - " BBB", 2995 - " BBB", 2996 - " BBB", 2997 -]; 2998 -const STER = [ 2999 - "", 3000 - " Y", 3001 - " YYY", 3002 - " YYYYYYY", 3003 - " YYYYYYYYYYYYYYY", 3004 - " YYYYYYYYYYY", 3005 - " YYYYYYYYY", 3006 - " YYYY YYYY", 3007 - " YY YY", 3008 - "", 3009 -]; 3010 -const EI = [ 3011 - " EEE", 3012 - " EEEEEEE", 3013 - " EEEEEEEEE", 3014 - " DDDDDDDDD", 3015 - " EEEEEEEEE", 3016 - " EDEEDEEDE", 3017 - " EEEEE", 3018 -]; 3019 -const KROON = [ 3020 - " Y", 3021 - " Y", 3022 - " YY YYY Y", 3023 - " YYY YYYYY YY", 3024 - " YYYYYYYYYYYY", 3025 - " YYYRYYBYYYRY", 3026 - " OOOOOOOOOOOOO", 3027 - " OOOOOOOOOOOOO", 3028 -]; 3029 -const LAMPION = [ 3030 - " K", 3031 - " K", 3032 - " OOOOO", 3033 - " YOOOOOOOY", 3034 - " OOOOOOOOO", 3035 - " OOOOOOOOO", 3036 - " OOOOOOOOO", 3037 - " OOOOOOO", 3038 - " OOO", 3039 - " KKK", 3040 -]; 3041 -const VUUR = [ 3042 - " R", 3043 - " RRR", 3044 - " RRRRR", 3045 - " RRRORRR", 3046 - " RRROOORRR", 3047 - " RROOOORRR", 3048 - " RROOYOORR", 3049 - " RROYYYOR", 3050 - " ROYYYOR", 3051 - "", 3052 -]; 3053 -const TULP = [ 3054 - "", 3055 - " R R", 3056 - " R R", 3057 - " RRRRRR", 3058 - " RRRRRR", 3059 - " GGGGGGG", 3060 - " G GG GG", 3061 - "GG G GG", 3062 - " G", 3063 -]; 3064 -const PAKJE = [ 3065 - " GGGG GGGG", 3066 - " GGGGGGGGGGG", 3067 - " RRRGGGGGRRR", 3068 - " RRRRRGRRRRR", 3069 - " GGGGGGGGGGG", 3070 - " RRRRRGRRRRR", 3071 - " RRRRRGRRRRR", 3072 - " RRRRRGRRRRR", 3073 -]; 3074 -const MIJTER = [ 3075 - " R", 3076 - " RRR", 3077 - " RRGRR", 3078 - " RRGGGGGR", 3079 - " RRRRGRRR", 3080 - " RRRRGRRR", 3081 - "GRRRRRRRRGG", 3082 - "GGGGGGGGGGG", 3083 -]; 3084 - 3085 -const GROTE_KROON = [ 3086 - " YYY YYY YYY", 3087 - " YYY Y YYY", 3088 - " Y YYY Y", 3089 - " YYY YYY YYY YYY YYY", 3090 - " YYY YYYYY YYYYY YYYY YYY", 3091 - " YYY YYYYYY YYYYY YYYYYY YY", 3092 - " YYYYY YYYYYYYYYYYYYYYYYYYYY YYYY", 3093 - " YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY", 3094 - " YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY", 3095 - " YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY", 3096 - " OOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOO", 3097 - " OOORRROOORRROOOOBBBOOOORRROOORRROOO", 3098 - " OOORRROOORRROOOOBBBOOOORRROOORRROOO", 3099 - " WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW", 3100 - " WWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWWW", 3101 -]; 3102 - 3103 -/* Een kristallen glas, rechtop en in drie standen gekanteld (naar rechts); het andere glas is het 3104 - gespiegelde. */ 3105 -const GLAS_0 = [ 3106 - "", 3107 - "", 3108 - "", 3109 - " CCCCCCCCCCCCCCCCCCCCCCCCC", 3110 - " CC L CC", 3111 - " CC L CC", 3112 - " CCYLYYYYYYYYYYYYYYYYYYYCC", 3113 - " CCYLYYYYYYYYYYYYYYYYYYYCC", 3114 - " CCLLYYYYYYYYYYBYYYYYYYYCC", 3115 - " CCLYYYYYYYYYYYYYYYYYYCC", 3116 - " CCLLYYYYBYYYYYYYYYYYYCC", 3117 - " CCCLYYYYYYYYYYYYYYYYCC", 3118 - " CCCYYYYYYYYYYYYYCCC", 3119 - " CCCYYYYYYYYYYYCCC", 3120 - " CCCCCCCCCCCCCC", 3121 - " CCC", 3122 - " CCC", 3123 - " CCC", 3124 - " CCC", 3125 - " CCC", 3126 - " CCC", 3127 - " CCC", 3128 - " CCC", 3129 - " CCC", 3130 - " CCC", 3131 - " CCC", 3132 - " CCC", 3133 - " CCCCCCCCCCCCCCCCCC", 3134 - " CCCCCCCCCCCCCCCCCCC", 3135 - "", 3136 -]; 3137 -const GLAS_1 = [ 3138 - "", 3139 - "", 3140 - " C", 3141 - " CCCCCCCCCCCCCCCC", 3142 - " CCLL CCCCCCCC", 3143 - " CCCL CC", 3144 - " CCLL CC", 3145 - " CCLLYYYYYYYYYYYYYYYYYYYCC", 3146 - " CCLYYYYYYYYYYYYYYYYYYYYCC", 3147 - " CCLLYYYYYYYYYYYYYYYYYYCC", 3148 - " CCYLYYYYBYYYYYYYYYYYYCC", 3149 - " CCYYYYYYYYYYYYYYYYYYCC", 3150 - " CCYYYYYYYYYYYBYYYCCC", 3151 - " CCCYYYYYYYYYYYCCC", 3152 - " CCCCCYYCCCCCC", 3153 - " CCCCC", 3154 - " CC", 3155 - " CCC", 3156 - " CC", 3157 - " CC", 3158 - " CC", 3159 - " CC", 3160 - " CC", 3161 - " CC", 3162 - " CCC", 3163 - " CC", 3164 - " CC", 3165 - " CCCCCCCCCC", 3166 - " CCCCCCCCCCCCCCCCCCCC", 3167 - " CCCCCC", 3168 -]; 3169 -const GLAS_2 = [ 3170 - "", 3171 - "", 3172 - " C", 3173 - " CCCCCCCCCC", 3174 - " CCCLL CCCCCCC", 3175 - " CCLL CCCCCCC", 3176 - " CC L CC", 3177 - " CCLLYYYYYYYYYYYYYYYYYYYCC", 3178 - " CCLYYYYYYYYYYYYYYYYYYYYCC", 3179 - " CCLYYYYYYYYYYBYYYYYYYYYCC", 3180 - " CCLYYYYBYYYYYYYYYYYYYYCC", 3181 - " CCCYYYYYYYYYYYYYYYYYYCC", 3182 - " CCYYYYYYYYYYYYYYYYYCC", 3183 - " CCCYYYBYYYYYYYYCCCC", 3184 - " CCCCYYYYYYYYCCCC", 3185 - " CCCCCCCCCC", 3186 - " CC", 3187 - " CC", 3188 - " CC", 3189 - " CC", 3190 - " CCC", 3191 - " CC", 3192 - " CCC", 3193 - " CC", 3194 - " CCC", 3195 - " CC", 3196 - " C CCC", 3197 - " CCCCCCCCCCC", 3198 - " CCCCCCCCCCCCC", 3199 - " CCCCCCCC", 3200 -]; 3201 -const GLAS_3 = [ 3202 - "", 3203 - "", 3204 - "", 3205 - " CCCCCC", 3206 - " CCCL CCCCCC", 3207 - " CCCLL CCCCCC", 3208 - " CCLL CCCCCC", 3209 - " CCLL CCC", 3210 - " CCLYYYYYYYYYYYYYYYYYYYYYCC", 3211 - " CCCLYYYYYYYYYYYYYYYYYYYYCC", 3212 - " CCLLYYYYYYYYYYBYYYYYYYYCC", 3213 - " CCCYYYYYYYYYYYYYYYYYYYCCC", 3214 - " CCYYYYYYYYYYYYYYYYYYCC", 3215 - " CCCYYYYYYYYYYYYYYYYCC", 3216 - " CCCCYYYYYYYYYYYCCCC", 3217 - " CCCCYYYYYYCCCC", 3218 - " CCCCCCCCC", 3219 - " CC", 3220 - " CCC", 3221 - " CCC", 3222 - " CCC", 3223 - " CC", 3224 - " CC", 3225 - " CCC", 3226 - " CCC", 3227 - " CCC", 3228 - " CCCCC CC", 3229 - " CCCCCCCCCC", 3230 - " CCCCCCCCCC", 3231 - " CCCCCCCC", 3232 -]; 3233 - 3234 -const PASTEL = ["#f4a6c0", "#9fd3f2", "#f6d86b", "#a8dba8", "#c9a7eb"]; 3235 - 3236 -/* De kalender. Volgorde telt: het eerste thema waarvan de periode klopt, geldt. Daarom staan de korte, 3237 - bijzondere dagen (4 en 5 mei, Koningsdag) vooraan. */ 3238 -const SEASONS = Object.freeze([ 3239 - { 3240 - id: "herdenking", 3241 - label: "Dodenherdenking", 3242 - klaar: true, 3243 - rule: { from: "05-04", to: "05-04" }, 3244 - // Op 4 mei alleen het rustige veld en de vlag, halfstok; verder niets. 3245 - quiet: true, 3246 - decor: { kind: "vlag", halfstok: true }, 3247 - }, 3248 - { 3249 - id: "bevrijdingsdag", 3250 - label: "Bevrijdingsdag", 3251 - klaar: true, 3252 - rule: { from: "05-05", to: "05-05" }, 3253 - accent: NL_RED, 3254 - decor: { kind: "vlag", halfstok: false, wimpel: true }, 3255 - traverser: { 3256 - palette: { W: "text:0.75", O: "#e8920c", G: "#3f7d3a", K: "text:0.95" }, 3257 - // Vleugelslag: op, half, neer, half. 3258 - frames: [DUIF_0, DUIF_1, DUIF_3, DUIF_2], 3259 - frameMs: 120, 3260 - wave: 22, 3261 - waveSeconds: 1.8, 3262 - crossSeconds: 9, 3263 - everySeconds: [20, 40], 3264 - firstSeconds: [2, 5], 3265 - }, 3266 - // Een vlammetje van het bevrijdingsvuur bij een klik. 3267 - clickArt: { palette: { R: NL_RED, O: ORANGE, Y: YELLOW }, shapes: [VUUR], cellScale: 2 }, 3268 - }, 3269 - { 3270 - id: "koningsdag", 3271 - label: "Koningsdag", 3272 - klaar: true, 3273 - rule: { kingsDay: true }, 3274 - accent: ORANGE, 3275 - // Een grote gouden kroon ergens op een vrije plek, die zacht glinstert. 3276 - decor: { 3277 - kind: "figuur", 3278 - art: GROTE_KROON, 3279 - palette: { Y: "#d4a017", O: ORANGE, R: NL_RED, B: NL_BLUE, W: "text:0.35", K: "text:0.8" }, 3280 - glitter: true, 3281 - sparkle: { color: "#f6d86b", symbols: ["\u2726", "\u2727", "*"] }, 3282 - }, 3283 - traverser: { 3284 - palette: { O: ORANGE, W: "text:0.45", K: "text:0.8", R: NL_RED, B: NL_BLUE, G: "#3b82c4" }, 3285 - frames: [BOOTJE_0, BOOTJE_1], 3286 - ground: true, 3287 - frameMs: 400, 3288 - bob: 3, 3289 - crossSeconds: 14, 3290 - everySeconds: [25, 45], 3291 - firstSeconds: [2, 5], 3292 - }, 3293 - clickArt: { 3294 - palette: { Y: YELLOW, O: ORANGE, R: NL_RED, B: NL_BLUE, G: GREEN }, 3295 - shapes: [KROON, TULP], 3296 - cellScale: 2, 3297 - }, 3298 - }, 3299 - { 3300 - id: "sintmaarten", 3301 - label: "Sint-Maarten", 3302 - klaar: true, 3303 - rule: { from: "11-11", to: "11-11" }, 3304 - accent: ORANGE, 3305 - // Lampionnetjes die zachtjes omhoog zweven en een beetje gloeien. 3306 - decor: { kind: "lampions", count: 7, art: LAMPION, palette: { O: ORANGE, Y: YELLOW, K: "text:0.6" } }, 3307 - clickArt: { palette: { O: ORANGE, Y: YELLOW, K: "text:0.6" }, shapes: [LAMPION], cellScale: 2 }, 3308 - }, 3309 - { 3310 - id: "sinterklaas", 3311 - label: "Sinterklaas", 3312 - klaar: true, 3313 - rule: { from: "11-24", to: "12-05" }, 3314 - accent: RED, 3315 - traverser: { 3316 - palette: { R: RED, G: GOLD, S: "#e0a47a", W: "text:0.85", H: "text:0.55", F: "text:0.3", M: "text:0.82", K: "text:0.95", L: "text:0.3" }, 3317 - frames: [SINT_0, SINT_1, SINT_2, SINT_3], 3318 - ground: true, 3319 - // Galop in vier standen; het geheel deint mee. 3320 - frameMs: 130, 3321 - bob: 5, 3322 - crossSeconds: 12, 3323 - everySeconds: [25, 45], 3324 - firstSeconds: [2, 5], 3325 - }, 3326 - // Pepernotenbuien: af en toe spat links of rechts een handvol pepernoten uit elkaar. 3327 - decor: { kind: "buien", colors: ["#8b5a2b", "#6b4423", "#a06a32"], count: 60, everySeconds: [2, 4.5], firstSeconds: [1, 2] }, 3328 - clickArt: { palette: { R: RED, G: GOLD }, shapes: [PAKJE, MIJTER], cellScale: 2 }, 3329 - }, 3330 - { 3331 - id: "kerst", 3332 - label: "Kerst", 3333 - klaar: true, 3334 - rule: { from: "12-06", to: "12-26" }, 3335 - accent: RED, 3336 - traverser: { 3337 - palette: { 3338 - R: RED, 3339 - W: "text:0.85", 3340 - S: "#e0a47a", 3341 - K: "text:0.9", 3342 - G: GOLD, 3343 - B: BROWN, 3344 - N: "#ff2a2a", 3345 - D: "#b08455", 3346 - L: "text:0.3", 3347 - }, 3348 - frames: [KERSTMAN_0, KERSTMAN_1, KERSTMAN_2, KERSTMAN_3], 3349 - // Een golvend spoor van sterren achter de slee. 3350 - trail: { 3351 - symbols: ["\u2605", "\u2726", "\u2727", "\u2736", "*", "\u00b7"], 3352 - colors: ["#e0b000", "#f6d86b", "text:0.7"], 3353 - perSecond: 40, 3354 - life: 2.4, 3355 - waveAmp: 30, 3356 - waveMs: 420, 3357 - spread: 16, 3358 - at: [0.03, 0.45], 3359 - }, 3360 - frameMs: 150, 3361 - wave: 26, 3362 - waveSeconds: 2.6, 3363 - crossSeconds: 10, 3364 - everySeconds: [25, 45], 3365 - firstSeconds: [2, 5], 3366 - }, 3367 - // Half echte sneeuwvlokjes, half letters uit het veld. 3368 - decor: { kind: "sneeuw", count: 90, symbolShare: 0.5 }, 3369 - clickArt: { palette: { G: GREEN, B: BROWN, Y: YELLOW, R: RED }, shapes: [KERSTBOOM, STER], cellScale: 2 }, 3370 - }, 3371 - { 3372 - id: "nieuwjaar", 3373 - label: "Nieuwjaar", 3374 - klaar: true, 3375 - rule: { from: "12-27", to: "01-14" }, 3376 - accent: GOLD, 3377 - // Af en toe vuurpijlen die opstijgen en openspatten; een klik geeft vuurwerk. 3378 - // Vaker vuurwerk, en af en toe een pijl die horizontaal over het scherm schiet en alsnog ontploft. 3379 - traverser: { kind: "vuurwerk", everySeconds: [2.5, 5], firstSeconds: [1, 2], sideways: 0.25 }, 3380 - // Vijf snelle klikken: een vuurwerkshow en twee kristallen glazen die klinken. 3381 - fiveClicks: "proost", 3382 - proostArt: { 3383 - frames: [GLAS_0, GLAS_1, GLAS_2, GLAS_3], 3384 - palette: { C: "#6f9fc4", L: "#cfe6f5", Y: "#e0b000", B: "#fff3b0" }, 3385 - }, 3386 - clickArt: { burst: true }, 3387 - }, 3388 - { 3389 - id: "pasen", 3390 - label: "Pasen", 3391 - klaar: true, 3392 - rule: { easter: [-7, 8] }, 3393 - accent: "#e879a6", 3394 - traverser: { 3395 - palette: { H: "text:0.6", F: "text:0.4", W: "text:0.3", P: "#f4a6c0", K: "text:0.95", G: "#b7884a", R: PASTEL[0], B: PASTEL[1], Y: PASTEL[2] }, 3396 - // Huppelen in vier standen: zitten, afzetten, zweven, landen; de sprong zit in de tekening. 3397 - frames: [PAASHAAS_0, PAASHAAS_1, PAASHAAS_2, PAASHAAS_3], 3398 - ground: true, 3399 - frameMs: 150, 3400 - crossSeconds: 7, 3401 - everySeconds: [25, 45], 3402 - firstSeconds: [2, 5], 3403 - }, 3404 - // Eieren verstopt in het veld; een klik op een ei laat het openspringen. 3405 - decor: { kind: "eieren", count: 6, art: EI, colors: PASTEL }, 3406 - clickArt: { palette: { E: PASTEL[3], D: PASTEL[4] }, shapes: [EI], cellScale: 2 }, 3407 - }, 3408 -]); 3409 - 3410 -/* Het seizoen dat op een datum geldt, of null. override komt uit ?tekenveld=: een id toont dat thema 3411 - meteen (ook als het niet klaar is), "geen" zet de thema's uit; een onbekende id wordt genegeerd. */ 3412 -function seasonFor(date, { override = null } = {}) { 3413 - if (override === "geen") return null; 3414 - if (override) { 3415 - const forced = SEASONS.find((season) => season.id === override); 3416 - if (forced) return forced; 3417 - } 3418 - return SEASONS.find((season) => season.klaar && inPeriod(date, season.rule)) || null; 3419 -} 3420 - 3421 -// Opstart van het tekenveld op de startpagina en de inlogpagina van de Kennisbank (<Code>.Code.Tekenveld). 3422 -// maak-tekenveld.js zet dit achter de engine en de seizoenskalender van Ember; samen vormen ze de 3423 -// JavaScriptExtension. De engine (createGlyphField, glyphFieldSettings) en de kalender (seasonFor) 3424 -// komen ongewijzigd uit Ember. 3425 - 3426 3426 // Waar een klik iets doet of waar inhoud staat: geen achtergrond, dus geen smiley. 3427 3427 const KB_NIET_ACHTERGROND = [ 3428 3428 "a", ... ... @@ -3431,8 +3431,7 @@ 3431 3431 "select", 3432 3432 "textarea", 3433 3433 "label", 3434 - "form:not(#loginForm)", 3435 - "#loginForm .panel", 1289 + "form", 3436 3436 "summary", 3437 3437 "[role='button']", 3438 3438 "[role='dialog']", ... ... @@ -3445,9 +3445,8 @@ 3445 3445 ".wb-home-onder", 3446 3446 ].join(","); 3447 3447 3448 -// De blokken van de startpagina; de hartslaglijn, de figuren, de vlag en de kroon kiezen bij voorkeur een 3449 -// plek waar die niet staan. De knoppen voor redacteuren (Wijzigen, Toevoegen) horen erbij, op de inlogpagina het inlogvak. 3450 -const KB_BLOKKEN = ".wb-zoek > *, .wb-tegels li, .wb-home-onder, .navbar, .document-header .document-menu, .document-header .btn, #loginForm .panel"; 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"; 3451 3451 3452 3452 // Na zoveel tijd zonder muis of aanraking ligt het veld stil (scheelt rekenwerk en 3453 3453 // beeldverkeer op de terminalserver); bij de eerste beweging loopt het weer. ... ... @@ -3461,8 +3461,8 @@ 3461 3461 const KB_BOEM_DUUR_MS = 62000; 3462 3462 3463 3463 function kbTekenveld() { 3464 - // Alleen op de startpagina (tweelaadregels)endeinlogpagina(de laderTekenveldInloggen); nooit twee keer.3465 - if (!document.querySelector(".wb-home , #loginForm")|| document.querySelector("canvas.kb-tekenveld"))return;1317 + // Alleen op de startpagina; de extensie wordt ook alleen daar geladen. 1318 + if (!document.querySelector(".wb-home")) return; 3466 3466 3467 3467 // De Kennisbank heeft alleen een lichte stand; de engine leest die van <html>. 3468 3468 document.documentElement.setAttribute("data-appearance", "light"); ... ... @@ -3479,18 +3479,6 @@ 3479 3479 obstacles: () => Array.from(document.querySelectorAll(KB_BLOKKEN), (element) => element.getBoundingClientRect()), 3480 3480 }; 3481 3481 3482 - /* Het seizoen van vandaag uit de kalender van Ember (Sinterklaas, Kerst, Nieuwjaar, Pasen, Koningsdag, 3483 - 4 en 5 mei, Sint-Maarten); met ?tekenveld=<id> is een thema meteen te zien. Het decor begint onder 3484 - de kopbalk van de Kennisbank. */ 3485 - function seizoenNu() { 3486 - const override = new URLSearchParams(window.location.search).get("tekenveld"); 3487 - return seasonFor(new Date(), { override }); 3488 - } 3489 - function kopbalkOnder() { 3490 - const balk = document.querySelector(".navbar"); 3491 - return balk ? Math.round(balk.getBoundingClientRect().bottom) : 0; 3492 - } 3493 - 3494 3494 let veld = null; 3495 3495 let rustTimer = 0; 3496 3496 let klikken = []; ... ... @@ -3514,7 +3514,6 @@ 3514 3514 function wakker(event) { 3515 3515 if (!veld) { 3516 3516 veld = createGlyphField(canvas, opties); 3517 - veld.configure({ season: seizoenNu(), decorTop: kopbalkOnder() }); 3518 3518 wekklik = event || null; 3519 3519 } 3520 3520 window.clearTimeout(rustTimer); ... ... @@ -3524,8 +3524,6 @@ 3524 3524 function telKlik(event) { 3525 3525 const nu = Date.now(); 3526 3526 if (event === wekklik || event.button !== 0 || nu < boemTot || !isAchtergrond(event)) return; 3527 - // Tijdens een seizoen is er geen explosie (vijf klikken doen dan iets van het seizoen zelf, of niets). 3528 - if (seizoenNu()) return; 3529 3529 klikken = klikken.filter((tijd) => nu - tijd < KB_BOEM_VENSTER_MS); 3530 3530 klikken.push(nu); 3531 3531 if (klikken.length < KB_BOEM_KLIKKEN) return;
- XWiki.StyleSheetExtension[0]
-
- Code
-
... ... @@ -1,5 +1,5 @@ 1 -/* Tekenveld op de startpagina en de inlogpaginavan de Kennisbank (<Code>.Code.Tekenveld); alleendaar2 - geladen. Het canvas ligt achter de inhoud; de vlakken eromheen worden doorzichtig. */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 3 4 4 /* Kleuren voor de engine: de tekstkleur en het donkere oranje (brand-warning) uit het kleurthema; 5 5 de hartslaglijn in het rood van het thema (brand-danger). */ ... ... @@ -46,23 +46,6 @@ 46 46 background: radial-gradient(closest-side, rgba(255, 255, 255, .97) 62%, rgba(255, 255, 255, 0) 100%); 47 47 } 48 48 49 -/* De footer laat het veld doorlopen; alleen de tekst houdt een rustige witte achtergrond. 50 - Ook op HEFAS, waar de footer anders een donkere balk is. */ 51 -#footerglobal { 52 - background: transparent !important; 53 - border-top: 0 !important; 54 -} 55 - 56 -#footerglobal #xwikiplatformversion { 57 - width: fit-content; 58 - margin: 0 auto; 59 - padding: 4px 16px; 60 - border-radius: 16px; 61 - background: #ffffff; 62 - box-shadow: 0 0 18px 12px #ffffff; 63 - color: #5f6368; 64 -} 65 - 66 66 .wb-home-onder { 67 67 background: #ffffff; 68 68 border-radius: 16px;