|
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); |
|
|
|
|
|
let currentY = y + this.config.lineHeight + 5; |
|
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 |
|
); |
|
|
|
|
|
ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; |
|
ctx.fillRect(x, y, this.config.width, trackHeight); |
|
|
|
|
|
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; |
|
|
|
|
|
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); |
|
|
|
|
|
const zCoord = Math.log2(camera.scale); |
|
const position = camera.transform.position; |
|
|
|
|
|
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 |
|
); |
|
|
|
|
|
Object.values(this.buttons).forEach(btn => { |
|
|
|
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); |
|
} |
|
|
|
draw(ctx, camera) { |
|
const canvasWidth = ctx.canvas.width; |
|
ctx.fillStyle = 'white'; |
|
ctx.font = '12px monospace'; |
|
ctx.textAlign = 'right'; |
|
|
|
|
|
ctx.fillText(`FPS: ${this.fps}`, canvasWidth - 20, 20); |
|
|
|
|
|
ctx.fillText(`Speed: ${this.speed} units/s`, canvasWidth - 20, 40); |
|
} |
|
} |
|
|