Add a wrapper around tweener to do some extra integration
Automatically removes tweens on destroyed actors, and provides additional "animation started/stopped" callbacks (eg, for tracking whether or not to show window clone titles)
This commit is contained in:
parent
7d474b2217
commit
e79c776c2e
@ -1,8 +1,7 @@
|
||||
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
||||
|
||||
const Clutter = imports.gi.Clutter;
|
||||
const Big = imports.gi.Big;
|
||||
const Tweener = imports.tweener.tweener;
|
||||
const Clutter = imports.gi.Clutter;
|
||||
|
||||
const DEFAULT_BUTTON_COLOR = new Clutter.Color();
|
||||
DEFAULT_BUTTON_COLOR.from_pixel(0xeeddcc66);
|
||||
|
@ -5,11 +5,11 @@ const Gio = imports.gi.Gio;
|
||||
const Mainloop = imports.mainloop;
|
||||
const Shell = imports.gi.Shell;
|
||||
const Signals = imports.signals;
|
||||
const Tweener = imports.tweener.tweener;
|
||||
|
||||
const Overlay = imports.ui.overlay;
|
||||
const Panel = imports.ui.panel;
|
||||
const RunDialog = imports.ui.runDialog;
|
||||
const Tweener = imports.ui.tweener;
|
||||
const WindowManager = imports.ui.windowManager;
|
||||
|
||||
const DEFAULT_BACKGROUND_COLOR = new Clutter.Color();
|
||||
@ -21,78 +21,6 @@ let overlayActive = false;
|
||||
let runDialog = null;
|
||||
let wm = null;
|
||||
|
||||
// The "FrameTicker" object is an object used to feed new frames to Tweener
|
||||
// so it can update values and redraw. The default frame ticker for
|
||||
// Tweener just uses a simple timeout at a fixed frame rate and has no idea
|
||||
// of "catching up" by dropping frames.
|
||||
//
|
||||
// We substitute it with custom frame ticker here that connects Tweener to
|
||||
// a Clutter.TimeLine. Now, Clutter.Timeline itself isn't a whole lot more
|
||||
// sophisticated than a simple timeout at a fixed frame rate, but at least
|
||||
// it knows how to drop frames. (See HippoAnimationManager for a more
|
||||
// sophisticated view of continous time updates; even better is to pay
|
||||
// attention to the vertical vblank and sync to that when possible.)
|
||||
//
|
||||
function ClutterFrameTicker() {
|
||||
this._init();
|
||||
}
|
||||
|
||||
ClutterFrameTicker.prototype = {
|
||||
FRAME_RATE : 60,
|
||||
|
||||
_init : function() {
|
||||
// We don't have a finite duration; tweener will tell us to stop
|
||||
// when we need to stop, so use 1000 seconds as "infinity"
|
||||
this._timeline = new Clutter.Timeline({ fps: this.FRAME_RATE,
|
||||
duration: 1000*1000 });
|
||||
this._currentTime = 0;
|
||||
this._frame = 0;
|
||||
|
||||
let me = this;
|
||||
this._timeline.connect('new-frame',
|
||||
function(timeline, frame) {
|
||||
me._onNewFrame(frame);
|
||||
});
|
||||
},
|
||||
|
||||
_onNewFrame : function(frame) {
|
||||
// If there is a lot of setup to start the animation, then
|
||||
// first frame number we get from clutter might be a long ways
|
||||
// into the animation (or the animation might even be done).
|
||||
// That looks bad, so we always start one frame into the
|
||||
// animation then only do frame dropping from there.
|
||||
let delta;
|
||||
if (this._frame == 0)
|
||||
delta = 1;
|
||||
else
|
||||
delta = frame - this._frame;
|
||||
|
||||
if (delta == 0) // protect against divide-by-0 if we get a frame twice
|
||||
delta = 1;
|
||||
|
||||
// currentTime is in milliseconds
|
||||
this._currentTime += (1000 * delta) / this.FRAME_RATE;
|
||||
this._frame = frame;
|
||||
this.emit('prepare-frame');
|
||||
},
|
||||
|
||||
getTime : function() {
|
||||
return this._currentTime;
|
||||
},
|
||||
|
||||
start : function() {
|
||||
this._timeline.start();
|
||||
},
|
||||
|
||||
stop : function() {
|
||||
this._timeline.stop();
|
||||
this._frame = 0;
|
||||
this._currentTime = 0;
|
||||
}
|
||||
};
|
||||
|
||||
Signals.addSignalMethods(ClutterFrameTicker.prototype);
|
||||
|
||||
function start() {
|
||||
let global = Shell.Global.get();
|
||||
|
||||
@ -101,7 +29,7 @@ function start() {
|
||||
global.grab_dbus_service();
|
||||
global.start_task_panel();
|
||||
|
||||
Tweener.setFrameTicker(new ClutterFrameTicker());
|
||||
Tweener.init();
|
||||
|
||||
// The background color really only matters if there is no desktop
|
||||
// window (say, nautilus) running. We set it mostly so things look good
|
||||
|
@ -7,13 +7,13 @@ const Gtk = imports.gi.Gtk;
|
||||
const Mainloop = imports.mainloop;
|
||||
const Shell = imports.gi.Shell;
|
||||
const Signals = imports.signals;
|
||||
const Tweener = imports.tweener.tweener;
|
||||
|
||||
const AppDisplay = imports.ui.appDisplay;
|
||||
const DocDisplay = imports.ui.docDisplay;
|
||||
const GenericDisplay = imports.ui.genericDisplay;
|
||||
const Main = imports.ui.main;
|
||||
const Panel = imports.ui.panel;
|
||||
const Tweener = imports.ui.tweener;
|
||||
const Workspaces = imports.ui.workspaces;
|
||||
|
||||
const OVERLAY_BACKGROUND_COLOR = new Clutter.Color();
|
||||
|
255
js/ui/tweener.js
Normal file
255
js/ui/tweener.js
Normal file
@ -0,0 +1,255 @@
|
||||
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
||||
|
||||
const Clutter = imports.gi.Clutter;
|
||||
const Lang = imports.lang;
|
||||
const Mainloop = imports.mainloop;
|
||||
const Signals = imports.signals;
|
||||
const Tweener = imports.tweener.tweener;
|
||||
|
||||
// This is a wrapper around imports.tweener.tweener that adds a bit of
|
||||
// Clutter integration and some additional callbacks:
|
||||
//
|
||||
// 1. If the tweening target is a Clutter.Actor, then the tweenings
|
||||
// will automatically be removed if the actor is destroyed
|
||||
//
|
||||
// 2. If target._delegate.onAnimationStart() exists, it will be
|
||||
// called when the target starts being animated.
|
||||
//
|
||||
// 3. If target._delegate.onAnimationComplete() exists, it will be
|
||||
// called once the target is no longer being animated.
|
||||
//
|
||||
// The onAnimationStart() and onAnimationComplete() callbacks differ
|
||||
// from the tweener onStart and onComplete parameters, in that (1)
|
||||
// they track whether or not the target has *any* tweens attached to
|
||||
// it, as opposed to be called for *each* tween, and (2)
|
||||
// onAnimationComplete() is always called when the object stops being
|
||||
// animated, regardless of whether it stopped normally or abnormally.
|
||||
//
|
||||
// onAnimationComplete() is called at idle time, which means that if a
|
||||
// tween completes and then another is added before returning to the
|
||||
// main loop, the complete callback will not be called (until the new
|
||||
// tween finishes).
|
||||
|
||||
|
||||
// ActionScript Tweener methods that imports.tweener.tweener doesn't
|
||||
// currently implement: getTweens, getVersion, registerTransition,
|
||||
// setTimeScale, updateTime.
|
||||
|
||||
// imports.tweener.tweener methods that we don't re-export:
|
||||
// pauseAllTweens, removeAllTweens, resumeAllTweens. (It would be hard
|
||||
// to clean up properly after removeAllTweens, and also, any code that
|
||||
// calls any of these is almost certainly wrong anyway, because they
|
||||
// affect the entire application.)
|
||||
|
||||
|
||||
// Called from Main.start
|
||||
function init() {
|
||||
Tweener.setFrameTicker(new ClutterFrameTicker());
|
||||
}
|
||||
|
||||
|
||||
function addCaller(target, tweeningParameters) {
|
||||
_wrapTweening(target, tweeningParameters);
|
||||
Tweener.addCaller(target, tweeningParameters);
|
||||
}
|
||||
|
||||
function addTween(target, tweeningParameters) {
|
||||
_wrapTweening(target, tweeningParameters);
|
||||
Tweener.addTween(target, tweeningParameters);
|
||||
}
|
||||
|
||||
function _wrapTweening(target, tweeningParameters) {
|
||||
let state = _getTweenState(target);
|
||||
|
||||
if (target instanceof Clutter.Actor && !state.destroyedId)
|
||||
state.destroyedId = target.connect('destroy', _actorDestroyed);
|
||||
|
||||
_addHandler(target, tweeningParameters, 'onStart', _tweenStarted);
|
||||
_addHandler(target, tweeningParameters, 'onComplete', _tweenCompleted);
|
||||
}
|
||||
|
||||
function _getTweenState(target) {
|
||||
// If we were paranoid, we could keep a plist mapping targets to
|
||||
// states... but we're not that paranoid.
|
||||
if (!target.__ShellTweenerState)
|
||||
_resetTweenState(target);
|
||||
return target.__ShellTweenerState;
|
||||
}
|
||||
|
||||
function _resetTweenState(target) {
|
||||
let state = target.__ShellTweenerState;
|
||||
|
||||
if (state) {
|
||||
if (state.destroyedId)
|
||||
target.disconnect(state.destroyedId);
|
||||
if (state.idleCompletedId)
|
||||
Mainloop.source_remove(state.idleCompletedId);
|
||||
}
|
||||
|
||||
target.__ShellTweenerState = {};
|
||||
}
|
||||
|
||||
function _addHandler(target, params, name, handler) {
|
||||
if (params[name]) {
|
||||
let oldHandler = params[name];
|
||||
let oldScope = params[name + 'Scope'];
|
||||
let oldParams = params[name + 'Params'];
|
||||
let eventScope = oldScope ? oldScope : target;
|
||||
|
||||
params[name] = function () {
|
||||
oldHandler.apply(eventScope, oldParams);
|
||||
handler(target);
|
||||
};
|
||||
} else
|
||||
params[name] = function () { handler(target); };
|
||||
}
|
||||
|
||||
function _actorDestroyed(target) {
|
||||
_resetTweenState(target);
|
||||
Tweener.removeTweens(target);
|
||||
}
|
||||
|
||||
function _tweenStarted(target) {
|
||||
let state = _getTweenState(target);
|
||||
let delegate = target._delegate;
|
||||
|
||||
if (!state.running && delegate && delegate.onAnimationStart)
|
||||
delegate.onAnimationStart();
|
||||
state.running = true;
|
||||
}
|
||||
|
||||
function _tweenCompleted(target) {
|
||||
let state = _getTweenState(target);
|
||||
|
||||
if (!state.idleCompletedId)
|
||||
state.idleCompletedId = Mainloop.idle_add(Lang.bind(null, _idleCompleted, target));
|
||||
}
|
||||
|
||||
function _idleCompleted(target) {
|
||||
let state = _getTweenState(target);
|
||||
let delegate = target._delegate;
|
||||
|
||||
if (!isTweening(target)) {
|
||||
_resetTweenState(target);
|
||||
if (delegate && delegate.onAnimationComplete)
|
||||
delegate.onAnimationComplete();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getTweenCount(scope) {
|
||||
return Tweener.getTweenCount(scope);
|
||||
}
|
||||
|
||||
// imports.tweener.tweener doesn't provide this method (which exists
|
||||
// in the ActionScript version) but it's easy to implement.
|
||||
function isTweening(scope) {
|
||||
return Tweener.getTweenCount(scope) != 0;
|
||||
}
|
||||
|
||||
function removeTweens(scope) {
|
||||
if (Tweener.removeTweens.apply(null, arguments)) {
|
||||
// If we just removed the last active tween, clean up
|
||||
if (Tweener.getTweenCount(scope) == 0)
|
||||
_tweenCompleted(scope);
|
||||
return true;
|
||||
} else
|
||||
return false;
|
||||
}
|
||||
|
||||
function pauseTweens() {
|
||||
return Tweener.pauseTweens.apply(null, arguments);
|
||||
}
|
||||
|
||||
function resumeTweens() {
|
||||
return Tweener.resumeTweens.apply(null, arguments);
|
||||
}
|
||||
|
||||
|
||||
function registerSpecialProperty(name, getFunction, setFunction,
|
||||
parameters, preProcessFunction) {
|
||||
Tweener.registerSpecialProperty(name, getFunction, setFunction,
|
||||
parameters, preProcessFunction);
|
||||
}
|
||||
|
||||
function registerSpecialPropertyModifier(name, modifyFunction, getFunction) {
|
||||
Tweener.registerSpecialPropertyModifier(name, modifyFunction, getFunction);
|
||||
}
|
||||
|
||||
function registerSpecialPropertySplitter(name, splitFunction, parameters) {
|
||||
Tweener.registerSpecialPropertySplitter(name, splitFunction, parameters);
|
||||
}
|
||||
|
||||
|
||||
// The "FrameTicker" object is an object used to feed new frames to
|
||||
// Tweener so it can update values and redraw. The default frame
|
||||
// ticker for Tweener just uses a simple timeout at a fixed frame rate
|
||||
// and has no idea of "catching up" by dropping frames.
|
||||
//
|
||||
// We substitute it with custom frame ticker here that connects
|
||||
// Tweener to a Clutter.TimeLine. Now, Clutter.Timeline itself isn't a
|
||||
// whole lot more sophisticated than a simple timeout at a fixed frame
|
||||
// rate, but at least it knows how to drop frames. (See
|
||||
// HippoAnimationManager for a more sophisticated view of continous
|
||||
// time updates; even better is to pay attention to the vertical
|
||||
// vblank and sync to that when possible.)
|
||||
//
|
||||
function ClutterFrameTicker() {
|
||||
this._init();
|
||||
}
|
||||
|
||||
ClutterFrameTicker.prototype = {
|
||||
FRAME_RATE : 60,
|
||||
|
||||
_init : function() {
|
||||
// We don't have a finite duration; tweener will tell us to stop
|
||||
// when we need to stop, so use 1000 seconds as "infinity"
|
||||
this._timeline = new Clutter.Timeline({ fps: this.FRAME_RATE,
|
||||
duration: 1000*1000 });
|
||||
this._currentTime = 0;
|
||||
this._frame = 0;
|
||||
|
||||
let me = this;
|
||||
this._timeline.connect('new-frame',
|
||||
function(timeline, frame) {
|
||||
me._onNewFrame(frame);
|
||||
});
|
||||
},
|
||||
|
||||
_onNewFrame : function(frame) {
|
||||
// If there is a lot of setup to start the animation, then
|
||||
// first frame number we get from clutter might be a long ways
|
||||
// into the animation (or the animation might even be done).
|
||||
// That looks bad, so we always start one frame into the
|
||||
// animation then only do frame dropping from there.
|
||||
let delta;
|
||||
if (this._frame == 0)
|
||||
delta = 1;
|
||||
else
|
||||
delta = frame - this._frame;
|
||||
|
||||
if (delta == 0) // protect against divide-by-0 if we get a frame twice
|
||||
delta = 1;
|
||||
|
||||
// currentTime is in milliseconds
|
||||
this._currentTime += (1000 * delta) / this.FRAME_RATE;
|
||||
this._frame = frame;
|
||||
this.emit('prepare-frame');
|
||||
},
|
||||
|
||||
getTime : function() {
|
||||
return this._currentTime;
|
||||
},
|
||||
|
||||
start : function() {
|
||||
this._timeline.start();
|
||||
},
|
||||
|
||||
stop : function() {
|
||||
this._timeline.stop();
|
||||
this._frame = 0;
|
||||
this._currentTime = 0;
|
||||
}
|
||||
};
|
||||
|
||||
Signals.addSignalMethods(ClutterFrameTicker.prototype);
|
@ -4,9 +4,9 @@ const Clutter = imports.gi.Clutter;
|
||||
const Mainloop = imports.mainloop;
|
||||
const Meta = imports.gi.Meta;
|
||||
const Shell = imports.gi.Shell;
|
||||
const Tweener = imports.tweener.tweener;
|
||||
|
||||
const Main = imports.ui.main;
|
||||
const Tweener = imports.ui.tweener;
|
||||
|
||||
const WINDOW_ANIMATION_TIME = 0.25;
|
||||
const SWITCH_ANIMATION_TIME = 0.5;
|
||||
|
@ -10,11 +10,11 @@ const Meta = imports.gi.Meta;
|
||||
const Pango = imports.gi.Pango;
|
||||
const Shell = imports.gi.Shell;
|
||||
const Signals = imports.signals;
|
||||
const Tweener = imports.tweener.tweener;
|
||||
|
||||
const Main = imports.ui.main;
|
||||
const Overlay = imports.ui.overlay;
|
||||
const Panel = imports.ui.panel;
|
||||
const Tweener = imports.ui.tweener;
|
||||
|
||||
// Windows are slightly translucent in the overlay mode
|
||||
const WINDOW_OPACITY = 0.9 * 255;
|
||||
@ -58,6 +58,7 @@ WindowClone.prototype = {
|
||||
reactive: true,
|
||||
x: realWindow.x,
|
||||
y: realWindow.y });
|
||||
this.actor._delegate = this;
|
||||
this.realWindow = realWindow;
|
||||
this.metaWindow = realWindow.meta_window;
|
||||
this.origX = realWindow.x;
|
||||
@ -77,12 +78,6 @@ WindowClone.prototype = {
|
||||
this._havePointer = false;
|
||||
this._inDrag = false;
|
||||
this._buttonDown = false;
|
||||
|
||||
// We track the number of animations we are doing for the clone so we can
|
||||
// hide the floating title while animating. It seems like it should be
|
||||
// possible to use Tweener.getTweenCount(clone), but that annoyingly only
|
||||
// updates after onComplete is called.
|
||||
this._animationCount = 0;
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
@ -91,27 +86,6 @@ WindowClone.prototype = {
|
||||
this._title.destroy();
|
||||
},
|
||||
|
||||
addTween: function (params) {
|
||||
this._animationCount++;
|
||||
this._updateTitle();
|
||||
|
||||
if (params.onComplete) {
|
||||
let oldOnComplete = params.onComplete;
|
||||
let oldOnCompleteScope = params.onCompleteScope;
|
||||
let oldOnCompleteParams = params.onCompleteParams;
|
||||
let eventScope = oldOnCompleteScope ? oldOnCompleteScope : this.actor;
|
||||
|
||||
params.onComplete = function () {
|
||||
oldOnComplete.apply(eventScope, oldOnCompleteParams);
|
||||
this._onAnimationComplete();
|
||||
};
|
||||
} else
|
||||
params.onComplete = this._onAnimationComplete;
|
||||
params.onCompleteScope = this;
|
||||
|
||||
Tweener.addTween(this.actor, params);
|
||||
},
|
||||
|
||||
_onEnter: function (actor, event) {
|
||||
// If the user drags faster than we can follow, he'll end up
|
||||
// leaving the window temporarily and then re-entering it
|
||||
@ -132,7 +106,7 @@ WindowClone.prototype = {
|
||||
|
||||
this._havePointer = false;
|
||||
|
||||
if (this._animationCount)
|
||||
if (Tweener.isTweening(this.actor))
|
||||
return;
|
||||
|
||||
actor.raise(this.stackAbove);
|
||||
@ -140,7 +114,7 @@ WindowClone.prototype = {
|
||||
},
|
||||
|
||||
_onButtonPress : function (actor, event) {
|
||||
if (this._animationCount)
|
||||
if (Tweener.isTweening(this.actor))
|
||||
return;
|
||||
|
||||
actor.raise_top();
|
||||
@ -209,13 +183,14 @@ WindowClone.prototype = {
|
||||
this.emit('dragged', stageX, stageY, event.get_time());
|
||||
if (this.realWindow.get_workspace() == origWorkspace) {
|
||||
// Didn't get moved elsewhere, restore position
|
||||
this.addTween({ x: this._dragStartX + this._dragOffsetX,
|
||||
y: this._dragStartY + this._dragOffsetY,
|
||||
time: SNAP_BACK_ANIMATION_TIME,
|
||||
transition: "easeOutQuad",
|
||||
onComplete: this._onSnapBackComplete,
|
||||
onCompleteScope: this
|
||||
});
|
||||
Tweener.addTween(this.actor,
|
||||
{ x: this._dragStartX + this._dragOffsetX,
|
||||
y: this._dragStartY + this._dragOffsetY,
|
||||
time: SNAP_BACK_ANIMATION_TIME,
|
||||
transition: "easeOutQuad",
|
||||
onComplete: this._onSnapBackComplete,
|
||||
onCompleteScope: this
|
||||
});
|
||||
// Most likely, the clone is going to move away from the
|
||||
// pointer now. But that won't cause a leave-event, so
|
||||
// do this by hand. Of course, if the window only snaps
|
||||
@ -234,12 +209,15 @@ WindowClone.prototype = {
|
||||
this.actor.set_position(this._dragOrigX, this._dragOrigY);
|
||||
},
|
||||
|
||||
_onAnimationComplete : function () {
|
||||
this._animationCount--;
|
||||
if (this._animationCount == 0) {
|
||||
this._updateTitle();
|
||||
this.actor.raise(this.stackAbove);
|
||||
}
|
||||
// Called by Tweener
|
||||
onAnimationStart : function () {
|
||||
this._updateTitle();
|
||||
},
|
||||
|
||||
// Called by Tweener
|
||||
onAnimationComplete : function () {
|
||||
this._updateTitle();
|
||||
this.actor.raise(this.stackAbove);
|
||||
},
|
||||
|
||||
_createTitle : function () {
|
||||
@ -313,7 +291,7 @@ WindowClone.prototype = {
|
||||
_updateTitle : function () {
|
||||
let shouldShow = (this._havePointer &&
|
||||
!this._buttonDown &&
|
||||
this._animationCount == 0);
|
||||
!Tweener.isTweening(this.actor));
|
||||
|
||||
if (shouldShow)
|
||||
this._showTitle();
|
||||
@ -528,21 +506,16 @@ Workspace.prototype = {
|
||||
let desiredSize = global.screen_width * fraction;
|
||||
let scale = Math.min(desiredSize / size, 1.0);
|
||||
|
||||
let tweenProperties = {
|
||||
x: xCenter - 0.5 * scale * clone.actor.width,
|
||||
y: yCenter - 0.5 * scale * clone.actor.height,
|
||||
scale_x: scale,
|
||||
scale_y: scale,
|
||||
time: Overlay.ANIMATION_TIME,
|
||||
opacity: WINDOW_OPACITY,
|
||||
transition: "easeOutQuad"
|
||||
};
|
||||
|
||||
// workspace_relative assumes that the workspace is zooming in or out
|
||||
if (workspaceZooming)
|
||||
tweenProperties['workspace_relative'] = this;
|
||||
|
||||
clone.addTween(tweenProperties);
|
||||
Tweener.addTween(clone.actor,
|
||||
{ x: xCenter - 0.5 * scale * clone.actor.width,
|
||||
y: yCenter - 0.5 * scale * clone.actor.height,
|
||||
scale_x: scale,
|
||||
scale_y: scale,
|
||||
workspace_relative: workspaceZooming ? this : null,
|
||||
time: Overlay.ANIMATION_TIME,
|
||||
opacity: WINDOW_OPACITY,
|
||||
transition: "easeOutQuad"
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -659,15 +632,16 @@ Workspace.prototype = {
|
||||
|
||||
for (let i = 1; i < this._windows.length; i++) {
|
||||
let clone = this._windows[i];
|
||||
clone.addTween({ x: clone.origX,
|
||||
y: clone.origY,
|
||||
scale_x: 1.0,
|
||||
scale_y: 1.0,
|
||||
workspace_relative: this,
|
||||
time: Overlay.ANIMATION_TIME,
|
||||
opacity: 255,
|
||||
transition: "easeOutQuad"
|
||||
});
|
||||
Tweener.addTween(clone.actor,
|
||||
{ x: clone.origX,
|
||||
y: clone.origY,
|
||||
scale_x: 1.0,
|
||||
scale_y: 1.0,
|
||||
workspace_relative: this,
|
||||
time: Overlay.ANIMATION_TIME,
|
||||
opacity: 255,
|
||||
transition: "easeOutQuad"
|
||||
});
|
||||
}
|
||||
|
||||
this.leavingOverlay = false;
|
||||
@ -1207,6 +1181,9 @@ Tweener.registerSpecialPropertyModifier("workspace_relative", _workspace_relativ
|
||||
function _workspace_relative_modifier(workspace) {
|
||||
let endX, endY;
|
||||
|
||||
if (!workspace)
|
||||
return [];
|
||||
|
||||
if (workspace.leavingOverlay) {
|
||||
endX = workspace.fullSizeX;
|
||||
endY = workspace.fullSizeY;
|
||||
|
Loading…
Reference in New Issue
Block a user