Skip to content

Instantly share code, notes, and snippets.

@MohammadMD1383
Created June 1, 2021 13:15
Show Gist options
  • Save MohammadMD1383/ebe65c5cb28bb8ab8cdcf34e5de99000 to your computer and use it in GitHub Desktop.
Save MohammadMD1383/ebe65c5cb28bb8ab8cdcf34e5de99000 to your computer and use it in GitHub Desktop.
an Android like progress ring/bar for html
class AndroidProgressBar extends HTMLElement {
#root: ShadowRoot;
#svg: SVGSVGElement;
#circle: SVGCircleElement;
static #MIN_SPEED_RATE: number = 1.5;
static #MAX_SPEED_RATE: number = 3;
constructor() {
super();
this.#root = this.attachShadow({mode: "open"});
this.#root.innerHTML = `
<style>
div {
height: 100%;
width: 100%;
}
/*noinspection CssUnresolvedCustomProperty*/svg {
animation: round var(--min-speed) infinite linear;
}
/*noinspection CssUnresolvedCustomProperty*/circle {
transition: stroke-dashoffset 0.3s;
stroke-linecap: round;
stroke-dasharray: var(--offset) var(--offset);
animation: progress-bar var(--max-speed) infinite ease-in-out;
transform-origin: 50% 50%;
}
/*noinspection CssUnresolvedCustomProperty*/@keyframes progress-bar {
0% { stroke-dashoffset: var(--max-offset); }
45% { stroke-dashoffset: var(--min-offset); }
55% { stroke-dashoffset: var(--min-offset); transform: rotate(0deg); }
100% { stroke-dashoffset: var(--max-offset); transform: rotate(360deg); }
}
@keyframes round {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
<div>
<svg height="100%" width="100%">
<circle fill="transparent" cx="50%" cy="50%"/>
</svg>
</div>
`;
this.#circle = this.#root.querySelector("circle")!;
this.#svg = this.#root.querySelector("svg")!;
}
static get observedAttributes(): Array<string> {
return ["color", "thickness", "speed"];
}
applyDefaultStyles(): void {
this.style.display = "inline-block";
}
drawCircle(thickness: number) {
const r = Math.min(this.clientWidth, this.clientHeight) / 2 - thickness;
if (r > 0) this.#circle.setAttribute("r", r.toString());
const circumference = r * 2 * Math.PI;
this.#svg.style.setProperty("--offset", circumference.toString());
this.#svg.style.setProperty("--min-offset", (circumference * 20 / 100).toString());
this.#svg.style.setProperty("--max-offset", (circumference * 90 / 100).toString());
}
set thickness(t: string) {
const thickness = parseFloat(t);
if (thickness < 0) throw new Error("thickness property cannot be a negative value");
this.#circle.style.strokeWidth = thickness.toString();
this.drawCircle(thickness);
}
set color(c: string) {
this.#circle.style.stroke = c;
}
set speed(s: string) {
const speed = parseFloat(s);
if (speed < 0) throw new Error("speed property cannot be a negative value");
this.#svg.style.setProperty("--min-speed", `${AndroidProgressBar.#MIN_SPEED_RATE / speed}s`);
this.#svg.style.setProperty("--max-speed", `${AndroidProgressBar.#MAX_SPEED_RATE / speed}s`);
}
connectedCallback(): void {
this.applyDefaultStyles();
new ResizeObserver(this.dimenChangedCallBack).observe(this);
const color = this.getAttribute("color");
if (color) this.color = color; else throw new Error("color property is not defined");
const thickness = this.getAttribute("thickness");
if (thickness) this.thickness = thickness; else throw new Error("thickness property is not defined");
const speed = this.getAttribute("speed");
this.speed = speed || "1";
}
attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
if (name === "color") this.color = newValue;
if (name === "thickness") this.thickness = newValue;
if (name === "speed") this.speed = newValue;
}
dimenChangedCallBack(entries: ResizeObserverEntry[]) {
const _this = entries[0].target as AndroidProgressBar;
const thickness = parseFloat(_this.getAttribute("thickness")!);
if (thickness < 0) throw new Error("thickness property cannot be a negative value");
_this.drawCircle(thickness);
}
}
customElements.define("android-progress-bar", AndroidProgressBar);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment