Web drawing now has wind and a few color mods
https://svonberg.org/wp-content/uploads/2026/02/webs2.html
<!DOCTYPE html>
<html lang=”en”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>Zen Spiderweb Generator</title>
<script src=”https://cdn.tailwindcss.com”></script>
<style>
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden;
background-color: #111827;
font-family: ‘Segoe UI’, Tahoma, Geneva, Verdana, sans-serif;
transition: background 1s ease;
}
#canvas-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
canvas {
display: block;
}
/* Custom Scrollbar */
.custom-scroll::-webkit-scrollbar {
width: 6px;
}
.custom-scroll::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.custom-scroll::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 3px;
}
.custom-scroll::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.4);
}
/* Range Slider Styling */
input[type=range] {
-webkit-appearance: none;
width: 100%;
background: transparent;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 16px;
width: 16px;
border-radius: 50%;
background: #e5e7eb;
cursor: pointer;
margin-top: -6px;
box-shadow: 0 0 5px rgba(0,0,0,0.5);
transition: transform 0.1s;
}
input[type=range]::-webkit-slider-thumb:hover {
transform: scale(1.1);
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 4px;
cursor: pointer;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
}
/* Section dividers */
.control-group {
border-top: 1px solid rgba(255,255,255,0.1);
padding-top: 1rem;
margin-top: 1rem;
}
/* Zen Button Specifics */
#open-btn {
z-index: 50; /* Ensure it is above everything */
transition: all 0.3s ease;
box-shadow: 0 0 15px rgba(0,0,0,0.5);
}
#open-btn:hover {
transform: scale(1.1) rotate(90deg);
}
</style>
</head>
<body>
<!– Canvas Layer –>
<div id=”canvas-container”>
<canvas id=”webCanvas”></canvas>
</div>
<!– UI Overlay –>
<div id=”ui-layer” class=”absolute top-0 right-0 p-4 md:p-6 z-10 w-full md:w-[400px] h-full pointer-events-none transition-transform duration-500 ease-in-out transform translate-x-0″>
<!– Main Controls Panel –>
<div id=”controls-panel” class=”bg-gray-900/90 backdrop-blur-md border border-gray-700/50 rounded-2xl p-5 shadow-2xl text-gray-200 custom-scroll h-full max-h-full overflow-y-auto pointer-events-auto flex flex-col”>
<div class=”flex justify-between items-center mb-4 flex-shrink-0″>
<div>
<h1 class=”text-xl font-light tracking-wider text-white”>Silk Weaver</h1>
<p class=”text-xs text-gray-400″>Procedural Generator v2.1</p>
</div>
<button id=”close-btn” class=”p-2 text-gray-300 hover:text-white transition-colors bg-white/10 hover:bg-white/20 rounded-lg flex items-center gap-2″ title=”Enter Zen Mode”>
<span class=”text-xs font-medium uppercase tracking-wider”>Zen Mode</span>
<svg xmlns=”http://www.w3.org/2000/svg” width=”18″ height=”18″ viewBox=”0 0 24 24″ fill=”none” stroke=”currentColor” stroke-width=”2″ stroke-linecap=”round” stroke-linejoin=”round”><path d=”M15 3h6v6M14 10l6.1-6.1M9 21H3v-6M10 14l-6.1 6.1″/></svg>
</button>
</div>
<div class=”space-y-4 flex-grow”>
<!– THEME SELECTOR –>
<div>
<label class=”text-xs uppercase tracking-widest text-indigo-400 font-semibold mb-2 block”>Atmosphere</label>
<div class=”grid grid-cols-4 gap-2″>
<button class=”theme-btn bg-gray-800 border-2 border-indigo-500 rounded h-8 w-full hover:brightness-110 transition-all” data-theme=”midnight” title=”Midnight”></button>
<button class=”theme-btn bg-green-900 border-2 border-transparent hover:border-gray-400 rounded h-8 w-full transition-all” data-theme=”forest” title=”Forest”></button>
<button class=”theme-btn bg-purple-900 border-2 border-transparent hover:border-gray-400 rounded h-8 w-full transition-all” data-theme=”sunset” title=”Sunset”></button>
<button class=”theme-btn bg-black border-2 border-transparent hover:border-gray-400 rounded h-8 w-full transition-all” data-theme=”cyber” title=”Cyber”></button>
</div>
</div>
<!– CORE SHAPE –>
<div class=”control-group”>
<label class=”text-xs uppercase tracking-widest text-indigo-400 font-semibold mb-3 block”>Structure</label>
<div class=”space-y-4″>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Radial Spokes</span>
<span id=”val-density”>12</span>
</div>
<input type=”range” id=”density” min=”5″ max=”30″ value=”12″ step=”1″>
</div>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Spiral Spacing</span>
<span id=”val-spacing”>20</span>
</div>
<input type=”range” id=”spacing” min=”10″ max=”60″ value=”20″ step=”5″>
</div>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Center Size</span>
<span id=”val-centerSize”>50</span>
</div>
<input type=”range” id=”centerSize” min=”0″ max=”200″ value=”50″ step=”10″>
</div>
</div>
</div>
<!– PHYSICS & CHAOS –>
<div class=”control-group”>
<label class=”text-xs uppercase tracking-widest text-indigo-400 font-semibold mb-3 block”>Physics & Chaos</label>
<div class=”space-y-4″>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Thread Slack (Gravity)</span>
<span id=”val-slack”>0.5</span>
</div>
<input type=”range” id=”slack” min=”0″ max=”1″ value=”0.5″ step=”0.1″>
</div>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Tear Probability</span>
<span id=”val-tears”>0%</span>
</div>
<input type=”range” id=”tears” min=”0″ max=”0.4″ value=”0″ step=”0.05″>
</div>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Irregularity</span>
<span id=”val-chaos”>0.3</span>
</div>
<input type=”range” id=”chaos” min=”0″ max=”1″ value=”0.3″ step=”0.1″>
</div>
</div>
</div>
<!– VISUALS –>
<div class=”control-group”>
<label class=”text-xs uppercase tracking-widest text-indigo-400 font-semibold mb-3 block”>Visual FX</label>
<div class=”space-y-4″>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Glow Strength</span>
<span id=”val-glow”>Low</span>
</div>
<input type=”range” id=”glow” min=”0″ max=”20″ value=”0″ step=”1″>
</div>
<div>
<div class=”flex justify-between text-xs text-gray-400 mb-1″>
<span>Wind Speed</span>
<span id=”val-wind”>0</span>
</div>
<input type=”range” id=”wind” min=”0″ max=”5″ value=”0″ step=”0.5″>
</div>
<div class=”flex items-center justify-between”>
<span class=”text-sm text-gray-300″>Morning Dew</span>
<label class=”relative inline-flex items-center cursor-pointer”>
<input type=”checkbox” id=”dew-toggle” class=”sr-only peer”>
<div class=”w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[”] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-indigo-500″></div>
</label>
</div>
</div>
</div>
<!– SYSTEM –>
<div class=”control-group”>
<div class=”flex items-center justify-between”>
<span class=”text-sm text-gray-300″>Instant Draw</span>
<label class=”relative inline-flex items-center cursor-pointer”>
<input type=”checkbox” id=”instant-toggle” class=”sr-only peer”>
<div class=”w-9 h-5 bg-gray-700 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[”] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-indigo-500″></div>
</label>
</div>
</div>
</div>
<!– Actions –>
<div class=”mt-6 grid grid-cols-2 gap-3 flex-shrink-0″>
<button id=”generate-btn” class=”col-span-2 bg-indigo-600 hover:bg-indigo-500 text-white font-medium py-3 px-4 rounded-xl transition-all shadow-lg shadow-indigo-500/20 active:scale-95 flex justify-center items-center gap-2″>
<svg xmlns=”http://www.w3.org/2000/svg” width=”16″ height=”16″ viewBox=”0 0 24 24″ fill=”none” stroke=”currentColor” stroke-width=”2″ stroke-linecap=”round” stroke-linejoin=”round”><path d=”M21 12a9 9 0 1 1-6.219-8.56″/></svg>
Re-Weave
</button>
<button id=”download-btn” class=”bg-gray-800 hover:bg-gray-700 text-gray-300 text-sm py-2 px-3 rounded-lg transition-colors border border-gray-700″>
Save Img
</button>
<button id=”clear-btn” class=”bg-gray-800 hover:bg-red-900/30 text-gray-300 hover:text-red-200 text-sm py-2 px-3 rounded-lg transition-colors border border-gray-700″>
Clear
</button>
</div>
</div>
</div>
<!– Zen Mode Restore Button (Fixed Visibility) –>
<button id=”open-btn” class=”fixed bottom-6 right-6 bg-white/10 hover:bg-white/20 text-white p-4 rounded-full backdrop-blur-md border border-white/30 hidden transition-all” title=”Exit Zen Mode”>
<svg xmlns=”http://www.w3.org/2000/svg” width=”28″ height=”28″ viewBox=”0 0 24 24″ fill=”none” stroke=”currentColor” stroke-width=”2″ stroke-linecap=”round” stroke-linejoin=”round”><circle cx=”12″ cy=”12″ r=”3″></circle><path d=”M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z”></path></svg>
</button>
<script>
/**
* Core Logic for Spiderweb Generator
*/
const canvas = document.getElementById(‘webCanvas’);
const ctx = canvas.getContext(‘2d’);
// State
let width, height;
let centerX, centerY;
let animationFrameId;
let time = 0; // For wind animation
// Web Data
let spokes = [];
let spirals = [];
let currentDrawIndex = 0;
let isConstructing = false; // Is the initial build animation running?
// Theme Configuration
const themes = {
midnight: { bg: ‘#111827’, web: ‘rgba(255, 255, 255, 0.2)’, dew: ‘rgba(255, 255, 255, 0.6)’ },
forest: { bg: ‘#064e3b’, web: ‘rgba(209, 250, 229, 0.2)’, dew: ‘rgba(167, 243, 208, 0.6)’ },
sunset: { bg: ‘linear-gradient(to bottom, #4c1d95, #c2410c)’, web: ‘rgba(254, 215, 170, 0.3)’, dew: ‘rgba(255, 237, 213, 0.7)’ },
cyber: { bg: ‘#000000’, web: ‘rgba(50, 255, 100, 0.3)’, dew: ‘rgba(50, 255, 100, 0.8)’ }
};
// Settings
const settings = {
theme: ‘midnight’,
spokeCount: 12,
spacing: 20, // Distance between spiral loops
centerSize: 50,
slack: 0.5,
chaos: 0.3,
tears: 0.0,
glow: 0,
wind: 0.0,
speed: 15,
dew: false,
instant: false
};
// UI Elements
const uiLayer = document.getElementById(‘ui-layer’);
const openBtn = document.getElementById(‘open-btn’);
const themeBtns = document.querySelectorAll(‘.theme-btn’);
// Helper: Random Range
const random = (min, max) => Math.random() * (max – min) + min;
// Helper: Apply Wind to a Point
// Returns a new object so we don’t mutate original geometry permanently
function applyWind(point) {
if (settings.wind <= 0.1) return point;
// Simple vertex displacement based on time and Y position
// Stronger effect further from center?
const dist = Math.sqrt((point.x – centerX)**2 + (point.y – centerY)**2);
const distFactor = Math.min(dist / 500, 1);
const offsetX = Math.sin(time * 0.002 + point.y * 0.01) * (settings.wind * 10) * distFactor;
const offsetY = Math.cos(time * 0.003 + point.x * 0.01) * (settings.wind * 5) * distFactor;
return {
x: point.x + offsetX,
y: point.y + offsetY
};
}
// Initialization
function resize() {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
centerX = width / 2;
centerY = height / 2;
generateWebData();
startDrawing();
}
window.addEventListener(‘resize’, resize);
// Generate the mathematical model of the web
function generateWebData() {
spokes = [];
spirals = [];
// 1. Generate Spokes (Radials)
const baseAngle = (Math.PI * 2) / settings.spokeCount;
const maxRadius = Math.max(width, height) * 0.8;
for (let i = 0; i < settings.spokeCount; i++) {
let angleOffset = (Math.random() – 0.5) * settings.chaos;
let angle = (i * baseAngle) + angleOffset;
let length = maxRadius * random(0.8, 1.2);
spokes.push({
angle: angle,
length: length,
endX: centerX + Math.cos(angle) * length,
endY: centerY + Math.sin(angle) * length
});
}
// 2. Generate Spiral Segments
let currentRadius = settings.centerSize;
let spokeIndex = 0;
// Prevent infinite loops
let safetyCount = 0;
while (safetyCount < 5000) {
safetyCount++;
let s1 = spokes[spokeIndex];
let nextIndex = (spokeIndex + 1) % spokes.length;
let s2 = spokes[nextIndex];
// R1 and R2 allow the spiral to spiral outwards
let r1 = currentRadius + random(-5, 5) * (settings.chaos * 5);
// Look ahead: The connection point on the next spoke is slightly further out
let spiralStep = (settings.spacing / settings.spokeCount);
let r2 = r1 + spiralStep + random(-2, 2) * (settings.chaos * 5);
// Stop if off screen
if (r1 > s1.length || r2 > s2.length) break;
// Probability to skip a segment (Tear)
if (Math.random() > settings.tears) {
let p1 = {
x: centerX + Math.cos(s1.angle) * r1,
y: centerY + Math.sin(s1.angle) * r1
};
let p2 = {
x: centerX + Math.cos(s2.angle) * r2,
y: centerY + Math.sin(s2.angle) * r2
};
// Control Point for Catenary (gravity sag)
let midX = (p1.x + p2.x) / 2;
let midY = (p1.y + p2.y) / 2;
// Pull vector towards center
let dirX = midX – centerX;
let dirY = midY – centerY;
let pullFactor = settings.slack * 0.4;
let cpX = midX – (dirX * pullFactor);
let cpY = midY – (dirY * pullFactor);
spirals.push({
p1: p1,
p2: p2,
cp: {x: cpX, y: cpY},
dew: Math.random() > 0.7
});
}
// Advance
spokeIndex = nextIndex;
currentRadius += spiralStep; // Increment radius constantly around the spiral
if (currentRadius > Math.max(width, height) * 0.7) break;
}
}
function getThemeColors() {
return themes[settings.theme] || themes[‘midnight’];
}
function drawDew(p1, p2, count) {
const theme = getThemeColors();
ctx.fillStyle = theme.dew;
for(let i=1; i<count; i++) {
const t = i/count;
const x = p1.x + (p2.x – p1.x) * t;
const y = p1.y + (p2.y – p1.y) * t;
ctx.beginPath();
ctx.arc(x, y, random(0.5, 1.5), 0, Math.PI * 2);
ctx.fill();
}
}
function drawDewOnCurve(p1, cp, p2) {
const theme = getThemeColors();
ctx.fillStyle = theme.dew;
const drops = Math.floor(random(1, 4));
for (let i = 0; i < drops; i++) {
const t = random(0.2, 0.8);
// Bezier Point
const x = (1 – t) * (1 – t) * p1.x + 2 * (1 – t) * t * cp.x + t * t * p2.x;
const y = (1 – t) * (1 – t) * p1.y + 2 * (1 – t) * t * cp.y + t * t * p2.y;
ctx.beginPath();
ctx.arc(x, y, random(0.5, 1.8), 0, Math.PI * 2);
ctx.fill();
}
}
function startDrawing() {
if (isConstructing) isConstructing = false; // Reset current build
currentDrawIndex = 0;
// Set Background based on theme
const theme = getThemeColors();
if (theme.bg.includes(‘gradient’)) {
document.body.style.background = theme.bg;
} else {
document.body.style.backgroundColor = theme.bg;
document.body.style.backgroundImage = ‘none’;
}
if (settings.instant) {
currentDrawIndex = spirals.length;
} else {
isConstructing = true;
}
if (!animationFrameId) animate();
}
function renderFrame() {
ctx.clearRect(0, 0, width, height);
const theme = getThemeColors();
// Setup Glow
if (settings.glow > 0) {
ctx.shadowBlur = settings.glow;
ctx.shadowColor = theme.web;
} else {
ctx.shadowBlur = 0;
}
// 1. Draw Spokes
ctx.strokeStyle = theme.web;
ctx.lineWidth = 1;
ctx.lineCap = “round”;
// Only draw spokes if we are generating or they are always visible
spokes.forEach(spoke => {
const start = applyWind({x: centerX, y: centerY});
const end = applyWind({x: spoke.endX, y: spoke.endY});
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.stroke();
});
// 2. Draw Spirals
// If constructing, limit by currentDrawIndex. If done, draw all.
const limit = isConstructing ? currentDrawIndex : spirals.length;
// Batch drawing for performance? Canvas handles single paths better
// But we need individual segments for wind
ctx.strokeStyle = theme.web; // Re-apply incase changed
ctx.lineWidth = Math.max(0.5, 0.8 – (settings.spokeCount * 0.01)); // Thinner webs for high density
ctx.beginPath();
for(let i=0; i < limit; i++) {
let seg = spirals[i];
// Apply wind to all control points
let p1 = applyWind(seg.p1);
let p2 = applyWind(seg.p2);
let cp = applyWind(seg.cp);
ctx.moveTo(p1.x, p1.y);
ctx.quadraticCurveTo(cp.x, cp.y, p2.x, p2.y);
}
ctx.stroke();
// 3. Draw Dew (Separate pass for color change)
if (settings.dew) {
ctx.shadowBlur = 0; // No glow on dew for crispness
for(let i=0; i < limit; i++) {
let seg = spirals[i];
if (seg.dew) {
// Recalculate positions for dew to match wind
let p1 = applyWind(seg.p1);
let p2 = applyWind(seg.p2);
let cp = applyWind(seg.cp);
drawDewOnCurve(p1, cp, p2);
}
}
}
// Logic for Construction Animation
if (isConstructing) {
let speed = Math.floor(settings.speed + (currentDrawIndex / 50));
currentDrawIndex += speed;
if (currentDrawIndex >= spirals.length) {
currentDrawIndex = spirals.length;
isConstructing = false;
}
}
}
function animate(timestamp) {
time = timestamp || 0;
renderFrame();
animationFrameId = requestAnimationFrame(animate);
}
// UI Interactions
function updateDisplay(id, val) {
const el = document.getElementById(`val-${id}`);
if (el) el.innerText = val;
}
// Attach listeners to all range inputs
[‘density’, ‘spacing’, ‘centerSize’, ‘slack’, ‘chaos’, ‘tears’, ‘glow’, ‘wind’].forEach(id => {
const input = document.getElementById(id);
input.addEventListener(‘input’, (e) => {
let val = parseFloat(e.target.value);
// Special formatting
if (id === ‘tears’) updateDisplay(id, Math.round(val * 100) + ‘%’);
else if (id === ‘glow’) updateDisplay(id, val === 0 ? ‘Off’ : (val > 15 ? ‘High’ : ‘Low’));
else updateDisplay(id, val);
// Map to settings
if (id === ‘density’) settings.spokeCount = parseInt(val);
else if (id === ‘spacing’) settings.spacing = parseInt(val);
else if (id === ‘centerSize’) settings.centerSize = parseInt(val);
else settings[id] = val;
// Regen logic
if ([‘density’, ‘spacing’, ‘centerSize’, ‘chaos’, ‘tears’, ‘slack’].includes(id)) {
generateWebData();
if (!settings.instant && !isConstructing) startDrawing(); // Restart anim if structural change
}
});
});
// Theme Buttons
themeBtns.forEach(btn => {
btn.addEventListener(‘click’, (e) => {
themeBtns.forEach(b => b.classList.remove(‘border-indigo-500’));
themeBtns.forEach(b => b.classList.add(‘border-transparent’));
e.target.classList.remove(‘border-transparent’);
e.target.classList.add(‘border-indigo-500’);
settings.theme = e.target.getAttribute(‘data-theme’);
startDrawing();
});
});
document.getElementById(‘dew-toggle’).addEventListener(‘change’, (e) => {
settings.dew = e.target.checked;
});
document.getElementById(‘instant-toggle’).addEventListener(‘change’, (e) => {
settings.instant = e.target.checked;
});
document.getElementById(‘generate-btn’).addEventListener(‘click’, () => {
generateWebData();
startDrawing();
});
document.getElementById(‘clear-btn’).addEventListener(‘click’, () => {
isConstructing = false;
currentDrawIndex = 0;
spokes = [];
spirals = [];
ctx.clearRect(0,0,width,height);
});
document.getElementById(‘download-btn’).addEventListener(‘click’, () => {
const link = document.createElement(‘a’);
link.download = `spiderweb_${settings.theme}.png`;
link.href = canvas.toDataURL();
link.click();
});
// Zen Mode Logic
const closeBtn = document.getElementById(‘close-btn’);
let uiVisible = true;
function toggleUI() {
uiVisible = !uiVisible;
if (uiVisible) {
uiLayer.classList.remove(‘translate-x-full’);
openBtn.classList.add(‘hidden’);
} else {
uiLayer.classList.add(‘translate-x-full’);
// Remove hidden immediately, CSS transitions handle the rest if you want opacity,
// but for now simple display toggling ensures it’s clickable.
openBtn.classList.remove(‘hidden’);
}
}
closeBtn.addEventListener(‘click’, toggleUI);
openBtn.addEventListener(‘click’, toggleUI);
document.addEventListener(‘click’, (e) => {
// If Zen mode is active (UI hidden) and we aren’t clicking the restore button
if (!uiVisible && !openBtn.contains(e.target)) {
// Gentle regen in Zen mode
generateWebData();
startDrawing();
}
});
// Start
resize();
animate();
</script>
</body>
</html>
I messaged myself someone else’s Epstein story and IG put me on restriction. I can still post and even share stories, but I’m not allowed to message! Yeah, *I’M* the problem! So if you messaged me, I can’t respond for 3 days. Thanks IG for protecting PDFs!

Guess what’s coming?
#doodle #fridaythe13th #thursdaythe12thisthenewfridaythe13th #thursdaythe12th

Kitty is new to me, not sure which of the venison brood came by
“The Hangman” from 1964 based on the poem by Maurice Ogden. Film made by Les Goldman and Paul Julian. “The 1964 animated short film Hangman, directed by Paul Julian and Les Goldman, is a 16.4K cautionary tale about apathy and complicity, based on Maurice Ogden’s 1951 poem. It depicts a mysterious figure erecting a gallows in a town square, systematically executing citizens while the fearful populace: remains silent, only for the narrator to realize he is next.”
The ending of "The Hangman" from 1964 based on the poem by Maurice Ogden. Film made by Les Goldman and Paul Julian.
— Scottobear (@scottobear.bsky.social) 2026-02-12T09:48:05.228Z

Thereโs a specific kind of internal hum that starts the moment you hit “Checkout” on a piece of niche tech. Itโs been a minute since Iโve been this genuinely keyed up for a mail call, but the wait for the x4 has me checking tracking tabs like itโs a competitive sport.
To keep my sanity, I did what any self-respecting nerd does: I opened a spreadsheet.
Putting the wait into perspective helps dampen the “where is it?” anxiety. My device left Shenzhen nine days ago. Since then, itโs been navigating a gauntlet of weather patterns, logistics hubs, and handoff hops.
Here is the breakdown of its 7,842-mile trek:
* Distance Covered: 7,105 miles (roughly 90% of the trip).
* Average Speed: A steady 36 mph.
* Touchpoints: 8 distinct scans across the globe.
Itโs currently cleared the customs hurdle, and with the bulk of the journey in the rearview mirror, Iโm optimistic for a delivery before the 14th.
Some people have that “buy it and forget it” zen mindset. I am not those people. I canโt just let it arrive “whenever.” To bridge the gap, Iโve been curate-stacking my digital environment:
* Customizing the Vibe: Designing a fresh set of wallpapers.
* Community Building: Getting the account settled over at readme.club.
* The Library: Hoarding choice EPUB files like a digital dragon.
Iโm also diving back into Calibre. I haven’t touched the software in nearly a decade, but seeing that itโs still the gold standard for library management is a testament to its staying power. Getting a Calibre server spun up is next on the weekend warrior list.
The SD card is “ready-ish,” and the Android apps are already staged on my phone and tablet. Software-wise, Iโm leaning toward Crosspoint. If I can swing a multi-boot setup to test the waters of every OS flavor available, thatโs the dream.
An afternoon hike along the Appalachian Trail, running roughly parallel to the Blue Ridge Parkway in Botetourt, was timed just before the snowfall so the snow showers could be seen as they arrived. A brief stop at Purgatory Overlook offered this captured view, one that never loses its appeal no matter the season.




















Woolgathering: What most people call their “self” is, for the most part, a bundle of different moods, impulses, and roles fighting for control. “I decided” usually means “one impulse temporarily won.”
If this sounds preposterous, spend some time in honest “self-observation”. I’m betting you find a constellation of “selves” rather a self.
The “self” you experience when hearing a nostalgic song, for example, can be very different from the “self” that you experience after stubbing your toe or getting cut off in traffic. Often dramatically different.
In the episode “Troubled Waters,” the good Lieutenant seems to pass some gas, and it can clearly be heard. After Columbo first examines the crime scene, he heads to the doctor’s office to get help for his seasickness. Just as he reaches the top of the steps, just before he reaches the doctor’s door, you can clearly hear two farts. He even sighs after the first one. Now, I thought it might have been the floor creaking or the ship settling, but that particular sound is never heard again in the episode. So, say what you will, but I’m convinced it’s a fart, and the sound guy missed it… or perhaps they left it in. After all, he was experiencing problems with his tummy.








Link to self-contained html
Latest ver:
https://svonberg.org/wp-content/uploads/2026/02/x4paper2.html
Added a dungeon and a pollack, and variant aliens
https://svonberg.org/wp-content/uploads/2026/02/x4paper3.html
Original:
https://svonberg.org/wp-content/uploads/2026/02/x4paper.html
# Animated 1-Bit Widget
A single-file, canvas-based generative art widget.
Animated procedural backgrounds, pixel-style drawing, and BMP export โ all in vanilla JavaScript.
—
## Features
– Animated procedural patterns
– Pixel-style drawing (mouse + touch)
– Text stamping tool
– Speed control and pause
– Export to BMP (client-side, no server)
– Mobile-friendly UI
—
## Usage
1. Download or clone the repo
2. Open the HTML file in a modern browser
3. Tap the menu button to change modes or export
No build steps. No dependencies.
—
## Tech
– HTML5 Canvas
– Vanilla JavaScript
– Tailwind CSS (CDN)
—
## Export
Snapshots combine background + drawing layers and are saved as uncompressed BMP files for crisp pixel output.
—
## License
Free to use, modify, and remix.