12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016 |
- import { PointF2D } from "../../Common/DataObjects/PointF2D";
- import { GraphicalNote } from "./GraphicalNote";
- import { GraphicalCurve } from "./GraphicalCurve";
- import { Slur } from "../VoiceData/Expressions/ContinuousExpressions/Slur";
- import { PlacementEnum } from "../VoiceData/Expressions/AbstractExpression";
- import { EngravingRules } from "./EngravingRules";
- import { StaffLine } from "./StaffLine";
- import { SkyBottomLineCalculator } from "./SkyBottomLineCalculator";
- import { Matrix2D } from "../../Common/DataObjects/Matrix2D";
- import { LinkedVoice } from "../VoiceData/LinkedVoice";
- import { GraphicalVoiceEntry } from "./GraphicalVoiceEntry";
- import { GraphicalStaffEntry } from "./GraphicalStaffEntry";
- import { Fraction } from "../../Common/DataObjects/Fraction";
- import { StemDirectionType } from "../VoiceData/VoiceEntry";
- import { VexFlowGraphicalNote } from "./VexFlow";
- import Vex from "vexflow";
- import VF = Vex.Flow;
- export class GraphicalSlur extends GraphicalCurve {
- // private intersection: PointF2D;
- constructor(slur: Slur, rules: EngravingRules) {
- super();
- this.slur = slur;
- this.rules = rules;
- }
- public slur: Slur;
- public staffEntries: GraphicalStaffEntry[] = [];
- public placement: PlacementEnum;
- public graceStart: boolean;
- public graceEnd: boolean;
- private rules: EngravingRules;
- public SVGElement: Node;
- /**
- * Compares the timespan of two Graphical Slurs
- * @param x
- * @param y
- */
- public static Compare (x: GraphicalSlur, y: GraphicalSlur ): number {
- if (x.staffEntries.length < 1) { // x.staffEntries[i] can return undefined in Beethoven Moonlight Sonata sample
- return -1;
- } else if (y.staffEntries.length < 1) {
- return 1;
- }
- const xTimestampSpan: Fraction = Fraction.minus(x.staffEntries[x.staffEntries.length - 1].getAbsoluteTimestamp(),
- x.staffEntries[0].getAbsoluteTimestamp());
- const yTimestampSpan: Fraction = Fraction.minus(y.staffEntries[y.staffEntries.length - 1].getAbsoluteTimestamp(),
- y.staffEntries[0].getAbsoluteTimestamp());
- if (xTimestampSpan.RealValue > yTimestampSpan.RealValue) {
- return 1;
- }
- if (yTimestampSpan.RealValue > xTimestampSpan.RealValue) {
- return -1;
- }
- return 0;
- }
- /**
- *
- * @param rules
- */
- public calculateCurve(rules: EngravingRules): void {
- // single GraphicalSlur means a single Curve, eg each GraphicalSlurObject is meant to be on the same StaffLine
- // a Slur can span more than one GraphicalSlurObjects
- const startStaffEntry: GraphicalStaffEntry = this.staffEntries[0];
- const endStaffEntry: GraphicalStaffEntry = this.staffEntries[this.staffEntries.length - 1];
- // where the Slur (not the graphicalObject) starts and ends (could belong to another StaffLine)
- let slurStartNote: GraphicalNote = startStaffEntry.findGraphicalNoteFromNote(this.slur.StartNote);
- if (!slurStartNote && this.graceStart) {
- slurStartNote = startStaffEntry.findGraphicalNoteFromGraceNote(this.slur.StartNote);
- }
- if (!slurStartNote) {
- slurStartNote = startStaffEntry.findEndTieGraphicalNoteFromNoteWithStartingSlur(this.slur.StartNote, this.slur);
- }
- let slurEndNote: GraphicalNote = endStaffEntry.findGraphicalNoteFromNote(this.slur.EndNote);
- if (!slurEndNote && this.graceEnd) {
- slurEndNote = endStaffEntry.findGraphicalNoteFromGraceNote(this.slur.EndNote);
- }
- const staffLine: StaffLine = startStaffEntry.parentMeasure.ParentStaffLine;
- const skyBottomLineCalculator: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;
- this.calculatePlacement(skyBottomLineCalculator, staffLine);
- // the Start- and End Reference Points for the Sky-BottomLine
- const startEndPoints: {startX: number, startY: number, endX: number, endY: number} =
- this.calculateStartAndEnd(slurStartNote, slurEndNote, staffLine, rules, skyBottomLineCalculator);
- const startX: number = startEndPoints.startX;
- const endX: number = startEndPoints.endX;
- let startY: number = startEndPoints.startY;
- let endY: number = startEndPoints.endY;
- const minAngle: number = rules.SlurTangentMinAngle;
- const maxAngle: number = rules.SlurTangentMaxAngle;
- let points: PointF2D[];
- if (this.placement === PlacementEnum.Above) {
- startY -= rules.SlurNoteHeadYOffset;
- endY -= rules.SlurNoteHeadYOffset;
- const startUpperRight: PointF2D = new PointF2D(this.staffEntries[0].parentMeasure.PositionAndShape.RelativePosition.x
- + this.staffEntries[0].PositionAndShape.RelativePosition.x,
- startY);
- if (slurStartNote) {
- startUpperRight.x += this.staffEntries[0].PositionAndShape.BorderRight;
- } else {
- // continuing Slur from previous StaffLine - must start after last Instruction of first Measure
- startUpperRight.x = this.staffEntries[0].parentMeasure.beginInstructionsWidth;
- }
- // must also add the GraceStaffEntry's ParentStaffEntry Position
- if (this.graceStart) {
- startUpperRight.x += endStaffEntry.PositionAndShape.RelativePosition.x;
- }
- const endUpperLeft: PointF2D = new PointF2D(this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.RelativePosition.x
- + this.staffEntries[this.staffEntries.length - 1].PositionAndShape.RelativePosition.x,
- endY);
- if (slurEndNote) {
- endUpperLeft.x += this.staffEntries[this.staffEntries.length - 1].PositionAndShape.BorderLeft;
- } else {
- // Slur continues to next StaffLine - must reach the end of current StaffLine
- endUpperLeft.x = this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.RelativePosition.x
- + this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.Size.width;
- }
- // must also add the GraceStaffEntry's ParentStaffEntry Position
- if (this.graceEnd) {
- endUpperLeft.x += endStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
- }
- // SkyLinePointsList between firstStaffEntry startUpperRightPoint and lastStaffentry endUpperLeftPoint
- points = this.calculateTopPoints(startUpperRight, endUpperLeft, staffLine, skyBottomLineCalculator);
- if (points.length === 0) {
- const pointF: PointF2D = new PointF2D((endUpperLeft.x - startUpperRight.x) / 2 + startUpperRight.x,
- (endUpperLeft.y - startUpperRight.y) / 2 + startUpperRight.y);
- points.push(pointF);
- }
- // Angle between original x-Axis and Line from Start-Point to End-Point
- const startEndLineAngleRadians: number = (Math.atan((endY - startY) / (endX - startX)));
- // translate origin at Start (positiveY from Bottom to Top => change sign for Y)
- const start2: PointF2D = new PointF2D(0, 0);
- let end2: PointF2D = new PointF2D(endX - startX, -(endY - startY));
- // and Rotate at new Origin startEndLineAngle degrees
- // clockwise/counterclockwise Rotation
- // after Rotation end2.Y must be 0
- // Inverse of RotationMatrix = TransposeMatrix of RotationMatrix
- const rotationMatrix: Matrix2D = Matrix2D.getRotationMatrix(startEndLineAngleRadians);
- const transposeMatrix: Matrix2D = rotationMatrix.getTransposeMatrix();
- end2 = rotationMatrix.vectorMultiplication(end2);
- const transformedPoints: PointF2D[] = this.calculateTranslatedAndRotatedPointListAbove(points, startX, startY, rotationMatrix);
- // calculate tangent Lines maximum Slopes between StartPoint and EndPoint to all Points in SkyLine
- // and tangent Lines characteristica
- const startLineSlope: number = this.calculateMaxLeftSlope(transformedPoints, start2, end2);
- const endLineSlope: number = this.calculateMaxRightSlope(transformedPoints, start2, end2);
- const startLineD: number = start2.y - start2.x * startLineSlope;
- const endLineD: number = end2.y - end2.x * endLineSlope;
- // calculate IntersectionPoint of the 2 Lines
- // if same Slope, then Point.X between Start and End and Point.Y fixed
- const intersectionPoint: PointF2D = new PointF2D();
- let sameSlope: boolean = false;
- if (Math.abs(Math.abs(startLineSlope) - Math.abs(endLineSlope)) < 0.0001) {
- intersectionPoint.x = end2.x / 2;
- intersectionPoint.y = 0;
- sameSlope = true;
- } else {
- intersectionPoint.x = (endLineD - startLineD) / (startLineSlope - endLineSlope);
- intersectionPoint.y = startLineSlope * intersectionPoint.x + startLineD;
- }
- // calculate HeightWidthRatio between the MaxYpoint (from the points between StartPoint and EndPoint)
- // and the X-distance from StartPoint to EndPoint
- const heightWidthRatio: number = this.calculateHeightWidthRatio(end2.x, transformedPoints);
- // Shift start- or endPoint and corresponding controlPoint away from note, if needed:
- // e.g. if there is a close object creating a high slope, better shift it away to reduce the slope:
- // idea is to compare the half heightWidthRatio of the bounding box of the skyline points with the slope (which is also a ratio: k/1)
- // if the slope is greater than the half heightWidthRatio (which will 99% be the case),
- // then add a y-offset to reduce the slope to the same value as the half heightWidthRatio of the bounding box
- const startYOffset: number = 0;
- const endYOffset: number = 0;
- /*if (Math.abs(heightWidthRatio) > 0.001) {
- // 1. start side:
- const startSlopeRatio: number = Math.abs(startLineSlope / (heightWidthRatio * 2));
- const maxLeftYOffset: number = Math.abs(startLineSlope);
- startYOffset = Math.max(0, maxLeftYOffset * (Math.min(10, startSlopeRatio - 1) / 10));
- // slope has to be adapted now due to the y-offset:
- startLineSlope -= startYOffset;
- // 2. end side:
- const endSlopeRatio: number = Math.abs(endLineSlope / (heightWidthRatio * 2));
- const maxRightYOffset: number = Math.abs(endLineSlope);
- endYOffset = Math.max(0, maxRightYOffset * (Math.min(10, endSlopeRatio - 1) / 10));
- // slope has to be adapted now due to the y-offset:
- endLineSlope += endYOffset;
- }*/
- // calculate tangent Lines Angles
- // (using the calculated Slopes and the Ratio from the IntersectionPoint's distance to the MaxPoint in the SkyLine)
- let startAngle: number = minAngle;
- let endAngle: number = -minAngle;
- // if the calculated Slopes (start and end) are equal, then Angles have fixed values
- if (!sameSlope) {
- const result: {startAngle: number, endAngle: number} =
- this.calculateAngles(minAngle, startLineSlope, endLineSlope, maxAngle);
- startAngle = result.startAngle;
- endAngle = result.endAngle;
- }
- // calculate Curve's Control Points
- const controlPoints: {startControlPoint: PointF2D, endControlPoint: PointF2D} =
- this.calculateControlPoints(end2.x, startAngle, endAngle, transformedPoints, heightWidthRatio, startY, endY);
- let startControlPoint: PointF2D = controlPoints.startControlPoint;
- let endControlPoint: PointF2D = controlPoints.endControlPoint;
- // transform ControlPoints to original Coordinate System
- // (rotate back and translate back)
- startControlPoint = transposeMatrix.vectorMultiplication(startControlPoint);
- startControlPoint.x += startX;
- startControlPoint.y = -startControlPoint.y + startY;
- endControlPoint = transposeMatrix.vectorMultiplication(endControlPoint);
- endControlPoint.x += startX;
- endControlPoint.y = -endControlPoint.y + startY;
- // middleControlPoint.x = (startControlPoint.x + endControlPoint.x) / 2;
- // middleControlPoint.y = (startControlPoint.y + endControlPoint.y) / 2 + 1.0;
- /* for DEBUG only */
- // this.intersection = transposeMatrix.vectorMultiplication(intersectionPoint);
- // this.intersection.x += startX;
- // this.intersection.y = -this.intersection.y + startY;
- /* for DEBUG only */
- // set private members
- this.bezierStartPt = new PointF2D(startX, startY - startYOffset);
- this.bezierStartControlPt = new PointF2D(startControlPoint.x, startControlPoint.y - startYOffset);
- this.bezierEndControlPt = new PointF2D(endControlPoint.x, endControlPoint.y - endYOffset);
- this.bezierEndPt = new PointF2D(endX, endY - endYOffset);
- // calculate slur Curvepoints and update Skyline
- const length: number = staffLine.SkyLine.length;
- const startIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(this.bezierStartPt.x, length);
- const endIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(this.bezierEndPt.x, length);
- const distance: number = this.bezierEndPt.x - this.bezierStartPt.x;
- const samplingUnit: number = skyBottomLineCalculator.SamplingUnit;
- for (let i: number = startIndex; i < endIndex; i++) {
- // get the right distance ratio and index on the curve
- const diff: number = i / samplingUnit - this.bezierStartPt.x;
- const curvePoint: PointF2D = this.calculateCurvePointAtIndex(Math.abs(diff) / distance);
- // update left- and rightIndex for better accuracy
- let index: number = skyBottomLineCalculator.getLeftIndexForPointX(curvePoint.x, length);
- // update SkyLine with final slur curve:
- if (index >= startIndex) {
- staffLine.SkyLine[index] = Math.min(staffLine.SkyLine[index], curvePoint.y);
- }
- index++;
- if (index < length) {
- staffLine.SkyLine[index] = Math.min(staffLine.SkyLine[index], curvePoint.y);
- }
- }
- } else {
- startY += rules.SlurNoteHeadYOffset;
- endY += rules.SlurNoteHeadYOffset;
- // firstStaffEntry startLowerRightPoint and lastStaffentry endLowerLeftPoint
- const startLowerRight: PointF2D = new PointF2D(this.staffEntries[0].parentMeasure.PositionAndShape.RelativePosition.x
- + this.staffEntries[0].PositionAndShape.RelativePosition.x,
- startY);
- if (slurStartNote) {
- startLowerRight.x += this.staffEntries[0].PositionAndShape.BorderRight;
- } else {
- // continuing Slur from previous StaffLine - must start after last Instruction of first Measure
- startLowerRight.x = this.staffEntries[0].parentMeasure.beginInstructionsWidth;
- }
- // must also add the GraceStaffEntry's ParentStaffEntry Position
- if (this.graceStart) {
- startLowerRight.x += endStaffEntry.PositionAndShape.RelativePosition.x;
- }
- const endLowerLeft: PointF2D = new PointF2D(this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.RelativePosition.x
- + this.staffEntries[this.staffEntries.length - 1].PositionAndShape.RelativePosition.x,
- endY);
- if (slurEndNote) {
- endLowerLeft.x += this.staffEntries[this.staffEntries.length - 1].PositionAndShape.BorderLeft;
- } else {
- // Slur continues to next StaffLine - must reach the end of current StaffLine
- endLowerLeft.x = this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.RelativePosition.x
- + this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.Size.width;
- }
- // must also add the GraceStaffEntry's ParentStaffEntry Position
- if (this.graceEnd) {
- endLowerLeft.x += endStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
- }
- // BottomLinePointsList between firstStaffEntry startLowerRightPoint and lastStaffentry endLowerLeftPoint
- points = this.calculateBottomPoints(startLowerRight, endLowerLeft, staffLine, skyBottomLineCalculator);
- if (points.length === 0) {
- const pointF: PointF2D = new PointF2D((endLowerLeft.x - startLowerRight.x) / 2 + startLowerRight.x,
- (endLowerLeft.y - startLowerRight.y) / 2 + startLowerRight.y);
- points.push(pointF);
- }
- // Angle between original x-Axis and Line from Start-Point to End-Point
- const startEndLineAngleRadians: number = Math.atan((endY - startY) / (endX - startX));
- // translate origin at Start
- const start2: PointF2D = new PointF2D(0, 0);
- let end2: PointF2D = new PointF2D(endX - startX, endY - startY);
- // and Rotate at new Origin startEndLineAngle degrees
- // clockwise/counterclockwise Rotation
- // after Rotation end2.Y must be 0
- // Inverse of RotationMatrix = TransposeMatrix of RotationMatrix
- const rotationMatrix: Matrix2D = Matrix2D.getRotationMatrix(-startEndLineAngleRadians);
- const transposeMatrix: Matrix2D = rotationMatrix.getTransposeMatrix();
- end2 = rotationMatrix.vectorMultiplication(end2);
- const transformedPoints: PointF2D[] = this.calculateTranslatedAndRotatedPointListBelow(points, startX, startY, rotationMatrix);
- // calculate tangent Lines maximum Slopes between StartPoint and EndPoint to all Points in BottomLine
- // and tangent Lines characteristica
- const startLineSlope: number = this.calculateMaxLeftSlope(transformedPoints, start2, end2);
- const endLineSlope: number = this.calculateMaxRightSlope(transformedPoints, start2, end2);
- const startLineD: number = start2.y - start2.x * startLineSlope;
- const endLineD: number = end2.y - end2.x * endLineSlope;
- // calculate IntersectionPoint of the 2 Lines
- // if same Slope, then Point.X between Start and End and Point.Y fixed
- const intersectionPoint: PointF2D = new PointF2D();
- let sameSlope: boolean = false;
- if (Math.abs(Math.abs(startLineSlope) - Math.abs(endLineSlope)) < 0.0001) {
- intersectionPoint.x = end2.x / 2;
- intersectionPoint.y = 0;
- sameSlope = true;
- } else {
- intersectionPoint.x = (endLineD - startLineD) / (startLineSlope - endLineSlope);
- intersectionPoint.y = startLineSlope * intersectionPoint.x + startLineD;
- }
- // calculate HeightWidthRatio between the MaxYpoint (from the points between StartPoint and EndPoint)
- // and the X-distance from StartPoint to EndPoint
- const heightWidthRatio: number = this.calculateHeightWidthRatio(end2.x, transformedPoints);
- // Shift start- or endPoint and corresponding controlPoint away from note, if needed:
- // e.g. if there is a close object creating a high slope, better shift it away to reduce the slope:
- // idea is to compare the half heightWidthRatio of the bounding box of the skyline points with the slope (which is also a ratio: k/1)
- // if the slope is greater than the half heightWidthRatio (which will 99% be the case),
- // then add a y-offset to reduce the slope to the same value as the half heightWidthRatio of the bounding box
- const startYOffset: number = 0;
- const endYOffset: number = 0;
- /*if (Math.abs(heightWidthRatio) > 0.001) {
- // 1. start side:
- const startSlopeRatio: number = Math.abs(startLineSlope / (heightWidthRatio * 2));
- const maxLeftYOffset: number = Math.abs(startLineSlope);
- startYOffset = Math.max(0, maxLeftYOffset * (Math.min(10, startSlopeRatio - 1) / 10));
- // slope has to be adapted now due to the y-offset:
- startLineSlope -= startYOffset;
- // 2. end side:
- const endSlopeRatio: number = Math.abs(endLineSlope / (heightWidthRatio * 2));
- const maxRightYOffset: number = Math.abs(endLineSlope);
- endYOffset = Math.max(0, maxRightYOffset * (Math.min(10, endSlopeRatio - 1) / 10));
- // slope has to be adapted now due to the y-offset:
- endLineSlope += endYOffset;
- } */
- // calculate tangent Lines Angles
- // (using the calculated Slopes and the Ratio from the IntersectionPoint's distance to the MaxPoint in the SkyLine)
- let startAngle: number = minAngle;
- let endAngle: number = -minAngle;
- // if the calculated Slopes (start and end) are equal, then Angles have fixed values
- if (!sameSlope) {
- const result: {startAngle: number, endAngle: number} =
- this.calculateAngles(minAngle, startLineSlope, endLineSlope, maxAngle);
- startAngle = result.startAngle;
- endAngle = result.endAngle;
- }
- // calculate Curve's Control Points
- const controlPoints: {startControlPoint: PointF2D, endControlPoint: PointF2D} =
- this.calculateControlPoints(end2.x, startAngle, endAngle, transformedPoints, heightWidthRatio, startY, endY);
- let startControlPoint: PointF2D = controlPoints.startControlPoint;
- let endControlPoint: PointF2D = controlPoints.endControlPoint;
- // transform ControlPoints to original Coordinate System
- // (rotate back and translate back)
- startControlPoint = transposeMatrix.vectorMultiplication(startControlPoint);
- startControlPoint.x += startX;
- startControlPoint.y += startY;
- endControlPoint = transposeMatrix.vectorMultiplication(endControlPoint);
- endControlPoint.x += startX;
- endControlPoint.y += startY;
- // set private members
- this.bezierStartPt = new PointF2D(startX, startY + startYOffset);
- this.bezierStartControlPt = new PointF2D(startControlPoint.x, startControlPoint.y + startYOffset);
- this.bezierEndControlPt = new PointF2D(endControlPoint.x, endControlPoint.y + endYOffset);
- this.bezierEndPt = new PointF2D(endX, endY + endYOffset);
- /* for DEBUG only */
- // this.intersection = transposeMatrix.vectorMultiplication(intersectionPoint);
- // this.intersection.x += startX;
- // this.intersection.y += startY;
- /* for DEBUG only */
- // calculate CurvePoints
- const length: number = staffLine.BottomLine.length;
- const startIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(this.bezierStartPt.x, length);
- const endIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(this.bezierEndPt.x, length);
- const distance: number = this.bezierEndPt.x - this.bezierStartPt.x;
- const samplingUnit: number = skyBottomLineCalculator.SamplingUnit;
- for (let i: number = startIndex; i < endIndex; i++) {
- // get the right distance ratio and index on the curve
- const diff: number = i / samplingUnit - this.bezierStartPt.x;
- const curvePoint: PointF2D = this.calculateCurvePointAtIndex(Math.abs(diff) / distance);
- // update start- and endIndex for better accuracy
- let index: number = skyBottomLineCalculator.getLeftIndexForPointX(curvePoint.x, length);
- // update BottomLine with final slur curve:
- if (index >= startIndex) {
- staffLine.BottomLine[index] = Math.max(staffLine.BottomLine[index], curvePoint.y);
- }
- index++;
- if (index < length) {
- staffLine.BottomLine[index] = Math.max(staffLine.BottomLine[index], curvePoint.y);
- }
- }
- }
- }
- /**
- * This method calculates the Start and End Positions of the Slur Curve.
- * @param slurStartNote
- * @param slurEndNote
- * @param staffLine
- * @param startX
- * @param startY
- * @param endX
- * @param endY
- * @param rules
- * @param skyBottomLineCalculator
- */
- private calculateStartAndEnd( slurStartNote: GraphicalNote,
- slurEndNote: GraphicalNote,
- staffLine: StaffLine,
- rules: EngravingRules,
- skyBottomLineCalculator: SkyBottomLineCalculator): {startX: number, startY: number, endX: number, endY: number} {
- let startX: number = 0;
- let startY: number = 0;
- let endX: number = 0;
- let endY: number = 0;
- if (slurStartNote) {
- // must be relative to StaffLine
- startX = slurStartNote.PositionAndShape.RelativePosition.x + slurStartNote.parentVoiceEntry.parentStaffEntry.PositionAndShape.RelativePosition.x
- + slurStartNote.parentVoiceEntry.parentStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
- // If Slur starts on a Gracenote
- if (this.graceStart) {
- startX += slurStartNote.parentVoiceEntry.parentStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
- }
- //const first: GraphicalNote = slurStartNote.parentVoiceEntry.notes[0];
- // Determine Start/End Point coordinates with the VoiceEntry of the Start/EndNote of the slur
- const slurStartVE: GraphicalVoiceEntry = slurStartNote.parentVoiceEntry;
- if (this.placement === PlacementEnum.Above) {
- startY = slurStartVE.PositionAndShape.RelativePosition.y + slurStartVE.PositionAndShape.BorderTop;
- // for (const articulation of slurStartVE.parentVoiceEntry.Articulations) {
- // if (articulation.placement === PlacementEnum.Above) {
- // startY -= 1;
- // break;
- // }
- // }
- } else {
- startY = slurStartVE.PositionAndShape.RelativePosition.y + slurStartVE.PositionAndShape.BorderBottom;
- // for (const articulation of slurStartVE.parentVoiceEntry.Articulations) {
- // if (articulation.placement === PlacementEnum.Below) {
- // startY += 1;
- // break;
- // }
- // }
- }
- // If the stem points towards the starting point of the slur, shift the slur by a small amount to start (approximately) at the x-position
- // of the notehead. Note: an exact calculation using the position of the note is too complicate for the payoff
- if ( slurStartVE.parentVoiceEntry.StemDirection === StemDirectionType.Down && this.placement === PlacementEnum.Below ) {
- startX -= 0.5;
- }
- if (slurStartVE.parentVoiceEntry.StemDirection === StemDirectionType.Up && this.placement === PlacementEnum.Above) {
- startX += 0.5;
- }
- // if (first.NoteStem && first.NoteStem.Direction === StemEnum.StemUp && this.placement === PlacementEnum.Above) {
- // startX += first.NoteStem.PositionAndShape.RelativePosition.x;
- // startY = skyBottomLineCalculator.getSkyLineMinAtPoint(staffLine, startX);
- // } else {
- // const last: GraphicalNote = <GraphicalNote>slurStartNote[slurEndNote.parentVoiceEntry.notes.length - 1];
- // if (last.NoteStem && last.NoteStem.Direction === StemEnum.StemDown && this.placement === PlacementEnum.Below) {
- // startX += last.NoteStem.PositionAndShape.RelativePosition.x;
- // startY = skyBottomLineCalculator.getBottomLineMaxAtPoint(staffLine, startX);
- // } else {
- // }
- // }
- } else {
- startX = 0;
- }
- if (slurEndNote) {
- endX = slurEndNote.PositionAndShape.RelativePosition.x + slurEndNote.parentVoiceEntry.parentStaffEntry.PositionAndShape.RelativePosition.x
- + slurEndNote.parentVoiceEntry.parentStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
- // If Slur ends in a Gracenote
- if (this.graceEnd) {
- endX += slurEndNote.parentVoiceEntry.parentStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
- }
- const slurEndVE: GraphicalVoiceEntry = slurEndNote.parentVoiceEntry;
- // check for articulation -> shift end y (slur further outward)
- // this should not be necessary for the start note, and for accents (>) it's even counter productive there
- // TODO alternatively, we could fix the bounding box of the note to include the ornament, but that seems tricky
- let articulationPlacement: PlacementEnum; // whether there's an articulation and where
- for (const articulation of slurEndVE.parentVoiceEntry.Articulations) {
- if (articulation.placement === PlacementEnum.NotYetDefined) {
- for (const modifier of ((slurEndNote as VexFlowGraphicalNote).vfnote[0] as any).modifiers) {
- if (modifier.getCategory() === VF.Articulation.CATEGORY) {
- if (modifier.position === VF.Modifier.Position.ABOVE) {
- articulation.placement = PlacementEnum.Above;
- articulationPlacement = PlacementEnum.Above;
- } else if (modifier.position === VF.Modifier.Position.BELOW) {
- articulation.placement = PlacementEnum.Below;
- articulationPlacement = PlacementEnum.Below;
- }
- break;
- }
- }
- }
- }
- if (this.placement === PlacementEnum.Above) {
- endY = slurEndVE.PositionAndShape.RelativePosition.y + slurEndVE.PositionAndShape.BorderTop;
- if (articulationPlacement === PlacementEnum.Above) {
- endY -= this.rules.SlurEndArticulationYOffset;
- }
- } else {
- endY = slurEndVE.PositionAndShape.RelativePosition.y + slurEndVE.PositionAndShape.BorderBottom;
- if (articulationPlacement === PlacementEnum.Below) {
- endY += this.rules.SlurEndArticulationYOffset;
- }
- }
- // If the stem points towards the endpoint of the slur, shift the slur by a small amount to start (approximately) at the x-position
- // of the notehead. Note: an exact calculation using the position of the note is too complicate for the payoff
- if ( slurEndVE.parentVoiceEntry.StemDirection === StemDirectionType.Down && this.placement === PlacementEnum.Below ) {
- endX -= 0.5;
- }
- if (slurEndVE.parentVoiceEntry.StemDirection === StemDirectionType.Up && this.placement === PlacementEnum.Above) {
- endX += 0.5;
- }
- // const first: GraphicalNote = <GraphicalNote>slurEndNote.parentVoiceEntry.notes[0];
- // if (first.NoteStem && first.NoteStem.Direction === StemEnum.StemUp && this.placement === PlacementEnum.Above) {
- // endX += first.NoteStem.PositionAndShape.RelativePosition.x;
- // endY = skyBottomLineCalculator.getSkyLineMinAtPoint(staffLine, endX);
- // } else {
- // const last: GraphicalNote = <GraphicalNote>slurEndNote.parentVoiceEntry.notes[slurEndNote.parentVoiceEntry.notes.length - 1];
- // if (last.NoteStem && last.NoteStem.Direction === StemEnum.StemDown && this.placement === PlacementEnum.Below) {
- // endX += last.NoteStem.PositionAndShape.RelativePosition.x;
- // endY = skyBottomLineCalculator.getBottomLineMaxAtPoint(staffLine, endX);
- // } else {
- // if (this.placement === PlacementEnum.Above) {
- // const highestNote: GraphicalNote = last;
- // endY = highestNote.PositionAndShape.RelativePosition.y;
- // if (highestNote.NoteHead) {
- // endY += highestNote.NoteHead.PositionAndShape.BorderMarginTop;
- // } else { endY += highestNote.PositionAndShape.BorderTop; }
- // } else {
- // const lowestNote: GraphicalNote = first;
- // endY = lowestNote.parentVoiceEntry
- // lowestNote.PositionAndShape.RelativePosition.y;
- // if (lowestNote.NoteHead) {
- // endY += lowestNote.NoteHead.PositionAndShape.BorderMarginBottom;
- // } else { endY += lowestNote.PositionAndShape.BorderBottom; }
- // }
- // }
- // }
- } else {
- endX = staffLine.PositionAndShape.Size.width;
- }
- // if GraphicalSlur breaks over System, then the end/start of the curve is at the corresponding height with the known start/end
- if (!slurStartNote && !slurEndNote) {
- startY = -1.5;
- endY = -1.5;
- }
- if (!slurStartNote) {
- if (this.placement === PlacementEnum.Above) {
- startY = endY - 1;
- } else {
- startY = endY + 1;
- }
- }
- if (!slurEndNote) {
- if (this.placement === PlacementEnum.Above) {
- endY = startY - 1;
- } else {
- endY = startY + 1;
- }
- }
- // if two slurs start/end at the same GraphicalNote, then the second gets an offset
- if (this.slur.startNoteHasMoreStartingSlurs() && this.slur.isSlurLonger()) {
- if (this.placement === PlacementEnum.Above) {
- startY -= rules.SlursStartingAtSameStaffEntryYOffset;
- } else { startY += rules.SlursStartingAtSameStaffEntryYOffset; }
- }
- if (this.slur.endNoteHasMoreEndingSlurs() && this.slur.isSlurLonger()) {
- if (this.placement === PlacementEnum.Above) {
- endY -= rules.SlursStartingAtSameStaffEntryYOffset;
- } else { endY += rules.SlursStartingAtSameStaffEntryYOffset; }
- }
- if (this.placement === PlacementEnum.Above) {
- startY = Math.min(startY, 1.5);
- endY = Math.min(endY, 1.5);
- } else {
- startY = Math.max(startY, staffLine.StaffHeight - 1.5);
- endY = Math.max(endY, staffLine.StaffHeight - 1.5);
- }
- return {startX, startY, endX, endY};
- }
- /**
- * This method calculates the placement of the Curve.
- * @param skyBottomLineCalculator
- * @param staffLine
- */
- private calculatePlacement(skyBottomLineCalculator: SkyBottomLineCalculator, staffLine: StaffLine): void {
- // old version: when lyrics are given place above:
- // if ( !this.slur.StartNote.ParentVoiceEntry.LyricsEntries.isEmpty || (this.slur.EndNote
- // && !this.slur.EndNote.ParentVoiceEntry.LyricsEntries.isEmpty) ) {
- // this.placement = PlacementEnum.Above;
- // return;
- // }
- if (this.rules.SlurPlacementFromXML) {
- this.placement = this.slur.PlacementXml;
- return;
- }
- // if any StaffEntry belongs to a Measure with multiple Voices, than
- // if Slur's Start- or End-Note belongs to a LinkedVoice Below else Above
- for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) {
- const graphicalStaffEntry: GraphicalStaffEntry = this.staffEntries[idx];
- if (graphicalStaffEntry.parentMeasure.hasMultipleVoices()) {
- if (this.slur.StartNote.ParentVoiceEntry.ParentVoice instanceof LinkedVoice ||
- this.slur.EndNote.ParentVoiceEntry.ParentVoice instanceof LinkedVoice) {
- this.placement = PlacementEnum.Below;
- } else { this.placement = PlacementEnum.Above; }
- return;
- }
- }
- // when lyrics are given place above:
- for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) {
- const graphicalStaffEntry: GraphicalStaffEntry = this.staffEntries[idx];
- if (graphicalStaffEntry.LyricsEntries.length > 0) {
- this.placement = PlacementEnum.Above;
- return;
- }
- }
- const startStaffEntry: GraphicalStaffEntry = this.staffEntries[0];
- const endStaffEntry: GraphicalStaffEntry = this.staffEntries[this.staffEntries.length - 1];
- // single Voice, opposite to StemDirection
- // here should only be one voiceEntry, so we can take graphicalVoiceEntries[0]:
- const startStemDirection: StemDirectionType = startStaffEntry.graphicalVoiceEntries[0].parentVoiceEntry.StemDirection;
- const endStemDirection: StemDirectionType = endStaffEntry.graphicalVoiceEntries[0].parentVoiceEntry.StemDirection;
- if (startStemDirection ===
- endStemDirection) {
- this.placement = (startStemDirection === StemDirectionType.Up) ? PlacementEnum.Below : PlacementEnum.Above;
- } else {
- // Placement at the side with the minimum border
- let sX: number = startStaffEntry.PositionAndShape.BorderLeft + startStaffEntry.PositionAndShape.RelativePosition.x
- + startStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
- let eX: number = endStaffEntry.PositionAndShape.BorderRight + endStaffEntry.PositionAndShape.RelativePosition.x
- + endStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
- if (this.graceStart) {
- sX += endStaffEntry.PositionAndShape.RelativePosition.x;
- }
- if (this.graceEnd) {
- eX += endStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
- }
- // get SkyBottomLine borders
- const minAbove: number = skyBottomLineCalculator.getSkyLineMinInRange(sX, eX) * -1;
- const maxBelow: number = skyBottomLineCalculator.getBottomLineMaxInRange(sX, eX) - staffLine.StaffHeight;
- if (maxBelow > minAbove) {
- this.placement = PlacementEnum.Above;
- } else { this.placement = PlacementEnum.Below; }
- }
- }
- /**
- * This method calculates the Points between Start- and EndPoint (case above).
- * @param start
- * @param end
- * @param staffLine
- * @param skyBottomLineCalculator
- */
- private calculateTopPoints(start: PointF2D, end: PointF2D, staffLine: StaffLine, skyBottomLineCalculator: SkyBottomLineCalculator): PointF2D[] {
- const points: PointF2D[] = [];
- let startIndex: number = skyBottomLineCalculator.getRightIndexForPointX(start.x, staffLine.SkyLine.length);
- let endIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(end.x, staffLine.SkyLine.length);
- if (startIndex < 0) {
- startIndex = 0;
- }
- if (endIndex >= staffLine.SkyLine.length) {
- endIndex = staffLine.SkyLine.length - 1;
- }
- for (let i: number = startIndex; i < endIndex; i++) {
- const skylineValue: number = staffLine.SkyLine[i];
- // ignore default value (= 0) which is upper border of staffline
- if (skylineValue !== 0) {
- const point: PointF2D = new PointF2D((0.5 + i) / skyBottomLineCalculator.SamplingUnit, skylineValue);
- points.push(point);
- }
- }
- return points;
- }
- /**
- * This method calculates the Points between Start- and EndPoint (case below).
- * @param start
- * @param end
- * @param staffLine
- * @param skyBottomLineCalculator
- */
- private calculateBottomPoints(start: PointF2D, end: PointF2D, staffLine: StaffLine, skyBottomLineCalculator: SkyBottomLineCalculator): PointF2D[] {
- const points: PointF2D[] = [];
- // get BottomLine indices
- let startIndex: number = skyBottomLineCalculator.getRightIndexForPointX(start.x, staffLine.BottomLine.length);
- let endIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(end.x, staffLine.BottomLine.length);
- if (startIndex < 0) {
- startIndex = 0;
- }
- if (endIndex >= staffLine.BottomLine.length) {
- endIndex = staffLine.BottomLine.length - 1;
- }
- for (let i: number = startIndex; i < endIndex; i++) {
- const bottomLineValue: number = staffLine.BottomLine[i];
- // ignore default value (= 4) which is lower border of staffline
- if (bottomLineValue !== 0) {
- const point: PointF2D = new PointF2D((0.5 + i) / skyBottomLineCalculator.SamplingUnit, bottomLineValue);
- points.push(point);
- }
- }
- return points;
- }
- /**
- * This method calculates the maximum slope between StartPoint and BetweenPoints.
- * @param points
- * @param start
- * @param end
- */
- private calculateMaxLeftSlope(points: PointF2D[], start: PointF2D, end: PointF2D): number {
- let slope: number = -Number.MAX_VALUE;
- const x: number = start.x;
- const y: number = start.y;
- for (let i: number = 0; i < points.length; i++) {
- if (Math.abs(points[i].y - Number.MAX_VALUE) < 0.0001 || Math.abs(points[i].y - (-Number.MAX_VALUE)) < 0.0001) {
- continue;
- }
- slope = Math.max(slope, (points[i].y - y) / (points[i].x - x));
- }
- // in case all Points don't have a meaningful value or the slope between Start- and EndPoint is just bigger
- slope = Math.max(slope, Math.abs(end.y - y) / (end.x - x));
- //limit to 80 degrees
- slope = Math.min(slope, 5.6713);
- return slope;
- }
- /**
- * This method calculates the maximum slope between EndPoint and BetweenPoints.
- * @param points
- * @param start
- * @param end
- */
- private calculateMaxRightSlope(points: PointF2D[], start: PointF2D, end: PointF2D): number {
- let slope: number = Number.MAX_VALUE;
- const x: number = end.x;
- const y: number = end.y;
- for (let i: number = 0; i < points.length; i++) {
- if (Math.abs(points[i].y - Number.MAX_VALUE) < 0.0001 || Math.abs(points[i].y - (-Number.MAX_VALUE)) < 0.0001) {
- continue;
- }
- slope = Math.min(slope, (y - points[i].y) / (x - points[i].x));
- }
- // in case no Point has a meaningful value or the slope between Start- and EndPoint is just smaller
- slope = Math.min(slope, (y - start.y) / (x - start.x));
- //limit to 80 degrees
- slope = Math.max(slope, -5.6713);
- return slope;
- }
- /**
- * This method returns the maximum (meaningful) points.Y.
- * @param points
- */
- private getPointListMaxY(points: PointF2D[]): number {
- let max: number = -Number.MAX_VALUE;
- for (let idx: number = 0, len: number = points.length; idx < len; ++idx) {
- const point: PointF2D = points[idx];
- if (Math.abs(point.y - (-Number.MAX_VALUE)) < 0.0001 || Math.abs(point.y - Number.MAX_VALUE) < 0.0001) {
- continue;
- }
- max = Math.max(max, point.y);
- }
- return max;
- }
- /**
- * This method calculates the translated and rotated PointsList (case above).
- * @param points
- * @param startX
- * @param startY
- * @param rotationMatrix
- */
- private calculateTranslatedAndRotatedPointListAbove(points: PointF2D[], startX: number, startY: number, rotationMatrix: Matrix2D): PointF2D[] {
- const transformedPoints: PointF2D[] = [];
- for (let i: number = 0; i < points.length; i++) {
- if (Math.abs(points[i].y - Number.MAX_VALUE) < 0.0001 || Math.abs(points[i].y - (-Number.MAX_VALUE)) < 0.0001) {
- continue;
- }
- let point: PointF2D = new PointF2D(points[i].x - startX, -(points[i].y - startY));
- point = rotationMatrix.vectorMultiplication(point);
- transformedPoints.push(point);
- }
- return transformedPoints;
- }
- /**
- * This method calculates the translated and rotated PointsList (case below).
- * @param points
- * @param startX
- * @param startY
- * @param rotationMatrix
- */
- private calculateTranslatedAndRotatedPointListBelow(points: PointF2D[], startX: number, startY: number, rotationMatrix: Matrix2D): PointF2D[] {
- const transformedPoints: PointF2D[] = [];
- for (let i: number = 0; i < points.length; i++) {
- if (Math.abs(points[i].y - Number.MAX_VALUE) < 0.0001 || Math.abs(points[i].y - (-Number.MAX_VALUE)) < 0.0001) {
- continue;
- }
- let point: PointF2D = new PointF2D(points[i].x - startX, points[i].y - startY);
- point = rotationMatrix.vectorMultiplication(point);
- transformedPoints.push(point);
- }
- return transformedPoints;
- }
- /**
- * This method calculates the HeightWidthRatio between the MaxYpoint (from the points between StartPoint and EndPoint)
- * and the X-distance from StartPoint to EndPoint.
- * @param endX
- * @param points
- */
- private calculateHeightWidthRatio(endX: number, points: PointF2D[]): number {
- if (points.length === 0) {
- return 0;
- }
- // in case of negative points
- const max: number = Math.max(0, this.getPointListMaxY(points));
- return max / endX;
- }
- /**
- * This method calculates the 2 ControlPoints of the SlurCurve.
- * @param endX
- * @param startAngle
- * @param endAngle
- * @param points
- */
- private calculateControlPoints(endX: number, startAngle: number, endAngle: number,
- points: PointF2D[], heightWidthRatio: number,
- startY: number, endY: number
- ): { startControlPoint: PointF2D, endControlPoint: PointF2D } {
- let heightFactor: number = this.rules.SlurHeightFactor;
- let widthFlattenFactor: number = 1;
- const cutoffAngle: number = this.rules.SlurHeightFlattenLongSlursCutoffAngle;
- const cutoffWidth: number = this.rules.SlurHeightFlattenLongSlursCutoffWidth;
- // console.log("width: " + endX);
- if (startAngle > cutoffAngle && endX > cutoffWidth) { // steep and wide slurs
- // console.log("steep angle: " + startAngle);
- widthFlattenFactor += endX / 70 * this.rules.SlurHeightFlattenLongSlursFactorByWidth; // double flattening for width = 70, factorByWidth = 1
- widthFlattenFactor *= 1 + (startAngle / 30 * this.rules.SlurHeightFlattenLongSlursFactorByAngle); // flatten more for higher angles.
- // TODO use sin or cos instead of startAngle directly
- heightFactor /= widthFlattenFactor; // flatten long slurs more
- }
- // TODO also offer a widthFlattenFactor for smaller slurs?
- // debug:
- // const measureNumber: number = this.staffEntries[0].parentMeasure.MeasureNumber; // debug
- // if (measureNumber === 10) {
- // console.log("endX: " + endX);
- // console.log("widthFlattenFactor: " + widthFlattenFactor);
- // console.log("heightFactor: " + heightFactor);
- // console.log("startAngle: " + startAngle);
- // console.log("heightWidthRatio: " + heightWidthRatio);
- // }
- // calculate HeightWidthRatio between the MaxYpoint (from the points between StartPoint and EndPoint)
- // and the X-distance from StartPoint to EndPoint
- // use this HeightWidthRatio to get a "normalized" Factor (based on tested parameters)
- // this Factor denotes the Length of the TangentLine of the Curve (a proportion of the X-distance from StartPoint to EndPoint)
- // finally from this Length and the calculated Angles we get the coordinates of the Control Points
- const factorStart: number = Math.min(0.5, Math.max(0.1, 1.7 * startAngle / 80 * heightFactor * Math.pow(Math.max(heightWidthRatio, 0.05), 0.4)));
- const factorEnd: number = Math.min(0.5, Math.max(0.1, 1.7 * (-endAngle) / 80 * heightFactor * Math.pow(Math.max(heightWidthRatio, 0.05), 0.4)));
- const startControlPoint: PointF2D = new PointF2D();
- startControlPoint.x = endX * factorStart * Math.cos(startAngle * GraphicalSlur.degreesToRadiansFactor);
- startControlPoint.y = endX * factorStart * Math.sin(startAngle * GraphicalSlur.degreesToRadiansFactor);
- const endControlPoint: PointF2D = new PointF2D();
- endControlPoint.x = endX - (endX * factorEnd * Math.cos(endAngle * GraphicalSlur.degreesToRadiansFactor));
- endControlPoint.y = -(endX * factorEnd * Math.sin(endAngle * GraphicalSlur.degreesToRadiansFactor));
- //Soften the slur in a "brute-force" way
- let controlPointYDiff: number = startControlPoint.y - endControlPoint.y;
- while (this.rules.SlurMaximumYControlPointDistance &&
- Math.abs(controlPointYDiff) > this.rules.SlurMaximumYControlPointDistance) {
- if (controlPointYDiff < 0) {
- startControlPoint.y += 1;
- endControlPoint.y -= 1;
- } else {
- startControlPoint.y -= 1;
- endControlPoint.y += 1;
- }
- controlPointYDiff = startControlPoint.y - endControlPoint.y;
- }
- return {startControlPoint: startControlPoint, endControlPoint: endControlPoint};
- }
- /**
- * This method calculates the angles for the Curve's Tangent Lines.
- * @param leftAngle
- * @param rightAngle
- * @param startLineSlope
- * @param endLineSlope
- * @param maxAngle
- */
- private calculateAngles(minAngle: number, startLineSlope: number, endLineSlope: number, maxAngle: number):
- {startAngle: number, endAngle: number} {
- // calculate Angles from the calculated Slopes, adding also a given angle
- const angle: number = 20;
- let calculatedStartAngle: number = Math.atan(startLineSlope) / GraphicalSlur.degreesToRadiansFactor;
- if (startLineSlope > 0) {
- calculatedStartAngle += angle;
- } else {
- calculatedStartAngle -= angle;
- }
- let calculatedEndAngle: number = Math.atan(endLineSlope) / GraphicalSlur.degreesToRadiansFactor;
- if (endLineSlope < 0) {
- calculatedEndAngle -= angle;
- } else {
- calculatedEndAngle += angle;
- }
- // +/- 80 is the max/min allowed Angle
- const leftAngle: number = Math.min(Math.max(minAngle, calculatedStartAngle), maxAngle);
- const rightAngle: number = Math.max(Math.min(-minAngle, calculatedEndAngle), -maxAngle);
- return {"startAngle": leftAngle, "endAngle": rightAngle};
- }
- private static degreesToRadiansFactor: number = Math.PI / 180;
- }
|