| Server IP : 167.235.67.158 / Your IP : 216.73.216.95 Web Server : Apache System : Linux ubuntu-8gb-nbg1-1 6.8.0-111-generic #111-Ubuntu SMP PREEMPT_DYNAMIC Sat Apr 11 23:16:02 UTC 2026 x86_64 User : upstairsbar.co.uk ( 982) PHP Version : 8.3.6 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/bc.the-word.com/oliver/ |
Upload File : |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<title>Ray Numbers</title>
<style>
:root {
color-scheme: dark;
--panel: rgba(17, 24, 39, 0.86);
--panel-strong: rgba(13, 18, 29, 0.94);
--line: rgba(255, 255, 255, 0.16);
--text: #f8fafc;
--muted: #a8b3c7;
--accent: #5eead4;
--remaining: #22c55e;
--removed: #ef4444;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
* {
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
margin: 0;
overflow: hidden;
background: #080c13;
}
body {
color: var(--text);
user-select: none;
}
#board {
position: fixed;
inset: 0;
width: 100vw;
height: 100vh;
cursor: grab;
touch-action: none;
background: #080c13;
}
#board.is-dragging {
cursor: grabbing;
}
.panel {
width: min(320px, calc(100vw - 32px));
display: flex;
flex-direction: column;
overflow: hidden;
border: 1px solid var(--line);
background: var(--panel);
backdrop-filter: blur(16px);
box-shadow: 0 18px 48px rgba(0, 0, 0, 0.28);
}
.panels {
position: fixed;
top: 16px;
left: 16px;
display: flex;
flex-direction: column;
gap: 12px;
width: min(320px, calc(100vw - 32px));
max-height: calc(100vh - 32px);
overflow-y: auto;
scrollbar-width: thin;
}
.panel__head {
width: 100%;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 12px;
align-items: end;
padding: 14px 14px 12px;
border: 0;
border-bottom: 1px solid var(--line);
color: var(--text);
background: var(--panel-strong);
font: inherit;
text-align: left;
cursor: pointer;
}
.panel__head:hover,
.panel__head:focus-visible {
background: rgba(24, 34, 52, 0.96);
outline: none;
}
.panel__head-text {
min-width: 0;
}
.panel__toggle {
color: var(--muted);
font-size: 12px;
font-weight: 800;
line-height: 1;
padding-bottom: 2px;
}
.panel__toggle::after {
content: "▼";
color: var(--accent);
}
.panel.is-collapsed .panel__head {
border-bottom: 0;
}
.panel.is-collapsed .panel__toggle::after {
content: "▶";
}
.panel.is-collapsed .panel__body {
display: none;
}
.panel__eyebrow {
display: block;
margin: 0 0 4px;
color: var(--accent);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.panel__title {
display: block;
margin: 0;
font-size: 22px;
font-weight: 850;
line-height: 1.15;
}
.panel__stats {
padding: 14px 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.stat {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
}
.stat__label {
display: flex;
align-items: center;
gap: 8px;
color: var(--muted);
font-size: 13px;
font-weight: 740;
}
.stat__dot {
width: 12px;
height: 12px;
border-radius: 3px;
}
.stat__dot--remaining {
background: var(--remaining);
}
.stat__dot--removed {
background: var(--removed);
}
.stat__value {
font-size: 17px;
font-weight: 850;
font-variant-numeric: tabular-nums;
}
.panel__action {
padding: 0 16px 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
.remove-button {
width: 100%;
min-height: 50px;
display: grid;
place-items: center;
border: 1px solid rgba(239, 68, 68, 0.55);
border-radius: 10px;
color: #fff;
background: linear-gradient(180deg, #f05252, #dc2626);
font: inherit;
font-size: 17px;
font-weight: 850;
cursor: pointer;
transition: transform 0.06s ease, filter 0.15s ease;
}
.remove-button:hover,
.remove-button:focus-visible {
filter: brightness(1.08);
outline: none;
}
.remove-button:active {
transform: translateY(1px);
}
.remove-button:disabled {
cursor: not-allowed;
border-color: var(--line);
color: var(--muted);
background: rgba(255, 255, 255, 0.08);
filter: none;
transform: none;
}
.reset-button {
width: 100%;
min-height: 44px;
display: grid;
place-items: center;
border: 1px solid var(--line);
border-radius: 10px;
color: var(--text);
background: rgba(255, 255, 255, 0.08);
font: inherit;
font-size: 15px;
font-weight: 760;
cursor: pointer;
transition: transform 0.06s ease, background 0.15s ease;
}
.reset-button:hover,
.reset-button:focus-visible {
border-color: rgba(94, 234, 212, 0.55);
background: rgba(20, 184, 166, 0.16);
outline: none;
}
.reset-button:active {
transform: translateY(1px);
}
.panel__field {
padding: 14px 16px 0;
}
.panel__label {
display: block;
margin-bottom: 8px;
color: var(--muted);
font-size: 13px;
font-weight: 740;
}
.panel__input {
width: 100%;
min-height: 46px;
padding: 0 14px;
border: 1px solid var(--line);
border-radius: 10px;
color: var(--text);
background: rgba(255, 255, 255, 0.06);
font: inherit;
font-size: 18px;
font-weight: 800;
font-variant-numeric: tabular-nums;
user-select: text;
}
.panel__input:hover {
border-color: rgba(255, 255, 255, 0.24);
}
.panel__input:focus {
border-color: rgba(94, 234, 212, 0.72);
background: rgba(20, 184, 166, 0.1);
outline: none;
}
.panel__input::placeholder {
color: rgba(168, 179, 199, 0.72);
font-weight: 600;
}
.panel__hint {
margin: 10px 16px 0;
color: var(--muted);
font-size: 12px;
font-weight: 740;
font-variant-numeric: tabular-nums;
}
.run-button {
width: 100%;
min-height: 50px;
display: grid;
place-items: center;
border: 1px solid rgba(94, 234, 212, 0.55);
border-radius: 10px;
color: #042f2e;
background: linear-gradient(180deg, #5eead4, #14b8a6);
font: inherit;
font-size: 17px;
font-weight: 850;
cursor: pointer;
transition: transform 0.06s ease, filter 0.15s ease;
}
.run-button:hover,
.run-button:focus-visible {
filter: brightness(1.08);
outline: none;
}
.run-button:active {
transform: translateY(1px);
}
.run-button:disabled {
cursor: not-allowed;
border-color: var(--line);
color: var(--muted);
background: rgba(255, 255, 255, 0.08);
filter: none;
transform: none;
}
.zoom-panel {
position: fixed;
top: 16px;
right: 16px;
display: grid;
grid-auto-flow: column;
gap: 8px;
padding: 8px;
border: 1px solid var(--line);
background: var(--panel);
backdrop-filter: blur(16px);
}
.zoom-panel__button {
width: 42px;
height: 42px;
display: grid;
place-items: center;
border: 1px solid var(--line);
border-radius: 8px;
color: var(--text);
background: rgba(255, 255, 255, 0.1);
font: inherit;
font-size: 18px;
font-weight: 900;
cursor: pointer;
}
.zoom-panel__button:hover,
.zoom-panel__button:focus-visible {
border-color: rgba(94, 234, 212, 0.72);
background: rgba(20, 184, 166, 0.22);
outline: none;
}
.status-pill {
position: fixed;
right: 16px;
bottom: 16px;
max-width: 250px;
padding: 8px 10px;
border: 1px solid var(--line);
background: var(--panel);
color: var(--muted);
font-size: 12px;
font-weight: 760;
backdrop-filter: blur(16px);
}
@media (max-width: 760px) {
.panels {
top: 10px;
left: 10px;
width: min(290px, calc(100vw - 20px));
max-height: calc(100vh - 20px);
}
.panel {
width: 100%;
}
.panel__title {
font-size: 19px;
}
.zoom-panel {
top: auto;
right: 10px;
bottom: 14px;
grid-auto-flow: row;
}
.status-pill {
display: none;
}
}
</style>
</head>
<body>
<canvas id="board" aria-label="One million number grid"></canvas>
<div class="panels">
<section class="panel" id="mainPanel" data-panel="main" aria-label="Ray numbers controls">
<button class="panel__head" id="mainPanelToggle" type="button" aria-expanded="true" aria-controls="mainPanelBody">
<span class="panel__head-text">
<span class="panel__eyebrow">Ray Numbers</span>
<span class="panel__title" id="iterationTitle">Not divisible by 2</span>
</span>
<span class="panel__toggle" aria-hidden="true"></span>
</button>
<div class="panel__body" id="mainPanelBody">
<div class="panel__stats" aria-live="polite">
<div class="stat">
<span class="stat__label"><span class="stat__dot stat__dot--remaining"></span>Numbers remaining</span>
<span class="stat__value" id="remainingValue">1,000,000</span>
</div>
<div class="stat">
<span class="stat__label"><span class="stat__dot stat__dot--removed"></span>Numbers removed</span>
<span class="stat__value" id="removedValue">0</span>
</div>
</div>
<div class="panel__action">
<button class="remove-button" id="removeButton" type="button">Remove</button>
<button class="reset-button" id="resetButton" type="button">Reset</button>
</div>
</div>
</section>
<section class="panel" id="customPanel" data-panel="custom" aria-label="Try one divisor">
<button class="panel__head" id="customPanelToggle" type="button" aria-expanded="true" aria-controls="customPanelBody">
<span class="panel__head-text">
<span class="panel__eyebrow">Quick try</span>
<span class="panel__title" id="customIterationTitle">Not divisible by …</span>
</span>
<span class="panel__toggle" aria-hidden="true"></span>
</button>
<div class="panel__body" id="customPanelBody">
<div class="panel__field">
<label class="panel__label" for="customDivisorInput">Divisor</label>
<input class="panel__input" id="customDivisorInput" type="number" inputmode="numeric" min="2" step="1" placeholder="e.g. 5">
</div>
<div class="panel__stats" aria-live="polite">
<div class="stat">
<span class="stat__label"><span class="stat__dot stat__dot--remaining"></span>Numbers remaining</span>
<span class="stat__value" id="customRemainingValue">1,000,000</span>
</div>
<div class="stat">
<span class="stat__label"><span class="stat__dot stat__dot--removed"></span>Numbers removed</span>
<span class="stat__value" id="customRemovedValue">0</span>
</div>
</div>
<div class="panel__action">
<button class="run-button" id="runCustomButton" type="button" disabled>Run</button>
</div>
</div>
</section>
<section class="panel" id="gridPanel" data-panel="grid" aria-label="Grid settings">
<button class="panel__head" id="gridPanelToggle" type="button" aria-expanded="true" aria-controls="gridPanelBody">
<span class="panel__head-text">
<span class="panel__eyebrow">Grid</span>
<span class="panel__title" id="gridSizeTitle">1,000,000 numbers</span>
</span>
<span class="panel__toggle" aria-hidden="true"></span>
</button>
<div class="panel__body" id="gridPanelBody">
<div class="panel__field">
<label class="panel__label" for="gridSizeInput">Number of items</label>
<input class="panel__input" id="gridSizeInput" type="number" inputmode="numeric" min="1" step="1" value="1000000" placeholder="e.g. 10000">
</div>
<p class="panel__hint" id="gridDimensionsLabel">1000 × 1000 grid</p>
<div class="panel__action">
<button class="run-button" id="applyGridButton" type="button">Update grid</button>
</div>
</div>
</section>
</div>
<section class="zoom-panel" aria-label="Board view controls">
<button class="zoom-panel__button" id="zoomOutButton" type="button" title="Zoom out" aria-label="Zoom out">-</button>
<button class="zoom-panel__button" id="resetViewButton" type="button" title="Reset view" aria-label="Reset view">⌂</button>
<button class="zoom-panel__button" id="zoomInButton" type="button" title="Zoom in" aria-label="Zoom in">+</button>
</section>
<div class="status-pill" id="viewStatus">Zoom 100%</div>
<script>
(() => {
"use strict";
const DEFAULT_TILE_COUNT = 1_000_000;
const MIN_TILE_COUNT = 1;
const MAX_TILE_COUNT = 4_000_000;
const TILE_NUMBER_MIN_ZOOM = 28;
const STATE_KEY = "rayNumbers:v2";
const MIN_ZOOM = 0.2;
const MAX_ZOOM = 96;
const ZOOM_STEP = 1.22;
const COLOR_REMAINING = { r: 34, g: 197, b: 94 };
const COLOR_REMOVED = { r: 190, g: 32, b: 38 };
const COLOR_EMPTY = { r: 18, g: 24, b: 35 };
const canvas = document.getElementById("board");
const ctx = canvas.getContext("2d", { alpha: false });
const iterationTitle = document.getElementById("iterationTitle");
const remainingValue = document.getElementById("remainingValue");
const removedValue = document.getElementById("removedValue");
const removeButton = document.getElementById("removeButton");
const resetButton = document.getElementById("resetButton");
const customIterationTitle = document.getElementById("customIterationTitle");
const customDivisorInput = document.getElementById("customDivisorInput");
const customRemainingValue = document.getElementById("customRemainingValue");
const customRemovedValue = document.getElementById("customRemovedValue");
const runCustomButton = document.getElementById("runCustomButton");
const gridSizeTitle = document.getElementById("gridSizeTitle");
const gridSizeInput = document.getElementById("gridSizeInput");
const gridDimensionsLabel = document.getElementById("gridDimensionsLabel");
const applyGridButton = document.getElementById("applyGridButton");
const viewStatus = document.getElementById("viewStatus");
let tileCount = DEFAULT_TILE_COUNT;
let gridCols = 1000;
let gridRows = 1000;
const mainGame = {
state: new Uint8Array(DEFAULT_TILE_COUNT),
divisor: 2,
removedCount: 0
};
const customGame = {
state: new Uint8Array(DEFAULT_TILE_COUNT),
lastDivisor: null,
removedCount: 0
};
let activeMode = "main";
const panelCollapse = {
main: false,
custom: false,
grid: false
};
let boardBitmap = null;
let boardImage = null;
let boardImageData = null;
let animationFrame = 0;
let resizeObserverFrame = 0;
let devicePixelRatioValue = window.devicePixelRatio || 1;
const view = { zoom: 1, panX: 0, panY: 0 };
const pointerState = {
pointers: new Map(),
mode: "idle",
startX: 0,
startY: 0,
startPanX: 0,
startPanY: 0,
pinchDistance: 0,
pinchZoom: 1,
pinchCenterX: 0,
pinchCenterY: 0,
pinchWorldX: 0,
pinchWorldY: 0
};
init();
function init() {
const settings = loadSettings();
applyTileCount(settings.tileCount, { skipSave: true });
if (settings.divisor >= 2) {
restoreMainState(settings.divisor);
}
ensureBoardCanvas();
rebuildBoardFromState(getActiveState());
resizeCanvas();
resetView();
updateMainPanel();
updateCustomPanel();
updateGridPanel();
initPanelCollapse();
bindEvents();
requestDraw();
}
function initPanelCollapse() {
document.querySelectorAll("[data-panel]").forEach(panel => {
const key = panel.dataset.panel;
setPanelCollapsed(panel, panelCollapse[key]);
panel.querySelector(".panel__head").addEventListener("click", () => {
panelCollapse[key] = !panelCollapse[key];
setPanelCollapsed(panel, panelCollapse[key]);
saveSettings();
});
});
}
function setPanelCollapsed(panel, collapsed) {
panel.classList.toggle("is-collapsed", collapsed);
const toggle = panel.querySelector(".panel__head");
toggle.setAttribute("aria-expanded", String(!collapsed));
toggle.title = collapsed ? "Expand panel" : "Collapse panel";
}
function computeGridDimensions(total) {
const cols = Math.ceil(Math.sqrt(total));
const rows = Math.ceil(total / cols);
return { cols, rows };
}
function applyTileCount(total, options = {}) {
tileCount = total;
const dimensions = computeGridDimensions(total);
gridCols = dimensions.cols;
gridRows = dimensions.rows;
mainGame.state = new Uint8Array(tileCount);
customGame.state = new Uint8Array(tileCount);
resetMainGame();
resetCustomGame();
activeMode = "main";
invalidateBoardCanvas();
canvas.setAttribute("aria-label", `${tileCount.toLocaleString()} number grid`);
if (!options.skipSave) {
saveSettings();
}
}
function invalidateBoardCanvas() {
boardBitmap = null;
boardImage = null;
boardImageData = null;
}
function bindEvents() {
window.addEventListener("resize", () => {
cancelAnimationFrame(resizeObserverFrame);
resizeObserverFrame = requestAnimationFrame(() => {
resizeCanvas();
requestDraw();
});
});
canvas.addEventListener("wheel", event => {
event.preventDefault();
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const factor = event.deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP;
zoomAt(x, y, factor);
}, { passive: false });
canvas.addEventListener("pointerdown", event => {
canvas.setPointerCapture(event.pointerId);
pointerState.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
canvas.classList.add("is-dragging");
updatePointerMode();
});
canvas.addEventListener("pointermove", event => {
if (!pointerState.pointers.has(event.pointerId)) {
return;
}
pointerState.pointers.set(event.pointerId, { x: event.clientX, y: event.clientY });
if (pointerState.mode === "pan") {
view.panX = pointerState.startPanX + event.clientX - pointerState.startX;
view.panY = pointerState.startPanY + event.clientY - pointerState.startY;
clampPan();
requestDraw();
} else if (pointerState.mode === "pinch") {
updatePinchZoom();
}
});
canvas.addEventListener("pointerup", endPointer);
canvas.addEventListener("pointercancel", endPointer);
canvas.addEventListener("lostpointercapture", event => {
pointerState.pointers.delete(event.pointerId);
updatePointerMode();
});
document.getElementById("zoomOutButton").addEventListener("click", () => {
zoomAt(canvas.clientWidth / 2, canvas.clientHeight / 2, 1 / ZOOM_STEP);
});
document.getElementById("zoomInButton").addEventListener("click", () => {
zoomAt(canvas.clientWidth / 2, canvas.clientHeight / 2, ZOOM_STEP);
});
document.getElementById("resetViewButton").addEventListener("click", resetView);
removeButton.addEventListener("click", performRemoval);
resetButton.addEventListener("click", resetGame);
runCustomButton.addEventListener("click", runCustomDivisor);
customDivisorInput.addEventListener("input", updateCustomRunButton);
customDivisorInput.addEventListener("keydown", event => {
if (event.key === "Enter" && !runCustomButton.disabled) {
event.preventDefault();
runCustomDivisor();
}
});
applyGridButton.addEventListener("click", applyGridSize);
gridSizeInput.addEventListener("input", updateGridApplyButton);
gridSizeInput.addEventListener("keydown", event => {
if (event.key === "Enter" && !applyGridButton.disabled) {
event.preventDefault();
applyGridSize();
}
});
window.addEventListener("keydown", event => {
const target = event.target;
if (target && ["INPUT", "TEXTAREA", "SELECT", "BUTTON"].includes(target.tagName)) {
return;
}
const panAmount = Math.max(28, 72 / view.zoom);
if (event.key === "+" || event.key === "=") {
event.preventDefault();
zoomAt(canvas.clientWidth / 2, canvas.clientHeight / 2, ZOOM_STEP);
} else if (event.key === "-" || event.key === "_") {
event.preventDefault();
zoomAt(canvas.clientWidth / 2, canvas.clientHeight / 2, 1 / ZOOM_STEP);
} else if (event.key === "Home") {
event.preventDefault();
resetView();
} else if (event.key === "ArrowLeft" || event.key.toLowerCase() === "a") {
event.preventDefault();
panBy(panAmount, 0);
} else if (event.key === "ArrowRight" || event.key.toLowerCase() === "d") {
event.preventDefault();
panBy(-panAmount, 0);
} else if (event.key === "ArrowUp" || event.key.toLowerCase() === "w") {
event.preventDefault();
panBy(0, panAmount);
} else if (event.key === "ArrowDown" || event.key.toLowerCase() === "s") {
event.preventDefault();
panBy(0, -panAmount);
}
});
}
function resetMainGame() {
mainGame.state.fill(0);
mainGame.divisor = 2;
mainGame.removedCount = 0;
}
function resetCustomGame() {
customGame.state.fill(0);
customGame.lastDivisor = null;
customGame.removedCount = 0;
}
function getActiveState() {
return activeMode === "main" ? mainGame.state : customGame.state;
}
function resetGame() {
resetMainGame();
activeMode = "main";
rebuildBoardFromState(mainGame.state);
saveSettings();
updateMainPanel();
requestDraw();
}
function removeByDivisor(stateArray, targetDivisor) {
let changed = 0;
for (let i = 0; i < tileCount; i += 1) {
if (stateArray[i] === 1) {
continue;
}
const number = i + 1;
if (number % targetDivisor !== 0) {
stateArray[i] = 1;
changed += 1;
}
}
return changed;
}
function applyGridSize() {
const nextCount = parseGridSizeInput();
if (nextCount === null || nextCount === tileCount) {
return;
}
applyTileCount(nextCount);
ensureBoardCanvas();
rebuildBoardFromState(mainGame.state);
resetView();
updateMainPanel();
updateCustomPanel();
updateGridPanel();
requestDraw();
}
function parseGridSizeInput() {
const value = Number.parseInt(gridSizeInput.value, 10);
if (!Number.isInteger(value) || value < MIN_TILE_COUNT || value > MAX_TILE_COUNT) {
return null;
}
return value;
}
function updateGridApplyButton() {
const parsed = parseGridSizeInput();
applyGridButton.disabled = parsed === null || parsed === tileCount;
}
function updateGridPanel() {
gridSizeTitle.textContent = `${tileCount.toLocaleString()} numbers`;
gridDimensionsLabel.textContent = `${gridCols.toLocaleString()} × ${gridRows.toLocaleString()} grid`;
if (document.activeElement !== gridSizeInput) {
gridSizeInput.value = String(tileCount);
}
updateGridApplyButton();
}
function runCustomDivisor() {
const targetDivisor = parseCustomDivisor();
if (targetDivisor === null) {
return;
}
resetMainGame();
customGame.state.fill(0);
customGame.removedCount = removeByDivisor(customGame.state, targetDivisor);
customGame.lastDivisor = targetDivisor;
activeMode = "custom";
rebuildBoardFromState(customGame.state);
saveSettings();
updateMainPanel();
updateCustomPanel();
requestDraw();
}
function parseCustomDivisor() {
const value = Number.parseInt(customDivisorInput.value, 10);
if (!Number.isInteger(value) || value < 2) {
return null;
}
return value;
}
function updateCustomRunButton() {
runCustomButton.disabled = parseCustomDivisor() === null;
}
function performRemoval() {
mainGame.removedCount += removeByDivisor(mainGame.state, mainGame.divisor);
mainGame.divisor += 1;
activeMode = "main";
rebuildBoardFromState(mainGame.state);
saveSettings();
updateMainPanel();
requestDraw();
}
function updateMainPanel() {
const remaining = tileCount - mainGame.removedCount;
iterationTitle.textContent = `Not divisible by ${mainGame.divisor}`;
remainingValue.textContent = remaining.toLocaleString();
removedValue.textContent = mainGame.removedCount.toLocaleString();
if (remaining === 0) {
removeButton.disabled = true;
removeButton.textContent = "No numbers left";
} else {
removeButton.disabled = false;
removeButton.textContent = "Remove";
}
}
function updateCustomPanel() {
const remaining = tileCount - customGame.removedCount;
customRemainingValue.textContent = remaining.toLocaleString();
customRemovedValue.textContent = customGame.removedCount.toLocaleString();
customIterationTitle.textContent = customGame.lastDivisor === null
? "Not divisible by …"
: `Not divisible by ${customGame.lastDivisor}`;
updateCustomRunButton();
}
function endPointer(event) {
pointerState.pointers.delete(event.pointerId);
if (pointerState.pointers.size === 0) {
canvas.classList.remove("is-dragging");
}
updatePointerMode();
}
function updatePointerMode() {
const points = Array.from(pointerState.pointers.values());
if (points.length === 0) {
pointerState.mode = "idle";
return;
}
if (points.length === 1) {
pointerState.mode = "pan";
pointerState.startX = points[0].x;
pointerState.startY = points[0].y;
pointerState.startPanX = view.panX;
pointerState.startPanY = view.panY;
return;
}
const first = points[0];
const second = points[1];
const center = getPointerCenter(first, second);
const distance = getPointerDistance(first, second);
pointerState.mode = "pinch";
pointerState.pinchDistance = distance || 1;
pointerState.pinchZoom = view.zoom;
pointerState.pinchCenterX = center.x;
pointerState.pinchCenterY = center.y;
pointerState.pinchWorldX = (center.x - view.panX) / view.zoom;
pointerState.pinchWorldY = (center.y - view.panY) / view.zoom;
}
function updatePinchZoom() {
const points = Array.from(pointerState.pointers.values());
if (points.length < 2) {
return;
}
const first = points[0];
const second = points[1];
const center = getPointerCenter(first, second);
const distance = getPointerDistance(first, second) || 1;
const nextZoom = clamp(pointerState.pinchZoom * distance / pointerState.pinchDistance, MIN_ZOOM, MAX_ZOOM);
view.zoom = nextZoom;
view.panX = center.x - pointerState.pinchWorldX * view.zoom;
view.panY = center.y - pointerState.pinchWorldY * view.zoom;
clampPan();
requestDraw();
}
function getPointerCenter(first, second) {
return { x: (first.x + second.x) / 2, y: (first.y + second.y) / 2 };
}
function getPointerDistance(first, second) {
return Math.hypot(first.x - second.x, first.y - second.y);
}
function resizeCanvas() {
devicePixelRatioValue = window.devicePixelRatio || 1;
const width = Math.max(1, Math.floor(window.innerWidth * devicePixelRatioValue));
const height = Math.max(1, Math.floor(window.innerHeight * devicePixelRatioValue));
canvas.width = width;
canvas.height = height;
canvas.style.width = `${window.innerWidth}px`;
canvas.style.height = `${window.innerHeight}px`;
ctx.setTransform(devicePixelRatioValue, 0, 0, devicePixelRatioValue, 0, 0);
clampPan();
}
function resetView() {
const sidePadding = window.innerWidth < 760 ? 24 : 48;
const verticalPadding = window.innerWidth < 760 ? 220 : 48;
const fitZoom = Math.min(
(canvas.clientWidth - sidePadding) / gridCols,
(canvas.clientHeight - verticalPadding) / gridRows
);
view.zoom = clamp(fitZoom, MIN_ZOOM, MAX_ZOOM);
view.panX = (canvas.clientWidth - gridCols * view.zoom) / 2;
view.panY = (canvas.clientHeight - gridRows * view.zoom) / 2;
clampPan();
requestDraw();
}
function zoomAt(screenX, screenY, factor) {
const worldX = (screenX - view.panX) / view.zoom;
const worldY = (screenY - view.panY) / view.zoom;
view.zoom = clamp(view.zoom * factor, MIN_ZOOM, MAX_ZOOM);
view.panX = screenX - worldX * view.zoom;
view.panY = screenY - worldY * view.zoom;
clampPan();
requestDraw();
}
function panBy(deltaX, deltaY) {
view.panX += deltaX;
view.panY += deltaY;
clampPan();
requestDraw();
}
function clampPan() {
const boardWidth = gridCols * view.zoom;
const boardHeight = gridRows * view.zoom;
const canvasWidth = canvas.clientWidth || window.innerWidth;
const canvasHeight = canvas.clientHeight || window.innerHeight;
const margin = Math.max(80, Math.min(canvasWidth, canvasHeight) * 0.55 + view.zoom);
if (boardWidth <= canvasWidth) {
view.panX = clamp(view.panX, -margin, canvasWidth - boardWidth + margin);
} else {
view.panX = clamp(view.panX, canvasWidth - boardWidth - margin, margin);
}
if (boardHeight <= canvasHeight) {
view.panY = clamp(view.panY, -margin, canvasHeight - boardHeight + margin);
} else {
view.panY = clamp(view.panY, canvasHeight - boardHeight - margin, margin);
}
}
function requestDraw() {
if (animationFrame) {
return;
}
animationFrame = requestAnimationFrame(draw);
}
function draw() {
animationFrame = 0;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
ctx.save();
ctx.setTransform(devicePixelRatioValue, 0, 0, devicePixelRatioValue, 0, 0);
ctx.imageSmoothingEnabled = false;
ctx.fillStyle = "#080c13";
ctx.fillRect(0, 0, width, height);
ctx.drawImage(
boardBitmap,
Math.round(view.panX),
Math.round(view.panY),
Math.round(gridCols * view.zoom),
Math.round(gridRows * view.zoom)
);
drawBoardBoundary();
if (view.zoom >= 8) {
drawGridLines();
}
if (view.zoom >= TILE_NUMBER_MIN_ZOOM) {
drawTileNumbers();
}
ctx.restore();
viewStatus.textContent = `Zoom ${Math.round(view.zoom * 100)}%`;
}
function drawBoardBoundary() {
ctx.strokeStyle = "rgba(255, 255, 255, 0.7)";
ctx.lineWidth = 2;
ctx.strokeRect(
Math.round(view.panX) + 0.5,
Math.round(view.panY) + 0.5,
Math.round(gridCols * view.zoom),
Math.round(gridRows * view.zoom)
);
}
function drawGridLines() {
const startCol = clamp(Math.floor((-view.panX) / view.zoom), 0, gridCols);
const endCol = clamp(Math.ceil((canvas.clientWidth - view.panX) / view.zoom), 0, gridCols);
const startRow = clamp(Math.floor((-view.panY) / view.zoom), 0, gridRows);
const endRow = clamp(Math.ceil((canvas.clientHeight - view.panY) / view.zoom), 0, gridRows);
ctx.beginPath();
ctx.strokeStyle = view.zoom >= 16 ? "rgba(0, 0, 0, 0.42)" : "rgba(0, 0, 0, 0.24)";
ctx.lineWidth = 1;
for (let col = startCol; col <= endCol; col += 1) {
const x = Math.round(view.panX + col * view.zoom) + 0.5;
ctx.moveTo(x, view.panY + startRow * view.zoom);
ctx.lineTo(x, view.panY + endRow * view.zoom);
}
for (let row = startRow; row <= endRow; row += 1) {
const y = Math.round(view.panY + row * view.zoom) + 0.5;
ctx.moveTo(view.panX + startCol * view.zoom, y);
ctx.lineTo(view.panX + endCol * view.zoom, y);
}
ctx.stroke();
}
function drawTileNumbers() {
const startCol = clamp(Math.floor((-view.panX) / view.zoom), 0, gridCols - 1);
const endCol = clamp(Math.ceil((canvas.clientWidth - view.panX) / view.zoom), 0, gridCols - 1);
const startRow = clamp(Math.floor((-view.panY) / view.zoom), 0, gridRows - 1);
const endRow = clamp(Math.ceil((canvas.clientHeight - view.panY) / view.zoom), 0, gridRows - 1);
const fontSize = clamp(Math.floor(view.zoom * 0.2), 6, 14);
const tileCenterOffset = view.zoom / 2;
ctx.save();
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.font = `800 ${fontSize}px Inter, ui-sans-serif, system-ui, sans-serif`;
ctx.lineWidth = Math.max(2, fontSize * 0.24);
ctx.strokeStyle = "rgba(0, 0, 0, 0.76)";
ctx.fillStyle = "rgba(255, 255, 255, 0.96)";
for (let row = startRow; row <= endRow; row += 1) {
for (let col = startCol; col <= endCol; col += 1) {
const number = row * gridCols + col + 1;
if (number > tileCount) {
continue;
}
const x = view.panX + col * view.zoom + tileCenterOffset;
const y = view.panY + row * view.zoom + tileCenterOffset;
const label = String(number);
ctx.strokeText(label, x, y);
ctx.fillText(label, x, y);
}
}
ctx.restore();
}
function ensureBoardCanvas() {
if (boardBitmap && boardBitmap.width === gridCols && boardBitmap.height === gridRows) {
return;
}
boardBitmap = document.createElement("canvas");
boardBitmap.width = gridCols;
boardBitmap.height = gridRows;
boardImage = boardBitmap.getContext("2d", { alpha: false });
boardImageData = boardImage.createImageData(gridCols, gridRows);
}
function rebuildBoardFromState(stateArray) {
ensureBoardCanvas();
const data = boardImageData.data;
const cellCount = gridCols * gridRows;
for (let cell = 0; cell < cellCount; cell += 1) {
const offset = cell * 4;
let color;
if (cell >= tileCount) {
color = COLOR_EMPTY;
} else {
color = stateArray[cell] === 1 ? COLOR_REMOVED : COLOR_REMAINING;
}
data[offset] = color.r;
data[offset + 1] = color.g;
data[offset + 2] = color.b;
data[offset + 3] = 255;
}
boardImage.putImageData(boardImageData, 0, 0);
}
function loadSettings() {
let saved = null;
try {
const value = localStorage.getItem(STATE_KEY);
saved = value ? JSON.parse(value) : null;
} catch (error) {
saved = null;
}
const savedTileCount = saved && Number.isInteger(saved.tileCount)
? clamp(saved.tileCount, MIN_TILE_COUNT, MAX_TILE_COUNT)
: DEFAULT_TILE_COUNT;
const divisor = saved && Number.isInteger(saved.divisor) && saved.divisor >= 2
? saved.divisor
: 2;
const collapse = saved && saved.panelCollapse && typeof saved.panelCollapse === "object"
? saved.panelCollapse
: {};
if (typeof collapse.main === "boolean") {
panelCollapse.main = collapse.main;
}
if (typeof collapse.custom === "boolean") {
panelCollapse.custom = collapse.custom;
}
if (typeof collapse.grid === "boolean") {
panelCollapse.grid = collapse.grid;
}
return { tileCount: savedTileCount, divisor };
}
function restoreMainState(divisor) {
activeMode = "main";
mainGame.divisor = divisor;
const keepMultiple = lcmRange(2, mainGame.divisor - 1);
if (keepMultiple === null || keepMultiple > tileCount) {
mainGame.state.fill(1);
mainGame.removedCount = tileCount;
return;
}
mainGame.removedCount = 0;
for (let i = 0; i < tileCount; i += 1) {
const removed = (i + 1) % keepMultiple !== 0 ? 1 : 0;
mainGame.state[i] = removed;
mainGame.removedCount += removed;
}
}
function saveSettings() {
try {
localStorage.setItem(STATE_KEY, JSON.stringify({
tileCount,
divisor: mainGame.divisor,
panelCollapse: {
main: panelCollapse.main,
custom: panelCollapse.custom,
grid: panelCollapse.grid
}
}));
} catch (error) {
// Ignore storage failures (private mode etc.).
}
}
function lcmRange(from, to) {
let result = 1;
for (let n = from; n <= to; n += 1) {
result = result / gcd(result, n) * n;
if (result > tileCount) {
return result;
}
}
return result;
}
function gcd(a, b) {
while (b !== 0) {
[a, b] = [b, a % b];
}
return a;
}
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
})();
</script>
</body>
</html>