カテゴリー
【Svelte Framework入門】Svelteを使ってJavascript/CSSからSVG曲線チャートを描く
※ 当ページには【広告/PR】を含む場合があります。
2022/01/24
2022/08/19

以前の関連記事で、実用性はさておきCSS純正折れ線チャートを描くやり方を紹介しました。
個人的にもっと欲を言えば、折れ線でなくて、べシェなどの曲線で補間したような滑らかな
結論としては、
今回は本稿で今最注目の軽量JSフレームワーク・
基本のテクニック 〜 SVG要素のPath属性を使った曲線の書き方
Pathによって曲線を描く場合に、
さらにPathのベシェ曲線には、より複雑で高度な
今回は、シンプルな実装でベシェ曲線を描きたいので、二次のベシェ曲線を制御点を1つだけ設定して、なんとか綺麗な曲線を目指したいので、
Qコマンド
Qコマンドの用法としては、
(...先行してMやQコマンドで始点が必要)
Q [中間制御点のx座標] [中間制御点のy座標]
[終点のx座標] [終点のy座標]
と言うように利用します。
もっとも簡単な例を一つ取り上げてみましょう。
<svg width="300" height="300" viewBox='-10 -10 310 310'>
<path d="
M 10 80
Q 225 250 250 100"
stroke="black" fill="transparent"/>
</svg>
この例でいうと、始点が
(10, 80)
(250, 100)
(225, 250)
なおSVGは画面向かって下方向がy軸座標の正方向ですのでご注意ください。
これを描くと以下のようになります。
べシェ曲線(黒線)の他に補助線や点も書き入れていますが、ここではQコマンドの中間制御点の役割と言うものを視覚的に表した図です。
始点から終点へと結ばれる曲線は、始点と制御点を結ぶ線分の中点、および終点と制御点を結ぶ線分の中点、そしてこの2つの中点を更に結んだ線分の中点(オレンジの点)を通過し、更にこの3つの線分に接するような曲線になります。
基本的にこの制御点はどこにとっても曲線自体は描くことができますが、制御点の取り方一つでべシェ曲線の見栄えが変わってきます。
もう一つQコマンドが扱いやすいのは2次のべシェ曲線を連続して記述できる点にあります。
例えば以下のように4つの点を滑らかに繋ぐべシェ曲線を簡単に描くことができます。
<svg width="300" height="300" viewBox='-10 -10 310 310'>
<path d="
M 10 80
Q 45 60 80 80
Q 140 110 200 200
Q 225 250 250 100"
stroke="black" fill="transparent"/>
</svg>
このSVGべシェ曲線は以下のようになります(点は追加で表示しています)。
と言うことでSVGのPath要素のQコマンドの復習は簡単にここまでとしまして、以降ではSvelteでSVG要素を直接操作して高度なべシェ曲線を描いていく方法を検討します。
SVG曲線をSvelteで描くやり方
Svelteの利点は初心者にも扱いやすい簡潔な記述方法でHTMLを操作できることにあります。
また、近年で複雑化・高機能化の傾向の強くなっている他のJSフレームワークと比較しても学習コストの低めで、割と短時間で習得できるのも魅力になっています。
Svelteの特徴として、HTMLのDOMとスクリプト部分とCSSスタイル部分が密接にハイブリッドされているsvelteコードでアプリを組み上げていくため、今回のテーマのようにSVG要素をCanvas要素やimg要素など無しで直接HTMLに埋め込むことも楽に出来ます。
Svelteアプリの開発環境を準備する
まずはお手元でSvelteアプリをお手軽に作りたいという方向けに、以前ブログ記事を認めたので詳しい手順はそちらをご覧ください。
基本はその記事の中で紹介した
App.svelte
この記事では
App.svelte
Sassコード側からSvelteへ(やや強引に)データを受け渡すやり方
ここでは
_data.scss
以下のようにTypescript&Sassが使えるようにしたSvelteプロジェクトに前回と同じ
$ tree
.
├── node_modules
├── package.json
├── public
│ ├── favicon.png
│ └── index.html
├── rollup.config.js
├── src
│ ├── App.svelte
│ ├── _data.scss #👈ファイルを追加
│ ├── global.d.ts
│ └── main.ts
├── svelte.config.js
├── tsconfig.json
└── yarn.lock
通常、Javascriptのコードで静的ファイルから読み込まれるデータは、jsonやjsなどのフォーマットファイルを直接import構文を使って内部に展開する必要があります。
scssフォーマットのファイルは単なるテキストデータであり、importから任意のテキストを読み込むことはセキリティのリスクの観点から不可能になっています。
サーバーサイドで稼働するネイティブなnode.jsアプリにするなら、fsなどのローカルから静的なテキストを読み込むライブラリも使えますが、今回のケースでは使えません。
もっともrollup.jsの知識がある方は、
では今回どのようにデータを仕込むかというと、Sassファイルの中にあるテキストデータ限定ですが、
@use 'data'; //👈_data.scss
//...
#dataholder {
display: none;
&::before { content: "#{data.$sim_xydata}"; }
}
こうしておくことで、HTMLの裏側でひっそりとデータがテキストとして張り付いていることになります。
後はjavascriptコード側で
Svelteコードの実装
では早速今回のメインのSvelteコードを以下のように実装してみます。
<script lang="ts">
import { onMount } from 'svelte';
let dataholderSpan: any;
let dataArr: any[] = [];
let path: string;
const prevData: number[] = [0, 0, 0, 0];
const bendCoeff = 0.4; // 0.2 ~ 0.6
onMount(() => {
//👇隠しデータセットを仕込んだ擬似要素を取り出し
const pseudo = window.getComputedStyle(dataholderSpan, '::before');
//👇隠し要素からcontentの文字列(2次配列)を読み込み、xy座標の2次配列に復元
dataArr = pseudo.content.replace(/^\"\[/,'').replace(/\]\"$/,'').match(/(\[.*?\])/g).map((xy: string) => {
const xymatch = xy.match(/\[(.*),(.*)\]/);
return [ parseFloat(xymatch[1]), parseFloat(xymatch[2]) ];
});
//👇SVGのPath要素で2次のベシェ曲線を描くコマンドを構築(☆)
path = `M${dataArr.map(p => {
const interpolitedX = (p[0] + prevData[0]) / 2;
const interpolitedY = (p[1] + prevData[1]) / 2 + bendCoeff * prevData[3];
prevData[2] = interpolitedX;
prevData[3] = p[1] - prevData[1];
prevData[0] = p[0];
prevData[1] = p[1];
return `${interpolitedX},${interpolitedY} ${p[0]},${p[1]}`;
}).join('Q')}`
});
</script>
<svg class="curve" width="100" height="10" viewBox='0 0 100 10'>
<path d="{path}" stroke="black" fill="transparent"/>
{#each dataArr as xyset}
<circle class="dataPoint" cx="{xyset[0]}" cy="{xyset[1]}" r="0.3" />
{/each}
</svg>
<span id="dataholder" bind:this="{dataholderSpan}"></span>
<style lang="scss">
@use 'data';
svg {
display: block;
width: 900px;
height: 250px;
.dataPoint { fill:none; stroke:#eea300; stroke-width:0.2; }
&.curve {
path { stroke-width: 0.15; }
}
}
#dataholder {
display: none;
&::before { content: "#{data.$sim_xydata}"; }
}
</style>
これをコンパイルすると、以下のような曲線に仕上げることができます。
最後に、今回のSvelteコードを簡単に解説しておきます。
とりあえずHTML要素をバインドする
ここでは2次のベシェ曲線を描くための各制御点を何処にすると良いかという話で、先程の
☆
//...中略
//👇一つ前に使った終点xy座標と制御点の情報を保持
const prevData: number[] = [0, 0, 0, 0];
//👇各始点-終点からなる線分の中点からの制御点の乖離率(0.2 ~ 0.6程度)
const bendCoeff = 0.4;
//...中略
path = `M${dataArr.map(p => {
//👇制御点のx座標 => 線分の中点のx座標
const interpolitedX = (p[0] + prevData[0]) / 2;
//👇制御点のy座標 => 線分の中点のy座標から前回のy座標の差分をモーメンタム量として
// 適当な乖離率(ここでは0.4)を掛けた値で補正
const interpolitedY = (p[1] + prevData[1]) / 2 + bendCoeff * prevData[3];
//👇古い情報を更新
prevData[3] = p[1] - prevData[1];
prevData[0] = p[0];
prevData[1] = p[1];
prevData[2] = interpolitedX;
return `${interpolitedX},${interpolitedY} ${p[0]},${p[1]}`;
}).join('Q')}`
上記のパートでも解説していたように、2次のベシェ曲線の見栄えの良さは制御点をどのように配置させるかによります。
通常、大量のデータ点から一つ一つ制御点を手動で調整することは困難ですので、ある程度曲線の綺麗さには妥協して、コードの自動処理から程よい制御点に収まってくれるような仕組みを考えなければなりません。
大量のデータ点から良い感じにベシェ曲線を描くための方法は探せば色々とあります。
今回の方法は、現在の制御点を置くより一つ前の区間の、y軸方向の変化量をモーメンタム量(勢いのようなもの)と見なし、その変化量に応じて固定した乖離率を掛けた量をその区間の中点からy軸方向移動させるようにしています。
この方法だと、見栄えはさほど完璧とまでは良いませんが、細かい変化する区間で制御点が激しく振動して大きなスパイクになったり、発散して無限大に高い制御点が発生して有りえないことになることは無いでしょう。
ここらへんはプログラマーの好みで調整してみられると良いと思います。
まとめ
今回はSvelteとSassを連携させる形でSVGのPath要素から2次のベシェ曲線を描く方法を検討してみました。
また、このブログ記事の企画でもっともチャレンジングな課題であった
CSSだけで曲線をもっと楽に書ける方法が今後確立されるかも知れないので、そのときにはまたこのブログにて紹介できれば幸いです。
記事を書いた人
ナンデモ系エンジニア
主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。
カテゴリー