quickshell and hyprland additions

This commit is contained in:
2026-03-15 13:56:00 +02:00
parent c9c27d1554
commit 1ad06b82a6
509 changed files with 68371 additions and 19 deletions

View File

@@ -0,0 +1,18 @@
.pragma library
/**
* Represents corner rounding configuration
*/
class CornerRounding {
/**
* @param {float} [radius=0]
* @param {float} [smoothing=0]
*/
constructor(radius = 0, smoothing = 0) {
this.radius = radius;
this.smoothing = smoothing;
}
}
// Static property
CornerRounding.Unrounded = new CornerRounding();

View File

@@ -0,0 +1,371 @@
.pragma library
.import "point.js" as PointModule
.import "utils.js" as UtilsModule
var Point = PointModule.Point;
var DistanceEpsilon = UtilsModule.DistanceEpsilon;
var interpolate = UtilsModule.interpolate;
var directionVector = UtilsModule.directionVector;
var distance = UtilsModule.distance;
/**
* Represents a cubic Bézier curve with anchor and control points
*/
class Cubic {
/**
* @param {Array<float>} points Array of 8 numbers [anchor0X, anchor0Y, control0X, control0Y, control1X, control1Y, anchor1X, anchor1Y]
*/
constructor(points) {
this.points = points;
}
get anchor0X() { return this.points[0]; }
get anchor0Y() { return this.points[1]; }
get control0X() { return this.points[2]; }
get control0Y() { return this.points[3]; }
get control1X() { return this.points[4]; }
get control1Y() { return this.points[5]; }
get anchor1X() { return this.points[6]; }
get anchor1Y() { return this.points[7]; }
/**
* @param {Point} anchor0
* @param {Point} control0
* @param {Point} control1
* @param {Point} anchor1
* @returns {Cubic}
*/
static create(anchor0, control0, control1, anchor1) {
return new Cubic([
anchor0.x, anchor0.y,
control0.x, control0.y,
control1.x, control1.y,
anchor1.x, anchor1.y
]);
}
/**
* @param {float} t
* @returns {Point}
*/
pointOnCurve(t) {
const u = 1 - t;
return new Point(
this.anchor0X * (u * u * u) +
this.control0X * (3 * t * u * u) +
this.control1X * (3 * t * t * u) +
this.anchor1X * (t * t * t),
this.anchor0Y * (u * u * u) +
this.control0Y * (3 * t * u * u) +
this.control1Y * (3 * t * t * u) +
this.anchor1Y * (t * t * t)
);
}
/**
* @returns {boolean}
*/
zeroLength() {
return Math.abs(this.anchor0X - this.anchor1X) < DistanceEpsilon &&
Math.abs(this.anchor0Y - this.anchor1Y) < DistanceEpsilon;
}
/**
* @param {Cubic} next
* @returns {boolean}
*/
convexTo(next) {
const prevVertex = new Point(this.anchor0X, this.anchor0Y);
const currVertex = new Point(this.anchor1X, this.anchor1Y);
const nextVertex = new Point(next.anchor1X, next.anchor1Y);
return convex(prevVertex, currVertex, nextVertex);
}
/**
* @param {float} value
* @returns {boolean}
*/
zeroIsh(value) {
return Math.abs(value) < DistanceEpsilon;
}
/**
* @param {Array<float>} bounds
* @param {boolean} [approximate=false]
*/
calculateBounds(bounds, approximate = false) {
if (this.zeroLength()) {
bounds[0] = this.anchor0X;
bounds[1] = this.anchor0Y;
bounds[2] = this.anchor0X;
bounds[3] = this.anchor0Y;
return;
}
let minX = Math.min(this.anchor0X, this.anchor1X);
let minY = Math.min(this.anchor0Y, this.anchor1Y);
let maxX = Math.max(this.anchor0X, this.anchor1X);
let maxY = Math.max(this.anchor0Y, this.anchor1Y);
if (approximate) {
bounds[0] = Math.min(minX, Math.min(this.control0X, this.control1X));
bounds[1] = Math.min(minY, Math.min(this.control0Y, this.control1Y));
bounds[2] = Math.max(maxX, Math.max(this.control0X, this.control1X));
bounds[3] = Math.max(maxY, Math.max(this.control0Y, this.control1Y));
return;
}
// Find extrema using derivatives
const xa = -this.anchor0X + 3 * this.control0X - 3 * this.control1X + this.anchor1X;
const xb = 2 * this.anchor0X - 4 * this.control0X + 2 * this.control1X;
const xc = -this.anchor0X + this.control0X;
if (this.zeroIsh(xa)) {
if (xb != 0) {
const t = 2 * xc / (-2 * xb);
if (t >= 0 && t <= 1) {
const it = this.pointOnCurve(t).x;
if (it < minX) minX = it;
if (it > maxX) maxX = it;
}
}
} else {
const xs = xb * xb - 4 * xa * xc;
if (xs >= 0) {
const t1 = (-xb + Math.sqrt(xs)) / (2 * xa);
if (t1 >= 0 && t1 <= 1) {
const it = this.pointOnCurve(t1).x;
if (it < minX) minX = it;
if (it > maxX) maxX = it;
}
const t2 = (-xb - Math.sqrt(xs)) / (2 * xa);
if (t2 >= 0 && t2 <= 1) {
const it = this.pointOnCurve(t2).x;
if (it < minX) minX = it;
if (it > maxX) maxX = it;
}
}
}
// Repeat for y coord
const ya = -this.anchor0Y + 3 * this.control0Y - 3 * this.control1Y + this.anchor1Y;
const yb = 2 * this.anchor0Y - 4 * this.control0Y + 2 * this.control1Y;
const yc = -this.anchor0Y + this.control0Y;
if (this.zeroIsh(ya)) {
if (yb != 0) {
const t = 2 * yc / (-2 * yb);
if (t >= 0 && t <= 1) {
const it = this.pointOnCurve(t).y;
if (it < minY) minY = it;
if (it > maxY) maxY = it;
}
}
} else {
const ys = yb * yb - 4 * ya * yc;
if (ys >= 0) {
const t1 = (-yb + Math.sqrt(ys)) / (2 * ya);
if (t1 >= 0 && t1 <= 1) {
const it = this.pointOnCurve(t1).y;
if (it < minY) minY = it;
if (it > maxY) maxY = it;
}
const t2 = (-yb - Math.sqrt(ys)) / (2 * ya);
if (t2 >= 0 && t2 <= 1) {
const it = this.pointOnCurve(t2).y;
if (it < minY) minY = it;
if (it > maxY) maxY = it;
}
}
}
bounds[0] = minX;
bounds[1] = minY;
bounds[2] = maxX;
bounds[3] = maxY;
}
/**
* @param {float} t
* @returns {{a: Cubic, b: Cubic}}
*/
split(t) {
const u = 1 - t;
const pointOnCurve = this.pointOnCurve(t);
return {
a: new Cubic([
this.anchor0X,
this.anchor0Y,
this.anchor0X * u + this.control0X * t,
this.anchor0Y * u + this.control0Y * t,
this.anchor0X * (u * u) + this.control0X * (2 * u * t) + this.control1X * (t * t),
this.anchor0Y * (u * u) + this.control0Y * (2 * u * t) + this.control1Y * (t * t),
pointOnCurve.x,
pointOnCurve.y
]),
b: new Cubic([
pointOnCurve.x,
pointOnCurve.y,
this.control0X * (u * u) + this.control1X * (2 * u * t) + this.anchor1X * (t * t),
this.control0Y * (u * u) + this.control1Y * (2 * u * t) + this.anchor1Y * (t * t),
this.control1X * u + this.anchor1X * t,
this.control1Y * u + this.anchor1Y * t,
this.anchor1X,
this.anchor1Y
])
};
}
/**
* @returns {Cubic}
*/
reverse() {
return new Cubic([
this.anchor1X, this.anchor1Y,
this.control1X, this.control1Y,
this.control0X, this.control0Y,
this.anchor0X, this.anchor0Y
]);
}
/**
* @param {Cubic} other
* @returns {Cubic}
*/
plus(other) {
return new Cubic(other.points.map((_, index) => this.points[index] + other.points[index]));
}
/**
* @param {float} x
* @returns {Cubic}
*/
times(x) {
return new Cubic(this.points.map(v => v * x));
}
/**
* @param {float} x
* @returns {Cubic}
*/
div(x) {
return this.times(1 / x);
}
/**
* @param {Cubic} other
* @returns {boolean}
*/
equals(other) {
return this.points.every((p, i) => other.points[i] === p);
}
/**
* @param {function(float, float): Point} f
* @returns {Cubic}
*/
transformed(f) {
const newCubic = new MutableCubic([...this.points]);
newCubic.transform(f);
return newCubic;
}
/**
* @param {float} x0
* @param {float} y0
* @param {float} x1
* @param {float} y1
* @returns {Cubic}
*/
static straightLine(x0, y0, x1, y1) {
return new Cubic([
x0,
y0,
interpolate(x0, x1, 1/3),
interpolate(y0, y1, 1/3),
interpolate(x0, x1, 2/3),
interpolate(y0, y1, 2/3),
x1,
y1
]);
}
/**
* @param {float} centerX
* @param {float} centerY
* @param {float} x0
* @param {float} y0
* @param {float} x1
* @param {float} y1
* @returns {Cubic}
*/
static circularArc(centerX, centerY, x0, y0, x1, y1) {
const p0d = directionVector(x0 - centerX, y0 - centerY);
const p1d = directionVector(x1 - centerX, y1 - centerY);
const rotatedP0 = p0d.rotate90();
const rotatedP1 = p1d.rotate90();
const clockwise = rotatedP0.dotProductScalar(x1 - centerX, y1 - centerY) >= 0;
const cosa = p0d.dotProduct(p1d);
if (cosa > 0.999) {
return Cubic.straightLine(x0, y0, x1, y1);
}
const k = distance(x0 - centerX, y0 - centerY) * 4/3 *
(Math.sqrt(2 * (1 - cosa)) - Math.sqrt(1 - cosa * cosa)) /
(1 - cosa) * (clockwise ? 1 : -1);
return new Cubic([
x0, y0,
x0 + rotatedP0.x * k,
y0 + rotatedP0.y * k,
x1 - rotatedP1.x * k,
y1 - rotatedP1.y * k,
x1, y1
]);
}
/**
* @param {float} x0
* @param {float} y0
* @returns {Cubic}
*/
static empty(x0, y0) {
return new Cubic([x0, y0, x0, y0, x0, y0, x0, y0]);
}
}
class MutableCubic extends Cubic {
/**
* @param {function(float, float): Point} f
*/
transform(f) {
this.transformOnePoint(f, 0);
this.transformOnePoint(f, 2);
this.transformOnePoint(f, 4);
this.transformOnePoint(f, 6);
}
/**
* @param {Cubic} c1
* @param {Cubic} c2
* @param {float} progress
*/
interpolate(c1, c2, progress) {
for (let i = 0; i < 8; i++) {
this.points[i] = interpolate(c1.points[i], c2.points[i], progress);
}
}
/**
* @private
* @param {function(float, float): Point} f
* @param {number} ix
*/
transformOnePoint(f, ix) {
const result = f(this.points[ix], this.points[ix + 1]);
this.points[ix] = result.x;
this.points[ix + 1] = result.y;
}
}

View File

@@ -0,0 +1,166 @@
.pragma library
.import "feature.js" as FeatureModule
.import "float-mapping.js" as MappingModule
.import "point.js" as PointModule
.import "utils.js" as UtilsModule
var Feature = FeatureModule.Feature;
var Corner = FeatureModule.Corner;
var Point = PointModule.Point;
var DoubleMapper = MappingModule.DoubleMapper;
var progressInRange = MappingModule.progressInRange;
var DistanceEpsilon = UtilsModule.DistanceEpsilon;
var IdentityMapping = [{ a: 0, b: 0 }, { a: 0.5, b: 0.5 }];
class ProgressableFeature {
/**
* @param {float} progress
* @param {Feature} feature
*/
constructor(progress, feature) {
this.progress = progress;
this.feature = feature;
}
}
class DistanceVertex {
/**
* @param {float} distance
* @param {ProgressableFeature} f1
* @param {ProgressableFeature} f2
*/
constructor(distance, f1, f2) {
this.distance = distance;
this.f1 = f1;
this.f2 = f2;
}
}
class MappingHelper {
constructor() {
this.mapping = [];
this.usedF1 = new Set();
this.usedF2 = new Set();
}
/**
* @param {ProgressableFeature} f1
* @param {ProgressableFeature} f2
*/
addMapping(f1, f2) {
if (this.usedF1.has(f1) || this.usedF2.has(f2)) {
return;
}
const index = this.mapping.findIndex(x => x.a === f1.progress);
const insertionIndex = -index - 1;
const n = this.mapping.length;
if (n >= 1) {
const { a: before1, b: before2 } = this.mapping[(insertionIndex + n - 1) % n];
const { a: after1, b: after2 } = this.mapping[insertionIndex % n];
if (
progressDistance(f1.progress, before1) < DistanceEpsilon ||
progressDistance(f1.progress, after1) < DistanceEpsilon ||
progressDistance(f2.progress, before2) < DistanceEpsilon ||
progressDistance(f2.progress, after2) < DistanceEpsilon
) {
return;
}
if (n > 1 && !progressInRange(f2.progress, before2, after2)) {
return;
}
}
this.mapping.splice(insertionIndex, 0, { a: f1.progress, b: f2.progress });
this.usedF1.add(f1);
this.usedF2.add(f2);
}
}
/**
* @param {Array<ProgressableFeature>} features1
* @param {Array<ProgressableFeature>} features2
* @returns {DoubleMapper}
*/
function featureMapper(features1, features2) {
const filteredFeatures1 = features1.filter(f => f.feature instanceof Corner);
const filteredFeatures2 = features2.filter(f => f.feature instanceof Corner);
const featureProgressMapping = doMapping(filteredFeatures1, filteredFeatures2);
return new DoubleMapper(...featureProgressMapping);
}
/**
* @param {Array<ProgressableFeature>} features1
* @param {Array<ProgressableFeature>} features2
* @returns {Array<{a: float, b: float}>}
*/
function doMapping(features1, features2) {
const distanceVertexList = [];
for (const f1 of features1) {
for (const f2 of features2) {
const d = featureDistSquared(f1.feature, f2.feature);
if (d !== Number.MAX_VALUE) {
distanceVertexList.push(new DistanceVertex(d, f1, f2));
}
}
}
distanceVertexList.sort((a, b) => a.distance - b.distance);
// Special cases
if (distanceVertexList.length === 0) {
return IdentityMapping;
} else if (distanceVertexList.length === 1) {
const { f1, f2 } = distanceVertexList[0];
const p1 = f1.progress;
const p2 = f2.progress;
return [
{ a: p1, b: p2 },
{ a: (p1 + 0.5) % 1, b: (p2 + 0.5) % 1 }
];
}
const helper = new MappingHelper();
distanceVertexList.forEach(({ f1, f2 }) => helper.addMapping(f1, f2));
return helper.mapping;
}
/**
* @param {Feature} f1
* @param {Feature} f2
* @returns {float}
*/
function featureDistSquared(f1, f2) {
if (f1 instanceof Corner && f2 instanceof Corner && f1.convex != f2.convex) {
return Number.MAX_VALUE;
}
return featureRepresentativePoint(f1).minus(featureRepresentativePoint(f2)).getDistanceSquared();
}
/**
* @param {Feature} feature
* @returns {Point}
*/
function featureRepresentativePoint(feature) {
const firstCubic = feature.cubics[0];
const lastCubic = feature.cubics[feature.cubics.length - 1];
const x = (firstCubic.anchor0X + lastCubic.anchor1X) / 2;
const y = (firstCubic.anchor0Y + lastCubic.anchor1Y) / 2;
return new Point(x, y);
}
/**
* @param {float} p1
* @param {float} p2
* @returns {float}
*/
function progressDistance(p1, p2) {
const it = Math.abs(p1 - p2);
return Math.min(it, 1 - it);
}

View File

@@ -0,0 +1,103 @@
.pragma library
.import "cubic.js" as CubicModule
var Cubic = CubicModule.Cubic;
/**
* Base class for shape features (edges and corners)
*/
class Feature {
/**
* @param {Array<Cubic>} cubics
*/
constructor(cubics) {
this.cubics = cubics;
}
/**
* @param {Array<Cubic>} cubics
* @returns {Edge}
*/
buildIgnorableFeature(cubics) {
return new Edge(cubics);
}
/**
* @param {Cubic} cubic
* @returns {Edge}
*/
buildEdge(cubic) {
return new Edge([cubic]);
}
/**
* @param {Array<Cubic>} cubics
* @returns {Corner}
*/
buildConvexCorner(cubics) {
return new Corner(cubics, true);
}
/**
* @param {Array<Cubic>} cubics
* @returns {Corner}
*/
buildConcaveCorner(cubics) {
return new Corner(cubics, false);
}
}
class Edge extends Feature {
constructor(cubics) {
super(cubics);
this.isIgnorableFeature = true;
this.isEdge = true;
this.isConvexCorner = false;
this.isConcaveCorner = false;
}
/**
* @param {function(float, float): Point} f
* @returns {Feature}
*/
transformed(f) {
return new Edge(this.cubics.map(c => c.transformed(f)));
}
/**
* @returns {Feature}
*/
reversed() {
return new Edge(this.cubics.map(c => c.reverse()));
}
}
class Corner extends Feature {
/**
* @param {Array<Cubic>} cubics
* @param {boolean} convex
*/
constructor(cubics, convex) {
super(cubics);
this.convex = convex;
this.isIgnorableFeature = false;
this.isEdge = false;
this.isConvexCorner = convex;
this.isConcaveCorner = !convex;
}
/**
* @param {function(float, float): Point} f
* @returns {Feature}
*/
transformed(f) {
return new Corner(this.cubics.map(c => c.transformed(f)), this.convex);
}
/**
* @returns {Feature}
*/
reversed() {
return new Corner(this.cubics.map(c => c.reverse()), !this.convex);
}
}

View File

@@ -0,0 +1,86 @@
.pragma library
.import "utils.js" as UtilsModule
var positiveModulo = UtilsModule.positiveModulo;
/**
* Maps values between two ranges
*/
class DoubleMapper {
constructor(...mappings) {
this.sourceValues = [];
this.targetValues = [];
for (const mapping of mappings) {
this.sourceValues.push(mapping.a);
this.targetValues.push(mapping.b);
}
}
/**
* @param {float} x
* @returns {float}
*/
map(x) {
return linearMap(this.sourceValues, this.targetValues, x);
}
/**
* @param {float} x
* @returns {float}
*/
mapBack(x) {
return linearMap(this.targetValues, this.sourceValues, x);
}
}
// Static property
DoubleMapper.Identity = new DoubleMapper({ a: 0, b: 0 }, { a: 0.5, b: 0.5 });
/**
* @param {Array<float>} xValues
* @param {Array<float>} yValues
* @param {float} x
* @returns {float}
*/
function linearMap(xValues, yValues, x) {
let segmentStartIndex = -1;
for (let i = 0; i < xValues.length; i++) {
const nextIndex = (i + 1) % xValues.length;
if (progressInRange(x, xValues[i], xValues[nextIndex])) {
segmentStartIndex = i;
break;
}
}
if (segmentStartIndex === -1) {
throw new Error("No valid segment found");
}
const segmentEndIndex = (segmentStartIndex + 1) % xValues.length;
const segmentSizeX = positiveModulo(xValues[segmentEndIndex] - xValues[segmentStartIndex], 1);
const segmentSizeY = positiveModulo(yValues[segmentEndIndex] - yValues[segmentStartIndex], 1);
let positionInSegment;
if (segmentSizeX < 0.001) {
positionInSegment = 0.5;
} else {
positionInSegment = positiveModulo(x - xValues[segmentStartIndex], 1) / segmentSizeX;
}
return positiveModulo(yValues[segmentStartIndex] + segmentSizeY * positionInSegment, 1);
}
/**
* @param {float} progress
* @param {float} progressFrom
* @param {float} progressTo
* @returns {boolean}
*/
function progressInRange(progress, progressFrom, progressTo) {
if (progressTo >= progressFrom) {
return progress >= progressFrom && progress <= progressTo;
} else {
return progress >= progressFrom || progress <= progressTo;
}
}

View File

@@ -0,0 +1,94 @@
.pragma library
.import "rounded-polygon.js" as RoundedPolygon
.import "cubic.js" as Cubic
.import "polygon-measure.js" as PolygonMeasure
.import "feature-mapping.js" as FeatureMapping
.import "utils.js" as Utils
class Morph {
constructor(start, end) {
this.morphMatch = this.match(start, end)
}
asCubics(progress) {
const ret = []
// The first/last mechanism here ensures that the final anchor point in the shape
// exactly matches the first anchor point. There can be rendering artifacts introduced
// by those points being slightly off, even by much less than a pixel
let firstCubic = null
let lastCubic = null
for (let i = 0; i < this.morphMatch.length; i++) {
const cubic = new Cubic.Cubic(Array.from({ length: 8 }).map((_, it) => Utils.interpolate(
this.morphMatch[i].a.points[it],
this.morphMatch[i].b.points[it],
progress,
)))
if (firstCubic == null)
firstCubic = cubic
if (lastCubic != null)
ret.push(lastCubic)
lastCubic = cubic
}
if (lastCubic != null && firstCubic != null)
ret.push(
new Cubic.Cubic([
lastCubic.anchor0X,
lastCubic.anchor0Y,
lastCubic.control0X,
lastCubic.control0Y,
lastCubic.control1X,
lastCubic.control1Y,
firstCubic.anchor0X,
firstCubic.anchor0Y,
])
)
return ret
}
forEachCubic(progress, mutableCubic, callback) {
for (let i = 0; i < this.morphMatch.length; i++) {
mutableCubic.interpolate(this.morphMatch[i].a, this.morphMatch[i].b, progress)
callback(mutableCubic)
}
}
match(p1, p2) {
const measurer = new PolygonMeasure.LengthMeasurer()
const measuredPolygon1 = PolygonMeasure.MeasuredPolygon.measurePolygon(measurer, p1)
const measuredPolygon2 = PolygonMeasure.MeasuredPolygon.measurePolygon(measurer, p2)
const features1 = measuredPolygon1.features
const features2 = measuredPolygon2.features
const doubleMapper = FeatureMapping.featureMapper(features1, features2)
const polygon2CutPoint = doubleMapper.map(0)
const bs1 = measuredPolygon1
const bs2 = measuredPolygon2.cutAndShift(polygon2CutPoint)
const ret = []
let i1 = 0
let i2 = 0
let b1 = bs1.cubics[i1++]
let b2 = bs2.cubics[i2++]
while (b1 != null && b2 != null) {
const b1a = (i1 == bs1.cubics.length) ? 1 : b1.endOutlineProgress
const b2a = (i2 == bs2.cubics.length) ? 1 : doubleMapper.mapBack(Utils.positiveModulo(b2.endOutlineProgress + polygon2CutPoint, 1))
const minb = Math.min(b1a, b2a)
const { a: seg1, b: newb1 } = b1a > minb + Utils.AngleEpsilon ? b1.cutAtProgress(minb) : { a: b1, b: bs1.cubics[i1++] }
const { a: seg2, b: newb2 } = b2a > minb + Utils.AngleEpsilon ? b2.cutAtProgress(Utils.positiveModulo(doubleMapper.map(minb) - polygon2CutPoint, 1)) : { a: b2, b: bs2.cubics[i2++] }
ret.push({ a: seg1.cubic, b: seg2.cubic })
b1 = newb1
b2 = newb2
}
return ret
}
}

View File

@@ -0,0 +1,154 @@
.pragma library
/**
* @param {number} x
* @param {number} y
* @returns {Point}
*/
function createPoint(x, y) {
return new Point(x, y);
}
class Point {
/**
* @param {float} x
* @param {float} y
*/
constructor(x, y) {
this.x = x;
this.y = y;
}
/**
* @param {float} x
* @param {float} y
* @returns {Point}
*/
copy(x = this.x, y = this.y) {
return new Point(x, y);
}
/**
* @returns {float}
*/
getDistance() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
/**
* @returns {float}
*/
getDistanceSquared() {
return this.x * this.x + this.y * this.y;
}
/**
* @param {Point} other
* @returns {float}
*/
dotProduct(other) {
return this.x * other.x + this.y * other.y;
}
/**
* @param {float} otherX
* @param {float} otherY
* @returns {float}
*/
dotProductScalar(otherX, otherY) {
return this.x * otherX + this.y * otherY;
}
/**
* @param {Point} other
* @returns {boolean}
*/
clockwise(other) {
return this.x * other.y - this.y * other.x > 0;
}
/**
* @returns {Point}
*/
getDirection() {
const d = this.getDistance();
return this.div(d);
}
/**
* @returns {Point}
*/
negate() {
return new Point(-this.x, -this.y);
}
/**
* @param {Point} other
* @returns {Point}
*/
minus(other) {
return new Point(this.x - other.x, this.y - other.y);
}
/**
* @param {Point} other
* @returns {Point}
*/
plus(other) {
return new Point(this.x + other.x, this.y + other.y);
}
/**
* @param {float} operand
* @returns {Point}
*/
times(operand) {
return new Point(this.x * operand, this.y * operand);
}
/**
* @param {float} operand
* @returns {Point}
*/
div(operand) {
return new Point(this.x / operand, this.y / operand);
}
/**
* @param {float} operand
* @returns {Point}
*/
rem(operand) {
return new Point(this.x % operand, this.y % operand);
}
/**
* @param {Point} start
* @param {Point} stop
* @param {float} fraction
* @returns {Point}
*/
static interpolate(start, stop, fraction) {
return new Point(
start.x + (stop.x - start.x) * fraction,
start.y + (stop.y - start.y) * fraction
);
}
/**
* @param {function(float, float): Point} f
* @returns {Point}
*/
transformed(f) {
const result = f(this.x, this.y);
return new Point(result.x, result.y);
}
/**
* @returns {Point}
*/
rotate90() {
return new Point(-this.y, this.x);
}
}

View File

@@ -0,0 +1,192 @@
.pragma library
.import "cubic.js" as Cubic
.import "point.js" as Point
.import "feature-mapping.js" as FeatureMapping
.import "utils.js" as Utils
.import "feature.js" as Feature
class MeasuredPolygon {
constructor(measurer, features, cubics, outlineProgress) {
this.measurer = measurer
this.features = features
this.outlineProgress = outlineProgress
this.cubics = []
const measuredCubics = []
let startOutlineProgress = 0
for(let i = 0; i < cubics.length; i++) {
if ((outlineProgress[i + 1] - outlineProgress[i]) > Utils.DistanceEpsilon) {
measuredCubics.push(
new MeasuredCubic(this, cubics[i], startOutlineProgress, outlineProgress[i + 1])
)
// The next measured cubic will start exactly where this one ends.
startOutlineProgress = outlineProgress[i + 1]
}
}
measuredCubics[measuredCubics.length - 1].updateProgressRange(measuredCubics[measuredCubics.length - 1].startOutlineProgress, 1)
this.cubics = measuredCubics
}
cutAndShift(cuttingPoint) {
if (cuttingPoint < Utils.DistanceEpsilon) return this
// Find the index of cubic we want to cut
const targetIndex = this.cubics.findIndex(it => cuttingPoint >= it.startOutlineProgress && cuttingPoint <= it.endOutlineProgress)
const target = this.cubics[targetIndex]
// Cut the target cubic.
// b1, b2 are two resulting cubics after cut
const { a: b1, b: b2 } = target.cutAtProgress(cuttingPoint)
// Construct the list of the cubics we need:
// * The second part of the target cubic (after the cut)
// * All cubics after the target, until the end + All cubics from the start, before the
// target cubic
// * The first part of the target cubic (before the cut)
const retCubics = [b2.cubic]
for(let i = 1; i < this.cubics.length; i++) {
retCubics.push(this.cubics[(i + targetIndex) % this.cubics.length].cubic)
}
retCubics.push(b1.cubic)
// Construct the array of outline progress.
// For example, if we have 3 cubics with outline progress [0 .. 0.3], [0.3 .. 0.8] &
// [0.8 .. 1.0], and we cut + shift at 0.6:
// 0. 0123456789
// |--|--/-|-|
// The outline progresses will start at 0 (the cutting point, that shifs to 0.0),
// then 0.8 - 0.6 = 0.2, then 1 - 0.6 = 0.4, then 0.3 - 0.6 + 1 = 0.7,
// then 1 (the cutting point again),
// all together: (0.0, 0.2, 0.4, 0.7, 1.0)
const retOutlineProgress = []
for (let i = 0; i < this.cubics.length + 2; i++) {
if (i === 0) {
retOutlineProgress.push(0)
} else if(i === this.cubics.length + 1) {
retOutlineProgress.push(1)
} else {
const cubicIndex = (targetIndex + i - 1) % this.cubics.length
retOutlineProgress.push(Utils.positiveModulo(this.cubics[cubicIndex].endOutlineProgress - cuttingPoint, 1))
}
}
// Shift the feature's outline progress too.
const newFeatures = []
for(let i = 0; i < this.features.length; i++) {
newFeatures.push(new FeatureMapping.ProgressableFeature(Utils.positiveModulo(this.features[i].progress - cuttingPoint, 1), this.features[i].feature))
}
// Filter out all empty cubics (i.e. start and end anchor are (almost) the same point.)
return new MeasuredPolygon(this.measurer, newFeatures, retCubics, retOutlineProgress)
}
static measurePolygon(measurer, polygon) {
const cubics = []
const featureToCubic = []
for (let featureIndex = 0; featureIndex < polygon.features.length; featureIndex++) {
const feature = polygon.features[featureIndex]
for (let cubicIndex = 0; cubicIndex < feature.cubics.length; cubicIndex++) {
if (feature instanceof Feature.Corner && cubicIndex == feature.cubics.length / 2) {
featureToCubic.push({ a: feature, b: cubics.length })
}
cubics.push(feature.cubics[cubicIndex])
}
}
const measures = [0] // Initialize with 0 like in Kotlin's scan
for (const cubic of cubics) {
const measurement = measurer.measureCubic(cubic)
if (measurement < 0) {
throw new Error("Measured cubic is expected to be greater or equal to zero")
}
const lastMeasure = measures[measures.length - 1]
measures.push(lastMeasure + measurement)
}
const totalMeasure = measures[measures.length - 1]
const outlineProgress = []
for (let i = 0; i < measures.length; i++) {
outlineProgress.push(measures[i] / totalMeasure)
}
const features = []
for (let i = 0; i < featureToCubic.length; i++) {
const ix = featureToCubic[i].b
features.push(
new FeatureMapping.ProgressableFeature(Utils.positiveModulo((outlineProgress[ix] + outlineProgress[ix + 1]) / 2, 1), featureToCubic[i].a))
}
return new MeasuredPolygon(measurer, features, cubics, outlineProgress)
}
}
class MeasuredCubic {
constructor(polygon, cubic, startOutlineProgress, endOutlineProgress) {
this.polygon = polygon
this.cubic = cubic
this.startOutlineProgress = startOutlineProgress
this.endOutlineProgress = endOutlineProgress
this.measuredSize = this.polygon.measurer.measureCubic(cubic)
}
updateProgressRange(
startOutlineProgress = this.startOutlineProgress,
endOutlineProgress = this.endOutlineProgress,
) {
this.startOutlineProgress = startOutlineProgress
this.endOutlineProgress = endOutlineProgress
}
cutAtProgress(cutOutlineProgress) {
const boundedCutOutlineProgress = Utils.coerceIn(cutOutlineProgress, this.startOutlineProgress, this.endOutlineProgress)
const outlineProgressSize = this.endOutlineProgress - this.startOutlineProgress
const progressFromStart = boundedCutOutlineProgress - this.startOutlineProgress
const relativeProgress = progressFromStart / outlineProgressSize
const t = this.polygon.measurer.findCubicCutPoint(this.cubic, relativeProgress * this.measuredSize)
const {a: c1, b: c2} = this.cubic.split(t)
return {
a: new MeasuredCubic(this.polygon, c1, this.startOutlineProgress, boundedCutOutlineProgress),
b: new MeasuredCubic(this.polygon, c2, boundedCutOutlineProgress, this.endOutlineProgress)
}
}
}
class LengthMeasurer {
constructor() {
this.segments = 3
}
measureCubic(c) {
return this.closestProgressTo(c, Number.POSITIVE_INFINITY).y
}
findCubicCutPoint(c, m) {
return this.closestProgressTo(c, m).x
}
closestProgressTo(cubic, threshold) {
let total = 0
let remainder = threshold
let prev = new Point.Point(cubic.anchor0X, cubic.anchor0Y)
for (let i = 1; i < this.segments; i++) {
const progress = i / this.segments
const point = cubic.pointOnCurve(progress)
const segment = point.minus(prev).getDistance()
if (segment >= remainder) {
return new Point.Point(progress - (1.0 - remainder / segment) / this.segments, threshold)
}
remainder -= segment
total += segment
prev = point
}
return new Point.Point(1.0, total)
}
}

View File

@@ -0,0 +1,229 @@
.pragma library
.import "point.js" as PointModule
.import "corner-rounding.js" as RoundingModule
.import "utils.js" as UtilsModule
.import "cubic.js" as CubicModule
var Point = PointModule.Point;
var CornerRounding = RoundingModule.CornerRounding;
var DistanceEpsilon = UtilsModule.DistanceEpsilon;
var directionVector = UtilsModule.directionVector;
var Cubic = CubicModule.Cubic;
class RoundedCorner {
/**
* @param {Point} p0
* @param {Point} p1
* @param {Point} p2
* @param {CornerRounding} [rounding=null]
*/
constructor(p0, p1, p2, rounding = null) {
this.p0 = p0;
this.p1 = p1;
this.p2 = p2;
this.rounding = rounding;
this.center = new Point(0, 0);
const v01 = p0.minus(p1);
const v21 = p2.minus(p1);
const d01 = v01.getDistance();
const d21 = v21.getDistance();
if (d01 > 0 && d21 > 0) {
this.d1 = v01.div(d01);
this.d2 = v21.div(d21);
this.cornerRadius = rounding?.radius ?? 0;
this.smoothing = rounding?.smoothing ?? 0;
// cosine of angle at p1 is dot product of unit vectors to the other two vertices
this.cosAngle = this.d1.dotProduct(this.d2);
// identity: sin^2 + cos^2 = 1
// sinAngle gives us the intersection
this.sinAngle = Math.sqrt(1 - Math.pow(this.cosAngle, 2));
// How much we need to cut, as measured on a side, to get the required radius
// calculating where the rounding circle hits the edge
// This uses the identity of tan(A/2) = sinA/(1 + cosA), where tan(A/2) = radius/cut
this.expectedRoundCut = this.sinAngle > 1e-3 ? this.cornerRadius * (this.cosAngle + 1) / this.sinAngle : 0;
} else {
// One (or both) of the sides is empty, not much we can do.
this.d1 = new Point(0, 0);
this.d2 = new Point(0, 0);
this.cornerRadius = 0;
this.smoothing = 0;
this.cosAngle = 0;
this.sinAngle = 0;
this.expectedRoundCut = 0;
}
}
get expectedCut() {
return ((1 + this.smoothing) * this.expectedRoundCut);
}
/**
* @param {float} allowedCut0
* @param {float} [allowedCut1]
* @returns {Array<Cubic>}
*/
getCubics(allowedCut0, allowedCut1 = allowedCut0) {
// We use the minimum of both cuts to determine the radius, but if there is more space
// in one side we can use it for smoothing.
const allowedCut = Math.min(allowedCut0, allowedCut1);
// Nothing to do, just use lines, or a point
if (
this.expectedRoundCut < DistanceEpsilon ||
allowedCut < DistanceEpsilon ||
this.cornerRadius < DistanceEpsilon
) {
this.center = this.p1;
return [Cubic.straightLine(this.p1.x, this.p1.y, this.p1.x, this.p1.y)];
}
// How much of the cut is required for the rounding part.
const actualRoundCut = Math.min(allowedCut, this.expectedRoundCut);
// We have two smoothing values, one for each side of the vertex
// Space is used for rounding values first. If there is space left over, then we
// apply smoothing, if it was requested
const actualSmoothing0 = this.calculateActualSmoothingValue(allowedCut0);
const actualSmoothing1 = this.calculateActualSmoothingValue(allowedCut1);
// Scale the radius if needed
const actualR = this.cornerRadius * actualRoundCut / this.expectedRoundCut;
// Distance from the corner (p1) to the center
const centerDistance = Math.sqrt(Math.pow(actualR, 2) + Math.pow(actualRoundCut, 2));
// Center of the arc we will use for rounding
this.center = this.p1.plus(this.d1.plus(this.d2).div(2).getDirection().times(centerDistance));
const circleIntersection0 = this.p1.plus(this.d1.times(actualRoundCut));
const circleIntersection2 = this.p1.plus(this.d2.times(actualRoundCut));
const flanking0 = this.computeFlankingCurve(
actualRoundCut,
actualSmoothing0,
this.p1,
this.p0,
circleIntersection0,
circleIntersection2,
this.center,
actualR
);
const flanking2 = this.computeFlankingCurve(
actualRoundCut,
actualSmoothing1,
this.p1,
this.p2,
circleIntersection2,
circleIntersection0,
this.center,
actualR
).reverse();
return [
flanking0,
Cubic.circularArc(
this.center.x,
this.center.y,
flanking0.anchor1X,
flanking0.anchor1Y,
flanking2.anchor0X,
flanking2.anchor0Y
),
flanking2
];
}
/**
* @private
* @param {float} allowedCut
* @returns {float}
*/
calculateActualSmoothingValue(allowedCut) {
if (allowedCut > this.expectedCut) {
return this.smoothing;
} else if (allowedCut > this.expectedRoundCut) {
return this.smoothing * (allowedCut - this.expectedRoundCut) / (this.expectedCut - this.expectedRoundCut);
} else {
return 0;
}
}
/**
* @private
* @param {float} actualRoundCut
* @param {float} actualSmoothingValues
* @param {Point} corner
* @param {Point} sideStart
* @param {Point} circleSegmentIntersection
* @param {Point} otherCircleSegmentIntersection
* @param {Point} circleCenter
* @param {float} actualR
* @returns {Cubic}
*/
computeFlankingCurve(
actualRoundCut,
actualSmoothingValues,
corner,
sideStart,
circleSegmentIntersection,
otherCircleSegmentIntersection,
circleCenter,
actualR
) {
// sideStart is the anchor, 'anchor' is actual control point
const sideDirection = (sideStart.minus(corner)).getDirection();
const curveStart = corner.plus(sideDirection.times(actualRoundCut * (1 + actualSmoothingValues)));
// We use an approximation to cut a part of the circle section proportional to 1 - smooth,
// When smooth = 0, we take the full section, when smooth = 1, we take nothing.
const p = Point.interpolate(
circleSegmentIntersection,
(circleSegmentIntersection.plus(otherCircleSegmentIntersection)).div(2),
actualSmoothingValues
);
// The flanking curve ends on the circle
const curveEnd = circleCenter.plus(
directionVector(p.x - circleCenter.x, p.y - circleCenter.y).times(actualR)
);
// The anchor on the circle segment side is in the intersection between the tangent to the
// circle in the circle/flanking curve boundary and the linear segment.
const circleTangent = (curveEnd.minus(circleCenter)).rotate90();
const anchorEnd = this.lineIntersection(sideStart, sideDirection, curveEnd, circleTangent) ?? circleSegmentIntersection;
// From what remains, we pick a point for the start anchor.
// 2/3 seems to come from design tools?
const anchorStart = (curveStart.plus(anchorEnd.times(2))).div(3);
return Cubic.create(curveStart, anchorStart, anchorEnd, curveEnd);
}
/**
* @private
* @param {Point} p0
* @param {Point} d0
* @param {Point} p1
* @param {Point} d1
* @returns {Point|null}
*/
lineIntersection(p0, d0, p1, d1) {
const rotatedD1 = d1.rotate90();
const den = d0.dotProduct(rotatedD1);
if (Math.abs(den) < DistanceEpsilon) return null;
const num = (p1.minus(p0)).dotProduct(rotatedD1);
// Also check the relative value. This is equivalent to abs(den/num) < DistanceEpsilon,
// but avoid doing a division
if (Math.abs(den) < DistanceEpsilon * Math.abs(num)) return null;
const k = num / den;
return p0.plus(d0.times(k));
}
}

View File

@@ -0,0 +1,343 @@
.pragma library
.import "feature.js" as Feature
.import "point.js" as Point
.import "cubic.js" as Cubic
.import "utils.js" as Utils
.import "corner-rounding.js" as CornerRounding
.import "rounded-corner.js" as RoundedCorner
class RoundedPolygon {
constructor(features, center) {
this.features = features
this.center = center
this.cubics = this.buildCubicList()
}
get centerX() {
return this.center.x
}
get centerY() {
return this.center.y
}
transformed(f) {
const center = this.center.transformed(f)
return new RoundedPolygon(this.features.map(x => x.transformed(f)), center)
}
normalized() {
const bounds = this.calculateBounds()
const width = bounds[2] - bounds[0]
const height = bounds[3] - bounds[1]
const side = Math.max(width, height)
// Center the shape if bounds are not a square
const offsetX = (side - width) / 2 - bounds[0] /* left */
const offsetY = (side - height) / 2 - bounds[1] /* top */
return this.transformed((x, y) => {
return new Point.Point((x + offsetX) / side, (y + offsetY) / side)
})
}
calculateMaxBounds(bounds = []) {
let maxDistSquared = 0
for (let i = 0; i < this.cubics.length; i++) {
const cubic = this.cubics[i]
const anchorDistance = Utils.distanceSquared(cubic.anchor0X - this.centerX, cubic.anchor0Y - this.centerY)
const middlePoint = cubic.pointOnCurve(.5)
const middleDistance = Utils.distanceSquared(middlePoint.x - this.centerX, middlePoint.y - this.centerY)
maxDistSquared = Math.max(maxDistSquared, Math.max(anchorDistance, middleDistance))
}
const distance = Math.sqrt(maxDistSquared)
bounds[0] = this.centerX - distance
bounds[1] = this.centerY - distance
bounds[2] = this.centerX + distance
bounds[3] = this.centerY + distance
return bounds
}
calculateBounds(bounds = [], approximate = true) {
let minX = Number.MAX_SAFE_INTEGER
let minY = Number.MAX_SAFE_INTEGER
let maxX = Number.MIN_SAFE_INTEGER
let maxY = Number.MIN_SAFE_INTEGER
for (let i = 0; i < this.cubics.length; i++) {
const cubic = this.cubics[i]
cubic.calculateBounds(bounds, approximate)
minX = Math.min(minX, bounds[0])
minY = Math.min(minY, bounds[1])
maxX = Math.max(maxX, bounds[2])
maxY = Math.max(maxY, bounds[3])
}
bounds[0] = minX
bounds[1] = minY
bounds[2] = maxX
bounds[3] = maxY
return bounds
}
buildCubicList() {
const result = []
// The first/last mechanism here ensures that the final anchor point in the shape
// exactly matches the first anchor point. There can be rendering artifacts introduced
// by those points being slightly off, even by much less than a pixel
let firstCubic = null
let lastCubic = null
let firstFeatureSplitStart = null
let firstFeatureSplitEnd = null
if (this.features.length > 0 && this.features[0].cubics.length == 3) {
const centerCubic = this.features[0].cubics[1]
const { a: start, b: end } = centerCubic.split(.5)
firstFeatureSplitStart = [this.features[0].cubics[0], start]
firstFeatureSplitEnd = [end, this.features[0].cubics[2]]
}
// iterating one past the features list size allows us to insert the initial split
// cubic if it exists
for (let i = 0; i <= this.features.length; i++) {
let featureCubics
if (i == 0 && firstFeatureSplitEnd != null) {
featureCubics = firstFeatureSplitEnd
} else if (i == this.features.length) {
if (firstFeatureSplitStart != null) {
featureCubics = firstFeatureSplitStart
} else {
break
}
} else {
featureCubics = this.features[i].cubics
}
for (let j = 0; j < featureCubics.length; j++) {
// Skip zero-length curves; they add nothing and can trigger rendering artifacts
const cubic = featureCubics[j]
if (!cubic.zeroLength()) {
if (lastCubic != null)
result.push(lastCubic)
lastCubic = cubic
if (firstCubic == null)
firstCubic = cubic
} else {
if (lastCubic != null) {
// Dropping several zero-ish length curves in a row can lead to
// enough discontinuity to throw an exception later, even though the
// distances are quite small. Account for that by making the last
// cubic use the latest anchor point, always.
lastCubic = new Cubic.Cubic([...lastCubic.points]) // Make a copy before mutating
lastCubic.points[6] = cubic.anchor1X
lastCubic.points[7] = cubic.anchor1Y
}
}
}
}
if (lastCubic != null && firstCubic != null) {
result.push(
new Cubic.Cubic([
lastCubic.anchor0X,
lastCubic.anchor0Y,
lastCubic.control0X,
lastCubic.control0Y,
lastCubic.control1X,
lastCubic.control1Y,
firstCubic.anchor0X,
firstCubic.anchor0Y,
])
)
} else {
// Empty / 0-sized polygon.
result.push(new Cubic.Cubic([this.centerX, this.centerY, this.centerX, this.centerY, this.centerX, this.centerY, this.centerX, this.centerY]))
}
return result
}
static calculateCenter(vertices) {
let cumulativeX = 0
let cumulativeY = 0
let index = 0
while (index < vertices.length) {
cumulativeX += vertices[index++]
cumulativeY += vertices[index++]
}
return new Point.Point(cumulativeX / (vertices.length / 2), cumulativeY / (vertices.length / 2))
}
static verticesFromNumVerts(numVertices, radius, centerX, centerY) {
const result = []
let arrayIndex = 0
for (let i = 0; i < numVertices; i++) {
const vertex = Utils.radialToCartesian(radius, (Math.PI / numVertices * 2 * i)).plus(new Point.Point(centerX, centerY))
result[arrayIndex++] = vertex.x
result[arrayIndex++] = vertex.y
}
return result
}
static fromNumVertices(numVertices, radius = 1, centerX = 0, centerY = 0, rounding = CornerRounding.Unrounded, perVertexRounding = null) {
return RoundedPolygon.fromVertices(this.verticesFromNumVerts(numVertices, radius, centerX, centerY), rounding, perVertexRounding, centerX, centerY)
}
static fromVertices(vertices, rounding = CornerRounding.Unrounded, perVertexRounding = null, centerX = Number.MIN_SAFE_INTEGER, centerY = Number.MAX_SAFE_INTEGER) {
const corners = []
const n = vertices.length / 2
const roundedCorners = []
for (let i = 0; i < n; i++) {
const vtxRounding = perVertexRounding?.[i] ?? rounding
const prevIndex = ((i + n - 1) % n) * 2
const nextIndex = ((i + 1) % n) * 2
roundedCorners.push(
new RoundedCorner.RoundedCorner(
new Point.Point(vertices[prevIndex], vertices[prevIndex + 1]),
new Point.Point(vertices[i * 2], vertices[i * 2 + 1]),
new Point.Point(vertices[nextIndex], vertices[nextIndex + 1]),
vtxRounding
)
)
}
// For each side, check if we have enough space to do the cuts needed, and if not split
// the available space, first for round cuts, then for smoothing if there is space left.
// Each element in this list is a pair, that represent how much we can do of the cut for
// the given side (side i goes from corner i to corner i+1), the elements of the pair are:
// first is how much we can use of expectedRoundCut, second how much of expectedCut
const cutAdjusts = Array.from({ length: n }).map((_, ix) => {
const expectedRoundCut = roundedCorners[ix].expectedRoundCut + roundedCorners[(ix + 1) % n].expectedRoundCut
const expectedCut = roundedCorners[ix].expectedCut + roundedCorners[(ix + 1) % n].expectedCut
const vtxX = vertices[ix * 2]
const vtxY = vertices[ix * 2 + 1]
const nextVtxX = vertices[((ix + 1) % n) * 2]
const nextVtxY = vertices[((ix + 1) % n) * 2 + 1]
const sideSize = Utils.distance(vtxX - nextVtxX, vtxY - nextVtxY)
// Check expectedRoundCut first, and ensure we fulfill rounding needs first for
// both corners before using space for smoothing
if (expectedRoundCut > sideSize) {
// Not enough room for fully rounding, see how much we can actually do.
return { a: sideSize / expectedRoundCut, b: 0 }
} else if (expectedCut > sideSize) {
// We can do full rounding, but not full smoothing.
return { a: 1, b: (sideSize - expectedRoundCut) / (expectedCut - expectedRoundCut) }
} else {
// There is enough room for rounding & smoothing.
return { a: 1, b: 1 }
}
})
// Create and store list of beziers for each [potentially] rounded corner
for (let i = 0; i < n; i++) {
// allowedCuts[0] is for the side from the previous corner to this one,
// allowedCuts[1] is for the side from this corner to the next one.
const allowedCuts = []
for(const delta of [0, 1]) {
const { a: roundCutRatio, b: cutRatio } = cutAdjusts[(i + n - 1 + delta) % n]
allowedCuts.push(
roundedCorners[i].expectedRoundCut * roundCutRatio +
(roundedCorners[i].expectedCut - roundedCorners[i].expectedRoundCut) * cutRatio
)
}
corners.push(
roundedCorners[i].getCubics(allowedCuts[0], allowedCuts[1])
)
}
const tempFeatures = []
for (let i = 0; i < n; i++) {
// Note that these indices are for pairs of values (points), they need to be
// doubled to access the xy values in the vertices float array
const prevVtxIndex = (i + n - 1) % n
const nextVtxIndex = (i + 1) % n
const currVertex = new Point.Point(vertices[i * 2], vertices[i * 2 + 1])
const prevVertex = new Point.Point(vertices[prevVtxIndex * 2], vertices[prevVtxIndex * 2 + 1])
const nextVertex = new Point.Point(vertices[nextVtxIndex * 2], vertices[nextVtxIndex * 2 + 1])
const cnvx = Utils.convex(prevVertex, currVertex, nextVertex)
tempFeatures.push(new Feature.Corner(corners[i], cnvx))
tempFeatures.push(
new Feature.Edge([Cubic.Cubic.straightLine(
corners[i][corners[i].length - 1].anchor1X,
corners[i][corners[i].length - 1].anchor1Y,
corners[(i + 1) % n][0].anchor0X,
corners[(i + 1) % n][0].anchor0Y,
)])
)
}
let center
if (centerX == Number.MIN_SAFE_INTEGER || centerY == Number.MIN_SAFE_INTEGER) {
center = RoundedPolygon.calculateCenter(vertices)
} else {
center = new Point.Point(centerX, centerY)
}
return RoundedPolygon.fromFeatures(tempFeatures, center.x, center.y)
}
static fromFeatures(features, centerX, centerY) {
const vertices = []
for (const feature of features) {
for (const cubic of feature.cubics) {
vertices.push(cubic.anchor0X)
vertices.push(cubic.anchor0Y)
}
}
if (Number.isNaN(centerX)) {
centerX = this.calculateCenter(vertices).x
}
if (Number.isNaN(centerY)) {
centerY = this.calculateCenter(vertices).y
}
return new RoundedPolygon(features, new Point.Point(centerX, centerY))
}
static circle(numVertices = 8, radius = 1, centerX = 0, centerY = 0) {
// Half of the angle between two adjacent vertices on the polygon
const theta = Math.PI / numVertices
// Radius of the underlying RoundedPolygon object given the desired radius of the circle
const polygonRadius = radius / Math.cos(theta)
return RoundedPolygon.fromNumVertices(
numVertices,
polygonRadius,
centerX,
centerY,
new CornerRounding.CornerRounding(radius)
)
}
static rectangle(width, height, rounding = CornerRounding.Unrounded, perVertexRounding = null, centerX = 0, centerY = 0) {
const left = centerX - width / 2
const top = centerY - height / 2
const right = centerX + width / 2
const bottom = centerY + height / 2
return RoundedPolygon.fromVertices([right, bottom, left, bottom, left, top, right, top], rounding, perVertexRounding, centerX, centerY)
}
static star(numVerticesPerRadius, radius = 1, innerRadius = .5, rounding = CornerRounding.Unrounded, innerRounding = null, perVertexRounding = null, centerX = 0, centerY = 0) {
let pvRounding = perVertexRounding
// If no per-vertex rounding supplied and caller asked for inner rounding,
// create per-vertex rounding list based on supplied outer/inner rounding parameters
if (pvRounding == null && innerRounding != null) {
pvRounding = Array.from({ length: numVerticesPerRadius * 2 }).flatMap(() => [rounding, innerRounding])
}
return RoundedPolygon.fromVertices(RoundedPolygon.starVerticesFromNumVerts(numVerticesPerRadius, radius, innerRadius, centerX, centerY), rounding, perVertexRounding, centerX, centerY)
}
static starVerticesFromNumVerts(numVerticesPerRadius, radius, innerRadius, centerX, centerY) {
const result = []
let arrayIndex = 0
for (let i = 0; i < numVerticesPerRadius; i++) {
let vertex = Utils.radialToCartesian(radius, (Math.PI / numVerticesPerRadius * 2 * i))
result[arrayIndex++] = vertex.x + centerX
result[arrayIndex++] = vertex.y + centerY
vertex = Utils.radialToCartesian(innerRadius, (Math.PI / numVerticesPerRadius * (2 * i + 1)))
result[arrayIndex++] = vertex.x + centerX
result[arrayIndex++] = vertex.y + centerY
}
return result
}
}

View File

@@ -0,0 +1,94 @@
.pragma library
.import "point.js" as PointModule
var Point = PointModule.Point;
var DistanceEpsilon = 1e-4;
var AngleEpsilon = 1e-6;
/**
* @param {Point} previous
* @param {Point} current
* @param {Point} next
* @returns {boolean}
*/
function convex(previous, current, next) {
return (current.minus(previous)).clockwise(next.minus(current));
}
/**
* @param {float} start
* @param {float} stop
* @param {float} fraction
* @returns {float}
*/
function interpolate(start, stop, fraction) {
return (1 - fraction) * start + fraction * stop;
}
/**
* @param {float} x
* @param {float} y
* @returns {Point}
*/
function directionVector(x, y) {
const d = distance(x, y);
return new Point(x / d, y / d);
}
/**
* @param {float} x
* @param {float} y
* @returns {float}
*/
function distance(x, y) {
return Math.sqrt(x * x + y * y);
}
/**
* @param {float} x
* @param {float} y
* @returns {float}
*/
function distanceSquared(x, y) {
return x * x + y * y;
}
/**
* @param {float} radius
* @param {float} angleRadians
* @param {Point} [center]
* @returns {Point}
*/
function radialToCartesian(radius, angleRadians, center = new Point(0, 0)) {
return new Point(Math.cos(angleRadians), Math.sin(angleRadians))
.times(radius)
.plus(center);
}
/**
* @param {float} value
* @param {float|object} min
* @param {float} [max]
* @returns {float}
*/
function coerceIn(value, min, max) {
if (max === undefined) {
if (typeof min === 'object' && 'start' in min && 'endInclusive' in min) {
return Math.max(min.start, Math.min(min.endInclusive, value));
}
throw new Error("Invalid arguments for coerceIn");
}
const [actualMin, actualMax] = min <= max ? [min, max] : [max, min];
return Math.max(actualMin, Math.min(actualMax, value));
}
/**
* @param {float} value
* @param {float} mod
* @returns {float}
*/
function positiveModulo(value, mod) {
return ((value % mod) + mod) % mod;
}