30
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

SVGを書くのが面倒なので入力補完できるようにしてみる 1

Last updated at Posted at 2020-07-11

 「素直にInkscapeとかイラレ使えば?」で片付く話なんですけども...お付き合いいただきたい。

これ ↓ 作ってるときの話です。
GitHub: mafumafuultu/svg.js dev

シリーズ

  1. SVGを書くのが面倒なので入力補完できるようにしてみる 1
    内部に持った状態を変更させるオブジェクト、メソッドチェイン、基本シェイプ、<path>のd要素
  2. SVGを書くのが面倒なので入力補完できるようにしてみる 2
    useに潜んでいた罠、<animate>
  3. SVGを書くのが面倒なので入力補完できるようにしてみる 3
    render周りを便利にしたい
  4. SVGを書くのが面倒なので入力補完できるようにしてみる 4

SVG書きたい!主要タグは知ってる!...属性...うっ...頭が...

IDEなどでは割とHTMLの属性の補完は効くんですが、SVGになるとどうも弱い。
<path>のd属性の書き方とか、シェイプ・画像・テキストレンダーの品質の設定とかそこそこ忘れるし,
<line>x1 y1 x2 y2属性とか書くの面倒くさいですね。
<polyline>points属性も、長くなると数値の羅列の迷路をさまようことになり、
「こっちのN番目と、こっちの N'番目を入れ替えて、ここのN''番目とN'''番目をN''''番目の後ろに...今、何番目....?」と迷子になりがち(個人差あり)

問題点

  • スペルの長い属性なんか覚えてない。故に何を値に設定すればいいとかわからない。
  • ある属性がついていたら、この属性を有効にして、こっちの属性は無視とかがあり面倒
  • 座標の区切りが見づらい

入力補完があって、座標も見やすくなれば大抵のことは解決するんだけども...うーむ...

JSで書けばいい

だって
jsdoc書けば、あいつら補完してくれるし
image.png
メソッドチェインの仕組み用意すれば、候補出るし
image.png
Arrayに座標をダラダラと書いておけば見やすいし、加工も楽なのでは?
image.png

条件

  • Vanilla JS
  • メソッドチェイン
    this返せばいいよね
  • document.createElementNSを何度も書くのは嫌
    → タグ名受け取って、そのタグ名のSVG要素を返すものがほしい。
  • 変数いっぱい作ってappendを何度もタイプするのは嫌
    → 子要素を受け取って自身にappendする関数を用意する
  • タグ名書いたら必須の属性を教える
    line(x1, y1, x2, y2) な感じでタグのチェイン開始時に必要なものを指定しないと動かないようにする
  • 新しい属性が出てきても対応できるようにする
    → 属性の関数を用意するのではなくattrsとか用意してObjectを渡して処理
  • <path>のd属性ってぱっと見わからない
    → d属性を作るチェインを作って<path>に食わせる?

etc.etc...

 これらをうまいことまとめるには
 SVGの要素を状態として持ち、その状態を変化させることに特化した仕組みを組んで、その仕組みに対してPrototype拡張してしまえばいいと考えた。

Prototype(疑似)拡張

で、とりあえずそんなものを組んでみることにした。

  • タグ名渡して、そのSVG要素を返す関数
  • 要素を状態として持つオブジェクト
  • Prototypeとして、すべての要素に用意したい機能
  • タグごとにほしい機能

この辺りは Object.createObject.assign で解決できる

/*
ネームスペース付きのcreateElementは何度も書きたくない
ついでに、HTMLタグも作れるようにした。
*/
const _svgTag = tag => document.createElementNS('http://www.w3.org/2000/svg', tag);
const _tag = tag => document.createElement(tag);

/* 共通でほしい機能 */
const __BASE_PROTO__ = {
    $: {
        value(...child) {
            return /* 状態として持った要素に子要素を追加 */ this;
        }
    },
    attrs: {
        value (o) {
            return /* 属性をセットする */ this;
        }
    },
    /* 略 */
};

var wrapper = function(el custom) {
    return Object.create(
        {'@': el}, // タグを状態として持つオブジェクト
        custom instanceof Object
            ? Object.assign({}, __BASE_PROTO__, custom) /* タグごとにほしい機能 */
            : __BASE_PROTO__
    );
};

基本の仕組みを作ってしまえば50%は完成したようなものなので残りの50%を埋めよう。

完成予定を確認

これを

svg(200, 200).$(
    group().attrs({stroke: 'orange'}).$(
        line(0,0,200,200).attrs({id: "foo"}),
        polyline([[0, 50], [40, 0], [90, 50]]),
        path().start(50, 50, true).line(100, 100).line(50, 100).line(50, 150).close()
    )
);

こうしたい

<svg width="200" height="200" viewBox="0 0 200 200" version="1.2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
	<g stroke="orange">
		<line x1="0" y1="0" x2="200" y2="200" id="foo" />
		<polyline points="0,50 40,0 90, 50" />
		<path d="M 50 50 l 100 100 , 50 100 , 50 150 Z" />
	</g>
</svg>

基本のSVGタグを用意する。

 <svg> <line> <rect> <circle> あたりは確実にほしい。

const svg = (width=400, height=300, viewBox = `0 0 ${width} ${height}`) => wrapper(_svgTag('svg')).attrs({version:1.2,xmlns :"http://www.w3.org/2000/svg", 'xmlns:xlink': 'http://www.w3.org/1999/xlink',width, height, viewBox});
const line = (x1=0,y1=0,x2=0,y2=0) => wrapper(_svgTag('line')).attrs({x1,y1,x2,y2});
const rect = (x=0, y=0, width=0, height=0) => wrapper(_svgTag('rect')).attrs({x, y, width, height});
// その他、基本シェイプも作る

<line>だけで書くのもつらいので <polyline> もほしい

const joinPos = ([x, y]) => `${x},${y}`;
const points = (...p) => p.map(joinPos).join(' ');
const polyline = (...p) => wrapper(_svgTag('polyline')).attrs({points: points(...p)});

<path><animate> はちょっと毛色が違う。
d属性は相対位置指定、絶対位置指定、ベジエ曲線の指定は直前の指定によって値を省略などがあってなかなか面倒ですし、arc(円弧)とか指定が多いので大変です。

const path = () => ({
	before: '',
	d : [],
	__ (s, abs = false)	{return n = abs ? s.toUpperCase() : s, this.before === n ? `, ` : (this.before = n , `${n} `);},
	__cache (v, abs, mk) {return this.d.push(`${this.__(mk, abs)}${v}`), this;},

	start (x, y, abs) {return this.d.length = 0, this.move(x, y, abs);},
	move(x, y, abs) {return this.__cache(`${x} ${y}`, abs, 'm');},
	line(x, y, abs) {return this.__cache(`${x} ${y}`, abs, 'l');},
    /* 略 */
    /* パスを閉じてタグを吐くか、d属性の値を吐けるように */
	close(attr={}, close=true) {return wrapper(_svgTag('path')).attrs({...attr, d: this.d.join(' ') + (close ? ' Z' : '')});},
	toPath(close=true) {return this.d.join(' ') + (close ? ' Z' : '');}
});

 SVGにはアニメーション系のタグがありパスに沿ってアニメーションをすることができるので、そこで使うこともできるように path()でd属性の値を吐けるようにしています。

 これで基本図形・パスをかけるようになりました。
ここまでできれば65%ですかね。

アニメーション系、レンダー、<g> <def>
意外とはまりそうな<use>などは次回以降に回して今回は終わります。

30
39
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
39

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?