MusicSheetDrawer.ts 42 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877
  1. import {EngravingRules} from "./EngravingRules";
  2. import {ITextMeasurer} from "../Interfaces/ITextMeasurer";
  3. import {GraphicalMusicSheet} from "./GraphicalMusicSheet";
  4. import {BoundingBox} from "./BoundingBox";
  5. import {GraphicalLayers, OutlineAndFillStyleEnum} from "./DrawingEnums";
  6. import {DrawingParameters} from "./DrawingParameters";
  7. import {GraphicalLine} from "./GraphicalLine";
  8. import {RectangleF2D} from "../../Common/DataObjects/RectangleF2D";
  9. import {PointF2D} from "../../Common/DataObjects/PointF2D";
  10. import {GraphicalRectangle} from "./GraphicalRectangle";
  11. import {GraphicalLabel} from "./GraphicalLabel";
  12. import {Label} from "../Label";
  13. import { isSpecialMark, isTopFont } from "../../Common/Daya/speed-tag";
  14. import {TextAlignmentEnum} from "../../Common/Enums/TextAlignment";
  15. import {ArgumentOutOfRangeException} from "../Exceptions";
  16. import {SelectionStartSymbol} from "./SelectionStartSymbol";
  17. import {SelectionEndSymbol} from "./SelectionEndSymbol";
  18. import {MusicSystem} from "./MusicSystem";
  19. import {GraphicalMeasure} from "./GraphicalMeasure";
  20. import {StaffLine} from "./StaffLine";
  21. import {SystemLine} from "./SystemLine";
  22. import {MusicSymbol} from "./MusicSymbol";
  23. import {GraphicalMusicPage} from "./GraphicalMusicPage";
  24. import {Instrument} from "../Instrument";
  25. import {MusicSymbolDrawingStyle, PhonicScoreModes} from "./DrawingMode";
  26. import {GraphicalObject} from "./GraphicalObject";
  27. import { GraphicalInstantaneousDynamicExpression } from "./GraphicalInstantaneousDynamicExpression";
  28. import { GraphicalContinuousDynamicExpression } from "./GraphicalContinuousDynamicExpression";
  29. import { VexFlowInstrumentBracket } from "./VexFlow";
  30. import { GraphicalNote } from "./GraphicalNote";
  31. // import { FontStyles } from "../../Common/Enums/FontStyles";
  32. export class LabelRenderSpecs {
  33. public BitmapWidth: number;
  34. public BitmapHeight: number;
  35. public FontHeightInPixel: number;
  36. public ScreenPosition: PointF2D;
  37. }
  38. /**
  39. * Draw a [[GraphicalMusicSheet]] (through the .drawSheet method)
  40. *
  41. * The drawing is implemented with a top-down approach, starting from a music sheet, going through pages, systems, staffs...
  42. * ... and ending in notes, beams, accidentals and other symbols.
  43. * It's worth to say, that this class just draws the symbols and graphical elements, using the positions that have been computed before.
  44. * But in any case, some of these previous positioning algorithms need the sizes of the concrete symbols (NoteHeads, sharps, flats, keys...).
  45. * Therefore, there are some static functions on the 'Bounding Boxes' section used to compute these symbol boxes at the
  46. * beginning for the later use in positioning algorithms.
  47. *
  48. * This class also includes the resizing and positioning of the symbols due to user interaction like zooming or panning.
  49. */
  50. export abstract class MusicSheetDrawer {
  51. public drawingParameters: DrawingParameters;
  52. public splitScreenLineColor: number;
  53. public midiPlaybackAvailable: boolean;
  54. public drawableBoundingBoxElement: string = "None"; // process.env.DRAW_BOUNDING_BOX_ELEMENT;
  55. public skyLineVisible: boolean = false;
  56. public bottomLineVisible: boolean = false;
  57. protected rules: EngravingRules;
  58. protected graphicalMusicSheet: GraphicalMusicSheet;
  59. protected textMeasurer: ITextMeasurer;
  60. private phonicScoreMode: PhonicScoreModes = PhonicScoreModes.Manual;
  61. constructor(textMeasurer: ITextMeasurer,
  62. drawingParameters: DrawingParameters) {
  63. this.textMeasurer = textMeasurer;
  64. this.splitScreenLineColor = -1;
  65. this.drawingParameters = drawingParameters;
  66. this.rules = drawingParameters.Rules;
  67. }
  68. public set Mode(value: PhonicScoreModes) {
  69. this.phonicScoreMode = value;
  70. }
  71. public drawSheet(graphicalMusicSheet: GraphicalMusicSheet): void {
  72. this.graphicalMusicSheet = graphicalMusicSheet;
  73. this.rules = graphicalMusicSheet.ParentMusicSheet.Rules;
  74. this.drawSplitScreenLine();
  75. if (this.drawingParameters.drawCursors) {
  76. for (const line of graphicalMusicSheet.Cursors) {
  77. if (!line) {
  78. // TODO GraphicalMusicSheet.calculateCursorLineAtTimestamp() can return undefined.
  79. // why does this happen in the VexFlowMusicSheetDrawer_Test? (it("draws cursor..."))
  80. continue;
  81. }
  82. const psi: BoundingBox = new BoundingBox(line);
  83. psi.AbsolutePosition = line.Start;
  84. psi.BorderBottom = line.End.y - line.Start.y;
  85. psi.BorderRight = line.Width / 2.0;
  86. psi.BorderLeft = -line.Width / 2.0;
  87. if (this.isVisible(psi)) {
  88. this.drawLineAsVerticalRectangle(line, <number>GraphicalLayers.Cursor);
  89. }
  90. }
  91. }
  92. // Draw the vertical ScrollIndicator
  93. if (this.drawingParameters.drawScrollIndicator) {
  94. this.drawScrollIndicator();
  95. }
  96. // Draw the pages
  97. const pagesToDraw: number = Math.min(this.graphicalMusicSheet.MusicPages.length, this.rules.MaxPageToDrawNumber);
  98. for (let i: number = 0; i < pagesToDraw; i ++) {
  99. const page: GraphicalMusicPage = this.graphicalMusicSheet.MusicPages[i];
  100. this.drawPage(page);
  101. }
  102. }
  103. public drawLineAsHorizontalRectangle(line: GraphicalLine, layer: number): void {
  104. let rectangle: RectangleF2D = new RectangleF2D(line.Start.x, line.End.y - line.Width / 2, line.End.x - line.Start.x, line.Width);
  105. rectangle = this.applyScreenTransformationForRect(rectangle);
  106. this.renderRectangle(rectangle, layer, line.styleId, line.colorHex);
  107. }
  108. public drawLineAsVerticalRectangle(line: GraphicalLine, layer: number): void {
  109. const lineStart: PointF2D = line.Start;
  110. const lineWidth: number = line.Width;
  111. let rectangle: RectangleF2D = new RectangleF2D(lineStart.x - lineWidth / 2, lineStart.y, lineWidth, line.End.y - lineStart.y);
  112. rectangle = this.applyScreenTransformationForRect(rectangle);
  113. this.renderRectangle(rectangle, layer, line.styleId);
  114. }
  115. public drawLineAsHorizontalRectangleWithOffset(line: GraphicalLine, offset: PointF2D, layer: number): void {
  116. const start: PointF2D = new PointF2D(line.Start.x + offset.x, line.Start.y + offset.y);
  117. const end: PointF2D = new PointF2D(line.End.x + offset.x, line.End.y + offset.y);
  118. const width: number = line.Width;
  119. let rectangle: RectangleF2D = new RectangleF2D(start.x, end.y - width / 2, end.x - start.x, width);
  120. rectangle = this.applyScreenTransformationForRect(rectangle);
  121. this.renderRectangle(rectangle, layer, line.styleId);
  122. }
  123. public drawLineAsVerticalRectangleWithOffset(line: GraphicalLine, offset: PointF2D, layer: number): void {
  124. const start: PointF2D = new PointF2D(line.Start.x + offset.x, line.Start.y + offset.y);
  125. const end: PointF2D = new PointF2D(line.End.x + offset.x, line.End.y + offset.y);
  126. const width: number = line.Width;
  127. let rectangle: RectangleF2D = new RectangleF2D(start.x, start.y, width, end.y - start.y);
  128. rectangle = this.applyScreenTransformationForRect(rectangle);
  129. this.renderRectangle(rectangle, layer, line.styleId);
  130. }
  131. public drawRectangle(rect: GraphicalRectangle, layer: number): void {
  132. const psi: BoundingBox = rect.PositionAndShape;
  133. let rectangle: RectangleF2D = new RectangleF2D(psi.AbsolutePosition.x, psi.AbsolutePosition.y, psi.BorderRight, psi.BorderBottom);
  134. rectangle = this.applyScreenTransformationForRect(rectangle);
  135. this.renderRectangle(rectangle, layer, <number>rect.style);
  136. }
  137. public abstract calculatePixelDistance(unitDistance: number): number;
  138. public drawLabel(graphicalLabel: GraphicalLabel, layer: number): Node {
  139. const NearestNote: GraphicalNote = this.graphicalMusicSheet.GetNearestNote(graphicalLabel.PositionAndShape.AbsolutePosition);
  140. // console.log("graphicalLabel.PositionAndShape", graphicalLabel.PositionAndShape);
  141. if (!this.isVisible(graphicalLabel.PositionAndShape)) {
  142. return undefined;
  143. }
  144. const label: Label = graphicalLabel.Label;
  145. if (label.text.trim() === "") {
  146. return undefined;
  147. }
  148. const calcResults: LabelRenderSpecs = this.calculateLabel(graphicalLabel);
  149. const screenPosition: PointF2D = this.applyScreenTransformation(graphicalLabel.PositionAndShape.AbsolutePosition);
  150. /** 修改前 空 */
  151. // 第一行文字上调
  152. const ParentObject: any = graphicalLabel.PositionAndShape.Parent.DataObject;
  153. if (this.rules.MetronomeMarksDrawn &&
  154. ParentObject?.measures?.[0]?.MeasureNumber === 1 &&
  155. ParentObject?.ParentMusicSystem?.Id === 0 &&
  156. isTopFont(graphicalLabel.Label.textAlignment)) {
  157. // console.log(ParentObject);
  158. calcResults.ScreenPosition.y -= 1 * 10;
  159. }
  160. // 判断修休息符号,偏移至小节尾
  161. if (label.text === ",") {
  162. let y = 0
  163. // 如果为 换气符号,重新寻找在那个小节上面
  164. const graphicalMusicPage = this.graphicalMusicSheet?.MusicPages?.[0]?.MusicSystems || []
  165. let hasMeasure: GraphicalMeasure
  166. try {
  167. for(let i = 0; i < graphicalMusicPage.length; i++){
  168. const musicSystem = graphicalMusicPage[i]
  169. if (graphicalLabel.PositionAndShape.AbsolutePosition.x < 10) {
  170. console.log(graphicalLabel.PositionAndShape.AbsolutePosition.x)
  171. const vexFlowMeasure = this.graphicalMusicSheet.MeasureList[NearestNote.sourceNote.SourceMeasure.MeasureNumber - 2]
  172. if (vexFlowMeasure && vexFlowMeasure[0]){
  173. hasMeasure = vexFlowMeasure[0]
  174. y = hasMeasure.PositionAndShape.AbsolutePosition.y - hasMeasure.PositionAndShape.Size.height / 2
  175. }
  176. break;
  177. }
  178. if (musicSystem.PositionAndShape.AbsolutePosition.y - 10 < graphicalLabel.PositionAndShape.AbsolutePosition.y && graphicalLabel.PositionAndShape.AbsolutePosition.y < musicSystem.PositionAndShape.AbsolutePosition.y + 10){
  179. const graphicalMeasures = musicSystem?.GraphicalMeasures.map((_n) => _n[0]).filter(Boolean) || []
  180. // @ts-ignore
  181. const end_xList = []
  182. let isWhole = false
  183. for(let j = 0; j < graphicalMeasures.length; j++){
  184. const measure = graphicalMeasures[j]
  185. // 全音符小节
  186. // @ts-ignore
  187. if (measure.vfVoices?.['1']?.tickables?.length === 1) {
  188. if (measure.PositionAndShape.AbsolutePosition.x < graphicalLabel.PositionAndShape.AbsolutePosition.x && graphicalLabel.PositionAndShape.AbsolutePosition.x < measure.PositionAndShape.AbsolutePosition.x + measure.PositionAndShape.Size.width) {
  189. hasMeasure = measure
  190. isWhole = true
  191. break;
  192. }
  193. }
  194. const end_x = measure.PositionAndShape.AbsolutePosition.x + measure.PositionAndShape.Size.width
  195. const label_x = graphicalLabel.PositionAndShape.AbsolutePosition.x
  196. if (end_x - 15 < label_x && label_x < end_x + 15){
  197. end_xList.push({
  198. measure,
  199. x: Math.abs(label_x - end_x)
  200. })
  201. }
  202. }
  203. if (isWhole) break;
  204. // const whole = end_xList.find((_n) => _n.measure.vfVoices?.['1']?.tickables?.length === 1)
  205. // if (whole) {
  206. // hasMeasure = whole.measure
  207. // } else {
  208. // hasMeasure = end_xList.sort((a, b) => a.x - b.x)?.[0]?.measure
  209. // }
  210. hasMeasure = end_xList.sort((a, b) => a.x - b.x)?.[0]?.measure
  211. // console.log("🚀 ~ whole", whole)
  212. // console.log("🚀 ~ end_xList", end_xList, hasMeasure)
  213. break;
  214. }
  215. }
  216. } catch (error) {
  217. console.log("🚀 ~ error", error)
  218. }
  219. let x: number = 0;
  220. // @ts-ignore
  221. if (hasMeasure){
  222. //&& hasMeasure.vfVoices?.['1']?.tickables?.length === 1
  223. // @ts-ignore
  224. x = hasMeasure.stave?.end_x || 0;
  225. } else {
  226. // console.log(NearestNote)
  227. // @ts-ignore
  228. const VFStave: any = NearestNote.sourceNote.SourceMeasure.VerticalMeasureList[0]?.getVFStave();
  229. x = VFStave.end_x;
  230. }
  231. // x = graphicalLabel.PositionAndShape.AbsolutePosition.x * 10
  232. // console.log(NearestNote.sourceNote.SourceMeasure.measureListIndex, NearestNote);
  233. // for (const measure of (ParentObject?.measures || [])) {
  234. // const itemx: number = graphicalLabel.PositionAndShape.RelativePosition.x * 10;
  235. // /**
  236. // * 由于大部分谱子生成后都往右有23的偏移,并且仅处理小节最后一个音符休息符问题,以及实在是没有时间仔细寻找父级节点。故暂时修处理
  237. // */
  238. // if (x === 0 && itemx <= measure.stave.end_x) {
  239. // // console.log(itemx, measure.stave.end_x, graphicalLabel, measure, layer);
  240. // // console.log("measure.measureNumber", measure.measureNumber);
  241. // x = measure.stave.end_x;
  242. // // index = measure.measureNumber;
  243. // }
  244. // }
  245. const drawMeasureNumbers: number = 0;//(index % 2 ? 0 : 1.5)// 同时显示小节编号时情况
  246. calcResults.ScreenPosition.x = x;
  247. calcResults.ScreenPosition.y = y === 0 ? (graphicalLabel.PositionAndShape.Parent.AbsolutePosition.y - 3 - drawMeasureNumbers) * 10 : y * 10;
  248. }
  249. // 修改 判断为音阶的话
  250. const clefList = ['C', 'G', 'D', 'A', 'E', 'B', 'F#', 'C#', 'G#', 'F', 'Bb', 'Eb', 'Ab', 'Db', 'Gb', 'Cb', 'Fb', 'D#', 'A#', 'E#']
  251. if (clefList.includes(label.text)){
  252. // const distanceH = this.rules.DYMusicClientType === 'teacher' ? 8 : 18
  253. calcResults.ScreenPosition.y = (graphicalLabel.PositionAndShape.Parent.AbsolutePosition.y + graphicalLabel.PositionAndShape.Parent.BorderTop + graphicalLabel.PositionAndShape.Parent.BorderMarginTop) * 10
  254. }
  255. // 修改 底部 play listen
  256. if (['play', 'listen'].includes(label.text.toLocaleLowerCase()) && label.textAlignment === TextAlignmentEnum.LeftBottom){
  257. calcResults.ScreenPosition.y = (graphicalLabel.PositionAndShape.Parent.AbsolutePosition.y - 1 + graphicalLabel.PositionAndShape.Parent.BorderBottom ) * 10
  258. }
  259. // 修改 部分文字需要识别为表情符号
  260. const expressionList = ['ffp', 'p-f', 'sfzp', 'szf', 'sffzp']
  261. for(let expressionIndex = 0; expressionIndex < expressionList.length; expressionIndex++){
  262. const expression = expressionList[expressionIndex].replace(/ /g, '')
  263. const _labelText = label.text.replace(/ /g, '')
  264. if (_labelText.toLocaleLowerCase() === expression.toLocaleLowerCase() || _labelText.includes(expression)){
  265. label.fontStyle = 3
  266. if (label.text === 'sfzp'){
  267. label.fontHeight = 2.3
  268. }
  269. break;
  270. }
  271. }
  272. // 修改 多声轨有R, L的子母向右偏移
  273. if (label.text == 'R' || label.text == 'L') {
  274. calcResults.ScreenPosition.x += 5
  275. }
  276. if (label.text === 'r' || label.text === 'l') {
  277. calcResults.ScreenPosition.x += 6
  278. }
  279. /**
  280. *
  281. * 修改前 空
  282. */
  283. // console.log(graphicalLabel, layer);
  284. const width: number = graphicalLabel.PositionAndShape.Size.width / 2 * 10;
  285. const screenX: number = screenPosition.x - width;
  286. // 修复偏移到屏幕外问题
  287. if (screenX < graphicalLabel.PositionAndShape.RelativePosition.x) {
  288. // console.log(screenX, graphicalLabel.PositionAndShape.RelativePosition.x)
  289. // screenPosition.x += 100;
  290. calcResults.ScreenPosition.x = (graphicalLabel.PositionAndShape.RelativePosition.x + this.rules.PageLeftMargin) * 10;
  291. }
  292. // if (screenPosition.x < 0) {
  293. // screenPosition.x = Math.max(graphicalLabel.PositionAndShape.Parent.AbsolutePosition.x * 10, screenPosition.x);
  294. // }
  295. // 修改 字体大小
  296. // if (new RegExp(/[\u4e00-\u9fa5]/).test(graphicalLabel.Label.text || '')) {
  297. // graphicalLabel.Label.fontHeight = 1.8
  298. // }
  299. // 修改 不需要显示的标记
  300. if (isSpecialMark(graphicalLabel.Label.text || "") && !clefList.includes(label.text)) {
  301. graphicalLabel.Label.fontHeight = 0;
  302. }
  303. // 当前小节有节拍速度并且有文字
  304. if (this.rules.MetronomeMarksDrawn && graphicalLabel.Label.fontHeight > 0 &&
  305. (isTopFont(graphicalLabel.Label.textAlignment) || [TextAlignmentEnum.CenterBottom].includes(graphicalLabel.Label.textAlignment))) {
  306. let measureListIndex: number = -1;
  307. let hasBpm: boolean = false;
  308. for (const expression of (ParentObject?.AbstractExpressions || [])) {
  309. if (expression.Label === graphicalLabel) {
  310. measureListIndex = expression?.parentMeasure?.measureListIndex;
  311. } else if (expression.expression?.TempoInBpm) {
  312. hasBpm = expression.expression.TempoInBpm > 0;
  313. }
  314. if (hasBpm && measureListIndex > -1) {
  315. calcResults.ScreenPosition.y -= 4 * 10;
  316. break;
  317. }
  318. }
  319. }
  320. // 简谱 只需要绘制标题,不需要小节文字
  321. if (this.rules.DYMusicScoreType === "jianpu") {
  322. if (isNaN(parseInt(graphicalLabel.Label.text, 10))) {
  323. this.renderLabel(graphicalLabel, layer, calcResults);
  324. }
  325. } else {
  326. this.renderLabel(graphicalLabel, layer, calcResults);
  327. }
  328. }
  329. protected calculateLabel(graphicalLabel: GraphicalLabel): LabelRenderSpecs {
  330. const result: LabelRenderSpecs = new LabelRenderSpecs();
  331. const label: Label = graphicalLabel.Label;
  332. // 修改 此处部分位置是空的,取父级坐标
  333. const mergerPosition: BoundingBox = new BoundingBox(this);
  334. mergerPosition.AbsolutePosition.x = graphicalLabel.PositionAndShape.AbsolutePosition.x;
  335. mergerPosition.AbsolutePosition.y = graphicalLabel.PositionAndShape.AbsolutePosition.y || graphicalLabel.PositionAndShape.Parent.AbsolutePosition.y;
  336. result.ScreenPosition = this.applyScreenTransformation(mergerPosition.AbsolutePosition);
  337. // console.log(this.calculatePixelDistance(label.fontHeight))
  338. result.FontHeightInPixel = this.calculatePixelDistance(label.fontHeight);
  339. const widthInPixel: number = this.calculatePixelDistance(graphicalLabel.PositionAndShape.Size.width);
  340. result.BitmapWidth = Math.ceil(widthInPixel);
  341. result.BitmapHeight = graphicalLabel.TextLines ? Math.ceil(result.FontHeightInPixel * (0.2 + graphicalLabel.TextLines.length)) : 10;
  342. switch (label.textAlignment) {
  343. // Adjust the OSMD-calculated positions to rendering coordinates
  344. // These have to match the Border settings in GraphicalLabel.setLabelPositionAndShapeBorders()
  345. // TODO isn't this a Vexflow-specific transformation that should be in VexflowMusicSheetDrawer?
  346. case TextAlignmentEnum.LeftTop:
  347. result.ScreenPosition.x -= result.BitmapWidth / 2;
  348. result.ScreenPosition.y += result.BitmapHeight * 1.5;
  349. break;
  350. case TextAlignmentEnum.LeftCenter:
  351. result.ScreenPosition.y -= result.BitmapHeight / 2;
  352. break;
  353. case TextAlignmentEnum.LeftBottom:
  354. result.ScreenPosition.y -= result.BitmapHeight;
  355. break;
  356. case TextAlignmentEnum.CenterTop:
  357. result.ScreenPosition.x -= result.BitmapWidth / 2;
  358. break;
  359. case TextAlignmentEnum.CenterCenter:
  360. result.ScreenPosition.x -= result.BitmapWidth / 2;
  361. result.ScreenPosition.y -= result.BitmapHeight / 2;
  362. break;
  363. case TextAlignmentEnum.CenterBottom:
  364. result.ScreenPosition.x -= result.BitmapWidth / 2;
  365. result.ScreenPosition.y -= result.BitmapHeight;
  366. break;
  367. case TextAlignmentEnum.RightTop:
  368. result.ScreenPosition.x -= result.BitmapWidth;
  369. break;
  370. case TextAlignmentEnum.RightCenter:
  371. result.ScreenPosition.x -= result.BitmapWidth;
  372. result.ScreenPosition.y -= result.BitmapHeight / 2;
  373. break;
  374. case TextAlignmentEnum.RightBottom:
  375. result.ScreenPosition.x -= result.BitmapWidth;
  376. result.ScreenPosition.y -= result.BitmapHeight;
  377. break;
  378. default:
  379. throw new ArgumentOutOfRangeException("");
  380. }
  381. return result;
  382. }
  383. protected abstract applyScreenTransformation(point: PointF2D): PointF2D;
  384. protected applyScreenTransformations(points: PointF2D[]): PointF2D[] {
  385. const transformedPoints: PointF2D[] = [];
  386. for (const point of points) {
  387. transformedPoints.push(this.applyScreenTransformation(point));
  388. }
  389. return transformedPoints;
  390. }
  391. protected abstract applyScreenTransformationForRect(rectangle: RectangleF2D): RectangleF2D;
  392. protected drawSplitScreenLine(): void {
  393. // empty
  394. }
  395. protected renderRectangle(rectangle: RectangleF2D, layer: number, styleId: number, colorHex: string = undefined, alpha: number = 1): Node {
  396. throw new Error("not implemented");
  397. }
  398. protected drawScrollIndicator(): void {
  399. // empty
  400. }
  401. protected drawSelectionStartSymbol(symbol: SelectionStartSymbol): void {
  402. // empty
  403. }
  404. protected drawSelectionEndSymbol(symbol: SelectionEndSymbol): void {
  405. // empty
  406. }
  407. protected renderLabel(graphicalLabel: GraphicalLabel, layer: GraphicalLayers, specs: LabelRenderSpecs): Node {
  408. throw new Error("not implemented");
  409. }
  410. protected renderSystemToScreen(system: MusicSystem, systemBoundingBoxInPixels: RectangleF2D,
  411. absBoundingRectWithMargin: RectangleF2D): void {
  412. // empty
  413. }
  414. protected abstract drawMeasure(measure: GraphicalMeasure): void;
  415. protected drawSkyLine(staffLine: StaffLine): void {
  416. // empty
  417. }
  418. protected drawBottomLine(staffLine: StaffLine): void {
  419. // empty
  420. }
  421. protected drawInstrumentBrace(brace: GraphicalObject, system: MusicSystem): void {
  422. // empty
  423. }
  424. protected drawGroupBracket(bracket: GraphicalObject, system: MusicSystem): void {
  425. // empty
  426. }
  427. protected isVisible(psi: BoundingBox): boolean {
  428. return true;
  429. }
  430. protected drawMusicSystem(system: MusicSystem): void {
  431. const absBoundingRectWithMargin: RectangleF2D = this.getSystemAbsBoundingRect(system);
  432. const systemBoundingBoxInPixels: RectangleF2D = this.getSytemBoundingBoxInPixels(absBoundingRectWithMargin);
  433. this.drawMusicSystemComponents(system, systemBoundingBoxInPixels, absBoundingRectWithMargin);
  434. }
  435. protected getSytemBoundingBoxInPixels(absBoundingRectWithMargin: RectangleF2D): RectangleF2D {
  436. const systemBoundingBoxInPixels: RectangleF2D = this.applyScreenTransformationForRect(absBoundingRectWithMargin);
  437. systemBoundingBoxInPixels.x = Math.round(systemBoundingBoxInPixels.x);
  438. systemBoundingBoxInPixels.y = Math.round(systemBoundingBoxInPixels.y);
  439. return systemBoundingBoxInPixels;
  440. }
  441. protected getSystemAbsBoundingRect(system: MusicSystem): RectangleF2D {
  442. const relBoundingRect: RectangleF2D = system.PositionAndShape.BoundingRectangle;
  443. const absBoundingRectWithMargin: RectangleF2D = new RectangleF2D(
  444. system.PositionAndShape.AbsolutePosition.x + system.PositionAndShape.BorderLeft - 1,
  445. system.PositionAndShape.AbsolutePosition.y + system.PositionAndShape.BorderTop - 1,
  446. (relBoundingRect.width + 6), (relBoundingRect.height + 2)
  447. );
  448. return absBoundingRectWithMargin;
  449. }
  450. protected drawMusicSystemComponents(musicSystem: MusicSystem, systemBoundingBoxInPixels: RectangleF2D,
  451. absBoundingRectWithMargin: RectangleF2D): void {
  452. const selectStartSymb: SelectionStartSymbol = this.graphicalMusicSheet.SelectionStartSymbol;
  453. const selectEndSymb: SelectionEndSymbol = this.graphicalMusicSheet.SelectionEndSymbol;
  454. if (this.drawingParameters.drawSelectionStartSymbol) {
  455. if (selectStartSymb !== undefined && this.isVisible(selectStartSymb.PositionAndShape)) {
  456. this.drawSelectionStartSymbol(selectStartSymb);
  457. }
  458. }
  459. if (this.drawingParameters.drawSelectionEndSymbol) {
  460. if (selectEndSymb !== undefined && this.isVisible(selectEndSymb.PositionAndShape)) {
  461. this.drawSelectionEndSymbol(selectEndSymb);
  462. }
  463. }
  464. for (const staffLine of musicSystem.StaffLines) {
  465. this.drawStaffLine(staffLine);
  466. if (this.rules.RenderLyrics) {
  467. // draw lyric dashes
  468. if (staffLine.LyricsDashes.length > 0) {
  469. this.drawDashes(staffLine.LyricsDashes);
  470. }
  471. // draw lyric lines (e.g. LyricExtends: "dich,___")
  472. if (staffLine.LyricLines.length > 0) {
  473. this.drawLyricLines(staffLine.LyricLines, staffLine);
  474. }
  475. }
  476. }
  477. for (const systemLine of musicSystem.SystemLines) {
  478. this.drawSystemLineObject(systemLine);
  479. }
  480. if (musicSystem.Parent === musicSystem.Parent.Parent.MusicPages[0]) {
  481. for (const label of musicSystem.Labels) {
  482. label.SVGNode = this.drawLabel(label, <number>GraphicalLayers.Notes);
  483. }
  484. }
  485. const instruments: Instrument[] = this.graphicalMusicSheet.ParentMusicSheet.Instruments;
  486. const instrumentsVisible: number = instruments.filter((instrument) => instrument.Visible).length;
  487. for (const bracket of musicSystem.InstrumentBrackets) {
  488. this.drawInstrumentBrace(bracket, musicSystem);
  489. }
  490. if (instruments.length > 0) {
  491. // TODO instead of this check we could save what instruments are in the group bracket,
  492. // and only draw it if all these instruments are visible.
  493. // Currently the instruments/stafflines aren't saved in the bracket however.
  494. if (instrumentsVisible > 1) {
  495. for (const bracket of musicSystem.GroupBrackets) {
  496. this.drawGroupBracket(bracket, musicSystem);
  497. }
  498. } else {
  499. for (const bracket of musicSystem.GroupBrackets) {
  500. (bracket as VexFlowInstrumentBracket).Visible = false; //.setType(VF.StaveConnector.type.NONE);
  501. }
  502. }
  503. }
  504. if (!this.leadSheet) {
  505. for (const measureNumberLabel of musicSystem.MeasureNumberLabels) {
  506. measureNumberLabel.SVGNode = this.drawLabel(measureNumberLabel, <number>GraphicalLayers.Notes);
  507. }
  508. }
  509. for (const staffLine of musicSystem.StaffLines) {
  510. this.drawStaffLineSymbols(staffLine);
  511. }
  512. if (this.drawingParameters.drawMarkedAreas) {
  513. this.drawMarkedAreas(musicSystem);
  514. }
  515. }
  516. protected activateSystemRendering(systemId: number, absBoundingRect: RectangleF2D,
  517. systemBoundingBoxInPixels: RectangleF2D, createNewImage: boolean): boolean {
  518. return true;
  519. }
  520. protected drawSystemLineObject(systemLine: SystemLine): void {
  521. // empty
  522. }
  523. protected drawStaffLine(staffLine: StaffLine): void {
  524. // console.log('staffLine',staffLine)
  525. for (const measure of staffLine.Measures) {
  526. this.drawMeasure(measure);
  527. }
  528. if (this.rules.RenderLyrics) {
  529. if (staffLine.LyricsDashes.length > 0) {
  530. this.drawDashes(staffLine.LyricsDashes);
  531. }
  532. }
  533. this.drawOctaveShifts(staffLine);
  534. this.drawPedals(staffLine);
  535. this.drawWavyLines(staffLine);
  536. this.drawExpressions(staffLine);
  537. if (this.skyLineVisible) {
  538. this.drawSkyLine(staffLine);
  539. }
  540. if (this.bottomLineVisible) {
  541. this.drawBottomLine(staffLine);
  542. }
  543. }
  544. protected drawLyricLines(lyricLines: GraphicalLine[], staffLine: StaffLine): void {
  545. staffLine.LyricLines.forEach(lyricLine => {
  546. // TODO maybe we should put this in the calculation (MusicSheetCalculator.calculateLyricExtend)
  547. // then we can also remove staffLine argument
  548. // but same addition doesn't work in calculateLyricExtend, because y-spacing happens after lyrics positioning
  549. lyricLine.Start.y += staffLine.PositionAndShape.AbsolutePosition.y;
  550. lyricLine.End.y += staffLine.PositionAndShape.AbsolutePosition.y;
  551. lyricLine.Start.x += staffLine.PositionAndShape.AbsolutePosition.x;
  552. lyricLine.End.x += staffLine.PositionAndShape.AbsolutePosition.x;
  553. this.drawGraphicalLine(lyricLine, this.rules.LyricUnderscoreLineWidth);
  554. });
  555. }
  556. protected drawExpressions(staffline: StaffLine): void {
  557. // implemented by subclass (VexFlowMusicSheetDrawer)
  558. }
  559. protected drawGraphicalLine(graphicalLine: GraphicalLine, lineWidth: number, colorOrStyle: string = "black"): Node {
  560. /* TODO similar checks as in drawLabel
  561. if (!this.isVisible(new BoundingBox(graphicalLine.Start,)) {
  562. return;
  563. }
  564. */
  565. return this.drawLine(graphicalLine.Start, graphicalLine.End, colorOrStyle, lineWidth);
  566. }
  567. protected drawLine(start: PointF2D, stop: PointF2D, color: string = "#FF0000FF", lineWidth: number): Node {
  568. // implemented by subclass (VexFlowMusicSheetDrawer)
  569. return undefined;
  570. }
  571. /**
  572. * Draw all dashes to the canvas
  573. * @param lyricsDashes Array of lyric dashes to be drawn
  574. * @param layer Number of the layer that the lyrics should be drawn in
  575. */
  576. protected drawDashes(lyricsDashes: GraphicalLabel[]): void {
  577. lyricsDashes.forEach(dash => dash.SVGNode = this.drawLabel(dash, <number>GraphicalLayers.Notes));
  578. }
  579. // protected drawSlur(slur: GraphicalSlur, abs: PointF2D): void {
  580. //
  581. // }
  582. protected drawOctaveShifts(staffLine: StaffLine): void {
  583. return;
  584. }
  585. protected abstract drawPedals(staffLine: StaffLine): void;
  586. protected abstract drawWavyLines(staffLine: StaffLine): void;
  587. protected drawStaffLines(staffLine: StaffLine): void {
  588. if (staffLine.StaffLines) {
  589. const position: PointF2D = staffLine.PositionAndShape.AbsolutePosition;
  590. for (let i: number = 0; i < 5; i++) {
  591. this.drawLineAsHorizontalRectangleWithOffset(staffLine.StaffLines[i], position, <number>GraphicalLayers.Notes);
  592. }
  593. }
  594. }
  595. // protected drawEnding(ending: GraphicalRepetitionEnding, absolutePosition: PointF2D): void {
  596. // if (undefined !== ending.Left)
  597. // drawLineAsVerticalRectangle(ending.Left, absolutePosition, <number>GraphicalLayers.Notes);
  598. // this.drawLineAsHorizontalRectangle(ending.Top, absolutePosition, <number>GraphicalLayers.Notes);
  599. // if (undefined !== ending.Right)
  600. // drawLineAsVerticalRectangle(ending.Right, absolutePosition, <number>GraphicalLayers.Notes);
  601. // this.drawLabel(ending.Label, <number>GraphicalLayers.Notes);
  602. // }
  603. /**
  604. * Draws an instantaneous dynamic expression (p, pp, f, ff, ...) to the canvas
  605. * @param instantaneousDynamic GraphicalInstantaneousDynamicExpression to be drawn
  606. */
  607. protected abstract drawInstantaneousDynamic(instantaneousDynamic: GraphicalInstantaneousDynamicExpression): void;
  608. /**
  609. * Draws a continuous dynamic expression (wedges) to the canvas
  610. * @param expression GraphicalContinuousDynamicExpression to be drawn
  611. */
  612. protected abstract drawContinuousDynamic(expression: GraphicalContinuousDynamicExpression): void;
  613. protected drawSymbol(symbol: MusicSymbol, symbolStyle: MusicSymbolDrawingStyle, position: PointF2D,
  614. scalingFactor: number = 1, layer: number = <number>GraphicalLayers.Notes): void {
  615. //empty
  616. }
  617. protected get leadSheet(): boolean {
  618. return this.graphicalMusicSheet.LeadSheet;
  619. }
  620. protected set leadSheet(value: boolean) {
  621. this.graphicalMusicSheet.LeadSheet = value;
  622. }
  623. protected drawPage(page: GraphicalMusicPage): void {
  624. if (!this.isVisible(page.PositionAndShape)) {
  625. return;
  626. }
  627. for (const system of page.MusicSystems) {
  628. if (this.isVisible(system.PositionAndShape)) {
  629. this.drawMusicSystem(system);
  630. }
  631. }
  632. if (page === page.Parent.MusicPages[0]) {
  633. for (const label of page.Labels) {
  634. label.SVGNode = this.drawLabel(label, <number>GraphicalLayers.Notes);
  635. }
  636. }
  637. // Draw bounding boxes for debug purposes. This has to be at the end because only
  638. // then all the calculations and recalculations are done
  639. if (this.drawableBoundingBoxElement) {
  640. this.drawBoundingBoxes(page.PositionAndShape, 0, this.drawableBoundingBoxElement);
  641. }
  642. }
  643. /**
  644. * Draw bounding boxes aroung GraphicalObjects
  645. * @param startBox Bounding Box that is used as a staring point to recursively go through all child elements
  646. * @param layer Layer to draw to
  647. * @param type Type of element to show bounding boxes for as string.
  648. */
  649. private drawBoundingBoxes(startBox: BoundingBox, layer: number = 0, type: string = "all"): void {
  650. const dataObjectString: string = (startBox.DataObject.constructor as any).name; // only works with non-minified build or sourcemap
  651. let typeMatch: boolean = false;
  652. if (type === "all") {
  653. typeMatch = true;
  654. } else {
  655. /*TODO: This seems to cause a circular reference and causes compilation to fail. */
  656. // if (type === "VexFlowStaffEntry") {
  657. // typeMatch = startBox.DataObject instanceof VexFlowStaffEntry; // circular dependencies with audio player? creates error
  658. // } else if (type === "VexFlowMeasure") {
  659. // typeMatch = startBox.DataObject instanceof VexFlowMeasure;
  660. // } else if (type === "VexFlowGraphicalNote") {
  661. // typeMatch = startBox.DataObject instanceof VexFlowGraphicalNote;
  662. // } else if (type === "VexFlowVoiceEntry") {
  663. // typeMatch = startBox.DataObject instanceof VexFlowVoiceEntry;
  664. // } else if (type === "GraphicalLabel") {
  665. // typeMatch = startBox.DataObject instanceof GraphicalLabel;
  666. // } else if (type === "VexFlowStaffLine") {
  667. // typeMatch = startBox.DataObject instanceof VexFlowStaffLine;
  668. // } else if (type === "SystemLine") {
  669. // typeMatch = startBox.DataObject instanceof SystemLine;
  670. // } else if (type === "StaffLineActivitySymbol") {
  671. // typeMatch = startBox.DataObject instanceof StaffLineActivitySymbol;
  672. // } else if (type === "VexFlowContinuousDynamicExpression") {
  673. // typeMatch = startBox.DataObject instanceof VexFlowContinuousDynamicExpression;
  674. // }
  675. }
  676. if (typeMatch || dataObjectString === type) {
  677. this.drawBoundingBox(startBox, undefined, true, dataObjectString, layer);
  678. }
  679. layer++;
  680. startBox.ChildElements.forEach(bb => this.drawBoundingBoxes(bb, layer, type));
  681. }
  682. public drawBoundingBox(bbox: BoundingBox,
  683. color: string = undefined, drawCross: boolean = false, labelText: string = undefined, layer: number = 0
  684. ): Node {
  685. let tmpRect: RectangleF2D = new RectangleF2D(bbox.AbsolutePosition.x + bbox.BorderMarginLeft,
  686. bbox.AbsolutePosition.y + bbox.BorderMarginTop,
  687. bbox.BorderMarginRight - bbox.BorderMarginLeft,
  688. bbox.BorderMarginBottom - bbox.BorderMarginTop);
  689. if (drawCross) {
  690. this.drawLineAsHorizontalRectangle(new GraphicalLine(
  691. new PointF2D(bbox.AbsolutePosition.x - 1, bbox.AbsolutePosition.y),
  692. new PointF2D(bbox.AbsolutePosition.x + 1, bbox.AbsolutePosition.y),
  693. 0.1,
  694. OutlineAndFillStyleEnum.BaseWritingColor,
  695. color),
  696. layer - 1);
  697. this.drawLineAsVerticalRectangle(new GraphicalLine(
  698. new PointF2D(bbox.AbsolutePosition.x, bbox.AbsolutePosition.y - 1),
  699. new PointF2D(bbox.AbsolutePosition.x, bbox.AbsolutePosition.y + 1),
  700. 0.1,
  701. OutlineAndFillStyleEnum.BaseWritingColor,
  702. color),
  703. layer - 1);
  704. }
  705. tmpRect = this.applyScreenTransformationForRect(tmpRect);
  706. const rectNode: Node = this.renderRectangle(tmpRect, <number>GraphicalLayers.Background, layer, color, 0.5);
  707. if (labelText) {
  708. const label: Label = new Label(labelText);
  709. const specs: LabelRenderSpecs = new LabelRenderSpecs();
  710. specs.BitmapWidth = tmpRect.width;
  711. specs.BitmapHeight = tmpRect.height;
  712. specs.FontHeightInPixel = tmpRect.height;
  713. specs.ScreenPosition = new PointF2D(tmpRect.x, tmpRect.y + 12);
  714. this.renderLabel(new GraphicalLabel(label, 0.8, TextAlignmentEnum.CenterCenter, this.rules),
  715. layer, specs);
  716. // theoretically we should return the nodes from renderLabel here as well, so they can also be removed later
  717. }
  718. return rectNode;
  719. }
  720. private drawMarkedAreas(system: MusicSystem): void {
  721. for (const markedArea of system.GraphicalMarkedAreas) {
  722. if (markedArea) {
  723. if (markedArea.systemRectangle) {
  724. this.drawRectangle(markedArea.systemRectangle, <number>GraphicalLayers.Background);
  725. }
  726. if (markedArea.settings) {
  727. markedArea.settings.SVGNode = this.drawLabel(markedArea.settings, <number>GraphicalLayers.Comment);
  728. }
  729. if (markedArea.labelRectangle) {
  730. this.drawRectangle(markedArea.labelRectangle, <number>GraphicalLayers.Background);
  731. }
  732. if (markedArea.label) {
  733. markedArea.label.SVGNode = this.drawLabel(markedArea.label, <number>GraphicalLayers.Comment);
  734. }
  735. }
  736. }
  737. }
  738. private drawStaffLineSymbols(staffLine: StaffLine): void {
  739. const parentInst: Instrument = staffLine.ParentStaff.ParentInstrument;
  740. const absX: number = staffLine.PositionAndShape.AbsolutePosition.x;
  741. const absY: number = staffLine.PositionAndShape.AbsolutePosition.y + 2;
  742. const borderRight: number = staffLine.PositionAndShape.BorderRight;
  743. if (parentInst.highlight && this.drawingParameters.drawHighlights) {
  744. this.drawLineAsHorizontalRectangle(
  745. new GraphicalLine(
  746. new PointF2D(absX, absY),
  747. new PointF2D(absX + borderRight, absY),
  748. 4,
  749. OutlineAndFillStyleEnum.Highlighted
  750. ),
  751. <number>GraphicalLayers.Highlight
  752. );
  753. }
  754. let style: MusicSymbolDrawingStyle = MusicSymbolDrawingStyle.Disabled;
  755. let symbol: MusicSymbol = MusicSymbol.PLAY;
  756. let drawSymbols: boolean = this.drawingParameters.drawActivitySymbols;
  757. switch (this.phonicScoreMode) {
  758. case PhonicScoreModes.Midi:
  759. symbol = MusicSymbol.PLAY;
  760. if (this.midiPlaybackAvailable && staffLine.ParentStaff.audible) {
  761. style = MusicSymbolDrawingStyle.PlaybackSymbols;
  762. }
  763. break;
  764. case PhonicScoreModes.Following:
  765. symbol = MusicSymbol.MIC;
  766. if (staffLine.ParentStaff.following) {
  767. style = MusicSymbolDrawingStyle.FollowSymbols;
  768. }
  769. break;
  770. default:
  771. drawSymbols = false;
  772. break;
  773. }
  774. if (drawSymbols) {
  775. const p: PointF2D = new PointF2D(absX + borderRight + 2, absY);
  776. this.drawSymbol(symbol, style, p);
  777. }
  778. if (this.drawingParameters.drawErrors) {
  779. for (const measure of staffLine.Measures) {
  780. const measurePSI: BoundingBox = measure.PositionAndShape;
  781. const absXPSI: number = measurePSI.AbsolutePosition.x;
  782. const absYPSI: number = measurePSI.AbsolutePosition.y + 2;
  783. if (measure.hasError && this.graphicalMusicSheet.ParentMusicSheet.DrawErroneousMeasures) {
  784. this.drawLineAsHorizontalRectangle(
  785. new GraphicalLine(
  786. new PointF2D(absXPSI, absYPSI),
  787. new PointF2D(absXPSI + measurePSI.BorderRight, absYPSI),
  788. 4,
  789. OutlineAndFillStyleEnum.ErrorUnderlay
  790. ),
  791. <number>GraphicalLayers.MeasureError
  792. );
  793. }
  794. }
  795. }
  796. }
  797. }