简体   繁体   中英

scrambling images on canvas producing whitespace

I'm starting with a canvas element. I'm making the left half red, and the right side blue. Every half second, setInterval calls a function, scramble, which splits both RHS and LHS into pieces, and shuffles them.

Here is a fiddle: https://jsfiddle.net/aeq1g3yb/

The code is below. The reason I'm using window.onload is because this thing is supposed to scramble pictures and I want the pictures to load first. I'm using colors here because of the cross-origin business that I don't know enough about, so this is my accommodation.

var n = 1;
var v = 1;

function scramble() {

  //get the canvas and change its width
  var c = document.getElementById("myCanvas");
  c.width = 600;
  var ctx = c.getContext("2d");

  //drawing 2 different colors side by side
  ctx.fillStyle = "red";
  ctx.fillRect(0, 0, c.width/2, c.height);
  ctx.fillStyle = "blue";
  ctx.fillRect(c.width/2, 0, c.width/2, c.height);

  //how big will each shuffled chunk be
  var stepsA = (c.width/2) / n;
  var stepsB = (c.width/2) / n;
  var step = stepsA + stepsB;

  var imgDataA = [];
  var imgDataB = [];

  for (var i = 0; i < n; i++) {
    var imgDataElementA = ctx.getImageData(stepsA*i, 0, stepsA, c.height);
    var imgDataElementB = ctx.getImageData(c.width/2+stepsB*i, 0, stepsB, c.height);
    imgDataA.push(imgDataElementA);
    imgDataB.push(imgDataElementB);
  }

  //clearing out the canvas before laying on the new stuff
  ctx.fillStyle = "white";
  ctx.fillRect(0, 0, c.width, c.height);

  //put the images back
  for (var i = 0; i < n; i++) {
    ctx.putImageData(imgDataA[i], step*i, 0);
    ctx.putImageData(imgDataB[i], step*i+stepsA, 0);
  }

  //gonna count the steps
  var count = document.getElementById("count");
  count.innerHTML = n;

  n += v;

  if (n >= 100 || n <= 1) {
    v *= -1;
  }

}; //closing function scramble

window.onload = function() { //gotta do this bc code executes before image loads
  scramble();
};

window.setInterval(scramble, 500);

More or less, this thing works the way I want it to. But there is one problem: Sometimes there are vertical white lines.

My question is:

Why are there white lines? If you view the fiddle, you will see the degree to which this impairs the effect of the shuffle.

You can`t divide a Pixel

The problem can be solve but will introduce some other artifacts as you can not divide integer pixels into fractions.

Quick solution

The following solution for your existing code rounds down for the start of a section and up for the width.

  for (var i = 0; i < n; i++) {
    var imgDataElementA = ctx.getImageData(
        Math.floor(stepsA * i), 0, 
        Math.ceil(stepsA + stepsA * i)  - Math.floor(stepsA * i), c.height
    );
    var imgDataElementB = ctx.getImageData(
        Math.floor(c.width / 2 + stepsB * i), 0, 
        Math.ceil(c.width / 2 + stepsB * i + stepsB) - Math.floor(c.width / 2 + stepsB * i), c.height);
    imgDataA.push(imgDataElementA);
    imgDataB.push(imgDataElementB);
  }

Quicker options

But doing this via the pixel image data is about the slowest possible way you could find to do it. You can just use the 2D context.imageDraw function to do the movement for you. Or if you want the best in terms of performance a WebGL solution would be the best with the fragment shader doing the scrambling for you as a parallel solution.

There is no perfect solution

But in the end you can not cut a pixel in half, there are a wide range of ways to attempt to solve this but each method has its own artifacts. Ideally you should only slice an image if the rule image.width % slices === 0 in all other cases you will have one or more slices that will not fit on an integer number of pixels.

Example of 4 rounding methods.

The demo shows 4 different methods and with 2 colors. Mouse over to see a closer view. Each method is separated horizontally with a white line. Hold the mouse button to increase the slice counter.

The top is your original. The next three are 3 different ways of dealing with the fractional pixel width.

 const mouse = {x : 0, y : 0, button : false} function mouseEvents(e){ const m = mouse; if(m.element){ m.bounds = m.element.getBoundingClientRect(); mx = e.pageX - m.bounds.left - scrollX; my = e.pageY - m.bounds.top - scrollY; m.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : m.button; } } ["down","up","move"].forEach(name => document.addEventListener("mouse"+name,mouseEvents)); const counterElement = document.getElementById("count"); // get constants for the demo const c = document.getElementById("myCanvas"); mouse.element = c; // The image with the blue and red const img = document.createElement("canvas"); // the zoom image overlay const zoom = document.createElement("canvas"); // the scrambled image const scram = document.createElement("canvas"); // Set sizes and get context const w = scram.width = zoom.width = img.width = c.width = 500; const h = scram.height = zoom.height = img.height = c.height; const dCtx = c.getContext("2d"); // display context const iCtx = img.getContext("2d"); // source image context const zCtx = zoom.getContext("2d"); // zoom context const sCtx = scram.getContext("2d"); // scrambled context // some constants const zoomAmount = 4; const zoomRadius = 60; const framesToStep = 10; function createTestPattern(ctx){ ctx.fillStyle = "red"; ctx.fillRect(0, 0, c.width/2, c.height/2); ctx.fillStyle = "blue"; ctx.fillRect(c.width/2, 0, c.width/2, c.height/2); ctx.fillStyle = "black"; ctx.fillRect(0, c.height/2, c.width/2, c.height/2); ctx.fillStyle = "#CCC"; ctx.fillRect(c.width/2, c.height/2, c.width/2, c.height/2); } createTestPattern(iCtx); sCtx.drawImage(iCtx.canvas, 0, 0); // Shows a zoom area so that blind men like me can see what is going on. function showMouseZoom(src,dest,zoom = zoomAmount,radius = zoomRadius){ dest.clearRect(0,0,w,h); dest.imageSmoothingEnabled = false; if(mouse.x >= 0 && mouse.y >= 0 && mouse.x < w && mouse.y < h){ dest.setTransform(zoom,0,0,zoom,mouse.x,mouse.y) dest.drawImage(src.canvas, -mouse.x, -mouse.y); dest.setTransform(1,0,0,1,0,0); dest.globalCompositeOperation = "destination-in"; dest.beginPath(); dest.arc(mouse.x,mouse.y,radius,0,Math.PI * 2); dest.fill(); dest.globalCompositeOperation = "source-over"; dest.lineWidth = 4; dest.strokeStyle = "black"; dest.stroke(); } } function scramble(src,dest,y,height) { const w = src.canvas.width; const h = src.canvas.height; const steps = (w/2) / slices; dest.fillStyle = "white"; dest.fillRect(0, y, w, height); for (var i = 0; i < slices * 2; i++) { dest.drawImage(src.canvas, ((i / 2) | 0) * steps + (i % 2) * (w / 2)- 0.5, y, steps + 1, height, i * steps - 0.5, y, steps+ 1, height ); } } function scrambleFloor(src,dest,y,height) { const w = src.canvas.width; const h = src.canvas.height; const steps = (w/2) / slices; dest.fillStyle = "white"; dest.fillRect(0, y, w, height); for (var i = 0; i < slices * 2; i++) { dest.drawImage(src.canvas, (((i / 2) | 0) * steps + (i % 2) * (w / 2)- 0.5) | 0, y, steps + 1, height, (i * steps - 0.5) | 0, y, steps + 1, height ); } } function scrambleNoOverlap(src,dest,y,height) { const w = src.canvas.width; const h = src.canvas.height; const steps = (w / 2) / slices; dest.fillStyle = "white"; dest.fillRect(0, y, w, height); for (var i = 0; i < slices * 2; i++) { dest.drawImage(src.canvas, ((i / 2) | 0) * steps + (i % 2) * (w / 2), y, steps, height, i * steps - 0.5, y, steps, height ); } } function scrambleOriginal(src,dest,y,height) { const w = src.canvas.width; const h = src.canvas.height; //how big will each shuffled chunk be var stepsA = (w/2) / slices; var stepsB = (w/2) / slices; var step = stepsA + stepsB; var imgDataA = []; var imgDataB = []; for (var i = 0; i < slices; i++) { var imgDataElementA = src.getImageData(stepsA*i, y, stepsA, height); var imgDataElementB = src.getImageData(w/2+stepsB*i, y, stepsB, height); imgDataA.push(imgDataElementA); imgDataB.push(imgDataElementB); } //clearing out the canvas before laying on the new stuff dest.fillStyle = "white"; dest.fillRect(0, y, w, height); //put the images back for (var i = 0; i < slices; i++) { dest.putImageData(imgDataA[i], step*i, y); dest.putImageData(imgDataB[i], step*i+stepsA, y); } }; //closing function scramble const scrambleMethods = [scrambleOriginal,scramble,scrambleFloor,scrambleNoOverlap]; var frameCount = 0; var sliceStep = 1; var slices = 1; function mainLoop(){ if(mouse.button){ if(frameCount++ % framesToStep === framesToStep-1){ // every 30 Frames slices += sliceStep; if(slices > 150 || slices < 2){ sliceStep = -sliceStep } counterElement.textContent = slices; // Prevent reflow by using textContent sCtx.clearRect(0,0,w,h); sCtx.imageSmoothingEnabled = true; const len = scrambleMethods.length; for(var i = 0; i < len; i ++){ scrambleMethods[i](iCtx,sCtx,(128/len) * i, 128/len-2); scrambleMethods[i](iCtx,sCtx,(128/len) * i + 128, 128/len-2); } } } dCtx.fillStyle = "white"; dCtx.fillRect(0,0,w,h); dCtx.drawImage(sCtx.canvas,0,0); showMouseZoom(dCtx,zCtx); dCtx.drawImage(zCtx.canvas,0,0); requestAnimationFrame(mainLoop); } //scramble(iCtx,sCtx); requestAnimationFrame(mainLoop); 
 canvas { border: 1px solid black; } #count { position : absolute; top : 0px; left : 10px; font-family: monospace; font-size: 20px; } 
 <canvas id="myCanvas" height = "256" title="Hold mouse button to chance slice count"></canvas> <p id="count"></p> 

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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