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

一筆書き イラスト 9種 — 点の順番と線の長さを計算して描くキャンバス

一筆書き イラストは、紙からペンを一度も離さずに顔や花や名前を一本の線で描いた絵です。いちばん近い点から順につなぐと、線は自分を何度も横切ります。そこで04番は交差した二区間を裏返して線を短くし、03番は点を暗さの重心へ動かし、07番は奇数の分かれ目を数えて一度で描く順番を探します。

線画 アニメーション 作り方を探すと見つかる一本線の絵は、何を計算して描かれているのでしょうか?

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

絵を一本の線に移すには、三つのことを順に決めます。線の長さをどこに多く使うか、その場所をどの順番で通るか、その順番をどれくらいの速さで描くかです。九つの場面は、このうち一つを深く掘り下げるか、いくつかを一続きにつないで見せます。

決めること 場面 計算するもの ここで測った値
どこに 01 · 02 · 03 マスの暗さ 0〜1 で、揺れの大きさ・ジグザグの折れ回数・点の数を増やす 01 揺れ最大 2.43px · 02 行ごとの折れ 10〜61回 · 03 点 1,800個
どの順番で 04 · 07 · 08 · 09 いちばん近い点を次に選んで交差を裏返す、または頂点ごとにつながる線を数える 04 4,080 → 3,864px · 07 封筒は奇数の点が 4 つで描けない
どの速さで 05 · 06 · 08 · 09 積み上げた長さの表で同じ速さにする、または曲がり具合で速さと太さを決める 05 毎秒 240px · 06 毎秒 70〜492px · 08 毎秒 620px

元の絵はコードで組んだ図形

01 の顔、02 の湯気の立つコーヒーカップ、03 の猫の顔、09 の花は、どれも楕円・線分・三角形までの距離を重ねて、マスごとに 0〜1 の暗さを返す関数です。外から読み込む写真や画像ファイルはなく、08 だけが見えないキャンバスに文字を描いてその画素を読み戻します。01 と 03 は明るさではなく暗さで重みをつけるので、黒いマスほど揺れが大きくなり、点も密に集まります。手持ちの絵で試すにはこの暗さの関数を画素を読む関数に取り替えることになり、その手順は最後の質問で扱います。

合うページ

写真館の予約ページの最初の画面には 01 の渦巻きの顔が、ブランド紹介ページのサイン欄には 05 の同じ速さのペンが合います。子ども向けの学習サイトで一筆書き イラスト 図形のパズルを出すなら、描けるかどうかと描く順番を答え合わせのように見せる 07 が向いています。物流会社が配達の回り方を説明するセクションには、絡まりがほどけていく 04 の配送ルートを置けます。仕上がった線画のファイルを渡すだけの素材集とちがって、ここでは描いていく動きそのものが画面に出ます。

ペンを一度も離さない絵が目を引くわけ — 点の順番と線の長さが作る輪郭

線が一本しかない絵では、点を訪ねる順番がそのまま絵になります。同じ 420 個の点でも、まずい順番でつなげば絵を横切って絡まる線になり、よい順番なら同じ点から輪郭が浮かびます。描きかけの絵は、見る人がペン先を目で追って次の一画の行き先を待つので、仕上がった絵より長く目を留めさせると考えられます。

いちばん近い点からつなぐと交差が生まれる理由

04 は倉庫から出発し、まだ寄っていない配送先のうちいちばん近い所へ移ることを繰り返して、倉庫を含む 420 か所をつなぎます。距離の比較は 87,990 回で、最初の道すじは 4,080px ですが、その中に交差した区間の組が 34 あります。近い所だけを選ぶと飛ばした点が後に残り、あとでそこへ戻るときにすでに引いた線を横切るためと考えられます。32 回裏返すと長さは 3,864px(−5.3%)に縮み、交差は 0 になります。09 の花も同じ計算で、61 回の裏返しで 74 の交差を消し、4,094px を 3,826px(−6.6%)に詰めます。

線の長さが描く時間になる

「線画 動く アニメーション」で探している方にとっての要は、線の長さを時間に置き換えることです。01 の渦巻きは 11,160px を 4.6 秒、02 のジグザグは 13,688px を 5 秒で引き、どちらも毎秒 2,400px を超える速さで画面を埋めます。05 のサインは 1,312.6px に 5.47 秒、06 のハートは 1,742.1px に 7.45 秒かけるので、目がペン先についていけます。速いペンは絵を埋めるために、遅いペンは手書きの手ざわりを出すために使い分けます。

一筆書き イラスト 9種 — 明るさの渦巻き・ジグザグの陰影・均した点・ほどいた絡まり・等速のペン・手の震えと太さ・一筆書きの図形判定・一本線の筆記体・一本線で描かれるファーストビュー

01 と 02 は渦巻きや横の行という決まった道に沿って線を揺らし、陰影を作ります。03 と 04 は点の置き場所と訪ねる順番を、05 と 06 はペンの速さと太さを扱い、07 は図形を一度で描けるかを判定し、08 は単語を一本の線でつなぎます。09 は 03・04・05・06 の計算を一つの花にまとめたファーストビューです。どの場面も外部ライブラリ・WebGL・画像ファイルなしの 2D キャンバス一枚で描き、シードを固定した乱数で開くたびに同じ絵になります。操作できるのは一時停止ボタンだけで、絵はひとりでに描かれていきます。

480×300 の画面を 2 秒撮って数えた値

描画を 24 枚撮り、隣り合う 2 枚を比べた 23 回のうち変化があった回数と、一度でも色が変わった面積の割合を数えました。色がにじみ込む 09 がいちばん広く 35.0% を占め、ペン先を追って線一本とその下の帯だけが伸びていく 05 と 08 は 4〜6% にとどまります。

場面 変化した面積 動いたフレーム 強さの平均
01 明るさの渦巻き 23.1% 23/23 92.9
02 ジグザグの陰影 21.4% 21/23 123.7
03 点描 12.4% 14/23 40.1
04 配送ルートのほどき 18.4% 20/23 74.4
05 等速のペン 4.4% 23/23 73.1
06 ボールペンのハート 23.4% 23/23 84.1
07 一筆書きの判定 11.5% 23/23 84.9
08 一本線の筆記体 5.7% 19/23 81.5
09 花のファーストビュー 35.0% 19/23 79.4

03 の 14/23 は、点が 0.22 秒ごとに動いたあと、次の段階まで新しい位置で休むためと考えられます。04 の 20/23 と 08・09 の 19/23 は、段階の切れ目で画面が止まる短い間を映していると考えられます。

近い記事とのちがい

ロゴの輪郭をなぞる動きはロゴのローディングアニメーション、読み取った暗さで糸を張る絵はストリングアート、文字の画素を点として読む方法は文字の画素サンプリング、手書きサインの速さと太さはサインを書くキャンバスで扱っています。この記事はその上に、点の順番を決めることと、線の長さを時間に変えることを足します。09 のように線の下へ色がにじむ表現は、水彩エフェクトへ続きます。

01暗さのぶんだけ揺れて絵が浮かぶ一本の渦巻き

6px 間隔で巻く渦巻きを 1.2px ずつたどり、暗い所を通るほど渦巻きと直角の向きへ大きく振れて波も短くなるので(一つの波の長さは明るい所で 62.83px、いちばん暗い所で 5.71px)、顔が浮かび上がります。ペンは 24.33 周・頂点 9,301 個・11,160px を毎秒 2,426.1px で 4.6 秒かけて描き、いちばん暗い所では 2.43px の振れ二つと線の太さ 2px が 6px の間隔を 0.86px はみ出すので、隣り合う周どうしが触れて黒く固まります。写真館の予約ページのファーストビューに、一本線の似顔絵として置けます。

暗さのぶんだけ揺れて絵が浮かぶ一本の渦巻き
新しいタブで開く

01 · 暗さのぶんだけ揺れて絵が浮かぶ一本の渦巻き — 計測 累積面積 23.066% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

SPIRAL_GAPWOBBLE_AMPWOBBLE_FREQ_DARK
        while (a * theta < SPIRAL_RADIUS) {
            const r = a * theta;
            const c = Math.cos(theta);
            const s = Math.sin(theta);
            const bx = CX + r * c;
            const by = CY + r * s;
            // 接線 = d/dθ (r cosθ, r sinθ) = (a cosθ − r sinθ, a sinθ + r cosθ)、法線は接線を 90° 回したもの
            const tx = a * c - r * s;
            const ty = a * s + r * c;
            const tl = Math.sqrt(tx * tx + ty * ty);
            const nx = -ty / tl;
            const ny = tx / tl;
            dSmooth = lerp(dSmooth, dark(bx, by), SAMPLE_STEP / DARK_SMOOTH);
            phase += lerp(WOBBLE_FREQ_LIGHT, WOBBLE_FREQ_DARK, dSmooth) * SAMPLE_STEP;
            const off = WOBBLE_AMP * dSmooth * Math.sin(phase);
            maxOffset = Math.max(maxOffset, Math.abs(off));
            xs.push(bx + nx * off);
            ys.push(by + ny * off);
            // 弧の長さ ds = √(r² + a²)·dθ なので、同じ歩幅で進むには dθ = ds / √(r² + a²)
            theta += SAMPLE_STEP / Math.sqrt(r * r + a * a);
        }
        return { xs: Float32Array.from(xs), ys: Float32Array.from(ys), turns: theta / TAU, maxOffset: maxOffset };

02暗い所で細かくなるジグザグ一本線の陰影

10px 間隔の 27 行を一本の線が 1px ずつ行き来し、暗さがジグザグの高さ(最大 4.4px)と位相の進み方の両方を上げるので、影は細かい山で埋まり、行の端は 14 区切りの半円でつながります。頂点 12,137 個・13,688px を毎秒 2,738px で 5 秒かけて描き、行ごとの折れは 10〜61 回(合計 806 回)で、背景の暗さが 0.03 しかない行でも 10 回は折れるので、真っすぐな行は一本もありません。カフェのメニューボードで、手で陰影をつけたカップの絵を背景に敷くのに向いています。

暗い所で細かくなるジグザグ一本線の陰影
新しいタブで開く

02 · 暗い所で細かくなるジグザグ一本線の陰影 — 計測 累積面積 21.386% · 動いたフレーム 21/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

ROW_GAPZIG_AMPZIG_FREQ_DARK
        for (let ry = ROW_TOP; ry <= H - ROW_TOP; ry += ROW_GAP) {
            const dir = rows % 2 === 0 ? 1 : -1;
            let phase = 0;
            for (let k = 0; k <= ROW_RIGHT - ROW_LEFT; k += 1) {
                const x = dir > 0 ? ROW_LEFT + k : ROW_RIGHT - k;
                const d = dark(x, ry);
                phase += lerp(ZIG_FREQ_LIGHT, ZIG_FREQ_DARK, d);
                xs.push(x);
                ys.push(ry + ZIG_AMP * d * triangleWave(phase));
            }
            // 折れ回数 — 三角波は位相 π ごとに一度向きを変える
            turnsPerRow.push(Math.floor(phase / Math.PI));
            rows += 1;
            if (ry + ROW_GAP > H - ROW_TOP) {
                break;
            }
            // 行末の半円 — 今の行の端から次の行の端まで、外側へふくらむ半径 ROW_GAP/2
            const ex = dir > 0 ? ROW_RIGHT : ROW_LEFT;
            const cy = ry + ROW_GAP / 2;
            for (let j = 1; j < TURN_STEPS; j += 1) {
                const a = -Math.PI / 2 + Math.PI * j / TURN_STEPS;
                xs.push(ex + dir * Math.cos(a) * ROW_GAP / 2);
                ys.push(cy + Math.sin(a) * ROW_GAP / 2);
            }
        }

03暗さの重心へ動いて均一に広がる点描

暗い所へ寄せて 1,800 個の点を撒いたあと、0.25 秒から 0.22 秒ごとに 14 回、点ごとに、その点がいちばん近い点になるマスたちを集めて暗さの重心を求め、そこへ滑らせ、毎秒 30px より速く動く点は珊瑚色、止まった点はインク色で描きます。いちばん近い隣までの距離は撒いた直後の平均 1.88px・ばらつき(標準偏差 ÷ 平均)0.68 から最後には 2.88px・0.5 になり、一回あたりの平均移動は 1.07px から 0 まで下がって、両目の所に点が 204 個残ります。ポートフォリオの作品紹介で、点の集まりから絵が浮かぶサムネイルに使えます。

暗さの重心へ動いて均一に広がる点描
新しいタブで開く

03 · 暗さの重心へ動いて均一に広がる点描 — 計測 累積面積 12.4% · 動いたフレーム 14/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STIPPLE_COUNTRELAX_ROUNDSMAX_DOTS
        let moved = 0;
        for (let i = 0; i < dots.n; i += 1) {
            // 受け持つマスがない点はその場に — 0 で割らない
            toX[i] = sumW[i] > 0 ? sumX[i] / sumW[i] : dots.xs[i];
            toY[i] = sumW[i] > 0 ? sumY[i] / sumW[i] : dots.ys[i];
            moved += dist2(toX[i], toY[i], dots.xs[i], dots.ys[i]);
        }
        return moved / Math.max(1, dots.n);

04交差した二区間を裏返してほどいていく一本線の道すじ

倉庫を含む 420 か所を 1.4 秒かけていちばん近い順につなぎ、1.55 秒からはフレームごとに 1,200 組の区間を比べて、交差した二区間の間の順番を逆にし、裏返した部分を珊瑚色の帯で光らせます。交差した 34 組をほどくのに裏返し 32 回・比べた組 261,459 がかかり、毎秒 60 フレームなら 218 フレーム後の 5.18 秒に、状態表示の頭が「交差をほどき中」から「ほどけました」に変わり、後ろの「· 全長」はほどけたあとも表示されたままです。物流会社が配送ルートを紹介するセクションで、一日の回り方を見せるのに合います。

交差した二区間を裏返してほどいていく一本線の道すじ
新しいタブで開く

04 · 交差した二区間を裏返してほどいていく一本線の道すじ — 計測 累積面積 18.442% · 動いたフレーム 20/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

STOP_COUNTTRIES_PER_FRAMEGREEDY_TIME
    function segmentsCross(ax, ay, bx, by, cx, cy, dx, dy) {
        const d1 = cross3(ax, ay, bx, by, cx, cy);
        const d2 = cross3(ax, ay, bx, by, dx, dy);
        const d3 = cross3(cx, cy, dx, dy, ax, ay);
        const d4 = cross3(cx, cy, dx, dy, bx, by);
        return ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0));
    }
            if (segmentsCross(xs[a], ys[a], xs[b], ys[b], xs[c], ys[c], xs[d], ys[d])) {
                // i+1 から j までの順番を逆に — a→c、b→d とつなぎ直されて交差が消え、長さが縮む
                let lo = u.i + 1;
                let hi = u.j;
                u.lastLo = lo;
                u.lastHi = hi;
                while (lo < hi) {
                    const tmp = o[lo];
                    o[lo] = o[hi];
                    o[hi] = tmp;
                    lo += 1;
                    hi -= 1;
                }
                u.flips += 1;
                u.flipsThisPass += 1;
                flipped += 1;
            }

05曲がり角でも同じ速さで描き進むペン

四つの輪と 6 点の下線からなる 39 点のサインを、前後の隣の点で傾きを決めた三次曲線でつなぎ、609 項目の積み上げ長さの表を使って、ペンが 1,312.6px 全体を毎秒 240px のまま 5.47 秒で描きます。0.12 秒ごとに落とすラベンダーの点は曲線に沿って 28.8px ずつ離れて 45 個並び(輪の頂上では直線距離が 3.4px まで縮みます)、線の下の帯は、区間ごとに同じ時間を与えたペンなら遅くなった所を空色、速くなった所を珊瑚色で示します。ブランド紹介ページのサイン画として、代表者の署名の代わりに添えられます。

曲がり角でも同じ速さで描き進むペン
新しいタブで開く

05 · 曲がり角でも同じ速さで描き進むペン — 計測 累積面積 4.36% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

PEN_SPEEDTICK_EVERYCURVE_STEPS
    function indexAtLength(c, s) {
        if (s <= 0) {
            return 0;
        }
        if (s >= c.total) {
            return c.lens.length - 1;
        }
        let lo = 0;
        let hi = c.lens.length - 1;
        while (hi - lo > 1) {
            const mid = (lo + hi) >> 1;
            if (c.lens[mid] < s) {
                lo = mid;
            }
            else {
                hi = mid;
            }
        }
        return lo + (s - c.lens[lo]) / Math.max(1e-9, c.lens[hi] - c.lens[lo]);
    }

06手の震えが混じり、速さで太さが変わるボールペンの線

40 点の外側のハート、内側への一歩、法線に沿って 7px 内へ押した内側のハートを一本の線で描き、各位置の前後 14px で最も鋭い曲がりが速さ(ここでは毎秒 70〜492px)を決め、太さは速さだけで決まり、TREMOR_AMP 1.2 で大きさを決めたノイズの震えが線を横へずらします。太さは 2.42〜4.96px で、速い所は細く、遅い角は太くなり、1,742.1px 全体を 7.45 秒で描くあいだに、内側のピンクのフェルトが不透明度 0 から 0.55 まで濃くなります。ハンドメイド雑貨の商品ページで、ステッチ風の飾りとして使えます。

手の震えが混じり、速さで太さが変わるボールペンの線
新しいタブで開く

06 · 手の震えが混じり、速さで太さが変わるボールペンの線 — 計測 累積面積 23.39% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

TREMOR_AMPWIDTH_KCORNER_SLOW
            let peak = 0;
            for (let j = lo; j < n && c.lens[j] - c.lens[i] <= CORNER_LOOK; j += 1) {
                peak = Math.max(peak, bend[j]);
            }
            const v = Math.max(PEN_SLOWEST, PEN_FAST / (1 + CORNER_SLOW * peak));
            speeds[i] = v;
            widths[i] = PEN_WIDTH / (1 + WIDTH_K * v);
            if (i > 0) {
                times[i] = times[i - 1] + (c.lens[i] - c.lens[i - 1]) / ((v + speeds[i - 1]) / 2);
            }

07奇数の分かれ目を数えて一度で描く順番を探す一筆書きの図形

四つの図形が順に現れ、0.45 秒のあいだ頂点ごとにつながる線の数が表示されて奇数の点に珊瑚色の輪がつき、奇数の点が 0 個か 2 個の図形は見つけた順番どおり毎秒 520px でパステルの帯として引かれ、線ごとに順番の数字が入ります。家(奇数 2 個)は 0 番から出て 1 番で終わる 10 画を 2.35 秒、蝶リボン(奇数 0 個)は輪を一つ差し込んで 6 画を 1.79 秒、星は 10 画を 3.27 秒で描き、奇数の点が 4 つある封筒には 1.8 秒間バツ印が押されます。子ども向け学習サイトのパズル画面で、描けるかどうかと描く順番を答え合わせのように見せられます。

奇数の分かれ目を数えて一度で描く順番を探す一筆書きの図形
新しいタブで開く

07 · 奇数の分かれ目を数えて一度で描く順番を探す一筆書きの図形 — 計測 累積面積 11.544% · 動いたフレーム 23/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

COUNT_TIMETRACE_SPEEDIMPOSSIBLE_TIME
        const deg = degrees(f);
        const odd = [];
        for (let i = 0; i < deg.length; i += 1) {
            if (deg[i] % 2 === 1) {
                odd.push(i);
            }
        }
        if (odd.length > 2) {
            return { possible: false, odd: odd, order: [], splices: 0 };
        }
            if (next >= 0) {
                // 戻る途中でまた歩き出した = 残った輪を一つ差し込む
                if (backing) {
                    splices += 1;
                    backing = false;
                }
                stack.push(next);
            }
            else {
                out.push(stack.pop());
                backing = true;
            }

08文字の間をつないで一本線で書かれる筆記体の名前

atelier の各文字を見えないキャンバスに italic 700 104px serif で描いて読み戻し、3px の格子で輪郭のマス(ここでは 535 個、端末にある serif 書体によって数が変わります)をいちばん近い順につなぎ、次の文字へは、前の文字の終わりから 6px 右と次の文字の最初の点から 6px 左に、基準線より 14px 下の点を二つ挟んだつなぎ画で渡ります。280 点と 6 本のつなぎ画が 2,299px の曲線一本になって毎秒 620px で 3.71 秒かけて書かれ、文字ごとに別のパステルの蛍光帯が下に引かれます。工房ブランドのロゴ紹介で、ロゴの下に名前を一筆で書き添えるのに合います。

文字の間をつないで一本線で書かれる筆記体の名前
新しいタブで開く

08 · 文字の間をつないで一本線で書かれる筆記体の名前 — 計測 累積面積 5.701% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

GLYPH_CELLBASELINE_DIPWRITE_SPEED
        for (let k = 0; k < WORD.length; k += 1) {
            const x = left + m.measureText(WORD.slice(0, k)).width;
            lefts.push(x);
            const g = glyphOutline(WORD[k], x);
            cells += g.cells;
            const path = glyphPath(g);
            if (path[0].length === 0) {
                continue;
            }
            if (px.length > 0) {
                // 前の文字の終わり → 基準線の下へ降りて → 次の文字の最初の点の下 → 最初の点。筆記体のつなぎ画
                px.push(px[px.length - 1] + 6, path[0][0] - 6);
                py.push(BASE_Y + BASELINE_DIP, BASE_Y + BASELINE_DIP);
                joins += 1;
            }
            starts.push(px.length * CURVE_STEPS);
            for (let i = 0; i < path[0].length; i += 1) {
                px.push(path[0][i]);
                py.push(path[1][i]);
            }
        }

09一本線で描かれたあと色がにじみ込む花のファーストビュー

700 個の点を 6 回で均し、茎の根元からいちばん近い順につないで、画面ではフレームごとに 14,000 組ずつほどきますが、ペンは最初にほどき終えておいた順番を 06 と同じインクの線で描き、震え 1.4 と角での減速を残したまま 3.4 秒に収まるよう時間を詰めます。最初の道すじは 61 回の裏返しで 74 の交差が 0 になって 4,094px から 3,826px に縮み、6.24 秒からは 10 の色面が 1.4 秒かけて線の下ににじみ込み、7.64 秒で絵が仕上がります。工房のランディングページのファーストビューで、まず線、次に色という順で見せたいときに使えます。

一本線で描かれたあと色がにじみ込む花のファーストビュー
新しいタブで開く

09 · 一本線で描かれたあと色がにじみ込む花のファーストビュー — 計測 累積面積 35.003% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · パスワードを見る

FLOWER_DOTSTRIES_09PEN_TIME_09
    const greedy09 = greedyOrder(final09.xs, final09.ys, final09.n, start09);
    const greedy09Length = pathLength(final09.xs, final09.ys, greedy09, final09.n);
    // ほどき終えた順番 — 同じ計算を最後まで回しておく。画面のほどき段階が時間内に終わらなくても、ペンはこの順番を描く
    const solved09 = makeUntangle(Int32Array.from(greedy09));
    while (!solved09.done) {
        untangleStep(solved09, final09.xs, final09.ys, TRIES_09);
    }
    const solved09Length = pathLength(final09.xs, final09.ys, solved09.order, final09.n);
    function orderedCurve(o) {
        const px = [];
        const py = [];
        for (let k = 0; k < o.length; k += 1) {
            px.push(final09.xs[o[k]]);
            py.push(final09.ys[o[k]]);
        }
        return curveThrough(px, py);
    }
    const ink09 = inkLine(orderedCurve(solved09.order), TREMOR_09);

点は明るさで撒き、順番は交差をほどいて、速さは積み上げた長さで — 一本の線を立てる計算

暗さを確率に変える

03 は 3px のマス 16,000 個(横 160・縦 100)で暗さを一度ずつ測って重みの表を作り、09 も同じ関数で花の点を撒きます。そのあと場所を無作為に選び、そのマスの重みを 1.4 乗した確率で点を残します。暗さ 0.5 のマスは 0.5^1.4 ≈ 0.38、0.9 のマスは 0.9^1.4 ≈ 0.86 の確率で点が残るので、1.8 対 1 だった暗さの差が約 2.3 対 1 に広がります。指数が大きいほど淡い背景に迷い込む点が減り、暗い輪郭に点が集まります。

交差の判定を手で計算する

segmentsCross は外積四つで決めます。a(0,0)–b(10,10) と c(0,10)–d(10,0) を入れると、a–b から見て c は +100、d は −100、c–d から見て a は −100、b は +100 で、どちらの組も符号が入れ替わるので交差と判定されます。二つの区間の長さは 14.14 + 14.14 = 28.28 で、間の順番を逆にすると端が a→c、b→d とつなぎ直されて 10 + 10 = 20 になります。交差した二区間を交差しないようにつなぎ直しても道すじは長くならないので、裏返しを重ねるほど全長は縮んでいきます。外積がちょうど 0、つまり三点が一直線に並ぶ場合は交差に数えません。長さが縮まない裏返しが同じ所で行ったり来たりするのを防いでいると考えられます。

長さを時間に、曲がりを速さに

05 のペンは経過時間 × 240 を長さ s に変え、indexAtLength が 609 項目の積み上げ長さの表を半分ずつ絞って、s の入った区切りを探します。区切りは 608 個で、2^10 = 1,024 が 608 より大きいので、多くても 10 回で決まります。06 は逆に曲がりから速さを出し、曲がり 0 の真っすぐな所では v = 560・太さ 6 ÷ (1 + 0.003 × 560) ≈ 2.24px、1px あたり 0.1 ラジアン曲がる所では v = 560 ÷ (1 + 60 × 0.1) = 80・太さ 6 ÷ 1.24 ≈ 4.84px になります。ハートではいちばん真っすぐな所でも毎秒 492px までしか上がらないので、実際の太さは 2.42px から始まります。キャンバスのパスは一本につき lineWidth が一つなので、06 は太さを 8 段に分け、段ごとにパスを一本ずつ引きます。

点が一万個に増えて止まる並べ替え・つなぎ目で折れる曲線・奇数の分かれ目が四つある図形 — 一本線の絵が壊れやすい場面

点が一万個になると並べ替えが画面を抱え込む

untangleStep の一巡は (N − 3)(N − 2) ÷ 2 組を比べます。420 点なら 87,153 組で、フレームあたり 1,200 組なら一巡に 73 フレームですが、10,000 点では一巡が 49,975,003 組に膨らみ、同じ割合で 41,646 フレーム(毎秒 60 フレームで 11.6 分)かかります。上限なしで一巡を一フレームに詰めれば、そのフレーム一つが 5,000 万組を抱え込み、いちばん近い順につなぐだけでも比較が 49,995,000 回になります。フレームごとの上限には別の落とし穴もあって、ほどける時間が画面の書き換え回数に左右されます。04 の 218 フレームは毎秒 60 フレームで 3.63 秒、30 フレームなら 7.27 秒かかるので、計算の上では描画段階 DRAW_TIME の 5.2 秒のうちに「ほどけました」へたどり着けず、一巡のうちにほどけ切らない場合もあると考えられ、120 フレームなら 1.82 秒で済みます。03 は点を 10,000 個求めても MAX_DOTS で 2,400 個に止まり、12px のバケツ分けで一回の移動の比較を 5,731,200 回から 218,776 回、約 26.2 分の 1 に減らします。

区間ごとに自分の弦を接線にするとつなぎ目で折れる

05 のサインの 39 点を、区間ごとに自分の弦を両端の接線にしてつなぐと、前の区間が終わる向きと次の区間が始まる向きが食い違います。同じ点で測ると 37 か所のつなぎ目で折れ角は最大 158.8°、平均 44.6° になり、20° を超える所が 34 か所あります。前後の隣の点から取った接線なら、つなぎ目の両側の区間が同じ接線を使うので、折れは 0° です。さらに長さの表もやめて区間ごとに同じ時間を与えると、区間の長さがそのまま速さになり、ペンは毎秒 76〜769px のあいだを 10.17 倍の幅で行き来します。折れる様子を自分の目で確かめたいときは、ダウンロードした zip をパスワード hqaux5m7 で開き、05 フォルダの index.html の buildCurve で cubicPiece に渡している二つの接線を、その区間の弦に差し替えてみてください。

四隅がどれも線三本の封筒は一筆で描けない

07 の封筒は長方形と、真ん中で出会う対角線四本でできているので、頂点ごとの線の数は [3, 3, 3, 3, 4] になり、奇数の点が四つあります。planStroke は順番を探しもせず、その場で描けないと返します。家は [3, 3, 4, 4, 2, 4] で奇数の点が二つなので一方から出てもう一方で終わり、蝶リボンと星は奇数の点がないので出発点に戻ります。図形を考えるときは、線を一本足すか消すとその両端の頂点の偶奇がどちらも入れ替わるので、奇数の点が四つある図形も、そのうち二つを線一本で結べば描けるようになります。

点をまばらに、震えを大きく、ペンを速く — 値一つで変わる線の手ざわり

下の表は、どれも一つのファイルの定数を一つだけ変え、同じ関数を回し直して測った値です。

バリエーション 変えた値 変わること
点をまばらに 03 STIPPLE_COUNT 1800 → 700 隣までの平均距離 2.88 → 5.09px、ばらつき 0.50 → 0.42 で点はむしろ揃いますが、両目の点が 204 個から 73 個に減って目もとが薄くなります。一回の移動の比較は 218,776 回から 84,284 回に減り、一巡 6.38 秒は変わりません
震えを大きく 06 TREMOR_AMP 1.2 → 3 二つのハートの線のいちばん狭いすき間が 2.16px から −0.13px になり、触れる所が 0 か所から 2 か所に増えます。長さ・時間・太さは変わりません
ペンを速く 05 PEN_SPEED 240 → 520 描く時間 5.47 → 2.52 秒、一巡 8.47 → 5.52 秒、0.12 秒ごとの点の間隔 28.8 → 62.4px、点の数 45 → 21 個になります

描き終えた一枚で止まるように — 動きを減らす設定・一時停止ボタン・絵の説明文のアクセシビリティ

動きを減らす設定で残るもの

九つの場面はどれも prefers-reduced-motion: reduce を読み、オンならループを回さず、終わりの時刻まで 1/60 秒刻みで計算だけして描き終えた一枚を出します。途中で設定を切り替えても、change リスナーが受けて同じ完成の一枚へ移ります。07 の場合、その一枚は家を引き終えた場面です。最初に開いたとき、01・02・05・06・08・09 は一巡目を途中から始めて(01 は 1.5 秒、02 は 2 秒、05 は 2.6 秒、06 は 0.8 秒、08 は 1.4 秒、09 は 4.6 秒)画面が空のまま開かないようにしていて、03・04・07 は 0 秒から始まります。

ボタンとスクリーンリーダー

九つのキャンバスはどれも role="img" と絵を説明する aria-label を持ち、右下の一時停止ボタンは aria-pressed で状態を伝え、キーボードのフォーカスには 3px のインク色の枠が出ます。状態表示がある 04 と 07 は、デモのあいだ aria-live を off にしておき、ボタンが初めて押されてから変わった文を読み上げます。タブが隠れるとループを止め、戻ったら時計を合わせ直して続きから描きます。参考は MDN の prefers-reduced-motion と MDN の requestAnimationFrame です。

サイトに一本線の絵を入れる前のよくある質問

手持ちの写真から作るには、コードのどこを変えればよいですか?

01・02・03・09 は faceDark のようにマスごとに 0〜1 を返す暗さの関数を呼んでいるので、写真を見えないキャンバスに描いて getImageData で読み、その位置の「1 − 明るさ」を返す関数に置き換えます。別のオリジンの画像を描いたキャンバスはこの読み取りでセキュリティエラーを出すので、08 のようにその行を try で包み、写真は同じサイトに置いてください。

一筆書き イラスト おしゃれに見せたいとき、線の色と太さはどこで変えますか?

配色は九つの index.html の冒頭にある配列で、INK = [23, 20, 26] と、CORAL・LAVENDER・BUTTER・SAGE・PINK・SKY の六つのパステルが九ファイルとも同じ値で並んでいます。06 と 09 の太さは PEN_WIDTH 6 と WIDTH_K 0.003 で決まり、09 はそこへ INK_SCALE_09 0.55 をかけて細い線にしています。点の数や震えを変えたときの見え方は、上のバリエーションの表が目安になります。

07 に自分で考えた一筆書きの図形を足すには、何を書けばよいですか?

07 の FIGURES の各項目は、頂点の座標を x・y の順に交互に並べた pts と、つながる二つの頂点番号を組にして並べた edges という二つの平たい配列を持ちます。この形で図形を一つ足せば、線の数の数え上げ・奇数の点の輪・順番探し・描けない印がそのまま動きます。奇数の点が 2 個を超えると描けない印がつき、奇数の点どうしを結ぶ線を一本足すごとに奇数の点は 2 個減ります。

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

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