🎚️

Vuetify3のスライダを対数スケールで使えるようにしてみた

2024/08/02に公開

数値計算を行うページを作る機会があり、パラメータの入力に Vuetify3VSlider を使っています。値によってはリニアスケールよりも対数スケールでスライダが動かせると都合がいいものがあります。今回は標準の VSlider をベースにして対数スケール対応のスライダを作ってみました。

前提条件

  • Vue 3.4.34
  • Vuetify 3.6.13
  • TypeScript 5.5.4

コンポーネントのプログラム

以下が対数スケールに対応したスライダのプログラムになります。

LogSlider.vue
<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 です。対数の復習になりますが、それぞれ値を x, y とすると、2つの値は

y = \log_{b} (x)
x = b^y

という単純な関係になります。ここで底 b0 < b かつ b \neq 1 の実数ならばどんな値をとっても構いませんが[1]、JavaScript では Math.logMath.exp を使うのが最も単純である都合上、b = e として使っています。

あとは最大値 max と最小値 min に対応する対数の値を求め、それを VSlider の最大値と最小値に割り当てます。VSlider の値が変わると logarithmicValue の値が変化するので、その値をリニアスケールに戻して linearValue に格納します。

イベントループ対策

VSlider が動いて logarithmicValue の値が変化すると linearValue へ値が格納されますが、その際に watch で監視している変更に引っかかります。このまま logarithmicValue の値を変更すると不具合が生じてしまいます。これを防止するために、VSlider が動いたときは watch の処理を止める必要があります。

その役割を担っているのが moving という変数です。これは VSlider が今まさに操作されているかを表す真理値で、VSlider の startend イベントによって値が入れ替わります。これらのイベントは Vuetify 3.2.0 から新しく導入されたものです。movingtrue であるときは watch 内の処理を行わないようにしてイベントがループすることを防いでいます。

使い方

v-sliderLogSlider に置き換えるだけでそのまま使えます。maxmin の値はこれまで通りリニアスケールの値を使えます。v-model でバインディングした値もリニアスケールです。

App.vue
<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>

Vuetify Play でのデモ

ただし step の値は元の v-slider のものと効果が異なり、例えば以下のように 2 と指定すると 1 → 2 → 4 → 8 →… と冪乗で変化するようになります。

App.vue
<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>

Vuetify Play でのデモ

使用例


対数スケールのスライダが使われている例

自作した双2次フィルタのページにて使っています。

カットオフ周波数 は 10~数十kHz の範囲で動かせるようになっていますが、100Hz → 150Hzと動かしても 10000Hz → 10050Hz と動かす意味はほとんどありません。加算的に変化していくのではなく、音階と同じように乗算的に周波数を変化させることが多いためです。デジタルフィルタによる周波数応答もリニアスケールではなく、対数グラフで表すと単純なプロットになることが多いです。したがって、周波数はリニアスケールよりも対数スケールで操作できると便利です。

Q も対数スケールのほうが便利です。この値はデジタルフィルタを作る際に (\sqrt{2})^xx は整数)のような値を使うことが多く、やはり乗算的に値を変化させていきます。

ただし、音量のデシベルのスライダについてはリニアスケールになっています。デシベルそのものが既に対数スケール化された数値であるためです。

参考文献

  • 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でのカスタマイズ可能なステップの要望において対数スケールについて言及があります。しかし無段階(ステップなし)で対数スケールを実現している点で目的がやや異なります。
脚注
  1. 対数の性質により、b は条件内の実数ならばどんな数値を入れてもスライダの動きが変化することはありませんが、極端な値を用いるとスライダを小刻みに動かしたときの挙動が変化することがあります。これは対数の性質というより、元にした Vuetify3 の VSlider の挙動の性質によるものです。 ↩︎

Discussion