tinyweb/themes/kodama2.html
Derick Phan aff8c654cc
Add kodama2 theme with styles for new handler features
Adds pagination, meta, and success message styles, plus input
selectors for new form fields (edit page, manual entry, transport node).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 09:05:12 -07:00

1390 lines
No EOL
50 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:ital,wght@0,400;0,500;0,600;1,400&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'IBM Plex Sans', -apple-system, sans-serif;
font-size: 16px;
line-height: 1.65;
color: #9ab4b8;
background: #070d14;
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;
}
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: 'IBM Plex Mono', 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: 'IBM Plex Sans', 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: 'IBM Plex Sans', 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: 'IBM Plex Mono', 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: 'IBM Plex Mono', 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: 'IBM Plex Mono', 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: 'IBM Plex Mono', 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: 'IBM Plex Mono', 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: 'IBM Plex Mono', 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="/">tinyweb</a>
<div class="links">
<a href="/pages">browse</a>
<a href="/tags">tags</a>
<a href="/subscriptions">network</a>
<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>