![見出し画像](https://assets.st-note.com/production/uploads/images/53123554/rectangle_large_type_2_fbcceb5d8ff7f7de18d9fd51a8ebdc69.jpeg?width=1200)
ORIGAMI LETTER フロントエンドBreakdown
こんにちは、AQUARING かに です。
昨年制作したブラザー工業さまの「ORIGAMI LETTER」が今年4月にiFデザインアワード2021 コミュニケーションデザイン部門でゴールドを受賞しました!🎉
このWebサイトでは、Webサイト上で折り紙を折るDigital Origami Letterと、プリントした折り紙を折るPrinted Origami Letterの、2つ方法でメッセージを送ることができます。
今回はCanvas2DやWebGLなどのフロントエンド技術を駆使して制作したDigital Origami Letterの解説をしていきます。
↓送信ページ
↓受信ページ
textarea要素をCanvas要素にベイクしてWebGLテクスチャとして扱う
今回なんといっても一番大変だったのがこれです。
メッセージを書いた手紙がそのまま折り紙に変化するという演出の都合上、ユーザーが自由入力した<textarea>要素をスタイルが適用されたままWebGLのテクスチャにして、以降のWebGL演出へシームレスに遷移させる必要がありました。
SVG.foreignObjectによる実装
「DOM to Canvas」などのワードで検索するとよくヒットするのがSVGの<foreignObject>による実装ですが、この方法だと画像やフォントなどのリソースをBase64に変換して埋め込む必要があったり、IE11が対応していないため不採用になりました。
CanvasRenderingContext2Dによる実装
<textarea>だけの再現ならなんとかCanvas2Dでもいけるかもしれない!と思いCanvas2Dでの実装に切り替えました。
やりたいこととしては、以下の3点です。
・現在<textarea>に適用されているスタイルの取得
・取得したスタイルをCanvasに反映させる
・Canvas2Dでの行の折り返し位置の再現
現在<textarea>に適用されているスタイルの取得
getComputedStyle() にElementを渡すことで、その要素に適用されている算出済みのすべてのスタイルの値をオブジェクトで取得することができます。
今回は<textarea>要素と、それを囲んでいる<div>の余白も知りたかっため、それぞれの値を取得しました。(color, font, letter-spacing, line-height, paddingなど)
取得したスタイルをCanvasに反映させる
Canvas2Dでのフォント系の指定は、CSSのfontプロパティによる一括指定で行います。
(実装したときは styles['font-family'] などを個別に取得して一括指定の文字列を作っていましたが、今思うと styles['font'] で取得したものを ctx.font に代入するのが最速かもしれないです...)
要素の幅や高さに関係する部分は、getBoundingClientRect() でウィンドウ座標上の要素の矩形から取得します。
Canvas2Dでの行の折り返し位置の再現
Canvas2Dで文字を描画するには fillText() を使用しますが、文字列内に改行コードがあっても無視されて一行で描画されてしまうため、改行部分は自分で実装する必要があります。(maxWidthというぱっと見それっぽい引数も存在しますが、これはテキスト幅が超えたときに無理やり長体になるだけなので無関係です。いつ使うんだこれ...)
Canvas2Dには measureText() という、文字列が描画されるときのサイズが取得できる便利関数が存在するので、入力された文字列をひと塊で扱うのではなく1文字ずつに分割して、右に1文字ずつ足していったときに要素幅を超えたら次の行に進む、という方法で実装しました。(letter-spacingも、1文字ずつの幅を足すときに加算することで再現しています)
最初は文字列の分割を String.split('') でやっていたのですが、これだと絵文字が入力された際に意図せず2文字に分割されて文字化けしてしまいました。(サロゲートペアというらしいです)
そこで、以下の記事を参考にスプレッド演算子による分割に切り替えたらうまくいきました。
// NG
'ABCあいう🥺'.spit('')
> ["A", "B", "C", "あ", "い", "う", "�", "�"]
// OK
[...'ABCあいう🥺']
> ["A", "B", "C", "あ", "い", "う", "🥺"]
行の改行位置がわかれば、あとはもう1行ごとを配列にしてそれぞれの行を fillText() で y を line-height 分ずらして描画するだけなので簡単です。
↓CodeSandboxでデモをつくりました。
手紙をふにゃっとゆらいで遷移させる
折り紙のモチーフを選択したときと、送信ボタンを推したときに手紙がふにゃっとゆらいで遷移します。
この部分は、タテヨコ64分割した平面メッシュにテクスチャを貼ってGLSLの頂点シェーダーでメッシュの頂点座標をsin関数でオフセットすることでふにゃっとした動きを実装しています。
varying vec2 vUv;
uniform float uDistortionProgress;// 0.0 ~ 1.0
uniform float uDistortionStrength;
const float PI = 3.141592653589793238;
void main() {
vUv = uv;
vec3 pos = position;
float percent = sin(uDistortionProgress * PI);// 0.0 ~ 1.0 ~ 0.0
float strength = percent * uDistortionStrength;
pos.x += sin((-pos.y + 0.5) * PI * 2.0) * strength;
pos.y += sin((pos.x + 0.5) * PI * 2.0) * strength;
gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
}
ひとつのアニメーションのなかで「まっすぐ→ふにゃ→まっすぐ」の動きを作る必要があったため、進捗率を制御するuniform変数 uDistortionProgress を 0.0 → 1.0 に動かすと、シェーダー内で 0.0 → 1.0 → 0.0 に変換されるようにGLSLを書いています。
↓参考
折りシーンの解説
連番素材
折りシーンでは、プリレンダリングしておいた折り紙の連番画像素材と、リアルタイムに生成した手紙内側のメッシュを合成することで自然に見せています。
こちらがその連番素材のひとつです。
1フレーム分のRGB画像とアルファマスク画像を一枚にまとめた2800x1400サイズの画像になっています。
アルファマスクの輪郭がぼんやり光っているように見えますが、これはRGB側の黒い背景を薄くアルファマスクすることでドロップシャドウとして利用している部分です。
もともとは透過情報が扱えるPNG形式でやっていましたが、連番素材のため全体のファイルサイズが膨大になるのと、サイズ軽量化のためにpng圧縮をかけるとディザリングでレンダリングが汚くなってしまうため、全体のファイルサイズや圧縮時の画の綺麗さから、最終的にJPG形式で実装側でアルファマスクをかける方式を採用しました。
折りシーンではこの連番素材の上に、次に紹介する内側のメッシュを重ねて合成しています。
折り紙メッシュのポリゴン構造
こちらも、意外にも苦戦した実装です。
内側表示用のメッシュを折り込んだ時の隠面消去(右下の面も巻き込んで透過させて後ろにある連番表示用メッシュを見せる)が3D的な実装だと難しく、最終的に頂点のXYとUVのみを動かす形での実装になりました。
折り込んだときに真ん中の頂点ごと動かしていて、UV座標がそのままだと歪んでしまうため、UV座標を頂点座標に同期させることで、手紙自体は動いていないように見せています。(説明が難しい...)
また、連番素材側に含まれている折りの内側にかかっているシャドウも活かすために、リアルタイムに作っている内側のメッシュを乗算ブレンドで重ねて表示することで、連番素材と違和感なく合成しています。
↓CodeSandboxでデモをつくりました。
アラベスクトランジション
GLSLにグラデーションマスク画像と、アラベスク模様のstroke画像を渡し、トランジションの進捗率を制御するuniform変数をJS側からGSAPで操作しています。
↓この画像をGLSLで明るくしていくと、中心から外側に段階的に広がるマスクが作れます
↓模様のストロークは1ブロック分を別画像で渡して、GLSL側で上下左右にリピートさせています
↓参考
クラス内で任意の座標をトランジションの中心に設定する機能を付けておいたおかげで、Digital Origami Letterではボタンの位置から、それ以外では画面の中心から広がるといった汎用性のある実装ができました。
ランタイムパフォーマンス
WebGL演出を行うサイトで切っても切れないのがパフォーマンス問題です。
今回のサイトでは、常にWebGLオブジェクトが動いているシーンというのは少なく(Digitalの送信後と受信ページの最初だけ)、インタラクション発生時(折り紙を折っているとき)や、自動アニメーションが再生されているときのみレンダリングする方法を採用しました。
そのおかげで無駄にレンダリングループが回ることもなく、ストレスのないユーザー体験が実現できました。
フレームワークに頼らない擬似SPA実装
Digital Origami Letter ではURLハッシュに連動したシーン遷移を自前で実装しています。
onhashchange イベントに、ページ遷移に関わる全てのトリガーを依存させることで簡単にSPAが実装できます。
・アンカーリンクからの遷移
・ヒストリーバック / ネクスト
・URLへのダイレクトアクセス
・URLハッシュ値の手動書き換え
なぜSPAを自前実装したかというと、HTMLをCMSに登録する必要があり Vue.js などのフレームワークが使用できなかったためです。(使えないわけではないが相性が悪いので)
さいごに
アワード受賞に際して久しぶりにソースコードを見返したのですが、「こんなに複雑な実装よくできたな〜」と我ながらに思いました。
折り紙メッシュのポリゴン分割は正直に3D的な折り方にこだわっていたら絶対に思いつかないような実装になったので、それっぽく見せるごまかしスキルも大事なんだな、とこのプロジェクトを通して実感しました。