Skip to content

Instantly share code, notes, and snippets.

@arsors
Created March 22, 2024 15:59
Show Gist options
  • Save arsors/402ce0b143c2914df7dae5a1c40f8d41 to your computer and use it in GitHub Desktop.
Save arsors/402ce0b143c2914df7dae5a1c40f8d41 to your computer and use it in GitHub Desktop.
/* Simple timeline visualizer for gsap. I do not support this active. */
/* Usage: new TimelineVisualizer(yourGsapTimelineObject) */
export default class TimelineVisualizer {
constructor(animation) {
this.timelineVisual = document.createElement('div');
this.timelineVisual.className = 'timeline-visual';
this.marker = document.createElement('div');
this.marker.className = 'timeline-marker';
this.animation = animation;
this.totalDuration = animation.totalDuration();
this.animationChildren = animation.getChildren(false);
this.appendStyles();
this.drawTimeline();
this.drawElements();
this.drawMarker();
this.updateMarker();
}
appendStyles() {
const style = document.createElement('style')
style.innerHTML = `
.timeline-visual {
position: fixed;
left: 0;
bottom: 0;
width: 100%;
z-index: 9999;
background-color: rgba(0, 0, 0, 0.8);
height: 30vh;
overflow: auto;
touch-action: none;
user-select: none;
cursor: grab;
}
.control-wrapper {
position: sticky;
left: 0;
top: 0;
z-index: 99999;
display: flex;
}
.zoom-button {
width: 30px;
height: 30px;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.8);
color: white;
font-size: 20px;
text-align: center;
}
.resize-button {
width: 30px;
height: 30px;
cursor: pointer;
background-color: rgba(0, 0, 0, 0.8);
color: white;
font-size: 20px;
text-align: center;
}
.timeline-element {
border: 1px solid #000;
background-color: rgba(255, 155, 0, 0.75);
position: relative;
height: 20px;
font-size: 10px;
overflow: hidden;
padding: 2px;
}
.timeline-setter {
background-color: rgba(0, 155, 255, 0.75);
}
.timeline-marker {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background-color: red;
z-index: 9999;
pointer-events: none;
}
`
document.body.appendChild(style)
}
drawTimeline() {
// Dragging
this.timelineVisual.style.setProperty('--timeline-width', '2');
this.timelineVisual.addEventListener('mousedown', e => {
const startX = e.clientX;
const startY = e.clientY;
const startScrollLeft = this.timelineVisual.scrollLeft;
const startScrollTop = this.timelineVisual.scrollTop;
const onMouseMove = e => {
this.timelineVisual.scrollLeft = startScrollLeft + startX - e.clientX;
this.timelineVisual.scrollTop = startScrollTop + startY - e.clientY;
};
const onMouseUp = e => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
// Controls
const controlWrapper = document.createElement('div');
controlWrapper.className = 'control-wrapper';
// Zoom
const zoomOutElement = document.createElement('div');
zoomOutElement.innerText = '-';
zoomOutElement.className = 'zoom-button';
zoomOutElement.addEventListener('click', () => {
this.timelineVisual.style.setProperty('--timeline-width', parseFloat(this.timelineVisual.style.getPropertyValue('--timeline-width')) - 1);
})
controlWrapper.appendChild(zoomOutElement);
const zoomInElement = document.createElement('div');
zoomInElement.innerText = '+';
zoomInElement.className = 'zoom-button';
zoomInElement.addEventListener('click', () => {
this.timelineVisual.style.setProperty('--timeline-width', parseFloat(this.timelineVisual.style.getPropertyValue('--timeline-width')) + 1);
})
controlWrapper.appendChild(zoomInElement);
// Resize
const resizeElement = document.createElement('div');
resizeElement.innerText = '⇕';
resizeElement.className = 'resize-button';
resizeElement.addEventListener('mousedown', e => {
const startY = e.clientY;
const startHeight = this.timelineVisual.clientHeight;
const onMouseMove = e => {
this.timelineVisual.style.height = `${startHeight - e.clientY + startY}px`;
};
const onMouseUp = e => {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
})
controlWrapper.appendChild(resizeElement);
// Append
this.timelineVisual.appendChild(controlWrapper);
document.body.appendChild(this.timelineVisual);
}
drawElements() {
const recursiveTimelineDrawer = (children, start = 0) => {
for (const child of children) {
if (child.getChildren) {
recursiveTimelineDrawer(child.getChildren(false), start + child.startTime());
} else {
const childEl = document.createElement('div');
const childElstyleleft = ((start + child.startTime()) / this.totalDuration * 100) + '%';
const childElstylewidth = (child.duration() / this.totalDuration * 100) + '%';
childEl.className = (child.duration() > 0) ? 'timeline-element' : 'timeline-element timeline-setter';
childEl.style.left = `calc(${childElstyleleft} * var(--timeline-width))`;
childEl.style.width = `calc(${childElstylewidth} * var(--timeline-width))`;
childEl.innerText = child.targets().map(target => target.classList[0]).join(', ');
childEl.title = (child.duration() > 0) ? `Tween. Duration: ${child.duration()}s` : 'Setter';
this.timelineVisual.appendChild(childEl);
childEl.addEventListener('mouseover', () => {
child.targets().forEach(target => {
target.style.border = '1px solid red';
});
});
childEl.addEventListener('mouseout', () => {
child.targets().forEach(target => {
target.style.border = '';
});
});
childEl.addEventListener('click', () => {
console.log(child.vars);
});
}
}
};
recursiveTimelineDrawer(this.animationChildren);
}
drawMarker() {
this.marker.style.height = `calc(100% + ${this.timelineVisual.scrollHeight - this.timelineVisual.clientHeight}px)`;
this.timelineVisual.appendChild(this.marker);
}
updateMarker() {
this.animation.eventCallback('onUpdate', () => {
this.marker.style.left = `calc(${(this.animation.time() / this.animation.totalDuration() * 100)}% * var(--timeline-width))`;
this.timelineVisual.scrollLeft = this.marker.offsetLeft - this.timelineVisual.clientWidth / 2;
const element = this.timelineVisual.querySelectorAll('.timeline-element');
element.forEach(el => {
if (el.offsetLeft < this.marker.offsetLeft && el.offsetLeft + el.offsetWidth > this.marker.offsetLeft) {
this.timelineVisual.scrollTop = el.offsetTop - this.timelineVisual.clientHeight / 2;
}
});
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment