Skip to content

Commit e1b61c8

Browse files
committed
feat: Add support for highlighting on raster canvas
1 parent b80c443 commit e1b61c8

16 files changed

Lines changed: 226 additions & 97 deletions

File tree

src.csharp/AlphaTab/Environment.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ private static void CreatePlatformSpecificRenderEngines(IMap<string, RenderEngin
3737
{
3838
renderEngines.Set(
3939
"gdi",
40-
new RenderEngineFactory(true, () => new GdiCanvas())
40+
new RenderEngineFactory(true, false, () => new GdiCanvas())
4141
);
4242
renderEngines.Set("default", renderEngines.Get("skia")!);
4343
}

src.kotlin/alphaTab/alphaTab/src/androidMain/kotlin/alphaTab/EnvironmentPartials.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import kotlin.contracts.ExperimentalContracts
99
internal actual fun createPlatformSpecificRenderEngines(engines: Map<String, RenderEngineFactory>) {
1010
engines.set(
1111
"android",
12-
RenderEngineFactory(true) { AndroidCanvas() }
12+
RenderEngineFactory(true, false) { AndroidCanvas() }
1313
)
1414
engines.set(
1515
"default",

src/AlphaTabApiBase.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import { IContainer } from '@src/platform/IContainer';
2121
import { IMouseEventArgs } from '@src/platform/IMouseEventArgs';
2222
import { IUiFacade } from '@src/platform/IUiFacade';
2323
import { ScrollMode } from '@src/PlayerSettings';
24-
import { BeatContainerGlyph } from '@src/rendering/glyphs/BeatContainerGlyph';
2524

2625
import { IScoreRenderer } from '@src/rendering/IScoreRenderer';
2726

@@ -194,7 +193,9 @@ export class AlphaTabApiBase<TSettings> {
194193
if (this.settings.player.enablePlayer) {
195194
this.setupPlayer();
196195
if (score) {
197-
this.player?.applyTranspositionPitches(MidiFileGenerator.buildTranspositionPitches(score, this.settings));
196+
this.player?.applyTranspositionPitches(
197+
MidiFileGenerator.buildTranspositionPitches(score, this.settings)
198+
);
198199
}
199200
} else {
200201
this.destroyPlayer();
@@ -290,7 +291,7 @@ export class AlphaTabApiBase<TSettings> {
290291
for (let track of tracks) {
291292
this._trackIndexes.push(track.index);
292293
}
293-
this._trackIndexLookup = new Set<number>(this._trackIndexes)
294+
this._trackIndexLookup = new Set<number>(this._trackIndexes);
294295
this.onScoreLoaded(score);
295296
this.loadMidiForScore();
296297
this.render();
@@ -300,7 +301,7 @@ export class AlphaTabApiBase<TSettings> {
300301
for (let track of tracks) {
301302
this._trackIndexes.push(track.index);
302303
}
303-
this._trackIndexLookup = new Set<number>(this._trackIndexes)
304+
this._trackIndexLookup = new Set<number>(this._trackIndexes);
304305
this.render();
305306
}
306307
}
@@ -1010,18 +1011,13 @@ export class AlphaTabApiBase<TSettings> {
10101011
}
10111012

10121013
// if playing, animate the cursor to the next beat
1013-
if (this.settings.player.enableElementHighlighting) {
1014-
this.uiFacade.removeHighlights();
1015-
}
1016-
1017-
// actively playing? -> animate cursor and highlight items
10181014
let shouldNotifyBeatChange = false;
10191015
if (this._playerState === PlayerState.Playing && !stop) {
10201016
if (this.settings.player.enableElementHighlighting) {
1021-
for (let highlight of beatsToHighlight) {
1022-
let className: string = BeatContainerGlyph.getGroupId(highlight.beat);
1023-
this.uiFacade.highlightElements(className, beat.voice.bar.index);
1024-
}
1017+
this.uiFacade.highlightBeats(
1018+
beatsToHighlight.map(h => h.beat),
1019+
beat.voice.bar.index
1020+
);
10251021
}
10261022

10271023
if (this.settings.player.enableAnimatedBeatCursor) {
@@ -1033,8 +1029,7 @@ export class AlphaTabApiBase<TSettings> {
10331029
let nextBeatBoundings: BeatBounds | null = cache.findBeat(nextBeat);
10341030
if (
10351031
nextBeatBoundings &&
1036-
nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds ===
1037-
barBoundings.staffSystemBounds
1032+
nextBeatBoundings.barBounds.masterBarBounds.staffSystemBounds === barBoundings.staffSystemBounds
10381033
) {
10391034
nextBeatX = nextBeatBoundings.visualBounds.x;
10401035
}
@@ -1050,6 +1045,11 @@ export class AlphaTabApiBase<TSettings> {
10501045

10511046
shouldScroll = !stop;
10521047
shouldNotifyBeatChange = true;
1048+
} else if (this.settings.player.enableElementHighlighting) {
1049+
this.uiFacade.highlightBeats( // clear highlights
1050+
[],
1051+
beat.voice.bar.index
1052+
);
10531053
}
10541054

10551055
if (shouldScroll && !this._beatMouseDown && this.settings.player.scrollMode !== ScrollMode.Off) {
@@ -1158,8 +1158,12 @@ export class AlphaTabApiBase<TSettings> {
11581158
this.settings.player.enableUserInteraction
11591159
) {
11601160
if (this._selectionEnd) {
1161-
let startTick: number = this._tickCache?.getBeatStart(this._selectionStart!.beat) ?? this._selectionStart!.beat.absolutePlaybackStart;
1162-
let endTick: number = this._tickCache?.getBeatStart(this._selectionEnd!.beat) ?? this._selectionEnd!.beat.absolutePlaybackStart;
1161+
let startTick: number =
1162+
this._tickCache?.getBeatStart(this._selectionStart!.beat) ??
1163+
this._selectionStart!.beat.absolutePlaybackStart;
1164+
let endTick: number =
1165+
this._tickCache?.getBeatStart(this._selectionEnd!.beat) ??
1166+
this._selectionEnd!.beat.absolutePlaybackStart;
11631167
if (endTick < startTick) {
11641168
let t: SelectionInfo = this._selectionStart!;
11651169
this._selectionStart = this._selectionEnd;
@@ -1419,6 +1423,7 @@ export class AlphaTabApiBase<TSettings> {
14191423
return;
14201424
}
14211425
(this.renderStarted as EventEmitterOfT<boolean>).trigger(resize);
1426+
this.uiFacade.onRenderStarted(resize);
14221427
this.uiFacade.triggerEvent(this.container, 'renderStarted', resize);
14231428
}
14241429

src/Environment.ts

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,12 @@ export class LayoutEngineFactory {
7474

7575
export class RenderEngineFactory {
7676
public readonly supportsWorkers: boolean;
77+
public readonly supportsDomHighlighting: boolean;
7778
public readonly createCanvas: () => ICanvas;
7879

79-
public constructor(supportsWorkers: boolean, canvas: () => ICanvas) {
80+
public constructor(supportsWorkers: boolean, supportsDomHighlighting: boolean, canvas: () => ICanvas) {
8081
this.supportsWorkers = supportsWorkers;
82+
this.supportsDomHighlighting = supportsDomHighlighting;
8183
this.createCanvas = canvas;
8284
}
8385
}
@@ -423,15 +425,15 @@ export class Environment {
423425
const renderEngines = new Map<string, RenderEngineFactory>();
424426
renderEngines.set(
425427
'svg',
426-
new RenderEngineFactory(true, () => {
428+
new RenderEngineFactory(true, true, () => {
427429
return new CssFontSvgCanvas();
428430
})
429431
);
430432
renderEngines.set('default', renderEngines.get('svg')!);
431433

432434
renderEngines.set(
433435
'skia',
434-
new RenderEngineFactory(false, () => {
436+
new RenderEngineFactory(false, false, () => {
435437
return new SkiaCanvas();
436438
})
437439
);
@@ -442,11 +444,11 @@ export class Environment {
442444

443445
/**
444446
* Enables the usage of alphaSkia as rendering backend.
445-
* @param musicFontData The raw binary data of the music font.
447+
* @param musicFontData The raw binary data of the music font.
446448
* @param alphaSkia The alphaSkia module.
447449
*/
448450
public static enableAlphaSkia(musicFontData: ArrayBuffer, alphaSkia: unknown) {
449-
SkiaCanvas.enable(musicFontData, alphaSkia)
451+
SkiaCanvas.enable(musicFontData, alphaSkia);
450452
}
451453

452454
/**
@@ -456,9 +458,7 @@ export class Environment {
456458
* @param fontInfo If provided the font info provided overrules
457459
* @returns The font info under which the font was registered.
458460
*/
459-
public static registerAlphaSkiaCustomFont(
460-
fontData: Uint8Array,
461-
fontInfo?: Font | undefined): Font {
461+
public static registerAlphaSkiaCustomFont(fontData: Uint8Array, fontInfo?: Font | undefined): Font {
462462
return SkiaCanvas.registerFont(fontData, fontInfo);
463463
}
464464

@@ -469,7 +469,7 @@ export class Environment {
469469
private static createPlatformSpecificRenderEngines(renderEngines: Map<string, RenderEngineFactory>) {
470470
renderEngines.set(
471471
'html5',
472-
new RenderEngineFactory(false, () => {
472+
new RenderEngineFactory(false, false, () => {
473473
return new Html5Canvas();
474474
})
475475
);
@@ -485,7 +485,7 @@ export class Environment {
485485
new TripletFeelEffectInfo(),
486486
new MarkerEffectInfo(),
487487
new TextEffectInfo(),
488-
new ChordsEffectInfo(),
488+
new ChordsEffectInfo()
489489
]),
490490
new SlashBarRendererFactory(),
491491
new EffectBarRendererFactory('score-effects', [
@@ -535,8 +535,8 @@ export class Environment {
535535
new TripletFeelEffectInfo(),
536536
new MarkerEffectInfo(),
537537
new TextEffectInfo(),
538-
new ChordsEffectInfo(),
539-
]),
538+
new ChordsEffectInfo()
539+
]),
540540
new SlashBarRendererFactory(),
541541
new EffectBarRendererFactory('score-effects', [
542542
new FermataEffectInfo(),
@@ -632,15 +632,12 @@ export class Environment {
632632
createWebWorker: (settings: Settings) => Worker,
633633
createAudioWorklet: (context: AudioContext, settings: Settings) => Promise<void>
634634
) {
635-
if(Environment.isRunningInWorker || Environment.isRunningInAudioWorklet) {
635+
if (Environment.isRunningInWorker || Environment.isRunningInAudioWorklet) {
636636
return;
637637
}
638-
638+
639639
// browser polyfills
640-
if (
641-
Environment.webPlatform === WebPlatform.Browser ||
642-
Environment.webPlatform === WebPlatform.BrowserModule
643-
) {
640+
if (Environment.webPlatform === WebPlatform.Browser || Environment.webPlatform === WebPlatform.BrowserModule) {
644641
Environment.registerJQueryPlugin();
645642
Environment.HighDpiFactor = window.devicePixelRatio;
646643
// ResizeObserver API does not yet exist so long on Safari (only start 2020 with iOS Safari 13.7 and Desktop 13.1)
@@ -660,7 +657,9 @@ export class Environment {
660657
this.append(...nodes);
661658
};
662659
(Document.prototype as Document).replaceChildren = (Element.prototype as Element).replaceChildren;
663-
(DocumentFragment.prototype as DocumentFragment).replaceChildren = (Element.prototype as Element).replaceChildren;
660+
(DocumentFragment.prototype as DocumentFragment).replaceChildren = (
661+
Element.prototype as Element
662+
).replaceChildren;
664663
}
665664
if (!('replaceAll' in String.prototype)) {
666665
(String.prototype as any).replaceAll = function (str: string, newStr: string) {
@@ -676,19 +675,24 @@ export class Environment {
676675
/**
677676
* @target web
678677
*/
679-
public static get alphaTabWorker(): any { return this.globalThis.Worker }
678+
public static get alphaTabWorker(): any {
679+
return this.globalThis.Worker;
680+
}
680681

681682
/**
682683
* @target web
683684
*/
684685
public static initializeWorker() {
685686
if (!Environment.isRunningInWorker) {
686-
throw new AlphaTabError(AlphaTabErrorType.General, "Not running in worker, cannot run worker initialization");
687+
throw new AlphaTabError(
688+
AlphaTabErrorType.General,
689+
'Not running in worker, cannot run worker initialization'
690+
);
687691
}
688692
AlphaTabWebWorker.init();
689693
AlphaSynthWebWorker.init();
690694
Environment.createWebWorker = _ => {
691-
throw new AlphaTabError(AlphaTabErrorType.General, "Nested workers are not supported");
695+
throw new AlphaTabError(AlphaTabErrorType.General, 'Nested workers are not supported');
692696
};
693697
}
694698

@@ -697,7 +701,10 @@ export class Environment {
697701
*/
698702
public static initializeAudioWorklet() {
699703
if (!Environment.isRunningInAudioWorklet) {
700-
throw new AlphaTabError(AlphaTabErrorType.General, "Not running in audio worklet, cannot run worklet initialization");
704+
throw new AlphaTabError(
705+
AlphaTabErrorType.General,
706+
'Not running in audio worklet, cannot run worklet initialization'
707+
);
701708
}
702709
AlphaSynthWebWorklet.init();
703710
}
@@ -761,4 +768,4 @@ export class Environment {
761768

762769
return WebPlatform.Browser;
763770
}
764-
}
771+
}

src/RenderingResources.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,12 +86,26 @@ export class RenderingResources {
8686
public mainGlyphColor: Color = new Color(0, 0, 0, 0xff);
8787

8888
/**
89-
* Gets or sets the color to use for music notation elements of the secondary voices.
89+
* The color to use for music notation elements of the primary voice when they are highlighted during playback.
90+
91+
*/
92+
public mainGlyphHighlightColor: Color = new Color(0, 120, 255, 0xff);
93+
94+
/**
95+
* The color to use for music notation elements of the secondary voices.
96+
9097
*/
9198
public secondaryGlyphColor: Color = new Color(0, 0, 0, 100);
9299

93100
/**
94-
* Gets or sets the color to use for displaying the song information above the music sheet.
101+
* The color to use for music notation elements of the primary voice when they are highlighted during playback.
102+
103+
*/
104+
public secondaryGlyphHighlightColor: Color = new Color(0, 120, 255, 100);
105+
106+
/**
107+
* The color to use for displaying the song information above the music sheet.
108+
95109
*/
96110
public scoreInfoColor: Color = new Color(0, 0, 0, 0xff);
97111
}

src/generated/RenderingResourcesJson.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,19 @@ export interface RenderingResourcesJson {
7373
*/
7474
mainGlyphColor?: ColorJson;
7575
/**
76-
* Gets or sets the color to use for music notation elements of the secondary voices.
76+
* The color to use for music notation elements of the primary voice when they are highlighted during playback.
77+
*/
78+
mainGlyphHighlightColor?: ColorJson;
79+
/**
80+
* The color to use for music notation elements of the secondary voices.
7781
*/
7882
secondaryGlyphColor?: ColorJson;
7983
/**
80-
* Gets or sets the color to use for displaying the song information above the music sheet.
84+
* The color to use for music notation elements of the primary voice when they are highlighted during playback.
85+
*/
86+
secondaryGlyphHighlightColor?: ColorJson;
87+
/**
88+
* The color to use for displaying the song information above the music sheet.
8189
*/
8290
scoreInfoColor?: ColorJson;
8391
}

src/generated/RenderingResourcesSerializer.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ export class RenderingResourcesSerializer {
3434
o.set("fingeringfont", Font.toJson(obj.fingeringFont));
3535
o.set("markerfont", Font.toJson(obj.markerFont));
3636
o.set("mainglyphcolor", Color.toJson(obj.mainGlyphColor));
37+
o.set("mainglyphhighlightcolor", Color.toJson(obj.mainGlyphHighlightColor));
3738
o.set("secondaryglyphcolor", Color.toJson(obj.secondaryGlyphColor));
39+
o.set("secondaryglyphhighlightcolor", Color.toJson(obj.secondaryGlyphHighlightColor));
3840
o.set("scoreinfocolor", Color.toJson(obj.scoreInfoColor));
3941
return o;
4042
}
@@ -85,9 +87,15 @@ export class RenderingResourcesSerializer {
8587
case "mainglyphcolor":
8688
obj.mainGlyphColor = Color.fromJson(v)!;
8789
return true;
90+
case "mainglyphhighlightcolor":
91+
obj.mainGlyphHighlightColor = Color.fromJson(v)!;
92+
return true;
8893
case "secondaryglyphcolor":
8994
obj.secondaryGlyphColor = Color.fromJson(v)!;
9095
return true;
96+
case "secondaryglyphhighlightcolor":
97+
obj.secondaryGlyphHighlightColor = Color.fromJson(v)!;
98+
return true;
9199
case "scoreinfocolor":
92100
obj.scoreInfoColor = Color.fromJson(v)!;
93101
return true;

0 commit comments

Comments
 (0)