Compare commits
22 Commits
eea5ec00cf
...
main
Author | SHA1 | Date | |
---|---|---|---|
efc6bca786 | |||
440cd959fa | |||
a73cc00c33 | |||
d22025691e | |||
4914e72a3e | |||
c4d3da4dc2 | |||
751e201f18 | |||
b2d2fb6d34 | |||
5f14032aee | |||
f2ed98bc5d | |||
292f6ee3d0 | |||
afcf37a284 | |||
c92d8c3897 | |||
ec21f676a6 | |||
1bec5c0259 | |||
b09d183a1c | |||
07dfed2e1c | |||
d4247e09ff | |||
bf448aa47f | |||
5af4cb886f | |||
b0751f0cb4 | |||
c49be4c321 |
29
.eslintrc.cjs
Normal 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
@@ -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
@@ -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.
|
|
||||||
|
@@ -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
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "es6"
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
370
lib/build.js
@@ -1,370 +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 })).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);
|
|
||||||
});
|
|
||||||
};
|
|
@@ -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,
|
|
||||||
},
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
}, {}),
|
|
||||||
};
|
|
||||||
};
|
|
26
lib/serve.js
@@ -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}`);
|
|
||||||
});
|
|
||||||
};
|
|
57
lib/utils.js
@@ -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;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
};
|
|
||||||
})();
|
|
58
lib/watch.js
@@ -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}`);
|
|
||||||
});
|
|
||||||
};
|
|
2964
package-lock.json
generated
35
package.json
@@ -1,34 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "iew-site-builder",
|
"name": "iew-site",
|
||||||
"version": "0.9.0",
|
"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": "^13.0.2",
|
|
||||||
"yargs": "^17.3.1"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
BIN
src/assets/_root/chim/chim.jsdos
Normal 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 ').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);
|
|
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
BIN
src/assets/_root/webtoys/bbe/beasties/blinker_shadow_red.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/bobbler_shadow_yellow.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/chomper_shadow_blue.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/creeper_shadow_red.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/diver_shadow_blue.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/gawker_shadow_yellow.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/grabber_shadow_red.png
Normal file
After Width: | Height: | Size: 7.8 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/puffer_shadow_green.png
Normal file
After Width: | Height: | Size: 7.9 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/reacher_shadow_yellow.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/scanner_shadow_red.png
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/seer_shadow_green.png
Normal file
After Width: | Height: | Size: 4.3 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/shuffler_shadow_yellow.png
Normal file
After Width: | Height: | Size: 17 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/snapper_shadow_blue.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/spooker_shadow_green.png
Normal file
After Width: | Height: | Size: 7.4 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/thinker_shadow_green.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
src/assets/_root/webtoys/bbe/beasties/walker_shadow_blue.png
Normal file
After Width: | Height: | Size: 11 KiB |
74
src/assets/_root/webtoys/bbe/index.html
Normal 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>← OR →</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>
|
656
src/assets/_root/webtoys/bbe/scripts/bbe.js
Normal 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 };
|
45
src/assets/_root/webtoys/bbe/scripts/lightning.js
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
32
src/assets/_root/webtoys/bbe/scripts/main.js
Normal 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
|
82
src/assets/_root/webtoys/bbe/styles.css
Normal 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;
|
||||||
|
}
|
BIN
src/assets/_root/webtoys/bps/bps.tar.gz
Normal file
BIN
src/assets/_root/webtoys/bps/bps.zip
Normal file
BIN
src/assets/_root/webtoys/bps/images/aliens_17_days.png
Normal file
After Width: | Height: | Size: 82 KiB |
BIN
src/assets/_root/webtoys/bps/images/aliens_current_affairs.png
Normal file
After Width: | Height: | Size: 98 KiB |
BIN
src/assets/_root/webtoys/bps/images/aliens_express_elevator.png
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
src/assets/_root/webtoys/bps/images/aliens_game_over.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
src/assets/_root/webtoys/bps/images/random.png
Normal file
After Width: | Height: | Size: 78 KiB |
BIN
src/assets/_root/webtoys/bps/images/true_lies_navel_lint.png
Normal file
After Width: | Height: | Size: 88 KiB |
BIN
src/assets/_root/webtoys/bps/images/true_lies_pathetic.png
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
src/assets/_root/webtoys/bps/images/weird_science_booze.png
Normal file
After Width: | Height: | Size: 114 KiB |
BIN
src/assets/_root/webtoys/bps/images/weird_science_dead_meat.png
Normal file
After Width: | Height: | Size: 70 KiB |
BIN
src/assets/_root/webtoys/bps/images/weird_science_he_pukes.png
Normal file
After Width: | Height: | Size: 90 KiB |
BIN
src/assets/_root/webtoys/bps/images/weird_science_sandwich.png
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
src/assets/_root/webtoys/bps/images/weird_science_stewwed.png
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
src/assets/_root/webtoys/bps/images/weird_science_turd_brain.png
Normal file
After Width: | Height: | Size: 86 KiB |
150
src/assets/_root/webtoys/bps/index.html
Normal 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's Web Stuff</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
138
src/assets/_root/webtoys/bps/scripts/main.js
Normal 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
|
37
src/assets/_root/webtoys/bps/scripts/player.js
Normal 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
|
1
src/assets/_root/webtoys/bps/scripts/rootdir.js
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export default "/webtoys/bps";
|
BIN
src/assets/_root/webtoys/bps/sounds/aliens_17_days.wav
Normal file
BIN
src/assets/_root/webtoys/bps/sounds/aliens_current_affairs.wav
Normal file
BIN
src/assets/_root/webtoys/bps/sounds/aliens_express_elevator.wav
Normal file
BIN
src/assets/_root/webtoys/bps/sounds/aliens_game_over.wav
Normal file
BIN
src/assets/_root/webtoys/bps/sounds/true_lies_navel_lint.wav
Normal file
BIN
src/assets/_root/webtoys/bps/sounds/true_lies_pathetic.mp3
Normal file
BIN
src/assets/_root/webtoys/bps/sounds/weird_science_booze.mp3
Normal file
BIN
src/assets/_root/webtoys/bps/sounds/weird_science_dead_meat.mp3
Normal file
BIN
src/assets/_root/webtoys/bps/sounds/weird_science_he_pukes.mp3
Normal file
BIN
src/assets/_root/webtoys/bps/sounds/weird_science_sandwich.mp3
Normal file
BIN
src/assets/_root/webtoys/bps/sounds/weird_science_stewwed.mp3
Normal file
BIN
src/assets/_root/webtoys/bps/sounds/weird_science_turd_brain.mp3
Normal file
111
src/assets/_root/webtoys/bps/styles/styles.css
Normal 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 */
|
4
src/assets/_root/webtoys/maze/assets/index-Bw4OooWg.js
Normal file
1
src/assets/_root/webtoys/maze/assets/index-f4dP9NA-.css
Normal file
BIN
src/assets/_root/webtoys/maze/favicon.ico
Normal file
After Width: | Height: | Size: 1.4 KiB |