diff --git a/src/Environment.ts b/src/Environment.ts index 409cdf9e7..cd9f676b3 100644 --- a/src/Environment.ts +++ b/src/Environment.ts @@ -61,6 +61,7 @@ import { Font } from './model'; import { Settings } from './Settings'; import { AlphaTabError, AlphaTabErrorType } from './AlphaTabError'; import { SlashBarRendererFactory } from './rendering/SlashBarRendererFactory'; +import { NumberedBarRendererFactory } from './rendering/NumberedBarRendererFactory'; export class LayoutEngineFactory { public readonly vertical: boolean; @@ -133,6 +134,7 @@ export class Environment { } .at-surface-svg text { dominant-baseline: central; + white-space:pre; } .at { font-family: 'alphaTab'; @@ -442,11 +444,11 @@ export class Environment { /** * Enables the usage of alphaSkia as rendering backend. - * @param musicFontData The raw binary data of the music font. + * @param musicFontData The raw binary data of the music font. * @param alphaSkia The alphaSkia module. */ public static enableAlphaSkia(musicFontData: ArrayBuffer, alphaSkia: unknown) { - SkiaCanvas.enable(musicFontData, alphaSkia) + SkiaCanvas.enable(musicFontData, alphaSkia); } /** @@ -456,9 +458,7 @@ export class Environment { * @param fontInfo If provided the font info provided overrules * @returns The font info under which the font was registered. */ - public static registerAlphaSkiaCustomFont( - fontData: Uint8Array, - fontInfo?: Font | undefined): Font { + public static registerAlphaSkiaCustomFont(fontData: Uint8Array, fontInfo?: Font | undefined): Font { return SkiaCanvas.registerFont(fontData, fontInfo); } @@ -485,7 +485,7 @@ export class Environment { new TripletFeelEffectInfo(), new MarkerEffectInfo(), new TextEffectInfo(), - new ChordsEffectInfo(), + new ChordsEffectInfo() ]), new SlashBarRendererFactory(), new EffectBarRendererFactory('score-effects', [ @@ -501,6 +501,7 @@ export class Environment { new AlternateEndingsEffectInfo() ]), new ScoreBarRendererFactory(), + new NumberedBarRendererFactory(), new EffectBarRendererFactory('tab-effects', [ new CrescendoEffectInfo(), new OttaviaEffectInfo(false), @@ -535,8 +536,8 @@ export class Environment { new TripletFeelEffectInfo(), new MarkerEffectInfo(), new TextEffectInfo(), - new ChordsEffectInfo(), - ]), + new ChordsEffectInfo() + ]), new SlashBarRendererFactory(), new EffectBarRendererFactory('score-effects', [ new FermataEffectInfo(), @@ -632,15 +633,12 @@ export class Environment { createWebWorker: (settings: Settings) => Worker, createAudioWorklet: (context: AudioContext, settings: Settings) => Promise ) { - if(Environment.isRunningInWorker || Environment.isRunningInAudioWorklet) { + if (Environment.isRunningInWorker || Environment.isRunningInAudioWorklet) { return; } - + // browser polyfills - if ( - Environment.webPlatform === WebPlatform.Browser || - Environment.webPlatform === WebPlatform.BrowserModule - ) { + if (Environment.webPlatform === WebPlatform.Browser || Environment.webPlatform === WebPlatform.BrowserModule) { Environment.registerJQueryPlugin(); Environment.HighDpiFactor = window.devicePixelRatio; // ResizeObserver API does not yet exist so long on Safari (only start 2020 with iOS Safari 13.7 and Desktop 13.1) @@ -660,7 +658,9 @@ export class Environment { this.append(...nodes); }; (Document.prototype as Document).replaceChildren = (Element.prototype as Element).replaceChildren; - (DocumentFragment.prototype as DocumentFragment).replaceChildren = (Element.prototype as Element).replaceChildren; + (DocumentFragment.prototype as DocumentFragment).replaceChildren = ( + Element.prototype as Element + ).replaceChildren; } if (!('replaceAll' in String.prototype)) { (String.prototype as any).replaceAll = function (str: string, newStr: string) { @@ -676,19 +676,24 @@ export class Environment { /** * @target web */ - public static get alphaTabWorker(): any { return this.globalThis.Worker } + public static get alphaTabWorker(): any { + return this.globalThis.Worker; + } /** * @target web */ public static initializeWorker() { if (!Environment.isRunningInWorker) { - throw new AlphaTabError(AlphaTabErrorType.General, "Not running in worker, cannot run worker initialization"); + throw new AlphaTabError( + AlphaTabErrorType.General, + 'Not running in worker, cannot run worker initialization' + ); } AlphaTabWebWorker.init(); AlphaSynthWebWorker.init(); Environment.createWebWorker = _ => { - throw new AlphaTabError(AlphaTabErrorType.General, "Nested workers are not supported"); + throw new AlphaTabError(AlphaTabErrorType.General, 'Nested workers are not supported'); }; } @@ -697,7 +702,10 @@ export class Environment { */ public static initializeAudioWorklet() { if (!Environment.isRunningInAudioWorklet) { - throw new AlphaTabError(AlphaTabErrorType.General, "Not running in audio worklet, cannot run worklet initialization"); + throw new AlphaTabError( + AlphaTabErrorType.General, + 'Not running in audio worklet, cannot run worklet initialization' + ); } AlphaSynthWebWorklet.init(); } @@ -761,4 +769,4 @@ export class Environment { return WebPlatform.Browser; } -} \ No newline at end of file +} diff --git a/src/RenderingResources.ts b/src/RenderingResources.ts index e9e009e9d..a95701a36 100644 --- a/src/RenderingResources.ts +++ b/src/RenderingResources.ts @@ -40,6 +40,16 @@ export class RenderingResources { */ public fretboardNumberFont: Font = new Font(RenderingResources.sansFont, 11, FontStyle.Plain); + /** + * Gets or sets the font to use for displaying the numbered music notation in the music sheet. + */ + public numberedNotationFont: Font = new Font(RenderingResources.sansFont, 16, FontStyle.Plain); + + /** + * Gets or sets the font to use for displaying the grace notes in numbered music notation in the music sheet. + */ + public numberedNotationGraceFont: Font = new Font(RenderingResources.sansFont, 14, FontStyle.Plain); + /** * Gets or sets the font to use for displaying the guitar tablature numbers in the music sheet. */ diff --git a/src/generated/RenderingResourcesJson.ts b/src/generated/RenderingResourcesJson.ts index 7108bc8df..1de8e8c70 100644 --- a/src/generated/RenderingResourcesJson.ts +++ b/src/generated/RenderingResourcesJson.ts @@ -36,6 +36,14 @@ export interface RenderingResourcesJson { * Gets or sets the font to use for displaying the fretboard numbers in chord diagrams. */ fretboardNumberFont?: FontJson; + /** + * Gets or sets the font to use for displaying the numbered music notation in the music sheet. + */ + numberedNotationFont?: FontJson; + /** + * Gets or sets the font to use for displaying the grace notes in numbered music notation in the music sheet. + */ + numberedNotationGraceFont?: FontJson; /** * Gets or sets the font to use for displaying the guitar tablature numbers in the music sheet. */ diff --git a/src/generated/RenderingResourcesSerializer.ts b/src/generated/RenderingResourcesSerializer.ts index c6ca49028..83cbbfb7e 100644 --- a/src/generated/RenderingResourcesSerializer.ts +++ b/src/generated/RenderingResourcesSerializer.ts @@ -25,6 +25,8 @@ export class RenderingResourcesSerializer { o.set("wordsfont", Font.toJson(obj.wordsFont)); o.set("effectfont", Font.toJson(obj.effectFont)); o.set("fretboardnumberfont", Font.toJson(obj.fretboardNumberFont)); + o.set("numberednotationfont", Font.toJson(obj.numberedNotationFont)); + o.set("numberednotationgracefont", Font.toJson(obj.numberedNotationGraceFont)); o.set("tablaturefont", Font.toJson(obj.tablatureFont)); o.set("gracefont", Font.toJson(obj.graceFont)); o.set("stafflinecolor", Color.toJson(obj.staffLineColor)); @@ -58,6 +60,12 @@ export class RenderingResourcesSerializer { case "fretboardnumberfont": obj.fretboardNumberFont = Font.fromJson(v)!; return true; + case "numberednotationfont": + obj.numberedNotationFont = Font.fromJson(v)!; + return true; + case "numberednotationgracefont": + obj.numberedNotationGraceFont = Font.fromJson(v)!; + return true; case "tablaturefont": obj.tablatureFont = Font.fromJson(v)!; return true; diff --git a/src/generated/model/StaffSerializer.ts b/src/generated/model/StaffSerializer.ts index f3a2cf310..1f034972c 100644 --- a/src/generated/model/StaffSerializer.ts +++ b/src/generated/model/StaffSerializer.ts @@ -35,6 +35,7 @@ export class StaffSerializer { o.set("displaytranspositionpitch", obj.displayTranspositionPitch); o.set("stringtuning", TuningSerializer.toJson(obj.stringTuning)); o.set("showslash", obj.showSlash); + o.set("shownumbered", obj.showNumbered); o.set("showtablature", obj.showTablature); o.set("showstandardnotation", obj.showStandardNotation); o.set("ispercussion", obj.isPercussion); @@ -71,6 +72,9 @@ export class StaffSerializer { case "showslash": obj.showSlash = v! as boolean; return true; + case "shownumbered": + obj.showNumbered = v! as boolean; + return true; case "showtablature": obj.showTablature = v! as boolean; return true; diff --git a/src/importer/AlphaTexImporter.ts b/src/importer/AlphaTexImporter.ts index e7130e9fd..8926b1cfe 100644 --- a/src/importer/AlphaTexImporter.ts +++ b/src/importer/AlphaTexImporter.ts @@ -1075,6 +1075,7 @@ export class AlphaTexImporter extends ScoreImporter { let showStandardNotation: boolean = false; let showTabs: boolean = false; let showSlash: boolean = false; + let showNumbered: boolean = false; while (this._sy === AlphaTexSymbols.String) { switch ((this._syData as string).toLowerCase()) { case 'score': @@ -1089,15 +1090,20 @@ export class AlphaTexImporter extends ScoreImporter { showSlash = true; this._sy = this.newSy(); break; + case 'numbered': + showNumbered = true; + this._sy = this.newSy(); + break; default: this.error('staff-properties', AlphaTexSymbols.String, false); break; } } - if (showStandardNotation || showTabs || showSlash) { + if (showStandardNotation || showTabs || showSlash || showNumbered) { this._currentStaff.showStandardNotation = showStandardNotation; this._currentStaff.showTablature = showTabs; this._currentStaff.showSlash = showSlash; + this._currentStaff.showNumbered = showNumbered; } if (this._sy !== AlphaTexSymbols.RBrace) { this.error('staff-properties', AlphaTexSymbols.RBrace, true); diff --git a/src/importer/PartConfiguration.ts b/src/importer/PartConfiguration.ts index 5a31b76dd..650e670a2 100644 --- a/src/importer/PartConfiguration.ts +++ b/src/importer/PartConfiguration.ts @@ -41,6 +41,7 @@ class PartConfigurationScoreView { } class PartConfigurationTrackViewGroup { + public showNumbered: boolean = false; public showSlash: boolean = false; public showStandardNotation: boolean = false; public showTablature: boolean = false; @@ -61,6 +62,7 @@ export class PartConfiguration { staff.showTablature = trackConfig.showTablature; staff.showStandardNotation = trackConfig.showStandardNotation; staff.showSlash = trackConfig.showSlash; + staff.showNumbered = trackConfig.showNumbered; } } trackIndex++; @@ -91,6 +93,7 @@ export class PartConfiguration { trackConfiguration.showStandardNotation = (flags & 0x01) !== 0; trackConfiguration.showTablature = (flags & 0x02) !== 0; trackConfiguration.showSlash = (flags & 0x04) !== 0; + trackConfiguration.showNumbered = (flags & 0x08) !== 0; scoreView.trackViewGroups.push(trackConfiguration); } } @@ -110,6 +113,7 @@ export class PartConfiguration { trackConfiguration.showStandardNotation = track.staves[0].showStandardNotation; trackConfiguration.showTablature = track.staves[0].showTablature; trackConfiguration.showSlash = track.staves[0].showSlash; + trackConfiguration.showNumbered = track.staves[0].showNumbered; scoreViews[0].trackViewGroups.push(trackConfiguration); @@ -133,6 +137,9 @@ export class PartConfiguration { if(track.showSlash) { flags = flags | 0x04; } + if(track.showNumbered) { + flags = flags | 0x08; + } writer.writeByte(flags); } } diff --git a/src/model/Staff.ts b/src/model/Staff.ts index cdc76b1e3..acbcbe515 100644 --- a/src/model/Staff.ts +++ b/src/model/Staff.ts @@ -82,6 +82,11 @@ export class Staff { */ public showSlash: boolean = false; + /** + * Gets or sets whether the numbered notation is shown. + */ + public showNumbered: boolean = false; + /** * Gets or sets whether the tabs are shown. */ diff --git a/src/rendering/LineBarRenderer.ts b/src/rendering/LineBarRenderer.ts index b9a2000c9..4ebf876ad 100644 --- a/src/rendering/LineBarRenderer.ts +++ b/src/rendering/LineBarRenderer.ts @@ -36,6 +36,10 @@ export abstract class LineBarRenderer extends BarRendererBase { return (this.lineSpacing + 1) * this.scale; } + public get tupletOffset(): number { + return 10 * this.scale; + } + public abstract get lineSpacing(): number; public abstract get heightLineCount(): number; public abstract get drawnLineCount(): number; @@ -153,21 +157,25 @@ export abstract class LineBarRenderer extends BarRendererBase { this._startSpacing = true; } - protected paintTuplets(cx: number, cy: number, canvas: ICanvas): void { + protected paintTuplets(cx: number, cy: number, canvas: ICanvas, bracketsAsArcs: boolean = false): void { for (const voice of this.bar.voices) { if (this.hasVoiceContainer(voice)) { const container = this.getVoiceContainer(voice)!; for (const tupletGroup of container.tupletGroups) { - this.paintTupletHelper(cx + this.beatGlyphsStart, cy, canvas, tupletGroup); + this.paintTupletHelper(cx + this.beatGlyphsStart, cy, canvas, tupletGroup, bracketsAsArcs); } } } } protected abstract getBeamDirection(helper: BeamingHelper): BeamDirection; + protected getTupletBeamDirection(helper: BeamingHelper): BeamDirection { + return this.getBeamDirection(helper); + } + protected abstract calculateBeamYWithDirection(h: BeamingHelper, x: number, direction: BeamDirection): number; - private paintTupletHelper(cx: number, cy: number, canvas: ICanvas, h: TupletGroup): void { + private paintTupletHelper(cx: number, cy: number, canvas: ICanvas, h: TupletGroup, bracketsAsArcs: boolean): void { const res = this.resources; let oldAlign: TextAlign = canvas.textAlign; let oldBaseLine = canvas.textBaseline; @@ -205,7 +213,7 @@ export abstract class LineBarRenderer extends BarRendererBase { } // check if we need to paint simple footer - let offset: number = 10 * this.scale; + let offset: number = this.tupletOffset; let size: number = 5 * this.scale; if (h.beats.length === 1 || !h.isFull) { @@ -215,7 +223,7 @@ export abstract class LineBarRenderer extends BarRendererBase { continue; } - let direction: BeamDirection = this.getBeamDirection(beamingHelper); + let direction: BeamDirection = this.getTupletBeamDirection(beamingHelper); let tupletX: number = beamingHelper.getBeatLineX(beat); let tupletY: number = this.calculateBeamYWithDirection(beamingHelper, tupletX, direction); @@ -269,7 +277,7 @@ export abstract class LineBarRenderer extends BarRendererBase { // calculate the y positions for our bracket let firstNonRestBeamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(firstNonRestBeat.index)!; let lastNonRestBeamingHelper = this.helpers.beamHelperLookup[h.voice.index].get(lastNonRestBeat.index)!; - let direction = this.getBeamDirection(firstBeamingHelper); + let direction = this.getTupletBeamDirection(firstBeamingHelper); let startY: number = this.calculateBeamYWithDirection(firstNonRestBeamingHelper, startX, direction); let endY: number = this.calculateBeamYWithDirection(lastNonRestBeamingHelper, endX, direction); if (isRestOnly) { @@ -302,13 +310,29 @@ export abstract class LineBarRenderer extends BarRendererBase { // draw the bracket canvas.beginPath(); canvas.moveTo(cx + this.x + startX, (cy + this.y + startY - offset) | 0); - canvas.lineTo(cx + this.x + startX, (cy + this.y + startY - offset - size) | 0); - canvas.lineTo(cx + this.x + offset1X, (cy + this.y + offset1Y - offset - size) | 0); + if (bracketsAsArcs) { + canvas.quadraticCurveTo( + cx + this.x + (offset1X + startX) / 2, (cy + this.y + offset1Y - offset - size) | 0, + cx + this.x + offset1X, (cy + this.y + offset1Y - offset - size) | 0 + ); + } else { + canvas.lineTo(cx + this.x + startX, (cy + this.y + startY - offset - size) | 0); + canvas.lineTo(cx + this.x + offset1X, (cy + this.y + offset1Y - offset - size) | 0); + } canvas.stroke(); + canvas.beginPath(); canvas.moveTo(cx + this.x + offset2X, (cy + this.y + offset2Y - offset - size) | 0); - canvas.lineTo(cx + this.x + endX, (cy + this.y + endY - offset - size) | 0); - canvas.lineTo(cx + this.x + endX, (cy + this.y + endY - offset) | 0); + if (bracketsAsArcs) { + canvas.quadraticCurveTo( + cx + this.x + (endX + offset2X) / 2, (cy + this.y + offset2Y - offset - size) | 0, + cx + this.x + endX, (cy + this.y + endY - offset) | 0, + ); + } else { + canvas.lineTo(cx + this.x + endX, (cy + this.y + endY - offset - size) | 0); + canvas.lineTo(cx + this.x + endX, (cy + this.y + endY - offset) | 0); + } + canvas.stroke(); // // Draw the string @@ -374,7 +398,7 @@ export abstract class LineBarRenderer extends BarRendererBase { return true; } - private paintFlag(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper): void { + protected paintFlag(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper): void { for (const beat of h.beats) { if (!this.shouldPaintFlag(beat, h)) { continue; @@ -539,14 +563,16 @@ export abstract class LineBarRenderer extends BarRendererBase { if (this.bar.masterBar.isRepeatEnd) { this.addPostBeatGlyph(new RepeatCloseGlyph(this.x, 0)); if (this.bar.masterBar.repeatCount > 2) { - this.addPostBeatGlyph(new RepeatCountGlyph(0, this.getLineHeight(-0.25), this.bar.masterBar.repeatCount)); + this.addPostBeatGlyph( + new RepeatCountGlyph(0, this.getLineHeight(-0.25), this.bar.masterBar.repeatCount) + ); } } else { this.addPostBeatGlyph(new BarSeperatorGlyph(0, 0)); } } - private paintBar(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper): void { + protected paintBar(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper): void { for (let i: number = 0, j: number = h.beats.length; i < j; i++) { let beat: Beat = h.beats[i]; if (!h.hasBeatLineX(beat)) { @@ -637,7 +663,14 @@ export abstract class LineBarRenderer extends BarRendererBase { } } - private static paintSingleBar(canvas: ICanvas, x1: number, y1: number, x2: number, y2: number, size: number): void { + protected static paintSingleBar( + canvas: ICanvas, + x1: number, + y1: number, + x2: number, + y2: number, + size: number + ): void { canvas.beginPath(); canvas.moveTo(x1, y1); canvas.lineTo(x2, y2); diff --git a/src/rendering/NumberedBarRenderer.ts b/src/rendering/NumberedBarRenderer.ts new file mode 100644 index 000000000..49c1fae4e --- /dev/null +++ b/src/rendering/NumberedBarRenderer.ts @@ -0,0 +1,303 @@ +import { Bar } from '@src/model/Bar'; +import { Beat } from '@src/model/Beat'; +import { Note } from '@src/model/Note'; +import { Voice } from '@src/model/Voice'; +import { ICanvas } from '@src/platform/ICanvas'; +import { BarRendererBase, NoteYPosition } from '@src/rendering/BarRendererBase'; +import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import { BeamDirection } from '@src/rendering/utils/BeamDirection'; +import { BeamingHelper } from '@src/rendering/utils/BeamingHelper'; +import { LineBarRenderer } from './LineBarRenderer'; +import { SlashNoteHeadGlyph } from './glyphs/SlashNoteHeadGlyph'; +import { BeatGlyphBase } from './glyphs/BeatGlyphBase'; +import { BeatOnNoteGlyphBase } from './glyphs/BeatOnNoteGlyphBase'; +import { NumberedBeatContainerGlyph } from './NumberedBeatContainerGlyph'; +import { NumberedBeatGlyph, NumberedBeatPreNotesGlyph } from './glyphs/NumberedBeatGlyph'; +import { ScoreTimeSignatureGlyph } from './glyphs/ScoreTimeSignatureGlyph'; +import { SpacingGlyph } from './glyphs/SpacingGlyph'; +import { NumberedKeySignatureGlyph } from './glyphs/NumberedKeySignatureGlyph'; +import { ModelUtils } from '@src/model/ModelUtils'; +import { Duration } from '@src/model'; +import { BeatXPosition } from './BeatXPosition'; + +/** + * This BarRenderer renders a bar using (Jianpu) Numbered Music Notation + */ +export class NumberedBarRenderer extends LineBarRenderer { + public static readonly StaffId: string = 'numbered'; + + public simpleWhammyOverflow: number = 0; + + private _isOnlyNumbered: boolean; + public shortestDuration = Duration.QuadrupleWhole; + public lowestOctave = -1000; + public highestOctave = -1000; + + public registerOctave(octave: number) { + if (this.lowestOctave === -1000) { + this.lowestOctave = octave; + this.highestOctave = octave; + } else { + if (octave < this.lowestOctave) { + this.lowestOctave = octave; + } + if (octave > this.highestOctave) { + this.highestOctave = octave; + } + } + } + + public constructor(renderer: ScoreRenderer, bar: Bar) { + super(renderer, bar); + this._isOnlyNumbered = !bar.staff.showSlash && !bar.staff.showTablature && !bar.staff.showStandardNotation; + } + + public override get lineSpacing(): number { + return BarRendererBase.RawLineSpacing; + } + + public override get heightLineCount(): number { + return 5; + } + + public override get drawnLineCount(): number { + return this._isOnlyNumbered ? 1 : 0; + } + + protected override get bottomGlyphOverflow(): number { + return 0; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + super.paint(cx, cy, canvas); + this.paintBeams(cx, cy, canvas); + this.paintTuplets(cx, cy, canvas, true); + } + + public override doLayout(): void { + super.doLayout(); + let hasTuplets: boolean = false; + for (let voice of this.bar.voices) { + if (this.hasVoiceContainer(voice)) { + let c = this.getVoiceContainer(voice)!; + if (c.tupletGroups.length > 0) { + hasTuplets = true; + break; + } + } + } + if (hasTuplets) { + this.registerOverflowTop(this.tupletSize); + } + + if (!this.bar.isEmpty) { + let barCount: number = ModelUtils.getIndex(this.shortestDuration) - 2; + if (barCount > 0) { + let barSpacing: number = NumberedBarRenderer.BarSpacing * this.scale; + let barSize: number = NumberedBarRenderer.BarSize * this.scale; + let barOverflow = (barCount - 1) * barSpacing + barSize; + + let dotOverflow = 0; + if (this.lowestOctave < 0) { + dotOverflow = + (Math.abs(this.lowestOctave) * NumberedBarRenderer.DotSpacing + NumberedBarRenderer.DotSize) * + this.scale; + } + + this.registerOverflowBottom(barOverflow + dotOverflow); + } + + if (this.highestOctave > 0) { + const dotOverflow = + (Math.abs(this.highestOctave) * NumberedBarRenderer.DotSpacing + NumberedBarRenderer.DotSize) * + this.scale; + this.registerOverflowTop(dotOverflow); + } + } + } + + private static BarSpacing = BarRendererBase.BeamSpacing + BarRendererBase.BeamThickness; + public static BarSize = 2; + + private static DotSpacing = 5; + public static DotSize = 2; + + protected override paintFlag(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper): void { + this.paintBar(cx, cy, canvas, h); + } + + protected override paintBar(cx: number, cy: number, canvas: ICanvas, h: BeamingHelper): void { + const res = this.resources; + + for (let i: number = 0, j: number = h.beats.length; i < j; i++) { + let beat: Beat = h.beats[i]; + // + // draw line + // + let barSpacing: number = NumberedBarRenderer.BarSpacing * this.scale; + let barSize: number = NumberedBarRenderer.BarSize * this.scale; + let barCount: number = ModelUtils.getIndex(beat.duration) - 2; + let barStart: number = cy + this.y; + + let beatLineX: number = this.getBeatX(beat, BeatXPosition.PreNotes) - this.beatGlyphsStart; + + var beamY = this.calculateBeamY(h, beatLineX); + + for (let barIndex: number = 0; barIndex < barCount; barIndex++) { + let barStartX: number = 0; + let barEndX: number = 0; + let barStartY: number = 0; + let barY: number = barStart + barIndex * barSpacing; + if (i === h.beats.length - 1) { + barStartX = beatLineX; + barEndX = this.getBeatX(beat, BeatXPosition.PostNotes) - this.beatGlyphsStart; + } else { + barStartX = beatLineX; + barEndX = this.getBeatX(h.beats[i + 1], BeatXPosition.PreNotes) - this.beatGlyphsStart; + } + + barStartY = (barY + beamY) | 0; + LineBarRenderer.paintSingleBar( + canvas, + cx + this.x + barStartX, + barStartY, + cx + this.x + barEndX, + barStartY, + barSize + ); + } + + const onNotes = this.getBeatContainer(beat)!.onNotes; + let dotCount = (onNotes as NumberedBeatGlyph).octaveDots; + let dotsY = 0; + let dotsOffset = 0; + if (dotCount > 0) { + dotsY = barStart + this.getLineY(0) - res.numberedNotationFont.size / 1.5; + dotsOffset = NumberedBarRenderer.DotSpacing - 1 * this.scale; + } else if (dotCount < 0) { + dotsY = barStart + beamY + barCount * barSpacing; + dotsOffset = NumberedBarRenderer.DotSpacing * this.scale; + } + let dotX: number = this.getBeatX(beat, BeatXPosition.OnNotes) + 4 * this.scale - this.beatGlyphsStart; + + dotCount = Math.abs(dotCount); + + for (let d = 0; d < dotCount; d++) { + canvas.fillCircle(cx + this.x + dotX, dotsY, NumberedBarRenderer.DotSize * this.scale); + dotsY += dotsOffset; + } + } + } + + public getNoteLine() { + return 0; + } + + public override get tupletOffset(): number { + return super.tupletOffset + this.resources.numberedNotationFont.size * this.scale; + } + + protected override getFlagTopY(_beat: Beat): number { + return this.getLineY(0) - (SlashNoteHeadGlyph.NoteHeadHeight / 2) * this.scale; + } + + protected override getFlagBottomY(_beat: Beat): number { + return this.getLineY(0) - (SlashNoteHeadGlyph.NoteHeadHeight / 2) * this.scale; + } + + protected override getBeamDirection(_helper: BeamingHelper): BeamDirection { + return BeamDirection.Down; + } + + protected override getTupletBeamDirection(_helper: BeamingHelper): BeamDirection { + return BeamDirection.Up; + } + + public override getNoteY(note: Note, requestedPosition: NoteYPosition): number { + let y = super.getNoteY(note, requestedPosition); + if (isNaN(y)) { + y = this.getLineY(0); + } + return y; + } + + protected override calculateBeamYWithDirection(_h: BeamingHelper, _x: number, _direction: BeamDirection): number { + const res = this.resources.numberedNotationFont; + return this.getLineY(0) + res.size * this.scale; + } + + protected override getBarLineStart(_beat: Beat, _direction: BeamDirection): number { + return this.getLineY(0) - (SlashNoteHeadGlyph.NoteHeadHeight / 2) * this.scale; + } + + protected override createLinePreBeatGlyphs(): void { + // Key signature + if ( + this.index === 0 || + (this.bar.previousBar && this.bar.masterBar.keySignature !== this.bar.previousBar.masterBar.keySignature) + ) { + this.createStartSpacing(); + this.createKeySignatureGlyphs(); + } + + if (this._isOnlyNumbered) { + this.createStartSpacing(); + this.createTimeSignatureGlyphs(); + } + } + private createKeySignatureGlyphs() { + this.addPreBeatGlyph( + new NumberedKeySignatureGlyph( + 0, + this.getLineY(0), + this.bar.masterBar.keySignature, + this.bar.masterBar.keySignatureType + ) + ); + } + + private createTimeSignatureGlyphs(): void { + this.addPreBeatGlyph(new SpacingGlyph(0, 0, 5 * this.scale)); + + this.addPreBeatGlyph( + new ScoreTimeSignatureGlyph( + 0, + this.getLineY(0), + this.bar.masterBar.timeSignatureNumerator, + this.bar.masterBar.timeSignatureDenominator, + this.bar.masterBar.timeSignatureCommon + ) + ); + } + + protected override createPostBeatGlyphs(): void { + if (this._isOnlyNumbered) { + super.createBeatGlyphs(); + } + } + + protected override createVoiceGlyphs(v: Voice): void { + for (const b of v.beats) { + let container: NumberedBeatContainerGlyph = new NumberedBeatContainerGlyph(b, this.getVoiceContainer(v)!); + container.preNotes = v.index == 0 ? new NumberedBeatPreNotesGlyph() : new BeatGlyphBase(); + container.onNotes = v.index == 0 ? new NumberedBeatGlyph() : new BeatOnNoteGlyphBase(); + this.addBeatGlyph(container); + } + } + + protected override paintBeamingStem( + _beat: Beat, + _cy: number, + x: number, + topY: number, + bottomY: number, + canvas: ICanvas + ): void { + canvas.lineWidth = BarRendererBase.StemWidth * this.scale; + canvas.beginPath(); + canvas.moveTo(x, topY); + canvas.lineTo(x, bottomY); + canvas.stroke(); + canvas.lineWidth = this.scale; + } +} diff --git a/src/rendering/NumberedBarRendererFactory.ts b/src/rendering/NumberedBarRendererFactory.ts new file mode 100644 index 000000000..b72ec445b --- /dev/null +++ b/src/rendering/NumberedBarRendererFactory.ts @@ -0,0 +1,38 @@ +import { Bar } from '@src/model/Bar'; +import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import { BarRendererFactory } from '@src/rendering/BarRendererFactory'; +import { SlashBarRenderer } from '@src/rendering/SlashBarRenderer'; +import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; +import { Track } from '@src/model/Track'; +import { Staff } from '@src/model'; +import { RenderStaff } from './staves/RenderStaff'; +import { NumberedBarRenderer } from './NumberedBarRenderer'; + +/** + * This Factory produces NumberedBarRenderer instances + */ +export class NumberedBarRendererFactory extends BarRendererFactory { + public get staffId(): string { + return SlashBarRenderer.StaffId; + } + + public override getStaffPaddingTop(staff: RenderStaff): number { + return staff.system.layout.renderer.settings.display.notationStaffPaddingTop; + } + + public override getStaffPaddingBottom(staff: RenderStaff): number { + return staff.system.layout.renderer.settings.display.notationStaffPaddingBottom; + } + + public create(renderer: ScoreRenderer, bar: Bar): BarRendererBase { + return new NumberedBarRenderer(renderer, bar); + } + + public override canCreate(track: Track, staff: Staff): boolean { + return super.canCreate(track, staff) && staff.showNumbered; + } + + public constructor() { + super(); + } +} diff --git a/src/rendering/NumberedBeatContainerGlyph.ts b/src/rendering/NumberedBeatContainerGlyph.ts new file mode 100644 index 000000000..fcb236e2d --- /dev/null +++ b/src/rendering/NumberedBeatContainerGlyph.ts @@ -0,0 +1,64 @@ +import { Beat } from '@src/model/Beat'; +import { Note } from '@src/model/Note'; +import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph'; +import { VoiceContainerGlyph } from '@src/rendering/glyphs/VoiceContainerGlyph'; +import { NumberedTieGlyph } from './glyphs/NumberedTieGlyph'; +import { NumberedSlurGlyph } from './glyphs/NumberedSlurGlyph'; + +export class NumberedBeatContainerGlyph extends BeatContainerGlyph { + private _effectSlurs: NumberedSlurGlyph[] = []; + + public constructor(beat: Beat, voiceContainer: VoiceContainerGlyph) { + super(beat, voiceContainer); + } + + protected override createTies(n: Note): void { + // create a tie if any effect requires it + if (!n.isVisible) { + return; + } + + if (n.isTieOrigin && n.tieDestination!.isVisible) { + let tie = new NumberedTieGlyph(n, n.tieDestination!, false); + this.addTie(tie); + } + if (n.isTieDestination) { + let tie = new NumberedTieGlyph(n.tieOrigin!, n, true); + this.addTie(tie); + } + if (n.isLeftHandTapped && !n.isHammerPullDestination) { + let tapSlur = new NumberedTieGlyph(n, n, false); + this.addTie(tapSlur); + } + // start effect slur on first beat + if (n.isEffectSlurOrigin && n.effectSlurDestination) { + let expanded: boolean = false; + for (let slur of this._effectSlurs) { + if (slur.tryExpand(n, n.effectSlurDestination, false, false)) { + expanded = true; + break; + } + } + if (!expanded) { + let effectSlur = new NumberedSlurGlyph(n, n.effectSlurDestination, false, false); + this._effectSlurs.push(effectSlur); + this.addTie(effectSlur); + } + } + // end effect slur on last beat + if (n.isEffectSlurDestination && n.effectSlurOrigin) { + let expanded: boolean = false; + for (let slur of this._effectSlurs) { + if (slur.tryExpand(n.effectSlurOrigin, n, false, true)) { + expanded = true; + break; + } + } + if (!expanded) { + let effectSlur = new NumberedSlurGlyph(n.effectSlurOrigin, n, false, true); + this._effectSlurs.push(effectSlur); + this.addTie(effectSlur); + } + } + } +} diff --git a/src/rendering/ScoreBarRenderer.ts b/src/rendering/ScoreBarRenderer.ts index e128c4619..f3ffbd039 100644 --- a/src/rendering/ScoreBarRenderer.ts +++ b/src/rendering/ScoreBarRenderer.ts @@ -427,13 +427,13 @@ export class ScoreBarRenderer extends LineBarRenderer { if (ModelUtils.keySignatureIsSharp(currentKey)) { for (let i: number = 0; i < Math.abs(currentKey); i++) { let step: number = ScoreBarRenderer.SharpKsSteps[i] + offsetClef; - newGlyphs.push(new AccidentalGlyph(0, this.getScoreY(step), AccidentalType.Sharp, false)); + newGlyphs.push(new AccidentalGlyph(0, this.getScoreY(step), AccidentalType.Sharp, 1)); newLines.set(step, true); } } else { for (let i: number = 0; i < Math.abs(currentKey); i++) { let step: number = ScoreBarRenderer.FlatKsSteps[i] + offsetClef; - newGlyphs.push(new AccidentalGlyph(0, this.getScoreY(step), AccidentalType.Flat, false)); + newGlyphs.push(new AccidentalGlyph(0, this.getScoreY(step), AccidentalType.Flat, 1)); newLines.set(step, true); } } @@ -450,7 +450,7 @@ export class ScoreBarRenderer extends LineBarRenderer { 0, this.getScoreY(previousKeyPositions[i] + offsetClef), AccidentalType.Natural, - false + 1 ) ); } diff --git a/src/rendering/ScoreBarRendererFactory.ts b/src/rendering/ScoreBarRendererFactory.ts index 573eaccfa..4c8d58043 100644 --- a/src/rendering/ScoreBarRendererFactory.ts +++ b/src/rendering/ScoreBarRendererFactory.ts @@ -6,7 +6,7 @@ import { ScoreRenderer } from '@src/rendering/ScoreRenderer'; import { RenderStaff } from './staves/RenderStaff'; /** - * This Factory procudes ScoreBarRenderer instances + * This Factory produces ScoreBarRenderer instances */ export class ScoreBarRendererFactory extends BarRendererFactory { public get staffId(): string { diff --git a/src/rendering/SlashBarRenderer.ts b/src/rendering/SlashBarRenderer.ts index d124ca9f2..867729f9a 100644 --- a/src/rendering/SlashBarRenderer.ts +++ b/src/rendering/SlashBarRenderer.ts @@ -13,6 +13,8 @@ import { SlashBeatContainerGlyph } from './SlashBeatContainerGlyph'; import { BeatGlyphBase } from './glyphs/BeatGlyphBase'; import { SlashBeatGlyph } from './glyphs/SlashBeatGlyph'; import { BeatOnNoteGlyphBase } from './glyphs/BeatOnNoteGlyphBase'; +import { SpacingGlyph } from './glyphs/SpacingGlyph'; +import { ScoreTimeSignatureGlyph } from './glyphs/ScoreTimeSignatureGlyph'; /** * This BarRenderer renders a bar using Slash Rhythm notation @@ -21,9 +23,12 @@ export class SlashBarRenderer extends LineBarRenderer { public static readonly StaffId: string = 'slash'; public simpleWhammyOverflow: number = 0; + private _isOnlySlash: boolean; public constructor(renderer: ScoreRenderer, bar: Bar) { super(renderer, bar); + // ignore numbered notation here + this._isOnlySlash = !bar.staff.showTablature && !bar.staff.showStandardNotation; } public override get lineSpacing(): number { @@ -60,7 +65,7 @@ export class SlashBarRenderer extends LineBarRenderer { } } } - if (hasTuplets) { + if (hasTuplets) { this.registerOverflowTop(this.tupletSize); } } @@ -97,7 +102,28 @@ export class SlashBarRenderer extends LineBarRenderer { return this.getLineY(0) - (SlashNoteHeadGlyph.NoteHeadHeight / 2) * this.scale; } - protected override createLinePreBeatGlyphs(): void {} + protected override createLinePreBeatGlyphs(): void { + // Key signature + if(this._isOnlySlash) { + this.createStartSpacing(); + this.createTimeSignatureGlyphs(); + } + } + + + private createTimeSignatureGlyphs(): void { + this.addPreBeatGlyph(new SpacingGlyph(0, 0, 5 * this.scale)); + + this.addPreBeatGlyph( + new ScoreTimeSignatureGlyph( + 0, + this.getLineY(0), + this.bar.masterBar.timeSignatureNumerator, + this.bar.masterBar.timeSignatureDenominator, + this.bar.masterBar.timeSignatureCommon + ) + ); + } protected override createVoiceGlyphs(v: Voice): void { for (const b of v.beats) { diff --git a/src/rendering/SlashBarRendererFactory.ts b/src/rendering/SlashBarRendererFactory.ts index ff99fa44e..e2cb04b2d 100644 --- a/src/rendering/SlashBarRendererFactory.ts +++ b/src/rendering/SlashBarRendererFactory.ts @@ -8,7 +8,7 @@ import { Staff } from '@src/model'; import { RenderStaff } from './staves/RenderStaff'; /** - * This Factory procudes SlashBarRenderer instances + * This Factory produces SlashBarRenderer instances */ export class SlashBarRendererFactory extends BarRendererFactory { public get staffId(): string { diff --git a/src/rendering/glyphs/AccidentalGlyph.ts b/src/rendering/glyphs/AccidentalGlyph.ts index 2dc75ec0c..da4273eb6 100644 --- a/src/rendering/glyphs/AccidentalGlyph.ts +++ b/src/rendering/glyphs/AccidentalGlyph.ts @@ -1,19 +1,16 @@ import { AccidentalType } from '@src/model/AccidentalType'; import { MusicFontGlyph } from '@src/rendering/glyphs/MusicFontGlyph'; import { MusicFontSymbol } from '@src/model/MusicFontSymbol'; -import { NoteHeadGlyph } from '@src/rendering/glyphs/NoteHeadGlyph'; export class AccidentalGlyph extends MusicFontGlyph { - private _isGrace: boolean; private _accidentalType: AccidentalType; - public constructor(x: number, y: number, accidentalType: AccidentalType, isGrace: boolean = false) { - super(x, y, isGrace ? NoteHeadGlyph.GraceScale : 1, AccidentalGlyph.getMusicSymbol(accidentalType)); - this._isGrace = isGrace; + public constructor(x: number, y: number, accidentalType: AccidentalType, scale: number) { + super(x, y, scale, AccidentalGlyph.getMusicSymbol(accidentalType)); this._accidentalType = accidentalType; } - private static getMusicSymbol(accidentalType: AccidentalType): MusicFontSymbol { + public static getMusicSymbol(accidentalType: AccidentalType): MusicFontSymbol { switch (accidentalType) { case AccidentalType.Natural: return MusicFontSymbol.AccidentalNatural; @@ -44,6 +41,6 @@ export class AccidentalGlyph extends MusicFontGlyph { this.width = 8; break; } - this.width = this.width * (this._isGrace ? NoteHeadGlyph.GraceScale : 1) * this.scale; + this.width = this.width * this.glyphScale * this.scale; } } diff --git a/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts b/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts index b318ed6a3..545ad9e16 100644 --- a/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts +++ b/src/rendering/glyphs/BendNoteHeadGroupGlyph.ts @@ -67,7 +67,7 @@ export class BendNoteHeadGroupGlyph extends ScoreNoteChordGlyphBase { this._postNoteParenthesis!.addParenthesisOnLine(line, true); } if (accidental !== AccidentalType.None) { - let g = new AccidentalGlyph(0, noteHeadGlyph.y, accidental, true); + let g = new AccidentalGlyph(0, noteHeadGlyph.y, accidental, NoteHeadGlyph.GraceScale); g.renderer = this.renderer; this._accidentals.renderer = this.renderer; this._accidentals.addGlyph(g); diff --git a/src/rendering/glyphs/NumberedBeatGlyph.ts b/src/rendering/glyphs/NumberedBeatGlyph.ts new file mode 100644 index 000000000..7a86ba73b --- /dev/null +++ b/src/rendering/glyphs/NumberedBeatGlyph.ts @@ -0,0 +1,296 @@ +import { GraceType } from '@src/model/GraceType'; +import { Note } from '@src/model/Note'; +import { BeatOnNoteGlyphBase } from '@src/rendering/glyphs/BeatOnNoteGlyphBase'; +import { NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; +import { BeatBounds } from '@src/rendering/utils/BeatBounds'; +import { NoteBounds } from '../utils/NoteBounds'; +import { Bounds } from '../utils/Bounds'; +import { NumberedNoteHeadGlyph } from './NumberedNoteHeadGlyph'; +import { AccidentalType, Duration, KeySignatureType, NoteAccidentalMode } from '@src/model'; +import { NumberedBarRenderer } from '../NumberedBarRenderer'; +import { AccidentalHelper } from '../utils/AccidentalHelper'; +import { BeatGlyphBase } from './BeatGlyphBase'; +import { AccidentalGroupGlyph } from './AccidentalGroupGlyph'; +import { AccidentalGlyph } from './AccidentalGlyph'; +import { ModelUtils } from '@src/model/ModelUtils'; +import { NoteHeadGlyph } from './NoteHeadGlyph'; +import { SpacingGlyph } from './SpacingGlyph'; +import { CircleGlyph } from './CircleGlyph'; +import { NumberedDashGlyph } from './NumberedDashGlyph'; + +export class NumberedBeatPreNotesGlyph extends BeatGlyphBase { + public isNaturalizeAccidental = false; + public accidental: AccidentalType = AccidentalType.None; + + public override doLayout(): void { + if (!this.container.beat.isRest && !this.container.beat.isEmpty) { + let accidentals: AccidentalGroupGlyph = new AccidentalGroupGlyph(); + accidentals.renderer = this.renderer; + + if (this.container.beat.notes.length > 0) { + const note = this.container.beat.notes[0]; + + // Notes + // - Compared to standard notation accidentals: + // - Flat keysigs: When there is a naturalize symbol (against key signature, not naturalizing same line) we have a # in Numbered notation + // - Flat keysigs: When there is a flat symbol standard notation we also have a flat in Numbered notation + // - C keysig: A sharp on standard notation is a sharp on numbered notation + // - # keysigs: When there is a # symbol on standard notation we also a sharp in numbered notation + // - # keysigs: When there is a naturalize symbol (against key signature, not naturalizing same line) we have a flat in Numbered notation + + // Or generally: + // - numbered notation has the same accidentals as standard notation if applied + // - when the standard notation naturalizes the accidental from the key signature, the numbered notation has the reversed accidental + + const accidentalMode = note ? note.accidentalMode : NoteAccidentalMode.Default; + const noteValue = AccidentalHelper.getNoteValue(note); + let accidentalToSet: AccidentalType = AccidentalHelper.computeAccidental( + this.renderer.bar.masterBar.keySignature, + accidentalMode, + noteValue, + note.hasQuarterToneOffset + ); + + if (accidentalToSet == AccidentalType.Natural) { + const ks: number = this.renderer.bar.masterBar.keySignature; + const ksi: number = ks + 7; + const naturalizeAccidentalForKeySignature: AccidentalType = + ksi < 7 ? AccidentalType.Sharp : AccidentalType.Flat; + accidentalToSet = naturalizeAccidentalForKeySignature; + this.isNaturalizeAccidental = true; + } + + // do we need an accidental on the note? + if (accidentalToSet !== AccidentalType.None) { + this.accidental = accidentalToSet; + let sr: NumberedBarRenderer = this.renderer as NumberedBarRenderer; + + let g = new AccidentalGlyph( + 0, + sr.getLineY(0), + accidentalToSet, + note.beat.graceType !== GraceType.None + ? NoteHeadGlyph.GraceScale * NoteHeadGlyph.GraceScale + : NoteHeadGlyph.GraceScale + ); + g.renderer = this.renderer; + accidentals.addGlyph(g); + this.addGlyph(accidentals); + this.addGlyph(new SpacingGlyph(0, 0, 4 * this.scale)); + } + } + } + super.doLayout(); + } +} + +export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { + public noteHeads: NumberedNoteHeadGlyph | null = null; + + public octaveDots:number = 0; + + public override getNoteX(_note: Note, requestedPosition: NoteXPosition): number { + if (this.noteHeads) { + let pos = this.noteHeads.x; + switch (requestedPosition) { + case NoteXPosition.Left: + break; + case NoteXPosition.Center: + pos += this.noteHeads.width / 2; + break; + case NoteXPosition.Right: + pos += this.noteHeads.width; + break; + } + return pos; + } + return 0; + } + + public override buildBoundingsLookup(beatBounds: BeatBounds, cx: number, cy: number) { + if (this.noteHeads && this.container.beat.notes.length > 0) { + const noteBounds = new NoteBounds(); + noteBounds.note = this.container.beat.notes[0]; + noteBounds.noteHeadBounds = new Bounds(); + noteBounds.noteHeadBounds.x = cx + this.x + this.noteHeads.x; + noteBounds.noteHeadBounds.y = cy + this.y + this.noteHeads.y - this.noteHeads.height / 2; + noteBounds.noteHeadBounds.w = this.width; + noteBounds.noteHeadBounds.h = this.height; + beatBounds.addNote(noteBounds); + } + } + + public override getNoteY(_note: Note, requestedPosition: NoteYPosition): number { + if (this.noteHeads) { + let pos = this.y + this.noteHeads.y; + + switch (requestedPosition) { + case NoteYPosition.Top: + case NoteYPosition.TopWithStem: + pos -= this.noteHeads.height / 2 + 2 * this.scale; + break; + case NoteYPosition.Center: + break; + case NoteYPosition.Bottom: + case NoteYPosition.BottomWithStem: + pos += this.noteHeads.height / 2; + break; + } + + return pos; + } + return 0; + } + + public override updateBeamingHelper(): void { + if (this.beamingHelper && this.noteHeads) { + this.beamingHelper.registerBeatLineX( + 'numbered', + this.container.beat, + this.container.x + this.x + this.noteHeads.x, + this.container.x + this.x + this.noteHeads.x + this.noteHeads.width + ); + } + } + + public static MajorKeySignatureOneValues: Array = [ + // Flats + 59, 66, 61, 68, 63, 58, 65, + // natural + 60, + // sharps (where the value is true, a flat accidental is required for the notes) + 67, 62, 69, 64, 71, 66, 61 + ]; + + public static MinorKeySignatureOneValues: Array = [ + // Flats + 71, 66, 73, 68, 63, 70, 65, + // natural + 72, + // sharps (where the value is true, a flat accidental is required for the notes) + 67, 74, 69, 64, 71, 66, 73 + ]; + + public override doLayout(): void { + // create glyphs + let sr = this.renderer as NumberedBarRenderer; + + if (sr.shortestDuration < this.container.beat.duration) { + sr.shortestDuration = this.container.beat.duration; + } + + const glyphY = sr.getLineY(sr.getNoteLine()); + + if (!this.container.beat.isEmpty) { + let numberWithinOctave = '0'; + if (this.container.beat.notes.length > 0) { + const kst: number = this.renderer.bar.masterBar.keySignatureType; + const ks: number = this.renderer.bar.masterBar.keySignature; + const ksi: number = ks + 7; + + const oneNoteValues = + kst === KeySignatureType.Minor + ? NumberedBeatGlyph.MinorKeySignatureOneValues + : NumberedBeatGlyph.MajorKeySignatureOneValues; + const oneNoteValue = oneNoteValues[ksi]; + + const note = this.container.beat.notes[0]; + + if (note.isDead) { + numberWithinOctave = 'X'; + } else { + let noteValue = note.displayValue - oneNoteValue; + + let index = noteValue < 0 ? ((noteValue % 12) + 12) % 12 : noteValue % 12; + + let dots = noteValue < 0 ? ((Math.abs(noteValue) + 12) / 12) | 0 : (noteValue / 12) | 0; + if (noteValue < 0) { + dots *= -1; + } + this.octaveDots = dots; + sr.registerOctave(dots); + + const stepList = + ModelUtils.keySignatureIsSharp(ks) || ModelUtils.keySignatureIsNatural(ks) + ? AccidentalHelper.FlatNoteSteps + : AccidentalHelper.SharpNoteSteps; + + let steps = stepList[index] + 1; + + const hasAccidental = AccidentalHelper.AccidentalNotes[index]; + if ( + hasAccidental && + !(this.container.preNotes as NumberedBeatPreNotesGlyph).isNaturalizeAccidental + ) { + if (ksi < 7) { + steps++; + } else { + steps--; + } + } + + numberWithinOctave = steps.toString(); + } + } + + const isGrace: boolean = this.container.beat.graceType !== GraceType.None; + const noteHeadGlyph = new NumberedNoteHeadGlyph( + 0, + glyphY, + numberWithinOctave, + isGrace + ); + this.noteHeads = noteHeadGlyph; + + this.addGlyph(noteHeadGlyph); + + // + // Note dots + if (this.container.beat.dots > 0 && this.container.beat.duration >= Duration.Quarter) { + this.addGlyph(new SpacingGlyph(0, 0, 5 * this.scale)); + for (let i: number = 0; i < this.container.beat.dots; i++) { + const dot = new CircleGlyph(0, sr.getLineY(0), 1.5 * this.scale); + dot.renderer = this.renderer; + this.addGlyph(dot); + } + } + + // + // Dashes + let numberOfQuarterNotes = 0; + switch (this.container.beat.duration) { + case Duration.QuadrupleWhole: + numberOfQuarterNotes = 16; + break; + case Duration.DoubleWhole: + numberOfQuarterNotes = 8; + break; + case Duration.Whole: + numberOfQuarterNotes = 4; + break; + case Duration.Half: + numberOfQuarterNotes = 2; + break; + } + + let numberOfAddedQuarters = numberOfQuarterNotes; + for(let i = 0; i < this.container.beat.dots; i++) { + numberOfAddedQuarters = (numberOfAddedQuarters / 2) | 0 + numberOfQuarterNotes += numberOfAddedQuarters + } + for(let i = 0; i < numberOfQuarterNotes - 1; i++) { + const dash = new NumberedDashGlyph(0, sr.getLineY(0)); + dash.renderer = this.renderer; + this.addGlyph(dash); + } + } + + super.doLayout(); + + if (this.container.beat.isEmpty) { + this.centerX = this.width / 2; + } else { + this.centerX = this.noteHeads!.x + this.noteHeads!.width / 2; + } + } +} diff --git a/src/rendering/glyphs/NumberedDashGlyph.ts b/src/rendering/glyphs/NumberedDashGlyph.ts new file mode 100644 index 000000000..cdbb84fae --- /dev/null +++ b/src/rendering/glyphs/NumberedDashGlyph.ts @@ -0,0 +1,16 @@ +import { ICanvas } from '@src/platform'; +import { Glyph } from './Glyph'; +import { NumberedBarRenderer } from '../NumberedBarRenderer'; + +export class NumberedDashGlyph extends Glyph { + private static Padding = 3; + public override doLayout(): void { + this.width = (14 + NumberedDashGlyph.Padding) * this.scale; + this.height = NumberedBarRenderer.BarSize * this.scale; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + const padding = NumberedDashGlyph.Padding * this.scale; + canvas.fillRect(cx + this.x, cy + this.y, this.width - padding, this.height); + } +} diff --git a/src/rendering/glyphs/NumberedKeySignatureGlyph.ts b/src/rendering/glyphs/NumberedKeySignatureGlyph.ts new file mode 100644 index 000000000..9a4c7ea93 --- /dev/null +++ b/src/rendering/glyphs/NumberedKeySignatureGlyph.ts @@ -0,0 +1,179 @@ +import { ICanvas, TextBaseline } from '@src/platform'; +import { Glyph } from './Glyph'; +import { AccidentalType, KeySignature, KeySignatureType } from '@src/model'; +import { AccidentalGlyph } from './AccidentalGlyph'; + +export class NumberedKeySignatureGlyph extends Glyph { + private _keySignature: KeySignature; + private _keySignatureType: KeySignatureType; + + private _text: string = ''; + private _accidental: AccidentalType = AccidentalType.None; + private _accidentalOffset: number = 0; + + public constructor(x: number, y: number, keySignature: KeySignature, keySignatureType: KeySignatureType) { + super(x, y); + this._keySignature = keySignature; + this._keySignatureType = keySignatureType; + } + + public override doLayout(): void { + super.doLayout(); + const text = '1 = '; + let text2 = ''; + let accidental = AccidentalType.None; + switch (this._keySignatureType) { + case KeySignatureType.Major: + switch (this._keySignature) { + case KeySignature.Cb: + text2 = ' C'; + accidental = AccidentalType.Flat; + break; + case KeySignature.Gb: + text2 = ' G'; + accidental = AccidentalType.Flat; + break; + case KeySignature.Db: + text2 = ' D'; + accidental = AccidentalType.Flat; + break; + case KeySignature.Ab: + text2 = ' A'; + accidental = AccidentalType.Flat; + break; + case KeySignature.Eb: + text2 = ' E'; + accidental = AccidentalType.Flat; + break; + case KeySignature.Bb: + text2 = ' B'; + accidental = AccidentalType.Flat; + break; + case KeySignature.F: + text2 = 'F'; + break; + case KeySignature.C: + text2 = 'C'; + accidental = AccidentalType.None; + break; + case KeySignature.G: + text2 = 'G'; + accidental = AccidentalType.None; + break; + case KeySignature.D: + text2 = 'D'; + accidental = AccidentalType.None; + break; + case KeySignature.A: + text2 = 'A'; + accidental = AccidentalType.None; + break; + case KeySignature.E: + text2 = 'E'; + accidental = AccidentalType.None; + break; + case KeySignature.B: + text2 = 'B'; + accidental = AccidentalType.None; + break; + case KeySignature.FSharp: + text2 = ' F'; + accidental = AccidentalType.Sharp; + break; + case KeySignature.CSharp: + text2 = ' C'; + accidental = AccidentalType.Sharp; + break; + } + break; + case KeySignatureType.Minor: + switch (this._keySignature) { + case KeySignature.Cb: + text2 = ' a'; + accidental = AccidentalType.Flat; + break; + case KeySignature.Gb: + text2 = ' e'; + accidental = AccidentalType.Flat; + break; + case KeySignature.Db: + text2 = ' b'; + accidental = AccidentalType.Flat; + break; + case KeySignature.Ab: + text2 = 'f'; + accidental = AccidentalType.None; + break; + case KeySignature.Eb: + text2 = 'c'; + accidental = AccidentalType.None; + break; + case KeySignature.Bb: + text2 = 'g'; + accidental = AccidentalType.None; + break; + case KeySignature.F: + text2 = 'd'; + break; + case KeySignature.C: + text2 = 'a'; + accidental = AccidentalType.None; + break; + case KeySignature.G: + text2 = 'e'; + accidental = AccidentalType.None; + break; + case KeySignature.D: + text2 = 'b'; + accidental = AccidentalType.None; + break; + case KeySignature.A: + text2 = ' f'; + accidental = AccidentalType.Sharp; + break; + case KeySignature.E: + text2 = ' c'; + accidental = AccidentalType.Sharp; + break; + case KeySignature.B: + text2 = ' g'; + accidental = AccidentalType.Sharp; + break; + case KeySignature.FSharp: + text2 = ' d'; + accidental = AccidentalType.Sharp; + break; + case KeySignature.CSharp: + text2 = ' a'; + accidental = AccidentalType.Sharp; + break; + } + break; + } + + this._text = text + text2; + this._accidental = accidental; + const c = this.renderer.scoreRenderer.canvas!; + const res = this.renderer.resources; + c.font = res.numberedNotationFont; + this._accidentalOffset = c.measureText(text).width; + this.width = c.measureText(text + text2).width; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + const res = this.renderer.resources; + canvas.font = res.numberedNotationFont; + canvas.textBaseline = TextBaseline.Middle; + canvas.fillText(this._text, cx + this.x, cy + this.y); + + if (this._accidental != AccidentalType.None) { + canvas.fillMusicFontSymbol( + cx + this.x + this._accidentalOffset, + cy + this.y, + 0.7, + AccidentalGlyph.getMusicSymbol(this._accidental), + false + ); + } + } +} diff --git a/src/rendering/glyphs/NumberedNoteHeadGlyph.ts b/src/rendering/glyphs/NumberedNoteHeadGlyph.ts new file mode 100644 index 000000000..4d5eb7a18 --- /dev/null +++ b/src/rendering/glyphs/NumberedNoteHeadGlyph.ts @@ -0,0 +1,31 @@ +import { ICanvas, TextAlign, TextBaseline } from '@src/platform/ICanvas'; +import { NoteHeadGlyph } from './NoteHeadGlyph'; +import { Glyph } from './Glyph'; + +export class NumberedNoteHeadGlyph extends Glyph { + public static readonly NoteHeadHeight: number = 17; + public static readonly NoteHeadWidth: number = 12; + + private _isGrace: boolean; + private _number: string; + + public constructor(x: number, y: number, number: string, isGrace: boolean) { + super(x, y); + this._isGrace = isGrace; + this._number = number; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + const res = this.renderer.resources; + canvas.font = this._isGrace ? res.numberedNotationGraceFont : res.numberedNotationFont; + canvas.textBaseline = TextBaseline.Middle; + canvas.textAlign = TextAlign.Left; + canvas.fillText(this._number.toString(), cx + this.x, cy + this.y); + } + + public override doLayout(): void { + const scale: number = (this._isGrace ? NoteHeadGlyph.GraceScale : 1) * this.scale; + this.width = NumberedNoteHeadGlyph.NoteHeadWidth * scale; + this.height = NumberedNoteHeadGlyph.NoteHeadHeight * scale; + } +} diff --git a/src/rendering/glyphs/NumberedSlurGlyph.ts b/src/rendering/glyphs/NumberedSlurGlyph.ts new file mode 100644 index 000000000..11c98c7e3 --- /dev/null +++ b/src/rendering/glyphs/NumberedSlurGlyph.ts @@ -0,0 +1,76 @@ +import { Note } from '@src/model/Note'; +import { ICanvas } from '@src/platform/ICanvas'; +import { BarRendererBase } from '@src/rendering/BarRendererBase'; +import { TabTieGlyph } from '@src/rendering/glyphs/TabTieGlyph'; +import { BeamDirection } from '@src/rendering/utils/BeamDirection'; + +export class NumberedSlurGlyph extends TabTieGlyph { + private _direction: BeamDirection; + private _forSlide: boolean; + + public constructor(startNote: Note, endNote: Note, forSlide: boolean, forEnd: boolean = false) { + super(startNote, endNote, forEnd); + this._direction = BeamDirection.Up; + this._forSlide = forSlide; + } + + protected override getTieHeight(startX: number, startY: number, endX: number, endY: number): number { + return Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight; + } + + public tryExpand(startNote: Note, endNote: Note, forSlide: boolean, forEnd: boolean): boolean { + // same type required + if (this._forSlide !== forSlide) { + return false; + } + if (this.forEnd !== forEnd) { + return false; + } + // same start and endbeat + if (this.startNote.beat.id !== startNote.beat.id) { + return false; + } + if (this.endNote.beat.id !== endNote.beat.id) { + return false; + } + // if we can expand, expand in correct direction + switch (this._direction) { + case BeamDirection.Up: + if (startNote.realValue > this.startNote.realValue) { + this.startNote = startNote; + this.startBeat = startNote.beat; + } + if (endNote.realValue > this.endNote.realValue) { + this.endNote = endNote; + this.endBeat = endNote.beat; + } + break; + case BeamDirection.Down: + if (startNote.realValue < this.startNote.realValue) { + this.startNote = startNote; + this.startBeat = startNote.beat; + } + if (endNote.realValue < this.endNote.realValue) { + this.endNote = endNote; + this.endBeat = endNote.beat; + } + break; + } + return true; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + let startNoteRenderer: BarRendererBase = this.renderer.scoreRenderer.layout!.getRendererForBar( + this.renderer.staff.staveId, + this.startBeat!.voice.bar + )!; + let direction: BeamDirection = this.getBeamDirection(this.startBeat!, startNoteRenderer); + let slurId: string = 'numbered.slur.' + this.startNote.beat.id + '.' + this.endNote.beat.id + '.' + direction; + let renderer = this.renderer; + let isSlurRendered: boolean = renderer.staff.getSharedLayoutData(slurId, false); + if (!isSlurRendered) { + renderer.staff.setSharedLayoutData(slurId, true); + super.paint(cx, cy, canvas); + } + } +} diff --git a/src/rendering/glyphs/NumberedTieGlyph.ts b/src/rendering/glyphs/NumberedTieGlyph.ts new file mode 100644 index 000000000..68943482e --- /dev/null +++ b/src/rendering/glyphs/NumberedTieGlyph.ts @@ -0,0 +1,51 @@ +import { Beat } from '@src/model/Beat'; +import { Note } from '@src/model/Note'; +import { BarRendererBase, NoteXPosition, NoteYPosition } from '@src/rendering/BarRendererBase'; +import { TieGlyph } from '@src/rendering/glyphs/TieGlyph'; +import { BeamDirection } from '@src/rendering/utils/BeamDirection'; + +export class NumberedTieGlyph extends TieGlyph { + protected startNote: Note; + protected endNote: Note; + + public constructor(startNote: Note, endNote: Note, forEnd: boolean = false) { + super(!startNote ? null : startNote.beat, !endNote ? null : endNote.beat, forEnd); + this.startNote = startNote; + this.endNote = endNote; + } + + protected override shouldDrawBendSlur() { + return this.renderer.settings.notation.extendBendArrowsOnTiedNotes && !!this.startNote.bendOrigin && this.startNote.isTieOrigin; + } + + public override doLayout(): void { + super.doLayout(); + } + + protected override getBeamDirection(beat: Beat, noteRenderer: BarRendererBase): BeamDirection { + return BeamDirection.Up; + + } + + protected override getStartY(): number { + return this.startNoteRenderer!.getNoteY(this.startNote, NoteYPosition.Top); + } + + protected override getEndY(): number { + return this.getStartY(); + } + + protected override getStartX(): number { + if(this.startNote === this.endNote) { + return this.getEndX() - 20 * this.scale; + } + return this.startNoteRenderer!.getNoteX(this.startNote, NoteXPosition.Center); + } + + protected override getEndX(): number { + if(this.startNote === this.endNote) { + return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Left); + } + return this.endNoteRenderer!.getNoteX(this.endNote, NoteXPosition.Center); + } +} diff --git a/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts b/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts index f9ecb8be0..1970cdffe 100644 --- a/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts +++ b/src/rendering/glyphs/ScoreBeatPreNotesGlyph.ts @@ -113,8 +113,9 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { let accidental: AccidentalType = sr.accidentalHelper.applyAccidental(n); let noteLine: number = sr.getNoteLine(n); let isGrace: boolean = this.container.beat.graceType !== GraceType.None; + const graceScale = isGrace ? NoteHeadGlyph.GraceScale : 1; if (accidental !== AccidentalType.None) { - let g = new AccidentalGlyph(0, sr.getScoreY(noteLine), accidental, isGrace); + let g = new AccidentalGlyph(0, sr.getScoreY(noteLine), accidental, graceScale); g.renderer = this.renderer; accidentals.addGlyph(g); } @@ -122,7 +123,7 @@ export class ScoreBeatPreNotesGlyph extends BeatGlyphBase { let harmonicFret: number = n.displayValue + n.harmonicPitch; accidental = sr.accidentalHelper.applyAccidentalForValue(n.beat, harmonicFret, isGrace, false); noteLine = sr.accidentalHelper.getNoteLineForValue(harmonicFret, false); - let g = new AccidentalGlyph(0, sr.getScoreY(noteLine), accidental, isGrace); + let g = new AccidentalGlyph(0, sr.getScoreY(noteLine), accidental, graceScale); g.renderer = this.renderer; accidentals.addGlyph(g); } diff --git a/src/rendering/utils/AccidentalHelper.ts b/src/rendering/utils/AccidentalHelper.ts index 99d006453..148061693 100644 --- a/src/rendering/utils/AccidentalHelper.ts +++ b/src/rendering/utils/AccidentalHelper.ts @@ -6,27 +6,27 @@ import { NoteAccidentalMode } from '@src/model/NoteAccidentalMode'; import { ModelUtils } from '@src/model/ModelUtils'; import { PercussionMapper } from '@src/model/PercussionMapper'; import { ScoreBarRenderer } from '@src/rendering/ScoreBarRenderer'; - +import { LineBarRenderer } from '../LineBarRenderer'; +import { KeySignature } from '@src/model'; class BeatLines { public maxLine: number = -1000; public minLine: number = -1000; } - /** * This small utilty public class allows the assignment of accidentals within a * desired scope. */ export class AccidentalHelper { private _bar: Bar; - private _barRenderer: ScoreBarRenderer; + private _barRenderer: LineBarRenderer; /** * a lookup list containing an info whether the notes within an octave * need an accidental rendered. the accidental symbol is determined based on the type of key signature. */ - private static KeySignatureLookup: Array = [ + public static KeySignatureLookup: Array = [ // Flats (where the value is true, a flat accidental is required for the notes) [true, true, true, true, true, true, true, true, true, true, true, true], [true, true, true, true, true, false, true, true, true, true, true, true], @@ -51,7 +51,7 @@ export class AccidentalHelper { * Contains the list of notes within an octave have accidentals set. */ // prettier-ignore - private static AccidentalNotes: boolean[] = [ + public static AccidentalNotes: boolean[] = [ false, true, false, true, false, false, true, false, true, false, true, false ]; @@ -72,12 +72,12 @@ export class AccidentalHelper { /** * The step offsets of the notes within an octave in case of for sharp keysignatures */ - private static SharpNoteSteps: number[] = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6]; + public static SharpNoteSteps: number[] = [0, 0, 1, 1, 2, 3, 3, 4, 4, 5, 5, 6]; /** * The step offsets of the notes within an octave in case of for flat keysignatures */ - private static FlatNoteSteps: number[] = [0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 6]; + public static FlatNoteSteps: number[] = [0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 6]; private _registeredAccidentals: Map = new Map(); private _appliedScoreLines: Map = new Map(); @@ -104,7 +104,7 @@ export class AccidentalHelper { */ public minLine: number = -1000; - public constructor(barRenderer: ScoreBarRenderer) { + public constructor(barRenderer: LineBarRenderer) { this._barRenderer = barRenderer; this._bar = barRenderer.bar; } @@ -164,7 +164,12 @@ export class AccidentalHelper { * @param isHelperNote true if the note registered via this call, is a small helper note (e.g. for bends) or false if it is a main note head (e.g. for harmonics) * @returns */ - public applyAccidentalForValue(relatedBeat: Beat, noteValue: number, quarterBend: boolean, isHelperNote: boolean): AccidentalType { + public applyAccidentalForValue( + relatedBeat: Beat, + noteValue: number, + quarterBend: boolean, + isHelperNote: boolean + ): AccidentalType { return this.getAccidental(noteValue, quarterBend, relatedBeat, isHelperNote, null); } @@ -175,12 +180,85 @@ export class AccidentalHelper { if (bar.staff.isPercussion) { line = AccidentalHelper.getPercussionLine(bar, noteValue); } else { - const accidentalMode = note ? note.accidentalMode : NoteAccidentalMode.Default; - line = AccidentalHelper.calculateNoteLine(bar, noteValue, accidentalMode); + line = AccidentalHelper.calculateNoteLine(bar, noteValue); } return line; } + public static computeAccidental( + keySignature: KeySignature, + accidentalMode: NoteAccidentalMode, + noteValue: number, + quarterBend: boolean + ) { + let ks: number = keySignature; + let ksi: number = ks + 7; + let index: number = noteValue % 12; + + let accidentalForKeySignature: AccidentalType = ksi < 7 ? AccidentalType.Flat : AccidentalType.Sharp; + let hasKeySignatureAccidentalSetForNote: boolean = AccidentalHelper.KeySignatureLookup[ksi][index]; + let hasNoteAccidentalWithinOctave: boolean = AccidentalHelper.AccidentalNotes[index]; + + // the general logic is like this: + // - we check if the key signature has an accidental defined + // - we calculate which accidental a note needs according to its index in the octave + // - if the accidental is already placed at this line, nothing needs to be done, otherwise we place it + // - if there should not be an accidental, but there is one in the key signature, we clear it. + + // the exceptions are: + // - for quarter bends we just place the corresponding accidental + // - the accidental mode can enforce the accidentals for the note + + let accidentalToSet: AccidentalType = AccidentalType.None; + if (quarterBend) { + accidentalToSet = hasNoteAccidentalWithinOctave ? accidentalForKeySignature : AccidentalType.Natural; + switch (accidentalToSet) { + case AccidentalType.Natural: + accidentalToSet = AccidentalType.NaturalQuarterNoteUp; + break; + case AccidentalType.Sharp: + accidentalToSet = AccidentalType.SharpQuarterNoteUp; + break; + case AccidentalType.Flat: + accidentalToSet = AccidentalType.FlatQuarterNoteUp; + break; + } + } else { + // define which accidental should be shown ignoring what might be set on the KS already + switch (accidentalMode) { + case NoteAccidentalMode.ForceSharp: + accidentalToSet = AccidentalType.Sharp; + break; + case NoteAccidentalMode.ForceDoubleSharp: + accidentalToSet = AccidentalType.DoubleSharp; + break; + case NoteAccidentalMode.ForceFlat: + accidentalToSet = AccidentalType.Flat; + break; + case NoteAccidentalMode.ForceDoubleFlat: + accidentalToSet = AccidentalType.DoubleFlat; + break; + default: + // if note has an accidental in the octave, we place a symbol + // according to the Key Signature + if (hasNoteAccidentalWithinOctave) { + accidentalToSet = accidentalForKeySignature; + } else if (hasKeySignatureAccidentalSetForNote) { + // note does not get an accidental, but KS defines one -> Naturalize + accidentalToSet = AccidentalType.Natural; + } + break; + } + } + + // if there is no accidental on the line, and the key signature has it set already, we clear it on the note + if (hasKeySignatureAccidentalSetForNote && accidentalToSet === accidentalForKeySignature) { + accidentalToSet = AccidentalType.None; + } + + return accidentalToSet; + } + private getAccidental( noteValue: number, quarterBend: boolean, @@ -188,125 +266,79 @@ export class AccidentalHelper { isHelperNote: boolean, note: Note | null = null ): AccidentalType { - let accidentalToSet: AccidentalType = AccidentalType.None; let line: number = 0; + let accidentalToSet = AccidentalType.None; + if (this._bar.staff.isPercussion) { line = AccidentalHelper.getPercussionLine(this._bar, noteValue); } else { const accidentalMode = note ? note.accidentalMode : NoteAccidentalMode.Default; - line = AccidentalHelper.calculateNoteLine(this._bar, noteValue, accidentalMode); - - let ks: number = this._bar.masterBar.keySignature; - let ksi: number = ks + 7; - let index: number = noteValue % 12; - - let accidentalForKeySignature: AccidentalType = ksi < 7 ? AccidentalType.Flat : AccidentalType.Sharp; - let hasKeySignatureAccidentalSetForNote: boolean = AccidentalHelper.KeySignatureLookup[ksi][index]; - let hasNoteAccidentalWithinOctave: boolean = AccidentalHelper.AccidentalNotes[index]; - - // the general logic is like this: - // - we check if the key signature has an accidental defined - // - we calculate which accidental a note needs according to its index in the octave - // - if the accidental is already placed at this line, nothing needs to be done, otherwise we place it - // - if there should not be an accidental, but there is one in the key signature, we clear it. - - // the exceptions are: - // - for quarter bends we just place the corresponding accidental - // - the accidental mode can enforce the accidentals for the note - - if (quarterBend) { - accidentalToSet = hasNoteAccidentalWithinOctave ? accidentalForKeySignature : AccidentalType.Natural; - switch (accidentalToSet) { - case AccidentalType.Natural: - accidentalToSet = AccidentalType.NaturalQuarterNoteUp; - break; - case AccidentalType.Sharp: - accidentalToSet = AccidentalType.SharpQuarterNoteUp; - break; - case AccidentalType.Flat: - accidentalToSet = AccidentalType.FlatQuarterNoteUp; - break; - } - } else { - // define which accidental should be shown ignoring what might be set on the KS already - switch (accidentalMode) { - case NoteAccidentalMode.ForceSharp: - accidentalToSet = AccidentalType.Sharp; - break; - case NoteAccidentalMode.ForceDoubleSharp: - accidentalToSet = AccidentalType.DoubleSharp; - break; - case NoteAccidentalMode.ForceFlat: - accidentalToSet = AccidentalType.Flat; - break; - case NoteAccidentalMode.ForceDoubleFlat: - accidentalToSet = AccidentalType.DoubleFlat; - break; - default: - // if note has an accidental in the octave, we place a symbol - // according to the Key Signature - if (hasNoteAccidentalWithinOctave) { - accidentalToSet = accidentalForKeySignature; - } else if (hasKeySignatureAccidentalSetForNote) { - // note does not get an accidental, but KS defines one -> Naturalize - accidentalToSet = AccidentalType.Natural; - } - break; - } - - // Issue #472: Tied notes across bars do not show the accidentals but also - // do not register them. - // https://ultimatemusictheory.com/tied-notes-with-accidentals/ - let skipAccidental = false; - if (note && note.isTieDestination && note.beat.index === 0) { - // candidate for skip, check further if start note is on the same line - const previousRenderer = this._barRenderer.previousRenderer as ScoreBarRenderer; - if (previousRenderer) { - const tieOriginLine = previousRenderer.accidentalHelper.getNoteLine(note.tieOrigin!); - if (tieOriginLine === line) { - skipAccidental = true; - } - } - } - - - if (skipAccidental) { - accidentalToSet = AccidentalType.None; - } else { - // do we need an accidental on the note? - if (accidentalToSet !== AccidentalType.None) { - // if we already have an accidental on this line we will reset it if it's the same - if (this._registeredAccidentals.has(line)) { - if (this._registeredAccidentals.get(line) === accidentalToSet) { - accidentalToSet = AccidentalType.None; + + accidentalToSet = AccidentalHelper.computeAccidental( + this._bar.masterBar.keySignature, + accidentalMode, + noteValue, + quarterBend + ); + + line = AccidentalHelper.calculateNoteLine(this._bar, noteValue); + + let skipAccidental = false; + switch (accidentalToSet) { + case AccidentalType.NaturalQuarterNoteUp: + case AccidentalType.SharpQuarterNoteUp: + case AccidentalType.FlatQuarterNoteUp: + // quarter notes are always set and not compared with lines + break; + default: + // Issue #472: Tied notes across bars do not show the accidentals but also + // do not register them. + // https://ultimatemusictheory.com/tied-notes-with-accidentals/ + if (note && note.isTieDestination && note.beat.index === 0) { + // candidate for skip, check further if start note is on the same line + const previousRenderer = this._barRenderer.previousRenderer as ScoreBarRenderer; + if (previousRenderer) { + const tieOriginLine = previousRenderer.accidentalHelper.getNoteLine(note.tieOrigin!); + if (tieOriginLine === line) { + skipAccidental = true; } } - // if there is no accidental on the line, and the key signature has it set already, we clear it on the note - else if (hasKeySignatureAccidentalSetForNote && accidentalToSet === accidentalForKeySignature) { - accidentalToSet = AccidentalType.None; - } + } - // register the new accidental on the line if any. - if (accidentalToSet !== AccidentalType.None) { - this._registeredAccidentals.set(line, accidentalToSet); - } + if (skipAccidental) { + accidentalToSet = AccidentalType.None; } else { - // if we don't want an accidental, but there is already one applied, we place a naturalize accidental - // and clear the registration - if (this._registeredAccidentals.has(line)) { - // if there is already a naturalize symbol on the line, we don't care. - if (this._registeredAccidentals.get(line) === AccidentalType.Natural) { - accidentalToSet = AccidentalType.None; - } else { - accidentalToSet = AccidentalType.Natural; + // do we need an accidental on the note? + if (accidentalToSet !== AccidentalType.None) { + // if we already have an accidental on this line we will reset it if it's the same + if (this._registeredAccidentals.has(line)) { + if (this._registeredAccidentals.get(line) === accidentalToSet) { + accidentalToSet = AccidentalType.None; + } + } + + // register the new accidental on the line if any. + if (accidentalToSet !== AccidentalType.None) { this._registeredAccidentals.set(line, accidentalToSet); } } else { - this._registeredAccidentals.delete(line); + // if we don't want an accidental, but there is already one applied, we place a naturalize accidental + // and clear the registration + if (this._registeredAccidentals.has(line)) { + // if there is already a naturalize symbol on the line, we don't care. + if (this._registeredAccidentals.get(line) === AccidentalType.Natural) { + accidentalToSet = AccidentalType.None; + } else { + accidentalToSet = AccidentalType.Natural; + this._registeredAccidentals.set(line, accidentalToSet); + } + } else { + this._registeredAccidentals.delete(line); + } } } - } + break; } } @@ -337,8 +369,7 @@ export class AccidentalHelper { let lines: BeatLines; if (this._beatLines.has(relatedBeat.id)) { lines = this._beatLines.get(relatedBeat.id)!; - } - else { + } else { lines = new BeatLines(); this._beatLines.set(relatedBeat.id, lines); } @@ -351,18 +382,14 @@ export class AccidentalHelper { } public getMaxLine(b: Beat): number { - return this._beatLines.has(b.id) - ? this._beatLines.get(b.id)!.maxLine - : 0; + return this._beatLines.has(b.id) ? this._beatLines.get(b.id)!.maxLine : 0; } public getMinLine(b: Beat): number { - return this._beatLines.has(b.id) - ? this._beatLines.get(b.id)!.minLine - : 0; + return this._beatLines.has(b.id) ? this._beatLines.get(b.id)!.minLine : 0; } - private static calculateNoteLine(bar: Bar, noteValue: number, mode: NoteAccidentalMode): number { + public static calculateNoteLine(bar: Bar, noteValue: number): number { let value: number = noteValue; let ks: number = bar.masterBar.keySignature; let clef: number = bar.clef as number; @@ -378,13 +405,7 @@ export class AccidentalHelper { ModelUtils.keySignatureIsSharp(ks) || ModelUtils.keySignatureIsNatural(ks) ? AccidentalHelper.SharpNoteSteps : AccidentalHelper.FlatNoteSteps; - // Add offset for note itself - // switch (mode) { - // default: - // // normal behavior: simply use the position where - // // the keysignature defines the position - // break; - // } + steps -= stepList[index]; return steps; diff --git a/src/rendering/utils/BeamingHelper.ts b/src/rendering/utils/BeamingHelper.ts index 9b0157d3b..745b99eaa 100644 --- a/src/rendering/utils/BeamingHelper.ts +++ b/src/rendering/utils/BeamingHelper.ts @@ -121,9 +121,11 @@ export class BeamingHelper { this.beats = []; } - public getBeatLineX(beat: Beat): number { + public getBeatLineX(beat: Beat, direction?: BeamDirection): number { + direction = direction ?? this.direction; + if (this.hasBeatLineX(beat)) { - if (this.direction === BeamDirection.Up) { + if (direction === BeamDirection.Up) { return this._beatLineXPositions.get(beat.index)!.up; } return this._beatLineXPositions.get(beat.index)!.down; diff --git a/test-data/visual-tests/special-tracks/numbered.gp b/test-data/visual-tests/special-tracks/numbered.gp new file mode 100644 index 000000000..4c7473591 Binary files /dev/null and b/test-data/visual-tests/special-tracks/numbered.gp differ diff --git a/test-data/visual-tests/special-tracks/numbered.png b/test-data/visual-tests/special-tracks/numbered.png new file mode 100644 index 000000000..e4d3dfc94 Binary files /dev/null and b/test-data/visual-tests/special-tracks/numbered.png differ diff --git a/test/visualTests/features/SpecialTracks.test.ts b/test/visualTests/features/SpecialTracks.test.ts index e7a25a239..01621a24c 100644 --- a/test/visualTests/features/SpecialTracks.test.ts +++ b/test/visualTests/features/SpecialTracks.test.ts @@ -16,4 +16,8 @@ describe('SpecialTracksTests', () => { it('slash', async () => { await VisualTestHelper.runVisualTest('special-tracks/slash.gp'); }); + + it('numbered', async () => { + await VisualTestHelper.runVisualTest('special-tracks/numbered.gp'); + }); });