Skip to content

Commit

Permalink
avoid slow string concatenation when creating large SVG (mandelbrot e…
Browse files Browse the repository at this point in the history
…xample)
  • Loading branch information
benchmarko committed Feb 2, 2025
1 parent 6b6eb4f commit 9fbd2b5
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 37 deletions.
28 changes: 28 additions & 0 deletions dist/examples/examples.js
Original file line number Diff line number Diff line change
Expand Up @@ -1993,6 +1993,34 @@ DATA 0,4,4,4,4,0,4,4,4,4,4
'
`);

cpcBasic.addItem("", `
REM mandelbro - Mandelbrot Set
REM https://rosettacode.org/wiki/Mandelbrot_set#Locomotive_Basic
REM https://en.wikipedia.org/wiki/Mandelbrot_set
MODE 3 ' Note the CPCBasic-only screen mode!
t=TIME
FOR xp = 0 TO 639
FOR yp = 0 TO 399
x = 0 : y = 0
x0 = xp / 213 - 2 : y0 = yp / 200 - 1
iteration = 0
maxIteration = 100
WHILE (x * x + y * y <= (2 * 2) AND iteration < maxIteration)
xtemp = x * x - y * y + x0
y = 2 * x * y + y0
x = xtemp
iteration = iteration + 1
WEND
IF iteration <> maxIteration THEN c = iteration ELSE c = 0
PLOT xp, yp, c MOD 16
NEXT
NEXT
t=TIME-t
FRAME
PRINT ROUND(t*10/3,3)
'
`);

cpcBasic.addItem("", `
REM nicholas - House of St. Nicholas
REM with Characters using Bresenham's Line Algorithm
Expand Down
2 changes: 1 addition & 1 deletion dist/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="locobasic.css">
<title>LocoBasic v0.1.32</title>
<title>LocoBasic v0.1.33</title>
</head>

<body>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "locobasic",
"version": "0.1.32",
"version": "0.1.33",
"description": "# LocoBasic - Loco BASIC",
"type": "commonjs",
"scripts": {
Expand Down
90 changes: 55 additions & 35 deletions src/Core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,17 @@ const colorsForPens: string[] = [
"#000080" // 1 Navy (repeated)
];

const strokeWidthForMode: number[] = [4, 2, 1, 1];

const vm = {
_output: "",
_lastPaper: -1,
_lastPen: -1,
_mode: 2,
_paperColors: [] as string[],
_penColors: [] as string[],
_graphicsBuffer: "",
_graphicsBuffer: [] as string[],
_graphicsPathBuffer: [] as string[],
_graphicsPen: 1,
_graphicsX: 0,
_graphicsY: 0,
Expand All @@ -45,7 +48,8 @@ const vm = {
vm._output = "";
vm._lastPaper = -1;
vm._lastPen = -1;
vm._graphicsBuffer = "";
vm._graphicsBuffer.length = 0;
vm._graphicsPathBuffer.length = 0;
vm._graphicsPen = -1;
vm._graphicsX = 0;
vm._graphicsY = 0;
Expand All @@ -54,55 +58,60 @@ const vm = {
drawMovePlot: (type: string, x: number, y: number) => {
x = Math.round(x);
y = Math.round(y);
if (!vm._graphicsBuffer) {
vm._graphicsBuffer = `<path d="`;
}

if (vm._graphicsBuffer.endsWith('d="')) {
// avoid 'Error: <path> attribute d: Expected moveto path command ('M' or 'm')'
if (type !== "M") {
vm._graphicsBuffer += `M${vm._graphicsX} ${vm._graphicsY}`;
}
}

let svg = "";
let svgPathCmd = "";
switch (type) {
case "L":
case "M":
y = 399 - y;
svg = `${type}${x} ${y}`;
svgPathCmd = `${type}${x} ${y}`;
break;
case "P":
y = 399 - y;
svg = `M${x - 1} ${y + 1}h1v1h-1v-1`;
svgPathCmd = `M${x} ${y}h1v1h-1v-1`;

//or circle: vm.flushGraphicsPath(); svgPathCmd = ""; vm._graphicsBuffer.push(`<circle cx="${x}" cy="${y}" r="${strokeWidthForMode[vm._mode]}" stroke="${colorsForPens[vm._graphicsPen]}" />`);
//or rect (slow): vm.flushGraphicsPath(); svgPathCmd = ""; vm._graphicsBuffer.push(`<rect x="${x}" y="${y}" width="${strokeWidthForMode[vm._mode]}" height="${strokeWidthForMode[vm._mode]}" fill="${colorsForPens[vm._graphicsPen]}" />`);
break;
case "l":
case "m":
y = -y;
svg = `${type}${x} ${y}`;
svgPathCmd = `${type}${x} ${y}`;
x = vm._graphicsX + x;
y = vm._graphicsY + y;
break;
case "p":
y = -y;
svg = `m${x - 1} ${y + 1}h1v1h-1v-1`;
svgPathCmd = `m${x} ${y}h1v1h-1v-1`;
x = vm._graphicsX + x;
y = vm._graphicsY + y;
break;
default:
console.error(`drawMovePlot: Unknown type: ${type}`);
break;
}
vm._graphicsBuffer += svg;

if (svgPathCmd) {
if (!vm._graphicsPathBuffer.length && svgPathCmd[0] !== "M") {
// avoid 'Error: <path> attribute d: Expected moveto path command ('M' or 'm')'
vm._graphicsPathBuffer.push(`M${vm._graphicsX} ${vm._graphicsY}`);
}
vm._graphicsPathBuffer.push(svgPathCmd);
}
vm._graphicsX = x;
vm._graphicsY = y;
},
flush: () => {
if (vm._graphicsBuffer) {
//vm._output += `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 640 400" transform="scale(1, -1) translate(0, -400)" stroke-width="1px" stroke="currentColor">${vm._drawBuffer}" /> </svg>`;
const strokeWidth = vm._mode >= 2 ? "1px" : vm._mode === 1 ? "2px" : "4px";
vm._output += `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 640 400" stroke-width="${strokeWidth}" stroke="currentColor">${vm._graphicsBuffer}" /> </svg>`;
vm._graphicsBuffer = "";
flushGraphicsPath: () => {
if (vm._graphicsPathBuffer.length) {
vm._graphicsBuffer.push(`<path stroke="${colorsForPens[vm._graphicsPen]}" d="${vm._graphicsPathBuffer.join("")}" />`);
vm._graphicsPathBuffer.length = 0;
}
},
flush: () =>{
vm.flushGraphicsPath();
if (vm._graphicsBuffer.length) {
vm._output += `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 640 400" stroke-width="${strokeWidthForMode[vm._mode]}px" stroke="currentColor">${vm._graphicsBuffer.join("\n")}" /> </svg>\n`;
vm._graphicsBuffer.length = 0;
}
if (vm._output) {
vm._fnOnPrint(vm._output);
Expand All @@ -113,12 +122,8 @@ const vm = {
if (num === vm._graphicsPen) {
return;
}
vm.flushGraphicsPath();
vm._graphicsPen = num;

if (vm._graphicsBuffer) {
vm._graphicsBuffer += `" />`; // close the path
}
vm._graphicsBuffer += `<path stroke="${colorsForPens[num]}" d="`;
},
mode: (num: number) => {
vm._mode = num;
Expand Down Expand Up @@ -153,6 +158,25 @@ const vm = {
setPenColors: (penColors: string[]) => vm._penColors = penColors
};

interface DummyVm extends IVm {
_output: string;
debug(...args: (string | number)[]): void;
}

// The functions from dummyVm will be stringified in the putScriptInFrame function
const dummyVm: DummyVm = {
_output: "",
debug(..._args: (string | number)[]) { /* console.debug(...args); */ }, // eslint-disable-line @typescript-eslint/no-unused-vars
cls() {},
drawMovePlot(type: string, x: number, y: number) { this.debug("drawMovePlot:", type, x, y); },
flush() { if (this._output) { console.log(this._output); this._output = ""; } },
graphicsPen(num: number) { this.debug("graphicsPen:", num); },
mode(num: number) { this.debug("mode:", num); },
paper(num: number) { this.debug("paper:", num); },
pen(num: number) { this.debug("pen:", num); },
print(...args: (string | number)[]) { this._output += args.join(''); },
prompt(msg: string) { console.log(msg); return ""; }
};

export class Core implements ICore {
private readonly startConfig: ConfigType = {
Expand Down Expand Up @@ -283,17 +307,13 @@ export class Core implements ICore {
}

public putScriptInFrame(script: string) {
const dummyFunctions = Object.values(dummyVm).filter((value) => value).map((value) => `${value}`).join(",\n ");
const result =
`(function(_o) {
${script}
})({
_output: "",
cls: () => undefined,
flush() { if (this._output) { console.log(this._output); this._output = ""; } },
paper: () => undefined,
pen: () => undefined,
print(...args) { this._output += args.join(''); },
prompt: (msg) => { console.log(msg); return ""; }
${dummyFunctions}
});`
return result;
}
Expand Down

0 comments on commit 9fbd2b5

Please sign in to comment.