smolworld / src /js /gui.js
p3nGu1nZz's picture
✨ Refactor physics module; add math utilities and event handling classes; update state structure for 3D entities
7ffaa9e
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);
}
}