はじめに
libtessの応用として、与えられた図形をバラバラにしてみようと思います。
コード全文
依存ライブラリ
<script src="https://cdn.jsdelivr.net/npm/p5@2.0.5/lib/p5.js"></script>
<script src="https://cdn.jsdelivr.net/npm/libtess@1.2.2/libtess.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/opentype.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fisce.js@1.1.6/src/index.min.js"></script>
メインコード
const {ResourceLoader} = fisce.foxUtils;
const {getTextContours} = fisce.foxApplications;
const {initTessy, triangulate} = fisce.foxTess;
/*
手順
textToContoursでとりあえず取得しちゃう
libtess用に改変
tessyを初期化
triangulateのnonzeroでcontour結合
再びベクトル配列に戻す
おわり。
fisceは1.1.6になりました
textToContoursはブラウザ依存で不便なので自作ライブラリのgetTextContoursを使うことにしました
p5は1.11.10にしました。vectorのrotateがバグってるようです。2.0.5は使いません。
原因わかったのでとりあえず2.0.5で書きます。
*/
async function setup() {
createCanvas(400, 400);
const ab = await ResourceLoader.getArrayBuffer("https://inaridarkfox4231.github.io/assets/KosugiMaru-Regular.ttf");
const font = await opentype.parse(ab);
const prepreContours = getTextContours({
targetText:"軛", textScale:280, font:font, position:{x:width/2, y:height/2}, alignV:"center", alignH:"center"
});
const preContours = [];
for(const ctr of prepreContours){
const contour = [];
for(let k=0; k<ctr.length; k++){
contour.push(ctr[k].x, ctr[k].y);
}
preContours.push(contour);
}
initTessy(libtess);
const rawContours = triangulate(preContours, {rule:"nonzero", boundaryOnly:true});
const ctx = drawingContext;
ctx.fillStyle = "gray";
const contours1 = sliceContours(rawContours, 0, PI/3);
const contours2 = sliceContours(contours1, 10, -PI/5);
const contours3 = sliceContours(contours2, 10, -PI/3-0.08);
const contours4 = sliceContours(contours3, 5, PI/2);
const contours5 = sliceContours(contours4, 5, PI*7/6);
const contours6 = sliceContours(contours5, 50, PI*3/4);
const contours7 = sliceContours(contours6, 50, PI*5/4);
const contours8 = sliceContours(contours7, 50, PI*7/4);
const contours9 = sliceContours(contours8, 0, 0);
const contours10 = sliceContours(contours9, 25, PI*3/4);
const contours11 = sliceContours(contours10, 50, PI/4);
const contours12 = sliceContours(contours11, 50, PI*3/2);
const contours13 = sliceContours(contours12, 80, -PI/4);
//drawContours(contours3, ctx);
const shapes = [];
const allContours = [rawContours, contours1, contours2, contours3, contours4, contours5, contours6, contours7, contours8, contours9, contours10, contours11, contours12, contours13];
let _id=0;
draw = () => {
background(205);
if(_id<14){
drawContours(allContours[_id], ctx);
}else{
for(const h of shapes){
h.update();
h.display(ctx);
}
for(let k=shapes.length-1; k>=0; k--){
if(!shapes[k].alive){
shapes.splice(k,1);
}
}
if(shapes.length===0){
console.log("complete!");
noLoop();
}
}
if(frameCount%8===0){_id++;}
if(_id === 14){
for(const ctr of contours13){
shapes.push(new Shape(ctr));
}
_id++;
}
}
}
function shiftContours(contours, a, b){
for(const contour of contours){
for(let i=0; i<contour.length; i+=2){
contour[i] += a;
contour[i+1] += b;
}
}
}
// libtess用
function drawContours(contours, ctx){
ctx.beginPath();
for(const contour of contours){
for(let k=0; k<contour.length; k+=2){
if(k===0){
ctx.moveTo(contour[0], contour[1]);
}else{
ctx.lineTo(contour[k], contour[k+1]);
}
}
ctx.lineTo(contour[0], contour[1]);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
}
// p5.Vector用
function drawContoursForVector(contours, ctx){
ctx.beginPath();
for(const contour of contours){
for(let k=0; k<contour.length; k++){
if(k===0){
ctx.moveTo(contour[0].x, contour[0].y);
}else{
ctx.lineTo(contour[k].x, contour[k].y);
}
}
ctx.lineTo(contour[0].x, contour[0].y);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
}
function sliceContours(contours, r, t){
const L = Math.max(width,height)*4;
let corners = [createVector(-L,-L),createVector(L,-L),createVector(L,L),createVector(-L,L)];
corners = corners.map((v) => {return v.add(r,-r);});
corners = corners.map((v) => {return v.rotate(t);});
corners = corners.map((v) => {return v.add(width/2,height/2);});
// rawに三角を加えてabs_geq_twoを使う
const lower = triangulate([...contours, [corners[0].x, corners[0].y, corners[1].x, corners[1].y, corners[2].x, corners[2].y]], {rule:"abs_geq_two", boundaryOnly:true});
// additiveを逆向きにしてabs_geq_twoを使う
const upper = triangulate([...contours, [corners[0].x, corners[0].y, corners[2].x, corners[2].y, corners[3].x, corners[3].y]], {rule:"abs_geq_two", boundaryOnly:true});
shiftContours(lower, 2*cos(t-PI/4), 2*sin(t-PI/4));
shiftContours(upper, -2*cos(t-PI/4), -2*sin(t-PI/4));
return [...lower, ...upper];
}
// 最初に重心とって
// あとはそこからの変位でやればいい
// ついでに重心から最も遠い距離取る
// それであのあれ、
// 回転させたときに
// ああ限界をね
// 計算できるわけよ
class Shape{
constructor(ctr){
this.v = createVector(random(0.2,0.8)*random([-1,1]), -random(5,10));
this.a = createVector(0, 0.8);
this.r = random([-1,1])*random(TAU/240,TAU/160);
this.alive = true;
// 先にベクトル化する
this.ctr = [];
for(let i=0; i<ctr.length; i+=2){ this.ctr.push(createVector(ctr[i], ctr[i+1])); }
// 重心計算
this.g = this.ctr.reduce((p0, p1) => p0.add(p1), createVector(0, 0)); // 2.0.5では()だとバグる
this.g.div(this.ctr.length);
// 重心の分だけ引く
for(const v of this.ctr){ v.sub(this.g); }
// 半径
this.radius = 0;
for(const v of this.ctr){ this.radius = Math.max(this.radius, v.length); }
}
update(){
this.v.add(this.a);
this.g.add(this.v);
for(const v of this.ctr){
v.rotate(this.r);
}
}
display(ctx){
if(this.g.y - this.radius > height*1.5){
this.alive = false;
return;
}
ctx.translate(this.g.x, this.g.y);
drawContoursForVector([this.ctr], ctx);
ctx.translate(-this.g.x, -this.g.y);
}
}
結果
説明
スライスにはabs_geq_twoを使います。以前説明した方法を使っています。
それで、割った後で若干ずらしています。これをやらないと、くっついてるとみなされ、いくら切ってもくっついたままになってしまうからです。壊した後は、それぞれの図形を重心計算して、重心の周りに回転させながら落としています。以上です。
なお自作ライブラリのgetTextContoursを使っています。textToContoursが2.0.5で実装されたんですが、ブラウザにより結果が異なるため、はっきり言って、使いたくないからです。
おわりに
拝読感謝。
