サーバーサイドメインで開発していた私が、先日フロントエンドの会社に入社いたしまして早1ヶ月。
そろそろ学んだことをアウトプットしてみようということで、本記事を書くに至りました。
これまでと完全に異分野というわけではありませんでしたが、新しい知見が得られるのはとても楽しいですね!
是非とも皆さんにもこの 楽しい を共有できれば幸いです。
ということで今回は、フロント周りに詳しくない人が流し読みをすると
「 Pug / SCSS / (CSSアニメーション/3D) 」についてフワッと用語・使い方を理解できるようになる、ことを目的に記事を書いていきます。
気軽に読んでいただけると嬉しいです!
概要
対象読者
- UIに興味のあるサーバーサイドエンジニアの方
- Pug/SCSSと聞いてピンとこない方
- CSSアニメーション/3Dを触ってみたい方
作ったもの
なんちゃって太陽系を作りました。
See the Pen 太陽系 by toto (@totoinu) on CodePen.
仕様
- 太陽を中心に惑星が回る
- 周回する軌道も描画する
- 惑星が球に見えるようにする
技術選定
項目 | 選定 | 備考 |
---|---|---|
HTML | Pug | - |
CSS | SCSS | - |
(コンパイル) | gulp | CodePenでは不要ですが、ローカル環境で開発するときに使いました。※今回は取りあげません |
申し送り...
皆さんの言いたいこと、よく分かります。
私自身も「イケてないなぁ〜(;・∀・)」というところが星の数ほどありますが、今回は学習内容を気軽にアウトプットするのが目的なので短時間でまとめた次第です。
ですが!改善方法などあれば、是非ともコメントくださると嬉しいです!
個人的に気になっている点
- 視点の角度($angle)を落とすと、惑星オブジェクトが平面に見える
- 惑星のグラデーションが、太陽の位置関係に対応していない(太陽より強い光源が果てにある世界...)
知識整理
ここでは「Pug」と「SCSS」,「BEM」について簡単に説明していきます。
ただし、細かいメソッドなどは(今回の開発内容で出るものに限り)後述します。
詳しいそれぞれの使い方については、必要なタイミングで記載していきますが
この章ではそれぞれの技術概要を説明いたします。
Pugとは
Pugのポイントは、「タグをいちいち書いたり閉じたりする必要がない代わりに、インデントで親子関係を明らかにする」ところにあります。
例として、htmlとPugで同じ表示になるものを書いてみました。
<body>
<section id="introduction">
<h1 class="title">自己紹介</h1>
<div class="detail">
<p>名前: <span class="text">とと犬</span></p>
<p>年齢: <span class="text">5</span></p>
<p>犬種: <span class="text">ミニチュアダックスフンド</span></p>
</div>
<img src="自撮り画像のパス" alt="自撮り" class="portrait"/>
</section>
</body>
body
section#introduction
h1.title 自己紹介
.detail
p
| 名前:
span.text とと犬
p
| 年齢:
span.text 5
p
| 犬種:
span.text ミニチュアダックスフンド
img(src="自撮り画像のパス" alt="自撮り").portrait
これらを比較して分かる通り、使い方としては下記の通りです。
- タグは書かない(タグ名まで省略されるとdiv扱い)
- タグ名の後に
.
または#
を使うと、それぞれclassやIDを意味する (.detail.body
のように繋げることも可能) - 属性値を書くときには、
()
内に記載する - タグのvalueは、1行なら1スペースの後に、改行後なら
インデント+|+スペース
の後に記載する※1行の例としては、span.text ミニチュアダックスフンド
このような書き方です。
他にも、for文やif文を書ける点も非常に便利なので色々と使ってみてください!
SCSSとは
SCSSとは、CSSをより手軽に書ける上に保守性が高い記法です。
- ネストした書き方が可能
- 定義したスタイルの塊を手軽に呼び出せる
- 変数の定義が可能
- (変数を使った)計算も可能
後二つについては、今回のコードを追う過程で例を挙げて記述していきます。
それではネストした書き方が可能とはどういうことでしょうか。
先ほどと同様に、CSSとSCSSで同じものを書いてみましたのでご覧ください。
.space {
position: relative;
}
.space__planet {
position: absolute;
top: 50%;
left: 50%;
}
.space__planet::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
}
.space {
position: relative;
&__planet {
position: absolute;
top: 50%;
left: 50%;
&::before{
content: "";
position: absolute;
top: 50%;
left: 50%;
}
}
}
つまり、ネストした書き方とは「クラス名の共通する部分(今回だとspace)を1度書くだけで済む」点がメリットとなります。
ではこの書き方がどのようなときに活きてくるのでしょうか?それを理解するにはBEMについて知る必要があります!
BEMとは、Block Element Modifierの略で、クラス名の命名規則です。
スタイルを割り当てるときに、セレクタの優先度に振り回されることってありますよね?(圧)
そんなときに!important
を使おうものなら、後任の方のバッシング不可避です。
そこで、全てのスタイルを1クラスレベルで設定しようという発想で生み出された記法がBEMになります。
BEMでクラスを適用する際には、下記のように一つの機能単位をBlockとして、その中のパーツをElementと呼びます。
このBlockとElementを.Block__Element
のように「__」でつなぎます。 (他にも様々な接続文字の使い方があるので調べてみてください)
(全てのスタイル適用は、Block
, Block__Element
, Block__Element--Modifier
(一例)のように書かれ、idやクラスとタグを組み合わせた指定は滅多にされません!)
このBEMでクラスを命名したとき、SCSSでは非常に分かりやすく、下記のようにBlock / Element / Modifierの順でネストされます。
.Block{
// some style
&__Element1{
// some style
&--Modifier{
// some style }
}
&__Element2{
// some style
}
}
// NG
a {
// some style
}
#someID {
// some style
}
具体的に、実例を挙げてみましょう。
// Pug
.introduction
.introduction__title
.introduction__body
.introduction__name とと犬
.introduction__age 5歳
.introduction__type バーニーズマウンテンドッグ
.introduction{
&__title{
// some style
}
&__body{
// some style
}
&__name{
// some style
}
&__age{
// some style
}
&__type{
// some style
}
}
SCSSのネスト構造と、BEMが合わさっって非常に読みやすいですね!
ちなみに、Modifierの使い所としては、ボタンを押下したタイミングのボタンスタイルや、
チェックボックスにチェックを入れたときのスタイルを記載するもので、下記のようなときに使用します。
.introduction{
&__submitButton{
background-color: orange;
&--activate{
background-color: red;
}
}
}
今回の実装説明
ここまででPugとSCSSの概要をつかめたと思います。
ここからは、今回作ったものを通してより具体的な使い方や、先に説明していない使い方などについて触れていこうと思います。
Pugファイルの中身
以下がPugファイルの全てです。
doctype html
html(lang="ja")
head
meta(charset="utf-8")
meta(name="viewport" content="width=device-width,initial-scale=1")
link(rel="stylesheet" type="text/css" href="./css/reset.css")
link(rel="stylesheet" type="text/css" href="./css/index.css")
title 宇宙
body
.space
.space__planet.space__sun
.space__planet.space__mercury
.space__planet.space__venus
.space__planet.space__earth
.space__planet.space__mars
.space__planet.space__jupiter
.space__planet.space__saturn
.space__planet.space__uranus
.space__planet.space__neptune
構成はシンプルで、惑星ごとに1つdivタグが用意されているのみです。
軌道...divタグのborder
惑星...divタグの擬似要素(before)
でそれぞれ表現しています。
そのため、このdivタグが回転してくれれば惑星も公転します。手軽ですね!
別解として、よりBEM記法を活用した下記のような書き方も可能ですが
「軌道」と「惑星本体」を別々に制御する必要が出てきますので、今回はシンプルな前者の書き方としました。
doctype html
html(lang="ja")
head
meta(charset="utf-8")
meta(name="viewport" content="width=device-width,initial-scale=1")
title 宇宙
body
.space
.space__planet.space__sun
.planet__orbit
.planet__object
.space__planet.space__neptune
.planet__orbit
.planet__object
.space__planet.space__uranus
.planet__orbit
.planet__object
.space__planet.space__saturn
.planet__orbit
.planet__object
//- ...続く
SCSSファイル(変数定義)
SCSSが今回のメインなので、分解して解説していきます。
変数定義方法
SCSSでの変数定義と呼び出しは次のように行われます。
// 定義方法: プリミティブな値
$color: rgb(94, 36, 36);
// 定義方法: 連想配列(map型)
$mydog: (
name: 'inu',
height: 10,
)
// 呼び出し方法
.dog {
background-color: #{$color};
height: #{map-get($mydog, 'height')}px;
}
変数の先頭には$(ダラー)をつけて、通常のスタイル定義(key: value;
)に似た形で$var: value;
で定義します。
また、連想配列の場合は、JSONを触ったことがあれば括弧が「丸括弧」になっているだけで違和感はないと思います。
呼び出し時には、スタイルの中に#{}
を書くとその中では**「変数の呼び出し, 簡単な計算」**を行うことができます。
連想配列の場合はJSと違ってobject.key
では呼び出せません!map-get関数を用いて、map-get(変数, キー)
で呼び出しましょう。
これを踏まえて、以降今回の変数定義です。
変数定義の実装内容
$baseObjectSize: 50; // 惑星本体(太陽を除く)のサイズ基準値
$baseOrbitSize: 0.5; // 惑星軌道のサイズ基準値
$baseCycle: 0.2; // 惑星が周回する周期の基準値
この変数値を書き換えると、全ての惑星の[惑星サイズ, 軌道サイズ, 公転速度]が変わリます。
より詳しく書きますと、例えば画面に表示するサイズを**「それぞれの惑星のサイズ比率を守って」**調整するために、基準値を設けて(サイズを適用している箇所に)この値を割り算しています。
例) width: #{$軌道直径/$軌道基準値}px;
$angle: 80;
$angleは「視点の高さ」を表しており、軌道を傾けています。
そのため、数値が高くなればなるほど横からの視点になっていきます。
この際、視点の高さが変わることで惑星が平面であることが透けてしまいますが、この対策は以降のanimationにて説明します。
$planets: (
mercury: (
cycleSec: 0.24, // 一周にかかる時間
orbitDiameter: 58, // 軌道直径サイズ
objectDiameter: 488, // 惑星直径サイズ
color: rgb(30, 104, 241) // 惑星の色
),
venus: (
cycleSec: 0.62,
orbitDiameter: 108,
objectDiameter: 1210,
color: rgb(241, 209, 30)
),
earth: (
cycleSec: 1,
orbitDiameter: 150,
objectDiameter: 1276,
color: rgb(60, 149, 250)
),
// 続く...
なるべく惑星の設定値を一塊にしたかったので、マップ型で定義しています。
ここでのサイズや時間に単位はなく、各惑星間の比率程度に思っておいてください。
(実際に適用される値は、基準値との乗除で算出されます)
SCSSファイル(惑星のスタイル)
惑星のスタイルを適用するにあたってのポイントは2点です。
- 保守性を爆上げするために、mixinを使って「惑星と軌道のスタイル」を書くのは1回のみ
- 惑星が平面であることが透けないように、animationで世界を騙す
必要な知識の整理
コードの説明前に、必要な知識をざっと説明していきます。
- mixinとは?
- animation(と
@keyframes
)とは?
まずは、mixinとは?ですが
これは「スタイルを返すメソッド」です。
例えば次のコードでは、「引数を元に、文字色と背景色のスタイルを返す」ミックスインを定義しています。
これを呼び出す際は、@include
なるものを用います。
@mixin setColor($font, $bg) {
color: #{$font};
background-color: #{$bg};
}
.myClass {
@include setColor(#333, #000)
}
@content
を使えば、さらに自由度の高いスタイル定義をすることも可能です。
メディアクエリ(レスポンシブ対応)を手軽に書くために用いられることも多いようです。
続いて、animationの説明です。
animationとは、要素の形や位置、向きなどを好きな時間動作させ続けられる。というものです。
実際はanimationだけでは足りません。以下のような役割分担がされています。
用語 | 目的 |
---|---|
transform | 状態を定義するよ |
@keyframes | あるタイミングでの状態を指定するよ |
animation | 動かすよ |
transformには、回転や移動以外にも、引き延ばしなど様々あります。
※参考: transformのドキュメント@MDN
※矢印は状態をスムーズに遷移することを表現したものであり、何かの動きをトレースしているわけではありません。
@keyframes move{
0%{ transform: translateX(0) rotateZ(0)}
50%{ transform: translateX(30px) rotateZ(0)}
100%{ transform: translateX(30px) rotateZ(45deg)}
}
.someClass{
animation: 1s linear infinite move;
}
animationでは、@keyframes
で定義したmove
という動きを指定しており、
1秒間で、一定のペースで(linear
)、繰り返し(infinite
)この動き(move
)を実行する。といった指定がされています。
@keyframes
では、1秒間のうち、「0秒地点」「0.5秒地点」「1秒地点」の状態をtransform
で定義しています。
惑星スタイルの実装内容
これまでのことを踏まえ、惑星のスタイルの設定内容を説明していきます。
惑星と軌道のスタイル定義については、2種類あります。
それは下記の2種類です。
- 各惑星に依存しない共通のスタイル
- 各惑星特有のスタイル
まずは共通のスタイル定義を見てみましょう。
ここでは、シンプルな定義の仕方しかしてませんね。
軌道スタイルは画面中央で円形となるようにして、惑星本体にも同様のスタイルを適用してます。
.space {
transform-style: preserve-3d;
position: relative;
&__planet {
// 惑星軌道のスタイル
top: 50%;
left: 50%;
position: absolute;
border-radius: 100%;
transform: translate(-50%, -50%);
&::before{
// 惑星本体のスタイル
content: "";
border-radius: 100%;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
}
続いて、各惑星特有のスタイルについて見ていきます。
太陽だけは、大きさと色をそのままベタ書きしてますね。
ですが他の惑星は、変数で定義した(各惑星の)[公転速度, 軌道直径, 惑星直径, 惑星の色]の塊(オブジェクト)をループすることで定義しています。
&__sun{
width: 20px;
height: 20px;
background-color: rgb(255, 51, 0);
}
@each $planetName, $planetObject in $planets {
@include orbit($planetName, $planetObject);
}
これだけだとしっくりこないかもしれませんので、詳しく書いていきましょう。
SCSSでは、@each
を使うことで連想配列や配列をループすることができます。上記のコードでは、連想配列の$planets
を、keyとvalueを受け取ってループしています。
この二つをmixin
で定義したorbitメソッドに渡すことで、それぞれの惑星の戻り値(スタイル定義)をここに挿入しているんですね。
それでは、具体的にmixin
ではどういった定義をしているのでしょうか?
@mixin orbit($name, $object) {
&__#{$name}{
transform-style: preserve-3d;
animation:#{map-get($object, 'cycleSec')/$baseCycle}s linear infinite rotation;
border: solid 0.5px rgba(190, 190, 190, 0.548);
width: #{map-get($object, 'orbitDiameter')/$baseOrbitSize}px;
height: #{map-get($object, 'orbitDiameter')/$baseOrbitSize}px;
margin: {
top: -#{map-get($object, 'orbitDiameter')/2/$baseOrbitSize}px;
left: -#{map-get($object, 'orbitDiameter')/2/$baseOrbitSize}px;
}
&::before{
background: linear-gradient(270deg, rgb(213, 227, 231), #{map-get($object, 'color')}, #{map-get($object, 'color')});
width: #{map-get($object, 'objectDiameter')/$baseObjectSize}px;
height: #{map-get($object, 'objectDiameter')/$baseObjectSize}px;
margin-top: -#{map-get($object, 'orbitDiameter')/2/$baseOrbitSize + map-get($object, 'objectDiameter')/2/$baseObjectSize}px;
animation: #{map-get($object, 'cycleSec')/$baseCycle}s linear infinite rotationObject;
}
}
}
ポイントは以下の点になります。
- セレクタ名も変数で定義している
- サイズや速度は、
#{}
内で基準値($baseOrbitSize, $baseObjectSize, $baseCycle
)で割ることで算出している - 惑星色をグラデーションにしている
1. セレクタ名も変数で定義している
実はSCSSでは、スタイル定義の中身だけでなくセレクタにも変数を使うことができます!
そのため今回は「クラスの命名」と「惑星のスタイル変数名」を揃えることで、うまく適用されるようにしています。
例)
// Pugでの要素
.space__planet.space__mercury
// SCSS: 惑星のスタイルを変数定義
$planets: (
mercury: (
cycleSec: 0.24,
orbitDiameter: 58,
objectDiameter: 488,
color: rgb(30, 104, 241)
),
// SCSS: mixin定義
@mixin orbit($name, $object) {
&__#{$name}{
// SCSS: スタイル定義場所
.space {
@each $planetName, $planetObject in $planets {
@include orbit($planetName, $planetObject);
2. サイズや速度は、#{}
内で基準値($baseOrbitSize, $baseObjectSize, $baseCycle
)で割ることで算出している
ここについては、先述の通り基準値で割って算出した値を、大きさや速度の値として割り当てることで
基準値を変えるだけで全ての惑星スタイルに適用されるようにしています。
3. 惑星色をグラデーションにしている
ここは難点が残るところですが、本来は太陽から光を受けている風にしたかったのですが
ひとまずどこか遠く右方から光を受けているようなグラデーションにしてみました。
background: linear-gradient(270deg, rgb(213, 227, 231), #{map-get($object, 'color')}, #{map-get($object, 'color')});
※参考: linear-gradient()のドキュメント @MDN
以上で惑星と軌道のスタイルが全て定義できたと思います。
これでPug/SCSSの概要と簡単な使い方が分かりましたね!
特に、SCSSの変数切り出しやメソッド(mixin)定義は、保守性を大いに高めてくれますので積極的に活用していきましょう!
(実際の開発では、変数のみを保持するSCSSファイルとして切り出します)
ここでは何点か読み飛ばしたスタイルがあります。それについて後述して本記事を締めたいと思います。
おまけ
今回、太陽系を作るにあたって立体感を出すことには、少しばかり時間をかけました。
それだけ重要なポイントなので、おまけですがCSSで3D感を出す方法について書いていきます。
仮に立体感を出す努力を怠った場合、下記のような表示になってしまいます。
See the Pen 太陽系_立体表示を怠ったver by toto (@totoinu) on CodePen.
惑星がz=0の平面を超えた回転ができていない点が問題です。
これを含めた問題について、以降対処していきます。
立体表示する際に気を付ける点
立体表示する際に肝となるのが下記のコードです。
body{
perspective: 1000px;
}
.space {
transform-style: preserve-3d;
&__planet {
transform-style: preserve-3d;
}
}
だいぶコードを端折りましたが、それぞれの要素について説明すると...
要素 | 概要 |
---|---|
perspective | 立体空間としてどれくらいを確保するのか |
transform-style | 各オブジェクトが平面外に出ることを許容するか |
以下、具体的にお話ししていきます。
perspectiveのドキュメント(MDNより)を見ると、
z=0 の平面とユーザーとの間の距離を定めて三次元に配置された要素に遠近感を与えます。 z>0 である三次元要素はより大きく、 z<0 である三次元要素はより小さくなります。効果の強度はこのプロパティの値から決められます。
と書かれています。
以下のCodePenでは、要素をY軸回転していますが
確かに「手前(z>0)に来る左辺は本来の大きさより大きく、奥(z<0)へ行く右辺は本来の大きさより小さくなっています」
See the Pen perspective by toto (@totoinu) on CodePen.
値によって遠近感が変わる際の仕組みについて、調べてもなかなか合点のいく回答が得られませんでしたので
体験知と自分なりの解釈を書いてみます。(公式解釈ではないのでご注意ください!)
[CodePenでやったこと]
- 複数のperspective条件下(0, 500, 1000, 10000)で、オブジェクトをY軸回転
- 立体の回転が分かりやすいよう、z=0平面の枠線を表示
- 正面から見ると歪みの違いが分からないので、perspective-originで横から見る
この結果として
- 数値が小さくなるほどに、歪みが大きくなり
- 数値が大きくなるほどに、perspectiveを未設定(立体感なし)に近づいていく
図解すると、このような感じなのかなと思います。
perspectiveが小さければ、オブジェクトとライト(勝手な想像)の距離が短くなり、描画(写像)はその分手前と奥をよく反映したものになる。
一方perspectiveが大きければ、オブジェクトとライト(勝手な想像)の距離が長くなり、その分、手前と奥の違いが十分に表現されなくなる。
このようなイメージでいれば扱いやすいのではないかと考えました。もし正確な仕組みが分かる方がいれば、是非ご指摘をお待ちしております!
ちなみに、ここで使っているtransform-style: preserve-3d;
とは、オブジェクトに立体感を持たせるためのもので、
これが設定されていなかったり、transform-style: flat;
となったりしていると、立体的に重複しているオブジェクトでもそれを識別してもらえません。
設定する際は、オブジェクトの大枠に適用します。
今回だと、下記2箇所に適用しています。
-
.space
→ 子要素の惑星同士の立体の前後関係を表現するため -
.space__planet
→ 惑星本体に立体感を持たせるため
.space {
transform-style: preserve-3d;
&__planet {
transform-style: preserve-3d;
}
}
また、先ほど使っていたperspective-originとは、「立体的にしたときにどこからその要素を見るのか」を指定します。
視点の位置は座標で指定でき、今回はX座標が要素の5倍の距離で観測しているために、要素が90度まで回転しても直線にならずに見えているわけです。
参考: perspective-originのドキュメント @MDN
おまけのまとめ
立体感を出す方法について色々とやってみました。
整理すると下記のようになります。
項目 | 概要 |
---|---|
perspective | 適用した要素の塊は、z=0の平面を飛び出して立体感が出せるようになる |
perspective-origin | 視点の座標を指定 |
perserve-3d | 要素の重複を許容し、前後の立体感を出します |
だいぶ粗い説明になってしまいましたが、これでフワッとした理解はしていただけたのではないでしょうか。
ここまでお読みくださりありがとうございました!
(次は、短くてタメになる記事を書きたい...)