SkyBottomLineCalculator.ts 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565
  1. /* tslint:disable no-unused-variable */
  2. //FIXME: Enble tslint again when all functions are implemented and in use!
  3. import { EngravingRules } from "./EngravingRules";
  4. import { StaffLine } from "./StaffLine";
  5. import { PointF2D } from "../../Common/DataObjects/PointF2D";
  6. import { CanvasVexFlowBackend } from "./VexFlow/CanvasVexFlowBackend";
  7. import { VexFlowMeasure } from "./VexFlow/VexFlowMeasure";
  8. import { unitInPixels } from "./VexFlow/VexFlowMusicSheetDrawer";
  9. import * as log from "loglevel";
  10. import { BoundingBox } from "./BoundingBox";
  11. /**
  12. * This class calculates and holds the skyline and bottom line information.
  13. * It also has functions to update areas of the two lines if new elements are
  14. * added to the staffline (e.g. measure number, annotations, ...)
  15. */
  16. export class SkyBottomLineCalculator {
  17. /** Parent Staffline where the skyline and bottom line is attached */
  18. private mStaffLineParent: StaffLine;
  19. /** Internal array for the skyline */
  20. private mSkyLine: number[];
  21. /** Internal array for the bottomline */
  22. private mBottomLine: number[];
  23. /** Engraving rules for formatting */
  24. private mRules: EngravingRules;
  25. /**
  26. * Create a new object of the calculator
  27. * @param staffLineParent staffline where the calculator should be attached
  28. */
  29. constructor(staffLineParent: StaffLine) {
  30. this.mStaffLineParent = staffLineParent;
  31. this.mRules = EngravingRules.Rules;
  32. }
  33. /**
  34. * This method calculates the Sky- and BottomLines for a StaffLine.
  35. */
  36. public calculateLines(): void {
  37. // calculate arrayLength
  38. const arrayLength: number = Math.max(Math.ceil(this.StaffLineParent.PositionAndShape.Size.width * this.SamplingUnit), 1);
  39. this.mSkyLine = [];
  40. this.mBottomLine = [];
  41. // Create a temporary canvas outside the DOM to draw the measure in.
  42. const tmpCanvas: any = new CanvasVexFlowBackend();
  43. // search through all Measures
  44. for (const measure of this.StaffLineParent.Measures as VexFlowMeasure[]) {
  45. // must calculate first AbsolutePositions
  46. measure.PositionAndShape.calculateAbsolutePositionsRecursive(0, 0);
  47. // Pre initialize and get stuff for more performance
  48. const vsStaff: any = measure.getVFStave();
  49. // Headless because we are outside the DOM
  50. tmpCanvas.initializeHeadless(vsStaff.getWidth());
  51. const ctx: any = tmpCanvas.getContext();
  52. const canvas: any = tmpCanvas.getCanvas();
  53. const width: number = canvas.width;
  54. const height: number = canvas.height;
  55. // This magic number is an offset from the top image border so that
  56. // elements above the staffline can be drawn correctly.
  57. vsStaff.setY(vsStaff.y + 100);
  58. const oldMeasureWidth: number = vsStaff.getWidth();
  59. // We need to tell the VexFlow stave about the canvas width. This looks
  60. // redundant because it should know the canvas but somehow it doesn't.
  61. // Maybe I am overlooking something but for no this does the trick
  62. vsStaff.setWidth(width);
  63. measure.format();
  64. vsStaff.setWidth(oldMeasureWidth);
  65. measure.draw(ctx);
  66. // imageData.data is a Uint8ClampedArray representing a one-dimensional array containing the data in the RGBA order
  67. // RGBA is 32 bit word with 8 bits red, 8 bits green, 8 bits blue and 8 bit alpha. Alpha should be 0 for all background colors.
  68. // Since we are only interested in black or white we can take 32bit words at once
  69. const imageData: any = ctx.getImageData(0, 0, width, height);
  70. const rgbaLength: number = 4;
  71. const measureArrayLength: number = Math.max(Math.ceil(measure.PositionAndShape.Size.width * this.mRules.SamplingUnit), 1);
  72. const tmpSkyLine: number[] = new Array(measureArrayLength);
  73. const tmpBottomLine: number[] = new Array(measureArrayLength);
  74. for (let x: number = 0; x < width; x++) {
  75. // SkyLine
  76. for (let y: number = 0; y < height; y++) {
  77. const yOffset: number = y * width * rgbaLength;
  78. const bufIndex: number = yOffset + x * rgbaLength;
  79. const alpha: number = imageData.data[bufIndex + 3];
  80. if (alpha > 0) {
  81. tmpSkyLine[x] = y;
  82. break;
  83. }
  84. }
  85. // BottomLine
  86. for (let y: number = height; y > 0; y--) {
  87. const yOffset: number = y * width * rgbaLength;
  88. const bufIndex: number = yOffset + x * rgbaLength;
  89. const alpha: number = imageData.data[bufIndex + 3];
  90. if (alpha > 0) {
  91. tmpBottomLine[x] = y;
  92. break;
  93. }
  94. }
  95. }
  96. this.mSkyLine.push(...tmpSkyLine);
  97. this.mBottomLine.push(...tmpBottomLine);
  98. // Set to true to only show the "mini canvases" and the corresponding skylines
  99. const debugTmpCanvas: boolean = false;
  100. if (debugTmpCanvas) {
  101. tmpSkyLine.forEach((y, x) => this.drawPixel(new PointF2D(x, y), tmpCanvas));
  102. tmpBottomLine.forEach((y, x) => this.drawPixel(new PointF2D(x, y), tmpCanvas, "blue"));
  103. const img: any = canvas.toDataURL("image/png");
  104. document.write('<img src="' + img + '"/>');
  105. }
  106. tmpCanvas.clear();
  107. }
  108. // Subsampling:
  109. // The pixel width is bigger then the measure size in units. So we split the array into
  110. // chunks with the size of MeasurePixelWidth/measureUnitWidth and reduce the value to its
  111. // average
  112. const arrayChunkSize: number = this.mSkyLine.length / arrayLength;
  113. const subSampledSkyLine: number[] = [];
  114. const subSampledBottomLine: number[] = [];
  115. for (let chunkIndex: number = 0; chunkIndex < this.mSkyLine.length; chunkIndex += arrayChunkSize) {
  116. let chunk: number[] = this.mSkyLine.slice(chunkIndex, chunkIndex + arrayChunkSize);
  117. subSampledSkyLine.push(Math.min(...chunk));
  118. chunk = this.mBottomLine.slice(chunkIndex, chunkIndex + arrayChunkSize);
  119. subSampledBottomLine.push(Math.max(...chunk));
  120. }
  121. this.mSkyLine = subSampledSkyLine;
  122. this.mBottomLine = subSampledBottomLine;
  123. if (this.mSkyLine.length !== arrayLength) {
  124. log.debug(`SkyLine calculation was not correct (${this.mSkyLine.length} instead of ${arrayLength})`);
  125. }
  126. if (this.mBottomLine.length !== arrayLength) {
  127. log.debug(`BottomLine calculation was not correct (${this.mBottomLine.length} instead of ${arrayLength})`);
  128. }
  129. // Remap the values from 0 to +/- height in units
  130. this.mSkyLine = this.mSkyLine.map(v => (v - Math.max(...this.mSkyLine)) / unitInPixels);
  131. this.mBottomLine = this.mBottomLine.map(v => (v - Math.min(...this.mBottomLine)) / unitInPixels + this.mRules.StaffHeight);
  132. }
  133. /**
  134. * Debugging drawing function that can draw single pixels
  135. * @param coord Point to draw to
  136. * @param backend the backend to be used
  137. * @param color the color to be used, default is red
  138. */
  139. private drawPixel(coord: PointF2D, backend: CanvasVexFlowBackend, color: string = "#FF0000FF"): void {
  140. const ctx: any = backend.getContext();
  141. const oldStyle: string = ctx.fillStyle;
  142. ctx.fillStyle = color;
  143. ctx.fillRect(coord.x, coord.y, 2, 2);
  144. ctx.fillStyle = oldStyle;
  145. }
  146. /**
  147. * This method updates the SkyLine for a given Wedge.
  148. * @param start Start point of the wedge
  149. * @param end End point of the wedge
  150. */
  151. public updateSkyLineWithWedge(start: PointF2D, end: PointF2D): void {
  152. // FIXME: Refactor if wedges will be added. Current status is that vexflow will be used for this
  153. let startIndex: number = Math.floor(start.x * this.SamplingUnit);
  154. let endIndex: number = Math.ceil(end.x * this.SamplingUnit);
  155. let slope: number = (end.y - start.y) / (end.x - start.x);
  156. if (endIndex - startIndex <= 1) {
  157. endIndex++;
  158. slope = 0;
  159. }
  160. if (startIndex < 0) {
  161. startIndex = 0;
  162. }
  163. if (startIndex >= this.BottomLine.length) {
  164. startIndex = this.BottomLine.length - 1;
  165. }
  166. if (endIndex < 0) {
  167. endIndex = 0;
  168. }
  169. if (endIndex >= this.BottomLine.length) {
  170. endIndex = this.BottomLine.length;
  171. }
  172. this.SkyLine[startIndex] = start.y;
  173. for (let i: number = startIndex + 1; i < Math.min(endIndex, this.SkyLine.length); i++) {
  174. this.SkyLine[i] = this.SkyLine[i - 1] + slope / this.SamplingUnit;
  175. }
  176. }
  177. /**
  178. * This method updates the BottomLine for a given Wedge.
  179. * @param start Start point of the wedge
  180. * @param end End point of the wedge
  181. */
  182. public updateBottomLineWithWedge(start: PointF2D, end: PointF2D): void {
  183. // FIXME: Refactor if wedges will be added. Current status is that vexflow will be used for this
  184. let startIndex: number = Math.floor(start.x * this.SamplingUnit);
  185. let endIndex: number = Math.ceil(end.x * this.SamplingUnit);
  186. let slope: number = (end.y - start.y) / (end.x - start.x);
  187. if (endIndex - startIndex <= 1) {
  188. endIndex++;
  189. slope = 0;
  190. }
  191. if (startIndex < 0) {
  192. startIndex = 0;
  193. }
  194. if (startIndex >= this.BottomLine.length) {
  195. startIndex = this.BottomLine.length - 1;
  196. }
  197. if (endIndex < 0) {
  198. endIndex = 0;
  199. }
  200. if (endIndex >= this.BottomLine.length) {
  201. endIndex = this.BottomLine.length;
  202. }
  203. this.BottomLine[startIndex] = start.y;
  204. for (let i: number = startIndex + 1; i < endIndex; i++) {
  205. this.BottomLine[i] = this.BottomLine[i - 1] + slope / this.SamplingUnit;
  206. }
  207. }
  208. /**
  209. * This method updates the SkyLine for a given range with a given value
  210. * //param to update the SkyLine for
  211. * @param start Start index of the range
  212. * @param end End index of the range
  213. * @param value ??
  214. */
  215. public updateSkyLineInRange(startIndex: number, endIndex: number, value: number): void {
  216. this.updateInRange(this.mSkyLine, startIndex, endIndex, value);
  217. }
  218. /**
  219. * This method updates the BottomLine for a given range with a given value
  220. * @param to update the BottomLine for
  221. * @param start Start index of the range
  222. * @param end End index of the range
  223. * @param value ??
  224. */
  225. public updateBottomLineInRange(startIndex: number, endIndex: number, value: number): void {
  226. this.updateInRange(this.BottomLine, startIndex, endIndex, value);
  227. }
  228. /**
  229. * Resets a SkyLine in a range to its original value
  230. * @param to reset the SkyLine in
  231. * @param startIndex Start index of the range
  232. * @param endIndex End index of the range
  233. */
  234. public resetSkyLineInRange(startIndex: number, endIndex: number): void {
  235. this.updateInRange(this.SkyLine, startIndex, endIndex);
  236. }
  237. /**
  238. * Resets a bottom line in a range to its original value
  239. * @param to reset the bottomline in
  240. * @param startIndex Start index of the range
  241. * @param endIndex End index of the range
  242. */
  243. public resetBottomLineInRange(startIndex: number, endIndex: number): void {
  244. this.setInRange(this.BottomLine, startIndex, endIndex);
  245. }
  246. /**
  247. * Update the whole skyline with a certain value
  248. * @param value value to be set
  249. */
  250. public setSkyLineWithValue(value: number): void {
  251. this.SkyLine.forEach(sl => sl = value);
  252. }
  253. /**
  254. * Update the whole bottomline with a certain value
  255. * @param value value to be set
  256. */
  257. public setBottomLineWithValue(value: number): void {
  258. this.BottomLine.forEach(bl => bl = value);
  259. }
  260. public getLeftIndexForPointX(x: number, length: number): number {
  261. const index: number = Math.floor(x * this.SamplingUnit);
  262. if (index < 0) {
  263. return 0;
  264. }
  265. if (index >= length) {
  266. return length - 1;
  267. }
  268. return index;
  269. }
  270. public getRightIndexForPointX(x: number, length: number): number {
  271. const index: number = Math.ceil(x * this.SamplingUnit);
  272. if (index < 0) {
  273. return 0;
  274. }
  275. if (index >= length) {
  276. return length - 1;
  277. }
  278. return index;
  279. }
  280. /**
  281. * This method updates the StaffLine Borders with the Sky- and BottomLines Min- and MaxValues.
  282. */
  283. public updateStaffLineBorders(): void {
  284. this.mStaffLineParent.PositionAndShape.BorderTop = this.getSkyLineMin();
  285. this.mStaffLineParent.PositionAndShape.BorderMarginTop = this.getSkyLineMin();
  286. this.mStaffLineParent.PositionAndShape.BorderBottom = this.getBottomLineMax();
  287. this.mStaffLineParent.PositionAndShape.BorderMarginBottom = this.getBottomLineMax();
  288. }
  289. /**
  290. * This method finds the minimum value of the SkyLine.
  291. * @param staffLine StaffLine to apply to
  292. */
  293. public getSkyLineMin(): number {
  294. return Math.min(...this.SkyLine.filter(s => !isNaN(s)));
  295. }
  296. public getSkyLineMinAtPoint(point: number): number {
  297. const index: number = Math.round(point * this.SamplingUnit);
  298. return this.mSkyLine[index];
  299. }
  300. /**
  301. * This method finds the SkyLine's minimum value within a given range.
  302. * @param staffLine Staffline to apply to
  303. * @param startIndex Starting index
  304. * @param endIndex End index
  305. */
  306. public getSkyLineMinInRange(startIndex: number, endIndex: number): number {
  307. return this.getMinInRange(this.SkyLine, startIndex, endIndex);
  308. }
  309. /**
  310. * This method finds the maximum value of the BottomLine.
  311. * @param staffLine Staffline to apply to
  312. */
  313. public getBottomLineMax(): number {
  314. return Math.max(...this.BottomLine.filter(s => !isNaN(s)));
  315. }
  316. public getBottomLineMaxAtPoint(point: number): number {
  317. const index: number = Math.round(point * this.SamplingUnit);
  318. return this.mBottomLine[index];
  319. }
  320. /**
  321. * This method finds the BottomLine's maximum value within a given range.
  322. * @param staffLine Staffline to find the max value in
  323. * @param startIndex Start index of the range
  324. * @param endIndex End index of the range
  325. */
  326. public getBottomLineMaxInRange(startIndex: number, endIndex: number): number {
  327. return this.getMaxInRange(this.BottomLine, startIndex, endIndex);
  328. }
  329. /**
  330. * This method returns the maximum value of the bottom line around a specific
  331. * bounding box. Will return undefined if the bounding box is not valid or inside staffline
  332. * @param boundingBox Bounding box where the maximum should be retrieved from
  333. * @returns Maximum value inside bounding box boundaries or undefined if not possible
  334. */
  335. public getBottomLineMaxInBoundingBox(boundingBox: BoundingBox): number {
  336. //TODO: Actually it should be the margin. But that one is not implemented
  337. const startPoint: number = Math.floor(boundingBox.AbsolutePosition.x + boundingBox.BorderLeft);
  338. const endPoint: number = Math.ceil(boundingBox.AbsolutePosition.x + boundingBox.BorderRight);
  339. return this.getMaxInRange(this.mBottomLine, startPoint, endPoint);
  340. }
  341. //#region Private methods
  342. /**
  343. * Updates sky- and bottom line with a boundingBox and it's children
  344. * @param boundingBox Bounding box to be added
  345. * @param topBorder top
  346. */
  347. public updateWithBoundingBoxRecursivly(boundingBox: BoundingBox): void {
  348. if (boundingBox.ChildElements && boundingBox.ChildElements.length > 0) {
  349. this.updateWithBoundingBoxRecursivly(boundingBox);
  350. } else {
  351. const currentTopBorder: number = boundingBox.BorderTop + boundingBox.AbsolutePosition.y;
  352. const currentBottomBorder: number = boundingBox.BorderBottom + boundingBox.AbsolutePosition.y;
  353. if (currentTopBorder < 0) {
  354. const startPoint: number = Math.floor(boundingBox.AbsolutePosition.x + boundingBox.BorderLeft);
  355. const endPoint: number = Math.ceil(boundingBox.AbsolutePosition.x + boundingBox.BorderRight) ;
  356. this.updateInRange(this.mSkyLine, startPoint, endPoint, currentTopBorder);
  357. } else if (currentBottomBorder > this.mRules.StaffHeight) {
  358. const startPoint: number = Math.floor(boundingBox.AbsolutePosition.x + boundingBox.BorderLeft);
  359. const endPoint: number = Math.ceil(boundingBox.AbsolutePosition.x + boundingBox.BorderRight);
  360. this.updateInRange(this.mBottomLine, startPoint, endPoint, currentBottomBorder);
  361. }
  362. }
  363. }
  364. /**
  365. * Update an array with the value given inside a range. NOTE: will only be updated if value > oldValue
  366. * @param array Array to fill in the new value
  367. * @param startIndex start index to begin with (default: 0)
  368. * @param endIndex end index of array (default: array length)
  369. * @param value value to fill in (default: 0)
  370. */
  371. private updateInRange(array: number[], startIndex: number = 0, endIndex: number = array.length, value: number = 0): void {
  372. startIndex = Math.floor(startIndex * this.SamplingUnit);
  373. endIndex = Math.ceil(endIndex * this.SamplingUnit);
  374. if (endIndex < startIndex) {
  375. throw new Error("start index of line is greater then the end index");
  376. }
  377. if (startIndex < 0) {
  378. startIndex = 0;
  379. }
  380. if (endIndex > array.length) {
  381. endIndex = array.length;
  382. }
  383. for (let i: number = startIndex; i < endIndex; i++) {
  384. array[i] = Math.abs(value) > Math.abs(array[i]) ? value : array[i];
  385. }
  386. }
  387. /**
  388. * Sets the value given to the range inside the array. NOTE: will always update the value
  389. * @param array Array to fill in the new value
  390. * @param startIndex start index to begin with (default: 0)
  391. * @param endIndex end index of array (default: array length)
  392. * @param value value to fill in (default: 0)
  393. */
  394. private setInRange(array: number[], startIndex: number = 0, endIndex: number = array.length, value: number = 0): void {
  395. startIndex = Math.floor(startIndex * this.SamplingUnit);
  396. endIndex = Math.ceil(endIndex * this.SamplingUnit);
  397. if (endIndex < startIndex) {
  398. throw new Error("start index of line is greater then the end index");
  399. }
  400. if (startIndex < 0) {
  401. startIndex = 0;
  402. }
  403. if (endIndex > array.length) {
  404. endIndex = array.length;
  405. }
  406. for (let i: number = startIndex; i < endIndex; i++) {
  407. array[i] = value;
  408. }
  409. }
  410. /**
  411. * Get all values of the selected line inside the given range
  412. * @param skyBottomArray Skyline or bottom line
  413. * @param startIndex start index
  414. * @param endIndex end index
  415. */
  416. private getMinInRange(skyBottomArray: number[], startIndex: number, endIndex: number): number {
  417. startIndex = Math.floor(startIndex * this.SamplingUnit);
  418. endIndex = Math.ceil(endIndex * this.SamplingUnit);
  419. if (skyBottomArray === undefined) {
  420. // Highly questionable
  421. return Number.MAX_VALUE;
  422. }
  423. if (startIndex < 0) {
  424. startIndex = 0;
  425. }
  426. if (startIndex >= skyBottomArray.length) {
  427. startIndex = skyBottomArray.length - 1;
  428. }
  429. if (endIndex < 0) {
  430. endIndex = 0;
  431. }
  432. if (endIndex >= skyBottomArray.length) {
  433. endIndex = skyBottomArray.length;
  434. }
  435. if (startIndex >= 0 && endIndex <= skyBottomArray.length) {
  436. return Math.min(...skyBottomArray.slice(startIndex, endIndex));
  437. }
  438. }
  439. /**
  440. * Get the maximum value inside the given indices
  441. * @param skyBottomArray Skyline or bottom line
  442. * @param startIndex start index
  443. * @param endIndex end index
  444. */
  445. private getMaxInRange(skyBottomArray: number[], startIndex: number, endIndex: number): number {
  446. startIndex = Math.floor(startIndex * this.SamplingUnit);
  447. endIndex = Math.ceil(endIndex * this.SamplingUnit);
  448. if (skyBottomArray === undefined) {
  449. // Highly questionable
  450. return Number.MIN_VALUE;
  451. }
  452. if (startIndex < 0) {
  453. startIndex = 0;
  454. }
  455. if (startIndex >= skyBottomArray.length) {
  456. startIndex = skyBottomArray.length - 1;
  457. }
  458. if (endIndex < 0) {
  459. endIndex = 0;
  460. }
  461. if (endIndex >= skyBottomArray.length) {
  462. endIndex = skyBottomArray.length;
  463. }
  464. if (startIndex >= 0 && endIndex <= skyBottomArray.length) {
  465. return Math.max(...skyBottomArray.slice(startIndex, endIndex));
  466. }
  467. }
  468. // FIXME: What does this do here?
  469. // private isStaffLineUpper(): boolean {
  470. // const instrument: Instrument = this.StaffLineParent.ParentStaff.ParentInstrument;
  471. // if (this.StaffLineParent.ParentStaff === instrument.Staves[0]) {
  472. // return true;
  473. // } else {
  474. // return false;
  475. // }
  476. // }
  477. // #endregion
  478. //#region Getter Setter
  479. /** Sampling units that are used to quantize the sky and bottom line */
  480. get SamplingUnit(): number {
  481. return this.mRules.SamplingUnit;
  482. }
  483. /** Parent staffline where the skybottomline calculator is attached to */
  484. get StaffLineParent(): StaffLine {
  485. return this.mStaffLineParent;
  486. }
  487. /** Get the plain skyline array */
  488. get SkyLine(): number[] {
  489. return this.mSkyLine;
  490. }
  491. /** Get the plain bottomline array */
  492. get BottomLine(): number[] {
  493. return this.mBottomLine;
  494. }
  495. //#endregion
  496. }