Created
March 22, 2024 15:59
-
-
Save arsors/402ce0b143c2914df7dae5a1c40f8d41 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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