简体   繁体   English

重复 HTML canvas 元素(框)以填充整个视口

[英]Repeat HTML canvas element (box) to fill whole viewport

I have an animated canvas to create a noise effect on the background.我有一个动画 canvas 来在背景上创建噪音效果。 Short explanation of my code is following: using for loop I am drawing 1px X 1px squares (dots) around canvas at random.我的代码的简短解释如下:使用for循环我在 canvas 周围随机绘制 1px X 1px 正方形(点)。 It creates a static noise (grain).它会产生 static 噪声(颗粒)。 Later, I animate it with requestAnimationFrame .稍后,我使用requestAnimationFrame对其进行动画处理。

My problem is that if I set the original size of the canvas to whole viewport, the browser can not handle huge drawing space and animation becomes extremely sluggish.我的问题是,如果我将 canvas 的原始大小设置为整个视口,浏览器将无法处理巨大的绘图空间,并且 animation 变得非常缓慢。 So I need to keep the original size of canvas small, like 50px and repeat it on whole viewport.所以我需要保持 canvas 的原始尺寸很小,比如 50px 并在整个视口上重复它。

I know I can easily use document.body.style.backgroundImage which on itself repeats the element automatically, but it does not fit my business logic.我知道我可以轻松使用document.body.style.backgroundImage ,它本身会自动重复元素,但它不符合我的业务逻辑。 I need this canvas element to layover any other element on the page and be transparent.我需要这个 canvas 元素来覆盖页面上的任何其他元素并保持透明。 Just setting opacity does not fit me as well.仅仅设置不透明度也不适合我。

 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; }

The best to generate noise on a canvas (and to work at a pixel level in general) is to use an ImageData .在 canvas 上产生噪声的最佳方法(并且通常在像素级别上工作)是使用ImageData
Painting every pixel one by one will be extremely slow (a lot of instructions to the GPU), while a simple loop over a TypedArray is quite fast.一个一个地绘制每个像素会非常慢(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>

If it's still too slow, you could also generate the noise only for a portion of the canvas size and draw it multiple times:如果它仍然太慢,您也可以只为 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>

Fooling the eye.糊弄眼睛。

You can fool the human eye quite easily when presenting information at 60fps.当以 60fps 呈现信息时,您可以很容易地欺骗人眼。

Visual FXs should never compromise performance as the visual FX is a secondary presentation less important than the primary content which should get the lions share of the CPU cycles.视觉效果永远不应该影响性能,因为视觉效果是次要的表现形式,不如主要内容重要,它应该占据 CPU 周期的大部分份额。

The snippet has a running render cost of 1 fillRect call per frame, exchanging CPU cycles for memory.该片段的运行渲染成本为每帧 1 个fillRect调用,将 CPU 周期换成 memory。 I think that now the problem is that it is too fast and maybe you may want to reduce the rate down to 30fps (see options).我认为现在的问题是它太快了,也许您可能希望将速率降低到 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>

Best viewed full page.最佳浏览整页。

How does it work它是如何工作的

The function createNoise(size, count, pow) creates count patterns of random noise. function createNoise(size, count, pow)创建随机噪声的count模式。 The resolution of the pattern is size .图案的分辨率是size The function returns a function that when called with a 2D context fills the context with one of the random patterns. function 返回一个 function,当使用 2D 上下文调用时,它会使用随机模式之一填充上下文。

The patterns offset is set randomly, and is also randomly mirrored along both axis.图案偏移是随机设置的,并且也沿两个轴随机镜像。 There is also a quick test to ensure that the same pattern is never drawn twice (over the desired frame rate) in a row.还有一个快速测试,以确保不会连续两次(超过所需的帧速率)绘制相同的图案。

The end effect is random noise that is near impossible for the human eye to distinguish from true random noise.最终效果是随机噪声,人眼几乎不可能将其与真正的随机噪声区分开来。

The returned function has a second [optional] argument that if true redraws the same pattern as the previous frame allowing the frame rate of the noise effect to be independent of any content being rendered, under or over the noise.返回的 function 有第二个 [可选] 参数,如果为 true,则重绘与前一帧相同的模式,允许噪声效果的帧速率独立于任何正在渲染的内容,无论是在噪声之下还是在噪声之上。

Simulating real noise模拟真实噪音

The 3rd argument of createNoise(size, count, pow) , [pow] is optional. createNoise(size, count, pow)的第三个参数[pow]是可选的。

Noise as seen on film and older analog CRTs is not evenly distributed.在胶片和老式模拟 CRT 上看到的噪声分布不均。 High energy (brighter) noise is much rarer then low energy noise.高能量(更亮)噪声比低能量噪声少得多。

The argument pow controls the distribution of noise.参数pow控制噪声的分布。 A value of 1 has an even distribution (each energy level has the same chance of occurring).值 1 具有均匀分布(每个能级具有相同的发生机会)。 Value > 1 decrease the odds of at the high energy side of the distribution.值 > 1 会降低分布的高能量侧的几率。 Personally I would use a value of 8 but that is of course a matter of personal taste.我个人会使用 8 的值,但这当然是个人喜好问题。

Snippet Options片段选项

To help evaluate this method click the canvas to cycle the presets.要帮助评估此方法,请单击 canvas 循环预设。 The presets modify the number of random patterns, the resolution of each pattern, the noise power, and the frame rate of the noise (Note frame rate is 60FPS the noise frame rate is independent of the base rate)预设修改随机模式的数量、每个模式的分辨率、噪声功率和噪声的帧率(注意帧率为 60FPS,噪声帧率与基本速率无关)

The setting that most closely matches your code is the first (res 128 by 128 10 frames, power: 1, frame rate 60)最接近您的代码的设置是第一个(分辨率 128 x 128 10 帧,功率:1,帧速率 60)

Note that when the resolution of the pattern is at or below 64 you can start to see the repeated pattern, even adding many random frames does not reduce this FX请注意,当图案的分辨率等于或低于 64 时,您可以开始看到重复的图案,即使添加许多随机帧也不会降低此 FX

Can this be improved这可以改进吗

Yes but you will need to step into the world of WebGL.是的,但您需要进入 WebGL 的世界。 A WebGL shader can render this effect quicker than the 2D fillRect can fill the canvas. WebGL 着色器可以比 2D fillRect更快地渲染此效果,可以填充 canvas。

WebGL is not just 3D, it is for anything with pixels. WebGL 不仅仅是 3D,它适用于任何有像素的东西。 It is well worth investing time into learning WebGL if you do a lot of visual work as it can generally make possible what is impossible via the 2D API.如果你做了很多视觉工作,花时间学习 WebGL 是非常值得的,因为它通常可以通过 2D API 实现不可能的事情。

WebGL / Canvas 2D hybrid solution WebGL / Canvas 2D混合解决方案

The following is a quick hack solution using WebGL.以下是使用 WebGL 的快速破解解决方案。 (As I doubt you will use this method I did not see the point of putting in too much time) (因为我怀疑你会使用这种方法,所以我没有看到投入太多时间的意义)

It has been built to be compatible with the 2D API.它已构建为与 2D API 兼容。 The rendered WebGL content is presented as an image that you then just render over the top of the 2D API content.呈现的 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); }

And idea from the future using element() .以及未来使用element()的想法。 Actually it works only on Firefox实际上它仅适用于 Firefox

The element() function allows an author to use an element in the document as an image. element() function 允许作者将文档中的元素用作图像。 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 .例如,这可用于在幻灯片中创建下一张/上一张幻灯片的实时预览,或引用 canvas 元素以生成精美的渐变甚至动画背景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.

You can create an offscreen canvas the size of your tile eg 50x50, don't append it to the DOM and generate the random noise on it.您可以创建一个屏幕外 canvas 大小,例如 50x50,不要将 append 放到 DOM 上并在其上生成随机噪声。 The pixel data of this canvas can be retrieved using: data=getImageData(0, 0, offScreenCanvas.width, offScreenCanvas.height);此 canvas 的像素数据可以使用以下命令检索: data=getImageData(0, 0, offScreenCanvas.width, offScreenCanvas.height);

This data then can be further used to be drawn onto your on-screen canvas using然后可以使用此数据进一步将其绘制到您的屏幕 canvas 上

putImageData(data, x, y);

So if you use a for-loop to put that data at x & in multiples of your offscreen canvas size - 50 - you can fill the whole canvas using this tile.因此,如果您使用 for 循环将该数据放在 x 和屏幕外 canvas 大小的倍数 - 50 - 您可以使用此图块填充整个 canvas。

One problem is that you will see an obvious seam in-between those tiles.一个问题是您会在这些瓷砖之间看到明显的接缝。 To compensate I would add another for loop and draw that tile at some random spot.为了补偿,我会添加另一个 for 循环并在某个随机位置绘制该图块。

Here's an example:这是一个例子:

 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; }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM