「素直にInkscapeとかイラレ使えば?」で片付く話なんですけども...お付き合いいただきたい。
これ ↓ 作ってるときの話です。
GitHub: mafumafuultu/svg.js dev
シリーズ
-
SVGを書くのが面倒なので入力補完できるようにしてみる 1
内部に持った状態を変更させるオブジェクト、メソッドチェイン、基本シェイプ、<path>
のd要素 -
SVGを書くのが面倒なので入力補完できるようにしてみる 2
useに潜んでいた罠、<animate>
-
SVGを書くのが面倒なので入力補完できるようにしてみる 3
render周りを便利にしたい - SVGを書くのが面倒なので入力補完できるようにしてみる 4
SVG書きたい!主要タグは知ってる!...属性...うっ...頭が...
IDEなどでは割とHTMLの属性の補完は効くんですが、SVGになるとどうも弱い。
<path>
のd属性の書き方とか、シェイプ・画像・テキストレンダーの品質の設定とかそこそこ忘れるし,
<line>
のx1
y1
x2
y2
属性とか書くの面倒くさいですね。
<polyline>
のpoints
属性も、長くなると数値の羅列の迷路をさまようことになり、
「こっちのN番目と、こっちの N'番目を入れ替えて、ここのN''番目とN'''番目をN''''番目の後ろに...今、何番目....?」と迷子になりがち(個人差あり)
問題点
- スペルの長い属性なんか覚えてない。故に何を値に設定すればいいとかわからない。
- ある属性がついていたら、この属性を有効にして、こっちの属性は無視とかがあり面倒
- 座標の区切りが見づらい
入力補完があって、座標も見やすくなれば大抵のことは解決するんだけども...うーむ...
JSで書けばいい
だって
jsdoc書けば、あいつら補完してくれるし
メソッドチェインの仕組み用意すれば、候補出るし
Arrayに座標をダラダラと書いておけば見やすいし、加工も楽なのでは?
条件
- 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.create
と Object.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>
などは次回以降に回して今回は終わります。