fix: knob & text
This commit is contained in:
@@ -97,8 +97,11 @@ export const Knob = ({
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
const elRef = useRef<HTMLDivElement>(null);
|
||||
const a0 = useRef(0);
|
||||
const v0 = useRef(0);
|
||||
// Drag tracking: previous pointer angle + a continuous value accumulator.
|
||||
// Accumulating per-move deltas (each < 180°) instead of measuring from the
|
||||
// grab angle lets the dial wrap past ±180° without the value snapping.
|
||||
const lastAngle = useRef(0);
|
||||
const acc = useRef(0);
|
||||
|
||||
const reactId = useId();
|
||||
const knobId = id ?? reactId;
|
||||
@@ -129,16 +132,21 @@ export const Knob = ({
|
||||
elRef.current?.focus();
|
||||
elRef.current?.setPointerCapture(e.pointerId);
|
||||
setDragging(true);
|
||||
a0.current = angleAt(e.clientX, e.clientY);
|
||||
v0.current = committed;
|
||||
lastAngle.current = angleAt(e.clientX, e.clientY);
|
||||
acc.current = clamp(committed);
|
||||
};
|
||||
|
||||
const onPointerMove = (e: PointerEvent<HTMLDivElement>) => {
|
||||
if (!dragging) return;
|
||||
let d = angleAt(e.clientX, e.clientY) - a0.current;
|
||||
const a = angleAt(e.clientX, e.clientY);
|
||||
let d = a - lastAngle.current;
|
||||
if (d > 180) d -= 360;
|
||||
if (d < -180) d += 360;
|
||||
commit(v0.current + (d / SWEEP) * span, true);
|
||||
lastAngle.current = a;
|
||||
// Clamp the accumulator (not just the committed value) so overshoot past
|
||||
// an end doesn't build a dead zone you must unwind before reversing.
|
||||
acc.current = clamp(acc.current + (d / SWEEP) * span);
|
||||
commit(acc.current, true);
|
||||
};
|
||||
|
||||
const endDrag = (e: PointerEvent<HTMLDivElement>) => {
|
||||
|
||||
@@ -165,7 +165,7 @@ function useFieldAnimation(
|
||||
if (node) spanRefs.current.set(e.id, node);
|
||||
else spanRefs.current.delete(e.id);
|
||||
}}
|
||||
className="modern-sk-field-char"
|
||||
className={cx('modern-sk-field-char', !multiline && 'modern-sk-field-char--composited')}
|
||||
>
|
||||
{e.char}
|
||||
</span>
|
||||
|
||||
@@ -269,11 +269,23 @@ textarea.modern-sk-field {
|
||||
.modern-sk-field-char {
|
||||
/* inline (not inline-block) so the overlay wraps exactly like the field;
|
||||
inherit white-space so multiline (pre-wrap) wraps while input (pre) won't.
|
||||
'pre' on the span itself would suppress wrapping at its boundaries. */
|
||||
'pre' on the span itself would suppress wrapping at its boundaries.
|
||||
Multiline keeps this inline path: inline boxes ignore transform, so the
|
||||
rise eases `top` (a layout property — acceptable here, textarea wraps
|
||||
instead of scrolling so there's no per-keystroke scroll-sync thrash). */
|
||||
position: relative;
|
||||
white-space: inherit;
|
||||
animation: modern-sk-char-in var(--dur-base) var(--ease-snap) both;
|
||||
}
|
||||
/* Single-line never wraps, so chars can be inline-block and animate
|
||||
`transform` (compositor-only) instead of `top` (relayout every frame).
|
||||
The single-line field scrolls horizontally and syncs the overlay each
|
||||
keystroke, so the layout-triggering `top` path janked there — this doesn't. */
|
||||
.modern-sk-field-char--composited {
|
||||
display: inline-block;
|
||||
will-change: transform, opacity;
|
||||
animation-name: modern-sk-char-in-rise;
|
||||
}
|
||||
.modern-sk-field-char--leaving {
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
@@ -292,6 +304,17 @@ textarea.modern-sk-field {
|
||||
top: 0;
|
||||
}
|
||||
}
|
||||
/* inline-block (single-line) variant — composited transform, no relayout */
|
||||
@keyframes modern-sk-char-in-rise {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-0.32em);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
@keyframes modern-sk-char-out {
|
||||
from {
|
||||
opacity: 1;
|
||||
|
||||
Reference in New Issue
Block a user