关于 dither-background
为设计 Agent 提供单色抖动背景的制作指南,适用于深色界面中的氛围背景。它强调可见的方形像素、有序阈值图案和缓慢漂移的有机形态。
主要功能
- 014×4 Bayer 式有序抖动
- 02近黑与灰阶配色
- 03方形像素与有机波形
- 04渐晕边缘控制亮度
- 05Canvas 动效与性能调节
适用场景
- 01为深色界面添加背景层
- 02衬托主视觉或数据叠层
- 03制作单色程序化纹理
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ mengto/ skills --skill dither-background安装后在 Claude Code 中输入/dither-background确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
demo/index.htmlPROMPT.mdsource.json详细介绍
抖动背景
适用场景
- 深色界面需要一层富有氛围感的单色背景。
- 视觉效果需要呈现放大的方形像素和清晰可见的有序抖动。
- 设计需要有机波浪、云状团块,或无需彩色渐变的程序化纵深感。
- 背景需要衬托带边框的 UI、主视觉内容或数据叠层。
视觉目标
- 近黑色底色,搭配炭灰色中间调、柔和的灰色层次和偶尔的白色高光。
- 方形像素格清晰可见,而非细小的胶片颗粒。
- 采用 4x4 Bayer 风格的抖动图案或等效的有序阈值处理。
- 形成宽阔的有机波浪或云状团块,而非随机的电视噪点。
- 边缘带有渐晕效果,让较亮的团块位于中央或偏离中心的位置。
HTML 和 CSS
<canvas class="dither-background" data-dither-background></canvas>
.dither-background {
position: fixed;
inset: 0;
z-index: 0;
width: 100vw;
height: 100vh;
background: #030303;
pointer-events: none;
}
.page-content {
position: relative;
z-index: 1;
}
Canvas 实现方案
需要动态效果或程序化纵深感时,使用真实的 canvas。
const BAYER_4X4 = [
0, 8, 2, 10,
12, 4, 14, 6,
3, 11, 1, 9,
15, 7, 13, 5,
].map((value) => (value + 0.5) / 16);
function smoothstep(edge0, edge1, value) {
const t = Math.max(0, Math.min(1, (value - edge0) / (edge1 - edge0)));
return t * t * (3 - 2 * t);
}
function noise2(x, y) {
const value = Math.sin(x * 127.1 + y * 311.7) * 43758.5453123;
return value - Math.floor(value);
}
function valueNoise(x, y) {
const ix = Math.floor(x);
const iy = Math.floor(y);
const fx = x - ix;
const fy = y - iy;
const ux = fx * fx * (3 - 2 * fx);
const uy = fy * fy * (3 - 2 * fy);
const a = noise2(ix, iy);
const b = noise2(ix + 1, iy);
const c = noise2(ix, iy + 1);
const d = noise2(ix + 1, iy + 1);
return (
a * (1 - ux) * (1 - uy) +
b * ux * (1 - uy) +
c * (1 - ux) * uy +
d * ux * uy
);
}
function fbm(x, y) {
let value = 0;
let amplitude = 0.5;
let frequency = 1;
for (let octave = 0; octave < 4; octave++) {
value += valueNoise(x * frequency, y * frequency) * amplitude;
frequency *= 2.02;
amplitude *= 0.5;
}
return value;
}
function initDitherBackground(canvas, options = {}) {
if (!canvas) return () => {};
const ctx = canvas.getContext("2d", { alpha: false });
if (!ctx) return () => {};
const reduceMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const cell = options.cellSize || 7;
const maxDpr = options.maxDpr || 1.5;
let width = 1;
let height = 1;
let cols = 1;
let rows = 1;
let rafId = 0;
const palette = options.palette || [
[3, 3, 3],
[16, 16, 17],
[34, 35, 37],
[74, 75, 78],
[168, 169, 171],
[236, 236, 232],
];
function resize() {
const dpr = Math.min(window.devicePixelRatio || 1, maxDpr);
width = Math.max(1, window.innerWidth);
height = Math.max(1, window.innerHeight);
canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
cols = Math.ceil(width / cell);
rows = Math.ceil(height / cell);
}
function sampleField(x, y, time) {
const nx = (x / cols - 0.5) * 2;
const ny = (y / rows - 0.5) * 2;
const distance = Math.sqrt(nx * nx * 0.84 + ny * ny * 1.28);
const vignette = 1 - smoothstep(0.18, 1.15, distance);
const drift = reduceMotion ? 0 : time * 0.018;
const wave =
Math.sin(nx * 2.8 + ny * 1.2 + drift) * 0.18 +
Math.sin(nx * -1.4 + ny * 3.8 - drift * 0.8) * 0.14;
const cloud = fbm(nx * 1.35 + drift * 0.16, ny * 1.35 - drift * 0.08);
const ridge = smoothstep(0.48, 0.92, cloud + wave);
const offAxisMass = smoothstep(0.98, 0.18, Math.hypot(nx + 0.22, ny - 0.08));
return Math.max(0, Math.min(1, ridge * vignette * 0.92 + offAxisMass * 0.18));
}
function render(time = 0) {
const seconds = time * 0.001;
ctx.fillStyle = "rgb(3,3,3)";
ctx.fillRect(0, 0, width, height);
for (let y = 0; y < rows; y++) {
for (let x = 0; x < cols; x++) {
const threshold = BAYER_4X4[(y % 4) * 4 + (x % 4)];
const brightness = sampleField(x, y, seconds);
const stepped = Math.floor(Math.max(0, Math.min(0.999, brightness + threshold * 0.18)) * palette.length);
const color = palette[Math.min(palette.length - 1, stepped)];
ctx.fillStyle = `rgb(${color[0]},${color[1]},${color[2]})`;
ctx.fillRect(x * cell, y * cell, cell, cell);
}
}
if (!reduceMotion) rafId = requestAnimationFrame(render);
}
function handleResize() {
cancelAnimationFrame(rafId);
resize();
render();
}
resize();
render();
window.addEventListener("resize", handleResize);
return () => {
cancelAnimationFrame(rafId);
window.removeEventListener("resize", handleResize);
};
}
const cleanupDither = initDitherBackground(
document.querySelector("[data-dither-background]"),
{
cellSize: 7,
maxDpr: 1.5,
}
);
调节参数
- 像素格尺寸:
5px-10px;较大的格子能让 Bayer 矩阵更清晰易辨。 - 调色板:仅使用近黑色、炭灰色、柔和的灰色和少量白色高光。
- 形状:调整
wave、cloud、ridge和offAxisMass,形成宽阔的团块。 - 渐晕:当前景需要更简洁时,增强边缘衰减。
- 动态效果:保持缓慢漂移;使用较小的时间乘数并避免闪烁。
- 性能:简化场域前,先增大
cellSize或限制maxDpr。
构图说明
- 将 canvas 放在界面后方,并设置
pointer-events: none。 - 将其用作带边框的 UI、主视觉文案或数据叠层的氛围背景。
- 控制前景对比度,并保持排版清晰。
- 让一个主要的亮部团块定义构图;避免全屏亮度均匀。
避免
- 彩虹渐变、彩色噪点,或缺少抖动结构的柔和模糊色块。
- 方形矩阵变得不可见的细小颗粒或静电般的斑点。
- 与前景文字争抢注意力的明亮全屏白噪点。
- 逐帧随机噪点造成的闪烁,而非缓慢漂移。
- 整个视口都被亮度相同的像素格覆盖。
快速检查
- 从正常观看距离能看清方形 Bayer 图案。
- 场域形成宽阔的有机波浪或云状团块。
- 调色板保持单色且克制。
- 边缘渐变至近黑色。
- 无需使用厚重的叠层,前景 UI 也清晰可读。
评论
支持 Markdown