【SvelteでHTMLアプリ開発】テオ・ヤンセン機構をSvelteアプリで再現してみる


※ 当ページには【広告/PR】を含む場合があります。
2022/05/14
合同会社タコスキングダム|TacosKingdom,LLC.

久しぶりにSvelteで簡単なHTMLアプリを作って遊んでみましょう。

今回のお題は、
「テオ・ヤンセン機構」です。

動画で見るととても生物的な動きをするのが特徴で、一見するととても複雑な構造をしていそうですが、原理的には非常に数学的な考え方でデザインされています。

今回はSvelteアプリで、SVGアニメーションとしてテオ・ヤンセン機構を再現してみましょう。


合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

テオ・ヤンセン機構の原理

数学的な原理的を詳しく解説しておられる方のブログで図解して分かりやすくまとめられていたので、興味があればそちらを参考にしてください。

参考|テオ・ヤンセン機構の計算【詳細版】

ここでは、テオ・ヤンセン機構のSvelteアプリへの実装のポイントを掻い摘んで紹介します。

まず、テオ・ヤンセン機構の基礎として
リンクと呼ばれるaからmまでの13個の特別な定数長さを設定します。

            
            const links: any = {
    a: 38, b: 41.5, c: 39.3, d: 40.1, e: 55.8, f: 39.4,
    g: 36.7, h: 65.7, i: 49, j: 50, k: 61.9, l: 7.8,
    m: 15
};
        
また接点として以下の点Oを原点、点Aから点Gまでの計8点の座標を与えておきます。

            
            const points: any = {
    O: [0.0, 0.0], A: [0.0, 0.0], B: [-links.a, -links.l], C: [0.0, 0.0],
    D: [0.0, 0.0], E: [0.0, 0.0], F: [0.0, 0.0], G: [0.0, 0.0]
};
        
テオ・ヤンセン機構の原則として、原点O周りにAが円運動を行うことで動力を得ます。

また点Bの位置は固定されて、これが運動の主軸として機能します。

この3点の座標が決まるので、後は
C→D→E→F→Gと順番に座標値を更新していくことで、テオ・ヤンセン機構のアニメーションが描画することが可能になります。


テオ・ヤンセン機構のシミュレーション(一脚)

では以下に一脚でどのように動くのかを確かめてみます。

足一脚の動きをじっくりと観察すると、割とシンプルな動きに感じられます。


合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

Svelteコードの実装

先程のSvelteアプリのソースコードを示します。

            
            <script lang="ts">
    import { onMount } from 'svelte';

    //👇SVGのビューポート設定
    const view = {
        w: 300, h: 300,
        t: -50, l: -120, b: 150, r: 180,
    };

    const period = 2; //👈周期[秒]
    const ticks = 300;
    const timerPeriod = period * 1000 / ticks;
    let timerCount = 0;

    const links: any = {
        a: 38, b: 41.5, c: 39.3, d: 40.1, e: 55.8, f: 39.4,
        g: 36.7, h: 65.7, i: 49, j: 50, k: 61.9, l: 7.8,
        m: 15
    };

    const points: any = {
        O: [0.0, 0.0], A: [0.0, 0.0], B: [-links.a, -links.l], C: [0.0, 0.0],
        D: [0.0, 0.0], E: [0.0, 0.0], F: [0.0, 0.0], G: [0.0, 0.0]
    };

    //👇座標系は$ラベル構文でリアクティブに更新する
    $: {
        let theta_ab: number, theta_bc: number, theta_de: number, theta_df: number;
        let xc: number, yc: number, AB: number, DE: number;
        points.A = [
            links.m * Math.sin(2*Math.PI*(timerCount / ticks)),
            links.m * Math.cos(2*Math.PI*(timerCount / ticks))
        ];
        points.B = [-links.a, -links.l];

        AB = Math.sqrt((links.a + points.A[0])*(links.a + points.A[0]) + (links.l + points.A[1])*(links.l + points.A[1]));
        xc = (AB*AB + links.b*links.b - links.j*links.j) / (2.0*AB);
        yc = Math.sqrt( links.b*links.b - xc*xc );
        theta_ab = Math.atan2(points.A[1] - points.B[1], points.A[0] - points.B[0]);
        points.C = [
            -links.a + xc*Math.cos(theta_ab) + yc*Math.cos(theta_ab+Math.PI/2),
            -links.l + xc*Math.sin(theta_ab) + yc*Math.sin(theta_ab+Math.PI/2)
        ];

        xc = (links.b*links.b + links.d*links.d - links.e*links.e) / (2.0*links.b);
        yc = Math.sqrt( links.d*links.d - xc*xc );
        theta_bc = Math.atan2(points.C[1] - points.B[1], points.C[0] - points.B[0]);
        points.E = [
            points.B[0] + xc*Math.cos(theta_bc) + yc*Math.cos(theta_bc+Math.PI/2),
            points.B[1] + xc*Math.sin(theta_bc) + yc*Math.sin(theta_bc+Math.PI/2)
        ];

        xc = (AB*AB + links.c*links.c - links.k*links.k) / (2.0*AB);
        yc = Math.sqrt( links.c*links.c - xc*xc );
        points.D = [
            points.B[0] + xc*Math.cos(theta_ab) + yc*Math.cos(theta_ab-Math.PI/2),
            points.B[1] + xc*Math.sin(theta_ab) + yc*Math.sin(theta_ab-Math.PI/2)
        ];

        DE = Math.sqrt((points.D[0] - points.E[0])*(points.D[0] - points.E[0]) + (points.D[1] - points.E[1])*(points.D[1] - points.E[1]));
        xc = (DE*DE + links.g*links.g - links.f*links.f) / (2.0*DE);
        yc = Math.sqrt( links.g*links.g - xc*xc );
        theta_de = Math.atan2(points.E[1] - points.D[1], points.E[0] - points.D[0]);
        points.F = [
            points.D[0] + xc*Math.cos(theta_de) + yc*Math.cos(theta_de+Math.PI/2),
            points.D[1] + xc*Math.sin(theta_de) + yc*Math.sin(theta_de+Math.PI/2)
        ];

        xc = (links.g*links.g + links.i*links.i - links.h*links.h) / (2.0*links.g);
        yc = Math.sqrt( links.i*links.i - xc*xc );
        theta_df = Math.atan2(points.F[1] - points.D[1], points.F[0] - points.D[0]);
        points.G = [
            points.D[0] + xc*Math.cos(theta_df) + yc*Math.cos(theta_df+Math.PI/2),
            points.D[1] + xc*Math.sin(theta_df) + yc*Math.sin(theta_df+Math.PI/2)
        ];
    }

    onMount(() => {
        const interval = setInterval(() => {
            timerCount = timerCount >= ticks - 1 ? 0 : timerCount + 1;
        }, timerPeriod);
        return () => clearInterval(interval);
    });
</script>

<svg width="{view.w}" height="{view.h}" viewBox='{view.l} {view.t} {view.r} {view.b}'>
    <g>
        <path class="OACBEC" d="
            M 0 0
            L {points.A[0]} {-points.A[1]}
            L {points.C[0]} {-points.C[1]}
            L {points.B[0]} {-points.B[1]}
            L {points.E[0]} {-points.E[1]}
            L {points.C[0]} {-points.C[1]}
        "/>
        <path class="EFGDF" d="
            M {points.E[0]} {-points.E[1]}
            L {points.F[0]} {-points.F[1]}
            L {points.G[0]} {-points.G[1]}
            L {points.D[0]} {-points.D[1]}
            L {points.F[0]} {-points.F[1]}
        "/>
        <path class="ADB" d="
            M {points.A[0]} {-points.A[1]}
            L {points.D[0]} {-points.D[1]}
            L {points.B[0]} {-points.B[1]}
        "/>
        <path class="Oal" d="
            M 0 0
            L 0 {links.l}
            L {-links.a} {links.l}
        "/>
    </g>
    <g>
        <circle class="O" cx="0" cy="0" r="2" />
        <circle cx="{points.A[0]}" cy="{-points.A[1]}" r="3" />
        <circle cx="{points.B[0]}" cy="{-points.B[1]}" r="3" />
        <circle cx="{points.C[0]}" cy="{-points.C[1]}" r="3" />
        <circle cx="{points.D[0]}" cy="{-points.D[1]}" r="3" />
        <circle cx="{points.E[0]}" cy="{-points.E[1]}" r="3" />
        <circle cx="{points.F[0]}" cy="{-points.F[1]}" r="3" />
        <circle cx="{points.G[0]}" cy="{-points.G[1]}" r="3" />
    </g>
    <g>
        <text x="0" y="0" class="point">O</text>
        <text x="{points.A[0]}" y="{-points.A[1]}" class="point">A</text>
        <text x="{points.B[0]}" y="{-points.B[1]}" class="point">B</text>
        <text x="{points.C[0]}" y="{-points.C[1]}" class="point">C</text>
        <text x="{points.D[0]}" y="{-points.D[1]}" class="point">D</text>
        <text x="{points.E[0]}" y="{-points.E[1]}" class="point">E</text>
        <text x="{points.F[0]}" y="{-points.F[1]}" class="point">F</text>
        <text x="{points.G[0]}" y="{-points.G[1]}" class="point">G</text>
    </g>
    <g>
        <text x="{points.B[0]/2}" y="{-points.B[1]/2}" class="link">a</text>
        <text x="{(points.B[0] + points.C[0])/2}" y="{-(points.B[1] + points.C[1])/2}" class="link">b</text>
        <text x="{(points.B[0] + points.D[0])/2}" y="{-(points.B[1] + points.D[1])/2}" class="link">c</text>
        <text x="{(points.B[0] + points.E[0])/2}" y="{-(points.B[1] + points.E[1])/2}" class="link">d</text>
        <text x="{(points.C[0] + points.E[0])/2}" y="{-(points.C[1] + points.E[1])/2}" class="link">e</text>
        <text x="{(points.E[0] + points.F[0])/2}" y="{-(points.E[1] + points.F[1])/2}" class="link">f</text>
        <text x="{(points.D[0] + points.F[0])/2}" y="{-(points.D[1] + points.F[1])/2}" class="link">g</text>
        <text x="{(points.F[0] + points.G[0])/2}" y="{-(points.F[1] + points.G[1])/2}" class="link">h</text>
        <text x="{(points.D[0] + points.G[0])/2}" y="{-(points.D[1] + points.G[1])/2}" class="link">i</text>
        <text x="{(points.A[0] + points.C[0])/2}" y="{-(points.A[1] + points.C[1])/2}" class="link">j</text>
        <text x="{(points.A[0] + points.D[0])/2}" y="{-(points.A[1] + points.D[1])/2}" class="link">k</text>
        <text x="0" y="{-points.B[1]}" class="link">l</text>
        <text x="{points.A[0]/2}" y="{-points.A[1]/2}" class="link">m</text>
    </g>
</svg>

<style lang="scss">
    svg {
        width: 300px;
        height: auto;
        path {
            &.OACBEC {
                stroke: blue;
                stroke-width: 1.2;
                fill: none;
            }
            &.EFGDF {
                stroke: red;
                stroke-width: 1.2;
                fill: none;
            }
            &.ADB {
                stroke: rgb(255, 153, 0);
                stroke-width: 1.2;
                fill: none;
            }
            &.Oal {
                stroke: rgb(55, 100, 41);
                stroke-width: 1.2;
                fill: none;
            }
        }
        circle {
            fill:none;
            stroke:#888888;
            stroke-width:2;
            &.O {
                fill:rgb(118, 118, 49);
                stroke:none;
            }
        }
        text {
            &.link {font-size: 7px}
            &.point {font-size: 10px}
        }
    }
</style>
        
なお、Svelteでの回転運動アニメーションに関しては、以前解説した以下の記事のものを流用しました。

合同会社タコスキングダム|タコキンのPスクール
【HTMLアプリ作成】SvelteでSVG要素を直接操作して制限時間表示できるタイマーを作ってみる

HTML&CSSゲームなどに応用のできるタイマーっぽいものをSvelteで一から作成する過程を解説します。

Svelteアニメーションの基礎の方から知りたい場合には、そちらもご参考ください。

以上、SVGだけで恰好の良いアニメーションが比較的簡単に作れるのがSvelteの魅力です。


合同会社タコスキングダム|タコキンのPスクール【Html&Cssアプリ】Html/JavascriptとSassを使った四択クイズゲームの作り方

参考サイト

テオ・ヤンセン機構の計算【詳細版】