news 2026/6/18 11:57:14

流动等高线 · Contour Flow

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
流动等高线 · Contour Flow

流动等高线 · Contour Flow

流动等高线 · Contour Flow

基于Marching Squares 算法​ 与Simplex Noise 分形噪声​ 实现的动态等高线可视化效果,模拟地形图缓慢“呼吸”的流动质感。


✨ 效果特性

  • 极简视觉风格:浅灰背景#f0f0f0+ 多层半透明淡灰等高线,还原参考图的克制美学

  • 自然等高线形态:Simplex Noise 分形叠加,生成连续、无机械感的曲线

  • 动态流动动画:通过噪声场 Z 轴时间偏移,等高线持续缓慢漂移、形变,呈现“地形呼吸”效果

  • 全屏自适应:自动适配窗口尺寸,铺满可视区域

  • 高性能绘制:预计算噪声场 + Canvas 2D 渲染


🎮 可调参数

在代码中可直接修改以下变量,实时调整视觉效果:

参数

说明

推荐值

gridSize

采样网格密度

3(越小越精细,性能消耗越高)

scale

噪声缩放比例

0.004(控制等高线疏密)

time * 0.08

流动速度

数值越大流动越快

levels

等高线层数

16

octaves

分形噪声叠加次数

5(影响细节丰富度)


🧠 技术原理简述

1. Simplex Noise(三维)

  • 使用经典 Simplex Noise 实现

  • 第三维z绑定时间,驱动噪声场随时间变化

2. FBM(Fractional Brownian Motion)

  • 多层噪声叠加(octaves = 5

  • 高频细节 + 低频趋势,形成自然的地形起伏感

3. Marching Squares

  • 对每个网格单元采样 4 个角点高度值

  • 根据阈值判断 16 种拓扑情况

  • 线性插值生成等高线段

4. 视觉层次

  • 16 层等高线

  • 透明度随层级变化,中心层更明显

  • 每 4 条线加粗一次,增强可读性


🚀 使用方法

  1. 将代码保存为index.html

  2. 使用现代浏览器直接打开

  3. 无需任何依赖、构建工具或服务器

open index.html

或修改参数后刷新即可看到变化。


📌 注意事项

  • 建议使用Chrome / Edge / Firefox​ 最新版

  • 降低gridSize在高分辨率屏幕上可能带来性能压力

  • 若需静态导出,可暂停requestAnimationFrame并截图


📄 License

MIT — 自由使用、修改与分发

<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>优雅流动的等高线</title> <style> body { margin: 0; padding: 0; overflow: hidden; background-color: #f4f4f5; } canvas { display: block; width: 100vw; height: 100vh; } </style> </head> <body> <canvas id="topoCanvas"></canvas> <script> const canvas = document.getElementById('topoCanvas'); const gl = canvas.getContext('webgl'); if (!gl) { alert('您的浏览器不支持 WebGL'); } const vsSource = ` attribute vec2 a_position; void main() { gl_Position = vec4(a_position, 0.0, 1.0); } `; const fsSource = ` precision highp float; uniform vec2 u_resolution; uniform float u_time; // 3D Simplex 噪声函数 vec4 permute(vec4 x){return mod(((x*34.0)+1.0)*x, 289.0);} vec4 taylorInvSqrt(vec4 r){return 1.79284291400159 - 0.85373472095314 * r;} float snoise(vec3 v){ const vec2 C = vec2(1.0/6.0, 1.0/3.0) ; const vec4 D = vec4(0.0, 0.5, 1.0, 2.0); vec3 i = floor(v + dot(v, C.yyy) ); vec3 x0 = v - i + dot(i, C.xxx) ; vec3 g = step(x0.yzx, x0.xyz); vec3 l = 1.0 - g; vec3 i1 = min( g.xyz, l.zxy ); vec3 i2 = max( g.xyz, l.zxy ); vec3 x1 = x0 - i1 + 1.0 * C.xxx; vec3 x2 = x0 - i2 + 2.0 * C.xxx; vec3 x3 = x0 - 1.0 + 3.0 * C.xxx; i = mod(i, 289.0 ); vec4 p = permute( permute( permute( i.z + vec4(0.0, i1.z, i2.z, 1.0 )) + i.y + vec4(0.0, i1.y, i2.y, 1.0 )) + i.x + vec4(0.0, i1.x, i2.x, 1.0 )); float n_ = 1.0/7.0; vec3 ns = n_ * D.wyz - D.xzx; vec4 j = p - 49.0 * floor(p * ns.z *ns.z); vec4 x_ = floor(j * ns.z); vec4 y_ = floor(j - 7.0 * x_ ); vec4 x = x_ *ns.x + ns.yyyy; vec4 y = y_ *ns.x + ns.yyyy; vec4 h = 1.0 - abs(x) - abs(y); vec4 b0 = vec4( x.xy, y.xy ); vec4 b1 = vec4( x.zw, y.zw ); vec4 s0 = floor(b0)*2.0 + 1.0; vec4 s1 = floor(b1)*2.0 + 1.0; vec4 sh = -step(h, vec4(0.0)); vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy ; vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww ; vec3 p0 = vec3(a0.xy,h.x); vec3 p1 = vec3(a0.zw,h.y); vec3 p2 = vec3(a1.xy,h.z); vec3 p3 = vec3(a1.zw,h.w); vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2, p2), dot(p3,p3))); p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w; vec4 m = max(0.5 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0); m = m * m; return 105.0 * dot( m*m, vec4( dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3) ) ); } void main() { vec2 uv = gl_FragCoord.xy / min(u_resolution.x, u_resolution.y); // 【参数调节区 - 已调整为更慢的速度】 float speed = u_time * 0.03; // 🐢 从 0.08 降低到 0.03,动画变慢约60% float scale = 1.4; // 稍微缩小缩放值,让波纹更大更舒展 float density = 10.0; // 稍微减少密度,视觉更清爽 float lineThickness = 0.47; // 线条略微加粗一点,增强呼吸感 // 生成复合噪声 // 第二层噪声速度也从 1.2 降低到 1.1,配合主层速度 float n = snoise(vec3(uv * scale, speed)); n += 0.25 * snoise(vec3(uv * scale * 1.8, speed * 1.1)); n = n * 0.5 + 0.5; // 生成等高线 float contour = abs(fract(n * density) - 0.5); float line = smoothstep(lineThickness, 0.5, contour); // 【颜色配置区】 vec3 bgColor = vec3(0.96, 0.96, 0.97); vec3 lineColor = vec3(0.82, 0.82, 0.84); // 线条颜色略微加深,增加层次感 vec3 color = mix(lineColor, bgColor, line); gl_FragColor = vec4(color, 1.0); } `; function createShader(gl, type, source) { const shader = gl.createShader(type); gl.shaderSource(shader, source); gl.compileShader(shader); if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) { console.error('Shader compile error:', gl.getShaderInfoLog(shader)); gl.deleteShader(shader); return null; } return shader; } const vertexShader = createShader(gl, gl.VERTEX_SHADER, vsSource); const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fsSource); const program = gl.createProgram(); gl.attachShader(program, vertexShader); gl.attachShader(program, fragmentShader); gl.linkProgram(program); gl.useProgram(program); const positionBuffer = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer); const positions = [ -1.0, -1.0, 1.0, -1.0, -1.0, 1.0, -1.0, 1.0, 1.0, -1.0, 1.0, 1.0, ]; gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW); const positionLocation = gl.getAttribLocation(program, "a_position"); gl.enableVertexAttribArray(positionLocation); gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0); const resolutionLocation = gl.getUniformLocation(program, "u_resolution"); const timeLocation = gl.getUniformLocation(program, "u_time"); function resizeCanvas() { canvas.width = window.innerWidth; canvas.height = window.innerHeight; gl.viewport(0, 0, canvas.width, canvas.height); gl.uniform2f(resolutionLocation, canvas.width, canvas.height); } window.addEventListener('resize', resizeCanvas); resizeCanvas(); let startTime = Date.now(); function render() { const currentTime = (Date.now() - startTime) / 1000.0; gl.uniform1f(timeLocation, currentTime); gl.drawArrays(gl.TRIANGLES, 0, 6); requestAnimationFrame(render); } render(); </script> </body> </html>
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>流动等高线 - 水滴般缓慢</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { width: 100vw; height: 100vh; overflow: hidden; background: #f8f8f8; } canvas { display: block; } </style> </head> <body> <canvas id="contourCanvas"></canvas> <script> const canvas = document.getElementById('contourCanvas'); const ctx = canvas.getContext('2d'); let width, height; let time = 0; // 极度缓慢的参数 const FLOW_SPEED = 0.0008; // 极慢的速度 const SMOOTHING = 0.95; // 更强的平滑 let smoothedTime = 0; let lastTime = 0; // 水滴般的缓动变量 let flowDirection = 1; let flowSpeedVariation = 0; let targetSpeed = FLOW_SPEED; // ==== Simplex Noise 实现 ==== const perm = new Uint8Array(512); const p = [151,160,137,91,90,15,131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,190,6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,88,237,149,56,87,174,20,125,136,171,168,68,175,74,165,71,134,139,48,27,166,77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,102,143,54,65,25,63,161,1,216,80,73,209,76,132,187,208,89,18,169,200,196,135,130,116,188,159,86,164,100,109,198,173,186,3,64,52,217,226,250,124,123,5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,223,183,170,213,119,248,152,2,44,154,163,70,221,153,101,155,167,43,172,9,129,22,39,253,19,98,108,110,79,113,224,232,178,185,112,104,218,246,97,228,251,34,242,193,238,210,144,12,191,179,162,241,81,51,145,235,249,14,239,107,49,192,214,31,181,199,106,157,184,84,204,176,115,121,50,45,127,4,150,254,138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180]; for (let i = 0; i < 256; i++) { perm[i] = p[i]; perm[i + 256] = p[i]; } function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); } function lerp(t, a, b) { return a + t * (b - a); } function grad(hash, x, y, z) { const h = hash & 15; const u = h < 8 ? x : y; const v = h < 4 ? y : (h === 12 || h === 14) ? x : z; return ((h & 1) === 0 ? u : -u) + ((h & 2) === 0 ? v : -v); } function noise(x, y, z) { const X = Math.floor(x) & 255; const Y = Math.floor(y) & 255; const Z = Math.floor(z) & 255; x -= Math.floor(x); y -= Math.floor(y); z -= Math.floor(z); const u = fade(x); const v = fade(y); const w = fade(z); const A = perm[X] + Y, AA = perm[A] + Z, AB = perm[A + 1] + Z; const B = perm[X + 1] + Y, BA = perm[B] + Z, BB = perm[B + 1] + Z; return lerp(w, lerp(v, lerp(u, grad(perm[AA], x, y, z), grad(perm[BA], x - 1, y, z)), lerp(u, grad(perm[AB], x, y - 1, z), grad(perm[BB], x - 1, y - 1, z))), lerp(v, lerp(u, grad(perm[AA + 1], x, y, z - 1), grad(perm[BA + 1], x - 1, y, z - 1)), lerp(u, grad(perm[AB + 1], x, y - 1, z - 1), grad(perm[BB + 1], x - 1, y - 1, z - 1)))); } function fbm(x, y, z, octaves) { let val = 0, amp = 0.5, freq = 1; for (let i = 0; i < octaves; i++) { val += amp * noise(x * freq, y * freq, z * freq); amp *= 0.5; freq *= 2; } return val; } // ==== 预计算噪声场 ==== const gridSize = 4; // 稍微增大网格,减少计算量 let noiseField = []; let fieldW, fieldH; function updateNoiseField() { fieldW = Math.ceil(width / gridSize) + 1; fieldH = Math.ceil(height / gridSize) + 1; noiseField = new Float32Array(fieldW * fieldH); const scale = 0.002; // 更小的噪声尺度,变化更缓慢 const nz = smoothedTime; // 添加多个频率的噪声,模拟水滴的层次感 for (let gy = 0; gy < fieldH; gy++) { for (let gx = 0; gx < fieldW; gx++) { const x = gx * gridSize * scale; const y = gy * gridSize * scale; // 主噪声 - 非常缓慢 const baseNoise = fbm(x, y, nz, 3) * 0.7; // 次噪声 - 更慢的变化 const slowNoise = fbm(x * 0.5, y * 0.5, nz * 0.3, 2) * 0.3; // 微噪声 - 几乎静止的细节 const microNoise = fbm(x * 3, y * 3, nz * 0.1, 1) * 0.1; noiseField[gy * fieldW + gx] = baseNoise + slowNoise + microNoise; } } } function getValue(gx, gy) { if (gx < 0 || gx >= fieldW || gy < 0 || gy >= fieldH) return 0; return noiseField[gy * fieldW + gx]; } function interpolate(a, b, threshold) { if (Math.abs(a - b) < 0.0001) return 0.5; return (threshold - a) / (b - a); } function getLines(caseIndex, top, right, bottom, left) { const lines = []; switch (caseIndex) { case 1: lines.push([left, top]); break; case 2: lines.push([top, right]); break; case 3: lines.push([left, right]); break; case 4: lines.push([right, bottom]); break; case 5: lines.push([left, top], [right, bottom]); break; case 6: lines.push([top, bottom]); break; case 7: lines.push([left, bottom]); break; case 8: lines.push([left, bottom]); break; case 9: lines.push([top, bottom]); break; case 10: lines.push([left, bottom], [top, right]); break; case 11: lines.push([right, bottom]); break; case 12: lines.push([left, right]); break; case 13: lines.push([top, right]); break; case 14: lines.push([left, top]); break; } return lines; } function drawContourLine(threshold, color, lineWidth) { ctx.beginPath(); ctx.strokeStyle = color; ctx.lineWidth = lineWidth; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; for (let gy = 0; gy < fieldH - 1; gy++) { for (let gx = 0; gx < fieldW - 1; gx++) { const v00 = getValue(gx, gy); const v10 = getValue(gx + 1, gy); const v11 = getValue(gx + 1, gy + 1); const v01 = getValue(gx, gy + 1); let caseIndex = 0; if (v00 > threshold) caseIndex |= 1; if (v10 > threshold) caseIndex |= 2; if (v11 > threshold) caseIndex |= 4; if (v01 > threshold) caseIndex |= 8; if (caseIndex === 0 || caseIndex === 15) continue; const x0 = gx * gridSize; const y0 = gy * gridSize; const x1 = (gx + 1) * gridSize; const y1 = (gy + 1) * gridSize; const pTop = { x: x0 + interpolate(v00, v10, threshold) * gridSize, y: y0 }; const pRight = { x: x1, y: y0 + interpolate(v10, v11, threshold) * gridSize }; const pBottom = { x: x0 + interpolate(v01, v11, threshold) * gridSize, y: y1 }; const pLeft = { x: x0, y: y0 + interpolate(v00, v01, threshold) * gridSize }; const lines = getLines(caseIndex, pTop, pRight, pBottom, pLeft); for (const line of lines) { ctx.moveTo(line[0].x, line[0].y); ctx.lineTo(line[1].x, line[1].y); } } } ctx.stroke(); } function resize() { width = canvas.width = window.innerWidth; height = canvas.height = window.innerHeight; } function draw(currentTime) { // 计算时间差,实现真正的时间控制 if (lastTime === 0) lastTime = currentTime; const deltaTime = (currentTime - lastTime) / 1000; // 转换为秒 lastTime = currentTime; // 水滴般的缓动变化 flowSpeedVariation += (Math.random() - 0.5) * 0.0001; flowSpeedVariation *= 0.99; // 逐渐衰减 const actualSpeed = FLOW_SPEED + flowSpeedVariation; // 平滑时间更新 smoothedTime = smoothedTime * SMOOTHING + time * (1 - SMOOTHING); // 清除画布,使用半透明覆盖实现拖影效果 ctx.fillStyle = 'rgba(248, 248, 248, 0.08)'; ctx.fillRect(0, 0, width, height); // 只在需要时完全重绘 if (time % 100 === 0) { ctx.fillStyle = '#f8f8f8'; ctx.fillRect(0, 0, width, height); } updateNoiseField(); const levels = 12; // 减少层级,更简洁 for (let i = 0; i < levels; i++) { const t = (i / levels) * 2 - 1; // 非常柔和的透明度和颜色 const alpha = 0.15 + 0.25 * (1 - Math.abs(t)); const gray = Math.floor(160 + 60 * Math.sin(i * 0.2 + smoothedTime * 0.1)); const color = `rgba(${gray}, ${gray}, ${gray}, ${alpha})`; const lw = i % 4 === 0 ? 0.8 : 0.4; drawContourLine(t, color, lw); } time += actualSpeed * deltaTime * 60; // 帧率无关的速度控制 requestAnimationFrame(draw); } window.addEventListener('resize', resize); resize(); requestAnimationFrame(draw); </script> </body> </html>
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/18 11:56:13

Scrapling:零配置Python网络爬虫的完整终极指南 [特殊字符]

Scrapling&#xff1a;零配置Python网络爬虫的完整终极指南 &#x1f680; 【免费下载链接】Scrapling &#x1f577;️ An adaptive Web Scraping framework that handles everything from a single request to a full-scale crawl! 项目地址: https://gitcode.com/GitHub_T…

作者头像 李华
网站建设 2026/6/18 11:50:11

Sunshine游戏串流服务器完整指南:10分钟搭建你的私人云游戏平台

Sunshine游戏串流服务器完整指南&#xff1a;10分钟搭建你的私人云游戏平台 【免费下载链接】Sunshine Self-hosted game stream host for Moonlight. 项目地址: https://gitcode.com/GitHub_Trending/su/Sunshine 还在为只能在书房玩游戏而烦恼吗&#xff1f;想象一下&…

作者头像 李华
网站建设 2026/6/18 11:50:01

遗传算法工业落地核心:选择、交叉、变异算子的工程化设计

1. 项目概述&#xff1a;为什么“遗传算法第二讲”比第一讲更值得你花时间啃透“遗传算法”这四个字&#xff0c;听上去像生物课和计算机课的混血儿——既带着DNA双螺旋的神秘感&#xff0c;又透着代码里for循环的机械味。但真正让我在工业优化项目里连续三年把它设为默认求解器…

作者头像 李华
网站建设 2026/6/18 11:44:59

高速PCB选材完整方案、解决现存痛点

​高速电路性能是基材树脂、玻纤布、铜箔、半固化片、阻焊辅料共同作用的综合结果&#xff0c;单一材料指标优异无法保障整体信号完整性&#xff0c;大量项目出现选材碎片化、仿真与实物偏差大、批量阻抗损耗一致性差等问题。本文梳理高速 PCB 材料系统化选型落地步骤、常见材料…

作者头像 李华