Replace masonry grid with an absolute-positioned 12-col canvas: drag to move, corner to resize, per-card free/overlap toggle (Alt = no-snap). Geometry persisted (migration 027: dashboard_layout.geom + extras). Two new addable decorative cards: blank spacer + animated blackflame (canvas particle flame). Old layout auto-migrates by flow-placement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
151 lines
5.8 KiB
HTML
151 lines
5.8 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<title>Blackflame card — mockup</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@500;600&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root{
|
|
--bg:#0a0a0e; --panel:#14141c; --panel-2:#1c1c26; --border:#2a2a36;
|
|
--text:#e8e6ed; --muted:#888094;
|
|
--accent:#ff4f2e; --accent-dim:#7a2716; --accent-soft:#3a1610;
|
|
--font-display:'Cinzel',serif; --font-mono:'JetBrains Mono',monospace;
|
|
}
|
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
body{background:#06060a;min-height:100vh;display:grid;place-items:center;
|
|
font-family:var(--font-mono);color:var(--text);gap:18px;padding:40px}
|
|
/* a Sacred-Valley-style card shell */
|
|
.sv-card{
|
|
width:440px;height:440px;position:relative;border-radius:14px;
|
|
background:radial-gradient(120% 120% at 50% 18%, #14131b 0%, #0b0a10 60%, #08070c 100%);
|
|
border:1px solid var(--border);overflow:hidden;
|
|
box-shadow:0 0 0 1px #00000060, 0 24px 60px -20px #000,
|
|
inset 0 0 70px -30px var(--accent-soft);
|
|
}
|
|
.sv-card::after{ /* faint inner ring */
|
|
content:"";position:absolute;inset:0;border-radius:14px;pointer-events:none;
|
|
box-shadow:inset 0 0 0 1px #ff4f2e12;
|
|
}
|
|
canvas{position:absolute;inset:0;width:100%;height:100%}
|
|
.label{
|
|
position:absolute;left:0;right:0;bottom:22px;text-align:center;z-index:3;
|
|
font-family:var(--font-display);letter-spacing:.42em;text-transform:uppercase;
|
|
font-size:15px;color:#f4ece9;text-indent:.42em;
|
|
text-shadow:0 0 18px #ff4f2e88, 0 0 4px #000;
|
|
}
|
|
.sub{
|
|
position:absolute;left:0;right:0;bottom:8px;text-align:center;z-index:3;
|
|
font-family:var(--font-mono);font-size:9.5px;letter-spacing:.32em;
|
|
text-transform:uppercase;color:#6a6475;
|
|
}
|
|
.crest{ /* the still, dark heart of the flame */
|
|
position:absolute;top:50%;left:50%;width:84px;height:84px;z-index:2;
|
|
transform:translate(-50%,-58%);border-radius:50%;
|
|
background:radial-gradient(circle at 50% 45%, #000 38%, #0a0a10 60%, transparent 72%);
|
|
box-shadow:0 0 26px 10px #000, 0 0 60px 18px #ff4f2e22;
|
|
}
|
|
.hint{font-family:var(--font-mono);font-size:11px;color:#6a6475;letter-spacing:.04em}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="sv-card" id="card">
|
|
<canvas id="fx"></canvas>
|
|
<div class="crest"></div>
|
|
<div class="label">The Void</div>
|
|
<div class="sub">Sacred Valley · core</div>
|
|
</div>
|
|
<div class="hint">Blackflame · canvas particle flame (dark heart, crimson licks, rising embers)</div>
|
|
|
|
<script>
|
|
const cv = document.getElementById('fx');
|
|
const ctx = cv.getContext('2d');
|
|
let W,H,DPR;
|
|
function resize(){
|
|
DPR = Math.min(2, window.devicePixelRatio||1);
|
|
const r = cv.getBoundingClientRect();
|
|
W = r.width; H = r.height;
|
|
cv.width = W*DPR; cv.height = H*DPR;
|
|
ctx.setTransform(DPR,0,0,DPR,0,0);
|
|
}
|
|
resize(); addEventListener('resize', resize);
|
|
|
|
// ---- rising flame: stacked soft blobs along oscillating "tongues" + embers ----
|
|
const TAU = Math.PI*2;
|
|
const tongues = Array.from({length:5}, (_,i)=>({
|
|
phase: Math.random()*TAU, speed: .6+Math.random()*.5,
|
|
x: 0.5 + (i-2)*0.085, sway: 0.03+Math.random()*0.04, w: 0.34-Math.abs(i-2)*0.05
|
|
}));
|
|
const embers = Array.from({length:46}, ()=>spawn());
|
|
function spawn(){ return {
|
|
x: 0.5 + (Math.random()-0.5)*0.5, y: 1+Math.random()*0.2,
|
|
vy: 0.0016+Math.random()*0.0030, vx:(Math.random()-0.5)*0.0012,
|
|
r: 0.6+Math.random()*1.8, life: 0, max: 120+Math.random()*120,
|
|
hot: Math.random()<0.5 };
|
|
}
|
|
|
|
function blob(x,y,r,c0,c1){
|
|
const g = ctx.createRadialGradient(x,y,0,x,y,r);
|
|
g.addColorStop(0,c0); g.addColorStop(1,c1);
|
|
ctx.fillStyle=g; ctx.beginPath(); ctx.arc(x,y,r,0,TAU); ctx.fill();
|
|
}
|
|
|
|
let t=0;
|
|
function frame(){
|
|
t += 0.016;
|
|
// base wash
|
|
ctx.globalCompositeOperation='source-over';
|
|
ctx.clearRect(0,0,W,H);
|
|
|
|
// flame body — additive so overlaps glow
|
|
ctx.globalCompositeOperation='lighter';
|
|
const baseY = H*0.92;
|
|
for(const tg of tongues){
|
|
const cx = W*(tg.x + Math.sin(t*tg.speed+tg.phase)*tg.sway);
|
|
const height = H*(0.62 + 0.08*Math.sin(t*1.7+tg.phase));
|
|
const steps = 26;
|
|
for(let s=0;s<steps;s++){
|
|
const f = s/steps; // 0 base → 1 tip
|
|
const y = baseY - f*height;
|
|
const flick = Math.sin(t*3.2 + tg.phase + f*7)*W*0.012*(0.4+f);
|
|
const x = cx + flick + Math.sin(t*tg.speed*1.3+f*4)*W*tg.sway*0.6;
|
|
const r = W*tg.w*(1-f*0.78)*(0.9+0.1*Math.sin(t*5+f*9));
|
|
// colour ramps dark-red → crimson → fades at the tip (black-flame: dark heart)
|
|
const a = (1-f)*0.5;
|
|
if(f<0.18){ blob(x,y,r*1.1, `rgba(60,12,6,${a*0.7})`, 'rgba(60,12,6,0)'); }
|
|
else if(f<0.62){ blob(x,y,r, `rgba(255,79,46,${a*0.5})`, 'rgba(122,39,22,0)'); }
|
|
else { blob(x,y,r*0.8, `rgba(255,150,90,${a*0.5})`, 'rgba(255,79,46,0)'); }
|
|
}
|
|
}
|
|
|
|
// embers
|
|
for(const e of embers){
|
|
e.life++; e.y -= e.vy; e.x += e.vx + Math.sin(t*2+e.y*8)*0.0004;
|
|
if(e.y<0.18 || e.life>e.max){ Object.assign(e, spawn()); }
|
|
const px=e.x*W, py=e.y*H, fade=1-e.life/e.max;
|
|
const col = e.hot? `rgba(255,170,110,${fade*0.9})` : `rgba(255,79,46,${fade*0.8})`;
|
|
ctx.shadowBlur=8; ctx.shadowColor='#ff4f2e';
|
|
blob(px,py,e.r*1.6, col, 'rgba(255,79,46,0)');
|
|
}
|
|
ctx.shadowBlur=0;
|
|
|
|
// carve the dark heart back in (the "black" of black-flame)
|
|
ctx.globalCompositeOperation='source-over';
|
|
const cx=W*0.5, cy=H*0.46, cr=W*0.20;
|
|
const core = ctx.createRadialGradient(cx,cy,0,cx,cy,cr);
|
|
core.addColorStop(0,'rgba(4,4,8,0.96)');
|
|
core.addColorStop(0.55,'rgba(6,5,10,0.7)');
|
|
core.addColorStop(1,'rgba(8,7,12,0)');
|
|
ctx.fillStyle=core; ctx.beginPath(); ctx.arc(cx,cy,cr,0,TAU); ctx.fill();
|
|
|
|
// base glow pool
|
|
ctx.globalCompositeOperation='lighter';
|
|
blob(W*0.5, H*0.95, W*0.4, 'rgba(255,79,46,0.10)', 'rgba(255,79,46,0)');
|
|
|
|
requestAnimationFrame(frame);
|
|
}
|
|
frame();
|
|
</script>
|
|
</body>
|
|
</html>
|