From bd48ff23ccf870c4aba393c8c99a535262db0c6c Mon Sep 17 00:00:00 2001 From: Eric Woodward Date: Thu, 1 Jan 2026 20:15:51 -0500 Subject: [PATCH] add Code section add only journal post for 2025 update copyrights to 2026 add licenses to BPS remove unused files update to 0.14.2 --- .editorconfig | 8 + .env | 2 + .gitignore | 2 +- .vscode/settings.json | 6 + package-lock.json | 4 +- package.json | 2 +- site.config.json5 | 113 +- src/assets/_root/webtoys/bbe/scripts/bbe.js | 1170 +++++++++-------- .../_root/webtoys/bbe/scripts/lightning.js | 82 +- src/assets/_root/webtoys/bbe/scripts/main.js | 30 +- src/assets/_root/webtoys/bps/index.html | 248 ++-- src/assets/_root/webtoys/bps/scripts/main.js | 224 ++-- .../_root/webtoys/bps/scripts/player.js | 40 +- .../_root/webtoys/bps/styles/styles.css | 2 +- src/assets/fragments/license/mit.md | 2 +- src/assets/images/eric-8bit.png | Bin 0 -> 40453 bytes src/assets/scripts/backgroundScroller.js | 18 +- src/assets/scripts/scripts.js | 100 +- src/assets/scripts/themeSwitcher.js | 8 +- src/layouts/partials/bio.ejs | 5 +- src/layouts/partials/footer.ejs | 2 +- src/layouts/partials/navmain.ejs | 3 + src/pages/about.ejs | 14 +- src/pages/chim/index.md | 12 +- src/pages/code/index.md | 26 + src/pages/licenses/cc-by-sa.md | 9 +- src/pages/now.md | 18 +- .../journal/2024/04-27-one-night-on-wpm.md | 2 +- .../journal/2025/12-31-year-in-review.md | 53 + src/support/errors/404.html.ejs | 1 + src/support/sitemap-v2.xml.ejs.broken | 77 -- src/support/sitemap.html.ejs.off | 62 - 32 files changed, 1173 insertions(+), 1172 deletions(-) create mode 100644 .editorconfig create mode 100644 .env create mode 100644 .vscode/settings.json create mode 100644 src/assets/images/eric-8bit.png create mode 100644 src/pages/code/index.md create mode 100644 src/sitePosts/journal/2025/12-31-year-in-review.md delete mode 100644 src/support/sitemap-v2.xml.ejs.broken delete mode 100644 src/support/sitemap.html.ejs.off diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7929358 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +# EditorConfig is awesome: https://editorconfig.org + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 4 +indent_style = space +insert_final_newline = true diff --git a/.env b/.env new file mode 100644 index 0000000..8ec22da --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +NODE_ENV=development +SESSION_KEY=THIS_IS_MY_TEMP_KEY \ No newline at end of file diff --git a/.gitignore b/.gitignore index d8d6855..5e6f416 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,4 @@ TODO.md # old directories kept locally stash/ -trash/ \ No newline at end of file +trash/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c689dfb --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "[json5]": { + "editor.insertSpaces": true, + "editor.tabSize": 4 + } +} diff --git a/package-lock.json b/package-lock.json index d8a62fc..989d54d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "iew-site", - "version": "0.14.0", + "version": "0.14.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "iew-site", - "version": "0.14.0", + "version": "0.14.2", "license": "MIT", "devDependencies": { "web-weevr": "git+ssh://git@git.itsericwoodward.com:eric/web-weevr.git" diff --git a/package.json b/package.json index fa4aa77..94fd632 100755 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iew-site", - "version": "0.14.1", + "version": "0.14.2", "description": "", "main": "index.js", "scripts": {}, diff --git a/site.config.json5 b/site.config.json5 index 9f9dc37..981ed4c 100644 --- a/site.config.json5 +++ b/site.config.json5 @@ -1,60 +1,59 @@ { - site: { - title: "It's Eric Woodward (dotcom)", - author: { - name: "Eric Woodward", - email: "hey@itsericwoodward.com", // not used - photo: "/images/eric-8bit.gif", - site: "https://itsericwoodward.com", - geo: { - position: "35.4, -80.5", - placename: "Concord", - region: "US-NC", - }, - }, + site: { + title: "It's Eric Woodward (dotcom)", + author: { + name: "Eric Woodward", + email: "hey@itsericwoodward.com", // not used + photo: "/images/eric-8bit.gif", + site: "https://itsericwoodward.com", + geo: { + position: "35.4, -80.5", + placename: "Concord", + region: "US-NC", + }, + }, - base_uri: "", - // csp: "default-src 'self' data: https://v8.js-dos.com 'unsafe-inline'; img-src 'self' https://*; media-src 'self' https://* data:; script-src 'self' https://v8.js-dos.com 'wasm-eval' 'unsafe-eval' 'unsafe-inline'; style-src 'self' https://v8.js-dos.com 'unsafe-inline'; worker-src 'self' blob:;", - csp: "default-src 'self' data: ; img-src 'self' https://*; media-src 'self' https://* data:;", - robots: "index,follow", - language: "en-us", - copyright: "Copyright 2014-2025 Eric Woodward, licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.", - basePath: "", - uri: "https://www.itsericwoodward.com", - comment_insert: "\n\ - ___________________________.\n\ - |;;| |;;||\n\ - |[]|---------------------|[]||\n\ - |;;| |;;||\n\ - |;;| |;;||\n\ - |;;| ItsEricWoodward.com |;;||\n\ - |;;| |;;||\n\ - |;;| |;;||\n\ - |;;| |;;||\n\ - |;;|_____________________|;;||\n\ - |;;;;;;;;;;;;;;;;;;;;;;;;;;;||\n\ - |;;;;;;_______________ ;;;;;||\n\ - |;;;;;| ___ |;;;;;||\n\ - |;;;;;| |;;;| |;;;;;||\n\ - |;;;;;| |;;;| |;;;;;||\n\ - |;;;;;| |;;;| |;;;;;||\n\ - |;;;;;| |;;;| |;;;;;||\n\ - |;;;;;| |___| |;;;;;||\n\ - \\_____|_______________|_____||\n\ - ~~~~~^^^^^^^^^^^^^^^^^~~~~~~\n\ - ", - }, - build: { - journalsPerPage: 5, - srcPath: "src", - outputPath: "out", - publishPath: "public", - }, - serve: { - authTypeUI: "basic", - handleStatic: true, - port: 4997, - shortCodeLink: "/q/", - static404: "./public/errors/404.html", - }, + base_uri: "", + csp: "default-src 'self' data: ; img-src 'self' https://*; media-src 'self' https://* data:; worker-src 'self' blob:;", + robots: "index,follow", + language: "en-us", + copyright: "Copyright 2014-2026 Eric Woodward, licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.", + basePath: "", + uri: "https://www.itsericwoodward.com", + comment_insert: "\n\ + ___________________________.\n\ + |;;| |;;||\n\ + |[]|---------------------|[]||\n\ + |;;| |;;||\n\ + |;;| |;;||\n\ + |;;| ItsEricWoodward.com |;;||\n\ + |;;| |;;||\n\ + |;;| |;;||\n\ + |;;| |;;||\n\ + |;;|_____________________|;;||\n\ + |;;;;;;;;;;;;;;;;;;;;;;;;;;;||\n\ + |;;;;;;_______________ ;;;;;||\n\ + |;;;;;| ___ |;;;;;||\n\ + |;;;;;| |;;;| |;;;;;||\n\ + |;;;;;| |;;;| |;;;;;||\n\ + |;;;;;| |;;;| |;;;;;||\n\ + |;;;;;| |;;;| |;;;;;||\n\ + |;;;;;| |___| |;;;;;||\n\ + \\_____|_______________|_____||\n\ + ~~~~~^^^^^^^^^^^^^^^^^~~~~~~\n\ + ", + }, + build: { + journalsPerPage: 5, + srcPath: "src", + outputPath: "out", + publishPath: "public", + }, + serve: { + authTypeUI: "basic", + handleStatic: true, + port: 4997, + shortCodeLink: "/q/", + static404: "./public/errors/404.html", + }, } diff --git a/src/assets/_root/webtoys/bbe/scripts/bbe.js b/src/assets/_root/webtoys/bbe/scripts/bbe.js index 31867a7..e765418 100644 --- a/src/assets/_root/webtoys/bbe/scripts/bbe.js +++ b/src/assets/_root/webtoys/bbe/scripts/bbe.js @@ -1,536 +1,545 @@ +// @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0 +/**************************************************************************** + * Byte Beasties Escape! + * + * Copyright 2023-2026 Eric Woodward + * Source released under CC0 Public Domain License v1.0 + * https://www.planarvagabond.com/licenses/cc0/ + * http://creativecommons.org/publicdomain/zero/1.0/ + ****************************************************************************/ import { createLightning, getMousePos } from "./lightning.js"; const startGame = (canvas) => { - const ctx = canvas.getContext("2d"), - BEASTIE_IMAGES = [ - "blinker_shadow_red.png", - "creeper_shadow_red.png", - "grabber_shadow_red.png", - "scanner_shadow_red.png", - "bobbler_shadow_yellow.png", - "gawker_shadow_yellow.png", - "reacher_shadow_yellow.png", - "shuffler_shadow_yellow.png", - "puffer_shadow_green.png", - "seer_shadow_green.png", - "spooker_shadow_green.png", - "thinker_shadow_green.png", - "chomper_shadow_blue.png", - "diver_shadow_blue.png", - "snapper_shadow_blue.png", - "walker_shadow_blue.png", - ], - GATE = { - HEIGHT: 20, - MARGIN: 25, - MAX_SPACE: 140, - MIN_SPACE: 60, - START: canvas.height - 200, - WIDTH: 65, - }, - game = { - interval: 1000 / 90, // ms/f = 1000 ms/s / 90 f/s - winScore: 15000, - winScores: { - easy: 15000, - medium: 30000, - hard: 50000, - endless: 0, - }, - blockerFrequency: 0.3, - blockerFrequencies: { - easy: 0.3, - medium: 0.4, - hard: 0.5, - endless: 0.3, - }, - hasBlockers: true, - isOver: false, - isPaused: false, - showRestartModal: false, - maxLead: 400, - maxSpeed: -8, + const ctx = canvas.getContext("2d"), + BEASTIE_IMAGES = [ + "blinker_shadow_red.png", + "creeper_shadow_red.png", + "grabber_shadow_red.png", + "scanner_shadow_red.png", + "bobbler_shadow_yellow.png", + "gawker_shadow_yellow.png", + "reacher_shadow_yellow.png", + "shuffler_shadow_yellow.png", + "puffer_shadow_green.png", + "seer_shadow_green.png", + "spooker_shadow_green.png", + "thinker_shadow_green.png", + "chomper_shadow_blue.png", + "diver_shadow_blue.png", + "snapper_shadow_blue.png", + "walker_shadow_blue.png", + ], + GATE = { + HEIGHT: 20, + MARGIN: 25, + MAX_SPACE: 140, + MIN_SPACE: 60, + START: canvas.height - 200, + WIDTH: 65, + }, + game = { + interval: 1000 / 90, // ms/f = 1000 ms/s / 90 f/s + winScore: 15000, + winScores: { + easy: 15000, + medium: 30000, + hard: 50000, + endless: 0, + }, + blockerFrequency: 0.3, + blockerFrequencies: { + easy: 0.3, + medium: 0.4, + hard: 0.5, + endless: 0.3, + }, + hasBlockers: true, + isOver: false, + isPaused: false, + showRestartModal: false, + maxLead: 400, + maxSpeed: -8, - speed: -1, - speedBoost: { - duration: 1, - multiplier: 2, - }, - }, - drag = 0.3, - beastie = { - width: 48, - height: 48, - x: canvas.width / 2 - 20, - y: GATE.START + 100, + speed: -1, + speedBoost: { + duration: 1, + multiplier: 2, + }, + }, + drag = 0.3, + beastie = { + width: 48, + height: 48, + x: canvas.width / 2 - 20, + y: GATE.START + 100, - // velocity - dx: 0, - dy: 0, + // velocity + dx: 0, + dy: 0, - // for tracking speed boosts - boostEnd: 0, + // for tracking speed boosts + boostEnd: 0, - score: 0, - image: null, - }, - player = { - dx: 0, - dy: 0, - }, - enemy = { - width: canvas.width, - height: canvas.height, - x: 0, - y: canvas.height - 50, - delaySeconds: 5, - speed: -2, - }, - random = (min, max) => Math.random() * (max - min) + min, - images = BEASTIE_IMAGES.reduce((acc, name) => { - const tempImg = new Image(); - tempImg.src = `/webtoys/bbe/beasties/${name}`; - acc[name] = tempImg; - return acc; - }, {}), - img = new Image(), - hasCollision = (gate) => - // Collision check: AAB (Axis-Aligned Bounding Box) - beastie.x < gate.x + GATE.WIDTH && - beastie.x > gate.x - beastie.width && - beastie.y < gate.y + GATE.HEIGHT && - beastie.y > gate.y - beastie.height, - settingsBeastie = document.getElementById("settingsBeastie"); + score: 0, + image: null, + }, + player = { + dx: 0, + dy: 0, + }, + enemy = { + width: canvas.width, + height: canvas.height, + x: 0, + y: canvas.height - 50, + delaySeconds: 5, + speed: -2, + }, + random = (min, max) => Math.random() * (max - min) + min, + images = BEASTIE_IMAGES.reduce((acc, name) => { + const tempImg = new Image(); + tempImg.src = `/webtoys/bbe/beasties/${name}`; + acc[name] = tempImg; + return acc; + }, {}), + img = new Image(), + hasCollision = (gate) => + // Collision check: AAB (Axis-Aligned Bounding Box) + beastie.x < gate.x + GATE.WIDTH && + beastie.x > gate.x - beastie.width && + beastie.y < gate.y + GATE.HEIGHT && + beastie.y > gate.y - beastie.height, + settingsBeastie = document.getElementById("settingsBeastie"); - let // vertical space between gates - minPlatformSpace = GATE.MIN_SPACE, - maxPlatformSpace = GATE.MAX_SPACE, - gates = [ - { - x: canvas.width / 2 - GATE.WIDTH / 2, - y: GATE.START, - touched: false, - blocker: false, - }, - ], - y = GATE.START, - keydown = false, - runtime = 0, - lastTimestamp; + let // vertical space between gates + minPlatformSpace = GATE.MIN_SPACE, + maxPlatformSpace = GATE.MAX_SPACE, + gates = [ + { + x: canvas.width / 2 - GATE.WIDTH / 2, + y: GATE.START, + touched: false, + blocker: false, + }, + ], + y = GATE.START, + keydown = false, + runtime = 0, + lastTimestamp; - img.onload = () => { - ctx.drawImage(img, beastie.x, beastie.y); - }; - // img.src = "/webtoys/bbe/beasties/blinker_shadow_red.png"; + img.onload = () => { + ctx.drawImage(img, beastie.x, beastie.y); + }; + // img.src = "/webtoys/bbe/beasties/blinker_shadow_red.png"; - Object.keys(images).forEach((key, i) => { - const input = document.createElement("input"), - label = document.createElement("label"), - bImage = images[key]; - label.className = "beastieImageInputWrapper"; - input.name = "beastie"; - input.type = "radio"; - // TODO: Add Cookie support - input.checked = i === 0; - input.value = key; - label.appendChild(input); - bImage.className = "beastieImage"; - label.appendChild(bImage); - settingsBeastie.appendChild(label); - }); + Object.keys(images).forEach((key, i) => { + const input = document.createElement("input"), + label = document.createElement("label"), + bImage = images[key]; + label.className = "beastieImageInputWrapper"; + input.name = "beastie"; + input.type = "radio"; + // TODO: Add Cookie support + input.checked = i === 0; + input.value = key; + label.appendChild(input); + bImage.className = "beastieImage"; + label.appendChild(bImage); + settingsBeastie.appendChild(label); + }); - const renderLightning = () => { - var color = "hsla(60, 100%, 50%, .7)"; + const renderLightning = () => { + var color = "hsla(60, 100%, 50%, .7)"; - ctx.save(); - ctx.globalCompositeOperation = "lighter"; + ctx.save(); + ctx.globalCompositeOperation = "lighter"; - ctx.strokeStyle = color; - ctx.shadowColor = color; + ctx.strokeStyle = color; + ctx.shadowColor = color; - ctx.fillStyle = color; - ctx.fillRect(enemy.x, enemy.y, enemy.width, canvas.height); - ctx.fillStyle = "hsla(0, 0%, 10%, 0.2)"; - ctx.shadowBlur = 0; - ctx.globalCompositeOperation = "source-over"; - ctx.fillRect(enemy.x, enemy.y, enemy.width, canvas.height); - ctx.globalCompositeOperation = "lighter"; - ctx.shadowBlur = 15; + ctx.fillStyle = color; + ctx.fillRect(enemy.x, enemy.y, enemy.width, canvas.height); + ctx.fillStyle = "hsla(0, 0%, 10%, 0.2)"; + ctx.shadowBlur = 0; + ctx.globalCompositeOperation = "source-over"; + ctx.fillRect(enemy.x, enemy.y, enemy.width, canvas.height); + ctx.globalCompositeOperation = "lighter"; + ctx.shadowBlur = 15; - const lightning = createLightning(canvas, enemy); - ctx.beginPath(); - for (let i = 0; i < lightning.length; i++) { - ctx.lineTo(lightning[i].x, Math.max(lightning[i].y, enemy.y)); - } - ctx.stroke(); - // requestAnimationFrame(renderLightning); - ctx.restore(); - }; + const lightning = createLightning(canvas, enemy); + ctx.beginPath(); + for (let i = 0; i < lightning.length; i++) { + ctx.lineTo(lightning[i].x, Math.max(lightning[i].y, enemy.y)); + } + ctx.stroke(); + // requestAnimationFrame(renderLightning); + ctx.restore(); + }; - const restartGame = () => { - const whichBeastie = document.querySelector( - 'input[name="beastie"]:checked' - ).value, - settingsMode = document.getElementById("settingsMode"); + const restartGame = () => { + const whichBeastie = document.querySelector( + 'input[name="beastie"]:checked' + ).value, + settingsMode = document.getElementById("settingsMode"); - if (settingsMode) { - const { value } = settingsMode; - game.winScore = game.winScores[value]; - game.blockerFrequency = game.blockerFrequencies[value]; - } + if (settingsMode) { + const { value } = settingsMode; + game.winScore = game.winScores[value]; + game.blockerFrequency = game.blockerFrequencies[value]; + } - img.src = `/webtoys/bbe/beasties/${whichBeastie}`; + img.src = `/webtoys/bbe/beasties/${whichBeastie}`; - game.isPaused = false; - game.isOver = false; - game.showRestartModal = false; - y = GATE.START; - minPlatformSpace = GATE.MIN_SPACE; - maxPlatformSpace = GATE.MAX_SPACE; - // all the gates - starts in the bottom middle - gates = [ - { - x: canvas.width / 2 - GATE.WIDTH / 2, - y: GATE.START, - touched: false, - blocker: false, - }, - ]; - beastie.dx = 0; - beastie.dy = 0; - beastie.boostEnd = 0; - beastie.score = 0; - beastie.x = canvas.width / 2 - 20; - beastie.y = canvas.height - 50; + game.isPaused = false; + game.isOver = false; + game.showRestartModal = false; + y = GATE.START; + minPlatformSpace = GATE.MIN_SPACE; + maxPlatformSpace = GATE.MAX_SPACE; + // all the gates - starts in the bottom middle + gates = [ + { + x: canvas.width / 2 - GATE.WIDTH / 2, + y: GATE.START, + touched: false, + blocker: false, + }, + ]; + beastie.dx = 0; + beastie.dy = 0; + beastie.boostEnd = 0; + beastie.score = 0; + beastie.x = canvas.width / 2 - 20; + beastie.y = canvas.height - 50; - player.dx = 0; - player.dy = 0; + player.dx = 0; + player.dy = 0; - enemy.y = canvas.height - 50; + enemy.y = canvas.height - 50; - runtime = 0; + runtime = 0; - while (y > 0) { - // the next gate can be placed above the previous one with a space - // somewhere between the min and max space - y -= GATE.HEIGHT + random(minPlatformSpace, maxPlatformSpace); + while (y > 0) { + // the next gate can be placed above the previous one with a space + // somewhere between the min and max space + y -= GATE.HEIGHT + random(minPlatformSpace, maxPlatformSpace); - gates.push({ - x: random( - GATE.MARGIN, - canvas.width - GATE.MARGIN - GATE.WIDTH - ), - y, - touched: false, - }); - } - }, - // game loop - loop = (timestamp) => { - requestAnimationFrame(loop); + gates.push({ + x: random( + GATE.MARGIN, + canvas.width - GATE.MARGIN - GATE.WIDTH + ), + y, + touched: false, + }); + } + }, + // game loop + loop = (timestamp) => { + requestAnimationFrame(loop); - // limit FPS - if (timestamp - lastTimestamp <= game.interval) return; + // limit FPS + if (timestamp - lastTimestamp <= game.interval) return; - if (!(game.isOver || game.isPaused)) - runtime += timestamp - lastTimestamp; - lastTimestamp = timestamp; + if (!(game.isOver || game.isPaused)) + runtime += timestamp - lastTimestamp; + lastTimestamp = timestamp; - if (game.isOver) { - ctx.save(); - ctx.fillStyle = "yellow"; - ctx.fillRect( - canvas.width / 4, - canvas.height / 4, - enemy.width / 2, - canvas.height / 2 - ); - ctx.fillStyle = "black"; - ctx.font = "32px sans"; - ctx.fillText( - beastie.y <= 0 ? "You Win!" : "Game Over", - canvas.width / 3, - canvas.height / 3, - canvas.width / 3 - ); - ctx.fillText( - `Score: + if (game.isOver) { + ctx.save(); + ctx.fillStyle = "yellow"; + ctx.fillRect( + canvas.width / 4, + canvas.height / 4, + enemy.width / 2, + canvas.height / 2 + ); + ctx.fillStyle = "black"; + ctx.font = "32px sans"; + ctx.fillText( + beastie.y <= 0 ? "You Win!" : "Game Over", + canvas.width / 3, + canvas.height / 3, + canvas.width / 3 + ); + ctx.fillText( + `Score: ${beastie.score}`, - canvas.width / 3, - (canvas.height / 3) * 2, - canvas.width / 3 - ); - ctx.restore(); - return; - } + canvas.width / 3, + (canvas.height / 3) * 2, + canvas.width / 3 + ); + ctx.restore(); + return; + } - if (game.showRestartModal) { - ctx.save(); - ctx.fillStyle = "yellow"; - ctx.fillRect( - canvas.width / 4, - canvas.height / 4, - enemy.width / 2, - canvas.height / 2 - ); - ctx.fillStyle = "black"; - ctx.font = "48px sans"; - ctx.fillText( - `Restart? (Y / N)`, - canvas.width / 3, - canvas.height / 3, - canvas.width / 3 - ); - ctx.restore(); - return; - } + if (game.showRestartModal) { + ctx.save(); + ctx.fillStyle = "yellow"; + ctx.fillRect( + canvas.width / 4, + canvas.height / 4, + enemy.width / 2, + canvas.height / 2 + ); + ctx.fillStyle = "black"; + ctx.font = "48px sans"; + ctx.fillText( + `Restart? (Y / N)`, + canvas.width / 3, + canvas.height / 3, + canvas.width / 3 + ); + ctx.restore(); + return; + } - if (game.isPaused) { - ctx.save(); - ctx.fillStyle = "yellow"; - ctx.fillRect( - canvas.width / 4, - canvas.height / 3, - canvas.width / 2, - canvas.height / 3 - ); - ctx.fillStyle = "black"; - ctx.font = "40px sans"; - ctx.fillText(`Paused`, canvas.width / 3 - 5, canvas.height / 2); - ctx.restore(); - return; - } - ctx.clearRect(0, 0, canvas.width, canvas.height); + if (game.isPaused) { + ctx.save(); + ctx.fillStyle = "yellow"; + ctx.fillRect( + canvas.width / 4, + canvas.height / 3, + canvas.width / 2, + canvas.height / 3 + ); + ctx.fillStyle = "black"; + ctx.font = "40px sans"; + ctx.fillText(`Paused`, canvas.width / 3 - 5, canvas.height / 2); + ctx.restore(); + return; + } + ctx.clearRect(0, 0, canvas.width, canvas.height); - // if beastie reaches the middle of the screen, move the gates down - // instead of beastie up to make it look like beastie is going up - if ( - beastie.y < canvas.height - canvas.height / 3 && - beastie.dy < 0 && - (beastie.score <= game.winScore || !game.winScore) - ) { - gates = gates.map(({ y, ...otherProps }) => ({ - ...otherProps, - y: y - beastie.dy, - })); + // if beastie reaches the middle of the screen, move the gates down + // instead of beastie up to make it look like beastie is going up + if ( + beastie.y < canvas.height - canvas.height / 3 && + beastie.dy < 0 && + (beastie.score <= game.winScore || !game.winScore) + ) { + gates = gates.map(({ y, ...otherProps }) => ({ + ...otherProps, + y: y - beastie.dy, + })); - // add more gates to the top of the screen as beastie moves up - while (gates[gates.length - 1].y > 0) { - gates.push({ - x: random(25, canvas.width - 25 - GATE.WIDTH), - y: - gates[gates.length - 1].y - - (GATE.HEIGHT + - random(minPlatformSpace, maxPlatformSpace)), - touched: false, - blocker: - game.hasBlockers && - gates.filter(({ blocker }) => blocker).length / - gates.length < - game.blockerFrequency && - Math.random() <= game.blockerFrequency - ? true - : false, - }); + // add more gates to the top of the screen as beastie moves up + while (gates[gates.length - 1].y > 0) { + gates.push({ + x: random(25, canvas.width - 25 - GATE.WIDTH), + y: + gates[gates.length - 1].y - + (GATE.HEIGHT + + random(minPlatformSpace, maxPlatformSpace)), + touched: false, + blocker: + game.hasBlockers && + gates.filter(({ blocker }) => blocker).length / + gates.length < + game.blockerFrequency && + Math.random() <= game.blockerFrequency + ? true + : false, + }); - // add a bit to the min/max gate space as the player goes up - minPlatformSpace += 0.5; - maxPlatformSpace += 0.5; + // add a bit to the min/max gate space as the player goes up + minPlatformSpace += 0.5; + maxPlatformSpace += 0.5; - // cap max space - maxPlatformSpace = Math.min( - maxPlatformSpace, - canvas.height / 2 - ); - } - // move enemy backwards - enemy.y -= beastie.dy; - } else { - if (beastie.y <= 0) beastie.dy = -10; - beastie.y += beastie.dy; - } + // cap max space + maxPlatformSpace = Math.min( + maxPlatformSpace, + canvas.height / 2 + ); + } + // move enemy backwards + enemy.y -= beastie.dy; + } else { + if (beastie.y <= 0) beastie.dy = -10; + beastie.y += beastie.dy; + } - // apply drag when key not pressed - if (!keydown) { - if (player.dx < 0) { - beastie.dx += drag; + // apply drag when key not pressed + if (!keydown) { + if (player.dx < 0) { + beastie.dx += drag; - // don't let dx go above 0 - if (beastie.dx > 0) { - beastie.dx = 0; - player.dx = 0; - } - } else if (player.dx > 0) { - beastie.dx -= drag; + // don't let dx go above 0 + if (beastie.dx > 0) { + beastie.dx = 0; + player.dx = 0; + } + } else if (player.dx > 0) { + beastie.dx -= drag; - if (beastie.dx < 0) { - beastie.dx = 0; - player.dx = 0; - } - } + if (beastie.dx < 0) { + beastie.dx = 0; + player.dx = 0; + } + } - if ( - player.dy === 0 || - (player.dy < 0 && Date.now() > beastie.boostEnd) - ) { - beastie.dy += drag; + if ( + player.dy === 0 || + (player.dy < 0 && Date.now() > beastie.boostEnd) + ) { + beastie.dy += drag; - // don't let dx go above 0 - if (beastie.dy > game.speed) { - beastie.dy = game.speed; - player.dy = game.speed; - } - } else if (player.dy > 0) { - beastie.dy -= drag; + // don't let dx go above 0 + if (beastie.dy > game.speed) { + beastie.dy = game.speed; + player.dy = game.speed; + } + } else if (player.dy > 0) { + beastie.dy -= drag; - if (beastie.dy < 0) { - beastie.dy = 0; - player.dy = 0; - } - } - } + if (beastie.dy < 0) { + beastie.dy = 0; + player.dy = 0; + } + } + } - beastie.x += beastie.dx; + beastie.x += beastie.dx; - // make beastie wrap the screen - if (beastie.x + beastie.width < 0) { - beastie.x = canvas.width; - } else if (beastie.x > canvas.width) { - beastie.x = -beastie.width; - } + // make beastie wrap the screen + if (beastie.x + beastie.width < 0) { + beastie.x = canvas.width; + } else if (beastie.x > canvas.width) { + beastie.x = -beastie.width; + } - ctx.fillStyle = "green"; - gates = gates.map((gate) => { - // draw gates - ctx.save(); - if (gate.blocker) ctx.fillStyle = "red"; - else ctx.fillStyle = gate.touched ? "lime" : "green"; - ctx.fillRect(gate.x, gate.y, GATE.WIDTH, GATE.HEIGHT); - if (gate.blocker) { - ctx.save(); - ctx.lineWidth = 4; - ctx.strokeStyle = "black"; - ctx.beginPath(); - ctx.moveTo(gate.x, gate.y); - ctx.lineTo(gate.x + GATE.WIDTH, gate.y + GATE.HEIGHT); - ctx.moveTo(gate.x + GATE.WIDTH, gate.y); - ctx.lineTo(gate.x, gate.y + GATE.HEIGHT); - ctx.stroke(); - ctx.restore(); - } + ctx.fillStyle = "green"; + gates = gates.map((gate) => { + // draw gates + ctx.save(); + if (gate.blocker) ctx.fillStyle = "red"; + else ctx.fillStyle = gate.touched ? "lime" : "green"; + ctx.fillRect(gate.x, gate.y, GATE.WIDTH, GATE.HEIGHT); + if (gate.blocker) { + ctx.save(); + ctx.lineWidth = 4; + ctx.strokeStyle = "black"; + ctx.beginPath(); + ctx.moveTo(gate.x, gate.y); + ctx.lineTo(gate.x + GATE.WIDTH, gate.y + GATE.HEIGHT); + ctx.moveTo(gate.x + GATE.WIDTH, gate.y); + ctx.lineTo(gate.x, gate.y + GATE.HEIGHT); + ctx.stroke(); + ctx.restore(); + } - // make beastie stop if it collides with a blocking gate - if (gate.blocker && hasCollision(gate)) { - beastie.dy = game.speed * 0.5; - gate.touched = true; - } + // make beastie stop if it collides with a blocking gate + if (gate.blocker && hasCollision(gate)) { + beastie.dy = game.speed * 0.5; + gate.touched = true; + } - // make beastie jump if it collides with a gate from above - if ( - // beastie is falling - // beastie.dy > 0 && - // beastie was previous above the gate - // prevDoodleY + doodle.height <= gate.y && - // doodle collides with gate + // make beastie jump if it collides with a gate from above + if ( + // beastie is falling + // beastie.dy > 0 && + // beastie was previous above the gate + // prevDoodleY + doodle.height <= gate.y && + // doodle collides with gate - !gate.blocker && - // !gate.touched && - hasCollision(gate) - ) { - // reset beastie position so it's on top of the gate - // beastie.y = gate.y - beastie.height; - if (!gate.touched) { - beastie.dy = Math.max( - game.maxSpeed, - game.speedBoost.multiplier * - Math.min(beastie.dy, game.speed) - ); - beastie.score += 100; - } - beastie.boostEnd = - Date.now() + game.speedBoost.duration * 1000; - gate.touched = true; - } + !gate.blocker && + // !gate.touched && + hasCollision(gate) + ) { + // reset beastie position so it's on top of the gate + // beastie.y = gate.y - beastie.height; + if (!gate.touched) { + beastie.dy = Math.max( + game.maxSpeed, + game.speedBoost.multiplier * + Math.min(beastie.dy, game.speed) + ); + beastie.score += 100; + } + beastie.boostEnd = + Date.now() + game.speedBoost.duration * 1000; + gate.touched = true; + } - return gate; - }); + return gate; + }); - // check on enemy - if (!runtime) runtime = 0; + // check on enemy + if (!runtime) runtime = 0; - if (runtime > enemy.delaySeconds * 1000) - enemy.y = enemy.y + enemy.speed; - if (enemy.y < 1) { - enemy.y = 0; - game.isOver = true; - } else if (enemy.y > beastie.y + game.maxLead) - enemy.y = beastie.y + game.maxLead; + if (runtime > enemy.delaySeconds * 1000) + enemy.y = enemy.y + enemy.speed; + if (enemy.y < 1) { + enemy.y = 0; + game.isOver = true; + } else if (enemy.y > beastie.y + game.maxLead) + enemy.y = beastie.y + game.maxLead; - // draw beastie - ctx.drawImage( - img, - beastie.x, - beastie.y, - beastie.width, - beastie.height - ); + // draw beastie + ctx.drawImage( + img, + beastie.x, + beastie.y, + beastie.width, + beastie.height + ); - // +100 points for each offscreen blocker that wasn't touched - beastie.score += - 100 * - gates.filter( - (gate) => - gate.y > canvas.height && gate.blocker && !gate.touched - ).length; - // remove any offscreen gates - gates = gates.filter((gate) => gate.y <= canvas.height); + // +100 points for each offscreen blocker that wasn't touched + beastie.score += + 100 * + gates.filter( + (gate) => + gate.y > canvas.height && gate.blocker && !gate.touched + ).length; + // remove any offscreen gates + gates = gates.filter((gate) => gate.y <= canvas.height); - beastie.score += Math.floor(Math.abs(beastie.dy)); - ctx.fillStyle = "white"; - ctx.font = "12px sans"; - ctx.fillText(`Score: ${beastie.score}`, 10, 20); + beastie.score += Math.floor(Math.abs(beastie.dy)); + ctx.fillStyle = "white"; + ctx.font = "12px sans"; + ctx.fillText(`Score: ${beastie.score}`, 10, 20); - renderLightning(); - }; + renderLightning(); + }; - // listen to keyboard events to move beastie - document.addEventListener("keydown", (e) => { - if (keydown) return; + // listen to keyboard events to move beastie + document.addEventListener("keydown", (e) => { + if (keydown) return; - if (e.code === "ArrowLeft") { - keydown = true; - player.dx = -1; - beastie.dx = -3; - } else if (e.code === "ArrowRight") { - keydown = true; - player.dx = 1; - beastie.dx = 3; - } else if (e.code === "KeyP") { - keydown = true; - game.isPaused = !game.isPaused; - } else if (e.code === "KeyR") { - keydown = true; - if (game.isOver) { - restartGame(); - return; - } - game.isPaused = true; - game.showRestartModal = true; - } else if (e.code === "KeyS") { - keydown = true; - game.isPaused = true; - document.getElementById("settings").classList.add("showSettings"); - } else if (e.code === "KeyY") { - keydown = true; - if (game.showRestartModal) restartGame(); - } else if (e.code === "KeyN") { - keydown = true; - if (game.showRestartModal) { - game.showRestartModal = false; - game.isPaused = false; - } + if (e.code === "ArrowLeft") { + keydown = true; + player.dx = -1; + beastie.dx = -3; + } else if (e.code === "ArrowRight") { + keydown = true; + player.dx = 1; + beastie.dx = 3; + } else if (e.code === "KeyP") { + keydown = true; + game.isPaused = !game.isPaused; + } else if (e.code === "KeyR") { + keydown = true; + if (game.isOver) { + restartGame(); + return; + } + game.isPaused = true; + game.showRestartModal = true; + } else if (e.code === "KeyS") { + keydown = true; + game.isPaused = true; + document.getElementById("settings").classList.add("showSettings"); + } else if (e.code === "KeyY") { + keydown = true; + if (game.showRestartModal) restartGame(); + } else if (e.code === "KeyN") { + keydown = true; + if (game.showRestartModal) { + game.showRestartModal = false; + game.isPaused = false; + } - /* + /* } else if (e.code === "ArrowDown") { keydown = true; player.dy = 1; @@ -540,117 +549,118 @@ const startGame = (canvas) => { player.dy = -1; beastie.dy = -3; */ - } - }); + } + }); - document.addEventListener("keyup", () => { - keydown = false; - }); + document.addEventListener("keyup", () => { + keydown = false; + }); - canvas.addEventListener( - "mousedown", - (e) => { - keydown = true; - const result = getMousePos(canvas, e); - if (result.x < canvas.width / 2) { - player.dx = -1; - beastie.dx = -3; - } else { - player.dx = 1; - beastie.dx = 3; - } - }, - false - ); + canvas.addEventListener( + "mousedown", + (e) => { + keydown = true; + const result = getMousePos(canvas, e); + if (result.x < canvas.width / 2) { + player.dx = -1; + beastie.dx = -3; + } else { + player.dx = 1; + beastie.dx = 3; + } + }, + false + ); - canvas.addEventListener( - "mousemove", - (e) => { - if (!keydown) return; - const result = getMousePos(canvas, e); - if (result.x < beastie.x) { - player.dx = -1; - beastie.dx = -3; - } else { - player.dx = 1; - beastie.dx = 3; - } - }, - false - ); + canvas.addEventListener( + "mousemove", + (e) => { + if (!keydown) return; + const result = getMousePos(canvas, e); + if (result.x < beastie.x) { + player.dx = -1; + beastie.dx = -3; + } else { + player.dx = 1; + beastie.dx = 3; + } + }, + false + ); - canvas.addEventListener( - "mouseup", - () => { - keydown = false; - }, - false - ); + canvas.addEventListener( + "mouseup", + () => { + keydown = false; + }, + false + ); - canvas.addEventListener( - "touchstart", - (e) => { - mousePos = getTouchPos(canvas, e); - const touch = e.touches[0], - mouseEvent = new MouseEvent("mousedown", { - clientX: touch.clientX, - clientY: touch.clientY, - }); - canvas.dispatchEvent(mouseEvent); - }, - false - ); - canvas.addEventListener( - "touchend", - () => { - var mouseEvent = new MouseEvent("mouseup", {}); - canvas.dispatchEvent(mouseEvent); - }, - false - ); - canvas.addEventListener( - "touchmove", - (e) => { - const touch = e.touches[0]; - keydown = true; - if (touch.clientX < beastie.x) { - player.dx = -1; - beastie.dx = -3; - } else { - player.dx = 1; - beastie.dx = 3; - } - canvas.dispatchEvent(mouseEvent); - }, - false - ); - document.getElementById("cancelSettings").addEventListener("click", (e) => { - // close modal - document.getElementById("settings").classList.remove("showSettings"); - // leave game paused - }); - document.getElementById("applySettings").addEventListener("click", (e) => { - // close modal - document.getElementById("settings").classList.remove("showSettings"); - // restart game - restartGame(); - }); - document - .getElementById("toggleSettings") - ?.addEventListener("click", (e) => { - // pause game - game.isPaused = true; + canvas.addEventListener( + "touchstart", + (e) => { + mousePos = getTouchPos(canvas, e); + const touch = e.touches[0], + mouseEvent = new MouseEvent("mousedown", { + clientX: touch.clientX, + clientY: touch.clientY, + }); + canvas.dispatchEvent(mouseEvent); + }, + false + ); + canvas.addEventListener( + "touchend", + () => { + var mouseEvent = new MouseEvent("mouseup", {}); + canvas.dispatchEvent(mouseEvent); + }, + false + ); + canvas.addEventListener( + "touchmove", + (e) => { + const touch = e.touches[0]; + keydown = true; + if (touch.clientX < beastie.x) { + player.dx = -1; + beastie.dx = -3; + } else { + player.dx = 1; + beastie.dx = 3; + } + canvas.dispatchEvent(mouseEvent); + }, + false + ); + document.getElementById("cancelSettings").addEventListener("click", (e) => { + // close modal + document.getElementById("settings").classList.remove("showSettings"); + // leave game paused + }); + document.getElementById("applySettings").addEventListener("click", (e) => { + // close modal + document.getElementById("settings").classList.remove("showSettings"); + // restart game + restartGame(); + }); + document + .getElementById("toggleSettings") + ?.addEventListener("click", (e) => { + // pause game + game.isPaused = true; - // show settings - document - .getElementById("settings") - .classList.toggle("showSettings"); - }); + // show settings + document + .getElementById("settings") + .classList.toggle("showSettings"); + }); - // start the game - restartGame(); + // start the game + restartGame(); - requestAnimationFrame(loop); + requestAnimationFrame(loop); }; export { startGame }; +// @license-end diff --git a/src/assets/_root/webtoys/bbe/scripts/lightning.js b/src/assets/_root/webtoys/bbe/scripts/lightning.js index 60093a0..864eb73 100644 --- a/src/assets/_root/webtoys/bbe/scripts/lightning.js +++ b/src/assets/_root/webtoys/bbe/scripts/lightning.js @@ -1,45 +1,55 @@ +// @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0 +/**************************************************************************** + * Byte Beasties Escape! + * + * Copyright 2023-2026 Eric Woodward + * Source released under CC0 Public Domain License v1.0 + * https://www.planarvagabond.com/licenses/cc0/ + * http://creativecommons.org/publicdomain/zero/1.0/ + ****************************************************************************/ export const createLightning = (canvas, enemy) => { - const { y: size } = enemy; - var center = { x: canvas.width / 2, y: canvas.height }; - var minSegmentHeight = 5; - var roughness = 2; - var maxDifference = size / 5; + const { y: size } = enemy; + var center = { x: canvas.width / 2, y: canvas.height }; + var minSegmentHeight = 5; + var roughness = 2; + var maxDifference = size / 5; - let lightning = []; - let segmentHeight = size / 3; - lightning.push({ - x: center.x, - y: center.y + 200, - }); - lightning.push({ - x: Math.random() * (canvas.width - 100) + 50, - y: Math.abs((Math.random() - 0.9) * 100), - }); - let currDiff = maxDifference; - while (segmentHeight > minSegmentHeight) { - const newSegments = []; - for (var i = 0; i < lightning.length - 1; i++) { - const start = lightning[i], - end = lightning[i + 1], - midX = (start.x + end.x) / 2, - newX = midX + (Math.random() * 2 - 1) * currDiff; - newSegments.push(start, { x: newX, y: (start.y + end.y) / 2 }); - } + let lightning = []; + let segmentHeight = size / 3; + lightning.push({ + x: center.x, + y: center.y + 200, + }); + lightning.push({ + x: Math.random() * (canvas.width - 100) + 50, + y: Math.abs((Math.random() - 0.9) * 100), + }); + let currDiff = maxDifference; + while (segmentHeight > minSegmentHeight) { + const newSegments = []; + for (var i = 0; i < lightning.length - 1; i++) { + const start = lightning[i], + end = lightning[i + 1], + midX = (start.x + end.x) / 2, + newX = midX + (Math.random() * 2 - 1) * currDiff; + newSegments.push(start, { x: newX, y: (start.y + end.y) / 2 }); + } - newSegments.push(lightning.pop()); - lightning = newSegments; + newSegments.push(lightning.pop()); + lightning = newSegments; - currDiff /= roughness; - segmentHeight /= 2; - } - return lightning; + currDiff /= roughness; + segmentHeight /= 2; + } + return lightning; }; // Get the position of the mouse relative to the canvas export const getMousePos = (canvasDom, mouseEvent) => { - var rect = canvasDom.getBoundingClientRect(); - return { - x: mouseEvent.clientX - rect.left, - y: mouseEvent.clientY - rect.top, - }; + var rect = canvasDom.getBoundingClientRect(); + return { + x: mouseEvent.clientX - rect.left, + y: mouseEvent.clientY - rect.top, + }; }; +// @license-end diff --git a/src/assets/_root/webtoys/bbe/scripts/main.js b/src/assets/_root/webtoys/bbe/scripts/main.js index b4d5469..422679f 100644 --- a/src/assets/_root/webtoys/bbe/scripts/main.js +++ b/src/assets/_root/webtoys/bbe/scripts/main.js @@ -1,8 +1,8 @@ // @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0 /**************************************************************************** - * Planar Vagabond's Guide to the Multiverse (planarvagabond.com) + * Byte Beasties Escape! * - * Copyright 2023-2024 Eric Woodward + * Copyright 2023-2026 Eric Woodward * Source released under CC0 Public Domain License v1.0 * https://www.planarvagabond.com/licenses/cc0/ * http://creativecommons.org/publicdomain/zero/1.0/ @@ -11,22 +11,22 @@ import { startGame } from "./bbe.js"; export default (() => { - // we load this library as a module to guarantee baseline ES6 functionality + // we load this library as a module to guarantee baseline ES6 functionality - // Indicate JS is loaded - document.documentElement.className = - document.documentElement.className.replace("no-js", "js"); + // Indicate JS is loaded + document.documentElement.className = + document.documentElement.className.replace("no-js", "js"); - setTimeout(() => { - const canvas = document.createElement("canvas"), - contentDiv = document.getElementById("game"); + setTimeout(() => { + const canvas = document.createElement("canvas"), + contentDiv = document.getElementById("game"); - canvas.setAttribute("width", "375"); - canvas.setAttribute("height", "667"); - canvas.setAttribute("id", "game"); - contentDiv.appendChild(canvas); + canvas.setAttribute("width", "375"); + canvas.setAttribute("height", "667"); + canvas.setAttribute("id", "game"); + contentDiv.appendChild(canvas); - startGame(canvas); - }, 1); + startGame(canvas); + }, 1); })(); // @license-end diff --git a/src/assets/_root/webtoys/bps/index.html b/src/assets/_root/webtoys/bps/index.html index 3b3051f..e525f8c 100644 --- a/src/assets/_root/webtoys/bps/index.html +++ b/src/assets/_root/webtoys/bps/index.html @@ -1,150 +1,168 @@ - - - - - - BPS (Bill Paxton Soundboard) - - - -
+ + + + + BPS (Bill Paxton Soundboard) + + + +
- - + src="scripts/main.js" + type="module" + > - + -

- Download ZIP - Download TAR -

+

+ Download ZIP + Download TAR +

-
+
-

What is This?

+

What is This?

-

- This is the BPS (Bill Paxton Soundboard), my entry into the UI Developers Guild Coding Challenge for - February 2024 at the company I work for. -

+

+ This is the BPS (Bill Paxton Soundboard), my entry into the UI + Developers Guild Coding Challenge for February 2024 at the company I + work for. +

-

- If you want to know a bit more about how (and why) it was made, be sure to check - out the blog post I wrote about it when it was released - on 2024-02-10. -

+

+ If you want to know a bit more about how (and why) it was made, be + sure to check out + the blog post I wrote about it + when it was released on 2024-02-10. +

-

The Rules

+

The Rules

-

- This particular challenge had 3 simple rules (copied verbatim below): -

+

+ This particular challenge had 3 simple rules (copied verbatim + below): +

-
    -
  1. Play some kind of music / sound
  2. -
  3. Be viewable
  4. -
  5. Don't work over 4hrs!!!!
  6. -
+
    +
  1. Play some kind of music / sound
  2. +
  3. Be viewable
  4. +
  5. Don't work over 4hrs!!!!
  6. +
-

- The BPS satisfies all 3 of the rules: it's an HTML5 / CSS / JS application that creates - a series of virtual audio tags, and then loads a WAV or MP3 into each one before - inserting them into the document. It then renders a series of clickable image-buttons - (each one being an img tag, surrounded by a figure tag, and - augmented by the text of a figcaption tag) and attaches a play() - function to the click handlers for those figures. Plus, I wrote it all in under 4 hours: -

+

+ The BPS satisfies all 3 of the rules: it's an HTML5 / CSS / JS + application that creates a series of virtual + audio tags, and then loads a WAV or MP3 into each one + before inserting them into the document. It then renders a series of + clickable image-buttons (each one being an img tag, + surrounded by a figure tag, and augmented by the text + of a figcaption tag) and attaches a + play() function to the click handlers for those + figures. Plus, I wrote it all in under 4 hours: +

- - -

How to Install Locally

- -
    -
  1. - Download either the ZIP'd - or TAR-balled version. -
  2. -
  3. - Decompress it:
      -
    • For the ZIP: unzip bps.zip
    • -
    • For the TAR-ball: tar -xvzf bps.tar.gz
    • +
    • + One hour thinking through the concept and writing the rough + draft + player.js and + main.js modules; +
    • +
    • + One hour to turn draft into an MVP, addressing + layout and audio issues; +
    • +
    • One hour to add images and expand the audio selection; and
    • +
    • + One final hour to add mobile support and some light + documentation. +
    -
  4. -
  5. - Change to the new directory: cd bps -
  6. -
  7. - Start an HTTP server: npx http-server -
  8. -
  9. - Point your web browser to - http://127.0.0.1:8080/. -
  10. -
-

Copyright

+

How to Install Locally

- +
    +
  1. + Download either the ZIP'd or + TAR-balled version. +
  2. +
  3. + Decompress it: +
      +
    • For the ZIP: unzip bps.zip
    • +
    • For the TAR-ball: tar -xvzf bps.tar.gz
    • +
    +
  4. +
  5. Change to the new directory: cd bps
  6. +
  7. Start an HTTP server: npx http-server
  8. +
  9. + Point your web browser to + http://127.0.0.1:8080/. +
  10. +
-

- The images and sound clips used for the Bill Paxton Soundboard are copyright their - respective owners, and used under the - fair use provision - of the Constitution of the United - States of America. -

+

Copyright

-

- All other content on this page (including scripts and text content) is released under a - Creative Commons CC0 1.0 Universal - license. -

+ -

Share and enjoy!

+

+ The images and sound clips used for the Bill Paxton Soundboard are + copyright their respective owners, and used under the + fair use provision + of the + Constitution of the United States of America. +

-

- More of Eric's Web Stuff -

+

+ All other content on this page (including scripts and text content) + is released under a + Creative Commons CC0 1.0 Universal + license. +

- - \ No newline at end of file +

Share and enjoy!

+ +

+ More of Eric's Web Stuff +

+ + diff --git a/src/assets/_root/webtoys/bps/scripts/main.js b/src/assets/_root/webtoys/bps/scripts/main.js index c58c0d9..facfac7 100644 --- a/src/assets/_root/webtoys/bps/scripts/main.js +++ b/src/assets/_root/webtoys/bps/scripts/main.js @@ -2,7 +2,7 @@ /**************************************************************************** * BPS (Bill Paxton Soundboard) * - * Copyright 2024 Eric Woodward + * Copyright 2024-2026 Eric Woodward * Source released under CC0 Public Domain License v1.0 * https://www.itsericwoodward.com/licenses/cc0/ * http://creativecommons.org/publicdomain/zero/1.0/ @@ -12,127 +12,127 @@ import { load, play } from "./player.js"; import rootdir from "./rootdir.js"; export default (() => { - // adapted from: https://cheatcode.co/tutorials/how-to-build-a-soundboard-with-javascript + // adapted from: https://cheatcode.co/tutorials/how-to-build-a-soundboard-with-javascript - const sounds = [ - { - name: "true_lies_navel_lint", - fmt: "wav", - image: "aliens_17_days.png", - text: "Navel Lint", - }, - { - name: "true_lies_pathetic", - fmt: "mp3", - image: "aliens_17_days.png", - text: "It's Pathetic!", - }, + const sounds = [ + { + name: "true_lies_navel_lint", + fmt: "wav", + image: "aliens_17_days.png", + text: "Navel Lint", + }, + { + name: "true_lies_pathetic", + fmt: "mp3", + image: "aliens_17_days.png", + text: "It's Pathetic!", + }, - { - name: "aliens_game_over", - fmt: "wav", - image: "aliens_17_days.png", - text: "Game Over!", - }, - { - name: "aliens_current_affairs", - fmt: "wav", - image: "aliens_17_days.png", - text: "Current Affairs", - }, - { - name: "aliens_17_days", - fmt: "wav", - image: "aliens_17_days.png", - text: "17 Days?", - }, - { - name: "aliens_express_elevator", - fmt: "wav", - image: "aliens_17_days.png", - text: "Express Elevator", - }, + { + name: "aliens_game_over", + fmt: "wav", + image: "aliens_17_days.png", + text: "Game Over!", + }, + { + name: "aliens_current_affairs", + fmt: "wav", + image: "aliens_17_days.png", + text: "Current Affairs", + }, + { + name: "aliens_17_days", + fmt: "wav", + image: "aliens_17_days.png", + text: "17 Days?", + }, + { + name: "aliens_express_elevator", + fmt: "wav", + image: "aliens_17_days.png", + text: "Express Elevator", + }, - { - name: "weird_science_booze", - fmt: "mp3", - image: "aliens_17_days.png", - text: "Boozehounds Return", - }, - { - name: "weird_science_dead_meat", - fmt: "mp3", - image: "aliens_17_days.png", - text: "Dead Meat", - }, - { - name: "weird_science_sandwich", - fmt: "mp3", - image: "aliens_17_days.png", - text: "Pork Sandwich", - }, - { - name: "weird_science_stewwed", - fmt: "mp3", - image: "aliens_17_days.png", - text: "You're Stewwed", - }, - { - name: "weird_science_turd_brain", - fmt: "mp3", - image: "aliens_17_days.png", - text: "Turd Brain", - }, - ]; + { + name: "weird_science_booze", + fmt: "mp3", + image: "aliens_17_days.png", + text: "Boozehounds Return", + }, + { + name: "weird_science_dead_meat", + fmt: "mp3", + image: "aliens_17_days.png", + text: "Dead Meat", + }, + { + name: "weird_science_sandwich", + fmt: "mp3", + image: "aliens_17_days.png", + text: "Pork Sandwich", + }, + { + name: "weird_science_stewwed", + fmt: "mp3", + image: "aliens_17_days.png", + text: "You're Stewwed", + }, + { + name: "weird_science_turd_brain", + fmt: "mp3", + image: "aliens_17_days.png", + text: "Turd Brain", + }, + ]; - sounds.forEach(({ name, fmt }) => { - load(name, `${rootdir}/sounds/${name}.${fmt}`); - }); + sounds.forEach(({ name, fmt }) => { + load(name, `${rootdir}/sounds/${name}.${fmt}`); + }); - // adapted from https://stackoverflow.com/questions/12813573/position-icons-into-circle + // adapted from https://stackoverflow.com/questions/12813573/position-icons-into-circle - let m = sounds.length; /* how many are ON the circle */ - let tan = Math.tan(Math.PI / m); /* tangent of half the base angle */ + let m = sounds.length; /* how many are ON the circle */ + let tan = Math.tan(Math.PI / m); /* tangent of half the base angle */ - const build = () => { - const figureButtons = sounds.map(({ name, text }, idx) => { - const caption = document.createElement("figcaption"), - figure = document.createElement("figure"), - img = document.createElement("img"); - caption.textContent = text; - img.alt = text; - img.src = `${rootdir}/images/${name}.png`; - figure.className = "figureButton"; - figure.onclick = () => play(name); - figure.style = `--i: ${idx}`; - figure.append(...[img, caption]); - return figure; - }); + const build = () => { + const figureButtons = sounds.map(({ name, text }, idx) => { + const caption = document.createElement("figcaption"), + figure = document.createElement("figure"), + img = document.createElement("img"); + caption.textContent = text; + img.alt = text; + img.src = `${rootdir}/images/${name}.png`; + figure.className = "figureButton"; + figure.onclick = () => play(name); + figure.style = `--i: ${idx}`; + figure.append(...[img, caption]); + return figure; + }); - const randomFigure = document.createElement("figure"), - randomCaption = document.createElement("figcaption"), - randomImg = document.createElement("img"); - randomCaption.textContent = "Random"; - randomImg.alt = "Random"; - randomImg.src = `${rootdir}/images/random.png`; - randomFigure.className = "figureButton"; - randomFigure.onclick = () => - play(sounds[~~(sounds.length * Math.random())].name); - randomFigure.append(...[randomImg, randomCaption]); + const randomFigure = document.createElement("figure"), + randomCaption = document.createElement("figcaption"), + randomImg = document.createElement("img"); + randomCaption.textContent = "Random"; + randomImg.alt = "Random"; + randomImg.src = `${rootdir}/images/random.png`; + randomFigure.className = "figureButton"; + randomFigure.onclick = () => + play(sounds[~~(sounds.length * Math.random())].name); + randomFigure.append(...[randomImg, randomCaption]); - const div = document.createElement("div"); - div.className = "circleWrapper"; - div.style = `--m: ${m}; --tan: ${+tan.toFixed(2)}`; - div.append(...[randomFigure, ...figureButtons]); - document.getElementById("output").replaceWith(div); - }; + const div = document.createElement("div"); + div.className = "circleWrapper"; + div.style = `--m: ${m}; --tan: ${+tan.toFixed(2)}`; + div.append(...[randomFigure, ...figureButtons]); + document.getElementById("output").replaceWith(div); + }; - document.addEventListener("DOMContentLoaded", () => { - setTimeout(() => { - build(); - }, 1); - }); + document.addEventListener("DOMContentLoaded", () => { + setTimeout(() => { + build(); + }, 1); + }); - return { play, build }; + return { play, build }; })(); // @license-end diff --git a/src/assets/_root/webtoys/bps/scripts/player.js b/src/assets/_root/webtoys/bps/scripts/player.js index 305d31a..cd2b497 100644 --- a/src/assets/_root/webtoys/bps/scripts/player.js +++ b/src/assets/_root/webtoys/bps/scripts/player.js @@ -2,7 +2,7 @@ /**************************************************************************** * BPS (Bill Paxton Soundboard) * - * Copyright 2024 Eric Woodward + * Copyright 2024-2026 Eric Woodward * Source released under CC0 Public Domain License v1.0 * https://www.itsericwoodward.com/licenses/cc0/ * http://creativecommons.org/publicdomain/zero/1.0/ @@ -13,25 +13,25 @@ let sounds = []; const injectPlayerIntoPage = (name, path) => { - const player = document.createElement("audio"); - player.id = name; - player.src = path; - player.volume = 0.5; - player.type = "audio/mpeg"; - document.body.appendChild(player); - }, - load = (name, path) => { - sounds = [...sounds, { name, path }]; - injectPlayerIntoPage(name, path); - }, - play = (name) => { - const player = document.getElementById(name); - if (player) { - player.pause(); - player.currentTime = 0; - player.play(); - } - }; + const player = document.createElement("audio"); + player.id = name; + player.src = path; + player.volume = 0.5; + player.type = "audio/mpeg"; + document.body.appendChild(player); + }, + load = (name, path) => { + sounds = [...sounds, { name, path }]; + injectPlayerIntoPage(name, path); + }, + play = (name) => { + const player = document.getElementById(name); + if (player) { + player.pause(); + player.currentTime = 0; + player.play(); + } + }; export { load, play }; // @license-end diff --git a/src/assets/_root/webtoys/bps/styles/styles.css b/src/assets/_root/webtoys/bps/styles/styles.css index 0264d93..d8c04d9 100644 --- a/src/assets/_root/webtoys/bps/styles/styles.css +++ b/src/assets/_root/webtoys/bps/styles/styles.css @@ -2,7 +2,7 @@ /**************************************************************************** * BPS (Bill Paxton Soundboard) * - * Copyright 2024 Eric Woodward + * Copyright 2024-2026 Eric Woodward * Source released under CC0 Public Domain License v1.0 * https://www.itsericwoodward.com/licenses/cc0/ * http://creativecommons.org/publicdomain/zero/1.0/ diff --git a/src/assets/fragments/license/mit.md b/src/assets/fragments/license/mit.md index 2b75054..78e4102 100644 --- a/src/assets/fragments/license/mit.md +++ b/src/assets/fragments/license/mit.md @@ -1,4 +1,4 @@ -Copyright © 2023-2024 [Eric Woodward](https://www.itsericwoodward.com/) +Copyright © 2023-2026 [Eric Woodward](https://www.itsericwoodward.com/) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/assets/images/eric-8bit.png b/src/assets/images/eric-8bit.png new file mode 100644 index 0000000000000000000000000000000000000000..162a7b382c08c35aa995a01f2b1eb68362628c5e GIT binary patch literal 40453 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 literal 0 HcmV?d00001 diff --git a/src/assets/scripts/backgroundScroller.js b/src/assets/scripts/backgroundScroller.js index fe28e19..fec873f 100644 --- a/src/assets/scripts/backgroundScroller.js +++ b/src/assets/scripts/backgroundScroller.js @@ -2,7 +2,7 @@ /**************************************************************************** * It's Eric Woodward's Site * - * Copyright 2014-2025 Eric Woodward + * Copyright 2014-2026 Eric Woodward * Source released under CC0 Public Domain License v1.0 * https://www.itsericwoodward.com/licenses/cc0/ * http://creativecommons.org/publicdomain/zero/1.0/ @@ -43,29 +43,21 @@ export default (actionBox) => { actionBox.classList.add("js-actionBox"); const actionBoxTitle = document.createElement("summary"); - actionBoxTitle.innerHTML = - "

Settings

"; + actionBoxTitle.innerHTML = "

Settings

"; actionBox.append(actionBoxTitle); // add toggle event document .getElementById("scrollingToggle") .addEventListener("click", function (e) { - const theList = - document.getElementById("gridContainer").classList; + const theList = document.getElementById("gridContainer").classList; theList.toggle("js-isAnimated"); - Cookies.set( - "scrollToggle", - !!theList.contains("js-isAnimated") - ); + Cookies.set("scrollToggle", !!theList.contains("js-isAnimated")); }); // add class if active at startup if (hasScrollToggle) - document - .getElementById("gridContainer") - .classList.add("js-isAnimated"); - + document.getElementById("gridContainer").classList.add("js-isAnimated"); }; // @license-end diff --git a/src/assets/scripts/scripts.js b/src/assets/scripts/scripts.js index 4ef2b7c..54ee301 100644 --- a/src/assets/scripts/scripts.js +++ b/src/assets/scripts/scripts.js @@ -2,76 +2,76 @@ /**************************************************************************** * It's Eric Woodward's Site * - * Copyright 2014-2025 Eric Woodward + * Copyright 2014-2026 Eric Woodward * Source released under CC0 Public Domain License v1.0 * https://www.itsericwoodward.com/licenses/cc0/ * http://creativecommons.org/publicdomain/zero/1.0/ ****************************************************************************/ -import backgroundScroller from './backgroundScroller.js'; -import themeSwitcher from './themeSwitcher.js'; +import backgroundScroller from "./backgroundScroller.js"; +import themeSwitcher from "./themeSwitcher.js"; export default (() => { - // we load this library via "module" to guarantee baseline ES6 functionality + // we load this library via "module" to guarantee baseline ES6 functionality - // check for loaded libraries - if (!window.Cookies) return; + // check for loaded libraries + if (!window.Cookies) return; - if (window.dayjs) dayjs.extend(window.dayjs_plugin_relativeTime); + if (window.dayjs) dayjs.extend(window.dayjs_plugin_relativeTime); - // Indicate JS is loaded - document.documentElement.className = - document.documentElement.className.replace("no-js", "js"); + // Indicate JS is loaded + document.documentElement.className = + document.documentElement.className.replace("no-js", "js"); - document.addEventListener("DOMContentLoaded", () => { - setTimeout(() => { - if (!window.Cookies) return; + document.addEventListener("DOMContentLoaded", () => { + setTimeout(() => { + if (!window.Cookies) return; - // Lazy-Load Media - if (typeof loadMedia === "function") { - loadMedia(".js-lazyLoader", null, true); - } + // Lazy-Load Media + if (typeof loadMedia === "function") { + loadMedia(".js-lazyLoader", null, true); + } - // Add relative dates via dayjs - if (window.dayjs) { - const times = document.getElementsByTagName("time"); + // Add relative dates via dayjs + if (window.dayjs) { + const times = document.getElementsByTagName("time"); - [].forEach.call(times, function (the_time) { - let pub_time = the_time.getAttribute("datetime"); - if (the_time.className.indexOf("js-noRelativeTime") === -1) - the_time.innerHTML += - ' (' + - dayjs(pub_time).from(dayjs()) + - ")"; - the_time.classList.add("isDone"); - }); - } + [].forEach.call(times, function (the_time) { + let pub_time = the_time.getAttribute("datetime"); + if (the_time.className.indexOf("js-noRelativeTime") === -1) + the_time.innerHTML += + ' (' + + dayjs(pub_time).from(dayjs()) + + ")"; + the_time.classList.add("isDone"); + }); + } - const actionBoxDiv = document.querySelector("#bio .actionBox"); + const actionBoxDiv = document.querySelector("#bio .actionBox"); - const actionBox = document.createElement("details"); + const actionBox = document.createElement("details"); - actionBoxDiv.append(actionBox); + actionBoxDiv.append(actionBox); - actionBox.setAttribute("open", "true"); + actionBox.setAttribute("open", "true"); - themeSwitcher(actionBox); + themeSwitcher(actionBox); - backgroundScroller(actionBox); + backgroundScroller(actionBox); - if (document.documentElement.className.indexOf("is404") > -1) { - document.getElementById("searchQuery").value = - window.location.pathname - .replace(/\\.html?$/, "") - .replace(/\//g, " "); - } + if (document.documentElement.className.indexOf("is404") > -1) { + document.getElementById("searchQuery").value = + window.location.pathname + .replace(/\\.html?$/, "") + .replace(/\//g, " "); + } - document - .getElementById("searchForm") - .addEventListener("submit", function (e) { - document.getElementById("searchQuery").value += - " site:" + window.location.hostname; - }); - }, 1); - }); + document + .getElementById("searchForm") + .addEventListener("submit", function (e) { + document.getElementById("searchQuery").value += + " site:" + window.location.hostname; + }); + }, 1); + }); })(); // @license-end diff --git a/src/assets/scripts/themeSwitcher.js b/src/assets/scripts/themeSwitcher.js index f59d84e..50a13f7 100644 --- a/src/assets/scripts/themeSwitcher.js +++ b/src/assets/scripts/themeSwitcher.js @@ -2,7 +2,7 @@ /**************************************************************************** * It's Eric Woodward's Site * - * Copyright 2014-2025 Eric Woodward + * Copyright 2014-2026 Eric Woodward * Source released under CC0 Public Domain License v1.0 * https://www.itsericwoodward.com/licenses/cc0/ * http://creativecommons.org/publicdomain/zero/1.0/ @@ -54,9 +54,7 @@ export default (actionBox) => { themeSwitch.classList.add("js-themeSwitch", "themeSwitch"); if (currentTheme) - document - .getElementsByTagName("body")[0] - .classList.add(currentTheme); + document.getElementsByTagName("body")[0].classList.add(currentTheme); actionBox.append(themeSwitch); @@ -72,5 +70,5 @@ export default (actionBox) => { .join(" "); Cookies.set("currentTheme", e.target.value); }); -} +}; // @license-end diff --git a/src/layouts/partials/bio.ejs b/src/layouts/partials/bio.ejs index 2a6441a..392c48c 100644 --- a/src/layouts/partials/bio.ejs +++ b/src/layouts/partials/bio.ejs @@ -41,7 +41,10 @@