Compare commits

..

20 Commits

Author SHA1 Message Date
efc6bca786 update CHIM to js-dos v8, update maze to latest 2025-07-06 12:24:01 -04:00
440cd959fa update maze of the minotaur 2025-06-13 00:26:41 -04:00
a73cc00c33 add maze of the minotaur 2025-06-06 02:48:29 -04:00
d22025691e magic decks, site config, library update 2024-07-03 00:02:10 -04:00
4914e72a3e add byte beasties escape webtoy 2024-04-29 23:42:44 -04:00
c4d3da4dc2 add theming support
replace CSS BG grid with SVG-based version
2024-04-27 15:37:02 -04:00
751e201f18 add post about scrolling grid 2024-04-09 23:47:56 -04:00
b2d2fb6d34 add scrolling grid bg
update copyright year
remove old libs
update to v0.12.0.
2024-04-09 23:39:41 -04:00
5f14032aee fix typo 2024-02-10 19:52:31 -05:00
f2ed98bc5d add WebToys and BPS 2024-02-10 19:21:54 -05:00
292f6ee3d0 migrate to web-weevr 2024-01-28 00:22:59 -05:00
afcf37a284 add doctor who cards
update license links
lotsa cleanup
2024-01-27 23:17:56 -05:00
c92d8c3897 Add link to cyberdeck project 2023-08-15 00:19:09 -04:00
ec21f676a6 Cyberdeck post 2023-08-15 00:15:40 -04:00
1bec5c0259 Move build process to external app 2023-08-09 22:05:16 -04:00
b09d183a1c Add licenses 2023-07-23 23:40:17 -04:00
07dfed2e1c Fix broken link on Journal Year pages 2023-07-21 00:29:22 -04:00
d4247e09ff Update build script 2023-07-21 00:20:36 -04:00
bf448aa47f Add forgotten post
Remove cross-env
2023-07-21 00:15:03 -04:00
5af4cb886f Add post about PVGttM
Add support for `status` prop on posts (draft or hidden)
Fix fonts
2023-07-21 00:12:16 -04:00
346 changed files with 74146 additions and 2691 deletions

29
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,29 @@
module.exports = {
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"prettier" // Make sure this is the last
],
"overrides": [
{
"env": {
"node": true
},
"files": [
".eslintrc.{js,cjs}"
],
"parserOptions": {
"sourceType": "script"
}
}
],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
}
}

16
.gitignore vendored
View File

@@ -1,4 +1,20 @@
# node project
node_modules/ node_modules/
# images that get run through image magick
src/assets/images-to-process/
# generated data
data/
# output directories
out/ out/
public/
# plans, thoughts, notes
IDEAS.md IDEAS.md
TODO.md TODO.md
# old directories kept locally
stash/
trash/

24
LICENSE
View File

@@ -1,21 +1,11 @@
MIT License The MIT License (MIT)
Copyright (c) 2018 Douglas Matoso Copyright (c) 2018-23 Eric Woodward
Permission is hereby granted, free of charge, to any person obtaining a copy Based on [NanoGen](https://github.com/doug2k1/nanogen), copyright (c) 2018 Douglas Matoso
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:
The above copyright notice and this permission notice shall be included in all 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:
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,8 +1,8 @@
# Mystic Site Builder (2022 Edition) # It's Eric Woodward (dotcom)
Micro static site generator in Node.js A custom-built static site generator in Node.js used to create the website https://www.itsericwoodward.com (and https://itsericwoodward.com, for good measure).
Based on the ideas in this post: https://medium.com/douglas-matoso-english/build-static-site-generator-nodejs-8969ebe34b22 Based (at least in part) on the ideas in this post: https://medium.com/douglas-matoso-english/build-static-site-generator-nodejs-8969ebe34b22
## Setup ## Setup
@@ -10,10 +10,11 @@ Based on the ideas in this post: https://medium.com/douglas-matoso-english/build
$ npm i $ npm i
$ npm run build $ npm run build
$ npm run serve $ npm run serve
$ npm run watch
``` ```
Go to http://localhost:5000 to see the generated site. Go to http://localhost:5000 to see the generated site.
## How to use ## How to use
If you want to use NanoGen to generate your own site, just fork this repository and add your content to the `src` folder. If you want to use this as the basis for generating your own site, just fork this repository and update the content in the `src` folder.

7
jsconfig.json Normal file
View File

@@ -0,0 +1,7 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6"
},
"exclude": ["node_modules"]
}

View File

@@ -1,373 +0,0 @@
const { exists } = require("fs-extra/lib/fs");
module.exports = async (config) => {
const { promises: fs } = require("fs"),
fse = require("fs-extra"),
path = require("path"),
ejs = require("ejs"),
frontMatter = require("front-matter"),
glob = require("glob"),
hljs = require("highlight.js"),
md = require("markdown-it")({
highlight: (str, lang) => {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(str, { language: lang }).value;
} catch (__) {}
}
return ""; // use external default escaping
},
html: true,
linkify: true,
typographer: true,
xhtmlOut: true,
}),
emoji = require("markdown-it-emoji"),
// { readJsonIfExists } = require("./utils"),
{ build, isRebuild, logFunction: log = () => {} } = config || {},
{ outputPath, journalsPerPage = 5, srcPath } = build,
{ site } = config,
copyAssets = async (directory) => {
const assets = await fs.readdir(directory);
assets.forEach(async (asset) => {
// we no longer merge scripts and styles, thanks to http/2's parallel file handling
if (asset === "_root") {
fse.copy(path.join(srcPath, "assets", asset), outputPath);
} else {
fse.copy(
path.join(srcPath, "assets", asset),
path.join(outputPath, asset)
);
}
});
},
getReadTime = (text) => {
const WPM = 275,
fixedString = text.replace(/[^\w\s]+/g, ""),
count = fixedString.split(/\s+/).length;
if (count < WPM) return "less than 1 minute";
else return `${Math.ceil(count / WPM)} minutes`;
},
tagSorter = (a, b) => a.toLowerCase().localeCompare(b.toLowerCase()),
parseFile = (file, pagePath, siteData, isSupport) => {
const { dir, ext, name } = path.parse(file) || {},
hasExt = name.indexOf(".") > -1,
destPath = path.join(outputPath, dir),
filePath = path.join(pagePath, file),
// read page file
data = fse.readFileSync(filePath, "utf-8"),
// render page
{ attributes, body } = frontMatter(data),
{ content_type: contentType, tags: originalTags = [] } =
attributes,
// TODO: Look for tags in posts as well, link to them, and add them to tag pages
tags =
typeof originalTags === "string"
? originalTags.split(/\W+/)
: [].concat(originalTags),
innerTags = (
contentType === "journal"
? body.match(/\b#(\w+)/g) || []
: []
).map((val) => val.replace("#", "")),
allTags = [...tags, ...innerTags].sort(tagSorter),
updatedBody =
contentType === "journal"
? allTags.reduce(
(acc, tag) =>
acc.replace(
`#${tag}`,
`
<a href="/journal/tags/${tag}/index.html">
#<span class="p-category category">${tag}</span>
</a>`
),
body
)
: body;
return {
...config,
page: {
name,
...attributes,
body: updatedBody,
destPath,
filePath,
path: path.join(dir, hasExt ? name : `${name}.html`),
tags: [...tags, ...innerTags].sort(tagSorter),
ext,
},
site: {
...site,
pages: isSupport ? siteData : [],
},
};
},
parseContent = (page, siteData) => {
const {
body,
content_type: contentType,
filePath,
// tags,
} = page || {},
{ ext } = path.parse(filePath) || {},
{ pages, tags } = siteData || {};
let content = body,
readTime;
if (ext === ".md") {
if (contentType === "journal" && typeof body === "string") {
readTime = getReadTime(body);
}
content = md.render(body);
} else if (ext === ".ejs") {
content = ejs.render(
body,
{ page, site: { ...site, pages, tags } },
{ filename: filePath }
);
}
return { ...page, content, readTime };
},
renderFile = async (page, isSupport) => {
const {
content,
destPath,
layout,
path: pagePath,
pages,
siteTags,
tags,
} = page || {};
try {
const layoutFileName = `${srcPath}/layouts/${
layout || "default"
}.ejs`,
layoutData = await fs.readFile(layoutFileName, "utf-8"),
completePage = isSupport
? content
: ejs.render(layoutData, {
content,
page,
site: {
...site,
pages,
tags:
page.content_type === "journal"
? siteTags
: tags,
},
filename: layoutFileName,
});
if (!completePage) {
console.log("failed!", pagePath, content);
return;
}
// create destination directory
fse.mkdirsSync(destPath);
// save the html file
fse.writeFileSync(
path.join(outputPath, pagePath),
completePage
);
} catch (e) {
console.log("failed!", pagePath);
console.log("paths", destPath, outputPath);
console.error(e);
return;
}
};
md.use(emoji);
log(`${isRebuild ? "Reb" : "B"}uilding...`);
// clear destination folder
fse.emptyDirSync(outputPath);
// copy assets folder
await copyAssets(path.join(srcPath, "assets"));
const files = ["pages", "sitePosts"].reduce((acc, pageDir) => {
return [
...acc,
...glob
.sync("**/*.@(md|ejs|html)", {
cwd: path.join(srcPath, pageDir),
})
.map((file) =>
parseFile(file, path.join(srcPath, pageDir))
),
];
}, []),
sortByPubDate = (a, b) => {
if (a.date_pub && b.date_pub) {
let a_dt = new Date(a.date_pub).getTime(),
b_dt = new Date(b.date_pub).getTime();
if (a_dt < b_dt) {
return 1;
}
if (b_dt < a_dt) {
return -1;
}
return 0;
}
if (a.date_pub) return -1;
if (b.date_pub) return 1;
return 0;
},
pages = files
.map(({ page }) => ({ ...page }))
.filter(({ is_draft }) => !is_draft)
.sort(sortByPubDate),
tagCloud = pages.reduce((acc, curr) => {
const { tags } = curr;
tags.forEach((tag) => {
if (acc[tag]) acc[tag]++;
else acc[tag] = 1;
});
return acc;
}, {}),
tags = Object.keys(tagCloud).sort(tagSorter),
yearCloud = pages
.filter(({ content_type = "" }) => content_type === "journal")
.reduce((acc, curr) => {
const { date_pub } = curr;
if (date_pub) {
const year = new Date(date_pub).getFullYear();
if (acc[year]) acc[year]++;
else acc[year] = 1;
}
return acc;
}, {}),
years = Object.keys(yearCloud).sort().reverse(),
pagesWithContent = pages.map((page) =>
parseContent(page, { pages, tags })
);
// add data for the whole site to each page as it's rendered
pagesWithContent.forEach((page) => {
renderFile({ ...page, pages: pagesWithContent, siteTags: tags });
});
/* Journal Stuff - Tags & Years */
// make page(s) for each tag
tags.forEach((tag) => {
// check counts
let postCount = tagCloud[tag],
pageCount = Math.ceil(postCount / journalsPerPage);
for (let i = 1; i <= pageCount; i++) {
const firstEntryIndex = journalsPerPage * (i - 1),
lastEntryIndex = journalsPerPage * i;
renderFile({
content: tag,
destPath: path.join(outputPath, "journal", "tags", tag),
entriesToList: pagesWithContent
.filter(
(p) =>
p && Array.isArray(p.tags) && p.tags.includes(tag)
)
.slice(firstEntryIndex, lastEntryIndex),
layout: "tag",
path: `journal/tags/${tag}/${
i === 1 ? "index.html" : `page${i}.html`
}`,
site: { ...site, pages: pagesWithContent, tags },
pageCount,
pageNum: i,
pages: pagesWithContent,
tag,
tags,
title: `Journal Entries Tagged with #${tag}`,
});
}
});
// make page(s) for each year
years.forEach((year) => {
// check counts
let postCount = yearCloud[year],
pageCount = Math.ceil(postCount / journalsPerPage);
for (let i = 1; i <= pageCount; i++) {
const firstEntryIndex = journalsPerPage * (i - 1),
lastEntryIndex = journalsPerPage * i;
// TODO: rethink the data passed in here - you're paging solution works (kinda), take it over the finish line!
renderFile({
content: year,
destPath: path.join(outputPath, "journal", year),
entriesToList: pagesWithContent
.filter(({ content_type = "", date_pub = "" }) => {
if (!date_pub || content_type !== "journal")
return false;
const p_dt = new Date(date_pub).getTime(),
y1_dt = new Date(
`${year}-01-01T00:00:00-0500`
).getTime(),
y2_dt = new Date(
`${year}-12-31T23:59:59-0500`
).getTime();
return p_dt >= y1_dt && p_dt <= y2_dt;
})
.slice(firstEntryIndex, lastEntryIndex),
layout: "journal-year",
path: `journal/${year}/${
i === 1 ? "index.html" : `page${i}.html`
}`,
site: { ...site, pages: pagesWithContent, tags },
pageCount,
pageNum: i,
pages: pagesWithContent,
tags,
title: `Journal Entries from ${year}`,
year,
});
}
});
/* Support pages - anything too weird / specific for markdown rendering */
// collect support pages
const support = ["support"].reduce((acc, pageDir) => {
return [
...acc,
...glob
.sync("**/*.@(md|ejs|html)", {
cwd: path.join(srcPath, pageDir),
})
.map((file) =>
parseFile(
file,
path.join(srcPath, pageDir),
pagesWithContent,
true
)
),
];
}, []);
// write each one out
support.forEach((fileData) => {
const { page } = fileData;
if (page?.ext === ".ejs") {
const pageAndContent = parseContent(page, {
pages: pagesWithContent,
tags,
});
return renderFile({ ...fileData, ...pageAndContent, tags }, true);
}
return renderFile(fileData, true);
});
};

View File

@@ -1,19 +0,0 @@
{
/*
*/
/** TACO Express Default Options **/
/*
The function used to log output (console.log, morgan, etc).
Should take one (or more) strings as arguments.
*/
logFunction: null,
build: {},
site: {},
serve: {
port: 5000,
},
}

View File

@@ -1,43 +0,0 @@
module.exports = (opts, envKey) => {
const
fs = require('fs'),
path = require('path'),
json5 = require('json5'),
{ convertCamelToUpperSnakeCase, readJsonIfExists } = require('./utils'),
{ cwd, env } = process,
def = readJsonIfExists(path.resolve(__dirname, 'defaults.json5')),
// gets value from ENV || options || defaults (in that order)
getVal = (envName) => {
const snakeEnvName = `${envKey}_${convertCamelToUpperSnakeCase(envName)}`;
if (env[snakeEnvName]) return env[snakeEnvName];
if (opts[envName]) return opts[envName];
return def[envName];
},
// gets array from ENV || options || defaults (in that order)
getArray = (envName, optName = '') => {
if (optName === '') {
optName = envName;
envName = convertCamelToUpperSnakeCase(envName);
}
envName = `${envKey}_${envName}`;
if (env[envName]) return env[envName].split(path.delimiter);
if (Array.isArray(opts[optName]) && opts[optName].length) return opts[optName];
return def[optName];
};
return {
...Object.keys(def).reduce((acc, curr) => {
if (Array.isArray(def[curr])) acc[curr] = getArray(curr);
else acc[curr] = getVal(curr);
return acc;
}, {}),
};
};

View File

@@ -1,26 +0,0 @@
module.exports = async (config) => {
let isReady = false;
const
http = require('http'),
address = require('network-address'),
handler = require('serve-handler'),
build = require('./build'),
{ build: buildOpts, logFunction: log = () => {}, serve: serveOpts } = config || {},
{ outputPath, srcPath } = buildOpts || {},
{ port = 5000 } = serveOpts || {},
server = http.createServer((request, response) => {
// You pass two more arguments for config and middleware
// More details here: https://github.com/vercel/serve-handler#options
return handler(request, response, { public: outputPath });
});
await build(config);
server.listen(port, async () => {
log(`Running at http://${address()}:${port} / http://localhost:${port}`);
});
};

View File

@@ -1,57 +0,0 @@
module.exports = (() => {
const
chalk = require('chalk'),
getTime = () => {
const
now = new Date(),
tzo = -now.getTimezoneOffset(),
dif = tzo >= 0 ? '+' : '-',
pad = (num) => {
const norm = Math.floor(Math.abs(num));
return `${norm < 10 ? '0' : ''}${norm}`;
};
return [
now.getFullYear(),
'-',
pad(now.getMonth() + 1),
'-',
pad(now.getDate()),
'T',
pad(now.getHours()),
':',
pad(now.getMinutes()),
':',
pad(now.getSeconds()),
dif,
pad(tzo / 60),
':',
pad(tzo % 60)
].join('');
};
return {
convertCamelToUpperSnakeCase:
str => str.replace(/[A-Z]/g, letter => `_${letter}`).toUpperCase(),
getTime,
log: (msg) => console.log(`${chalk.grey(`${getTime()}:`)} ${msg}`),
readJsonIfExists: (filePath) => {
const
fs = require('fs'),
json5 = require('json5');
try {
return json5.parse(fs.readFileSync(filePath, {encoding: 'utf8'}));
} catch (err) {
if (err.code === 'ENOENT') return {};
throw err;
}
},
};
})();

View File

@@ -1,58 +0,0 @@
module.exports = async (config) => {
let isReady = false;
const
http = require('http'),
chokidar = require('chokidar'),
address = require('network-address'),
handler = require('serve-handler'),
build = require('./build'),
rebuild = (cfg) => {
isReady = false;
build({ ...cfg, isRebuild: true });
isReady = true;
},
{ build: buildOpts, logFunction: log = () => {}, serve: serveOpts } = config || {},
{ outputPath, srcPath } = buildOpts || {},
{ port = 5000 } = serveOpts || {},
watcher = chokidar.watch([srcPath, '*.json'], {
ignored: /(^|[\/\\])\../, // ignore dotfiles
persistent: true
})
.on('add', (path) => {
if (isReady) {
log(`File ${path} has been added`)
rebuild(config);
}
})
.on('change', (path) => {
if (isReady) {
log(`File ${path} has been changed`)
rebuild(config);
}
})
.on('ready', () => {
isReady = true;
})
.on('unlink', (path) => {
if (isReady) {
log(`File ${path} has been removed`)
rebuild(config);
}
}),
server = http.createServer((request, response) => {
// You pass two more arguments for config and middleware
// More details here: https://github.com/vercel/serve-handler#options
return handler(request, response, { public: outputPath });
});
await build(config);
server.listen(port, () => {
log(`Running at http://${address()}:${port} / http://localhost:${port}`);
});
};

1564
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +1,17 @@
{ {
"name": "iew-site-builder", "name": "iew-site",
"version": "0.9.1", "version": "0.14.1",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {},
"build": "node app.js build",
"build:prod": "cross-env NODE_ENV=production node ./lib/build",
"serve": "node app.js serve",
"watch": "node app.js watch"
},
"keywords": [], "keywords": [],
"author": "", "author": "Eric Woodward (https://www.itsericwoodward.com)",
"license": "ISC", "license": "MIT",
"dependencies": { "engines": {
"chalk": "^4.1.2", "npm": ">=9",
"chokidar": "^3.4.3", "node": ">=18"
"dotenv": "^16.0.1", },
"highlight.js": "^11.3.1",
"json5": "^2.1.3",
"markdown-it": "^13.0.1",
"markdown-it-emoji": "^2.0.0",
"network-address": "^1.1.2",
"serve-handler": "^6.1.3"
},
"devDependencies": { "devDependencies": {
"ejs": "^3.1.5", "web-weevr": "git+ssh://git@git.itsericwoodward.com:eric/web-weevr.git"
"front-matter": "^4.0.2",
"fs-extra": "^10.0.0",
"glob": "^8.0.3",
"serve": "^14.1.2",
"yargs": "^17.3.1"
} }
} }

View File

@@ -3,24 +3,58 @@
title: "It's Eric Woodward (dotcom)", title: "It's Eric Woodward (dotcom)",
author: { author: {
name: "Eric Woodward", name: "Eric Woodward",
email: "redacted@nunyodam.com", // not used email: "hey@itsericwoodward.com", // not used
photo: "/images/eric-8bit.gif", photo: "/images/eric-8bit.gif",
site: "https://itsericwoodward.com", site: "https://itsericwoodward.com",
geo: {
position: "35.4, -80.5",
placename: "Concord",
region: "US-NC",
},
}, },
base_uri: "", 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", robots: "index,follow",
language: "en-us", language: "en-us",
copyright: "Copyright 2014-2022 Eric Woodward, licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.", copyright: "Copyright 2014-2025 Eric Woodward, licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.",
basePath: "", basePath: "",
uri: "https://www.itsericwoodward.com", 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: { build: {
journalsPerPage: 5, journalsPerPage: 5,
srcPath: "src", srcPath: "src",
outputPath: "out", outputPath: "out",
publishPath: "public",
}, },
serve: { serve: {
authTypeUI: "basic",
handleStatic: true,
port: 4997, port: 4997,
shortCodeLink: "/q/",
static404: "./public/errors/404.html",
}, },
} }

Binary file not shown.

View File

@@ -1,348 +0,0 @@
/*!
* jQLite JavaScript Library v1.1.1 (http://code.google.com/p/jqlite/)
* Copyright (c) 2010 Brett Fattori (bfattori@gmail.com)
* Licensed under the MIT license
* http://www.opensource.org/licenses/mit-license.php
*
* Many thanks to the jQuery team's efforts. Some code is
* Copyright (c) 2010, John Resig. See
* http://jquery.org/license
*
* @author Brett Fattori (bfattori@gmail.com)
* @author $Author: bfattori $
* @version $Revision: 145 $
*
* Created: 03/29/2010
* Modified: $Date: 2010-06-21 11:08:14 -0400 (Mon, 21 Jun 2010) $
*/
(function(){function B(){return+new Date}var D=function(a,b){if(a===""&&b)return b;var d=a.split(" "),c=d.shift(),e;if(c.charAt(0)=="#"){var g=i.getElementById(c.substring(1));e=g?[g]:[]}else{e=c.charAt(0)!=="."?c.split(".")[0]:"*";var h=c.split("."),j=null;if(e.indexOf("[")!=-1){j=e;e=e.substr(0,e.indexOf("["))}g=function(o){var n=arguments.callee,k;if(!(k=!n.needClass)){k=n.classes;if(o.className.length==0)k=false;else{for(var r=o.className.split(" "),l=k.length,p=0;p<k.length;p++)f.inArray(k[p],
r)!=-1&&l--;k=l==0}}if(k=k){if(!(k=!n.needAttribute)){n=n.attributes;k=true;for(r=0;r<n.length;r++){l=n[r].split("=");p=l[0].indexOf("!")!=-1||l[0].indexOf("*")!=-1?l[0].charAt(l[0].length-1)+"=":"=";if(p!="=")l[0]=l[0].substring(0,l[0].length-1);switch(p){case "=":k&=o.getAttribute(l[0])===l[1];break;case "!=":k&=o.getAttribute(l[0])!==l[1];break;case "*=":k&=o.getAttribute(l[0]).indexOf(l[1])!=-1;break;default:k=false}}k=k}k=k}if(k)return o};for(var u=[],s=0;s<b.length;s++)for(var C=b[s].getElementsByTagName(e),
v=0;v<C.length;v++)u.push(C[v]);h&&h.shift();e=[];g.classes=h;if(j!=null){var w=j.indexOf("[");s=j.lastIndexOf("]");w=j.substring(w+1,s).split("][")}g.attributes=j!=null?w:null;g.needClass=c.indexOf(".")!=-1&&h.length>0;g.needAttribute=j!=null;for(c=0;c<u.length;c++)g(u[c])&&e.push(u[c])}return D(d.join(" "),e)},Q=function(a,b){b=b||i;if(a.nodeType&&a.nodeType===E){a=i.body;if(a===null)return[i]}if(a.nodeType&&a.nodeType===m)return[a];if(a.jquery&&typeof a.jquery==="string")return a.toArray();if(b)b=
F(b);if(f.isArray(a))return a;else if(typeof a==="string"){for(var d=[],c=0;c<b.length;c++){var e=[b[c]];if(!f.forceSimpleSelectorEngine&&e[0].querySelectorAll){e=e[0].querySelectorAll(a);for(var g=0;g<e.length;g++)d.push(e.item(g))}else d=d.concat(D(a,e))}return d}else return null},G=false;setTimeout(function(){var a=i.body;if(a){var b=i.createElement("script"),d="i"+(new Date).getTime();b.type="text/javascript";try{b.appendChild(i.createTextNode("window."+d+"=1;"))}catch(c){}a.insertBefore(b,a.firstChild);
var e=true;if(window[d])delete window[d];else e=false;a.removeChild(b);G=e}else setTimeout(arguments.callee,33)},33);var H=function(a){var b=i.createElement("div");b.innerHTML=a;return{scripts:b.getElementsByTagName("script"),data:a}},I=function(a){a=a.replace(/-/g," ");a=a;var b=true;b=b||false;a=!a?"":a.toString().replace(/^\s*|\s*$/g,"");var d="";if(a.length<=0)a="";else{var c=false;d+=b?a.charAt(0):a.charAt(0).toUpperCase();for(b=1;b<a.length;b++){d+=c?a.charAt(b).toUpperCase():a.charAt(b).toLowerCase();
var e=a.charCodeAt(b);c=e==32||e==45||e==46;if(e==99||e==67)if(a.charCodeAt(b-1)==77||a.charCodeAt(b-1)==109)c=true}a=d}return a.replace(/ /g,"")},J={click:"MouseEvents",dblclick:"MouseEvents",mousedown:"MouseEvents",mouseup:"MouseEvents",mouseover:"MouseEvents",mousemove:"MouseEvents",mouseout:"MouseEvents",contextmenu:"MouseEvents",keypress:"KeyEvents",keydown:"KeyEvents",keyup:"KeyEvents",load:"HTMLEvents",unload:"HTMLEvents",abort:"HTMLEvents",error:"HTMLEvents",resize:"HTMLEvents",scroll:"HTMLEvents",
select:"HTMLEvents",change:"HTMLEvents",submit:"HTMLEvents",reset:"HTMLEvents",focus:"HTMLEvents",blur:"HTMLEvents",touchstart:"MouseEvents",touchend:"MouseEvents",touchmove:"MouseEvents"},K=function(a,b,d){if(f.isFunction(d)){if(typeof b==="string")b=b.toLowerCase();var c=J[b];if(b.indexOf("on")==0)b=b.substring(2);if(c){c=function(e){var g=arguments.callee,h=e.data||[];h.unshift(e);g=g.fn.apply(a,h);if(typeof g!="undefined"&&g===false){if(e.preventDefault&&e.stopPropagation){e.preventDefault();
e.stopPropagation()}else{e.returnValue=false;e.cancelBubble=true}return false}return true};c.fn=d;a.addEventListener?a.addEventListener(b,c,false):a.attachEvent("on"+b,c)}else{if(!a._handlers)a._handlers={};c=a._handlers[b]||[];c.push(d);a._handlers[b]=c}}},f=function(a,b){return(new x).init(a,b)},i=window.document,y=Object.prototype.hasOwnProperty,z=Object.prototype.toString,L=Array.prototype.push,R=Array.prototype.slice,m=1,E=9,A=[],M=false,N=false,q;f.forceSimpleSelectorEngine=false;f.each=function(a,
b){var d,c=0,e=a.length;if(e===undefined||f.isFunction(a))for(d in a){if(b.call(a[d],d,a[d])===false)break}else for(d=a[0];c<e&&b.call(d,c,d)!==false;d=a[++c]);return a};f.noop=function(){};f.isFunction=function(a){return z.call(a)==="[object Function]"};f.isArray=function(a){return z.call(a)==="[object Array]"};f.isPlainObject=function(a){if(!a||z.call(a)!=="[object Object]"||a.nodeType||a.setInterval)return false;if(a.constructor&&!y.call(a,"constructor")&&!y.call(a.constructor.prototype,"isPrototypeOf"))return false;
var b;for(b in a);return b===undefined||y.call(a,b)};f.merge=function(a,b){var d=a.length,c=0;if(typeof b.length==="number")for(var e=b.length;c<e;c++)a[d++]=b[c];else for(;b[c]!==undefined;)a[d++]=b[c++];a.length=d;return a};f.param=function(a){var b="";a&&f.each(a,function(d,c){b+=(b.length!=0?"&":"")+c+"="+encodeURIComponent(d)});return b};f.evalScripts=function(a){for(var b=i.getElementsByTagName("head")[0]||i.documentElement,d=0;d<a.length;d++){var c=i.createElement("script");c.type="text/javascript";
if(G)c.appendChild(i.createTextNode(a[d].text));else c.text=a[d].text;b.insertBefore(c,b.firstChild);b.removeChild(c)}};f.ready=function(){for(M=true;A.length>0;)A.shift()()};var t="jQuery"+B(),S=0,O={};f.noData={embed:true,object:true,applet:true};f.cache={};f.data=function(a,b,d){if(!(a.nodeName&&jQuery.noData[a.nodeName.toLowerCase()])){a=a==window?O:a;var c=a[t];c||(c=a[t]=++S);if(b&&!jQuery.cache[c])jQuery.cache[c]={};if(d!==undefined)jQuery.cache[c][b]=d;return b?jQuery.cache[c][b]:c}};f.removeData=
function(a,b){a=a==window?O:a;var d=a[t];if(b){if(jQuery.cache[d]){delete jQuery.cache[d][b];b="";for(b in jQuery.cache[d])break;b||jQuery.removeData(a)}}else{try{delete a[t]}catch(c){a.removeAttribute&&a.removeAttribute(t)}delete jQuery.cache[d]}};f.ajax={status:-1,statusText:"",responseText:null,responseXML:null,send:function(a,b,d){if(f.isFunction(b)){d=b;b={}}if(a){var c=true,e=null,g=null;if(typeof b.async!=="undefined"){c=b.async;delete b.async}if(typeof b.username!=="undefined"){e=b.username;
delete b.username}if(typeof b.password!=="undefined"){g=b.password;delete b.password}b=f.param(b);if(b.length!=0)a+=(a.indexOf("?")==-1?"?":"&")+b;b=new XMLHttpRequest;b.open("GET",a,c,e,g);b.send();if(c){a=function(h){var j=arguments.callee;h.status==200?f.ajax.complete(h,j.cb):f.ajax.error(h,j.cb)};a.cb=d;d=function(){var h=arguments.callee;h.req.readyState!=4?setTimeout(h,250):h.xcb(h.req)};d.req=b;d.xcb=a;setTimeout(d,250)}}},complete:function(a,b){f.ajax.status=a.status;f.ajax.responseText=a.responseText;
f.ajax.responseXML=a.responseXML;f.isFunction(b)&&b(a.responseText,a.status)},error:function(a,b){f.ajax.status=a.status;f.ajax.statusText=a.statusText;f.isFunction(b)&&b(a.status,a.statusText)}};f.makeArray=function(a,b){var d=b||[];if(a!=null)a.length==null||typeof a==="string"||jQuery.isFunction(a)||typeof a!=="function"&&a.setInterval?L.call(d,a):f.merge(d,a);return d};f.inArray=function(a,b){for(var d=0;d<b.length;d++)if(b[d]===a)return d;return-1};f.trim=function(a){return a!=null?a.toString().replace(/^\s*|\s*$/g,
""):""};var x=function(){};x.prototype={selector:"",context:null,length:0,jquery:"jqlite-1.1.1",init:function(a,b){if(!a)return this;if(a.nodeType){this.context=this[0]=a;this.length=1}else if(typeof a==="function")this.ready(a);else{var d=[];if(a.jquery&&typeof a.jquery==="string")d=a.toArray();else if(f.isArray(a))d=a;else if(typeof a==="string"&&f.trim(a).indexOf("<")==0&&f.trim(a).indexOf(">")!=-1){d=f.trim(a).toLowerCase();d=d.indexOf("<option")==0?"SELECT":d.indexOf("<li")==0?"UL":d.indexOf("<tr")==
0?"TBODY":d.indexOf("<td")==0?"TR":"DIV";d=i.createElement(d);d.innerHTML=a;d=[d.removeChild(d.firstChild)]}else{if(a.indexOf(",")!=-1){d=a.split(",");for(var c=0;c<d.length;c++)d[c]=f.trim(d[c])}else d=[a];c=[];for(var e=0;e<d.length;e++)c=c.concat(Q(d[e],b));d=c}L.apply(this,d)}return this},each:function(a){return f.each(this,a)},size:function(){return this.length},toArray:function(){return R.call(this,0)},ready:function(a){if(M)a();else{A.push(a);return this}},data:function(a,b){if(typeof a===
"undefined"&&this.length)return jQuery.data(this[0]);else if(typeof a==="object")return this.each(function(){jQuery.data(this,a)});var d=a.split(".");d[1]=d[1]?"."+d[1]:"";if(b===undefined){if(data===undefined&&this.length)data=jQuery.data(this[0],a);return data===undefined&&d[1]?this.data(d[0]):data}else return this.each(function(){jQuery.data(this,a,b)})},removeData:function(a){return this.each(function(){jQuery.removeData(this,a)})},addClass:function(a){return this.each(function(){if(this.className.length!=
0){var b=this.className.split(" ");if(f.inArray(a,b)==-1){b.push(a);this.className=b.join(" ")}}else this.className=a})},removeClass:function(a){return this.each(function(){if(this.className.length!=0){var b=this.className.split(" "),d=f.inArray(a,b);if(d!=-1){b.splice(d,1);this.className=b.join(" ")}}})},hasClass:function(a){if(this[0].className.length==0)return false;return f.inArray(a,this[0].className.split(" "))!=-1},isElementName:function(a){return this[0].nodeName.toLowerCase()===a.toLowerCase()},
toggleClass:function(a){return this.each(function(){if(this.className.length==0)this.className=a;else{var b=this.className.split(" "),d=f.inArray(a,b);d!=-1?b.splice(d,1):b.push(a);this.className=b.join(" ")}})},hide:function(a){return this.each(function(){if(this.style&&this.style.display!=null)if(this.style.display.toString()!="none"){this._oldDisplay=this.style.display.toString()||(this.nodeName!="span"?"block":"inline");this.style.display="none"}f.isFunction(a)&&a(this)})},show:function(a){return this.each(function(){this.style.display=
(this._oldDisplay&&this._oldDisplay!=""?this._oldDisplay:null)||(this.nodeName!="span"?"block":"inline");f.isFunction(a)&&a(this)})},css:function(a,b){if(typeof a==="string"&&b==null)return this[0].style[I(a)];else{a=typeof a==="string"?P(a,b):a;return this.each(function(){var d=this;typeof d.style!="undefined"&&f.each(a,function(c,e){e=typeof e==="number"?e+"px":e;var g=I(c);d.style[g]||(g=c);d.style[g]=e})})}},load:function(a,b,d){if(f.isFunction(b)){d=b;b={}}return this.each(function(){var c=function(e,
g){var h=arguments.callee;if(e){var j=H(e);h.elem.innerHTML=j.data;f.evalScripts(j.scripts)}f.isFunction(h.cback)&&h.cback(e,g)};c.cback=d;c.elem=this;f.ajax.send(a,b,c)})},html:function(a){return a?this.each(function(){var b=H(a);this.innerHTML=b.data;f.evalScripts(b.scripts)}):this[0].innerHTML},attr:function(a,b){return typeof a==="string"&&b==null?this[0]?this[0].getAttribute(a):"":this.each(function(){a=typeof a==="string"?P(a,b):a;for(var d in a)this.setAttribute(d,a[d])})},eq:function(a){var b=
this.toArray();this.context=this[0]=a<0?b[b.length+a]:b[a];this.length=1;return this},first:function(){this.context=this[0]=this.toArray()[0];this.length=1;return this},last:function(){var a=this.toArray();this.context=this[0]=a[a.length-1];this.length=1;return this},index:function(a){var b=-1;if(this.length!=0){var d=this[0];if(a){var c=f(a)[0];this.each(function(g){if(this===c){b=g;return false}})}else{a=this.parent()[0].firstChild;for(var e=[];a!=null;){a.nodeType===m&&e.push(a);a=a.nextSibling}f.each(a,
function(g){if(this===d){b=g;return false}})}}return b},next:function(a){var b=[];if(a){var d=f(a);this.each(function(){for(var c=this.nextSibling;c!=null&&c.nodeType!==m;)c=c.nextSibling;if(c!=null){var e=false;d.each(function(){if(this==c){e=true;return false}});e&&b.push(c)}})}else this.each(function(){for(var c=this.nextSibling;c!=null&&c.nodeType!==m;)c=c.nextSibling;c!=null&&b.push(c)});return f(b)},prev:function(a){var b=[];if(a){var d=f(a);this.each(function(){for(var c=this.previousSibling;c!=
null&&c.nodeType!==m;)c=c.previousSibling;if(c!=null){var e=false;d.each(function(){if(this==c){e=true;return false}});e&&b.push(c)}})}else this.each(function(){for(var c=this.previousSibling;c!=null&&c.nodeType!==m;)c=c.previousSibling;c!=null&&b.push(c)});return f(b)},parent:function(a){var b=[];if(a){var d=f(a);this.each(function(){var c=this.parentNode,e=false;d.each(function(){if(this==c){e=true;return false}});e&&b.push(c)})}else this.each(function(){b.push(this.parentNode)});return f(b)},parents:function(a){var b=
[];if(a){var d=f(a);this.each(function(){for(var c=this;c!=i.body;){d.each(function(){this==c&&b.push(c)});c=c.parentNode}})}else this.each(function(){for(var c=this;c!=i.body;){c=c.parentNode;b.push(c)}});return f(b)},children:function(a){var b=[];if(a){var d=f(a);this.each(function(){for(var c=this.firstChild;c!=null;){c.nodeType==m&&d.each(function(){this===c&&b.push(c)});c=c.nextSibling}})}else this.each(function(){for(var c=this.firstChild;c!=null;){c.nodeType==m&&b.push(c);c=c.nextSibling}});
return f(b)},append:function(a){a=F(a);return this.each(function(){for(var b=0;b<a.length;b++)this.appendChild(a[b])})},remove:function(a){return this.each(function(){a?$(a,this).remove():this.parentNode.removeChild(this)})},empty:function(){return this.each(function(){this.innerHTML=""})},val:function(a){if(a==null){var b=null;if(this&&this.length!=0&&typeof this[0].value!="undefined")b=this[0].value;return b}else return this.each(function(){if(typeof this.value!="undefined")this.value=a})},bind:function(a,
b){return this.each(function(){K(this,a,b)})},trigger:function(a,b){return this.each(function(){var d;var c;c=a;if(typeof c==="string")c=c.toLowerCase();var e=null,g=J[c]||"Event";if(i.createEvent){e=i.createEvent(g);e._eventClass=g;c&&e.initEvent(c,true,true)}if(i.createEventObject){e=i.createEventObject();if(c){e.type=c;e._eventClass=g}}c=e;if(c._eventClass!=="Event"){c.data=b;d=this.dispatchEvent(c)}else if(e=(this._handlers||{})[a])for(g=0;g<e.length;g++){var h=f.isArray(b)?b:[];h.unshift(c);
h=e[g].apply(this,h);if(!(typeof h=="undefined"?true:h))break}return d})},submit:function(a){return this.each(function(){if(f.isFunction(a))K(this,"onsubmit",a);else this.submit&&this.submit()})}};if(i.addEventListener)q=function(){i.removeEventListener("DOMContentLoaded",q,false);f.ready()};else if(i.attachEvent)q=function(){if(i.readyState==="complete"){i.detachEvent("onreadystatechange",q);f.ready()}};if(!N){N=true;if(i.readyState==="complete")return f.ready();if(i.addEventListener){i.addEventListener("DOMContentLoaded",
q,false);window.addEventListener("load",f.ready,false)}else if(i.attachEvent){i.attachEvent("onreadystatechange",q);window.attachEvent("onload",f.ready)}}var P=function(a,b){var d={};d[a]=b;return d},F=function(a){if(a.nodeType&&(a.nodeType===m||a.nodeType===E))a=[a];else if(typeof a==="string")a=f(a).toArray();else if(a.jquery&&typeof a.jquery==="string")a=a.toArray();return a};if(typeof window.jQuery=="undefined"){window.jQuery=f;window.jQuery.fn=x.prototype;window.$=window.jQuery;window.now=B}jQuery.extend=
jQuery.fn.extend=function(){var a=arguments[0]||{},b=1,d=arguments.length,c=false,e,g,h,j;if(typeof a==="boolean"){c=a;a=arguments[1]||{};b=2}if(typeof a!=="object"&&!jQuery.isFunction(a))a={};if(d===b){a=this;--b}for(;b<d;b++)if((e=arguments[b])!=null)for(g in e){h=a[g];j=e[g];if(a!==j)if(c&&j&&(jQuery.isPlainObject(j)||jQuery.isArray(j))){h=h&&(jQuery.isPlainObject(h)||jQuery.isArray(h))?h:jQuery.isArray(j)?[]:{};a[g]=jQuery.extend(c,h,j)}else if(j!==undefined)a[g]=j}return a};jQuery.each("click,dblclick,mouseover,mouseout,mousedown,mouseup,keydown,keypress,keyup,focus,blur,change,select,error,load,unload,scroll,resize,touchstart,touchend,touchmove".split(","),
function(a,b){jQuery.fn[b]=function(d){return d?this.bind(b,d):this.trigger(b)}})})();
(function() {
this.Dosbox = (function() {
function Dosbox(options) {
this.onload = options.onload;
this.onrun = options.onrun;
this.ui = new Dosbox.UI(options);
this.module = new Dosbox.Module({
canvas: this.ui.canvas
});
this.ui.onStart((function(_this) {
return function() {
_this.ui.showLoader();
return _this.downloadScript();
};
})(this));
}
Dosbox.prototype.run = function(archiveUrl, executable) {
return new Dosbox.Mount(this.module, archiveUrl, {
success: (function(_this) {
return function() {
var func, hide;
_this.ui.updateMessage("Launching " + executable);
hide = function() {
return _this.ui.hideLoader();
};
func = function() {
return _this._dosbox_main(_this, executable);
};
setTimeout(func, 1000);
return setTimeout(hide, 3000);
};
})(this),
progress: (function(_this) {
return function(total, current) {
return _this.ui.updateMessage("Mount " + executable + " (" + (current * 100 / total | 0) + "%)");
};
})(this)
});
};
Dosbox.prototype.requestFullScreen = function() {
if (this.module.requestFullScreen) {
return this.module.requestFullScreen(true, false);
}
};
Dosbox.prototype.downloadScript = function() {
this.module.setStatus('Downloading js-dos');
this.ui.updateMessage('Downloading js-dos');
return new Dosbox.Xhr('https://js-dos.com/cdn/js-dos-v3.js', {
success: (function(_this) {
return function(script) {
var func;
_this.ui.updateMessage('Initializing dosbox');
func = function() {
return _this._jsdos_init(_this.module, script, _this.onload);
};
return setTimeout(func, 1000);
};
})(this),
progress: (function(_this) {
return function(total, current) {
return _this.ui.updateMessage("Downloading js-dos (" + (current * 100 / total | 0) + "%)");
};
})(this)
});
};
Dosbox.prototype._jsdos_init = function(module, script, onload) {
var Module;
Module = module;
eval(script);
if (onload) {
return onload(this);
}
};
Dosbox.prototype._dosbox_main = function(dosbox, executable) {
var exception, func;
try {
if (dosbox.onrun) {
func = function() {
return dosbox.onrun(dosbox, executable);
};
setTimeout(func, 1000);
}
return dosbox.module.ccall('dosbox_main', 'int', ['string'], [executable]);
} catch (error) {
exception = error;
if (exception === 'SimulateInfiniteLoop') {
} else {
return typeof console !== "undefined" && console !== null ? typeof console.error === "function" ? console.error(exception) : void 0 : void 0;
}
}
};
return Dosbox;
})();
}).call(this);
(function() {
Dosbox.Module = (function() {
function Module(options) {
this.elCanvas = options.canvas;
this.canvas = this.elCanvas[0];
}
Module.prototype.preRun = [];
Module.prototype.postRun = [];
Module.prototype.totalDependencies = 0;
Module.prototype.print = function(text) {
text = Array.prototype.slice.call(arguments).join(' ');
return typeof console !== "undefined" && console !== null ? typeof console.log === "function" ? console.log(text) : void 0 : void 0;
};
Module.prototype.printErr = function(text) {
text = Array.prototype.slice.call(arguments).join(' ');
return typeof console !== "undefined" && console !== null ? typeof console.error === "function" ? console.error(text) : void 0 : void 0;
};
Module.prototype.setStatus = function(text) {
return typeof console !== "undefined" && console !== null ? typeof console.log === "function" ? console.log(text) : void 0 : void 0;
};
Module.prototype.monitorRunDependencies = function(left) {
var status;
this.totalDependencies = Math.max(this.totalDependencies, left);
status = left ? "Preparing... (" + (this.totalDependencies - left) + "/" + this.totalDependencies + ")" : 'All downloads complete.';
return this.setStatus(status);
};
return Module;
})();
}).call(this);
(function() {
Dosbox.Mount = (function() {
function Mount(module, url, options) {
this.module = module;
new Dosbox.Xhr(url, {
success: (function(_this) {
return function(data) {
var bytes;
bytes = _this._toArray(data);
if (_this._mountZip(bytes)) {
return options.success();
} else {
return typeof console !== "undefined" && console !== null ? typeof console.error === "function" ? console.error('Unable to mount', url) : void 0 : void 0;
}
};
})(this),
progress: options.progress
});
}
Mount.prototype._mountZip = function(bytes) {
var buffer, extracted;
buffer = this.module._malloc(bytes.length);
this.module.HEAPU8.set(bytes, buffer);
extracted = this.module.ccall('extract_zip', 'int', ['number', 'number'], [buffer, bytes.length]);
this.module._free(buffer);
return extracted === 0;
};
Mount.prototype._toArray = function(data) {
var arr, i, len;
if (typeof data === 'string') {
arr = new Array(data.length);
i = 0;
len = data.length;
while (i < len) {
arr[i] = data.charCodeAt(i);
++i;
}
return arr;
}
return data;
};
return Mount;
})();
}).call(this);
(function() {
Dosbox.UI = (function() {
function UI(options) {
this.appendCss();
this.div = $('#' + (options.id || 'dosbox'));
this.wrapper = $('<div class="dosbox-container">');
this.canvas = $('<canvas class="dosbox-canvas" oncontextmenu="event.preventDefault()">');
this.overlay = $('<div class="dosbox-overlay">');
this.loaderMessage = $('<div class="dosbox-loader-message">');
this.loader = $('<div class="dosbox-loader">').append($('<div class="st-loader">').append($('<span class="equal">'))).append(this.loaderMessage);
this.start = $('<div class="dosbox-start">Click to start');
this.div.append(this.wrapper);
this.wrapper.append(this.canvas);
this.wrapper.append(this.loader);
this.wrapper.append(this.overlay);
this.overlay.append($('<div class="dosbox-powered">Powered by &nbsp;').append($('<a href="http://js-dos.com">js-dos.com')));
this.overlay.append(this.start);
}
UI.prototype.onStart = function(fun) {
return this.start.click((function(_this) {
return function() {
fun();
return _this.overlay.hide();
};
})(this));
};
UI.prototype.appendCss = function() {
var head, style;
head = document.head || document.getElementsByTagName('head')[0];
style = document.createElement('style');
style.type = 'text/css';
if (style.styleSheet) {
style.styleSheet.cssText = this.css;
} else {
style.appendChild(document.createTextNode(this.css));
}
return head.appendChild(style);
};
UI.prototype.showLoader = function() {
this.loader.show();
return this.loaderMessage.html('');
};
UI.prototype.updateMessage = function(message) {
return this.loaderMessage.html(message);
};
UI.prototype.hideLoader = function() {
return this.loader.hide();
};
UI.prototype.css = '.dosbox-container { position: relative; min-width: 320px; min-height: 200px; } .dosbox-canvas { } .dosbox-overlay, .dosbox-loader { position: absolute; left: 0; right: 0; top: 0; bottom: 0; background-color: #333; } .dosbox-start { text-align: center; position: absolute; left: 0; right: 0; bottom: 50%; color: #f80; font-size: 1.5em; text-decoration: underline; cursor: pointer; } .dosbox-overlay a { color: #f80; } .dosbox-loader { display: none; } .dosbox-powered { position: absolute; right: 1em; bottom: 1em; font-size: 0.8em; color: #9C9C9C; } .dosbox-loader-message { text-align: center; position: absolute; left: 0; right: 0; bottom: 50%; margin: 0 0 -3em 0; box-sizing: border-box; color: #f80; font-size: 1.5em; } @-moz-keyframes loading { 0% { left: 0; } 50% { left: 8.33333em; } 100% { left: 0; } } @-webkit-keyframes loading { 0% { left: 0; } 50% { left: 8.33333em; } 100% { left: 0; } } @keyframes loading { 0% { left: 0; } 50% { left: 8.33333em; } 100% { left: 0; } } .st-loader { width: 10em; height: 2.5em; position: absolute; top: 50%; left: 50%; margin: -1.25em 0 0 -5em; box-sizing: border-box; } .st-loader:before, .st-loader:after { content: ""; display: block; position: absolute; top: 0; bottom: 0; width: 1.25em; box-sizing: border-box; border: 0.25em solid #f80; } .st-loader:before { left: -0.76923em; border-right: 0; } .st-loader:after { right: -0.76923em; border-left: 0; } .st-loader .equal { display: block; position: absolute; top: 50%; margin-top: -0.5em; left: 4.16667em; height: 1em; width: 1.66667em; border: 0.25em solid #f80; box-sizing: border-box; border-width: 0.25em 0; -moz-animation: loading 1.5s infinite ease-in-out; -webkit-animation: loading 1.5s infinite ease-in-out; animation: loading 1.5s infinite ease-in-out; }';
return UI;
})();
}).call(this);
(function() {
Dosbox.Xhr = (function() {
function Xhr(url, options) {
var e;
this.success = options.success;
this.progress = options.progress;
if (window.ActiveXObject) {
try {
this.xhr = new ActiveXObject('Microsoft.XMLHTTP');
} catch (error) {
e = error;
this.xhr = null;
}
} else {
this.xhr = new XMLHttpRequest();
}
this.xhr.open('GET', url, true);
this.xhr.overrideMimeType('text/plain; charset=x-user-defined');
this.xhr.addEventListener('progress', (function(_this) {
return function(evt) {
if (_this.progress) {
return _this.progress(evt.total, evt.loaded);
}
};
})(this));
this.xhr.onreadystatechange = (function(_this) {
return function() {
return _this._onReadyStateChange();
};
})(this);
this.xhr.send();
}
Xhr.prototype._onReadyStateChange = function() {
if (this.xhr.readyState === 4 && this.success) {
return this.success(this.xhr.responseText);
}
};
return Xhr;
})();
}).call(this);

View File

@@ -1,41 +0,0 @@
{
"name": "Its Eric Woodward! (dotcom)",
"icons": [
{
"src": "\/images\/favicons\/android-icon-36x36.png",
"sizes": "36x36",
"type": "image\/png",
"density": "0.75"
},
{
"src": "\/images\/favicons\/android-icon-48x48.png",
"sizes": "48x48",
"type": "image\/png",
"density": "1.0"
},
{
"src": "\/images\/favicons\/android-icon-72x72.png",
"sizes": "72x72",
"type": "image\/png",
"density": "1.5"
},
{
"src": "\/images\/favicons\/android-icon-96x96.png",
"sizes": "96x96",
"type": "image\/png",
"density": "2.0"
},
{
"src": "\/images\/favicons\/android-icon-144x144.png",
"sizes": "144x144",
"type": "image\/png",
"density": "3.0"
},
{
"src": "\/images\/favicons\/android-icon-192x192.png",
"sizes": "192x192",
"type": "image\/png",
"density": "4.0"
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,74 @@
<!DOCTYPE html>
<html>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<head>
<title>Byte Beasties: Escape!</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="content" id="content">
<h1>Byte Beasties: Escape!</h1>
<noscript>
Sorry, this game requires JS to function.
</noscript>
<div class="game" id="game">
<div class="settings" id="settings">
<div>
<h2>Story</h2>
<p>
The Byte Beasties are on the run from the GTP (General Thinking Program),
an evil AI that wants to absorb them into its software.
</p>
<p>
Help the Byte Beasties escape GTP's yellow absorption wave by
hitting the green logic gates and avoiding the red NULL gates.
</p>
</div>
<hr />
<form id="settingsForm" name="settings"" method="dialog"">
<h2>Settings</h2>
<p>
<label>Mode:
<select name="mode" id="settingsMode">
<option value="easy">Easy</option>
<option value="medium">Medium</option>
<option value="hard">Hard</option>
<option value="endless">Endless</option>
</select>
</label>
</p>
<p class="beastiePickerWrapper">
Beastie:
<div class="settingsBeastie" id="settingsBeastie"></div>
</p>
<p class="settingsFooter">
<button id="cancelSettings">Cancel</button>
<button id="applySettings">Apply & Reset</button>
</p>
</form>
</div>
</div>
<h2>Controls</h2>
<ul class="controls">
<li><strong>&larr; OR &rarr;</strong>: Move the beastie</dd>
<li><strong>P</strong>: Pause</li>
<li><strong>R</strong> Restart game</li>
<li><strong>S</strong>: Show Settings</dd>
</ul>
</div>
<!-- We load this library via "module" script-tag to guarantee ES6 minimum functionality -->
<script type="module" src="scripts/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,656 @@
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,
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,
// 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");
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";
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)";
ctx.save();
ctx.globalCompositeOperation = "lighter";
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;
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");
if (settingsMode) {
const { value } = settingsMode;
game.winScore = game.winScores[value];
game.blockerFrequency = game.blockerFrequencies[value];
}
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;
player.dx = 0;
player.dy = 0;
enemy.y = canvas.height - 50;
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);
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;
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:
${beastie.score}`,
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.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,
}));
// 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;
// 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;
// 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 (
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;
if (beastie.dy < 0) {
beastie.dy = 0;
player.dy = 0;
}
}
}
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;
}
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 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;
}
return gate;
});
// 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;
// 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);
beastie.score += Math.floor(Math.abs(beastie.dy));
ctx.fillStyle = "white";
ctx.font = "12px sans";
ctx.fillText(`Score: ${beastie.score}`, 10, 20);
renderLightning();
};
// 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;
}
/*
} else if (e.code === "ArrowDown") {
keydown = true;
player.dy = 1;
beastie.dy = 3;
} else if (e.code === "ArrowUp") {
keydown = true;
player.dy = -1;
beastie.dy = -3;
*/
}
});
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(
"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(
"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");
});
// start the game
restartGame();
requestAnimationFrame(loop);
};
export { startGame };

View File

@@ -0,0 +1,45 @@
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;
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;
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,
};
};

View File

@@ -0,0 +1,32 @@
// @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0
/****************************************************************************
* Planar Vagabond's Guide to the Multiverse (planarvagabond.com)
*
* Copyright 2023-2024 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 { startGame } from "./bbe.js";
export default (() => {
// 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");
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);
startGame(canvas);
}, 1);
})();
// @license-end

View File

@@ -0,0 +1,82 @@
html, body {
height: 100%;
margin: 0;
}
body {
display: flex;
align-items: center;
justify-content: center;
background-color: #333;
color: #ddd;
}
canvas {
border: 1px solid black;
z-index: 0;
}
.beastieImage {
max-width: 2rem;
}
.beastieImageInputWrapper {
align-items: center;
display: flex;
flex-direction: row;
justify-content: center;
padding: .5rem;
}
.content {
display: flex;
flex-direction: column;
max-width: 375px;
}
.game {
position: relative;
}
.settings {
background-color: rgba(33, 33, 33, .95);
bottom: 0;
left: 0;
opacity: 0;
padding: 0 1rem 1rem;
position: absolute;
right: 0;
top: 0;
transition: opacity ease 0.3s;
z-index: 1;
}
.settings.showSettings {
opacity: 1;
}
.settingsBeastie {
align-items: center;
display: flex;
flex-wrap: wrap;
justify-content: center;
}
.settingsBeastie label {
padding: .5rem;
}
.settingsFooter {
margin: 1rem;
text-align: right;
}
.toggleSettings {
background-color: rgba(33, 33, 33, .9);
border: 1px solid rgba(33, 33, 33, .9);
cursor: pointer;
position: absolute;
right: 10px;
top: 5px;
z-index: 2;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@@ -0,0 +1,150 @@
<!DOCTYPE html>
<html lang="en">
<!--
/****************************************************************************
* BPS (Bill Paxton Soundboard)
*
* Copyright 2024 Eric Woodward
* Source released under CC0 Public Domain License v1.0
* https://www.itsericwoodward.com/licenses/cc0/
* http://creativecommons.org/publicdomain/zero/1.0/
****************************************************************************/
-->
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>BPS (Bill Paxton Soundboard)</title>
<link rel="stylesheet" href="styles/styles.css">
</head>
<body>
<div id="output"></div>
<!--
`cat main.js | openssl dgst -sha256 -binary | openssl base64 -A`, then repeat for 384 and 512
-->
<script crossorigin="anonymous" integrity="sha256-YvGi/WY2dKJwaU3cCMLSkiJCnE2hFCiiUdRwqWTGvEE=
sha384-mtemuzaWEW/0HsyxJyDCaqMquUejVMwzs5VSj5KOr0jrg0+bG/aV2JGsvn/5AbRP
sha512-z5iEy8ijJzKBzzzBZSdt1DFqwsHLRAV2fvGk4N7P0VikiXtRla258zX7YiSvSIwYskOnnPySzFLaDAdXWUkgNQ=="
src="scripts/main.js" type="module" ></script>
<noscript>
<p>Sorry, but the Bill Paxton Soundboard requires JavaScript to work.</p>
<p>Please enable it or try using a different browser.</p>
</noscript>
<p class="center">
<a class="downloadButton" href="bps.zip">Download ZIP</a>
<a class="downloadButton" href="bps.tar.gz">Download TAR</a>
</p>
<hr />
<h2>What is This?</h2>
<p>
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.
</p>
<p>
If you want to know a bit more about how (and why) it was made, be sure to check
out <a href="/journal/2024/02-10-webtoys-bps">the blog post I wrote about it</a> when it was released
on 2024-02-10.
</p>
<h3>The Rules</h3>
<p>
This particular challenge had 3 simple rules (copied verbatim below):
</p>
<ol>
<li><em>Play some kind of music / sound</em></li>
<li><em>Be viewable</em></li>
<li><em>Don't work over 4hrs!!!!</em></li>
</ol>
<p>
The BPS satisfies all 3 of the rules: it's an HTML5 / CSS / JS application that creates
a series of virtual <code>audio</code> 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 <code>img</code> tag, surrounded by a <code>figure</code> tag, and
augmented by the text of a <code>figcaption</code> tag) and attaches a <code>play()</code>
function to the click handlers for those <code>figure</code>s. Plus, I wrote it all in under 4 hours:
</p>
<ul>
<li>
One hour thinking through the concept and writing the rough draft
<a href="scripts/player.js"><code>player.js</code></a> and
<a href="scripts/main.js"><code>main.js</code></a> modules;
</li>
<li>
One hour to turn draft into an MVP, addressing
<a href="styles/styles.css">layout</a> and audio issues;
</li>
<li>
One hour to add images and expand the audio selection; and
</li>
<li>
One final hour to add mobile support and some light documentation.
</li>
</ul>
<h3>How to Install Locally</h3>
<ol>
<li>
Download either the <a href="bps.zip">ZIP'd</a>
or <a href="bps.tar.gz">TAR-balled</a> version.
</li>
<li>
Decompress it:
<ul>
<li>For the ZIP: <code>unzip bps.zip</code></li>
<li>For the TAR-ball: <code>tar -xvzf bps.tar.gz</code></li>
</ul>
</li>
<li>
Change to the new directory: <code>cd bps</code>
</li>
<li>
Start an HTTP server: <code>npx http-server</code>
</li>
<li>
Point your web browser to
<a href="http://127.0.0.1:8080/">http://127.0.0.1:8080/</a>.
</li>
</ol>
<h3>Copyright</h3>
<ul>
<li><em>True Lies</em> is copyright 1994 20th Century Fox.</li>
<li><em>Aliens</em> is copyright 1986 Twentieth Century Fox.</li>
<li><em>Weird Science</em> is copyright 1985 Universal Pictures (I think).</li>
</ul>
<p>
The images and sound clips used for the Bill Paxton Soundboard are copyright their
respective owners, and used under the
<a href="https://fairuse.stanford.edu/overview/fair-use/">fair use provision</a>
of the <a href="https://constitution.congress.gov/constitution/">Constitution of the United
States of America</a>.
</p>
<p>
All other content on this page (including scripts and text content) is released under a
<a href="/licenses/cc0/">Creative Commons CC0 1.0 Universal</a>
license.
</p>
<p>Share and enjoy!</p>
<p class="center">
<a href="/web.html">More of Eric&#39;s Web Stuff</a>
</p>
</body>
</html>

View File

@@ -0,0 +1,138 @@
// @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0
/****************************************************************************
* BPS (Bill Paxton Soundboard)
*
* Copyright 2024 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 { 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
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: "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}`);
});
// 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 */
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 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);
});
return { play, build };
})();
// @license-end

View File

@@ -0,0 +1,37 @@
// @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0
/****************************************************************************
* BPS (Bill Paxton Soundboard)
*
* Copyright 2024 Eric Woodward
* Source released under CC0 Public Domain License v1.0
* https://www.itsericwoodward.com/licenses/cc0/
* http://creativecommons.org/publicdomain/zero/1.0/
****************************************************************************/
// adapted from https://cheatcode.co/tutorials/how-to-build-a-soundboard-with-javascript
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();
}
};
export { load, play };
// @license-end

View File

@@ -0,0 +1 @@
export default "/webtoys/bps";

Binary file not shown.

View File

@@ -0,0 +1,111 @@
/* @license magnet:?xt=urn:btih:90dc5c0be029de84e523b9b3922520e79e0e6f08&dn=cc0.txt CC0-1.0 */
/****************************************************************************
* BPS (Bill Paxton Soundboard)
*
* Copyright 2024 Eric Woodward
* Source released under CC0 Public Domain License v1.0
* https://www.itsericwoodward.com/licenses/cc0/
* http://creativecommons.org/publicdomain/zero/1.0/
****************************************************************************/
html {
color: #DDE6ED;
background-color: #161f2b;
font-size: 20px;
margin: .5rem;
}
a {
color: #abc1d3;
}
a:visited {
color: #799cb9;
}
code {
background-color: #444b55;
}
p {
text-align: justify;
}
.center { text-align: center; }
.circleWrapper {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
}
.downloadButton {
border: 1px dashed;
border-radius: .5rem;
display: inline-block;
font-size: .75rem;
margin: 1rem;
max-width: 8rem;
padding: 1rem;
text-align: center;
text-decoration: none;
}
.figureButton {
cursor: pointer;
margin: 0.5rem;
max-width: 5.5rem;
}
.figureButton:hover {
filter: brightness(1.5);
}
.figureButton figcaption {
text-align: center;
}
.figureButton img { max-width: 100% }
/****************************************************************************
* Media Queries
****************************************************************************/
@media all and (min-width: 950px) {
/* circular layout derived from https://stackoverflow.com/a/12817454 */
.circleWrapper {
--d: 6.5em; /* image size */
--rel: .75; /* how much extra space we want between images, 1 = one image size */
--r: calc(.5*(1 + var(--rel))*var(--d)/var(--tan)); /* circle radius */
--s: calc(2*var(--r) + var(--d)); /* container size */
height: var(--s);
margin: 1rem auto;
position: relative;
width: var(--s);
}
.figureButton {
--az: calc(var(--i)*1turn/var(--m));
height: var(--d);
left: 50%;
margin: calc(-.5*var(--d));
position: absolute;
top: 50%;
transform:
rotate(var(--az))
translate(var(--r))
rotate(calc(-1*var(--az)));
width: var(--d);
}
.downloadButton {
font-size: 1rem;
margin: 4rem 1rem 1rem;
}
}
/* @license-end */

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Some files were not shown because too many files have changed in this diff Show More