const canvas = document.getElementById('kumonosu-rainCanvas');
const ctx = canvas.getContext('2d', {
willReadFrequently: true
});
const hitCanvas = document.createElement('canvas');
const hitCtx = hitCanvas.getContext('2d', {
willReadFrequently: true
});
const cloudCanvas = document.createElement('canvas');
const cloudCtx = cloudCanvas.getContext('2d');
let width, height;
let drops = [];
let splashes = [];
let cloudLayers = [];
let lightningBolts = [];
const dropCount = 1300;
const textString = "梅雨の大雨";
const fontFamily = "'Noto Sans JP'";
const RAIN_ANGLE = 5;
const rainAngleRad = (RAIN_ANGLE * Math.PI) / 180;
const rainVelocityX = Math.sin(rainAngleRad);
const rainVelocityY = Math.cos(rainAngleRad);
let isLightning = false;
let lightningTimer = 0;
let lightningIntensity = 0;
function resize() {
width = canvas.width = window.innerWidth;
height = canvas.height = window.innerHeight;
hitCanvas.width = width;
hitCanvas.height = height;
cloudCanvas.width = width;
cloudCanvas.height = Math.floor(height * 0.6);
drawCollisionMap();
initClouds();
}
function getResponsiveFontSize() {
return Math.min(Math.max(width * 0.16, 80), 300);
}
function drawCollisionMap() {
hitCtx.fillStyle = 'black';
hitCtx.fillRect(0, 0, width, height);
let fontSize = getResponsiveFontSize();
hitCtx.font = `900 ${fontSize}px ${fontFamily}`;
hitCtx.textAlign = 'center';
hitCtx.textBaseline = 'middle';
hitCtx.fillStyle = 'white';
hitCtx.fillText(textString, width / 2, height / 2);
}
function draw3DText() {
let fontSize = getResponsiveFontSize();
ctx.font = `900 ${fontSize}px ${fontFamily}`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const cx = width / 2;
const cy = height / 2;
ctx.shadowColor = 'black';
ctx.shadowBlur = 30;
ctx.shadowOffsetY = 25;
ctx.fillStyle = 'black';
ctx.fillText(textString, cx, cy);
ctx.shadowBlur = 0;
ctx.shadowOffsetY = 0;
ctx.fillStyle = '#111';
ctx.fillText(textString, cx + 4, cy + 4);
let gradient = ctx.createLinearGradient(0, cy - fontSize / 2, 0, cy + fontSize / 2);
gradient.addColorStop(0, '#050505');
gradient.addColorStop(0.3, '#1a1a1a');
gradient.addColorStop(0.5, '#080808');
gradient.addColorStop(1, '#000');
ctx.fillStyle = gradient;
ctx.fillText(textString, cx, cy);
ctx.save();
ctx.fillStyle = isLightning ? `rgba(255,255,255,${0.1 + lightningIntensity * 0.8})` : 'rgba(255,255,255,0.08)';
ctx.fillText(textString, cx, cy - 1);
ctx.restore();
}
function createCloudTexture(size, brightness) {
const tempCanvas = document.createElement('canvas');
tempCanvas.width = size;
tempCanvas.height = size;
const tempCtx = tempCanvas.getContext('2d');
const centerX = size / 2;
const centerY = size / 2;
for (let i = 0; i < 3; i++) {
const offsetX = (Math.random() - 0.5) * size * 0.3;
const offsetY = (Math.random() - 0.5) * size * 0.3;
const radius = size * (0.4 + Math.random() * 0.2);
const gradient = tempCtx.createRadialGradient(centerX + offsetX, centerY + offsetY, 0, centerX + offsetX, centerY + offsetY, radius);
const alpha = 0.12 + Math.random() * 0.1;
gradient.addColorStop(0, `rgba(${brightness + 20}, ${brightness + 20}, ${brightness + 30}, ${alpha})`);
gradient.addColorStop(0.5, `rgba(${brightness}, ${brightness}, ${brightness + 10}, ${alpha * 0.5})`);
gradient.addColorStop(1, `rgba(${brightness - 10}, ${brightness - 10}, ${brightness}, 0)`);
tempCtx.fillStyle = gradient;
tempCtx.fillRect(0, 0, size, size);
}
return tempCanvas;
}
class CloudLayer {
constructor(depth, yPos) {
this.depth = depth;
this.speed = (0.12 + depth * 0.2);
this.offset = 0;
this.y = yPos;
this.brightness = 20 + depth * 12;
this.cloudTextures = [];
const numClouds = 6;
for (let i = 0; i < numClouds; i++) {
const size = 300 + Math.random() * 500;
this.cloudTextures.push({
texture: createCloudTexture(size, this.brightness),
size: size,
x: (i / numClouds) * width * 1.5,
offsetY: (Math.random() - 0.5) * 100
});
}
}
update() {
this.offset += this.speed;
if (this.offset > width * 1.5) this.offset = 0;
}
draw() {
ctx.save();
let alpha = 0.6 + this.depth * 0.4;
if (isLightning && lightningIntensity > 0) alpha = Math.min(1, alpha + lightningIntensity * 0.4);
ctx.globalAlpha = alpha;
if (isLightning && lightningIntensity > 0) ctx.globalCompositeOperation = 'lighter';
for (let loop = 0; loop < 2; loop++) {
this.cloudTextures.forEach(cloud => {
const x = cloud.x - this.offset + (loop * width * 1.5);
const y = this.y + cloud.offsetY;
if (x + cloud.size > -cloud.size && x < width + cloud.size) ctx.drawImage(cloud.texture, x - cloud.size / 2, y - cloud.size / 2);
});
}
ctx.restore();
}
}
function initClouds() {
cloudLayers = [];
cloudLayers.push(new CloudLayer(0.3, height * 0.1));
cloudLayers.push(new CloudLayer(0.6, height * 0.2));
cloudLayers.push(new CloudLayer(1.0, height * 0.35));
}
class LightningBolt {
constructor() {
this.segments = [];
this.life = 1.0;
this.brightness = Math.random() * 0.5 + 0.5;
this.generateBolt();
}
generateBolt() {
const startX = Math.random() * width;
const startY = 0;
const endX = startX + (Math.random() - 0.5) * 500;
const endY = height * (Math.random() * 0.5 + 0.2);
this.segments = this.createBranch(startX, startY, endX, endY, 1);
if (Math.random() > 0.6) {
const idx = Math.floor(this.segments.length * 0.5);
const branchPoint = this.segments[idx];
const branchEnd = {
x: branchPoint.x + (Math.random() - 0.5) * 300,
y: branchPoint.y + Math.random() * 200 + 50
};
this.segments.push(...this.createBranch(branchPoint.x, branchPoint.y, branchEnd.x, branchEnd.y, 0.5));
}
}
createBranch(x1, y1, x2, y2, widthMult) {
const segments = [];
const steps = 12;
let currentX = x1,
currentY = y1;
for (let i = 0; i <= steps; i++) {
const t = i / steps;
const targetX = x1 + (x2 - x1) * t + (Math.random() - 0.5) * 80;
const targetY = y1 + (y2 - y1) * t;
segments.push({
x1: currentX,
y1: currentY,
x2: targetX,
y2: targetY,
width: (Math.random() * 2.5 + 1) * widthMult
});
currentX = targetX;
currentY = targetY;
}
return segments;
}
update() {
this.life -= 0.1;
}
draw() {
ctx.save();
ctx.globalAlpha = this.life * this.brightness;
ctx.lineCap = 'round';
ctx.shadowBlur = 25;
ctx.shadowColor = 'rgba(210, 230, 255, 0.9)';
this.segments.forEach(seg => {
ctx.strokeStyle = 'rgba(180, 210, 255, 0.2)';
ctx.lineWidth = seg.width * 12;
ctx.beginPath();
ctx.moveTo(seg.x1, seg.y1);
ctx.lineTo(seg.x2, seg.y2);
ctx.stroke();
ctx.strokeStyle = 'rgba(255, 255, 255, 1)';
ctx.lineWidth = seg.width * 2;
ctx.beginPath();
ctx.moveTo(seg.x1, seg.y1);
ctx.lineTo(seg.x2, seg.y2);
ctx.stroke();
});
ctx.restore();
}
}
class Splash {
constructor(x, y, impactAngle) {
this.x = x;
this.y = y;
const baseAngle = -Math.PI / 2 - rainAngleRad;
const spreadAngle = (Math.random() - 0.5) * Math.PI * 0.7;
const angle = baseAngle + spreadAngle;
const speed = Math.random() * 5 + 2;
this.vx = Math.cos(angle) * speed;
this.vy = Math.sin(angle) * speed;
this.life = 1.0;
this.size = Math.random() * 2 + 0.5;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.vy += 0.4;
this.vx *= 0.97;
this.life -= 0.05;
}
draw() {
ctx.fillStyle = `rgba(240, 245, 255, ${this.life})`;
ctx.fillRect(this.x, this.y, this.size, this.size);
}
}
class Drop {
constructor() {
this.reset();
this.y = Math.random() * height;
this.x = Math.random() * width + height * rainVelocityX;
}
reset() {
this.x = Math.random() * width - height * 0.3 * rainVelocityX;
this.y = -20;
const baseSpeed = Math.random() * 14 + 11;
this.vy = baseSpeed * rainVelocityY;
this.baseVx = baseSpeed * rainVelocityX;
this.vx = this.baseVx;
this.state = 'falling';
this.size = Math.random() * 1.4 + 1.2;
this.oscillationSpeed = Math.random() * 0.08 + 0.04;
this.phase = Math.random() * Math.PI * 2;
this.z = Math.random();
this.canCollide = (this.z > 0.46 && this.z < 0.54);
}
isSolid(tx, ty) {
if (ty < 0 || ty >= height || tx < 0 || tx >= width) return false;
if (ty > height / 2 - 200 && ty < height / 2 + 200) {
const pixel = hitCtx.getImageData(Math.floor(tx), Math.floor(ty), 1, 1).data;
return pixel[0] > 120;
}
return false;
}
update() {
let nextY = this.y + this.vy;
let nextX = this.x + this.vx;
if (this.canCollide) {
let isCurrentlyInSolid = this.isSolid(nextX, nextY);
let wasInSolid = this.isSolid(this.x, this.y);
if (this.state === 'falling' && isCurrentlyInSolid && !wasInSolid) {
this.state = 'flowing';
this.y = nextY;
this.vy = 0;
this.vx = 0;
for (let k = 0; k < 6; k++) splashes.push(new Splash(this.x, this.y, rainAngleRad));
} else if (this.state === 'flowing') {
if (isCurrentlyInSolid) {
if (Math.random() < 0.12) {
this.vy = 0.08;
} else {
if (this.vy < 4) this.vy += 0.25;
}
let noise = (Math.random() - 0.5) * 1.5;
this.vx = (Math.sin(this.y * this.oscillationSpeed + this.phase) * 0.4) + noise;
this.y += this.vy;
this.x += this.vx;
} else {
this.state = 'detaching';
this.vx = this.baseVx * 0.4;
this.vy = 1.5;
}
} else if (this.state === 'detaching') {
this.y += this.vy;
this.x += this.vx;
this.vy += 0.45;
this.vx += this.baseVx * 0.04;
if (this.vy > 10) {
this.state = 'falling';
this.vx = this.baseVx;
}
} else {
this.y = nextY;
this.x = nextX;
if (this.vy < 24 * rainVelocityY) this.vy += 0.5 * rainVelocityY;
}
} else {
this.y = nextY;
this.x = nextX;
if (this.vy < 24 * rainVelocityY) this.vy += 0.5 * rainVelocityY;
}
if (this.y > height || this.x > width + 150 || this.x < -150) this.reset();
}
draw() {
let opacity = this.z * 0.45;
if (isLightning) opacity = Math.min(0.9, opacity + lightningIntensity * 0.5);
if (this.state === 'falling') {
ctx.fillStyle = `rgba(180, 205, 230, ${opacity})`;
const speed = Math.sqrt(this.vx * this.vx + this.vy * this.vy);
const len = speed * (this.z + 1.1);
const angle = Math.atan2(this.vy, this.vx);
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(angle);
ctx.fillRect(0, -0.6, len, 1.2);
ctx.restore();
} else {
let stretch = this.vy * 16;
if (stretch < this.size) stretch = this.size;
let grad = ctx.createLinearGradient(this.x, this.y - stretch, this.x, this.y);
grad.addColorStop(0, `rgba(255, 255, 255, 0)`);
grad.addColorStop(1, `rgba(255, 255, 255, ${0.2 + (isLightning ? lightningIntensity * 0.6 : 0)})`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.moveTo(this.x - this.size / 2, this.y);
ctx.lineTo(this.x, this.y - stretch);
ctx.lineTo(this.x + this.size / 2, this.y);
ctx.fill();
ctx.beginPath();
ctx.fillStyle = `rgba(255, 255, 255, 0.12)`;
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fill();
}
}
}
function animate() {
if (!isLightning && Math.random() < 0.005) {
isLightning = true;
lightningTimer = Math.random() * 15 + 10;
lightningIntensity = 1.0;
lightningBolts.push(new LightningBolt());
if (Math.random() > 0.6) setTimeout(() => lightningBolts.push(new LightningBolt()), 100);
}
if (isLightning) {
lightningTimer--;
lightningIntensity = Math.max(0, lightningIntensity - 0.05);
if (lightningTimer <= 0) {
isLightning = false;
lightningIntensity = 0;
}
}
ctx.fillStyle = 'rgba(0, 0, 0, 0.45)';
ctx.fillRect(0, 0, width, height);
cloudLayers.forEach(layer => {
layer.update();
layer.draw();
});
for (let i = lightningBolts.length - 1; i >= 0; i--) {
lightningBolts[i].update();
lightningBolts[i].draw();
if (lightningBolts[i].life <= 0) lightningBolts.splice(i, 1);
}
draw3DText();
drops.forEach(drop => {
drop.update();
drop.draw();
});
for (let i = splashes.length - 1; i >= 0; i--) {
splashes[i].update();
splashes[i].draw();
if (splashes[i].life <= 0) splashes.splice(i, 1);
}
requestAnimationFrame(animate);
}
window.addEventListener('resize', resize);
document.fonts.ready.then(() => {
resize();
for (let i = 0; i < dropCount; i++) drops.push(new Drop());
animate();
});