Adds pagination, meta, and success message styles, plus input selectors for new form fields (edit page, manual entry, transport node).
1393 lines
No EOL
50 KiB
HTML
1393 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;
|
|
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: 'IBM Plex Sans', -apple-system, 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: '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 · 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> |