1/* global nn, NetizenNav, NetizenGrid, NetizenASCIISplash, NetizenContentBlock, NetizenBendPanel, NetizenBentText, NetizenSyrupImage, loadASCIIArt, loadData */ 2 3window.data = null // contains all the content data 4let nav, splash, content, grid, bendPanel, subNav 5 6// default settings for processed images 7// TODO: make customizable via bend panel 8const syrupSettings = { 9 huffhack: { 10 process: false, 11 opacity: 0.66, 12 zIndex: 3, 13 seed: 7 14 }, 15 dither: { 16 process: true, 17 opacity: 0.67, 18 zIndex: 2, 19 algorithm: 'atkinson', 20 dotSize: 1, 21 threshold: 174 22 }, 23 pixelate: { 24 process: true, 25 opacity: 1, 26 zIndex: 1, 27 size: 8, 28 threshold: 128 29 }, 30 ascii: { 31 process: true, 32 opacity: 0.5, 33 zIndex: 4, 34 chars: 'O*', 35 fontSize: 8, 36 fgColor: 'white' 37 } 38} 39 40// *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O 41// *.O util functions *.O 42// *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O 43 44function resetGridAndContent (exceptInitatives) { 45 grid.clearAllBlocks() 46 // clear all syrup images 47 nn.getAll('.syrup').forEach(ele => { 48 ele.classList.remove('syrup') 49 ele.querySelectorAll('img').forEach(img => img.remove()) 50 ele.onmouseover = null 51 ele.onmouseout = null 52 ele.onclick = null 53 }) 54 // remove any extra grids 55 nn.getAll('.grid').filter(d => d.id !== 'main-grid').forEach(d => d.remove()) 56 57 if (exceptInitatives) return 58 // reset initiatives 59 subNav.classList.remove('sub-nav') 60 subNav.querySelectorAll('*').forEach(e => e.remove()) 61 // revert center cell-content to original colors 62 content.ele.children[1].style.background = 'var(--prim-b)' 63 content.ele.children[1].style.color = 'var(--prim-a)' 64} 65 66function addSyrupImgToGrid (opts = {}) { 67 opts.ele1.classList.add('syrup') 68 69 const syrup = new NetizenSyrupImage({ 70 image: opts.path, 71 ele: opts.ele1, 72 colors: [opts.ele1.style.backgroundColor, opts.ele2.style.backgroundColor], 73 huffhack: syrupSettings.huffhack, 74 dither: syrupSettings.dither, 75 pixelate: syrupSettings.pixelate, 76 ascii: syrupSettings.ascii 77 }) 78 79 nn.create('img') 80 .set('src', opts.path) 81 .set('name', 'clean') 82 .addTo(opts.ele1) 83 84 const fadeIn = e => { e.style.opacity = 1 } 85 const fadeOut = e => { e.style.opacity = 0 } 86 87 opts.ele1.onmouseover = () => { 88 opts.ele1.querySelectorAll('.syrup-pre').forEach(fadeOut) 89 opts.ele1.querySelectorAll('.syrup-post').forEach(fadeIn) 90 opts.ele1.querySelectorAll('img').forEach(img => { 91 if (img.name !== 'clean') img.style.opacity = 0 92 }) 93 } 94 opts.ele1.onmouseout = () => { 95 opts.ele1.querySelectorAll('.syrup-pre').forEach(fadeIn) 96 opts.ele1.querySelectorAll('.syrup-post').forEach(fadeOut) 97 opts.ele1.querySelectorAll('img').forEach(img => { 98 if (img.name !== 'clean') { 99 img.style.opacity = syrup[img.name].opacity 100 } 101 }) 102 } 103 104 if (opts.callback) opts.ele1.onclick = () => opts.callback() 105} 106 107// *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O 108// *.O nav section change functions *.O 109// *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O 110 111// Below-the-fold initiative teasers on the home grid. Only initiatives with 112// real images in their own `grid` array can feature here (bias-out-of-the-box, 113// browserfest, and artware-incubator have none yet) -- same random-pick 114// pattern showInitiatives() already uses for D.R.E.A.M.'s own thumbnail. 115// goToPath is the same routine deep links and the splash credit use: 116// loadCaseStudy() assumes the initiatives sub-nav already exists in the DOM 117// (it looks up `.sub-nav-item[name=...]`), which is only true after 118// nav.updatePage('initiatives') has run and had time to render it -- calling 119// loadCaseStudy() straight from home throws on that lookup. 120// 121// Called from both showHome() and main()'s own "populate the initial grid" 122// block below: showHome() is guarded by `if (nav.state === 0) return`, which 123// is already true on a cold load (nav.state starts at 0), so it never runs on 124// first paint -- main() has always had its own separate, unguarded call for 125// exactly that reason (see the-crew.jpg being added in both places already). 126function populateHomeGrid () { 127 // WHICH initiatives feature on home is shared data (data/main.json 128 // "featuredOnHome" -- mobile reads the same key), so content changes only 129 // happen in one place. WHERE they land is desktop-specific: grid block 130 // slots, assigned positionally. Reading order per Jon: video first (block 131 // 2, the big top cell), projects between, NEWSLETTER (5) before BEND THIS 132 // SITE (6), cows (7), dream teaser (8). Two teaser slots means the third 133 // featuredOnHome entry only shows on mobile and the initiatives page -- 134 // the trade for giving BEND its own block instead of it sharing block 1 135 // with a teaser whose syrup images painted right over the canvas. 136 const slots = [[1, 2], [8, 7]] 137 const featured = (window.data.featuredOnHome || []).slice(0, slots.length) 138 const featuredInitiatives = featured.map((name, i) =>
138 ({ name, blocks: slots[i] })) 139 featuredInitiatives.forEach(({ name, blocks }) => { 140 const init = window.data.initiatives[name] 141 const images = (init.grid || []).map(g => g.image) 142 if (!images.length) return 143 addSyrupImgToGrid({ 144 path: nn.random(images), 145 ele1: grid.getBlock(blocks[0]), 146 ele2: grid.getBlock(blocks[1]), 147 callback: () => goToPath('/initiatives/' + name) 148 }) 149 }) 150 151 // Newsletter CTA: same graphical-lettering treatment as BEND THIS SITE 152 // (NetizenBentText, stroke colour borrowed from a neighbouring cell -- 153 // the convention bendPanel established). Block 5. Re-entering home 154 // re-populates the grid, so destroy the previous instance first or its 155 // ResizeObserver keeps watching a detached canvas. 156 if (window.data.newsletterUrl) { 157 if (window._newsletterText) window._newsletterText.destroy() 158 window._newsletterText = new NetizenBentText({ 159 parent: grid.getBlock(5), 160 text: ['NEWS', 'LETTER'], 161 color: grid.getBlock(6).style.backgroundColor, 162 padding: 10, 163 xOverlap: 30, 164 yOverlap: 80 165 }) 166 window._newsletterText.canvas.style.cursor = 'pointer' 167 window._newsletterText.canvas.onclick = () => 168 window.open(window.data.newsletterUrl, '_blank') 169 } 170 171 // Feature video (new release spotlight), block 2 -- the first big element. 172 // Same syrup treatment as every other grid image (addSyrupImgToGrid), 173 // instead of a plain thumbnail, for visual consistency with the rest of 174 // the grid. Paired with block 1 for its second colour -- the reverse of 175 // the pairing populateHomeGrid() already uses when block 1 hosts a 176 // teaser (ele1: block 1, ele2: block 2). 177 // 178 // hqdefault.jpg (not maxresdefault.jpg) because it's guaranteed to exist 179 // for every video -- maxresdefault silently 200s with a tiny placeholder 180 // instead of 404ing when it's missing, so there'd be no reliable way to 181 // detect and fall back. 182 if (window.data.featureVideo && window.data.featureVideo.youtubeId) { 183 const v = window.data.featureVideo 184 const block = grid.getBlock(2) 185 block.classList.add('feature-video') 186 addSyrupImgToGrid({ 187 path: `https://img.youtube.com/vi/${v.youtubeId}/hqdefault.jpg`, 188 ele1: block, 189 ele2: grid.getBlock(1), 190 callback: () => window.open(`https://www.youtube.com/watch?v=${v.youtubeId}`, '_blank', 'noopener') 191 }) 192 // a CSS background-image, not an <img> -- addSyrupImgToGrid's hover 193 // handlers do querySelectorAll('img') and assume every non-"clean" img 194 // has a matching entry in `syrup` (huffhack/pixelate/dither/ascii); an 195 // actual <img> here would hit that and throw on mouseout 196 nn.create('span').set('class', 'play-icon').addTo(block) 197 } 198 199 // BEND THIS SITE lives in block 6 now (its own block -- reading order puts 200 // it after the newsletter). resetGridAndContent()'s clearAllBlocks() strips 201 // every block's children on each home re-entry, bend canvas included, so 202 // re-home it here. Re-pointing .container keeps NetizenBentText's sizing 203 // honest before the redraw. 204 if (typeof bendPanel !== 'undefined' && bendPanel && bendPanel.text) { 205 const bendBlock = grid.getBlock(6) 206 if (bendPanel.text.canvas.parentNode !== bendBlock) { 207 bendPanel.text.container = bendBlock 208 bendBlock.appendChild(bendPanel.text.canvas) 209 bendPanel.text.update() 210 } 211 } 212} 213 214function showHome () { 215 if (nav.state === 0) return 216 217 if (content.showing) { 218 content.hide() 219 splash.start() 220 } 221 222 resetGridAndContent() 223 224 addSyrupImgToGrid({ 225 path: 'images/the-crew.jpg', 226 ele1: grid.getBlock(4), 227 ele2: grid.getBlock(3), 228 callback: () => nav.updatePage('about') 229 }) 230 231 loadASCIIArt({ 232 path: 'ascii-art/cows.txt', 233 parent: grid.getBlock(7), 234 link: 'http://www.textfiles.com/art/cow.txt' 235 }) 236 237 populateHomeGrid() 238 239 grid.transitionTo(window.grids.default) 240 241 if (bendPanel.showing) bendPanel.hidePanel() 242 bendPanel.enable() 243} 244 245// --------------------------------------------------------------- ABOUT SECTION 246function showAbout () { 247 if (nav.state === 'about') return 248 249 resetGridAndContent() 250 251 content.show('about') 252 253 splash.stop() 254 255 grid.transitionTo(window.grids.default) 256 257 loadASCIIArt({ 258 path: 'ascii-art/cows.txt', 259 parent: grid.getBlock(7),
260 link: 'http://www.textfiles.com/art/cow.txt' 261 }) 262 263 nn.create('img') 264 .set('src', 'images/the-crew.jpg') 265 .css({ cursor: 'auto' }) 266 .addTo(grid.getBlock(4)) 267} 268 269// --------------------------------------------------------- INITIATIVES SECTION 270async function showInitiatives () { 271 if (nav.state === 'initiatives') return 272 content.show('initiatives') 273 274 resetGridAndContent() 275 276 subNav.classList.add('sub-nav') 277 window.data._initiativesOrder.forEach(proj => { 278 const key = proj.split('.')[0] 279 const obj = window.data.initiatives[key] 280 const item = nn.create('span') 281 .content(obj.title) 282 .set('class', 'sub-nav-item') 283 .set('name', key) 284 .addTo(subNav) 285 .on('click', () => window.loadCaseStudy(obj)) 286 setTimeout(() => { item.style.opacity = 1 }, 900) 287 }) 288 289 grid.transitionTo(window.grids.default2) 290 291 const dreamPics = [1, 2, 3, 7, 9, 12] 292 const dreamObj = window.data.initiatives.dream 293 addSyrupImgToGrid({ 294 path: `images/dream/dream${nn.random(dreamPics)}.jpg`, 295 ele1: grid.getBlock(3), 296 ele2: grid.getBlock(4), 297 callback: () => window.loadCaseStudy(dreamObj) 298 }) 299 300 const ascii = await loadASCIIArt({ 301 path: 'ascii-art/netnet.txt', 302 parent: grid.getBlock(1), 303 css: { fontSize: '2rem', cursor: 'pointer' } 304 }) 305 const nnObj = window.data.initiatives.netnet 306 ascii.onclick = () => window.loadCaseStudy(nnObj) 307} 308 309// ------------------------------------------------------------- SUPPORT SECTION 310function showSupport () { 311 if (nav.state === 'support') return 312 resetGridAndContent() 313 content.show('support') 314 315 grid.transitionTo(window.grids.default) 316 317 const asciiBlock = grid.grid === window.grids.default 318 ? grid.getBlock(4) : grid.getBlock(5) 319 loadASCIIArt({ 320 path: 'ascii-art/netizen-heart.txt', 321 parent: asciiBlock, 322 css: { letterSpacing: '0.75rem' } 323 }) 324} 325 326// *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O 327// *.O main function *.O 328// *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O *.O 329 330// Attribution for whatever the splash is currently showing. 331// NetizenASCIISplash advances its own image on click and exposes no event, so 332// this polls imagePath rather than reaching into the class. Cheap, and it keeps 333// the credit out of that component entirely. 334function setupSplashCredit () { 335 const ele = nn.get('#splash-credit') 336 if (!ele) return 337 // sit just above the bottom edge of the splash canvas 338 const place = () => { 339 const h = (splash && splash.canvas) ? splash.canvas.getBoundingClientRect().height : 0 340 if (h) ele.style.top = Math.round(h - 30) + 'px' 341 } 342 place() 343 window.addEventListener('resize', place) 344 345 // The credit is position:fixed, so on its own it would sit in the viewport's 346 // bottom right forever -- following you down the page and lingering on 347 // interior sections. It belongs to the splash, so it should only be visible 348 // when the splash is what you're actually looking at: at the top of the 349 // page, and on the home section. 350 // 351 // Gated on nav.state (0 on home, the section name otherwise) rather than 352 // #splash-canvas opacity. Opacity seemed reasonable at first -- NetizenNav 353 // fades the canvas to 0 on interior pages -- but prototypes/splash-lab.js's 354 // "keep splash alive on interior pages" feature deliberately forces that 355 // same opacity back to 1, since the whole point of that feature is for the 356 // reveal to keep running behind About/Support/Initiatives. Two of our own 357 // features were fighting over one signal. nav.state is set synchronously by 358 // NetizenNav.updatePage() no matter what any prototype does to the canvas, 359 // so it can't be fought over the same way. 360 let shown = null 361 let resolved = false 362 const visible = () => { 363 if (!resolved) return false 364 if (window.scrollY > 30) return false 365 return !nav || nav.state === 0 366 } 367 const updateVisibility = () => { 368 const on = visible() 369 ele.style.opacity = on ? '1' : '0' 370 ele.style.pointerEvents = on ? 'auto' : 'none' 371 } 372 window.addEventListener('scroll', updateVisibility, { passive: true }) 373 374 // React the instant nav changes, not just on the next 400ms poll -- 375 // separate from the fact that opacity-based timing is gone now too. 376 if (nav) { 377 const origActivate = nav._activate.bind(nav) 378 nav._activate = function (sec) { origActivate(sec); updateVisibility() } 379 const origDeactivate = nav._deactivate.bind(nav) 380 nav._deactivate = function () { origDeactivate(); updateVisibility() } 381 } 382 383 const render = () => { 384 place() 385 const src = splash && splash.imagePath 386 // initiatives load async, so a lookup can legitimately come back empty on 387 // the first pass. Only stop re-checking once we've actually resolved one -- 388 // caching on src alone would pin the very first (empty) answer forever. 389 if (src !== shown || !resolved) { 390 shown = src 391 const c = window.splashCreditForSrc(src) 392 if (!c) { 393 ele.innerHTML = ''; ele.hidden = true; resolved = false 394 } else { 395 resolved = true 396 ele.hidden = false 397 ele.innerHTML = c.href 398 ? `<a href="${c.href}">
398${c.label}</a>` 399 : `<span>${c.label}</span>` 400 // href="/initiatives/dream" would otherwise trigger a real page 401 // navigation on click (pushState-based routing has no native 402 // equivalent of the old hash-anchor trick). Drive it through the 403 // same in-app router URLHandling() uses on a cold load instead. 404 const a = ele.querySelector('a') 405 if (a) a.addEventListener('click', e => { e.preventDefault(); goToPath(c.href) }) 406 } 407 } 408 updateVisibility() // also catches nav changes, which fade the canvas 409 } 410 render() 411 setInterval(render, 400) 412} 413 414// Shared by the cold-load path handler below, by in-page links (the splash 415// credit) that need the same routing but can't rely on a real navigation, 416// and by the popstate listener that drives browser back/forward. 417async function goToPath (path) { 418 const clean = path.replace(/^\/+/, '') // '/initiatives/dream' -> 'initiatives/dream' 419 if (clean.startsWith('about')) { 420 nav.updatePage('about') 421 } else if (clean.startsWith('initiatives')) { 422 nav.updatePage('initiatives') 423 const parts = clean.split('/') 424 if (parts.length > 1) { 425 await nn.sleep(1200) 426 const item = nn.get(`.sub-nav-item[name="${parts[1]}"]`) 427 if (item) item.click() 428 } 429 } else if (clean.startsWith('support')) { 430 nav.updatePage('support') 431 } else if (nav.state !== 0) { 432 nav.updatePage('logo') // back/forward landed on '/' 433 } 434} 435 436async function URLHandling () { 437 return goToPath(window.location.pathname) 438} 439 440// window.location.pathname has already moved by the time this fires (that's 441// how popstate works), so goToPath -> nav.updatePage's own pathname check 442// keeps this from pushing a duplicate history entry. 443window.addEventListener('popstate', () => goToPath(window.location.pathname)) 444 445function resize () { 446 if (window.blockerTimer) { 447 clearTimeout(window.blockerTimer) 448 clearTimeout(window.killLoader) 449 } 450 451 if (nn.width < 900) bendPanel.disable() 452 else bendPanel.enable() 453 454 nn.get('#loader').css('display', 'flex') 455 nn.get('#loader').css('opacity', 1) 456 457 content.adjustHeight() 458 459 window.blockerTimer = setTimeout(() => { 460 nn.get('#loader').css('opacity', 0) 461 window.killLoader = setTimeout(() => nn.get('#loader').css('display', 'none'), 800) 462 }, 250) 463} 464 465async function main () { 466 if (nn.isMobile()) { 467 // absolute, not relative: with real paths now (e.g. /initiatives/dream) 468 // a relative 'mobile.html' would resolve to /initiatives/mobile.html. 469 // Carry the intended path along so mobile.js can land on the same 470 // section instead of silently bouncing every deep link back to home. 471 const path = window.location.pathname 472 window.location = path && path !== '/' 473 ? `/mobile.html?redirect=${encodeURIComponent(path)}` 474 : '/mobile.html' 475 return 476 } 477 478 // load initial data 479 window.data = await loadData() 480 481 // setup the nav menu 482 nav = new NetizenNav({ 483 parent: 'header', 484 logo: '#netizen-logo', 485 onSelection: (sec) => { 486 switch (sec) { 487 case 'logo': showHome(); break 488 case 'about': showAbout(); break 489 case 'initiatives': showInitiatives(); break 490 case 'support': showSupport(); break 491 } 492 } 493 }) 494 495 // setup splash page slide show 496 splash = new NetizenASCIISplash({ 497 fontSize: 16, 498 ele: nn.get('#splash-canvas'), 499 radius: 200, 500 duration: 1000, 501 images: nn.shuffle(window.data.splashImages.map(window.splashSrc)) 502 }) 503 504 setupSplashCredit() 505 506 // setup content section (displays over the splash + above the grid) 507 content = new NetizenContentBlock() 508 subNav = content.ele.children[0] 509 subNav.classList.add('sub-nav') 510 511 // setup main grid section 512 grid = new NetizenGrid({ 513 selector: '#main-grid', 514 grid: window.grids.default 515 }) 516 517 // setup bend panel 518 await document.fonts.load('200px Fira Mono') 519 bendPanel = new NetizenBendPanel({ 520 textEle: grid.getBlock(6), 521 panelEle: '.grid div:nth-child(3)', // 'main', 522 onShow: () => { 523 const t = nn.get('.grid').top + window.pageYOffset - nn.get('header').height 524 window.scrollTo({ top: t, behavior: 'smooth' }) 525 grid.transitionTo(window.grids.default2) 526 // remove the crew fromt the grid 527 nn.getAll('.syrup').forEach(ele => { 528 ele.classList.remove('syrup') 529 ele.querySelectorAll('img').forEach(img => img.remove()) 530 ele.onmouseover = null 531 ele.onmouseout = null 532 ele.onclick = null 533 }) 534 }, 535 onHide: () => { 536 grid.transitionTo(window.grids.default) 537 // add crew back to grid 538 addSyrupImgToGrid({ 539 path: 'images/the-crew.jpg', 540 ele1: grid.getBlock(4), 541 ele2: grid.getBlock(3), 542 callback: () => nav.updatePage('about') 543 }) 544 } 545 }) 546 547 if (nn.width < 900) bendPanel.disable() 548 549 // popualte the inital grid 550 loadASCIIArt({ 551 path: 'ascii-art/cows.txt', 552 parent: grid.getBlock(7),
553 link: 'http://www.textfiles.com/art/cow.txt' 554 }) 555 556 addSyrupImgToGrid({ 557 path: 'images/the-crew.jpg', 558 ele1: grid.getBlock(4), 559 ele2: grid.getBlock(3), 560 callback: () => nav.updatePage('about') 561 }) 562 563 window.scrollTo({ top: 0, behavior: 'smooth' }) 564 await nn.sleep(2000) 565 566 // check for window location logic 567 const total = window.data._initiativesOrder.length 568 const waitingForDataToLoad = setInterval(async () => { 569 const loaded = Object.keys(window.data.initiatives).length 570 if (loaded === total) { // if data is all loaded 571 clearInterval(waitingForDataToLoad) 572 573 // loadData() kicks off each initiative's fetch without awaiting it (a 574 // forEach with an async callback doesn't block), so window.data.initiatives 575 // is still empty immediately after the crew-photo setup above -- this 576 // loop existing at all is the tell. populateHomeGrid() needs real 577 // `.grid` arrays per initiative, so it waits here too. 578 populateHomeGrid() 579 580 await URLHandling() 581 582 nn.get('#loader').css('opacity', 0) 583 setTimeout(() => nn.get('#loader').css('display', 'none'), 800) 584 } 585 // ... otherwise, keep looping 586 }, 200) 587} 588 589nn.on('load', main) 590nn.on('resize', resize)
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.