From 290479ebf869262c422b77d4a823d0c81598d633 Mon Sep 17 00:00:00 2001 From: Rei Date: Thu, 17 Oct 2024 00:52:03 +0900 Subject: [PATCH] feat: show canvas (#3) * feat: show canvas * feat: improve typecheck --- index.html | 1 + src/app.ts | 125 ++++++++++++++---------------------------- src/bind.ts | 52 ++++++++++++++++++ src/lib/findPeriod.ts | 36 ++++++++++++ src/main.ts | 45 ++++++++++++++- src/ui/table.ts | 43 +++++++++++++++ src/worker.ts | 40 +++++++++----- tsconfig.json | 1 + 8 files changed, 244 insertions(+), 99 deletions(-) create mode 100644 src/lib/findPeriod.ts create mode 100644 src/ui/table.ts diff --git a/index.html b/index.html index 5f50519..dc10a28 100644 --- a/index.html +++ b/index.html @@ -42,6 +42,7 @@

Oscilloscope

>
+
diff --git a/src/app.ts b/src/app.ts index 523dced..a48313c 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,93 +1,52 @@ import { BitGrid } from "@ca-ts/algo/bit"; -import type { WorkerRequestMessage, WorkerResponseMessage } from "./worker"; -import MyWorker from "./worker?worker"; - -const worker = new MyWorker(); - +import type { AnalyzeResult } from "./lib/analyzeOscillator"; +const cellSize = 10; export class App { - constructor() {} -} - -function post(req: WorkerRequestMessage) { - worker.postMessage(req); -} + histories: BitGrid[] | null = null; + private ctx: CanvasRenderingContext2D; + private gen = 0; + constructor(private $canvas: HTMLCanvasElement) { + const ctx = this.$canvas.getContext("2d"); + if (ctx == null) { + throw Error("Context"); + } + this.ctx = ctx; + + const update = () => { + this.render(); + requestAnimationFrame(update); + }; + + update(); + } -const $message = document.querySelector("#message") as HTMLElement; -const $outputTable = document.querySelector("#output-table") as HTMLElement; -worker.addEventListener("message", (e) => { - const message = e.data as WorkerResponseMessage; - $message.textContent = ""; - $outputTable.style.display = "none"; + render() { + if (this.histories == null) { + return; + } + + const ctx = this.ctx; + ctx.reset && ctx.reset(); + ctx.beginPath(); + this.histories[this.gen].forEachAlive((x, y) => { + ctx.rect(x * cellSize, y * cellSize, cellSize, cellSize); + }); + ctx.fill(); + this.gen = (this.gen + 1) % this.histories.length; + } - $analyzeButton.disabled = false; - if (message.kind === "response-error") { - $message.textContent = "Error: " + message.message; - } else { - $outputTable.style.display = "block"; - const data = message.data; + setup(data: AnalyzeResult) { + const $canvas = this.$canvas; const bitGridData = data.bitGridData; + $canvas.width = (bitGridData.width ?? 0) * cellSize; + $canvas.height = (bitGridData.height ?? 0) * cellSize; + $canvas.style.width = "100%"; const width32 = Math.ceil((bitGridData.width ?? 0) / 32); const height = bitGridData.height ?? 0; - const hisotories = bitGridData.histories.map( + + this.histories = bitGridData.histories.map( (h) => new BitGrid(width32, height, h) ); - - const $outputPeriod = document.querySelector( - "#output-period" - ) as HTMLElement; - $outputPeriod.textContent = data.period.toString(); - const $outputCellsMin = document.querySelector( - "#output-cells-min" - ) as HTMLElement; - $outputCellsMin.textContent = data.population.min.toString(); - - const $outputCellsMax = document.querySelector( - "#output-cells-max" - ) as HTMLElement; - $outputCellsMax.textContent = data.population.max.toString(); - - const $outputCellsAvg = document.querySelector( - "#output-cells-avg" - ) as HTMLElement; - $outputCellsAvg.textContent = data.population.avg.toString(); - - const $outputCellsMedian = document.querySelector( - "#output-cells-median" - ) as HTMLElement; - $outputCellsMedian.textContent = data.population.median.toString(); - - const $outputWidth = document.querySelector("#output-width") as HTMLElement; - $outputWidth.textContent = data.boundingBox.sizeX.toString(); - - const $outputHeight = document.querySelector( - "#output-height" - ) as HTMLElement; - $outputHeight.textContent = data.boundingBox.sizeY.toString(); - - const $outputArea = document.querySelector("#output-area") as HTMLElement; - $outputArea.textContent = ( - data.boundingBox.sizeX * data.boundingBox.sizeY - ).toString(); - - const $outputStator = document.querySelector( - "#output-stator" - ) as HTMLElement; - $outputStator.textContent = data.stator.toString(); - - const $outputRotor = document.querySelector("#output-rotor") as HTMLElement; - $outputRotor.textContent = data.rotor.toString(); - - const $outputVolatility = document.querySelector( - "#output-volatility" - ) as HTMLElement; - $outputVolatility.textContent = data.volatility; + this.gen = 0; } -}); - -const $input = document.querySelector("#input") as HTMLTextAreaElement; -const $analyzeButton = document.querySelector("#analyze") as HTMLButtonElement; - -$analyzeButton.addEventListener("click", () => { - $analyzeButton.disabled = true; - post({ kind: "request-analyze", rle: $input.value }); -}); +} diff --git a/src/bind.ts b/src/bind.ts index e69de29..e75443a 100644 --- a/src/bind.ts +++ b/src/bind.ts @@ -0,0 +1,52 @@ +export const $message = document.querySelector("#message") as HTMLElement; +export const $outputTable = document.querySelector( + "#output-table" +) as HTMLElement; + +export const $outputPeriod = document.querySelector( + "#output-period" +) as HTMLElement; +export const $outputCellsMin = document.querySelector( + "#output-cells-min" +) as HTMLElement; +export const $outputCellsMax = document.querySelector( + "#output-cells-max" +) as HTMLElement; +export const $outputCellsAvg = document.querySelector( + "#output-cells-avg" +) as HTMLElement; + +export const $outputCellsMedian = document.querySelector( + "#output-cells-median" +) as HTMLElement; + +export const $outputWidth = document.querySelector( + "#output-width" +) as HTMLElement; + +export const $outputHeight = document.querySelector( + "#output-height" +) as HTMLElement; + +export const $outputArea = document.querySelector( + "#output-area" +) as HTMLElement; + +export const $outputStator = document.querySelector( + "#output-stator" +) as HTMLElement; + +export const $outputRotor = document.querySelector( + "#output-rotor" +) as HTMLElement; + +export const $outputVolatility = document.querySelector( + "#output-volatility" +) as HTMLElement; + +export const $input = document.querySelector("#input") as HTMLTextAreaElement; +export const $analyzeButton = document.querySelector( + "#analyze" +) as HTMLButtonElement; + +export const $canvas = document.querySelector("#canvas") as HTMLCanvasElement; diff --git a/src/lib/findPeriod.ts b/src/lib/findPeriod.ts new file mode 100644 index 0000000..126e0c9 --- /dev/null +++ b/src/lib/findPeriod.ts @@ -0,0 +1,36 @@ +/** + * Find repeating pattern minimum periid. + * + * ```txt + * 000000 -> 1 + * 010101 -> 2 + * 110110 -> 3 + * 111110 -> 6 + * ``` + * + * KMP + */ +export function findPeriod(binary: (0 | 1)[]): number { + const n = binary.length; + const lps = new Array(n).fill(0); // Longest Prefix Suffix table + + // Build the LPS array + let length = 0; + for (let i = 1; i < n; i++) { + while (length > 0 && binary[i] !== binary[length]) { + length = lps[length - 1]; + } + if (binary[i] === binary[length]) { + length++; + } + lps[i] = length; + } + + const smallestPeriod = n - lps[n - 1]; + + // If the string is composed of a repeating pattern, the smallest period divides the total length perfectly + if (n % smallestPeriod === 0) { + return smallestPeriod; + } + return n; // Otherwise, the period is the entire string +} diff --git a/src/main.ts b/src/main.ts index 544612b..131b2a3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,47 @@ import "./style.css"; +import { BitGrid } from "@ca-ts/algo/bit"; +import type { WorkerRequestMessage, WorkerResponseMessage } from "./worker"; +import MyWorker from "./worker?worker"; +import { + $analyzeButton, + $canvas, + $input, + $message, + $outputTable, +} from "./bind"; +import { setTable } from "./ui/table"; + import { App } from "./app"; -const app = new App(); +const worker = new MyWorker(); + +const app = new App($canvas); + +function post(req: WorkerRequestMessage) { + worker.postMessage(req); +} + +worker.addEventListener("message", (e) => { + const message = e.data as WorkerResponseMessage; + $message.textContent = ""; + $outputTable.style.display = "none"; + + $analyzeButton.disabled = false; + if (message.kind === "response-error") { + $message.textContent = "Error: " + message.message; + } else { + $outputTable.style.display = "block"; + const data = message.data; + setTable(data); + app.setup(data); + const ctx = $canvas.getContext("2d"); + if (ctx == null) { + throw new Error("canvas"); + } + } +}); + +$analyzeButton.addEventListener("click", () => { + $analyzeButton.disabled = true; + post({ kind: "request-analyze", rle: $input.value }); +}); diff --git a/src/ui/table.ts b/src/ui/table.ts new file mode 100644 index 0000000..e3a3822 --- /dev/null +++ b/src/ui/table.ts @@ -0,0 +1,43 @@ +import type { AnalyzeResult } from "../lib/analyzeOscillator"; +import { + $analyzeButton, + $canvas, + $input, + $message, + $outputArea, + $outputCellsAvg, + $outputCellsMax, + $outputCellsMedian, + $outputCellsMin, + $outputHeight, + $outputPeriod, + $outputRotor, + $outputStator, + $outputVolatility, + $outputWidth, +} from "../bind"; +export function setTable(data: AnalyzeResult) { + $outputPeriod.textContent = data.period.toString(); + + $outputCellsMin.textContent = data.population.min.toString(); + + $outputCellsMax.textContent = data.population.max.toString(); + + $outputCellsAvg.textContent = data.population.avg.toString(); + + $outputCellsMedian.textContent = data.population.median.toString(); + + $outputWidth.textContent = data.boundingBox.sizeX.toString(); + + $outputHeight.textContent = data.boundingBox.sizeY.toString(); + + $outputArea.textContent = ( + data.boundingBox.sizeX * data.boundingBox.sizeY + ).toString(); + + $outputStator.textContent = data.stator.toString(); + + $outputRotor.textContent = data.rotor.toString(); + + $outputVolatility.textContent = data.volatility; +} diff --git a/src/worker.ts b/src/worker.ts index 41ed166..f61d67a 100644 --- a/src/worker.ts +++ b/src/worker.ts @@ -1,4 +1,4 @@ -import { analyzeOscillator, AnalyzeResult } from "./lib/analyzeOscillator"; +import { analyzeOscillator, type AnalyzeResult } from "./lib/analyzeOscillator"; import { parseRLE } from "@ca-ts/rle"; import { parseRule } from "@ca-ts/rule"; @@ -17,30 +17,31 @@ export type WorkerResponseMessage = message: string; }; -onmessage = (e) => { - const data = e.data as WorkerRequestMessage; - function post(res: WorkerResponseMessage) { - postMessage(res); - } +function handleRequest(data: WorkerRequestMessage): WorkerResponseMessage { let rle; let rule; + if (data.rle.trim() === "") { + return { + kind: "response-error", + message: "RLE is empty", + }; + } try { rle = parseRLE(data.rle); rule = parseRule(rle.ruleString); } catch (error) { - post({ + console.error(error); + return { kind: "response-error", message: "Unsupported rule or rle error", - }); - return; + }; } if (rule.type !== "outer-totalistic") { - post({ + return { kind: "response-error", message: "Unsupported rule", - }); - return; + }; } try { const result = analyzeOscillator( @@ -48,11 +49,20 @@ onmessage = (e) => { rule.transition, { maxGeneration: 100_000 } ); - post({ kind: "response-analyzed", data: result }); + return { kind: "response-analyzed", data: result }; } catch (error) { - post({ + console.error(error); + return { kind: "response-error", message: "Analyzation Error", - }); + }; + } +} + +onmessage = (e) => { + const data = e.data as WorkerRequestMessage; + function post(res: WorkerResponseMessage) { + postMessage(res); } + post(handleRequest(data)); }; diff --git a/tsconfig.json b/tsconfig.json index d10defa..2f2d01a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,6 +12,7 @@ "isolatedModules": true, "moduleDetection": "force", "noEmit": true, + "verbatimModuleSyntax": true, /* Linting */ "strict": true,