物理包裝器 wrapper
產生物理世界,讓內部元素具有物理效果 ヾ(⌐■_■)ノ♪
使用範例
基本用法
開始運行後,被 body 包裹的元素會產生物理效果
I
T
語
錄
若有雷同,
那就雷同
•
世界上
有 10 種
人
,
一種
懂二進位
,
一種不懂
•
要改的太多了
,不如
改天
吧
•
人
和
程式
,一個能
跑
就行
•
在我的
機器
上
可以 work
•
這 TMD 誰
寫
.
.
.
啊是我
•
這不是
bug
,這是
feature
•
你有
試過
重啟
嗎?
•
昨天
還
好好的
查看範例原始碼
vue
<template>
<div class="w-full flex flex-col gap-4 border border-gray-300">
<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-20 pl-10">
<wrapper-physics-body>
<base-btn
label="開始"
@click="wrapperRef?.start()"
/>
</wrapper-physics-body>
<wrapper-physics-body>
<base-btn
label="重置"
@click="wrapperRef?.reset()"
/>
</wrapper-physics-body>
</div>
<div
v-for="section, i in sections"
:key="i"
class="flex"
:class="section.class"
>
<wrapper-physics-body
v-for="text, j in section.texts"
: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 { ref } from 'vue'
import BaseBtn from '../../base-btn.vue'
import WrapperPhysics from '../wrapper-physics.vue'
import WrapperPhysicsBody from '../wrapper-physics-body.vue'
const wrapperRef = ref<InstanceType<typeof WrapperPhysics>>()
const sections = [
{
class: 'text-lg font-bold mt-2',
texts: ['I', 'T', '語', '錄'],
},
{
class: 'text-sm opacity-50 mb-2',
texts: ['若有雷同,', '那就雷同'],
},
{
texts: ['• ', '世界上', '有 10 種', '人', ',', '一種', '懂二進位', ',', '一種不懂'],
},
{
texts: ['• ', '要改的太多了', ',不如', '改天', '吧'],
},
{
texts: ['• ', '人', '和', '程式', ',一個能', '跑', '就行'],
},
{
texts: ['• ', '在我的', '機器', '上', '可以 work'],
},
{
texts: ['• ', '這 TMD 誰', '寫', '.', '.', '.', '啊是我'],
},
{
texts: ['• ', '這不是', 'bug', ',這是', 'feature'],
},
{
texts: ['• ', '你有', '試過', '重啟', '嗎?'],
},
{
texts: ['• ', '昨天', '還', '好好的'],
},
]
</script>
物理性質
調整物理性質,讓元素嗨起來!ᕕ( ゚ ∀。)ᕗ
鱈魚
真
的
不
是
一
種
很肥
的
🐟
🐟
鱈魚:「我怎麼覺得這個範例偷嘴我?╭(°A ,°`)╮」
查看範例原始碼
vue
<template>
<div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
<div class="flex gap-2">
<base-btn
label="開始"
@click="wrapperRef?.start()"
/>
<base-btn
label="重置"
@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 { ref } from 'vue'
import BaseBtn from '../../base-btn.vue'
import WrapperPhysics from '../wrapper-physics.vue'
import WrapperPhysicsBody from '../wrapper-physics-body.vue'
type BodyProp = InstanceType<typeof WrapperPhysicsBody>['$props']
interface Item extends BodyProp {
text: string;
}
const wrapperRef = ref<InstanceType<typeof WrapperPhysics>>()
/** 回彈力設為 1.5,表示回彈速度會是進入速度的 1.5 倍
*
* 畫面會超嗨喔!ᕕ( ゚ ∀。)ᕗ
*/
const restitution = 1.5
const list: Item[] = [
{ text: '鱈魚', isStatic: true },
{ text: '真', restitution },
{ text: '的', restitution },
{ text: '不', restitution },
{ text: '是', frictionAir: 1 },
{ text: '一', restitution },
{ text: '種', restitution },
{ text: '很肥', isStatic: true },
{ text: '的', restitution },
{ text: '🐟', restitution },
]
</script>
調整重力
調整重力方向,讓元素飄起來!(ノ>ω<)ノ
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
重力方向:
查看範例原始碼
vue
<template>
<div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
<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">
<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 class="">
重力方向:
<analog-stick
size="14rem"
@trigger="handleTrigger"
/>
</div>
<div class="flex-col">
<base-input
v-model.number="bodyProp.friction"
type="range"
:label="`摩擦力: ${bodyProp.friction}`"
class="w-full"
:min="0"
:step="0.01"
:max="1"
/>
<base-input
v-model.number="bodyProp.frictionAir"
type="range"
:label="`空氣阻力: ${bodyProp.frictionAir}`"
class="w-full"
:min="0"
:step="0.01"
:max="1"
/>
<base-input
v-model.number="bodyProp.restitution"
type="range"
:label="`彈力: ${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 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 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>
視窗滾動
結合滾動效果,將捲動之速度映射至重力加速度,製造上下搖晃效果
🐟
🐟
🐟
🐟
🐟
🐟
鱈魚:「夭壽虐魚喔!Σ(っ °Д °;)っ」
查看範例原始碼
vue
<template>
<div class="w-full flex flex-col gap-4 border border-gray-300">
<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>
<style scoped lang="sass">
</style>
物體形狀
調整物體碰撞箱(hit box)外型,例如圓形才符合鱈魚的真實體型。╮(╯▽╰)╭
鱈魚:「現在是當我聽不到是不是?ಠ_ಠ」
甚麼是碰撞箱(hit box)
碰撞箱是指實際上在物理世界中參與碰撞的實際形狀。
你一定在想為甚麼不用物體本身的形狀?
因為物體通常很複雜,如果使用過度複雜的形狀進行運算會導致性能問題,所以通常都會適當簡化,使用形狀更單純的碰撞箱呦。(. ❛ ᴗ ❛.)
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
重力方向:
查看範例原始碼
vue
<template>
<div class="w-full flex flex-col gap-4 border border-gray-300">
<wrapper-physics
v-bind="env"
immediate
class="h-[30rem] w-full flex 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"
:class="item.class"
>
🐟
</wrapper-physics-body>
</template>
</wrapper-physics>
</div>
<div class="flex gap-6 p-4">
<div class="">
重力方向:
<analog-stick
size="14rem"
@trigger="handleTrigger"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import AnalogStick from '../../analog-stick.vue'
import WrapperPhysics from '../wrapper-physics.vue'
import WrapperPhysicsBody from '../wrapper-physics-body.vue'
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
使用 Scope Prop 玩出更多花樣
改
不
盡
的
需
求
與
鍵
盤
的
呢
喃
交
織
隨
時
間
凋
零
,
不
是
任
務
而
是
我
不
再
新
鮮
的
肝
查看範例原始碼
vue
<template>
<div class="w-full flex flex-col gap-4 border border-gray-300 p-6">
<div class="flex gap-2">
<base-btn
label="開始"
@click="wrapperRef?.start()"
/>
<base-btn
label="重置"
@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 { ref } from 'vue'
import { getVectorLength, mapNumber } from '../../../common/utils'
import BaseBtn from '../../base-btn.vue'
import WrapperPhysics from '../wrapper-physics.vue'
import WrapperPhysicsBody from '../wrapper-physics-body.vue'
type ScopeProp = Parameters<
NonNullable<InstanceType<typeof WrapperPhysicsBody>['$slots']['default']>
>[0]
const wrapperRef = ref<InstanceType<typeof WrapperPhysics>>()
const env = ref({
gravity: {
scale: 0.0005,
x: -1,
y: -0.3,
},
})
const list = [
'改不盡的需求與鍵盤的呢喃交織',
'隨時間凋零,不是任務',
'而是我不再新鮮的',
'肝',
]
function getStyle(scope: ScopeProp): CSSProperties {
/** 計算偏移距離 */
const distance = getVectorLength({
x: scope.offsetX,
y: scope.offsetY,
})
/** 偏越多越淡 */
const opacity = mapNumber(
distance,
0,
scope.height * 5,
1,
0,
)
return { opacity }
}
</script>
手機感測器
配合手機的加速度計,做出更有趣的互動!(/≧▽≦)/
TIP
感謝前端社團和 Line 社群的朋友們提供靈感!
(o゜▽ ゜)o☆
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
🐟
無法取得此設備的感測器。(>人<;)
查看範例原始碼
vue
<template>
<div class="relative w-full flex flex-col gap-4 border border-gray-300">
<wrapper-physics
v-bind="env"
immediate
class="h-[30rem] w-full flex 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"
: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">
無法取得此設備的感測器。(>人<;)
</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 { getVectorLength, mapNumber } from '../../../common/utils'
import WrapperPhysics from '../wrapper-physics.vue'
import WrapperPhysicsBody from '../wrapper-physics-body.vue'
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>
原理
概念為利用 Matter.js 模擬物理效果,並將對應元素之狀態同步至 DOM 元素上。
具體流程如下:
- wrapper-physics 收集內部註冊的 wrapper-physics-body。
- 根據 body 的位置、尺寸與 prop,建立對應的 Matter.js 物體。
- 透過 Matter.js 模擬物理效果並將物理效果儲存。
- body 取得儲存的物理效果,並同步至 DOM 元素上。
如此這般,我們成功實現物理效果模擬了!就像是替身一樣!(ノ>ω<)ノ
JOJO!我不想當 DOM 了!(⊙益⊙)
原始碼
API
需要兩個元件,分別為:
- wrapper-physics:產生物理世界。
- wrapper-physics-body:定義物體,放在物理世界中。
🧩 WrapperPhysics
定義物理世界與特性,對其中物體產生交互作用。
Props
interface Props {
/** 立即開始,物體會在元件建立完成後馬上會開始掉落 */
immediate?: boolean;
/** 重力加速度
*
* x, y 為加速度的方向,scale 為加速度的大小
*/
gravity?: Matter.Gravity;
}
Methods
defineExpose({
/** 開始 */
start,
/** 重置所有元素,元素會回到初始位置 */
reset,
})
🧩 WrapperPhysicsBody
定義物體與性質,放在物理世界中,會受其物理規則影響。
Props
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;
}
Slots
defineSlots<{
default?: (props: {
width: number;
height: number;
/** X 偏移量 */
offsetX: number;
/** Y 偏移量 */
offsetY: number;
/** 旋轉量 */
rotate: number;
}) => unknown;
}>()