ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

柔和水粉边缘晕染算法:Canvas 离屏径向渐变混合模拟纸质吸水

柔和水粉边缘晕染算法:Canvas 离屏径向渐变混合模拟纸质吸水 在纸质手账的创作中水粉Gouache与透明水彩一直是最能表达秋日情绪的媒介。一片枫叶的上色绝不是单调均匀的色块平铺当水分在粗纹水彩纸上慢慢化开时颜料颗粒会随着水流在纸张纤维缝隙中轻柔扩散形成内侧轻盈透明、外沿却微微沉淀出一圈深色“水痕Water Edge”的奇妙质感。但在传统的数字绘画与 Web 画板实现中许多涂色工具采用的是均一的纯色填充算法如 Flood Fill或者简单的实心圆形画笔。这样的笔触边缘冰冷僵硬完全失去了手绘天然的呼吸感与偶然性。为了在手账插画上色功能中重现这种真实温润的纸质吸水晕染效果听汐探索了一套纯前端的水粉边缘晕染仿真算法。该算法不依赖复杂的 3D 着色器或重量级图像库仅依靠 HTML5 2D Canvas 的离屏多层径向渐变插值、物理毛细扩散扰动与正片叠底Multiply混合模式即可在轻量级浏览器环境中实时渲染出以假乱真的水粉笔触。一、真实水粉纸面扩散的物理特征在编写图形算法前我们先拆解真实水粉颜料在吸水纸张上的三大视觉表现内实外虚与羽化过渡Radial Attenuation笔尖落纸处的色度最为浓郁随后颜料随着水分向四周呈非线性的指数衰减直至边缘变得轻薄通透。表面张力导致的边缘深色水痕Pigment Edge Deposition在水分蒸发过程中表面张力会将悬浮在液体中的微小颜料颗粒向外圈推移导致干涸后的最外边缘往往比靠近边缘的内圈更深一点这就是手绘独有的“收边感”。纸张纤维阻滞与不规则边缘Fiber Irregularity水彩纸并非绝对平滑微小的棉浆纤维凹凸会导致扩散并非完美的同心圆而是带有微小的毛刺与参差起伏。二、算法设计分层渲染与混合管道我们将单次落笔的晕染渲染拆解为以下流水线[用户笔触落点 (x, y, radius, color)] │ ├─► 阶段 1生成多级不规则抖动外轮廓点集高斯微扰 │ ├─► 阶段 2离屏 Canvas 构建非线性径向渐变色阶模拟中心浓郁、边缘深色水痕 │ ├─► 阶段 3配合 globalCompositeOperation multiply正片叠底与已有的底色重叠相溶 │ └─► 阶段 4叠加大颗粒纸张纤维掩码Noise Mask完成最终纸质融合三、纯前端核心工程实战代码下面是封装好的水粉晕染笔刷引擎可以直接挂载到任意 Web 交互画布中export interface GouacheDropOptions { x: number; y: number; radius: number; baseColor: { r: number; g: number; b: number }; // RGB 基础色 opacity?: number; // 基础透明度0.3 ~ 0.8 wetness?: number; // 纸面湿度影响扩散范围1.0 ~ 2.0 } export class GouacheWashRenderer { private offscreenCanvas: HTMLCanvasElement; private offscreenCtx: CanvasRenderingContext2D; constructor(maxRadius 200) { this.offscreenCanvas document.createElement(canvas); this.offscreenCanvas.width maxRadius * 4; this.offscreenCanvas.height maxRadius * 4; this.offscreenCtx this.offscreenCanvas.getContext(2d, { willReadFrequently: true })!; } /** * 在目标画布上绘制一朵自然晕开的水粉水滴 */ public drawGouacheDrop( targetCtx: CanvasRenderingContext2D, options: GouacheDropOptions ): void { const { x, y, radius, baseColor, opacity 0.5, wetness 1.3 } options; const { r, g, b } baseColor; const actualRadius radius * wetness; const cx this.offscreenCanvas.width / 2; const cy this.offscreenCanvas.height / 2; // 清空离屏画布 this.offscreenCtx.clearRect(0, 0, this.offscreenCanvas.width, this.offscreenCanvas.height); // 1. 构建模拟“水痕”的特殊非线性径向渐变 const grad this.offscreenCtx.createRadialGradient(cx, cy, 0, cx, cy, actualRadius); // 中心区域浓郁而饱满 grad.addColorStop(0, rgba(${r}, ${g}, ${b}, ${opacity * 0.95})); // 中间过渡轻薄通透的水感 grad.addColorStop(0.5, rgba(${r}, ${g}, ${b}, ${opacity * 0.6})); // 次边缘最清浅的半透明区 grad.addColorStop(0.85, rgba(${r}, ${g}, ${b}, ${opacity * 0.4})); // 关键外沿颜料颗粒沉淀聚集色度骤升形成标志性的深色水痕收边 grad.addColorStop(0.96, rgba(${r * 0.85}, ${g * 0.85}, ${b * 0.85}, ${opacity * 0.75})); // 彻底消散在水彩纸上 grad.addColorStop(1.0, rgba(${r}, ${g}, ${b}, 0)); this.offscreenCtx.fillStyle grad; // 2. 绘制带有微小纤维毛刺的不规则多边形轮廓 this.drawIrregularSplotch(this.offscreenCtx, cx, cy, actualRadius); // 3. 将离屏成果以正片叠底Multiply模式合成到主手账画布上 targetCtx.save(); // 正片叠底模拟颜料层层渗透进纸纤维的深色叠加物理特性 targetCtx.globalCompositeOperation multiply; targetCtx.drawImage( this.offscreenCanvas, cx - actualRadius, cy - actualRadius, actualRadius * 2, actualRadius * 2, x - actualRadius, y - actualRadius, actualRadius * 2, actualRadius * 2 ); targetCtx.restore(); } /** * 模拟水分与纸张微孔摩擦的不规则形状生成器 */ private drawIrregularSplotch( ctx: CanvasRenderingContext2D, cx: number, cy: number, r: number ): void { const segments 36; // 36 个顶点构成平滑闭合回路 ctx.beginPath(); for (let i 0; i segments; i) { const angle (i / segments) * Math.PI * 2; // 在半径方向引入 4% 到 8% 的随机轻微扰动 const jitter 1 (Math.sin(i * 3.7) * 0.04 (Math.random() - 0.5) * 0.03); const curRadius r * jitter; const px cx Math.cos(angle) * curRadius; const py cy Math.sin(angle) * curRadius; if (i 0) { ctx.moveTo(px, py); } else { ctx.lineTo(px, py); } } ctx.closePath(); ctx.fill(); } }四、动态叠加上色与笔触干湿控制在手账插画绘制时用户往往会按住鼠标或 Apple Pencil 进行连续拖动。如果简单地在mousemove每次触发时都画一个完整的渐变圆会因为密度过大导致画面瞬间变成一块死黑。为了实现水粉湿画法Wet-on-Wet中流畅的笔触连贯感我们引入笔触距离插值与湿度衰减器export class GouacheBrushController { private renderer new GouacheWashRenderer(150); private lastX 0; private lastY 0; private currentWetness 1.4; public startStroke(x: number, y: number): void { this.lastX x; this.lastY y; this.currentWetness 1.4; // 刚蘸水时湿度最高 } public moveStroke( targetCtx: CanvasRenderingContext2D, curX: number, curY: number, color: { r: number; g: number; b: number } ): void { const dist Math.hypot(curX - this.lastX, curY - this.lastY); // 限制最小落笔间距防止重复渲染同一区域 if (dist 12) return; // 随着笔画延伸湿度稍微降低笔迹逐渐变干 this.currentWetness Math.max(1.0, this.currentWetness - 0.015); this.renderer.drawGouacheDrop(targetCtx, { x: curX, y: curY, radius: 28, baseColor: color, opacity: 0.35, wetness: this.currentWetness }); this.lastX curX; this.lastY curY; } }五、纸张纹理底图Grain Texture的双向映射即使渐变再平滑如果底下是一块惨白的纯色背景也会让人觉得是数码图形。我们在整个手账画板底层铺设了一张高分辨率的米黄色粗纹水彩纸Cold-Pressed Paper纹理。在 Canvas 初始化时使用ctx.drawImage将水彩纸背景绘制一次随后水粉算法的所有multiply正片叠底操作都会自动将纸张凹凸深浅的阴影包含进颜料层。这样一来较深的纸纹凹陷处会自动沉淀更多颜色凸起处则微微泛光真正达到了手绘水粉在吸水纸面上呼吸生动的视觉神韵。六、结语计算机图形学的迷人之处并不总是追求最锋利、最高清的几何边缘。在充斥着像素对齐与扁平色块的数字屏幕上用数学算法小心翼翼地找回水滴在纸面上那一圈淡淡的干涸水痕是手账开发者给生活最浪漫的技术致意。
返回列表