modern-text
Version:
Measure and render text in a way that describes the DOM.
317 lines (313 loc) • 10.2 kB
JavaScript
'use strict';
const modernPath2d = require('modern-path2d');
class ShapeCurve {
arcLengthDivisions = 200;
_cacheArcLengths;
_needsUpdate = false;
getPointAt(u, output = new modernPath2d.Vector2()) {
return this.getPoint(this.getUToTMapping(u), output);
}
getLength() {
const lengths = this.getLengths();
return lengths[lengths.length - 1];
}
getLengths(divisions = this.arcLengthDivisions) {
if (this._cacheArcLengths && this._cacheArcLengths.length === divisions + 1 && !this._needsUpdate) {
return this._cacheArcLengths;
}
this._needsUpdate = false;
const cache = [0];
let last = this.getPoint(0);
let sum = 0;
for (let i = 1; i <= divisions; i++) {
const current = this.getPoint(i / divisions);
sum += current.distanceTo(last);
cache.push(sum);
last = current;
}
this._cacheArcLengths = cache;
return cache;
}
getUToTMapping(u, distance) {
const lengths = this.getLengths();
const lengthsLen = lengths.length;
const targetLength = distance === void 0 ? u * lengths[lengthsLen - 1] : distance;
let low = 0;
let high = lengthsLen - 1;
let i = 0;
while (low <= high) {
i = Math.floor(low + (high - low) / 2);
const comparison = lengths[i] - targetLength;
if (comparison < 0) {
low = i + 1;
} else if (comparison > 0) {
high = i - 1;
} else {
high = i;
break;
}
}
i = high;
if (lengths[i] === targetLength) {
return i / (lengthsLen - 1);
}
const lengthBefore = lengths[i];
const lengthAfter = lengths[i + 1];
const segmentLength = lengthAfter - lengthBefore;
const segmentFraction = (targetLength - lengthBefore) / segmentLength;
return (i + segmentFraction) / (lengthsLen - 1);
}
getTangent(t, output = new modernPath2d.Vector2()) {
const delta = 1e-4;
const t1 = Math.max(0, t - delta);
const t2 = Math.min(1, t + delta);
return output.copyFrom(this.getPoint(t2).sub(this.getPoint(t1)).normalize());
}
getNormal(t, output = new modernPath2d.Vector2()) {
this.getTangent(t, output);
return output.set(-output.y, output.x).normalize();
}
}
class SegmentedCurve extends ShapeCurve {
curveT = 0;
getPoint(t, output = new modernPath2d.Vector2()) {
return this.getCurve(t).getPoint(this.curveT, output);
}
getPointAt(u, output = new modernPath2d.Vector2()) {
return this.getPoint(u, output);
}
getTangent(t, output = new modernPath2d.Vector2()) {
return this.getCurve(t).getTangent(this.curveT, output);
}
getNormal(t, output = new modernPath2d.Vector2()) {
return this.getCurve(t).getNormal(this.curveT, output);
}
}
class LineShapeCurve extends ShapeCurve {
constructor(start, end) {
super();
this.start = start;
this.end = end;
}
getPoint(t, output = new modernPath2d.Vector2()) {
if (t === 1) {
output.copyFrom(this.end);
} else {
output.copyFrom(this.end).sub(this.start).scale(t).add(this.start);
}
return output;
}
getPointAt(u, output = new modernPath2d.Vector2()) {
return this.getPoint(u, output);
}
getTangent(_t, output = new modernPath2d.Vector2()) {
return output.subVectors(this.end, this.start).normalize();
}
}
class CircleCurve extends ShapeCurve {
constructor(center, radius, start = 0, end = Math.PI * 2) {
super();
this.center = center;
this.radius = radius;
this.start = start;
this.end = end;
}
getPoint(t, output = new modernPath2d.Vector2()) {
return output.copyFrom(this.center).add(this.getNormal(t).scale(this.radius));
}
getTangent(t, output = new modernPath2d.Vector2()) {
const { x, y } = this.getNormal(t);
return output.set(-y, x);
}
getNormal(t, output = new modernPath2d.Vector2()) {
const { start, end } = this;
const _t = t * (end - start) + start - 0.5 * Math.PI;
return output.set(Math.cos(_t), Math.sin(_t));
}
}
class EllipseCurve extends ShapeCurve {
constructor(center = new modernPath2d.Vector2(), radiusX = 1, radiusY = 1, rotation = 0, startAngle = 0, endAngle = Math.PI * 2, clockwise = false) {
super();
this.center = center;
this.radiusX = radiusX;
this.radiusY = radiusY;
this.rotation = rotation;
this.startAngle = startAngle;
this.endAngle = endAngle;
this.clockwise = clockwise;
}
getPoint(t, output = new modernPath2d.Vector2()) {
const twoPi = Math.PI * 2;
let deltaAngle = this.endAngle - this.startAngle;
const samePoints = Math.abs(deltaAngle) < Number.EPSILON;
while (deltaAngle < 0) {
deltaAngle += twoPi;
}
while (deltaAngle > twoPi) {
deltaAngle -= twoPi;
}
if (deltaAngle < Number.EPSILON) {
deltaAngle = samePoints ? 0 : twoPi;
}
if (this.clockwise && !samePoints) {
deltaAngle = deltaAngle === twoPi ? -twoPi : deltaAngle - twoPi;
}
const angle = this.startAngle + t * deltaAngle;
let x = this.center.x + this.radiusX * Math.cos(angle);
let y = this.center.y + this.radiusY * Math.sin(angle);
if (this.rotation !== 0) {
const cos = Math.cos(this.rotation);
const sin = Math.sin(this.rotation);
const tx = x - this.center.x;
const ty = y - this.center.y;
x = tx * cos - ty * sin + this.center.x;
y = tx * sin + ty * cos + this.center.y;
}
return output.set(x, y);
}
}
class HeartCurve extends SegmentedCurve {
constructor(center, size, start = 0, end = 1) {
super();
this.center = center;
this.size = size;
this.start = start;
this.end = end;
this.update();
}
curves = [];
update() {
const { x, y } = this.center;
const c1Center = new modernPath2d.Vector2(x + 0.5 * this.size, y - 0.5 * this.size);
const c5Center = new modernPath2d.Vector2(x - 0.5 * this.size, y - 0.5 * this.size);
const c3Center = new modernPath2d.Vector2(x, y + 0.5 * this.size);
const curve1 = new CircleCurve(c1Center, Math.SQRT1_2 * this.size, -0.25 * Math.PI, 0.75 * Math.PI);
const curve5 = new CircleCurve(c5Center, Math.SQRT1_2 * this.size, -0.75 * Math.PI, 0.25 * Math.PI);
const curve3 = new CircleCurve(c3Center, 0.5 * Math.SQRT1_2 * this.size, 0.75 * Math.PI, 1.25 * Math.PI);
const bottom = new modernPath2d.Vector2(x, y + this.size);
const right = new modernPath2d.Vector2(x + this.size, y);
const rightInner = modernPath2d.Vector2.lerp(right, bottom, 0.75);
const left = new modernPath2d.Vector2(x - this.size, y);
const leftInner = modernPath2d.Vector2.lerp(left, bottom, 0.75);
const curve2 = new LineShapeCurve(right, rightInner);
const curve4 = new LineShapeCurve(leftInner, left);
this.curves = [curve1, curve2, curve3, curve4, curve5];
return this;
}
getCurve(t) {
let val = (t * (this.end - this.start) + this.start) % 1;
if (val < 0) {
val += 1;
}
val *= 9 * Math.PI / 8 + 1.5;
const PI_1_2 = 0.5 * Math.PI;
let index;
if (val < PI_1_2) {
index = 0;
this.curveT = val / PI_1_2;
} else if (val < PI_1_2 + 0.75) {
index = 1;
this.curveT = (val - PI_1_2) / 0.75;
} else if (val < 5 * Math.PI / 8 + 0.75) {
index = 2;
this.curveT = (val - PI_1_2 - 0.75) / (Math.PI / 8);
} else if (val < 5 * Math.PI / 8 + 1.5) {
index = 3;
this.curveT = (val - 5 * Math.PI / 8 - 0.75) / 0.75;
} else {
index = 4;
this.curveT = (val - 5 * Math.PI / 8 - 1.5) / PI_1_2;
}
return this.curves[index];
}
}
class PolygonCurve extends SegmentedCurve {
constructor(center, radius = 0, number = 0, start = 0, end = 1) {
super();
this.center = center;
this.radius = radius;
this.number = number;
this.start = start;
this.end = end;
this.update();
}
curves = [];
points = [];
update() {
for (let i = 0; i < this.number; i++) {
let radian = i * 2 * Math.PI / this.number;
radian -= 0.5 * Math.PI;
this.points.push(
new modernPath2d.Vector2(this.radius * Math.cos(radian), this.radius * Math.sin(radian)).add(this.center)
);
}
for (let i = 0; i < this.number; i++) {
this.curves.push(new LineShapeCurve(this.points[i], this.points[(i + 1) % this.number]));
}
return this;
}
getCurve(t) {
let pos = (t * (this.end - this.start) + this.start) % 1;
if (pos < 0) {
pos += 1;
}
const v = pos * this.number;
const index = Math.floor(v);
this.curveT = v - index;
return this.curves[index];
}
}
class RectangularCurve extends SegmentedCurve {
constructor(center, rx, aspectRatio = 1, start = 0, end = 1) {
super();
this.center = center;
this.rx = rx;
this.aspectRatio = aspectRatio;
this.start = start;
this.end = end;
this.update();
}
curves = [];
update() {
const { x, y } = this.center;
const offsetX = this.rx;
const offsetY = this.rx / this.aspectRatio;
const points = [
new modernPath2d.Vector2(x - offsetX, y - offsetY),
new modernPath2d.Vector2(x + offsetX, y - offsetY),
new modernPath2d.Vector2(x + offsetX, y + offsetY),
new modernPath2d.Vector2(x - offsetX, y + offsetY)
];
for (let i = 0; i < 4; i++) {
this.curves.push(new LineShapeCurve(points[i].clone(), points[(i + 1) % 4].clone()));
}
return this;
}
getCurve(t) {
let current = (t * (this.end - this.start) + this.start) % 1;
if (current < 0) {
current += 1;
}
current *= (1 + this.aspectRatio) * 2;
let i;
if (current < this.aspectRatio) {
i = 0;
this.curveT = current / this.aspectRatio;
} else if (current < this.aspectRatio + 1) {
i = 1;
this.curveT = current - this.aspectRatio;
} else if (current < 2 * this.aspectRatio + 1) {
i = 2;
this.curveT = (current - this.aspectRatio - 1) / this.aspectRatio;
} else {
i = 3;
this.curveT = current - 2 * this.aspectRatio - 1;
}
return this.curves[i];
}
}
exports.CircleCurve = CircleCurve;
exports.EllipseCurve = EllipseCurve;
exports.HeartCurve = HeartCurve;
exports.PolygonCurve = PolygonCurve;
exports.RectangularCurve = RectangularCurve;