告别切图!用柏林噪声纯前端生成纹理,包体积直降 30%+

4 阅读7分钟

作为前端老鸟,谁没被 UI 丢来的一堆纹理图折磨过?项目包体积里一半是各种木纹、石纹、磨砂纹,上线后加载慢到用户直骂娘,还得处理各种适配问题。最近我在项目里用柏林噪声(Perlin Noise)纯前端生成纹理,直接砍掉了 80% 的纹理图片资源,包体积从 1.2M 降到 200K,加载速度提了一大截,今天就把这套实战方案分享给各位道友。

为啥放弃图片?前端生成纹理香在哪?

先唠唠痛点:咱前端做页面,不管是电商详情页的木纹卡片、金融后台的磨砂面板,还是小游戏里的石头地面,UI 总爱给一堆 png/jpg 纹理图。这些图看着小,积少成多就成了包体积的 “隐形杀手”—— 加载慢、占带宽、不同分辨率还得切多套图,适配起来头都大。

而用柏林噪声生成纹理,优势直接拉满:

  1. 体积极致小:就几行 JS 代码,替代几十上百 KB 的图片,包体积立减 80%+;
  2. 适配无压力:画布多大纹理就多大,1px 到 4K 屏都清晰,不用切多套图;
  3. 个性化可控:改几个参数就能生成不同风格的纹理,不用反复找 UI 改图;
  4. 性能贼拉稳:前端实时生成,比加载图片少了网络请求,首屏加载更快。

核心原理:柏林噪声到底是个啥?

咱前端不用钻数学牛角尖,简单说:柏林噪声能生成连续、自然、无重复的随机值,把这些值映射成颜色 / 透明度,就是自然的纹理。对比普通随机数(生成的纹理全是噪点),柏林噪声的优势是 “平滑过渡”,生成的纹理和真实世界的木纹、石纹一模一样。

核心逻辑就 3 步:

  1. 生成梯度向量网格(给每个网格点分配随机方向);
  2. 计算目标点到网格点的距离,做梯度点积;
  3. 用平滑函数插值,让结果从一个值自然过渡到另一个值。

实战:3 种高频纹理纯前端实现(可直接 CV)

下面直接上代码,都是我项目里跑通的实战案例,基于原生 Canvas + 柏林噪声,不用依赖任何库,复制就能用。

先封装核心柏林噪声类(通用版)

不管生成啥纹理,先把这个核心类粘进去,后续所有纹理都基于它:

javascript

运行

class PerlinNoise {
  constructor(seed = Math.random()) {
    this.seed = seed;
    // 置换表:保证噪声的随机性和一致性
    this.p = [];
    this._initPermTable();
    // 2D梯度向量(8个方向,覆盖所有角度)
    this.gradients = [[1,1],[-1,1],[1,-1],[-1,-1],[1,0],[-1,0],[0,1],[0,-1]];
  }

  // 初始化置换表(Fisher-Yates洗牌,保证随机)
  _initPermTable() {
    const base = Array.from({length:256}, (_,i)=>i);
    const random = (s) => {
      const x = Math.sin(s++) * 10000;
      return x - Math.floor(x);
    };
    let r = random(this.seed);
    for(let i=255; i>0; i--) {
      const j = Math.floor(r*(i+1));
      [base[i], base[j]] = [base[j], base[i]];
      r = random(r);
    }
    this.p = base.concat(base); // 复制一份避免越界
  }

  // 平滑函数:让噪声过渡更自然
  _fade(t) {
    return t*t*t*(t*(t*6-15)+10);
  }

  // 线性插值
  _lerp(a, b, t) {
    return a + t*(b-a);
  }

  // 梯度计算核心
  _grad(hash, x, y) {
    const g = this.gradients[hash%8];
    return x*g[0] + y*g[1];
  }

  // 2D噪声计算(返回0-1之间的值)
  noise(x, y) {
    const X = Math.floor(x) & 255;
    const Y = Math.floor(y) & 255;
    x -= Math.floor(x);
    y -= Math.floor(y);
    const u = this._fade(x);
    const v = this._fade(y);

    const A = this.p[X] + Y;
    const B = this.p[X+1] + Y;

    const top = this._lerp(this._grad(this.p[A],x,y), this._grad(this.p[B],x-1,y), u);
    const bottom = this._lerp(this._grad(this.p[A+1],x,y-1), this._grad(this.p[B+1],x-1,y-1), u);
    return (this._lerp(top, bottom, v)+1)/2; // 归一化到0-1
  }

  // 多层噪声叠加(增加纹理细节)
  octaveNoise(x, y, octaves=4, persistence=0.5) {
    let total = 0, freq=1, amp=1, max=0;
    for(let i=0; i<octaves; i++) {
      total += this.noise(x*freq, y*freq)*amp;
      max += amp;
      amp *= persistence;
      freq *= 2;
    }
    return total/max;
  }
}

场景 1:木纹纹理(电商 / 卡片常用)

电商详情页的木纹卡片、家具类页面的背景,不用再找 UI 要图了:

html

预览

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>前端生成木纹纹理</title>
  <style>
    .card {
      width: 400px;
      height: 300px;
      margin: 50px auto;
      border-radius: 8px;
      overflow: hidden;
      box-shadow: 0 4px 12px rgba(0,0,0,0.2);
    }
  </style>
</head>
<body>
  <div class="card">
    <canvas id="woodCanvas" width="400" height="300"></canvas>
  </div>

  <script>
    // 先粘贴上面的PerlinNoise类
    
    class WoodTexture {
      constructor() {
        this.canvas = document.getElementById('woodCanvas');
        this.ctx = this.canvas.getContext('2d');
        this.noise = new PerlinNoise(123); // 固定种子,纹理不变
        this.drawWood();
      }

      drawWood() {
        const {width, height} = this.canvas;
        const imgData = this.ctx.createImageData(width, height);
        const data = imgData.data;
        const scale = 0.02; // 纹理密度,越小越稀疏

        for(let y=0; y<height; y++) {
          for(let x=0; x<width; x++) {
            // 核心:纵向条纹+细节噪点,模拟木纹
            const baseNoise = this.noise.noise(x*scale, y*scale*0.5); // 纵向主纹理
            const detailNoise = this.noise.octaveNoise(x*scale*4, y*scale*2, 2)*0.1; // 细节
            const noiseVal = baseNoise + detailNoise;

            // 映射为木纹颜色(棕色系,有自然波动)
            const baseR = 130, baseG = 80, baseB = 40;
            const offset = Math.floor(noiseVal*40); // 颜色波动范围
            const r = baseR + offset;
            const g = baseG + offset*0.8;
            const b = baseB + offset*0.5;

            const idx = (y*width + x)*4;
            data[idx] = r;
            data[idx+1] = g;
            data[idx+2] = b;
            data[idx+3] = 255;
          }
        }

        this.ctx.putImageData(imgData, 0, 0);
      }
    }

    window.onload = () => new WoodTexture();
  </script>
</body>
</html>

参数调整技巧:改scale(0.01-0.05)控制木纹密度,改baseR/baseG/baseB换木纹颜色(比如深胡桃木、浅橡木)。

场景 2:磨砂 / 毛玻璃纹理(后台 / 高级 UI)

金融后台、高端官网的磨砂面板,不用切图,代码生成更丝滑:

html

预览

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>前端生成磨砂纹理</title>
  <style>
    .panel {
      width: 500px;
      height: 300px;
      margin: 50px auto;
      position: relative;
      border-radius: 12px;
      overflow: hidden;
      background: #f5f5f5;
    }
    #frostCanvas {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      opacity: 0.8;
    }
  </style>
</head>
<body>
  <div class="panel">
    <canvas id="frostCanvas" width="500" height="300"></canvas>
    <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-size: 20px;">磨砂面板示例</div>
  </div>

  <script>
    // 先粘贴PerlinNoise类
    
    class FrostTexture {
      constructor() {
        this.canvas = document.getElementById('frostCanvas');
        this.ctx = this.canvas.getContext('2d');
        this.noise = new PerlinNoise(456);
        this.drawFrost();
      }

      drawFrost() {
        const {width, height} = this.canvas;
        const imgData = this.ctx.createImageData(width, height);
        const data = imgData.data;
        const scale = 0.03; // 磨砂颗粒大小

        for(let y=0; y<height; y++) {
          for(let x=0; x<width; x++) {
            // 多层噪声叠加,模拟磨砂的细微颗粒
            const noiseVal = this.noise.octaveNoise(x*scale, y*scale, 3);
            // 映射为浅灰色系,模拟磨砂
            const gray = 240 + Math.floor((noiseVal-0.5)*20); // 230-250之间波动
            const idx = (y*width + x)*4;
            data[idx] = gray;
            data[idx+1] = gray;
            data[idx+2] = gray;
            data[idx+3] = 255;
          }
        }

        this.ctx.putImageData(imgData, 0, 0);
        // 可选:加轻微模糊,更像毛玻璃
        this.ctx.filter = 'blur(0.5px)';
        this.ctx.drawImage(this.canvas, 0, 0);
      }
    }

    window.onload = () => new FrostTexture();
  </script>
</body>
</html>

优化点:加blur(0.5-1px)滤镜,磨砂效果更真实;调整gray的波动范围,控制磨砂深浅。

场景 3:石头纹理(小游戏 / 可视化)

小游戏的地面、地理可视化的岩石背景,生成的纹理比图片更自然:

javascript

运行

// 基于上面的PerlinNoise类,只改draw方法
class StoneTexture {
  constructor() {
    this.canvas = document.getElementById('stoneCanvas');
    this.ctx = this.canvas.getContext('2d');
    this.noise = new PerlinNoise(789);
    this.drawStone();
  }

  drawStone() {
    const {width, height} = this.canvas;
    const imgData = this.ctx.createImageData(width, height);
    const data = imgData.data;
    const scale = 0.015;

    for(let y=0; y<height; y++) {
      for(let x=0; x<width; x++) {
        // 多层噪声叠加,模拟石头的粗糙感
        const noiseVal = this.noise.octaveNoise(x*scale, y*scale, 5);
        // 石头颜色(灰色系,有深浅变化)
        const baseGray = 120;
        const offset = Math.floor((noiseVal-0.5)*60);
        const gray = baseGray + offset;

        const idx = (y*width + x)*4;
        data[idx] = gray;
        data[idx+1] = gray;
        data[idx+2] = gray;
        data[idx+3] = 255;
      }
    }

    this.ctx.putImageData(imgData, 0, 0);
  }
}

项目落地避坑指南(踩过的坑分享)

  1. 性能优化:如果纹理尺寸大(比如全屏背景),把生成逻辑放到 Web Worker 里,避免阻塞主线程;
  2. 种子固定:生成固定纹理时,给 PerlinNoise 传固定种子(比如 123),不然每次刷新纹理都变;
  3. 缓存复用:生成一次纹理后,转成 base64 存起来,不用每次刷新都重新生成;
  4. 兼容性:Canvas 和 ES6 语法在现代浏览器都支持,不用兼容 IE 的话直接用,要兼容就转 ES5。

总结

咱前端卷性能、卷体验,有时候不用盯着框架和库死磕,这种 “小技巧” 反而能立竿见影 —— 几行 JS 代码替代一堆图片,包体积降 80%+,加载速度提上去,用户体验直接起飞。

除了上面的木纹、磨砂、石头纹,还能扩展到布纹、星云纹、液态纹,核心都是 “把柏林噪声的 0-1 值映射成视觉属性”。各位道友可以自己试试改参数,玩出不同的纹理效果,写进简历 / 博客里,比单纯说 “会用 Canvas” 有料多了。

最后说一句:技术不是越复杂越好,能解决业务问题、提升性能的小技巧,才是真・前端干货。