Повторите элемент холста HTML (поле), чтобы заполнить всю область просмотра

У меня есть анимированный холст для создания эффекта шума на фоне. Краткое объяснение моего кода следующее: используя цикл for, я случайным образом рисую квадраты (точки) размером 1px X 1px вокруг холста. Он создает статический шум (зерно). Позже я анимирую его с помощью requestAnimationFrame.

Моя проблема в том, что если я установлю исходный размер холста на всю область просмотра, браузер не сможет обрабатывать огромное пространство для рисования, и анимация станет очень вялой. Поэтому мне нужно сохранить исходный размер холста небольшим, например, 50 пикселей, и повторить его на всей области просмотра.

Я знаю, что могу легко использовать document.body.style.backgroundImage, который сам по себе автоматически повторяет элемент, но это не соответствует моей бизнес-логике. Мне нужно, чтобы этот элемент холста перекрывал любой другой элемент на странице и был прозрачным. Просто установка непрозрачности мне тоже не подходит.

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

Я бы создал небольшую гифку и использовал ее в качестве фона.

Temani Afif 17.12.2020 13:23
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
Улучшение производительности загрузки с помощью Google Tag Manager и атрибута Defer
В настоящее время производительность загрузки веб-сайта имеет решающее значение не только для удобства пользователей, но и для ранжирования в...
Безумие обратных вызовов в javascript [JS]
Безумие обратных вызовов в javascript [JS]
Здравствуйте! Юный падаван 🚀. Присоединяйся ко мне, чтобы разобраться в одной из самых запутанных концепций, когда вы начинаете изучать мир...
Система управления парковками с использованием HTML, CSS и JavaScript
Система управления парковками с использованием HTML, CSS и JavaScript
Веб-сайт по управлению парковками был создан с использованием HTML, CSS и JavaScript. Это простой сайт, ничего вычурного. Основная цель -...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
2
1
501
4
Перейти к ответу Данный вопрос помечен как решенный

Ответы 4

И идея из будущего с помощью element(). На самом деле это работает только в Firefox

Функция element() позволяет автору использовать элемент документа в качестве изображения. По мере изменения внешнего вида элемента, на который делается ссылка, меняется и изображение. Это можно использовать, например, для создания предварительного просмотра следующего/предыдущего слайда в слайд-шоу или для ссылки на элемент холста для причудливого сгенерированного градиента или даже анимированного фона. 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.

К сожалению, как вы сами упомянули, это не работает в моем Chrome. Кроме того, я считаю, что использование холста в качестве изображения не создаст эффекта прозрачности. Мне нужно, чтобы этот шумовой эффект был поверх текста и кнопок, а не за ними, как это будет при использовании холста в качестве фонового изображения. С фоновым изображением оно либо полностью позади, либо полностью сверху, закрывая все остальное.

DemiA 17.12.2020 13:51

Несмотря на то, что он все еще находится в черновиках, я бы не стал делать большие ставки на то, что он когда-либо получит широкую поддержку. IIRC даже FF планировал отказаться от него из-за проблем с производительностью.

Kaiido 17.12.2020 13:52

@Demiko мой пример на самом деле создает эффект прозрачности, который вы ищете, и он находится поверх всех элементов (обратите внимание на использование position:fixed, z-index и opacity)

Temani Afif 17.12.2020 13:55

@TemaniAfif извините, на самом деле я ввел вас в заблуждение. То, что вы делаете, правильно. На самом деле речь идет не об использовании холста в качестве фонового изображения или добавлении дочернего элемента к телу, а не о прозрачности. Я добиваюсь идеального наложения холста на другие элементы. Чего я не могу добиться, так это повторить добавленный дочерний элемент (холст). document.body.style.backgroundImage включает повтор как сокращение. И я предполагаю, что -moz-element() также делает это неявно, но appendChild требует явного повторения. Надеюсь, я никого не слишком запутал.

DemiA 17.12.2020 16:35

Вы можете создать закадровый холст размером с вашу плитку, например. 50x50, не добавляйте его в DOM и не генерируйте на нем случайный шум. Пиксельные данные этого холста можно получить, используя: data=getImageData(0, 0, offScreenCanvas.width, offScreenCanvas.height);

Затем эти данные можно использовать для рисования на холсте на экране с помощью

putImageData(data, x, y);

Поэтому, если вы используете цикл for, чтобы поместить эти данные в x и в кратном размере вашего закадрового холста — 50 — вы можете заполнить весь холст, используя эту плитку.

Одна проблема заключается в том, что вы увидите явный шов между этими плитками. Чтобы компенсировать это, я бы добавил еще один цикл 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;
}
Ответ принят как подходящий

Лучше всего для создания шума на холсте (и вообще для работы на уровне пикселей) использовать 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>

Если это все еще слишком медленно, вы также можете генерировать шум только для части размера холста и рисовать его несколько раз:

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>

Отлично. Он делает именно то, что я хочу. Спасибо. Но прежде чем использовать его, я попытался погрузиться в него глубже, чтобы лучше понять, и нет, я совсем застрял. Я разместил еще один вопрос, потому что он выходит за рамки этого вопроса, так что, может быть, вы поделитесь своим мнением? stackoverflow.com/questions/65361623/…

DemiA 18.12.2020 18:49

Обманывать глаз.

Вы можете довольно легко обмануть человеческий глаз, представляя информацию со скоростью 60 кадров в секунду.

Визуальные эффекты никогда не должны снижать производительность, поскольку визуальные эффекты являются вторичным представлением, менее важным, чем основной контент, который должен получать львиную долю циклов процессора.

Текущая стоимость рендеринга фрагмента составляет 1 вызов fillRect за кадр, при этом циклы ЦП обмениваются на память. Я думаю, что теперь проблема в том, что это слишком быстро, и, возможно, вы захотите уменьшить скорость до 30 кадров в секунду (см. варианты).

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. Функция возвращает функцию, которая при вызове с 2D-контекстом заполняет контекст одним из случайных шаблонов.

Смещение паттернов задается случайным образом, а также случайным образом зеркально отражается по обеим осям. Существует также быстрый тест, гарантирующий, что один и тот же шаблон никогда не будет отображаться дважды (с превышением желаемой частоты кадров) подряд.

Конечным эффектом является случайный шум, который человеческому глазу практически невозможно отличить от истинного случайного шума.

Возвращаемая функция имеет второй [необязательный] аргумент, который, если он равен true, перерисовывает тот же шаблон, что и предыдущий кадр, что позволяет частоте кадров эффекта шума быть независимой от любого визуализируемого контента, под или над шумом.

Имитация реального шума

Третий аргумент createNoise(size, count, pow), [pow] является необязательным.

Шум, наблюдаемый на пленочных и старых аналоговых ЭЛТ, распределяется неравномерно. Высокоэнергетический (яркий) шум встречается гораздо реже, чем низкоэнергетический шум.

Аргумент pow управляет распределением шума. Значение 1 имеет равномерное распределение (каждый энергетический уровень имеет одинаковую вероятность появления). Значение > 1 снижает шансы на высокую энергию распределения. Лично я бы использовал значение 8, но это, конечно, вопрос личного вкуса.

Параметры фрагмента

Чтобы помочь оценить этот метод, щелкните холст, чтобы циклически переключаться между пресетами. Предустановки изменяют количество случайных шаблонов, разрешение каждого шаблона, мощность шума и частоту кадров шума (примечание: частота кадров составляет 60 кадров в секунду, частота кадров шума не зависит от базовой частоты)

Настройка, которая наиболее точно соответствует вашему коду, — первая (разрешение 128 на 128 10 кадров, мощность: 1, частота кадров 60).

Обратите внимание, что когда разрешение шаблона равно 64 или ниже, вы можете начать видеть повторяющийся шаблон, даже добавление множества случайных кадров не уменьшает этот эффект.

Можно ли это улучшить

Да, но вам нужно будет войти в мир WebGL. Шейдер WebGL может отобразить этот эффект быстрее, чем 2D fillRect заполнит холст.

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

Спасибо за интересный обзор.

DemiA 18.12.2020 18:47

Другие вопросы по теме