Skip to content

Commit 2015092

Browse files
committed
perf(parameter-slider): eliminate drag lag and speed settle
1 parent e18b9de commit 2015092

File tree

3 files changed

+10
-167
lines changed

3 files changed

+10
-167
lines changed

components/tool-ui/parameter-slider/parameter-slider.tsx

Lines changed: 7 additions & 164 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,7 @@ function getAriaValueText(
6060
}
6161

6262
const TICK_COUNT = 16;
63-
const TEXT_PADDING_X = 4;
64-
const TEXT_PADDING_X_OUTER = 0; // Less inset on outer-facing side (near edges)
6563
const TEXT_PADDING_Y = 2;
66-
const DETECTION_MARGIN_X = 12;
67-
const DETECTION_MARGIN_X_OUTER = 4; // Small margin at edges for steep falloff - segments fully close at terminal positions
68-
const DETECTION_MARGIN_Y = 12;
69-
const TRACK_HEIGHT = 48;
7064
const TEXT_RELEASE_INSET = 8;
7165
const TRACK_EDGE_INSET = 4; // px from track edge - keeps elements visible at extremes
7266
const THUMB_WIDTH = 12; // w-3
@@ -100,129 +94,6 @@ function toRadixThumbPosition(percent: number): string {
10094
return `calc(${safePercent}% + ${offsetPx}px)`;
10195
}
10296

103-
function signedDistanceToRoundedRect(
104-
px: number,
105-
py: number,
106-
left: number,
107-
right: number,
108-
top: number,
109-
bottom: number,
110-
radiusLeft: number,
111-
radiusRight: number,
112-
): number {
113-
const innerLeft = left + radiusLeft;
114-
const innerRight = right - radiusRight;
115-
const innerTop = top + Math.max(radiusLeft, radiusRight);
116-
const innerBottom = bottom - Math.max(radiusLeft, radiusRight);
117-
118-
const inLeftCorner = px < innerLeft;
119-
const inRightCorner = px > innerRight;
120-
const inCornerY = py < innerTop || py > innerBottom;
121-
122-
if ((inLeftCorner || inRightCorner) && inCornerY) {
123-
const radius = inLeftCorner ? radiusLeft : radiusRight;
124-
const cornerX = inLeftCorner ? innerLeft : innerRight;
125-
const cornerY = py < innerTop ? top + radius : bottom - radius;
126-
const distToCornerCenter = Math.hypot(px - cornerX, py - cornerY);
127-
return distToCornerCenter - radius;
128-
}
129-
130-
const dx = Math.max(left - px, px - right, 0);
131-
const dy = Math.max(top - py, py - bottom, 0);
132-
133-
if (dx === 0 && dy === 0) {
134-
return -Math.min(px - left, right - px, py - top, bottom - py);
135-
}
136-
137-
return Math.max(dx, dy);
138-
}
139-
140-
const OUTER_EDGE_RADIUS_FACTOR = 0.3; // Reduced radius on outer-facing sides for steeper falloff
141-
142-
function calculateGap(
143-
thumbCenterX: number,
144-
textRect: { left: number; right: number; height: number; centerY: number },
145-
isLeftAligned: boolean,
146-
): number {
147-
const { left, right, height, centerY } = textRect;
148-
// Asymmetric padding/margin: outer-facing side has less padding, more margin
149-
const paddingLeft = isLeftAligned ? TEXT_PADDING_X_OUTER : TEXT_PADDING_X;
150-
const paddingRight = isLeftAligned ? TEXT_PADDING_X : TEXT_PADDING_X_OUTER;
151-
const marginLeft = isLeftAligned
152-
? DETECTION_MARGIN_X_OUTER
153-
: DETECTION_MARGIN_X;
154-
const marginRight = isLeftAligned
155-
? DETECTION_MARGIN_X
156-
: DETECTION_MARGIN_X_OUTER;
157-
const paddingY = TEXT_PADDING_Y;
158-
const marginY = DETECTION_MARGIN_Y;
159-
const thumbCenterY = centerY;
160-
161-
// Inner boundary (where max gap occurs)
162-
const innerLeft = left - paddingLeft;
163-
const innerRight = right + paddingRight;
164-
const innerTop = centerY - height / 2 - paddingY;
165-
const innerBottom = centerY + height / 2 + paddingY;
166-
const innerHeight = height + paddingY * 2;
167-
const innerRadius = innerHeight / 2;
168-
// Smaller radius on outer-facing side (left for label, right for value)
169-
const innerRadiusLeft = isLeftAligned
170-
? innerRadius * OUTER_EDGE_RADIUS_FACTOR
171-
: innerRadius;
172-
const innerRadiusRight = isLeftAligned
173-
? innerRadius
174-
: innerRadius * OUTER_EDGE_RADIUS_FACTOR;
175-
176-
// Outer boundary (where effect starts) - proportionally larger
177-
const outerLeft = left - paddingLeft - marginLeft;
178-
const outerRight = right + paddingRight + marginRight;
179-
const outerTop = centerY - height / 2 - paddingY - marginY;
180-
const outerBottom = centerY + height / 2 + paddingY + marginY;
181-
const outerHeight = height + paddingY * 2 + marginY * 2;
182-
const outerRadius = outerHeight / 2;
183-
const outerRadiusLeft = isLeftAligned
184-
? outerRadius * OUTER_EDGE_RADIUS_FACTOR
185-
: outerRadius;
186-
const outerRadiusRight = isLeftAligned
187-
? outerRadius
188-
: outerRadius * OUTER_EDGE_RADIUS_FACTOR;
189-
190-
const outerDist = signedDistanceToRoundedRect(
191-
thumbCenterX,
192-
thumbCenterY,
193-
outerLeft,
194-
outerRight,
195-
outerTop,
196-
outerBottom,
197-
outerRadiusLeft,
198-
outerRadiusRight,
199-
);
200-
201-
// Outside outer boundary - no gap
202-
if (outerDist > 0) return 0;
203-
204-
const innerDist = signedDistanceToRoundedRect(
205-
thumbCenterX,
206-
thumbCenterY,
207-
innerLeft,
208-
innerRight,
209-
innerTop,
210-
innerBottom,
211-
innerRadiusLeft,
212-
innerRadiusRight,
213-
);
214-
215-
// Inside inner boundary - max gap
216-
const maxGap = height + paddingY * 2;
217-
if (innerDist <= 0) return maxGap;
218-
219-
// Between boundaries - linear interpolation
220-
// outerDist is negative (inside outer), innerDist is positive (outside inner)
221-
const totalDist = Math.abs(outerDist) + innerDist;
222-
const t = Math.abs(outerDist) / totalDist;
223-
224-
return maxGap * t;
225-
}
22697

22798
interface SliderRowProps {
22899
config: SliderConfig;
@@ -254,7 +125,6 @@ function SliderRow({
254125
const labelRef = useRef<HTMLSpanElement>(null);
255126
const valueRef = useRef<HTMLSpanElement>(null);
256127

257-
const [dragGap, setDragGap] = useState(0);
258128
const [fullGap, setFullGap] = useState(0);
259129
const [intersectsText, setIntersectsText] = useState(false);
260130
const [layoutVersion, setLayoutVersion] = useState(0);
@@ -294,6 +164,7 @@ function SliderRow({
294164
const valueEl = valueRef.current;
295165

296166
if (!track || !labelEl || !valueEl) return;
167+
if (isDragging) return;
297168

298169
const trackRect = track.getBoundingClientRect();
299170
const labelRect = labelEl.getBoundingClientRect();
@@ -307,33 +178,6 @@ function SliderRow({
307178
getRadixThumbInBoundsOffsetPx(valuePercent);
308179
const thumbHalfWidth = THUMB_WIDTH / 2;
309180

310-
// Text is raised by TEXT_VERTICAL_OFFSET from center
311-
const trackCenterY = TRACK_HEIGHT / 2 - TEXT_VERTICAL_OFFSET;
312-
313-
const labelGap = calculateGap(
314-
thumbCenterPx,
315-
{
316-
left: labelRect.left - trackRect.left,
317-
right: labelRect.right - trackRect.left,
318-
height: labelRect.height,
319-
centerY: trackCenterY,
320-
},
321-
true,
322-
); // label is left-aligned
323-
324-
const valueGap = calculateGap(
325-
thumbCenterPx,
326-
{
327-
left: valueRect.left - trackRect.left,
328-
right: valueRect.right - trackRect.left,
329-
height: valueRect.height,
330-
centerY: trackCenterY,
331-
},
332-
false,
333-
); // value is right-aligned
334-
335-
setDragGap(Math.max(labelGap, valueGap));
336-
337181
// Tight intersection check for release state
338182
// Inset by px-2 (8px) padding to check against actual text, not padded container
339183
const labelLeft = labelRect.left - trackRect.left + TEXT_RELEASE_INSET;
@@ -362,11 +206,10 @@ function SliderRow({
362206
? valueFullGap
363207
: 0;
364208
setFullGap(releaseGap);
365-
}, [value, min, max, layoutVersion]);
209+
}, [value, min, max, layoutVersion, isDragging]);
366210

367-
// While dragging: gradual separation based on distance
368-
// On release: fully open if intersecting text, fully closed otherwise
369-
const gap = isDragging ? dragGap : intersectsText ? fullGap : 0;
211+
// Keep drag path lightweight; only apply split gap after release.
212+
const gap = isDragging ? 0 : intersectsText ? fullGap : 0;
370213

371214
const ticks = useMemo(() => {
372215
// Generate equidistant ticks regardless of step value
@@ -501,7 +344,7 @@ function SliderRow({
501344
"isolate h-12",
502345
isDragging
503346
? "[&>span]:transition-none"
504-
: "[&>span]:transition-[left,transform] [&>span]:duration-120 [&>span]:ease-[cubic-bezier(0.22,1,0.36,1)]",
347+
: "[&>span]:transition-[left,transform] [&>span]:duration-90 [&>span]:ease-[cubic-bezier(0.22,1,0.36,1)]",
505348
"[&>span]:will-change-[left,transform]",
506349
"motion-reduce:[&>span]:transition-none",
507350
disabled && "pointer-events-none opacity-50",
@@ -532,7 +375,7 @@ function SliderRow({
532375
"absolute inset-0 will-change-[clip-path]",
533376
isDragging
534377
? "transition-none"
535-
: "transition-[clip-path] duration-120 ease-[cubic-bezier(0.22,1,0.36,1)]",
378+
: "transition-[clip-path] duration-90 ease-[cubic-bezier(0.22,1,0.36,1)]",
536379
"motion-reduce:transition-none",
537380
resolvedFillClassName ?? "bg-primary/30 dark:bg-primary/40",
538381
)}
@@ -575,7 +418,7 @@ function SliderRow({
575418
"squircle pointer-events-none absolute inset-0 rounded-sm",
576419
isDragging
577420
? "transition-none"
578-
: "transition-[opacity,background] duration-120 ease-[cubic-bezier(0.22,1,0.36,1)]",
421+
: "transition-[opacity,background] duration-90 ease-[cubic-bezier(0.22,1,0.36,1)]",
579422
"motion-reduce:transition-none",
580423
)}
581424
style={{

lib/tests/tool-ui/parameter-slider/visual-alignment-contract.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,9 @@ describe("parameter-slider visual alignment contract", () => {
160160
}),
161161
);
162162

163-
expect(html).toContain("transition-[clip-path] duration-120");
163+
expect(html).toContain("transition-[clip-path] duration-90");
164164
expect(html).toContain(
165-
"[&amp;&gt;span]:transition-[left,transform] [&amp;&gt;span]:duration-120",
165+
"[&amp;&gt;span]:transition-[left,transform] [&amp;&gt;span]:duration-90",
166166
);
167167
expect(html).toContain("ease-[cubic-bezier(0.22,1,0.36,1)]");
168168
expect(html).not.toContain("duration-180");

0 commit comments

Comments
 (0)