aframe-babia-components
Version:
A data visualization set of components for A-Frame.
457 lines (384 loc) • 17.3 kB
JavaScript
/* global AFRAME */
if (typeof AFRAME === 'undefined') {
throw new Error('Component attempted to register before AFRAME was available.');
}
AFRAME.registerComponent('babia-experiment', {
schema: {
taskTitle: { type: 'string', default: 'Default task title' },
taskDescription: { type: 'string', default: 'Default task text' },
openTaskImg: { type: 'string' },
closeTaskImg: { type: 'string' },
timeLimitEnding: { type: 'boolean', default: false }, // If false, go for the button
timeLimitTime: { type: 'number', default: 300 }, // In seconds
forceFinishWhenTimeLimit: { type: 'boolean', default: false }, // In seconds
finishButton: { type: 'boolean', default: true },
recordDelta: { type: 'number', default: 3000 }, // In milliseconds, each delta the position and rotation will be recorded
recordAudio: { type: 'boolean', default: true },
taskAudio: { type: 'boolean', default: false },
taskAudioUrl: { type: 'string', default: null },
taskVideo: { type: 'boolean', default: false },
taskVideoId: { type: 'string', default: null },
taskVideoWidth: { type: 'number', default: 3 },
taskVideoHeight: { type: 'number', default: 1.75 },
lookat: {type: 'string', default: "[camera]" }
},
/**
* Set if component needs multiple instancing.
*/
multiple: true,
/**
* Data recorded during the experiment
*/
recordedData: {},
/**
* If the experiment is still running
*/
runningExperiment: false,
/**
* Initial creation and setting of the mesh.
*/
init: function () { },
/**
* Called when component is attached and when component data changes.
* Generally modifies the entity based on the data.
*/
update: function (oldData) {
const self = this
if (this.data != this.oldData) {
// Get camera
let isThereBabiaCamera = this.el.querySelectorAll("[babia-camera]")
if (isThereBabiaCamera.length > 0) {
this.babiaCameraEl = isThereBabiaCamera[0]
// Add raycaster things and others attributes
let babiaCameraAttribute = this.babiaCameraEl.getAttribute("babia-camera")
this.babiaCameraEl.setAttribute("babia-camera", {
raycasterMouse: babiaCameraAttribute['raycasterMouse'] + ', #babiaTaskPopup, #babiaTaskPopup--close-icon, #babiaStartButton, #babiaFinishButton, #babiaTaskAudio, #babiaTaskVideo',
raycasterHand: babiaCameraAttribute['raycasterHand'] + ', #babiaTaskPopup, #babiaTaskPopup--close-icon, #babiaStartButton, #babiaFinishButton, #babiaTaskAudio, #babiaTaskVideo'
})
} else {
this.babiaCameraEl = this.el.sceneEl.camera.el;
}
// Find babia visualizations
this.findBabiaCharts()
// Hide babia visualizations
this.hideCharts()
// Add task if not babia task
if (this.el.querySelectorAll('[babia-task]').length == 0) {
if (this.data.taskAudio) {
this.addAudioTask()
} else if (this.data.taskVideo) {
this.addVideoTask()
} else {
this.addTask()
}
}
// Add start button
this.addStartButton()
// Add finish button
if (this.data.finishButton) {
this.addFinishButton()
}
}
},
findBabiaCharts: function () {
this.babiaCharts = this.el.querySelectorAll("[babia-pie], [babia-bars], [babia-barsmap], [babia-bubbles], [babia-city], [babia-cyls], [babia-cylsmap], [babia-doughnut], [babia-terrain], [babia-boats], [babia-network], [babiaxr-codecity]")
},
hideCharts: function () {
// Hide charts
for (let child of this.babiaCharts) {
child.setAttribute('visible', false)
}
},
showCharts: function () {
// Hide charts
for (let child of this.babiaCharts) {
child.setAttribute('visible', true)
};
},
addTask: function () {
const self = this
this.taskEntity = document.createElement('a-entity');
this.taskEntity.setAttribute('class', 'babiaxrayscasterclass')
this.taskEntity.setAttribute('id', 'babiaTaskPopup')
this.taskEntity.setAttribute('babia-poster', {
openIconImage: '../assets/popups/info.jpg',
closeIconImage: '../assets/popups/close.jpg',
title: self.data.taskTitle,
titleWrapCount: 30,
titleColor: 'black',
bodyColor: 'black',
posterBoxColor: 'white',
bodyFont: 'roboto',
posterBoxHeight: 4.5,
body: self.data.taskDescription
})
this.taskEntity.setAttribute('scale', { x: 0.5, y: 0.5, z: 0.5 })
// Add position
this.babiaCameraPosition = this.babiaCameraEl.getAttribute('position')
this.taskEntity.setAttribute('position', { x: this.babiaCameraPosition.x, y: this.babiaCameraPosition.y + 1.5, z: this.babiaCameraPosition.z - 4 })
this.taskEntity.setAttribute('babia-lookat', this.data.lookat)
// Add to the scene
this.el.parentElement.append(this.taskEntity)
},
/**
* Things related to the audio task
*/
audioPlaying: false,
addAudioTask: function () {
const self = this
this.taskAudioEntity = document.createElement('a-entity');
this.taskAudioEntity.setAttribute('class', 'babiaxrayscasterclass')
this.taskAudioEntity.setAttribute('geometry', {
primitive: 'plane',
width: 1.1,
height: 0.5
})
this.taskAudioEntity.setAttribute('text', {
value: 'Play audio!',
color: 'white',
align: 'center',
wrapCount: 30,
width: 3.6,
})
this.taskAudioEntity.setAttribute('id', 'babiaTaskAudio')
this.taskAudioEntity.setAttribute('sound', {
src: `url(${self.data.taskAudioUrl})`
})
// Play Sound when click
this.taskAudioEntity.addEventListener('click', function (event) {
if (self.audioPlaying) {
self.audioPlaying = false
self.taskAudioEntity.components.sound.stopSound()
self.taskAudioEntity.setAttribute('text', 'value', 'Play audio!')
} else {
self.audioPlaying = true
self.taskAudioEntity.components.sound.playSound()
self.taskAudioEntity.setAttribute('text', 'value', 'Stop audio!')
}
}, false)
// When audio finished
this.taskAudioEntity.addEventListener('sound-ended', function (event) {
self.audioPlaying = false
self.taskAudioEntity.setAttribute('text', 'value', 'Play audio!')
}, false)
// Add position
this.babiaCameraPosition = this.babiaCameraEl.getAttribute('position')
this.taskAudioEntity.setAttribute('position', { x: this.babiaCameraPosition.x, y: this.babiaCameraPosition.y + 1.5, z: this.babiaCameraPosition.z - 4 })
this.taskAudioEntity.setAttribute('babia-lookat', this.data.lookat)
// Add to the scene
this.el.parentElement.append(this.taskAudioEntity)
},
/**
* Things related to the audio task
*/
videoPlaying: false,
addVideoTask: function () {
const self = this
this.taskVideoEntity = document.createElement('a-video');
this.taskVideoEntity.setAttribute('class', 'babiaxrayscasterclass')
this.taskVideoEntity.setAttribute('id', 'babiaTaskVideo')
this.taskVideoEntity.setAttribute('src', self.data.taskVideoId)
this.taskVideoStream = document.querySelector(self.data.taskVideoId)
this.taskVideoEntity.setAttribute('width', self.data.taskVideoWidth)
this.taskVideoEntity.setAttribute('height', self.data.taskVideoHeight)
// Play Sound when click
this.taskVideoEntity.addEventListener('click', function (event) {
if (self.videoPlaying) {
self.videoPlaying = false
self.taskVideoStream.pause();
} else {
self.videoPlaying = true
self.taskVideoStream.play();
}
}, false)
// Add position
this.babiaCameraPosition = this.babiaCameraEl.getAttribute('position')
this.taskVideoEntity.setAttribute('position', { x: this.babiaCameraPosition.x, y: this.babiaCameraPosition.y + 1.5, z: this.babiaCameraPosition.z - 4 })
this.taskVideoEntity.setAttribute('babia-lookat', this.data.lookat)
// Add to the scene
this.el.parentElement.append(this.taskVideoEntity)
},
addStartButton: function () {
const self = this
this.startButtonEntity = document.createElement('a-plane')
this.startButtonEntity.setAttribute('id', 'babiaStartButton')
this.startButtonEntity.setAttribute('scale', { x: 0.4, y: 0.4, z: 0.4 })
this.startButtonEntity.setAttribute('babia-lookat', this.data.lookat)
this.startButtonEntity.setAttribute('class', '.babiaxrayscasterclass')
this.startButtonEntity.setAttribute('color', '#3ac961')
this.startButtonEntity.setAttribute('geometry', {
primitive: 'plane',
width: 1.8,
height: 0.5
})
this.startButtonEntity.setAttribute('text', {
value: 'Start!',
color: 'white',
align: 'center',
wrapCount: 30,
width: 3.6
})
this.babiaCameraPosition = this.babiaCameraEl.getAttribute('position')
this.startButtonEntity.setAttribute('position', { x: this.babiaCameraPosition.x - 0.5, y: this.babiaCameraPosition.y + 1, z: this.babiaCameraPosition.z - 1.5 })
// Start recording time
this.startButtonEntity.addEventListener('click', async function (event) {
self.showCharts()
self.runningExperiment = true;
// Start Recording
self.recordingData = window.setInterval(function () {
recordData(self)
}, self.data.recordDelta);
self.recordedData['startTime'] = Date.now();
// Audio
if (self.data.recordAudio) {
self.audioRecorder = await recordAudio();
self.audioRecorder.start();
}
self.startButtonEntity.setAttribute('visible', false)
// Show finish button
if (self.data.finishButton) {
self.finishButtonEntity.setAttribute('visible', true)
}
// Time Limit defined
if (self.data.timeLimitEnding) {
self.recordedData['maxTime'] = self.recordedData['startTime'] + self.data.timeLimitTime * 1000
}
}, false);
this.el.parentElement.append(this.startButtonEntity);
},
addFinishButton: function () {
const self = this
this.finishButtonEntity = document.createElement('a-plane')
this.finishButtonEntity.setAttribute('id', 'babiaFinishButton')
this.finishButtonEntity.setAttribute('scale', { x: 0.4, y: 0.4, z: 0.4 })
this.finishButtonEntity.setAttribute('babia-lookat', this.data.lookat)
this.finishButtonEntity.setAttribute('class', 'babiaxrayscasterclass')
this.finishButtonEntity.setAttribute('color', '#9e0000')
this.finishButtonEntity.setAttribute('visible', false)
this.finishButtonEntity.setAttribute('geometry', {
primitive: 'plane',
width: 1.8,
height: 0.5
})
this.finishButtonEntity.setAttribute('text', {
value: 'Finish!',
color: 'white',
align: 'center',
wrapCount: 30,
width: 3.6
})
this.finishButtonEntity.setAttribute('position', { x: this.babiaCameraPosition.x + 0.5, y: this.babiaCameraPosition.y + 1, z: this.babiaCameraPosition.z - 1.5 })
// Start recording time
this.finishButtonEntity.addEventListener('click', function (event) {
self.hideCharts()
// Stop recording
self.runningExperiment = false;
recordData(self)
clearInterval(this.recordingData)
if (self.audioRecorder) {
self.stopAndDownloadAudio()
}
self.recordedData['finishTime'] = Date.now();
self.recordedData['totalDuration'] = self.recordedData['finishTime'] - self.recordedData['startTime'];
downloadObjectAsJson(self.recordedData, "experimentdetails")
self.finishButtonEntity.setAttribute('visible', false)
}, false);
this.el.parentElement.append(this.finishButtonEntity);
},
stopAndDownloadAudio: async function () {
let audio = await this.audioRecorder.stop()
const a = document.createElement("a");
document.body.appendChild(a);
a.style = "display: none";
a.href = audio.audioUrl;
a.download = "audio.webm";
a.click();
window.URL.revokeObjectURL(audio.audioUrl)
},
tick: function () {
const self = this
// Check if the experiment is running
if (this.runningExperiment) {
// Check if the time limit
if (this.data.timeLimitEnding) {
if (Date.now() > this.recordedData.maxTime) {
// Reached the time
this.recordedData['finishTime'] = this.recordedData.maxTime
this.recordedData['totalDuration'] = this.recordedData['finishTime'] - this.recordedData['startTime'];
this.runningExperiment = false
// If forced, hide charts and download data
if (this.data.forceFinishWhenTimeLimit) {
alert("The time limit is done! Thank you!")
this.hideCharts()
downloadObjectAsJson(self.recordedData, "experimentdetails")
self.finishButtonEntity.setAttribute('visible', false)
recordData(self)
clearInterval(this.recordingData)
if (self.audioRecorder) {
self.stopAndDownloadAudio()
}
} else {
alert("The time limit is done, but you can continue doing the experiment, please, click on Finish when done!")
}
}
}
}
}
})
function recordData(self) {
// Save Position and rotation of the camera
let cameraEl = self.el.sceneEl.camera.el;
self.recordedData[Date.now()] = {
position: cameraEl.getAttribute('position'),
rotation: cameraEl.getAttribute('rotation')
}
}
function getAttributeByRegex(element, regex, exclude) {
const attributes = element.attributes;
for (let i = attributes.length - 1; i >= 0; i--) {
const attr = attributes[i];
if (attr.name.startsWith(regex) && attr.name !== exclude && attr.name !== "babia-camera") {
return true
}
}
return false
}
function downloadObjectAsJson(exportObj, exportName) {
let dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(exportObj));
let downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", exportName + ".json");
document.body.appendChild(downloadAnchorNode); // required for firefox
downloadAnchorNode.click();
downloadAnchorNode.remove();
}
const recordAudio = () => {
return new Promise(resolve => {
navigator.mediaDevices.getUserMedia({ audio: true })
.then(stream => {
const mediaRecorder = new MediaRecorder(stream);
const audioChunks = [];
mediaRecorder.addEventListener("dataavailable", event => {
audioChunks.push(event.data);
});
const start = () => {
mediaRecorder.start();
};
const stop = () => {
return new Promise(resolve => {
mediaRecorder.addEventListener("stop", () => {
const audioBlob = new Blob(audioChunks);
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
const play = () => {
audio.play();
};
resolve({ audioBlob, audioUrl, play });
});
mediaRecorder.stop();
});
};
resolve({ start, stop });
});
});
};