ポケロペンチオという軍団

日々のあれやこれやをなんやかんや

WEB

マウスにあわせて目玉が動くアニメーションをWEBページ上で実装したときの覚書

動く目玉

受託案件で、動く目玉を作ってもらいたいという要望がありました。

単純に作成するだけでも比較的難易度が高かったのですが、ブラウザ互換性やパフォーマンスの観点でつまづくことが多く、okが出るまで苦労しました。
ここに実装の記録を残しておこうと思います。

動く目玉の要件定義

今回作成した目玉の要件定義は以下のようなものになっています。

  1. 常時、瞳がカーソルポインターの方向を見続ける
  2. 瞬きを発生させる

完成品サンプルは下になります。

動く目玉の実装に使ったツール

  1. jQuery(Ver3.2.1)
  2. sass

動く目玉を実装するうえでつまづいたこと

最も苦労したのはパフォーマンスでした。

マシンスペックが高くないPCで閲覧すると瞬きが遅くなることが多く、さまざな方法での試行錯誤が必要となりました。

transitionを用いたcssアニメーションでは環境により瞬きが遅くなることが確認されたのですが、@keyframesを用いて1%ずつアニメーションを定義するという方法をとることによりパフォーマンスは改善されました。

1%ずつのアニメーションをcssで直接書くのは苦行なので、sassを利用して1%ずつのアニメーションを自動出力する方法をとりました。

動く目玉のソースコード

ソースコードはこちらです。

動く目玉のHTML(PHPです)

<div class="c_eye">
<?php for ($i=0; $i < 2; $i++) : ?>
<div class="c_eye__eye">
<?php
$outer_circle_r = 43;//瞳の可動域半径
$inner_circle_r = 26;//瞳の半径。
$border_width = 1;
$outer_eye_border_color = "";//瞳の可動域円周色
$outer_eye_border_stroke = "";//瞳の可動域円周太さ
$inner_eye_fill_color = '#4c4c4c';//瞳の色
?>
<div class="c_eye__outer_0">
<svg class="c_eye__outer_1" width="<?php echo ($outer_circle_r + $border_width) * 2 ?>" height="<?php echo ($outer_circle_r + $border_width) * 2 ?>" xmlns="http://www.w3.org/2000/svg" data-js_eye_master>
<g class="" transform="translate(<?php echo $outer_circle_r + $border_width?>,<?php echo $outer_circle_r + $border_width?>)" data-js_eye_eye>
<circle cx="0" cy="0" r="<?php echo $outer_circle_r ?>" stroke="<?php echo $outer_eye_border_color ?>" stroke-width="<?php echo $outer_eye_border_stroke ?>" fill="transparent" />
<circle cx="0" cy="0" r="<?php echo $inner_circle_r ?>" fill="<?php echo $inner_eye_fill_color ?>" />
</g>
</svg>
<div class="c_eye__outer_2" data-js-blink>
</div>
</div>
</div>
<?php endfor ?>
</div>

動く目玉のsass

@charset "UTF-8";

.c_eye{
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
flex-direction: row;
-webkit-justify-content: center;
justify-content: center;
-webkit-box-align: center;
-moz-box-align: center;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center;

$blink_r: 60px;//瞬き部分の円の半径

@mixin size {
width: $blink_r * 2;
height: $blink_r * 2;
}

&__eye{
margin: 0 20px;
display:inline-block
}

&__outer{
&_0{
position: relative;
@include size;
}
&_1{
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}

&_2{
position: absolute;

@include size;
background: radial-gradient(circle, rgba(0,0,0,.0) $blink_r, white $blink_r);
border-radius: 100%;

&.is-current{
animation: c_eye__outer_2 0.25s ;
$percent: 100;

@keyframes c_eye__outer_2 {
@for $i from 0 through $percent {
#{$i}% {
$v: abs( (0 - $blink_r) / ( ($percent / 2) - 0) * $i + $blink_r) ;
background: radial-gradient(circle, rgba(0,0,0,.0) $v, white $v);
}
}
}
}
}
}
}

動く目玉のcss

.c_eye {
display: -webkit-box;
display: -moz-box;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
flex-direction: row;
-webkit-justify-content: center;
justify-content: center;
-webkit-box-align: center;
-moz-box-align: center;
-ms-flex-align: center;
-webkit-align-items: center;
align-items: center;
}

.c_eye__eye {
margin: 0 20px;
display: inline-block;
}

.c_eye__outer_0 {
position: relative;
width: 120px;
height: 120px;
}

.c_eye__outer_1 {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
margin: auto;
}

.c_eye__outer_2 {
position: absolute;
width: 120px;
height: 120px;
background: radial-gradient(circle, transparent 60px, white 60px);
border-radius: 100%;
}

.c_eye__outer_2.is-current {
animation: c_eye__outer_2 0.25s;
}

@keyframes c_eye__outer_2 {
0% {
background: radial-gradient(circle, transparent 60px, white 60px);
}
1% {
background: radial-gradient(circle, transparent 58.8px, white 58.8px);
}
2% {
background: radial-gradient(circle, transparent 57.6px, white 57.6px);
}
3% {
background: radial-gradient(circle, transparent 56.4px, white 56.4px);
}
4% {
background: radial-gradient(circle, transparent 55.2px, white 55.2px);
}
5% {
background: radial-gradient(circle, transparent 54px, white 54px);
}
6% {
background: radial-gradient(circle, transparent 52.8px, white 52.8px);
}
7% {
background: radial-gradient(circle, transparent 51.6px, white 51.6px);
}
8% {
background: radial-gradient(circle, transparent 50.4px, white 50.4px);
}
9% {
background: radial-gradient(circle, transparent 49.2px, white 49.2px);
}
10% {
background: radial-gradient(circle, transparent 48px, white 48px);
}
11% {
background: radial-gradient(circle, transparent 46.8px, white 46.8px);
}
12% {
background: radial-gradient(circle, transparent 45.6px, white 45.6px);
}
13% {
background: radial-gradient(circle, transparent 44.4px, white 44.4px);
}
14% {
background: radial-gradient(circle, transparent 43.2px, white 43.2px);
}
15% {
background: radial-gradient(circle, transparent 42px, white 42px);
}
16% {
background: radial-gradient(circle, transparent 40.8px, white 40.8px);
}
17% {
background: radial-gradient(circle, transparent 39.6px, white 39.6px);
}
18% {
background: radial-gradient(circle, transparent 38.4px, white 38.4px);
}
19% {
background: radial-gradient(circle, transparent 37.2px, white 37.2px);
}
20% {
background: radial-gradient(circle, transparent 36px, white 36px);
}
21% {
background: radial-gradient(circle, transparent 34.8px, white 34.8px);
}
22% {
background: radial-gradient(circle, transparent 33.6px, white 33.6px);
}
23% {
background: radial-gradient(circle, transparent 32.4px, white 32.4px);
}
24% {
background: radial-gradient(circle, transparent 31.2px, white 31.2px);
}
25% {
background: radial-gradient(circle, transparent 30px, white 30px);
}
26% {
background: radial-gradient(circle, transparent 28.8px, white 28.8px);
}
27% {
background: radial-gradient(circle, transparent 27.6px, white 27.6px);
}
28% {
background: radial-gradient(circle, transparent 26.4px, white 26.4px);
}
29% {
background: radial-gradient(circle, transparent 25.2px, white 25.2px);
}
30% {
background: radial-gradient(circle, transparent 24px, white 24px);
}
31% {
background: radial-gradient(circle, transparent 22.8px, white 22.8px);
}
32% {
background: radial-gradient(circle, transparent 21.6px, white 21.6px);
}
33% {
background: radial-gradient(circle, transparent 20.4px, white 20.4px);
}
34% {
background: radial-gradient(circle, transparent 19.2px, white 19.2px);
}
35% {
background: radial-gradient(circle, transparent 18px, white 18px);
}
36% {
background: radial-gradient(circle, transparent 16.8px, white 16.8px);
}
37% {
background: radial-gradient(circle, transparent 15.6px, white 15.6px);
}
38% {
background: radial-gradient(circle, transparent 14.4px, white 14.4px);
}
39% {
background: radial-gradient(circle, transparent 13.2px, white 13.2px);
}
40% {
background: radial-gradient(circle, transparent 12px, white 12px);
}
41% {
background: radial-gradient(circle, transparent 10.8px, white 10.8px);
}
42% {
background: radial-gradient(circle, transparent 9.6px, white 9.6px);
}
43% {
background: radial-gradient(circle, transparent 8.4px, white 8.4px);
}
44% {
background: radial-gradient(circle, transparent 7.2px, white 7.2px);
}
45% {
background: radial-gradient(circle, transparent 6px, white 6px);
}
46% {
background: radial-gradient(circle, transparent 4.8px, white 4.8px);
}
47% {
background: radial-gradient(circle, transparent 3.6px, white 3.6px);
}
48% {
background: radial-gradient(circle, transparent 2.4px, white 2.4px);
}
49% {
background: radial-gradient(circle, transparent 1.2px, white 1.2px);
}
50% {
background: radial-gradient(circle, transparent 0px, white 0px);
}
51% {
background: radial-gradient(circle, transparent 1.2px, white 1.2px);
}
52% {
background: radial-gradient(circle, transparent 2.4px, white 2.4px);
}
53% {
background: radial-gradient(circle, transparent 3.6px, white 3.6px);
}
54% {
background: radial-gradient(circle, transparent 4.8px, white 4.8px);
}
55% {
background: radial-gradient(circle, transparent 6px, white 6px);
}
56% {
background: radial-gradient(circle, transparent 7.2px, white 7.2px);
}
57% {
background: radial-gradient(circle, transparent 8.4px, white 8.4px);
}
58% {
background: radial-gradient(circle, transparent 9.6px, white 9.6px);
}
59% {
background: radial-gradient(circle, transparent 10.8px, white 10.8px);
}
60% {
background: radial-gradient(circle, transparent 12px, white 12px);
}
61% {
background: radial-gradient(circle, transparent 13.2px, white 13.2px);
}
62% {
background: radial-gradient(circle, transparent 14.4px, white 14.4px);
}
63% {
background: radial-gradient(circle, transparent 15.6px, white 15.6px);
}
64% {
background: radial-gradient(circle, transparent 16.8px, white 16.8px);
}
65% {
background: radial-gradient(circle, transparent 18px, white 18px);
}
66% {
background: radial-gradient(circle, transparent 19.2px, white 19.2px);
}
67% {
background: radial-gradient(circle, transparent 20.4px, white 20.4px);
}
68% {
background: radial-gradient(circle, transparent 21.6px, white 21.6px);
}
69% {
background: radial-gradient(circle, transparent 22.8px, white 22.8px);
}
70% {
background: radial-gradient(circle, transparent 24px, white 24px);
}
71% {
background: radial-gradient(circle, transparent 25.2px, white 25.2px);
}
72% {
background: radial-gradient(circle, transparent 26.4px, white 26.4px);
}
73% {
background: radial-gradient(circle, transparent 27.6px, white 27.6px);
}
74% {
background: radial-gradient(circle, transparent 28.8px, white 28.8px);
}
75% {
background: radial-gradient(circle, transparent 30px, white 30px);
}
76% {
background: radial-gradient(circle, transparent 31.2px, white 31.2px);
}
77% {
background: radial-gradient(circle, transparent 32.4px, white 32.4px);
}
78% {
background: radial-gradient(circle, transparent 33.6px, white 33.6px);
}
79% {
background: radial-gradient(circle, transparent 34.8px, white 34.8px);
}
80% {
background: radial-gradient(circle, transparent 36px, white 36px);
}
81% {
background: radial-gradient(circle, transparent 37.2px, white 37.2px);
}
82% {
background: radial-gradient(circle, transparent 38.4px, white 38.4px);
}
83% {
background: radial-gradient(circle, transparent 39.6px, white 39.6px);
}
84% {
background: radial-gradient(circle, transparent 40.8px, white 40.8px);
}
85% {
background: radial-gradient(circle, transparent 42px, white 42px);
}
86% {
background: radial-gradient(circle, transparent 43.2px, white 43.2px);
}
87% {
background: radial-gradient(circle, transparent 44.4px, white 44.4px);
}
88% {
background: radial-gradient(circle, transparent 45.6px, white 45.6px);
}
89% {
background: radial-gradient(circle, transparent 46.8px, white 46.8px);
}
90% {
background: radial-gradient(circle, transparent 48px, white 48px);
}
91% {
background: radial-gradient(circle, transparent 49.2px, white 49.2px);
}
92% {
background: radial-gradient(circle, transparent 50.4px, white 50.4px);
}
93% {
background: radial-gradient(circle, transparent 51.6px, white 51.6px);
}
94% {
background: radial-gradient(circle, transparent 52.8px, white 52.8px);
}
95% {
background: radial-gradient(circle, transparent 54px, white 54px);
}
96% {
background: radial-gradient(circle, transparent 55.2px, white 55.2px);
}
97% {
background: radial-gradient(circle, transparent 56.4px, white 56.4px);
}
98% {
background: radial-gradient(circle, transparent 57.6px, white 57.6px);
}
99% {
background: radial-gradient(circle, transparent 58.8px, white 58.8px);
}
100% {
background: radial-gradient(circle, transparent 60px, white 60px);
}
}

動く目玉のjavascript(jQueryです)

$(window).on('load', function() {
var cOrCalc = ' ';
var cAttr1 = 'data-js_eye_master';
var cAttr2 = 'data-js_eye_eye';
$('[' + cAttr1 + ']' + cOrCalc + '[' + cAttr2 + ']').each(function(){
// 黒目の部分(子要素のうち2番目のcircle要素)
var inner = this.querySelectorAll('circle')[1];
// 白目の部分(子要素のうち1番目のcircle要素)
var outer = this.querySelectorAll('circle')[0];

fSetEyePos(-1000, -1000, inner, outer);//目玉の適当な初期状態

$(window).mousemove(function(ev){
fSetEyePos(ev.pageX, ev.pageY, inner, outer);

});
});

// 黒目にカーソルポインターの方向を向かせる
function fSetEyePos(X, Y, inner, outer){
// 要素の絶対座標と幅・高さを取得
var rect_inner = inner.getBoundingClientRect();
var rect_outer = outer.getBoundingClientRect();
// 白目の半径と黒目の半径の差分(あとで使う)
var R_DIFF = rect_outer.width/2 - rect_inner.width/2;
// 白目の中心座標
var X_CENTER = rect_inner.left + rect_inner.width/2;
var Y_CENTER = rect_inner.top + rect_inner.height/2;
// マウスカーソルの絶対座標より、白目の中心からの位置のズレを取得
var dx = X - X_CENTER;
var dy = Y - Y_CENTER;
// 白目の中心からの距離
var dr = Math.sqrt(dx*dx+dy*dy);
if( dr > R_DIFF ){
dx *= R_DIFF/dr;
dy *= R_DIFF/dr;
}
$(inner).attr('transform','translate('+dx+','+dy+')');
}

// 以下瞬き用スクリプト
var vBlinkObj = $('[data-js-blink]');

var cEyeBlinkInterval = 400;//瞬きを実行しようとする周期

setInterval(function(){
var val = Math.ceil( Math.random()*100 );//1〜100
var cBlinkPercentage = 20;

if(val < cBlinkPercentage){//定周期で瞬きを実行しようとするが、実行しようとするたびに乱数を取得し、cBlinkPercentage以下の乱数が取得された場合には瞬きを実行する。

var cClassName = 'is-current';
$(vBlinkObj).addClass(cClassName);

var timerId = setInterval(function(){
$(vBlinkObj).removeClass(cClassName);

clearInterval(timerId);

},cEyeBlinkInterval - 20);//次の瞬きのために、cEyeBlinkIntervalの20ms前にクラスを外しておく

}
},cEyeBlinkInterval);

});

動く目玉のソースコードについて

svgを利用した瞳の実装

瞳を描画するだけなら、border-radiusで描画すれば良いのですが、瞳の可動域を制御することが難しそうだと考えました。

svgを使用することにより、瞳の可動域半径を簡単に定義することができるようになっています。

sassによる1%ずつのアニメーション実装

瞬きの円は、中心に近い部分は透過色(つまり見えないので目が開いている状態を実現できる)で、中心から遠い部分は白として、グラデーションを用いて塗り分けています。

今回の最大のポイントである、sassによる1%ずつのアニメーション実装部分が下のコードになります。


$percent: 100;
@keyframes c_eye__outer_2 {
@for $i from 0 through $percent {
#{$i}% {
$v: abs( (0 - $blink_r) / ( ($percent / 2) - 0) * $i + $blink_r) ;
background: radial-gradient(circle, rgba(0,0,0,.0) $v, white $v);
}
}
}

$blink_r(=60)は直上のソースコード内には含まれていない定数ですが、瞬きの円の透過部分の半径を意味しています。


background: radial-gradient(circle, rgba(0,0,0,.0) $v, white $v);

アニメーション自体とは関係ありませんが、上のコードで、rgba(0,0,0,.0) $vとwhite $v に同じ値$vを設定することにより、グラデーションの色の切り替わりをなめらかでなく、即座に色変化するようにしています。

このグラデーションの切り替わり位置をアニメーションを用いて変化させることにより、瞬きを実装しました。

コードの内容としては、下図を見ていただくとわかりやすいかと思います。

動く目玉の瞬き

つまり、アニメーション0%時には目が完全に開いている状態で、50%になると完全に目を閉じ、100%になると目を完全に開く、という動作を実現しています。

jQueryの動作内容について

jQueryでは瞳がカーソルポインターの方に向く機能と、瞬きの機能の二つの機能を実装しています。

カーソルポインターを向く瞳の実装

目の中心を原点とした直行座標系を考え、三平方の定理を用いて瞳の配置座標を計算しています。
この部分は図形に関する知識が必要になります。

瞬きの実装

定周期でタイマーを回し、sassで実装したアニメーションを実行させるためのクラスの付与・排除を行なっています。

一定の間隔で瞬きが発生すると機械的に見えてしまうので、乱数を用いて、ある一定の値の範囲に該当する乱数が取得された場合にのみ瞬きを発生させることとしました。

動く目玉の実装の終わりに

動く目玉の実装には、図形の知識、関数の知識、sassによるプログラミングなど、普通のwebサイトを作る上ではあまり使う機会の多くない技術が必要で、楽しい分実装には一苦労でした。

またこういったものを作る機会に出会えることを願って、今回はこれにて。

-WEB