diff --git a/lib/src/artist.ts b/lib/src/artist.ts new file mode 100644 index 0000000..341b29f --- /dev/null +++ b/lib/src/artist.ts @@ -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. + } +} diff --git a/lib/src/canvas-2d-renderer.ts b/lib/src/canvas-2d-renderer.ts index 71aa944..ad84df8 100644 --- a/lib/src/canvas-2d-renderer.ts +++ b/lib/src/canvas-2d-renderer.ts @@ -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 { - 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(); + } } } diff --git a/lib/src/package.json b/lib/src/package.json index 5cafae1..cb844d6 100644 --- a/lib/src/package.json +++ b/lib/src/package.json @@ -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": { diff --git a/package-lock.json b/package-lock.json index f50081f..6d5acc2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,16 +1,16 @@ { - "name": "template-of-lib-repo", + "name": "renderer-canvas-2d", "version": "0.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "template-of-lib-repo", + "name": "renderer-canvas-2d", "version": "0.0.0", "license": "ISC", "dependencies": { "@gxc-solutions/math": "^0.0.1", - "@gxc-solutions/renderer-base": "^0.0.3" + "@gxc-solutions/renderer-base": "^0.0.9" }, "devDependencies": { "@eslint/eslintrc": "^3.3.4", @@ -701,9 +701,12 @@ "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==" + "version": "0.0.9", + "resolved": "https://npm.gxc-solutions.ru/@gxc-solutions/renderer-base/-/renderer-base-0.0.9.tgz", + "integrity": "sha512-aLu3eTyzDcGQw+FsYt25MNg/6gNORXa8FynUX5rtEFinmdFEDdqxOr08dhFViw1FIT3ZdYtJbY9aegFK3V/OcA==", + "peerDependencies": { + "@gxc-solutions/math": "^0.0.1" + } }, "node_modules/@humanfs/core": { "version": "0.19.1", diff --git a/package.json b/package.json index 47ca163..1e6222b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "template-of-lib-repo", + "name": "renderer-canvas-2d", "version": "0.0.0", "main": "index.js", "scripts": { @@ -32,6 +32,6 @@ }, "dependencies": { "@gxc-solutions/math": "^0.0.1", - "@gxc-solutions/renderer-base": "^0.0.3" + "@gxc-solutions/renderer-base": "^0.0.9" } } diff --git a/playground/index.html b/playground/index.html index 14185cd..5b68b26 100644 --- a/playground/index.html +++ b/playground/index.html @@ -6,7 +6,8 @@ Playground -

Hello world!

+

2D Canvas Renderer Playground

+ diff --git a/playground/index.ts b/playground/index.ts index 5dab6a8..8d3359b 100644 --- a/playground/index.ts +++ b/playground/index.ts @@ -1 +1,281 @@ -console.warn("Hello world!"); +import { Canvas2DRenderer } from "./renderer-canvas-2d/canvas-2d-renderer"; +const canvas = document.getElementById("canvas") as HTMLCanvasElement; +const renderer = new Canvas2DRenderer(canvas); + +const image = new Image(100, 100); +image.src = "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSX7vk1hi2Qnqbwbkz4t9x-akEeuuHA78H9PA&s"; +await new Promise((resolve) => image.addEventListener("load", () => resolve())); + +await renderer.render({ + background: { + type: "solid", + color: { + r: 200, + g: 255, + b: 255, + a: 1, + }, + }, + node: { + id: "a", + type: "node", + children: [ + { + type: "rectangle", + blendMode: "source-over", + fill: { + type: "solid", + color: { + r: 100, + g: 100, + b: 100, + a: 1, + }, + }, + id: "a", + name: "test 2", + opacity: 1, + rectangle: { + width: 100, + height: 100, + x: 0, + y: 0, + }, + stroke: { + width: 2, + color: { + r: 0, + g: 0, + b: 0, + a: 1, + }, + dash: [], + }, + transform: { + rotate: 30, + scaleX: 1, + scaleY: 1, + skewX: 0, + skewY: 0, + translateX: 0, + translateY: 0, + }, + }, + { + type: "ellipse", + blendMode: "source-over", + ellipse: { + center: { x: 300, y: 300 }, + radiusX: 30, + radiusY: 30, + }, + fill: { + type: "solid", + color: { + r: 150, + g: 0, + b: 100, + a: 1, + }, + }, + id: "a", + name: "test ellipse", + opacity: 0.5, + stroke: { + width: 5, + color: { + r: 0, + g: 255, + b: 0, + a: 1, + }, + dash: [], + }, + transform: { + scaleX: 1, + scaleY: 1, + translateX: 0, + translateY: 0, + rotate: 0, + skewX: 0, + skewY: 0, + }, + }, + { + type: "text", + blendMode: "source-over", + text: "Hello world", + font: { + font: "serif", + color: { + r: 255, + g: 0, + b: 0, + a: 1, + }, + size: 72, + }, + textStroke: { + color: { + r: 100, + g: 200, + b: 0, + a: 1, + }, + dash: [], + width: 0, + }, + rectangle: { + x: 150, + y: 150, + width: 100, + height: 100, + }, + fill: { + type: "solid", + color: { + r: 150, + g: 0, + b: 100, + a: 1, + }, + }, + id: "a", + name: "test ellipse", + opacity: 0.5, + stroke: { + width: 5, + color: { + r: 0, + g: 255, + b: 0, + a: 1, + }, + dash: [], + }, + transform: { + scaleX: 1, + scaleY: 1, + translateX: 0, + translateY: 0, + rotate: 30, + skewX: 0, + skewY: 0, + }, + }, + { + type: "image", + blendMode: "source-over", + source: image, + fill: { + type: "solid", + color: { + r: 100, + g: 100, + b: 100, + a: 1, + }, + }, + id: "a", + name: "test 2", + opacity: 1, + rectangle: { + width: 200, + height: 200, + x: 300, + y: 300, + }, + stroke: { + width: 2, + color: { + r: 0, + g: 0, + b: 0, + a: 1, + }, + dash: [], + }, + transform: { + rotate: -15, + scaleX: 1, + scaleY: 1, + skewX: 0, + skewY: 0, + translateX: 0, + translateY: 0, + }, + }, + ], + name: "test", + transform: { + rotate: 0, + scaleX: 1, + scaleY: 1, + skewX: 0, + skewY: 0, + translateX: 0, + translateY: 0, + }, + }, + selection: { + id: "", + type: "node", + name: "", + children: [], + transform: { + rotate: 0, + scaleX: 1, + scaleY: 1, + skewX: 0, + skewY: 0, + translateX: 0, + translateY: 0, + }, + }, + spotlight: [], + static: [ + { + type: "rectangle", + blendMode: "source-over", + fill: { + type: "solid", + color: { + r: 255, + g: 255, + b: 255, + a: 0, + }, + }, + id: "", + name: "", + opacity: 1, + rectangle: { + width: 500, + height: 500, + x: 0, + y: 0, + }, + stroke: { + width: 2, + color: { + r: 255, + g: 0, + b: 0, + a: 1, + }, + dash: [], + }, + transform: { + rotate: 0, + scaleX: 1, + scaleY: 1, + skewX: 0, + skewY: 0, + translateX: 0, + translateY: 0, + }, + }, + ], +}); + +console.warn(renderer);