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!
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 ...
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/
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/.
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.