Draw Image Not Keeping Dimensions Javascript/jQuery - javascript

I'm creating something in Javascript/jQuery that will take an image, and reconstruct it as a multidimensional array. Due to the user being enabled to upload their own image, I need to get the image dimensions on the fly, and use them to scale the canvas accordingly, and draw the image.
Only issue is, the process of scaling the canvas, and then drawing to it is apparently throwing me off. What is happening now is that the canvas is scaling to the image size correctly, but what gets drawn to the canvas has its dimensions all whacked up.
Hopefully someone can give me some insight!
HTML
<div class="canvas">
<canvas id="myCanvas" style="border:1px solid #d3d3d3;">
Your browser does not support the HTML5 canvas tag.</canvas>
</div>
<div class="options">
<input type='file' id="imgInp" />
</div>
<div id="status">
<h1>Image to Multidimensional Array</h1>
<p>What this does it pretty much self explanatory, it turns any uploaded image into a multidimensional array.
<br/>The array is structured so that for every array inside the main array is a Y coord. Every bit of data inside of that Y array is an X coord.</p>
<h2>Output</h2>
<br>
<p>
<span id="statusout" onClick="this.select();"></span>
</p>
</div>
JAVASCRIPT
function loadCanvas(dataURL, width, height) {
$('#myCanvas').css({
width: width,
height: height
});
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
context.clearRect(0, 0, canvas.width, canvas.height);
// load image from data url
var imageObj = new Image();
imageObj.onload = function() {
context.drawImage(this, 0, 0);
var imgData = context.getImageData(0, 0, canvas.width, canvas.height);
var datalocation;
var datalocationhex;
var coord;
for (var y = 0; y < canvas.height; y++) {
$('#statusout').append("[");
for (var x = 0; x < canvas.width; x++) {
datalocation = context.getImageData(x, y, 1, 1).data;
datalocationhex = "#" + ("000000" + rgbToHex(datalocation[0], datalocation[1], datalocation[2])).slice(-6);
coord = "x=" + x + ", y=" + y;
if (x == 0) {
$('#statusout').append("'" + datalocationhex + "'");
} else {
$('#statusout').append(",'" + datalocationhex + "'");
}
}
if (y == canvas.height) {
$('#statusout').append("]<br>");
} else {
$('#statusout').append("],<br>");
}
}
};
function rgbToHex(r, g, b) {
if (r > 255 || g > 255 || b > 255)
throw "Invalid color component";
return ((r << 16) | (g << 8) | b).toString(16);
}
imageObj.src = dataURL;
}
function readURL(input) {
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function(e) {
var img = new Image();
img.src = e.target.result;
if (img.complete) { // was cached
var width = img.width;
var height = img.height;
loadCanvas(e.target.result, width, height);
} else { // wait for decoding
img.onload = function() {
var width = img.width;
var height = img.height;
loadCanvas(e.target.result, width, height);
}
}
}
reader.readAsDataURL(input.files[0]);
}
}
$("#imgInp").change(function() {
readURL(this);
});
So yeah, that's what I've got.
Thanks a lot for any help in advance!
EDIT
Here's a link to the project on CodePen to get a better view of things.
EDIT v2 It looks like the canvas is a little confused. The multidimensional array is getting spit out fine - well, kind of. It will go spits out the correct code for the image, and then a bunch of extra blank spots. So something is still going wrong with the canvas draw.

As it turns out, the Canvas element will only listen to width and height attributes, and not CSS rules when it comes to drawing the canvas.
This is what I changed:
$('#myCanvas').css({
width: width,
height: height
});
Turned into this:
$('#myCanvas').attr({
width: width,
height: height
});

Related

how to convert frames of GIF into spritesheet

I am trying to merge multiple frames from a GIF to convert them into spritesheet. I am somehow able to extract frames using libgif.js
here is my code. The Canvas in which i want to put all my images is empty and is at the end i dont know what is wrong with it.
<img hidden src="https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif" rel:auto_play="0"
rel:rubbable="0" />
<div id="frames">
</div>
<canvas id="myCanvas" style="border:1px solid #d3d3d3;">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://unpkg.com/jsgif#1.0.2/libgif.js"></script>
<script>
$(document).ready(function () {
$('img').each(function (idx, img_tag) {
var total = 0;
if (/^.+\.gif$/.test($(img_tag).prop("src"))) {
var rub = new SuperGif({ gif: img_tag, progressbar_height: 0 });
rub.load(function () {
for (var i = 0; i < rub.get_length(); i++) {
total += 1;
rub.move_to(i);
var canvas = cloneCanvas(rub.get_canvas());
$("#frames").append('<img id = "' + i + '"src= "' + canvas + '">');
}
for (var i = 0; i <= total; i++) {
id = i.toString();
var img = document.getElementById(id);
window.onload = function () {
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.drawImage(img, 10, 10);
}
}
});
}
});
});
function cloneCanvas(oldCanvas) {
var img = oldCanvas.toDataURL("image/png");
return img;
}
</script>
The main issue in your solution is that you are looping through all inserted frame-images and using window.onload with the expectation to draw images on the canvas. However, you are assigning windows.onload on the image elements after the images have already been attached to the dom in the previous (for i = 0) iteration, through $("#frames").append(.... This means that your onload handlers are getting registered after the event has already fired.
I rearranged your code into the snippet below to make sure that onload is registered before appending the image-frames into the document.
In this solution here I created the "spritesheet" by putting the image-frames vertically one-after-the-other. Tweak the solution accordingly to create your desired "spritesheet layout".
The tricky part is that you need to set a width and height for the canvas in a dynamic fashion, which means you have to wait for all image-frames to be loaded into the document and meassure their collective widths and heights.
The algorithm should be:
Collect all widths and heights of image-frames onload
Calculate the total canvas.width and canvas.height depending on
your desired layout (in my vertical-one-ofter-the-other solution
this means to use the maximal image width as canvas width and the
sum of all image heights as height. If you need a different
spritesheet layout then you need to adapt this logic differently)
Draw all the images on the canvas, which by this point has enough
width and height to fit all your gif-frame-images.
In my solution I did not implement this algorith, I rather hard-coded the canvas width and height after manually calculating the max-img-width and total-img-height for your particular gif example.
$(document).ready(function () {
const c = document.getElementById("myCanvas");
const ctx = c.getContext("2d");
let canvasWidth = 0;
let canvasHeight = 0;
$('img').each(function (idx, img_tag) {
var total = 0;
if (/^.+\.gif$/.test($(img_tag).prop("src"))) {
var rub = new SuperGif({ gif: img_tag, progressbar_height: 0 });
rub.load(function () {
for (let i = 0; i < rub.get_length(); i++) {
total += 1;
rub.move_to(i);
var canvas = cloneCanvas(rub.get_canvas());
const img = $('<img id = "' + i + '"src= "' + canvas + '">');
img.on('load', function() {
// draw the image
ctx.drawImage(this, 0, canvasHeight);
// extend canvas width, if newly loaded image exceeds current width
canvasWidth = $(this).width() > canvasWidth ? $(this).width() : canvasWidth;
// add canvas height by the newly loade image height value
canvasHeight += $(this).height();
// resize sprite-canvas to host the new image
canvas.width = canvasWidth;
canvas.height = canvasHeight;
});
$("#frames").append(img);
}
});
}
});
});
function cloneCanvas(oldCanvas) {
var img = oldCanvas.toDataURL("image/png");
return img;
}
Gif:
<img hidden src="https://upload.wikimedia.org/wikipedia/commons/2/2c/Rotating_earth_%28large%29.gif" rel:auto_play="0"
rel:rubbable="0" />
<hr />
Frames:
<div id="frames">
</div>
<hr />
Canvas:
<hr />
<canvas id="myCanvas" style="border:1px solid #d3d3d3;" width="400" height="17600">
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://unpkg.com/jsgif#1.0.2/libgif.js"></script>

Clearing previous positions of canvas object and not the entire canvas

l believe to have a logic error in the way of which my logic is meant to find the previous coordinates of my canvas object, a moving image, and delete the frame drawn before it so that it does not duplicated the image every time it is drawn onto the canvas.
There is a reproducible demo below and l added comments where l believe the problem occurs.
var canvas = document.getElementById("c");
var ctx = canvas.getContext("2d");
var the_background = document.getElementById("the_background");
var imgTag = new Image();
var X_POS = canvas.width;
var Y_POS = 0;
imgTag.src = "http://i.stack.imgur.com/Rk0DW.png"; // load image
function animate() {
/* Error resides from here */
var coords = {};
coords.x = Math.floor(this.X_POS - imgTag);
coords.y = Math.floor(this.Y_POS - imgTag);
ctx.clearRect(coords.x, coords.y, X_POS, Y_POS);
/* To here */
ctx.drawImage(imgTag, X_POS, Y_POS);
X_POS -= 5;
if (X_POS > 200) requestAnimationFrame(animate)
}
window.onload = function() {
ctx.drawImage(the_background, 0, 0, canvas.width, canvas.height);
animate();
}
html,
body {
width: 100%;
height: 100%;
margin: 0px;
border: 0;
overflow: hidden;
display: block;
}
<html>
<canvas id="c" width="600" height="400"></canvas>
<img style="display: none;" id="the_button" src="https://i.imgur.com/wO7Wc2w.png" />
<img style="display: none;" id="the_background" src="https://img.freepik.com/free-photo/hand-painted-watercolor-background-with-sky-clouds-shape_24972-1095.jpg?size=626&ext=jpg" />
</html>
It seems logical to only clear the subset of the canvas that's changing, but the normal approach is to clear and redraw the entire canvas per frame. After the car moves, the background that's cleared underneath it needs to be filled in, and trying to redraw only that subset will lead to visual artifacts and general fussiness. Clearing small portions of the screen is a premature optimization.
Canvases can't keep track of much of anything other than pixels, so an animation is more like a flipbook of stationary frames and less like a cardboard cutout animation, where the same pieces of cardboard move along and overlap one another.
Math.floor(this.X_POS - imgTag) looks wrong -- imgTag is an image object, which doesn't make sense to subtract from a number. You may have meant to grab the x property from this.
Use image.onload to ensure the image is actually loaded before running the loop. It's a bit odd to use image tags in the DOM just to load images for a canvas. I'd do that programmatically from JS which saves some typing and makes it easier to manage the data.
const loadImg = url => new Promise((resolve, reject) => {
const img = new Image();
img.onerror = reject;
img.onload = () => resolve(img);
img.src = url;
});
const images = [
"https://img.freepik.com/free-photo/hand-painted-watercolor-background-with-sky-clouds-shape_24972-1095.jpg?size=626&ext=jpg",
"http://i.stack.imgur.com/Rk0DW.png",
];
Promise
.all(images.map(loadImg))
.then(([backgroundImg, carImg]) => {
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const car = {x: canvas.width, y: 0};
(function update() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(
backgroundImg, 0, 0, canvas.width, canvas.height
);
ctx.drawImage(
carImg, car.x, car.y, carImg.width, carImg.height
);
car.x -= 5;
if (car.x > 200) {
requestAnimationFrame(update);
}
})();
})
.catch(err => console.error(err))
;
<canvas id="canvas" width="600" height="400"></canvas>

How can I render tens of thousands of elements?

Before you go off saying I'm crazy, believe me, I know. I'm not going for a website that renders fast, loads fast, or gets a high lighthouse score. I just want it to work.
I have some javascript that picks up all the pixel colors of an image. With this function, I create a div element that is 1px by 1px and set the background color to the pixel color of those same coordinates. Then the coordinates are used to set the top and left values. My code does what it's told.
Here's my problem, my image is 700px by 387px. If you do the math, that works out to 270,900 html elements. Chrome, simply isn't built for this madness. I want to see this work, I want to "manually" create an image with div elements, somehow. My cpu maxes out when I try to do so, and I'm sure I'd run out of ram eventually.
Everything works fine if I only try hundreds or a few thousand pixels, but any more, and chrome dies. I'm not sure if it's calculating in the browser that may be my problem, or if chrome cant display this many elements, or both. I suppose I could do the same math on my server with python, and append it to html, but then chrome probably couldn't display it.
Obviously, this isn't super important, just fun. I think the community will enjoy the challenge as well.
Here's calculating 100 pixels:
onload = e => {
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
function rgbToHex(r, g, b) {
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}
function img(x, y) {
var img = document.getElementById('my-image');
var canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
var pixelData = canvas.getContext('2d').getImageData(x, y, 1, 1).data;
return rgbToHex(pixelData[0], pixelData[1], pixelData[2]);
}
//x = 700 y = 387
for (var x = 0; x < 10; x++) {
for (var y = 0; y < 10; y++) {
document.body.insertAdjacentHTML("beforeend", "<div style='top:" + y + "px; left:" + x + "px;background:" + img(x, y) + ";' />");
}
}
};
div {
position: absolute;
width: 1px;
height: 1px;
}
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/New_born_Frisian_red_white_calf.jpg/640px-New_born_Frisian_red_white_calf.jpg" id="my-image" crossorigin="anonymous">
here's calculating 2500px (still works, takes a while)
onload = e => {
function componentToHex(c) {
var hex = c.toString(16);
return hex.length == 1 ? "0" + hex : hex;
}
function rgbToHex(r, g, b) {
return "#" + componentToHex(r) + componentToHex(g) + componentToHex(b);
}
function img(x, y) {
var img = document.getElementById('my-image');
var canvas = document.createElement('canvas');
canvas.width = img.width;
canvas.height = img.height;
canvas.getContext('2d').drawImage(img, 0, 0, img.width, img.height);
var pixelData = canvas.getContext('2d').getImageData(x, y, 1, 1).data;
return rgbToHex(pixelData[0], pixelData[1], pixelData[2]);
}
//x = 700 y = 387
for (var x = 0; x < 50; x++) {
for (var y = 0; y < 50; y++) {
document.body.insertAdjacentHTML("beforeend", "<div style='top:" + y + "px; left:" + x + "px;background:" + img(x, y) + ";' />");
}
}
};
div {
position: absolute;
width: 1px;
height: 1px;
}
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/f/fb/New_born_Frisian_red_white_calf.jpg/640px-New_born_Frisian_red_white_calf.jpg" id="my-image" crossorigin="anonymous">
Cheers, Isaac.
At the moment, you are doing the following for every pixel
Create canvas
get the context and draw to the image
get the context and get the pixel data for one pixel
create a DIV
add it to the DOM
Now, lets streamline this
The following can be done ONCE
create the canvas
get the context
use the context to draw the image
use the context to get the image data
create an empty string
Now, for each pixel
get the pixel data
add the html for the div to the string
And finally, just ONCE
Add the string with all the divs to the DOM
Something like:
const componentToHex = c => c.toString(16).padStart(2, '0');
const rgbToHex = (r, g, b) => `#${componentToHex(r)}${componentToHex(g)}${componentToHex(b)}`;
const img = document.getElementById('my-image');
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
const w = canvas.width = img.width;
const h = canvas.height = img.height;
context.drawImage(img, 0, 0, img.width, img.height);
const imageData = context.getImageData(0, 0, w, h).data;
const pixel = (x, y) => {
const index = (w * y + x) * 4;
return rgbToHex(imageData[index], imageData[index + 1], imageData[index + 2]);
}
const df = document.createDocumentFragment();
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
const div = df.appendChild(document.createElement('div'));
div.style.top = y + "px";
div.style.left = x + "px";
div.style.backgroundColor = pixel(x, y);
}
}
document.body.appendChild(df);
Note: it may not be the case now, but such a loop may work faster inside a function - often loops in the global context are slower
So, you could wrap the whole code above in
(() => {
// the code from above
})();
And see significant improvement again - not sure, used to be the case in years gone by
changed to use document fragment for a further 25% speed improvement
Now takes 1.4 seconds in firefox for a 640x480 image, 2.3 seconds in chrome - which didn't really see a big difference between using insertAdjacentHTML vs a document fragment
another thing to note. In Firefox the page becomes sluggish, in chrome, for 640x480, no such issue

How do I make the canvas display the recoloured image instead of the same image?

I had an image of Canada and manually recolored its pixels to shades of blue. I then tried to make a script that would do the same(into red) without me going through all the hassle.
But for some reason, the canvas simply repaints the same image of the blue Canada in Mozilla and Chrome(sometimes not even draws it).
Mozilla says accessing ImageData is a security error. What did I get wrong and how do I fix it?
Snippet right here:
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
var imgPath = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d8/Copper_sulfate.jpg/234px-Copper_sulfate.jpg";
var imgObj = new Image();
imgObj.crossOrigin = "anonymous";
imgObj.src = imgPath;
imgObj.onload = function() {
context.drawImage(imgObj, 0, 0);
map = context.getImageData(0, 0, 234, 240);
imagedata = map.data;
for (p = 0; p < imagedata.length; p += 4) {
imagedata[p] = imagedata[p + 2];
imagedata[p + 2] = 0;
imagedata[p + 3] = 255;
};
context.putImageData(map, 0, 0);
};
<img id="Canada" src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/d8/Copper_sulfate.jpg/234px-Copper_sulfate.jpg" />
<canvas id='canvas' width='234px' height='240px'></canvas>

Dynamically changing Text color depending on Background Image

right now I am writing a small chrome extension. the extension grabs the Bing Image of the Day and sets it as the body background image, and then the extension also grabs the weather, time, date, etc. The problem is that sometimes the image of the day is for example, bright on one side and dark on the other. So, no matter what color the text is, some of it is illegible. How can I determine what color each text should be depending on the background image color behind a SPECIFIC text?
Thanks in advance.
You can use jQuery's attr() function. For example, if you img tag has an id attribute of 'my_image':
<img id="my_image" src="first.jpg"/>
Jquery
$("#my_image").attr("src","second.jpg");
I got something to work.... It is kind of makeshift and not foolproof, but I found a script that calculates overall brightness, which then allows the script to decide whether it should display black or white text. Seems to work well, as of now. Here it is:
function getImageBrightness(imageSrc) {
var img = document.createElement("img");
img.src = imageSrc;
img.style.display = "none";
document.body.appendChild(img);
var colorSum = 0;
img.onload = function () {
// create canvas
var canvas = document.createElement("canvas");
canvas.width = this.width;
canvas.height = this.height;
var ctx = canvas.getContext("2d");
ctx.drawImage(this, 0, 0);
var imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
var data = imageData.data;
var r, g, b, avg;
for (var x = 0, len = data.length; x < len; x += 4) {
r = data[x];
g = data[x + 1];
b = data[x + 2];
avg = Math.floor((r + g + b) / 3);
colorSum += avg;
}
brightness = Math.floor(colorSum / (this.width * this.height));
console.log(brightness);
if (brightness < 150) {
$(".contrast").css("color", "white");
} else {
$(".contrast").css("color", "black");
}
}
}
credit to: https://jsfiddle.net/s7Wx2/

Categories

Resources