useDragValue
ポインタのドラッグで数値を動かします。範囲・step・スケールをこの hook が持つので、ハンドラが受け取るのはピクセルではなく値です。
SourceKnob / Slider / XYPad はこの上に作られています。自作のコントロールに同じドラッグの挙動が要るときに使ってください。
function App() { const areaRef = useRef<HTMLDivElement>(null) const [value, setValue] = useState<[number, number]>([50, 50]) const { refCallback, dragging } = useDragValue<HTMLDivElement>({ // y is reversed so that its maximum is at the top. axis: [ { min: 0, max: 100, step: 1 }, { min: 0, max: 100, step: 1, reverse: true }, ], baseElementRef: areaRef, updateOnPointerDown: true, onChange: (value) => setValue(value), }) // The element that starts the drag is also the one the pointer is measured // against, so it takes both refs. const ref = useComposedRefs<HTMLDivElement>(areaRef, refCallback) return ( <div style={{ userSelect: 'none' }}> <div ref={ref} style={{ position: 'relative', width: '100%', height: 160, marginBottom: '0.5rem', border: '1px solid currentColor', borderRadius: 8, touchAction: 'none', }} > <div style={{ position: 'absolute', left: `${value[0]}%`, top: `${100 - value[1]}%`, width: 16, height: 16, borderRadius: '50%', background: 'currentColor', translate: '-50% -50%', pointerEvents: 'none', }} /> </div> <div>dragging: {String(dragging)}</div> <div> x: {value[0]}, y: {value[1]} </div> </div> ) }
インポート
import { useDragValue } from '@tremolo-ui/react'
値を持つのは呼び出し側
この hook は値を持ちません。 保存するのは onChange で、現在の値は getValue から読み直します。baseElementRef のときはそもそも読みません。ポインタの位置が値そのものだからです。同じ値をテキストフィールドからも MIDI メッセージからもプリセットからも設定でき、ドラッグと state がずれない理由がこれです。
ドラッグの 2 つのマッピング
どちらになるかは渡したオプションで決まります。ちょうど一方だけを渡してください。
baseElementRef: 値は「指している位置」
ポインタを要素の bounding rect に対して正規化するので、値はポインタにぴったり追随します。updateOnPointerDown を付けると、ポインタが動く前に押した位置へ値が飛びます。Slider と XYPad の挙動です。
getValue: 値は「動かした距離」だけ動く
ポインタの位置には意味がありません。値は元の位置から離れていき、pixelRange ピクセルが範囲全体に相当します。ノブがドラッグに追随するときの挙動で、値がポインタへ飛ばないのはこのためです。
function App() { const [value, setValue] = useState(50) const { refCallback, dragging } = useDragValue<HTMLDivElement>({ axis: { min: 0, max: 100, reverse: true }, // The value moves away from where it stood, so the hook has to be able to // read it. 200px of movement covers the whole range. getValue: () => [0, value], pixelRange: 200, // Hold shift to cover a tenth of the range with the same movement. sensitivity: (state) => (state.event.shiftKey ? 0.1 : 1), cursor: 'ns-resize', onChange: ([, y]) => setValue(y), }) return ( <div style={{ userSelect: 'none' }}> <div ref={refCallback} style={{ padding: '2rem', marginBottom: '0.5rem', border: '1px solid currentColor', borderRadius: 8, textAlign: 'center', touchAction: 'none', }} > Drag up and down </div> <div>dragging: {String(dragging)}</div> <div>value: {value.toFixed(1)}</div> </div> ) }
微調整
sensitivity は動くたびに読まれるので、ドラッグの途中で押された修飾キーにも答えられます。
useDragValue({
sensitivity: (state) => (state.event.shiftKey ? 0.1 : 1),
})
baseElementRef では値は普通「指している位置」なので、1 以外にするとマッピングが相対的になり、そのドラッグの間はポインタと値が離れたままになります。
軸
axis は両方の軸に効く 1 つの AxisOptions か、軸ごとの [x, y] を取ります。min / max / step / scale のほかに reverse があり、y 軸を上向きに伸ばすのに使います。画面の y は下向きに伸びるためです。
1 軸しか動かないコントロールでも値は両軸ぶん来ます。上の相対マッピングの例のように、使わない方は無視してください。
オプション
| Prop | 型 | 既定値 |
|---|---|---|
axis* | AxisOptions | readonly [AxisOptions, AxisOptions] | — |
baseElementRef | RefObject<Element | null> | — |
getValue | () => XY<number> | — |
pixelRange | number | readonly [number, number] | 100 |
sensitivity | (state: DragState) => number | — |
updateOnPointerDown | boolean | — |
threshold | number | 0 |
cursor | string | — |
pointerLock | boolean | false |
shouldStart | (event: PointerEvent) => boolean | — |
onChange | (value: XY<number>, state: DragState) => void | — |
onDragStart | (value: XY<number>, state: DragState) => void | — |
onDragEnd | (value: XY<number>, state: DragState) => void | — |
すべての説明: UseDragValueOptions