GraphicalSlur.ts 42 KB

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