diff --git a/.forgejo/workflows/publish.yaml b/.forgejo/workflows/publish.yaml
index d192d28..b8f7077 100644
--- a/.forgejo/workflows/publish.yaml
+++ b/.forgejo/workflows/publish.yaml
@@ -23,5 +23,4 @@ jobs:
make
- name: Deploy
run: |
- npm install netlify-cli --save-dev
- npx netlify deploy --dir target --prod
\ No newline at end of file
+ netlify deploy --dir target --prod
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 188ebd1..2f7896d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1 @@
target/
-.vscode/
-.idea/
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
deleted file mode 100644
index 780b388..0000000
--- a/CLAUDE.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# Webtools
-
-This is a static site for a set of tools available on the web. The root of the site is located in "site", with index.html holding a list of available tools. Within each subdirectory of "site" is a separate tool. Each tool is self-contained with it's own index.html, CSS and JavaScript. Multipes of these files can be added if necessary. Note that each HTML has a standard header, with a typical title convension and importing "pico.min.css".
-
-Some tools can include a WASM file for more complex tasks. The WASM targets are built from Go files, with each cmd located as a sub directory in the "cmds" directory. Each one is built as a standalone WASM target.
-
-To build:
-
-- Run `build.wasm` to build the WASM targets. Additional targets will need to be added to this Make target
-- Run `build` to build both the WASM and prepare the site. The site will be available in a "target" directory.
-- Run `dev` to build the site and run a dev HTML server to test the site on port 8000. The dev server can be killed by sending a SIGINT signal.
diff --git a/Makefile b/Makefile
index d3f1b85..d9bdbe1 100644
--- a/Makefile
+++ b/Makefile
@@ -14,7 +14,6 @@ build.wasm:
GOOS=js GOARCH=wasm go build -o target/wasm/clocks.wasm ./cmds/clocks
GOOS=js GOARCH=wasm go build -o target/wasm/gotemplate.wasm ./cmds/gotemplate
GOOS=js GOARCH=wasm go build -o target/wasm/timestamps.wasm ./cmds/timestamps
- GOOS=js GOARCH=wasm go build -o target/wasm/android-icons.wasm ./cmds/android-icons
cp $(GOROOT)/lib/wasm/wasm_exec.js target/wasm/.
.Phony: build.site
diff --git a/cmds/android-icons/main.go b/cmds/android-icons/main.go
deleted file mode 100644
index cb3ddc8..0000000
--- a/cmds/android-icons/main.go
+++ /dev/null
@@ -1,144 +0,0 @@
-//go:build js
-
-package main
-
-import (
- "archive/zip"
- "bytes"
- "encoding/base64"
- "fmt"
- "image"
- "image/png"
- "strings"
- "syscall/js"
-
- "golang.org/x/image/draw"
-)
-
-type density struct {
- dir string
- size int
-}
-
-var densities = []density{
- {"mipmap-mdpi", 48},
- {"mipmap-hdpi", 72},
- {"mipmap-xhdpi", 96},
- {"mipmap-xxhdpi", 144},
- {"mipmap-xxxhdpi", 192},
-}
-
-func main() {
- js.Global().Set("prepareZip", js.FuncOf(prepareZip))
- <-make(chan struct{})
-}
-
-func prepareZip(this js.Value, args []js.Value) interface{} {
- // args[0] is an array of {name: string, data: string (base64 PNG data)}
- files := args[0]
- length := files.Length()
-
- if length == 0 {
- js.Global().Call("alert", "No files selected.")
- return nil
- }
-
- var buf bytes.Buffer
- zw := zip.NewWriter(&buf)
-
- for i := 0; i < length; i++ {
- file := files.Index(i)
- name := file.Get("name").String()
- dataURL := file.Get("data").String()
-
- // Strip data URL prefix (data:image/png;base64,)
- commaIdx := strings.Index(dataURL, ",")
- if commaIdx < 0 {
- setStatus(fmt.Sprintf("Invalid data for %s", name))
- return nil
- }
- b64Data := dataURL[commaIdx+1:]
-
- imgData, err := base64.StdEncoding.DecodeString(b64Data)
- if err != nil {
- setStatus(fmt.Sprintf("Failed to decode %s: %v", name, err))
- return nil
- }
-
- src, err := png.Decode(bytes.NewReader(imgData))
- if err != nil {
- setStatus(fmt.Sprintf("Failed to decode PNG %s: %v", name, err))
- return nil
- }
-
- // Strip extension from name for the base name
- baseName := name
- if strings.HasSuffix(strings.ToLower(baseName), ".png") {
- baseName = baseName[:len(baseName)-4]
- }
-
- for _, d := range densities {
- resized := resizeImage(src, d.size)
-
- var pngBuf bytes.Buffer
- if err := png.Encode(&pngBuf, resized); err != nil {
- setStatus(fmt.Sprintf("Failed to encode %s/%s.png: %v", d.dir, baseName, err))
- return nil
- }
-
- path := fmt.Sprintf("%s/%s.png", d.dir, baseName)
- w, err := zw.Create(path)
- if err != nil {
- setStatus(fmt.Sprintf("Failed to create zip entry %s: %v", path, err))
- return nil
- }
- if _, err := w.Write(pngBuf.Bytes()); err != nil {
- setStatus(fmt.Sprintf("Failed to write zip entry %s: %v", path, err))
- return nil
- }
- }
- }
-
- if err := zw.Close(); err != nil {
- setStatus(fmt.Sprintf("Failed to finalize zip: %v", err))
- return nil
- }
-
- // Convert zip bytes to a JS Uint8Array and trigger download
- zipBytes := buf.Bytes()
- jsArray := js.Global().Get("Uint8Array").New(len(zipBytes))
- js.CopyBytesToJS(jsArray, zipBytes)
-
- // Create Blob and download
- blobParts := js.Global().Get("Array").New()
- blobParts.Call("push", jsArray)
- blobOpts := js.Global().Get("Object").New()
- blobOpts.Set("type", "application/zip")
- blob := js.Global().Get("Blob").New(blobParts, blobOpts)
-
- url := js.Global().Get("URL").Call("createObjectURL", blob)
- anchor := js.Global().Get("document").Call("createElement", "a")
- anchor.Set("href", url)
- anchor.Set("download", "android-icons.zip")
- js.Global().Get("document").Get("body").Call("appendChild", anchor)
- anchor.Call("click")
- anchor.Call("remove")
- js.Global().Get("URL").Call("revokeObjectURL", url)
-
- setStatus(fmt.Sprintf("Done! Prepared icons for %d image(s).", length))
- return nil
-}
-
-func resizeImage(src image.Image, size int) image.Image {
- dst := image.NewNRGBA(image.Rect(0, 0, size, size))
- draw.CatmullRom.Scale(dst, dst.Bounds(), src, src.Bounds(), draw.Over, nil)
- return dst
-}
-
-func setStatus(msg string) {
- querySelector("#status").Set("innerText", msg)
-}
-
-func querySelector(query string) js.Value {
- return js.Global().Get("document").Call("querySelector", query)
-}
diff --git a/go.mod b/go.mod
index 4e30240..7f220ff 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,5 @@
module github.com/lmika/webtools
-go 1.25.0
+go 1.24.3
-require (
- github.com/alecthomas/participle/v2 v2.1.4 // indirect
- golang.org/x/image v0.37.0 // indirect
-)
+require github.com/alecthomas/participle/v2 v2.1.4 // indirect
diff --git a/go.sum b/go.sum
index 8e1c4f0..bff288a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,2 @@
github.com/alecthomas/participle/v2 v2.1.4 h1:W/H79S8Sat/krZ3el6sQMvMaahJ+XcM9WSI2naI7w2U=
github.com/alecthomas/participle/v2 v2.1.4/go.mod h1:8tqVbpTX20Ru4NfYQgZf4mP18eXPTBViyMWiArNEgGI=
-golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
-golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
diff --git a/site/android-icons/index.html b/site/android-icons/index.html
deleted file mode 100644
index d30965e..0000000
--- a/site/android-icons/index.html
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
-
- Android Icon Resizer - Tools
-
-
-
-
-
-
-
-
-
-
-
-
-
Circle Crop Preview
-
-
-
-
-
-
-
-
-
-
diff --git a/site/android-icons/main.js b/site/android-icons/main.js
deleted file mode 100644
index 056050b..0000000
--- a/site/android-icons/main.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import "/wasm/wasm_exec.js";
-
-const go = new Go();
-let wasmReady = false;
-
-WebAssembly.instantiateStreaming(fetch("/wasm/android-icons.wasm"), go.importObject)
- .then((result) => {
- go.run(result.instance);
- wasmReady = true;
- });
-
-const fileInput = document.getElementById("file-input");
-const previewArea = document.getElementById("preview-area");
-const prepareBtn = document.getElementById("prepare-btn");
-const statusEl = document.getElementById("status");
-const circlePreview = document.getElementById("circle-preview");
-const circleCanvas = document.getElementById("circle-canvas");
-
-let loadedFiles = [];
-
-function renderCirclePreview() {
- if (loadedFiles.length === 0) {
- circlePreview.style.display = "none";
- return;
- }
-
- const nameLower = (f) => f.name.toLowerCase();
-
- let bgFile = loadedFiles.find((f) => /back/.test(nameLower(f)));
- let fgFile = loadedFiles.find((f) => !/back/.test(nameLower(f)) && !/mono/.test(nameLower(f)));
-
- // Fallbacks: if only one image or no "back" image, use the first file
- if (!bgFile && !fgFile) {
- bgFile = loadedFiles[0];
- } else if (!bgFile) {
- bgFile = fgFile;
- fgFile = null;
- }
-
- const size = 192;
- circleCanvas.width = size;
- circleCanvas.height = size;
- const ctx = circleCanvas.getContext("2d");
- ctx.clearRect(0, 0, size, size);
-
- // Clip to circle
- ctx.save();
- ctx.beginPath();
- ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
- ctx.closePath();
- ctx.clip();
-
- const drawLayer = (src) => {
- return new Promise((resolve) => {
- const img = new Image();
- img.onload = () => {
- ctx.drawImage(img, 0, 0, size, size);
- resolve();
- };
- img.onerror = resolve;
- img.src = src;
- });
- };
-
- drawLayer(bgFile.data).then(() => {
- if (fgFile && fgFile !== bgFile) {
- return drawLayer(fgFile.data);
- }
- }).then(() => {
- ctx.restore();
-
- // Draw border ring
- ctx.beginPath();
- ctx.arc(size / 2, size / 2, size / 2 - 1.5, 0, Math.PI * 2);
- ctx.closePath();
- ctx.strokeStyle = "rgba(128, 128, 128, 0.5)";
- ctx.lineWidth = 3;
- ctx.stroke();
-
- circlePreview.style.display = "block";
- });
-}
-
-fileInput.addEventListener("change", () => {
- const files = Array.from(fileInput.files);
- loadedFiles = [];
- previewArea.innerHTML = "";
- circlePreview.style.display = "none";
-
- if (files.length === 0) {
- prepareBtn.disabled = true;
- return;
- }
-
- files.forEach((file) => {
- const reader = new FileReader();
- reader.onload = (e) => {
- const dataURL = e.target.result;
- loadedFiles.push({ name: file.name, data: dataURL });
-
- const item = document.createElement("div");
- item.className = "preview-item";
-
- const img = document.createElement("img");
- img.src = dataURL;
-
- const label = document.createElement("span");
- label.textContent = file.name;
-
- item.appendChild(img);
- item.appendChild(label);
- previewArea.appendChild(item);
-
- if (loadedFiles.length === files.length) {
- prepareBtn.disabled = false;
- renderCirclePreview();
- }
- };
- reader.readAsDataURL(file);
- });
-});
-
-prepareBtn.addEventListener("click", () => {
- if (!wasmReady) {
- statusEl.textContent = "WASM module is still loading, please wait...";
- return;
- }
- if (loadedFiles.length === 0) {
- return;
- }
- statusEl.textContent = "Preparing...";
- // Pass file data to Go WASM
- setTimeout(() => {
- prepareZip(loadedFiles);
- }, 50);
-});
diff --git a/site/android-icons/style.css b/site/android-icons/style.css
deleted file mode 100644
index 1aea483..0000000
--- a/site/android-icons/style.css
+++ /dev/null
@@ -1,54 +0,0 @@
-#preview-area {
- display: flex;
- flex-wrap: wrap;
- gap: 1rem;
- margin: 1rem 0;
-}
-
-.preview-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.25rem;
-}
-
-.preview-item img {
- width: 96px;
- height: 96px;
- object-fit: contain;
- border: 1px solid var(--pico-muted-border-color);
- border-radius: 4px;
- background: repeating-conic-gradient(#eee 0% 25%, #fff 0% 50%) 50% / 16px 16px;
-}
-
-.preview-item span {
- font-size: 0.8rem;
- color: var(--pico-muted-color);
- word-break: break-all;
- max-width: 96px;
- text-align: center;
-}
-
-.actions {
- margin: 1rem 0;
-}
-
-#circle-preview {
- display: none;
- margin: 1rem 0;
-}
-
-#circle-preview h3 {
- margin-bottom: 0.5rem;
-}
-
-#circle-canvas {
- background: repeating-conic-gradient(#eee 0% 25%, #fff 0% 50%) 50% / 16px 16px;
- border-radius: 50%;
- box-shadow: 0 0 0 3px var(--pico-muted-border-color);
-}
-
-#status {
- margin-top: 0.5rem;
- font-style: italic;
-}
diff --git a/site/finska/index.html b/site/finska/index.html
deleted file mode 100644
index b68d16b..0000000
--- a/site/finska/index.html
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
- Finska Score Card - Tools
-
-
-
-
-
- Finska Scorecard
-
-
-
-
-
- |
-
- |
-
-
- |
-
-
-
-
-
-
- |
-
- |
- |
-
-
- |
- |
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/site/finska/scripts/controllers.js b/site/finska/scripts/controllers.js
deleted file mode 100644
index acb321f..0000000
--- a/site/finska/scripts/controllers.js
+++ /dev/null
@@ -1,253 +0,0 @@
-import { Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
-import { Scorecard, getStoreDAO } from "./models.js";
-
-const storeDao = getStoreDAO();
-
-export class FinskaScorecardController extends Controller {
- static targets = ["score1Input", "score2Input", "scoreTable", "team1Header", "team2Header"];
- static values = {
- maxScore: Number,
- overflowScoreTo: Number
- }
-
- connect() {
- let rules = {
- maxScore: Math.floor(this.maxScoreValue),
- overflowScoreTo: Math.floor(this.overflowScoreToValue)
- };
-
- this._undoStack = [];
- this._scorecard = storeDao.loadOrCreate(rules);
- this._loadTeamNames();
- this.updateTable();
- }
-
- _loadTeamNames() {
- const stored1 = localStorage.getItem('finska-team1-name');
- const stored2 = localStorage.getItem('finska-team2-name');
- this._team1Name = stored1 || 'Team A';
- this._team2Name = stored2 || 'Team B';
- this._renderTeamHeader(this.team1HeaderTarget, this._team1Name, 'editTeam1');
- this._renderTeamHeader(this.team2HeaderTarget, this._team2Name, 'editTeam2');
- }
-
- _renderTeamHeader(td, name, editAction) {
- td.innerHTML = ``;
- }
-
- _renderEditHeader(td, currentName, teamNum) {
- td.innerHTML = ``;
- td.querySelector('input').focus();
- td.querySelector('input').select();
- }
-
- editTeam1() {
- this._renderEditHeader(this.team1HeaderTarget, this._team1Name, '1');
- }
-
- editTeam2() {
- this._renderEditHeader(this.team2HeaderTarget, this._team2Name, '2');
- }
-
- confirmTeam1() {
- const input = this.team1HeaderTarget.querySelector('input');
- const newName = input.value.trim() || 'Team A';
- this._team1Name = newName;
- localStorage.setItem('finska-team1-name', newName);
- this._renderTeamHeader(this.team1HeaderTarget, this._team1Name, 'editTeam1');
- }
-
- confirmTeam2() {
- const input = this.team2HeaderTarget.querySelector('input');
- const newName = input.value.trim() || 'Team B';
- this._team2Name = newName;
- localStorage.setItem('finska-team2-name', newName);
- this._renderTeamHeader(this.team2HeaderTarget, this._team2Name, 'editTeam2');
- }
-
- cancelTeam1() {
- this._renderTeamHeader(this.team1HeaderTarget, this._team1Name, 'editTeam1');
- }
-
- cancelTeam2() {
- this._renderTeamHeader(this.team2HeaderTarget, this._team2Name, 'editTeam2');
- }
-
- editKeyDown1(e) {
- this._editKeyDown(e, this.confirmTeam1.bind(this), this.cancelTeam1.bind(this));
- }
-
- editKeyDown2(e) {
- this._editKeyDown(e, this.confirmTeam2.bind(this), this.cancelTeam2.bind(this));
- }
-
- _editKeyDown(e, confirmFn, cancelFn) {
- if (e.key === 'Enter') {
- e.preventDefault();
- confirmFn();
- } else if (e.key === 'Escape') {
- e.preventDefault();
- cancelFn();
- }
- }
-
- _escapeHtml(str) {
- const div = document.createElement('div');
- div.textContent = str;
- return div.innerHTML;
- }
-
- _escapeAttr(str) {
- return str.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>');
- }
-
- updateTable() {
- let tableBody = this.scoreTableTarget;
- let tableRows = tableBody.querySelectorAll("tr");
- let pairs = this._scorecard.pairs();
-
- for (let pairIndex = 0; pairIndex < pairs.length; pairIndex++) {
- let tableRow;
- if (pairIndex >= tableRows.length) {
- tableRow = this._appendRow();
- } else {
- tableRow = tableRows[pairIndex];
- }
-
- this._updateRow(tableRow, pairs[pairIndex])
- }
-
- // Remove any extra rows
- for (let i = pairs.length; i < tableRows.length; i++) {
- tableBody.removeChild(tableRows[i]);
- }
-
- console.log(JSON.stringify(this._scorecard.toJson()));
- }
-
- _updateRow(tableRow, pair) {
- let tds = tableRow.querySelectorAll("td");
-
- this._updateCell(pair.p1, tds[0], tds[1]);
- this._updateCell(pair.p2, tds[2], tds[3]);
- }
-
- _updateCell(score, scoreCell, totalCell) {
- scoreCell.classList.value = "";
- totalCell.classList.value = "";
-
- if (score != null) {
- scoreCell.textContent = score.score;
- totalCell.textContent = score.total;
-
- if (score.score === 0) {
- scoreCell.classList.add("score-foul");
- totalCell.classList.add("score-foul");
- } else if (score.wasOverflow) {
- scoreCell.classList.add("score-overflow");
- totalCell.classList.add("score-overflow");
- } else if (score.wasWin) {
- scoreCell.classList.add("score-win");
- totalCell.classList.add("score-win");
- }
- } else {
- scoreCell.textContent = "";
- totalCell.textContent = "";
- }
- }
-
- _appendRow() {
- let newRow = document.createElement("tr");
- newRow.classList.add("score-entry");
-
- for (let i = 0; i < 4; i++) {
- newRow.appendChild(document.createElement("td"));
- }
-
- this.scoreTableTarget.appendChild(newRow);
- return newRow;
- }
-
- addScore1() {
- this._addScore(this.score1InputTarget, this.score2InputTarget,
- this._scorecard.addPlayer1Score.bind(this._scorecard),
- this._scorecard.removeLastPlayer1Score.bind(this._scorecard));
- }
-
- addScore2() {
- this._addScore(this.score2InputTarget, this.score1InputTarget,
- this._scorecard.addPlayer2Score.bind(this._scorecard),
- this._scorecard.removeLastPlayer2Score.bind(this._scorecard));
-
- }
-
- _addScore(inputElem, focusToInputElem, addScoreFn, queueUndoFn) {
- let score = parseInt(inputElem.value);
- if (isNaN(score)) {
- score = 0;
- }
-
- addScoreFn(score);
- this._undoStack.push(queueUndoFn);
-
- inputElem.value = "";
-
- storeDao.save(this._scorecard);
- this.updateTable();
-
- focusToInputElem.focus();
- }
-
- score1KeyDown(e) {
- this._handleKeyDown(e, this.addScore1.bind(this));
- }
-
- score2KeyDown(e) {
- this._handleKeyDown(e, this.addScore2.bind(this));
- }
-
- _handleKeyDown(e, addScoreFn) {
- if (e.key === "Enter") {
- e.preventDefault();
- addScoreFn();
- }
- }
-
- undoLast() {
- if (this._undoStack.length === 0) {
- return;
- }
- if (!confirm("Really undo last move?")) {
- return;
- }
-
- (this._undoStack.pop())();
-
- storeDao.save(this._scorecard);
- this.updateTable();
- }
-
- resetAll() {
- if (!confirm("Really reset?")) {
- return;
- }
-
- this._scorecard.reset();
- storeDao.clear();
- this._undoStack = [];
- this._team1Name = 'Team A';
- this._team2Name = 'Team B';
- localStorage.removeItem('finska-team1-name');
- localStorage.removeItem('finska-team2-name');
- this._renderTeamHeader(this.team1HeaderTarget, this._team1Name, 'editTeam1');
- this._renderTeamHeader(this.team2HeaderTarget, this._team2Name, 'editTeam2');
- this.updateTable();
- }
-}
diff --git a/site/finska/scripts/main.js b/site/finska/scripts/main.js
deleted file mode 100644
index 34ece68..0000000
--- a/site/finska/scripts/main.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import { Application } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js";
-import { FinskaScorecardController } from "./controllers.js"
-
-window.Stimulus = Application.start();
-
-window.Stimulus.register('finska-scorecard', FinskaScorecardController);
-
diff --git a/site/finska/scripts/models.js b/site/finska/scripts/models.js
deleted file mode 100644
index 321122d..0000000
--- a/site/finska/scripts/models.js
+++ /dev/null
@@ -1,152 +0,0 @@
-export class ScoreEntry {
- constructor(score, total, foul, wasOverflow, wasWin) {
- this.score = score;
- this.total = total;
- this.fouls = foul;
- this.wasOverflow = wasOverflow;
- this.wasWin = wasWin;
- }
-};
-
-export class Scorecard {
-
- constructor(rules) {
- this.rules = rules;
- this.reset();
- }
-
- reset() {
- this._player1Scores = [];
- this._player2Scores = [];
- }
-
- addPlayer1Score(newScore) {
- this._addScore(this._player1Scores, newScore);
- }
-
- removeLastPlayer1Score() {
- this._player1Scores.pop();
- }
-
- addPlayer2Score(newScore) {
- this._addScore(this._player2Scores, newScore);
- }
-
- removeLastPlayer2Score() {
- this._player2Scores.pop();
- }
-
- _addScore(playerScores, newScore) {
- let lastEntry;
- if (playerScores.length === 0) {
- lastEntry = new ScoreEntry(0, 0, 0, false, false);
- } else {
- lastEntry = playerScores[playerScores.length - 1];
- }
-
- let newEntry = this._newEntryFromPrevious(newScore, lastEntry);
-
- playerScores.push(newEntry);
- }
-
- length() {
- return Math.max(this._player1Scores.length, this._player2Scores.length);
- }
-
- pairs() {
- let pairs = [];
-
- for (let i = 0; i < this.length(); i++) {
- pairs.push({
- p1: (i < this._player1Scores.length ? this._player1Scores[i] : null),
- p2: (i < this._player2Scores.length ? this._player2Scores[i] : null),
- })
- }
-
- return pairs;
- }
-
- _newEntryFromPrevious(score, previousScoreEntry) {
- if (previousScoreEntry === null) {
- return new ScoreEntry(score, score, (score === 0 ? 1 : 0));
- }
-
- let wasOverflow = false, wasWin = false;
- let newTotal = previousScoreEntry.total + score;
-
- if (newTotal === this.rules.maxScore) {
- wasWin = true;
- } else if (newTotal > this.rules.maxScore) {
- newTotal = this.rules.overflowScoreTo;
- wasOverflow = true;
- }
-
- let newFouls = previousScoreEntry.foul;
- if (score === 0) {
- newFouls++;
- }
-
- return new ScoreEntry(score, newTotal, newFouls, wasOverflow, wasWin);
- }
-
- toJson() {
- return {
- "version": 1,
- "rules": this.rules,
- "p1": { "scores": this._player1Scores.map(p => p.score) },
- "p2": { "scores": this._player2Scores.map(p => p.score) },
- };
- }
-
- static fromJson(o) {
- let scorecard = new Scorecard(o.rules);
- o["p1"]["scores"].forEach(x => scorecard.addPlayer1Score(x));
- o["p2"]["scores"].forEach(x => scorecard.addPlayer2Score(x));
- return scorecard;
- }
-}
-
-class StoreDAO {
-
- constructor(localStorage) {
- this._localStorage = localStorage;
- }
-
- save(scoreCard) {
- this._localStorage.setItem('finska-scorecard', JSON.stringify(scoreCard.toJson()));
- }
-
- loadOrCreate(rules) {
- try {
- console.log("Loading scorecard");
- let scoreCardJson = this._localStorage.getItem('finska-scorecard');
- if (scoreCardJson !== null) {
- return Scorecard.fromJson(JSON.parse(scoreCardJson));
- }
- } catch (e) {
- console.log(`Could not restore game: ${e}`);
- }
-
- return new Scorecard(rules);
- }
-
- clear() {
- this._localStorage.removeItem('finska-scorecard');
- }
-}
-
-class DummyStoreDAO {
- save(scoreCard) { }
- loadOrCreate(rules) {
- return new Scorecard(rules);
- }
- clear() { }
-}
-
-export function getStoreDAO() {
- if (!!window.localStorage) {
- return new StoreDAO(window.localStorage);
- } else {
- return new DummyStoreDAO();
- }
-}
\ No newline at end of file
diff --git a/site/finska/style.css b/site/finska/style.css
deleted file mode 100644
index 9bf0432..0000000
--- a/site/finska/style.css
+++ /dev/null
@@ -1,84 +0,0 @@
-tfoot input {
- border-width: 1px;
-}
-
-.score-foul {
- color: #861D13;
- background-color: #F8DCD6;
- font-weight: bold;
-}
-
-.score-overflow {
- color: #5B4200;
- background-color: #FCEFD9;
- font-weight: bold;
-}
-
-.score-win {
- color: #394D00;
- background-color: #DEFC85;
- font-weight: bold;
-}
-
-.team-header {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 0.25rem;
-}
-
-.team-header .team-name {
- font-weight: bold;
-}
-
-.team-header .edit-btn {
- background: none;
- border: none;
- cursor: pointer;
- padding: 0.1rem 0.3rem;
- font-size: 0.9rem;
- margin: 0;
- width: auto;
- line-height: 1;
- opacity: 0.5;
-}
-
-.team-header .edit-btn:hover {
- opacity: 1;
-}
-
-.team-header-edit {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 0.25rem;
-}
-
-.team-header-edit input {
- border-width: 1px;
- margin: 0;
- padding: 0.2rem 0.4rem;
- font-size: 0.9rem;
- width: 6rem;
- text-align: center;
-}
-
-.team-header-edit .confirm-btn,
-.team-header-edit .cancel-btn {
- background: none;
- border: none;
- cursor: pointer;
- padding: 0.1rem 0.3rem;
- font-size: 1rem;
- margin: 0;
- width: auto;
- line-height: 1;
-}
-
-.team-header-edit .confirm-btn {
- color: #2e7d32;
-}
-
-.team-header-edit .cancel-btn {
- color: #c62828;
-}
diff --git a/site/freelens-logo/script.js b/site/freelens-logo/script.js
index 7be5111..0404e63 100644
--- a/site/freelens-logo/script.js
+++ b/site/freelens-logo/script.js
@@ -43,7 +43,6 @@
const availableWidth = width - PADDING * 2;
const availableHeight = height - PADDING * 2;
const halfHeight = availableHeight / 2;
- const threeQuartersHeight = availableHeight * 3 / 4;
ctx.fillStyle = 'white';
ctx.textAlign = 'center';
@@ -58,7 +57,7 @@
ctx.font = `bold ${fontSize}px sans-serif`;
ctx.fillText(lower, width / 2, height - PADDING - halfHeight / 2 + 4);
} else {
- let fontSize = fitText(upper, availableWidth, threeQuartersHeight);
+ let fontSize = fitText(upper, availableWidth, halfHeight);
ctx.font = `bold ${fontSize}px sans-serif`;
ctx.fillText(upper, width / 2, height / 2);
}
diff --git a/site/gradient-image/index.html b/site/gradient-image/index.html
deleted file mode 100644
index 2c1179c..0000000
--- a/site/gradient-image/index.html
+++ /dev/null
@@ -1,50 +0,0 @@
-
-
-
-
-
- Gradient Image - Tools
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/site/gradient-image/main.js b/site/gradient-image/main.js
deleted file mode 100644
index 4271b7e..0000000
--- a/site/gradient-image/main.js
+++ /dev/null
@@ -1,70 +0,0 @@
-const PREVIEW_SIZE = 256;
-
-const fromColorEl = document.getElementById("from-color");
-const toColorEl = document.getElementById("to-color");
-const gradientTypeEl = document.getElementById("gradient-type");
-const rotationEl = document.getElementById("rotation");
-const rotationValueEl = document.getElementById("rotation-value");
-const imageSizeEl = document.getElementById("image-size");
-const canvas = document.getElementById("preview-canvas");
-const downloadBtn = document.getElementById("download-btn");
-
-function drawGradient(ctx, size) {
- const fromColor = fromColorEl.value;
- const toColor = toColorEl.value;
- const type = gradientTypeEl.value;
- const rotation = parseInt(rotationEl.value);
-
- const cx = size / 2;
- const cy = size / 2;
-
- let gradient;
- if (type === "radial") {
- const radius = size / 2;
- gradient = ctx.createRadialGradient(cx, cy, 0, cx, cy, radius);
- } else {
- const angle = (rotation * Math.PI) / 180;
- const len = size / 2;
- const dx = Math.cos(angle) * len;
- const dy = Math.sin(angle) * len;
- gradient = ctx.createLinearGradient(cx - dx, cy - dy, cx + dx, cy + dy);
- }
-
- gradient.addColorStop(0, fromColor);
- gradient.addColorStop(1, toColor);
-
- ctx.fillStyle = gradient;
- ctx.fillRect(0, 0, size, size);
-}
-
-function renderPreview() {
- canvas.width = PREVIEW_SIZE;
- canvas.height = PREVIEW_SIZE;
- const ctx = canvas.getContext("2d");
- drawGradient(ctx, PREVIEW_SIZE);
-}
-
-function download() {
- const size = parseInt(imageSizeEl.value);
- const offscreen = document.createElement("canvas");
- offscreen.width = size;
- offscreen.height = size;
- const ctx = offscreen.getContext("2d");
- drawGradient(ctx, size);
-
- const link = document.createElement("a");
- link.download = `gradient-${size}x${size}.png`;
- link.href = offscreen.toDataURL("image/png");
- link.click();
-}
-
-fromColorEl.addEventListener("input", renderPreview);
-toColorEl.addEventListener("input", renderPreview);
-gradientTypeEl.addEventListener("input", renderPreview);
-rotationEl.addEventListener("input", () => {
- rotationValueEl.textContent = rotationEl.value;
- renderPreview();
-});
-downloadBtn.addEventListener("click", download);
-
-renderPreview();
diff --git a/site/gradient-image/style.css b/site/gradient-image/style.css
deleted file mode 100644
index bfe7c47..0000000
--- a/site/gradient-image/style.css
+++ /dev/null
@@ -1,22 +0,0 @@
-.controls {
- display: flex;
- flex-direction: column;
-}
-
-.preview {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 1rem;
-}
-
-#preview-canvas {
- width: 256px;
- height: 256px;
- border: 1px solid var(--pico-muted-border-color);
- border-radius: 4px;
-}
-
-#download-btn {
- width: 100%;
-}
diff --git a/site/hex-color/index.html b/site/hex-color/index.html
deleted file mode 100644
index 921b912..0000000
--- a/site/hex-color/index.html
+++ /dev/null
@@ -1,37 +0,0 @@
-
-
-
-
-
- Hex Color Converter - Tools
-
-
-
-
-
-
-
-
-
-
-
-
- | Preview |
- Hex |
- Normalized (R, G, B, A) |
- Action |
-
-
-
-
-
-
-
-
-
-
diff --git a/site/hex-color/script.js b/site/hex-color/script.js
deleted file mode 100644
index 07852ed..0000000
--- a/site/hex-color/script.js
+++ /dev/null
@@ -1,116 +0,0 @@
-async function addHexFromClipboard() {
- try {
- const text = await navigator.clipboard.readText();
- const hex = text.trim();
-
- // Parse hex color
- const color = parseHexColor(hex);
- if (!color) {
- alert('Invalid hex color in clipboard. Expected format: #RRGGBB or #RRGGBBAA');
- return;
- }
-
- // Add row to table
- addColorRow(hex, color);
-
- // Show table if hidden
- document.getElementById('colorTable').classList.remove('hidden');
- } catch (err) {
- alert('Failed to read from clipboard: ' + err.message);
- }
-}
-
-document.getElementById('addHexBtn').addEventListener('click', addHexFromClipboard);
-
-document.getElementById('showHiddenBtn').addEventListener('click', () => {
- const rows = document.querySelectorAll('#colorTableBody tr.hidden');
- rows.forEach(row => row.classList.remove('hidden'));
- updateShowHiddenButton();
-});
-
-document.addEventListener('keydown', (e) => {
- if (e.key === 'p' || e.key === 'P') {
- addHexFromClipboard();
- }
-});
-
-function parseHexColor(hex) {
- // Remove leading hash if present
- const cleanHex = hex.startsWith('#') ? hex.slice(1) : hex;
-
- // Validate hex string (6 or 8 characters)
- if (!/^[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/.test(cleanHex)) {
- return null;
- }
-
- // Parse components
- const r = parseInt(cleanHex.slice(0, 2), 16) / 255;
- const g = parseInt(cleanHex.slice(2, 4), 16) / 255;
- const b = parseInt(cleanHex.slice(4, 6), 16) / 255;
- const a = cleanHex.length === 8 ? parseInt(cleanHex.slice(6, 8), 16) / 255 : 1.0;
-
- return { r, g, b, a, hex: '#' + cleanHex };
-}
-
-function addColorRow(originalHex, color) {
- const tbody = document.getElementById('colorTableBody');
- const row = tbody.insertRow();
-
- // Preview cell
- const previewCell = row.insertCell();
- const preview = document.createElement('div');
- preview.style.width = '40px';
- preview.style.height = '40px';
- preview.style.backgroundColor = `rgba(${color.r * 255}, ${color.g * 255}, ${color.b * 255}, ${color.a})`;
- preview.style.border = '1px solid #ccc';
- preview.style.borderRadius = '4px';
- previewCell.appendChild(preview);
-
- // Hex cell
- const hexCell = row.insertCell();
- hexCell.textContent = color.hex;
-
- // Normalized components cell
- const normalizedCell = row.insertCell();
- const normalizedText = `${color.r.toFixed(1)}, ${color.g.toFixed(1)}, ${color.b.toFixed(1)}, ${color.a.toFixed(1)}`;
- normalizedCell.textContent = normalizedText;
-
- // Action cell with copy and hide buttons
- const actionCell = row.insertCell();
-
- const copyBtn = document.createElement('button');
- copyBtn.textContent = 'Copy';
- copyBtn.className = 'secondary';
- copyBtn.addEventListener('click', () => {
- navigator.clipboard.writeText(normalizedText).then(() => {
- const originalText = copyBtn.textContent;
- copyBtn.textContent = 'Copied!';
- setTimeout(() => {
- copyBtn.textContent = originalText;
- }, 1500);
- }).catch(err => {
- alert('Failed to copy: ' + err.message);
- });
- });
- actionCell.appendChild(copyBtn);
-
- const hideBtn = document.createElement('button');
- hideBtn.textContent = 'Hide';
- hideBtn.className = 'secondary';
- hideBtn.addEventListener('click', () => {
- row.classList.add('hidden');
- updateShowHiddenButton();
- });
- actionCell.appendChild(hideBtn);
-}
-
-function updateShowHiddenButton() {
- const hiddenRows = document.querySelectorAll('#colorTableBody tr.hidden');
- const showHiddenBtn = document.getElementById('showHiddenBtn');
-
- if (hiddenRows.length > 0) {
- showHiddenBtn.classList.remove('hidden');
- } else {
- showHiddenBtn.classList.add('hidden');
- }
-}
diff --git a/site/hex-color/style.css b/site/hex-color/style.css
deleted file mode 100644
index a5b0997..0000000
--- a/site/hex-color/style.css
+++ /dev/null
@@ -1,20 +0,0 @@
-.hidden {
- display: none;
-}
-
-#colorTable {
- margin-top: 2rem;
-}
-
-#colorTable button {
- margin: 0;
- margin-right: 0.5rem;
-}
-
-#colorTable td {
- vertical-align: middle;
-}
-
-#showHiddenBtn {
- margin-top: 1rem;
-}
diff --git a/site/image-inner-resize/index.html b/site/image-inner-resize/index.html
deleted file mode 100644
index 521be20..0000000
--- a/site/image-inner-resize/index.html
+++ /dev/null
@@ -1,32 +0,0 @@
-
-
-
-
-
- Image Inner Resize - Tools
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/site/image-inner-resize/main.js b/site/image-inner-resize/main.js
deleted file mode 100644
index a22fc52..0000000
--- a/site/image-inner-resize/main.js
+++ /dev/null
@@ -1,57 +0,0 @@
-const fileInput = document.getElementById("file-input");
-const scaleEl = document.getElementById("scale");
-const scaleValueEl = document.getElementById("scale-value");
-const canvas = document.getElementById("preview-canvas");
-const downloadBtn = document.getElementById("download-btn");
-
-let sourceImage = null;
-
-function render() {
- if (!sourceImage) return;
-
- const w = sourceImage.naturalWidth;
- const h = sourceImage.naturalHeight;
- const scale = parseInt(scaleEl.value) / 100;
-
- canvas.width = w;
- canvas.height = h;
- const ctx = canvas.getContext("2d");
- ctx.clearRect(0, 0, w, h);
-
- const sw = w * scale;
- const sh = h * scale;
- const sx = (w - sw) / 2;
- const sy = (h - sh) / 2;
-
- ctx.drawImage(sourceImage, sx, sy, sw, sh);
-}
-
-fileInput.addEventListener("change", () => {
- const file = fileInput.files[0];
- if (!file) return;
-
- const reader = new FileReader();
- reader.onload = (e) => {
- const img = new Image();
- img.onload = () => {
- sourceImage = img;
- downloadBtn.disabled = false;
- render();
- };
- img.src = e.target.result;
- };
- reader.readAsDataURL(file);
-});
-
-scaleEl.addEventListener("input", () => {
- scaleValueEl.textContent = scaleEl.value;
- render();
-});
-
-downloadBtn.addEventListener("click", () => {
- if (!sourceImage) return;
- const link = document.createElement("a");
- link.download = "resized.png";
- link.href = canvas.toDataURL("image/png");
- link.click();
-});
diff --git a/site/image-inner-resize/style.css b/site/image-inner-resize/style.css
deleted file mode 100644
index dbaa0f5..0000000
--- a/site/image-inner-resize/style.css
+++ /dev/null
@@ -1,23 +0,0 @@
-.controls {
- display: flex;
- flex-direction: column;
-}
-
-.preview {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 1rem;
-}
-
-#preview-canvas {
- max-width: 100%;
- max-height: 512px;
- border: 1px solid var(--pico-muted-border-color);
- border-radius: 4px;
- background: repeating-conic-gradient(#eee 0% 25%, #fff 0% 50%) 50% / 16px 16px;
-}
-
-#download-btn {
- width: 100%;
-}
diff --git a/site/index.html b/site/index.html
index 437258e..c71b5cc 100644
--- a/site/index.html
+++ b/site/index.html
@@ -24,16 +24,6 @@
Gradient Bands
Two-letter Country Codes
Timestamp Converter
- Generic Scorecard - 2 Players
- Generic Scorecard - 4 Players
- Mahjong Scorecard
- Finska Scorecard
- Mental Arithmatic Game
- Neon Snake: vibe-coded by Google Gemini
- Hex Color Converter
- Android Icon Resizer
- Gradient Image
- Image Inner Resize
-
-
-
-
diff --git a/site/mahjong/guide.html b/site/mahjong/guide.html
index 3133de2..43ceeb7 100644
--- a/site/mahjong/guide.html
+++ b/site/mahjong/guide.html
@@ -32,92 +32,66 @@
m |
Mahjong |
- 20 |
ps |
Pung of simples |
- 4 |
pt |
Pung of terminals |
- 8 |
ph |
Pung of honours |
- 8 |
xps |
Exposed pung of simples |
- 2 |
xpt |
Exposed pung of terminals |
- 4 |
xph |
Exposed pung of honours |
- 4 |
ks |
Kong of simples |
- 16 |
kt |
Kong of terminals |
- 32 |
kh |
Kong of honours |
- 32 |
xks |
Exposed kong of simples |
- 8 |
xkt |
Exposed kong of terminals |
- 16 |
xkh |
Exposed kong of honours |
- 16 |
pd |
Pair of dragons |
- 2 |
pw |
Pair of winds (either player or prevailing) |
- 2 |
b |
Bonus (flower or season) |
- 2 |
-
-
- p |
- Point (used for adjusting scores) |
- 1 |
-
-
- d |
- Penalty (used for adjusting scores) |
- -1 |
diff --git a/site/mahjong/index.html b/site/mahjong/index.html
index 3ac7778..e1dce3f 100644
--- a/site/mahjong/index.html
+++ b/site/mahjong/index.html
@@ -78,21 +78,15 @@
+
+
-
-
diff --git a/site/mahjong/main.js b/site/mahjong/main.js
index 584bcec..f9ba03c 100644
--- a/site/mahjong/main.js
+++ b/site/mahjong/main.js
@@ -2,10 +2,13 @@ import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/di
import { GameState, getGameState, setGameState, calculateScore } from "./models.js";
function emitGameStateEvent(details) {
- let ev = new CustomEvent("gamestatechanged", { detail: details });
- window.dispatchEvent(ev);
+ // window.setTimeout(() => {
+ let ev = new CustomEvent("gamestatechanged", { detail: details });
+ window.dispatchEvent(ev);
+ // }, 1);
}
+
window.Stimulus = Application.start();
Stimulus.register("scorecard", class extends Controller {
@@ -15,6 +18,8 @@ Stimulus.register("scorecard", class extends Controller {
}
connect() {
+ // this._rebuildTable();
+ // this.element.classList.remove("hidden");
}
handleGameState(ev) {
@@ -70,8 +75,8 @@ Stimulus.register("scorecard", class extends Controller {
let td = document.createElement("td");
td.textContent = c.s;
if (c.w) {
- td.textContent += " đ";
td.classList.add("winner");
+ td.textContent += " đ";
}
tr.append(td);
}
@@ -89,7 +94,7 @@ Stimulus.register("scorecard", class extends Controller {
this.element.querySelector("table.scorecard").replaceWith(tbl);
- this.prevailingTarget.textContent = `Prevailing: ${gameState.prevailing}`;
+ this.prevailingTarget.textContent = `Prevailing: ${gameState.prevaling}`;
}
});
@@ -97,6 +102,7 @@ Stimulus.register("newgame", class extends Controller {
static targets = ["playerName"];
connect() {
+ this.element.classList.remove("hidden");
}
handleGameState(ev) {
@@ -127,9 +133,10 @@ Stimulus.register("newgame", class extends Controller {
});
Stimulus.register("endround", class extends Controller {
- static targets = ["form", "input", "wind", "prevailing"];
+ static targets = ["form", "input", "wind"];
connect() {
+ // this.element.classList.remove("hidden");
}
handleGameState(ev) {
@@ -141,7 +148,6 @@ Stimulus.register("endround", class extends Controller {
break;
case "endround":
this._prepForms();
- this._updatePreview();
this.element.classList.remove("hidden");
break;
}
@@ -149,10 +155,7 @@ Stimulus.register("endround", class extends Controller {
updatePreview(ev) {
ev.preventDefault();
- this._updatePreview();
- }
- _updatePreview() {
let scoreExprs = this._readScoreExprs();
let nextRound = getGameState().determineNextRound(scoreExprs);
@@ -160,23 +163,15 @@ Stimulus.register("endround", class extends Controller {
let previewElem = this.element.querySelector(`.preview[data-player="${i}"]`);
previewElem.textContent = nextRound.roundScores[i].score;
if (parseInt(i) === nextRound.roundWinner) {
- previewElem.classList.add("winner");
- previewElem.textContent += " đ";
- } else {
- previewElem.classList.remove("winner");
+ previewElem.textContent += " đ";
}
}
if (nextRound.windsBump) {
- this.windTarget.textContent = ` will be East next round`;
+ this.windTarget.textContent = `${getGameState().players[nextRound.nextRoundEast].name} will be East next round`;
} else {
- this.windTarget.textContent = ` will remain East next round`;
+ this.windTarget.textContent = `${getGameState().players[nextRound.nextRoundEast].name} will remain East next round`;
}
- let nextWindNameElem = document.createElement("strong");
- nextWindNameElem.textContent = getGameState().players[nextRound.nextRoundEast].name;
- this.windTarget.prepend(nextWindNameElem);
-
- this.prevailingTarget.textContent = `Next prevailing: ${nextRound.nextPrevailing}`;
}
goBack(ev) {
@@ -229,13 +224,4 @@ Stimulus.register("endround", class extends Controller {
}
return scoreExprs;
}
-});
-
-document.addEventListener("DOMContentLoaded", () => {
- let game = getGameState();
- if (game !== null) {
- emitGameStateEvent({mode: "startgame"});
- } else {
- emitGameStateEvent({mode: "newgame"});
- }
-})
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/site/mahjong/models.js b/site/mahjong/models.js
index 6789bc4..48d1762 100644
--- a/site/mahjong/models.js
+++ b/site/mahjong/models.js
@@ -7,24 +7,22 @@ const windDistributions = [
];
const scoreTokens = {
- 'm': 20,
- 'ps': 4,
- 'pt': 8,
- 'ph': 8,
- 'xps': 2,
- 'xpt': 4,
- 'xph': 4,
- 'ks': 16,
- 'kt': 32,
- 'kh': 32,
- 'xks': 8,
- 'xkt': 16,
- 'xkh': 16,
- 'pd': 2,
- 'pw': 2,
- 'b': 2,
- 'p': 1,
- 'd': -1,
+ 'm': 1,
+ 'ps': 1,
+ 'pt': 1,
+ 'ph': 1,
+ 'xps': 1,
+ 'xpt': 1,
+ 'xph': 1,
+ 'ks': 1,
+ 'kt': 1,
+ 'kh': 1,
+ 'xks': 1,
+ 'xkt': 1,
+ 'xkh': 1,
+ 'pd': 1,
+ 'pw': 1,
+ 'b': 1,
}
function parseTokens(str) {
@@ -69,7 +67,7 @@ export class GameState {
this.players = players;
this.rounds = rounds;
this.bumpWind = 0;
- this.prevailing = "E";
+ this.prevaling = "E";
}
static newGame(playerNames) {
@@ -80,21 +78,7 @@ export class GameState {
}
let rounds = [];
- let gs = new GameState(players, rounds);
- gs.save();
- return gs;
- }
-
- static load() {
- let json = localStorage.getItem('mahjong-scorecard');
- if (!json) {
- return null;
- }
- let o = JSON.parse(json);
- let gs = new GameState(o.p, o.r);
- gs.bumpWind = o.b;
- gs.prevailing = o.w;
- return gs;
+ return new GameState(players, rounds);
}
determineNextRound(playerScoreExprs) {
@@ -108,7 +92,7 @@ export class GameState {
}
let windsBump = roundWinner !== currentEast;
- let nextPrevailing = this.prevailing;
+ let nextPrevailing = this.prevaling;
if (windsBump) {
nextPrevailing = windDistributions[3][((this.bumpWind + 1) / this.players.length)|0];
}
@@ -149,24 +133,14 @@ export class GameState {
this.players[pi].wind = windDistribution[i];
}
- this.prevailing = nr.nextPrevailing;
+ this.prevaling = nr.nextPrevailing;
if (nr.windsBump) {
this.bumpWind++;
}
- this.save();
- }
-
- save() {
- localStorage.setItem('mahjong-scorecard', JSON.stringify({
- p: this.players,
- r: this.rounds,
- b: this.bumpWind,
- w: this.prevailing,
- }));
}
}
-let gameState = GameState.load();
+let gameState = new GameState();
export function getGameState() {
return gameState;
diff --git a/site/mahjong/style.css b/site/mahjong/style.css
index 4f1eed1..1940ccb 100644
--- a/site/mahjong/style.css
+++ b/site/mahjong/style.css
@@ -1,7 +1,3 @@
-.container {
- margin-block-end: 20px;
-}
-
.hidden {
display: none;
}
@@ -22,41 +18,4 @@
gap: 10px;
justify-content: space-between;
align-items: center;
- margin-block-end: 12px;
-}
-
-@media (max-width: 600px) {
- .btns {
- flex-direction: column;
- gap: 20px;
- }
-
- .btns :nth-child(1) {
- align-self: start;
- }
-
- .btns :nth-child(2) {
- align-self: end;
- }
-}
-
-.winner {
- color: #9B2318;
- font-weight: bold;
-}
-
-
-th {
- background: #E2E2E2;
-}
-
-@media (prefers-color-scheme: dark) {
- th {
- background: #474747;
- }
-
- .winner {
- color: #F06048;
- font-weight: bold;
- }
}
\ No newline at end of file
diff --git a/site/mental-arithmatic/index.html b/site/mental-arithmatic/index.html
deleted file mode 100644
index 16c4321..0000000
--- a/site/mental-arithmatic/index.html
+++ /dev/null
@@ -1,61 +0,0 @@
-
-
-
-
-
-
Mental Arithmatic - Tools
-
-
-
-
-
-