これまでにカンファレンスなどで発表した資料をここにまとめます。 随時更新です。
最終更新: 2025-01-18
↓↓↓ 発表日降順で並んでいます
続きを読むCSS での色記法には様々な記法が認められています。
例えば私が好きなコーンフラワーブルーは、
cornflowerblue#6495edrgb(100, 149, 237)hsl(218.54, 79%, 66%)hwb(218.54 39% 7%)lab(61.2% 2.4% -40.2%)これらどの記法でも同じ色が表示されます。

mdn の <color> のページを見ると、他にも lch(), oklab(), oklch() などの記法があるようです。
とはいえプログラムから色を扱っていると「なんでもいいから RGB で教えてくれ」と思いますよね?
これらの色記法をどうにかして RGB 形式に変換してみます。
どの色記法であれブラウザが受け入れてくれてるということはブラウザの API で正規化できるってことです、たぶん。
手始めに適当な要素の style 属性に書き込んで、それを読み出してみます。
const div = document.createElement('div'); div.style.color = '#6495ed'; console.log(div.style.color); // => rgb(100, 149, 237) div.style.color = 'hsl(218.54, 79%, 66%)'; console.log(div.style.color); // => rgb(100, 149, 237)
やった!

が、このやり方では <named-color> には対応できません。
const div = document.createElement('div'); div.style.color = 'cornflowerblue'; console.log(div.style.color); // => cornflowerblue
ダメだ。もうちょっと頑張ってみましょう。
getComputedStyle() で読み出す (ボツ案)getComputedStyle() ってやつを使います。ブラウザの開発者ツールで見られる "Computed Style" を返してくれる関数です。
const div = document.createElement('div'); document.body.appendChild(div); div.style.color = '#6495ed'; console.log(getComputedStyle(div).color); // => rgb(100, 149, 237) div.style.color = 'cornflowerblue'; console.log(getComputedStyle(div).color); // => rgb(100, 149, 237)
やるじゃん!ちゃんと <named-color> に対応できました。
ただし document.body.appendChild(div) しているのでページに影響を与えます。丁寧にやるならこんな感じかな。
const div = document.createElement('div'); document.body.appendChild(div); div.style.display = 'none'; div.style.color = 'cornflowerblue'; console.log(getComputedStyle(div).color); // => rgb(100, 149, 237) document.body.removeChild(div);
実はこれでもまだ color() 関数記法 ってやつに対応していないんです。
const div = document.createElement('div'); document.body.appendChild(div); div.style.color = 'color(srgb 0.392 0.584 0.929)'; console.log(getComputedStyle(div).color); // => color(srgb 0.392 0.584 0.929)

おそらく色を決定するためのプロファイルが環境依存で、プロファイルを定めないと color() 記法と RGB の値が一意にならないからだろうね。
うーむ残念。次いきましょう。
1px × 1px の canvas を作って塗ったり読んだりします。
const canvas = document.createElement('canvas'); canvas.width = canvas.height = 1; const ctx = canvas.getContext('2d'); ctx.fillStyle = 'color(srgb 0.392 0.584 0.929)'; ctx.fillRect(0, 0, 1, 1); const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data; console.log(a === 255 ? `rgb(${r}, ${g}, ${b})` : `rgba(${r}, ${g}, ${b}, ${a / 255})`); // => rgb(100, 149, 237)
結局 canvas かー。
今回試した3つのやり方をそれぞれ関数化したものがここにあります。
どれも DOM API を前提としているのでブラウザ以外のランタイム (Node.js, Deno) などでは動きません。
あと、Chrome 以外のブラウザでの動作確認もしていません。
結局 canvas かー。style 属性でうまいこといけると思ったんだけどな。
私からは以上です。
JavaScript を使って画面上の要素を入れ替えたとき、スクロール位置が勝手に動いて画面がカクつくという現象に遭遇しました。調査して分かったことをまとめます。
カラフルな <div> が並んでいて、右上のボタンを押すとすぐ上の要素と位置が入れ替わるデモを作りました。

ここまではなんの変哲もありません。ところが、まったく同じコードでも要素が大きくなると違う挙動になります。

ボタンを押すと同時にスクロール位置がシフトしているのが分かります。背景に敷いてある「01, 02, 03, 04, ...」の数字に着目すると分かりやすいかも。
この記事ではどのような条件でスクロールシフトするのか調べてみます。
解決策だけ見たい人は スクロール調整 (スクロールアンカリング) を無効にする方法 を見てください。
デモのすべてのコードは GitHub に置いています。 → https://github.com/todays-mitsui/swap-element-lab
今回のデモは React で組んでいます。要素のレンダリングはのような感じです。
function App() { const [colors, setColors] = useState(['#ff6e61', '#4a8a98', '#fff2c2', ...]); return ( <> {colors.map((color, index) => ( <div style={{ backgroundColor: color }} key={color}> <h3>{color}</h3> <button type="button">↑</button> </div> ))} </> ); }
要素に背景色 (background-color) をつけているのは入れ替わりをわかりやすくするためで、スクロール位置がずれる挙動には直接関係ありません。
ここに要素入れ替えの処理を追加します。
const handleSwap = (i: number) => { const newColors = [...colors]; [newColors[i], newColors[i-1]] = [newColors[i-1], newColors[i]]; setColors(newColors); };
素朴に i 番目 と i-1 番目 を入れ替えてから setColors しているだけです。
二つを合わせるとこんな感じになります。
function App() { const [colors, setColors] = useState(['#ff6e61', '#4a8a98', '#fff2c2', ...]); const handleSwap = (i: number) => { const newColors = [...colors]; [newColors[i], newColors[i-1]] = [newColors[i-1], newColors[i]]; setColors(newColors); }; return ( <> {colors.map((color, index) => ( <div style={{ backgroundColor: color }} key={color}> <h3>{color}</h3> <button type="button" onClick={() => handleSwap(index)}>↑</button> </div> ))} </> ); }
いろいろと実験してみました。入れ替え後の要素の上端 (top) がビューポートに対してどの位置にあるかによってレイアウトシフトが 起こる/起こらない が変わるようです。
要素が高くてもスクロールシフトが起こらない場合があります。入れ替え後に要素がビューポートにおさまる位置にあればスクロールシフトは起こりません。
https://todays-mitsui.github.io/swap-element-lab/?height=500&scroll=2100

逆に、要素が低くても入れ替え後にビューポートの上端付近に来る場合にはレイアウトシフトが起こります。
入れ替え後にも要素が確実にビューポートに入るように調整される、と考えると分かりやすいです。

このように考えると理解しやすいですね。
先ほどまではボタン押下で上の要素と入れ替えるという動作のもと実験していました。これをボタン押下で 下の要素 と入れ替えるにすると、もっと理解しがたい挙動になります。
大きめの要素をすぐしたの要素と入れ替えます。入れ替え後に要素はビューポートの下のほうにわずかに見えるばかりですがスクロール調整は起こりません。
https://todays-mitsui.github.io/swap-element-lab/?height=500&scroll=2000&direction=down

元の要素を見失ってしまいそうなスクロールシフトが起こることもあります。

整理します。
このような挙動は スクロールアンカリング という機能によるものだそうです。この挙動を無効にする方法があります。
CSS の overflow-anchor プロパティ を使います。{ overflow-anchor: none; } を指定した要素は入れ替えによって位置が変わってもスクロール調整の対象になりません。
function App() {
const [colors, setColors] = useState(['#ff6e61', '#4a8a98', '#fff2c2', ...]);
return (
<>
{colors.map((color, index) => (
- <div style={{ backgroundColor: color }} key={color}>
- <div style={{ backgroundColor: color, overflowAnchor: "none" }} key={color}>
<h3>{color}</h3>
<button type="button">↑</button>
</div>
))}
</>
);
}
※ コード例示の都合で style タグを使っています。普通に CSS で設定すれば OK です
今回のデモでは「スクロール位置固定」というオプションに ✅ を入れることでこの挙動を体験できます。
https://todays-mitsui.github.io/swap-element-lab/?height=550&scroll=6000&fixed=true
こんな感じかなー。たぶん。
この記事の公開後すぐに id:nanto_vi さんから「それってスクロールアンカリングの影響では?」と情報をもらったので スクロール調整 (スクロールアンカリング) を無効にする方法 セクションを加筆修正しました。ズバリの情報ありがとうございました。
私からは以上です。
JavaScript から wasm の関数を呼び出して結果の文字列を受け取る方法を書き残します。
前編はこちら。
前後編のコードを全部まとめたリポジトリがここにあります。
まずは 前の記事 にも挙げた前提知識をおさらいしましょう。
詳しくはこちらを参照してください。
まずは wasm のコンパイル前のコードを見てみましょう。
今回は TinyGo で wasm をコンパイルします。なぜ TinyGo かと云うと //go:wasmimport ディレクティブが使えるからです。
wasm には文字列型が無いために Go から string をそのまま return することはできません。代わりにポインタとバイト列の長さの組みを返して、JavaScript 側で指定した領域の線形メモリの読むことで文字列を受け渡します。
ところが2025年1月現在は wasm にコンパイル可能な関数にかなりの制限があります。なんと複数の値を返すことができません。苦肉の策としてポインタを返す関数とバイト列の長さを返す関数を別々に定義します。
// 複数の関数 `returnString()`, `getBufSize()` で状態を共有するためのグローバル変数 var bufSize uint32 //go:wasmexport returnString func returnString() unsafe.Pointer { str := "やっほー From WebAssembly!🤟" // 文字列をバイト列に変換 buf := []byte(str) // バイト列のサイズを取得 // getBufSize() から参照できるようにグローバル変数に保存 bufSize = uint32(len(buf)) // バイト列の先頭のポインタを取得して返す // 本当は多値 `uint32(ptr), bufSize` を返したいが // 2025年1月現在 TinyGo は単一の uint32 しか返せない return unsafe.Pointer(&buf[0]) } //go:wasmexport getBufSize func getBufSize() uint32 { return bufSize }
returnString がポンタを返します。getBufSize はバイト列の長さを返します。
二つの関数の間で状態を共有するためにグローバル変数 bufSize を定義しておいて、都度書き込んだり読み出したりします。くそダサい実装ですね。仕方ない。
続いて JavaScript から文字列を読み出しましょう。
// wasm から文字列をメモリに書き込み、その開始位置のポインタを受け取る const ptr = wasm.exports.returnString(); // wasm から文字列の長さを受け取る const len = wasm.exports.getBufSize();
前述したとおり TinyGo では一度に複数の値を返せないので、二つの関数を立て続けに呼び出して1つずつ値を受け取ります。
ちなみにこれは wasm の制約ではなく TinyGo コンパイラの制約です。wasm では (result u32 u32) のようにして多値を返すことは可能です。
// wasm モジュールは線形メモリを exports.memory として露出している // このメモリをやりとりに使う const memory = new Uint8Array(wasm.exports.memory.buffer);
wasm モジュールは外界とデータをやりとりするために線形メモリを exports.memory としてモジュールの外に露出しています。このメモリから文字列を読み取りたいのでまずは Uint8Array に変換します。
// ptr, len で指定された範囲のバイト列を読み取る const utf8str: Uint8Array = memory.slice(ptr, ptr + len); // メモリには UTF-8 形式で書き込まれているので // デコードして JavaScript の文字列 (UTF-16 形式) に変換する return new TextDecoder().decode(utf8str);
memory のうち ptr ~ ptr + len の範囲を切り取り、TextDecoder でデコードします。
Go では文字列が UTF-8 形式でエンコードされています。一方で JavaScript では UTF-16 形式で文字列を扱っているためにエンコード変換が必要です。
デモコードは deno で実行することを想定して書きました。deno で実行してみます。
$ deno run --allow-read receiveStringFromWasm.ts
やっほー From WebAssembly!🤟
いい感じです。
今回のデモコードにはメモリの取り扱いの不備があります。
Go からローカル変数のポインタを返していますが、このメモリは Go の GC によって (JavaScript 側で参照する前に) 回収されてしまう恐れがあります。このあたりは Go の GC と wasm の仕様の噛み合わせが悪い部分ですね。
wasm から JavaScript へ文字列を受け取る方法を見てきました。
Go 側で多値を返せないせいでとてもダサいコードになっていましたね。今後に期待。
私からは以上です。