Well in Time
CSS 生成藝術初探與 css-doodle 簡介
05-23-202013 Min Read

好歌分享:San Cisco - 'On The Line'

前言

生成藝術一直是我很想學習的主題,雖然知道大部分的人會使用 processing, paper.js, 或是 zimjs 來製作 Generative art, 但在對於自己的藝術天份很有自知之明的情況下,一直都沒有去嘗試製作,再加上這些工具的學習也是有ㄧ定門檻。

然而,最近在 Youtube 上看到 css-doodle作者 - 袁川CSSConf CN 2019 的演講,才了解到要"開始"似乎沒有這麼難,只要掌握一些基本觀念與技巧即可,而且他開發的 web component css-doodle 無論是語法或是使用都算易懂好上手。

今天藉由這篇文章分享該演講中我很喜歡的一些重點,並試著介紹與運用 CSS/JS 和 css-doodle 製作一些簡單的生成藝術作品。

先給大家看第一個小 demo,這是參照袁川在演講中提及的一個範例實作,利用純 CSS 與 JS 所製作的簡單生成藝術:

簡單的事重複做就不簡單

在生成藝術中,Loop 佔了很主要的角色,需要透過迴圈的方式去自動產生圖案。而圖案其實不需要複雜,在第一個範例中的元素就只有直線

簡單的直線,透過 transform: rotate(),就能夠有不一樣的變化,而透過組合,將多個擁有不同狀態的直線串接在一起,再加上時間因子作為變數去改變狀態,就可以是一個簡單的生成藝術。

程式碼也很簡短:

先設定一個 5x5 的表格:

div.grid
  div
  div
  // 略 ... 共 25 個 div

現在利用 CSS grid 可以很輕易控制表格的呈現,接著我們可以將之後預期要來拿隨機變化的屬性以 css variable 的方式設定:

:root {
  --theme: #FF9800;
  --deg: 45deg;
  --gride-size: 50px;
}
.grid {
  width: calc(var(--gride-size) * 5);
  margin: 0 auto;
  display: grid;
  border: 5px solid var(--theme);
  box-shadow: 0 0 18px 1px var(--theme) inset;
  grid-template-columns: repeat(5, 1fr);
  grid-auto-rows: var(--gride-size);
  overflow: hidden;
}
.grid div {
  border: 1px solid var(--theme);
  background-color: var(--theme);
  width: 1px;
  transition: all 1s ease-in;
  transform: skew(var(--deg));
  transform-origin: top;
}

JS 的部分就只需要在固定的 time interval 中間賦以隨機產生的數值到 CSS variable 中:

const getRandomBoolean = () => Math.floor(Math.random() * Math.floor(2));
const randomColor = () => {
  const color = Math.floor(Math.random()*16777215).toString(16);
  return color.length !== 6 ? `f${color}` : color;
}
let sign = '+';
let theme = randomColor();
const root = document.documentElement;
const divs = document.querySelectorAll('.grid > div');
setInterval(() => {
  theme = randomColor();
  divs.forEach(div => {
    sign = (getRandomBoolean() === 1) ? '+' : '-';
    div.style.setProperty('transform', `skew(${sign}45deg)`);
    root.style.setProperty('--theme', `#${theme}`);
  });
}, 2000);

以這樣的基本想法出發,就可以修改成不同的變化,像是加入 clip-path 來進一步操作畫面中的圖案元素:

帶入一點數學,可以產生更多不同的 polygon:

上面這範例中我是隨便用三角函數設定一個公式來跑,但在演講中,袁川有提及 lissajous curves 這個古老的數學公式,他發現非常適合用在 clip-path 上頭,搭配 poylgonfill-rule 屬性,可以做出以下的效果,像是許多特殊的海洋生物一般:

lissajous-curve-clip-path (出處:https://youtu.be/mEpocRIc3q8?t=737)

發揮更多想像力

妥善使用 迴圈pattern隨機性這三個要素,就可以有許多的創意組合,除了上面的線條和 clip-path 外,CSS 繪圖常用到的 border-style, border-image, gradient, box-shadow 等等都能拿來嘗試。

附上幾個袁川在 codepen 上的作品:

yuanchuan-works

這只是其作品的冰山一角,有興趣的讀者可以到他的 codepen 上欣賞各種絢麗的畫作。

像是這個用 z-index 堆疊出的城市天際線圖,不得不佩服他的創造力:

另外,在袁川的演講中,讓我特別印象深刻的是他利用 text-shadow 的效果,將一個括號,運用在生成藝術中,你沒看錯,就是 ( 這個括號。

我找不到袁川影片中的範例,但自己依照他的介紹用純 css/js 實作了一個版本:

除了 text-shadow,上面範例中也用上了 filter, rotation, font-size 等等的隨機屬性變化,來生成這幅圖案。

CSS-Doodle

上面的範例都是用 pure css/js 完成的,但袁川其實製作了一個 web component,把許多製作 generative art 需要的一些功能幫你包好成多個函式,像是想要產生 grid,不用再到 html 內複製一大堆 div,也不用自己用 JS createElement,只要透過 css-doodle,一句話就能做到:

:doodle {
  @grid: 1x10 / 85%;
}

css-doodle 是基於 Shadow DOM v1 和 Custom Elements v1 實作的 web component,基本上目前主流瀏覽器都能支援。

像上面的例子,透過設定 css-doodle web component 的 shadow-dom 屬性,可以讓他幫你產生 grid layout 的 divs。而在 component 內除了能撰寫一般的 CSS 外,也能利用他提供的 utility function 快速達到一些 random、pick one 等等的效果。

我很喜歡作者在官網上很霸氣地一句話:The limit is the limit of CSS itself.

從他的作品集來看,所言不假。

css-doodle 的官網就是一個詳細的使用手冊,每個 function 與屬性的旁邊都有對應的實際範例幫助你理解。提供的函式說多不多,說少也不少,一個一個看完也不一定能馬上記住,還是得要在實作時邊對照查詢。

所以說做中學還是最快的,今天文章最後就來解析一個袁川利用 css-doodle 製作的作品,一方面能臨摹大神的創意,另一方面也能比較深刻的了解這些函式的用法。

css-doodle 作品解析

挑一個我很喜歡的作品,非常的漂亮!

這個作品分成兩個 component,一個是背後不斷滑落的線條,另一個是類似不斷旋轉的 DNA 螺旋。

我們單就 DNA 螺旋來看,程式碼非常簡短:

:doodle {
  @grid: 45x1 / 40vmin;
  position: relative;
  z-index: 1; 
}
:container {
  transform: translate(50%, 33vmin)
}
:after, :before {
  content: '';
  @place-cell: center;
  @size: 100%;
  background: radial-gradient(
    @p(#FFFDE1, #FB3569) @r(70%),
    transparent 0
  ) 
  @pn(30% 50%, 70% 50%, 50% 60%) / 
  @r(.1vmin, 5vmin) @lr() 
  no-repeat;
}

@place-cell: centerr;
@size: 100%;

will-change: transform;
animation: r 4s linear infinite;
animation-delay: calc(-4s / @size() * @i());

--translate: translateY(calc(-66vmin / @size() * @i()));
@keyframes r {
  from { transform: var(--translate) rotate(0) }
  to { transform: var(--translate) rotateZ(-1turn) }
}

要使用 css-doodle 的話,上面這段 css 是要放在 <css-doodle> component 內的:

<css-doodle>
<!-- Your css -->
</css-doodle>

:doodle

:doodle 是針對 <css-doodle> 這個元素本身的 selector,範例中設定了 positionz-index,比較特別的是 @grid 的使用。

@grid

@grid 是用來定義 css-doodle 的 grid layout,你也可以直接設定在 <css-doodle grid="5"> 上,但是 @grid 的屬性設定會有比較高的優先權。

@grid: 45x1 / 40vmin; 代表的是 doodle size 為 41vmin,且其中有 45 x 1 的 grid。(vmin 代表的是當前 vhvw 中最小的值)

當你設定了 grid 後,css-doodleshadow-dom 會長出類似如下的結構:

css-doodle-shadowdom

會產生一個 <div class="container">,並且在裡面產生對應你所設定的 grid 數量的 <div cell>

其中有個重點是,在 <css-doodle> 元件內設定的 CSS 會套用到每一個 <div cell>,以 [cell]:nth-of-type(1) 這樣的 css selector 把生成的 css style 個別應用到 DOM 上。

:container

:container 代表的是 :doodle 內 grid layout 的 container,也就是上面說到的 <div class="container">。範例中設置 transform: translate(50%, 33vmin) 也就只是把其偏移到畫面中間的位置。


接下來的 :after, :before 是整個圖形的重點:

:after, :before {
  content: '';
  @place-cell: center;
  @size: 100%;
  background: radial-gradient(
    @p(#FFFDE1, #FB3569) @r(70%),
    transparent 0
  ) 
  @pn(30% 50%, 70% 50%, 50% 60%) / 
  @r(.1vmin, 5vmin) @lr() 
  no-repeat;
}

@place-cell

@place-cell 是用來指定 cell 相對於整個 grid layout 中的位置,在此 component 的 :after, :before 都設定 @place-cell: center,就代表 grid 中 cell 的 :after:before 都置於相對於整個 grid 的中心位置。

@size

很單純就是同時設定 wdithheight 的值。

Background

作者利用 grid 內每個 <div cell>:after:before,使用 backgroundradial-gradient 屬性來製造我們看到的圓點。

主要使用到 background-imagebackground-position-x|ybackground-sizebackground-repeat 四個屬性。

如果單純取其中一個 cell 來觀察的話,會長這樣:

css-doodle-cell-div-single-dot

若再加上 css-doodle 提供的一些 random (@r), pick(@p) 等 utility function,就可以造成每一個 cell div 都各自擁有兩個不同顏色、大小、位置的圓點。

接下來針對 background 屬性所使用到的 utility function 作介紹。

@p, @pick(v1, v2,...)

@p@pick 的 alias。它會從給定的 list 中隨機挑選數值出來:

@p(#FFFDE1, #FB3569) 就會從這兩個顏色中隨機挑選一個。

@r, @rand(start [,end])

@r@rand 的 alias。接受至少一個參數,作為區間的頭與尾,它會從給定的區間中隨機挑選兩個數值。

因此範例中,background-imageradial-gradient 屬性:

radial-gradient( @p(#FFFDE1, #FB3569) @r(70%), transparent 0 ) 

第一層的設定等同於隨機選取兩個顏色(紅、白),並從 0 ~ 70% 之間選取一個百分比數值作為大小。

@pn, @pick-n(v1, v2,...)

@pn@pick-n 的 alias。會從給定的 list 中,一個一個取出,對應到 grid 中的 cell

以範例來說,就會依序設置 cellbackground-position 值為 30% 50%,70% 50% 和 50% 60%。(依照我觀察,似乎不會保證依照列表的順序,但還是會一個一個對應設置到 cell 上)

@lr, @last-rand

至於 background-size,範例使用 @r(.1vmin, 5vmin) @lr(),其中 @r(.1vmin, 5vmin) 就是從 .1vim 到 5vmin 中取一個值,而 @lr 則代表取得最後一個 random 函式所取得的數值,也就是 @r(.1vmin, 5vmin) 的結果。最終的 background-size 就會是兩個相同的隨機值。

到目前為止,設定出來的圖形會長這樣:

css-doodle-no-animation

所有的點會集中在一起,因為我們的 grid 是 45x1,也就是只有一個 column。

範例中,作者用 translateY 的方式來將每個點隨機向 Y 軸移動,並加上 @keyframerotate 的效果,就完成了螺旋的生成圖案:

animation: r 4s linear infinite;
animation-delay: calc(-4s / @size() * @i());

--translate: translateY(calc(-66vmin / @size() * @i()));
@keyframes r {
  from { transform: var(--translate) rotate(0) }
  to { transform: var(--translate) rotateZ(-1turn) }
}

@i, @index

唯一用到的 utility function 是 @i,代表當前套用到該 css 的 cell 的 index。

螺旋的部分到此為止,而另一個滑落效果的 doodle 運用的技巧也差不多,但是效果卻完全不同,有興趣的讀者可以研究 codepen 上的原始碼。

結論

生成藝術除了要有創意外,擁有好的實作工具也很重要,css-doodle 算是給了想嘗試生成藝術的人一個好的開頭,像是文章一開始所製作的簡單生成藝術,也都能夠透過 css-doodle 來實作,程式碼會簡短很多。

而透過 processing 等工具能做出更多效果,甚至能搭配音樂來做出不同變化,今天只是透過 css-doodle 的啟發,練習了一些簡單的生成藝術,體驗了一下,非常有趣,推薦大家一起來試試!

袁川的影片後半段還有很多使用絢麗技巧的生成藝術,非常推薦大家花個四十分鐘把影片看完,相信會有不少收穫,就算不想自己嘗試,欣賞他所創作的作品也是一種享受。

資料來源

  1. CSS生成藝術@袁川_CSSConf CN 2019
  2. css-doodle
  3. processing
  4. paper.js
  5. zimjs
© by Arvin Huang. All rights reserved.
theme inspired by @mhadaily
Last build: 09-08-2024