Meta Description" name="description" />
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>AR Burger</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #111; overflow: hidden; font-family: 'Georgia', serif; }
#overlay {
position: fixed; inset: 0;
display: flex; flex-direction: column;
align-items: center; justify-content: center;
background: linear-gradient(135deg, #1a0a00, #111);
z-index: 10;
}
#overlay h1 {
color: #f5c842;
font-size: clamp(28px, 6vw, 48px);
letter-spacing: 4px;
text-transform: uppercase;
text-shadow: 0 0 30px #f5c84260;
margin-bottom: 8px;
}
#overlay p {
color: #ffffff70;
font-size: 14px;
letter-spacing: 2px;
margin-bottom: 40px;
}
#preview-canvas { border-radius: 20px; margin-bottom: 32px; box-shadow: 0 0 60px #f5c84230; }
.btn {
padding: 16px 44px;
border: none; border-radius: 50px;
font-size: 16px; font-weight: bold;
letter-spacing: 2px; cursor: pointer;
text-transform: uppercase;
transition: all 0.2s;
}
#ar-btn {
background: #f5c842; color: #1a0a00;
box-shadow: 0 0 30px #f5c84250;
}
#ar-btn:hover { transform: scale(1.05); box-shadow: 0 0 50px #f5c84280; }
#ar-btn:disabled { background: #555; color: #999; box-shadow: none; cursor: not-allowed; }
#not-supported {
margin-top: 14px; color: #ff6b6b;
font-size: 13px; letter-spacing: 1px;
display: none;
}
/* AR UI overlay (shown during AR session) */
#ar-ui {
position: fixed; inset: 0; pointer-events: none;
z-index: 20; display: none;
}
#ar-ui.active { display: block; }
#ar-hint {
position: absolute; bottom: 130px; left: 50%;
transform: translateX(-50%);
background: #00000099;
color: #fff; padding: 10px 22px;
border-radius: 30px; font-size: 14px;
letter-spacing: 1px; white-space: nowrap;
}
#ar-hint.placed { display: none; }
#exit-btn {
position: absolute; top: 30px; right: 20px;
background: #00000088; color: #fff;
border: 1px solid #ffffff40; border-radius: 30px;
padding: 10px 20px; font-size: 13px;
cursor: pointer; pointer-events: all;
letter-spacing: 1px;
}
#tap-hint {
position: absolute; bottom: 60px; left: 50%;
transform: translateX(-50%);
color: #f5c842; font-size: 13px;
letter-spacing: 2px; text-align: center;
pointer-events: none;
}
#scale-btns {
position: absolute; bottom: 50px; right: 20px;
display: flex; flex-direction: column; gap: 10px;
pointer-events: all;
}
#scale-btns button {
width: 44px; height: 44px;
border-radius: 50%; border: none;
background: #00000099; color: #fff;
font-size: 22px; cursor: pointer;
border: 1px solid #ffffff30;
}
</style>
</head>
<body>
<!-- Landing screen with 3D preview -->
<div id="overlay">
<h1>Cheeseburger</h1>
<p>AR Menu Experience</p>
<canvas id="preview-canvas" width="280" height="200"></canvas>
<button class="btn" id="ar-btn">π± Launch AR</button>
<div id="not-supported">WebXR AR not supported on this device/browser</div>
<p style="margin-top:16px;color:#ffffff40;font-size:11px;">Best on Android Chrome Β· Requires camera permission</p>
</div>
<!-- AR session UI -->
<div id="ar-ui">
<div id="ar-hint">π Move camera slowly over a flat surface</div>
<div id="tap-hint">Tap to place burger</div>
<div id="scale-btns">
<button id="scale-up">οΌ</button>
<button id="scale-down">οΌ</button>
</div>
<button id="exit-btn">β Exit AR</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// βββ BURGER BUILDER βββββββββββββββββββββββββββββββββββββββββββββββ
function buildBurger(scene) {
const group = new THREE.Group();
function ellipsoid(rx, ry, rz, color, rough = 0.65, metal = 0) {
const g = new THREE.SphereGeometry(1, 48, 48);
g.applyMatrix4(new THREE.Matrix4().makeScale(rx, ry, rz));
const m = new THREE.MeshStandardMaterial({ color, roughness: rough, metalness: metal });
const mesh = new THREE.Mesh(g, m);
mesh.castShadow = mesh.receiveShadow = true;
return mesh;
}
function pattyDisk(rx, ry, color) {
const g = new THREE.CylinderGeometry(rx, rx * 1.06, ry, 48, 4);
const pos = g.attributes.position;
for (let i = 0; i < pos.count; i++) {
const y = pos.getY(i);
if (Math.abs(y) < ry * 0.45) {
pos.setX(i, pos.getX(i) + (Math.random() - 0.5) * 0.015);
pos.setZ(i, pos.getZ(i) + (Math.random() - 0.5) * 0.015);
}
pos.setY(i, y + (Math.random() - 0.5) * 0.008);
}
g.computeVertexNormals();
const m = new THREE.MeshStandardMaterial({ color, roughness: 0.92 });
const mesh = new THREE.Mesh(g, m);
mesh.castShadow = true;
return mesh;
}
function cheeseDrape(yPos) {
const shape = new THREE.Shape();
const s = 0.285;
shape.moveTo(-s, -s); shape.lineTo(s, -s);
shape.lineTo(s, s); shape.lineTo(-s, s); shape.closePath();
const g = new THREE.ShapeGeometry(shape, 6);
g.applyMatrix4(new THREE.Matrix4().makeRotationX(-Math.PI / 2));
const pos = g.attributes.position;
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i), z = pos.getZ(i);
const d = Math.sqrt(x * x + z * z);
pos.setY(i, pos.getY(i) - Math.max(0, d - 0.13) * 0.45);
}
g.computeVertexNormals();
const m = new THREE.MeshStandardMaterial({ color: 0xf5b800, roughness: 0.28, metalness: 0.04, side: THREE.DoubleSide });
const mesh = new THREE.Mesh(g, m);
mesh.position.y = yPos;
mesh.castShadow = true;
return mesh;
}
// Scale: real burger ~11cm tall β 0.11 in meters
const S = 0.2; // 20cm for good AR visibility
let y = 0;
// Bottom bun
const bbun = ellipsoid(S * 1.4, S * 0.4, S * 1.3, 0xcc7a18);
bbun.position.y = y + S * 0.4; group.add(bbun);
y += S * 0.65;
// Bottom bun crown
const bbc = ellipsoid(S * 1.38, S * 0.1, S * 1.28, 0xe09030, 0.55);
bbc.position.y = y; group.add(bbc);
y += S * 0.08;
// Cheese bottom
group.add(cheeseDrape(y + S * 0.06));
y += S * 0.08;
// Patty
const pat = pattyDisk(S * 1.28, S * 0.35, 0x1e0d04);
pat.position.y = y + S * 0.175; group.add(pat);
y += S * 0.38;
// Char marks
for (let i = -1; i <= 1; i++) {
const cg = new THREE.BoxGeometry(S * 2.4, S * 0.02, S * 0.1);
const cm = new THREE.MeshStandardMaterial({ color: 0x080301, roughness: 1 });
const ch = new THREE.Mesh(cg, cm);
ch.position.set(0, y - S * 0.03, i * S * 0.35);
ch.rotation.y = 0.12;
group.add(ch);
}
// Cheese top
group.add(cheeseDrape(y + S * 0.02));
y += S * 0.14;
// Top bun base
const tbbase = ellipsoid(S * 1.38, S * 0.18, S * 1.28, 0xc07010);
tbbase.position.y = y + S * 0.12; group.add(tbbase);
y += S * 0.18;
// Top bun dome
const tdome = ellipsoid(S * 1.36, S * 0.82, S * 1.26, 0xd4872a, 0.52, 0.02);
tdome.position.y = y + S * 0.72; group.add(tdome);
// Glaze
const gg = new THREE.SphereGeometry(1, 32, 32);
gg.applyMatrix4(new THREE.Matrix4().makeScale(S * 1.12, S * 0.55, S * 1.08));
const gm = new THREE.MeshStandardMaterial({ color: 0xe8a030, roughness: 0.2, metalness: 0.1, transparent: true, opacity: 0.55 });
const glaze = new THREE.Mesh(gg, gm);
glaze.position.y = y + S * 1.08; group.add(glaze);
const topY = y + S * 1.28;
// Sesame seeds
const seedPositions = [[0,0],[0.55,0.3],[-0.5,0.35],[0.2,-0.6],[-0.3,-0.55],[0.65,-0.2],[-0.65,-0.1],[0,0.7]];
seedPositions.forEach(([sx, sz]) => {
const sg = new THREE.SphereGeometry(S * 0.045, 8, 8);
sg.applyMatrix4(new THREE.Matrix4().makeScale(1.4, 0.65, 1));
const sm = new THREE.MeshStandardMaterial({ color: 0xf0e0a0, roughness: 0.8 });
const seed = new THREE.Mesh(sg, sm);
seed.position.set(sx * S, topY, sz * S);
group.add(seed);
});
// Lift so base sits at y=0
group.position.y = 0;
scene.add(group);
return group;
}
// βββ PREVIEW (landing screen) ββββββββββββββββββββββββββββββββββββββ
(function initPreview() {
const canvas = document.getElementById('preview-canvas');
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a0a00);
const camera = new THREE.PerspectiveCamera(40, 280 / 200, 0.01, 50);
camera.position.set(0, 0.18, 0.7);
camera.lookAt(0, 0.1, 0);
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setSize(280, 200);
renderer.shadowMap.enabled = true;
scene.add(new THREE.AmbientLight(0xfff5e0, 0.6));
const dl = new THREE.DirectionalLight(0xfff5cc, 2.5);
dl.position.set(2, 4, 2); dl.castShadow = true; scene.add(dl);
scene.add(new THREE.PointLight(0xffaa44, 1.0, 10)).position.set(-2, 1.5, 2);
const burger = buildBurger(scene);
burger.position.y = 0;
let t = 0;
function loop() {
requestAnimationFrame(loop);
t += 0.016;
burger.rotation.y = t * 0.5;
burger.position.y = Math.sin(t * 0.8) * 0.008;
renderer.render(scene, camera);
}
loop();
})();
// βββ WebXR AR SESSION βββββββββββββββββββββββββββββββββββββββββββββ
const arBtn = document.getElementById('ar-btn');
const arUI = document.getElementById('ar-ui');
const arHint = document.getElementById('ar-hint');
const tapHint = document.getElementById('tap-hint');
const exitBtn = document.getElementById('exit-btn');
const notSupported = document.getElementById('not-supported');
// Check WebXR support
if (!('xr' in navigator)) {
arBtn.disabled = true;
notSupported.style.display = 'block';
notSupported.textContent = 'WebXR not available. Use Android Chrome.';
} else {
navigator.xr.isSessionSupported('immersive-ar').then(supported => {
if (!supported) {
arBtn.disabled = true;
notSupported.style.display = 'block';
notSupported.textContent = 'AR not supported on this device/browser.';
}
});
}
let xrSession = null;
let hitTestSource = null;
let hitTestSourceRequested = false;
let burgerPlaced = false;
let arBurger = null;
let burgerScale = 1;
arBtn.addEventListener('click', startAR);
exitBtn.addEventListener('click', () => { if (xrSession) xrSession.end(); });
document.getElementById('scale-up').addEventListener('click', () => {
if (arBurger) { burgerScale = Math.min(3, burgerScale + 0.2); arBurger.scale.setScalar(burgerScale); }
});
document.getElementById('scale-down').addEventListener('click', () => {
if (arBurger) { burgerScale = Math.max(0.3, burgerScale - 0.2); arBurger.scale.setScalar(burgerScale); }
});
async function startAR() {
const sessionInit = {
requiredFeatures: ['hit-test'],
optionalFeatures: ['dom-overlay', 'light-estimation'],
domOverlay: { root: document.body }
};
try {
const session = await navigator.xr.requestSession('immersive-ar', sessionInit);
onSessionStarted(session);
} catch (e) {
notSupported.style.display = 'block';
notSupported.textContent = 'Could not start AR: ' + e.message;
}
}
// AR Three.js setup
const arScene = new THREE.Scene();
const arCamera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.01, 20);
const arRenderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
arRenderer.setPixelRatio(window.devicePixelRatio);
arRenderer.setSize(window.innerWidth, window.innerHeight);
arRenderer.xr.enabled = true;
arRenderer.outputEncoding = THREE.sRGBEncoding;
arRenderer.shadowMap.enabled = true;
document.body.appendChild(arRenderer.domElement);
arRenderer.domElement.style.display = 'none';
arRenderer.domElement.style.position = 'fixed';
arRenderer.domElement.style.inset = '0';
arRenderer.domElement.style.zIndex = '5';
// Lighting for AR (will blend with real world)
arScene.add(new THREE.AmbientLight(0xffffff, 1.2));
const arDirLight = new THREE.DirectionalLight(0xfff8ee, 1.5);
arDirLight.position.set(1, 3, 1);
arDirLight.castShadow = true;
arScene.add(arDirLight);
// Reticle (placement ring)
const reticleGeo = new THREE.RingGeometry(0.08, 0.11, 32);
reticleGeo.applyMatrix4(new THREE.Matrix4().makeRotationX(-Math.PI / 2));
const reticle = new THREE.Mesh(reticleGeo, new THREE.MeshBasicMaterial({ color: 0xf5c842 }));
reticle.visible = false;
reticle.matrixAutoUpdate = false;
arScene.add(reticle);
// Shadow receiver plane
const shadowGeo = new THREE.CircleGeometry(0.5, 32);
shadowGeo.applyMatrix4(new THREE.Matrix4().makeRotationX(-Math.PI / 2));
const shadowMat = new THREE.ShadowMaterial({ opacity: 0.4 });
const shadowPlane = new THREE.Mesh(shadowGeo, shadowMat);
shadowPlane.receiveShadow = true;
shadowPlane.visible = false;
arScene.add(shadowPlane);
// Tap to place
arRenderer.domElement.addEventListener('click', () => {
if (reticle.visible && !burgerPlaced) {
placeBurger();
}
});
function placeBurger() {
if (!arBurger) {
arBurger = buildBurger(arScene);
arBurger.visible = false;
}
const pos = new THREE.Vector3();
pos.setFromMatrixPosition(reticle.matrix);
arBurger.position.copy(pos);
arBurger.visible = true;
shadowPlane.position.copy(pos);
shadowPlane.visible = true;
burgerPlaced = true;
reticle.visible = false;
arHint.classList.add('placed');
tapHint.style.display = 'none';
}
async function onSessionStarted(session) {
xrSession = session;
burgerPlaced = false;
burgerScale = 1;
hitTestSourceRequested = false;
hitTestSource = null;
arRenderer.xr.setReferenceSpaceType('local');
await arRenderer.xr.setSession(session);
// Show AR UI
document.getElementById('overlay').style.display = 'none';
arRenderer.domElement.style.display = 'block';
arUI.classList.add('active');
arHint.classList.remove('placed');
tapHint.style.display = 'block';
session.addEventListener('end', onSessionEnded);
arRenderer.setAnimationLoop(renderAR);
}
function onSessionEnded() {
xrSession = null;
hitTestSource = null;
hitTestSourceRequested = false;
arRenderer.domElement.style.display = 'none';
arUI.classList.remove('active');
document.getElementById('overlay').style.display = 'flex';
arRenderer.setAnimationLoop(null);
// Remove burger from scene so it resets next time
if (arBurger) { arScene.remove(arBurger); arBurger = null; }
}
const clock = new THREE.Clock();
function renderAR(timestamp, frame) {
if (frame) {
const refSpace = arRenderer.xr.getReferenceSpace();
const session = arRenderer.xr.getSession();
// Init hit test
if (!hitTestSourceRequested) {
session.requestReferenceSpace('viewer').then(viewerSpace => {
session.requestHitTestSource({ space: viewerSpace }).then(src => {
hitTestSource = src;
});
});
hitTestSourceRequested = true;
session.addEventListener('end', () => {
hitTestSourceRequested = false;
hitTestSource = null;
});
}
if (hitTestSource && !burgerPlaced) {
const results = frame.getHitTestResults(hitTestSource);
if (results.length > 0) {
const pose = results[0].getPose(refSpace);
reticle.visible = true;
reticle.matrix.fromArray(pose.transform.matrix);
} else {
reticle.visible = false;
}
}
}
// Gentle burger animation
const t = clock.getElapsedTime();
if (arBurger && burgerPlaced) {
arBurger.rotation.y = t * 0.4;
}
arRenderer.render(arScene, arCamera);
}
window.addEventListener('resize', () => {
arCamera.aspect = window.innerWidth / window.innerHeight;
arCamera.updateProjectionMatrix();
arRenderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>
2
2
135KB
606KB
894.0ms
308.0ms
894.0ms