跳到內容

每條游來游去的小魚,都是正在瀏覽這一頁的訪客,有一條是你自己 ( ´ ▽ ` )ノ 覺得干擾閱讀的話,可以把小魚關掉。

脆弱的滑動條

slider

會斷掉 (´・ω・`)

名稱描述
Anime.js輕量級 JavaScript 動畫函式庫
Pointer 事件偵測滑鼠或觸控點移動、點擊、懸停等等事件,取得座標、目標等等資訊
物理模擬模擬真實世界物理現象,如重力、碰撞、速度等物理效果
CSS 動畫基於 CSS transition 和 animation 實現
JS 動畫基於 JavaScript 實現的動畫,達成更複雜、精準的動畫控制,常見套件有 GSAP、anime.js 等

拉太偏會讓握把斷掉 (´・ω・`)

value: 50
<template>
<div class="mx-auto max-w-[20rem] w-full flex flex-col gap-4 py-14">
value: {{ Math.floor(value) }}
<slider-stubborn
v-model="value"
class="w-full"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import SliderStubborn from '../slider-fragile.vue'
const value = ref(50)
</script>

可以主動修復或斷開握把 (´・ω・`)ノ

<template>
<div class="mx-auto max-w-[20rem] w-full flex flex-col gap-4">
<div class="w-full py-12">
<slider-stubborn
ref="slider"
v-model="value"
class="w-full"
durable
thumb-color="#34c600"
joint-color="#a0d48e"
/>
</div>
<div class="flex gap-4">
<base-btn
:label="t('repair')"
class="flex-1"
@click="sliderRef?.repair()"
/>
<base-btn
:label="t('break')"
class="flex-1"
@click="sliderRef?.break()"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseBtn from '../../base-btn.vue'
import SliderStubborn from '../slider-fragile.vue'
const sliderRef = useTemplateRef('slider')
const value = ref(50)
const { t } = useI18n()
</script>

寧死不屈的甜度 ◝(´・ω・`)◟

飲料類型:
甜度:10 分
冰量:0 分
<template>
<div class="mx-auto max-w-[28rem] w-full flex flex-col gap-6 py-20">
<div class="example-wrap flex flex-col items-start gap-4">
<div class="font-bold">
{{ t('drinkType') }}
</div>
<div class="w-full flex flex-wrap justify-between gap-4 whitespace-nowrap">
<label
v-for="item, i in list"
:key="i"
>
<input
v-model="value"
type="radio"
:value="item"
>
{{ t(`drinkOptions.${item}`) }}
</label>
</div>
</div>
<div class="grid grid-cols-6 w-full flex-nowrap items-center gap-8">
<div class="col-span-2 text-lg">
{{ t('sweetness') }}:10 {{ t('points') }}
</div>
<div class="col-span-4 pr-4">
<slider-fragile
ref="slider"
:model-value="10"
disabled
:max="10"
/>
</div>
</div>
<div class="grid grid-cols-6 w-full flex-nowrap items-center gap-8">
<div class="col-span-2 text-lg">
{{ t('ice') }}:{{ iceValue }} {{ t('points') }}
</div>
<div class="col-span-4 pr-4">
<slider-fragile
v-model="iceValue"
durable
:step="3"
:max="9"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import SliderFragile from '../slider-fragile.vue'
const sliderRef = useTemplateRef('slider')
const { t } = useI18n()
const value = ref('綠茶')
const list = [
'綠茶',
'紅茶',
'蜂蜜奶茶',
'鮮奶茶',
'泰奶',
'水果茶',
'檸檬茶',
]
const iceValue = ref(0)
watch(value, () => {
sliderRef.value?.repair()
})
</script>

過低時斷給你看,具現化何謂一分錢一分貨 (◞‸◟ )

預算:$5000
<template>
<div class="mx-auto max-w-[20rem] w-full flex flex-col gap-6 py-[20vh]">
<div class="w-full flex flex-col flex-nowrap gap-8">
<div class="flex-1 text-lg font-bold">
{{ t('budget') }}${{ data }}
</div>
<div class="flex-1">
<slider-fragile
ref="slider"
v-model="data"
durable
:max="10000"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, useTemplateRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import SliderFragile from '../slider-fragile.vue'
const sliderRef = useTemplateRef('slider')
const { t } = useI18n()
const data = ref(5000)
watch(data, (value) => {
if (value < 1000) {
sliderRef.value?.break()
}
else {
sliderRef.value?.repair()
}
})
</script>

主要由兩個元件組成:

slider-fragile.vue 負責處理滑動條的基本邏輯,像是計算數值、偵測滑鼠是否在範圍內。

它會將計算好的比例 (ratio)、滑鼠互動狀態 (isHeld) 等資訊傳遞給握把元件 slider-fragile-thumb.vue。

斷裂與移動效果皆由 slider-fragile-thumb.vue 負責。

當使用者按住握把 (isHeld 為 true) 並拖曳時,計算滑鼠指標相對於握把中心的距離。

距離越大,震動效果越大,模擬被用力拉扯的感覺。

有以下狀態:

  • 正常狀態: 握把根據父元件傳來的 ratio 定位在滑動條上。

  • 斷裂: 若拖曳的距離超過設定的 breakLength 閾值且時間超過 minSecondsToBreak,isBroken 會變成 true,表示握把斷裂。

  • 斷裂後:

    • 放開滑鼠時,執行物理模擬,給予握把一個初始速度 (依放開前滑鼠的移動方向),然後模擬重力、空氣阻力,並在碰到視窗邊界時反彈,直到速度減緩至停止 (thumbData.sleep)。
    • 斷裂後如果再次按住握把,它會跟隨滑鼠移動。

比較特別的部分是正常狀態與斷裂狀態定位方式不同,正常狀態是 absolute 定位,斷裂狀態是 fixed 定位。

interface Props {
modelValue: number;
disabled?: boolean;
min?: number;
max?: number;
step?: number;
thumbSize?: number;
thumbColor?: string;
jointColor?: string;
trackClass?: string;
durable?: boolean;
breakLength?: number;
minSecondsToBreak?: number;
}
const emit = defineEmits<{
'update:modelValue': [value: Props['modelValue']];
}>()
interface Expose {
repair: () => void;
break: () => void;
}