Device Orientation Camera Viewer
Visualize real-time smartphone tilt angles with a 3D CAD-style camera frustum using Web standard DeviceOrientation events and pure CSS 3D transforms.
Key Implementation Points
3D Stage and Perspective Setup
Create an isometric 3D viewport using perspective and transform-style: preserve-3d with an orientation-neutral floor grid for depth perception.
.viewport-stage {
width: 100%;
height: 340px;
perspective: 900px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.world-space {
position: relative;
width: 0;
height: 0;
transform-style: preserve-3d;
transform: rotateX(62deg) rotateZ(-36deg);
transition: transform 0.2s ease-out;
}
/* 3D Reference Floor Grid */
.floor-grid {
position: absolute;
width: 300px;
height: 300px;
left: -150px;
top: -150px;
transform: translateZ(-90px);
background-color: rgba(255, 255, 255, 0.7);
background-image:
linear-gradient(to right, rgba(148, 163, 184, 0.35) 1px, transparent 1px),
linear-gradient(to bottom, rgba(148, 163, 184, 0.35) 1px, transparent 1px);
background-size: 30px 30px;
border: 1px solid #cbd5e1;
border-radius: 12px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05);
pointer-events: none;
}
.floor-crosshair {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.floor-crosshair::before,
.floor-crosshair::after {
content: "";
position: absolute;
background-color: rgba(100, 116, 139, 0.45);
}
.floor-crosshair::before { width: 100%; height: 1px; }
.floor-crosshair::after { width: 1px; height: 100%; }
Camera Frustum and Axes with Pure CSS 3D
Construct a wireframe view frustum and RGB coordinate axes that rotate via intrinsic Euler angles (rotateZ for yaw, rotateX for pitch, and rotateY for roll) mapped from CSS custom properties.
/* Dynamic Camera Rig rotated by device orientation angles */
.camera-rig {
position: absolute;
width: 0;
height: 0;
transform-style: preserve-3d;
/* Applying intrinsic Euler angles: yaw (alpha) -> pitch (beta) -> roll (gamma) */
transform:
rotateZ(calc(var(--alpha, 0) * 1deg))
rotateX(calc(var(--beta, 0) * 1deg))
rotateY(calc(var(--gamma, 0) * 1deg));
transition: transform 0.06s linear;
will-change: transform;
}
/* Smartphone / Camera Body */
.device-body {
position: absolute;
width: 96px;
height: 164px;
left: -48px;
top: -82px;
background: linear-gradient(145deg, #1e293b, #0f172a);
border: 2px solid #334155;
border-radius: 16px;
box-shadow: 0 16px 28px -6px rgba(15, 23, 42, 0.35);
transform-style: preserve-3d;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 8px;
}
/* Device Screen Graphic */
.device-screen {
width: 100%;
height: 100%;
border-radius: 10px;
background: radial-gradient(circle at center, rgba(56, 189, 248, 0.25) 0%, rgba(15, 23, 42, 0.9) 80%);
border: 1px solid rgba(56, 189, 248, 0.35);
display: grid;
place-items: center;
position: relative;
overflow: hidden;
}
/* Screen viewfinder reticle */
.screen-reticle {
width: 28px;
height: 28px;
border: 1px dashed rgba(56, 189, 248, 0.7);
border-radius: 50%;
position: relative;
}
.screen-reticle::after {
content: "";
position: absolute;
inset: 50%;
width: 4px;
height: 4px;
background-color: #38bdf8;
border-radius: 50%;
transform: translate(-50%, -50%);
}
.device-notch {
width: 24px;
height: 3px;
background-color: #475569;
border-radius: 2px;
position: absolute;
top: 5px;
}
/* 3D Camera View Frustum */
.camera-frustum {
position: absolute;
left: 0;
top: 0;
transform-style: preserve-3d;
pointer-events: none;
}
/* Far plane projection frame */
.frustum-far-plane {
position: absolute;
width: 130px;
height: 84px;
left: -65px;
top: -42px;
transform: translateZ(120px);
border: 1.5px solid #0284c7;
background-color: rgba(14, 165, 233, 0.08);
box-shadow: 0 0 16px rgba(14, 165, 233, 0.2);
border-radius: 4px;
}
/* Corner bracket accents on frustum far plane */
.frustum-far-plane::before,
.frustum-far-plane::after {
content: "";
position: absolute;
width: 8px;
height: 8px;
border-color: #0284c7;
border-style: solid;
}
.frustum-far-plane::before {
top: -2px; left: -2px;
border-width: 2px 0 0 2px;
}
.frustum-far-plane::after {
bottom: -2px; right: -2px;
border-width: 0 2px 2px 0;
}
/* 4 Corner wireframe lines connecting origin to far plane */
.frustum-ray {
position: absolute;
left: 0;
top: 0;
width: 1px;
height: 140px;
background: linear-gradient(to bottom, rgba(2, 132, 199, 0.8), rgba(2, 132, 199, 0.08));
transform-origin: top center;
}
.ray-tl { transform: rotateY(25deg) rotateX(-18deg); }
.ray-tr { transform: rotateY(-25deg) rotateX(-18deg); }
.ray-bl { transform: rotateY(25deg) rotateX(18deg); }
.ray-br { transform: rotateY(-25deg) rotateX(18deg); }
/* Coordinate Axes Gizmo (X: Red, Y: Green, Z: Blue) */
.axis-gizmo {
position: absolute;
left: 0;
top: 0;
transform-style: preserve-3d;
}
.axis-line {
position: absolute;
left: 0;
top: 0;
transform-origin: left center;
height: 2px;
display: flex;
align-items: center;
font-size: 9px;
font-weight: 700;
padding-left: 42px;
font-family: ui-monospace, SFMono-Regular, monospace;
}
.axis-x {
width: 40px;
background-color: #ef4444;
color: #ef4444;
transform: rotateY(0deg);
}
.axis-y {
width: 40px;
background-color: #10b981;
color: #10b981;
transform: rotateZ(90deg);
}
.axis-z {
width: 40px;
background-color: #3b82f6;
color: #3b82f6;
transform: rotateY(-90deg);
}
Viewer Markup and Telemetry HUD
Structure the camera rig inside the 3D world alongside real-time orientation telemetry HUD readouts.
<!-- 3D Camera Scene -->
<div class="viewport-stage" id="stage">
<div class="world-space" id="worldSpace">
<!-- Reference Floor Grid -->
<div class="floor-grid">
<div class="floor-crosshair"></div>
</div>
<!-- Dynamic Camera Rig -->
<div class="camera-rig" id="cameraRig">
<!-- Smartphone/Camera Body -->
<div class="device-body">
<div class="device-notch"></div>
<div class="device-screen">
<div class="screen-reticle"></div>
</div>
</div>
<!-- 3D View Frustum Wireframe -->
<div class="camera-frustum">
<div class="frustum-far-plane"></div>
<!-- Rays connecting origin to corners -->
<div class="frustum-ray ray-tl"></div>
<div class="frustum-ray ray-tr"></div>
<div class="frustum-ray ray-bl"></div>
<div class="frustum-ray ray-br"></div>
</div>
<!-- RGB Coordinate Axes Gizmo -->
<div class="axis-gizmo">
<div class="axis-line axis-x">+X</div>
<div class="axis-line axis-y">+Y</div>
<div class="axis-line axis-z">+Z</div>
</div>
</div>
</div>
</div>
<!-- Telemetry HUD & Sensor Activation -->
<div class="control-panel">
<div class="hud-header">
<span class="hud-title">Orientation Telemetry</span>
<span class="status-badge" id="statusBadge">
<span class="status-dot"></span>
<span id="statusText">Waiting</span>
</span>
</div>
<!-- Euler Angle Readouts -->
<div class="hud-readouts">
<div class="readout-card">
<div class="readout-label">Alpha (Yaw)</div>
<div class="readout-val" id="valAlpha">0°</div>
</div>
<div class="readout-card">
<div class="readout-label">Beta (Pitch)</div>
<div class="readout-val" id="valBeta">0°</div>
</div>
<div class="readout-card">
<div class="readout-label">Gamma (Roll)</div>
<div class="readout-val" id="valGamma">0°</div>
</div>
</div>
<!-- Sensor Activation Button (Handles iOS 13+ Permission Request) -->
<button type="button" class="action-btn" id="btnSensor">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="14" height="20" x="5" y="2" rx="2" ry="2"/>
<path d="M12 18h.01"/>
</svg>
<span>Enable Sensor</span>
</button>
</div>
Device Orientation Handling and iOS Permission
Acquire orientation angles via DeviceOrientationEvent, handle the required iOS 13+ permission workflow with DeviceOrientationEvent.requestPermission(), and update CSS variables in real time.
const cameraRig = document.getElementById("cameraRig");
const valAlpha = document.getElementById("valAlpha");
const valBeta = document.getElementById("valBeta");
const valGamma = document.getElementById("valGamma");
const statusBadge = document.getElementById("statusBadge");
const statusText = document.getElementById("statusText");
const btnSensor = document.getElementById("btnSensor");
let isSensorActive = false;
// Update 3D camera transforms and angle HUD
function updateOrientation(alpha, beta, gamma) {
const a = Math.round(alpha || 0);
const b = Math.round(beta || 0);
const g = Math.round(gamma || 0);
cameraRig.style.setProperty("--alpha", a);
cameraRig.style.setProperty("--beta", b);
cameraRig.style.setProperty("--gamma", g);
valAlpha.textContent = `${a}°`;
valBeta.textContent = `${b}°`;
valGamma.textContent = `${g}°`;
}
// Handler for DeviceOrientationEvent
function handleDeviceOrientation(event) {
if (event.beta === null && event.gamma === null) return;
if (!isSensorActive) {
isSensorActive = true;
statusBadge.classList.add("active");
statusText.textContent = "Active";
btnSensor.textContent = "Sensor Connected";
btnSensor.disabled = true;
}
updateOrientation(event.alpha, event.beta, event.gamma);
}
// Request permission and attach listener (iOS 13+ requires explicit user gesture)
async function requestSensorPermission() {
if (
typeof DeviceOrientationEvent !== "undefined" &&
typeof DeviceOrientationEvent.requestPermission === "function"
) {
try {
const permissionState = await DeviceOrientationEvent.requestPermission();
if (permissionState === "granted") {
window.addEventListener("deviceorientation", handleDeviceOrientation);
statusText.textContent = "Listening...";
} else {
statusText.textContent = "Denied";
}
} catch (error) {
console.error("Sensor permission request error:", error);
statusText.textContent = "Error";
}
} else if ("DeviceOrientationEvent" in window) {
window.addEventListener("deviceorientation", handleDeviceOrientation);
statusText.textContent = "Listening...";
} else {
statusText.textContent = "Unsupported";
}
}
btnSensor.addEventListener("click", requestSensorPermission);
// Try automatic attachment on load for browsers that do not require explicit gesture
if (
typeof DeviceOrientationEvent !== "undefined" &&
typeof DeviceOrientationEvent.requestPermission !== "function" &&
"DeviceOrientationEvent" in window
) {
window.addEventListener("deviceorientation", handleDeviceOrientation);
}
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Device Orientation Camera Viewer</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: #f8fafc;
background-image:
linear-gradient(#e2e8f0 1px, transparent 1px),
linear-gradient(90deg, #e2e8f0 1px, transparent 1px);
background-size: 24px 24px;
color: #0f172a;
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
min-height: 100vh;
display: grid;
place-items: center;
padding: 1.5rem;
user-select: none;
}
.scene {
width: 100%;
max-width: 440px;
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
}
.viewport-stage {
width: 100%;
height: 340px;
perspective: 900px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.world-space {
position: relative;
width: 0;
height: 0;
transform-style: preserve-3d;
transform: rotateX(62deg) rotateZ(-36deg);
transition: transform 0.2s ease-out;
}
/* 3D Reference Floor Grid */
.floor-grid {
position: absolute;
width: 300px;
height: 300px;
left: -150px;
top: -150px;
transform: translateZ(-90px);
background-color: rgba(255, 255, 255, 0.7);
background-image:
linear-gradient(to right, rgba(148, 163, 184, 0.35) 1px, transparent 1px),
linear-gradient(to bottom, rgba(148, 163, 184, 0.35) 1px, transparent 1px);
background-size: 30px 30px;
border: 1px solid #cbd5e1;
border-radius: 12px;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.05);
pointer-events: none;
}
.floor-crosshair {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
}
.floor-crosshair::before,
.floor-crosshair::after {
content: "";
position: absolute;
background-color: rgba(100, 116, 139, 0.45);
}
.floor-crosshair::before { width: 100%; height: 1px; }
.floor-crosshair::after { width: 1px; height: 100%; }
/* Dynamic Camera Rig rotated by device orientation angles */
.camera-rig {
position: absolute;
width: 0;
height: 0;
transform-style: preserve-3d;
/* Applying intrinsic Euler angles: yaw (alpha) -> pitch (beta) -> roll (gamma) */
transform:
rotateZ(calc(var(--alpha, 0) * 1deg))
rotateX(calc(var(--beta, 0) * 1deg))
rotateY(calc(var(--gamma, 0) * 1deg));
transition: transform 0.06s linear;
will-change: transform;
}
/* Smartphone / Camera Body */
.device-body {
position: absolute;
width: 96px;
height: 164px;
left: -48px;
top: -82px;
background: linear-gradient(145deg, #1e293b, #0f172a);
border: 2px solid #334155;
border-radius: 16px;
box-shadow: 0 16px 28px -6px rgba(15, 23, 42, 0.35);
transform-style: preserve-3d;
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-between;
padding: 8px;
}
/* Device Screen Graphic */
.device-screen {
width: 100%;
height: 100%;
border-radius: 10px;
background: radial-gradient(circle at center, rgba(56, 189, 248, 0.25) 0%, rgba(15, 23, 42, 0.9) 80%);
border: 1px solid rgba(56, 189, 248, 0.35);
display: grid;
place-items: center;
position: relative;
overflow: hidden;
}
/* Screen viewfinder reticle */
.screen-reticle {
width: 28px;
height: 28px;
border: 1px dashed rgba(56, 189, 248, 0.7);
border-radius: 50%;
position: relative;
}
.screen-reticle::after {
content: "";
position: absolute;
inset: 50%;
width: 4px;
height: 4px;
background-color: #38bdf8;
border-radius: 50%;
transform: translate(-50%, -50%);
}
.device-notch {
width: 24px;
height: 3px;
background-color: #475569;
border-radius: 2px;
position: absolute;
top: 5px;
}
/* 3D Camera View Frustum */
.camera-frustum {
position: absolute;
left: 0;
top: 0;
transform-style: preserve-3d;
pointer-events: none;
}
/* Far plane projection frame */
.frustum-far-plane {
position: absolute;
width: 130px;
height: 84px;
left: -65px;
top: -42px;
transform: translateZ(120px);
border: 1.5px solid #0284c7;
background-color: rgba(14, 165, 233, 0.08);
box-shadow: 0 0 16px rgba(14, 165, 233, 0.2);
border-radius: 4px;
}
/* Corner bracket accents on frustum far plane */
.frustum-far-plane::before,
.frustum-far-plane::after {
content: "";
position: absolute;
width: 8px;
height: 8px;
border-color: #0284c7;
border-style: solid;
}
.frustum-far-plane::before {
top: -2px; left: -2px;
border-width: 2px 0 0 2px;
}
.frustum-far-plane::after {
bottom: -2px; right: -2px;
border-width: 0 2px 2px 0;
}
/* 4 Corner wireframe lines connecting origin to far plane */
.frustum-ray {
position: absolute;
left: 0;
top: 0;
width: 1px;
height: 140px;
background: linear-gradient(to bottom, rgba(2, 132, 199, 0.8), rgba(2, 132, 199, 0.08));
transform-origin: top center;
}
.ray-tl { transform: rotateY(25deg) rotateX(-18deg); }
.ray-tr { transform: rotateY(-25deg) rotateX(-18deg); }
.ray-bl { transform: rotateY(25deg) rotateX(18deg); }
.ray-br { transform: rotateY(-25deg) rotateX(18deg); }
/* Coordinate Axes Gizmo (X: Red, Y: Green, Z: Blue) */
.axis-gizmo {
position: absolute;
left: 0;
top: 0;
transform-style: preserve-3d;
}
.axis-line {
position: absolute;
left: 0;
top: 0;
transform-origin: left center;
height: 2px;
display: flex;
align-items: center;
font-size: 9px;
font-weight: 700;
padding-left: 42px;
font-family: ui-monospace, SFMono-Regular, monospace;
}
.axis-x {
width: 40px;
background-color: #ef4444;
color: #ef4444;
transform: rotateY(0deg);
}
.axis-y {
width: 40px;
background-color: #10b981;
color: #10b981;
transform: rotateZ(90deg);
}
.axis-z {
width: 40px;
background-color: #3b82f6;
color: #3b82f6;
transform: rotateY(-90deg);
}
/* UI Control Panel & HUD */
.control-panel {
width: 100%;
background: rgba(255, 255, 255, 0.85);
backdrop-filter: blur(8px);
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 16px;
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 1rem;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 10px 15px -3px rgba(0, 0, 0, 0.05);
}
.hud-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.hud-title {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
color: #64748b;
}
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.375rem;
padding: 0.2rem 0.625rem;
background-color: #f1f5f9;
border: 1px solid #e2e8f0;
border-radius: 9999px;
color: #475569;
font-size: 0.75rem;
font-weight: 600;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #94a3b8;
}
.status-badge.active {
background-color: #dcfce7;
border-color: #86efac;
color: #166534;
}
.status-badge.active .status-dot {
background-color: #22c55e;
box-shadow: 0 0 6px #22c55e;
}
/* Value Readout Grid */
.hud-readouts {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
.readout-card {
background-color: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
padding: 0.625rem 0.5rem;
text-align: center;
}
.readout-label {
font-size: 0.7rem;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
}
.readout-val {
font-size: 1.125rem;
font-weight: 700;
color: #0f172a;
font-family: ui-monospace, SFMono-Regular, monospace;
margin-top: 0.15rem;
}
.action-btn {
width: 100%;
padding: 0.625rem 1rem;
border-radius: 9999px;
border: 1px solid #0f172a;
background: #0f172a;
color: #ffffff;
font-family: inherit;
font-size: 0.875rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
transition: all 0.2s ease;
}
.action-btn:hover {
background: #1e293b;
border-color: #1e293b;
}
.action-btn:active {
transform: scale(0.98);
}
.action-btn:disabled {
background: #f1f5f9;
border-color: #e2e8f0;
color: #94a3b8;
cursor: default;
box-shadow: none;
transform: none;
}
</style>
</head>
<body>
<main class="scene">
<!-- 3D Camera Scene -->
<div class="viewport-stage" id="stage">
<div class="world-space" id="worldSpace">
<!-- Reference Floor Grid -->
<div class="floor-grid">
<div class="floor-crosshair"></div>
</div>
<!-- Dynamic Camera Rig -->
<div class="camera-rig" id="cameraRig">
<!-- Smartphone/Camera Body -->
<div class="device-body">
<div class="device-notch"></div>
<div class="device-screen">
<div class="screen-reticle"></div>
</div>
</div>
<!-- 3D View Frustum Wireframe -->
<div class="camera-frustum">
<div class="frustum-far-plane"></div>
<!-- Rays connecting origin to corners -->
<div class="frustum-ray ray-tl"></div>
<div class="frustum-ray ray-tr"></div>
<div class="frustum-ray ray-bl"></div>
<div class="frustum-ray ray-br"></div>
</div>
<!-- RGB Coordinate Axes Gizmo -->
<div class="axis-gizmo">
<div class="axis-line axis-x">+X</div>
<div class="axis-line axis-y">+Y</div>
<div class="axis-line axis-z">+Z</div>
</div>
</div>
</div>
</div>
<!-- Telemetry HUD & Sensor Activation -->
<div class="control-panel">
<div class="hud-header">
<span class="hud-title">Orientation Telemetry</span>
<span class="status-badge" id="statusBadge">
<span class="status-dot"></span>
<span id="statusText">Waiting</span>
</span>
</div>
<!-- Euler Angle Readouts -->
<div class="hud-readouts">
<div class="readout-card">
<div class="readout-label">Alpha (Yaw)</div>
<div class="readout-val" id="valAlpha">0°</div>
</div>
<div class="readout-card">
<div class="readout-label">Beta (Pitch)</div>
<div class="readout-val" id="valBeta">0°</div>
</div>
<div class="readout-card">
<div class="readout-label">Gamma (Roll)</div>
<div class="readout-val" id="valGamma">0°</div>
</div>
</div>
<!-- Sensor Activation Button (Handles iOS 13+ Permission Request) -->
<button type="button" class="action-btn" id="btnSensor">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="14" height="20" x="5" y="2" rx="2" ry="2"/>
<path d="M12 18h.01"/>
</svg>
<span>Enable Sensor</span>
</button>
</div>
</main>
<script>
const cameraRig = document.getElementById("cameraRig");
const valAlpha = document.getElementById("valAlpha");
const valBeta = document.getElementById("valBeta");
const valGamma = document.getElementById("valGamma");
const statusBadge = document.getElementById("statusBadge");
const statusText = document.getElementById("statusText");
const btnSensor = document.getElementById("btnSensor");
let isSensorActive = false;
// Update 3D camera transforms and angle HUD
function updateOrientation(alpha, beta, gamma) {
const a = Math.round(alpha || 0);
const b = Math.round(beta || 0);
const g = Math.round(gamma || 0);
cameraRig.style.setProperty("--alpha", a);
cameraRig.style.setProperty("--beta", b);
cameraRig.style.setProperty("--gamma", g);
valAlpha.textContent = `${a}°`;
valBeta.textContent = `${b}°`;
valGamma.textContent = `${g}°`;
}
// Handler for DeviceOrientationEvent
function handleDeviceOrientation(event) {
if (event.beta === null && event.gamma === null) return;
if (!isSensorActive) {
isSensorActive = true;
statusBadge.classList.add("active");
statusText.textContent = "Active";
btnSensor.textContent = "Sensor Connected";
btnSensor.disabled = true;
}
updateOrientation(event.alpha, event.beta, event.gamma);
}
// Request permission and attach listener (iOS 13+ requires explicit user gesture)
async function requestSensorPermission() {
if (
typeof DeviceOrientationEvent !== "undefined" &&
typeof DeviceOrientationEvent.requestPermission === "function"
) {
try {
const permissionState = await DeviceOrientationEvent.requestPermission();
if (permissionState === "granted") {
window.addEventListener("deviceorientation", handleDeviceOrientation);
statusText.textContent = "Listening...";
} else {
statusText.textContent = "Denied";
}
} catch (error) {
console.error("Sensor permission request error:", error);
statusText.textContent = "Error";
}
} else if ("DeviceOrientationEvent" in window) {
window.addEventListener("deviceorientation", handleDeviceOrientation);
statusText.textContent = "Listening...";
} else {
statusText.textContent = "Unsupported";
}
}
btnSensor.addEventListener("click", requestSensorPermission);
// Try automatic attachment on load for browsers that do not require explicit gesture
if (
typeof DeviceOrientationEvent !== "undefined" &&
typeof DeviceOrientationEvent.requestPermission !== "function" &&
"DeviceOrientationEvent" in window
) {
window.addEventListener("deviceorientation", handleDeviceOrientation);
}
</script>
</body>
</html>