tinyweb/themes/kodama2.html
2026-06-05 05:29:36 +00:00

1395 lines
No EOL
51 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="referrer" content="no-referrer">
<meta http-equiv="x-dns-prefetch-control" content="off">
<style>
* { margin: 0; padding: 0; box-sizing: border-box;
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='3' fill='none' stroke='%23557766' stroke-width='1.5'/%3E%3Ccircle cx='10' cy='10' r='1' fill='%2377aa88'/%3E%3C/svg%3E") 10 10, default;
}
html, body { min-height: 100vh; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 1.65;
color: #9ab4b8;
background: #070d14;
}
a, button, input[type="submit"], summary, label {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Ccircle cx='10' cy='10' r='6' fill='none' stroke='%23b8ddc8' stroke-width='1' opacity='0.6'/%3E%3Ccircle cx='10' cy='10' r='1.5' fill='%23b8ddc8'/%3E%3C/svg%3E") 10 10, pointer;
}
input, textarea {
cursor: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cline x1='10' y1='3' x2='10' y2='17' stroke='%2377aa88' stroke-width='1.5'/%3E%3C/svg%3E") 10 10, text;
}
/* deep forest vignette */
body::after {
content: '';
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: radial-gradient(ellipse at center, transparent 20%, rgba(2, 5, 10, 0.75) 100%);
pointer-events: none;
z-index: 999;
}
#scene, #trail {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
}
#scene { z-index: 0; }
#trail { z-index: 998; }
.shell {
max-width: 660px;
margin: 0 auto;
padding: 0 1.5rem;
position: relative;
z-index: 6;
}
nav {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 1.5rem 0 1.2rem;
border-bottom: 1px solid rgba(40, 70, 80, 0.3);
}
nav .site {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
font-size: 0.85rem;
font-weight: 500;
color: #b0ccc4;
text-decoration: none;
letter-spacing: 0.06em;
border-bottom: none;
transition: text-shadow 0.3s;
}
nav .site:hover {
text-shadow: 0 0 12px rgba(150, 220, 180, 0.4);
}
nav .links { display: flex; gap: 1.2rem; }
nav .links a {
font-size: 0.82rem;
color: #3a5560;
text-decoration: none;
border-bottom: none;
transition: color 0.2s, text-shadow 0.3s;
}
nav .links a:hover {
color: #90bab0;
text-shadow: 0 0 8px rgba(130, 200, 160, 0.2);
}
#greeting {
padding: 1.5rem 0 0;
font-size: 0.85rem;
color: #28454e;
font-style: italic;
opacity: 0;
animation: fadeIn 1.5s ease forwards 0.3s;
}
@keyframes fadeIn { to { opacity: 1; } }
.content { padding: 1.8rem 0 3rem; }
h1 {
font-size: 1.4rem;
font-weight: 600;
color: #b0ccc4;
margin-bottom: 1rem;
}
h1 a { color: #b0ccc4; text-decoration: none; border-bottom: none; }
h1 a:hover { color: #78a898; }
h2 {
font-size: 1.05rem;
font-weight: 600;
color: #8cb0a8;
margin: 1.8rem 0 0.5rem;
}
p { margin: 0.5rem 0; color: #5a7880; }
a {
color: #8cb0a8;
text-decoration: none;
border-bottom: 1px solid rgba(60, 100, 90, 0.3);
transition: all 0.2s;
}
a:hover {
color: #c0e4d4;
border-bottom-color: rgba(100, 160, 130, 0.4);
}
em { color: #4a6870; }
input[type="text"],
input[type="url"],
input[name="q"],
input[name="url"],
input[name="note"],
input[name="tags"],
input[name="site_name"],
input[name="dest_hash"],
input[name="title"],
input[name="manual_title"],
input[name="summary"],
input[name="transport_host"],
input[name="transport_port"] {
background: rgba(8, 18, 22, 0.8);
border: 1px solid rgba(40, 70, 65, 0.4);
border-radius: 4px;
padding: 0.6rem 0.85rem;
color: #90b4ac;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
font-size: 0.95rem;
transition: border-color 0.2s, box-shadow 0.3s;
}
input:focus, textarea:focus {
outline: none;
border-color: rgba(80, 140, 110, 0.5);
box-shadow: 0 0 18px rgba(100, 200, 150, 0.06);
}
button, input[type="submit"] {
background: rgba(10, 22, 25, 0.8);
border: 1px solid rgba(40, 70, 65, 0.4);
border-radius: 4px;
padding: 0.6rem 1.1rem;
color: #5a7880;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
font-size: 0.88rem;
transition: all 0.2s;
}
button:hover, input[type="submit"]:hover {
background: rgba(15, 35, 35, 0.8);
color: #90b4ac;
border-color: rgba(80, 130, 110, 0.5);
box-shadow: 0 0 12px rgba(100, 200, 150, 0.06);
}
.result {
padding: 1rem 0;
border-bottom: 1px solid rgba(30, 55, 60, 0.25);
transition: background 0.2s;
}
.result:hover { background: rgba(80, 160, 130, 0.02); }
.result:last-child { border-bottom: none; }
.result > a:first-child {
font-size: 1.02rem;
font-weight: 500;
color: #a0c4bc;
border-bottom: none;
}
.result > a:first-child:hover { color: #d0eee0; }
.note { margin-top: 0.3rem; font-size: 0.9rem; color: #3e6058; }
.meta {
font-size: 0.85rem;
color: #3a5560;
}
.pagination {
font-size: 0.85rem;
color: #3a5560;
margin: 1.2rem 0;
text-align: center;
}
.pagination a {
color: #5a8878;
border-bottom: 1px solid rgba(60, 100, 90, 0.3);
}
.pagination a:hover {
color: #a0d4c0;
}
.success {
color: #5a9878;
background: rgba(40, 100, 70, 0.08);
border: 1px solid rgba(40, 100, 70, 0.2);
border-radius: 4px;
padding: 0.5rem 0.85rem;
font-size: 0.9rem;
}
.tags { margin-top: 0.3rem; }
.tag, .tags a {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
font-size: 0.7rem;
color: #3a5e55;
border: 1px solid rgba(40, 70, 60, 0.35);
border-radius: 3px;
padding: 0.1rem 0.35rem;
margin-right: 0.25rem;
}
.tag:hover, .tags a:hover {
color: #78a898;
border-color: rgba(80, 130, 100, 0.4);
}
details {
margin: 1rem 0;
border: 1px solid rgba(30, 55, 50, 0.3);
border-radius: 4px;
padding: 0.7rem 0.9rem;
background: rgba(6, 14, 16, 0.6);
}
summary { font-size: 0.85rem; color: #3e6058; font-weight: 500; }
summary:hover { color: #78a898; }
details ul { margin-top: 0.5rem; padding-left: 1.2rem; }
details li { margin: 0.35rem 0; font-size: 0.9rem; }
ul, ol { padding-left: 1.2rem; margin: 0.5rem 0; }
li { margin: 0.45rem 0; color: #5a7880; }
li a { border-bottom: none; }
li a:hover { border-bottom: 1px solid rgba(80, 130, 110, 0.4); }
pre {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
font-size: 0.8rem;
background: rgba(6, 14, 16, 0.6);
border: 1px solid rgba(30, 55, 50, 0.3);
border-radius: 4px;
padding: 0.9rem;
overflow-x: auto;
color: #4e7870;
margin: 0.8rem 0;
}
code {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
font-size: 0.82rem;
background: rgba(8, 18, 22, 0.6);
border-radius: 3px;
padding: 0.1rem 0.35rem;
color: #5a7880;
}
textarea {
background: rgba(6, 14, 16, 0.6);
border: 1px solid rgba(40, 70, 65, 0.4);
border-radius: 4px;
padding: 0.7rem 0.9rem;
color: #9ab4b8;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
font-size: 0.8rem;
line-height: 1.6;
resize: vertical;
width: 100%;
}
table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
th {
text-align: left;
font-size: 0.72rem;
font-weight: 500;
color: #3a5560;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 0.5rem 0.7rem;
border-bottom: 1px solid rgba(30, 55, 50, 0.3);
}
td {
padding: 0.5rem 0.7rem;
border-bottom: 1px solid rgba(20, 40, 40, 0.25);
font-size: 0.9rem;
}
label { color: #5a7880; }
input[type="checkbox"] { accent-color: #3a6858; }
hr { border: none; border-top: 1px solid rgba(30, 55, 50, 0.3); margin: 1rem 0; }
small {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
font-size: 0.7rem;
color: #28454e;
}
footer {
border-top: 1px solid rgba(30, 55, 60, 0.25);
padding: 1.5rem 0 2rem;
text-align: center;
color: #1e3840;
font-size: 0.8rem;
}
footer .clock {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
font-size: 0.72rem;
color: #162a30;
margin-top: 0.25rem;
}
::selection { background: #1a3838; color: #d0eedc; }
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(30, 55, 50, 0.4); border-radius: 3px; }
@media (max-width: 600px) {
nav { flex-direction: column; gap: 0.5rem; }
nav .links { gap: 0.8rem; flex-wrap: wrap; }
h1 { font-size: 1.2rem; }
}
</style>
</head>
<body>
<canvas id="scene"></canvas>
<canvas id="trail"></canvas>
<div class="shell">
<nav>
<a class="site" href="/">{{site_name}}</a>
<div class="links">
<a href="/pages">browse</a>
<a href="/tags">tags</a>
<a href="/subscriptions">network</a>
{{forum_link}}
<a href="/style">customize</a>
<a href="/about">about</a>
</div>
</nav>
<div id="greeting"></div>
<div class="content">
{{content}}
</div>
<footer>
<div>curated by hand &middot; shared over mesh</div>
<div class="clock" id="clock"></div>
</footer>
</div>
<script>
(function() {
var W, H, t;
var scene = document.getElementById('scene');
var ctx = scene.getContext('2d');
var trailC = document.getElementById('trail');
var tctx = trailC.getContext('2d');
// seeded rng
var _s = 42;
function sr() { _s = (_s * 16807) % 2147483647; return (_s - 1) / 2147483646; }
function resetSeed(v) { _s = v || 42; }
/* ══════════════════════════════════════
SCENE GENERATION — branches only at edges
══════════════════════════════════════ */
var trunks = [];
var midBranches = [];
var fgBranches = [];
var perchSpots = [];
var fogClouds = [];
var farCanopy = [];
var midFoliage = [];
var nearLeafClusters = [];
function genBranch(x, y, angle, len, segs) {
var pts = [{ x: x, y: y }];
var cx = x, cy = y, step = len / segs, droop = 0;
for (var i = 0; i < segs; i++) {
droop += 0.03 + sr() * 0.05;
cx += Math.cos(angle + droop) * step * (0.75 + sr() * 0.5);
cy += Math.sin(angle + droop) * step * (0.75 + sr() * 0.5);
pts.push({ x: cx, y: cy });
}
return pts;
}
function genTrunk(x0, y0, x1, y1, bw, lean) {
var pts = [], steps = 10;
for (var i = 0; i <= steps; i++) {
var p = i / steps;
pts.push({
x: x0 + (x1 - x0) * p + Math.sin(p * 2.5 + lean) * bw * 0.6,
y: y0 + (y1 - y0) * p
});
}
return { pts: pts, w: bw };
}
function generateScene() {
trunks = []; midBranches = []; fgBranches = []; perchSpots = [];
resetSeed(42);
// four trunks — two main at edges, two thinner secondary ones
trunks.push(genTrunk(W * 0.05, H * 1.05, W * 0.03, H * -0.08, 34 + sr() * 14, 0.2));
trunks.push(genTrunk(W * 0.93, H * 1.05, W * 0.95, H * -0.03, 28 + sr() * 12, -0.15));
trunks.push(genTrunk(W * 0.18, H * 1.05, W * 0.15, H * 0.08, 16 + sr() * 8, 0.35));
trunks.push(genTrunk(W * 0.82, H * 1.05, W * 0.85, H * 0.05, 14 + sr() * 7, -0.3));
// branches from trunks — more generous branching
for (var ti = 0; ti < trunks.length; ti++) {
var trunk = trunks[ti];
var pts = trunk.pts;
var isLeft = pts[0].x < W * 0.5;
for (var j = 1; j < pts.length - 1; j++) {
if (sr() < 0.65) {
var inward = isLeft ? 1 : -1;
var dir = sr() < 0.7 ? inward : -inward;
var angle = (sr() * 0.55 - 0.15) * dir;
var blen = W * (0.08 + sr() * 0.28);
var bw = trunk.w * (0.2 + sr() * 0.3) * (1 - j / pts.length * 0.35);
var bpts = genBranch(pts[j].x, pts[j].y, angle, blen, 3 + Math.floor(sr() * 3));
var isFg = j > pts.length * 0.65;
if (isFg) {
fgBranches.push({ pts: bpts, w: bw });
} else {
midBranches.push({ pts: bpts, w: bw });
for (var k = 1; k < bpts.length; k++) {
if (sr() < 0.55) perchSpots.push({ x: bpts[k].x, y: bpts[k].y - bw * 0.4 });
}
}
// sub-branches — more frequent
if (sr() < 0.5 && bpts.length > 2) {
var si = 1 + Math.floor(sr() * (bpts.length - 2));
var sAngle = angle + (sr() - 0.5) * 1.4;
var sLen = blen * (0.25 + sr() * 0.2);
var sPts = genBranch(bpts[si].x, bpts[si].y, sAngle, sLen, 2 + Math.floor(sr() * 2));
(isFg ? fgBranches : midBranches).push({ pts: sPts, w: bw * 0.35 });
// tertiary twigs
if (sr() < 0.35 && sPts.length > 1) {
var ti2 = Math.floor(sr() * sPts.length);
var tAngle = sAngle + (sr() - 0.5) * 1.8;
var tPts = genBranch(sPts[ti2].x, sPts[ti2].y, tAngle, sLen * 0.4, 2);
(isFg ? fgBranches : midBranches).push({ pts: tPts, w: bw * 0.15 });
}
}
}
}
}
// background depth branches — faint, thin, behind everything
// these come from off-screen to add depth
var bgBranches = [];
for (var i = 0; i < 6; i++) {
var fromLeft = sr() < 0.5;
var sx = fromLeft ? W * (-0.05 + sr() * 0.15) : W * (0.85 + sr() * 0.2);
var sy = H * (0.1 + sr() * 0.5);
var inward = fromLeft ? 1 : -1;
var angle = (sr() * 0.4 - 0.1) * inward;
var bpts = genBranch(sx, sy, angle, W * (0.15 + sr() * 0.25), 3 + Math.floor(sr() * 2));
midBranches.push({ pts: bpts, w: 3 + sr() * 5, bg: true });
}
// 4 diagonal crossing branches from off-screen edges
for (var i = 0; i < 4; i++) {
var fromLeft = i < 2;
var sx = fromLeft ? -15 : W + 15;
var sy = H * (0.35 + i * 0.12 + sr() * 0.15);
var angle = fromLeft ? -0.1 + sr() * 0.2 : Math.PI + 0.1 - sr() * 0.2;
var bpts = genBranch(sx, sy, angle, W * (0.25 + sr() * 0.25), 4 + Math.floor(sr() * 2));
var bw = 5 + sr() * 9;
fgBranches.push({ pts: bpts, w: bw });
// sub-branches on crossing branches
if (bpts.length > 2 && sr() < 0.6) {
var si = 1 + Math.floor(sr() * (bpts.length - 2));
var sAngle = angle + (sr() - 0.5) * 1.2;
var sPts = genBranch(bpts[si].x, bpts[si].y, sAngle, W * 0.08 + sr() * W * 0.1, 2);
fgBranches.push({ pts: sPts, w: bw * 0.35 });
}
}
// ── FOLIAGE GENERATION ──
// Far canopy: dense dark masses covering upper screen + sides
farCanopy = [];
var canopyDefs = [
// x-factor, y-factor, size, blobCount
// top-right cluster (dense)
[0.88, 0.00, 200, 35], [0.75, 0.04, 170, 30], [0.95, 0.08, 160, 28],
[0.82, 0.12, 130, 22], [0.68, 0.10, 110, 18],
// top-left cluster
[0.05, 0.02, 190, 32], [0.18, 0.06, 155, 26], [0.10, 0.13, 120, 20],
// top center (sparser, gap for sky)
[0.42, 0.00, 100, 16], [0.55, 0.03, 90, 14], [0.35, 0.06, 80, 12],
// side masses that drape down
[0.98, 0.22, 110, 18], [0.02, 0.25, 120, 20],
[0.92, 0.30, 80, 14], [0.07, 0.33, 85, 14],
// mid-height masses at edges
[0.96, 0.42, 70, 12], [0.04, 0.45, 75, 12]
];
for (var i = 0; i < canopyDefs.length; i++) {
var def = canopyDefs[i];
var mass = {
x: W * def[0], y: H * def[1],
blobs: [], edgeLeaves: [],
size: def[2]
};
for (var j = 0; j < def[3]; j++) {
var a = sr() * Math.PI * 2;
var dist = sr() * mass.size * 0.7;
mass.blobs.push({
ox: Math.cos(a) * dist,
oy: Math.sin(a) * dist * 0.5 - mass.size * 0.12,
r: mass.size * (0.18 + sr() * 0.32)
});
}
var edgeCount = 12 + Math.floor(sr() * 14);
for (var j = 0; j < edgeCount; j++) {
var a = sr() * Math.PI * 2;
var dist = mass.size * (0.5 + sr() * 0.5);
mass.edgeLeaves.push({
ox: Math.cos(a) * dist,
oy: Math.sin(a) * dist * 0.5,
sz: 5 + sr() * 12,
rot: a + sr() * 0.8 - 0.4
});
}
farCanopy.push(mass);
}
// Mid foliage: dense clumps at branch points + extra scattered ones
midFoliage = [];
var allMidPts = [];
for (var i = 0; i < midBranches.length; i++) {
var bpts = midBranches[i].pts;
for (var k = 1; k < bpts.length; k++) {
allMidPts.push({ x: bpts[k].x, y: bpts[k].y });
}
}
for (var ti = 0; ti < trunks.length; ti++) {
var tpts = trunks[ti].pts;
for (var k = 1; k < 6 && k < tpts.length; k++) {
allMidPts.push({ x: tpts[k].x, y: tpts[k].y });
}
}
// also add some free-floating canopy points in upper third
for (var i = 0; i < 8; i++) {
allMidPts.push({ x: W * (0.1 + sr() * 0.8), y: H * (0.05 + sr() * 0.3) });
}
var usedPts = [];
for (var i = 0; i < Math.min(allMidPts.length, 24); i++) {
var pi = Math.floor(sr() * allMidPts.length);
var pt = allMidPts[pi];
var tooClose = false;
for (var u = 0; u < usedPts.length; u++) {
var ddx = pt.x - usedPts[u].x, ddy = pt.y - usedPts[u].y;
if (ddx * ddx + ddy * ddy < 1800) { tooClose = true; break; }
}
if (tooClose) continue;
usedPts.push(pt);
var clumpSize = 30 + sr() * 45;
var cluster = {
x: pt.x + (sr() - 0.5) * 25,
y: pt.y - clumpSize * 0.25 + (sr() - 0.5) * 15,
blobs: [], edgeLeaves: [], size: clumpSize
};
var blobCount = 10 + Math.floor(sr() * 10);
for (var j = 0; j < blobCount; j++) {
var a = sr() * Math.PI * 2;
var dist = sr() * clumpSize * 0.55;
cluster.blobs.push({
ox: Math.cos(a) * dist,
oy: Math.sin(a) * dist * 0.6 - clumpSize * 0.1,
r: clumpSize * (0.22 + sr() * 0.28)
});
}
var ec = 8 + Math.floor(sr() * 10);
for (var j = 0; j < ec; j++) {
var a = sr() * Math.PI * 2;
var dist = clumpSize * (0.35 + sr() * 0.45);
cluster.edgeLeaves.push({
ox: Math.cos(a) * dist,
oy: Math.sin(a) * dist * 0.6,
sz: 3 + sr() * 6,
rot: a + sr() * 0.7 - 0.35
});
}
midFoliage.push(cluster);
}
// Near leaf clusters: hanging bunches from foreground branches
nearLeafClusters = [];
for (var i = 0; i < fgBranches.length; i++) {
var bpts = fgBranches[i].pts;
for (var k = 1; k < bpts.length; k++) {
if (sr() < 0.5) {
var cSz = 14 + sr() * 22;
var cluster = {
x: bpts[k].x, y: bpts[k].y,
blobs: [], edgeLeaves: [], size: cSz
};
var bc = 5 + Math.floor(sr() * 5);
for (var j = 0; j < bc; j++) {
cluster.blobs.push({
ox: (sr() - 0.5) * cSz * 0.6,
oy: sr() * cSz * 0.7,
r: cSz * (0.28 + sr() * 0.22)
});
}
var ec = 5 + Math.floor(sr() * 6);
for (var j = 0; j < ec; j++) {
var a = sr() * Math.PI;
cluster.edgeLeaves.push({
ox: Math.cos(a) * cSz * 0.55,
oy: cSz * 0.25 + Math.sin(a) * cSz * 0.45,
sz: 2.5 + sr() * 4.5,
rot: Math.PI * 0.5 + (sr() - 0.5) * 0.8
});
}
nearLeafClusters.push(cluster);
}
}
}
}
// simple smooth branch stroke — single pass, clean
function strokeBranch(pts, bw, color) {
if (pts.length < 2) return;
ctx.beginPath();
ctx.moveTo(pts[0].x, pts[0].y);
for (var i = 1; i < pts.length; i++) {
if (i < pts.length - 1) {
var mx = (pts[i].x + pts[i+1].x) / 2;
var my = (pts[i].y + pts[i+1].y) / 2;
ctx.quadraticCurveTo(pts[i].x, pts[i].y, mx, my);
} else {
ctx.lineTo(pts[i].x, pts[i].y);
}
}
ctx.strokeStyle = color;
ctx.lineWidth = bw;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.stroke();
}
function drawTrunk(trunk) {
var pts = trunk.pts, bw = trunk.w;
// single smooth tapered stroke
for (var i = 1; i < pts.length; i++) {
var taper = 1 - (i / pts.length) * 0.6;
ctx.beginPath();
ctx.moveTo(pts[i-1].x, pts[i-1].y);
ctx.lineTo(pts[i].x, pts[i].y);
ctx.strokeStyle = 'rgba(5, 10, 10, 0.9)';
ctx.lineWidth = bw * taper;
ctx.lineCap = 'round';
ctx.stroke();
}
// subtle highlight edge
for (var i = 1; i < pts.length; i++) {
var taper = 1 - (i / pts.length) * 0.6;
ctx.beginPath();
ctx.moveTo(pts[i-1].x - bw * taper * 0.15, pts[i-1].y);
ctx.lineTo(pts[i].x - bw * taper * 0.15, pts[i].y);
ctx.strokeStyle = 'rgba(15, 25, 22, 0.4)';
ctx.lineWidth = bw * taper * 0.3;
ctx.lineCap = 'round';
ctx.stroke();
}
}
/* ══════════════════════════════════════
FOLIAGE — three depth layers
Dense masses with leaf-edge detail
══════════════════════════════════════ */
// draw a single leaf shape at given position
function drawLeafAt(cx, cy, sz, rot, color) {
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(rot);
ctx.beginPath();
ctx.moveTo(0, -sz);
ctx.bezierCurveTo(sz * 0.6, -sz * 0.25, sz * 0.5, sz * 0.45, 0, sz * 0.85);
ctx.bezierCurveTo(-sz * 0.5, sz * 0.45, -sz * 0.6, -sz * 0.25, 0, -sz);
ctx.fillStyle = color;
ctx.fill();
ctx.restore();
}
// draw a foliage mass: dense overlapping circles + leaf edges
function drawFoliageMass(mass, sway, coreColor, edgeColor, edgeAlpha) {
var mx = mass.x + sway;
var my = mass.y;
// solid core — many overlapping circles
for (var j = 0; j < mass.blobs.length; j++) {
var b = mass.blobs[j];
ctx.beginPath();
ctx.arc(mx + b.ox, my + b.oy, b.r, 0, Math.PI * 2);
ctx.fillStyle = coreColor;
ctx.fill();
}
// leaf-shaped edges to break the silhouette
for (var j = 0; j < mass.edgeLeaves.length; j++) {
var lf = mass.edgeLeaves[j];
var leafSway = Math.sin(t * 0.15 + j * 0.9) * 0.06;
drawLeafAt(
mx + lf.ox, my + lf.oy,
lf.sz, lf.rot + leafSway, edgeColor
);
}
}
// Layer 1: Far canopy — near-black masses at top of screen
function drawFarCanopy() {
ctx.globalAlpha = 0.65;
for (var i = 0; i < farCanopy.length; i++) {
var sway = Math.sin(t * 0.06 + i * 1.3) * 4;
drawFoliageMass(
farCanopy[i], sway,
'rgba(4, 10, 8, 0.7)',
'rgba(6, 14, 11, 0.6)',
0.6
);
}
ctx.globalAlpha = 1;
}
// Layer 2: Mid vegetation — dark green, illuminated by kodama
function drawMidFoliage() {
for (var i = 0; i < midFoliage.length; i++) {
var c = midFoliage[i];
var sway = Math.sin(t * 0.1 + i * 0.9) * 2.5;
// compute kodama illumination
var illum = 0;
for (var k = 0; k < kodamas.length; k++) {
var sp = kodamas[k];
if (sp.opacity < 0.05) continue;
var dx = c.x - sp._px, dy = c.y - sp._py;
var dist = Math.sqrt(dx * dx + dy * dy);
var reach = sp.size * sp.glowR * 4;
if (dist < reach) {
illum = Math.max(illum, (1 - dist / reach) * sp.opacity);
}
}
// core color shifts from near-black to dark green with illumination
var cr = Math.floor(5 + illum * 20);
var cg = Math.floor(12 + illum * 40);
var cb = Math.floor(9 + illum * 18);
var ca = 0.75 + illum * 0.15;
var coreCol = 'rgba(' + cr + ',' + cg + ',' + cb + ',' + ca + ')';
// edge leaves get brighter green
var er = Math.floor(8 + illum * 35);
var eg = Math.floor(18 + illum * 55);
var eb = Math.floor(12 + illum * 25);
var edgeCol = 'rgba(' + er + ',' + eg + ',' + eb + ',' + (0.65 + illum * 0.2) + ')';
ctx.globalAlpha = 0.8;
drawFoliageMass(c, sway, coreCol, edgeCol, 0.7);
}
ctx.globalAlpha = 1;
}
// Layer 3: Near leaf clusters — darker, hanging from foreground branches
function drawNearLeafClusters() {
ctx.globalAlpha = 0.88;
for (var i = 0; i < nearLeafClusters.length; i++) {
var sway = Math.sin(t * 0.14 + i * 1.1) * 1.8;
drawFoliageMass(
nearLeafClusters[i], sway,
'rgba(3, 8, 6, 0.85)',
'rgba(5, 12, 9, 0.75)',
0.75
);
}
ctx.globalAlpha = 1;
}
/* ══════════════════════════════════════
FOG — fewer, bigger, softer
══════════════════════════════════════ */
function initFog() {
fogClouds = [];
for (var i = 0; i < 10; i++) {
fogClouds.push({
x: Math.random() * W * 1.5 - W * 0.25,
y: H * (0.08 + Math.random() * 0.65),
rx: 150 + Math.random() * 300,
ry: 50 + Math.random() * 80,
speed: 0.04 + Math.random() * 0.08,
dir: Math.random() < 0.5 ? 1 : -1,
alpha: 0.02 + Math.random() * 0.04,
phase: Math.random() * Math.PI * 2,
r: 10 + Math.floor(Math.random() * 10),
g: 22 + Math.floor(Math.random() * 15),
b: 35 + Math.floor(Math.random() * 12)
});
}
}
function drawFog() {
for (var i = 0; i < fogClouds.length; i++) {
var f = fogClouds[i];
f.x += f.speed * f.dir;
if (f.dir > 0 && f.x - f.rx > W) f.x = -f.rx;
if (f.dir < 0 && f.x + f.rx < 0) f.x = W + f.rx;
var a = f.alpha * (0.6 + 0.4 * Math.sin(t * 0.2 + f.phase));
var grd = ctx.createRadialGradient(f.x, f.y, 0, f.x, f.y, f.rx);
grd.addColorStop(0, 'rgba(' + f.r + ',' + f.g + ',' + f.b + ',' + a + ')');
grd.addColorStop(0.4, 'rgba(' + f.r + ',' + f.g + ',' + f.b + ',' + (a * 0.35) + ')');
grd.addColorStop(1, 'rgba(' + f.r + ',' + f.g + ',' + f.b + ',0)');
ctx.fillStyle = grd;
ctx.beginPath();
ctx.ellipse(f.x, f.y, f.rx, f.ry, 0, 0, Math.PI * 2);
ctx.fill();
}
}
/* ══════════════════════════════════════
STREAM — simple but visible
══════════════════════════════════════ */
function drawStream() {
var sTop = H * 0.88, sH = H - sTop;
// water base
var grd = ctx.createLinearGradient(0, sTop, 0, H);
grd.addColorStop(0, 'rgba(6, 16, 25, 0)');
grd.addColorStop(0.2, 'rgba(8, 20, 32, 0.4)');
grd.addColorStop(0.6, 'rgba(12, 28, 42, 0.5)');
grd.addColorStop(1, 'rgba(10, 24, 38, 0.4)');
ctx.fillStyle = grd;
ctx.fillRect(0, sTop, W, sH);
// flowing current — a few smooth lines
for (var i = 0; i < 8; i++) {
var sy = sTop + sH * (0.15 + (i / 8) * 0.6);
var speed = t * (12 + i * 3);
ctx.globalAlpha = 0.04 + (i % 3) * 0.015;
ctx.beginPath();
for (var x = 0; x < W; x += 4) {
var wy = sy + Math.sin(x * 0.012 + speed * 0.04) * 2.5 + Math.sin(x * 0.035 + speed * 0.06) * 1.2;
if (x === 0) ctx.moveTo(x, wy); else ctx.lineTo(x, wy);
}
ctx.strokeStyle = 'rgba(110, 165, 195, 0.6)';
ctx.lineWidth = 0.8;
ctx.stroke();
}
ctx.globalAlpha = 1;
// white water highlights — just a few
ctx.globalAlpha = 0.06;
for (var i = 0; i < 5; i++) {
var wx = ((t * 20 + i * 211) % (W + 200)) - 100;
var wy = sTop + sH * (0.25 + (i / 5) * 0.45);
var ww = 40 + Math.sin(t * 1.2 + i) * 20;
ctx.beginPath();
ctx.moveTo(wx, wy);
ctx.bezierCurveTo(wx + ww * 0.3, wy - 1.5, wx + ww * 0.7, wy + 1, wx + ww, wy);
ctx.strokeStyle = 'rgba(190, 220, 235, 0.7)';
ctx.lineWidth = 1.2 + Math.sin(t * 1.5 + i) * 0.5;
ctx.stroke();
}
ctx.globalAlpha = 1;
// kodama reflections
ctx.globalAlpha = 0.05;
for (var i = 0; i < kodamas.length; i++) {
var sp = kodamas[i];
if (sp.opacity < 0.15) continue;
var rx = sp._px;
var ry = sTop + 15 + Math.sin(t * 0.6 + i) * 3;
var rGrd = ctx.createRadialGradient(rx, ry, 0, rx, ry, sp.size * 2);
rGrd.addColorStop(0, 'rgba(220, 250, 240, ' + (sp.opacity * 0.35) + ')');
rGrd.addColorStop(1, 'rgba(150, 210, 190, 0)');
ctx.fillStyle = rGrd;
ctx.beginPath();
ctx.ellipse(rx, ry, sp.size * 2, 5, 0, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
}
/* ══════════════════════════════════════
FIREFLIES — fewer, gentle
══════════════════════════════════════ */
var fireflies = [];
function initFireflies() {
fireflies = [];
for (var i = 0; i < 25; i++) {
fireflies.push({
x: Math.random() * W, y: Math.random() * H * 0.85,
vy: -(Math.random() * 0.08 + 0.01),
vx: (Math.random() - 0.5) * 0.1,
r: Math.random() * 1.2 + 0.3,
o: Math.random() * 0.2 + 0.04,
drift: Math.random() * Math.PI * 2,
hue: 130 + Math.random() * 30
});
}
}
function drawFireflies() {
for (var i = 0; i < fireflies.length; i++) {
var d = fireflies[i];
d.x += d.vx + Math.sin(t * 0.5 + d.drift) * 0.05;
d.y += d.vy + Math.cos(t * 0.3 + d.drift) * 0.02;
if (d.y < -5) { d.y = H * 0.85; d.x = Math.random() * W; }
if (d.x < -5) d.x = W + 5;
if (d.x > W + 5) d.x = -5;
var fl = d.o * (0.3 + 0.7 * Math.sin(t * 1.0 + d.drift));
// soft glow
ctx.beginPath();
ctx.arc(d.x, d.y, d.r * 4, 0, Math.PI * 2);
ctx.fillStyle = 'hsla(' + d.hue + ', 45%, 60%, ' + (fl * 0.08) + ')';
ctx.fill();
// core
ctx.beginPath();
ctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
ctx.fillStyle = 'hsla(' + d.hue + ', 35%, 75%, ' + fl + ')';
ctx.fill();
}
}
/* ══════════════════════════════════════
FALLING LEAVES — just 5
══════════════════════════════════════ */
var leafParts = [];
function initLeaves() {
leafParts = [];
for (var i = 0; i < 5; i++) {
leafParts.push({
x: Math.random() * W, y: Math.random() * H,
vy: 0.1 + Math.random() * 0.15,
rot: Math.random() * Math.PI * 2,
rs: (Math.random() - 0.5) * 0.012,
sz: 3 + Math.random() * 3,
hue: 105 + Math.random() * 40,
a: 0.05 + Math.random() * 0.06,
drift: Math.random() * Math.PI * 2
});
}
}
function drawLeaves() {
for (var i = 0; i < leafParts.length; i++) {
var l = leafParts[i];
l.x += Math.sin(t * 0.4 + l.drift) * 0.3;
l.y += l.vy;
l.rot += l.rs;
if (l.y > H + 10) { l.y = -10; l.x = Math.random() * W; }
ctx.save();
ctx.translate(l.x, l.y);
ctx.rotate(l.rot);
ctx.globalAlpha = l.a;
ctx.beginPath();
ctx.moveTo(0, -l.sz);
ctx.bezierCurveTo(l.sz * 0.5, -l.sz * 0.2, l.sz * 0.4, l.sz * 0.3, 0, l.sz);
ctx.bezierCurveTo(-l.sz * 0.4, l.sz * 0.3, -l.sz * 0.5, -l.sz * 0.2, 0, -l.sz);
ctx.fillStyle = 'hsla(' + l.hue + ', 30%, 18%, 1)';
ctx.fill();
ctx.restore();
}
ctx.globalAlpha = 1;
}
/* ══════════════════════════════════════
KODAMA — whiter glow, clustered
══════════════════════════════════════ */
var kodamas = [];
function initKodama() {
kodamas = [];
var total = 14;
var idx = 0;
// 3 clusters of 2-3 on perch spots
for (var c = 0; c < 3 && idx < total; c++) {
var pp = perchSpots.length > 0 ?
perchSpots[Math.floor(Math.random() * perchSpots.length)] :
{ x: W * (0.2 + Math.random() * 0.6), y: H * (0.35 + Math.random() * 0.35) };
var count = 2 + Math.floor(Math.random() * 2);
for (var j = 0; j < count && idx < total; j++, idx++) {
var ox = (j - count / 2) * (10 + Math.random() * 8);
makeKodama(pp.x + ox, pp.y + Math.random() * 4 - 2, 1, idx);
}
}
// remaining scattered — mix of depths
for (; idx < total; idx++) {
var depth = idx < total - 3 ? (Math.random() < 0.4 ? 0 : 1) : 2;
var pp = perchSpots.length > 0 && Math.random() < 0.45 ?
perchSpots[Math.floor(Math.random() * perchSpots.length)] :
{ x: W * (0.15 + Math.random() * 0.7), y: H * (0.2 + Math.random() * 0.55) };
makeKodama(pp.x, pp.y, depth, idx);
}
}
function makeKodama(px, py, depth, idx) {
var scale = depth === 0 ? 0.35 + Math.random() * 0.15 :
depth === 1 ? 0.7 + Math.random() * 0.3 :
1.1 + Math.random() * 0.4;
var maxOp = depth === 0 ? 0.18 + Math.random() * 0.1 :
depth === 1 ? 0.55 + Math.random() * 0.3 :
0.8 + Math.random() * 0.15;
var ga = depth === 0 ? 0.06 + Math.random() * 0.06 :
depth === 1 ? 0.15 + Math.random() * 0.15 :
0.28 + Math.random() * 0.18;
kodamas.push({
_px: px, _py: py,
x: px / W, baseY: py / H,
size: (8 + Math.random() * 9) * scale,
phase: Math.random() * Math.PI * 2,
tiltSpd: 1 + Math.random() * 1.5,
bobSpd: 0.35 + Math.random() * 0.4,
bobAmt: 1.2,
opacity: 0,
maxOp: maxOp,
fadeSpd: 0.001 + Math.random() * 0.0015,
appearing: true,
timer: Math.random() * 900,
life: 800 + Math.random() * 1000,
rattleT: 0, rattling: false,
depth: depth,
// shape
headR: 0.4 + Math.random() * 0.1,
bodyH: 0.25 + Math.random() * 0.15,
bodyW: 0.3 + Math.random() * 0.12,
eyeSp: 0.18 + Math.random() * 0.08,
eyeSz: 0.06 + Math.random() * 0.025,
eyeY: -0.06 + Math.random() * 0.06,
mouth: Math.random() > 0.3,
mouthSz: 0.03 + Math.random() * 0.025,
mouthY: 0.15 + Math.random() * 0.08,
arms: Math.random() > 0.4,
glowR: 2.5 + Math.random() * 1.5,
glowA: ga,
tint: Math.floor(Math.random() * 8),
hw: [
(Math.random()-0.5)*0.22, (Math.random()-0.5)*0.18,
(Math.random()-0.5)*0.18, (Math.random()-0.5)*0.22,
(Math.random()-0.5)*0.22, (Math.random()-0.5)*0.18,
(Math.random()-0.5)*0.18, (Math.random()-0.5)*0.22
],
headTall: 0.85 + Math.random() * 0.4
});
}
function drawOneKodama(x, y, sz, tilt, op, sp) {
ctx.save();
ctx.translate(x, y);
ctx.globalAlpha = op;
var r = sz * sp.headR;
// ── WIDE ENVIRONMENT LIGHT — near-white center, green only at edge ──
var envR = sz * sp.glowR * 2.8;
var eg = ctx.createRadialGradient(0, 0, sz * 0.2, 0, 0, envR);
eg.addColorStop(0, 'rgba(248, 255, 252, ' + (sp.glowA * 0.55) + ')');
eg.addColorStop(0.15, 'rgba(240, 252, 248, ' + (sp.glowA * 0.3) + ')');
eg.addColorStop(0.4, 'rgba(180, 225, 210, ' + (sp.glowA * 0.08) + ')');
eg.addColorStop(0.7, 'rgba(80, 150, 120, ' + (sp.glowA * 0.02) + ')');
eg.addColorStop(1, 'rgba(40, 100, 80, 0)');
ctx.fillStyle = eg;
ctx.beginPath();
ctx.arc(0, 0, envR, 0, Math.PI * 2);
ctx.fill();
// ── ground pool ──
var gg = ctx.createRadialGradient(0, sz * 0.7, 0, 0, sz * 0.7, sz * 2.8);
gg.addColorStop(0, 'rgba(245, 255, 250, ' + (sp.glowA * 0.4) + ')');
gg.addColorStop(0.3, 'rgba(200, 240, 225, ' + (sp.glowA * 0.12) + ')');
gg.addColorStop(1, 'rgba(120, 180, 160, 0)');
ctx.fillStyle = gg;
ctx.beginPath();
ctx.ellipse(0, sz * 0.7, sz * 2.8, sz * 0.8, 0, 0, Math.PI * 2);
ctx.fill();
// ── body aura — white core ──
var ag = ctx.createRadialGradient(0, -sz * 0.08, r * 0.1, 0, -sz * 0.08, r * sp.glowR);
ag.addColorStop(0, 'rgba(252, 255, 253, ' + (sp.glowA * 1.3) + ')');
ag.addColorStop(0.15, 'rgba(245, 252, 250, ' + (sp.glowA * 0.8) + ')');
ag.addColorStop(0.4, 'rgba(210, 240, 230, ' + (sp.glowA * 0.25) + ')');
ag.addColorStop(1, 'rgba(140, 200, 180, 0)');
ctx.fillStyle = ag;
ctx.beginPath();
ctx.arc(0, -sz * 0.08, r * sp.glowR, 0, Math.PI * 2);
ctx.fill();
// ── body ──
var bw = sz * sp.bodyW, bh = sz * sp.bodyH, by = sz * 0.15;
ctx.fillStyle = 'rgb(' + (234 + sp.tint) + ',' + (242 + sp.tint) + ',' + (238 + sp.tint) + ')';
ctx.beginPath();
ctx.ellipse(0, by + bh * 0.4, bw * 0.5, bh * 0.55, 0, 0, Math.PI * 2);
ctx.fill();
if (sp.arms) {
ctx.beginPath();
ctx.ellipse(-bw * 0.5 - sz * 0.03, by + bh * 0.1, sz * 0.045, sz * 0.035, -0.3, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.ellipse(bw * 0.5 + sz * 0.03, by + bh * 0.1, sz * 0.045, sz * 0.035, 0.3, 0, Math.PI * 2);
ctx.fill();
}
// ── head ──
ctx.save();
ctx.rotate(tilt);
var hx = 0, hy = -sz * 0.15;
var rx = r, ry = r * sp.headTall;
var w = sp.hw;
ctx.fillStyle = 'rgb(' + (240 + sp.tint) + ',' + (248 + sp.tint) + ',' + (244 + sp.tint) + ')';
ctx.beginPath();
ctx.moveTo(hx + r * w[0], hy - ry);
ctx.bezierCurveTo(hx + rx * (0.55 + w[0]), hy - ry * (0.9 + w[1]), hx + rx * (1.0 + w[1]), hy - ry * (0.4 + w[0]), hx + rx * (1.0 + w[2]), hy + ry * w[2]);
ctx.bezierCurveTo(hx + rx * (1.0 + w[3]), hy + ry * (0.5 + w[2]), hx + rx * (0.5 + w[3]), hy + ry * (1.0 + w[3]), hx + r * w[4], hy + ry * (0.95 + w[4] * 0.3));
ctx.bezierCurveTo(hx - rx * (0.5 + w[5]), hy + ry * (1.0 + w[5]), hx - rx * (1.0 + w[5]), hy + ry * (0.5 + w[4]), hx - rx * (1.0 + w[6]), hy + ry * w[6]);
ctx.bezierCurveTo(hx - rx * (1.0 + w[7]), hy - ry * (0.4 + w[6]), hx - rx * (0.55 + w[7]), hy - ry * (0.9 + w[7]), hx + r * w[0], hy - ry);
ctx.fill();
// highlight
ctx.fillStyle = 'rgba(248, 255, 252, 0.22)';
ctx.beginPath();
ctx.arc(-rx * 0.1, hy - ry * 0.1, r * 0.4, 0, Math.PI * 2);
ctx.fill();
// eyes
ctx.fillStyle = 'rgba(3, 10, 8, 0.92)';
var ey = -sz * 0.15 + sz * sp.eyeY;
ctx.beginPath();
ctx.arc(-sz * sp.eyeSp, ey, sz * sp.eyeSz, 0, Math.PI * 2);
ctx.fill();
ctx.beginPath();
ctx.arc(sz * sp.eyeSp, ey, sz * sp.eyeSz, 0, Math.PI * 2);
ctx.fill();
if (sp.mouth) {
ctx.fillStyle = 'rgba(3, 10, 8, 0.65)';
ctx.beginPath();
ctx.arc(0, ey + sz * sp.mouthY, sz * sp.mouthSz, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore(); // head
ctx.restore(); // pos
}
function updateAndDrawKodama() {
var sorted = kodamas.slice().sort(function(a, b) { return a.depth - b.depth; });
for (var i = 0; i < sorted.length; i++) {
var s = sorted[i];
s.timer++;
if (s.appearing) {
s.opacity += s.fadeSpd;
if (s.opacity >= s.maxOp) s.opacity = s.maxOp;
if (s.timer > s.life) s.appearing = false;
} else {
s.opacity -= s.fadeSpd;
if (s.opacity <= 0) {
s.opacity = 0;
if (perchSpots.length > 0 && Math.random() < 0.6) {
var pp = perchSpots[Math.floor(Math.random() * perchSpots.length)];
s._px = pp.x; s._py = pp.y;
} else {
s._px = W * (0.15 + Math.random() * 0.7);
s._py = H * (0.2 + Math.random() * 0.55);
}
s.x = s._px / W; s.baseY = s._py / H;
s.maxOp = s.depth === 0 ? 0.18 + Math.random() * 0.1 :
s.depth === 1 ? 0.55 + Math.random() * 0.3 : 0.8 + Math.random() * 0.15;
s.timer = 0; s.life = 800 + Math.random() * 1000;
s.appearing = true; s.rattleT = 0;
}
}
if (s.opacity <= 0) continue;
if (!s.rattling && Math.random() < 0.003) { s.rattling = true; s.rattleT = 0; }
var tilt = Math.sin(t * s.tiltSpd + s.phase) * 0.07;
if (s.rattling) {
s.rattleT++;
tilt = Math.sin(s.rattleT * 0.9) * 0.28 * Math.max(0, 1 - s.rattleT / 25);
if (s.rattleT > 25) s.rattling = false;
}
var bob = Math.sin(t * s.bobSpd + s.phase) * s.bobAmt;
s._px = s.x * W;
s._py = s.baseY * H + bob;
drawOneKodama(s._px, s._py, s.size, tilt, s.opacity, s);
}
}
/* ══════════════════════════════════════
CURSOR TRAIL
══════════════════════════════════════ */
var trailPts = [];
document.addEventListener('mousemove', function(e) {
trailPts.push({ x: e.clientX, y: e.clientY, t: Date.now() });
if (trailPts.length > 25) trailPts.shift();
});
function drawTrail() {
tctx.clearRect(0, 0, W, H);
var now = Date.now();
while (trailPts.length && now - trailPts[0].t > 350) trailPts.shift();
if (trailPts.length > 1) {
for (var i = 1; i < trailPts.length; i++) {
var age = (now - trailPts[i].t) / 350;
tctx.beginPath();
tctx.moveTo(trailPts[i-1].x, trailPts[i-1].y);
tctx.lineTo(trailPts[i].x, trailPts[i].y);
tctx.strokeStyle = 'rgba(160, 220, 190, ' + ((1 - age) * 0.15) + ')';
tctx.lineWidth = (1 - age) * 1.8;
tctx.lineCap = 'round';
tctx.stroke();
}
}
requestAnimationFrame(drawTrail);
}
/* ══════════════════════════════════════
MAIN RENDER — proper depth ordering
══════════════════════════════════════ */
function render() {
t = Date.now() * 0.001;
ctx.clearRect(0, 0, W, H);
// 1. subtle ambient canopy glow — very faint
ctx.globalAlpha = 0.08;
for (var i = 0; i < 5; i++) {
var fx = W * (0.1 + (i / 5) * 0.8) + Math.sin(t * 0.08 + i * 1.7) * 25;
var fy = H * (0.05 + (i % 3) * 0.1);
var grd = ctx.createRadialGradient(fx, fy, 0, fx, fy, 100 + i * 25);
grd.addColorStop(0, 'rgba(10, 35, 45, 0.4)');
grd.addColorStop(1, 'rgba(6, 22, 30, 0)');
ctx.fillStyle = grd;
ctx.beginPath();
ctx.arc(fx, fy, 100 + i * 25, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// 1b. far canopy foliage — large soft masses, almost part of background
drawFarCanopy();
// 2. back fog layer
ctx.globalAlpha = 0.7;
drawFog();
ctx.globalAlpha = 1;
// 3. trunks — at screen edges
resetSeed(42);
for (var i = 0; i < trunks.length; i++) drawTrunk(trunks[i]);
// 4. midground branches — bg branches first (faint), then regular
for (var i = 0; i < midBranches.length; i++) {
var b = midBranches[i];
if (b.bg) {
ctx.globalAlpha = 0.35;
strokeBranch(b.pts, b.w, 'rgba(8, 18, 16, 0.6)');
strokeBranch(b.pts, b.w * 0.2, 'rgba(14, 26, 22, 0.2)');
} else {
ctx.globalAlpha = 0.85;
strokeBranch(b.pts, b.w, 'rgba(6, 14, 13, 0.88)');
strokeBranch(b.pts, b.w * 0.25, 'rgba(18, 32, 28, 0.3)');
}
}
ctx.globalAlpha = 1;
// 4b. mid foliage — dark green clusters, react to kodama light
drawMidFoliage();
// 5. stream
drawStream();
// 6. kodama (they illuminate everything around them)
updateAndDrawKodama();
// 7. fireflies
drawFireflies();
// 8. foreground branches — frame the edges, avoid center
ctx.globalAlpha = 0.92;
for (var i = 0; i < fgBranches.length; i++) {
var b = fgBranches[i];
strokeBranch(b.pts, b.w, 'rgba(3, 7, 7, 0.93)');
}
ctx.globalAlpha = 1;
// 8b. near leaf clusters — individual shapes on foreground branches
drawNearLeafClusters();
// 9. front fog wisps — just 3, sparse
ctx.globalAlpha = 0.25;
for (var i = 0; i < 3 && i < fogClouds.length; i++) {
var f = fogClouds[i];
var a = f.alpha * 1.8 * (0.5 + 0.5 * Math.sin(t * 0.15 + f.phase + 3));
var grd = ctx.createRadialGradient(f.x, f.y + H * 0.1, 0, f.x, f.y + H * 0.1, f.rx * 0.6);
grd.addColorStop(0, 'rgba(' + f.r + ',' + f.g + ',' + f.b + ',' + a + ')');
grd.addColorStop(1, 'rgba(' + f.r + ',' + f.g + ',' + f.b + ',0)');
ctx.fillStyle = grd;
ctx.beginPath();
ctx.ellipse(f.x, f.y + H * 0.1, f.rx * 0.6, f.ry * 0.4, 0, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1;
// 10. falling leaves — very subtle, on top
drawLeaves();
requestAnimationFrame(render);
}
/* ══════════════════════════════════════
INIT
══════════════════════════════════════ */
var hr = new Date().getHours();
var g = hr < 5 ? "the forest is quiet. the spirits are listening." :
hr < 12 ? "morning light filters through the canopy." :
hr < 17 ? "afternoon. the kodama watch from the branches." :
hr < 21 ? "dusk settles. the spirits begin to glow." :
"deep night. the forest is alive with light.";
document.getElementById('greeting').textContent = g;
function tick() {
var d = new Date();
var el = document.getElementById('clock');
if (el) el.textContent = d.toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
}
tick(); setInterval(tick, 30000);
function init() {
W = window.innerWidth; H = window.innerHeight;
scene.width = W; scene.height = H;
trailC.width = W; trailC.height = H;
generateScene();
initFog();
initFireflies();
initLeaves();
initKodama();
}
init();
window.addEventListener('resize', init);
render();
drawTrail();
})();
</script>
</body>
</html>