add controlPanel component

This commit is contained in:
nadiarvi 2025-05-01 17:17:26 +09:00
parent 409ebdf7fa
commit 4e609c171a
15 changed files with 408 additions and 41 deletions

185
lib/_buttons.js Normal file
View File

@ -0,0 +1,185 @@
const STYLE_DEFAULT = {
background: '#eee',
color: '#111',
border_color: '',
border_width: 0,
border_radius: 5,
text_font: 'sans-serif',
text_size: 12,
}
const STYLE_HOVER = {
background: '#ccc',
color: '#111',
}
const STYLE_PRESSED = {
background: '#aaa',
color: '#000',
}
const STYLE_DISABLED = {
background: '#777',
color: '#333',
}
export default class Button {
#was_pressed = false;
#was_hovering = false;
#props = { };
#bounds = { minx: null, miny: null, maxx: null, maxy: null, centerx: null, centery: null };
#cstyles = { default: STYLE_DEFAULT, hover: STYLE_HOVER, pressed: STYLE_PRESSED, disabled: STYLE_DISABLED };
constructor(properties) {
this.#props = Object.assign({
content: '',
x: null, y: null,
w: null, h: null,
width: null,
height: null,
style_default: STYLE_DEFAULT,
style_hover: STYLE_HOVER,
style_pressed: STYLE_PRESSED,
style_disabled: STYLE_DISABLED,
on_mouse_enter: null,
on_mouse_exit: null,
on_press: null,
on_release: null,
align_x: -1,
align_y: -1,
enabled: true,
}, properties);
if ( this.#props.x === null || this.#props.y === null || this.#props.width === null || this.#props.height === null )
throw( '"x", "y", "width", and "height" must all be defined in the button properties!' );
Object.seal(this.#props);
this.#calculateStyles();
this.#calculateBounds();
}
#calculateBounds() {
const offset_x = (this.#props.align_x-1)*this.#props.width/2,
offset_y = (this.#props.align_y-1)*this.#props.height/2;
this.#bounds.minx = this.#props.x + offset_x,
this.#bounds.miny = this.#props.y + offset_y,
this.#bounds.maxx = this.#props.x + this.#props.width + offset_x,
this.#bounds.maxy = this.#props.y + this.#props.height + offset_y,
this.#bounds.centerx = this.#props.x + this.#props.width/2 + offset_x,
this.#bounds.centery = this.#props.y + this.#props.height/2 + offset_y;
}
#calculateStyles() {
this.#cstyles.default = Object.assign({}, STYLE_DEFAULT, this.#props.style_default);
this.#cstyles.hover = Object.assign({}, STYLE_DEFAULT, this.#props.style_default, this.#props.style_hover);
this.#cstyles.pressed = Object.assign({}, STYLE_DEFAULT, this.#props.style_default, this.#props.style_pressed);
this.#cstyles.disabled = Object.assign({}, STYLE_DEFAULT, this.#props.style_default, this.#props.style_disabled);
}
update(properties) {
try {
Object.assign(this.#props, properties);
} catch(e) {console.warn( `Encountered an unrecognized property! Original error: ${e.message}` )}
if ( 'x' in properties || 'y' in properties || 'width' in properties || 'height' in properties || 'align_x' in properties || 'align_y' in properties )
this.#calculateBounds();
if ( 'style_default' in properties || 'style_hover' in properties || 'style_pressed' in properties || 'style_disabled' in properties )
this.#calculateStyles();
}
/** Shorthand for .update({ x: <value>, y: <value>, ... }) */
place( x, y, width=null, height=null ) {
this.#props.x = x,
this.#props.y = y;
if ( width !== null ) this.#props.width = width;
if ( height !== null ) this.#props.height = height;
this.#calculateBounds();
}
/** Shorthand for .update({ style_<stylename>: <value> }) */
style( stylename, style ) {
if (!( 'style_'+stylename in this.#props ))
throw( `Style name must be either "default", "hover", "pressed", or "disabled". Received "${stylename}" instead.` );
this.#props['style_'+stylename] = style;
this.#calculateStyles();
}
/** Shorthand for .update({ content: <value> }) */
text( content ) { this.#props.content = content }
/** Shorthand for .update({ enabled: true }) */
enable() { this.#props.enabled = true }
/** Shorthand for .update({ enabled: false }) */
disable() { this.#props.enabled = false }
/**
* Returns whether the specified point (by default the mouse's position) is hovering over the button.
* @param {number} x (optional) x override.
* @param {number} y (optional) y override.
* @returns {boolean}
*/
isHovering(x=mouseX, y=mouseY) {
return x > this.#bounds.minx && x < this.#bounds.maxx && y > this.#bounds.miny && y < this.#bounds.maxy;
}
/**
* Returns whether the button is currently being pressed.
* @returns {boolean}
*/
isPressed() {
return this.isHovering() && mouseIsPressed;
}
#getCurrentStyle( hovering, pressed ) {
if ( !this.#props.enabled ) return this.#cstyles.disabled;
if ( pressed ) return this.#cstyles.pressed;
if ( hovering ) return this.#cstyles.hover;
return this.#cstyles.default;
}
/**
* Draws the button on the specified canvas, or the global canvas by default.
* @param context (optional) The p5 canvas to draw to.
*/
draw( context=globalThis ) {
const is_hovering = this.isHovering();
const is_pressed = mouseIsPressed && (is_hovering || this.#was_pressed);
const style = this.#getCurrentStyle( is_hovering, is_pressed );
if ( style.background ) context.fill( style.background );
else context.noFill();
if ( style.border_color && style.border_radius ) {
context.stroke( style.border_color );
context.strokeWeight( style.border_width );
} else context.noStroke();
context.rect( this.#bounds.minx, this.#bounds.miny, this.#props.width, this.#props.height, style.border_radius );
noStroke();
context.fill( style.color );
context.textAlign( CENTER, CENTER );
context.textSize( style.text_size );
context.textFont( style.text_font );
context.text( this.#props.content, this.#bounds.centerx, this.#bounds.centery );
if ( !this.#was_pressed && is_pressed && this.#props.on_press ) this.#props.on_press();
if ( this.#was_pressed && !is_pressed && this.#props.on_release ) this.#props.on_release();
if ( !this.#was_hovering && is_hovering && this.#props.on_mouse_enter ) this.#props.on_mouse_enter();
if ( this.#was_hovering && !is_hovering && this.#props.on_mouse_exit ) this.#props.on_mouse_exit();
this.#was_pressed = is_pressed;
this.#was_hovering = is_hovering;
}
}

BIN
public/assets/down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

BIN
public/assets/empty.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
public/assets/left.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/assets/right.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/assets/up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

21
src/components/Arrow.js Normal file
View File

@ -0,0 +1,21 @@
export class Arrow {
static images = {};
static preload() {
Arrow.images.up = loadImage('/assets/up.png');
Arrow.images.down = loadImage('/assets/down.png');
Arrow.images.left = loadImage('/assets/left.png');
Arrow.images.right = loadImage('/assets/right.png');
Arrow.images.empty = loadImage('/assets/empty.png');
}
constructor(direction) {
this.direction = direction;
this.image = Arrow.images[direction];
}
draw(x, y) {
image(this.image, x, y, 40, 40);
}
}

View File

@ -0,0 +1,74 @@
import { Arrow } from "./Arrow";
import { colors } from "../utils/theme";
export class ControlPanel {
constructor({ name, x, y, numBoxes }) {
this.name = name;
this.x = x;
this.y = y;
this.numBoxes = numBoxes;
this.boxWidth = 48;
this.boxHeight = 48;
this.boxSpacing = 8;
this.contents = Array(numBoxes).fill(null);
this.empty = new Arrow('empty');
this.fontSize = 20;
this.gap = this.fontSize;
}
updateBox(index, content) {
if (index >= 0 && index < this.numBoxes) {
this.contents[index] = content;
}
}
draw() {
rectMode(CORNER);
// Label
fill(colors.tertiary);
noStroke();
rect(this.x, this.y, this.getPanelWidth(), this.getTextBoxHeight(), 5);
fill(colors.secondary);
textAlign(CENTER, CENTER);
textSize(this.fontSize);
text(this.name, this.x + this.getPanelWidth() / 2, this.y + this.getTextBoxHeight() /2);
// White panel
fill(255);
noStroke();
rect(this.x, this.y + 24 + this.gap, this.getPanelWidth(), this.boxHeight + this.boxSpacing, 6);
// Boxes
for (let i = 0; i < this.numBoxes; i++) {
const bx = this.x + 12 + i * (this.boxWidth + this.boxSpacing);
const by = this.y + 24 + this.gap + (this.boxSpacing * 1) / 2;
// Draw box
// fill(255);
// stroke(0);
// strokeWeight(1.5);
// rect(bx, by, this.boxWidth, this.boxHeight, 4);
// Draw content if it exists
if (this.contents[i]) {
if (this.contents[i] instanceof Arrow) {
this.contents[i].draw(bx + this.boxWidth/2 - 20, by + this.boxHeight/2 - 20);
}
} else {
this.empty.draw(bx + this.boxWidth / 2 - 20, by + this.boxHeight / 2 - 20);
}
}
}
getPanelWidth() {
return this.numBoxes * (this.boxWidth + this.boxSpacing) + 12;
}
getTextBoxHeight(){
return this.fontSize + 16;
}
}

View File

@ -1,4 +1,5 @@
import { colors } from './theme.js';
import { colors } from './utils/theme.js';
import { Arrow } from './components/Arrow.js';
import StartScene from './scenes/startScene.js';
import GameScene from './scenes/gameScene.js';
@ -13,7 +14,7 @@ function setup(){
mgr = new SceneManager();
mgr.addScene(StartScene);
mgr.addScene(GameScene);
mgr.showScene(StartScene);
mgr.showScene(GameScene);
};
function draw(){
@ -31,8 +32,13 @@ function mousePressed(){
mgr.handleEvent('mousePressed');
};
function preload(){
Arrow.preload();
}
window.setup = setup;
window.draw = draw;
window.windowResized = windowResized;
window.mousePressed = mousePressed;
window.preload = preload;

View File

@ -1,13 +1,39 @@
import { colors } from '../theme.js';
import { draw as drawWorld, groundHeight } from '../world.js';
import { colors } from '../utils/theme.js';
import { Cat } from '../cat.js';
import { buttonS } from '../utils/theme.js';
import { MyButton } from '../utils/components.js';
import { Arrow } from '../components/Arrow.js';
import { ControlPanel } from '../components/controlPanel.js';
export default function GameScene() {
let cat;
let runButton;
let blocks;
this.name = "GameScene";
this.setup = () => {
cat = new Cat(width / 6, height - 167.5, 150);
runButton = new MyButton({
x: width / 16 * 15,
y: height / 16,
text: "run >>",
mode: "CENTER",
style: buttonS,
onPress: () => {
console.log("Run button pressed");
}
});
blocks = new ControlPanel({
name: 'blocks',
x: width / 32,
y: height / 32,
numBoxes: 4
});
};
this.draw = () => {
@ -27,12 +53,15 @@ export default function GameScene() {
// Sprite
cat.draw();
runButton.draw();
blocks.draw();
};
this.onResize = () => {
if (cat) {
cat.setPosition(width / 2, height - 177.5);
}
cat.setPosition(width / 2, height - 177.5);
runButton.setPosition((width / 16) * 15, height / 16);
blocks.x = width / 16 - 40;
blocks.y = height / 16 + 8;
}
}

View File

@ -1,6 +1,6 @@
import { colors } from '../theme.js';
import { colors } from '../utils/theme.js';
import { Cat } from '../cat.js';
import { drawRect } from '../utils/draw.js';
import { buttonL } from '../utils/theme.js';
import GameScene from './gameScene.js';
export default function StartScene() {
@ -13,7 +13,7 @@ export default function StartScene() {
};
let startButton;
// this.name = "StartScene";
this.name = "StartScene";
this.setup = () => {
cat = new Cat(width / 2, height - 200, 200);
@ -25,24 +25,24 @@ export default function StartScene() {
startButton.locate(width/2, height/2 + 50);
// Size
startButton.width = 300;
startButton.height = 75;
startButton.width = buttonL.width;
startButton.height = buttonL.height;
// Visual styling
startButton.color = colors.tertiary;
startButton.stroke = colors.secondary;
startButton.strokeWeight = 3;
startButton.color = buttonL.color;
startButton.stroke = buttonL.stroke;
startButton.strokeWeight = buttonL.strokeWeight;
// Text properties
startButton.text = 'start';
startButton.textFont = "Pixelify Sans";
startButton.textSize = 32;
startButton.textColor = colors.secondary;
startButton.textFont = buttonL.textFont;
startButton.textSize = buttonL.textSize;
startButton.textColor = buttonL.textColor;
startButton.onPress = () => {
console.log("press");
this.sceneManager.showScene(GameScene);
}
};
};
this.draw = () => {
@ -89,19 +89,9 @@ export default function StartScene() {
this.onResize = () => {
if (cat) {
cat.setPosition(width / 2, height - 200);
startButton.locate(width/2, height/2 + 50);
}
}
this.mousePressed = function (){
const x = mouseX;
const y = mouseY;
return;
this.sceneManager.showScene(GameScene);
};
this.exit = function () {
if (cat) {

View File

@ -1,10 +0,0 @@
const colors = {
primary: "#a6d1ff",
secondary: "#ff4f64",
tertiary: "#fff29d",
accent: "#a4e4b6"
};
export {
colors
}

40
src/utils/components.js Normal file
View File

@ -0,0 +1,40 @@
export class MyButton {
constructor({ x, y, text, mode = "CORNER", style = {}, onPress = null }) {
this.button = new Clickable();
// Apply layout
this.button.mode = mode;
this.button.locate(x, y);
// Apply text
this.button.text = text;
// Apply styles
this.button.width = style.width || 150;
this.button.height = style.height || 50;
this.button.color = style.color || "#ffffff";
this.button.stroke = style.stroke || "#000000";
this.button.strokeWeight = style.strokeWeight || 2;
this.button.textFont = style.textFont || "sans-serif";
this.button.textSize = style.textSize || 16;
this.button.textColor = style.textColor || "#000000";
// Event
if (onPress) {
this.button.onPress = onPress;
}
}
draw() {
this.button.draw();
}
setText(newText) {
this.button.text = newText;
}
setPosition(x, y) {
this.button.locate(x, y);
}
};

32
src/utils/theme.js Normal file
View File

@ -0,0 +1,32 @@
const colors = {
primary: "#a6d1ff",
secondary: "#ff4f64",
tertiary: "#fff29d",
accent: "#a4e4b6",
};
const button = {
color: colors.tertiary,
stroke: colors.secondary,
strokeWeight: 3,
textFont: "Pixelify Sans",
textColor: colors.secondary
}
const buttonL = {
...button,
width: 300,
height: 75,
textSize: 32,
};
const buttonS = {
...button,
width: 100,
height: 40,
textSize: 20,
strokeWeight: 3,
};
export { colors, buttonL, buttonS };

View File

@ -1,4 +1,4 @@
import { colors } from './theme.js';
import { colors } from './utils/theme.js';
export const groundHeight = 100;