GraphicalSlur.ts 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850
  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. export class GraphicalSlur extends GraphicalCurve {
  16. // private intersection: PointF2D;
  17. constructor(slur: Slur) {
  18. super();
  19. this.slur = slur;
  20. }
  21. public slur: Slur;
  22. public staffEntries: GraphicalStaffEntry[] = [];
  23. public placement: PlacementEnum;
  24. public graceStart: boolean;
  25. public graceEnd: boolean;
  26. /**
  27. * Compares the timespan of two Graphical Slurs
  28. * @param x
  29. * @param y
  30. */
  31. public static Compare (x: GraphicalSlur, y: GraphicalSlur ): number {
  32. if (x.staffEntries.length < 1) { // x.staffEntries[i] can return undefined in Beethoven Moonlight Sonata sample
  33. return -1;
  34. } else if (y.staffEntries.length < 1) {
  35. return 1;
  36. }
  37. const xTimestampSpan: Fraction = Fraction.minus(x.staffEntries[x.staffEntries.length - 1].getAbsoluteTimestamp(),
  38. x.staffEntries[0].getAbsoluteTimestamp());
  39. const yTimestampSpan: Fraction = Fraction.minus(y.staffEntries[y.staffEntries.length - 1].getAbsoluteTimestamp(),
  40. y.staffEntries[0].getAbsoluteTimestamp());
  41. if (xTimestampSpan.RealValue > yTimestampSpan.RealValue) {
  42. return 1;
  43. }
  44. if (yTimestampSpan.RealValue > xTimestampSpan.RealValue) {
  45. return -1;
  46. }
  47. return 0;
  48. }
  49. /**
  50. *
  51. * @param rules
  52. */
  53. public calculateCurve(rules: EngravingRules): void {
  54. // single GraphicalSlur means a single Curve, eg each GraphicalSlurObject is meant to be on the same StaffLine
  55. // a Slur can span more than one GraphicalSlurObjects
  56. const startStaffEntry: GraphicalStaffEntry = this.staffEntries[0];
  57. const endStaffEntry: GraphicalStaffEntry = this.staffEntries[this.staffEntries.length - 1];
  58. // where the Slur (not the graphicalObject) starts and ends (could belong to another StaffLine)
  59. let slurStartNote: GraphicalNote = startStaffEntry.findGraphicalNoteFromNote(this.slur.StartNote);
  60. if (slurStartNote === undefined && this.graceStart) {
  61. slurStartNote = startStaffEntry.findGraphicalNoteFromGraceNote(this.slur.StartNote);
  62. }
  63. if (slurStartNote === undefined) {
  64. slurStartNote = startStaffEntry.findEndTieGraphicalNoteFromNoteWithStartingSlur(this.slur.StartNote, this.slur);
  65. }
  66. let slurEndNote: GraphicalNote = endStaffEntry.findGraphicalNoteFromNote(this.slur.EndNote);
  67. if (slurEndNote === undefined && this.graceEnd) {
  68. slurEndNote = endStaffEntry.findGraphicalNoteFromGraceNote(this.slur.EndNote);
  69. }
  70. const staffLine: StaffLine = startStaffEntry.parentMeasure.ParentStaffLine;
  71. const skyBottomLineCalculator: SkyBottomLineCalculator = staffLine.SkyBottomLineCalculator;
  72. this.calculatePlacement(skyBottomLineCalculator, staffLine);
  73. // the Start- and End Reference Points for the Sky-BottomLine
  74. const startEndPoints: {startX: number, startY: number, endX: number, endY: number} =
  75. this.calculateStartAndEnd(slurStartNote, slurEndNote, staffLine, rules, skyBottomLineCalculator);
  76. const startX: number = startEndPoints.startX;
  77. const endX: number = startEndPoints.endX;
  78. let startY: number = startEndPoints.startY;
  79. let endY: number = startEndPoints.endY;
  80. const minAngle: number = rules.SlurTangentMinAngle;
  81. const maxAngle: number = rules.SlurTangentMaxAngle;
  82. let start: PointF2D, end: PointF2D;
  83. let points: PointF2D[];
  84. if (this.placement === PlacementEnum.Above) {
  85. startY -= rules.SlurNoteHeadYOffset;
  86. endY -= rules.SlurNoteHeadYOffset;
  87. start = new PointF2D(startX, startY);
  88. end = new PointF2D(endX, endY);
  89. const startUpperRight: PointF2D = new PointF2D(this.staffEntries[0].parentMeasure.PositionAndShape.RelativePosition.x
  90. + this.staffEntries[0].PositionAndShape.RelativePosition.x,
  91. startY);
  92. if (slurStartNote !== undefined) {
  93. startUpperRight.x += this.staffEntries[0].PositionAndShape.BorderRight;
  94. } else {
  95. // continuing Slur from previous StaffLine - must start after last Instruction of first Measure
  96. startUpperRight.x = this.staffEntries[0].parentMeasure.beginInstructionsWidth;
  97. }
  98. // must also add the GraceStaffEntry's ParentStaffEntry Position
  99. if (this.graceStart) {
  100. startUpperRight.x += endStaffEntry.PositionAndShape.RelativePosition.x;
  101. }
  102. const endUpperLeft: PointF2D = new PointF2D(this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.RelativePosition.x
  103. + this.staffEntries[this.staffEntries.length - 1].PositionAndShape.RelativePosition.x,
  104. endY);
  105. if (slurEndNote !== undefined) {
  106. endUpperLeft.x += this.staffEntries[this.staffEntries.length - 1].PositionAndShape.BorderLeft;
  107. } else {
  108. // Slur continues to next StaffLine - must reach the end of current StaffLine
  109. endUpperLeft.x = this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.RelativePosition.x
  110. + this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.Size.width;
  111. }
  112. // must also add the GraceStaffEntry's ParentStaffEntry Position
  113. if (this.graceEnd) {
  114. endUpperLeft.x += endStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
  115. }
  116. // SkyLinePointsList between firstStaffEntry startUpperRightPoint and lastStaffentry endUpperLeftPoint
  117. points = this.calculateTopPoints(startUpperRight, endUpperLeft, staffLine, skyBottomLineCalculator);
  118. if (points.length === 0) {
  119. const pointF: PointF2D = new PointF2D((endUpperLeft.x - startUpperRight.x) / 2 + startUpperRight.x,
  120. (endUpperLeft.y - startUpperRight.y) / 2 + startUpperRight.y);
  121. points.push(pointF);
  122. }
  123. // Angle between original x-Axis and Line from Start-Point to End-Point
  124. const startEndLineAngleRadians: number = (Math.atan((endY - startY) / (endX - startX)));
  125. // translate origin at Start (positiveY from Bottom to Top => change sign for Y)
  126. const start2: PointF2D = new PointF2D(0, 0);
  127. let end2: PointF2D = new PointF2D(endX - startX, -(endY - startY));
  128. // and Rotate at new Origin startEndLineAngle degrees
  129. // clockwise/counterclockwise Rotation
  130. // after Rotation end2.Y must be 0
  131. // Inverse of RotationMatrix = TransposeMatrix of RotationMatrix
  132. let rotationMatrix: Matrix2D, transposeMatrix: Matrix2D;
  133. rotationMatrix = Matrix2D.getRotationMatrix(startEndLineAngleRadians);
  134. transposeMatrix = rotationMatrix.getTransposeMatrix();
  135. end2 = rotationMatrix.vectorMultiplication(end2);
  136. const transformedPoints: PointF2D[] = this.calculateTranslatedAndRotatedPointListAbove(points, startX, startY, rotationMatrix);
  137. // calculate tangent Lines maximum Slopes between StartPoint and EndPoint to all Points in SkyLine
  138. // and tangent Lines characteristica
  139. const leftLineSlope: number = this.calculateMaxLeftSlope(transformedPoints, start2, end2);
  140. const rightLineSlope: number = this.calculateMaxRightSlope(transformedPoints, start2, end2);
  141. const leftLineD: number = start2.y - start2.x * leftLineSlope;
  142. const rightLineD: number = end2.y - end2.x * rightLineSlope;
  143. // calculate IntersectionPoint of the 2 Lines
  144. // if same Slope, then Point.X between Start and End and Point.Y fixed
  145. const intersectionPoint: PointF2D = new PointF2D();
  146. let sameSlope: boolean = false;
  147. if (Math.abs(Math.abs(leftLineSlope) - Math.abs(rightLineSlope)) < 0.0001) {
  148. intersectionPoint.x = end2.x / 2;
  149. intersectionPoint.y = 0;
  150. sameSlope = true;
  151. } else {
  152. intersectionPoint.x = (rightLineD - leftLineD) / (leftLineSlope - rightLineSlope);
  153. intersectionPoint.y = leftLineSlope * intersectionPoint.x + leftLineD;
  154. }
  155. // calculate tangent Lines Angles
  156. // (using the calculated Slopes and the Ratio from the IntersectionPoint's distance to the MaxPoint in the SkyLine)
  157. const leftAngle: number = minAngle;
  158. const rightAngle: number = -minAngle;
  159. // if the calculated Slopes (left and right) are equal, then Angles have fixed values
  160. if (!sameSlope) {
  161. this.calculateAngles(leftAngle, rightAngle, leftLineSlope, rightLineSlope, maxAngle);
  162. }
  163. // calculate Curve's Control Points
  164. const controlPoints: {leftControlPoint: PointF2D, rightControlPoint: PointF2D} =
  165. this.calculateControlPoints(end2.x, leftAngle, rightAngle, transformedPoints);
  166. let leftControlPoint: PointF2D = controlPoints.leftControlPoint;
  167. let rightControlPoint: PointF2D = controlPoints.rightControlPoint;
  168. // transform ControlPoints to original Coordinate System
  169. // (rotate back and translate back)
  170. leftControlPoint = transposeMatrix.vectorMultiplication(leftControlPoint);
  171. leftControlPoint.x += startX;
  172. leftControlPoint.y = -leftControlPoint.y + startY;
  173. rightControlPoint = transposeMatrix.vectorMultiplication(rightControlPoint);
  174. rightControlPoint.x += startX;
  175. rightControlPoint.y = -rightControlPoint.y + startY;
  176. /* for DEBUG only */
  177. // this.intersection = transposeMatrix.vectorMultiplication(intersectionPoint);
  178. // this.intersection.x += startX;
  179. // this.intersection.y = -this.intersection.y + startY;
  180. /* for DEBUG only */
  181. // set private members
  182. this.bezierStartPt = start;
  183. this.bezierStartControlPt = leftControlPoint;
  184. this.bezierEndControlPt = rightControlPoint;
  185. this.bezierEndPt = end;
  186. // calculate CurvePoints
  187. const length: number = staffLine.SkyLine.length;
  188. const startIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(this.bezierStartPt.x, length);
  189. const endIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(this.bezierEndPt.x, length);
  190. const distance: number = this.bezierEndPt.x - this.bezierStartPt.x;
  191. const samplingUnit: number = skyBottomLineCalculator.SamplingUnit;
  192. for (let i: number = startIndex; i < endIndex; i++) {
  193. // get the right distance ratio and index on the curve
  194. const diff: number = i / samplingUnit - this.bezierStartPt.x;
  195. const curvePoint: PointF2D = this.calculateCurvePointAtIndex(Math.abs(diff) / distance);
  196. // update left- and rightIndex for better accuracy
  197. let index: number = skyBottomLineCalculator.getLeftIndexForPointX(curvePoint.x, length);
  198. // update SkyLine with final slur curve:
  199. if (index >= startIndex) {
  200. staffLine.SkyLine[index] = Math.min(staffLine.SkyLine[index], curvePoint.y);
  201. }
  202. index++;
  203. if (index < length) {
  204. staffLine.SkyLine[index] = Math.min(staffLine.SkyLine[index], curvePoint.y);
  205. }
  206. }
  207. } else {
  208. startY += rules.SlurNoteHeadYOffset;
  209. endY += rules.SlurNoteHeadYOffset;
  210. start = new PointF2D(startX, startY);
  211. end = new PointF2D(endX, endY);
  212. // firstStaffEntry startLowerRightPoint and lastStaffentry endLowerLeftPoint
  213. const startLowerRight: PointF2D = new PointF2D(this.staffEntries[0].parentMeasure.PositionAndShape.RelativePosition.x
  214. + this.staffEntries[0].PositionAndShape.RelativePosition.x,
  215. startY);
  216. if (slurStartNote !== undefined) {
  217. startLowerRight.x += this.staffEntries[0].PositionAndShape.BorderRight;
  218. } else {
  219. // continuing Slur from previous StaffLine - must start after last Instruction of first Measure
  220. startLowerRight.x = this.staffEntries[0].parentMeasure.beginInstructionsWidth;
  221. }
  222. // must also add the GraceStaffEntry's ParentStaffEntry Position
  223. if (this.graceStart) {
  224. startLowerRight.x += endStaffEntry.PositionAndShape.RelativePosition.x;
  225. }
  226. const endLowerLeft: PointF2D = new PointF2D(this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.RelativePosition.x
  227. + this.staffEntries[this.staffEntries.length - 1].PositionAndShape.RelativePosition.x,
  228. endY);
  229. if (slurEndNote !== undefined) {
  230. endLowerLeft.x += this.staffEntries[this.staffEntries.length - 1].PositionAndShape.BorderLeft;
  231. } else {
  232. // Slur continues to next StaffLine - must reach the end of current StaffLine
  233. endLowerLeft.x = this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.RelativePosition.x
  234. + this.staffEntries[this.staffEntries.length - 1].parentMeasure.PositionAndShape.Size.width;
  235. }
  236. // must also add the GraceStaffEntry's ParentStaffEntry Position
  237. if (this.graceEnd) {
  238. endLowerLeft.x += endStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
  239. }
  240. // BottomLinePointsList between firstStaffEntry startLowerRightPoint and lastStaffentry endLowerLeftPoint
  241. points = this.calculateBottomPoints(startLowerRight, endLowerLeft, staffLine, skyBottomLineCalculator);
  242. if (points.length === 0) {
  243. const pointF: PointF2D = new PointF2D((endLowerLeft.x - startLowerRight.x) / 2 + startLowerRight.x,
  244. (endLowerLeft.y - startLowerRight.y) / 2 + startLowerRight.y);
  245. points.push(pointF);
  246. }
  247. // Angle between original x-Axis and Line from Start-Point to End-Point
  248. const startEndLineAngleRadians: number = Math.atan((endY - startY) / (endX - startX));
  249. // translate origin at Start
  250. const start2: PointF2D = new PointF2D(0, 0);
  251. let end2: PointF2D = new PointF2D(endX - startX, endY - startY);
  252. // and Rotate at new Origin startEndLineAngle degrees
  253. // clockwise/counterclockwise Rotation
  254. // after Rotation end2.Y must be 0
  255. // Inverse of RotationMatrix = TransposeMatrix of RotationMatrix
  256. let rotationMatrix: Matrix2D, transposeMatrix: Matrix2D;
  257. rotationMatrix = Matrix2D.getRotationMatrix(-startEndLineAngleRadians);
  258. transposeMatrix = rotationMatrix.getTransposeMatrix();
  259. end2 = rotationMatrix.vectorMultiplication(end2);
  260. const transformedPoints: PointF2D[] = this.calculateTranslatedAndRotatedPointListBelow(points, startX, startY, rotationMatrix);
  261. // calculate tangent Lines maximum Slopes between StartPoint and EndPoint to all Points in BottomLine
  262. // and tangent Lines characteristica
  263. const leftLineSlope: number = this.calculateMaxLeftSlope(transformedPoints, start2, end2);
  264. const rightLineSlope: number = this.calculateMaxRightSlope(transformedPoints, start2, end2);
  265. const leftLineD: number = start2.y - start2.x * leftLineSlope;
  266. const rightLineD: number = end2.y - end2.x * rightLineSlope;
  267. // calculate IntersectionPoint of the 2 Lines
  268. // if same Slope, then Point.X between Start and End and Point.Y fixed
  269. const intersectionPoint: PointF2D = new PointF2D();
  270. let sameSlope: boolean = false;
  271. if (Math.abs(Math.abs(leftLineSlope) - Math.abs(rightLineSlope)) < 0.0001) {
  272. intersectionPoint.x = end2.x / 2;
  273. intersectionPoint.y = 0;
  274. sameSlope = true;
  275. } else {
  276. intersectionPoint.x = (rightLineD - leftLineD) / (leftLineSlope - rightLineSlope);
  277. intersectionPoint.y = leftLineSlope * intersectionPoint.x + leftLineD;
  278. }
  279. // calculate tangent Lines Angles
  280. // (using the calculated Slopes and the Ratio from the IntersectionPoint's distance to the MaxPoint in the SkyLine)
  281. const leftAngle: number = minAngle;
  282. const rightAngle: number = -minAngle;
  283. // if the calculated Slopes (left and right) are equal, then Angles have fixed values
  284. if (!sameSlope) {
  285. this.calculateAngles(leftAngle, rightAngle, leftLineSlope, rightLineSlope, maxAngle);
  286. }
  287. // calculate Curve's Control Points
  288. const controlPoints: {leftControlPoint: PointF2D, rightControlPoint: PointF2D} =
  289. this.calculateControlPoints(end2.x, leftAngle, rightAngle, transformedPoints);
  290. let leftControlPoint: PointF2D = controlPoints.leftControlPoint;
  291. let rightControlPoint: PointF2D = controlPoints.rightControlPoint;
  292. // transform ControlPoints to original Coordinate System
  293. // (rotate back and translate back)
  294. leftControlPoint = transposeMatrix.vectorMultiplication(leftControlPoint);
  295. leftControlPoint.x += startX;
  296. leftControlPoint.y += startY;
  297. rightControlPoint = transposeMatrix.vectorMultiplication(rightControlPoint);
  298. rightControlPoint.x += startX;
  299. rightControlPoint.y += startY;
  300. // set private members
  301. this.bezierStartPt = start;
  302. this.bezierStartControlPt = leftControlPoint;
  303. this.bezierEndControlPt = rightControlPoint;
  304. this.bezierEndPt = end;
  305. /* for DEBUG only */
  306. // this.intersection = transposeMatrix.vectorMultiplication(intersectionPoint);
  307. // this.intersection.x += startX;
  308. // this.intersection.y += startY;
  309. /* for DEBUG only */
  310. // calculate CurvePoints
  311. const length: number = staffLine.BottomLine.length;
  312. const startIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(this.bezierStartPt.x, length);
  313. const endIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(this.bezierEndPt.x, length);
  314. const distance: number = this.bezierEndPt.x - this.bezierStartPt.x;
  315. const samplingUnit: number = skyBottomLineCalculator.SamplingUnit;
  316. for (let i: number = startIndex; i < endIndex; i++) {
  317. // get the right distance ratio and index on the curve
  318. const diff: number = i / samplingUnit - this.bezierStartPt.x;
  319. const curvePoint: PointF2D = this.calculateCurvePointAtIndex(Math.abs(diff) / distance);
  320. // update left- and rightIndex for better accuracy
  321. let index: number = skyBottomLineCalculator.getLeftIndexForPointX(curvePoint.x, length);
  322. // update BottomLine with final slur curve:
  323. if (index >= startIndex) {
  324. staffLine.BottomLine[index] = Math.max(staffLine.BottomLine[index], curvePoint.y);
  325. }
  326. index++;
  327. if (index < length) {
  328. staffLine.BottomLine[index] = Math.max(staffLine.BottomLine[index], curvePoint.y);
  329. }
  330. }
  331. }
  332. }
  333. /**
  334. * This method calculates the Start and End Positions of the Slur Curve.
  335. * @param slurStartNote
  336. * @param slurEndNote
  337. * @param staffLine
  338. * @param startX
  339. * @param startY
  340. * @param endX
  341. * @param endY
  342. * @param rules
  343. * @param skyBottomLineCalculator
  344. */
  345. private calculateStartAndEnd( slurStartNote: GraphicalNote,
  346. slurEndNote: GraphicalNote,
  347. staffLine: StaffLine,
  348. rules: EngravingRules,
  349. skyBottomLineCalculator: SkyBottomLineCalculator): {startX: number, startY: number, endX: number, endY: number} {
  350. let startX: number = 0;
  351. let startY: number = 0;
  352. let endX: number = 0;
  353. let endY: number = 0;
  354. if (slurStartNote !== undefined) {
  355. // must be relative to StaffLine
  356. startX = slurStartNote.PositionAndShape.RelativePosition.x + slurStartNote.parentVoiceEntry.parentStaffEntry.PositionAndShape.RelativePosition.x
  357. + slurStartNote.parentVoiceEntry.parentStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
  358. // If Slur starts on a Gracenote
  359. if (this.graceStart) {
  360. startX += slurStartNote.parentVoiceEntry.parentStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
  361. }
  362. //const first: GraphicalNote = slurStartNote.parentVoiceEntry.notes[0];
  363. // Determine Start/End Point coordinates with the VoiceEntry of the Start/EndNote of the slur
  364. const slurStartVE: GraphicalVoiceEntry = slurStartNote.parentVoiceEntry;
  365. if (this.placement === PlacementEnum.Above) {
  366. startY = slurStartVE.PositionAndShape.RelativePosition.y + slurStartVE.PositionAndShape.BorderTop;
  367. } else {
  368. startY = slurStartVE.PositionAndShape.RelativePosition.y + slurStartVE.PositionAndShape.BorderBottom;
  369. }
  370. // 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
  371. // of the notehead. Note: an exact calculation using the position of the note is too complicate for the payoff
  372. if ( slurStartVE.parentVoiceEntry.StemDirection === StemDirectionType.Down && this.placement === PlacementEnum.Below ) {
  373. startX -= 0.5;
  374. }
  375. if (slurStartVE.parentVoiceEntry.StemDirection === StemDirectionType.Up && this.placement === PlacementEnum.Above) {
  376. startX += 0.5;
  377. }
  378. // if (first.NoteStem !== undefined && first.NoteStem.Direction === StemEnum.StemUp && this.placement === PlacementEnum.Above) {
  379. // startX += first.NoteStem.PositionAndShape.RelativePosition.x;
  380. // startY = skyBottomLineCalculator.getSkyLineMinAtPoint(staffLine, startX);
  381. // } else {
  382. // const last: GraphicalNote = <GraphicalNote>slurStartNote[slurEndNote.parentVoiceEntry.notes.length - 1];
  383. // if (last.NoteStem !== undefined && last.NoteStem.Direction === StemEnum.StemDown && this.placement === PlacementEnum.Below) {
  384. // startX += last.NoteStem.PositionAndShape.RelativePosition.x;
  385. // startY = skyBottomLineCalculator.getBottomLineMaxAtPoint(staffLine, startX);
  386. // } else {
  387. // }
  388. // }
  389. } else {
  390. startX = staffLine.Measures[0].beginInstructionsWidth;
  391. }
  392. if (slurEndNote !== undefined) {
  393. endX = slurEndNote.PositionAndShape.RelativePosition.x + slurEndNote.parentVoiceEntry.parentStaffEntry.PositionAndShape.RelativePosition.x
  394. + slurEndNote.parentVoiceEntry.parentStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
  395. // If Slur ends in a Gracenote
  396. if (this.graceEnd) {
  397. endX += slurEndNote.parentVoiceEntry.parentStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
  398. }
  399. const slurEndVE: GraphicalVoiceEntry = slurEndNote.parentVoiceEntry;
  400. if (this.placement === PlacementEnum.Above) {
  401. endY = slurEndVE.PositionAndShape.RelativePosition.y + slurEndVE.PositionAndShape.BorderTop;
  402. } else {
  403. endY = slurEndVE.PositionAndShape.RelativePosition.y + slurEndVE.PositionAndShape.BorderBottom;
  404. }
  405. // If the stem points towards the endpoint of the slur, shift the slur by a small amount to start (approximately) at the x-position
  406. // of the notehead. Note: an exact calculation using the position of the note is too complicate for the payoff
  407. if ( slurEndVE.parentVoiceEntry.StemDirection === StemDirectionType.Down && this.placement === PlacementEnum.Below ) {
  408. endX -= 0.5;
  409. }
  410. if (slurEndVE.parentVoiceEntry.StemDirection === StemDirectionType.Up && this.placement === PlacementEnum.Above) {
  411. endX += 0.5;
  412. }
  413. // const first: GraphicalNote = <GraphicalNote>slurEndNote.parentVoiceEntry.notes[0];
  414. // if (first.NoteStem !== undefined && first.NoteStem.Direction === StemEnum.StemUp && this.placement === PlacementEnum.Above) {
  415. // endX += first.NoteStem.PositionAndShape.RelativePosition.x;
  416. // endY = skyBottomLineCalculator.getSkyLineMinAtPoint(staffLine, endX);
  417. // } else {
  418. // const last: GraphicalNote = <GraphicalNote>slurEndNote.parentVoiceEntry.notes[slurEndNote.parentVoiceEntry.notes.length - 1];
  419. // if (last.NoteStem !== undefined && last.NoteStem.Direction === StemEnum.StemDown && this.placement === PlacementEnum.Below) {
  420. // endX += last.NoteStem.PositionAndShape.RelativePosition.x;
  421. // endY = skyBottomLineCalculator.getBottomLineMaxAtPoint(staffLine, endX);
  422. // } else {
  423. // if (this.placement === PlacementEnum.Above) {
  424. // const highestNote: GraphicalNote = last;
  425. // endY = highestNote.PositionAndShape.RelativePosition.y;
  426. // if (highestNote.NoteHead !== undefined) {
  427. // endY += highestNote.NoteHead.PositionAndShape.BorderMarginTop;
  428. // } else { endY += highestNote.PositionAndShape.BorderTop; }
  429. // } else {
  430. // const lowestNote: GraphicalNote = first;
  431. // endY = lowestNote.parentVoiceEntry
  432. // lowestNote.PositionAndShape.RelativePosition.y;
  433. // if (lowestNote.NoteHead !== undefined) {
  434. // endY += lowestNote.NoteHead.PositionAndShape.BorderMarginBottom;
  435. // } else { endY += lowestNote.PositionAndShape.BorderBottom; }
  436. // }
  437. // }
  438. // }
  439. } else {
  440. endX = staffLine.PositionAndShape.Size.width;
  441. }
  442. // if GraphicalSlur breaks over System, then the end/start of the curve is at the corresponding height with the known start/end
  443. if (slurStartNote === undefined && slurEndNote === undefined) {
  444. startY = 0;
  445. endY = 0;
  446. }
  447. if (slurStartNote === undefined) {
  448. startY = endY;
  449. }
  450. if (slurEndNote === undefined) {
  451. endY = startY;
  452. }
  453. // if two slurs start/end at the same GraphicalNote, then the second gets an offset
  454. if (this.slur.startNoteHasMoreStartingSlurs() && this.slur.isSlurLonger()) {
  455. if (this.placement === PlacementEnum.Above) {
  456. startY -= rules.SlursStartingAtSameStaffEntryYOffset;
  457. } else { startY += rules.SlursStartingAtSameStaffEntryYOffset; }
  458. }
  459. if (this.slur.endNoteHasMoreEndingSlurs() && this.slur.isSlurLonger()) {
  460. if (this.placement === PlacementEnum.Above) {
  461. endY -= rules.SlursStartingAtSameStaffEntryYOffset;
  462. } else { endY += rules.SlursStartingAtSameStaffEntryYOffset; }
  463. }
  464. return {startX, startY, endX, endY};
  465. }
  466. /**
  467. * This method calculates the placement of the Curve.
  468. * @param skyBottomLineCalculator
  469. * @param staffLine
  470. */
  471. private calculatePlacement(skyBottomLineCalculator: SkyBottomLineCalculator, staffLine: StaffLine): void {
  472. // old version: when lyrics are given place above:
  473. // if ( !this.slur.StartNote.ParentVoiceEntry.LyricsEntries.isEmpty || (this.slur.EndNote !== undefined
  474. // && !this.slur.EndNote.ParentVoiceEntry.LyricsEntries.isEmpty) ) {
  475. // this.placement = PlacementEnum.Above;
  476. // return;
  477. // }
  478. // if any StaffEntry belongs to a Measure with multiple Voices, than
  479. // if Slur's Start- or End-Note belongs to a LinkedVoice Below else Above
  480. for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) {
  481. const graphicalStaffEntry: GraphicalStaffEntry = this.staffEntries[idx];
  482. if (graphicalStaffEntry.parentMeasure.hasMultipleVoices()) {
  483. if (this.slur.StartNote.ParentVoiceEntry.ParentVoice instanceof LinkedVoice ||
  484. this.slur.EndNote.ParentVoiceEntry.ParentVoice instanceof LinkedVoice) {
  485. this.placement = PlacementEnum.Below;
  486. } else { this.placement = PlacementEnum.Above; }
  487. return;
  488. }
  489. }
  490. // when lyrics are given place above:
  491. for (let idx: number = 0, len: number = this.staffEntries.length; idx < len; ++idx) {
  492. const graphicalStaffEntry: GraphicalStaffEntry = this.staffEntries[idx];
  493. if (graphicalStaffEntry.LyricsEntries.length > 0) {
  494. this.placement = PlacementEnum.Above;
  495. return;
  496. }
  497. }
  498. const startStaffEntry: GraphicalStaffEntry = this.staffEntries[0];
  499. const endStaffEntry: GraphicalStaffEntry = this.staffEntries[this.staffEntries.length - 1];
  500. // Deactivated: single Voice, opposite to StemDirection
  501. // if (startStaffEntry.hasStem() && endStaffEntry.hasStem() && startStaffEntry.getStemDirection() === endStaffEntry.getStemDirection()) {
  502. // this.placement = (startStaffEntry.getStemDirection() === StemDirectionType.Up) ? PlacementEnum.Below : PlacementEnum.Above;
  503. // } else {
  504. // Placement at the side with the minimum border
  505. let sX: number = startStaffEntry.PositionAndShape.BorderLeft + startStaffEntry.PositionAndShape.RelativePosition.x
  506. + startStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
  507. let eX: number = endStaffEntry.PositionAndShape.BorderRight + endStaffEntry.PositionAndShape.RelativePosition.x
  508. + endStaffEntry.parentMeasure.PositionAndShape.RelativePosition.x;
  509. if (this.graceStart) {
  510. sX += endStaffEntry.PositionAndShape.RelativePosition.x;
  511. }
  512. if (this.graceEnd) {
  513. eX += endStaffEntry.staffEntryParent.PositionAndShape.RelativePosition.x;
  514. }
  515. // get SkyBottomLine borders
  516. let minAbove: number = skyBottomLineCalculator.getSkyLineMinInRange(sX, eX);
  517. let maxBelow: number = skyBottomLineCalculator.getBottomLineMaxInRange(sX, eX);
  518. // get lowest and highest placed NoteHead
  519. const notesMinY: number = Math.min(startStaffEntry.PositionAndShape.BorderTop,
  520. endStaffEntry.PositionAndShape.BorderTop);
  521. const notesMaxY: number = Math.max(startStaffEntry.PositionAndShape.BorderBottom,
  522. endStaffEntry.PositionAndShape.BorderBottom);
  523. // get lowest and highest placed NoteHead
  524. minAbove = notesMinY - minAbove;
  525. maxBelow = maxBelow - notesMaxY;
  526. if (Math.abs(maxBelow) > Math.abs(minAbove)) {
  527. this.placement = PlacementEnum.Above;
  528. } else { this.placement = PlacementEnum.Below; }
  529. //}
  530. }
  531. /**
  532. * This method calculates the Points between Start- and EndPoint (case above).
  533. * @param start
  534. * @param end
  535. * @param staffLine
  536. * @param skyBottomLineCalculator
  537. */
  538. private calculateTopPoints(start: PointF2D, end: PointF2D, staffLine: StaffLine, skyBottomLineCalculator: SkyBottomLineCalculator): PointF2D[] {
  539. const points: PointF2D[] = [];
  540. let startIndex: number = skyBottomLineCalculator.getRightIndexForPointX(start.x, staffLine.SkyLine.length);
  541. let endIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(end.x, staffLine.SkyLine.length);
  542. if (startIndex < 0) {
  543. startIndex = 0;
  544. }
  545. if (endIndex >= staffLine.SkyLine.length) {
  546. endIndex = staffLine.SkyLine.length - 1;
  547. }
  548. for (let i: number = startIndex; i < endIndex; i++) {
  549. const point: PointF2D = new PointF2D((0.5 + i) / skyBottomLineCalculator.SamplingUnit, staffLine.SkyLine[i]);
  550. points.push(point);
  551. }
  552. return points;
  553. }
  554. /**
  555. * This method calculates the Points between Start- and EndPoint (case below).
  556. * @param start
  557. * @param end
  558. * @param staffLine
  559. * @param skyBottomLineCalculator
  560. */
  561. private calculateBottomPoints(start: PointF2D, end: PointF2D, staffLine: StaffLine, skyBottomLineCalculator: SkyBottomLineCalculator): PointF2D[] {
  562. const points: PointF2D[] = [];
  563. // get BottomLine indices
  564. let startIndex: number = skyBottomLineCalculator.getRightIndexForPointX(start.x, staffLine.BottomLine.length);
  565. let endIndex: number = skyBottomLineCalculator.getLeftIndexForPointX(end.x, staffLine.BottomLine.length);
  566. if (startIndex < 0) {
  567. startIndex = 0;
  568. }
  569. if (endIndex >= staffLine.BottomLine.length) {
  570. endIndex = staffLine.BottomLine.length - 1;
  571. }
  572. for (let i: number = startIndex; i < endIndex; i++) {
  573. const point: PointF2D = new PointF2D((0.5 + i) / skyBottomLineCalculator.SamplingUnit, staffLine.BottomLine[i]);
  574. points.push(point);
  575. }
  576. return points;
  577. }
  578. /**
  579. * This method calculates the maximum slope between StartPoint and BetweenPoints.
  580. * @param points
  581. * @param start
  582. * @param end
  583. */
  584. private calculateMaxLeftSlope(points: PointF2D[], start: PointF2D, end: PointF2D): number {
  585. let slope: number = -Number.MAX_VALUE;
  586. const x: number = start.x;
  587. const y: number = start.y;
  588. for (let i: number = 0; i < points.length; i++) {
  589. if (Math.abs(points[i].y - Number.MAX_VALUE) < 0.0001 || Math.abs(points[i].y - (-Number.MAX_VALUE)) < 0.0001) {
  590. continue;
  591. }
  592. slope = Math.max(slope, (points[i].y - y) / (points[i].x - x));
  593. }
  594. // in case all Points don't have a meaningful value or the slope between Start- and EndPoint is just bigger
  595. slope = Math.max(slope, Math.abs(end.y - y) / (end.x - x));
  596. return slope;
  597. }
  598. /**
  599. * This method calculates the maximum slope between EndPoint and BetweenPoints.
  600. * @param points
  601. * @param start
  602. * @param end
  603. */
  604. private calculateMaxRightSlope(points: PointF2D[], start: PointF2D, end: PointF2D): number {
  605. let slope: number = Number.MAX_VALUE;
  606. const x: number = end.x;
  607. const y: number = end.y;
  608. for (let i: number = 0; i < points.length; i++) {
  609. if (Math.abs(points[i].y - Number.MAX_VALUE) < 0.0001 || Math.abs(points[i].y - (-Number.MAX_VALUE)) < 0.0001) {
  610. continue;
  611. }
  612. slope = Math.min(slope, (y - points[i].y) / (x - points[i].x));
  613. }
  614. // in case no Point has a meaningful value or the slope between Start- and EndPoint is just smaller
  615. slope = Math.min(slope, (y - start.y) / (x - start.x));
  616. return slope;
  617. }
  618. /**
  619. * This method returns the maximum (meaningful) points.Y.
  620. * @param points
  621. */
  622. private getPointListMaxY(points: PointF2D[]): number {
  623. let max: number = -Number.MAX_VALUE;
  624. for (let idx: number = 0, len: number = points.length; idx < len; ++idx) {
  625. const point: PointF2D = points[idx];
  626. if (Math.abs(point.y - (-Number.MAX_VALUE)) < 0.0001 || Math.abs(point.y - Number.MAX_VALUE) < 0.0001) {
  627. continue;
  628. }
  629. max = Math.max(max, point.y);
  630. }
  631. return max;
  632. }
  633. /**
  634. * This method calculates the translated and rotated PointsList (case above).
  635. * @param points
  636. * @param startX
  637. * @param startY
  638. * @param rotationMatrix
  639. */
  640. private calculateTranslatedAndRotatedPointListAbove(points: PointF2D[], startX: number, startY: number, rotationMatrix: Matrix2D): PointF2D[] {
  641. const transformedPoints: PointF2D[] = [];
  642. for (let i: number = 0; i < points.length; i++) {
  643. if (Math.abs(points[i].y - Number.MAX_VALUE) < 0.0001 || Math.abs(points[i].y - (-Number.MAX_VALUE)) < 0.0001) {
  644. continue;
  645. }
  646. let point: PointF2D = new PointF2D(points[i].x - startX, -(points[i].y - startY));
  647. point = rotationMatrix.vectorMultiplication(point);
  648. transformedPoints.push(point);
  649. }
  650. return transformedPoints;
  651. }
  652. /**
  653. * This method calculates the translated and rotated PointsList (case below).
  654. * @param points
  655. * @param startX
  656. * @param startY
  657. * @param rotationMatrix
  658. */
  659. private calculateTranslatedAndRotatedPointListBelow(points: PointF2D[], startX: number, startY: number, rotationMatrix: Matrix2D): PointF2D[] {
  660. const transformedPoints: PointF2D[] = [];
  661. for (let i: number = 0; i < points.length; i++) {
  662. if (Math.abs(points[i].y - Number.MAX_VALUE) < 0.0001 || Math.abs(points[i].y - (-Number.MAX_VALUE)) < 0.0001) {
  663. continue;
  664. }
  665. let point: PointF2D = new PointF2D(points[i].x - startX, points[i].y - startY);
  666. point = rotationMatrix.vectorMultiplication(point);
  667. transformedPoints.push(point);
  668. }
  669. return transformedPoints;
  670. }
  671. /**
  672. * This method calculates the HeightWidthRatio between the MaxYpoint (from the points between StartPoint and EndPoint)
  673. * and the X-distance from StartPoint to EndPoint.
  674. * @param endX
  675. * @param points
  676. */
  677. private calculateHeightWidthRatio(endX: number, points: PointF2D[]): number {
  678. if (points.length === 0) {
  679. return 0;
  680. }
  681. // in case of negative points
  682. const max: number = Math.max(0, this.getPointListMaxY(points));
  683. return max / endX;
  684. }
  685. /**
  686. * This method calculates the 2 ControlPoints of the SlurCurve.
  687. * @param endX
  688. * @param leftAngle
  689. * @param rightAngle
  690. * @param points
  691. */
  692. private calculateControlPoints(endX: number,
  693. leftAngle: number, rightAngle: number, points: PointF2D[]): { leftControlPoint: PointF2D, rightControlPoint: PointF2D } {
  694. // calculate HeightWidthRatio between the MaxYpoint (from the points between StartPoint and EndPoint)
  695. // and the X-distance from StartPoint to EndPoint
  696. // use this HeightWidthRatio to get a "normalized" Factor (based on tested parameters)
  697. // this Factor denotes the Length of the TangentLine of the Curve (a proportion of the X-distance from StartPoint to EndPoint)
  698. // finally from this Length and the calculated Angles we get the coordinates of the Control Points
  699. const heightWidthRatio: number = this.calculateHeightWidthRatio(endX, points);
  700. const factor: number = GraphicalSlur.k * heightWidthRatio + GraphicalSlur.d;
  701. const relativeLength: number = endX * factor;
  702. const leftControlPoint: PointF2D = new PointF2D();
  703. leftControlPoint.x = relativeLength * Math.cos(leftAngle * GraphicalSlur.degreesToRadiansFactor);
  704. leftControlPoint.y = relativeLength * Math.sin(leftAngle * GraphicalSlur.degreesToRadiansFactor);
  705. const rightControlPoint: PointF2D = new PointF2D();
  706. rightControlPoint.x = endX - (relativeLength * Math.cos(rightAngle * GraphicalSlur.degreesToRadiansFactor));
  707. rightControlPoint.y = -(relativeLength * Math.sin(rightAngle * GraphicalSlur.degreesToRadiansFactor));
  708. return {leftControlPoint, rightControlPoint};
  709. }
  710. /**
  711. * This method calculates the angles for the Curve's Tangent Lines.
  712. * @param leftAngle
  713. * @param rightAngle
  714. * @param leftLineSlope
  715. * @param rightLineSlope
  716. * @param maxAngle
  717. */
  718. private calculateAngles(leftAngle: number, rightAngle: number, leftLineSlope: number, rightLineSlope: number, maxAngle: number): void {
  719. // calculate Angles from the calculated Slopes, adding also a given angle
  720. const angle: number = 20;
  721. let calculatedLeftAngle: number = Math.atan(leftLineSlope) / GraphicalSlur.degreesToRadiansFactor;
  722. if (leftLineSlope > 0) {
  723. calculatedLeftAngle += angle;
  724. } else {
  725. calculatedLeftAngle -= angle;
  726. }
  727. let calculatedRightAngle: number = Math.atan(rightLineSlope) / GraphicalSlur.degreesToRadiansFactor;
  728. if (rightLineSlope < 0) {
  729. calculatedRightAngle -= angle;
  730. } else {
  731. calculatedRightAngle += angle;
  732. }
  733. // +/- 80 is the max/min allowed Angle
  734. leftAngle = Math.min(Math.max(leftAngle, calculatedLeftAngle), maxAngle);
  735. rightAngle = Math.max(Math.min(rightAngle, calculatedRightAngle), -maxAngle);
  736. }
  737. private static degreesToRadiansFactor: number = Math.PI / 180;
  738. private static k: number = 0.9;
  739. private static d: number = 0.2;
  740. }