<div {{ stimulus_controller('@pwa/device-orientation', {
throttle: 30
}) }}>
<div class="ar-pointer">
<h1>AR Direction Pointer</h1>
<p>Point your device at different locations</p>
<div class="ar-view">
<div id="ar-scene" class="scene">
<div id="pointer" class="pointer">
<div class="arrow"></div>
<div class="target-info" id="target-info">---</div>
</div>
<div class="horizon-line"></div>
<div class="poi" style="--angle: 0deg">
<div class="poi-marker"></div>
<div class="poi-label">North</div>
</div>
<div class="poi" style="--angle: 90deg">
<div class="poi-marker"></div>
<div class="poi-label">East</div>
</div>
<div class="poi" style="--angle: 180deg">
<div class="poi-marker"></div>
<div class="poi-label">South</div>
</div>
<div class="poi" style="--angle: 270deg">
<div class="poi-marker"></div>
<div class="poi-label">West</div>
</div>
</div>
</div>
<div class="orientation-data" id="orientation-data">
<div>Heading: <strong>---°</strong></div>
<div>Pitch: <strong>---°</strong></div>
<div>Roll: <strong>---°</strong></div>
</div>
</div>
</div>
<script>
const scene = document.getElementById('ar-scene');
const pointer = document.getElementById('pointer');
const targetInfo = document.getElementById('target-info');
const orientationData = document.getElementById('orientation-data');
// Define targets (in degrees)
const targets = [
{ angle: 0, name: 'North', color: '#ef4444' },
{ angle: 45, name: 'NE', color: '#f59e0b' },
{ angle: 90, name: 'East', color: '#10b981' },
{ angle: 135, name: 'SE', color: '#3b82f6' },
{ angle: 180, name: 'South', color: '#8b5cf6' },
{ angle: 225, name: 'SW', color: '#ec4899' },
{ angle: 270, name: 'West', color: '#6366f1' },
{ angle: 315, name: 'NW', color: '#14b8a6' }
];
document.addEventListener('pwa--device-orientation:updated', (event) => {
const { alpha, beta, gamma } = event.detail;
if (alpha !== null && beta !== null && gamma !== null) {
// Rotate scene based on heading
scene.style.transform = `rotateZ(${-alpha}deg) rotateX(${beta - 90}deg)`;
// Update pointer
pointer.style.transform = `rotateY(${gamma}deg)`;
// Find nearest target
const nearestTarget = findNearestTarget(alpha);
const distance = Math.abs(normalizeAngle(alpha - nearestTarget.angle));
if (distance < 15) {
targetInfo.textContent = `→ ${nearestTarget.name}`;
targetInfo.style.color = nearestTarget.color;
targetInfo.style.opacity = '1';
} else {
targetInfo.style.opacity = '0.3';
targetInfo.textContent = '---';
}
// Update data display
orientationData.innerHTML = `
<div>Heading: <strong>${Math.round(alpha)}°</strong></div>
<div>Pitch: <strong>${Math.round(beta - 90)}°</strong></div>
<div>Roll: <strong>${Math.round(gamma)}°</strong></div>
`;
}
});
function findNearestTarget(angle) {
let nearest = targets[0];
let minDiff = Math.abs(normalizeAngle(angle - nearest.angle));
targets.forEach(target => {
const diff = Math.abs(normalizeAngle(angle - target.angle));
if (diff < minDiff) {
minDiff = diff;
nearest = target;
}
});
return nearest;
}
function normalizeAngle(angle) {
while (angle > 180) angle -= 360;
while (angle < -180) angle += 360;
return angle;
}
</script>
<style>
.ar-pointer {
max-width: 900px;
margin: 0 auto;
padding: 20px;
}
.ar-view {
width: 100%;
height: 500px;
background: linear-gradient(to bottom, #0ea5e9 0%, #38bdf8 50%, #7dd3fc 100%);
border-radius: 12px;
overflow: hidden;
position: relative;
margin: 20px 0;
perspective: 1000px;
}
.scene {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 0.05s ease-out;
}
.horizon-line {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 2px;
background: rgba(255, 255, 255, 0.5);
transform: translateZ(0);
}
.pointer {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 100px;
height: 100px;
z-index: 10;
}
.arrow {
width: 0;
height: 0;
border-left: 30px solid transparent;
border-right: 30px solid transparent;
border-bottom: 60px solid rgba(239, 68, 68, 0.8);
position: absolute;
top: 0;
left: 50%;
transform: translateX(-50%);
}
.target-info {
position: absolute;
bottom: -40px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
white-space: nowrap;
transition: opacity 0.3s;
}
.poi {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) rotateZ(var(--angle)) translateY(-200px);
}
.poi-marker {
width: 20px;
height: 20px;
background: rgba(255, 255, 255, 0.8);
border: 3px solid #1f2937;
border-radius: 50%;
margin: 0 auto;
}
.poi-label {
margin-top: 10px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 6px 12px;
border-radius: 12px;
font-size: 14px;
font-weight: bold;
text-align: center;
white-space: nowrap;
}
.orientation-data {
display: flex;
justify-content: space-around;
padding: 20px;
background: #f9fafb;
border-radius: 8px;
font-size: 18px;
}
.orientation-data strong {
color: #3b82f6;
}
</style>