created themes folder with kodama template
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.
This commit is contained in:
parent
17e804cc17
commit
104bb7ba2d
3 changed files with 747 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1,3 +1,4 @@
|
||||||
__pycache__/
|
__pycache__/
|
||||||
tinyweb_identity
|
tinyweb_identity
|
||||||
index.db
|
index.db
|
||||||
|
index.db
|
||||||
|
|
|
||||||
BIN
index.db
BIN
index.db
Binary file not shown.
746
themes/kodama.html
Normal file
746
themes/kodama.html
Normal file
|
|
@ -0,0 +1,746 @@
|
||||||
|
<!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>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue