まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

タグ:css

ウェブ画面でタイル状のブロックを縦方向にいくつか並べて表示する際に「1つ上のタイルにぶらさがる」感じを出したいことがままあります。

例えば並べるブロックの数が不定で、画面を一度表示した後からブロックを追加したくなったりする際に「1つ前のブロックの続きである」ことを明確にするため、1つ前のブロックにぶら下がる感じで表現したい、というケースです。更に具体的にはブロックチェーンのブロックを並べるようなケースなどで、こんな風に:
2020101300


この「ぶらさがってる」感じはブロックの左右に紐状(上図ではチェーン)の画像を表示することで目的に近いものが表現できます。これを CSS で実現するにはどうすればよいか、というのが今回のテーマです。なお今回はブロック部分を Bootstrap4 の card クラスを使って実現していますが、他の多くの環境でも同様に実現できる、はず。

まず紐となる画像を用意しました。今回用意したのは↓のチェーン状の画像で、サイズは 23x50 ピクセルです(画像の高さである 50 を前提に以下の CSS を記述します):
chain


また HTML 的には以下のようにしたいと思っています。今回は Bootstrap4 の card クラスが付与された <div> を複数並べるのですが、2つ目以降の <div> には mycard クラスも付与し、この mycard クラスの CSS で2つ目以降のブロックは1つ前のブロックにぶら下がっているように表現できることを目指します:
<div class="container" style="padding:80px 0;">
  <div class="card text-white bg-primary border-danger">
    <div class="card-body">
      <h4 class="card-title">Title #0</h4>
      <p class="card-text">Text for #0</p>
    </div>
  </div>

  <div class="card text-white bg-primary border-danger mycard">
    <div class="card-body">
      <h4 class="card-title">Title #1</h4>
      <p class="card-text">Text for #1</p>
    </div>
  </div>

  <div class="card text-white bg-primary border-danger mycard">
    <div class="card-body">
      <h4 class="card-title">Title #2</h4>
      <p class="card-text">Text for #2</p>
    </div>
  </div>


    :
</div>

で、この mycard クラスをどのように定義すればよいか? が今回の課題なのですが、こんな感じで実現してみました:
<style type="text/css">
.mycard{
  margin: 55px 0;
}
.mycard::before{
  content: url(./chain.png);
  margin: 0px;
  position: absolute;
  top: -52px;
  left: 10px;
}
.mycard::after{
  content: url(./chain.png);
  margin: 0px;
  position: absolute;
  top: -52px;
  right: 10px;
}
</style>

まず mycard クラスを付与するブロックは画像(./chain.png)の高さぶんだけの余幅を持って表示される必要があります(その余幅に画像が入ります)。今回、画像の高さが 50px なので、少し余裕をもたせて margin: 55px 0; というスタイルを指定しています(つまり実際には 55px ぶん下に表示されることになります)。

この空いた余幅に画像を含めたいのですが、今回は上述のように左端と右端の両方に画像を入れ、2箇所で引っかかっているようにぶら下げようとしています。そのため ::before 要素::after 要素両方を使って mycard クラスの左端と右端に画像を差し込みます。

まず ::before 要素ですが、content に目的の画像を含めます(content: url( './chain.png' );)。更に画像位置をブロックの左端に設定するため、position: absolute; top: -52px; left:10px; を指定しています。これによって 50px の高さをもつ画像を 52px ぶん上から表示させて空間の隙間に収まるようにしています(また左端ギリギリではなく、10px ぶんのマージンを取っています)。

::after 要素にも同様の設定を行いますが、こちらは右端に表示したいため right: 10px; と、ブロックの右側に 10px ぶんのマージンができるような位置を指定しています。これによってブロックの右側に画像を表示させるよう指定しています。

こうして出来上がったサンプルがこちらです:
https://dotnsf.github.io/hanging_css/

2020101301


一番上の #0 には mycard クラスがついていないので、パネルのブロックがそのまま表示されてます。#1 以下には全て mycard クラスを指定しているのでチェーンでぶらさがる効果が付与されています。

この方法ならいくつでも足せるし、JavaScript で後から動的に足すこともできるし、ブロックの要素に mycard クラスを1つ追加するだけで実現できるので簡単です。

上記ページのソースコードをまるごと参照していただき、chain.png を同じフォルダに置いてブラウザで開けばローカルでも見れると思います。


贅沢をいうと、現在はチェーンの画像サイズに合わせて CSS を書いて調整しているのですが、この部分をもう少しスマートにできたらいいなあ、と。


CSS だけで画像やブロック要素にぼかしを入れることができます。説明の前に、実際に試せるウェブページを作って公開したので、こちらを御覧ください:
https://dotnsf.github.io/blur_change/

アクセスすると以下のような画面が表示されます。これはある画像がかなり強めにぼかされて表示されています。この時点では「なんとなく人間?」くらいはわかるかもしれませんが、性別や表情、姿勢など、どのような画像が表示されているのか、まだわかりませんよね:
2020090701


画面下にスライダーバーがあり、これでぼかしのレベルを調整できます。最初はスライダーの一番右(最大ぼかし)に設定されていて、これを左に移動させることでぼかしを少なくすることができます:
2020090702


だんだん元の絵が見えてきます:
2020090703


スライダーを一番左まで移動させるとぼかしはゼロになり、元の絵が表示されます:
2020090704


なお元の絵はいらすとや様の「ブレザーとネクタイの女子学生のイラスト」を使わせていただきました:



このようなぼかしを実現するには、CSS で対象要素(今回の場合は <img> 要素)に以下のようなスタイルを指定します:
#img{
  -ms-filter: blur(100px);
  filter: blur(100px);
}

この blur 属性の指定でぼかしを実現しています。上例では blur(100px); という指定をしていますが、この 100 の部分の数字が大きいほど強いぼかしが入り、0 だとぼかしもゼロになって元の画像のままで表示されます。

上述のサンプルページでは、スライダーバーの動きに従ってスタイル指定を書き換えることで動的にぼかしのレベルが変わるような JavaScript を実装しています。具体的には jQuery を併用して以下のような HTML および JavaScript を実装しました:
<script>
$(function(){
  var t = $('#img')[0];
  t.style['-ms-filter'] = 'blur(100px)';
  t.style['filter'] = 'blur(100px)';
});

function changeBlur(){
  var b = $('#rng').val();
  var t = $('#img')[0];
  t.style['-ms-filter'] = 'blur(' + b + 'px)';
  t.style['filter'] = 'blur(' + b + 'px)';
}
</script>
</head>
<body>

<div class="container">
  <img id="img" src="https://1.bp.blogspot.com/-anFajTcy3CM/XxU0nhH2UyI/AAAAAAABaOA/Fovyifhdjvs8HRvHhNZrOWXEDcTN1_E5gCNcBGAsYHQ/s450/school_blazer_girl_necktie.png" width="80%"/>
  <br/>
  <input id="rng" type="range" value="100" min="0" max="100" step="5" oninput="changeBlur()" width="100%"/>
</div>

この例ではスライダーバーの値に合わせてぼかしのレベルを変えていますが、他に setTimeout を使ったり、クリックイベントハンドラを使うなどして少しずつぼかしを薄くする、といった使い方ができると思っています。


なお、JavaScript で動的にスタイルを書き換える手法については以前のブログエントリで解説しているので、こちらも参照ください:
JavaScript でスタイルを動的に書き換える






ウェブページ内の特定要素の見た目を JavaScript で動的に変更する方法を調べました。

一般的にウェブページ内の要素の見た目はスタイルシートで管理されます。例えばこのような感じ:
<head>
<style>
#container1{
  background-color: #fcc;
  height: 100px;
}
</style>
</head>
<body>

<div class="container" id="container1">
</div>

</body>

この時点では #container1 要素はピンク(#ffcccc)で塗りつぶされて表示されます:
2020082601


この部分の「背景色を動的に(JavaScript で)別の色に変えたい」場合、いくつかの方法が考えられます。一般的には背景色が指定された class を複数用意し、id ではなく class 指定で背景色を与えた上で、JavaScript で指定 class を切り替える(jQuery であれば removeClass + addClass)方法です。 ただこの方法は「事前に class を定義しておく」必要があります。切り替える瞬間の情報や属性値を参照して背景色を決めるとか、ランダム要素を使って背景色を決めるなど、事前に定義する内容が定まらないような場合では使えません。このような場合も含めて #container1 要素に定義された内容そのものを変更する(つまりスタイル定義内容そのものを動的に変更する)にはどのようにすればよいかを調べました。以下、jQuery を併用した場合で説明します。

例えば #container1 で定義されるエリアをクリックしたら背景色をランダムに変える、という場合であれば以下のような JavaScript で実現できます:
<script>
$(function(){
  $('#container1').on( 'click', function( e ){
    //. #container1 のスタイルシート定義を動的に変える。具体的には背景色を変更する
    var bgs = [ '#fcc', '#cfc', '#ccf', '#cff', '#fcf', '#ffc', '#ccc' ];
    var bg = bgs[Math.floor( Math.random() * bgs.length )];
    var t = $('#container1')[0];
    t.style['background-color'] = bg;
  });
});
</script>

スタイルシートの内容を変更したい要素をセレクターを指定して取り出し、その取得結果(配列)の最初の要素を取り出します。そして取り出した要素の style 属性がスタイルシートの内容なのでここを JavaScript で上書きするよう記述します(上例では t.style 内の 'background-color' 属性をランダムに上書きしています)。この方法であれば class を使うことなく、切り替えることもなく、スタイルシート定義を書き換えることで見た目の変更を実現できます。


実際に動くサンプルを用意しました:
https://dotnsf.github.io/dynamic-css/


上記 URL にアクセスすると以下のような画面が表示されます。画面上部に高さ 100px でピンク色の矩形が表示されています:
2020082602


この矩形部分をクリック(タップ)すると、7種類の中からランダムに選ばれた背景色に切り替わります:
2020082603


JavaScript でスタイルシートの内容を動的に書き換える方法のサンプルでした。class を事前に用意して指定し直す、というロジックが使えない場合でも実現可能な方法です。


CSS で画面内の特定要素に回転をかけることができる、ということを知り、その様子を視覚的に確認できるサービスを作って公開してみました。

この技術(というほどのものではないけど)は CSS の tranform 属性を使って、回転軸と回転角度を指定することで実現できます。ともあれまずは実際に挙動を確認してみましょう:
https://dotnsf.github.io/transform-rotate/


上記 URL にアクセスすると以下のような画面が表示されます。画像(デフォルトではいらすとやさんの「コンピュータを使うペンギン」のイラスト)と、その下に3つのスライダーバーが表示されているはずです:
2020020101


x のスライダーバーを動かすと、動かした角度だけ画面が縦方向に回転します:
2020020102


y のスライダーバーを動かすと、同様に横方向に回転します:
2020020103


z のスライダーバーは画面と平行に回転します:
2020020104


x, y, z のスライダーバーを同時に動かすこともできます。縦、横、平行すべて指定したぶんだけ回転して画像が表示される、ということがわかると思います:
2020020105


これらはすべて CSS の transform 属性だけで実現しています。以下、その技術的な内容を紹介しますが、前提として x, y, z という軸の名前とその意味、そして立体的な位置関係を理解しておく必要があるので、そちらから説明します。以下の図のような関係になっていると理解してください:
20200201


図のような x 軸、y 軸、z 軸があって、自分は z 軸正方面から画面を見ています。実際に見えている画面は x 軸と y 軸からなる平面上に描かれています。

そしてスライダーバーはそれぞれ x 軸を中心に回転、y 軸を中心に回転、z 軸を中心に回転するスライダーバーになっていて、それぞれ指定した角度だけ回転した結果が画面に表示されます。そのため(結果的には)x で回転すると縦方向、y で回転すると横方向に回転したように見えます。また z で回転すると見えている平面上で時計回り/反時計回りに回転しているように見せることができています。

で、これをどう実現しているかという部分が CSS の transform 属性です。例えば id="a" の要素を x 軸中心に 30 度回転させて表示したい場合は、以下のようなスタイルを適用します:
#a{
  transform: rotateX( 30deg );
}

同様にして y 軸中心に 30 度回転させて表示したい場合は以下です:
#a{
  transform: rotateY( 30deg );
}

z 軸中心に 30 度回転させる場合は以下です:
#a{
  transform: rotateZ( 30deg );
}

「30 度」という部分は -180 から 180 まで変えることができます。それぞれの結果は上述のようになります。上述のサービスではこれらのスタイルを各スライダーバーの値を参照しながら動的に変更して表示しており、その結果がこのようになっています。


これはオマケの情報ですが、このサービスではデフォルトでペンギンの画像を表示して回転させていますが、imgurl という画像の URL をパラメータ指定することで表示画像を変更することができます。例えばこの画像を使いたい場合は、画像の URL(https://1.bp.blogspot.com/-A-AUINeSdLY/XexqsbbnoVI/AAAAAAABWiA/1zwr87fQJbsntV_Ez_ky6-RJPRfgeCZ5ACNcBGAsYHQ/s400/kinshi_mark_computer.png)をパラメータとして以下のように指定します:

https://dotnsf.github.io/transform-rotate/?imgurl=https://1.bp.blogspot.com/-A-AUINeSdLY/XexqsbbnoVI/AAAAAAABWiA/1zwr87fQJbsntV_Ez_ky6-RJPRfgeCZ5ACNcBGAsYHQ/s400/kinshi_mark_computer.png


こんな感じで表示できるはずです:
2020020106




便利な地図表示ライブラリ Leaflet(と OpenStreetMap)を使って表示する地図は原則的には上が北になります。これを変更して(つまり地図を回転させて、上が北側にならないようにして)表示することに挑戦してみました。

最初は「やり方をググれば見つかるだろう」と軽く考えていたのですが、これはどうやらかなり難しいらしく、Stackoverflow で同様の質問を見つけましたが、ここでの回答には「Leaflet ネイティブで用意された方法は存在しない」「簡単な方法はない」とされています。かなりハードル高そうです。。

ネイティブには用意されていない、、簡単ではないものを自力で解決する、、 言うは簡単ですが実際には細かな調整含めて結構面倒なものでした。ただ一応サンプルとして動く形にできましたので、公開&紹介します。

まず、ソースコードはこちらに用意しました。実装は index.html ファイルを参照してください(バックエンドなしにこのファイル1つだけで実装しています):
https://github.com/dotnsf/om


実際に動く様子を確認・体験できるように github ページも用意しました。スマホのブラウザでこちらにアクセスしてください:
https://dotnsf.github.io/om/


なお、このページでは DeviceOrientation イベントをハンドリングしているため、環境によっては事前準備が必要です。こちらのページでも紹介しているように Android や iOS 12.1 以下の場合は事前設定不要です(ページを表示すればそのまま動きます)。 iOS 12.2 以上 13.0 未満の場合は URL 先にアクセスする前に予め Safari の設定で「モーションと画面の向きのアクセス」を ON にしておいてください(この設定後に URL にアクセスすれば動きます):
2020011001



また iOS 13.0 以上の場合は事前の準備は不要ですが、ページロード直後の初期状態で表示される「センサーの有効化」というボタンをクリックしてから「許可」してください:
2020011002


いずれの環境においても上記の準備を済ませた上でアクセスすると、東京を中心とした関東の地図が表示されます。また画面の中心には赤字でこのスマホデバイスの横方向への傾き角度が表示されます:
2020011701


スマホを横方向に傾けると、その傾きに合わせて地図が画面内で回転して表示されます。下図ではスマホを向かって右側に(時計回り方向に)傾けています。スマホの傾きに関係なく、常に真上(下図では左上方向)が北になります:
2020011700


実際に動かしている様子を動画にしました:



以下、技術的な解説を加えていきます。

まず CSS で body など地図を表示する要素のサイズを width も height も 100% に指定して、全画面で地図が表示されるようにしています。

そして回転処理そのものは地図を表示する <div> 要素に CSS の transform: rotate() 属性を適用して実現しています。今回の例では <div id="demoMap"> 内に Leaflet 地図を表示していますが、デバイスの傾き(角度: deg)を検知して、
 $('#demoMap').css( 'transform', 'rotate(-'+deg+'deg)' );
という処理を実行することで <div> ごと回転をかけて表示しています(加えて deg の数値を <div id="me"> 内で表示していますが、こちらは特別な処理をしていないため、詳細は省略します)。

ただこれだけだとうまくいきません。Leaflet の仕様なのか、CSS transform の仕様なのか「もともと表示されている要素が回転して表示される」らしく、縦 100% 横 100% の画面を回転させると、角度によっては表示されない部分が出来てしまうのでした:
2020011801



↑黒がスマホ本体(黒枠の中だけが実際に表示される)、左側は本体画面に地図が表示されている。スマホを回転させて表示すると、元の地図の矩形部分は表示されるが、表示しきれない部分(黄色の部分)が生じる。実際には黄色部分はグレーで表示されてしまいます。


この状況を回避するための工夫を加えています。具体的には <div id="demoMap"> のサイズを width, height ともに 200% に指定して実際のスマホ画面の4倍の面積のエリアをロードするようにしています(更に left と top を -50% にして、そのロードしたエリアの中心部分だけを表示するよう調整しています。こうすることで表示された画面の中心を軸に画面が自然に回転するようになります)。細かいことですが、この仕様に合わせて <div id="me"> の位置もこの変更に合わせて調整したり、(表示位置がズレないよう)画面にスクロールバーが出ないよう CSS で抑制したりしています:
2020011802


↑地図部分を実画面サイズよりも大きくすることで、回転させても表示不可エリアが生じないように調整しています。


こうした工夫によってなんとか実現できました。ただ地図上に書かれた文字が斜めになってしまうのはさすがにどうしようもないです。その辺りは地図 API 側でサポートしてくれるのを待つしかないかなあ。。

なお index.html 内の JavaScript 変数 lat(緯度), lng(経度)が地図の中心点座標、zoom がズームレベルです。これらの値を変えると表示するエリアや拡大縮小レベルを変えることができますので、自分の環境で挑戦してみたい方は是非挑戦してみてください。


このページのトップヘ