<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, user-scalable=no"
/>
<title>XR CSS AR Viewer</title>
<script src="https://aframe.io/releases/1.4.0/aframe.min.js"></script>
<script src="https://raw.githack.com/AR-js-org/AR.js/master/aframe/build/aframe-ar.js"></script>
<style>
body {
margin: 0;
overflow: hidden;
}
#ui-overlay {
position: fixed;
top: 10px;
left: 10px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 8px;
}
#ui-overlay button {
padding: 12px 20px;
background: rgba(0, 0, 0, 0.7);
color: white;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
font-size: 14px;
cursor: pointer;
}
#ui-overlay button:active {
background: rgba(255, 255, 255, 0.2);
}
#status {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 20px;
border-radius: 20px;
font-family: system-ui, sans-serif;
font-size: 14px;
z-index: 1000;
}
.a-enter-vr {
display: none !important;
}
</style>
</head>
<body>
<div id="ui-overlay">
<button onclick="cycleDemo()">🔄 Change Demo</button>
<button onclick="resetPosition()">📍 Reset Position</button>
</div>
<div id="status">Loading...</div>
<a-scene
embedded
arjs="sourceType: webcam; debugUIEnabled: false; detectionMode: mono_and_matrix; matrixCodeType: 3x3;"
renderer="logarithmicDepthBuffer: true; antialias: true;"
vr-mode-ui="enabled: false"
>
<a-camera gps-camera rotation-reader></a-camera>
<a-entity id="panel-container" position="0 0 -2"></a-entity>
</a-scene>
<script>
const DEMOS = {
menu: [
{
id: 0,
x: 0,
y: 0,
w: 340,
h: 391,
bg: [0.078, 0.078, 0.157, 0.95],
color: [0, 0, 0, 0],
opacity: 1,
radius: 16,
fontSize: 16,
},
{
id: 1,
x: 20,
y: 20,
w: 97.2,
h: 80,
bg: [0.384, 0, 0.933, 1],
color: [1, 1, 1, 1],
opacity: 1,
radius: 8,
fontSize: 16,
text: "VR Menu",
},
{
id: 3,
x: 40,
y: 120,
w: 120,
h: 69,
bg: [0.012, 0.855, 0.776, 1],
color: [0, 0, 0, 1],
opacity: 1,
radius: 8,
fontSize: 16,
text: "Start Game",
},
{
id: 4,
x: 40,
y: 201,
w: 100.8,
h: 69,
bg: [0.012, 0.855, 0.776, 1],
color: [0, 0, 0, 1],
opacity: 1,
radius: 8,
fontSize: 16,
text: "Settings",
},
{
id: 5,
x: 40,
y: 282,
w: 62.4,
h: 69,
bg: [0.012, 0.855, 0.776, 1],
color: [0, 0, 0, 1],
opacity: 1,
radius: 8,
fontSize: 16,
text: "Exit",
},
],
dashboard: [
{
id: 0,
x: 0,
y: 0,
w: 430,
h: 200.4,
bg: [0, 0, 0, 0.9],
color: [0, 0, 0, 0],
opacity: 1,
radius: 12,
fontSize: 16,
},
{
id: 1,
x: 15,
y: 15,
w: 154.4,
h: 60,
bg: [0.098, 0.463, 0.824, 1],
color: [1, 1, 1, 1],
opacity: 1,
radius: 0,
fontSize: 16,
text: "System Monitor",
},
{
id: 3,
x: 30,
y: 90,
w: 130,
h: 80.4,
bg: [0.149, 0.196, 0.22, 1],
color: [0, 0, 0, 0],
opacity: 1,
radius: 8,
fontSize: 16,
},
{
id: 4,
x: 45,
y: 105,
w: 21.6,
h: 16.8,
bg: [0, 0, 0, 0],
color: [0.502, 0.502, 0.502, 1],
opacity: 1,
radius: 0,
fontSize: 12,
text: "CPU",
},
{
id: 5,
x: 45,
y: 121.8,
w: 43.2,
h: 33.6,
bg: [0, 0, 0, 0],
color: [0.298, 0.686, 0.314, 1],
opacity: 1,
radius: 0,
fontSize: 24,
text: "45%",
},
{
id: 6,
x: 170,
y: 90,
w: 130,
h: 80.4,
bg: [0.149, 0.196, 0.22, 1],
color: [0, 0, 0, 0],
opacity: 1,
radius: 8,
fontSize: 16,
},
{
id: 7,
x: 185,
y: 105,
w: 21.6,
h: 16.8,
bg: [0, 0, 0, 0],
color: [0.502, 0.502, 0.502, 1],
opacity: 1,
radius: 0,
fontSize: 12,
text: "RAM",
},
{
id: 8,
x: 185,
y: 121.8,
w: 72,
h: 33.6,
bg: [0, 0, 0, 0],
color: [0.298, 0.686, 0.314, 1],
opacity: 1,
radius: 0,
fontSize: 24,
text: "8.2GB",
},
{
id: 9,
x: 310,
y: 90,
w: 130,
h: 80.4,
bg: [0.149, 0.196, 0.22, 1],
color: [0, 0, 0, 0],
opacity: 1,
radius: 8,
fontSize: 16,
},
{
id: 10,
x: 325,
y: 105,
w: 21.6,
h: 16.8,
bg: [0, 0, 0, 0],
color: [0.502, 0.502, 0.502, 1],
opacity: 1,
radius: 0,
fontSize: 12,
text: "GPU",
},
{
id: 11,
x: 325,
y: 121.8,
w: 43.2,
h: 33.6,
bg: [0, 0, 0, 0],
color: [0.298, 0.686, 0.314, 1],
opacity: 1,
radius: 0,
fontSize: 24,
text: "72%",
},
],
nested: [
{
id: 0,
x: 0,
y: 0,
w: 390,
h: 250,
bg: [0.129, 0.129, 0.129, 1],
color: [0, 0, 0, 0],
opacity: 1,
radius: 0,
fontSize: 16,
},
{
id: 2,
x: 20,
y: 20,
w: 100,
h: 100,
bg: [0.957, 0.263, 0.212, 1],
color: [1, 1, 1, 1],
opacity: 1,
radius: 8,
fontSize: 16,
text: "A",
},
{
id: 3,
x: 130,
y: 20,
w: 100,
h: 100,
bg: [0.298, 0.686, 0.314, 1],
color: [1, 1, 1, 1],
opacity: 1,
radius: 8,
fontSize: 16,
text: "B",
},
{
id: 5,
x: 20,
y: 130,
w: 100,
h: 100,
bg: [0.129, 0.588, 0.953, 1],
color: [1, 1, 1, 1],
opacity: 1,
radius: 8,
fontSize: 16,
text: "C",
},
{
id: 6,
x: 130,
y: 130,
w: 100,
h: 100,
bg: [1, 0.922, 0.231, 1],
color: [0, 0, 0, 1],
opacity: 1,
radius: 8,
fontSize: 16,
text: "D",
},
{
id: 7,
x: 240,
y: 130,
w: 100,
h: 100,
bg: [0.612, 0.153, 0.69, 1],
color: [1, 1, 1, 1],
opacity: 1,
radius: 8,
fontSize: 16,
text: "E",
},
],
};
const demoNames = Object.keys(DEMOS);
let currentDemoIndex = 0;
const SCALE = 0.001;
function createTextCanvas(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);
return canvas.toDataURL();
}
function rgbToHex(r, g, b) {
const toHex = (c) => {
const hex = Math.round(c * 255).toString(16);
return hex.length === 1 ? "0" + hex : hex;
};
return "#" + toHex(r) + toHex(g) + toHex(b);
}
function loadDemo(elements) {
const container = document.getElementById("panel-container");
while (container.firstChild) {
container.removeChild(container.firstChild);
}
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;
const 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.002;
if (el.bg[3] > 0) {
const panel = document.createElement("a-plane");
panel.setAttribute("width", w);
panel.setAttribute("height", h);
panel.setAttribute("position", `${x} ${y} ${z}`);
panel.setAttribute("color", rgbToHex(el.bg[0], el.bg[1], el.bg[2]));
panel.setAttribute("opacity", el.bg[3] * el.opacity);
panel.setAttribute("side", "double");
if (el.radius > 0) {
panel.setAttribute("scale", "1 1 1");
}
container.appendChild(panel);
}
if (el.text) {
const textDataUrl = createTextCanvas(
el.text,
el.fontSize,
el.color,
el.w,
el.h
);
const textPlane = document.createElement("a-plane");
textPlane.setAttribute("width", w);
textPlane.setAttribute("height", h);
textPlane.setAttribute("position", `${x} ${y} ${z + 0.001}`);
textPlane.setAttribute("src", textDataUrl);
textPlane.setAttribute("transparent", "true");
textPlane.setAttribute("side", "double");
container.appendChild(textPlane);
}
});
updateStatus(
`Demo: ${demoNames[currentDemoIndex]} (${elements.length} elements)`
);
}
function cycleDemo() {
currentDemoIndex = (currentDemoIndex + 1) % demoNames.length;
loadDemo(DEMOS[demoNames[currentDemoIndex]]);
}
function resetPosition() {
const container = document.getElementById("panel-container");
container.setAttribute("position", "0 0 -2");
container.setAttribute("rotation", "0 0 0");
updateStatus("Position reset");
}
function updateStatus(msg) {
document.getElementById("status").textContent = msg;
}
window.addEventListener("load", () => {
updateStatus("Initializing AR...");
const scene = document.querySelector("a-scene");
if (scene.hasLoaded) {
loadDemo(DEMOS.menu);
} else {
scene.addEventListener("loaded", () => {
loadDemo(DEMOS.menu);
});
}
});
setTimeout(() => {
if (
document.getElementById("status").textContent === "Initializing AR..."
) {
updateStatus("Tap to interact with panels");
}
}, 3000);
</script>
</body>
</html>