背景の中で何種類かのパターンを繰り返して表示する

背景の中で何種類かのパターンを繰り返して表示する

全然書かなくなるということになったので、たまたま業務でやったやつを残しておく。
表題がめちゃくちゃわかりにくいけど、要はこんな感じのやつ。

例えば500 * 1000のパターンを4種類繰り返して表示するみたいなこと。

実装

CSSではできないのでcanvasを背面に敷きましょう。

HTML & CSS

<div class="wrapper" id="js-wrapper">
 <canvas id="c"></canvas>
 <div class="contents">
  ここに内容を書く
 </div>
</div>
.wrapper {
  position: relative
}

canvas {
  position: absolute;
  top: 0;
  left: 0
}

.contents {
  position: relative
}

そういうわけでcanvasです。


const c = document.getElementById('c');
const ctx = c.getContext('2d');
const PATTERN_HEIGHT = 1000;

let w;
let h;

// 描画のサイズが知りたいので親要素も取得する。
const wrapper = document.getElementById('js-wrapper');

const init = () => {
  w = c.width = wrapper.offsetWidth;
  h = c.height = wrapper.offsetHeight;
}
init();

// パターン画像の種類
const PATTERN_1 = 'PATTERN_1';
const PATTERN_2 = 'PATTERN_2';
const PATTERN_3 = 'PATTERN_3';

// パターンのサイズを柔軟にしたいのでpatternObj.imgにはimgではなくcanvasを格納する
const patternSource = [
  {
    name: PATTERN_1,
    url: '/assets/img/pattern_1.png',
    img: document.createElement('canvas')
  },
  {
    name: PATTERN_2,
    url: '/assets/img/pattern_2.png',
    img: document.createElement('canvas')
  },
  {
    name: PATTERN_3,
    url: '/assets/img/pattern_3.png',
    img: document.createElement('canvas')
  },
];

// パターンの順番
const patternOrder = [
  PATTERN_2,
  PATTERN_3,
  PATTERN_2,
  PATTERN_1,
];

// イメージをロードする
const loadImage = patternObj => new Promise(resolve => {
  const img = new Image();
  img.src = patternObj.url;
  img.onload = () => {
    // canvasにimgを任意のサイズで描画する。
    const imgWidth = patternObj.img.width = img.width;
    const imgHeight = patternObj.img.height = img.height;
    const imgCtx = patternObj.img.getContext('2d');
    imgCtx.drawImage(img, 0, 0, imgWidth, imgHeight);
    resolve()
  }
});

// imageリソースを全て取得してからコールバックを実行
const loadImages = async (patternSource, callback) => {
  await Promise.all(
    patternSource.map(
      pattern => loadImage(pattern)
    )
  )

  callback();
};

const drawPattern = (
  ctx,
  startX, startY,
  width, height,
  maxY,
  patternSource,
  patternOrder
) => {
  let i = 0;
  let drawHeight = startY;
  while(drawHeight < maxY) {
    const source = patternSource.filter(source => source.name === patternOrder[i % patternOrder.length])[0].img;

    const pattern = ctx.createPattern(source, 'repeat');
    ctx.fillStyle = pattern;
    ctx.fillRect(startX, startY + height * i, width, height);
    i++;
    drawHeight += height;
  }
}

const draw = () => {
  init();
  ctx.clearRect(0, 0, w, h);
  // 左右ハーフステップで繰り返させる
  drawPattern(
    ctx,
    0, 0,
    w / 2, PATTERN_HEIGHT,
    h,
    patternSource,
    patternOrder
  )
  drawPattern(
    ctx,
    w / 2, -PATTERN_HEIGHT / 2,
    w / 2, PATTERN_HEIGHT,
    h,
    patternSource,
    patternOrder
  )
}

ctx.beginPath()
loadImages(patternSource, draw)

だるい

はい。

CSSの `pointer-events: none` が便利

CSSpointer-events: none が便利

早速書かなくなっているので、小さいことでも書いていこうという気持ちを見せてる💪

サイト作ったりしてて、ディレクターに確認出したりするとたまーに「ここなんかクリックできないんだけど」みたいなことを言われることある。ちょっとややこしいデザインのサイトとかだと割とありがち。
それを聞くと「はーん、レイヤーの順番問題だな」というふうになって、position: relativeかけたりz-indexいじったりすることになるわけなんだけど、pointer-events: noneを利用すれば解消される。

あるいはハンバーガーメニューかなんかで、クリックすると画面全体を覆うようなレイヤーが登場するような場合も使える。
opacity: 0だけだと、透明になってるだけなのでレイヤーがそこに存在し続けて、普通にクリックできる状態になってしまうため、今まではvisibility: hiddenで存在を消してやってたのだけど、アニメーションかける時にちょっとこのプロパティを気遣ってやる必要があって、まぁまぁめんどくさいなと思ってたら、それもpointer-events: noneでいける。

なんで今まで知らんかったんやろうという気持ち。

そういうわけでDEMOです。

See the Pen pointer event test by bom_phage (@bom_phage) on CodePen.

はい。

小ネタでした。

ちょいちょい触って感じたsin/cosの便利なところ

ちょいちょい触って感じたsin/cosの便利なところ

canvasとかその辺触るとだいたいsin/cosが出てきて、数学嫌いな僕みたいなのはそれだけで😇ってなりがち。だけどちょっと触ってみると全然複雑なものでもない。
ポイントは2点で

  1. sin/cosに同一の値(角度)を与え、x座表に起点+cos、y座標に起点+sinを与えると円を描く
  2. 必ず-1〜1までの値を返す(その値間を反復する)

1. 円を描く

1.については以下の記事に非常に分かりやすく載ってる。
単位円を用いた三角比の定義
正直結構理解するのに苦しんでたけど、これを読んでなるほど!!ってなった。

実際にコード(JavaScript)で示すと、

// この値をいじることでアニメーションなりなんなりさせる
let angle = 0;

// JSでは基本ラジアンという単位を使うので変換用
const radian = angle => angle * Math.PI / 180;

// 起点座標(300なのは適当)
const BASE_X = 300;
const BASE_Y = 300;

// 半径のサイズ
const SIZE_RADIUS = 200

// 起点座標を中心にしたx/y座標
let x = Math.cos(radian(angle)) * SIZE_RADIUS + BASE_X;
let y = Math.sin(radian(angle)) * SIZE_RADIUS + BASE_Y;

この座標を元に位置だったりを計算してやれば円形にものを配置したり、円形に動かしたりできる。
x座標を直線的な動きにするといわゆるsin curveになる。
それ以外にもsinやcosの値を重ねたりなんだりすると面白い動きになったりする。

See the Pen sin/cos by bom_phage (@bom_phage) on CodePen.

2. 反復する値を使う

実際円を描く機会というのもそんなにあるかと言われればないわけで(そもそもこういうtipsが役に立つ機会はない)、もう一方の-1から1の間の値を必ず返すという方が使い勝手がいい気がする。
しかも、何より素晴らしいのはsinとcosで同じ値を入れても別々の値を返してくれるということだ。要件にもよるけど、割と嬉しい。

そのまま使うのもいいし、正規化して使うとさらに使い良いこともある

-1 〜 1の範囲だとちょっと使いにくいよねという場合は0 〜 1正規化*wikipediaしてしまえばいい。
それはそんなに面倒なことでもなくコード(JavaScript)なら

const normalizeSin = angle => (Math.sin(radian(angle)) + 1) * 0.5;
const normalizeCos = angle => (Math.cos(radian(angle)) + 1) * 0.5;

0 〜 1の範囲を返すようにできる。

何に使う?

ある範囲を反復するので、繰り返すようなアニメーションの処理に使う。というかtransformのプロパティに使えば、なんかガチャガチャ動くものになるし、例えばopacityの値に使うとかhslの値に使うとか色々ある。
あと特徴としてはsin curveの形で増減するのでeasingにもつかえる。

というか色々数字いじると楽しいし、意外な形になるので触ってみて遊ぶのが一番良さそう😘

See the Pen sin/cos2 by bom_phage (@bom_phage) on CodePen.

はい。

そういうわけで、なんとなく書いておきました。

source要素のmedia属性について

source要素のmediaについて

ウェッブサイトを制作する上で、スマホ用とPC用で画像を切り替えたいみたいなことってかなりあると思う。画像はなんやかんやとファイルサイズ食うものでもあるし、PCの方ではガツンとでかいやつ使いたいけど、それさすがにスマホで読ませるのキツイよねって言う時とか、単純にそれぞれで最適化した画像を見せたいみたいなことはざらにある。

そういう時は僕は<picture>タグを使うことにしている。

それぞれの画像に対してmediaQueryで切り分けてdisplay: noneをかけるみたいなことでも実現できるけど、その場合、display: noneになってる画像のリソースも読みに行ってしまう。表示させないのに読み込ませるのはどうなの?みたいな気持ちがあるが、<picture>タグはその心配がないし、それであれば使わない理由もない。

ちなみに背景画像をmediaQueryで切り分けて読み込ませた場合は、読んでない方は読み込まれない。<picture>もコレと一緒。そういうわけでサンプルも用意してみた。開発者ツールのNetwork > imgで読んでるリソースの確認ができる。

See the Pen mediaQuery test by bom_phage (@bom_phage) on CodePen.

display: noneのものも読み込まれてるのがわかると思う。

本題​

ところで、このsourceタグのmedia要素だけど、書き方としてはCSSと一緒。僕はCSSのメディアクエリに関してはemで記述することにしていて、ほんならこの属性値もemでいったろかいって思ったけど、それがなかなかうまくいかなかった👶🏻
ちなみにメディアクエリをemで指定することに関しては下記の記事を読んで、そうかと思ったからだ 。
[CSS]Media Queriesで使う単位はpx, em, remのどれが適しているか検証 -px指定は注意が必要

とりあえずemで指定するとこんな感じ。768px未満でスマホ、以上でPCのイメージ。

<picture>
  <source media="(max-width:47.9375em)" srcset="img-sp.jpg">
  <source media="(min-width:48em)" srcset="img-pc.jpg">
  <img src="img-pc.jpg" alt=""><!-- IE用 -->
</picture>

全くダメというわけではない。なんなら普通にいけてるやんと思ってて気づかなかった。

ランドスケープの時に崩れるやんけ🧠🧠🧠

ランドスケープ(横向き)にした時、横幅は768未満のはずなのに、なぜか以上で読み込んで欲しい画像が読み込まれとる👴🏻
Safariだけでない、Chromeも、、、
この件について、明確な原因はわかってないし、調べ方がわかんなかった。エミュレータならいけるっぽい。
ランドスケープ時にはルートのフォントサイズが変わるってことなんだろうな、とか思ってた。だけど実はremは問題なく動く。これは一体、、、
まぁそういうわけで、とりあえずpxを使っとこうぜという話でした。

<picture>
  <source media="(max-width:767px)" srcset="img-sp.jpg">
  <source media="(min-width:768px)" srcset="img-pc.jpg">
  <img src="img-pc.jpg" alt=""><!-- IE用 -->
</picture>

はい。

2Dのメタボール(Metaball)理解した

2Dのメタボール(metaball)について理解した。

メタボールっつーのは何かと言うとこれです。写経したやつだけど。
(PCで見てくれよな!)

See the Pen metaball canvas by bom_phage (@bom_phage) on CodePen.

メタボールというか、こういう滑らかにくっつく表現やりたいなーと思っていて、調べてたけどどうにもよくわからないでいた。特によく出てくるのはSVG filter使うやつ。liquid みたいな言葉で検索するとよく出てくるけど、とにかくパフォーマンスが悪い。ほんでSVG filterに対する知識があんまりないので、???みたいな気持ちになってた。

ちょっと間忘れてたけど、思い出してふとやってみるかということになって写経したところ、ばっちり理解した。

原理

上述のcodepenのやつだと若干実装が違うんだけど、ポイントは

  • 不透明度の円形グラデーションを複数用意する(中心は不透明度が高く、外側に行くにつれて透明)
  • ある値(閾値)以下の不透明度の場合は不透明度を0にする(透明にする)

の2つ。これをやるとメタボールみたいに境界がぬるっとした感じの描画になる。
例えば、中心から外側にrgba(0, 0, 0, 1) => rgba(0, 0, 0, 0)というグラデを作って、描画時に不透明度0.95以下の部分は rgba(0, 0, 0, 0)にしちゃう感じ。 そうすると不透明度1 〜 0.95の部分だけの円ができる。 これが複数個集まると、重なる部分は不透明度が足し算されて、円と円の間の部分に不透明度0.95を超える部分が出てきて境界が滑らかなる。 => メタボール!という感じ:sun_with_face:

まぁそれはそうだがそれはどんな風に実装すんねんっていうのを下に書いとく。
実際触ると一目瞭然だと思うし、canvascssfilterで実装したやつを用意している。

実装

canvas

canvasの場合は、表示用と描画用の二種類のcanvasが必要になる。描画用の方にradialGradientでぼかした円を描画して、その内容を表示用のカンバスにコピーする。そのときに不透明度が閾値より低い部分は透明にすることで、メタボールが描画できる。
この実装ではTHRESHOLDという定数が閾値(初期値210※1)になってるのでここを10とかにしてもらうとボケた円がでてくる。
※1 canvasの場合透明度の範囲も0〜255

See the Pen metaball canvas2 by bom_phage (@bom_phage) on CodePen.

◆前述の実装との違い

実装見てもらったら話は早いけど、描画用のcanvasからgetImageDataで全ピクセルの情報を取得している。その時、ピクセルのデータは1pxずつrgbaという順番で配列に格納されている。
前述の実装の場合はコピー時にその全データに対して、閾値より低い場合に0を与えている。これだとrgb閾値以上でないといけなくなってしまう。それだと色に限りがでてしまって困る。
そういうわけでaの値のみ評価するようにしたのが後述の方。そっちのがforが回る回数も少なくて済むし、いろんな色がいけるぞ。

CSS filter

cssでもやってること一緒。円にblurをかけて、描画領域のcontrastを上げることで、不透明度の低い(コントラストが低い)部分を飛ばしている感じです。でもCSSだとcontrastをあげる都合上、パキパキしたのしかできないし、色の自由度が著しく低いと思う(解決方法がわからん)。
しかもcssfilterめっちゃ重い!!

See the Pen metaball by bom_phage (@bom_phage) on CodePen.


はい

そういうわけで実装するならcanvasでやりましょう!原理がわかれば無限に応用効くと思うし、いろいろできそうでワクワクするぞ!!
3Dも理解できるように頑張るぞ!

See the Pen metaball particles by bom_phage (@bom_phage) on CodePen.

PIXI.jsであそんでる

PIXI.jsであそんでる。

個人プロジェクトで使おうという意図もあるので、別に遊んでるわけではないけど、割と気に入って最近よく試している。何が気に入っているかと言われると、非常に簡単な点と、パフォーマンスがいいことかなー。

canvasでやっていき問題

僕としては2Dの表現をもっといろいろやりたいなーみたいなのがあって、canvasをやり始めた。端的に言うとパーティクルとか、画像をうねつかせるようなのとか、そういうのまずできるようになりたいな、みたいな。なので、ゲームを作りたいとかそういうのはあんまりない。今もそんなに興味はない。
ただ、キャラクターを動かすとかはちょっとやってみたい思いがあるけど、素材を用意するのが面倒だなというので尻込みしてる。

それで結局canvasgetContext('2d')して愚直にやってたんだけど、どうもパフォーマンスがあんまりだし、それ用のライブラリというものが世の中にはあって、触らないのもどうなんだとおもってCreate.jsとPIXI.jsを試してみた。

実際、書き方的には2Dを弄る分にはcanvasだろうとcreate.jsだろうとPIXI.jsだろうとあんまり変わらんなという印象。ただ、PIXIはデフォルトでWebGL使ってくれるのでパフォーマンスがいいし、カスタムフィルターみたいなのが用意されてて、できることも多い。

というか、canvasである程度アニメーションの書き方がわかってなかったら、どっちにせよで全然わかんなかっただろうなとも思う。

PIXI.js

そういうわけで今はPIXIにはまってちょっと触ってみてる。自分で作る時も次はPIXI入れるだろうなと思う。

パーティクル

See the Pen PARTICLE PIXI by bom_phage (@bom_phage) on CodePen.

パーティクルをマスクしたやつ

See the Pen PARTICLE PIXI LINES MASK by bom_phage (@bom_phage) on CodePen.

結局z軸問題がある

今の所z軸をいい感じに弄る方法がわかってないので、んーっていうところもある。three.jsももっと触ってみたいなと思うのと、シェーダーをちゃんとやりたいなと思う。

CSSであそぼう

CSSであそぼう

CSSであんまり使ってないというか、あんまり挙動理解してないのいろいろあったので遊んでみた。

transformとperspective

matrix3D とか正直触ってみたけど行列を理解する頭がまだないので、よくわからん。結局translateとかscaleとかrotateみたいな慣れ親しんだものたちを触るほうが直感的だし、理解がはやくていいなとなった。
perspectiveも何回かしか試したことなかったけど便利。

というか、DOMを弄るほうがcanvasの2dいじるよりパフォーマンスいい気がする。しかもcanvasだとz軸がないけれど、CSSにはあるのでそういう面でもすごく直感的。だけど、DOMを常に操作しているということと、場合によってはDOMを大量に挿入するなどすることを考えるとあんまり前向きになれない部分もある。

See the Pen CSS Play by bom_phage (@bom_phage) on CodePen.

See the Pen CSS Play2 by bom_phage (@bom_phage) on CodePen.

clip

clipとか使うこと全然なかった。未だにどういう時に使うのかわかってない。だけどtextもclipできるみたいなことがわかったので、それなら確かに使うような場面があるかもしらんな、と言う気持ちになったのでちょっとさわってみた。

See the Pen CSS play3 by bom_phage (@bom_phage) on CodePen.

んー、でもつかうかなー