mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +00:00
timeline display
This commit is contained in:
@@ -85,6 +85,7 @@ export class AnimTimelineElement extends LitElement {
|
||||
* setting the cursor style to the grabby hand.
|
||||
*/
|
||||
@state() _panning = false;
|
||||
@state() _scrubbing = false;
|
||||
|
||||
/** How many seconds are represented by each px of horizontal space. */
|
||||
secondsPerPx = new Computed(() => (this.rangeEnd - this.rangeStart) / this._renderWidth);
|
||||
@@ -93,13 +94,13 @@ export class AnimTimelineElement extends LitElement {
|
||||
|
||||
constructor () {
|
||||
super();
|
||||
this.#resizeObserver = new ResizeObserver(() => this.#updateRenderWidth());
|
||||
this.#resizeObserver = new ResizeObserver(() => this.updateRenderWidth());
|
||||
this.#resizeObserver.observe(this);
|
||||
}
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.#updateRenderWidth();
|
||||
this.updateRenderWidth();
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
@@ -125,12 +126,12 @@ export class AnimTimelineElement extends LitElement {
|
||||
event.preventDefault();
|
||||
event.stopImmediatePropagation();
|
||||
|
||||
if (event.ctrlKey) {
|
||||
this.#zoomTimeline(event.clientX, event.deltaY);
|
||||
if (event.ctrlKey || Math.abs(event.deltaY ) > 0) {
|
||||
this.zoomTimeline(event.clientX, event.deltaY);
|
||||
} else if (event.deltaX) {
|
||||
this.#pan(-event.deltaX);
|
||||
this.pan(-event.deltaX);
|
||||
} else if (event.shiftKey && event.deltaY) {
|
||||
this.#pan(-event.deltaY);
|
||||
this.pan(-event.deltaY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,7 +155,7 @@ export class AnimTimelineElement extends LitElement {
|
||||
studio.select(id);
|
||||
}
|
||||
|
||||
#zoomTimeline(origin: number, delta: number): void {
|
||||
zoomTimeline(origin: number, delta: number): void {
|
||||
const currentRange = this.rangeEnd - this.rangeStart;
|
||||
const elementRect = this.getBoundingClientRect();
|
||||
const focusPoint = (origin - elementRect.x) / elementRect.width;
|
||||
@@ -168,19 +169,37 @@ export class AnimTimelineElement extends LitElement {
|
||||
this.dispatchEvent(new CustomEvent("rangeEnd-change", { detail: rangeEnd }));
|
||||
}
|
||||
|
||||
onDragStart(): void {
|
||||
onMidDragStart(): void {
|
||||
this._panning = true;
|
||||
}
|
||||
|
||||
onDragEnd(): void {
|
||||
onMidDragEnd(): void {
|
||||
this._panning = false;
|
||||
}
|
||||
|
||||
onDrag(event: PointerEvent): void {
|
||||
this.#pan(event.movementX);
|
||||
onMidDrag(event: PointerEvent): void {
|
||||
this.pan(event.movementX);
|
||||
}
|
||||
|
||||
#pan(deltaPx: number): void {
|
||||
onDragStart(): void {
|
||||
this._scrubbing = true;
|
||||
}
|
||||
|
||||
onDragEnd(): void {
|
||||
this._scrubbing = false;
|
||||
}
|
||||
|
||||
onDrag(event: PointerEvent): void {
|
||||
let elementBounds = this.getBoundingClientRect();
|
||||
let relativeX = event.clientX - elementBounds.left;
|
||||
let ratioX = Math.max(0, Math.min(relativeX / elementBounds.width, 1));
|
||||
const seconds = (this.rangeEnd - this.rangeStart) * ratioX + this.rangeStart;
|
||||
const updated = Math.round(seconds * this.clipFps) / this.clipFps;
|
||||
|
||||
studio.seekTime(updated);
|
||||
}
|
||||
|
||||
pan(deltaPx: number): void {
|
||||
const deltaChange = deltaPx * 0.001;
|
||||
const scaledChange = (this.rangeEnd - this.rangeStart) * deltaChange;
|
||||
|
||||
@@ -201,7 +220,7 @@ export class AnimTimelineElement extends LitElement {
|
||||
let relativeX = event.clientX - elementBounds.left;
|
||||
let ratioX = Math.max(0, Math.min(relativeX / elementBounds.width, 1));
|
||||
const seconds = (this.rangeEnd - this.rangeStart) * ratioX + this.rangeStart;
|
||||
const updated = seconds;
|
||||
const updated = Math.round(seconds * this.clipFps) / this.clipFps;
|
||||
|
||||
studio.seekTime(updated);
|
||||
}
|
||||
@@ -211,7 +230,7 @@ export class AnimTimelineElement extends LitElement {
|
||||
let relativeX = event.clientX - elementBounds.left;
|
||||
let ratioX = Math.max(0, Math.min(relativeX / elementBounds.width, 1));
|
||||
const seconds = (this.rangeEnd - this.rangeStart) * ratioX + this.rangeStart;
|
||||
const updated = seconds;
|
||||
const updated = Math.round(seconds * this.clipFps) / this.clipFps;
|
||||
|
||||
studio.seekTime(updated);
|
||||
}
|
||||
@@ -225,7 +244,7 @@ export class AnimTimelineElement extends LitElement {
|
||||
studio.moveKeyframeTime(id, time);
|
||||
}
|
||||
|
||||
#updateRenderWidth(): void {
|
||||
updateRenderWidth(): void {
|
||||
this._renderWidth = this.getBoundingClientRect().width;
|
||||
}
|
||||
|
||||
@@ -255,56 +274,87 @@ export class AnimTimelineElement extends LitElement {
|
||||
protected override render() {
|
||||
const range = this.rangeEnd - this.rangeStart;
|
||||
|
||||
let subdivisionScale = 0.5;
|
||||
let majorSubdivisionScale = 1;
|
||||
let fps = this.clipFps;
|
||||
|
||||
let frameRange = Math.ceil(range * this.clipFps);
|
||||
|
||||
let spf = 1 / fps;
|
||||
|
||||
let majorSubdivisionSize = this.clipFps;
|
||||
let minorSubdivisionSize = this.clipFps / 2;
|
||||
let unlablelledSubdivisionSize = this.clipFps / 4;
|
||||
|
||||
if (range < 0.5) {
|
||||
subdivisionScale = 1 / this.clipFps;
|
||||
majorSubdivisionScale = 0.25 * this.clipFps;
|
||||
majorSubdivisionSize = this.clipFps / 2;
|
||||
minorSubdivisionSize = this.clipFps / 12;
|
||||
unlablelledSubdivisionSize = 1;
|
||||
} else if (range < 2) {
|
||||
subdivisionScale = 0.25 * this.clipFps;
|
||||
majorSubdivisionScale = 0.5;
|
||||
} else if (range > 120) {
|
||||
subdivisionScale = 30;
|
||||
majorSubdivisionScale = 60;
|
||||
} else if (range > 60) {
|
||||
subdivisionScale = 10;
|
||||
majorSubdivisionScale = 30;
|
||||
} else if (range > 10) {
|
||||
subdivisionScale = 5;
|
||||
majorSubdivisionScale = 10;
|
||||
} else if (range > 5) {
|
||||
subdivisionScale = 1;
|
||||
majorSubdivisionScale = 5;
|
||||
majorSubdivisionSize = this.clipFps / 2;
|
||||
minorSubdivisionSize = this.clipFps / 4;
|
||||
unlablelledSubdivisionSize = 2;
|
||||
} else if (range < 5) {
|
||||
majorSubdivisionSize = this.clipFps;
|
||||
minorSubdivisionSize = this.clipFps / 2;
|
||||
unlablelledSubdivisionSize = this.clipFps / 4;
|
||||
} else if (range < 10) {
|
||||
majorSubdivisionSize = this.clipFps * 2;
|
||||
minorSubdivisionSize = this.clipFps;
|
||||
unlablelledSubdivisionSize = this.clipFps / 2;
|
||||
} else if (range < 60) {
|
||||
majorSubdivisionSize = this.clipFps * 10;
|
||||
minorSubdivisionSize = this.clipFps * 5;
|
||||
unlablelledSubdivisionSize = this.clipFps;
|
||||
} else if (range < 120) {
|
||||
majorSubdivisionSize = this.clipFps * 20;
|
||||
minorSubdivisionSize = this.clipFps * 10;
|
||||
unlablelledSubdivisionSize = this.clipFps * 2;
|
||||
} else {
|
||||
majorSubdivisionSize = Math.ceil(frameRange / 10);
|
||||
minorSubdivisionSize = Math.ceil(frameRange / 20);
|
||||
unlablelledSubdivisionSize = Math.ceil(frameRange / 40);
|
||||
}
|
||||
|
||||
const scaledStart = this.rangeStart / subdivisionScale;
|
||||
const scaledEnd = this.rangeEnd / subdivisionScale;
|
||||
|
||||
let subdivisions : Array<{seconds: number, fraction: number, isMajor: boolean}> = [];
|
||||
let currentOffset = scaledStart;
|
||||
let currentOffsetSeconds = this.rangeStart;
|
||||
let subdivisions : Array<{seconds: number, fraction: number, isMajor: boolean, isLabelled: boolean}> = [];
|
||||
|
||||
while (currentOffset < scaledEnd) {
|
||||
const currentSeconds = currentOffsetSeconds;
|
||||
const isMajor = currentOffsetSeconds % majorSubdivisionScale === 0;
|
||||
subdivisions.push({ seconds: currentSeconds, fraction: (currentSeconds - this.rangeStart) / range, isMajor });
|
||||
let nextSubdiv = Math.ceil(currentOffset);
|
||||
if (Math.abs(nextSubdiv - currentOffset) < 0.1) {
|
||||
nextSubdiv += 1;
|
||||
let initialFrame = Math.ceil(this.rangeStart * this.clipFps);
|
||||
|
||||
for (let i = 0; i < frameRange; i++) {
|
||||
let frame = i + initialFrame;
|
||||
let seconds = frame * spf;
|
||||
let fraction = (seconds - this.rangeStart) / range;
|
||||
if (frame % majorSubdivisionSize === 0) {
|
||||
subdivisions.push({
|
||||
seconds,
|
||||
fraction,
|
||||
isMajor: true,
|
||||
isLabelled: true
|
||||
});
|
||||
} else if (frame % minorSubdivisionSize === 0) {
|
||||
subdivisions.push({
|
||||
seconds,
|
||||
fraction,
|
||||
isMajor: false,
|
||||
isLabelled: true
|
||||
});
|
||||
} else if (frame % unlablelledSubdivisionSize === 0) {
|
||||
subdivisions.push({seconds, fraction, isMajor: false, isLabelled: false });
|
||||
}
|
||||
currentOffset = nextSubdiv;
|
||||
currentOffsetSeconds = currentOffset * subdivisionScale;
|
||||
}
|
||||
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({
|
||||
wrapper: true,
|
||||
panning: this._panning,
|
||||
panning: this._panning || this._scrubbing,
|
||||
})}
|
||||
${drag({
|
||||
button: 1,
|
||||
start: this.onMidDragStart,
|
||||
move: this.onMidDrag,
|
||||
end: this.onMidDragEnd,
|
||||
})}
|
||||
${drag({
|
||||
button: 0,
|
||||
start: this.onDragStart,
|
||||
move: this.onDrag,
|
||||
end: this.onDragEnd,
|
||||
@@ -313,7 +363,7 @@ export class AnimTimelineElement extends LitElement {
|
||||
@click=${this.onSetPlayhead}
|
||||
>
|
||||
|
||||
${subdivisions.map(({ seconds, fraction, isMajor}, idx) => {
|
||||
${subdivisions.map(({ seconds, fraction, isMajor, isLabelled}, idx) => {
|
||||
const secondsPercentage = fraction * 100;
|
||||
|
||||
return html`
|
||||
@@ -326,7 +376,7 @@ export class AnimTimelineElement extends LitElement {
|
||||
left: `${secondsPercentage}%`
|
||||
})}
|
||||
></div>
|
||||
${true ? html`
|
||||
${isMajor || isLabelled ? html`
|
||||
<span class="timestamp"
|
||||
style=${styleMap({
|
||||
left: `calc(${secondsPercentage}% + 8px)`
|
||||
|
||||
Reference in New Issue
Block a user