| 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 : root ( 0) 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/timer/ |
Upload File : |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
<title>Number Pyramid</title>
<style>
:root {
--red: #e02323;
--orange: #f5a623;
--bg: #fafafa;
--panel: rgba(255, 255, 255, 0.92);
--border: #1a1a1a;
--text: #1a1a1a;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
padding: 0;
height: 100%;
overflow: hidden;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
-webkit-tap-highlight-color: transparent;
}
#app { position: fixed; inset: 0; }
canvas {
display: block;
width: 100%;
height: 100%;
touch-action: none; /* we handle gestures ourselves */
cursor: grab;
}
canvas:active { cursor: grabbing; }
/* Layer counter (top right) */
#counter {
position: fixed;
top: 16px;
right: 16px;
background: var(--panel);
backdrop-filter: blur(6px);
border: 2px solid var(--border);
border-radius: 10px;
padding: 8px 14px;
font-size: 16px;
font-weight: 700;
box-shadow: 0 4px 14px rgba(0,0,0,0.12);
user-select: none;
}
/* Bottom-left control cluster */
#controls {
position: fixed;
left: 16px;
bottom: 16px;
display: flex;
gap: 10px;
align-items: center;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
border: 2px solid var(--border);
background: var(--panel);
backdrop-filter: blur(6px);
color: var(--text);
border-radius: 10px;
font-size: 16px;
font-weight: 700;
cursor: pointer;
box-shadow: 0 4px 14px rgba(0,0,0,0.12);
user-select: none;
transition: transform 0.05s ease, background 0.15s ease;
}
.btn:hover { background: #fff; }
.btn:active { transform: translateY(1px) scale(0.98); }
#addLayer { padding: 10px 18px; }
.icon-btn {
width: 46px;
height: 46px;
padding: 0;
font-size: 0;
}
.icon-btn svg { width: 22px; height: 22px; }
/* Zoom controls (bottom right) */
#zoom {
position: fixed;
right: 16px;
bottom: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
#legend {
position: fixed;
top: 16px;
left: 16px;
background: var(--panel);
backdrop-filter: blur(6px);
border: 2px solid var(--border);
border-radius: 10px;
padding: 8px 12px;
font-size: 13px;
font-weight: 600;
box-shadow: 0 4px 14px rgba(0,0,0,0.12);
user-select: none;
max-width: 230px;
line-height: 1.45;
}
.swatch {
display: inline-block;
width: 12px; height: 12px;
border: 1.5px solid var(--border);
border-radius: 3px;
vertical-align: -1px;
margin-right: 4px;
}
.swatch.red { background: var(--red); }
.swatch.orange { background: var(--orange); }
.swatch.green { background: #2ea843; }
#legend .row { white-space: nowrap; }
#legend .hint { font-weight: 500; color: #555; margin-top: 4px; white-space: normal; }
</style>
</head>
<body>
<div id="app">
<canvas id="pyramid"></canvas>
<div id="counter">Layers: <span id="layerCount">1</span></div>
<div id="legend">
<div class="row"><span class="swatch green"></span>new number (row center)</div>
<div class="row"><span class="swatch red"></span>odd # of digits</div>
<div class="row"><span class="swatch orange"></span>even # of digits</div>
<div class="hint">value = ⌊13^(d/2)⌋, d = distance from edge</div>
</div>
<div id="controls">
<button id="addLayer" class="btn">+ Add layer</button>
</div>
<div id="zoom">
<button id="zoomIn" class="btn icon-btn" aria-label="Zoom in" title="Zoom in">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="10.5" cy="10.5" r="6.5"></circle>
<line x1="15.5" y1="15.5" x2="21" y2="21"></line>
<line x1="10.5" y1="7.5" x2="10.5" y2="13.5"></line>
<line x1="7.5" y1="10.5" x2="13.5" y2="10.5"></line>
</svg>
</button>
<button id="zoomOut" class="btn icon-btn" aria-label="Zoom out" title="Zoom out">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="10.5" cy="10.5" r="6.5"></circle>
<line x1="15.5" y1="15.5" x2="21" y2="21"></line>
<line x1="7.5" y1="10.5" x2="13.5" y2="10.5"></line>
</svg>
</button>
<button id="fit" class="btn icon-btn" aria-label="Fit to view" title="Fit to view">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 9V5a1 1 0 0 1 1-1h4"></path>
<path d="M20 9V5a1 1 0 0 0-1-1h-4"></path>
<path d="M4 15v4a1 1 0 0 0 1 1h4"></path>
<path d="M20 15v4a1 1 0 0 1-1 1h-4"></path>
</svg>
</button>
</div>
</div>
<script>
(() => {
"use strict";
// ---- Geometry constants (world units) ----
const CELL = 64; // cell size in world units
const GAP = 0; // gap between cells (cells share borders like the screenshot)
const FIT_PADDING = 80; // screen px padding when fitting
const MIN_SCALE = 0.0005;
const MAX_SCALE = 6;
// ---- State ----
let layers = 1;
let scale = 1;
let tx = 0; // screen translation x: screenX = worldX * scale + tx
let ty = 0; // screen translation y
const canvas = document.getElementById("pyramid");
const ctx = canvas.getContext("2d");
let dpr = Math.max(1, window.devicePixelRatio || 1);
let viewW = 0, viewH = 0; // CSS pixels
// ---- Value computation: value(d) = floor(sqrt(13^d)) using BigInt isqrt ----
const THIRTEEN = 13n;
const valueCache = new Map(); // d -> string (decimal)
function isqrt(n) {
if (n < 0n) throw new Error("neg");
if (n < 2n) return n;
// initial guess via bit length
let x = 1n << (BigInt(n.toString(2).length + 1) >> 1n);
while (true) {
const y = (x + n / x) >> 1n;
if (y >= x) break;
x = y;
}
// x is now floor-ish; correct it
while (x * x > n) x -= 1n;
while ((x + 1n) * (x + 1n) <= n) x += 1n;
return x;
}
function valueStr(d) {
if (valueCache.has(d)) return valueCache.get(d);
let s;
if (d === 0) {
s = "1";
} else {
const pow = THIRTEEN ** BigInt(d); // 13^d
s = isqrt(pow).toString(); // floor(sqrt(13^d))
}
valueCache.set(d, s);
return s;
}
// Color rule: odd digit count -> red, even digit count -> orange
function colorFor(str) {
return (str.length % 2 === 1) ? "#e02323" : "#f5a623";
}
// ---- Pyramid layout helpers ----
// Row r (1-indexed) has 2r-1 cells, centered on x = 0.
// Bounding box of the whole pyramid:
function pyramidBounds() {
const widthCells = 2 * layers - 1;
const w = widthCells * CELL;
const h = layers * CELL;
return {
minX: -w / 2,
maxX: w / 2,
minY: 0,
maxY: h,
w, h,
};
}
// ---- View transforms ----
function worldToScreen(wx, wy) {
return { x: wx * scale + tx, y: wy * scale + ty };
}
function screenToWorld(sx, sy) {
return { x: (sx - tx) / scale, y: (sy - ty) / scale };
}
function clampScale(s) {
return Math.min(MAX_SCALE, Math.max(MIN_SCALE, s));
}
function fitView() {
const b = pyramidBounds();
const availW = Math.max(10, viewW - FIT_PADDING * 2);
const availH = Math.max(10, viewH - FIT_PADDING * 2);
const s = clampScale(Math.min(availW / b.w, availH / b.h));
scale = s;
// center bbox center at canvas center
const cx = (b.minX + b.maxX) / 2; // = 0
const cy = (b.minY + b.maxY) / 2;
tx = viewW / 2 - cx * scale;
ty = viewH / 2 - cy * scale;
}
// Returns true if the pyramid currently fits inside the padded viewport.
function pyramidFits() {
const b = pyramidBounds();
const tl = worldToScreen(b.minX, b.minY);
const br = worldToScreen(b.maxX, b.maxY);
return (
tl.x >= FIT_PADDING &&
tl.y >= FIT_PADDING &&
br.x <= viewW - FIT_PADDING &&
br.y <= viewH - FIT_PADDING
);
}
function zoomAt(sx, sy, factor) {
const before = screenToWorld(sx, sy);
scale = clampScale(scale * factor);
// keep the world point under (sx,sy) fixed
tx = sx - before.x * scale;
ty = sy - before.y * scale;
draw();
}
// ---- Rendering ----
function resize() {
dpr = Math.max(1, window.devicePixelRatio || 1);
viewW = canvas.clientWidth;
viewH = canvas.clientHeight;
canvas.width = Math.round(viewW * dpr);
canvas.height = Math.round(viewH * dpr);
draw();
}
function draw() {
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
ctx.clearRect(0, 0, viewW, viewH);
const cellScreen = CELL * scale;
// Visible world rectangle (for culling)
const vMin = screenToWorld(0, 0);
const vMax = screenToWorld(viewW, viewH);
const drawText = cellScreen > 18; // skip text when cells are tiny
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.lineWidth = Math.max(1, Math.min(2, cellScreen * 0.03));
ctx.strokeStyle = "#1a1a1a";
const widthCellsBottom = 2 * layers - 1;
for (let r = 1; r <= layers; r++) {
const count = 2 * r - 1;
const rowWorldLeft = -count * CELL / 2;
const wy = (r - 1) * CELL;
// cull rows outside view vertically
if (wy + CELL < vMin.y || wy > vMax.y) continue;
for (let j = 0; j < count; j++) {
const wx = rowWorldLeft + j * CELL;
// cull cols outside view horizontally
if (wx + CELL < vMin.x || wx > vMax.x) continue;
const d = Math.min(j, count - 1 - j); // distance from nearest edge
const str = valueStr(d);
const isCenter = (j === r - 1); // central cell introduces the new number
const col = isCenter ? "#2ea843" : colorFor(str);
const p = worldToScreen(wx, wy);
const size = cellScreen;
ctx.fillStyle = col;
ctx.fillRect(p.x, p.y, size, size);
ctx.strokeRect(p.x, p.y, size, size);
if (drawText) {
ctx.fillStyle = "#1a1a1a";
// fit font size to cell width
let fontSize = Math.min(size * 0.42, size * 0.9 / Math.max(1, str.length * 0.62));
fontSize = Math.max(6, fontSize);
ctx.font = `700 ${fontSize}px -apple-system, "Segoe UI", Roboto, Arial, sans-serif`;
// ensure it fits; shrink if measured too wide
let measured = ctx.measureText(str).width;
const maxW = size * 0.86;
if (measured > maxW) {
fontSize *= maxW / measured;
fontSize = Math.max(5, fontSize);
ctx.font = `700 ${fontSize}px -apple-system, "Segoe UI", Roboto, Arial, sans-serif`;
}
ctx.fillText(str, p.x + size / 2, p.y + size / 2 + size * 0.02);
}
}
}
}
// ---- Add layer ----
function addLayer() {
layers += 1;
document.getElementById("layerCount").textContent = String(layers);
// If the new (larger) pyramid no longer fits, zoom out to fit it.
if (!pyramidFits()) {
fitView();
}
draw();
}
// ---- Pointer / gesture handling ----
const pointers = new Map(); // id -> {x, y}
let pinchPrev = null; // {dist, midX, midY}
function getPos(e) {
const rect = canvas.getBoundingClientRect();
return { x: e.clientX - rect.left, y: e.clientY - rect.top };
}
canvas.addEventListener("pointerdown", (e) => {
canvas.setPointerCapture(e.pointerId);
pointers.set(e.pointerId, getPos(e));
if (pointers.size === 2) {
pinchPrev = pinchState();
}
});
canvas.addEventListener("pointermove", (e) => {
if (!pointers.has(e.pointerId)) return;
const prev = pointers.get(e.pointerId);
const pos = getPos(e);
pointers.set(e.pointerId, pos);
if (pointers.size === 1) {
// pan
tx += pos.x - prev.x;
ty += pos.y - prev.y;
draw();
} else if (pointers.size === 2) {
const cur = pinchState();
if (pinchPrev) {
// zoom around midpoint
const factor = cur.dist / pinchPrev.dist;
const before = screenToWorld(cur.midX, cur.midY);
scale = clampScale(scale * factor);
tx = cur.midX - before.x * scale;
ty = cur.midY - before.y * scale;
// pan by midpoint movement
tx += cur.midX - pinchPrev.midX;
ty += cur.midY - pinchPrev.midY;
draw();
}
pinchPrev = cur;
}
});
function endPointer(e) {
if (pointers.has(e.pointerId)) pointers.delete(e.pointerId);
if (pointers.size < 2) pinchPrev = null;
}
canvas.addEventListener("pointerup", endPointer);
canvas.addEventListener("pointercancel", endPointer);
canvas.addEventListener("pointerleave", endPointer);
function pinchState() {
const pts = [...pointers.values()];
const dx = pts[0].x - pts[1].x;
const dy = pts[0].y - pts[1].y;
return {
dist: Math.hypot(dx, dy) || 1,
midX: (pts[0].x + pts[1].x) / 2,
midY: (pts[0].y + pts[1].y) / 2,
};
}
// Mouse wheel / trackpad zoom around cursor
canvas.addEventListener("wheel", (e) => {
e.preventDefault();
const pos = getPos(e);
const factor = Math.exp(-e.deltaY * 0.0015);
zoomAt(pos.x, pos.y, factor);
}, { passive: false });
// ---- Buttons ----
document.getElementById("addLayer").addEventListener("click", addLayer);
document.getElementById("zoomIn").addEventListener("click", () => zoomAt(viewW / 2, viewH / 2, 1.25));
document.getElementById("zoomOut").addEventListener("click", () => zoomAt(viewW / 2, viewH / 2, 0.8));
document.getElementById("fit").addEventListener("click", () => { fitView(); draw(); });
window.addEventListener("resize", resize);
// ---- Init ----
function init() {
viewW = canvas.clientWidth;
viewH = canvas.clientHeight;
resize();
fitView();
draw();
}
// ensure layout is ready
requestAnimationFrame(init);
})();
</script>
</body>
</html>