Convert all SVG text nodes into path nodes with Raphael JS - javascript
I'm attempting to write a RaphaelJS function that will take existing text nodes within a Raphael paper instance and convert them into paths.
The goal is to replicate the position, size and attribute of the text exactly as it appears on the page, but have it rendered using paths instead of text. I cannot initially render the text using the Raphael paper.print() function because the text is updated dynamically and requires "text" based attributes to do so. Converting existing text nodes to paths will occur as the "final" step in the process (after the text modifications are complete).
I am doing this to eliminate the need for having fonts installed to view or handle the SVG later.
The challenges I face are:
Text nodes may include tspans with x and dy definitions. The paths created must line it perfectly witch each of the childNode letters (tspans).
Retrieving the actual position data of text node, and each tspan. This is where I'm having trouble and hopefully someone with more experience can assist me. Since stroke widths and other attributes affect the positioning/bbox values, I'm not sure what's the most efficient method of obtaining the correct positioning data for the text.
What I have tried so far:
A simple breakdown of my code.
I wrote a custom attribute function, textFormat, that formats the text in a staggered formation. This function parses the text node, splits it by each letter adding a new line \n character, and adjusts the positioning to look staggered.
The textToPaths function is a paper function that is supposed to loop through the paper nodes, and convert all found text nodes into path using the Raphael paper.print() function. This is the function I am having trouble with.
View the Complete JSFiddle Example Here
The problem code
I'm not sure how to obtain accurate and consistent x and y values to pass into the paper.print() function. Right now, I am using getBoundingClientRect() but it's still off and skewed. My assumption is the stroke widths are affecting the x and y calculations.
//Loop through each tspan and print the path for each.
var i,
children = node.node.childNodes,
len = children.length;
for (i = 0; i < len; i++) {
var tspan = children[i],
tspanText = tspan.innerHTML,
x = tspan.getBoundingClientRect().left - node.node.getBoundingClientRect().left, //How do I get the correct x value?
y = tspan.getBoundingClientRect().top - node.node.getBoundingClientRect().top; //How do I get the correcy y value?
var path = paper.print(x, y, tspanText, font, fontSize),
attrs = node.attrs;
delete attrs.x;
delete attrs.y;
path.attr(attrs);
path.attr('fill', '#ff0000'); //Red, for testing purposes.
}
Complete Code View the JSFiddle Example
//Register Cufon Font
var paper = Raphael(document.getElementById('paper'), '600', '600');
var text1 = paper.text(100, 100, 'abc').attr({fill: 'none',stroke: '#000000',"stroke-width": '12',"stroke-miterlimit": '1',"font-family" : "Lobster", "font-size": '30px','stroke-opacity': '1'});
var text2 = paper.text(100, 100, 'abc').attr({fill: 'none',stroke: '#ffffff',"stroke-width": '8',"stroke-miterlimit": '1',"font-family" : "Lobster", "font-size": '30px','stroke-opacity': '1'});
var text3 = paper.text(100, 100, 'abc').attr({fill: '#000000',stroke: '#ffffff',"stroke-width": '0',"stroke-miterlimit": '1',"font-family" : "Lobster", "font-size": '30px','stroke-opacity': '1'});
var text = paper.set(text1, text2, text3);
text.attr('textFormat', 'stagger');
/* paper.textToPaths
* Description: Converts all text nodes to paths within a paper.
*
* Example: paper.textToPaths();
*/
(function(R) {
R.fn.textToPaths = function() {
var paper = this;
//Loop all nodes in the paper.
for (var node = paper.bottom; node != null; node = node.next ) {
if ( node.node.style.display === 'none' || node.type !== "text" || node.attrs.opacity == "0") continue; //skip non-text and hidden nodes.
//Get the font config for this text node.
var text = node.attr('text'),
fontFamily = node.attr('font-family'),
fontSize = parseInt(node.attr('font-size')),
fontWeight = node.attr('font-weight'),
font = paper.getFont(fontFamily, fontWeight);
//Loop through each tspan and print the path for each.
var i,
children = node.node.childNodes,
len = children.length;
for (i = 0; i < len; i++) {
var tspan = children[i],
tspanText = tspan.innerHTML,
x = tspan.getBoundingClientRect().left - node.node.getBoundingClientRect().left, //How do I get the correct x value?
y = tspan.getBoundingClientRect().top - node.node.getBoundingClientRect().top; //How do I get the correcy y value?
var path = paper.print(x, y, tspanText, font, fontSize),
attrs = node.attrs;
delete attrs.x;
delete attrs.y;
path.attr(attrs);
path.attr('fill', '#ff0000'); //Red, for testing purposes.
}
}
};
})(window.Raphael);
textToPaths = function() {
//Run textToPaths
paper.textToPaths();
};
/* Custom Element Attribute: textFormat
* Description: Formats a text element to either staggered or normal text.
*
* Example: element.attr('textFormat, 'stagger');
*/
paper.customAttributes.textFormat = function( value ) {
// Sets the SVG dy attribute, which Raphael doesn't control
var selector = Raphael.svg ? 'tspan' : 'v:textpath',
has = "hasOwnProperty",
$node = $(this.node),
text = $node.text(),
$tspans = $node.find(selector);
console.log('format');
switch(value)
{
case 'stagger' :
var stagger = function(el) {
var R = Raphael,
letters = '',
newline = '\n';
for (var c=0; c < text.length; c++) {
var letter = text[c],
append = '';
if(c < text.length - 1)
append = newline;
letters += letter+append;
}
el.attr('text', letters);
var children = el.node.childNodes;
var i,
a = el.attrs,
node = el.node,
len = children.length,
letterOffset = 0,
tspan,
tspanHeight,
tspanWidth,
tspanX,
prevTspan,
prevTspanRight = 0,
tspanDiff = 0,
tspanTemp,
fontSize,
leading = 1.2,
tempText;
for (i = 0; i < len; i++) {
tspan = children[i];
tspanHeight = tspan.getComputedTextLength();
tspanWidth = tspan.getComputedTextLength();
tspanX = tspan.getAttribute('x'),
prevTspanRight = tspan.getBoundingClientRect().right
if(tspanX !== null)
{
tspanDiff = tspanDiff + prevTspanRight - tspan.getBoundingClientRect().left;
var setX = parseInt(tspanX) + parseInt(tspanDiff);
tspan.setAttribute('x', setX);
tspan.setAttribute('dy', 15);
}
prevTspan = tspan;
}
}
stagger(this);
break;
case 'normal' :
this.attr('text', text);
break;
default :
this.attr('text', text);
break;
}
eve("raphael.attr.textFormat." + this.id, this, value);
// change no default Raphael attributes
return {};
};
staggerText = function() {
//Run textToPaths
text.attr('textFormat', 'stagger');
};
If anyone can help me solve this problem I would greatly appreciate it. Thanks!
You can convert fonts to SVG/Canvas path commands using Opentype.js.
The lib will return to you a series of path drawing commands; these are intended for drawing on an HTML5 <canvas> element.
However it is trivial to build an SVG path with those commands since the font-conversion does not include any commands that are compatible with Canvas path drawing that would be incompatible with an SVG path command.
Related
PaperJs Add 2 raster as 2 symbols in the same project
I have this project in paperjs: var url = "http://www.clker.com/cliparts/q/I/s/P/E/3/yellow-umbrella-md.png"; raster = new Raster(url); raster.rotate(10); raster.scale(0.4); var url2 = "https://images.vexels.com/media/users/3/145373/isolated/preview/98721f602aa3fadb040e0a161ab3f966-waterdrop-vislumbrante-vis-o-ilustra--o-by-vexels.png"; secondRaster = new Raster(url); secondRaster.scale(0.9); var count = 150; var symbol = new Symbol(raster); var secondSymbol = new Symbol(secondRaster); for (var i = 0; i < count; i++) { // The center position is a random point in the view: var center = Point.random() * view.size; var placedSymbol = symbol.place(center); placedSymbol.scale(i / count); } function onFrame(event) { // Run through the active layer's children list and change // the position of the placed symbols: for (var i = 0; i < count; i++) { var item = project.activeLayer.children[i]; // Move the item 1/20th of its width to the right. This way // larger circles move faster than smaller circles: item.position.y += item.bounds.width / 80; // If the item has left the view on the right, move it back // to the left: if (item.bounds.bottom > view.size.width) { item.position.y = -item.bounds.width; } } } The first raster has a symbol works good, but the second can't make it work... I read about to add more than one symbol to project.activeLayer.children but don't work. Even if I do a group of an array with both symbols also don't show up. I read in a post that symbols can't be added as a group. Being that be true, it should be ok to be added even though isolated... Anybody had done something similar? Thank you
There are some mistakes in your code: The most important one, that make you think that the second raster doesn't work, is that you are creating the second raster with the variable url instead of url2. So both rasters use the same image as source... You need to place the second symbol like you do with the first one otherwise it will never get rendered. When iterating through active layer children, make sure to iterate over all children by using project.activeLayer.children.length (as you are placing count * 2 symbols). When checking for bottom reaching items, use height instead of width. Here is a sketch demonstrating the solution. var COUNT = 10; var raster = new Raster('http://www.clker.com/cliparts/q/I/s/P/E/3/yellow-umbrella-md.png'); raster.rotate(10); raster.scale(0.4); var secondRaster = new Raster('https://images.vexels.com/media/users/3/145373/isolated/preview/98721f602aa3fadb040e0a161ab3f966-waterdrop-vislumbrante-vis-o-ilustra--o-by-vexels.png'); secondRaster.scale(0.15); var symbol = new Symbol(raster); var secondSymbol = new Symbol(secondRaster); for (var i = 1; i <= COUNT; i++) { // first symbol symbol.place(Point.random() * view.size).scale(i / COUNT); // second symbol secondSymbol.place(Point.random() * view.size).scale(i / COUNT); } function onFrame(event) { for (var i = 0; i < project.activeLayer.children.length; i++) { var item = project.activeLayer.children[i]; item.position.y += item.bounds.height / 80; if (item.bounds.bottom > view.size.height) { item.position.y = -item.bounds.height; } } }
textarea - how to count wrapped lines/rows
I need some function That will count rows ( i know that on stackoverflow there are more than hundreds of these questions) but in my case i need to count them even when there is no end of line (mean "/n") because typical function is textarea.value.substr(0, textarea.selectionStart).split("\n").length;. It means that if user overflows max length of the row but he doesn't use "enter" and the text is on "new line". Well, i dont know how to describe it better, so there is a example on fiddle https://jsfiddle.net/fNPvf/12872/ try to write long sentence with no spaces, enters etc.. and you will see where the problem is what i really don't want is css rule nowrap, overflow-x etc..
Here you go. /** #type {HTMLTextAreaElement} */ var _buffer; /** * Returns the number of lines in a textarea, including wrapped lines. * * __NOTE__: * [textarea] should have an integer line height to avoid rounding errors. */ function countLines(textarea) { if (_buffer == null) { _buffer = document.createElement('textarea'); _buffer.style.border = 'none'; _buffer.style.height = '0'; _buffer.style.overflow = 'hidden'; _buffer.style.padding = '0'; _buffer.style.position = 'absolute'; _buffer.style.left = '0'; _buffer.style.top = '0'; _buffer.style.zIndex = '-1'; document.body.appendChild(_buffer); } var cs = window.getComputedStyle(textarea); var pl = parseInt(cs.paddingLeft); var pr = parseInt(cs.paddingRight); var lh = parseInt(cs.lineHeight); // [cs.lineHeight] may return 'normal', which means line height = font size. if (isNaN(lh)) lh = parseInt(cs.fontSize); // Copy content width. _buffer.style.width = (textarea.clientWidth - pl - pr) + 'px'; // Copy text properties. _buffer.style.font = cs.font; _buffer.style.letterSpacing = cs.letterSpacing; _buffer.style.whiteSpace = cs.whiteSpace; _buffer.style.wordBreak = cs.wordBreak; _buffer.style.wordSpacing = cs.wordSpacing; _buffer.style.wordWrap = cs.wordWrap; // Copy value. _buffer.value = textarea.value; var result = Math.floor(_buffer.scrollHeight / lh); if (result == 0) result = 1; return result; } Demo here
If I'm understanding the problem correctly, you need to count two things. The number of hard line breaks "\n". The number of lines that wrap past X characters. Here's some pseudo code: var lineLengthLimit = 40; var lineCounter = 0; foreach(lines as line){ lineCounter+=Math.floor(line.length/lineLengthLimit); } lineCounter += lines.length; Another option might be what this guy suggested: https://stackoverflow.com/a/3697249/1207539 but it seems a bit sketchy.
Have to write full path when checking background image javascript
if ($("#canvas").css('background-image') == 'url(images/endOfGame.jpg)') { does not work. But this does: var element = document.getElementById('canvas'); var style = window.getComputedStyle(element); var imagex = style.getPropertyValue('background-image'); console.log(imagex); if (imagex === "url(file:///C:/Users/Jack/Documents/myGames/Pong/images/endOfGame.jpg)") { and this does not: var element = document.getElementById('canvas'); var style = window.getComputedStyle(element); var imagex = style.getPropertyValue('background-image'); console.log(imagex); if (imagex === "url(images/endOfGame.jpg)") { why? I have to change the full file path code for every computer i run my game on. Not good. Thanks.
You could use indexOf which returns the character position of the found text (0 and above) or -1 if not found: if (imagex.indexOf("url(images/endOfGame.jpg)") >= 0) { // yes, string contains that text } I would prefer: if (imagex.indexOf("images/endOfGame.jpg") >= 0) { // yes, string contains that text } ignoring url(..). The following version ignores differences in case (upper or lower): if (imagex.toUpperCase().indexOf("images/endOfGame.jpg".toUpperCase()) >= 0) { // yes, string contains that text }
Raphael JS on rect click redirect to url takes the last url for all rectangles
I am trying to iterate over the list and create the rectangle boxes, and on click of each user should be redirected to specific page, though struggling to understand the click handler. Please help me to refine below code, so that it redirects to corresponding url instead of the last one. (I think its my lack of knowledge in JS itself.) var items = [{'url': 'http://google.com'}, {'url': 'http://stackoverflow.com'}]; var bh = 120; var bw = 120; var br = 8; var start_x = 100; var start_y = 80; r = Raphael("holder", 840, 780) for (var i = 0; i < items.length; i++){ group = r.set() group.push(r.rect(start_x, start_y, bh, bw, br)); start_x = start_x+200; group[0].node.onclick = function(){ alert(items[i].url); }; } jsFiddle Demo Above code is kind of version what I am working on and it renders multiple rects on SVG, the problem I am running into is on click of the rect, it returns the last one only. Thanks.
The problem is that there's one variable called i, and after the loop is over its value is items.length. You need to remember the correct value for each node. Try this: for (var i = 0; i < items.length; i++){ ... var rect = r.rect(start_x, start_y, bh, bw, br); rect.node.setAttribute('data-index', i); group.push(rect); ... rect.node.onclick = function(event) { alert(items[event.target.getAttribute('data-index')].url); }; }
How can I use greasemonkey and javascript to change the colour of every letter on a page to a randomly selected one?
So I've read a few similar questions, and I've managed to do things like change the background colour, but I have not yet been able to get this to work; What I want, is for each subsequent letter on a page to be randomly coloured. The colour space used isn't really important, as that's easy to fix once it actually works (am using this one at the moment), but I can't even get the text to change colour yet. I'm hoping I'm just making a silly mistake somewhere... This is what i'm trying at the moment; and it kind of works, but it's very dependant on what tagName i use, and because of the nature of most webpages, it can break a lot of things if i'm not careful... jsFiddle var elements = document.getElementsByTagName('p'); for(var i=0,l=elements.length;i<l;++i) { var str = elements[i].textContent; elements[i].innerHTML = ''; for(var j=0,ll=str.length;j<ll;++j) { var n = document.createElement('span'); elements[i].appendChild(n); n.textContent = str[j]; n.style.color = get_random_colour(); } } function get_random_colour() { var letters = '0123456789ABCDEF'.split(''); var colour = '#'; for (var i = 0; i < 6; i++ ) { colour += letters[Math.round(Math.random() * 15)]; } return colour; } In this example, p works fine, and doesn't seem to break anything, but if I do * or html or body then it breaks the page. Is there a way to get all the text on the page, and not break it? And another thing; I later changed the colour function to hopefully only pick colours that are in HSV(random,1,1) so that i only get nice bright colours, but it's not working. I'm presuming I just have some JS error in there, but I'm not that familiar with JS, so I'm finding it hard to find... Here are the changes
To do this, you will want to recurse through just the text nodes, careful not to trash child HTML elements. See the demo at jsFiddle. var x = document.querySelector ("body"); // Etc. buggerTextNodesIn (x); function buggerTextNodesIn (node) { var wrapClass = 'gmColorBarf'; function turnerizeTextNodes (node) { if (node.nodeType === Node.TEXT_NODE) { //--- Skip this node if it's already been wrapped. if ( ! node.parentNode.classList.contains (wrapClass) ) { var oldText = node.nodeValue; var parent = node.parentNode; for (var J = 0, len = oldText.length; J < len; ++J) { var wrapSpan = document.createElement ("span"); wrapSpan.classList.add (wrapClass); wrapSpan.textContent = oldText[J]; wrapSpan.style.color = getRandomColor (); parent.insertBefore (wrapSpan, node); } parent.removeChild (node); } } else if (node.nodeType === Node.ELEMENT_NODE) { /*--- IMPORTANT! Start "bottom up" since we will be adding gazillions of nodes and "top down" would skew our length measurement. */ for (var K = node.childNodes.length - 1; K >= 0; --K) { turnerizeTextNodes (node.childNodes[K] ); } } } turnerizeTextNodes (node); } function getRandomColor () { var letters = '0123456789ABCDEF'.split (''); var color = '#'; for (var J = 0; J < 6; ++J) { color += letters[Math.round(Math.random() * 15)]; } return color; } Note that to get iframed content, the easiest way is to tune the #include, #exclude, and/or #match directives to trigger on the iframe URL(s) -- if they don't already.