全国免费咨询:

13245491521

VR图标白色 VR图标黑色
X

中高端软件定制开发服务商

与我们取得联系

13245491521     13245491521

2025-09-27_水波纹进度条,带有“水波纹”或“扭曲”效果,filter,svg

您的位置:首页 >> 新闻 >> 行业资讯

水波纹进度条,带有“水波纹”或“扭曲”效果,filter,svg 点击关注公众号,“技术干货” 及时达!「绘制基础图形 (HTML/SVG)」 我们先用svg标签画出两个叠在一起的圆环(circle):一个作为灰色的背景,另一个作为亮黄色的进度条。通过 CSS 的stroke-dasharray和stroke-dashoffset属性,我们可以精确地控制黄色圆环显示多少,从而实现进度条功能。「创建 “水波纹” 滤镜 (SVG Filter)」 这是最关键的一步。我们在 SVG 中定义了一个filter。滤镜内部,首先使用「feTurbulence」标签生成一张看不见的、类似云雾或大理石纹理的「随机噪声图」。这个噪声图本身就是动态变化的。然后,使用「feDisplacementMap」标签,将这张噪声图作为一张 “置换地图”,应用到我们第一步画的圆环上。它会根据噪声图的明暗信息,去「扭曲和移动」圆环上的每一个点,于是就产生了我们看到的波纹效果。「添加交互控制 (JavaScript)」 最后,我们用 JavaScript 监听几个 HTML 滑块(input type="range")的变化。当用户拖动滑块时,JS 会实时地去修改 SVG 滤镜中的各种参数,比如feTurbulence的baseFrequency(波纹的频率)和feDisplacementMap的scale(波纹的幅度),让用户可以自由定制喜欢的效果。!DOCTYPEhtmlhtmllang="zh"head metacharset="UTF-8" meta 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-dashoffset0.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-grouplabel{ display: flex; justify-content: space-between; } input[type="range"]{ width:100%; } /style/headbody divclass="progress-container" svgclass="progress-ring"viewBox="0 0 120 120" !-- 背景圆环 -- circleclass="progress-ring__circle progress-ring__background"r="50"cx="60"cy="60"/circle !-- 进度圆环 -- circleclass="progress-ring__circle progress-ring__progress"r="50"cx="60"cy="60"/circle /svg divclass="progress-text"50%/div /div !-- SVG 滤镜定义 -- svgwidth="0"height="0" filterid="wobble-filter" !-- feTurbulence: 创建湍流噪声 - baseFrequency: 噪声的基础频率,值越小,波纹越大越平缓 - numOctaves: 噪声的倍频数,值越大,细节越多越锐利 - type: 'fractalNoise' 或 'turbulence' -- feTurbulenceid="turbulence"type="fractalNoise"baseFrequency="0.05 0.05"numOctaves="2"result="turbulenceResult" !-- 动画:让 turbulence 的基础频率动起来,模拟流动效果 -- animateattribute/animate /feTurbulence !-- feDisplacementMap: 用一个图像(这里是上面的噪声)来置换另一个图像 - in: 输入源,这里是 SourceGraphic,即我们的圆环 - in2: 置换图源,这里是上面生成的噪声 - scale: 置换的缩放因子,即波纹的幅度/强度 - xChannelSelector / yChannelSelector: 指定使用噪声的哪个颜色通道进行置换 -- feDisplacementMapin="SourceGraphic"in2="turbulenceResult"scale="15"xChannelSelector="R"yChannelSelector="G"/feDisplacementMap /filter /svg divclass="controls" divclass="control-group" labelfor="progress"进度:spanid="progress-value"50%/span/label inputtype="range"id="progress"min="0"max="100"value="50" /div divclass="control-group" labelfor="scale"波纹幅度 (scale):spanid="scale-value"15/span/label inputtype="range"id="scale"min="0"max="50"value="15"step="1" /div divclass="control-group" labelfor="frequency"波纹频率 (baseFrequency):spanid="frequency-value"0.05/span/label inputtype="range"id="frequency"min="0.01"max="0.2"value="0.05"step="0.01" /div divclass="control-group" labelfor="octaves"波纹细节 (numOctaves):spanid="octaves-value"2/span/label inputtype="range"id="octaves"min="1"max="10"value="2"step="1" /div /div script constroot =document.documentElement; constprogressCircle =document.querySelector('.progress-ring__progress'); constprogressText =document.querySelector('.progress-text'); constradius = progressCircle.r.baseVal.value; constcircumference =2*Math.PI* radius; progressCircle.style.strokeDasharray=`${circumference}${circumference}`; functionsetProgress(percent) { constoffset = circumference - (percent /100) * circumference; progressCircle.style.strokeDashoffset= offset; progressText.textContent=`${Math.round(percent)}%`; root.style.setProperty('--progress', percent); } // --- 控制器逻辑 --- constprogressSlider =document.getElementById('progress'); constscaleSlider =document.getElementById('scale'); constfrequencySlider =document.getElementById('frequency'); constoctavesSlider =document.getElementById('octaves'); constprogressValue =document.getElementById('progress-value'); constscaleValue =document.getElementById('scale-value'); constfrequencyValue =document.getElementById('frequency-value'); constoctavesValue =document.getElementById('octaves-value'); constturbulence =document.getElementById('turbulence'); constdisplacementMap =document.querySelector('feDisplacementMap'); progressSlider.addEventListener('input',(e) ={ constvalue = e.target.value; setProgress(value); progressValue.textContent=`${value}%`; scaleSlider.addEventListener('input',(e) ={ constvalue = e.target.value; displacementMap.setAttribute('scale', value); scaleValue.textContent= value; frequencySlider.addEventListener('input',(e) ={ constvalue = e.target.value; turbulence.setAttribute('baseFrequency',`${value}${value}`); frequencyValue.textContent= value; octavesSlider.addEventListener('input',(e) ={ constvalue = e.target.value; turbulence.setAttribute('numOctaves', value); octavesValue.textContent= value; // 初始化 setProgress(50); /script /body/html第二版本 - 带进度条边框宽度版本!DOCTYPEhtmlhtmllang="zh"head metacharset="UTF-8" meta 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-dashoffset0.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-grouplabel{ display: flex; justify-content: space-between; } input[type="range"]{ width:100%; } /style/headbody divclass="progress-container" svgclass="progress-ring"viewBox="0 0 120 120" !-- 背景圆环 -- circleclass="progress-ring__circle progress-ring__background"r="50"cx="60"cy="60"/circle !-- 进度圆环 -- circleclass="progress-ring__circle progress-ring__progress"r="50"cx="60"cy="60"/circle /svg divclass="progress-text"50%/div /div !-- SVG 滤镜定义 -- svgwidth="0"height="0" filterid="wobble-filter" !-- feTurbulence: 创建湍流噪声 - baseFrequency: 噪声的基础频率,值越小,波纹越大越平缓 - numOctaves: 噪声的倍频数,值越大,细节越多越锐利 - type: 'fractalNoise' 或 'turbulence' -- feTurbulenceid="turbulence"type="fractalNoise"baseFrequency="0.05 0.05"numOctaves="2"result="turbulenceResult" !-- 动画:让 turbulence 的基础频率动起来,模拟流动效果 -- animateattribute/animate /feTurbulence !-- feDisplacementMap: 用一个图像(这里是上面的噪声)来置换另一个图像 - in: 输入源,这里是 SourceGraphic,即我们的圆环 - in2: 置换图源,这里是上面生成的噪声 - scale: 置换的缩放因子,即波纹的幅度/强度 - xChannelSelector / yChannelSelector: 指定使用噪声的哪个颜色通道进行置换 -- feDisplacementMapin="SourceGraphic"in2="turbulenceResult"scale="15"xChannelSelector="R"yChannelSelector="G"/feDisplacementMap /filter /svg divclass="controls" divclass="control-group" labelfor="progress"进度:spanid="progress-value"50%/span/label inputtype="range"id="progress"min="0"max="100"value="50" /div divclass="control-group" labelfor="stroke-width"边框宽度:spanid="stroke-width-value"20/span/label inputtype="range"id="stroke-width"min="1"max="50"value="20"step="1" /div divclass="control-group" labelfor="scale"波纹幅度 (scale):spanid="scale-value"15/span/label inputtype="range"id="scale"min="0"max="50"value="15"step="1" /div divclass="control-group" labelfor="frequency"波纹频率 (baseFrequency):spanid="frequency-value"0.05/span/label inputtype="range"id="frequency"min="0.01"max="0.2"value="0.05"step="0.01" /div divclass="control-group" labelfor="octaves"波纹细节 (numOctaves):spanid="octaves-value"2/span/label inputtype="range"id="octaves"min="1"max="10"value="2"step="1" /div /div script constroot =document.documentElement; constprogressCircle =document.querySelector('.progress-ring__progress'); constprogressText =document.querySelector('.progress-text'); constradius = progressCircle.r.baseVal.value; constcircumference =2*Math.PI* radius; progressCircle.style.strokeDasharray=`${circumference}${circumference}`; functionsetProgress(percent) { constoffset = circumference - (percent /100) * circumference; progressCircle.style.strokeDashoffset= offset; progressText.textContent=`${Math.round(percent)}%`; root.style.setProperty('--progress', percent); } // --- 控制器逻辑 --- constprogressSlider =document.getElementById('progress'); conststrokeWidthSlider =document.getElementById('stroke-width'); constscaleSlider =document.getElementById('scale'); constfrequencySlider =document.getElementById('frequency'); constoctavesSlider =document.getElementById('octaves'); constprogressValue =document.getElementById('progress-value'); conststrokeWidthValue =document.getElementById('stroke-width-value'); constscaleValue =document.getElementById('scale-value'); constfrequencyValue =document.getElementById('frequency-value'); constoctavesValue =document.getElementById('octaves-value'); constturbulence =document.getElementById('turbulence'); constdisplacementMap =document.querySelector('feDisplacementMap'); progressSlider.addEventListener('input',(e) ={ constvalue = e.target.value; setProgress(value); progressValue.textContent=`${value}%`; strokeWidthSlider.addEventListener('input',(e) ={ constvalue = e.target.value; root.style.setProperty('--stroke-width', value); strokeWidthValue.textContent= value; scaleSlider.addEventListener('input',(e) ={ constvalue = e.target.value; displacementMap.setAttribute('scale', value); scaleValue.textContent= value; frequencySlider.addEventListener('input',(e) ={ constvalue = e.target.value; turbulence.setAttribute('baseFrequency',`${value}${value}`); frequencyValue.textContent= value; octavesSlider.addEventListener('input',(e) ={ constvalue = e.target.value; turbulence.setAttribute('numOctaves', value); octavesValue.textContent= value; // 初始化 setProgress(50); /script /body/htmlvue3 版本templatedivclass="progress-container":style="containerStyle" svgclass="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 !-- 进度圆环 -- circle class="progress-ring__circle progress-ring__progress" :style="{ stroke: activeColor, strokeDashoffset: strokeDashoffset }" :r="radius" cx="60" cy="60" /circle /svg divclass="progress-text":style="{ color: textColor }" {{ Math.round(progress) }}% /div !-- SVG 滤镜定义 (在组件内部,不会污染全局) -- svgwidth="0"height="0"style="position: absolute" filter:id="filterId" feTurbulence ref="turbulenceFilter" type="fractalNoise" :baseFrequency="`${frequency} ${frequency}`" :numOctaves="octaves" result="turbulenceResult" animate attribute dur="10s" :values="`${frequency} ${frequency};${frequency + 0.03} ${frequency - 0.03};${frequency} ${frequency};`" repeatCount="indefinite" /animate /feTurbulence feDisplacementMap ref="displacementMapFilter" in="SourceGraphic" in2="turbulenceResult" :scale="scale" xChannelSelector="R" yChannelSelector="G" /feDisplacementMap /filter /svg/div/template scriptsetupimport{ computed, ref, watchEffect, onMounted }from'vue'; // 定义组件接收的 Propsconstprops =defineProps({size: {type:Number,default:250},progress: {type:Number,default:50,validator:(v) =v =0=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,避免多个组件实例之间滤镜冲突constfilterId =`wobble-filter-${Math.random().toString(36).substring(7)}`; // --- 响应式计算 ---constradius =50;constcircumference =2*Math.PI* radius; // 计算进度条的偏移量conststrokeDashoffset =computed(() ={returncircumference - (props.progress/100) * circumference;}); // 计算容器样式constcontainerStyle =computed(() =({width:`${props.size}px`,height:`${props.size}px`,})); // --- DOM 引用 (虽然Vue会自动更新属性,但保留引用以备将来更复杂的操作) ---constturbulenceFilter =ref(null);constdisplacementMapFilter =ref(null); onMounted(() ={// 可以在这里访问 DOM 元素// console.log(turbulenceFilter.value);});/script stylescoped.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-dashoffset0.35sease; 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;}/stylereact 版本公共组件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 ( divstyle={containerStyle} svg class style={{ width:'100%', height:'100%', transform:'rotate(-90deg)', filter:`url(#${filterId})`, }} viewBox="0 0 120 120" circle class style={{...circleStyle,stroke:inactiveColor}} r={radius} cx="60" cy="60" / circle class style={{ ...circleStyle, stroke:activeColor, strokeDashoffset:strokeDashoffset, strokeLinecap:'round', }} r={radius} cx="60" cy="60" / /svg divstyle={textStyle} {`${Math.round(progress)}%`} /div svgwidth="0"height="0"style={{position:'absolute' }} filterid={filterId} feTurbulence type="fractalNoise" baseFrequency={`${frequency} ${frequency}`} numOctaves={octaves} result="turbulenceResult" animate attribute 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 divid="app-container" WavyProgress progress={progress} strokeWidth={strokeWidth} scale={scale} frequency={frequency} octaves={octaves} / divclass divclass label进度:span{progress}%/span/label input type="range" value={progress} onChange={(e)= setProgress(Number(e.target.value))} min="0" max="100" / /div divclass label边框宽度:span{strokeWidth}/span/label input type="range" value={strokeWidth} onChange={(e)= setStrokeWidth(Number(e.target.value))} min="1" max="50" step="1" / /div divclass 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 divclass 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 divclass 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; `` # canvas-版本 !DOCTYPEhtml htmllang="zh-CN" head metacharset="UTF-8" meta title笔刷效果环形进度条 (流动方向修正版)/title scriptsrc="https://cdn.tailwindcss.com"/script linkhref="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap"rel="stylesheet" style body{ font-family:'Inter', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } input[type="range"]{ -webkit-appearance: none; appearance: none; width:100%; height:8px; background:#4a5568; border-radius:5px; outline: none; opacity:0.7; transition: opacity .2s; } input[type="range"]:hover{ opacity:1; } input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width:20px; height:20px; background:#90eea8; cursor: pointer; border-radius:50%; } input[type="range"]::-moz-range-thumb { width:20px; height:20px; background:#90eea8; cursor: pointer; border-radius:50%; } /style /head bodyclass="bg-gray-900 text-white flex flex-col lg:flex-row items-center justify-center min-h-screen p-4" divclass="w-full lg:w-1/2 flex items-center justify-center p-8" canvasid="progressCanvas"/canvas /div divclass="w-full lg:w-1/3 bg-gray-800 p-6 rounded-2xl shadow-2xl space-y-5 border border-gray-700" h2class="text-2xl font-bold text-center text-green-300 mb-6"配置属性/h2 divclass="space-y-2" divclass="flex justify-between items-center" labelfor="percentage"class="font-medium text-gray-300"百分比/label spanid="percentageValue"class="px-2 py-1 bg-gray-700 text-green-300 rounded-md text-sm font-mono"48%/span /div inputtype="range"id="percentage"min="0"max="100"value="48"class="w-full" /div divclass="space-y-2" divclass="flex justify-between items-center" labelfor="lineWidth"class="font-medium text-gray-300"进度条粗细/label spanid="lineWidthValue"class="px-2 py-1 bg-gray-700 text-green-300 rounded-md text-sm font-mono"16px/span /div inputtype="range"id="lineWidth"min="5"max="60"value="16"class="w-full" /div divclass="space-y-2" divclass="flex justify-between items-center" labelfor="roughness"class="font-medium text-gray-300"边缘粗糙度/label spanid="roughnessValue"class="px-2 py-1 bg-gray-700 text-green-300 rounded-md text-sm font-mono"3/span /div inputtype="range"id="roughness"min="0"max="40"value="3"class="w-full" /div divclass="space-y-2" divclass="flex justify-between items-center" labelfor="animationSpeed"class="font-medium text-gray-300"过渡速度/label spanid="animationSpeedValue"class="px-2 py-1 bg-gray-700 text-green-300 rounded-md text-sm font-mono"7/span /div inputtype="range"id="animationSpeed"min="1"max="100"value="7"class="w-full" /div divclass="space-y-2" divclass="flex justify-between items-center" labelfor="flowSpeed"class="font-medium text-gray-300"流动速度/label spanid="flowSpeedValue"class="px-2 py-1 bg-gray-700 text-green-300 rounded-md text-sm font-mono"3/span /div inputtype="range"id="flowSpeed"min="1"max="100"value="3"class="w-full" /div divclass="grid grid-cols-1 sm:grid-cols-2 gap-4 pt-4" divclass="flex flex-col items-center space-y-2" labelfor="progressColor"class="font-medium text-gray-300"进度颜色/label inputtype="color"id="progressColor"value="#ADFF2F"class="w-full h-10 p-1 bg-gray-700 rounded-md cursor-pointer border-2 border-gray-600" /div divclass="flex flex-col items-center space-y-2" labelfor="baseColor"class="font-medium text-gray-300"底环颜色/label inputtype="color"id="baseColor"value="#333333"class="w-full h-10 p-1 bg-gray-700 rounded-md cursor-pointer border-2 border-gray-600" /div /div /div script constcanvas =document.getElementById('progressCanvas'); constctx = canvas.getContext('2d'); constcontrols = { percentage:document.getElementById('percentage'), lineWidth:document.getElementById('lineWidth'), roughness:document.getElementById('roughness'), animationSpeed:document.getElementById('animationSpeed'), flowSpeed:document.getElementById('flowSpeed'), progressColor:document.getElementById('progressColor'), baseColor:document.getElementById('baseColor'), constvalueDisplays = { percentage:document.getElementById('percentageValue'), lineWidth:document.getElementById('lineWidthValue'), roughness:document.getElementById('roughnessValue'), animationSpeed:document.getElementById('animationSpeedValue'), flowSpeed:document.getElementById('flowSpeedValue'), letconfig = { percentage:48, lineWidth:16, radius:100, roughness:3, steps:100, animationSpeed:7, flowSpeed:3, progressColor:'#ADFF2F', baseColor:'#333333', letanimatedPercentage =0; letcurrentDisplacements = []; lettargetDisplacements = []; lettexturePhase =0; constlerp= (start, end, amt) = (1- amt) * start + amt * end; functiongenerateTargetDisplacements() { targetDisplacements = []; for(leti =0; i = config.steps; i++) { constouter = (Math.random() -0.5) *2; constinner = (Math.random() -0.5) *2; targetDisplacements.push({ outer, inner }); } } functionsetupCanvas() { constdpr =window.devicePixelRatio||1; constsize = (config.radius+ config.lineWidth+ config.roughness) *2.2; canvas.width= size * dpr; canvas.height= size * dpr; canvas.style.width=`${size}px`; canvas.style.height=`${size}px`; ctx.scale(dpr, dpr); } functiondrawRoughArc(cx, cy, radius, lineWidth, startAngle, endAngle, color, roughness, steps, displacements) { constinnerRadius = radius - lineWidth /2; constouterRadius = radius + lineWidth /2; if(steps =0|| displacements.length===0)return; constangleStep = (endAngle - startAngle) / steps; constouterPoints = []; constinnerPoints = []; for(leti =0; i = steps; i++) { constangle = startAngle + i * angleStep; constcosA =Math.cos(angle); constsinA =Math.sin(angle); // 根据点的实际角度和流动相位来确定使用哪个纹理数据 letnormalizedAngle = angle % (Math.PI*2); if(normalizedAngle 0) normalizedAngle +=Math.PI*2; constindexFromAngle =Math.round((normalizedAngle / (Math.PI*2)) * config.steps); consttotalDisplacements = displacements.length; constdisplacementIndex = (indexFromAngle +Math.floor(texturePhase)) % totalDisplacements; constdisp = displacements[displacementIndex] || {outer:0,inner:0 constcurrentOuterRadius = outerRadius + disp.outer* roughness; constcurrentInnerRadius = innerRadius + disp.inner* roughness; outerPoints.push({x: cx + cosA * currentOuterRadius,y: cy + sinA * currentOuterRadius }); innerPoints.push({x: cx + cosA * currentInnerRadius,y: cy + sinA * currentInnerRadius }); } ctx.fillStyle= color; ctx.beginPath(); ctx.moveTo(outerPoints[0].x, outerPoints[0].y); for(leti =1; i outerPoints.length; i++) { ctx.lineTo(outerPoints[i].x, outerPoints[i].y); } ctx.lineTo(innerPoints[innerPoints.length-1].x, innerPoints[innerPoints.length-1].y); for(leti = innerPoints.length-2; i =0; i--) { ctx.lineTo(innerPoints[i].x, innerPoints[i].y); } ctx.closePath(); ctx.fill(); } functiondraw(percentageToDraw) { constsize = (config.radius+ config.lineWidth+ config.roughness) *2.2; constcenter = size /2; ctx.clearRect(0,0, canvas.width, canvas.height); drawRoughArc(center, center, config.radius, config.lineWidth,0,Math.PI*2, config.baseColor, config.roughness, config.steps, currentDisplacements); if(percentageToDraw 0) { constendAngle = (Math.PI*2* percentageToDraw) /100-Math.PI/2; conststartAngle = -Math.PI/2; constprogressSteps =Math.max(1,Math.round(config.steps* (percentageToDraw /100))); drawRoughArc(center, center, config.radius, config.lineWidth, startAngle, endAngle, config.progressColor, config.roughness, progressSteps, currentDisplacements); } ctx.fillStyle= config.progressColor; ctx.font=`bold${config.radius *0.5}px Inter`; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText(`${Math.round(percentageToDraw)}%`, center, center); } functionanimate() { requestAnimationFrame(animate); // 1. 百分比平滑过渡 consttargetPercentage = config.percentage; consteasingFactor = config.animationSpeed/1000; constdiff = targetPercentage - animatedPercentage; if(Math.abs(diff) 0.01) { animatedPercentage += diff * easingFactor; }else{ animatedPercentage = targetPercentage; } // 2. 边缘呼吸效果的平滑过渡 consttransitionSpeed = config.flowSpeed/1000; for(leti =0; i = config.steps; i++) { if(currentDisplacements[i] && targetDisplacements[i]) { currentDisplacements[i].outer=lerp(currentDisplacements[i].outer, targetDisplacements[i].outer, transitionSpeed); currentDisplacements[i].inner=lerp(currentDisplacements[i].inner, targetDisplacements[i].inner, transitionSpeed); } } // --- 核心改动:纹理整体流动,改为减少相位来反向流动 --- texturePhase -= config.flowSpeed/50; // 保持texturePhase在一个合理的范围内,防止数字过大 if(texturePhase 0) texturePhase += config.steps; texturePhase %= config.steps; // 4. 当呼吸效果接近目标时,生成新目标 if(currentDisplacements.length0Math.abs(currentDisplacements[0].outer- targetDisplacements[0].outer) 0.01) { generateTargetDisplacements(); } // 5. 每帧执行绘制 draw(animatedPercentage); } functionupdateConfigFromControls() { constsizeChanged = config.lineWidth!==parseFloat(controls.lineWidth.value) || config.roughness!==parseFloat(controls.roughness.value); config.percentage=parseFloat(controls.percentage.value); config.lineWidth=parseFloat(controls.lineWidth.value); config.roughness=parseFloat(controls.roughness.value); config.animationSpeed=parseFloat(controls.animationSpeed.value); config.flowSpeed=parseFloat(controls.flowSpeed.value); config.progressColor= controls.progressColor.value; config.baseColor= controls.baseColor.value; valueDisplays.percentage.textContent=`${Math.round(config.percentage)}%`; valueDisplays.lineWidth.textContent=`${config.lineWidth}px`; valueDisplays.roughness.textContent=`${config.roughness}`; valueDisplays.animationSpeed.textContent=`${Math.round(config.animationSpeed)}`; valueDisplays.flowSpeed.textContent=`${Math.round(config.flowSpeed)}`; if(sizeChanged) { setupCanvas(); // 重新设置位移数据,确保流畅 generateTargetDisplacements(); currentDisplacements =JSON.parse(JSON.stringify(targetDisplacements)); } } for(constkeyincontrols) { controls[key].addEventListener('input', updateConfigFromControls); } window.addEventListener('resize', setupCanvas); functioninitialize() { updateConfigFromControls(); setupCanvas(); generateTargetDisplacements(); currentDisplacements =JSON.parse(JSON.stringify(targetDisplacements)); requestAnimationFrame(animate); } initialize(); /script /body /html # 微信小程序测试版本 template viewclass="container" viewclass="progress-display-area" rough-circular-progress :canvas-size="250" :percentage="config.percentage" :line-width="config.lineWidth" :roughness="config.roughness" :font-size="config.fontSize" progress-color="#ADFF2F" base-color="#444444" /rough-circular-progress /view viewclass="controls-area" viewclass="control-item" viewclass="control-label" text进度 (Percentage)/text textclass="value-display"{{ config.percentage.toFixed(0) }}%/text /view slider :value="config.percentage" @changing="onSliderChange('percentage', $event)" min="0" max="100" active-color="#ADFF2F" block-size="20" / /view viewclass="control-item" viewclass="control-label" text线宽 (LineWidth)/text textclass="value-display"{{ config.lineWidth.toFixed(1) }}/text /view slider :value="config.lineWidth" @changing="onSliderChange('lineWidth', $event)" min="5" max="40" step="0.5" active-color="#ADFF2F" block-size="20" / /view viewclass="control-item" viewclass="control-label" text粗糙度 (Roughness)/text textclass="value-display"{{ config.roughness.toFixed(1) }}/text /view slider :value="config.roughness" @changing="onSliderChange('roughness', $event)" min="0" max="10" step="0.1" active-color="#ADFF2F" block-size="20" / /view viewclass="control-item" viewclass="control-label" text字号 (FontSize)/text textclass="value-display"{{ config.fontSize.toFixed(0) }}/text /view slider :value="config.fontSize" @changing="onSliderChange('fontSize', $event)" min="20" max="80" active-color="#ADFF2F" block-size="20" / /view /view /view /template script // 引入组件 importRoughCircularProgressfrom'@/components/rough-circular-progress.vue'; exportdefault{ // 注册组件 components: { RoughCircularProgress }, data() { return{ // 将所有可配置参数集中管理 config: { percentage:48, lineWidth:20, roughness:4, fontSize:50, } }; }, methods: { // 创建一个通用的滑块更新方法 onSliderChange(key, event) { // 使用 key 来动态更新 config 对象中对应的属性 this.config[key] = event.detail.value; } } /script stylescoped .container{ display: flex; flex-direction: column; align-items: center; min-height:100vh; background-color:#1a1a1a; padding:20px; box-sizing: border-box; } .progress-display-area{ flex-shrink:0; display: flex; justify-content: center; align-items: center; width:100%; padding:40px0; } .controls-area{ width:90%; max-width:400px; } .control-item{ margin-bottom:25px; } .control-label{ display: flex; justify-content: space-between; align-items: center; margin-bottom:10px; color:#cccccc; font-size:15px; } .value-display{ font-weight: bold; color:#ffffff; background-color:#333333; padding:2px8px; border-radius:4px; font-family: monospace;/* 使用等宽字体让数字更好看 */ } /* 覆盖 uni-app slider 的默认样式,使其更贴合主题 */ /deep/.uni-slider-handle-wrapper{ height:40px; } /style将环形进度条改为直线形式,同时保留核心的 “笔刷” 和“流动”「效果」template viewclass="progress-container":style="{ width: width + 'px', height: height + 'px' }" canvas type="2d" id="linearProgressCanvas" canvas-id="linearProgressCanvas" :style="{ width: width + 'px', height: height + 'px' }" /canvas /view/template script export default { name:"rough-linear-progress", props: {// 画布宽度 width: { type: Number, default:300 },// 画布高度(即进度条粗细) height: { type: Number, default:40 },// 进度百分比 (0-100) percentage: { type: Number, default:60 },// 边缘粗糙度/波浪幅度 roughness: { type: Number, default:5 },// 进度条颜色 progressColor: { type: String, default:'#ADFF2F' },// 背景颜色 baseColor: { type: String, default:'#333333' },// 文字大小 fontSize: { type: Number, default:16 },// 文字颜色 fontColor: { type: String, default:'#111111' },// 是否显示文字 showText: { type:Boolean, default:true },// 过渡动画速度 (值越小越快) transitionSpeed: { type: Number, default:0.07 } },data() {return{ ctx:null, canvas:null, animatedPercentage:0, animationFrameId:null, }; }, watch: {'$props': { handler() {if(!this.animationFrameId) {this.startAnimation(); } }, deep:true, immediate:false } }, mounted() {this.$nextTick(() = {this.initCanvas(); }); }, beforeDestroy() {this.stopAnimation(); }, methods: { initCanvas() {constquery = uni.createSelectorQuery().in(this); query.select('#linearProgressCanvas') .fields({ node:true, size:true}) .exec((res) = {if(!res[0] || !res[0].node) { console.error('无法找到Canvas节点');return; } this.canvas = res[0].node;this.ctx =this.canvas.getContext('2d'); constdpr = uni.getSystemInfoSync().pixelRatio;this.canvas.width =this.width * dpr;this.canvas.height =this.height * dpr;this.ctx.scale(dpr, dpr); this.animatedPercentage =this.percentage;this.startAnimation(); }); }, startAnimation() {if(this.animationFrameId)return;this.animate(); }, stopAnimation() {if(this.animationFrameId this.canvas) {this.canvas.cancelAnimationFrame(this.animationFrameId);this.animationFrameId =null; } }, animate() {this.animationFrameId =this.canvas.requestAnimationFrame(this.animate); consttargetPercentage =this.percentage;constdiff = targetPercentage -this.animatedPercentage; if(Math.abs(diff) 0.01) {this.animatedPercentage += diff *this.transitionSpeed; }else{this.animatedPercentage = targetPercentage; } this.draw(); }, draw() {this.ctx.clearRect(0,0,this.width,this.height); // 绘制背景this.drawRoughRect(0,0,this.width,this.height,this.baseColor,this.roughness); // 绘制进度条constprogressWidth = (this.width *this.animatedPercentage) /100;if(progressWidth 0) {this.drawRoughRect(0,0, progressWidth,this.height,this.progressColor,this.roughness); } // 绘制文字if(this.showText) {this.ctx.fillStyle =this.fontColor;this.ctx.font = `bold ${this.fontSize}px sans-serif`;this.ctx.textAlign ='center';this.ctx.textBaseline ='middle';this.ctx.fillText(`${Math.round(this.animatedPercentage)}%`,this.width /2,this.height /2); } }, /** * --- 核心改造函数 --- * 绘制带粗糙边缘的矩形 */ drawRoughRect(x, y, width, height, color, roughness) {constpoints = [];conststep =10;// 每隔10px计算一个锚点 // 1. 生成上边缘的点for(let i =0; i = width; i += step) { points.push({ x: x + i, y: y + (Math.random() -0.5) * roughness }); } points.push({x: x + width, y: y + (Math.random() -0.5) * roughness}); // 2. 生成右边缘的点for(let i =0; i = height; i += step) { points.push({ x: x + width + (Math.random() -0.5) * roughness, y: y + i }); } points.push({x: x + width + (Math.random() -0.5) * roughness, y: y + height}); // 3. 生成下边缘的点(反向)for(let i = width; i =0; i -= step) { points.push({ x: x + i, y: y + height + (Math.random() -0.5) * roughness }); } points.push({x: x, y: y + height + (Math.random() -0.5) * roughness}); // 4. 生成左边缘的点(反向)for(let i = height; i =0; i -= step) { points.push({ x: x + (Math.random() -0.5) * roughness, y: y + i }); } points.push({x: x + (Math.random() -0.5) * roughness, y: y}); this.ctx.fillStyle = color;this.ctx.beginPath();this.ctx.moveTo(points[0].x, points[0].y);for(let i =1; i points.length; i++) {this.ctx.lineTo(points[i].x, points[i].y); }this.ctx.closePath();this.ctx.fill(); } } }/script style scoped .progress-container { display: flex; justify-content: center; align-items: center; }/styleAI编程资讯AI Coding专区指南:https://aicoding.juejin.cn/aicoding 点击"阅读原文"了解详情~ 阅读原文

上一篇:2021-08-09_机器之心ACL论文分享会干货回顾,下一场NeurIPS,12月见! 下一篇:2024-01-21_28句木心语录,开给青年人的止痛药

TAG标签:

14
网站开发网络凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设网站改版域名注册主机空间手机网站建设网站备案等方面的需求...
请立即点击咨询我们或拨打咨询热线:13245491521 13245491521 ,我们会详细为你一一解答你心中的疑难。
项目经理在线

相关阅读 更多>>

猜您喜欢更多>>

我们已经准备好了,你呢?
2022我们与您携手共赢,为您的企业营销保驾护航!

不达标就退款

高性价比建站

免费网站代备案

1对1原创设计服务

7×24小时售后支持

 

全国免费咨询:

13245491521

业务咨询:13245491521 / 13245491521

节假值班:13245491521()

联系地址:

Copyright © 2019-2025      ICP备案:沪ICP备19027192号-6 法律顾问:律师XXX支持

在线
客服

技术在线服务时间:9:00-20:00

在网站开发,您对接的直接是技术员,而非客服传话!

电话
咨询

13245491521
7*24小时客服热线

13245491521
项目经理手机

微信
咨询

加微信获取报价