| 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, maximum-scale=1.0, user-scalable=no" />
<title>Pascal's Triangle</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;
cursor: grab;
}
canvas:active { cursor: grabbing; }
#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;
}
#controls {
position: fixed;
left: 16px;
bottom: 16px;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.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, #removeLayer, #save { padding: 10px 18px; }
#save.saved { background: #2ea843; color: #fff; border-color: #1f7a31; }
.icon-btn { width: 46px; height: 46px; padding: 0; font-size: 0; }
.icon-btn svg { width: 22px; height: 22px; }
#zoom {
position: fixed;
right: 16px;
bottom: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
/* Colour picker popup */
#palette {
position: fixed;
display: none;
z-index: 50;
background: var(--panel);
backdrop-filter: blur(8px);
border: 2px solid var(--border);
border-radius: 12px;
padding: 10px;
box-shadow: 0 8px 24px rgba(0,0,0,0.22);
width: 188px;
}
#palette .title {
font-size: 12px;
font-weight: 700;
margin: 0 0 8px;
color: #333;
user-select: none;
}
#palette .swatches {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 6px;
}
#palette .chip {
width: 100%;
aspect-ratio: 1 / 1;
border-radius: 6px;
border: 2px solid rgba(0,0,0,0.25);
cursor: pointer;
transition: transform 0.05s ease;
}
#palette .chip:hover { transform: scale(1.1); }
#palette .chip.reset {
background:
linear-gradient(45deg, #fff 45%, #e02323 45%, #e02323 55%, #fff 55%);
position: relative;
}
#palette .reset-row {
margin-top: 8px;
display: flex;
gap: 6px;
}
#palette .reset-btn {
flex: 1;
border: 2px solid var(--border);
background: #fff;
border-radius: 8px;
padding: 6px;
font-size: 12px;
font-weight: 700;
cursor: pointer;
}
#palette .reset-btn:hover { background: #f3f3f3; }
</style>
</head>
<body>
<div id="app">
<canvas id="pyramid"></canvas>
<div id="counter">Layers: <span id="layerCount">1</span></div>
<div id="controls">
<button id="addLayer" class="btn">+ Add layer</button>
<button id="removeLayer" class="btn">- Remove layer</button>
<button id="save" class="btn">Save</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 id="palette">
<p class="title">Pick a colour</p>
<div class="swatches" id="swatches"></div>
<div class="reset-row">
<button class="reset-btn" id="resetCell">Reset to default</button>
</div>
</div>
</div>
<script>
(() => {
"use strict";
const STORAGE_KEY = "pascals-triangle-state-v1";
// ---- Geometry constants (world units) ----
const CELL = 64;
const FIT_PADDING = 80;
const MIN_SCALE = 0.0005;
const MAX_SCALE = 6;
const TAP_MOVE_THRESHOLD = 6; // px
// ---- State ----
let layers = 1; // number of rows (row n = 0 .. layers-1)
let scale = 1;
let tx = 0;
let ty = 0;
let customColors = {}; // "n,k" -> hex string
const canvas = document.getElementById("pyramid");
const ctx = canvas.getContext("2d");
let dpr = Math.max(1, window.devicePixelRatio || 1);
let viewW = 0, viewH = 0;
// ---- Pascal's triangle values (BigInt), cached by row ----
const triRows = [[1n]]; // triRows[n] = row n (n+1 entries)
function ensureRows(maxN) {
for (let n = triRows.length; n <= maxN; n++) {
const prev = triRows[n - 1];
const row = new Array(n + 1);
row[0] = 1n;
row[n] = 1n;
for (let k = 1; k < n; k++) row[k] = prev[k - 1] + prev[k];
triRows[n] = row;
}
}
const strCache = new Map(); // "n,k" -> decimal string
function valueStr(n, k) {
const key = n + "," + k;
let s = strCache.get(key);
if (s === undefined) {
ensureRows(n);
s = triRows[n][k].toString();
strCache.set(key, s);
}
return s;
}
// Default colour: plain white background
function defaultColorFor() {
return "#ffffff";
}
// Choose readable text colour for a given background hex
function textColorFor(hex) {
const h = hex.replace("#", "");
const r = parseInt(h.slice(0, 2), 16);
const g = parseInt(h.slice(2, 4), 16);
const b = parseInt(h.slice(4, 6), 16);
const lum = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return lum > 0.6 ? "#1a1a1a" : "#ffffff";
}
// ---- Layout ----
// Row n (0-indexed) has n+1 cells, centered on x = 0.
function pyramidBounds() {
const w = layers * CELL; // bottom row (n = layers-1) has `layers` cells
const h = layers * CELL;
return { minX: -w / 2, maxX: w / 2, minY: 0, maxY: h, w, h };
}
function rowLeft(n) {
const count = n + 1;
return -count * CELL / 2;
}
// ---- 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);
scale = clampScale(Math.min(availW / b.w, availH / b.h));
const cx = (b.minX + b.maxX) / 2;
const cy = (b.minY + b.maxY) / 2;
tx = viewW / 2 - cx * scale;
ty = viewH / 2 - cy * scale;
}
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);
tx = sx - before.x * scale;
ty = sy - before.y * scale;
hidePalette();
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;
const vMin = screenToWorld(0, 0);
const vMax = screenToWorld(viewW, viewH);
const drawText = cellScreen > 18;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.lineWidth = Math.max(1, Math.min(2, cellScreen * 0.03));
ctx.strokeStyle = "#1a1a1a";
for (let n = 0; n < layers; n++) {
const count = n + 1;
const left = rowLeft(n);
const wy = n * CELL;
if (wy + CELL < vMin.y || wy > vMax.y) continue;
ensureRows(n);
for (let k = 0; k < count; k++) {
const wx = left + k * CELL;
if (wx + CELL < vMin.x || wx > vMax.x) continue;
const str = valueStr(n, k);
const custom = customColors[n + "," + k];
const col = custom || defaultColorFor(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 = textColorFor(col);
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`;
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;
ensureRows(layers - 1);
document.getElementById("layerCount").textContent = String(layers);
if (!pyramidFits()) fitView();
hidePalette();
draw();
}
function removeLayer() {
if (layers <= 1) return;
const removed = layers - 1; // row index being removed (0-indexed)
layers -= 1;
// drop any custom colours that belonged to the removed row
for (const key of Object.keys(customColors)) {
if (parseInt(key.split(",")[0], 10) === removed) delete customColors[key];
}
document.getElementById("layerCount").textContent = String(layers);
hidePalette();
draw();
}
// ---- Hit testing (which cell is at a screen point) ----
function hitTest(sx, sy) {
const w = screenToWorld(sx, sy);
const n = Math.floor(w.y / CELL);
if (n < 0 || n >= layers) return null;
const left = rowLeft(n);
const k = Math.floor((w.x - left) / CELL);
if (k < 0 || k > n) return null;
return { n, k };
}
// ---- Colour palette popup ----
const PALETTE = [
"#e02323", "#f5a623", "#f8e71c", "#2ea843", "#1abc9c", "#2d7ff9",
"#8e44ad", "#ff6fb5", "#8b5a2b", "#1a1a1a", "#ffffff", "#9b9b9b",
];
const paletteEl = document.getElementById("palette");
const swatchesEl = document.getElementById("swatches");
let selectedCell = null;
function buildSwatches() {
swatchesEl.innerHTML = "";
for (const c of PALETTE) {
const chip = document.createElement("div");
chip.className = "chip";
chip.style.background = c;
chip.title = c;
chip.addEventListener("click", () => applyColor(c));
swatchesEl.appendChild(chip);
}
}
function showPalette(clientX, clientY, cell) {
selectedCell = cell;
paletteEl.style.display = "block";
const pw = paletteEl.offsetWidth || 188;
const ph = paletteEl.offsetHeight || 160;
let x = clientX + 12;
let y = clientY + 12;
if (x + pw > window.innerWidth - 8) x = clientX - pw - 12;
if (y + ph > window.innerHeight - 8) y = clientY - ph - 12;
paletteEl.style.left = Math.max(8, x) + "px";
paletteEl.style.top = Math.max(8, y) + "px";
}
function hidePalette() {
paletteEl.style.display = "none";
selectedCell = null;
}
function applyColor(c) {
if (!selectedCell) return;
customColors[selectedCell.n + "," + selectedCell.k] = c;
draw();
hidePalette();
}
document.getElementById("resetCell").addEventListener("click", () => {
if (!selectedCell) return;
delete customColors[selectedCell.n + "," + selectedCell.k];
draw();
hidePalette();
});
// ---- Pointer / gesture handling ----
const pointers = new Map();
let pinchPrev = null;
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);
const pos = getPos(e);
pointers.set(e.pointerId, {
x: pos.x, y: pos.y,
startX: pos.x, startY: pos.y,
clientX: e.clientX, clientY: e.clientY,
moved: false,
});
if (pointers.size === 2) pinchPrev = pinchState();
});
canvas.addEventListener("pointermove", (e) => {
const info = pointers.get(e.pointerId);
if (!info) return;
const pos = getPos(e);
const dx = pos.x - info.x, dy = pos.y - info.y;
info.x = pos.x; info.y = pos.y;
info.clientX = e.clientX; info.clientY = e.clientY;
if (Math.hypot(pos.x - info.startX, pos.y - info.startY) > TAP_MOVE_THRESHOLD) {
info.moved = true;
}
if (pointers.size === 1) {
tx += dx; ty += dy;
draw();
} else if (pointers.size === 2) {
const cur = pinchState();
if (pinchPrev) {
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;
tx += cur.midX - pinchPrev.midX;
ty += cur.midY - pinchPrev.midY;
draw();
}
pinchPrev = cur;
}
});
function endPointer(e) {
const info = pointers.get(e.pointerId);
const wasOnly = pointers.size === 1;
pointers.delete(e.pointerId);
if (pointers.size < 2) pinchPrev = null;
if (info && wasOnly && !info.moved) {
// treat as a tap -> select cell
const cell = hitTest(info.x, info.y);
if (cell) {
buildSwatches();
showPalette(info.clientX, info.clientY, cell);
} else {
hidePalette();
}
}
}
canvas.addEventListener("pointerup", endPointer);
canvas.addEventListener("pointercancel", (e) => { pointers.delete(e.pointerId); if (pointers.size < 2) pinchPrev = null; });
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,
};
}
canvas.addEventListener("wheel", (e) => {
e.preventDefault();
const pos = getPos(e);
zoomAt(pos.x, pos.y, Math.exp(-e.deltaY * 0.0015));
}, { passive: false });
// hide palette when clicking elsewhere
document.addEventListener("pointerdown", (e) => {
if (!paletteEl.contains(e.target) && e.target !== canvas) hidePalette();
});
// ---- Persistence ----
function saveState() {
const state = {
layers,
scale, tx, ty,
customColors,
};
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
const btn = document.getElementById("save");
btn.textContent = "Saved \u2713";
btn.classList.add("saved");
setTimeout(() => { btn.textContent = "Save"; btn.classList.remove("saved"); }, 1400);
} catch (err) {
alert("Could not save: " + err.message);
}
}
function loadState() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return false;
const s = JSON.parse(raw);
if (typeof s.layers === "number" && s.layers >= 1) layers = Math.floor(s.layers);
if (typeof s.scale === "number") scale = s.scale;
if (typeof s.tx === "number") tx = s.tx;
if (typeof s.ty === "number") ty = s.ty;
if (s.customColors && typeof s.customColors === "object") customColors = s.customColors;
ensureRows(layers - 1);
document.getElementById("layerCount").textContent = String(layers);
return true;
} catch (err) {
return false;
}
}
// ---- Buttons ----
document.getElementById("addLayer").addEventListener("click", addLayer);
document.getElementById("removeLayer").addEventListener("click", removeLayer);
document.getElementById("save").addEventListener("click", saveState);
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", () => { hidePalette(); fitView(); draw(); });
window.addEventListener("resize", resize);
// ---- Init ----
function init() {
viewW = canvas.clientWidth;
viewH = canvas.clientHeight;
const restored = loadState();
resize();
if (!restored) fitView();
draw();
}
requestAnimationFrame(init);
})();
</script>
</body>
</html>