Water Clock util
Using water as ink to write the current time on glass, stroke by stroke. ◝( •ω• )◟
Tech Keywords
| Name | Description |
|---|---|
| Canvas Shader | Written in GLSL and executed directly on the GPU, faster than the Canvas 2D API but harder to master |
| WebGL2 Instanced Rendering | Draws large numbers of identical geometries at once via WebGL2 instanced drawing, greatly reducing draw calls |
| Metaball | Each object emits a smooth potential; wherever the summed potential exceeds a threshold becomes the surface, so nearby objects blend together naturally |
| Height Map | A texture that stores how high each point is; neighbouring differences give the slope, and the slope gives the normals that drive lighting and refraction |
| Caustics | Light refracted by a curved transparent surface gathers into bright patterns behind it, like the light web on a pool floor or the bright spot inside a water drop's shadow |
| Fresnel | Reflection grows stronger at grazing angles, which is why ice and glass glow at the edges and stay clear in the middle |
| DOM to Image | Converts DOM elements into images, based on SVG foreignObject |
| Particle System | Spawns large numbers of small objects, commonly used to simulate smoke, fire, rain, and snow |
| Simplex Noise | Improved Perlin Noise that computes faster without directional bias, commonly used in procedural generation |
| Intl.DateTimeFormat | Built-in date and time formatting that supports time zones and 12/24-hour cycles |
Usage Examples
Basic Usage
Whatever you put in the slot gets refracted by the water, like looking through a rainy window.
Every second, the beads of the last digit evaporate and a single drop drags out the new number. Leave the digits alone for a while and water gathers along their bottom edge, then trickles down the glass.
fog mists up the glass: dry areas blur into a haze while water wipes the mist away and stays clear, so the digits written in water stand out, like writing on a fogged-up window.
The photo already has its own lighting, so the example lowers shadow-strength; on a plain white background the water shows up only through light and shade, so set it back to 1. The larger refraction is, the stronger the distortion around each drop's rim. time-zone sets the time zone, so you can check what time it is in another city. ( ´ ▽ ` )ノ

View example source code
<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>How it Works
The digits are trails left by a drop dragged along each stroke, with no font involved. The film quickly breaks into beads of different sizes, metaballs join nearby beads into a beaded stream, and drops that grow heavy enough slide down the glass, swallowing any water in their way.
All the light and shade is computed physically. Sight refracts through each drop by Snell's law, magnifying and distorting the content behind; light passing through the drops lands on the page as shadows and caustics. White bent in any direction is still white, so on a pure white background the water shows up through this light and shade alone. (≖‿ゝ≖)✧
Source Code
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,
})