From e2dd80067401cacddc55d0419a47d9d468afb93b Mon Sep 17 00:00:00 2001 From: PeenScreeker Date: Fri, 8 Sep 2023 22:07:11 -0400 Subject: [PATCH 1/8] chore: move cgaz math functions to util files --- layout/hud/cgaz.xml | 2 + scripts/hud/cgaz.js | 205 +++++++++++------------------------------ scripts/util/colors.js | 71 ++++++++++++++ scripts/util/math.js | 115 +++++++++++++++++++++++ 4 files changed, 243 insertions(+), 150 deletions(-) create mode 100644 scripts/util/colors.js create mode 100644 scripts/util/math.js diff --git a/layout/hud/cgaz.xml b/layout/hud/cgaz.xml index 5374ad42..435de46a 100644 --- a/layout/hud/cgaz.xml +++ b/layout/hud/cgaz.xml @@ -4,6 +4,8 @@ + + diff --git a/scripts/hud/cgaz.js b/scripts/hud/cgaz.js index 647efd61..71b849d9 100644 --- a/scripts/hud/cgaz.js +++ b/scripts/hud/cgaz.js @@ -420,32 +420,31 @@ class Cgaz { } const velocity = MomentumPlayerAPI.GetVelocity(); - const speed = this.getSize(velocity); + const speed = getSize(velocity); const stopSpeed = Math.max(speed, MomentumMovementAPI.GetStopspeed()); const dropSpeed = Math.max(speed - stopSpeed * lastMoveData.friction * tickInterval, 0); const speedSquared = speed * speed; const dropSpeedSquared = dropSpeed * dropSpeed; - const velDir = this.getNormal(velocity, 0.001); + const velDir = getNormal(velocity, 0.001); const velAngle = Math.atan2(velocity.y, velocity.x); const wishDir = lastMoveData.wishdir; - const wishAngle = this.getSizeSquared(wishDir) > 0.001 ? Math.atan2(wishDir.y, wishDir.x) : 0; + const wishAngle = getSizeSquared(wishDir) > 0.001 ? Math.atan2(wishDir.y, wishDir.x) : 0; const viewAngle = (MomentumPlayerAPI.GetAngles().y * Math.PI) / 180; const viewDir = { x: Math.cos(viewAngle), y: Math.sin(viewAngle) }; - const forwardMove = Math.round(this.getDot(viewDir, wishDir)); - const rightMove = Math.round(this.getCross(viewDir, wishDir)); + const forwardMove = Math.round(getDot(viewDir, wishDir)); + const rightMove = Math.round(getCross(viewDir, wishDir)); const bIsFalling = lastMoveData.moveStatus === 0; - const bHasAirControl = phyMode && this.floatEquals(wishAngle, viewAngle, 0.01) && bIsFalling; - const bSnapShift = - !this.floatEquals(Math.abs(forwardMove), Math.abs(rightMove), 0.01) && !(phyMode && bIsFalling); + const bHasAirControl = phyMode && floatEquals(wishAngle, viewAngle, 0.01) && bIsFalling; + const bSnapShift = !floatEquals(Math.abs(forwardMove), Math.abs(rightMove), 0.01) && !(phyMode && bIsFalling); // find cgaz angles - const angleOffset = this.remapAngle(velAngle - wishAngle); + const angleOffset = remapAngle(velAngle - wishAngle); const slowCgazAngle = this.findSlowAngle(dropSpeed, dropSpeedSquared, speedSquared, maxSpeed); const fastCgazAngle = this.findFastAngle(dropSpeed, maxSpeed, maxAccel); const turnCgazAngle = this.findTurnAngle(speed, dropSpeed, maxAccel, fastCgazAngle); @@ -528,10 +527,10 @@ class Cgaz { turnMirrorAngle ); - let mirrorOffset = this.remapAngle(velAngle - viewAngle); - const inputAngle = this.remapAngle(viewAngle - wishAngle); + let mirrorOffset = remapAngle(velAngle - viewAngle); + const inputAngle = remapAngle(viewAngle - wishAngle); - if (this.floatEquals(Math.abs(inputAngle), 0.25 * Math.PI, 0.01)) { + if (floatEquals(Math.abs(inputAngle), 0.25 * Math.PI, 0.01)) { mirrorOffset += (inputAngle > 0 ? -1 : 1) * Math.PI * 0.25; this.updateZone( this.leftMirrorZone, @@ -573,13 +572,11 @@ class Cgaz { } if (this.snapEnable && this.snapAccel) { - const snapOffset = this.remapAngle( - (bSnapShift ? 0 : Math.PI * 0.25 * (rightMove > 0 ? -1 : 1)) - viewAngle - ); + const snapOffset = remapAngle((bSnapShift ? 0 : Math.PI * 0.25 * (rightMove > 0 ? -1 : 1)) - viewAngle); // draw snap zones if (speed >= this.snapMinSpeed) { - const targetOffset = this.remapAngle(velAngle - viewAngle); + const targetOffset = remapAngle(velAngle - viewAngle); const targetAngle = this.findFastAngle(dropSpeed, MAX_GROUND_SPEED, MAX_GROUND_SPEED * tickInterval); const leftTarget = -targetAngle - targetOffset + Math.PI * 0.25; const rightTarget = targetAngle - targetOffset - Math.PI * 0.25; @@ -594,9 +591,7 @@ class Cgaz { this.clearZones(this.primeZones); if (this.primeEnable) { - const snapOffset = this.remapAngle( - (bSnapShift ? 0 : Math.PI * 0.25 * (rightMove > 0 ? -1 : 1)) - viewAngle - ); + const snapOffset = remapAngle((bSnapShift ? 0 : Math.PI * 0.25 * (rightMove > 0 ? -1 : 1)) - viewAngle); if (speed > this.primeMinSpeed) { const primeMaxSpeed = @@ -643,7 +638,7 @@ class Cgaz { // arrow if (this.primeArrowEnable) { - if (this.getSizeSquared(wishDir) > 0) { + if (getSizeSquared(wishDir) > 0) { let arrowAngle = wishAngle - Math.atan2( @@ -656,7 +651,7 @@ class Cgaz { } else { this.compassArrowIcon.RemoveClass('arrow__up'); this.compassArrowIcon.AddClass('arrow__down'); - arrowAngle = this.remapAngle(arrowAngle + Math.PI); + arrowAngle = remapAngle(arrowAngle + Math.PI); } const leftEdge = this.mapToScreenWidth(arrowAngle) - this.primeArrowSize; this.primeArrow.style.marginLeft = this.NaNCheck(leftEdge, 0) + 'px'; @@ -668,18 +663,17 @@ class Cgaz { } } - let velocityAngle = this.remapAngle(viewAngle - velAngle); + let velocityAngle = remapAngle(viewAngle - velAngle); // compass if (this.compassMode) { const ticks = this.tickContainer.Children(); - const bShouldHighlight = - Math.abs(this.remapAngle(8 * velAngle) * 0.125) < 0.01 && speed >= this.accelMinSpeed; + const bShouldHighlight = Math.abs(remapAngle(8 * velAngle) * 0.125) < 0.01 && speed >= this.accelMinSpeed; const color = bShouldHighlight ? this.compassHlColor : this.compassColor; // ticks for (const [i, tick] of ticks.entries()) { - const tickAngle = this.NaNCheck(this.wrapToHalfPi(viewAngle + i * 0.25 * Math.PI), 0); + const tickAngle = this.NaNCheck(wrapToHalfPi(viewAngle + i * 0.25 * Math.PI), 0); const tickPx = this.NaNCheck(this.mapToScreenWidth(tickAngle), 0); tick.style.position = `${tickPx}px 0px 0px`; tick.style.backgroundColor = color; @@ -692,11 +686,11 @@ class Cgaz { } else { this.compassArrowIcon.RemoveClass('arrow__up'); this.compassArrowIcon.AddClass('arrow__down'); - velocityAngle = this.remapAngle(velocityAngle - Math.PI); + velocityAngle = remapAngle(velocityAngle - Math.PI); } const leftEdge = this.mapToScreenWidth(velocityAngle) - this.compassArrowSize; this.compassArrow.style.marginLeft = this.NaNCheck(leftEdge, 0) + 'px'; - this.compassArrowIcon.style.washColor = this.getRgbFromRgba(color); + this.compassArrowIcon.style.washColor = getRgbFromRgba(color); } this.compassArrow.visible = this.compassMode % 2 && speed >= this.accelMinSpeed; this.tickContainer.visible = this.compassMode > 1; @@ -718,7 +712,7 @@ class Cgaz { if (this.compassStatMode) { this.yawStat.text = MomentumPlayerAPI.GetAngles().y.toFixed(0); this.yawStat.style.color = - Math.abs(this.remapAngle(8 * velAngle) * 0.125) < 0.01 && speed >= this.accelMinSpeed + Math.abs(remapAngle(8 * velAngle) * 0.125) < 0.01 && speed >= this.accelMinSpeed ? this.compassHlColor : this.compassColor; @@ -795,8 +789,8 @@ class Cgaz { for (let i = 0; i < points; ++i) angles.push( -breakPoints[i], - this.remapAngle(Math.PI * 0.5 - breakPoints[i]), - this.remapAngle(breakPoints[i] - Math.PI * 0.5) + remapAngle(Math.PI * 0.5 - breakPoints[i]), + remapAngle(breakPoints[i] - Math.PI * 0.5) ); return angles.sort((a, b) => a - b); @@ -834,8 +828,8 @@ class Cgaz { static updateZone(zone, left, right, offset, zoneClass, splitZone) { let wrap = right > left; - zone.leftAngle = this.remapAngle(left - offset); - zone.rightAngle = this.remapAngle(right - offset); + zone.leftAngle = remapAngle(left - offset); + zone.rightAngle = remapAngle(right - offset); wrap = zone.rightAngle > zone.leftAngle ? !wrap : wrap; @@ -865,8 +859,8 @@ class Cgaz { for (let i = 0; i < zones.length; ++i) { // wrap the angles to only [-pi/2, pi/2] - const left = this.wrapToHalfPi(this.snapAngles[i] - snapOffset); - const right = this.wrapToHalfPi(this.snapAngles[(i + 1) % zones.length] - snapOffset); + const left = wrapToHalfPi(this.snapAngles[i] - snapOffset); + const right = wrapToHalfPi(this.snapAngles[(i + 1) % zones.length] - snapOffset); const bUseUncolored = !this.snapHeightgainEnable && !this.snapColorMode && i % 2; let snapColor = bUseUncolored ? this.snapAltColor : this.snapColor; let hlSnapColor = bUseUncolored ? this.snapHlAltColor : this.snapHlColor; @@ -891,7 +885,7 @@ class Cgaz { this.updateZone(zones[i], left, right, 0, snapClass, this.snapSplitZone); if (this.snapColorMode) { - snapColor = this.colorLerp(this.snapSlowColor, this.snapFastColor, alpha); + snapColor = colorLerp(this.snapSlowColor, this.snapFastColor, alpha); } let bHighlight = false; @@ -904,10 +898,10 @@ class Cgaz { break; case 2: // "target" zones only highlight when moving - if (this.getSize(MomentumPlayerAPI.GetVelocity()) > this.accelMinSpeed) { + if (getSize(MomentumPlayerAPI.GetVelocity()) > this.accelMinSpeed) { let stopPoint, direction; if (left - leftTarget <= 0 && right - leftTarget >= 0) { - stopPoint = this.floatEquals(zones[i].rightPx, zones[i].leftPx, 1) + stopPoint = floatEquals(zones[i].rightPx, zones[i].leftPx, 1) ? 0 : this.NaNCheck( ( @@ -919,7 +913,7 @@ class Cgaz { direction = '0% 0%, 100% 0%'; bHighlight = true; } else if (left - rightTarget <= 0 && right - rightTarget >= 0) { - stopPoint = this.floatEquals(zones[i].rightPx, zones[i].leftPx, 1) + stopPoint = floatEquals(zones[i].rightPx, zones[i].leftPx, 1) ? 0 : this.NaNCheck( ( @@ -943,14 +937,13 @@ class Cgaz { } static updatePrimeSight(viewDir, viewAngle, targetAngle, boundaryAngle, velAngle, wishDir, wishAngle) { - const cross = this.getCross(wishDir, viewDir); + const cross = getCross(wishDir, viewDir); const inputMode = - Math.round(this.getSize(wishDir)) * (1 << Math.round(2 * Math.pow(cross, 2))) + - (Math.round(cross) > 0 ? 1 : 0); + Math.round(getSize(wishDir)) * (1 << Math.round(2 * Math.pow(cross, 2))) + (Math.round(cross) > 0 ? 1 : 0); - const angleOffset = this.remapAngle(velAngle - wishAngle); - const targetOffset = this.remapAngle(velAngle - viewAngle); - const inputAngle = this.remapAngle(viewAngle - wishAngle) * this.getSizeSquared(wishDir); + const angleOffset = remapAngle(velAngle - wishAngle); + const targetOffset = remapAngle(velAngle - viewAngle); + const inputAngle = remapAngle(viewAngle - wishAngle) * getSizeSquared(wishDir); const velocity = MomentumPlayerAPI.GetVelocity(); const gainZonesMap = new Map(); @@ -981,17 +974,17 @@ class Cgaz { } const leftOffset = -Math.PI * 0.25 - viewAngle; - const leftTarget = this.wrapToHalfPi(-targetAngle - velAngle); + const leftTarget = wrapToHalfPi(-targetAngle - velAngle); const leftAngles = fillLeftZones ? this.primeAngles : this.snapAngles; const rightOffset = Math.PI * 0.25 - viewAngle; - const rightTarget = this.wrapToHalfPi(targetAngle - velAngle); + const rightTarget = wrapToHalfPi(targetAngle - velAngle); const rightAngles = fillRightZones ? this.primeAngles : this.snapAngles; const iLeft = this.updateFirstPrimeZone(leftTarget, leftOffset, this.primeFirstZoneLeft, leftAngles); const iRight = this.updateFirstPrimeZone(rightTarget, rightOffset, this.primeFirstZoneRight, rightAngles); if (fillLeftZones || this.primeShowInactive) { - this.primeFirstZoneLeft.rightPx = this.mapToScreenWidth(this.wrapToHalfPi(leftTarget - leftOffset)); + this.primeFirstZoneLeft.rightPx = this.mapToScreenWidth(wrapToHalfPi(leftTarget - leftOffset)); this.primeFirstZoneLeft.style.backgroundColor = this.primeAltColor; this.drawZone(this.primeFirstZoneLeft); this.primeFirstZoneLeft.isInactive = !fillLeftZones; @@ -999,12 +992,12 @@ class Cgaz { this.clearZones([this.primeFirstZoneLeft]); } - speedGain = this.findPrimeGain(this.primeFirstZoneLeft, velocity, this.rotateVector(viewDir, 0.25 * Math.PI)); + speedGain = this.findPrimeGain(this.primeFirstZoneLeft, velocity, rotateVector(viewDir, 0.25 * Math.PI)); gainZonesMap.set(this.primeFirstZoneLeft, speedGain); if (speedGain > gainMax) gainMax = speedGain; if (fillRightZones || this.primeShowInactive) { - this.primeFirstZoneRight.leftPx = this.mapToScreenWidth(this.wrapToHalfPi(rightTarget - rightOffset)); + this.primeFirstZoneRight.leftPx = this.mapToScreenWidth(wrapToHalfPi(rightTarget - rightOffset)); this.primeFirstZoneRight.style.backgroundColor = this.primeAltColor; this.drawZone(this.primeFirstZoneRight); this.primeFirstZoneRight.isInactive = !fillRightZones; @@ -1012,12 +1005,12 @@ class Cgaz { this.clearZones([this.primeFirstZoneRight]); } - speedGain = this.findPrimeGain(this.primeFirstZoneRight, velocity, this.rotateVector(viewDir, -0.25 * Math.PI)); + speedGain = this.findPrimeGain(this.primeFirstZoneRight, velocity, rotateVector(viewDir, -0.25 * Math.PI)); gainZonesMap.set(this.primeFirstZoneRight, speedGain); if (speedGain > gainMax) gainMax = speedGain; if (fillLeftZones) { - const leftBoundary = this.wrapToHalfPi(-boundaryAngle - velAngle); + const leftBoundary = wrapToHalfPi(-boundaryAngle - velAngle); const jLeft = this.findArrayInfimum(this.primeAngles, leftBoundary); const zoneRange = { start: iLeft, @@ -1028,7 +1021,7 @@ class Cgaz { } if (fillRightZones) { - const rightBoundary = this.wrapToHalfPi(boundaryAngle - velAngle); + const rightBoundary = wrapToHalfPi(boundaryAngle - velAngle); const jRight = this.findArrayInfimum(this.primeAngles, rightBoundary); const zoneRange = { start: iRight, @@ -1054,7 +1047,7 @@ class Cgaz { if (gain < 0) { zone.color = this.primeLossColor; } else if (this.primeColorgainEnable) { - zone.color = this.colorLerp(this.primeAltColor, this.primeGainColor, gainFactor); + zone.color = colorLerp(this.primeAltColor, this.primeGainColor, gainFactor); } else { zone.color = this.primeGainColor; } @@ -1063,7 +1056,7 @@ class Cgaz { this.zoneCopy(this.primeHighlightZone, zone); this.drawZone(this.primeHighlightZone); this.primeHighlightZone.style.marginTop = (gain < 0 ? this.primeHeight : 0) + 'px'; - zone.color = this.enhanceAlpha(zone.color); + zone.color = enhanceAlpha(zone.color); } zone.style.backgroundColor = zone.color; @@ -1090,8 +1083,8 @@ class Cgaz { index = (index + zoneRange.direction + angleCount) % angleCount; zone = this.primeZones[index]; - const left = this.wrapToHalfPi(this.primeAngles[index] - offset); - const right = this.wrapToHalfPi(this.primeAngles[(index + 1) % angleCount] - offset); + const left = wrapToHalfPi(this.primeAngles[index] - offset); + const right = wrapToHalfPi(this.primeAngles[(index + 1) % angleCount] - offset); this.updateZone(zone, left, right, 0, PRIME_SIGHT_CLASS, this.primeSplitZone); const speedGain = this.findPrimeGain(zone, velocity, wishDir); gainZonesMap.set(zone, speedGain); @@ -1135,24 +1128,24 @@ class Cgaz { static findPrimeGain(zone, velocity, wishDir) { const avgAngle = 0.5 * (zone.leftAngle + zone.rightAngle); - const zoneVector = this.rotateVector(wishDir, -avgAngle); + const zoneVector = rotateVector(wishDir, -avgAngle); const snapProject = { x: Math.round(zoneVector.x * this.primeAccel), y: Math.round(zoneVector.y * this.primeAccel) }; - const newSpeed = this.getSize({ + const newSpeed = getSize({ x: Number(velocity.x) + Number(snapProject.x), y: Number(velocity.y) + Number(snapProject.y) }); - return newSpeed - this.getSize(velocity); + return newSpeed - getSize(velocity); } static updateFirstPrimeZone(target, offset, zone, angles) { const i = this.findArrayInfimum(angles, target); - const left = this.wrapToHalfPi(angles[i] - offset); - const right = this.wrapToHalfPi(angles[(i + 1) % angles.length] - offset); + const left = wrapToHalfPi(angles[i] - offset); + const right = wrapToHalfPi(angles[(i + 1) % angles.length] - offset); this.updateZone(zone, left, right, 0, PRIME_SIGHT_CLASS, this.primeSplitZone); return i; } @@ -1203,7 +1196,7 @@ class Cgaz { arrowIcon.style.height = this.NaNCheck(width, 0) + 'px'; arrowIcon.style.width = this.NaNCheck(width, 0) + 'px'; - arrowIcon.style.washColor = this.getRgbFromRgba(color); + arrowIcon.style.washColor = getRgbFromRgba(color); arrowIcon.style.overflow = 'noclip noclip'; arrowIcon.style.verticalAlign = align; } @@ -1251,98 +1244,10 @@ class Cgaz { } } - static wrapToHalfPi(angle) { - return Math.abs(angle) > Math.PI * 0.5 ? this.wrapToHalfPi(angle - Math.sign(angle) * Math.PI) : angle; - } - - static getSize(vec) { - return Math.sqrt(this.getSizeSquared(vec)); - } - - static getSizeSquared(vec) { - return vec.x * vec.x + vec.y * vec.y; - } - - static getNormal(vec, threshold) { - const mag = this.getSize(vec); - const vecNormal = { - x: vec.x, - y: vec.y - }; - if (mag < threshold * threshold) { - vecNormal.x = 0; - vecNormal.y = 0; - } else { - const inv = 1 / mag; - vecNormal.x *= inv; - vecNormal.y *= inv; - } - return vecNormal; - } - - static getDot(vec1, vec2) { - return vec1.x * vec2.x + vec1.y * vec2.y; - } - - static getCross(vec1, vec2) { - return vec1.x * vec2.y - vec1.y * vec2.x; - } - - static floatEquals(A, B, threshold) { - return Math.abs(A - B) < threshold; - } - static NaNCheck(val, def) { return Number.isNaN(Number(val)) ? def : val; } - // Converts [0, 2Pi) to [-Pi, Pi] - static remapAngle(angle) { - angle += Math.PI; - const integer = Math.trunc(angle / (2 * Math.PI)); - angle -= integer * 2 * Math.PI; - return angle < 0 ? angle + Math.PI : angle - Math.PI; - } - - static rotateVector(vector, angle) { - const cos = Math.cos(angle); - const sin = Math.sin(angle); - - return { - x: vector.x * cos - vector.y * sin, - y: vector.y * cos + vector.x * sin - }; - } - - static getColorStringFromArray(color) { - return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3] / 255})`; - } - - static splitColorString(string) { - return string - .slice(5, -1) - .split(',') - .map((c, i) => (i === 3 ? Number.parseInt(c * 255) : Number.parseInt(c))); - } - - static colorLerp(stringA, stringB, alpha) { - const arrayA = this.splitColorString(stringA); - const arrayB = this.splitColorString(stringB); - if (arrayA.length === 3) arrayA.push(255); - if (arrayB.length === 3) arrayB.push(255); - return this.getColorStringFromArray(arrayA.map((Ai, i) => Ai + alpha * (arrayB[i] - Ai))); - } - - static getRgbFromRgba(colorString) { - const [r, g, b] = this.splitColorString(colorString); - return `rgb(${r}, ${g}, ${b})`; - } - - static enhanceAlpha(colorString) { - const [r, g, b, a] = this.splitColorString(colorString); - return this.getColorStringFromArray([r, g, b, 0.25 * a + 192]); - } - static { $.RegisterForUnhandledEvent('ChaosLevelInitPostEntity', this.onLoad.bind(this)); $.RegisterForUnhandledEvent('OnDefragHUDProjectionChange', this.onProjectionChange.bind(this)); diff --git a/scripts/util/colors.js b/scripts/util/colors.js new file mode 100644 index 00000000..25ef04fe --- /dev/null +++ b/scripts/util/colors.js @@ -0,0 +1,71 @@ +/** + * Utility functions for Javascript. + * Could be ported to C++ and exposed globally in the future. + */ + +/** + * Returns a string formatted: + * rgba(R, G, B, A) + * from color array, where input array elements are range [0, 255]. + * R, G, B values ranged [0, 255], A ranged [0, 1]. + * @param {array} color + * @returns {string} + */ +function getColorStringFromArray(color) { + return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3] / 255})`; +} + +/** + * Returns a array of RGBA values ranged [0, 255]. + * Input string must be formatted: + * rgba(R, G, B, A) + * where R, G, B values ranged [0, 255], A ranged [0, 1]. + * @param {array} color + * @returns {string} + */ +function splitColorString(string) { + return string + .slice(5, -1) + .split(',') + .map((c, i) => (i === 3 ? Number.parseInt(c * 255) : Number.parseInt(c))); +} + +/** + * Blends two colors linearly (not HSV lerp). + * RGB inputs are converted to RGBA with alpha value of 1. + * @param {string} colorA + * @param {string} colorB + * @param {number} alpha + * @returns {string} + */ +function colorLerp(colorA, colorB, alpha) { + const arrayA = splitColorString(colorA); + const arrayB = splitColorString(colorB); + const interp = Math.max(Math.min(alpha, 1), 0); + if (arrayA.length === 3) arrayA.push(255); + if (arrayB.length === 3) arrayB.push(255); + return getColorStringFromArray(arrayA.map((Ai, i) => Ai + interp * (arrayB[i] - Ai))); +} + +/** + * Removes A value from string formatted: + * rgba(R, G, B, A) + * @param {string} colorString + * @returns {string} + */ +function getRgbFromRgba(colorString) { + const [r, g, b] = splitColorString(colorString); + return `rgb(${r}, ${g}, ${b})`; +} + +/** + * Compresses A value from string formatted: + * rgba(R, G, B, A) + * to the range [0.75, 1] + * @param {string} colorString + * @returns {string} + */ +function enhanceAlpha(colorString) { + const [r, g, b, a] = splitColorString(colorString); + return getColorStringFromArray([r, g, b, Math.min(0.25 * a + 192, 255)]); +} diff --git a/scripts/util/math.js b/scripts/util/math.js new file mode 100644 index 00000000..9b6b13d3 --- /dev/null +++ b/scripts/util/math.js @@ -0,0 +1,115 @@ +/** + * Utility functions for Javascript. + * Could be ported to C++ and exposed globally in the future. + */ + +/** + * 2D length of input vector + * @param {object} vec + * @returns {number} + */ +function getSize(vec) { + return Math.sqrt(getSizeSquared(vec)); +} + +/** + * 2D length squared of input vector + * @param {object} vec + * @returns {number} + */ +function getSizeSquared(vec) { + return vec.x * vec.x + vec.y * vec.y; +} + +/** + * Returns copy of input vector scaled to length 1. + * If input vector length is less than threshold, + * the zero vector is returned. + * @param {object} vec + * @returns {object} + */ +function getNormal(vec, threshold) { + const mag = getSize(vec); + const vecNormal = { + x: vec.x, + y: vec.y + }; + if (mag < threshold * threshold) { + vecNormal.x = 0; + vecNormal.y = 0; + } else { + const inv = 1 / mag; + vecNormal.x *= inv; + vecNormal.y *= inv; + } + return vecNormal; +} + +/** + * Dot product of two vectors. + * @param {object} vec1 + * @param {object} vec2 + * @returns {number} + */ +function getDot(vec1, vec2) { + return vec1.x * vec2.x + vec1.y * vec2.y; +} + +/** + * Cross product of two 2D vectors. + * Defined as Z-component of resultant vector. + * @param {object} vec1 + * @param {object} vec2 + * @returns {number} + */ +function getCross(vec1, vec2) { + return vec1.x * vec2.y - vec1.y * vec2.x; +} + +/** + * Rotate 2D vector anti-clockwise by specified angle (in radians). + * @param {object} vector + * @param {number} angle + * @returns {object} + */ +function rotateVector(vector, angle) { + const cos = Math.cos(angle); + const sin = Math.sin(angle); + + return { + x: vector.x * cos - vector.y * sin, + y: vector.y * cos + vector.x * sin + }; +} + +/** + * Float equals check with threshold. + * @param {number} A + * @param {number} B + * @param {number} threshold + * @returns {boolean} + */ +function floatEquals(A, B, threshold) { + return Math.abs(A - B) < threshold; +} + +/** + * Clamp angle to range [-Pi/2, Pi/2] by wrapping + * @param {number} angle + * @returns {number} + */ +function wrapToHalfPi(angle) { + return Math.abs(angle) > Math.PI * 0.5 ? wrapToHalfPi(angle - Math.sign(angle) * Math.PI) : angle; +} + +/** + * Converts [0, 2Pi) to [-Pi, Pi] + * @param {number} angle + * @returns {number} + */ +function remapAngle(angle) { + angle += Math.PI; + const integer = Math.trunc(angle / (2 * Math.PI)); + angle -= integer * 2 * Math.PI; + return angle < 0 ? angle + Math.PI : angle - Math.PI; +} From 692995aa72cf840f1efaa504267fe0e9a1f051f0 Mon Sep 17 00:00:00 2001 From: PeenScreeker Date: Fri, 15 Sep 2023 21:27:45 -0400 Subject: [PATCH 2/8] chore: convert range based for loops to for .. of loops --- scripts/hud/cgaz.js | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/scripts/hud/cgaz.js b/scripts/hud/cgaz.js index 71b849d9..8af809f3 100644 --- a/scripts/hud/cgaz.js +++ b/scripts/hud/cgaz.js @@ -211,8 +211,8 @@ class Cgaz { HIGHLIGHTED_ALT_SNAP_CLASS = new StyleObject(this.snapHeight, this.snapOffset, this.snapHlAltColor); this.setupContainer(this.snapContainer, this.snapOffset); - for (let i = 0; i < this.snapZones?.length; ++i) { - this.applyClass(this.snapZones[i], i % 2 ? UNCOLORED_SNAP_CLASS : COLORED_SNAP_CLASS); + for (const [i, snapZone] of this.snapZones.entries()) { + this.applyClass(snapZone, i % 2 ? UNCOLORED_SNAP_CLASS : COLORED_SNAP_CLASS); } } @@ -857,7 +857,7 @@ class Cgaz { const snapGains = this.findSnapGains(this.snapAngles); const zones = this.snapZones; - for (let i = 0; i < zones.length; ++i) { + for (const [i, zone] of zones.entries()) { // wrap the angles to only [-pi/2, pi/2] const left = wrapToHalfPi(this.snapAngles[i] - snapOffset); const right = wrapToHalfPi(this.snapAngles[(i + 1) % zones.length] - snapOffset); @@ -874,15 +874,15 @@ class Cgaz { const height = this.NaNCheck(this.snapHeight, 0); if (this.snapHeightgainEnable && !Number.isNaN(heightFactor)) { - zones[i].style.height = heightFactor * height + 'px'; - zones[i].style.marginBottom = height + 'px'; - zones[i].style.verticalAlign = 'bottom'; + zone.style.height = heightFactor * height + 'px'; + zone.style.marginBottom = height + 'px'; + zone.style.verticalAlign = 'bottom'; } else { - zones[i].style.height = height + 'px'; - zones[i].style.marginBottom = height + 'px'; + zone.style.height = height + 'px'; + zone.style.marginBottom = height + 'px'; } - this.updateZone(zones[i], left, right, 0, snapClass, this.snapSplitZone); + this.updateZone(zone, left, right, 0, snapClass, this.snapSplitZone); if (this.snapColorMode) { snapColor = colorLerp(this.snapSlowColor, this.snapFastColor, alpha); @@ -901,24 +901,24 @@ class Cgaz { if (getSize(MomentumPlayerAPI.GetVelocity()) > this.accelMinSpeed) { let stopPoint, direction; if (left - leftTarget <= 0 && right - leftTarget >= 0) { - stopPoint = floatEquals(zones[i].rightPx, zones[i].leftPx, 1) + stopPoint = floatEquals(zone.rightPx, zone.leftPx, 1) ? 0 : this.NaNCheck( ( - (this.mapToScreenWidth(leftTarget) - zones[i].leftPx) / - (zones[i].rightPx - zones[i].leftPx) + (this.mapToScreenWidth(leftTarget) - zone.leftPx) / + (zone.rightPx - zone.leftPx) ).toFixed(3), 0 ); direction = '0% 0%, 100% 0%'; bHighlight = true; } else if (left - rightTarget <= 0 && right - rightTarget >= 0) { - stopPoint = floatEquals(zones[i].rightPx, zones[i].leftPx, 1) + stopPoint = floatEquals(zone.rightPx, zone.leftPx, 1) ? 0 : this.NaNCheck( ( - (zones[i].rightPx - this.mapToScreenWidth(rightTarget)) / - (zones[i].rightPx - zones[i].leftPx) + (zone.rightPx - this.mapToScreenWidth(rightTarget)) / + (zone.rightPx - zone.leftPx) ).toFixed(3), 0 ); @@ -932,7 +932,7 @@ class Cgaz { break; } - zones[i].style.backgroundColor = bHighlight ? hlSnapColor : snapColor; + zone.style.backgroundColor = bHighlight ? hlSnapColor : snapColor; } } From f0177caf3d166771d4b175f89e12536d8a637589 Mon Sep 17 00:00:00 2001 From: PeenScreeker Date: Fri, 24 Nov 2023 23:56:59 -0500 Subject: [PATCH 3/8] chore: move screen mapping to utils --- scripts/hud/cgaz.js | 117 +++++++++++++++++++++++++------------------ scripts/util/math.js | 26 ++++++++++ 2 files changed, 93 insertions(+), 50 deletions(-) diff --git a/scripts/hud/cgaz.js b/scripts/hud/cgaz.js index 8af809f3..90d906b1 100644 --- a/scripts/hud/cgaz.js +++ b/scripts/hud/cgaz.js @@ -653,7 +653,9 @@ class Cgaz { this.compassArrowIcon.AddClass('arrow__down'); arrowAngle = remapAngle(arrowAngle + Math.PI); } - const leftEdge = this.mapToScreenWidth(arrowAngle) - this.primeArrowSize; + const leftEdge = + mapAngleToScreenDist(arrowAngle, this.hFov, this.screenX, this.scale, this.projection) - + this.primeArrowSize; this.primeArrow.style.marginLeft = this.NaNCheck(leftEdge, 0) + 'px'; this.primeArrow.visible = true; } else { @@ -674,7 +676,10 @@ class Cgaz { // ticks for (const [i, tick] of ticks.entries()) { const tickAngle = this.NaNCheck(wrapToHalfPi(viewAngle + i * 0.25 * Math.PI), 0); - const tickPx = this.NaNCheck(this.mapToScreenWidth(tickAngle), 0); + const tickPx = this.NaNCheck( + mapAngleToScreenDist(tickAngle, this.hFov, this.screenX, this.scale, this.projection), + 0 + ); tick.style.position = `${tickPx}px 0px 0px`; tick.style.backgroundColor = color; } @@ -688,7 +693,9 @@ class Cgaz { this.compassArrowIcon.AddClass('arrow__down'); velocityAngle = remapAngle(velocityAngle - Math.PI); } - const leftEdge = this.mapToScreenWidth(velocityAngle) - this.compassArrowSize; + const leftEdge = + mapAngleToScreenDist(velocityAngle, this.hFov, this.screenX, this.scale, this.projection) - + this.compassArrowSize; this.compassArrow.style.marginLeft = this.NaNCheck(leftEdge, 0) + 'px'; this.compassArrowIcon.style.washColor = getRgbFromRgba(color); } @@ -701,7 +708,13 @@ class Cgaz { for (let i = 0; i < pitchLines?.length; ++i) { const viewPitch = MomentumPlayerAPI.GetAngles().x; const pitchDelta = this.compassPitchTarget[i] - viewPitch; - const pitchDeltaPx = this.mapToScreenHeight((pitchDelta * Math.PI) / 180); + const pitchDeltaPx = mapAngleToScreenDist( + (pitchDelta * Math.PI) / 180, + this.vFov, + this.screenY, + this.scale, + this.projection + ); pitchLines[i].style.position = `0px ${this.NaNCheck(pitchDeltaPx, 0)}px 0px`; pitchLines[i].style.backgroundColor = Math.abs(pitchDelta) > 0.15 ? this.compassColor : this.compassHlColor; @@ -731,7 +744,9 @@ class Cgaz { // draw w-turn indicator if (this.windicatorEnable && Math.abs(wTurnAngle) < this.hFov && speed >= this.accelMinSpeed) { this.windicatorArrow.visible = true; - const leftEdge = this.mapToScreenWidth(wTurnAngle) - this.windicatorSize; + const leftEdge = + mapAngleToScreenDist(wTurnAngle, this.hFov, this.screenX, this.scale, this.projection) - + this.windicatorSize; this.windicatorArrow.style.marginLeft = this.NaNCheck(leftEdge, 0) + 'px'; const minAngle = Math.min(wTurnAngle, 0); @@ -834,21 +849,32 @@ class Cgaz { wrap = zone.rightAngle > zone.leftAngle ? !wrap : wrap; // map angles to screen - zone.leftPx = this.mapToScreenWidth(zone.leftAngle); - zone.rightPx = this.mapToScreenWidth(zone.rightAngle); + zone.leftPx = mapAngleToScreenDist(zone.leftAngle, this.hFov, this.screenX, this.scale, this.projection); + zone.rightPx = mapAngleToScreenDist(zone.rightAngle, this.hFov, this.screenX, this.scale, this.projection); if (wrap) { // draw second part of split zone this.applyClass(splitZone, zoneClass); splitZone.leftAngle = -this.hFov; splitZone.rightAngle = zone.rightAngle; - splitZone.leftPx = this.mapToScreenWidth(this.accelSplitZone.leftAngle); - splitZone.rightPx = this.mapToScreenWidth(this.accelSplitZone.rightAngle); - //this.drawZone(splitZone, left, this.mapToScreenWidth(this.hFov)); + splitZone.leftPx = mapAngleToScreenDist( + this.accelSplitZone.leftAngle, + this.hFov, + this.screenX, + this.scale, + this.projection + ); + splitZone.rightPx = mapAngleToScreenDist( + this.accelSplitZone.rightAngle, + this.hFov, + this.screenX, + this.scale, + this.projection + ); this.drawZone(splitZone); zone.rightAngle = this.hFov; - zone.rightPx = this.mapToScreenWidth(zone.rightAngle); + zone.rightPx = mapAngleToScreenDist(zone.rightAngle, this.hFov, this.screenX, this.scale, this.projection); } this.drawZone(zone); } @@ -905,7 +931,14 @@ class Cgaz { ? 0 : this.NaNCheck( ( - (this.mapToScreenWidth(leftTarget) - zone.leftPx) / + (mapAngleToScreenDist( + leftTarget, + this.hFov, + this.screenX, + this.scale, + this.projection + ) - + zone.leftPx) / (zone.rightPx - zone.leftPx) ).toFixed(3), 0 @@ -917,7 +950,14 @@ class Cgaz { ? 0 : this.NaNCheck( ( - (zone.rightPx - this.mapToScreenWidth(rightTarget)) / + (zone.rightPx - + mapAngleToScreenDist( + rightTarget, + this.hFov, + this.screenX, + this.scale, + this.projection + )) / (zone.rightPx - zone.leftPx) ).toFixed(3), 0 @@ -984,7 +1024,13 @@ class Cgaz { const iRight = this.updateFirstPrimeZone(rightTarget, rightOffset, this.primeFirstZoneRight, rightAngles); if (fillLeftZones || this.primeShowInactive) { - this.primeFirstZoneLeft.rightPx = this.mapToScreenWidth(wrapToHalfPi(leftTarget - leftOffset)); + this.primeFirstZoneLeft.rightPx = mapAngleToScreenDist( + wrapToHalfPi(leftTarget - leftOffset), + this.hFov, + this.screenX, + this.scale, + this.projection + ); this.primeFirstZoneLeft.style.backgroundColor = this.primeAltColor; this.drawZone(this.primeFirstZoneLeft); this.primeFirstZoneLeft.isInactive = !fillLeftZones; @@ -997,7 +1043,13 @@ class Cgaz { if (speedGain > gainMax) gainMax = speedGain; if (fillRightZones || this.primeShowInactive) { - this.primeFirstZoneRight.leftPx = this.mapToScreenWidth(wrapToHalfPi(rightTarget - rightOffset)); + this.primeFirstZoneRight.leftPx = mapAngleToScreenDist( + wrapToHalfPi(rightTarget - rightOffset), + this.hFov, + this.screenX, + this.scale, + this.projection + ); this.primeFirstZoneRight.style.backgroundColor = this.primeAltColor; this.drawZone(this.primeFirstZoneRight); this.primeFirstZoneRight.isInactive = !fillRightZones; @@ -1209,41 +1261,6 @@ class Cgaz { } } - static mapToScreenWidth(angle) { - const screenWidth = this.screenX / this.scale; - - const overhang = 1.1; - if (Math.abs(angle) >= overhang * this.hFov) { - return (Math.sign(angle) > 0 ? overhang : 1 - overhang) * screenWidth; - } - - switch (this.projection) { - case 0: - return Math.round((1 + Math.tan(angle) / Math.tan(this.hFov)) * screenWidth * 0.5); - case 1: - return Math.round((1 + angle / this.hFov) * screenWidth * 0.5); - case 2: - return Math.round((1 + Math.tan(angle * 0.5) / Math.tan(this.hFov * 0.5)) * screenWidth * 0.5); - } - } - - static mapToScreenHeight(angle) { - const screenHeight = this.screenY / this.scale; - - if (Math.abs(angle) >= this.vFov) { - return Math.sign(angle) > 0 ? screenHeight : 0; - } - - switch (this.projection) { - case 0: - return Math.round((1 + Math.tan(angle) / Math.tan(this.vFov)) * screenHeight * 0.5); - case 1: - return Math.round((1 + angle / this.vFov) * screenHeight * 0.5); - case 2: - return Math.round((1 + Math.tan(angle * 0.5) / Math.tan(this.vFov * 0.5)) * screenHeight * 0.5); - } - } - static NaNCheck(val, def) { return Number.isNaN(Number(val)) ? def : val; } diff --git a/scripts/util/math.js b/scripts/util/math.js index 9b6b13d3..8b493277 100644 --- a/scripts/util/math.js +++ b/scripts/util/math.js @@ -113,3 +113,29 @@ function remapAngle(angle) { angle -= integer * 2 * Math.PI; return angle < 0 ? angle + Math.PI : angle - Math.PI; } + +/** + * Convert an angle to a projected screen length in pixels. + * @param {number} angle + * @param {number} fov + * @param {number} distance + * @param {number} [scale=1] scale + * @param {number} [projection=0] projection + * @returns {number} + */ +function mapAngleToScreenDist(angle, fov, length, scale = 1, projection = 0) { + const screenDist = length / scale; + + if (Math.abs(angle) >= fov) { + return Math.sign(angle) > 0 ? screenDist + 1 : -1; + } + + switch (projection) { + case 0: + return Math.round((1 + Math.tan(angle) / Math.tan(fov)) * screenDist * 0.5); + case 1: + return Math.round((1 + angle / fov) * screenDist * 0.5); + case 2: + return Math.round((1 + Math.tan(angle * 0.5) / Math.tan(fov * 0.5)) * screenDist * 0.5); + } +} From a838c92025963dadaa13cdf0a0f2ca94a8d3a597 Mon Sep 17 00:00:00 2001 From: PeenScreeker Date: Mon, 27 Nov 2023 20:10:09 -0500 Subject: [PATCH 4/8] chore: use position instead of margin-left to place defrag hud panels --- scripts/hud/cgaz.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/scripts/hud/cgaz.js b/scripts/hud/cgaz.js index 90d906b1..cf267a09 100644 --- a/scripts/hud/cgaz.js +++ b/scripts/hud/cgaz.js @@ -656,7 +656,7 @@ class Cgaz { const leftEdge = mapAngleToScreenDist(arrowAngle, this.hFov, this.screenX, this.scale, this.projection) - this.primeArrowSize; - this.primeArrow.style.marginLeft = this.NaNCheck(leftEdge, 0) + 'px'; + this.primeArrow.style.position = `${this.NaNCheck(leftEdge, 0)}px 0px 0px`; this.primeArrow.visible = true; } else { this.primeArrow.visible = false; @@ -696,7 +696,7 @@ class Cgaz { const leftEdge = mapAngleToScreenDist(velocityAngle, this.hFov, this.screenX, this.scale, this.projection) - this.compassArrowSize; - this.compassArrow.style.marginLeft = this.NaNCheck(leftEdge, 0) + 'px'; + this.compassArrow.style.position = `${this.NaNCheck(leftEdge, 0)}px 0px 0px`; this.compassArrowIcon.style.washColor = getRgbFromRgba(color); } this.compassArrow.visible = this.compassMode % 2 && speed >= this.accelMinSpeed; @@ -747,7 +747,7 @@ class Cgaz { const leftEdge = mapAngleToScreenDist(wTurnAngle, this.hFov, this.screenX, this.scale, this.projection) - this.windicatorSize; - this.windicatorArrow.style.marginLeft = this.NaNCheck(leftEdge, 0) + 'px'; + this.windicatorArrow.style.position = `${this.NaNCheck(leftEdge, 0)}px 0px 0px`; const minAngle = Math.min(wTurnAngle, 0); const maxAngle = Math.max(wTurnAngle, 0); @@ -1207,8 +1207,8 @@ class Cgaz { const width = zone.rightPx - zone.leftPx; zone.style.width = this.NaNCheck(Number(width).toFixed(0), 0) + 'px'; - // assign position via margin (center screen at 0) - zone.style.marginLeft = this.NaNCheck(Number(zone.leftPx).toFixed(0), 0) + 'px'; + // assign position via position (center screen at 0) + zone.style.position = `${this.NaNCheck(Number(zone.leftPx).toFixed(0), 0)}px 0px 0px`; } static zoneCopy(pasteZone, copyZone) { From f21ca358c98e0d15b252205cffc32581376afd4d Mon Sep 17 00:00:00 2001 From: PeenScreeker Date: Wed, 29 Nov 2023 09:15:07 -0500 Subject: [PATCH 5/8] feat: convert colors.js to typescript --- scripts/hud/cgaz.js | 8 ++-- scripts/util/colors.js | 71 ----------------------------------- scripts/util/colors.ts | 84 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 75 deletions(-) delete mode 100644 scripts/util/colors.js create mode 100644 scripts/util/colors.ts diff --git a/scripts/hud/cgaz.js b/scripts/hud/cgaz.js index cf267a09..c292c771 100644 --- a/scripts/hud/cgaz.js +++ b/scripts/hud/cgaz.js @@ -697,7 +697,7 @@ class Cgaz { mapAngleToScreenDist(velocityAngle, this.hFov, this.screenX, this.scale, this.projection) - this.compassArrowSize; this.compassArrow.style.position = `${this.NaNCheck(leftEdge, 0)}px 0px 0px`; - this.compassArrowIcon.style.washColor = getRgbFromRgba(color); + this.compassArrowIcon.style.washColor = rgbaStringToRgb(color); } this.compassArrow.visible = this.compassMode % 2 && speed >= this.accelMinSpeed; this.tickContainer.visible = this.compassMode > 1; @@ -911,7 +911,7 @@ class Cgaz { this.updateZone(zone, left, right, 0, snapClass, this.snapSplitZone); if (this.snapColorMode) { - snapColor = colorLerp(this.snapSlowColor, this.snapFastColor, alpha); + snapColor = rgbaStringLerp(this.snapSlowColor, this.snapFastColor, alpha); } let bHighlight = false; @@ -1099,7 +1099,7 @@ class Cgaz { if (gain < 0) { zone.color = this.primeLossColor; } else if (this.primeColorgainEnable) { - zone.color = colorLerp(this.primeAltColor, this.primeGainColor, gainFactor); + zone.color = rgbaStringLerp(this.primeAltColor, this.primeGainColor, gainFactor); } else { zone.color = this.primeGainColor; } @@ -1248,7 +1248,7 @@ class Cgaz { arrowIcon.style.height = this.NaNCheck(width, 0) + 'px'; arrowIcon.style.width = this.NaNCheck(width, 0) + 'px'; - arrowIcon.style.washColor = getRgbFromRgba(color); + arrowIcon.style.washColor = rgbaStringToRgb(color); arrowIcon.style.overflow = 'noclip noclip'; arrowIcon.style.verticalAlign = align; } diff --git a/scripts/util/colors.js b/scripts/util/colors.js deleted file mode 100644 index 25ef04fe..00000000 --- a/scripts/util/colors.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Utility functions for Javascript. - * Could be ported to C++ and exposed globally in the future. - */ - -/** - * Returns a string formatted: - * rgba(R, G, B, A) - * from color array, where input array elements are range [0, 255]. - * R, G, B values ranged [0, 255], A ranged [0, 1]. - * @param {array} color - * @returns {string} - */ -function getColorStringFromArray(color) { - return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3] / 255})`; -} - -/** - * Returns a array of RGBA values ranged [0, 255]. - * Input string must be formatted: - * rgba(R, G, B, A) - * where R, G, B values ranged [0, 255], A ranged [0, 1]. - * @param {array} color - * @returns {string} - */ -function splitColorString(string) { - return string - .slice(5, -1) - .split(',') - .map((c, i) => (i === 3 ? Number.parseInt(c * 255) : Number.parseInt(c))); -} - -/** - * Blends two colors linearly (not HSV lerp). - * RGB inputs are converted to RGBA with alpha value of 1. - * @param {string} colorA - * @param {string} colorB - * @param {number} alpha - * @returns {string} - */ -function colorLerp(colorA, colorB, alpha) { - const arrayA = splitColorString(colorA); - const arrayB = splitColorString(colorB); - const interp = Math.max(Math.min(alpha, 1), 0); - if (arrayA.length === 3) arrayA.push(255); - if (arrayB.length === 3) arrayB.push(255); - return getColorStringFromArray(arrayA.map((Ai, i) => Ai + interp * (arrayB[i] - Ai))); -} - -/** - * Removes A value from string formatted: - * rgba(R, G, B, A) - * @param {string} colorString - * @returns {string} - */ -function getRgbFromRgba(colorString) { - const [r, g, b] = splitColorString(colorString); - return `rgb(${r}, ${g}, ${b})`; -} - -/** - * Compresses A value from string formatted: - * rgba(R, G, B, A) - * to the range [0.75, 1] - * @param {string} colorString - * @returns {string} - */ -function enhanceAlpha(colorString) { - const [r, g, b, a] = splitColorString(colorString); - return getColorStringFromArray([r, g, b, Math.min(0.25 * a + 192, 255)]); -} diff --git a/scripts/util/colors.ts b/scripts/util/colors.ts new file mode 100644 index 00000000..59e497e5 --- /dev/null +++ b/scripts/util/colors.ts @@ -0,0 +1,84 @@ +/** + * Functions for manipulating RGB/RGBA strings and tuples. + */ + +/** + * Tuple of R, G, B, A values ranged [0, 255] + */ +type RgbaTuple = [number, number, number, number]; + +/** + * Returns a string formatted `rgba(R, G, B, A)` from RGBA number tuple, where + * `R`, `G`, `B` are values ranged [0, 255], `A` ranged [0, 1]. + */ +function tupleToRgbaString([r, g, b, a = 255]: RgbaTuple): string { + return `rgba(${r}, ${g}, ${b}, ${a / 255})`; +} + +/** + * Returns a corresponding RGBA tuple for an RGB string. + * Input string must be formatted as `rgb(R, G, B)`, where `R`, `G`, `B` + * are values ranged `[0, 255]`. A is set to 255. + * + * For performance, this function does not check the input string. + */ + +function rgbStringToTuple(str: string): RgbaTuple { + return [ + ...str + .slice(4, -1) + .split(',') + .map((c) => Number.parseInt(c)), + 255 + ] as RgbaTuple; +} + +/** + * Returns a corresponding RGBA tuple for an RGBA string. + * Input string must be formatted as `rgb(R, G, B, A)`, where `R`, `G`, `B` + * are values ranged `[0, 255]`, `A` ranged `[0, 1]`. + * + * For performance, this function does not check the input string. + */ + +function rgbaStringToTuple(str: string): RgbaTuple { + if (str[3] !== 'a') return rgbStringToTuple(str); + + return str + .slice(5, -1) + .split(',') + .map((c, i) => (i === 3 ? Math.round(Number.parseFloat(c) * 255) : Number.parseInt(c))) as RgbaTuple; +} +/** + * Blends two colors linearly (not HSV lerp). + */ +function rgbaTupleLerp(colorA: RgbaTuple, colorB: RgbaTuple, alpha: number): RgbaTuple { + const interp = Math.max(Math.min(alpha, 1), 0); + return (colorA as number[]).map((Ai, i) => Math.round(Ai + interp * (colorB[i] - Ai))) as RgbaTuple; +} + +/** + * Blends two colors linearly (not HSV lerp). + * RGB inputs are converted to RGBA with A value of 1. + */ +function rgbaStringLerp(colorA: string, colorB: string, alpha: number): string { + const arrayA = rgbaStringToTuple(colorA); + const arrayB = rgbaStringToTuple(colorB); + return tupleToRgbaString(rgbaTupleLerp(arrayA, arrayB, alpha)); +} + +/** + * Removes A value from string formatted `rgba(R, G, B, A)`. + */ +function rgbaStringToRgb(str: string): string { + const [r, g, b] = rgbaStringToTuple(str); + return `rgb(${r}, ${g}, ${b})`; +} + +/** + * Compresses A value from string formatted `rgba(R, G, B, A)` to the range `[0.75, 1]` + */ +function enhanceAlpha(str: string): string { + const [r, g, b, a = 255] = rgbaStringToTuple(str); + return tupleToRgbaString([r, g, b, Math.min(0.25 * a + 192, 255)]); +} From cd89482dd28fe8490a373951fb405b6aa775b7af Mon Sep 17 00:00:00 2001 From: PeenScreeker Date: Thu, 30 Nov 2023 00:07:58 -0500 Subject: [PATCH 6/8] feat: convert math.js to typescript --- scripts/hud/cgaz.js | 31 +++++++------- scripts/util/{math.js => math.ts} | 70 ++++++++++++------------------- 2 files changed, 42 insertions(+), 59 deletions(-) rename scripts/util/{math.js => math.ts} (58%) diff --git a/scripts/hud/cgaz.js b/scripts/hud/cgaz.js index c292c771..a7201396 100644 --- a/scripts/hud/cgaz.js +++ b/scripts/hud/cgaz.js @@ -420,24 +420,24 @@ class Cgaz { } const velocity = MomentumPlayerAPI.GetVelocity(); - const speed = getSize(velocity); + const speed = getSize2D(velocity); const stopSpeed = Math.max(speed, MomentumMovementAPI.GetStopspeed()); const dropSpeed = Math.max(speed - stopSpeed * lastMoveData.friction * tickInterval, 0); const speedSquared = speed * speed; const dropSpeedSquared = dropSpeed * dropSpeed; - const velDir = getNormal(velocity, 0.001); + const velDir = getNormal2D(velocity, 0.001); const velAngle = Math.atan2(velocity.y, velocity.x); const wishDir = lastMoveData.wishdir; - const wishAngle = getSizeSquared(wishDir) > 0.001 ? Math.atan2(wishDir.y, wishDir.x) : 0; + const wishAngle = getSizeSquared2D(wishDir) > 0.001 ? Math.atan2(wishDir.y, wishDir.x) : 0; const viewAngle = (MomentumPlayerAPI.GetAngles().y * Math.PI) / 180; const viewDir = { x: Math.cos(viewAngle), y: Math.sin(viewAngle) }; - const forwardMove = Math.round(getDot(viewDir, wishDir)); - const rightMove = Math.round(getCross(viewDir, wishDir)); + const forwardMove = Math.round(getDot2D(viewDir, wishDir)); + const rightMove = Math.round(getCross2D(viewDir, wishDir)); const bIsFalling = lastMoveData.moveStatus === 0; const bHasAirControl = phyMode && floatEquals(wishAngle, viewAngle, 0.01) && bIsFalling; @@ -638,7 +638,7 @@ class Cgaz { // arrow if (this.primeArrowEnable) { - if (getSizeSquared(wishDir) > 0) { + if (getSizeSquared2D(wishDir) > 0) { let arrowAngle = wishAngle - Math.atan2( @@ -924,7 +924,7 @@ class Cgaz { break; case 2: // "target" zones only highlight when moving - if (getSize(MomentumPlayerAPI.GetVelocity()) > this.accelMinSpeed) { + if (getSize2D(MomentumPlayerAPI.GetVelocity()) > this.accelMinSpeed) { let stopPoint, direction; if (left - leftTarget <= 0 && right - leftTarget >= 0) { stopPoint = floatEquals(zone.rightPx, zone.leftPx, 1) @@ -977,13 +977,14 @@ class Cgaz { } static updatePrimeSight(viewDir, viewAngle, targetAngle, boundaryAngle, velAngle, wishDir, wishAngle) { - const cross = getCross(wishDir, viewDir); + const cross = getCross2D(wishDir, viewDir); const inputMode = - Math.round(getSize(wishDir)) * (1 << Math.round(2 * Math.pow(cross, 2))) + (Math.round(cross) > 0 ? 1 : 0); + Math.round(getSize2D(wishDir)) * (1 << Math.round(2 * Math.pow(cross, 2))) + + (Math.round(cross) > 0 ? 1 : 0); const angleOffset = remapAngle(velAngle - wishAngle); const targetOffset = remapAngle(velAngle - viewAngle); - const inputAngle = remapAngle(viewAngle - wishAngle) * getSizeSquared(wishDir); + const inputAngle = remapAngle(viewAngle - wishAngle) * getSizeSquared2D(wishDir); const velocity = MomentumPlayerAPI.GetVelocity(); const gainZonesMap = new Map(); @@ -1038,7 +1039,7 @@ class Cgaz { this.clearZones([this.primeFirstZoneLeft]); } - speedGain = this.findPrimeGain(this.primeFirstZoneLeft, velocity, rotateVector(viewDir, 0.25 * Math.PI)); + speedGain = this.findPrimeGain(this.primeFirstZoneLeft, velocity, rotateVector2D(viewDir, 0.25 * Math.PI)); gainZonesMap.set(this.primeFirstZoneLeft, speedGain); if (speedGain > gainMax) gainMax = speedGain; @@ -1057,7 +1058,7 @@ class Cgaz { this.clearZones([this.primeFirstZoneRight]); } - speedGain = this.findPrimeGain(this.primeFirstZoneRight, velocity, rotateVector(viewDir, -0.25 * Math.PI)); + speedGain = this.findPrimeGain(this.primeFirstZoneRight, velocity, rotateVector2D(viewDir, -0.25 * Math.PI)); gainZonesMap.set(this.primeFirstZoneRight, speedGain); if (speedGain > gainMax) gainMax = speedGain; @@ -1180,18 +1181,18 @@ class Cgaz { static findPrimeGain(zone, velocity, wishDir) { const avgAngle = 0.5 * (zone.leftAngle + zone.rightAngle); - const zoneVector = rotateVector(wishDir, -avgAngle); + const zoneVector = rotateVector2D(wishDir, -avgAngle); const snapProject = { x: Math.round(zoneVector.x * this.primeAccel), y: Math.round(zoneVector.y * this.primeAccel) }; - const newSpeed = getSize({ + const newSpeed = getSize2D({ x: Number(velocity.x) + Number(snapProject.x), y: Number(velocity.y) + Number(snapProject.y) }); - return newSpeed - getSize(velocity); + return newSpeed - getSize2D(velocity); } static updateFirstPrimeZone(target, offset, zone, angles) { diff --git a/scripts/util/math.js b/scripts/util/math.ts similarity index 58% rename from scripts/util/math.js rename to scripts/util/math.ts index 8b493277..f9096b09 100644 --- a/scripts/util/math.js +++ b/scripts/util/math.ts @@ -3,21 +3,28 @@ * Could be ported to C++ and exposed globally in the future. */ +/** + * 2D vector object with x and y member variables + */ +type Vec2D = { x: number; y: number }; + +/** + * 3D vector object with x, y, and z member variables + */ +type Vec3D = { x: number; y: number; z: number }; +type Vector = Vec2D | Vec3D; + /** * 2D length of input vector - * @param {object} vec - * @returns {number} */ -function getSize(vec) { - return Math.sqrt(getSizeSquared(vec)); +function getSize2D(vec: Vector): number { + return Math.sqrt(getSizeSquared2D(vec)); } /** * 2D length squared of input vector - * @param {object} vec - * @returns {number} */ -function getSizeSquared(vec) { +function getSizeSquared2D(vec: Vector): number { return vec.x * vec.x + vec.y * vec.y; } @@ -25,11 +32,9 @@ function getSizeSquared(vec) { * Returns copy of input vector scaled to length 1. * If input vector length is less than threshold, * the zero vector is returned. - * @param {object} vec - * @returns {object} */ -function getNormal(vec, threshold) { - const mag = getSize(vec); +function getNormal2D(vec: Vector, threshold: number): Vec2D { + const mag = getSize2D(vec); const vecNormal = { x: vec.x, y: vec.y @@ -47,67 +52,50 @@ function getNormal(vec, threshold) { /** * Dot product of two vectors. - * @param {object} vec1 - * @param {object} vec2 - * @returns {number} */ -function getDot(vec1, vec2) { +function getDot2D(vec1: Vector, vec2: Vector): number { return vec1.x * vec2.x + vec1.y * vec2.y; } /** * Cross product of two 2D vectors. * Defined as Z-component of resultant vector. - * @param {object} vec1 - * @param {object} vec2 - * @returns {number} */ -function getCross(vec1, vec2) { +function getCross2D(vec1: Vector, vec2: Vector): number { return vec1.x * vec2.y - vec1.y * vec2.x; } /** - * Rotate 2D vector anti-clockwise by specified angle (in radians). - * @param {object} vector - * @param {number} angle - * @returns {object} + * Returns 2D copy of input vector rotated anti-clockwise by specified angle (in radians). */ -function rotateVector(vector, angle) { +function rotateVector2D(vec: Vector, angle: number): Vec2D { const cos = Math.cos(angle); const sin = Math.sin(angle); return { - x: vector.x * cos - vector.y * sin, - y: vector.y * cos + vector.x * sin + x: vec.x * cos - vec.y * sin, + y: vec.y * cos + vec.x * sin }; } /** * Float equals check with threshold. - * @param {number} A - * @param {number} B - * @param {number} threshold - * @returns {boolean} */ -function floatEquals(A, B, threshold) { +function floatEquals(A: number, B: number, threshold: number): boolean { return Math.abs(A - B) < threshold; } /** * Clamp angle to range [-Pi/2, Pi/2] by wrapping - * @param {number} angle - * @returns {number} */ -function wrapToHalfPi(angle) { +function wrapToHalfPi(angle: number): number { return Math.abs(angle) > Math.PI * 0.5 ? wrapToHalfPi(angle - Math.sign(angle) * Math.PI) : angle; } /** * Converts [0, 2Pi) to [-Pi, Pi] - * @param {number} angle - * @returns {number} */ -function remapAngle(angle) { +function remapAngle(angle: number): number { angle += Math.PI; const integer = Math.trunc(angle / (2 * Math.PI)); angle -= integer * 2 * Math.PI; @@ -116,14 +104,8 @@ function remapAngle(angle) { /** * Convert an angle to a projected screen length in pixels. - * @param {number} angle - * @param {number} fov - * @param {number} distance - * @param {number} [scale=1] scale - * @param {number} [projection=0] projection - * @returns {number} */ -function mapAngleToScreenDist(angle, fov, length, scale = 1, projection = 0) { +function mapAngleToScreenDist(angle: number, fov: number, length: number, scale: number = 1, projection: number = 0) { const screenDist = length / scale; if (Math.abs(angle) >= fov) { From 054027752cef791f9c7a0627cdff0cbb41a52799 Mon Sep 17 00:00:00 2001 From: PeenScreeker Date: Fri, 1 Dec 2023 03:19:21 -0500 Subject: [PATCH 7/8] feat: update synchronizer to use util file functions --- layout/hud/synchronizer.xml | 3 +- scripts/hud/synchronizer.js | 83 +++++++------------------------------ 2 files changed, 17 insertions(+), 69 deletions(-) diff --git a/layout/hud/synchronizer.xml b/layout/hud/synchronizer.xml index 4bcc1e47..25bdb2d2 100644 --- a/layout/hud/synchronizer.xml +++ b/layout/hud/synchronizer.xml @@ -4,7 +4,8 @@ - + + diff --git a/scripts/hud/synchronizer.js b/scripts/hud/synchronizer.js index c8aa9749..2bf5128b 100644 --- a/scripts/hud/synchronizer.js +++ b/scripts/hud/synchronizer.js @@ -45,12 +45,12 @@ class Synchronizer { this.addToBuffer(this.gainRatioHistory, 0); this.addToBuffer(this.yawRatioHistory, 0); - const bValidWishMove = this.getSize(lastMoveData.wishdir) > 0.1; + const bValidWishMove = getSize2D(lastMoveData.wishdir) > 0.1; const strafeRight = (bValidWishMove ? 1 : 0) * lastTickStats.strafeRight; const direction = this.dynamicEnable === 1 ? strafeRight : 1; const flip = this.flipEnable === 1 ? -1 : 1; - if (bValidWishMove && this.getSizeSquared(MomentumPlayerAPI.GetVelocity()) > Math.pow(this.minSpeed, 2)) { + if (bValidWishMove && getSizeSquared2D(MomentumPlayerAPI.GetVelocity()) > Math.pow(this.minSpeed, 2)) { this.gainRatioHistory[this.interpFrames - 1] = this.sampleWeight * this.NaNCheck(lastTickStats.speedGain / lastTickStats.idealGain, 0); @@ -62,7 +62,7 @@ class Synchronizer { const yawRatio = this.getBufferedSum(this.yawRatioHistory); const colorTuple = this.colorEnable - ? this.getColorTuple(gainRatio, false) //strafeRight * yawRatio > 1) + ? this.getColorPair(gainRatio, false) //strafeRight * yawRatio > 1) : COLORS.NEUTRAL; const color = `gradient(linear, 0% 0%, 0% 100%, from(${colorTuple[0]}), to(${colorTuple[1]}))`; let flow; @@ -114,37 +114,34 @@ class Synchronizer { `(${(lastJumpStats.yawRatio * 100).toFixed(2)}%)`.padStart(10, ' '); this.panels.stats[1].text = (lastJumpStats.speedGain * 100).toFixed(2); - const colorTuple = this.StatColorEnable - ? this.getColorTuple(lastJumpStats.speedGain, lastJumpStats.yawRatio > 0) + const colorPair = this.StatColorEnable + ? this.getColorPair(lastJumpStats.speedGain, lastJumpStats.yawRatio > 0) : COLORS.NEUTRAL; - for (const stat of this.panels.stats) stat.style.color = colorTuple[1]; + for (const stat of this.panels.stats) stat.style.color = colorPair[1]; } - static getColorTuple(ratio, bOverStrafing) { + static getColorPair(ratio, bOverStrafing) { // cases where gain effectiveness is >90% if (ratio > 1.02) return COLORS.EXTRA; else if (ratio > 0.99) return COLORS.PERFECT; else if (ratio > 0.95) return COLORS.GOOD; else if (ratio <= -5) return COLORS.STOP; - const lerpColorTuples = (c1, c2, alpha) => { - return [ - this.lerpColorStrings(c1[0], c2[0], alpha.toFixed(3)), - this.lerpColorStrings(c1[1], c2[1], alpha.toFixed(3)) - ]; + const lerpColorPairs = (c1, c2, alpha) => { + return [rgbaStringLerp(c1[0], c2[0], alpha.toFixed(3)), rgbaStringLerp(c1[1], c2[1], alpha.toFixed(3))]; }; // cases where gain effectiveness is <90% if (!bOverStrafing) { - if (ratio > 0.85) return lerpColorTuples(COLORS.SLOW, COLORS.GOOD, (ratio - 0.85) / 0.1); + if (ratio > 0.85) return lerpColorPairs(COLORS.SLOW, COLORS.GOOD, (ratio - 0.85) / 0.1); else if (ratio > 0.75) return COLORS.SLOW; - else if (ratio > 0.5) return lerpColorTuples(COLORS.NEUTRAL, COLORS.SLOW, (ratio - 0.5) / 0.25); + else if (ratio > 0.5) return lerpColorPairs(COLORS.NEUTRAL, COLORS.SLOW, (ratio - 0.5) / 0.25); else if (ratio > 0) return COLORS.NEUTRAL; - else if (ratio > -5) return lerpColorTuples(COLORS.NEUTRAL, COLORS.STOP, Math.abs(ratio) / 5); + else if (ratio > -5) return lerpColorPairs(COLORS.NEUTRAL, COLORS.STOP, Math.abs(ratio) / 5); } else { - if (ratio > 0.8) return lerpColorTuples(COLORS.SLOW, COLORS.GOOD, (ratio - 0.8) / 0.15); - else if (ratio > 0) return lerpColorTuples(COLORS.LOSS, COLORS.SLOW, (ratio - 0.25) / 0.55); - else if (ratio > -5) return lerpColorTuples(COLORS.LOSS, COLORS.STOP, Math.abs(ratio) / 5); + if (ratio > 0.8) return lerpColorPairs(COLORS.SLOW, COLORS.GOOD, (ratio - 0.8) / 0.15); + else if (ratio > 0) return lerpColorPairs(COLORS.LOSS, COLORS.SLOW, (ratio - 0.25) / 0.55); + else if (ratio > -5) return lerpColorPairs(COLORS.LOSS, COLORS.STOP, Math.abs(ratio) / 5); } } @@ -170,35 +167,6 @@ class Synchronizer { return Math.acos(speed < threshold ? 1 : threshold / speed); } - static getSize(vec) { - return Math.sqrt(this.getSizeSquared(vec)); - } - - static getSizeSquared(vec) { - return vec.x * vec.x + vec.y * vec.y; - } - - static getNormal(vec, threshold) { - const mag = this.getSize(vec); - const vecNormal = { - x: vec.x, - y: vec.y - }; - if (mag < threshold * threshold) { - vecNormal.x = 0; - vecNormal.y = 0; - } else { - const inv = 1 / mag; - vecNormal.x *= inv; - vecNormal.y *= inv; - } - return vecNormal; - } - - static getCross(vec1, vec2) { - return vec1.x * vec2.y - vec1.y * vec2.x; - } - static initializeBuffer(size) { return Array.from({ length: size }).fill(0); } @@ -212,27 +180,6 @@ class Synchronizer { return history.reduce((sum, element) => sum + element, 0); } - static getColorStringFromArray(color) { - return `rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3] / 255})`; - } - - static splitColorString(string) { - return string - .slice(5, -1) - .split(',') - .map((c, i) => (i === 3 ? +c * 255 : +c)); - } - - static lerpColorStrings(stringA, stringB, alpha) { - const colorA = this.splitColorString(stringA); - const colorB = this.splitColorString(stringB); - return this.getColorStringFromArray(this.lerpColorArrays(colorA, colorB, alpha)); - } - - static lerpColorArrays(A, B, alpha) { - return A.map((Ai, i) => Ai + alpha * (B[i] - Ai)); - } - static setDisplayMode(newMode) { this.displayMode = newMode ?? 0; switch (this.displayMode) { From 05e475ce0725a2ac8fc00829fad6f22106d833c1 Mon Sep 17 00:00:00 2001 From: PeenScreeker Date: Fri, 1 Dec 2023 04:21:37 -0500 Subject: [PATCH 8/8] feat: HSV lerp for rgb colors --- scripts/util/colors.ts | 93 +++++++++++++++++++++++++++++++++++++----- 1 file changed, 82 insertions(+), 11 deletions(-) diff --git a/scripts/util/colors.ts b/scripts/util/colors.ts index 59e497e5..79f5a70e 100644 --- a/scripts/util/colors.ts +++ b/scripts/util/colors.ts @@ -22,7 +22,6 @@ function tupleToRgbaString([r, g, b, a = 255]: RgbaTuple): string { * * For performance, this function does not check the input string. */ - function rgbStringToTuple(str: string): RgbaTuple { return [ ...str @@ -37,10 +36,7 @@ function rgbStringToTuple(str: string): RgbaTuple { * Returns a corresponding RGBA tuple for an RGBA string. * Input string must be formatted as `rgb(R, G, B, A)`, where `R`, `G`, `B` * are values ranged `[0, 255]`, `A` ranged `[0, 1]`. - * - * For performance, this function does not check the input string. */ - function rgbaStringToTuple(str: string): RgbaTuple { if (str[3] !== 'a') return rgbStringToTuple(str); @@ -49,22 +45,97 @@ function rgbaStringToTuple(str: string): RgbaTuple { .split(',') .map((c, i) => (i === 3 ? Math.round(Number.parseFloat(c) * 255) : Number.parseInt(c))) as RgbaTuple; } + /** - * Blends two colors linearly (not HSV lerp). + * Blends two tuples linearly by alpha value. */ function rgbaTupleLerp(colorA: RgbaTuple, colorB: RgbaTuple, alpha: number): RgbaTuple { - const interp = Math.max(Math.min(alpha, 1), 0); + const interp: number = Math.max(Math.min(alpha, 1), 0); return (colorA as number[]).map((Ai, i) => Math.round(Ai + interp * (colorB[i] - Ai))) as RgbaTuple; } /** - * Blends two colors linearly (not HSV lerp). + * Blends two strings linearly by alpha. * RGB inputs are converted to RGBA with A value of 1. */ -function rgbaStringLerp(colorA: string, colorB: string, alpha: number): string { - const arrayA = rgbaStringToTuple(colorA); - const arrayB = rgbaStringToTuple(colorB); - return tupleToRgbaString(rgbaTupleLerp(arrayA, arrayB, alpha)); +function rgbaStringLerp(colorA: string, colorB: string, alpha: number, useHsv: boolean = false): string { + const arrayA: RgbaTuple = rgbaStringToTuple(colorA); + const arrayB: RgbaTuple = rgbaStringToTuple(colorB); + if (!useHsv) return tupleToRgbaString(rgbaTupleLerp(arrayA, arrayB, alpha)); + + const fromHsv: RgbaTuple = rgbaToHsva(arrayA) as RgbaTuple; + const toHsv: RgbaTuple = rgbaToHsva(arrayB) as RgbaTuple; + + // Take the shortest path to the new hue + if (Math.abs(fromHsv[0] - toHsv[0]) > 180) { + if (toHsv[0] > fromHsv[0]) { + fromHsv[0] += 360; + } else { + toHsv[0] += 360; + } + } + const newHsv = rgbaTupleLerp(fromHsv, toHsv, alpha); + + newHsv[0] = newHsv[0] % 360; + if (newHsv[0] < 0) { + newHsv[0] += 360; + } + + const newRgb: RgbaTuple = hsvaToRgba(newHsv) as RgbaTuple; + + return tupleToRgbaString(newRgb); +} + +/** + * Converts HSVA tuple to RGBA tuple + */ +function hsvaToRgba([h, s, v, a]: RgbaTuple): RgbaTuple { + const hueDir: number = h / 60; // divide color wheel into Red, Yellow, Green, Cyan, Blue, Magenta + const hueDirFloor: number = Math.floor(hueDir); // see which of the six regions holds the color to be converted + const hueDirFraction: number = hueDir - hueDirFloor; + + const rgbValues: RgbaTuple = [v, v * (1 - s), v * (1 - hueDirFraction * s), v * (1 - (1 - hueDirFraction) * s)]; + const rgbSwizzle: number[][] = [ + [0, 3, 1], + [2, 0, 1], + [1, 0, 3], + [1, 2, 0], + [3, 1, 0], + [0, 1, 2] + ]; + const swizzleIndex: number = hueDirFloor % 6; + + return [ + rgbValues[rgbSwizzle[swizzleIndex][0]] * 255, + rgbValues[rgbSwizzle[swizzleIndex][1]] * 255, + rgbValues[rgbSwizzle[swizzleIndex][2]] * 255, + a + ] as RgbaTuple; +} + +/** + * Converts RGBA tuple to HSVA tuple + */ +function rgbaToHsva([r, g, b, a]: RgbaTuple): RgbaTuple { + const rgbMin: number = Math.min(r, g, b); + const rgbMax: number = Math.max(r, g, b); + const rgbRange: number = rgbMax - rgbMin; + + const h: number = + rgbMax === rgbMin + ? 0 + : rgbMax === r + ? (((g - b) / rgbRange) * 60 + 360) % 360 + : rgbMax === g + ? ((b - r) / rgbRange) * 60 + 120 + : rgbMax === b + ? ((r - g) / rgbRange) * 60 + 240 + : 0; + + const s: number = rgbMax === 0 ? 0 : rgbRange / rgbMax; + const v: number = rgbMax / 255; + + return [h, s, v, a] as RgbaTuple; } /**