Skip to content

Commit a4e4aff

Browse files
committed
fix: ensure clean beam-stem alignment on subpixel rendering
1 parent 1edf83d commit a4e4aff

1 file changed

Lines changed: 43 additions & 25 deletions

File tree

packages/alphatab/src/rendering/LineBarRenderer.ts

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -693,32 +693,48 @@ export abstract class LineBarRenderer extends BarRendererBase {
693693
continue;
694694
}
695695

696-
const beatLineX: number = this.getBeatX(beat, BeatXPosition.Stem);
696+
const stemX: number = this.getBeatX(beat, BeatXPosition.Stem);
697697
let y1: number = cy + this.y;
698698
if (direction === BeamDirection.Up) {
699699
y1 += this.getFlagBottomY(beat, direction);
700700
} else {
701701
y1 += this.getFlagTopY(beat, direction);
702702
}
703703

704-
// ensure we are pixel aligned on the end of the stem to avoid anti-aliasing artifacts
705-
// when combining stems and beams on sub-pixel level
706-
const y2: number = (cy + this.y + this.calculateBeamY(h, beatLineX)) | 0;
704+
const y2: number = cy + this.y + this.calculateBeamY(h, stemX);
705+
706+
// improve subpixel related artifacts on stem/beam overlaps
707+
let stemY1: number;
708+
let stemY2: number;
709+
if (y1 < y2) {
710+
stemY1 = y1;
711+
stemY2 = y2;
712+
} else {
713+
stemY1 = y2;
714+
stemY2 = y1;
715+
}
716+
717+
if (direction === BeamDirection.Up) {
718+
stemY1 = Math.ceil(stemY1);
719+
} else {
720+
stemY2 = Math.floor(stemY2);
721+
}
707722

708723
if (y1 < y2) {
709-
this.paintBeamingStem(beat, cy + this.y, cx + this.x + beatLineX, y1, y2, canvas);
724+
this.paintBeamingStem(beat, cy + this.y, cx + this.x + stemX, stemY1, stemY2, canvas);
710725
} else {
711-
this.paintBeamingStem(beat, cy + this.y, cx + this.x + beatLineX, y2, y1, canvas);
726+
this.paintBeamingStem(beat, cy + this.y, cx + this.x + stemX, stemY2, stemY1, canvas);
712727
}
713728

714729
using _ = ElementStyleHelper.beat(canvas, beamsElement, beat);
715730

716731
const brokenBarOffset: number = this.smuflMetrics.brokenBeamWidth * scaleMod;
717732
const barCount: number = ModelUtils.getIndex(beat.duration) - 2;
718733
const barStart: number = cy + this.y;
734+
const stemThickness = this.smuflMetrics.stemThickness;
719735

720736
for (let barIndex: number = 0; barIndex < barCount; barIndex++) {
721-
let barStartX: number = 0;
737+
let barStartX: number = Math.floor(stemX + stemThickness);
722738
let barEndX: number = 0;
723739
let barStartY: number = 0;
724740
let barEndY: number = 0;
@@ -737,8 +753,7 @@ export abstract class LineBarRenderer extends BarRendererBase {
737753
beat.beamingMode === BeatBeamingMode.ForceSplitOnSecondaryToNext
738754
) {
739755
// start part
740-
barStartX = beatLineX;
741-
barEndX = barStartX + brokenBarOffset;
756+
barEndX = Math.ceil(barStartX + brokenBarOffset);
742757
barStartY = barY + this.calculateBeamY(h, barStartX);
743758
barEndY = barY + this.calculateBeamY(h, barEndX);
744759
LineBarRenderer.paintSingleBar(
@@ -751,8 +766,8 @@ export abstract class LineBarRenderer extends BarRendererBase {
751766
);
752767

753768
// end part
754-
barEndX = this.getBeatX(h.beats[i + 1], BeatXPosition.Stem);
755-
barStartX = barEndX - brokenBarOffset;
769+
barEndX = Math.floor(this.getBeatX(h.beats[i + 1], BeatXPosition.Stem));
770+
barStartX = Math.floor(barEndX - brokenBarOffset);
756771
barStartY = barY + this.calculateBeamY(h, barStartX);
757772
barEndY = barY + this.calculateBeamY(h, barEndX);
758773
LineBarRenderer.paintSingleBar(
@@ -766,24 +781,15 @@ export abstract class LineBarRenderer extends BarRendererBase {
766781
} else {
767782
if (isFullBarJoin) {
768783
// full bar?
769-
barStartX = beatLineX;
770-
barEndX = this.getBeatX(h.beats[i + 1], BeatXPosition.Stem);
784+
barEndX = Math.ceil(this.getBeatX(h.beats[i + 1], BeatXPosition.Stem));
771785
} else if (i === 0 || !BeamingHelper.isFullBarJoin(h.beats[i - 1], beat, barIndex)) {
772-
barStartX = beatLineX;
773-
barEndX = barStartX + brokenBarOffset;
786+
barEndX = Math.ceil(barStartX + brokenBarOffset);
774787
} else {
775788
continue;
776789
}
777790
barStartY = barY + this.calculateBeamY(h, barStartX);
778791
barEndY = barY + this.calculateBeamY(h, barEndX);
779792

780-
// ensure we are pixel aligned on the end of the stem to avoid anti-aliasing artifacts
781-
// when combining stems and beams on sub-pixel level
782-
if (barIndex === 0) {
783-
barStartY = barStartY | 0;
784-
barEndY = barEndY | 0;
785-
}
786-
787793
LineBarRenderer.paintSingleBar(
788794
canvas,
789795
cx + this.x + barStartX,
@@ -794,9 +800,8 @@ export abstract class LineBarRenderer extends BarRendererBase {
794800
);
795801
}
796802
} else if (i > 0 && !BeamingHelper.isFullBarJoin(beat, h.beats[i - 1], barIndex)) {
797-
barStartX = beatLineX - brokenBarOffset;
798-
barEndX = beatLineX;
799-
barEndX = beatLineX;
803+
barEndX = Math.ceil(stemX);
804+
barStartX = Math.floor(stemX - brokenBarOffset);
800805
barStartY = barY + this.calculateBeamY(h, barStartX);
801806
barEndY = barY + this.calculateBeamY(h, barEndX);
802807
LineBarRenderer.paintSingleBar(
@@ -811,6 +816,19 @@ export abstract class LineBarRenderer extends BarRendererBase {
811816
}
812817
}
813818

819+
// const firstStartX = this.getBeatX(h.beats[0], BeatXPosition.Stem);
820+
// const firstStartY = this.calculateBeamY(h, firstStartX);
821+
// const lastEndX = this.getBeatX(h.beats[h.beats.length - 1], BeatXPosition.Stem);
822+
// const lastEndY = this.calculateBeamY(h, lastEndX);
823+
// canvas.lineWidth = 0.5;
824+
// const c = canvas.color;
825+
// canvas.color = new Color(255, 0, 0);
826+
// canvas.moveTo(cx + this.x + firstStartX, cy + this.y + firstStartY);
827+
// canvas.lineTo(cx + this.x + lastEndX, cy + this.y + lastEndY);
828+
// canvas.stroke();
829+
// canvas.lineWidth = 1;
830+
// canvas.color = c;
831+
814832
if (h.graceType === GraceType.BeforeBeat) {
815833
const beatLineX: number = this.getBeatX(h.beats[0], BeatXPosition.Stem);
816834
const flagWidth =

0 commit comments

Comments
 (0)