mcpField
W
web-wave-designer

web-wave-designer

github.com/curiositechデザイン・UI257202 回インストール2 日前に更新
リポジトリ

web-wave-designer について

web-wave-designerは、SVGフィルターやCSSアニメーション、レイヤー構成で水の表現を作るWeb開発者向けスキルです。海のうねりから水中の屈折までを扱い、パフォーマンス面の指針も提供します。

主な機能

  1. 01SVGの乱流・変位フィルター
  2. 02波や波紋の作成パターン
  3. 03水中屈折と泡の表現
  4. 04CSS・JavaScriptアニメーション
  5. 05レイヤー設計と性能調整

活用シーン

  1. 01海の背景や岸辺の波の制作
  2. 02水越しに見えるコンテンツの歪み
  3. 03池の波紋やリキッドガラスUIの設計

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/curiositech/some_claude_skills --skill web-wave-designer

インストール後、Claude Code で/web-wave-designerを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

CHANGELOG.md
.claude-plugin/
plugin.json
references/
animation-patterns.md
displacement-and-color.md
turbulence-deep-dive.md

詳細

Web Wave Designer

SVG フィルター、CSS アニメーション、レイヤリング技法を使用して、Web アプリケーション向けのリアルで高性能な海や水の波のエフェクトを作成するエキスパートです。穏やかなさざ波からダイナミックな大波まで、水をテーマにしたビジュアルを専門とし、特に水中での光の屈折に関する知識が豊富です。

このスキルを使う場面

次の用途に使用します:

  • 海の波の背景や海景
  • 水中の歪み/屈折エフェクト
  • 泡を伴う浜辺の波
  • 池やプールの波紋アニメーション
  • リキッドガラスの UI エフェクト
  • 水をテーマにした読み込み状態
  • 奥行きのある視差効果付きの海のレイヤー
  • ゲーム向けの様式化された水や漫画風の水
  • 水面の反射エフェクト

次の用途には使用しないでください:

  • 3D のボリュームを持つ海のレンダリング → WebGL/Three.js/Ocean.js を使用してください
  • リアルタイムの流体シミュレーション → canvas の物理エンジンを使用してください
  • 動画エフェクト → 動画編集ソフトウェアを使用してください
  • 動きのない単純な青いグラデーション
  • 水滴の物理シミュレーション → パーティクルシステムを使用してください

重要な違い:turbulence と fractalNoise

重要:水のエフェクトには type="turbulence" を使用してください(雲のような fractalNoise は使用しません)。

タイプビジュアル最適な用途
turbulence連続した流れのパターン。透明な黒から始まります水、波、液体
fractalNoiseランダムな雲状のパッチ。不透明です雲、煙、地形

<feTurbulence type="turbulence" baseFrequency="0.01 0.1" />


<feTurbulence type="fractalNoise" baseFrequency="0.01" />

水のための SVG フィルターパイプライン

基本的な水のエフェクトを作るフィルターチェーン:

Source -> feTurbulence -> feDisplacementMap -> feComponentTransfer -> feComposite
            (waves)        (distortion)         (color/opacity)      (blend)
1. feTurbulence - 波のパターン生成
<feTurbulence
  type="turbulence"           
  baseFrequency="0.01 0.1"    
  numOctaves="3"              
  seed="42"                   
  result="waves"
/>

baseFrequency の説明(2 つの値):

X 周波数Y 周波数結果
0.010.1縦方向に揺らぐ、長い横波
0.0050.05深い海のうねり
0.020.15荒い水面の波
0.030.03四角い波紋(池)

比率が重要です:

  • X が Y よりかなり小さい:横に引き伸ばされた波(海)
  • X == Y:円形の波紋(池、プール)
  • X が Y よりかなり大きい:縦方向の筋(水しぶき)
2. feDisplacementMap - 屈折エフェクト

水の背後にあるコンテンツが波打って見えるように、屈折や歪みを作り出します。

<feDisplacementMap
  in="SourceGraphic"          
  in2="waves"                 
  scale="20"                  
  xChannelSelector="R"        
  yChannelSelector="G"        
  result="refracted"
/>
Scale の値効果
10-15プールの穏やかな波紋
15-25標準的な水の屈折
25-40強い波の歪み
40+サイケデリック(非現実的)
3. feComponentTransfer - 水の色

チャンネルを操作して、ノイズを水のような色に変換します。

<feComponentTransfer in="waves" result="waterColor">
  <feFuncR type="linear" slope="0.3" intercept="0"/>     
  <feFuncG type="linear" slope="0.7" intercept="0.2"/>   
  <feFuncB type="linear" slope="1.2" intercept="0.3"/>   
  <feFuncA type="linear" slope="0.6" intercept="0.3"/>   
</feComponentTransfer>
4. feGaussianBlur - コースティクス(水中の光)
<feGaussianBlur
  in="waves"
  stdDeviation="2"            
  result="caustics"
/>
5. 合成 - レイヤーの組み立て
<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"/>

波の種類別レシピ

海面の波
<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>
池の波紋(円形)
<filter id="pondRipples">
  <feTurbulence type="turbulence"
                baseFrequency="0.02 0.02"  
                numOctaves="2"
                seed="10"
                result="ripples"/>
  <feDisplacementMap in="SourceGraphic" in2="ripples" scale="12"/>
</filter>
浜辺の波(砕ける波)
<filter id="shoreWaves">
  <feTurbulence type="turbulence"
                baseFrequency="0.008 0.12"  
                numOctaves="3"
                seed="5"
                result="waves"/>
  <feGaussianBlur in="waves" stdDeviation="1.5" result="softWaves"/>
  <feDisplacementMap in="SourceGraphic" in2="softWaves" scale="30"/>
  
  <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>
水中の歪み(水越しに見る効果)
<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"/>
  
  <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>
リキッドガラスエフェクト(モダンな 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"/>
  
  <feGaussianBlur stdDeviation="0.5"/>
</filter>
様式化された波/漫画風の波
<filter id="cartoonWater">
  <feTurbulence type="turbulence"
                baseFrequency="0.02 0.08"
                numOctaves="1"  
                result="waves"/>
  <feDisplacementMap in="SourceGraphic" in2="waves" scale="15"/>
  
</filter>

アニメーション技法

JavaScript requestAnimationFrame(最も滑らか)
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(宣言的、CPU 負荷が高い)
<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>

警告:フィルター属性に対する SVG animate は、毎フレーム、フィルター全体を再計算することになります。使用は控えめにしてください。

CSS Transform アニメーション(最高のパフォーマンス)

フィルターではなく、水の要素を動かします:

.wave-layer {
  animation: wave-drift 20s linear infinite;
}

@keyframes wave-drift {
  from { transform: translateX(0) translateY(0); }
  to { transform: translateX(-50%) translateY(5px); }
}
シードアニメーション(波形の変化)

baseFrequency の負荷をかけずに形状を変化させるには、seed をアニメーションさせます:

<feTurbulence baseFrequency="0.01 0.1">
  <animate
    attributeName="seed"
    dur="20s"
    values="1;50;100;50;1"
    repeatCount="indefinite"
  />
</feTurbulence>

レイヤリング戦略

複数レイヤーの海
<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%); }
}
レイヤーパラメーターガイド
レイヤー不透明度速度フィルター ScalebaseFrequency
背面(深層)0.2-0.480-100s150.004 0.04
中間0.4-0.650-70s200.006 0.06
前面(水面)0.6-0.830-45s250.01 0.1
泡0.7-0.925-35s100.02 0.02

カラーパレット

深海
.deep-ocean {
  background: linear-gradient(180deg,
    #0c4a6e 0%,    /* Deep blue */
    #075985 30%,
    #0369a1 60%,
    #0284c7 100%   /* Surface shimmer */
  );
}
  • 基本色:#0369a1
  • 深色:#0c4a6e
  • ハイライト:#38bdf8
熱帯/カリブ海
.tropical {
  background: linear-gradient(180deg,
    #06b6d4 0%,    /* Cyan surface */
    #22d3ee 40%,
    #67e8f9 70%,
    #a5f3fc 100%   /* Shallow sand reflection */
  );
}
  • 基本色:#06b6d4
  • 浅瀬:#67e8f9
  • 泡:#ecfeff
荒れた海
.stormy {
  background: linear-gradient(180deg,
    #1e293b 0%,    /* Dark clouds */
    #334155 30%,
    #475569 60%,
    #64748b 100%   /* Whitecaps */
  );
}
  • 基本色:#475569
  • 深色:#1e293b
  • 白波:#cbd5e1
夕暮れの反射
.sunset-water {
  background: linear-gradient(180deg,
    #831843 0%,    /* Pink sky */
    #9d174d 20%,
    #be185d 40%,
    #0369a1 60%,   /* Water starts */
    #0c4a6e 100%
  );
}

完全な実装テンプレート

テンプレート 1:海のシーン全体
<!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>
テンプレート 2:水中ビュー(水面の奥にコンテンツを表示)
<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>
テンプレート 3:React の水コンポーネント
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>
テンプレート 4:CSS のみの波(SVG 不使用)

SVG フィルターを使わずに、シンプルで高パフォーマンスな波を作成するには:

.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);
  }
}

パフォーマンスの最適化

重要なルール
  1. type="turbulence" を使用する - 水に適したタイプです(fractalNoise ではありません)
  2. numOctaves は 4 以下にする - 4 を超えても視覚的な向上はわずかですが、CPU 負荷は指数関数的に増加します
  3. Scale は 10~30 にする - 40 を超えると不自然になり、処理も遅くなります
  4. baseFrequency のアニメーションは避ける - 代わりに CSS トランスフォームまたは seed アニメーションを使用します
  5. GPU ヒントを使う - アニメーションするレイヤーに will-change: transform を追加します
  6. SVG の defs をまとめる - <defs> ブロックを 1 つにし、複数のフィルターを配置します
パフォーマンスの段階
段階手法目標 FPS使用例
UltraCSS の放射状グラデーションのみ60fpsモバイル、ローエンド端末
HighSVG フィルター、CSS トランスフォームアニメーション60fps背景の波
MediumSVG フィルター + seed アニメーション45-60fpsインタラクティブな水
LowSVG フィルター + baseFrequency アニメーション30-40fpsヒーローセクションのみ
モバイル向けの最適化
@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); }
}
パフォーマンスの検出
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);
}

フレームワークとの統合

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>

デバッグのヒント

フィルターパイプラインを可視化する

<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>
よくある問題
問題原因解決策
効果が消えるフィルター領域が小さすぎるx="-20%" y="-20%" width="140%" height="140%" を追加します
波が四角い/箱状になるfractalNoise を使用しているtype="turbulence" に変更します
波が均一すぎるすべてのレイヤーで同じ seed を使用している異なる seed 値を使用します
水平方向に動かないbaseFrequency の値が同じbaseFrequency="0.01 0.1" を使用します(x/y に異なる値を設定)
アニメーションがカクつくフィルター属性をアニメーションしている代わりに CSS トランスフォームアニメーションを使用します
エッジに乱れがある境界で変位が発生しているx/y/width/height を使ってフィルター領域を広げます
ブラウザーの DevTools
  1. Elements パネル > SVG フィルターを選択 > 属性を確認
  2. Performance パネル > 記録 > レイアウトの過剰な再計算がないか確認
  3. Layers パネル(Chrome)> 波のレイヤーで GPU アクセラレーションが有効か確認

web-cloud-designer との統合

完全な大気のシーンを作成するには、水と雲の効果を組み合わせます:

<div class="scene">
  
  <div class="sky">
    <div class="cloud-layer cloud-back"></div>
    <div class="cloud-layer cloud-front"></div>
  </div>

  
  <div class="horizon"></div>

  
  <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>

参考資料

  • Red Stapler:「Realistic Water Effect SVG Turbulence」
  • Mitkov Systems:「Liquid Glass Water Animation」(2025)
  • O'Reilly SVG Book:feTurbulence の章
  • MDN:SVG フィルタープリミティブのドキュメント
  • CSS-Tricks:「Underwater Blur Effect」
  • Codrops:「Water Distortion Effect」

水は、あらゆる自然の原動力です。 - レオナルド・ダ・ヴィンチ

コメント