JavaScript Font Metrics - javascript

Given (1) a font-family and (2) a unicode character code.
Is it possible to, within JavaScript, produce an image that looks like:
http://www.freetype.org/freetype2/docs/tutorial/metrics.png
Basically, I want to:
display the character itself (enlarged)
get the various font metrics
draw a bunch of light grey lines
Now, drawing the light grey lines is simple -- I just use SVG. However, how do I extract the font-metrics of the character?

Based on the library mentioned above I made this codepen
http://codepen.io/sebilasse/pen/gPBQqm?editors=1010
[edit: capHeight is now based on the letter H as suggested by #sebdesign below]
HTML
<h4>
Change font name
<input value="Maven Pro"></input>
<small>[local or google]</small>
and font size
<input value="40px" size=8></input>
and
<button onclick="getMetrics()">
<strong>get metrics</strong>
</button>
</h4>
<div id="illustrationContainer"></div>
<pre id="log"></pre>
<canvas id="cvs" width="220" height="200"></canvas>
JS
(getMetrics());
function getMetrics() {
var testtext = "Sixty Handgloves ABC";
// if there is no getComputedStyle, this library won't work.
if(!document.defaultView.getComputedStyle) {
throw("ERROR: 'document.defaultView.getComputedStyle' not found. This library only works in browsers that can report computed CSS values.");
}
// store the old text metrics function on the Canvas2D prototype
CanvasRenderingContext2D.prototype.measureTextWidth = CanvasRenderingContext2D.prototype.measureText;
/**
* shortcut function for getting computed CSS values
*/
var getCSSValue = function(element, property) {
return document.defaultView.getComputedStyle(element,null).getPropertyValue(property);
};
// debug function
var show = function(canvas, ctx, xstart, w, h, metrics)
{
document.body.appendChild(canvas);
ctx.strokeStyle = 'rgba(0, 0, 0, 0.5)';
ctx.beginPath();
ctx.moveTo(xstart,0);
ctx.lineTo(xstart,h);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(xstart+metrics.bounds.maxx,0);
ctx.lineTo(xstart+metrics.bounds.maxx,h);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0,h/2-metrics.ascent);
ctx.lineTo(w,h/2-metrics.ascent);
ctx.closePath();
ctx.stroke();
ctx.beginPath();
ctx.moveTo(0,h/2+metrics.descent);
ctx.lineTo(w,h/2+metrics.descent);
ctx.closePath();
ctx.stroke();
}
/**
* The new text metrics function
*/
CanvasRenderingContext2D.prototype.measureText = function(textstring) {
var metrics = this.measureTextWidth(textstring),
fontFamily = getCSSValue(this.canvas,"font-family"),
fontSize = getCSSValue(this.canvas,"font-size").replace("px",""),
isSpace = !(/\S/.test(textstring));
metrics.fontsize = fontSize;
// for text lead values, we meaure a multiline text container.
var leadDiv = document.createElement("div");
leadDiv.style.position = "absolute";
leadDiv.style.opacity = 0;
leadDiv.style.font = fontSize + "px " + fontFamily;
leadDiv.innerHTML = textstring + "<br/>" + textstring;
document.body.appendChild(leadDiv);
// make some initial guess at the text leading (using the standard TeX ratio)
metrics.leading = 1.2 * fontSize;
// then we try to get the real value from the browser
var leadDivHeight = getCSSValue(leadDiv,"height");
leadDivHeight = leadDivHeight.replace("px","");
if (leadDivHeight >= fontSize * 2) { metrics.leading = (leadDivHeight/2) | 0; }
document.body.removeChild(leadDiv);
// if we're not dealing with white space, we can compute metrics
if (!isSpace) {
// Have characters, so measure the text
var canvas = document.createElement("canvas");
var padding = 100;
canvas.width = metrics.width + padding;
canvas.height = 3*fontSize;
canvas.style.opacity = 1;
canvas.style.fontFamily = fontFamily;
canvas.style.fontSize = fontSize;
var ctx = canvas.getContext("2d");
ctx.font = fontSize + "px " + fontFamily;
var w = canvas.width,
h = canvas.height,
baseline = h/2;
// Set all canvas pixeldata values to 255, with all the content
// data being 0. This lets us scan for data[i] != 255.
ctx.fillStyle = "white";
ctx.fillRect(-1, -1, w+2, h+2);
ctx.fillStyle = "black";
ctx.fillText(textstring, padding/2, baseline);
var pixelData = ctx.getImageData(0, 0, w, h).data;
// canvas pixel data is w*4 by h*4, because R, G, B and A are separate,
// consecutive values in the array, rather than stored as 32 bit ints.
var i = 0,
w4 = w * 4,
len = pixelData.length;
// Finding the ascent uses a normal, forward scanline
while (++i < len && pixelData[i] === 255) {}
var ascent = (i/w4)|0;
// Finding the descent uses a reverse scanline
i = len - 1;
while (--i > 0 && pixelData[i] === 255) {}
var descent = (i/w4)|0;
// find the min-x coordinate
for(i = 0; i<len && pixelData[i] === 255; ) {
i += w4;
if(i>=len) { i = (i-len) + 4; }}
var minx = ((i%w4)/4) | 0;
// find the max-x coordinate
var step = 1;
for(i = len-3; i>=0 && pixelData[i] === 255; ) {
i -= w4;
if(i<0) { i = (len - 3) - (step++)*4; }}
var maxx = ((i%w4)/4) + 1 | 0;
// set font metrics
metrics.ascent = (baseline - ascent);
metrics.descent = (descent - baseline);
metrics.bounds = { minx: minx - (padding/2),
maxx: maxx - (padding/2),
miny: 0,
maxy: descent-ascent };
metrics.height = 1+(descent - ascent);
}
// if we ARE dealing with whitespace, most values will just be zero.
else {
// Only whitespace, so we can't measure the text
metrics.ascent = 0;
metrics.descent = 0;
metrics.bounds = { minx: 0,
maxx: metrics.width, // Best guess
miny: 0,
maxy: 0 };
metrics.height = 0;
}
return metrics;
};
//callback();
var fontName = document.getElementsByTagName('input')[0].value;
var fontSize = document.getElementsByTagName('input')[1].value;
var WebFontConfig = {
google: {
families: [ [encodeURIComponent(fontName),'::latin'].join('') ]
}
};
var wf = document.createElement('script');
wf.src = ('https:' == document.location.protocol ? 'https' : 'http') +
'://ajax.googleapis.com/ajax/libs/webfont/1/webfont.js';
wf.type = 'text/javascript';
wf.async = 'true';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(wf, s);
document.body.style.fontFamily = ['"'+fontName+'"', "Arial sans"].join(' ')
var canvas = document.getElementById('cvs'),
context = canvas.getContext("2d");
var w=220, h=200;
canvas.style.font = [fontSize, fontName].join(' ');
context.font = [fontSize, fontName].join(' ');
context.clearRect(0, 0, canvas.width, canvas.height);
// draw bounding box and text
var xHeight = context.measureText("x").height;
var capHeight = context.measureText("H").height;
var metrics = context.measureText("Sxy");
var xStart = (w - metrics.width)/2;
context.fontFamily = fontName;
context.fillStyle = "#FFAF00";
context.fillRect(xStart, h/2-metrics.ascent, metrics.bounds.maxx-metrics.bounds.minx, 1+metrics.bounds.maxy-metrics.bounds.miny);
context.fillStyle = "#333333";
context.fillText(testtext, xStart, h/2);
metrics.fontsize = parseInt(metrics.fontsize);
metrics.offset = Math.ceil((metrics.leading - metrics.height) / 2);
metrics.width = JSON.parse(JSON.stringify(metrics.width));
metrics.capHeight = capHeight;
metrics.xHeight = xHeight - 1;
metrics.ascender = metrics.capHeight - metrics.xHeight;
metrics.descender = metrics.descent;
var myMetrics = {
px: JSON.parse(JSON.stringify(metrics)),
relative: {
fontsize: 1,
offset: (metrics.offset / metrics.fontsize),
height: (metrics.height / metrics.fontsize),
capHeight: (metrics.capHeight / metrics.fontsize),
ascender: (metrics.ascender / metrics.fontsize),
xHeight: (metrics.xHeight / metrics.fontsize),
descender: (metrics.descender / metrics.fontsize)
},
descriptions: {
ascent: 'distance above baseline',
descent: 'distance below baseline',
height: 'ascent + 1 for the baseline + descent',
leading: 'distance between consecutive baselines',
bounds: {
minx: 'can be negative',
miny: 'can also be negative',
maxx: 'not necessarily the same as metrics.width',
maxy: 'not necessarily the same as metrics.height'
},
capHeight: 'height of the letter H',
ascender: 'distance above the letter x',
xHeight: 'height of the letter x (1ex)',
descender: 'distance below the letter x'
}
}
Array.prototype.slice.call(
document.getElementsByTagName('canvas'), 0
).forEach(function(c, i){
if (i > 0) document.body.removeChild(c);
});
document.getElementById('illustrationContainer').innerHTML = [
'<div style="margin:0; padding:0; position: relative; font-size:',fontSize,'; line-height: 1em; outline:1px solid black;">',
testtext,
'<div class="__ascender" style="position: absolute; width:100%; top:',myMetrics.relative.offset,'em; height:',myMetrics.relative.ascender,'em; background:rgba(220,0,5,.5);"></div>',
'<div class="__xHeight" style="position: absolute; width:100%; top:',myMetrics.relative.offset + myMetrics.relative.ascender,'em; height:',myMetrics.relative.xHeight,'em; background:rgba(149,204,13,.5);"></div>',
'<div class="__xHeight" style="position: absolute; width:100%; top:',myMetrics.relative.offset + myMetrics.relative.ascender + myMetrics.relative.xHeight,'em; height:',myMetrics.relative.descender,'em; background:rgba(13,126,204,.5);"></div>',
'</div>'
].join('');
myMetrics.illustrationMarkup = document.getElementById('illustrationContainer').innerHTML;
var logstring = ["/* metrics for", fontName,
"*/\nvar metrics =",
JSON.stringify(myMetrics, null, ' ')].join(' ');
document.getElementById('log').textContent = logstring;
}

Related

HTML Canvas - color transition between shapes ( shapes not overlapping)

The image is what I am looking forward to achieve using html canvas, without using blur or shadow.
Question :
Is there way to achieve right section of the image from left section of the image?
The fiddle for the problem is (basic drawing) here
var canvas = document.createElement('canvas'),
d = canvas.width = canvas.height = 400,
sq_size = d / 10,
ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
var color=["rgb(10,110,10)", "rgb(81,169,255)","rgb(81,239,255)",
"rgb(81,255,202)",
"rgb(81,255,132)","rgb(99,255,81)","rgb(169,255,81)",
"rgb(239,255,81)", "rgb(255,202,81)","rgb(255,132,81)"];
var x=0, len=color.length;
for(var i=0; i < d; i++){
while(x < d) {
var c = Math.floor(Math.random() * len);
ctx.fillStyle = color[c];
ctx.fillRect(x,i,sq_size,sq_size);
x = x + sq_size;
}
x = 0;
i = i+sq_size;
}
The closest you can get without implementing a blur.
You can use image smoothing ctx.imageSmoothingEnabled to blur an image that is very low resolution. Then mix the blurred and unblurred images using ctx.globalAlpha
Example
pixelSize controls the amount of blurring. A value of 1 is the max amount and gets less as this value gets bigger. Must be an integer value eg 1, 2, 3, 4, ...
Note results will vary depending on the device and browser / version used.
requestAnimationFrame(animationLoop);
const size = canvas.width;
const ctx = canvas.getContext('2d');
var mix = 123; // amount to transition
const transitionTime = 2; // seconds per transition
const swatches = [0,1,2];
const pixelSize = 2;
// eCurve p = 1 linear curve [default p = 2] is quadratic curve p = 3 cubic curve and so on.
const eCurve = (v, p = 2) => v < 0 ? 0 : v > 1 ? 1 : v ** p / (v ** p + (1 - v) ** p);
const cols = [
[10, 110, 10], [81, 169, 255], [81, 239, 255], [81, 255, 202], [81, 255, 132],
[99, 255, 81], [169, 255, 81], [239, 255, 81], [255,202, 81], [255,132,81]
];
const img = document.createElement("canvas");
img.height = img.width = swatches.length * pixelSize;
const imgCtx = img.getContext('2d');
function randomSwatch() {
swatches.forEach(y => { swatches.forEach(x => {
imgCtx.fillStyle = "rgb("+cols[Math.random() * cols.length | 0].join(",")+")";
imgCtx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
}); });
}
function animationLoop() {
mix = (mix >= 4 ? (randomSwatch(), 0) : mix) + 1 / (transitionTime * 60);
ctx.globalAlpha = 1;
ctx.imageSmoothingEnabled = false;
ctx.drawImage(img, 0, 0, size, size);
ctx.imageSmoothingEnabled = true;
const a = mix % 2;
ctx.globalAlpha = eCurve(a > 1 ? 2 - a : a, 3);
ctx.drawImage(img, 0, 0, size, size);
requestAnimationFrame(animationLoop);
}
<canvas id="canvas" height="300" width="300"></canvas>
This solution works best for me. Complies with my large dataset.
Not O(1) and I cannot possibly think it can be, or even O(n) (Minor banter).
P.s: Code can be optimised further.
Fiddle here
var canvas = document.createElement('canvas'),
d = canvas.width = canvas.height = 250,
sq_size = d / 5,
ctx = canvas.getContext('2d');
document.body.appendChild(canvas);
canvas.setAttribute('id','cav');
var color=["rgb(10,110,10)", "rgb(81,169,255)","rgb(81,239,255)", "rgb(81,255,202)", "rgb(81,255,132)","rgb(99,255,81)","rgb(169,255,81)", "rgb(239,255,81)", "rgb(255,202,81)","rgb(255,132,81)"];
var x=0, len=color.length;
var prevcolorh;
var colorobj = {};
for(var i=0; i < d;){
colorobj[i] = {};
while(x < d) {
var c = Math.floor(Math.random() * len);
colorobj[i][x] = color[c];
var gradient = ctx.createLinearGradient(x, 0, x+sq_size, 0);
var a = (prevcolorh !== undefined) ? prevcolorh : colorobj[i][x];
gradient.addColorStop(0, a);
gradient.addColorStop(1, colorobj[i][x]);
prevcolorh = colorobj[i][x];
ctx.fillStyle = gradient;
ctx.fillRect(x, i, d, d);
x = x + sq_size;
}
x = 0;
i = i+sq_size;
}
var rgbs = {};
for(var i=0; i<d; i+=sq_size) {
var imgd = ctx.getImageData(0, i+(sq_size/2), d, sq_size);
rgbs[i] = {};
var arr = [];
for (var j = 0, c = 0, n = imgd.data.length; j < n; j += 4, c++) {
if(j > 0 && j < d*4) {
arr.push([imgd.data[j],imgd.data[j+1],imgd.data[j+2],imgd.data[+3]]);
}
}
rgbs[i] = arr;
}
for(var k in rgbs) {
for(var i=0; i<rgbs[k].length; i++) {
if(rgbs[parseInt(k)+sq_size] !== undefined) {
var gradient2 = ctx.createLinearGradient(0, parseInt(k)+(sq_size/2), 0, parseInt(k)+(sq_size/2) + sq_size);
gradient2.addColorStop(0, 'rgba('+rgbs[k][i].join(',')+')');
gradient2.addColorStop(1, 'rgba('+rgbs[parseInt(k)+sq_size][i].join(',')+')');
ctx.fillStyle = gradient2;
ctx.fillRect(i, parseInt(k)+(3*sq_size/4), 1, sq_size/2);
}
}
}

How do I draw a box around multiple shapes in html5 canvas

I am trying to draw a box around multiple shapes in canvas to say that those shapes are related like a group.
Tried as below :
var ctx = c.getContext("2d"),
radius = 10,
rect = c.getBoundingClientRect(),
ctx.fillText("Draw something here..", 10, 10);
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(250, 300, radius, 0, 6.28);
ctx.fill();
ctx.fillStyle = "yellow";
ctx.beginPath();
ctx.arc(200, 100, radius, 0, 10.28);
ctx.fill();
ctx.fillStyle = "brown";
ctx.beginPath();
ctx.arc(350, 210, radius, 0, 10.28);
ctx.fill();
var x = (250+200+350)/3;
var y = (300+100+210)/3;
var radius = Math.sqrt((x1*x1)+(y1*y1));
var _minX = x - radius;
var _minY = y - radius;
var _maxX = x + radius;
var _maxY = y + radius;
ctx.rect(_minX,_minY,(_maxX-_minX+2),(_maxY-_minY+2));
ctx.stroke();
But it is not drawing properly.
How to get bounding box coordinates for canvas content? this link explains only for the path not for the existing shapes.
Below is the image how I want to draw:
Fabric <-- See if this library helps had used this for one of my project it is simple and quick.
This Code is not production ready, Or the best solution, but it works in "most cases".
I'm using the imageData, to check for non-white pixel. (If NOT 0 - RGBA Pixel ==> Object) and with this it narrows the possible Rectangle down. You would also need to tweak it, if you don't want the text to be in the Rectangle.
This code could / should be optimized.
EDIT: Now I am only checking if an Alpha Value is set. And some Random Object creation to test multiple Outcomes
Info: Objects that are clipped/cut happen, because they are out of the canvas size.
var c = document.getElementById("canvas");
var ctx = c.getContext("2d");
var colors = ["red", "blue", "green", "black"];
ctx.fillText("Draw something here..", 0, 10);
/** CREATEING SOME RANDOM OBJECTS (JUST FOR TEST) **/
createRandomObjects();
function createRandomIntMax(max){
return parseInt(Math.random() * 1000 * max) % max + 1;
}
function createRandomObjects(){
var objectsToDraw = createRandomIntMax(20);
for(var idx = 0; idx < objectsToDraw; idx++){
ctx.fillStyle = colors[createRandomIntMax(colors.length)];
ctx.beginPath();
ctx.arc(createRandomIntMax(c.width), createRandomIntMax(c.height), createRandomIntMax(30), 0, 2 * Math.PI);
ctx.fill();
}
}
/** GETTING IMAGE DATA **/
var myImageData = ctx.getImageData(0, 0, c.width, c.height);
/** FINDING BORDERS **/
findBorders(myImageData.data);
function findBorders(imageData) {
var result = {
left: c.width,
top: c.height,
right: -1,
bottom: -1
}
var idx = 0;
var lineLow = -1;
var lineHigh = -1;
var currentLine = 0;
var currentPoint, helper;
while (idx < imageData.length) {
currentPoint = imageData[idx + 3];
/** CHECKING FOR OBJECT **/
if (currentPoint != 0) {
helper = parseInt(idx % (c.width * 4)) / 4;
if (lineLow < 0) {
lineLow = helper;
lineHigh = helper;
} else {
lineHigh = helper;
}
}
if (idx !== 0 && (idx % (c.width * 4)) === 0) {
currentLine = idx / (c.width * 4);
// Updating the Border Points
if (lineLow > -1) {
result.left = Math.min(lineLow, result.left);
result.top = Math.min(currentLine, result.top);
result.right = Math.max(lineHigh, result.right);
result.bottom = Math.max(currentLine, result.bottom);
}
lineLow = -1;
lineHigh = -1;
}
idx += 4;
}
ctx.rect(result.left, result.top, result.right - result.left, result.bottom - result.top);
ctx.stroke()
}
<canvas id="canvas"></canvas>
USE getBoundingClientRect() to get the exact boundaries

How can I center a letter in circle html canvas?

So far, to do the centering, I am using the following two lines of code:
ctx.textAlign="center";
ctx.textBaseline = "middle";
This almost does the job, but some characters like "g" and "y" are not completely centered. How can I make sure that all of them are supported? Here is a JSbin that shows that the majority of characters like "g" is below the center line.
Expectation:
Reality:
To make my "expectation" work, I subtract 15px from the y value of the letter, but this messes up small letters like "a" and makes them go outside of the bounds on the top.
Measuring text.
One way is to render the character and then scan the pixels to find the extent, top, bottom, left, and right, to find the real center of the character.
This is an expensive process so added to that you would store the results of previous measurements in a map and return those results for the same character and font.
The example below creates the object charSizer. You set a font charSizer.font = "28px A font" then you can get the information regarding any character. charSizer.measure(char) which returns an object containing information regarding the characters dimensions.
You can measure characters in production and serve the information to the page to reduce client side processing but you will need to target each browser as they all render text differently.
Example
The example has instructions. The left canvas show char render to normal center using ctx.textAlign = "center" and ctx.textBaseline = "middle". Also included are color codded lines to show extent, center, bounds center, and weighted center. The middle canvas draw the char in circle using bounds center and the right canvas uses weighted center.
This is an example only, untested and not up to production quality.
const charSizer = (() => {
const known = new Map();
var w,h,wc,hc;
const workCan = document.createElement("canvas");
const ctx = workCan.getContext("2d");
var currentFont;
var fontHeight = 0;
var fontId = "";
function resizeCanvas(){
wc = (w = workCan.width = fontHeight * 2.5 | 0) / 2;
hc = (h = workCan.height = fontHeight * 2.5 | 0) / 2;
ctx.font = currentFont;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillStyle = "black";
}
function measure(char){
const info = {
char,
width : ctx.measureText(char).width,
top : null,
left : w,
right : 0,
bottom : 0,
weightCenter : { x : 0, y : 0 },
center : { x : 0, y : 0 },
offset : { x : 0, y : 0 },
wOffset : { x : 0, y : 0 },
area : 0,
width : 0,
height : 0,
}
ctx.clearRect(0,0,w,h);
ctx.fillText(char,wc,hc);
const pixels8 = ctx.getImageData(0,0,w,h).data;
const pixels = new Uint32Array(pixels8.buffer);
var x,y,i;
i = 0;
for(y = 0; y < h; y ++){
for(x = 0; x < w; x ++){
const pix = pixels[i++];
if(pix){
const alpha = pixels8[(i<<2)+3];
info.bottom = y;
info.right = Math.max(info.right, x);
info.left = Math.min(info.left, x);
info.top = info.top === null ? y : info.top;
info.area += alpha;
info.weightCenter.x += (x - wc) * (alpha/255);
info.weightCenter.y += (y - hc) * (alpha/255);
}
}
}
if(info.area === 0){
return {empty : true};
}
info.area /= 255;
info.weightCenter.x /= info.area;
info.weightCenter.y /= info.area;
info.height = info.bottom - info.top + 1;
info.width = info.right - info.left + 1;
info.center.x = info.left + info.width / 2;
info.center.y = info.top + info.height / 2;
info.offset.x = wc - info.center.x;
info.offset.y = hc - info.center.y;
info.wOffset.x = -info.weightCenter.x;
info.wOffset.y = -info.weightCenter.y;
info.top -= hc;
info.bottom -= hc;
info.left -= wc;
info.right -= wc;
info.center.x -= wc;
info.center.y -= hc;
return info;
}
const API = {
set font(font){
currentFont = font;
fontHeight = Number(font.split("px")[0]);
resizeCanvas();
fontId = font;
},
measure(char){
var info = known.get(char + fontId);
if(info) { return {...info} } // copy so it is save from change
info = measure(char);
known.set(char + fontId,info);
return info;
}
}
return API;
})()
//==============================================================================
//==============================================================================
// Demo code from here down not part of answer code.
const size = 160;
const sizeh = 80;
const fontSize = 120;
function line(x,y,w,h){
ctx.fillRect(x,y,w,h);
}
function hLine(y){ line(0,y,size,1) }
function vLine(x){ line(x,0,1,size) }
function circle(ctx,col = "red",x= sizeh,y = sizeh,r = sizeh*0.8,lineWidth = 2) {
ctx.lineWidth = lineWidth;
ctx.strokeStyle = col;
ctx.beginPath();
ctx.arc(x,y,r,0,Math.PI * 2);
ctx.stroke();
}
const ctx = canvas.getContext("2d");
const ctx1 = canvas1.getContext("2d");
const ctx2 = canvas2.getContext("2d");
canvas.width = size;
canvas.height = size;
canvas1.width = size;
canvas1.height = size;
canvas2.width = size;
canvas2.height = size;
canvas.addEventListener("click",nextChar);
canvas1.addEventListener("click",nextFont);
ctx.font = "20px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText("Click this canvas", sizeh,sizeh-30);
ctx.fillText("cycle", sizeh,sizeh);
ctx.fillText("characters", sizeh,sizeh + 30);
ctx1.font = "20px Arial";
ctx1.textAlign = "center";
ctx1.textBaseline = "middle";
ctx1.fillText("Click this canvas", sizeh,sizeh - 30);
ctx1.fillText("cycle", sizeh,sizeh);
ctx1.fillText("fonts", sizeh,sizeh + 30);
charSizer.font = "128px Arial";
ctx1.textAlign = "center";
ctx1.textBaseline = "middle";
ctx2.textAlign = "center";
ctx2.textBaseline = "middle";
const chars = "\"ABCDQWZ{#pqjgw|/*";
const fonts = [
fontSize+"px Arial",
fontSize+"px Arial Black",
fontSize+"px Georgia",
fontSize+"px Impact, Brush Script MT",
fontSize+"px Rockwell Extra Bold",
fontSize+"px Franklin Gothic Medium",
fontSize+"px Brush Script MT",
fontSize+"px Comic Sans MS",
fontSize+"px Impact",
fontSize+"px Lucida Sans Unicode",
fontSize+"px Tahoma",
fontSize+"px Trebuchet MS",
fontSize+"px Verdana",
fontSize+"px Courier New",
fontSize+"px Lucida Console",
fontSize+"px Georgia",
fontSize+"px Times New Roman",
fontSize+"px Webdings",
fontSize+"px Symbol",];
var currentChar = 0;
var currentFont = 0;
var firstClick = true;
function nextChar(){
if(firstClick){
setCurrentFont();
firstClick = false;
}
ctx.clearRect(0,0,size,size);
ctx1.clearRect(0,0,size,size);
ctx2.clearRect(0,0,size,size);
var c = chars[(currentChar++) % chars.length];
var info = charSizer.measure(c);
if(!info.empty){
ctx.fillStyle = "red";
hLine(sizeh + info.top);
hLine(sizeh + info.bottom);
vLine(sizeh + info.left);
vLine(sizeh + info.right);
ctx.fillStyle = "black";
hLine(sizeh);
vLine(sizeh);
ctx.fillStyle = "red";
hLine(sizeh + info.center.y);
vLine(sizeh + info.center.x);
ctx.fillStyle = "blue";
hLine(sizeh + info.weightCenter.y);
vLine(sizeh + info.weightCenter.x);
ctx.fillStyle = "black";
circle(ctx,"black");
ctx.fillText(c,sizeh,sizeh);
ctx1.fillStyle = "black";
circle(ctx1);
ctx1.fillText(c,sizeh + info.offset.x,sizeh+ info.offset.y);
ctx2.fillStyle = "black";
circle(ctx2,"blue");
ctx2.fillText(c,sizeh + info.wOffset.x, sizeh + info.wOffset.y);
}
}
function setCurrentFont(){
fontUsed.textContent = fonts[currentFont % fonts.length];
charSizer.font = fonts[currentFont % fonts.length];
ctx.font = fonts[currentFont % fonts.length];
ctx2.font = fonts[currentFont % fonts.length];
ctx1.font = fonts[(currentFont ++) % fonts.length];
}
function nextFont(){
setCurrentFont();
currentChar = 0;
nextChar();
}
canvas { border : 2px solid black; }
.red {color :red;}
.blue {color :blue;}
<canvas id="canvas"></canvas><canvas id="canvas1"></canvas><canvas id="canvas2"></canvas><br>
Font <span id="fontUsed">not set</span> [center,middle] <span class=red>[Spacial center]</span> <span class=blue> [Weighted center]</span><br>
Click left canvas cycles char, click center to cycle font. Not not all browsers support all fonts

emulate div behavior in canvas

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.

Dynamically adjust text color based on background image

I am working on a product that outputs images from users and the image information is overlayed on top of the aforementioned images. As you might imagine, the images require different text colors due to lightness/darkness. Is there a way to achieve this with JavaScript?
EDIT: I found a similar question to mine and there was a solution given in a jsfiddle (http://jsfiddle.net/xLF38/818). I am using jQuery for my site though. How would I convert the vanilla JavaScript to jQuery?
var rgb = getAverageRGB(document.getElementById('i'));
document.body.style.backgroundColor = 'rgb(' + rgb.r + ',' + rgb.g + ',' + rgb.b + ')';
function getAverageRGB(imgEl) {
var blockSize = 5, // only visit every 5 pixels
defaultRGB = {
r: 0,
g: 0,
b: 0
}, // for non-supporting envs
canvas = document.createElement('canvas'),
context = canvas.getContext && canvas.getContext('2d'),
data, width, height,
i = -4,
length,
rgb = {
r: 0,
g: 0,
b: 0
},
count = 0;
if (!context) {
return defaultRGB;
}
height = canvas.height = imgEl.naturalHeight || imgEl.offsetHeight || imgEl.height;
width = canvas.width = imgEl.naturalWidth || imgEl.offsetWidth || imgEl.width;
context.drawImage(imgEl, 0, 0);
try {
data = context.getImageData(0, 0, width, height);
} catch (e) {
/* security error, img on diff domain */
alert('x');
return defaultRGB;
}
length = data.data.length;
while ((i += blockSize * 4) < length) {
++count;
rgb.r += data.data[i];
rgb.g += data.data[i + 1];
rgb.b += data.data[i + 2];
}
// ~~ used to floor values
rgb.r = ~~ (rgb.r / count);
rgb.g = ~~ (rgb.g / count);
rgb.b = ~~ (rgb.b / count);
return rgb;
}
I finally found something to do precisely what I want it to do! Enter Brian Gonzalez's
jquery.adaptive-backgrounds.js. Check this out:
$parent.css({
// backgroundColor: data.color
color: data.color
});
I just commented out the backgroundColor rule and made a new one for color. For white text, a text-shadow like:
text-shadow: 0 0 1px rgba($black, 0.3); // using Sass
should be enough. Thank you to everyone for your answers!
This is possible using the canvas element. You would have to create a canvas element, draw the image element into the canvas, get the canvas's image data, look at the portion where the text is, convert those values to grayscale, average them, then compare them with a halfway point. Some example code:
var img = document.getElementById('myImage');
var c = document.createElement('canvas');
var ctx = c.getContext('2d');
var w = img.width, h = img.height;
c.width = w; c.height = h;
ctx.drawImage(img, 0, 0);
var data = ctx.getImageData(0, 0, w, h).data;
var brightness = 0;
var sX = 0, sY = 0, eX = w, eY = h;
var start = (w * sY + sX) * 4, end = (w * eY + eX) * 4;
for (var i = start, n = end; i < n; i += 4) {
var r = data[i],
g = data[i + 1],
b = data[i + 2];
brightness += 0.34 * r + 0.5 * g + 0.16 * b;
if (brightness !== 0) brightness /= 2;
}
if (brightness > 0.5) var textColor = "#FFFFFF";
else var textColor = "#000000";
I haven't tested this code, though it should work. Make sure to change the sX, sY, eX, eY values to only the area where your text is, otherwise you will get unsatisfactory results (it will still work). Good luck!
EDIT:
You will not have to display your image in any special way. Just make sure that the color of the overlay text is the variable textColor.
you could check the background-image attribute with jQuery then adjust the text color dynamically.
var x = $(body).attr("background-image");
switch(x)
{
case "something.png":
// set color here
break;
}

Categories

Resources