diff options
| author | Fuwn <[email protected]> | 2025-12-02 00:10:52 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2025-12-02 00:10:52 -0800 |
| commit | 61f1624a14f80b416c530cbb82e430302d12bda3 (patch) | |
| tree | bd7a6a4e0b6d86c8fe84b3949662f9059750f783 | |
| parent | feat(index.html): Remove introduction (diff) | |
| download | rysk-61f1624a14f80b416c530cbb82e430302d12bda3.tar.xz rysk-61f1624a14f80b416c530cbb82e430302d12bda3.zip | |
fix(analysis.js): Improve analysis engine
| -rw-r--r-- | js/analysis.js | 1682 | ||||
| -rw-r--r-- | js/index.js | 2 |
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); |