feat(html/js): setup canvas for both babylon and p5js

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
pobadoba
2026-05-06 00:33:08 +09:00
parent 66f5f35a9e
commit 2acb724e1f
3 changed files with 309 additions and 48 deletions

View File

@@ -1,61 +1,68 @@
html,
body {
margin: 0;
padding: 5px;
align-items: center;
text-align: center;
min-height: 100%;
}
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 {
display: block;
}
#one {
display: block;
width: 800px;
height: 600px;
.maze-layout {
width: min(1100px, calc(100vw - 24px));
margin: 12px auto 20px;
display: grid;
gap: 12px;
}
#two {
display: block;
margin-top: 10px;
width: 800px;
height: 600px;
.panel {
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
background: rgba(7, 12, 18, 0.72);
box-shadow: 0 18px 40px rgba(0, 0, 0, 0.35);
overflow: hidden;
}
/* Options in index.html */
.options {
display: inline-block;
clear: both;
margin-left: auto;
margin-right: auto;
text-align: center;
.panel-label {
padding: 10px 14px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
font-size: 12px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #93a4b8;
}
.option {
float: left;
padding: 50px;
#renderCanvas {
width: 100%;
height: min(62vh, 680px);
}
.emoji {
font-size: 100px;
vertical-align: middle;
line-height: 2;
clear: both;
margin: auto;
#p5-panel {
display: flex;
justify-content: center;
padding: 8px 0 14px;
}
.description {
margin: auto;
margin-top: -50px;
font-family: 'Times New Roman', Times, serif;
font-size: 25px;
letter-spacing: 0.6px;
word-spacing: 2px;
color: #000000;
font-weight: normal;
text-decoration: none;
font-style: normal;
font-variant: small-caps;
text-transform: none;
#p5-panel canvas {
width: min(100%, 1000px);
height: auto;
border-radius: 12px;
}
@media (max-width: 720px) {
.maze-layout {
width: calc(100vw - 16px);
margin: 8px auto 16px;
}
#renderCanvas {
height: 52vh;
}
}

View File

@@ -2,12 +2,24 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" type="text/css" href="/css/style.css" />
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite App</title>
<title>Untitled Maze Game</title>
</head>
<body>
<body class="maze-page">
<main class="maze-layout">
<section class="panel">
<div class="panel-label">Babylon Scene</div>
<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>
<canvas id="renderCanvas"></canvas>
</body>
</html>

View File

@@ -1,16 +1,258 @@
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 engine = new BABYLON.Engine(canvas, true);
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);
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(() => {
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();
});
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;
};