vue3-radial-progress
Version:
A smart and light radial progress bar component for Vue 3.
307 lines (280 loc) • 10.2 kB
JavaScript
import { defineComponent, reactive, ref, computed, watch, openBlock, createElementBlock, normalizeStyle, createElementVNode, renderSlot } from 'vue';
function randomString() {
return Math.random().toString(16).substring(2);
}
var script = defineComponent({
props: {
// Sets width/diameter of the inner stroke.
diameter: {
type: Number,
required: false,
default: 200
},
// Sets the total steps/progress to the end.
totalSteps: {
type: Number,
required: true,
default: 10
},
// Sets the current progress of the inner stroke.
completedSteps: {
type: Number,
required: true,
default: 0
},
// Sets the start color of the inner stroke (gradient).
startColor: {
type: String,
required: false,
default: "#00C58E"
},
// Sets the end color of the inner stroke (gradient).
stopColor: {
type: String,
required: false,
default: "#00E0A1"
},
// Sets the color of the inner stroke to be applied to the shape.
innerStrokeColor: {
type: String,
required: false,
default: "#2F495E"
},
// Sets the width of the stroke to be applied to the shape.
// Read more: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-width
strokeWidth: {
type: Number,
required: false,
default: 10
},
// Sets the width of the inner stroke to be applied to the shape.
innerStrokeWidth: {
type: Number,
required: false,
default: 10
},
// Sets the shape to be used at the end of stroked.
// Read more: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-linecap
strokeLinecap: {
type: String,
required: false,
default: "round"
},
// Sets how long the animation should take to complete one cycle.
// Read more: https://www.w3schools.com/cssref/css3_pr_animation-duration.asp
animateSpeed: {
type: Number,
required: false,
default: 1000
},
// Sets the frames per seconds to update inner stroke animation.
fps: {
type: Number,
required: false,
default: 60
},
// Sets how the animation progresses through the duration of each cycle.
// Read more: https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timing-function
timingFunc: {
type: String,
required: false,
default: "linear"
},
// Sets the inner stroke direction.
isClockwise: {
type: Boolean,
required: false,
default: true
}
},
setup(props) {
const gradient = reactive({
fx: 0.99,
fy: 0.5,
cx: 0.5,
cy: 0.5,
r: 0.65
});
const radialGradientId = `rg-${randomString()}`;
const strokeDashoffset = ref(0);
const currentAngle = ref(0);
const gradientAnimation = ref(null);
const radius = computed(() => props.diameter / 2);
const innerCircleDiameter = computed(() => props.diameter - props.innerStrokeWidth * 2);
const circumference = computed(() => Math.PI * innerCircleDiameter.value);
const stepSize = computed(() => props.totalSteps === 0 ? 0 : 100 / props.totalSteps);
const finishedPercentage = computed(() => stepSize.value * props.completedSteps);
const circleSlice = computed(() => 2 * Math.PI / props.totalSteps);
const animationIncrements = computed(() => 100 / props.fps);
const totalPoints = computed(() => props.animateSpeed / animationIncrements.value);
const animateSlice = computed(() => circleSlice.value / totalPoints.value);
const innerCircleRadius = computed(() => innerCircleDiameter.value / 2);
const containerStyle = computed(() => ({
height: `${props.diameter}px`,
width: `${props.diameter}px`
}));
const progressStyle = computed(() => ({
height: `${props.diameter}px`,
width: `${props.diameter}px`,
strokeWidth: `${props.strokeWidth}px`,
strokeDashoffset: strokeDashoffset.value,
transition: `stroke-dashoffset ${props.animateSpeed}ms ${props.timingFunc}`
}));
const strokeStyle = computed(() => ({
height: `${props.diameter}px`,
width: `${props.diameter}px`,
strokeWidth: `${props.innerStrokeWidth}px`
}));
const innerCircleStyle = computed(() => ({
width: `${innerCircleDiameter.value}px`
}));
watch(() => [props.diameter, props.totalSteps, props.completedSteps, props.strokeWidth], changeProgress, {
immediate: true
});
function getPointOfCircle(angle) {
const radius = 0.5;
const x = radius + radius * Math.cos(angle);
const y = radius + radius * Math.sin(angle);
return {
x,
y
};
}
function gotoPoint() {
const point = getPointOfCircle(currentAngle.value);
if (point.x && point.y) {
gradient.fx = point.x;
gradient.fy = point.y;
}
}
function direction() {
return props.isClockwise ? 1 : -1;
}
function changeProgress() {
strokeDashoffset.value = (100 - finishedPercentage.value) / 100 * circumference.value * direction();
if (gradientAnimation.value) {
clearInterval(gradientAnimation.value);
}
const angleOffset = (props.completedSteps - 1) * circleSlice.value;
let i = (currentAngle.value - angleOffset) / animateSlice.value;
const incrementer = Math.abs(i - totalPoints.value) / totalPoints.value;
const isMoveForward = i < totalPoints.value;
gradientAnimation.value = setInterval(() => {
if (isMoveForward && i >= totalPoints.value || !isMoveForward && i < totalPoints.value) {
gradientAnimation.value && clearInterval(gradientAnimation.value);
return;
}
currentAngle.value = angleOffset + animateSlice.value * i;
gotoPoint();
i += isMoveForward ? incrementer : -incrementer;
}, animationIncrements.value);
}
return {
gradientAnimation,
innerCircleRadius,
radialGradientId,
strokeDashoffset,
innerCircleStyle,
containerStyle,
circumference,
progressStyle,
currentAngle,
strokeStyle,
gradient,
radius
};
}
});
const _hoisted_1 = ["width", "height"];
const _hoisted_2 = ["id", "fx", "fy", "cx", "cy", "r"];
const _hoisted_3 = ["stop-color"];
const _hoisted_4 = ["stop-color"];
const _hoisted_5 = ["r", "cx", "cy", "stroke", "stroke-dasharray", "stroke-linecap"];
const _hoisted_6 = ["transform", "r", "cx", "cy", "stroke", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap"];
function render(_ctx, _cache, $props, $setup, $data, $options) {
return openBlock(), createElementBlock("div", {
class: "vrp__wrapper",
style: normalizeStyle(_ctx.containerStyle)
}, [createElementVNode("div", {
class: "vrp__inner",
style: normalizeStyle(_ctx.innerCircleStyle)
}, [renderSlot(_ctx.$slots, "default")], 4), (openBlock(), createElementBlock("svg", {
width: _ctx.diameter,
height: _ctx.diameter,
version: "1.1",
xmlns: "http://www.w3.org/2000/svg"
}, [createElementVNode("defs", null, [createElementVNode("radialGradient", {
id: _ctx.radialGradientId,
fx: _ctx.gradient.fx,
fy: _ctx.gradient.fy,
cx: _ctx.gradient.cx,
cy: _ctx.gradient.cy,
r: _ctx.gradient.r
}, [createElementVNode("stop", {
offset: "30%",
"stop-color": _ctx.startColor
}, null, 8, _hoisted_3), createElementVNode("stop", {
offset: "100%",
"stop-color": _ctx.stopColor
}, null, 8, _hoisted_4)], 8, _hoisted_2)]), createElementVNode("circle", {
r: _ctx.innerCircleRadius,
cx: _ctx.radius,
cy: _ctx.radius,
fill: "transparent",
stroke: _ctx.innerStrokeColor,
"stroke-dasharray": _ctx.circumference,
"stroke-dashoffset": "0",
"stroke-linecap": _ctx.strokeLinecap,
style: normalizeStyle(_ctx.strokeStyle)
}, null, 12, _hoisted_5), createElementVNode("circle", {
transform: 'rotate(270, ' + _ctx.radius + ',' + _ctx.radius + ')',
r: _ctx.innerCircleRadius,
cx: _ctx.radius,
cy: _ctx.radius,
fill: "transparent",
stroke: `url('#${_ctx.radialGradientId}')`,
"stroke-dasharray": _ctx.circumference,
"stroke-dashoffset": _ctx.circumference,
"stroke-linecap": _ctx.strokeLinecap,
style: normalizeStyle(_ctx.progressStyle)
}, null, 12, _hoisted_6)], 8, _hoisted_1))], 4);
}
function styleInject(css, ref) {
if ( ref === void 0 ) ref = {};
var insertAt = ref.insertAt;
if (!css || typeof document === 'undefined') { return; }
var head = document.head || document.getElementsByTagName('head')[0];
var style = document.createElement('style');
style.type = 'text/css';
if (insertAt === 'top') {
if (head.firstChild) {
head.insertBefore(style, head.firstChild);
} else {
head.appendChild(style);
}
} else {
head.appendChild(style);
}
if (style.styleSheet) {
style.styleSheet.cssText = css;
} else {
style.appendChild(document.createTextNode(css));
}
}
var css_248z = "\n.vrp__wrapper[data-v-6a0cf1f6] {\r\n position: relative;\n}\n.vrp__inner[data-v-6a0cf1f6] {\r\n position: absolute;\r\n top: 0;\r\n right: 0;\r\n bottom: 0;\r\n left: 0;\r\n border-radius: 50%;\r\n margin: 0 auto;\r\n display: flex;\r\n flex-direction: column;\r\n align-items: center;\r\n justify-content: center;\n}\r\n";
styleInject(css_248z);
script.render = render;
script.__scopeId = "data-v-6a0cf1f6";
// IIFE injects install function into component, allowing component
// to be registered via Vue.use() as well as Vue.component(),
var entry_esm = /*#__PURE__*/(() => {
// Assign InstallableComponent type
const installable = script; // Attach install function executed by Vue.use()
installable.install = app => {
app.component("RadialProgressBar", installable);
};
return installable;
})(); // It's possible to expose named exports when writing components that can
// also be used as directives, etc. - eg. import { RollupDemoDirective } from 'rollup-demo';
// export const RollupDemoDirective = directive;
export { entry_esm as default };