水波紋進度條,帶有“水波紋”或“扭曲”效果,filter,svg

PixPin_2025-09-05_15-31-32.png

  • 繪製基礎圖形 (HTML/SVG)

    • 我們先用 <svg> 標籤畫出兩個疊在一起的圓環(<circle>):一個作為灰色的背景,另一個作為亮黃色的進度條。
    • 透過 CSS 的 stroke-dasharraystroke-dashoffset 屬性,我們可以精確地控制黃色圓環顯示多少,從而實現進度條功能。
  • 創建“水波紋”濾鏡 (SVG Filter)

    • 這是最關鍵的一步。我們在 SVG 中定義了一個 <filter>
    • 濾鏡內部,首先使用 feTurbulence 標籤生成一張看不見的、類似雲霧或大理石紋理的隨機噪聲圖。這個噪聲圖本身就是動態變化的。
    • 然後,使用 feDisplacementMap 標籤,將這張噪聲圖作為一張“置換地圖”,應用到我們第一步畫的圓環上。它會根據噪聲圖的明暗信息,去扭曲和移動圓環上的每一個點,於是就產生了我們看到的波紋效果。
  • 添加交互控制 (JavaScript)

    • 最後,我們用 JavaScript 監聽幾個 HTML 滑塊(<input type="range">)的變化。
    • 當使用者拖動滑塊時,JS 會即時地去修改 SVG 濾鏡中的各種參數,比如 feTurbulencebaseFrequency(波紋的頻率)和 feDisplacementMapscale(波紋的幅度),讓使用者可以自由定制喜歡的效果。

第二版本-帶進度條邊框寬度版本

image.png

vue3版本

<template>
  <div class="progress-container" :style="containerStyle">
    <svg class="progress-ring" viewBox="0 0 120 120">
      <!-- 背景圓環 -->
      <circle
        class="progress-ring__circle progress-ring__background"
        :style="{ stroke: inactiveColor }"
        :r="radius"
        cx="60"
        cy="60"
      />
      <!-- 進度圓環 -->
      <circle
        class="progress-ring__circle progress-ring__progress"
        :style="{
          stroke: activeColor,
          strokeDashoffset: strokeDashoffset
        }"
        :r="radius"
        cx="60"
        cy="60"
      />
    </svg>
    <div class="progress-text" :style="{ color: textColor }">
      {{ Math.round(progress) }}%
    </div>

    <!-- SVG 濾鏡定義 (在組件內部,不會污染全局) -->
    <svg width="0" height="0" style="position: absolute">
      <filter :id="filterId">
        <feTurbulence
          ref="turbulenceFilter"
          type="fractalNoise"
          :baseFrequency="`${frequency} ${frequency}`"
          :numOctaves="octaves"
          result="turbulenceResult"
        >
          <animate
            attributeName="baseFrequency"
            dur="10s"
            :values="`${frequency} ${frequency};${frequency + 0.03} ${frequency - 0.03};${frequency} ${frequency};`"
            repeatCount="indefinite"
          />
        </feTurbulence>
        <feDisplacementMap
          ref="displacementMapFilter"
          in="SourceGraphic"
          in2="turbulenceResult"
          :scale="scale"
          xChannelSelector="R"
          yChannelSelector="G"
        />
      </filter>
    </svg>
  </div>
</template>

<script setup>
import { computed, ref, watchEffect, onMounted } from 'vue';

// 定義組件接收的 Props
const props = defineProps({
  size: { type: Number, default: 250 },
  progress: { type: Number, default: 50, validator: (v) => v >= 0 && v <= 100 },
  strokeWidth: { type: Number, default: 20 },
  scale: { type: Number, default: 15 },
  frequency: { type: Number, default: 0.05 },
  octaves: { type: Number, default: 2 },
  activeColor: { type: String, default: '#ceff00' },
  inactiveColor: { type: String, default: '#333' },
  textColor: { type: String, default: '#ceff00' },
});

// 生成一個唯一的 ID,避免多個組件實例之間濾鏡衝突
const filterId = `wobble-filter-${Math.random().toString(36).substring(7)}`;

// --- 響應式計算 ---
const radius = 50;
const circumference = 2 * Math.PI * radius;

// 計算進度條的偏移量
const strokeDashoffset = computed(() => {
  return circumference - (props.progress / 100) * circumference;
});

// 計算容器樣式
const containerStyle = computed(() => ({
  width: `${props.size}px`,
  height: `${props.size}px`,
}));

// --- DOM 引用 (雖然Vue會自動更新屬性,但保留引用以備將來更複雜的操作) ---
const turbulenceFilter = ref(null);
const displacementMapFilter = ref(null);

onMounted(() => {
  // 可以在這裡訪問 DOM 元素
  // console.log(turbulenceFilter.value);
});
</script>

<style scoped>
.progress-container {
  position: relative;
  display: inline-block; /* 改為 inline-block 以適應 size prop */
}

.progress-ring {
  width: 100%;
  height: 100%;
  transform: rotate(-90deg);
  /* 動態應用濾鏡 */
  filter: v-bind(`url(#${filterId})`);
}

.progress-ring__circle {
  fill: none;
  stroke-width: v-bind('strokeWidth');
  transition: stroke-dashoffset 0.35s;
  stroke-dasharray: v-bind(`\`${circumference} ${circumference}\``);
}

.progress-ring__progress {
  stroke-linecap: round;
}

.progress-text {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  font-size: v-bind(`\`${size * 0.2}px\``); /* 字體大小與容器大小關聯 */
  font-weight: bold;
}
</style>

react版本公共組件


import React, { useState, useMemo, useId } from 'react';

// --- WavyProgress Component ---
// 將 WavyProgress 組件直接定義在 App.jsx 文件中,以解決導入問題
const WavyProgress = ({
  size = 250,
  progress = 50,
  strokeWidth = 20,
  scale = 15,
  frequency = 0.05,
  octaves = 2,
  activeColor = '#ceff00',
  inactiveColor = '#333',
  textColor = '#ceff00',
}) => {
  const filterId = useId();

  const radius = 50;
  const circumference = 2 * Math.PI * radius;

  const strokeDashoffset = useMemo(() => {
    return circumference - (progress / 100) * circumference;
  }, [progress, circumference]);

  const containerStyle = useMemo(() => ({
    position: 'relative',
    width: `${size}px`,
    height: `${size}px`,
  }), [size]);

  const textStyle = useMemo(() => ({
    color: textColor,
    position: 'absolute',
    top: '50%',
    left: '50%',
    transform: 'translate(-50%, -50%)',
    fontSize: `${size * 0.2}px`,
    fontWeight: 'bold',
  }), [textColor, size]);

  const circleStyle = {
    fill: 'none',
    strokeWidth: strokeWidth,
    transition: 'stroke-dashoffset 0.35s ease',
    strokeDasharray: `${circumference} ${circumference}`,
  };

  return (
    <div style={containerStyle}>
      <svg
        className="progress-ring"
        style={{
          width: '100%',
          height: '100%',
          transform: 'rotate(-90deg)',
          filter: `url(#${filterId})`,
        }}
        viewBox="0 0 120 120"
      >
        <circle
          className="progress-ring__background"
          style={{ ...circleStyle, stroke: inactiveColor }}
          r={radius}
          cx="60"
          cy="60"
        />
        <circle
          className="progress-ring__progress"
          style={{
            ...circleStyle,
            stroke: activeColor,
            strokeDashoffset: strokeDashoffset,
          }}
          r={radius}
          cx="60"
          cy="60"
        />
      </svg>
      <div style={textStyle}>
        {`${Math.round(progress)}%`}
      </div>
      <svg width="0" height="0" style={{ position: 'absolute' }}>
        <filter id={filterId}>
          <feTurbulence
            type="fractalNoise"
            baseFrequency={`${frequency} ${frequency}`}
            numOctaves={octaves}
            result="turbulenceResult"
          >
            <animate
              attributeName="baseFrequency"
              dur="10s"
              values={`${frequency} ${frequency};${frequency + 0.03} ${frequency - 0.03};${frequency} ${frequency};`}
              repeatCount="indefinite"
            />
          </feTurbulence>
          <feDisplacementMap
            in="SourceGraphic"
            in2="turbulenceResult"
            scale={scale}
            xChannelSelector="R"
            yChannelSelector="G"
          />
        </filter>
      </svg>
    </div>
  );
};

// --- App Component ---
// App 組件現在可以直接使用上面的 WavyProgress 組件
const App = () => {
  const [progress, setProgress] = useState(50);
  const [strokeWidth, setStrokeWidth] = useState(20);
  const [scale, setScale] = useState(15);
  const [frequency, setFrequency] = useState(0.05);
  const [octaves, setOctaves] = useState(2);

  // 將 CSS 樣式直接嵌入到組件中
  const styles = `
    body {
      background-color: #1a1a1a;
      margin: 0;
      font-family: Arial, sans-serif;
    }

    #app-container {
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
      flex-direction: column;
      gap: 40px;
    }

    .controls {
      display: flex;
      flex-direction: column;
      gap: 15px;
      background: #2c2c2c;
      padding: 20px;
      border-radius: 8px;
      color: white;
      width: 300px;
    }

    .control-group {
      display: flex;
      flex-direction: column;
      gap: 5px;
    }

    .control-group label {
      display: flex;
      justify-content: space-between;
    }

    input[type="range"] {
      width: 100%;
    }
  `;

  return (
    <>
      <style>{styles}</style>
      <div id="app-container">
        <WavyProgress
          progress={progress}
          strokeWidth={strokeWidth}
          scale={scale}
          frequency={frequency}
          octaves={octaves}
        />
        <div className="controls">
          <div className="control-group">
            <label>進度: <span>{progress}%</span></label>
            <input
              type="range"
              value={progress}
              onChange={(e) => setProgress(Number(e.target.value))}
              min="0"
              max="100"
            />
          </div>
          <div className="control-group">
            <label>邊框寬度: <span>{strokeWidth}</span></label>
            <input
              type="range"
              value={strokeWidth}
              onChange={(e) => setStrokeWidth(Number(e.target.value))}
              min="1"
              max="50"
              step="1"
            />
          </div>
          <div className="control-group">
            <label>波紋幅度 (scale): <span>{scale}</span></label>
            <input
              type="range"
              value={scale}
              onChange={(e) => setScale(Number(e.target.value))}
              min="0"
              max="50"
              step="1"
            />
          </div>
          <div className="control-group">
            <label>波紋頻率 (frequency): <span>{frequency.toFixed(2)}</span></label>
            <input
              type="range"
              value={frequency}
              onChange={(e) => setFrequency(Number(e.target.value))}
              min="0.01"
              max="0.2"
              step="0.01"
            />
          </div>
          <div className="control-group">
            <label>波紋細節 (octaves): <span>{octaves}</span></label>
            <input
              type="range"
              value={octaves}
              onChange={(e) => setOctaves(Number(e.target.value))}
              min="1"
              max="10"
              step="1"
            />
          </div>
        </div>
      </div>
    </>
  );
};

export default App;

---

原文出處:https://juejin.cn/post/7546261151232786458

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝8   💬8   ❤️16
409
🥈
我愛JS
📝1   💬6   ❤️4
90
🥉
酷豪
📝1   ❤️1
49
#4
AppleLily
📝1   💬4   ❤️1
37
#5
💬3  
10
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次