mirror of
https://github.com/philomena-dev/philomena.git
synced 2024-11-30 14:57:59 +01:00
document and optimize
This commit is contained in:
parent
694bc31e50
commit
0877ac685b
3 changed files with 84 additions and 43 deletions
|
@ -8,15 +8,10 @@
|
||||||
* <input type="dualrange" min="0" max="100" valuemin="0" valuemax="100">
|
* <input type="dualrange" min="0" max="100" valuemin="0" valuemax="100">
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { lerp } from './utils/lerp';
|
||||||
import { $$ } from './utils/dom';
|
import { $$ } from './utils/dom';
|
||||||
|
|
||||||
function lerp(delta: number, from: number, to: number): number {
|
// Make a given slider head draggable.
|
||||||
if (delta >= 1) { return to; }
|
|
||||||
else if (delta <= 0) { return from; }
|
|
||||||
|
|
||||||
return from + (to - from) * delta;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty: string, limitProperty: string) {
|
function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty: string, limitProperty: string) {
|
||||||
const parent = el.parentElement;
|
const parent = el.parentElement;
|
||||||
|
|
||||||
|
@ -24,11 +19,41 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize variables and constants.
|
||||||
|
const inputEvent = new InputEvent('input');
|
||||||
let minPos = 0;
|
let minPos = 0;
|
||||||
let maxPos = 0;
|
let maxPos = 0;
|
||||||
|
let cachedMin = 0;
|
||||||
|
let cachedMax = 0;
|
||||||
|
let cachedValue = 0;
|
||||||
|
let cachedLimit = 0;
|
||||||
let curValue = 0;
|
let curValue = 0;
|
||||||
let dragging = false;
|
let dragging = false;
|
||||||
|
|
||||||
|
// Clamps the slider head value to not cross over the other slider head's value.
|
||||||
|
function clampValue(value: number): number {
|
||||||
|
if (cachedValue >= cachedLimit && value < cachedLimit) {
|
||||||
|
return cachedLimit;
|
||||||
|
}
|
||||||
|
else if (cachedValue < cachedLimit && value >= cachedLimit) {
|
||||||
|
return cachedLimit - 1; // Offset by 1 to ensure stored value is less than limit.
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility accessor to get the minimum value of dualrange.
|
||||||
|
function getMin(): number {
|
||||||
|
return Number(dataEl.getAttribute('min') || '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility accessor to get the maximum value of dualrange.
|
||||||
|
function getMax(): number {
|
||||||
|
return Number(dataEl.getAttribute('max') || '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initializes cached variables. Should be used
|
||||||
|
// when the pointer event begins.
|
||||||
function initVars() {
|
function initVars() {
|
||||||
if (!parent) { return; }
|
if (!parent) { return; }
|
||||||
|
|
||||||
|
@ -36,32 +61,14 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty:
|
||||||
|
|
||||||
minPos = rect.x;
|
minPos = rect.x;
|
||||||
maxPos = rect.x + rect.width - el.clientWidth;
|
maxPos = rect.x + rect.width - el.clientWidth;
|
||||||
|
cachedMin = getMin();
|
||||||
|
cachedMax = getMax();
|
||||||
|
cachedValue = Number(dataEl.getAttribute(valueProperty) || '0');
|
||||||
|
cachedLimit = Number(dataEl.getAttribute(limitProperty) || '0');
|
||||||
curValue = Number(dataEl.getAttribute(valueProperty) || '0');
|
curValue = Number(dataEl.getAttribute(valueProperty) || '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
function clampValue(value: number): number {
|
// Called during pointer movement.
|
||||||
const storedValue = Number(dataEl.getAttribute(valueProperty) || '0');
|
|
||||||
const limitValue = Number(dataEl.getAttribute(limitProperty) || '0');
|
|
||||||
|
|
||||||
if (storedValue >= limitValue && value < limitValue) {
|
|
||||||
return limitValue;
|
|
||||||
}
|
|
||||||
else if (storedValue < limitValue && value >= limitValue) {
|
|
||||||
return limitValue - 1; // Offset by 1 to ensure stored value is less than limit.
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMin(): number {
|
|
||||||
return Number(dataEl.getAttribute('min') || '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMax(): number {
|
|
||||||
return Number(dataEl.getAttribute('max') || '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define functions to control the drag behavior of the slider.
|
|
||||||
function dragMove(e: PointerEvent) {
|
function dragMove(e: PointerEvent) {
|
||||||
if (!dragging) { return; }
|
if (!dragging) { return; }
|
||||||
|
|
||||||
|
@ -69,40 +76,40 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty:
|
||||||
|
|
||||||
let desiredPos = e.clientX;
|
let desiredPos = e.clientX;
|
||||||
|
|
||||||
if (desiredPos > maxPos) {
|
// `lerp` cleverly clamps the value between min and max,
|
||||||
desiredPos = maxPos;
|
// so no need for any explicit checks for that here, only
|
||||||
}
|
// the crossover check is required.
|
||||||
else if (desiredPos < minPos) {
|
|
||||||
desiredPos = minPos;
|
|
||||||
}
|
|
||||||
|
|
||||||
curValue = clampValue(
|
curValue = clampValue(
|
||||||
lerp(
|
lerp(
|
||||||
(desiredPos - minPos) / (maxPos - minPos),
|
(desiredPos - minPos) / (maxPos - minPos),
|
||||||
getMin(),
|
cachedMin,
|
||||||
getMax()
|
cachedMax
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
desiredPos = lerp(curValue / getMax(), minPos, maxPos);
|
// Same here, lerp clamps the value so it doesn't get out
|
||||||
|
// of the slider boundary.
|
||||||
|
desiredPos = lerp(curValue / cachedMax, minPos, maxPos);
|
||||||
|
|
||||||
el.style.left = `${desiredPos}px`;
|
el.style.left = `${desiredPos}px`;
|
||||||
|
|
||||||
dataEl.setAttribute(valueProperty, curValue.toString());
|
dataEl.setAttribute(valueProperty, curValue.toString());
|
||||||
dataEl.dispatchEvent(new InputEvent('input'));
|
dataEl.dispatchEvent(inputEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called when the pointer is let go of.
|
||||||
function dragEnd(e: PointerEvent) {
|
function dragEnd(e: PointerEvent) {
|
||||||
if (!dragging) { return; }
|
if (!dragging) { return; }
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
dataEl.setAttribute(valueProperty, curValue.toString());
|
dataEl.setAttribute(valueProperty, curValue.toString());
|
||||||
dataEl.dispatchEvent(new InputEvent('input'));
|
dataEl.dispatchEvent(inputEvent);
|
||||||
|
|
||||||
dragging = false;
|
dragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Called when the slider head is clicked or tapped.
|
||||||
function dragBegin(e: PointerEvent) {
|
function dragBegin(e: PointerEvent) {
|
||||||
if (!parent) { return; }
|
if (!parent) { return; }
|
||||||
|
|
||||||
|
@ -112,7 +119,7 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty:
|
||||||
dragging = true;
|
dragging = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set initial position;
|
// Set the initial variables and position;
|
||||||
initVars();
|
initVars();
|
||||||
el.style.left = `${lerp(curValue / getMax(), minPos, maxPos)}px`;
|
el.style.left = `${lerp(curValue / getMax(), minPos, maxPos)}px`;
|
||||||
|
|
||||||
|
@ -122,6 +129,10 @@ function setupDrag(el: HTMLDivElement, dataEl: HTMLInputElement, valueProperty:
|
||||||
window.addEventListener('pointermove', dragMove);
|
window.addEventListener('pointermove', dragMove);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets up the slider input element.
|
||||||
|
// Creates `div` elements for presentation, hides the
|
||||||
|
// original `input` element. The logic is that
|
||||||
|
// we use divs for presentation, and input for data storage.
|
||||||
function setupSlider(el: HTMLInputElement) {
|
function setupSlider(el: HTMLInputElement) {
|
||||||
const parent = el.parentElement;
|
const parent = el.parentElement;
|
||||||
|
|
||||||
|
@ -155,6 +166,7 @@ function setupSlider(el: HTMLInputElement) {
|
||||||
setupDrag(maxHead, el, 'valuemax', 'valuemin');
|
setupDrag(maxHead, el, 'valuemax', 'valuemin');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sets up all sliders currently on the page.
|
||||||
function setupSliders() {
|
function setupSliders() {
|
||||||
$$<HTMLInputElement>('input[type="dualrange"]').forEach(el => {
|
$$<HTMLInputElement>('input[type="dualrange"]').forEach(el => {
|
||||||
setupSlider(el);
|
setupSlider(el);
|
||||||
|
|
17
assets/js/utils/__tests__/lerp.spec.ts
Normal file
17
assets/js/utils/__tests__/lerp.spec.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { lerp } from '../lerp';
|
||||||
|
|
||||||
|
describe('Linear interpolation', () => {
|
||||||
|
describe('lerp', () => {
|
||||||
|
it('should interpolate the min-max range based on a delta', () => {
|
||||||
|
expect(lerp(0.5, 0, 100)).toEqual(50);
|
||||||
|
expect(lerp(0.75, 0, 100)).toEqual(75);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clamp the value between min and max', () => {
|
||||||
|
expect(lerp(-999, 0, 100)).toEqual(0);
|
||||||
|
expect(lerp(0, 0, 100)).toEqual(0);
|
||||||
|
expect(lerp(999, 0, 100)).toEqual(100);
|
||||||
|
expect(lerp(1, 0, 100)).toEqual(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
12
assets/js/utils/lerp.ts
Normal file
12
assets/js/utils/lerp.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// Simple linear interpolation.
|
||||||
|
// Returns a value between min and max based on a delta.
|
||||||
|
// The delta is a number between 0 and 1.
|
||||||
|
// If the delta is not within the 0-1 range, this function will
|
||||||
|
// clamp the value between min and max, depending on whether
|
||||||
|
// the delta >= 1 or <= 0.
|
||||||
|
export function lerp(delta: number, from: number, to: number): number {
|
||||||
|
if (delta >= 1) { return to; }
|
||||||
|
else if (delta <= 0) { return from; }
|
||||||
|
|
||||||
|
return from + (to - from) * delta;
|
||||||
|
}
|
Loading…
Reference in a new issue