数字故障美学:用 Canvas 实现 RGB 偏移、像素排序与扫描线
故障艺术 (Glitch Art) 不只是一种视觉风格------它是数据被错误解读时产生的意外美。本文将用 Canvas 实现 6 种失真算法,让你彻底理解「破坏」的美学。
什么是 Glitch Art?
在模拟信号时代,雪花屏、视频撕裂、磁带卡顿是技术缺陷。今天,这些「错误」成了一种视觉语言------赛博朋克的标志性元素、音乐视频的转场特效、独立游戏的滤镜风格。
本文将实现:
- RGB 通道偏移 (Channel Shift)
- 像素排序 (Pixel Sort)
- 扫描线 (Scanlines)
- 色差 (Chromatic Aberration)
- 水平撕裂 (Screen Tear)
- 数据损坏模拟 (Data Corruption)
1. RGB 通道偏移
将 R、G、B 三个通道分别偏移不同距离,模拟色差和信号错位。
javascript
function rgbShift(ctx, width, height, shiftX = 8, shiftY = 0) {
const imageData = ctx.getImageData(0, 0, width, height);
const src = imageData.data;
const dst = new Uint8ClampedArray(src.length);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
// R 通道向左偏移
const rx = Math.max(0, x - shiftX);
const rIdx = (y * width + rx) * 4;
// B 通道向右偏移
const bx = Math.min(width - 1, x + shiftX);
const bIdx = (y * width + bx) * 4;
// G 通道保持不变
dst[idx] = src[rIdx]; // R
dst[idx + 1] = src[idx + 1]; // G
dst[idx + 2] = src[bIdx + 2]; // B
dst[idx + 3] = 255; // A
}
}
ctx.putImageData(new ImageData(dst, width, height), 0, 0);
}
效果:画面出现红蓝双重影,仿佛 3D 电影摘下眼镜后的视觉。
2. 像素排序------Glitch 艺术的核心
像素排序是 glitch 艺术最具辨识度的技术:选取一行(或列)像素,按亮度排序,创造出「融化」的视觉效果。
javascript
function pixelSort(ctx, width, height, threshold = 128, vertical = false) {
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
for (let y = 0; y < height; y++) {
let start = -1;
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
const brightness = (data[idx] + data[idx + 1] + data[idx + 2]) / 3;
if (brightness < threshold && start === -1) {
start = x; // 暗部开始,标记排序区间
} else if (brightness >= threshold && start !== -1) {
sortSegment(data, y, start, x - 1, width);
start = -1;
}
}
if (start !== -1) {
sortSegment(data, y, start, width - 1, width);
}
}
ctx.putImageData(imageData, 0, 0);
}
function sortSegment(data, y, x1, x2, width) {
const pixels = [];
for (let x = x1; x <= x2; x++) {
const idx = (y * width + x) * 4;
pixels.push({
r: data[idx], g: data[idx + 1],
b: data[idx + 2], a: data[idx + 3],
brightness: (data[idx] + data[idx + 1] + data[idx + 2]) / 3,
});
}
pixels.sort((a, b) => a.brightness - b.brightness);
for (let i = 0; i < pixels.length; i++) {
const idx = (y * width + (x1 + i)) * 4;
data[idx] = pixels[i].r;
data[idx + 1] = pixels[i].g;
data[idx + 2] = pixels[i].b;
data[idx + 3] = pixels[i].a;
}
}
3. 扫描线
模拟 CRT 显示器的水平扫描线------每隔 N 行降低亮度。
javascript
function scanlines(ctx, width, height, gap = 3, opacity = 0.3) {
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
for (let y = 0; y < height; y++) {
if (y % gap === 0) continue; // 保留扫描线间隙
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
data[idx] = data[idx] * (1 - opacity);
data[idx + 1] = data[idx + 1] * (1 - opacity);
data[idx + 2] = data[idx + 2] * (1 - opacity);
}
}
ctx.putImageData(imageData, 0, 0);
}
4. 色差 (Chromatic Aberration)
与 RGB 偏移不同,色差模拟的是镜头缺陷------边缘比中心偏移更大。
javascript
function chromaticAberration(ctx, width, height, maxShift = 12) {
const imageData = ctx.getImageData(0, 0, width, height);
const src = imageData.data;
const dst = new Uint8ClampedArray(src.length);
const cx = width / 2, cy = height / 2;
const maxDist = Math.sqrt(cx * cx + cy * cy);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const idx = (y * width + x) * 4;
const dist = Math.sqrt((x - cx) ** 2 + (y - cy) ** 2);
const shift = (dist / maxDist) * maxShift; // 边缘偏移最大
const rx = clamp(x + shift, 0, width - 1);
const bx = clamp(x - shift, 0, width - 1);
dst[idx] = src[(y * width + Math.floor(rx)) * 4];
dst[idx + 1] = src[idx + 1];
dst[idx + 2] = src[(y * width + Math.floor(bx)) * 4 + 2];
dst[idx + 3] = 255;
}
}
ctx.putImageData(new ImageData(dst, width, height), 0, 0);
}
5. 水平撕裂
模拟 VHS 磁带损坏------随机行被替换为相邻行的内容。
javascript
function screenTear(ctx, width, height, intensity = 0.1) {
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
for (let y = 0; y < height - 1; y++) {
if (Math.random() > intensity) continue;
// 将当前行的部分像素替换为下一行的
const tearStart = Math.floor(Math.random() * width * 0.8);
const tearWidth = Math.floor(Math.random() * width * 0.3) + 10;
for (let x = tearStart; x < Math.min(tearStart + tearWidth, width); x++) {
const srcIdx = (y * width + x) * 4;
const dstIdx = ((y + 1) * width + (x + Math.floor(Math.random() * 20 - 10))) * 4;
const clampedIdx = Math.max(0, Math.min(dstIdx, data.length - 4));
data[srcIdx] = data[clampedIdx];
data[srcIdx + 1] = data[clampedIdx + 1];
data[srcIdx + 2] = data[clampedIdx + 2];
}
}
ctx.putImageData(imageData, 0, 0);
}
6. 数据损坏模拟 (Data Corruption)
随机替换像素块为纯色或噪点,模拟 JPEG 文件头损坏的效果。
javascript
function dataCorruption(ctx, width, height, blockCount = 5) {
for (let i = 0; i < blockCount; i++) {
const bx = Math.floor(Math.random() * (width - 40));
const by = Math.floor(Math.random() * (height - 40));
const bw = Math.floor(Math.random() * 30) + 20;
const bh = Math.floor(Math.random() * 30) + 20;
// 随机纯色块或噪点块
if (Math.random() > 0.5) {
ctx.fillStyle = `hsl(${Math.random() * 360}, 80%, ${Math.random() * 50 + 30}%)`;
ctx.fillRect(bx, by, bw, bh);
} else {
const imageData = ctx.getImageData(bx, by, bw, bh);
for (let j = 0; j < imageData.data.length; j += 4) {
imageData.data[j] = Math.random() * 255;
imageData.data[j + 1] = Math.random() * 255;
imageData.data[j + 2] = Math.random() * 255;
}
ctx.putImageData(imageData, bx, by);
}
}
}
组合使用
真正的 glitch 效果是多种算法叠加:
javascript
function fullGlitchPipeline(ctx, width, height) {
rgbShift(ctx, width, height, 6);
pixelSort(ctx, width, height, 140);
chromaticAberration(ctx, width, height, 4);
scanlines(ctx, width, height, 2, 0.2);
screenTear(ctx, width, height, 0.05);
dataCorruption(ctx, width, height, 3);
}
每种算法的参数都可以做成滑块,让用户实时调节------这就是 Glitch Studio 的核心设计。
在线体验
完整源码及实时 demo,支持 6 种失真算法叠加、参数调节和实时预览:
下一篇预告:《把摄像头变成终端艺术:实时 ASCII 字符画原理与实现》