forked from nikita/muzika-gromche
96 lines
3.3 KiB
Vue
96 lines
3.3 KiB
Vue
<script setup lang="ts">
|
|
import Slider from '@/components/library/Slider.vue';
|
|
import Add from "@material-design-icons/svg/filled/add.svg";
|
|
import Remove from "@material-design-icons/svg/filled/remove.svg";
|
|
import { clamp } from '@vueuse/core';
|
|
import { computed } from 'vue';
|
|
import ToolButtonSmall from './ToolButtonSmall.vue';
|
|
|
|
const {
|
|
orientation = "horizontal",
|
|
defaultZoom = undefined,
|
|
extended = false,
|
|
} = defineProps<{
|
|
orientation?: "horizontal" | "vertical",
|
|
defaultZoom?: number,
|
|
extended?: boolean,
|
|
}>();
|
|
|
|
/** Zoom factor from 1 (fit content) to about 10 (content takes up 10x more space than the viewport).
|
|
* If extended is set, lower bound will be less than 1 (about 0.5) and upper bound is about 20x.
|
|
*/
|
|
const zoom = defineModel<number>("zoom", { required: true });
|
|
|
|
const zoomStepButtons = 10;
|
|
const zoomStepSlider = 1;
|
|
/* 0..100 or if extended is set -20..100 */
|
|
const zoomMin = computed(() => extended ? -2 * zoomStepButtons : 0);
|
|
const zoomMax = computed(() => extended ? 200 : 100);
|
|
|
|
const scale = 16;
|
|
// dirty hack because no exp growth: after extended threshold scale is more steep
|
|
const scale2 = 8;
|
|
const scaleThreshold = 100;
|
|
const zoomThreshold = 1 + scaleThreshold / scale;
|
|
|
|
// zoom: external level 0.5 .. 7.25
|
|
// slider value: interval 0 .. 100 or -20 .. 100
|
|
function toSliderValue(zoom: number): number {
|
|
if (zoom > zoomThreshold) {
|
|
return toSliderValue(zoomThreshold) + (zoom - zoomThreshold) * scale2;
|
|
}
|
|
if (zoom >= 1) {
|
|
return (zoom - 1) * scale;
|
|
} else {
|
|
return -2 * zoomMin.value * (zoom - 1);
|
|
}
|
|
}
|
|
|
|
function fromSliderValue(value: number): number {
|
|
// dirty hack because no exp growth
|
|
if (value > scaleThreshold) {
|
|
return fromSliderValue(scaleThreshold) + (value - scaleThreshold) / scale2;
|
|
}
|
|
if (value >= 0) {
|
|
return 1 + value / scale;
|
|
} else {
|
|
return 1 - value / (2 * zoomMin.value);
|
|
}
|
|
}
|
|
|
|
const defaultValue = computed(() => defaultZoom === undefined ? undefined : toSliderValue(defaultZoom));
|
|
|
|
// Internal integer representation that avoids floating point errors.
|
|
const zoomSliderValue = computed<number>({
|
|
get() {
|
|
return Math.round(toSliderValue(zoom.value));
|
|
},
|
|
set(value) {
|
|
value = clamp(Math.round(value), zoomMin.value, zoomMax.value);
|
|
zoom.value = fromSliderValue(value);
|
|
},
|
|
});
|
|
|
|
function onButton(direction: number): void {
|
|
let val = zoomSliderValue.value - zoomMin.value;
|
|
if (val % zoomStepButtons !== 0) {
|
|
// go to the nearest full step up or down depending on the direction
|
|
val = ((direction > 0) ? Math.ceil : Math.floor)(val / zoomStepButtons);
|
|
zoomSliderValue.value = val * zoomStepButtons + zoomMin.value;
|
|
} else {
|
|
zoomSliderValue.value += direction * zoomStepButtons;
|
|
}
|
|
}
|
|
</script>
|
|
<template>
|
|
<!-- for some reason min-width does not propagate up from Slider -->
|
|
<div class="tw:px-2 tw:flex tw:items-center tw:gap-2"
|
|
:class="orientation == 'vertical' ? 'tw:flex-col' : 'tw:flex-row'">
|
|
<ToolButtonSmall :icon="Remove" title="Zoom Out" @click="onButton(-1)" :disabled="zoomSliderValue <= zoomMin" />
|
|
<Slider :min="zoomMin" :max="zoomMax" :step="zoomStepSlider" v-model.number="zoomSliderValue" :orientation
|
|
:defaultValue />
|
|
<ToolButtonSmall :icon="Add" title="Zoom In" @click="onButton(+1)" :disabled="zoomSliderValue >= zoomMax" />
|
|
</div>
|
|
</template>
|
|
<style scoped></style>
|