🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付

前回的故事

曾經寫過一篇名為「用HTML和CSS來畫出真實的眼球。」的文章。
在那時,我灌注了當時所知的所有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);
  }
}

眼球的層次結構

這次按照這樣的順序置放了層次結構。

  1. 虹彩 .eye__iris
    1. 毛樣體(大) .eye__ciliary
    2. 毛樣體(小) .eye__ciliary--sub
    3. 瞳孔
  2. 眼球的陰影1 .eye::after
  3. 眼球的陰影2 .eye::before
  4. 反射 .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來實現。

或許有人會感到奇怪,
其實scaletranslaterotate
可以獨立於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-imageradial-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-pathpolygon()來切割出菱形,
從而使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


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝12   💬9   ❤️4
553
🥈
我愛JS
📝3   💬9   ❤️7
189
🥉
AppleLily
📝1   💬4   ❤️1
68
#4
xxuan
💬1  
3
#5
💬1  
3
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
🔧 阿川の電商水電行
Shopify 顧問、維護與客製化
💡
小任務 / 單次支援方案
單次處理 Shopify 修正/微調
⭐️
維護方案
每月 Shopify 技術支援 + 小修改 + 諮詢
🚀
專案建置
Shopify 功能導入、培訓 + 分階段交付