tinyweb/themes/junimo.html
lichenblankie a9f426132e privacy pass: degoogle, CSP, referrer
- 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
2026-06-05 05:29:36 +00:00

1626 lines
54 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; }
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: #e0d8c8;
background: #1b1040;
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='%23c8a860' stroke-width='1.5'/%3E%3Ccircle cx='10' cy='10' r='1' fill='%23c8a860'/%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='%23f0d878' stroke-width='1' opacity='0.6'/%3E%3Ccircle cx='10' cy='10' r='1.5' fill='%23f0d878'/%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='%23c8a860' stroke-width='1.5'/%3E%3C/svg%3E") 10 10, text;
}
/* warm vignette */
body::after {
content: '';
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
background: radial-gradient(ellipse at center top, transparent 40%, rgba(10, 5, 30, 0.6) 100%);
pointer-events: none;
z-index: 999;
}
/* background scene canvas — sky, stars, hills, hut */
#scene {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 0;
}
/* firefly particles canvas */
#particles {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 1;
}
/* cursor trail canvas */
#trail {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 998;
}
/* junimo spirits */
#junimos {
position: fixed;
top: 0; left: 0;
width: 100%; height: 100%;
pointer-events: none;
z-index: 2;
}
/* Stardew HUD overlay */
.stardew-hud {
position: fixed;
top: 10px;
right: 10px;
z-index: 900;
pointer-events: none;
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
/* wooden plank frame mixin — light wood border, beige interior */
.hud-frame {
background: #e8d8b0;
border: 3px solid #b88840;
border-radius: 2px;
box-shadow:
inset 0 0 0 1px #d0b878,
inset 0 0 0 2px rgba(0,0,0,0.08),
0 2px 6px rgba(0,0,0,0.5),
0 0 0 1px rgba(0,0,0,0.3);
image-rendering: pixelated;
}
/* clock/date box */
.hud-clock {
padding: 5px 10px 6px;
display: flex;
flex-direction: column;
align-items: center;
min-width: 110px;
}
.hud-date-row {
display: flex;
align-items: baseline;
gap: 4px;
width: 100%;
justify-content: center;
border-bottom: 1px solid #c8b080;
padding-bottom: 4px;
margin-bottom: 3px;
}
.hud-day {
font-size: 0.75rem;
font-weight: 700;
color: #3a2a10;
letter-spacing: 0.02em;
}
.hud-season {
font-size: 0.55rem;
color: #7a6a40;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.hud-time-row {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
justify-content: center;
}
.hud-sky {
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid #a08040;
overflow: hidden;
position: relative;
flex-shrink: 0;
}
.hud-sky-inner {
width: 100%;
height: 100%;
border-radius: 50%;
}
.hud-sun {
position: absolute;
width: 8px;
height: 8px;
background: #f8e040;
border-radius: 50%;
box-shadow: 0 0 4px #f8e040;
}
.hud-time {
font-size: 0.72rem;
font-weight: 700;
color: #3a2a10;
}
.hud-weather-icon {
font-size: 0.6rem;
color: #7a6a40;
}
/* gold box */
.hud-gold {
padding: 3px 8px;
display: flex;
align-items: center;
gap: 4px;
}
.hud-g-letter {
font-size: 0.7rem;
font-weight: 700;
color: #8a7030;
}
.hud-gold-bars {
display: flex;
gap: 1px;
height: 10px;
align-items: stretch;
}
.hud-gold-bar {
width: 6px;
background: #58a830;
border: 1px solid #408020;
border-radius: 1px;
}
.hud-gold-bar.empty {
background: #c8b888;
border-color: #a89868;
}
.hud-gold-amount {
font-size: 0.7rem;
font-weight: 700;
color: #3a2a10;
min-width: 30px;
text-align: right;
}
/* energy bar */
.hud-energy {
padding: 3px 6px;
display: flex;
align-items: center;
gap: 4px;
}
.hud-energy-bar {
width: 10px;
height: 50px;
background: #c8b888;
border: 2px solid #a08040;
border-radius: 1px;
position: relative;
overflow: hidden;
}
.hud-energy-fill {
position: absolute;
bottom: 0;
width: 100%;
height: 75%;
background: linear-gradient(to top, #38a828, #68d838);
}
.hud-energy-right {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.hud-energy-label {
font-size: 0.55rem;
font-weight: 700;
color: #8a7030;
}
.hud-energy-num {
font-size: 0.5rem;
color: #3a2a10;
white-space: nowrap;
}
@media (max-width: 600px) {
.stardew-hud { top: 6px; right: 6px; }
.hud-clock { min-width: 90px; padding: 4px 6px; }
.hud-sky { width: 22px; height: 22px; }
}
.shell {
max-width: 660px;
margin: 0 auto;
padding: 0 1.5rem;
position: relative;
z-index: 3;
}
/* nav */
nav {
display: flex;
align-items: baseline;
justify-content: space-between;
padding: 1.5rem 0 1.2rem;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
nav .site {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
font-size: 0.85rem;
font-weight: 500;
color: #f0d878;
text-decoration: none;
letter-spacing: 0.06em;
border-bottom: none;
transition: text-shadow 0.3s;
}
nav .site:hover {
text-shadow: 0 0 10px rgba(240, 216, 120, 0.5);
}
nav .links { display: flex; gap: 1.2rem; }
nav .links a {
font-size: 0.82rem;
color: #8878a0;
text-decoration: none;
border-bottom: none;
transition: color 0.2s, text-shadow 0.3s;
}
nav .links a:hover {
color: #c8b8e0;
text-shadow: 0 0 6px rgba(180, 160, 220, 0.3);
}
/* greeting */
#greeting {
padding: 1.5rem 0 0;
font-size: 0.85rem;
color: #6a5a80;
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: 700;
color: #f0d878;
margin-bottom: 1rem;
}
h1 a { color: #f0d878; text-decoration: none; border-bottom: none; }
h1 a:hover { color: #f8e8a0; }
h2 {
font-size: 1.05rem;
font-weight: 600;
color: #d0c0a0;
margin: 1.8rem 0 0.5rem;
}
p { margin: 0.5rem 0; color: #a098b0; }
a {
color: #c8b8e0;
text-decoration: none;
border-bottom: 1px solid rgba(200, 184, 224, 0.2);
transition: all 0.2s;
}
a:hover {
color: #e0d0f8;
border-bottom-color: rgba(200, 184, 224, 0.5);
}
em { color: #8a80a0; }
/* 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: rgba(20, 15, 50, 0.6);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 4px;
padding: 0.6rem 0.85rem;
color: #d0c8b8;
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(240, 216, 120, 0.4);
box-shadow: 0 0 12px rgba(240, 216, 120, 0.08);
}
button, input[type="submit"] {
background: rgba(30, 20, 60, 0.7);
border: 1px solid rgba(255,255,255,0.12);
border-radius: 4px;
padding: 0.6rem 1.1rem;
color: #a098b0;
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(50, 35, 90, 0.7);
color: #d0c0e0;
border-color: rgba(240, 216, 120, 0.3);
box-shadow: 0 0 10px rgba(240, 216, 120, 0.06);
}
/* search results */
.result {
padding: 1rem 0;
border-bottom: 1px solid rgba(255,255,255,0.05);
transition: background 0.2s;
}
.result:hover {
background: rgba(255,255,255,0.02);
}
.result:last-child { border-bottom: none; }
.result > a:first-child {
font-size: 1.02rem;
font-weight: 600;
color: #d8c890;
border-bottom: none;
}
.result > a:first-child:hover { color: #f0e0a8; }
.note {
margin-top: 0.3rem;
font-size: 0.9rem;
color: #7a7090;
}
.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: #8878a0;
border: 1px solid rgba(255,255,255,0.08);
border-radius: 3px;
padding: 0.1rem 0.35rem;
margin-right: 0.25rem;
}
.tag:hover, .tags a:hover {
color: #c8b8e0;
border-color: rgba(255,255,255,0.18);
}
/* trusted / remote results */
details {
margin: 1rem 0;
border: 1px solid rgba(255,255,255,0.08);
border-radius: 4px;
padding: 0.7rem 0.9rem;
background: rgba(20, 15, 50, 0.4);
}
summary {
font-size: 0.85rem;
color: #7a7090;
font-weight: 500;
}
summary:hover { color: #c8b8e0; }
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: #a098b0; }
li a { border-bottom: none; }
li a:hover { border-bottom: 1px solid rgba(200,184,224,0.4); }
/* code */
pre {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
font-size: 0.8rem;
background: rgba(15, 10, 40, 0.5);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 4px;
padding: 0.9rem;
overflow-x: auto;
color: #9088a8;
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(15, 10, 40, 0.4);
border-radius: 3px;
padding: 0.1rem 0.35rem;
color: #a898c0;
}
/* textarea */
textarea {
background: rgba(20, 15, 50, 0.6);
border: 1px solid rgba(255,255,255,0.1);
border-radius: 4px;
padding: 0.7rem 0.9rem;
color: #d0c8b8;
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%;
}
/* tables */
table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
th {
text-align: left;
font-size: 0.72rem;
font-weight: 500;
color: #6a6080;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 0.5rem 0.7rem;
border-bottom: 1px solid rgba(255,255,255,0.08);
}
td {
padding: 0.5rem 0.7rem;
border-bottom: 1px solid rgba(255,255,255,0.04);
font-size: 0.9rem;
}
/* misc */
label { color: #a098b0; }
input[type="checkbox"] { accent-color: #8878a0; }
hr { border: none; border-top: 1px solid rgba(255,255,255,0.06); margin: 1rem 0; }
small {
font-family: ui-monospace, 'SF Mono', 'Cascadia Code', 'Segoe UI Mono', Menlo, Consolas, monospace;
font-size: 0.7rem;
color: #5a5070;
}
/* footer */
footer {
border-top: 1px solid rgba(255,255,255,0.06);
padding: 1.5rem 0 2rem;
text-align: center;
color: #4a4060;
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: #3a3050;
margin-top: 0.25rem;
}
::selection { background: #3a2870; color: #f0e8d0; }
::-webkit-scrollbar { width: 5px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #2a2050; 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="particles"></canvas>
<canvas id="trail"></canvas>
<canvas id="junimos"></canvas>
<div class="stardew-hud">
<div class="hud-clock hud-frame">
<div class="hud-date-row">
<span class="hud-day" id="hud-day"></span>
</div>
<div class="hud-time-row">
<div class="hud-sky" id="hud-sky">
<div class="hud-sky-inner" id="hud-sky-inner"></div>
<div class="hud-sun" id="hud-sun"></div>
</div>
<span class="hud-time" id="hud-time"></span>
</div>
</div>
<div class="hud-gold hud-frame">
<span class="hud-g-letter">G</span>
<div class="hud-gold-bars" id="hud-gold-bars"></div>
<span class="hud-gold-amount" id="hud-gold"></span>
</div>
<div class="hud-energy hud-frame">
<div class="hud-energy-bar"><div class="hud-energy-fill" id="hud-energy-fill"></div></div>
<div class="hud-energy-right">
<span class="hud-energy-label">E</span>
<span class="hud-energy-num" id="hud-energy-num"></span>
</div>
</div>
</div>
<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 — stardew-style
var h = new Date().getHours();
var g = h < 5 ? "the junimos are sleeping... but the forest hums." :
h < 12 ? "good morning! the junimos are ready to help." :
h < 17 ? "a warm afternoon. the valley is peaceful." :
h < 21 ? "evening glow. the junimos dance in the twilight." :
"starlight over the valley. 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);
// Stardew HUD
var daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
var now = new Date();
var stardewDay = ((now.getDate() - 1) % 28) + 1;
var dayOfWeek = daysOfWeek[now.getDay() === 0 ? 6 : now.getDay() - 1];
document.getElementById('hud-day').textContent = dayOfWeek + '. ' + stardewDay;
// gold bars + amount
var goldSeed = now.getFullYear() * 366 + now.getMonth() * 31 + now.getDate();
var gold = (goldSeed * 127 + 2389) % 99000 + 1000;
var goldBarsEl = document.getElementById('hud-gold-bars');
var filledBars = Math.min(7, Math.floor(gold / 10000) + 1);
for (var gi = 0; gi < 7; gi++) {
var bar = document.createElement('div');
bar.className = 'hud-gold-bar' + (gi < filledBars ? '' : ' empty');
goldBarsEl.appendChild(bar);
}
document.getElementById('hud-gold').textContent = gold;
function hudTick() {
var d = new Date();
var hrs = d.getHours();
var mins = d.getMinutes();
var ampm = hrs >= 12 ? 'pm' : 'am';
var h12 = hrs % 12 || 12;
document.getElementById('hud-time').textContent = h12 + ':' + (mins < 10 ? '0' : '') + mins + ' ' + ampm;
// sky circle — gradient changes with time of day
var skyInner = document.getElementById('hud-sky-inner');
var sunEl = document.getElementById('hud-sun');
var dayProgress = (hrs * 60 + mins) / (24 * 60);
var skyColor1, skyColor2;
if (hrs >= 6 && hrs < 8) {
skyColor1 = '#f8a860'; skyColor2 = '#80b8e8'; // sunrise
} else if (hrs >= 8 && hrs < 17) {
skyColor1 = '#60a8e8'; skyColor2 = '#a0d0f8'; // day
} else if (hrs >= 17 && hrs < 20) {
skyColor1 = '#e88040'; skyColor2 = '#4060a0'; // sunset
} else {
skyColor1 = '#101838'; skyColor2 = '#182848'; // night
}
skyInner.style.background = 'linear-gradient(to bottom, ' + skyColor1 + ', ' + skyColor2 + ')';
// sun/moon position — arc across the circle
var angle = (dayProgress - 0.25) * Math.PI * 2; // 6am = left, noon = top, 6pm = right
var sunX = 10 + Math.cos(angle) * 8;
var sunY = 10 - Math.sin(angle) * 8;
sunEl.style.left = sunX + 'px';
sunEl.style.top = sunY + 'px';
if (hrs >= 20 || hrs < 6) {
sunEl.style.background = '#e8e0c0';
sunEl.style.boxShadow = '0 0 3px #e8e0c0';
} else {
sunEl.style.background = '#f8e040';
sunEl.style.boxShadow = '0 0 4px #f8e040';
}
// energy bar
var maxEnergy = 270;
var energy = Math.max(20, Math.round(maxEnergy - dayProgress * maxEnergy * 0.75));
document.getElementById('hud-energy-fill').style.height = (energy / maxEnergy * 100) + '%';
document.getElementById('hud-energy-num').textContent = energy + '/' + maxEnergy;
if (energy < maxEnergy * 0.25) {
document.getElementById('hud-energy-fill').style.background = 'linear-gradient(to top, #c83030, #e06040)';
}
}
hudTick();
setInterval(hudTick, 30000);
// ===== BACKGROUND SCENE — starry sky + rolling hills + junimo hut =====
var sc = document.getElementById('scene');
var sctx = sc.getContext('2d');
function resizeScene() {
sc.width = window.innerWidth;
sc.height = window.innerHeight;
drawScene();
}
function drawScene() {
var w = sc.width, h = sc.height;
// night sky gradient
var sky = sctx.createLinearGradient(0, 0, 0, h);
sky.addColorStop(0, '#0a0820');
sky.addColorStop(0.3, '#161050');
sky.addColorStop(0.6, '#1b1040');
sky.addColorStop(0.85, '#1a2848');
sky.addColorStop(1, '#1a3028');
sctx.fillStyle = sky;
sctx.fillRect(0, 0, w, h);
// pixel stars — draw as small squares for pixel-art feel
var starSeed = 42;
function seededRandom() {
starSeed = (starSeed * 16807 + 0) % 2147483647;
return (starSeed - 1) / 2147483646;
}
for (var i = 0; i < 120; i++) {
var sx = seededRandom() * w;
var sy = seededRandom() * h * 0.7;
var ss = seededRandom() > 0.85 ? 2 : 1;
var sa = 0.3 + seededRandom() * 0.7;
sctx.fillStyle = 'rgba(255, 255, 240, ' + sa + ')';
sctx.fillRect(Math.floor(sx), Math.floor(sy), ss, ss);
}
// a few colored stars
for (var i = 0; i < 8; i++) {
var sx = seededRandom() * w;
var sy = seededRandom() * h * 0.5;
var colors = ['rgba(255,200,150,0.6)', 'rgba(150,200,255,0.5)', 'rgba(255,255,180,0.7)'];
sctx.fillStyle = colors[i % 3];
sctx.fillRect(Math.floor(sx), Math.floor(sy), 2, 2);
}
// ===== PIXEL MOON =====
var moonX = w * 0.15;
var moonY = h * 0.12;
var mp = 3; // moon pixel size
// moon glow
var mgrd = sctx.createRadialGradient(moonX, moonY, mp * 2, moonX, moonY, mp * 14);
mgrd.addColorStop(0, 'rgba(255, 250, 200, 0.12)');
mgrd.addColorStop(0.5, 'rgba(255, 250, 200, 0.04)');
mgrd.addColorStop(1, 'rgba(255, 250, 200, 0)');
sctx.fillStyle = mgrd;
sctx.beginPath();
sctx.arc(moonX, moonY, mp * 14, 0, Math.PI * 2);
sctx.fill();
// moon body — crescent shape drawn as pixels
var moonPx = [
// full circle pixels (8x8 ish)
[0,-3],[1,-3],
[-2,-2],[-1,-2],[0,-2],[1,-2],[2,-2],
[-3,-1],[-2,-1],[-1,-1],[0,-1],[1,-1],[2,-1],
[-3,0],[-2,0],[-1,0],[0,0],[1,0],[2,0],
[-3,1],[-2,1],[-1,1],[0,1],[1,1],[2,1],
[-2,2],[-1,2],[0,2],[1,2],[2,2],
[0,3],[1,3]
];
// shadow pixels to carve crescent (offset to upper-right)
var moonShadow = [
[1,-2],[2,-2],
[1,-1],[2,-1],[3,-1],
[2,0],[3,0],
[1,1],[2,1],[3,1],
[1,2],[2,2],
];
// draw moon base
sctx.fillStyle = '#f0e8c8';
for (var mi = 0; mi < moonPx.length; mi++) {
sctx.fillRect(moonX + moonPx[mi][0] * mp, moonY + moonPx[mi][1] * mp, mp, mp);
}
// lighter highlight
sctx.fillStyle = '#f8f0d8';
sctx.fillRect(moonX - 2 * mp, moonY - 1 * mp, mp, mp);
sctx.fillRect(moonX - 1 * mp, moonY - 2 * mp, mp, mp);
// carve out shadow for crescent
sctx.fillStyle = '#0a0820';
for (var mi = 0; mi < moonShadow.length; mi++) {
sctx.fillRect(moonX + moonShadow[mi][0] * mp, moonY + moonShadow[mi][1] * mp, mp, mp);
}
// distant hills (dark blue-green)
var hillBase = h * 0.82;
sctx.fillStyle = '#142828';
sctx.beginPath();
sctx.moveTo(0, hillBase);
for (var x = 0; x <= w; x += 3) {
var y = hillBase - Math.sin(x * 0.003) * 40 - Math.sin(x * 0.008 + 1) * 20 - Math.sin(x * 0.001 + 2) * 30;
sctx.lineTo(x, y);
}
sctx.lineTo(w, h);
sctx.lineTo(0, h);
sctx.closePath();
sctx.fill();
// ===== TOWN SILHOUETTE on distant hills =====
var tp = 2; // town pixel size
var townBase = function(tx) {
return hillBase - Math.sin(tx * 0.003) * 40 - Math.sin(tx * 0.008 + 1) * 20 - Math.sin(tx * 0.001 + 2) * 30;
};
// clock tower
var ctX = w * 0.3;
var ctY = townBase(ctX);
sctx.fillStyle = '#1a3030';
sctx.fillRect(ctX - tp * 2, ctY - tp * 10, tp * 4, tp * 10);
sctx.fillRect(ctX - tp * 3, ctY - tp * 8, tp * 6, tp * 2);
// pointed roof
sctx.fillRect(ctX - tp, ctY - tp * 12, tp * 2, tp * 2);
sctx.fillRect(ctX, ctY - tp * 13, tp, tp);
// clock face glow
sctx.fillStyle = 'rgba(240, 220, 120, 0.25)';
sctx.fillRect(ctX - tp, ctY - tp * 7, tp * 2, tp * 2);
// Pierre's shop
var psX = w * 0.35;
var psY = townBase(psX);
sctx.fillStyle = '#1a3030';
sctx.fillRect(psX - tp * 4, psY - tp * 6, tp * 8, tp * 6);
// roof
sctx.fillStyle = '#1a2828';
sctx.fillRect(psX - tp * 5, psY - tp * 7, tp * 10, tp);
sctx.fillRect(psX - tp * 4, psY - tp * 8, tp * 8, tp);
// windows
sctx.fillStyle = 'rgba(240, 200, 80, 0.15)';
sctx.fillRect(psX - tp * 2, psY - tp * 4, tp * 2, tp * 2);
sctx.fillRect(psX + tp, psY - tp * 4, tp * 2, tp * 2);
// community center (larger, to the left)
var ccX = w * 0.18;
var ccY = townBase(ccX);
sctx.fillStyle = '#162828';
sctx.fillRect(ccX - tp * 6, ccY - tp * 7, tp * 12, tp * 7);
sctx.fillRect(ccX - tp * 4, ccY - tp * 9, tp * 8, tp * 2);
sctx.fillRect(ccX - tp * 2, ccY - tp * 10, tp * 4, tp);
// chimney
sctx.fillRect(ccX + tp * 3, ccY - tp * 11, tp * 2, tp * 3);
// warm window glow
sctx.fillStyle = 'rgba(120, 220, 100, 0.12)';
sctx.fillRect(ccX - tp * 4, ccY - tp * 5, tp * 3, tp * 3);
sctx.fillStyle = 'rgba(240, 200, 80, 0.1)';
sctx.fillRect(ccX + tp * 2, ccY - tp * 5, tp * 3, tp * 3);
// small houses
var shColors = ['#1a2e2e', '#1a2a30', '#182a2a'];
for (var shi = 0; shi < 3; shi++) {
var shX = w * (0.4 + shi * 0.06);
var shY = townBase(shX);
sctx.fillStyle = shColors[shi];
sctx.fillRect(shX - tp * 2, shY - tp * 4, tp * 4, tp * 4);
sctx.fillRect(shX - tp * 3, shY - tp * 5, tp * 6, tp);
// tiny window
sctx.fillStyle = 'rgba(240, 200, 80, 0.12)';
sctx.fillRect(shX - tp, shY - tp * 3, tp, tp);
}
// mid hills (darker green)
var hill2Base = h * 0.88;
sctx.fillStyle = '#0f2218';
sctx.beginPath();
sctx.moveTo(0, hill2Base);
for (var x = 0; x <= w; x += 3) {
var y = hill2Base - Math.sin(x * 0.005 + 3) * 25 - Math.sin(x * 0.012 + 1) * 12;
sctx.lineTo(x, y);
}
sctx.lineTo(w, h);
sctx.lineTo(0, h);
sctx.closePath();
sctx.fill();
// foreground hill (deep dark)
var hill3Base = h * 0.93;
sctx.fillStyle = '#0a1810';
sctx.beginPath();
sctx.moveTo(0, hill3Base);
for (var x = 0; x <= w; x += 3) {
var y = hill3Base - Math.sin(x * 0.007 + 5) * 15 - Math.sin(x * 0.015) * 8;
sctx.lineTo(x, y);
}
sctx.lineTo(w, h);
sctx.lineTo(0, h);
sctx.closePath();
sctx.fill();
// ===== JUNIMO HUT — pixel art style =====
var hutX = w * 0.85;
var hutY = h * 0.82 - Math.sin(w * 0.85 * 0.005 + 3) * 25 - Math.sin(w * 0.85 * 0.012 + 1) * 12;
var p = 3; // pixel size
// hut body (brownish wood)
sctx.fillStyle = '#5a3a20';
for (var bx = -4; bx <= 4; bx++) {
for (var by = 0; by <= 5; by++) {
sctx.fillRect(hutX + bx * p, hutY - by * p, p, p);
}
}
// darker wood planks
sctx.fillStyle = '#4a2e18';
for (var bx = -4; bx <= 4; bx += 2) {
sctx.fillRect(hutX + bx * p, hutY - 1 * p, p, p);
sctx.fillRect(hutX + (bx+1) * p, hutY - 3 * p, p, p);
}
// door
sctx.fillStyle = '#2a1a10';
for (var by = 0; by <= 2; by++) {
sctx.fillRect(hutX - p, hutY - by * p, p, p);
sctx.fillRect(hutX, hutY - by * p, p, p);
}
// roof (thatched / straw colored)
sctx.fillStyle = '#8a7030';
for (var row = 0; row < 4; row++) {
var roofW = 6 - row;
for (var rx = -roofW; rx <= roofW; rx++) {
sctx.fillRect(hutX + rx * p, hutY - (6 + row) * p, p, p);
}
}
// roof highlight
sctx.fillStyle = '#a88838';
for (var rx = -4; rx <= 3; rx++) {
sctx.fillRect(hutX + rx * p, hutY - 7 * p, p, p);
}
// chimney
sctx.fillStyle = '#6a4428';
sctx.fillRect(hutX + 3 * p, hutY - 9 * p, p, p);
sctx.fillRect(hutX + 3 * p, hutY - 10 * p, p, p);
sctx.fillRect(hutX + 4 * p, hutY - 9 * p, p, p);
sctx.fillRect(hutX + 4 * p, hutY - 10 * p, p, p);
// window glow
sctx.fillStyle = 'rgba(240, 200, 80, 0.5)';
sctx.fillRect(hutX + 2 * p, hutY - 3 * p, p * 2, p * 2);
sctx.fillStyle = 'rgba(240, 200, 80, 0.15)';
sctx.beginPath();
sctx.arc(hutX + 3 * p, hutY - 2 * p, p * 6, 0, Math.PI * 2);
sctx.fill();
// a few pixel trees near the hut
function drawPixelTree(tx, ty, treeH) {
// trunk
sctx.fillStyle = '#3a2a18';
sctx.fillRect(tx, ty - treeH * p, p, treeH * p);
// canopy layers
var greens = ['#1a4a20', '#1e5a28', '#226830'];
for (var layer = 0; layer < 3; layer++) {
sctx.fillStyle = greens[layer];
var layerW = 3 - layer;
var layerY = ty - (treeH + layer * 2) * p;
for (var lx = -layerW; lx <= layerW; lx++) {
sctx.fillRect(tx + lx * p, layerY, p, p * 2);
}
}
}
drawPixelTree(hutX - 10 * p, hutY + p, 4);
drawPixelTree(hutX + 10 * p, hutY + p, 3);
drawPixelTree(hutX - 14 * p, hutY + 2 * p, 5);
// ===== FARM BUILDINGS =====
// silo (tall, thin, to the right of hut)
var siloX = hutX + 18 * p;
var siloY = hutY + 2 * p;
sctx.fillStyle = '#5a4a38';
sctx.fillRect(siloX - p * 2, siloY - p * 10, p * 4, p * 10);
// silo cap
sctx.fillStyle = '#7a6040';
sctx.fillRect(siloX - p * 3, siloY - p * 11, p * 6, p);
sctx.fillRect(siloX - p * 2, siloY - p * 12, p * 4, p);
// silo bands
sctx.fillStyle = '#4a3828';
sctx.fillRect(siloX - p * 2, siloY - p * 4, p * 4, p);
sctx.fillRect(siloX - p * 2, siloY - p * 8, p * 4, p);
// barn (wide, left of hut)
var barnX = hutX - 22 * p;
var barnY = hutY + 3 * p;
// barn walls
sctx.fillStyle = '#6a3028';
for (var bxi = -5; bxi <= 5; bxi++) {
for (var byi = 0; byi <= 6; byi++) {
sctx.fillRect(barnX + bxi * p, barnY - byi * p, p, p);
}
}
// barn roof
sctx.fillStyle = '#4a4a4a';
for (var row = 0; row < 3; row++) {
var roofW = 7 - row;
for (var rx = -roofW; rx <= roofW; rx++) {
sctx.fillRect(barnX + rx * p, barnY - (7 + row) * p, p, p);
}
}
// barn door
sctx.fillStyle = '#3a1a10';
sctx.fillRect(barnX - p, barnY - p * 3, p * 3, p * 3);
// hay window
sctx.fillStyle = 'rgba(200, 180, 80, 0.2)';
sctx.fillRect(barnX + p * 2, barnY - p * 5, p * 2, p * 2);
// shipping bin (small box near hut)
var sbX = hutX - 7 * p;
var sbY = hutY + p;
sctx.fillStyle = '#5a4a30';
sctx.fillRect(sbX - p * 2, sbY - p * 2, p * 4, p * 2);
sctx.fillStyle = '#6a5838';
sctx.fillRect(sbX - p * 2, sbY - p * 3, p * 4, p);
// lid highlight
sctx.fillStyle = '#7a6840';
sctx.fillRect(sbX - p, sbY - p * 3, p * 2, p);
// ===== FOREGROUND DETAILS — fence, flowers, mushrooms =====
var fgY = h * 0.93;
var fp = 2; // foreground pixel size
// wooden fence posts across the bottom
function drawFencePost(fx, fy) {
// post
sctx.fillStyle = '#4a3520';
sctx.fillRect(fx, fy - fp * 5, fp, fp * 5);
sctx.fillRect(fx + fp, fy - fp * 5, fp, fp * 5);
// cap
sctx.fillStyle = '#5a4228';
sctx.fillRect(fx - fp, fy - fp * 6, fp * 4, fp);
// highlight
sctx.fillStyle = '#6a5238';
sctx.fillRect(fx, fy - fp * 5, fp, fp);
}
// rail between posts
function drawFenceRail(fx1, fx2, fy) {
sctx.fillStyle = '#4a3520';
sctx.fillRect(fx1 + fp * 2, fy - fp * 3, fx2 - fx1 - fp * 2, fp);
sctx.fillStyle = '#3a2818';
sctx.fillRect(fx1 + fp * 2, fy - fp * 2, fx2 - fx1 - fp * 2, fp);
}
// place fence posts
var fencePosts = [];
for (var fi = 0; fi < 6; fi++) {
var fx = w * 0.05 + fi * w * 0.12;
var fy = fgY - Math.sin(fx * 0.007 + 5) * 15 - Math.sin(fx * 0.015) * 8 + fp * 2;
fencePosts.push({ x: fx, y: fy });
drawFencePost(fx, fy);
}
for (var fi = 0; fi < fencePosts.length - 1; fi++) {
drawFenceRail(fencePosts[fi].x, fencePosts[fi + 1].x, fencePosts[fi].y);
}
// pixel flowers
function drawFlower(fx, fy, petalColor, centerColor) {
// stem
sctx.fillStyle = '#2a5a1a';
sctx.fillRect(fx, fy - fp * 2, fp, fp * 2);
// petals
sctx.fillStyle = petalColor;
sctx.fillRect(fx - fp, fy - fp * 3, fp, fp);
sctx.fillRect(fx + fp, fy - fp * 3, fp, fp);
sctx.fillRect(fx, fy - fp * 4, fp, fp);
sctx.fillRect(fx, fy - fp * 2, fp, fp);
// center
sctx.fillStyle = centerColor;
sctx.fillRect(fx, fy - fp * 3, fp, fp);
}
// pixel mushroom
function drawMushroom(mx, my, capColor) {
// stem
sctx.fillStyle = '#e8e0d0';
sctx.fillRect(mx, my - fp * 2, fp, fp * 2);
sctx.fillRect(mx + fp, my - fp * 2, fp, fp * 2);
// cap
sctx.fillStyle = capColor;
sctx.fillRect(mx - fp, my - fp * 4, fp * 4, fp);
sctx.fillRect(mx - fp * 2, my - fp * 3, fp * 6, fp);
// spots
sctx.fillStyle = '#f8f0e0';
sctx.fillRect(mx - fp, my - fp * 3, fp, fp);
sctx.fillRect(mx + fp * 2, my - fp * 3, fp, fp);
}
// pixel parsnip/turnip
function drawCrop(cx, cy) {
// leaves
sctx.fillStyle = '#3a8a2a';
sctx.fillRect(cx, cy - fp * 4, fp, fp * 2);
sctx.fillRect(cx - fp, cy - fp * 3, fp, fp);
sctx.fillRect(cx + fp, cy - fp * 3, fp, fp);
// root
sctx.fillStyle = '#e8d8a0';
sctx.fillRect(cx, cy - fp * 2, fp, fp * 2);
sctx.fillStyle = '#d0c080';
sctx.fillRect(cx, cy - fp, fp, fp);
}
// scatter flowers, mushrooms, crops along the foreground
var fgSeed = 77;
function fgRand() {
fgSeed = (fgSeed * 16807) % 2147483647;
return (fgSeed - 1) / 2147483646;
}
var flowerColors = [
['#e06080', '#f0d060'], // pink/yellow
['#6080e0', '#f0e080'], // blue/yellow
['#e0e060', '#e08040'], // yellow/orange
['#d060d0', '#f0d060'], // purple/yellow
['#f08080', '#f0e080'], // red/yellow
];
for (var di = 0; di < 14; di++) {
var dx = fgRand() * w * 0.95 + w * 0.02;
var dy = fgY - Math.sin(dx * 0.007 + 5) * 15 - Math.sin(dx * 0.015) * 8;
var what = fgRand();
if (what < 0.45) {
var fc = flowerColors[Math.floor(fgRand() * flowerColors.length)];
drawFlower(dx, dy, fc[0], fc[1]);
} else if (what < 0.7) {
var capCol = fgRand() > 0.5 ? '#c83030' : '#d08030';
drawMushroom(dx, dy, capCol);
} else {
drawCrop(dx, dy);
}
}
}
resizeScene();
window.addEventListener('resize', resizeScene);
// ===== TWINKLING STARS (animated) =====
var twinkleStars = [];
var starSeed2 = 99;
function sr2() {
starSeed2 = (starSeed2 * 16807) % 2147483647;
return (starSeed2 - 1) / 2147483646;
}
for (var i = 0; i < 20; i++) {
twinkleStars.push({
x: sr2(), y: sr2() * 0.65,
phase: sr2() * Math.PI * 2,
speed: 0.5 + sr2() * 1.5,
size: sr2() > 0.7 ? 2 : 1
});
}
// ===== FIREFLY 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 < 30; i++) {
dots.push({
x: Math.random() * pc.width,
y: Math.random() * pc.height,
vy: (Math.random() - 0.5) * 0.1,
vx: (Math.random() - 0.5) * 0.15,
r: Math.random() * 1.2 + 0.5,
o: Math.random() * 0.4 + 0.1,
drift: Math.random() * Math.PI * 2,
hue: Math.random() > 0.6 ? '200, 230, 80' : '240, 210, 60'
});
}
// ===== SHOOTING STARS =====
var shootingStars = [];
function spawnShootingStar() {
shootingStars.push({
x: Math.random() * pc.width * 0.7,
y: Math.random() * pc.height * 0.3,
vx: 3 + Math.random() * 4,
vy: 1.5 + Math.random() * 2,
life: 0,
maxLife: 30 + Math.random() * 30,
size: 1 + (Math.random() > 0.7 ? 1 : 0),
trail: []
});
}
function drawParticles() {
pctx.clearRect(0, 0, pc.width, pc.height);
var t = Date.now() * 0.001;
// twinkling stars
for (var i = 0; i < twinkleStars.length; i++) {
var ts = twinkleStars[i];
var brightness = 0.3 + 0.7 * Math.pow((Math.sin(t * ts.speed + ts.phase) + 1) / 2, 2);
pctx.fillStyle = 'rgba(255, 255, 240, ' + brightness + ')';
pctx.fillRect(
Math.floor(ts.x * pc.width),
Math.floor(ts.y * pc.height),
ts.size, ts.size
);
}
// shooting stars
if (Math.random() < 0.003) spawnShootingStar();
for (var si = shootingStars.length - 1; si >= 0; si--) {
var ss = shootingStars[si];
ss.life++;
ss.x += ss.vx;
ss.y += ss.vy;
ss.trail.push({ x: ss.x, y: ss.y });
if (ss.trail.length > 12) ss.trail.shift();
var ssa = Math.max(0, 1 - ss.life / ss.maxLife);
// draw trail
for (var ti = 0; ti < ss.trail.length; ti++) {
var ta = (ti / ss.trail.length) * ssa * 0.8;
pctx.fillStyle = 'rgba(255, 255, 220, ' + ta + ')';
pctx.fillRect(Math.floor(ss.trail[ti].x), Math.floor(ss.trail[ti].y), ss.size, ss.size);
}
// bright head
pctx.fillStyle = 'rgba(255, 255, 240, ' + ssa + ')';
pctx.fillRect(Math.floor(ss.x), Math.floor(ss.y), ss.size + 1, ss.size + 1);
// sparkle glow around head
pctx.beginPath();
pctx.arc(ss.x, ss.y, ss.size * 3, 0, Math.PI * 2);
pctx.fillStyle = 'rgba(255, 255, 200, ' + (ssa * 0.15) + ')';
pctx.fill();
if (ss.life > ss.maxLife) shootingStars.splice(si, 1);
}
// fireflies
for (var i = 0; i < dots.length; i++) {
var d = dots[i];
d.x += d.vx + Math.sin(t * 0.7 + d.drift) * 0.1;
d.y += d.vy + Math.cos(t * 0.5 + d.drift) * 0.08;
if (d.y < -5) d.y = pc.height + 5;
if (d.y > pc.height + 5) d.y = -5;
if (d.x < -5) d.x = pc.width + 5;
if (d.x > pc.width + 5) d.x = -5;
var flicker = d.o * (0.3 + 0.7 * Math.pow((Math.sin(t * 2.5 + d.drift) + 1) / 2, 3));
// outer glow
pctx.beginPath();
pctx.arc(d.x, d.y, d.r * 4, 0, Math.PI * 2);
pctx.fillStyle = 'rgba(' + d.hue + ', ' + (flicker * 0.15) + ')';
pctx.fill();
// core
pctx.beginPath();
pctx.arc(d.x, d.y, d.r, 0, Math.PI * 2);
pctx.fillStyle = 'rgba(' + d.hue + ', ' + flicker + ')';
pctx.fill();
}
requestAnimationFrame(drawParticles);
}
drawParticles();
// ===== CURSOR TRAIL — golden sparkle =====
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: e.clientX, y: e.clientY, t: Date.now() });
if (points.length > 25) points.shift();
});
function drawTrail() {
tctx.clearRect(0, 0, tc.width, tc.height);
var now = Date.now();
while (points.length && now - points[0].t > 350) points.shift();
if (points.length > 1) {
for (var i = 1; i < points.length; i++) {
var age = (now - points[i].t) / 350;
var alpha = (1 - age) * 0.35;
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(240, 216, 120, ' + alpha + ')';
tctx.lineWidth = width;
tctx.lineCap = 'round';
tctx.stroke();
}
}
requestAnimationFrame(drawTrail);
}
drawTrail();
// ===== JUNIMO SPIRITS — pixel-art style =====
var jc = document.getElementById('junimos');
var jctx = jc.getContext('2d');
var spirits = [];
var numSpirits = 8;
// junimo colors matching stardew valley
var junimoColors = [
{ body: [76, 200, 60], light: [110, 230, 90], tip: [180, 80, 200] }, // green w/ purple tip
{ body: [140, 80, 200], light: [170, 120, 230], tip: [200, 80, 180] }, // purple w/ pink tip
{ body: [220, 200, 50], light: [240, 225, 90], tip: [240, 180, 50] }, // yellow w/ orange tip
{ body: [80, 120, 220], light: [110, 150, 240], tip: [80, 60, 200] }, // blue w/ indigo tip
{ body: [230, 140, 60], light: [245, 170, 90], tip: [230, 100, 50] }, // orange
{ body: [80, 210, 190], light: [120, 235, 215], tip: [60, 180, 160] }, // teal
{ body: [200, 60, 70], light: [230, 90, 100], tip: [170, 40, 50] }, // red
{ body: [230, 120, 170],light: [245, 155, 200], tip: [200, 80, 140] } // pink
];
function resizeJunimos() {
jc.width = window.innerWidth;
jc.height = window.innerHeight;
}
resizeJunimos();
window.addEventListener('resize', resizeJunimos);
// hop sparkle particles
var sparkles = [];
function spawnSparkles(sx, sy, col) {
for (var k = 0; k < 6; k++) {
sparkles.push({
x: sx + (Math.random() - 0.5) * 8,
y: sy,
vx: (Math.random() - 0.5) * 2,
vy: -(Math.random() * 2.5 + 1),
life: 0,
maxLife: 15 + Math.random() * 15,
size: 1 + (Math.random() > 0.5 ? 1 : 0),
color: col
});
}
}
for (var i = 0; i < numSpirits; i++) {
spirits.push({
x: 0.08 + (i / numSpirits) * 0.84 + (Math.random() - 0.5) * 0.08,
baseY: 0.78 + Math.random() * 0.15,
size: 14 + Math.random() * 8,
phase: Math.random() * Math.PI * 2,
bounceSpeed: 2.5 + Math.random() * 1.5,
wobbleSpeed: 1.2 + Math.random() * 0.8,
opacity: 0.5 + Math.random() * 0.3,
targetOpacity: 0.85 + Math.random() * 0.15,
fadeSpeed: 0.01 + Math.random() * 0.005,
appearing: true,
timer: 0,
lifespan: 800 + Math.random() * 600,
hopTime: 0,
hopping: false,
colorIdx: i % junimoColors.length,
armWave: Math.random() * Math.PI * 2,
lookAngle: 0,
excited: false,
excitedTimer: 0
});
}
function drawPixelRect(x, y, w, h, color) {
jctx.fillStyle = color;
jctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h));
}
function drawJunimoSpirit(x, y, size, bounce, wobble, opacity, sp) {
jctx.save();
jctx.translate(Math.round(x), Math.round(y));
jctx.globalAlpha = opacity;
var col = junimoColors[sp.colorIdx];
var p = Math.max(1, Math.round(size / 7)); // pixel unit size
var t = Date.now() * 0.001;
// soft colored glow underneath
var grd = jctx.createRadialGradient(0, 0, p, 0, 0, size * 1.2);
grd.addColorStop(0, 'rgba(' + col.body[0] + ',' + col.body[1] + ',' + col.body[2] + ', 0.15)');
grd.addColorStop(1, 'rgba(' + col.body[0] + ',' + col.body[1] + ',' + col.body[2] + ', 0)');
jctx.fillStyle = grd;
jctx.beginPath();
jctx.arc(0, 0, size * 1.2, 0, Math.PI * 2);
jctx.fill();
jctx.save();
jctx.rotate(wobble);
// body dimensions: rounded square, ~5p wide x 5p tall
var bw = p * 5; // body width
var bh = p * 5; // body height
var bx = -bw / 2;
var by = -bh / 2 - p;
// === BLACK OUTLINE (drawn first, slightly larger) ===
var o = Math.max(1, Math.round(p * 0.4)); // outline thickness
// outline - top row (narrower)
drawPixelRect(bx + p, by - o, bw - p * 2, o, 'rgba(0,0,0,0.9)');
// outline - bottom row (narrower)
drawPixelRect(bx + p, by + bh, bw - p * 2, o, 'rgba(0,0,0,0.9)');
// outline - left column
drawPixelRect(bx - o, by + p, o, bh - p * 2, 'rgba(0,0,0,0.9)');
// outline - right column
drawPixelRect(bx + bw, by + p, o, bh - p * 2, 'rgba(0,0,0,0.9)');
// corner pixels for rounding
drawPixelRect(bx, by, p, p, 'rgba(0,0,0,0.9)');
drawPixelRect(bx + bw - p, by, p, p, 'rgba(0,0,0,0.9)');
drawPixelRect(bx, by + bh - p, p, p, 'rgba(0,0,0,0.9)');
drawPixelRect(bx + bw - p, by + bh - p, p, p, 'rgba(0,0,0,0.9)');
// === MAIN BODY FILL ===
var bodyColor = 'rgb(' + col.body[0] + ',' + col.body[1] + ',' + col.body[2] + ')';
var lightColor = 'rgb(' + col.light[0] + ',' + col.light[1] + ',' + col.light[2] + ')';
// fill body (rounded square — skip corners)
// top row (narrower)
drawPixelRect(bx + p, by, bw - p * 2, p, bodyColor);
// middle rows (full width)
drawPixelRect(bx, by + p, bw, bh - p * 2, bodyColor);
// bottom row (narrower)
drawPixelRect(bx + p, by + bh - p, bw - p * 2, p, bodyColor);
// highlight on upper-left area
drawPixelRect(bx + p, by + p, p * 2, p, lightColor);
drawPixelRect(bx + p, by + p * 2, p, p, lightColor);
// === ANTENNA / STEM ===
// dark stem line going up from top center
drawPixelRect(-p / 2, by - p * 3, p, p * 3, 'rgba(0,0,0,0.85)');
// colored tip at top of stem
var tipColor = 'rgb(' + col.tip[0] + ',' + col.tip[1] + ',' + col.tip[2] + ')';
drawPixelRect(-p, by - p * 4, p * 2, p, tipColor);
// === ARMS — tiny black sticks on sides ===
var armBob = Math.sin(t * 3 + sp.armWave) * p * 0.5;
// left arm
drawPixelRect(bx - p * 2, by + p * 2 + armBob, p * 2, p, 'rgba(0,0,0,0.85)');
// right arm
drawPixelRect(bx + bw, by + p * 2 - armBob, p * 2, p, 'rgba(0,0,0,0.85)');
// === FEET — two small black bumps at bottom ===
drawPixelRect(bx + p, by + bh, p, p, 'rgba(0,0,0,0.85)');
drawPixelRect(bx + bw - p * 2, by + bh, p, p, 'rgba(0,0,0,0.85)');
// === EYES — two tiny dark dots ===
drawPixelRect(bx + p, by + p * 2, p, p, 'rgba(10,10,20,0.9)');
drawPixelRect(bx + p * 3, by + p * 2, p, p, 'rgba(10,10,20,0.9)');
// === CHEEKS — pink marks ===
drawPixelRect(bx, by + p * 3, p, p, 'rgba(240,130,160,0.7)');
drawPixelRect(bx + bw - p, by + p * 3, p, p, 'rgba(240,130,160,0.7)');
jctx.restore(); // wobble
jctx.restore(); // position
}
function drawJunimos() {
jctx.clearRect(0, 0, jc.width, jc.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.84 + 0.08;
s.baseY = 0.78 + Math.random() * 0.15;
s.targetOpacity = 0.85 + Math.random() * 0.15;
s.timer = 0;
s.lifespan = 800 + Math.random() * 600;
s.appearing = true;
s.colorIdx = Math.floor(Math.random() * junimoColors.length);
s.hopTime = 0;
s.excited = false;
}
}
if (s.opacity <= 0) continue;
var px = s.x * jc.width;
var py = s.baseY * jc.height;
// cursor interaction — detect proximity
var dx = mx - px;
var dy = my - py;
var dist = Math.sqrt(dx * dx + dy * dy);
var cursorNear = dist < 120;
// look toward cursor — tilt head slightly
var targetLook = 0;
if (dist < 300 && s.opacity > 0.2) {
targetLook = Math.atan2(dx, 100) * 0.15; // subtle lean toward cursor
}
s.lookAngle += (targetLook - s.lookAngle) * 0.08;
// get excited when cursor is close — start hopping rapidly
if (cursorNear && !s.excited && s.opacity > 0.3) {
s.excited = true;
s.excitedTimer = 0;
if (!s.hopping) {
s.hopping = true;
s.hopTime = 0;
// spawn sparkles on excited hop
var col = junimoColors[s.colorIdx];
spawnSparkles(px, py, 'rgba(' + col.body[0] + ',' + col.body[1] + ',' + col.body[2] + ',');
}
}
if (s.excited) {
s.excitedTimer++;
// keep hopping while excited (rapid little hops)
if (!s.hopping && s.excitedTimer % 20 === 0 && cursorNear) {
s.hopping = true;
s.hopTime = 0;
var col = junimoColors[s.colorIdx];
spawnSparkles(px, py, 'rgba(' + col.body[0] + ',' + col.body[1] + ',' + col.body[2] + ',');
}
if (!cursorNear && s.excitedTimer > 40) s.excited = false;
}
// random hopping (when not excited)
if (!s.hopping && !s.excited && Math.random() < 0.005) {
s.hopping = true;
s.hopTime = 0;
var col = junimoColors[s.colorIdx];
spawnSparkles(px, py, 'rgba(' + col.body[0] + ',' + col.body[1] + ',' + col.body[2] + ',');
}
var bounce = Math.abs(Math.sin(t * s.bounceSpeed + s.phase)) * 2;
if (s.hopping) {
s.hopTime++;
var hopHeight = s.excited ? 7 : 10;
var hopDuration = s.excited ? 20 : 35;
bounce = Math.sin(s.hopTime * 0.15) * hopHeight * Math.max(0, 1 - s.hopTime / hopDuration);
if (bounce < 0) bounce = 0;
if (s.hopTime > hopDuration) s.hopping = false;
}
var wobble = Math.sin(t * s.wobbleSpeed + s.phase) * 0.06 + s.lookAngle;
if (s.hopping) {
wobble = Math.sin(s.hopTime * 0.3) * 0.12 + s.lookAngle;
}
// extra wiggle when excited
if (s.excited && cursorNear) {
wobble += Math.sin(t * 12) * 0.05;
}
py -= bounce;
drawJunimoSpirit(px, py, s.size, bounce, wobble, s.opacity, s);
}
// draw sparkles
for (var si = sparkles.length - 1; si >= 0; si--) {
var sp = sparkles[si];
sp.life++;
sp.x += sp.vx;
sp.y += sp.vy;
sp.vy += 0.08; // gravity
var sa = Math.max(0, 1 - sp.life / sp.maxLife);
// pixel sparkle — tiny colored square
jctx.fillStyle = sp.color + sa + ')';
jctx.fillRect(Math.round(sp.x), Math.round(sp.y), sp.size, sp.size);
// white highlight sparkle
if (sp.life < sp.maxLife * 0.5) {
jctx.fillStyle = 'rgba(255,255,255,' + (sa * 0.6) + ')';
jctx.fillRect(Math.round(sp.x), Math.round(sp.y), 1, 1);
}
if (sp.life > sp.maxLife) sparkles.splice(si, 1);
}
requestAnimationFrame(drawJunimos);
}
drawJunimos();
})();
</script>
</body>
</html>