emulate div behavior in canvas - javascript

I have a dom editor which a user can insert textbox and images. One of my requirements involve saving a snapshot of what is in the editor into an image. I did some research and there are some solutions, but they don't seem 100% foolproof. I've tried implementing a solution myself, clobbering code here and there:
function measureText(text, size, font) {
var lDiv = document.createElement('lDiv');
document.body.appendChild(lDiv);
lDiv.style.fontSize = size;
lDiv.style.fontFamily = font;
lDiv.style.position = "absolute";
lDiv.style.left = -1000;
lDiv.style.top = -1000;
lDiv.innerHTML = text;
var metrics = font.measureText(text, size.slice(0, size.length - 2));
var lResult = {
width: lDiv.clientWidth,
height: metrics.height + lDiv.clientHeight
};
document.body.removeChild(lDiv);
lDiv = null;
return lResult;
}
function wrapText(context, item) {
var words = item.text.split(' ');
var line = '';
var x = parseInt(item.x);
var y = parseInt(item.y);
var width = parseInt(item.width.slice(0, item.width.length - 2));
var height = parseInt(item.height.slice(0, item.height.length - 2));
var fontsize = parseInt(item.size.slice(0, item.size.length - 2));
var font = new Font();
font.onload = function () {
context.save();
context.beginPath();
context.rect(x, y, width, height);
context.clip();
context.font = item.size + " " + item.font;
context.textBaseline = "top";
for (var n = 0; n < words.length; n++) {
var testLine = line + words[n] + ' ';
var metrics = measureText(testLine, item.size, font);
var testWidth = metrics.width;
if (testWidth > width && n > 0) {
console.log("Drawing '" + line + "' to " + x + " " + y);
context.fillText(line, x, y);
line = words[n] + ' ';
y += metrics.height
}
else {
line = testLine;
}
}
context.fillText(line, x, y);
context.restore();
}
font.fontFamily = item.font;
font.src = font.fontFamily;
}
this.toImage = function () {
console.log("testing");
var canvas = document.getElementById("testcanvas");
canvas.width = 400;
canvas.height = 400;
var ctx = canvas.getContext("2d");
var imageObj = new Image();
var thisService = this;
imageObj.onload = function () {
ctx.drawImage(imageObj, 0, 0, 400, 400);
for (var i = 0; i < thisService.canvasItems.length; i++) {
var component = thisService.canvasItems[i];
if (component.type == "textbox") {
var x = component.x.slice(0, component.x.length - 2);
var y = component.y.slice(0, component.y.length - 2);
var w = component.width.slice(0, component.width.length - 2);
var h = component.height.slice(0, component.height.length - 2);
wrapText(ctx, component);
}
}
};
imageObj.src = this.base.front_image;
}
Somehow I believe I almost made it, however from the ,
There seems to be some positioning/font placement issues, just a few pixels lower. The top 1 a div with no padding, (its model can be seem on the panel on the left), while the bottom one is the canvas.
I wish to have a 1 to 1 accurate mapping here, can anyone enlighten what might be the problem?

As far as I know its not possible to draw HTML into a canvas with 100% accuracy due to the obvious "security" reasons.
You can still get pretty close using the rasterizeHTML.js library.
It uses a SVG image containing the content you want to render. To draw HTML content, you'd use a element containing the HTML, then draw that SVG image into your canvas.

Related

Set background image to canvas and export it with text+image

I am trying to solving one problem , I have a textarea which works fine and exporting text to canvas , but I don't know how to achieve the same effect and add background image option to this, it could be a static background image from the beginning, here is script .When i was trying to set background image via html , i had background image behind the text , but when i was exporting i had only text in my png.
var canvas = $('#canvas');
var ctx = canvas.get(0).getContext('2d');
canvas.width = 600;
canvas.height = 400;
$('#submit').click(function (e) {
e.preventDefault();
var text = $('#text').val(),
fontSize = parseInt($('#font-size').val()),
width = parseInt($('#width').val()),
lines = [],
line = '',
lineTest = '',
words = text.split(' '),
currentY = 0;
ctx.font = fontSize + 'px Arial';
for (var i = 0, len = words.length; i < len; i++) {
lineTest = line + words[i] + ' ';
// Check total width of line or last word
if (ctx.measureText(lineTest).width > width) {
// Calculate the new height
currentY = lines.length * fontSize + fontSize;
// Record and reset the current line
lines.push({ text: line, height: currentY });
line = words[i] + ' ';
} else {
line = lineTest;
}
}
// Catch last line in-case something is left over
if (line.length > 0) {
currentY = lines.length * fontSize + fontSize;
lines.push({ text: line.trim(), height: currentY });
}
// Visually output text
ctx.clearRect(0, 0, 500, 500);
for (var i = 0, len = lines.length; i < len; i++) {
ctx.fillText(lines[i].text, 0, lines[i].height);
}
var canvasCtx = document.getElementById("canvas").getContext('2d');
var img = document.getElementById("yourimg");
canvasCtx.drawImage(img,x,y);
var img = new Image();
img.onload = function(){
canvasCtx.drawImage(img,x,y);
};
img.src = "https://i.stack.imgur.com/7Whkw.png";
});
i have solved it!
var canvas = $('#canvas');
var ctx = canvas.get(0).getContext('2d');
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var img = new Image();
var height = 600;
var width = 600;
img.onload = function() {
context.save();
context.rect(0, 0, 200, 200);//Здесь первый 0=X
context.drawImage(img,0, 0,width,height);//Первый 0=X
context.restore();
};
img.src = 'https://i.stack.imgur.com/5uvJN.png';
$('#submit').click(function (e) {
e.preventDefault();
var text = $('#text').val(),
fontSize = parseInt($('#font-size').val()),
width = parseInt($('#width').val()),
lines = [],
line = '',
lineTest = '',
words = text.split(' '),
currentY = 0;
ctx.font = fontSize + 'px Arial';
for (var i = 0, len = words.length; i < len; i++) {
lineTest = line + words[i] + ' ';
// Check total width of line or last word
if (ctx.measureText(lineTest).width > width) {
// Calculate the new height
currentY = lines.length * fontSize + fontSize;
// Record and reset the current line
lines.push({ text: line, height: currentY });
line = words[i] + ' ';
} else {
line = lineTest;
}
}
// Catch last line in-case something is left over
if (line.length > 0) {
currentY = lines.length * fontSize + fontSize;
lines.push({ text: line.trim(), height: currentY });
}
// Visually output text
ctx.clearRect(0, 500, 500, 500);
for (var i = 0, len = lines.length; i < len; i++) {
ctx.fillText(lines[i].text, 410, lines[i].height);
}
});
one more question how i can set padding top for textarea text on canvas?

How do i get image data from external url - and return colors

Im using clusterfck.js to analyze and return colors. I wan't to grap images from search-results, but it seems to be a problem with the way clusterfck does the data reading. Here's the code from clusterfck:
$(".kmeans-button").click(function() {
if (!colors) {
colors = getImageColors();
}
var k = $(this).attr("data-k"); // $.data() returned undefined
$("#clusters").empty().append("<div>calculating distances...</div>");
kmeans.clusterColors(colors, k); // Threaded analyzing (worker)
})
function getImageColors() {
var img = $("#test-image");
var width = img.width();
var height = img.height();
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var context = canvas.getContext("2d");
context.drawImage(img.get(0), 0, 0);
var data = context.getImageData(0, 0, width, height).data;
var colors = [];
for(var x = 0; x < width-12; x += 3) {
for(var y = 0; y < height-12; y += 3) { // sample image, every 3rd row and column
var offs = x*4 + y*4*width;
var color = [data[offs + 0], data[offs + 1], data[offs + 2]];
colors.push(color);
}
}
return colors;
}
I tried to put in img.crossOrigin = "Anonymous"; with no luck. I guess it needs to be loaded in af function and somehow callback colors. This is the best i could come up with, but it's not working.
$(".kmeans-button").click(function() {
if (!colors) {
getImageColors2(function(colors2) {
colors=colors2.slice(0);
var k = $(this).attr("data-k-e"); // $.data() returned undefined
$("#clusters").empty().append("<div>calculating colors...</div>");
kmeans.clusterColors(colors, k);
})
}
})
function getImageColors2(callback) {
var img2= document.getElementById("test-image");
var img = new Image();
img.onload = function () {
var width = img.width();
var height = img.height();
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var context = canvas.getContext("2d");
context.drawImage(img.get(0), 0, 0);
var data = context.getImageData(0, 0, width, height).data;
var colors = [];
for(var x = 0; x < width-12; x += 3) {
for(var y = 0; y < height-12; y += 3) { // sample image, every 3rd row and column
var offs = x*4 + y*4*width;
var color = [data[offs + 0], data[offs + 1], data[offs + 2]];
colors.push(color);
}
}
callback(colors);
}
img.src=img2.src;
}
I assume that it can't access the data without loading into a image when on a external server. What am i missing her ? Maybe i got it all wrong ?
Thanks for your comments! I got it working now with the callback function :-)
function getpalette(colors) {
$("#clusters").empty().append("<div>calculating colors...</div>");
kmeans_paint.clusterColors(colors, k);
}
$(".kmeans-error-button").click(function() {
k = $(this).attr("data-k-e");
if (!colors) {
getImageColors(getpalette);
}
})
function getImageColors(callback) {
var img = new Image();
img.setAttribute('crossOrigin', 'anonymous');
img.onload = function () {
var width = img.width;
var height = img.height;
var canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
var context = canvas.getContext("2d");
context.drawImage(img, 0, 0);
var data = context.getImageData(0, 0, width, height).data;
var colors = [];
for(var x = 0; x < width-12; x += 3) {
for(var y = 0; y < height-12; y += 3) { // sample image, every 3rd row and column
var offs = x*4 + y*4*width;
var color = [data[offs + 0], data[offs + 1], data[offs + 2]];
colors.push(color);
}
}
callback(colors);
}
img.src=document.getElementById("test-image").src;
}

Resizing Image Without Affecting Look of Original Canvas?

I have a canvas where a user draws. After tapping a button, I do a few things in a second canvas such as trimming away the white space and re-centering the drawing (so as to not affect the original canvas).
I also create a third canvas so I can resize the output to a certain size. My problem is that I don't want the original canvas where users draw to be affected. Right now everything works and my image is resized, but so is the original canvas. How to I leave the original canvas unaffected?
Here's my function:
//Get Canvas
c = document.getElementById('simple_sketch');
//Define Context
var ctx = c.getContext('2d');
//Create Copy of Canvas
var copyOfContext = document.createElement('canvas').getContext('2d');
//Get Pixels
var pixels = ctx.getImageData(0, 0, c.width, c.height);
//Get Length of Pixels
var lengthOfPixels = pixels.data.length;
//Define Placeholder Variables
var i;
var x;
var y;
var bound = {
top: null,
left: null,
right: null,
bottom: null
};
//Loop Through Pixels
for (i = 0; i < lengthOfPixels; i += 4) {
if (pixels.data[i+3] !== 0) {
x = (i / 4) % c.width;
y = ~~((i / 4) / c.width);
if (bound.top === null) {
bound.top = y;
}
if (bound.left === null) {
bound.left = x;
} else if (x < bound.left) {
bound.left = x;
}
if (bound.right === null) {
bound.right = x;
} else if (bound.right < x) {
bound.right = x;
}
if (bound.bottom === null) {
bound.bottom = y;
} else if (bound.bottom < y) {
bound.bottom = y;
}
}
}
//Calculate Trimmed Dimensions
var padding = 1;
var trimmedHeight = bound.bottom + padding - bound.top;
var trimmedWidth = bound.right + padding - bound.left;
//Get Longest Dimension (We Need a Square Image That Fits the Drawing)
var longestDimension = Math.max(trimmedHeight, trimmedWidth);
//Define Rect
var trimmedRect = ctx.getImageData(bound.left, bound.top, trimmedWidth, trimmedHeight);
//Define New Canvas Parameters
copyOfContext.canvas.width = longestDimension;
copyOfContext.canvas.height = longestDimension;
copyOfContext.putImageData(trimmedRect, (longestDimension - trimmedWidth)/2, (longestDimension - trimmedHeight)/2);
copyOfContext.globalCompositeOperation = "source-out";
copyOfContext.fillStyle = "#fff";
copyOfContext.fillRect(0, 0, longestDimension, longestDimension);
//Define Resized Context
var resizedContext = c.getContext('2d');
resizedContext.canvas.width = 32;
resizedContext.canvas.height = 32;
resizedContext.drawImage(copyOfContext.canvas, 0, 0, 32, 32);
//Get Cropped Image URL
var croppedImageURL = resizedContext.canvas.toDataURL("image/jpeg");
//Open Image in New Window
window.open(croppedImageURL, '_blank');
How to make a "spare" copy of an html5 canvas:
var theCopy=copyCanvas(originalCanvas);
function copyCanvas(originalCanvas){
var c=originalCanvas.cloneNode();
c.getContext('2d').drawImage(originalCanvas,0,0);
return(c);
}
Make a spare copy of the original canvas you don't want affected. Then after you've altered the original, but want the original contents back ...
// optionally clear the canvas before restoring the original content
originalCanvasContext.drawImage(theCopy,0,0);

Darken Image Slider/Button -- Javascript

I have a webpage in which users upload an image of some hand written work. Sometimes it's scanned pencil which can be very difficult to read.
Is it possible to possible to have a slider/button that I could use to darken or maybe even sharpen a particular image? I would need a slider/button per image as the page I view contains several user uploaded images.
Thanks.
Yes, there are two ways, one is css filters (see posit labs answer), the other one is with canvas, here is a nice tutorial for that, and here is my demo.
For the demo, you would have to use an image in your own domain (otherwise the canvas becomes tainted and you can't access the pixels), that's why you see the Data URI src in the image, is the only way to make the image origin from the fiddle.
HTML
<img id="myImage" src="mydomain/img.png">
<button class="filter-btn" data-filter="darken" data-img="#myImage">Darken</button>
<button class="filter-btn" data-filter="sharpen" data-img="#myImage">Sharpen</button>
If you copy and paste the JavaScript, the only thing you have to do is use this markup for it to work, the image can be configured however you want, the buttons are the important part.
Each button has a filter-btn class, to indicate that it's intended to apply a filter, then, you specify the filter via the data-filter attribute (in this case it can be sharpen or darken), and finally you link the button to the image via the data-img attribute, where you can specify any css selector to get to the image.
JavaScript
Remember, you don't have to touch any of these if you follow the HTML markup, but if you have any questions about the code, shoot!
ImageFilter = {}
ImageFilter.init = function () {
var buttons = document.querySelectorAll(".filter-btn");
for (var i = 0; i < buttons.length; i++) {
var btn = buttons[i];
var filter = btn.dataset.filter;
var img = btn.dataset.img;
img.crossOrigin = "Anonymous";
(function (filter, img) {
btn.addEventListener("click", function () {
ImageFilter.doFilter(filter, img);
});
})(filter, img);
}
}
window.addEventListener("load", ImageFilter.init);
ImageFilter.getImage = function (selector) {
return document.querySelector(selector);
}
ImageFilter.createData = function (canvas, w, h) {
var context = canvas.getContext("2d");
return context.createImageData(w, h);
}
ImageFilter.doFilter = function (type, image) {
var image = ImageFilter.getImage(image);
switch (type) {
case "darken":
var adjustment = -5;
var canvas = ImageFilter.newCanvas(image);
var data = ImageFilter.getData(canvas);
var actualData = data.data;
for (var i = 0; i < actualData.length; i++) {
actualData[i] += adjustment;
actualData[i + 1] += adjustment;
actualData[i + 2] += adjustment;
}
ImageFilter.putData(data, canvas);
var newImg = image.cloneNode(true);
newImg.src = ImageFilter.getSource(canvas);
newImg.id = image.id;
replaceNode(image, newImg);
break;
case "sharpen":
var weights = [0, -1, 0, -1, 5, -1,
0, -1, 0];
var canvas = ImageFilter.newCanvas(image);
var data = ImageFilter.getData(canvas);
var side = Math.round(Math.sqrt(weights.length));
var halfSide = Math.floor(side / 2);
var src = data.data;
var sw = data.width;
var sh = data.height;
var w = sw;
var h = sh;
var output = ImageFilter.createData(canvas, w, h);
var dst = output.data;
var alphaFac = 1;
for (var y = 0; y < h; y++) {
for (var x = 0; x < w; x++) {
var sy = y;
var sx = x;
var dstOff = (y * w + x) * 4;
var r = 0,
g = 0,
b = 0,
a = 0;
for (var cy = 0; cy < side; cy++) {
for (var cx = 0; cx < side; cx++) {
var scy = sy + cy - halfSide;
var scx = sx + cx - halfSide;
if (scy >= 0 && scy < sh && scx >= 0 && scx < sw) {
var srcOff = (scy * sw + scx) * 4;
var wt = weights[cy * side + cx];
r += src[srcOff] * wt;
g += src[srcOff + 1] * wt;
b += src[srcOff + 2] * wt;
a += src[srcOff + 3] * wt;
}
}
}
dst[dstOff] = r;
dst[dstOff + 1] = g;
dst[dstOff + 2] = b;
dst[dstOff + 3] = a + alphaFac * (255 - a);
}
}
ImageFilter.putData(output, canvas);
var newImg = image.cloneNode(true);
newImg.src = ImageFilter.getSource(canvas);
replaceNode(image, newImg);
break;
}
}
ImageFilter.newCanvas = function (image) {
var canvas = document.createElement("canvas");
canvas.width = image.width;
canvas.height = image.height;
var context = canvas.getContext("2d");
context.drawImage(image, 0, 0, image.width, image.height);
return canvas;
}
ImageFilter.getData = function (canvas) {
var context = canvas.getContext("2d");
return context.getImageData(0, 0, canvas.width, canvas.height);
}
ImageFilter.putData = function (data, canvas) {
var context = canvas.getContext("2d");
context.putImageData(data, 0, 0);
}
ImageFilter.getSource = function (canvas) {
return canvas.toDataURL();
}
function replaceNode(node1, node2) {
var parent = node1.parentNode;
var next = node1.nextSibling;
if (next) parent.insertBefore(node2, next);
else parent.appendChild(node2);
parent.removeChild(node1);
}
That's it, see the demo, hope it helps!
Updates
Firefox fix: creating a new image and replacing the old one each time seems to fix the firefox bug where it doesn't update the image's src. (29/01/15 2:07a.m)
Short answer: yes.
The easiest way to do this would be with CSS Filters, but they aren't supported on old browsers (support table). The example below applies a 200% contrast filter.
filter: contrast(2);
Another option would be to use HTML Canvas to draw the images and manually manipulate the pixels. It's not very fast, and it's much more complicated than CSS Filters. I won't go into depth, but here is an article about filtering images with canvas.
In my opinion, the users should be responsible for uploading quality images. It seems silly to correct their mistake by adding extra controls to your site.

Why are my event listeners only working for the last element?

The problem is that when I drag/drop a picture, it always gets put on the last letter.
Here's an image of what I am talking about:
Here is my code:
function mattes_draw_letter(x, y, width, height, letter, position)
{
var canvas = document.createElement('canvas');
canvas.style.position = "absolute";
canvas.style.top = 0 + "px";
canvas.id = "canvas_opening_" + position;
canvas.style.zIndex = 5;
var ctx = canvas.getContext("2d");
ctx.lineWidth = 1;
ctx.fillStyle = '#bfbfbf';
ctx.strokeStyle = '#000000';
ctx.beginPath();
ctx.moveTo(x + letter[0] * width, y + letter[1] * height);
for (i = 0; i < letter.length; i+=2)
{
if (typeof letter[i+3] !== 'undefined')
{
ctx.lineTo(x + letter[i+2] * width, y + letter[i+3] * height);
}
}
ctx.fill();
ctx.stroke();
ctx.closePath();
$("#mattes").append(canvas);
canvas.addEventListener("drop", function(event) {drop(event, this);}, false);
canvas.addEventListener("dragover", function(event) {allowDrop(event);}, false);
canvas.addEventListener("click", function() {photos_add_selected_fid(this);}, false);
}
Here is the code that has the for loop:
function mattes_create_openings(openings)
{
var opening_array = new Array();
var x = 0;
var y = 0;
var opening_width = 0;
var opening_height = 0;
$(openings).each(function(i, el) {
x = $(el).find("x").text() * ppi;
y = $(el).find("y").text() * ppi;
opening_width = $(el).find("width").text() * ppi;
opening_height = $(el).find("height").text() * ppi;
var type = $(el).find("type").text();
var text = $(el).find("text").text();
var ellipse = (type == "ellipse") ? " border-radius: 50% " : "";
if ("letter" == type)
{
var letter = mattes_get_letter_array(text);
var letter_width = (opening_width - (text.length * 0.5 * ppi)) / text.length;
var space = 0.5 * ppi;
var letterX = x;
var letterY = y;
var letter_height = opening_height;
mattes_draw_letter(letterX, letterY, letter_width, letter_height, letter, i);
letterX += (space + letter_width);
}
else
{
$("#mattes").append("<div id='opening_" + i + "' style='background-color: #bfbfbf; position: absolute; left: " + x + "px; top: " + y + "px; height: " + opening_height + "px; width: " + opening_width + "px; overflow: hidden; z-index: 4;" + ellipse + "' ondrop='drop(event, this)' ondragover='allowDrop(event)' onclick='photos_add_selected_fid(this);'> </div>");
}
//TODO multiple openings max/min -x,y,width, height
opening_array["x"] = x;
opening_array["y"] = y;
opening_array["opening_width"] = opening_width;
opening_array["opening_height"] = opening_height;
});
return opening_array;
}
You need to wrap your code inside $.each in a function call, like this:
$(openings).each(function(i, el) {
bindEvents(i, el);
};
function bindEvents(indx, ele) {
// Code what you have written inside $.each originally
};
The problem was not the event handlers, but the size of the canvas. The last canvas created was on top of the others so it was the only event that was being activated.

Categories

Resources