feat(html/js): setup canvas for both babylon and p5js
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
@@ -1,61 +1,68 @@
|
|||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 5px;
|
min-height: 100%;
|
||||||
align-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.maze-page {
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top, #1c2733 0%, #0a0f15 55%, #06080c 100%);
|
||||||
|
color: #e8eef6;
|
||||||
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
canvas {
|
canvas {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
#one {
|
.maze-layout {
|
||||||
display: block;
|
width: min(1100px, calc(100vw - 24px));
|
||||||
width: 800px;
|
margin: 12px auto 20px;
|
||||||
height: 600px;
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#two {
|
.panel {
|
||||||
display: block;
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
margin-top: 10px;
|
border-radius: 16px;
|
||||||
width: 800px;
|
background: rgba(7, 12, 18, 0.72);
|
||||||
height: 600px;
|
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.35);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Options in index.html */
|
.panel-label {
|
||||||
.options {
|
padding: 10px 14px;
|
||||||
display: inline-block;
|
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
clear: both;
|
font-size: 12px;
|
||||||
margin-left: auto;
|
letter-spacing: 0.14em;
|
||||||
margin-right: auto;
|
text-transform: uppercase;
|
||||||
text-align: center;
|
color: #93a4b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option {
|
#renderCanvas {
|
||||||
float: left;
|
width: 100%;
|
||||||
padding: 50px;
|
height: min(62vh, 680px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.emoji {
|
#p5-panel {
|
||||||
font-size: 100px;
|
display: flex;
|
||||||
vertical-align: middle;
|
justify-content: center;
|
||||||
line-height: 2;
|
padding: 8px 0 14px;
|
||||||
clear: both;
|
|
||||||
margin: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
#p5-panel canvas {
|
||||||
margin: auto;
|
width: min(100%, 1000px);
|
||||||
margin-top: -50px;
|
height: auto;
|
||||||
|
border-radius: 12px;
|
||||||
font-family: 'Times New Roman', Times, serif;
|
}
|
||||||
font-size: 25px;
|
|
||||||
letter-spacing: 0.6px;
|
@media (max-width: 720px) {
|
||||||
word-spacing: 2px;
|
.maze-layout {
|
||||||
color: #000000;
|
width: calc(100vw - 16px);
|
||||||
font-weight: normal;
|
margin: 8px auto 16px;
|
||||||
text-decoration: none;
|
}
|
||||||
font-style: normal;
|
|
||||||
font-variant: small-caps;
|
#renderCanvas {
|
||||||
text-transform: none;
|
height: 52vh;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,24 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/style.css" />
|
||||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Vite App</title>
|
<title>Untitled Maze Game</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="maze-page">
|
||||||
<script type="module" src="/src/multi_sketch.js"></script>
|
<main class="maze-layout">
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-label">Babylon Scene</div>
|
||||||
<canvas id="renderCanvas"></canvas>
|
<canvas id="renderCanvas"></canvas>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-label">p5 Control Panel</div>
|
||||||
|
<div id="p5-panel"></div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script type="module" src="/src/multi_sketch.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,16 +1,258 @@
|
|||||||
import * as BABYLON from "babylonjs";
|
import * as BABYLON from "babylonjs";
|
||||||
|
import { sketch } from "p5js-wrapper";
|
||||||
|
|
||||||
|
const sharedState = (window.mazeGameState ??= {
|
||||||
|
config: {
|
||||||
|
seed: Math.floor(Math.random() * 100000),
|
||||||
|
level: 1,
|
||||||
|
mazeWidth: 11,
|
||||||
|
mazeHeight: 11,
|
||||||
|
minChestDeadEnds: 2,
|
||||||
|
},
|
||||||
|
runtime: {
|
||||||
|
runActive: false,
|
||||||
|
hasKey: false,
|
||||||
|
elapsedSeconds: 0,
|
||||||
|
message: "Adjust settings, then start a run.",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const canvas = document.getElementById("renderCanvas");
|
const canvas = document.getElementById("renderCanvas");
|
||||||
const engine = new BABYLON.Engine(canvas, true);
|
const engine = new BABYLON.Engine(canvas, true);
|
||||||
|
|
||||||
const scene = new BABYLON.Scene(engine);
|
const scene = new BABYLON.Scene(engine);
|
||||||
const camera = new BABYLON.ArcRotateCamera("cam", 0, 0, 10, BABYLON.Vector3.Zero(), scene);
|
scene.clearColor = new BABYLON.Color4(0.05, 0.07, 0.1, 1);
|
||||||
|
|
||||||
|
const camera = new BABYLON.ArcRotateCamera(
|
||||||
|
"cam",
|
||||||
|
-Math.PI / 2,
|
||||||
|
Math.PI / 2.4,
|
||||||
|
10,
|
||||||
|
BABYLON.Vector3.Zero(),
|
||||||
|
scene,
|
||||||
|
);
|
||||||
camera.attachControl(canvas, true);
|
camera.attachControl(canvas, true);
|
||||||
|
|
||||||
const light = new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
|
new BABYLON.HemisphericLight("light", new BABYLON.Vector3(0, 1, 0), scene);
|
||||||
|
|
||||||
const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", {}, scene);
|
const sphere = BABYLON.MeshBuilder.CreateSphere("sphere", { diameter: 2 }, scene);
|
||||||
|
const sphereMaterial = new BABYLON.StandardMaterial("sphereMaterial", scene);
|
||||||
|
sphereMaterial.diffuseColor = new BABYLON.Color3(0.2, 0.55, 0.95);
|
||||||
|
sphereMaterial.emissiveColor = new BABYLON.Color3(0.05, 0.12, 0.2);
|
||||||
|
sphere.material = sphereMaterial;
|
||||||
|
|
||||||
|
const ground = BABYLON.MeshBuilder.CreateGround("ground", { width: 14, height: 14 }, scene);
|
||||||
|
const groundMaterial = new BABYLON.StandardMaterial("groundMaterial", scene);
|
||||||
|
groundMaterial.diffuseColor = new BABYLON.Color3(0.12, 0.14, 0.17);
|
||||||
|
groundMaterial.specularColor = BABYLON.Color3.Black();
|
||||||
|
ground.material = groundMaterial;
|
||||||
|
|
||||||
engine.runRenderLoop(() => {
|
engine.runRenderLoop(() => {
|
||||||
|
const level = sharedState.config.level;
|
||||||
|
sphere.rotation.y += 0.01;
|
||||||
|
sphere.scaling.x = 1 + (level - 1) * 0.05;
|
||||||
|
sphere.scaling.z = 1 + (level - 1) * 0.05;
|
||||||
|
sphereMaterial.diffuseColor = sharedState.runtime.hasKey
|
||||||
|
? new BABYLON.Color3(0.25, 0.8, 0.45)
|
||||||
|
: new BABYLON.Color3(0.2, 0.55, 0.95);
|
||||||
scene.render();
|
scene.render();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
engine.resize();
|
||||||
|
});
|
||||||
|
|
||||||
|
const panelSize = { width: 1000, height: 270 };
|
||||||
|
const buttonRects = {};
|
||||||
|
const sliderRects = {};
|
||||||
|
let draggingSlider = null;
|
||||||
|
|
||||||
|
function clamp(value, min, max) {
|
||||||
|
return Math.min(max, Math.max(min, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function valueToX(rect, min, max, value) {
|
||||||
|
const ratio = (value - min) / (max - min);
|
||||||
|
return rect.x + ratio * rect.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
function xToValue(rect, min, max, x) {
|
||||||
|
const ratio = clamp((x - rect.x) / rect.width, 0, 1);
|
||||||
|
return Math.round(min + ratio * (max - min));
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetRun(message) {
|
||||||
|
sharedState.runtime.runActive = true;
|
||||||
|
sharedState.runtime.hasKey = false;
|
||||||
|
sharedState.runtime.elapsedSeconds = 0;
|
||||||
|
sharedState.runtime.message = message;
|
||||||
|
sharedState.config.level = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function restartLevel(message) {
|
||||||
|
sharedState.runtime.hasKey = false;
|
||||||
|
sharedState.runtime.elapsedSeconds = 0;
|
||||||
|
sharedState.runtime.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function randomizeSeed() {
|
||||||
|
sharedState.config.seed = Math.floor(Math.random() * 100000);
|
||||||
|
sharedState.runtime.message = `Seed set to ${sharedState.config.seed}.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawButton(rect, label, active = false) {
|
||||||
|
push();
|
||||||
|
stroke(active ? "#7db4ff" : "#284055");
|
||||||
|
strokeWeight(2);
|
||||||
|
fill(active ? "#17314c" : "#101820");
|
||||||
|
rect(rect.x, rect.y, rect.w, rect.h, 12);
|
||||||
|
noStroke();
|
||||||
|
fill("#eef5ff");
|
||||||
|
textAlign(CENTER, CENTER);
|
||||||
|
textSize(16);
|
||||||
|
text(label, rect.x + rect.w / 2, rect.y + rect.h / 2 + 1);
|
||||||
|
pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawSlider(rect, label, value, min, max, suffix = "") {
|
||||||
|
const knobX = valueToX(rect, min, max, value);
|
||||||
|
push();
|
||||||
|
fill("#dbe6f2");
|
||||||
|
noStroke();
|
||||||
|
textAlign(LEFT, BOTTOM);
|
||||||
|
textSize(15);
|
||||||
|
text(`${label}: ${value}${suffix}`, rect.x, rect.y - 8);
|
||||||
|
stroke("#33495e");
|
||||||
|
strokeWeight(6);
|
||||||
|
line(rect.x, rect.y + rect.h / 2, rect.x + rect.w, rect.y + rect.h / 2);
|
||||||
|
noStroke();
|
||||||
|
fill("#79aef2");
|
||||||
|
circle(knobX, rect.y + rect.h / 2, 20);
|
||||||
|
pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
sketch.setup = function () {
|
||||||
|
createCanvas(panelSize.width, panelSize.height).parent("p5-panel");
|
||||||
|
pixelDensity(1);
|
||||||
|
textFont("monospace");
|
||||||
|
noLoop();
|
||||||
|
};
|
||||||
|
|
||||||
|
sketch.draw = function () {
|
||||||
|
background("#091018");
|
||||||
|
|
||||||
|
noStroke();
|
||||||
|
fill("#dce7f4");
|
||||||
|
textAlign(LEFT, TOP);
|
||||||
|
textSize(18);
|
||||||
|
text("Maze game settings", 18, 14);
|
||||||
|
|
||||||
|
fill("#91a4b8");
|
||||||
|
textSize(12);
|
||||||
|
text(
|
||||||
|
"This panel controls run settings and mirrors the shared state for the Babylon scene.",
|
||||||
|
18,
|
||||||
|
38,
|
||||||
|
);
|
||||||
|
|
||||||
|
const buttonY = 64;
|
||||||
|
buttonRects.start = { x: 18, y: buttonY, w: 140, h: 38 };
|
||||||
|
buttonRects.restart = { x: 168, y: buttonY, w: 140, h: 38 };
|
||||||
|
buttonRects.seed = { x: 318, y: buttonY, w: 160, h: 38 };
|
||||||
|
|
||||||
|
drawButton(buttonRects.start, "Start run", sharedState.runtime.runActive);
|
||||||
|
drawButton(buttonRects.restart, "Restart level");
|
||||||
|
drawButton(buttonRects.seed, "Randomize seed");
|
||||||
|
|
||||||
|
sliderRects.width = { x: 18, y: 154, w: 420, h: 22 };
|
||||||
|
sliderRects.height = { x: 18, y: 206, w: 420, h: 22 };
|
||||||
|
sliderRects.deadEnds = { x: 500, y: 154, w: 420, h: 22 };
|
||||||
|
|
||||||
|
drawSlider(sliderRects.width, "Maze width", sharedState.config.mazeWidth, 9, 31, " cells");
|
||||||
|
drawSlider(sliderRects.height, "Maze height", sharedState.config.mazeHeight, 9, 31, " cells");
|
||||||
|
drawSlider(
|
||||||
|
sliderRects.deadEnds,
|
||||||
|
"Minimum chest dead-ends",
|
||||||
|
sharedState.config.minChestDeadEnds,
|
||||||
|
1,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
|
||||||
|
fill("#dce7f4");
|
||||||
|
textSize(14);
|
||||||
|
textAlign(LEFT, TOP);
|
||||||
|
text(`Seed: ${sharedState.config.seed}`, 500, 196);
|
||||||
|
text(`Level: ${sharedState.config.level}`, 500, 220);
|
||||||
|
text(`Time: ${sharedState.runtime.elapsedSeconds.toFixed(1)}s`, 640, 220);
|
||||||
|
text(`Key: ${sharedState.runtime.hasKey ? "yes" : "no"}`, 780, 220);
|
||||||
|
|
||||||
|
fill("#93a4b8");
|
||||||
|
textSize(12);
|
||||||
|
text(`Status: ${sharedState.runtime.message}`, 18, 242, 960);
|
||||||
|
};
|
||||||
|
|
||||||
|
sketch.mousePressed = function () {
|
||||||
|
const x = mouseX;
|
||||||
|
const y = mouseY;
|
||||||
|
|
||||||
|
const isInside = (rect) => x >= rect.x && x <= rect.x + rect.w && y >= rect.y && y <= rect.y + rect.h;
|
||||||
|
|
||||||
|
if (isInside(buttonRects.start)) {
|
||||||
|
resetRun("Run started.");
|
||||||
|
redraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInside(buttonRects.restart)) {
|
||||||
|
restartLevel("Level restarted.");
|
||||||
|
redraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInside(buttonRects.seed)) {
|
||||||
|
randomizeSeed();
|
||||||
|
redraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInside(sliderRects.width)) {
|
||||||
|
draggingSlider = "width";
|
||||||
|
sharedState.config.mazeWidth = xToValue(sliderRects.width, 9, 31, x) | 1;
|
||||||
|
redraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInside(sliderRects.height)) {
|
||||||
|
draggingSlider = "height";
|
||||||
|
sharedState.config.mazeHeight = xToValue(sliderRects.height, 9, 31, x) | 1;
|
||||||
|
redraw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isInside(sliderRects.deadEnds)) {
|
||||||
|
draggingSlider = "deadEnds";
|
||||||
|
sharedState.config.minChestDeadEnds = xToValue(sliderRects.deadEnds, 1, 10, x);
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sketch.mouseDragged = function () {
|
||||||
|
if (draggingSlider === "width") {
|
||||||
|
sharedState.config.mazeWidth = xToValue(sliderRects.width, 9, 31, mouseX) | 1;
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draggingSlider === "height") {
|
||||||
|
sharedState.config.mazeHeight = xToValue(sliderRects.height, 9, 31, mouseX) | 1;
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (draggingSlider === "deadEnds") {
|
||||||
|
sharedState.config.minChestDeadEnds = xToValue(sliderRects.deadEnds, 1, 10, mouseX);
|
||||||
|
redraw();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
sketch.mouseReleased = function () {
|
||||||
|
draggingSlider = null;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user