Incorrect display of svg in the icon OpenLayers [duplicate] - svg

This question already has answers here:
What are XML namespaces for?
(10 answers)
Are SVG parameters such as 'xmlns' and 'version' needed?
(4 answers)
How to apply a color to a SVG Text element
(3 answers)
Closed last year.
Please tell me how to use svg for icons in OpenLayers? What structure should svg have?
We have encountered several problems when using svg and have prepared an example:
It is impossible to correctly change the color of the image using the "color" property (example with Green Smile and RedSmile)
Images are not displayed if you remove the link to the standard "xmlns="http://www.w3.org/2000/svg "" (example with Yellow Smile). Why doesn't it work?
Thank you for the answers!

As mentioned in the ol/style/Icon docs, the color property adds a "tint" to the icon. This is done to add support for jpg and png icons.
What it means, is that the color is mixed into the icon color.
In your example, since all paths are black -> black + red = black, and the background is white -> white + red = red. If you want to color the lines only, remove the fill property from <rect> and fill all <path> white.
This is how the new icon would then look:
let greenIcon =
'<svg width="148" height="117" viewBox="0 0 148 117" fill="none" xmlns="http://www.w3.org/2000/svg">' +
'<rect width="148" height="117"/>' +
'<path d="M86.5833 55.833C90.0351 55.833 92.8333 53.0348 92.8333 49.583C92.8333 46.1312 90.0351 43.333 86.5833 43.333C83.1316 43.333 80.3333 46.1312 80.3333 49.583C80.3333 53.0348 83.1316 55.833 86.5833 55.833Z" fill="white"/>' +
'<path d="M57.4167 55.833C60.8685 55.833 63.6667 53.0348 63.6667 49.583C63.6667 46.1312 60.8685 43.333 57.4167 43.333C53.9649 43.333 51.1667 46.1312 51.1667 49.583C51.1667 53.0348 53.9649 55.833 57.4167 55.833Z" fill="white"/>' +
'<path d="M72 76.6663C65.8333 76.6663 60.5417 73.2913 57.625 68.333H50.6667C54 76.8747 62.2917 82.9163 72 82.9163C81.7083 82.9163 90 76.8747 93.3334 68.333H86.375C83.5 73.2913 78.1667 76.6663 72 76.6663ZM71.9583 18.333C48.9583 18.333 30.3333 36.9997 30.3333 59.9997C30.3333 82.9997 48.9583 101.666 71.9583 101.666C95 101.666 113.667 82.9997 113.667 59.9997C113.667 36.9997 95 18.333 71.9583 18.333ZM72 93.333C53.5833 93.333 38.6667 78.4163 38.6667 59.9997C38.6667 41.583 53.5833 26.6663 72 26.6663C90.4167 26.6663 105.333 41.583 105.333 59.9997C105.333 78.4163 90.4167 93.333 72 93.333Z" fill="white"/>' +
"</svg>";

Related

Best way to dynamically style svg marker elements

My question: Can svg <marker> elements inherit color from the <line> they are referenced on?
The background: I have a D3 graph that has different styled lines, and I want my lines to have arrows at the end.
So at the top of my <svg> I have const defs = svg.append('defs'); and then further along I generate my defs using a generator function:
function makeDefs(defs: Selection<SVGDefsElement, unknown, null, undefined>, color: string, status: string) {
const markerSize = 3
const start = defs.append
.append('marker')
.attr('id', `arrow-start-${color}-${status}`)
.attr('viewBox', '-5 -10 20 20')
.attr('markerWidth', markerSize)
.attr('markerHeight', markerSize)
.attr('orient', 'auto-start-reverse');
start
.append('path')
.attr(
'd',
status === 'PUBLISHED' ? customPaths.arrowLarge : customPaths.arrowSmall
)
.attr('stroke', color)
.attr('fill', color);
}
And use it like so:
makeDefs(defs, 'red', 'DRAFT');
And then I add the markers to my lines with:
// d3 code to draw the lines etc
line.attr(
'marker-start',
(d) =>
`url(
#arrow-start-${d.color}-${d.status}
)`
);
This all works great, my arrows have lines. But my current setup feels burdensome and clunky. I have about 20 colors and 3 statuses. With my current setup that would be 60 different:
makeDefs(defs, 'one-of-20-colors', 'one-of-3-statues');
My understanding of markers is that they can inherit color using the currentcolor attribute. Currently my <defs> sit up top underneath my main <svg> so any color inherited is inherited directly from that top level svg which is not what I want. The issue in my case is my <line> elements are the elements who's color I want to inherit, but according to the MDN docs <line>s cannot have <defs> as children, thus leaving me with the only option, of defining all my <defs> up front all at once.
Is there a trick or some attribute I'm missing here?
Any way to pass color to my marker when doing:
line.attr(
'marker-start',
(d) =>
`url(
#arrow-start-${d.color}-${d.status}
)`
);
?
For what is is worth, I'm currently wrapping all my <line>s in <g>. I suppose I could wrap them in <svg>s instead, and apply the fill and stroke there, and then define my <defs> per svg container? I tried this briefly and swapping the <g> for an <svg> broke a lot, but I'm not even sure if it would work, or be better for that matter.

SVG with size in px and percentage?

I'm trying to create an SVG element with a width defined by a percentage of the parent and a fixed value, say 50% + 20px. For normal html elements, in the CSS you can use calc(50% + 20px). Is there an equivalent way to do this for embedded SVGs? Specifically, I'm using snap.svg, though I'm not sure if this capability exists with SVGs in general.
EDIT:
Tried setting <svg> width with percentages and px, which I couldn't get to work. I tried both:
<svg width='calc(50% + 20px)'>...</svg>
<svg width='50% + 20px'>...</svg>
I also tried setting it in CSS:
svg {
width: calc(50% + 20px);
}
It should be possible with the upcoming SVG2 as width etc. become geometry properties and then you can style them with calc

getting text width in SVG prior to rendering

I want to put a rectangle around a text in SVG.
The height of the text is known to me (the font-size attribute of the text element). But the width is dependent on the actual content. Using getBBox() or getComputedTextLength() should work. But this only works after rendering.
Is there a way to specify that in an other way? For example defining the x and width attributes relative to other values? I didn't find anything like that in the SVG Spec.
Figuring where text ends presumably requires roughly the same underlying code path as the rendering itself implements - going through the width of each character based on the font and style, etc... As I am not aware the SVG standards define a method for directly getting this information without doing the actual full rendering, till such methods emerge or are reported here by others, the approach should be to render invisibly before doing the actual rendering.
You can do that in a hidden layer (z-index, opacity and stuff) or outside the visible viewport, whichever works best in experimentation. You just need to get the browser do the rendering to find out, so you render invisibly for that sake, then use getComputedTextLength()
I know this is old, but a few ideas:
If you can choose a mono-spaced font, you know your width by a simple constant multiplication with glyph count
If you are bound to proportional fonts, you can find an average glyph size, do the math as with mono-space, and leave enough padding. Alternatively you can fill the padding with text element textLength attribute. If the constant is chosen carefully, the results are not very displeasing.
EDIT: As matanster found it to be hacky
Predetermine glyph widths with getComputedTextLength() and build a lookup table. Downside is that it does not account for kerning, but if your cache size is not a problem, you can append glyph-pair widths to this lookup.
Going beyond that is to find some way to do server side rendering: Is there a way to perform server side rendering of SVG graphics using React?
It is possible using canvas with measureText():
// Get text width before rendering
const getTextWidth = (text, font) => {
const element = document.createElement('canvas');
const context = element.getContext('2d');
context.font = font;
return context.measureText(text).width;
}
// Demo
const font = '16px serif';
const text = 'My svg text';
const textWidth = getTextWidth(text, font);
document.body.innerHTML = `
<svg>
<text x="0" y="20" font="${font}">${text}</text>
<rect x="0" y="30" width="${textWidth}" height="4" fill="red" />
</svg>
`;
Adapted from https://stackoverflow.com/a/31305410/1657101

SVG polar gradients

I'm a beginner at SVG, but I'd like to learn some techniques.
To be short, is there a simple way to create something like this?
I was thinking about creating a polar gradient and then clipping it:
But how do I generate a polar gradient?
Even if there's no native method, maybe it could be made with a simple linear gradient and then using some rectangular-polar coordinate transformation. Is there a way to do so?
So this is the solution I developed:
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg viewBox="0 0 100 100" version="1.1" onload="makeGradient();">
<script>
function makeGradient() {
var root = document.rootElement, i = 256, cir, a;
for (; i--;) {
a = i*Math.PI/128;
cir = document.createElementNS("http://www.w3.org/2000/svg", "circle");
cir.setAttribute("cx", 50 - Math.sin(a)*45);
cir.setAttribute("cy", 50 - Math.cos(a)*45);
cir.setAttribute("r", "5");
cir.setAttribute("fill", "rgb(" + i + ", " + i + ", " + i + ")");
root.appendChild(cir);
}
}
</script>
</svg>
Minified version (395 bytes):
<?xml version="1.0" standalone="no"?><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" version="1.1" onload="g(this.namespaceURI,document,Math)"><script>function g(n,d,m,i,c,a,p,z){for(i=256;i--;){a=i*m.PI/128;c=d.createElementNS(n,"circle");for(p in z={cx:10-m.sin(a)*9,cy:10-m.cos(a)*9,r:1,fill:"rgb("+[i,i,i]+")"})c.setAttribute(p,z[p]);d.rootElement.appendChild(c)}}</script></svg>
This was made creating circles filled with 256 shades of gray (it sounds like porn literature for coders!) and conveniently placed.
The radii can be adjusted: I've chosen 45 for the whole spinner and 5 for the single circles. Moreover, the detail can be adjusted too if 256 are too many:
for (; i -= 2;) { ...
Use powers of 2 for optimal results. Or just define the number of steps:
var steps = 100, i = steps;
for (; i--;) {
a = i*2*Math.PI/steps;
...
cir.setAttribute("fill", "rgb(" + i*255/steps + ", " + ...);
}
A big "thank you" to Erik Dahlström for the hint, and thank you Michael Mullany for the attempt :)
Edit: Here's a fiddle to demonstrate the code.
Edit 2: Here's another fiddle using curved segments to create the spinner. You can adjust the number of segments and the size, and even see it spinning. I don't know why when the size is auto, there's a bottom margin of 5 pixels on the SVG, this making the spinning slightly off-centered...
There are no paintservers in SVG 1.1 that allow this directly, but you can e.g do it using a bit of script. Here's an article that explains how.
There is no support for polar gradients in SVG 1.1 (what's available in most edge browsers today) although there are proposals to allow capabilities like these in SVG 2. The only workaround I can think of is to apply a blend filter using an externally generated image as your multiply source. But then, I'm assuming the whole point it to try to avoid external images so this would be a little pointless:)

Calculating viewBox parameters based on path elements in SVG

I get an XML or JSON with paths only, and I need to recreate the SVG image.
I create an empty
<svg xmlns='http://www.w3.org/2000/svg' version='1.1'></svg>,
I add a <g transform="scale(1 -1)" fill='#aaa' stroke='black' stroke-width='5' ></g> in it, and then in this element I add all of the paths in it (e.g. <path d= ... />).
In the end I get a SVG image, but because I haven't set the viewBox attribute in the SVG element the image isn't properly displayed - when I open it in browser, a part of it is displayed full size.
Can the viewBox be calculated from the values from the paths?
Thank you!
Similar to Martin Spa's answer, but a better way to do get the max viewport area is using the getBBox function:
var clientrect = path.getBBox();
var viewBox = clientrect.x+' '+clientrect.y+' '+clientrect.width+' '+clientrect.height;
You can then set the viewbox to these co-ordinates.
n.b. i think you can change the viewbox of an svg after it's rendered so you may have to re-render the svg.
OK so I solved it the following way:
removed all letters from the paths string and made an array out of it with
var values = pathValue.split('L').join(' ').split('M').join(' ').split('z').join('').split(' ');
found max and min from those values:
var max = Math.max.apply( Math, values );
var min = Math.min.apply( Math, values );
set the viewBox:
viewBox = max min max max
This worked in my case excellent. Hope that it will be helpful to someone else too.

Resources