流动等高线 · Contour Flow

📅 2026/6/18 11:57:23
流动等高线 · 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等高线层数16octaves分形噪声叠加次数5影响细节丰富度 技术原理简述1. Simplex Noise三维使用经典 Simplex Noise 实现第三维z绑定时间驱动噪声场随时间变化2. FBMFractional Brownian Motion多层噪声叠加octaves 5高频细节 低频趋势形成自然的地形起伏感3. Marching Squares对每个网格单元采样 4 个角点高度值根据阈值判断 16 种拓扑情况线性插值生成等高线段4. 视觉层次16 层等高线透明度随层级变化中心层更明显每 4 条线加粗一次增强可读性 使用方法将代码保存为index.html使用现代浏览器直接打开无需任何依赖、构建工具或服务器open index.html或修改参数后刷新即可看到变化。 注意事项建议使用Chrome / Edge / Firefox​ 最新版降低gridSize在高分辨率屏幕上可能带来性能压力若需静态导出可暂停requestAnimationFrame并截图 LicenseMIT — 自由使用、修改与分发!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.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 idtopoCanvas/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 langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.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 idcontourCanvas/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