重复 HTML canvas 元素(框)以填充整个视口
Repeat HTML canvas element (box) to fill whole viewport
我有一个动画 canvas 可以在背景上创建噪音效果。我的代码的简短解释如下:使用 for
循环我在 canvas 周围随机绘制 1px X 1px 方块(点)。它会产生静态噪音(颗粒)。稍后,我用 requestAnimationFrame
.
对其进行动画处理
我的问题是,如果我将 canvas 的原始大小设置为整个视口,浏览器将无法处理巨大的绘图 space 并且动画会变得极其缓慢。所以我需要保持 canvas 的原始尺寸较小,例如 50px,并在整个视口上重复它。
我知道我可以轻松使用 document.body.style.backgroundImage
,它本身会自动重复元素,但它不符合我的业务逻辑。我需要这个 canvas 元素来覆盖页面上的任何其他元素并且是透明的。只设置不透明度也不适合我。
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.className = "canvases";
canvas.width = 500;
canvas.height = 500;
document.body.appendChild(canvas);
function generateNoise() {
window.requestAnimationFrame(generateNoise);
ctx.clearRect(0, 0, canvas.width, canvas.height);
let x;
let y;
let rgb;
for (x = 0; x < canvas.width; x++) {
for (y = 0; y < canvas.height; y++) {
rgb = Math.floor(Math.random() * 255);
ctx.fillStyle = `rgba(${rgb}, ${rgb}, ${rgb}, 0.2`;
ctx.fillRect(x, y, 1, 1);
}
}
canvas.setAttribute("style", "position: absolute;");
canvas.style.zIndex = 1;
document.body.appendChild(canvas);
}
generateNoise();
.canvases {
margin: 50px;
border: 1px solid black;
}
以及未来使用 element()
的想法。实际上它只适用于 Firefox
The element() function allows an author to use an element in the document as an image. As the referenced element changes appearance, the image changes as well. This can be used, for example, to create live previews of the next/previous slide in a slideshow, or to reference a canvas element for a fancy generated gradient or even an animated background. ref
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.id = "canvases";
canvas.width = 100;
canvas.height = 100;
document.body.appendChild(canvas);
function generateNoise() {
window.requestAnimationFrame(generateNoise);
ctx.clearRect(0, 0, canvas.width, canvas.height);
let x;
let y;
let rgb;
for (x = 0; x < canvas.width; x++) {
for (y = 0; y < canvas.height; y++) {
rgb = Math.floor(Math.random() * 255);
ctx.fillStyle = `rgba(${rgb}, ${rgb}, ${rgb}, 0.2`;
ctx.fillRect(x, y, 1, 1);
}
}
document.querySelector('.hide').appendChild(canvas);
}
generateNoise();
.hide {
height:0;
overflow:hidden;
}
body::before {
content:"";
position:fixed;
z-index:9999;
top:0;
left:0;
right:0;
bottom:0;
pointer-events:none;
opacity:0.8;
background:-moz-element(#canvases); /* use the canvas as background and repeat it */
}
<div class="hide"></div>
<h1>a title</h1>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nec sagittis purus. Integer eget ante nec nisi malesuada vehicula. Vivamus urna velit, sodales in consequat ac, elementum id felis. Nulla vitae interdum eros. Aliquam non enim leo. Nunc blandit odio ut lectus egestas, nec luctus dui blandit. Suspendisse sed magna vel lorem mattis pretium sit amet in quam. Aenean varius elementum massa at gravida.
您可以创建一个离屏 canvas 您的磁贴大小,例如50x50,不要将其附加到 DOM 并在其上生成随机噪声。 canvas 的像素数据可以使用以下方法检索:
data=getImageData(0, 0, offScreenCanvas.width, offScreenCanvas.height);
然后可以使用
将这些数据进一步用于绘制到屏幕上canvas
putImageData(data, x, y);
因此,如果您使用 for 循环将该数据放置在屏幕外 canvas 大小的 x 和倍数 - 50 - 您可以使用此图块填充整个 canvas。
一个问题是您会在这些瓷砖之间看到明显的接缝。作为补偿,我会添加另一个 for 循环并在某个随机点绘制该图块。
这是一个例子:
var canvas = document.createElement("canvas");
var offScreenCanvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var offCtx = offScreenCanvas.getContext("2d");
canvas.className = "canvases";
canvas.width = 500;
canvas.height = 500;
offScreenCanvas.width = 50;
offScreenCanvas.height = 50;
document.body.appendChild(canvas);
canvas.setAttribute("style", "position: absolute;");
canvas.style.zIndex = 1;
function generateNoise() {
window.requestAnimationFrame(generateNoise);
ctx.clearRect(0, 0, canvas.width, canvas.height);
let x;
let y;
let rgb;
for (x = 0; x < offScreenCanvas.width; x++) {
for (y = 0; y < offScreenCanvas.height; y++) {
rgb = Math.floor(Math.random() * 255);
offCtx.fillStyle = `rgba(${rgb}, ${rgb}, ${rgb}, 0.2`;
offCtx.fillRect(x, y, 1, 1);
}
}
var data = offCtx.getImageData(0, 0, offScreenCanvas.width, offScreenCanvas.height);
for (x = 0; x < canvas.width; x += offScreenCanvas.width) {
for (y = 0; y < canvas.height; y += offScreenCanvas.height) {
ctx.rotate(parseInt(Math.random() * 4) * 90 * Math.PI / 180);
ctx.putImageData(data, x, y);
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
}
for (a = 0; a < 40; a++) {
ctx.putImageData(data, parseInt(Math.random() * canvas.width), parseInt(Math.random() * canvas.height));
}
}
generateNoise();
.canvases {
margin: 50px;
border: 1px solid black;
}
在 canvas 上生成噪点(通常在像素级工作)的最佳方法是使用 ImageData。
一个一个地绘制每个像素将非常慢(对 GPU 的大量指令),而在 TypedArray 上进行简单循环非常快。
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
let image; // using let because we may change it on resize
onresize = (evt) => {
const width = canvas.width = window.innerWidth;
const height = canvas.height =window.innerHeight;
image = new ImageData(width, height);
};
onresize();
const anim = (t) => {
const arr = new Uint32Array(image.data.buffer);
for( let i = 0; i < arr.length; i++ ) {
// random color with fixed 0.2 opacity
arr[i] = (Math.random() * 0xFFFFFF) + 0x33000000;
}
ctx.putImageData(image, 0, 0);
requestAnimationFrame(anim);
};
anim();
canvas { position: absolute; top: 0; left: 0 }
<canvas></canvas>
如果还是太慢,你也可以只对canvas尺寸的一部分产生噪点,再画多次:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
let image; // using let because we may change it on resize
onresize = (evt) => {
const width = canvas.width = window.innerWidth;
const height = canvas.height =window.innerHeight;
image = new ImageData(width / 2, height / 2);
};
onresize();
const anim = (t) => {
ctx.clearRect(0,0,canvas.width,canvas.height);
const arr = new Uint32Array(image.data.buffer);
for( let i = 0; i < arr.length; i++ ) {
// random color with fixed 0.2 opacity
arr[i] = (Math.random() * 0xFFFFFF) + 0x33000000;
}
const width = image.width;
const height = image.height;
ctx.putImageData(image, 0, 0);
ctx.drawImage(canvas, 0, 0, width, height, width, 0, width, height);
ctx.drawImage(canvas, 0, 0, width * 2, height, 0, height, width * 2, height);
requestAnimationFrame(anim);
};
anim();
canvas { position: absolute; top: 0; left: 0 }
<canvas></canvas>
骗人眼球
以 60fps 的速度呈现信息时,您可以很容易地欺骗人眼。
视觉 FX 永远不应影响性能,因为视觉 FX 是次要呈现,不如主要内容重要,主要内容应该在 CPU 周期中占据最大份额。
该代码段的 运行 渲染成本为每帧 1 fillRect
次调用,用 CPU 周期换取内存。我认为现在的问题是它太快了,也许你可能想将速率降低到 30fps(参见选项)。
var ctx = canvas.getContext("2d");
var noiseIdx = 0
const noiseSettings = [
[128, 10, 1, 1],
[128, 10, 2, 1],
[128, 10, 4, 1],
[128, 10, 8, 1],
[128, 10, 1, 2],
[128, 10, 8, 2],
[256, 20, 1, 1],
[256, 20, 1, 2],
[32, 30, 1, 1],
[64, 20, 1, 1],
];
var noise, rate, frame = 0;
setNoise();
function setNoise() {
const args = noiseSettings[noiseIdx++ % noiseSettings.length];
noise = createNoise(...args);
info.textContent = "Click to cycle. Res: " + args[0] + " by " + args[0] + "px " + args[1] +
" frames. Noise power: " + args[2] + " " + (60 / args[3]) + "FPS";
rate = args[3];
}
mainLoop();
function mainLoop() {
if (ctx.canvas.width !== innerWidth || ctx.canvas.height !== innerHeight) {
canvas.width = innerWidth;
canvas.height = innerHeight;
} else {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
frame++;
noise(ctx, (frame % rate) !== 0);
requestAnimationFrame(mainLoop);
}
canvas.addEventListener("click", setNoise);
function createNoise(size, frameCount, pow = 1) {
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const frames = [];
while (frameCount--) { frames.push(createNoisePattern(canvas)) }
var prevFrame = -1;
const mat = new DOMMatrix();
return (ctx, hold = false) => {
if (!hold || prevFrame === -1) {
var f = Math.random() * frames.length | 0;
f = f === prevFrame ? (f + 1) % frames.length : f;
mat.a = Math.random() < 0.5 ? -1 : 1;
mat.d = Math.random() < 0.5 ? -1 : 1;
mat.e = Math.random() * size | 0;
mat.f = Math.random() * size | 0;
prevFrame = f;
frames[f].setTransform(mat);
}
ctx.fillStyle = frames[prevFrame];
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
function createNoisePattern(canvas) {
const ctx = canvas.getContext("2d");
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const d32 = new Uint32Array(imgData.data.buffer);
const alpha = (0.2 * 255) << 24;
var i = d32.length;
while (i--) {
const r = Math.random()**pow * 255 | 0;
d32[i] = (r << 16) + (r << 8) + r + alpha;
}
ctx.putImageData(imgData, 0, 0);
return ctx.createPattern(canvas, "repeat")
}
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
<div id="info"></div>
最佳浏览整页。
它是如何工作的
函数 createNoise(size, count, pow)
创建 count
随机噪声模式。图案的分辨率为size
。函数 returns 一个在使用 2D 上下文调用时用随机模式之一填充上下文的函数。
图案偏移是随机设置的,也是沿两个轴随机镜像的。还有一个快速测试,以确保相同的图案不会连续绘制两次(超过所需的帧速率)。
最终效果是随机噪声,人眼几乎无法区分真正的随机噪声。
返回的函数有第二个[可选]参数,如果为真,则重绘与前一帧相同的模式,允许噪声效果的帧速率独立于任何正在渲染的内容,在噪声之下或之上。
模拟真实噪音
createNoise(size, count, pow)
的第三个参数,[pow]
是可选的。
在胶片和较旧的模拟 CRT 上看到的噪声分布不均匀。高能(更亮)噪声比低能噪声少得多。
参数 pow
控制噪音的分布。值为 1 时分布均匀(每个能级出现的几率相同)。值 > 1 会降低分布高能量侧的几率。我个人会使用 8 的值,但这当然是个人品味问题。
代码段选项
为了帮助评估此方法,请单击 canvas 循环预设。预设修改随机模式的数量、每个模式的分辨率、噪声功率和噪声的帧速率(注意帧速率为 60FPS,噪声帧速率与基本速率无关)
与您的代码最匹配的设置是第一个(分辨率 128 x 128 10 帧,功率:1,帧速率 60)
请注意,当图案的分辨率等于或低于 64 时,您可以开始看到重复的图案,即使添加许多随机帧也不会降低此 FX
这个可以改进吗
是的,但您需要进入 WebGL 的世界。 WebGL 着色器可以比 2D fillRect
填充 canvas.
更快地渲染此效果
WebGL 不仅仅是 3D,它适用于任何有像素的东西。如果您进行了大量的视觉工作,那么花时间学习 WebGL 是非常值得的,因为它通常可以通过 2D API.
使不可能的事情成为可能。
WebGL / Canvas 2D 混合解决方案
以下是使用 WebGL 的快速破解解决方案。 (因为我怀疑你会使用这种方法,所以我没有看到投入太多时间的意义)
它被构建为与 2D API 兼容。呈现的 WebGL 内容呈现为图像,然后您只需在 2D API 内容的顶部呈现。
var w, h, cw, ch, canvas, ctx, globalTime,webGL;
const NOISE_ALPHA = 0.5;
const NOISE_POWER = 1.2;
const shadersSource = {
VertexShader : {
type : "VERTEX_SHADER",
source : `
attribute vec2 position;
uniform vec2 resolution;
varying vec2 texPos;
void main() {
gl_Position = vec4((position / resolution) * 2.0 - 1.0, 0.0, 1.0);
texPos = gl_Position.xy;
}`
},
FragmentShader : {
type : "FRAGMENT_SHADER",
source : `
precision mediump float;
uniform float time;
varying vec2 texPos;
const float randC1 = 43758.5453;
const vec3 randC2 = vec3(12.9898, 78.233, 151.7182);
float randomF(float seed) {
return pow(fract(sin(dot(gl_FragCoord.xyz + seed, randC2)) * randC1 + seed), ${NOISE_POWER.toFixed(4)});
}
void main() {
gl_FragColor = vec4(vec3(randomF((texPos.x + 1.01) * (texPos.y + 1.01) * time)), ${NOISE_ALPHA});
}`
}
};
var globalTime = performance.now();
resizeCanvas();
startWebGL(ctx);
ctx.font = "64px arial black";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
function webGLRender(){
var gl = webGL.gl;
gl.uniform1f(webGL.locs.timer, globalTime / 100 + 100);
gl.drawArrays(gl.TRIANGLES, 0, 6);
ctx.drawImage(webGL, 0, 0, canvas.width, canvas.height);
}
function display(){
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "red";
ctx.fillText("Hello world "+ (globalTime / 1000).toFixed(1), cw, ch);
webGL && webGLRender();
}
function update(timer){ // Main update loop
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
requestAnimationFrame(update);
// creates vertex and fragment shaders
function createProgramFromScripts( gl, ids) {
var shaders = [];
for (var i = 0; i < ids.length; i += 1) {
var script = shadersSource[ids[i]];
if (script !== undefined) {
var shader = gl.createShader(gl[script.type]);
gl.shaderSource(shader, script.source);
gl.compileShader(shader);
shaders.push(shader);
}else{
throw new ReferenceError("*** Error: unknown script ID : " + ids[i]);
}
}
var program = gl.createProgram();
shaders.forEach((shader) => { gl.attachShader(program, shader); });
gl.linkProgram(program);
return program;
}
function createCanvas() {
var c,cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
function resizeCanvas() {
if (canvas === undefined) { canvas = createCanvas() }
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
setGlobals && setGlobals();
}
function setGlobals(){
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
// setup simple 2D webGL
function startWebGL(ctx) {
webGL = document.createElement("canvas");
webGL.width = ctx.canvas.width;
webGL.height = ctx.canvas.height;
webGL.gl = webGL.getContext("webgl");
var gl = webGL.gl;
var program = createProgramFromScripts(gl, ["VertexShader", "FragmentShader"]);
gl.useProgram(program);
var positionLocation = gl.getAttribLocation(program, "position");
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0.0, 0.0,1.0, 0.0,0.0, 1.0,0.0, 1.0,1.0, 0.0,1.0, 1.0]), gl.STATIC_DRAW);
var resolutionLocation = gl.getUniformLocation(program, "resolution");
webGL.locs = {
timer: gl.getUniformLocation(program, "time"),
};
gl.uniform2f(resolutionLocation, webGL.width, webGL.height);
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
setRectangle(gl, 0, 0, ctx.canvas.width, ctx.canvas.height);
}
function setRectangle(gl, x, y, width, height) {
var x1 = x;
var x2 = x + width;
var y1 = y;
var y2 = y + height;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]), gl.STATIC_DRAW);
}
我有一个动画 canvas 可以在背景上创建噪音效果。我的代码的简短解释如下:使用 for
循环我在 canvas 周围随机绘制 1px X 1px 方块(点)。它会产生静态噪音(颗粒)。稍后,我用 requestAnimationFrame
.
我的问题是,如果我将 canvas 的原始大小设置为整个视口,浏览器将无法处理巨大的绘图 space 并且动画会变得极其缓慢。所以我需要保持 canvas 的原始尺寸较小,例如 50px,并在整个视口上重复它。
我知道我可以轻松使用 document.body.style.backgroundImage
,它本身会自动重复元素,但它不符合我的业务逻辑。我需要这个 canvas 元素来覆盖页面上的任何其他元素并且是透明的。只设置不透明度也不适合我。
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.className = "canvases";
canvas.width = 500;
canvas.height = 500;
document.body.appendChild(canvas);
function generateNoise() {
window.requestAnimationFrame(generateNoise);
ctx.clearRect(0, 0, canvas.width, canvas.height);
let x;
let y;
let rgb;
for (x = 0; x < canvas.width; x++) {
for (y = 0; y < canvas.height; y++) {
rgb = Math.floor(Math.random() * 255);
ctx.fillStyle = `rgba(${rgb}, ${rgb}, ${rgb}, 0.2`;
ctx.fillRect(x, y, 1, 1);
}
}
canvas.setAttribute("style", "position: absolute;");
canvas.style.zIndex = 1;
document.body.appendChild(canvas);
}
generateNoise();
.canvases {
margin: 50px;
border: 1px solid black;
}
以及未来使用 element()
的想法。实际上它只适用于 Firefox
The element() function allows an author to use an element in the document as an image. As the referenced element changes appearance, the image changes as well. This can be used, for example, to create live previews of the next/previous slide in a slideshow, or to reference a canvas element for a fancy generated gradient or even an animated background. ref
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.id = "canvases";
canvas.width = 100;
canvas.height = 100;
document.body.appendChild(canvas);
function generateNoise() {
window.requestAnimationFrame(generateNoise);
ctx.clearRect(0, 0, canvas.width, canvas.height);
let x;
let y;
let rgb;
for (x = 0; x < canvas.width; x++) {
for (y = 0; y < canvas.height; y++) {
rgb = Math.floor(Math.random() * 255);
ctx.fillStyle = `rgba(${rgb}, ${rgb}, ${rgb}, 0.2`;
ctx.fillRect(x, y, 1, 1);
}
}
document.querySelector('.hide').appendChild(canvas);
}
generateNoise();
.hide {
height:0;
overflow:hidden;
}
body::before {
content:"";
position:fixed;
z-index:9999;
top:0;
left:0;
right:0;
bottom:0;
pointer-events:none;
opacity:0.8;
background:-moz-element(#canvases); /* use the canvas as background and repeat it */
}
<div class="hide"></div>
<h1>a title</h1>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed nec sagittis purus. Integer eget ante nec nisi malesuada vehicula. Vivamus urna velit, sodales in consequat ac, elementum id felis. Nulla vitae interdum eros. Aliquam non enim leo. Nunc blandit odio ut lectus egestas, nec luctus dui blandit. Suspendisse sed magna vel lorem mattis pretium sit amet in quam. Aenean varius elementum massa at gravida.
您可以创建一个离屏 canvas 您的磁贴大小,例如50x50,不要将其附加到 DOM 并在其上生成随机噪声。 canvas 的像素数据可以使用以下方法检索:
data=getImageData(0, 0, offScreenCanvas.width, offScreenCanvas.height);
然后可以使用
将这些数据进一步用于绘制到屏幕上canvasputImageData(data, x, y);
因此,如果您使用 for 循环将该数据放置在屏幕外 canvas 大小的 x 和倍数 - 50 - 您可以使用此图块填充整个 canvas。
一个问题是您会在这些瓷砖之间看到明显的接缝。作为补偿,我会添加另一个 for 循环并在某个随机点绘制该图块。
这是一个例子:
var canvas = document.createElement("canvas");
var offScreenCanvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
var offCtx = offScreenCanvas.getContext("2d");
canvas.className = "canvases";
canvas.width = 500;
canvas.height = 500;
offScreenCanvas.width = 50;
offScreenCanvas.height = 50;
document.body.appendChild(canvas);
canvas.setAttribute("style", "position: absolute;");
canvas.style.zIndex = 1;
function generateNoise() {
window.requestAnimationFrame(generateNoise);
ctx.clearRect(0, 0, canvas.width, canvas.height);
let x;
let y;
let rgb;
for (x = 0; x < offScreenCanvas.width; x++) {
for (y = 0; y < offScreenCanvas.height; y++) {
rgb = Math.floor(Math.random() * 255);
offCtx.fillStyle = `rgba(${rgb}, ${rgb}, ${rgb}, 0.2`;
offCtx.fillRect(x, y, 1, 1);
}
}
var data = offCtx.getImageData(0, 0, offScreenCanvas.width, offScreenCanvas.height);
for (x = 0; x < canvas.width; x += offScreenCanvas.width) {
for (y = 0; y < canvas.height; y += offScreenCanvas.height) {
ctx.rotate(parseInt(Math.random() * 4) * 90 * Math.PI / 180);
ctx.putImageData(data, x, y);
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
}
for (a = 0; a < 40; a++) {
ctx.putImageData(data, parseInt(Math.random() * canvas.width), parseInt(Math.random() * canvas.height));
}
}
generateNoise();
.canvases {
margin: 50px;
border: 1px solid black;
}
在 canvas 上生成噪点(通常在像素级工作)的最佳方法是使用 ImageData。
一个一个地绘制每个像素将非常慢(对 GPU 的大量指令),而在 TypedArray 上进行简单循环非常快。
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
let image; // using let because we may change it on resize
onresize = (evt) => {
const width = canvas.width = window.innerWidth;
const height = canvas.height =window.innerHeight;
image = new ImageData(width, height);
};
onresize();
const anim = (t) => {
const arr = new Uint32Array(image.data.buffer);
for( let i = 0; i < arr.length; i++ ) {
// random color with fixed 0.2 opacity
arr[i] = (Math.random() * 0xFFFFFF) + 0x33000000;
}
ctx.putImageData(image, 0, 0);
requestAnimationFrame(anim);
};
anim();
canvas { position: absolute; top: 0; left: 0 }
<canvas></canvas>
如果还是太慢,你也可以只对canvas尺寸的一部分产生噪点,再画多次:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
let image; // using let because we may change it on resize
onresize = (evt) => {
const width = canvas.width = window.innerWidth;
const height = canvas.height =window.innerHeight;
image = new ImageData(width / 2, height / 2);
};
onresize();
const anim = (t) => {
ctx.clearRect(0,0,canvas.width,canvas.height);
const arr = new Uint32Array(image.data.buffer);
for( let i = 0; i < arr.length; i++ ) {
// random color with fixed 0.2 opacity
arr[i] = (Math.random() * 0xFFFFFF) + 0x33000000;
}
const width = image.width;
const height = image.height;
ctx.putImageData(image, 0, 0);
ctx.drawImage(canvas, 0, 0, width, height, width, 0, width, height);
ctx.drawImage(canvas, 0, 0, width * 2, height, 0, height, width * 2, height);
requestAnimationFrame(anim);
};
anim();
canvas { position: absolute; top: 0; left: 0 }
<canvas></canvas>
骗人眼球
以 60fps 的速度呈现信息时,您可以很容易地欺骗人眼。
视觉 FX 永远不应影响性能,因为视觉 FX 是次要呈现,不如主要内容重要,主要内容应该在 CPU 周期中占据最大份额。
该代码段的 运行 渲染成本为每帧 1 fillRect
次调用,用 CPU 周期换取内存。我认为现在的问题是它太快了,也许你可能想将速率降低到 30fps(参见选项)。
var ctx = canvas.getContext("2d");
var noiseIdx = 0
const noiseSettings = [
[128, 10, 1, 1],
[128, 10, 2, 1],
[128, 10, 4, 1],
[128, 10, 8, 1],
[128, 10, 1, 2],
[128, 10, 8, 2],
[256, 20, 1, 1],
[256, 20, 1, 2],
[32, 30, 1, 1],
[64, 20, 1, 1],
];
var noise, rate, frame = 0;
setNoise();
function setNoise() {
const args = noiseSettings[noiseIdx++ % noiseSettings.length];
noise = createNoise(...args);
info.textContent = "Click to cycle. Res: " + args[0] + " by " + args[0] + "px " + args[1] +
" frames. Noise power: " + args[2] + " " + (60 / args[3]) + "FPS";
rate = args[3];
}
mainLoop();
function mainLoop() {
if (ctx.canvas.width !== innerWidth || ctx.canvas.height !== innerHeight) {
canvas.width = innerWidth;
canvas.height = innerHeight;
} else {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
frame++;
noise(ctx, (frame % rate) !== 0);
requestAnimationFrame(mainLoop);
}
canvas.addEventListener("click", setNoise);
function createNoise(size, frameCount, pow = 1) {
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const frames = [];
while (frameCount--) { frames.push(createNoisePattern(canvas)) }
var prevFrame = -1;
const mat = new DOMMatrix();
return (ctx, hold = false) => {
if (!hold || prevFrame === -1) {
var f = Math.random() * frames.length | 0;
f = f === prevFrame ? (f + 1) % frames.length : f;
mat.a = Math.random() < 0.5 ? -1 : 1;
mat.d = Math.random() < 0.5 ? -1 : 1;
mat.e = Math.random() * size | 0;
mat.f = Math.random() * size | 0;
prevFrame = f;
frames[f].setTransform(mat);
}
ctx.fillStyle = frames[prevFrame];
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
}
function createNoisePattern(canvas) {
const ctx = canvas.getContext("2d");
const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const d32 = new Uint32Array(imgData.data.buffer);
const alpha = (0.2 * 255) << 24;
var i = d32.length;
while (i--) {
const r = Math.random()**pow * 255 | 0;
d32[i] = (r << 16) + (r << 8) + r + alpha;
}
ctx.putImageData(imgData, 0, 0);
return ctx.createPattern(canvas, "repeat")
}
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
<div id="info"></div>
最佳浏览整页。
它是如何工作的
函数 createNoise(size, count, pow)
创建 count
随机噪声模式。图案的分辨率为size
。函数 returns 一个在使用 2D 上下文调用时用随机模式之一填充上下文的函数。
图案偏移是随机设置的,也是沿两个轴随机镜像的。还有一个快速测试,以确保相同的图案不会连续绘制两次(超过所需的帧速率)。
最终效果是随机噪声,人眼几乎无法区分真正的随机噪声。
返回的函数有第二个[可选]参数,如果为真,则重绘与前一帧相同的模式,允许噪声效果的帧速率独立于任何正在渲染的内容,在噪声之下或之上。
模拟真实噪音
createNoise(size, count, pow)
的第三个参数,[pow]
是可选的。
在胶片和较旧的模拟 CRT 上看到的噪声分布不均匀。高能(更亮)噪声比低能噪声少得多。
参数 pow
控制噪音的分布。值为 1 时分布均匀(每个能级出现的几率相同)。值 > 1 会降低分布高能量侧的几率。我个人会使用 8 的值,但这当然是个人品味问题。
代码段选项
为了帮助评估此方法,请单击 canvas 循环预设。预设修改随机模式的数量、每个模式的分辨率、噪声功率和噪声的帧速率(注意帧速率为 60FPS,噪声帧速率与基本速率无关)
与您的代码最匹配的设置是第一个(分辨率 128 x 128 10 帧,功率:1,帧速率 60)
请注意,当图案的分辨率等于或低于 64 时,您可以开始看到重复的图案,即使添加许多随机帧也不会降低此 FX
这个可以改进吗
是的,但您需要进入 WebGL 的世界。 WebGL 着色器可以比 2D fillRect
填充 canvas.
WebGL 不仅仅是 3D,它适用于任何有像素的东西。如果您进行了大量的视觉工作,那么花时间学习 WebGL 是非常值得的,因为它通常可以通过 2D API.
使不可能的事情成为可能。WebGL / Canvas 2D 混合解决方案
以下是使用 WebGL 的快速破解解决方案。 (因为我怀疑你会使用这种方法,所以我没有看到投入太多时间的意义)
它被构建为与 2D API 兼容。呈现的 WebGL 内容呈现为图像,然后您只需在 2D API 内容的顶部呈现。
var w, h, cw, ch, canvas, ctx, globalTime,webGL;
const NOISE_ALPHA = 0.5;
const NOISE_POWER = 1.2;
const shadersSource = {
VertexShader : {
type : "VERTEX_SHADER",
source : `
attribute vec2 position;
uniform vec2 resolution;
varying vec2 texPos;
void main() {
gl_Position = vec4((position / resolution) * 2.0 - 1.0, 0.0, 1.0);
texPos = gl_Position.xy;
}`
},
FragmentShader : {
type : "FRAGMENT_SHADER",
source : `
precision mediump float;
uniform float time;
varying vec2 texPos;
const float randC1 = 43758.5453;
const vec3 randC2 = vec3(12.9898, 78.233, 151.7182);
float randomF(float seed) {
return pow(fract(sin(dot(gl_FragCoord.xyz + seed, randC2)) * randC1 + seed), ${NOISE_POWER.toFixed(4)});
}
void main() {
gl_FragColor = vec4(vec3(randomF((texPos.x + 1.01) * (texPos.y + 1.01) * time)), ${NOISE_ALPHA});
}`
}
};
var globalTime = performance.now();
resizeCanvas();
startWebGL(ctx);
ctx.font = "64px arial black";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
function webGLRender(){
var gl = webGL.gl;
gl.uniform1f(webGL.locs.timer, globalTime / 100 + 100);
gl.drawArrays(gl.TRIANGLES, 0, 6);
ctx.drawImage(webGL, 0, 0, canvas.width, canvas.height);
}
function display(){
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "red";
ctx.fillText("Hello world "+ (globalTime / 1000).toFixed(1), cw, ch);
webGL && webGLRender();
}
function update(timer){ // Main update loop
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
requestAnimationFrame(update);
// creates vertex and fragment shaders
function createProgramFromScripts( gl, ids) {
var shaders = [];
for (var i = 0; i < ids.length; i += 1) {
var script = shadersSource[ids[i]];
if (script !== undefined) {
var shader = gl.createShader(gl[script.type]);
gl.shaderSource(shader, script.source);
gl.compileShader(shader);
shaders.push(shader);
}else{
throw new ReferenceError("*** Error: unknown script ID : " + ids[i]);
}
}
var program = gl.createProgram();
shaders.forEach((shader) => { gl.attachShader(program, shader); });
gl.linkProgram(program);
return program;
}
function createCanvas() {
var c,cs;
cs = (c = document.createElement("canvas")).style;
cs.position = "absolute";
cs.top = cs.left = "0px";
cs.zIndex = 1000;
document.body.appendChild(c);
return c;
}
function resizeCanvas() {
if (canvas === undefined) { canvas = createCanvas() }
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
setGlobals && setGlobals();
}
function setGlobals(){
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
// setup simple 2D webGL
function startWebGL(ctx) {
webGL = document.createElement("canvas");
webGL.width = ctx.canvas.width;
webGL.height = ctx.canvas.height;
webGL.gl = webGL.getContext("webgl");
var gl = webGL.gl;
var program = createProgramFromScripts(gl, ["VertexShader", "FragmentShader"]);
gl.useProgram(program);
var positionLocation = gl.getAttribLocation(program, "position");
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0.0, 0.0,1.0, 0.0,0.0, 1.0,0.0, 1.0,1.0, 0.0,1.0, 1.0]), gl.STATIC_DRAW);
var resolutionLocation = gl.getUniformLocation(program, "resolution");
webGL.locs = {
timer: gl.getUniformLocation(program, "time"),
};
gl.uniform2f(resolutionLocation, webGL.width, webGL.height);
var buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
setRectangle(gl, 0, 0, ctx.canvas.width, ctx.canvas.height);
}
function setRectangle(gl, x, y, width, height) {
var x1 = x;
var x2 = x + width;
var y1 = y;
var y2 = y + height;
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([x1, y1, x2, y1, x1, y2, x1, y2, x2, y1, x2, y2]), gl.STATIC_DRAW);
}