export class GUI { static drawText(ctx, text, x, y, options = {}) { const { font = '14px "Courier New", monospace', color = 'white', align = 'left', } = options; ctx.save(); ctx.font = font; ctx.fillStyle = color; ctx.textAlign = align; ctx.fillText(text, x, y); ctx.restore(); } static drawTextCursor(ctx, x, y, options = {}) { const { height = 16, width = 2, color = 'white' } = options; ctx.save(); ctx.fillStyle = color; ctx.fillRect(x, y - height + 2, width, height); ctx.restore(); } static measureText(ctx, text, font) { ctx.save(); ctx.font = font; const metrics = ctx.measureText(text); ctx.restore(); return metrics; } } export class TextArea { constructor(config) { this.config = config; this.lines = []; this.scrollOffset = 0; this.maxScrollOffset = 0; this.visibleLines = 0; } setLines(lines) { this.lines = lines; this.updateScrollLimits(); } addLine(line) { this.lines.push(line); this.updateScrollLimits(); } clear() { this.lines = []; this.scrollOffset = 0; this.maxScrollOffset = 0; } updateScrollLimits() { const totalLines = this.lines.length; if (totalLines <= this.visibleLines) { this.maxScrollOffset = 0; this.scrollOffset = 0; return; } this.maxScrollOffset = totalLines - this.visibleLines; } setVisibleLines(count) { this.visibleLines = count; this.updateScrollLimits(); } draw(ctx, x, y, width, height) { const startIdx = Math.max(0, this.lines.length - this.visibleLines - this.scrollOffset); const endIdx = this.lines.length - this.scrollOffset; const visibleLines = this.lines.slice(startIdx, endIdx); // Add extra margin to starting Y position let currentY = y + this.config.lineHeight + 5; // Add 5px top margin visibleLines.forEach(line => { GUI.drawText(ctx, line, x, currentY, { font: this.config.font }); currentY += this.config.lineHeight; }); } } export class ScrollBar { constructor(config) { this.config = config; } draw(ctx, x, y, height, scrollRatio, viewportRatio) { const trackHeight = height - this.config.margin * 2; const thumbHeight = Math.max( this.config.minThumbHeight, viewportRatio * trackHeight ); // Draw track ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; ctx.fillRect(x, y, this.config.width, trackHeight); // Draw thumb const thumbY = y + (trackHeight - thumbHeight) * scrollRatio; ctx.fillStyle = 'rgba(255, 255, 255, 0.5)'; ctx.fillRect(x, thumbY, this.config.width, thumbHeight); } } export class ZoomControls { constructor() { this.buttons = { reset: { x: 0, y: 0, width: 50, height: 20, hover: false, text: 'Reset' }, zoomIn: { x: 0, y: 0, width: 20, height: 20, hover: false, text: '+' }, zoomOut: { x: 0, y: 0, width: 20, height: 20, hover: false, text: '-' } }; } updatePositions(canvasWidth, canvasHeight) { const margin = 20; const bottom = canvasHeight - margin; const { reset, zoomIn, zoomOut } = this.buttons; // Position from right: Reset | + | - reset.x = canvasWidth - margin - reset.width; reset.y = bottom - reset.height; zoomIn.x = reset.x - zoomIn.width - 2; zoomIn.y = bottom - zoomIn.height; zoomOut.x = zoomIn.x - zoomOut.width - 2; zoomOut.y = bottom - zoomOut.height; } draw(ctx, camera) { const canvasWidth = ctx.canvas.width; const canvasHeight = ctx.canvas.height; this.updatePositions(canvasWidth, canvasHeight); // Calculate Z coordinate based on zoom (1 = 0, >1 = positive, <1 = negative) const zCoord = Math.log2(camera.scale); const position = camera.transform.position; // Draw camera position with Z coordinate (always 2 decimal places) ctx.fillStyle = 'white'; ctx.font = '12px monospace'; ctx.textAlign = 'right'; ctx.fillText( `Camera (${position.x.toFixed(2)}, ${position.y.toFixed(2)}, ${zCoord.toFixed(2)})`, this.buttons.reset.x + this.buttons.reset.width, this.buttons.reset.y - 25 // Increased distance from buttons ); // Draw buttons with subtler hover effect Object.values(this.buttons).forEach(btn => { // Increase hover brightness ctx.fillStyle = btn.hover ? 'rgba(255,255,255,0.3)' : 'rgba(255,255,255,0.1)'; ctx.fillRect(btn.x, btn.y, btn.width, btn.height); ctx.strokeStyle = 'rgba(255,255,255,0.5)'; ctx.strokeRect(btn.x, btn.y, btn.width, btn.height); ctx.fillStyle = 'white'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText(btn.text, btn.x + btn.width / 2, btn.y + btn.height / 2); }); } isPointInButton(x, y, button) { return x >= button.x && x <= button.x + button.width && y >= button.y && y <= button.y + button.height; } handleClick(x, y, camera) { const { reset, zoomIn, zoomOut } = this.buttons; if (this.isPointInButton(x, y, reset)) { camera.setZoom(1); camera.moveTo(0, 0); return true; } else if (this.isPointInButton(x, y, zoomIn)) { camera.zoomBy(1.1); return true; } else if (this.isPointInButton(x, y, zoomOut)) { camera.zoomBy(0.9); return true; } return false; } handleMouseMove(x, y) { Object.values(this.buttons).forEach(btn => { btn.hover = this.isPointInButton(x, y, btn); }); return Object.values(this.buttons).some(btn => btn.hover); } } export class FPSCounter { constructor() { this.fps = 0; this.speed = 0; } update(fps, velocity) { this.fps = fps; this.speed = velocity.x.toFixed(2); // Format to 2 decimal places } draw(ctx, camera) { const canvasWidth = ctx.canvas.width; ctx.fillStyle = 'white'; ctx.font = '12px monospace'; ctx.textAlign = 'right'; // Draw FPS ctx.fillText(`FPS: ${this.fps}`, canvasWidth - 20, 20); // Draw velocity in units/s ctx.fillText(`Speed: ${this.speed} units/s`, canvasWidth - 20, 40); } }