GraphicalSlur.ts 52 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016
  1. import { PointF2D } from "../../Common/DataObjects/PointF2D";
  2. import { GraphicalNote } from "./GraphicalNote";
  3. import { GraphicalCurve } from "./GraphicalCurve";
  4. import { Slur } from "../VoiceData/Expressions/ContinuousExpressions/Slur";
  5. import { PlacementEnum } from "../VoiceData/Expressions/AbstractExpression";
  6. import { EngravingRules } from "./EngravingRules";
  7. import { StaffLine } from "./StaffLine";
  8. import { SkyBottomLineCalculator } from "./SkyBottomLineCalculator";
  9. import { Matrix2D } from "../../Common/DataObjects/Matrix2D";
  10. import { LinkedVoice } from "../VoiceData/LinkedVoice";
  11. import { GraphicalVoiceEntry } from "./GraphicalVoiceEntry";
  12. import { GraphicalStaffEntry } from "./GraphicalStaffEntry";
  13. import { Fraction } from "../../Common/DataObjects/Fraction";
  14. import { StemDirectionType } from "../VoiceData/VoiceEntry";
  15. import { VexFlowGraphicalNote } from "./VexFlow";
  16. import Vex from "vexflow";
  17. import VF = Vex.Flow;
  18. export class GraphicalSlur extends GraphicalCurve {
  19. // private intersection: PointF2D;
  20. constructor(slur: Slur, rules: EngravingRules) {
  21. super();
  22. this.slur = slur;
  23. this.rules = rules;
  24. }
  25. public slur: Slur;
  26. public staffEntries: GraphicalStaffEntry[] = [];
  27. public placement: PlacementEnum;
  28. public graceStart: boolean;
  29. public graceEnd: boolean;
  30. private rules: EngravingRules;
  31. public SVGElement: Node;
  32. /**
  33. * Compares the timespan of two Graphical Slurs
  34. * @param x
  35. * @param y
  36. */
  37. public static Compare (x: GraphicalSlur, y: GraphicalSlur ): number {
  38. if (x.staffEntries.length < 1) { // x.staffEntries[i] can return undefined in Beethoven Moonlight Sonata sample
  39. return -1;
  40. } else if (y.staffEntries.length < 1) {
  41. return 1;
  42. }
  43. const xTimestampSpan: Fraction = Fraction.minus(x.staffEntries[x.staffEntries.length - 1].getAbsoluteTimestamp(),
  44. x.staffEntries[0].getAbsoluteTimestamp());
  45. const yTimestampSpan: Fraction = Fraction.minus(y.staffEntries[y.staffEntries.length - 1].getAbsoluteTimestamp(),
  46. y.staffEntries[0].getAbsoluteTimestamp());
  47. if (xTimestampSpan.RealValue > yTimestampSpan.RealValue) {
  48. return 1;
  49. }
  50. if (yTimestampSpan.RealValue > xTimestampSpan.RealValue) {
  51. return -1;
  52. }
  53. return 0;
  54. }
  55. /**
  56. *
  57. * @param rules
  58. */
  59. public calculateCurve(rules: EngravingRules): void {
  60. // single GraphicalSlur means a single Curve, eg each GraphicalSlurObject is meant to be on the same StaffLine
  61. // a Slur can span more than one GraphicalSlurObjects
  62. const startStaffEntry: GraphicalStaffEntry = this.staffEntries[0];
  63. const endStaffEntry: GraphicalStaffEntry = this.staffEntries[this.staffEntries.length - 1];
  64. // where the Slur (not the graphicalObject) starts and ends (could belong to another StaffLine)
  65. let slurStartNote: GraphicalNote = startStaffEntry.findGraphicalNoteFromNote(this.slur.StartNote);
  66. if (!slurStartNote && this.graceStart) {
  67. slurStartNote = startStaffEntry.findGraphicalNoteFromGraceNote(this.slur.StartNote);
  68. }
  69. if (!slurStartNote) {
  70. slurStartNote = startStaffEntry.findEndTieGraphicalNoteFromNoteWithStartingSlur(this.slur.StartNote, this.slur);
  71. }
  72. let slurEndNote: GraphicalNote = endStaffEntry.findGraphicalNoteFromNote(this.slur.EndNote);
  73. if (!slurEndNote && this.graceEnd) {
  74. slurEndNote = endStaffEntry.findGraphicalNoteFromGraceNote(this.slur.EndNote);
  75. }
  76. const staffLine: StaffLine = startStaffEntry.parentMeasure.ParentStaffLine;
  77. const skyBottomLineCalculator: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;
  78. this.calculatePlacement(skyBottomLineCalculator, staffLine);
  79. // the Start- and End Reference Points for the Sky-BottomLine
  80. const startEndPoints: {startX: number, startY: number, endX: number, endY: number} =
  81. this.calculateStartAndEnd(slurStartNote, slurEndNote, staffLine, rules, skyBottomLineCalculator);
  82. const startX: number = startEndPoints.startX;
  83. const endX: number = startEndPoints.endX;
  84. let startY: number = startEndPoints.startY;
  85. let endY: number = startEndPoints.endY;
  86. const minAngle: number = rules.SlurTangentMinAngle;
  87. const maxAngle: number = rules.SlurTangentMaxAngle;
  88. let points: PointF2D[];
  89. if (this.placement === PlacementEnum.Above) {
  90. startY -= rules.SlurNoteHeadYOffset;
  91. endY -= rules.SlurNoteHeadYOffset;
  92. const startUpperRight: PointF2D = new PointF2D(this.staffEntries[0].parentMeasure.PositionAndShape.RelativePosition.x
  93. + this.staffEntries[0].PositionAndShape.RelativePosition.x,
  94. startY);
  95. if (slurStartNote) {
  96. startUpperRight.x += this.staffEntries[0].PositionAndShape.BorderRight;
  97. } else {
  98. // continuing Slur from previous StaffLine - must start after last Instruction of first Measure
  99. startUpperRight.x = this.staffEntries[0].parentMeasure.beginInstructionsWidth;
  100. }
  101. // must also add the GraceStaffEntry's ParentStaffEntry Position
  102. if (this.graceStart) {
  103. startUpperRight.x += endStaffEntry.PositionAndShape.RelativePosition.x;
  104. }
  105. const endUpperLeft: PointF2D = new PointF2D(this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.RelativePosition.x
  106. + this.staffEntries[this.staffEntries.length - 1].PositionAndShape.RelativePosition.x,
  107. endY);
  108. if (slurEndNote) {
  109. endUpperLeft.x += this.staffEntries[this.staffEntries.length - 1].PositionAndShape.BorderLeft;
  110. } else {
  111. // Slur continues to next StaffLine - must reach the end of current StaffLine
  112. endUpperLeft.x = this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.RelativePosition.x
  113. + this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.Size.width;
  114. }
  115. // must also add the GraceStaffEntry's ParentStaffEntry Position
  116. if (this.graceEnd) {
  117. endUpperLeft.x += endStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
  118. }
  119. // SkyLinePointsList between firstStaffEntry startUpperRightPoint and lastStaffentry endUpperLeftPoint
  120. points = this.calculateTopPoints(startUpperRight, endUpperLeft, staffLine, skyBottomLineCalculator);
  121. if (points.length === 0) {
  122. const pointF: PointF2D = new PointF2D((endUpperLeft.x - startUpperRight.x) / 2 + startUpperRight.x,
  123. (endUpperLeft.y - startUpperRight.y) / 2 + startUpperRight.y);
  124. points.push(pointF);
  125. }
  126. // Angle between original x-Axis and Line from Start-Point to End-Point
  127. const startEndLineAngleRadians: number = (Math.atan((endY - startY) / (endX - startX)));
  128. // translate origin at Start (positiveY from Bottom to Top => change sign for Y)
  129. const start2: PointF2D = new PointF2D(0, 0);
  130. let end2: PointF2D = new PointF2D(endX - startX, -(endY - startY));
  131. // and Rotate at new Origin startEndLineAngle degrees
  132. // clockwise/counterclockwise Rotation
  133. // after Rotation end2.Y must be 0
  134. // Inverse of RotationMatrix = TransposeMatrix of RotationMatrix
  135. const rotationMatrix: Matrix2D = Matrix2D.getRotationMatrix(startEndLineAngleRadians);
  136. const transposeMatrix: Matrix2D = rotationMatrix.getTransposeMatrix();
  137. end2 = rotationMatrix.vectorMultiplication(end2);
  138. const transformedPoints: PointF2D[] = this.calculateTranslatedAndRotatedPointListAbove(points, startX, startY, rotationMatrix);
  139. // calculate tangent Lines maximum Slopes between StartPoint and EndPoint to all Points in SkyLine
  140. // and tangent Lines characteristica
  141. const startLineSlope: number = this.calculateMaxLeftSlope(transformedPoints, start2, end2);
  142. const endLineSlope: number = this.calculateMaxRightSlope(transformedPoints, start2, end2);
  143. const startLineD: number = start2.y - start2.x * startLineSlope;
  144. const endLineD: number = end2.y - end2.x * endLineSlope;
  145. // calculate IntersectionPoint of the 2 Lines
  146. // if same Slope, then Point.X between Start and End and Point.Y fixed
  147. const intersectionPoint: PointF2D = new PointF2D();
  148. let sameSlope: boolean = false;
  149. if (Math.abs(Math.abs(startLineSlope) - Math.abs(endLineSlope)) < 0.0001) {
  150. intersectionPoint.x = end2.x / 2;
  151. intersectionPoint.y = 0;
  152. sameSlope = true;
  153. } else {
  154. intersectionPoint.x = (endLineD - startLineD) / (startLineSlope - endLineSlope);
  155. intersectionPoint.y = startLineSlope * intersectionPoint.x + startLineD;
  156. }
  157. // calculate HeightWidthRatio between the MaxYpoint (from the points between StartPoint and EndPoint)
  158. // and the X-distance from StartPoint to EndPoint
  159. const heightWidthRatio: number = this.calculateHeightWidthRatio(end2.x, transformedPoints);
  160. // Shift start- or endPoint and corresponding controlPoint away from note, if needed:
  161. // e.g. if there is a close object creating a high slope, better shift it away to reduce the slope:
  162. // 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)
  163. // if the slope is greater than the half heightWidthRatio (which will 99% be the case),
  164. // then add a y-offset to reduce the slope to the same value as the half heightWidthRatio of the bounding box
  165. const startYOffset: number = 0;
  166. const endYOffset: number = 0;
  167. /*if (Math.abs(heightWidthRatio) > 0.001) {
  168. // 1. start side:
  169. const startSlopeRatio: number = Math.abs(startLineSlope / (heightWidthRatio * 2));
  170. const maxLeftYOffset: number = Math.abs(startLineSlope);
  171. startYOffset = Math.max(0, maxLeftYOffset * (Math.min(10, startSlopeRatio - 1) / 10));
  172. // slope has to be adapted now due to the y-offset:
  173. startLineSlope -= startYOffset;
  174. // 2. end side:
  175. const endSlopeRatio: number = Math.abs(endLineSlope / (heightWidthRatio * 2));
  176. const maxRightYOffset: number = Math.abs(endLineSlope);
  177. endYOffset = Math.max(0, maxRightYOffset * (Math.min(10, endSlopeRatio - 1) / 10));
  178. // slope has to be adapted now due to the y-offset:
  179. endLineSlope += endYOffset;
  180. }*/
  181. // calculate tangent Lines Angles
  182. // (using the calculated Slopes and the Ratio from the IntersectionPoint's distance to the MaxPoint in the SkyLine)
  183. let startAngle: number = minAngle;
  184. let endAngle: number = -minAngle;
  185. // if the calculated Slopes (start and end) are equal, then Angles have fixed values
  186. if (!sameSlope) {
  187. const result: {startAngle: number, endAngle: number} =
  188. this.calculateAngles(minAngle, startLineSlope, endLineSlope, maxAngle);
  189. startAngle = result.startAngle;
  190. endAngle = result.endAngle;
  191. }
  192. // calculate Curve's Control Points
  193. const controlPoints: {startControlPoint: PointF2D, endControlPoint: PointF2D} =
  194. this.calculateControlPoints(end2.x, startAngle, endAngle, transformedPoints, heightWidthRatio, startY, endY);
  195. let startControlPoint: PointF2D = controlPoints.startControlPoint;
  196. let endControlPoint: PointF2D = controlPoints.endControlPoint;
  197. // transform ControlPoints to original Coordinate System
  198. // (rotate back and translate back)
  199. startControlPoint = transposeMatrix.vectorMultiplication(startControlPoint);
  200. startControlPoint.x += startX;
  201. startControlPoint.y = -startControlPoint.y + startY;
  202. endControlPoint = transposeMatrix.vectorMultiplication(endControlPoint);
  203. endControlPoint.x += startX;
  204. endControlPoint.y = -endControlPoint.y + startY;
  205. // middleControlPoint.x = (startControlPoint.x + endControlPoint.x) / 2;
  206. // middleControlPoint.y = (startControlPoint.y + endControlPoint.y) / 2 + 1.0;
  207. /* for DEBUG only */
  208. // this.intersection = transposeMatrix.vectorMultiplication(intersectionPoint);
  209. // this.intersection.x += startX;
  210. // this.intersection.y = -this.intersection.y + startY;
  211. /* for DEBUG only */
  212. // set private members
  213. this.bezierStartPt = new PointF2D(startX, startY - startYOffset);
  214. this.bezierStartControlPt = new PointF2D(startControlPoint.x, startControlPoint.y - startYOffset);
  215. this.bezierEndControlPt = new PointF2D(endControlPoint.x, endControlPoint.y - endYOffset);
  216. this.bezierEndPt = new PointF2D(endX, endY - endYOffset);
  217. // calculate slur Curvepoints and update Skyline
  218. const length: number = staffLine.SkyLine.length;
  219. const startIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(this.bezierStartPt.x, length);
  220. const endIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(this.bezierEndPt.x, length);
  221. const distance: number = this.bezierEndPt.x - this.bezierStartPt.x;
  222. const samplingUnit: number = skyBottomLineCalculator.SamplingUnit;
  223. for (let i: number = startIndex; i < endIndex; i++) {
  224. // get the right distance ratio and index on the curve
  225. const diff: number = i / samplingUnit - this.bezierStartPt.x;
  226. const curvePoint: PointF2D = this.calculateCurvePointAtIndex(Math.abs(diff) / distance);
  227. // update left- and rightIndex for better accuracy
  228. let index: number = skyBottomLineCalculator.getLeftIndexForPointX(curvePoint.x, length);
  229. // update SkyLine with final slur curve:
  230. if (index >= startIndex) {
  231. staffLine.SkyLine[index] = Math.min(staffLine.SkyLine[index], curvePoint.y);
  232. }
  233. index++;
  234. if (index < length) {
  235. staffLine.SkyLine[index] = Math.min(staffLine.SkyLine[index], curvePoint.y);
  236. }
  237. }
  238. } else {
  239. startY += rules.SlurNoteHeadYOffset;
  240. endY += rules.SlurNoteHeadYOffset;
  241. // firstStaffEntry startLowerRightPoint and lastStaffentry endLowerLeftPoint
  242. const startLowerRight: PointF2D = new PointF2D(this.staffEntries[0].parentMeasure.PositionAndShape.RelativePosition.x
  243. + this.staffEntries[0].PositionAndShape.RelativePosition.x,
  244. startY);
  245. if (slurStartNote) {
  246. startLowerRight.x += this.staffEntries[0].PositionAndShape.BorderRight;
  247. } else {
  248. // continuing Slur from previous StaffLine - must start after last Instruction of first Measure
  249. startLowerRight.x = this.staffEntries[0].parentMeasure.beginInstructionsWidth;
  250. }
  251. // must also add the GraceStaffEntry's ParentStaffEntry Position
  252. if (this.graceStart) {
  253. startLowerRight.x += endStaffEntry.PositionAndShape.RelativePosition.x;
  254. }
  255. const endLowerLeft: PointF2D = new PointF2D(this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.RelativePosition.x
  256. + this.staffEntries[this.staffEntries.length - 1].PositionAndShape.RelativePosition.x,
  257. endY);
  258. if (slurEndNote) {
  259. endLowerLeft.x += this.staffEntries[this.staffEntries.length - 1].PositionAndShape.BorderLeft;
  260. } else {
  261. // Slur continues to next StaffLine - must reach the end of current StaffLine
  262. endLowerLeft.x = this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.RelativePosition.x
  263. + this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.Size.width;
  264. }
  265. // must also add the GraceStaffEntry's ParentStaffEntry Position
  266. if (this.graceEnd) {
  267. endLowerLeft.x += endStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
  268. }
  269. // BottomLinePointsList between firstStaffEntry startLowerRightPoint and lastStaffentry endLowerLeftPoint
  270. points = this.calculateBottomPoints(startLowerRight, endLowerLeft, staffLine, skyBottomLineCalculator);
  271. if (points.length === 0) {
  272. const pointF: PointF2D = new PointF2D((endLowerLeft.x - startLowerRight.x) / 2 + startLowerRight.x,
  273. (endLowerLeft.y - startLowerRight.y) / 2 + startLowerRight.y);
  274. points.push(pointF);
  275. }
  276. // Angle between original x-Axis and Line from Start-Point to End-Point
  277. const startEndLineAngleRadians: number = Math.atan((endY - startY) / (endX - startX));
  278. // translate origin at Start
  279. const start2: PointF2D = new PointF2D(0, 0);
  280. let end2: PointF2D = new PointF2D(endX - startX, endY - startY);
  281. // and Rotate at new Origin startEndLineAngle degrees
  282. // clockwise/counterclockwise Rotation
  283. // after Rotation end2.Y must be 0
  284. // Inverse of RotationMatrix = TransposeMatrix of RotationMatrix
  285. const rotationMatrix: Matrix2D = Matrix2D.getRotationMatrix(-startEndLineAngleRadians);
  286. const transposeMatrix: Matrix2D = rotationMatrix.getTransposeMatrix();
  287. end2 = rotationMatrix.vectorMultiplication(end2);
  288. const transformedPoints: PointF2D[] = this.calculateTranslatedAndRotatedPointListBelow(points, startX, startY, rotationMatrix);
  289. // calculate tangent Lines maximum Slopes between StartPoint and EndPoint to all Points in BottomLine
  290. // and tangent Lines characteristica
  291. const startLineSlope: number = this.calculateMaxLeftSlope(transformedPoints, start2, end2);
  292. const endLineSlope: number = this.calculateMaxRightSlope(transformedPoints, start2, end2);
  293. const startLineD: number = start2.y - start2.x * startLineSlope;
  294. const endLineD: number = end2.y - end2.x * endLineSlope;
  295. // calculate IntersectionPoint of the 2 Lines
  296. // if same Slope, then Point.X between Start and End and Point.Y fixed
  297. const intersectionPoint: PointF2D = new PointF2D();
  298. let sameSlope: boolean = false;
  299. if (Math.abs(Math.abs(startLineSlope) - Math.abs(endLineSlope)) < 0.0001) {
  300. intersectionPoint.x = end2.x / 2;
  301. intersectionPoint.y = 0;
  302. sameSlope = true;
  303. } else {
  304. intersectionPoint.x = (endLineD - startLineD) / (startLineSlope - endLineSlope);
  305. intersectionPoint.y = startLineSlope * intersectionPoint.x + startLineD;
  306. }
  307. // calculate HeightWidthRatio between the MaxYpoint (from the points between StartPoint and EndPoint)
  308. // and the X-distance from StartPoint to EndPoint
  309. const heightWidthRatio: number = this.calculateHeightWidthRatio(end2.x, transformedPoints);
  310. // Shift start- or endPoint and corresponding controlPoint away from note, if needed:
  311. // e.g. if there is a close object creating a high slope, better shift it away to reduce the slope:
  312. // 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)
  313. // if the slope is greater than the half heightWidthRatio (which will 99% be the case),
  314. // then add a y-offset to reduce the slope to the same value as the half heightWidthRatio of the bounding box
  315. const startYOffset: number = 0;
  316. const endYOffset: number = 0;
  317. /*if (Math.abs(heightWidthRatio) > 0.001) {
  318. // 1. start side:
  319. const startSlopeRatio: number = Math.abs(startLineSlope / (heightWidthRatio * 2));
  320. const maxLeftYOffset: number = Math.abs(startLineSlope);
  321. startYOffset = Math.max(0, maxLeftYOffset * (Math.min(10, startSlopeRatio - 1) / 10));
  322. // slope has to be adapted now due to the y-offset:
  323. startLineSlope -= startYOffset;
  324. // 2. end side:
  325. const endSlopeRatio: number = Math.abs(endLineSlope / (heightWidthRatio * 2));
  326. const maxRightYOffset: number = Math.abs(endLineSlope);
  327. endYOffset = Math.max(0, maxRightYOffset * (Math.min(10, endSlopeRatio - 1) / 10));
  328. // slope has to be adapted now due to the y-offset:
  329. endLineSlope += endYOffset;
  330. } */
  331. // calculate tangent Lines Angles
  332. // (using the calculated Slopes and the Ratio from the IntersectionPoint's distance to the MaxPoint in the SkyLine)
  333. let startAngle: number = minAngle;
  334. let endAngle: number = -minAngle;
  335. // if the calculated Slopes (start and end) are equal, then Angles have fixed values
  336. if (!sameSlope) {
  337. const result: {startAngle: number, endAngle: number} =
  338. this.calculateAngles(minAngle, startLineSlope, endLineSlope, maxAngle);
  339. startAngle = result.startAngle;
  340. endAngle = result.endAngle;
  341. }
  342. // calculate Curve's Control Points
  343. const controlPoints: {startControlPoint: PointF2D, endControlPoint: PointF2D} =
  344. this.calculateControlPoints(end2.x, startAngle, endAngle, transformedPoints, heightWidthRatio, startY, endY);
  345. let startControlPoint: PointF2D = controlPoints.startControlPoint;
  346. let endControlPoint: PointF2D = controlPoints.endControlPoint;
  347. // transform ControlPoints to original Coordinate System
  348. // (rotate back and translate back)
  349. startControlPoint = transposeMatrix.vectorMultiplication(startControlPoint);
  350. startControlPoint.x += startX;
  351. startControlPoint.y += startY;
  352. endControlPoint = transposeMatrix.vectorMultiplication(endControlPoint);
  353. endControlPoint.x += startX;
  354. endControlPoint.y += startY;
  355. // set private members
  356. this.bezierStartPt = new PointF2D(startX, startY + startYOffset);
  357. this.bezierStartControlPt = new PointF2D(startControlPoint.x, startControlPoint.y + startYOffset);
  358. this.bezierEndControlPt = new PointF2D(endControlPoint.x, endControlPoint.y + endYOffset);
  359. this.bezierEndPt = new PointF2D(endX, endY + endYOffset);
  360. /* for DEBUG only */
  361. // this.intersection = transposeMatrix.vectorMultiplication(intersectionPoint);
  362. // this.intersection.x += startX;
  363. // this.intersection.y += startY;
  364. /* for DEBUG only */
  365. // calculate CurvePoints
  366. const length: number = staffLine.BottomLine.length;
  367. const startIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(this.bezierStartPt.x, length);
  368. const endIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(this.bezierEndPt.x, length);
  369. const distance: number = this.bezierEndPt.x - this.bezierStartPt.x;
  370. const samplingUnit: number = skyBottomLineCalculator.SamplingUnit;
  371. for (let i: number = startIndex; i < endIndex; i++) {
  372. // get the right distance ratio and index on the curve
  373. const diff: number = i / samplingUnit - this.bezierStartPt.x;
  374. const curvePoint: PointF2D = this.calculateCurvePointAtIndex(Math.abs(diff) / distance);
  375. // update start- and endIndex for better accuracy
  376. let index: number = skyBottomLineCalculator.getLeftIndexForPointX(curvePoint.x, length);
  377. // update BottomLine with final slur curve:
  378. if (index >= startIndex) {
  379. staffLine.BottomLine[index] = Math.max(staffLine.BottomLine[index], curvePoint.y);
  380. }
  381. index++;
  382. if (index < length) {
  383. staffLine.BottomLine[index] = Math.max(staffLine.BottomLine[index], curvePoint.y);
  384. }
  385. }
  386. }
  387. }
  388. /**
  389. * This method calculates the Start and End Positions of the Slur Curve.
  390. * @param slurStartNote
  391. * @param slurEndNote
  392. * @param staffLine
  393. * @param startX
  394. * @param startY
  395. * @param endX
  396. * @param endY
  397. * @param rules
  398. * @param skyBottomLineCalculator
  399. */
  400. private calculateStartAndEnd( slurStartNote: GraphicalNote,
  401. slurEndNote: GraphicalNote,
  402. staffLine: StaffLine,
  403. rules: EngravingRules,
  404. skyBottomLineCalculator: SkyBottomLineCalculator): {startX: number, startY: number, endX: number, endY: number} {
  405. let startX: number = 0;
  406. let startY: number = 0;
  407. let endX: number = 0;
  408. let endY: number = 0;
  409. if (slurStartNote) {
  410. // must be relative to StaffLine
  411. startX = slurStartNote.PositionAndShape.RelativePosition.x + slurStartNote.parentVoiceEntry.parentStaffEntry.PositionAndShape.RelativePosition.x
  412. + slurStartNote.parentVoiceEntry.parentStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
  413. // If Slur starts on a Gracenote
  414. if (this.graceStart) {
  415. startX += slurStartNote.parentVoiceEntry.parentStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
  416. }
  417. //const first: GraphicalNote = slurStartNote.parentVoiceEntry.notes[0];
  418. // Determine Start/End Point coordinates with the VoiceEntry of the Start/EndNote of the slur
  419. const slurStartVE: GraphicalVoiceEntry = slurStartNote.parentVoiceEntry;
  420. if (this.placement === PlacementEnum.Above) {
  421. startY = slurStartVE.PositionAndShape.RelativePosition.y + slurStartVE.PositionAndShape.BorderTop;
  422. // for (const articulation of slurStartVE.parentVoiceEntry.Articulations) {
  423. // if (articulation.placement === PlacementEnum.Above) {
  424. // startY -= 1;
  425. // break;
  426. // }
  427. // }
  428. } else {
  429. startY = slurStartVE.PositionAndShape.RelativePosition.y + slurStartVE.PositionAndShape.BorderBottom;
  430. // for (const articulation of slurStartVE.parentVoiceEntry.Articulations) {
  431. // if (articulation.placement === PlacementEnum.Below) {
  432. // startY += 1;
  433. // break;
  434. // }
  435. // }
  436. }
  437. // 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
  438. // of the notehead. Note: an exact calculation using the position of the note is too complicate for the payoff
  439. if ( slurStartVE.parentVoiceEntry.StemDirection === StemDirectionType.Down && this.placement === PlacementEnum.Below ) {
  440. startX -= 0.5;
  441. }
  442. if (slurStartVE.parentVoiceEntry.StemDirection === StemDirectionType.Up && this.placement === PlacementEnum.Above) {
  443. startX += 0.5;
  444. }
  445. // if (first.NoteStem && first.NoteStem.Direction === StemEnum.StemUp && this.placement === PlacementEnum.Above) {
  446. // startX += first.NoteStem.PositionAndShape.RelativePosition.x;
  447. // startY = skyBottomLineCalculator.getSkyLineMinAtPoint(staffLine, startX);
  448. // } else {
  449. // const last: GraphicalNote = <GraphicalNote>slurStartNote[slurEndNote.parentVoiceEntry.notes.length - 1];
  450. // if (last.NoteStem && last.NoteStem.Direction === StemEnum.StemDown && this.placement === PlacementEnum.Below) {
  451. // startX += last.NoteStem.PositionAndShape.RelativePosition.x;
  452. // startY = skyBottomLineCalculator.getBottomLineMaxAtPoint(staffLine, startX);
  453. // } else {
  454. // }
  455. // }
  456. } else {
  457. startX = 0;
  458. }
  459. if (slurEndNote) {
  460. endX = slurEndNote.PositionAndShape.RelativePosition.x + slurEndNote.parentVoiceEntry.parentStaffEntry.PositionAndShape.RelativePosition.x
  461. + slurEndNote.parentVoiceEntry.parentStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
  462. // If Slur ends in a Gracenote
  463. if (this.graceEnd) {
  464. endX += slurEndNote.parentVoiceEntry.parentStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
  465. }
  466. const slurEndVE: GraphicalVoiceEntry = slurEndNote.parentVoiceEntry;
  467. // check for articulation -> shift end y (slur further outward)
  468. // this should not be necessary for the start note, and for accents (>) it's even counter productive there
  469. // TODO alternatively, we could fix the bounding box of the note to include the ornament, but that seems tricky
  470. let articulationPlacement: PlacementEnum; // whether there's an articulation and where
  471. for (const articulation of slurEndVE.parentVoiceEntry.Articulations) {
  472. if (articulation.placement === PlacementEnum.NotYetDefined) {
  473. for (const modifier of ((slurEndNote as VexFlowGraphicalNote).vfnote[0] as any).modifiers) {
  474. if (modifier.getCategory() === VF.Articulation.CATEGORY) {
  475. if (modifier.position === VF.Modifier.Position.ABOVE) {
  476. articulation.placement = PlacementEnum.Above;
  477. articulationPlacement = PlacementEnum.Above;
  478. } else if (modifier.position === VF.Modifier.Position.BELOW) {
  479. articulation.placement = PlacementEnum.Below;
  480. articulationPlacement = PlacementEnum.Below;
  481. }
  482. break;
  483. }
  484. }
  485. }
  486. }
  487. if (this.placement === PlacementEnum.Above) {
  488. endY = slurEndVE.PositionAndShape.RelativePosition.y + slurEndVE.PositionAndShape.BorderTop;
  489. if (articulationPlacement === PlacementEnum.Above) {
  490. endY -= this.rules.SlurEndArticulationYOffset;
  491. }
  492. } else {
  493. endY = slurEndVE.PositionAndShape.RelativePosition.y + slurEndVE.PositionAndShape.BorderBottom;
  494. if (articulationPlacement === PlacementEnum.Below) {
  495. endY += this.rules.SlurEndArticulationYOffset;
  496. }
  497. }
  498. // If the stem points towards the endpoint of the slur, shift the slur by a small amount to start (approximately) at the x-position
  499. // of the notehead. Note: an exact calculation using the position of the note is too complicate for the payoff
  500. if ( slurEndVE.parentVoiceEntry.StemDirection === StemDirectionType.Down && this.placement === PlacementEnum.Below ) {
  501. endX -= 0.5;
  502. }
  503. if (slurEndVE.parentVoiceEntry.StemDirection === StemDirectionType.Up && this.placement === PlacementEnum.Above) {
  504. endX += 0.5;
  505. }
  506. // const first: GraphicalNote = <GraphicalNote>slurEndNote.parentVoiceEntry.notes[0];
  507. // if (first.NoteStem && first.NoteStem.Direction === StemEnum.StemUp && this.placement === PlacementEnum.Above) {
  508. // endX += first.NoteStem.PositionAndShape.RelativePosition.x;
  509. // endY = skyBottomLineCalculator.getSkyLineMinAtPoint(staffLine, endX);
  510. // } else {
  511. // const last: GraphicalNote = <GraphicalNote>slurEndNote.parentVoiceEntry.notes[slurEndNote.parentVoiceEntry.notes.length - 1];
  512. // if (last.NoteStem && last.NoteStem.Direction === StemEnum.StemDown && this.placement === PlacementEnum.Below) {
  513. // endX += last.NoteStem.PositionAndShape.RelativePosition.x;
  514. // endY = skyBottomLineCalculator.getBottomLineMaxAtPoint(staffLine, endX);
  515. // } else {
  516. // if (this.placement === PlacementEnum.Above) {
  517. // const highestNote: GraphicalNote = last;
  518. // endY = highestNote.PositionAndShape.RelativePosition.y;
  519. // if (highestNote.NoteHead) {
  520. // endY += highestNote.NoteHead.PositionAndShape.BorderMarginTop;
  521. // } else { endY += highestNote.PositionAndShape.BorderTop; }
  522. // } else {
  523. // const lowestNote: GraphicalNote = first;
  524. // endY = lowestNote.parentVoiceEntry
  525. // lowestNote.PositionAndShape.RelativePosition.y;
  526. // if (lowestNote.NoteHead) {
  527. // endY += lowestNote.NoteHead.PositionAndShape.BorderMarginBottom;
  528. // } else { endY += lowestNote.PositionAndShape.BorderBottom; }
  529. // }
  530. // }
  531. // }
  532. } else {
  533. endX = staffLine.PositionAndShape.Size.width;
  534. }
  535. // if GraphicalSlur breaks over System, then the end/start of the curve is at the corresponding height with the known start/end
  536. if (!slurStartNote && !slurEndNote) {
  537. startY = -1.5;
  538. endY = -1.5;
  539. }
  540. if (!slurStartNote) {
  541. if (this.placement === PlacementEnum.Above) {
  542. startY = endY - 1;
  543. } else {
  544. startY = endY + 1;
  545. }
  546. }
  547. if (!slurEndNote) {
  548. if (this.placement === PlacementEnum.Above) {
  549. endY = startY - 1;
  550. } else {
  551. endY = startY + 1;
  552. }
  553. }
  554. // if two slurs start/end at the same GraphicalNote, then the second gets an offset
  555. if (this.slur.startNoteHasMoreStartingSlurs() && this.slur.isSlurLonger()) {
  556. if (this.placement === PlacementEnum.Above) {
  557. startY -= rules.SlursStartingAtSameStaffEntryYOffset;
  558. } else { startY += rules.SlursStartingAtSameStaffEntryYOffset; }
  559. }
  560. if (this.slur.endNoteHasMoreEndingSlurs() && this.slur.isSlurLonger()) {
  561. if (this.placement === PlacementEnum.Above) {
  562. endY -= rules.SlursStartingAtSameStaffEntryYOffset;
  563. } else { endY += rules.SlursStartingAtSameStaffEntryYOffset; }
  564. }
  565. if (this.placement === PlacementEnum.Above) {
  566. startY = Math.min(startY, 1.5);
  567. endY = Math.min(endY, 1.5);
  568. } else {
  569. startY = Math.max(startY, staffLine.StaffHeight - 1.5);
  570. endY = Math.max(endY, staffLine.StaffHeight - 1.5);
  571. }
  572. return {startX, startY, endX, endY};
  573. }
  574. /**
  575. * This method calculates the placement of the Curve.
  576. * @param skyBottomLineCalculator
  577. * @param staffLine
  578. */
  579. private calculatePlacement(skyBottomLineCalculator: SkyBottomLineCalculator, staffLine: StaffLine): void {
  580. // old version: when lyrics are given place above:
  581. // if ( !this.slur.StartNote.ParentVoiceEntry.LyricsEntries.isEmpty || (this.slur.EndNote
  582. // && !this.slur.EndNote.ParentVoiceEntry.LyricsEntries.isEmpty) ) {
  583. // this.placement = PlacementEnum.Above;
  584. // return;
  585. // }
  586. if (this.rules.SlurPlacementFromXML) {
  587. this.placement = this.slur.PlacementXml;
  588. return;
  589. }
  590. // if any StaffEntry belongs to a Measure with multiple Voices, than
  591. // if Slur's Start- or End-Note belongs to a LinkedVoice Below else Above
  592. for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) {
  593. const graphicalStaffEntry: GraphicalStaffEntry = this.staffEntries[idx];
  594. if (graphicalStaffEntry.parentMeasure.hasMultipleVoices()) {
  595. if (this.slur.StartNote.ParentVoiceEntry.ParentVoice instanceof LinkedVoice ||
  596. this.slur.EndNote.ParentVoiceEntry.ParentVoice instanceof LinkedVoice) {
  597. this.placement = PlacementEnum.Below;
  598. } else { this.placement = PlacementEnum.Above; }
  599. return;
  600. }
  601. }
  602. // when lyrics are given place above:
  603. for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) {
  604. const graphicalStaffEntry: GraphicalStaffEntry = this.staffEntries[idx];
  605. if (graphicalStaffEntry.LyricsEntries.length > 0) {
  606. this.placement = PlacementEnum.Above;
  607. return;
  608. }
  609. }
  610. const startStaffEntry: GraphicalStaffEntry = this.staffEntries[0];
  611. const endStaffEntry: GraphicalStaffEntry = this.staffEntries[this.staffEntries.length - 1];
  612. // single Voice, opposite to StemDirection
  613. // here should only be one voiceEntry, so we can take graphicalVoiceEntries[0]:
  614. const startStemDirection: StemDirectionType = startStaffEntry.graphicalVoiceEntries[0].parentVoiceEntry.StemDirection;
  615. const endStemDirection: StemDirectionType = endStaffEntry.graphicalVoiceEntries[0].parentVoiceEntry.StemDirection;
  616. if (startStemDirection ===
  617. endStemDirection) {
  618. this.placement = (startStemDirection === StemDirectionType.Up) ? PlacementEnum.Below : PlacementEnum.Above;
  619. } else {
  620. // Placement at the side with the minimum border
  621. let sX: number = startStaffEntry.PositionAndShape.BorderLeft + startStaffEntry.PositionAndShape.RelativePosition.x
  622. + startStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
  623. let eX: number = endStaffEntry.PositionAndShape.BorderRight + endStaffEntry.PositionAndShape.RelativePosition.x
  624. + endStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
  625. if (this.graceStart) {
  626. sX += endStaffEntry.PositionAndShape.RelativePosition.x;
  627. }
  628. if (this.graceEnd) {
  629. eX += endStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
  630. }
  631. // get SkyBottomLine borders
  632. const minAbove: number = skyBottomLineCalculator.getSkyLineMinInRange(sX, eX) * -1;
  633. const maxBelow: number = skyBottomLineCalculator.getBottomLineMaxInRange(sX, eX) - staffLine.StaffHeight;
  634. if (maxBelow > minAbove) {
  635. this.placement = PlacementEnum.Above;
  636. } else { this.placement = PlacementEnum.Below; }
  637. }
  638. }
  639. /**
  640. * This method calculates the Points between Start- and EndPoint (case above).
  641. * @param start
  642. * @param end
  643. * @param staffLine
  644. * @param skyBottomLineCalculator
  645. */
  646. private calculateTopPoints(start: PointF2D, end: PointF2D, staffLine: StaffLine, skyBottomLineCalculator: SkyBottomLineCalculator): PointF2D[] {
  647. const points: PointF2D[] = [];
  648. let startIndex: number = skyBottomLineCalculator.getRightIndexForPointX(start.x, staffLine.SkyLine.length);
  649. let endIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(end.x, staffLine.SkyLine.length);
  650. if (startIndex < 0) {
  651. startIndex = 0;
  652. }
  653. if (endIndex >= staffLine.SkyLine.length) {
  654. endIndex = staffLine.SkyLine.length - 1;
  655. }
  656. for (let i: number = startIndex; i < endIndex; i++) {
  657. const skylineValue: number = staffLine.SkyLine[i];
  658. // ignore default value (= 0) which is upper border of staffline
  659. if (skylineValue !== 0) {
  660. const point: PointF2D = new PointF2D((0.5 + i) / skyBottomLineCalculator.SamplingUnit, skylineValue);
  661. points.push(point);
  662. }
  663. }
  664. return points;
  665. }
  666. /**
  667. * This method calculates the Points between Start- and EndPoint (case below).
  668. * @param start
  669. * @param end
  670. * @param staffLine
  671. * @param skyBottomLineCalculator
  672. */
  673. private calculateBottomPoints(start: PointF2D, end: PointF2D, staffLine: StaffLine, skyBottomLineCalculator: SkyBottomLineCalculator): PointF2D[] {
  674. const points: PointF2D[] = [];
  675. // get BottomLine indices
  676. let startIndex: number = skyBottomLineCalculator.getRightIndexForPointX(start.x, staffLine.BottomLine.length);
  677. let endIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(end.x, staffLine.BottomLine.length);
  678. if (startIndex < 0) {
  679. startIndex = 0;
  680. }
  681. if (endIndex >= staffLine.BottomLine.length) {
  682. endIndex = staffLine.BottomLine.length - 1;
  683. }
  684. for (let i: number = startIndex; i < endIndex; i++) {
  685. const bottomLineValue: number = staffLine.BottomLine[i];
  686. // ignore default value (= 4) which is lower border of staffline
  687. if (bottomLineValue !== 0) {
  688. const point: PointF2D = new PointF2D((0.5 + i) / skyBottomLineCalculator.SamplingUnit, bottomLineValue);
  689. points.push(point);
  690. }
  691. }
  692. return points;
  693. }
  694. /**
  695. * This method calculates the maximum slope between StartPoint and BetweenPoints.
  696. * @param points
  697. * @param start
  698. * @param end
  699. */
  700. private calculateMaxLeftSlope(points: PointF2D[], start: PointF2D, end: PointF2D): number {
  701. let slope: number = -Number.MAX_VALUE;
  702. const x: number = start.x;
  703. const y: number = start.y;
  704. for (let i: number = 0; i < points.length; i++) {
  705. if (Math.abs(points[i].y - Number.MAX_VALUE) < 0.0001 || Math.abs(points[i].y - (-Number.MAX_VALUE)) < 0.0001) {
  706. continue;
  707. }
  708. slope = Math.max(slope, (points[i].y - y) / (points[i].x - x));
  709. }
  710. // in case all Points don't have a meaningful value or the slope between Start- and EndPoint is just bigger
  711. slope = Math.max(slope, Math.abs(end.y - y) / (end.x - x));
  712. //limit to 80 degrees
  713. slope = Math.min(slope, 5.6713);
  714. return slope;
  715. }
  716. /**
  717. * This method calculates the maximum slope between EndPoint and BetweenPoints.
  718. * @param points
  719. * @param start
  720. * @param end
  721. */
  722. private calculateMaxRightSlope(points: PointF2D[], start: PointF2D, end: PointF2D): number {
  723. let slope: number = Number.MAX_VALUE;
  724. const x: number = end.x;
  725. const y: number = end.y;
  726. for (let i: number = 0; i < points.length; i++) {
  727. if (Math.abs(points[i].y - Number.MAX_VALUE) < 0.0001 || Math.abs(points[i].y - (-Number.MAX_VALUE)) < 0.0001) {
  728. continue;
  729. }
  730. slope = Math.min(slope, (y - points[i].y) / (x - points[i].x));
  731. }
  732. // in case no Point has a meaningful value or the slope between Start- and EndPoint is just smaller
  733. slope = Math.min(slope, (y - start.y) / (x - start.x));
  734. //limit to 80 degrees
  735. slope = Math.max(slope, -5.6713);
  736. return slope;
  737. }
  738. /**
  739. * This method returns the maximum (meaningful) points.Y.
  740. * @param points
  741. */
  742. private getPointListMaxY(points: PointF2D[]): number {
  743. let max: number = -Number.MAX_VALUE;
  744. for (let idx: number = 0, len: number = points.length; idx < len; ++idx) {
  745. const point: PointF2D = points[idx];
  746. if (Math.abs(point.y - (-Number.MAX_VALUE)) < 0.0001 || Math.abs(point.y - Number.MAX_VALUE) < 0.0001) {
  747. continue;
  748. }
  749. max = Math.max(max, point.y);
  750. }
  751. return max;
  752. }
  753. /**
  754. * This method calculates the translated and rotated PointsList (case above).
  755. * @param points
  756. * @param startX
  757. * @param startY
  758. * @param rotationMatrix
  759. */
  760. private calculateTranslatedAndRotatedPointListAbove(points: PointF2D[], startX: number, startY: number, rotationMatrix: Matrix2D): PointF2D[] {
  761. const transformedPoints: PointF2D[] = [];
  762. for (let i: number = 0; i < points.length; i++) {
  763. if (Math.abs(points[i].y - Number.MAX_VALUE) < 0.0001 || Math.abs(points[i].y - (-Number.MAX_VALUE)) < 0.0001) {
  764. continue;
  765. }
  766. let point: PointF2D = new PointF2D(points[i].x - startX, -(points[i].y - startY));
  767. point = rotationMatrix.vectorMultiplication(point);
  768. transformedPoints.push(point);
  769. }
  770. return transformedPoints;
  771. }
  772. /**
  773. * This method calculates the translated and rotated PointsList (case below).
  774. * @param points
  775. * @param startX
  776. * @param startY
  777. * @param rotationMatrix
  778. */
  779. private calculateTranslatedAndRotatedPointListBelow(points: PointF2D[], startX: number, startY: number, rotationMatrix: Matrix2D): PointF2D[] {
  780. const transformedPoints: PointF2D[] = [];
  781. for (let i: number = 0; i < points.length; i++) {
  782. if (Math.abs(points[i].y - Number.MAX_VALUE) < 0.0001 || Math.abs(points[i].y - (-Number.MAX_VALUE)) < 0.0001) {
  783. continue;
  784. }
  785. let point: PointF2D = new PointF2D(points[i].x - startX, points[i].y - startY);
  786. point = rotationMatrix.vectorMultiplication(point);
  787. transformedPoints.push(point);
  788. }
  789. return transformedPoints;
  790. }
  791. /**
  792. * This method calculates the HeightWidthRatio between the MaxYpoint (from the points between StartPoint and EndPoint)
  793. * and the X-distance from StartPoint to EndPoint.
  794. * @param endX
  795. * @param points
  796. */
  797. private calculateHeightWidthRatio(endX: number, points: PointF2D[]): number {
  798. if (points.length === 0) {
  799. return 0;
  800. }
  801. // in case of negative points
  802. const max: number = Math.max(0, this.getPointListMaxY(points));
  803. return max / endX;
  804. }
  805. /**
  806. * This method calculates the 2 ControlPoints of the SlurCurve.
  807. * @param endX
  808. * @param startAngle
  809. * @param endAngle
  810. * @param points
  811. */
  812. private calculateControlPoints(endX: number, startAngle: number, endAngle: number,
  813. points: PointF2D[], heightWidthRatio: number,
  814. startY: number, endY: number
  815. ): { startControlPoint: PointF2D, endControlPoint: PointF2D } {
  816. let heightFactor: number = this.rules.SlurHeightFactor;
  817. let widthFlattenFactor: number = 1;
  818. const cutoffAngle: number = this.rules.SlurHeightFlattenLongSlursCutoffAngle;
  819. const cutoffWidth: number = this.rules.SlurHeightFlattenLongSlursCutoffWidth;
  820. // console.log("width: " + endX);
  821. if (startAngle > cutoffAngle && endX > cutoffWidth) { // steep and wide slurs
  822. // console.log("steep angle: " + startAngle);
  823. widthFlattenFactor += endX / 70 * this.rules.SlurHeightFlattenLongSlursFactorByWidth; // double flattening for width = 70, factorByWidth = 1
  824. widthFlattenFactor *= 1 + (startAngle / 30 * this.rules.SlurHeightFlattenLongSlursFactorByAngle); // flatten more for higher angles.
  825. // TODO use sin or cos instead of startAngle directly
  826. heightFactor /= widthFlattenFactor; // flatten long slurs more
  827. }
  828. // TODO also offer a widthFlattenFactor for smaller slurs?
  829. // debug:
  830. // const measureNumber: number = this.staffEntries[0].parentMeasure.MeasureNumber; // debug
  831. // if (measureNumber === 10) {
  832. // console.log("endX: " + endX);
  833. // console.log("widthFlattenFactor: " + widthFlattenFactor);
  834. // console.log("heightFactor: " + heightFactor);
  835. // console.log("startAngle: " + startAngle);
  836. // console.log("heightWidthRatio: " + heightWidthRatio);
  837. // }
  838. // calculate HeightWidthRatio between the MaxYpoint (from the points between StartPoint and EndPoint)
  839. // and the X-distance from StartPoint to EndPoint
  840. // use this HeightWidthRatio to get a "normalized" Factor (based on tested parameters)
  841. // this Factor denotes the Length of the TangentLine of the Curve (a proportion of the X-distance from StartPoint to EndPoint)
  842. // finally from this Length and the calculated Angles we get the coordinates of the Control Points
  843. 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)));
  844. 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)));
  845. const startControlPoint: PointF2D = new PointF2D();
  846. startControlPoint.x = endX * factorStart * Math.cos(startAngle * GraphicalSlur.degreesToRadiansFactor);
  847. startControlPoint.y = endX * factorStart * Math.sin(startAngle * GraphicalSlur.degreesToRadiansFactor);
  848. const endControlPoint: PointF2D = new PointF2D();
  849. endControlPoint.x = endX - (endX * factorEnd * Math.cos(endAngle * GraphicalSlur.degreesToRadiansFactor));
  850. endControlPoint.y = -(endX * factorEnd * Math.sin(endAngle * GraphicalSlur.degreesToRadiansFactor));
  851. //Soften the slur in a "brute-force" way
  852. let controlPointYDiff: number = startControlPoint.y - endControlPoint.y;
  853. while (this.rules.SlurMaximumYControlPointDistance &&
  854. Math.abs(controlPointYDiff) > this.rules.SlurMaximumYControlPointDistance) {
  855. if (controlPointYDiff < 0) {
  856. startControlPoint.y += 1;
  857. endControlPoint.y -= 1;
  858. } else {
  859. startControlPoint.y -= 1;
  860. endControlPoint.y += 1;
  861. }
  862. controlPointYDiff = startControlPoint.y - endControlPoint.y;
  863. }
  864. return {startControlPoint: startControlPoint, endControlPoint: endControlPoint};
  865. }
  866. /**
  867. * This method calculates the angles for the Curve's Tangent Lines.
  868. * @param leftAngle
  869. * @param rightAngle
  870. * @param startLineSlope
  871. * @param endLineSlope
  872. * @param maxAngle
  873. */
  874. private calculateAngles(minAngle: number, startLineSlope: number, endLineSlope: number, maxAngle: number):
  875. {startAngle: number, endAngle: number} {
  876. // calculate Angles from the calculated Slopes, adding also a given angle
  877. const angle: number = 20;
  878. let calculatedStartAngle: number = Math.atan(startLineSlope) / GraphicalSlur.degreesToRadiansFactor;
  879. if (startLineSlope > 0) {
  880. calculatedStartAngle += angle;
  881. } else {
  882. calculatedStartAngle -= angle;
  883. }
  884. let calculatedEndAngle: number = Math.atan(endLineSlope) / GraphicalSlur.degreesToRadiansFactor;
  885. if (endLineSlope < 0) {
  886. calculatedEndAngle -= angle;
  887. } else {
  888. calculatedEndAngle += angle;
  889. }
  890. // +/- 80 is the max/min allowed Angle
  891. const leftAngle: number = Math.min(Math.max(minAngle, calculatedStartAngle), maxAngle);
  892. const rightAngle: number = Math.max(Math.min(-minAngle, calculatedEndAngle), -maxAngle);
  893. return {"startAngle": leftAngle, "endAngle": rightAngle};
  894. }
  895. private static degreesToRadiansFactor: number = Math.PI / 180;
  896. }