diff --git a/lib/_buttons.js b/lib/_buttons.js new file mode 100644 index 0000000..b34d8bb --- /dev/null +++ b/lib/_buttons.js @@ -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: , y: , ... }) */ + 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_: }) */ + 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: }) */ + 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; + } +} \ No newline at end of file diff --git a/public/assets/down.png b/public/assets/down.png new file mode 100644 index 0000000..69a1fc7 Binary files /dev/null and b/public/assets/down.png differ diff --git a/public/assets/empty.png b/public/assets/empty.png new file mode 100644 index 0000000..a3869b1 Binary files /dev/null and b/public/assets/empty.png differ diff --git a/public/assets/left.png b/public/assets/left.png new file mode 100644 index 0000000..57c8404 Binary files /dev/null and b/public/assets/left.png differ diff --git a/public/assets/right.png b/public/assets/right.png new file mode 100644 index 0000000..1b5d98a Binary files /dev/null and b/public/assets/right.png differ diff --git a/public/assets/up.png b/public/assets/up.png new file mode 100644 index 0000000..251da77 Binary files /dev/null and b/public/assets/up.png differ diff --git a/src/components/Arrow.js b/src/components/Arrow.js new file mode 100644 index 0000000..089b5d5 --- /dev/null +++ b/src/components/Arrow.js @@ -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); + } + } + \ No newline at end of file diff --git a/src/components/ControlPanel.js b/src/components/ControlPanel.js new file mode 100644 index 0000000..aec2f4f --- /dev/null +++ b/src/components/ControlPanel.js @@ -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; + } +} diff --git a/src/main.js b/src/main.js index 5f4b46d..067481d 100644 --- a/src/main.js +++ b/src/main.js @@ -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; diff --git a/src/scenes/gameScene.js b/src/scenes/gameScene.js index 52ca3a1..a8cfe63 100644 --- a/src/scenes/gameScene.js +++ b/src/scenes/gameScene.js @@ -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; } } diff --git a/src/scenes/startScene.js b/src/scenes/startScene.js index 926d863..63a6e4d 100644 --- a/src/scenes/startScene.js +++ b/src/scenes/startScene.js @@ -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) { diff --git a/src/theme.js b/src/theme.js deleted file mode 100644 index c51baea..0000000 --- a/src/theme.js +++ /dev/null @@ -1,10 +0,0 @@ -const colors = { - primary: "#a6d1ff", - secondary: "#ff4f64", - tertiary: "#fff29d", - accent: "#a4e4b6" -}; - -export { - colors -} \ No newline at end of file diff --git a/src/utils/components.js b/src/utils/components.js new file mode 100644 index 0000000..0b10163 --- /dev/null +++ b/src/utils/components.js @@ -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); + } +}; + \ No newline at end of file diff --git a/src/utils/theme.js b/src/utils/theme.js new file mode 100644 index 0000000..f5cd09d --- /dev/null +++ b/src/utils/theme.js @@ -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 }; diff --git a/src/world.js b/src/world.js index 94bf5af..de41ca5 100644 --- a/src/world.js +++ b/src/world.js @@ -1,4 +1,4 @@ -import { colors } from './theme.js'; +import { colors } from './utils/theme.js'; export const groundHeight = 100;