Enhance underwater animation: add new creature types, improve movement animations, and implement light rays effect
This commit is contained in:
@@ -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; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -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,6 +118,8 @@ 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
|
||||||
@@ -53,93 +127,188 @@ function createUnderwater() {
|
|||||||
bg.className = 'underwater-bg';
|
bg.className = 'underwater-bg';
|
||||||
container.appendChild(bg);
|
container.appendChild(bg);
|
||||||
|
|
||||||
const standardCount = 8;
|
// Light Rays (God Rays)
|
||||||
const totalSymbols = symbolCount + standardCount;
|
if (enableLightRays) {
|
||||||
|
const rays = document.createElement('div');
|
||||||
let isMobile = window.matchMedia("only screen and (max-width: 768px)").matches;
|
rays.className = 'underwater-god-rays';
|
||||||
let finalCount = totalSymbols;
|
container.appendChild(rays);
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
finalCount = enableRandomMobile ? totalSymbols : standardCount;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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';
|
||||||
|
|
||||||
|
// Mix Emojis and GIFs
|
||||||
|
if (Math.random() > 0.4) {
|
||||||
let img = document.createElement('img');
|
let img = document.createElement('img');
|
||||||
img.src = '../Seasonals/Resources/underwater_images/seaweed.png';
|
img.src = seaweeds[Math.floor(Math.random() * seaweeds.length)];
|
||||||
img.onerror = function() {
|
img.onerror = function() {
|
||||||
this.style.display = 'none';
|
this.style.display = 'none';
|
||||||
this.parentElement.innerHTML = '🌿';
|
|
||||||
this.parentElement.style.fontSize = '3rem';
|
|
||||||
this.parentElement.style.bottom = '0';
|
|
||||||
this.parentElement.style.transformOrigin = 'bottom center';
|
|
||||||
};
|
};
|
||||||
seaweed.appendChild(img);
|
seaweed.appendChild(img);
|
||||||
|
} else {
|
||||||
|
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) {
|
||||||
for (let i = 0; i < finalCount; i++) {
|
// Evaluate an actual count between 1 and maxCount if random symbols are enabled
|
||||||
let symbol = document.createElement('div');
|
const actualCount = (useRandomSymbols && maxCount > 0) ? Math.floor(Math.random() * maxCount) + 1 : maxCount;
|
||||||
|
for (let i = 0; i < actualCount; i++) {
|
||||||
const randomItem = activeItems[Math.floor(Math.random() * activeItems.length)];
|
let creature = document.createElement('div');
|
||||||
symbol.className = `underwater-symbol underwater-${randomItem}`;
|
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
|
||||||
|
|
||||||
let img = document.createElement('img');
|
let img = document.createElement('img');
|
||||||
img.src = `../Seasonals/Resources/underwater_images/${randomItem}.png`;
|
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() {
|
img.onerror = function() {
|
||||||
this.style.display = 'none';
|
this.style.display = 'none';
|
||||||
this.parentElement.innerHTML = getUnderwaterEmojiFallback(randomItem);
|
|
||||||
};
|
};
|
||||||
symbol.appendChild(img);
|
creature.appendChild(img);
|
||||||
|
container.appendChild(creature);
|
||||||
const topPos = 10 + Math.random() * 80; // 10 to 90vh
|
}
|
||||||
const delaySeconds = Math.random() * 10;
|
|
||||||
|
|
||||||
let durationSeconds = 15;
|
|
||||||
if (useRandomDuration) {
|
|
||||||
durationSeconds = Math.random() * 10 + 15; // 15 to 25 seconds slow swimming
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Randomly pick direction: left-to-right OR right-to-left
|
spawnStatic(crabImages, crabCount, crabSize);
|
||||||
const goRight = Math.random() > 0.5;
|
spawnStatic(starfishImages, starfishCount, starfishSize);
|
||||||
if (goRight) {
|
spawnStatic(shellImages, shellCount, shellSize);
|
||||||
symbol.style.animationName = 'underwater-swim-right';
|
|
||||||
symbol.style.left = '-10vw';
|
// Swimmers logic
|
||||||
} else {
|
function spawnSwimmerLoop(imageArray, maxCount, baseSize, typeName) {
|
||||||
symbol.style.animationName = 'underwater-swim-left';
|
if (maxCount <= 0) return;
|
||||||
symbol.style.right = '-10vw';
|
let spawnLimit = isMobile ? (enableRandomMobile ? maxCount : Math.floor(maxCount / 2)) : maxCount;
|
||||||
symbol.style.transform = 'scaleX(-1)'; // flip fish to face left
|
|
||||||
|
// Randomize the actual amount spawned up to the limit
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
symbol.style.top = `${topPos}vh`;
|
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');
|
||||||
|
img.src = randomImage;
|
||||||
|
img.style.height = `${baseSize}vh`;
|
||||||
|
img.style.width = 'auto';
|
||||||
|
img.style.maxWidth = 'none';
|
||||||
|
|
||||||
|
img.onerror = function() {
|
||||||
|
this.style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
const depth = Math.random();
|
||||||
|
const distanceScale = 0.4 + (depth * 0.8);
|
||||||
|
const blurAmount = depth < 0.4 ? (1 - depth) * 3 : 0;
|
||||||
|
const opacity = 0.4 + (depth * 0.5);
|
||||||
|
|
||||||
|
symbol.style.opacity = `${opacity}`;
|
||||||
|
symbol.style.filter = `blur(${blurAmount}px)`;
|
||||||
|
symbol.style.zIndex = Math.floor(depth * 30) + 10;
|
||||||
|
|
||||||
|
symbol.style.animationIterationCount = 'infinite';
|
||||||
|
|
||||||
|
let durationSeconds = (1 - depth) * 20 + 15 + Math.random() * 5;
|
||||||
|
if (!useRandomDuration) durationSeconds = 20;
|
||||||
|
|
||||||
|
// Apply a negative delay on spawn so they start mid-screen scattered
|
||||||
|
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.animationDuration = `${durationSeconds}s`;
|
||||||
symbol.style.animationDelay = `${delaySeconds}s`;
|
symbol.style.animationDelay = `${startDelay}s`;
|
||||||
|
|
||||||
// Small vertical sway
|
symbol.appendChild(img);
|
||||||
const swimSway = document.createElement('div');
|
} else {
|
||||||
swimSway.style.animation = `underwater-sway ${Math.random() * 2 + 3}s ease-in-out infinite`;
|
const goRight = Math.random() > 0.5;
|
||||||
swimSway.appendChild(symbol.cloneNode(true));
|
const directionScale = goRight ? 'scaleX(-1)' : 'scaleX(1)';
|
||||||
symbol.innerHTML = '';
|
|
||||||
symbol.appendChild(swimSway);
|
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`;
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
|
|||||||
Reference in New Issue
Block a user