Working on the lit evolution.

This commit is contained in:
Elf M. Sternberg 2024-11-16 20:07:16 -08:00
parent 60f398b143
commit 9c97349ba7
27 changed files with 6660 additions and 1286 deletions

128
.gitignore vendored
View File

@ -1,13 +1,115 @@
*.pyc
*.pyo
*#
.#*
*~
js/magnets.js
js/sat.js
js/wordlist.js
node_modules
server/magnet_server.js
index.html
style.css
private/
# Created by https://www.gitignore.io/api/node
# Edit at https://www.gitignore.io/?templates=node
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
dist
# Uncomment the public line if your project uses Gatsby
# https://nextjs.org/blog/next-9-1#public-directory-support
# https://create-react-app.dev/docs/using-the-public-folder/#docsNav
# public
# Storybook build outputs
.out
.storybook-out
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Temporary folders
tmp/
temp/
# End of https://www.gitignore.io/api/node
api/**
storybook-static/
# Wireit's cache
.wireit
custom-elements.json

18
LICENSE.md Normal file
View File

@ -0,0 +1,18 @@
The MIT License (MIT)
Copyright (c) 2024 Elf M. Sternberg <elf.sternberg@gmail.com>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute,
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
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.

113
README.md
View File

@ -1,101 +1,20 @@
# Fridgemagnets - A Nifty HTML5 Toy With Some Twitterable Features!
This repository contains the boilerplate package which I use to start most of my smaller Typescript
projects. It is "primitive" in that it doesn't have hot module reloading or VSCode integration;
instead, it runs the build process using a watcher that enables fast iteration via the command line.
This boilerplate is focused on web components, primarily those build with [Lit](https://lit.dev).
## Demo
## Dependencies
Main website: [HTML5 Magnets!](http://html5magnets.elfsternberg.com)
Aside from the NodeJS dependencies, this script uses
[codespell](https://github.com/codespell-project/codespell), which is a Python-based spell checker
for comments and documentation.
Happy results: [@HTML5Magnets](https://twitter.com/#!/html5magnets)
## Main Idea
Fridgemagnets is a straightforward simulation of a relaxing
refrigerator poem tileset. It was inspired by
[TwitterMagnets](http://twittermagnets.com/), a Flash app written by the
brilliant graphic designers at
[PlusGood](http://www.plusgood.co.uk/). I have a bit of Flash envy,
since I'm not an Adobe developer, and the TwitterMagnets application
bugged me. It didn't resize, it didn't do mobile very well, and
nobody has a space reserved on their fridge for the poem: a "poem" is
just a meaningful arrangement of words deliberately placed in close
proximity that seems to convey meaning.
Fridgemagnets has a lot of new and fun technologies: it uses the audio
API, it involves all manner of write-only-DOM tricks to make resizing
work well, and it's my first major piece of express.js software. (I
originally thought of using Zappa, but decided against it; dispatch is
not the biggest thing Node has to deal with, and express by itself
works just fine in Coffee.)
I can now add the Twitter API, the HTML5 Audio API, and some basic
game mechanics ([Separate Axis
Theorem](http://www.metanetsoftware.com/technique/tutorialA.html) for
collision management, anyone?) to my resume.
This is known to work in later versions of Chrome, Firefox, and IE8+
under Windows XP. No promise is implied of it working on your version
of those, or any other browser. It's not (yet) phone-ready.
## Requirements
Node.js. Most of the subsidiary requirements can be found in the two
package.json files. For development purposes Coffeescript, LessCSS,
and HAML are in heavy use.
If you're running the server, you need MySQL. The schema for the
MySQL database can be found in the server folder.
A config file. There's an example in the server folder.
A twitter developer's account. Get one at dev.twitter.com.
If you're going to be using the test/deploy routine, inotify-tools and
python's "fabric" program are very useful.
If you're going to make this publicly available, I strongly recommend
you run this as its own user in a low-permissions container, behind
Nginx and a lot of smarts. Also, the Node.js program "forever" is
very useful in keeping the server up.
## Acknowledgements
[PlusGood](http://www.plusgood.co.uk/), for the inspiration.
[Emily Richards aka Snowflake](http://ccmixter.org/people/snowflake),
for her beautiful music.
The entire crew at [Nodejitsu](http://nodejitsu.com/), for all the
encouragement, even if I don't use their services.
## CREDITS
"Ethereal Space" is copyright (c) 2011 Snowflake, licensed under a
Creative Commons 3.0 Attribution-Required license.
jQuery, jQuery UI and associated assets, Buzz.js, and jQuery CSS
Transform are copyright their respective owners, and available under
a permissive MIT license.
## LICENSE AND COPYRIGHT NOTICE: NO WARRANTY GRANTED OR IMPLIED
Copyright (c) 2012 Elf M. Sternberg
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:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
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.
- Elf M. Sternberg <elf@pendorwright.com>
## Decisions:
- TSC is still the best analyzer of Typescript's types.
- Scripting ESBuild gives you enormous power.
- Many of the commands have the `${NODE_RUNNER}` prefix. If you set `export NODE_RUNNER=bun`, you
can get a huge speedup in building and linting.
- Knip makes sure you're not importing anything you're not using.
- As this is a Lit-focused project, both Lit-Analyzer and WC-Analyzer are included
- xo

133
build.mjs Normal file
View File

@ -0,0 +1,133 @@
import { execFileSync } from "child_process";
import * as chokidar from "chokidar";
import esbuild from "esbuild";
import fs from "fs";
import { globSync } from "glob";
import path from "path";
import { cwd } from "process";
import process from "process";
import { fileURLToPath } from "url";
// Hack replaces what was lost when using Bun / later Node versions
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const isProdBuild = process.env.NODE_ENV === "production";
const definitions = {
"process.env.NODE_ENV": JSON.stringify(isProdBuild ? "production" : "development"),
"process.env.CWD": JSON.stringify(cwd()),
};
// If you have assets in your src folder that won't be built/bundled, put them into "otherFiles" to
// copy them. All this is a replacement for rollup-copy-plugin, which I used to use.
const otherFiles = [
["./src/*.css", "."],
["./src/*.png", "."]
];
const isFile = (filePath) => fs.statSync(filePath).isFile();
function nameCopyTarget(src, dest, strip) {
const target = path.join(dest, strip ? src.replace(strip, "") : path.parse(src).base);
return [src, target];
}
function copyOthers() {
for (const [source, rawdest, strip] of otherFiles) {
const matchedPaths = globSync(source);
const dest = path.join("dist", rawdest);
const copyTargets = matchedPaths.map((path) => nameCopyTarget(path, dest, strip));
for (const [src, dest] of copyTargets) {
if (isFile(src)) {
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.copyFileSync(src, dest);
}
}
}
}
// This starts the definitions used for esbuild: Targets, arguments, the function for running a
// build, and the options for building: building or watching. If you're building more than one app,
// order them by the approximately largest project to smallest to build even faster.
const apps = [
["index.ts", "."]
];
const baseArgs = {
bundle: true,
write: true,
sourcemap: true,
minify: isProdBuild,
splitting: true,
treeShaking: true,
external: ["*.woff", "*.woff2"],
tsconfig: "./tsconfig.json",
loader: { ".css": "text", ".md": "text" },
define: definitions,
format: "esm",
};
async function buildOneSource(source, dest) {
const DIST = path.join(__dirname, "./dist", dest);
console.log(`[${new Date(Date.now()).toISOString()}] Starting build for target ${source}`);
try {
const start = Date.now();
copyOthers();
await esbuild.build({
...baseArgs,
entryPoints: [`./src/${source}`],
entryNames: '[dir]/[name]',
outdir: DIST,
});
const end = Date.now();
console.log(
`[${new Date(end).toISOString()}] Finished build for target ${source} in ${
Date.now() - start
}ms`,
);
} catch (exc) {
console.error(`[${new Date(Date.now()).toISOString()}] Failed to build ${source}: ${exc}`);
}
}
async function buildAll(apps) {
await Promise.allSettled(apps.map(([source, dest]) => buildOneSource(source, dest)));
}
let timeoutId = null;
function debouncedBuild() {
if (timeoutId !== null) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
console.log("\x1bc");
buildAll(apps);
}, 250);
}
if (process.argv.length > 2 && (process.argv[2] === "-h" || process.argv[2] === "--help")) {
console.log(`Build:
options:
-w, --watch: Build all ${apps.length} applications
-h, --help: This help message
`);
process.exit(0);
}
if (process.argv.length > 2 && (process.argv[2] === "-w" || process.argv[2] === "--watch")) {
console.log("Watching ./src for changes");
chokidar.watch("./src").on("all", (event, path) => {
if (!["add", "change", "unlink"].includes(event)) {
return;
}
if (!/(\.css|\.ts|\.js)$/.test(path)) {
return;
}
debouncedBuild();
});
} else {
await buildAll(apps);
}

84
eslint.config.mjs Normal file
View File

@ -0,0 +1,84 @@
import eslint from "@eslint/js";
import tsparser from "@typescript-eslint/parser";
import litconf from "eslint-plugin-lit";
import wcconf from "eslint-plugin-wc";
import globals from "globals";
import tseslint from "typescript-eslint";
export default [
// You would not believe how much this change has frustrated users: ["if an ignores key is used
// without any other keys in the configuration object, then the patterns act as global
// ignores"](https://eslint.org/docs/latest/use/configure/ignore)
{
ignores: [
"dist/",
// don't lint the cache
".wireit/",
// let packages have their own configurations
"packages/",
// don't ever lint node_modules
"node_modules/",
".storybook/*",
// don't lint build output (make sure it's set to your correct build folder name)
// don't lint nyc coverage output
"coverage/",
"src/locale-codes.ts",
"storybook-static/",
"src/locales/",
],
},
eslint.configs.recommended,
wcconf.configs["flat/recommended"],
litconf.configs["flat/recommended"],
...tseslint.configs.recommended,
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
},
files: ["src/**"],
rules: {
"no-unused-vars": "off",
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
globals: {
...globals.nodeBuiltin,
},
},
files: ["scripts/*.mjs", "*.ts", "*.mjs"],
rules: {
"no-unused-vars": "off",
// We WANT our scripts to output to the console!
"no-console": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
];

15
index.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en-GB">
<head>
<meta charset="utf-8" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400..900&display=swap" rel="stylesheet" />
<script src="./dist/index.js" type="text/javascript"></script>
<link href="./dist/styles.css" rel="stylesheet" />
</head>
<body>
<fridge-magnets id="fridgemagnets">
</fridge-magnets>
</body>
</html>

5088
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

168
package.json Normal file
View File

@ -0,0 +1,168 @@
{
"name": "elf-boilerplate",
"version": "0.0.0",
"dependencies": {
"@lit/localize": "^0.12.2",
"@neodrag/vanilla": "^2.0.5",
"lit": "^3.2.0"
},
"devDependencies": {
"@types/eslint__js": "^8.42.3",
"@types/node": "^22.5.5",
"@typescript-eslint/eslint-plugin": "^8.6.0",
"@typescript-eslint/parser": "^8.6.0",
"chokidar": "^4.0.1",
"esbuild": "^0.24.0",
"eslint": "^9.11.0",
"eslint-plugin-lit": "^1.15.0",
"eslint-plugin-wc": "^2.1.1",
"glob": "^10.4.5",
"http-server": "^14.1.1",
"knip": "^5.30.4",
"lit-analyzer": "^2.0.3",
"lockfile-lint": "^4.14.0",
"prettier": "^3.3.3",
"rimraf": "^5.0.10",
"syncpack": "^13.0.0",
"typescript": "^5.6.2",
"typescript-eslint": "^8.6.0",
"wireit": "^0.14.9"
},
"engines": {
"node": ">=20"
},
"license": "MIT",
"optionalDependencies": {
"@esbuild/darwin-arm64": "^0.23.0",
"@esbuild/linux-amd64": "^0.18.11",
"@esbuild/linux-arm64": "^0.23.0"
},
"scripts": {
"build": "wireit",
"clean": "wireit",
"demo": "wireit",
"fix": "wireit",
"format": "wireit",
"lint": "wireit",
"realclean": "wireit",
"watch": "wireit"
},
"type": "module",
"wireit": {
"build": {
"command": "${NODE_RUNNER} build.mjs",
"files": [
"./src/**/*.{css,ts,js}"
],
"output": [
"dist/**"
],
"env": {
"NODE_RUNNER": {
"external": true,
"default": "node"
}
}
},
"clean": {
"command": "rimraf ./dist"
},
"demo": {
"command": "${NODE_RUNNER} ./node_modules/.bin/http-server . --port ${HTTP_DEMO_PORT}",
"server": true,
"env": {
"NODE_RUNNER": {
"external": true,
"default": "node"
},
"HTTP_DEMO_PORT": {
"external": true,
"default": "8000"
}
}
},
"fix": {
"command": "prettier --write .",
"dependencies": [
"fix:eslint",
"fix:package"
]
},
"format": {
"command": "prettier --write ."
},
"fix:eslint": {
"command": "${NODE_RUNNER ./scripts/eslint.mjs --fix",
"env": {
"NODE_RUNNER": {
"external": true,
"default": "node"
}
}
},
"lint:components": {
"command": "lit-analyzer src"
},
"lint:eslint": {
"command": "${NODE_RUNNER} ./scripts/eslint.mjs --precommit",
"env": {
"NODE_RUNNER": {
"external": true,
"default": "node"
}
}
},
"lint:lockfile": {
"_comment": "Ensure every entry has a resolved hash",
"shell": true,
"command": "[ -z \"$(jq -r '.packages | to_entries[] | select((.key | startswith(\"node_modules\")) and (.value | has(\"resolved\") | not)) | .key' < package-lock.json)\" ]",
"dependencies": [
"lint:lockfile:base"
]
},
"lint:lockfile:base": {
"command": "lockfile-lint --path ./package-lock.json --allowed-hosts npm --validate-https --validate-integrity"
},
"lint:spelling": {
"command": "${NODE_RUNNER} scripts/check-spelling.mjs",
"env": {
"NODE_RUNNER": {
"external": true,
"default": "node"
}
}
},
"prettier": {
"command": "prettier ."
},
"lint": {
"dependencies": [
"lint:types",
"lint:components",
"lint:lit",
"lint:lockfile",
"lint:eslint",
"prettier"
]
},
"lint:imports": {
"command": "knip --config scripts/knip.config.ts"
},
"lint:lit": {
"command": "lit-analyzer src"
},
"lint:types": {
"command": "tsc --noEmit -p ."
},
"watch": {
"command": "${NODE_RUNNER} build.mjs --watch",
"server": true,
"env": {
"NODE_RUNNER": {
"external": true,
"default": "node"
}
}
}
}
}

56
scripts/eslint.mjs Normal file
View File

@ -0,0 +1,56 @@
import { execFileSync } from "child_process";
import { ESLint } from "eslint";
import fs from "fs";
import path from "path";
import process from "process";
import { fileURLToPath } from "url";
function changedFiles() {
const gitStatus = execFileSync("git", ["diff", "--name-only", "HEAD"], { encoding: "utf8" });
const gitUntracked = execFileSync("git", ["ls-files", "--others", "--exclude-standard"], {
encoding: "utf8",
});
const changed = gitStatus
.split("\n")
.filter((line) => line.trim().substring(0, 4) === "web/")
.filter((line) => /\.(m|c)?(t|j)s$/.test(line))
.map((line) => line.substring(4))
.filter((line) => fs.existsSync(line));
const untracked = gitUntracked
.split("\n")
.filter((line) => /\.(m|c)?(t|j)s$/.test(line))
.filter((line) => fs.existsSync(line));
const sourceFiles = [...changed, ...untracked].filter((line) => /^src\//.test(line));
const scriptFiles = [...changed, ...untracked].filter(
(line) => /^scripts\//.test(line) || !/^src\//.test(line),
);
return [...sourceFiles, ...scriptFiles];
}
const __dirname = fileURLToPath(new URL(".", import.meta.url));
const projectRoot = path.join(__dirname, "..");
process.chdir(projectRoot);
const hasFlag = (flags) => process.argv.length > 1 && flags.includes(process.argv[2]);
const [configFile, files] = hasFlag(["-n", "--nightmare"])
? [path.join(__dirname, "eslint.nightmare.mjs"), changedFiles()]
: hasFlag(["-p", "--precommit"])
? [path.join(__dirname, "eslint.precommit.mjs"), changedFiles()]
: [path.join(projectRoot, "eslint.config.mjs"), ["."]];
const eslint = new ESLint({
overrideConfigFile: configFile,
warnIgnored: false,
});
const results = await eslint.lintFiles(files);
const formatter = await eslint.loadFormatter("stylish");
const resultText = formatter.format(results);
const errors = results.reduce((acc, result) => acc + result.errorCount, 0);
console.log(resultText);
process.exit(errors > 1 ? 1 : 0);

View File

@ -0,0 +1,199 @@
import eslint from "@eslint/js";
import tsparser from "@typescript-eslint/parser";
import litconf from "eslint-plugin-lit";
import wcconf from "eslint-plugin-wc";
import globals from "globals";
import tseslint from "typescript-eslint";
const MAX_DEPTH = 4;
const MAX_NESTED_CALLBACKS = 4;
const MAX_PARAMS = 5;
const rules = {
"accessor-pairs": "error",
"array-callback-return": "error",
"block-scoped-var": "error",
"consistent-return": "error",
"consistent-this": ["error", "that"],
"curly": ["error", "all"],
"dot-notation": [
"error",
{
allowKeywords: true,
},
],
"eqeqeq": "error",
"func-names": "error",
"guard-for-in": "error",
"max-depth": ["error", MAX_DEPTH],
"max-nested-callbacks": ["error", MAX_NESTED_CALLBACKS],
"max-params": ["error", MAX_PARAMS],
"new-cap": "error",
"no-alert": "error",
"no-array-constructor": "error",
"no-bitwise": "error",
"no-caller": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-div-regex": "error",
"no-dupe-args": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-else-return": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-function": "error",
"no-labels": "error",
"no-eq-null": "error",
"no-eval": "error",
"no-ex-assign": "error",
"no-extend-native": "error",
"no-extra-bind": "error",
"no-extra-boolean-cast": "error",
"no-extra-label": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-implied-eval": "error",
"no-implicit-coercion": "error",
"no-implicit-globals": "error",
"no-inner-declarations": ["error", "functions"],
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-iterator": "error",
"no-invalid-this": "error",
"no-label-var": "error",
"no-lone-blocks": "error",
"no-lonely-if": "error",
"no-loop-func": "error",
"no-magic-numbers": ["error", { ignore: [0, 1, -1] }],
"no-multi-str": "error",
"no-negated-condition": "error",
"no-nested-ternary": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-wrappers": "error",
"no-obj-calls": "error",
"no-octal": "error",
"no-octal-escape": "error",
"no-param-reassign": "error",
"no-proto": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-restricted-syntax": ["error", "WithStatement"],
"no-script-url": "error",
"no-self-assign": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef": "error",
"no-undef-init": "error",
"no-unexpected-multiline": "error",
"no-useless-constructor": "error",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": "error",
"no-unreachable": "error",
"no-unused-expressions": "error",
"no-unused-labels": "error",
"no-use-before-define": "error",
"no-useless-call": "error",
"no-dupe-class-members": "error",
"no-var": "error",
"no-void": "error",
"no-with": "error",
"prefer-arrow-callback": "error",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"prefer-template": "error",
"radix": "error",
"require-yield": "error",
"strict": ["error", "global"],
"use-isnan": "error",
"valid-typeof": "error",
"vars-on-top": "error",
"yoda": ["error", "never"],
"no-unused-vars": "off",
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
};
export default [
// You would not believe how much this change has frustrated users: ["if an ignores key is used
// without any other keys in the configuration object, then the patterns act as global
// ignores"](https://eslint.org/docs/latest/use/configure/ignore)
{
ignores: [
"dist/",
".wireit/",
// don't ever lint node_modules
"node_modules/",
],
},
eslint.configs.recommended,
wcconf.configs["flat/recommended"],
litconf.configs["flat/recommended"],
...tseslint.configs.recommended,
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
globals: {
...globals.browser,
},
},
files: ["src/**"],
rules,
},
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
globals: {
...globals.nodeBuiltin,
},
},
files: ["scripts/*.mjs", "*.ts", "*.mjs"],
rules,
},
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
globals: {
...globals.nodeBuiltin,
...globals.jest,
},
},
files: ["src/**/*.test.ts"],
rules,
},
];

View File

@ -0,0 +1,73 @@
import eslint from "@eslint/js";
import tsparser from "@typescript-eslint/parser";
import litconf from "eslint-plugin-lit";
import wcconf from "eslint-plugin-wc";
import globals from "globals";
import tseslint from "typescript-eslint";
export default [
// You would not believe how much this change has frustrated users: ["if an ignores key is used
// without any other keys in the configuration object, then the patterns act as global
// ignores"](https://eslint.org/docs/latest/use/configure/ignore)
{
ignores: [
"dist/",
".wireit/",
// don't ever lint node_modules
"node_modules/",
],
},
eslint.configs.recommended,
wcconf.configs["flat/recommended"],
litconf.configs["flat/recommended"],
...tseslint.configs.recommended,
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
},
files: ["src/**"],
rules: {
"no-unused-vars": "off",
"no-console": ["error", { allow: ["debug", "warn", "error"] }],
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
{
languageOptions: {
parser: tsparser,
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
globals: {
...globals.nodeBuiltin,
},
},
files: ["scripts/*.mjs", "*.ts", "*.mjs"],
rules: {
"no-unused-vars": "off",
"no-console": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
];

29
scripts/knip.config.ts Normal file
View File

@ -0,0 +1,29 @@
import { type KnipConfig } from "knip";
const config: KnipConfig = {
"entry": [
"./src/index.ts",
],
"project": ["src/**/*.ts", "src/**/*.js", "./scripts/*.mjs"],
// "ignore": ["src/**/*.test.ts", "src/**/*.stories.ts"],
// Prevent Knip from complaining about web components, which export their classes but also
// export their registration, and we don't always use both.
"ignoreExportsUsedInFile": true,
"typescript": {
config: ["tsconfig.json"],
},
"wireit": {
config: ["package.json"],
},
"eslint": {
entry: [
"eslint.config.mjs",
"scripts/eslint.precommit.mjs",
"scripts/eslint.nightmare.mjs",
"scripts/eslint.mjs",
],
config: ["package.json"],
}
};
export default config;

48
src/fridge-magnets.ts Normal file
View File

@ -0,0 +1,48 @@
import { LitElement, html, css } from "lit";
import { customElement } from "lit/decorators/custom-element.js";
import "./fridge-tile.js";
@customElement("fridge-magnets")
export class FridgeMagnets extends LitElement {
static get styles() {
return css`
:host {
display: block;
}
#fridgemagnets {
width: 100%;
height: 100%;
display: grid;
grid-template-rows: 100fr 18ex;
}
#fridge {
overflow: hidden;
position: relative;
width: 100%;
background: url("./dist/pingbg.png") repeat;
}
#footer {
background-color: #32cd32;
width: 100%;
height: 18ex;
}
`;
}
render() {
return html` <div id="fridgemagnets">
<div id="fridge">
<fridge-tile word="Magnificent"></fridge-tile>
</div>
<div id="footer">Footer</div>
</div>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"fridge-magnets": FridgeMagnets;
}
}

154
src/fridge-tile.ts Normal file
View File

@ -0,0 +1,154 @@
import { LitElement, html, css } from "lit";
import { customElement } from "lit/decorators/custom-element.js";
import { property } from "lit/decorators/property.js";
import { styleMap } from "lit/directives/style-map.js";
import { LitDragEvent, LitDraggable } from "./lit-draggable.js";
export function bound(_target: unknown, key: string, descriptor: PropertyDescriptor): PropertyDescriptor {
if (typeof descriptor?.value !== "function") {
throw new Error("Only methods can be @bound.");
}
return {
configurable: true,
get() {
const method = descriptor.value.bind(this);
Object.defineProperty(this, key, { value: method, configurable: true, writable: true });
return method;
},
};
}
@customElement("fridge-tile")
export class FridgeTile extends LitElement {
@property({ type: String })
word = "";
dragHandle: LitDraggable;
static get styles() {
return css`
:host {
display: block;
position: absolute;
}
:host([data-dragging="idle"]) {
cursor: grab;
}
:host([data-dragging="dragging"]) {
cursor: grabbing;
}
.word {
font-family: Georgia, Palatino, "Palatino Linotype", Times, "Times New Roman", serif;
box-shadow: 0 0 0.375rem 0.125rem #aaa;
text-align: center;
user-select: none;
cursor: pointer;
color: #444;
font-size: 0.9375rem;
padding: 0.1875rem 0.25rem 0.25rem 0.25rem;
position: relative;
background: white;
z-index: 100;
}
.word.dragging {
font-size: 1.1875rem;
}
`;
}
constructor() {
super();
this.dragHandle = new LitDraggable(this);
this.onDragEnd = this.onDragEnd.bind(this);
this.addEventListener("lit-drag-end", this.onDragEnd);
}
onDragEnd(ev: LitDragEvent) {
this.style.setProperty("top", `${+ev.offsetY}px`);
this.style.setProperty("left", `${+ev.offsetX}px`);
}
render() {
const styles = styleMap({ width: `${this.word.length}ch` });
return html`<div part="word" style="${styles}" class="word">${this.word}</div>`;
}
}
//
// base_style:
// 'font-size': "15px"
//
// drag_style:
// 'font-size': "19px"
//
// visible: false
//
// # Initial tilt.
// rotation: (Math.random() * 30) - 15
//
// constructor: (@word, @board, @master) ->
// super()
// @el = $('<div class="word">' + @word.w + '</div>')
// @el.css @base_style
// @board.append(@el)
// @rotation = (Math.random() * 30) - 15
//
// @el.draggable
// helper: "original"
// refreshPositions: false
// revertDuration: 1
//
// start: (event) =>
// mod = (Math.random() * 16) - 8
// @rotation = if Math.abs(@rotation + mod) > 15 then @rotation - mod else @rotation + mod
// style = clone(@drag_style)
// style.rotate = @rotation
// @el.animate(style, 200, () => @new_width = @el.width())
// true
//
// stop: (event) =>
// # Drop the thing dead center, at least on the x-axis,
// # and animate its return to the new font size.
// mod = (Math.random() * 16) - 8
// @rotation = if Math.abs(@rotation + mod) > 15 then @rotation - mod else @rotation + mod
// style = clone(@base_style)
// style.rotate = @rotation
// style['left'] = parseInt(@el.position().left + (0.5 * (@new_width - @width())))
// @el.animate style, 200, 'easeOutQuad', () =>
// @reset_dims()
// explode_hearts(@board, @)
// @master.poemed(@)
// true
//
// fadeOut: -> $.Deferred((d) => @el.fadeOut('fast', (() => @unset_dims(); @visible = false; d.resolve()))).promise()
//
// # Shape for deteriming poemed collision
// fuzzyshape: -> shape @left() - WIDTH_FUZZ, @top() - HEIGHT_FUZZ, @width() + (2 * WIDTH_FUZZ), @height() + (2 * HEIGHT_FUZZ)
//
// get_new_pos: ->
// bh = => parseInt(Math.random() * (@board.height() - @height()) * 0.985)
// bw = => parseInt(Math.random() * (@board.width() - @width()) * 0.98)
// [top, left] = [bh(), bw()]
// [top, left] = [bh(), bw()] until @master.unoccupied(left, top, @width(), @height())
// [top, left]
//
// flyIn: ->
// fd = (mod) ->
// m = parseInt(40 * Math.random())
// if (Math.random() < 0.5) then mod + m else -1 * m
// @el.css
// left: fd(@board.width())
// top: fd(@board.height())
// dfd = $.Deferred()
// x = Math.random()
// [top, left] = @get_new_pos()
// @el.fadeIn().animate {top: top, left: left, rotate: @rotation}, 1500, 'easeOutQuint', () =>
// @visible = true
// dfd.resolve()
// dfd.promise()
//
//

View File

@ -1,36 +0,0 @@
!!! 5
%html{:xmlns => "http://www.w3.org/1999/xhtml"}
%head
%meta{:content => "text/html; charset=utf-8", "http-equiv" => "Content-Type"}/
%meta{"http-equiv" => "Acesss-Control-Allow-Origin", :content => "*"}/
%title Fridge Magnets in HTML5
%link{:href => "style.css", :rel => "stylesheet", :type => "text/css"}/
%link{:href => "ui-lightness/jquery-ui-1.8.18.custom.css", :rel => "stylesheet", :type => "text/css"}/
%body
#board
#results
#footer
#stripe
#muteunmute(data-state='on')
%img(src="unmute.png")
%button(id="shuffler") Shuffle
%p#f1
HTML5 implementation by <a href="http://elfsternberg.com">Elf M. Sternberg</a>. You can see all our poems
<a href="https://twitter.com/#!/html5magnets">@html5magnets</a> on Twitter.
#f2
%div
Comments and feedback to <a href="mailto:elf.sternberg@gmail.com">elf.sternberg@gmail.com</a> | inspired by <a href="http://twittermagnets.com/">twittermagents.com</a> and an allergic reaction to all things flash.</p>
%p The music is <em><span xmlns:dc="http://purl.org/dc/elements/1.1/" href="http://purl.org/dc/dcmitype/Sound" property="dc:title" rel="dc:type">Ethereal Space</span></em> by <a xmlns:cc="http://creativecommons.org/ns#" href="http://ccmixter.org/files/snowflake/33318" property="cc:attributionName" rel="cc:attributionURL">snowflake</a> and is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by/3.0/">Creative Commons Attribution (3.0)</a> license.
%div(style="clear:both")/
#message(style="display: none")
%p
%script{:src => "js/jquery-1.7.1.min.js", :type => "text/javascript"}
%script{:src => "js/jquery-ui-1.8.18.custom.min.js", :type => "text/javascript"}
%script{:src => "js/jquery-css-transform.js", :type => "text/javascript"}
%script{:src => "js/jquery-animate-css-rotate-scale.js", :type => "text/javascript"}
%script{:src => "js/buzz.js", :type => "text/javascript"}
%script{:src => "js/sat.js", :type => "text/javascript"}
%script{:src => "js/magnets.js", :type => "text/javascript"}

2
src/index.ts Normal file
View File

@ -0,0 +1,2 @@
import "./fridge-magnets.js";
import "./fridge-tile.js";

150
src/lit-draggable.ts Normal file
View File

@ -0,0 +1,150 @@
import { ReactiveControllerHost, ReactiveController } from "lit";
export type DragBoundaries = {
top: number;
left: number;
bottom: number;
right: number;
};
export type DragOptions = {
preventSelect: boolean;
};
export type DragAxes = "x" | "y" | "both" | "none";
export type DragBounds = HTMLElement | Partial<DragBoundaries> | "parent" | "body" | (string & Record<never, never>);
export interface LitDragEvent extends Event {
offsetX: number;
offsetY: number;
node: HTMLElement;
}
function makeLitDragEvent(name: string): LitDragEvent {
class _LitDragEvent extends Event implements LitDragEvent {
static readonly eventName = name;
offsetX: number = 0;
offsetY: number = 0;
node: HTMLElement;
// container: HTMLElement;
constructor(source: LitDraggable) {
super(_LitDragEvent.eventName, { bubbles: true, composed: true });
this.offsetX = source.translateX;
this.offsetY = source.translateY;
this.node = source.host;
// this.container = source.container;
}
}
return _LitDragEvent as unknown as LitDragEvent;
}
export const LitDragStart = makeLitDragEvent("lit-drag-start");
export const LitDragging = makeLitDragEvent("lit-dragging");
export const LitDragEnd: LitDragEvent = makeLitDragEvent("lit-drag-end");
const defaultOptions: DragOptions = {
preventSelect: true,
};
export class LitDraggable implements ReactiveController {
host: ReactiveControllerHost & HTMLElement;
options: DragOptions;
translateX = 0;
translateY = 0;
initialX = 0;
initialY = 0;
dragging = false;
currentSelect?: string;
pointers = new Set<number>();
constructor(host: ReactiveControllerHost & HTMLElement, options: DragOptions = defaultOptions) {
(this.host = host).addController(this);
this.options = options;
this.dragStart = this.dragStart.bind(this);
this.drag = this.drag.bind(this);
this.dragEnd = this.dragEnd.bind(this);
}
// TODO: Make this work with a handle, which will be a `ref` object.
hostConnected() {
console.log(this.host);
this.host.addEventListener("pointerdown", this.dragStart);
}
hostDisconnected() {
this.host.removeEventListener("pointerdown", this.dragStart);
}
dragStart(ev: PointerEvent) {
if (ev.button === 2) {
return;
}
document.addEventListener("pointerup", this.dragEnd);
document.addEventListener("pointermove", this.drag);
const event_target = ev.composedPath()[0] as HTMLElement;
if (!(event_target === this.host || this.host.shadowRoot?.contains(event_target))) {
console.log("HUH?");
return;
}
this.pointers.add(ev.pointerId);
this.dragging = true;
if (this.options.preventSelect) {
this.currentSelect = document.body.style.userSelect;
document.body.style.userSelect = "none";
}
this.initialX = ev.clientX;
this.initialY = ev.clientY;
this.host.dataset.litDrag = "true";
this.host.dispatchEvent(new LitDragStart(this));
}
drag(ev: PointerEvent) {
if (!this.dragging || !this.pointers.has(ev.pointerId)) {
return;
}
this.host.dataset.litDragging = "true";
ev.preventDefault();
this.translateX = ev.clientX - this.initialX;
this.translateY = ev.clientY - this.initialY;
this.host.dispatchEvent(new LitDragging(this));
this.host.style.setProperty("transform", `translate3d(${+this.translateX}px, ${+this.translateY}px, 0)`);
}
dragEnd(ev: PointerEvent) {
document.removeEventListener("pointerup", this.dragEnd);
document.removeEventListener("pointermove", this.drag);
if (!this.dragging || !this.pointers.has(ev.pointerId)) {
return;
}
this.pointers.delete(ev.pointerId);
this.dragging = false;
delete this.host.dataset.litDrag;
delete this.host.dataset.litDragging;
if (this.currentSelect) {
document.body.style.userSelect = this.currentSelect;
this.currentSelect = undefined;
}
this.initialX = this.translateX;
this.initialY = this.translateY;
this.host.dispatchEvent(new LitDragEnd(this));
this.host.style.removeProperty("transform");
}
}

View File

@ -1,530 +0,0 @@
SUFFIX = 1
PREFIX = 2
# Average number of words visible on any given iteration.
AVG_VISIBLE = 60
clone = (obj) ->
return obj if not obj? or typeof obj isnt 'object'
newInstance = new obj.constructor()
for key of obj
newInstance[key] = clone obj[key]
newInstance
HEIGHT_FUZZ = 8
WIDTH_FUZZ = 6
# A dimensioned object is one that appears on the board: it has an X
# and Y coordinate, a width and a height. From this, we can create a
# bounding box using the "shape" function. Dimensioned objects can be
# compared to other dimensioned objects to assert whether or not
# they're in collision. Some objects have bounding boxes that pull in
# or push out the borders abstractly, in order to provide for "fuzzy"
# collisions that correspond to drop shadows or similar visual effects.
shape = (x, y, w, h) -> [{x: x, y: y}, {x: x + w, y: y}, {x: x + w, y: y + h}, {x: x, y: y + h}]
class Dimensioned
_width: null
_height: null
_left_p: null
_top_p: null
_left: null
_top: null
_pos: null
constructor: (@el) ->
unset_dims: ->
@_left = @_top = @_width = @_height = @_pos = null
reset_dims: ->
@unset_dims()
[@left(), @top(), @width(), @height()]
positioned: -> return @_width? and @height?
visibleReposition: ->
@reposition()
@el.css {top: @top(), left: @left()}
@
reposition: ->
parent = @el.offsetParent()
[@_top, @_left] = [parseInt(@_top_p * parent.height()), parseInt(@_left_p * parent.width())]
@_pos = {left: @_left, top: @_top}
@
width: -> @_width = if @_width? then @_width else @el.outerWidth()
height: -> @_height = if @_height? then @_height else @el.outerHeight()
pos: -> @_pos = if @_pos? then @_pos else @el.position()
left: -> @_left = if @_left?
@_left
else
@_left = @pos().left
@_left_p = @_left / @el.offsetParent().width()
@_left
top: -> @_top = if @_top?
@_top
else
@_top = @pos().top
@_top_p = @_top / @el.offsetParent().height()
@_top
dims: -> [@width(), @height()]
shape: ->
shape @left(), @top(), @width(), @height()
# I can't decide if this is the right way to go, with a two-pass "set
# it all up, then make it all blow up," but it works quite well, all
# things considered. And after much consideration (like, one minute
# of realizing I never, ever used the features) it became obvious I
# didn't need Dimensioned.
class Heart
constructor: (@parent, @top, @left, symbol) ->
dv = '<div class="heart" style="display:none;top:' + parseInt(@top) + 'px;left:' + \
parseInt(@left) + 'px' + '">' + symbol + '</div>'
@el = $(dv)
@el.css {'font-size': 'larger'} if Math.random() > 0.6
@rot_dist = parseInt(90 * Math.random()) * (if Math.random() < 0.5 then 1 else -1)
[@dir, @dst, @dur] = [Math.random() * 2 * Math.PI, Math.random() * 110, Math.random() * 1200 + 700]
$(@parent).append(@el)
explode: ->
el = $(@el)
el.show().animate({opacity: 0.0, top: parseInt(@top + (Math.sin(@dir) * @dst)), left: parseInt(@left + (Math.cos(@dir) * @dst)), rotate: @rot_dist}, @dur, "easeOutCubic", (() -> el.remove()))
explode_hearts = (@board, @el) ->
randomsymbol = -> ['&#x0266A;','&#x02605;','&#x02736;'][parseInt(Math.random() * 3)]
symbol = if Math.random() < 0.3 then randomsymbol() else '&#x02665;'
parent = @board.el
[top, left, height, width] = [@el.top(), @el.left(), @el.height(), @el.width()]
hearts = for i in [0..(22 + (6 - Math.floor(Math.random() * 12)))]
new Heart(parent, top + (0.5 * height), left + (0.5 * width), symbol)
(h.explode() for h in hearts)
# The board is the principle object on which all other objects are
# dependent. I decided to make it a 'Dimensioned' because I'm going
# to be constantly querying its height and width.
class Board extends Dimensioned
append: (ob) -> @el.append(ob)
css: (width, height) ->
@el.css
width: width
height: height
@reset_dims()
class Footer extends Dimensioned
# A Tile is a word tile. It has a single word.
class Tile extends Dimensioned
base_style:
'font-size': "15px"
drag_style:
'font-size': "19px"
visible: false
# Initial tilt.
rotation: (Math.random() * 30) - 15
constructor: (@word, @board, @master) ->
@el = $('<div class="word">' + @word.w + '</div>')
@el.css @base_style
@board.append(@el)
@rotation = (Math.random() * 30) - 15
@el.draggable
helper: "original"
refreshPositions: false
revertDuration: 1
start: (event) =>
mod = (Math.random() * 16) - 8
@rotation = if Math.abs(@rotation + mod) > 15 then @rotation - mod else @rotation + mod
style = clone(@drag_style)
style.rotate = @rotation
@el.animate(style, 200, () => @new_width = @el.width())
true
stop: (event) =>
# Drop the thing dead center, at least on the x-axis,
# and animate its return to the new font size.
mod = (Math.random() * 16) - 8
@rotation = if Math.abs(@rotation + mod) > 15 then @rotation - mod else @rotation + mod
style = clone(@base_style)
style.rotate = @rotation
style['left'] = parseInt(@el.position().left + (0.5 * (@new_width - @width())))
@el.animate style, 200, 'easeOutQuad', () =>
@reset_dims()
explode_hearts(@board, @)
@master.poemed(@)
true
fadeOut: -> $.Deferred((d) => @el.fadeOut('fast', (() => @unset_dims(); @visible = false; d.resolve()))).promise()
# Shape for deteriming poemed collision
fuzzyshape: -> shape @left() - WIDTH_FUZZ, @top() - HEIGHT_FUZZ, @width() + (2 * WIDTH_FUZZ), @height() + (2 * HEIGHT_FUZZ)
get_new_pos: ->
bh = => parseInt(Math.random() * (@board.height() - @height()) * 0.985)
bw = => parseInt(Math.random() * (@board.width() - @width()) * 0.98)
[top, left] = [bh(), bw()]
[top, left] = [bh(), bw()] until @master.unoccupied(left, top, @width(), @height())
[top, left]
flyIn: ->
fd = (mod) ->
m = parseInt(40 * Math.random())
if (Math.random() < 0.5) then mod + m else -1 * m
@el.css
left: fd(@board.width())
top: fd(@board.height())
dfd = $.Deferred()
x = Math.random()
[top, left] = @get_new_pos()
@el.fadeIn().animate {top: top, left: left, rotate: @rotation}, 1500, 'easeOutQuint', () =>
@visible = true
dfd.resolve()
dfd.promise()
class PoemDisplay extends Dimensioned
el: $('#results')
_max_box: null
dialog: $('#message')
dtimer: null
constructor: (@board) ->
@el.css({top: @board.height()})
sentSuccess: (data, textStatus) =>
$('p', @dialog).html "Your poem has been immortalized! It can be seen on Twitter at <a href='https://twitter.com/#!/html5magnets'>@html5magnets</a>."
if data.error
$('p', @dialog).html data.message
@dialog.dialog("open")
if dtimer != null
clearTimeout(dtimer)
dtimer = null
dtimer = setTimeout (() => @dialog.dialog("close")), 7500
sentError: (query, textStatus) =>
console.log(query, textStatus)
sendToServer: (haiku) =>
$.ajax 'http://html5magnets.elfsternberg.com/poems/',
type: "POST"
data: {"message": haiku}
dataType: 'json'
success: @sentSuccess
error: @sentError
update: (lines) ->
lines = (l for l in lines when l.length > 0)
if lines.length == 0
@el.fadeOut()
return
@el.html('')
@el.show()
res = for words in lines
line = words[0].w
for word in words[1...words.length]
line += if word.s == 1 then word.w else '&nbsp;' + word.w
@el.append($('<p>' + line + '</p>'))
sentence = for words in lines
line = words[0].w
for word in words[1...words.length]
line += if word.s == 1 then word.w else ' ' + word.w
line
haiku_add = 0
if sentence.length > 1
haiku = sentence.join(" / ")
if haiku.length < 140
haiku_add = 38
@el.append('<div id="tweetthis"><img src="tweetthis.png"></div>')
$('#tweetthis').click(() => @sendToServer(lines))
if lines.length != @lastlines
lh = $('p', @el).height()
setTimeout((() => @el.animate {top: @board.height() - ((lh * (lines.length + 1.7)) + haiku_add)}), 1)
@
max_box: =>
return shape(@board.height() - (16 * 6.7), 0, 480, (16 * 6.7))
# A poem is three or more *moved* words in fuzzy collision.
class Poem
words: []
constructor: (@master) ->
@poembox = new PoemDisplay(@master.board)
real_poem: (poem = null) ->
poem = @words if not poem?
if poem.length > 1 then poem else []
has: (word) ->
return (w for w in @words when w == word).length > 0
find_bbox: (words = null, sp = 0) ->
words = @words if not words
return null if words.length < 2
[ul, ur, lr, ll] = words[0].shape()
[mx, my, nx, ny] = [ul.x, ul.y, lr.x, lr.y]
for i in [1...words.length]
[ul1, ur1, lr1, ll1] = words[i].shape()
mx = ul1.x if ul1.x < mx
my = ul1.y if ul1.y < my
nx = lr1.x if lr1.x > nx
ny = lr1.y if lr1.y > ny
return [{x: mx - sp, y: my - sp}, {x: nx + sp, y: my - sp}, {x: nx + sp, y: ny + sp}, {x: nx + sp, y: my - sp}]
check_dismissal: (word) ->
# If the word is colliding with another word in the poem, it
# is not being dismissed.
fuzzyshape = word.fuzzyshape()
for w in @words
if w != word and colliding(fuzzyshape, w.fuzzyshape())
@inorder()
return @words
# Remove word from @words
@words = @real_poem(w for w in @words when w != word)
return @words if @words.length < 2
# Reconstitute poem from what remains
find_split_poem = (poem) =>
# Why 2? Because a poem of length 1 is just a word!
throw "Don't run on an empty poem!" if poem.length < 2
# Transfer all words in *poem2* that are in collision with
# words in poem1. If the poems don't change, return them,
# otherwise repeat the process.
edgefollow = (poem1, poem2) =>
to_xfr = (w2 for w2 in poem2 when \
((w1 for w1 in poem1 when \
colliding(w1.fuzzyshape(), w2.fuzzyshape())).length > 0))
# Words are not being shuffled around
return [poem1, poem2] if to_xfr.length == 0
# Else...
poem1 = poem1.concat(to_xfr)
poem2 = (w for w in poem2 when w not in poem1)
edgefollow(poem1, poem2)
wordlist = (i for i in poem)
first_word = wordlist.pop()
[lpoem, rpoem] = edgefollow([first_word], wordlist)
return [] if lpoem.length < 2 and lpoem.length < 2
return rpoem if lpoem.length < 2
return lpoem if rpoem.length < 2
return if Math.vector.magnitude(@find_bbox(lpoem)[0]) < Math.vector.magnitude(@find_bbox(rpoem)[0])
lpoem
else
rpoem
@words = @real_poem(find_split_poem(@words))
if @words
@inorder()
@words
# Looks at the bounding box for the current poem and adds any words
# to it that are in collision with the existing poem.
# :: [tiles] -> [tiles]
research_poem: (poem) ->
nbbox = @find_bbox(poem)
newpoem = (i for i in poem)
potentials = (w for w in @master.visible() when \
(w not in newpoem) and colliding(w.fuzzyshape(), nbbox))
# [word, poem] -> boolean
collides_with_existing_poem = (nw1, poem1) ->
fzs1 = nw1.fuzzyshape()
acw1 = nw1.word
((nw2 for nw2 in poem1 when \
acw1 != nw2.word and \
colliding(nw2.fuzzyshape(), fzs1)).length > 0)
addenda = (nw for nw in potentials when collides_with_existing_poem(nw, newpoem))
if addenda.length == 0 then newpoem else @research_poem(newpoem.concat(addenda))
# Looks to see if the word has come into collision with another
# word, creating a new poem.
# :: tile -> [tiles]
maybe_new_poem: (word) ->
throw "Do not call maybe_new_poem on a working poem." if @words.length > 0
fuzzyshape = word.fuzzyshape()
@words = @real_poem((w for w in @master.visible() when \
colliding(w.fuzzyshape(), fuzzyshape)))
if @words.length
@words = @research_poem(@words)
@inorder()
@words
check_for_addition: (word) ->
# See if this word collides with any of the words in our poem:
fuzzyshape = word.fuzzyshape()
for w in @words
if colliding(fuzzyshape, w.fuzzyshape()) and w != word
@words.push(word)
# One collision is all it takes.
break
@words = @research_poem(@words)
@inorder()
@words
check: (word) ->
return @words = @maybe_new_poem(word) if @words.length == 0
if @has(word)
@words = @check_dismissal(word)
return @words = if @words.length == 0 then @maybe_new_poem(word) else @words
# This word doesn't create a new poem, and it isn't present in
# our existing poem.
return @words = @check_for_addition(word)
inorder: ->
return @poembox.update([]) if @words.length < 2
nbbox = @find_bbox(@words)
avg_height = 0
for w in @words
avg_height = avg_height + w.height()
avg_height = parseInt(avg_height / @words.length)
ret = []
for i in (i for i in [nbbox[0].y...nbbox[2].y] by avg_height)
zbot = i + avg_height
zone_words = (w for w in @words when w.top() >= i and w.top() < zbot)
zone_words.sort (a, b) -> a.left() - b.left()
ret.push((i.word for i in zone_words))
@poembox.update(ret)
class Magnets extends Dimensioned
constructor: (@wordlist) ->
@el = $(window)
@footer = new Footer($('#footer'))
@board = new Board($('#board'))
@recbox = $('#recbox')
@results = $('#results')
@words = (new Tile(word, @board, @) for word in @wordlist)
@resize()
@poem = new Poem(@)
$('#shuffler').click(@reword)
$(window).resize(@resize)
resize: =>
@unset_dims()
@board.css('100%', @height() - @footer.height())
(word.visibleReposition() for word in @words when word.visible)
@
unoccupied:(left, top, width, height) ->
reserved = []
if @poem.real_poem().length > 0
reserved.push(@poem.find_bbox(null, 10))
reserved.push(@poem.poembox.max_box())
target = shape(left, top, width, height)
for s in reserved
if colliding(target, s)
return false
true
visible: ->
(w for w in @words when w.visible)
poemed: (word) ->
@poem.check(word)
livewords: -> (w for w in @words when w.visible)
reword: =>
poemed = (w for w in @words when @poem.has(w))
flyprob = AVG_VISIBLE / (@words.length - poemed.length)
$.when.apply(null, (w.fadeOut() for w in @words when not @poem.has(w))).then () =>
$.when.apply(null, (w.flyIn() for w in @words when not @poem.has(w) and Math.random() < flyprob)).then () =>
(w.reset_dims() for w in @words when w.visible)
@
class MusicPlayer
constructor: (control, tunes) ->
@control = $(control)
@control.data('state', 'on')
@active = true
@music = new buzz.sound(tunes, {preload:true, autoload: true, loop: true})
@music.setVolume(0)
@music.bind 'canplaythrough', () =>
@music.play()
@music.fadeTo(60, 10000)
@control.click (ev) =>
@active = if @active then @fadeOut() else @fadeIn()
fadeOut: ->
@music.fadeOut(600, () => @music.pause())
$('img', @control).attr('src', 'mute.png')
false
fadeIn: ->
@music.play().fadeIn(1200)
$('img', @control).attr('src', 'unmute.png')
true
$ ->
$.ajax
url: 'js/wordlist.js'
data: {}
success: (data) -> (new Magnets(data)).resize().reword()
error: -> console.log(arguments)
dataType: 'json'
v = new MusicPlayer('#muteunmute',
['media/snowflake_-_Ethereal_Space.mp3',
'media/snowflake_-_Ethereal_Space.ogg'])
$( "#message" ).dialog
autoOpen: false
show: "fadeIn"
hide: "fadeOut"

BIN
src/pingbg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

View File

@ -1,97 +0,0 @@
#Copyright (c) 2012 Elf M. Sternberg
#
# Much of the code here I would never have understood if it hadn't
# been for the patient work of Caleb Helbling
# (http://www.propulsionjs.com/), as well as the Wikipedia pages for
# the Separating Axis Theorem. It took me a week to wrap my head
# around these ideas.
#
#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:
#
#The above copyright notice and this permission notice shall be included in
#all copies or substantial portions of the Software.
#
#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.
Math.vector =
add: (v1, v2) -> {x: (v1.x + v2.x), y: (v1.y + v2.y)}
# Scale a given vector.
scalar: (v, s) -> {x: (v.x * s), y: (v.y * s)}
dot: (v1, v2) -> v1.x * v2.x + v1.y * v2.y
magnitude2: (v) ->
x = v.x
y = v.y
x * x + y * y
magnitude: (v) -> Math.sqrt(Math.vector.magnitude2(v))
normalize: (v) ->
mag = Math.vector.magnitude(v)
{x: (v.x / mag), y: (v.y / mag)}
leftNormal: (v) -> {x: -v.y, y: v.x}
this.colliding = (shape1, shape2) ->
# Return the axes of a shape. In a polygon, each potential
# separating axis is the normal to each edge. For our purposes, a
# "shape" is an array of points with the structure [{x: 0, y: 0}, .. ]
# We assume that the final edge is from the last point back to the
# first.
genAxes = (shape) ->
throw "Cannot handle non-polygons" if shape.length < 3
# Calculate the normal of a single pair of points in the
# shape.
axis = (shape, pi) ->
p1 = shape[pi]
p2 = shape[if pi == (shape.length - 1) then 0 else pi + 1]
edge = {x: p1.x - p2.x, y: p1.y - p2.y}
Math.vector.normalize(Math.vector.leftNormal(edge))
(axis(shape, i) for i in [0...shape.length])
# Calculate the extremis of the shape "above" a given axis
genProjection = (shape, axis) ->
min = Math.vector.dot(axis, shape[0])
max = min
for i in [1...shape.length]
p = Math.vector.dot(axis, shape[i])
min = p if p < min
max = p if p > max
{min: min, max: max}
axes1 = genAxes(shape1)
axes2 = genAxes(shape2)
axes = axes1.concat axes2
for axis in axes
proj1 = genProjection(shape1, axis)
proj2 = genProjection(shape2, axis)
if not ( \
(proj1.min >= proj2.min and proj1.min <= proj2.max) or \
(proj1.max >= proj2.min and proj1.max <= proj2.max) or \
(proj2.min >= proj1.min and proj2.min <= proj1.max) or \
(proj2.max >= proj1.min and proj2.max <= proj1.max))
return false
return true

View File

@ -1,269 +0,0 @@
/* -*- mode: css; -*- */
/* ___ ___ ___ ___ _
/ __/ __/ __| | _ \___ ___ ___| |_
| (__\__ \__ \ | / -_|_-</ -_) _|
\___|___/___/ |_|_\___/__/\___|\__|
html5doctor.com Reset Stylesheet
v1.5
Last Updated: 2010-08-12
Author: Richard Clark - http://richclarkdesign.com
Twitter: @rich_clark
*/
html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
abbr, address, cite, code,
del, dfn, em, img, ins, kbd, q, samp,
small, strong, sub, sup, var,
b, i,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, figcaption, figure,
footer, header, hgroup, menu, nav, section, summary,
time, mark, audio, video {
margin:0;
padding:0;
border:0;
outline:0;
vertical-align:baseline;
background:transparent;
}
body {
line-height:1;
}
article,aside,canvas,details,figcaption,figure,
footer,header,hgroup,menu,nav,section,summary {
display:block;
}
nav ul {
list-style:none;
}
blockquote, q {
quotes:none;
}
blockquote:before, blockquote:after,
q:before, q:after {
content:'';
content:none;
}
a {
margin:0;
padding:0;
font-size:100%;
vertical-align:baseline;
background:transparent;
}
a {
margin:0;
padding:0;
font-size:100%;
vertical-align:baseline;
background:transparent;
}
ins {
background-color:#ff9;
color:#000;
text-decoration:none;
}
mark {
background-color:#ff9;
color:#000;
font-style:italic;
font-weight:bold;
}
del {
text-decoration: line-through;
}
abbr[title], dfn[title] {
border-bottom:1px dotted #000;
cursor:help;
}
table {
border-collapse:collapse;
border-spacing:0;
}
hr {
display:block;
height:1px;
border:0;
border-top:1px solid #cccccc;
margin:1em 0;
padding:0;
}
input, select {
vertical-align:middle;
}
.small-rounded {
-moz-border-radius-topleft: 5px;
-moz-border-radius-topright: 5px;
-moz-border-radius-bottomleft: 5px;
-moz-border-radius-bottomright: 5px;
-webkit-border-bottom-right-radius: 5px;
-webkit-border-top-left-radius: 5px;
-webkit-border-top-right-radius: 5px;
-webkit-border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
border-top-left-radius: 5px;
border-top-right-radius: 5px;
border-bottom-left-radius: 5px;
}
html {
overflow: hidden;
}
@limegreen: #32cd32;
#board {
overflow: hidden;
position: relative;
width: 100%;
background: url('pingbg.png') repeat;
}
.word {
font-family: Georgia, Palatino,"Palatino Linotype", Times, "Times New Roman", serif;
-moz-box-shadow: 0 0 6px 2px #aaa;
-webkit-box-shadow: 0 0 6px 2px #aaa;
box-shadow: 0 0 6px 2px #aaa;
display: none;
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: none;
-o-user-select: none;
text-align: center;
user-select: none;
cursor: pointer;
color: #444;
font-size: 13px;
padding: 3px 4px 4px 4px;
position: absolute;
background: white;
z-index: 100;
}
#footer {
height: 18ex;
font-family: "Trebuchet MS", "Lucida Sans Unicode", "Lucida Grande", "Lucida Sans", Arial, sans-serif;
font-size: 12px;
width: 100%;
}
#stripe {
width: 100%;
margin: 0.4% 0 0.4% 0;
background-color: @limegreen;
height: 10ex;
}
.heart {
position: absolute;
color: deeppink;
font-size: 22px;
font-weight: bold;
z-index: 30;
}
#f1 { width: 46%; float: left; padding-left: 1%;}
#f2 { width: 46%; float: right; text-align: right; padding-right: 1%;}
.recline {
position: relative;
height: 40px;
width: 100%;
clear: right;
background: url(writerect_bg.png) top left repeat-x;
}
#results {
-moz-border-radius-topright: 5px;
-webkit-border-top-right-radius: 5px;
border-top-right-radius: 5px;
border-top: 1px solid #888;
border-right: 1px solid #888;
border-bottom: 1px solid #888;
position: absolute;
color: #666;
left: 0px;
bottom: 5px;
padding: 1em;
background: url(alphamod.png) repeat;
display: none;
p {
white-space: nowrap;
line-height: 1.1;
padding: 0;
}
}
#shuffler {
font-family: Georgia, Palatino,"Palatino Linotype", Times, "Times New Roman", serif;
font-size: 16px;
font-weight: bold;
color: white;
position: relative;
background-color: @limegreen;
margin-top: 10px;
float: right;
cursor: pointer;
margin-right: 32px;
width: 5em;
vertical-align: middle;
border-top: 1px solid white;
border-left: 1px solid white;
border-bottom: 1px solid black;
border-right: 1px solid black;
box-shadow: 1px 3px 6px rgba(0, 0, 0, 0.80);
-moz-box-shadow: 1px 3px 6px rgba(0, 0, 0, 0.80);
-webkit-box-shadow: 1px 3px 6px rgba(0, 0, 0, 0.80);
}
#shuffler:hover {
background-color: darken(@limegreen, 10%);
}
#shuffler:active {
background-color: darken(@limegreen, 10%);
border-top: 1px solid black;
border-left: 1px solid black;
border-bottom: 1px solid white;
border-right: 1px solid white;
}
#muteunmute {
cursor: pointer;
float: right;
padding-right: 128px;
width: 42px;
height: 42px;
img {
width: 42px;
height: 42px;
}
}
#tweetthis {
padding-top: 6px;
}

23
src/styles.css Normal file
View File

@ -0,0 +1,23 @@
html, body {
overflow: hidden;
padding: 0;
margin: 0;
}
#fridgemagnets {
width: 100vw;
height: 100vh;
}
#fridge {
overflow: hidden;
width: 100vw;
background: url('pingbg.png') repeat;
}
#footer {
background-color: #32cd32;
width: 100vw;
height: 18ex;
}

View File

@ -1,34 +0,0 @@
testCase = require('nodeunit').testCase
require('./sat.coffee')
module.exports = testCase
"TestAddition": (test) ->
m = Math.vector.add {x: 1, y: 1}, {x: -1, y: -1}
test.ok(m.x == 0 and m.y == 0)
m = Math.vector.add {x: 1, y: 1}, {x: 1, y: 1}
test.ok(m.x == 2 and m.y == 2)
test.done()
"TestScalar": (test) ->
m = Math.vector.scalar({x: 2, y: 2}, 2)
test.ok(m.x == 4 and m.y == 4)
test.done()
"TestMag2": (test) ->
m = Math.vector.magnitude2({x: 2, y: 2})
test.ok(m == 8)
test.done()
"TestMag": (test) ->
m = Math.vector.magnitude({x: 2, y: 2})
test.ok(m == Math.sqrt(8))
test.done()
"TestNormalize": (test) ->
m = Math.vector.normalize({x: 5, y: 0})
test.ok(m.x == 1 and m.y == 0)
m = Math.vector.normalize({x: 0, y: 5})
test.ok(m.x == 0 and m.y == 1)
m = Math.vector.normalize({x: 4, y: 3})
test.ok((m.x * m.x + m.y * m.y) == 1)
test.done()

View File

@ -1,210 +0,0 @@
[{"w": "a", "s": 0},
{"w": "a", "s": 0},
{"w": "about", "s": 0},
{"w": "above", "s": 0},
{"w": "after", "s": 0},
{"w": "all", "s": 0},
{"w": "almost", "s": 0},
{"w": "always", "s": 0},
{"w": "am", "s": 0},
{"w": "an", "s": 0},
{"w": "an", "s": 0},
{"w": "and", "s": 0},
{"w": "and", "s": 0},
{"w": "animal", "s": 0},
{"w": "apple", "s": 0},
{"w": "are", "s": 0},
{"w": "as", "s": 0},
{"w": "as", "s": 0},
{"w": "ask", "s": 0},
{"w": "at", "s": 0},
{"w": "bad", "s": 0},
{"w": "be", "s": 0},
{"w": "beauty", "s": 0},
{"w": "believe", "s": 0},
{"w": "beneath", "s": 0},
{"w": "between", "s": 0},
{"w": "bird", "s": 0},
{"w": "birthday", "s": 0},
{"w": "blend", "s": 0},
{"w": "blue", "s": 0},
{"w": "bring", "s": 0},
{"w": "but", "s": 0},
{"w": "but", "s": 0},
{"w": "butterfly", "s": 0},
{"w": "by", "s": 0},
{"w": "calendar", "s": 0},
{"w": "can", "s": 0},
{"w": "celebrate", "s": 0},
{"w": "change", "s": 0},
{"w": "cloud", "s": 0},
{"w": "cold", "s": 0},
{"w": "come", "s": 0},
{"w": "comfort", "s": 0},
{"w": "could", "s": 0},
{"w": "d", "s": 1},
{"w": "dark", "s": 0},
{"w": "day", "s": 0},
{"w": "delightful", "s": 0},
{"w": "desire", "s": 0},
{"w": "did", "s": 0},
{"w": "do", "s": 0},
{"w": "dream", "s": 0},
{"w": "e", "s": 1},
{"w": "eat", "s": 0},
{"w": "ed", "s": 1},
{"w": "er", "s": 1},
{"w": "es", "s": 1},
{"w": "est", "s": 1},
{"w": "evening", "s": 0},
{"w": "every", "s": 0},
{"w": "fall", "s": 0},
{"w": "favorite", "s": 0},
{"w": "feel", "s": 0},
{"w": "float", "s": 0},
{"w": "flower", "s": 0},
{"w": "for", "s": 0},
{"w": "from", "s": 0},
{"w": "full", "s": 0},
{"w": "fun", "s": 0},
{"w": "garden", "s": 0},
{"w": "get", "s": 0},
{"w": "ghost", "s": 0},
{"w": "good", "s": 0},
{"w": "grass", "s": 0},
{"w": "green", "s": 0},
{"w": "grow", "s": 0},
{"w": "happy", "s": 0},
{"w": "has", "s": 0},
{"w": "have", "s": 0},
{"w": "he", "s": 0},
{"w": "here", "s": 0},
{"w": "here", "s": 0},
{"w": "him", "s": 0},
{"w": "his", "s": 0},
{"w": "hot", "s": 0},
{"w": "house", "s": 0},
{"w": "how", "s": 0},
{"w": "I", "s": 0},
{"w": "I", "s": 0},
{"w": "if", "s": 0},
{"w": "in", "s": 0},
{"w": "ing", "s": 1},
{"w": "ing", "s": 1},
{"w": "is", "s": 0},
{"w": "is", "s": 0},
{"w": "it", "s": 0},
{"w": "keep", "s": 0},
{"w": "laugh", "s": 0},
{"w": "learn", "s": 0},
{"w": "leave", "s": 0},
{"w": "let", "s": 0},
{"w": "light", "s": 0},
{"w": "like", "s": 0},
{"w": "like", "s": 0},
{"w": "live", "s": 0},
{"w": "long", "s": 0},
{"w": "look", "s": 0},
{"w": "love", "s": 0},
{"w": "ly", "s": 1},
{"w": "magic", "s": 0},
{"w": "make", "s": 0},
{"w": "man", "s": 0},
{"w": "me", "s": 0},
{"w": "memory", "s": 0},
{"w": "month", "s": 0},
{"w": "more", "s": 0},
{"w": "morning", "s": 0},
{"w": "must", "s": 0},
{"w": "my", "s": 0},
{"w": "never", "s": 0},
{"w": "nibble", "s": 0},
{"w": "night", "s": 0},
{"w": "no", "s": 0},
{"w": "of", "s": 0},
{"w": "of", "s": 0},
{"w": "off", "s": 0},
{"w": "on", "s": 0},
{"w": "only", "s": 0},
{"w": "or", "s": 0},
{"w": "out", "s": 0},
{"w": "out", "s": 0},
{"w": "paint", "s": 0},
{"w": "people", "s": 0},
{"w": "perfect", "s": 0},
{"w": "play", "s": 0},
{"w": "proof", "s": 0},
{"w": "puff", "s": 0},
{"w": "r", "s": 1},
{"w": "rain", "s": 0},
{"w": "room", "s": 0},
{"w": "s", "s": 1},
{"w": "s", "s": 1},
{"w": "s", "s": 1},
{"w": "say", "s": 0},
{"w": "season", "s": 0},
{"w": "see", "s": 0},
{"w": "she", "s": 0},
{"w": "shine", "s": 0},
{"w": "simple", "s": 0},
{"w": "sky", "s": 0},
{"w": "snow", "s": 0},
{"w": "so", "s": 0},
{"w": "some", "s": 0},
{"w": "song", "s": 0},
{"w": "spring", "s": 0},
{"w": "summer", "s": 0},
{"w": "sun", "s": 0},
{"w": "sweet", "s": 0},
{"w": "take", "s": 0},
{"w": "talk", "s": 0},
{"w": "than", "s": 0},
{"w": "that", "s": 0},
{"w": "the", "s": 0},
{"w": "the", "s": 0},
{"w": "their", "s": 0},
{"w": "then", "s": 0},
{"w": "there", "s": 0},
{"w": "they", "s": 0},
{"w": "this", "s": 0},
{"w": "though", "s": 0},
{"w": "through", "s": 0},
{"w": "time", "s": 0},
{"w": "to", "s": 0},
{"w": "to", "s": 0},
{"w": "together", "s": 0},
{"w": "too", "s": 0},
{"w": "touch", "s": 0},
{"w": "trick", "s": 0},
{"w": "truth", "s": 0},
{"w": "up", "s": 0},
{"w": "us", "s": 0},
{"w": "use", "s": 0},
{"w": "vacation", "s": 0},
{"w": "walk", "s": 0},
{"w": "want", "s": 0},
{"w": "warm", "s": 0},
{"w": "was", "s": 0},
{"w": "watch", "s": 0},
{"w": "we", "s": 0},
{"w": "weather", "s": 0},
{"w": "were", "s": 0},
{"w": "when", "s": 0},
{"w": "which", "s": 0},
{"w": "whisper", "s": 0},
{"w": "who", "s": 0},
{"w": "why", "s": 0},
{"w": "will", "s": 0},
{"w": "winter", "s": 0},
{"w": "with", "s": 0},
{"w": "woman", "s": 0},
{"w": "word", "s": 0},
{"w": "work", "s": 0},
{"w": "world", "s": 0},
{"w": "would", "s": 0},
{"w": "y", "s": 1},
{"w": "year", "s": 0},
{"w": "you", "s": 0},
{"w": "you", "s": 0},
{"w": "your", "s": 0}
]

211
src/wordlist.ts Normal file
View File

@ -0,0 +1,211 @@
export const words = [
{ w: "a", s: 0 },
{ w: "a", s: 0 },
{ w: "about", s: 0 },
{ w: "above", s: 0 },
{ w: "after", s: 0 },
{ w: "all", s: 0 },
{ w: "almost", s: 0 },
{ w: "always", s: 0 },
{ w: "am", s: 0 },
{ w: "an", s: 0 },
{ w: "an", s: 0 },
{ w: "and", s: 0 },
{ w: "and", s: 0 },
{ w: "animal", s: 0 },
{ w: "apple", s: 0 },
{ w: "are", s: 0 },
{ w: "as", s: 0 },
{ w: "as", s: 0 },
{ w: "ask", s: 0 },
{ w: "at", s: 0 },
{ w: "bad", s: 0 },
{ w: "be", s: 0 },
{ w: "beauty", s: 0 },
{ w: "believe", s: 0 },
{ w: "beneath", s: 0 },
{ w: "between", s: 0 },
{ w: "bird", s: 0 },
{ w: "birthday", s: 0 },
{ w: "blend", s: 0 },
{ w: "blue", s: 0 },
{ w: "bring", s: 0 },
{ w: "but", s: 0 },
{ w: "but", s: 0 },
{ w: "butterfly", s: 0 },
{ w: "by", s: 0 },
{ w: "calendar", s: 0 },
{ w: "can", s: 0 },
{ w: "celebrate", s: 0 },
{ w: "change", s: 0 },
{ w: "cloud", s: 0 },
{ w: "cold", s: 0 },
{ w: "come", s: 0 },
{ w: "comfort", s: 0 },
{ w: "could", s: 0 },
{ w: "d", s: 1 },
{ w: "dark", s: 0 },
{ w: "day", s: 0 },
{ w: "delightful", s: 0 },
{ w: "desire", s: 0 },
{ w: "did", s: 0 },
{ w: "do", s: 0 },
{ w: "dream", s: 0 },
{ w: "e", s: 1 },
{ w: "eat", s: 0 },
{ w: "ed", s: 1 },
{ w: "er", s: 1 },
{ w: "es", s: 1 },
{ w: "est", s: 1 },
{ w: "evening", s: 0 },
{ w: "every", s: 0 },
{ w: "fall", s: 0 },
{ w: "favorite", s: 0 },
{ w: "feel", s: 0 },
{ w: "float", s: 0 },
{ w: "flower", s: 0 },
{ w: "for", s: 0 },
{ w: "from", s: 0 },
{ w: "full", s: 0 },
{ w: "fun", s: 0 },
{ w: "garden", s: 0 },
{ w: "get", s: 0 },
{ w: "ghost", s: 0 },
{ w: "good", s: 0 },
{ w: "grass", s: 0 },
{ w: "green", s: 0 },
{ w: "grow", s: 0 },
{ w: "happy", s: 0 },
{ w: "has", s: 0 },
{ w: "have", s: 0 },
{ w: "he", s: 0 },
{ w: "here", s: 0 },
{ w: "here", s: 0 },
{ w: "him", s: 0 },
{ w: "his", s: 0 },
{ w: "hot", s: 0 },
{ w: "house", s: 0 },
{ w: "how", s: 0 },
{ w: "I", s: 0 },
{ w: "I", s: 0 },
{ w: "if", s: 0 },
{ w: "in", s: 0 },
{ w: "ing", s: 1 },
{ w: "ing", s: 1 },
{ w: "is", s: 0 },
{ w: "is", s: 0 },
{ w: "it", s: 0 },
{ w: "keep", s: 0 },
{ w: "laugh", s: 0 },
{ w: "learn", s: 0 },
{ w: "leave", s: 0 },
{ w: "let", s: 0 },
{ w: "light", s: 0 },
{ w: "like", s: 0 },
{ w: "like", s: 0 },
{ w: "live", s: 0 },
{ w: "long", s: 0 },
{ w: "look", s: 0 },
{ w: "love", s: 0 },
{ w: "ly", s: 1 },
{ w: "magic", s: 0 },
{ w: "make", s: 0 },
{ w: "man", s: 0 },
{ w: "me", s: 0 },
{ w: "memory", s: 0 },
{ w: "month", s: 0 },
{ w: "more", s: 0 },
{ w: "morning", s: 0 },
{ w: "must", s: 0 },
{ w: "my", s: 0 },
{ w: "never", s: 0 },
{ w: "nibble", s: 0 },
{ w: "night", s: 0 },
{ w: "no", s: 0 },
{ w: "of", s: 0 },
{ w: "of", s: 0 },
{ w: "off", s: 0 },
{ w: "on", s: 0 },
{ w: "only", s: 0 },
{ w: "or", s: 0 },
{ w: "out", s: 0 },
{ w: "out", s: 0 },
{ w: "paint", s: 0 },
{ w: "people", s: 0 },
{ w: "perfect", s: 0 },
{ w: "play", s: 0 },
{ w: "proof", s: 0 },
{ w: "puff", s: 0 },
{ w: "r", s: 1 },
{ w: "rain", s: 0 },
{ w: "room", s: 0 },
{ w: "s", s: 1 },
{ w: "s", s: 1 },
{ w: "s", s: 1 },
{ w: "say", s: 0 },
{ w: "season", s: 0 },
{ w: "see", s: 0 },
{ w: "she", s: 0 },
{ w: "shine", s: 0 },
{ w: "simple", s: 0 },
{ w: "sky", s: 0 },
{ w: "snow", s: 0 },
{ w: "so", s: 0 },
{ w: "some", s: 0 },
{ w: "song", s: 0 },
{ w: "spring", s: 0 },
{ w: "summer", s: 0 },
{ w: "sun", s: 0 },
{ w: "sweet", s: 0 },
{ w: "take", s: 0 },
{ w: "talk", s: 0 },
{ w: "than", s: 0 },
{ w: "that", s: 0 },
{ w: "the", s: 0 },
{ w: "the", s: 0 },
{ w: "their", s: 0 },
{ w: "then", s: 0 },
{ w: "there", s: 0 },
{ w: "they", s: 0 },
{ w: "this", s: 0 },
{ w: "though", s: 0 },
{ w: "through", s: 0 },
{ w: "time", s: 0 },
{ w: "to", s: 0 },
{ w: "to", s: 0 },
{ w: "together", s: 0 },
{ w: "too", s: 0 },
{ w: "touch", s: 0 },
{ w: "trick", s: 0 },
{ w: "truth", s: 0 },
{ w: "up", s: 0 },
{ w: "us", s: 0 },
{ w: "use", s: 0 },
{ w: "vacation", s: 0 },
{ w: "walk", s: 0 },
{ w: "want", s: 0 },
{ w: "warm", s: 0 },
{ w: "was", s: 0 },
{ w: "watch", s: 0 },
{ w: "we", s: 0 },
{ w: "weather", s: 0 },
{ w: "were", s: 0 },
{ w: "when", s: 0 },
{ w: "which", s: 0 },
{ w: "whisper", s: 0 },
{ w: "who", s: 0 },
{ w: "why", s: 0 },
{ w: "will", s: 0 },
{ w: "winter", s: 0 },
{ w: "with", s: 0 },
{ w: "woman", s: 0 },
{ w: "word", s: 0 },
{ w: "work", s: 0 },
{ w: "world", s: 0 },
{ w: "would", s: 0 },
{ w: "y", s: 1 },
{ w: "year", s: 0 },
{ w: "you", s: 0 },
{ w: "you", s: 0 },
{ w: "your", s: 0 },
];

61
tsconfig.base.json Normal file
View File

@ -0,0 +1,61 @@
{
"compilerOptions": {
"strict": true,
"baseUrl": ".",
"esModuleInterop": true,
"paths": {
"@goauthentik/docs/*": ["../website/docs/*"]
},
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"sourceMap": true,
"target": "esnext",
"module": "esnext",
"moduleResolution": "node",
"lib": [
"ES5",
"ES2015",
"ES2016",
"ES2017",
"ES2018",
"ES2019",
"ES2020",
"ESNext",
"DOM",
"DOM.Iterable",
"WebWorker"
],
"noUnusedLocals": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"useDefineForClassFields": false,
"alwaysStrict": true,
"noImplicitAny": true,
"plugins": [
{
"name": "ts-lit-plugin",
"strict": true,
"rules": {
"no-unknown-tag-name": "off",
"no-missing-import": "off",
"no-incompatible-type-binding": "off",
"no-unknown-property": "off",
"no-unknown-attribute": "off"
}
},
{
"name": "@genesiscommunitysuccess/custom-elements-lsp",
"designSystemPrefix": "ak-",
"parser": {
"timeout": 2000
}
}
]
}
}

17
tsconfig.json Normal file
View File

@ -0,0 +1,17 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"paths": {
"@goauthentik/admin/*": ["./src/admin/*"],
"@goauthentik/common/*": ["./src/common/*"],
"@goauthentik/components/*": ["./src/components/*"],
"@goauthentik/docs/*": ["../website/docs/*"],
"@goauthentik/elements/*": ["./src/elements/*"],
"@goauthentik/flow/*": ["./src/flow/*"],
"@goauthentik/locales/*": ["./src/locales/*"],
"@goauthentik/polyfill/*": ["./src/polyfill/*"],
"@goauthentik/standalone/*": ["./src/standalone/*"],
"@goauthentik/user/*": ["./src/user/*"]
}
}
}