summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFuwn <[email protected]>2025-12-02 00:10:52 -0800
committerFuwn <[email protected]>2025-12-02 00:10:52 -0800
commit61f1624a14f80b416c530cbb82e430302d12bda3 (patch)
treebd7a6a4e0b6d86c8fe84b3949662f9059750f783
parentfeat(index.html): Remove introduction (diff)
downloadrysk-61f1624a14f80b416c530cbb82e430302d12bda3.tar.xz
rysk-61f1624a14f80b416c530cbb82e430302d12bda3.zip
fix(analysis.js): Improve analysis engine
-rw-r--r--js/analysis.js1682
-rw-r--r--js/index.js2
2 files changed, 1081 insertions, 603 deletions
diff --git a/js/analysis.js b/js/analysis.js
index fd78855..b19dcd5 100644
--- a/js/analysis.js
+++ b/js/analysis.js
@@ -1,673 +1,1149 @@
function analyseCriteria(face) {
- let points = {
- leftIris: face.annotations.rightEyeIris[0],
- rightIris: face.annotations.leftEyeIris[0],
- leftLateralCanthus: face.annotations.rightEyeLower1[0],
- leftMedialCanthus: face.annotations.rightEyeLower1[7],
- rightLateralCanthus: face.annotations.leftEyeLower1[0],
- rightMedialCanthus: face.annotations.leftEyeLower1[7],
- leftEyeUpper: face.annotations.rightEyeUpper0[4],
- leftEyeLower: face.annotations.rightEyeLower0[4],
- rightEyeUpper: face.annotations.leftEyeUpper0[4],
- rightEyeLower: face.annotations.leftEyeLower0[4],
- leftEyebrow: face.annotations.rightEyebrowUpper[6],
- rightEyebrow: face.annotations.leftEyebrowUpper[6],
- leftZygo: face.annotations.silhouette[28],
- rightZygo: face.annotations.silhouette[8],
- noseBottom: face.annotations.noseBottom[0],
- leftNoseCorner: face.annotations.noseRightCorner[0],
- rightNoseCorner: face.annotations.noseLeftCorner[0],
- leftCupidBow: face.annotations.lipsUpperOuter[4],
- lipSeparation: face.annotations.lipsUpperInner[5],
- rightCupidBow: face.annotations.lipsUpperOuter[6],
- leftLipCorner: face.annotations.lipsUpperOuter[0],
- rightLipCorner: face.annotations.lipsUpperOuter[10],
- lowerLip: face.annotations.lipsLowerOuter[4],
- upperLip: face.annotations.lipsUpperOuter[5],
- leftGonial: face.annotations.silhouette[24],
- rightGonial: face.annotations.silhouette[12],
- chinLeft: face.annotations.silhouette[19],
- chinTip: face.annotations.silhouette[18],
- chinRight: face.annotations.silhouette[17],
- };
- return [points, {
- midfaceRatio: new MidfaceRatio(face, points),
- facialWidthToHeightRatio: new FacialWidthToHeightRatio(face, points),
- chinToPhiltrumRatio: new ChinToPhiltrumRatio(face, points),
- canthalTilt: new CanthalTilt(face, points),
- mouthToNoseRatio: new MouthToNoseRatio(face, points),
- bigonialWidth: new BigonialWidth(face, points),
- lipRatio: new LipRatio(face, points),
- eyeSeparationRatio: new EyeSeparationRatio(face, points),
- eyeToMouthAngle: new EyeToMouthAngle(face, points),
- lowerThirdHeight: new LowerThirdHeight(face, points),
- palpebralFissureLength: new PalpebralFissureLength(face, points),
- eyeColor: new EyeColor(face, points),
- }];
+ // Note: MediaPipe uses camera perspective (mirrored), so left/right are swapped
+ // leftIris uses rightEyeIris because from camera's view, right eye is on left side
+ let points = {
+ leftIris: face.annotations.rightEyeIris[0],
+ rightIris: face.annotations.leftEyeIris[0],
+ leftLateralCanthus: face.annotations.rightEyeLower1[0],
+ leftMedialCanthus: face.annotations.rightEyeLower1[7],
+ rightLateralCanthus: face.annotations.leftEyeLower1[0],
+ rightMedialCanthus: face.annotations.leftEyeLower1[7],
+ leftEyeUpper: face.annotations.rightEyeUpper0[4],
+ leftEyeLower: face.annotations.rightEyeLower0[4],
+ rightEyeUpper: face.annotations.leftEyeUpper0[4],
+ rightEyeLower: face.annotations.leftEyeLower0[4],
+ leftEyebrow: face.annotations.rightEyebrowUpper[6],
+ rightEyebrow: face.annotations.leftEyebrowUpper[6],
+ leftZygo: face.annotations.silhouette[28],
+ rightZygo: face.annotations.silhouette[8],
+ noseBottom: face.annotations.noseBottom[0],
+ leftNoseCorner: face.annotations.noseRightCorner[0],
+ rightNoseCorner: face.annotations.noseLeftCorner[0],
+ leftCupidBow: face.annotations.lipsUpperOuter[4],
+ lipSeparation: face.annotations.lipsUpperInner[5],
+ rightCupidBow: face.annotations.lipsUpperOuter[6],
+ leftLipCorner: face.annotations.lipsUpperOuter[0],
+ rightLipCorner: face.annotations.lipsUpperOuter[10],
+ lowerLip: face.annotations.lipsLowerOuter[4],
+ upperLip: face.annotations.lipsUpperOuter[5],
+ leftGonial: face.annotations.silhouette[24],
+ rightGonial: face.annotations.silhouette[12],
+ chinLeft: face.annotations.silhouette[19],
+ chinTip: face.annotations.silhouette[18],
+ chinRight: face.annotations.silhouette[17],
+ };
+
+ // Validate critical points exist
+ for (let key in points) {
+ if (!points[key] || !Array.isArray(points[key]) || points[key].length < 2) {
+ console.warn(`Missing or invalid point: ${key}`);
+ }
+ }
+ return [
+ points,
+ {
+ midfaceRatio: new MidfaceRatio(face, points),
+ facialWidthToHeightRatio: new FacialWidthToHeightRatio(face, points),
+ chinToPhiltrumRatio: new ChinToPhiltrumRatio(face, points),
+ canthalTilt: new CanthalTilt(face, points),
+ mouthToNoseRatio: new MouthToNoseRatio(face, points),
+ bigonialWidth: new BigonialWidth(face, points),
+ lipRatio: new LipRatio(face, points),
+ eyeSeparationRatio: new EyeSeparationRatio(face, points),
+ eyeToMouthAngle: new EyeToMouthAngle(face, points),
+ lowerThirdHeight: new LowerThirdHeight(face, points),
+ palpebralFissureLength: new PalpebralFissureLength(face, points),
+ eyeColor: new EyeColor(face, points),
+ },
+ ];
}
async function setupDatabase() {
- return await fetch("database.json")
- .then(res => res.text())
- .then(text => {
- console.log("✅ database.json loaded");
- return JSON.parse(text);
- })
- .catch(err => {
- console.error("❌ Failed to load or parse database.json:", err);
- return { entries: {} };
- });
-
+ return await fetch("database.json")
+ .then((res) => res.text())
+ .then((text) => {
+ console.log("✅ database.json loaded");
+ return JSON.parse(text);
+ })
+ .catch((err) => {
+ console.error("❌ Failed to load or parse database.json:", err);
+ return { entries: {} };
+ });
}
class Criteria {
- constructor(face, points) {
- this.face = face;
- this.points = points;
-
- for(let i in points) {
- if (points.hasOwnProperty(i)) {
- Object.defineProperty(this, i, { get: () => this.points[i] });
- }
- }
+ constructor(face, points) {
+ this.face = face;
+ this.points = points;
- return this;
+ for (let i in points) {
+ if (points.hasOwnProperty(i)) {
+ Object.defineProperty(this, i, { get: () => this.points[i] });
+ }
}
- createPoint(name, value) {
- this.points[name] = value;
- Object.defineProperty(this, name, { get: () => this.points[name] });
- }
+ return this;
+ }
- calculate() {
- /* abstract */
- }
+ createPoint(name, value) {
+ this.points[name] = value;
+ Object.defineProperty(this, name, { get: () => this.points[name] });
+ }
- render() {
- /* abstract */
- }
+ calculate() {
+ /* abstract */
+ }
- ideal() {
- /* abstract */
- }
+ render() {
+ /* abstract */
+ }
- assess() {
- /* abstract */
- }
+ ideal() {
+ /* abstract */
+ }
- draw(ctx) {
- /* abstract */
- }
+ assess() {
+ /* abstract */
+ }
- necessaryPoints() {
- /* abstract */
- }
+ draw(ctx) {
+ /* abstract */
+ }
+
+ necessaryPoints() {
+ /* abstract */
+ }
}
class MidfaceRatio extends Criteria {
- constructor(face, points) {
- super(face, points);
-
- let bottomLine = Fn.fromTwoPoints(this.leftCupidBow, this.rightCupidBow);
- let leftLine = bottomLine.perpendicular(this.leftIris);
- let rightLine = bottomLine.perpendicular(this.rightIris);
- this.createPoint("bottomLeftMidface", bottomLine.intersect(leftLine));
- this.createPoint("bottomRightMidface", bottomLine.intersect(rightLine));
- }
-
- calculate() {
- this.ratio = ((distance(this.leftIris, this.rightIris) / distance(this.leftIris, this.bottomLeftMidface)) +
- (distance(this.leftIris, this.rightIris) / distance(this.rightIris, this.bottomRightMidface))) / 2;
- }
-
- render() {
- return `${round(this.ratio, 2)}`;
- }
-
- ideal() {
- return `${database.entries.midfaceRatio.idealLower} to ${database.entries.midfaceRatio.idealUpper}`;
- }
-
- assess() {
- let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } = database.entries.midfaceRatio;
- return assess(this.ratio, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh);
- }
-
- draw(ctx) {
- draw(ctx, "red", [this.leftIris, this.rightIris, this.bottomRightMidface, this.bottomLeftMidface]);
- }
-
- necessaryPoints() {
- return ["leftIris", "rightIris", "bottomLeftMidface", "bottomRightMidface"];
- }
+ constructor(face, points) {
+ super(face, points);
+
+ let bottomLine = Fn.fromTwoPoints(this.leftCupidBow, this.rightCupidBow);
+ let leftLine = bottomLine.perpendicular(this.leftIris);
+ let rightLine = bottomLine.perpendicular(this.rightIris);
+ this.createPoint("bottomLeftMidface", bottomLine.intersect(leftLine));
+ this.createPoint("bottomRightMidface", bottomLine.intersect(rightLine));
+ }
+
+ calculate() {
+ let eyeDistance = distance(this.leftIris, this.rightIris);
+ let leftDistance = distance(this.leftIris, this.bottomLeftMidface);
+ let rightDistance = distance(this.rightIris, this.bottomRightMidface);
+
+ // Avoid division by zero
+ if (leftDistance < 1e-10 || rightDistance < 1e-10) {
+ console.warn("MidfaceRatio: distance too small, using fallback");
+ this.ratio = 0;
+ return;
+ }
+
+ this.ratio = (eyeDistance / leftDistance + eyeDistance / rightDistance) / 2;
+ }
+
+ render() {
+ return `${round(this.ratio, 2)}`;
+ }
+
+ ideal() {
+ return `${database.entries.midfaceRatio.idealLower} to ${database.entries.midfaceRatio.idealUpper}`;
+ }
+
+ assess() {
+ let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } =
+ database.entries.midfaceRatio;
+ return assess(
+ this.ratio,
+ idealLower,
+ idealUpper,
+ deviation,
+ deviatingLow,
+ deviatingHigh
+ );
+ }
+
+ draw(ctx) {
+ draw(ctx, "red", [
+ this.leftIris,
+ this.rightIris,
+ this.bottomRightMidface,
+ this.bottomLeftMidface,
+ ]);
+ }
+
+ necessaryPoints() {
+ return ["leftIris", "rightIris", "bottomLeftMidface", "bottomRightMidface"];
+ }
}
class FacialWidthToHeightRatio extends Criteria {
- constructor(face, points) {
- super(face, points);
-
- let topLine = Fn.fromTwoPoints(this.leftEyeUpper, this.rightEyeUpper);
- let bottomLine = Fn.fromTwoPoints(this.leftCupidBow, this.rightCupidBow);
- let leftLine = topLine.perpendicular(this.leftZygo);
- let rightLine = topLine.perpendicular(this.rightZygo);
- this.createPoint("topLeft", leftLine.intersect(topLine));
- this.createPoint("topRight", rightLine.intersect(topLine));
- this.createPoint("bottomLeft", leftLine.intersect(bottomLine));
- this.createPoint("bottomRight", rightLine.intersect(bottomLine));
- }
-
- calculate() {
- this.ratio = (((distance(this.topLeft, this.topRight) / distance(this.topLeft, this.bottomLeft))
- + (distance(this.bottomLeft, this.bottomRight) / distance(this.topRight, this.bottomRight))) / 2);
- }
-
- render() {
- return `${round(this.ratio, 2)}`;
- }
-
- ideal() {
- return `more than ${database.entries.facialWidthToHeightRatio.idealLower}`;
- }
-
- assess() {
- let { idealLower, deviation, deviatingLow } = database.entries.facialWidthToHeightRatio;
- return assess(this.ratio, idealLower, undefined, deviation, deviatingLow, undefined);
- }
-
- draw(ctx) {
- draw(ctx, "lightblue", [this.topLeft, this.topRight, this.bottomRight, this.bottomLeft])
- }
-
- necessaryPoints() {
- return ["topLeft", "topRight", "bottomLeft", "bottomRight"];
- }
+ constructor(face, points) {
+ super(face, points);
+
+ let topLine = Fn.fromTwoPoints(this.leftEyeUpper, this.rightEyeUpper);
+ let bottomLine = Fn.fromTwoPoints(this.leftCupidBow, this.rightCupidBow);
+ let leftLine = topLine.perpendicular(this.leftZygo);
+ let rightLine = topLine.perpendicular(this.rightZygo);
+ this.createPoint("topLeft", leftLine.intersect(topLine));
+ this.createPoint("topRight", rightLine.intersect(topLine));
+ this.createPoint("bottomLeft", leftLine.intersect(bottomLine));
+ this.createPoint("bottomRight", rightLine.intersect(bottomLine));
+ }
+
+ calculate() {
+ let topWidth = distance(this.topLeft, this.topRight);
+ let leftHeight = distance(this.topLeft, this.bottomLeft);
+ let bottomWidth = distance(this.bottomLeft, this.bottomRight);
+ let rightHeight = distance(this.topRight, this.bottomRight);
+
+ // Avoid division by zero
+ if (leftHeight < 1e-10 || rightHeight < 1e-10) {
+ console.warn(
+ "FacialWidthToHeightRatio: height too small, using fallback"
+ );
+ this.ratio = 0;
+ return;
+ }
+
+ this.ratio = (topWidth / leftHeight + bottomWidth / rightHeight) / 2;
+ }
+
+ render() {
+ return `${round(this.ratio, 2)}`;
+ }
+
+ ideal() {
+ return `more than ${database.entries.facialWidthToHeightRatio.idealLower}`;
+ }
+
+ assess() {
+ let { idealLower, deviation, deviatingLow } =
+ database.entries.facialWidthToHeightRatio;
+ return assess(
+ this.ratio,
+ idealLower,
+ undefined,
+ deviation,
+ deviatingLow,
+ undefined
+ );
+ }
+
+ draw(ctx) {
+ draw(ctx, "lightblue", [
+ this.topLeft,
+ this.topRight,
+ this.bottomRight,
+ this.bottomLeft,
+ ]);
+ }
+
+ necessaryPoints() {
+ return ["topLeft", "topRight", "bottomLeft", "bottomRight"];
+ }
}
class ChinToPhiltrumRatio extends Criteria {
- calculate() {
- this.ratio = distance(this.chinTip, this.lowerLip) / distance(this.upperLip, this.noseBottom);
- }
-
- render() {
- return `${round(this.ratio, 2)}`;
- }
-
- ideal() {
- return `${database.entries.chinToPhiltrumRatio.idealLower} to ${database.entries.chinToPhiltrumRatio.idealUpper}`;
- }
-
- assess() {
- let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } = database.entries.chinToPhiltrumRatio;
- return assess(this.ratio, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh);
- }
-
- draw(ctx) {
- draw(ctx, "blue", [this.chinTip, this.lowerLip]);
- draw(ctx, "blue", [this.upperLip, this.noseBottom]);
- }
-
- necessaryPoints() {
- return ["chinTip", "lowerLip", "upperLip", "noseBottom"];
- }
+ calculate() {
+ let chinDistance = distance(this.chinTip, this.lowerLip);
+ let philtrumDistance = distance(this.upperLip, this.noseBottom);
+
+ // Avoid division by zero
+ if (philtrumDistance < 1e-10) {
+ console.warn(
+ "ChinToPhiltrumRatio: philtrumDistance too small, using fallback"
+ );
+ this.ratio = 0;
+ return;
+ }
+
+ this.ratio = chinDistance / philtrumDistance;
+ }
+
+ render() {
+ return `${round(this.ratio, 2)}`;
+ }
+
+ ideal() {
+ return `${database.entries.chinToPhiltrumRatio.idealLower} to ${database.entries.chinToPhiltrumRatio.idealUpper}`;
+ }
+
+ assess() {
+ let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } =
+ database.entries.chinToPhiltrumRatio;
+ return assess(
+ this.ratio,
+ idealLower,
+ idealUpper,
+ deviation,
+ deviatingLow,
+ deviatingHigh
+ );
+ }
+
+ draw(ctx) {
+ draw(ctx, "blue", [this.chinTip, this.lowerLip]);
+ draw(ctx, "blue", [this.upperLip, this.noseBottom]);
+ }
+
+ necessaryPoints() {
+ return ["chinTip", "lowerLip", "upperLip", "noseBottom"];
+ }
}
class CanthalTilt extends Criteria {
- calculate() {
- let line = [this.rightZygo[0] - this.leftZygo[0], this.rightZygo[1] - this.leftZygo[1]];
- let lineFn = Fn.fromTwoPoints(this.rightZygo, this.leftZygo);
- let left = [this.leftLateralCanthus[0] - this.leftMedialCanthus[0], this.leftLateralCanthus[1] - this.leftMedialCanthus[1]];
- let right = [this.rightLateralCanthus[0] - this.rightMedialCanthus[0], this.rightLateralCanthus[1] - this.rightMedialCanthus[1]];
- let pointOnLeftLine = lineFn.getY(this.leftMedialCanthus[0]) + left[1];
- let pointOnRightLine = lineFn.getY(this.rightMedialCanthus[0]) + right[1];
- this.leftCanthalTilt = Math.acos(
- Math.abs((-1) * (line[0] * left[0] + line[1] * left[1]))
- /
- (Math.sqrt(line[0] ** 2 + line[1] ** 2) * Math.sqrt(left[0] ** 2 + left[1] ** 2))
- ) * (180 / Math.PI) * (lineFn.getY(this.leftLateralCanthus[0]) - pointOnLeftLine > 0 ? 1 : -1);
- this.rightCanthalTilt = Math.acos(
- Math.abs(line[0] * right[0] + line[1] * right[1])
- /
- (Math.sqrt(line[0] ** 2 + line[1] ** 2) * Math.sqrt(right[0] ** 2 + right[1] ** 2))
- ) * (180 / Math.PI) * (lineFn.getY(this.rightLateralCanthus[0]) - pointOnRightLine > 0 ? 1 : -1);
- }
-
- render() {
- return `left ${round(this.rightCanthalTilt, 0)}°, right ${round(this.leftCanthalTilt, 0)}°`;
- }
-
- ideal() {
- return `more than ${database.entries.canthalTilt.idealLower}`;
- }
-
- assess() {
- let { idealLower, deviation, deviatingLow } = database.entries.canthalTilt;
- return assess((this.leftCanthalTilt + this.rightCanthalTilt) / 2, idealLower, undefined, deviation, deviatingLow, undefined);
- }
-
- draw(ctx) {
- draw(ctx, "pink", [this.leftLateralCanthus, this.leftMedialCanthus]);
- draw(ctx, "pink", [this.rightLateralCanthus, this.rightMedialCanthus]);
- }
-
- necessaryPoints() {
- return ["leftLateralCanthus", "leftMedialCanthus", "rightLateralCanthus", "rightMedialCanthus"];
- }
+ calculate() {
+ let line = [
+ this.rightZygo[0] - this.leftZygo[0],
+ this.rightZygo[1] - this.leftZygo[1],
+ ];
+ let lineFn = Fn.fromTwoPoints(this.rightZygo, this.leftZygo);
+ let left = [
+ this.leftLateralCanthus[0] - this.leftMedialCanthus[0],
+ this.leftLateralCanthus[1] - this.leftMedialCanthus[1],
+ ];
+ let right = [
+ this.rightLateralCanthus[0] - this.rightMedialCanthus[0],
+ this.rightLateralCanthus[1] - this.rightMedialCanthus[1],
+ ];
+ let pointOnLeftLine = lineFn.getY(this.leftMedialCanthus[0]) + left[1];
+ let pointOnRightLine = lineFn.getY(this.rightMedialCanthus[0]) + right[1];
+
+ // Calculate left canthal tilt
+ let lineMagnitude = Math.sqrt(line[0] ** 2 + line[1] ** 2);
+ let leftMagnitude = Math.sqrt(left[0] ** 2 + left[1] ** 2);
+ let rightMagnitude = Math.sqrt(right[0] ** 2 + right[1] ** 2);
+
+ if (lineMagnitude < 1e-10 || leftMagnitude < 1e-10) {
+ console.warn("CanthalTilt: left calculation - magnitude too small");
+ this.leftCanthalTilt = 0;
+ } else {
+ let leftDotProduct = Math.abs(
+ -1 * (line[0] * left[0] + line[1] * left[1])
+ );
+ let leftCosAngle = leftDotProduct / (lineMagnitude * leftMagnitude);
+ leftCosAngle = Math.max(-1, Math.min(1, leftCosAngle)); // Clamp for acos
+ this.leftCanthalTilt =
+ Math.acos(leftCosAngle) *
+ (180 / Math.PI) *
+ (lineFn.getY(this.leftLateralCanthus[0]) - pointOnLeftLine > 0
+ ? 1
+ : -1);
+ }
+
+ // Calculate right canthal tilt
+ if (lineMagnitude < 1e-10 || rightMagnitude < 1e-10) {
+ console.warn("CanthalTilt: right calculation - magnitude too small");
+ this.rightCanthalTilt = 0;
+ } else {
+ let rightDotProduct = Math.abs(line[0] * right[0] + line[1] * right[1]);
+ let rightCosAngle = rightDotProduct / (lineMagnitude * rightMagnitude);
+ rightCosAngle = Math.max(-1, Math.min(1, rightCosAngle)); // Clamp for acos
+ this.rightCanthalTilt =
+ Math.acos(rightCosAngle) *
+ (180 / Math.PI) *
+ (lineFn.getY(this.rightLateralCanthus[0]) - pointOnRightLine > 0
+ ? 1
+ : -1);
+ }
+ }
+
+ render() {
+ return `left ${round(this.rightCanthalTilt, 0)}°, right ${round(
+ this.leftCanthalTilt,
+ 0
+ )}°`;
+ }
+
+ ideal() {
+ return `more than ${database.entries.canthalTilt.idealLower}`;
+ }
+
+ assess() {
+ let { idealLower, deviation, deviatingLow } = database.entries.canthalTilt;
+ return assess(
+ (this.leftCanthalTilt + this.rightCanthalTilt) / 2,
+ idealLower,
+ undefined,
+ deviation,
+ deviatingLow,
+ undefined
+ );
+ }
+
+ draw(ctx) {
+ draw(ctx, "pink", [this.leftLateralCanthus, this.leftMedialCanthus]);
+ draw(ctx, "pink", [this.rightLateralCanthus, this.rightMedialCanthus]);
+ }
+
+ necessaryPoints() {
+ return [
+ "leftLateralCanthus",
+ "leftMedialCanthus",
+ "rightLateralCanthus",
+ "rightMedialCanthus",
+ ];
+ }
}
class MouthToNoseRatio extends Criteria {
- calculate() {
- this.ratio = distance(this.leftLipCorner, this.rightLipCorner) / distance(this.leftNoseCorner, this.rightNoseCorner);
- }
-
- render() {
- return `${round(this.ratio, 2)}`;
- }
-
- ideal() {
- return `${database.entries.mouthToNoseRatio.idealLower} to ${database.entries.mouthToNoseRatio.idealUpper}`;
- }
-
- assess() {
- let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } = database.entries.mouthToNoseRatio;
- return assess(this.ratio, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh);
- }
-
- draw(ctx) {
- draw(ctx, "purple", [this.leftLipCorner, this.rightLipCorner]);
- draw(ctx, "purple", [this.leftNoseCorner, this.rightNoseCorner]);
- }
-
- necessaryPoints() {
- return ["leftLipCorner", "rightLipCorner", "leftNoseCorner", "rightNoseCorner"];
- }
+ calculate() {
+ let mouthWidth = distance(this.leftLipCorner, this.rightLipCorner);
+ let noseWidth = distance(this.leftNoseCorner, this.rightNoseCorner);
+
+ // Avoid division by zero
+ if (noseWidth < 1e-10) {
+ console.warn("MouthToNoseRatio: noseWidth too small, using fallback");
+ this.ratio = 0;
+ return;
+ }
+
+ this.ratio = mouthWidth / noseWidth;
+ }
+
+ render() {
+ return `${round(this.ratio, 2)}`;
+ }
+
+ ideal() {
+ return `${database.entries.mouthToNoseRatio.idealLower} to ${database.entries.mouthToNoseRatio.idealUpper}`;
+ }
+
+ assess() {
+ let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } =
+ database.entries.mouthToNoseRatio;
+ return assess(
+ this.ratio,
+ idealLower,
+ idealUpper,
+ deviation,
+ deviatingLow,
+ deviatingHigh
+ );
+ }
+
+ draw(ctx) {
+ draw(ctx, "purple", [this.leftLipCorner, this.rightLipCorner]);
+ draw(ctx, "purple", [this.leftNoseCorner, this.rightNoseCorner]);
+ }
+
+ necessaryPoints() {
+ return [
+ "leftLipCorner",
+ "rightLipCorner",
+ "leftNoseCorner",
+ "rightNoseCorner",
+ ];
+ }
}
class BigonialWidth extends Criteria {
- calculate() {
- this.ratio = distance(this.leftZygo, this.rightZygo) / distance(this.leftGonial, this.rightGonial);
- }
-
- render() {
- return `${round(this.ratio, 2)}`;
- }
-
- ideal() {
- return `${database.entries.bigonialWidth.idealLower} to ${database.entries.bigonialWidth.idealUpper}`;
- }
-
- assess() {
- let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } = database.entries.bigonialWidth;
- return assess(this.ratio, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh);
- }
-
- draw(ctx) {
- draw(ctx, "gold", [this.leftGonial, this.rightGonial]);
- draw(ctx, "gold", [this.leftZygo, this.rightZygo]);
- }
-
- necessaryPoints() {
- return ["leftZygo", "rightZygo", "leftGonial", "rightGonial"];
- }
+ calculate() {
+ let zygomaticWidth = distance(this.leftZygo, this.rightZygo);
+ let gonialWidth = distance(this.leftGonial, this.rightGonial);
+
+ // Avoid division by zero
+ if (gonialWidth < 1e-10) {
+ console.warn("BigonialWidth: gonialWidth too small, using fallback");
+ this.ratio = 0;
+ return;
+ }
+
+ this.ratio = zygomaticWidth / gonialWidth;
+ }
+
+ render() {
+ return `${round(this.ratio, 2)}`;
+ }
+
+ ideal() {
+ return `${database.entries.bigonialWidth.idealLower} to ${database.entries.bigonialWidth.idealUpper}`;
+ }
+
+ assess() {
+ let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } =
+ database.entries.bigonialWidth;
+ return assess(
+ this.ratio,
+ idealLower,
+ idealUpper,
+ deviation,
+ deviatingLow,
+ deviatingHigh
+ );
+ }
+
+ draw(ctx) {
+ draw(ctx, "gold", [this.leftGonial, this.rightGonial]);
+ draw(ctx, "gold", [this.leftZygo, this.rightZygo]);
+ }
+
+ necessaryPoints() {
+ return ["leftZygo", "rightZygo", "leftGonial", "rightGonial"];
+ }
}
class LipRatio extends Criteria {
- constructor(face, points) {
- super(face, points);
-
- let topLip = Fn.fromTwoPoints(this.leftCupidBow, this.rightCupidBow);
- let lowerLip = topLip.parallel(this.lowerLip);
- this.createPoint("upperLipEnd", topLip.intersect(topLip.perpendicular(this.lipSeparation)));
- this.createPoint("lowerLipEnd", lowerLip.intersect(lowerLip.perpendicular(this.lipSeparation)));
- }
-
- calculate() {
- this.ratio = distance(this.lowerLipEnd, this.lipSeparation) / distance(this.upperLipEnd, this.lipSeparation);
- }
-
- render() {
- return `${round(this.ratio, 2)}`;
- }
-
- ideal() {
- return `${database.entries.lipRatio.idealLower} to ${database.entries.lipRatio.idealUpper}`;
- }
-
- assess() {
- let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } = database.entries.lipRatio;
- return assess(this.ratio, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh);
- }
-
- draw(ctx) {
- draw(ctx, "lightgreen", [this.upperLipEnd, this.lipSeparation]);
- draw(ctx, "lightgreen", [this.lipSeparation, this.lowerLipEnd]);
- }
-
- necessaryPoints() {
- return ["upperLipEnd", "lowerLipEnd", "lipSeparation"];
- }
+ constructor(face, points) {
+ super(face, points);
+
+ let topLip = Fn.fromTwoPoints(this.leftCupidBow, this.rightCupidBow);
+ let lowerLip = topLip.parallel(this.lowerLip);
+ this.createPoint(
+ "upperLipEnd",
+ topLip.intersect(topLip.perpendicular(this.lipSeparation))
+ );
+ this.createPoint(
+ "lowerLipEnd",
+ lowerLip.intersect(lowerLip.perpendicular(this.lipSeparation))
+ );
+ }
+
+ calculate() {
+ let lowerDistance = distance(this.lowerLipEnd, this.lipSeparation);
+ let upperDistance = distance(this.upperLipEnd, this.lipSeparation);
+
+ // Avoid division by zero
+ if (upperDistance < 1e-10) {
+ console.warn("LipRatio: upperDistance too small, using fallback");
+ this.ratio = 0;
+ return;
+ }
+
+ this.ratio = lowerDistance / upperDistance;
+ }
+
+ render() {
+ return `${round(this.ratio, 2)}`;
+ }
+
+ ideal() {
+ return `${database.entries.lipRatio.idealLower} to ${database.entries.lipRatio.idealUpper}`;
+ }
+
+ assess() {
+ let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } =
+ database.entries.lipRatio;
+ return assess(
+ this.ratio,
+ idealLower,
+ idealUpper,
+ deviation,
+ deviatingLow,
+ deviatingHigh
+ );
+ }
+
+ draw(ctx) {
+ draw(ctx, "lightgreen", [this.upperLipEnd, this.lipSeparation]);
+ draw(ctx, "lightgreen", [this.lipSeparation, this.lowerLipEnd]);
+ }
+
+ necessaryPoints() {
+ return ["upperLipEnd", "lowerLipEnd", "lipSeparation"];
+ }
}
class EyeSeparationRatio extends Criteria {
- calculate() {
- this.ratio = distance(this.leftIris, this.rightIris) / distance(this.leftZygo, this.rightZygo);
- }
-
- render() {
- return `${round(this.ratio, 2)}`;
- }
-
- ideal() {
- return `${database.entries.eyeSeparationRatio.idealLower} to ${database.entries.eyeSeparationRatio.idealUpper}`;
- }
-
- assess() {
- let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } = database.entries.eyeSeparationRatio;
- return assess(this.ratio, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh);
- }
-
- draw(ctx) {
- draw(ctx, "orange", [this.leftIris, this.rightIris]);
- draw(ctx, "orange", [this.leftZygo, this.rightZygo]);
- }
-
- necessaryPoints() {
- return ["leftIris", "rightIris", "leftZygo", "rightZygo"];
- }
+ calculate() {
+ let eyeDistance = distance(this.leftIris, this.rightIris);
+ let faceWidth = distance(this.leftZygo, this.rightZygo);
+
+ // Avoid division by zero
+ if (faceWidth < 1e-10) {
+ console.warn("EyeSeparationRatio: faceWidth too small, using fallback");
+ this.ratio = 0;
+ return;
+ }
+
+ this.ratio = eyeDistance / faceWidth;
+ }
+
+ render() {
+ return `${round(this.ratio, 2)}`;
+ }
+
+ ideal() {
+ return `${database.entries.eyeSeparationRatio.idealLower} to ${database.entries.eyeSeparationRatio.idealUpper}`;
+ }
+
+ assess() {
+ let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } =
+ database.entries.eyeSeparationRatio;
+ return assess(
+ this.ratio,
+ idealLower,
+ idealUpper,
+ deviation,
+ deviatingLow,
+ deviatingHigh
+ );
+ }
+
+ draw(ctx) {
+ draw(ctx, "orange", [this.leftIris, this.rightIris]);
+ draw(ctx, "orange", [this.leftZygo, this.rightZygo]);
+ }
+
+ necessaryPoints() {
+ return ["leftIris", "rightIris", "leftZygo", "rightZygo"];
+ }
}
class EyeToMouthAngle extends Criteria {
- calculate() {
- let a = [this.leftIris[0] - this.lipSeparation[0], this.leftIris[1] - this.lipSeparation[1]];
- let b = [this.rightIris[0] - this.lipSeparation[0], this.rightIris[1] - this.lipSeparation[1]];
- this.angle = Math.acos(
- (a[0] * b[0] + a[1] * b[1])
- /
- (Math.sqrt(a[0] ** 2 + a[1] ** 2) * Math.sqrt(b[0] ** 2 + b[1] ** 2))
- ) * (180 / Math.PI);
- }
-
- render() {
- return `${round(this.angle, 0)}°`;
- }
-
- ideal() {
- return `${database.entries.eyeToMouthAngle.idealLower}° to ${database.entries.eyeToMouthAngle.idealUpper}°`;
- }
-
- assess() {
- let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } = database.entries.eyeToMouthAngle;
- return assess(this.angle, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh);
- }
-
- draw(ctx) {
- draw(ctx, "brown", [this.leftIris, this.lipSeparation, this.rightIris, this.lipSeparation, this.leftIris]);
- }
-
- necessaryPoints() {
- return ["leftIris", "lipSeparation", "rightIris"];
- }
+ calculate() {
+ let a = [
+ this.leftIris[0] - this.lipSeparation[0],
+ this.leftIris[1] - this.lipSeparation[1],
+ ];
+ let b = [
+ this.rightIris[0] - this.lipSeparation[0],
+ this.rightIris[1] - this.lipSeparation[1],
+ ];
+
+ let dotProduct = a[0] * b[0] + a[1] * b[1];
+ let magnitudeA = Math.sqrt(a[0] ** 2 + a[1] ** 2);
+ let magnitudeB = Math.sqrt(b[0] ** 2 + b[1] ** 2);
+
+ // Avoid division by zero and clamp acos input to valid range [-1, 1]
+ if (magnitudeA < 1e-10 || magnitudeB < 1e-10) {
+ console.warn("EyeToMouthAngle: magnitude too small, using fallback");
+ this.angle = 0;
+ return;
+ }
+
+ let cosAngle = dotProduct / (magnitudeA * magnitudeB);
+ // Clamp to valid range for acos
+ cosAngle = Math.max(-1, Math.min(1, cosAngle));
+
+ this.angle = Math.acos(cosAngle) * (180 / Math.PI);
+ }
+
+ render() {
+ return `${round(this.angle, 0)}°`;
+ }
+
+ ideal() {
+ return `${database.entries.eyeToMouthAngle.idealLower}° to ${database.entries.eyeToMouthAngle.idealUpper}°`;
+ }
+
+ assess() {
+ let { idealLower, idealUpper, deviation, deviatingLow, deviatingHigh } =
+ database.entries.eyeToMouthAngle;
+ return assess(
+ this.angle,
+ idealLower,
+ idealUpper,
+ deviation,
+ deviatingLow,
+ deviatingHigh
+ );
+ }
+
+ draw(ctx) {
+ draw(ctx, "brown", [
+ this.leftIris,
+ this.lipSeparation,
+ this.rightIris,
+ this.lipSeparation,
+ this.leftIris,
+ ]);
+ }
+
+ necessaryPoints() {
+ return ["leftIris", "lipSeparation", "rightIris"];
+ }
}
class LowerThirdHeight extends Criteria {
- calculate() {
- let middlePoint = [this.leftNoseCorner[0] + (1/2) * (this.rightNoseCorner[0] - this.leftNoseCorner[0]), this.leftNoseCorner[1] + (1/2) * (this.rightNoseCorner[1] - this.leftNoseCorner[1])];
- let middleLine = Fn.fromTwoPoints(this.leftNoseCorner, this.rightNoseCorner).perpendicular(middlePoint);
- let topPoint = middleLine.intersect(Fn.fromTwoPoints(this.leftEyebrow, this.rightEyebrow));
- let bottomPoint = middleLine.intersect(Fn.fromTwoPoints(this.chinLeft, this.chinRight));
- this.ratio = distance(bottomPoint, middlePoint) / distance(middlePoint, topPoint);
- }
-
- render() {
- return `${round(this.ratio, 2)}`;
- }
-
- ideal() {
- return `more than ${database.entries.lowerThirdHeight.idealLower}`;
- }
-
- assess() {
- let { idealLower, deviation, deviatingLow } = database.entries.lowerThirdHeight;
- return assess(this.ratio, idealLower, undefined, deviation, deviatingLow, undefined);
- }
-
- draw(ctx) {
- draw(ctx, "grey", [this.leftEyebrow, this.rightEyebrow, this.rightNoseCorner, this.leftNoseCorner]);
- draw(ctx, "grey", [this.leftNoseCorner, this.rightNoseCorner, this.chinRight, this.chinLeft]);
- }
-
- necessaryPoints() {
- return ["leftNoseCorner", "rightNoseCorner", "leftEyebrow", "rightEyebrow", "chinLeft", "chinRight"];
- }
+ calculate() {
+ const MIDPOINT_FACTOR = 0.5; // Constant for midpoint calculation
+ let middlePoint = [
+ this.leftNoseCorner[0] +
+ MIDPOINT_FACTOR * (this.rightNoseCorner[0] - this.leftNoseCorner[0]),
+ this.leftNoseCorner[1] +
+ MIDPOINT_FACTOR * (this.rightNoseCorner[1] - this.leftNoseCorner[1]),
+ ];
+ let middleLine = Fn.fromTwoPoints(
+ this.leftNoseCorner,
+ this.rightNoseCorner
+ ).perpendicular(middlePoint);
+ let topPoint = middleLine.intersect(
+ Fn.fromTwoPoints(this.leftEyebrow, this.rightEyebrow)
+ );
+ let bottomPoint = middleLine.intersect(
+ Fn.fromTwoPoints(this.chinLeft, this.chinRight)
+ );
+
+ let bottomDistance = distance(bottomPoint, middlePoint);
+ let topDistance = distance(middlePoint, topPoint);
+
+ // Avoid division by zero
+ if (topDistance < 1e-10) {
+ console.warn("LowerThirdHeight: topDistance too small, using fallback");
+ this.ratio = 0;
+ return;
+ }
+
+ this.ratio = bottomDistance / topDistance;
+ }
+
+ render() {
+ return `${round(this.ratio, 2)}`;
+ }
+
+ ideal() {
+ return `more than ${database.entries.lowerThirdHeight.idealLower}`;
+ }
+
+ assess() {
+ let { idealLower, deviation, deviatingLow } =
+ database.entries.lowerThirdHeight;
+ return assess(
+ this.ratio,
+ idealLower,
+ undefined,
+ deviation,
+ deviatingLow,
+ undefined
+ );
+ }
+
+ draw(ctx) {
+ draw(ctx, "grey", [
+ this.leftEyebrow,
+ this.rightEyebrow,
+ this.rightNoseCorner,
+ this.leftNoseCorner,
+ ]);
+ draw(ctx, "grey", [
+ this.leftNoseCorner,
+ this.rightNoseCorner,
+ this.chinRight,
+ this.chinLeft,
+ ]);
+ }
+
+ necessaryPoints() {
+ return [
+ "leftNoseCorner",
+ "rightNoseCorner",
+ "leftEyebrow",
+ "rightEyebrow",
+ "chinLeft",
+ "chinRight",
+ ];
+ }
}
class PalpebralFissureLength extends Criteria {
- calculate() {
- this.leftPFL = distance(this.leftLateralCanthus, this.leftMedialCanthus) / distance(this.leftEyeUpper, this.leftEyeLower);
- this.rightPFL = distance(this.rightLateralCanthus, this.rightMedialCanthus) / distance(this.rightEyeUpper, this.rightEyeLower);
- }
-
- render() {
- return `left ${round(this.rightPFL, 2)}, right ${round(this.leftPFL, 2)}`;
- }
-
- ideal() {
- return `more than ${database.entries.palpebralFissureLength.idealLower}`;
- }
-
- assess() {
- let { idealLower, deviation, deviatingLow } = database.entries.palpebralFissureLength;
- return assess((this.leftPFL + this.rightPFL) / 2, idealLower, undefined, deviation, deviatingLow, undefined);
- }
-
- draw(ctx) {
- draw(ctx, "aquamarine", [this.leftLateralCanthus, this.leftMedialCanthus]);
- draw(ctx, "aquamarine", [this.leftEyeUpper, this.leftEyeLower]);
- draw(ctx, "aquamarine", [this.rightLateralCanthus, this.rightMedialCanthus]);
- draw(ctx, "aquamarine", [this.rightEyeUpper, this.rightEyeLower]);
+ calculate() {
+ let leftCanthalDistance = distance(
+ this.leftLateralCanthus,
+ this.leftMedialCanthus
+ );
+ let leftEyeHeight = distance(this.leftEyeUpper, this.leftEyeLower);
+ let rightCanthalDistance = distance(
+ this.rightLateralCanthus,
+ this.rightMedialCanthus
+ );
+ let rightEyeHeight = distance(this.rightEyeUpper, this.rightEyeLower);
+
+ // Avoid division by zero
+ if (leftEyeHeight < 1e-10) {
+ console.warn("PalpebralFissureLength: leftEyeHeight too small");
+ this.leftPFL = 0;
+ } else {
+ this.leftPFL = leftCanthalDistance / leftEyeHeight;
}
- necessaryPoints() {
- return ["leftLateralCanthus", "leftMedialCanthus", "leftEyeUpper", "leftEyeLower", "rightLateralCanthus", "rightMedialCanthus", "rightEyeUpper", "rightEyeLower"];
- }
+ if (rightEyeHeight < 1e-10) {
+ console.warn("PalpebralFissureLength: rightEyeHeight too small");
+ this.rightPFL = 0;
+ } else {
+ this.rightPFL = rightCanthalDistance / rightEyeHeight;
+ }
+ }
+
+ render() {
+ return `left ${round(this.rightPFL, 2)}, right ${round(this.leftPFL, 2)}`;
+ }
+
+ ideal() {
+ return `more than ${database.entries.palpebralFissureLength.idealLower}`;
+ }
+
+ assess() {
+ let { idealLower, deviation, deviatingLow } =
+ database.entries.palpebralFissureLength;
+ return assess(
+ (this.leftPFL + this.rightPFL) / 2,
+ idealLower,
+ undefined,
+ deviation,
+ deviatingLow,
+ undefined
+ );
+ }
+
+ draw(ctx) {
+ draw(ctx, "aquamarine", [this.leftLateralCanthus, this.leftMedialCanthus]);
+ draw(ctx, "aquamarine", [this.leftEyeUpper, this.leftEyeLower]);
+ draw(ctx, "aquamarine", [
+ this.rightLateralCanthus,
+ this.rightMedialCanthus,
+ ]);
+ draw(ctx, "aquamarine", [this.rightEyeUpper, this.rightEyeLower]);
+ }
+
+ necessaryPoints() {
+ return [
+ "leftLateralCanthus",
+ "leftMedialCanthus",
+ "leftEyeUpper",
+ "leftEyeLower",
+ "rightLateralCanthus",
+ "rightMedialCanthus",
+ "rightEyeUpper",
+ "rightEyeLower",
+ ];
+ }
}
class EyeColor extends Criteria {
- calculate() {
- this.leftIrisCoordinates = this.face.annotations.rightEyeIris;
- this.leftIrisWidth = this.leftIrisCoordinates[1][0] - this.leftIrisCoordinates[3][0];
- this.leftIrisHeight = this.leftIrisCoordinates[4][1] - this.leftIrisCoordinates[2][1];
- this.rightIrisCoordinates = this.face.annotations.leftEyeIris;
- this.rightIrisWidth = this.rightIrisCoordinates[3][0] - this.rightIrisCoordinates[1][0];
- this.rightIrisHeight = this.rightIrisCoordinates[4][1] - this.rightIrisCoordinates[2][1];
- }
-
- render() {
- return `<canvas height="0" width="0"></canvas><canvas height="0" width="0"></canvas>`;
- }
-
- ideal() {
- return "";
- }
-
- assess() {
- return "";
- }
-
- detect(image, [ctx0, ctx1]) {
- ctx0.canvas.width = this.leftIrisWidth;
- ctx0.canvas.height = this.leftIrisHeight;
- ctx0.drawImage(image, this.leftIrisCoordinates[3][0], this.leftIrisCoordinates[2][1], this.leftIrisWidth, this.leftIrisHeight, 0, 0, this.leftIrisWidth, this.leftIrisHeight);
- ctx1.canvas.width = this.rightIrisWidth;
- ctx1.canvas.height = this.rightIrisHeight;
- ctx1.drawImage(image, this.rightIrisCoordinates[1][0], this.rightIrisCoordinates[2][1], this.rightIrisWidth, this.rightIrisHeight, 0, 0, this.rightIrisWidth, this.rightIrisHeight);
- }
-
- necessaryPoints() {
- return [];
- }
+ calculate() {
+ this.leftIrisCoordinates = this.face.annotations.rightEyeIris;
+ this.leftIrisWidth =
+ this.leftIrisCoordinates[1][0] - this.leftIrisCoordinates[3][0];
+ this.leftIrisHeight =
+ this.leftIrisCoordinates[4][1] - this.leftIrisCoordinates[2][1];
+ this.rightIrisCoordinates = this.face.annotations.leftEyeIris;
+ this.rightIrisWidth =
+ this.rightIrisCoordinates[3][0] - this.rightIrisCoordinates[1][0];
+ this.rightIrisHeight =
+ this.rightIrisCoordinates[4][1] - this.rightIrisCoordinates[2][1];
+ }
+
+ render() {
+ return `<canvas height="0" width="0"></canvas><canvas height="0" width="0"></canvas>`;
+ }
+
+ ideal() {
+ return "";
+ }
+
+ assess() {
+ return "";
+ }
+
+ detect(image, [ctx0, ctx1]) {
+ ctx0.canvas.width = this.leftIrisWidth;
+ ctx0.canvas.height = this.leftIrisHeight;
+ ctx0.drawImage(
+ image,
+ this.leftIrisCoordinates[3][0],
+ this.leftIrisCoordinates[2][1],
+ this.leftIrisWidth,
+ this.leftIrisHeight,
+ 0,
+ 0,
+ this.leftIrisWidth,
+ this.leftIrisHeight
+ );
+ ctx1.canvas.width = this.rightIrisWidth;
+ ctx1.canvas.height = this.rightIrisHeight;
+ ctx1.drawImage(
+ image,
+ this.rightIrisCoordinates[1][0],
+ this.rightIrisCoordinates[2][1],
+ this.rightIrisWidth,
+ this.rightIrisHeight,
+ 0,
+ 0,
+ this.rightIrisWidth,
+ this.rightIrisHeight
+ );
+ }
+
+ necessaryPoints() {
+ return [];
+ }
}
function distance([ax, ay], [bx, by]) {
- return Math.sqrt((ax - bx) ** 2 + (ay - by) ** 2);
+ return Math.sqrt((ax - bx) ** 2 + (ay - by) ** 2);
}
class Fn {
- constructor(a, b) {
- // y = ax + b
- this.a = a;
- this.b = b;
-
- this.slope = this.a;
- this.yintersect = this.b;
- }
-
- static fromTwoPoints([ax, ay], [bx, by]) {
- // y = (ay / by) / (ax - bx) * (x - ax) + ay
- return Fn.fromOffset((ay - by) / (ax - bx), ax, ay);
- }
-
- static fromOffset(a, b, c) {
- // y = a * (x - b) + c
- return new Fn(a, c - (a * b));
- }
-
- getY(x) {
- return this.a * x + this.b;
- }
-
- perpendicular([x, y]) {
- return Fn.fromOffset((-1) * (1 / this.a), x, y);
- }
-
- parallel([x, y]) {
- return Fn.fromOffset(this.a, x, y);
- }
-
- intersect(fn) {
- let x = (fn.b - this.b) / (this.a - fn.a);
- return [x, this.getY(x)];
- }
-
- draw(ctx, color) {
- let points = [];
- for(let i = 0; i < ctx.canvas.width; i += 1) {
- points.push([i, this.getY(i)]);
- }
- draw(ctx, color || "red", points);
- }
+ constructor(a, b) {
+ // y = ax + b
+ this.a = a;
+ this.b = b;
+
+ this.slope = this.a;
+ this.yintersect = this.b;
+ }
+
+ static fromTwoPoints([ax, ay], [bx, by]) {
+ // Handle vertical line (same x-coordinate)
+ if (Math.abs(ax - bx) < 1e-10) {
+ // Return a vertical line representation
+ // For vertical lines, we'll use a very large slope and special handling
+ return new Fn(Infinity, ax); // Store x-coordinate in b for vertical lines
+ }
+ // y = (ay - by) / (ax - bx) * (x - ax) + ay
+ return Fn.fromOffset((ay - by) / (ax - bx), ax, ay);
+ }
+
+ static fromOffset(a, b, c) {
+ // y = a * (x - b) + c
+ return new Fn(a, c - a * b);
+ }
+
+ getY(x) {
+ // Handle vertical line
+ if (!isFinite(this.a)) {
+ return this.b; // For vertical lines, b stores the x-coordinate
+ }
+ return this.a * x + this.b;
+ }
+
+ perpendicular([x, y]) {
+ // Handle vertical line (perpendicular is horizontal)
+ if (!isFinite(this.a)) {
+ return new Fn(0, y); // Horizontal line
+ }
+ // Handle horizontal line (perpendicular is vertical)
+ if (Math.abs(this.a) < 1e-10) {
+ return new Fn(Infinity, x); // Vertical line
+ }
+ return Fn.fromOffset(-1 * (1 / this.a), x, y);
+ }
+
+ parallel([x, y]) {
+ return Fn.fromOffset(this.a, x, y);
+ }
+
+ intersect(fn) {
+ // Handle vertical lines
+ if (!isFinite(this.a)) {
+ // This line is vertical at x = this.b
+ if (!isFinite(fn.a)) {
+ // Both are vertical - parallel or same line
+ return [this.b, 0]; // Return arbitrary point
+ }
+ let x = this.b;
+ return [x, fn.getY(x)];
+ }
+ if (!isFinite(fn.a)) {
+ // Other line is vertical at x = fn.b
+ let x = fn.b;
+ return [x, this.getY(x)];
+ }
+ // Handle parallel lines
+ if (Math.abs(this.a - fn.a) < 1e-10) {
+ // Lines are parallel, return midpoint or first point
+ return [0, this.b];
+ }
+ let x = (fn.b - this.b) / (this.a - fn.a);
+ return [x, this.getY(x)];
+ }
+
+ draw(ctx, color) {
+ let points = [];
+ for (let i = 0; i < ctx.canvas.width; i += 1) {
+ points.push([i, this.getY(i)]);
+ }
+ draw(ctx, color || "red", points);
+ }
}
function round(n, digits) {
- digits = 10 ** (isNaN(digits) ? 2 : digits);
- return Math.round(n * digits) / digits;
+ digits = 10 ** (isNaN(digits) ? 2 : digits);
+ return Math.round(n * digits) / digits;
}
-function assess(value, idealLower, idealUpper, deviation, deviatingLow, deviatingHigh) {
- function renderMultiplier(multiplier) {
- if (multiplier === 0) {
- return "slightly too";
- } else if (multiplier === 1) {
- return "noticeably";
- } else if (multiplier === 2) {
- return "significantly too";
- } else if (multiplier === 3) {
- return "horribly";
- } else {
- return "extremely";
- }
- }
-
- function calculate(value, idealLower, idealUpper, deviation) {
-
- if (idealUpper !== undefined && idealLower !== undefined) {
- if (idealUpper >= value && idealLower <= value) {
- return {
- type: "perfect",
- };
- }
- } else if (((idealUpper && !idealLower) && value <= idealUpper) || ((!idealUpper && idealLower) && value >= idealLower)) {
- return {
- type: "perfect",
- };
- }
-
- if (value < idealLower) {
- let multiplier = 0;
- while ((value += deviation) < idealLower) {
- multiplier++;
- }
- return {
- type: "low",
- multiplier: Math.min(multiplier, 4),
- text: renderMultiplier(multiplier),
- };
- }
-
- if (value > idealUpper) {
- let multiplier = 0;
- while ((value -= deviation) > idealUpper) {
- multiplier++;
- }
- return {
- type: "high",
- multiplier: Math.min(multiplier, 4),
- text: renderMultiplier(multiplier),
- };
- }
-
- }
-
- let { type, multiplier, text } = calculate(value, idealLower, idealUpper, deviation);
-
- if (type === "perfect") {
- return `<span class="perfect">perfect</span>`;
- } else if (type === "low") {
- return `<span class="deviation-${multiplier}">${text} ${deviatingLow}</span>`;
- } else if (type === "high") {
- return `<span class="deviation-${multiplier}">${text} ${deviatingHigh}</span>`;
- }
+function assess(
+ value,
+ idealLower,
+ idealUpper,
+ deviation,
+ deviatingLow,
+ deviatingHigh
+) {
+ // Validate inputs
+ if (typeof value !== "number" || !isFinite(value)) {
+ console.warn("Invalid value in assess:", value);
+ return '<span class="deviation-4">invalid measurement</span>';
+ }
+
+ if (deviation <= 0) {
+ console.warn("Invalid deviation in assess:", deviation);
+ return '<span class="deviation-4">invalid configuration</span>';
+ }
+
+ function renderMultiplier(multiplier) {
+ if (multiplier === 0) {
+ return "slightly too";
+ } else if (multiplier === 1) {
+ return "noticeably";
+ } else if (multiplier === 2) {
+ return "significantly too";
+ } else if (multiplier === 3) {
+ return "horribly";
+ } else {
+ return "extremely";
+ }
+ }
+
+ function calculate(value, idealLower, idealUpper, deviation) {
+ // Check if value is in ideal range
+ if (idealUpper !== undefined && idealLower !== undefined) {
+ if (idealUpper >= value && idealLower <= value) {
+ return {
+ type: "perfect",
+ };
+ }
+ } else if (
+ (idealUpper && !idealLower && value <= idealUpper) ||
+ (!idealUpper && idealLower && value >= idealLower)
+ ) {
+ return {
+ type: "perfect",
+ };
+ }
+
+ // Calculate deviation multiplier (don't mutate the original value)
+ if (idealLower !== undefined && value < idealLower) {
+ let multiplier = 0;
+ let testValue = value;
+ while ((testValue += deviation) < idealLower) {
+ multiplier++;
+ }
+ return {
+ type: "low",
+ multiplier: Math.min(multiplier, 4),
+ text: renderMultiplier(multiplier),
+ };
+ }
+
+ if (idealUpper !== undefined && value > idealUpper) {
+ let multiplier = 0;
+ let testValue = value;
+ while ((testValue -= deviation) > idealUpper) {
+ multiplier++;
+ }
+ return {
+ type: "high",
+ multiplier: Math.min(multiplier, 4),
+ text: renderMultiplier(multiplier),
+ };
+ }
+
+ // Fallback (shouldn't reach here)
+ return {
+ type: "perfect",
+ };
+ }
+
+ let result = calculate(value, idealLower, idealUpper, deviation);
+ if (!result) {
+ return '<span class="deviation-4">calculation error</span>';
+ }
+
+ let { type, multiplier, text } = result;
+
+ if (type === "perfect") {
+ return `<span class="perfect">perfect</span>`;
+ } else if (type === "low") {
+ return `<span class="deviation-${multiplier}">${text} ${deviatingLow}</span>`;
+ } else if (type === "high") {
+ return `<span class="deviation-${multiplier}">${text} ${deviatingHigh}</span>`;
+ }
}
-function draw(ctx, color, points) {
- ctx.strokeStyle = color;
- ctx.fillStyle = color;
+// Cache watermark settings to avoid recalculating on every draw
+let watermarkCache = null;
- let current = points[0];
-
- var fontBase = canvas.width * 0.6; // selected default width for canvas
- var fontSize = 20; // default size for font
-
+function draw(ctx, color, points) {
+ // Validate inputs
+ if (!points || points.length === 0) {
+ return;
+ }
+
+ // Validate points are valid coordinates
+ for (let point of points) {
+ if (
+ !Array.isArray(point) ||
+ point.length < 2 ||
+ !isFinite(point[0]) ||
+ !isFinite(point[1])
+ ) {
+ console.warn("Invalid point in draw:", point);
+ return;
+ }
+ }
+
+ ctx.strokeStyle = color;
+ ctx.fillStyle = color;
+
+ let current = points[0];
+
+ // Draw watermark only once per canvas (cache the settings)
+ if (
+ !watermarkCache ||
+ watermarkCache.canvasWidth !== canvas.width ||
+ watermarkCache.canvasHeight !== canvas.height
+ ) {
+ watermarkCache = {
+ canvasWidth: canvas.width,
+ canvasHeight: canvas.height,
+ fontBase: canvas.width * 0.6,
+ fontSize: 20,
+ text: "www.incel.solutions (powered by $INCEL COIN)",
+ };
- var ratio = fontSize / fontBase; // calc ratio
- var size = canvas.width * ratio; // get font size based on current width
- if(canvas.width > 600) {
- ctx.font= size + 'px sans-serif';
- } else {
- ctx.font= '18px sans-serif';
-
- } var textWidth=ctx.measureText("text").width;
- ctx.globalAlpha=.50;
- ctx.fillStyle='white'
- var text = "www.incel.solutions (powered by $INCEL COIN)"
- cw = canvas.width;
- ch = canvas.height;
- var textWidth=ctx.measureText(text).width;
- ctx.fillStyle='gray'
- ctx.fillText(text,cw-textWidth-10,ch-20);
- ctx.fillStyle='white'
- ctx.fillText(text,cw-textWidth-10+2,ch-20+2);
-
- for (let i of points.concat([points[0]])) {
- let [x, y] = i;
-
- ctx.beginPath();
- ctx.moveTo(current[0], current[1]);
- ctx.lineTo(x, y);
- ctx.stroke();
- ctx.beginPath();
- ctx.arc(x, y, ctx.arcRadius, 0, 2 * Math.PI);
- ctx.fill();
-
- current = i;
- }
+ var ratio = watermarkCache.fontSize / watermarkCache.fontBase;
+ watermarkCache.size = canvas.width * ratio;
+ watermarkCache.font =
+ canvas.width > 600
+ ? watermarkCache.size + "px sans-serif"
+ : "18px sans-serif";
+ }
+
+ // Draw watermark (only if not already drawn on this frame)
+ if (!ctx._watermarkDrawn) {
+ ctx.save();
+ ctx.font = watermarkCache.font;
+ ctx.globalAlpha = 0.5;
+ var textWidth = ctx.measureText(watermarkCache.text).width;
+ var cw = watermarkCache.canvasWidth;
+ var ch = watermarkCache.canvasHeight;
+
+ // Gray shadow
+ ctx.fillStyle = "gray";
+ ctx.fillText(watermarkCache.text, cw - textWidth - 10, ch - 20);
+
+ // White text
+ ctx.fillStyle = "white";
+ ctx.fillText(watermarkCache.text, cw - textWidth - 10 + 2, ch - 20 + 2);
+
+ ctx.restore();
+ ctx._watermarkDrawn = true;
+ }
+
+ // Draw the actual shape/points
+ for (let i of points.concat([points[0]])) {
+ let [x, y] = i;
+
+ ctx.beginPath();
+ ctx.moveTo(current[0], current[1]);
+ ctx.lineTo(x, y);
+ ctx.stroke();
+ ctx.beginPath();
+ ctx.arc(x, y, ctx.arcRadius || 3, 0, 2 * Math.PI);
+ ctx.fill();
+
+ current = i;
+ }
}
diff --git a/js/index.js b/js/index.js
index 0bc87ea..3881d38 100644
--- a/js/index.js
+++ b/js/index.js
@@ -252,6 +252,8 @@ if (gradingToggle) {
let render = () => {
analysis.resetToImage();
+ // Reset watermark flag for new frame
+ ctx._watermarkDrawn = false;
for (let i of Object.values(analysis.criteria)) {
if (i.toggle.checked) {
i.analysis.draw(ctx);