Bachelorarbeit/packages/model-viewer/index.html

1267 lines
45 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ceiling-AR Design Parameter Assessment Suite</title>
<script type="module" src="./dist/model-viewer.js"></script>
<style>
/* This keeps child nodes hidden while the element loads */
:not(:defined) > * {
display: none;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
margin: 0;
padding: 20px;
background: #f5f7fa;
line-height: 1.6;
}
.test-container {
max-width: 900px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
}
model-viewer {
width: 100%;
height: 400px;
background-color: #eee;
overflow-x: hidden;
}
.webxr-container {
width: 100%;
height: 120px;
background: transparent;
}
.webxr-container > :not([slot="ar-button"]) {
display: none !important;
}
/* WebXR UI Elements */
.vr-ui-container {
position: absolute;
bottom: 16px;
width: 100%;
text-align: center;
pointer-events: none;
display: none;
}
model-viewer[ar-status="session-started"] .vr-ui-container {
display: block;
pointer-events: auto;
}
.vr-slider {
position: absolute;
bottom: 120px;
left: 50%;
transform: translateX(-50%);
width: 300px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px;
border-radius: 10px;
backdrop-filter: blur(10px);
}
.vr-slider input[type="range"] {
width: 100%;
margin: 10px 0;
height: 6px;
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
outline: none;
-webkit-appearance: none;
}
.vr-slider input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: #4285f4;
border-radius: 50%;
cursor: pointer;
}
.vr-slider input[type="range"]::-moz-range-thumb {
width: 20px;
height: 20px;
background: #4285f4;
border-radius: 50%;
cursor: pointer;
border: none;
}
.vr-controls {
position: absolute;
bottom: 200px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
}
.vr-button {
background: rgba(66, 133, 244, 0.9);
color: white;
border: none;
padding: 12px 20px;
border-radius: 20px;
cursor: pointer;
font-size: 14px;
backdrop-filter: blur(10px);
}
.vr-button:hover {
background: rgba(66, 133, 244, 1);
}
.vr-text-display {
position: absolute;
bottom: 280px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 15px;
border-radius: 10px;
max-width: 250px;
text-align: center;
backdrop-filter: blur(10px);
}
/* Regular UI styles */
.test-instructions {
background: linear-gradient(135deg, #e3f2fd, #f0f8ff);
padding: 25px;
border-radius: 8px;
margin-bottom: 25px;
border-left: 4px solid #2196F3;
}
.test-instructions h3 {
margin-top: 0;
color: #1976d2;
}
.button-container {
text-align: center;
margin: 25px 0;
}
button {
background: #2196F3;
color: white;
border: none;
padding: 14px 28px;
border-radius: 6px;
cursor: pointer;
margin: 8px;
font-size: 16px;
font-weight: 500;
transition: all 0.2s ease;
}
button:hover {
background: #1976D2;
transform: translateY(-1px);
}
button:disabled {
background: #adb5bd;
cursor: not-allowed;
transform: none;
}
.redo-button {
background: #ff9800;
}
.redo-button:hover {
background: #f57c00;
}
.hidden {
display: none;
}
.consent-box {
background: #fff8e1;
border: 1px solid #ffcc02;
padding: 25px;
border-radius: 8px;
margin-bottom: 30px;
}
.progress-bar {
width: 100%;
height: 8px;
background: #e9ecef;
border-radius: 4px;
margin: 20px 0;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #2196F3, #21CBF3);
transition: width 0.3s ease;
border-radius: 4px;
}
.status-display {
background: #f8f9fa;
padding: 15px;
margin: 15px 0;
border-radius: 8px;
border: 1px solid #e9ecef;
font-family: 'Courier New', monospace;
font-size: 12px;
}
#ar-button {
background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEyIDJMMTMuMDkgOC4yNkwyMCA5TDEzLjA5IDE1Ljc0TDEyIDIyTDEwLjkxIDE1Ljc0TDQgOUwxMC45MSA4LjI2TDEyIDJaIiBmaWxsPSIjNDI4NWY0Ii8+Cjwvc3ZnPgo=);
background-repeat: no-repeat;
background-size: 20px 20px;
background-position: 12px 50%;
background-color: #fff;
position: absolute;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
bottom: 132px;
padding: 0px 16px 0px 40px;
font-family: Roboto Regular, Helvetica Neue, sans-serif;
font-size: 14px;
color: #4285f4;
height: 36px;
line-height: 36px;
border-radius: 18px;
border: 1px solid #DADCE0;
}
#ar-button:active {
background-color: #E8EAED;
}
#ar-button:focus {
outline: none;
}
#ar-button:focus-visible {
outline: 1px solid #4285f4;
}
@media (max-width: 768px) {
.test-container {
padding: 15px;
}
.vr-slider {
width: 250px;
padding: 15px;
}
}
</style>
</head>
<body>
<!-- Informed Consent -->
<div id="consent-screen" class="test-container">
<div class="consent-box">
<h2>Informed Consent - Ceiling-AR Design Parameter Assessment</h2>
<p><strong>Purpose:</strong> This empirical study investigates optimal design parameters for ceiling-based augmented reality applications within a Design Science Research framework for academic research purposes.</p>
<p><strong>Data Collection Protocol:</strong></p>
<ul>
<li>Device specifications (display dimensions, browser version, operating system)</li>
<li>WebXR interaction metrics and motion tracking data</li>
<li>Parameter adjustment patterns and timing</li>
<li>Device orientation and viewing angle parameters</li>
<li>First input detection and natural gesture analysis</li>
<li>Optional demographic information (age)</li>
</ul>
<p><strong>Data Protection:</strong> All collected data will be anonymized and used exclusively for scientific analysis. No personally identifiable information is recorded.</p>
<p><strong>Voluntary Participation:</strong> Participation is entirely voluntary. You may withdraw at any time without consequence.</p>
<label>
<input type="checkbox" id="consent-checkbox">
I consent to data collection and agree to participate in this research study.
</label>
</div>
<button onclick="startTestSuite()" id="start-button" disabled>Initialize Assessment Suite</button>
</div>
<!-- Assessment Suite Container -->
<div id="test-suite" class="test-container hidden">
<h1>Ceiling-AR Design Parameter Assessment Suite</h1>
<div id="progress">Assessment <span id="current-test">1</span> of <span id="total-tests">7</span></div>
<div class="progress-bar">
<div class="progress-fill" id="progress-fill" style="width: 14.3%"></div>
</div>
<div id="status-display" class="status-display">
Status: Ready to begin assessments
</div>
<!-- Assessment 1: First Input Detection -->
<div id="test-1" class="test-phase">
<div class="test-instructions">
<h3>Assessment 1: Natural Interaction Detection</h3>
<p><strong>Objective:</strong> Capture your first natural interaction with ceiling-mounted AR content to understand intuitive user behavior patterns.</p>
<p><strong>Procedure:</strong> Enter WebXR and make your first natural gesture. The system will automatically record this interaction and proceed to the next test.</p>
</div>
<model-viewer
id="first-input-model"
src="https://modelviewer.dev/shared-assets/models/Astronaut.glb"
ar
ar-modes="webxr"
camera-orbit="0deg 75deg 2m"
ar-placement="ceiling"
reveal="manual">
<button slot="ar-button" id="ar-button">
Enter WebXR - First Input Test
</button>
<div class="vr-ui-container">
<div class="vr-text-display">
<div id="first-input-status">Make your first natural gesture</div>
</div>
</div>
</model-viewer>
<div class="button-container">
<button onclick="manualCompleteFirstTest()" id="first-input-done" class="hidden">Manual Complete</button>
<button onclick="redoTest(1)" class="redo-button hidden" id="first-input-redo">Redo Assessment</button>
</div>
</div>
<!-- Assessment 2: Rotation Speed Optimization -->
<div id="test-2" class="test-phase hidden">
<div class="test-instructions">
<h3>Assessment 2: Rotation Speed Optimization</h3>
<p><strong>Objective:</strong> Determine optimal rotation speed for ceiling-AR applications through direct WebXR interaction.</p>
<p><strong>Procedure:</strong> Use the slider in WebXR to adjust rotation speed. Find your preferred speed for ceiling-based object examination.</p>
</div>
<model-viewer
id="speed-model"
src="https://modelviewer.dev/shared-assets/models/Astronaut.glb"
ar
ar-modes="webxr"
camera-orbit="0deg 75deg 2m"
ar-placement="ceiling"
auto-rotate
reveal="manual">
<button slot="ar-button" id="ar-button">
Enter WebXR - Speed Test
</button>
<div class="vr-ui-container">
<div class="vr-slider">
<div style="margin-bottom: 10px;">Rotation Speed</div>
<input type="range" id="speed-slider-vr" min="10" max="100" step="5" value="50">
<div id="speed-value-vr" style="text-align: center;">50%</div>
</div>
<div class="vr-controls">
<button class="vr-button" onclick="confirmSpeedSetting()">Confirm Speed</button>
</div>
</div>
</model-viewer>
<div class="button-container">
<button onclick="nextTest()" id="speed-done" class="hidden">Confirm Speed Setting</button>
<button onclick="redoTest(2)" class="redo-button hidden" id="speed-redo">Redo Assessment</button>
</div>
</div>
<!-- Assessment 3: Distance and Angle Optimization -->
<div id="test-3" class="test-phase hidden">
<div class="test-instructions">
<h3>Assessment 3: Ergonomic Position Optimization</h3>
<p><strong>Objective:</strong> Find optimal viewing distance and angle for comfortable ceiling-AR interaction.</p>
<p><strong>Procedure:</strong> Use WebXR sliders to adjust viewing distance and angle. Position for maximum comfort.</p>
</div>
<model-viewer
id="ergonomic-model"
src="https://modelviewer.dev/shared-assets/models/Astronaut.glb"
ar
ar-modes="webxr"
camera-orbit="0deg 75deg 2m"
ar-placement="ceiling"
reveal="manual">
<button slot="ar-button" id="ar-button">
Enter WebXR - Ergonomic Test
</button>
<div class="vr-ui-container">
<div class="vr-slider">
<div style="margin-bottom: 10px;">Distance: <span id="distance-value-vr">2.0m</span></div>
<input type="range" id="distance-slider-vr" min="0.5" max="4.0" step="0.1" value="2.0">
<div style="margin: 15px 0 10px;">Angle: <span id="angle-value-vr">75°</span></div>
<input type="range" id="angle-slider-vr" min="45" max="90" step="5" value="75">
</div>
<div class="vr-controls">
<button class="vr-button" onclick="confirmErgonomicSettings()">Confirm Settings</button>
</div>
</div>
</model-viewer>
<div class="button-container">
<button onclick="nextTest()" id="ergonomic-done" class="hidden">Confirm Position Settings</button>
<button onclick="redoTest(3)" class="redo-button hidden" id="ergonomic-redo">Redo Assessment</button>
</div>
</div>
<!-- Assessment 4: Text Size Optimization -->
<div id="test-4" class="test-phase hidden">
<div class="test-instructions">
<h3>Assessment 4: Text Legibility Optimization</h3>
<p><strong>Objective:</strong> Determine optimal text scaling for ceiling-AR applications ensuring readability at viewing angles.</p>
<p><strong>Procedure:</strong> Adjust text size using WebXR slider until text is clearly legible yet appropriately sized.</p>
</div>
<model-viewer
id="text-model"
src="https://modelviewer.dev/shared-assets/models/Astronaut.glb"
ar
ar-modes="webxr"
camera-orbit="0deg 75deg 2m"
ar-placement="ceiling"
reveal="manual">
<button slot="ar-button" id="ar-button">
Enter WebXR - Text Size Test
</button>
<button class="hotspot" slot="hotspot-text" data-position="0.2 0.1 0.1" data-normal="0 1 0">
<div class="annotation" id="test-text" style="font-size: 16px; background: rgba(255,255,255,0.9); padding: 10px; border-radius: 5px; color: black;">
This sample text represents typical information display requirements for ceiling-AR applications.
Text must remain legible despite challenging viewing angles inherent to overhead displays.
</div>
</button>
<div class="vr-ui-container">
<div class="vr-slider">
<div style="margin-bottom: 10px;">Text Size: <span id="text-size-value-vr">16px</span></div>
<input type="range" id="text-size-slider-vr" min="10" max="32" step="2" value="16">
</div>
<div class="vr-controls">
<button class="vr-button" onclick="confirmTextSize()">Confirm Text Size</button>
</div>
</div>
</model-viewer>
<div class="button-container">
<button onclick="nextTest()" id="text-done" class="hidden">Confirm Text Size</button>
<button onclick="redoTest(4)" class="redo-button hidden" id="text-redo">Redo Assessment</button>
</div>
</div>
<!-- Assessment 5: Scale Reference -->
<div id="test-5" class="test-phase hidden">
<div class="test-instructions">
<h3>Assessment 5: Scale Reference Calibration</h3>
<p><strong>Objective:</strong> Establish optimal object scaling using natural size perception for ceiling-AR content.</p>
<p><strong>Procedure:</strong> Adjust object scale in WebXR until it appears at an appropriate size reference.</p>
</div>
<model-viewer
id="reference-model"
src="https://modelviewer.dev/shared-assets/models/Astronaut.glb"
ar
ar-modes="webxr"
camera-orbit="0deg 75deg 2m"
ar-placement="ceiling"
reveal="manual">
<button slot="ar-button" id="ar-button">
Enter WebXR - Scale Reference Test
</button>
<div class="vr-ui-container">
<div class="vr-slider">
<div style="margin-bottom: 10px;">Object Scale: <span id="scale-value-vr">100%</span></div>
<input type="range" id="scale-slider-vr" min="30" max="300" step="10" value="100">
</div>
<div class="vr-controls">
<button class="vr-button" onclick="confirmScale()">Confirm Scale</button>
</div>
</div>
</model-viewer>
<div class="button-container">
<button onclick="nextTest()" id="reference-done" class="hidden">Confirm Scale Reference</button>
<button onclick="redoTest(5)" class="redo-button hidden" id="reference-redo">Redo Assessment</button>
</div>
</div>
<!-- Assessment 6: Spatial Positioning -->
<div id="test-6" class="test-phase hidden">
<div class="test-instructions">
<h3>Assessment 6: Spatial Positioning Optimization</h3>
<p><strong>Objective:</strong> Determine optimal spatial positioning for ceiling-AR content placement achieving natural ceiling integration.</p>
<p><strong>Procedure:</strong> Adjust X/Y positioning in WebXR to achieve optimal visual anchoring.</p>
</div>
<model-viewer
id="ceiling-model"
src="https://modelviewer.dev/shared-assets/models/Astronaut.glb"
ar
ar-modes="webxr"
camera-orbit="0deg 90deg 2.5m"
ar-placement="ceiling"
reveal="manual">
<button slot="ar-button" id="ar-button">
Enter WebXR - Spatial Position Test
</button>
<div class="vr-ui-container">
<div class="vr-slider">
<div style="margin-bottom: 10px;">X Position: <span id="x-pos-value-vr">0</span></div>
<input type="range" id="x-position-slider-vr" min="-2" max="2" step="0.1" value="0">
<div style="margin: 15px 0 10px;">Y Position: <span id="y-pos-value-vr">0</span></div>
<input type="range" id="y-position-slider-vr" min="-2" max="2" step="0.1" value="0">
</div>
<div class="vr-controls">
<button class="vr-button" onclick="confirmPosition()">Confirm Position</button>
</div>
</div>
</model-viewer>
<div class="button-container">
<button onclick="nextTest()" id="ceiling-done" class="hidden">Confirm Spatial Position</button>
<button onclick="redoTest(6)" class="redo-button hidden" id="ceiling-redo">Redo Assessment</button>
</div>
</div>
<!-- Assessment 7: Demographics and Completion -->
<div id="test-7" class="test-phase hidden">
<div class="test-instructions">
<h3>Assessment 7: Study Completion and Data Collection</h3>
<p><strong>Objective:</strong> Collect final evaluations and demographic information for comprehensive analysis.</p>
<p><strong>Procedure:</strong> Provide overall assessment and any additional observations regarding the ceiling-AR interaction experience.</p>
</div>
<div style="margin: 20px 0;">
<label style="display: block; margin-bottom: 10px;">Age (optional for demographic analysis):</label>
<input type="number" id="age-input" min="16" max="99" placeholder="e.g., 25" style="padding: 8px; border-radius: 4px; border: 1px solid #ccc;">
</div>
<div style="margin: 20px 0;">
<label style="display: block; margin-bottom: 10px;">Additional observations or feedback:</label>
<textarea id="feedback-textarea" style="width: 100%; height: 120px; padding: 10px; border-radius: 4px; border: 1px solid #ccc;"
placeholder="Please describe any notable observations, challenges experienced with ceiling-AR interaction, or suggestions for interface improvements..."></textarea>
</div>
<div style="margin: 20px 0;">
<label style="display: block; margin-bottom: 15px;">Overall ceiling-AR interaction experience rating (1-10):</label>
<div style="display: flex; justify-content: space-between; flex-wrap: wrap; gap: 10px;">
<label style="display: flex; flex-direction: column; align-items: center; cursor: pointer; padding: 10px; border-radius: 6px;">
<input type="radio" name="overall" value="1">1<br><small>Poor</small>
</label>
<label style="display: flex; flex-direction: column; align-items: center; cursor: pointer; padding: 10px; border-radius: 6px;">
<input type="radio" name="overall" value="3">3<br><small>Below average</small>
</label>
<label style="display: flex; flex-direction: column; align-items: center; cursor: pointer; padding: 10px; border-radius: 6px;">
<input type="radio" name="overall" value="5">5<br><small>Average</small>
</label>
<label style="display: flex; flex-direction: column; align-items: center; cursor: pointer; padding: 10px; border-radius: 6px;">
<input type="radio" name="overall" value="7">7<br><small>Above average</small>
</label>
<label style="display: flex; flex-direction: column; align-items: center; cursor: pointer; padding: 10px; border-radius: 6px;">
<input type="radio" name="overall" value="10">10<br><small>Excellent</small>
</label>
</div>
</div>
<div class="button-container">
<button onclick="completeTestSuite()">Complete Assessment Suite & Export Data</button>
<button onclick="redoTest(7)" class="redo-button">Redo Final Assessment</button>
</div>
</div>
</div>
<!-- Completion Screen -->
<div id="completion-screen" class="test-container hidden">
<h2>Assessment Suite Completed Successfully</h2>
<p>Thank you for your participation in this research study. Your data has been successfully collected and is ready for download.</p>
<div class="button-container">
<button onclick="downloadResults()">Download Results as CSV</button>
</div>
<div id="data-preview" class="status-display" style="max-height: 300px; overflow-y: auto;">
</div>
</div>
<script>
class CeilingARLogger {
constructor() {
this.sessionId = this.generateSessionId();
this.startTime = Date.now();
this.currentTest = 0;
this.xrSession = null;
this.firstInputDetected = false;
this.motionLog = [];
this.sessionStartTime = null;
this.testData = {
device: {
sessionId: this.sessionId,
userAgent: navigator.userAgent,
displayWidth: window.innerWidth,
displayHeight: window.innerHeight,
pixelRatio: window.devicePixelRatio,
platform: navigator.platform,
language: navigator.language,
timestamp: new Date().toISOString()
},
results: {
firstInputTest: {
duration: 0,
firstInputType: null,
firstInputData: null,
motionFrames: 0,
sessionDuration: 0,
completedSuccessfully: false
},
speedTest: {
finalSpeed: 50,
adjustments: [],
interactionCount: 0,
timeInVR: 0,
completedSuccessfully: false
},
ergonomicTest: {
finalDistance: 2.0,
finalAngle: 75,
distanceAdjustments: [],
angleAdjustments: [],
interactionCount: 0,
timeInVR: 0,
completedSuccessfully: false
},
textTest: {
finalTextSize: 16,
adjustments: [],
interactionCount: 0,
timeInVR: 0,
completedSuccessfully: false
},
referenceTest: {
finalScale: 100,
adjustments: [],
interactionCount: 0,
timeInVR: 0,
completedSuccessfully: false
},
ceilingTest: {
finalXPosition: 0,
finalYPosition: 0,
xAdjustments: [],
yAdjustments: [],
interactionCount: 0,
timeInVR: 0,
completedSuccessfully: false
},
demographics: {
age: null,
feedback: '',
overallRating: null
}
}
};
this.setupEventListeners();
}
generateSessionId() {
return 'CEILING_AR_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
setupEventListeners() {
// Setup device orientation tracking
if (window.DeviceOrientationEvent) {
window.addEventListener('deviceorientation', (e) => {
this.logDeviceOrientation(e);
});
}
// Setup touch tracking
document.addEventListener('touchstart', (e) => {
this.logTouchEvent('touchstart', e);
});
}
logDeviceOrientation(event) {
const orientationData = {
timestamp: Date.now(),
alpha: event.alpha,
beta: event.beta,
gamma: event.gamma
};
if (!this.testData.orientationLog) {
this.testData.orientationLog = [];
}
this.testData.orientationLog.push(orientationData);
}
logTouchEvent(type, event) {
if (this.currentTest > 0) {
const touchData = {
timestamp: Date.now(),
type: type,
x: event.touches?.[0]?.clientX || 0,
y: event.touches?.[0]?.clientY || 0,
currentTest: this.currentTest
};
if (!this.testData.touchLog) {
this.testData.touchLog = [];
}
this.testData.touchLog.push(touchData);
}
}
initializeWebXRTracking(modelViewer, testType) {
modelViewer.addEventListener('ar-status', (event) => {
if (event.detail.status === 'session-started') {
this.xrSession = modelViewer.model.webXRCamera.session;
this.sessionStartTime = performance.now();
this.firstInputDetected = false;
this.motionLog = [];
console.log(`WebXR session started for ${testType}`);
this.updateStatus(`WebXR active - ${testType}`);
if (testType === 'firstInput') {
this.setupFirstInputTracking();
}
}
});
}
setupFirstInputTracking() {
if (!this.xrSession) return;
const onXRFrame = (time, frame) => {
if (!this.xrSession || this.firstInputDetected) return;
const inputSources = this.xrSession.inputSources;
for (let inputSource of inputSources) {
if (inputSource.gripSpace) {
const gripPose = frame.getPose(inputSource.gripSpace, this.xrSession.renderState.baseLayer.framebuffer);
if (gripPose) {
this.logMotionData({
timestamp: performance.now() - this.sessionStartTime,
type: 'grip',
position: gripPose.transform.position,
orientation: gripPose.transform.orientation,
inputSource: inputSource.handedness
});
}
}
// Check for first input
if (inputSource.gamepad) {
inputSource.gamepad.buttons.forEach((button, index) => {
if (button.pressed && !this.firstInputDetected) {
this.handleFirstInput('button', { buttonIndex: index, inputSource: inputSource.handedness });
return;
}
});
inputSource.gamepad.axes.forEach((axis, index) => {
if (Math.abs(axis) > 0.1 && !this.firstInputDetected) {
this.handleFirstInput('axis', { axisIndex: index, value: axis, inputSource: inputSource.handedness });
return;
}
});
}
}
if (this.xrSession && !this.firstInputDetected) {
this.xrSession.requestAnimationFrame(onXRFrame);
}
};
this.xrSession.requestAnimationFrame(onXRFrame);
}
logMotionData(data) {
this.motionLog.push({
...data,
sessionDuration: performance.now() - this.sessionStartTime
});
if (this.motionLog.length > 1000) {
this.motionLog = this.motionLog.slice(-500);
}
}
handleFirstInput(inputType, inputData) {
if (this.firstInputDetected) return;
this.firstInputDetected = true;
const endTime = performance.now();
const sessionDuration = endTime - this.sessionStartTime;
this.testData.results.firstInputTest.duration = sessionDuration;
this.testData.results.firstInputTest.firstInputType = inputType;
this.testData.results.firstInputTest.firstInputData = inputData;
this.testData.results.firstInputTest.motionFrames = this.motionLog.length;
this.testData.results.firstInputTest.sessionDuration = sessionDuration;
console.log('First input detected:', inputType, inputData);
this.updateStatus(`First input detected: ${inputType} - Auto completing test`);
// Update UI
document.getElementById('first-input-status').textContent = 'First input detected! Completing test...';
// Auto-complete first test
setTimeout(() => {
this.exitWebXRSession();
this.markTestComplete('firstInputTest');
nextTest();
}, 2000);
}
logParameterAdjustment(testType, parameter, value) {
const adjustment = {
timestamp: Date.now(),
parameter: parameter,
value: value
};
const testNames = {
'speed': 'speedTest',
'ergonomic': 'ergonomicTest',
'text': 'textTest',
'reference': 'referenceTest',
'ceiling': 'ceilingTest'
};
const testName = testNames[testType];
if (testName && this.testData.results[testName]) {
if (!this.testData.results[testName].adjustments) {
this.testData.results[testName].adjustments = [];
}
this.testData.results[testName].adjustments.push(adjustment);
this.testData.results[testName].interactionCount++;
}
}
exitWebXRSession() {
if (this.xrSession) {
this.xrSession.end().then(() => {
console.log('WebXR session ended');
this.xrSession = null;
}).catch(err => {
console.error('Error ending WebXR session:', err);
});
}
}
markTestComplete(testName) {
if (this.testData.results[testName]) {
this.testData.results[testName].completedSuccessfully = true;
this.testData.results[testName].endTime = Date.now();
}
}
updateStatus(message) {
const statusElement = document.getElementById('status-display');
if (statusElement) {
const timestamp = new Date().toLocaleTimeString();
statusElement.textContent = `[${timestamp}] ${message}`;
}
}
exportToCSV() {
const csvData = [];
csvData.push([
'SessionID', 'TestType', 'Parameter', 'Value', 'Timestamp',
'DeviceWidth', 'DeviceHeight', 'UserAgent', 'Age', 'OverallRating', 'Completed'
]);
Object.keys(this.testData.results).forEach(testName => {
const testResults = this.testData.results[testName];
Object.keys(testResults).forEach(param => {
csvData.push([
this.sessionId,
testName,
param,
JSON.stringify(testResults[param]),
Date.now(),
this.testData.device.displayWidth,
this.testData.device.displayHeight,
this.testData.device.userAgent,
this.testData.results.demographics.age || 'N/A',
this.testData.results.demographics.overallRating || 'N/A',
testResults.completedSuccessfully || false
]);
});
});
return csvData;
}
}
let logger = new CeilingARLogger();
let currentTestIndex = 1;
const totalTests = 7;
// Initialize consent checkbox
document.getElementById('consent-checkbox').addEventListener('change', function () {
document.getElementById('start-button').disabled = !this.checked;
});
function updateProgress() {
const progress = (currentTestIndex / totalTests) * 100;
document.getElementById('progress-fill').style.width = progress + '%';
document.getElementById('current-test').textContent = currentTestIndex;
}
function startTestSuite() {
document.getElementById('consent-screen').classList.add('hidden');
document.getElementById('test-suite').classList.remove('hidden');
logger.updateStatus('Test suite started - Beginning Assessment 1');
showTest(1);
initializeFirstInputTest();
}
function showTest(testNumber) {
// Hide all tests
for (let i = 1; i <= totalTests; i++) {
document.getElementById(`test-${i}`).classList.add('hidden');
}
// Show current test
document.getElementById(`test-${testNumber}`).classList.remove('hidden');
currentTestIndex = testNumber;
logger.currentTest = testNumber;
updateProgress();
}
function initializeFirstInputTest() {
const modelViewer = document.getElementById('first-input-model');
logger.initializeWebXRTracking(modelViewer, 'firstInput');
// Show manual complete button after 30 seconds
setTimeout(() => {
document.getElementById('first-input-done').classList.remove('hidden');
document.getElementById('first-input-redo').classList.remove('hidden');
}, 30000);
}
function manualCompleteFirstTest() {
if (!logger.firstInputDetected) {
logger.testData.results.firstInputTest.firstInputType = 'manual_completion';
logger.testData.results.firstInputTest.firstInputData = { reason: 'user_clicked_complete' };
logger.markTestComplete('firstInputTest');
}
nextTest();
}
// WebXR slider functions with event prevention
function setupWebXRSliders() {
// Speed test slider
const speedSlider = document.getElementById('speed-slider-vr');
if (speedSlider) {
speedSlider.addEventListener('beforexrselect', (ev) => {
ev.preventDefault();
});
speedSlider.addEventListener('input', function() {
const value = this.value;
document.getElementById('speed-value-vr').textContent = value + '%';
logger.testData.results.speedTest.finalSpeed = parseFloat(value);
logger.logParameterAdjustment('speed', 'rotationSpeed', parseFloat(value));
updateSpeedModel(value);
});
}
// Ergonomic test sliders
const distanceSlider = document.getElementById('distance-slider-vr');
if (distanceSlider) {
distanceSlider.addEventListener('beforexrselect', (ev) => {
ev.preventDefault();
});
distanceSlider.addEventListener('input', function() {
const value = this.value;
document.getElementById('distance-value-vr').textContent = value + 'm';
logger.testData.results.ergonomicTest.finalDistance = parseFloat(value);
logger.logParameterAdjustment('ergonomic', 'distance', parseFloat(value));
updateErgonomicModel();
});
}
const angleSlider = document.getElementById('angle-slider-vr');
if (angleSlider) {
angleSlider.addEventListener('beforexrselect', (ev) => {
ev.preventDefault();
});
angleSlider.addEventListener('input', function() {
const value = this.value;
document.getElementById('angle-value-vr').textContent = value + '°';
logger.testData.results.ergonomicTest.finalAngle = parseFloat(value);
logger.logParameterAdjustment('ergonomic', 'angle', parseFloat(value));
updateErgonomicModel();
});
}
// Text size slider
const textSizeSlider = document.getElementById('text-size-slider-vr');
if (textSizeSlider) {
textSizeSlider.addEventListener('beforexrselect', (ev) => {
ev.preventDefault();
});
textSizeSlider.addEventListener('input', function() {
const value = this.value;
document.getElementById('text-size-value-vr').textContent = value + 'px';
logger.testData.results.textTest.finalTextSize = parseFloat(value);
logger.logParameterAdjustment('text', 'fontSize', parseFloat(value));
updateTextSize(value);
});
}
// Scale slider
const scaleSlider = document.getElementById('scale-slider-vr');
if (scaleSlider) {
scaleSlider.addEventListener('beforexrselect', (ev) => {
ev.preventDefault();
});
scaleSlider.addEventListener('input', function() {
const value = this.value;
document.getElementById('scale-value-vr').textContent = value + '%';
logger.testData.results.referenceTest.finalScale = parseFloat(value);
logger.logParameterAdjustment('reference', 'scale', parseFloat(value));
updateReferenceScale(value / 100);
});
}
// Position sliders
const xPositionSlider = document.getElementById('x-position-slider-vr');
if (xPositionSlider) {
xPositionSlider.addEventListener('beforexrselect', (ev) => {
ev.preventDefault();
});
xPositionSlider.addEventListener('input', function() {
const value = this.value;
document.getElementById('x-pos-value-vr').textContent = value;
logger.testData.results.ceilingTest.finalXPosition = parseFloat(value);
logger.logParameterAdjustment('ceiling', 'xPosition', parseFloat(value));
updateCeilingPosition();
});
}
const yPositionSlider = document.getElementById('y-position-slider-vr');
if (yPositionSlider) {
yPositionSlider.addEventListener('beforexrselect', (ev) => {
ev.preventDefault();
});
yPositionSlider.addEventListener('input', function() {
const value = this.value;
document.getElementById('y-pos-value-vr').textContent = value;
logger.testData.results.ceilingTest.finalYPosition = parseFloat(value);
logger.logParameterAdjustment('ceiling', 'yPosition', parseFloat(value));
updateCeilingPosition();
});
}
}
// Model update functions
function updateSpeedModel(speed) {
const modelViewer = document.getElementById('speed-model');
if (modelViewer) {
modelViewer.style.setProperty('--auto-rotate-delay', `${101 - speed}0ms`);
}
}
function updateErgonomicModel() {
const modelViewer = document.getElementById('ergonomic-model');
if (modelViewer) {
const distance = logger.testData.results.ergonomicTest.finalDistance;
const angle = logger.testData.results.ergonomicTest.finalAngle;
modelViewer.cameraOrbit = `0deg ${angle}deg ${distance}m`;
}
}
function updateTextSize(size) {
const textElement = document.getElementById('test-text');
if (textElement) {
textElement.style.fontSize = size + 'px';
}
}
function updateReferenceScale(scale) {
const modelViewer = document.getElementById('reference-model');
if (modelViewer) {
modelViewer.scale = `${scale} ${scale} ${scale}`;
}
}
function updateCeilingPosition() {
const modelViewer = document.getElementById('ceiling-model');
if (modelViewer) {
const x = logger.testData.results.ceilingTest.finalXPosition;
const y = logger.testData.results.ceilingTest.finalYPosition;
modelViewer.cameraTarget = `${x}m 0m ${y}m`;
}
}
// WebXR confirmation functions
function confirmSpeedSetting() {
logger.markTestComplete('speedTest');
document.getElementById('speed-done').classList.remove('hidden');
document.getElementById('speed-redo').classList.remove('hidden');
}
function confirmErgonomicSettings() {
logger.markTestComplete('ergonomicTest');
document.getElementById('ergonomic-done').classList.remove('hidden');
document.getElementById('ergonomic-redo').classList.remove('hidden');
}
function confirmTextSize() {
logger.markTestComplete('textTest');
document.getElementById('text-done').classList.remove('hidden');
document.getElementById('text-redo').classList.remove('hidden');
}
function confirmScale() {
logger.markTestComplete('referenceTest');
document.getElementById('reference-done').classList.remove('hidden');
document.getElementById('reference-redo').classList.remove('hidden');
}
function confirmPosition() {
logger.markTestComplete('ceilingTest');
document.getElementById('ceiling-done').classList.remove('hidden');
document.getElementById('ceiling-redo').classList.remove('hidden');
}
function nextTest() {
// Mark current test as complete
const testNames = ['', 'firstInputTest', 'speedTest', 'ergonomicTest', 'textTest', 'referenceTest', 'ceilingTest', 'demographics'];
if (testNames[currentTestIndex]) {
logger.markTestComplete(testNames[currentTestIndex]);
}
logger.exitWebXRSession();
if (currentTestIndex < totalTests) {
showTest(currentTestIndex + 1);
} else {
completeTestSuite();
}
}
function redoTest(testNumber) {
logger.exitWebXRSession();
// Reset test data
const testNames = ['', 'firstInputTest', 'speedTest', 'ergonomicTest', 'textTest', 'referenceTest', 'ceilingTest', 'demographics'];
if (testNames[testNumber]) {
const testName = testNames[testNumber];
if (logger.testData.results[testName]) {
logger.testData.results[testName].completedSuccessfully = false;
logger.testData.results[testName].adjustments = [];
logger.testData.results[testName].interactionCount = 0;
}
}
// Reset UI and restart test
resetTestUI(testNumber);
showTest(testNumber);
if (testNumber === 1) {
setTimeout(() => initializeFirstInputTest(), 1000);
}
}
function resetTestUI(testNumber) {
const buttonSuffixes = ['first-input', 'speed', 'ergonomic', 'text', 'reference', 'ceiling'];
const suffix = testNumber === 1 ? 'first-input' : buttonSuffixes[testNumber - 1];
if (suffix) {
const doneButton = document.getElementById(`${suffix}-done`);
const redoButton = document.getElementById(`${suffix}-redo`);
if (doneButton) doneButton.classList.add('hidden');
if (redoButton) redoButton.classList.add('hidden');
}
}
function completeTestSuite() {
// Collect demographic data
logger.testData.results.demographics.age =
parseInt(document.getElementById('age-input').value) || null;
logger.testData.results.demographics.feedback =
document.getElementById('feedback-textarea').value;
const overallRating = document.querySelector('input[name="overall"]:checked');
logger.testData.results.demographics.overallRating =
overallRating ? parseInt(overallRating.value) : null;
logger.markTestComplete('demographics');
logger.updateStatus('All assessments completed successfully');
// Show completion screen
document.getElementById('test-suite').classList.add('hidden');
document.getElementById('completion-screen').classList.remove('hidden');
// Show data preview
const preview = JSON.stringify(logger.testData, null, 2);
document.getElementById('data-preview').textContent =
preview.substring(0, 2000) + '...\n[Truncated preview - complete data available in CSV export]';
}
function downloadResults() {
const csvData = logger.exportToCSV();
const csvContent = csvData.map(row => row.join(',')).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.setAttribute('hidden', '');
a.setAttribute('href', url);
a.setAttribute('download', `ceiling_ar_assessment_${logger.sessionId}.csv`);
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function () {
console.log('Ceiling-AR Assessment Suite initialized');
console.log('Session ID:', logger.sessionId);
logger.updateStatus('System initialized - Ready to begin');
updateProgress();
setupWebXRSliders();
});
</script>
</body>
</html>