<body>
<div class="score">score:</div>
<div class="cover center hidden">click to resume</div>
<div id="title" class="center">SyncoSpace
<div class="info"><a target="_blank" href="https://github.com/jacklehamster/syncopath"><img src="https://avatars.githubusercontent.com/u/5171849" style="width: 50px; height: 50px"></a></div>
</div>
<div id="qr" class="center">
<img id="qrcode" class="hidden top-right" alt="QR code" />
</div>
<div id="mute">🔇</div>
<div id="zzfx" class="hidden"><a href='https://zzfx.3d2k.com/' target="_blank">Sound produced with ZZFX</a></div>
</body>
body {
margin: 0;
}
canvas {
display: block;
}
.controls {
position: absolute;
top: 10px;
left: 10px;
background: rgba(255, 255, 255, 0.8);
padding: 10px;
}
.syncousers {
position: absolute;
right: 5px;
top: 5px;
color: red;
}
#qrcode {
width: 100px;
height: 100px;
}
.center {
position: absolute;
display: flex;
justify-content: center;
align-items: center;
height: 100%;
width: 100%;
}
.top-right {
position: absolute;
right: 5px;
top: 25px;
}
.score {
color: white;
position: absolute;
white-space: pre;
}
.hidden {
display: none;
}
#mute {
cursor: pointer;
position: absolute;
right: 5px;
bottom: 5px;
}
#zzfx {
position: absolute;
left: 5px;
bottom: 5px;
display: none;
font-family: monospace;
}
a {
color: gray;
}
a:hover {
color: silver;
}
.cover {
width: 100%;
height: 100%;
position: absolute;
z-index: 1000;
background-color: #000000bb;
color: white;
}
#title {
color: white;
font-size: 50pt;
font-family: Tahoma;
font-style: italic;
opacity: 0;
transition: 1s opacity;
}
.info {
position: absolute;
bottom: 40px;
z-index: 1000;
cursor: pointer;
}
import * as THREE from "https://esm.sh/three";
import {
provideSocketClient,
handleUsersChanged,
} from "https://esm.sh/@dobuki/syncopath@1.0.24";
import { zzfx } from "https://esm.sh/zzfx";
const params = new URLSearchParams(location.href);
const room = params.get("room") ?? localStorage.getItem("syncoroom") ?? "codepen-" + Math.random().toString().split(".").pop().substring(0, 4);
localStorage.setItem("syncoroom", room);
// SYNCOPATH
const socketClient = provideSocketClient({ host: "demo.dobuki.net", room });
const phones = {};
let boxes = [];
let particles = [];
let grids = []; // Array for two grids
let bullets = [];
let score = 0;
let best = 0;
let globalSpeed = 1;
handleUsersChanged(socketClient)
.onUserAdded(async (clientId, isSelf, observers) => {
if (isSelf) return;
const phone = createPhone();
phones[clientId] = phone;
phone.disabledUntil = 0;
phone.gameOver = false;
const peerData = socketClient.peerData(clientId);
const PRECISION = 100;
const handleOrientationEvent = (event) => {
if (event.alpha === null) return;
peerData.setData(`acceleration`, {
x: Math.round(event.alpha * PRECISION) / PRECISION,
y: Math.round(event.beta * PRECISION) / PRECISION,
z: Math.round(event.gamma * PRECISION) / PRECISION,
});
};
window.addEventListener("MozOrientation", handleOrientationEvent, true);
window.addEventListener("deviceorientation", handleOrientationEvent, true);
window.addEventListener("touchstart", (event) => {
event.preventDefault();
if (!peerData.state.tap) peerData.setData(`tap`, "~{now}");
});
window.addEventListener("touchend", (event) => {
event.preventDefault();
if (peerData.state.tap) peerData.setData(`tap`, 0);
});
observers.add(
peerData.observe(`acceleration`).onChange((acceleration) => {
if (acceleration) {
const { x, y, z } = acceleration;
setPhoneRotation(phone, y - 90, z, x);
phone.dx = z;
phone.dy = y;
document.querySelector("#mute").classList.remove("hidden");
document.querySelector(".cover").classList.add("hidden");
refreshZzfx();
}
})
);
function move() {
if (phones[clientId]) {
requestAnimationFrame(move);
const now = Date.now();
// Move phone if not disabled
if (now >= phone.disabledUntil && !phone.gameOver) {
phone.position.x += (phone.dx ?? 0) / 1000;
phone.position.y += (phone.dy ?? 0) / 1000;
phone.position.x *= 0.99;
phone.position.y *= 0.99;
if (!phone.visible) {
phone.visible = true;
score = 0;
globalSpeed = 1;
document.querySelector(".score").textContent = `score: ${score}`;
document.querySelector("#title").style.opacity = 0;
}
}
// Move bullets
bullets.forEach(b => {
const bulletDir = new THREE.Vector3(0, 1, 0);
bulletDir.applyQuaternion(b.quaternion);
b.position.addScaledVector(bulletDir, 0.5);
b.time = (b.time ?? 0) + 1;
if (b.time > 50) scene.remove(b);
// Fade bullets based on distance
b.material.opacity = Math.max(0, 1 - (b.position.z - phone.position.z) / 10);
});
bullets = bullets.filter(b => b.time <= 50);
// Move boxes
boxes.forEach(box => {
box.position.z += 0.1 * globalSpeed;
if (box.position.z > 5) {
scene.remove(box);
boxes = boxes.filter(b => b !== box);
}
// Fade boxes based on distance
box.material.opacity = Math.max(0, 1 - (box.position.z - phone.position.z) / 10);
// Check collision with phone
if (!phone.gameOver && now >= phone.disabledUntil && box.position.distanceTo(phone.position) < 0.8 * box.size) {
particles.push(createParticles(phone.position, 0x3388ff));
phone.visible = false;
phone.disabledUntil = now + 3000;
phone.gameOver = true;
if (!mute) zzfx( [1,,100,.05,.2,.3,2,1.5,,,50,.1,,,,,,.8,,.2]);
document.querySelector(".score").textContent = `score: ${score}\nbest: ${best}`;
document.querySelector("#title").style.opacity = 1;
}
});
// Collision detection (bullet vs box)
bullets.forEach(bullet => {
boxes.forEach(box => {
if (bullet.position.distanceTo(box.position) < box.size) {
particles.push(createParticles(box.position));
scene.remove(bullet);
score += Math.ceil((1.5 - box.size) * 5);
if (box.size > .5) {
box.size *= Math.random();
box.scale.set(
box.size, box.size, box.size);
} else {
score += 20;
scene.remove(box);
boxes = boxes.filter(b => b !== box);
}
best = Math.max(score, best);
globalSpeed = 1 + score / 100;
document.querySelector(".score").textContent = `score: ${score}`;
bullets = bullets.filter(b => b !== bullet);
if (!mute) zzfx( [1.5,,200,.02,.1,.2,1,2,,,50,.05,,,,,,.9,,.1]);
}
});
});
// Update particles
particles.forEach(p => {
p.time += 1;
p.children.forEach(particle => {
particle.position.add(particle.velocity);
particle.material.opacity -= 0.03;
});
if (p.time > 30) scene.remove(p);
});
particles = particles.filter(p => p.time <= 30);
// Move grids
grids.forEach(grid => {
grid.position.z += 0.1 * globalSpeed;
if (grid.position.z > 10) grid.position.z = -10; // Loop back
// Fade grid based on distance
grid.material.opacity = Math.max(0.2, 1 - (grid.position.z - camera.position.z) / 15);
});
// Spawn new boxes
if (Math.random() < 0.05 && now >= phone.disabledUntil) {
boxes.push(createBox());
}
render();
}
}
move();
observers.add(
peerData.observe(`tap`).onChange((tap) => {
if (tap && Date.now() >= phone.disabledUntil) {
if (phone.gameOver) {
phone.gameOver = false;
}
if (!mute) zzfx( [5,,133,.01,.06,.08,1,.9,,17,,,,,,,,.69,,,251]);
phone.screenMaterial.color.set(0xff0000);
bullets.push(createBullet(phone));
} else {
phone.screenMaterial.color.set(0x0000ff);
}
})
);
})
.onUserRemoved(user => {
if (phones[user]?.mesh) scene.remove(phones[user]);
delete phones[user];
document.querySelector("#title").style.opacity = 1;
});
// Scene setup
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Create phone
function createPhone() {
const phoneGeometry = new THREE.BoxGeometry(0.3, .7, 0.1);
const phoneMaterial = new THREE.MeshPhongMaterial({ color: 0x3388ff });
const phone = new THREE.Mesh(phoneGeometry, phoneMaterial);
scene.add(phone);
const screenGeometry = new THREE.PlaneGeometry(0.2, .5);
const screenMaterial = new THREE.MeshBasicMaterial({ color: 0x0000ff });
const screen = new THREE.Mesh(screenGeometry, screenMaterial);
screen.position.z = 0.06;
phone.add(screen);
phone.screen = screen;
phone.screenMaterial = screenMaterial;
phone.visible = false;
return phone;
}
// Create bullet
function createBullet(phone) {
const bulletGeometry = new THREE.BoxGeometry(.2, 1, .2);
const bulletMaterial = new THREE.MeshPhongMaterial({
color: 0xffffff,
emissive: 0xffffff,
emissiveIntensity: 0.5,
transparent: true, // Enable transparency
opacity: 1
});
const bullet = new THREE.Mesh(bulletGeometry, bulletMaterial);
scene.add(bullet);
bullet.rotation.x = phone.rotation.x;
bullet.rotation.y = phone.rotation.y;
bullet.rotation.z = phone.rotation.z;
bullet.position.x = phone.position.x;
bullet.position.y = phone.position.y;
bullet.position.z = phone.position.z;
return bullet;
}
// Create approaching box
function createBox() {
const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
const boxMaterial = new THREE.MeshPhongMaterial({
color: 0xff0000,
emissive: 0xff6666,
emissiveIntensity: 0.2,
transparent: true, // Enable transparency
opacity: 1
});
const box = new THREE.Mesh(boxGeometry, boxMaterial);
scene.add(box);
box.position.set(
(Math.random() - 0.5) * 5,
(Math.random() - 0.5) * 5,
-5
);
box.boxGeometry = boxGeometry;
box.size = 1;
return box;
}
// Create particles for explosion
function createParticles(position, color = 0xffaa00) {
const particleGroup = new THREE.Group();
const particleCount = 20;
const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
const material = new THREE.MeshBasicMaterial({ color, transparent: true, opacity: 1 });
for (let i = 0; i < particleCount; i++) {
const particle = new THREE.Mesh(geometry, material.clone());
particle.position.copy(position);
particle.velocity = new THREE.Vector3(
(Math.random() - 0.5) * 0.2,
(Math.random() - 0.5) * 0.2,
(Math.random() - 0.5) * 0.2
);
particleGroup.add(particle);
}
particleGroup.time = 0;
scene.add(particleGroup);
return particleGroup;
}
// Create two alternating grids
function createGrids() {
const size = 20;
const divisions = 20;
const grid1 = new THREE.GridHelper(size, divisions, 0x00ff00, 0x00ff00);
grid1.position.set(0, -2, -10); // Start far
grid1.material.opacity = 0.5;
grid1.material.transparent = true;
scene.add(grid1);
const grid2 = new THREE.GridHelper(size, divisions, 0x00ff00, 0x00ff00);
grid2.position.set(0, -2, 0); // Offset by half the distance
grid2.material.opacity = 0.5;
grid2.material.transparent = true;
scene.add(grid2);
grids = [grid1, grid2];
}
// Lighting
const ambientLight = new THREE.AmbientLight(0x404040);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
directionalLight.position.set(0, 1, 1);
scene.add(directionalLight);
// Camera position
camera.position.z = 2;
camera.position.y = 1;
// Initialize grids
createGrids();
// Set rotation
function setPhoneRotation(phone, x, y, z) {
phone.rotation.x = THREE.MathUtils.degToRad(x);
phone.rotation.y = THREE.MathUtils.degToRad(y);
phone.rotation.z = THREE.MathUtils.degToRad(z);
render();
}
function render() {
renderer.render(scene, camera);
}
render();
// Handle window resize
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
render();
});
document.addEventListener("touchmove", (event) => event.preventDefault(), { passive: false });
// QR Code
import QRCode from "https://cdn.skypack.dev/qrcode";
const qrCode = document.getElementById("qrcode");
function setQrCode() {
qrCode.classList.add("hidden");
const url = `https://codepen.io/Vincent-Le-Quang-Dobuki/full/ZYEROqY?room=${room}`;
QRCode.toDataURL(url, function (err, url) {
qrCode.src = url;
qrCode.classList.remove("hidden");
});
}
setQrCode();
// Sound
function refreshZzfx() {
document.querySelector("#zzfx").style.display = mute || document.querySelector("#mute").classList.contains("hidden") ? "none" : "block";
}
let mute = true;
document.querySelector("#mute").addEventListener("click", () => {
mute = !mute;
document.querySelector("#mute").textContent = mute ? '🔇' : '🔊';
refreshZzfx();
if (!mute) zzfx( [,,479,.03,.12,.31,1,1.7,,,225,.05,.03,,,,,.92,.17,.17]);
});
const div = document.body.appendChild(document.createElement("div"));
div.textContent = room;
div.style.position = "absolute";
div.style.right = "5px";
div.style.top = "5px";
div.style.color = "#333";
div.style.zIndex = "100";
socketClient.onClose(() => {
document.querySelector(".cover").classList.remove("hidden");
window.addEventListener("focus", () => {
document.querySelector(".cover").classList.add("hidden");
}, { once: true });
});
document.querySelector("#title").style.opacity = 1;
This Pen doesn't use any external CSS resources.
This Pen doesn't use any external JavaScript resources.