How to extract one element from SVG with its reference element? - svg

here is an example SVG:
<svg xmlns="http://www.w3.org/2000/svg" version="1.1">
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="78" y1="269.543" x2="237" y2="269.543">......</linearGradient>
<symbol id="test" viewBox="-16.126 -14.41 32.251 28.819">...</symbol>
<rect x="78" y="203.043" style="fill:url(#SVGID_1_);" width="159" height="133"/>
<circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red" />
<g>
<use xlink:href="#test" width="32.251" height="28.819" x="-16.126" y="-14.41" transform="matrix(1 0 0 -1 402.9284 846.39)" style="overflow:visible;"></use>
</g>
</svg>
I want to get extract three subelements: rect, circle, g, however, you know, rect refers linearGradient and g refers symbol, how to extract one element along with its referenct elements?

I actually once did an implementation for a node.js library. See svg-icon-toolbox for the complete source. It does not use svg.js, but cheerio, a jQuery-like library to parse and manipulate XML sources.
The principle it works by is like that of a garbage collector: everything not marked as a valid reference is swept up and removed.
Identify the element to be preserved with its id.
var cheerio = require('cheerio');
// elements always removed
const remove = [
'animate',
'animateColor',
'animateMotion',
'animateTransform',
'cursor',
'script',
'set'
].join(',');
// elements always preserved
const preserve = [
'color-profile',
'font'
].join(',');
// elements whose links are ignored
const ignore = [
'a',
'altGlyph'
].join(',');
// removes everything not needed for a single icon export
function reduceTo ($copy, id) {
var reflist = new Set();
//mark elements for preservation
function mark (ref) {
var $target = $copy.find(ref);
//avoid doubles
if (!$target.parents(preserve).length && !reflist.has(ref)) {
reflist.add(ref);
//mark for preservation
$target.prop('refby', id);
//mark as having preserved children
$target.parentsUntil('svg').prop('passedby', id);
}
//find links
$target.find('*').addBack() // descendents and self
.add($target.parents()) // parents
.not([remove, ignore, preserve].join(','))
.each((i, el) => {
var $elem = $(el);
//unpack links and recurse
var link = $elem.attr('xlink:href');
if(link) {
mark(link);
}
funcProps.forEach((prop) => {
var value = $elem.css(prop) || $elem.attr(prop);
link = funcRegex.exec(value);
if (link) {
mark(link[1]);
}
});
});
}
//remove elements not needed
function sweep ($inspect) {
//filter out elements generally preserved
$inspect.children().not(preserve).each((i, el) => {
var $child = $(el);
//elements with children to be preserved: recurse
if ($child.prop('passedby') === id && $child.prop('refby') !== id) {
sweep($child);
//elements without mark: remove
} else if ($child.is(remove) || $child.prop('refby') !== id) {
$child.remove();
}
});
}
mark('#' + id);
sweep($copy);
return $copy;
}
var $ = cheerio.load(svgString, { xmlMode: true });
var reduced = reduceTo ($, id);
var serialized = $.xml();

Related

Pixi JS fill behaviour vs SVG fill behaviour

I'm wondering if there's a way to get the behaviour of Pixi.JS's Graphics API to fill the same way SVG paths are filled. I am guessing there may not be a quick-fix way to do this.
Basically, in SVGs when a path with a fill crosses over itself, the positive space will automatically get filled, whereas in the Pixi.JS Graphics API, if the path crosses itself it seems to try and fill the largest outside space.
This is quite difficult to explain in text so here is a codepen to show the differences between the two and how the same data will cause them to render in different ways.
And so you can see the code, here is my SVG:
<svg width="800" height="600" viewBox="0 0 800 600" xmlns="http://www.w3.org/2000/svg" style="background-color:#5BBA6F">
<path style="fill: #E74C3C; stroke:black; stroke-width:3" d="M 200 200 L 700 200 L 600 400 L 500 100 z" />
</svg>
And here is the Pixi.js implementation:
import * as PIXI from "https://cdn.skypack.dev/pixi.js";
const app = new PIXI.Application({
width: 800,
height: 600,
backgroundColor: 0x5bba6f
});
app.start();
document.getElementById("root").appendChild(app.view)
const lineData = {
color: 0xe74c3c,
start: [200, 200],
lines: [
[700, 200],
[600, 400],
[500, 100],
]
};
const { start, lines, color } = lineData;
const g = new PIXI.Graphics();
if (g) {
g.clear();
g.lineStyle({width:3})
g.beginFill(color);
g.moveTo(start[0], start[1]);
lines.map((l) => g.lineTo(l[0], l[1]));
g.closePath();
g.endFill();
app.stage.addChild(g);
}
Thanks for taking a look.
Here's our answer (it was right there in the Pixi.js issues).
To get this behaviour you need to change the "PIXI.graphicsUtils.buildPoly.triangulate" function and pull in another library called "Tess2".
import Tess2 from 'https://cdn.skypack.dev/tess2';
function triangulate(graphicsData, graphicsGeometry)
{
let points = graphicsData.points;
const holes = graphicsData.holes;
const verts = graphicsGeometry.points;
const indices = graphicsGeometry.indices;
if (points.length >= 6)
{
const holeArray = [];
// Comming soon
for (let i = 0; i < holes.length; i++)
{
const hole = holes[i];
holeArray.push(points.length / 2);
points = points.concat(hole.points);
}
console.log(points)
// Tesselate
const res = Tess2.tesselate({
contours: [points],
windingRule: Tess2.WINDING_ODD ,
elementType: Tess2.POLYGONS,
polySize: 3,
vertexSize: 2
});
if (!res.elements.length)
{
return;
}
const vrt = res.vertices;
const elm = res.elements;
const vertPos = verts.length / 2;
for (var i = 0; i < res.elements.length; i++ )
{
indices.push(res.elements[i] + vertPos);
}
for(let i = 0; i < vrt.length; i++) {
verts.push(vrt[i]);
}
}
}
Then extend the Pixi.js graphics class like this:
class TessGraphics extends PIXI.Graphics {
render(r) {
PIXI.graphicsUtils.buildPoly.triangulate = triangulate;
super.render(r);
}
}
I've updated the codepen to contain the broken version and the fixed version.
Not exactly a super easy fix. But hey I'm excited it's going!

SVG PATH Creation is not creating in exact path using mouse XY Co ordinates

I am drawing the SVG Path using XY Coordinates of element and mouse move. SVG path is not appending with correct Mouse point.
This is my code snippet
<svg xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" >
<g><path id=""></path></g> </svg>
<script>
.
.
.
var coord = getMousePosition(evt);
var getpos = document.getElementById(id).getBoundingClientRect();
var CTM = svg.getScreenCTM();
var pathValue = "M "+getpos.x+" "+getpos.y+" "+coord.x+" "+coord.y;
document.getElementById(id).setAttribute("d", pathValue);
.
.
</script>
.........
Looks like you are missed the 'L' command of your svg path...
let path;
addEventListener('mousedown', function(e) {
svg.innerHTML += `<path
fill=none
stroke-width=5
stroke="hsl(${Math.random()*360},66%,66%)"
d="M${e.x},${e.y}"
/>`;
path = svg.querySelector('path:last-child');
})
addEventListener('mouseup', function() {
path = null;
})
addEventListener('mousemove', function(e) {
path && path.setAttribute('d', path.getAttribute('d') + `L${e.x},${e.y}`);
})
addEventListener('resize', function() {
svg.setAttribute('width', innerWidth);
svg.setAttribute('height', innerHeight);
})
dispatchEvent(new Event('resize'));
body {
margin: 0;
overflow: hidden;
}
<svg id=svg></svg>
Click "Run code snippet" and try to draw in snippet frame ...

Get SVG-Object_s at given coordinates?

I'd like to obtain object IDs from an SVG-file via coordinates.
For example in
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" version="1.1"
height="50" width="50">
<rect id="rectRED"
x="15" y="5" height="30" width="30"
style="fill:#ff0000;fill-opacity:0.5;stroke:#000000;stroke-width:1.5" />
<rect id="rectBLUE"
x="5" y="15" height="30" width="30"
style="fill:#0000ff;fill-opacity:0.5;stroke:#000000;stroke-width:1.5" />
</svg>
getObjectsAt(10,25) should return a List containing rectBLUE
getObjectsAt(25,25) should return a List containing rectRED and rectBLUE
getObjectsAt(10,10) should return something like NIL
Is there a way to accomplish this?
There's document.elementFromPoint method, but it only returns the topmost element. To get all the elements under a point you could find the topmost one, hide it and look at the point again until no more elements are there:
var elementsAt = function( x, y ){
var elements = [], current = document.elementFromPoint( x, y );
// at least one element was found and it's inside a ViewportElement
// otherwise it would traverse up to the <html> root of jsfiddle webiste.
while( current && current.nearestViewportElement ){
elements.push( current );
// hide the element and look again
current.style.display = "none";
current = document.elementFromPoint( x, y );
}
// restore the display
elements.forEach( function( elm ){
elm.style.display = '';
});
return elements;
}
http://jsfiddle.net/duo02d38/

how to make parent component enclose children

I'm writing a model viewer with reactjs and svg. I want to make a parent component enclose a group of child components.
The child components are automatically sized according to their label text width. But how should the parent component get access the child sizes in order to calculate the smallest enclosing rect?
The problem seems to be how to communicate information upwards in the component hierarchy.
// child automatically sized after text width
var Child = React.createClass({
componentDidMount: function () {
var rect = this.refs.rect.getDOMNode();
var text = this.refs.text.getDOMNode();
rect.setAttribute('width', text.offsetWidth + 8);
},
render: function() {
return <g transform={'translate(' + this.props.x + ', ' + this.props.y + ')'}>
<rect ref="rect" height={50} fill="#cfc"/>
<text ref="text" x={4} y={15}>{this.props.text}</text>
</g>;
}
});
// parent that I want to automatically enclose all children
var Parent = React.createClass({
render: function() {
return <g>
<rect x={10} y={10} width={200} height={100} fill="#ccf"/>
<Child text="first child" x={20} y={20}/>
<Child text="second child" x={180} y={80}/>
</g>;
}
});
var Diagram = React.createClass({
render: function() {
return <svg width={300} height={250}><Parent/></svg>;
}
});
React.render(<Diagram/>, document.getElementById('container'));
Try the code here https://jsfiddle.net/dagrende/ep6xrLqy/1/.

How to create SVG shapes based on data?

I have structure with data and shapes definition:
var data = [
{
"id": "first",
"shapes": [
{
"shape": "polygon",
"points": [["8","64"],["8","356"],["98","356"],["98","64"]]
}
]
},
{
"id": "second",
"shapes": [
{
"shape": "ellipse",
"cx": "63", "cy": "306", "rx": "27","ry": "18"
}, {
"shape": "polygon",
"points": [["174","262"],["171","252"],["167","262"]]
}
]
}
]; // in the data may be stored any SVG shape
I would like to create SVG:
<svg width="218" height="400">
<g transform="translate(0,400) scale(1,-1)">
<g>
<polygon points="8,64 8,356 98,356 98,64"/>
</g>
<g>
<ellipse cx="63" cy="306" rx="27" ry="18"/>
<polygon points="174,262 171,252 167,262"/>
</g>
</g>
</svg>
For each data element I'm appending <g>:
var groups = svg.selectAll("g").data(data, function (d) {
return d.id;
});
groups.enter().append("g");
Now I'm binding data for group shapes:
var shapes = groups.selectAll(".shape").data(function (d) {
return d.shapes;
}, function(d,i){return [d.shape,i].join('-');});
So far it was as expected. Now I want to for each entering DOM node dispatch drawing function with proper shape but apparently shapes.enter().each() is not working in this context (not defined). I suppose it works rather on each DOM node than on each data to be bound. And this is working:
shapes.enter().append("g").each(function(draw, i) {
var shape = draw.shape;
d3.select(this).call(drawer[shape]);
});
But painful side-effect is that SVG has two levels of <g>:
<svg width="218" height="400">
<g transform="translate(0,400) scale(1,-1)">
<g>
<g>
<polygon points="8,64 8,356 98,356 98,64"/>
</g>
</g>
...
</svg>
How to get rid of that? How to build data based shapes correctly?
Two ways to add basic shapes based on data provided by AmeliaBR are ok but slightly outside d3.js API. After long consideration I've decided to add answer my own question.
Inspiration for searching other solution was comment of Lars Kotthoff under question suggesting using paths instead of primitive SVG shapes. In this approach instead of adding second level of <g> there should be added <path>:
shapes.enter().append("path").attr("d", function(d) {
return drawer[d.shape](d);
});
Original drawer object generating basic shapes would return value for attribute d of <path>.
var drawer = {
polygon: function (d) {
return [
"M", d.points[0].join(','),
"L", d.points
.slice(1, d.points.length)
.map(function (e) {
return e.join(",")
}), "Z"].join(" ");
},
ellipse: function (d) {
return [
'M', [d.cx, d.cy].join(','),
'm', [-d.rx, 0].join(','),
'a', [d.rx, d.ry].join(','),
0, "1,0", [2 * d.rx, 0].join(','),
'a', [d.rx, d.ry].join(','),
0, "1,0", [-2 * d.rx, 0].join(',')].join(' ');
},
circle: function (d) {
return this.ellipse({
cx: d.cx,
cy: d.cy,
rx: d.r,
ry: d.r
});
},
rect: function (d) {
return this.polygon({
points: [
[d.x, d.y],
[d.x + d.width, d.y],
[d.x + d.width, d.y + d.height],
[d.x, d.y + d.height]
]
});
},
path: function (d) {
return d.d;
}
};
Working example you can check at http://fiddle.jshell.net/daKkJ/4/
Tricky question. Shame that you can't call each on an enter() selection. Neither can you use a function(d){} in an append statement.(See comments)
But I got it working, using a Javascript forEach() call on the data array itself. It calls your function with the array entry, index, and array itself as parameters, and you can specify a this context -- I just passed in the desired parent element as a selection.
The fabulous fiddle: http://fiddle.jshell.net/994XM/1/
(simpler than your case, but should be able to adapt easily)
It's a bit confusing since the D3 code inside the forEach is all per element, with the d and i values already available for you, so you don't need any internal functions in your D3 method calls. But once you figure that out, it all works.
I found this question through Google, and the jsfiddles linked to above have ceased to work. You now have to call document.createElementNS, giving it the SVG namespace, otherwise the shapes don't show up.
I created an updated fiddle. I also cleaned out a lot of the unnecessary bits, so it should be easier to see what's going on.

Resources