diff --git a/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts index 437899ea0..adba41f6e 100644 --- a/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts @@ -56,8 +56,56 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { const tapSlur: TabTieGlyph = new TabTieGlyph(`tab.tie.leftHandTap.${n.id}`, n, n, false); this.addTie(tapSlur); } - // start effect slur on first beat - if (n.isEffectSlurOrigin && n.effectSlurDestination) { + // H/P arc start-side: create individual arc per hammer-pull pair + if (n.isHammerPullOrigin && n.hammerPullDestination) { + const dest = n.hammerPullDestination; + const slurText = dest.fret >= n.fret ? 'H' : 'P'; + let expanded: boolean = false; + for (const slur of this._effectSlurs) { + if (slur.tryExpand(n, dest, false, false, slurText)) { + expanded = true; + break; + } + } + if (!expanded) { + const effectSlur: TabSlurGlyph = new TabSlurGlyph( + `tab.slur.effect.${n.id}`, + n, + dest, + false, + false, + slurText + ); + this._effectSlurs.push(effectSlur); + this.addTie(effectSlur); + } + } + // H/P arc end-side: for cross-bar rendering + if (n.isHammerPullDestination && n.hammerPullOrigin) { + const origin = n.hammerPullOrigin; + const slurText = n.fret >= origin.fret ? 'H' : 'P'; + let expanded: boolean = false; + for (const slur of this._effectSlurs) { + if (slur.tryExpand(origin, n, false, true, slurText)) { + expanded = true; + break; + } + } + if (!expanded) { + const effectSlur: TabSlurGlyph = new TabSlurGlyph( + `tab.slur.effect.${origin.id}`, + origin, + n, + false, + true, + slurText + ); + this._effectSlurs.push(effectSlur); + this.addTie(effectSlur); + } + } + // start non-H/P effect slur (e.g. legato slide) + if (n.isEffectSlurOrigin && n.effectSlurDestination && !n.isHammerPullOrigin) { let expanded: boolean = false; for (const slur of this._effectSlurs) { if (slur.tryExpand(n, n.effectSlurDestination, false, false)) { @@ -77,8 +125,8 @@ export class TabBeatContainerGlyph extends BeatContainerGlyph { this.addTie(effectSlur); } } - // end effect slur on last beat - if (n.isEffectSlurDestination && n.effectSlurOrigin) { + // end non-H/P effect slur + if (n.isEffectSlurDestination && n.effectSlurOrigin && !n.isHammerPullDestination) { let expanded: boolean = false; for (const slur of this._effectSlurs) { if (slur.tryExpand(n.effectSlurOrigin, n, false, true)) { diff --git a/packages/alphatab/src/rendering/glyphs/TabSlurGlyph.ts b/packages/alphatab/src/rendering/glyphs/TabSlurGlyph.ts index 97ba66030..4c34d45c7 100644 --- a/packages/alphatab/src/rendering/glyphs/TabSlurGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TabSlurGlyph.ts @@ -7,17 +7,27 @@ import { BeamDirection } from '@coderline/alphatab/rendering/utils/BeamDirection */ export class TabSlurGlyph extends TabTieGlyph { private _forSlide: boolean; + private readonly _slurText?: string; - public constructor(slurEffectId: string, startNote: Note, endNote: Note, forSlide: boolean, forEnd:boolean) { + public constructor(slurEffectId: string, startNote: Note, endNote: Note, forSlide: boolean, forEnd:boolean, slurText?: string) { super(slurEffectId, startNote, endNote, forEnd); this._forSlide = forSlide; + this._slurText = slurText; } public override getTieHeight(startX: number, _startY: number, endX: number, _endY: number): number { return (Math.log(endX - startX + 1) * this.renderer.settings.notation.slurHeight) / 2; } - public tryExpand(startNote: Note, endNote: Note, forSlide: boolean, forEnd: boolean): boolean { + protected override getSlurText(): string | undefined { + return this._slurText; + } + + public tryExpand(startNote: Note, endNote: Note, forSlide: boolean, forEnd: boolean, slurText?: string): boolean { + // same label required (when provided) + if (slurText !== undefined && this._slurText !== slurText) { + return false; + } // same type required if (this._forSlide !== forSlide) { return false; diff --git a/packages/alphatab/src/rendering/glyphs/TieGlyph.ts b/packages/alphatab/src/rendering/glyphs/TieGlyph.ts index 5b96249a0..8a0cf8119 100644 --- a/packages/alphatab/src/rendering/glyphs/TieGlyph.ts +++ b/packages/alphatab/src/rendering/glyphs/TieGlyph.ts @@ -165,6 +165,8 @@ export abstract class TieGlyph extends Glyph implements ITieGlyph { return; } + const isDown = this.tieDirection === BeamDirection.Down; + if (this.shouldDrawBendSlur()) { TieGlyph.drawBendSlur( canvas, @@ -172,7 +174,7 @@ export abstract class TieGlyph extends Glyph implements ITieGlyph { cy + this._startY, cx + this._endX, cy + this._endY, - this.tieDirection === BeamDirection.Down, + isDown, this.renderer.smuflMetrics.tieHeight ); } else { @@ -183,11 +185,31 @@ export abstract class TieGlyph extends Glyph implements ITieGlyph { cy + this._startY, cx + this._endX, cy + this._endY, - this.tieDirection === BeamDirection.Down, + isDown, this._tieHeight, this.renderer.smuflMetrics.tieMidpointThickness ); } + + const slurText = this.getSlurText(); + if (slurText) { + const midX = cx + (this._startX + this._endX) / 2; + const midY = cy + (this._startY + this._endY) / 2; + const apexOffset = this._tieHeight * 0.75; + const apexY = midY + (isDown ? apexOffset : -apexOffset); + const w = canvas.measureText(slurText).width; + const fontSize = canvas.font.size; + // text above: fontSize already includes descender space below the baseline, + // providing natural padding for capital letters like H/P + const textY = isDown + ? apexY + fontSize * 0.3 + : apexY - fontSize * 1.05; + canvas.fillText(slurText, midX - w / 2, textY); + } + } + + protected getSlurText(): string | undefined { + return undefined; } protected abstract shouldDrawBendSlur(): boolean;