Enhance underwater animation: add new creature types, improve movement animations, and implement light rays effect

This commit is contained in:
CodeDevMLH
2026-02-26 21:52:36 +01:00
parent c6d04b9b3b
commit a162b30bcd
2 changed files with 277 additions and 74 deletions

View File

@@ -62,16 +62,29 @@
z-index: 40; z-index: 40;
} }
@keyframes underwater-swim-right { @keyframes underwater-traverse-right {
0% { transform: translateX(0) translateY(0) scaleX(-1); } 0% { left: -25vw; }
50% { transform: translateX(65vw) translateY(-5vh) scaleX(-1); } 100% { left: 125vw; }
100% { transform: translateX(130vw) translateY(0) scaleX(-1); }
} }
@keyframes underwater-swim-left { @keyframes underwater-traverse-left {
0% { transform: translateX(0) translateY(0); } 0% { left: 125vw; }
50% { transform: translateX(-65vw) translateY(5vh); } 100% { left: -25vw; }
100% { transform: translateX(-130vw) translateY(0); } }
@keyframes underwater-traverse-up {
0% { top: 120vh; }
100% { top: -20vh; }
}
@keyframes underwater-traverse-down {
0% { top: -20vh; }
100% { top: 120vh; }
}
@keyframes underwater-sway-y {
0% { transform: translateY(-2vh); }
100% { transform: translateY(2vh); }
} }
@keyframes underwater-sway { @keyframes underwater-sway {
@@ -86,3 +99,30 @@
90% { opacity: 0; } 90% { opacity: 0; }
100% { transform: translateY(-110vh) translateX(10px); opacity: 0; } 100% { transform: translateY(-110vh) translateX(10px); opacity: 0; }
} }
.underwater-god-rays {
position: absolute;
top: -50vh;
left: -50vw;
width: 200vw;
height: 200vh;
background: repeating-linear-gradient(
15deg,
rgba(255, 255, 255, 0.02) 0px,
rgba(255, 255, 255, 0.05) 100px,
transparent 100px,
transparent 300px
);
animation: god-rays-sway 20s ease-in-out infinite alternate;
pointer-events: none;
z-index: 12;
transform-origin: top center;
mix-blend-mode: overlay;
filter: blur(5px);
}
@keyframes god-rays-sway {
0% { transform: rotate(-3deg) translateX(-5%); opacity: 0.4; }
50% { opacity: 0.8; }
100% { transform: rotate(3deg) translateX(5%); opacity: 0.4; }
}

View File

@@ -5,6 +5,78 @@ const symbolCount = config.SymbolCount || 15;
const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true; const useRandomSymbols = config.EnableRandomSymbols !== undefined ? config.EnableRandomSymbols : true;
const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false; const enableRandomMobile = config.EnableRandomSymbolsMobile !== undefined ? config.EnableRandomSymbolsMobile : false;
const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true; const enableDifferentDuration = config.EnableDifferentDuration !== undefined ? config.EnableDifferentDuration : true;
const enableLightRays = config.EnableLightRays !== undefined ? config.EnableLightRays : true;
const seaweedCount = config.SeaweedCount !== undefined ? config.SeaweedCount : 50;
// Entity counts configured
const fishCount = config.FishCount !== undefined ? config.FishCount : 15;
const seahorseCount = config.SeahorseCount !== undefined ? config.SeahorseCount : 3;
const jellyfishCount = config.JellyfishCount !== undefined ? config.JellyfishCount : 3;
const turtleCount = config.TurtleCount !== undefined ? config.TurtleCount : 1;
const crabCount = config.CrabCount !== undefined ? config.CrabCount : 2;
const starfishCount = config.StarfishCount !== undefined ? config.StarfishCount : 2;
const shellCount = config.ShellCount !== undefined ? config.ShellCount : 2;
const seaweeds = [
"../Seasonals/Resources/underwater_assets/seaweed_1.gif",
"../Seasonals/Resources/underwater_assets/seaweed_2.gif"
];
// Statics for bottom
const crabImages = [
"../Seasonals/Resources/underwater_assets/crab_1.gif",
"../Seasonals/Resources/underwater_assets/crab_2.gif",
"../Seasonals/Resources/underwater_assets/crab_3.gif"
];
const starfishImages = [
"../Seasonals/Resources/underwater_assets/starfish_1.gif",
"../Seasonals/Resources/underwater_assets/starfish_2.gif"
];
const shellImages = [
"../Seasonals/Resources/underwater_assets/shell_1.gif"
];
const fishImages = [
"../Seasonals/Resources/underwater_assets/fish_1.gif",
"../Seasonals/Resources/underwater_assets/fish_2.gif",
"../Seasonals/Resources/underwater_assets/fish_3.gif",
"../Seasonals/Resources/underwater_assets/fish_5.gif",
"../Seasonals/Resources/underwater_assets/fish_6.gif",
"../Seasonals/Resources/underwater_assets/fish_7.png",
"../Seasonals/Resources/underwater_assets/fish_8.png",
"../Seasonals/Resources/underwater_assets/fish_9.png",
"../Seasonals/Resources/underwater_assets/fish_10.png",
"../Seasonals/Resources/underwater_assets/fish_11.png",
"../Seasonals/Resources/underwater_assets/fish_12.png",
"../Seasonals/Resources/underwater_assets/fish_13.png",
"../Seasonals/Resources/underwater_assets/fish_14.png",
"../Seasonals/Resources/underwater_assets/fish_15.png"
];
const seahorsesImages = [
"../Seasonals/Resources/underwater_assets/seahorse_1.gif",
"../Seasonals/Resources/underwater_assets/seahorse_2.gif"
];
const turtleImages = [
"../Seasonals/Resources/underwater_assets/turtle.gif"
];
const jellyfishImages = [
"../Seasonals/Resources/underwater_assets/jellyfish_1.gif",
"../Seasonals/Resources/underwater_assets/jellyfish_2.gif"
];
// MARK: Base sizes for all creatures (in vh)
const seahorseSize = 8;
const turtleSize = 14;
const jellyfishSize = 18;
const fishSize = 8;
const crabSize = 4;
const starfishSize = 4;
const shellSize = 7;
let msgPrinted = false; let msgPrinted = false;
@@ -46,100 +118,197 @@ function createUnderwater() {
container.className = 'underwater-container'; container.className = 'underwater-container';
container.setAttribute("aria-hidden", "true"); container.setAttribute("aria-hidden", "true");
document.body.appendChild(container); document.body.appendChild(container);
} else {
container.innerHTML = ''; // Prevent infinite duplication on theme reload!
} }
// Deep blue overlay // Deep blue overlay
const bg = document.createElement('div'); const bg = document.createElement('div');
bg.className = 'underwater-bg'; bg.className = 'underwater-bg';
container.appendChild(bg); container.appendChild(bg);
const standardCount = 8;
const totalSymbols = symbolCount + standardCount;
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches; // Light Rays (God Rays)
let finalCount = totalSymbols; if (enableLightRays) {
const rays = document.createElement('div');
if (isMobile) { rays.className = 'underwater-god-rays';
finalCount = enableRandomMobile ? totalSymbols : standardCount; container.appendChild(rays);
} }
const useRandomDuration = enableDifferentDuration !== false; const useRandomDuration = enableDifferentDuration !== false;
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
// Seaweed swaying at the bottom
for (let i = 0; i < 4; i++) { // Seaweed swaying at the bottom (evenly distributed based on count)
const activeSeaweedCount = Math.max(1, seaweedCount);
const seaweedSpacing = 95 / activeSeaweedCount;
for (let i = 0; i < seaweedCount; i++) {
let seaweed = document.createElement('div'); let seaweed = document.createElement('div');
seaweed.className = 'underwater-seaweed'; seaweed.className = 'underwater-seaweed';
seaweed.style.left = `${10 + (i * 25)}vw`; seaweed.style.position = 'absolute';
// MARK: Distance from the bottom edge for the seaweed
seaweed.style.bottom = '-18px';
let offset = (Math.random() * seaweedSpacing) - (seaweedSpacing / 2);
seaweed.style.left = `max(0vw, min(95vw, calc(${(i * seaweedSpacing)}vw + ${offset}vw)))`;
seaweed.style.animationDelay = `-${Math.random() * 5}s`; seaweed.style.animationDelay = `-${Math.random() * 5}s`;
// Randomly flip // Random parallax scale for seaweed depth
if (Math.random() > 0.5) { const depth = Math.random();
seaweed.style.transform = 'scaleX(-1)'; const scale = 0.5 + depth * 0.7; // 0.5 to 1.2
} const blur = depth < 0.3 ? `blur(2px)` : 'none';
seaweed.style.filter = blur;
let flip = Math.random() > 0.5 ? 'scaleX(-1)' : 'scaleX(1)';
seaweed.style.transform = `scale(${scale}) ${flip}`;
seaweed.style.zIndex = depth < 0.5 ? '15' : '30';
let img = document.createElement('img'); // Mix Emojis and GIFs
img.src = '../Seasonals/Resources/underwater_images/seaweed.png'; if (Math.random() > 0.4) {
img.onerror = function() { let img = document.createElement('img');
this.style.display = 'none'; img.src = seaweeds[Math.floor(Math.random() * seaweeds.length)];
this.parentElement.innerHTML = '🌿'; img.onerror = function() {
this.parentElement.style.fontSize = '3rem'; this.style.display = 'none';
this.parentElement.style.bottom = '0'; };
this.parentElement.style.transformOrigin = 'bottom center'; seaweed.appendChild(img);
}; } else {
seaweed.appendChild(img); seaweed.innerHTML = '🌿';
seaweed.style.fontSize = '3rem';
seaweed.style.bottom = '0';
seaweed.style.transformOrigin = 'bottom center';
}
container.appendChild(seaweed); container.appendChild(seaweed);
} }
const activeItems = ['fish_orange', 'fish_blue', 'jellyfish', 'turtle']; // Static Bottom Creatures logic
function spawnStatic(imageArray, maxCount, baseSize) {
// Evaluate an actual count between 1 and maxCount if random symbols are enabled
const actualCount = (useRandomSymbols && maxCount > 0) ? Math.floor(Math.random() * maxCount) + 1 : maxCount;
for (let i = 0; i < actualCount; i++) {
let creature = document.createElement('div');
creature.className = 'underwater-static-bottom';
creature.style.position = 'absolute';
creature.style.bottom = '5px';
creature.style.left = `${Math.random() * 95}vw`;
creature.style.zIndex = '20'; // In between seaweed layers
for (let i = 0; i < finalCount; i++) { let img = document.createElement('img');
let symbol = document.createElement('div'); img.src = imageArray[Math.floor(Math.random() * imageArray.length)];
img.style.height = `${baseSize}vh`;
// Random scale variance and flip
const scale = 0.7 + Math.random() * 0.5; // 0.7 to 1.2 x baseSize
const flip = Math.random() > 0.5 ? 'scaleX(-1)' : 'scaleX(1)';
img.style.transform = `scale(${scale}) ${flip}`;
img.onerror = function() {
this.style.display = 'none';
};
creature.appendChild(img);
container.appendChild(creature);
}
}
spawnStatic(crabImages, crabCount, crabSize);
spawnStatic(starfishImages, starfishCount, starfishSize);
spawnStatic(shellImages, shellCount, shellSize);
// Swimmers logic
function spawnSwimmerLoop(imageArray, maxCount, baseSize, typeName) {
if (maxCount <= 0) return;
let spawnLimit = isMobile ? (enableRandomMobile ? maxCount : Math.floor(maxCount / 2)) : maxCount;
const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)]; // Randomize the actual amount spawned up to the limit
symbol.className = `underwater-symbol underwater-${randomItem}`; const actualCount = (useRandomSymbols && spawnLimit > 0) ? Math.floor(Math.random() * spawnLimit) + 1 : spawnLimit;
for (let i = 0; i < actualCount; i++) {
// Spawn immediately but use negative delay to distribute them across the screen!
spawnSingleSwimmer(imageArray, baseSize, typeName);
}
}
function spawnSingleSwimmer(imageArray, baseSize, typeName) {
if (!document.querySelector('.underwater-container')) return;
let symbol = document.createElement('div');
symbol.className = `underwater-symbol`;
const randomImage = imageArray[Math.floor(Math.random() * imageArray.length)];
let img = document.createElement('img'); let img = document.createElement('img');
img.src = `../Seasonals/Resources/underwater_images/${randomItem}.png`; img.src = randomImage;
img.style.height = `${baseSize}vh`;
img.style.width = 'auto';
img.style.maxWidth = 'none';
img.onerror = function() { img.onerror = function() {
this.style.display = 'none'; this.style.display = 'none';
this.parentElement.innerHTML = getUnderwaterEmojiFallback(randomItem);
}; };
symbol.appendChild(img);
const topPos = 10 + Math.random() * 80; // 10 to 90vh const depth = Math.random();
const delaySeconds = Math.random() * 10; const distanceScale = 0.4 + (depth * 0.8);
const blurAmount = depth < 0.4 ? (1 - depth) * 3 : 0;
const opacity = 0.4 + (depth * 0.5);
let durationSeconds = 15; symbol.style.opacity = `${opacity}`;
if (useRandomDuration) { symbol.style.filter = `blur(${blurAmount}px)`;
durationSeconds = Math.random() * 10 + 15; // 15 to 25 seconds slow swimming symbol.style.zIndex = Math.floor(depth * 30) + 10;
}
symbol.style.animationIterationCount = 'infinite';
// Randomly pick direction: left-to-right OR right-to-left let durationSeconds = (1 - depth) * 20 + 15 + Math.random() * 5;
const goRight = Math.random() > 0.5; if (!useRandomDuration) durationSeconds = 20;
if (goRight) {
symbol.style.animationName = 'underwater-swim-right'; // Apply a negative delay on spawn so they start mid-screen scattered
symbol.style.left = '-10vw'; const startDelay = -(Math.random() * durationSeconds);
// Animate based on type
if (typeName === 'jellyfish') {
const goUp = Math.random() > 0.5;
symbol.style.animationName = goUp ? 'underwater-traverse-up' : 'underwater-traverse-down';
symbol.style.left = `${Math.random() * 90}vw`;
const flip = Math.random() > 0.5 ? 'scaleX(-1)' : 'scaleX(1)';
symbol.style.transform = `scale(${distanceScale}) ${flip}`;
durationSeconds *= 0.8;
symbol.style.animationDuration = `${durationSeconds}s`;
symbol.style.animationDelay = `${startDelay}s`;
symbol.appendChild(img);
} else { } else {
symbol.style.animationName = 'underwater-swim-left'; const goRight = Math.random() > 0.5;
symbol.style.right = '-10vw'; const directionScale = goRight ? 'scaleX(-1)' : 'scaleX(1)';
symbol.style.transform = 'scaleX(-1)'; // flip fish to face left
symbol.style.animationName = goRight ? 'underwater-traverse-right' : 'underwater-traverse-left';
symbol.style.animationDelay = `${startDelay}s`;
const rotationDiv = document.createElement('div');
let swayDur = Math.random() * 2 + 2;
if (typeName === 'seahorse') swayDur *= 1.5;
else if (typeName === 'turtle') swayDur *= 2;
rotationDiv.style.animation = `underwater-sway-y ${swayDur}s ease-in-out infinite alternate`;
// Random internal sway to prevent synchronized wiggling
rotationDiv.style.animationDelay = `-${Math.random() * 5}s`;
// Apply flip scale directly to the image inside rotationDiv
img.style.transform = `scale(${distanceScale}) ${directionScale}`;
rotationDiv.appendChild(img);
symbol.appendChild(rotationDiv);
symbol.style.top = `${Math.random() * 80 + 5}vh`;
symbol.style.animationDuration = `${durationSeconds}s`;
} }
symbol.style.top = `${topPos}vh`;
symbol.style.animationDuration = `${durationSeconds}s`;
symbol.style.animationDelay = `${delaySeconds}s`;
// Small vertical sway
const swimSway = document.createElement('div');
swimSway.style.animation = `underwater-sway ${Math.random() * 2 + 3}s ease-in-out infinite`;
swimSway.appendChild(symbol.cloneNode(true));
symbol.innerHTML = '';
symbol.appendChild(swimSway);
container.appendChild(symbol); container.appendChild(symbol);
} }
// Bubbles // Start swimmer loops
spawnSwimmerLoop(fishImages, fishCount, fishSize, 'fish');
spawnSwimmerLoop(seahorsesImages, seahorseCount, seahorseSize, 'seahorse');
spawnSwimmerLoop(jellyfishImages, jellyfishCount, jellyfishSize, 'jellyfish');
spawnSwimmerLoop(turtleImages, turtleCount, turtleSize, 'turtle');
const bubbleCount = isMobile ? 15 : 30; const bubbleCount = isMobile ? 15 : 30;
for (let i = 0; i < bubbleCount; i++) { for (let i = 0; i < bubbleCount; i++) {
@@ -163,13 +332,7 @@ function createUnderwater() {
} }
} }
function getUnderwaterEmojiFallback(type) {
if (type === 'fish_orange') return '🐠';
if (type === 'fish_blue') return '🐟';
if (type === 'jellyfish') return '🪼';
if (type === 'turtle') return '🐢';
return '🫧';
}
function initializeUnderwater() { function initializeUnderwater() {
if (!underwater) return; if (!underwater) return;