Effortless smooth values and ranges sliders
const slide = new Slider("#slider", {
val: 50, // initial value
min: 0, // min value
max: 100, // max value
col: colors(1) // color
});const slide = new Slider("#slider", {
val: 50,
max: 100,
stp: 10, // step size
col: colors(1)
});const slide = new Slider("#slider", {
max: 100,
val: [8, 16, 32, 64], // 4 values
col: colors(4), // 4 colors
});// two values + one color = range
const slide = new Slider("#slider", {
val: [25, 75], // initial range
col: colors(1) // single color
});// minimum range = 10
const slide = new Slider("#slider", {
val: [25, 75],
rng: 10, // min range
col: colors(1)
});// n values + n / 2 colors = ranges
const slide = new Slider("#slider", {
val: [5, 10, 20, 35, 50, 80], // 6 values
col: colors(3), // 3 colors
});// n values + (n - 1) colors = linked ranges
const slide = new Slider("#slider", {
val: [10, 50, 70], // 3 values
col: colors(2) // 2 colors
});// 3x linked ranges
const slide = new Slider("#slider", {
val: [10, 20, 50, 70], // 4 values
col: colors(3) // 3 colors
});// time format function
const slide = new Slider("#slider", {
max: 5400, // 1 hour 30 minutes
val: 3600, // 1 hour
col: colors(1),
fmt: s => // format function
(s < 3600 ? [60, 1] : [3600, 60, 1])
.map(x => `0${~~(s / x) % 60}`.slice(-2))
.join(":")
});// date format function
const slide = new Slider("#slider", {
min: new Date( // January 1st
new Date().getFullYear(), 0, 1
).getTime(),
max: new Date( // December 31
new Date().getFullYear(), 11, 31
).getTime(),
stp: 86400000, // 1 day step
rng: 86400000, // 1 day min range
col: colors(), // range color
val: [ // initial range = [January 1st, now]
new Date(new Date().getFullYear(), 0, 1),
Date.now()
],
fmt: s => // format function
new Date(s).toLocaleDateString()
});const slide = new Slider("#slider", {
min: 0,
max: 0.1,
stp: 0.001,
val: [0.016, 0.032, 0.064],
col: colors(3)
});const slide = new Slider("#slider", {
min: 65,
max: 90,
rng: 1,
col: colors(),
val: [65, 70],
fmt: s =>
String.fromCharCode(s)
});<div id="vals"></div>
<div id="range"></div>
<div id="links"></div>
<div class="slides"></div>body {
display: flex;
justify-content: space-evenly;
align-items: center;
}
.slider {
line-height: 1;
}
.slider.vert {
height: 240px;
}
.slides {
display: none;
}const vals = new Slider("#vals", {
max: 64,
val: [8, 16, 32],
col: colors(3),
dir: 1
});
const range = new Slider("#range", {
max: 64,
val: [16, 32],
col: colors(),
dir: 1
});
const links = new Slider("#links", {
min: 1,
max: 9,
stp: 1,
val: [2, 4, 6],
col: colors(2),
dir: 1
});const colors = (num = 1) => [
"ff595e", "ff924c", "ffca3a",
"c5ca30", "8ac926", "52a675",
"1982c4", "4267ac", "6a4c93"]
.sort(() => Math.random() - 0.5)
.slice(0, num)
.map(col => "#" + col);:root {
--slideSize: 36px; /* slider size */
--trackSize: 6px; /* slide track size */
--focusSize: 10px; /* sliding track size */
--trackColor: #e5e7eb; /* track background color */
--textColor: #6b7280; /* min & max labels color */
--fontSize: 14px; /* labels font size */
--animate: 0.2s ease; /* sliding state animation */
}<div id="slider"></div>
<div class="slides"></div>body {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.slider {
line-height: 1;
width: 360px;
max-width: 360px;
max-width: 90%;
}
.slider.vert {
height: 240px;
}
.slides {
font-size: var(--fontSize);
user-select: none;
-webkit-user-select: none;
pointer-events: none;
}const slides = document
.querySelector(".slides");
const disp = dat =>
slides.innerHTML = [dat]
.flat()
.map(
(val, idx) =>
`<font color="${slide.colors[idx]}">${JSON.stringify(val)}</font>`
)
.join(" ");
slide
.on(
"slide",
evt =>
disp(evt.detail)
);
slide
.on(
"change",
evt =>
disp(evt.detail)
);
disp(slide.values);/**
* @force
* @export
* @class Slider :
*/
class Slider {
/**
* @construct
* @param {HTMLElement|string} container :
* @param {!SliderOptions=} opts :
*/
constructor(container, opts = {}) {
opts = {
min: 0,
max: 100,
stp: 1,
num: 1,
col: "#4A90E2",
fmt: v =>
v.toFixed(this.dec),
val: null,
rng: 0,
dir: 0,
...opts
};
this.track
= this.ranges
= this.valueBox
= this.labelBox
= this.minLabel
= this.maxLabel
= this.valueLabels
= this._frameLoop
= null;
this.dec = String(opts.stp)
.includes(".") ? String(opts.stp)
.split(".")[1].length : 0;
this.wrap = typeof container === "string" ? document.querySelector(container) : container;
this._dir = opts.dir;
this._step = opts.stp;
this._range = opts.max - opts.min;
this._disp = opts.fmt;
this._colors = [opts.col].flat();
this._ranged = opts.rng;
this._min = opts.min;
this._max = opts.max;
this._activePointers = new Map();
this._isMoving = false;
this._frameLoop = null;
const values = opts.val !== null ? [opts.val].flat() : this._defaultValues(opts.num);
this._num = values.length;
this._vals = Array(this._num)
.fill(0);
this._targetVals = Array(this._num)
.fill(0);
this._evts = new NativeEventTarget();
this._createElements();
this._setupEvents();
this.values = values;
}
_createElements() {
const c = (cls = "") =>
Object.assign(
document.createElement("div"),
{
className: cls
}
);
this.wrap.classList.add("slider");
if(this._dir)
this.wrap.classList.add("vert");
// this.wrap.tabIndex = 0;
this.track = c("track");
this.valueBox = c("vals");
this.labelBox = c("lbls");
this.minLabel = c();
this.maxLabel = c();
this.ranges = Array(Math.max(
this._num,
this._colors.length
))
.fill(0)
.map(() =>
c("range"));
this.valueLabels = Array(this._num)
.fill(0)
.map(() =>
c("val"));
this.valueBox.append(...this.valueLabels);
this.track.append(...this.ranges);
this.labelBox.append(
this.minLabel,
this.maxLabel
);
this.wrap.append(
this.valueBox,
this.track,
this.labelBox
);
this.minLabel.innerText = this._disp(this._min);
this.maxLabel.innerText = this._disp(this._max);
}
_setupEvents() {
this.wrap.addEventListener(
"pointerdown",
e =>
this._onDown(e)
);
this.wrap.addEventListener(
"pointermove",
e =>
this._onMove(e)
);
this.wrap.addEventListener(
"pointerup",
e =>
this._onUp(e)
);
this.wrap.addEventListener(
"pointercancel",
e =>
this._onUp(e)
);
}
_defaultValues(n) {
const stp = this._range / (n + 1);
return Array(n)
.fill(0)
.map((_, i) =>
this._roundToStep(this._min + stp * (i + 1)));
}
/**
* @export
* @getter
* @type {number|Array} values : slider values
*/
get values() {
const vals = this._vals.map(v =>
this._roundToStep(v));
if(this._num === 1)
return vals[0];
if(this._colors.length >= this._num)
return vals;
if(this._colors.length === this._num - 1) {
const sorted = [...vals].sort((a, b) =>
a - b);
return Array(sorted.length - 1)
.fill(0)
.map((_, i) =>
[sorted[i], sorted[i + 1]]);
}
if(this._num % 2 === 0) {
const ranges = [];
for(let i = 0; i < vals.length; i += 2)
ranges.push([vals[i], vals[i + 1]].sort((a, b) =>
a - b));
return ranges.length === 1 ? ranges[0] : ranges;
}
const sorted = [...vals].sort((a, b) =>
a - b);
return Array(sorted.length - 1)
.fill(0)
.map((_, i) =>
[sorted[i], sorted[i + 1]]);
}
/**
* @setter
* @type {number|Array} values : slider values
*/
set values(v) {
const newVals = [v].flat();
if(newVals.length !== this._num)
throw new Error(`${this._num} values expected`);
this._vals = newVals.map(v =>
this._forceRange(+v));
this._targetVals = [...this._vals];
this._updateUI();
}
/**
* @export
* @getter
* @type {Array} colors : slider colors
*/
get colors() {
return this._colors;
}
/**
* @setter
* @type {Array} colors : slider colors
*/
set colors(c) {
this._colors = [c].flat();
this._updateUI();
}
_calculateLabelPositions(pos) {
const rSize = this._dim(this.wrap);
if(rSize === 0)
return pos;
const minGap = 4;
// 1. Create label data objects and sort by initial position
let labels = pos.map((p, i) => {
const labelEl = this.valueLabels[i];
const fullSize = this._dim(labelEl);
return {
idx: i,
px: (p / 100) * rSize,
fullSize: fullSize,
halfSize: fullSize / 2
};
})
.sort((a, b) =>
a.px - b.px);
if(labels.length === 0)
return pos;
// 2. Iteratively resolve overlaps by simulating repulsive forces
for(let iter = 0; iter < 100; iter++) {
let forces = Array(labels.length)
.fill(0);
let systemStable = true;
// Calculate repulsive forces between overlapping labels
for(let i = 0; i < labels.length - 1; i++) {
const l1 = labels[i];
const l2 = labels[i + 1];
const requiredDist = l1.halfSize + l2.halfSize + minGap;
const actualDist = l2.px - l1.px;
if(actualDist < requiredDist) {
systemStable = false;
const overlap = requiredDist - actualDist;
forces[i] -= overlap / 2;
forces[i + 1] += overlap / 2;
}
}
// Calculate boundary forces to keep labels inside the track
const first = labels[0];
if(first.px < first.halfSize) {
systemStable = false;
forces[0] += (first.halfSize - first.px);
}
const last = labels[labels.length - 1];
if(last.px > rSize - last.halfSize) {
systemStable = false;
forces[labels.length - 1] -= (last.px - (rSize - last.halfSize));
}
if(systemStable)
break;
// Apply forces to update label positions
labels.forEach((label, i) => {
label.px += forces[i];
});
}
// 3. Create the final result array with percentage values
const result = Array(pos.length);
labels.forEach(l => {
result[l.idx] = (l.px / rSize) * 100;
});
return result;
}
_updateUI() {
const pos = this._vals.map(v =>
((v - this._min) / this._range) * 100);
const cLen = this._colors.length;
if(this._num % 2 === 0 && cLen < this._num && cLen !== this._num - 1) { // ranges
const activeRangeIndices = new Set(Array.from(
this._activePointers.values(),
p =>
Math.floor(p.activeIdx / 2)
));
const rangeData = [];
for(let i = 0; i < this._num; i += 2) {
const oidx = i / 2;
rangeData.push({
start: Math.min(
pos[i],
pos[i + 1]
),
end: Math.max(
pos[i],
pos[i + 1]
),
width: Math.abs(pos[i] - pos[i + 1]),
color: this._colors[oidx % cLen],
oidx,
actv: activeRangeIndices.has(oidx)
});
}
const zOrder = [...rangeData].sort((a, b) => {
if(a.actv !== b.actv)
return a.actv ? 1 : -1;
return b.width - a.width;
})
.map(d =>
d.oidx);
rangeData.forEach(def => {
const zIndex = zOrder.indexOf(def.oidx) + 1;
this._updateLine(
this.ranges[def.oidx],
def.start,
def.width,
def.color,
zIndex
);
});
}
else { // single | multiple values
const sortedForZ = this._vals.map((v, i) =>
({
v, i
}))
.sort((a, b) =>
b.v - a.v)
.map(x =>
x.i);
if(cLen >= this._num) {
pos.forEach((p, i) =>
this._updateLine(
this.ranges[i],
0,
p,
this._colors[i],
sortedForZ.indexOf(i) + 1
));
}
else { // linked ranges
const sVals = [...this._vals].sort((a, b) =>
a - b);
for(let i = 0; i < this._num - 1; i++) {
const p1 = ((sVals[i] - this._min) / this._range) * 100;
const p2 = ((sVals[i + 1] - this._min) / this._range) * 100;
this._updateLine(
this.ranges[i],
p1,
p2 - p1,
this._colors[i % cLen],
i + 1
);
}
}
}
const adjPos = this._calculateLabelPositions(pos);
this._vals.forEach((val, i) => {
let cIdx;
if(cLen >= this._num)
cIdx = i;
else if(this._num % 2 === 0 && cLen !== this._num - 1)
cIdx = Math.floor(i / 2) % cLen;
else {
const sortedVals = [...new Set([...this._vals].sort((a, b) =>
a - b))];
cIdx = Math.min(
sortedVals.indexOf(val),
cLen - 1
);
}
const label = this.valueLabels[i];
label.innerText = this._disp(this._roundToStep(val));
this._updateLabel(
label,
adjPos[i],
this._colors[cIdx]
);
});
}
_updateLine(el, start, size, col, zdx) {
Object.assign(
el.style,
{
backgroundColor: col,
zIndex: zdx
}
);
if(this._dir) { // vert
el.style.top = (100 - start - size) + "%";
el.style.height = size + "%";
}
else {
el.style.left = start + "%";
el.style.width = size + "%";
}
}
_updateLabel(el, lft, col) {
const rSize = this._dim(this.wrap);
const lSize = this._dim(el);
const pxPos = lft / 100 * rSize;
const edge = lSize / 2;
let t = -50;
if(pxPos < edge)
t = -(pxPos / edge * 50);
else if(pxPos > rSize - edge)
t = -100 + (rSize - pxPos) / edge * 50;
el.style.color = col;
/*Object.assign(
el.style,
{
color: col
}
);*/
if(this._dir) { // vert
el.style.top = (100 - lft) + "%";
el.style.transform = `translateY(${t}%)`;
}
else {
el.style.left = lft + "%";
el.style.transform = `translateX(${t}%)`;
}
}
_roundToStep(v) {
return this._forceRange(+parseFloat(this._step * Math.round(v / this._step))
.toFixed(this.dec));
}
_forceRange(v) {
return Math.max(
this._min,
Math.min(
this._max,
v
)
);
}
_onDown(e) {
if(!!e.button)
return;
const pos = this._pos(e);
const val = this._min + pos * this._range;
const usedIndices = Array.from(
this._activePointers.values(),
p =>
p.activeIdx
);
let closestIdx = -1, minDiff = Infinity;
this._vals.forEach((v, i) => {
if(usedIndices.includes(i))
return;
const diff = Math.abs(val - v);
if(diff < minDiff) {
minDiff = diff;
closestIdx = i;
}
});
if(closestIdx !== -1) {
const pid = e.pointerId;
this.wrap.setPointerCapture(pid);
this._activePointers.set(
pid,
{
activeIdx: closestIdx,
startVal: this._vals[closestIdx],
startPos: pos
}
);
this._slideState(true);
this._updateUI(); // update zindex on down
}
}
_onMove(e) {
const pointer = this._activePointers.get(e.pointerId);
if(!pointer)
return;
const pos = this._pos(e);
const delta = pos - pointer.startPos;
let newVal = this._roundToStep(pointer.startVal + delta * this._range);
if(this._ranged && this._num % 2 === 0 && this._colors.length < this._num) {
const pIdx = pointer.activeIdx % 2 === 0 ? pointer.activeIdx + 1 : pointer.activeIdx - 1;
const pVal = this._vals[pIdx];
const isLower = pointer.activeIdx % 2 === 0;
if(isLower ? newVal > pVal : newVal < pVal) {
this._targetVals[pIdx] = newVal;
this._targetVals[pointer.activeIdx] = pVal;
[pointer.activeIdx, pointer.startVal, pointer.startPos] = [pIdx, pVal, pos];
return this._animate();
}
newVal = isLower ? Math.min(
newVal,
pVal - this._ranged
) : Math.max(
newVal,
pVal + this._ranged
);
}
if(!this._isMoving) {
this._isMoving = true;
this._dispatch("start");
}
this._targetVals[pointer.activeIdx] = newVal;
this._animate();
}
_onUp(e) {
const pid = e.pointerId;
if(!this._activePointers.has(pid))
return;
this.wrap.releasePointerCapture(pid);
this._activePointers.delete(pid);
if(this._activePointers.size === 0) {
this._isMoving = false;
this._slideState(false);
this._updateUI(); // reset zindex
}
}
_pos(evt) {
const rect = this.wrap.getBoundingClientRect();
return this._dir ? 1 - (evt.clientY - rect.top) / rect.height : (evt.clientX - rect.left) / rect.width;
}
_dim(elt) {
return this._dir ? elt.offsetHeight : elt.offsetWidth;
}
_animate() {
if(this._frameLoop)
return;
const step = () => {
let needsUpdate = false,
valueChanged = false;
const EASE = 0.2,
PRECISION = this._step / 10;
this._vals.forEach((val, i) => {
const delta = this._targetVals[i] - val;
if(Math.abs(delta) > 0) {
if(Math.abs(delta) < PRECISION) {
this._vals[i] = this._targetVals[i];
valueChanged = true;
}
else {
this._vals[i] += delta * EASE;
needsUpdate = true;
}
}
});
if(needsUpdate || valueChanged) {
this._updateUI();
this._dispatch("slide");
if(needsUpdate)
this._frameLoop = requestAnimationFrame(step);
else {
this._frameLoop = null;
this._dispatch("change");
}
}
else
this._frameLoop = null;
};
this._frameLoop = requestAnimationFrame(step);
}
_slideState(stt) {
this.wrap.classList.toggle(
"sliding",
stt
);
}
/**
* @force
* @export
* @method on :
* @param {string} evt :
* @param {Function} handler :
*/
on(evt, handler) {
this._evts.addEventListener(
evt,
handler
);
return this;
}
/**
* @force
* @export
* @method off :
* @param {string} evt :
* @param {Function} handler :
*/
off(evt, handler) {
this._evts.removeEventListener(
evt,
handler
);
}
_dispatch(type) {
this._evts.dispatchEvent(new CustomEvent(
type,
{
detail: this.values
}
));
}
destroy() {
if(this._frameLoop)
cancelAnimationFrame(this._frameLoop);
this.wrap.remove();
}
}
// closure compiler warning
var NativeEventTarget = self["EventTarget"];:root {
--slideSize: 36px;
--trackSize: 6px;
--focusSize: 10px;
--trackColor: #e5e7eb;
--textColor: #6b7280;
--fontSize: 14px;
--animate: 0.2s ease;
--textSpc: calc(var(--trackSize) * 1);
--textOff: calc((var(--fontSize) + var(--textSpc)) * -1);
--slidSpc: calc(var(--focusSize) * 3 / 4);
--slidOff: calc((var(--fontSize) + var(--slidSpc)) * -1);
}
@media (prefers-color-scheme: dark) {
:root {
--trackColor: #363636;
--textColor: #9ca3af;
}
}
.slider {
position: relative;
height: var(--slideSize);
color: var(--textColor);
cursor: pointer;
-webkit-tap-highlight-color: transparent;
touch-action: none;
user-select: none;
outline: none;
}
.slider.sliding {
/* cursor: grabbing; */
}
.slider div {
user-select: none;
-webkit-user-select: none;
}
.slider .track {
position: absolute;
top: 50%;
left: 0;
width: 100%;
height: var(--trackSize);
transform: translateY(-50%);
background: var(--trackColor);
will-change: height;
transition: height var(--animate);
}
.slider .range {
position: absolute;
height: 100%;
will-change: left, width;
}
.slider .vals,
.slider .lbls {
position: absolute;
width: 100%;
font-size: var(--fontSize);
pointer-events: none;
transition: transform var(--animate);
}
.slider .vals {
top: 50%;
transform: translateY(var(--textOff));
}
.slider .val {
position: absolute;
white-space: nowrap;
will-change: left, transform;
}
.slider .lbls {
top: 50%;
display: flex;
justify-content: space-between;
transform: translateY(var(--textSpc));
}
.slider.sliding .track {
height: var(--focusSize);
}
.slider.sliding .vals {
transform: translateY(var(--slidOff));
}
.slider.sliding .lbls {
transform: translateY(var(--slidSpc));
}
/* vertical */
.slider.vert {
width: var(--slideSize);
}
.slider.vert .track {
top: 0;
left: 50%;
width: var(--trackSize);
height: 100%;
transform: translateX(-50%);
will-change: width;
transition: width var(--animate);
}
.slider.vert .range {
width: 100%;
will-change: top, height;
}
.slider.vert .vals,
.slider.vert .lbls {
top: 0;
width: auto;
height: 100%;
/* line-height: .8rem; */
line-height: var(--fontSize);
}
.slider.vert .vals {
right: 50%;
transform: translate(calc(var(--textSpc) * -1), 0);
}
.slider.vert .val {
right: -100%;
will-change: top, transform;
}
.slider.vert .lbls {
left: 50%;
flex-direction: column-reverse;
transform: translateX(var(--textSpc));
}
.slider.vert.sliding .track {
width: var(--focusSize);
}
.slider.vert.sliding .vals {
transform: translateX(calc(var(--focusSize) * -1));
}
.slider.vert.sliding .lbls {
transform: translateX(var(--focusSize));
}Just seeking ♪♬
Swipe or click
<div id="seektrack"></div>body {
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.seektrack {
width: 360px;
max-width: 360px;
max-width: 90%;
}const seekDatTrack = new SeekTrack(
document.querySelector("#seektrack"),
v => console.log(v));
seekDatTrack.dur = 180;
seekDatTrack.val = 100;/**
* @force
* @export
* @class SeekTrack : seek that track
*/
class SeekTrack {
/**
* @construct
* @param {HTMLElement} wrap :
* @param {Function} seek :
*/
constructor(wrap, seek) {
[this._track, this._timed, this._texts, this._start, this._total]
= ["track", "timed", "texts", "start", "total"].map(cls => {
const el = document.createElement("div");
el.classList.add(cls);
return el;
});
// Init state
this._min
= this._max
= this._val
= this._aim
= this._startPos
= this._startVal
= 0;
this._down
= this._moved
= this._anm
= false;
this._lst
= this._frm
= this._rect
= null;
this._cbk = seek;
this._wrap = wrap;
this._wrap.className = "seektrack";
this._track.append(this._timed);
this._texts.append(
this._start,
this._total
);
this._wrap.append(
this._texts,
this._track
);
this._wrap["onpointerdown"] = e => {
this._rect = this._wrap.getBoundingClientRect();
this._down = true;
this._moved = false;
this._startPos = e.clientX - this._rect.left;
this._startVal = this._val;
this._wrap.setPointerCapture(e.pointerId);
};
this._wrap["onpointermove"] = e => {
if(!this._down)
return;
this._moved = true;
const pos = (e.clientX - this._rect.left) / this._rect.width;
const delta = pos - this._startPos / this._rect.width;
this._aim = Math.max(
this._min,
Math.min(
this._max,
Math.round(this._startVal + delta * this._max)
)
);
this._animate();
};
this._wrap["onpointerup"] = this._wrap["onpointercancel"] = e => {
if(!this._down)
return;
this._down = false;
this._wrap.releasePointerCapture(e.pointerId);
if(!this._moved) {
const clickPos = (e.clientX - this._rect.left) / this._rect.width;
this._aim = Math.max(
this._min,
Math.min(
this._max,
Math.round(clickPos * this._max)
)
);
this._animate();
}
};
this._updateUI();
}
_fmt(s) {
return (s < 3600 ? [60, 1] : [3600, 60, 1])
.map(x =>
`0${~~(s / x) % 60}`.slice(-2))
.join(":");
}
_animate() {
if(this._frm)
cancelAnimationFrame(this._frm);
this._anm = true;
const step = () => {
const delta = this._aim - this._val;
if(Math.abs(delta) < 0.2) {
this._val = this._aim;
this._updateUI();
this._frm = null;
this._anm = false;
if(this.val !== this._lst) {
this._lst = this.val;
this._cbk(this.val);
}
}
else {
this._val += delta * 0.2;
this._updateUI();
this._frame(step);
}
};
this._frame(step);
}
_frame(cb) {
this._frm = requestAnimationFrame(cb);
}
_updateUI() {
this._timed.style.width = (this._val / this._max) * 100 + "%";
this._start.textContent = this._fmt(this._val);
this._total.textContent = this._fmt(this._max - this._val);
}
/**
* @force
* @export
* @getter
* @type {number} val : current time
*/
get val() {
return Math.round(this._val);
}
/**
* @setter
*/
set val(v) {
if(this._down || this._anm)
return;
this._val = this._aim = Math.max(
this._min,
Math.min(
this._max,
+v
)
);
this._updateUI();
}
/**
* @force
* @export
* @getter
* @type {number} dur : total value
*/
get dur() {
return this._max;
}
/**
* @setter
*/
set dur(v) {
this._max = v;
this._updateUI();
}
destroy() {
if(this._frm)
cancelAnimationFrame(this._frm);
this._wrap.remove();
}
}.seektrack {
position: relative;
width: 100%;
height: 100%;
font-size: 13px;
color: #6b7280;
cursor: pointer;
touch-action: none;
user-select: none;
}
.seektrack .track {
position: relative;
width: 100%;
height: 6px;
background: #c8c8c8;
}
.seektrack .timed {
width: 0%;
height: 100%;
will-change: width;
background: #4A90E2;
}
.seektrack .texts {
width: 100%;
display: flex;
justify-content: space-between;
pointer-events: none;
padding-bottom: .3rem;
}2K JS + 0.4K CSS
/**
* Nico Pr — https://nicopr.fr
* SEEKTRACK STANDALONE RELEASE v0.1.0
* 27/04/2025 01:18:32
*/
(function(){
function c(a){a.o&&cancelAnimationFrame(a.o);a.s=!0;const e=()=>{const b=a.l-a.g;.2>Math.abs(b)?(a.g=a.l,d(a),a.o=null,a.s=!1,a.val!==a.H&&(a.H=a.val,a.J(a.val))):(a.g+=.2*b,d(a),a.o=requestAnimationFrame(e))};a.o=requestAnimationFrame(e)}function d(a){a.D.style.width=a.g/a.h*100+"%";a.I.textContent=f(a.g);a.F.textContent=f(a.h-a.g)}function f(a){return(3600>a?[60,1]:[3600,60,1]).map(e=>`0${~~(a/e)%60}`.slice(-2)).join(":")}
class g{constructor(a,e){[this.G,this.D,this.C,this.I,this.F]=["track","timed","texts","start","total"].map(b=>{const l=document.createElement("div");l.classList.add(b);return l});this.u=this.h=this.g=this.l=this.A=this.B=0;this.m=this.v=this.s=!1;this.H=this.o=this.j=null;this.J=e;this.i=a;this.i.className="seektrack";this.G.append(this.D);this.C.append(this.I,this.F);this.i.append(this.C,this.G);this.i.onpointerdown=b=>{this.j=this.i.getBoundingClientRect();this.m=!0;this.v=!1;this.A=b.clientX-
this.j.left;this.B=this.g;this.i.setPointerCapture(b.pointerId)};this.i.onpointermove=b=>{this.m&&(this.v=!0,this.l=Math.max(this.u,Math.min(this.h,Math.round(this.B+((b.clientX-this.j.left)/this.j.width-this.A/this.j.width)*this.h))),c(this))};this.i.onpointerup=this.i.onpointercancel=b=>{this.m&&(this.m=!1,this.i.releasePointerCapture(b.pointerId),this.v||(this.l=Math.max(this.u,Math.min(this.h,Math.round((b.clientX-this.j.left)/this.j.width*this.h))),c(this)))};d(this)}get val(){return Math.round(this.g)}set val(a){this.m||
this.s||(this.g=this.l=Math.max(this.u,Math.min(this.h,+a)),d(this))}get dur(){return this.h}set dur(a){this.h=a;d(this)}}var h=g,k=["SeekTrack"],m=this||self;k[0]in m||"undefined"==typeof m.execScript||m.execScript("var "+k[0]);for(var n;k.length&&(n=k.shift());)k.length||void 0===h?m[n]&&m[n]!==Object.prototype[n]?m=m[n]:m=m[n]={}:m[n]=h;}).call(this);.seektrack{position:relative;width:100%;height:100%;font-size:13px;color:#6b7280;cursor:pointer;touch-action:none;user-select:none}.seektrack .track{position:relative;width:100%;height:6px;background:#c8c8c8}.seektrack .timed{width:0%;height:100%;will-change:width;background:#4a90e2}.seektrack .texts{width:100%;display:flex;justify-content:space-between;pointer-events:none;padding-bottom:.3rem}