wip control indicators
This commit is contained in:
@@ -23,6 +23,13 @@ const UI_CSS = `
|
|||||||
.marker { position: absolute; top: 8px; height: 8px; transform: translateX(-50%); pointer-events: none; z-index: 1; }
|
.marker { position: absolute; top: 8px; height: 8px; transform: translateX(-50%); pointer-events: none; z-index: 1; }
|
||||||
.marker-event { width: 2px; background: rgba(255,255,255,0.5); }
|
.marker-event { width: 2px; background: rgba(255,255,255,0.5); }
|
||||||
.marker-label { width: 4px; background: gold; z-index: 2; }
|
.marker-label { width: 4px; background: gold; z-index: 2; }
|
||||||
|
|
||||||
|
/* --- Hover Visibility Logic --- */
|
||||||
|
.spacetime-hover-reveal { opacity: 0; pointer-events: none; transition: opacity 0.2s ease; }
|
||||||
|
.spacetime-container:hover .spacetime-hover-reveal { opacity: 1; pointer-events: auto; }
|
||||||
|
|
||||||
|
.spacetime-indicator { position: absolute; left: 33px; bottom: 29px; display: flex; gap: 12px; color: rgba(255,255,255,0.4); font-family: monospace; font-size: 14px; pointer-events: none; opacity: 1; transition: opacity 0.2s ease; }
|
||||||
|
.spacetime-container:hover .spacetime-indicator { opacity: 0; }
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export class SpacetimeController {
|
export class SpacetimeController {
|
||||||
@@ -44,13 +51,13 @@ export class SpacetimeController {
|
|||||||
...options
|
...options
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.hasSpatial = this.opts.orbit || this.opts.pan || this.opts.zoom;
|
||||||
|
|
||||||
// Camera State
|
// Camera State
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
this.hasDragged = false;
|
this.hasDragged = false;
|
||||||
this.dragButton = 0;
|
this.dragButton = 0;
|
||||||
this.lastMouse = { x: 0, y: 0 };
|
this.lastMouse = {x: 0, y: 0};
|
||||||
this.touchMode = '';
|
|
||||||
this.lastPinchDist = 0;
|
|
||||||
|
|
||||||
// Timeline State
|
// Timeline State
|
||||||
this.playMode = this.opts.autoplay ? 'realtime' : 'paused';
|
this.playMode = this.opts.autoplay ? 'realtime' : 'paused';
|
||||||
@@ -59,10 +66,10 @@ export class SpacetimeController {
|
|||||||
this.lastFrameTime = performance.now();
|
this.lastFrameTime = performance.now();
|
||||||
this.playbackAccumulator = 0;
|
this.playbackAccumulator = 0;
|
||||||
|
|
||||||
// Ensure container is absolute-positioning friendly
|
|
||||||
if (getComputedStyle(this.container).position === 'static') {
|
if (getComputedStyle(this.container).position === 'static') {
|
||||||
this.container.style.position = 'relative';
|
this.container.style.position = 'relative';
|
||||||
}
|
}
|
||||||
|
this.container.classList.add('spacetime-container');
|
||||||
|
|
||||||
if (!document.getElementById('spacetime-ui-styles')) {
|
if (!document.getElementById('spacetime-ui-styles')) {
|
||||||
const style = document.createElement('style');
|
const style = document.createElement('style');
|
||||||
@@ -85,25 +92,39 @@ export class SpacetimeController {
|
|||||||
this.overlay = document.createElement('div');
|
this.overlay = document.createElement('div');
|
||||||
this.overlay.className = 'spacetime-overlay';
|
this.overlay.className = 'spacetime-overlay';
|
||||||
|
|
||||||
// --- Top: Reset Button ---
|
// --- Feature Indicator (Bottom Left) ---
|
||||||
const topWrapper = document.createElement('div');
|
const activeFeatures = [];
|
||||||
topWrapper.className = 'reset-wrapper';
|
if (this.opts.timeline) activeFeatures.push('t');
|
||||||
this.resetBtn = document.createElement('button');
|
if (this.opts.pan) activeFeatures.push('p');
|
||||||
this.resetBtn.className = 'reset-btn';
|
if (this.opts.zoom) activeFeatures.push('z');
|
||||||
this.resetBtn.textContent = 'Reset View';
|
if (this.opts.orbit) activeFeatures.push('o');
|
||||||
|
|
||||||
this.resetBtn.addEventListener('click', () => {
|
this.indicator = document.createElement('div');
|
||||||
this.diorama.loadState();
|
this.indicator.className = 'spacetime-indicator';
|
||||||
this.resetBtn.style.display = 'none';
|
this.indicator.innerHTML = activeFeatures.map(f => `<span>${f}</span>`).join('');
|
||||||
});
|
this.overlay.appendChild(this.indicator);
|
||||||
|
|
||||||
topWrapper.appendChild(this.resetBtn);
|
// --- Top: Reset Button (Only if spatial tools are enabled) ---
|
||||||
this.overlay.appendChild(topWrapper);
|
if (this.hasSpatial) {
|
||||||
|
const topWrapper = document.createElement('div');
|
||||||
|
topWrapper.className = 'reset-wrapper';
|
||||||
|
this.resetBtn = document.createElement('button');
|
||||||
|
this.resetBtn.className = 'reset-btn spacetime-hover-reveal';
|
||||||
|
this.resetBtn.textContent = 'Reset View';
|
||||||
|
|
||||||
|
this.resetBtn.addEventListener('click', () => {
|
||||||
|
this.diorama.loadState();
|
||||||
|
this.resetBtn.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
topWrapper.appendChild(this.resetBtn);
|
||||||
|
this.overlay.appendChild(topWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
// --- Bottom: Timeline Controls ---
|
// --- Bottom: Timeline Controls ---
|
||||||
if (this.opts.timeline) {
|
if (this.opts.timeline) {
|
||||||
this.uiContainer = document.createElement('div');
|
this.uiContainer = document.createElement('div');
|
||||||
this.uiContainer.className = 'visualizer-ui';
|
this.uiContainer.className = 'visualizer-ui spacetime-hover-reveal';
|
||||||
|
|
||||||
if (this.opts.subticks) {
|
if (this.opts.subticks) {
|
||||||
this.subRow = document.createElement('div');
|
this.subRow = document.createElement('div');
|
||||||
@@ -148,7 +169,6 @@ export class SpacetimeController {
|
|||||||
this.syncUI();
|
this.syncUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drop the overlay on top of the canvas
|
|
||||||
this.container.appendChild(this.overlay);
|
this.container.appendChild(this.overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,18 +212,22 @@ export class SpacetimeController {
|
|||||||
attachEvents() {
|
attachEvents() {
|
||||||
this.canvas.addEventListener('contextmenu', e => e.preventDefault());
|
this.canvas.addEventListener('contextmenu', e => e.preventDefault());
|
||||||
|
|
||||||
|
// Correctly set or remove the grab cursor
|
||||||
|
this.canvas.style.cursor = this.hasSpatial ? 'grab' : 'default';
|
||||||
|
|
||||||
const notifyCameraChange = () => {
|
const notifyCameraChange = () => {
|
||||||
this.hasDragged = true;
|
this.hasDragged = true;
|
||||||
if (this.diorama.checkpoint) this.resetBtn.style.display = 'block';
|
if (this.resetBtn && this.diorama.checkpoint) this.resetBtn.style.display = 'block';
|
||||||
this.diorama.requestRender();
|
this.diorama.requestRender();
|
||||||
};
|
};
|
||||||
|
|
||||||
// All of these drag events are strictly bound to the canvas element.
|
|
||||||
// Clicking the UI container will NEVER trigger these because they are siblings.
|
|
||||||
this.canvas.addEventListener('mousedown', (e) => {
|
this.canvas.addEventListener('mousedown', (e) => {
|
||||||
|
// Short-circuit drag logic entirely if no spatial controls are enabled
|
||||||
|
if (!this.hasSpatial) return;
|
||||||
|
|
||||||
this.isDragging = true;
|
this.isDragging = true;
|
||||||
this.hasDragged = false;
|
this.hasDragged = false;
|
||||||
this.lastMouse = { x: e.clientX, y: e.clientY };
|
this.lastMouse = {x: e.clientX, y: e.clientY};
|
||||||
|
|
||||||
this.dragButton = e.button;
|
this.dragButton = e.button;
|
||||||
if (this.dragButton === 0) {
|
if (this.dragButton === 0) {
|
||||||
@@ -219,7 +243,7 @@ export class SpacetimeController {
|
|||||||
window.addEventListener('mouseup', () => {
|
window.addEventListener('mouseup', () => {
|
||||||
if (this.isDragging) {
|
if (this.isDragging) {
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
this.canvas.style.cursor = 'grab';
|
if (this.hasSpatial) this.canvas.style.cursor = 'grab';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -229,7 +253,7 @@ export class SpacetimeController {
|
|||||||
const dy = e.clientY - this.lastMouse.y;
|
const dy = e.clientY - this.lastMouse.y;
|
||||||
|
|
||||||
if (Math.hypot(dx, dy) > 3) {
|
if (Math.hypot(dx, dy) > 3) {
|
||||||
this.lastMouse = { x: e.clientX, y: e.clientY };
|
this.lastMouse = {x: e.clientX, y: e.clientY};
|
||||||
if (this.dragButton === 0) this.orbit(dx, dy);
|
if (this.dragButton === 0) this.orbit(dx, dy);
|
||||||
else if (this.dragButton === 1) this.pan(dx, dy);
|
else if (this.dragButton === 1) this.pan(dx, dy);
|
||||||
else if (this.dragButton === 2) this.zoomLinear(dy);
|
else if (this.dragButton === 2) this.zoomLinear(dy);
|
||||||
@@ -238,8 +262,8 @@ export class SpacetimeController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.canvas.addEventListener('click', () => {
|
this.canvas.addEventListener('click', () => {
|
||||||
// Because UI clicks hit the overlay and container (bypassing canvas entirely),
|
// hasDragged will always be false if hasSpatial is false,
|
||||||
// this will exclusively trigger if the user cleanly taps the 3D scene.
|
// ensuring static scenes still act as tap-to-play toggles.
|
||||||
if (!this.hasDragged && this.opts.timeline) {
|
if (!this.hasDragged && this.opts.timeline) {
|
||||||
this.playMode = this.playMode === 'paused' ? 'realtime' : 'paused';
|
this.playMode = this.playMode === 'paused' ? 'realtime' : 'paused';
|
||||||
this.syncUI();
|
this.syncUI();
|
||||||
@@ -247,7 +271,6 @@ export class SpacetimeController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- TEMPORAL CONTROL ---
|
// --- TEMPORAL CONTROL ---
|
||||||
|
|
||||||
getMaxTime() {
|
getMaxTime() {
|
||||||
@@ -342,7 +365,10 @@ export class SpacetimeController {
|
|||||||
this.playMode = 'paused';
|
this.playMode = 'paused';
|
||||||
let nextTime = this.getMaxTime() + 1;
|
let nextTime = this.getMaxTime() + 1;
|
||||||
for (const ev of this.frame.world.events) {
|
for (const ev of this.frame.world.events) {
|
||||||
if (ev.time > this.frame.currentTime) { nextTime = ev.time; break; }
|
if (ev.time > this.frame.currentTime) {
|
||||||
|
nextTime = ev.time;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (nextTime > this.getMaxTime()) nextTime = this.isLooping ? 0 : this.getMaxTime();
|
if (nextTime > this.getMaxTime()) nextTime = this.isLooping ? 0 : this.getMaxTime();
|
||||||
this.frame.seek(nextTime);
|
this.frame.seek(nextTime);
|
||||||
|
|||||||
@@ -83,6 +83,8 @@
|
|||||||
subticks: false,
|
subticks: false,
|
||||||
autoplay: false,
|
autoplay: false,
|
||||||
loop: false,
|
loop: false,
|
||||||
|
zoom: false,
|
||||||
|
pan: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const w_pulses = new World(`
|
const w_pulses = new World(`
|
||||||
@@ -116,6 +118,9 @@
|
|||||||
subticks: false,
|
subticks: false,
|
||||||
loop: true,
|
loop: true,
|
||||||
autoplay: true,
|
autoplay: true,
|
||||||
|
pan: false,
|
||||||
|
zoom: false,
|
||||||
|
orbit: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const w_piston = new World(`
|
const w_piston = new World(`
|
||||||
@@ -214,6 +219,7 @@
|
|||||||
// };
|
// };
|
||||||
// setInterval(update_pulses, factor * 8 * 50)
|
// setInterval(update_pulses, factor * 8 * 50)
|
||||||
// update_pulses();
|
// update_pulses();
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Renderer Initialization Failed:", err);
|
console.error("Renderer Initialization Failed:", err);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user