fiiiirst
This commit is contained in:
parent
9b84ea95d6
commit
6d2537e3e3
53 changed files with 4719 additions and 2 deletions
644
js/render.js
Normal file
644
js/render.js
Normal file
|
|
@ -0,0 +1,644 @@
|
|||
"use strict";
|
||||
function randSeed(seed, min, max) {
|
||||
const s = Math.sin(seed * 12.9898) * 43758.5453;
|
||||
return min + (s - Math.floor(s)) * (max - min);
|
||||
}
|
||||
|
||||
function roundedBlob(ctx, x, y, w, h, wobble = 4) {
|
||||
ctx.beginPath();
|
||||
const steps = 28;
|
||||
for (let i = 0; i <= steps; i++) {
|
||||
const a = (i / steps) * Math.PI * 2;
|
||||
const rr = 1 + Math.sin(a * 3.1) * 0.025 + Math.cos(a * 5.7) * 0.018;
|
||||
const px = x + Math.cos(a) * w * rr + Math.sin(a * 2.0) * wobble * 0.08;
|
||||
const py = y + Math.sin(a) * h * rr + Math.cos(a * 1.7) * wobble * 0.08;
|
||||
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
|
||||
}
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function getLightingState(targetWorld) {
|
||||
const progress = targetWorld.dayProgress();
|
||||
const light = targetWorld.lightLevel();
|
||||
const dawnStrength = clamp(1 - Math.abs(progress - 0.22) / 0.14, 0, 1);
|
||||
const duskStrength = clamp(1 - Math.abs(progress - 0.58) / 0.16, 0, 1);
|
||||
const goldenStrength = Math.max(dawnStrength * 0.62, duskStrength);
|
||||
const nightStrength = clamp(1 - light * 1.62, 0, 0.54);
|
||||
const noonStrength = clamp((light - 0.72) / 0.28, 0, 1) * clamp(1 - goldenStrength * 0.72, 0, 1);
|
||||
const elevation = clamp(Math.sin(progress * Math.PI), 0.10, 1);
|
||||
const horizon = Math.cos(progress * Math.PI * 2 - Math.PI / 2);
|
||||
const shadowLength = lerp(1.58, 0.52, elevation);
|
||||
const warmth = clamp(0.28 + goldenStrength * 0.70 + noonStrength * 0.18 - nightStrength * 0.12, 0, 1);
|
||||
const bloom = CONFIG.lightingQuality === "cinematic" ? clamp(goldenStrength * 0.42 + nightStrength * 0.18 + noonStrength * 0.14, 0, 0.58) : 0;
|
||||
return {
|
||||
quality: CONFIG.lightingQuality,
|
||||
progress,
|
||||
light,
|
||||
dawnStrength,
|
||||
duskStrength,
|
||||
goldenStrength,
|
||||
nightStrength,
|
||||
noonStrength,
|
||||
elevation,
|
||||
warmth,
|
||||
bloom,
|
||||
sunDirX: -horizon,
|
||||
sunDirY: 0.42 + (1 - elevation) * 0.32,
|
||||
shadowLength,
|
||||
shadowColor: nightStrength > 0.22 ? "rgba(40, 50, 91, 0.36)" : (goldenStrength > 0.15 ? "rgba(116, 76, 48, 0.28)" : "rgba(83, 72, 54, 0.27)"),
|
||||
shadowAlpha: (nightStrength > 0.22 ? 0.135 : lerp(0.13, 0.22, 1 - elevation) + goldenStrength * 0.035),
|
||||
ambientTint: nightStrength > 0.2 ? "#253a8e" : (goldenStrength > 0.2 ? "#ffad6d" : "#fff7d6"),
|
||||
vignetteAlpha: nightStrength > 0.18 ? 0.13 + nightStrength * 0.13 : 0.06 + goldenStrength * 0.045,
|
||||
};
|
||||
}
|
||||
|
||||
function projectedShadowParams(lighting, scale = 1) {
|
||||
const edgeLight = clamp((1 - lighting.elevation) * 0.60 + lighting.goldenStrength * 0.40 + lighting.nightStrength * 0.55, 0, 1);
|
||||
const moonLit = lighting.nightStrength > 0.18;
|
||||
const lightEdge = moonLit
|
||||
? (lighting.progress < 0.5 ? 1 : -1)
|
||||
: (lighting.progress < 0.5 ? -1 : 1);
|
||||
const castDir = -lightEdge;
|
||||
const weather = world.weather || "sunny";
|
||||
const weatherAlpha = weather === "sunny" ? 1.12 : weather === "cloudy" ? 0.72 : 0.38;
|
||||
const length = lerp(0.62, 1.82, edgeLight) * scale;
|
||||
return {
|
||||
dx: castDir * 12 * length,
|
||||
dy: 5.2 * length,
|
||||
skew: castDir * lerp(0.08, 0.34, edgeLight),
|
||||
flatness: lerp(0.42, 0.24, edgeLight),
|
||||
spread: lerp(0.98, 1.52, edgeLight),
|
||||
alpha: clamp((0.16 + edgeLight * 0.09 - lighting.nightStrength * 0.04) * (0.92 + scale * 0.12) * weatherAlpha, 0.05, 0.32),
|
||||
color: lighting.nightStrength > 0.22 ? "rgba(42, 48, 72, 0.34)" : "rgba(78, 65, 43, 0.31)",
|
||||
};
|
||||
}
|
||||
|
||||
function imageVisibleBottomFromTop(img, top, drawH) {
|
||||
const b = imageAlphaBounds(img);
|
||||
if (!b) return top + drawH;
|
||||
return top + ((b.y + b.h) / b.imageH) * drawH;
|
||||
}
|
||||
|
||||
function drawImageProjectedShadow(ctx, img, x, groundY, drawW, drawH, params, options = {}) {
|
||||
if (!img) return false;
|
||||
const alpha = options.alpha ?? params.alpha;
|
||||
const rx = Math.max(5, drawW * (options.widthScale ?? 1) * 0.30);
|
||||
const ry = Math.max(2, drawH * (options.heightScale ?? 1) * 0.055);
|
||||
ctx.save();
|
||||
ctx.globalAlpha = clamp(alpha, 0.08, 0.22);
|
||||
ctx.fillStyle = params.color;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(Math.round(x), Math.round(groundY), rx, ry, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
return true;
|
||||
}
|
||||
|
||||
function drawProjectedShadow(ctx, x, y, rx, ry, params) {
|
||||
const pixelEllipse = (cx, cy, ew, eh, angle, steps = 10) => {
|
||||
const cos = Math.cos(angle);
|
||||
const sin = Math.sin(angle);
|
||||
ctx.beginPath();
|
||||
for (let i = 0; i < steps; i++) {
|
||||
const a = (i / steps) * Math.PI * 2;
|
||||
const px = Math.cos(a) * ew;
|
||||
const py = Math.sin(a) * eh;
|
||||
const sx = Math.round(cx + px * cos - py * sin);
|
||||
const sy = Math.round(cy + px * sin + py * cos);
|
||||
if (i === 0) ctx.moveTo(sx, sy);
|
||||
else ctx.lineTo(sx, sy);
|
||||
}
|
||||
ctx.closePath();
|
||||
};
|
||||
|
||||
ctx.save();
|
||||
ctx.translate(x, y);
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.fillStyle = params.color;
|
||||
const dx = Math.round(params.dx + rx * 0.10);
|
||||
const dy = Math.round(params.dy + ry * 0.10);
|
||||
const angle = Math.atan2(dy, dx) * 0.42;
|
||||
|
||||
ctx.globalAlpha = Math.max(0.04, params.alpha * 0.50);
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(Math.round(-rx * 0.28), Math.round(-ry * 0.50));
|
||||
ctx.lineTo(Math.round(dx + rx * 1.18), Math.round(dy - ry * 0.36));
|
||||
ctx.lineTo(Math.round(dx + rx * 1.42), Math.round(dy + ry * 0.42));
|
||||
ctx.lineTo(Math.round(-rx * 0.08), Math.round(ry * 0.72));
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
|
||||
ctx.globalAlpha = Math.max(0.055, params.alpha * 0.78);
|
||||
pixelEllipse(dx * 0.42, dy * 0.42, Math.max(3, rx * 0.90), Math.max(1.8, ry * 0.86), angle, 10);
|
||||
ctx.fill();
|
||||
|
||||
ctx.globalAlpha = Math.max(0.07, params.alpha * 1.12);
|
||||
pixelEllipse(0, 0, Math.max(3, rx * 0.76), Math.max(1.6, ry * 0.72), 0, 10);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawRadialGlow(ctx, x, y, radius, color, alpha) {
|
||||
if (alpha <= 0 || radius <= 0) return;
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = "lighter";
|
||||
const g = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
||||
g.addColorStop(0, color.replace("ALPHA", alpha.toFixed(3)));
|
||||
g.addColorStop(0.52, color.replace("ALPHA", (alpha * 0.34).toFixed(3)));
|
||||
g.addColorStop(1, color.replace("ALPHA", "0"));
|
||||
ctx.fillStyle = g;
|
||||
ctx.beginPath();
|
||||
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawItemGlow(ctx, item, lighting, visibleAlpha = 1) {
|
||||
if (lighting.quality !== "cinematic") return;
|
||||
const nightBoost = lighting.nightStrength;
|
||||
const goldenBoost = lighting.goldenStrength;
|
||||
if (item.type === "food" || item.type === "sweet") {
|
||||
const sweet = item.type === "sweet";
|
||||
drawRadialGlow(ctx, item.x, item.y, item.r * (2.35 + goldenBoost * 0.55), sweet ? "rgba(128, 190, 88, ALPHA)" : "rgba(255, 218, 120, ALPHA)", visibleAlpha * (0.045 + goldenBoost * 0.045 + nightBoost * 0.055));
|
||||
} else if (item.type === "water") {
|
||||
drawRadialGlow(ctx, item.x, item.y, item.r * (2.5 + nightBoost * 0.7), "rgba(116, 183, 255, ALPHA)", visibleAlpha * (0.045 + nightBoost * 0.09));
|
||||
} else if (item.type === "grass" && goldenBoost > 0.35) {
|
||||
drawRadialGlow(ctx, item.x, item.y - item.r * 0.2, item.r * 2.1, "rgba(183, 255, 99, ALPHA)", visibleAlpha * goldenBoost * 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
function drawCreatureGlow(ctx, tarinai, drawW, drawH, lighting) {
|
||||
if (lighting.quality !== "cinematic" || tarinai.dead) return;
|
||||
const selected = tarinai.world.selected === tarinai;
|
||||
const happy = (tarinai.goodMode === "smile" || tarinai.state === "cursor_friend") && tarinai.mood > 58 && tarinai.stress < 44 && tarinai.state !== "sleep";
|
||||
const nightAlpha = lighting.nightStrength * (happy ? 0.10 : 0.032);
|
||||
const selectedAlpha = selected ? 0.13 + lighting.bloom * 0.055 : 0;
|
||||
const mealAlpha = tarinai.eatTimer > 0.02 ? tarinai.eatTimer * 0.12 : 0;
|
||||
const alpha = clamp(nightAlpha + selectedAlpha + mealAlpha + lighting.goldenStrength * (happy ? 0.04 : 0.012), 0, 0.22);
|
||||
if (alpha <= 0.01) return;
|
||||
const color = selected ? "rgba(178, 236, 96, ALPHA)" : (lighting.nightStrength > 0.22 ? "rgba(180, 213, 255, ALPHA)" : "rgba(255, 222, 132, ALPHA)");
|
||||
drawRadialGlow(ctx, tarinai.x, tarinai.y - drawH * 0.10, Math.max(drawW, drawH) * (1.0 + lighting.bloom * 0.34), color, alpha);
|
||||
}
|
||||
|
||||
function drawDawnRimLight(ctx, drawW, drawH, lighting) {
|
||||
const alpha = clamp(lighting.dawnStrength * (0.10 + lighting.nightStrength * 0.08), 0, 0.16);
|
||||
if (alpha <= 0.01) return;
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = "screen";
|
||||
const g = ctx.createLinearGradient(-drawW * 0.55, -drawH * 0.55, drawW * 0.18, drawH * 0.16);
|
||||
g.addColorStop(0, `rgba(255, 229, 170, ${alpha})`);
|
||||
g.addColorStop(0.46, `rgba(255, 213, 142, ${alpha * 0.30})`);
|
||||
g.addColorStop(1, "rgba(255, 213, 142, 0)");
|
||||
ctx.fillStyle = g;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(-drawW * 0.06, -drawH * 0.06, drawW * 0.60, drawH * 0.42, -0.18, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Cached static garden layer: expensive soil/decor loops are rendered only when
|
||||
// canvas size, coarse light phase, or weather changes.
|
||||
let activeCtx = null;
|
||||
|
||||
const backgroundCache = {
|
||||
canvas: null,
|
||||
ctx: null,
|
||||
w: 0,
|
||||
h: 0,
|
||||
key: "",
|
||||
};
|
||||
|
||||
function backgroundCacheKey(world, lighting) {
|
||||
const weather = world.weather || "sunny";
|
||||
return `${weather}:${lighting.progress.toFixed(3)}:${lighting.light.toFixed(3)}:${lighting.goldenStrength.toFixed(3)}:${lighting.nightStrength.toFixed(3)}`;
|
||||
}
|
||||
|
||||
function ensureBackgroundCache(w, h, lighting) {
|
||||
const key = backgroundCacheKey(world, lighting);
|
||||
if (!backgroundCache.canvas) {
|
||||
backgroundCache.canvas = document.createElement("canvas");
|
||||
backgroundCache.ctx = backgroundCache.canvas.getContext("2d");
|
||||
}
|
||||
if (backgroundCache.w === w && backgroundCache.h === h && backgroundCache.key === key) {
|
||||
return backgroundCache.canvas;
|
||||
}
|
||||
|
||||
backgroundCache.w = w;
|
||||
backgroundCache.h = h;
|
||||
backgroundCache.key = key;
|
||||
backgroundCache.canvas.width = Math.max(1, Math.floor(w));
|
||||
backgroundCache.canvas.height = Math.max(1, Math.floor(h));
|
||||
|
||||
const mainCtx = activeCtx;
|
||||
try {
|
||||
activeCtx = backgroundCache.ctx;
|
||||
drawGardenBackdrop(w, h, lighting);
|
||||
drawGardenBed(w, h, lighting);
|
||||
return backgroundCache.canvas;
|
||||
} catch (err) {
|
||||
console.error("background cache redraw failed", err);
|
||||
backgroundCache.key = "";
|
||||
return null;
|
||||
} finally {
|
||||
activeCtx = mainCtx;
|
||||
}
|
||||
}
|
||||
|
||||
function drawGardenBackdrop(w, h, lighting) {
|
||||
const light = lighting.light;
|
||||
const sky = activeCtx.createLinearGradient(0, 0, 0, h);
|
||||
const topColor = light > 0.5 ? `rgba(${Math.round(142 + light * 62 + lighting.goldenStrength * 24)}, ${Math.round(198 + light * 34 + lighting.goldenStrength * 10)}, ${Math.round(238 + light * 15 - lighting.goldenStrength * 24)}, 1)` : `rgba(${Math.round(42 + light * 78)}, ${Math.round(55 + light * 98)}, ${Math.round(95 + light * 124)}, 1)`;
|
||||
const midColor = light > 0.5 ? `rgba(${Math.round(214 + light * 28 + lighting.goldenStrength * 24)}, ${Math.round(235 + light * 12 + lighting.goldenStrength * 4)}, ${Math.round(246 - light * 8 - lighting.goldenStrength * 30)}, 1)` : `rgba(${Math.round(78 + light * 90)}, ${Math.round(88 + light * 108)}, ${Math.round(128 + light * 104)}, 1)`;
|
||||
const bottomColor = light > 0.5 ? `rgba(${Math.round(230 + lighting.goldenStrength * 18)}, ${Math.round(226 - lighting.goldenStrength * 10)}, ${Math.round(202 - lighting.goldenStrength * 22)}, 1)` : "rgba(74, 84, 119, 1)";
|
||||
sky.addColorStop(0, topColor);
|
||||
sky.addColorStop(0.48, midColor);
|
||||
sky.addColorStop(1, bottomColor);
|
||||
activeCtx.fillStyle = sky;
|
||||
activeCtx.fillRect(0, 0, w, h);
|
||||
|
||||
for (let i = 0; i < 2; i++) {
|
||||
const cx = randSeed(300 + i, 80, w - 80);
|
||||
const cy = randSeed(420 + i, 28, h * 0.24);
|
||||
const alpha = light > 0.4 ? 0.09 + lighting.noonStrength * 0.04 : 0.04;
|
||||
activeCtx.save();
|
||||
activeCtx.fillStyle = `rgba(255,255,255,${alpha})`;
|
||||
for (let j = 0; j < 3; j++) {
|
||||
activeCtx.beginPath();
|
||||
activeCtx.ellipse(cx + j * 22, cy + Math.sin(j) * 5, 34, 18, 0, 0, Math.PI * 2);
|
||||
activeCtx.fill();
|
||||
}
|
||||
activeCtx.restore();
|
||||
}
|
||||
|
||||
if (light < 0.22) {
|
||||
activeCtx.save();
|
||||
activeCtx.fillStyle = `rgba(255,255,255,${0.62 + lighting.nightStrength * 0.30})`;
|
||||
for (let i = 0; i < 26; i++) {
|
||||
const sx = randSeed(i + Math.floor(world.time * 0.1), 40, w - 40);
|
||||
const sy = randSeed(i + Math.floor(world.time * 0.3) + 12, 26, h * 0.34);
|
||||
const size = randSeed(i + 910, 1.2, 2.4);
|
||||
activeCtx.fillRect(sx, sy, size, size);
|
||||
}
|
||||
activeCtx.restore();
|
||||
}
|
||||
|
||||
if (lighting.bloom > 0.08) {
|
||||
const sunX = w * (0.15 + (1 - lighting.progress) * 0.70);
|
||||
const sunY = h * (0.12 + (1 - lighting.elevation) * 0.22);
|
||||
drawRadialGlow(activeCtx, sunX, sunY, Math.max(w, h) * (0.24 + lighting.goldenStrength * 0.10), "rgba(255, 220, 138, ALPHA)", lighting.bloom * 0.10);
|
||||
}
|
||||
|
||||
if (world.weather === "cloudy") {
|
||||
activeCtx.fillStyle = "rgba(218, 223, 214, 0.16)";
|
||||
activeCtx.fillRect(0, 0, w, h);
|
||||
} else if (world.weather === "light_rain") {
|
||||
activeCtx.fillStyle = "rgba(178, 197, 208, 0.22)";
|
||||
activeCtx.fillRect(0, 0, w, h);
|
||||
}
|
||||
}
|
||||
|
||||
function drawGardenBed(w, h, lighting) {
|
||||
const light = lighting.light;
|
||||
const x = 18, y = 22, bw = w - 36, bh = h - 44;
|
||||
const border = activeCtx.createLinearGradient(0, y, 0, y + bh);
|
||||
border.addColorStop(0, light > 0.42 ? (lighting.goldenStrength > 0.25 ? "#bf8f5e" : "#a88c66") : "#6f6c72");
|
||||
border.addColorStop(1, light > 0.42 ? (lighting.goldenStrength > 0.25 ? "#9c7043" : "#8a6f4b") : "#565a68");
|
||||
activeCtx.fillStyle = border;
|
||||
roundedRect(activeCtx, x, y, bw, bh, 34);
|
||||
activeCtx.fill();
|
||||
|
||||
activeCtx.save();
|
||||
activeCtx.globalAlpha = 0.14;
|
||||
activeCtx.strokeStyle = "rgba(255,255,255,0.85)";
|
||||
activeCtx.lineWidth = 2;
|
||||
roundedRect(activeCtx, x + 6, y + 6, bw - 12, bh - 12, 28);
|
||||
activeCtx.stroke();
|
||||
activeCtx.restore();
|
||||
|
||||
const soil = activeCtx.createLinearGradient(0, y + 14, 0, y + bh - 14);
|
||||
soil.addColorStop(0, light > 0.42 ? (lighting.goldenStrength > 0.25 ? "#f1dfbc" : "#ece6d2") : "#bbbcc6");
|
||||
soil.addColorStop(0.38, light > 0.42 ? (lighting.goldenStrength > 0.25 ? "#e5d4b5" : "#dfdccb") : "#afb1bd");
|
||||
soil.addColorStop(1, light > 0.42 ? (lighting.goldenStrength > 0.25 ? "#d3c5a5" : "#d6d2bf") : "#a0a3af");
|
||||
activeCtx.fillStyle = soil;
|
||||
roundedRect(activeCtx, x + 16, y + 14, bw - 32, bh - 30, 28);
|
||||
activeCtx.fill();
|
||||
|
||||
activeCtx.save();
|
||||
activeCtx.beginPath();
|
||||
roundedRect(activeCtx, x + 16, y + 14, bw - 32, bh - 30, 28);
|
||||
activeCtx.clip();
|
||||
|
||||
for (let i = 0; i < 46; i++) {
|
||||
const px = randSeed(900 + i, x + 24, x + bw - 24);
|
||||
const py = randSeed(1200 + i, y + 22, y + bh - 24);
|
||||
const r = randSeed(1600 + i, 1.2, 3.8);
|
||||
const a = randSeed(1900 + i, 0.03, 0.09) * (light > 0.4 ? 1.05 + lighting.goldenStrength * 0.35 : 0.70);
|
||||
activeCtx.fillStyle = `rgba(${Math.round(randSeed(2000 + i, 166, 196))}, ${Math.round(randSeed(2300 + i, 156, 182))}, ${Math.round(randSeed(2600 + i, 128, 150))}, ${a})`;
|
||||
activeCtx.beginPath();
|
||||
activeCtx.ellipse(px, py, r * 1.5, r, randSeed(3000 + i, -0.8, 0.8), 0, Math.PI * 2);
|
||||
activeCtx.fill();
|
||||
}
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const px = randSeed(3300 + i, x + 40, x + bw - 40);
|
||||
const py = randSeed(3600 + i, y + 44, y + bh - 46);
|
||||
const rw = randSeed(3900 + i, 24, 64);
|
||||
const rh = randSeed(4200 + i, 12, 28);
|
||||
activeCtx.fillStyle = light > 0.42 ? `rgba(161, 186, 118, ${0.07 + lighting.goldenStrength * 0.05})` : "rgba(118, 141, 132, 0.06)";
|
||||
activeCtx.beginPath();
|
||||
activeCtx.ellipse(px, py, rw, rh, randSeed(4500 + i, -1, 1), 0, Math.PI * 2);
|
||||
activeCtx.fill();
|
||||
}
|
||||
|
||||
// edge grasses and tiny flowers
|
||||
for (let i = 0; i < 14; i++) {
|
||||
const side = i % 2;
|
||||
const px = side === 0 ? randSeed(5000 + i, x + 36, x + bw - 36) : randSeed(5400 + i, x + 30, x + bw - 30);
|
||||
const py = side === 0 ? y + 20 + randSeed(5700 + i, -4, 8) : y + bh - 24 + randSeed(6000 + i, -8, 4);
|
||||
activeCtx.save();
|
||||
activeCtx.translate(px, py);
|
||||
activeCtx.strokeStyle = light > 0.34 ? `rgba(101, 157, 68, ${0.38 + lighting.goldenStrength * 0.16})` : "rgba(97, 126, 128, 0.25)";
|
||||
activeCtx.lineWidth = 1.4;
|
||||
for (let j = 0; j < 3; j++) {
|
||||
const len = randSeed(6300 + i * 5 + j, 7, 14);
|
||||
const dir = randSeed(6600 + i * 5 + j, -0.65, 0.65);
|
||||
activeCtx.beginPath();
|
||||
activeCtx.moveTo(0, 0);
|
||||
activeCtx.quadraticCurveTo(Math.sin(dir) * len * 0.35, -len * 0.45, Math.sin(dir) * len, -len);
|
||||
activeCtx.stroke();
|
||||
}
|
||||
activeCtx.restore();
|
||||
}
|
||||
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const px = randSeed(7000 + i, x + 44, x + bw - 44);
|
||||
const py = randSeed(7400 + i, y + 46, y + bh - 44);
|
||||
if (i % 3 === 0) {
|
||||
activeCtx.save();
|
||||
activeCtx.translate(px, py);
|
||||
activeCtx.globalAlpha = 0.30;
|
||||
for (let p = 0; p < 4; p++) {
|
||||
activeCtx.fillStyle = p === 3 ? "rgba(248, 204, 82, 0.75)" : "rgba(255,255,255,0.80)";
|
||||
activeCtx.beginPath();
|
||||
if (p === 3) activeCtx.arc(0, 0, 1.6, 0, Math.PI * 2);
|
||||
else activeCtx.ellipse(Math.cos((p / 3) * Math.PI * 2) * 2.2, Math.sin((p / 3) * Math.PI * 2) * 2.2, 1.8, 1.3, 0, 0, Math.PI * 2);
|
||||
activeCtx.fill();
|
||||
}
|
||||
activeCtx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const px = randSeed(7800 + i, x + 36, x + bw - 36);
|
||||
const py = randSeed(8200 + i, y + 34, y + bh - 34);
|
||||
if (i % 2 === 0) {
|
||||
activeCtx.save();
|
||||
activeCtx.translate(px, py);
|
||||
activeCtx.rotate(randSeed(8500 + i, -1.3, 1.3));
|
||||
activeCtx.fillStyle = light > 0.45 ? `rgba(160, 179, 126, ${0.22 + lighting.goldenStrength * 0.12})` : "rgba(126, 144, 139, 0.16)";
|
||||
activeCtx.beginPath();
|
||||
activeCtx.ellipse(-4, 0, 4.8, 2.2, -0.4, 0, Math.PI * 2);
|
||||
activeCtx.ellipse(4, 0, 4.8, 2.2, 0.4, 0, Math.PI * 2);
|
||||
activeCtx.ellipse(0, -4, 4.8, 2.2, 0, 0, Math.PI * 2);
|
||||
activeCtx.fill();
|
||||
activeCtx.restore();
|
||||
} else {
|
||||
activeCtx.save();
|
||||
activeCtx.translate(px, py);
|
||||
activeCtx.rotate(randSeed(8800 + i, -1.1, 1.1));
|
||||
activeCtx.fillStyle = "rgba(158, 140, 108, 0.22)";
|
||||
activeCtx.beginPath();
|
||||
activeCtx.ellipse(0, 0, randSeed(9000 + i, 5, 9), randSeed(9300 + i, 2.2, 3.6), 0, 0, Math.PI * 2);
|
||||
activeCtx.fill();
|
||||
activeCtx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
activeCtx.restore();
|
||||
|
||||
// subtle vignette
|
||||
const vignette = activeCtx.createRadialGradient(w * 0.5, h * 0.5, Math.min(w, h) * 0.22, w * 0.5, h * 0.5, Math.max(w, h) * 0.66);
|
||||
vignette.addColorStop(0, "rgba(0,0,0,0)");
|
||||
vignette.addColorStop(1, light > 0.4 ? `rgba(94, 70, 48, ${0.09 + lighting.goldenStrength * 0.06})` : `rgba(16, 22, 52, ${0.17 + lighting.nightStrength * 0.14})`);
|
||||
activeCtx.fillStyle = vignette;
|
||||
activeCtx.fillRect(0, 0, w, h);
|
||||
}
|
||||
|
||||
function drawLightRays(ctx, w, h, lighting) {
|
||||
if (lighting.quality !== "cinematic" || lighting.goldenStrength <= 0.08) return;
|
||||
ctx.save();
|
||||
ctx.globalCompositeOperation = "screen";
|
||||
const rayAlpha = lighting.goldenStrength * LIGHTING_TUNING.rayAlpha;
|
||||
const originX = w * (lighting.dawnStrength > lighting.duskStrength ? 0.08 : 0.90);
|
||||
const originY = h * (0.02 + (1 - lighting.elevation) * 0.18);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const spread = (i - 1) * 0.20;
|
||||
const g = ctx.createLinearGradient(originX, originY, w * (0.50 + spread), h * (0.58 + i * 0.06));
|
||||
g.addColorStop(0, `rgba(255, 230, 170, ${rayAlpha * (1 - i * 0.10)})`);
|
||||
g.addColorStop(0.30, `rgba(255, 198, 120, ${rayAlpha * 0.18})`);
|
||||
g.addColorStop(1, "rgba(255, 188, 108, 0)");
|
||||
ctx.fillStyle = g;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(originX, originY);
|
||||
ctx.lineTo(w * (0.10 + i * 0.28), h);
|
||||
ctx.lineTo(w * (0.42 + i * 0.28), h);
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawAtmosphericLighting(ctx, w, h, lighting) {
|
||||
ctx.save();
|
||||
if (lighting.nightStrength > 0) {
|
||||
ctx.globalAlpha = lighting.nightStrength * LIGHTING_TUNING.nightOverlayAlpha;
|
||||
ctx.fillStyle = "#223683";
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
if (lighting.goldenStrength > 0) {
|
||||
ctx.globalAlpha = lighting.goldenStrength * LIGHTING_TUNING.goldenOverlayAlpha;
|
||||
ctx.fillStyle = "#ffad68";
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
if (lighting.noonStrength > 0) {
|
||||
ctx.globalAlpha = lighting.noonStrength * 0.035;
|
||||
ctx.fillStyle = "#fff8cf";
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
ctx.restore();
|
||||
|
||||
if (lighting.quality === "cinematic") {
|
||||
drawRadialGlow(ctx, w * 0.5, h * 0.46, Math.max(w, h) * 0.50, lighting.nightStrength > 0.18 ? "rgba(91, 121, 255, ALPHA)" : "rgba(255, 236, 175, ALPHA)", lighting.bloom * LIGHTING_TUNING.bloomWashAlpha);
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
const vignette = ctx.createRadialGradient(w * 0.5, h * 0.46, Math.min(w, h) * 0.18, w * 0.5, h * 0.48, Math.max(w, h) * 0.76);
|
||||
vignette.addColorStop(0, "rgba(0,0,0,0)");
|
||||
vignette.addColorStop(0.72, "rgba(0,0,0,0)");
|
||||
vignette.addColorStop(1, `rgba(13, 17, 34, ${lighting.vignetteAlpha})`);
|
||||
ctx.fillStyle = vignette;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawRain(ctx, w, h, t) {
|
||||
if (world.weather !== "light_rain") return;
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.28;
|
||||
ctx.strokeStyle = "rgba(197, 220, 230, 0.70)";
|
||||
ctx.lineWidth = 1;
|
||||
for (let i = 0; i < 34; i++) {
|
||||
const x = (randSeed(18000 + i, 0, w) + t * 26) % w;
|
||||
const y = (randSeed(18400 + i, 0, h) + t * 72) % h;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, y);
|
||||
ctx.lineTo(x - 4, y + 10);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function weatherLabel(weather) {
|
||||
return { sunny: "\u6674\u308c", cloudy: "\u66c7\u308a", light_rain: "\u5c0f\u96e8" }[weather] || "\u6674\u308c";
|
||||
}
|
||||
|
||||
function render() {
|
||||
activeCtx = ctx;
|
||||
const w = world.w, h = world.h;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
|
||||
const lighting = getLightingState(world);
|
||||
const light = lighting.light;
|
||||
const cachedBackground = ensureBackgroundCache(w, h, lighting);
|
||||
if (cachedBackground) {
|
||||
ctx.drawImage(cachedBackground, 0, 0, w, h);
|
||||
} else {
|
||||
drawGardenBackdrop(w, h, lighting);
|
||||
drawGardenBed(w, h, lighting);
|
||||
}
|
||||
drawLightRays(ctx, w, h, lighting);
|
||||
drawRain(ctx, w, h, world.time);
|
||||
|
||||
for (const it of world.items) {
|
||||
if (it.type !== "zunchi") it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
for (const it of world.items) {
|
||||
if (it.type === "zunchi") it.draw(ctx, world.time, lighting);
|
||||
}
|
||||
|
||||
ctx.save();
|
||||
ctx.lineWidth = 1.15;
|
||||
ctx.setLineDash([4, 4]);
|
||||
ctx.strokeStyle = light > 0.42 ? "rgba(115, 151, 86, 0.32)" : "rgba(202, 220, 255, 0.22)";
|
||||
for (const t of world.tarinai) {
|
||||
const p = t.parentToFollow?.();
|
||||
if (!p || !t.juvenile || t.dead || p.dead || dist(t, p) > 96) continue;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(t.x, t.y);
|
||||
ctx.quadraticCurveTo((t.x + p.x) / 2, Math.min(t.y, p.y) - 10, p.x, p.y);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.setLineDash([]);
|
||||
ctx.restore();
|
||||
|
||||
const sorted = world.drawList || world.tarinai;
|
||||
sorted.length = 0;
|
||||
for (const t of world.tarinai) sorted.push(t);
|
||||
sorted.sort((a, b) => a.y - b.y);
|
||||
for (const t of sorted) t.draw(ctx, world.time, lighting);
|
||||
for (const ef of world.effects) ef.draw(ctx);
|
||||
|
||||
// Light affecting the whole scene, including characters and ground.
|
||||
drawAtmosphericLighting(ctx, w, h, lighting);
|
||||
|
||||
const pressure = clamp((world.tarinai.length - 44) / 24, 0, 1);
|
||||
if (pressure > 0) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = pressure * 0.06;
|
||||
ctx.fillStyle = "#b64545";
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Time HUD
|
||||
ctx.save();
|
||||
const hudW = 150;
|
||||
const hudH = 56;
|
||||
const hudX = w - hudW - 22;
|
||||
const hudY = 16;
|
||||
ctx.fillStyle = light > 0.42 ? "rgba(255,251,244,0.76)" : "rgba(247,246,255,0.62)";
|
||||
roundedRect(ctx, hudX, hudY, hudW, hudH, 18);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = light > 0.42 ? "rgba(118,103,83,0.12)" : "rgba(206,213,242,0.15)";
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "#2a241d";
|
||||
ctx.font = "bold 16px ui-rounded, sans-serif";
|
||||
ctx.textAlign = "left";
|
||||
ctx.fillText(`${world.day}\u65e5\u76ee`, hudX + 24, hudY + 25);
|
||||
ctx.font = "12px ui-rounded, sans-serif";
|
||||
ctx.fillStyle = "rgba(111,101,88,0.86)";
|
||||
ctx.fillText(`${world.phaseName()} / ${world.clockString()} / ${weatherLabel(world.weather)}`, hudX + 24, hudY + 45);
|
||||
ctx.restore();
|
||||
|
||||
drawSelectedCard(ctx, world, lighting);
|
||||
|
||||
|
||||
ctx.save();
|
||||
ctx.textAlign = "right";
|
||||
ctx.font = "10px ui-rounded, sans-serif";
|
||||
ctx.fillStyle = light > 0.42 ? "rgba(84,72,55,0.42)" : "rgba(230,235,255,0.48)";
|
||||
ctx.fillText(`${window.__tarinaiFps || 0} fps`, w - 14, h - 12);
|
||||
ctx.restore();
|
||||
|
||||
|
||||
if (world.paused) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = "rgba(255,255,255,0.50)";
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
ctx.fillStyle = "rgba(42,36,29,0.70)";
|
||||
ctx.textAlign = "center";
|
||||
ctx.font = "bold 28px ui-rounded, sans-serif";
|
||||
ctx.fillText("\u4e00\u6642\u505c\u6b62\u4e2d", w / 2, h / 2);
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
function drawSelectedCard(ctx, world, lighting) {
|
||||
const t = world.selected;
|
||||
if (!t || t.dead || !world.tarinai.includes(t)) return;
|
||||
const w = world.w, h = world.h;
|
||||
const cardW = 152;
|
||||
const cardH = 66;
|
||||
let x = t.x + 18;
|
||||
let y = t.y - 86;
|
||||
x = clamp(x, 10, w - cardW - 10);
|
||||
y = clamp(y, 10, h - cardH - 10);
|
||||
ctx.save();
|
||||
ctx.fillStyle = lighting.light > 0.42 ? "rgba(255,250,239,0.82)" : "rgba(239,243,255,0.74)";
|
||||
roundedRect(ctx, x, y, cardW, cardH, 14);
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = "rgba(84,68,48,0.12)";
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "rgba(42,36,29,0.86)";
|
||||
ctx.font = "bold 11px ui-rounded, sans-serif";
|
||||
ctx.fillText(t.name, x + 10, y + 18);
|
||||
ctx.font = "10px ui-rounded, sans-serif";
|
||||
const child = t.juvenile && t.parentToFollow?.() ? " / \u5b50" : "";
|
||||
const zHint = t.digest > 4.9 ? " / \u305a\u3093\u3061" : "";
|
||||
ctx.fillText(`${stateLabel(t.state)}${child}${zHint}`, x + 10, y + 35);
|
||||
ctx.fillText(`\u6c17\u5206 ${fmt(t.mood)} \u4e16\u4ee3 ${t.generation} \u3064\u3064\u304d ${t.pokeCount}/9`, x + 10, y + 52);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function roundedRect(ctx, x, y, w, h, r) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.arcTo(x + w, y, x + w, y + h, r);
|
||||
ctx.arcTo(x + w, y + h, x, y + h, r);
|
||||
ctx.arcTo(x, y + h, x, y, r);
|
||||
ctx.arcTo(x, y, x + w, y, r);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
|
||||
window.TarinaiRender = { randSeed, roundedBlob, getLightingState, projectedShadowParams, imageVisibleBottomFromTop, drawImageProjectedShadow, drawProjectedShadow, drawRadialGlow, drawItemGlow, drawCreatureGlow, drawDawnRimLight, drawGardenBackdrop, drawGardenBed, drawLightRays, drawAtmosphericLighting, drawRain, drawSelectedCard, render, roundedRect };
|
||||
Loading…
Add table
Add a link
Reference in a new issue