You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

347 lines
11 KiB

import * as THREE from 'three';
/**
* Abstract class for content
*/
export class Content {
constructor(data) {
this.data = data;
this.displayMesh = null;
this.texture = null;
}
/**
* @returns {THREE.Mesh}
*/
createDisplayMesh() {
throw new Error("Method 'createDisplayMesh()' must be implemented");
}
update() {
}
dispose() {
if (this.displayMesh) {
this.displayMesh.geometry.dispose();
this.displayMesh.material.dispose();
}
if (this.texture) this.texture.dispose();
}
}
export class TextContent extends Content {
constructor(data) {
super(data);
this.canvas = null;
this.context = null;
this.lines = [];
this.lineHeight = 80; // Font size + padding
this.maxLinesVisible = 0;
this.scrollOffset = 0; // Index of the first line to display
this.color = 'black';
}
createDisplayMesh() {
this.canvas = document.createElement('canvas');
const canvasSize = 1024;
this.canvas.width = 2048;
this.canvas.height = canvasSize;
this.context = this.canvas.getContext('2d');
document.fonts.load('60px Redacted70').then(() => {
this._wrapText();
});
this.texture = new THREE.CanvasTexture(this.canvas);
const material = new THREE.MeshBasicMaterial({ map: this.texture, transparent: true });
const geometry = new THREE.PlaneGeometry(10, 5);
this.displayMesh = new THREE.Mesh(geometry, material);
document.fonts.load('100px Redacted70').then(() => {
this._redrawCanvas();
});
return this.displayMesh;
}
_wrapText() {
const context = this.context;
const canvasWidth = this.canvas.width;
const margin = 20;
const maxWidth = canvasWidth - (margin * 2);
context.font = '100px Redacted70';
const words = this.data.split(' ');
let line = '';
this.lines = [];
for (let n = 0; n < words.length; n++) {
const testLine = line + words[n] + ' ';
const metrics = context.measureText(testLine);
const testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
this.lines.push(line);
line = words[n] + ' ';
} else {
line = testLine;
}
}
this.lines.push(line);
this.maxLinesVisible = Math.floor(this.canvas.height / this.lineHeight) - 1; // Leave space for scroll indicators
}
_redrawCanvas() {
const context = this.context;
const canvasWidth = this.canvas.width;
const canvasHeight = this.canvas.height;
const margin = 20;
context.clearRect(0, 0, canvasWidth, canvasHeight);
// Background
const gradient = context.createLinearGradient(0, 0, 0, canvasHeight);
gradient.addColorStop(0, 'rgba(255.0,255.0,255.0,0.5)');
gradient.addColorStop(1, 'rgba(255.0,255.0,255.0,0.3)');
context.fillStyle = gradient;
context.fillRect(0, 0, canvasWidth, canvasHeight);
// Text
context.fillStyle = this.color;
context.font = '100px Redacted70';
for (let i = 0; i < this.maxLinesVisible; i++) {
const lineIndex = this.scrollOffset + i;
if (lineIndex < this.lines.length) {
const y = (i + 1) * this.lineHeight;
context.fillText(this.lines[lineIndex], margin, y);
}
}
// Scroll indicators
if (this.scrollOffset > 0) {
context.fillText('#', canvasWidth - margin - 40, this.lineHeight);
}
if (this.scrollOffset + this.maxLinesVisible < this.lines.length) {
context.fillText('@', canvasWidth - margin - 40, canvasHeight - margin);
}
this.texture.needsUpdate = true;
}
scrollUp() {
if (this.scrollOffset > 0) {
this.scrollOffset--;
this._redrawCanvas();
}
}
scrollDown() {
if (this.scrollOffset + this.maxLinesVisible < this.lines.length) {
this.scrollOffset++;
this._redrawCanvas();
}
}
dispose() {
super.dispose();
this.canvas = null;
this.context = null;
}
}
export class ImageContent extends Content {
constructor(data) {
super(data); // data should be the image URL or base64 string
this.image = null;
}
createDisplayMesh() {
return new Promise((resolve, reject) => {
const loader = new THREE.TextureLoader();
loader.load(
this.data,
(texture) => {
this.texture = texture;
const aspect = texture.image.width / texture.image.height;
const height = 5;
const width = height * aspect;
const geometry = new THREE.PlaneGeometry(width, height);
const material = new THREE.MeshBasicMaterial({ map: texture, transparent: true });
this.displayMesh = new THREE.Mesh(geometry, material);
resolve(this.displayMesh);
},
undefined,
(err) => {
reject(err);
}
);
});
}
dispose() {
super.dispose();
this.image = null;
}
}
export class VideoContent extends Content {
constructor(data) {
super(data);
this.video = null;
}
createDisplayMesh() {
this.video = document.createElement('video');
this.video.src = this.data;
this.video.crossOrigin = 'anonymous';
this.video.loop = true;
this.video.muted = false;
this.video.playsInline = true;
this.video.autoplay = true;
// Return a promise so you can await mesh creation
return new Promise((resolve, reject) => {
this.video.addEventListener('loadedmetadata', () => {
const aspect = this.video.videoWidth / this.video.videoHeight;
const height = 5; // or any desired height
const width = height * aspect;
this.texture = new THREE.VideoTexture(this.video);
this.texture.minFilter = THREE.LinearFilter;
this.texture.magFilter = THREE.LinearFilter;
this.texture.format = THREE.RGBAFormat;
const geometry = new THREE.PlaneGeometry(width, height);
const material = new THREE.MeshBasicMaterial({ map: this.texture, side: THREE.DoubleSide });
this.displayMesh = new THREE.Mesh(geometry, material);
this.video.play();
resolve(this.displayMesh);
});
this.video.addEventListener('error', (e) => {
reject(e);
});
});
}
dispose() {
super.dispose();
if (this.video) {
this.video.pause();
this.video.src = '';
this.video.load();
this.video = null;
}
}
}
export class AudioContent extends Content {
constructor(data, audioListener) {
super(data); // data should be the audio URL or base64 string
this.audioListener = audioListener; // Pass in a THREE.AudioListener instance
this.audio = null;
this.isLoaded = false;
this.volume = 0.1;
this.isFadingOut = false;
this.fadeInfo = null;
this.itemContentDisplay = null;
this.object = null;
}
createDisplayMesh() {
return new Promise((resolve, reject) => {
const geometry = new THREE.SphereGeometry(1, 4, 4);
const color = new THREE.Color(0, 0, 0, 1);
const material = new THREE.MeshBasicMaterial({ color: color, wireframe: true });
this.displayMesh = new THREE.Mesh(geometry, material);
this.audio = new THREE.PositionalAudio(this.audioListener);
const loader = new THREE.AudioLoader();
loader.load(
this.data,
(buffer) => {
this.audio.setBuffer(buffer);
this.audio.setLoop(true);
this.audio.setVolume(this.volume);
this.isLoaded = true;
this.displayMesh.add(this.audio);
this.audio.play();
resolve(this.displayMesh); // Only resolve after audio is loaded
},
undefined,
(err) => {
console.error('Audio load error:', err);
reject(err);
}
);
});
}
play() {
if (this.isLoaded && this.audio && !this.audio.isPlaying) {
this.audio.play();
}
}
/**
* Starts a gradual fade-out of the audio. The update method will handle disposal.
* @param {number} duration - The duration of the fade in milliseconds.
*/
fadeOutAndDispose(duration = 2000, obj, itemContentDisplay) { // Default to 2 seconds
this.object = obj;
this.itemContentDisplay = itemContentDisplay;
if (this.audio && this.audio.isPlaying && !this.isFadingOut) {
this.isFadingOut = true;
this.fadeInfo = {
startTime: performance.now(),
duration: duration,
initialVolume: this.audio.getVolume()
};
} else if (!this.audio || !this.audio.isPlaying) {
// If not playing, dispose immediately
this.dispose();
}
}
update() {
if (!this.isFadingOut || !this.audio) {
return;
}
const elapsedTime = performance.now() - this.fadeInfo.startTime;
if (elapsedTime >= this.fadeInfo.duration) {
// Fade complete
this.audio.setVolume(0);
this.isFadingOut = false;
this.dispose();
} else {
// In progress, calculate new volume
const fadeProgress = elapsedTime / this.fadeInfo.duration;
const newVolume = this.fadeInfo.initialVolume * (1 - fadeProgress);
this.audio.setVolume(newVolume);
}
}
dispose() {
super.dispose();
if (this.audio) {
this.object.remove(this.itemContentDisplay);
this.itemContentDisplay = null;
this.audio.stop();
this.audio.disconnect();
this.object = null;
this.audio = null;
this.isLoaded = false;
}
}
}