<!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>XR CSS Live Sync</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, sans-serif;
background: #0d1117;
color: #c9d1d9;
}
.editor-mode {
display: flex;
height: 100vh;
}
.editor-mode #editor-panel {
width: 400px;
display: flex;
flex-direction: column;
border-right: 1px solid #30363d;
}
.editor-mode #viewer {
flex: 1;
position: relative;
}
.ar-mode {
width: 100vw;
height: 100vh;
}
.ar-mode #editor-panel {
display: none;
}
.ar-mode #viewer {
width: 100%;
height: 100%;
}
#editor-panel h2 {
padding: 12px 16px;
background: linear-gradient(90deg, #238636, #1f6feb);
font-size: 14px;
color: #fff;
}
.editor-section {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.editor-section label {
padding: 8px 16px;
background: #21262d;
font-size: 12px;
color: #8b949e;
}
textarea {
flex: 1;
background: #0d1117;
color: #c9d1d9;
border: none;
padding: 12px 16px;
font-family: "SF Mono", "Consolas", monospace;
font-size: 13px;
line-height: 1.5;
resize: none;
outline: none;
}
textarea:focus {
background: #161b22;
}
#controls {
padding: 12px 16px;
background: #161b22;
border-top: 1px solid #30363d;
display: flex;
gap: 8px;
}
button {
flex: 1;
padding: 10px;
background: #238636;
color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
button:hover {
background: #2ea043;
}
button.secondary {
background: #1f6feb;
}
button.secondary:hover {
background: #388bfd;
}
#info {
position: fixed;
bottom: 16px;
left: 16px;
background: rgba(22, 27, 34, 0.95);
padding: 12px 16px;
border-radius: 8px;
font-size: 12px;
border: 1px solid #30363d;
z-index: 100;
}
#info span {
color: #58a6ff;
font-weight: 600;
}
#sync-status {
padding: 8px 16px;
background: #21262d;
font-size: 11px;
border-bottom: 1px solid #30363d;
}
#sync-status.connected {
color: #3fb950;
}
#sync-status.disconnected {
color: #f85149;
}
#presets {
padding: 8px 16px;
background: #21262d;
border-bottom: 1px solid #30363d;
}
#presets select {
width: 100%;
padding: 6px 8px;
background: #0d1117;
color: #c9d1d9;
border: 1px solid #30363d;
border-radius: 4px;
}
#qr-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.9);
z-index: 1000;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 20px;
}
#qr-overlay.show {
display: flex;
}
#qr-overlay canvas {
border-radius: 8px;
}
#qr-overlay p {
color: #c9d1d9;
text-align: center;
}
#qr-overlay button {
width: 200px;
}
#ar-ui {
display: none;
position: fixed;
top: 10px;
left: 10px;
z-index: 100;
flex-direction: column;
gap: 8px;
}
.ar-mode #ar-ui {
display: flex;
}
#ar-ui button {
padding: 12px 20px;
background: rgba(0, 0, 0, 0.7);
border: 1px solid rgba(255, 255, 255, 0.3);
backdrop-filter: blur(10px);
}
</style>
</head>
<body class="editor-mode">
<div id="editor-panel">
<h2>🔄 XR CSS Live Sync</h2>
<div id="sync-status" class="disconnected">● Disconnected</div>
<div id="presets">
<select id="presetSelect">
<option value="">-- Select Preset --</option>
<option value="menu">VR Menu</option>
<option value="dashboard">Dashboard</option>
<option value="buttons">Interactive Buttons</option>
</select>
</div>
<div class="editor-section">
<label>HTML</label>
<textarea id="htmlInput" spellcheck="false">
<div class="panel">
<div class="title">XR Panel</div>
<div class="buttons">
<div class="btn" onclick="action('start')">Start</div>
<div class="btn" onclick="action('settings')">Settings</div>
<div class="btn" onclick="action('exit')">Exit</div>
</div>
</div></textarea
>
</div>
<div class="editor-section">
<label>CSS</label>
<textarea id="cssInput" spellcheck="false">
.panel {
width: 300px;
background: rgba(20, 20, 40, 0.95);
padding: 20px;
border-radius: 16px;
}
.title {
background: #6200ee;
color: white;
padding: 15px;
border-radius: 8px;
font-size: 18px;
}
.buttons {
display: flex;
flex-direction: column;
gap: 10px;
margin: 20px 0 0 0;
}
.btn {
background: #03dac6;
color: black;
padding: 12px 20px;
border-radius: 8px;
cursor: pointer;
}
.btn:hover {
background: #018786;
}</textarea
>
</div>
<div id="controls">
<button id="renderBtn">▶ Render</button>
<button id="syncBtn" class="secondary">📱 Sync AR</button>
</div>
</div>
<div id="viewer"></div>
<div id="ar-ui">
<button onclick="cycleDemo()">🔄 Change</button>
<button onclick="resetPosition()">📍 Reset</button>
</div>
<div id="info">
Elements: <span id="elementCount">0</span> | Time:
<span id="renderTime">0</span>ms | Clients:
<span id="clientCount">1</span>
</div>
<div id="qr-overlay">
<canvas id="qrCanvas"></canvas>
<p>Scan with your iPhone to open AR view<br /><span id="arUrl"></span></p>
<button onclick="closeQR()">Close</button>
</div>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<script src="https://unpkg.com/qrcode@1.5.3/build/qrcode.min.js"></script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
const urlParams = new URLSearchParams(window.location.search);
const isARMode = urlParams.get("ar") === "1";
const roomId = urlParams.get("room") || "default";
if (isARMode) {
document.body.className = "ar-mode";
}
let syncChannel = null;
let peerConnection = null;
let dataChannel = null;
let isHost = !isARMode;
let clientCount = 1;
const SIGNAL_KEY = `xr-signal-${roomId}`;
function initSync() {
try {
syncChannel = new BroadcastChannel(`xr-sync-${roomId}`);
syncChannel.onmessage = (e) => {
if (e.data.type === "update" && !isHost) {
receiveUpdate(e.data.payload);
} else if (e.data.type === "ping") {
syncChannel.postMessage({
type: "pong",
from: isHost ? "host" : "client",
});
} else if (e.data.type === "pong") {
clientCount++;
updateClientCount();
}
};
updateSyncStatus(true);
if (isHost) {
clientCount = 1;
syncChannel.postMessage({ type: "ping" });
}
} catch (e) {
console.log("BroadcastChannel not supported, using polling");
initPollingSync();
}
}
function initPollingSync() {
if (isHost) {
} else {
setInterval(() => {
const data = localStorage.getItem(`xr-data-${roomId}`);
if (data) {
try {
receiveUpdate(JSON.parse(data));
} catch (e) {}
}
}, 100);
}
updateSyncStatus(true);
}
function broadcastUpdate(elements) {
const payload = { elements, timestamp: Date.now() };
if (syncChannel) {
syncChannel.postMessage({ type: "update", payload });
}
localStorage.setItem(`xr-data-${roomId}`, JSON.stringify(payload));
}
function receiveUpdate(payload) {
if (payload.elements) {
renderElements(payload.elements);
}
}
function updateSyncStatus(connected) {
const el = document.getElementById("sync-status");
el.className = connected ? "connected" : "disconnected";
el.textContent = connected
? "● Connected to room: " + roomId
: "● Disconnected";
}
function updateClientCount() {
document.getElementById("clientCount").textContent = clientCount;
}
let wasmReady = false;
let processToJson = null;
async function initWasm() {
try {
const module = await import("./pkg/css2xr.js");
await module.default();
processToJson = module.process_to_json;
wasmReady = true;
console.log("WASM Ready");
render();
} catch (e) {
console.log("WASM not available, using JS fallback");
wasmReady = true; render();
}
}
const PRESETS = {
menu: {
html: `<div class="panel">
<div class="title">VR Menu</div>
<div class="buttons">
<div class="btn" onclick="startGame()">Start Game</div>
<div class="btn" onclick="openSettings()">Settings</div>
<div class="btn" onclick="exitApp()">Exit</div>
</div>
</div>`,
css: `.panel {
width: 340px;
background: rgba(20, 20, 40, 0.95);
padding: 20px;
border-radius: 16px;
}
.title {
background: #6200ee;
color: white;
padding: 20px;
border-radius: 8px;
font-size: 18px;
}
.buttons {
display: flex;
flex-direction: column;
gap: 12px;
margin: 20px 0 0 0;
}
.btn {
background: #03dac6;
color: black;
padding: 15px 20px;
border-radius: 8px;
cursor: pointer;
}`,
},
dashboard: {
html: `<div class="dashboard">
<div class="header">System Monitor</div>
<div class="stats">
<div class="stat" onclick="showCPU()">
<div class="label">CPU</div>
<div class="value">45%</div>
</div>
<div class="stat" onclick="showRAM()">
<div class="label">RAM</div>
<div class="value">8.2GB</div>
</div>
<div class="stat" onclick="showGPU()">
<div class="label">GPU</div>
<div class="value">72%</div>
</div>
</div>
</div>`,
css: `.dashboard {
width: 430px;
background: rgba(0, 0, 0, 0.9);
padding: 15px;
border-radius: 12px;
}
.header {
background: #1976d2;
color: white;
padding: 20px;
}
.stats {
display: flex;
gap: 10px;
margin: 15px 0 0 0;
}
.stat {
width: 130px;
background: #263238;
padding: 15px;
border-radius: 8px;
cursor: pointer;
}
.label {
color: gray;
font-size: 12px;
}
.value {
color: #4caf50;
font-size: 24px;
}`,
},
buttons: {
html: `<div class="container">
<div class="btn primary" data-action="confirm">Confirm</div>
<div class="btn secondary" data-action="cancel">Cancel</div>
<div class="btn danger" data-action="delete">Delete</div>
</div>`,
css: `.container {
display: flex;
gap: 15px;
padding: 20px;
background: rgba(30,30,30,0.9);
border-radius: 12px;
}
.btn {
padding: 15px 30px;
border-radius: 8px;
color: white;
cursor: pointer;
font-size: 16px;
}
.primary { background: #4caf50; }
.secondary { background: #2196f3; }
.danger { background: #f44336; }`,
},
};
const container = document.getElementById("viewer");
const scene = new THREE.Scene();
scene.background = isARMode ? null : new THREE.Color(0x0d1117);
const camera = new THREE.PerspectiveCamera(
60,
container.clientWidth / container.clientHeight,
0.1,
1000
);
camera.position.set(0, 0, 5);
const renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: isARMode,
});
renderer.setSize(container.clientWidth, container.clientHeight);
renderer.setPixelRatio(window.devicePixelRatio);
container.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
const dirLight = new THREE.DirectionalLight(0xffffff, 0.8);
dirLight.position.set(5, 5, 5);
scene.add(dirLight);
let panelGroup = new THREE.Group();
scene.add(panelGroup);
const SCALE = 0.01;
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
let hoveredObject = null;
function createRoundedRectShape(w, h, r) {
const shape = new THREE.Shape();
const x = -w / 2,
y = -h / 2;
r = Math.min(r, w / 2, h / 2);
shape.moveTo(x + r, y);
shape.lineTo(x + w - r, y);
shape.quadraticCurveTo(x + w, y, x + w, y + r);
shape.lineTo(x + w, y + h - r);
shape.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
shape.lineTo(x + r, y + h);
shape.quadraticCurveTo(x, y + h, x, y + h - r);
shape.lineTo(x, y + r);
shape.quadraticCurveTo(x, y, x + r, y);
return shape;
}
function createTextTexture(text, fontSize, color, w, h) {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const scale = 4;
canvas.width = w * scale;
canvas.height = h * scale;
ctx.scale(scale, scale);
ctx.fillStyle = `rgba(${color[0] * 255}, ${color[1] * 255}, ${
color[2] * 255
}, ${color[3]})`;
ctx.font = `bold ${fontSize}px system-ui, sans-serif`;
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(text, w / 2, h / 2);
const texture = new THREE.CanvasTexture(canvas);
texture.needsUpdate = true;
return texture;
}
function renderElements(elements) {
while (panelGroup.children.length > 0) {
const child = panelGroup.children[0];
if (child.geometry) child.geometry.dispose();
if (child.material) {
if (child.material.map) child.material.map.dispose();
child.material.dispose();
}
panelGroup.remove(child);
}
let maxX = 0,
maxY = 0;
elements.forEach((el) => {
maxX = Math.max(maxX, el.x + el.w);
maxY = Math.max(maxY, el.y + el.h);
});
const offsetX = maxX / 2,
offsetY = maxY / 2;
elements.forEach((el, idx) => {
const w = el.w * SCALE;
const h = el.h * SCALE;
const x = (el.x + el.w / 2 - offsetX) * SCALE;
const y = -(el.y + el.h / 2 - offsetY) * SCALE;
const z = idx * 0.005;
if (el.bg && el.bg[3] > 0) {
const r = (el.radius || 0) * SCALE;
const shape = createRoundedRectShape(w, h, r);
const geometry = new THREE.ShapeGeometry(shape);
const material = new THREE.MeshStandardMaterial({
color: new THREE.Color(el.bg[0], el.bg[1], el.bg[2]),
transparent: el.bg[3] < 1 || el.opacity < 1,
opacity: el.bg[3] * (el.opacity || 1),
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, z);
mesh.userData = {
elementId: el.id,
interactive: el.interactive,
events: el.events,
originalColor: new THREE.Color(el.bg[0], el.bg[1], el.bg[2]),
};
panelGroup.add(mesh);
}
if (el.text) {
const texture = createTextTexture(
el.text,
el.fontSize || 16,
el.color || [1, 1, 1, 1],
el.w,
el.h
);
const textGeo = new THREE.PlaneGeometry(w, h);
const textMat = new THREE.MeshBasicMaterial({
map: texture,
transparent: true,
side: THREE.DoubleSide,
});
const textMesh = new THREE.Mesh(textGeo, textMat);
textMesh.position.set(x, y, z + 0.002);
panelGroup.add(textMesh);
}
});
document.getElementById("elementCount").textContent = elements.length;
}
function onPointerMove(event) {
const rect = renderer.domElement.getBoundingClientRect();
mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const intersects = raycaster.intersectObjects(panelGroup.children);
if (hoveredObject && hoveredObject.userData.interactive) {
hoveredObject.material.color.copy(
hoveredObject.userData.originalColor
);
}
hoveredObject = null;
document.body.style.cursor = "default";
for (const intersect of intersects) {
if (intersect.object.userData.interactive) {
hoveredObject = intersect.object;
hoveredObject.material.color.multiplyScalar(1.2);
document.body.style.cursor = "pointer";
break;
}
}
}
function onPointerDown(event) {
if (hoveredObject && hoveredObject.userData.events) {
const events = hoveredObject.userData.events;
if (events.click) {
console.log("Click event:", events.click);
try {
const action = events.click;
console.log(`Executing: ${action}`);
} catch (e) {}
}
}
}
renderer.domElement.addEventListener("pointermove", onPointerMove);
renderer.domElement.addEventListener("pointerdown", onPointerDown);
const htmlInput = document.getElementById("htmlInput");
const cssInput = document.getElementById("cssInput");
function render() {
if (!wasmReady) return;
const start = performance.now();
try {
let elements;
if (processToJson) {
const json = processToJson(
htmlInput.value,
cssInput.value,
800,
600
);
elements = JSON.parse(json);
} else {
elements = PRESETS.menu
? parseSimple(htmlInput.value, cssInput.value)
: [];
}
const time = (performance.now() - start).toFixed(2);
renderElements(elements);
document.getElementById("renderTime").textContent = time;
if (isHost) {
broadcastUpdate(elements);
}
} catch (e) {
console.error("Render error:", e);
}
}
function parseSimple(html, css) {
return PRESETS.menu ? [] : [];
}
let renderTimeout;
function scheduleRender() {
clearTimeout(renderTimeout);
renderTimeout = setTimeout(render, 300);
}
if (!isARMode) {
htmlInput.addEventListener("input", scheduleRender);
cssInput.addEventListener("input", scheduleRender);
}
document.getElementById("renderBtn")?.addEventListener("click", render);
document
.getElementById("presetSelect")
?.addEventListener("change", (e) => {
const preset = PRESETS[e.target.value];
if (preset) {
htmlInput.value = preset.html;
cssInput.value = preset.css;
render();
}
});
document.getElementById("syncBtn")?.addEventListener("click", () => {
const url = `${window.location.origin}${window.location.pathname}?ar=1&room=${roomId}`;
document.getElementById("arUrl").textContent = url;
QRCode.toCanvas(document.getElementById("qrCanvas"), url, {
width: 256,
margin: 2,
color: { dark: "#000", light: "#fff" },
});
document.getElementById("qr-overlay").classList.add("show");
});
window.closeQR = () => {
document.getElementById("qr-overlay").classList.remove("show");
};
window.cycleDemo = () => {
if (syncChannel) {
syncChannel.postMessage({ type: "cycleDemo" });
}
};
window.resetPosition = () => {
panelGroup.position.set(0, 0, 0);
panelGroup.rotation.set(0, 0, 0);
};
window.addEventListener("resize", () => {
camera.aspect = container.clientWidth / container.clientHeight;
camera.updateProjectionMatrix();
renderer.setSize(container.clientWidth, container.clientHeight);
});
function animate() {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
}
initSync();
initWasm();
animate();
if (isARMode) {
const data = localStorage.getItem(`xr-data-${roomId}`);
if (data) {
try {
receiveUpdate(JSON.parse(data));
} catch (e) {}
}
}
</script>
</body>
</html>