<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>XR CSS Editor (WASM)</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: system-ui, sans-serif;
background: #0d1117;
color: #c9d1d9;
display: flex;
height: 100vh;
}
#editor-panel {
width: 400px;
display: flex;
flex-direction: column;
border-right: 1px solid #30363d;
}
#editor-panel h2 {
padding: 12px 16px;
background: #238636;
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;
}
button {
width: 100%;
padding: 10px;
background: #238636;
color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
}
button:hover {
background: #2ea043;
}
#viewer {
flex: 1;
position: relative;
}
#info {
position: absolute;
bottom: 16px;
left: 16px;
background: rgba(22, 27, 34, 0.9);
padding: 12px 16px;
border-radius: 8px;
font-size: 12px;
border: 1px solid #30363d;
}
#info span {
color: #58a6ff;
font-weight: 600;
}
#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;
}
#status {
padding: 8px 16px;
background: #161b22;
font-size: 11px;
color: #8b949e;
border-bottom: 1px solid #30363d;
}
#status.ready {
color: #3fb950;
}
#status.error {
color: #f85149;
}
</style>
</head>
<body>
<div id="editor-panel">
<h2>🦀 XR CSS Editor (WASM)</h2>
<div id="status">Loading WASM...</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="nested">Nested Flex</option>
</select>
</div>
<div class="editor-section">
<label>HTML</label>
<textarea id="htmlInput" spellcheck="false">
<div class="panel">
<div class="title">Hello XR</div>
<div class="buttons">
<div class="btn">Start</div>
<div class="btn">Settings</div>
<div class="btn">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;
}
.buttons {
display: flex;
flex-direction: column;
gap: 10px;
margin: 20px 0 0 0;
}
.btn {
background: #03dac6;
color: black;
padding: 12px 20px;
border-radius: 8px;
}</textarea
>
</div>
<div id="controls">
<button id="renderBtn">â–¶ Render (Ctrl+Enter)</button>
</div>
</div>
<div id="viewer"></div>
<div id="info">
Elements: <span id="elementCount">0</span> | Time:
<span id="renderTime">0</span>ms
</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 type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
import init, { process_to_json } from "./pkg/css2xr.js";
let wasmReady = false;
async function initWasm() {
try {
await init();
wasmReady = true;
document.getElementById("status").textContent = "✓ WASM Ready";
document.getElementById("status").className = "ready";
render();
} catch (e) {
document.getElementById("status").textContent =
"✗ WASM Error: " + e.message;
document.getElementById("status").className = "error";
console.error(e);
}
}
const PRESETS = {
menu: {
html: `<div class="panel">
<div class="title">VR Menu</div>
<div class="buttons">
<div class="btn">Start Game</div>
<div class="btn">Settings</div>
<div class="btn">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;
}`,
},
dashboard: {
html: `<div class="dashboard">
<div class="header">System Monitor</div>
<div class="stats">
<div class="stat">
<div class="label">CPU</div>
<div class="value">45%</div>
</div>
<div class="stat">
<div class="label">RAM</div>
<div class="value">8.2GB</div>
</div>
<div class="stat">
<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;
}
.label {
color: gray;
font-size: 12px;
}
.value {
color: #4caf50;
font-size: 24px;
}`,
},
nested: {
html: `<div class="container">
<div class="row">
<div class="box a">A</div>
<div class="box b">B</div>
</div>
<div class="row">
<div class="box c">C</div>
<div class="box d">D</div>
<div class="box e">E</div>
</div>
</div>`,
css: `.container {
width: 390px;
height: 250px;
background: #212121;
padding: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.row {
display: flex;
gap: 10px;
}
.box {
width: 100px;
height: 100px;
border-radius: 8px;
color: white;
}
.a { background: #f44336; }
.b { background: #4caf50; }
.c { background: #2196f3; }
.d { background: #ffeb3b; color: black; }
.e { background: #9c27b0; }`,
},
};
const container = document.getElementById("viewer");
const scene = new THREE.Scene();
scene.background = 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 });
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;
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[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,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set(x, y, z);
panelGroup.add(mesh);
}
if (el.text) {
const texture = createTextTexture(
el.text,
el.fontSize,
el.color,
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;
}
const htmlInput = document.getElementById("htmlInput");
const cssInput = document.getElementById("cssInput");
function render() {
if (!wasmReady) return;
const start = performance.now();
try {
const json = process_to_json(
htmlInput.value,
cssInput.value,
800,
600
);
const elements = JSON.parse(json);
const time = (performance.now() - start).toFixed(2);
renderElements(elements);
document.getElementById("renderTime").textContent = time;
} catch (e) {
console.error("Render error:", e);
}
}
document.getElementById("renderBtn").addEventListener("click", render);
document.addEventListener("keydown", (e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
e.preventDefault();
render();
}
});
document
.getElementById("presetSelect")
.addEventListener("change", (e) => {
const preset = PRESETS[e.target.value];
if (preset) {
htmlInput.value = preset.html;
cssInput.value = preset.css;
render();
}
});
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);
}
initWasm();
animate();
</script>
</body>
</html>