数字画面里的岩彩感,经常被一个“随机颗粒”滑块概括。可所有像素都等概率变亮或变暗,得到的往往更像电视雪花。想让纸面有层次,至少要把底色、颗粒和边缘积色分开,知道每一层在改变什么。
先定义视觉目标
这里追求的是带颗粒的平面色块,不是矿物颜料的化学或光学模拟。底色决定大面积观感;少量局部颗粒让表面不均匀;靠近色块边界的深色带暗示积色。参数只控制图像,没有证据支持把它们直接命名为真实矿石粒径或胶液浓度。
下面用一个圆形掩膜生成 RGBA 数组。半径外完全透明,半径内以距边缘的距离决定深色带;伪随机数用于轻微噪声,并设置固定种子。固定种子不是为了证明自然,而是为了比较参数时不被另一次随机图案干扰。
读旧像素,写新像素
Canvas 的像素操作文档解释了ImageData的 RGBA 布局。浏览器接入时可将本例数组交给ImageData;这里输出简单的 PPM 文件,方便在无图形依赖的环境中检查。PPM 不支持透明,导出时会合成到白纸上,这个处理不能省略。
颗粒强度太高会吞掉轮廓;边缘带太宽会像描边。先用同一随机种子比较几个强度,再看缩小后的画面,通常比盯着放大像素更有用。若要接真实画笔,还需要处理采样间距、压感和重复叠色,本例没有覆盖。
运行node pigment.js会生成pigment.ppm。本地检查了种子复现、边界透明以及所有通道范围;没有运行浏览器 Canvas,也没有做真实颜料对比或性能跑分。给近似起一个准确名字,反而能让它成为一个好用的设计参数。
可独立运行的核心例子
const fs=require('node:fs'); function random(seed) {return ()=>{seed=(Math.imul(seed,1664525)+1013904223)>>>0;return seed/2**32;};} function pigment(w,h,seed) { const out=new Uint8ClampedArray(w*h*4), rand=random(seed), r=Math.min(w,h)*.4; for(let y=0;y<h;y++)for(let x=0;x<w;x++) { const d=Math.hypot(x-w/2,y-h/2),i=(y*w+x)*4; if(d>r) continue; const edge=Math.max(0,1-(r-d)/8),grain=(rand()-.5)*18; out.set([120+grain-30*edge,145+grain-30*edge,170+grain-25*edge,255],i); } return out; } function ppm(data,w,h) { const rgb=[]; for(let i=0;i<data.length;i+=4) { const a=data[i+3]/255; rgb.push(...[0,1,2].map(c=>Math.round(data[i+c]*a+255*(1-a)))); } return `P3\n${w} ${h}\n255\n`+rgb.join(' '); } const a=pigment(96,96,19),b=pigment(96,96,19); if(a.some((v,i)=>v!==b[i])||a[3]!==0)throw Error('复现或边界失败'); if(a.length!==96*96*4 || a.some(v=>v<0||v>255)) throw Error('像素越界'); fs.writeFileSync('pigment.ppm',ppm(a,96,96)); console.log('已生成 pigment.ppm;固定种子19');内图解释叠层思路,不是圆形像素例子的输出截图。