I'm building an image color picker with pinch zoom and pan with JS Vanilla. I'm almost good but I still have some issues with :
The pinch zoom behaviour : When I pinch zoom, it's also translating. I would like to zoom on the same z axis.
The bahaviour on my mobile is completely different than on Chrome dev tool Mobile emulator. For example, my pan bounds limits are not taking into account on mobile.
Here is what I've done :
I've created a canva in which I draw an image. This image is scaled according to a zoom listened with touchstart, touchend and touchmove.
I've created a other canva for the picker in which I draw the zoomed image just after having draw the canva above.
My HTML code :
<div class="canva">
<%= cl_image_tag "#{@sketch.photo.key}", id: "my-image" %>
<div id="image-picker_image-container">
<div id="image-picker_image-element">
<div id="image-picker_cursors">
<div class="image-picker_cursor is-active" id="cursor-picker">
<canvas id="cursorCanva" width="130" height="130"></canvas>
</div>
</div>
<canvas id="image-picker_image_canvas"></canvas>
</div>
</div>
</div>
My JS code :
// Main canva
const img = new Image();
img.crossOrigin = "anonymous";
img.src = document.getElementById("my-image").getAttribute("src");
const canvas = document.getElementById("image-picker_image_canvas");
const ctx = canvas.getContext("2d");
// Zoom canva
const imgZoom = new Image();
imgZoom.crossOrigin = "anonymous";
imgZoom.src = document.getElementById("my-image").getAttribute("src");
const zoomCanva = document.getElementById("cursorCanva");
const zoomLevel = 30;
// Picker behaviour variables
const newCursor = document.getElementById("cursor-picker");
var x = 0,
y = 0,
mousedown = false;
// Pinch Zoom variables
var image_x = 0,
image_y = 0;
var zoom = 0.5;
var mouse_x = 0,
mouse_y = 0,
finger_dist = 0;
// We define the size of the main canva
canvas.width = window.screen.width;
canvas.height = window.screen.height * 0.6;
// We define the size of the zoom canva
const zoomCanva_w = zoomCanva.width;
const zoomCanva_h = zoomCanva.height;
console.log(zoomCanva_w);
const canvas_w = canvas.width,
canvas_h = canvas.height;
// Functions >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
// Pick function to get color of the hovered pixel
function pick(event, destination) {
const bounding = canvas.getBoundingClientRect();
let x = 0;
let y = 0;
if (touchMode) {
x = event.touches[0].pageX - bounding.left;
y = event.touches[0].pageY - bounding.top;
} else {
x = event.clientX - bounding.left;
y = event.clientY - bounding.top;
}
const pixel = ctx.getImageData(x, y, 1, 1);
const data = pixel.data;
const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${data[3] / 255})`;
destination.style.background = rgba;
destination.style.borderColor = rgba;
// destination.textContent = `(${x}, ${y})`;
return rgba;
}
// Get the Hex code of a color
function hexColour(c) {
let hex = Math.abs(c).toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
// Convert RGB code to Hex code
function rgbToHex(r, g, b) {
return "#" + hexColour(r) + hexColour(g) + hexColour(b);
}
function update_canvas() {
const mainCanvasCTX = canvas.getContext("2d");
// console.log(zoom);
const limitXRight = img.width - (canvas_w * zoom) / 2 - zoomLevel / 2;
const limitXLeft = - zoomLevel / 2 - (canvas_w * zoom) / 2;
const limitYTop = - zoomLevel / 2 - (canvas_h * zoom) / 2 ;
const limitYBottom = img.height - zoomLevel / 2 - (canvas_h * zoom) / 2;
// Keep picture in bounds
// Limit X Right
if (image_x > limitXRight) image_x = limitXRight;
// Limit Y Bottom
if (image_y > limitYBottom) image_y = limitYBottom;
// Limit X Left
if (image_x < limitXLeft) image_x = limitXLeft;
// Limit Y Top
if (image_y < limitYTop) image_y = limitYTop;
console.log("x="+image_x);
console.log("y="+image_y);
// Draw the scaled image onto the canvas
mainCanvasCTX.clearRect(0, 0, canvas_w, canvas_h);
mainCanvasCTX.drawImage(
img,
image_x,
image_y,
canvas_w * zoom,
canvas_h * zoom,
0,
0,
canvas_w,
canvas_h
);
const pixelatedZoomCtx = document
.getElementById("cursorCanva")
.getContext("2d");
pixelatedZoomCtx.imageSmoothingEnabled = false;
pixelatedZoomCtx.mozImageSmoothingEnabled = false;
pixelatedZoomCtx.webkitImageSmoothingEnabled = false;
pixelatedZoomCtx.msImageSmoothingEnabled = false;
const zoomCursor = (ctx, x, y) => {
// console.log(x);
// console.log(
// Math.min(Math.max(0, x - zoomLevel / 2), img.width - zoomLevel)
// );
ctx.clearRect(0, 0, zoomCanva_w, zoomCanva_h);
ctx.drawImage(
canvas,
Math.min(Math.max(0, x), img.width),
Math.min(Math.max(0, y), img.height),
zoomLevel,
zoomLevel,
0,
0,
pixelatedZoomCtx.canvas.width,
pixelatedZoomCtx.canvas.height
);
// Adding a circle and a cross to select color more precisely
const rayon = 30;
const centerX = pixelatedZoomCtx.canvas.width / 2;
const centerY = pixelatedZoomCtx.canvas.height / 2;
const crossLength = 5;
ctx.beginPath();
ctx.lineWidth = 5;
ctx.strokeStyle = "white";
ctx.arc(centerX, centerY, rayon, 0, 2 * Math.PI, false);
ctx.moveTo(centerX, centerY - crossLength);
ctx.lineWidth = 3;
ctx.lineTo(centerX, centerY + crossLength);
ctx.moveTo(centerX - crossLength, centerY);
ctx.lineTo(centerX + crossLength, centerY);
ctx.stroke();
ctx.strokeStyle = "white";
const pixel = ctx.getImageData(60, 60, 1, 1); // Allow to get color when we move and zoom canvas
const data = pixel.data;
const rgba = `rgba(${data[0]}, ${data[1]}, ${data[2]}, ${
data[3] / 255
})`;
const newCursor = document.getElementById("cursor-picker");
newCursor.children[0].style.background = rgba;
newCursor.children[0].style.borderColor = rgba;
colorChecker.style.background = rgba;
colorCheckerHexCode.textContent = rgbToHex(
data[0],
data[1],
data[2]
).toUpperCase();
};
zoomCursor(pixelatedZoomCtx, canvas_w / 2, canvas_h / 2); // Initialize cursor zoom content
}
function reset_settings() {
if (img.height > img.width) {
zoom = img.height / canvas_h;
} else {
zoom = img.width / canvas_w;
}
image_x = 0;
image_y = -(img.height - zoomLevel / 2 - (canvas_h * zoom) / 2);
newCursor.style.top = canvas_h / 2 + "px";
newCursor.style.left = canvas_w / 2 + "px";
update_canvas(); // Draw the image in its new position
}
function get_distance(e) {
var diffX = e.touches[0].clientX - e.touches[1].clientX;
var diffY = e.touches[0].clientY - e.touches[1].clientY;
return Math.sqrt(diffX * diffX + diffY * diffY); // Pythagorean theorem
}
img.addEventListener(
"load",
function () {
reset_settings();
},
false
); // Reset (x,y,zoom) when new image loads
// We add a touchstart listener on canvas to get fingers positions and calculate finger distance
canvas.addEventListener(
"touchstart",
function (e) {
if (e.touches.length > 1) {
// if multiple touches (pinch zooming)
finger_dist = get_distance(e); // Save current finger distance
} // Else just moving around
mouse_x = e.touches[0].clientX; // Save finger position
mouse_y = e.touches[0].clientY; //
},
false
);
// We add a touchmove listener on canvas to zoom canvas according to old and new distance of the fingers
canvas.addEventListener(
"touchmove",
function (e) {
e.preventDefault(); // Stop the window from moving
if (e.touches.length > 1) {
// If pinch-zooming
var new_finger_dist = get_distance(e); // Get current distance between fingers
zoom = zoom * Math.abs(finger_dist / new_finger_dist); // Zoom is proportional to change
let zoomMin = 0;
let zoomMax = 0;
if (img.height > img.width) {
zoomMin = img.height / canvas_h;
} else {
zoomMin = img.width / canvas_w;
}
zoomMax = zoomMin / 15; // We define maximum zoom relatively to minZoom
if (zoom > zoomMin) zoom = zoomMin;
if (zoom < zoomMax) zoom = zoomMax;
finger_dist = new_finger_dist; // Save current distance for next time
} else {
// Else just moving around
const decalX = zoom * (mouse_x - e.touches[0].clientX)
const decalY = zoom * (mouse_y - e.touches[0].clientY)
image_x = image_x + decalX; // Move the image
image_y = image_y + decalY; //
console.log("image_x : " + image_x);
console.log("mouse_x : " + mouse_x);
console.log("touchX : " + e.touches[0].clientX);
mouse_x = e.touches[0].clientX; // Save finger position for next time
mouse_y = e.touches[0].clientY; //
}
update_canvas(); // draw the new position
},
false
);
// We add a touchend listener on canvas to get coordinates of fingers when touch end
canvas.addEventListener(
"touchend",
function (e) {
mouse_x = e.touches[0].clientX;
mouse_y = e.touches[0].clientY; // could be down to 1 finger, back to moving image
},
false
);