每條游來游去的小魚,都是正在瀏覽這一頁的訪客,有一條是你自己 ( ´ ▽ ` )ノ 覺得干擾閱讀的話,可以把小魚關掉。
反白水彩
util反白效果變成有質感的水彩,原本醜醜的效果退散!◝( •ω• )◟
| 名稱 | 描述 |
|---|---|
| Document getSelection() | 取得使用者目前選取的文字內容或游標位置 |
| 淺水流體模擬 | 把水當成一層薄薄的流體來解:平流、黏滯、外力,再做一次投影把散度壓回零(不可壓縮)。水彩的暈染、回水、邊緣積色都是它自己跑出來的,不是畫上去的 |
| Kubelka–Munk 模型 | 由顏料的吸收 K 與散射 S 算出一層顏料的反射率與透射率,再疊到底材上。顏料學的標準模型——濃淡之間的色相位移是算出來的,不是對同一個色票調透明度 |
| mix-blend-mode | 讓一個元素與它底下的頁面做混色運算,而不是單純疊上去。螢光筆用 multiply(顏料把紙壓暗)、深色底用 screen(顏料把紙提亮),底下的字因此照樣讀得到 |
| Value Noise 與 fBm | 在格點上取亂數再平滑內插得到的連續噪聲;把數個頻率疊起來(fBm)就得到自然的粗細層次。紙紋、乾筆條紋都靠它 |
| Noise | 比 Math.random 更自然隨機效果,常用於地形、雲朵、材質等 |
反白以下文字看看。
<template> <div ref="stageRef" class="w-full flex flex-col gap-5 py-2" > <!-- 六色與示範反白 --> <div class="example-ctrl flex flex-col items-center gap-3"> <div class="flex flex-wrap justify-center gap-2"> <button v-for="item in MARKER_COLOR_KEY_LIST" :key="item" class="swatch" :class="item === color ? 'is-active' : ''" :style="{ backgroundColor: swatchCss(item) }" :aria-label="item" @click="pickColor(item)" /> </div>
<div class="flex flex-wrap items-center justify-center gap-3"> <base-btn :label="t('demo')" @click="selectSampleText()" /> <span class="text-xs opacity-60">{{ t('hint') }}</span> </div> </div>
<article class="demo-paper mx-auto max-w-2xl w-full flex flex-col gap-4 rounded-lg px-8 py-10 text-xl leading-[2]"> <span class="text-2xl font-bold"> {{ t('heading') }} </span> <span ref="sampleRef" class="text-xl" > {{ t('paragraphOne') }} </span> <span class="text-xl"> {{ t('paragraphTwo') }} </span> </article>
<util-selection-marker :color="color" :scope="stageRef" :z-index="30" /> </div></template>
<script setup lang="ts">import type { MarkerColorKey } from '../marker-paint'import { ref } from 'vue'import { useI18n } from 'vue-i18n'import BaseBtn from '../../base-btn.vue'import { MARKER_COLOR_KEY_LIST, resolveMarkerColor } from '../marker-paint'import UtilSelectionMarker from '../util-selection-marker.vue'
const { t } = useI18n()
const stageRef = ref<HTMLElement>()/** 示範反白的那一段:用 ref 指定,不靠標籤名稱找,改了標籤也不會失效 */const sampleRef = ref<HTMLElement>()const color = ref<MarkerColorKey>('yellow')
function pickColor(next: MarkerColorKey) { color.value = next}
/** 色票只是給人點的,直接拿淺色那一組畫出來就好 */function swatchCss(key: MarkerColorKey) { const [red, green, blue] = resolveMarkerColor(key, false) return `rgb(${Math.round(red * 255)} ${Math.round(green * 255)} ${Math.round(blue * 255)})`}
/** 直接替使用者反白一段示範文字:不必自己拖曳也看得到效果 */function selectSampleText() { const textNode = sampleRef.value?.firstChild if (!textNode || textNode.nodeType !== Node.TEXT_NODE) return
const length = textNode.textContent?.length ?? 0 const range = document.createRange() range.setStart(textNode, 0) range.setEnd(textNode, Math.min(length, 26))
const selection = window.getSelection() selection?.removeAllRanges() selection?.addRange(range)}</script>
<style scoped lang="sass">.demo-paper background-color: light-dark(#FDFCF8, #23252B) box-shadow: 0 1px 3px light-dark(rgba(0, 0, 0, 0.08), rgba(0, 0, 0, 0.4))
.swatch width: 1.75rem height: 1.75rem border: 2px solid light-dark(rgba(0, 0, 0, 0.12), rgba(255, 255, 255, 0.18)) border-radius: 9999px cursor: pointer
&.is-active border-color: light-dark(#333, #FFF) transform: scale(1.15)</style>顏料疊在紙上會吸收一部分光線,疊越多越暗,這就是減法混色。元件在 canvas 上只畫顏料,混色交給 CSS 的 mix-blend-mode: multiply,底下的字照樣清楚。
深色底上 multiply 會把顏料壓暗到看不見,所以深色模式改用 screen 把底色提亮,每種顏料也另外備一組深色模式專用的顏色。
深淺以頁面的 color-scheme 為準,頁面沒指定才看系統偏好。網站自己有深淺切換時,顏料才不會因為跟系統偏好不一致,混到看不見。
水彩的積色、顆粒與暈開都靠模擬算出來,參考 Curtis 等人 1997 年的〈Computer-Generated Watercolor〉:
- 淺水層:紙面上的水依水位高低流動,把顏料推到邊緣;水乾了,顏料就在邊緣積成一圈深色。
- 顏料層:顏料跟著水移動,同時沉進紙紋凹處,形成顆粒。
- 毛細層:水滲進紙纖維,讓邊緣稍微暈開。
模擬網格一格只有一個多像素(高解析度螢幕更細),積色線若貼著格子,邊緣就會一格一格地鋸齒。所以顏料快到濕區邊界時會慢下來,堆在一條平滑的曲線上,不會全擠進最後一格。
筆刷也模仿真的筆,落筆處最濕、越刷越乾;起筆、收筆與拖曳停頓時筆會放慢,那些地方會積水;每根筆毛各自帶水,快乾時筆觸會斷斷續續露出紙色,也就是書法說的飛白。
最後用 Kubelka–Munk 模型(計算半透明顏料疊在紙上呈現什麼顏色的光學模型)上色, 顏料越厚顏色越深,但深到一定程度就收住,免得黃色一路暗成棕色。積色處還會混進一點同色系較深的顏料,例如黃色的積色處帶點赭石。 紙面模擬冷壓水彩紙的凸點紋理,再用側光打出一點立體感。( ´ ▽ ` )ノ
拖曳時筆觸不閃爍
Section titled “拖曳時筆觸不閃爍”元件會記下每一行已經刷過的範圍。反白範圍變長就接著刷,變短就把多出來的那段擦掉,已經刷好的部分維持原樣,拖曳時畫面就不會一直閃。
interface Props { /** * 顏料顏色。 * @default 'yellow' */ color?: MarkerColorKey; /** * 配色相,同時決定與頁面的混色方式: * 淺色用 `multiply`(顏料把紙壓暗),深色用 `screen`(顏料把紙提亮)。 * `auto` 跟著頁面走,讀反白處的 CSS `color-scheme`,頁面沒指定才看系統的深淺色偏好。 * 網站自己有深淺切換時,才不會因為跟系統偏好不一致,把顏料混到看不見。 * @default 'auto' */ theme?: MarkerTheme; /** * 只理會這個範圍內的反白。給 CSS 選擇器或元素,不給就是整份文件。 * @default null */ scope?: string | HTMLElement | null; /** * 至少要反白幾個字才畫。 * * 藏掉原生反白之後這個值不該再往上調——低於門檻的反白會變成**完全沒有回饋**, * 使用者看不出自己選到了什麼。 * @default 1 */ minTextLength?: number; /** * 把瀏覽器原生的反白底色藏起來,讓顏料成為唯一的反白指示。 * * 兩套指示疊在同一塊面積上會互相打架——原生那塊不透明的底色會把顏料的濃淡整個壓平。 * 給了 `scope` 時只藏該範圍內的反白,範圍外照常顯示;沒給 `scope` 就是整份文件。 * * 只有在畫布真的能用時才會藏。瀏覽器不支援 WebGL2 的話一律保留原生反白, * 不然使用者會完全看不出自己選了什麼。 * @default true */ hideNativeSelection?: boolean; /** * 筆寬倍率,乘在預設的筆寬(略超出行高)上。1 是預設寬度,0.5 是一半、2 是兩倍。 * * 筆寬本來就以行高為單位,所以倍率在任何字級下的效果一致。 * 太寬會蓋到上下兩行,實際上限取決於行距。 * @default 1 */ widthScale?: number; /** 停用,等同完全不掛載。 @default false */ disabled?: boolean; /** 畫布的 z-index。 @default 20 */ zIndex?: number;}const emit = defineEmits<{ /** 開始畫。detail 帶筆數與反白的文字 */ (event: 'draw', detail: MarkerDrawDetail): void; /** 整組刷完 */ (event: 'settle'): void; /** 淡出完畢、畫布清空 */ (event: 'clear'): void;}>()