ソースコード共有サービス emoscode (エモすこ)のせわしない新機能

先日 [emoscode(エモすこ)](https://emoscode.net/) というWebサービスをリリースしました。詳しくは[ひとり開発アドベントカレンダー](https://qiita.com/advent-calendar/2019/solo-developer)に投稿した[12/4の記事](https://qiita.com/tomotaco/items/c07a6896395b582c35c2)をご覧ください。

お気づきの方もおられると思いますが、このサービス、コメントの数が増えてくると重大な問題が発生しています。そう、「コメントに隠れてソースコードが見辛い」というものです。当初のGUIではそれぞれのコメントをドラッグ移動で動かしてやらないと、その下に隠れているコードを見る事ができませんでした。

このたび、これを解決する画期的な新機能をリリースいたしました。それではご覧ください。

![emoscode201911205-2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/59265/9998aa6c-f29d-d00d-69f0-bdb4d697faf5.gif)

一気にせわしなくなりましたね! 師走だけに!


コメントの物理っぽい挙動はこんな感じのコードになっています。
これを requestAnimationFrame() に設定した関数から呼び出しています。

```caption.ts

export class Caption
{
        :
    updatePosition(): void {
        if (this.arrow == null) return;
        if (draggingArrow == this.idComment) return;
        if (this.isEditing) return;
        const posCaption = this.getRect().getCenter();
        const velocityTick = this.velocity.getScaled(timeElapsedSecond);
        const posCaptionNext = posCaption.getAdded(velocityTick);
        const posMarker = this.arrow.rectText.getCenter();
        const vecNext = posMarker.getSubtracted(posCaptionNext);
        const lengthNext = vecNext.getLength();
        const unitVecNext = vecNext.getNormalized();
        const initLength = this.arrow.initLength;
        const magnitudeToModify = (lengthNext - initLength) * 0.1;
        const velocityTickNext = velocityTick.getAdded(unitVecNext.getScaled(magnitudeToModify));
        this.velocity = velocityTickNext.getScaled(1.0 / timeElapsedSecond);        
        this.setLeftTopPosition(this.x + velocityTickNext.x, this.y + velocityTickNext.y);
    }
        :
}
```

上記コードで使用している2次元ベクトル・矩形クラスは以下のようなコードになっています。当初出来合いのライブラリを探したのですが Javascript/Typescript で目的に沿うシンプルなものが見当たらず、結局自作しました。

```rect.ts
export class Vector2
{
	x: number = 0.0;
	y: number = 0.0;

	constructor(x: number, y: number)
	{
		this.x = x;
		this.y = y;
	}

	setX(x: number): void
	{
		this.x = x;
	}

	setY(y: number): void
	{
		this.y = y;
	}
	
	w(): number
	{
		return this.x;
	}
	
	h(): number
	{
		return this.y;
	}
	
	clone(): Vector2
	{
		return new Vector2(this.x, this.y);
	}
	
	getSquaredLength(): number
	{
		return this.x * this.x + this.y * this.y;
	}

	getLength(): number
	{
		return Math.sqrt(this.getSquaredLength());
	}

	scale(s: number): void
	{
		this.x *= s;
		this.y *= s;
	}
	
	getScaled(s: number): Vector2
	{
		let ret = this.clone();
		ret.scale(s);
		return ret;
	}
	
	add(vec: Vector2): void
	{
		this.x += vec.x;
		this.y += vec.y;
	}

	getAdded(vec: Vector2): Vector2
	{
		let ret = this.clone();
		ret.add(vec);
		return ret;
	}
	
	subtract(vec: Vector2): void
	{
		this.x -= vec.x;
		this.y -= vec.y;
	}
	
	getSubtracted(vec: Vector2): Vector2
	{
		let ret = this.clone();
		ret.subtract(vec);
		return ret;
	}
	
	normalize(): void
	{
		let length = this.getLength();
		let inversedLength = 1.0 / length;
		this.scale(inversedLength);
	}
	
	getNormalized(): Vector2
	{
		let ret = this.clone();
		ret.normalize();
		return ret;
	}
	
	getPerpendicular(): Vector2
	{
		return new Vector2(this.y, -this.x);
	}		
}

export class Rect2
{
	posLeftTop: Vector2;
	size: Vector2;
	constructor(posLeftTop: Vector2, size: Vector2)
	{
		this.posLeftTop = posLeftTop;
		this.size = size;
	}
	
	getCenter(): Vector2
	{
		return this.posLeftTop.getAdded(this.size.getScaled(0.5));
	}
	
	setPosition(posLeftTop: Vector2): void
	{
		this.posLeftTop = posLeftTop;
	}
	
	getNearestX(x: number): number
	{
		let distanceXLeft = Math.abs(x - this.posLeftTop.x);
		let rightX = this.posLeftTop.x + this.size.w();
		let distanceXRight = Math.abs(x - rightX);
		return distanceXLeft < distanceXRight ? this.posLeftTop.x : rightX;
	}

	getNearestY(y: number): number
	{
		let distanceYTop = Math.abs(y - this.posLeftTop.y);
		let bottomY = this.posLeftTop.y + this.size.h();
		let distanceYBottom = Math.abs(y - bottomY);
		return distanceYTop < distanceYBottom ? this.posLeftTop.y : bottomY;
	}
}
```

以上となります!