曾經寫過一篇名為「用HTML和CSS來畫出真實的眼球。」的文章。
在那時,我灌注了當時所知的所有CSS技巧,
我認為我做出了令人滿意的眼球效果。
然而,自從這篇文章以來,已經過去了六年。
在這段期間,網頁相關技術的創新日新月異,
CSS相比以前更能做到各種各樣的事情。
因此,我想要重新挑戰曾經放棄的事情和未完成的夢想。
是的,我想要讓眼球轉動起來。
我就是這麼想的。
這次我們仍然會使用HTML和CSS進行標記。
不需要svg、canvas或js。
上次我使用了pug和stylus來編寫,但這次我將使用pug和scss進行編寫。
此外,基本部分與上次沒有變化,
所以這次我會著重於要點進行說明。
只想知道結果的人可以點這裡 > eye pure css, without svg&canvas ver.2 (reactive)
.container
label.open
input.openCb(type='checkbox', checked=true)
| full open
ul.vision
each _, i in Array(361)
li.vision__item(class=[
`vision__item--r${Math.floor(i / 19)}`,
`vision__item--c${i % 19}`,
])
.eye
.eye__iris
each i in [0, 1]
ul.eye__ciliary(class=(i ===1)&&'eye__ciliary--sub')
each n in Array(72)
li.eye__ciliaryItem
.eye__reflect
.eye__reflect.eye__reflect--sub
.eye__reflect.eye__reflect--tiny
p.text touch this
我會在介紹各部分的同時進行整體說明。
首先,作為部件大致上分為
.vision
部分和.eye
部分。
.vision
部分負責作為感測器,
用來響應觸摸或滑鼠懸停來變化CSS。
361個面板排成19x19的正方形。
.eye
自然就是眼球的部分。
.eye__iris
是虹彩部分,裡面包含.eye__ciliary
和.eye__ciliary--sub
。
這些元素模擬了睫狀體。
上一次是使用偽元素和子元素來減少元素數量來實現,
這一次則是直接準備好所需的元素。
.eye__reflect
是用來表現眼球上反射的光。
這次準備了三個(上次是兩個)。
此外,還設定了.open
和.text
等元素,但這些會在後面再說明。
前提是,眼球無論如何移動,輪廓始終保持圓形。
因此,感測器只需對虹彩部分進行反應即可。
不過,為了演出「正在移動的感覺」,
眼球整體會朝向滑鼠懸停的地方移動。
為此,我準備了四個CSS變數(→自定義屬性)。
--vxMove
/--vyMove
: 眼球整體在x/y方向上移動的長度--vxAngle
/--vyAngle
: 虹彩在x/y軸上旋轉的角度如上所述,感測器是一個邊長為20px的正方形,
以19x19的瓷磚狀排列。
為什麼要選擇19這個奇數,而不是偶數呢?
因為如果是偶數,就無法創造出「正對著面前」的情況。
當滑鼠懸停在這些感測器的每一個上時,會計算變數。
感測器元素有類別vision__item--cXX
來表示「橫向第幾個」,以及
vision__item--rXX
來表示「縱向第幾個」,
因此可以利用scss的@for
迴圈來寫。
$grid-count: 19;
$grid-center: floor($grid-count / 2);
.container {
@for $i from 0 through ($grid-count - 1) {
&:has(.vision__item--c#{$i}:hover) {
--vxMove: #{($i - $grid-center) * 1px};
--vxAngle: atan(#{($i - $grid-center) * 0.06});
}
&:has(.vision__item--r#{$i}:hover) {
--vyMove: #{($i - $grid-center) * 1px};
--vyAngle: atan(#{($grid-center - $i) * 0.06});
}
}
}
這段結果的CSS如下。
.container:has(.vision__item--c0:hover) {
--vxMove: -9px;
--vxAngle: atan(-0.54);
}
.container:has(.vision__item--r0:hover) {
--vyMove: -9px;
--vyAngle: atan(0.54);
}
.container:has(.vision__item--c1:hover) {
--vxMove: -8px;
--vxAngle: atan(-0.48);
}
.container:has(.vision__item--r1:hover) {
--vyMove: -8px;
--vyAngle: atan(0.48);
}
/* 以下省略 */
重點在於使用:has()
來定義.container
的變數。
透過這種方式,.container
的子孫元素.eye
(眼球整體)或.eye__iris
(虹彩)
都會在被改變的CSS變數的範圍內。
此外,為了更接近實感,
使用了反三角函數的反正切(atan())。
將等腰三角形的底邊等分後,從某個點到頂點畫一條線,
距離底邊中心越遠,角度就會越小,而使用反三角函數可以重現這一點。
這兩個變數將如下使用。
$pers-depth: 90px;
.eye {
transition: transform .25s ease;
transform: translate(var(--vxMove), var(--vyMove));
&__iris {
transition: transform .25s ease;
transform: rotateX(var(--vyAngle)) rotateY(var(--vxAngle)) translateZ($pers-depth);
}
}
這次按照這樣的順序置放了層次結構。
.eye__iris
.eye__ciliary
.eye__ciliary--sub
.eye::after
.eye::before
.eye__reflect
向下到手前側(z軸的正方向)堆疊的感覺。
眼球的陰影在周邊部分較強,
因而在虹彩移動時,越接近圓的外緣,虹彩也會變得較暗。
這樣能夠讓效果看起來更加真實。
此外,在球體的情況下,球體旋轉時,反射理應完全不受影響,
但實際上,球體表面微小的扭曲會反映出來,因此
每當移動時,反射會略微搖晃。
為了表現這一點,我準備了一個變數,並且只有當滑鼠懸停在奇數感測器時,
反射才會微微移動。
.container {
&:has(.vision__item:nth-child(odd):hover) {
--vRefAngle: .8deg;
}
}
.eye {
clip-path: $lidFull;
#{$eyeClosed} & {
clip-path: $lidHalf;
}
position: absolute;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
width: $sizeAll;
height: $sizeAll;
border-radius: 50%;
background-color: #fff;
perspective: $pers-distance;
transition: transform .25s ease, scale .25s ease-out, clip-path .25s ease;
transform: translate(var(--vxMove), var(--vyMove));
scale: .8;
.container:has(.vision:hover) & {
scale: 1;
}
#{$eyeClosed}:has(.vision:hover) & {
animation: eyeBlink 10s ease infinite backwards;
}
&::before,
&::after {
position: absolute;
border-radius: 50%;
content: "";
top: 0;
left: 0;
width: 100%;
height: 100%;
}
&::before {
z-index: 3;
box-shadow: inset 0 -30px 50px rgba(16, 16, 8, 0.8), inset 0 -5px 15px rgba(16, 16, 8, 0.8);
mix-blend-mode: multiply;
}
&::after {
z-index: 2;
opacity: 0.75;
background-image: radial-gradient(circle at 45% 45%, #ffe 30%, #222 70%);
mix-blend-mode: color-burn;
}
}
在.eye
中,使用偽元素增加陰影,並且,如前所述,根據感測器的懸停情況,讓眼球稍微朝著該方向傾斜。
此外,還增加了一個功能,即眼球會朝著你靠近。
透過將初始的scale
設定為0.8,在懸停時改為1來實現。
或許有人會感到奇怪,
其實scale
、translate
和rotate
等
是可以獨立於transform
來設置的。
這樣會使得transform
相關的值可以相對自由、更易於理解地進行設置。
如果利用CSS自定義屬性,transform
屬性本身
就不需使用複雜的方式在@keyframes
中進行設置了。
陰影的加成方式幾乎沒有變,
使用mix-blend-mode
的「multiply(乘算)」和「color-burn(顏色混燒)」來使影子更具效果……這樣,微妙地調整以表現出深度感。
所以,感覺很重要。
.eye {
&__iris {
position: relative;
z-index: 1;
width: $sizeIris;
height: $sizeIris;
border-radius: 50%;
background-image: radial-gradient(circle closest-side at center, #b86e29 45%, #94c7d4 55%, #94c7d4 65%, #58697C 94%, #fff 100%);
transition: transform .25s ease;
transform: rotateX(var(--vyAngle)) rotateY(var(--vxAngle)) translateZ($pers-depth);
mix-blend-mode: multiply;
&::before {
@include absolute-center;
content: "";
z-index: 3;
width: 36px;
height: 36px;
background: radial-gradient(circle at center, #000 50%, transparent 100%);
border-radius: 50%;
opacity: .8;
filter: blur(3px);
transition: transform 0.3s ease;
.container:hover & {
transform: translate(-50%, -50%) scale(0.8);
}
}
}
}
虹彩(.eye__iris
)主要有兩個作用:根據感測器移動,以及貢獻虹彩自身的基本外觀。
關於運動,正如前面提到的,通過沿z軸方向推進後旋轉,
讓整個眼球看起來像在移動。
在外觀上,通過background-image
的radial-gradient()
從中心繪製漸層來表達。
從中心開始,顏色由棕色→淺藍色→藍色→灰色藍色→白色過渡。
灰色藍色與白色之間的部分,營造出稍微暈染的邊界,增加透視感。
::before
是瞳孔部分,在.container
懸停時稍微縮小。
.eye {
&__ciliary {
@include absolute-center;
display: grid;
width: 90%;
height: 90%;
z-index: 1;
&--sub {
z-index: 2;
width: 55%;
height: 55%;
}
}
&__ciliaryItem {
position: absolute;
width: 100%;
height: 100%;
clip-path: polygon(
50% 0,
calc(50% + 2px) 50%,
50% 100%,
calc(50% - 2px) 50%
);
opacity: .3;
background-color: $col-bk8;
@for $num from 2 through 72 {
&:nth-child(#{$num}) {
transform: rotateZ(5deg * ($num - 1));
}
}
}
}
這部分上次是減少要素以增加CSS的寫法,
但這次是增加要素數量而減少CSS的寫法。
具體來說,準備了72個要素,每個要素以5度為間隔旋轉,
形成360度輻射狀的細長菱形。
和上次一樣,這個毛樣體會複製並將顏色加深調整到與瞳孔大小相同,
並疊加在一起。
重點是改變了菱形的創建方法。
上次是用正方形的元素在45度軸上壓扁來實現,
而這次是利用clip-path
的polygon()
來切割出菱形,
從而使CSS最小化。
.eye {
&__reflect {
--yRef: 12deg;
--xRef: -12deg;
position: absolute;
z-index: 4;
width: 16px;
height: 16px;
border-radius: 50%;
opacity: .85;
background-color: #fff;
filter: blur(.5px);
transition: transform .2s ease;
transform: rotateX(calc(var(--vRefAngle) + var(--yRef))) rotateY(calc(var(--vRefAngle) + var(--xRef))) translateZ($pers-depth);
&--sub {
--yRef: 20deg;
--xRef: -20deg;
width: 75px;
height: 75px;
opacity: .2;
filter: blur(1px);
background-color: transparent;
background-image: linear-gradient(-45deg, #fff, #fff0);
}
&--tiny {
--yRef: 26deg;
--xRef: -4deg;
width: 8px;
height: 8px;
opacity: .9;
}
}
}
反射(.eye__reflect
)這次加入了三種反射。
這三種反射首先在z軸前方懸浮90px,
然後在中心的位置進行x/y軸旋轉移動。
因此,從外觀上看是橢圓形的,
相較於前一次看起來更自然。
在這裡也使用了CSS自定義屬性。
transform
屬性僅在.eye__reflect
中設置,
但通過分別設置--yRef
和--xRef
變數在.eye__reflect
、.eye__reflect--sub
、.eye__reflect--tiny
中,
使得transform
的值可變化。
即使這樣也可以說明,這樣也絕對是精彩的完成效果,不過,我想到了些事情。
如果能眨眼的話會更可愛吧?
因此,這是一個艱鉅的挑戰,我想要進行更多的修改以實現眨眼。
為了眨眼,眼瞼必須閉合。
因此,必須先創建眼瞼。
以下是我創建的眼瞼:
$lidFull: path("M0,100s-10,100,100,100,100-100,100-100c0,0,10-100-100-100S0,100,0,100Z");
$lidHalf: path("M0,100s45,55,100,55,100-55,100-55c0,0-45-55-100-55S0,100,0,100Z");
$lidQuarter: path("M0,100s45,20,100,20,100-20,100-20c0,0-45-20-100-20S0,100,0,100Z");
$lidClose: path("M0,100s45,0,100,0,100-0,100-0c0,0-45-0-100-0S0,100,0,100Z");
依次解釋,$lidFull
是全開,即正常(什麼是正常)眼球狀態,
$lidHalf
是正常張開的狀態,$lidQuarter
是半閉眼狀態,
$lidClose
則是閉眼狀態。
這是用三次貝賽曲線來描繪的,
單純在線型菱形的上下兩點上增加控制點,
通過改變這上下兩點和控制點來操作。
還製作了動畫效果。
@keyframes eyeBlink {
0%, 2%, 4%, 100% {
clip-path: $lidHalf;
}
1% {
clip-path: $lidClose;
}
3% {
clip-path: $lidQuarter;
}
}
眨眼時稍微半閉的過程來增加真實感。
順帶一提,@keyframes
的%部分如果值相同,
可以像上面那樣合併成一條來書寫。
以下是實際的應用部分。
$eyeClosed: '.container:has(.openCb:not(:checked))';
.eye {
clip-path: $lidFull;
#{$eyeClosed} & {
clip-path: $lidHalf;
}
#{$eyeClosed}:has(.vision:hover) & {
animation: eyeBlink 10s ease infinite backwards;
}
}
眼瞼的形狀作為clip-path
的值使用。
當取消左上角的「full open」勾選框時,會出現眼瞼,
然後在懸停時觸發眨眼。
其實即便在「full open」狀態下,clip-path
也是生效的,
但因為那一範圍在眼球的圓形之外,因此不受到影響。
最後在眼球下面附上了一行「touch this」的文字。
如果不摸一下,是無法感受到可愛的……
這樣就完成了。
查看Pen eye pure css, without svg&canvas ver.2 (reactive) by ichimonzi (@ichimonzi) on CodePen。
也考慮過讓眼瞼看起來更真實(比如,讓眼瞼變得非對稱的形狀),但這樣會使得閉合時的行為變得難以控制,所以放棄了。若過度調整這些部分,會覺得「那不就與SVG一樣了嗎?」的感覺……
若能表現出虹彩內部的反射和毛樣體每一根的微小差異,會變得更真實。
但這樣的事情不是HTML和CSS可以做到的吧? 我這麼想。
各位也請試著製作自己獨特的眼球,來好好疼愛它吧。
原文出處:https://qiita.com/ichimonji_haji/items/f5fbd8e1ccb6d8f757e6