Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/alphatab/src/NotationSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,12 @@ export enum NotationElement {
/**
* The slurs shown on bend effects within the score staff.
*/
ScoreBendSlur = 55
ScoreBendSlur = 55,

/**
* The hammer-on pull-off text shown on slurs.
*/
EffectHammerOnPullOffText = 56,
}

/**
Expand Down
13 changes: 11 additions & 2 deletions packages/alphatab/src/RenderingResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ export class RenderingResources {
[NotationElement.RepeatCount, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
[NotationElement.BarNumber, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
[NotationElement.ScoreBendSlur, new Font(RenderingResources._sansFont, 11, FontStyle.Plain)],
[NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)]
[NotationElement.EffectAlternateEndings, new Font(RenderingResources._serifFont, 15, FontStyle.Plain)],
[NotationElement.EffectHammerOnPullOffText, RenderingResources._effectFont]
]);

/**
Expand Down Expand Up @@ -381,8 +382,16 @@ export class RenderingResources {
break;
}

return this.getFontForNotationElement(notationElement);
}

/**
* @internal
* @param element
*/
public getFontForNotationElement(notationElement: NotationElement): Font {
return this.elementFonts.has(notationElement)
? this.elementFonts.get(notationElement)!
: RenderingResources.defaultFonts.get(NotationElement.ScoreWords)!;
: RenderingResources.defaultFonts.get(notationElement)!;
}
}
56 changes: 52 additions & 4 deletions packages/alphatab/src/rendering/glyphs/TabBeatContainerGlyph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand All @@ -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)) {
Expand Down
14 changes: 12 additions & 2 deletions packages/alphatab/src/rendering/glyphs/TabSlurGlyph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
43 changes: 40 additions & 3 deletions packages/alphatab/src/rendering/glyphs/TieGlyph.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Note } from '@coderline/alphatab/model/Note';
import type { ICanvas } from '@coderline/alphatab/platform/ICanvas';
import { NotationElement } from '@coderline/alphatab/NotationSettings';
import { TextAlign, type ICanvas } from '@coderline/alphatab/platform/ICanvas';
import { type BarRendererBase, NoteXPosition, NoteYPosition } from '@coderline/alphatab/rendering/BarRendererBase';
import { Glyph } from '@coderline/alphatab/rendering/glyphs/Glyph';
import type { LineBarRenderer } from '@coderline/alphatab/rendering/LineBarRenderer';
Expand Down Expand Up @@ -33,6 +34,7 @@ export abstract class TieGlyph extends Glyph implements ITieGlyph {

private _startX: number = 0;
private _startY: number = 0;
private _slurTextY: number = 0;
private _endX: number = 0;
private _endY: number = 0;
private _tieHeight: number = 0;
Expand Down Expand Up @@ -146,6 +148,25 @@ export abstract class TieGlyph extends Glyph implements ITieGlyph {
}

this._boundingBox = tieBoundingBox;
const slurText = this.getSlurText();
if (slurText) {
// text will be aligned on the arc with a slight padding
const c = this.renderer.scoreRenderer.canvas!;
const settings = this.renderer.settings;
const res = settings.display.resources;
c.font = res.getFontForNotationElement(NotationElement.EffectHammerOnPullOffText);
const textSize = c.measureText(slurText);
const padding = this.renderer.smuflMetrics.tieMidpointThickness;

if (this.tieDirection === BeamDirection.Up) {
tieBoundingBox.y -= textSize.height + padding;
this._slurTextY = tieBoundingBox.y;
tieBoundingBox.h += textSize.height + padding;
} else {
this._slurTextY = tieBoundingBox.y + tieBoundingBox.h + padding;
tieBoundingBox.h += textSize.height + padding;
}
}

this.height = tieBoundingBox.h;

Expand All @@ -165,14 +186,16 @@ export abstract class TieGlyph extends Glyph implements ITieGlyph {
return;
}

const isDown = this.tieDirection === BeamDirection.Down;

if (this.shouldDrawBendSlur()) {
TieGlyph.drawBendSlur(
canvas,
cx + this._startX,
cy + this._startY,
cx + this._endX,
cy + this._endY,
this.tieDirection === BeamDirection.Down,
isDown,
this.renderer.smuflMetrics.tieHeight
);
} else {
Expand All @@ -183,11 +206,25 @@ 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 ta = canvas.textAlign;
canvas.textAlign = TextAlign.Center;
canvas.font = this.renderer.resources.getFontForNotationElement(NotationElement.EffectHammerOnPullOffText);
const midX = cx + (this._startX + this._endX) / 2;
canvas.fillText(slurText, midX, cy + this._slurTextY);
canvas.textAlign = ta;
}
}

protected getSlurText(): string | undefined {
return undefined;
}

protected abstract shouldDrawBendSlur(): boolean;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { describe, expect, it } from 'vitest';
import { ScoreLoader } from '@coderline/alphatab/importer/ScoreLoader';
import { LayoutMode } from '@coderline/alphatab/LayoutMode';
import { BeatBarreEffectInfo } from '@coderline/alphatab/rendering/effects/BeatBarreEffectInfo';
import { Settings } from '@coderline/alphatab/Settings';
import { TestPlatform } from 'test/TestPlatform';
import { VisualTestHelper, VisualTestOptions, VisualTestRun } from 'test/visualTests/VisualTestHelper';
import { describe, expect, it } from 'vitest';

describe('EffectsAndAnnotationsTests', () => {
it('markers', async () => {
Expand Down Expand Up @@ -573,4 +573,28 @@ describe('EffectsAndAnnotationsTests', () => {
);
});
});

describe('hopo-arcs', () => {
async function test(test: string, tex: string) {
await VisualTestHelper.runVisualTestTex(
tex,
`test-data/visual-tests/effects-and-annotations/hopo-arcs-${test}.png`
);
}

it('at1', async () => await test('at1', ':4 5.3{h} 7.3 r r'));
it('at2', async () => await test('at2', ':4 7.3{h} 5.3 r r'));
it('at3', async () => await test('at3', ':4 5.3{h} 7.3 7.3{h} 5.3'));
it('at4', async () => await test('at4', ':4 5.3{h} 7.3 8.4{h} 5.4'));
it('at5', async () => await test('at5', ':4 5.3{h} 7.3{h} 5.3 r'));
it('at6', async () => await test('at6', ':8 5.3{h} 7.3{h} 5.3{h} 7.3 r r r r'));
it('at7', async () => await test('at7', ':4 5.3{sl} 7.3 r r'));
it('at8', async () => await test('at8', ':4 5.3 7.3 5.3 7.3'));
it('at9', async () => await test('at9', ':4 (5.3{h} 5.4) (7.3 7.4) r r'));
it('at10', async () => await test('at10', ':4 (5.3 5.4{h}) (7.3 7.4) r r'));
it('at11', async () => await test('at11', ':4 (5.3{h} 5.4{h}) (7.3 7.4) r r'));
it('at12', async () => await test('at12', ':4 (5.3{h} 7.4{h}) (7.3 5.4) r r'));
it('at13', async () => await test('at13', ':4 (5.3{h} 7.4{h}) (7.3{h} 5.4{h}) (5.3 7.4) r'));
it('at14', async () => await test('at14', ':4 5.3 {h} 7.3{h} 5.3 | 5.4 {h} 7.4{h} 5.4'));
});
});
Loading