wheel-duo
Version:
Animated dual-wheel component with customizable spin, sway, and callback support
190 lines (189 loc) • 7.63 kB
JavaScript
function a(t, e) {
const i = e.trim();
if (!i) throw new Error(`${t} must be a non-empty CSS selector`);
return i;
}
function n(t, e) {
if (!Number.isFinite(e)) throw new Error(`${t} must be a finite number`);
return e;
}
function l(t, e) {
const i = n(t, e);
if (i <= 0) throw new Error(`${t} must be greater than 0`);
return i;
}
function w(t) {
const e = a("rootSelector", t.rootSelector), i = a("firstWheelSelector", t.firstWheelSelector), s = a("secondWheelSelector", t.secondWheelSelector), o = a("triggerSelector", t.triggerSelector), c = [n("targetAngles[0]", t.targetAngles[0]), n("targetAngles[1]", t.targetAngles[1])], r = l("rotations", t.rotations ?? 6), h = l("duration", t.duration ?? 5e3), f = n("overshootDeg", t.overshootDeg ?? 15), m = l("returnDuration", t.returnDuration ?? 750), g = n("swayOptions.amplitude", t.swayOptions?.amplitude ?? 6), u = l("swayOptions.period", t.swayOptions?.period ?? 1500), d = e.startsWith(".") ? e.slice(1) : e;
return {
rootSelector: e,
firstWheelSelector: i,
secondWheelSelector: s,
triggerSelector: o,
targetAngles: c,
rotations: r,
duration: h,
overshootDeg: f,
returnDuration: m,
swayAmplitude: g,
swayPeriod: u,
callback: t.callback,
rootClassName: d
};
}
function S() {
return {
initialized: !1,
phase: "idle",
warmedElements: /* @__PURE__ */ new WeakSet(),
finalRotation: /* @__PURE__ */ new WeakMap()
};
}
function y(t) {
t.phase = "idle", t.warmedElements = /* @__PURE__ */ new WeakSet(), t.finalRotation = /* @__PURE__ */ new WeakMap();
}
var p = class {
domRefs = null;
swayAnimation = null;
swayingElement = null;
runtime = S();
config;
onFirstClick = () => {
this.runPhaseOne();
};
onSecondClick = () => {
this.runPhaseTwo();
};
constructor(t) {
this.config = w(t);
}
init() {
if (this.runtime.initialized) return;
const t = this.getRequiredElement(document, this.config.rootSelector, "rootSelector"), e = this.getRequiredElement(t, this.config.firstWheelSelector, "firstWheelSelector"), i = this.getRequiredElement(t, this.config.secondWheelSelector, "secondWheelSelector"), s = this.getRequiredElement(t, this.config.triggerSelector, "triggerSelector");
this.domRefs = {
root: t,
firstWheel: e,
secondWheel: i,
trigger: s
}, this.runtime.initialized = !0, this.runtime.phase = "idle", this.warmUp(e), this.warmUp(i), this.startSway(e), s.addEventListener("click", this.onFirstClick);
}
destroy() {
!this.runtime.initialized || !this.domRefs || (this.stopSway(), this.cancelAnimations(this.domRefs.firstWheel), this.cancelAnimations(this.domRefs.secondWheel), this.domRefs.trigger.removeEventListener("click", this.onFirstClick), this.domRefs.trigger.removeEventListener("click", this.onSecondClick), this.runtime.initialized = !1, this.runtime.phase = "idle");
}
reset() {
const t = this.getDomRefs();
this.destroy(), t.firstWheel.style.transform = "", t.secondWheel.style.transform = "", t.root.classList.remove(`${this.config.rootClassName}--state-one-active`, `${this.config.rootClassName}--state-one-complete`, `${this.config.rootClassName}--state-two-active`, `${this.config.rootClassName}--state-two-complete`), y(this.runtime), this.init();
}
warmUp(t) {
if (this.runtime.warmedElements.has(t)) return;
const e = t.animate([
{ filter: "blur(0)" },
{
filter: "blur(0.4px)",
offset: 0.5
},
{ filter: "blur(0)" }
], {
duration: 64,
fill: "forwards"
});
t.getBoundingClientRect(), e.onfinish = () => {
e.commitStyles?.(), e.cancel();
}, t.querySelectorAll("img").forEach((i) => {
i.decode?.().catch(() => {
});
}), this.runtime.warmedElements.add(t);
}
async runPhaseOne() {
if (this.runtime.phase !== "idle") return;
const t = this.getDomRefs(), [e] = this.config.targetAngles;
this.runtime.phase = "spinningFirst", t.root.classList.add(`${this.config.rootClassName}--state-one-active`), this.stopSway(), await this.rotateWheelTo(t.firstWheel, e), t.root.classList.replace(`${this.config.rootClassName}--state-one-active`, `${this.config.rootClassName}--state-one-complete`), this.startSway(t.secondWheel), t.trigger.removeEventListener("click", this.onFirstClick), t.trigger.addEventListener("click", this.onSecondClick, { once: !0 }), this.runtime.phase = "spinningSecond";
}
async runPhaseTwo() {
if (this.runtime.phase !== "spinningSecond") return;
const t = this.getDomRefs(), [, e] = this.config.targetAngles;
t.root.classList.add(`${this.config.rootClassName}--state-two-active`), this.stopSway(), await this.rotateWheelTo(t.secondWheel, e), t.root.classList.replace(`${this.config.rootClassName}--state-two-active`, `${this.config.rootClassName}--state-two-complete`), this.runtime.phase = "complete", this.config.callback?.();
}
async rotateWheelTo(t, e) {
const i = this.getCurrentRotation(t), s = (this.normalize(e) - this.normalize(i) + 360) % 360, o = i + this.config.rotations * 360 + s, c = o + this.config.overshootDeg, r = this.config.duration + this.config.returnDuration, h = this.config.duration / r, f = t.animate([
{
transform: `rotate(${i}deg)`,
easing: "cubic-bezier(0.86,0,0.07,1)"
},
{
offset: h,
transform: `rotate(${c}deg)`,
easing: "cubic-bezier(0.77,0,0.175,1)"
},
{ transform: `rotate(${o}deg)` }
], {
duration: r,
fill: "forwards"
}), m = t.animate([
{ filter: "blur(0)" },
{
offset: 0.2,
filter: "blur(1px)"
},
{
offset: 0.4,
filter: "blur(2px)"
},
{
offset: 0.65,
filter: "blur(1px)"
},
{
offset: 1,
filter: "blur(0)"
}
], {
duration: r,
fill: "forwards",
easing: "ease-in-out"
});
await Promise.all([f.finished, m.finished]), this.runtime.finalRotation.set(t, this.normalize(o));
}
startSway(t) {
this.stopSway(), this.swayingElement = t;
const e = this.runtime.finalRotation.get(t) ?? this.getCurrentRotation(t);
this.swayAnimation = t.animate([{ transform: `rotate(${e - this.config.swayAmplitude}deg)` }, { transform: `rotate(${e + this.config.swayAmplitude}deg)` }], {
duration: this.config.swayPeriod,
direction: "alternate",
iterations: 1 / 0,
easing: "ease-in-out",
delay: -this.config.swayPeriod / 2
});
}
stopSway() {
if (!this.swayAnimation || !this.swayingElement) return;
const t = getComputedStyle(this.swayingElement).transform;
this.swayAnimation.commitStyles?.(), this.swayAnimation.cancel(), this.swayingElement.style.transform = t !== "none" ? t : "", this.swayAnimation = null, this.swayingElement = null;
}
normalize(t) {
return (t % 360 + 360) % 360;
}
getCurrentRotation(t) {
const e = getComputedStyle(t).transform;
if (!e || e === "none") return 0;
const { a: i, b: s } = new DOMMatrixReadOnly(e);
return Math.atan2(s, i) * 180 / Math.PI;
}
cancelAnimations(t) {
t.getAnimations().forEach((e) => {
e.cancel();
});
}
getDomRefs() {
if (!this.domRefs) throw new Error("WheelDuo is not initialized. Call init() before using this method.");
return this.domRefs;
}
getRequiredElement(t, e, i) {
const s = t.querySelector(e);
if (!s) throw new Error(`WheelDuo init failed: element for "${i}" not found (${e})`);
return s;
}
};
export {
p as WheelDuo
};
//# sourceMappingURL=index.es.js.map