generated from gxc-solutions/gxc-template-repo
This commit is contained in:
parent
0e7cb83ff0
commit
47e9cf85b8
6 changed files with 204 additions and 2 deletions
2
.npmrc
2
.npmrc
|
|
@ -1 +1 @@
|
||||||
regestry=https://npm.gxc-solutions.ru/
|
@gxc-solutions:registry=https://npm.gxc-solutions.ru/
|
||||||
93
lib/src/canvas-2d-renderer.ts
Normal file
93
lib/src/canvas-2d-renderer.ts
Normal file
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { degToRad } from "@gxc-solutions/math/functions";
|
||||||
|
import { IDrawObject, IRenderer, IScene } from "@gxc-solutions/renderer-base/interfaces";
|
||||||
|
import { SequentialDrawThread } from "./draw-thread";
|
||||||
|
|
||||||
|
const drawRotatedRect = (
|
||||||
|
context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
angle: number,
|
||||||
|
color: string,
|
||||||
|
) => {
|
||||||
|
context.save();
|
||||||
|
// переносим систему координат в центр прямоугольника
|
||||||
|
context.translate(x + width / 2, y + height / 2);
|
||||||
|
// поворот вокруг центра
|
||||||
|
context.rotate(degToRad(angle));
|
||||||
|
// рисуем прямоугольник так, чтобы его центр оказался в (0,0)
|
||||||
|
context.fillStyle = color;
|
||||||
|
context.fillRect(-width / 2, -height / 2, width, height);
|
||||||
|
|
||||||
|
context.restore();
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawRotatedStrokeRect = (
|
||||||
|
context: CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D,
|
||||||
|
x: number,
|
||||||
|
y: number,
|
||||||
|
width: number,
|
||||||
|
height: number,
|
||||||
|
angle: number,
|
||||||
|
color: string,
|
||||||
|
) => {
|
||||||
|
context.save(); // сохраняем систему координат
|
||||||
|
context.translate(x + width / 2, y + height / 2); // переносим начало в центр фигуры
|
||||||
|
context.rotate(degToRad(angle)); // вращаем систему координат
|
||||||
|
context.strokeStyle = color;
|
||||||
|
context.lineWidth = 2;
|
||||||
|
context.strokeRect(-width / 2, -height / 2, width, height);
|
||||||
|
context.restore(); // возвращаем всё обратно
|
||||||
|
};
|
||||||
|
|
||||||
|
export class Canvas2DRenderer implements IRenderer {
|
||||||
|
public readonly type = "2d";
|
||||||
|
|
||||||
|
private _context: CanvasRenderingContext2D;
|
||||||
|
private _thread: SequentialDrawThread<(offscreenCanvas: OffscreenCanvasRenderingContext2D, objects: IDrawObject[]) => void>;
|
||||||
|
|
||||||
|
get holder() {
|
||||||
|
return this._canvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private _canvas: HTMLCanvasElement) {
|
||||||
|
this._context = this._canvas.getContext("2d");
|
||||||
|
|
||||||
|
this._thread = new SequentialDrawThread(
|
||||||
|
(context2d, objects: IDrawObject[]) => {
|
||||||
|
const canvas = context2d.canvas;
|
||||||
|
context2d.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
objects.forEach((drawObject) => {
|
||||||
|
if (drawObject.type === "rectangle-object") {
|
||||||
|
drawRotatedRect(
|
||||||
|
context2d,
|
||||||
|
drawObject.x,
|
||||||
|
drawObject.y,
|
||||||
|
drawObject.width,
|
||||||
|
drawObject.height,
|
||||||
|
drawObject.angle,
|
||||||
|
drawObject.color, // TODO в worker не знает про toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
this._canvas.width,
|
||||||
|
this._canvas.height,
|
||||||
|
[drawRotatedRect, drawRotatedStrokeRect, degToRad],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async render(scene: IScene): Promise<void> {
|
||||||
|
const bitmap = await this._thread.run([scene.objects]);
|
||||||
|
|
||||||
|
const prev = this._context.globalCompositeOperation;
|
||||||
|
this._context.globalCompositeOperation = "copy";
|
||||||
|
// Важно: убедись, что размеры совпадают
|
||||||
|
this._context.drawImage(bitmap, 0, 0, this._canvas.width, this._canvas.height);
|
||||||
|
this._context.globalCompositeOperation = prev;
|
||||||
|
|
||||||
|
// Освобождаем ресурсы
|
||||||
|
if ("close" in bitmap) bitmap.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
87
lib/src/draw-thread.ts
Normal file
87
lib/src/draw-thread.ts
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
type DrawFn = (ctx: OffscreenCanvasRenderingContext2D, ...args: any[]) => void;
|
||||||
|
|
||||||
|
export class SequentialDrawThread<F extends DrawFn> {
|
||||||
|
private _worker: Worker;
|
||||||
|
private _idCounter = 0;
|
||||||
|
private _callbacks = new Map<number, { resolve: (value: any) => void; reject: (err: any) => void }>();
|
||||||
|
|
||||||
|
constructor(fn: F, width = 800, height = 600, scope?: Array<(...args: any[]) => any>) {
|
||||||
|
const source = `
|
||||||
|
${this._createScope(scope)}
|
||||||
|
|
||||||
|
const canvas = new OffscreenCanvas(${width}, ${height});
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
const fn = ${fn.toString()};
|
||||||
|
let busy = false;
|
||||||
|
let pending = null;
|
||||||
|
|
||||||
|
const processTask = async (id, args) => {
|
||||||
|
busy = true;
|
||||||
|
try {
|
||||||
|
await fn(ctx, ...args);
|
||||||
|
const bitmap = canvas.transferToImageBitmap();
|
||||||
|
self.postMessage({ id, result: bitmap }, [bitmap]);
|
||||||
|
} catch (err) {
|
||||||
|
self.postMessage({ id, error: err.message || err.toString() });
|
||||||
|
} finally {
|
||||||
|
busy = false;
|
||||||
|
if (pending) {
|
||||||
|
const next = pending;
|
||||||
|
pending = null;
|
||||||
|
processTask(next.id, next.args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onmessage = (event) => {
|
||||||
|
const { id, args } = event.data;
|
||||||
|
if (!busy) {
|
||||||
|
processTask(id, args);
|
||||||
|
} else {
|
||||||
|
pending = { id, args };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const blob = new Blob([source], { type: "application/javascript" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
this._worker = new Worker(url);
|
||||||
|
|
||||||
|
this._worker.onmessage = (event: MessageEvent) => {
|
||||||
|
const { id, result, error } = event.data;
|
||||||
|
const callback = this._callbacks.get(id);
|
||||||
|
if (!callback) return;
|
||||||
|
|
||||||
|
if (error) callback.reject(new Error(error));
|
||||||
|
else callback.resolve(result);
|
||||||
|
|
||||||
|
this._callbacks.delete(id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public run(args: Parameters<F> extends [any, ...infer Rest] ? Rest : never): Promise<ImageBitmap> {
|
||||||
|
const id = this._idCounter++;
|
||||||
|
return new Promise<ImageBitmap>((resolve, reject) => {
|
||||||
|
this._callbacks.set(id, { resolve, reject });
|
||||||
|
this._worker.postMessage({ id, args });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public terminate() {
|
||||||
|
this._worker.terminate();
|
||||||
|
this._callbacks.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _createScope(scope?: Array<(...args: any[]) => any>) {
|
||||||
|
if (!scope) return "";
|
||||||
|
return scope
|
||||||
|
.map((fn) => {
|
||||||
|
if (!fn.name) {
|
||||||
|
throw new Error("Все функции в scope должны иметь имя");
|
||||||
|
}
|
||||||
|
return `const ${fn.name} = ${fn.toString()};`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,14 @@
|
||||||
{
|
{
|
||||||
"name": "@gxc-solutions/lib",
|
"name": "@gxc-solutions/renderer-canvas-2d",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"author": "GXC Solutions",
|
"author": "GXC Solutions",
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public",
|
"access": "public",
|
||||||
"registry": "https://npm.gxc-solutions.ru"
|
"registry": "https://npm.gxc-solutions.ru"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@gxc-solutions/math": "^0.0.1",
|
||||||
|
"@gxc-solutions/renderer-base": "^0.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
14
package-lock.json
generated
14
package-lock.json
generated
|
|
@ -8,6 +8,10 @@
|
||||||
"name": "template-of-lib-repo",
|
"name": "template-of-lib-repo",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@gxc-solutions/math": "^0.0.1",
|
||||||
|
"@gxc-solutions/renderer-base": "^0.0.3"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.3.4",
|
"@eslint/eslintrc": "^3.3.4",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
|
|
@ -247,6 +251,16 @@
|
||||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@gxc-solutions/math": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://npm.gxc-solutions.ru/@gxc-solutions/math/-/math-0.0.1.tgz",
|
||||||
|
"integrity": "sha512-m6lxTkjXkyaUoI3+cJKpgt/AQlyApLSJ2p9D2EJ9+XzHCSjOWs29GzczPLgopYdLEWYN/YPr77V4CScGpR7qxw=="
|
||||||
|
},
|
||||||
|
"node_modules/@gxc-solutions/renderer-base": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://npm.gxc-solutions.ru/@gxc-solutions/renderer-base/-/renderer-base-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-rNglFmWe3LNbTn/KV8s7waV5IPgZ7T+NUbNSChiK+TdGK8bAfuE5yyAeJ7pmvpYbAaRC4emmgnpDQlRr0Kx3nA=="
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -25,5 +25,9 @@
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"rimraf": "^6.0.1",
|
"rimraf": "^6.0.1",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@gxc-solutions/math": "^0.0.1",
|
||||||
|
"@gxc-solutions/renderer-base": "^0.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue