As a follow up to this question and answer...I have another issue to solve:
When I draw on a canvas and then apply some transformations like rotation, I would like to keep what was drawn and continue the drawing.
To test this, use the mouse to draw something and then click "rotate".
This is what I'm trying, but the canvas gets erased.
JS
//main variables
canvas = document.createElement("canvas");
canvas.width = 500;
canvas.height = 300;
canvas.ctx = canvas.getContext("2d");
ctx = canvas.ctx;
canvas_aux = document.createElement("canvas");
canvas_aux.width = 500;
canvas_aux.height = 300;
canvas_aux.ctx = canvas.getContext("2d");
ctx_aux = canvas_aux.ctx;
function rotate()
{
ctx_aux.drawImage(canvas, 0, 0); //new line: save current drawing
timer += timerStep;
var cw = canvas.width / 2;
var ch = canvas.height / 2;
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset the transform so we can clear
ctx.clearRect(0, 0, canvas.width, canvas.height); // clear the canvas
createMatrix(cw, ch -50, scale, timer);
var m = matrix;
ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
//draw();
ctx.drawImage(canvas_aux, 0, 0); //new line: repaint current drawing
if(timer <= rotation )
{
requestAnimationFrame(rotate);
}
}
DEMO (updated version of original in linked question/answer)
https://jsfiddle.net/mgf8uz7s/1/
The image on your canvas will be rotated by that angle. Press and hold the Shift + Space keys and then click and drag the image to rotate it in whichever direction you want. Alternatively, press the “ 4 ” key to rotate it in 15-degree increments counter-clockwise or “ 6 ” to rotate it in 15-degree increments clockwise.
Using HTML5 Canvas effectively requires a strong foundation in drawing, coloring, and transforming basic two-dimensional shapes. While the selection of built-in shapes is relatively limited, we can draw any shape we desire using a series of line segments called paths , which we will discuss in the upcoming section Using Paths to Create Lines.
On Canvas, basic rectangle shapes can be drawn in three different ways: filling, stroking, or clearing. We can also build rectangles (or any other shape) by using paths, which we will cover in the next section. First, let’s look at the API functions used for these three operations:
Well, the entire co-ordinate system rotated by 0.5 radians (roughly 30º) around the top-left corner of the canvas before we drew the image. So if you think about it, 50 across and 35 down isn’t the same as place as it used to be. So how do we rotate our image and keep it in the same place?
You have several options which will depend on what the requirements are.
Offscreen buffer/s to hold the rendered lines. Render to the offscreen buffer then draw the buffer to the display canvas. This is the quickest method but you are working with pixels, thus if you zoom you will get pixel artifacts and it will limit the size of the drawing area (still large but not pseudo infinite) and severely restrict the number of undos your can provide due to memory limits
Buffer paths as they are draw, basicly recording mouse movements and clicks, then re-rendering all visible paths each time you update the display. This will let you zoom and rotate without pixel artifacts, give you a draw area as large as you like (within limit of 64bit doubles) and a bonus undo all the way back to the first line. The problem with this method is that it quickly becomes very slow (though you can improve rendering speed with webGL)
A combination of the above two methods. Record the paths as they are drawn, but also render them to an offscreen canvas/s. Use the offscreen canvas to update the display and keep the refresh rate high. You only re-render the offscreen canvas when you need to, ie when you undo or if you zoom, you will not need to re-render when you pan or rotate.
I am not going to do a full drawing package so this is just an example that uses an offscreen buffer to hold the visible paths. All paths that are drawn are recorded in a paths array. When the user changes the view, pan, zoom, rotate, the paths are redrawn to the offscreen canvas to match the new view.
There is some boilerplate to handle setup and mouse that can be ignored. As there is a lot of code and time is short you will have to pick out what you need from it as the comments are short.
There is a paths
object for paths. view
holds the transform and associated functions. Some functions for pan, zoom, rotate. And a display function that renders and handles all mouse and user IO. The pan,zoom and scale controls are accessed via holding the mouse modifiers ctrl, alt, shift
var drawing = createImage(100,100); // offscreen canvas for drawing paths
// the onResize is a callback used by the boilerplate code at the bottom of this snippet
// it is called whenever the display size has changed (including starting app). It is
// debounced by 100ms to prevent needless calls
var onResize = function(){
drawing.width = canvas.width;
drawing.height = canvas.height;
redrawBuffers = true; // flag that drawing buffers need redrawing
ctx.font = "18px arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
view.pos.x = cw; // set origin at center of screen
view.pos.y = ch;
view.update();
}
const paths = []; // array of all recorded paths
const path = { // descriptor of a path object
addPoint(x,y){ // adds a point to the path
this.points.push({x,y});
},
draw(ctx){ // draws this path on context ctx
var i = 0;
ctx.beginPath();
ctx.moveTo(this.points[i].x,this.points[i++].y);
while(i < this.points.length){
ctx.lineTo(this.points[i].x,this.points[i++].y);
}
ctx.stroke();
}
}
// creates a new path and adds it to the array of paths.
// returns the new path
function addPath(){
var newPath;
newPath = Object.assign({points : []},path);
paths.push(newPath)
return newPath;
}
// draws all recorded paths onto context cts using the current view
function drawAll(ctx){
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,w,h);
var m = view.matrix;
ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
var i = 0;
for(i = 0; i < paths.length; i ++){
paths[i].draw(ctx);
}
}
// this controls the view
const view = {
matrix : [1,0,0,1,0,0], // current view transform
invMatrix : [1,0,0,1,0,0], // current inverse view transform
rotate : 0, // current x axis direction in radians
scale : 1, // current scale
pos : { // current position of origin
x : 0,
y : 0,
},
update(){ // call to update transforms
var xdx = Math.cos(this.rotate) * this.scale;
var xdy = Math.sin(this.rotate) * this.scale;
var m = this.matrix;
var im = this.invMatrix;
m[0] = xdx;
m[1] = xdy;
m[2] = -xdy;
m[3] = xdx;
m[4] = this.pos.x;
m[5] = this.pos.y;
// calculate the inverse transformation
cross = m[0] * m[3] - m[1] * m[2];
im[0] = m[3] / cross;
im[1] = -m[1] / cross;
im[2] = -m[2] / cross;
im[3] = m[0] / cross;
},
mouseToWorld(){ // conver screen to world coords
var xx, yy, m;
m = this.invMatrix;
xx = mouse.x - this.matrix[4];
yy = mouse.y - this.matrix[5];
mouse.xr = xx * m[0] + yy * m[2];
mouse.yr = xx * m[1] + yy * m[3];
},
toWorld(x,y,point = {}){ // convert screen to world coords
var xx, yy, m;
m = this.invMatrix;
xx = x - this.matrix[4];
yy = y - this.matrix[5];
point.x = xx * m[0] + yy * m[2];
point.y = xx * m[1] + yy * m[3];
return point;
},
toScreen(x,y,point = {}){ // convert world coords to coords
var m;
m = this.matrix;
point.x = x * m[0] + y * m[2] + m[4];
point.y = x * m[1] + y * m[3] + m[5];
return point;
},
clickOrigin : { // used to hold coords to deal with pan zoom and rotate
x : 0,
y : 0,
scale : 1,
},
dragging : false, // true is dragging
startDrag(){ // called to start a Orientation UI input such as rotate, pan and scale
if(!view.dragging){
view.dragging = true;
view.clickOrigin.x = mouse.xr;
view.clickOrigin.y = mouse.yr;
view.clickOrigin.screenX = mouse.x;
view.clickOrigin.screenY = mouse.y;
view.clickOrigin.scale = view.scale;
}
}
}
// functions to do pan zoom and scale
function panView(){ // pans the view
view.startDrag(); // set origins as referance point
view.pos.x -= (view.clickOrigin.screenX - mouse.x);
view.pos.y -= (view.clickOrigin.screenY - mouse.y);
view.update();
view.mouseToWorld(); // get the new mouse pos
view.clickOrigin.screenX = mouse.x; // save the new mouse coords
view.clickOrigin.screenY = mouse.y;
}
// scales the view
function scaleView(){
view.startDrag();
var y = view.clickOrigin.screenY - mouse.y;
if(y !== 0){
view.scale = view.clickOrigin.scale + (y/ch);
view.update();
}
}
// rotates the view by setting the x axis direction
function rotateView(){
view.startDrag();
workingCoord = view.toScreen(0,0,workingCoord); // get location of origin
var x = workingCoord.x - mouse.x;
var y = workingCoord.y - mouse.y;
var dist = Math.sqrt(x * x + y * y);
if(dist > 2 / view.scale){
view.rotate = Math.atan2(-y,-x);
view.update();
}
}
var currentPath; // Holds the currently drawn path
var redrawBuffers = false; // if true this indicates that all paths need to be redrawn
var workingCoord; // var to use as a coordinate
// main loop function called from requestAnimationFrame callback in boilerplate code
function display() {
var showTransform = false; // flags that view is being changed
// clear the canvas and set defaults
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.clearRect(0, 0, w, h);
view.mouseToWorld(); // get the mouse world coords
// get the transform matrix
var m = view.matrix;
// show feedback
if(mouse.shift || mouse.alt || mouse.ctrl){
if(mouse.shift){
ctx.fillText("Click drag to pan",cw, 20);
}else if(mouse.ctrl){
ctx.fillText("Click drag to rotate",cw, 20);
}else{
ctx.fillText("Click drag to scale : " + view.scale.toFixed(4),cw, 20);
}
}else{
ctx.fillText("Click drag to draw.",cw, 20);
ctx.fillText("Hold [shift], [ctrl], or [alt] and use mouse to pan, rotate, scale",cw, 40);
}
if(mouse.buttonRaw === 1){ // when mouse is down
if(mouse.shift || mouse.alt || mouse.ctrl){ // pan zoom rotate
if(mouse.shift){
panView();
}else if(mouse.ctrl){
rotateView();
}else{
scaleView();
}
m = view.matrix;
showTransform = true;
redrawBuffers = true;
}else{ // or add a path
if(currentPath === undefined){
currentPath = addPath();
}
currentPath.addPoint(mouse.xr,mouse.yr)
}
}else{
// if there is a path then draw it onto the offscreen canvas and
// reset the path to undefined
if(currentPath !== undefined){
currentPath.draw(drawing.ctx);
currentPath = undefined;
}
view.dragging = false; // incase there is a pan/zoom/scale happening turn it off
}
if(showTransform){ // redraw all paths when pan rotate or zoom
redrawBuffers = false;
drawAll(drawing.ctx);
ctx.drawImage(drawing,0,0);
}else{ // draws the sceen when normal drawing mode.
if(redrawBuffers){
redrawBuffers = false;
drawAll(drawing.ctx);
}
ctx.drawImage(drawing,0,0);
ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
drawing.ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
// draw a cross hair.
if(mouse.buttonRaw === 0){
var invScale = 1 / view.scale; // get inverted scale
ctx.beginPath();
ctx.moveTo(mouse.xr - 10 * invScale,mouse.yr);
ctx.lineTo(mouse.xr + 10 * invScale,mouse.yr);
ctx.moveTo(mouse.xr ,mouse.yr - 10 * invScale);
ctx.lineTo(mouse.xr ,mouse.yr + 10 * invScale);
ctx.lineWidth = invScale;
ctx.stroke();
ctx.lineWidth = 1;
}
}
// draw a new path if being drawn
if(currentPath){
currentPath.draw(ctx);
}
// If rotating or about to rotate show feedback
if(mouse.ctrl){
ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
view.mouseToWorld(); // get the mouse world coords
ctx.strokeStyle = "black";
ctx.lineWidth = 3;
ctx.beginPath();
ctx.arc(0,0,3,0,Math.PI * 2);
ctx.moveTo(0,0);
ctx.lineTo(mouse.xr,mouse.yr);
ctx.stroke();
ctx.lineWidth = 1.5;
ctx.strokeStyle = "red";
ctx.beginPath();
ctx.arc(0,0,3,0,Math.PI * 2);
ctx.moveTo(0,0);
ctx.lineTo(mouse.xr,mouse.yr);
ctx.stroke();
ctx.strokeStyle = "black";
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo(200000 / view.scale,0);
ctx.stroke();
ctx.scale(1/ view.scale,1 / view.scale);
ctx.fillText("X axis",100 ,-10 );
}
}
/******************************************************************************/
// end of answer code
/******************************************************************************/
//Boiler plate from here down and can be ignored.
var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;
;(function(){
const RESIZE_DEBOUNCE_TIME = 100;
var createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
createCanvas = function () {
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;
}
resizeCanvas = function () {
if (canvas === undefined) {
canvas = createCanvas();
}
canvas.width = innerWidth;
canvas.height = innerHeight;
ctx = canvas.getContext("2d");
if (typeof setGlobals === "function") {
setGlobals();
}
if (typeof onResize === "function") {
if(firstRun){
onResize();
firstRun = false;
}else{
resizeCount += 1;
setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
}
}
}
function debounceResize() {
resizeCount -= 1;
if (resizeCount <= 0) {
onResize();
}
}
setGlobals = function () {
cw = (w = canvas.width) / 2;
ch = (h = canvas.height) / 2;
}
mouse = (function () {
function preventDefault(e) {
e.preventDefault();
}
var mouse = {
x : 0,
y : 0,
w : 0,
alt : false,
shift : false,
ctrl : false,
buttonRaw : 0,
over : false,
bm : [1, 2, 4, 6, 5, 3],
active : false,
bounds : null,
crashRecover : null,
mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
};
var m = mouse;
function mouseMove(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left;
m.y = e.pageY - m.bounds.top;
m.alt = e.altKey;
m.shift = e.shiftKey;
m.ctrl = e.ctrlKey;
if (t === "mousedown") {
m.buttonRaw |= m.bm[e.which - 1];
} else if (t === "mouseup") {
m.buttonRaw &= m.bm[e.which + 2];
} else if (t === "mouseout") {
m.buttonRaw = 0;
m.over = false;
} else if (t === "mouseover") {
m.over = true;
} else if (t === "mousewheel") {
m.w = e.wheelDelta;
} else if (t === "DOMMouseScroll") {
m.w = -e.detail;
}
if (m.callbacks) {
m.callbacks.forEach(c => c(e));
}
e.preventDefault();
}
m.addCallback = function (callback) {
if (typeof callback === "function") {
if (m.callbacks === undefined) {
m.callbacks = [callback];
} else {
m.callbacks.push(callback);
}
}
}
m.start = function (element) {
if (m.element !== undefined) {
m.removeMouse();
}
m.element = element === undefined ? document : element;
m.mouseEvents.forEach(n => {
m.element.addEventListener(n, mouseMove);
});
m.element.addEventListener("contextmenu", preventDefault, false);
m.active = true;
}
m.remove = function () {
if (m.element !== undefined) {
m.mouseEvents.forEach(n => {
m.element.removeEventListener(n, mouseMove);
});
m.element.removeEventListener("contextmenu", preventDefault);
m.element = m.callbacks = undefined;
m.active = false;
}
}
return mouse;
})();
function update(timer) { // Main update loop
globalTime = timer;
display(); // call demo code
requestAnimationFrame(update);
}
setTimeout(function(){
resizeCanvas();
mouse.start(canvas, true);
window.addEventListener("resize", resizeCanvas);
requestAnimationFrame(update);
},0);
})();
/** SimpleFullCanvasMouse.js end **/
// creates a blank image with 2d context
function createImage(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}
UPDATE
toScreen(x,y)
function to view object. Converts from world coordinates to screen coordinates.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With