@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;700&family=Syncopate:wght@700&display=swap');
* {
box-sizing: border-box;
font-family: "Inter", sans-serif;
margin: 0;
padding: 0;
}
body {
margin: 0;
overflow: hidden;
background-color: #0a0a0a;
user-select: none;
}
#kumonosu-ui-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
display: flex;
flex-direction: column;
align-items: center;
z-index: 10;
}
.kumonosu-top-bar {
margin-top: 30px;
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
padding: 10px 25px;
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.1);
pointer-events: auto;
display: flex;
gap: 20px;
align-items: center;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.5);
}
.kumonosu-bar-text {
color: rgba(255, 255, 255, 0.5);
font-size: 11px;
letter-spacing: 3px;
text-transform: uppercase;
font-weight: 700;
}
#kumonosu-dice-count {
padding: 6px 12px;
font-size: 14px;
border-radius: 2px;
border: 1px solid rgba(255, 255, 255, 0.2);
background: transparent;
color: #00e5ff;
cursor: pointer;
outline: none;
appearance: none;
-webkit-appearance: none;
font-weight: 700;
transition: all 0.2s ease;
text-align: center;
}
#kumonosu-dice-count:hover {
border-color: #00e5ff;
background: rgba(0, 229, 255, 0.1);
}
#kumonosu-result-board {
margin-top: 60px;
color: #ffffff;
font-family: "Syncopate", sans-serif;
font-size: 80px;
opacity: 0;
transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1);
text-align: center;
text-shadow: 0 0 20px rgba(0, 229, 255, 0.3);
transform: translateY(20px);
}
#kumonosu-result-board.kumonosu-show {
opacity: 1;
transform: translateY(0);
}
.kumonosu-sub-text {
font-family: "Inter", sans-serif;
font-size: 14px;
font-weight: 300;
color: rgba(255, 255, 255, 0.4);
display: block;
margin-top: 10px;
letter-spacing: 4px;
}
canvas {
display: block;
}
// 直接URLを指定することで単一のスクリプトタグに集約
import * as THREE from "https://esm.sh/three@0.160.0";
import {
RoundedBoxGeometry
} from "https://esm.sh/three@0.160.0/examples/jsm/geometries/RoundedBoxGeometry.js";
import * as CANNON from "https://esm.sh/cannon-es@0.20.0";
let scene, camera, renderer, world;
let diceObjects = [];
let isHolding = false;
let needsResultCheck = false;
let mouse = new THREE.Vector2();
let raycaster = new THREE.Raycaster();
const FRUSTUM_SIZE = 23;
let dragPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), -15);
const uiResult = document.getElementById("kumonosu-result-board");
const uiTotal = document.getElementById("kumonosu-total-score");
const uiDetail = document.getElementById("kumonosu-detail-score");
const palette = ["#1a1a1a", "#222222", "#2a2a2a", "#333333"];
const commonColors = {
dots: "#ffffff",
outline: "#444444",
shadow: "#000000"
};
init();
animate();
function init() {
scene = new THREE.Scene();
scene.background = new THREE.Color("#0a0a0a");
const aspect = window.innerWidth / window.innerHeight;
camera = new THREE.OrthographicCamera(
(FRUSTUM_SIZE * aspect) / -2,
(FRUSTUM_SIZE * aspect) / 2, FRUSTUM_SIZE / 2, FRUSTUM_SIZE / -2, 1, 1000);
camera.position.set(50, 50, 50);
camera.lookAt(0, 0, 0);
renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
renderer.domElement.style.touchAction = 'none';
document.body.appendChild(renderer.domElement);
world = new CANNON.World();
world.gravity.set(0, -40, 0);
world.broadphase = new CANNON.NaiveBroadphase();
world.solver.iterations = 20;
world.allowSleep = true;
const wallMat = new CANNON.Material();
const diceMat = new CANNON.Material();
world.addContactMaterial(new CANNON.ContactMaterial(wallMat, diceMat, {
friction: 0.1,
restitution: 0.5
}));
createPhysicsWalls(wallMat);
updateDiceCount(3);
window.addEventListener("resize", onWindowResize);
window.addEventListener("mousedown", onInputStart);
window.addEventListener("mousemove", onInputMove);
window.addEventListener("mouseup", onInputEnd);
window.addEventListener("touchstart", onInputStart, {
passive: false
});
window.addEventListener("touchmove", onInputMove, {
passive: false
});
window.addEventListener("touchend", onInputEnd);
const countSelect = document.getElementById("kumonosu-dice-count");
if (countSelect) {
countSelect.addEventListener("change", (e) => {
updateDiceCount(parseInt(e.target.value));
});
}
}
function updateMousePosition(e) {
let x, y;
if (e.changedTouches) {
x = e.changedTouches[0].clientX;
y = e.changedTouches[0].clientY;
} else {
x = e.clientX;
y = e.clientY;
}
mouse.x = (x / window.innerWidth) * 2 - 1;
mouse.y = -(y / window.innerHeight) * 2 + 1;
}
function onInputStart(e) {
if (e.target.tagName === "SELECT" || e.target.closest(".kumonosu-top-bar")) return;
if (e.cancelable) e.preventDefault();
isHolding = true;
needsResultCheck = false;
if (uiResult) uiResult.classList.remove("kumonosu-show");
updateMousePosition(e);
diceObjects.forEach(obj => {
obj.body.wakeUp();
obj.spinOffset = Math.random() * 100;
obj.isReturning = false;
});
}
function onInputMove(e) {
if (!isHolding) return;
if (e.cancelable) e.preventDefault();
updateMousePosition(e);
}
function onInputEnd() {
if (!isHolding) return;
isHolding = false;
releaseDice();
}
function createPhysicsWalls(material) {
const floorBody = new CANNON.Body({
mass: 0,
material: material
});
floorBody.addShape(new CANNON.Plane());
floorBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1, 0, 0), -Math.PI / 2);
world.addBody(floorBody);
const wallDistance = 12;
const createWall = (x, z, rot) => {
const body = new CANNON.Body({
mass: 0,
material: material
});
body.addShape(new CANNON.Plane());
body.position.set(x, 0, z);
body.quaternion.setFromAxisAngle(new CANNON.Vec3(0, 1, 0), rot);
world.addBody(body);
};
createWall(wallDistance, 0, -Math.PI / 2);
createWall(-wallDistance, 0, Math.PI / 2);
createWall(0, -wallDistance, 0);
createWall(0, wallDistance, Math.PI);
}
function createVectorDiceTexture(number, colorHex) {
const size = 256;
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
ctx.fillStyle = colorHex;
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = "#ffffff";
const dotSize = size / 6.5;
const center = size / 2;
const q1 = size / 4;
const q3 = (size * 3) / 4;
function drawDot(x, y) {
ctx.beginPath();
ctx.arc(x, y, dotSize / 2, 0, Math.PI * 2);
ctx.fill();
}
if (number === 1) drawDot(center, center);
else if (number === 2) {
drawDot(q1, q1);
drawDot(q3, q3);
} else if (number === 3) {
drawDot(q1, q1);
drawDot(center, center);
drawDot(q3, q3);
} else if (number === 4) {
drawDot(q1, q1);
drawDot(q3, q1);
drawDot(q1, q3);
drawDot(q3, q3);
} else if (number === 5) {
drawDot(q1, q1);
drawDot(q3, q1);
drawDot(center, center);
drawDot(q1, q3);
drawDot(q3, q3);
} else if (number === 6) {
drawDot(q1, q1);
drawDot(q3, q1);
drawDot(q1, center);
drawDot(q3, center);
drawDot(q1, q3);
drawDot(q3, q3);
}
const tex = new THREE.CanvasTexture(canvas);
tex.anisotropy = renderer.capabilities.getMaxAnisotropy();
return tex;
}
function updateDiceCount(count) {
diceObjects.forEach((obj) => {
scene.remove(obj.mesh);
scene.remove(obj.outline);
scene.remove(obj.shadow);
world.removeBody(obj.body);
});
diceObjects = [];
if (uiResult) uiResult.classList.remove("kumonosu-show");
const boxSize = 2.5;
const geometry = new RoundedBoxGeometry(boxSize, boxSize, boxSize, 3, 0.25);
const outlineGeo = geometry.clone();
const shadowGeo = new THREE.CircleGeometry(boxSize * 0.6, 32);
const shape = new CANNON.Box(new CANNON.Vec3(boxSize / 2, boxSize / 2, boxSize / 2));
const outlineMat = new THREE.MeshBasicMaterial({
color: commonColors.outline,
side: THREE.BackSide
});
const shadowMat = new THREE.MeshBasicMaterial({
color: commonColors.shadow,
transparent: true,
opacity: 0.4
});
for (let i = 0; i < count; i++) {
let diceColor;
if (i === 0) {
diceColor = "#9b0000";
} else if (i === 1) {
diceColor = "#002a7a";
} else {
diceColor = palette[Math.floor(Math.random() * palette.length)];
}
const diceMaterials = [];
for (let j = 1; j <= 6; j++) {
diceMaterials.push(new THREE.MeshBasicMaterial({
map: createVectorDiceTexture(j, diceColor)
}));
}
const matArray = [
diceMaterials[0], diceMaterials[5], diceMaterials[1],
diceMaterials[4], diceMaterials[2], diceMaterials[3]
];
const mesh = new THREE.Mesh(geometry, matArray);
scene.add(mesh);
const outline = new THREE.Mesh(outlineGeo, outlineMat);
outline.position.copy(mesh.position);
outline.scale.setScalar(1.02);
scene.add(outline);
const shadow = new THREE.Mesh(shadowGeo, shadowMat);
shadow.rotation.x = -Math.PI / 2;
shadow.position.y = 0.01;
scene.add(shadow);
const startX = (i - (count - 1) / 2) * 3.5;
const body = new CANNON.Body({
mass: 5,
shape: shape,
position: new CANNON.Vec3(startX, boxSize, 0),
sleepSpeedLimit: 0.5
});
body.quaternion.setFromEuler(Math.random() * Math.PI, Math.random() * Math.PI, Math.random() * Math.PI);
world.addBody(body);
diceObjects.push({
mesh,
outline,
shadow,
body,
spinOffset: 0,
isReturning: false
});
}
}
function releaseDice() {
const SAFE_LIMIT = 9;
diceObjects.forEach((obj) => {
const {
body
} = obj;
const isOutside = Math.abs(body.position.x) > SAFE_LIMIT || Math.abs(body.position.z) > SAFE_LIMIT;
if (isOutside) {
obj.isReturning = true;
} else {
body.wakeUp();
applyThrowForce(body);
}
});
setTimeout(() => {
needsResultCheck = true;
}, 500);
}
function applyThrowForce(body) {
const xDist = -body.position.x;
const zDist = -body.position.z;
body.velocity.set(xDist * 1.8 + (Math.random() - 0.5) * 20, -20 - Math.random() * 10, zDist * 1.8 + (Math.random() - 0.5) * 20);
body.angularVelocity.set(
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 40);
}
function calculateResult() {
let total = 0;
let details = [];
const faceNormals = [
new THREE.Vector3(1, 0, 0), new THREE.Vector3(-1, 0, 0),
new THREE.Vector3(0, 1, 0), new THREE.Vector3(0, -1, 0),
new THREE.Vector3(0, 0, 1), new THREE.Vector3(0, 0, -1)
];
const faceValues = [1, 6, 2, 5, 3, 4];
diceObjects.forEach(({
mesh
}) => {
let maxDot = -Infinity;
let resultValue = 1;
faceNormals.forEach((normal, index) => {
const worldNormal = normal.clone().applyQuaternion(mesh.quaternion);
if (worldNormal.y > maxDot) {
maxDot = worldNormal.y;
resultValue = faceValues[index];
}
});
total += resultValue;
details.push(resultValue);
});
if (uiTotal) uiTotal.innerText = total;
if (uiDetail) uiDetail.innerText = details.length > 1 ? `LOG: [ ${details.join(" | ")} ]` : "";
if (uiResult) uiResult.classList.add("kumonosu-show");
needsResultCheck = false;
}
function animate() {
requestAnimationFrame(animate);
if (isHolding) {
raycaster.setFromCamera(mouse, camera);
const targetPoint = new THREE.Vector3();
const intersect = raycaster.ray.intersectPlane(dragPlane, targetPoint);
if (intersect) {
const time = performance.now() * 0.01;
diceObjects.forEach((obj, i) => {
const offsetX = Math.sin(time + i) * 1.2;
const offsetZ = Math.cos(time + i * 2) * 1.2;
obj.body.position.x += (targetPoint.x + offsetX - obj.body.position.x) * 0.2;
obj.body.position.y += (15 - obj.body.position.y) * 0.2;
obj.body.position.z += (targetPoint.z + offsetZ - obj.body.position.z) * 0.2;
obj.body.quaternion.setFromEuler(time * 3 + obj.spinOffset, time * 2, time * 1.5);
obj.body.velocity.set(0, 0, 0);
obj.body.angularVelocity.set(0, 0, 0);
obj.isReturning = false;
});
}
} else {
const time = performance.now() * 0.01;
diceObjects.forEach((obj) => {
if (obj.isReturning) {
obj.body.position.x += (0 - obj.body.position.x) * 0.15;
obj.body.position.z += (0 - obj.body.position.z) * 0.15;
obj.body.position.y += (12 - obj.body.position.y) * 0.1;
obj.body.quaternion.setFromEuler(time * 5, time * 5, 0);
obj.body.velocity.set(0, 0, 0);
obj.body.angularVelocity.set(0, 0, 0);
if (Math.abs(obj.body.position.x) < 9 && Math.abs(obj.body.position.z) < 9) {
obj.isReturning = false;
obj.body.wakeUp();
applyThrowForce(obj.body);
}
}
});
world.step(1 / 60);
}
for (let i = 0; i < diceObjects.length; i++) {
const {
mesh,
outline,
shadow,
body
} = diceObjects[i];
mesh.position.copy(body.position);
mesh.quaternion.copy(body.quaternion);
outline.position.copy(mesh.position);
outline.quaternion.copy(mesh.quaternion);
shadow.position.x = body.position.x;
shadow.position.z = body.position.z;
const height = Math.max(0, body.position.y - 1);
const scale = Math.max(0.4, 1 - height * 0.05);
const opacity = Math.max(0, 0.4 - height * 0.02);
shadow.scale.setScalar(scale);
shadow.material.opacity = opacity;
}
if (needsResultCheck) {
let allStopped = true;
for (let o of diceObjects) {
if (o.isReturning) {
allStopped = false;
break;
}
if (o.body.velocity.lengthSquared() > 0.1 || o.body.angularVelocity.lengthSquared() > 0.1) {
allStopped = false;
break;
}
}
if (allStopped) calculateResult();
}
renderer.render(scene, camera);
}
function onWindowResize() {
const aspect = window.innerWidth / window.innerHeight;
camera.left = (-FRUSTUM_SIZE * aspect) / 2;
camera.right = (FRUSTUM_SIZE * aspect) / 2;
camera.top = FRUSTUM_SIZE / 2;
camera.bottom = -FRUSTUM_SIZE / 2;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}