Compare commits
11 Commits
d43e474251
...
master
Author | SHA1 | Date |
---|---|---|
Elf M. Sternberg | c849032741 | |
Elf M. Sternberg | 55e425dd85 | |
Elf M. Sternberg | 26bf5bc883 | |
Elf M. Sternberg | 566bc9d9f4 | |
Elf M. Sternberg | 6f10ef9e9e | |
Elf M. Sternberg | 0f23358f1d | |
Elf M. Sternberg | 0ba271be28 | |
Elf M. Sternberg | 6c9ac44eac | |
Elf M. Sternberg | 3b2a7f22f4 | |
Elf M. Sternberg | cf560f576d | |
Elf M. Sternberg | 52c3ec642d |
112
.eslintrc.json
112
.eslintrc.json
|
@ -1,58 +1,58 @@
|
|||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint", "eslint-plugin-import", "prettier"],
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": ["airbnb", "plugin:@typescript-eslint/recommended", "prettier"],
|
||||
"parserOptions": {
|
||||
"project": ["tsconfig.json"],
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"rules": {
|
||||
"no-await-in-loop": "off",
|
||||
"no-use-before-define": "off",
|
||||
"no-nested-ternary": "off",
|
||||
"@typescript-eslint/no-use-before-define": ["error"],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"import/extensions": "off",
|
||||
"import/prefer-default-export": "off",
|
||||
"@typescript-eslint/ban-types": "warn",
|
||||
"@typescript-eslint/no-use-before-define": "warn",
|
||||
"arrow-body-style": "warn",
|
||||
"camelcase": "warn",
|
||||
"dot-notation": "warn",
|
||||
"eqeqeq": "warn",
|
||||
"import/first": "warn",
|
||||
"import/newline-after-import": "warn",
|
||||
"import/no-extraneous-dependencies": "warn",
|
||||
"import/order": "warn",
|
||||
"no-else-return": "warn",
|
||||
"no-param-reassign": "warn",
|
||||
"no-return-assign": "warn",
|
||||
"no-sequences": "warn",
|
||||
"no-shadow": "off",
|
||||
"@typescript-eslint/no-shadow": ["warn"],
|
||||
"no-underscore-dangle": "warn",
|
||||
"no-unneeded-ternary": "warn",
|
||||
"object-shorthand": "warn",
|
||||
"prefer-arrow-callback": "warn",
|
||||
"prefer-const": "warn",
|
||||
"prefer-destructuring": "warn",
|
||||
"prefer-template": "warn"
|
||||
},
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"node": {
|
||||
"extensions": [".js", ".ts"]
|
||||
},
|
||||
"typescript": {
|
||||
"project": "./tsconfig.json"
|
||||
}
|
||||
}
|
||||
}
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint", "prettier", "jest", "unused-imports"],
|
||||
"extends": ["@open-wc", "prettier", "eslint:recommended"],
|
||||
"rules": {
|
||||
"@typescript-eslint/ban-ts-comment": "warn",
|
||||
"@typescript-eslint/ban-types": "warn",
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-shadow": ["warn"],
|
||||
"@typescript-eslint/no-unused-vars": "off",
|
||||
"@typescript-eslint/no-use-before-define": ["error", { "functions": false }],
|
||||
"arrow-body-style": "warn",
|
||||
"camelcase": "off",
|
||||
"default-param-last": "off",
|
||||
"dot-notation": "warn",
|
||||
"eqeqeq": "warn",
|
||||
"import/extensions": "off",
|
||||
"import/first": "warn",
|
||||
"import/newline-after-import": "warn",
|
||||
"import/no-cycle": "warn",
|
||||
"import/no-extraneous-dependencies": "warn",
|
||||
"import/order": "warn",
|
||||
"import/prefer-default-export": "warn",
|
||||
"no-else-return": "warn",
|
||||
"no-nested-ternary": "off",
|
||||
"no-param-reassign": "warn",
|
||||
"no-return-assign": "warn",
|
||||
"no-sequences": "warn",
|
||||
"no-shadow": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
"no-unneeded-ternary": "warn",
|
||||
"no-unused-vars": "off",
|
||||
"no-use-before-define": "off",
|
||||
"object-shorthand": "warn",
|
||||
"one-var": ["error", "never"],
|
||||
"one-var-declaration-per-line": ["error", "initializations"],
|
||||
"prefer-arrow-callback": "warn",
|
||||
"prefer-const": "warn",
|
||||
"prefer-destructuring": "warn",
|
||||
"prefer-template": "warn",
|
||||
"sort-imports": [
|
||||
"error",
|
||||
{
|
||||
"ignoreDeclarationSort": true
|
||||
}
|
||||
],
|
||||
"unused-imports/no-unused-imports": "error",
|
||||
"unused-imports/no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
"vars": "all",
|
||||
"varsIgnorePattern": "^_",
|
||||
"args": "after-used",
|
||||
"argsIgnorePattern": "^_"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
14
.prettierrc
14
.prettierrc
|
@ -1,8 +1,10 @@
|
|||
{
|
||||
"trailingComma": "es5",
|
||||
"printWidth": 110,
|
||||
"tabWidth": 4,
|
||||
"useTabs": false,
|
||||
"semi": true,
|
||||
"singleQuote": false
|
||||
"useTabs": false,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"trailingComma": "all",
|
||||
"bracketSameLine": false,
|
||||
"arrowParens": "avoid"
|
||||
}
|
||||
|
|
74
README.md
74
README.md
|
@ -1,30 +1,62 @@
|
|||
# Dominoclock
|
||||
|
||||
This is code that's been around since 1996 or so, and is one of the
|
||||
three first Javascript programs I ever wrote. It's just the "time of
|
||||
day" counter for the fictional world that's the setting of my
|
||||
long-running [space opera
|
||||
series](https://www.pendorwright.com/journals/). It has its own
|
||||
calendar, and unlike Star Trek, I had in mind what the "star dates"
|
||||
would mean early on.
|
||||
It's nostalgia week at Pendorwright Labs, where toy programs are the
|
||||
order of the day. DominoClock is a simulation (or a re-implementation)
|
||||
of a nifty electromechanical watch I saw back about 25 years ago; it was
|
||||
a watch with a domino face, and the points on it elevated as the minutes
|
||||
slipped by. It was kinda-sorta meant for the blind, so re-implementing
|
||||
it as a web application is a bit silly. It only has a resolution of
|
||||
every five minutes, so implementing the seconds hand is also a bit
|
||||
silly, but it lets you see that the clock is working as planned.
|
||||
|
||||
# Motivation
|
||||
|
||||
This is a slightly modernized version, just to see what it would be
|
||||
like to write this in 2021. The answer is that not much has changed;
|
||||
the code runs just fine, although `getYear()` has been deprecated,
|
||||
replaced by `.geUTCFullYear()`. The syntax of 2021 Javascript is a lot
|
||||
nicer than 1996, although there is a limit to how much density one can
|
||||
achieve when it's a lot of fiddly calculations around converting
|
||||
human-readable dates into Pendorian-readable ones.
|
||||
In the `orginal/` folder, you'll find the Java (!) program I wrote back
|
||||
in 1996. This is literally my first and, if memory serves me correctly,
|
||||
only Java applet I ever wrote. It's written in Java so old it may even
|
||||
be before Java 1.0 was released. Sun used to send a sales team to nerdy
|
||||
offices and, while the main guy talked to the managers, the sales
|
||||
engineer would slip the devs a CD or two with labels like "Java 0.9
|
||||
beta" and "Sun Proprietary - Not for general release" handwritten on
|
||||
them. By the time management got around to asking the nerds, "Have you
|
||||
heard about this Java thing?" we'd already been playing with it for
|
||||
months.
|
||||
|
||||
What this project _really_ involves is preserving the basic elements
|
||||
of prettier, eslint, vitejs, and typescript that I routinely use these
|
||||
days as the basis of my Javascript work. Most of the configuration
|
||||
files are short, as you'd expect from a vanilla javascript project
|
||||
with a single source file and no framework, but they do include things
|
||||
like sourcemap inclusion, minification, and using rollup to generate
|
||||
proper EcmaScript-6.
|
||||
I just wanted to see it again.
|
||||
|
||||
I also wanted to practice a bit more with web components. I think
|
||||
they're more important than React, and I have a strong preference for
|
||||
the way they integrate with the browser ecosystem rather than fight
|
||||
against it the way React does.
|
||||
|
||||
# Running It
|
||||
|
||||
```
|
||||
$ npm install
|
||||
$ npm run dev
|
||||
```
|
||||
|
||||
It'll be on port 3000.
|
||||
|
||||
It's written using the original colors I chose back in 1996, so it's
|
||||
more than a bit ugly. This version, though, has rounded corners and
|
||||
dots, so there's that. It does respond to a variety of CSS variables, so
|
||||
you can change the size and colors. I *really* should make it resize
|
||||
the faces in response to the overall size of the container allocated to
|
||||
the `<domino-clock />` component, but that's a future task.
|
||||
|
||||
# DEMO
|
||||
|
||||
There is a demo: [The Domino
|
||||
Clock.](https://elfsternberg.com/projects/dominoclock/)
|
||||
|
||||
# TODO
|
||||
|
||||
- Add a few more CSS variables.
|
||||
- Make it respond to the size of the container, rather than forcing its
|
||||
size.
|
||||
- Make the faces aria-compliant. `aria-valuenow` seems like the safest
|
||||
bet.
|
||||
|
||||
# License
|
||||
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
{
|
||||
"schemaVersion": "1.0.0",
|
||||
"readme": "",
|
||||
"modules": [
|
||||
{
|
||||
"kind": "javascript-module",
|
||||
"path": "src/DominoClock.ts",
|
||||
"declarations": [
|
||||
{
|
||||
"kind": "class",
|
||||
"description": "",
|
||||
"name": "DominoClock",
|
||||
"members": [
|
||||
{
|
||||
"kind": "field",
|
||||
"name": "faces",
|
||||
"type": {
|
||||
"text": "HTMLCollection"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "field",
|
||||
"name": "timer",
|
||||
"type": {
|
||||
"text": "number"
|
||||
},
|
||||
"default": "0"
|
||||
},
|
||||
{
|
||||
"kind": "method",
|
||||
"name": "paint"
|
||||
}
|
||||
],
|
||||
"superclass": {
|
||||
"name": "LitElement",
|
||||
"package": "lit"
|
||||
},
|
||||
"customElement": true
|
||||
}
|
||||
],
|
||||
"exports": [
|
||||
{
|
||||
"kind": "js",
|
||||
"name": "DominoClock",
|
||||
"declaration": {
|
||||
"name": "DominoClock",
|
||||
"module": "src/DominoClock.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "js",
|
||||
"name": "default",
|
||||
"declaration": {
|
||||
"name": "DominoClock",
|
||||
"module": "src/DominoClock.ts"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "javascript-module",
|
||||
"path": "src/DominoFace.ts",
|
||||
"declarations": [
|
||||
{
|
||||
"kind": "class",
|
||||
"description": "",
|
||||
"name": "DominoClockface",
|
||||
"members": [
|
||||
{
|
||||
"kind": "field",
|
||||
"name": "name",
|
||||
"type": {
|
||||
"text": "string"
|
||||
},
|
||||
"default": "\"hour\""
|
||||
},
|
||||
{
|
||||
"kind": "field",
|
||||
"name": "time",
|
||||
"type": {
|
||||
"text": "string"
|
||||
},
|
||||
"default": "\"12\""
|
||||
},
|
||||
{
|
||||
"kind": "field",
|
||||
"name": "dots",
|
||||
"type": {
|
||||
"text": "HTMLCollection"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "method",
|
||||
"name": "calculateDots"
|
||||
}
|
||||
],
|
||||
"superclass": {
|
||||
"name": "LitElement",
|
||||
"package": "lit"
|
||||
},
|
||||
"customElement": true
|
||||
}
|
||||
],
|
||||
"exports": [
|
||||
{
|
||||
"kind": "js",
|
||||
"name": "DominoClockface",
|
||||
"declaration": {
|
||||
"name": "DominoClockface",
|
||||
"module": "src/DominoFace.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "js",
|
||||
"name": "default",
|
||||
"declaration": {
|
||||
"name": "DominoClockface",
|
||||
"module": "src/DominoFace.ts"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "javascript-module",
|
||||
"path": "src/index.ts",
|
||||
"declarations": [],
|
||||
"exports": [
|
||||
{
|
||||
"kind": "js",
|
||||
"name": "DominoClock",
|
||||
"declaration": {
|
||||
"name": "DominoClock",
|
||||
"module": "src/index.ts"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "js",
|
||||
"name": "default",
|
||||
"declaration": {
|
||||
"name": "DominoClock",
|
||||
"module": "src/index.ts"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"kind": "javascript-module",
|
||||
"path": "build/assets/index.c09ad8c7.js",
|
||||
"declarations": [
|
||||
{
|
||||
"kind": "variable",
|
||||
"name": "n3"
|
||||
},
|
||||
{
|
||||
"kind": "variable",
|
||||
"name": "e3",
|
||||
"default": "t2"
|
||||
}
|
||||
],
|
||||
"exports": [
|
||||
{
|
||||
"kind": "custom-element-definition",
|
||||
"name": "e3",
|
||||
"declaration": {
|
||||
"name": "n3",
|
||||
"module": "build/assets/index.c09ad8c7.js"
|
||||
}
|
||||
},
|
||||
{
|
||||
"kind": "custom-element-definition",
|
||||
"name": "e3",
|
||||
"declaration": {
|
||||
"name": "n4",
|
||||
"module": "build/assets/index.c09ad8c7.js"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
48
index.html
48
index.html
|
@ -7,10 +7,52 @@
|
|||
<meta http-equiv="X-UI-Compatible" content="ie-edge" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<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=Mulish&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
body {
|
||||
font-family: "Mulish", sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 1.36;
|
||||
letter-spacing: 0.012em;
|
||||
padding: 1rem;
|
||||
}
|
||||
p {
|
||||
color: #282828;
|
||||
max-width: 45ch;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Domino Clock:</h2>
|
||||
<domino-clock></domino-clock>
|
||||
<script type="module" src="/src/index.ts"></script>
|
||||
<h1>The Domino Clock</h1>
|
||||
<p>
|
||||
Sometime around 1996, when I was working for CompuServe, I saw a lovely demonstration of an
|
||||
electromechanical watch that used a sort of domino face to tell the time. I liked it so much that
|
||||
when I was looking for a toy project for my first Java Applet, I decided to implement the
|
||||
algorithm of the watch.
|
||||
</p>
|
||||
<p>
|
||||
25 years later, I found the source code buried somewhere in an old
|
||||
CD. I decided to try an re-implement the logic in Javascript, and to
|
||||
experiment with a
|
||||
<a href="https://developer.mozilla.org/en-US/docs/Web/Web_Components">Web Component</a>,
|
||||
using the <a href="https://lit.dev">lit-html library</a>. This is the result.
|
||||
</p>
|
||||
<p>
|
||||
It might seem a bit silly to have implemented the seconds face when the minutes face has a
|
||||
resolution of only five minutes, but it allows you to watch the clock in operation and learn what
|
||||
each symbol means: an empty face is 0 (12) and all four pips visible is 11 (or 55
|
||||
minutes/seconds).
|
||||
</p>
|
||||
<domino-clock></domino-clock>
|
||||
<p>
|
||||
The source code, both for this project and for the Java original,
|
||||
<a href="https://git.elfsternberg.com/elf/dominoclock"
|
||||
>are available at my personal Git repository</a
|
||||
>
|
||||
at <a href="https://elfsternberg.com">ElfSternberg.com</a>.
|
||||
</p>
|
||||
<script type="module" src="./src/index.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
//Draw a clock based upon the Domino display.
|
||||
|
||||
import java.applet.Applet;
|
||||
import java.util.*;
|
||||
import java.lang.*;
|
||||
import java.awt.*;
|
||||
|
||||
public class DominoClock extends Applet implements Runnable {
|
||||
|
||||
int StartX, StartY, SquareD, DotRad;
|
||||
Color bgColor, dominoColor, dotColor;
|
||||
Thread clock = null;
|
||||
String background;
|
||||
|
||||
public void init() {
|
||||
|
||||
try {
|
||||
background = getParameter( "bgcolor" );
|
||||
} catch (NullPointerException e) {};
|
||||
|
||||
if (( background == null ) ||
|
||||
( background.charAt(0) != '#' ) ||
|
||||
( background.length() != 7 )) {
|
||||
Integer rgbValue = new Integer(0);
|
||||
bgColor = new Color(rgbValue.intValue());
|
||||
} else {
|
||||
Integer rgbValue = new Integer(0);
|
||||
rgbValue = Integer.valueOf( background.substring(1,7), 16 );
|
||||
bgColor = new Color(rgbValue.intValue());
|
||||
}
|
||||
|
||||
try {
|
||||
background = getParameter( "dominocolor" );
|
||||
} catch (NullPointerException e) {};
|
||||
if (( background == null ) ||
|
||||
( background.charAt(0) != '#' ) ||
|
||||
( background.length() != 7 )) {
|
||||
Integer rgbValue = new Integer(14331680);
|
||||
dominoColor = new Color(rgbValue.intValue());
|
||||
} else {
|
||||
Integer rgbValue = new Integer(14331680);
|
||||
rgbValue = Integer.valueOf( background.substring(1,7), 16 );
|
||||
dominoColor = new Color(rgbValue.intValue());
|
||||
}
|
||||
|
||||
try {
|
||||
background = getParameter( "dotcolor" );
|
||||
} catch (NullPointerException e) {};
|
||||
if (( background == null ) ||
|
||||
( background.charAt(0) != '#' ) ||
|
||||
( background.length() != 7 )) {
|
||||
Integer rgbValue = new Integer(3100495);
|
||||
dotColor = new Color(rgbValue.intValue());
|
||||
} else {
|
||||
Integer rgbValue = new Integer(3100495);
|
||||
rgbValue = Integer.valueOf( background.substring(1,7), 16 );
|
||||
dotColor = new Color(rgbValue.intValue());
|
||||
}
|
||||
|
||||
Dimension D = size();
|
||||
|
||||
if (D.height > (3 * D.width)) {
|
||||
SquareD = D.width;
|
||||
StartX = 0;
|
||||
StartY = 0; // (D.height / 3) - SquareD;
|
||||
}
|
||||
else {
|
||||
SquareD = (D.height / 3);
|
||||
StartY = 0;
|
||||
StartX = 0; //.width - (SquareD / 3);
|
||||
}
|
||||
|
||||
DotRad = (int) (Math.sqrt(2 * SquareD * SquareD) / 6);
|
||||
}
|
||||
|
||||
public void paint(Graphics g) {
|
||||
|
||||
int Hour, Min, Sec;
|
||||
Date D = new Date();
|
||||
|
||||
Hour = D.getHours();
|
||||
if (Hour > 12) Hour = Hour - 12;
|
||||
Min = D.getMinutes();
|
||||
Sec = D.getSeconds();
|
||||
|
||||
Tock(StartX, StartY, Hour, g);
|
||||
Tock(StartX, StartY + SquareD, (Min / 5), g);
|
||||
Tock(StartX, StartY + (2 * SquareD), (Sec / 5), g);
|
||||
}
|
||||
|
||||
public void update(Graphics g) {
|
||||
paint(g); // Overridden to prevent flicker.
|
||||
}
|
||||
|
||||
public void start() {
|
||||
clock = new Thread(this);
|
||||
clock.start();
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
clock.stop();
|
||||
}
|
||||
|
||||
public void run() {
|
||||
while(true) {
|
||||
try {
|
||||
Thread.sleep(1000);
|
||||
}
|
||||
catch(InterruptedException e) {}
|
||||
repaint();
|
||||
}
|
||||
}
|
||||
|
||||
private void Tock(int StartX, int StartY, int time, Graphics g) {
|
||||
|
||||
g.setColor(dominoColor);
|
||||
g.fillRect(StartX, StartY, SquareD, SquareD);
|
||||
g.setColor(dotColor);
|
||||
|
||||
if ((time == 1) || (time == 5) || (time == 8) || (time == 9) || (time == 11))
|
||||
g.fillOval(StartX + DotRad, StartY + DotRad, DotRad, DotRad);
|
||||
|
||||
if ((time == 2) || (time == 5) || (time == 6) || (time == 10) || (time == 11))
|
||||
g.fillOval(StartX + SquareD - (2 * DotRad), StartY + DotRad, DotRad, DotRad);
|
||||
|
||||
if ((time == 3) || (time == 6) || (time == 7) || (time == 9) || (time == 11))
|
||||
g.fillOval(StartX + SquareD - (2 * DotRad), StartY + SquareD - (2 * DotRad), DotRad, DotRad);
|
||||
|
||||
if ((time == 4) || (time == 7) || (time == 8) || (time == 10) || (time == 11))
|
||||
g.fillOval(StartX + DotRad, StartY + SquareD - (2 * DotRad), DotRad, DotRad);
|
||||
|
||||
g.drawLine(StartX, StartY + SquareD - 1, StartX + SquareD, StartY + SquareD - 1);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public static void main(String args[]) {
|
||||
Frame f1 = new Frame("Domino Clock");
|
||||
DominoClock s1 = new DominoClock();
|
||||
|
||||
f1.add("Center", s1);
|
||||
f1.resize(300, 300);
|
||||
f1.show();
|
||||
|
||||
s1.init();
|
||||
s1.start();
|
||||
}
|
||||
|
||||
}
|
File diff suppressed because it is too large
Load Diff
70
package.json
70
package.json
|
@ -1,33 +1,41 @@
|
|||
{
|
||||
"name": "pendordate",
|
||||
"version": "1.0.0",
|
||||
"description": "The Pendor Clock, updated for 2021",
|
||||
"main": "build/index.js",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"lint": "eslint",
|
||||
"dev": "vite -m development",
|
||||
"test": "jest"
|
||||
},
|
||||
"author": "Elf M. Sternberg <elf.sternberg@gmail.com>",
|
||||
"license": "MPL-2.0",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.4.0",
|
||||
"@typescript-eslint/parser": "^5.4.0",
|
||||
"eslint": "^8.2.0",
|
||||
"eslint-config-airbnb": "^19.0.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-import-resolver-typescript": "^2.5.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"jest": "^27.3.1",
|
||||
"prettier": "^2.4.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"typescript": "^4.4.4",
|
||||
"vite": "^2.6.14",
|
||||
"vite-plugin-compression": "^0.3.5",
|
||||
"vite-plugin-ejs": "^1.4.3",
|
||||
"vite-plugin-eslint": "^1.3.0"
|
||||
}
|
||||
"name": "DominoClock",
|
||||
"version": "1.0.0",
|
||||
"description": "The Domino Clock",
|
||||
"main": "build/index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"analyze": "cem analyze --litelement",
|
||||
"build": "vite build --base='./'",
|
||||
"lint": "eslint --ignore-path .gitignore && prettier --check --ignore-path .gitignore src/**/*.ts",
|
||||
"fix": "eslint --fix --ignore-path .gitignore && prettier --write --ignore-path .gitignore src/**/*.ts",
|
||||
"dev": "vite -m development",
|
||||
"test": "jest"
|
||||
},
|
||||
"author": "Elf M. Sternberg <elf.sternberg@gmail.com>",
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"lit": "^2.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@custom-elements-manifest/analyzer": "^0.4.17",
|
||||
"@open-wc/eslint-config": "^9.2.1",
|
||||
"@types/jest": "^27.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^5.48.0",
|
||||
"@typescript-eslint/parser": "^5.48.0",
|
||||
"eslint": "^8.3.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-plugin-import": "^2.25.3",
|
||||
"eslint-plugin-jest": "^27.2.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"jest": "^27.3.1",
|
||||
"prettier": "^2.4.1",
|
||||
"typescript": "^4.5.2",
|
||||
"vite": "^2.6.14",
|
||||
"vite-plugin-compression": "^0.3.5",
|
||||
"vite-plugin-ejs": "^1.4.3",
|
||||
"vite-plugin-eslint": "^1.3.0"
|
||||
},
|
||||
"customElements": "custom-elements.json"
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, queryAll } from "lit/decorators.js";
|
||||
import PulseController from "./PulseController.ts";
|
||||
import "./DominoFace.ts";
|
||||
|
||||
function times() {
|
||||
const now = new Date();
|
||||
const rawHours = now.getHours();
|
||||
return {
|
||||
hour: rawHours > 12 ? rawHours - 12 : rawHours,
|
||||
minute: Math.floor(now.getUTCMinutes() / 5),
|
||||
second: Math.floor(now.getUTCSeconds() / 5),
|
||||
};
|
||||
}
|
||||
|
||||
@customElement("domino-clock")
|
||||
export class DominoClock extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
--dominoclock-default-direction: row;
|
||||
--dominoclock-default-face-gap: 0.25rem;
|
||||
}
|
||||
|
||||
.dominoclock {
|
||||
display: flex;
|
||||
flex-direction: var(--dominoclock-direction, var(--dominoclock-default-direction, row));
|
||||
gap: var(--dominoclock-face-gap, var(--dominoclock-default-face-gap, 0.25rem));
|
||||
}
|
||||
`;
|
||||
|
||||
@queryAll("domino-face") faces: HTMLCollection;
|
||||
|
||||
timer: PulseController;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.timer = new PulseController(this, 250, this.paint);
|
||||
}
|
||||
|
||||
paint() {
|
||||
const update = times();
|
||||
Array.from(this.faces).forEach(face => {
|
||||
const name = face.getAttribute("name");
|
||||
face.setAttribute("time", `${update[name]}`);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const update = times();
|
||||
return html` <div class="dominoclock">
|
||||
<domino-face name="hour" time=${update.hour}></domino-face>
|
||||
<domino-face name="minute" time=${update.minute}></domino-face>
|
||||
<domino-face name="second" time=${update.second}></domino-face>
|
||||
</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
export default DominoClock;
|
|
@ -0,0 +1,87 @@
|
|||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, queryAll } from "lit/decorators.js";
|
||||
|
||||
type Times = number[];
|
||||
type Coordinates = Times;
|
||||
const MATRIX: Coordinates[] = [
|
||||
[1, 5, 8, 9, 11],
|
||||
[2, 5, 6, 10, 11],
|
||||
[3, 6, 7, 9, 11],
|
||||
[4, 7, 8, 10, 11],
|
||||
];
|
||||
|
||||
// An element that takes a single attribute, "time," which is a number between 1
|
||||
// and 12; that number is scanned in the Coordinates matrix above to determine
|
||||
// which of four dots should be illuminated, reflecting 12 patterns that can be
|
||||
// memorized to represent the time. After rendering, if the attribute changes,
|
||||
// the active/hidden dots are recalculated.
|
||||
|
||||
@customElement("domino-face")
|
||||
export class DominoClockface extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
--dominoclock-default-dot-size: 1rem;
|
||||
--dominoclock-default-dot-color: #2f4f4f;
|
||||
--dominoclock-default-face-size: 5rem;
|
||||
--dominoclock-default-background-color: #daaf20;
|
||||
--dominoclock-default-border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.face {
|
||||
position: relative;
|
||||
width: var(--dominoclock-face-size, var(--dominoclock-default-face-size, 5rem));
|
||||
height: var(--dominoclock-face-size, var(--dominoclock-default-face-size, 5rem));
|
||||
background-color: var(
|
||||
--dominoclock-background-color,
|
||||
var(--dominoclock-default-background-color, #daaf20)
|
||||
);
|
||||
border: 1px solid #282828;
|
||||
border-radius: var(--dominoclock-border-radius, var(--dominoclock-default-border-radius, 0.5rem));
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center center;
|
||||
}
|
||||
|
||||
.dot > div {
|
||||
transition: opacity 0.6s ease;
|
||||
width: var(--dominoclock-dot-size, var(--dominoclock-default-dot-size, 1rem));
|
||||
height: var(--dominoclock-dot-size, var(--dominoclock-default-dot-size, 1rem));
|
||||
background-color: var(--dominoclock-dot-color, var(--dominoclock-default-dot-color, #2f4f4f));
|
||||
border-radius: calc(var(--dominoclock-dot-size, var(--dominoclock-default-dot-size, 1rem)) / 2);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.dot.active > div {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ type: String }) name = "hour";
|
||||
|
||||
@property({ type: String }) time = "12";
|
||||
|
||||
@queryAll(".dot") dots: HTMLCollection;
|
||||
|
||||
calculateDots() {
|
||||
const time = parseInt(this.time, 10);
|
||||
return [0, 1, 2, 3].map(i => MATRIX[i].includes(time));
|
||||
}
|
||||
|
||||
render() {
|
||||
const points = this.calculateDots();
|
||||
return html`
|
||||
<div class="face">
|
||||
${points.map(pos => html` <div class="dot${pos ? " active" : ""}"><div></div></div> `)}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
export default DominoClockface;
|
|
@ -0,0 +1,46 @@
|
|||
import type { ReactiveController, ReactiveControllerHost } from "lit";
|
||||
|
||||
type Pulse = () => void;
|
||||
|
||||
export class PulseController implements ReactiveController {
|
||||
host: ReactiveControllerHost;
|
||||
|
||||
interval: number = 0;
|
||||
|
||||
timer: number = 0;
|
||||
|
||||
// eslint-disable-next-line
|
||||
_onPulse: Pulse = () => undefined;
|
||||
|
||||
constructor(host: ReactiveControllerHost, interval: number = 1000, pulse?: Pulse) {
|
||||
this.host = host;
|
||||
this.host.addController(this);
|
||||
if (pulse) {
|
||||
this._onPulse = pulse;
|
||||
}
|
||||
this.handle = this.handle.bind(this);
|
||||
this.interval = interval;
|
||||
}
|
||||
|
||||
hostConnected() {
|
||||
this.timer = window.setTimeout(this.handle, this.interval);
|
||||
}
|
||||
|
||||
hostDisconnected() {
|
||||
if (this.timer !== 0) {
|
||||
window.clearTimout(this.timer);
|
||||
}
|
||||
this.timer = 0;
|
||||
}
|
||||
|
||||
set onPulse(cb: Pulse) {
|
||||
this._onPulse = cb;
|
||||
}
|
||||
|
||||
handle() {
|
||||
this._onPulse.call(this.host);
|
||||
this.timer = window.setTimeout(this.handle, this.interval);
|
||||
}
|
||||
}
|
||||
|
||||
export default PulseController;
|
132
src/index.ts
132
src/index.ts
|
@ -1,130 +1,4 @@
|
|||
type Times = number[];
|
||||
type Cats = [string, Times]
|
||||
const MATRIX: Cats[] = [["o0", [1, 5, 8, 9, 11]],
|
||||
["o1", [2, 5, 6, 10, 11]],
|
||||
["o2", [3, 6, 7, 9, 11]],
|
||||
["o3", [4, 7, 8, 10, 11]]];
|
||||
|
||||
const TEMPLATE = `
|
||||
<style>
|
||||
.dominoclock {
|
||||
display: flex;
|
||||
flex-direction: var(--dominoclock-direction, row);
|
||||
}
|
||||
.face {
|
||||
position: relative;
|
||||
width: var(--dominoclock-size, 5rem);
|
||||
height: var(--dominoclock-size, 5rem);
|
||||
background-color: var(--dominoclock-background, #daaf20);
|
||||
border: 1px solid #282828;
|
||||
border-radius: var(--domino-clock-borderradius, 0.5rem);
|
||||
|
||||
}
|
||||
|
||||
.face:not(:last-child) {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.dot {
|
||||
position: absolute;
|
||||
width: var(--dominoclock-dot-size, 1rem);
|
||||
height: var(--dominoclock-dot-size, 1rem);
|
||||
background-color: var(--dominoclock-dot-color, #2f4f4f);
|
||||
transition: opacity 0.3s ease;
|
||||
border-radius: calc(var(--dominoclock-dot-size, 1rem) / 2);
|
||||
opacity: 0;
|
||||
}
|
||||
.o0 {
|
||||
top: var(--dominoclock-dot-size, 1rem);
|
||||
left: var(--dominoclock-dot-size, 1rem);
|
||||
}
|
||||
.o1 {
|
||||
top: var(--dominoclock-dot-size, 1rem);
|
||||
left: calc(3 * var(--dominoclock-dot-size, 1rem));
|
||||
}
|
||||
.o2 {
|
||||
top: calc(3 * var(--dominoclock-dot-size, 1rem));
|
||||
left: calc(3 * var(--dominoclock-dot-size, 1rem));
|
||||
}
|
||||
.o3 {
|
||||
top: calc(3 * var(--dominoclock-dot-size, 1rem));
|
||||
left: var(--dominoclock-dot-size, 1rem);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="dominoclock">
|
||||
<div class="face" id="hours">
|
||||
<div class="dot o0" ></div>
|
||||
<div class="dot o1" ></div>
|
||||
<div class="dot o2" ></div>
|
||||
<div class="dot o3" ></div>
|
||||
</div>
|
||||
<div class="face" id="minutes">
|
||||
<div class="dot o0" ></div>
|
||||
<div class="dot o1" ></div>
|
||||
<div class="dot o2" ></div>
|
||||
<div class="dot o3" ></div>
|
||||
</div>
|
||||
<div class="face" id="seconds">
|
||||
<div class="dot o0" ></div>
|
||||
<div class="dot o1" ></div>
|
||||
<div class="dot o2" ></div>
|
||||
<div class="dot o3" ></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
type Face = "hours" | "minutes" | "seconds"
|
||||
|
||||
const isFace = (v: unknown): v is Face => {
|
||||
if (typeof v === "string" && ["hours", "minutes", "seconds"].includes(v)) { return true; }
|
||||
throw new Error(`Expected a Face, got ${v}`);
|
||||
}
|
||||
|
||||
class DominoClock extends HTMLElement {
|
||||
|
||||
elements: { [K in Face]: HTMLDivElement };
|
||||
|
||||
timer: number;
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.timer = 0;
|
||||
this.innerHTML = TEMPLATE;
|
||||
this.elements = Object.fromEntries(
|
||||
Array.from(this.getElementsByTagName('div'))
|
||||
.filter((element) => element.className === "face")
|
||||
.map((element) => [isFace(element.id) && element.id, element]));
|
||||
this.paint = this.paint.bind(this);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.timer = window.setTimeout(this.paint, 250)
|
||||
}
|
||||
|
||||
tock(board: Face, time: number) {
|
||||
const dots = Array.from(this.elements[board].getElementsByTagName('div'));
|
||||
dots.forEach((element) => element.style.opacity = "0");
|
||||
dots.forEach((element) => {
|
||||
const row = MATRIX.find((i) => Array.from(element.classList).includes(i[0]));
|
||||
if (!row) { console.log(`Didn't find a row for ${element.className}?`); return; }
|
||||
if (row[1].includes(time)) {
|
||||
element.style.opacity = "1"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
paint() {
|
||||
const now = new Date();
|
||||
const rawHours = now.getHours();
|
||||
const hours = rawHours > 12 ? rawHours - 12 : rawHours;
|
||||
this.tock("hours", hours);
|
||||
this.tock("minutes", Math.floor(now.getUTCMinutes() / 5));
|
||||
this.tock("seconds", Math.floor(now.getUTCSeconds() / 5));
|
||||
window.clearTimeout(this.timer);
|
||||
this.timer = window.setTimeout(this.paint, 250);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('domino-clock', DominoClock);
|
||||
import { DominoClock } from "./DominoClock.ts";
|
||||
|
||||
export { DominoClock };
|
||||
export default DominoClock;
|
||||
|
|
|
@ -1,30 +1,32 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"sourceMap": false,
|
||||
"noImplicitAny": false,
|
||||
"module": "esnext",
|
||||
"target": "es2019",
|
||||
"lib": ["es2019", "dom", "dom.iterable"],
|
||||
"removeComments": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"declaration": true,
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"baseUrl": "./",
|
||||
"esModuleInterop": true,
|
||||
"resolveJsonModule": true,
|
||||
"moduleResolution": "node",
|
||||
"downlevelIteration": true,
|
||||
"noUnusedLocals": true /* Report errors on unused locals. */,
|
||||
"experimentalDecorators": true,
|
||||
"noUnusedParameters": true /* Report errors on unused parameters. */,
|
||||
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
|
||||
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
|
||||
"noUncheckedIndexedAccess": true /* Include 'undefined' in index signature results */,
|
||||
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./src/**/*"]
|
||||
"compilerOptions": {
|
||||
"target": "es2018",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"noEmitOnError": true,
|
||||
"lib": ["es2017", "dom"],
|
||||
"strict": true,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": false,
|
||||
"importHelpers": true,
|
||||
"outDir": "dist",
|
||||
"sourceMap": true,
|
||||
"inlineSources": true,
|
||||
"baseUrl": "./",
|
||||
"rootDir": "./",
|
||||
"declaration": true,
|
||||
"incremental": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./src/**/*"]
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ const config = (mode) => ({
|
|||
|
||||
build: {
|
||||
outDir: "build",
|
||||
assetDir: "./assets",
|
||||
sourcemap: mode === "development",
|
||||
minify: !mode === "development",
|
||||
brotliSize: false,
|
||||
|
|
Loading…
Reference in New Issue