Jump to content

Saving / restoring canvas?


Mad_Griffith

Recommended Posts

Hi guys, I have a question concerning saving and restoring the canvas after scaling. This is the scenario:

 

Photo is loaded

Context is scaled

Animation Loop

Photo is drawn

Overlay Image is drawn

RequestAnimationFrame

 

Where should I put context.save() and context.restore() to prevent the scaling from affecting the Overlay Image? I tried a few solutions but to no avail.

 

Thanks!

Link to comment
Share on other sites

You should save before doing a transform, then call restore once you want to return to the original state.

Given the code structure you displayed, this should work:

Photo is loaded
Animation Loop
Save context
Context is scaled
Photo is drawn
Restore context
Overlay Image is drawn
RequestAnimationFrame

 

I don't know what other operations you're doing with the scaled canvas, but if it's just drawing the photo and then the overlay this will work.

Link to comment
Share on other sites

You have to scale every time you plan to draw the object that needs to be scaled. If you only draw the object once then you only need to scale once. If you draw it on every frame then you need to scale it on every frame.

Link to comment
Share on other sites

here it is. The processProfileImg() is where I am having the problem at the moment:



window.onload = drawCanvas;


var canvas = document.getElementById('composition-image'),
context = canvas.getContext('2d'),
distanceFromRightSide = 990,
distanceFromBottomSide = 520,
startX = 0,
startY = 0,
drag = false,
sliderPrevPos = parseInt($('#zoom input[type="range"]').attr('value')),
zoomSpeed = 3.5;


function drawCanvas() {


canvasImg = new Image();


canvas.width = 1200;
canvas.height = 900;


canvasImg.onload = function() {


context.drawImage(canvasImg, 0, 0);


}


canvasImg.src = './img/bg.jpg';


mouseActions();


fileIsUploaded();


}


function fileIsUploaded() {


$('input[type="file"]').change(function() {


files = this.files[0];


openUploadedFile(files);


});


}


function openUploadedFile(file) {


try {


var openedFile = new FileReader();


openedFile.readAsDataURL(file);
openedFile.onload = processProfileImg;


} catch (error) {


// Fail silently if user cancels the 'Browse' window


}


}


function processProfileImg(event) {


profileImg = new Image();


profileImg.src = event.target.result;


profileImg.onload = function() {


setAspectRatio();


profileImgX = canvas.width - distanceFromRightSide - ((profileImg.width - 183) / 2),
profileImgY = canvas.height - distanceFromBottomSide - ((profileImg.height - 242) / 2);


EXIF.getData(profileImg, function() {


orientateProfileImg();


});


(function animationLoop(event) {
      save.context();
renderProfileImg(event);
requestAnimationFrame(animationLoop, profileImg);
      drawOverlay();


})()


hideDefaultProfileImg();


}


enableZoomSlider();
drawProfileImgBoundaries();
resetZoom();


}


function drawOverlay() {


overlayImg = new Image();


overlayImg.src = './img/profile_default_red_frame.png';


context.drawImage(overlayImg, canvas.width - distanceFromRightSide, canvas.height - distanceFromBottomSide, 183, 242);


}


function drawProfileImgBoundaries() {


context.rect(canvas.width - distanceFromRightSide, canvas.height - distanceFromBottomSide, 183, 242);


context.clip();


}


function renderProfileImg(event) {


context.clearRect(210, 380, 183, 242);


var leftBoundary = 210,
rightBoundary = 393,
topBoundary = 380,
bottomBoundary = 622,
left = profileImgX,
right = profileImgX + profileImg.width,
top = profileImgY,
bottom = profileImgY + profileImg.height;


dragProfileImg(left, right, top, bottom, leftBoundary, rightBoundary, topBoundary, bottomBoundary);


zoomProfileImg(leftBoundary, rightBoundary, topBoundary, bottomBoundary);


context.drawImage(profileImg, profileImgX, profileImgY, profileImg.width, profileImg.height);


}


/**
 * Clicks
 **/


function mouseActions() {


mouseX = 0,
mouseY = 0,
mousePressed = false;
dragging = false;


canvas.onmousemove = function(e) {
mouseX = e.offsetX;
mouseY = e.offsetY;
}


$(document).mousedown(function() {
mousePressed = true;
}).mouseup(function() {
mousePressed = false;
dragging = false;
});


$('#profile-blue').click(
function() {
$('#panel1').css({
'display': 'block'
});
}
);


$('#continue-button').click(
function() {
$('#panel1').css({
'display': 'none'
});
$('#panel2').css({
'display': 'block'
});
}
);


$('#back-button').click(
function() {
$('#panel2').css({
'display': 'none'
});
}
);


$('#facebook').click(function() {


canvasToData();


formData.append('message', $('input[type="file"]')[0].files.length !== 0 ? 'I’ve joined team remain for the EU Referendum. Which side are you on? http://www.euwhichsideareyouon.com' : 'It’s time to decide which side are you on for the EU Referendum. Choose you team here: http://www.euwhichsideareyouon.com');


FB.login(function(response) {


var auth = response.authResponse;


$.ajax({
url: 'https://graph.facebook.com/' + auth.userID + '/photos?access_token=' + auth.accessToken,
type: 'POST',
data: formData,
processData: false,
contentType: false,
cache: false,
beforeSend: function(formData) {
$('#facebook').addClass('facebook-beforesend');
},
complete: function() {
$('#facebook').removeClass('facebook-beforesend');
$('#facebook').addClass('facebook-success');
setTimeout(
function() {
$('#facebook').removeClass('facebook-success');
}, 3000);
}
});


}, {


scope: 'publish_actions'


});
});


$('#twitter').click(function() {


canvasToData();


formData.append('message', $('input[type="file"]')[0].files.length !== 0 ? 'photo' : 'nophoto');


$.ajax({
url: './twitter/tweet-with-media.php',
type: 'POST',
data: formData,
processData: false,
contentType: false,
cache: false,
beforeSend: function() {
$('#twitter').addClass('twitter-beforesend');
}
}).then(function(data) {
window.open(data); // authorisation URL
}).done(function() {
$('#twitter').removeClass('twitter-beforesend');
$('#twitter').addClass('twitter-success');
setTimeout(
function() {
$('#twitter').removeClass('twitter-success');
}, 3000);
});
});


function facebook() {


window.fbAsyncInit = function() {


FB.init({


appId: '1851476801746058',
xfbml: true,
version: 'v2.5'


});


};


(function(d, s, id) {


var js, fjs = d.getElementsByTagName(s)[0];


if (d.getElementById(id)) {


return;


}


js = d.createElement(s);


js.id = id;


js.src = "//connect.facebook.net/en_US/sdk.js";


fjs.parentNode.insertBefore(js, fjs);


}(document, 'script', 'facebook-jssdk'));


}


facebook();


}


/**
 * Helpers
 **/


function resetZoom() {


sliderPrevPos = $('#zoom input[type="range"]')[0].defaultValue;


$('#zoom input[type="range"]')[0].value = $('#zoom input[type="range"]')[0].defaultValue;


}


function zoomProfileImg(leftBoundary, rightBoundary, topBoundary, bottomBoundary) {


$('#zoom input[type="range"]').change(function() {


var sliderCurrPos = parseInt(this.value),
zoomDelta = parseInt(Math.abs(sliderCurrPos - sliderPrevPos) * zoomSpeed);


if (sliderCurrPos > sliderPrevPos) {


profileImg.width += zoomDelta;
profileImg.height += zoomDelta;


} else if (sliderCurrPos < sliderPrevPos) {


profileImg.width -= zoomDelta;
profileImg.height -= zoomDelta;


}


sliderPrevPos = sliderCurrPos;


});


}


function orientateProfileImg() {


  orientation = EXIF.getTag(this, "Orientation");


switch (orientation) {
case 0:
// 90° rotate left
context.translate(canvas.width / 2 + 183, canvas.height / 2 - 242);
context.rotate(90 * (Math.PI / 180));
break;
case 2:
// horizontal flip
context.scale(-1, 1);
break;
case 3:
// 180° rotate left
context.rotate(Math.PI);
break;
case 4, -90:
// vertical flip
context.translate(0, 1000);
context.scale(1, -1);
break;
case 5:
// vertical flip + 90 rotate right
      context.translate(0, 1000);
context.rotate(-90 * (Math.PI / 180));
context.scale(1, -1);
break;
case 6:
// 90° rotate right
context.rotate(-90 * (Math.PI / 180));
break;
case 7:
// horizontal flip + 90 rotate right
context.rotate(-90 * (Math.PI / 180));
context.scale(-1, 1);
break;
case 8, 90:
// 90° rotate left
context.translate(canvas.width / 2 + 183, canvas.height / 2 - 242);
context.rotate(90 * (Math.PI / 180));
break;
default:
      // vertical flip
      context.translate(0, 1000);
      context.scale(1, -1);
break;
}


}


function setAspectRatio() {


profileImg.width = profileImg.naturalWidth;
profileImg.height = profileImg.naturalHeight;


if (profileImg.width > profileImg.height) {


profileImg.width = 242 * profileImg.width / profileImg.height;
profileImg.height = 242;


} else if (profileImg.width < profileImg.height) {


profileImg.height = 183 * profileImg.height / profileImg.width;
profileImg.width = 183;


} else if (profileImg.height == profileImg.width) {


profileImg.width = 242;
profileImg.height = 242;


}


}


function dragProfileImg(left, right, top, bottom, leftBoundary, rightBoundary, topBoundary, bottomBoundary) {


if (mouseX < rightBoundary && mouseX > leftBoundary && mouseY < bottomBoundary && mouseY > topBoundary) {


canvas.style.cursor = 'move';


} else {


canvas.style.cursor = 'default';


drag = false;


}


if (mousePressed) {


if (!drag) {


startX = mouseX - profileImgX;
startY = mouseY - profileImgY;


}


if (mouseX < right && mouseX > left && mouseY < bottom && mouseY > top) {


if (!dragging) {


dragging = true;
drag = true;
}


}


} else {


drag = false;
}


if (drag) {


profileImgX = mouseX - startX;
profileImgY = mouseY - startY;


}


if (profileImgX > leftBoundary) {


profileImgX = leftBoundary;


}


if ((profileImgX + profileImg.width) < rightBoundary) {


profileImgX = rightBoundary - profileImg.width;


}


if (profileImgY > topBoundary) {


profileImgY = topBoundary;


}


if ((profileImgY + profileImg.height) < bottomBoundary) {


profileImgY = bottomBoundary - profileImg.height;


}


}


function hideDefaultProfileImg() {


$('#profile-red').fadeOut('fast');


}


function enableZoomSlider() {


$('#zoom input[type="range"]').prop("disabled", false);


}


function canvasToData() {


canvasDataUrl = canvas.toDataURL("image/jpeg"),
canvasBlob = dataURItoBlob(canvasDataUrl);


formData = new FormData();
formData.append("canvasToData", canvasBlob);


}


function dataURItoBlob(dataURI) {


var byteString = atob(dataURI.split(',')[1]),
mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0],
ab = new ArrayBuffer(byteString.length),
ia = new Uint8Array(ab);


for (var i = 0; i < byteString.length; i++) {


ia[i] = byteString.charCodeAt(i);


}


var bb = new Blob([ab], {
"type": mimeString
});


return bb;


}
Link to comment
Share on other sites

You've forgotten to declare all your variables with the var keyword, which means that most of them are in the global scope.

 

I think you meant to write context.save() here:

save.context();

You're making an unnecessary function call here, just set it to EXIF.getData(profileImg, orientateProfileImg)

EXIF.getData(profileImg, function() {
  orientateProfileImg();
});

For efficiency, you should create this outside the loop:

  overlayImg = new Image();
  overlayImg.src = './img/profile_default_red_frame.png';

Now, as I said earlier, to solve your problem you need to do the transformation right before drawing the object you want transformed.

/* --- Some code here --- */

function processProfileImg(event) {
  var overlayImg = new Image();
  overlayImg.src = './img/profile_default_red_frame.png';

  var profileImg = new Image();
  profileImg.src = event.target.result;
  profileImg.onload = function() {

    /* --- Some code here --- */

    // Get image orientation
    var orientation;
    EXIF.getData(profileImg, function() {
      orientation = EXIF.getTag(this, "Orientation")
    });

    function animationLoop() {
      // Save
      context.save();
      // Transform
      transformContext(orientation);
      // Draw
      renderProfileImg();
      // Restore
      context.restore();
      
      // Draw overlay
      context.drawImage(overlayImg, canvas.width - distanceFromRightSide, canvas.height - distanceFromBottomSide, 183, 242);

      // Next iteration
      requestAnimationFrame(animationLoop, profileImg);
    }
    animationLoop();
    
    hideDefaultProfileImg();
  }

  /* --- Some code here --- */
}

function transformContext(orientation) {
  switch (orientation) {
  case 0:
    // 90° rotate left
    context.translate(canvas.width / 2 + 183, canvas.height / 2 - 242);
    context.rotate(90 * (Math.PI / 180));
    break;
  case 2:
    // horizontal flip
    context.scale(-1, 1);
    break;
  case 3:
    // 180° rotate left
    context.rotate(Math.PI);
    break;
  case 4, -90:
    // vertical flip
    context.translate(0, 1000);
    context.scale(1, -1);
    break;
  case 5:
    // vertical flip + 90 rotate right
    context.translate(0, 1000);
    context.rotate(-90 * (Math.PI / 180));
    context.scale(1, -1);
    break;
  case 6:
    // 90° rotate right
    context.rotate(-90 * (Math.PI / 180));
    break;
  case 7:
    // horizontal flip + 90 rotate right
    context.rotate(-90 * (Math.PI / 180));
    context.scale(-1, 1);
    break;
  case 8, 90:
    // 90° rotate left
    context.translate(canvas.width / 2 + 183, canvas.height / 2 - 242);
    context.rotate(90 * (Math.PI / 180));
    break;
  default:
    // vertical flip
    context.translate(0, 1000);
    context.scale(1, -1);
    break;
  }
}

/* --- Some code here --- */
Link to comment
Share on other sites

Thanks a lot! I got it now. Also good to know about the efficiency improvement (it was in fact downloading the picture at every loop iteration... I hope my bill won't be too high at the end of the month ! :D ).

 

One more thing is that the orientation I am doing also inverts the dragging... is there any native html5 method I can use to prevent such a behaviour or do I have to stick with adding an exception to dragProfileImg() ?

Link to comment
Share on other sites

You would have to invert the operation on the profileImgX and profileImgY variables when you transform, then change it back after the transformation is done.

 

Another option is to transform the image before starting the animation and paste it on a new canvas, then draw that canvas onto the other canvas.

var transformedImage = new Canvas();
var transformContext = transformedImage.getContext("2d");
switch(orientation) {
  /* ...  Set width, height x and y and do transformations to the context  ... */
}
transformedImage.drawImage(profileImg, ... width, height, etc. ...);

Then in your animation loop just draw the image:

function animationLoop() {
  // Draw image
  renderProfileImg(transformedImage);

  // Draw overlay
  context.drawImage(overlayImg, canvas.width - distanceFromRightSide, canvas.height - distanceFromBottomSide, 183, 242);

  // Next iteration
  requestAnimationFrame(animationLoop, profileImg);
}

function renderProfileImg(img) {
  /* Calculate position, width, height, etc) */
  context.drawImage(img, ... ... );
}
Link to comment
Share on other sites

It's more efficient. In the first case you're doing extra calculations to figure out where things would be in the previous coordinate system, in the second case you're transforming the image only once and then not doing any other calculations.

 

Overall, the performance difference between the two is negligible for a simple application like this one. I would go with storing the transformed image on a new canvas so that I wouldn't have to figure out what inverse operations to do to calculate the drag position.

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...