Release package version 0.0.2
Some checks failed
CI / build (push) Failing after 16s

Added base rendering
This commit is contained in:
Andrey Kernichniy 2026-03-07 23:02:31 +07:00
parent 63818eb2b8
commit a100988848
7 changed files with 587 additions and 85 deletions

178
lib/src/artist.ts Normal file
View file

@ -0,0 +1,178 @@
import { degToRad } from "@gxc-solutions/math/functions";
import { IPoint, ISize } from "@gxc-solutions/math/interfaces";
import { IFont, IStroke, ITransform } from "@gxc-solutions/renderer-base";
import {
IBaseFill,
ISolidFill,
IConicGradient,
ILinerGradient,
IRadialGradient,
ITextureFill,
} from "@gxc-solutions/renderer-base/interfaces/fill";
import {
isTextureFill,
isConicGradientFill,
isLinerGradientFill,
isRadialGradientFill,
isSolidFill,
} from "@gxc-solutions/renderer-base/utils/fill";
export class Artist {
constructor(private _context: CanvasRenderingContext2D) {}
setFont(font: IFont) {
const { color, font: fontFamily, size } = font;
this._context.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b}, ${color.a})`;
this._context.font = `${size}px ${fontFamily}`;
this._context.textAlign = "left";
this._context.textBaseline = "middle";
this._context.direction = "ltr";
this._context.fontKerning = "auto";
this._context.fontStretch = "normal";
this._context.fontVariantCaps = "normal";
// this._context.letterSpacing
this._context.textRendering = "auto";
//this._context.wordSpacing
}
setStroke(stroke: IStroke) {
const { color, dash, width } = stroke;
this._context.lineWidth = width;
this._context.strokeStyle = this._context.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b}, ${color.a})`;
this._context.setLineDash(dash);
}
setTransform(transform: ITransform) {
this._context.translate(transform.translateX, transform.translateY);
this._context.scale(transform.scaleX, transform.scaleY);
this._context.rotate(degToRad(transform.rotate));
}
setFill(fill: IBaseFill) {
if (isSolidFill(fill)) {
this._setSolidFill(fill);
}
if (isRadialGradientFill(fill)) {
this._setRadialGradientFill(fill);
}
if (isLinerGradientFill(fill)) {
this._setLinerGradientFill(fill);
}
if (isConicGradientFill(fill)) {
this._setConicGradientFill(fill);
}
if (isTextureFill(fill)) {
this._setTextureFill(fill);
}
}
private _setSolidFill(fill: ISolidFill) {
const { color } = fill;
this._context.fillStyle = `rgb(${color.r}, ${color.g}, ${color.b}, ${color.a})`;
}
private _setRadialGradientFill(fill: IRadialGradient) {
console.warn(`${fill.type} not implemented!`);
}
private _setLinerGradientFill(fill: ILinerGradient) {
console.warn(`${fill.type} not implemented!`);
}
private _setConicGradientFill(fill: IConicGradient) {
console.warn(`${fill.type} not implemented!`);
}
private _setTextureFill(fill: ITextureFill) {
console.warn(`${fill.type} not implemented!`);
}
drawRotatedStrokeRect(position: IPoint, size: ISize, angle: number, color: string) {
this._context.save(); // сохраняем систему координат
this._context.translate(position.x + size.width / 2, position.y + size.height / 2); // переносим начало в центр фигуры
this._context.rotate(degToRad(angle)); // вращаем систему координат
this._context.strokeStyle = color;
this._context.lineWidth = 2;
this._context.strokeRect(-size.width / 2, -size.height / 2, size.width, size.height);
this._context.restore(); // возвращаем всё обратно
}
drawRect(position: IPoint, size: ISize, color: string) {
this._context.strokeStyle = color;
this._context.lineWidth = 2;
this._context.strokeRect(size.width, size.height, size.width, size.height);
this._context.restore(); // возвращаем всё обратно
}
drawRotatedRect(position: IPoint, size: ISize, angle: number, color: string) {
this._context.save();
// переносим систему координат в центр прямоугольника
this._context.translate(position.x + size.width / 2, position.y + size.height / 2);
// поворот вокруг центра
this._context.rotate(degToRad(angle));
// рисуем прямоугольник так, чтобы его центр оказался в (0,0)
this._context.fillStyle = color;
this._context.fillRect(-size.width / 2, -size.height / 2, size.width, size.height);
this._context.restore();
}
drawEllipse(position: IPoint, size: ISize, angle: number, color: string, segment = 360) {
this._context.save();
this._context.fillStyle = color;
this._context.beginPath();
this._context.ellipse(position.x, position.y, size.width / 2, size.height / 2, degToRad(angle), 0, degToRad(segment));
this._context.fill();
this._context.restore();
}
drawLine(start: IPoint, end: IPoint, width: number, color: string) {
this._context.save();
this._context.beginPath();
this._context.lineWidth = width;
this._context.strokeStyle = color;
this._context.moveTo(start.x, start.y);
this._context.lineTo(end.x, end.y);
this._context.stroke();
this._context.restore();
}
drawText(text: string, position: IPoint, font: string, size: number, angle: number, color: string) {
this._context.save();
this._context.translate(position.x, position.y);
this._context.rotate(degToRad(angle));
this._context.fillStyle = color;
this._context.font = `${size}px ${font}`;
// TODO
this._context.textAlign = "left";
// TODO
this._context.textBaseline = "middle";
this._context.direction = "ltr";
console.warn(this._context.measureText(text));
this._context.fillText(text, position.x, position.y);
this._context.restore();
}
drawImage(source: HTMLImageElement, position: IPoint, size: ISize) {
this._context.save();
const scaleY = size.height / source.naturalHeight;
const scaleX = size.width / source.naturalWidth;
this._context.translate(position.x + size.width / 2, position.y + size.height / 2);
this._context.scale(scaleY, scaleX);
this._context.drawImage(source, -source.naturalWidth / 2, -source.naturalHeight / 2);
this._context.restore();
}
drawPath(path: string) {
const path2d = new Path2D(path);
this._context.stroke(path2d);
}
draw() {
// this._context.
}
}

View file

@ -1,51 +1,29 @@
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(); // возвращаем всё обратно
};
import {
DrawLeafs,
IBaseFill,
IDrawNode,
IEllipseDrawObject,
IImageDrawObject,
IRectangleDrawObject,
IRenderer,
IScene,
ITextDrawObject,
} from "@gxc-solutions/renderer-base/interfaces";
import {
isDrawNode,
isEllipseDrawObject,
isImageDrawObject,
isRectangleDrawObject,
isTextDrawObject,
} from "@gxc-solutions/renderer-base/utils/draw-object";
import { Artist } from "./artist";
import { degToRad } from "@gxc-solutions/math";
export class Canvas2DRenderer implements IRenderer {
public readonly type = "2d";
private _context: CanvasRenderingContext2D;
private _thread: SequentialDrawThread<(offscreenCanvas: OffscreenCanvasRenderingContext2D, objects: IDrawObject[]) => void>;
private _artist: Artist;
get holder() {
return this._canvas;
@ -53,41 +31,103 @@ export class Canvas2DRenderer implements IRenderer {
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],
);
this._artist = new Artist(this._context);
}
public async render(scene: IScene): Promise<void> {
const bitmap = await this._thread.run([scene.objects]);
this._context.clearRect(0, 0, this._canvas.width, this._canvas.height);
this.renderBg(scene.background);
scene.static.forEach((leaf) => this.drawLeaf(leaf));
this.renderNode(scene.node);
this.renderNode(scene.selection);
}
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;
renderBg(fill: IBaseFill) {
this._context.save();
this._artist.setFill(fill);
this._context.fillRect(0, 0, this._canvas.width, this._canvas.height);
this._context.restore();
}
// Освобождаем ресурсы
if ("close" in bitmap) bitmap.close();
renderNode(drawNode: IDrawNode) {
drawNode.children.forEach((leaf) => {
if (isDrawNode(leaf)) {
this._artist.setTransform(leaf.transform);
this._context.save();
this.renderNode(leaf);
this._context.restore();
}
this.drawLeaf(leaf as DrawLeafs);
});
}
private drawLeaf(leaf: DrawLeafs) {
if (isEllipseDrawObject(leaf)) {
this._context.save();
const dn = leaf as IEllipseDrawObject;
this._context.globalAlpha = dn.opacity;
this._context.globalCompositeOperation = dn.blendMode as GlobalCompositeOperation;
this._artist.setTransform(dn.transform);
this._artist.setFill(dn.fill);
this._context.ellipse(
dn.ellipse.center.x - dn.ellipse.radiusX,
dn.ellipse.center.y - dn.ellipse.radiusY,
dn.ellipse.radiusX,
dn.ellipse.radiusY,
degToRad(0),
0,
degToRad(360),
);
this._context.fill();
this._artist.setStroke(dn.stroke);
if (dn.stroke.width > 0) {
this._context.stroke();
}
this._context.restore();
}
if (isImageDrawObject(leaf)) {
this._context.save();
const dn = leaf as IImageDrawObject;
this._context.globalAlpha = dn.opacity;
this._context.globalCompositeOperation = dn.blendMode as GlobalCompositeOperation;
this._artist.setTransform(dn.transform);
this._artist.drawImage(
dn.source,
{ x: dn.rectangle.x, y: dn.rectangle.y },
{ width: dn.rectangle.width, height: dn.rectangle.height },
);
this._context.restore();
}
if (isRectangleDrawObject(leaf)) {
this._context.save();
const dn = leaf as IRectangleDrawObject;
this._context.globalAlpha = dn.opacity;
this._context.globalCompositeOperation = dn.blendMode as GlobalCompositeOperation;
this._artist.setTransform(dn.transform);
this._artist.setFill(dn.fill);
this._context.fillRect(dn.rectangle.x, dn.rectangle.y, dn.rectangle.width, dn.rectangle.height);
this._artist.setStroke(dn.stroke);
if (dn.stroke.width > 0) {
this._context.strokeRect(dn.rectangle.x, dn.rectangle.y, dn.rectangle.width, dn.rectangle.height);
}
this._context.restore();
}
if (isTextDrawObject(leaf)) {
this._context.save();
const dn = leaf as ITextDrawObject;
this._context.globalAlpha = dn.opacity;
this._context.globalCompositeOperation = dn.blendMode as GlobalCompositeOperation;
this._artist.setTransform(dn.transform);
this._artist.setStroke(dn.textStroke);
this._artist.setFont(dn.font);
const metrics = this._context.measureText(dn.text);
console.warn(metrics);
if (dn.textStroke.width > 0) {
this._context.strokeText(dn.text, dn.rectangle.x, dn.rectangle.y);
}
this._context.fillText(dn.text, dn.rectangle.x, dn.rectangle.y);
this._context.restore();
}
}
}

View file

@ -1,6 +1,6 @@
{
"name": "@gxc-solutions/renderer-canvas-2d",
"version": "0.0.1",
"version": "0.0.2",
"main": "index.js",
"author": "GXC Solutions",
"publishConfig": {