- Replace Google Fonts with system font stacks across all themes - Add Referrer-Policy, X-Content-Type-Options, X-Frame-Options, CSP headers - Add rel="noreferrer noopener" on all outbound links - Add no-referrer and dns-prefetch-control meta tags to all themes - Clean tracking params on outbound links from trusted/remote sources - Remove Google domains from CSP whitelists
1394 lines
No EOL
51 KiB
HTML
1394 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="/">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> |