每條游來游去的小魚,都是正在瀏覽這一頁的訪客,有一條是你自己 ( ´ ▽ ` )ノ 覺得干擾閱讀的話,可以把小魚關掉。
物理包裝器
wrapper產生物理世界,讓內部元素具有物理效果 ヾ(⌐■_■)ノ♪
| 名稱 | 描述 |
|---|---|
| Matter-js | 2D 物理引擎 |
| Vue Provide/Inject | Vue 的依賴注入機制,允許父元件向其所有後代元件提供資料或方法 |
開始運行後,被 body 包裹的元素會產生物理效果
I
T
語
錄
若有雷同,
那就雷同
•
世界上
有 10 種
人
,
一種
懂二進位
,
一種不懂
•
要改的太多了
,不如
改天
吧
•
人
和
程式
,一個能
跑
就行
•
在我的
機器
上
可以 work
•
這 TMD 誰
寫
.
.
.
啊是我
•
這不是
bug
,這是
feature
•
你有
試過
重啟
嗎?
•
昨天
還
好好的
<template> <div class="example-wrap w-full flex flex-col gap-4"> <wrapper-physics ref="wrapperRef" class="h-[30rem] w-full flex flex-col items-start gap-1 p-4 md:w-[40rem]" > <div class="flex gap-8 pl-4 md:gap-20 md:pl-10"> <wrapper-physics-body> <base-btn :label="t('start')" @click="wrapperRef?.start()" /> </wrapper-physics-body> <wrapper-physics-body> <base-btn :label="t('reset')" @click="wrapperRef?.reset()" /> </wrapper-physics-body> </div>
<div v-for="section, i in sections" :key="i" class="flex flex-wrap" :class="section.class" > <wrapper-physics-body v-for="text, j in section.textList" :key="j" class="whitespace-nowrap" > {{ text }} </wrapper-physics-body> </div>
<div class="flex-1" />
<wrapper-physics-body is-static> <base-btn label="🐟" /> </wrapper-physics-body> </wrapper-physics> </div></template>
<script setup lang="ts">import { computed, ref } from 'vue'import { useI18n } from 'vue-i18n'import BaseBtn from '../../base-btn.vue'import WrapperPhysicsBody from '../wrapper-physics-body.vue'import WrapperPhysics from '../wrapper-physics.vue'
const { t } = useI18n()const wrapperRef = ref<InstanceType<typeof WrapperPhysics>>()
const sections = computed(() => { const result = [ { class: 'text-lg font-bold mt-2', textList: t('title').split(''), }, { class: 'text-sm opacity-50 mb-2', textList: t('subtitle').split('~'), }, { textList: t('quote1').split('~'), }, { textList: t('quote2').split('~'), }, { textList: t('quote3').split('~'), }, { textList: t('quote4').split('~'), }, { textList: t('quote5').split('~'), }, { textList: t('quote6').split('~'), }, { textList: t('quote7').split('~'), }, { textList: t('quote8').split('~'), }, ]
return result})</script>調整物理性質,讓元素嗨起來!ᕕ( ゚ ∀。)ᕗ
鱈魚
真
的
不
是
一
種
很肥
的
🐟
🐟
鱈魚:「我怎麼覺得這個範例偷嘴我?╭(°A ,°`)╮」
查看範例原始碼
<template> <div class="w-full flex flex-col gap-4 example-wrap"> <div class="flex gap-2"> <base-btn :label="t('start')" @click="wrapperRef?.start()" /> <base-btn :label="t('reset')" @click="wrapperRef?.reset()" /> </div>
<wrapper-physics ref="wrapperRef" class="h-[30rem] w-full flex flex-col items-center justify-center border border-dashed md:w-[40rem]" > <div class="flex"> <wrapper-physics-body v-for="item, i in list" :key="i" v-bind="item" > {{ item.text }} </wrapper-physics-body> </div>
<wrapper-physics-body> 🐟 </wrapper-physics-body> </wrapper-physics> </div></template>
<script setup lang="ts">import { computed, ref } from 'vue'import { useI18n } from 'vue-i18n'import BaseBtn from '../../base-btn.vue'import WrapperPhysicsBody from '../wrapper-physics-body.vue'import WrapperPhysics from '../wrapper-physics.vue'
type BodyProps = InstanceType<typeof WrapperPhysicsBody>['$props']
interface Item extends BodyProps { text: string;}
const { t } = useI18n()const wrapperRef = ref<InstanceType<typeof WrapperPhysics>>()
/** 回彈力設為 1.5,表示回彈速度會是進入速度的 1.5 倍 * * 畫面會超嗨喔!ᕕ( ゚ ∀。)ᕗ */const restitution = 1.5
const list = computed<Item[]>(() => [ { text: t('codfish'), isStatic: true }, { text: t('really'), restitution }, { text: t('not'), restitution }, { text: t('is'), restitution }, { text: t('a'), frictionAir: 1 }, { text: t('kind'), restitution }, { text: t('of'), restitution }, { text: t('veryFat'), isStatic: true }, { text: t('de'), restitution }, { text: '🐟', restitution },])</script>調整重力方向,讓元素飄起來!(ノ>ω<)ノ
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
重力方向:
<template> <div class="w-full flex flex-col gap-4 example-wrap"> <wrapper-physics v-bind="env" immediate class="h-[30rem] w-full flex flex-col items-center justify-center border border-dashed md:w-[40rem]" > <div class="flex flex-wrap justify-center"> <wrapper-physics-body v-for="item in list" :key="item" v-bind="bodyProp" class="select-none" :class="item" > 🐟 </wrapper-physics-body> </div> </wrapper-physics> </div>
<div class="flex flex-col gap-6 p-4 md:flex-row"> <div> {{ t('gravityDirection') }} <analog-stick size="14rem" @trigger="handleTrigger" /> </div>
<div class="flex-col"> <base-input v-model.number="bodyProp.friction" type="range" :label="`${t('friction')}: ${bodyProp.friction}`" class="w-full" :min="0" :step="0.01" :max="1" /> <base-input v-model.number="bodyProp.frictionAir" type="range" :label="`${t('airFriction')}: ${bodyProp.frictionAir}`" class="w-full" :min="0" :step="0.01" :max="1" /> <base-input v-model.number="bodyProp.restitution" type="range" :label="`${t('restitution')}: ${bodyProp.restitution}`" class="w-full" :min="0" :step="0.01" :max="1.1" /> </div> </div></template>
<script setup lang="ts">import { ref } from 'vue'import { useI18n } from 'vue-i18n'import AnalogStick from '../../analog-stick.vue'import BaseInput from '../../base-input.vue'import WrapperPhysics from '../wrapper-physics.vue'import WrapperPhysicsBody from '../wrapper-physics-body.vue'
type BodyProp = InstanceType<typeof WrapperPhysicsBody>['$props']
const { t } = useI18n()
const env = ref({ gravity: { scale: 0.001, x: 0, y: 1, },})
const bodyProp = ref<BodyProp>({ frictionAir: 0.01, friction: 0.01, restitution: 1,})
const list = [ 'text-base', 'text-lgF', 'text-xl', 'text-2xl', 'text-3xl', 'text-4xl', 'text-5xl', 'text-6xl', 'text-7xl',]
function handleTrigger(e: { x: number; y: number }) { env.value.gravity.x = e.x env.value.gravity.y = e.y}</script>結合滾動效果,將捲動之速度映射至重力加速度,製造上下搖晃效果
🐟
🐟
🐟
🐟
🐟
🐟
鱈魚:「夭壽虐魚喔!Σ(っ °Д °;)っ」
查看範例原始碼
<template> <div class="w-full flex flex-col gap-4 example-wrap"> <wrapper-physics v-bind="env" immediate class="h-[100vh] w-full flex items-start justify-center" > <div class="flex"> <wrapper-physics-body v-for="item in list" :key="item" v-bind="bodyProp" class="select-none" :class="item" > 🐟 </wrapper-physics-body> </div> </wrapper-physics> </div></template>
<script setup lang="ts">import { refAutoReset, useWindowScroll,} from '@vueuse/core'import { clamp } from 'remeda'import { computed, watch } from 'vue'import WrapperPhysics from '../wrapper-physics.vue'import WrapperPhysicsBody from '../wrapper-physics-body.vue'
const { y } = useWindowScroll()
const gravityScale = refAutoReset(0.001, 200)watch(y, (value, prevValue) => { const delta = clamp( value - prevValue, { max: 100, min: -100 }, )
gravityScale.value = 0.001 + delta * 0.00015})
const env = computed(() => ({ gravity: { scale: gravityScale.value, x: 0, y: 1, },}))
const bodyProp = { frictionAir: 0, friction: 0, restitution: 0.5,}
const list = [ 'text-base', 'text-xl', 'text-2xl', 'text-4xl', 'text-5xl', 'text-7xl',]</script>調整物體碰撞箱(hit box)外型,例如圓形才符合鱈魚的真實體型。╮(╯▽╰)╭
鱈魚:「現在是當我聽不到是不是?ಠ_ಠ」
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
重力方向:
<template> <div class="example-wrap w-full flex flex-col gap-4"> <wrapper-physics v-bind="env" immediate class="h-[30rem] w-full flex flex-wrap content-center items-center justify-center md:w-[40rem]" > <template v-for="item in list"> <wrapper-physics-body v-for="index in item.quantity" :key="index" v-bind="bodyProp" class="aspect-square flex select-none items-center justify-center rounded-full bg-slate-300 dark:bg-slate-600" :class="item.class" > 🐟 </wrapper-physics-body> </template> </wrapper-physics> </div>
<div class="flex gap-6 p-4"> <div> {{ t('gravityDirection') }} <analog-stick size="14rem" @trigger="handleTrigger" /> </div> </div></template>
<script setup lang="ts">import { ref } from 'vue'import { useI18n } from 'vue-i18n'import AnalogStick from '../../analog-stick.vue'import WrapperPhysicsBody from '../wrapper-physics-body.vue'import WrapperPhysics from '../wrapper-physics.vue'
const { t } = useI18n()
const env = ref({ gravity: { scale: 0.001, x: 0, y: 1, },})
const bodyProp: InstanceType<typeof WrapperPhysicsBody>['$props'] = { polygon: 'circle', frictionAir: 0, friction: 0, restitution: 0.7,}
function handleTrigger(e: { x: number; y: number }) { env.value.gravity.x = e.x env.value.gravity.y = e.y}
const list = [ { class: 'w-14 h-14 text-3xl', quantity: 2, }, { class: 'w-10 h-10', quantity: 3, }, { class: 'w-6 h-6 text-xs', quantity: 10, },]</script>Scope Prop
Section titled “Scope Prop”使用 Scope Prop 玩出更多花樣
改
不
盡
的
需
求
與
鍵
盤
的
呢
喃
交
織
隨
時
間
凋
零
,
不
是
任
務
而
是
我
不
再
新
鮮
的
肝
<template> <div class="example-wrap w-full flex flex-col gap-4"> <div class="flex gap-2"> <base-btn :label="t('start')" @click="wrapperRef?.start()" /> <base-btn :label="t('reset')" @click="wrapperRef?.reset()" /> </div>
<wrapper-physics v-bind="env" ref="wrapperRef" class="h-[30rem] w-full flex flex-row-reverse items-center justify-center gap-4 border border-dashed md:w-[40rem]" > <div v-for="text, i in list" :key="i" class="flex flex-col" > <wrapper-physics-body v-for="word, j in text.split('')" v-slot="scope" :key="j" polygon="circle" :friction-air="(j + i) * 0.005" > <span :style="getStyle(scope)"> {{ word }} </span> </wrapper-physics-body> </div> </wrapper-physics> </div></template>
<script setup lang="ts">import type { CSSProperties } from 'vue'import { computed, ref } from 'vue'import { useI18n } from 'vue-i18n'import { getVectorLength, mapNumber } from '../../../common/utils'import BaseBtn from '../../base-btn.vue'import WrapperPhysicsBody from '../wrapper-physics-body.vue'import WrapperPhysics from '../wrapper-physics.vue'
type ScopeProps = Parameters< NonNullable<InstanceType<typeof WrapperPhysicsBody>['$slots']['default']>>[0]
const { t } = useI18n()const wrapperRef = ref<InstanceType<typeof WrapperPhysics>>()
const env = ref({ gravity: { scale: 0.0005, x: -1, y: -0.3, },})
const list = computed(() => [ t('poemLine1'), t('poemLine2'), t('poemLine3'), t('poemLine4'),])
function getStyle(scope: ScopeProps): CSSProperties { /** 計算偏移距離 */ const distance = getVectorLength({ x: scope.offsetX, y: scope.offsetY, })
/** 偏越多越淡 */ const opacity = mapNumber( distance, 0, scope.height * 5, 1, 0, )
return { opacity }}</script>配合手機的加速度計,做出更有趣的互動!(/≧▽≦)/
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
無法取得此設備的感測器。(>人<;)
<template> <div class="relative w-full flex flex-col gap-4 example-wrap"> <wrapper-physics v-bind="env" immediate class="h-[30rem] w-full flex flex-wrap content-center items-center justify-center md:w-[40rem]" > <template v-for="item in list"> <wrapper-physics-body v-for="index in item.quantity" :key="index" v-bind="bodyProp" class="aspect-square flex select-none items-center justify-center rounded-full bg-slate-300 dark:bg-slate-600" :class="item.class" > 🐟 </wrapper-physics-body> </template> </wrapper-physics>
<div v-if="!isSupport" class="not-support-box" > <div class="rounded bg-red-500 p-5 text-white"> {{ t('sensorNotAvailable') }} </div> </div> </div></template>
<script setup lang="ts">import { throttleFilter, useDeviceMotion } from '@vueuse/core'import { isNullish, pipe } from 'remeda'import { computed, ref, watch } from 'vue'import { useI18n } from 'vue-i18n'import { getVectorLength, mapNumber } from '../../../common/utils'import WrapperPhysics from '../wrapper-physics.vue'import WrapperPhysicsBody from '../wrapper-physics-body.vue'
const { t } = useI18n()
const { accelerationIncludingGravity: acceleration,} = useDeviceMotion({ eventFilter: throttleFilter(35),})
const isSupport = computed(() => { if (acceleration.value === null) { return false }
return acceleration.value.x !== null})
watch(acceleration, (value) => { if (!isSupport.value || !value) return
updateEnv(value)}, { deep: true })
const env = ref({ gravity: { scale: 0.001, x: 0, y: 1, },})
function updateEnv(acc: DeviceMotionEventAcceleration) { const { x, y } = acc if (isNullish(x) || isNullish(y)) return
const scale = pipe( getVectorLength({ x, y }), /** 重力最大值為 9.8 */ (value) => mapNumber( value, 0, 9.8, 0, 0.0005, ), )
/** x 方向與環境 x 軸相反 */ env.value.gravity = { scale, x: -x, y, }}
const bodyProp = { frictionAir: 0.01, friction: 0.01, restitution: 0.1,}
const list = [ { class: 'w-14 h-14 text-3xl', quantity: 2, }, { class: 'w-10 h-10', quantity: 3, }, { class: 'w-6 h-6 text-xs', quantity: 10, },]</script>
<style lang="sass">.not-support-box position: absolute width: 100% height: 100% display: flex justify-content: center align-items: center</style>委婉的報價單
Section titled “委婉的報價單”預算過低時,原地解散!◝( •ω• )◟
請選擇您的預算範圍:
營業時間:全天
無
休
E
-
m
a
i
l
:
h
i
@
c
o
d
l
i
n
.
m
e
<template> <div class="w-full flex flex-col gap-4 example-wrap"> <!-- 預算 radio --> <div class="flex flex-col items-start gap-4 example-wrap"> <div class="font-bold"> {{ t('selectBudget') }} </div>
<div class="w-full flex flex-wrap justify-between gap-4 whitespace-nowrap"> <label v-for="item, i in budgetList" :key="i" class="flex-1 cursor-pointer border rounded p-4 duration-300 hover:bg-gray-200/50" :class="{ active: budget === i }" > <input v-model="budget" type="radio" :value="i" > {{ item.text }} </label> </div> </div>
<wrapper-physics ref="wrapperRef" class="h-[30vh] w-full flex flex-col items-center justify-center gap-2" :class="{ 'select-none': budget === 0 }" > <div v-for="row, i in rows" :key="i" class="min-h-[1rem] flex" > <wrapper-physics-body v-for="item in row" :key="item.text" :restitution="0.98" :friction="0.005" :friction-air="0.005" v-bind="item" > {{ item.text }} </wrapper-physics-body> </div> </wrapper-physics> </div></template>
<script setup lang="ts">import { computed, ref, watchEffect } from 'vue'import { useI18n } from 'vue-i18n'import WrapperPhysicsBody from '../wrapper-physics-body.vue'import WrapperPhysics from '../wrapper-physics.vue'
type BodyProps = InstanceType<typeof WrapperPhysicsBody>['$props']
interface Row extends BodyProps { text: string;}
const { t } = useI18n()const wrapperRef = ref<InstanceType<typeof WrapperPhysics>>()
const rows = computed<Row[][]>(() => [ [ { text: t('businessHours'), isStatic: true }, { text: t('no'), restitution: 1.4 }, { text: t('rest'), isStatic: true }, ], 'E-mail:hi@codlin.me'.split('').map((char) => ({ text: char })),])
const budget = ref(-1)const budgetList = computed(() => [ { text: t('budgetLow') }, { text: t('budgetMid') }, { text: t('budgetHigh') },])
watchEffect(() => { if (budget.value === 0) { wrapperRef.value?.start() } else { wrapperRef.value?.reset() }})</script>以 Matter.js 模擬物理效果,再將結果同步到 DOM 元素上。
具體流程如下:
- wrapper-physics 收集內部註冊的 wrapper-physics-body。
- 根據 body 的位置、尺寸與 prop,建立對應的 Matter.js 物體。
- 透過 Matter.js 模擬物理效果並將物理效果儲存。
- body 取得儲存的物理效果,並同步至 DOM 元素上。
如此這般,我們成功實現物理效果模擬了!就像是替身一樣!(ノ>ω<)ノ
JOJO!我不想當 DOM 了!(⊙益⊙)
詳細說明請見此文章。
需要兩個元件,分別為:
- wrapper-physics:產生物理世界。
- wrapper-physics-body:定義物體,放在物理世界中。
🧩 WrapperPhysics
Section titled “🧩 WrapperPhysics”定義物理世界與特性,對其中物體產生交互作用。
interface Props { /** 立即開始,物體會在元件建立完成後馬上會開始掉落 */ immediate?: boolean;
/** 重力加速度 * * x, y 為加速度的方向,scale 為加速度的大小 */ gravity?: Matter.Gravity;}Methods
Section titled “Methods”interface Expose { /** 開始 */ start: () => void; /** 重置所有元素,元素會回到初始位置 */ reset: () => void;}🧩 WrapperPhysicsBody
Section titled “🧩 WrapperPhysicsBody”定義物體與性質,放在物理世界中,會受其物理規則影響。
interface Props { /** 物體形狀,預設為 rectangle * * - rectangle:尺寸同 DOM 之長寬 * - circle:取 DOM 長寬最大值為直徑 */ polygon?: 'rectangle' | 'circle';
/** 空氣阻力。物體在空氣中受到的阻力 */ frictionAir?: number; /** 摩擦力。物體本身的摩擦力,必須為 0 ~ 1,0 表示持續滑動,1 表示受力後會立即停止 */ friction?: number; /** 回彈力。碰撞的回彈係數,0 表示不反彈,1 表示完全反彈 */ restitution?: number; /** 物體質量 */ mass?: number; /** 靜止。會變成像地面那樣完全靜止的存在 */ isStatic?: boolean;}defineSlots<{ default?: (props: { width: number; height: number; /** X 偏移量 */ offsetX: number; /** Y 偏移量 */ offsetY: number; /** 旋轉量 */ rotate: number; }) => unknown;}>()