- ウェブサイトをAstroでリニューアルした
2018年にGatsbyで構築したこのウェブサイトをAstroでリニューアルしました。この記事では、リニューアルに至った背景や技術スタックについて紹介します。
リニューアルの背景
このサイト自体は2018年5月にファーストコミットが行われており6年もののサイトになっていました。サイト自体のデザインやコードが古くなり、手を入れたい気持ちが高まっていました。一方でGatsbyの本を出版している程度には思い入れがあるため、現状のGatsbyサイトを残したいという気持ちがストッパーになっていました。
しかし、Gatsby本体の開発が実質停止状態になっていたのに加え、仕事で扱うことの多いNext.jsと比べても、そのモダンさとのギャップが目立つようになりました。そうした理由からリニューアルを行うことにしました。
技術的なお話
Astroの採用
リニューアルの構成を検討するにあたり、Next.jsやRemixといった主要フレームワークも候補に挙げましたが、最終的にはAstroを採用することにしました。Astroはアプリケーションというより、ドキュメントサイトやブログなどの「コンテンツ中心」のウェブサイト向けに特化しており、やりたいことがシンプルに実装でき、ブログ中心のウェブサイトにはピッタリだと考えました。
以前から続けてきたesaへの投稿をAPI経由で取得し、ウェブサイトに表示するフローはそのまま維持しています。これはAstroのv5で導入された「Content Layer」を利用することで簡単に実現できました。ビルド時にコンテンツをキャッシュできるため、API制限のあるサービスを扱う際も負荷がかからず、とても使い勝手がいいです。
ほぼ同じことができるGatsbyJSのGraphQLのデータレイヤーよりも洗練されていると感じています。
Content Layer用のローダーの自作
esaは国内向けのサービスであるため、AstroのContent Layerに対応したローダーが存在せず、自前で実装する必要がありました。
他のローダーの実装を参考にしながら開発を行いましたが、非常にスムーズに実装できました。しばらくして、コードが固まったタイミングでnpmパッケージとして公開します。
Astroはマークダウンを扱うremark系の処理を公式が持っていて、それに相乗りする形でマークダウンの変換処理も任せることができました。旧式のマークダウン変換処理と差分がありましたが、適切なプラグインを導入し、妥協できるレベルの出力にしました。
今まではesaが変換してくれたHTML1を利用していましたが、フレームワークに乗っかったことで、より柔軟な変換処理ができるようになりました。
zodでスキーマを定義してデータのバリデーションを行いつつも、TypeScriptの型として提供されるのは、とてもいい仕組みだと思いました。
マイクロインタラクション
このサイトではリニューアル現在はクライアントで動作するJavaScriptを(3rd Party Scriptは除いて)書いていません。ただその中でも、ページ左上ナビゲーションの現在位置を示すドットに、View Transition APIを活用したインタラクションを実装しています。
ページ間の遷移時に、ブラウザがスムーズにアニメーションを付けてくれるのが特徴です。今回のリニューアルでお気に入りの箇所です。
その他の開発技術
今回のリニューアルではTypeScriptをファーストクラスで採用し、スタイリングにはTailwind CSS、パッケージマネージャーにはpnpmを利用しています。また、今回はデプロイをNetlifyではなくVercelで行っています。Netfliyも同人誌を書くレベルで好きだったのですが、時は残酷です。
数年ぶりの大きなアップデートでしたが、「ウェブフロントエンドの変化がとにかく早い」と言われていた時期に比べると、あまり変化はないと思いました。適切な頻度で知識のアップデートを行えば、ついていく分には問題がなさそうで安心しています。
これからの展望
このウェブサイトはもともとフロントエンドエンジニアとしての技術の素振りも兼ねていました。今となってはUIデザイナーが本業なので、素振りというよりは今までとは違った表現やメモ置き場として運用していきたいと思っています。
ゆくゆくは海外のデザインエンジニアがやっているようなCrafts的なものを作っていきたいです。
旧サイトのGatsbyJS版のソースコードはGitHubのパブリックリポジトリに引き続き残しておきますので、興味がある方はぜひ覗いてみてください。
注釈
生のマークダウンの他に変換されたHTMLがAPIレスポンスに含まれています。 ↩
2024/12/31 - 買ってよかったもの 2023
年が終わるので、買ってよかったものを書く。今年は体調を崩していたのでデバイスはほとんど買わない年だった。
ひらくPCバッグmini
今年のベストバイ
グッドデザイン賞を取っているPCバッグ
自立するので省スペース、とてもいいサイズ感
13inchのMBAを入れているが、他のカバンより軽く感じる
新宿に現地で見れるお店があるので、気になる人は行ってみるのがおすすめ。店員の方がとても親切に説明しれます。
SANODESK(昇降デスク)
引っ越しを機に購入。物持ちがいいのでそれまで20年ぐらい同じ机を使っていた。
でかい作業机はそれだけで便利
SANODESKはFlexiSpotのサブブランドらしい
組み立てが思ったより簡単だった
MateViewを使っているが余裕がある
昇降機能は使ってない
天板が薄いのでカスタマイズとかはできない
その他
岩手県普代村の生うに(ふるさと納税)
ふるさと納税で頼んだもの。とても美味しかった、来年も頼む。
洗浄機付きの電動髭剃り
今までカミソリで髭を剃っていたので肌荒れがひどかったが、電動に切り替えてかなりマシになった。
洗浄機は大きくて邪魔だけど、メンテが楽になので自分には合ってる気がする。
ぼっち・ざ・ろっく(漫画)
アニメを見てハマった結果、原作漫画を全部買った。よい。
NFCタグ
サンワサプライのやつ。
プロフィールやポートフォリオ用に購入。ちょい反応が悪いがウケはよい。
2023/12/31 - ウェブ技術で縦書きを含む画像を生成したい
ここ最近、Web技術を利用した画像生成に興味があります。本記事では、日本語における表現の一種である縦書きに焦点を当て、Web技術を使った縦書きを含む画像生成方法についての調査をまとめました。
現状の縦書き対応と問題点
単なる縦書きはやりやすくなっている
ウェブ上の縦書き対応は着実に進んできています。CSSでwriting-modeを使えば比較的容易に実現できます。しかし、HTMLから画像を生成する分野では話が違います。
HTML(Likeなもの)を使った画像生成で代表的な手法であるSatoriやhtml2canvasではどちらともwriting-modeをサポートしていないため、縦書きを含む画像を生成できません。
別アプローチのSVG foreignObjectを使った画像出力方法では、縦書きにはある程度対応できますが、カスタムフォントの埋め込みが難しいという課題があります。画像を作るからにはある程度デザイン性の高いものを生成したいため、カスタムフォントが使えないと話にならないケースが多いと思います。
これらの結果から、CSSを活かしたような画像生成は難しいように思えます。
デザインツールにおける縦書き実現方法の調査
例えば、デザインツールのFigmaでは縦書きに対応していませんが、Canvaは対応しています。そこで、Canvaがどのように縦書きを実現しているのか調べました。
CanvaのUIを調べると、Canvas実装ではなく、HTML構造を元にしていることがわかります。また、ダウンロード処理を見ると、エクスポートのキューに入り、処理が終わるとダウンロードされていることから、サーバーで何らかの処理が行われていると推測できます。
これにより、ヘッドレスブラウザを使用している可能性が高いと考え、調べたところ、以下の記事がヒットしました。
https://www.canva.dev/blog/engineering/discovering-headroll-cve-2023-0704-in-chromium/
To convert a design from a rich web representation to PNG entirely on the server side, Canva loads the design into Headless Chromium and requests a PNG export of the page.
リッチなウェブ表現をPNGに変換するために、CanvaはデザインをHeadless Chromiumでロードし、ページのPNG出力をリクエストします。
つまり、クライアントで構築したHTMLをサーバーサイドにあるヘッドレスブラウザを使って画像に変換することで縦書きを含む画像の出力をしていることがわかりました。
ヘッドレスブラウザを使えば、カスタムフォントの扱い等でオリジンなどを気にせず、ローカルリソースに置き換えることができるため、扱いが容易になります。
この方法を使えばWeb技術を使って縦書き画像をつくれそうです。ただし、自分の調査しているものはクライアント上で簡潔させたいので別の方法で考えた方が良さそうです。
クライアントサイドでのカスタムフォント対応
カスタムフォントを使用しながら、クライアントサイドで完結させる方法を模索しました。原始的ではありますが、1文字ずつレンダリングしていく方法が賢明だと思いました。簡易な実装ではありますが、以下のような方針です。
参考コード
export const renderVerticalText = (
ctx: CanvasRenderingContext2D,
word: string,
sx: number = 0,
sy: number = 0,
) => {
const fontSize = 24;
let height = sy;
ctx.font = `bold ${fontSize}px vertical-font`; // 縦書き用のフォントを使う必要がある
const words = splitWords(word); // 単語ごとに分割する
words.forEach((word, i) => {
ctx.save();
ctx.translate(sx, height);
if (word.match(/[「(【『]/)) {
// 文字を半分だけ上にずらす
ctx.fillText(word, 0, fontSize - 2 - fontSize / 2);
height += (fontSize / 2);
} else if (word.match(/[」)】、。』]/)) {
ctx.fillText(word, 0, fontSize - 2);
height += (fontSize / 2);
} else if (word === " ") {
height += (fontSize / 2);
} else if (word.length > 1) {
const padding = (fontSize / 5);
// 前後が空白か確認して、空白でなければpaddingを足す
// 本当は括弧系の場合も考慮するべき
const paddingStart = i > 0 && words[i - 1] !== " " ? padding : 0;
const paddingEnd = words[i + 1] !== " " ? padding : 0;
console.log(paddingStart, paddingEnd);
const size = ctx.measureText(word);
height += size.width * scaleY + paddingStart;
ctx.rotate(Math.PI / 2);
ctx.fillText(word, 1 + paddingStart, -2);
height += paddingEnd;
} else {
// 全角系の文字
height += fontSize * scaleY;
const { width } = ctx.measureText(word);
ctx.fillText(word, (fontSize - width) / 2, fontSize - 2);
}
ctx.restore();
});
return { height: height - sy };
};
この方法であれば、Satoriやhtml2canvasなどの既存のツールを使わずとも、縦書き対応が可能です。ただし、この方法では制御するためのコードが複雑になりやすい上に、描写上の問題がいくつか残っている点に注意してください。
また、この処理を行う際に利用するフォントは縦書き用のフォントを使わなければいけません。CanvasのfillTextでは縦書き用のグリフを指定できないので、縦書き用のフォントを作らないとーや『』などの鉤括弧類の方向が誤ったまま表示されます。
まとめ
縦書き用の画像を動的に生成するには、まだそれなりの努力が必要そうです。もし、縦書きに関する詳細な知識をお持ちの方がいらっしゃれば、ツッコミをいただきたいです。
2023/04/30 - 翻訳でHacktoberfestに参加しました
毎年10月に開催されるHacktoberfestに参加しました。このイベントはOSSへの貢献を行い、期間中に規定数(4つ)の貢献を行った人に特典がプレゼントされるものになっています。
自分はドキュメンテーションの翻訳でHacktoberfestに参加したので、参加記録を書いておきます。今年のHacktoberfestは終わってしまいましたが、来年以降の参加者にとって参考になればと思います。
今年のHacktoberfest
Hacktoberfestは毎年行われていますが、トップページに今年は何が新しくなったのかを説明する「NEW FOR 2022」ページへのリンクが置かれていました。
ざっくりとした内容は次のようなものになっています。
CONTRIBUTING TO OPEN SOURCE ISN’T JUST FOR TECHNICAL FOLKS WHO WANT TO WRITE CODE. THERE ARE LOTS OF OPPORTUNITIES TO USE YOUR PROFESSIONAL SKILLS IN SUPPORT OF OPEN-SOURCE PROJECTS. THIS YEAR, WE’RE MAKING A POINT TO ENCOURAGE CONTRIBUTIONS THAT REQUIRE SOME TECHNICAL EXPERIENCE OR NONE AT ALL. NO MATTER YOUR EXPERIENCE, YOU CAN PARTICIPATE IN HACKTOBERFEST!
大約: 今年はコードを書く以外の貢献も歓迎します
このように昨年までのHacktoberfestでは推奨されなかったドキュメンテーションや翻訳などのテクニカルライティングも貢献としてカウントされるようになりました。
貢献の手順
自分が貢献したのはAstroというウェブフロントエンド分野のサイトジェネレーターのプロジェクトです。
Astroにはドキュメントが複数の言語で用意されており、その中に日本語もあります。ただし、日本語のドキュメントは最新のものに追いついているわけではなく、古いものや日本語版のページは存在せず英語版のまま表示されているものも存在します。
Astroとの関わり方としては、仕事で触っているもののドキュメントが古くなっていることに気づき、貢献を考えていたところたまたまHacktoberfestの貢献対象だと知りました。
Astroに貢献した具体的な手順を説明します。
Astroの翻訳プロジェクトには翻訳の貢献に関するドキュメントがあります。貢献の仕方には翻訳とレビューがありましたが、その時点で日本語レビューを受け付けているプルリクエストはなかったので翻訳を選択しました。
ドキュメントに従い翻訳ステータスが記録されているIssueを確認し、自分が翻訳したいページを見つけます。
ステータスとしては、まだ日本語の翻訳が全くされていないものと、翻訳が古くなっているものに分かれています。自分は後者を選択しました。
翻訳したあとはPRを作成します。
Hacktoberfestの貢献と認められるには、hacktoberfestのタグがついたリポジトリで貢献するか、hacktoberfest-acceptedのラベルがついたプルリクエストで貢献する必要があります。Astroは後者なので、PRのdescriptionにhacktoberfestに参加する意思を示さないといけません。
Hacktoberfest参加者向けの文章も用意されているので、参考にしながら進めます。
特典の申し込み
PRが作成されると、日本語コントリビューターによるレビューを経てマージされました。Hacktoberfestの貢献もPR作成から7日後に承認されました。これはHacktoberfestのWebページで確認できますが、承認されるたびにメールが届くのでページでなくても確認することができました。
4つのPRが承認されると、Hacktoberfestからメールが届きます。このメールから特典への申込を行います。
Hacktoberfest用のECショップへのリンクを踏んで、必要情報を記入し申し込みを行いました。
自分は2019年にもHacktoberfestに参加しましたが、Tシャツが届くまで結構期間がありました。おそらく、忘れたことに届くことでしょう。その日が楽しみです。
感想
ここ最近、オープンソースプロジェクトへの貢献をほとんどしていませんでしたが、このイベントを通して久しぶりに貢献することができました。
Astroは個人的に気になっているプロジェクトなので今後も暇を見つけつつ貢献していけたらなと思っています。
2022/11/02 - Figmaのイベントでプラグイン開発について話してきた
Figmaの公式コミュニティであるFriends of Figma, TokyoのFigmaお楽しみトーク Vol.2というイベントでFigmaプラグイン開発について話してきました。
今回のイベントはエンジニア寄りの回で、デザインというよりはデザインxエンジニアリングをテーマにしたトークが中心でした。
話したこと
一言で言うと「Create Figma Pluginというライブラリを使うと、プラグインのリリースにすごく近づくからおすすめ」という話をしてきました。
自分はプラグインとウィジェットをそれぞれ1つずつリリースしていますが、リリースするまでに放棄してしまったものがいくつもあり、なかなかリリースまで行きつくことがありませんでした。そういった状況を打開してくれたのがCreate Figma Pluginです。
今回の発表ではプラグインのリリースにおいて何が壁になっているか?というのを確認して、Create Figma Pluginを使えばその壁を乗り越えられる。という話の展開で進めました。
Create Figma Pluginはテンプレートの展開やビルド環境の構築だけでなく、UIコンポーネント集やユーティリティ集も含んでいます。(完全に名前で損をしていると思います)これらの機能によりリリースまでに必要な面倒事を避けられます。
スライド作成について
今回初めてFigmaでのスライド作成に挑戦しました。基本的には1920x1080のフレームを作っていき、その中をスライドのエリアとして扱うようです。実際のファイルはこちらです。
タイポグラフィ周りで多少工夫しています。今回はオープンソースのゴシック書体である「IBM Plex Sans」とスタンダードなサンセリフ書体である「Futura」を組み合わせています。Figmaの標準機能で日本語と英語に対して別フォントを設定するのは大変ですが、ワンクリックでフォントの設定が出し分けられる弊プラグイン「Japanese Font Mixer」を利用して設定しました。
また、日本語はそのまま使うとひらがなの間が空きすぎるので「Propotional alternate widhts」を有効にし、最後にパラグラフのLetter Spacingを1%に設定しています。割と雑に書いてもイイ感じになるので、かなり程よい設定な気がしています。このへんの設定は既存のスライドアプリでは面倒な気がするのでFigmaが優位になる点だと思います。
この辺りのノウハウはまた別の記事で紹介するかもしれません。作成編と発表編に分けて技術同人誌に書いても面白い気がします。
感想
久しぶりに勉強会に話す立場で参加しました。トークの準備は大変ではありますが、自分としてもいい整理と勉強の機会になりました。イベントに誘ってくださったseyaさんやコミュニティの方々、参加して下さった方々に感謝します。
他のお二人もデータ定義にデザイナーも巻き込んでいく話やTailwindのコードを出力するプラグインなどとても面白い話をしていたので、聞く立場としても大変楽しかったです。これがオフラインイベントであれば懇親会等でかなりディープな意見交換ができたと思うとコロナ禍が残念でなりません。
余談
エンジニア回というだけで完全にエンジニア寄りのテーマ設定にしてしまい、Twitterやコメントの反応から察するにターゲット設定を間違った気がしています。本来、Figmaコミュニティなのでユーザー属性的にもう少しデザイナーを意識して「作り方」ではなく「作れるもの・作ったもの」にフォーカスした方がウケるトークをできたと思います。ただ、自分が作っているものはミニツール的なものなので、1つのトークを構成するほどのものはないので妥当と言えば妥当だったかもしれません。
また最初のくだりでプラグインは公開した方が「カッコイイ」って話をしましたが、ちょっと熱が入りすぎましたかもしれません。時間配分も含めてちゃんと考えればよかったです。
2022/09/11 - GitHub Actionsのイベントによって挙動を変える
GitHub Actionsでは定期実行(schedule)のイベントがサポートされておりCron形式で定期的に実行されるワークフローを定義できます。しかし、scheduleではなくpushイベントで動作を確認したいときでも、定期的に実行されるワークフローの中に破壊的な処理(ファイル書き込みやAPI呼び出し)があると安心して実行することができません。
ローカルでDockerコンテナを使って確認するactというツールもありますが、個人ごとに環境を用意する必要があったり、微妙に挙動が不安に感じることもあってnot for meな気がしています。
そこで、各種CLIに付随するdry run的な挙動を実現するために、Workflowをトリガーするイベントによって処理を分岐させ安全に挙動を確認できるようにしました。
イベントによって挙動を変える
次のワークフローはDenoで外部APIから取得してきたデータをファイルに格納し、add-and-commitアクションでリポジトリにcommitするものです。
.github/workflows/task.yml
name: Run task
on:
schedule:
- cron: '30 0 * * *'
push:
jobs:
run:
runs-on: ubuntu-latest
steps:
- run: echo "event name is:" ${{ github.event_name }}
- name: Checkout repo
uses: actions/checkout@v3
- name: Setup Deno
uses: denoland/setup-deno@v1
with:
deno-version: v1.23
# ファイルの書き込みを行うプログラムを実行しています
- name: Run script
run: >
deno run
--allow-write
--allow-env=MY_SECRET_TOKEN
--allow-net=api.example.com
index.ts
env:
MY_SECRET_TOKEN: ${{ secrets.MY_SECRET_TOKEN }}
# 作成されたファイルのDiffが出力される
- run: git add . -N && git diff .
- uses: EndBug/add-and-commit@v9
if: github.event_name == 'schedule' # この条件文によってScheduleのときだけコミットされるようになる
ワークフローのイベントにはScheduleとPushが設定されているため、定期的に実行されるだけでなくプッシュされたときにもワークフローが実行されます。
ただ、プッシュされた時はファイルが作成されたことは確認したいのですが、コミットされるのは望ましくありません。
そこでStepに条件文を指定し、トリガーとなったイベント名を取得するgithub.event_nameを使ってScheduleの時のみコミットを行うように指定します。
また、デバッグ用として作成されたファイルのDiffを表示するためにgit add . -N && git diff . を実行するようにしました。
これら対応を行うことである程度安心して挙動を確認できるようになりました。
まとめ
GitHub Actionsのワークフロー構文に関してはドキュメントがしっかりしていて、しかも日本語対応しています。ドキュメントを読みましょう。
今回は触れませんでしたが、簡単なタスクを書くのであればDenoが良かったです。
2022/07/29 - デザインとエンジニアリングの狭間で
この記事はエンジニアリングに興味があるデザイナー、デザインに興味があるエンジニア Advent Calendar 22日目の記事です。
私は以前ウェブアプリケーションエンジニア(サーバーとウェブフロントが半々ぐらい)として働いており、今の会社ではデザイナーチームに所属しています。
ただし、コードもちょいちょい書いているので、既存の職種に当てはめると「UXエンジニア」・「デザインエンジニア」みたいなものになると思います。
UIデザインに手を出した時期が5,6年前で、SPAで企業向けのアプリケーションを作っていました。その際にUIデザイナーとして働き、エンジニアリングの経験を活かしたデザインで価値が出せると感じたこともありWebアプリケーションエンジニアとしてはデザイン寄りの働き方をしてきました。
それ以前からもPhotoshopやSketchを使ってデザインモックを作ったり、名刺・ポスター・パンフレットなどを作っていましたが、UIデザインの楽しさに気づいたのはそのタイミングでした。
今の会社にエンジニアリングのわかるUIデザイナー枠で呼ばれたこともあり、働き方を大きく変えることになりました。
そんな分野を横断した私の立場から「デザイン・エンジニアリングの狭間で我々は何ができるのか?」というのを考え直してみました。
なおこの記事中でのエンジニアリングは「Webフロントエンド」、デザインは「WebにおけるUIデザイン」として読みかえてください。
技術的な難易度・可能性を考慮できる
ウェブフロントエンド出身デザイナー、一番の強みは「技術的にできること・難しいこと」が把握できるというところです。
一枚のモックを作るだけだと、ウェブデザイナーとしてグラフィック出身の人たちにはかないませんが、そのモック上でどう動くか?という点ではウェブフロントエンドの知識を活かせる部分になります。
コンテンツがない時にどう表示するのか?
フォームでエラーが出たときの表示はどうするのか?
UIを伴うSaaSを組み込む際にどういう動きをするのか?
といったエンジニアが気になる点はもちろんですが
ネイティブのシェアAPIを使ってフォールバックはどう表示するのか?
アニメーションはGIF、SVG、Lottieのどれを使うのか?
After Effects的なこの効果は再現できるのか?
グラフの表現はどこまで可能なのか?
といったデザイナー的に気になる点も考えることができます。
もちろん、実際は本業のエンジニアと相談しながら進めることになりますが、ある程度エンジニア的な知識があった方が話は進めやすいと思います。
また、プロトタイプ的なタイミングではスマホのジェスチャーを使ったインタラクションを増やすときに何ができて、何ができないのかといったことをある程度把握した上で考えられます。FigmaやXDではカバーしていない範囲であっても、After Effectsで軽く作ってみたり、実際にコードを書いてプロトタイプを作るといったことも選択肢になります。
実際の例として一般的なUIから外れた画面を考える際1、JSでジェスチャーを含むプロトタイプをつくって関係者に共有し、合意をとった上で実装に入ったことがあります。
プロトタイプのためにコミュニケーションを取るのは精神的なハードルを感じていて、一人で完結するメリットを感じた瞬間でした。
英語を読む抵抗感が薄い
経験上ウェブエンジニアをやってきた人間は英語のツールやドキュメントに対しての抵抗感が薄い人が多いと感じています。
UIデザインをつくる上でGoogleのMaterial DesignやAppleのHuman Interface Guidelinesといったものを参照する場面は多いと思います。しかしこれらのガイドラインは英語で書かれており人によっては抵抗を感じるようです。
「日本語に翻訳されたものを見る」という選択もありますが翻訳の段階で抜け落ちる情報があったり、最新の情報に追いついていないという場合もあるので、原文を読むのがベストでしょう。
また、近年UIデザインのカバーする領域が広がっており、アクセシビリティやデザインシステムといったテーマが話されることが多くなっています。
これらの分野はまだ情報が少なく、調べる際に日本語だけだと限られた情報にしかアクセスできません。
本来原文で読むべき内容であっても要約してチームに伝えることはできます。チーム内で得意・不得意はあると思っているので、情報収集は積極的にやっています。
手触りや細部の表現まで意識できる
手触りという意味ではネイティブアプリライクな画面を作る機会もありました。どうしてもウェブだと「アプリほど気持ちよくない」とか「没入感が弱い」となりがちですが自分はウェブでもよりよい手触りが実現できると考えています。
基本的な実装技術の欠落により操作感が落ちることはあるのですが、UIパーツごとの最適化不足による体験の悪化もあると思っています。
Webではリソースの少なさから既存のUIライブラリに頼ることが多いと思います。そういった理由から理想のインタラクションを諦める場面も多いと思います。エンジニアリングの経験があることで実装の可否とコスト感がわかり実現できる場合もあります。
理想のインタラクションが実現できたときは本当に感動しました。
また、今の職場は業務に直接関係しない研究が推奨されているのでユーザーの投稿したコンテンツや自動生成の画像・動画の質を高めるといったテーマは一貫してやっていました。JSで自然言語処理を行い改行位置の調整や画像に対してオリジナルフィルター、CSSでできる文字詰めはもちろん、縦書きCanvasの文字詰めにも挑戦してきました。アニメーションを実装する際は、YouTubeのAfter Effectsのチュートリアルを見ながらレイヤーの構造や動かし方を理解するフローを試したりしました。
こういった細かい部分はエンジニアだけでもデザイナーだけでも進めにくいのでまさに自分にあったテーマだと思います。
今後はクリエイティブコーダー界隈やゲーム開発界隈のように更に別分野から知識を取り入れていけば面白いものができそうな気がしています。
デザインとエンジニアリングの溝を埋められる
どちらともの分野に踏み込んだ人間に多くの現場で求められるのが、デザイン・エンジニアリングをまたぐような分野と、その溝を狭めることだと思います。
例えば、自分はFigmaの普及活動をやっています。非デザイナーに対してはFigmaで作図や簡単なワイヤーを書くワークショップを、デザイナーにはAutoLayoutやComponent, Libraryなど再利用しやすいFigmaデータの作り方をレクチャーしました。
このおかげかどうかはわかりませんが、あるPdM(プロダクトマネージャー)はワイヤー段階の作業をFigmaで行ってくれましたし、XD派だったデザイナーの人はFigmaに興味を持ってくれてFigmaを積極的に触ってくれるようになりました。
一方で、グラフィック出身のデザイナーと関わる機会も増えて、ウェブ出身のデザイナーとは違った文化であったりツールセットの知識なども増えてきています。
また、デザインシステムやアクセシビリティについて考える機会も増えています。
今の職場では小さい規模感のアプリケーションがたくさんある、という感じなのでデザインシステムを作るといった話はありませんが、デザイントークンの定義やデザイナー・エンジニア間でのワークフローの話は盛んに行われています。そういった場面でもデザインとエンジニアリングの経験が活きていると感じています。
まだまだ道半ば
ここまでデザイン・エンジニアリングの狭間でできることを書きましたが、実際のところ中途半端な場面は多いと感じています。
デザインの成果物で重視されがちなビジュアルやグラフィックの面では他に得意な人がいて任せがちなのもそろそろやめられるといいなと思っています。
プライベートではグラフィック寄りなものや、同人誌の表紙なども作っていて以前よりはだいぶ苦手意識が薄くなっています。以前は全く理解できなかったIllustratorも、今では和解してお気に入りのツールになりつつあります。
これは意図しないことですが、プロトタイピングはかなりうまくなっていて「これを作りたい」と思ったものを手っ取り早く具体化することができるようになりました。
プロトタイピングの中でも手をかけるべきところと手を抜くべきことの判断はうまくなりましたし、いろいろなデザイン・コード資産も溜まってきてクオリティも徐々に高くなってきました。2あとは実際にリリースできる打率を上げられればまた別の価値を出せる気がしています。
中途半端な器用貧乏から、二刀流を自称できるぐらいの結果を出せるよう頑張っていきます。俺たちの戦いはこれからだ!
注釈
本来、一般的なUIパターンを利用しユーザーの慣れを利用するのが原則です。 ↩
今年は特にCanvas(2D)への理解が深まりました。来年はWebGLもちゃんとやりたい。 ↩
2021/12/22 - 技術書典10でReactとGatsbyJSの入門本を頒布します
技術書典10で『つのぶえ出版』としてReactとGatsbyJSの入門本を頒布予定です。
どんな本か
一言でいうと、React/Gatsbyでウェブサイトを書けるようになる本です。
2021年に通用するGatsbyJSのガイドブックを目指して執筆しました。
近年、GatsbyをはじめNext.jsでウェブサイト作る機会が増えてきました。しかし、その土台となるReactは本来のアプリケーション開発を想定したものなので、巷の入門本もアプリケーションを作るための学習を目的としたものになっています。
この本ではGatsbyでサイトを作ることをゴールとし、その過程でReactの学習を行います。本来Reactの入門本では必須と思われるhooksやcontext, fluxに関してもほとんどスキップし、コンポーネントの書き方やCSS Modulesといったマークアップに近い内容を分厚くしています。
今後、更に増えてくるであろうReactのマークアップやGatsby, Next.jsでのウェブサイト製作への入門を考えている方にぜひ読んでもらいたいと思っています。
作れるもの
本書ではGatsbyでウェブサイトとブログの2サイトを開発しながら、ReactとGatsbyに慣れていきます。
1つ目のサイトは、ポートフォリオを想定したサイトです。
2つめのサイトは、ブログを想定したサイトです。
この2つのサイトを作ることで、ランディングページをはじめコーポレートサイトのようなものも作れるようになります。
あらすじ
第1章ではReactのおさらいをしつつJSXでポートフォリオサイトを構築します。最後にReact単体でウェブサイトを作る際のデメリットを確認し第2章でGatsbyに入門します。
第2章ではポートフォリオサイトの要件を達成するGatsbyのサイトを作成します。Gatsbyのルーティング、データの取扱、Netlifyのデプロイまで一通りサイトが作れるレベルまでやります。
第3章では記事データをmicroCMSという国産のHeadless CMSで管理し、Gatsbyのブログを構築します。
第4章ではよりGatsbyを乗りこなすためのTipsを紹介します。
内容を調整している段階ですが、現時点で100ページを超える内容になっています。電子版は1,000円、物理本セットは1,500円を予定しています。
BOOTHで「GatsbyJS Guidebook」を購入してくれた方には無料アップデートという形で配布したいと考えています。
余談: なぜ今GatsbyJSなのか
なぜNext.jsの勢いがすごいなか、GatsbyJSの本を書くのかに対して疑問を持つ方もいると思います。
単純にNext.jsは他の人が書いてくれると思ったので、Gatsby本は自分が書くべきと思ったからです。
2020/12/24 - jsx-presentationを使ってJSXでpptxファイルを生成する
週末にpptxファイルが生成できるスライド作成ツールのプロトタイプを作りました。
スライドエディタで作成したスライドをPowerPoint形式で出力できるようにした。まだまだサイズやアスペクト比を一致させてない雑な作りだけど、動いたときはかなり嬉しかった。ただし、自分はKeynote派なのでPowerPointはあまり使う機会が… pic.twitter.com/2yec28lTnB— もっと@Gatsby本を改訂するぞ (@mottox2) June 7, 2020
ご覧の通り画面に対応したpptxファイルがダウンロードされていることがわかると思います。
このpptxファイル生成にはVercelのServerless Functionsを利用しており、jsx-presentationというライブラリを利用しています。
実際には次のようなコードで動作しています。(少々泥臭い感じなのは見逃してください。)
実際のコード
import React from "react";
import { NextApiRequest, NextApiResponse } from 'next'
import { generate, Presentation, Slide, Text, Shape } from "jsx-presentation";
import { Block } from '../../types/blocks'
export default async (req: NextApiRequest, res: NextApiResponse) => {
const blocks = req.body as Block[]
const content = await generatePptx(blocks)
res.statusCode = 200
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.presentationm')
res.send(content)
}
const scale = 0.3;
const generatePptx = (blocks: Block[]) => {
return generate(
<Presentation>
<Slide>
{blocks.map((block) => {
if (block.type === "text")
return (
<Text
fontSize={Math.round(block.fontSize * scale * 2)}
color="333333"
textAlign="left"
position="absolute"
width={100}
top={Math.round(block.top * scale)}
left={Math.round(block.left * scale)}
key={block.id}
>
{block.text}
</Text>
);
else if (block.type === "rect")
return (
<Shape
fill={block.backgroundColor.replace("#", "")}
position="absolute"
top={Math.round(block.top * scale)}
left={Math.round(block.left * scale)}
width={Math.round(block.width * scale)}
height={Math.round(block.height * scale)}
key={block.id}
/>
);
else throw "Invalid Block Exception";
})}
</Slide>
</Presentation>,
{ skipGenerate: true }
);
};
JSXで記述した結果がpptxに反映されています。jsx-presentationからインポートしたコンポーネントで記述しています。
つまりjsx-presentationを使えば、コード上からパワーポイントのプレゼンテーションファイルを生成することができます。そこで簡単に使い方を紹介します。
使い方
一番簡単な構成は次のようなコードです。yarn add jsx-presentationでインストールして次のようなindex.tsxを作成しましょう。
import React from "react";
import { generate, Presentation, Slide, Text } from "jsx-presentation";
generate(
<Presentation>
<Slide>
<Text fontSize={16} color='333333' textAlign='center' />
</Slide>
</Presentation>
);
覚えることとしては次の3つです。
generateの引数にはPresentationを親とするJSXを渡す
Presentationの子にはSlideを渡す
TextやShapeなどのpropsには(TSの型を参考に)CSS風の値を渡す
これらのルールを守りながら上記のファイルを実行しましょう。babelやtscなど実行方法は問いません。個人的にはts-nodeでやるのが楽だと思っています。
スターターを利用する
ただ、ドキュメントもほとんどない状況なのでちゃんとやっていくのは難しいでしょう。そこで利用するのがjsx-presentation-starterです。
このリポジトリには、ある程度コンポーネントが網羅された状況のファイル郡が用意されているのでそこから必要な部分だけを編集していく形になります。
ディレクトリ構造も次のようになっており、ある程度の肥大化を想定した形になっています。
* index.tsx
* components/
* PageHeader.tsx
* slides/
* Slide1.tsx
* Slide2.tsx
ぜひ、Reactの記法でスライドを書く体験をしてみてください。
2020/06/07 - Reactで寿司を回すタイピングゲームを作った
@dala00さんが企画している、1週間でテーマに沿ったWebサービスを作る #web1weekという企画に参加しました。今回は第二回の開催で「Like」というお題でした。
esaをより便利に使うための拡張機能「Refined esa」をつくった に引き続き、Webエンジニアの多くが好きな「寿司」をテーマにしたものを作ろうと思いました。
寿司といいえばタイピングです。また、先日ちょうど寿司を回すデモを作っていたので、タイピングで寿司を回すミニゲームにしました。ゲームを公開するのは人生で始めてです!
ゲームはこちら
ソースはこちら
遊び方
Enterを押すとゲームが開始します。画面下部に文字が表示されるので、20秒の制限時間内にできるだけ入力してください。
入力するとどんどん寿司の回転が早くなるので、できるだけ回してください。制限時間を使い切ると、回した回数が表示されます。できるだけ多く回すように頑張ってください。
技術的な話
寿司の3D空間はreact-three-fiberを使ったWebGL、それ以外は普通のReactアプリの構成で作っています。
react-three-fiberはthreejsをReactで扱うためのライブラリです。threejsはJavaScriptで3D表現を扱うためのライブラリでしたが、命令的な処理を記述する必要があります。これを宣言的な記述で扱えるのがreact-three-fiberの特長です。
例えば、今回のアプリケーションではBackgroundというコンポーネントを作っており、そのコンポーネント内で寿司を回すロジックを書いています。また、Reactで書いているので、他のコンポーネントで生成した値を3D表現にわたすのも非常に簡単です。
次のコードは作成したゲームのコンポーネントです。通常のReactコンポーネントと同じような扱いをしています。
const App = () => {
const mounted = useMounted()
const { mode, setMode } = useMode('title')
const [ stats, setStats ] = useState<Stats>(initialStats)
const [ rotation, setRotation ] = useState(0)
const onTimerEnd = useCallback(() => setMode('result'), [])
const restart = useCallback(() => setMode('title'), [])
return <div className={styles.container}>
{mounted && <Background count={stats.score} mode={mode} setRotation={setRotation} />}
{ mode === 'title' && <Title start={() => setMode('game')}/>}
{ mode === 'result' && <Result score={stats.score} miss={stats.miss} rotation={rotation} restart={restart} />}
{ mode === 'game' &&
<Game onEnd={onTimerEnd} onReset={() => setMode('title')} setStats={setStats} />
}
</div>
}
background.js
import React, { useRef, useState, useMemo, useEffect, Suspense } from 'react';
import { Canvas, useFrame, useThree, useResource } from 'react-three-fiber'
import { Vector3, PerspectiveCamera } from 'three';
const colors = ['#E43327', '#E6E02A']
export function Background({count, mode, setRotation}: any) {
const color = colors[count % 2]
return (
<div className='container'>
<Canvas shadowMap={true}>
<ambientLight />
<pointLight castShadow position={[0, 10, 20]} />
<gridHelper args={[300, 100, 0x888888, 0x888888]} position={[0, -0.65, 0]}/>
<mesh receiveShadow position={[0, -0.7, 0]}>
<boxBufferGeometry attach="geometry" args={[200, 0.1, 200]} />
<meshStandardMaterial attach="material" color={'#a0a0a0'} roughness={0.0} />
</mesh>
<Sushi setRotation={setRotation} mode={mode} position={[0, 0, 0]} speed={0.03 * count} scale={[1 + 0.18 * count,1,1]} color={color} />
<Camera />
</Canvas>
</div>
);
}
let rotation = 0
const Sushi = ({speed, color, mode, setRotation, ...props}) => {
const group = useRef<any>()
useEffect(() => {
if (mode === 'result') setRotation(rotation / 3.14 / 2)
rotation = 0
}, [mode])
useFrame(() => {
group.current.rotation.y += speed || 0.01
if (mode === 'game') rotation += speed || 0.01
})
return <group {...props} ref={group}>
<Box position={[0, 0.7, 0]} geometry={[2, 0.4, 1]} castShadow color={color} />
<Box position={[0, 0, 0]} geometry={[2, 1, 1]} castShadow color="#CCCCCC" />
</group>
}
const Box = (props: any) => {
return (
<mesh {...props}>
<boxBufferGeometry attach="geometry" args={props.geometry || [1, 1, 1]} />
<meshStandardMaterial attach="material" color={props.color || '#666600'} />
</mesh>
)
}
const Camera = (props: any) => {
const ref = useRef<PerspectiveCamera>()
const { setDefaultCamera } = useThree()
useEffect(() => {
if (!ref.current) return
setDefaultCamera(ref.current)
ref.current.lookAt(new Vector3(0, 0, 0))
}, [ref, setDefaultCamera])
return <perspectiveCamera position={[3, 3,3 ]} args={['90', window.innerWidth / window.innerHeight]} ref={ref} {...props} />
}
export default Background;
その他気にしたこと
デザイン
本当はリッチな背景を用意したり、寿司を流したかったのですが、3D表現を始めて1週間なので難しく諦めました。むしろ寿司ネタ以外をモノクロにすることでそれっぽくなるんじゃないかと思い余計な装飾が取り払いました。結果として、寿司が際立つインパクトのあるビジュアルになったと思います。
回して伸ばす
寿司を1つだけ3D空間に表示して回してみたところインパクトが足りませんでした。そこで(意味がわからないですが)回すついでに伸ばしてみたところ謎のシュールさが生まれたのでこのアイディアを採用しました。
ただ、最初のプレイでシュールさを感じてもらえるようにするのが難しく、ゲームバランスを考えるゲーム制作者の苦労を感じました。
やらなかったこと
途中でめんどくさくなって放棄した要素が結構多い
ミスのエフェクト
表示される言葉のバリエーション
レスポンシブ対応
ちゃんとしたゲームインターフェース
連続正解ボーナス
シェア文言を複数パターン用意
特定の点数を超えると文言が変化するやつ
感想
ゲームを作るのは始めてでしたが、気になる箇所を潰す間に飽きてしまいました。めちゃくちゃ高いクオリティでゲームを公開している開発者に敬意を持ちました。
3D表現という意味でもチャレンジングなことができて満足しています。こちらはもう少し練度をあげたいと思ったので、やっていきを継続します。
だらさん、楽しい企画をありがとうございました!
2020/05/24 - esaをより便利に使うための拡張機能「Refined esa」をつくった
@dala00さんが企画している、1週間でテーマに沿ったWebサービスを作る #web1weekという企画に参加しました。今回は第二回の開催で「Like」というお題でした。
自分は「自分の好きなWebサービスをより好きになるサービス」というコンセプトをもとにesaをより便利に使うための「Refined esa」というChrome拡張機能を作成しました。
Chromeストア
GitHub
できること
履歴機能
自分がひとりでesaを使っている感じではたいてい同じ記事をひたすら編集・見返す運用になっています。そういった運用に合わせて、閲覧履歴が簡単に見れると嬉しいので履歴機能を作りました。
実装としては、次の2ステップでした。
**.esa.io/posts/\d+$にマッチするURLであれば、DOMの中を見てtitleやcategoryなどの情報をストレージに保存。
履歴が開かれたタイミングでストレージにある情報を表示。
本当はショートカットキーの実装や、インクリメンタルサーチを作ろうと思いましたが時間的な制約から今回はスコープ外としました。
文字数カウント
自分はesa.ioをブログエディタとして使っています。ブログを書く際に文字数を基準にするタイミングがあるのですが、毎回コピペして文字数をカウントしていました。
そういったことから「テキストエディタに文字数カウントがあればいいのに」と思っていたので作りました。
コードとしては、Markdownのプレビュー内の変更を監視して、変更があれば文字数カウントを行い表示するだけなので簡単に実現できました。
できなかったこと
本当はエディタの文章を解析して、Lintによる文章修正や正規表現による置換を行いたかったのですが、実現ができませんでした。
Chrome拡張機能では「Content Scripts」という機能を使ってページを操作するのですが、同じDOMを操作することは出来るのですが、もともとページにあるScriptで定義された変数へのアクセスはできません。 参考
昔のesaエディタ(いわゆるv1のエディタ)であれば、標準のTextareaなのでvalueをいじれば触れたかもしれませんが、現状のesaエディタはCodeMirrorというエディタを採用して簡単にいじることができません。
window空間の変数にアクセスできればいろいろな操作が可能でした。例えば次のようなコードでは、JavaScriptを使って値の挿入が可能です。実際にページを開いてChrome Dev Toolsから動作を確認できます。
document.querySelector('.CodeMirror').CodeMirror.setValue('Hello CodeMirror')
これができれば機能の幅が広がったのですが、期間内に解決できそうになかったのでスルーしました。
エンジニアリング的な話
TypeScript対応
chrome-extension-cliを使ってコードを生成しましたが、初期状態ではTypeScriptが使えません。
tsc派とbabel派がいると思いますが、自分はbabel派なのでbabel-loader経由で変換を行いました。
加えて、chrome拡張機能ではchrome側のAPIを叩けるのですが、その型定義ファイルが@types/chromeで配布されているので利用しました。
yarn add -D @types/chrome
dom-chef
ReactやVueなどを使わずビューを生成するには通常次のようなコードを書くでしょう。
const div = document.createElement('div')
div.classList.add('hoge')
div.textContent('Hello World')
console.log(div) // => <div class='hoge'>Hello World</div>
ただ、ReactやVueに慣れてしまったフロントエンドの人からすると冗長な感は否めません。
しかし、拡張機能にReactやVue(あるいはjQuery)を入れると、拡張機能のためだけにこれらのライブラリを読み込むことになります。
そこで導入したのがdom-chefというライブラリです。dom-chefでは上記のコードと同じことが次のコードで実現できます。
import React from 'dom-chef'
const div = <div className='hoge'>Hello World</div>
見て分かる通りJSXです。ただし、実行されるのはReactではなくdom-chefになっています。深堀りするために、このJSXが変換された後のコードを見てみましょう。
import React from 'dom-chef'
const div = React.createElement('div', { className: 'hoge' }, 'Hello World')
import Reactとしていますが、実際はdom-chefのcreateElementメソッドが実行されます。
dom-chefはReactのAPIを模して作られており、dom-chefのcreateElementはざっくり次のような処理を行っています。
const createElement = (type, attributes, children) => {
const element = document.createElement('div')
// textの場合はtextNodeを生成しくっつける。
// 実際は他にArrayやNodeだった場合の処理もある
element.appendChild(document.createTextNode(children))
// attributes周りの処理を行っている。以下はclassの例
const existingClassname = element.getAttribute('class') ?? '';
setAttribute(element, 'class', (existingClassname + ' ' + String(value)).trim());
}
先程のコードと同じですね。dom-chefはJSX記法を用いて直接DOMを生成できるライブラリであることがわかると思います。
crx-hotreload
Chrome拡張機能ではコードを更新しただけでは変更が反映されず明示的に拡張機能をリロードする必要があります。しかし、crx-hotreloadというライブラリを入れるとディレクトリ内のファイルの変更を監視し、変更が検出されると、拡張機能をリロードしてくれます。
参考にしたもの
Chrome拡張機能「Refined GitHub」をかなり参考にしています。
2020/05/23 - Next.jsのStatic Generationを追う際に参考にしたページ
Next.jsのStatic Generationを追っておく際に見たページをまとめた。簡単に要点をまとめてはいるが、詳しくはリンク先のオリジナルを見てほしい。
2019 in Review
Guillermo Rauch(@rauchg) … Vercel社(元ZEIT)のCEO のブログ記事
2019年はJamstackが伸びた年であること
Staticは高速で安定性が高い
SSGやClient JS/APIでdynamicを実現できること
Next.jsはJamstackの機能を網羅している
Static GenerationとかAPI RoutesとかDynamic Routing
Vercelを使えば便利だよと等々
Static Generation / SSG Improvements
ページ単位でSSG(静的生成)とSSRを出し分けられるようにする。
Next.js 9になってデータを必要としないページはhtmlとして出力されていた。(いわゆるAutomatic Static Optimization)
データを必要とする場合は既存のexportを使うことでStaticにできたが、すべてのページが静的になってSSRが使えない。
また、getInitialPropsではクライアントでの遷移時にデータを取得する必要がありDBやCMSへのアクセスが発生していた。
これらを解決するためにgetStaticProps、getStaticPropsを導入し、静的なデータを扱えるようにした。
(サーバーサイドで実行されるgetServerSidePropsも追加された)
RFC: Incremental Static Regeneration
上記のStatic Generationが実装され、高速に低コストでウェブサイトを配信できるようになった。
欠点として、静的に生成されたページのデータ更新された場合、完全な再ビルドが必要だった。
このリビルドを避けて、オンデマンドで新しいページを生成するのがIncremental Static Regeneration
各ページにtimeoutを設定し、有効期限が切れると再レンダリングが走る。
Staticにツイートを表示するデモ https://static-tweet.now.sh/
Static Hoisting
Vercel社(元ZEIT) Guillermo Rauch氏のブログ記事
Hoisting(変数の巻き上げ)の説明し、Jamstackは計算結果を訪問者に巻き上げるものだと言っている。
CDNとJamstackの比較。JamstackはすべてのEdge Serverで共有される。
Next.jsはページ単位にアセットを生成するので静的な部分を巻き上げやすい構成になっている。
Dashboardのような一見動的な画面もClient JSでやるからデータの問い合わせをする静的ページ。
2020/05/11