diff --git a/packages/alphatab/src/EngravingSettings.ts b/packages/alphatab/src/EngravingSettings.ts index 7ffe0d130..7050388b7 100644 --- a/packages/alphatab/src/EngravingSettings.ts +++ b/packages/alphatab/src/EngravingSettings.ts @@ -454,7 +454,7 @@ export class EngravingSettings { // // custom alphatab sizes this.numberedBarRendererBarSize = this.staffLineThickness * 2; - this.numberedBarRendererBarSpacing = this.beamSpacing + this.numberedBarRendererBarSize; + this.numberedBarRendererBarSpacing = this.beamSpacing; this.preNoteEffectPadding = 0.4 * this.oneStaffSpace; this.postNoteEffectPadding = 0.2 * this.oneStaffSpace; this.lineRangedGlyphDashGap = 0.5 * this.oneStaffSpace; @@ -539,7 +539,7 @@ export class EngravingSettings { public numberedBarRendererBarSpacing = 0; /** - * The size of the dashed drawn in numbered notation to indicate the durations. + * The padding minimum between the duration dashes. */ public numberedDashGlyphPadding = 0; diff --git a/packages/alphatab/src/Environment.ts b/packages/alphatab/src/Environment.ts index b57f472a3..20c5a5371 100644 --- a/packages/alphatab/src/Environment.ts +++ b/packages/alphatab/src/Environment.ts @@ -41,6 +41,7 @@ import { LetRingEffectInfo } from '@coderline/alphatab/rendering/effects/LetRing import { LyricsEffectInfo } from '@coderline/alphatab/rendering/effects/LyricsEffectInfo'; import { MarkerEffectInfo } from '@coderline/alphatab/rendering/effects/MarkerEffectInfo'; import { NoteOrnamentEffectInfo } from '@coderline/alphatab/rendering/effects/NoteOrnamentEffectInfo'; +import { NumberedBarKeySignatureEffectInfo } from '@coderline/alphatab/rendering/effects/NumberedBarKeySignatureEffectInfo'; import { OttaviaEffectInfo } from '@coderline/alphatab/rendering/effects/OttaviaEffectInfo'; import { PalmMuteEffectInfo } from '@coderline/alphatab/rendering/effects/PalmMuteEffectInfo'; import { PickSlideEffectInfo } from '@coderline/alphatab/rendering/effects/PickSlideEffectInfo'; @@ -530,7 +531,9 @@ export class Environment { // // Numbered - new NumberedBarRendererFactory([]), + new NumberedBarRendererFactory([ + { effect: new NumberedBarKeySignatureEffectInfo(), mode: EffectBandMode.OwnedTop, order: 1000 } + ]), // // Tabs @@ -861,5 +864,4 @@ export class Environment { // so we need to declare this specific helper function and implement it in Kotlin ourselves. array.sort((a, b) => b - a); } - } diff --git a/packages/alphatab/src/NotationSettings.ts b/packages/alphatab/src/NotationSettings.ts index b5b605e36..4d8f3f53f 100644 --- a/packages/alphatab/src/NotationSettings.ts +++ b/packages/alphatab/src/NotationSettings.ts @@ -348,6 +348,11 @@ export enum NotationElement { */ EffectWhammyBarLine = 50, + /** + * The key signature for numbered notation staff. + */ + EffectNumberedNotationKeySignature = 51, + } /** diff --git a/packages/alphatab/src/generated/EngravingSettingsJson.ts b/packages/alphatab/src/generated/EngravingSettingsJson.ts index adf10d15c..710f54bde 100644 --- a/packages/alphatab/src/generated/EngravingSettingsJson.ts +++ b/packages/alphatab/src/generated/EngravingSettingsJson.ts @@ -215,7 +215,7 @@ export interface EngravingSettingsJson { */ numberedBarRendererBarSpacing?: number; /** - * The size of the dashed drawn in numbered notation to indicate the durations. + * The padding minimum between the duration dashes. */ numberedDashGlyphPadding?: number; /** diff --git a/packages/alphatab/src/rendering/BarRendererBase.ts b/packages/alphatab/src/rendering/BarRendererBase.ts index 26f01d02d..5726b4756 100644 --- a/packages/alphatab/src/rendering/BarRendererBase.ts +++ b/packages/alphatab/src/rendering/BarRendererBase.ts @@ -5,7 +5,6 @@ import type { Note } from '@coderline/alphatab/model/Note'; import { SimileMark } from '@coderline/alphatab/model/SimileMark'; import { type Voice, VoiceSubElement } from '@coderline/alphatab/model/Voice'; import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; -import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; import { EffectBandContainer } from '@coderline/alphatab/rendering/EffectBandContainer'; import { @@ -28,6 +27,7 @@ import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingH import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; import type { MasterBarBounds } from '@coderline/alphatab/rendering/utils/MasterBarBounds'; +import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; import type { Settings } from '@coderline/alphatab/Settings'; /** @@ -677,13 +677,13 @@ export class BarRendererBase { return this.beatGlyphsStart + this.voiceContainer.getBeatX(beat, requestedPosition, useSharedSizes); } - public getRatioPositionX(ticks: number): number { + public getRatioPositionX(ratio: number): number { const firstOnNoteX = this.bar.isEmpty ? this.beatGlyphsStart : this.getBeatX(this.bar.voices[0].beats[0], BeatXPosition.MiddleNotes); const x = firstOnNoteX; const w = this.postBeatGlyphsStart - firstOnNoteX; - return x + w * ticks; + return x + w * ratio; } public getNoteX(note: Note, requestedPosition: NoteXPosition): number { diff --git a/packages/alphatab/src/rendering/LineBarRenderer.ts b/packages/alphatab/src/rendering/LineBarRenderer.ts index 1c525af66..c9bcb988c 100644 --- a/packages/alphatab/src/rendering/LineBarRenderer.ts +++ b/packages/alphatab/src/rendering/LineBarRenderer.ts @@ -71,7 +71,7 @@ export abstract class LineBarRenderer extends BarRendererBase { protected updateFirstLineY() { const fullLineHeight = this.lineOffset * (this.heightLineCount - 1); - const actualLineHeight = (this.drawnLineCount - 1) * this.lineOffset; + const actualLineHeight = this.drawnLineCount === 0 ? 0 : (this.drawnLineCount - 1) * this.lineOffset; const lineYOffset = this.smuflMetrics.staffLineThickness / 2; this.firstLineY = (((fullLineHeight - actualLineHeight) / 2) | 0) - lineYOffset; @@ -471,7 +471,7 @@ export abstract class LineBarRenderer extends BarRendererBase { beamsElement: BeatSubElement ): void { canvas.color = h.voice!.index === 0 ? this.resources.mainGlyphColor : this.resources.secondaryGlyphColor; - if (!h.isRestBeamHelper) { + if (this.shouldPaintBeamingHelper(h)) { if (this.drawBeamHelperAsFlags(h)) { this.paintFlag(cx, cy, canvas, h, flagsElement); } else { @@ -480,8 +480,13 @@ export abstract class LineBarRenderer extends BarRendererBase { } } + protected shouldPaintBeamingHelper(h: BeamingHelper) { + return !h.isRestBeamHelper; + } + protected abstract getFlagTopY(beat: Beat, direction: BeamDirection): number; protected abstract getFlagBottomY(beat: Beat, direction: BeamDirection): number; + protected shouldPaintFlag(beat: Beat): boolean { // no flags for bend grace beats if (beat.graceType === GraceType.BendGrace) { @@ -654,8 +659,8 @@ export abstract class LineBarRenderer extends BarRendererBase { const direction: BeamDirection = this.getBeamDirection(h); const isGrace: boolean = h.graceType !== GraceType.None; const scaleMod: number = isGrace ? EngravingSettings.GraceScale : 1; - let barSpacing: number = (this.smuflMetrics.beamSpacing + this.smuflMetrics.beamThickness) * scaleMod; - let barSize: number = this.smuflMetrics.beamThickness * scaleMod; + let barSpacing: number = (this.beamSpacing + this.beamThickness) * scaleMod; + let barSize: number = this.beamThickness * scaleMod; if (direction === BeamDirection.Down) { barSpacing = -barSpacing; barSize = -barSize; @@ -833,30 +838,49 @@ export abstract class LineBarRenderer extends BarRendererBase { for (const v of this.helpers.beamHelpers) { for (const h of v) { - if (h.isRestBeamHelper) { - // no stems or beams to consider + if (!this.shouldPaintBeamingHelper(h)) { + // no visible helper } // notes with stems else if (h.beats.length === 1 && h.beats[0].duration >= Duration.Half) { + const tupletDirection = this.getTupletBeamDirection(h); if (h.direction === BeamDirection.Up) { let topY = this.getFlagTopY(h.beats[0], h.direction); - if (h.hasTuplet) { + if (h.hasTuplet && tupletDirection === h.direction) { topY -= this.tupletSize + this.tupletOffset; } if (topY < maxNoteY) { maxNoteY = topY; } + if (h.hasTuplet && tupletDirection !== h.direction) { + let bottomY = this.getFlagBottomY(h.beats[0], tupletDirection); + bottomY += this.tupletSize + this.tupletOffset; + + if (bottomY > minNoteY) { + minNoteY = bottomY; + } + } + // bottom handled via beat container bBox } else { let bottomY = this.getFlagBottomY(h.beats[0], h.direction); - if (h.hasTuplet) { + if (h.hasTuplet && tupletDirection === h.direction) { bottomY += this.tupletSize + this.tupletOffset; } if (bottomY > minNoteY) { minNoteY = bottomY; } + if (h.hasTuplet && tupletDirection !== h.direction) { + let topY = this.getFlagTopY(h.beats[0], tupletDirection); + topY -= this.tupletSize + this.tupletOffset; + + if (topY < maxNoteY) { + maxNoteY = topY; + } + } + // top handled via beat container bBox } } @@ -865,10 +889,11 @@ export abstract class LineBarRenderer extends BarRendererBase { else { this.ensureBeamDrawingInfo(h, h.direction); const drawingInfo = h.drawingInfos.get(h.direction)!; + const tupletDirection = this.getTupletBeamDirection(h); if (h.direction === BeamDirection.Up) { let topY = Math.min(drawingInfo.startY, drawingInfo.endY); - if (h.hasTuplet) { + if (h.hasTuplet && tupletDirection === h.direction) { topY -= this.tupletSize + this.tupletOffset; } @@ -876,15 +901,19 @@ export abstract class LineBarRenderer extends BarRendererBase { maxNoteY = topY; } - const bottomY: number = + let bottomY: number = this.getBarLineStart(h.beatOfLowestNote, h.direction) + noteOverflowPadding; + if (h.hasTuplet && tupletDirection !== h.direction) { + bottomY += this.tupletSize + this.tupletOffset; + } + if (bottomY > minNoteY) { minNoteY = bottomY; } } else { let bottomY = Math.max(drawingInfo.startY, drawingInfo.endY); - if (h.hasTuplet) { + if (h.hasTuplet && tupletDirection === h.direction) { bottomY += this.tupletSize + this.tupletOffset; } @@ -892,8 +921,11 @@ export abstract class LineBarRenderer extends BarRendererBase { minNoteY = bottomY; } - const topY: number = - this.getBarLineStart(h.beatOfHighestNote, h.direction) - noteOverflowPadding; + let topY: number = this.getBarLineStart(h.beatOfHighestNote, h.direction) - noteOverflowPadding; + if (h.hasTuplet && tupletDirection !== h.direction) { + topY -= this.tupletSize + this.tupletOffset; + } + if (topY < maxNoteY) { maxNoteY = topY; } @@ -911,27 +943,12 @@ export abstract class LineBarRenderer extends BarRendererBase { } } - protected ensureBeamDrawingInfo(h: BeamingHelper, direction: BeamDirection): void { - if (h.drawingInfos.has(direction)) { - return; - } - const scale = h.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; - const barCount: number = ModelUtils.getIndex(h.shortestDuration) - 2; - + protected initializeBeamDrawingInfo(h: BeamingHelper, direction: BeamDirection) { const drawingInfo = new BeamingHelperDrawInfo(); - h.drawingInfos.set(direction, drawingInfo); - - // the beaming logic works like this: - // 1. we take the first and last note, add the stem, and put a diagnal line between them. - // 2. the height of the diagonal line must not exceed a max height, - // - if this is the case, the line on the more distant note just gets longer - // 3. any middle elements (notes or rests) shift this diagonal line up/down to avoid overlaps const firstBeat = h.beats[0]; const lastBeat = h.beats[h.beats.length - 1]; - const isRest = h.isRestBeamHelper; - // 1. put direct diagonal line. drawingInfo.startBeat = firstBeat; drawingInfo.startX = this.getBeatX(firstBeat, BeatXPosition.Stem); @@ -980,13 +997,41 @@ export abstract class LineBarRenderer extends BarRendererBase { drawingInfo.startY = drawingInfo.endY + maxSlope; } + return drawingInfo; + } + + protected get beamSpacing() { + return this.smuflMetrics.beamSpacing; + } + protected get beamThickness() { + return this.smuflMetrics.beamThickness; + } + + protected ensureBeamDrawingInfo(h: BeamingHelper, direction: BeamDirection): void { + if (h.drawingInfos.has(direction)) { + return; + } + + // the beaming logic works like this: + // 1. we take the first and last note, add the stem, and put a diagnal line between them. + // 2. the height of the diagonal line must not exceed a max height, + // - if this is the case, the line on the more distant note just gets longer + // 3. any middle elements (notes or rests) shift this diagonal line up/down to avoid overlaps + + const drawingInfo = this.initializeBeamDrawingInfo(h, direction); + h.drawingInfos.set(direction, drawingInfo); + + const isRest = h.isRestBeamHelper; + const scale = h.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; + const barCount: number = ModelUtils.getIndex(h.shortestDuration) - 2; + // 3. adjust beam drawing order // we can only draw up to 2 beams towards the noteheads, then we have to grow to the other side // here we shift accordingly let barDrawingShift = 0; if (barCount > 2 && !isRest) { - const beamSpacing = this.smuflMetrics.beamSpacing * scale; - const beamThickness = this.smuflMetrics.beamThickness * scale; + const beamSpacing = this.beamSpacing * scale; + const beamThickness = this.beamThickness * scale; const totalBarsHeight = barCount * beamThickness + (barCount - 1) * beamSpacing; if (direction === BeamDirection.Up) { @@ -1038,7 +1083,7 @@ export abstract class LineBarRenderer extends BarRendererBase { if (h.restBeats.length > 0) { // space needed for the bars, rests need to be below them const scaleMod: number = h.graceType !== GraceType.None ? EngravingSettings.GraceScale : 1; - barSpacing = barCount * (this.smuflMetrics.beamSpacing + this.smuflMetrics.beamThickness) * scaleMod; + barSpacing = barCount * (this.beamSpacing + this.beamThickness) * scaleMod; } for (const b of h.restBeats) { diff --git a/packages/alphatab/src/rendering/MultiBarRestBeatContainerGlyph.ts b/packages/alphatab/src/rendering/MultiBarRestBeatContainerGlyph.ts index 36c5b95d9..bf7ba056f 100644 --- a/packages/alphatab/src/rendering/MultiBarRestBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/MultiBarRestBeatContainerGlyph.ts @@ -23,6 +23,10 @@ export class MultiBarRestBeatContainerGlyph extends BeatContainerGlyphBase { public override get absoluteDisplayStart(): number { return this.renderer.bar.masterBar.start; } + public override get beatId(): number { + return -1; + } + public override get onTimeX(): number { return 0; } @@ -99,7 +103,6 @@ export class MultiBarRestBeatContainerGlyph extends BeatContainerGlyphBase { case BeatXPosition.OnNotes: case BeatXPosition.MiddleNotes: case BeatXPosition.Stem: - return g.x + g.width / 2; case BeatXPosition.PostNotes: return g.x + g.width; case BeatXPosition.EndBeat: diff --git a/packages/alphatab/src/rendering/NumberedBarRenderer.ts b/packages/alphatab/src/rendering/NumberedBarRenderer.ts index 83c103775..bc241f973 100644 --- a/packages/alphatab/src/rendering/NumberedBarRenderer.ts +++ b/packages/alphatab/src/rendering/NumberedBarRenderer.ts @@ -1,21 +1,26 @@ +import { EngravingSettings } from '@coderline/alphatab/EngravingSettings'; +import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; import { type Bar, BarSubElement } from '@coderline/alphatab/model/Bar'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; import { Duration } from '@coderline/alphatab/model/Duration'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; import { ModelUtils } from '@coderline/alphatab/model/ModelUtils'; import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import type { Note } from '@coderline/alphatab/model/Note'; import type { Voice } from '@coderline/alphatab/model/Voice'; -import { CanvasHelper, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; import type { NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; import { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; -import { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; -import { NumberedBeatContainerGlyph } from '@coderline/alphatab/rendering/NumberedBeatContainerGlyph'; -import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import { BarLineGlyph } from '@coderline/alphatab/rendering/glyphs/BarLineGlyph'; import { BarNumberGlyph } from '@coderline/alphatab/rendering/glyphs/BarNumberGlyph'; -import { NumberedKeySignatureGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedKeySignatureGlyph'; +import { + NumberedDashBeatContainerGlyph, + NumberedNoteBeatContainerGlyphBase +} from '@coderline/alphatab/rendering/glyphs/NumberedDashBeatContainerGlyph'; import { ScoreTimeSignatureGlyph } from '@coderline/alphatab/rendering/glyphs/ScoreTimeSignatureGlyph'; -import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; +import { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; +import { NumberedBeatContainerGlyph } from '@coderline/alphatab/rendering/NumberedBeatContainerGlyph'; +import type { ScoreRenderer } from '@coderline/alphatab/rendering/ScoreRenderer'; import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection'; import type { BeamingHelper } from '@coderline/alphatab/rendering/utils/BeamingHelper'; import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; @@ -31,29 +36,11 @@ export class NumberedBarRenderer extends LineBarRenderer { private _isOnlyNumbered: boolean; public shortestDuration = Duration.QuadrupleWhole; - public lowestOctave: number | null = null; - public highestOctave: number | null = null; - public octaves = new Map(); get dotSpacing(): number { return this.smuflMetrics.glyphHeights.get(MusicFontSymbol.AugmentationDot)! * 2; } - public registerOctave(beat: Beat, octave: number) { - this.octaves.set(beat, octave); - if (this.lowestOctave === null) { - this.lowestOctave = octave; - this.highestOctave = octave; - } else { - if (octave < this.lowestOctave!) { - this.lowestOctave = octave; - } - if (octave > this.highestOctave!) { - this.highestOctave = octave; - } - } - } - public override get repeatsBarSubElement(): BarSubElement { return BarSubElement.NumberedRepeats; } @@ -103,39 +90,8 @@ export class NumberedBarRenderer extends LineBarRenderer { return BeatSubElement.NumberedTuplet; } - public override doLayout(): void { - super.doLayout(); - if (this.voiceContainer.tupletGroups.size > 0) { - this.registerOverflowTop(this.tupletSize); - } - - if (!this.bar.isEmpty) { - const barCount: number = ModelUtils.getIndex(this.shortestDuration) - 2; - const dotSpacing = this.dotSpacing; - if (barCount > 0) { - const barSpacing: number = this.smuflMetrics.numberedBarRendererBarSpacing; - const barSize: number = this.smuflMetrics.numberedBarRendererBarSize; - const barOverflow = (barCount - 1) * barSpacing + barSize; - - let dotOverflow = 0; - const lowestOctave = this.lowestOctave; - if (lowestOctave !== null) { - dotOverflow = - Math.abs(lowestOctave) * dotSpacing + - this.smuflMetrics.glyphHeights.get(MusicFontSymbol.AugmentationDot)!; - } - - this.registerOverflowBottom(barOverflow + dotOverflow); - } - - const highestOctave = this.highestOctave; - if (highestOctave !== null) { - const dotOverflow = - Math.abs(highestOctave) * dotSpacing + - this.smuflMetrics.glyphHeights.get(MusicFontSymbol.AugmentationDot)!; - this.registerOverflowTop(dotOverflow); - } - } + protected override shouldPaintBeamingHelper(_h: BeamingHelper): boolean { + return true; } protected override paintFlag( @@ -155,90 +111,140 @@ export class NumberedBarRenderer extends LineBarRenderer { h: BeamingHelper, flagsElement: BeatSubElement ): void { - if (h.beats.length === 0) { + if (h.beats.length === 0 || h.graceType !== GraceType.None) { return; } - const res = this.resources; for (let i: number = 0, j: number = h.beats.length; i < j; i++) { const beat: Beat = h.beats[i]; using _ = ElementStyleHelper.beat(canvas, flagsElement, beat); - // - // draw line - // - const barSpacing: number = this.smuflMetrics.numberedBarRendererBarSpacing; - const barSize: number = this.smuflMetrics.numberedBarRendererBarSize; - const barCount: number = ModelUtils.getIndex(beat.duration) - 2; - const barStart: number = cy + this.y; + const direction: BeamDirection = this.getBeamDirection(h); + const isGrace: boolean = h.graceType !== GraceType.None; + const scaleMod: number = isGrace ? EngravingSettings.GraceScale : 1; - const beatLineX: number = this.getBeatX(beat, BeatXPosition.PreNotes); + let barSpacing: number = (this.beamSpacing + this.beamThickness) * scaleMod; + let barSize = this.beamThickness * scaleMod; + if (direction === BeamDirection.Down) { + barSpacing = -barSpacing; + barSize = -barSize; + } - const beamY = this.calculateBeamY(h, beatLineX); + let barCount: number = ModelUtils.getIndex(beat.duration) - 2; + let beatLineX: number = this.getBeatX(beat, BeatXPosition.PreNotes); + + let barStartX: number = 0; + let barEndX: number = 0; + if (i === h.beats.length - 1) { + barStartX = beatLineX; + barEndX = this.getBeatX(beat, BeatXPosition.PostNotes); + } else { + barStartX = beatLineX; + barEndX = this.getBeatX(h.beats[i + 1], BeatXPosition.PreNotes); + } + const barStart: number = cy + this.y + this.calculateBeamY(h, beatLineX); for (let barIndex: number = 0; barIndex < barCount; barIndex++) { - let barStartX: number = 0; - let barEndX: number = 0; - let barStartY: number = 0; const barY: number = barStart + barIndex * barSpacing; - if (i === h.beats.length - 1) { - barStartX = beatLineX; - barEndX = this.getBeatX(beat, BeatXPosition.PostNotes); - } else { - barStartX = beatLineX; - barEndX = this.getBeatX(h.beats[i + 1], BeatXPosition.PreNotes); - } - barStartY = barY + beamY; - canvas.fillRect(cx + this.x + barStartX, barStartY, barEndX - barStartX, barSize); + LineBarRenderer.paintSingleBar( + canvas, + cx + this.x + barStartX, + barY, + cx + this.x + barEndX, + barY, + barSize + ); } - let dotCount = this.octaves.has(beat) ? this.octaves.get(beat)! : 0; - const dotSpacing = this.dotSpacing; - let dotsY = 0; - let dotsOffset = 0; - if (dotCount > 0) { - dotsY = barStart + this.getLineY(0) - res.numberedNotationFont.size; - dotsOffset = dotSpacing * -1; - } else if (dotCount < 0) { - dotsY = barStart + beamY + barCount * (barSpacing + barSize); - dotsOffset = dotSpacing; + // dashes for additional numbers + const container = this.voiceContainer.getBeatContainer(beat) as NumberedBeatContainerGlyph | undefined; + if (container && container.hasAdditionalNumbers) { + for (const additionalNumber of container.iterateAdditionalNumbers()) { + barCount = additionalNumber.barCount; + beatLineX = + this.beatGlyphsStart + additionalNumber.x + additionalNumber.getBeatX(BeatXPosition.PreNotes, false); + for (let barIndex = 0; barIndex < barCount; barIndex++) { + const barY: number = barStart + barIndex * barSpacing; + const additionalBarEndX = + this.beatGlyphsStart + + additionalNumber.x + + additionalNumber.getBeatX(BeatXPosition.PostNotes, false); + LineBarRenderer.paintSingleBar( + canvas, + cx + this.x + beatLineX, + barY, + cx + this.x + additionalBarEndX, + barY, + barSize + ); + } + } } - const dotX: number = this.getBeatX(beat, BeatXPosition.MiddleNotes); - - dotCount = Math.abs(dotCount); + } + } - for (let d = 0; d < dotCount; d++) { - CanvasHelper.fillMusicFontSymbolSafe( - canvas, - cx + this.x + dotX, - dotsY, - 1, - MusicFontSymbol.AugmentationDot, - true - ); - dotsY += dotsOffset; - } + protected override calculateOverflows(rendererTop: number, rendererBottom: number): void { + super.calculateOverflows(rendererTop, rendererBottom); + if (this.bar.isEmpty) { + return; } + this.calculateBeamingOverflows(rendererTop, rendererBottom); } public getNoteLine(_note: Note) { return 0; } - public override get tupletOffset(): number { - // Shift tuplet above the number by: - // * 1 to get back to the center (calculateBeamYWithDirection places the beam below the number) - // * 1.5 to get back to the top of the number - return super.tupletOffset + this.resources.numberedNotationFont.size * 1.5; + private _calculateBarHeight(beat: Beat) { + const barCount: number = ModelUtils.getIndex(beat.duration) - 2; + let barHeight = 0; + if (barCount > 0) { + const smufl = this.smuflMetrics; + barHeight = + smufl.numberedBarRendererBarSpacing + + barCount * (smufl.numberedBarRendererBarSpacing + smufl.numberedBarRendererBarSize); + } + + return barHeight; } - protected override getFlagTopY(_beat: Beat, _direction: BeamDirection): number { - return this.getLineY(0) - this.resources.numberedNotationFont.size; + protected override getFlagTopY(beat: Beat, direction: BeamDirection): number { + const barHeight: number = this._calculateBarHeight(beat); + const container = this.voiceContainer.getBeatContainer(beat); + if (!container) { + if (direction === BeamDirection.Up) { + return this.voiceContainer.getBoundingBoxTop() - barHeight; + } + return this.voiceContainer.getBoundingBoxBottom(); + } + + if (direction === BeamDirection.Up) { + return container.getBoundingBoxTop() - barHeight; + } + return container.getBoundingBoxBottom(); } - protected override getFlagBottomY(_beat: Beat, _direction: BeamDirection): number { - return this.getLineY(0) - this.resources.numberedNotationFont.size; + protected override getFlagBottomY(beat: Beat, direction: BeamDirection): number { + const barHeight: number = this._calculateBarHeight(beat); + + const container = this.voiceContainer.getBeatContainer(beat); + if (!container) { + if (direction === BeamDirection.Down) { + return this.voiceContainer.getBoundingBoxBottom() + barHeight; + } + return this.getLineY(0); + } + + if (direction === BeamDirection.Down) { + return container.getBoundingBoxBottom() + barHeight; + } + return this.getLineY(0); + } + + public override completeBeamingHelper(helper: BeamingHelper): void { + super.completeBeamingHelper(helper); + helper.direction = BeamDirection.Down; } protected override getBeamDirection(_helper: BeamingHelper): BeamDirection { @@ -257,14 +263,28 @@ export class NumberedBarRenderer extends LineBarRenderer { return y; } - protected override calculateBeamYWithDirection(_h: BeamingHelper, _x: number, _direction: BeamDirection): number { - const res = this.resources.numberedNotationFont; - return this.getLineY(0) + res.size; + protected override calculateBeamYWithDirection(h: BeamingHelper, _x: number, direction: BeamDirection): number { + if (h.beats.length === 0) { + return this.getLineY(0); + } + + this.ensureBeamDrawingInfo(h, direction); + const info = h.drawingInfos.get(direction)!; + if (direction === BeamDirection.Up) { + return Math.min(info.startY, info.endY); + } else { + return Math.max(info.startY, info.endY); + } } - protected override getBarLineStart(_beat: Beat, _direction: BeamDirection): number { - const noteHeadHeight = this.smuflMetrics.glyphHeights.get(MusicFontSymbol.NoteheadBlack)!; - return this.getLineY(0) - noteHeadHeight / 2; + protected override getBarLineStart(beat: Beat, _direction: BeamDirection): number { + // NOTE: this is only for the overflow calculation, this renderer has a custom bar drawing logic + const container = this.voiceContainer.getBeatContainer(beat); + if (!container) { + return this.voiceContainer.getBoundingBoxTop(); + } + + return container.getBoundingBoxTop(); } protected override createPreBeatGlyphs(): void { @@ -273,16 +293,11 @@ export class NumberedBarRenderer extends LineBarRenderer { this.addPreBeatGlyph(new BarLineGlyph(false, this.bar.staff.track.score.stylesheet.extendBarLines)); } this.createLinePreBeatGlyphs(); + this.createStartSpacing(); this.addPreBeatGlyph(new BarNumberGlyph(0, this.getLineHeight(-0.5), this.bar.index + 1)); } protected override createLinePreBeatGlyphs(): void { - // Key signature - if (!this.bar.previousBar || this.bar.keySignature !== this.bar.previousBar.keySignature) { - this.createStartSpacing(); - this._createKeySignatureGlyphs(); - } - if ( this._isOnlyNumbered && (!this.bar.previousBar || @@ -300,15 +315,8 @@ export class NumberedBarRenderer extends LineBarRenderer { this._createTimeSignatureGlyphs(); } } - private _createKeySignatureGlyphs() { - this.addPreBeatGlyph( - new NumberedKeySignatureGlyph(0, this.getLineY(0), this.bar.keySignature, this.bar.keySignatureType) - ); - } private _createTimeSignatureGlyphs(): void { - this.addPreBeatGlyph(new SpacingGlyph(0, 0, this.smuflMetrics.oneStaffSpace)); - const masterBar = this.bar.masterBar; const g = new ScoreTimeSignatureGlyph( 0, @@ -336,8 +344,39 @@ export class NumberedBarRenderer extends LineBarRenderer { } super.createVoiceGlyphs(v); + + const absoluteStart = this.bar.masterBar.start; for (const b of v.beats) { - this.addBeatGlyph(new NumberedBeatContainerGlyph(b)); + const mainContainer = new NumberedBeatContainerGlyph(b); + this.addBeatGlyph(mainContainer); + + // create dashes and filler glyphs + // we want a glyph on every quarter tick + + if (b.duration < Duration.Quarter) { + const endTick = b.displayStart + b.displayDuration; + let dashTick = b.displayStart + MidiUtils.QuarterTime; + while (dashTick < endTick) { + const isFullTick = endTick - dashTick >= MidiUtils.QuarterTime; + if (isFullTick) { + const dash = new NumberedDashBeatContainerGlyph(v.index, absoluteStart + dashTick); + this.addBeatGlyph(dash); + mainContainer.addDash(dash); + } + // special case to create second note number, this logic doesn't play well with tuplets + else if (b.duration === Duration.Half && b.dots > 1) { + const remainingTickNumber = new NumberedNoteBeatContainerGlyphBase( + b, + absoluteStart + dashTick, + endTick - dashTick + ); + this.addBeatGlyph(remainingTickNumber); + mainContainer.addNotes(remainingTickNumber); + } + + dashTick += MidiUtils.QuarterTime; + } + } } } @@ -350,6 +389,14 @@ export class NumberedBarRenderer extends LineBarRenderer { _canvas: ICanvas ): void {} + protected override get beamSpacing(): number { + return this.smuflMetrics.numberedBarRendererBarSpacing; + } + + protected override get beamThickness(): number { + return this.smuflMetrics.numberedBarRendererBarSize; + } + protected override paintBeamHelper( cx: number, cy: number, diff --git a/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts b/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts index 0566a47e6..3237ddaf4 100644 --- a/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/NumberedBeatContainerGlyph.ts @@ -1,8 +1,14 @@ import type { Beat } from '@coderline/alphatab/model/Beat'; import type { Note } from '@coderline/alphatab/model/Note'; import { NumberedTieGlyph } from '@coderline/alphatab/rendering//glyphs/NumberedTieGlyph'; +import type { BarBounds } from '@coderline/alphatab/rendering/_barrel'; import { BeatContainerGlyph } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; import { NumberedBeatGlyph, NumberedBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedBeatGlyph'; +import { + type INumberedBeatDashGlyph, + type NumberedDashBeatContainerGlyph, + NumberedNoteBeatContainerGlyphBase +} from '@coderline/alphatab/rendering/glyphs/NumberedDashBeatContainerGlyph'; import { NumberedSlurGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedSlurGlyph'; import type { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; @@ -12,6 +18,20 @@ import type { TieGlyph } from '@coderline/alphatab/rendering/glyphs/TieGlyph'; export class NumberedBeatContainerGlyph extends BeatContainerGlyph { private _slurs: Map = new Map(); private _effectSlurs: NumberedSlurGlyph[] = []; + private _dashes?: INumberedBeatDashGlyph[]; + + public hasAdditionalNumbers = false; + public *iterateAdditionalNumbers() { + const dashes = this._dashes; + if (!dashes) { + return; + } + for (const d of dashes) { + if (d instanceof NumberedNoteBeatContainerGlyphBase) { + yield d as NumberedNoteBeatContainerGlyphBase; + } + } + } public constructor(beat: Beat) { super(beat); @@ -19,12 +39,46 @@ export class NumberedBeatContainerGlyph extends BeatContainerGlyph { this.onNotes = new NumberedBeatGlyph(); } + public addDash(dash: NumberedDashBeatContainerGlyph) { + let dashes = this._dashes; + if (!dashes) { + dashes = []; + this._dashes = dashes; + } + dashes.push(dash); + } + + public addNotes(dash: NumberedNoteBeatContainerGlyphBase) { + let dashes = this._dashes; + if (!dashes) { + dashes = []; + this._dashes = dashes; + } + dashes.push(dash); + this.hasAdditionalNumbers = true; + } + public override doLayout(): void { this._slurs.clear(); this._effectSlurs = []; super.doLayout(); } + public override buildBoundingsLookup(barBounds: BarBounds, cx: number, cy: number) { + super.buildBoundingsLookup(barBounds, cx, cy); + // extend bounds to include dashes + const dashes = this._dashes; + if (dashes) { + const beatBounds = barBounds.beats[barBounds.beats.length - 1]; + const lastDash = dashes[dashes.length - 1]; + const visualEndX = lastDash.x + lastDash.contentWidth; + beatBounds.visualBounds.w = visualEndX - beatBounds.visualBounds.x; + + const realEnd = lastDash.x + lastDash.width; + beatBounds.realBounds.w = realEnd - beatBounds.realBounds.x; + } + } + protected override createTies(n: Note): void { // create a tie if any effect requires it if (!n.isVisible) { diff --git a/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts b/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts index c982f1c9c..1c1a05faa 100644 --- a/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/ScoreBeatContainerGlyph.ts @@ -197,6 +197,5 @@ export class ScoreBeatContainerGlyph extends BeatContainerGlyph { protected override updateWidth(): void { super.updateWidth(); this.width += this._flagStretch; - this.minWidth += this._flagStretch; } } diff --git a/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts b/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts index 19168513e..2cecac7c8 100644 --- a/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/SlashBeatContainerGlyph.ts @@ -69,6 +69,5 @@ export class SlashBeatContainerGlyph extends BeatContainerGlyph { protected override updateWidth(): void { super.updateWidth(); this.width += this._flagStretch; - this.minWidth += this._flagStretch; } } diff --git a/packages/alphatab/src/rendering/effects/NumberedBarKeySignatureEffectInfo.ts b/packages/alphatab/src/rendering/effects/NumberedBarKeySignatureEffectInfo.ts new file mode 100644 index 000000000..31a60c593 --- /dev/null +++ b/packages/alphatab/src/rendering/effects/NumberedBarKeySignatureEffectInfo.ts @@ -0,0 +1,46 @@ +import type { Beat } from '@coderline/alphatab/model/Beat'; +import { NotationElement } from '@coderline/alphatab/NotationSettings'; +import type { BarRendererBase } from '@coderline/alphatab/rendering/BarRendererBase'; +import { EffectBarGlyphSizing } from '@coderline/alphatab/rendering/EffectBarGlyphSizing'; +import { EffectInfo } from '@coderline/alphatab/rendering/EffectInfo'; +import type { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; +import { NumberedKeySignatureGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedKeySignatureGlyph'; +import type { Settings } from '@coderline/alphatab/Settings'; + +/** + * @internal + */ +export class NumberedBarKeySignatureEffectInfo extends EffectInfo { + public get notationElement(): NotationElement { + return NotationElement.EffectNumberedNotationKeySignature; + } + + public get hideOnMultiTrack(): boolean { + return false; + } + + public get canShareBand(): boolean { + return false; + } + + public get sizingMode(): EffectBarGlyphSizing { + return EffectBarGlyphSizing.FullBar; + } + + public shouldCreateGlyph(_settings: Settings, beat: Beat): boolean { + const bar = beat.voice.bar; + return ( + beat.index === 0 && + beat.voice.index === 0 && + (!bar.previousBar || bar.keySignature !== bar.previousBar.keySignature) + ); + } + + public createNewGlyph(renderer: BarRendererBase, _beat: Beat): EffectGlyph { + return new NumberedKeySignatureGlyph(0, 0, renderer.bar.keySignature, renderer.bar.keySignatureType); + } + + public canExpand(_from: Beat, _to: Beat): boolean { + return false; + } +} diff --git a/packages/alphatab/src/rendering/glyphs/BarNumberGlyph.ts b/packages/alphatab/src/rendering/glyphs/BarNumberGlyph.ts index 500599a0c..283b79da6 100644 --- a/packages/alphatab/src/rendering/glyphs/BarNumberGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/BarNumberGlyph.ts @@ -1,4 +1,4 @@ -import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import { TextBaseline, type ICanvas } from '@coderline/alphatab/platform/ICanvas'; import type { RenderingResources } from '@coderline/alphatab/RenderingResources'; import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer'; @@ -16,6 +16,13 @@ export class BarNumberGlyph extends Glyph { } public override doLayout(): void { + // TODO: activate this and update paddings accordingly. + // if (!this.renderer.staff!.isFirstInSystem) { + // this.width = 0; + // this.height = 0; + // return; + // } + this.renderer.scoreRenderer.canvas!.font = this.renderer.resources.barNumberFont; const size = this.renderer.scoreRenderer.canvas!.measureText(this._number); this.width = size.width; @@ -38,6 +45,7 @@ export class BarNumberGlyph extends Glyph { const res: RenderingResources = this.renderer.resources; const baseline = canvas.textBaseline; canvas.font = res.barNumberFont; + canvas.textBaseline = TextBaseline.Top; canvas.fillText(this._number, cx + this.x, cy + this.y); canvas.textBaseline = baseline; } diff --git a/packages/alphatab/src/rendering/glyphs/BeatContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/BeatContainerGlyph.ts index f505ff11e..660648891 100644 --- a/packages/alphatab/src/rendering/glyphs/BeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/BeatContainerGlyph.ts @@ -21,6 +21,7 @@ import { Bounds } from '@coderline/alphatab/rendering/utils/Bounds'; * @internal */ export abstract class BeatContainerGlyphBase extends Glyph { + public abstract get beatId(): number; public abstract get absoluteDisplayStart(): number; public abstract get displayDuration(): number; public abstract get onTimeX(): number; @@ -49,10 +50,14 @@ export abstract class BeatContainerGlyphBase extends Glyph { */ export class BeatContainerGlyph extends BeatContainerGlyphBase { private _ties: ITieGlyph[] = []; + private _tieWidth = 0; public beat: Beat; public preNotes!: BeatGlyphBase; public onNotes!: BeatOnNoteGlyphBase; - public minWidth: number = 0; + + public override get beatId(): number { + return this.beat.id; + } public override get isLastOfVoice(): boolean { return this.beat.isLastOfVoice; @@ -132,7 +137,7 @@ export class BeatContainerGlyph extends BeatContainerGlyphBase { } protected get postBeatStretch() { - return this.onNotes.computedWidth - this.onNotes.onTimeX; + return (this.onNotes.computedWidth + this._tieWidth) - this.onNotes.onTimeX; } public registerLayoutingInfo(layoutings: BarLayoutingInfo): void { @@ -169,11 +174,15 @@ export class BeatContainerGlyph extends BeatContainerGlyphBase { this.onNotes.renderer = this.renderer; this.onNotes.container = this; this.onNotes.doLayout(); + this.createBeatTies(); + this.updateWidth(); + } + + protected createBeatTies() { let i: number = this.beat.notes.length - 1; while (i >= 0) { this.createTies(this.beat.notes[i--]); } - this.updateWidth(); } public override doMultiVoiceLayout(): void { @@ -181,7 +190,7 @@ export class BeatContainerGlyph extends BeatContainerGlyphBase { } protected updateWidth(): void { - this.minWidth = this.preNotes.width + this.onNotes.width; + let width = this.preNotes.width + this.onNotes.width; let tieWidth: number = 0; for (const tie of this._ties) { const tg = tie as unknown as Glyph; @@ -189,8 +198,9 @@ export class BeatContainerGlyph extends BeatContainerGlyphBase { tieWidth = tg.width; } } - this.minWidth += tieWidth; - this.width = this.minWidth; + this._tieWidth = tieWidth; + width += tieWidth; + this.width = width; } protected createTies(_n: Note): void { diff --git a/packages/alphatab/src/rendering/glyphs/DeadSlappedBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/DeadSlappedBeatGlyph.ts index bd1787f96..1f856c26e 100644 --- a/packages/alphatab/src/rendering/glyphs/DeadSlappedBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/DeadSlappedBeatGlyph.ts @@ -7,31 +7,47 @@ import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRende * @internal */ export class DeadSlappedBeatGlyph extends Glyph { + private _topY = 0; public constructor() { super(0, 0); } + public override getBoundingBoxTop(): number { + return this._topY; + } + + public override getBoundingBoxBottom(): number { + return this._topY + this.height; + } + public override doLayout(): void { this.width = this.renderer.smuflMetrics.glyphWidths.get(MusicFontSymbol.NoteheadSlashWhiteHalf)!; - } - public override paint(cx: number, cy: number, canvas: ICanvas): void { const renderer = this.renderer as LineBarRenderer; const crossHeight = renderer.getLineHeight(renderer.heightLineCount - 1); const staffTop = renderer.getLineY(0); - const staffHeight = renderer.getLineHeight(renderer.drawnLineCount - 1); + const staffHeight = renderer.drawnLineCount > 0 ? renderer.getLineHeight(renderer.drawnLineCount - 1) : 0; + + const topY = staffTop + staffHeight / 2 - crossHeight / 2; + + this.height = crossHeight; + + this._topY = topY; + } + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + const crossHeight = this.height; - // center X on staff - const centerY = staffTop + staffHeight / 2 - crossHeight / 2; + const topY = this._topY; const lw = canvas.lineWidth; canvas.lineWidth = this.renderer.smuflMetrics.deadSlappedLineWidth; - canvas.moveTo(cx + this.x, cy + centerY); - canvas.lineTo(cx + this.x + this.width, cy + centerY + crossHeight); + canvas.moveTo(cx + this.x, cy + topY); + canvas.lineTo(cx + this.x + this.width, cy + topY + crossHeight); - canvas.moveTo(cx + this.x, cy + centerY + crossHeight); - canvas.lineTo(cx + this.x + this.width, cy + centerY); + canvas.moveTo(cx + this.x, cy + topY + crossHeight); + canvas.lineTo(cx + this.x + this.width, cy + topY); canvas.stroke(); diff --git a/packages/alphatab/src/rendering/glyphs/MultiVoiceContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/MultiVoiceContainerGlyph.ts index 0640eb327..f003944c1 100644 --- a/packages/alphatab/src/rendering/glyphs/MultiVoiceContainerGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/MultiVoiceContainerGlyph.ts @@ -24,6 +24,8 @@ export class MultiVoiceContainerGlyph extends Glyph { public voiceDrawOrder?: number[]; + private readonly _beatGlyphLookup = new Map(); + public beatGlyphs = new Map(); public tupletGroups = new Map(); @@ -58,7 +60,7 @@ export class MultiVoiceContainerGlyph extends Glyph { private _scaleToForce(force: number): void { this.width = this.renderer.layoutingInfo.calculateVoiceWidth(force); - const positions: Map = this.renderer.layoutingInfo.buildOnTimePositions(force); + const positions = this.renderer.layoutingInfo.buildOnTimePositions(force); for (const beatGlyphs of this.beatGlyphs.values()) { for (let i: number = 0, j: number = beatGlyphs.length; i < j; i++) { const currentBeatGlyph = beatGlyphs[i]; @@ -170,6 +172,12 @@ export class MultiVoiceContainerGlyph extends Glyph { beatGlyphs.length === 0 ? 0 : beatGlyphs[beatGlyphs.length - 1].x + beatGlyphs[beatGlyphs.length - 1].width; bg.renderer = this.renderer; beatGlyphs.push(bg); + + const id = bg.beatId; + if (id >= 0) { + this._beatGlyphLookup.set(id, bg); + } + const newWidth = bg.x + bg.width; if (newWidth > this.width) { this.width = newWidth; @@ -186,6 +194,7 @@ export class MultiVoiceContainerGlyph extends Glyph { tupletGroups.push(bg.tupletGroup!); } } + public getBeatX( beat: Beat, requestedPosition: BeatXPosition = BeatXPosition.PreNotes, @@ -222,11 +231,10 @@ export class MultiVoiceContainerGlyph extends Glyph { } public getBeatContainer(beat: Beat): BeatContainerGlyphBase | undefined { - if (!this.beatGlyphs.has(beat.voice.index)) { + if (!this._beatGlyphLookup.has(beat.id)) { return undefined; } - const beats = this.beatGlyphs.get(beat.voice.index)!; - return beat.index < beats.length ? beats[beat.index] : undefined; + return this._beatGlyphLookup.get(beat.id); } public buildBoundingsLookup(barBounds: BarBounds, cx: number, cy: number): void { diff --git a/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts index 9c4793b02..93c124aab 100644 --- a/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NumberedBeatGlyph.ts @@ -15,7 +15,6 @@ import { BeatGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatGlyphBas import { BeatOnNoteGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatOnNoteGlyphBase'; import { DeadSlappedBeatGlyph } from '@coderline/alphatab/rendering/glyphs/DeadSlappedBeatGlyph'; import type { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { NumberedDashGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedDashGlyph'; import { NumberedNoteHeadGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedNoteHeadGlyph'; import { SpacingGlyph } from '@coderline/alphatab/rendering/glyphs/SpacingGlyph'; import type { NumberedBarRenderer } from '@coderline/alphatab/rendering/NumberedBarRenderer'; @@ -32,11 +31,16 @@ export class NumberedBeatPreNotesGlyph extends BeatGlyphBase { public isNaturalizeAccidental = false; public accidental: AccidentalType = AccidentalType.None; + public skipLayout = false; + protected override get effectElement() { return BeatSubElement.NumberedEffects; } public override doLayout(): void { + if (this.skipLayout) { + return; + } if (!this.container.beat.isRest && !this.container.beat.isEmpty) { const accidentals: AccidentalGroupGlyph = new AccidentalGroupGlyph(); accidentals.renderer = this.renderer; @@ -107,12 +111,9 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { public noteHeads: NumberedNoteHeadGlyph | null = null; public deadSlapped: DeadSlappedBeatGlyph | null = null; - public octaveDots: number = 0; - protected override get effectElement() { return BeatSubElement.NumberedEffects; } - public override getNoteX(_note: Note, requestedPosition: NoteXPosition): number { let g: Glyph | null = null; if (this.noteHeads) { @@ -226,6 +227,8 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { sr.shortestDuration = this.container.beat.duration; } + let octaveDots = 0; + if (!this.container.beat.isEmpty) { const glyphY = sr.getLineY(0); let numberWithinOctave = '0'; @@ -248,13 +251,10 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { const index = noteValue < 0 ? ((noteValue % 12) + 12) % 12 : noteValue % 12; - let dots = noteValue < 0 ? ((Math.abs(noteValue) + 12) / 12) | 0 : (noteValue / 12) | 0; + octaveDots = noteValue < 0 ? ((Math.abs(noteValue) + 12) / 12) | 0 : (noteValue / 12) | 0; if (noteValue < 0) { - dots *= -1; + octaveDots *= -1; } - this.octaveDots = dots; - sr.registerOctave(this.container.beat, dots); - const stepList = ModelUtils.keySignatureIsSharp(ks) || ModelUtils.keySignatureIsNatural(ks) ? AccidentalHelper.flatNoteSteps @@ -291,7 +291,8 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { glyphY, numberWithinOctave, isGrace, - this.container.beat + this.container.beat, + octaveDots ); this.noteHeads = noteHeadGlyph; @@ -307,35 +308,6 @@ export class NumberedBeatGlyph extends BeatOnNoteGlyphBase { this.addEffect(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, glyphY, this.container.beat); - dash.renderer = this.renderer; - this.addNormal(dash); - } } super.doLayout(); diff --git a/packages/alphatab/src/rendering/glyphs/NumberedDashBeatContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedDashBeatContainerGlyph.ts new file mode 100644 index 000000000..444becb5d --- /dev/null +++ b/packages/alphatab/src/rendering/glyphs/NumberedDashBeatContainerGlyph.ts @@ -0,0 +1,182 @@ +import { MidiUtils } from '@coderline/alphatab/midi/MidiUtils'; +import type { Beat } from '@coderline/alphatab/model/Beat'; +import { Duration } from '@coderline/alphatab/model/Duration'; +import type { GraceGroup } from '@coderline/alphatab/model/GraceGroup'; +import { GraceType } from '@coderline/alphatab/model/GraceType'; +import type { Note } from '@coderline/alphatab/model/Note'; +import type { TupletGroup } from '@coderline/alphatab/model/TupletGroup'; +import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; +import type { BarBounds } from '@coderline/alphatab/rendering/_barrel'; +import type { NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase'; +import type { BeatXPosition } from '@coderline/alphatab/rendering/BeatXPosition'; +import { BeatContainerGlyphBase } from '@coderline/alphatab/rendering/glyphs/BeatContainerGlyph'; +import type { NumberedBeatPreNotesGlyph } from '@coderline/alphatab/rendering/glyphs/NumberedBeatGlyph'; +import type { NumberedBarRenderer } from '@coderline/alphatab/rendering/NumberedBarRenderer'; +import { NumberedBeatContainerGlyph } from '@coderline/alphatab/rendering/NumberedBeatContainerGlyph'; +import type { BarLayoutingInfo } from '@coderline/alphatab/rendering/staves/BarLayoutingInfo'; + +/** + * @internal + */ +export interface INumberedBeatDashGlyph { + readonly contentWidth: number; + readonly x: number; + readonly width: number; +} + +/** + * @internal + */ +export class NumberedNoteBeatContainerGlyphBase extends NumberedBeatContainerGlyph implements INumberedBeatDashGlyph { + private _absoluteDisplayStart: number; + private _displayDuration: number; + public constructor(beat: Beat, absoluteDisplayStart: number, displayDuration: number) { + super(beat); + this._absoluteDisplayStart = absoluteDisplayStart; + this._displayDuration = displayDuration; + (this.preNotes as NumberedBeatPreNotesGlyph).skipLayout = true; + + this.barCount = NumberedNoteBeatContainerGlyphBase._ticksToBarCount(displayDuration); + } + + private static _ticksToBarCount(displayDuration: number): number { + // we know that displayDuration < MidiUtils.QuarterTime, otherwise this glyph is not created + if (displayDuration >= MidiUtils.toTicks(Duration.Eighth)) { + return 1; + } else if (displayDuration >= MidiUtils.toTicks(Duration.Sixteenth)) { + return 2; + } else if (displayDuration >= MidiUtils.toTicks(Duration.ThirtySecond)) { + return 3; + } else if (displayDuration >= MidiUtils.toTicks(Duration.SixtyFourth)) { + return 4; + } else if (displayDuration >= MidiUtils.toTicks(Duration.OneHundredTwentyEighth)) { + return 5; + } else if (displayDuration >= MidiUtils.toTicks(Duration.TwoHundredFiftySixth)) { + return 6; + } + return 0; + } + + public readonly barCount: number; + + public override get beatId(): number { + return -1; + } + + public get contentWidth() { + return this.onNotes.width; + } + + public override get absoluteDisplayStart(): number { + return this._absoluteDisplayStart; + } + + public override get displayDuration(): number { + return this._displayDuration; + } + + public override get graceType(): GraceType { + return GraceType.None; + } + public override get graceIndex(): number { + return 0; + } + public override get graceGroup(): GraceGroup | null { + return null; + } + + public override get isFirstOfTupletGroup(): boolean { + return false; + } + public override get tupletGroup(): TupletGroup | null { + return null; + } + public override get isLastOfVoice(): boolean { + return false; + } + + public override buildBoundingsLookup(_barBounds: BarBounds, _cx: number, _cy: number): void {} +} + +/** + * @internal + */ +export class NumberedDashBeatContainerGlyph extends BeatContainerGlyphBase implements INumberedBeatDashGlyph { + private _absoluteDisplayStart: number; + private _voiceIndex: number; + public constructor(voiceIndex: number, absoluteDisplayStart: number) { + super(0, 0); + this._absoluteDisplayStart = absoluteDisplayStart; + this._voiceIndex = voiceIndex; + } + + public override get beatId(): number { + return -1; + } + + public get contentWidth() { + return this.renderer.smuflMetrics.numberedDashGlyphWidth; + } + + public override get absoluteDisplayStart(): number { + return this._absoluteDisplayStart; + } + public override get displayDuration(): number { + return MidiUtils.QuarterTime; + } + + public override get onTimeX(): number { + return this.renderer.smuflMetrics.numberedDashGlyphWidth / 2; + } + + public override get graceType(): GraceType { + return GraceType.None; + } + public override get graceIndex(): number { + return 0; + } + public override get graceGroup(): GraceGroup | null { + return null; + } + public override get voiceIndex(): number { + return this._voiceIndex; + } + + public override get isFirstOfTupletGroup(): boolean { + return false; + } + public override get tupletGroup(): TupletGroup | null { + return null; + } + public override get isLastOfVoice(): boolean { + return false; + } + + public override getNoteY(_note: Note, _requestedPosition: NoteYPosition): number { + return 0; + } + public override doMultiVoiceLayout(): void {} + public override getRestY(_requestedPosition: NoteYPosition): number { + return 0; + } + public override getNoteX(_note: Note, _requestedPosition: NoteXPosition): number { + return 0; + } + public override getBeatX(_requestedPosition: BeatXPosition, _useSharedSizes: boolean): number { + return 0; + } + public override registerLayoutingInfo(layoutings: BarLayoutingInfo): void { + const width = this.renderer.smuflMetrics.numberedDashGlyphWidth; + layoutings.addBeatSpring(this, width / 2, width / 2); + } + public override applyLayoutingInfo(_info: BarLayoutingInfo): void {} + public override buildBoundingsLookup(_barBounds: BarBounds, _cx: number, _cy: number): void {} + + public override paint(cx: number, cy: number, canvas: ICanvas): void { + const renderer = this.renderer as NumberedBarRenderer; + const dashWidth = renderer.smuflMetrics.numberedDashGlyphWidth; + const dashHeight = renderer.smuflMetrics.numberedBarRendererBarSize; + const dashY = Math.ceil(cy + renderer.getLineY(0) - dashHeight); + canvas.fillRect(cx + this.x, dashY, dashWidth, dashHeight); + } +} diff --git a/packages/alphatab/src/rendering/glyphs/NumberedDashGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedDashGlyph.ts deleted file mode 100644 index 8de543555..000000000 --- a/packages/alphatab/src/rendering/glyphs/NumberedDashGlyph.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; -import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; -import type { ICanvas } from '@coderline/alphatab/platform/ICanvas'; - -/** - * @internal - */ -export class NumberedDashGlyph extends Glyph { - private _beat: Beat; - - public constructor(x: number, y: number, beat: Beat) { - super(x, y); - this._beat = beat; - } - - public override doLayout(): void { - this.width = - this.renderer.smuflMetrics.numberedDashGlyphWidth + this.renderer.smuflMetrics.numberedDashGlyphPadding; - this.height = this.renderer.smuflMetrics.numberedBarRendererBarSize; - } - - public override paint(cx: number, cy: number, canvas: ICanvas): void { - using _ = ElementStyleHelper.beat(canvas, BeatSubElement.NumberedDuration, this._beat); - const padding = this.renderer.smuflMetrics.numberedDashGlyphPadding; - canvas.fillRect(cx + this.x, Math.ceil(cy + this.y - this.height), this.width - padding, this.height); - } -} diff --git a/packages/alphatab/src/rendering/glyphs/NumberedKeySignatureGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedKeySignatureGlyph.ts index 298670881..92bb162be 100644 --- a/packages/alphatab/src/rendering/glyphs/NumberedKeySignatureGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NumberedKeySignatureGlyph.ts @@ -1,22 +1,23 @@ -import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { AccidentalGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGlyph'; -import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; import { AccidentalType } from '@coderline/alphatab/model/AccidentalType'; -import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; +import { BarSubElement } from '@coderline/alphatab/model/Bar'; import { KeySignature } from '@coderline/alphatab/model/KeySignature'; +import { KeySignatureType } from '@coderline/alphatab/model/KeySignatureType'; import { CanvasHelper, type ICanvas, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; -import { BarSubElement } from '@coderline/alphatab/model/Bar'; +import { AccidentalGlyph } from '@coderline/alphatab/rendering/glyphs/AccidentalGlyph'; +import { EffectGlyph } from '@coderline/alphatab/rendering/glyphs/EffectGlyph'; +import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; /** * @internal */ -export class NumberedKeySignatureGlyph extends Glyph { +export class NumberedKeySignatureGlyph extends EffectGlyph { private _keySignature: KeySignature; private _keySignatureType: KeySignatureType; private _text: string = ''; private _accidental: AccidentalType = AccidentalType.None; private _accidentalOffset: number = 0; + private _padding: number = 0; public constructor(x: number, y: number, keySignature: KeySignature, keySignatureType: KeySignatureType) { super(x, y); @@ -161,10 +162,15 @@ export class NumberedKeySignatureGlyph extends Glyph { this._text = text + text2; this._accidental = accidental; const c = this.renderer.scoreRenderer.canvas!; - const res = this.renderer.resources; + const settings = this.renderer.settings; + const res = settings.display.resources; c.font = res.numberedNotationFont; this._accidentalOffset = c.measureText(text).width; - this.width = c.measureText(text + text2).width; + const fullSize = c.measureText(text + text2); + this._padding = + this.renderer.index === 0 ? settings.display.firstStaffPaddingLeft : settings.display.staffPaddingLeft; + this.width = this._padding + fullSize.width; + this.height = fullSize.height; } public override paint(cx: number, cy: number, canvas: ICanvas): void { @@ -172,13 +178,14 @@ export class NumberedKeySignatureGlyph extends Glyph { const res = this.renderer.resources; canvas.font = res.numberedNotationFont; - canvas.textBaseline = TextBaseline.Middle; - canvas.fillText(this._text, cx + this.x, cy + this.y); + canvas.textBaseline = TextBaseline.Alphabetic; + canvas.fillText(this._text, cx + this.x + this._padding, cy + this.y + this.height); if (this._accidental !== AccidentalType.None) { - CanvasHelper.fillMusicFontSymbolSafe(canvas, - cx + this.x + this._accidentalOffset, - cy + this.y, + CanvasHelper.fillMusicFontSymbolSafe( + canvas, + cx + this.x + this._padding + this._accidentalOffset, + cy + this.y + this.height, 1, AccidentalGlyph.getMusicSymbol(this._accidental), false diff --git a/packages/alphatab/src/rendering/glyphs/NumberedNoteHeadGlyph.ts b/packages/alphatab/src/rendering/glyphs/NumberedNoteHeadGlyph.ts index 65a3efde4..5a9ee0967 100644 --- a/packages/alphatab/src/rendering/glyphs/NumberedNoteHeadGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/NumberedNoteHeadGlyph.ts @@ -1,8 +1,9 @@ -import { type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; -import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; -import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; import { type Beat, BeatSubElement } from '@coderline/alphatab/model/Beat'; +import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol'; import { NoteSubElement } from '@coderline/alphatab/model/Note'; +import { CanvasHelper, type ICanvas, TextAlign, TextBaseline } from '@coderline/alphatab/platform/ICanvas'; +import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph'; +import { ElementStyleHelper } from '@coderline/alphatab/rendering/utils/ElementStyleHelper'; /** * @internal @@ -11,12 +12,36 @@ export class NumberedNoteHeadGlyph extends Glyph { private _isGrace: boolean; private _beat: Beat; private _number: string; + private _octaveDots: number; + private _octaveDotsY: number = 0; + private _octaveDotHeight: number = 0; - public constructor(x: number, y: number, number: string, isGrace: boolean, beat: Beat) { + public constructor(x: number, y: number, number: string, isGrace: boolean, beat: Beat, octaveDots: number) { super(x, y); this._isGrace = isGrace; this._number = number; this._beat = beat; + this._octaveDots = octaveDots; + } + + public override getBoundingBoxTop(): number { + let y = -this.height / 2; + + if (this._octaveDots > 0 && this._octaveDotsY < y) { + y = this._octaveDotsY; + } + return this.y + y; + } + + public override getBoundingBoxBottom(): number { + let y = this.height / 2; + + const dotsBottom = this._octaveDotsY + Math.abs(this._octaveDots) * this._octaveDotHeight * 2; + if (this._octaveDots < 0 && y < dotsBottom) { + y = dotsBottom; + } + + return this.y + y; } public override paint(cx: number, cy: number, canvas: ICanvas): void { @@ -28,9 +53,25 @@ export class NumberedNoteHeadGlyph extends Glyph { const res = this.renderer.resources; canvas.font = this._isGrace ? res.numberedNotationGraceFont : res.numberedNotationFont; + const baseline = canvas.textBaseline; canvas.textBaseline = TextBaseline.Middle; canvas.textAlign = TextAlign.Left; canvas.fillText(this._number.toString(), cx + this.x, cy + this.y); + canvas.textBaseline = baseline; + + const dotCount = Math.abs(this._octaveDots); + let dotsY = this._octaveDotsY + res.engravingSettings.glyphTop.get(MusicFontSymbol.AugmentationDot)!; + for (let d = 0; d < dotCount; d++) { + CanvasHelper.fillMusicFontSymbolSafe( + canvas, + cx + this.x + this.width / 2, + cy + this.y + dotsY, + 1, + MusicFontSymbol.AugmentationDot, + true + ); + dotsY += this._octaveDotHeight * 2; + } } public override doLayout(): void { @@ -38,8 +79,27 @@ export class NumberedNoteHeadGlyph extends Glyph { const font = this._isGrace ? res.numberedNotationGraceFont : res.numberedNotationFont; const c = this.renderer.scoreRenderer.canvas!; c.font = font; - const size = c.measureText(`${this._number}`); + const size = c.measureText(`${this._number}`); this.height = size.height; this.width = size.width; + + const dotCount = this._octaveDots; + + const dotHeight = res.engravingSettings.glyphHeights.get(MusicFontSymbol.AugmentationDot)!; + const allDotsHeight = Math.abs(dotCount) * dotHeight * 2; + if (dotCount > 0) { + this._octaveDotsY = + -(this.height / 2) - + allDotsHeight - + res.engravingSettings.glyphTop.get(MusicFontSymbol.AugmentationDot)!; + } else if (dotCount < 0) { + this._octaveDotsY = + this.height / 2 + + // one for the padding + dotHeight + + // align the dots + res.engravingSettings.glyphTop.get(MusicFontSymbol.AugmentationDot)!; + } + this._octaveDotHeight = dotHeight; } } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts index 55a11fa14..66cd6f24d 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreBendGlyph.ts @@ -35,6 +35,11 @@ export class ScoreBendGlyph extends ScoreHelperNotesBaseGlyph implements ITieGly this._container = container; } + public override doLayout(): void { + super.doLayout(); + this.width = 0; + } + public override getBoundingBoxTop(): number { return super.getBoundingBoxTop() - this._calculateMaxSlurHeight(BeamDirection.Up); } diff --git a/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts b/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts index 228a1203b..23b76eb49 100644 --- a/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/ScoreWhammyBarGlyph.ts @@ -145,6 +145,7 @@ export class ScoreWhammyBarGlyph extends ScoreHelperNotesBaseGlyph implements IT } super.doLayout(); + this.width = this.width / 2; } public override paint(cx: number, cy: number, canvas: ICanvas): void { diff --git a/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts index bd09b2030..845d43169 100644 --- a/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabWhammyBarGlyph.ts @@ -108,6 +108,7 @@ export class TabWhammyBarGlyph extends EffectGlyph { this.originalBottomOffset = bottomY; this.height = topY + bottomY; + this.width = 0; } private _getOffset(value: number): number { diff --git a/packages/alphatab/test-data/musicxml-testsuite/32a-Notations.png b/packages/alphatab/test-data/musicxml-testsuite/32a-Notations.png index 223592066..ef5812391 100644 Binary files a/packages/alphatab/test-data/musicxml-testsuite/32a-Notations.png and b/packages/alphatab/test-data/musicxml-testsuite/32a-Notations.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/onnotes-beat.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/onnotes-beat.png index c26dcd41d..c6f25acee 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/onnotes-beat.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/onnotes-beat.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-bar.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-bar.png index 2758512c2..459d83807 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-bar.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-bar.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-beat.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-beat.png index dafff5a83..b9132bb81 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-beat.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-beat.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-master.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-master.png index b035c3122..92305770c 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-master.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-master.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-note.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-note.png index 1750d63fa..58e8b7c87 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-note.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-note.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-system.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-system.png index 41ab1f937..09069e00c 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/real-system.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/real-system.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-bar.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-bar.png index 2758512c2..459d83807 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-bar.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-bar.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-beat.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-beat.png index eccb73651..8dbd79dbb 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-beat.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-beat.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-master.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-master.png index f96e3577e..7152d88da 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-master.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-master.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-note.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-note.png index 1750d63fa..58e8b7c87 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-note.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-note.png differ diff --git a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-system.png b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-system.png index 9dfbe8a02..79fc3bfd6 100644 Binary files a/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-system.png and b/packages/alphatab/test-data/visual-tests/bounds-lookup/visual-system.png differ diff --git a/packages/alphatab/test-data/visual-tests/effects-and-annotations/dead-slap.png b/packages/alphatab/test-data/visual-tests/effects-and-annotations/dead-slap.png index 4c3fb9b7f..f2c8d71b0 100644 Binary files a/packages/alphatab/test-data/visual-tests/effects-and-annotations/dead-slap.png and b/packages/alphatab/test-data/visual-tests/effects-and-annotations/dead-slap.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/colors-disabled.png b/packages/alphatab/test-data/visual-tests/general/colors-disabled.png index c0ee5cafa..bf6d3d9c4 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/colors-disabled.png and b/packages/alphatab/test-data/visual-tests/general/colors-disabled.png differ diff --git a/packages/alphatab/test-data/visual-tests/general/colors.png b/packages/alphatab/test-data/visual-tests/general/colors.png index 20c6c9328..bd4b0131e 100644 Binary files a/packages/alphatab/test-data/visual-tests/general/colors.png and b/packages/alphatab/test-data/visual-tests/general/colors.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/bends-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/bends-default.png index d8c6e90f5..c5f0ddeef 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/bends-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/bends-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/bends-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/bends-songbook.png index 9834afcc1..9b2fab2ce 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/bends-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/bends-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/full-default-large.png b/packages/alphatab/test-data/visual-tests/notation-legend/full-default-large.png index 043e05093..e53d56b75 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/full-default-large.png and b/packages/alphatab/test-data/visual-tests/notation-legend/full-default-large.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/full-default-small.png b/packages/alphatab/test-data/visual-tests/notation-legend/full-default-small.png index e4e345000..9e20f736b 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/full-default-small.png and b/packages/alphatab/test-data/visual-tests/notation-legend/full-default-small.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/full-default.png b/packages/alphatab/test-data/visual-tests/notation-legend/full-default.png index b38ecb7a4..679503301 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/full-default.png and b/packages/alphatab/test-data/visual-tests/notation-legend/full-default.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/full-songbook.png b/packages/alphatab/test-data/visual-tests/notation-legend/full-songbook.png index 936560d5f..0d34c23b9 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/full-songbook.png and b/packages/alphatab/test-data/visual-tests/notation-legend/full-songbook.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1500.png b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1500.png index 401ce05bf..f3b0634d2 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1500.png and b/packages/alphatab/test-data/visual-tests/notation-legend/resize-sequence-1500.png differ diff --git a/packages/alphatab/test-data/visual-tests/notation-legend/smufl-petaluma-1300.png b/packages/alphatab/test-data/visual-tests/notation-legend/smufl-petaluma-1300.png index 82e4394de..fa96f4b6f 100644 Binary files a/packages/alphatab/test-data/visual-tests/notation-legend/smufl-petaluma-1300.png and b/packages/alphatab/test-data/visual-tests/notation-legend/smufl-petaluma-1300.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-tracks/numbered-durations.png b/packages/alphatab/test-data/visual-tests/special-tracks/numbered-durations.png new file mode 100644 index 000000000..67d220232 Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/special-tracks/numbered-durations.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-tracks/numbered-tuplets.png b/packages/alphatab/test-data/visual-tests/special-tracks/numbered-tuplets.png new file mode 100644 index 000000000..607cff1de Binary files /dev/null and b/packages/alphatab/test-data/visual-tests/special-tracks/numbered-tuplets.png differ diff --git a/packages/alphatab/test-data/visual-tests/special-tracks/numbered.png b/packages/alphatab/test-data/visual-tests/special-tracks/numbered.png index 8ba28563b..5367c88a6 100644 Binary files a/packages/alphatab/test-data/visual-tests/special-tracks/numbered.png and b/packages/alphatab/test-data/visual-tests/special-tracks/numbered.png differ diff --git a/packages/alphatab/test/visualTests/features/SpecialTracks.test.ts b/packages/alphatab/test/visualTests/features/SpecialTracks.test.ts index 3e631b270..e6463e9a0 100644 --- a/packages/alphatab/test/visualTests/features/SpecialTracks.test.ts +++ b/packages/alphatab/test/visualTests/features/SpecialTracks.test.ts @@ -1,3 +1,4 @@ +import { SystemsLayoutMode } from '@coderline/alphatab/DisplaySettings'; import { VisualTestHelper } from 'test/visualTests/VisualTestHelper'; describe('SpecialTracksTests', () => { @@ -22,4 +23,102 @@ describe('SpecialTracksTests', () => { it('numbered', async () => { await VisualTestHelper.runVisualTest('special-tracks/numbered.gp'); }); + + it('numbered-tuplets', async () => { + await VisualTestHelper.runVisualTestTex( + ` + \\track "Num" + \\staff {numbered} + + C2.2 {tu 3}*3 | + C7.2 {tu 3}*3 | + + C2.16 {tu 3}*3 | + C7.16 {tu 3}*3 | + + + \\track "Std & Num" + \\staff {score numbered} + + C2.2 {tu 3}*3 | + C7.2 {tu 3}*3 | + + C2.16 {tu 3}*3 | + C7.16 {tu 3}*3 | + `, + 'test-data/visual-tests/special-tracks/numbered-tuplets.png', + undefined, + o => { + o.tracks = o.score.tracks.map(t => t.index); + } + ); + }); + + it('numbered-durations', async () =>{ + await VisualTestHelper.runVisualTestTex( + ` + \\bracketExtendMode noBrackets + \\track {defaultSystemsLayout 1} + \\staff {numbered} + \\section ("16th notes") + C4.16 * 16 | + + \\section ("8th notes") + C4.8 * 8 | + \\section ("8th notes dotted") + C4.8 {d} * 6 r.16 | + \\section ("8th notes double-dotted") + C4.8 {dd} * 4 r.8 | + + \\section ("Quarter notes") + C4.4 * 4 | + \\section ("Quarter notes dotted") + C4.4 {d} * 2 r.4 | + \\section ("Quarter notes double-dotted") + C4.4 {dd} * 2 r.8 | + + \\section ("Half notes") + C4.2 * 2 | + \\section ("Half notes dotted") + C4.2 {d} r.4 | + \\section ("Half notes double dotted") + C4.2 {dd} r.8 | + + \\section ("Whole notes") + \\ts (8 4) + C4.1 * 2 | + \\section ("Half notes dotted") + C4.1 {d} r.2 | + \\section ("Half notes double dotted") + C4.1 {dd} r.4 | + + \\staff {numbered} + C4.4 * 4 | + + C4.4 * 4 | + C4.4 * 4 | + C4.4 * 4 | + + C4.4 * 4 | + C4.4 * 4 | + C4.4 * 4 | + + C4.4 * 4 | + C4.4 * 4 | + C4.4 * 4 | + + C4.4 * 8 | + C4.4 * 8 | + C4.4 * 8 | + + r + `, + 'test-data/visual-tests/special-tracks/numbered-durations.png', + undefined, + o => { + o.settings.display.systemsLayoutMode = SystemsLayoutMode.UseModelLayout; + o.tracks = o.score.tracks.map(t => t.index); + } + ); + }) });