如今,使用 來建立複雜的形狀是一項簡單的任務
clip-path
,但為形狀添加邊框總是很痛苦。沒有強大的 CSS 解決方案,我們總是需要為每個特定情況生成特定的“hacky”代碼。在本文中,我将向您展示如何使用 CSS Paint API 解決此問題。
探索 CSS Paint API 系列:
- 第 1 部分: 圖像碎片效應
- 第 2 部分: Blob 動畫
- 第 3 部分: 多邊形邊框(您來了!)
- 第 4 部分: 舍入形狀
在我們深入研究第三個實驗之前,以下是我們正在建構的内容的簡要概述。而且,請注意,我們在這裡所做的一切僅在基于 Chromium 的浏覽器中受支援,是以您需要在 Chrome、Edge 或 Opera 中檢視示範。
你會發現那裡沒有複雜的 CSS 代碼,而是一個通用代碼,我們隻調整幾個變量來控制形狀。
主要思想
為了實作多邊形邊框,我将依靠 CSS
clip-path
屬性和使用 Paint API 建立的自定義蒙版的組合。
- 我們從一個基本的矩形開始。
- 我們申請
獲得我們的多邊形形狀。clip-path
- 我們應用自定義蒙版來獲得我們的多邊形邊框
CSS 設定
這是
clip-path
我們将要執行的步驟的 CSS :
.box {
--path: 50% 0,100% 100%,0 100%;
width: 200px;
height: 200px;
background: red;
display: inline-block;
clip-path: polygon(var(--path));
}
到目前為止沒有什麼複雜的,但請注意 CSS 變量的使用
--path
。整個技巧依賴于那個單一變量。由于我将使用 a
clip-path
和 a
mask
,是以兩者都需要使用相同的參數,是以是
--path
變量。而且,是的,Paint API 将使用相同的變量來建立自定義蒙版。
整個過程的CSS代碼變為:
.box {
--path: 50% 0,100% 100%,0 100%;
--border: 5px;
width: 200px;
height: 200px;
background: red;
display: inline-block;
clip-path: polygon(var(--path));
-webkit-mask: paint(polygon-border)
}
除了 之外
clip-path
,我們還應用了自定義蒙版,此外還添加了一個額外的變量
--border
來控制邊框的粗細。如您所見,到目前為止,一切仍然是非常基本和通用的 CSS。畢竟,這是使 CSS Paint API 非常适合使用的原因之一。
JavaScript 設定
我強烈建議閱讀我上一篇文章的第一部分,以了解 Paint API 的結構。
現在,讓我們看看
paint()
當我們跳入 JavaScript 時函數内部發生了什麼:
const points = properties.get('--path').toString().split(',');
const b = parseFloat(properties.get('--border').value);
const w = size.width;
const h = size.height;
const cc = function(x,y) {
// ...
}
var p = points[0].trim().split(" ");
p = cc(p[0],p[1]);
ctx.beginPath();
ctx.moveTo(p[0],p[1]);
for (var i = 1; i < points.length; i++) {
p = points[i].trim().split(" ");
p = cc(p[0],p[1]);
ctx.lineTo(p[0],p[1]);
}
ctx.closePath();
ctx.lineWidth = 2*b;
ctx.strokeStyle = '#000';
ctx.stroke();
擷取和設定 CSS 自定義屬性的能力是它們如此出色的原因之一。我們可以讓 JavaScript 首先讀取
--path
變量的值,然後将其轉換為點數組(見上面的第一行)。是以,這意味着
50% 0,100% 100%,0 100%
成為面具的點,即
points = ["50% 0","100% 100%","0 100%"]
。
然後我們循環周遊這些點以使用moveTo和繪制多邊形lineTo。這個多邊形與在 CSS 中使用
clip-path
屬性繪制的多邊形完全相同。
最後,在繪制完形狀後,我給它添加了一個描邊。我使用定義了筆觸的粗細,并使用
lineWidth
設定了純色
strokeStyle
。換句話說,隻有形狀的筆觸是可見的,因為我沒有用任何顔色填充形狀(即它是透明的)。
現在我們要做的就是更新路徑和厚度以建立任何多邊形邊界。值得注意的是,我們在這裡不限于純色,因為我們使用的是 CSS
background
屬性。我們可以考慮漸變或圖像。
現場示範
如果我們需要添加内容,我們必須考慮一個僞元素。否則,内容會在此過程中被剪輯。支援内容并不是非常困難。我們将
mask
屬性移動到僞元素。我們可以保留
clip-path
主元素上的聲明。
到目前為止的問題?
我知道在檢視最後一個腳本後,您可能有一些迫切的問題要問。請允許我先發制人地回答一些我敢打賭你會想到的事情。
那是什麼 cc()
功能?
cc()
我正在使用該函數将每個點的值轉換為像素值。對于每個點,我都得到了
x
和
y
坐标 - 使用
points[i].trim().split(" ")
- 然後我轉換這些坐标,使它們在 canvas 元素中可用,進而允許我們使用這些點進行繪制。
const cc = function(x,y) {
var fx=0,fy=0;
if (x.indexOf('%') > -1) {
fx = (parseFloat(x)/100)*w;
} else if(x.indexOf('px') > -1) {
fx = parseFloat(x);
}
if (y.indexOf('%') > -1) {
fy = (parseFloat(y)/100)*h;
} else if(y.indexOf('px') > -1) {
fy = parseFloat(y);
}
return [fx,fy];
}
邏輯很簡單:如果它是一個百分比值,我使用寬度(或高度)來找到最終值。如果它是一個像素值,我隻是簡單地得到沒有機關的值。例如,如果我們有
[50% 20%]
寬度等于
200px
且高度等于 的位置
100px
,那麼我們得到
[100 20]
。如果是
[20px 50px]
,那麼我們得到
[20 50]
。等等。
clip-path
如果遮罩已經将元素剪裁到形狀的筆劃,為什麼還要使用 CSS ?
clip-path
隻使用面具是我想到的第一個想法,但我偶然發現了這種方法的兩個主要問題。第一個與
stroke()
工作方式有關。來自MDN:
筆觸與路徑的中心對齊;換句話說,筆畫的一半畫在内側,一半畫在外側。
那種“一半内,一半外”讓我很頭疼,而且在把所有東西放在一起時,我總是以一種奇怪的溢出結束。這就是 CSS 的
clip-path
幫助所在;它夾住了外部,隻保留了内側——不再溢出!
您會注意到
ctx.lineWidth = 2*b
. 我将邊框厚度加倍,因為我将剪下它的一半,以整個形狀所需的正确厚度結束。
第二個問題與形狀的可懸停區域有關。衆所周知,遮罩不會影響該區域,我們仍然可以懸停/與整個矩形進行互動。再次,伸手
clip-path
解決問題,另外我們将互動限制在形狀本身。
下面的示範說明了這兩個問題。第一個元素有掩碼和剪輯路徑,而第二個隻有掩碼。我們可以清楚地看到溢出問題。嘗試将滑鼠懸停在第二個上,以檢視即使光标位于三角形之外我們也可以更改顔色。
為什麼要使用 @property
邊界值?
@property
這是一個有趣且相當棘手的部分。預設情況下,自定義屬性(如
--border
)被視為“CSSUnparsedValue”,這意味着它們被視為字元串。從CSS 規範:
“ CSSUnparsedValue ”對象表示引用自定義屬性的屬性值。它們由字元串片段清單和變量引用組成。
使用
@property
,我們可以注冊自定義屬性并為其指定類型,以便浏覽器可以識别它并将其作為有效類型而不是字元串處理。在我們的例子中,我們将邊框注冊為一種
<length>
類型,以便稍後它成為CSSUnitValue。這是什麼也做是允許我們使用任何長度機關(
px
,
em
,
ch
,
vh
,等)的邊界值。
這聽起來可能有點複雜,但讓我嘗試用 DevTools 螢幕截圖來說明差異。
我
console.log()
在我定義的變量上使用
5em
。第一個已注冊,但第二個未注冊。
在第一種情況下,浏覽器識别類型并将其轉換為像素值,這很有用,因為我們隻需要函數内部的像素值
paint()
。在第二種情況下,我們将變量作為字元串擷取,這不是很有用,因為我們無法在函數内部将
em
機關轉換為
px
機關
paint()
。
嘗試所有機關。它将始終以
paint()
函數内部的計算像素值作為結果。
怎麼樣的 --path
變化?
--path
我想對
--path
變量使用相同的方法,但不幸的是,我認為我将 CSS 推到了它在這裡可以做的極限。使用
@property
,我們可以注冊複雜類型,甚至是多值變量。但這對于我們需要的路徑仍然不夠。
我們可以使用
+
和
#
符号來定義空格分隔或逗号分隔的值清單,但我們的路徑是空格分隔的百分比(或長度)值的逗号分隔清單。我會使用類似的東西
[<length-percentage>+]#
,但它不存在。
對于路徑,我不得不将其作為字元串值進行操作。這暫時将我們限制在百分比和像素值上。為此,我定義了
cc()
将字元串值轉換為像素值的函數。
我們可以在CSS 規範中閱讀:
文法字元串的内部文法是CSS 值定義文法的一個子集。預期規範的未來級别将擴充允許的文法的複雜性,允許自定義屬性更接近 CSS 屬性允許的全部範圍。
即使文法擴充為能夠注冊路徑,我們仍然會遇到問題,以防我們需要
calc()
在我們的路徑中包含:
--path: 0 0,calc(100% - 40px) 0,100% 40px,100% 100%,0 100%;
在上面,
calc(100% - 40px)
是浏覽器認為 a 的值
<length-percentage>
,但浏覽器在知道百分比的引用之前無法計算該值。換句話說,我們無法在
paint()
函數内部獲得等效的像素值,因為隻有在
var()
.
為了克服這個問題,我們可以擴充
cc()
函數來進行轉換。我們進行了百分比值和像素值的轉換,是以讓我們将它們合并為一個轉換。我們将考慮 2 種情況:
calc(P% - Xpx)
和
calc(P% + Xpx)
。我們的腳本變成:
const cc = function(x,y) {
var fx=0,fy=0;
if (x.indexOf('calc') > -1) {
var tmp = x.replace('calc(','').replace(')','');
if (tmp.indexOf('+') > -1) {
tmp = tmp.split('+');
fx = (parseFloat(tmp[0])/100)*w + parseFloat(tmp[1]);
} else {
tmp = tmp.split('-');
fx = (parseFloat(tmp[0])/100)*w - parseFloat(tmp[1]);
}
} else if (x.indexOf('%') > -1) {
fx = (parseFloat(x)/100)*w;
} else if(x.indexOf('px') > -1) {
fx = parseFloat(x);
}
if (y.indexOf('calc') > -1) {
var tmp = y.replace('calc(','').replace(')','');
if (tmp.indexOf('+') > -1) {
tmp = tmp.split('+');
fy = (parseFloat(tmp[0])/100)*h + parseFloat(tmp[1]);
} else {
tmp = tmp.split('-');
fy = (parseFloat(tmp[0])/100)*h - parseFloat(tmp[1]);
}
} else if (y.indexOf('%') > -1) {
fy = (parseFloat(y)/100)*h;
} else if(y.indexOf('px') > -1) {
fy = parseFloat(y);
}
return [fx,fy];
}
我們
indexOf()
用來測試 的存在
calc
,然後,通過一些字元串操作,我們提取兩個值并找到最終的像素值。
是以,我們還需要更新這一行:
p = points[i].trim().split(" ");
…到:
p = points[i].trim().split(/(?!\(.*)\s(?![^(]*?\))/g);
由于我們需要考慮
calc()
,使用空格字元不适用于拆分。那是因為
calc()
也包含空格。是以我們需要一個正規表達式。不要問我——這是在 Stack Overflow 上嘗試了很多之後才奏效的方法。
以下是基本示範,用于說明我們迄今為止為支援所做的更新
calc()
請注意,我們已将
calc()
表達式存儲在
--v
我們注冊為
<length-percentage>
. 這也是技巧的一部分,因為如果我們這樣做,浏覽器就會使用正确的格式。無論
calc()
表達式的複雜程度如何,浏覽器總是将其轉換為格式
calc(P% +/- Xpx)
. 出于這個原因,我們隻需要在
paint()
函數内部處理該格式。
在下面不同的示例中,我們
calc()
為每個示例使用了不同的表達式:
如果您檢查每個框的代碼并檢視 的計算值
--v
,您總會發現相同的格式,這非常有用,因為我們可以進行任何我們想要的計算。
應該注意的是,使用變量
--v
不是強制性的。我們可以
calc()
直接在路徑中包含 。我們隻需要確定我們插入了正确的格式,因為浏覽器不會為我們處理它(請記住,我們無法注冊路徑變量,是以它是浏覽器的字元串)。當我們需要
calc()
在路徑中有很多并且為每個建立一個變量會使代碼太長時,這會很有用。我們将在最後看到一些示例。
我們可以有虛線邊框嗎?
我們可以!它隻需要一個指令。該
<canvas>
元素已經有一個内置函數來繪制虛線筆劃setLineDash():
所述 setLineDash()
的帆布2D API的方法CanvasRenderingContext2D接口設定撫摸線時所使用的線虛線圖案。它使用一組值來指定描述模式的線和間隙的交替長度。
我們所要做的就是引入另一個變量來定義我們的破折号模式。
現場示範
在 CSS 中,我們簡單地添加了一個 CSS 變量 ,
--dash
掩碼中的内容如下:
// ...
const d = properties.get('--dash').toString().split(',');
// ...
ctx.setLineDash(d);
我們還可以使用 控制偏移量lineDashOffset。稍後我們将看到控制偏移如何幫助我們實作一些很酷的動畫。
為什麼不使用 @property
代替注冊破折号變量?
@property
從技術上講,我們可以将破折号變量注冊為 a,
<length>#
因為它是一個逗号分隔的長度值清單。它确實有效,但我無法檢索函數内的
paint()
值。我不知道這是一個錯誤,缺乏支援,還是我隻是遺漏了一塊拼圖。
這是一個示範來說明問題:
我正在
--dash
使用這個注冊變量:
@property --dash{
syntax: '<length>#';
inherits: true;
initial-value: 0;
}
……然後将變量聲明為:
--dash: 10em,3em;
如果我們檢查元素,我們可以看到浏覽器正在正确處理變量,因為計算值是像素值
但是我們隻得到
paint()
函數内部的第一個值
在我找到解決方案之前,我一直堅持使用
--dash
變量作為字元串,例如
--path
. 在這種情況下沒什麼大不了的,因為我認為我們不需要超過像素值。
用例!
在探索了這項技術的幕後,現在讓我們關注 CSS 部分并檢視多邊形邊框的一些用例。
一組按鈕
我們可以輕松生成具有酷炫懸停效果的自定義形狀按鈕。
請注意如何
calc()
在最後一個按鈕的路徑内使用我們之前描述的方式。它工作正常,因為我遵循正确的格式。
面包屑
建立面包屑系統時不再頭疼!在下面,您不會發現“hacky”或複雜的 CSS 代碼,而是一些非常通用且易于了解的代碼,我們隻需調整一些變量即可。
卡片顯示動畫
如果我們對厚度應用一些動畫,我們可以獲得一些奇特的懸停效果
我們可以使用相同的想法來建立一個展示卡片的動畫:
标注和語音氣泡
“我們怎麼能在那個小箭頭上加邊框???” 我認為每個人在處理标注或對話氣泡類設計時都偶然發現了這個問題。Paint API 使這變得微不足道。
在該示範中,您将找到一些可以擴充的示例。你隻需要找到你的對話氣泡的路徑,然後調整一些變量來控制邊框的粗細和箭頭的大小/位置。
動畫破折号
在我們結束之前的最後一個。這次我們将專注于虛線邊框以建立更多動畫。我們已經在按鈕集合中做了一個,将虛線邊框轉換為實心邊框。讓我們解決另外兩個問題。
将滑鼠懸停在下面,看看我們得到的不錯的效果:
使用過 SVG 一段時間的人可能熟悉我們通過動畫實作的排序效果
stroke-dasharray
。克裡斯甚至在不久前解決了這個概念。感謝 Paint API,我們可以直接在 CSS 中做到這一點。這個想法幾乎與我們在 SVG 中使用的想法相同。我們定義破折号變量:
--dash: var(--a),1000;
變量
--a
從 開始
0
,是以我們的模式是一條實線(長度等于 0),有一個間隙(長度為 1000);是以沒有邊界。我們将動畫設定
--a
為一個很大的值來繪制我們的邊界。
我們還讨論了 using
lineDashOffset
,我們可以将其用于另一種動畫。将滑鼠懸停在下方并檢視結果:
最後,一個 CSS 解決方案可以為适用于任何形狀的破折号的位置設定動畫!
const o = properties.get('--offset');
ctx.lineDashOffset=o;