Stacked instances of SVG text with fixed widths and auto heights - javascript

I'm looking to create a text effect in which lines of text automatically scale to hit a specifically defined width, with auto-adjusting heights.
Ideally, I would then be able to stack multiple words on top of one another to achieve something visually similar to the below.
Example image
Is this something that should be possible with SVG text? Could it be done through pure CSS?

In the next example I'm using yourtext but you can change it. The main idea is using textLength to set thelength of the text.
The lengthAdjust attribute controls how the text is stretched into the length defined by the textLength attribute.
In this case I'm using lengthAdjust="spacingAndGlyphs" but you may want to use spacing instead.
Please observe that the top and bottom text have a dx attribute that indicates a shift along the y-axis on the position of the tspan element. In thise case I'm choosing 16 (as the sont size)
also please observe that the text is centered around the point x:0,y:0. You can choose a different one.
svg {
font-family:arial;
font-weight:bold;
font-size:16px;
width: 90vh;
border: solid;
}
<svg viewBox="-50 -50 100 100">
<text text-anchor="middle" dominant-baseline="middle">
<tspan dy="-16" x="0" textLength="70" lengthAdjust="spacingAndGlyphs" id="top">EXAMPLE</tspan>
<tspan y="0" x="0" textLength="70" lengthAdjust="spacingAndGlyphs" id="mid">TEXT</tspan>
<tspan dy="16" x="0" textLength="70" lengthAdjust="spacingAndGlyphs" id="bottom">GOES HERE</tspan>
</text>
</svg>

Quite likely, you should include some javaScript to get the desired result.
The main problem:
svg <text> elements don't have anything like multi line text or line heights. So you need to split your text content into a lot of <tspan> elements with different y offsets to emulate something similar to a HTML <p>.
Besides, a lot of important properties can't yet be stylesd with css. Most importantly x and y which are crucial to mimic a line height.
Example: mimic multi line svg text - scale font-size to width
let svg = document.querySelector('svg')
let svgPseudoP = document.querySelector('.svgPseudoP');
svgSplitTextLines(svgPseudoP)
//split newlines
function svgSplitTextLines(el) {
let texts = el.querySelectorAll('text');
for (let t = 0; t < texts.length; t++) {
let text0 = texts[t];
let [x0, y0] = [text0.getAttribute('x'), text0.getAttribute('y')];
//trim empty elements and whitespace
let lines = text0.innerHTML.split(/\r?\n/);
lines = lines.map((str) => {
return str.trim()
}).filter(Boolean)
//set first line as textContent
text0.textContent = lines[0];
// calculate proportions
let width0 = text0.getComputedTextLength();
let style0 = window.getComputedStyle(text0);
let fontSize0 = parseFloat(style0.getPropertyValue('font-size'));
// ratio between capital letter height and font size
let ascenderRatio = 0.71582;
// define ideal leading
let leading = fontSize0 * 0.2;
for (let i = 1; i < lines.length; i++) {
let str = lines[i];
let tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
tspan.textContent = str;
tspan.setAttribute('x', x0);
text0.appendChild(tspan);
// scale font size according to width
let width = tspan.getComputedTextLength();
let scale = width0 / width;
let newFontSize = parseFloat(fontSize0) * scale;
tspan.setAttribute('style', 'font-size:' + newFontSize + 'px');
// emulate line height by increasing Y offset
let tspanPrev = tspan.previousElementSibling;
let yPrev = tspanPrev ? +tspanPrev.getAttribute('y') : +text0.getAttribute('y');
let newY = yPrev + (newFontSize * ascenderRatio)
tspan.setAttribute('y', newY + leading);
}
}
}
svg {
width: 50%;
border: 1px solid #ccc;
}
text {
font-family: Arial;
font-weight: bold;
text-anchor: middle;
text-transform: uppercase;
}
<svg class="svgPseudoP" viewBox="0 0 100 100">
<text x="50%" y="20" font-size="10">
Example
Text
Goes here
</text>
<text x="25%" y="60" font-size="8">
Example2
Text
Goes
here
</text>
</svg>
You will essentially need these steps:
set a desired line width that all lines should get (this could e.g be the first line/text element)
get each line's width via text.getComputedTextLength()
scale the font-size accordingly:
let scale = widthIdeal / widthCurrentLine;
let newFontSize = fontSizeFirst * scale
calculate line height/leading
This will require to get the ratio between capital letter height and the
fonts em square – otherwise lines with larger font sizes will add larger margins than smaller ones.
E.g write a capital in Arial at 100 points in inkscape, Illustrator etc. and convert it to paths/outlines and check it's height: 71.582 pt
So the capital to em square ratio is: 100/71.582 = 0.71582
This value depends on the actual font files metrics – so there is nor standardized capital letter height. However a ratio about 0.72–0.75 should be fine for a lot of font families.
Example: Uneven leading due to not ideal capital to em square ratio.
The above example code will also split markup based new lines to <tspan> elements:
<text x="50%" y="20" font-size="10">
Example
Text
Goes here
</text>
will be converted to:
<text x="50%" y="20" font-size="10">
Example
<tspan x="50%" style="font-size:18.9px" y="35.5">Text</tspan>
<tspan x="50%" style="font-size:8.1px" y="43.3">Goes here</tspan>
</text>

Related

Zoom to bounding box of path on externally loaded svg using D3

I'm using D3 to load an external SVG map that isn't using topojson (as the map was hand created and non a traditional map). I'm trying to target the elements #lines path so that when clicked, each path zooms and fills its bounding box.
I'm trying to use this example from Mike Bostock but can't figure out how to replicate it with the data that isn't using topojson. See this line:
.data(topojson.feature(us, us.objects.states).features)
It this even possible?
Here is the code I'm using to load the SVG.
var mapContainer = $('.map');
d3.xml("../assets/subwaymap.svg", function(error, subwayMap) {
if (error) throw error;
$('.map').append(subwayMap.documentElement)
I've tried getting the bounding box using .getBBOX but am confused at how it connects. It seems like all examples I've seen use d3.create("svg") and then tack all the functionality within that but since my data is already appended to the DOM, would this be necessary? Fairly new to D3. Thanks!
Two initial considerations: d3.create("svg") is rarely used in real D3 codes. Also, you don't have data appended to the DOM, just SVG elements you loaded (unless you're calling that "data").
Back to your question, you don't need path.bounds to make your code work, actually you don't even need d3.zoom. All you need is get the element's box (with getBBox) and do the appropriate transform.
The real problem, though, is that you need to wrap all the elements in a <g>, because you cannot apply the transform to the root SVG in SVG 1.1 (apparently this is possible in SVG 2).
Here is a basic demo. In this demo I'm using an external SVG made with different elements (circle, rectangle, text...), which represents the SVG you're appending. You get this SVG with:
const svg = d3.select("svg");
Then, considering you somehow manage to fix the <g> problem I mentioned, you get that group...
const g = svg.select("g");
... and you select the elements you want to zoom in (here, everything), binding an event listener:
const elements = g.selectAll("*")
.on("click", clicked);
In this demo I'm using Bostock's math, to save (my) time, but you can change it. Click the element to zoom in, click it again to zoom out.
const width = 500,
height = 400;
const svg = d3.select("svg");
const g = svg.select("g");
const elements = g.selectAll("*")
.each(function() {
d3.select(this).datum({})
})
.on("click", clicked);
function clicked(d) {
d.clicked = !d.clicked;
const bounds = this.getBBox();
const x0 = bounds.x;
const x1 = bounds.x + bounds.width;
const y0 = bounds.y;
const y1 = bounds.y + bounds.height;
g.transition().duration(1000).attr("transform", d.clicked ? "translate(" + (width / 2) + "," + (height / 2) + ") scale(" + (1 / Math.max((x1 - x0) / width, (y1 - y0) / height)) + ") translate(" + (-(x0 + x1) / 2) + "," + (-(y0 + y1) / 2) + ")" : "transform(0,0) scale(1)");
}
<svg width="500" height="400">
<g>
<circle cx="50" cy="50" r="30" stroke="black" stroke-width="3" fill="teal"></circle>
<rect x="300" y="20" rx="20" ry="20" width="150" height="150" style="fill:tomato;stroke:black;stroke-width:3"/>
<polygon points="200,100 250,190 160,210" style="fill:lavender;stroke:purple;stroke-width:3" />
<path d="M 140 350 q 150 -200 350 0" stroke="blue" stroke-width="5" fill="none" />
<text x="30" y="300" transform="rotate(-30, 30, 300)">Foo Bar Baz</text>
</g>
</svg>
<script src="https://d3js.org/d3.v5.min.js"></script>

svg : find x,y coordinates of rect vertices

I have various svg rects on a web page on which a transform is applied in the form :
transform="translate(418 258) rotate(-45.2033 15 18) scale(2.5 2.5)"
I need to get the x,y coordinates of the 4 vertices of each rect after the transform is applied.
Here is an exemple of code :
<g transform="translate(418 258) rotate(-45.25 15 18) scale(2.5 2.5)">
<rect id="box" x="0" y="0" width="31" height="37" style="fill:none;stroke:rgb(102, 102, 102);stroke-width:1.5px;">
</rect>
</g>
I have tried the following formula in plain js :
x' = x * cos(angle) + y * sin(angle)
y' = -x * sin(angle) + y * cos(angle)
but the results are slightly different from the svg display in various browsers.
I guess this can be done using js/svg primitives, but I don't know how, and didn't find any code example. Perhaps changing the rects into paths after transform would do the trick...
Last but not least, I'm using jquery but not d3.
Thanks in advance.
You can read the transform attribute and convert it to a matrix.
Then for each of the rectangle's four corners, you can use that matrix to calculate the transformed corner locations.
See the following demo.
This demo assumes that there is an element with id of "box", and that the transform you care about is just the one on the parent <g> element. If your circumstances are more complex than that, then you will need to do some more work on this code.
// Get a reference to the "box" rect element
var box = document.getElementById("box");
// Get its x, y, width and height
var bx = box.x.baseVal.value;
var by = box.y.baseVal.value;
var bw = box.width.baseVal.value;
var bh = box.height.baseVal.value;
// Get the box's parent element (the <g>)
var parentGroup = box.parentNode;
// Read the transform attribute and convert it to a transform matrix object
var transformMatrix = parentGroup.transform.baseVal.consolidate().matrix;
// For each of the rectangle's four corners, use the transform matrix to calculate its new coordinates
console.log("point 1 = ", doPoint(bx, by));
console.log("point 2 = ", doPoint(bx + bw, by));
console.log("point 3 = ", doPoint(bx + bw, by + bh));
console.log("point 4 = ", doPoint(bx, by + bh));
function doPoint(x, y)
{
// We need a reference to the <svg> element for the next step
var svg = box.ownerSVGElement;
// Create an SVGPoint object with the correct x and y values
var pt = svg.createSVGPoint();
pt.x = x;
pt.y = y;
// Use the "matrixTransform()" method on SVGPoint to apply the transform matrix to the coordinate values
pt = pt.matrixTransform(transformMatrix);
// Return the updated x and y values
return {x: pt.x, y: pt.y};
}
<svg>
<g transform="translate(418 258) rotate(-45.25 15 18) scale(2.5 2.5)">
<rect id="box" x="0" y="0" width="31" height="37" style="fill:none;stroke:rgb(102, 102, 102);stroke-width:1.5px;">
</rect>
</g>
</svg>

BBox calculation of svg <g> elements

I just came across a weired case of bouncing box calculation and it seems I did not grasp the whole truth yet.
First of all, a bounding box is defined as the tightest box, an untransformed element can be enclosed with.
I always was under the impression, that for groups, that means, that it gets basically the union of the bounding box of all children.
However, today I came across this:
<g id="outer">
<g id="inner" transform="translate(100, 100)">
<rect x="0" y="0" width="100" height="100" />
</g>
</g>
The bounding boxes of the elements are as follows:
rect: x: 0, y: 0, w: 100, h: 100
#inner: x: 0, y: 0, w: 100, h: 100
#outer: x: 100, y: 100, w: 100, h: 100
My expectation would have been, that all boxes are the same but as you can see, the outer box is NOT the union of the inner elements (in that case it would equal the #inner's bbox). Instead it takes into account the transformation of the inner elements.
So, is it right to say, that the bbox of a group is the union of the TRANSFORMED bbox's of its children? Or more programatically said, the union of all getBoundingClientRect calls (assuming that scroll is 0 because getCoundingClientRect ignores scroll)?
I would really appreciate a link pointing me to the correct part of the specs.
The bounding box returned by getBBox is the box in the element's transformed coordinate system
Returns the tight bounding box in current user space (i.e., after application of the ‘transform’ attribute, if any) on the geometry of all contained graphics elements, exclusive of stroking, clipping, masking and filter effects)...
The outer SVG element has a different co-ordinate system. I.e. where it places the origin is not the same as the inner <g> element because of the inner element's transform.
getBoundingClientRect operates in the global co-ordinate system however.
In this demo the red polygon represents the #outer BBox during an animation where the rect is rotating.
const SVG_NS = 'http://www.w3.org/2000/svg';
let o = outer.getBBox()
let i = inner.getBBox()
let BBpoly = drawBBox(o);
function drawBBox(bb){
let p = [{x:bb.x,y:bb.y},
{x:bb.x+bb.width,y:bb.y},
{x:bb.x+bb.width,y:bb.y+bb.height},
{x:bb.x,y:bb.y+bb.height}];
let BBpoly = drawPolygon(p, BBoxes);
return BBpoly;
}
function drawPolygon(p, parent) {
let poly = document.createElementNS(SVG_NS, 'polygon');
let ry = [];
for (var i = 0; i < p.length; i++) {
ry.push(String(p[i].x + ", " + p[i].y));
}
var points = ry.join(" ");
poly.setAttributeNS(null, 'points', points);
parent.appendChild(poly);
return poly;
}
function updatePolygon(p,poly){
let ry = [];
for (var i = 0; i < p.length; i++) {
ry.push(String(p[i].x + ", " + p[i].y));
}
var points = ry.join(" ");
poly.setAttributeNS(null, 'points', points);
}
let a = 0;
function Frame(){
requestAnimationFrame(Frame);
inner.setAttributeNS(null,"transform", `rotate(${a}, 120,120)`)
let bb = outer.getBBox()
let p = [{x:bb.x,y:bb.y},
{x:bb.x+bb.width,y:bb.y},
{x:bb.x+bb.width,y:bb.y+bb.height},
{x:bb.x,y:bb.y+bb.height}];
updatePolygon(p,BBpoly);
a++
}
Frame()
svg{border:1px solid; width:300px;}
polygon{fill:none; stroke:red; }
<svg viewBox="0 0 250 250">
<g id="BBoxes"></g>
<g id="outer">
<g id="inner">
<rect x="70" y="70" width="100" height="100" />
</g>
</g>
</svg>

How can I find the translated co-ordinates of an SVG element?

I have a SVG element with x and y set, but I'm also translating it by a certain vector using transform="translate(a, b)", which changes the co-ordinates it's rendered to but obviously doesn't update its x and y attributes. Is there a way to get the actual co-ordinates, which in this case would be x + a and y + b, without having to directly parse the values out of the transform attribute?
Not that this is a D3-specific question, but my code looks like this:
svg.selectAll(selector)
.attr("x", x)
.attr("y", y)
.attr("width", width)
.attr("height", height)
.attr("transform", `translate(${a}, ${b})`);
OK, this is not really D3 related, but pure SVG/javascript:
I use this function here that I call "flatten", basically you want to reset matrix to non-transformed one (matrix(1 0 0 1 0 0)) and update path points with their sort of flattened values:
flattenShape(item, matrix) {
let points = item.pathPoints;
let l = points.length;
for (let i = 0; i<l; i++) {
let cache = this.mainSVG.createSVGPoint();
cache.x = points[i].x;
cache.y = points[i].y;
cache = cache.matrixTransform(matrix);
points[i].x = cache.x;
points[i].y = cache.y;
}
item.d = this.constructPath(points);
item.transform = "matrix(1 0 0 1 0 0)";
};
item - your SVG element, matrix - you need to get the actual SVGMatrix of the element in question. I get it using:
let matrix = YOUR_SVG_ELEMENT.transform.baseVal.consolidate().matrix;
So my approach is maybe too specific but generally speaking:
consolidate matrix of the SVG element that you are transforming.
use SVGPoint to perform: matrixTransform(matrix) for each coordinate in question.
reset transform attribute to initial state e.g. matrixTransform(matrix)
You can use the getBoundingClientRect() method to get the position of the node.
Here's a snippet showing two rects with one of them translated:
var svgXY = d3.select('svg').node().getBoundingClientRect();
var rect1 = d3.select('rect#test1').node().getBoundingClientRect();
console.log('Rect 1: { top: ' + (rect1.top-svgXY.top) + ', left: ' + (rect1.left-svgXY.left) + '}');
var rect2 = d3.select('rect#test2').node().getBoundingClientRect();
console.log('Rect 2: { top: ' + (rect2.top-svgXY.top) + ', left: ' + (rect2.left-svgXY.left) + '}');
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://code.jquery.com/jquery-3.2.1.js"></script>
<svg width="300" height="200">
<rect x="20" y="40" fill="red" width="100" height="40" id="test1"></rect>
<rect x="20" y="40" fill="green" width="100" height="40" transform="translate(40, 30)" id="test2"></rect>
</svg>
Or if you're using jQuery, you can get the position by using $('rect#test1').position().
Hope this helps.
Edit:
body had a margin of 8px by default and hence x was equal to 28. I've added the CSS and check out the snippet now.

How to get Mid point of <g> tag in svg using javascript

I am working in SVG tags using javascript. I tried to get group tag <g> midpoint in svg. Is it possible to get mid point value of group tag using javascript?
Here's my demo group tag <g>
<g id="object_7" transform="translate(573,703) scale(0.5,0.51)" style="pointer-events:inherit">
<path d="m-40,-19l3,-3l74,0l3,3l0,37l-3,3l-74,0l-3,-3l0,-37z" id="uid127" stroke-linejoin="round" stroke-linecap="round" fill="#1e1d19" stroke="#000000"/>
<path d="m-9,21l4,2l10,0l4,-2" id="uid129" stroke-linejoin="round" stroke-linecap="round" fill-opacity="0" fill="none" stroke="#000"/>
<path d="m-40,-19l3,-3l74,0l3,3l-77,40l-3,-3l0,-37z" id="uid131" stroke-linejoin="round" stroke-linecap="round" fill-opacity="0.12" fill="#000000"/>
</g>
Here I need to get midpoint point of group tag. I used to get mouse co-ordinates for getting center of x and y position in group tag, but I did not achieve it. Can anyone please guide me?
You can get the bounding box of the <g> element by getting a reference to it and calling the function getBBox().
var bbox = document.getElementById("object_7").getBBox();
Note however that this is the union of all the bounding boxes of the group's children. If the group has a transform, it is not reflected in the bbox value. If you are adding elements to the group, this is probably the one you want.
If you want the bounds of the object in screen space, then you can get the group element's transform and apply it to the centre point you have calculated.
var ctm = document.getElementById("object_7").getCTM()
// Calculate the centre of the group
var cx = bbox.x + bbox.width/2;
var cy = bbox.y + bbox.height/2;
// Transform cx,cy by the group's transform
var pt = document.getElementById("mysvg").createSVGPoint();
pt.x = cx;
pt.y = cy;
pt = pt.matrixTransform(ctm);
// centre point in screen coordinates is in pt.x and pt.y
Demo here
If you want to get absolute middle point/position of g tag in screen:
let el = document.getElementById("object_7")
let midX = (el.getBoundingClientRect().left + el.getBoundingClientRect().right) / 2
let midY = (el.getBoundingClientRect().top + el.getBoundingClientRect().bottom) / 2
It also works for other svg elements.

Categories

Resources