Is it possible to rotate CanvasRenderingContext2D around Y-axis only using Javascript? - javascript

I was trying to find the way to rotate ctx around Y-axis only using Javascript not CSS.
My code is below.
function renderFrame() {
requestAnimationFrame(renderFrame);
x = 0;
analyser.getByteFrequencyData(dataArray);
ctx1.fillStyle = "rgba(0,0,0,0.2)";
ctx1.fillRect(0, 0, WIDTH, HEIGHT);
ctx2.fillStyle = "rgba(0,0,0,0.2)";
ctx2.fillRect(0, 0, WIDTH, HEIGHT);
let r, g, b;
let bars = 118;
for (let i = 0; i < bars; i++) {
barHeight = dataArray[i] * 2.5;
if (dataArray[i] > 210) {
// pink
r = 250;
g = 0;
b = 255;
} else if (dataArray[i] > 200) {
// yellow
r = 250;
g = 255;
b = 0;
} else if (dataArray[i] > 190) {
// yellow/green
r = 204;
g = 255;
b = 0;
} else if (dataArray[i] > 180) {
// blue/green
r = 0;
g = 219;
b = 131;
} else {
// light blue
r = 0;
g = 199;
b = 255;
}
ctx1.fillStyle = `rgb(${r},${g},${b})`;
ctx1.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);
ctx2.fillStyle = `rgb(${r},${g},${b})`;
ctx2.fillRect(x, HEIGHT - barHeight, barWidth, barHeight);
x += barWidth + 10;
}
}
renderFrame();
I want to rotate ctx around Y-axis not canvas.
Is there possible way? or do i have to use WebGL?
If you ask why i need this,
I want canvas and context to fit in the left screen of the image.
what could be the best way to make canvas to be fit in?
Oh, i'm using requestAnimationFrame to keep drawing something on context.
That's why I need to rotate the context, not only the canvas.

Related

Making HSL pallet with selectable gradient problem

I am trying to make a color pallet in which the user can click on the gradient and it will show you the RGB and HSL values so far I have printed out all the Hue values and now what I want to do is make the appropriate display of Saturation and Luminesance/lightness values as can be seen here https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Colors/Color_picker_tool
I have tried to use a double nested loop as a generator such as:
function * hslGen(hue){
for(let s = 0; s < 100; s++){
for(let l = 100; l > 0; l--){
yield `hsl(${hue}, ${s}%, ${l}%)`;}
}
}
But the result looks the following https://codepen.io/superpauly/pen/XWMQboe?editors=1010 which confivuration should I have the loops to display a collor pallet like shown in the example above?
So I have got the generator and the rest of the code is as follow:
const canvasContext = canvRef.current.getContext("2d");
for (let hue = 0; hue < 360; hue++) {
// Generates Hue Spectrum
canvasContext.fillStyle = "hsl(" + hue + ", 100%, 50%)";
canvasContext.fillRect(5 * hue, 0, 5, 75);
}
canvRef.current.addEventListener("click", (e) => {
data = canvasContext.getImageData(e.layerX, e.layerY, 1, 1).data;
color.rgb = `rgb( ${data[0]}, ${data[1]}, ${data[2]})`;
color.h = rgbToHue(data);
color.hsl = `hsl( ${color.h}, 100%, 50%)`;
let pixel = 0;
for (let m of hslGen(color.h)){
canvasContext.fillStyle = m; //HSL Generator string
canvasContext.fillRect(pixel+=1, 75, 1440, 500); // Here is where I try to make the gradient bu it fails.
}
Thank you.
You need track x and y coordinates for each pixel:
import React, {
useEffect,
useRef,
useState
} from "https://cdn.skypack.dev/react#17.0.1";
import ReactDOM from "https://cdn.skypack.dev/react-dom#17.0.1";
console.clear();
function * hslGen(hue){
for(let l = 100; l >= 0; l--)
{
for(let s = 0; s <= 100; s++)
{
yield `hsl(${hue}, ${s}%, ${l}%)`;
}
}
}
const RGBToHex = (r, g, b) => {
r = r.toString(16);
g = g.toString(16);
b = b.toString(16);
if (r.length == 1) r = "0" + r;
if (g.length == 1) g = "0" + g;
if (b.length == 1) b = "0" + b;
return `#${r+g+b}`;
};
function rgbToHue(getImageData) {
let rgbHue = {
red: getImageData[0] / 255,
green: getImageData[1] / 255,
blue: getImageData[2] / 255
};
let maxValueKey = Object.keys(rgbHue).reduce((a, b) =>
rgbHue[a] > rgbHue[b] ? a : b
);
let maxValue = Math.max(rgbHue["red"], rgbHue["green"], rgbHue["blue"]);
let minValue = Math.min(rgbHue["red"], rgbHue["green"], rgbHue["blue"]);
let hue = 0.0;
switch (maxValueKey) {
case "red":
hue = (rgbHue["green"] - rgbHue["blue"]) / (maxValue - minValue);
break;
case "green":
hue = 2.0 + (rgbHue["blue"] - rgbHue["red"]) / (maxValue - minValue);
break;
case "blue":
hue = 4.0 + (rgbHue["red"] - rgbHue["green"]) / (maxValue - minValue);
break;
}
hue = hue * 60.0;
if (hue < 0.0) {
hue = hue + 360.0;
}
console.log("Hue in Deg: " + hue);
return hue;
}
const ColorPicker = () => {
const canvRef = useRef();
const color = { rgb: "", hsl: "",
h:0};
let [col, setCol] = useState({});
let data = 0;
useEffect(() => {
const canvasContext = canvRef.current.getContext("2d");
for (let hue = 0; hue < 360; hue++) {
canvasContext.fillStyle = "hsl(" + hue + ", 100%, 50%)";
canvasContext.fillRect(5 * hue, 0, 5, 75);
}
canvRef.current.addEventListener("click", (e) => {
data = canvasContext.getImageData(e.layerX, e.layerY, 1, 1).data;
color.rgb = `rgb( ${data[0]}, ${data[1]}, ${data[2]})`;
color.h = rgbToHue(data);
color.hsl = `hsl( ${color.h}, 100%, 50%)`;
setCol({
rgb: color.rgb,
hsl: color.hsl
});
debugger;
let x = 0,
y = 0,
pix = 3; //pixel size
console.log("Start");
for (let m of hslGen(color.h)){
canvasContext.fillStyle = m;
canvasContext.fillRect(100 + x, 175 + y, pix, pix); //Need to made this a 2d saturation, light graph
console.log(m, x, y);
x += pix;
x = x % (pix * 101);
if (!x)
y += pix;
}
console.log("End");
});
console.log(color);
}, []);
return (
<>
<canvas
ref={canvRef}
height={window.innerHeight}
width={window.innerWidth}
></canvas>
<span>HSL: {col.hsl}</span>
<span>RGB: {col.rgb}</span>
</>
);
};
ReactDOM.render(, document.getElementById("colorCanvas"));
https://codepen.io/vanowm/pen/JjWgJOO
If you're set on using the generator you can do something like so:
const scale = 4; // likely don't want the user to have to click on a single pixel so need some kind of scale factor
const hueInput = document.getElementById('hue');
const canvas = document.getElementById('color-picker');
canvas.width = 100 * scale;
canvas.height = 100 * scale;
canvas.style.width = canvas.width + 'px';
canvas.style.height = canvas.height + 'px';
const context = canvas.getContext('2d');
function * hslGen(hue){
for(let s = 0; s < 100; s++){
for(let l = 100; l > 0; l--){
yield `hsl(${hue}, ${s}%, ${l}%)`;}
}
}
function updateColorPicker() {
const hue = hueInput.value;
// need to keep track of both x and y as it's two dimensional
let y = 0, x = 0;
for (const m of hslGen(hue)) {
context.fillStyle = m; //HSL Generator string
context.fillRect(x * scale, y * scale, scale, scale);
// iterator is doing saturation then lightness so we populate y first then x
y++;
if (y >= 100) {
y = 0;
x++;
}
}
}
hueInput.addEventListener('change', updateColorPicker);
updateColorPicker();
<label> HUE <input type="number" id="hue" value="5"></label>
<br>
<canvas id="color-picker"></canvas>
Because the expected output is 2 dimensional you need both an x and y component. Because of this, the generator in it's current form doesn't seem to make much sense.
A better approach would be to either remove the generator altogether, or make it fit the problem better. Here's an example without the generator :
const scale = 4; // likely don't want the user to have to click on a single pixel so need some kind of scale factor
const hueInput = document.getElementById('hue');
const canvas = document.getElementById('color-picker');
canvas.width = 100 * scale;
canvas.height = 100 * scale;
canvas.style.width = canvas.width + 'px';
canvas.style.height = canvas.height + 'px';
const context = canvas.getContext('2d');
function updateColorPicker() {
const hue = hueInput.value;
// need to keep track of both x and y as it's two dimensional
for(let x = 0; x < 100; x++) {
for(let y = 0; y < 100; y++) {
var m = `hsl(${hue}, ${x}%, ${100-y}%)`;
context.fillStyle = m; //HSL Generator string
context.fillRect(x * scale, y * scale, scale, scale);
}
}
}
hueInput.addEventListener('change', updateColorPicker);
updateColorPicker();
<label> HUE <input type="number" id="hue" value="5"></label>
<br>
<canvas id="color-picker"></canvas>

Converting image to black and white with Javascript [duplicate]

This question already has answers here:
How do you convert a color image to black/white using Javascript?
(11 answers)
Closed 3 years ago.
I have a js function, it returns the image on canvas that I give in the source and B&W of it side by side, the thing is I am a beginner in js so I do not know which line should I edit to get the only B&W image as a result on the canvas not the original image.
function imageLoaded(ev) {
element = document.getElementById("canvas");
const c = element.getContext("2d");
im = ev.target;
width = element.width;
height = element.height;
//image on the left of the canvas:
c.drawImage(im, 0, 0);
let imageData = c.getImageData(0, 0, width, height);
// width index is output position.
w2 = width / 2;
// run through the image.
// height of the image.
for (y = 0; y < height; y++) {
// *4 for 4 ints per pixel.
//this is an input index.
inpos = y * width * 4;
//this is an output index.
outpos = inpos + w2 * 4
// width of the image.
for (x = 0; x < w2; x++) {
r = imageData.data[inpos++];
g = imageData.data[inpos++];
b = imageData.data[inpos++];
a = imageData.data[inpos++];
// this is transforming RGB color space to gray scale.
gray = (0.30 * r + 0.59 * g + 0.11 * b);
// proper threshold value for black and white
if (gray > 55) {
//set the pixel to white.
imageData.data[outpos++] = 255;
imageData.data[outpos++] = 255;
imageData.data[outpos++] = 255;
imageData.data[outpos++] = a;
} else {
//set the pixel to black.
imageData.data[outpos++] = 0;
imageData.data[outpos++] = 0;
imageData.data[outpos++] = 0;
imageData.data[outpos++] = a;
}
}
}
//put pixel data on canvas.
c.putImageData(imageData, 0, 0);
}
im = new Image();
im.onload = imageLoaded;
//im.src = "B_01.jpg";
//im.src = "img.png";
//im.src = "164228060-dodge-challenger-wallpapers.jpg";
im.src = "rab.jpg";
//im.src = "https://picsum.photos/id/431/200/300";
Here is the final code:
function imageLoaded(ev) {
element = document.getElementById("canvas");
const c = element.getContext("2d");
im = ev.target;
width = element.width;
height = element.height;
//image on the left of the canvas:
c.drawImage(im, 0, 0);
let imageData = c.getImageData(0, 0, width, height);
// width index is output position.
w2 = width;
// run through the image.
// height of the image.
for (y = 0; y < height; y++) {
// *4 for 4 ints per pixel.
//this is an input index.
inpos = y * width * 4;
//this is an output index.
outpos = inpos;
// width of the image.
for (x = 0; x < w2; x++) {
r = imageData.data[inpos++];
g = imageData.data[inpos++];
b = imageData.data[inpos++];
a = imageData.data[inpos++];
// this is transforming RGB color space to gray scale.
gray = (0.30 * r + 0.59 * g + 0.11 * b);
// proper threshold value for black and white
if (gray > 55) {
//set the pixel to white.
imageData.data[outpos++] = 255;
imageData.data[outpos++] = 255;
imageData.data[outpos++] = 255;
imageData.data[outpos++] = a;
} else {
//set the pixel to black.
imageData.data[outpos++] = 0;
imageData.data[outpos++] = 0;
imageData.data[outpos++] = 0;
imageData.data[outpos++] = a;
}
}
}
//put pixel data on canvas.
c.putImageData(imageData, 0, 0);
}
im = new Image();
im.onload = imageLoaded;
im.src = "rab.jpg";
I have changed two things:
I removed the division by 2, so you can see the full image:
// width index is output position.
w2 = width;
And I set outpos equal to inpos:
//this is an input index.
inpos = y * width * 4;
//this is an output index.
outpos = inpos;
This mean that the input position ist the same as the output position, which causes an override of the current pixel color.

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();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = 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>

Automatically Crop HTML5 canvas to contents

Let's say this is my canvas, with an evil-looking face drawn on it. I want to use toDataURL() to export my evil face as a PNG; however, the whole canvas is rasterised, including the 'whitespace' between the evil face and canvas edges.
+---------------+
| |
| |
| (.Y. ) |
| /_ |
| \____/ |
| |
| |
+---------------+
What is the best way to crop/trim/shrinkwrap my canvas to its contents, so my PNG is no larger than the face's 'bounding-box', like below? The best way seems to be scaling the canvas, but supposing the contents are dynamic...? I'm sure there should be a simple solution to this, but it's escaping me, with much Googling.
+------+
|(.Y. )|
| /_ |
|\____/|
+------+
Thanks!
Edited (see comments)
function cropImageFromCanvas(ctx) {
var canvas = ctx.canvas,
w = canvas.width, h = canvas.height,
pix = {x:[], y:[]},
imageData = ctx.getImageData(0,0,canvas.width,canvas.height),
x, y, index;
for (y = 0; y < h; y++) {
for (x = 0; x < w; x++) {
index = (y * w + x) * 4;
if (imageData.data[index+3] > 0) {
pix.x.push(x);
pix.y.push(y);
}
}
}
pix.x.sort(function(a,b){return a-b});
pix.y.sort(function(a,b){return a-b});
var n = pix.x.length-1;
w = 1 + pix.x[n] - pix.x[0];
h = 1 + pix.y[n] - pix.y[0];
var cut = ctx.getImageData(pix.x[0], pix.y[0], w, h);
canvas.width = w;
canvas.height = h;
ctx.putImageData(cut, 0, 0);
var image = canvas.toDataURL();
}
If I understood well you want to "trim" away all the surronding your image / drawing, and adjust the canvas to that size (like if you do a "trim" command in Photoshop).
Here is how I'll do it.
Run thru all the canvas pixels checking if their alpha component is > 0 (that means that something is drawn in that pixel). Alternativelly you could check for the r,g,b values, if your canvas background is fullfilled with a solid color, for instance.
Get te coordinates of the top most left pixel non-empty, and same for the bottom most right one. So you'll get the coordinates of an imaginay "rectangle" containing the canvas area that is not empty.
Store that region of pixeldata.
Resize your canvas to its new dimensions (the ones of the region we got at step 2.)
Paste the saved region back to the canvas.
Et, voilá :)
Accesing pixeldata is quite slow depending on the size of your canvas (if its huge it can take a while). There are some optimizations around to work with raw canvas pixeldata (I think there is an article about this topic at MDN), I suggest you to google about it.
I prepared a small sketch in jsFiddle that you can use as starting point for your code.
Working sample at jsFiddle
Hope I've helped you.
c:.
Here's my take. I felt like all the other solutions were overly complicated. Though, after creating it, I now see it's the same solution as one other's, expect they just shared a fiddle and not a function.
function trimCanvas(canvas){
const context = canvas.getContext('2d');
const topLeft = {
x: canvas.width,
y: canvas.height,
update(x,y){
this.x = Math.min(this.x,x);
this.y = Math.min(this.y,y);
}
};
const bottomRight = {
x: 0,
y: 0,
update(x,y){
this.x = Math.max(this.x,x);
this.y = Math.max(this.y,y);
}
};
const imageData = context.getImageData(0,0,canvas.width,canvas.height);
for(let x = 0; x < canvas.width; x++){
for(let y = 0; y < canvas.height; y++){
const alpha = imageData.data[((y * (canvas.width * 4)) + (x * 4)) + 3];
if(alpha !== 0){
topLeft.update(x,y);
bottomRight.update(x,y);
}
}
}
const width = bottomRight.x - topLeft.x;
const height = bottomRight.y - topLeft.y;
const croppedCanvas = context.getImageData(topLeft.x,topLeft.y,width,height);
canvas.width = width;
canvas.height = height;
context.putImageData(croppedCanvas,0,0);
return canvas;
}
Here's code in ES syntax, short, fast and concise:
/**
* Trim a canvas.
*
* #author Arjan Haverkamp (arjan at avoid dot org)
* #param {canvas} canvas A canvas element to trim. This element will be trimmed (reference)
* #param {int} threshold Alpha threshold. Allows for trimming semi-opaque pixels too. Range: 0 - 255
* #returns {Object} Width and height of trimmed canvcas and left-top coordinate of trimmed area. Example: {width:400, height:300, x:65, y:104}
*/
const trimCanvas = (canvas, threshold = 0) => {
const ctx = canvas.getContext('2d'),
w = canvas.width, h = canvas.height,
imageData = ctx.getImageData(0, 0, w, h),
tlCorner = { x:w+1, y:h+1 },
brCorner = { x:-1, y:-1 };
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
if (imageData.data[((y * w + x) * 4) + 3] > threshold) {
tlCorner.x = Math.min(x, tlCorner.x);
tlCorner.y = Math.min(y, tlCorner.y);
brCorner.x = Math.max(x, brCorner.x);
brCorner.y = Math.max(y, brCorner.y);
}
}
}
const cut = ctx.getImageData(tlCorner.x, tlCorner.y, brCorner.x - tlCorner.x, brCorner.y - tlCorner.y);
canvas.width = brCorner.x - tlCorner.x;
canvas.height = brCorner.y - tlCorner.y;
ctx.putImageData(cut, 0, 0);
return {width:canvas.width, height:canvas.height, x:tlCorner.x, y:tlCorner.y};
}
The top voted answer here, as well as the implementations i found online trim one extra pixel which was very apparent when trying to trim text out of canvas. I wrote my own that worked better for me:
var img = new Image;
img.onload = () => {
var canvas = document.getElementById('canvas');
canvas.width = img.width;
canvas.height = img.height;
var ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0);
document.getElementById('button').addEventListener('click', ()=>{
autoCropCanvas(canvas, ctx);
document.getElementById('button').remove();
});
};
img.src = 'data:image/png;base64,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';
function autoCropCanvas(canvas, ctx) {
var bounds = {
left: 0,
right: canvas.width,
top: 0,
bottom: canvas.height
};
var rows = [];
var cols = [];
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
for (var x = 0; x < canvas.width; x++) {
cols[x] = cols[x] || false;
for (var y = 0; y < canvas.height; y++) {
rows[y] = rows[y] || false;
const p = y * (canvas.width * 4) + x * 4;
const [r, g, b, a] = [imageData.data[p], imageData.data[p + 1], imageData.data[p + 2], imageData.data[p + 3]];
var isEmptyPixel = Math.max(r, g, b, a) === 0;
if (!isEmptyPixel) {
cols[x] = true;
rows[y] = true;
}
}
}
for (var i = 0; i < rows.length; i++) {
if (rows[i]) {
bounds.top = i ? i - 1 : i;
break;
}
}
for (var i = rows.length; i--; ) {
if (rows[i]) {
bounds.bottom = i < canvas.height ? i + 1 : i;
break;
}
}
for (var i = 0; i < cols.length; i++) {
if (cols[i]) {
bounds.left = i ? i - 1 : i;
break;
}
}
for (var i = cols.length; i--; ) {
if (cols[i]) {
bounds.right = i < canvas.width ? i + 1 : i;
break;
}
}
var newWidth = bounds.right - bounds.left;
var newHeight = bounds.bottom - bounds.top;
var cut = ctx.getImageData(bounds.left, bounds.top, newWidth, newHeight);
canvas.width = newWidth;
canvas.height = newHeight;
ctx.putImageData(cut, 0, 0);
}
<canvas id=canvas style='border: 1px solid pink'></canvas>
<button id=button>crop canvas</button>

How do I zoom on a canvas?

I'm fairly new to Javascript and HTML5, and I'm trying to figure out how to zoom on a canvas. Let's say my Javascript code looks like this:
window.addEventListener('load', function() {
var theCanvas = document.getElementById('myCanvas');
theCanvas.style.border = "black 1px solid";
if(theCanvas && theCanvas.getContext) {
var context = theCanvas.getContext('2d');
if(context) {
var x = 10;
var y = 10;
var z = 255;
var color = "rgb(0," + z + ",0)";
context.fillStyle = "rgb(100,0,0)";
for(var y = 0; y <= 290; y += 10) {
for(var x = 0; x <= 290; x += 10) {
if(z >= 1) {
z -= 1;
}
color = "rgb(0," + z + ",0)";
if(x % 20 === 0) {
context.fillStyle = color;
} else {
context.fillStyle = color;
}
context.fillRect(x, y, 10, 10);
}
}
}
}
}, false);
In summary, this code just fills the canvas with tiled rectangles of changing color. But how would one go about zooming in and out on something like this?
You'll want the scale method of context.
And note that, once you've drawn something on the canvas, it can't really be zoomed or scaled — you've got to re-draw the entire canvas at the new "zoom level".

Categories