I came across this fiddle:
http://jsfiddle.net/wz32sy7y/1/
I'm having a hard time understanding how I would expand the circle to have a bigger radius.
I tried change the radius property r, but this desynchronizez the animation.
The radius seems to be some magical number, but I cannot determine how it's calculated.
<svg width="160" height="160" xmlns="http://www.w3.org/2000/svg">
<g>
<title>Layer 1</title>
<circle id="circle" class="circle_animation" r="79.85699"
cy="81" cx="81" stroke-width="8" stroke="#6fdb6f" fill="none"/>
</g>
</svg>
For a given radius r, the circumference of the circle is 2πr.
The values in this fiddle are slightly off due to rounding, but you can verify that the relationship holds by setting new values for the radius and circumference.
There are three places in the fiddle where the circumference is used. Once in the JavaScript:
var initialOffset = '440';
Twice in the CSS:
.circle_animation {
stroke-dasharray: 440; /* this value is the pixel circumference of the circle */
stroke-dashoffset: 440;
transition: all 1s linear;
}
Here is a version of the fiddle where the radius is set to 20 and the circumference to 2 π × 20 = 125.664:
http://jsfiddle.net/6x3rbpfu/1/
Here we set the radius to 50 and the radius to 314.159:
http://jsfiddle.net/6x3rbpfu/2/
The following fiddle will allow you to set the width arbitrarily, using the tag and the "r" attribute, and not changing your CSS every time. Try changing the value in the "r" attribute in the SVG to whatever you like.
https://jsfiddle.net/ma46yjvx/1/
Dash offset animation in SVG works by making a really long dash, using SVG's dashed outline features, and then creeping the border along that path, using an offset in pixels. It makes it look like it is drawing.
So when we scale the radius, we need to scale the amount that we offset the dash per animation step. Thus, using the same magic number the author used (dunno where it comes from, but it works!), we have this:
var time = 10;
var initialOffset = '440';
var i = 1
var r = $(".circle_animation").attr("r"); //Get the radius, so we can know the multiplier
var interval = setInterval(function() {
$('.circle_animation').css(
'stroke-dashoffset',
initialOffset-(i*(initialOffset/time)*(r/69.85699)) //Scale it!
);
$('h2').text(i);
if (i == time) {
clearInterval(interval);
}
i++;
}, 1000);
Related
The page I am working on contains a certain gradient that consists of seven colors. The colors in the gradient should go one after another radially, not in a linear way. The circle should be filled with the gradient based on a number from 1 to 100 (including floats) that represents a percentage of a circle that is filled. The number is sent dynamically by the backend and is a student GPA score.
I know that there were similar question here already. Like this one or this one. They differ from mine on one key aspect - I have to fill the element dynamically and I count the circumference of the circle element, so I cannot use multiple paths. And also this cannot just be a linear gradient, otherwise I would've done this already.
Also I don't know how to use d3.js or anything like that.
Please help me, the task is due tomorrow.
const circle = document.querySelector(".progress-ring__circle");
const radius = circle.r.baseVal.value;
const circumference = 2 * Math.PI * radius;
circle.style.strokeDasharray = `${circumference} ${circumference}`;
circle.style.strokeDashoffset = circumference;
function setProgress(percent) {
const offset = circumference - (percent / 100) * circumference;
circle.style.strokeDashoffset = offset;
};
setProgress(75);
.progress-ring__circle {
transform-origin: center;
transform: rotate(-90deg);
transition: stroke-dashoffset 0.3s;
}
<svg class="progress-ring" width="90" height="90">
<defs>
<linearGradient id="MyGradient">
<stop offset="5%" stop-color="green" />
<stop offset="95%" stop-color="gold" />
</linearGradient>
</defs>
<circle
class="progress-ring__circle"
stroke="url(#MyGradient)"
stroke-width="10"
cx="45"
cy="45"
r="30"
fill="none"
/>
</svg>
I have a code that displays the percentage as a circle. Is it possible to do something to make the animation start from the top, to the right, and not like now, it starts from the right. Is it possible to round this line? Is there any other, better code to do something like that? I'm only interested in vanillaJS.
var circle = document.querySelector('circle');
var radius = circle.r.baseVal.value;
var circumference = radius * 2 * Math.PI;
circle.style.strokeDasharray = circumference;
circle.style.strokeDashoffset = circumference;
function setProgress(percent) {
var offset = circumference - percent / 100 * circumference;
circle.style.strokeDashoffset = offset;
}
setProgress(60);
<svg class="progress-ring" width="120" height="120">
<circle class="progress-ring__circle" stroke="#000" stroke-width="8" fill="transparent" r="56" cx="60" cy="60">
</svg>
As I've commented you may rotate the svg element transform:rotate(-90deg). Alternatively you may rotate the circle. Also you can use a path instead of a circle and make it start at the top.
If you want to use a path this is how you do it:
In this case the path starts at the top M60,4
Next comes an arc where both radiuses are 56. The first arc ends at 60,116
Follows a second arc A56,56,0 0 1 60,4 and finnaly you close the path z
For the circumference you don't need to know the radius. You can do var circumference = circle.getTotalLength(); where getTotalLength is a method that is returning the total length of a path.
var circle = document.querySelector('path');
var circumference = circle.getTotalLength();
circle.style.strokeDasharray = circumference;
circle.style.strokeDashoffset = circumference;
function setProgress(percent) {
var offset = circumference - percent / 100 * circumference;
circle.style.strokeDashoffset = offset;
}
setProgress(60);
<svg class="progress-ring" width="120" height="120">
<path fill="none" class="progress-ring__circle" stroke="black" stroke-linecap="round" stroke-width="8" d="M60,4A56,56,0 0 1 60,116A56,56,0 0 1 60,4z" />
</svg>
First of all, Welcome on StackOverflow.
I think you have a trigonometry problem here. You have a trigonometric circle with your code and it start like others trigonometric circles at the right :
A simple solution is to rotate your circle with CSS :
svg{
transform: rotate(-90deg);
}
The essence of the problem is that when I move an SVG element around, the widths of the lines varies.
I'm working in a browser environment and I want crisp lines (no anti-aliasing), so shape-rendering is set to crispEdges. For example, you might have
"d M 0 0 L 10 0 L 10 10 L 0 10 z"
to define a square, with style set to
'fill: none; stroke: black; stroke-width: 1px; shape-rendering: crispEdges'
Then use translate() to drag it around with the mouse. It works, but as the square moves, the thickness of the lines jumps around by a pixel, and I want the thickness to remain constant.
I think I know why it's happening, but I can't find any way to prevent it. It happens (I think) due to the way the coordinates relative to the viewBox are mapped to coordinates relative to the physical monitor's pixels. When a one pixel wide (in SVG terms) line is mapped to the monitor, it may or may not straddle several pixels, so it may end up a (screen) pixel thicker or thinner.
I've tried messing with the ratio of the viewBox size to the size of the drawing area, rounding the translation amount to be a whole number or to take the form integer + 0.50, but no solution like this seems likely to work because it comes down to the level of zoom in the browser. vector-effect: non-scaling-stroke doesn't fix it either.
Is there a way to tell SVG to treat all paths as 1d geometric shapes, and to use a particular pen for all of them? Put another way, I want the pen used to draw everything to "scale with the zoom", rather than having the lines scale after they've been drawn.
Please, vanilla JS only.
You can test what I mean by running the code below. Click on the square and drag it around (no touch devices). Note that at some levels of browser zoom, it may behave nicely and not do what I've described.
There's something odd going on. The svg element (the square) starts out looking uniform, with all edges of the same width. It only moves in response to a mousemove, and that event (presumably) happens when the mouse moves by a whole (never fractional) screen pixel. Therefore, one would expect the square to move by a whole screen pixel and remain aligned to the screen pixels if it started off that way.
Follow-up...The problem is not with the screen CTM. I tested that the inverse really is an inverse; that is, CTM x CTM^{-1} is the identity for every value I tried. And the values that matrixTransform(getScreenCTM().inverse()) provides are linear in the inputs (or as linear as floating-point numbers allow).
<html>
<body>
<svg xmlns="http://www.w3.org/2000/svg" id="svgtest"
style = "border: 1px solid; display: block; margin-left: auto; margin-right: auto; shape-rendering: crispEdges;"
width = "400" height = "400" viewBox = "0 0 100 100" >
</svg>
<script>
var theSquare = {
x : 10,
y : 10
};
var initialSquare = {
x : 10,
y : 10
};
var SideLength = 10;
var initialX = 0;
var initialY = 0;
var draggingSquare = 0;
function pointInRect(p, r) {
return p.x > r.xLeft && p.x < r.xRight && p.y > r.yTop && p.y < r.yBottom;
}
function mouseToLocal(theEvent) {
var screenPt = svgArea.createSVGPoint();
screenPt.x = theEvent.clientX;
screenPt.y = theEvent.clientY;
var svgPt = screenPt.matrixTransform(svgArea.getScreenCTM().inverse());
return {
x : svgPt.x,
y : svgPt.y
};
}
function doMouseDown(event) {
var localCoord = mouseToLocal(event);
initialX = localCoord.x;
initialY = localCoord.y;
var pt = {x: initialX, y: initialY};
var rect = {xLeft: theSquare.x, xRight: theSquare.x + SideLength,
yTop: theSquare.y, yBottom: theSquare.y + SideLength};
if (pointInRect(pt,rect))
{
draggingSquare = 1;
initialSquare.x = theSquare.x;
initialSquare.y = theSquare.y;
}
else
draggingSquare = 0;
}
function doMouseUp(event) {
draggingSquare = 0;
}
function doMouseMove(event) {
if (draggingSquare == 0)
return;
var localCoord = mouseToLocal(event);
zeroTranslate.setTranslate(initialSquare.x + localCoord.x - initialX,
+initialSquare.y + localCoord.y - initialY);
theSquare.x = initialSquare.x + localCoord.x - initialX;
theSquare.y = initialSquare.y + localCoord.y - initialY;
}
var svgArea = document.getElementById('svgtest');
var theSVGSquare = document.createElementNS("http://www.w3.org/2000/svg", 'path' );
theSVGSquare.setAttributeNS(null, "d", "M 0 0" +
" L " + SideLength + " 0" +
" L " + SideLength + " " + SideLength +
" L 0 " +SideLength +
" z");
theSVGSquare.setAttributeNS(null, 'style', 'fill: none; stroke: black; stroke-width: 0.5px; shape-rendering: crispEdges' );
var tforms = theSVGSquare.transform;
var zeroTranslate = svgArea.createSVGTransform();
zeroTranslate.setTranslate(initialSquare.x,initialSquare.y);
theSVGSquare.transform.baseVal.insertItemBefore(zeroTranslate,0);
svgArea.appendChild(theSVGSquare);
svgArea.addEventListener("mousedown",doMouseDown,false);
svgArea.addEventListener("mouseup",doMouseUp,false);
svgArea.addEventListener("mousemove",doMouseMove,false);
</script>
</body>
</html>
I'm posting this "answer" as a way to officially close the question, and to thank Robert Longson for his attention.
In short, what I want to do can't be done in a browser, which is surprising since all I want to do is draw a line. Using the HTML canvas tag produces something blurry due to anti-aliasing, and SVG produces jittery lines. It comes down to the fact that the browser doesn't provide the information needed to work at a device-pixel level of accuracy.
I posted a discussion of this problem, with examples, on my personal website:
Canvas drawing is blurry and SVG is jittery
I am in the process of trying to create a 30s countdown timer display using SVG and a spot of JS. The idea is simple
Draw the face of the countdown clock as an SVG circle
Inside it draw a closed SVG path in the form of the sector of circle
Use window.requestAnimationFrame to update that sector at one second intervals
My effort is shown below. While it works the final result is far from being smooth and convincing.
When the spent time gets into the second quadrant of the circle the sector appears to swell past the circumference
When it is in the third and fourth quadrant it appears to detach from the circumference.
What am I doing wrong here and how could it be improved?
var _hold = {tickStart:0,stopTime:30,lastDelta:0};
String.prototype.format = function (args)
{
var newStr = this,key;
for (key in args) {newStr = newStr.replace('{' + key + '}',args[key]);}
return newStr;
};
Boolean.prototype.intval = function(places)
{
places = ('undefined' == typeof(places))?0:places;
return (~~this) << places;
};
function adjustSpent(timeStamp)
{
if (0 === _hold.tickStart) _hold.tickStart = timeStamp;
var delta = Math.trunc((timeStamp - _hold.tickStart)/1000);
if (_hold.lastDelta < delta)
{
_hold.lastDelta = delta;
var angle = 2*Math.PI*(delta/_hold.stopTime),
dAngle = 57.2958*angle,
cx = cy = 50,
radius = 38,
top = 12,
x = cx + radius*Math.sin(angle),
y = cy - radius*Math.cos(angle),
large = (180 < dAngle).intval();
var d = (360 <= dAngle)?"M50,50 L50,12 A38,38 1 0,1 51,12 z":"M50,50 L50,12 A38,38 1 {ll},1 {xx},{yy} z".format({ll:large,xx:x,yy:y});
var spent = document.getElementById('spent');
if (spent) spent.setAttribute("d",d);
}
if (delta < _hold.stopTime) window.requestAnimationFrame(adjustSpent);
}
window.requestAnimationFrame(adjustSpent);
timer
{
position:absolute;
height:20vh;
width:20vh;
border-radius:100%;
background-color:orange;
left:calc(50vw - 5vh);
top:15vh;
}
#clockface{fill:white;}
#spent{fill:#6683C2;}
<timer>
<svg width="20vh" height="20vh" viewBox="0 0 100 100">
<circle cx="50" cy="50" r="38" id="clockface"></circle>
<path d="M50,50 L50,12 A38,38 1 0,1 51,12 z" id="spent"></path>
</svg>
</timer>
A posible solution would be using a stroke animation like this:
The blue circle has a radius of 38/2 = 19
The stroke-width of the blue circle is 38 giving the illusion of a circle of 38 units.
Please take a look at the path: it's also a circle of radius = 19.
svg {
border: 1px solid;
height:90vh;
}
#clockface {
fill: silver;
}
#spent {
fill:none;
stroke: #6683c2;
stroke-width: 38px;
stroke-dasharray: 119.397px;
stroke-dashoffset: 119.397px;
animation: dash 5s linear infinite;
}
#keyframes dash {
to {
stroke-dashoffset: 0;
}
}
<svg viewBox="0 0 100 100">
<circle cx="50" cy="50" r="38" id="clockface"></circle>
<path d="M50,31 A19,19 1 0,1 50,69 A19,19 1 0,1 50,31" id="spent"></path>
</svg>
In this case I've used css animations but you can control the value for stroke-dashoffset with JavaScript.
The value for stroke-dasharray was obtained using spent.getTotalLength()
If you are not aquainted with stroke animations in SVG please read How SVG Line Animation Works
In the site that I am currently building I have an SVG path which is basically a straight line. When the user scrolls down the page, this path draws down the page until the user hits the bottom of the page.
The code I am using for this is taken from this page https://www.w3schools.com/howto/howto_js_scrolldrawing.asp
and can be seen here:
<svg style="min-height: 113.5%;" version="1.1" id="line_2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="4px" height="113.5%" xml:space="preserve">
<path style="max-height: 113.5%;" id="blue_line" fill="freeze" stroke-width="4" stroke="#3b7fdc" d="M0 0 v2884 400" />
</svg>
<script>
// Get the id of the <path> element and the length of <path>
var triangle = document.getElementById("blue_line");
var length = triangle.getTotalLength();
// The start position of the drawing
triangle.style.strokeDasharray = length;
// Hide the triangle by offsetting dash. Remove this line to show the triangle before scroll draw
triangle.style.strokeDashoffset = length;
// Find scroll percentage on scroll (using cross-browser properties), and offset dash same amount as percentage scrolled
window.addEventListener("scroll", myFunction);
function myFunction() {
var scrollpercent = (document.body.scrollTop + document.documentElement.scrollTop) / (document.documentElement.scrollHeight - document.documentElement.clientHeight);
var draw = length * scrollpercent;
// Reverse the drawing (when scrolling upwards)
triangle.style.strokeDashoffset = length - draw;
}
This works really well, however, currently, when the user scrolls back up the page, the SVG path 'reverses' back up the page with it. My desired effect is for the svg to only draw down the page and to not reverse when the user scrolls back up the page.
Does anyone know how to edit the script above to achieve this desired behaviour?
I've made a slight change to the script and it now does what you need...
<script>
// Get the id of the <path> element and the length of <path>
var triangle = document.getElementById("triangle");
var length = triangle.getTotalLength();
// The start position of the drawing
triangle.style.strokeDasharray = length;
// Hide the triangle by offsetting dash. Remove this line to show the triangle before scroll draw
triangle.style.strokeDashoffset = length;
// Find scroll percentage on scroll (using cross-browser properties), and offset dash same amount as percentage scrolled
window.addEventListener("scroll", myFunction);
var lastScrollpercent = 0;
function myFunction() {
var scrollpercent = (document.body.scrollTop + document.documentElement.scrollTop) / (document.documentElement.scrollHeight - document.documentElement.clientHeight);
if (scrollpercent < lastScrollpercent) return;
lastScrollpercent = scrollpercent;
var draw = length * scrollpercent;
// Reverse the drawing (when scrolling upwards)
triangle.style.strokeDashoffset = length - draw;
}
</script>
To summarise, I added lastScrollpercent and set it to 0. When the page is scrolled it works out the current percentage that it's been scrolled and only draws anything if that is currently more than lastScrollpercent, which it then updates (it's the value of scrollpercent last time anything was drawn). If the current scroll percent is less then it just returns (does nothing). That way, if you scroll up then it stops drawing, and if you then scroll down again it will only continue from where it left off.