PageSourceSearch

https://scriptoindustries.com/js/signup.js

js scriptoindustries.com collected 2026-09-24 14:52:05 UTC 34,729 bytes, 810 lines download raw bytes

1// signup.js — the "CRT terminal" email sign-up panel: its own Pixi
2// Application/canvas, the heading text, the email field, the submit button, and
3// the Google-Form submission. The email text/caret and the button are rendered
4// IN the canvas (an invisible HTML <input> underneath only captures keystrokes)
5// so the CRT shader scans them. Driven by the engine (scripto.js): signup.init()
6// at startup, signup.resize() on resize.
7
8var signup = {};
9
10signup.init = async function(){
11   sc.signUpFrame = document.getElementById('signUp');
12   sc.aboutUsFrame = document.getElementById('scripto');   // used to size the signup frame
13
14   sc.signUpPixiApp = new PIXI.Application();
15   await sc.signUpPixiApp.init({
16      width: sc.signUpFrame.offsetWidth,
17      height: sc.signUpFrame.offsetHeight,
18      antialias: true,
19      background: 0x000000,
20      backgroundAlpha: 1,
21      resolution: sc.renderResolution || 1,
22      autoDensity: sc.isTouchPhone
23   });
24   sc.signUpFrame.appendChild(sc.signUpPixiApp.canvas);
25   sc.signUpPixiApp.renderer.events.autoPreventDefault = false;
26   sc.signUpPixiApp.canvas.style['touch-action'] = 'auto';
27
28   sc.signUpPixiApp.stage.addChild(sc.sprites.crtBG);
29   sc.sprites.crtBG.anchor.set(0);
30
31   sc.signUpStyle = new PIXI.TextStyle({
32      fontFamily: "\"Courier New\", Courier, monospace",
33      fontSize: 16,
34      fill: "#e8e8e8",
35      fontWeight: "bold"
36   });
37   sc.updatesText = new PIXI.Text({ text: 'We are working on a lot of new apps and games!', style: sc.signUpStyle });
38   sc.updatesText.anchor.set(0, 0.5);
39   sc.signUpPixiApp.stage.addChild(sc.updatesText);
40
41   sc.signUpText = new PIXI.Text({ text: 'Sign up to get updates by email: ', style: sc.signUpStyle });
42   sc.signUpText.anchor.set(0, 0.5);
43   sc.signUpPixiApp.stage.addChild(sc.signUpText);
44
45   // --- Email field --------------------------------------------------------
46   // PixiJS v8 has no native editable text field (the old PIXI.TextInput plugin is
47   // unmaintained with no v8 support), so the field is split in two: a real but
48   // INVISIBLE <input> overlaid on the canvas captures focus, typing, paste and the
49   // mobile keyboard, while the VISIBLE text is a PIXI.Text rendered INSIDE the
50   // canvas (sc.inputDisplayText) that mirrors input.value. Rendering the text in
51   // WebGL is the whole point: the CRT shader (effects.js) now scans it, the caret
52   // and the underline like everything else on the stage.
53   sc.signUpInput = document.createElement('input');
54   // type 'text' (not 'email') so the browser doesn't pop its native
55   // email-validation tooltip — we run our own validateEmail() check and show
56   // the red "invalid email" message. inputmode keeps the email keyboard on mobile.
57   sc.signUpInput.type = 'text';
58   sc.signUpInput.inputMode = 'email';
59   sc.signUpInput.id = 'signUpEmailInput';
60   sc.signUpFrame.appendChild(sc.signUpInput);
61
62   sc.inputStyle = new PIXI.TextStyle({
63      fontFamily: "\"Courier New\", Courier, monospace",
64      fontSize: 16,
65      fill: "#e8e8e8",
66      fontWeight: "bold"
67   });
68   // typed text + caret live in one container so they scroll/clip together
69   sc.inputContainer = new PIXI.Container();
70   sc.signUpPixiApp.stage.addChild(sc.inputContainer);
71   sc.inputDisplayText = new PIXI.Text({ text: '', style: sc.inputStyle });
72   sc.inputDisplayText.anchor.set(0, 0.5);
73   sc.inputContainer.addChild(sc.inputDisplayText);
74   sc.inputCaret = new PIXI.Graphics();
75   sc.inputContainer.addChild(sc.inputCaret);
76   // clip the typed text to the field width so a long email scrolls instead of
77   // spilling across the panel (mask is a separate stage child, drawn in resize)
78   sc.inputMask = new PIXI.Graphics();
79   sc.signUpPixiApp.stage.addChild(sc.inputMask);
80   sc.inputContainer.mask = sc.inputMask;
81   // the field underline (was the HTML border-bottom)
82   sc.inputUnderline = new PIXI.Graphics();
83   sc.signUpPixiApp.stage.addChild(sc.inputUnderline);
84
85   // mirror the real input into the Pixi text. keyup/click catch caret moves from
86   // arrow/home/end and mouse; 'input' covers typing, paste and autofill.
87   sc.signUpInput.addEventListener('input', function(){ sc.caretOn = true; signup.updateInputDisplay(); signup.updateEmailValidity(); });
88   sc.signUpInput.addEventListener('keyup', function(){ signup.updateInputDisplay(); });
89   sc.signUpInput.addEventListener('click', function(){ signup.updateInputDisplay(); });
90   sc.signUpInput.addEventListener('focus', function(){ sc.inputFocused = true; sc.caretOn = true; signup.updateInputDisplay(); });
91   sc.signUpInput.addEventListener('blur',  function(){ sc.inputFocused = false; signup.updateCaretVisibility(); });
92   // Submit on Enter, like a normal form field (shares the button's validation).
93   sc.signUpInput.addEventListener('keydown', function(e){
94      if (e.key === 'Enter') { e.preventDefault(); TrySumbitEmail(); }
95   });
96
97   // blink the caret while focused (decoupled from the render loop)
98   sc.caretOn = true;
99   setInterval(function(){ sc.caretOn = !sc.caretOn; signup.updateCaretVisibility(); }, 530);
100
101   // --- Submit button ------------------------------------------------------
102   // Drawn with Pixi Graphics (rounded outline) + a PIXI.Text label instead of the
103   // old img/submit_but.png sprite, so it lives in the canvas and the CRT shader
104   // hits it too. Sized/positioned in resize(); restyle via the values there.
105   sc.submitButton = new PIXI.Container();
106   sc.submitButtonBg = new PIXI.Graphics();
107   sc.submitButton.addChild(sc.submitButtonBg);
108   sc.submitButtonStyle = new PIXI.TextStyle({
109      fontFamily: "\"Courier New\", Courier, monospace",
110      fontSize: 16,
111      fill: "#555555",
112      fontWeight: "bold"
113   });
114   sc.submitButtonLabel = new PIXI.Text({ text: 'Submit', style: sc.submitButtonStyle });
115   sc.submitButtonLabel.anchor.set(0.5);
116   sc.submitButton.addChild(sc.submitButtonLabel);
117   sc.submitButton.eventMode = 'static';   // v8 replacement for .interactive = true
118   sc.submitButton.cursor = 'pointer';      // v8 replacement for .buttonMode = true
119   sc.submitButton.on('pointerdown', onButtonDown)
120                  .on('pointerup', onButtonUp)
121                  .on('pointerupoutside', onButtonUpOutside)
122                  .on('pointerover', onButtonOver)
123                  .on('pointerout', onButtonOut);
124   sc.signUpPixiApp.stage.addChild(sc.submitButton);
125
126   // own (green) style so it doesn't recolour the shared headings via sc.signUpStyle
127   sc.submittedStyle = new PIXI.TextStyle({
128      fontFamily: "\"Courier New\", Courier, monospace",
129      fontSize: 16,
130      fill: "#39ff14",          // terminal phosphor green
131      fontWeight: "bold"
132   });
133   sc.submittedText = new PIXI.Text({ text: 'Email Submitted!', style: sc.submittedStyle });
134   sc.submittedText.anchor.set(0, 0);
135   sc.submittedText.alpha = 0;
136   sc.signUpPixiApp.stage.addChild(sc.submittedText);
137
138   sc.errorStyle = new PIXI.TextStyle({
139      fontFamily: "\"Courier New\", Courier, monospace",
140      fontSize: 16,
141      fill: "red",
142      fontWeight: "bold"
143   });
144   sc.errorText = new PIXI.Text({ text: 'invalid email', style: sc.errorStyle });
145   sc.errorText.anchor.set(0, 0);
146   sc.errorText.alpha = 0;
147   sc.signUpPixiApp.stage.addChild(sc.errorText);
148
149   // Easter egg: mashing Submit on an invalid email (see the `egg` controller).
150   // Added last so it sits on top; centred + sized in resize().
151   sc.systemFailureStyle = new PIXI.TextStyle({
152      fontFamily: "\"Courier New\", Courier, monospace",
153      fontSize: 16,
154      fill: "#ff3030",
155      fontWeight: "bold",
156      letterSpacing: 3
157   });
158   sc.systemFailureText = new PIXI.Text({ text: 'SYSTEM FAILURE', style: sc.systemFailureStyle });
159   sc.systemFailureText.anchor.set(0.5);
160   sc.systemFailureText.visible = false;
161   sc.signUpPixiApp.stage.addChild(sc.systemFailureText);
162
163   // "Rebooting" line shown after the failure, with animated cycling dots.
164   sc.rebootingStyle = new PIXI.TextStyle({
165      fontFamily: "\"Courier New\", Courier, monospace",
166      fontSize: 16,
167      fill: "#e8e8e8",
168      fontWeight: "bold"
169   });
170   sc.rebootingText = new PIXI.Text({ text: 'Rebooting', style: sc.rebootingStyle });
171   sc.rebootingText.anchor.set(0.5);
172   sc.rebootingText.visible = false;
173   sc.signUpPixiApp.stage.addChild(sc.rebootingText);
174};
175
176signup.resize = function(){
177   sc.signUpFrame.style.height = (sc.aboutUsFrame.offsetHeight * 0.8) + "px";
178   sc.sprites.crtBG.width = sc.signUpFrame.offsetWidth;
179   sc.sprites.crtBG.height = sc.signUpFrame.offsetHeight;
180
181   var signupStartX;
182   var fontSizeTest;
183   if(sc.isMobileLayout){
184      fontSizeTest = (sc.signUpFrame.offsetWidth * 0.032);
185      signupStartX = sc.signUpFrame.offsetWidth * 0.07;
186   } else {
187      fontSizeTest = (sc.signUpFrame.offsetWidth * 0.022);
188      signupStartX = sc.signUpFrame.offsetWidth * 0.1;
189   }
190
191   // Spacing/sizing derive from the base font size rather than the heading
192   // text height, so the row spacing stays put even though the headings are a
193   // touch smaller below.
194   var lineheight = fontSizeTest * 1.5;   // submit-button sizing
195   var rowGap = fontSizeTest * 2.0;        // vertical gap between the stacked rows
196   var headingGap = fontSizeTest * 1.4;    // tighter gap between the two heading lines
197
198   // The two heading lines are slightly smaller than the email input row.
199   sc.signUpStyle.fontSize = fontSizeTest * 0.85;
200   sc.submittedStyle.fontSize = fontSizeTest * 0.85;   // own style now, keep it in sync
201
202   sc.updatesText.x = signupStartX;
203   sc.updatesText.y = (sc.signUpFrame.offsetHeight * 0.35);
204
205   sc.signUpText.x = signupStartX;
206   sc.signUpText.y = sc.updatesText.y + headingGap;
207
208   var inputX = sc.signUpText.x;
209   var inputY = sc.signUpText.y + rowGap;
210   var fieldWidth = sc.signUpFrame.offsetWidth / 2;
211
212   // geometry the display/caret updater reads (it runs from input events, not here)
213   sc.inputX = inputX;
214   sc.inputY = inputY;
215   sc.fieldWidth = fieldWidth;
216   sc.inputLineHeight = lineheight;
217   sc.inputStyle.fontSize = fontSizeTest;
218   sc.submitButtonStyle.fontSize = fontSizeTest;
219
220   // The invisible capture <input> sits exactly over the visible field area so taps
221   // focus it (and raise the mobile keyboard). Its own text/caret are transparent —
222   // the Pixi text below is what's actually seen. fontSize matches so the browser's
223   // internal selection/scroll math lines up with our caret measurement.
224   sc.signUpInput.style.position = 'absolute';
225   sc.signUpInput.style.left = inputX + 'px';
226   sc.signUpInput.style.top = inputY + 'px';
227   sc.signUpInput.style.transform = 'translateY(-50%)';
228   sc.signUpInput.style.width = fieldWidth + 'px';
229   sc.signUpInput.style.height = lineheight + 'px';
230   sc.signUpInput.style.padding = '0';
231   sc.signUpInput.style.fontSize = fontSizeTest + 'px';
232   sc.signUpInput.style.fontFamily = "\"Courier New\", Courier, monospace";
233   sc.signUpInput.style.fontWeight = 'bold';
234   sc.signUpInput.style.background = 'transparent';
235   sc.signUpInput.style.border = 'none';
236   sc.signUpInput.style.outline = 'none';
237   sc.signUpInput.style.color = 'transparent';        // visible text is the Pixi mirror
238   sc.signUpInput.style.caretColor = 'transparent';   // visible caret is the Pixi one
239
240   // Pixi field: container holds text + caret; mask clips to the field width; the
241   // underline replaces the old CSS border-bottom.
242   sc.inputContainer.x = inputX;
243   sc.inputContainer.y = inputY;
244   var underlineThickness = Math.max(2, fontSizeTest * 0.1);
245   sc.inputUnderline.clear();
246   sc.inputUnderline.rect(inputX, inputY + lineheight * 0.5, fieldWidth, underlineThickness).fill(0xe8e8e8);
247   sc.inputMask.clear();
248   sc.inputMask.rect(inputX, inputY - lineheight * 0.6, fieldWidth, lineheight * 1.2).fill(0xffffff);
249   sc.caretWidth = Math.max(1, fontSizeTest * 0.09);
250   signup.updateInputDisplay();
251
252   // Submit button: store geometry, set a hit area covering the WHOLE rect (the
253   // outline has no fill, so without this only the stroke/text pixels are
254   // clickable), then draw it. Same width/height ratio the old submit_but.png used.
255   var btnHeight = lineheight * 1.5;
256   var btnWidth = lineheight * 4.0286;
257   sc.submitButton.x = inputX + sc.signUpFrame.offsetWidth * 0.55;
258   sc.submitButton.y = inputY - btnHeight / 2;   // centre on the input line
259   sc.submitBtnW = btnWidth;
260   sc.submitBtnH = btnHeight;
261   sc.submitBtnRadius = btnHeight * 0.3;
262   sc.submitBtnStroke = Math.max(2, btnHeight * 0.11);
263   sc.submitButton.hitArea = new PIXI.Rectangle(0, 0, btnWidth, btnHeight);
264   sc.submitButtonLabel.x = btnWidth / 2;
265   sc.submitButtonLabel.y = btnHeight / 2;
266   signup.refreshSubmitButtonColor();
267
268   sc.submittedText.x = signupStartX;
269   sc.submittedText.y = sc.updatesText.y + rowGap * 2;   // sits where the input was
270
271   sc.errorText.x = signupStartX;
272   sc.errorText.y = inputY + rowGap;                      // one row below the input
273   sc.errorStyle.fontSize = fontSizeTest;
274
275   // easter-egg messages, centred in the panel
276   sc.systemFailureStyle.fontSize = fontSizeTest * 1.6;
277   sc.systemFailureText.x = sc.signUpFrame.offsetWidth / 2;
278   sc.systemFailureText.y = sc.signUpFrame.offsetHeight / 2;
279   sc.rebootingStyle.fontSize = fontSizeTest * 1.2;
280   sc.rebootingText.x = sc.signUpFrame.offsetWidth / 2;
281   sc.rebootingText.y = sc.signUpFrame.offsetHeight / 2;
282
283   sc.signUpPixiApp.renderer.resize(sc.signUpFrame.offsetWidth, sc.signUpFrame.offsetHeight);
284};
285
286// --- email field display (Pixi mirror of the invisible <input>) ------------
287// Push the real input's value + caret position into the Pixi text. Called from the
288// input's events and from resize(). Long text scrolls so the caret stays in view.
289signup.updateInputDisplay = function(){
290   if(!sc.inputDisplayText) return;
291   var val = sc.signUpInput.value || '';
292   var empty = (val.length === 0);
293
294   sc.inputDisplayText.text = empty ? 'Enter your Email Address...' : val;
295   sc.inputDisplayText.alpha = empty ? 0.5 : 1;   // dim the placeholder
296
297   // caret x = width of the text up to the cursor (selectionStart)
298   var caretX = 0;
299   if(!empty){
300      var pos = sc.signUpInput.selectionStart;
301      if(pos == null) pos = val.length;
302      if(pos > 0) caretX = PIXI.CanvasTextMetrics.measureText(val.substring(0, pos), sc.inputStyle).width;
303   }
304
305   // scroll the text left if the caret would fall past the field's right edge
306   var scroll = (caretX > sc.fieldWidth) ? (caretX - sc.fieldWidth) : 0;
307   sc.inputDisplayText.x = -scroll;
308
309   var h = sc.inputStyle.fontSize * 1.1;
310   sc.inputCaret.clear();
311   sc.inputCaret.rect(0, -h / 2, sc.caretWidth || 2, h).fill(0xe8e8e8);
312   sc.inputCaret.x = caretX - scroll;
313
314   signup.updateCaretVisibility();
315};
316
317// Caret shows only while the field is focused, blinking via sc.caretOn.
318signup.updateCaretVisibility = function(){
319   if(!sc.inputCaret) return;
320   sc.inputCaret.visible = !!sc.inputFocused && !!sc.caretOn;
321};
322
323// --- submit button handlers ---
324// Outline + label colour by state: the original gray (base), a slightly lighter
325// shade once a valid email is entered (a "ready to submit" hint), and brightest on
326// hover. Precedence: hover > valid > base.
327var SUBMIT_BASE_COLOR  = 0x555555;
328var SUBMIT_VALID_COLOR = 0x777777;
329var SUBMIT_HOVER_COLOR = 0x9a9a9a;
330
331// Redraw the button outline + recolour the label at the given colour. Geometry is
332// stored on sc by resize() so a recolour can repaint without re-laying-out.
333signup.drawSubmitButton = function(color){
334   if(!sc.submitButtonBg || sc.submitBtnW == null) return;
335   sc.submitButtonBg.clear();
336   sc.submitButtonBg.roundRect(0, 0, sc.submitBtnW, sc.submitBtnH, sc.submitBtnRadius)
337                    .stroke({ width: sc.submitBtnStroke, color: color });
338   sc.submitButtonLabel.style.fill = color;
339};
340
341// Pick the button colour from current state (hover beats valid beats base).
342signup.refreshSubmitButtonColor = function(){
343   var color = SUBMIT_BASE_COLOR;
344   if(sc.submitButton && sc.submitButton.isOver) color = SUBMIT_HOVER_COLOR;
345   else if(sc.emailValid)                         color = SUBMIT_VALID_COLOR;
346   signup.drawSubmitButton(color);
347};
348
349// Re-check validity of the current input and update the button's "ready" tint.
350signup.updateEmailValidity = function(){
351   sc.emailValid = validateEmail(sc.signUpInput.value);
352   signup.refreshSubmitButtonColor();
353};
354
355function onButtonDown()
356{
357   this.isdown = true;
358   this.alpha = 0.6;
359}
360
361function onButtonUp() {
362   this.alpha = 1;
363   // Only submit on a genuine press+release on the button. Without this guard,
364   // a stray pointerup (e.g. releasing after a drag onto the button) could submit.
365   var wasDown = this.isdown;
366   this.isdown = false;
367   if (wasDown) {
368      TrySumbitEmail();
369   }
370}
371
372// Released off the button — just reset the visual state, don't submit.
373function onButtonUpOutside() {
374   this.alpha = 1;
375   this.isdown = false;
376}
377
378function onButtonOver() {
379   this.isOver = true;
380   signup.refreshSubmitButtonColor();
381}
382
383function onButtonOut() {
384   this.isOver = false;
385   this.alpha = 1;
386   signup.refreshSubmitButtonColor();
387}
388
389function TrySumbitEmail()
390{
391   // Ignore clicks while the easter-egg failure/reboot sequence is playing out.
392   if(egg.phase !== 'none') return;
393
394   if(validateEmail(sc.signUpInput.value)){
395      egg.reset();          // a real submit breaks the rage streak
396      SumbitEmail();
397   }
398   else {
399      egg.registerInvalidClick();
400   }
401}
402
403// --- glitch effect engine ------------------------------------------------
404// Reusable "glitch-in" engine: animates a Pixi object's filters from full
405// distortion down to clean over `inDur`, then detaches them so idle frames stay
406// cheap. Used for the invalid-email error and the success confirmation.
407//
408// Four presets are available (FX_COMBO is the one we use everywhere now; the
409// other three are kept for reuse — pass `mode` to glitchFX.run to pick one):
410//   FX_GLITCH = digital glitch  (GlitchFilter: torn slices + RGB fringe)
411//   FX_RGB    = rgb split        (RGBSplitFilter: channels separate then converge)
412//   FX_CRT    = crt tune-in      (CRTFilter noise/scanline + vertical jitter)
413//   FX_COMBO  = glitch + rgb     (both at once) <-- chosen default
414// glitchFX.update() is driven per-frame from app.update() (scene.js).
415var FX_GLITCH = 0, FX_RGB = 1, FX_CRT = 2, FX_COMBO = 3;
416
417var glitchFX = {
418   instances: []   // one entry per object currently animating
419};
420
421// Lazily build (and cache on the target) the filter set this object reuses.
422glitchFX._filtersFor = function(target){
423   if(!target._fx){
424      var glitch = new PIXI.filters.GlitchFilter({ slices: 6, offset: 0 });
425      glitch.padding = 60;   // room for displaced slices so they aren't clipped
426      var rgb = new PIXI.filters.RGBSplitFilter({ red: { x: 0, y: 0 }, green: { x: 0, y: 0 }, blue: { x: 0, y: 0 } });
427      rgb.padding = 20;
428      var crt = new PIXI.filters.CRTFilter({ curvature: 0, lineWidth: 3, lineContrast: 0, noise: 0, vignetting: 0 });
429      crt.padding = 40;   // room for the curvature bulge so it isn't clipped
430      target._fx = { glitch: glitch, rgb: rgb, crt: crt };
431   }
432   return target._fx;
433};
434
435// Start a glitch-in on `target`. opts: { mode, inDur, fadeIn, onResolved }.
436//   fadeIn:true     also tweens alpha 0 -> 1 over inDur (caller owns alpha otherwise)
437//   onResolved      called once the distortion has fully resolved to clean
438glitchFX.run = function(target, opts){
439   opts = opts || {};
440   var mode = (opts.mode == null) ? FX_COMBO : opts.mode;
441   var inDur = opts.inDur || 0.5;
442   var fx = this._filtersFor(target);
443
444   // attach only the filters this preset needs
445   if(mode === FX_GLITCH)    target.filters = [ fx.glitch ];
446   else if(mode === FX_RGB)  target.filters = [ fx.rgb ];
447   else if(mode === FX_CRT)  target.filters = [ fx.crt ];
448   else                      target.filters = [ fx.glitch, fx.rgb ];
449
450   // replace any in-flight run on the same target
451   this.instances = this.instances.filter(function(i){ return i.target !== target; });
452   this.instances.push({
453      target: target, mode: mode, fx: fx,
454      t: 0, inDur: inDur, refreshT: 0,
455      baseX: target.x,                 // restored after crt shake
456      baseY: target.y,                 // restored after crt jitter
457      baseH: target.height || 24,      // jitter amplitude scales with element size
458      onResolved: opts.onResolved || null
459   });
460
461   if(opts.fadeIn){
462      target.alpha = 0;
463      createjs.Tween.get(target, { override: true }).to({ alpha: 1 }, inDur * 1000);
464   }
465};
466
467glitchFX.update = function(){
468   if(!this.instances.length) return;
469
470   for(var n = this.instances.length - 1; n >= 0; n--){
471      var i = this.instances[n];
472      i.t += sc.dt;
473      // distortion strength: 1 at the start, eased to 0 by the end of inDur
474      var k = 1 - Math.min(i.t / i.inDur, 1);
475      k = k * k;
476      var m = i.mode, fx = i.fx;
477
478      if(m === FX_GLITCH || m === FX_COMBO){      // digital glitch
479         fx.glitch.offset = 60 * k;
480         i.refreshT += sc.dt;                      // re-shuffle torn slices a few times/sec
481         if(k > 0.02 && i.refreshT > 0.05){
482            i.refreshT = 0;
483            fx.glitch.refresh();
484            fx.glitch.seed = Math.random();
485         }
486         var ch = 4 * k;                           // RGB fringe on the slices
487         fx.glitch.red  = { x:  ch, y: 0 };
488         fx.glitch.blue = { x: -ch, y: 0 };
489      }
490
491      if(m === FX_RGB || m === FX_COMBO){          // rgb channel split converging
492         var s = 12 * k;
493         var j = (m === FX_COMBO) ? 0 : (Math.random() - 0.5) * 2 * k;   // tiny shake (solo only)
494         fx.rgb.red   = { x: -s + j, y: 0 };
495         fx.rgb.green = { x: 0, y: s * 0.3 };
496         fx.rgb.blue  = { x:  s + j, y: 0 };
497      }
498
499      if(m === FX_CRT){                            // crt tune-in (cranked up)
500         fx.crt.time += sc.dt * 70;                                 // fast rolling noise
501         fx.crt.noise = (0.8 + 0.2 * Math.random()) * k;           // heavy, flickering static
502         fx.crt.noiseSize = 1.5;
503         fx.crt.lineContrast = 1.5 * k;                            // hard black scanlines
504         fx.crt.lineWidth = 3 + 18 * k;                            // fat bands that thin out
505         fx.crt.vignetting = 0.4 * k;                              // dark pulsing edges
506         fx.crt.curvature = 3 * k;                                 // screen bulge that settles flat
507         // big "vertical hold" roll, scaled to the element size, with an occasional
508         // hard jump (the picture losing sync) on top of the shimmer. Vertical only.
509         var jump = (Math.random() < 0.18 * k) ? (Math.random() - 0.5) * i.baseH * 2.5 * k : 0;
510         i.target.y = i.baseY + (Math.random() - 0.5) * i.baseH * 1.2 * k + jump;
511      }
512
513      // resolved to clean — drop the filters and let any alpha tween finish on its own
514      if(i.t >= i.inDur){
515         i.target.filters = [];
516         i.target.x = i.baseX;
517         i.target.y = i.baseY;
518         this.instances.splice(n, 1);
519         if(i.onResolved) i.onResolved();
520      }
521   }
522};
523
524app.glitchFX = glitchFX;
525
526// Invalid email: glitch the red message in, hold, fade out.
527function showInvalidEmail(){
528   glitchFX.run(sc.errorText, { mode: FX_COMBO });
529   createjs.Tween.get(sc.errorText, { override: true })
530                 .to({ alpha: 1 }, 500)
531                 .wait(2500)
532                 .to({ alpha: 0 }, 600);
533}
534
535function SumbitEmail()
536{
537   console.log(sc.signUpInput.value + " submitted!");
538
539   // Fire the Google Form WITHOUT any cookies. credentials:'omit' tells the
540   // browser to send no cookies and store none from the response, so Google's
541   // third-party tracking cookies never enter the picture for the signup (and
542   // Chrome's "blocked third-party cookie" warning doesn't fire for it). 'no-cors'
543   // is required because the Forms endpoint sends no CORS headers — the response
544   // is opaque, but that's fine: this is fire-and-forget, same as the old hidden
545   // iframe POST. (urlencoded body matches the original form's default enctype.)
546   var formBody = new URLSearchParams();
547   formBody.append('entry.341504647', sc.signUpInput.value);
548   formBody.append('entry.594837785', sc.userCountry);
549   fetch('https://docs.google.com/forms/d/e/1FAIpQLSfRFpPJyMmMDlOBx3hTvhWGIxYi-vm-h8OidXe15VAMA31ndA/formResponse',
550         { method: 'POST', mode: 'no-cors', credentials: 'omit', body: formBody })
551      .catch(function(){ /* opaque/fire-and-forget; nothing to read or recover */ });
552
553   // swap the form out for the green confirmation
554   createjs.Tween.removeTweens(sc.errorText);
555   sc.errorText.filters = [];
556   sc.errorText.alpha = 0;
557   sc.submitButton.visible = false;
558   sc.inputContainer.visible = false;
559   sc.inputUnderline.visible = false;
560   sc.signUpPixiApp.stage.removeChild(sc.errorText);
561   // hide + blur the capture input so it can't be focused while the form is gone
562   sc.signUpInput.blur();
563   sc.signUpInput.style.display = 'none';
564
565   showSubmitted();
566}
567
568// Green "Email Submitted!" glitches in, holds, fades out, then the form returns.
569// One alpha tween owns the whole in/hold/out envelope — running glitchFX with
570// fadeIn here would start a second tween that this one's override:true cancels.
571function showSubmitted(){
572   glitchFX.run(sc.submittedText, { mode: FX_COMBO });
573   sc.submittedText.alpha = 0;
574   createjs.Tween.get(sc.submittedText, { override: true })
575                 .to({ alpha: 1 }, 500)            // glitch + fade in
576                 .wait(3000)                       // hold the confirmation
577                 .to({ alpha: 0 }, 600)            // then fade it out
578                 .call(restoreSignupForm);
579}
580
581// Bring the email field + submit button back so another address can be entered.
582// Both are Pixi objects again, so they glitch in with the real filter effect.
583function restoreSignupForm(){
584   sc.submitButton.visible = true;
585   sc.inputContainer.visible = true;
586   sc.inputUnderline.visible = true;
587   sc.signUpPixiApp.stage.addChild(sc.errorText);
588   sc.errorText.alpha = 0;
589
590   // fresh, empty field for the next sign-up
591   sc.signUpInput.value = '';
592   sc.signUpInput.style.display = '';
593   signup.updateInputDisplay();
594   signup.updateEmailValidity();   // empty again → button back to base colour
595
596   // glitch the field text (placeholder) and the button back in
597   glitchFX.run(sc.inputDisplayText, { mode: FX_COMBO });
598   glitchFX.run(sc.submitButton, { mode: FX_COMBO, fadeIn: true });
599}
600
601// Show/hide the whole sign-up form (headings, field, button) in one call — used by
602// the easter egg to clear the screen for SYSTEM FAILURE and bring it back on reboot.
603signup.setFormVisible = function(v){
604   sc.updatesText.visible = v;
605   sc.signUpText.visible = v;
606   sc.inputContainer.visible = v;
607   sc.inputUnderline.visible = v;
608   sc.submitButton.visible = v;
609   sc.errorText.visible = v;
610};
611
612// --- easter egg: rage-clicking Submit on an invalid email -----------------
613// Mash Submit with an invalid email fast enough (each click within WINDOW of the
614// last, else the streak resets). From START_AT the whole CRT screen glitches,
615// harder with each click; at FAIL_AT everything clears to "SYSTEM FAILURE", then a
616// "Rebooting..." line, then the form tunes back in with the CRT channel-in (FX_CRT).
617// Whole-screen glitch is GlitchFilter + RGBSplitFilter appended to the stage's
618// filter list ALONGSIDE the persistent CRT filter, so the CRT stays live throughout.
619var egg = {
620   WINDOW: 0.4,        // max seconds between clicks to keep the streak alive
621   START_AT: 10,       // streak at which the screen-wide glitch begins
622   FAIL_AT: 20,        // streak at which SYSTEM FAILURE triggers
623   FAIL_HOLD: 2.5,     // seconds SYSTEM FAILURE holds before it fades out
624   DOTS_DUR: 4.0,      // seconds the "Rebooting" dots animate
625   PAUSE: 0.7,         // blank-screen pause between each stage
626   FADE_MS: 150,       // quick alpha fade in/out for the failure + reboot text
627
628   count: 0,
629   lastT: -999,
630   phase: 'none',      // 'none' | 'fail' | 'gap1' | 'dots' | 'gap2'  (state machine)
631   phaseT: 0,
632
633   glitch: null,       // lazily-built stage filters
634   rgb: null,
635   active: false,      // screen glitch currently attached to the stage
636   screenK: 0,         // current screen-glitch strength (eased to 0)
637   _rt: 0              // glitch slice-refresh timer
638};
639
640egg.reset = function(){ this.count = 0; this.lastT = -999; };
641
642egg.ensureScreenFilters = function(){
643   if(this.glitch) return;
644   this.glitch = new PIXI.filters.GlitchFilter({ slices: 8, offset: 0 });
645   this.glitch.padding = 80;
646   this.rgb = new PIXI.filters.RGBSplitFilter({ red: { x: 0, y: 0 }, green: { x: 0, y: 0 }, blue: { x: 0, y: 0 } });
647   this.rgb.padding = 30;
648};
649
650// Called from TrySumbitEmail's invalid branch.
651egg.registerInvalidClick = function(){
652   var now = performance.now() / 1000;
653   if(now - this.lastT <= this.WINDOW) this.count++;
654   else                                this.count = 1;
655   this.lastT = now;
656
657   if(this.count >= this.FAIL_AT){
658      this.triggerFailure();
659   } else if(this.count >= this.START_AT){
660      // ramp from a clearly-visible floor at the first trigger up to full strength
661      // just before failure (step goes 1 .. FAIL_AT-START_AT-1, e.g. 1..9).
662      var span = this.FAIL_AT - this.START_AT - 1;
663      var step = this.count - this.START_AT;                     // 0 .. span
664      this.triggerScreenGlitch(0.35 + 0.65 * (step / span));
665   } else {
666      showInvalidEmail();   // ordinary 1..9 behaviour
667   }
668};
669
670// attach (if needed) and pump the screen glitch up to `strength` (0..1)
671egg.triggerScreenGlitch = function(strength){
672   this.ensureScreenFilters();
673   if(!this.active){
674      this.active = true;
675      sc.signUpPixiApp.stage.filters = [ crtFilter.filter, this.glitch, this.rgb ];
676   }
677   if(strength > this.screenK) this.screenK = strength;
678};
679
680// push the glitch/rgb filter uniforms for the current strength k
681egg._applyScreenGlitch = function(k){
682   this._rt += sc.dt;
683   this.glitch.offset = 80 * k;
684   if(k > 0.02 && this._rt > 0.04){ this._rt = 0; this.glitch.refresh(); this.glitch.seed = Math.random(); }
685   this.glitch.red  = { x:  6 * k, y: 0 };
686   this.glitch.blue = { x: -6 * k, y: 0 };
687   var s = 16 * k;
688   this.rgb.red   = { x: -s, y: 0 };
689   this.rgb.green = { x: 0, y: s * 0.3 };
690   this.rgb.blue  = { x:  s, y: 0 };
691};
692
693egg.detachScreenGlitch = function(){
694   this.active = false;
695   this.screenK = 0;
696   sc.signUpPixiApp.stage.filters = [ crtFilter.filter ];
697   sc.signUpPixiApp.stage.y = 0;
698};
699
700egg.triggerFailure = function(){
701   this.count = 0;
702   this.phase = 'fail';
703   this.phaseT = 0;
704
705   this.ensureScreenFilters();
706   if(!this.active){
707      this.active = true;
708      sc.signUpPixiApp.stage.filters = [ crtFilter.filter, this.glitch, this.rgb ];
709   }
710   this.screenK = 1;   // climax
711
712   // clear the screen (CRT stays), then tune SYSTEM FAILURE in with the channel-in
713   signup.setFormVisible(false);
714   sc.signUpInput.blur();
715   sc.signUpInput.style.display = 'none';
716   sc.systemFailureText.visible = true;
717   sc.systemFailureText.alpha = 0;
718   createjs.Tween.get(sc.systemFailureText, { override: true }).to({ alpha: 1 }, this.FADE_MS);
719   glitchFX.run(sc.systemFailureText, { mode: FX_CRT, inDur: 1.2 });
720};
721
722egg._startDots = function(){
723   this.phase = 'dots';
724   this.phaseT = 0;
725   sc.rebootingText.text = 'Rebooting.';
726   sc.rebootingText.visible = true;
727   sc.rebootingText.alpha = 0;
728   createjs.Tween.get(sc.rebootingText, { override: true }).to({ alpha: 1 }, this.FADE_MS);
729};
730
731egg._startRebootIn = function(){
732   this.phase = 'none';
733   this.phaseT = 0;
734   this.detachScreenGlitch();
735
736   signup.setFormVisible(true);
737   sc.signUpInput.style.display = '';
738   sc.signUpInput.value = '';
739   signup.updateInputDisplay();
740   signup.updateEmailValidity();
741   // the initial text + input come back with the same CRT channel-in effect
742   glitchFX.run(sc.updatesText,      { mode: FX_CRT, inDur: 1.0, fadeIn: true });
743   glitchFX.run(sc.signUpText,       { mode: FX_CRT, inDur: 1.0, fadeIn: true });
744   glitchFX.run(sc.inputDisplayText, { mode: FX_CRT, inDur: 1.0, fadeIn: true });
745   glitchFX.run(sc.submitButton,     { mode: FX_CRT, inDur: 1.0, fadeIn: true });
746};
747
748// per-frame; driven from app.update (scene.js)
749egg.update = function(){
750   if(this.phase !== 'none'){
751      this.phaseT += sc.dt;
752
753      // ease the whole-screen glitch out across the failure + dots phases
754      if(this.active){
755         this.screenK -= sc.dt * 1.2;
756         if(this.screenK <= 0){
757            this.detachScreenGlitch();
758         } else {
759            this._applyScreenGlitch(this.screenK);
760            sc.signUpPixiApp.stage.y = (Math.random() - 0.5) * 12 * this.screenK;  // vertical-hold wobble
761         }
762      }
763
764      if(this.phase === 'fail'){
765         // hold SYSTEM FAILURE, then fade it out and pause on a blank screen
766         if(this.phaseT >= this.FAIL_HOLD){
767            createjs.Tween.get(sc.systemFailureText, { override: true })
768                          .to({ alpha: 0 }, this.FADE_MS)
769                          .call(function(){ sc.systemFailureText.visible = false; });
770            this.phase = 'gap1';
771            this.phaseT = 0;
772         }
773      } else if(this.phase === 'gap1'){
774         // short pause before "Rebooting..." appears
775         if(this.phaseT >= this.PAUSE) this._startDots();
776      } else if(this.phase === 'dots'){
777         var dots = (Math.floor(this.phaseT / 0.6) % 3) + 1;    // 1 → 2 → 3, cycling
778         sc.rebootingText.text = 'Rebooting' + '.'.repeat(dots);
779         // animate the dots, then fade out and pause before the form returns
780         if(this.phaseT >= this.DOTS_DUR){
781            createjs.Tween.get(sc.rebootingText, { override: true })
782                          .to({ alpha: 0 }, this.FADE_MS)
783                          .call(function(){ sc.rebootingText.visible = false; });
784            this.phase = 'gap2';
785            this.phaseT = 0;
786         }
787      } else if(this.phase === 'gap2'){
788         // short pause before the form tunes back in
789         if(this.phaseT >= this.PAUSE) this._startRebootIn();
790      }
791      return;
792   }
793
794   // escalation phase (clicks 10..19): glitch decays between clicks
795   if(!this.active) return;
796   this.screenK -= sc.dt * 1.8;
797   if(this.screenK <= 0){
798      this.detachScreenGlitch();
799      return;
800   }
801   this._applyScreenGlitch(this.screenK);
802   sc.signUpPixiApp.stage.y = (Math.random() - 0.5) * 10 * this.screenK;
803};
804
805app.egg = egg;
806
807function validateEmail(email) {
808   const re = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
809   return re.test(String(email).toLowerCase());
810}

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.