document and optimize

This commit is contained in:
Luna D. 2024-06-03 23:43:51 +02:00
parent 694bc31e50
commit 0877ac685b
No known key found for this signature in database
GPG key ID: 4B1C63448394F688
3 changed files with 84 additions and 43 deletions

View file

@ -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);

View 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
View 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;
}