水時鐘 util
拿水當墨水,在玻璃上一筆一筆寫出現在的時間。◝( •ω• )◟
技術關鍵字
| 名稱 | 描述 |
|---|---|
| Canvas Shader | 使用 GLSL 開發,直接在 GPU 上執行,比 Canvas 2D API 更快,但也更難 |
| WebGL2 Instanced Rendering | 透過 WebGL2 的 Instanced Drawing 一次繪製大量相同幾何體,大幅減少 draw call |
| Metaball | 每個物體在空間中散發一個平滑的位能,疊加後超過門檻的地方就是表面,靠近時會自然黏成一團 |
| 高度圖 | 用一張貼圖記錄每個點有多高,相鄰兩點相減就是坡度,由坡度推出法線,光影與折射都靠它 |
| 焦散 | 光穿過彎曲的透明表面時被折射聚集,在後方投出一塊塊亮紋,泳池底的光網、水珠影子裡的亮點都是它 |
| Fresnel | 視線越貼近表面反射越強的現象,冰塊、玻璃邊緣發亮、正面透明就是它 |
| DOM to Image | 將 DOM 元素轉換為圖片的技術,基於 SVG foreignObject 實現 |
| 粒子系統 | 產生大量小物件的系統,常用於模擬煙霧、火焰、雨雪等效果 |
| Simplex Noise | 改良版 Perlin Noise,計算更快且無方向性偏差,常用於程序化生成 |
| Intl.DateTimeFormat | 瀏覽器內建的日期時間格式化,可以指定時區與 12/24 小時制 |
使用範例
基本用法
窗外是哪裡的雨

查看範例原始碼
vue
<template>
<div class="w-full flex flex-col gap-4 py-2">
<div class="example-ctrl grid grid-cols-2 gap-4 p-4 lg:grid-cols-4">
<div class="flex flex-col gap-1">
<span class="text-sm font-bold">
{{ t('city') }}
</span>
<base-select
v-model="timeZone"
:options="TIME_ZONE_LIST"
:option-label-map="timeZoneLabelMap"
/>
</div>
<base-checkbox
v-model="options.showSeconds"
:label="t('showSeconds')"
/>
<base-checkbox
v-model="options.hour12"
:label="t('hour12')"
/>
<base-input
v-model.number="options.fog"
type="range"
:label="`${t('fog')}: ${options.fog}`"
:min="0"
:max="1"
:step="0.05"
/>
<base-input
v-model.number="options.refraction"
type="range"
:label="`${t('refraction')}: ${options.refraction}`"
:min="0"
:max="3"
:step="0.05"
/>
<base-input
v-model.number="options.shadowStrength"
type="range"
:label="`${t('shadowStrength')}: ${options.shadowStrength}`"
:min="0"
:max="2"
:step="0.05"
/>
<base-input
v-model.number="options.lightAngle"
type="range"
:label="`${t('lightAngle')}: ${options.lightAngle}°`"
:min="0"
:max="359"
:step="1"
/>
<base-input
v-model.number="options.strokeWidth"
type="range"
:label="`${t('strokeWidth')}: ${options.strokeWidth}`"
:min="0.5"
:max="2"
:step="0.05"
/>
<base-input
v-model.number="options.rainAmount"
type="range"
:label="`${t('rainAmount')}: ${options.rainAmount}`"
:min="0"
:max="1"
:step="0.05"
/>
</div>
<util-water-clock
class="aspect-video rounded-xl"
:time-zone="timeZone"
:show-seconds="options.showSeconds"
:hour12="options.hour12"
:fog="options.fog"
:refraction="options.refraction"
:shadow-strength="options.shadowStrength"
:light-angle="options.lightAngle"
:stroke-width="options.strokeWidth"
:rain-amount="options.rainAmount"
>
<img
src="/photography-morning-light-of-rice.jpg"
:alt="t('sceneAlt')"
class="h-full w-full object-cover"
>
</util-water-clock>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseCheckbox from '../../base-checkbox.vue'
import BaseInput from '../../base-input.vue'
import BaseSelect from '../../base-select.vue'
import UtilWaterClock from '../util-water-clock.vue'
type TimeZone = 'Asia/Taipei' | 'Asia/Tokyo' | 'Europe/London' | 'America/New_York'
const TIME_ZONE_LIST: TimeZone[] = ['Asia/Taipei', 'Asia/Tokyo', 'Europe/London', 'America/New_York']
const { t } = useI18n()
const timeZone = ref<TimeZone>('Asia/Taipei')
const timeZoneLabelMap = computed<Record<TimeZone, string>>(() => ({
'Asia/Taipei': t('taipei'),
'Asia/Tokyo': t('tokyo'),
'Europe/London': t('london'),
'America/New_York': t('newYork'),
}))
const options = ref({
showSeconds: false,
hour12: false,
fog: 0.2,
refraction: 3,
shadowStrength: 0.2,
lightAngle: 315,
strokeWidth: 1,
rainAmount: 0.7,
})
</script>原理
數字是一顆水珠照筆順拖出的水痕,沒有用到字型。水膜撐不住,很快斷成一串大小不一的水珠,靠得近的再用 Metaball 黏成一道水流;夠大的水珠會沿玻璃滑落,沿路併吞碰到的水。
光影都依物理計算。視線穿過水珠時依 Snell 定律折射,後方內容因此放大、扭曲;光線穿過水珠落到頁面上,形成影子與焦散。白色怎麼折射都還是白色,純白底上的水就靠這些光影現形。(≖‿ゝ≖)✧
原始碼
API
Props
interface Props {
/** 顯示秒數。@default true */
showSeconds?: boolean;
/** 12 小時制。@default false */
hour12?: boolean;
/** IANA 時區,例如 `'Asia/Tokyo'`。未指定時用裝置時區 */
timeZone?: string;
/** 字高(px)。未指定時依容器大小自動縮放 */
digitHeight?: number;
/** 寫出數字的水珠大小倍率,1 為預設大小。@default 1 */
strokeWidth?: number;
/** 背景雨量:雨珠與玻璃上的霧珠,0 為不下雨、1 為大雨。@default 0.3 */
rainAmount?: number;
/**
* 折射強度,實際上是水面到背景的距離倍率。
* 越大,水珠外圈扭曲後方內容的幅度越大,再大就上下顛倒;影子一直貼著水珠,不受影響。@default 3
*/
refraction?: number;
/**
* 直射光強度:水珠投在背景上的影子與焦散都來自它。
* 白色怎麼折射都還是白色,純白底上的水全靠這些光影現形,0 就只剩折射與反射。@default 1
*/
shadowStrength?: number;
/**
* 玻璃起霧的程度,0 為清澈、1 為霧最濃。
* 起霧時乾的地方會模糊,有水的地方看出去仍然清楚,水寫的字會特別顯眼。@default 0.2
*/
fog?: number;
/** 光從哪個方向照過來(度),0 為正上方、順時針增加。@default 315 */
lightAngle?: number;
/** canvas 的 z-index。@default 1 */
zIndex?: number;
/**
* 擷取玻璃後方內容的倍率(相對 CSS 像素,已含裝置像素比),越低越快、紋理越糊。
* 未指定時用 `min(devicePixelRatio, 2)`
*/
captureScale?: number;
}Slots
defineSlots<{
/** 玻璃後方的內容,水會折射它 */
default?: () => unknown;
}>()Methods
defineExpose({
/** 重新擷取玻璃後方的內容。內容是 canvas、影片這類 DOM 觀察不到的變化時,改完要手動呼叫 */
recapture,
})