export class Renderer { constructor(canvas, game) { this.game = game; game.renderer = this; this.canvas = canvas; this.ctx = this.canvas.getContext('2d'); this.width = canvas.width; this.height = canvas.height; this.anchorOption = { hor: 'left', ver: 'top' }; this.virtualWidth = 100; this.virtualHeight = 100; } setSize(width, height) { this.width = width; this.height = height; this.canvas.width = width; this.canvas.height = height; } draw() { this.ctx.clearRect(0, 0, this.width, this.height); this.game.draw(this); window.requestAnimationFrame(() => this.draw()); } begin() { const drawing = new Drawing(this); drawing.anchor(this.anchorOption.hor, this.anchorOption.ver); return drawing; } push() { this.ctx.save(); } pop() { this.ctx.restore(); } anchor(hor, ver) { let x = 0; let y = 0; switch (hor) { case 'center': x = this.width / 2; break; case 'right': x = this.width; break; } switch (ver) { case 'center': y = this.height / 2; break; case 'bottom': y = this.height; } this.translate(x, y); this.anchorOption = { hor, ver }; } rotate(degrees) { this.ctx.rotate(degrees * Math.PI / 180); } translate(x, y) { this.ctx.translate(x, y); } scale(x, y) { this.ctx.scale(x, y); } } class Drawing { constructor(renderer) { this.renderer = renderer; this.ctx = renderer.ctx; this.ctx.save(); this.anchorPoint = { x: 0, y: 0, }; this.offset = { x: 0, y: 0, }; this.translation = { x: 0, y: 0, }; this.rotation = 0; this.fillColor = undefined; this.strokeColor = undefined; this.lineWidth = 1; this.lineCap = 'butt'; this.lineJoin = 'miter'; this.type = undefined; this.args = {}; this.textOptions = { font: 'sans-serif', fontSize: 16, alignment: 'left', baseline: 'alphabetic', }; } /* Shapes */ arc(x, y, radius, startAngle, endAngle) { this.type = 'arc'; this.args = { x, y, radius, startAngle, endAngle }; return this; } circle(x, y, radius) { this.type = 'circle'; this.args = { x, y, radius }; return this; } rect(x, y, width, height) { this.type = 'rect'; this.args = { x, y, width, height }; return this; } roundedRect(x, y, width, height, roundness) { this.type = 'roundedRect'; this.args = { x, y, width, height, roundness }; return this; } triangle(x, y, width, height) { this.type = 'triangle'; this.args = { x1: x, y1: y, x2: x + width, y2: y, x3: x + width / 2, y3: y + height }; return this; } /* Path */ vertex(x, y) { this.type = 'line'; if (!this.args.vertices) { this.args.vertices = []; } this.args.vertices.push([x, y]); return this; } finish() { this.args.close = true; return this; } /* Text */ text(string, x = 0, y = 0) { this.type = 'text'; this.args = { x, y, string }; return this; } /** * Set text alignment * @param {'start'|'end'|'left'|'center'|'right'} alignment * @returns {this} */ align(alignment) { this.textOptions.alignment = alignment; return this; } /** * Sets text baseline * @param {'alphabetic'|'top'|'hanging'|'middle'|'ideographic'|'bottom'} base * @return {Drawing} */ baseline(base) { this.textOptions.baseline = base; return this; } fontSize(fontSize) { this.textOptions.fontSize = fontSize; return this; } /* Transformations */ anchor(hor, ver) { switch (hor) { case 'center': this.anchorPoint.x = this.renderer.width / 2; this.offset.x = -0.5; break; case 'right': this.anchorPoint.x = this.renderer.width; this.offset.x = -1; break; default: this.anchorPoint.x = 0; this.offset.x = 0; break; } switch (ver) { case 'center': this.anchorPoint.y = this.renderer.height / 2; this.offset.y = -0.5; break; case 'bottom': this.anchorPoint.y = this.renderer.height; this.offset.y = -1; break; default: this.anchorPoint.y = 0; this.offset.y = 0; break; } return this; } rotate(amount, mode = 'DEGREES') { if (mode === 'DEGREES') { amount *= 180; } this.rotation += amount * Math.PI; return this; } translate(x, y) { this.translation.x += x; this.translation.y += y; return this; } /** * Translate to center of rectangle * @param {number} x x position * @param {number} y y position * @param {number} width width * @param {number} height height * @returns {this} */ center(x, y, width, height) { this.translation.x += x + this.offset.x * width + width / 2; this.translation.y += y + this.offset.y * height + height / 2; return this; } /* Colors */ fill(...color) { this.fillColor = parseColor('rgb', ...color); return this; } stroke(...color) { this.strokeColor = parseColor('rgb', ...color); return this; } /** * Sets line width for strokes * @param {number} width */ strokeWidth(width) { this.lineWidth = width; return this; } /** * Sets line cap for strokes * @param {'butt'|'round'|'square'} style */ strokeCap(style) { this.lineCap = style; return this; } calcCoords(x, y, width, height) { return [ x + this.offset.x * width, y + this.offset.y * height, ]; } close() { if (this.fillColor) { this.ctx.fillStyle = this.fillColor; } if (this.strokeColor) { this.ctx.strokeStyle = this.strokeColor; this.ctx.lineWidth = this.lineWidth; this.ctx.lineCap = this.lineCap; } this.ctx.rotate(this.rotation); this.ctx.translate(this.translation.x, this.translation.y); switch (this.type) { case 'arc': drawArc(this.ctx, this.args, !!this.fillColor, !!this.strokeColor); break; case 'circle': drawCircle(this.ctx, this.args, !!this.fillColor, !!this.strokeColor); break; case 'line': drawShape(this.ctx, this.args.close, this.args.vertices, !!this.fillColor, !!this.strokeColor); break; case 'rect': /*if (this.fillColor) { this.ctx.fillRect(...this.args); } if (this.strokeColor) { this.ctx.strokeRect(...this.args); }*/ break; case 'roundedRect': this.args.x = this.calcCoords(this.args.x, 0, this.args.width, 0)[0]; this.args.y = this.calcCoords(0, this.args.y, 0, this.args.height)[1]; drawRoundedRect(this.ctx, this.args, !!this.fillColor, !!this.strokeColor); break; case 'shape': console.log('test'); drawShape(this.ctx, true, this.args, !!this.fillColor, !!this.strokeColor); break; case 'text': this.ctx.font = this.textOptions.fontSize + 'px ' + this.textOptions.font; this.args.x = this.calcCoords(this.args.x, 0, 0)[0]; this.args.y = this.calcCoords(0, this.args.y, 0, 0)[1]; drawText(this.ctx, this.args, !!this.fillColor, !!this.strokeColor, this.textOptions); break; case 'triangle': drawTriangle(this.ctx, this.args, !!this.fillColor, !!this.strokeColor); break; } this.ctx.restore(); } } function parseColor(colorMode, ...color) { if (color.length === 0) { return '#000000'; } if (color.length === 1 && typeof color[0] === 'string') { return color[0]; } if (color.length === 1 && typeof color[0] === 'number') { color.push(color[0]); } if (color.length === 2 && typeof color[0] === 'number') { color.push(color[0]); } if (color.length === 3) { return `${colorMode}(${color[0]}, ${color[1]}, ${color[2]})`; } return '#000000'; } function drawArc(ctx, args, fill, stroke) { const { x, y, radius, startAngle, endAngle, close, } = args; ctx.beginPath(); ctx.arc(x, y, radius, startAngle, endAngle); if (close) { ctx.lineTo(x, y); ctx.closePath(); } if (fill) { ctx.fill(); } if (stroke) { ctx.stroke(); } } function drawCircle(ctx, args, fill, stroke) { const { x, y, radius } = args; ctx.beginPath(); ctx.arc(x, y, radius, 0, 2 * Math.PI); if (fill) { ctx.fill(); } if (stroke) { ctx.stroke(); } } function drawShape(ctx, close, vertices, fill, stroke) { ctx.beginPath(); ctx.moveTo(...vertices[0]); for (let i = 1; i < vertices.length; i++) { ctx.lineTo(...vertices[i]); } if (close) { ctx.closePath(); } if (fill) { ctx.fill(); } if (stroke) { ctx.stroke(); } } function drawRoundedRect(ctx, args, fill, stroke) { const { x, y, width, height, roundness } = args; ctx.beginPath(); ctx.moveTo(x + roundness, y); ctx.arc(x + width - roundness, y + roundness, roundness, -0.5 * Math.PI, 0); ctx.arc(x + width - roundness, y + height - roundness, roundness, 0, 0.5 * Math.PI); ctx.arc(x + roundness, y + height - roundness, roundness, 0.5 * Math.PI, Math.PI); ctx.arc(x + roundness, y + roundness, roundness, Math.PI, 1.5 * Math.PI); if (fill) { ctx.fill(); } if (stroke) { ctx.stroke(); } } function drawText(ctx, args, fill, stroke, options) { const { x, y, string } = args; ctx.font = options.fontSize + 'px ' + options.font; ctx.textAlign = options.alignment; ctx.textBaseline = options.baseline; if (fill) { ctx.fillText(string, x, y); } if (stroke) { ctx.strokeText(string, x, y); } } function drawTriangle(ctx, args, fill, stroke) { const { x1, y1, x2, y2, x3, y3 } = args; drawShape(ctx, true, [ [x1, y1], [x2, y2], [x3, y3] ], fill, stroke) }