要素を不規則に組み合わせたデザインのレスポンシブ対応のTips

[ドワンゴアドベントカレンダー](https://qiita.com/advent-calendar/2020/dwango)22日目の投稿です。

今年の6月にドワンゴの社員になり、エンジニアさんで埋められてるカレンダーの中、恐れながら記事を書かせていただきます。
私のいる部署は、社内プロダクトのキャンペーンLPのデザイン・コーディングをすることが多く、規模としては、1〜3ページくらいの小規模な静的ページです。
そこで、レスポンシブレイアウトを組む上でのTipを、実際にコードを書いて自分なりにまとめてみようと思いました。
今回は、メインビジュアルのような不規則なオブジェクトの組み合わせで作られたデザインの時にどうするかを考えてみます。

## 今回作るもの
今年の8月にリリースされた「[RPGアツマールギフト機能リリースキャンペーンLP](https://site.nicovideo.jp/atsumaru/campaign/gift-webmoney/)」のメインビジュアルを簡略化したものを題材にします。
自分がコーディングを担当したサイトで、ちょうどいいサンプルになりそうだったので…。

### PC
<img width="600" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47240/dff4515b-efa3-caed-12e4-691a2870de60.jpeg">

### SP
<img width="300" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47240/56f2b314-8f2b-188c-e557-607127285c93.jpeg">

### 要件
- デザインカンプは以下2種類
 - PC: 横幅1366px
 - SP: 横幅375px
- 基本は1024pxでSPに切り替え
- タブレットは、768px〜1024pxをブレイクポイントとする
- 1366pxより大きい画面サイズの時は、PCデザインカンプと同じ大きさで固定する
- 1025px〜1366pxは、PCデザインカンプの比率を保って拡大・縮小させる
- 768px〜1024pxは、SPデザインカンプの比率で画面幅768pxの時の大きさで固定する
- 320px〜767pxは、SPデザインカンプの比率を保って拡大・縮小させる
- 全てのデバイスにおいて横スクロールが出ないようにする

場合によってはタブレット幅のデザインカンプも作る場合もありますが、大抵はコーディング側で巻き取ることが多いのではないかと思います。
この要件で、多くのブラウザサイズでレイアウト崩れを起こさないようにするには、SCSSで専用の関数やmixinをいくつか用意しておくと便利です。

## 変数ファイルを作成
ここには、最低限のブレイクポイントの変数を記載しておきます。
ファイル名は `variables.scss` とします。

```
$pc-max-width: 1366;
$pc-min-width: 1025;
$tablet-max-width: 1024;
$tablet-min-width: 768;
$sp-max-width: 767;
$sp-mini-width: 360;
```

## メディアクエリをまとめたmixinを作成

メディアクエリのmixinを用意します。
最低限だと、下記4つで足りるかと思います。
ファイル名は `mixin.scss` とします。

- pc(1025px以上)
- tablet(768px以上1024px以下)
- sp(767px以下)
- iPhoneSEや一部のAndroid端末(360px以下)
- 任意のブレイクポイント

```
@mixin mq($media-width) {
  // pc(1025px以上)
  @if $media-width == pc {
    @media only screen and (min-width: $pc-min-width * 1px) {
      @content;
    }
  }
  // pc、tablet共通(768px以上)
  @else if $media-width == pcTablet {
    @media only screen and (min-width: $tablet-min-width * 1px) {
      @content;
    }
  }
  // tablet(768px以上1024px以下)
  @else if $media-width == tablet {
    @media only screen and (min-width: $tablet-min-width * 1px) and (max-width: $tablet-max-width * 1px) {
      @content;
    }
  }
  // tablet、sp共通(1024px以下)
  @else if $media-width == tabletSp {
    @media only screen and (max-width: $tablet-max-width * 1px) {
      @content;
    }
  }
  // sp(768px未満)
  @else if $media-width == sp {
    @media only screen and (max-width: $sp-max-width * 1px) {
      @content;
    }
  }
  // iPhoneSEや一部のAndroid端末(360px以下)
  @else if $media-width == spMini {
    @media only screen and (max-width: $sp-mini * 1px) {
      @content;
    }
  }
  // 任意のブレイクポイント
  @else {
    @media only screen and (max-width: $media-width * 1px) {
      @content;
    }
  }
}
```

## アスペクト比を固定するfunctionを作成

画像のように、画面幅に応じてアスペクト比を保ったまま拡大縮小させる必要がありますので、そのfunctionを用意します。
指定した画面幅を基準に、pxをvwに変換する式です。
vw関数は、PCレイアウト用、vw_spはSPレイアウト用です。
それぞれ引数を２つ用意し、第１引数は、オブジェクトの値を受け取ります。
width,height,paddingなどですね。
第２引数は、viewportの基準となる値を受け取ります。
PCデザインカンプの横幅が1366pxなので、vw関数の第2引数には初期値に1366と入れておきます。
vw_sp関数にも同様に、SPデザインカンプの横幅の数値を入れておきます。
ファイル名は `function.scss` とします。

```
@function vw($size, $viewport: 1366){
  $rate: 100 / $viewport;
  @return $rate * $size * 1vw;
}

@function vw_sp($size, $viewport: 375){
  $rate: 100 / $viewport;
  @return $rate * $size * 1vw;
}
```

## 特定のブレイクポイント内でサイズを固定するfunctionを作成
時には、拡大縮小せず、サイズを固定したい場面もあると思います。
タブレット(1024px以下)サイズでSPレイアウトに切り替わると、余白が大きくなったり、逆に上記のvw関数を使用していると、異常にデカくなってしまうケースがあります。
それを防ぐため、指定したブレイクポイント内で、viewportを基準にして、サイズを固定するfunctionを用意します。
vw_fix関数は、PCレイアウト用、vw_fix_spはSPレイアウト用です。今回使用するのはSP用のみになります。
`$viewport` の初期値には、vw関数と同じものを入れておきます。
`$breakpoint` の初期値は、よくサイズを固定するブレイクポイントの数値を入れておきます。
SPは、タブレット用ブレイクポイント（1024〜768px）の下限768pxに設定しておきます。
`function.scss` に追加しておきます。

```
@function vw_fix($size, $viewport: 1366, $breakpoint: 1366){
  @return $size * $breakpoint / $viewport * 1px;
}

@function vw_fix_sp($size, $viewport: 375, $breakpoint: 768){
  @return $size * $breakpoint / $viewport * 1px;
}
```

## 実際に組んでみる

では実際にコードを書いていきましょう。

### HTML

pictureタグで、ブレイクポイント1024pxでPC、SPの画像が切り替わるよう設計しておきます。
コメントアウトで、画像の説明を入れておきました（社内案件のため、codepenなどで作れず…すみません）

```
<div class="mv">
    <!-- ギフト機能リリースの画像 -->
    <h1 class="mv-gift-release">
        <picture>
            <source srcset="./assets/images/mv-gift-release-sp.png" media="(max-width: 1024px)">
            <img src="./assets/images/mv-gift-release.png" alt="ギフト機能リリース！">
        </picture>
    </h1>
    <!-- 左端の男の子の画像 -->
    <div class="mv-chara-rutiano">
        <picture>
            <source srcset="./assets/images/mv-chara-rutiano-sp.png" media="(max-width: 1024px)">
            <img src="./assets/images/mv-chara-rutiano.png" alt="ルチアーノ画像">
        </picture>
    </div>
    <!-- 左から2番目の女の子の画像 -->
    <div class="mv-chara-kimigashine">
        <picture>
            <source srcset="./assets/images/mv-chara-kimigashine-sp.png" media="(max-width: 1024px)">
            <img src="./assets/images/mv-chara-kimigashine.png" alt="キミガシネ画像">
        </picture>
    </div>
    <!-- 右にいる女の子5人グループ -->
    <div class="mv-vtuber">
        <picture>
            <source srcset="./assets/images/mv-vtuber-sp.png" media="(max-width: 1024px)">
            <img src="./assets/images/mv-vtuber.png" alt="VTuber画像">
        </picture>
    </div>
</div>
```

### SCSS

#### 基準の位置の考え方
今回全てposition: absoluteを使って、top、leftで位置を指定しているので、画面を拡大・縮小した時要素の横軸の位置の基準がずれないようにするには、中央を起点とする必要があります。
なので、まずは下記スタイルを基準に考えます。

```
left: 50%;
transform: translateX(-50%);
```

ここから、calcを使用して、左右にずらします。

```
left: calc(50% - 540px);
```

#### calcとSCSS関数や変数の組み合わせ方

calcとvw()関数組み合わせたい場合は下記のように記載します。
`540px` が `#{vw(540)}` に変わっただけですね。

```
left: calc(50% - #{vw(540)});
```

#### レスポンシブ対応の雛形
あとは、今まで作ったmixinやfunctionを各メディアクエリに当てはめていけば完成です。

```
.mv-gift-release {
  /*
   * 全ブレイクポイントで共通して使用するスタイルと
   * 画面幅1366px以上の時のスタイルを記載する
   */

  @include mq(pc) {
    /*
     * 画面幅1025px以上の時のスタイルを記載する
     * vw()を使い、viewportが1366pxの時の見た目を基準に拡大縮小する
     */
  }
  @include mq(tablet) {
    /*
     * 画面幅768px以上1025px未満の時のスタイルを記載する
     * vw_fix_sp()を使い、viewportが375pxで、画面幅を768pxにした時の見た目のサイズで固定する
     */
  }
  @include mq(sp) {
    /*
     * 画面幅768px未満の時のスタイルを記載する
     * vw_sp()を使い、viewportが375pxの時の見た目を基準に拡大縮小する
     */
  }
}
```

#### 作成したfunctionの使い方

vw()やvw_sp()などをどう使うかですが、デザインカンプの数値をそのまま入れることができます。
これらの関数は、デザインカンプのアートボードサイズを基準に設計されたものなので、デザインカンプのwidth,heightの数値はそのまま使用することができます。キャプチャはfigmaのものになります。

**PC**

<img width="600" alt="貼り付けた画像_2020_12_21_20_29.png"  src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47240/a2414bec-87d0-49a5-be0c-8a2447f91dc8.png">

**SP**

<img width="600" alt="貼り付けた画像_2020_12_21_20_47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47240/309c4812-f08c-6e65-91d7-cc753632f1b7.png">

「ギフト機能リリース！」のwidthだけ書くと、下記のようになります。
topの値は、[Mapture](https://anatoo.jp/mapture/)などを使用して、デザインカンプと同じ位置に来る値を探す必要があります。

```
.mv-gift-release {
  width: 520px;
  @include mq(pc) {
    width: vw(520);
  }
  @include mq(tablet) {
    width: vw_fix_sp(340);
  }
  @include mq(sp) {
    width: vw-sp(340);
  }
}
```

#### SCSS完成

以上のルールを組み合わせて完成したSCSSがこちらです。

```
.mv {
  position: relative;
  overflow: hidden;
  height: 760px;
  @include mq(pc) {
    height: vw(760);
  }
  @include mq(tablet) {
    height: vw_fix_sp(680);
  }
  @include mq(sp) {
    height: vw_sp(680);
  }
  .mv-gift-release {
    position: absolute;
    top: 238px;
    left: 50%;
    z-index: 1;
    width: 530px;
    transform: translateX(-50%);
    @include mq(pc) {
      top: vw(238);
      width: vw(530);
    }
    @include mq(tablet) {
      top: vw_fix_sp(150);
      width: vw_fix_sp(340);
    }
    @include mq(sp) {
      top: vw-sp(150);
      width: vw-sp(340);
    }
  }
  .mv-chara-rutiano {
    position: absolute;
    top: 77px;
    left: calc(50% - 540px);
    width: 380px;
    transform: translateX(-50%);
    @include mq(pc) {
      top: vw(77);
      left: calc(50% - #{vw(540)});
      width: vw(380);
    }
    @include mq(tablet) {
      top: vw_fix_sp(27);
      left: calc(50% - #{vw_fix_sp(75)});
      width: vw_fix_sp(102);
    }
    @include mq(sp) {
      top: vw-sp(27);
      left: calc(50% - #{vw-sp(75)});
      width: vw-sp(102);
    }
  }
  .mv-chara-kimigashine {
    position: absolute;
    top: 37px;
    left: calc(50% - 380px);
    width: 507px;
    transform: translateX(-50%);
    @include mq(pc) {
      top: vw(37);
      left: calc(50% - #{vw(380)});
      width: vw(507);
    }
    @include mq(tablet) {
      top: vw_fix_sp(27);
      left: calc(50% + #{vw_fix_sp(83)});
      width: vw_fix_sp(102);
    }
    @include mq(sp) {
      top: vw-sp(27);
      left: calc(50% + #{vw-sp(83)});
      width: vw-sp(102);
    }
  }
  .mv-vtuber {
    position: absolute;
    top: 8px;
    left: calc(50% + 480px);
    width: 495px;
    transform: translateX(-50%);
    @include mq(pc) {
      top: vw(8);
      left: calc(50% + #{vw(480)});
      width: vw(495);
    }
    @include mq(tablet) {
      top: vw_fix_sp(270);
      left: calc(50% - #{vw_fix_sp(10)});
      width: vw_fix_sp(543);
    }
    @include mq(sp) {
      top: vw_sp(270);
      left: calc(50% - #{vw_sp(10)});
      width: vw_sp(543);
    }
  }
}
```

## 実際の動き

![ani.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/47240/ccf28ff3-7943-4940-c630-eccf8e695c83.gif)

少し見辛いですが、 `768px ~ 1024px` ではSPデザインカンプの比率で一定のサイズで固定、 `1366px以上` PCデザインカンプの比率で一定のサイズで固定で、それ以外ではPC/SPそれぞれの比率を保って拡大縮小しているのがわかるかと思います。
もちろん横スクロールは出ておりません。

## おまけ
最近はこの手法でコーディングすることが多いですが、metaタグのviewportをjsで操作して、ページ全体を比率を保って拡大縮小するやり方もあります。
こちらの記事をみるのが理解が早いと思います。
https://liginc.co.jp/451892

このライブラリを使用すると、たった数行で、ページ全体の比率固定をコントロールすることができます。
たとえば、デザインが375pxで作られていて、320px端末のために要所要所でブレイクポイントを切ってスタイル調整する時間がない時などに重宝します。
uaと組み合わせれば、端末ごとにviewportを切り替えることもできます。

```
<script>
 var ua = navigator.userAgent
 var sp = (ua.indexOf('iPhone') > 0 || ua.indexOf('Android') > 0 && ua.indexOf('Mobile') > 0)
 var tab = (ua.indexOf('iPad') > 0 || (!sp && ua.indexOf('Android') > 0))
 new ViewportExtra(tab ? 1280 : 375)
</script>
```

## まとめ
いかがでしたでしょうか？
今まで感覚でレスポンシブ対応していたこともあり、自分なりにいつかまとめてみたいと思っていたので、この場を借りて書くことにしました。
他にもベストなやり方があると思うので、是非教えていただけたら幸いです。
また、今回紹介したのはあくまで手法の1つであり、デザインによってどの見せ方が正しいかは変わります。
ユーザーにとって良い体験を提供できるよう、これからも精進していこうと思います。