1
0
Fork 0
muzika-gromche/Frontend/src/components/library/ZoomSlider.vue

96 lines
3.3 KiB
Vue
Raw Normal View History

<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>