Skip to content

Every little fish swimming around is a visitor on this page right now, and one of them is you ( ´ ▽ ` )ノ If they get in the way of reading, feel free to turn them off.

Water Clock util ​

Using water as ink to write the current time on glass, stroke by stroke. ◝( •ω• )◟

Tech Keywords ​

NameDescription
Canvas ShaderWritten in GLSL and executed directly on the GPU, faster than the Canvas 2D API but harder to master
WebGL2 Instanced RenderingDraws large numbers of identical geometries at once via WebGL2 instanced drawing, greatly reducing draw calls
MetaballEach object emits a smooth potential; wherever the summed potential exceeds a threshold becomes the surface, so nearby objects blend together naturally
Height MapA texture that stores how high each point is; neighbouring differences give the slope, and the slope gives the normals that drive lighting and refraction
CausticsLight 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
FresnelReflection grows stronger at grazing angles, which is why ice and glass glow at the edges and stay clear in the middle
DOM to ImageConverts DOM elements into images, based on SVG foreignObject
Particle SystemSpawns large numbers of small objects, commonly used to simulate smoke, fire, rain, and snow
Simplex NoiseImproved Perlin Noise that computes faster without directional bias, commonly used in procedural generation
Intl.DateTimeFormatBuilt-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. ( ´ ▽ ` )ノ

Whose rain is outside the window
Rice field in the morning light behind the glass
View example source code
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>

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,
})

v0.91.0