I am trying to create a progress bar (arc) with SVG. I currently have the progress bar working, it is moving the desired amount using a value stored in a data attribute, and looks pretty good. although i am trying to get an image to move around the arc with the bar. The image should start at 0 with the bar and move around to the completion point, say 50% which will be at the top.
<div class="w-100 case-progress-bar input p-2" style="position: relative;" data-percentage="80">
<svg class='progress_bar' viewBox="0 0 100 50" >
<g fill-opacity="0" stroke-width="4">
<path d="M5 50a45 45 0 1 1 90 0" stroke="#EBEDF8"></path>
<path class="progress" d="M5 50a45 45 0 1 1 90 0" stroke="#f00" stroke-dasharray="142" stroke-dashoffset="142"></path>
</g>
<circle fill="url(#image)" id='case_progress__prog_fill' class="case_progress__prog" cx="0" cy="0" r="8" fill="#999" stroke="#fff" stroke-width="1" />
<defs>
<pattern id="image" x="0%" y="0%" height="100%" width="100%" viewBox="0 0 60 60">
<image x="0%" y="0%" width="60" height="60" xlink:href="https://via.placeholder.com/150x150"></image>
</pattern>
</defs>
</svg>
</div>
(function(){
var $wrapper = $('.case-progress-bar'),
$bar = $wrapper.find('.progress_bar'),
$progress = $bar.find('.progress'),
$percentage = $wrapper.data('percentage');
$progress.css({
'stroke-dashoffset': 'calc(142 - (0 * 142 / 100))',
'transition': 'all 1s'
});
var to = setTimeout(function(){
$progress.css('stroke-dashoffset', 'calc(142 - (' + $percentage + ' * 142 / 100))');
clearTimeout(to);
}, 500);
})();
this is what I currently have
this is what i'm trying to achieve
To solve, you need to combine two animations:
Painting half of the arc from the beginning to the middle (top)
Animation of movement of a circle with an image inside
Set the same time for both animations
<div class="w-100 case-progress-bar input p-2" style="position: relative;" data-percentage="80">
<svg class='progress_bar' viewBox="0 0 100 50" >
<g fill-opacity="0" stroke-width="4">
<path id="pfad" d="M5 50C5 44.1 6.1 38.5 8.2 33.4 10.8 26.8 14.9 20.9 20.2 16.3 28.1 9.3 38.6 5 50 5" stroke="#EBEDF8"></path>
<path d="M5 50a45 45 0 1 1 90 0" stroke="#EBEDF8"></path>
<!-- Animation to fill half an arc -->
<path class="progress" d="M5 50a45 45 0 1 1 90 0" stroke="#f00" stroke-dasharray="142" stroke-dashoffset="142">
<animate attributeName="stroke-dashoffset" from="142" to="71" dur="4s" fill="freeze" />
</path>
</g>
<defs>
<pattern id="image" x="0%" y="0%" height="100%" width="100%" viewBox="0 0 60 60">
<image x="0%" y="0%" width="60" height="60" xlink:href="https://via.placeholder.com/150x150"></image>
</pattern>
</defs>
<circle fill="url(#image)" id='case_progress__prog_fill' class="case_progress__prog" cx="0" cy="0" r="8" fill="#999" stroke="#fff" stroke-width="1" >
<!-- Animation of movement of a circle with an image -->
<animateMotion begin="0s" dur="4s" fill="freeze">
<mpath xlink:href="#pfad" />
</animateMotion>
</circle>
</svg>
</div>
This is a case where it has advantages not to use thestroke-dasharray trick.
SVG can draw a marker at the end of a path. That marker can be any sort of grafic, and its syntax is just like that of a <symbol>. The position of the marker is defined by the path d attribute, and not influenced by a dashed stroke.
The general strategy is to compute the endpoint of the path
endpoint_x = center_x - cos(percentage / 100 * 180°) * radius
endpoint_y = center_y - sin(percentage / 100 * 180°) * radius
It is possible to do so relatively seamlessly because you decided to use only a half-circle to represent 100%. I have changed the way the path data are written to make that possible:
`M5 50 A 45 45 0 ${large} 1 ${x} ${y}`
A means: draw an arc and use absolute coordinates.
45 45 0 use a rx of 45, a ry of 45, do not rotate the axis of the arc.
${large} is the important bit. It discerns arcs of less than 180° from those that have more than 180°. As soon as that value would be crossed, the flag must change from 0 to 1. But since you are never expecting values above 180°, you would not need it.
1 means looking in the direction of the path, the arc should be drawn to the left side.
${x} ${y} are the final coordinates expressed in absolute, not relative coordinates.
The <marker> element has a number of attributes that must be considered:
orient="0" means the marker will not change its direction with the direction of the path at its end. orient="auto" would make it turn around , as you would like to see with an arrow for example.
markerUnits="userSpaceOnUse" means the numbers in the other attributes are in px units of the coordinate system of the path. Default would be markerUnits="strokeWidth", which would mean a size relative to the width of the stroke.
viewBox="-8 -8 16 16" is choosen because the circle used is centered around the coordinate system origin.
markerWidth="16" markerHeight="16" is saying how large the marker should be drawn.
refX="0" refY="-10" describes how the marker should be positioned: Take a point in the coordinate system of the marker itself (slightly above its topmost point and in the middle), and align it exactly with the end of the path.
Finally, note the marker-end="url(#image)" presentation attribute for the path. This is what sets the marker, and defines that it will be at the end of the path.
(function(){
var $wrapper = $('.case-progress-bar'),
$bar = $wrapper.find('.progress_bar'),
$progress = $bar.find('.progress'),
$percentage = $wrapper.data('percentage');
function computePath (percentage) {
var x = 50 - Math.cos(percentage / 100 * Math.PI) * 45,
y = 50 - Math.sin(percentage / 100 * Math.PI) * 45,
large = percentage > 100 ? 1 : 0;
return `M5 50 A 45 45 0 ${large} 1 ${x} ${y}`;
}
var to = setTimeout(function(){
$progress.attr('d', computePath($percentage));
clearTimeout(to);
}, 500);
})();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="w-100 case-progress-bar input p-2" style="position: relative;" data-percentage="80">
<svg class='progress_bar' viewBox="0 0 100 70" >
<g fill-opacity="0" stroke-width="4">
<path d="M5 50a45 45 0 1 1 90 0" stroke="#EBEDF8"></path>
<path class="progress" d="M5 50 A 45 45 0 0 1 95 50`" stroke="#f00"
marker-end="url(#image)"></path>
</g>
<marker id="image" orient="0" markerUnits="userSpaceOnUse"
viewBox="-8 -8 16 16"
markerWidth="16" markerHeight="16"
refX="0" refY="-10">
<circle r="8" fill="#aaf" />
<path d="M-6 0h12" stroke="#000" stroke-width="2" />
</marker>
</svg>
</div>
Just use a little JavaScript since you can't do trigonometry in CSS easily yet.
Codepen: https://codepen.io/mullany/pen/02cd0773588b3d975c8443ab6a87f670
(function(){
var $wrapper = $('.case-progress-bar'),
$bar = $wrapper.find('.progress_bar'),
$progress = $bar.find('.progress'),
$percentage = $wrapper.data('percentage');
var $circpos = $('.case_progress__prog');
$progress.css({
'stroke-dashoffset': 'calc(142 - (0 * 142 / 100))',
'transition': 'all 1s'
});
var to = setTimeout(function(){
$progress.css('stroke-dashoffset', 'calc(142 - (' + $percentage + ' * 142 / 100))');
var angleInRadians = 180*(1-$percentage/100) * 0.01745329251;
var xPos = 5 + 45 * (1 + Math.cos(angleInRadians) );
var yPos = 5 + 45 * (1 - Math.sin(angleInRadians) );
$circpos.css('cx', xPos);
$circpos.css('cy', yPos);
clearTimeout(to);
}, 500);
})();
I had a very similarly shaped loading SVG. You cen see it in action here. The green one at the bottom of the main banner.
What I used was the ProgressBar.js library. It made it very easy. I just focused on adjusting my SVG in terms of shape, because of the design of my site and then just used:
<svg id="progress-bar" xmlns="http://www.w3.org/2000/svg" width="650" height="526" viewBox="0 0 650 526">
<path opacity=".55" id="progress-bar-base" fill="none" stroke="#fefefe" stroke-width="20" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M84.591 512.622S20 441.232 20 324.941 79.903 131.327 137.4 84.476 269.116 20 324.94 20s124.217 15.743 184.57 62.183 89.159 101.578 103.475 142.419c14.316 40.84 25.203 105.21 8.788 170.477-16.415 65.268-43.628 101.409-56.482 117.543"></path>
<linearGradient id="grade" gradientUnits="userSpaceOnUse" x1="592.08" y1="157.665" x2="46.149" y2="472.859">
<stop offset="0" stop-color="#2cab9a"></stop>
<stop offset="1" stop-color="#8ed1c3"></stop>
</linearGradient>
<path fill-opacity="0" id="progress-bar-indicator" stroke="url(#grade)" stroke-width="24" stroke-linecap="round" stroke-linejoin="round" stroke-miterlimit="10" d="M84.591 512.622S20 441.232 20 324.941 79.903 131.327 137.4 84.476 269.116 20 324.94 20s124.217 15.743 184.57 62.183 89.159 101.578 103.475 142.419c14.316 40.84 25.203 105.21 8.788 170.477-16.415 65.268-43.628 101.409-56.482 117.543" style="stroke-dasharray: 1362.73, 1362.73; stroke-dashoffset: 1140.45;"></path>
</svg>
var progressBar = new ProgressBar.Path('#progress-bar-indicator', {
easing: 'easeInOut',
duration: 2500
});
progressBar.set(0); // Initiate it at zero.
progressBar.animate(0.33); // this is your percentage of load
It's a very basic example, but I think you could adjust it to your needs.
As for the image, perhaps you could add it to your own SVG at the tip of the loading curve and it should move with it I think.
I've been having some issue getting the path of my SVG to animate using stroke-dasharray in conjunction with stroke-dashoffset. The path length was calculated with Js. Below I have included a JsFiddle showing exactly what I am trying to accomplish.
I have searched and searched, and a lot of the examples I came across did not work for me. At this point I am concluding that I am missing something, but I am at my wits end trying to figure out what that is.
<div>
<svg x="0px" y="0px" width="312px" height="312px" viewBox="0 0 512 512">
<g>
<path class="pathOne" d="M320,128c52.562,0,95.375,42.438,96,94.813c-0.25,1.938-0.438,3.875-0.5,5.875l-0.812,23.5l22.25,7.75 C462.688,268.906,480,293.062,480,320c0,35.312-28.688,64-64,64H96c-35.281,0-64-28.688-64-64c0-34.938,28.188-63.438,63-64 c1.5,0.219,3.063,0.406,4.625,0.5l24.313,1.594l8-22.969C140.938,209.313,165.063,192,192,192c3.125,0,6.563,0.375,11.188,1.188 l22.406,4.031l11.156-19.844C253.875,146.938,285.75,128,320,128 M320,96c-47.938,0-89.219,26.688-111.156,65.688 C203.375,160.719,197.781,160,192,160c-41.938,0-77.219,27.063-90.281,64.563C99.813,224.438,97.969,224,96,224c-53,0-96,43-96,96 s43,96,96,96h320c53,0,96-43,96-96c0-41.938-27.062-77.25-64.562-90.313C447.5,227.75,448,225.938,448,224 C448,153.313,390.688,96,320,96L320,96z" fill="#333333"/>
</g>
</svg>
</div>
svg {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
path.pathOne {
stroke-dasharray: 2566;
stroke-dataoffset: 2566;
animation: cloud 5s linear alternate infinite;
}
#keyframes cloud {
from {
stroke-dashoffset: 2566;
}
to {
stroke-dashoffset: 0;
}
}
https://jsfiddle.net/maisonm/9c3baxh5/
Your grafic does not show the stroke of a path, but a filled path without a stroke. Here is a illustration of your path data as you could see them in the svg editor Inkscape:
That is the path you could animate. Was that what you wanted? In that case, define a stroke and remove the fill:
path.pathOne {
fill: none;
stroke: black;
stroke-width: 3;
stroke-dasharray: 2566;
stroke-dataoffset: 2566;
animation: cloud 5s linear alternate infinite;
}
#keyframes cloud {
from {
stroke-dashoffset: 2566;
}
to {
stroke-dashoffset: 0;
}
}
<svg x="0px" y="0px" width="250px" height="250px" viewBox="0 0 512 512">
<g>
<path class="pathOne" d="M320,128c52.562,0,95.375,42.438,96,94.813c-0.25,1.938-0.438,3.875-0.5,5.875l-0.812,23.5l22.25,7.75 C462.688,268.906,480,293.062,480,320c0,35.312-28.688,64-64,64H96c-35.281,0-64-28.688-64-64c0-34.938,28.188-63.438,63-64 c1.5,0.219,3.063,0.406,4.625,0.5l24.313,1.594l8-22.969C140.938,209.313,165.063,192,192,192c3.125,0,6.563,0.375,11.188,1.188 l22.406,4.031l11.156-19.844C253.875,146.938,285.75,128,320,128 M320,96c-47.938,0-89.219,26.688-111.156,65.688 C203.375,160.719,197.781,160,192,160c-41.938,0-77.219,27.063-90.281,64.563C99.813,224.438,97.969,224,96,224c-53,0-96,43-96,96 s43,96,96,96h320c53,0,96-43,96-96c0-41.938-27.062-77.25-64.562-90.313C447.5,227.75,448,225.938,448,224 C448,153.313,390.688,96,320,96L320,96z" fill="#333333"/>
</g>
</svg>
Note that both subpaths are dashed on their own, resulting in the impression that there are two animations. But that is not what is happening, it's simply that the dash and the offset are applied to each subpath separately.
Since the path length is computed for the total of both subpaths, you see a time lag before the animation repeats.
But maybe what you were aiming for was this?
Handwriting solution
When an SVG shape has a double path with different distances between the paths and the shape at the points of change of direction, then it is better to use the technique - Handwriting to animate drawing the paths.
Its essence is to animate the middle line, which is wide enough to cover the widest parts of the original shape.
This middle line must be drawn in a vector editor and saved as path
Next, animate this path by drawing a line from maximum to zero . Add this element to the mask and apply a mask to the original shape of the cloud outline.
<animate attributeName="stroke-dasharray"
begin="svg1.click" dur="4s" values="1281,0;0,1281" fill="freeze" />
where 1281 maximum centerline length
The length of the animated mask changes to reveal the original outline of the cloud.
At the same time, creating the illusion of drawing lines with different widths and shapes.
<svg id="svg1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="312" height="312" viewBox="0 0 512 512" >
<defs>
<mask id="msk">
<rect width="100%" height="100%" fill="white" />
<!-- middle line -->
<path stroke="#111" stroke-dashoffset="700" stroke-width="45" fill="none" d="m262.3 400c56 0.2 111.3 0 168 0 14.8 0 28.2-10.8 39.1-20.7 9-8.2 15.8-19.2 19.9-30.7 5.2-14.5 9.1-31 5.4-46-4.8-19.1-18.9-35.5-33.7-48.3-7.8-6.7-24.3-4.1-27.6-13.8-5.7-16.5-1.3-35.8-7.7-52.2-7.1-18.4-17.9-36.4-33-49.1-18.4-15.4-42-27.5-66-29.1-24.1-1.7-49.5 6.8-69.8 19.9-16.9 10.9-21.1 35.5-39.1 46-12.4 6.2-27.9-0.8-41.4 2.3-13 3-26.9 6.5-36.8 15.3-13.2 11.7-14.3 25-26.1 46-3.1 5.6-12.9 0.2-19.2 1.5-15.4 3.2-31.9 5.9-44.5 15.3-15.3 11.5-27.7 28.5-33 46.8-4 14-4.3 30.7 2.3 43.7 12.1 24 35.3 50.8 62.1 51.4 65.1 1.5 120.7 1.3 181 1.5z">
<!-- A mask animation with a wide line that reveals the outlines of the cloud -->
<animate id="an" attributeName="stroke-dasharray" begin="svg1.click" dur="8s" values="1281,0;0,1281" fill="freeze" repeatCount="1" />
</path>
</mask>
</defs>
<!-- Cloud contours -->
<path mask="url(#msk)" fill="dodgerblue" class="pathOne" d="m320 128c52.6 0 95.4 42.4 96 94.8-0.2 1.9-0.4 3.9-0.5 5.9l-0.8 23.5 22.3 7.8C462.7 268.9 480 293.1 480 320c0 35.3-28.7 64-64 64H96c-35.3 0-64-28.7-64-64 0-34.9 28.2-63.4 63-64 1.5 0.2 3.1 0.4 4.6 0.5l24.3 1.6 8-23C140.9 209.3 165.1 192 192 192c3.1 0 6.6 0.4 11.2 1.2l22.4 4 11.2-19.8C253.9 146.9 285.8 128 320 128m0-32C272.1 96 230.8 122.7 208.8 161.7 203.4 160.7 197.8 160 192 160 150.1 160 114.8 187.1 101.7 224.6 99.8 224.4 98 224 96 224 43 224 0 267 0 320c0 53 43 96 96 96h320c53 0 96-43 96-96 0-41.9-27.1-77.2-64.6-90.3C447.5 227.8 448 225.9 448 224 448 153.3 390.7 96 320 96Z"/>
<text x="200" y="300" font-size="36px" fill="dodgerblue" > Click me
<animate id="op" attributeName="opacity" begin="an.end" dur="1s" values="1;0" fill="freeze" repeatCount="1" />
</text>
</svg>
Option with repeated animation after a 2s pause
To do this, add a restart condition after a pause of 2s:
begin="svg1.click;an.end+2s"
<svg id="svg1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0" y="0" width="312" height="312" viewBox="0 0 512 512" >
<defs>
<mask id="msk">
<rect width="100%" height="100%" fill="white" />
<!-- middle line -->
<path stroke="#111" stroke-dashoffset="700" stroke-width="45" fill="none" d="m262.3 400c56 0.2 111.3 0 168 0 14.8 0 28.2-10.8 39.1-20.7 9-8.2 15.8-19.2 19.9-30.7 5.2-14.5 9.1-31 5.4-46-4.8-19.1-18.9-35.5-33.7-48.3-7.8-6.7-24.3-4.1-27.6-13.8-5.7-16.5-1.3-35.8-7.7-52.2-7.1-18.4-17.9-36.4-33-49.1-18.4-15.4-42-27.5-66-29.1-24.1-1.7-49.5 6.8-69.8 19.9-16.9 10.9-21.1 35.5-39.1 46-12.4 6.2-27.9-0.8-41.4 2.3-13 3-26.9 6.5-36.8 15.3-13.2 11.7-14.3 25-26.1 46-3.1 5.6-12.9 0.2-19.2 1.5-15.4 3.2-31.9 5.9-44.5 15.3-15.3 11.5-27.7 28.5-33 46.8-4 14-4.3 30.7 2.3 43.7 12.1 24 35.3 50.8 62.1 51.4 65.1 1.5 120.7 1.3 181 1.5z">
<!-- A mask animation with a wide line that reveals the outlines of the cloud -->
<animate id="an" attributeName="stroke-dasharray" begin="svg1.click;an.end+2s" dur="6s" values="1281,0;0,1281" fill="freeze" />
</path>
</mask>
</defs>
<!-- Cloud contours -->
<path mask="url(#msk)" fill="dodgerblue" class="pathOne" d="m320 128c52.6 0 95.4 42.4 96 94.8-0.2 1.9-0.4 3.9-0.5 5.9l-0.8 23.5 22.3 7.8C462.7 268.9 480 293.1 480 320c0 35.3-28.7 64-64 64H96c-35.3 0-64-28.7-64-64 0-34.9 28.2-63.4 63-64 1.5 0.2 3.1 0.4 4.6 0.5l24.3 1.6 8-23C140.9 209.3 165.1 192 192 192c3.1 0 6.6 0.4 11.2 1.2l22.4 4 11.2-19.8C253.9 146.9 285.8 128 320 128m0-32C272.1 96 230.8 122.7 208.8 161.7 203.4 160.7 197.8 160 192 160 150.1 160 114.8 187.1 101.7 224.6 99.8 224.4 98 224 96 224 43 224 0 267 0 320c0 53 43 96 96 96h320c53 0 96-43 96-96 0-41.9-27.1-77.2-64.6-90.3C447.5 227.8 448 225.9 448 224 448 153.3 390.7 96 320 96Z"/>
<text x="200" y="300" font-size="36px" fill="dodgerblue" > Click me
<animate id="op" attributeName="opacity" begin="6s" dur="1s" values="1;0" fill="freeze" />
</text>
</svg>