離譜的 CSS!從錶盤刻度到藝術剪紙

某日,群里有這樣一個問題,如何實現這樣的錶盤刻度:

這其實是個挺有意思的問題,方法也有很多。

單標籤,使用 conic-gradient 實現錶盤刻度

最簡單便捷的方式,就是利用角向漸變的方式 conic-gradient,程式碼也非常簡單,首先,我們實現一個重複角向漸變:

<div></div>
div {
    width: 300px;
    height: 300px;
    border-radius: 50%;
    background: repeating-conic-gradient(
        #000 0, #000 .8deg, transparent 1deg, transparent calc((360 / 60) * 1deg)
    );
}

其實比較難理解的是 calc((360 / 60) * 1deg),這是因為錶盤一共通常有 60 個刻度。效果大概是這樣:

接下來,只需要將中間鏤空即可。如果背景色是白色,直接疊加一個圓形即可,當然,更好的方式是通過 mask 屬性進行鏤空:

{
    background: repeating-conic-gradient(
        #000 0, #000 .8deg, transparent 1deg, transparent calc(360 / 60 * 1deg)
    );
    mask: radial-gradient(transparent 0, transparent 140px, #000 140px)
}

這樣,我們就得到了一個錶盤刻度:

這是使用一個標籤就能實現的方式,當然,缺點也很明顯:

  1. 鋸齒感嚴重,漸變的通病
  2. 由於是使用的角向漸變,刻度存在頭重腳輕的現象,越向內部,寬度越窄(刻度愈大,差異愈加明顯)

使用多個標籤實現

如果不介意使用太多的標籤,那麼通常而言,更容易想到的方法就是利用 60 個標籤,加上旋轉實現:

<div class="g-container">
        <div class="g-item"></div>
        // ... 一共 60 個
        <div class="g-item"></div>
    </div>
.g-item {
    position: absolute;
    width: 4px;
    height: 12px;
    background: #000;
    left: 0;
    top: 0;
    transform-origin: 0 150px;
}
@for $i from 1 through 60 { 
    .g-item:nth-child(#{$i}) {
        transform: rotate(#{($i - 1) * 6deg});
    }
}

像是這樣,我們通過 60 個 div 標籤,利用 SASS 的 for 語法減少重複的程式碼量,批量實現每個元素逐漸繞一點旋轉一定的角度,也是可以實現一個錶盤刻度的:

這個方案的好處是,每個刻度粗細一致,並且,不會產生鋸齒。

藉助 -webkit-box-reflect 減少標籤數

當然,上述方案的缺點就在於,使用了 60 個標籤去完成這樣一個簡單的圖形,有點太奢侈了。

我們希望儘可能的優化優化標籤的個數。此時,我們很容易的想到 — -webkit-box-reflect,倒影效果屬性。

-webkit-box-reflect 是一個非常有意思的屬性,它讓 CSS 有能力像鏡子一樣,反射我們元素原本繪製的內容。

-webkit-box-reflect 的語法非常簡單,最基本的用法像是這樣:

div {
    -webkit-box-reflect: below;
}

其中,below 可以是 below | above | left | right 代表下上左右,也就是有 4 個方向可以選。

假設我們有如下一張圖片:

<div></div>
div {
    background-image: url('//images.pokemontcg.io/xy2/12_hires.png');
}

加上 -webkit-box-reflect: right,也就是右側的倒影:

div {
    background-image: url('//images.pokemontcg.io/xy2/12_hires.png');
    -webkit-box-reflect: right;
}

效果如下,生成了一個元素右側的鏡像元素:

藉助 -webkit-box-reflect: right,我們至少可以從 60 個標籤減少到 15 個標籤的使用。簡單的嵌套兩層即可。

我們簡單改變一下 HTML 結構:

<div class="g-parent">
    <div class="g-container">
        <div class="g-item"></div>
        // ... 一共 16 個
        <div class="g-item"></div>
    </div>
</div>

這一次,我們只需要實現 1/4 圓的刻度即可:

@for $i from 1 through 16 { 
    .g-item:nth-child(#{$i}) {
        transform: rotate(#{($i - 1) * 6deg});
    }
}

我們可以得到這樣一個圖形:

基於這個圖形,我們只需要先向左側倒影一次,再向下倒影一次即可:

.g-container {
    -webkit-box-reflect: below;
}
.g-parent {
    -webkit-box-reflect: left;
}

效果如下:

大致的效果就出來了,當然,0點、3點、6點、9點上左下右 4 個刻度有點問題。不過 -webkit-box-reflect 也提供距離調整功能,再簡單修改下程式碼:

.g-container {
    -webkit-box-reflect: below 4px;
}
.g-parent {
    -webkit-box-reflect: left -4px;
}

這次,效果就是我們想要的最終效果:

我們就成功地藉助 -webkit-box-reflect 節約了 3/4 的標籤數量。完整的程式碼:CodePen Demo — Clock ticks

-webkit-box-reflect 與剪紙藝術

到這裡,我不由得想到,這種對摺、再對摺,鏡像再鏡像的方式,與我們小時候的摺紙藝術非常的類似

那麼,基於這樣一個模板:

<div class="g-parent">
    <div class="g-container">
        <div class="g-item"></div>
    </div>
</div>
.g-container {
    -webkit-box-reflect: below;
}
.g-parent {
    -webkit-box-reflect: left;
}

我只需要繪製 .g-item 裡面的內容,將他通過 2 次 -webkit-box-reflect 鏡像,就能得到一個剪紙圖形

而如何得到隨機有意思的不規則圖形呢?

clip-path 是個很不錯的選擇,我們通過 clip-path 隨機對一個矩形進行裁剪:

.g-item {
    width: 150px;
    height: 150px;
    background: #000;
    clip-path: polygon(25% 0%,71% 66%,59% 0%,79% 23%,95% 4%,100% 40%,77% 100%,38% 100%,47% 71%,36% 30%,23% 60%,0% 100%,5% 37%);
}

效果如下:

經過兩次鏡像後的效果如下:

是不是有那麼點意思了?可以隨機利用 clip-path 多嘗試幾次,可以得到不同的效果:

CodePen Demo — Pure CSS Page Cutting

-webkit-box-reflect 配合 clip-path 配合 mask

但是上面的圖形看著還是太簡單了,幾個原因,一是對摺的次數和角度不夠,缺少對摺次數和不同角度的對摺,二是圖形不夠負責。

我又想起了之前看到過的一篇類似的剪紙相關的文章 — Paper Snowflakes: Combining Clipping and Masking in CSS

再上述的基礎上,還使用了 mask,將圖形切割的更細。

我們再來一次,還是同樣的結構,當然,為了得到更負責的圖形,我們設置了 4 個 .g-item

<div class="g-parent">
    <div class="g-container">
        <div class="g-item"></div>
        <div class="g-item"></div>
        <div class="g-item"></div>
        <div class="g-item"></div>
    </div>
</div>

首先,還是設置一個 clip-path 切割後的圖形:

.g-item:nth-child(1) {
    width: 150px;
    height: 150px;
    background: #000;
    clip-path: polygon(17% 41%,6% 39%,16% 91%,18% 78%,56% 11%,28% 71%,99% 67%,25% 65%,69% 72%,46% 28%,90% 76%,67% 34%,48% 30%,79% 36%,59% 15%,23% 92%,16% 1%,32% 81%,72% 38%,50% 59%,71% 98%,66% 87%,83% 14%,36% 71%,49% 7%,9% 25%,52% 76%,10% 83%,17% 41%);
}

效果如下:

這個圖可能它會特別的奇怪,沒有問題,我們繼續。

如果,我們將一個矩形,從左下角開始算,分為 4 份,那麼每一個份就是 90° / 4 = 22.5°,我們希望切割得到其中一份:

image

我們可以藉助 mask 去完成這個切割:

.g-item:nth-child(1) {
    width: 150px;
    height: 150px;
    background: #000;
    clip-path: polygon(.....);
    mask: conic-gradient(from 0turn at 0 100%, #000, #000 22.5deg, transparent 22.5deg, transparent);
}

上述的圖形,就被切割成了這樣:

OK,基於此,我們可以得到同樣的第二份圖形,但是我們給它加一個 rotateY(180deg)

.g-item:nth-child(2) {
    width: 150px;
    height: 150px;
    background: #000;
    clip-path: polygon(.....);
    mask: conic-gradient(from 0turn at 0 100%, #000, #000 22.5deg, transparent 22.5deg, transparent);
    transform: rotateY(180deg);
}

效果如下:

我們再通過 rotateZ(),給第二圖形旋轉一定的角度,讓他和第一個貼合在一起:

.g-item:nth-child(2) {
    clip-path: polygon(.....);
    mask: conic-gradient(from 0turn at 0 100%, #000, #000 22.5deg, transparent 22.5deg, transparent);
    transform: rotateY(180deg) rotateZ(-45deg);
}

就得到了一個斜向角度的鏡像影像:

因為 .g-item 被切割成了 4 份,所以第 3、4 個圖形如法炮製即可,這樣,整個 .g-item 的效果如下:

再打開 -webkit-box-reflect,整個的圖形效果如下:

這樣,一個剪紙圖形就誕生啦!

當然,為了得到不一樣的效果,我們可以藉助 JavaScript 去隨機生成 CSS 中的各類參數,完整的程式碼,大概是這樣:

<div class="g-parent">
    <div class="g-container">
        <div class="g-item"></div>
        <div class="g-item"></div>
        <div class="g-item"></div>
        <div class="g-item"></div>
    </div>
</div>
.g-container,
.g-parent {
    position: relative;
    display: flex;
    width: 150px;
    height: 150px;
}
.g-item {
    position: absolute;
    top: 0;
    left: 0;
    bottom: 0;
    right: 0;
    background: #000;
    transform-origin: 0 100%;
    clip-path: var(--polygon, polygon(40% 0%,0% 91%,52% 100%,0% 37%,77% 23%,77% 76%,43% 22%,55% 88%,100% 100%,100% 10%));
}
.g-item {
    mask: conic-gradient(from 0turn at 0 100%, #000, #000 22.5deg, transparent 22.5deg, transparent);
}
@for $i from 1 through 5 { 
    .g-item:nth-child(#{$i}) {
        transform: rotateZ(calc(22.5deg * #{$i - 1}));
    }
}
.g-item:nth-child(2) {
    transform: rotateY(180deg) rotateZ(-60deg);
}
.g-item:nth-child(4) {
    transform: rotateY(180deg) rotateZ(-105deg);
}
.g-container {
    -webkit-box-reflect: below;
}
.g-parent {
    -webkit-box-reflect: left;
}
const ele = document.querySelectorAll('.g-item');

document.addEventListener('click', function(e) {
    let num = Math.floor(Math.random() * 30 + 10);
    
    const maskR =  Math.floor(Math.random() * 22.5 + 22.5 ) + 'deg';
    const r1 = Math.floor(Math.random() * 100) + '%';
    const r2 = Math.floor(Math.random() * 100) + '%'; 
    
    let polygon = 'polygon(' + r1 + ' ' + r2 + ',';
    
    for (let i=0; i<num; i++) {
         const newR1 = Math.floor(Math.random() * 100) + '%';
         const newR2 = Math.floor(Math.random() * 100) + '%';

        polygon += newR1 + ' ' + newR2 + ','
    }
    
    polygon += r1 + ' ' + r2 + ')';
    
    [...ele].forEach(item => {
        item.setAttribute('style', `--polygon:${polygon};-webkit-mask:conic-gradient(from 0turn at 0 100%, #000, #000 ${maskR}, transparent ${maskR}, transparent)`);
    });
});

這樣,每次點擊滑鼠,我們都能得到不同的隨機剪紙圖案:

看看這個簡單錄製的 GIF:

完整的程式碼,你可以猛擊這裡 CodePen Demo — Pure CSS Art Page Cutting

最後

本文到此結束,希望對你有幫助 😃

更多精彩 CSS 技術文章匯總在我的 Github — iCSS ,持續更新,歡迎點個 star 訂閱收藏。

如果還有什麼疑問或者建議,可以多多交流,原創文章,文筆有限,才疏學淺,文中若有不正之處,萬望告知。