<!DOCTYPE html>
<html lang="ja" >
<head>
<meta charset="UTF-8" >
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" >
<title>Crystal Physics Match</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/matter-js/0.19.0/matter.min.js" ></script>
<style>
body { margin: 0 ; padding: 0 ; overflow: hidden; background-color: #050510; color: #fff; font-family: 'Segoe UI', sans-serif; user-select: none; -webkit-user-select: none; }
#score-board { position: absolute; top: 20px; left: 20px; font-size: 24px; font-weight: bold; text-shadow: 0 0 10px rgba(255,255,255,0.8); pointer-events: none; z-index: 10; }
#combo-text { position: absolute; top: 0; left: 0; font-size: 24px; color: #fff; opacity: 0; pointer-events: none; font-weight: 800; text-shadow: 0 0 20px #fff; z-index: 10; transition: transform 0.1s; }
#hint-text { position: absolute; bottom: 80px; width: 100%; text-align: center; color: rgba(255,255,255,0.5); pointer-events: none; font-size: 14px; }
#ui-layer { position: absolute; bottom: 20px; width: 100%; text-align: center; pointer-events: none; z-index: 10; }
.btn { pointer-events: auto; background: rgba(255 ,255 ,255 ,0.1 ); border: 1 px solid rgba (255 ,255 ,255 ,0.4 ) ; color: #fff; padding: 12px 40px; border-radius: 40px; cursor: pointer; font-size: 16px; transition: 0.2s; backdrop-filter: blur(10px); text-transform: uppercase; letter-spacing: 2px; }
.btn:hover { background: rgba(255 ,255 ,255 ,0.4 ); box-shadow: 0 0 20 px rgba (255 ,255 ,255 ,0.6 ) ; transform: scale(1.05 ); }
canvas { display: block; }
</style>
</head>
<body>
<div id="score-board" >SCORE: <span id="score" >0 </span></div>
<div id="combo-text" >COMBO!</div>
<div id="hint-text" >3 つ以上つながったクリスタルをタップして破壊</div>
<div id="ui-layer" >
<button class ="btn" onclick="resetGame()" >RESET</button>
</div>
<script>
const CONFIG = {
ballSize: 26 ,
colors: ['#00FFFF' , '#FF00CC' , '#4444FF' , '#FFFF00' ],
glowColor: ['#AAFFFF' , '#FF99EE' , '#AAAAFF' , '#FFFFDD' ],
matchDistanceRatio: 2.3 ,
minMatch: 3
};
const Engine = Matter.Engine,
Render = Matter.Render,
Runner = Matter.Runner,
Bodies = Matter.Bodies,
Composite = Matter.Composite,
Events = Matter.Events,
Query = Matter.Query,
Vector = Matter.Vector,
World = Matter.World;
const engine = Engine.create();
const world = engine.world;
const render = Render.create({
element: document.body,
engine: engine,
options: {
width: window.innerWidth,
height: window.innerHeight,
wireframes: false ,
background: '#050510'
}
});
const ctx = render.context;
const canvas = render.canvas;
let score = 0 ;
let particles = [];
let balls = [];
let hoverMatches = [];
const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
function createNoiseBuffer ( ) {
const bufferSize = audioCtx.sampleRate * 0.1 ;
const buffer = audioCtx.createBuffer(1 , bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0 );
for (let i = 0 ; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1 ;
}
return buffer;
}
const noiseBuffer = createNoiseBuffer();
function playCollisionSound (volume ) {
if (audioCtx.state === 'suspended' ) audioCtx.resume();
if (volume < 0.05 ) return ;
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sine' ;
osc.frequency.setValueAtTime(2000 + Math.random() * 1000 , t);
gain.gain.setValueAtTime(0 , t);
gain.gain.linearRampToValueAtTime(volume * 0.3 , t + 0.005 );
gain.gain.exponentialRampToValueAtTime(0.001 , t + 0.1 );
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(t);
osc.stop(t + 0.1 );
}
function playShatterSound (comboSize ) {
if (audioCtx.state === 'suspended' ) audioCtx.resume();
const t = audioCtx.currentTime;
const noise = audioCtx.createBufferSource();
noise.buffer = noiseBuffer;
const noiseGain = audioCtx.createGain();
noiseGain.gain.setValueAtTime(0.5 , t);
noiseGain.gain.exponentialRampToValueAtTime(0.01 , t + 0.1 );
noise.connect(noiseGain);
noiseGain.connect(audioCtx.destination);
noise.start(t);
const count = Math.min(comboSize, 5 );
for (let i = 0 ; i < count; i++) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle' ;
const baseFreq = 880 ;
const ratios = [1 , 1.25 , 1.5 , 2 , 2.5 ];
osc.frequency.value = baseFreq * ratios[i % ratios.length] * (1 + Math.random()*0.02 );
const delay = i * 0.05 ;
gain.gain.setValueAtTime(0 , t + delay);
gain.gain.linearRampToValueAtTime(0.1 , t + delay + 0.01 );
gain.gain.exponentialRampToValueAtTime(0.001 , t + delay + 0.5 );
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(t + delay);
osc.stop(t + delay + 0.6 );
}
}
function playErrorSound ( ) {
if (audioCtx.state === 'suspended' ) audioCtx.resume();
const t = audioCtx.currentTime;
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth' ;
osc.frequency.setValueAtTime(150 , t);
osc.frequency.exponentialRampToValueAtTime(100 , t + 0.1 );
gain.gain.setValueAtTime(0.1 , t);
gain.gain.exponentialRampToValueAtTime(0.01 , t + 0.1 );
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(t);
osc.stop(t + 0.1 );
}
Events.on (engine, 'collisionStart' , function(event ) {
const pairs = event .pairs;
for (let i = 0 ; i < pairs.length; i++) {
const bodyA = pairs[i].bodyA;
const bodyB = pairs[i].bodyB;
if (bodyA.label === 'ball' || bodyB.label === 'ball' ) {
const speed = Vector.magnitude(Vector.sub(bodyA.velocity, bodyB.velocity));
if (speed > 5 ) {
playCollisionSound(Math.min(speed / 20 , 1.0 ));
}
}
}
});
function createWalls ( ) {
const w = window.innerWidth;
const h = window.innerHeight;
const wallOpt = { isStatic: true , render: { visible: false } };
World.add (world, [
Bodies.rectangle(w/2 , h + 50 , w, 100 , wallOpt),
Bodies.rectangle(-50 , h/2 , 100 , h * 2 , wallOpt),
Bodies.rectangle(w + 50 , h/2 , 100 , h * 2 , wallOpt)
]);
}
function spawnBall (x, y ) {
const colorIdx = Math.floor(Math.random() * CONFIG.colors.length);
const ball = Bodies.polygon(x, y, 6 , CONFIG.ballSize, {
restitution: 0.4 ,
friction: 0.05 ,
angle: Math.random() * Math.PI,
render: {
fillStyle: CONFIG.colors[colorIdx]
},
label: 'ball' ,
customColorIdx: colorIdx
});
return ball;
}
function fillBalls ( ) {
const w = window.innerWidth;
let count = 0 ;
const interval = setInterval(() => {
const x = Math.random() * (w - 100 ) + 50 ;
const ball = spawnBall(x, -50 );
World.add (world, ball);
balls.push(ball);
count++;
if (count > 60 ) clearInterval(interval);
}, 60 );
}
function findMatches (startBody ) {
const targetColor = startBody.customColorIdx;
const queue = [startBody];
const matches = new Set([startBody]);
const searchDist = CONFIG.ballSize * CONFIG.matchDistanceRatio;
let head = 0 ;
while (head < queue.length){
const current = queue[head++];
for (let other of balls) {
if (matches.has(other)) continue ;
if (other.customColorIdx !== targetColor) continue ;
const d = Vector.magnitude(Vector.sub(current.position, other.position));
if (d < searchDist) {
matches.add (other);
queue.push(other);
}
}
}
return Array.from (matches);
}
function createParticles (x, y, color ) {
for (let i = 0 ; i < 10 ; i++) {
particles.push({
x: x, y: y,
vx: (Math.random() - 0.5 ) * 15 ,
vy: (Math.random() - 0.5 ) * 15 ,
life: 1.0 ,
size: Math.random() * 4 + 2 ,
color: color
});
}
}
function renderLoop ( ) {
ctx.fillStyle = 'rgba(5, 5, 16, 0.5)' ;
ctx.fillRect(0 , 0 , canvas.width, canvas.height);
ctx.globalCompositeOperation = 'lighter' ;
balls.forEach(b => {
const pos = b.position;
const r = CONFIG.ballSize;
const vertices = b.vertices;
const isHovered = hoverMatches.includes(b);
ctx.shadowBlur = isHovered ? 30 : 10 ;
ctx.shadowColor = isHovered ? '#FFFFFF' : CONFIG.glowColor[b.customColorIdx];
ctx.beginPath();
ctx.moveTo(vertices[0 ].x, vertices[0 ].y);
for (let j = 1 ; j < vertices.length; j += 1 ) {
ctx.lineTo(vertices[j].x, vertices[j].y);
}
ctx.lineTo(vertices[0 ].x, vertices[0 ].y);
ctx.closePath();
ctx.fillStyle = isHovered ? '#FFFFFF' : b.render.fillStyle;
ctx.fill();
ctx.strokeStyle = 'rgba(255,255,255,0.5)' ;
ctx.lineWidth = 1 ;
ctx.stroke();
});
for (let i = particles.length - 1 ; i >= 0 ; i--) {
let p = particles[i];
p.x += p.vx;
p.y += p.vy;
p.life -= 0.04 ;
if (p.life <= 0 ) { particles.splice(i, 1 ); continue ; }
ctx.shadowBlur = 0 ;
ctx.globalAlpha = p.life;
ctx.fillStyle = p.color;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0 , Math.PI * 2 );
ctx.fill();
}
ctx.globalAlpha = 1.0 ;
ctx.globalCompositeOperation = 'source-over' ;
requestAnimationFrame(renderLoop);
}
canvas.addEventListener('mousemove' , (e) => checkHover(e.clientX, e.clientY));
function checkHover (x, y ) {
const bodies = Query.point(balls, { x: x, y: y });
if (bodies.length > 0 ) {
const matches = findMatches(bodies[0 ]);
if (matches.length >= CONFIG.minMatch) {
hoverMatches = matches;
canvas.style.cursor = 'pointer' ;
return ;
}
}
hoverMatches = [];
canvas.style.cursor = 'default' ;
}
canvas.addEventListener('mousedown' , (e) => handleClick(e.clientX, e.clientY));
canvas.addEventListener('touchstart' , (e) => {
const t = e.touches[0 ];
checkHover(t.clientX, t.clientY);
handleClick(t.clientX, t.clientY);
}, {passive: false });
function handleClick (x, y ) {
const bodies = Query.point(balls, { x: x, y: y });
if (bodies.length > 0 ) {
const matches = findMatches(bodies[0 ]);
if (matches.length >= CONFIG.minMatch) {
playShatterSound(matches.length);
score += matches.length * 100 ;
document.getElementById('score' ).innerText = score;
const comboText = document.getElementById('combo-text' );
comboText.innerText = matches.length + " HITS!" ;
comboText.style.opacity = 1 ;
comboText.style.left = (x + 20 ) + 'px' ;
comboText.style.top = (y - 50 ) + 'px' ;
comboText.style.transform = 'scale(1.5)' ;
setTimeout(() => {
comboText.style.opacity = 0 ;
comboText.style.transform = 'scale(1)' ;
}, 800 );
matches.forEach(b => {
createParticles(b.position.x, b.position.y, CONFIG.glowColor[b.customColorIdx]);
World.remove (world, b);
const idx = balls.indexOf(b);
if (idx > -1 ) balls.splice(idx, 1 );
});
hoverMatches = [];
setTimeout(() => {
for (let i=0 ; i<matches.length; i++){
const newBall = spawnBall(Math.random() * window.innerWidth, -50 - (i*30 ));
World.add (world, newBall);
balls.push(newBall);
}
}, 300 );
} else {
playErrorSound();
}
}
}
function resetGame ( ) {
World.clear(world);
Engine.clear(engine);
balls = [];
score = 0 ;
document.getElementById('score' ).innerText = 0 ;
createWalls();
fillBalls();
}
createWalls();
fillBalls();
const runner = Runner.create();
Runner.run(runner, engine);
renderLoop();
window.addEventListener('resize' , () => {
render.canvas.width = window.innerWidth;
render.canvas.height = window.innerHeight;
World.clear(world);
balls = [];
createWalls();
fillBalls();
});
</script>
</body>
</html>
copy
コメント