FeedGine launch

This commit is contained in:
2026-04-22 10:57:37 +02:00
commit 32b6ceba80
26 changed files with 4620 additions and 0 deletions

531
scripts/hex-background.js Normal file
View File

@@ -0,0 +1,531 @@
/**
* Space Background — Glass Shards · Animated Nebula · Drifting Stars · Shooting Stars
* No hex grid. Cursor scatters floating glass fragments.
*/
(function () {
'use strict';
let canvas, ctx;
let W = 0, H = 0;
let mouse = { x: -2000, y: -2000 };
let rafId;
let initialized = false;
let time = 0;
/* ──────────────────────────────────────────────
NEBULA BLOBS (animated radial gradients) — dimmed
────────────────────────────────────────────── */
const NEBULAS = [
{ px: 0.12, py: 0.25, pr: 0.50, cr: 255, cg: 80, cb: 0, a: 0.040, spx: 0.00014, spy: 0.00007, phase: 0.0 },
{ px: 0.82, py: 0.55, pr: 0.55, cr: 0, cg: 255, cb: 136, a: 0.028, spx:-0.00009, spy: 0.00011, phase: 2.1 },
{ px: 0.50, py: 0.88, pr: 0.42, cr: 0, cg: 180, cb: 255, a: 0.022, spx: 0.00007, spy:-0.00009, phase: 4.3 },
{ px: 0.72, py: 0.12, pr: 0.38, cr: 160, cg: 0, cb: 255, a: 0.016, spx:-0.00010, spy: 0.00005, phase: 1.5 },
{ px: 0.35, py: 0.65, pr: 0.32, cr: 255, cg: 140, cb: 0, a: 0.016, spx: 0.00005, spy:-0.00006, phase: 3.7 },
];
function drawNebulas() {
for (let i = 0; i < NEBULAS.length; i++) {
const n = NEBULAS[i];
const ox = Math.sin(time * n.spx * 800 + n.phase) * W * 0.10;
const oy = Math.cos(time * n.spy * 800 + n.phase + 1.2) * H * 0.10;
const cx = n.px * W + ox;
const cy = n.py * H + oy;
const rad = n.pr * Math.min(W, H);
const pa = n.a * (0.65 + 0.35 * Math.sin(time * 0.28 + n.phase));
const g = ctx.createRadialGradient(cx, cy, 0, cx, cy, rad);
g.addColorStop(0, `rgba(${n.cr},${n.cg},${n.cb},${pa})`);
g.addColorStop(0.45,`rgba(${n.cr},${n.cg},${n.cb},${pa * 0.35})`);
g.addColorStop(1, `rgba(${n.cr},${n.cg},${n.cb},0)`);
ctx.fillStyle = g;
ctx.beginPath();
ctx.arc(cx, cy, rad, 0, Math.PI * 2);
ctx.fill();
}
}
/* ──────────────────────────────────────────────
DRIFTING STARS — dimmed halos
────────────────────────────────────────────── */
const STAR_COUNT = 220;
let stars = [];
function initStars() {
stars = [];
for (let i = 0; i < STAR_COUNT; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * H,
r: 0.35 + Math.random() * 1.65,
vx: (Math.random() - 0.5) * 0.055,
vy: 0.018 + Math.random() * 0.055,
phase: Math.random() * Math.PI * 2,
twinkleSpeed: 0.4 + Math.random() * 1.6,
// 0=white 1=neon-green 2=neon-teal
type: Math.random() < 0.78 ? 0 : (Math.random() < 0.5 ? 1 : 2),
});
}
}
function drawStars() {
for (let i = 0; i < stars.length; i++) {
const s = stars[i];
const tw = 0.25 + 0.75 * (0.5 + 0.5 * Math.sin(time * s.twinkleSpeed + s.phase));
s.x += s.vx;
s.y += s.vy;
if (s.y > H + 2) { s.y = -2; s.x = Math.random() * W; }
if (s.x < -2) s.x = W + 2;
if (s.x > W + 2) s.x = -2;
const clr = s.type === 0
? `rgba(255,255,255,${tw * 0.70})`
: s.type === 1
? `rgba(0,255,136,${tw * 0.50})`
: `rgba(0,220,255,${tw * 0.50})`;
ctx.beginPath();
ctx.arc(s.x, s.y, s.r, 0, Math.PI * 2);
ctx.fillStyle = clr;
ctx.fill();
// Halo only on larger stars, dimmed
if (s.r > 1.1) {
const halo = ctx.createRadialGradient(s.x, s.y, 0, s.x, s.y, s.r * 3.5);
const ha = tw * (s.type === 0 ? 0.14 : 0.10);
halo.addColorStop(0, s.type === 0
? `rgba(255,255,255,${ha})`
: s.type === 1
? `rgba(0,255,136,${ha})`
: `rgba(0,220,255,${ha})`);
halo.addColorStop(1, 'rgba(0,0,0,0)');
ctx.beginPath();
ctx.arc(s.x, s.y, s.r * 3.5, 0, Math.PI * 2);
ctx.fillStyle = halo;
ctx.fill();
}
}
}
/* ──────────────────────────────────────────────
SHOOTING STARS
────────────────────────────────────────────── */
const MAX_SHOOTING_STARS = 3;
let shootingStars = [];
let nextShootingStarAt = 0; // time value when next star spawns
function spawnShootingStar() {
// Spawn from top or right edge, travel down-left or down-right
const fromRight = Math.random() < 0.5;
const startX = fromRight ? W * (0.5 + Math.random() * 0.6) : W * Math.random() * 0.7;
const startY = Math.random() * H * 0.45;
const angle = (Math.PI / 4) + (Math.random() - 0.5) * 0.6; // ~45° downward
const speed = 6 + Math.random() * 9;
const length = 80 + Math.random() * 160;
shootingStars.push({
x: startX,
y: startY,
vx: Math.cos(angle) * speed * (fromRight ? -1 : 1),
vy: Math.sin(angle) * speed,
length,
alpha: 0,
fadeIn: true,
life: 0,
maxLife: (length / speed) * 1.6, // frames to live
});
}
function updateDrawShootingStars() {
// Possibly spawn a new one
if (shootingStars.length < MAX_SHOOTING_STARS && time > nextShootingStarAt) {
spawnShootingStar();
// Next star between 414 seconds of time units (time += 0.016/frame)
nextShootingStarAt = time + 4 + Math.random() * 10;
}
for (let i = shootingStars.length - 1; i >= 0; i--) {
const s = shootingStars[i];
s.life++;
// Fade in quickly, fade out near end
if (s.life < 8) {
s.alpha = s.life / 8;
} else if (s.life > s.maxLife - 10) {
s.alpha = Math.max(0, (s.maxLife - s.life) / 10);
} else {
s.alpha = 1;
}
s.x += s.vx;
s.y += s.vy;
// Draw trail
const tailX = s.x - (s.vx / Math.hypot(s.vx, s.vy)) * s.length;
const tailY = s.y - (s.vy / Math.hypot(s.vx, s.vy)) * s.length;
const grad = ctx.createLinearGradient(s.x, s.y, tailX, tailY);
grad.addColorStop(0, `rgba(255,255,255,${s.alpha * 0.90})`);
grad.addColorStop(0.15,`rgba(220,235,255,${s.alpha * 0.55})`);
grad.addColorStop(1, `rgba(180,210,255,0)`);
ctx.beginPath();
ctx.moveTo(s.x, s.y);
ctx.lineTo(tailX, tailY);
ctx.strokeStyle = grad;
ctx.lineWidth = 1.5;
ctx.stroke();
// Bright head dot
ctx.beginPath();
ctx.arc(s.x, s.y, 1.4, 0, Math.PI * 2);
ctx.fillStyle = `rgba(255,255,255,${s.alpha * 0.95})`;
ctx.fill();
// Remove if expired or off-screen
if (s.life >= s.maxLife || s.x < -50 || s.x > W + 50 || s.y > H + 50) {
shootingStars.splice(i, 1);
}
}
}
/* ──────────────────────────────────────────────
GLASS SHARDS — fixed physics
────────────────────────────────────────────── */
const SHARD_COUNT = 30;
const SHARD_RADIUS = 230; // mouse influence radius
const SCATTER_FORCE = 3.0;
const ROT_V_MAX = 0.018; // cap on rotation speed
const SHARD_COLORS = [
{ r: 255, g: 102, b: 0 }, // neon orange
{ r: 0, g: 255, b: 136 }, // neon green
{ r: 0, g: 220, b: 255 }, // neon teal
{ r: 255, g: 255, b: 255 }, // white
{ r: 255, g: 180, b: 0 }, // amber
];
let shards = [];
function buildVerts(size, sides) {
const verts = [];
const base = Math.random() * Math.PI * 2;
for (let i = 0; i < sides; i++) {
const a = base + (Math.PI * 2 * i / sides) + (Math.random() - 0.5) * 0.65;
const r = size * (0.55 + Math.random() * 0.45);
verts.push([Math.cos(a) * r, Math.sin(a) * r]);
}
return verts;
}
function initShards() {
shards = [];
for (let i = 0; i < SHARD_COUNT; i++) {
const size = 11 + Math.random() * 42;
const sides = 3 + Math.floor(Math.random() * 3); // 35 sides
const col = SHARD_COLORS[Math.floor(Math.random() * SHARD_COLORS.length)];
shards.push({
x: Math.random() * W,
y: Math.random() * H,
vx: (Math.random() - 0.5) * 0.22,
vy: (Math.random() - 0.5) * 0.22,
rot: Math.random() * Math.PI * 2,
rotV: (Math.random() - 0.5) * 0.003, // very gentle initial spin
verts: buildVerts(size, sides),
size, col,
alpha: 0.18 + Math.random() * 0.24,
alphaTarget: 0.18 + Math.random() * 0.24,
svx: 0, svy: 0,
phase: Math.random() * Math.PI * 2,
floatSpeed: 0.20 + Math.random() * 0.45, // slower float
});
}
}
function drawShard(s) {
ctx.save();
ctx.translate(s.x, s.y);
ctx.rotate(s.rot);
const { r, g, b } = s.col;
const al = s.alpha;
ctx.beginPath();
ctx.moveTo(s.verts[0][0], s.verts[0][1]);
for (let i = 1; i < s.verts.length; i++) ctx.lineTo(s.verts[i][0], s.verts[i][1]);
ctx.closePath();
// Glass fill
const fill = ctx.createLinearGradient(-s.size, -s.size, s.size * 0.6, s.size * 0.6);
fill.addColorStop(0, `rgba(${r},${g},${b},${al * 0.18})`);
fill.addColorStop(0.45,`rgba(255,255,255,${al * 0.08})`);
fill.addColorStop(1, `rgba(${r},${g},${b},${al * 0.03})`);
ctx.fillStyle = fill;
ctx.fill();
// Neon outline — dimmed
ctx.shadowColor = `rgba(${r},${g},${b},0.45)`;
ctx.shadowBlur = 7;
ctx.strokeStyle = `rgba(${r},${g},${b},${al * 0.70})`;
ctx.lineWidth = 1.0;
ctx.stroke();
// Soft outer glow — dimmed
ctx.shadowBlur = 14;
ctx.strokeStyle = `rgba(${r},${g},${b},${al * 0.18})`;
ctx.lineWidth = 2.0;
ctx.stroke();
ctx.shadowBlur = 0;
// Inner highlight
if (s.verts.length >= 2) {
ctx.beginPath();
ctx.moveTo(s.verts[0][0] * 0.55, s.verts[0][1] * 0.55);
ctx.lineTo(s.verts[1][0] * 0.55, s.verts[1][1] * 0.55);
ctx.strokeStyle = `rgba(255,255,255,${al * 0.40})`;
ctx.lineWidth = 0.8;
ctx.shadowBlur = 3;
ctx.shadowColor = 'rgba(255,255,255,0.3)';
ctx.stroke();
ctx.shadowBlur = 0;
}
ctx.restore();
}
function updateShards() {
const infSq = SHARD_RADIUS * SHARD_RADIUS;
for (let i = 0; i < shards.length; i++) {
const s = shards[i];
// Gentle float bob
s.vy += Math.sin(time * s.floatSpeed + s.phase) * 0.002;
// Mouse scatter
if (mouse.x > -1000) {
const dx = s.x - mouse.x;
const dy = s.y - mouse.y;
const distSq = dx * dx + dy * dy;
if (distSq < infSq) {
const dist = Math.sqrt(distSq);
const force = (1 - dist / SHARD_RADIUS) * SCATTER_FORCE;
const ang = Math.atan2(dy, dx);
s.svx += Math.cos(ang) * force * 0.065;
s.svy += Math.sin(ang) * force * 0.065;
// Small nudge to rotation — NOT multiplicative
s.rotV += (Math.random() - 0.5) * 0.004 * (1 - dist / SHARD_RADIUS);
// Brighten on interaction
s.alphaTarget = Math.min(0.75, s.alphaTarget + 0.03);
}
}
// Rotation damping — keeps shards from spinning endlessly
s.rotV *= 0.96;
// Hard cap on rotation speed
if (s.rotV > ROT_V_MAX) s.rotV = ROT_V_MAX;
if (s.rotV < -ROT_V_MAX) s.rotV = -ROT_V_MAX;
// Scatter velocity damping
s.svx *= 0.92;
s.svy *= 0.92;
s.vx *= 0.998;
s.vy *= 0.998;
// Apply
s.x += s.vx + s.svx;
s.y += s.vy + s.svy;
s.rot += s.rotV;
// Fade alpha toward target
s.alpha += (s.alphaTarget - s.alpha) * 0.025;
// Slowly restore target
const base = 0.18;
if (s.alphaTarget > base) s.alphaTarget -= 0.006;
// Edge wrap
const m = s.size + 10;
if (s.x < -m) s.x = W + m;
if (s.x > W + m) s.x = -m;
if (s.y < -m) s.y = H + m;
if (s.y > H + m) s.y = -m;
}
}
/* ──────────────────────────────────────────────
CURSOR AURA — dimmed
────────────────────────────────────────────── */
function drawCursorAura() {
const r = 130 + Math.sin(time * 1.8) * 22;
const aura = ctx.createRadialGradient(mouse.x, mouse.y, 0, mouse.x, mouse.y, r);
aura.addColorStop(0, 'rgba(255,102,0,0.04)');
aura.addColorStop(0.5, 'rgba(255,102,0,0.012)');
aura.addColorStop(1, 'rgba(255,102,0,0)');
ctx.fillStyle = aura;
ctx.beginPath();
ctx.arc(mouse.x, mouse.y, r, 0, Math.PI * 2);
ctx.fill();
}
/* ──────────────────────────────────────────────
MAIN LOOP
────────────────────────────────────────────── */
function animate() {
if (!initialized) return;
time += 0.016;
ctx.clearRect(0, 0, W, H);
drawNebulas();
drawStars();
updateDrawShootingStars();
updateShards();
for (let i = 0; i < shards.length; i++) drawShard(shards[i]);
if (mouse.x > -1000) drawCursorAura();
rafId = requestAnimationFrame(animate);
}
/* ──────────────────────────────────────────────
MOBILE — one-shot static render
────────────────────────────────────────────── */
function initMobileStatic() {
const c = document.getElementById('hexCanvas');
if (!c) return;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
const w = window.innerWidth;
const h = window.innerHeight;
c.width = w * dpr;
c.height = h * dpr;
c.style.width = w + 'px';
c.style.height = h + 'px';
const cx = c.getContext('2d');
cx.scale(dpr, dpr);
// Nebula blobs — dimmed
const g1 = cx.createRadialGradient(w*0.15, h*0.3, 0, w*0.15, h*0.3, w*0.5);
g1.addColorStop(0, 'rgba(255,80,0,0.04)'); g1.addColorStop(1, 'rgba(255,80,0,0)');
cx.fillStyle = g1; cx.fillRect(0, 0, w, h);
const g2 = cx.createRadialGradient(w*0.82, h*0.65, 0, w*0.82, h*0.65, w*0.45);
g2.addColorStop(0, 'rgba(0,255,136,0.03)'); g2.addColorStop(1, 'rgba(0,255,136,0)');
cx.fillStyle = g2; cx.fillRect(0, 0, w, h);
const g3 = cx.createRadialGradient(w*0.5, h*0.85, 0, w*0.5, h*0.85, w*0.4);
g3.addColorStop(0, 'rgba(0,180,255,0.025)'); g3.addColorStop(1, 'rgba(0,180,255,0)');
cx.fillStyle = g3; cx.fillRect(0, 0, w, h);
// Stars
for (let i = 0; i < 130; i++) {
const sx = Math.random() * w;
const sy = Math.random() * h;
const sr = 0.35 + Math.random() * 1.5;
const sa = 0.20 + Math.random() * 0.55;
cx.beginPath();
cx.arc(sx, sy, sr, 0, Math.PI * 2);
cx.fillStyle = `rgba(255,255,255,${sa})`;
cx.fill();
}
// Glass shards
const MCOLS = [[255,102,0],[0,255,136],[0,220,255],[255,255,255],[255,180,0]];
for (let i = 0; i < 14; i++) {
const sx = Math.random() * w;
const sy = Math.random() * h;
const ss = 10 + Math.random() * 35;
const sides = 3 + Math.floor(Math.random() * 3);
const [cr,cg,cb] = MCOLS[Math.floor(Math.random() * MCOLS.length)];
cx.save();
cx.translate(sx, sy);
cx.rotate(Math.random() * Math.PI * 2);
cx.beginPath();
for (let j = 0; j < sides; j++) {
const a = (Math.PI * 2 * j / sides) + (Math.random()-0.5)*0.6;
const r = ss * (0.55 + Math.random() * 0.45);
j === 0 ? cx.moveTo(Math.cos(a)*r, Math.sin(a)*r)
: cx.lineTo(Math.cos(a)*r, Math.sin(a)*r);
}
cx.closePath();
cx.fillStyle = `rgba(${cr},${cg},${cb},0.04)`;
cx.strokeStyle = `rgba(${cr},${cg},${cb},0.38)`;
cx.lineWidth = 1;
cx.fill();
cx.shadowColor = `rgba(${cr},${cg},${cb},0.45)`;
cx.shadowBlur = 6;
cx.stroke();
cx.shadowBlur = 0;
cx.restore();
}
}
/* ──────────────────────────────────────────────
INIT
────────────────────────────────────────────── */
function init() {
if (window.innerWidth <= 768 || ('ontouchstart' in window)) {
initMobileStatic();
return;
}
canvas = document.getElementById('hexCanvas');
if (!canvas) return;
ctx = canvas.getContext('2d', { alpha: true });
resize();
window.addEventListener('resize', debounce(resize, 200));
document.addEventListener('mousemove', e => {
mouse.x = e.clientX;
mouse.y = e.clientY;
}, { passive: true });
document.addEventListener('mouseleave', () => {
mouse.x = -2000;
mouse.y = -2000;
});
// First shooting star after a short delay
nextShootingStarAt = 3 + Math.random() * 5;
initialized = true;
animate();
}
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, 2);
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
canvas.style.position = 'fixed';
canvas.style.top = '0';
canvas.style.left = '0';
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.scale(dpr, dpr);
initStars();
initShards();
}
function debounce(fn, ms) {
let t;
return (...a) => { clearTimeout(t); t = setTimeout(() => fn(...a), ms); };
}
function destroy() {
if (rafId) cancelAnimationFrame(rafId);
initialized = false;
stars = []; shards = []; shootingStars = [];
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.HexBackground = { init, destroy };
})();