带有“水波纹”或“扭曲”效果的动态边框,进度条

  • 绘制基础图形 (HTML/SVG)

    • 我们先用 <svg> 标签画出两个叠在一起的圆环(<circle>):一个作为灰色的背景,另一个作为亮黄色的进度条。
    • 通过 CSS 的 stroke-dasharraystroke-dashoffset 属性,我们可以精确地控制黄色圆环显示多少,从而实现进度条功能。
  • 创建"水波纹"滤镜 (SVG Filter)

    • 这是最关键的一步。我们在 SVG 中定义了一个 <filter>
    • 滤镜内部,首先使用 feTurbulence 标签生成一张看不见的、类似云雾或大理石纹理的随机噪声图。这个噪声图本身就是动态变化的。
    • 然后,使用 feDisplacementMap 标签,将这张噪声图作为一张"置换地图",应用到我们第一步画的圆环上。它会根据噪声图的明暗信息,去扭曲和移动圆环上的每一个点,于是就产生了我们看到的波纹效果。
  • 添加交互控制 (JavaScript)

    • 最后,我们用 JavaScript 监听几个 HTML 滑块(<input type="range">)的变化。
    • 当用户拖动滑块时,JS 会实时地去修改 SVG 滤镜中的各种参数,比如 feTurbulencebaseFrequency(波纹的频率)和 feDisplacementMapscale(波纹的幅度),让用户可以自由定制喜欢的效果。
xml 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>动态水波纹边框</title>
    <style>
        :root {
            --progress: 50; /* 进度: 0-100 */
            --base-frequency-x: 0.05;
            --base-frequency-y: 0.05;
            --num-octaves: 2;
            --scale: 15;
            --active-color: #ceff00;
            --inactive-color: #333;
            --bg-color: #1a1a1a;
            --text-color: #ceff00;
        }

        body {
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            background-color: var(--bg-color);
            font-family: Arial, sans-serif;
            margin: 0;
            flex-direction: column;
            gap: 40px;
        }

        .progress-container {
            width: 250px;
            height: 250px;
            position: relative;
        }

        .progress-ring {
            width: 100%;
            height: 100%;
            transform: rotate(-90deg); /* 让起点在顶部 */
            filter: url(#wobble-filter); /* 应用SVG滤镜 */
        }

        .progress-ring__circle {
            fill: none;
            stroke-width: 20;
            transition: stroke-dashoffset 0.35s;
        }

        .progress-ring__background {
            stroke: var(--inactive-color);
        }

        .progress-ring__progress {
            stroke: var(--active-color);
            stroke-linecap: round; /* 圆角端点 */
        }

        .progress-text {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: var(--text-color);
            font-size: 50px;
            font-weight: bold;
        }

        .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%;
        }
    </style>
</head>
<body>

    <div class="progress-container">
        <svg class="progress-ring" viewBox="0 0 120 120">
            <!-- 背景圆环 -->
            <circle class="progress-ring__circle progress-ring__background" r="50" cx="60" cy="60"></circle>
            <!-- 进度圆环 -->
            <circle class="progress-ring__circle progress-ring__progress" r="50" cx="60" cy="60"></circle>
        </svg>
        <div class="progress-text">50%</div>
    </div>

    <!-- SVG 滤镜定义 -->
    <svg width="0" height="0">
        <filter id="wobble-filter">
            <!-- 
                feTurbulence: 创建湍流噪声
                - baseFrequency: 噪声的基础频率,值越小,波纹越大越平缓
                - numOctaves: 噪声的倍频数,值越大,细节越多越锐利
                - type: 'fractalNoise' 或 'turbulence'
            -->
            <feTurbulence id="turbulence" type="fractalNoise" baseFrequency="0.05 0.05" numOctaves="2" result="turbulenceResult">
                <!-- 动画:让 turbulence 的基础频率动起来,模拟流动效果 -->
                <animate attributeName="baseFrequency" dur="10s" values="0.05 0.05;0.08 0.02;0.05 0.05;" repeatCount="indefinite"></animate>
            </feTurbulence>
            
            <!-- 
                feDisplacementMap: 用一个图像(这里是上面的噪声)来置换另一个图像
                - in: 输入源,这里是 SourceGraphic,即我们的圆环
                - in2: 置换图源,这里是上面生成的噪声
                - scale: 置换的缩放因子,即波纹的幅度/强度
                - xChannelSelector / yChannelSelector: 指定使用噪声的哪个颜色通道进行置换
            -->
            <feDisplacementMap in="SourceGraphic" in2="turbulenceResult" scale="15" xChannelSelector="R" yChannelSelector="G"></feDisplacementMap>
        </filter>
    </svg>

    <div class="controls">
        <div class="control-group">
            <label for="progress">进度: <span id="progress-value">50%</span></label>
            <input type="range" id="progress" min="0" max="100" value="50">
        </div>
        <div class="control-group">
            <label for="scale">波纹幅度 (scale): <span id="scale-value">15</span></label>
            <input type="range" id="scale" min="0" max="50" value="15" step="1">
        </div>
        <div class="control-group">
            <label for="frequency">波纹频率 (baseFrequency): <span id="frequency-value">0.05</span></label>
            <input type="range" id="frequency" min="0.01" max="0.2" value="0.05" step="0.01">
        </div>
         <div class="control-group">
            <label for="octaves">波纹细节 (numOctaves): <span id="octaves-value">2</span></label>
            <input type="range" id="octaves" min="1" max="10" value="2" step="1">
        </div>
    </div>

    <script>
        const root = document.documentElement;
        const progressCircle = document.querySelector('.progress-ring__progress');
        const progressText = document.querySelector('.progress-text');
        const radius = progressCircle.r.baseVal.value;
        const circumference = 2 * Math.PI * radius;

        progressCircle.style.strokeDasharray = `${circumference} ${circumference}`;

        function setProgress(percent) {
            const offset = circumference - (percent / 100) * circumference;
            progressCircle.style.strokeDashoffset = offset;
            progressText.textContent = `${Math.round(percent)}%`;
            root.style.setProperty('--progress', percent);
        }

        // --- 控制器逻辑 ---
        const progressSlider = document.getElementById('progress');
        const scaleSlider = document.getElementById('scale');
        const frequencySlider = document.getElementById('frequency');
        const octavesSlider = document.getElementById('octaves');
        
        const progressValue = document.getElementById('progress-value');
        const scaleValue = document.getElementById('scale-value');
        const frequencyValue = document.getElementById('frequency-value');
        const octavesValue = document.getElementById('octaves-value');

        const turbulence = document.getElementById('turbulence');
        const displacementMap = document.querySelector('feDisplacementMap');

        progressSlider.addEventListener('input', (e) => {
            const value = e.target.value;
            setProgress(value);
            progressValue.textContent = `${value}%`;
        });

        scaleSlider.addEventListener('input', (e) => {
            const value = e.target.value;
            displacementMap.setAttribute('scale', value);
            scaleValue.textContent = value;
        });

        frequencySlider.addEventListener('input', (e) => {
            const value = e.target.value;
            turbulence.setAttribute('baseFrequency', `${value} ${value}`);
            frequencyValue.textContent = value;
        });
        
        octavesSlider.addEventListener('input', (e) => {
            const value = e.target.value;
            turbulence.setAttribute('numOctaves', value);
            octavesValue.textContent = value;
        });

        // 初始化
        setProgress(50);
    </script>

</body>
</html>

第二版本-带进度条边框宽度版本

xml 复制代码
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>动态水波纹边框</title>
    <style>
        :root {
            --progress: 50; /* 进度: 0-100 */
            --stroke-width: 20; /* 边框宽度 */
            --base-frequency-x: 0.05;
            --base-frequency-y: 0.05;
            --num-octaves: 2;
            --scale: 15;
            --active-color: #ceff00;
            --inactive-color: #333;
            --bg-color: #1a1a1a;
            --text-color: #ceff00;
        }

        body {
            display: flex;
            justify-content: center;
            align-items: center;
            min-height: 100vh;
            background-color: var(--bg-color);
            font-family: Arial, sans-serif;
            margin: 0;
            flex-direction: column;
            gap: 40px;
        }

        .progress-container {
            width: 250px;
            height: 250px;
            position: relative;
        }

        .progress-ring {
            width: 100%;
            height: 100%;
            transform: rotate(-90deg); /* 让起点在顶部 */
            filter: url(#wobble-filter); /* 应用SVG滤镜 */
        }

        .progress-ring__circle {
            fill: none;
            stroke-width: var(--stroke-width);
            transition: stroke-dashoffset 0.35s;
        }

        .progress-ring__background {
            stroke: var(--inactive-color);
        }

        .progress-ring__progress {
            stroke: var(--active-color);
            stroke-linecap: round; /* 圆角端点 */
        }

        .progress-text {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            color: var(--text-color);
            font-size: 50px;
            font-weight: bold;
        }

        .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%;
        }
    </style>
</head>
<body>

    <div class="progress-container">
        <svg class="progress-ring" viewBox="0 0 120 120">
            <!-- 背景圆环 -->
            <circle class="progress-ring__circle progress-ring__background" r="50" cx="60" cy="60"></circle>
            <!-- 进度圆环 -->
            <circle class="progress-ring__circle progress-ring__progress" r="50" cx="60" cy="60"></circle>
        </svg>
        <div class="progress-text">50%</div>
    </div>

    <!-- SVG 滤镜定义 -->
    <svg width="0" height="0">
        <filter id="wobble-filter">
            <!-- 
                feTurbulence: 创建湍流噪声
                - baseFrequency: 噪声的基础频率,值越小,波纹越大越平缓
                - numOctaves: 噪声的倍频数,值越大,细节越多越锐利
                - type: 'fractalNoise' 或 'turbulence'
            -->
            <feTurbulence id="turbulence" type="fractalNoise" baseFrequency="0.05 0.05" numOctaves="2" result="turbulenceResult">
                <!-- 动画:让 turbulence 的基础频率动起来,模拟流动效果 -->
                <animate attributeName="baseFrequency" dur="10s" values="0.05 0.05;0.08 0.02;0.05 0.05;" repeatCount="indefinite"></animate>
            </feTurbulence>
            
            <!-- 
                feDisplacementMap: 用一个图像(这里是上面的噪声)来置换另一个图像
                - in: 输入源,这里是 SourceGraphic,即我们的圆环
                - in2: 置换图源,这里是上面生成的噪声
                - scale: 置换的缩放因子,即波纹的幅度/强度
                - xChannelSelector / yChannelSelector: 指定使用噪声的哪个颜色通道进行置换
            -->
            <feDisplacementMap in="SourceGraphic" in2="turbulenceResult" scale="15" xChannelSelector="R" yChannelSelector="G"></feDisplacementMap>
        </filter>
    </svg>

    <div class="controls">
        <div class="control-group">
            <label for="progress">进度: <span id="progress-value">50%</span></label>
            <input type="range" id="progress" min="0" max="100" value="50">
        </div>
        <div class="control-group">
            <label for="stroke-width">边框宽度: <span id="stroke-width-value">20</span></label>
            <input type="range" id="stroke-width" min="1" max="50" value="20" step="1">
        </div>
        <div class="control-group">
            <label for="scale">波纹幅度 (scale): <span id="scale-value">15</span></label>
            <input type="range" id="scale" min="0" max="50" value="15" step="1">
        </div>
        <div class="control-group">
            <label for="frequency">波纹频率 (baseFrequency): <span id="frequency-value">0.05</span></label>
            <input type="range" id="frequency" min="0.01" max="0.2" value="0.05" step="0.01">
        </div>
         <div class="control-group">
            <label for="octaves">波纹细节 (numOctaves): <span id="octaves-value">2</span></label>
            <input type="range" id="octaves" min="1" max="10" value="2" step="1">
        </div>
    </div>

    <script>
        const root = document.documentElement;
        const progressCircle = document.querySelector('.progress-ring__progress');
        const progressText = document.querySelector('.progress-text');
        const radius = progressCircle.r.baseVal.value;
        const circumference = 2 * Math.PI * radius;

        progressCircle.style.strokeDasharray = `${circumference} ${circumference}`;

        function setProgress(percent) {
            const offset = circumference - (percent / 100) * circumference;
            progressCircle.style.strokeDashoffset = offset;
            progressText.textContent = `${Math.round(percent)}%`;
            root.style.setProperty('--progress', percent);
        }

        // --- 控制器逻辑 ---
        const progressSlider = document.getElementById('progress');
        const strokeWidthSlider = document.getElementById('stroke-width');
        const scaleSlider = document.getElementById('scale');
        const frequencySlider = document.getElementById('frequency');
        const octavesSlider = document.getElementById('octaves');
        
        const progressValue = document.getElementById('progress-value');
        const strokeWidthValue = document.getElementById('stroke-width-value');
        const scaleValue = document.getElementById('scale-value');
        const frequencyValue = document.getElementById('frequency-value');
        const octavesValue = document.getElementById('octaves-value');

        const turbulence = document.getElementById('turbulence');
        const displacementMap = document.querySelector('feDisplacementMap');

        progressSlider.addEventListener('input', (e) => {
            const value = e.target.value;
            setProgress(value);
            progressValue.textContent = `${value}%`;
        });
        
        strokeWidthSlider.addEventListener('input', (e) => {
            const value = e.target.value;
            root.style.setProperty('--stroke-width', value);
            strokeWidthValue.textContent = value;
        });

        scaleSlider.addEventListener('input', (e) => {
            const value = e.target.value;
            displacementMap.setAttribute('scale', value);
            scaleValue.textContent = value;
        });

        frequencySlider.addEventListener('input', (e) => {
            const value = e.target.value;
            turbulence.setAttribute('baseFrequency', `${value} ${value}`);
            frequencyValue.textContent = value;
        });
        
        octavesSlider.addEventListener('input', (e) => {
            const value = e.target.value;
            turbulence.setAttribute('numOctaves', value);
            octavesValue.textContent = value;
        });

        // 初始化
        setProgress(50);
    </script>

</body>
</html>
相关推荐
weixin_382395234 小时前
为小工厂量身打造:本地部署的物料管理系统带缺料计算
前端·制造
__zRainy__4 小时前
解决pnpm v10+不自动构建
前端·pnpm·工程化
猫猫不是喵喵.4 小时前
Vue3 Props 属性
前端·javascript·vue.js
醉城夜风~5 小时前
CSS元素显示模式(display)
前端·css
AI大模型-小华6 小时前
Codex 三方充值快速入门指南
java·前端·数据库·chatgpt·ai编程·codex·chatgpt pro
做前端的娜娜子8 小时前
同一链接实现 PC Web 与移动 H5 自适应
前端·掘金·金石计划
小帅不太帅9 小时前
架构没变、规模没变,DeepSeek V4 Flash 正式版凭什么暴涨 47 分?
前端·aigc·deepseek
jarvisuni9 小时前
DeepSeekFlash前端依旧拉垮,而且变慢了很多!
前端·javascript·算法
卷福同学10 小时前
AI编程出海第二步:验证关键词能否做站
前端·人工智能·后端