rfsでのサイズ指定を直感的にするsass function

画面サイズに応じてフォントサイズ（などのプロパティ）が変わるスタイルライブラリのrfs。
公式の言葉を借りれば、「CSS プロパティを様々なデバイスでレスポンシブルにスケールできる、Bootstrap のリサイジングエンジンです。」。
Bootstrapの一部ですが、単独のライブラリとして提供されているので、ときどき使っています。

https://github.com/twbs/rfs

こちらを使ってフォントサイズなどを指定するのですが、デフォルトではビューポートの幅が1200pxである前提で計算されるので、モバイル版でフォントサイズを指定するときに直感的に作業できなくて困っていました。これを、ビューポートの幅をもとに指定すべきサイズを計算して指定できるようなfunctionを作りました。（他にまっとうな方法があるのかもしれないが…）

まず完成形はこちらです。Dart Sassです。
rfsの各種設定を初期値のまま運用する前提で計算しています。小さいサイズについてはそのまま値を返すようにしています。

## 完成

```scss:scss
@use "sass:math";
@use "../node_modules/rfs/scss" as rfs;

@function rfs-font-size($size, $viewport, $breakpoint:1200) {
	$result: math.div($size, $size * 0 + 1);
	@if math.div($size, 1.25) > 16 {	
		$result: 10 * math.div($breakpoint * $size - 18 * $breakpoint + 18 * $viewport, $breakpoint + 9 * $viewport);
	}
    @return $result;
}

@media (min-width: 320px) {
    //意図したサイズにしたいビューポートの幅を2つめの引数に指定
    @include rfs.font-size(rfs-font-size(34px, 320));
}
```

### かんたんな解説
このfunctionで返った値をfont-sizeなどのmixinに渡すだけです。
rfsの初期値を変更している場合ですが、変更しそうな設定は以下のものかと想像します（変えたことはない）。

```scss:scss
// Base value
$rfs-base-value: 1.25rem !default;
$rfs-unit: rem !default;

// Breakpoint at where values start decreasing if screen width is smaller
$rfs-breakpoint: 1200px !default;
$rfs-breakpoint-unit: px !default;

// Factor of decrease
$rfs-factor: 10 !default;

// 1 rem = $rfs-rem-value px
$rfs-rem-value: 16 !default;
```

このうち、`$rfs-breakpoint`の値については変更に対応しています（その他の値の変更にも（たぶん）対応している[バージョン](#ほかの変数も指定できるバージョン20211105追記)も作りました）。
`$rfs-breakpoint` を変更した場合は、3つめの引数にその値を指定してください。

```scss:scss
@use "../node_modules/rfs/scss" as rfs with (
        $rfs-breakpoint: 768px
    );

@media (min-width: 320px) {
    @include rfs.font-size(rfs-font-size(34px, 320, 768));
}
```

### ほかの変数も指定できるバージョン（2021/11/05追記）

ほかの変数も指定できるものも作りました。

```scss:scss
@function rfs-font-size2($size, $viewport, $breakpoint:1200, $rem:16, $base:1.25, $factor:10) {
	$result: math.div($size, $size * 0 + 1);
	@if math.div($size, $base) > $rem {
		$rb: $rem * $base;
		$v1: $breakpoint * ($rb * (-$factor) + $rb + $factor * $size) + $viewport * $rb * ($factor - 1);
		$v2: $breakpoint + $viewport * ($factor - 1);
		$result: math.div($v1, $v2);
	}
	@return $result;
}
```


## rfsについて

rfsでは、各種サイズをこのように指定します。

```scss:scss
@use "../node_modules/rfs/scss" as rfs;

@media (min-width: 320px) {
    @include rfs.font-size(34px);
}
```

設定にもよりますが、基本的にはこのように、`rem`と`vw`を`calc`で足し合わせた値としてコンパイルされます。

```css:css
@media (min-width: 320px) {
    font-size: calc(1.3375rem + 1.05vw);
}
```

「ブレイクポイントを設定して、その中間ではフォントサイズをスケールさせたい」というようなときにどうも上手く設定できずに困っていました。

- デバイスの幅が767px以下では、フォントサイズを34pxに
- デバイスの幅が768px以上では、フォントサイズを75pxに

というような指定をしたい場合です。
何も考えずに以下のように指定しますが、意図した結果になりません。

```scss:scss
@use "../node_modules/rfs/scss" as rfs;


.my-article {
    @include rfs.font-size(34px);
    @media (min-width: 768px) {
       @include rfs.font-size(75px);
    }
}
```

これは、このようにコンパイルされます。

```css:css
.my-article {
    font-size: calc(1.3375rem + 1.05vw);
}
@media (min-width: 768px) {
    .my-article {
       font-size: calc(1.59375rem + 4.125vw);
    }
}
```

それぞれの実質的なフォントサイズは、ビューポートの幅によって決まるので、以下のようになります。

| 画面幅 | @media        | 指定サイズ | コンパイル結果       | 実質サイズ |
|---    |---            |---        |---                 |---       |
|320px  |―              |34px       |1.3375rem + 1.05vw  | 24.76px  |
|414px  |―              |〃          |〃                   | 25.747px |
|768px  |min-width:768px|75px       |1.59375rem + 4.125vw| 57.18px  |
|1024px |〃              |〃          |〃                   | 67.74px  |
|1200px |〃              |〃          |〃                   | 75px     |

この実質サイズはrfsのブレイクポイントの初期設定が1200pxであることから来ています。
vwの値を1200として計算すると、おおむね指定のフォントサイズになります（表の最終行）。


## functionの解説

rfsではだいたい以下のような計算でサイズ（最後の`$font-size`）が決まるようです。

```scss:scss
$size: 34; //指定したサイズ

$rem: 16;
$value: $rem * 1.25;
$factor: 10;
$breakpoint: 1200;
$min: $value + ($size - $value) / $factor;
$diff: $size - min;

$font-size: "calc(#{($min / $rem)}rem + #{$diff * 100 / $breakpoint})vw)";
```

calcの中の式も展開すると下記のようになります。
ビューポートの幅`$viewport`は表示される環境によって変わりますが、仮に320pxとしています。

```scss:scss
$size: 34;
$breakpoint: 1200;
$viewport: 320; //ビューポートの幅

$font-size: (20 + ($size - 20) / 10) + ($size - (20 + ($size - 20) / 10)) * 100 / $breakpoint * $viewport / 100;
```

このような計算になります。

これを方程式として、目的の`$size`となるような `$font-size`の値を求めます。
数学っぽく、変数を`x`、`y`定数を`a`、`b`として、以下のように書き換えると、このようになります。

```
x = (20 + (y - 20) / 10) + (y - (20 + (y - 20) / 10)) * 100 / a * b / 100
```

```math
x = (20 + \frac{y - 20}{10}) + (y - (20 + \frac{y - 20}{10})) × \frac{100}{a} × \frac{b}{100}
```

これを`y`について解きます。
計算はこちらのサイト（[Wolfram\|Alpha](https://ja.wolframalpha.com/input/?i=4x%2B3%3D19&lk=3)）でやりました。
いわゆるマジックナンバーだらけですが、よしとします。

```
y = 10 * (a * x - 18 * a + 18 * b) / (a + 9 * b)
```

```math
y = \frac{10(ax - 18a + 18b)}{a + 9b}
```

scss表記に戻すとこうなります。（除算のところは適宜math.div()で置き換えてください）

```scss
$font-size: 10 * ($breakpoint * $size - 18 * $breakpoint + 18 * $viewport) / ($breakpoint + 9 * $viewport);
```

## ほかの変数も指定できるバージョンの解説（2021/11/05追記）

`$rem * $base`を`c`、`$factor`を`d`として書き換えると、このようになります。

```
x = (c + (y - c) / d) + (y - (c + (y - c) / d)) * 100 / a * b / 100
```

```math
x = (c + \frac{y - c}{d}) + (y - (c + \frac{y - c}{d})) × \frac{100}{a} × \frac{b}{100}
```

これを`y`について解きます。計算は上記と同じサイトで。

```
y = (a * (c * (-d) + c + d * x) + b * c * (d - 1)) / (a + b * (d - 1))
```

```math
y = \frac{a (c (-d) + c + d x) + b c (d - 1)}{a + b (d - 1)}
```

scss表記に戻すとこうなります。（除算のところは適宜math.div()で置き換えてください）
`c`のところを`$rem * $base`にしています。鬼長いので、ちょっと分割しています。

```scss:scss
$rb: $rem * $base;
$v1: $breakpoint * ($rb * (-$factor) + $rb + $factor * $size) + $viewport * $rb * ($factor - 1);
$v2: $breakpoint + $viewport * ($factor - 1);
$font-size: $v1 / $v2;
```