GODRICH
#cssアニメーション#ボタン#ローディング#トランジション#スクロール#ダッシュボード#実用ui#デザイントレンド

ワードクラウド 作り方 9種 — 渦巻き配置・形の中・3D球・リアルタイム投票

ワードクラウド 作り方の要は、文章の単語を数えて多く出た語ほど大きく描き、互いに重ならない場所を一枚の盤の上で探すことです。投票が一つ届くたびに単語の場所が全部入れ替わると、誰も読めない。

アンケートの要約からリアルタイム投票まで — 単語が居場所を見つける九つの画面

自動再生 · タイルを押すとその項目へ · すべて1つのzip

何も触らなければ、九つの盤はそれぞれ決まったシードでひとりでに動きます。01〜05は単語が一つずつ降りてくる過程を見せてから最初に戻り、06は最初の一周で泡が一つずつ膨らみながら押し合い、そのあとは6秒ごとに外へ弾かれてはまた集まります。07は自分で回る球がクリーム色の輪で示した仮のポインターのほうへ傾き、08は休まず風に揺れ続け、09は前もって抽選しておいた56票を125msごとに一票ずつ入れていきます。キャンバスに初めてポインターを載せた時点で自動再生の印が外れ、01〜05と08ではポインターの下の語に輪と吹き出し(05は「語 · 話題」、ほかは「語 · 回数」)が、06と09では吹き出しだけ(09は「語 · N票」)が、07では球の手前側の語にだけ吹き出しが出ます。

バッジを並べて大きさだけを変えたステータス バッジ UI 9選との違いは置き場所の決め方にあります。あちらのバッジは行に沿って順に流れていきますが、ここの九つの盤では、一語ずつがすでに置かれた文字と重ならない場所を計算で探し当てます。

自由回答の山を一枚の絵にまとめたいとき

アンケートの自由回答を集めて、何が多く書かれたかを一目で見せたいなら01から始めるのが近道です。回答の文をそのまま渡せば、語に切り分けて数え、大きさを付けるところまで一度に済むので、いま例の回答30件が入っている場所に実際の回答を差し込めば動きます。会議の振り返りで出た意見を系統ごとにまとめて見せたい場合は、話題ごとに色と基準点を変えた05のほうが向いています。

講演の画面に映す投票ボード

講演や授業の最中に聞き手の票を受けて映し出すなら09が合います。ボタンは日程表に用意した次の一票をcastVoteに渡し、この関数は票数を上げ、目標の大きさを付け直し、受け取った語を光らせ、checkLeaderで1位が変わったかを確かめるだけなので、実際の回答が届いたときに選択肢の番号でこの関数を呼ぶ部分を差し替えれば済むと考えられます。ワードクラウド 無料 リアルタイムの投票画面を、登録の要るサービスを通さず自分のページの中だけで動かしたい場合にも、この盤がそのまま土台になります。ただし選択肢の14語は最初から決まっていて(開始時の票は3・3・2・2・2に1票の語が九つ)、自由入力で新しい語が増える盤にするなら、01のように数える段階を前に付ける必要があります。

ワードクラウドとは何か — なぜ大きな単語から先に場所を決めるのか?

文章の中で多く使われた語ほど大きな文字で描き、一枚に寄せ集めた図がワードクラウドです。九つのうち「多く出た語から置く」順番を守るのは01〜05と08です。07は球の上の席番号をシードで混ぜて大きさと関係なく散らし、06は泡を一つずつ膨らませてから力で押して整え、09は選択肢の場所を守る側なので、この順番には従いません。

空の盤なら最初の一歩で場所が空いている

最初に置く語は盤が空なので、渦巻きの一歩目でそのまま場所が決まります。02を測ると、最初の語は1歩、続く四語は139・154・112・162歩でした。大きな語を後回しにすると、散らばった小さな文字のあいだに50pxの文字が入るすき間が残らず、渦巻きを長く回るか0.85倍ずつ縮むしかなくなると考えられます。大きさで回数を表す図で1位の語が縮んでしまえば、その図は順位を取り違えて伝えることになります。

小さな語は残ったすき間を埋める

03はこの順番を最後まで押し通します。前半の48語を置いたあと、同じ語を11・10・9・8pxで書き直して大きな文字のすき間を埋め、盤がいっぱいになった153語で止まります。04も深いマスから順に座らせるので、最初の語がいちばん深い106.7pxの位置に収まり、ハートの縁へ行くほど小さな語が残ります。

大きさは面積で、場所は渦巻きで — 回数を平方根で縮め、ビットのマス目の重なりで空きを探す計算

回数が2倍なら文字の面積も2倍

01〜05・07・08の大きさはsizeForの一行で決まります(03〜05・07・08はそこへ盤ごとの倍率を掛け、07は×0.62)。06と09はsizeForを定義だけして、自分の平方根の式で大きさを決めます。大きさ=50×(回数÷1位の回数)^0.5で、10pxより下には下がりません。文字が占める面積は大きさの2乗に比例するので、指数が0.5なら面積が回数に比例します。01の16語は12回で50px、6回で35.4px、3回で25px、1回で14.4pxとばらばらですが、大きさ²÷回数を計算すると16語すべて208.3で一致しました。指数を1にすると文字の高さが回数に比例し、面積は回数の2乗で増えるので、1位が実際の票差よりずっと先を行くように見えてしまいます。

32画素を整数一つに詰めるビットのマス目

重なりは文字の箱ではなく、文字の形そのもので判定します。makeSpriteが小さなキャンバスに語を描き、getImageDataでアルファが40を超えた画素を1として記録し、周囲3×3まで塗って文字どうしのあいだに1pxのすき間を残します。一行の画素32個が32ビット整数一つに収まるので、480pxの盤は一行に整数17個(480÷32=15に余裕2)、それが300行です。語をxの位置に当ててみるときは、整数をox & 31(32で割った余り)だけ右へずらし、前の整数からあふれたビットをつないで盤の整数とANDを取ります。結果が0でなければ、どこかで一画素でも重なったということです。

        bitTests += 1;
        const s = ox & 31;
        const base = ox >> 5;
        const inv = 32 - s;
        for (let r = 0; r < sp.h; r += 1) {
            const row = (oy + r) * BOARD_INTS + base;
            const sr = r * sp.mw;
            let carry = 0;
            for (let k = 0; k < sp.mw; k += 1) {
                const v = sp.bits[sr + k];
                const part = s === 0 ? v : ((v >>> s) | carry);
                if ((board[row + k] & part) !== 0) {
                    return true;
                }
                carry = s === 0 ? 0 : (v << inv);
            }
            if (carry !== 0 && (board[row + sp.mw] & carry) !== 0) {
                return true;
            }
        }

整数一つのANDで画素32個を一度に見ている計算になるので、箱の中の画素を一つずつ比べる場合に比べて、比較の回数は計算上32分の1に減ります。02では渦巻きのすべての歩みがこの判定を一回ずつ通り、ko版の語セットで9,007歩・判定9,007回と一致しました。

渦巻き一周で半径が21.99px広がる

場所の候補は、中心から外へ回っていく渦巻きの上の点です。i歩目の角度はi×0.1ラジアン、半径はi×0.35pxで、横を1.6倍に引き伸ばして横長の盤に合わせます。一周が62.8歩なので半径は一周ごとに21.99pxずつ広がり、盤の対角線の半分に届く809歩を回り切っても空きがなければ、その大きさでは置ける場所がないという意味になります。

ワードクラウド 作り方 9種 — 頻度で大きさ・渦巻き配置・マス分割の当たり判定・ハートの中・話題の群れ・単語の泡・3D球・風の揺れ・リアルタイム投票

九つの盤はキャンバス2Dと素のスクリプトだけで描いています。ワードクラウド javascriptの中身を最初から最後まで読み通せるよう、ライブラリ・WebGL・CDN・画像ファイルは使わず、乱数が要る06・07・08・09はシードを受け取るmakeRngから値を取り出すので、何度開いても同じ場面になります。フレームのコールバック(requestAnimationFrame)が届くたびに経過時間をため、16ms単位で場面を進め、タブを長く離れて戻っても1フレームで6単位を超えてまとめて計算することはありません。CSSのキーフレームもタイマーも一つも使っていません。

2秒を24枚に撮って比べた動きの量

盤ごとに2秒を24枚で撮り、隣り合う2枚の差を23回求めました。累積面積は、そのあいだに一度でも変わった画素が盤に占める割合です。

盤 累積面積 動いたフレーム 強度の平均
01 文章を数える単語盤 15.3% 18/23 84.3
02 渦巻き配置 18.5% 23/23 73.2
03 マス分割の当たり判定 63.0% 23/23 46.2
04 ハートの中 20.3% 22/23 61.1
05 話題の群れ 18.2% 23/23 71.3
06 単語の泡 44.1% 23/23 92.2
07 3Dの単語の球 31.1% 23/23 69.5
08 風の揺れ 29.7% 23/23 33.4
09 リアルタイム投票 10.1% 22/23 76.7

03が63.0%でいちばん広いのは、語の数が変わるたびにマスの境界を描き直し、置いたばかりの語がかかるマスを空色で塗るためと考えられます。09が10.1%でいちばん狭いのは、票を受けた語とよける隣だけが動くためと考えられます。01の18/23と04・09の22/23は、語が置き終わったあとや票と票のあいだで画面が一瞬止まった場面が撮られたものと考えられます。この記事の配置にかかわる数値(歩数・語数・判定回数・ピクセル)は、とくに断りがなければko版の語セットで測った値です。

01文章を数えて面積で大きさを決める単語盤

例の回答30件を空白・読点・句点・疑問符といった区切り文字で切って75語を数えると16種類が残り、多く出た語から105msおきに飛び出して頭の上に「N回」の札を浮かべます。アンケートの自由回答をそのまま受け取りたいときの盤で、英字は小文字にそろえて同じ語として数え、回数が同じなら先に出た語が前に並びます。この ja 版の回答は「、」でつないだ文で、数え上げた回数はko版と同じ12・9・8・7・6回から始まります。

文章を数えて面積で大きさを決める単語盤
新しいタブで開く

01 · 文章を数えて面積で大きさを決める単語盤 — 計測 累積面積 15.29% · 動いたフレーム 18/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SIZE_EXPONENTSPLIT_PATTERNAPPEAR_GAP_MS
        const counts = new Map();
        const order = [];
        let tokens = 0;
        for (let i = 0; i < lines.length; i += 1) {
            const parts = lines[i].split(SPLIT_PATTERN);
            for (let k = 0; k < parts.length; k += 1) {
                const w = parts[k].trim().toLowerCase();
                if (!w) {
                    continue;
                }
                tokens += 1;
                const c = counts.get(w);
                if (c === undefined) {
                    order.push(w);
                    counts.set(w, 1);
                }
                else {
                    counts.set(w, c + 1);
                }
            }
        }
        const entries = order.map(function (w, i) { return { text: w, count: counts.get(w) || 0, theme: i }; });
        entries.sort(function (a, b) { return b.count - a.count || a.theme - b.theme; });
        return { entries: entries, tokens: tokens };

02渦巻きをたどって空きを探す配置

前半の36語が中心(240, 146)から渦巻きを一歩ずつ踏み、一歩ごとに文字のビットのマスを盤に当てて、最初に空いた場所へ降ります。通った軌跡を薄く残し、いま測っている文字の箱をピンクの点線で見せるので、ブログのタグ一覧画面で配置がどう決まるかを説明するときにそのまま使えます。36語にかかった歩数はko版の語セットで9,007歩(1語あたり平均250.2、最長325)、この ja 版の語では8,890歩で、盤の外で落とされた歩みはありませんでした。

渦巻きをたどって空きを探す配置
新しいタブで開く

02 · 渦巻きをたどって空きを探す配置 — 計測 累積面積 18.528% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SPIRAL_STEPSPIRAL_TURNBOARD_INTS
    function spiralAt(i) {
        const a = i * SPIRAL_TURN;
        const r = i * SPIRAL_STEP;
        return [Math.cos(a) * r * SPIRAL_ASPECT, Math.sin(a) * r];
    }
    // 渦巻き探索 — 基準点 (cx, cy) から渦巻きを一歩ずつ進め、文字の箱の中心をその点に置いて、ビットのマスが重ならない最初の位置を返す。
    // trail を渡すと、通った渦巻きの点をすべて記録する(02 がその軌跡を描く)
    function searchSpiral(board, sp, cx, cy, trail) {
        for (let i = 0; i < SPIRAL_LIMIT; i += 1) {
            spiralSteps += 1;
            const p = spiralAt(i);
            const ox = Math.round(cx + p[0] - sp.w / 2);
            const oy = Math.round(cy + p[1] - sp.h / 2);
            if (trail) {
                trail.push([cx + p[0], cy + p[1]]);
            }
            if (!collides(board, sp, ox, oy)) {
                return [ox, oy];
            }
        }
        return null;
    }

03マスを四つずつ割って速く探す当たり判定

盤を四つのマスに分け、マスの中の語が四つを超えたらそのマスをまた四つに割り(深さ6未満のとき)、新しい語の箱は自分がかかるマスの中の箱とだけ比べます。コメントのキーワード集計画面のように小さな語が数百に増える盤のための方式で、下の帯がマス側の判定回数と、一つずつ全部比べた場合の回数を並べて数えます。153語を置くあいだの判定は140,332回、全部比べていれば2,301,880回で16.4倍の差があり、末端のマスは187個、いちばん深いマスは5段でした。

マスを四つずつ割って速く探す当たり判定
新しいタブで開く

03 · マスを四つずつ割って速く探す当たり判定 — 計測 累積面積 63.024% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

QUAD_CAPACITYQUAD_MAX_DEPTHFILL_SIZES
    function quadHits(n, b, all) {
        if (!boxesTouch(n, b)) {
            return false;
        }
        if (n.kids) {
            for (let k = 0; k < 4; k += 1) {
                if (quadHits(n.kids[k], b, all)) {
                    return true;
                }
            }
            return false;
        }
        for (let i = 0; i < n.ids.length; i += 1) {
            const id = n.ids[i];
            if (marks[id] === markStamp) {
                continue;
            }
            marks[id] = markStamp;
            treeChecks += 1;
            if (boxesTouch(all[id], b)) {
                return true;
            }
        }
        return false;
    }

04ハートの形の中を深さの順に埋める

4pxのマス9,000個のうちハートの内側3,516マスについて、縁までの距離を二度なぞって前もって測り(隣は+3・斜めは+4で比べ、最後に4/3を掛けてpxに直す)、深いマスから大きな語を座らせます。ハートの外の画素を先に盤に塗っておくので一つの重なり判定だけで形の中に収まり、記念日やお礼メッセージのポスターに使うなら、insideHeartの式を別の形に替えれば済むと考えられます。いちばん深い所は106.7pxで、前半の48語に埋め草18語を足した66語で形がいっぱいになり、深さが足りない後ろのほうの大きな語は9.2pxまで縮みました。

ハートの形の中を深さの順に埋める
新しいタブで開く

04 · ハートの形の中を深さの順に埋める — 計測 累積面積 20.323% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

HEART_SCALEDEPTH_CELLSHRINK_RATIO
        // 盤の外は外側(0)とみなす
        const at = function (i, j) { return i < 0 || j < 0 || i >= GW || j >= GH ? 0 : d[j * GW + i]; };
        for (let j = 0; j < GH; j += 1) {
            for (let i = 0; i < GW; i += 1) {
                const c = j * GW + i;
                if (d[c] === 0) {
                    continue;
                }
                d[c] = Math.min(d[c], at(i - 1, j) + 3, at(i, j - 1) + 3, at(i - 1, j - 1) + 4, at(i + 1, j - 1) + 4);
            }
        }
        for (let j = GH - 1; j >= 0; j -= 1) {
            for (let i = GW - 1; i >= 0; i -= 1) {
                const c = j * GW + i;
                if (d[c] === 0) {
                    continue;
                }
                d[c] = Math.min(d[c], at(i + 1, j) + 3, at(i, j + 1) + 3, at(i + 1, j + 1) + 4, at(i - 1, j + 1) + 4);
            }
        }
        for (let c = 0; c < d.length; c += 1) {
            d[c] = d[c] * DEPTH_CELL / 3;
        }

05話題ごとに群れを作る色の雲

語ごとに付いた話題四つ(仕事のペース・いっしょに・働き方・学びと成長)に基準点を一つずつ決め、一枚の盤を共有しながら、それぞれの語が自分の話題の基準点から渦巻きを始めます。会議の振り返り意見の分類のように、何が多かったかとどの系統だったかを同時に見せたいときに向き、四隅の名札の場所を先にふさいでおいて、置いたばかりの語から基準点まで糸を450ms見せます。48語が話題ごとに12・12・13・11語ずつ、縮むことなくすべて収まり、話題ごとの最初の語は基準点から0〜1px以内に座りました。

話題ごとに群れを作る色の雲
新しいタブで開く

05 · 話題ごとに群れを作る色の雲 — 計測 累積面積 18.234% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

THEME_ANCHORSTHEME_COLORSLINK_MS
        for (let i = 0; i < THEME_LABELS.length; i += 1) {
            const b = labelBox(i);
            labels.push(b);
            blockRect(board, b[0] - 2, b[1] - 2, b[2] + 4, b[3] + 4);
        }
        placed = [];
        for (let i = 0; i < VOCAB.length; i += 1) {
            const e = VOCAB[i];
            const a = THEME_ANCHORS[e.theme];
            const p = placeEntry(board, e, sizeFor(e.count, top) * SIZE_SCALE, a[0], a[1], THEME_COLORS[e.theme], null);
            if (p) {
                placed.push(p);
            }
        }

06単語の泡が押し合って落ち着く配置

泡22個の半径を回数の平方根に比例させ(50pxからいちばん小さい20.4pxまで)、ティックごとに中心へ引き寄せたあと、半径の和に3px(BUBBLE_GAP)を足した距離より近い二つの泡を足りない距離の0.45倍ずつ三回押して離します。相手の面積の割合で押す量を分けるので小さな泡ほど大きくよけ、トレンドキーワードの順位ボードなら上位の大きな泡ほど押されずに中央へ残ると考えられます。重なった距離の合計は一つずつ膨らむあいだの1,504msに143.6pxで最大になり、膨らみ終わると約25pxまで下がりました(膨らむのは最初の一周だけで、どの周も3,000msの時点で外へ弾かれてから集まり直します)。

単語の泡が押し合って落ち着く配置
新しいタブで開く

06 · 単語の泡が押し合って落ち着く配置 — 計測 累積面積 44.077% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PULLPUSHRELAX_PASSES
        for (let i = 0; i < bubbles.length; i += 1) {
            const b = bubbles[i];
            b.vx = (b.vx + (W / 2 - b.x) * PULL) * DAMPING;
            b.vy = (b.vy + (H / 2 - b.y) * PULL * PULL_Y) * DAMPING;
            b.x += b.vx;
            b.y += b.vy;
            b.press = 0;
        }
                    const dx = b.x - a.x;
                    const dy = b.y - a.y;
                    const d = Math.hypot(dx, dy) || 0.01;
                    const gap = ra + rb + BUBBLE_GAP - d;
                    if (gap <= 0) {
                        continue;
                    }
                    if (pass === 0) {
                        overlapSum += gap;
                        a.press += gap;
                        b.press += gap;
                    }
                    const nx = dx / d;
                    const ny = dy / d;
                    // 小さい泡ほど大きくよける — 自分の取り分 = 相手の面積 / 二つの面積の和
                    const share = (rb * rb) / (ra * ra + rb * rb);
                    a.x -= nx * gap * PUSH * share;
                    a.y -= ny * gap * PUSH * share;
                    b.x += nx * gap * PUSH * (1 - share);
                    b.y += ny * gap * PUSH * (1 - share);

07球の形で回る3Dの単語の球

42語の高さを均等に分け、経度は一つごとに黄金角137.51°ずつ回して球の上に散らすと、いちばん近い隣との距離が半径1に対して0.477〜0.525にそろいます。毎秒31.5°で回しながら、倍率3.2÷(3.2+z)で手前の文字は1.455倍、奥は0.762倍で描き、遠いものから塗って手前の文字が奥の文字を覆うようにします。ポートフォリオの技術スタック紹介のように語が数十個に決まった場所に合い、ポインターを載せると仮のポインターの代わりに実際のポインターのほうへ最大34.4°まで傾き、手前の語に重ねると輪は出さずに吹き出しだけを出します。

球の形で回る3Dの単語の球
新しいタブで開く

07 · 球の形で回る3Dの単語の球 — 計測 累積面積 31.059% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GOLDEN_ANGLECAMERA_DISTPOINTER_SCHEDULE
    function spherePoint(i, n) {
        const y = 1 - (i + 0.5) * 2 / n;
        const r = Math.sqrt(Math.max(0, 1 - y * y));
        const phi = i * GOLDEN_ANGLE;
        return [Math.cos(phi) * r, y, Math.sin(phi) * r];
    }
    function project(ux, uy, uz) {
        const a = yaw + tiltY;
        const x1 = ux * Math.cos(a) - uz * Math.sin(a);
        const z1 = ux * Math.sin(a) + uz * Math.cos(a);
        const y2 = uy * Math.cos(tiltX) - z1 * Math.sin(tiltX);
        const z2 = uy * Math.sin(tiltX) + z1 * Math.cos(tiltX);
        const k = CAMERA_DIST / (CAMERA_DIST + z2);
        return [W / 2 + x1 * SPHERE_R * k, H / 2 - y2 * SPHERE_R * k, z2, k];
    }

08風にそよぐ単語の背景

渦巻きで置いた40語の場所はそのままにして、語の中心の座標で2Dの値ノイズを読んで傾きと浮き上がりを決め、ノイズの面を時間とともに横へずらして風の流れのようにつなげます。突風の帯は秒速190pxで左から右へ一度通り抜けるのに3,789msかかり、帯がかかった語は大きく揺れてクリーム色の側へ明るくなるので、ランディングページのヒーロー背景に敷くのに向いています。4秒間で40語を測った最大の傾きは8.6°、最大の浮き上がりは5.7pxでした。

風にそよぐ単語の背景
新しいタブで開く

08 · 風にそよぐ単語の背景 — 計測 累積面積 29.658% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

NOISE_SCALESWAY_MAXGUST_SPEED
    function noise2(x, y) {
        const ix = Math.floor(x);
        const iy = Math.floor(y);
        const fx = x - ix;
        const fy = y - iy;
        const u = fx * fx * (3 - 2 * fx);
        const v = fy * fy * (3 - 2 * fy);
        return lerp(lerp(lattice(ix, iy), lattice(ix + 1, iy), u), lerp(lattice(ix, iy + 1), lattice(ix + 1, iy + 1), u), v);
    }
            const gust = gustAt(cx, ts);
            const g = gust * calm;
            const n1 = noise2(cx * NOISE_SCALE - ts * WIND_SPEED, cy * NOISE_SCALE);
            const n2 = noise2(cx * NOISE_SCALE + 17.3, cy * NOISE_SCALE - ts * WIND_SPEED * 0.8);
            // 突風が通る語はクリーム色の側へ少し明るくなる
            const glow = calm * (gust - GUST_BASE) / (1 - GUST_BASE);
            drawPlaced(p, 1, 1, n1 * SWAY_MAX * g, (n2 * 0.5 + 0.5) * LIFT_MAX * g, mixRGB(p.color, CREAM, 0.35 * clamp(glow, 0, 1)));

09投票が届くたびに育つリアルタイム単語盤

選択肢の14語が票を受けるたびに目標の大きさを付け直し、いまの大きさはティックごとに残りの差の12%ずつ近づいて大きくなります。大きくなった箱が隣と重なれば、票の少ないほう(同じなら後ろの番号)だけが渦巻き700歩以内で最初に出会う空きへ目標を移してばねで追いかけ、講演や授業のリアルタイム投票画面でもほかの語は自分の場所を守ります。日程表の一周で1位は304msに対話、2,304msに締め切り、2,560msに協力、3,312msに締め切りと4回入れ替わり、そのあいだによけた回数は54回で、盤全体を並べ直したことは一度もありません。

投票が届くたびに育つリアルタイム単語盤
新しいタブで開く

09 · 投票が届くたびに育つリアルタイム単語盤 — 計測 累積面積 10.128% · 動いたフレーム 22/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

REFLOW_STIFFNESSSIZE_LERPVOTE_GAP_MS
    function relocate(i) {
        const o = options[i];
        for (let s = 1; s < SEARCH_LIMIT; s += 1) {
            const p = spiralAt(s);
            const r = rectOf(o, o.tx + p[0], o.ty + p[1], o.goal);
            if (fits(i, r)) {
                o.tx += p[0];
                o.ty += p[1];
                moves += 1;
                return true;
            }
        }
        return false;
    }
        for (let i = 0; i < options.length; i += 1) {
            for (let j = i + 1; j < options.length; j += 1) {
                const a = options[i];
                const b = options[j];
                if (!touch(rectOf(a, a.tx, a.ty, a.goal), rectOf(b, b.tx, b.ty, b.goal))) {
                    continue;
                }
                relocate(b.votes > a.votes ? i : j);
            }
        }
        for (let i = 0; i < options.length; i += 1) {
            const o = options[i];
            o.vx = (o.vx + (o.tx - o.x) * REFLOW_STIFFNESS) * REFLOW_DAMPING;
            o.vy = (o.vy + (o.ty - o.y) * REFLOW_STIFFNESS) * REFLOW_DAMPING;
            o.x += o.vx;
            o.y += o.vy;
        }

ワードクラウドの配置が壊れる三つの場面 — 盤の端にかかる長い単語・遅れて読み込まれるフォント・数百に増えた単語

盤の端にかかる長い単語

渦巻きは横に伸びたまま盤の対角線の半分まで回るので、候補の多くは盤の縁にかかるか外へはみ出します。collidesが、箱が盤の外へ一画素でも出たら重なったとみなすのはそのためで、この判定がなければ長い語が盤の端で切れたまま置かれます。最後まで場所が見つからなかったときはplaceEntryが0.85倍に縮めて最初から探し直し、8pxを下回ったら捨てます。

    function placeEntry(board, e, size, cx, cy, color, trail) {
        let s = size;
        let shrinks = 0;
        while (s >= SIZE_FLOOR) {
            const sp = makeSprite(e.text, s);
            if (trail) {
                trail.length = 0;
            }
            const at = searchSpiral(board, sp, cx, cy, trail);
            if (at) {
                stamp(board, sp, at[0], at[1]);
                return { text: e.text, count: e.count, theme: e.theme, size: s, x: at[0], y: at[1], w: sp.w, h: sp.h, dx: sp.dx, dy: sp.dy, color: color, shrinks: shrinks };
            }
            s *= SHRINK_RATIO;
            shrinks += 1;
            shrinkCount += 1;
        }
        dropCount += 1;
        return null;
    }

空の盤の中心にko版の語を三つつないだ長い語句を90pxで置いてみると、5回縮んで39.9px・幅461pxでやっと収まり、en版の「collaboration」は1回縮んで76.5px・幅478pxに置かれました。50pxで置いた「collaboration」は縮まずに幅314pxのままです。デモの標準の配置では01・02・05・08が一度も縮まず、04だけがハートの深さが足りずに後ろのほうの大きな語が9.2pxまで縮みました。

遅れて読み込まれるフォント

ビットのマスは語を一度描いて作った絵なので、マスを作ったあとでフォントが変わると、画面の文字とマスがずれます。同じ語を50pxで、デモのフォントと代わりのセリフ体で測り比べると、ko版の短い語では1px前後の差でしたが、en版の「collaboration」は314pxと324px、この ja 版の「フィードバック」は332pxと343pxで、10〜11px開きました。そこで九つの盤はすべてdocument.fonts.readyが済んでから配置を始め、そのあと新しいフォントが入ってloadingdoneが届けば文字の幅を測り直して配置を作り直します。デモのフォントは端末に入っているフォント一覧なのでダウンロードするフォントはありませんが、ウェブフォントを読み込んで使うなら、この順番がいっそう効いてくると考えられます。

数百に増えた単語

一つずつ全部比べるやり方では、置かれた語が増えるほど新しい一語にかかる比較も増えていきます。03で153語を置いたときはマス分割の側が140,332回、全部比べる側が2,301,880回(16.4倍)でしたが、大きさを縮めて238語まで詰めると473,909回対17,821,535回で37.6倍になりました。語が1.6倍に増えるあいだに差は2倍を超えて開いた計算なので、数百語を扱う盤なら、03のマス分割や02のビットのマスのように比べる相手を減らす仕組みを先に入れるのが順当です。

この三つの落とし穴を防ぐ判定が入った九つの盤と、同じソースから作ったReact版は一つの圧縮ファイルにまとめてあり、開くときにp49vy6meを入力すれば中身を取り出せます。

渦巻きを細かく、大小の差を大きく、ばねをやわらかく — 値を一つ変えた三つの版

名前 変えた値 印象
渦巻きをもっと細かく 02 SPIRAL_STEP 0.35 → 0.15 一周で広がる半径が21.99pxから9.42pxに減って歩数が9,007から19,547に増え、同じ文字が383×221pxではなく360×211pxの中に集まり、外枠の中で文字が占める割合が38.9%から43.3%に上がる
大小の差をもっと大きく 01 SIZE_EXPONENT 0.5 → 0.8 1位の箱の取り分が13.5%から19.0%に増え、1回の語は14.4pxから下限の10pxまで下がり、塗られた画素は20,224から14,534に減る。16語がすべて収まるのは同じ
ばねをもっとやわらかく 09 REFLOW_STIFFNESS 0.18 → 0.08 目標を40px動かしたとき±0.5px以内に初めて入る時刻は224msから384msに遅れる代わりに、行き過ぎが9.52pxから3.15pxに減り、一度入ったら出ないので最後まで留まり始める時刻は432msから384msに早まる

三つの版が失うものはそれぞれ違います。02は細かくした分だけ自動再生の一周が12.0秒から17.8秒に伸び、01の0.8は面積の比例が崩れて大きさ²÷回数が1位の208.3から最後の71.1まで開くので、1位が実際の回数差より大きく見えます。09は最初の反応が遅れる代わりに揺れが小さいので、票がまとまって届く投票画面なら0.08のほうが読みやすいと考えられます。

アクセシビリティ — 読み上げで追える頻度順の一覧と、動きを減らす設定での止まった配置

キャンバスの横に隠した頻度順の一覧

キャンバスに描いた文字はスクリーンリーダーが読めないので、九つの盤はすべて横に目に見えない<ol>の一覧を置き、「語 — 12回」の形で順位どおりに書き出します。1pxの箱にclip-path: inset(50%)をかけた隠し方なので、display: noneと違って読み上げには残り、順位や回数が変わったときだけ<li>を書き直します。一覧の長さは01が16語、02が36語、03〜05が48語、06が22語、07が42語、08が40語、09が14語です。キャンバス自体にはrole="img"と、場面の説明に「いちばん多い言葉:」の1〜3位を添えたaria-labelを付け、順位が変われば一緒に直します(09は「いま票が多い順:」)。

動きを減らす設定で描く最後の一場面

動きを減らす設定(prefers-reduced-motion)が有効なら、九つの盤はすべてフレームのループを始めず、配置を最後まで計算した最後の一場面だけを描きます。01〜05はすべての語が場所に収まった画面、06は画面に描かずに600ティック回して落ち着かせた泡のかたまり、07は0.7ラジアンで傾かない球、08は風がやんだ盤、09は56票を入れ切った最終集計(締め切り16票・協力12票・対話12票・フィードバック8票)です。ページを開いたまま設定を切り替えてもchangeのリスナーがすぐに反映し、ポインターの吹き出しと09のボタンはこの状態でも使えますが、07の球はループが回らないのでポインターのほうへは傾きません。

1位が変わったときだけ知らせる09の通知行

09の通知行は自動再生のあいだaria-live="off"にして125msごとに届く票を読み上げ続けないようにし、人が初めてポインターを載せるかボタンを押した時点でpoliteに切り替えます。そのあとも一票ごとではなく、1位が変わったときだけ「1位が入れ替わりました: 締め切り」のような文に書き換えます。ボタンは本物の<button>なのでTabでたどり着いてEnterで一票ずつ入れられ、フォーカスの輪郭は2pxです。

会議の投票画面やレポートにワードクラウドを載せるときのよくある質問

ワードクラウド 作り方 無料のこのコードで、画像に保存してレポートに貼れますか?

デモには保存の仕組みがないので、自分で付け足す作業になります。動きを減らす設定のときと同じようにfinalScene()で配置し終えた場面を描き、そのキャンバスを画像として書き出す処理を足せば一枚が得られ、キャンバスの実際の画素数は表示サイズに端末の倍率(上限2)を掛けた分です。数値が大事なレポートなら、隠した頻度順の一覧を表に移して図の横に並べておくほうが、読む人には正確に伝わります。

回数が同じなのに大きく見える語があるのはなぜですか?

大きさが決めているのは文字のサイズなので、字数の多い語は同じサイズでも箱が広くなります。01の同じ回答をko版からen版の語に替えると、回数はそのままなのに塗られた画素が20,224から35,109に増えました。面積を回数にぴったり合わせたいなら箱の面積から大きさを逆算する必要がありますが、このデモは文字のサイズのほうを基準にしています。

語がいくつまでなら渦巻きとビットのマスだけで足りますか?

02のように数十語を置く盤なら、36語に渦巻き9,007歩で片付きます。03で測ったように語が153から238に増えると、一つずつ比べる手間が230万回から1,782万回へ跳ね上がるので、百語を超える盤にはマス分割を付けるほうが無難です。点と線でつないだグラフを引き寄せと反発で並べる方向はネットワークグラフ9種で、同じダッシュボードの流れの棒グラフは棒グラフ アニメーション 9種で続けて読めます。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。