Skip to content
Merged
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
33 changes: 33 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 13 additions & 10 deletions packages/alphatab/src/platform/svg/CssFontSvgCanvas.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { TextAlign } from '@coderline/alphatab/platform/ICanvas';
import { SvgCanvas } from '@coderline/alphatab/platform/svg/SvgCanvas';
import { MusicFontSymbol } from '@coderline/alphatab/model/MusicFontSymbol';
import { SvgCanvas } from '@coderline/alphatab/platform/svg/SvgCanvas';

/**
* This SVG canvas renders the music symbols by adding a CSS class 'at' to all elements.
Expand Down Expand Up @@ -43,22 +42,26 @@ export class CssFontSvgCanvas extends SvgCanvas {
symbols: string,
centerAtPosition?: boolean
): void {
x *= this.scale;
y *= this.scale;

this.buffer += `<g transform="translate(${x} ${y})" class="at" ><text`;
const scale = this.scale * relativeScale;
const s = this.scale;
if (s === 1) {
this.buffer += '<g transform="translate(' + x + ' ' + y + ')" class="at" ><text';
} else {
const sx = x * s;
const sy = y * s;
this.buffer += `<g transform="translate(${sx} ${sy})" class="at" ><text`;
}
const scale = s * relativeScale;
if (scale !== 1) {
this.buffer += ` style="font-size: ${scale * 100}%; stroke:none"`;
} else {
this.buffer += ' style="stroke:none"';
}
if (this.color.rgba !== '#000000') {
this.buffer += ` fill="${this.color.rgba}"`;
this.buffer += ' fill="' + this.color.rgba + '"';
}
if (centerAtPosition) {
this.buffer += ` text-anchor="${this.getSvgTextAlignment(TextAlign.Center)}"`;
this.buffer += ' text-anchor="middle"';
}
this.buffer += `>${symbols}</text></g>`;
this.buffer += '>' + symbols + '</text></g>';
}
}
78 changes: 63 additions & 15 deletions packages/alphatab/src/platform/svg/SvgCanvas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export abstract class SvgCanvas implements ICanvas {
}

public beginGroup(identifier: string): void {
this.buffer += `<g class="${identifier}">`;
this.buffer += '<g class="' + identifier + '">';
}

public endGroup(): void {
Expand All @@ -51,9 +51,24 @@ export abstract class SvgCanvas implements ICanvas {

public fillRect(x: number, y: number, w: number, h: number): void {
if (w > 0) {
this.buffer += `<rect x="${x * this.scale}" y="${y * this.scale}" width="${
w * this.scale
}" height="${h * this.scale}" fill="${this.color.rgba}" />\n`;
// scale=1 fast path: skip the 4 multiplies and use `+` concat to avoid template-literal overhead.
const s = this.scale;
if (s === 1) {
this.buffer +=
'<rect x="' +
x +
'" y="' +
y +
'" width="' +
w +
'" height="' +
h +
'" fill="' +
this.color.rgba +
'" />\n';
} else {
this.buffer += `<rect x="${x * s}" y="${y * s}" width="${w * s}" height="${h * s}" fill="${this.color.rgba}" />\n`;
}
}
}

Expand All @@ -77,12 +92,22 @@ export abstract class SvgCanvas implements ICanvas {
}

public moveTo(x: number, y: number): void {
this._currentPath += ` M${x * this.scale},${y * this.scale}`;
const s = this.scale;
if (s === 1) {
this._currentPath += ' M' + x + ',' + y;
} else {
this._currentPath += ` M${x * s},${y * s}`;
}
}

public lineTo(x: number, y: number): void {
this._currentPathIsEmpty = false;
this._currentPath += ` L${x * this.scale},${y * this.scale}`;
const s = this.scale;
if (s === 1) {
this._currentPath += ' L' + x + ',' + y;
} else {
this._currentPath += ` L${x * s},${y * s}`;
}
}

public quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void {
Expand All @@ -92,9 +117,13 @@ export abstract class SvgCanvas implements ICanvas {

public bezierCurveTo(cp1X: number, cp1Y: number, cp2X: number, cp2Y: number, x: number, y: number): void {
this._currentPathIsEmpty = false;
this._currentPath += ` C${cp1X * this.scale},${cp1Y * this.scale},${cp2X * this.scale},${cp2Y * this.scale},${
x * this.scale
},${y * this.scale}`;
const s = this.scale;
if (s === 1) {
this._currentPath +=
' C' + cp1X + ',' + cp1Y + ',' + cp2X + ',' + cp2Y + ',' + x + ',' + y;
} else {
this._currentPath += ` C${cp1X * s},${cp1Y * s},${cp2X * s},${cp2Y * s},${x * s},${y * s}`;
}
}

public fillCircle(x: number, y: number, radius: number): void {
Expand Down Expand Up @@ -125,9 +154,9 @@ export abstract class SvgCanvas implements ICanvas {

public fill(): void {
if (!this._currentPathIsEmpty) {
this.buffer += `<path d="${this._currentPath}"`;
this.buffer += '<path d="' + this._currentPath + '"';
if (this.color.rgba !== '#000000') {
this.buffer += ` fill="${this.color.rgba}"`;
this.buffer += ' fill="' + this.color.rgba + '"';
}
this.buffer += ' style="stroke: none"/>';
}
Expand All @@ -137,7 +166,7 @@ export abstract class SvgCanvas implements ICanvas {

public stroke(): void {
if (!this._currentPathIsEmpty) {
let s: string = `<path d="${this._currentPath}" stroke="${this.color.rgba}"`;
let s: string = '<path d="' + this._currentPath + '" stroke="' + this.color.rgba + '"';
if (this.lineWidth !== 1 || this.scale !== 1) {
s += ` stroke-width="${this.lineWidth * this.scale}"`;
}
Expand All @@ -152,9 +181,22 @@ export abstract class SvgCanvas implements ICanvas {
if (text === '') {
return;
}
let s: string = `<text x="${x * this.scale}" y="${
y * this.scale
}" style='stroke: none; font:${this.font.toCssString(this.settings.display.scale)}; ${this.getSvgBaseLine()}'`;
const sc = this.scale;
let s: string;
if (sc === 1) {
s =
'<text x="' +
x +
'" y="' +
y +
'" style=\'stroke: none; font:' +
this.font.toCssString(this.settings.display.scale) +
'; ' +
this.getSvgBaseLine() +
"'";
} else {
s = `<text x="${x * sc}" y="${y * sc}" style='stroke: none; font:${this.font.toCssString(this.settings.display.scale)}; ${this.getSvgBaseLine()}'`;
}
if (this.color.rgba !== '#000000') {
s += ` fill="${this.color.rgba}"`;
}
Expand All @@ -165,7 +207,13 @@ export abstract class SvgCanvas implements ICanvas {
this.buffer += s;
}

private static readonly _escapeTextRegex = /[&<>"']/;

private static _escapeText(text: string) {
// Short-circuit: most rendered text (bar numbers, fret numbers, etc.) has no escapable chars.
if (!SvgCanvas._escapeTextRegex.test(text)) {
return text;
}
return text
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
Expand Down
96 changes: 96 additions & 0 deletions packages/alphatab/src/profiling/Profiler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* Profiling instrumentation. Call sites are stripped from production /
* library / vitest / playground builds by `stripProfilingPlugin` and only
* retained in the bench harness.
*/

/**
* @internal
* @record
*/
export interface StageStats {
count: number;
totalNs: number;
maxNs: number;
}

/**
* @internal
* @record
*/
export interface ProfilerSnapshot {
stages: Map<string, StageStats>;
counters: Map<string, number>;
}

/**
* @internal
* @record
*/
interface ProfilerFrame {
name: string;
startNs: number;
}

/**
* @internal
*/
export class Profiler {
private static readonly _stackLimit = 64;
private static readonly _stages = new Map<string, StageStats>();
private static readonly _counters = new Map<string, number>();
private static readonly _stack: ProfilerFrame[] = [];

static begin(name: string): void {
if (Profiler._stack.length >= Profiler._stackLimit) {
throw new Error(`Profiler stack overflow on '${name}'`);
}
Profiler._stack.push({ name, startNs: Profiler._nowNs() });
}

static end(name: string): void {
const top = Profiler._stack.pop();
if (!top || top.name !== name) {
throw new Error(`Profiler.end('${name}') mismatched; expected '${top?.name ?? '<empty>'}'`);
}
const elapsed = Profiler._nowNs() - top.startNs;
if (!Profiler._stages.has(name)) {
Profiler._stages.set(name, { count: 1, totalNs: elapsed, maxNs: elapsed });
} else {
const stats = Profiler._stages.get(name)!;
stats.count++;
stats.totalNs += elapsed;
if (elapsed > stats.maxNs) {
stats.maxNs = elapsed;
}
}
}

static bump(name: string, delta: number = 1): void {
const current = Profiler._counters.has(name) ? Profiler._counters.get(name)! : 0;
Profiler._counters.set(name, current + delta);
}

static snapshot(): ProfilerSnapshot {
const stages = new Map<string, StageStats>();
for (const [name, stats] of Profiler._stages) {
stages.set(name, { count: stats.count, totalNs: stats.totalNs, maxNs: stats.maxNs });
}
const counters = new Map<string, number>();
for (const [name, value] of Profiler._counters) {
counters.set(name, value);
}
return { stages, counters };
}

static reset(): void {
Profiler._stages.clear();
Profiler._counters.clear();
// splice(0) instead of `.length = 0` for transpiler compatibility.
Profiler._stack.splice(0);
}

private static _nowNs(): number {
return Math.round(performance.now() * 1_000_000);
}
}
Loading
Loading