// use requestAnimationFrame when doing any form of animation via javascript
requestAnimationFrame(draw);
var zoomIntensity = 0.2;
var canvas = document.getElementById("canvas");
var canvas2 = document.getElementById("canvas2");
var context = canvas.getContext("2d");
var context2 = canvas2.getContext("2d");
var width = 200;
var height = 200;
context.font = "24px arial";
context.textAlign = "center";
context.lineJoin = "round"; // to prevent miter spurs on strokeText
//fill smaller canvas with random pixels
for(var x = 0; x < 100; x++){
for(var y = 0; y < 100; y++) {
var rando = function(){return Math.floor(Math.random() * 9)};
var val = rando();
if(x === 0 || y === 0 || x === 99 || y === 99){
context2.fillStyle = "#FF0000";
}else{
context2.fillStyle = "#" + val + val + val;
}
context2.fillRect(x,y,1,1);
}
}
// mouse holds mouse position button state, and if mouse over canvas with overid
var mouse = {
pos : {x : 0, y : 0},
worldPos : {x : 0, y : 0},
posLast : {x : 0, y : 0},
button : false,
overId : "", // id of element mouse is over
dragging : false,
whichWheel : -1, // first wheel event will get the wheel
wheel : 0,
}
// View handles zoom and pan (can also handle rotate but have taken that out as rotate can not be contrained without losing some of the image or seeing some of the background.
const view = (()=>{
const matrix = [1,0,0,1,0,0]; // current view transform
const invMatrix = [1,0,0,1,0,0]; // current inverse view transform
var m = matrix; // alias
var im = invMatrix; // alias
var scale = 1; // current scale
const bounds = {
topLeft : 0,
left : 0,
right : 200,
bottom : 200,
}
var useConstraint = true; // if true then limit pan and zoom to
// keep bounds within the current context
var maxScale = 1;
const workPoint1 = {x :0, y : 0};
const workPoint2 = {x :0, y : 0};
const wp1 = workPoint1; // alias
const wp2 = workPoint2; // alias
var ctx;
const pos = { // current position of origin
x : 0,
y : 0,
}
var dirty = true;
const API = {
canvasDefault () { ctx.setTransform(1,0,0,1,0,0) },
apply(){
if(dirty){ this.update() }
ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
},
getScale () { return scale },
getMaxScale () { return maxScale },
matrix, // expose the matrix
invMatrix, // expose the inverse matrix
update(){ // call to update transforms
dirty = false;
m[3] = m[0] = scale;
m[1] = m[2] = 0;
m[4] = pos.x;
m[5] = pos.y;
if(useConstraint){
this.constrain();
}
this.invScale = 1 / scale;
// calculate the inverse transformation
var 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;
},
constrain(){
maxScale = Math.min(
ctx.canvas.width / (bounds.right - bounds.left) ,
ctx.canvas.height / (bounds.bottom - bounds.top)
);
if (scale < maxScale) { m[0] = m[3] = scale = maxScale }
wp1.x = bounds.left;
wp1.y = bounds.top;
this.toScreen(wp1,wp2);
if (wp2.x > 0) { m[4] = pos.x -= wp2.x }
if (wp2.y > 0) { m[5] = pos.y -= wp2.y }
wp1.x = bounds.right;
wp1.y = bounds.bottom;
this.toScreen(wp1,wp2);
if (wp2.x < ctx.canvas.width) { m[4] = (pos.x -= wp2.x - ctx.canvas.width) }
if (wp2.y < ctx.canvas.height) { m[5] = (pos.y -= wp2.y - ctx.canvas.height) }
},
toWorld(from,point = {}){ // convert screen to world coords
var xx, yy;
if(dirty){ this.update() }
xx = from.x - m[4];
yy = from.y - m[5];
point.x = xx * im[0] + yy * im[2];
point.y = xx * im[1] + yy * im[3];
return point;
},
toScreen(from,point = {}){ // convert world coords to screen coords
if(dirty){ this.update() }
point.x = from.x * m[0] + from.y * m[2] + m[4];
point.y = from.x * m[1] + from.y * m[3] + m[5];
return point;
},
scaleAt(at, amount){ // at in screen coords
if(dirty){ this.update() }
scale *= amount;
pos.x = at.x - (at.x - pos.x) * amount;
pos.y = at.y - (at.y - pos.y) * amount;
dirty = true;
},
move(x,y){ // move is in screen coords
pos.x += x;
pos.y += y;
dirty = true;
},
setContext(context){
ctx = context;
dirty = true;
},
setBounds(top,left,right,bottom){
bounds.top = top;
bounds.left = left;
bounds.right = right;
bounds.bottom = bottom;
useConstraint = true;
dirty = true;
}
};
return API;
})();
view.setBounds(0,0,canvas2.width,canvas2.height);
view.setContext(context);
//draw the larger canvas
function draw(){
view.canvasDefault(); // se default transform to clear screen
context.imageSmoothingEnabled = false;
context.fillStyle = "white";
context.fillRect(0, 0, width, height);
view.apply(); // set the current view
context.drawImage(canvas2, 0,0);
view.canvasDefault();
if(view.getScale() === view.getMaxScale()){
context.fillStyle = "black";
context.strokeStyle = "white";
context.lineWidth = 2.5;
context.strokeText("Max scale.",context.canvas.width / 2,24);
context.fillText("Max scale.",context.canvas.width / 2,24);
}
requestAnimationFrame(draw);
if(mouse.overId === "canvas"){
canvas.style.cursor = mouse.button ? "none" : "move";
}else{
canvas.style.cursor = "default";
}
}
// add events to document so that mouse is captured when down on canvas
// This allows the mouseup event to be heard no matter where the mouse has
// moved to.
"mousemove,mousedown,mouseup,mousewheel,wheel,DOMMouseScroll".split(",")
.forEach(eventName=>document.addEventListener(eventName,mouseEvent));
function mouseEvent (event){
mouse.overId = event.target.id;
if(event.target.id === "canvas" || mouse.dragging){ // only interested in canvas mouse events including drag event started on the canvas.
mouse.posLast.x = mouse.pos.x;
mouse.posLast.y = mouse.pos.y;
mouse.pos.x = event.clientX - canvas.offsetLeft;
mouse.pos.y = event.clientY - canvas.offsetTop;
view.toWorld(mouse.pos, mouse.worldPos); // gets the world coords (where on canvas 2 the mouse is)
if (event.type === "mousemove"){
if(mouse.button){
view.move(
mouse.pos.x - mouse.posLast.x,
mouse.pos.y - mouse.posLast.y
)
}
} else if (event.type === "mousedown") { mouse.button = true; mouse.dragging = true }
else if (event.type === "mouseup") { mouse.button = false; mouse.dragging = false }
else if(event.type === "mousewheel" && (mouse.whichWheel === 1 || mouse.whichWheel === -1)){
mouse.whichWheel = 1;
mouse.wheel = event.wheelDelta;
}else if(event.type === "wheel" && (mouse.whichWheel === 2 || mouse.whichWheel === -1)){
mouse.whichWheel = 2;
mouse.wheel = -event.deltaY;
}else if(event.type === "DOMMouseScroll" && (mouse.whichWheel === 3 || mouse.whichWheel === -1)){
mouse.whichWheel = 3;
mouse.wheel = -event.detail;
}
if(mouse.wheel !== 0){
event.preventDefault();
view.scaleAt(mouse.pos, Math.exp((mouse.wheel / 120) *zoomIntensity));
mouse.wheel = 0;
}
}
}
div { user-select: none;} /* mouse prevent drag selecting content */
canvas { border:2px solid black;}
<div>
<canvas id="canvas" width="200" height="200"></canvas>
<canvas id="canvas2" width="100" height="100"></canvas>
<p>Mouse wheel to zoom. Mouse click drag to pan.</p>
<p>Zoomed image constrained to canvas</p>
</div>