Basic geometrical SVG elements can be positioned at fractions of a pixel, but apparently not so text elements as the example script demonstrates. While the motion of the rect element is smooth, the text element's motion is more like stuttering, probably because the position is rounded to the next full pixel so the text does not appear blurry due to anti-aliasing issues or so. But in this context of moving text this rounding does not make sense but causes the stuttering motion. This is just a guess, maybe I am wrong.
Does anyone have a solution how to make the motion of the text element as smooth as the rect element's? Thank you.
<html>
<svg width="800" height="800">
<rect x='0' y='0' width='250' height='50' fill='none' stroke='black' stroke-width='4' />
<text x='0' y='0' fill='black' font-size='40' font-weight='bold' text-anchor='middle'>Hello, world!</text>
</svg>
<script>
function moveElements() {
document.getElementsByTagName("rect")[0].setAttribute("x", p);
document.getElementsByTagName("rect")[0].setAttribute("y", p);
document.getElementsByTagName("text")[0].setAttribute("x", p + 125);
document.getElementsByTagName("text")[0].setAttribute("y", p + 38);
p += .1;
}
p = 0;
setInterval(moveElements, 20);
</script>
</html>
Since this behaviour only occurs when the text's baseline is parallel to the pixel grid, or in other words: exactly horizontal, the issue can be worked around by rotating the text by a tiny fraction hardly visible to the eye but big enough to escape the pixel grid, like so (not too elegant, more a hack, but works for me):
<html>
<svg width="800" height="800">
<rect x='0' y='0' width='250' height='50' fill='none' stroke='black' stroke-width='4' />
<text x='0' y='0' fill='black' font-size='40' font-weight='bold' text-anchor='middle'>Hello, world!</text>
</svg>
<script>
function moveElements() {
document.getElementsByTagName("rect")[0].setAttribute("x", p);
document.getElementsByTagName("rect")[0].setAttribute("y", p);
document.getElementsByTagName("text")[0].setAttribute("x", p + 125);
document.getElementsByTagName("text")[0].setAttribute("y", p + 38);
document.getElementsByTagName("text")[0].setAttribute("transform", "rotate(.05 " + (p + 125) + " " + (p + 25) + ")");
p += .1;
}
p = 0;
setInterval(moveElements, 20);
</script>
</html>
Related
Is there any way to view the space occupied by group element inside a SVG element.
SVG element's width = 600 and Height = 500
g is translated to (100,100)
Use getBBox as shown in the snippet:
const emptyBox = document.querySelector('.empty').getBBox();
console.log('EMPTY BOX: ', emptyBox);
const nonEmptyBox = document.querySelector('.non-empty').getBBox();
console.log('NON-EMPTY BOX: ', nonEmptyBox);
<svg>
<g class="empty"/>
<g class="non-empty">
<circle cx="100" cy="100" r="50" />
</g>
</svg>
What is the best way to scale and center a graph using d3-graphviz? I was hopeful that I could use scale(0.5) but this leaves the resulting graph uncentered.
I could probably go in with an .attributer() and manually adjust the <svg> and <g> elements to get what I'm looking for, but I figured there was probably a better way?
d3.select("#graph")
.graphviz()
.width(300)
.height(300)
.fit(true)
.scale(.5)
.renderDot('digraph {a -> b}');
<script src="//d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/#hpcc-js/wasm#0.3.11/dist/index.min.js"></script>
<script src="https://unpkg.com/d3-graphviz#3.0.5/build/d3-graphviz.js"></script>
<div id="graph" style="width: 300px; height: 300px; border: 1px solid black"></div>
There's no simple built-in way, but you can achieve almost anything with the attributer like so:
const px2pt = 3 / 4;
function attributer(datum, index, nodes) {
var selection = d3.select(this);
if (datum.tag == "svg") {
var width = datum.attributes.width;
var height = datum.attributes.height;
w = datum.attributes.viewBox.split(" ")[2];
h = datum.attributes.viewBox.split(" ")[3];
var x = (width * px2pt - w / 2) / 2;
var y = (height * px2pt - h / 2) / 2;
selection
.attr("width", width)
.attr("height", height)
.attr("viewBox", -x + " " + -y + " " + (width * px2pt) + " " + (height * px2pt));
datum.attributes.width = width;
datum.attributes.height = height;
datum.attributes.viewBox = -x + " " + -y + " " + (width * px2pt) + " " + (height * px2pt);
}
}
d3.select("#graph").graphviz()
.width(300)
.height(300)
.fit(true)
.scale(.5)
.attributer(attributer)
.renderDot('digraph {a -> b}');
<script src="//d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/#hpcc-js/wasm#0.3.11/dist/index.min.js"></script>
<script src="https://unpkg.com/d3-graphviz#3.0.5/build/d3-graphviz.js"></script>
<div id="graph" style="width: 300px; height: 300px; border: 1px solid black"></div>
Based on magjac's comment, I skipped .fit(), .scale(), .width(), and .height() and did it all in the attributer. This allows the solution to work even for larger graphs.
A few things to note:
Setting the height and width of the <svg> to 100% allows us to skip .width() and .height() and have the <svg> fill its container div.
Introduced a scale variable that can be set (0-1) to determine the scale of the graph
Added comments to help with anyone who finds their way here
Thank you magjac for this awesome library!
const scale = 0.8;
function attributer(datum, index, nodes) {
var selection = d3.select(this);
if (datum.tag == "svg") {
datum.attributes = {
...datum.attributes,
width: '100%',
height: '100%',
};
// svg is constructed by hpcc-js/wasm, which uses pt instead of px, so need to convert
const px2pt = 3 / 4;
// get graph dimensions in px. These can be grabbed from the viewBox of the svg
// that hpcc-js/wasm generates
const graphWidth = datum.attributes.viewBox.split(' ')[2] / px2pt;
const graphHeight = datum.attributes.viewBox.split(' ')[3] / px2pt;
// new viewBox width and height
const w = graphWidth / scale;
const h = graphHeight / scale;
// new viewBox origin to keep the graph centered
const x = -(w - graphWidth) / 2;
const y = -(h - graphHeight) / 2;
const viewBox = `${x * px2pt} ${y * px2pt} ${w * px2pt} ${h * px2pt}`;
selection.attr('viewBox', viewBox);
datum.attributes.viewBox = viewBox;
}
}
d3.select("#graph").graphviz()
.attributer(attributer)
.renderDot('digraph {a -> b -> c ->d -> e}');
<script src="//d3js.org/d3.v5.min.js"></script>
<script src="https://unpkg.com/#hpcc-js/wasm#0.3.11/dist/index.min.js"></script>
<script src="https://unpkg.com/d3-graphviz#3.0.5/build/d3-graphviz.js"></script>
<div id="graph" style="width: 300px; height: 300px; border: 1px solid black"></div>
We use the zoom behavior to do this.
Basically, what we do is that we wait for the graph to change (.on('transitionEnd')) and then recenter the graph and set the zoom level to 1.
For this, we grab our graph's SVG element and read its viewBox property.
This property is close to the size of our graph.
Then, we get the zoom behavior of our graph (the same that is used for zooming and panning with the mouse) and use its transform function.
The transform function can be used to set the zoom level as well as the translation, i.e., the panning of our graph, by passing a transform object.
d3.zoomIdentity gives us a new transform object with scale = 1, x = 0, y = 0, and by calling translate on it we can specify an x and y value to translate to. By default, d3-graphviz has on a plain graph, according to my small-scale empirical study, an x-translate of 4 and a y-translate that corresponds to the viewbox height - 4.
This leads to the following code (which also uses a transition to make the zoom transform smooth):
// you can also use 'renderEnd' if you do not use animations
graphVisualization.on('transitionEnd', () => {
// Some zoom examples: https://observablehq.com/#d3/programmatic-zoom
const svg = d3.select('#graph-wrapper svg')
const viewBox = svg.attr('viewBox').split(' ')
// const graphWidth = +viewBox[2]
const graphHeight = +viewBox[3]
const transform = graphVisualization.zoomBehavior()?.transform
// Define scale and translate
// Resetting zoom to 1
// +4 and -4 are used since they seem to be the default d3-graphviz offsets
svg.transition('translateTransition').call(transform, d3.zoomIdentity.translate(4, graphHeight - 4))
})
I'm using a-frame to create a scene with a box that moves according to a sine function, but the box doesn't show up inside the viewport.
I already tried to update the box position by changing the "position" attribute and resetting the box.object3D.position.
HTML file
<a-scene log="hello scene">
<a-assets>
<img id="boxTexture" src="https://i.imgur.com/mYmmbrp.jpg">
</a-assets>
<a-box src="#boxTexture" position="0 0 -5" rotation="0 45 45" scale="0.5 0.5 0.5">
<!-- <a-animation attribute="position" to="0 2.2 -5" direction="alternate" dur="2000"
repeat="indefinite"></a-animation> -->
<a-animation attribute="scale" begin="mouseenter" dur="300" to="2.3 2.3 2.3"></a-animation>
<a-animation attribute="scale" begin="mouseleave" dur="300" to="0.5 0.5 0.5"></a-animation>
<a-animation attribute="rotation" begin="click" dur="2000" to="360 405 45"></a-animation>
</a-box>
<a-camera>
<a-cursor></a-cursor>
</a-camera>
JavaScript file
// low frequency oscillator
function lfo(amp, freq, phase) {
var date = new Date();
var millis = 0.001 * date.getMilliseconds();
var value = amp * Math.sin(millis * freq + phase);
return value;
}
var boxes = document.querySelectorAll('a-box');
function moveBoxes() {
// loop through all the boxes in the document
for (var i = 0; i < boxes.length; i++) {
let x = lfo(1, 1, 0);
let y = lfo(1, 1, 0.2);
let z = lfo(1, 1, 0.7);
boxes[i].object3D.position.set(x, y, z);
//boxes[i].setAttribute("position", x+" "+y+" "+z);
}
}
I'd except to see the box either on center of the scene or at least at the upper-left corner, but it's neither.
The 2D viewport does not map to the 3D positions within the 3D scene.
The A-Frame mouse cursor maps mouse clicks to in-3D clicks. I'm not sure, but you might be able to use this sort of method to convert 2D X/Y coordinate to 3D coordinate at your specified distance: https://github.com/aframevr/aframe/blob/master/src/components/cursor.js#L213
But otherwise, I would just make something a child of the camera.
<a-entity camera>
<a-box move-sin-wave-or-whatever-animation position="0 0 -1"></a-box>
</a-entity>
Is there any way to "bend" an SVG object as its being animated along a bezier path? Ive been using mostly GSAP for animating things. The effect would look something like this: https://www.behance.net/gallery/49401667/Twisted-letters-2 (the one with the blue pencil). I have managed to get the red arrow to animate along the path but the shape stays the same the whole time. Id like for it to follow along the green path and bend as it goes around the curve so that at the end of the animation it has the shape of the purple arrow. Here is the codepen.
GSAP code:
var motionPath = MorphSVGPlugin.pathDataToBezier("#motionPath", {align:"#arrow1"});
var tl1 = new TimelineMax({paused:true, reversed:true});
tl1.set("#arrow1", {xPercent:-50, yPercent:-50});
tl1.to("#arrow1", 4, {bezier:{values:motionPath, type:"cubic"}});
$("#createAnimation").click(function(){
tl1.reversed() ? tl1.play() : tl1.reverse();
});
Is there a way to do this with just GSAP? Or will I need something like Pixi?
This is how I would do it:
First I need an array of points to draw the arrow and a track. I want to move the arrow on the track, and the arrow should bend following the track. In order to achieve this effect with every frame of the animation I'm calculating the new position of the points for the arrow.
Also: the track is twice as long as it seams to be.
Please read the comments in the code
let track = document.getElementById("track");
let trackLength = track.getTotalLength();
let t = 0.1;// the position on the path. Can take values from 0 to .5
// an array of points used to draw the arrow
let points = [
[0, 0],[6.207, -2.447],[10.84, -4.997],[16.076, -7.878],[20.023, -10.05],[21.096, -4.809],[25.681, -4.468],[31.033, -4.069],[36.068, -3.695],[40.81, -3.343],[45.971, -2.96],[51.04, -2.584],[56.075, -2.21],[60.838, -1.856],[65.715, -1.49],[71.077, -1.095],[75.956, -0.733],[80, 0],[75.956, 0.733],[71.077, 1.095],[65.715, 1.49],[60.838, 1.856],[56.075, 2.21],[51.04, 2.584],[45.971, 2.96],[40.81, 3.343],[36.068, 3.695],[31.033, 4.069],[25.681, 4.468],[21.096, 4.809],[20.023, 10.05],[16.076, 7.878],[10.84, 4.997],[6.207, 2.447],[0, 0]
];
function move() {
requestAnimationFrame(move);
if (t > 0) {
t -= 0.001;
} else {
t = 0.5;
}
let ry = newPoints(track, t);
drawArrow(ry);
}
move();
function newPoints(track, t) {
// a function to change the value of every point on the points array
let ry = [];
points.map(p => {
ry.push(getPos(track, t, p[0], p[1]));
});
return ry;
}
function getPos(track, t, d, r) {
// a function to get the position of every point of the arrow on the track
let distance = d + trackLength * t;
// a point on the track
let p = track.getPointAtLength(distance);
// a point near p used to calculate the angle of rotation
let _p = track.getPointAtLength((distance + 1) % trackLength);
// the angle of rotation on the path
let a = Math.atan2(p.y - _p.y, p.x - _p.x) + Math.PI / 2;
// returns an array of coordinates: the first is the x, the second is the y
return [p.x + r * Math.cos(a), p.y + r * Math.sin(a)];
}
function drawArrow(points) {
// a function to draw the arrow in base of the points array
let d = `M${points[0][0]},${points[0][1]}L`;
points.shift();
points.map(p => {
d += `${p[0]}, ${p[1]} `;
});
d += "Z";
arrow.setAttributeNS(null, "d", d);
}
svg {
display: block;
margin: 2em auto;
border: 1px solid;
overflow: visible;
width:140vh;
}
#track {
stroke: #d9d9d9;
vector-effect: non-scaling-stroke;
}
<svg viewBox="-20 -10 440 180">
<path id="track" fill="none"
d="M200,80
C-50,280 -50,-120 200,80
C450,280 450,-120 200,80
C-50,280 -50,-120 200,80
C450,280 450,-120 200,80Z" />
<path id="arrow" d="" />
</svg>
I made an svg clock using Java by drawing a circle and three animated lines into an svg file. This is how my clock looks like.
Now I want to add the hours to the clock like the following.
How can I do the calculations given the cx, cy, r, and stroke-width of the circle ? I want the user to supply these values and I draw the rest including the hour, minute, and second hand. I am not worried about the clock hands, I can figure that out, but I am not sure how to place the hours in the clock dynamically. I don't want to place them manually.
Here is my SVG file.
<svg xmlns = 'http://www.w3.org/2000/svg' version = '1.1' width="500.0" height="500.0">
<circle cx="100.0" cy="100.0" r="50.0" stroke="none" stroke-width="1.0" fill="rgba(102,205,170,0.4)"/>
<line x1="100" y1="100" x2="100" y2="62" stroke="red" stroke-width="2.0" stroke-linecap="round">
<animateTransform attributeName="transform" type="rotate" from="0 100 100" to="360 100 100" dur="500ms" repeatCount="indefinite"></animateTransform>
</line>
<line x1="100" y1="100" x2="100" y2="62" stroke="black" stroke-width="2.0" stroke-linecap="round">
<animateTransform attributeName="transform" type="rotate" from="0 100 100" to="360 100 100" dur="3s" repeatCount="indefinite"></animateTransform>
</line>
<line x1="100" y1="100" x2="100" y2="75" stroke="black" stroke-width="4.0" stroke-linecap="round">
<animateTransform attributeName="transform" type="rotate" from="0 100 100" to="360 100 100" dur="40s" repeatCount="indefinite"></animateTransform>
</line>
</svg>
How do I do this and what are the variables that I need as part of the calculations ? I'm pretty much looking for a formula in order to place the hours correctly.
Update: The answer by Michaël Lhomme is given me the best drawings
How can I make the hours go inside the circle? They are very close but I would want them inside.
Thanks!
You need to use cos and sin functions to calculate the coordinates of each point on the circle :
/*
* #param radius The clock radius
* #param cx X coordinates of the clock's center
* #param cy Y coordinates of the clock's center
*/
void drawHours(int radius, int cx, int cy) {
for(int i = 1; i <= 12; i++) {
double x = radius * Math.cos(Math.PI / -2 + (2 * i * Math.PI) / 12) + cx
double y = radius * Math.sin(Math.PI / -2 + (2 * i * Math.PI) / 12) + cy
String text = Integer.toString(i);
//draw text at [x, y] using style 'text-anchor' set to 'middle' and 'alignment-baseline' set to 'middle'
}
}
To adjust the locations of the text use svg alignment properties 'text-anchor' and 'alignment-baseline'
Start at the center of your circle, and use sin and cos trig functions to calculate the center of the point at which you are going to display a given numeral for the hour.
To calculate the center point for each numeral, something like this might work (untested):
// This will calculate the center point for the location at which a
// label for the given hour should be:
Point2D.Double getHourLocation(int hour, int clockDiameter)
{
double radians;
double x;
double y;
// assume 360 degrees, so range needs to be 0..360:
// first, adjust hour so 0 is at the top, 15s is at 3 o'clock, etc
// 360/12 = 30, so
hour *= 30;
// calculate x, y angle points relative to center
radians = Math.toRadians(hour * Math.PI / 180);
x = Math.cos(radians);
y = Math.sin(radians);
// sin() and cos() will be between -1.0 and 1.0 so adjust for that
x *= 100;
y *= 100;
// x, y points should now be relative to center of clock
x += clockDiameter;
y += clockDiameter;
return new Point2D.Double(x, y);
}
Once you have calculated this location, you'll have to adjust for your font characteristics. For example, assume that you know that the "12" text should be centered at (-50,50) - you'll need to adjust the x,y coordinates to factor in the text width and height to determine where to actually begin drawing.