diff --git a/src/main.ts b/src/main.ts index f830e65..08bc30c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,128 +1,90 @@ -// src/main.ts - -// src/main.ts import { SceneGraph } from './scene-graph/scene-graph'; import { Rectangle } from './scene-graph/rectangle'; +import { Circle } from './scene-graph/circle'; import { WebGPURenderer } from './renderer/webgpu-renderer'; import { WebGPURenderStrategy } from './renderer/webgpu-render-strategy'; +import { CanvasRenderer } from './renderer/canvas-renderer'; +import { CanvasRenderStrategy } from './renderer/canvas-render-strategy'; -async function main() { - // Set up the canvas and WebGPURenderer +async function webGPURendering() { + // Set up the canvas const canvas = document.getElementById('myCanvas') as HTMLCanvasElement; + canvas.width = 800; + canvas.height = 600; + + // Create the WebGPU renderer const webgpuRenderer = new WebGPURenderer(canvas); + + // Initialize the WebGPU context and pipeline await webgpuRenderer.initialize(); + // Get the device and pipeline from the WebGPU renderer const device = webgpuRenderer.getDevice(); const pipeline = webgpuRenderer.getPipeline(); // Create the WebGPU render strategy - const webGPURenderStrategy = new WebGPURenderStrategy(device, pipeline); + const webgpuRenderStrategy = new WebGPURenderStrategy(device, pipeline, canvas); // Create the scene graph - const sceneGraph = new SceneGraph(webGPURenderStrategy); - - // Create a rectangle with the WebGPU render strategy + const sceneGraph = new SceneGraph(webgpuRenderStrategy); - const rect = new Rectangle(webGPURenderStrategy, 100, 50, 'red', 'black', 2); + // Create a dynamic rectangle + const rect = new Rectangle(webgpuRenderStrategy, 200, 100, { r: 0, g: 0, b: 1, a: 1 }, { r: 0, g: 0, b: 0, a: 1 }, 2); rect.x = 150; - rect.y = 100; + rect.y = 150; - // Add the rectangle to the scene graph - sceneGraph.root.addChild(rect); + // Create a dynamic circle + const circle = new Circle(webgpuRenderStrategy, 50, { r: 1, g: 0, b: 0, a: 1 }, { r: 0, g: 0, b: 0, a: 1 }, 2); + circle.x = 400; + circle.y = 300; + + // Add the shapes to the scene graph + sceneGraph.root.addChild(rect); + sceneGraph.root.addChild(circle); - // Start the rendering loop function renderLoop() { webgpuRenderer.render(sceneGraph); requestAnimationFrame(renderLoop); } + renderLoop(); } -main(); - -/* - -/////////////////////////////////////////////////////////////////////////// -CANVAS -/////////////////////////////////////////////////////////////////////////// - -import { SceneGraph } from './scene-graph/scene-graph'; -import { Rectangle } from './scene-graph/rectangle'; -import { CanvasRenderer } from './renderer/canvas-renderer'; -import { CanvasRenderStrategy } from './renderer/canvas-render-strategy'; - -// Set up the canvas -const canvas = document.getElementById('myCanvas') as HTMLCanvasElement; -canvas.width = 800; -canvas.height = 600; - -// Create the scene graph -const sceneGraph = new SceneGraph(); - -// Create the canvas render strategy -const canvasRenderStrategy = new CanvasRenderStrategy(); - -// Create a rectangle -const rect = new Rectangle(canvasRenderStrategy, 100, 50, 'red', 'black', 2); -rect.x = 150; -rect.y = 100; - -// Add a click event to change the color of the rectangle -rect.onClick = () => { - rect.fillColor = rect.fillColor === 'red' ? 'green' : 'red'; - console.log("Rectangle clicked! Color changed."); -}; - -// Add the rectangle to the scene graph -sceneGraph.root.addChild(rect); - -// Create the renderer -const renderer = new CanvasRenderer(canvas, sceneGraph); - -// Start the rendering loop manually -renderer.start(); - -///////////////////////////////////////////////////////////////////////////// -WEBGPU -///////////////////////////////////////////////////////////////////////////// - -// src/main.ts -import { SceneGraph } from './scene-graph/scene-graph'; -import { Rectangle } from './scene-graph/rectangle'; -import { WebGPURenderer } from './renderer/webgpu-renderer'; -import { WebGPURenderStrategy } from './rendering/webgpu-render-strategy'; - -async function main() { - // Set up the canvas and WebGPURenderer +async function canvasRendering() { + // Set up the canvas const canvas = document.getElementById('myCanvas') as HTMLCanvasElement; - const webgpuRenderer = new WebGPURenderer(canvas); - await webgpuRenderer.initialize(); + canvas.width = 800; + canvas.height = 600; - const device = webgpuRenderer.getDevice(); - const pipeline = webgpuRenderer.getPipeline(); + // Create the canvas render strategy + const canvasRenderStrategy = new CanvasRenderStrategy(); // Create the scene graph - const sceneGraph = new SceneGraph(); + const sceneGraph = new SceneGraph(canvasRenderStrategy); - // Create the WebGPU render strategy - const webGPURenderStrategy = new WebGPURenderStrategy(device, pipeline); + var red = {r:1,g:0,b:0,a:1}; + var black = {r:0,g:0,b:0,a:1}; - // Create a rectangle with the WebGPU render strategy - const rect = new Rectangle(webGPURenderStrategy, 100, 50, 'red', 'black', 2); + // Create a rectangle + const rect = new Rectangle(canvasRenderStrategy, 100, 50, red, black, 2); rect.x = 150; rect.y = 100; + // Add a click event to change the color of the rectangle + rect.onClick = () => { + rect.fillColor = rect.fillColor === red ? black : red; + console.log("Rectangle clicked! Color changed."); + }; + // Add the rectangle to the scene graph sceneGraph.root.addChild(rect); - // Start the rendering loop - function renderLoop() { - webgpuRenderer.render(sceneGraph); - requestAnimationFrame(renderLoop); - } - renderLoop(); -} + // Create the renderer + const renderer = new CanvasRenderer(canvas, sceneGraph); -main(); + // Start the rendering loop manually + renderer.start(); +} -*/ \ No newline at end of file +//canvasRendering(); +webGPURendering(); diff --git a/src/renderer/canvas-render-strategy.ts b/src/renderer/canvas-render-strategy.ts index 50d6de0..946f5e4 100644 --- a/src/renderer/canvas-render-strategy.ts +++ b/src/renderer/canvas-render-strategy.ts @@ -7,9 +7,10 @@ import { Rectangle } from '../scene-graph/rectangle'; import { Circle } from '../scene-graph/circle'; import { Line } from '../scene-graph/line'; import { Polygon } from '../scene-graph/polygon'; +import { rgbaToCssString } from '../utils/color'; export class CanvasRenderStrategy implements RenderStrategy { - render(node: Node, ctxOrEncoder: CanvasRenderingContext2D | GPURenderPassEncoder, pipeline?: GPURenderPipeline): void { + render(node: Node, ctxOrEncoder: CanvasRenderingContext2D | GPURenderPassEncoder): void { if (ctxOrEncoder instanceof CanvasRenderingContext2D) { if (!node.visible) return; @@ -41,28 +42,28 @@ export class CanvasRenderStrategy implements RenderStrategy { } private drawRectangle(rect: Rectangle, ctx: CanvasRenderingContext2D) { - if (rect.fillColor !== 'transparent') { - ctx.fillStyle = rect.fillColor; + if (rect.fillColor.a !== 0) { + ctx.fillStyle = rgbaToCssString(rect.fillColor); ctx.fillRect(0, 0, rect.boundingBox!.width, rect.boundingBox!.height); } if (rect.strokeWidth > 0) { - ctx.strokeStyle = rect.strokeColor; + ctx.strokeStyle = rgbaToCssString(rect.strokeColor); ctx.lineWidth = rect.strokeWidth; ctx.strokeRect(0, 0, rect.boundingBox!.width, rect.boundingBox!.height); } } private drawCircle(circle: Circle, ctx: CanvasRenderingContext2D) { - if (circle.fillColor !== 'transparent') { - ctx.fillStyle = circle.fillColor; + if (circle.fillColor.a !== 0) { + ctx.fillStyle = rgbaToCssString(circle.fillColor); ctx.beginPath(); ctx.arc(circle.boundingBox!.width / 2, circle.boundingBox!.height / 2, circle.boundingBox!.width / 2, 0, Math.PI * 2); ctx.fill(); } if (circle.strokeWidth > 0) { - ctx.strokeStyle = circle.strokeColor; + ctx.strokeStyle = rgbaToCssString(circle.strokeColor); ctx.lineWidth = circle.strokeWidth; ctx.stroke(); } @@ -72,7 +73,7 @@ export class CanvasRenderStrategy implements RenderStrategy { ctx.beginPath(); ctx.moveTo(line.startX, line.startY); ctx.lineTo(line.endX, line.endY); - ctx.strokeStyle = line.strokeColor; + ctx.strokeStyle = rgbaToCssString(line.strokeColor); ctx.lineWidth = line.strokeWidth; ctx.stroke(); } @@ -89,13 +90,13 @@ export class CanvasRenderStrategy implements RenderStrategy { ctx.closePath(); - if (polygon.fillColor !== 'transparent') { - ctx.fillStyle = polygon.fillColor; + if (polygon.fillColor.a !== 0) { + ctx.fillStyle = rgbaToCssString(polygon.fillColor); ctx.fill(); } if (polygon.strokeWidth > 0) { - ctx.strokeStyle = polygon.strokeColor; + ctx.strokeStyle = rgbaToCssString(polygon.strokeColor); ctx.lineWidth = polygon.strokeWidth; ctx.stroke(); } diff --git a/src/renderer/webgpu-render-strategy.ts b/src/renderer/webgpu-render-strategy.ts index f7fb880..aa47c91 100644 --- a/src/renderer/webgpu-render-strategy.ts +++ b/src/renderer/webgpu-render-strategy.ts @@ -1,7 +1,6 @@ // src/rendering/webgpu-render-strategy.ts import { RenderStrategy } from './render-strategy'; import { Node } from '../scene-graph/node'; -import { WebGPUShape } from '../scene-graph/webgpu-shape'; import { Rectangle } from '../scene-graph/rectangle'; import { Circle } from '../scene-graph/circle'; @@ -9,19 +8,22 @@ export class WebGPURenderStrategy implements RenderStrategy { private device: GPUDevice; private pipeline: GPURenderPipeline; + private canvas: HTMLCanvasElement; - constructor(device: GPUDevice, pipeline: GPURenderPipeline) { + constructor(device: GPUDevice, pipeline: GPURenderPipeline, canvas: HTMLCanvasElement) { this.device = device; this.pipeline = pipeline; + this.canvas = canvas; } - render(node: Node, ctxOrEncoder: CanvasRenderingContext2D | GPURenderPassEncoder, pipeline?: GPURenderPipeline): void { + render(node: Node, ctxOrEncoder: CanvasRenderingContext2D | GPURenderPassEncoder): void { if (!(ctxOrEncoder instanceof GPURenderPassEncoder)) { return; // Only handle GPURenderPassEncoder in this strategy } - const passEncoder = ctxOrEncoder; - if (!node.visible) return; + if (!node.visible) return; + + const passEncoder = ctxOrEncoder; // Ensure we're dealing with a specific shape and render it using WebGPU if (node instanceof Rectangle) { @@ -35,31 +37,126 @@ export class WebGPURenderStrategy implements RenderStrategy { } } - private drawRectangle(passEncoder: GPURenderPassEncoder, rectangle: Rectangle) { - passEncoder.setPipeline(this.pipeline); + private drawRectangle(passEncoder: GPURenderPassEncoder, rect: Rectangle) { + // Create and update uniform buffers for both vertex and fragment shaders - // Create vertices based on the rectangle's properties (example placeholder logic) - const vertices = new Float32Array([ - rectangle.x, rectangle.y, - rectangle.x + rectangle.boundingBox!.width, rectangle.y, - rectangle.x + rectangle.boundingBox!.width, rectangle.y + rectangle.boundingBox!.height, - rectangle.x, rectangle.y + rectangle.boundingBox!.height, + // vertex uniform data values MUST normalized to [-1,1] using canvas dimensions + let normalizedX = (rect.x / this.canvas.width) * 2 - 1; + let normalizedY = (rect.y / this.canvas.height) * 2 - 1; + let normalizedWidth = (rect.width / this.canvas.width) * 2; + let normalizedHeight = (rect.height / this.canvas.height) * 2; + + const vertexUniformData = new Float32Array([ + normalizedX, normalizedY, // Position + normalizedWidth, normalizedHeight, // Size ]); + const vertexUniformBuffer = this.createUniformBuffer(vertexUniformData); + const fragmentUniformData = new Float32Array([ + rect.fillColor.r, rect.fillColor.g, rect.fillColor.b, rect.fillColor.a, // Color + ]); + const fragmentUniformBuffer = this.createUniformBuffer(fragmentUniformData); + + const bindGroup = this.createBindGroup(vertexUniformBuffer, fragmentUniformBuffer); + + // Set up the vertex buffer (for the rectangle geometry) + const vertices = new Float32Array([ + 0.0, 0.0, // Bottom-left + 1.0, 0.0, // Bottom-right + 0.0, 1.0, // Top-left + 1.0, 1.0, // Top-right + ]); const vertexBuffer = this.device.createBuffer({ size: vertices.byteLength, usage: GPUBufferUsage.VERTEX, mappedAtCreation: true, }); - new Float32Array(vertexBuffer.getMappedRange()).set(vertices); vertexBuffer.unmap(); + // Render the rectangle + passEncoder.setPipeline(this.pipeline); + passEncoder.setBindGroup(0, bindGroup); passEncoder.setVertexBuffer(0, vertexBuffer); passEncoder.draw(4, 1, 0, 0); } + private createUniformBuffer(data: Float32Array): GPUBuffer { + const uniformBuffer = this.device.createBuffer({ + size: data.byteLength, + usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST, + }); + + this.device.queue.writeBuffer(uniformBuffer, 0, data.buffer); + + return uniformBuffer; + } + + private createBindGroup(vertexUniformBuffer: GPUBuffer, fragmentUniformBuffer: GPUBuffer): GPUBindGroup { + return this.device.createBindGroup({ + layout: this.pipeline.getBindGroupLayout(0), + entries: [ + { binding: 0, resource: { buffer: vertexUniformBuffer }}, // Vertex shader buffer + { binding: 1, resource: { buffer: fragmentUniformBuffer }}, // Fragment shader buffer + ], + }); + } + private drawCircle(passEncoder: GPURenderPassEncoder, circle: Circle) { - // Implement WebGPU-specific rendering logic for a circle + + // Normalize the position and size (radius) + const normalizedX = (circle.x / this.canvas.width) * 2 - 1; + const normalizedY = (circle.y / this.canvas.height) * 2 - 1; + const normalizedRadiusX = (circle.radius / this.canvas.width) * 2; + const normalizedRadiusY = (circle.radius / this.canvas.height) * 2; + + // Create and update uniform buffers for both vertex and fragment shaders + const vertexUniformData = new Float32Array([ + normalizedX, normalizedY, // Normalized Position (translation) + normalizedRadiusX, normalizedRadiusY // Normalized Radius (scaling factors) + ]); + const vertexUniformBuffer = this.createUniformBuffer(vertexUniformData); + + const fragmentUniformData = new Float32Array([ + circle.fillColor.r, circle.fillColor.g, circle.fillColor.b, circle.fillColor.a, // Color + ]); + const fragmentUniformBuffer = this.createUniformBuffer(fragmentUniformData); + + const bindGroup = this.createBindGroup(vertexUniformBuffer, fragmentUniformBuffer); + + // Circle drawing logic using triangle-list + const numSegments = 30; // Increase number of segments for smoother circle + const angleStep = (Math.PI * 2) / numSegments; + + const vertices: number[] = []; + + // Create the circle vertices using a triangle list approach + for (let i = 0; i < numSegments; i++) { + // Center of the circle for the current triangle + vertices.push(0.0, 0.0); // center vertex + + // First perimeter point of the triangle + const angle1 = i * angleStep; + vertices.push(Math.cos(angle1), Math.sin(angle1)); + + // Second perimeter point of the triangle (next segment) + const angle2 = (i + 1) * angleStep; + vertices.push(Math.cos(angle2), Math.sin(angle2)); + } + + const vertexBuffer = this.device.createBuffer({ + size: vertices.length * 4, // 4 bytes per float + usage: GPUBufferUsage.VERTEX, + mappedAtCreation: true, + }); + + new Float32Array(vertexBuffer.getMappedRange()).set(vertices); + vertexBuffer.unmap(); + + // Render the circle using triangle-list + passEncoder.setPipeline(this.pipeline); + passEncoder.setBindGroup(0, bindGroup); + passEncoder.setVertexBuffer(0, vertexBuffer); + passEncoder.draw(vertices.length / 2, 1, 0, 0); } } \ No newline at end of file diff --git a/src/renderer/webgpu-renderer.ts b/src/renderer/webgpu-renderer.ts index d9a8a08..b755c2d 100644 --- a/src/renderer/webgpu-renderer.ts +++ b/src/renderer/webgpu-renderer.ts @@ -61,23 +61,23 @@ export class WebGPURenderer { // Vertex Shader const vertexShaderCode = ` + @group(0) @binding(0) var vertexUniforms: vec4; + @vertex - fn main_vertex(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4 { - var positions = array, 3>( - vec2(0.0, 0.5), - vec2(-0.5, -0.5), - vec2(0.5, -0.5) - ); - let position = positions[vertexIndex]; - return vec4(position, 0.0, 1.0); + fn main_vertex(@location(0) position: vec2) -> @builtin(position) vec4 { + let scaledPosition = position * vertexUniforms.zw; // Scale using width and height + let pos = scaledPosition + vertexUniforms.xy; // Translate using x and y + return vec4(pos, 0.0, 1.0); } `; // Fragment Shader const fragmentShaderCode = ` + @group(0) @binding(1) var fragmentUniforms: vec4; + @fragment fn main_fragment() -> @location(0) vec4 { - return vec4(1.0, 0.0, 0.0, 1.0); // Red color + return fragmentUniforms; // Use the color defined in the fragment uniform } `; @@ -90,10 +90,45 @@ export class WebGPURenderer { code: fragmentShaderCode, }); + // Define the vertex buffer layout + const vertexBufferLayout: GPUVertexBufferLayout = { + arrayStride: 2 * 4, // 2 floats, each 4 bytes + attributes: [ + { + shaderLocation: 0, // Corresponds to the attribute location in the shader + offset: 0, + format: 'float32x2', + }, + ], + }; + + // Define the bind group layout + const bindGroupLayout = this.device.createBindGroupLayout({ + entries: [ + { + binding: 0, + visibility: GPUShaderStage.VERTEX, + buffer: { type: 'uniform' } + }, + { + binding: 1, + visibility: GPUShaderStage.FRAGMENT, + buffer: { type: 'uniform' } + } + ] + }); + + // Create the pipeline layout using the bind group layout + const pipelineLayout = this.device.createPipelineLayout({ + bindGroupLayouts: [bindGroupLayout] + }); + this.pipeline = this.device.createRenderPipeline({ + layout: pipelineLayout, vertex: { module: vertexShaderModule, entryPoint: 'main_vertex', + buffers: [vertexBufferLayout], }, fragment: { module: fragmentShaderModule, @@ -103,7 +138,6 @@ export class WebGPURenderer { }], }, primitive: { topology: 'triangle-list' }, - layout: 'auto', }); } diff --git a/src/scene-graph/canvas-shape.ts b/src/scene-graph/canvas-shape.ts index e8ec78e..0d15a8a 100644 --- a/src/scene-graph/canvas-shape.ts +++ b/src/scene-graph/canvas-shape.ts @@ -1,11 +1,12 @@ // src/scene-graph/canvas-shape.ts import { RenderStrategy } from '../renderer/render-strategy'; +import { RGBA } from '../types/rgba'; import { Shape } from './shape'; export class CanvasShape extends Shape { private ctx: CanvasRenderingContext2D; - constructor(renderStrategy: RenderStrategy, ctx: CanvasRenderingContext2D, fillColor: string = 'transparent', strokeColor: string = 'black', strokeWidth: number = 1) { + constructor(renderStrategy: RenderStrategy, ctx: CanvasRenderingContext2D, fillColor: RGBA = {r:0,g:0,b:0,a:0}, strokeColor: RGBA = {r:0,g:0,b:0,a:1}, strokeWidth: number = 1) { super(renderStrategy, fillColor, strokeColor, strokeWidth); this.ctx = ctx; } diff --git a/src/scene-graph/circle.ts b/src/scene-graph/circle.ts index 88e7cc5..4f5f63d 100644 --- a/src/scene-graph/circle.ts +++ b/src/scene-graph/circle.ts @@ -2,14 +2,25 @@ // Represents a circle with a specific radius, fill color, and stroke. import { RenderStrategy } from '../renderer/render-strategy'; +import { RGBA } from '../types/rgba'; import { Shape } from './shape'; export class Circle extends Shape { - private radius: number; + private _radius: number; - constructor(renderStrategy: RenderStrategy, radius: number, fillColor: string = 'transparent', strokeColor: string = 'black', strokeWidth: number = 1) { + constructor(renderStrategy: RenderStrategy, radius: number, fillColor: RGBA = {r:0,g:0,b:0,a:0}, strokeColor: RGBA = {r:0,g:0,b:0,a:1}, strokeWidth: number = 1) { super(renderStrategy, fillColor, strokeColor, strokeWidth); - this.radius = radius; + this._radius = radius; + this.calculateBoundingBox(); + } + + get radius() { + return this._radius; + } + + set radius(radius: number) { + this._radius = radius; + this.triggerRerender(); } containsPoint(x: number, y: number): boolean { @@ -17,11 +28,12 @@ export class Circle extends Shape { } protected calculateBoundingBox() { + const diameter = this.radius * 2; this._boundingBox = { x: this.x - this.radius, y: this.y - this.radius, - width: this.radius * 2, - height: this.radius * 2, + width: diameter, + height: diameter, }; } } \ No newline at end of file diff --git a/src/scene-graph/line.ts b/src/scene-graph/line.ts index 7734193..21d1389 100644 --- a/src/scene-graph/line.ts +++ b/src/scene-graph/line.ts @@ -2,6 +2,7 @@ // Represents a line between two points. import { RenderStrategy } from '../renderer/render-strategy'; +import { RGBA } from '../types/rgba'; import { Shape } from './shape'; export class Line extends Shape { @@ -10,8 +11,8 @@ export class Line extends Shape { private _endX: number; private _endY: number; - constructor(renderStrategy: RenderStrategy, startX: number, startY: number, endX: number, endY: number, strokeColor: string = 'black', strokeWidth: number = 1) { - super(renderStrategy, 'transparent', strokeColor, strokeWidth); + constructor(renderStrategy: RenderStrategy, startX: number, startY: number, endX: number, endY: number, strokeColor: RGBA = {r:0,g:0,b:0,a:1}, strokeWidth: number = 1) { + super(renderStrategy, {r:0,g:0,b:0,a:0}, strokeColor, strokeWidth); this._startX = startX; this._startY = startY; this._endX = endX; @@ -50,7 +51,7 @@ export class Line extends Shape { const maxX = Math.max(this._startX, this._endX); const maxY = Math.max(this._startY, this._endY); - this.boundingBox = { + this._boundingBox = { x: this.x + minX, y: this.y + minY, width: maxX - minX, diff --git a/src/scene-graph/polygon.ts b/src/scene-graph/polygon.ts index 97d44c9..e58c0b2 100644 --- a/src/scene-graph/polygon.ts +++ b/src/scene-graph/polygon.ts @@ -2,12 +2,13 @@ // Represents a polygon defined by a series of points. import { RenderStrategy } from '../renderer/render-strategy'; +import { RGBA } from '../types/rgba'; import { Shape } from './shape'; export class Polygon extends Shape { private _points: { x: number; y: number }[]; - constructor(renderStrategy: RenderStrategy, points: { x: number; y: number }[], fillColor: string = 'transparent', strokeColor: string = 'black', strokeWidth: number = 1) { + constructor(renderStrategy: RenderStrategy, points: { x: number; y: number }[], fillColor: RGBA = {r:0,g:0,b:0,a:0}, strokeColor: RGBA = {r:0,g:0,b:0,a:1}, strokeWidth: number = 1) { super(renderStrategy, fillColor, strokeColor, strokeWidth); this._points = points; } diff --git a/src/scene-graph/rectangle.ts b/src/scene-graph/rectangle.ts index 12d4932..7034d81 100644 --- a/src/scene-graph/rectangle.ts +++ b/src/scene-graph/rectangle.ts @@ -2,29 +2,38 @@ // Represents a rectangle with a specific width, height, fill color, and stroke. import { RenderStrategy } from '../renderer/render-strategy'; +import { RGBA } from '../types/rgba'; import { Shape } from './shape'; export class Rectangle extends Shape { - private width: number; - private height: number; + private _width: number; + private _height: number; - constructor(renderStrategy: RenderStrategy, width: number, height: number, fillColor: string = 'transparent', strokeColor: string = 'black', strokeWidth: number = 1) { + constructor(renderStrategy: RenderStrategy, width: number, height: number, fillColor: RGBA = {r:0,g:0,b:0,a:0}, strokeColor: RGBA = {r:0,g:0,b:0,a:1}, strokeWidth: number = 1) { super(renderStrategy, fillColor, strokeColor, strokeWidth); - this.width = width; - this.height = height; + this._width = width; + this._height = height; this.calculateBoundingBox(); // Calculate initial bounding box } + get width() { + return this._width; + } + + get height() { + return this._height; + } + containsPoint(x: number, y: number): boolean { - return x >= this.x && y >= this.y && x <= this.x + this.width && y <= this.y + this.height; + return x >= this.x && y >= this.y && x <= this.x + this._width && y <= this.y + this._height; } protected calculateBoundingBox() { - this.boundingBox = { + this._boundingBox = { x: this.x - this._strokeWidth / 2, y: this.y - this._strokeWidth / 2, - width: this.width + this._strokeWidth, - height: this.height + this._strokeWidth, + width: this._width + this._strokeWidth, + height: this._height + this._strokeWidth, }; } diff --git a/src/scene-graph/shape.ts b/src/scene-graph/shape.ts index da6822d..debd7ea 100644 --- a/src/scene-graph/shape.ts +++ b/src/scene-graph/shape.ts @@ -1,16 +1,17 @@ // src/scene-graph/shape.ts import { RenderStrategy } from '../renderer/render-strategy'; +import { RGBA } from '../types/rgba'; import { Node } from './node'; export abstract class Shape extends Node { - protected _fillColor: string; - protected _strokeColor: string; + protected _fillColor: RGBA; + protected _strokeColor: RGBA; protected _strokeWidth: number; protected _isDirty: boolean = true; protected _boundingBox: { x: number; y: number; width: number; height: number } | null = null; protected _previousBoundingBox: { x: number; y: number; width: number; height: number } | null = null; - constructor(renderStrategy: RenderStrategy, fillColor: string = 'transparent', strokeColor: string = 'black', strokeWidth: number = 1) { + constructor(renderStrategy: RenderStrategy, fillColor: RGBA = {r: 0, g: 0, b: 0, a: 0}, strokeColor: RGBA = {r: 0, g: 0, b: 0, a: 0}, strokeWidth: number = 1) { super(renderStrategy); this._fillColor = fillColor; this._strokeColor = strokeColor; @@ -23,7 +24,7 @@ export abstract class Shape extends Node { return this._fillColor; } - set fillColor(value: string) { + set fillColor(value: RGBA) { this._fillColor = value; this.triggerRerender(); } @@ -32,7 +33,7 @@ export abstract class Shape extends Node { return this._strokeColor; } - set strokeColor(value: string) { + set strokeColor(value: RGBA) { this._strokeColor = value; this.triggerRerender(); } @@ -70,8 +71,8 @@ export abstract class Shape extends Node { this._boundingBox = { x: this.x - this._strokeWidth / 2, y: this.y - this._strokeWidth / 2, - width: 0, - height: 0, + width: this._strokeWidth, // placeholder; actual dimensions should be set by subclasses + height: this._strokeWidth, // placeholder; actual dimensions should be set by subclasses }; } diff --git a/src/scene-graph/text.ts b/src/scene-graph/text.ts index 948be18..101538c 100644 --- a/src/scene-graph/text.ts +++ b/src/scene-graph/text.ts @@ -1,5 +1,7 @@ // src/scene-graph/text.ts import { RenderStrategy } from '../renderer/render-strategy'; +import { RGBA } from '../types/rgba'; +import { rgbaToCssString } from '../utils/color'; import { Shape } from './shape'; export class Text extends Shape { @@ -12,11 +14,11 @@ export class Text extends Shape { renderStrategy: RenderStrategy, text: string, font: string = '16px Arial', - color: string = 'black', + color: RGBA = {r: 0, g: 0, b: 0, a: 1}, textAlign: CanvasTextAlign = 'left', textBaseline: CanvasTextBaseline = 'alphabetic' ) { - super(renderStrategy, color, 'transparent'); + super(renderStrategy, color, {r: 0, g: 0, b: 0, a: 0}); this.text = text; this.font = font; this.textAlign = textAlign; @@ -25,7 +27,7 @@ export class Text extends Shape { draw(ctx: CanvasRenderingContext2D) { ctx.font = this.font; - ctx.fillStyle = this._fillColor; + ctx.fillStyle = rgbaToCssString(this._fillColor); ctx.textAlign = this.textAlign; ctx.textBaseline = this.textBaseline; ctx.fillText(this.text, 0, 0); diff --git a/src/types/rgba.ts b/src/types/rgba.ts new file mode 100644 index 0000000..bfd64fe --- /dev/null +++ b/src/types/rgba.ts @@ -0,0 +1,6 @@ +export interface RGBA { + r: number; + g: number; + b: number; + a: number; +} \ No newline at end of file diff --git a/src/utils/color.ts b/src/utils/color.ts new file mode 100644 index 0000000..23e6d18 --- /dev/null +++ b/src/utils/color.ts @@ -0,0 +1,6 @@ +import { RGBA } from "../types/rgba"; + +export function rgbaToCssString(rgba: RGBA): string { + const { r, g, b, a } = rgba; + return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a})`; +} \ No newline at end of file