created right click handler, refacter renderers

This commit is contained in:
evanpelle
2024-09-16 17:09:01 -07:00
parent e9cd3664c9
commit 7c5ebaf456
13 changed files with 335 additions and 183 deletions
+1 -1
View File
@@ -109,13 +109,13 @@
* Make boats more intuitive (larger area to click off coast) DONE 9/11/2024
* FakeHumans retaliate when attacked DONE 9/11/2024
--- v3 Release DONE
* Add discord link DONE 9/14/2024
* front page mobile friendly DONE 9/15/2024
* game mobile friendly DONE 9/16/2024
* UI: basic win condition & popup DONE 9/16/2024
* right click popup alliance option
* make fake humans easier
* click alliance sends alliance request
* notification for alliance request
* comfirm alliance
+4 -11
View File
@@ -3,7 +3,7 @@ import {Cell, MutableGame, PlayerEvent, PlayerID, MutablePlayer, TileEvent, Play
import {createGame} from "../core/GameImpl";
import {EventBus} from "../core/EventBus";
import {Config} from "../core/configuration/Config";
import {GameRenderer} from "./graphics/GameRenderer";
import {createRenderer, GameRenderer} from "./graphics/GameRenderer";
import {InputHandler, MouseUpEvent, ZoomEvent, DragEvent, MouseDownEvent} from "./InputHandler"
import {ClientID, ClientIntentMessageSchema, ClientJoinMessageSchema, ClientLeaveMessageSchema, ClientMessageSchema, GameID, Intent, ServerMessage, ServerMessageSchema, ServerSyncMessage, Turn} from "../core/Schemas";
import {TerrainMap} from "../core/TerrainMapLoader";
@@ -16,8 +16,7 @@ import {WinCheckExecution} from "../core/execution/WinCheckExecution";
export function createClientGame(name: string, clientID: ClientID, playerID: PlayerID, ip: string | null, gameID: GameID, config: Config, terrainMap: TerrainMap): ClientGame {
let eventBus = new EventBus()
let game = createGame(terrainMap, eventBus, config)
let terrainRenderer = new TerrainRenderer(game)
let gameRenderer = new GameRenderer(eventBus, game, clientID, terrainRenderer)
let gameRenderer = createRenderer(game, eventBus, clientID)
return new ClientGame(
name,
@@ -119,14 +118,9 @@ export class ClientGame {
public start() {
console.log('version 3')
this.isActive = true
// TODO: make each class do this, or maybe have client intercept all requests?
//this.eventBus.on(TickEvent, (e) => this.tick(e))
this.eventBus.on(TileEvent, (e) => this.renderer.tileUpdate(e))
this.eventBus.on(PlayerEvent, (e) => this.playerEvent(e))
this.eventBus.on(BoatEvent, (e) => this.renderer.boatEvent(e))
this.eventBus.on(MouseUpEvent, (e) => this.inputEvent(e))
this.eventBus.on(ZoomEvent, (e) => this.renderer.onZoom(e))
this.eventBus.on(DragEvent, (e) => this.renderer.onMove(e))
this.renderer.initialize()
this.input.initialize()
@@ -179,14 +173,13 @@ export class ClientGame {
console.log('setting name')
this.myPlayer = event.player
}
this.renderer.playerEvent(event)
}
private inputEvent(event: MouseUpEvent) {
if (!this.isActive) {
return
}
const cell = this.renderer.screenToWorldCoordinates(event.x, event.y)
const cell = this.renderer.transformHandler.screenToWorldCoordinates(event.x, event.y)
if (!this.gs.isOnMap(cell)) {
return
}
+35
View File
@@ -15,6 +15,13 @@ export class MouseDownEvent implements GameEvent {
) { }
}
export class RightClickEvent implements GameEvent {
constructor(
public readonly x: number,
public readonly y: number,
) { }
}
export class ZoomEvent implements GameEvent {
constructor(
public readonly x: number,
@@ -32,6 +39,8 @@ export class DragEvent implements GameEvent {
export class InputHandler {
private contextMenuActive = false
private lastPointerX: number = 0;
private lastPointerY: number = 0;
@@ -51,10 +60,23 @@ export class InputHandler {
document.addEventListener("pointerup", (e) => this.onPointerUp(e));
document.addEventListener("wheel", (e) => this.onScroll(e), {passive: false});
document.addEventListener('pointermove', this.onPointerMove.bind(this));
document.addEventListener('contextmenu', (e: MouseEvent) => {
this.onRightClick(e)
});
this.pointers.clear()
}
private onPointerDown(event: PointerEvent) {
if (this.contextMenuActive) {
this.contextMenuActive = false
return
}
if (event.button > 0) {
return
}
this.pointerDown = true
this.pointers.set(event.pointerId, event);
@@ -72,6 +94,9 @@ export class InputHandler {
}
onPointerUp(event: PointerEvent) {
if (event.button > 0) {
return
}
this.pointerDown = false
this.pointers.delete(event.pointerId);
const dist = Math.abs(event.x - this.lastPointerDownX) + Math.abs(event.y - this.lastPointerDownY);
@@ -85,6 +110,10 @@ export class InputHandler {
}
private onPointerMove(event: PointerEvent) {
if (event.button > 0) {
return
}
this.pointers.set(event.pointerId, event);
@@ -112,6 +141,12 @@ export class InputHandler {
}
}
private onRightClick(event: MouseEvent) {
event.preventDefault()
this.contextMenuActive = true
this.eventBus.emit(new RightClickEvent(event.clientX, event.clientY))
}
private getPinchDistance(): number {
const pointerEvents = Array.from(this.pointers.values());
const dx = pointerEvents[0].clientX - pointerEvents[1].clientX;
+39 -156
View File
@@ -6,64 +6,44 @@ import {NameRenderer} from "./NameRenderer";
import {TerrainRenderer} from "./TerrainRenderer";
import {TerritoryRenderer} from "./TerritoryRenderer";
import {ClientID} from "../../core/Schemas";
import {renderTroops} from "./Utils";
import {createCanvas, renderTroops} from "./Utils";
import {UIRenderer} from "./UIRenderer";
import {EventBus} from "../../core/EventBus";
import {TransformHandler} from "./TransformHandler";
import {Layer} from "./Layer";
export function createRenderer(game: Game, eventBus: EventBus, clientID: ClientID): GameRenderer {
const canvas = createCanvas()
const transformHandler = new TransformHandler(game, eventBus, canvas.getBoundingClientRect())
const layers: Layer[] = [
new TerrainRenderer(game),
new TerritoryRenderer(game, eventBus),
new NameRenderer(game, game.config().theme()),
new UIRenderer(eventBus, game, game.config().theme(), clientID)
]
return new GameRenderer(game, eventBus, canvas, transformHandler, layers)
}
export class GameRenderer {
private territoryCanvas: HTMLCanvasElement
private canvas: HTMLCanvasElement
private territoryContext: CanvasRenderingContext2D
private scale: number = 1.8
private offsetX: number = -350
private offsetY: number = -200
private context: CanvasRenderingContext2D
private nameRenderer: NameRenderer;
private territoryRenderer: TerritoryRenderer;
private uiRenderer: UIRenderer;
private theme: Theme
constructor(private eventBus: EventBus, private gs: Game, private clientID: ClientID, private terrainRenderer: TerrainRenderer) {
this.theme = gs.config().theme()
this.nameRenderer = new NameRenderer(gs, this.theme)
this.territoryRenderer = new TerritoryRenderer(gs)
this.uiRenderer = new UIRenderer(eventBus, gs, this.theme, clientID)
constructor(private game: Game, private eventBus: EventBus, private canvas: HTMLCanvasElement, public transformHandler: TransformHandler, private layers: Layer[]) {
this.context = canvas.getContext("2d")
}
initialize() {
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext('2d');
// Set canvas style to fill the screen
this.canvas.style.position = 'fixed';
this.canvas.style.left = '0';
this.canvas.style.top = '0';
this.canvas.style.width = '100%';
this.canvas.style.height = '100%';
this.canvas.style.touchAction = 'none';
this.nameRenderer.initialize()
this.terrainRenderer.init()
this.territoryRenderer.init()
this.uiRenderer.init()
this.layers.forEach(l => l.init())
document.body.appendChild(this.canvas);
window.addEventListener('resize', () => this.resizeCanvas());
this.resizeCanvas();
this.territoryCanvas = document.createElement('canvas')
this.territoryCanvas.width = this.gs.width();
this.territoryCanvas.height = this.gs.height();
this.territoryContext = this.territoryCanvas.getContext('2d')
this.territoryContext.globalAlpha = 0.4;
this.transformHandler = new TransformHandler(this.game, this.eventBus, this.canvas.getBoundingClientRect())
requestAnimationFrame(() => this.renderGame());
}
@@ -76,55 +56,43 @@ export class GameRenderer {
renderGame() {
// Set background
this.context.fillStyle = this.theme.backgroundColor().toHex();
this.context.fillStyle = this.game.config().theme().backgroundColor().toHex();
this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Save the current context state
this.context.save();
this.transformHandler.handleTransform(this.context)
// Disable image smoothing for pixelated effect
if (this.scale > 3) {
this.context.imageSmoothingEnabled = false;
} else {
this.context.imageSmoothingEnabled = true;
}
// Apply zoom and pan
this.context.setTransform(
this.scale,
0,
0,
this.scale,
this.gs.width() / 2 - this.offsetX * this.scale,
this.gs.height() / 2 - this.offsetY * this.scale
);
this.terrainRenderer.draw(this.context)
this.territoryRenderer.draw(this.context)
const [upperLeft, bottomRight] = this.boundingRect()
this.nameRenderer.render(this.context, this.scale, upperLeft, bottomRight)
this.layers.forEach(l => {
if (l.shouldTransform()) {
l.render(this.context, this.transformHandler)
}
})
this.context.restore()
this.layers.forEach(l => {
if (!l.shouldTransform()) {
l.render(this.context, this.transformHandler)
}
})
this.renderSpawnBar()
this.uiRenderer.render(this.context)
requestAnimationFrame(() => this.renderGame());
}
// TODO: move to UIRenderer
renderSpawnBar() {
if (!this.gs.inSpawnPhase()) {
if (!this.game.inSpawnPhase()) {
return
}
const barHeight = 15;
const barBackgroundWidth = this.canvas.width;
const ratio = this.gs.ticks() / this.gs.config().numSpawnPhaseTurns()
const ratio = this.game.ticks() / this.game.config().numSpawnPhaseTurns()
// Draw bar background
this.context.fillStyle = 'rgba(0, 0, 0, 0.5)';
@@ -135,19 +103,7 @@ export class GameRenderer {
}
tick() {
this.nameRenderer.tick()
}
tileUpdate(event: TileEvent) {
this.territoryRenderer.tileUpdate(event)
// this.tileToRenderQueue.push({tileEvent: event, lastUpdate: this.gs.ticks() + this.random.nextFloat(0, .5)})
}
playerEvent(event: PlayerEvent) {
}
boatEvent(event: BoatEvent) {
this.territoryRenderer.boatEvent(event)
this.layers.forEach(l => l.tick())
}
resize(width: number, height: number): void {
@@ -155,77 +111,4 @@ export class GameRenderer {
this.canvas.height = Math.ceil(height / window.devicePixelRatio);
}
paintCell(cell: Cell, color: Colord) {
color = color.alpha(10) // Assign the result back to color
this.territoryContext.fillStyle = color.toHslString()
this.territoryContext.fillRect(cell.x, cell.y, 1, 1);
}
clearCell(cell: Cell) {
this.territoryContext.clearRect(cell.x, cell.y, 1, 1);
}
onZoom(event: ZoomEvent) {
const oldScale = this.scale;
const zoomFactor = 1 + event.delta / 600;
this.scale /= zoomFactor;
// Clamp the scale to prevent extreme zooming
this.scale = Math.max(0.5, Math.min(20, this.scale));
const canvasRect = this.canvas.getBoundingClientRect();
const canvasX = event.x - canvasRect.left;
const canvasY = event.y - canvasRect.top;
// Calculate the world point we want to zoom towards
const zoomPointX = (canvasX - this.gs.width() / 2) / oldScale + this.offsetX;
const zoomPointY = (canvasY - this.gs.height() / 2) / oldScale + this.offsetY;
// Adjust the offset
this.offsetX = zoomPointX - (canvasX - this.gs.width() / 2) / this.scale;
this.offsetY = zoomPointY - (canvasY - this.gs.height() / 2) / this.scale;
}
onMove(event: DragEvent) {
this.offsetX -= event.deltaX / this.scale;
this.offsetY -= event.deltaY / this.scale;
}
screenToWorldCoordinates(screenX: number, screenY: number): Cell {
const canvasRect = this.canvas.getBoundingClientRect();
const canvasX = screenX - canvasRect.left;
const canvasY = screenY - canvasRect.top;
// Calculate the world point we want to zoom towards
const centerX = (canvasX - this.gs.width() / 2) / this.scale + this.offsetX;
const centerY = (canvasY - this.gs.height() / 2) / this.scale + this.offsetY;
const gameX = centerX + this.gs.width() / 2
const gameY = centerY + this.gs.height() / 2
return new Cell(Math.floor(gameX), Math.floor(gameY));
}
boundingRect(): [Cell, Cell] {
// Calculate the world point we want to zoom towards
const LeftX = (- this.gs.width() / 2) / this.scale + this.offsetX;
const TopY = (- this.gs.height() / 2) / this.scale + this.offsetY;
const gameLeftX = LeftX + this.gs.width() / 2
const gameTopY = TopY + this.gs.height() / 2
// Calculate the world point we want to zoom towards
const rightX = (screen.width - this.gs.width() / 2) / this.scale + this.offsetX;
const rightY = (screen.height - this.gs.height() / 2) / this.scale + this.offsetY;
const gameRightX = rightX + this.gs.width() / 2
const gameBottomY = rightY + this.gs.height() / 2
return [new Cell(Math.floor(gameLeftX), Math.floor(gameTopY)), new Cell(Math.floor(gameRightX), Math.floor(gameBottomY))]
}
}
+8
View File
@@ -0,0 +1,8 @@
import {TransformHandler} from "./TransformHandler"
export interface Layer {
init()
tick()
render(context: CanvasRenderingContext2D, transformHandler: TransformHandler)
shouldTransform(): boolean
}
+13 -6
View File
@@ -2,7 +2,9 @@ import {Cell, Game, Player, PlayerType} from "../../core/Game"
import {PseudoRandom} from "../../core/PseudoRandom"
import {calculateBoundingBox} from "../../core/Util"
import {Theme} from "../../core/configuration/Config"
import {Layer} from "./Layer"
import {placeName} from "./NameBoxCalculator"
import {TransformHandler} from "./TransformHandler"
import {renderTroops} from "./Utils"
class RenderInfo {
@@ -17,7 +19,7 @@ class RenderInfo {
) { }
}
export class NameRenderer {
export class NameRenderer implements Layer {
private lastChecked = 0
private refreshRate = 1000
@@ -32,9 +34,12 @@ export class NameRenderer {
constructor(private game: Game, private theme: Theme) {
}
shouldTransform(): boolean {
return true
}
public initialize() {
public init() {
this.canvas = document.createElement('canvas');
this.context = this.canvas.getContext('2d');
@@ -45,6 +50,7 @@ export class NameRenderer {
this.canvas.height = this.game.height();
}
// TODO: remove tick, move this to render
public tick() {
const now = Date.now()
if (now - this.lastChecked > this.refreshRate) {
@@ -74,11 +80,12 @@ export class NameRenderer {
}
}
public render(mainContex: CanvasRenderingContext2D, scale: number, uppperLeft: Cell, bottomRight: Cell) {
public render(mainContex: CanvasRenderingContext2D, transformHandler: TransformHandler) {
const [upperLeft, bottomRight] = transformHandler.screenBoundingRect()
for (const render of this.renders) {
render.isVisible = this.isVisible(render, uppperLeft, bottomRight)
if (render.player.isAlive() && render.isVisible && render.fontSize * scale > 10) {
this.renderPlayerInfo(render, mainContex, scale, uppperLeft, bottomRight)
render.isVisible = this.isVisible(render, upperLeft, bottomRight)
if (render.player.isAlive() && render.isVisible && render.fontSize * transformHandler.scale > 10) {
this.renderPlayerInfo(render, mainContex, transformHandler.scale, upperLeft, bottomRight)
}
}
}
+9 -2
View File
@@ -1,14 +1,21 @@
import {inherits} from "util"
import {Game} from "../../core/Game";
import {throws} from "assert";
import {Layer} from "./Layer";
import {TransformHandler} from "./TransformHandler";
export class TerrainRenderer {
export class TerrainRenderer implements Layer {
private canvas: HTMLCanvasElement
private context: CanvasRenderingContext2D
private imageData: ImageData
constructor(private game: Game) { }
shouldTransform(): boolean {
return true
}
tick() {
}
init() {
this.canvas = document.createElement('canvas');
@@ -34,7 +41,7 @@ export class TerrainRenderer {
})
}
draw(context: CanvasRenderingContext2D) {
render(context: CanvasRenderingContext2D, transformHandler: TransformHandler) {
context.drawImage(
this.canvas,
-this.game.width() / 2,
+13 -3
View File
@@ -4,8 +4,11 @@ import {PseudoRandom} from "../../core/PseudoRandom";
import {Colord} from "colord";
import {bfs, dist} from "../../core/Util";
import {Theme} from "../../core/configuration/Config";
import {Layer} from "./Layer";
import {TransformHandler} from "./TransformHandler";
import {EventBus} from "../../core/EventBus";
export class TerritoryRenderer {
export class TerritoryRenderer implements Layer {
private canvas: HTMLCanvasElement
private context: CanvasRenderingContext2D
private imageData: ImageData
@@ -17,8 +20,15 @@ export class TerritoryRenderer {
private boatToTrail = new Map<Boat, Set<Tile>>()
constructor(private game: Game) {
constructor(private game: Game, eventBus: EventBus) {
this.theme = game.config().theme()
eventBus.on(TileEvent, e => this.tileUpdate(e))
eventBus.on(BoatEvent, e => this.boatEvent(e))
}
shouldTransform(): boolean {
return true
}
tick() {
}
init() {
@@ -40,7 +50,7 @@ export class TerritoryRenderer {
})
}
draw(context: CanvasRenderingContext2D) {
render(context: CanvasRenderingContext2D, transformHandler: TransformHandler) {
this.renderTerritory()
this.context.putImageData(this.imageData, 0, 0);
context.drawImage(
+94
View File
@@ -0,0 +1,94 @@
import {EventBus} from "../../core/EventBus"
import {Cell, Game} from "../../core/Game";
import {ZoomEvent, DragEvent} from "../InputHandler";
export class TransformHandler {
public scale: number = 1.8
private offsetX: number = -350
private offsetY: number = -200
constructor(private game: Game, private eventBus: EventBus, private boundingRect: DOMRect) {
this.eventBus.on(ZoomEvent, (e) => this.onZoom(e))
this.eventBus.on(DragEvent, (e) => this.onMove(e))
}
handleTransform(context: CanvasRenderingContext2D) {
// Disable image smoothing for pixelated effect
if (this.scale > 3) {
context.imageSmoothingEnabled = false;
} else {
context.imageSmoothingEnabled = true;
}
// Apply zoom and pan
context.setTransform(
this.scale,
0,
0,
this.scale,
this.game.width() / 2 - this.offsetX * this.scale,
this.game.height() / 2 - this.offsetY * this.scale
);
}
screenToWorldCoordinates(screenX: number, screenY: number): Cell {
const canvasRect = this.boundingRect;
const canvasX = screenX - canvasRect.left;
const canvasY = screenY - canvasRect.top;
// Calculate the world point we want to zoom towards
const centerX = (canvasX - this.game.width() / 2) / this.scale + this.offsetX;
const centerY = (canvasY - this.game.height() / 2) / this.scale + this.offsetY;
const gameX = centerX + this.game.width() / 2
const gameY = centerY + this.game.height() / 2
return new Cell(Math.floor(gameX), Math.floor(gameY));
}
screenBoundingRect(): [Cell, Cell] {
// Calculate the world point we want to zoom towards
const LeftX = (- this.game.width() / 2) / this.scale + this.offsetX;
const TopY = (- this.game.height() / 2) / this.scale + this.offsetY;
const gameLeftX = LeftX + this.game.width() / 2
const gameTopY = TopY + this.game.height() / 2
// Calculate the world point we want to zoom towards
const rightX = (screen.width - this.game.width() / 2) / this.scale + this.offsetX;
const rightY = (screen.height - this.game.height() / 2) / this.scale + this.offsetY;
const gameRightX = rightX + this.game.width() / 2
const gameBottomY = rightY + this.game.height() / 2
return [new Cell(Math.floor(gameLeftX), Math.floor(gameTopY)), new Cell(Math.floor(gameRightX), Math.floor(gameBottomY))]
}
onZoom(event: ZoomEvent) {
const oldScale = this.scale;
const zoomFactor = 1 + event.delta / 600;
this.scale /= zoomFactor;
// Clamp the scale to prevent extreme zooming
this.scale = Math.max(0.5, Math.min(20, this.scale));
const canvasRect = this.boundingRect
const canvasX = event.x - canvasRect.left;
const canvasY = event.y - canvasRect.top;
// Calculate the world point we want to zoom towards
const zoomPointX = (canvasX - this.game.width() / 2) / oldScale + this.offsetX;
const zoomPointY = (canvasY - this.game.height() / 2) / oldScale + this.offsetY;
// Adjust the offset
this.offsetX = zoomPointX - (canvasX - this.game.width() / 2) / this.scale;
this.offsetY = zoomPointY - (canvasY - this.game.height() / 2) / this.scale;
}
onMove(event: DragEvent) {
this.offsetX -= event.deltaX / this.scale;
this.offsetY -= event.deltaY / this.scale;
}
}
+75 -4
View File
@@ -5,20 +5,61 @@ import {Game, Player} from "../../core/Game";
import {ClientID} from "../../core/Schemas";
import {renderTroops} from "./Utils";
import winModalHtml from '../WinModal.html';
import {RightClickEvent} from "../InputHandler";
import {Layer} from "./Layer";
import {TransformHandler} from "./TransformHandler";
export class UIRenderer {
interface MenuOption {
label: string;
action: () => void;
}
export class UIRenderer implements Layer {
private exitButton: HTMLButtonElement;
private winModal: HTMLElement | null = null;
private customMenu = document.getElementById('customMenu');
constructor(private eventBus: EventBus, private game: Game, private theme: Theme, private clientID: ClientID) {
}
render(context: CanvasRenderingContext2D, transformHandler: TransformHandler) {
}
shouldTransform(): boolean {
return false
}
tick() {
}
init() {
this.createExitButton()
this.createWinModal()
this.initRightClickMenu()
this.eventBus.on(WinEvent, (e) => this.onWinEvent(e))
this.eventBus.on(RightClickEvent, (e) => this.onRightClick(e))
}
initRightClickMenu() {
if (!this.customMenu) {
console.error('Custom menu not found');
return;
}
document.addEventListener('click', () => {
this.customMenu!.style.display = 'none';
});
const menuItems = this.customMenu.querySelectorAll('li');
menuItems.forEach(item => {
item.addEventListener('click', () => {
alert(`You clicked: ${item.textContent}`);
this.customMenu!.style.display = 'none';
});
});
}
createWinModal() {
@@ -124,9 +165,6 @@ export class UIRenderer {
document.body.appendChild(this.exitButton);
}
render(context: CanvasRenderingContext2D) {
}
onWinEvent(event: WinEvent) {
console.log(`${event.winner.name()} won the game!!}`)
@@ -165,4 +203,37 @@ export class UIRenderer {
window.location.reload();
}
private onRightClick(e: RightClickEvent) {
this.customMenu!.style.display = 'block';
this.customMenu!.style.left = `${e.x}px`;
this.customMenu!.style.top = `${e.y}px`;
}
private populateMenu(options: MenuOption[]) {
if (!this.customMenu) return;
// Clear existing menu items
this.customMenu.innerHTML = '';
// Create new menu items
const ul = document.createElement('ul');
options.forEach(option => {
const li = document.createElement('li');
li.textContent = option.label;
li.onclick = () => {
option.action();
this.hideMenu();
};
ul.appendChild(li);
});
this.customMenu.appendChild(ul);
}
private hideMenu() {
if (this.customMenu) {
this.customMenu.style.display = 'none';
}
}
}
+14
View File
@@ -14,4 +14,18 @@ export function renderTroops(troops: number): string {
troopsStr = String(Math.floor(troops))
}
return troopsStr
}
export function createCanvas(): HTMLCanvasElement {
const canvas = document.createElement('canvas');
// Set canvas style to fill the screen
canvas.style.position = 'fixed';
canvas.style.left = '0';
canvas.style.top = '0';
canvas.style.width = '100%';
canvas.style.height = '100%';
canvas.style.touchAction = 'none';
return canvas
}
+6
View File
@@ -41,6 +41,12 @@
<h3 id="next-game"> </h3>
</div>
<div id="customMenu">
<ul>
</ul>
</div>
<style>
body {
+24
View File
@@ -207,4 +207,28 @@ h3 {
width: 40px;
height: 40px;
}
}
#customMenu {
display: none;
position: absolute;
background-color: #f9f9f9;
border: 1px solid #ccc;
padding: 10px;
z-index: 1000;
}
#customMenu ul {
list-style-type: none;
margin: 0;
padding: 0;
}
#customMenu ul li {
padding: 8px 12px;
cursor: pointer;
}
#customMenu ul li:hover {
background-color: #f1f1f1;
}