/*! * GSDevTools 3.13.0 * https://gsap.com * * @license Copyright 2008-2025, GreenSock. All rights reserved. * Subject to the terms at https://gsap.com/standard-license * @author: Jack Doyle, jack@greensock.com */ /* eslint-disable */ import { Draggable } from "./Draggable.js"; var gsap, _coreInitted, _doc, _docEl, _win, _recordedRoot, Animation, _rootTween, _rootInstance, _rootIsDirty, _keyboardInstance, _globalTimeline, _independentRoot, _delayedCall, _context, _recording = true, _startupPhase = true, //for the first 2 seconds, we don't record any zero-duration tweens because they're typically just setup stuff and/or the "from" or "startAt" tweens. In version 1.20.3 we started flagging those with data:"isStart"|"isFromStart" but this logic helps GSDevTools work with older versions too. _globalStartTime = 0, _windowExists = function _windowExists() { return typeof window !== "undefined"; }, _getGSAP = function _getGSAP() { return gsap || _windowExists() && (gsap = window.gsap) && gsap.registerPlugin && gsap; }, _isString = function _isString(value) { return typeof value === "string"; }, _isFunction = function _isFunction(value) { return typeof value === "function"; }, _isObject = function _isObject(value) { return typeof value === "object"; }, _isUndefined = function _isUndefined(value) { return typeof value === "undefined"; }, _bonusValidated = 1, //GSDevTools _svgNS = "http://www.w3.org/2000/svg", _domNS = "http://www.w3.org/1999/xhtml", _idSeed = 0, //we assign an ID to each GSDevTools instance so that we can segregate the sessionStorage data accordingly. _lookup = {}, _supportsStorage = function () { try { sessionStorage.setItem("gsTest", "1"); sessionStorage.removeItem("gsTest"); return true; } catch (e) { return false; } }(), _parseAnimation = function _parseAnimation(animationOrId) { return animationOrId instanceof Animation ? animationOrId : animationOrId ? gsap.getById(animationOrId) : null; }, _createElement = function _createElement(type, container, cssText) { var element = _doc.createElementNS ? _doc.createElementNS(type === "svg" ? _svgNS : _domNS, type) : _doc.createElement(type); if (container) { if (_isString(container)) { container = _doc.querySelector(container); } container.appendChild(element); } if (type === "svg") { element.setAttribute("xmlns", _svgNS); element.setAttribute("xmlns:xlink", _domNS); } cssText && (element.style.cssText = cssText); return element; }, _clearSelection = function _clearSelection() { if (_doc.selection) { _doc.selection.empty(); } else if (_win.getSelection) { _win.getSelection().removeAllRanges(); } }, _getChildrenOf = function _getChildrenOf(timeline, includeTimelines) { var a = [], cnt = 0, Tween = gsap.core.Tween, tween = timeline._first; while (tween) { if (tween instanceof Tween) { if (tween.vars.id) { a[cnt++] = tween; } } else { if (includeTimelines && tween.vars.id) { a[cnt++] = tween; } a = a.concat(_getChildrenOf(tween, includeTimelines)); cnt = a.length; } tween = tween._next; } return a; }, _getClippedDuration = function _getClippedDuration(animation, excludeRootRepeats) { var max = 0, repeat = Math.max(0, animation._repeat), t = animation._first; if (!t) { max = animation.duration(); } while (t) { max = Math.max(max, t.totalDuration() > 999 ? t.endTime(false) : t._start + t._tDur / t._ts); t = t._next; } return !excludeRootRepeats && repeat ? max * (repeat + 1) + animation._rDelay * repeat : max; }, _globalizeTime = function _globalizeTime(animation, rawTime) { var a = animation, time = arguments.length > 1 ? +rawTime : a.rawTime(); while (a) { time = a._start + time / (a._ts || 1); a = a.parent; } return time; }, _timeToProgress = function _timeToProgress(time, animation, defaultValue, relativeProgress) { var add, i, a; if (_isString(time)) { if (time.charAt(1) === "=") { add = parseInt(time.charAt(0) + "1", 10) * parseFloat(time.substr(2)); if (add < 0 && relativeProgress === 0) { //if something like inTime:"-=2", we measure it from the END, not the beginning relativeProgress = 100; } time = relativeProgress / 100 * animation.duration() + add; } else if (isNaN(time) && animation.labels && animation.labels[time] !== -1) { time = animation.labels[time]; } else if (animation === _recordedRoot) { //perhaps they defined an id of an animation, like "myAnimation+=2" i = time.indexOf("="); if (i > 0) { add = parseInt(time.charAt(i - 1) + "1", 10) * parseFloat(time.substr(i + 1)); time = time.substr(0, i - 1); } else { add = 0; } a = gsap.getById(time); if (a) { time = _globalizeTime(a, defaultValue / 100 * a.duration()) + add; } } } time = isNaN(time) ? defaultValue : parseFloat(time); return Math.min(100, Math.max(0, time / animation.duration() * 100)); }, _addedCSS, _createRootElement = function _createRootElement(element, minimal, css) { if (!_addedCSS) { _createElement("style", _docEl).innerHTML = '.gs-dev-tools{height:51px;bottom:0;left:0;right:0;display:block;position:fixed;overflow:visible;padding:0;font-size:15px;font-family:-apple-system,BlinkMacSystemFont,avenir next,sans-serif;color:#bbbaa6}.gs-dev-tools *{box-sizing:content-box;visibility:visible}.gs-dev-tools .gs-top{position:relative;z-index:499}.gs-dev-tools .gs-bottom{display:flex;align-items:center;justify-content:space-between;gap:1rem;background-color:#0e100f;height:42px;position:relative}.gs-dev-tools .timeline{position:relative;height:8px;margin-left:15px;margin-right:15px;overflow:visible}.gs-dev-tools .progress-bar,.gs-dev-tools .timeline-track{height:8px;position:absolute;top:0;left:-15px;right:-15px}.gs-dev-tools .timeline-track{background-color:#222}.gs-dev-tools .progress-bar{background:linear-gradient(114.41deg,#0ae448 20.74%,#abff84 65.5%);height:8px;top:0;width:0;pointer-events:none}.gs-dev-tools .seek-bar{width:100%;position:absolute;height:24px;top:-12px;left:0;background-color:transparent}.gs-dev-tools .in-point,.gs-dev-tools .out-point{width:15px;height:26px;position:absolute;top:-18px}.gs-dev-tools .in-point-shape{fill:#0ae448;transform:translateX(1px)}.gs-dev-tools .out-point-shape{fill:#ff8709}.gs-dev-tools .in-point{transform:translateX(-100%)}.gs-dev-tools .out-point{left:100%}.gs-dev-tools .playhead{position:absolute;top:-5px;transform:translate(-50%,0);left:0;border-radius:50%;width:16px;height:16px;background:linear-gradient(114.41deg,#0ae448 20.74%,#abff84 65.5%)}.gs-dev-tools .gs-btn-white{fill:#fffce1}.gs-dev-tools .pause{opacity:0}.gs-dev-tools .select-animation{vertical-align:middle;position:relative;padding:6px 10px}.gs-dev-tools .select-animation-container{flex-grow:4;width:40%}.gs-dev-tools .select-arrow{display:inline-block;width:12px;height:7px;margin:0 7px;transform:translate(0,-2px)}.gs-dev-tools .select-arrow-shape{stroke:currentcolor;stroke-width:2px;fill:none}.gs-dev-tools .rewind{height:14px}.gs-dev-tools .ease-border,.gs-dev-tools .rewind-path{fill:currentColor}.gs-dev-tools .play-pause{width:18px;height:18px}.gs-dev-tools .ease{width:20px;height:20px;min-width:30px;display:none}.gs-dev-tools .ease-path{fill:none;stroke:#abff84;stroke-width:2px}.gs-dev-tools .time-scale{text-align:center;min-width:30px}.gs-dev-tools .loop{width:15px}.gs-dev-tools label span{text-decoration:none}.gs-dev-tools button:focus,.gs-dev-tools select:focus{outline:0}.gs-dev-tools label{position:relative;cursor:pointer}.gs-dev-tools label.locked{text-decoration:none;cursor:auto}.gs-dev-tools label input,.gs-dev-tools label select{position:absolute;left:0;top:0;z-index:1;font:inherit;font-size:inherit;line-height:inherit;height:100%;width:100%;color:#000!important;opacity:0;background:0 0;border:none;padding:0;margin:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer}.gs-dev-tools label input+.display{position:relative;z-index:2}.gs-dev-tools .gs-bottom-right{vertical-align:middle;display:flex;align-items:center;flex-grow:4;width:40%;justify-content:flex-end}.gs-dev-tools .time-container{margin:0 5px}.gs-dev-tools .logo{width:32px;height:32px;position:relative;top:2px;margin:0 12px}.gs-dev-tools .gs-hit-area{background-color:transparent;width:100%;height:100%;top:0;position:absolute}.gs-dev-tools.minimal{height:auto;display:flex;align-items:stretch}.gs-dev-tools.minimal .gs-top{order:2;flex-grow:4;background-color:#000}.gs-dev-tools.minimal .gs-bottom{background-color:#0e100f;border-top:none}.gs-dev-tools.minimal .timeline{top:50%;transform:translate(0,-50%)}.gs-dev-tools.minimal .gs-bottom-right,.gs-dev-tools.minimal .in-point,.gs-dev-tools.minimal .out-point,.gs-dev-tools.minimal .rewind,.gs-dev-tools.minimal .select-animation-container{display:none}.gs-dev-tools.minimal .play-pause{width:20px;height:20px;padding:4px 6px;margin-left:14px}.gs-dev-tools.minimal .time-scale{min-width:26px}.gs-dev-tools.minimal .loop{width:18px;min-width:18px;display:none}@media only screen and (max-width:600px){.gs-dev-tools{height:auto;display:flex;align-items:stretch}.gs-dev-tools .gs-top{order:2;flex-grow:4;background-color:#000;height:42px}.gs-dev-tools .gs-bottom{background-color:#000;border-top:none}.gs-dev-tools .timeline{top:50%;transform:translate(0,-50%)}.gs-dev-tools .gs-bottom-right,.gs-dev-tools .in-point,.gs-dev-tools .out-point,.gs-dev-tools .rewind,.gs-dev-tools .select-animation-container{display:none}.gs-dev-tools .play-pause{width:18px;height:18px;padding:4px 6px;margin-left:14px}.gs-dev-tools .time-scale{min-width:26px}.gs-dev-tools .loop{width:18px;min-width:18px;display:none}.gs-dev-tools .progress-bar,.gs-dev-tools .timeline-track{right:0}}'; _addedCSS = true; } if (_isString(element)) { element = _doc.querySelector(element); } var root = _createElement("div", element || _docEl.getElementsByTagName("body")[0] || _docEl); root.setAttribute("class", "gs-dev-tools" + (minimal ? " minimal" : "")); root.innerHTML = '
0.00 / 0.00
'; if (element) { root.style.position = "absolute"; root.style.top = minimal ? "calc(100% - 42px)" : "calc(100% - 51px)"; } if (css) { if (_isString(css)) { root.style.cssText = css; } else if (_isObject(css)) { css.data = "root"; gsap.set(root, css).kill(); } if (root.style.top) { root.style.bottom = "auto"; } if (root.style.width) { gsap.set(root, { xPercent: -50, left: "50%", right: "auto", data: "root" }).kill(); } } if (!minimal && root.offsetWidth < 600) { root.setAttribute("class", "gs-dev-tools minimal"); if (element) { root.style.top = "calc(100% - 42px)"; } } return root; }, _clickedOnce = true, //perhaps we shouldn't preventDefault() on the first mousedown/touchstart/pointerdown so that iframes get focus properly. Did that previously, but now it seems to prevent interaction on the first click (annoying). _addListener = function _addListener(e, type, callback, capture) { var handler, altType; if (type === "mousedown" || type === "mouseup") { e.style.cursor = "pointer"; } if (type === "mousedown") { //some browsers call BOTH mousedown AND touchstart, for example, on a single interaction so we need to skip one of them if both are called within 100ms. altType = !_isUndefined(e.onpointerdown) ? "pointerdown" : !_isUndefined(e.ontouchstart) ? "touchstart" : null; if (altType) { handler = function handler(event) { if (event.target.nodeName.toLowerCase() !== "select" && event.type === altType) { //don't preventDefault() on a up or down by a certain amount. _shiftSelectedValue = function _shiftSelectedValue(element, amount, label) { var options = element.options, i = Math.min(options.length - 1, Math.max(0, element.selectedIndex + amount)); element.selectedIndex = i; if (label) { label.innerHTML = options[i].innerHTML; } return options[i].value; }, //moves everything from _globalTimeline into _recordedRoot and updates the _rootTween if it is currently controlling the Global timeline (_recordedRoot). _recordedTemp is just a temporary recording area for anything that happens while _recordedRoot is paused. Returns true if the _recordedRoot's duration changed due to the merge. _merge = function _merge() { var t = _globalTimeline._first, duration, next, target; if (_rootInstance) { duration = _recordedRoot._dur; while (t) { next = t._next; target = t._targets && t._targets[0]; if (!(_isFunction(target) && target === t.vars.onComplete && !t._dur) && !(target && target._gsIgnore)) { //typically, delayedCalls aren't included in the _recordedTemp, but since the hijacked add() below fires BEFORE TweenLite's constructor sets the target, we couldn't check that target === vars.onComplete there. And Draggable creates a tween with just an onComplete (no onReverseComplete), thus it fails that test. Therefore, we test again here to avoid merging that in. _recordedRoot.add(t, t._start - t._delay); } t = next; } return duration !== _recordedRoot.duration(); } }, _updateRootDuration = function _updateRootDuration() { if (_rootInstance) { _rootInstance.update(); _rootIsDirty = false; } gsap.ticker.remove(_updateRootDuration); }, _buildPlayPauseMorph = function _buildPlayPauseMorph(svg) { var tl = gsap.timeline({ data: "root", parent: _independentRoot, onComplete: function onComplete() { return tl.kill(); } }, _independentRoot._time); tl.to(svg.querySelector(".play-1"), { duration: 0.4, attr: { d: "M5.75,3.13 C5.75,9.79 5.75,16.46 5.75,23.13 4.08,23.13 2.41,23.13 0.75,23.13 0.75,16.46 0.75,9.79 0.75,3.12 2.41,3.12 4.08,3.12 5.75,3.12" }, ease: "power2.inOut", rotation: 360, transformOrigin: "50% 50%" }).to(svg.querySelector(".play-2"), { duration: 0.4, attr: { d: "M16.38,3.13 C16.38,9.79 16.38,16.46 16.38,23.13 14.71,23.13 13.04,23.13 11.38,23.13 11.38,16.46 11.38,9.79 11.38,3.12 13.04,3.12 14.71,3.12 16.38,3.12" }, ease: "power2.inOut", rotation: 360, transformOrigin: "50% 50%" }, 0.05); return tl; }, _buildLoopAnimation = function _buildLoopAnimation(svg) { var tl = gsap.timeline({ data: "root", id: "loop", parent: _independentRoot, paused: true, onComplete: function onComplete() { return tl.kill(); } }, _independentRoot._time); tl.to(svg, { duration: 0.5, rotation: 360, ease: "power3.inOut", transformOrigin: "50% 50%" }).to(svg.querySelectorAll(".loop-path"), { duration: 0.5, fill: "#91e600", ease: "none" }, 0); return tl; }, _getAnimationById = function _getAnimationById(id) { return gsap.getById(id) || _independentRoot.getById(id) || id === _recordedRoot.vars.id && _recordedRoot; }, _initCore = function _initCore(core) { gsap = core || _getGSAP(); if (!_coreInitted) { if (gsap && _windowExists()) { _doc = document; _docEl = _doc.documentElement; _win = window; _context = gsap.core.context || function () {}; gsap.registerPlugin(Draggable); _globalTimeline = gsap.globalTimeline; _globalTimeline._sort = true; _globalTimeline.autoRemoveChildren = false; Animation = gsap.core.Animation; _independentRoot = gsap.timeline({ data: "indy", autoRemoveChildren: true, smoothChildTiming: true }); _independentRoot.kill(); _independentRoot._dp = 0; //don't let it revert to the global timeline as its parent. _independentRoot.to({}, { duration: 1e12 }); _recordedRoot = gsap.timeline({ data: "root", id: "Global Timeline", autoRemoveChildren: false, smoothChildTiming: true, parent: _independentRoot }, 0); _rootTween = gsap.to(_recordedRoot, { duration: 1, time: 1, ease: "none", data: "root", id: "_rootTween", paused: true, immediateRender: false, parent: _independentRoot }, 0); // so that auto-overwriting works. Initially we transferred the tweens to the _recordedRoot. _globalTimeline.killTweensOf = function (targets, props, onlyActive) { _recordedRoot.killTweensOf(targets, props, onlyActive); _recordedRoot.killTweensOf.call(_globalTimeline, targets, props, onlyActive); }; _independentRoot._start = gsap.ticker.time; gsap.ticker.add(function (time) { return _independentRoot.render(time - _independentRoot._start); }); // before 3.7.0, the listener below was necessary (in place of the line above) // gsap.ticker.add(time => { // _independentRoot._initted || _independentRoot.render(0.001, true); // to prevent callbacks from being fired on the first tick, like onComplete would fire immediately and then again at the correct time. // _independentRoot.render(time - _independentRoot._start); // }); //align the all of the playheads so they're starting at 0 now. _globalTimeline._start += _globalTimeline._time; _recordedRoot._start = _globalTimeline._time = _globalTimeline._tTime = 0; _delayedCall = function _delayedCall(delay, callback, params, scope) { return gsap.to(callback, { delay: delay, duration: 0, onComplete: callback, onReverseComplete: callback, onCompleteParams: params, onReverseCompleteParams: params, callbackScope: scope, parent: _independentRoot }, _independentRoot._time); }; //in case GSDevTools.create() is called before anything is actually on the global timeline, we've gotta update it or else the duration will be 0 and it'll be stuck. _delayedCall(0.01, function () { return _rootInstance ? _rootInstance.update() : _merge(); }); //initially we record everything into the _recordedRoot Timeline because developers might call GSDevTools.create() AFTER some of their code executes, but after 2 seconds if there aren't any GSDevTool instances that have globalSync enabled, we should dump all the stuff from _recordedRoot into the global timeline to improve performance and avoid issues where _recordedRoot is paused and reaches its end and wants to stop the playhead. _delayedCall(2, function () { var t, next, offset; if (!_rootInstance) { _merge(); t = _recordedRoot._first; offset = _recordedRoot._start; while (t) { next = t._next; //any animations that aren't finished should be dumped into the root timeline. If they're done, just kill them. if (t._tDur !== t._tTime || !t._dur && t.progress() !== 1) { _globalTimeline.add(t, t._start - t._delay + offset); } else { t.kill(); } t = next; } } if (GSDevTools.globalRecordingTime > 2) { _delayedCall(GSDevTools.globalRecordingTime - 2, function () { _rootInstance && _rootInstance.update(); _recording = false; _globalTimeline.autoRemoveChildren = true; }); } else { _recording = false; _globalTimeline.autoRemoveChildren = true; } _startupPhase = false; }); _coreInitted = 1; } } }, _checkIndependence = function _checkIndependence(animation, vars) { if (!vars.globalSync && animation.parent !== _globalTimeline) { //in case it's nested in a timeline (playing it won't help if the parent timeline isn't playing). _globalTimeline.add(animation, _globalTimeline.time()); } }, GSDevTools = function GSDevTools(vars) { if (!_coreInitted) { _initCore(); gsap || console.warn("Please gsap.registerPlugin(GSDevTools)"); } this.vars = vars = vars || {}; if (vars.animation) { (GSDevTools.getByAnimation(vars.animation) || { kill: function kill() { return 0; } }).kill(); } vars.id = vars.id || (_isString(vars.animation) ? vars.animation : _idSeed++); //try to find a unique ID so that sessionStorage can be mapped to it (otherwise, for example, all the embedded codepens on a page would share the same settings). So if no id is defined, see if there's a string-based "animation" defined. Last of all, we default to a numeric counter that we increment. _lookup[vars.id + ""] = this; "globalSync" in vars || (vars.globalSync = !vars.animation); //if the user calls create() and passes in an animation AFTER the initial recording time has elapsed, there's a good chance the animation won't be in the recordedRoot, so we change the default globalSync to false because that's the most intuitive behavior. //GENERAL/UTILITY var _self = this, root = _createRootElement(vars.container, vars.minimal, vars.css), find = function find(s) { return root.querySelector(s); }, record = function record(key, value) { if (vars.persist !== false && _supportsStorage) { sessionStorage.setItem("gs-dev-" + key + vars.id, value); } return value; }, recall = function recall(key) { var value; if (vars.persist !== false && _supportsStorage) { value = sessionStorage.getItem("gs-dev-" + key + vars.id); return key === "animation" ? value : key === "loop" ? value === "true" : parseFloat(value); // handle data typing too. } }, //SCRUBBER/PROGRESS playhead = find(".playhead"), timelineTrack = find(".timeline-track"), progressBar = find(".progress-bar"), timeLabel = find(".time"), durationLabel = find(".duration"), pixelToTimeRatio, timeAtDragStart, dragged, skipDragUpdates, progress = 0, inPoint = find(".in-point"), outPoint = find(".out-point"), inProgress = 0, outProgress = 100, pausedWhenDragStarted, list = find(".animation-list"), animationLabel = find(".animation-label"), selectedAnimation, //the currently selected animation linkedAnimation, //the animation that's linked to all the controls and scrubber. This is always _rootTween if globalSync is true, so it can be different than the selectedAnimation! declaredAnimation, //whatever the user defines in the config object initially (often this will be null). If the user defines a string, it'll be resolved to a real Animation instance for this variable. startTime, endTime, _fullyInitialized, //we call initialize() initially, and then again on the very next tick just in case someone called GSDevTools.create() BEFORE they create their animations. This variable tracks that state. Note: we don't record sessionStorage.setItem() until we're fully initialized, otherwise we may inadvertently set in/out points to the defaults just because the animation couldn't be found (yet). keyboardHandler, playPauseButton = find(".play-pause"), playPauseMorph = _buildPlayPauseMorph(playPauseButton), paused = false, loopButton = find(".loop"), loopAnimation = _buildLoopAnimation(loopButton), loopEnabled, timeScale = find(".time-scale select"), timeScaleLabel = find(".time-scale-label"), //spits back a common onPress function for anything that's dragged along the timeline (playhead, inPoint, outPoint). The originRatio is a value from 0-1 indicating how far along the x-axis the origin is located (0.5 is in the center, 0 is left, 1 is on right side). limitElement is optional, and sets the bounds such that the element can't be dragged past the limitElement. onPressTimeline = function onPressTimeline(element, originRatio, limitToInOut) { return function (e) { var trackBounds = timelineTrack.getBoundingClientRect(), elementBounds = element.getBoundingClientRect(), left = elementBounds.width * originRatio, x = gsap.getProperty(element, "x"), minX = trackBounds.left - elementBounds.left - left + x, maxX = trackBounds.right - elementBounds.right + (elementBounds.width - left) + x, unlimitedMinX = minX, limitBounds; if (limitToInOut) { if (element !== inPoint) { limitBounds = inPoint.getBoundingClientRect(); if (limitBounds.left) { //if inPoint is hidden (like display:none), ignore. minX += limitBounds.left + limitBounds.width - trackBounds.left; } } if (element !== outPoint) { limitBounds = outPoint.getBoundingClientRect(); if (limitBounds.left) { //if outPoint is hidden (like display:none), ignore. maxX -= trackBounds.left + trackBounds.width - limitBounds.left; } } } pausedWhenDragStarted = paused; this.applyBounds({ minX: minX, maxX: maxX }); pixelToTimeRatio = linkedAnimation.duration() / trackBounds.width; timeAtDragStart = -unlimitedMinX * pixelToTimeRatio; if (!skipDragUpdates) { linkedAnimation.pause(timeAtDragStart + pixelToTimeRatio * this.x); } else { linkedAnimation.pause(); } if (this.target === playhead) { if (this.activated) { this.allowEventDefault = false; } this.activated = true; } dragged = true; }; }, progressDrag = Draggable.create(playhead, { type: "x", cursor: "ew-resize", allowNativeTouchScrolling: false, allowEventDefault: true, //otherwise, when dragged outside an iframe, the mouseup doesn't bubble up so it could seem "stuck" to the mouse. onPress: onPressTimeline(playhead, 0.5, true), onDrag: function onDrag() { var time = timeAtDragStart + pixelToTimeRatio * this.x; if (time < 0) { time = 0; } else if (time > linkedAnimation._dur) { time = linkedAnimation._dur; } if (!skipDragUpdates) { linkedAnimation.time(time); } progressBar.style.width = Math.min(outProgress - inProgress, Math.max(0, time / linkedAnimation._dur * 100 - inProgress)) + "%"; timeLabel.innerHTML = time.toFixed(2); }, onRelease: function onRelease() { paused || linkedAnimation.resume(); } })[0], resetInOut = function resetInOut() { inProgress = 0; outProgress = 100; inPoint.style.left = "0%"; outPoint.style.left = "100%"; record("in", inProgress); record("out", outProgress); updateProgress(true); }, inDrag = Draggable.create(inPoint, { type: "x", cursor: "ew-resize", zIndexBoost: false, allowNativeTouchScrolling: false, allowEventDefault: true, //otherwise, when dragged outside an iframe, the mouseup doesn't bubble up so it could seem "stuck" to the mouse. onPress: onPressTimeline(inPoint, 1, true), onDoubleClick: resetInOut, onDrag: function onDrag() { inProgress = (timeAtDragStart + pixelToTimeRatio * this.x) / linkedAnimation.duration() * 100; linkedAnimation.progress(inProgress / 100); updateProgress(true); }, onRelease: function onRelease() { if (inProgress < 0) { inProgress = 0; } _clearSelection(); //for responsiveness, convert the px-based transform into %-based left position. inPoint.style.left = inProgress + "%"; record("in", inProgress); gsap.set(inPoint, { x: 0, data: "root", display: "block" }); //set display:block so that it remains visible even when the minimal skin is enabled. if (!paused) { linkedAnimation.resume(); } } })[0], outDrag = Draggable.create(outPoint, { type: "x", cursor: "ew-resize", allowNativeTouchScrolling: false, allowEventDefault: true, //otherwise, when dragged outside an iframe, the mouseup doesn't bubble up so it could seem "stuck" to the mouse. zIndexBoost: false, onPress: onPressTimeline(outPoint, 0, true), onDoubleClick: resetInOut, onDrag: function onDrag() { outProgress = (timeAtDragStart + pixelToTimeRatio * this.x) / linkedAnimation.duration() * 100; linkedAnimation.progress(outProgress / 100); updateProgress(true); }, onRelease: function onRelease() { if (outProgress > 100) { outProgress = 100; } _clearSelection(); //for responsiveness, convert the px-based transform into %-based left position. outPoint.style.left = outProgress + "%"; record("out", outProgress); gsap.set(outPoint, { x: 0, data: "root", display: "block" }); //set display:block so that it remains visible even when the minimal skin is enabled. if (!pausedWhenDragStarted) { play(); linkedAnimation.resume(); } } })[0], updateProgress = function updateProgress(force) { // NOTE: "force" is actually the "time" when this method gets called by the gsap.ticker! if (progressDrag.isPressed && force !== true) { return; } var p = !loopEnabled && selectedAnimation._repeat === -1 ? selectedAnimation.totalTime() / selectedAnimation.duration() * 100 : linkedAnimation.progress() * 100 || 0, repeatDelayPhase = selectedAnimation._repeat && selectedAnimation._rDelay && selectedAnimation.totalTime() % (selectedAnimation.duration() + selectedAnimation._rDelay) > selectedAnimation.duration(), target; if (p > 100) { p = 100; } if (p >= outProgress) { if (loopEnabled && !linkedAnimation.paused() && !progressDrag.isDragging) { if (!repeatDelayPhase) { p = inProgress; target = linkedAnimation._targets && linkedAnimation._targets[0]; if (target === selectedAnimation) { //in case there are callbacks on the timeline, when we jump back to the start we should seek() so that the playhead doesn't drag [backward] past those and trigger them. target.seek(startTime + (endTime - startTime) * inProgress / 100); } if (selectedAnimation._repeat > 0 && !inProgress && outProgress === 100) { if (selectedAnimation.totalProgress() === 1) { linkedAnimation.totalProgress(0, true).resume(); } } else { linkedAnimation.progress(p / 100, true).resume(); } } } else { if (p !== outProgress || selectedAnimation._repeat === -1) { p = outProgress; linkedAnimation.progress(p / 100); } if (!paused && (outProgress < 100 || selectedAnimation.totalProgress() === 1 || selectedAnimation._repeat === -1)) { pause(); } } } else if (p < inProgress) { p = inProgress; linkedAnimation.progress(p / 100, true); } if (p !== progress || force === true) { progressBar.style.left = inProgress + "%"; progressBar.style.width = Math.max(0, p - inProgress) + "%"; playhead.style.left = p + "%"; timeLabel.innerHTML = linkedAnimation._time.toFixed(2); durationLabel.innerHTML = linkedAnimation._dur.toFixed(2); if (dragged) { playhead.style.transform = "translate(-50%,0)"; playhead._gsap.x = "0px"; playhead._gsap.xPercent = -50; dragged = false; } progress = p; } linkedAnimation.paused() !== paused && togglePlayPause(); // if the user has an addPause() in the middle of the animation. }, onPressSeekBar = function onPressSeekBar(e) { if (progressDrag.isPressed) { return; } var bounds = e.target.getBoundingClientRect(), x = (e.changedTouches ? e.changedTouches[0] : e).clientX, p = (x - bounds.left) / bounds.width * 100; if (p < inProgress) { inProgress = p = Math.max(0, p); inPoint.style.left = inProgress + "%"; inDrag.startDrag(e); return; } else if (p > outProgress) { outProgress = p = Math.min(100, p); outPoint.style.left = outProgress + "%"; outDrag.startDrag(e); return; } linkedAnimation.progress(p / 100).pause(); updateProgress(true); progressDrag.startDrag(e); }, //PLAY/PAUSE button play = function play() { if (linkedAnimation.progress() >= outProgress / 100) { _checkIndependence(linkedAnimation, vars); var target = linkedAnimation._targets && linkedAnimation._targets[0]; if (target === selectedAnimation) { //in case there are callbacks on the timeline, when we jump back to the start we should seek() so that the playhead doesn't drag [backward] past those and trigger them. target.seek(startTime + (endTime - startTime) * inProgress / 100); } if (linkedAnimation._repeat && !inProgress) { linkedAnimation.totalProgress(0, true); //for repeating animations, don't get stuck in the last iteration - jump all the way back to the start. } else if (!linkedAnimation.reversed()) { linkedAnimation.progress(inProgress / 100, true); } } playPauseMorph.play(); linkedAnimation.resume(); if (paused) { _self.update(); } paused = false; }, pause = function pause() { playPauseMorph.reverse(); if (linkedAnimation) { linkedAnimation.pause(); } paused = true; }, togglePlayPause = function togglePlayPause() { if (paused) { play(); } else { pause(); } }, //REWIND button onPressRewind = function onPressRewind(e) { if (progressDrag.isPressed) { return; } //_self.update(); _checkIndependence(linkedAnimation, vars); var target = linkedAnimation._targets && linkedAnimation._targets[0]; if (target === selectedAnimation) { //in case there are callbacks on the timeline, when we jump back to the start we should seek() so that the playhead doesn't drag [backward] past those and trigger them. target.seek(startTime + (endTime - startTime) * inProgress / 100); } linkedAnimation.progress(inProgress / 100, true); if (!paused) { linkedAnimation.resume(); } }, //LOOP button loop = function loop(value) { loopEnabled = value; record("loop", loopEnabled); if (loopEnabled) { loopAnimation.play(); if (linkedAnimation.progress() >= outProgress / 100) { var target = linkedAnimation._targets && linkedAnimation._targets[0]; if (target === selectedAnimation) { //in case there are callbacks on the timeline, when we jump back to the start we should seek() so that the playhead doesn't drag [backward] past those and trigger them. target.seek(startTime + (endTime - startTime) * inProgress / 100); } if (selectedAnimation._repeat && !inProgress && outProgress === 100) { linkedAnimation.totalProgress(0, true); } else { linkedAnimation.progress(inProgress / 100, true); } play(); } } else { loopAnimation.reverse(); } }, toggleLoop = function toggleLoop() { return loop(!loopEnabled); }, //ANIMATIONS list updateList = function updateList() { var animations = _getChildrenOf(declaredAnimation && !vars.globalSync ? declaredAnimation : _recordedRoot, true), options = list.children, matches = 0, option, i; if (declaredAnimation && !vars.globalSync) { animations.unshift(declaredAnimation); } else if (!vars.hideGlobalTimeline) { animations.unshift(_recordedRoot); } for (i = 0; i < animations.length; i++) { option = options[i] || _createElement("option", list); option.animation = animations[i]; matches = i && animations[i].vars.id === animations[i - 1].vars.id ? matches + 1 : 0; option.setAttribute("value", option.innerHTML = animations[i].vars.id + (matches ? " [" + matches + "]" : animations[i + 1] && animations[i + 1].vars.id === animations[i].vars.id ? " [0]" : "")); } for (; i < options.length; i++) { list.removeChild(options[i]); } }, animation = function animation(anim) { var ts = parseFloat(timeScale.options[timeScale.selectedIndex].value) || 1, tl, maxDuration; if (!arguments.length) { return selectedAnimation; } if (_isString(anim)) { anim = _getAnimationById(anim); } //console.log("animation() ", anim.vars.id); if (!(anim instanceof Animation)) { console.warn("GSDevTools error: invalid animation."); } if (anim.scrollTrigger) { console.warn("GSDevTools can't work with ScrollTrigger-based animations; either the scrollbar -OR- the GSDevTools scrubber can control the animation."); } if (anim === selectedAnimation) { return; } if (selectedAnimation) { selectedAnimation._inProgress = inProgress; selectedAnimation._outProgress = outProgress; } selectedAnimation = anim; if (linkedAnimation) { ts = linkedAnimation.timeScale(); if (linkedAnimation._targets && linkedAnimation._targets[0] === declaredAnimation) { declaredAnimation.resume(); linkedAnimation.kill(); } } inProgress = selectedAnimation._inProgress || 0; outProgress = selectedAnimation._outProgress || 100; inPoint.style.left = inProgress + "%"; outPoint.style.left = outProgress + "%"; if (_fullyInitialized) { //don't record inProgress/outProgress unless we're fully instantiated because people may call GSDevTools.create() before creating/defining their animations, thus the inTime/outTime may not exist yet. record("animation", selectedAnimation.vars.id); record("in", inProgress); record("out", outProgress); } startTime = 0; maxDuration = vars.maxDuration || Math.min(1000, _getClippedDuration(selectedAnimation)); if (selectedAnimation === _recordedRoot || vars.globalSync) { _merge(); linkedAnimation = _rootTween; _rootInstance && _rootInstance !== _self && console.warn("Error: GSDevTools can only have one instance that's globally synchronized."); _rootInstance = _self; if (selectedAnimation !== _recordedRoot) { tl = selectedAnimation; endTime = tl.totalDuration(); if (endTime > 99999999) { //in the case of an infinitely repeating animation, just use a single iteration's duration instead. endTime = tl.duration(); } while (tl.parent) { startTime = startTime / tl._ts + tl._start; endTime = endTime / tl._ts + tl._start; tl = tl.parent; } } else { endTime = _recordedRoot.duration(); } if (endTime - startTime > maxDuration) { //cap end time at 1000 because it doesn't seem reasonable to accommodate super long stuff. endTime = startTime + maxDuration; } _recordedRoot.pause(startTime); _rootTween.vars.time = endTime; _rootTween.invalidate(); _rootTween.duration(endTime - startTime).timeScale(ts); //wait for a tick before starting because some browsers freeze things immediately following a . e.target.blur(); } paused && play(); }, //TIMESCALE button onChangeTimeScale = function onChangeTimeScale(e) { var ts = parseFloat(timeScale.options[timeScale.selectedIndex].value) || 1, target; linkedAnimation.timeScale(ts); record("timeScale", ts); if (!paused) { if (linkedAnimation.progress() >= outProgress / 100) { target = linkedAnimation._targets && linkedAnimation._targets[0]; if (target === selectedAnimation) { //in case there are callbacks on the timeline, when we jump back to the start we should seek() so that the playhead doesn't drag [backward] past those and trigger them. target.seek(startTime + (endTime - startTime) * inProgress / 100); } linkedAnimation.progress(inProgress / 100, true).pause(); } else { linkedAnimation.pause(); } _delayedCall(0.01, function () { return linkedAnimation.resume(); }); } timeScaleLabel.innerHTML = ts + "x"; if (timeScale.blur) { //so that if an option is selected, and then the user tries to hit the up/down arrow, it doesn't just try selecting something else in the