2020年7月22日水曜日

「(HTML) + CSS」のみを使って、今「リアルな電卓」を作ってみた

https://qiita.com/j5c8k6m8/items/4695ee12b35e9c14047e?utm_source=Qiita%E3%83%8B%E3%83%A5%E3%83%BC%E3%82%B9&utm_campaign=18a6ac0d31-Qiita_newsletter_420_07_22_2020&utm_medium=email&utm_term=0_e44feaa081-18a6ac0d31-33246877
シェアしました。

完成品

まずは、CodePenで完成品を紹介します。ボタンを押して計算を試してみてください。
※ スマホだと反応が悪い場合があります。

はじめに

はじめて、CSS カウンター の存在をはじめて知ったときは驚きました。
テーブルの行番号は (必要であればJavaScriptで動的に) HTMLに記載するしか方法がない と思っていたからです。
Qiitaにも、CSSカウンターのサンプルを載せた良記事があります。
ふと、CSSカウンターはどこまでできるのだろうか?という疑問が浮かびました。
インクリメント(≒演算)ができるのであれば、電卓も作れるのか?
ついでに、CSSの記事だからタピオカのようにリアルに作るべきだろう1
という軽い気持ちから、 Qiita夏祭り2020 イベントに乗っかって、
〇〇(言語)のみを使って、今△△(アプリ)を作るとしたら のテーマ記事として作成しました。

解説

以下のステップで電卓を実装していきました。
興味のある部分だけでも読んでください。
  1. 実現方式検討
  2. Pug/SCSSの利用
  3. 演算部分の作成
  4. 表示/非表示の制御
  5. スケルトンの作成
  6. リアルなスタイルの作成
  7. デジタル表示の作成
  8. アニメーションの追加
  9. 最終調整

1. 実現方式検討

CSSカウンターを用いて、足し算 と 引き算 を実現します。
CSSカウンターを用いることで、
 - counter-incrementプロパティ を使用し、HTML中の該当要素数を 数える
 - counter関数 を使用し、(そこまでの)HTML中の該当要素を 表示する
ことができます。
一桁の足し算であれば、実装は簡単です。
ラジオボタンで入力値を作成し、:checked 疑似クラス と 一般兄弟結合子 を使用し、
チェックされた後続のinputタグの要素数を数えるCSSカウンターで実現できます。
複数桁の足し算や引き算 の実現はどうすればよいでしょうか?
CSSカウンターの counter-incrementプロパティ には、
単純に個数を数えるだけでなく、 カウンタに加える値を指定 できます。
カウンターを10増加させる場合の指定
counter-increment: example-counter 10;
これを使えば、複数桁の足し算と引き算も、簡単に実現できます。
電卓を作るうえでは、上記に加えて下記のような工夫が必要ですが、
基本的にはこの応用で作成ができそうです。
  • 桁数や入力数を増やすと、HTMLやCSSの記述が膨大となる
  • 電卓は入力された数字の数に応じて、桁数が変わる
  • 入力状態に合わせた、表示の制御が必要
  • リアルな電卓にするためのCSS作成

2. Pug/SCSSの利用

桁数や入力数を増やすと、HTMLやCSSの記述が膨大となる課題の解決のため、
ループが使用できるPugとSCSSを利用します。
(Code PenでもPugとSCSSが利用できます。)
PugとSCSSはコンパイルすることでHTMLとCSSに変換されます。
なので、PugとSCSSを使ってもタイトルは嘘ではありません。
では早速、PugとSCSSの変数定義/ループ/分岐を利用して、
先ほどの足し算と引き算の例を、0~9文字の9桁で、10個まで演算可能に修正します。
(実際のPugやSCSSの書き方はコードを参照してください。)
なんということでしょう。
HTML: 1,122行 / CSS: 80行 のコードを
Pug: 14行 / SCSS: 18行 と 記載量が約1/40 となりました。
これで、電卓の演算部分のロジックやレイアウトに注力しやすくなりました。 

3. 演算部分の作成

では、実際に電卓の演算部分を作成しましょう。
先ほどまでの例では、入力された値(ラジオボタン)に対して、桁を固定していました。
SCSS抜粋
$pow: 1;
@for $digit from 1 through $DIGIT {
  .digit#{$digit}:checked ~ input.digit#{$digit} {
    counter-increment: total #{$pow};
  }
  $pow: $pow * 10;
}
電卓では、先頭桁から入力されるため、以下のように入力された値に応じて、
セレクタを上書きし、桁を変えます。
SCSS抜粋
@for $digit from 1 through $DIGIT {
  $pow: 1;
  @for $digit2 from $digit through $DIGIT {
    @if $digit == $digit2 {
      .digit#{$digit}:checked ~ input.digit#{$digit} {
        counter-increment: total $pow;
      }
    } @else {
      .digit#{$digit2}:checked ~ .digit#{$digit}:checked ~ input.digit#{$digit} {
        counter-increment: total $pow;
      }
    }
    $pow: $pow * 10;
  }
}
また、ディスプレイには合計値だけではなく、入力途中の値も入れる必要があります。
入力途中のカウンタを、入力値別に持つことはできますが、
カウンターはcounter-resetを使わなければ、兄弟要素に対して働くため、
bodyに対して、counter-resetを設定した全体用のカウンターと、
counter-resetを設定しない個別用のカウンターの2つを用意すれば実現できます。
また、カウンターを別に定義すると、counter-incrementのプロパティ自体を、
上書きしてしまい、期待通りに動作しないことに注意して下さい。
これは期待通りに動作しない
body {
  counter-reset: total;
}

省略

.digit#{$digit}:checked ~ input.digit#{$digit} {
    counter-increment: total $pow;
    counter-increment: value $pow;
}
プロパティの値に複数のカウンタの設定を入れること
body {
  counter-reset: total;
}

省略

.digit#{$digit}:checked ~ input.digit#{$digit} {
    counter-increment: total $pow value $pow;
}
先頭桁からの入力と、入力途中の値を表示させると、以下のようになります。

4. 表示/非表示の制御

電卓とするときには、入力されている状態に応じて表示/非表示を切り替える必要があります。
上記の例、入力値毎にdivで区切って親子構造を作成していました。
しかし、CSSセレクタでは親や前の要素の指定がむつかしいため、表示/非表示の制御を考えると、
親子構造ではなく、最上位に入力状態を表すinputを並べ、演算のinputも先頭に移動して
タグの構造を見直し、カウンタも入力値ごとに保持するように修正します。
input群の後ろに、inputと紐づけた入力用のlabel群を用意し、
状態に応じてラベルを非表示にするようなセレクタを記載します。
電卓の C (クリア)は、全体をformタグで囲い、 <input type="reset"/> で実現できます。
電卓の = は、以降の操作を C 以外不可とするため、labelを非表示にする必要がありますが、
既にセレクタが複雑となっているため、CSSの !important 規則を使うことで記載量を減らします。
電卓のボタンに相当する、ラベル、および C には cursor: pointer; プロパティを付与します。
CSSが少し複雑にはなりましたが、表示/非表示を含めると、以下のようになります。

5. スケルトンの作成

さて、演算、表示/非表示制御の部分ができたので、電卓の見た目の作成に入ります。
今回作成するものは、レスポンシブである必要もないため、絶対位置指定とします。
つまり、position: absolute;で各オフセットを指定します。
レイアウトを細かく調整できるように、必要なサイズに関わる情報をSCSSの変数で指定します。
displayで表示する数字は、background-color を指定して、
入力途中の数値を表示するときは、合計の上にかぶせるようにします。
input属性を display: none で隠してしまうと、
CSSカウンターでカウントされなくなってしまうため、ラジオボタンは裏に隠れるように配置します。
入力できていないボタンは、表示されない状態ですが、
次の「リアルなレイアウトの作成」で、表示されるようにします。

6. リアルなスタイルの作成

まずは、色、および、丸みから追加していきます。
携帯やスマホなど、実際の端末は、面取り加工等がされているため、
角々しさをとることで、リアルにしていきます。
イメージとしては、キーボードのような丸みですが、
まずは、定番のCSSで三角を作る方法1 のように、シンプルに単一のタグで、
borderの border-colorborder-widthborder-radiusを変えることで、
下記のように疑似的に表現します。
border-radius は各角個別に楕円で指定できること、
border-width との差分が内側の radius になるところがポイントです。
なお、SCSSの変数を用いて、border-radius の個別指定を行う際は、
以下のように記載すると、 / が変数の割り算になるので注意が必要です。
これは誤り(真ん中が割り算になってしまう)
border-radius: $LEFT $RIGHT $RIGHT $LEFT / $TOP $TOP $BOTTOM $BOTTOM;
こっちが正しい
border-radius: #{$LEFT} #{$RIGHT} #{$RIGHT} #{$LEFT} / #{$TOP} #{$TOP} #{$BOTTOM} #{$BOTTOM};
これを先ほどの、スケルトンに適用すると、下記のようになります。
懐かしい感じになりました。
それではここから、shadowなどを利用して、リアルさを出します。
box-shadowを1つだけ利用すると浮いている感じになってしまうので、
外側と合わせて、内側(inside)も指定することで、ボヤかすような感じにします。
ニューモーフィズム2 を作成するときのように、明るい影と、暗い影を使います。
box-shadow にカンマ区切りで4つ(暗い外側 / 暗い外側 / 明るい外側 / 明るい内側)のスタイルを指定します。
box-shadowは、border内には効果を作らないため、
borderの存在しないbox-shadow用の透明な要素を重ねて効果を付けます。
shadowのオプションは、感覚で 調整して、電卓に適用してみましょう。
shadowは別要素で作成するため、直接labelにスタイルを付けていましたが、
labelは透明にして、背景をボタンにするように修正します。
<input type="reset"></input> に対しても、
クリックしてもアウトラインが消えるようなスタイルも追加します。
input[type="reset"] {
    background-color: Transparent;
    border: none;
    outline:none;
}
また、背景色も 白 #fff だと、明るいshadowの効果が表れないため、
背景(bodyのbackground)に薄く色を付けるのに加えます。
光源が左上の想定のため、最前面にも右下に向かって濃くなるよう、
linear-gradient プロパティを使用して半透明のdivも加えます。
リアルにはなってきましたが、ただ、ぼやけているようにも見えるので、
光源の反射のような効果を入れてみます。
詳細は省きますが、borderに沿った要素を重ね、
linear-gradient と radial-gradientで反射を重ねます。
radial-gradient に 指定する位置(%) は (約0.7) を掛ければつながります。
以下は、重なっている要素がわかりやすいように、borderの色を付けた例です。
では、電卓に適用しましょう。
目線が、反射にむかうため、ぼやけている印象が少なくなったと思います。

7. デジタル表示の作成

液晶部分は、デジタル表示にしたいため、 けしかん様の 「DSEG」フォント を幅を
transform プロパティを使用して、電卓風に細くして、使用させていただきました。
「DSEG」フォントの使用
@font-face {
  font-family: "dseg7-classic-mini-bold-italic";
  src: url("https://cdn.jsdelivr.net/npm/dseg@0.45.1/fonts/DSEG7-Classic-MINI/DSEG7ClassicMini-BoldItalic.woff2")
    format("woff2");
}
.dseg {
  font-family: "dseg7-classic-mini-bold-italic";
  transform: scale(0.46, 1);
}
液晶部分の背景は、linear-gradient を少しずらして繰り返すことで、
ざらざら感を出します。
以下は、電卓の液晶背景色を5倍にした例です。
液晶部分を追加後は、下記の通りになります。

8. アニメーション追加

ボタンのアニメーションは、凹む動作のため、リアルなスタイルの作成で作成したエフェクトを、
0.2秒かけて、透明化させることで実現します。
押しても意味がないボタンは、カーソルホバー時に禁止カーソルとするため、
ダミーのラベルを一番下に配置し、マウス操作で違和感がないようにします。

9. 最終調整

さて、最終調整です。
電卓として、以下3点の挙動を追加します。
  • 入力中の + や - の表示、 = を押した後の表示がディスプレイ上わかるようにする。
  • 桁あふれ時の動作。ブラウザにもよるかもしれないが、CSSカウンターの最大値は 2,147,483,647 と想定される。CSSで数値の判定はできないため、8桁 * 10回の操作を最大とする。
  • 同時押しはブラウザで再現できないが 1379C が順に押された時の挙動を追加する。
冒頭のCodePenは、上記のコンパイル済みの HTML / CSS にしたものになります。

さいごに

「(HTML) + CSS」のみを使って、今「リアルな電卓」を作ってみた
というタイトルの割には、今(新しい技術)は使わなかった。(IE動作未確認だが、IEでも動きそう)
新しい技術を使えば、掛け算や割り算を実現する方法も見つかるかもしれない。
途中、心が折れそうになったが、イベントのおかげで、記事を書ききり、勢いで投稿する。
後半のソースが汚いのと、記事の文章がおかしいのは、今後見直すかもしれない。
j5c8k6m8
情報のモデリングと再帰とP2Pとポエムが好き 絵に描いた餅が嫌い 中堅SIer タグ安全なQiitaを実現したい https://qiita.com/j5c8k6m8/items/2359627ff8c3778d95f6

0 コメント:

コメントを投稿