Vuetify3のスライダを対数スケールで使えるようにしてみた
数値計算を行うページを作る機会があり、パラメータの入力に Vuetify3 の VSlider を使っています。値によってはリニアスケールよりも対数スケールでスライダが動かせると都合がいいものがあります。今回は標準の VSlider をベースにして対数スケール対応のスライダを作ってみました。
前提条件
- Vue 3.4.34
- Vuetify 3.6.13
- TypeScript 5.5.4
コンポーネントのプログラム
以下が対数スケールに対応したスライダのプログラムになります。
<script setup lang="ts">
import { ref, watch } from 'vue';
const { max, min, step } = defineProps<{
max: number;
min: number;
step?: number;
}>();
const linearValue = defineModel<number>();
const logarithmicValue = ref<number>(0);
const moving = ref<boolean>(false);
watch(
() => linearValue.value,
() => {
if (linearValue.value && !moving.value) {
logarithmicValue.value = Math.log(linearValue.value);
}
},
{ immediate: true },
);
function logarithmicValueUpdated() {
linearValue.value = Math.exp(logarithmicValue.value);
}
</script>
<template>
<v-slider
v-model:model-value="logarithmicValue"
@update:model-value="logarithmicValueUpdated"
@start="moving = true"
@end="moving = false"
:max="Math.log(max)"
:min="Math.log(min)"
:step="typeof step !== 'undefined' ? Math.log(step) : undefined"
>
<template #thumb-label>
<slot name="thumb-label"> {{ Number(linearValue).toFixed(3) }} </slot>
</template>
</v-slider>
</template>
Vuetify Play にて実際に動かせる例を書いてみました。
解説
双方向バインドで渡された値は、コンポーネント中ではリニアスケールの値 linearValue
で表現しています。この値を対数スケールの値で表したものが logarithmicValue
です。対数の復習になりますが、それぞれ値を
という単純な関係になります。ここで底 Math.log
と Math.exp
を使うのが最も単純である都合上、
あとは最大値 max
と最小値 min
に対応する対数の値を求め、それを VSlider の最大値と最小値に割り当てます。VSlider の値が変わると logarithmicValue
の値が変化するので、その値をリニアスケールに戻して linearValue
に格納します。
イベントループ対策
VSlider が動いて logarithmicValue
の値が変化すると linearValue
へ値が格納されますが、その際に watch
で監視している変更に引っかかります。このまま logarithmicValue
の値を変更すると不具合が生じてしまいます。これを防止するために、VSlider が動いたときは watch
の処理を止める必要があります。
その役割を担っているのが moving
という変数です。これは VSlider が今まさに操作されているかを表す真理値で、VSlider の start
と end
イベントによって値が入れ替わります。これらのイベントは Vuetify 3.2.0 から新しく導入されたものです。moving
が true
であるときは watch
内の処理を行わないようにしてイベントがループすることを防いでいます。
使い方
v-slider
を LogSlider
に置き換えるだけでそのまま使えます。max
と min
の値はこれまで通りリニアスケールの値を使えます。v-model
でバインディングした値もリニアスケールです。
<script setup lang="ts">
import { ref } from 'vue';
import LogSlider from './LogSlider.vue';
const value = ref<number>(1);
</script>
<template>
<LogSlider v-model="value" :max="1e6" :min="1" />
</template>
ただし step
の値は元の v-slider
のものと効果が異なり、例えば以下のように 2
と指定すると 1 → 2 → 4 → 8 →… と冪乗で変化するようになります。
<script setup lang="ts">
import { ref } from 'vue';
import LogSlider from './LogSlider.vue';
const value = ref<number>(1);
</script>
<template>
<LogSlider v-model="value" :max="1e6" :min="1" :step="2" />
</template>
使用例
対数スケールのスライダが使われている例
自作した双2次フィルタのページにて使っています。
カットオフ周波数 は 10~数十kHz の範囲で動かせるようになっていますが、100Hz → 150Hzと動かしても 10000Hz → 10050Hz と動かす意味はほとんどありません。加算的に変化していくのではなく、音階と同じように乗算的に周波数を変化させることが多いためです。デジタルフィルタによる周波数応答もリニアスケールではなく、対数グラフで表すと単純なプロットになることが多いです。したがって、周波数はリニアスケールよりも対数スケールで操作できると便利です。
Q も対数スケールのほうが便利です。この値はデジタルフィルタを作る際に
ただし、音量のデシベルのスライダについてはリニアスケールになっています。デシベルそのものが既に対数スケール化された数値であるためです。
参考文献
-
VueJS smart logarithmic slider
https://codepen.io/janwirth/pen/ZJEKxp
Vuetifyではありませんが、Vueで同一の目的を達成しているという点で先駆者です。 -
[Feature Request] Define custom steps for v-slider #5930
https://github.com/vuetifyjs/vuetify/issues/5930
VSliderでのカスタマイズ可能なステップの要望において対数スケールについて言及があります。しかし無段階(ステップなし)で対数スケールを実現している点で目的がやや異なります。
-
対数の性質により、
は条件内の実数ならばどんな数値を入れてもスライダの動きが変化することはありませんが、極端な値を用いるとスライダを小刻みに動かしたときの挙動が変化することがあります。これは対数の性質というより、元にした Vuetify3 の VSlider の挙動の性質によるものです。 ↩︎b
Discussion