サービスがクローズしたjsdoitのコードをgithub-pagesに移行するまで
1年半ほど前にクローズになった、jsdoitというソースコード共有サービスがあります。
サービスが稼働している時は500以上のコードを上げていて、サービスクローズ前に全てのコードをローカルに落として個人のgoogle driveに保管していたのですが、パッと見返したいコードなどがあるため、git管理をしてgithub-pagesで見られるように移行しました。
各ソースコードのOGP対応もさせてみました。
いざ作業に取り掛かると、知らなかったツール・使ってみたいと思っていたツールに触れることができたのでよい機会でした。
せっかくなので、移行までの簡単なフローと、便利だと思ったツールを書いていこうと思います。
jsdoitのファイル群
jsdoitからソースをダウンロードするとhtml,js,cssのファイルが格納されています。 ただ、画像や動画、音声などのアセットは含まれていませんでした。
移行フロー
大まかな流れはこちらです。全てnodeのスクリプトを書いています。
https://github.com/takumifukasawa/jsdoitArchives/tree/master/batches
1. ソースコードのパース・変換 - 画像など切れているリンクの置き換え - opg用のmetaタグ挿入 2. サムネイル生成 - アーカイブ一覧ページのhtml用の画像(465x465) - ogp画像(1200x630) 3. アーカイブ一覧ページのhtml生成
使用ツール・サービス
unpkg
リンクの置き換えに関して、jsdoitのサーバーにアップされていたライブラリのリンクは全て置き換えていく必要があるので、 可能な限り洗い出し、正規表現で検知・置き換えをしていきました。
ライブラリに関しては、動くデモを作るために可能な限りコードを書いた当時のバージョンを使っていきたい意図がありました。 主要ライブラリだと何かしらのcdnに上がっているのですが、ライブラリに依存するライブラリは見つからない場合があります。 threejsのOrbitControlsはまさにそのパターンです。バージョンが異なるとエラーが出て動かない場合があります。
unpkgはnpmのバージョンごとにコンテンツを配信してくれているサービスです。 threejsや関連モジュールはunpkgを参照するようにリンクを変換し、各ライブラリのバージョンを揃えることができました。
node-html-parser
ダウンロードしたhtmlの中にはogp向けのmetaタグは入っていなかったので、ogp用のmetaタグは全て新規追加していく必要がありました。
htmlそのものはテキストファイルなので、htmlファイルを読み込んでもdocument.appendChildなどのnodeベースで要素を追加していくことができないため、どう変換するのがよいか考えながらツールを探した結果こちらが便利でした。
htmlの中身をパースしてquerySelectorを用い要素にアクセスすることができるインターフェースになっている & 文字列への変換メソッドが生えている点が便利で、以下のような形でogp用のmetaタグを追加していきました。
import { parse } from 'node-html-parser'; ... // htmlファイルを読み込む const htmlContent = fs.readFileSync( ... ); // htmlファイルの内容をパースし、head要素を取得 const root = parse(htmlContent); const head = root.querySelector("head"); // headの中身を文字列にしてキャッシュ const headText = head.toString(); // ogp向けのmetaを後続に追加 headText += "<meta ... />"; ... // headの中身を置き換え head.set_content(haadText); // htmlファイルを書き込み fs.writeFileSync( ... );
puppeteer
画像キャプチャはheadlessブラウザのpuppeteerを使いました。
公式のサンプルを引用させていただくと以下のようなコードのみで画像キャプチャをしてくれます。 headlessブラウザを使ったことがなかったのでよい機会になりました。
const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); await page.goto('https://example.com'); await page.screenshot({ path: 'example.png' }); await browser.close(); })();
余談
playwrightも流行っているみたいです。
9割方は、ひとまず動くところまで機械的に変換することができました。 残りは手動になりそうですが、見つけ次第整えていきたいと思います。
【Javascript】Promiseを直列実行
内容は短めですが、よく使うのでそのメモになります。
複雑なアニメーションなど、とあるPromiseがresolveになったら違うPromiseの状態の監視を始めたい場合が頻繁にあります。
thenで繋いでもよいのですが、Promiseが増えるごとにthenの記述も合わせて増えていくため少し冗長です。
そこで以下のような関数を用意し、Promiseを返す関数の配列をreduceして繋いであげることで、記述量を減らすようにしました。
rejectな場合への対応がいらない場合に、順番に実行していきたい時に多用しています。
async function execPromiseInSequence (arr) { return arr.reduce( (chained, func) => chained.then(func), Promise.resolve() ); }; // ex async function main() { // 上から順に実行される await execPromiseInSequence([ async () => { ... }, async () => { ... } ... ]); }
【firebase】functions x hosting 環境でBasic認証をかけている時のBasic認証の挙動確認
firebase functions と firebase hosting を併用しているプロジェクトで Basic認証をかけている時の Basic認証の対象の挙動が分からなかったので確認しました。
firebase で Basic認証をかける
firebase で Basic 認証をかけるためには functions を使用する必要があります。
このようなフォルダ構成の時、
- functions // functionsにアップロードされる対象 - index.js - static - index.html - public // hostingにアップロードされる対象 - sample.png - firebase.json
以下のようなfunctionsでBasic認証をかけることができます。
import * as functions from "firebase-functions"; import express from "express"; import path from "path"; const basicAuth = require("basic-auth-connect"); //--------------------------------------------------------------------------------------------- // constants //--------------------------------------------------------------------------------------------- const USE_BASIC_AUTH = true; const AUTH_USER = "user"; const AUTH_PASSWORD = "pass"; //--------------------------------------------------------------------------------------------- // main //--------------------------------------------------------------------------------------------- const server = express(); // basic認証 if (USE_BASIC_AUTH) { server.use(basicAuth(AUTH_USER, AUTH_PASSWORD)); } server.use(express.static(path.join(`${__dirname}/static/`))); export const hostingApp = functions.https.onRequest(server);
firebase.jsonはこのようになります。
{ "functions": { "source": "./functions" }, "hosting": { "public": "./public", "rewrites": [ { "source": "**", "function": "hostingApp" } ], ... } }
hosting にアップされたファイルにはBasic認証がかからない
hosting の rewrite ルールの優先度はドキュメントにこのように書いてあります。
https://firebase.google.com/docs/hosting/functions?hl=ja#use_a_web_framework
注: public ディレクトリ内の静的ファイルは rewrite よりも優先されるため、静的ファイルは Cloud Functions エンドポイントと一緒に提供されます。
つまり、hosting は functions でかけているBasic認証の対象にならないので、先ほどのフォルダ構成とBasic認証がかかるかかからないかの関係性はこのようになります。
- functions // functionsにアップロードされる対象 - index.js - static - index.html <- Basic認証がかかる - public // hostingの対象 - sample.png <- Basic認証がかからない
逆にいうと、基本的にはBasic認証をかけたいが、とあるファイルだけBasic認証の対象から外したい、という場合には rewrite のルールを書かずに hostingにアップロードするのが良いのかもしれません。
【WebGL】three.js : soft particle
この投稿の内容は会社のアドベントカレンダーにて書いた記事と同じものになっています。
ですが、自分のブログにも同じ内容を残しておきたいので移植させていただいています。
three.js で soft particle (ソフトパーティクル)を実装する方法を紹介していきたいと思います。
ソフトパーティクルを使って、霧のような雰囲気の中をキツネが駆けるシーンを作ってみました。
この動作デモはこちらから、動作デモのソースはこちらのリポジトリからご覧いただくことができます。
ソフトパーティクルについて
ソフトパーティクルとは
おおまかに一言でまとめると「深度値の比較をして透過率・色を調整する」方法のことです。
特徴
まずはこちらの画像を比較してみます。
1枚目はソフトパーティクルを無効にしたもの、2枚目が有効にしたものです。 煙に着目すると1枚目は床(地面)との境界に線が入っており、2枚目は床に馴染んでいるという違いがあります。
煙は、霧感を出すためにキツネが走っている床のあたりに板ポリのビルボードにテクスチャを貼って散りばめています。ほとんどの煙の板ポリは床に突き抜けているような状態 = 板ポリと床は交差している状態にあるのですが、そのままだと1枚目の画像のように床の境界で煙のテクスチャが切れてしまうので不自然な見た目になります。
そこで、「他のオブジェクトとの重なりの境界を馴染ませる方法」としてソフトパーティクルを使います。
実装方針
カメラから見て床と煙の境界付近のピクセルに着目すると、カメラから床・煙それぞれへの距離はかなり近くなります。完全な境界の場合は、それぞれの距離はほぼ同じになっているはずです。この、距離が近い = 深度値が近いことを利用していきます。
まずは床を描画して床の深度値を取得します。その後、煙を描画する際に深度値を計算します。 その際、煙のピクセルシェーダー内で、床の深度値と煙の深度値を比較して近ければ近いほど透過するような処理をつけます。
すると、煙が描画されるピクセルのうち、床に遠い部分から近い部分にフェードアウトするような状態になります。
↓ 赤い枠線の範囲が板ポリゴンの床より上に見えている範囲だとしたら、
↓ 赤が濃い部分(床に近い部分)ほど透過をかけるようなイメージ
このように、深度値の比較をして透過率・色を調整することで他のオブジェクトとの重なりを馴染ませることができます。
ソフト「パーティクル」と呼ばれていますが、この後のデモのように、パーティクルのようなたくさん物量をばらまくようなオブジェクトでなくとも同じ見た目を表現することももちろん可能です。
three.jsでの実装
簡易的なデモをこちらに用意しました。重なっている部分にフェードがかかっていることが分かるかなと思います。
赤い箱は赤単色を出力するマテリアルを、白い箱はソフトパーティクルのシェーダーを適用したマテリアルを割り当てています。マテリアルは RawShaderMaterial を使っています。
See the Pen [test] three.js : soft particle by takumifukasawa (@takumifukasawa) on CodePen.
白い箱以外のオブジェクトも試してみました。
↓ ビルボードの白い板ポリ
↓ 煙のテクスチャを割り当てた板ポリのビルボード: ソフトパーティクル無効
↓ 煙のテクスチャを割り当てた板ポリのビルボード: ソフトパーティクル有効
JavaScript
requestAnimationFrame で毎フレーム実行される関数の中身を抜粋してコメントをつけてみます。
// RenderTarget の横幅縦幅は別途更新 const renderTarget = new THREE.WebGLRenderTarget(1, 1); renderTarget.texture.format = THREE.RGBAFormat; renderTarget.texture.minFilter = THREE.NearestFilter; renderTarget.texture.magFilter = THREE.NearestFilter; renderTarget.texture.generateMipmaps = false; renderTarget.stencilBuffer = false; renderTarget.depthBuffer = true; renderTarget.depthTexture = new THREE.DepthTexture(); renderTarget.depthTexture.type = THREE.UnsignedShortType; renderTarget.depthTexture.format = THREE.DepthFormat; ... const tick = () => { ... const ctx = renderer.getContext(); // ---------------------------------------------------------------------------- // 1. ソフトパーティクル以外(= 赤い箱)の深度値を深度テクスチャに描画 // ---------------------------------------------------------------------------- // ソフトパーティクルのメッシュを非表示 softParticleMesh.visible = false; // 深度値を描画する renderTarget を割り当て renderer.setRenderTarget(renderTarget); // depth だけ renderTarget に描画するため、色情報は描画しないようにマスク ctx.colorMask(false, false, false, false); renderer.render(scene, camera); // ---------------------------------------------------------------------------- // 2. ソフトパーティクルを含め全てのオブジェクトを描画 // ---------------------------------------------------------------------------- // 画面に表示させるために renderTarget の指定を解除 renderer.setRenderTarget(null); // ソフトパーティクルのメッシュを表示 softParticleMesh.visible = true; // 深度値比較用の各種 uniform 値をシェーダーに渡す softParticleMesh.material.uniforms.tDepth.value = renderTarget.depthTexture; softParticleMesh.material.uniforms.cameraNear.value = camera.near; softParticleMesh.material.uniforms.cameraFar.value = camera.far; softParticleMesh.material.uniforms.depthFade.value = params.depthFade; softParticleMesh.material.uniforms.resolution.value = new THREE.Vector2( width * ratio, height * ratio ); // 色情報のマスクを解除し描画 ctx.colorMask(true, true, true, true); renderer.render(scene, camera); };
流れをまとめると、
ソフトパーティクル以外のオブジェクトのz-bufferを深度テクスチャに焼く → 深度テクスチャなど必要な情報をソフトパーティクルのシェーダーに渡す → 全てのオブジェクトを描画
となっています。
z-buffer を深度テクスチャに焼く際、 gl.colorMask という WebGL API を直接叩いています。この関数は、フレームバッファーに書き込むチャンネルをフラグ形式で指定することができます。
深度情報のみ欲しい場合はRGBA情報は必要ないので、gl.colorMask(false, false, false, false) として色情報を書き込む処理を無効にし、負荷軽減対策をしています。
GLSL
ソフトパーティクルの処理を入れている白い箱の頂点シェーダーとフラグメントシェーダーの抜粋になります。 こちらもコメントをつけていきます。
// ---------------------------------------------------------------------------- // 頂点シェーダー // ---------------------------------------------------------------------------- attribute vec3 position; attribute vec2 uv; uniform mat4 projectionMatrix; uniform mat4 modelViewMatrix; varying vec4 vViewPosition; void main() { // ビュー座標をフラグメントシェーダーに渡す vViewPosition = modelViewMatrix * vec4(position, 1.); gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.); } // ---------------------------------------------------------------------------- // ピクセルシェーダー // ---------------------------------------------------------------------------- precision highp float; #include <packing> varying vec4 vViewPosition; uniform sampler2D uDepthTexture; uniform float uCameraNear; uniform float uCameraFar; uniform float uDepthFade; uniform vec2 uResolution; // refs: https://threejs.org/examples/webgl_depth_texture.html float readDepth(sampler2D depthSampler, vec2 coord) { float fragCoordZ = texture2D(depthSampler, coord).x; float viewZ = perspectiveDepthToViewZ(fragCoordZ, cameraNear, cameraFar); return viewZToOrthographicDepth(viewZ, cameraNear, cameraFar); } void main() { // スクリーン上の座標を0~1で取得 vec2 screenCoord = gl_FragCoord.xy / uResolution.xy; // 深度テクスチャから、現在のピクセルの他のオブジェクト(= 赤い箱)の深度値を取得 float sceneDepth = readDepth(uDepthTexture, screenCoord); // 現在のピクセルの、これから描画するオブジェクト(= 白い箱)の深度値を取得 float viewZ = vViewPosition.z; float currentDepth = viewZToOrthographicDepth(viewZ, uCameraNear, uCameraFar); // 深度値を比較して、一定の値以下の場合はフェードする // 0除算しないように調整 float eps = .0001; float fade = clamp(abs(currentDepth - sceneDepth) / max(uDepthFade, eps), 0., 1.); gl_FragColor = vec4(vec3(1.), fade); }
ポイントは readDepth 関数です。この関数自体はthree.jsのサンプルから引用しました。
まず、深度テクスチャには z-buffer の値が入っているのですが、z-buffer の性質の関係で値は非線形ではありません。 そのため、取り出す際には camera の near clip と far clip を元に線形に直す作業が必要になります。それを行っているのが readDepth です。
z-buffer の性質についてはこちらの記事がとてもわかりやすかったです。
readDepth 関数の中で呼んでいる viewZToOrthographicDepth
, perspectiveDepthToViewZ
はピクセルシェーダー冒頭の #include <packing>
で展開される中身に含まれています。
#include <...>
は three.js のGLSLのコードを挿入する記述です。fog など three.js の機能に絡むシェーダーを使いたい時に便利です。
float viewZToOrthographicDepth( const in float viewZ, const in float near, const in float far ) { return ( viewZ + near ) / ( near - far ); } float perspectiveDepthToViewZ( const in float invClipZ, const in float near, const in float far ) { return ( near * far ) / ( ( far - near ) * invClipZ - far ); }
改善点
iOS Safari, Chrome で見るとガタついているようなアーティファクトが発生していました。原因は、深度テクスチャの書き込み時もしくは読み取り時に精度が落ちていることにあるようです。
フェードする閾値だけを調整できるようにしているのですが、フェード開始地点・フェードする閾値の2つを調整できるようにするとより細かい調整が可能かなと思います。
霧の表現
キツネが駆けるデモでは霧のような雰囲気を出すためにソフトパーティクルを使っているのですが、煙のポリゴンをたくさん出して霧の雰囲気を出そうとするとその分透過の描画・重なりも増えるので描画が重くなりやすいです。
なので、霧のような雰囲気をより出すためにはパーティクルは控えめにしつつ(数を減らす・サイズを小さくするなど)、他の方法と組み合わせるのがよいと思います。例えば distance fog や height fog ですね。キツネが駆けるデモでは three.js の fog (distance fog) と組み合わせています。
【typescript】ファイルの容量を元に適切なファイルサイズの数値と単位を返す関数
ファイルアップローダーを作成する際、ファイルサイズを表示する場合にデザイン的に以下のあたりに気を使うことが多いのかなと思っています。
- ファイルの容量に応じて適切な単位に切り替える
- 小数点以下第何位まで表示するか
例えば約3MBのファイルをアップする際、3000KBのようにKB表記にしてしまうとわかりづらく、一方で表記をMB合わせにして約30KBのファイルをアップする場合、0.03MBと表記するとこちらもあまり直感的ではありません。
つまり、ファイルの容量に応じて自動で単位を切り替えるようになっているのが理想です。
macのfinderでは適切なファイルサイズがKBの場合は 166KB
と表記し、MBの場合は 1.6MB
のような表記になっています。つまり、単位によって表示する小数点以下の位が変わっています。
今後使い回すことのできるように、そのあたりを関数にまとめてみました。 作成した関数では小数点以下の位の数は統一するようにしていますが、macのfinderのように単位によって切り替える場合はこの関数の中身を調整してあげればよさそうです。
const units = ["Byte", "KB", "MB", "GB"];
のように単位を列挙し、その数に応じて表記を調整しています。
GBまでの表記が必要なければ配列の中身をByte,KB,MBの3つにすると、どれだけファイル容量が大きくてもMB基準の表記となります。
【React】timeout を管理する様々な custom hooks
ここ3ヶ月ぐらいはずっと Nextjs SSR x typescript なフロントエンド開発を行っていました。
hooksが導入されてからのReactは初めてでしたが、hooksはロジックを分離するのにとてもいい方法だと感じました。
Reactのデザインパターンは、コンポーネントをどれぐらいの粒度で細分化させるか(Atomic Designなど)や、ロジックとview部分をどう分けるかが議論のポイントだったように思っていたのですが、hooksの登場によりcomponentが直接hooksを参照する形でロジックを利用することができるようになったので、後者に関しては解決される部分が大きいのかなと思いました。
ロジックを小さい単位で切り出すことができていれば他のプロジェクトでもそのまま使うことができ、レシピ集的に使うことのできる点は嬉しいです。
useTimeout.ts
timeoutを管理するだけのロジックも、一度切り出してしまえばcomponent間で使い回すことができます。同一componentでも複数呼び出すことができます。
useKeepAliveTimer.ts
とあるhooksにさらに機能を持たせたい場合、そのhooksを拡張するのではなく、hooksを呼び出すhooksを作ることで機能拡張ができるのも良いなと思いました。
例えば「一定時間フラグを立たせておく」ロジックを実現したい場合は、前述のuseTimeout.tsを呼び出すhooksを作ることで実現できます。
YoutubeのIFrame Playerでcontrolsをオフにすると自動再生がされない
追記(2020/07/05)
解決法を発見しました。
onPlayerReadyでsetVolume(0)
かmute()
を呼んで明示的に音声をオフにしてからplayVideoを呼ぶと自動再生されました。
↓ 検証コードはこちらになります
ただ、自動再生後に音量をスライダーなどで変えてsetVolumeを呼ぶと再生が停止されてしまいます。
どうやら自動再生時にオリジナルのUIによる動作を完全に制御するには、iframe内の要素を一度はクリックする必要があるようです。
元内容
javascriptでのYoutubeのIFrame Player API埋め込みを使って、 独自UIによるYoutube Iframeの制御の挙動を調査していたときに発見しました。
APIを使った埋め込みとiframeのembedのどちらも試した結果同じ挙動になっていたので、おそらくバグではなくYoutubeの仕様と思われます。
↓ 検証コードはこちらになります
controls オプション
controlsを0にするとYoutubeのUIを非表示にすることが可能なのですが、 その場合にautoplayを1にしても自動再生が効かなくなるようです。
controlsのオプションはこのような仕様になっており、特に自動再生との兼ね合いは書いてありませんでした。
動画プレーヤーのコントロールを表示するかどうかを指定します。 controls=0 – プレーヤー コントロールを表示しません。 controls=1(デフォルト)– プレーヤー コントロールを表示します。
また、APIで埋め込んだiframeは最初に手動で動画をクリックして再生するまではplayVideoメソッドが効かないという挙動もありました。
そのため、独自UIと自動再生の両立が難しそうです。
YoutubeのAPIを触るのは3年ぶりぐらいだったのですが、 もともとできないようになっていたのか、いつからか出来ないようになったのかまではわかりませんでした。