I have a <circle>
element in an SVG document, to which I apply a <radialGradient>
to give the illusion of it being a sphere:
<svg version="1.1" id="sphere_svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="640px" height="640px" viewBox="0 0 640 640" enable-background="new 0 0 640 640" xml:space="preserve">
<defs>
<radialGradient id="sphere_gradient" cx="292.3262" cy="287.4077" r="249.2454" fx="147.7949" fy="274.5532" gradientTransform="matrix(1.0729 0 0 1.0729 -23.3359 -23.3359)" gradientUnits="userSpaceOnUse">
<stop id="sphere_gradient_0" offset="0" style="stop-color:#F37D7F"/>
<stop id="sphere_gradient_1" offset="0.4847" style="stop-color:#ED1F24"/>
<stop id="sphere_gradient_2" offset="1" style="stop-color:#7E1416"/>
</radialGradient>
</defs>
<circle fill="url(#sphere_gradient)" cx="320" cy="320" r="320"/>
</svg>
It looks something like this:
I can render this in a three.js WebGLRenderer
container by using Gabe Lerner's canvg
library:
/* sphere_asset is a div containing the svg element */
var red_svg_html = new String($('#sphere_asset').html());
var red_svg_canvas = document.createElement("canvas");
canvg(red_svg_canvas, red_svg_html);
var red_svg_texture = new THREE.Texture(red_svg_canvas);
var red_particles = new THREE.Geometry();
var red_particle_material = new THREE.PointCloudMaterial({
map: red_svg_texture,
transparent: true,
size: 0.15,
alphaTest: 0.10
});
var red_particle_count = 25;
for (var p = 0; p < red_particle_count; p++) {
var pX = 0.9 * (Math.random() - 0.5),
pY = 0.9 * (Math.random() - 0.5),
pZ = 0.9 * (Math.random() - 0.5),
red_particle = new THREE.Vector3(pX, pY, pZ);
red_particles.vertices.push(red_particle);
}
var red_particle_system = new THREE.PointCloud(red_particles, red_particle_material);
scene.add(red_particle_system);
So far, so good. I can even programmatically modify the gradient and render different categories of particles:
What I would like to do is now switch over from WebGLRenderer
to using an SVGRenderer
, so that I can allow the end user to set the desired orientation and then export a vector image (SVG, or converted to PDF on the back end) that can be used for publication-quality work.
Using the SVG sandbox example from three.js as the basis for experimentation, I have tried a couple different techniques and have not had much luck. I'm hoping someone with experience with three.js may have some suggestions.
My first attempt was to use canvg
to render the SVG into a PNG image, and then apply that to an <image>
node:
var red_svg_html = new String($('#sphere_asset').html());
var red_svg_canvas = document.createElement("canvas");
canvg(red_svg_canvas, red_svg_html);
var red_png_data = red_svg_canvas.toDataURL('image/png');
var red_node = document.createElementNS('http://www.w3.org/2000/svg', 'image');
red_node.setAttributeNS('http://www.w3.org/1999/xlink', 'href', red_png_data);
red_node.setAttributeNS('http://www.w3.org/2000/svg', 'height', '10');
red_node.setAttributeNS('http://www.w3.org/2000/svg', 'width', '10');
var red_particle_count = 25;
for (var i = 0; i < red_particle_count; i++) {
var object = new THREE.SVGObject(red_node.cloneNode());
object.position.x = 0.9 * (Math.random() - 0.5);
object.position.y = 0.9 * (Math.random() - 0.5);
object.position.z = 0.9 * (Math.random() - 0.5);
scene.add(object);
}
No nodes show up in my viewbox.
The next thing I tried was a THREE.Sprite
object, using canvg
and THREE.Texture
routines:
var red_svg_html = new String($('#sphere_asset').html());
var red_svg_canvas = document.createElement("canvas");
canvg(red_svg_canvas, red_svg_html);
var red_svg_texture = new THREE.Texture(red_svg_canvas);
red_svg_texture.needsUpdate = true;
var red_sprite = THREE.ImageUtils.loadTexture(red_png_data);
var red_particle_count = 25;
for (var p = 0; p < red_particle_count; p++) {
var material = new THREE.SpriteMaterial( {
map: red_svg_texture,
transparent: true,
size: 0.15,
alphaTest: 0.10
});
var sprite = new THREE.Sprite( material );
sprite.position.x = 0.9 * (Math.random() - 0.5),
sprite.position.y = 0.9 * (Math.random() - 0.5),
sprite.position.z = 0.9 * (Math.random() - 0.5),
sprite.scale.set(0.1, 0.1, 0.1);
scene.add(sprite);
}
This was slightly better, in that I get white, opaque boxes where the spheres would otherwise appear in the rendered viewbox.
A third attempt was made to create an <svg>
to nest within the parent SVG node, which contains a reference-able <radialGradient>
with the id #sphere_gradient
:
var xmlns = "http://www.w3.org/2000/svg";
var svg = document.createElementNS(xmlns, 'svg');
svg.setAttributeNS(null, 'version', '1.1');
svg.setAttributeNS(null, 'x', '0px');
svg.setAttributeNS(null, 'y', '0px');
svg.setAttributeNS(null, 'width', '640px');
svg.setAttributeNS(null, 'height', '640px');
svg.setAttributeNS(null, 'viewBox', '0 0 640 640');
svg.setAttributeNS(null, 'enable-background', 'new 0 0 640 640');
var defs = document.createElementNS(xmlns, "defs");
var radialGradient = document.createElementNS(xmlns, "radialGradient");
radialGradient.setAttributeNS(null, "id", "sphere_gradient");
radialGradient.setAttributeNS(null, "cx", "292.3262");
radialGradient.setAttributeNS(null, "cy", "287.4077");
radialGradient.setAttributeNS(null, "r", "249.2454");
radialGradient.setAttributeNS(null, "fx", "147.7949");
radialGradient.setAttributeNS(null, "fy", "274.5532");
radialGradient.setAttributeNS(null, "gradientTransform", "matrix(1.0729 0 0 1.0729 -23.3359 -23.3359)");
radialGradient.setAttributeNS(null, "gradientUnits", "userSpaceOnUse");
var stop0 = document.createElementNS(null, "stop");
stop0.setAttributeNS(null, "offset", "0");
stop0.setAttributeNS(null, "stop-color", "#f37d7f");
radialGradient.appendChild(stop0);
var stop1 = document.createElementNS(null, "stop");
stop1.setAttributeNS(null, "offset", "0.4847");
stop1.setAttributeNS(null, "stop-color", "#ed1f24");
radialGradient.appendChild(stop1);
var stop2 = document.createElementNS(null, "stop");
stop2.setAttributeNS(null, "offset", "1");
stop2.setAttributeNS(null, "stop-color", "#7e1416");
radialGradient.appendChild(stop2);
defs.appendChild(radialGradient);
svg.appendChild(defs);
var red_circle = document.createElementNS(xmlns, "circle")
red_circle.setAttribute('fill', 'url(#sphere_gradient)');
red_circle.setAttribute('r', '320');
red_circle.setAttribute('cx', '320');
red_circle.setAttribute('cy', '320');
svg.appendChild(red_circle);
var red_particle_count = 25;
for (var i = 0; i < red_particle_count; i++) {
var object = new THREE.SVGObject(svg.cloneNode(true));
object.position.x = 0.85 * (Math.random() - 0.5);
object.position.y = 0.85 * (Math.random() - 0.5);
object.position.z = 0.85 * (Math.random() - 0.5);
scene.add(object);
}
No nodes are rendered. Adjustments of the <circle>
element's r
, cx
or cy
do not change the end result.
Interestingly, if I change the fill
attribute from url(#sphere_gradient)
to red
, I get a large circle mostly rendered outside my viewbox, which is not attached to the scene (it does not rotate with other elements in my parent scene, like the sides of a cube).
Is there a (working and performant) way to draw spheres or rounded, sphere-like particles in space using a SVGRenderer
in three.js?
Most attributes in SVG are in the null namespace so
is correctly written as
FWIW
should ideally be written as
although in this case your original form will work in most UAs.
In the final example the stop elements must be in the SVG namespace i.e.
should be
A gradient without stops is not drawn at all so that's why you don't see anything till you change the fill to red.
SVG has a painters model. Things are drawn in the order they occur in the file. If you want something to go on top of something else you need to place it later in the file.
Here is what you need to add to your SVGRenerer.js to fix your problem with rendering Particles:
In your first attempt, if you add the snap svg library, you could get the svg element(s) from canvg and wrap it into snap structure (as a paper for snap) and then use the paper.toString() method to produce the svg code for your svg File.
Edit: Or you could use fabric.js to convert canvas to svg:
Or you could use canvas2svg library.