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.