1<!DOCTYPE html> 2<html lang="en"> 3<head> 4 <meta charset="utf-8"> 5 <meta name="viewport" content="width=device-width, initial-scale=1"> 6 <title>The Sock Drawer â Ed The Sock</title> 7 <link rel="stylesheet" href="/assets/wp-blocks.css?v=6dddaf47"> 8 <link rel="preconnect" href="https://fonts.googleapis.com"> 9 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 10 <!-- Roboto 400/500 + Oxanium 400/600 â hero captions/attribution (config/site-fonts.json) --> 11 <link rel="preload" as="style" href="https://fonts.googleapis.com/css2?family=Oxanium:wght@400;600&family=Roboto:wght@400;500&family=Source+Sans+3:wght@400&display=swap"> 12 <link href="https://fonts.googleapis.com/css2?family=Oxanium:wght@400;600&family=Roboto:wght@400;500&family=Source+Sans+3:wght@400&display=swap" rel="stylesheet"> 13
13<script> 14 (function () { 15 if (!document.fonts || !document.fonts.load) return; 16 ["Roboto", "Oxanium", "Source Sans 3"].forEach(function (name) { 17 document.fonts.load('400 16px "' + name + '"').catch(function () {}); 18 document.fonts.load('500 16px "' + name + '"').catch(function () {}); 19 document.fonts.load('600 16px "' + name + '"').catch(function () {}); 20 }); 21 })(); 22 </script>
22 23</head> 24<body class="page-no-title"> 25<link rel="preconnect" href="https://fonts.googleapis.com"> 26<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 27<!-- Nav font: the original uses "InterFace" (Dalton Maag, commercial, served 28 via the Squarespace Adobe Fonts kit â can't be rehosted). Source Sans 3 29 measured closest of 12 Google candidates: "The Sock Drawer" @19.2px is 30 134.4px vs InterFace 131.4px (Roboto was 148, Inter 157). --> 31<link href="https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400&display=swap" rel="stylesheet"> 32<style> 33/* Shared header height for page-content offsets (hero, page titles). 34 Measured live: desktop header 119px, mobile 94px. */ 35:root { 36 --eds-header-h: 119px; 37} 38 39@media (max-width: 900px) { 40 :root { 41 --eds-header-h: 94px; 42 } 43} 44 45/* Site body background = mint brick (brick + 15% green), like the original. 46 Pages cover it with their own cover blocks; it shows in the strip behind 47 the fixed frosted header, which blurs it into the sage tint that keeps 48 the ED THE SOCK.COM logo readable (on the original this is the first 49 section's background extending under the header). */ 50body { 51 background-color: #b9cfc2; 52 background-image: linear-gradient(rgba(32, 157, 80, 0.15), rgba(32, 157, 80, 0.15)), 53 url("/assets/images/site-background.webp"); 54 background-size: cover; 55 background-position: center top; 56 background-attachment: fixed; 57 background-repeat: no-repeat; 58} 59 60/* Theme gives cover blocks a default 16px padding all around; original 61 Squarespace sections have no top inset (page offsets via --eds-header-h 62 margins assume content starts at the cover's top edge) and a 26px side 63 gutter for the fluid grid. */ 64.wp-block-cover.alignfull { 65 padding: 0 26px 16px; 66} 67 68/* Squarespace-style frosted fixed header: content scrolls underneath. 69 Original: header-blur-background blur(12px) + header-background-solid 70 rgba(32,157,80,0.09) @ .95 + drop shadow; logo 307x116 at ~26px left. */ 71.eds-site-header { 72 position: fixed; 73 top: 0; 74 left: 0; 75 right: 0; 76 z-index: 100; 77 background: transparent !important; 78} 79 80/* The frosted tint lives on a pseudo-element, NOT the header itself: 81 backdrop-filter on the header would make it a containing block for 82 position:fixed descendants, trapping the mobile menu overlay inside 83 the header strip instead of letting it fill the screen. */ 84.eds-site-header::before { 85 content: ""; 86 position: absolute; 87 inset: 0; 88 z-index: -1; 89 background: rgba(32, 157, 80, 0.09); 90 -webkit-backdrop-filter: blur(12px); 91 backdrop-filter: blur(12px); 92 /* Original: .header-dropshadow renders box-shadow 0 12px 12px black at 93 element opacity .15 â a soft gray band under the header */ 94 box-shadow: 0 12px 12px rgba(0, 0, 0, 0.15); 95} 96 97/* Fallback when backdrop-filter is unsupported: keep the old solid tint */ 98@supports not ((backdrop-filter: blur(12px)) or (-webkit-backdrop-filter: blur(12px))) { 99 .eds-site-header::before { 100 background: rgba(168, 197, 181, 0.95); 101 } 102} 103 104/* Full-screen mobile menu overlay, matching the original Squarespace one: 105 warm light-gray sheet (sampled #e1dfd9), large left-aligned black links 106 vertically centered, close X top-right (WP renders the X natively). */ 107.eds-site-header .wp-block-navigation__responsive-container.is-menu-open { 108 /* !important: WP core sets background-color:inherit on .is-menu-open, 109 which resolves to the theme's white */ 110 background-color: #e1dfd9 !important; 111 /* Let note-dragging own the gesture (menu doesn't scroll) */ 112 touch-action: none; 113} 114 115/* While the menu is open nothing is selectable â double/triple taps and 116 long-presses were selecting text mid-play. Links and the close X still 117 tap fine (user-select doesn't affect clicks). :has() covers the page 118 behind the sheet too. */ 119body:has(.eds-site-header .wp-block-navigation__responsive-container.is-menu-open) { 120 -webkit-user-select: none; 121 user-select: none; 122} 123 124/* Home-only hitch: while the sheet is open, skip painting the page behind 125 it (hero WebP layers + live drop-shadow filters + embeds). Other pages 126 are light enough that this barely matters; on home it frees the main 127 thread for the note canvas. Header stays visible (owns the overlay). */ 128html.eds-menu-open body > *:not(.eds-site-header) { 129 visibility: hidden !important; 130 pointer-events: none !important; 131} 132html.eds-menu-open .eds-ed-overlay, 133html.eds-menu-open .eds-ed-highlight, 134html.eds-menu-open .eds-logo-overlay, 135html.eds-menu-open .eds-logo-red-boost { 136 filter: none !important; 137 animation: none !important; 138} 139 140.eds-site-header .wp-block-navigation__responsive-container.is-menu-open, 141.eds-site-header .wp-block-navigation__responsive-container.is-menu-open * { 142 -webkit-user-select: none !important; 143 user-select: none !important; 144 -webkit-touch-callout: none; 145} 146 147.eds-site-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-close, 148.eds-site-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-dialog, 149.eds-site-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-content { 150 height: 100%; 151} 152 153.eds-site-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-container-content { 154 display: flex; 155 flex-direction: column; 156 overflow-y: auto; 157 padding-left: 11px; /* original links start 27px from edge (16px WP padding + 11) */ 158} 159 160/* Vertical centering via auto margins instead of justify-content:center â 161 centers when there's room, top-aligns and scrolls (no clipping) when the 162 viewport is short (landscape phones). The ul defaults to flex:1, which 163 would swallow the auto margins, so pin it to its content size. */ 164.eds-site-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__container { 165 flex: 0 0 auto; 166 margin-top: auto; 167 margin-bottom: auto; 168} 169 170/* Short viewports (landscape phones): smaller links + tighter gap so all 171 five items fit without scrolling */ 172@media (max-height: 500px) { 173 .eds-site-header .wp-block-navigation__responsive-container.is-menu-open a { 174 font-size: 24px; 175 } 176 177 .eds-site-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__container { 178 gap: 10px; 179 } 180} 181 182.eds-site-header .wp-block-navigation__responsive-container.is-menu-open a { 183 font-size: 33px; 184 font-weight: 400; 185 color: #000; 186} 187 188/* Full-bleed inner row, tight vertical padding, logo near the corner */ 189.eds-site-header > .wp-block-group { 190 max-width: none;
191 padding: 2px 24px 2px 26px; 192} 193 194.eds-site-header .wp-block-site-logo { 195 margin: 0; 196} 197 198/* Nav font â original computed: InterFace 19.18px w400, letter-spacing 0.19px */ 199.eds-site-header .wp-block-navigation a { 200 font-family: "Source Sans 3", "Roboto", sans-serif; 201 font-size: 19.2px; 202 font-weight: 400; 203 letter-spacing: 0.01em; 204} 205 206/* Current-page underline â original: 1px black repeat-x gradient sitting 207 ~2px above the link box bottom (Squarespace .header-nav-item--active) */ 208.eds-site-header .wp-block-navigation .current-menu-item > a { 209 background-image: linear-gradient(currentColor, currentColor); 210 background-size: 1px 1px; 211 background-repeat: repeat-x; 212 background-position: 0 calc(100% - 2px); 213} 214 215/* Load animation â Squarespace header-element preSlide/slideIn: 216 .preSlide{opacity:0;transform:translate(0,30%)} then slides to rest. 217 Pure CSS (animation plays without JS; `backwards` holds the start state 218 during the delay). */ 219@keyframes eds-header-in { 220 from { 221 opacity: 0; 222 transform: translateY(30%); 223 } 224} 225 226.eds-site-header .wp-block-site-logo { 227 animation: eds-header-in 0.65s ease 0.05s backwards; 228} 229 230.eds-site-header .wp-block-navigation { 231 animation: eds-header-in 0.65s ease 0.18s backwards; 232} 233 234@media (prefers-reduced-motion: reduce) { 235 .eds-site-header .wp-block-site-logo, 236 .eds-site-header .wp-block-navigation { 237 animation: none; 238 } 239} 240 241/* Music-note particle layer behind the mobile menu links. The canvas is a 242 direct child of the overlay (z-index 0) and the dialog sits above it. */ 243.eds-menu-notes { 244 display: none; 245} 246 247.eds-site-header .wp-block-navigation__responsive-container.is-menu-open .eds-menu-notes { 248 display: block; 249 position: absolute; 250 inset: 0; 251 width: 100%; 252 height: 100%; 253 pointer-events: none; 254 z-index: 0; 255} 256 257.eds-site-header .wp-block-navigation__responsive-container.is-menu-open .wp-block-navigation__responsive-close { 258 position: relative; 259 z-index: 1; 260} 261 262/* Guitar-hero mode (easter egg): the menu links vanish, the canvas game 263 takes the sheet. The close X lives outside the content container and
264 stays usable. NOTE: opacity, not visibility:hidden â hiding the focused 265 menu link fires a focusout that WP's navigation script treats as "focus 266 left the modal" and it closes the menu mid-game. */ 267.eds-site-header .wp-block-navigation__responsive-container.is-menu-open.eds-gh-mode .wp-block-navigation__responsive-container-content { 268 opacity: 0; 269 pointer-events: none; 270 transition: opacity 0.25s ease; 271} 272</style>
273<script> 274/* Mobile-menu music notes: float up behind the links, drift with device 275 tilt, and "break" on a vigorous shake â falling into a pile at the 276 bottom until the menu is reopened. 277 No bare less-than chars in this script (wptexturize gotcha â see 278 docs/wordpress-operations.md). Comparisons are written flipped. */ 279(function () { 280 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; 281 282 var GLYPHS = ['\u266A', '\u266B', '\u2669', '\u266C']; 283 var COLORS = [[26, 110, 60], [32, 157, 80], [70, 70, 70]]; 284 var canvas, ctx, overlay, raf = 0, running = false, startPending = false; 285 var notes = [], W = 0, H = 0, dpr = 1; 286 var tilt = 0; /* smoothed gamma, -1..1 */ 287 var broken = false; 288 var shakeHits = [], lastJolt = 0, joltMag = 0; 289 var pile = [], BIN = 22, STEP = 11; 290 var desiredFloats = 0, refillAt = 0; 291 292 function rnd(a, b) { return a + Math.random() * (b - a); } 293 294 function makeNote(atBottom) { 295 var c = COLORS[Math.floor(rnd(0, COLORS.length))]; 296 return { 297 x: rnd(10, Math.max(11, W - 10)), 298 y: atBottom ? rnd(H, H + H * 0.6) : rnd(0, H), 299 spd: rnd(0.35, 1.1), 300 ph: rnd(0, 6.28), 301 phv: rnd(0.01, 0.03), 302 size: rnd(26, 54), 303 glyph: GLYPHS[Math.floor(rnd(0, GLYPHS.length))], 304 col: 'rgba(' + c[0] + ',' + c[1] + ',' + c[2] + ',' + rnd(0.16, 0.38).toFixed(2) + ')', 305 mode: 'float', 306 rot: 0, vx: 0, vy: 0, settled: false, restY: 0 307 }; 308 } 309 310 function resetNotes() { 311 broken = false; 312 shakeHits = []; 313 pile = []; 314 notes = []; 315 var count = Math.max(16, Math.min(32, Math.round((W * H) / 22000))); 316 desiredFloats = count; 317 for (var i = 0; count > i; i++) notes.push(makeNote(false)); 318 } 319 320 /* Keep the sky stocked: as notes go dead (dropped or shaken), send fresh 321 floaters from the bottom â until over 50 dead ones have piled up, then 322 recycle the pile instead of growing it forever. */ 323 function recycleDead() { 324 for (var i = 0; notes.length > i; i++) { 325 var n = notes[i]; 326 if (n.mode === 'fall' && n.settled) { 327 var b = Math.max(0, Math.min(Math.floor(n.x / BIN), Math.floor(W / BIN))); 328 if (pile[b]) pile[b]--; 329 notes[i] = makeNote(true); 330 return; 331 } 332 } 333 } 334 335 function refill() { 336 if (broken) return; /* shaken-broke stays broke */ 337 if (egg && egg.phase !== 'collect') return; /* not during the game */ 338 var floats = 0, dead = 0; 339 for (var i = 0; notes.length > i; i++) { 340 if (notes[i].mode === 'float') floats++; 341 else if (notes[i].mode === 'fall') dead++; 342 } 343 if (floats >= desiredFloats) return; 344 if (dead > 50) recycleDead(); 345 else notes.push(makeNote(true)); 346 } 347 348 function unsettleAll() { 349 pile = []; 350 for (var i = 0; notes.length > i; i++) { 351 notes[i].settled = false; 352 if (notes[i].mode !== 'drag') notes[i].mode = 'fall'; 353 } 354 } 355 356 function floorFor(n) { 357 if (n.settled) return n.restY; 358 var b = Math.max(0, Math.min(Math.floor(n.x / BIN), Math.floor(W / BIN))); 359 return H - 8 - (pile[b] || 0) * STEP; 360 } 361 362 function settle(n) { 363 var b = Math.max(0, Math.min(Math.floor(n.x / BIN), Math.floor(W / BIN))); 364 pile[b] = (pile[b] || 0) + 1; 365 n.restY = H - 8 - (pile[b] - 1) * STEP; 366 n.settled = true; 367 n.vy = 0; 368 } 369 370 function step() { 371 ctx.clearRect(0, 0, W, H); 372 var now = Date.now(); 373 var jolting = 400 > now - lastJolt; 374 if (now > refillAt) { refillAt = now + 700; refill(); } 375 for (var i = 0; notes.length > i; i++) { 376 var n = notes[i]; 377 if (n.mode === 'drag') { 378 /* position is driven by the pointer handlers */ 379 } else if (n.mode === 'float') { 380 n.ph += n.phv; 381 n.y -= n.spd; 382 n.x += Math.sin(n.ph) * 0.5 + tilt * 1.4; 383 n.rot = Math.sin(n.ph) * 0.3 + tilt * 0.5; 384 if (0 > n.y + 60) { notes[i] = makeNote(true); continue; } 385 if (0 > n.x - 10) n.x = W + 8; 386 if (n.x > W + 12) n.x = -8; 387 } else { /* fall (broken by shake, or dropped after a drag) */ 388 if (jolting && 0.35 > Math.random()) { 389 n.settled = false;
390 n.vy -= rnd(0, joltMag * 0.9); 391 n.vx += rnd(-1, 1) * joltMag * 0.6; 392 } 393 if (!n.settled) { 394 n.vy += 0.42; 395 n.vx += tilt * 0.06; 396 n.vx *= 0.985; 397 n.x += n.vx; 398 n.y += n.vy; 399 n.rot += n.vx * 0.02; 400 var fy = floorFor(n); 401 if (n.y > fy) { 402 n.y = fy; 403 if (Math.abs(n.vy) > 2.2) { 404 n.vy = -n.vy * 0.32; 405 n.vx *= 0.7; 406 } else { 407 settle(n); 408 } 409 } 410 if (8 > n.x) { n.x = 8; n.vx = Math.abs(n.vx) * 0.5; } 411 if (n.x > W - 8) { n.x = W - 8; n.vx = -Math.abs(n.vx) * 0.5; } 412 if (0 > n.y) { n.y = 0; n.vy = Math.abs(n.vy) * 0.4; } 413 } 414 } 415 ctx.save(); 416 ctx.translate(n.x, n.y); 417 ctx.rotate(n.rot); 418 ctx.font = n.size + 'px serif'; 419 ctx.fillStyle = n.col; 420 ctx.textAlign = 'center'; 421 ctx.fillText(n.glyph, 0, 0); 422 ctx.restore(); 423 } 424 updateEgg(now); 425 if (running) raf = requestAnimationFrame(step); 426 } 427 428 function start() { 429 if (running || startPending || !overlay) return; 430 startPending = true; 431 if (!canvas) { 432 canvas = document.createElement('canvas'); 433 canvas.className = 'eds-menu-notes'; 434 canvas.setAttribute('aria-hidden', 'true'); 435 overlay.insertBefore(canvas, overlay.firstChild); 436 ctx = canvas.getContext('2d'); 437 } 438 /* Menu layout often settles a frame after is-menu-open â measuring too 439 early yields a tiny bitmap that CSS stretches fullscreen (huge notes). */ 440 function tryStart() { 441 if (!overlay || !overlay.classList.contains('is-menu-open')) { 442 startPending = false; 443 return; 444 } 445 var m = readOverlaySize(); 446 if (!m) { 447 requestAnimationFrame(tryStart); 448 return; 449 } 450 startPending = false; 451 applyCanvasSize(m.w, m.h); 452 resetNotes(); 453 resetEgg(); 454 if (overlay.classList.contains('eds-gh-mode')) { 455 overlay.classList.remove('eds-gh-mode'); 456 } 457 running = true; 458 raf = requestAnimationFrame(step); 459 } 460 requestAnimationFrame(function () { 461 requestAnimationFrame(tryStart); 462 }); 463 } 464 465 function stop() { 466 startPending = false; 467 running = false; 468 drag = null; 469 try { releaseAllHolds(); } catch (err) {} 470 /* Only touch the class if present: classList.remove() writes the 471 attribute even when the token is absent, which re-fires the 472 MutationObserver and would loop stop() forever. */ 473 if (overlay && overlay.classList.contains('eds-gh-mode')) { 474 overlay.classList.remove('eds-gh-mode'); 475 } 476 if (raf) cancelAnimationFrame(raf); 477 raf = 0; 478 } 479 480 function readOverlaySize() { 481 if (!overlay) return null; 482 var r = overlay.getBoundingClientRect(); 483 var w = Math.round(r.width), h = Math.round(r.height); 484 if (80 > w || 80 > h) return null; 485 return { w: w, h: h }; 486 } 487 488 function applyCanvasSize(w, h) { 489 dpr = Math.min(window.devicePixelRatio || 1, 2); 490 W = w; 491 H = h; 492 canvas.width = W * dpr; 493 canvas.height = H * dpr; 494 ctx.setTransform(dpr, 0, 0, dpr, 0, 0); 495 } 496 497 /* Rotation/resize while the menu is open: re-measure the overlay, resize 498 the bitmap, scale note positions into the new space and let settled 499 notes re-fall to the new floor (their old rest heights are stale). */ 500 function resize() { 501 if (!running || !overlay || !canvas) return; 502 var m = readOverlaySize(); 503 if (!m) return; 504 var w = m.w, h = m.h; 505 if (w === W && h === H) { 506 /* Still refresh dpr (zoom / move between screens). */ 507 var nextDpr = Math.min(window.devicePixelRatio || 1, 2); 508 if (nextDpr === dpr) return; 509 applyCanvasSize(w, h); 510 return; 511 } 512 var sx = w / W, sy = h / H; 513 applyCanvasSize(w, h); 514 pile = []; 515 drag = null; 516 for (var i = 0; notes.length > i; i++) { 517 var n = notes[i]; 518 n.x = Math.max(8, Math.min(n.x * sx, W - 8)); 519 n.y = Math.min(n.y * sy, H - 8); 520 if (n.mode === 'drag') n.mode = 'fall'; 521 n.settled = false;
522 } 523 } 524 525 /* Electric guitar: each touched note plays the next pitch of the intro 526 riff (user-supplied tab, standard tuning, key of D). Karplus-Strong 527 plucked string into tanh distortion + lowpass "cab". MIDI numbers: 528 string base + fret (D=50, G=55, B=59, e=64). */ 529 var AC = null, distIn = null, drvIn = null, offIn = null, seqIdx = 0; 530 531 var SEQ = (function () { 532 function rep2(a) { return a.concat(a); } 533 /* half-measure pattern: low note, then B14 G13 G11 e14 G13 e13 G13 */ 534 function line(low) { return [low, 73, 68, 66, 78, 68, 77, 68]; } 535 var L1 = rep2(line(61)); /* D11 measures */ 536 var L2 = rep2(line(63)); /* D13 measures */ 537 var L3 = rep2(line(66)); /* G11 measures */ 538 var part1 = [].concat(L1, L2, L3, L1); 539 var part3 = [75, 68, 73, 68, 75, 68, 77, 68, 78, 68, 77, 68, 75, 68, 73]; 540 /* part 1 twice, part 2 (= first three lines) once, part 3 once */ 541 return [].concat(part1, part1, L1, L2, L3, part3); 542 })(); 543 544 function ensureAudio() { 545 if (AC) return true; 546 var Ctor = window.AudioContext || window.webkitAudioContext; 547 if (!Ctor) return false; 548 AC = new Ctor(); 549 var master = AC.createGain(); 550 master.gain.value = 0.24; 551 /* Heavy drive: tanh(16x) is nearly a square wave at full amplitude â 552 the saturated lead tone. Mid bump before the clip adds growl. */ 553 var mid = AC.createBiquadFilter(); 554 mid.type = 'peaking'; 555 mid.frequency.value = 850; 556 mid.Q.value = 0.9; 557 mid.gain.value = 6; 558 var dist = AC.createWaveShaper(); 559 var n = 2048, curve = new Float32Array(n); 560 for (var i = 0; n > i; i++) { 561 var x = (i * 2) / (n - 1) - 1; 562 curve[i] = Math.tanh(16 * x); 563 } 564 dist.curve = curve; 565 dist.oversample = '4x'; 566 var tone = AC.createBiquadFilter(); 567 tone.type = 'lowpass'; 568 tone.frequency.value = 3200; 569 tone.Q.value = 0.7; 570 mid.connect(dist); 571 dist.connect(tone); 572 tone.connect(master); 573 master.connect(AC.destination); 574 distIn = mid; 575 576 /* ROCK-mode amp: huge pre-gain slammed into a hard clipper â the 577 waveform is essentially a square by the time it leaves. Separate 578 chain so SOCK-mode plucks keep the cleaner sound above. */ 579 var pre = AC.createGain(); 580 pre.gain.value = 14; 581 var mid2 = AC.createBiquadFilter(); 582 mid2.type = 'peaking'; 583 mid2.frequency.value = 750; 584 mid2.Q.value = 0.8; 585 mid2.gain.value = 8; 586 var clip = AC.createWaveShaper(); 587 var curve2 = new Float32Array(n); 588 for (i = 0; n > i; i++) { 589 var x2 = (i * 2) / (n - 1) - 1; 590 curve2[i] = Math.tanh(80 * x2); /* near-square hard clip */ 591 } 592 clip.curve = curve2; 593 clip.oversample = '4x'; 594 var cab = AC.createBiquadFilter(); 595 cab.type = 'lowpass'; 596 cab.frequency.value = 2600; 597 cab.Q.value = 1.1; 598 var master2 = AC.createGain(); 599 master2.gain.value = 0.1; /* clipped signal is loud â pull it back */ 600 pre.connect(mid2); 601 mid2.connect(clip); 602 clip.connect(cab); 603 cab.connect(master2); 604 master2.connect(AC.destination); 605 drvIn = pre; 606 607 /* off-note chain: identical amp, slightly quieter output (the clipper 608 compresses everything, so post-clip gain is the only loudness lever) */ 609 var pre3 = AC.createGain(); 610 pre3.gain.value = 14; 611 var mid3 = AC.createBiquadFilter(); 612 mid3.type = 'peaking'; 613 mid3.frequency.value = 750; 614 mid3.Q.value = 0.8; 615 mid3.gain.value = 8; 616 var clip3 = AC.createWaveShaper(); 617 clip3.curve = curve2; 618 clip3.oversample = '4x'; 619 var cab3 = AC.createBiquadFilter(); 620 cab3.type = 'lowpass'; 621 cab3.frequency.value = 2600; 622 cab3.Q.value = 1.1; 623 var master3 = AC.createGain(); 624 master3.gain.value = 0.075; 625 pre3.connect(mid3); 626 mid3.connect(clip3); 627 clip3.connect(cab3); 628 cab3.connect(master3); 629 master3.connect(AC.destination); 630 offIn = pre3; 631 return true; 632 } 633 634 var pluckBufs = {}; /* per-pitch cache, keyed midi + clean/dirty */ 635 636 function pluck(midi, dirty) { 637 if (!ensureAudio()) return; 638 if (AC.state === 'suspended') AC.resume(); 639 /* dirty (ROCK mode): longer, less damped string â the clipper turns 640 the sustained tail into a singing square wave */ 641 var dur = dirty ? 1.6 : 1.1; 642 var key = midi + (dirty ? 'd' : 'c'); 643 var buf = pluckBufs[key]; 644 if (!buf) { 645 var f = 440 * Math.pow(2, (midi - 69) / 12); 646 var sr = AC.sampleRate, N = Math.round(sr * dur); 647 var damp = dirty ? 0.4995 : 0.498; 648 buf = AC.createBuffer(1, N, sr); 649 var d = buf.getChannelData(0); 650 var period = Math.max(2, Math.round(sr / f)); 651 var i; 652 for (i = 0; period >
652 i; i++) d[i] = Math.random() * 2 - 1; 653 for (i = period; N > i; i++) d[i] = (d[i - period] + d[i - period + 1]) * damp; 654 pluckBufs[key] = buf; 655 } 656 var src = AC.createBufferSource(); 657 src.buffer = buf; 658 var g = AC.createGain(); 659 var t = AC.currentTime; 660 g.gain.setValueAtTime(1, t); 661 g.gain.exponentialRampToValueAtTime(0.001, t + dur); 662 src.connect(g); 663 g.connect(dirty ? drvIn : distIn); 664 src.start(t); 665 src.stop(t + dur); 666 } 667 668 function playNext() { 669 try { 670 pluck(SEQ[seqIdx]); 671 seqIdx = (seqIdx + 1) % SEQ.length; 672 } catch (err) { /* audio is a bonus â never break the menu */ } 673 } 674 675 /* off-note for presses that don't count: same square-wave amp and the 676 same attack as a real note, but randomly sour (0.6-1.7 semitones flat 677 or sharp), slightly quieter, and with much less sustain â just "off" */ 678 function offNote(midi) { 679 if (!ensureAudio()) return; 680 if (AC.state === 'suspended') AC.resume(); 681 var sour = (0.5 > Math.random() ? -1 : 1) * rnd(0.6, 1.7); 682 var f = 440 * Math.pow(2, (midi + sour - 69) / 12); 683 var sr = AC.sampleRate, dur = 0.5, N = Math.round(sr * dur); 684 var buf = AC.createBuffer(1, N, sr); 685 var d = buf.getChannelData(0); 686 var period = Math.max(2, Math.round(sr / f)); 687 var i; 688 for (i = 0; period > i; i++) d[i] = Math.random() * 2 - 1; 689 for (i = period; N > i; i++) d[i] = (d[i - period] + d[i - period + 1]) * 0.4995; 690 var src = AC.createBufferSource(); 691 src.buffer = buf; 692 var g = AC.createGain(); 693 var t = AC.currentTime; 694 g.gain.setValueAtTime(1, t); 695 g.gain.exponentialRampToValueAtTime(0.001, t + dur); 696 src.connect(g); 697 g.connect(offIn); 698 src.start(t); 699 src.stop(t + dur); 700 } 701 702 /* representative pitch per lane, for empty-lane flubs */ 703 var LANE_PITCH = [61, 66, 68, 73, 78]; 704 705 /* Held game notes: the string barely decays and the clipper compresses 706 whatever is left, so it sings until the finger lifts (the Slash way). 707 Keyed by pointerId so multi-finger play works. */ 708 var held = {}; 709 var holdBufs = {}; /* per-pitch cache: synthesizing 6s of string on every 710 hit hitched the frame and bunched the lane notes */ 711 712 function pluckHold(midi, pid) { 713 if (!ensureAudio()) return; 714 if (AC.state === 'suspended') AC.resume(); 715 releaseHold(pid); 716 var dur = 6; 717 var buf = holdBufs[midi]; 718 if (!buf) { 719 var f = 440 * Math.pow(2, (midi - 69) / 12); 720 var sr = AC.sampleRate, N = Math.round(sr * dur); 721 buf = AC.createBuffer(1, N, sr); 722 var d = buf.getChannelData(0); 723 var period = Math.max(2, Math.round(sr / f)); 724 var i; 725 for (i = 0; period > i; i++) d[i] = Math.random() * 2 - 1; 726 for (i = period; N > i; i++) d[i] = (d[i - period] + d[i - period + 1]) * 0.49985; 727 holdBufs[midi] = buf; 728 } 729 var src = AC.createBufferSource(); 730 src.buffer = buf; 731 var g = AC.createGain(); 732 var t = AC.currentTime; 733 g.gain.setValueAtTime(1, t); 734 src.connect(g); 735 g.connect(drvIn); 736 src.start(t); 737 src.stop(t + dur); 738 held[pid] = { g: g, src: src }; 739 src.onended = function () { 740 if (held[pid] && held[pid].src === src) delete held[pid]; 741 }; 742 } 743 744 function releaseHold(pid) { 745 var v = held[pid]; 746 if (!v) return; 747 delete held[pid]; 748 /* short tap = short note: the clipper rebuilds even a whisper back to 749 full volume, so a pre-clip fade alone can't end the note â the 750 source must actually stop. Disconnect as a belt-and-braces so the 751 voice dies even if stop() misbehaves. */ 752 var t = AC.currentTime; 753 try { 754 v.g.gain.cancelScheduledValues(t); 755 v.g.gain.setValueAtTime(Math.max(0.001, v.g.gain.value), t); 756 v.g.gain.exponentialRampToValueAtTime(0.001, t + 0.1); 757 } catch (err) {} 758 try { v.src.stop(t + 0.12); } catch (err2) {} 759 setTimeout(function () { 760 try { v.src.disconnect(); } catch (e2) {} 761 try { v.g.disconnect(); } catch (e3) {} 762 }, 200); 763 } 764 765 function releaseAllHolds() { 766 for (var k in held) releaseHold(k); 767 } 768 769 /* ---- SOCK/ROCK easter egg ---------------------------------------- 770 Faint "SOCK" letters sit above the menu. Dragging or throwing a note 771 across a letter lights it. All four lit: the word flashes ("guitar 772 hero mode"), the ambient notes vanish, the S explodes into shards and 773 becomes an R (ROCK slams), the menu links hide, and a 5-button 774 rhythm game starts â lane notes rise from the bottom carrying the 775 riff's own pitches; pressing the matching button in the hit window 776 plucks that pitch. Reset on menu reopen. */ 777 var egg = null; 778 var WORD = ['S', 'O', 'C', 'K']; 779 var GH_COLORS = ['#2ecc40', '#ff4136', '#ffdc00', '#0074d9', '#ff851b']; 780 /* difficulty knobs */ 781 var HIT_WINDOW = 110; /* px around the button row that still counts */ 782 var APPROACH = 300; /* a lane re-arms when its next note is this close */ 783 var MISS_DEBOUNCE = 700; /* misses this close together count as one */ 784 var STRIKE_MISSES = 6; /* counted misses inside STRIKE_WINDOW = one strike */ 785 var STRIKE_WINDOW = 8000; 786 var MAX_STRIKES = 3; 787 var edImg = null, edReady = false;
788 789 function loadEd() { 790 if (edImg) return; 791 edImg = new Image(); 792 edImg.onload = function () { edReady = true; }; 793 edImg.src = '/assets/images/ed-side-eyes.webp'; 794 } 795 796 function resetEgg() { 797 egg = { 798 phase: 'collect', lit: [false, false, false, false], t0: 0, 799 litAt: [0, 0, 0, 0], 800 shards: [], rings: [], gnotes: [], gseq: 0, spawnAt: 0, 801 press: [0, 0, 0, 0, 0], 802 score: 0, gcount: 0, nextSeq: 0, badPresses: 0, 803 strikes: 0, strikeAt: 0, recentMisses: [], 804 laneLock: [false, false, false, false, false] 805 }; 806 } 807 808 function laneFor(m) { 809 if (m === 68) return 2; /* the pedal tone, middle lane */ 810 if (64 > m) return 0; /* low riff notes */ 811 if (67 > m) return 1; 812 if (76 > m) return 3; 813 return 4; /* highest notes */ 814 } 815 816 function letterRects() { 817 var lw = Math.min(60, W / 7.5), gap = Math.min(16, W * 0.03); 818 var total = 4 * lw + 3 * gap; 819 var x0 = (W - total) / 2, top = 46, h = Math.round(lw * 1.1); 820 var out = []; 821 for (var i = 0; 4 > i; i++) out.push({ x: x0 + i * (lw + gap), y: top, w: lw, h: h }); 822 return out; 823 } 824 825 function btnGeom() { 826 /* landscape: bigger buttons, higher row â more runway to watch the 827 notes climb (the score lives in the top-left corner there) */ 828 var landscape = W > H; 829 var spacing = Math.min(landscape ? 88 : 72, (W - 60) / 4); 830 var xs = []; 831 for (var i = 0; 5 > i; i++) xs.push(W / 2 + (i - 2) * spacing); 832 return { 833 xs: xs, 834 y: landscape ? 112 : letterRects()[0].y + 130, 835 r: landscape ? Math.min(34, spacing * 0.42) : Math.min(26, spacing * 0.4) 836 }; 837 } 838 839 function drawWord(allLit, asRock, scale, jx, jy) { 840 var rects = letterRects(); 841 for (var i = 0; 4 > i; i++) { 842 var r = rects[i]; 843 var ch = (asRock && i === 0) ? 'R' : WORD[i]; 844 var cx = r.x + r.w / 2 + (jx || 0), cy = r.y + r.h - 6 + (jy || 0); 845 /* pop: freshly lit letters swell to ~1.6x and settle back over 420ms */ 846 var pop = 1; 847 if (egg.litAt && egg.litAt[i]) { 848 var pt = (Date.now() - egg.litAt[i]) / 420; 849 if (1 > pt) pop = 1 + 0.6 * Math.sin(pt * 3.1416); 850 } 851 ctx.save(); 852 ctx.translate(cx, cy); 853 ctx.scale((scale || 1) * pop, (scale || 1) * pop); 854 ctx.font = 'bold ' + r.h + 'px sans-serif'; 855 ctx.textAlign = 'center'; 856 if (allLit || egg.lit[i]) { 857 ctx.shadowColor = '#209d50'; 858 ctx.shadowBlur = 16; 859 ctx.fillStyle = '#1a7a40'; 860 ctx.fillText(ch, 0, 0); 861 } else { 862 ctx.globalAlpha = 0.2; 863 ctx.lineWidth = 2; 864 ctx.strokeStyle = '#333'; 865 ctx.strokeText(ch, 0, 0); 866 } 867 ctx.restore(); 868 } 869 } 870 871 function updateEgg(now) { 872 if (!egg) return; 873 var i, j, r; 874 if (egg.phase === 'collect') { 875 var rects = letterRects(); 876 for (i = 0; notes.length > i; i++) { 877 var n = notes[i]; 878 /* one letter per interaction: a note lights at most one letter 879 per grab/throw (re-armed on the next grab) */ 880 if ((n.mode === 'drag' || (n.mode === 'fall' && !n.settled)) && !n.letterUsed) { 881 for (j = 0; 4 > j; j++) { 882 if (!egg.lit[j]) { 883 r = rects[j]; 884 if (n.x > r.x && r.x + r.w > n.x && n.y > r.y && r.y + r.h + 12 > n.y) { 885 egg.lit[j] = true; 886 egg.litAt[j] = now; 887 n.letterUsed = true; 888 egg.rings.push({ x: r.x + r.w / 2, y: r.y + r.h / 2, r: 16, life: 1 }); 889 break; 890 } 891 } 892 } 893 } 894 } 895 drawWord(false, false, 1); 896 if (egg.lit[0] && egg.lit[1] && egg.lit[2] && egg.lit[3]) { 897 egg.phase = 'flash'; 898 egg.t0 = now; 899 } 900 } else if (egg.phase === 'flash') { 901 var blink = Math.floor((now - egg.t0) / 130); 902 drawWord(blink % 2 === 0, false, 1); 903 if (blink > 7) { 904 egg.phase = 'explode'; 905 egg.t0 = now; 906 notes = []; 907 drag = null; 908 var s = letterRects()[0]; 909 for (i = 0; 26 > i; i++) { 910 egg.shards.push({ 911 x: s.x + s.w / 2, y: s.y + s.h / 2, 912 vx: rnd(-7, 7), vy: rnd(-9, 3), life: 1, 913 col: 0.5 > Math.random() ? '#1a7a40' : '#555' 914 }); 915 } 916 } 917 } else if (egg.phase === 'explode') { 918 var dur = now - egg.t0; 919 var asRock = dur > 240;
920 var scale = 1, jx = 0, jy = 0; 921 if (asRock) { 922 var p = Math.min(1, (dur - 240) / 280); 923 scale = 1.9 - 0.9 * p; 924 if (500 > dur) { jx = rnd(-4, 4); jy = rnd(-3, 3); } /* the slam */ 925 } 926 drawWord(true, asRock, scale, jx, jy); 927 for (i = egg.shards.length - 1; i >= 0; i--) { 928 var sh = egg.shards[i]; 929 sh.vy += 0.45; 930 sh.x += sh.vx; 931 sh.y += sh.vy; 932 sh.life -= 0.025; 933 if (0 > sh.life) { egg.shards.splice(i, 1); continue; } 934 ctx.globalAlpha = Math.max(0, sh.life); 935 ctx.fillStyle = sh.col; 936 ctx.fillRect(sh.x - 3, sh.y - 3, 6, 6); 937 ctx.globalAlpha = 1; 938 } 939 if (dur > 1100) { 940 egg.phase = 'game'; 941 egg.shards.length = 0; 942 overlay.classList.add('eds-gh-mode'); 943 egg.spawnAt = now + 600; 944 } 945 } else if (egg.phase === 'over') { 946 /* more than 25 missed notes: GAME OVER, Ed fading into view */ 947 var oa = Math.min(1, (now - egg.t0) / 1800); 948 if (edReady) { 949 var iw = edImg.width, ih = edImg.height; 950 var sc = Math.min((W * 0.7) / iw, (H * 0.5) / ih); 951 /* slow breathing pulse around Ed's center */ 952 var pulse = 1 + 0.05 * Math.sin((now - egg.t0) / 280); 953 ctx.save(); 954 ctx.globalAlpha = oa; 955 ctx.translate(W / 2, H * 0.38 + ih * sc * 0.5); 956 ctx.scale(pulse, pulse); 957 ctx.drawImage(edImg, -iw * sc / 2, -ih * sc / 2, iw * sc, ih * sc); 958 ctx.restore(); 959 } 960 ctx.save(); 961 ctx.globalAlpha = oa; 962 var fs = Math.min(76, W / 5.2); 963 ctx.font = 'bold ' + fs + 'px sans-serif'; 964 ctx.textAlign = 'center'; 965 ctx.shadowColor = '#209d50'; 966 ctx.shadowBlur = 18; 967 ctx.fillStyle = '#1a7a40'; 968 ctx.fillText('GAME', W / 2, H * 0.15); 969 ctx.fillText('OVER', W / 2, H * 0.15 + fs * 1.08); 970 var ostr = '' + egg.score; 971 while (5 > ostr.length) ostr = '0' + ostr; 972 ctx.shadowBlur = 8; 973 ctx.font = 'bold ' + Math.round(fs * 0.45) + 'px monospace'; 974 ctx.fillText(ostr, W / 2, H * 0.15 + fs * 1.08 + fs * 0.75); 975 ctx.restore(); 976 } else { /* game */ 977 /* ROCK gives way to the score counter, zero-padded to 5 digits. 978 Landscape: tuck it in the top-left so the lanes stay clear. */ 979 var landscape = W > H; 980 var str = '' + egg.score; 981 while (5 > str.length) str = '0' + str; 982 var lr = letterRects()[0]; 983 ctx.save(); 984 ctx.shadowColor = '#209d50'; 985 ctx.shadowBlur = 16; 986 ctx.fillStyle = '#1a7a40'; 987 if (landscape) { 988 ctx.font = 'bold 30px monospace'; 989 ctx.textAlign = 'left'; 990 ctx.fillText(str, 16, 44); 991 } else { 992 ctx.font = 'bold ' + Math.round(lr.h * 0.85) + 'px monospace'; 993 ctx.textAlign = 'center'; 994 ctx.fillText(str, W / 2, lr.y + lr.h - 6); 995 } 996 ctx.restore(); 997 var b = btnGeom(); 998 /* spawn on a fixed 300ms grid (steady 1/8th notes). born is the grid 999 time, so spacing stays exact even when a frame arrives late */ 1000 while (now > egg.spawnAt) { 1001 if (now - egg.spawnAt > 1200) egg.spawnAt = now; /* resync after a stall */ 1002 var midi = SEQ[egg.gseq]; 1003 egg.gnotes.push({ 1004 lane: laneFor(midi), midi: midi, y: H + 26, 1005 born: egg.spawnAt, id: egg.gcount++ 1006 }); 1007 egg.gseq = (egg.gseq + 1) % SEQ.length; 1008 egg.spawnAt += 300; 1009 } 1010 /* analytic position from the wall clock: frame hitches (audio 1011 synthesis, GC, refresh-rate changes) can never bunch notes up. 1012 ~1.4s bottom-to-buttons, capped for portrait's long lane. */ 1013 var vel = Math.min((H - b.y) / 1400, 0.204); /* px per ms */ 1014 for (i = egg.gnotes.length - 1; i >= 0; i--) { 1015 var gn = egg.gnotes[i]; 1016 gn.y = H + 26 - (now - gn.born) * vel; 1017 if (b.y - HIT_WINDOW > gn.y) { /* missed: sequence moves on regardless */ 1018 if (gn.id >= egg.nextSeq) egg.nextSeq = gn.id + 1; 1019 egg.gnotes.splice(i, 1); 1020 /* strikes: a burst of misses in a short window is a strike; 1021 spaced-out misses age out and cost nothing. Back-to-back 1022 misses (a "double miss") are debounced into one. */ 1023 var lastMiss = egg.recentMisses.length 1024 ? egg.recentMisses[egg.recentMisses.length - 1] : 0; 1025 if (now - lastMiss > MISS_DEBOUNCE) egg.recentMisses.push(now); 1026 while (egg.recentMisses.length && now - egg.recentMisses[0] > STRIKE_WINDOW) { 1027 egg.recentMisses.shift(); 1028 } 1029 if (egg.recentMisses.length >= STRIKE_MISSES) { 1030 egg.strikes++; 1031 egg.strikeAt = now; 1032 egg.recentMisses = []; 1033 if (egg.strikes >= MAX_STRIKES) { 1034 egg.phase = 'over'; 1035 egg.t0 = now; 1036 egg.gnotes = []; 1037 loadEd(); 1038 try { releaseAllHolds(); } catch (err) {} 1039 break; 1040 } 1041 } 1042 continue; 1043 } 1044 ctx.beginPath(); 1045 ctx.arc(b.xs[gn.lane], gn.y, 18, 0, 6.29); 1046 ctx.fillStyle = GH_COLORS[gn.lane]; 1047 ctx.fill(); 1048 ctx.fillStyle = '#fff'; 1049 ctx.font = 'bold 19px serif'; 1050 ctx.textAlign = 'center'; 1051 ctx.fillText('\u266A', b.xs[gn.lane], gn.y + 7); 1052 } 1053 /* re-arm locked lanes once their next note is approaching the keys */ 1054 for (i = 0; egg.gnotes.length > i; i++) { 1055 var an = egg.gnotes[i]; 1056 if (egg.laneLock[an.lane] && APPROACH > an.y - b.y) { 1057 egg.laneLock[an.lane] = false;
1058 } 1059 } 1060 /* strikes: three X slots under the score (beside it in landscape) */ 1061 for (i = 0; MAX_STRIKES > i; i++) { 1062 var got = egg.strikes > i; 1063 var xpop = (got && i === egg.strikes - 1 && 320 > now - egg.strikeAt) 1064 ? 1 + 0.9 * (1 - (now - egg.strikeAt) / 320) : 1; 1065 ctx.save(); 1066 if (landscape) ctx.translate(30 + i * 28, 70); 1067 else ctx.translate(W / 2 + (i - 1) * 30, letterRects()[0].y + 92); 1068 ctx.scale(xpop, xpop); 1069 ctx.font = 'bold 20px sans-serif'; 1070 ctx.textAlign = 'center'; 1071 ctx.globalAlpha = got ? 0.95 : 0.25; 1072 ctx.fillStyle = got ? '#c0392b' : '#555'; 1073 ctx.fillText('\u2715', 0, 7); 1074 ctx.restore(); 1075 } 1076 /* buttons (1â5 labels on desktop only â keyboard map) */ 1077 for (i = 0; 5 > i; i++) { 1078 var pressed = 180 > now - egg.press[i]; 1079 ctx.beginPath(); 1080 ctx.arc(b.xs[i], b.y, pressed ? b.r + 4 : b.r, 0, 6.29); 1081 ctx.fillStyle = GH_COLORS[i]; 1082 ctx.globalAlpha = pressed ? 1 : 0.85; 1083 ctx.fill(); 1084 ctx.globalAlpha = 1; 1085 ctx.lineWidth = pressed ? 5 : 3; 1086 ctx.strokeStyle = 'rgba(255,255,255,0.9)'; 1087 ctx.stroke(); 1088 /* 1â5 labels are desktop keyboard hints â hide on touch */ 1089 if (window.matchMedia('(hover: hover) and (pointer: fine)').matches) { 1090 ctx.fillStyle = 'rgba(0,0,0,0.55)'; 1091 ctx.font = 'bold ' + Math.round(b.r * 0.95) + 'px sans-serif'; 1092 ctx.textAlign = 'center'; 1093 ctx.textBaseline = 'middle'; 1094 ctx.fillText(String(i + 1), b.xs[i], b.y + 1); 1095 } 1096 } 1097 } 1098 /* hit / letter-light rings (all phases) */ 1099 for (i = egg.rings.length - 1; i >= 0; i--) { 1100 var rg = egg.rings[i]; 1101 rg.r += 3; 1102 rg.life -= 0.06; 1103 if (0 > rg.life) { egg.rings.splice(i, 1); continue; } 1104 ctx.beginPath(); 1105 ctx.arc(rg.x, rg.y, rg.r, 0, 6.29); 1106 ctx.globalAlpha = Math.max(0, rg.life); 1107 ctx.lineWidth = 4; 1108 ctx.strokeStyle = rg.col || '#209d50'; 1109 ctx.stroke(); 1110 ctx.globalAlpha = 1; 1111 } 1112 } 1113 1114 /* Finger drag: grab the nearest note (unless tapping a link), carry it, 1115 and on release it falls with the toss velocity. */ 1116 var drag = null; 1117 1118 function grabAt(x, y) { 1119 var best = null, bd = 3600; /* 60px reach, squared */ 1120 for (var i = 0; notes.length > i; i++) { 1121 var n = notes[i]; 1122 var dx = n.x - x; 1123 var dy = n.y - n.size * 0.35 - y; /* glyph is drawn above the baseline */ 1124 var d = dx * dx + dy * dy; 1125 if (bd > d) { bd = d; best = n; } 1126 } 1127 return best; 1128 } 1129 1130 function bestNoteInLane(lane, by) { 1131 var bn = null, bd = HIT_WINDOW; 1132 for (var k = 0; egg.gnotes.length > k; k++) { 1133 var gn = egg.gnotes[k]; 1134 if (gn.lane === lane) { 1135 var dd = Math.abs(gn.y - by); 1136 if (bd > dd) { bd = dd; bn = gn; } 1137 } 1138 } 1139 return bn; 1140 } 1141 1142 /* Shared hit path for touch/click and desktop keys 1â5. */ 1143 function hitLane(lane, holdId) { 1144 if (!egg || egg.phase !== 'game' || 0 > lane || lane > 4) return false; 1145 var b = btnGeom(); 1146 var best = bestNoteInLane(lane, b.y); 1147 var scoring = !!(best && (best.id === egg.nextSeq || egg.badPresses >= 2)); 1148 egg.press[lane] = Date.now(); 1149 if (egg.laneLock[lane]) return true; 1150 if (scoring) { 1151 egg.badPresses = 0; 1152 egg.nextSeq = best.id + 1; 1153 egg.score += 100; 1154 egg.laneLock[lane] = true; 1155 try { pluckHold(best.midi, holdId); } catch (err) {} 1156 egg.rings.push({ x: b.xs[lane], y: b.y, r: 14, life: 1 }); 1157 egg.gnotes.splice(egg.gnotes.indexOf(best), 1); 1158 } else if (best) { 1159 egg.badPresses++;
1160 try { offNote(best.midi); } catch (err) {} 1161 egg.rings.push({ x: b.xs[lane], y: b.y, r: 10, life: 0.6, col: '#888' }); 1162 } else { 1163 try { offNote(LANE_PITCH[lane]); } catch (err) {} 1164 } 1165 return true; 1166 } 1167 1168 function onDown(e) { 1169 if (!running || drag) return; 1170 if (e.target && e.target.closest && e.target.closest('a, button')) return; 1171 var r = canvas.getBoundingClientRect(); 1172 var x = e.clientX - r.left, y = e.clientY - r.top; 1173 /* game mode: buttons instead of grabs */ 1174 if (egg && egg.phase === 'game') { 1175 var b = btnGeom(); 1176 /* full-square hit boxes: the squares tile the button row edge to 1177 edge, so no dead zones between keys */ 1178 var half = (b.xs[1] - b.xs[0]) / 2; 1179 var i, lane = -1; 1180 for (i = 0; 5 > i; i++) { 1181 if (half >= Math.abs(x - b.xs[i]) && half >= Math.abs(y - b.y)) { 1182 lane = i; 1183 break; 1184 } 1185 } 1186 var best = 0 > lane ? null : bestNoteInLane(lane, b.y); 1187 var scoring = !!(best && (best.id === egg.nextSeq || egg.badPresses >= 2)); 1188 if (!scoring) { 1189 /* would-be miss: double the squares. If a key holding the correct 1190 note sits in the generous zone, that key activates â forgives a 1191 finger that lands just off the note it was aiming for. */ 1192 var gd = 1e9; 1193 for (i = 0; 5 > i; i++) { 1194 if (2 * half >= Math.abs(x - b.xs[i]) && 2 * half >= Math.abs(y - b.y)) { 1195 var cand = bestNoteInLane(i, b.y); 1196 if (cand && (cand.id === egg.nextSeq || egg.badPresses >= 2)) { 1197 var dc = Math.abs(x - b.xs[i]); 1198 if (gd > dc) { gd = dc; lane = i; } 1199 } 1200 } 1201 } 1202 } 1203 if (0 > lane) return; 1204 hitLane(lane, e.pointerId); 1205 e.preventDefault(); 1206 return; 1207 } 1208 if (egg && egg.phase !== 'collect') return; /* mid-transition */ 1209 var n = grabAt(x, y); 1210 if (!n) return; 1211 var wasFloating = n.mode === 'float'; 1212 n.mode = 'drag'; 1213 n.settled = false; 1214 n.letterUsed = false; /* re-arm: this grab may light one letter */ 1215 drag = { n: n, x: x, y: y, vx: 0, vy: 0, id: e.pointerId }; 1216 if (wasFloating) playNext(); /* dead (fallen) notes stay dead */ 1217 e.preventDefault(); 1218 } 1219 1220 var KEY_LANES = { 1221 Digit1: 0, Digit2: 1, Digit3: 2, Digit4: 3, Digit5: 4, 1222 Numpad1: 0, Numpad2: 1, Numpad3: 2, Numpad4: 3, Numpad5: 4 1223 }; 1224 1225 function onKeyDown(e) { 1226 if (!running || !egg || egg.phase !== 'game') return; 1227 if (!(e.code in KEY_LANES)) return; 1228 if (e.repeat) return; 1229 if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || 1230 e.target.isContentEditable)) return; 1231 e.preventDefault(); 1232 hitLane(KEY_LANES[e.code], 'key-' + e.code); 1233 } 1234 1235 function onKeyUp(e) { 1236 if (!(e.code in KEY_LANES)) return; 1237 try { releaseHold('key-' + e.code); } catch (err) {} 1238 } 1239 1240 function onMove(e) { 1241 if (!drag || e.pointerId !== drag.id) return; 1242 var r = canvas.getBoundingClientRect(); 1243 var x = e.clientX - r.left, y = e.clientY - r.top; 1244 drag.vx = drag.vx * 0.4 + (x - drag.x) * 0.6; 1245 drag.vy = drag.vy * 0.4 + (y - drag.y) * 0.6; 1246 var n = drag.n; 1247 n.x = x; 1248 n.y = y + n.size * 0.35; 1249 n.rot = Math.max(-0.6, Math.min(0.6, drag.vx * 0.05)); 1250 drag.x = x; 1251 drag.y = y; 1252 e.preventDefault(); 1253 } 1254 1255 function onUp(e) { 1256 try { releaseHold(e.pointerId); } catch (err) {} 1257 if (!drag || e.pointerId !== drag.id) return; 1258 var n = drag.n; 1259 n.mode = 'fall'; 1260 n.vx = Math.max(-14, Math.min(14, drag.vx)); 1261 n.vy = Math.max(-16, Math.min(16, drag.vy)); 1262 drag = null; 1263 } 1264 1265 function onOrientation(e) { 1266 if (e.gamma == null) return; 1267 var g = Math.max(-45, Math.min(45, e.gamma)) / 45; 1268 tilt = tilt * 0.85 + g * 0.15; 1269 } 1270 1271 function onMotion(e) { 1272 var a = e.acceleration; 1273 var grav = false; 1274 if (!a || (a.x == null && a.y == null && a.z == null)) { 1275 a = e.accelerationIncludingGravity; 1276 grav = true; 1277 } 1278 if (!a) return; 1279 var m = Math.sqrt((a.x || 0) * (a.x || 0) + (a.y || 0) * (a.y || 0) + (a.z || 0) * (a.z || 0)); 1280 if (grav) m = Math.abs(m - 9.81); 1281 if (m > 16) { 1282 var now = Date.now(); 1283 shakeHits.push(now); 1284 shakeHits = shakeHits.filter(function (t) { return 1200 > now - t; }); 1285 lastJolt = now; 1286 joltMag = Math.min(m / 8, 5); 1287 if (shakeHits.length > 3 && !broken) { 1288 broken = true; 1289 unsettleAll(); 1290 } else if (broken) { 1291 unsettleAll(); 1292 } 1293 } 1294 } 1295 1296 /* iOS 13+ requires a user-gesture permission request for motion/gyro */ 1297 function askPermission() { 1298 ['DeviceOrientationEvent', 'DeviceMotionEvent'].forEach(function (k) { 1299 var C = window[k]; 1300 if (C && typeof C.requestPermission === 'function') { 1301 C.requestPermission().catch(function () {}); 1302 } 1303 }); 1304 } 1305 1306 function boot() { 1307 var nav = document.querySelector('.eds-site-header .wp-block-navigation'); 1308 if (!nav) return;
1309 overlay = nav.querySelector('.wp-block-navigation__responsive-container'); 1310 if (!overlay) return; 1311 1312 /* Act only on actual open/close transitions â the callback fires for 1313 every class-attribute write on the overlay, including our own. */ 1314 new MutationObserver(function () { 1315 var open = overlay.classList.contains('is-menu-open'); 1316 if (open && !running) start(); 1317 else if (!open && running) stop(); 1318 }).observe(overlay, { attributes: true, attributeFilter: ['class'] }); 1319 1320 overlay.addEventListener('pointerdown', onDown); 1321 overlay.addEventListener('pointermove', onMove); 1322 overlay.addEventListener('pointerup', onUp); 1323 overlay.addEventListener('pointercancel', onUp); 1324 1325 /* WP closes the modal when a tap lands on its backdrop elements. In 1326 guitar-hero mode the link content is visibility:hidden, so EVERY tap 1327 is a "backdrop" tap and the menu would slam shut mid-game. Swallow 1328 clicks in the capture phase unless they're on a real control (menu 1329 link or the close X) â matches the original sheet behavior, where 1330 empty-space taps did nothing. */ 1331 overlay.addEventListener('click', function (e) { 1332 if (!running) return; 1333 if (e.target && e.target.closest && e.target.closest('a, button')) return; 1334 e.stopImmediatePropagation(); 1335 e.preventDefault(); 1336 }, true); 1337 1338 /* WP's navigation script also closes the menu on focusout when focus 1339 appears to leave the modal â which happens when the game hides the 1340 link content. Keep those events away from it once the egg is past 1341 normal play. */ 1342 overlay.addEventListener('focusout', function (e) { 1343 if (running && egg && egg.phase !== 'collect') e.stopImmediatePropagation(); 1344 }, true); 1345 1346 document.addEventListener('click', function (e) { 1347 if (e.target && e.target.closest && 1348 e.target.closest('.wp-block-navigation__responsive-container-open')) { 1349 askPermission(); 1350 } 1351 }, true); 1352 1353 window.addEventListener('deviceorientation', onOrientation); 1354 window.addEventListener('devicemotion', onMotion); 1355 window.addEventListener('resize', resize); 1356 window.addEventListener('keydown', onKeyDown); 1357 window.addEventListener('keyup', onKeyUp); 1358 /* orientationchange can fire before layout settles â measure late */ 1359 window.addEventListener('orientationchange', function () { 1360 setTimeout(resize, 300); 1361 }); 1362 if (typeof ResizeObserver !== 'undefined') { 1363 new ResizeObserver(function () { 1364 if (running) resize(); 1365 }).observe(overlay); 1366 } 1367 } 1368 1369 if (document.readyState === 'loading') { 1370 document.addEventListener('DOMContentLoaded', boot); 1371 } else { 1372 boot(); 1373 } 1374})(); 1375</script>
1375 1376<div class="nfd-container eds-site-header wp-block-group alignfull nfd-bg-surface nfd-theme-white nfd-wb-header__header-1 has-background" style="background-color:#a8c5b5"><div class="wp-block-group"><div class="wp-block-site-logo"><a href="/" rel="home"><img src="/assets/images/banner-top-white.png" alt="Ed The Sock" width="307" decoding="async"/></a></div><div class="nfd-gap-2xl wp-block-group"><nav class="wp-block-navigation nfd-gap-lg" aria-label="Site"> 1377 <button type="button" class="wp-block-navigation__responsive-container-open" aria-label="Open menu"> 1378 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true"><path d="M4 7h16v2H4V7zm0 5h16v2H4v-2zm0 5h16v2H4v-2z" fill="currentColor"/></svg> 1379 </button> 1380 <div class="wp-block-navigation__responsive-container" tabindex="-1"> 1381 <button type="button" class="wp-block-navigation__responsive-container-close" aria-label="Close menu">×</button> 1382 <div class="wp-block-navigation__responsive-dialog"> 1383 <div class="wp-block-navigation__responsive-container-content"> 1384 <ul class="wp-block-navigation__container"> 1385<li class="wp-block-navigation-item"><a href="/">Home</a></li> 1386<li class="wp-block-navigation-item"><a href="/podcasts/">Podcasts</a></li> 1387<li class="wp-block-navigation-item current-menu-item"><a href="/sock-drawer/">The Sock Drawer</a></li> 1388<li class="wp-block-navigation-item"><a href="/faqs/">FAQs</a></li> 1389<li class="wp-block-navigation-item"><a href="/about/">About</a></li> 1390 </ul> 1391 </div> 1392 </div> 1393 </div> 1394</nav></div></div></div> 1395<main id="content"> 1396<div class="wp-block-cover alignfull is-light has-custom-content-position is-position-top-center" style="min-height:100vh"><span aria-hidden="true" class="wp-block-cover__background has-background-dim-20 has-background-dim" style="background-color:#20a052"></span><img class="wp-block-cover__image-background wp-image-94" alt="" src="/assets/images/site-background.webp" data-object-fit="cover"/><div class="wp-block-cover__inner-container"> 1397<style> 1398/* Original: flap top = header bottom + 27px (page cover is top-aligned) */ 1399:root .eds-sd { 1400 margin-block-start: calc(var(--eds-header-h, 119px) + 27px); 1401} 1402</style> 1403<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Permanent+Marker&family=Saira+Stencil+One&display=swap"> 1404<!-- COMPONENT: sock-drawer-under-construction â ED THE SOCK · "Sock Drawer" title --> 1405<!-- Fonts: site-wide via config/site-fonts.json (docs/site-fonts.md) --> 1406<!-- Ed the Sock · "Sock Drawer" page title â paste into a Squarespace Code Block --> 1407 1408<div class="eds-sd"> 1409 <div class="eds-sd-flap"> 1410 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1040 300" width="1040" height="300"> 1411<defs> 1412 <linearGradient id="kraft" x1="0" y1="0" x2="0" y2="1"> 1413 <stop offset="0" stop-color="#d8b888"></stop> 1414 <stop offset="0.55" stop-color="#c69c63"></stop> 1415 <stop offset="1" stop-color="#b3863f"></stop> 1416 </linearGradient> 1417 <radialGradient id="mott" cx="0.25" cy="0.18" r="1"> 1418 <stop offset="0" stop-color="#ffffff" stop-opacity="0.16"></stop> 1419 <stop offset="0.5" stop-color="#ffffff" stop-opacity="0"></stop> 1420 <stop offset="1" stop-color="#3a2410" stop-opacity="0.18"></stop> 1421 </radialGradient> 1422 <clipPath id="flap"><path d="M10.0,18.1 L34.8,28.9 L59.5,7.5 L84.3,16.4 L109.0,10.0 L133.8,8.1 L158.6,12.4 L183.3,19.9 L208.1,11.0 L232.9,13.3 L257.6,15.1 L282.4,12.3 L307.1,16.8 L331.9,23.0 L356.7,12.7 L381.4,7.1 L406.2,11.2 L431.0,18.8 L455.7,21.0 L480.5,16.0 L505.2,19.8 L530.0,20.2 L554.8,14.4 L579.5,13.3 L604.3,8.4 L629.0,16.6 L653.8,22.3 L678.6,13.9 L703.3,22.9 L728.1,14.5 L752.9,20.0 L777.6,11.4 L802.4,20.3 L827.1,13.1 L851.9,14.9 L876.7,17.3 L901.4,8.8 L926.2,16.5 L951.0,14.9 L975.7,7.6 L1000.5,12.9 L1025.2,8.2 L1026.7,15.0 L1021.9,36.4 L1021.8,57.9 L1030.9,79.3 L1020.9,100.7 L1030.4,122.1 L1030.4,143.6 L1026.7,165.0 L1032.0,186.4 L1030.0,207.9 L1028.5,229.3 L1026.2,250.7 L1026.0,272.1 L1030.0,288.9 L1005.2,260.4 L980.5,287.5 L955.7,275.2 L931.0,279.6 L906.2,277.6 L881.4,288.1 L856.7,282.2 L831.9,283.1 L807.1,286.2 L782.4,281.0 L757.6,286.2 L732.9,288.3 L708.1,283.5 L683.3,289.2 L658.6,285.0 L633.8,293.5 L609.0,289.7 L584.3,286.7 L559.5,282.0 L534.8,292.9 L510.0,285.4 L485.2,293.0 L460.5,281.8 L435.7,275.2 L411.
14220,288.0 L386.2,276.5 L361.4,267.0 L336.7,276.3 L311.9,284.3 L287.1,293.7 L262.4,281.5 L237.6,287.1 L212.9,288.4 L188.1,286.7 L163.3,291.1 L138.6,285.8 L113.8,276.6 L89.0,286.4 L64.3,285.1 L39.5,274.1 L14.8,284.6 L16.7,285.0 L14.9,263.6 L13.8,242.1 L15.0,220.7 L7.6,199.3 L9.3,177.9 L12.9,156.4 L11.2,135.0 L15.4,113.6 L12.3,92.1 L14.3,70.7 L16.0,49.3 L19.9,27.9 Z"></path></clipPath> 1423 <filter id="paper"><feTurbulence type="fractalNoise" baseFrequency="0.85" numOctaves="2" stitchTiles="stitch" result="n"></feTurbulence> 1424 <feColorMatrix in="n" type="matrix" values="0 0 0 0 0.36 0 0 0 0 0.25 0 0 0 0 0.10 0 0 0 0.5 0"></feColorMatrix></filter> 1425</defs> 1426<g clip-path="url(#flap)"> 1427 <rect x="0" y="0" width="1040" height="300" fill="url(#kraft)"></rect> 1428 <rect x="0" y="0" width="1040" height="300" fill="url(#mott)"></rect> 1429 <rect x="0" y="0" width="1040" height="300" filter="url(#paper)" opacity="0.10"></rect> 1430 1431 <rect x="0" y="268" width="1040" height="32" fill="#a8783f"></rect> 1432 <path d="M0,300 L0,285 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 a 16,16 0 0 1 32,0 L1040,300 Z" fill="#e3c794"></path> 1433 <line x1="0" y1="268" x2="1040" y2="268" stroke="#7a5028" stroke-width="2.2" opacity="0.7"></line> 1434 <line x1="16" y1="270" x2="16" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="48" y1="270" x2="48" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="80" y1="270" x2="80" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="112" y1="270" x2="112" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="144" y1="270" x2="144" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="176" y1="270" x2="176" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="208" y1="270" x2="208" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="240" y1="270" x2="240" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="272" y1="270" x2="272" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="304" y1="270" x2="304" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="336" y1="270" x2="336" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="368" y1="270" x2="368" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="400" y1="270" x2="400" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="432" y1="270" x2="432" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="464" y1="270" x2="464" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="496" y1="270" x2="496" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="528" y1="270" x2="528" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="560" y1="270" x2="560" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="592" y1="270" x2="592" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="624" y1="270" x2="624" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="656" y1="270" x2="656" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="688" y1="270" x2="688" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="720" y1="270" x2="720" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="752" y1="270" x2="752" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="784" y1="270" x2="784" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="816" y1="270" x2="816" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="848" y1="270" x2="848" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="880" y1="270" x2="880" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="912" y1="270" x2="912" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="944" y1="270" x2="944" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="976" y1="270" x2="976" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line><line x1="1008" y1="270" x2="1008" y2="285" stroke="#7a5028" stroke-width="1.4" opacity="0.5"></line> 1435 1436 <path d="M10.0,18.1 L34.8,28.9 L59.5,7.5 L84.3,16.4 L109.0,10.0 L133.8,8.1 L158.6,12.4 L183.3,19.9 L208.1,11.0 L232.9,13.3 L257.6,15.1 L282.4,12.3 L307.1,16.8 L331.9,23.0 L356.7,12.7 L381.4,7.1 L406.2,11.2 L431.0,18.8 L455.7,21.0 L480.5,16.0 L505.2,19.8 L530.0,20.2 L554.8,14.4 L579.5,13.3 L604.3,8.4 L629.0,16.6 L653.8,22.3 L678.6,13.9 L703.3,22.9 L728.1,14.5 L752.9,20.0 L777.6,11.4 L802.4,20.3 L827.1,13.1 L851.9,14.9 L876.7,17.3 L901.4,8.8 L926.2,16.5 L951.0,14.9 L975.7,7.6 L1000.5,12.9 L1025.2,8.2 L1026.7,15.0 L1021.9,36.4 L1021.8,57.9 L1030.9,79.3 L1020.9,100.7 L1030.4,122.1 L1030.4,143.6 L1026.7,165.0 L1032.0,186.4 L1030.0,207.9 L1028.5,229.3 L1026.2,250.7 L1026.0,272.1 L1030.0,288.9 L1005.2,260.4 L980.5,287.5 L955.7,27
14365.2 L931.0,279.6 L906.2,277.6 L881.4,288.1 L856.7,282.2 L831.9,283.1 L807.1,286.2 L782.4,281.0 L757.6,286.2 L732.9,288.3 L708.1,283.5 L683.3,289.2 L658.6,285.0 L633.8,293.5 L609.0,289.7 L584.3,286.7 L559.5,282.0 L534.8,292.9 L510.0,285.4 L485.2,293.0 L460.5,281.8 L435.7,275.2 L411.0,288.0 L386.2,276.5 L361.4,267.0 L336.7,276.3 L311.9,284.3 L287.1,293.7 L262.4,281.5 L237.6,287.1 L212.9,288.4 L188.1,286.7 L163.3,291.1 L138.6,285.8 L113.8,276.6 L89.0,286.4 L64.3,285.1 L39.5,274.1 L14.8,284.6 L16.7,285.0 L14.9,263.6 L13.8,242.1 L15.0,220.7 L7.6,199.3 L9.3,177.9 L12.9,156.4 L11.2,135.0 L15.4,113.6 L12.3,92.1 L14.3,70.7 L16.0,49.3 L19.9,27.9 Z" fill="none" stroke="#5e3a16" stroke-width="6" opacity="0.28"></path> 1437</g> 1438</svg> 1439 <div class="eds-sd-body"> 1440 <span class="eds-sd-mark">â² THIS SIDE UP â²</span> 1441 <span class="eds-sd-title">SOCK DRAWER</span> 1442 </div> 1443 <div class="eds-sd-stamp"><span>ED’S STASH<br><i>â â â </i><br>STRAIGHT FROM<br>THE VAULT</span></div> 1444 </div> 1445 <div class="eds-sd-note"> 1446 <span class="eds-tape"></span> 1447 <span class="eds-note-big">UNDER<br>CONSTRUCTION</span> 1448 <span class="eds-note-sub">I’m busy. Come back later.</span> 1449 <span class="eds-curl"></span> 1450 </div> 1451</div> 1452<style> 1453 .eds-sd{width:100%;max-width:1040px;margin:0 auto;display:flex;flex-direction:column; 1454 align-items:center;justify-content:center;box-sizing:border-box;container-type:inline-size} 1455 .eds-sd *{box-sizing:border-box} 1456 .eds-sd-flap{position:relative;width:100%;max-width:1040px;container-type:inline-size; 1457 transform:rotate(-1.6deg);filter:drop-shadow(0 14px 22px rgba(0,0,0,.42))} 1458 .eds-sd-flap>svg{display:block;width:100%;height:auto} 1459 .eds-sd-body{position:absolute;inset:0 0 6% 0;display:flex;flex-direction:column; 1460 align-items:center;justify-content:center;text-align:center;mix-blend-mode:multiply; 1461 font-family:'Saira Stencil One',sans-serif} 1462 .eds-sd-mark{font-size:2cqw;letter-spacing:.7cqw;color:#322a1f;margin-bottom:1.5cqw} 1463 .eds-sd-title{font-size:9cqw;letter-spacing:.7cqw;line-height:.85;color:#2c2418; 1464 text-shadow:0 0 .8cqw rgba(40,30,16,.18),.2cqw .3cqw 0 rgba(40,30,16,.1)} 1465 .eds-sd-stamp{position:absolute;right:3.2%;bottom:9%;width:15cqw;height:15cqw;border-radius:50%; 1466 border:.3cqw double rgba(150,42,30,.62);color:rgba(150,42,30,.7);display:flex; 1467 align-items:center;justify-content:center;text-align:center;font-family:'Saira Stencil One',sans-serif; 1468 font-size:1.3cqw;line-height:1.3;transform:rotate(-12deg);mix-blend-mode:multiply} 1469 .eds-sd-stamp i{font-style:normal;font-size:1.1cqw;opacity:.8} 1470 1471 /* yellow sticky note with a curled corner, tucked under the flap */ 1472 .eds-sd-note{position:relative;width:30.5cqw;max-width:324px;margin:calc(-2cqw - 28px) auto 0; 1473 padding:4.4cqw 3cqw 5cqw; 1474 background:linear-gradient(158deg,#fff39e 0%,#ffe85a 16%,#fcdf4b 100%); 1475 transform:rotate(-3deg);transform-origin:top center; 1476 border-radius:7px 11px 5px 9px / 10px 6px 9px 7px; 1477 box-shadow:0 14px 22px rgba(0,0,0,.30),0 2px 4px rgba(0,0,0,.18); 1478 display:flex;flex-direction:column;align-items:center;text-align:center} 1479 .eds-tape{position:absolute;z-index:3;top:-2.6cqw;left:50%;width:11cqw;height:3.6cqw; 1480 transform:translateX(-50%) rotate(-1.5deg); 1481 background:rgba(116,168,206,.5);box-shadow:0 2px 4px rgba(0,0,0,.15); 1482 clip-path:polygon(4% 0,96% 0,100% 12.5%,96% 25%,100% 37.5%,96% 50%,100% 62.5%,96% 75%,100% 87.5%,96% 100%,4% 100%,0% 87.5%,4% 75%,0% 62.5%,4% 50%,0% 37.5%,4% 25%,0% 12.5%,4% 0)} 1483 .eds-curl{position:absolute;right:0;bottom:0;width:0;height:0; 1484 border-style:solid;border-width:0 0 3.6cqw 3.6cqw; 1485 border-color:transparent transparent #e6d048 transparent; 1486 filter:drop-shadow(-4px -4px 6px rgba(0,0,0,.24))} 1487 .eds-note-big{font-family:'Permanent Marker',cursive;font-size:3cqw;line-height:1.35; 1488 color:#33312e;letter-spacing:.05cqw;transform:rotate(-1.6deg) skewX(-3deg)} 1489 .eds-note-sub{font-family:'Permanent Marker',cursive;font-size:3cqw;line-height:1.35; 1490 color:#33312e;margin-top:2.6cqw;transform:rotate(1deg) skewX(-3deg)} 1491</style> 1492<!-- âââââ end block âââââ --> 1493 1494<div class="wp-block-columns alignwide" style="margin-top:3rem"> 1495 1496<div class="wp-block-column"> 1497<iframe title="Return of Ed the Sock!" src="https://www.youtube.com/embed/lHrphaMnHjc?feature=oembed" style="width:100%;aspect-ratio:16/9;border:0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen loading="lazy"></iframe> 1498</div> 1499 1500 1501<div class="wp-block-column"> 1502<iframe title="Ed the Sock â NewMusic" src="https://www.youtube.com/embed/r2Of8-MA3Es?start=4&feature=oembed" style="width:100%;aspect-ratio:16/9;border:0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen loading="lazy"></iframe> 1503</div> 1504 1505</div> 1506 1507</div></div> 1508</main> 1509<style> 1510/* Squarespace-style scroll-in animation (site default): blocks fade in and 1511 slide up as they enter the viewport â images AND text blocks alike. 1512 The hidden state is applied by JS only, so content stays visible without JS. 1513 (.eds-anim-img kept as an alias for anything already carrying it.) */ 1514.eds-anim-img, 1515.eds-anim { 1516 opacity: 0; 1517 transform: translateY(26px); 1518} 1519 1520.eds-anim-img.eds-anim-in, 1521.eds-anim.eds-anim-in { 1522 opacity: 1; 1523 transform: none; 1524 transition: opacity 0.9s ease, transform 0.9s ease; 1525} 1526 1527@media (prefers-reduced-motion: reduce) { 1528 .eds-anim-img, 1529 .eds-anim, 1530 .eds-anim-img.eds-anim-in, 1531 .eds-anim.eds-anim-in { 1532 opacity: 1; 1533 transform: none; 1534 transition: none; 1535 } 1536} 1537</style>
1538<script> 1539/* No bare less-than chars in this script (wptexturize gotcha â docs/wordpress-operations.md) */ 1540(function () { 1541 if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; 1542 if (typeof IntersectionObserver === 'undefined') return; 1543 1544 function boot() { 1545 var targets = document.querySelectorAll( 1546 'main figure.wp-block-image, main p, main h1, main h2, main h3, ' + 1547 'main h4, main h5, main h6, main .wp-block-buttons, main cite, ' + 1548 'main .eds-podcast-card, main .eds-footer-social' 1549 ); 1550 if (!targets.length) return; 1551 1552 var io = new IntersectionObserver(function (entries) { 1553 entries.forEach(function (entry) { 1554 if (entry.isIntersecting) { 1555 entry.target.classList.add('eds-anim-in'); 1556 io.unobserve(entry.target); 1557 } 1558 }); 1559 }, { threshold: 0.12, rootMargin: '0px 0px -5% 0px' }); 1560 1561 var initial = 0; 1562 targets.forEach(function (el) { 1563 if (el.dataset.edsAnim === '1') return; 1564 /* Skip patterns that animate themselves (hero carousel quotes, 1565 cassette title) â a second transform/opacity would fight them. 1566 Also skip text nested in an already-animated ancestor (e.g. a 1567 cite inside an observed p) and empty paragraphs. */ 1568 if (el.closest('.eds-component-container, .eds-pod')) return; 1569 if (el.parentElement && el.parentElement.closest('[data-eds-anim="1"]')) return; 1570 if (el.tagName === 'P' && !el.textContent.trim() && !el.querySelector('img,iframe')) return; 1571 el.dataset.edsAnim = '1'; 1572 el.classList.add('eds-anim'); 1573 /* Original (Squarespace global animations, "detailed") also animates 1574 above-the-fold blocks on page load, staggered. IO fires immediately 1575 for visible elements; give that initial batch a small stagger. */ 1576 var r = el.getBoundingClientRect(); 1577 if (window.innerHeight > r.top && r.bottom > 0) { 1578 el.style.transitionDelay = Math.min(initial * 90, 540) + 'ms'; 1579 initial += 1; 1580 } 1581 io.observe(el); 1582 }); 1583 } 1584 1585 if (document.readyState === 'loading') { 1586 document.addEventListener('DOMContentLoaded', boot); 1587 } else { 1588 boot(); 1589 } 1590})(); 1591</script>
1591 1592<!-- Footer is intentionally blank like the original Squarespace site (its footer 1593 had zero height â no logo, no social bar, no copyright). The wp:html block 1594 above only carries the site-wide image fade-in helper and renders nothing. -->
1595<script src="/assets/nav.js?v=479a8164" defer></script>
vendor: 1 bytes, line 1595
1595
1596<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"2edaaf8bdf97466dbf51d0efdf012e40","r":1,"spa":2}' crossorigin="anonymous"></script>
1596 1597</body> 1598</html> 1599
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.