Save the custom kodama template to themes/kodama.html so it's version-controlled as a file rather than only living in the database. Stop tracking index.db since it's runtime data, not source code.
746 lines
No EOL
23 KiB
HTML
746 lines
No EOL
23 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: #c8c8c8;
|
|
background: #111;
|
|
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='%23888' stroke-width='1.5'/%3E%3Ccircle cx='10' cy='10' r='1' fill='%23aaa'/%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='%23fff' stroke-width='1' opacity='0.6'/%3E%3Ccircle cx='10' cy='10' r='1.5' fill='%23fff'/%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='%23aaa' stroke-width='1.5'/%3E%3C/svg%3E") 10 10, text;
|
|
}
|
|
|
|
/* scanline overlay */
|
|
body::before {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
background: repeating-linear-gradient(
|
|
0deg,
|
|
transparent,
|
|
transparent 2px,
|
|
rgba(255, 255, 255, 0.008) 2px,
|
|
rgba(255, 255, 255, 0.008) 4px
|
|
);
|
|
pointer-events: none;
|
|
z-index: 1000;
|
|
}
|
|
|
|
/* vignette */
|
|
body::after {
|
|
content: '';
|
|
position: fixed;
|
|
top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
background: radial-gradient(ellipse at center, transparent 60%, rgba(0, 0, 0, 0.4) 100%);
|
|
pointer-events: none;
|
|
z-index: 999;
|
|
}
|
|
|
|
/* floating particles canvas */
|
|
#particles {
|
|
position: fixed;
|
|
top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
pointer-events: none;
|
|
z-index: 0;
|
|
}
|
|
|
|
/* cursor trail canvas */
|
|
#trail {
|
|
position: fixed;
|
|
top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
pointer-events: none;
|
|
z-index: 998;
|
|
}
|
|
|
|
|
|
/* kodama spirits */
|
|
#kodama {
|
|
position: fixed;
|
|
top: 0; left: 0;
|
|
width: 100%; height: 100%;
|
|
pointer-events: none;
|
|
z-index: 2;
|
|
}
|
|
|
|
.shell {
|
|
max-width: 660px;
|
|
margin: 0 auto;
|
|
padding: 0 1.5rem;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
/* nav */
|
|
nav {
|
|
display: flex;
|
|
align-items: baseline;
|
|
justify-content: space-between;
|
|
padding: 1.5rem 0 1.2rem;
|
|
border-bottom: 1px solid #232323;
|
|
}
|
|
|
|
nav .site {
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
font-size: 0.85rem;
|
|
font-weight: 500;
|
|
color: #e8e8e8;
|
|
text-decoration: none;
|
|
letter-spacing: 0.06em;
|
|
border-bottom: none;
|
|
transition: text-shadow 0.3s;
|
|
}
|
|
|
|
nav .site:hover {
|
|
text-shadow: 0 0 8px rgba(255,255,255,0.3);
|
|
}
|
|
|
|
nav .links { display: flex; gap: 1.2rem; }
|
|
|
|
nav .links a {
|
|
font-size: 0.82rem;
|
|
color: #606060;
|
|
text-decoration: none;
|
|
border-bottom: none;
|
|
transition: color 0.2s, text-shadow 0.3s;
|
|
}
|
|
|
|
nav .links a:hover {
|
|
color: #c8c8c8;
|
|
text-shadow: 0 0 6px rgba(255,255,255,0.15);
|
|
}
|
|
|
|
/* greeting */
|
|
#greeting {
|
|
padding: 1.5rem 0 0;
|
|
font-size: 0.85rem;
|
|
color: #484848;
|
|
font-style: italic;
|
|
opacity: 0;
|
|
animation: fadeIn 1.5s ease forwards 0.3s;
|
|
}
|
|
|
|
@keyframes fadeIn {
|
|
to { opacity: 1; }
|
|
}
|
|
|
|
/* content */
|
|
.content { padding: 1.8rem 0 3rem; }
|
|
|
|
/* headings */
|
|
h1 {
|
|
font-size: 1.4rem;
|
|
font-weight: 600;
|
|
color: #e8e8e8;
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
h1 a { color: #e8e8e8; text-decoration: none; border-bottom: none; }
|
|
h1 a:hover { color: #999; }
|
|
|
|
h2 {
|
|
font-size: 1.05rem;
|
|
font-weight: 600;
|
|
color: #d0d0d0;
|
|
margin: 1.8rem 0 0.5rem;
|
|
}
|
|
|
|
p { margin: 0.5rem 0; color: #999; }
|
|
|
|
a {
|
|
color: #c8c8c8;
|
|
text-decoration: none;
|
|
border-bottom: 1px solid #2e2e2e;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
a:hover {
|
|
color: #fff;
|
|
border-bottom-color: #555;
|
|
}
|
|
|
|
em { color: #777; }
|
|
|
|
/* inputs */
|
|
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"] {
|
|
background: #181818;
|
|
border: 1px solid #2a2a2a;
|
|
border-radius: 4px;
|
|
padding: 0.6rem 0.85rem;
|
|
color: #d0d0d0;
|
|
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: #454545;
|
|
box-shadow: 0 0 12px rgba(255,255,255,0.03);
|
|
}
|
|
|
|
button, input[type="submit"] {
|
|
background: #1c1c1c;
|
|
border: 1px solid #303030;
|
|
border-radius: 4px;
|
|
padding: 0.6rem 1.1rem;
|
|
color: #999;
|
|
font-family: 'IBM Plex Sans', sans-serif;
|
|
font-size: 0.88rem;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
button:hover, input[type="submit"]:hover {
|
|
background: #242424;
|
|
color: #d0d0d0;
|
|
border-color: #454545;
|
|
box-shadow: 0 0 10px rgba(255,255,255,0.04);
|
|
}
|
|
|
|
/* search results */
|
|
.result {
|
|
padding: 1rem 0;
|
|
border-bottom: 1px solid #1c1c1c;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.result:hover {
|
|
background: rgba(255,255,255,0.01);
|
|
}
|
|
|
|
.result:last-child { border-bottom: none; }
|
|
|
|
.result > a:first-child {
|
|
font-size: 1.02rem;
|
|
font-weight: 500;
|
|
color: #ddd;
|
|
border-bottom: none;
|
|
}
|
|
|
|
.result > a:first-child:hover { color: #fff; }
|
|
|
|
.note {
|
|
margin-top: 0.3rem;
|
|
font-size: 0.9rem;
|
|
color: #606060;
|
|
}
|
|
|
|
.tags { margin-top: 0.3rem; }
|
|
|
|
.tag, .tags a {
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
font-size: 0.7rem;
|
|
color: #555;
|
|
border: 1px solid #252525;
|
|
border-radius: 3px;
|
|
padding: 0.1rem 0.35rem;
|
|
margin-right: 0.25rem;
|
|
}
|
|
|
|
.tag:hover, .tags a:hover {
|
|
color: #999;
|
|
border-color: #404040;
|
|
}
|
|
|
|
/* trusted / remote results */
|
|
details {
|
|
margin: 1rem 0;
|
|
border: 1px solid #1e1e1e;
|
|
border-radius: 4px;
|
|
padding: 0.7rem 0.9rem;
|
|
background: #151515;
|
|
}
|
|
|
|
summary {
|
|
font-size: 0.85rem;
|
|
color: #606060;
|
|
font-weight: 500;
|
|
}
|
|
|
|
summary:hover { color: #999; }
|
|
|
|
details ul { margin-top: 0.5rem; padding-left: 1.2rem; }
|
|
details li { margin: 0.35rem 0; font-size: 0.9rem; }
|
|
|
|
/* lists */
|
|
ul, ol { padding-left: 1.2rem; margin: 0.5rem 0; }
|
|
|
|
li { margin: 0.45rem 0; color: #999; }
|
|
li a { border-bottom: none; }
|
|
li a:hover { border-bottom: 1px solid #444; }
|
|
|
|
/* code */
|
|
pre {
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
font-size: 0.8rem;
|
|
background: #151515;
|
|
border: 1px solid #232323;
|
|
border-radius: 4px;
|
|
padding: 0.9rem;
|
|
overflow-x: auto;
|
|
color: #808080;
|
|
margin: 0.8rem 0;
|
|
}
|
|
|
|
code {
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
font-size: 0.82rem;
|
|
background: #1a1a1a;
|
|
border-radius: 3px;
|
|
padding: 0.1rem 0.35rem;
|
|
color: #999;
|
|
}
|
|
|
|
/* textarea */
|
|
textarea {
|
|
background: #151515;
|
|
border: 1px solid #2a2a2a;
|
|
border-radius: 4px;
|
|
padding: 0.7rem 0.9rem;
|
|
color: #c8c8c8;
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
font-size: 0.8rem;
|
|
line-height: 1.6;
|
|
resize: vertical;
|
|
width: 100%;
|
|
}
|
|
|
|
/* tables */
|
|
table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
|
|
|
|
th {
|
|
text-align: left;
|
|
font-size: 0.72rem;
|
|
font-weight: 500;
|
|
color: #505050;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.08em;
|
|
padding: 0.5rem 0.7rem;
|
|
border-bottom: 1px solid #232323;
|
|
}
|
|
|
|
td {
|
|
padding: 0.5rem 0.7rem;
|
|
border-bottom: 1px solid #191919;
|
|
font-size: 0.9rem;
|
|
}
|
|
|
|
/* misc */
|
|
label { color: #999; }
|
|
input[type="checkbox"] { accent-color: #555; }
|
|
|
|
hr { border: none; border-top: 1px solid #1e1e1e; margin: 1rem 0; }
|
|
|
|
small {
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
font-size: 0.7rem;
|
|
color: #484848;
|
|
}
|
|
|
|
/* footer */
|
|
footer {
|
|
border-top: 1px solid #1c1c1c;
|
|
padding: 1.5rem 0 2rem;
|
|
text-align: center;
|
|
color: #333;
|
|
font-size: 0.8rem;
|
|
}
|
|
|
|
footer .clock {
|
|
font-family: 'IBM Plex Mono', monospace;
|
|
font-size: 0.72rem;
|
|
color: #282828;
|
|
margin-top: 0.25rem;
|
|
}
|
|
|
|
::selection { background: #333; color: #fff; }
|
|
|
|
::-webkit-scrollbar { width: 5px; }
|
|
::-webkit-scrollbar-track { background: transparent; }
|
|
::-webkit-scrollbar-thumb { background: #222; 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="particles"></canvas>
|
|
<canvas id="trail"></canvas>
|
|
<canvas id="kodama"></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() {
|
|
// greeting
|
|
var h = new Date().getHours();
|
|
var g = h < 5 ? "still up? the quiet hours are good for finding things." :
|
|
h < 12 ? "morning. what are you looking for?" :
|
|
h < 17 ? "afternoon. the index is ready." :
|
|
h < 21 ? "evening. settle in." :
|
|
"late night. good browsing ahead.";
|
|
document.getElementById('greeting').textContent = g;
|
|
|
|
// clock
|
|
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);
|
|
|
|
// floating dust particles
|
|
var pc = document.getElementById('particles');
|
|
var pctx = pc.getContext('2d');
|
|
var dots = [];
|
|
|
|
function resizeParticles() {
|
|
pc.width = window.innerWidth;
|
|
pc.height = window.innerHeight;
|
|
}
|
|
resizeParticles();
|
|
window.addEventListener('resize', resizeParticles);
|
|
|
|
for (var i = 0; i < 40; i++) {
|
|
dots.push({
|
|
x: Math.random() * pc.width,
|
|
y: Math.random() * pc.height,
|
|
vy: -(Math.random() * 0.15 + 0.05),
|
|
vx: (Math.random() - 0.5) * 0.1,
|
|
r: Math.random() * 1.2 + 0.3,
|
|
o: Math.random() * 0.25 + 0.05,
|
|
drift: Math.random() * Math.PI * 2
|
|
});
|
|
}
|
|
|
|
function drawParticles() {
|
|
pctx.clearRect(0, 0, pc.width, pc.height);
|
|
var t = Date.now() * 0.001;
|
|
for (var i = 0; i < dots.length; i++) {
|
|
var d = dots[i];
|
|
d.x += d.vx + Math.sin(t + d.drift) * 0.05;
|
|
d.y += d.vy;
|
|
if (d.y < -5) { d.y = pc.height + 5; d.x = Math.random() * pc.width; }
|
|
if (d.x < -5) d.x = pc.width + 5;
|
|
if (d.x > pc.width + 5) d.x = -5;
|
|
var flicker = d.o * (0.6 + 0.4 * Math.sin(t * 1.5 + d.drift));
|
|
pctx.beginPath();
|
|
pctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
|
|
pctx.fillStyle = 'rgba(200, 200, 200, ' + flicker + ')';
|
|
pctx.fill();
|
|
}
|
|
requestAnimationFrame(drawParticles);
|
|
}
|
|
drawParticles();
|
|
|
|
// cursor trail
|
|
var tc = document.getElementById('trail');
|
|
var tctx = tc.getContext('2d');
|
|
var points = [];
|
|
var mx = 0, my = 0;
|
|
|
|
function resizeTrail() {
|
|
tc.width = window.innerWidth;
|
|
tc.height = window.innerHeight;
|
|
}
|
|
resizeTrail();
|
|
window.addEventListener('resize', resizeTrail);
|
|
|
|
document.addEventListener('mousemove', function(e) {
|
|
mx = e.clientX;
|
|
my = e.clientY;
|
|
points.push({ x: mx, y: my, t: Date.now() });
|
|
if (points.length > 30) points.shift();
|
|
});
|
|
|
|
function drawTrail() {
|
|
tctx.clearRect(0, 0, tc.width, tc.height);
|
|
var now = Date.now();
|
|
// fade out points older than 400ms
|
|
while (points.length && now - points[0].t > 400) points.shift();
|
|
if (points.length > 1) {
|
|
for (var i = 1; i < points.length; i++) {
|
|
var age = (now - points[i].t) / 400;
|
|
var alpha = (1 - age) * 0.25;
|
|
var width = (1 - age) * 2;
|
|
tctx.beginPath();
|
|
tctx.moveTo(points[i-1].x, points[i-1].y);
|
|
tctx.lineTo(points[i].x, points[i].y);
|
|
tctx.strokeStyle = 'rgba(200, 200, 200, ' + alpha + ')';
|
|
tctx.lineWidth = width;
|
|
tctx.lineCap = 'round';
|
|
tctx.stroke();
|
|
}
|
|
}
|
|
requestAnimationFrame(drawTrail);
|
|
}
|
|
drawTrail();
|
|
|
|
// kodama (tree spirits)
|
|
var kc = document.getElementById('kodama');
|
|
var kctx = kc.getContext('2d');
|
|
var spirits = [];
|
|
var numSpirits = 8;
|
|
|
|
function resizeKodama() {
|
|
kc.width = window.innerWidth;
|
|
kc.height = window.innerHeight;
|
|
}
|
|
resizeKodama();
|
|
window.addEventListener('resize', resizeKodama);
|
|
|
|
for (var i = 0; i < numSpirits; i++) {
|
|
// each spirit gets unique proportions
|
|
var headR = 0.38 + Math.random() * 0.12; // head radius ratio (bigger = bigger head)
|
|
var bodyH = 0.25 + Math.random() * 0.2; // body height ratio
|
|
var bodyW = 0.3 + Math.random() * 0.15; // body width ratio
|
|
var eyeSpread = 0.18 + Math.random() * 0.1; // how far apart eyes are
|
|
var eyeSize = 0.055 + Math.random() * 0.03; // eye dot size
|
|
var eyeY = -0.08 + Math.random() * 0.08; // eye vertical position
|
|
var hasMouth = Math.random() > 0.3; // 70% have visible mouth
|
|
var mouthSize = 0.03 + Math.random() * 0.03;
|
|
var mouthY = 0.15 + Math.random() * 0.1;
|
|
var hasArms = Math.random() > 0.4; // 60% have little arm bumps
|
|
var glowSize = 1.4 + Math.random() * 0.8; // glow radius multiplier
|
|
var glowAlpha = 0.08 + Math.random() * 0.12; // glow brightness
|
|
var tint = Math.floor(Math.random() * 15); // slight warm/cool variation
|
|
|
|
spirits.push({
|
|
x: Math.random() * 0.8 + 0.1,
|
|
baseY: 0.75 + Math.random() * 0.18,
|
|
size: 10 + Math.random() * 12,
|
|
phase: Math.random() * Math.PI * 2,
|
|
tiltSpeed: 1.2 + Math.random() * 2,
|
|
bobSpeed: 0.6 + Math.random() * 0.8,
|
|
opacity: 0,
|
|
targetOpacity: 0.4 + Math.random() * 0.45,
|
|
fadeSpeed: 0.002 + Math.random() * 0.004,
|
|
appearing: true,
|
|
timer: Math.random() * 600,
|
|
lifespan: 500 + Math.random() * 600,
|
|
rattleTime: 0,
|
|
rattling: false,
|
|
// unique shape params
|
|
headR: headR,
|
|
bodyH: bodyH,
|
|
bodyW: bodyW,
|
|
eyeSpread: eyeSpread,
|
|
eyeSize: eyeSize,
|
|
eyeY: eyeY,
|
|
hasMouth: hasMouth,
|
|
mouthSize: mouthSize,
|
|
mouthY: mouthY,
|
|
hasArms: hasArms,
|
|
glowSize: glowSize,
|
|
glowAlpha: glowAlpha,
|
|
tint: tint,
|
|
// 8 offsets that warp the head into a unique rock-like blob
|
|
hw: [
|
|
(Math.random()-0.5)*0.25, (Math.random()-0.5)*0.2,
|
|
(Math.random()-0.5)*0.2, (Math.random()-0.5)*0.25,
|
|
(Math.random()-0.5)*0.25, (Math.random()-0.5)*0.2,
|
|
(Math.random()-0.5)*0.2, (Math.random()-0.5)*0.25
|
|
],
|
|
headTall: 0.8 + Math.random() * 0.5 // overall tall vs wide
|
|
});
|
|
}
|
|
|
|
function drawKodamaSpirit(x, y, size, tilt, opacity, sp) {
|
|
kctx.save();
|
|
kctx.translate(x, y);
|
|
kctx.globalAlpha = opacity;
|
|
|
|
var r = size * sp.headR; // head radius
|
|
|
|
// outer glow aura
|
|
var grd = kctx.createRadialGradient(0, -size * 0.1, r * 0.3, 0, -size * 0.1, r * sp.glowSize);
|
|
grd.addColorStop(0, 'rgba(255, 255, 250, ' + sp.glowAlpha + ')');
|
|
grd.addColorStop(0.5, 'rgba(255, 255, 250, ' + (sp.glowAlpha * 0.3) + ')');
|
|
grd.addColorStop(1, 'rgba(255, 255, 250, 0)');
|
|
kctx.fillStyle = grd;
|
|
kctx.beginPath();
|
|
kctx.arc(0, -size * 0.1, r * sp.glowSize, 0, Math.PI * 2);
|
|
kctx.fill();
|
|
|
|
// body - stubby rounded shape
|
|
var bw = size * sp.bodyW;
|
|
var bh = size * sp.bodyH;
|
|
var by = size * 0.15;
|
|
kctx.fillStyle = 'rgb(' + (238 + sp.tint) + ',' + (237 + sp.tint) + ',' + (230 + sp.tint) + ')';
|
|
kctx.beginPath();
|
|
kctx.ellipse(0, by + bh * 0.4, bw * 0.5, bh * 0.55, 0, 0, Math.PI * 2);
|
|
kctx.fill();
|
|
|
|
// arms - tiny bumps on sides
|
|
if (sp.hasArms) {
|
|
kctx.beginPath();
|
|
kctx.ellipse(-bw * 0.5 - size * 0.04, by + bh * 0.1, size * 0.05, size * 0.04, -0.3, 0, Math.PI * 2);
|
|
kctx.fill();
|
|
kctx.beginPath();
|
|
kctx.ellipse(bw * 0.5 + size * 0.04, by + bh * 0.1, size * 0.05, size * 0.04, 0.3, 0, Math.PI * 2);
|
|
kctx.fill();
|
|
}
|
|
|
|
// head (tilts)
|
|
kctx.save();
|
|
kctx.rotate(tilt);
|
|
|
|
// head - unique rock-like blob shape per spirit
|
|
var hx = 0, hy = -size * 0.15;
|
|
var rx = r, ry = r * sp.headTall;
|
|
var w = sp.hw;
|
|
kctx.fillStyle = 'rgb(' + (243 + sp.tint) + ',' + (242 + sp.tint) + ',' + (237 + sp.tint) + ')';
|
|
kctx.beginPath();
|
|
// top
|
|
kctx.moveTo(hx + r * w[0], hy - ry);
|
|
// top-right
|
|
kctx.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]);
|
|
// bottom-right
|
|
kctx.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));
|
|
// bottom-left
|
|
kctx.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]);
|
|
// top-left
|
|
kctx.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);
|
|
kctx.fill();
|
|
|
|
// subtle inner highlight
|
|
kctx.fillStyle = 'rgba(255, 255, 252, 0.25)';
|
|
kctx.beginPath();
|
|
kctx.arc(-rx * 0.12, hy - ry * 0.1, r * 0.45, 0, Math.PI * 2);
|
|
kctx.fill();
|
|
|
|
// eyes - small round dark dots
|
|
kctx.fillStyle = 'rgba(15, 15, 15, 0.9)';
|
|
var ey = -size * 0.15 + size * sp.eyeY;
|
|
var es = size * sp.eyeSize;
|
|
kctx.beginPath();
|
|
kctx.arc(-size * sp.eyeSpread, ey, es, 0, Math.PI * 2);
|
|
kctx.fill();
|
|
kctx.beginPath();
|
|
kctx.arc(size * sp.eyeSpread, ey, es, 0, Math.PI * 2);
|
|
kctx.fill();
|
|
|
|
// mouth - tiny dot, not all have one
|
|
if (sp.hasMouth) {
|
|
kctx.fillStyle = 'rgba(15, 15, 15, 0.7)';
|
|
kctx.beginPath();
|
|
kctx.arc(0, ey + size * sp.mouthY, size * sp.mouthSize, 0, Math.PI * 2);
|
|
kctx.fill();
|
|
}
|
|
|
|
kctx.restore(); // head tilt
|
|
kctx.restore(); // position
|
|
}
|
|
|
|
function drawKodama() {
|
|
kctx.clearRect(0, 0, kc.width, kc.height);
|
|
var t = Date.now() * 0.001;
|
|
|
|
for (var i = 0; i < spirits.length; i++) {
|
|
var s = spirits[i];
|
|
s.timer++;
|
|
|
|
if (s.appearing) {
|
|
s.opacity += s.fadeSpeed;
|
|
if (s.opacity >= s.targetOpacity) s.opacity = s.targetOpacity;
|
|
if (s.timer > s.lifespan) s.appearing = false;
|
|
} else {
|
|
s.opacity -= s.fadeSpeed;
|
|
if (s.opacity <= 0) {
|
|
s.opacity = 0;
|
|
s.x = Math.random() * 0.8 + 0.1;
|
|
s.baseY = 0.75 + Math.random() * 0.18;
|
|
s.targetOpacity = 0.4 + Math.random() * 0.45;
|
|
s.timer = 0;
|
|
s.lifespan = 500 + Math.random() * 600;
|
|
s.appearing = true;
|
|
s.rattleTime = 0;
|
|
}
|
|
}
|
|
|
|
if (s.opacity <= 0) continue;
|
|
|
|
if (!s.rattling && Math.random() < 0.004) {
|
|
s.rattling = true;
|
|
s.rattleTime = 0;
|
|
}
|
|
|
|
var tilt = Math.sin(t * s.tiltSpeed + s.phase) * 0.1;
|
|
if (s.rattling) {
|
|
s.rattleTime++;
|
|
tilt = Math.sin(s.rattleTime * 0.9) * 0.35 * Math.max(0, 1 - s.rattleTime / 25);
|
|
if (s.rattleTime > 25) s.rattling = false;
|
|
}
|
|
|
|
var bobY = Math.sin(t * s.bobSpeed + s.phase) * 2.5;
|
|
var px = s.x * kc.width;
|
|
var py = s.baseY * kc.height + bobY;
|
|
|
|
drawKodamaSpirit(px, py, s.size, tilt, s.opacity, s);
|
|
}
|
|
requestAnimationFrame(drawKodama);
|
|
}
|
|
drawKodama();
|
|
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html> |