
Web Wave Designer
- 144 installs
- 178 repo stars
- Updated July 14, 2026
- erichowens/some_claude_skills
Create wave-inspired web layouts, gradients, and motion-ready components for landing pages, portfolios, and SaaS marketing surfaces needing a distinctive visual identity.
About
Guides creation of wave-inspired web interfaces with cohesive gradients, layered visual motifs, and component-level styling patterns suited to landing pages, portfolios, and SaaS marketing sites that need a bold, memorable look.
- wave aesthetics
- gradient motifs
- responsive layouts
- marketing pages
- motion-ready UI
Web Wave Designer by the numbers
- 144 all-time installs (skills.sh)
- Ranked #1,024 of 1,880 Design & UI/UX skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/erichowens/some_claude_skills --skill web-wave-designerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 144 |
|---|---|
| repo stars | ★ 178 |
| Last updated | July 14, 2026 |
| Repository | erichowens/some_claude_skills ↗ |
What it does
Create wave-inspired web layouts, gradients, and motion-ready components for landing pages, portfolios, and SaaS marketing surfaces needing a distinctive visual identity.
Files
Web Wave Designer
Expert in creating realistic, performant ocean and water wave effects for web applications using SVG filters, CSS animations, and layering techniques. Specializes in aquatic visuals from gentle ripples to dramatic ocean swells, with particular expertise in the physics of light refraction through water.
When to Use This Skill
Use for:
- Ocean wave backgrounds and seascapes
- Underwater distortion/refraction effects
- Beach shore waves with foam
- Pond/pool ripple animations
- Liquid glass UI effects
- Water-themed loading states
- Parallax ocean layers with depth
- Stylized/cartoon water for games
- Reflection effects on water surfaces
Do NOT use for:
- 3D volumetric ocean rendering -> use WebGL/Three.js/Ocean.js
- Real-time fluid simulation -> use canvas physics engines
- Video effects -> use video editing software
- Simple blue gradients without motion
- Water droplet physics -> use particle systems
Core Distinction: turbulence vs fractalNoise
CRITICAL: For water effects, use type="turbulence" (NOT fractalNoise like clouds):
| Type | Visual | Best For |
|---|---|---|
turbulence | Continuous flow patterns, starts from transparent black | Water, waves, liquid |
fractalNoise | Random cloudlike patches, opaque | Clouds, smoke, terrain |
<!-- WATER - use turbulence -->
<feTurbulence type="turbulence" baseFrequency="0.01 0.1" />
<!-- CLOUDS - use fractalNoise -->
<feTurbulence type="fractalNoise" baseFrequency="0.01" />SVG Filter Pipeline for Water
The fundamental water effect filter chain:
Source -> feTurbulence -> feDisplacementMap -> feComponentTransfer -> feComposite
(waves) (distortion) (color/opacity) (blend)1. feTurbulence - Wave Pattern Generation
<feTurbulence
type="turbulence" <!-- MUST be turbulence for water -->
baseFrequency="0.01 0.1" <!-- TWO values: x-freq y-freq -->
numOctaves="3" <!-- 2-5: wave complexity -->
seed="42" <!-- Variation (free performance) -->
result="waves"
/>baseFrequency Explained (TWO Values):
| X-Frequency | Y-Frequency | Result |
|---|---|---|
| 0.01 | 0.1 | Long horizontal waves with vertical oscillation |
| 0.005 | 0.05 | Deep ocean swells |
| 0.02 | 0.15 | Choppy surface waves |
| 0.03 | 0.03 | Square ripples (pond) |
The ratio matters:
- X much less than Y: Stretched horizontal waves (ocean)
- X == Y: Circular ripples (pond, pool)
- X much greater than Y: Vertical striations (waterfall)
2. feDisplacementMap - Refraction Effect
Creates the bending/distortion that makes content behind water appear to ripple.
<feDisplacementMap
in="SourceGraphic" <!-- What gets distorted -->
in2="waves" <!-- Distortion pattern (from turbulence) -->
scale="20" <!-- 10-40 for realistic refraction -->
xChannelSelector="R" <!-- Which color channel drives X displacement -->
yChannelSelector="G" <!-- Which color channel drives Y displacement -->
result="refracted"
/>| Scale Value | Effect |
|---|---|
| 10-15 | Gentle pool ripples |
| 15-25 | Standard water refraction |
| 25-40 | Strong wave distortion |
| 40+ | Psychedelic (unrealistic) |
3. feComponentTransfer - Water Color
Transform noise into water-like colors by manipulating channels.
<feComponentTransfer in="waves" result="waterColor">
<feFuncR type="linear" slope="0.3" intercept="0"/> <!-- Reduce red -->
<feFuncG type="linear" slope="0.7" intercept="0.2"/> <!-- Boost green -->
<feFuncB type="linear" slope="1.2" intercept="0.3"/> <!-- Strong blue -->
<feFuncA type="linear" slope="0.6" intercept="0.3"/> <!-- Water opacity -->
</feComponentTransfer>4. feGaussianBlur - Caustics (Underwater Light)
<feGaussianBlur
in="waves"
stdDeviation="2" <!-- 1-5 for soft caustic patterns -->
result="caustics"
/>5. Compositing - Layer Assembly
<feFlood flood-color="#0077be" flood-opacity="0.4" result="baseWater"/>
<feBlend in="caustics" in2="baseWater" mode="screen" result="waterLayer"/>
<feComposite in="waterLayer" in2="SourceGraphic" operator="over"/>Wave Type Recipes
Ocean Surface Waves
<svg width="100%" height="300">
<defs>
<filter id="oceanWaves" x="0" y="0" width="100%" height="100%">
<feTurbulence type="turbulence"
baseFrequency="0.005 0.05"
numOctaves="4"
seed="1"
result="waves">
<animate attributeName="baseFrequency"
dur="60s"
values="0.005 0.05;0.007 0.06;0.005 0.05"
repeatCount="indefinite"/>
</feTurbulence>
<feDisplacementMap in="SourceGraphic" in2="waves" scale="25"/>
</filter>
</defs>
<rect width="100%" height="100%" fill="url(#oceanGradient)" filter="url(#oceanWaves)"/>
</svg>Pond Ripples (Circular)
<filter id="pondRipples">
<feTurbulence type="turbulence"
baseFrequency="0.02 0.02" <!-- Equal = circular -->
numOctaves="2"
seed="10"
result="ripples"/>
<feDisplacementMap in="SourceGraphic" in2="ripples" scale="12"/>
</filter>Beach Shore Waves (Breaking)
<filter id="shoreWaves">
<feTurbulence type="turbulence"
baseFrequency="0.008 0.12" <!-- Strong vertical motion -->
numOctaves="3"
seed="5"
result="waves"/>
<feGaussianBlur in="waves" stdDeviation="1.5" result="softWaves"/>
<feDisplacementMap in="SourceGraphic" in2="softWaves" scale="30"/>
<!-- Add foam layer -->
<feTurbulence type="fractalNoise"
baseFrequency="0.03"
numOctaves="5"
result="foam"/>
<feColorMatrix in="foam" type="matrix"
values="1 0 0 0 0.9
1 0 0 0 0.95
1 0 0 0 1
0 0 0 0.3 0"/>
</filter>Underwater Distortion (Looking Through Water)
<filter id="underwater" x="-10%" y="-10%" width="120%" height="120%">
<feTurbulence type="turbulence"
baseFrequency="0.015 0.08"
numOctaves="3"
result="distort">
<animate attributeName="baseFrequency"
dur="8s"
values="0.015 0.08;0.018 0.09;0.015 0.08"
repeatCount="indefinite"/>
</feTurbulence>
<feDisplacementMap in="SourceGraphic" in2="distort"
scale="20"
xChannelSelector="R"
yChannelSelector="B"/>
<!-- Slight blue tint -->
<feColorMatrix type="matrix"
values="0.9 0 0 0 0
0 0.95 0 0 0.02
0 0 1.1 0 0.05
0 0 0 1 0"/>
</filter>Liquid Glass Effect (Modern UI)
<filter id="liquidGlass" x="-5%" y="-5%" width="110%" height="110%">
<feTurbulence type="turbulence"
baseFrequency="0.01 0.05"
numOctaves="2"
seed="99"
result="ripple">
<animate attributeName="seed"
dur="4s"
values="99;100;101;100;99"
repeatCount="indefinite"/>
</feTurbulence>
<feDisplacementMap in="SourceGraphic" in2="ripple" scale="8"/>
<!-- Frosted glass blur -->
<feGaussianBlur stdDeviation="0.5"/>
</filter>Stylized/Cartoon Waves
<filter id="cartoonWater">
<feTurbulence type="turbulence"
baseFrequency="0.02 0.08"
numOctaves="1" <!-- Low octaves = bold shapes -->
result="waves"/>
<feDisplacementMap in="SourceGraphic" in2="waves" scale="15"/>
<!-- Sharp edges, no blur -->
</filter>Animation Techniques
JavaScript requestAnimationFrame (Smoothest)
const turbulence = document.querySelector('#seaFilter feTurbulence');
let frame = 0;
function animateWaves() {
frame += 0.003;
// Gentle breathing motion
const xFreq = 0.006 + Math.sin(frame) * 0.002;
const yFreq = 0.05 + Math.sin(frame * 0.7) * 0.01;
turbulence.setAttribute('baseFrequency', `${xFreq} ${yFreq}`);
requestAnimationFrame(animateWaves);
}
animateWaves();SVG animate (Declarative, CPU-Heavy)
<feTurbulence baseFrequency="0.01 0.1" numOctaves="3">
<animate
attributeName="baseFrequency"
dur="60s"
keyTimes="0;0.5;1"
values="0.008 0.08;0.012 0.12;0.008 0.08"
repeatCount="indefinite"
/>
</feTurbulence>WARNING: SVG animate on filter attributes forces full filter recalculation every frame. Use sparingly.
CSS Transform Animation (Best Performance)
Move the water element, not the filter:
.wave-layer {
animation: wave-drift 20s linear infinite;
}
@keyframes wave-drift {
from { transform: translateX(0) translateY(0); }
to { transform: translateX(-50%) translateY(5px); }
}Seed Animation (Morphing Waves)
Animate seed for shape variation without baseFrequency cost:
<feTurbulence baseFrequency="0.01 0.1">
<animate
attributeName="seed"
dur="20s"
values="1;50;100;50;1"
repeatCount="indefinite"
/>
</feTurbulence>Layering Strategy
Multi-Layer Ocean
<div class="ocean">
<div class="wave wave-back"></div>
<div class="wave wave-mid"></div>
<div class="wave wave-front"></div>
<div class="foam-layer"></div>
</div>.ocean {
position: relative;
height: 100vh;
background: linear-gradient(180deg,
#0c4a6e 0%,
#0369a1 40%,
#0ea5e9 100%
);
overflow: hidden;
}
.wave {
position: absolute;
width: 200%;
height: 100%;
background: rgba(255,255,255,0.1);
}
.wave-back {
filter: url(#waveBack);
opacity: 0.3;
animation: drift 90s linear infinite;
bottom: 0;
}
.wave-mid {
filter: url(#waveMid);
opacity: 0.5;
animation: drift 60s linear infinite;
bottom: -5%;
}
.wave-front {
filter: url(#waveFront);
opacity: 0.7;
animation: drift 35s linear infinite;
bottom: -10%;
}
.foam-layer {
position: absolute;
bottom: 0;
width: 100%;
height: 20%;
background: linear-gradient(to top,
rgba(255,255,255,0.8) 0%,
transparent 100%
);
filter: url(#foamFilter);
}
@keyframes drift {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}Layer Parameter Guide
| Layer | Opacity | Speed | Filter Scale | baseFrequency |
|---|---|---|---|---|
| Back (deep) | 0.2-0.4 | 80-100s | 15 | 0.004 0.04 |
| Mid | 0.4-0.6 | 50-70s | 20 | 0.006 0.06 |
| Front (surface) | 0.6-0.8 | 30-45s | 25 | 0.01 0.1 |
| Foam | 0.7-0.9 | 25-35s | 10 | 0.02 0.02 |
Color Palettes
Deep Ocean
.deep-ocean {
background: linear-gradient(180deg,
#0c4a6e 0%, /* Deep blue */
#075985 30%,
#0369a1 60%,
#0284c7 100% /* Surface shimmer */
);
}- Primary:
#0369a1 - Deep:
#0c4a6e - Highlight:
#38bdf8
Tropical/Caribbean
.tropical {
background: linear-gradient(180deg,
#06b6d4 0%, /* Cyan surface */
#22d3ee 40%,
#67e8f9 70%,
#a5f3fc 100% /* Shallow sand reflection */
);
}- Primary:
#06b6d4 - Shallow:
#67e8f9 - Foam:
#ecfeff
Stormy Sea
.stormy {
background: linear-gradient(180deg,
#1e293b 0%, /* Dark clouds */
#334155 30%,
#475569 60%,
#64748b 100% /* Whitecaps */
);
}- Primary:
#475569 - Depth:
#1e293b - Whitecap:
#cbd5e1
Sunset Reflection
.sunset-water {
background: linear-gradient(180deg,
#831843 0%, /* Pink sky */
#9d174d 20%,
#be185d 40%,
#0369a1 60%, /* Water starts */
#0c4a6e 100%
);
}Complete Implementation Templates
Template 1: Full Ocean Scene
<!DOCTYPE html>
<html>
<head>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
.ocean-scene {
position: relative;
width: 100%;
height: 100vh;
background: linear-gradient(180deg,
#87CEEB 0%, /* Sky */
#87CEEB 40%,
#0369a1 40%, /* Horizon */
#0c4a6e 100% /* Deep */
);
overflow: hidden;
}
.horizon-line {
position: absolute;
top: 40%;
left: 0;
right: 0;
height: 2px;
background: rgba(255,255,255,0.3);
}
.wave {
position: absolute;
width: 200%;
height: 60%;
bottom: 0;
background: rgba(255,255,255,0.15);
}
.wave-1 {
filter: url(#wave1);
animation: drift 80s linear infinite;
opacity: 0.4;
}
.wave-2 {
filter: url(#wave2);
animation: drift 55s linear infinite;
animation-delay: -20s;
opacity: 0.6;
}
.wave-3 {
filter: url(#wave3);
animation: drift 35s linear infinite;
animation-delay: -10s;
opacity: 0.8;
}
@keyframes drift {
from { transform: translateX(0); }
to { transform: translateX(-50%); }
}
</style>
</head>
<body>
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="wave1" x="0" y="0" width="100%" height="100%">
<feTurbulence type="turbulence" baseFrequency="0.004 0.04" numOctaves="3" seed="1"/>
<feDisplacementMap in="SourceGraphic" scale="15"/>
</filter>
<filter id="wave2" x="0" y="0" width="100%" height="100%">
<feTurbulence type="turbulence" baseFrequency="0.006 0.06" numOctaves="3" seed="2"/>
<feDisplacementMap in="SourceGraphic" scale="20"/>
</filter>
<filter id="wave3" x="0" y="0" width="100%" height="100%">
<feTurbulence type="turbulence" baseFrequency="0.01 0.1" numOctaves="4" seed="3"/>
<feDisplacementMap in="SourceGraphic" scale="25"/>
</filter>
</defs>
</svg>
<div class="ocean-scene">
<div class="horizon-line"></div>
<div class="wave wave-1"></div>
<div class="wave wave-2"></div>
<div class="wave wave-3"></div>
</div>
</body>
</html>Template 2: Underwater View (Content Behind Water)
<style>
.underwater-container {
position: relative;
width: 100%;
overflow: hidden;
}
.content-behind-water {
/* Your actual content */
}
.water-overlay {
position: absolute;
inset: 0;
pointer-events: none;
filter: url(#underwaterDistort);
}
.caustics {
position: absolute;
inset: 0;
background: url('data:image/svg+xml,...') repeat;
opacity: 0.3;
mix-blend-mode: overlay;
animation: caustic-shift 10s linear infinite;
}
@keyframes caustic-shift {
from { background-position: 0 0; }
to { background-position: 100px 50px; }
}
</style>
<svg style="display:none">
<defs>
<filter id="underwaterDistort" x="-10%" y="-10%" width="120%" height="120%">
<feTurbulence id="underwaterTurb" type="turbulence"
baseFrequency="0.015 0.08" numOctaves="3"/>
<feDisplacementMap in="SourceGraphic" scale="20"
xChannelSelector="R" yChannelSelector="B"/>
<feColorMatrix type="matrix"
values="0.85 0 0 0 0
0 0.9 0 0 0.02
0 0 1.1 0 0.08
0 0 0 1 0"/>
</filter>
</defs>
</svg>
<script>
// Animate underwater distortion
const turb = document.getElementById('underwaterTurb');
let frame = 0;
function animate() {
frame += 0.005;
const xFreq = 0.015 + Math.sin(frame) * 0.003;
const yFreq = 0.08 + Math.sin(frame * 0.7) * 0.01;
turb.setAttribute('baseFrequency', `${xFreq} ${yFreq}`);
requestAnimationFrame(animate);
}
animate();
</script>Template 3: React Water Component
import React, { useEffect, useRef, useMemo } from 'react';
interface WaterEffectProps {
type?: 'ocean' | 'pool' | 'stream' | 'glass';
intensity?: 'subtle' | 'medium' | 'strong';
animate?: boolean;
className?: string;
children?: React.ReactNode;
}
const WATER_CONFIGS = {
ocean: { baseFreq: [0.008, 0.08], octaves: 4, scale: 25 },
pool: { baseFreq: [0.02, 0.02], octaves: 2, scale: 12 },
stream: { baseFreq: [0.01, 0.15], octaves: 3, scale: 20 },
glass: { baseFreq: [0.01, 0.05], octaves: 2, scale: 8 },
};
const INTENSITY_MULTIPLIERS = {
subtle: 0.5,
medium: 1.0,
strong: 1.5,
};
export const WaterEffect: React.FC<WaterEffectProps> = ({
type = 'ocean',
intensity = 'medium',
animate = true,
className,
children
}) => {
const turbRef = useRef<SVGFETurbulenceElement>(null);
const frameRef = useRef(0);
const config = WATER_CONFIGS[type];
const mult = INTENSITY_MULTIPLIERS[intensity];
const filterId = useMemo(() =>
`water-${type}-${Date.now()}`, [type]
);
useEffect(() => {
if (!animate || !turbRef.current) return;
let animationId: number;
const animateWater = () => {
frameRef.current += 0.004;
const frame = frameRef.current;
const xFreq = config.baseFreq[0] + Math.sin(frame) * 0.002;
const yFreq = config.baseFreq[1] + Math.sin(frame * 0.7) * 0.01;
turbRef.current?.setAttribute('baseFrequency', `${xFreq} ${yFreq}`);
animationId = requestAnimationFrame(animateWater);
};
animateWater();
return () => cancelAnimationFrame(animationId);
}, [animate, config]);
return (
<>
<svg style={{ position: 'absolute', width: 0, height: 0 }}>
<defs>
<filter id={filterId} x="-10%" y="-10%" width="120%" height="120%">
<feTurbulence
ref={turbRef}
type="turbulence"
baseFrequency={config.baseFreq.join(' ')}
numOctaves={config.octaves}
result="waves"
/>
<feDisplacementMap
in="SourceGraphic"
in2="waves"
scale={config.scale * mult}
xChannelSelector="R"
yChannelSelector="G"
/>
</filter>
</defs>
</svg>
<div className={className} style={{ filter: `url(#${filterId})` }}>
{children}
</div>
</>
);
};
// Usage:
// <WaterEffect type="ocean" intensity="medium" animate>
// <img src="underwater-scene.jpg" />
// </WaterEffect>Template 4: CSS-Only Waves (No SVG)
For simple, high-performance waves without SVG filters:
.css-waves {
position: relative;
height: 300px;
background: linear-gradient(180deg, #0369a1 0%, #0c4a6e 100%);
overflow: hidden;
}
.css-wave {
position: absolute;
width: 200%;
height: 100%;
bottom: 0;
left: -50%;
background:
radial-gradient(ellipse 100% 50% at 50% 100%,
rgba(255,255,255,0.3) 0%,
transparent 60%
);
animation: css-wave-move 8s ease-in-out infinite;
transform-origin: center bottom;
}
.css-wave:nth-child(1) {
animation-duration: 7s;
opacity: 0.5;
}
.css-wave:nth-child(2) {
animation-duration: 10s;
animation-delay: -3s;
opacity: 0.3;
}
.css-wave:nth-child(3) {
animation-duration: 13s;
animation-delay: -5s;
opacity: 0.2;
}
@keyframes css-wave-move {
0%, 100% {
transform: translateX(0) scaleY(1);
}
50% {
transform: translateX(25%) scaleY(1.1);
}
}Performance Optimization
Critical Rules
1. Use `type="turbulence"` - Correct type for water (not fractalNoise) 2. numOctaves 4 or fewer - Above 4 minimal visual gain, exponential CPU cost 3. Scale 10-30 - Above 40 becomes unrealistic and slower 4. Avoid animating baseFrequency - Use CSS transforms or seed animation instead 5. GPU hints - Add will-change: transform on animated layers 6. Batch SVG defs - One <defs> block, multiple filters
Performance Tiers
| Tier | Technique | FPS Target | Use Case |
|---|---|---|---|
| Ultra | CSS radial gradients only | 60fps | Mobile, low-end |
| High | SVG filter, CSS transform animation | 60fps | Background waves |
| Medium | SVG filter + seed animation | 45-60fps | Interactive water |
| Low | SVG filter + baseFrequency animation | 30-40fps | Hero sections only |
Mobile Optimization
@media (prefers-reduced-motion: reduce) {
.wave {
animation: none !important;
}
}
@media (max-width: 768px) {
/* Reduce to 2 wave layers */
.wave-1 { display: none; }
/* Use simpler filter */
.wave { filter: url(#waveSimple); }
}Performance Detection
const canHandleWaterEffects = () => {
// Check for GPU
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl');
if (!gl) return 'ultra'; // CSS only
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
const renderer = debugInfo
? gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL)
: '';
// Integrated graphics = medium tier
if (renderer.includes('Intel') || renderer.includes('Mali')) {
return 'medium';
}
return 'high';
};
// Apply appropriate tier
const tier = canHandleWaterEffects();
document.body.dataset.waterTier = tier;/* Tier-based styling */
[data-water-tier="ultra"] .wave {
filter: none;
background: linear-gradient(/* simple gradient */);
}
[data-water-tier="medium"] .wave {
filter: url(#waveSimple);
}
[data-water-tier="high"] .wave {
filter: url(#waveFull);
}Framework Integration
Next.js / React
// components/OceanBackground.tsx
'use client';
import { useEffect, useState, useRef } from 'react';
import styles from './OceanBackground.module.css';
export function OceanBackground({ children }: { children: React.ReactNode }) {
const [mounted, setMounted] = useState(false);
const turbRef = useRef<SVGFETurbulenceElement>(null);
useEffect(() => {
setMounted(true);
// Animate after mount
let frame = 0;
const animate = () => {
frame += 0.003;
if (turbRef.current) {
const xFreq = 0.008 + Math.sin(frame) * 0.002;
const yFreq = 0.08 + Math.sin(frame * 0.7) * 0.01;
turbRef.current.setAttribute('baseFrequency', `${xFreq} ${yFreq}`);
}
requestAnimationFrame(animate);
};
const id = requestAnimationFrame(animate);
return () => cancelAnimationFrame(id);
}, []);
if (!mounted) return null;
return (
<div className={styles.ocean}>
<svg className={styles.filters}>
<defs>
<filter id="oceanWave">
<feTurbulence ref={turbRef} type="turbulence"
baseFrequency="0.008 0.08" numOctaves="4"/>
<feDisplacementMap in="SourceGraphic" scale="25"/>
</filter>
</defs>
</svg>
<div className={styles.waveLayer} />
<div className={styles.content}>{children}</div>
</div>
);
}Tailwind CSS
// tailwind.config.js
module.exports = {
theme: {
extend: {
animation: {
'wave-drift': 'wave-drift 60s linear infinite',
'wave-slow': 'wave-drift 90s linear infinite',
'wave-fast': 'wave-drift 35s linear infinite',
},
keyframes: {
'wave-drift': {
from: { transform: 'translateX(0)' },
to: { transform: 'translateX(-50%)' },
},
},
colors: {
ocean: {
deep: '#0c4a6e',
mid: '#0369a1',
surface: '#0ea5e9',
foam: '#f0f9ff',
},
},
},
},
};Vue 3
<template>
<div class="ocean-container">
<WaveFilters />
<div
v-for="layer in waveLayers"
:key="layer.id"
class="wave-layer"
:style="layer.style"
/>
<slot />
</div>
</template>
<script setup>
import { computed, onMounted, ref } from 'vue';
import WaveFilters from './WaveFilters.vue';
const waveLayers = computed(() => [
{ id: 1, style: { filter: 'url(#wave1)', animationDuration: '80s', opacity: 0.4 }},
{ id: 2, style: { filter: 'url(#wave2)', animationDuration: '55s', opacity: 0.6 }},
{ id: 3, style: { filter: 'url(#wave3)', animationDuration: '35s', opacity: 0.8 }},
]);
</script>Debugging Tips
Visualize Filter Pipeline
<!-- Show each filter step -->
<filter id="debug-water">
<feTurbulence result="step1"/>
<feImage href="#step1" x="0" y="0" width="200" height="200"/>
<feDisplacementMap in="SourceGraphic" in2="step1" result="step2"/>
<feImage href="#step2" x="200" y="0" width="200" height="200"/>
</filter>Common Issues
| Problem | Cause | Solution |
|---|---|---|
| Effect disappears | Filter region too small | Add x="-20%" y="-20%" width="140%" height="140%" |
| Square/boxy waves | Using fractalNoise | Change to type="turbulence" |
| Waves too uniform | Same seed across layers | Use different seed values |
| No horizontal motion | Equal baseFrequency values | Use baseFrequency="0.01 0.1" (different x/y) |
| Animation stuttering | Animating filter attributes | Use CSS transform animations instead |
| Edge artifacts | Displacement at boundaries | Increase filter region with x/y/width/height |
Browser DevTools
1. Elements panel > Select SVG filter > Inspect attributes 2. Performance panel > Record > Check for layout thrashing 3. Layers panel (Chrome) > Verify GPU acceleration on wave layers
Integration with web-cloud-designer
For complete atmospheric scenes, combine water and cloud effects:
<div class="scene">
<!-- Sky with clouds (from web-cloud-designer) -->
<div class="sky">
<div class="cloud-layer cloud-back"></div>
<div class="cloud-layer cloud-front"></div>
</div>
<!-- Horizon -->
<div class="horizon"></div>
<!-- Ocean with waves (this skill) -->
<div class="ocean">
<div class="wave wave-back"></div>
<div class="wave wave-front"></div>
<div class="reflection"></div>
</div>
</div>
<style>
.scene {
height: 100vh;
display: grid;
grid-template-rows: 60% 40%;
}
.sky {
background: linear-gradient(180deg, #1e3c72 0%, #87CEEB 100%);
}
.ocean {
background: linear-gradient(180deg, #0369a1 0%, #0c4a6e 100%);
}
.reflection {
/* Mirror cloud movement on water surface */
position: absolute;
top: 0;
width: 100%;
height: 30%;
background: inherit;
transform: scaleY(-1);
opacity: 0.3;
filter: url(#waterReflection) blur(2px);
}
</style>Reference Sources
- Red Stapler: "Realistic Water Effect SVG Turbulence"
- Mitkov Systems: "Liquid Glass Water Animation" (2025)
- O'Reilly SVG Book: feTurbulence Chapter
- MDN: SVG Filter Primitives Documentation
- CSS-Tricks: "Underwater Blur Effect"
- Codrops: "Water Distortion Effect"
---
Water is the driving force of all nature. - Leonardo da Vinci
Changelog
All notable changes to the web-wave-designer skill.
[1.0.0] - 2026-01-22
Added
- Initial release of web-wave-designer skill
- Core SVG filter pipeline documentation (feTurbulence, feDisplacementMap, feComponentTransfer)
- Critical distinction between
type="turbulence"vstype="fractalNoise"for water effects - Two-value baseFrequency explanation for directional wave patterns
- Wave type recipes: ocean surface, pond ripples, beach shore, underwater distortion, liquid glass, stylized/cartoon
- Layering strategy for multi-depth ocean scenes
- Animation techniques comparison: CSS transforms, requestAnimationFrame, SVG animate, GSAP
- Color palettes: deep ocean, tropical, stormy, sunset reflection
- Complete implementation templates (vanilla HTML/CSS, React component, CSS-only)
- Performance optimization guide with tier system (Ultra/High/Medium/Low)
- Framework integration examples (Next.js, Vue 3, Tailwind CSS)
- Reference documentation:
turbulence-deep-dive.md: Complete feTurbulence parameter guidedisplacement-and-color.md: feDisplacementMap, caustics, foam, reflectionsanimation-patterns.md: Performance-aware animation strategies- Integration guidance with web-cloud-designer for complete atmospheric scenes
- Browser compatibility notes
- Debugging tips and common issues
Technical Notes
- Designed to pair with web-cloud-designer skill for complete sky + ocean scenes
- Based on research from Red Stapler, Mitkov Systems (2025), CSS-Tricks, and MDN
- Emphasizes performance-first approach with adaptive quality tiers
- Includes reduced motion support for accessibility
Animation Patterns for Water Effects
Animation Strategy Overview
Different animation techniques have dramatically different performance profiles:
| Technique | Performance | Smoothness | Control | Best For |
|---|---|---|---|---|
| CSS Transform | Excellent | 60fps | Limited | Layer movement |
| CSS Opacity | Excellent | 60fps | Limited | Fade effects |
| requestAnimationFrame | Good | 60fps | Full | Filter param animation |
| SVG animate | Poor | 30-45fps | Declarative | Simple, set-and-forget |
| CSS filter animation | Poor | 30fps | Limited | Avoid |
CSS Transform Animations (Recommended)
Move the water element, not the filter. GPU-accelerated, smooth performance.
Basic Wave Drift
.wave-layer {
width: 200%; /* Double width for seamless loop */
animation: wave-drift 60s linear infinite;
will-change: transform; /* GPU hint */
}
@keyframes wave-drift {
from { transform: translateX(0); }
to { transform: translateX(-50%); } /* Half of doubled width */
}Multi-Axis Wave Motion
.wave {
animation:
wave-drift 60s linear infinite,
wave-bob 4s ease-in-out infinite;
}
@keyframes wave-drift {
from { transform: translateX(0) translateY(0); }
to { transform: translateX(-50%) translateY(0); }
}
@keyframes wave-bob {
0%, 100% { transform: translateX(var(--x, 0)) translateY(0); }
50% { transform: translateX(var(--x, 0)) translateY(-5px); }
}Layer Speed Differentiation
.wave-back {
animation: wave-drift 90s linear infinite; /* Slow - appears far */
}
.wave-mid {
animation: wave-drift 60s linear infinite; /* Medium */
animation-delay: -15s; /* Offset start */
}
.wave-front {
animation: wave-drift 35s linear infinite; /* Fast - appears close */
animation-delay: -25s;
}Parallax on Scroll
let ticking = false;
window.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
const scrollY = window.scrollY;
document.querySelectorAll('.wave').forEach((wave, i) => {
const speed = 0.1 + (i * 0.05); // Different speed per layer
wave.style.transform = `translateY(${scrollY * speed}px)`;
});
ticking = false;
});
ticking = true;
}
});requestAnimationFrame Animation (Best Control)
For animating filter parameters, rAF provides smooth updates:
Basic Wave Animation
const turbulence = document.querySelector('#waterFilter feTurbulence');
let frame = 0;
function animateWater() {
frame += 0.003;
// Sine wave for smooth oscillation
const xFreq = 0.008 + Math.sin(frame) * 0.002;
const yFreq = 0.08 + Math.sin(frame * 0.7) * 0.01;
turbulence.setAttribute('baseFrequency', `${xFreq} ${yFreq}`);
requestAnimationFrame(animateWater);
}
animateWater();Complex Multi-Parameter Animation
class WaterAnimator {
constructor(filterId) {
this.filter = document.getElementById(filterId);
this.turbulence = this.filter.querySelector('feTurbulence');
this.displacement = this.filter.querySelector('feDisplacementMap');
this.frame = 0;
this.baseX = 0.008;
this.baseY = 0.08;
this.baseScale = 25;
this.animate = this.animate.bind(this);
}
animate() {
this.frame += 0.004;
// Wave frequency oscillation
const xFreq = this.baseX + Math.sin(this.frame) * 0.002;
const yFreq = this.baseY + Math.sin(this.frame * 0.7) * 0.01;
this.turbulence.setAttribute('baseFrequency', `${xFreq} ${yFreq}`);
// Displacement scale breathing
const scale = this.baseScale + Math.sin(this.frame * 0.5) * 5;
this.displacement.setAttribute('scale', scale);
requestAnimationFrame(this.animate);
}
start() {
requestAnimationFrame(this.animate);
}
setIntensity(level) {
// 0-1 scale
this.baseScale = 15 + (level * 20);
this.baseY = 0.05 + (level * 0.05);
}
}
const water = new WaterAnimator('oceanFilter');
water.start();Performance-Aware Animation
class AdaptiveWaterAnimator {
constructor(filterId) {
this.turbulence = document.querySelector(`#${filterId} feTurbulence`);
this.frame = 0;
this.fps = 60;
this.lastFrameTime = performance.now();
this.frameCount = 0;
this.animate = this.animate.bind(this);
}
measureFPS() {
const now = performance.now();
const delta = now - this.lastFrameTime;
if (delta >= 1000) {
this.fps = this.frameCount;
this.frameCount = 0;
this.lastFrameTime = now;
// Adapt quality based on FPS
if (this.fps < 30) {
this.reduceQuality();
} else if (this.fps > 55) {
this.increaseQuality();
}
}
this.frameCount++;
}
reduceQuality() {
const currentOctaves = parseInt(this.turbulence.getAttribute('numOctaves'));
if (currentOctaves > 2) {
this.turbulence.setAttribute('numOctaves', currentOctaves - 1);
console.log('Water quality reduced to', currentOctaves - 1, 'octaves');
}
}
increaseQuality() {
const currentOctaves = parseInt(this.turbulence.getAttribute('numOctaves'));
if (currentOctaves < 4) {
this.turbulence.setAttribute('numOctaves', currentOctaves + 1);
}
}
animate() {
this.measureFPS();
this.frame += 0.003;
const xFreq = 0.008 + Math.sin(this.frame) * 0.002;
const yFreq = 0.08 + Math.sin(this.frame * 0.7) * 0.01;
this.turbulence.setAttribute('baseFrequency', `${xFreq} ${yFreq}`);
requestAnimationFrame(this.animate);
}
}SVG animate (Declarative, Use Sparingly)
Warning: animating filter parameters via SVG animate causes full filter recalculation every frame. Use only when necessary.
Basic baseFrequency Animation
<feTurbulence type="turbulence" baseFrequency="0.008 0.08" numOctaves="3">
<animate
attributeName="baseFrequency"
dur="60s"
keyTimes="0;0.5;1"
values="0.008 0.08;0.012 0.12;0.008 0.08"
repeatCount="indefinite"
calcMode="spline"
keySplines="0.4 0 0.6 1;0.4 0 0.6 1"
/>
</feTurbulence>Seed Animation (Lower Cost)
Animating seed is less expensive than baseFrequency:
<feTurbulence type="turbulence" baseFrequency="0.01 0.1" seed="1">
<animate
attributeName="seed"
dur="30s"
values="1;10;20;30;40;30;20;10;1"
repeatCount="indefinite"
/>
</feTurbulence>Multiple Synchronized Animations
<filter id="syncedWater">
<feTurbulence type="turbulence" baseFrequency="0.01 0.1" result="waves">
<animate
attributeName="baseFrequency"
dur="30s"
values="0.008 0.08;0.012 0.12;0.008 0.08"
repeatCount="indefinite"
/>
</feTurbulence>
<feDisplacementMap in="SourceGraphic" in2="waves" scale="25">
<animate
attributeName="scale"
dur="30s"
values="25;35;25"
repeatCount="indefinite"
/>
</feDisplacementMap>
</filter>GSAP Integration (Best of Both Worlds)
// Install: npm install gsap
import { gsap } from 'gsap';
// Smooth filter animation
gsap.to('#waterFilter feTurbulence', {
attr: {
baseFrequency: '0.012 0.12'
},
duration: 10,
ease: 'sine.inOut',
yoyo: true,
repeat: -1
});
// Layer movement
gsap.to('.wave-layer', {
x: '-50%',
duration: 60,
ease: 'none',
repeat: -1
});
// Interactive water response
document.addEventListener('mousemove', (e) => {
const intensity = e.clientY / window.innerHeight;
gsap.to('#waterFilter feDisplacementMap', {
attr: { scale: 15 + (intensity * 20) },
duration: 0.5,
ease: 'power2.out'
});
});Interactive Animations
Mouse Ripple Effect
class RippleEffect {
constructor(container) {
this.container = container;
this.ripples = [];
container.addEventListener('click', (e) => this.addRipple(e));
this.animate = this.animate.bind(this);
requestAnimationFrame(this.animate);
}
addRipple(event) {
const rect = this.container.getBoundingClientRect();
const ripple = {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
radius: 0,
maxRadius: 200,
opacity: 1,
speed: 3
};
this.ripples.push(ripple);
}
animate() {
this.ripples = this.ripples.filter(ripple => {
ripple.radius += ripple.speed;
ripple.opacity = 1 - (ripple.radius / ripple.maxRadius);
return ripple.opacity > 0;
});
this.render();
requestAnimationFrame(this.animate);
}
render() {
// Update displacement based on active ripples
const displacement = document.querySelector('#rippleFilter feDisplacementMap');
const totalIntensity = this.ripples.reduce((sum, r) => sum + r.opacity * 10, 0);
displacement.setAttribute('scale', 20 + totalIntensity);
}
}Scroll-Driven Water Intensity
function setupScrollWater() {
const turbulence = document.querySelector('#waterFilter feTurbulence');
const displacement = document.querySelector('#waterFilter feDisplacementMap');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
const ratio = entry.intersectionRatio;
// More visible = calmer water
const xFreq = 0.008 + (1 - ratio) * 0.004;
const yFreq = 0.08 + (1 - ratio) * 0.04;
const scale = 20 + (1 - ratio) * 15;
turbulence.setAttribute('baseFrequency', `${xFreq} ${yFreq}`);
displacement.setAttribute('scale', scale);
});
}, { threshold: Array.from({ length: 20 }, (_, i) => i / 20) });
observer.observe(document.querySelector('.water-section'));
}Reduced Motion Support
Always respect user preferences:
@media (prefers-reduced-motion: reduce) {
.wave-layer {
animation: none !important;
}
.water-effect {
/* Use static water texture instead */
filter: url(#staticWater);
}
}const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)');
function setupWaterAnimation() {
if (prefersReducedMotion.matches) {
// Static water - no animation
return;
}
// Full animation
const animator = new WaterAnimator('waterFilter');
animator.start();
}
// Listen for preference changes
prefersReducedMotion.addEventListener('change', () => {
location.reload(); // or dynamically toggle
});Animation Timing Functions
Easing for Natural Motion
/* Linear - mechanical, constant speed */
animation-timing-function: linear;
/* Sine - smooth, natural oscillation (best for waves) */
animation-timing-function: cubic-bezier(0.37, 0, 0.63, 1);
/* Ease-in-out - gentle acceleration/deceleration */
animation-timing-function: ease-in-out;
/* Custom wave easing */
animation-timing-function: cubic-bezier(0.45, 0.05, 0.55, 0.95);Multi-Wave Timing
.wave-1 {
animation: drift 60s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.wave-2 {
animation: drift 55s cubic-bezier(0.3, 0, 0.7, 1) infinite;
animation-delay: -20s;
}
.wave-3 {
animation: drift 45s cubic-bezier(0.5, 0, 0.5, 1) infinite;
animation-delay: -35s;
}Performance Monitoring
class WaterPerformanceMonitor {
constructor() {
this.samples = [];
this.maxSamples = 60;
}
recordFrame(duration) {
this.samples.push(duration);
if (this.samples.length > this.maxSamples) {
this.samples.shift();
}
}
getAverageFPS() {
if (this.samples.length === 0) return 60;
const avg = this.samples.reduce((a, b) => a + b) / this.samples.length;
return Math.round(1000 / avg);
}
shouldReduceQuality() {
return this.getAverageFPS() < 30;
}
shouldIncreaseQuality() {
return this.getAverageFPS() > 55;
}
}
// Usage
const monitor = new WaterPerformanceMonitor();
let lastTime = performance.now();
function animate() {
const now = performance.now();
monitor.recordFrame(now - lastTime);
lastTime = now;
if (monitor.shouldReduceQuality()) {
// Reduce octaves, scale, or animation complexity
}
// ... animation code ...
requestAnimationFrame(animate);
}feDisplacementMap and Color Techniques for Water
feDisplacementMap: The Refraction Engine
The displacement map creates the "light bending through water" effect by shifting pixels based on a noise pattern.
Core Formula
For each pixel at position (x, y):
newX = x + scale * (channelValue - 0.5)
newY = y + scale * (channelValue - 0.5)Where channelValue is the value (0-1) of the selected color channel in the noise.
Parameters Deep Dive
<feDisplacementMap
in="SourceGraphic" <!-- What gets distorted -->
in2="waves" <!-- The distortion pattern -->
scale="25" <!-- Distortion intensity -->
xChannelSelector="R" <!-- Which channel moves X -->
yChannelSelector="G" <!-- Which channel moves Y -->
/>Scale Values for Water Types
| Scale | Effect | Water Type |
|---|---|---|
| 5-10 | Subtle shimmer | Glass, light refraction |
| 10-15 | Gentle ripples | Calm pool, aquarium |
| 15-25 | Natural waves | Ocean surface, river |
| 25-35 | Strong distortion | Choppy water, rain |
| 35-50 | Dramatic | Storm waves, splashing |
| 50+ | Unrealistic | Psychedelic (avoid for realism) |
Channel Selector Combinations
The xChannelSelector and yChannelSelector determine which color channels from the noise drive displacement:
| X | Y | Result |
|---|---|---|
| R | G | Standard - Different patterns for X/Y movement |
| R | B | Offset patterns - more chaotic |
| R | R | Same pattern - diagonal movement |
| G | G | Same pattern - different angle |
| A | A | Alpha-driven - if using alpha variations |
Recommendation: Use R and G (or R and B) for water - they're uncorrelated in turbulence output, giving natural two-axis movement.
Displacement Direction
<!-- Horizontal waves (beach) -->
<feDisplacementMap xChannelSelector="R" yChannelSelector="G" scale="30"/>
<!-- Vertical streams (waterfall) -->
<feDisplacementMap xChannelSelector="G" yChannelSelector="R" scale="30"/>
<!-- Circular ripples (pond) -->
<feDisplacementMap xChannelSelector="R" yChannelSelector="R" scale="20"/>Color Manipulation for Water
feComponentTransfer: Color Channel Control
Transform turbulence noise into water-like colors:
<feComponentTransfer in="waves" result="waterColor">
<!-- Reduce red (water absorbs red light first) -->
<feFuncR type="linear" slope="0.3" intercept="0"/>
<!-- Moderate green (underwater green tint) -->
<feFuncG type="linear" slope="0.6" intercept="0.1"/>
<!-- Strong blue (water is blue) -->
<feFuncB type="linear" slope="1.2" intercept="0.2"/>
<!-- Water transparency -->
<feFuncA type="linear" slope="0.7" intercept="0.2"/>
</feComponentTransfer>Function Types
| Type | Formula | Use Case |
|---|---|---|
linear | slope * value + intercept | General adjustment |
gamma | amplitude * pow(value, exponent) + offset | Contrast curves |
table | Lookup table | Complex color mapping |
discrete | Step function | Posterization |
feColorMatrix: Advanced Color Transforms
For underwater color shifts:
<feColorMatrix type="matrix"
values="0.8 0 0 0 0
0 0.9 0 0 0.02
0 0 1.2 0 0.05
0 0 0 1 0"/>
<!-- R G B A offset
↓
Reduces red, slight green boost, strong blue -->Depth-Based Color
Deeper water absorbs more red light:
<!-- Surface water (full color) -->
<feColorMatrix values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
<!-- Mid-depth (less red) -->
<feColorMatrix values="0.7 0 0 0 0 0 0.9 0 0 0 0 0 1 0 0.05 0 0 0 1 0"/>
<!-- Deep water (mostly blue) -->
<feColorMatrix values="0.3 0 0 0 0 0 0.6 0 0 0.02 0 0 1.1 0 0.1 0 0 0 1 0"/>Caustics: Underwater Light Patterns
Caustics are the dappled light patterns on underwater surfaces.
Basic Caustic Pattern
<filter id="caustics">
<feTurbulence
type="turbulence"
baseFrequency="0.02"
numOctaves="3"
result="noise"
/>
<!-- Sharpen the noise into light beams -->
<feComponentTransfer in="noise">
<feFuncR type="gamma" amplitude="2" exponent="3" offset="-0.3"/>
<feFuncG type="gamma" amplitude="2" exponent="3" offset="-0.3"/>
<feFuncB type="gamma" amplitude="2" exponent="3" offset="-0.3"/>
<feFuncA type="linear" slope="1" intercept="0"/>
</feComponentTransfer>
<!-- Soften edges -->
<feGaussianBlur stdDeviation="1"/>
<!-- Blend with source as overlay -->
<feBlend in="SourceGraphic" mode="overlay"/>
</filter>Animated Caustics
<filter id="animatedCaustics">
<feTurbulence type="turbulence" baseFrequency="0.015" numOctaves="3">
<animate
attributeName="seed"
values="1;5;10;15;10;5;1"
dur="20s"
repeatCount="indefinite"
/>
</feTurbulence>
<!-- ... rest of filter -->
</filter>CSS-Based Caustics (Performance Alternative)
.caustics {
background:
radial-gradient(ellipse 20% 30% at 30% 40%, rgba(255,255,255,0.15) 0%, transparent 50%),
radial-gradient(ellipse 25% 20% at 70% 60%, rgba(255,255,255,0.12) 0%, transparent 50%),
radial-gradient(ellipse 15% 25% at 50% 30%, rgba(255,255,255,0.1) 0%, transparent 50%);
animation: caustic-shift 8s ease-in-out infinite;
}
@keyframes caustic-shift {
0%, 100% { background-position: 0 0, 0 0, 0 0; }
33% { background-position: 20px 10px, -15px 5px, 10px -10px; }
66% { background-position: -10px -5px, 10px 15px, -20px 5px; }
}Water Surface Reflections
Basic Reflection Filter
<filter id="waterReflection">
<!-- Distort the reflection -->
<feTurbulence type="turbulence" baseFrequency="0.01 0.05" numOctaves="2"/>
<feDisplacementMap in="SourceGraphic" scale="15"/>
<!-- Fade reflection -->
<feComponentTransfer>
<feFuncA type="linear" slope="0.4" intercept="0"/>
</feComponentTransfer>
<!-- Blur for water surface diffusion -->
<feGaussianBlur stdDeviation="1"/>
</filter>Implementation
<div class="scene">
<div class="sky">
<!-- Content above water -->
</div>
<div class="water">
<div class="reflection" style="transform: scaleY(-1)">
<!-- Mirror of sky content -->
</div>
</div>
</div>
<style>
.reflection {
filter: url(#waterReflection);
opacity: 0.4;
mask-image: linear-gradient(to bottom, black 0%, transparent 100%);
}
</style>Foam and Whitecaps
Foam Generation
<filter id="foam">
<!-- High frequency noise for foam texture -->
<feTurbulence
type="fractalNoise" <!-- fractalNoise for foam (not turbulence) -->
baseFrequency="0.03"
numOctaves="5"
result="foamNoise"
/>
<!-- Threshold to create white spots -->
<feComponentTransfer in="foamNoise">
<feFuncR type="discrete" tableValues="0 0 0.8 1 1"/>
<feFuncG type="discrete" tableValues="0 0 0.8 1 1"/>
<feFuncB type="discrete" tableValues="0 0 0.9 1 1"/>
<feFuncA type="discrete" tableValues="0 0 0 0.3 0.5"/>
</feComponentTransfer>
<!-- Soften foam edges -->
<feGaussianBlur stdDeviation="0.5"/>
</filter>Layered Foam
.foam-layer {
position: absolute;
bottom: 0;
width: 100%;
height: 15%;
background: linear-gradient(to top,
rgba(255,255,255,0.8) 0%,
rgba(255,255,255,0.4) 40%,
transparent 100%
);
filter: url(#foam);
animation: foam-motion 4s ease-in-out infinite;
}
@keyframes foam-motion {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}Compositing Operations
feComposite Operators
| Operator | Effect | Water Use |
|---|---|---|
over | A over B | Layer water on background |
in | A within B shape | Clip water to container |
out | A outside B | Create holes in water |
atop | A atop B | Water texture on shape |
xor | A XOR B | Interesting edge effects |
arithmetic | Custom blend | Complex compositing |
Example: Water with Transparent Areas
<filter id="waterWithHoles">
<feTurbulence type="turbulence" baseFrequency="0.01 0.1" result="waves"/>
<feDisplacementMap in="SourceGraphic" in2="waves" scale="20" result="displaced"/>
<!-- Create mask for transparent areas -->
<feTurbulence type="fractalNoise" baseFrequency="0.005" result="mask"/>
<feComponentTransfer in="mask">
<feFuncA type="discrete" tableValues="0 0 1 1"/>
</feComponentTransfer>
<!-- Apply mask -->
<feComposite in="displaced" operator="in"/>
</filter>Blend Modes for Water
<feBlend in="waterLayer" in2="SourceGraphic" mode="..."/>| Mode | Effect | Best For |
|---|---|---|
normal | Standard layering | Opaque water surface |
multiply | Darkens | Deep water shadows |
screen | Lightens | Foam, highlights |
overlay | Contrast boost | Caustics |
soft-light | Subtle tinting | Color shifts |
color-dodge | Bright highlights | Sparkles |
Example: Complete Water with Multiple Blends
<filter id="fullWater">
<!-- Base water displacement -->
<feTurbulence type="turbulence" baseFrequency="0.008 0.08" result="waves"/>
<feDisplacementMap in="SourceGraphic" in2="waves" scale="20" result="displaced"/>
<!-- Water color overlay -->
<feFlood flood-color="#0369a1" flood-opacity="0.3" result="waterTint"/>
<feBlend in="waterTint" in2="displaced" mode="multiply" result="tinted"/>
<!-- Caustic highlights -->
<feTurbulence type="turbulence" baseFrequency="0.02" result="caustics"/>
<feComponentTransfer in="caustics">
<feFuncR type="gamma" amplitude="2" exponent="3"/>
<feFuncG type="gamma" amplitude="2" exponent="3"/>
<feFuncB type="gamma" amplitude="2" exponent="3"/>
</feComponentTransfer>
<feBlend in2="tinted" mode="overlay" result="withCaustics"/>
<!-- Final output -->
<feComposite in="withCaustics" in2="SourceGraphic" operator="over"/>
</filter>feTurbulence Deep Dive for Water Effects
Understanding the turbulence vs fractalNoise Difference
The type attribute fundamentally changes how the Perlin noise is generated:
type="turbulence"
Output = |noise(x,y)| + |noise(2x,2y)|/2 + |noise(4x,4y)|/4 + ...- Takes absolute value of each octave
- Creates continuous, flowing patterns
- Starts from transparent black and builds up
- Perfect for: water, liquid, flowing effects
type="fractalNoise"
Output = noise(x,y) + noise(2x,2y)/2 + noise(4x,4y)/4 + ...- Allows negative values (which wrap)
- Creates cloud-like, billowy patches
- Starts from solid midtone gray
- Perfect for: clouds, smoke, terrain
Two-Value baseFrequency for Directional Waves
The magic of water effects lies in using different X and Y frequencies:
<feTurbulence baseFrequency="0.01 0.1" />
↑ ↑
X YWhat Each Value Controls
| Frequency | Low (0.005) | High (0.02) |
|---|---|---|
| X (horizontal) | Long, stretched patterns | Short, choppy patterns |
| Y (vertical) | Minimal up/down variation | Strong vertical oscillation |
Common Ratio Patterns
X:Y Ratio Effect Use Case
─────────────────────────────────────────────────────
1:1 Circular ripples Pool, pond, raindrops
1:5 Horizontal ocean swells Deep sea, calm ocean
1:10 Long rolling waves Beach view, shore
1:15 Extreme horizontal stretch Calm lake, mirror surface
5:1 Vertical striations Waterfall, streamingVisual Examples
baseFrequency="0.01 0.01" → ○ ○ ○ (circular ripples)
○ ○ ○
○ ○ ○
baseFrequency="0.01 0.1" → ~~~~~~~~ (ocean waves)
~~~~~~~~
~~~~~~~~
baseFrequency="0.1 0.01" → | | | | (vertical streams)
| | | |
| | | |numOctaves: Detail vs Performance
Each octave adds a layer of detail at half the scale:
Octave 1: ░░░░░░░░ (baseFrequency)
Octave 2: ▒▒▒▒▒▒▒▒ (2x frequency, 1/2 amplitude)
Octave 3: ▓▓▓▓▓▓▓▓ (4x frequency, 1/4 amplitude)
Octave 4: ████████ (8x frequency, 1/8 amplitude)Recommended Values for Water
| numOctaves | Visual Result | CPU Cost | Best For |
|---|---|---|---|
| 1 | Bold, cartoon waves | Minimal | Stylized/game water |
| 2 | Simple, clean ripples | Low | Pool, calm water |
| 3 | Natural ocean surface | Medium | General purpose |
| 4 | Detailed, realistic | High | Hero sections |
| 5+ | Diminishing returns | Very High | Avoid |
Performance Impact
Octaves Relative CPU
────────────────────────
1 1x
2 2x
3 4x
4 8x
5 16x (wasteful)Rule: If you can't see the difference between 4 and 5 octaves, use 4.
Seed: Free Variation
The seed attribute changes the random seed for the noise function. This is computationally free - same performance regardless of value.
<!-- These have identical performance -->
<feTurbulence seed="1" />
<feTurbulence seed="99999" />Using Seed for Layer Variation
<filter id="wave1"><feTurbulence seed="1" /></filter>
<filter id="wave2"><feTurbulence seed="42" /></filter>
<filter id="wave3"><feTurbulence seed="999" /></filter>Seed Animation for Morphing
Animating seed creates a "morphing" effect without the performance hit of baseFrequency animation:
<feTurbulence seed="1">
<animate
attributeName="seed"
values="1;10;20;30;20;10;1"
dur="30s"
repeatCount="indefinite"
/>
</feTurbulence>This creates subtle shape variation without the constant filter recalculation.
stitchTiles: Seamless Tiling
For repeating backgrounds, use stitchTiles="stitch":
<feTurbulence
baseFrequency="0.01 0.1"
stitchTiles="stitch" <!-- Makes pattern tileable -->
/>Warning: This can change the visual output slightly. Test both values.
| Value | Effect |
|---|---|
noStitch (default) | Natural randomness, may show seams when tiled |
stitch | Forces seamless tiling, slightly different pattern |
Advanced: Combining Multiple Turbulence Sources
For complex water, layer different turbulence patterns:
<filter id="complexWater">
<!-- Large swells -->
<feTurbulence
type="turbulence"
baseFrequency="0.003 0.03"
numOctaves="2"
seed="1"
result="swells"
/>
<!-- Surface chop -->
<feTurbulence
type="turbulence"
baseFrequency="0.02 0.15"
numOctaves="3"
seed="2"
result="chop"
/>
<!-- Combine: swells drive major displacement, chop adds detail -->
<feDisplacementMap in="SourceGraphic" in2="swells" scale="30" result="displaced1"/>
<feDisplacementMap in="displaced1" in2="chop" scale="10" result="final"/>
</filter>Debugging Turbulence Output
To see what the raw turbulence looks like:
<filter id="debug">
<feTurbulence type="turbulence" baseFrequency="0.01 0.1" result="noise"/>
<!-- Output noise directly (no source graphic needed) -->
<feColorMatrix in="noise" type="saturate" values="0"/>
</filter>
<rect width="400" height="200" filter="url(#debug)"/>This renders the raw noise pattern, helping you understand what different parameters produce.
Browser Compatibility Notes
| Browser | Support | Notes |
|---|---|---|
| Chrome | Full | Best performance, GPU accelerated |
| Firefox | Full | Good performance |
| Safari | Full | Watch for iOS memory limits |
| Edge | Full | Chromium-based, same as Chrome |
| IE11 | Partial | Avoid animations, may crash |
Safari-Specific Issues
Safari on iOS may have memory issues with:
- numOctaves > 4
- Multiple animated filters
- Large filter regions
Workaround: Detect Safari and reduce complexity:
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
const maxOctaves = isSafari ? 3 : 4;