Keyframe animation doesn't 't show up in the Animations timeline
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Models / Animation.js
1 /*
2  * Copyright (C) 2020 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 WI.Animation = class Animation extends WI.Object
27 {
28     constructor(animationId, {name, cssAnimationName, cssTransitionProperty, effect, backtrace} = {})
29     {
30         super();
31
32         console.assert(animationId);
33         console.assert((!cssAnimationName && !cssTransitionProperty) || !!cssAnimationName !== !!cssTransitionProperty);
34
35         this._animationId = animationId;
36
37         this._name = name || null;
38         this._cssAnimationName = cssAnimationName || null;
39         this._cssTransitionProperty = cssTransitionProperty || null;
40         this._updateEffect(effect);
41         this._backtrace = backtrace || [];
42
43         this._effectTarget = undefined;
44         this._requestEffectTargetCallbacks = null;
45     }
46
47     // Static
48
49     static fromPayload(payload)
50     {
51         return new WI.Animation(payload.animationId, {
52             name: payload.name,
53             cssAnimationName: payload.cssAnimationName,
54             cssTransitionProperty: payload.cssTransitionProperty,
55             effect: payload.effect,
56             backtrace: Array.isArray(payload.backtrace) ? payload.backtrace.map((item) => WI.CallFrame.fromPayload(WI.mainTarget, item)) : [],
57         });
58     }
59
60     static displayNameForAnimationType(animationType, plural)
61     {
62         switch (animationType) {
63         case WI.Animation.Type.WebAnimation:
64             return plural ? WI.UIString("Web Animations") : WI.UIString("Web Animation");
65         case WI.Animation.Type.CSSAnimation:
66             return plural ? WI.UIString("CSS Animations") : WI.UIString("CSS Animation");
67         case WI.Animation.Type.CSSTransition:
68             return plural ? WI.UIString("CSS Transitions") : WI.UIString("CSS Transition");
69         }
70
71         console.assert(false, "Unknown animation type", animationType);
72         return null;
73     }
74
75     static displayNameForPlaybackDirection(playbackDirection)
76     {
77         switch (playbackDirection) {
78         case WI.Animation.PlaybackDirection.Normal:
79             return WI.UIString("Normal", "Web Animation Playback Direction Normal", "Indicates that the playback direction of this web animation is normal (e.g. forwards)");
80         case WI.Animation.PlaybackDirection.Reverse:
81             return WI.UIString("Reverse", "Web Animation Playback Direction Reverse", "Indicates that the playback direction of this web animation is reversed (e.g. backwards)");
82         case WI.Animation.PlaybackDirection.Alternate:
83             return WI.UIString("Alternate", "Web Animation Playback Direction Alternate", "Indicates that the playback direction of this web animation alternates between normal and reversed on each iteration");
84         case WI.Animation.PlaybackDirection.AlternateReverse:
85             return WI.UIString("Alternate Reverse", "Web Animation Playback Direction Alternate Reverse", "Indicates that the playback direction of this web animation alternates between reversed and normal on each iteration");
86         }
87
88         console.assert(false, "Unknown playback direction", playbackDirection);
89         return null;
90     }
91
92     static displayNameForFillMode(fillMode)
93     {
94         switch (fillMode) {
95         case WI.Animation.FillMode.None:
96             return WI.UIString("None", "Web Animation Fill Mode None", "Indicates that this web animation does not apply any styles before it begins and after it ends");
97         case WI.Animation.FillMode.Forwards:
98             return WI.UIString("Forwards", "Web Animation Fill Mode Forwards", "Indicates that this web animation also applies styles after it ends");
99         case WI.Animation.FillMode.Backwards:
100             return WI.UIString("Backwards", "Web Animation Fill Mode Backwards", "Indicates that this web animation also applies styles before it begins");
101         case WI.Animation.FillMode.Both:
102             return WI.UIString("Both", "Web Animation Fill Mode Both", "Indicates that this web animation also applies styles before it begins and after it ends");
103         case WI.Animation.FillMode.Auto:
104             return WI.UIString("Auto", "Web Animation Fill Mode Auto", "Indicates that this web animation either does not apply any styles before it begins and after it ends or that it applies to both, depending on it's configuration");
105         }
106
107         console.assert(false, "Unknown fill mode", fillMode);
108         return null;
109     }
110
111     static resetUniqueDisplayNameNumbers()
112     {
113         WI.Animation._nextUniqueDisplayNameNumber = 1;
114     }
115
116     // Public
117
118     get animationId() { return this._animationId; }
119     get name() { return this._name; }
120     get cssAnimationName() { return this._cssAnimationName; }
121     get cssTransitionProperty() { return this._cssTransitionProperty; }
122     get backtrace() { return this._backtrace; }
123
124     get animationType()
125     {
126         if (this._cssAnimationName)
127             return WI.Animation.Type.CSSAnimation;
128         if (this._cssTransitionProperty)
129             return WI.Animation.Type.CSSTransition;
130         return WI.Animation.Type.WebAnimation;
131     }
132
133     get startDelay()
134     {
135         return "startDelay" in this._effect ? this._effect.startDelay : NaN;
136     }
137
138     get endDelay()
139     {
140         return "endDelay" in this._effect ? this._effect.endDelay : NaN;
141     }
142
143     get iterationCount()
144     {
145         return "iterationCount" in this._effect ? this._effect.iterationCount : NaN;
146     }
147
148     get iterationStart()
149     {
150         return "iterationStart" in this._effect ? this._effect.iterationStart : NaN;
151     }
152
153     get iterationDuration()
154     {
155         return "iterationDuration" in this._effect ? this._effect.iterationDuration : NaN;
156     }
157
158     get timingFunction()
159     {
160         return "timingFunction" in this._effect ? this._effect.timingFunction : null;
161     }
162
163     get playbackDirection()
164     {
165         return "playbackDirection" in this._effect ? this._effect.playbackDirection : null;
166     }
167
168     get fillMode()
169     {
170         return "fillMode" in this._effect ? this._effect.fillMode : null;
171     }
172
173     get keyframes()
174     {
175         return "keyframes" in this._effect ? this._effect.keyframes : [];
176     }
177
178     get displayName()
179     {
180         if (this._name)
181             return this._name;
182
183         if (this._cssAnimationName)
184             return this._cssAnimationName;
185
186         if (this._cssTransitionProperty)
187             return this._cssTransitionProperty;
188
189         if (!this._uniqueDisplayNameNumber)
190             this._uniqueDisplayNameNumber = WI.Animation._nextUniqueDisplayNameNumber++;
191         return WI.UIString("Animation %d").format(this._uniqueDisplayNameNumber);
192     }
193
194     requestEffectTarget(callback)
195     {
196         if (this._effectTarget !== undefined) {
197             callback(this._effectTarget);
198             return;
199         }
200
201         if (this._requestEffectTargetCallbacks) {
202             this._requestEffectTargetCallbacks.push(callback);
203             return;
204         }
205
206         this._requestEffectTargetCallbacks = [callback];
207
208         WI.domManager.ensureDocument();
209
210         let target = WI.assumingMainTarget();
211         target.AnimationAgent.requestEffectTarget(this._animationId, (error, nodeId) => {
212             this._effectTarget = !error ? WI.domManager.nodeForId(nodeId) : null;
213
214             for (let requestEffectTargetCallback of this._requestEffectTargetCallbacks)
215                 requestEffectTargetCallback(this._effectTarget);
216
217             this._requestEffectTargetCallbacks = null;
218         });
219     }
220
221     // AnimationManager
222
223     nameChanged(name)
224     {
225         this._name = name || null;
226
227         this.dispatchEventToListeners(WI.Animation.Event.NameChanged);
228     }
229
230     effectChanged(effect)
231     {
232         this._updateEffect(effect);
233     }
234
235     targetChanged()
236     {
237         this._effectTarget = undefined;
238
239         this.dispatchEventToListeners(WI.Animation.Event.TargetChanged);
240     }
241
242     // Private
243
244     _updateEffect(effect)
245     {
246         this._effect = effect || {};
247
248         if ("iterationCount" in this._effect) {
249             if (this._effect.iterationCount === -1)
250                 this._effect.iterationCount = Infinity;
251             else if (this._effect.iterationCount === null) {
252                 // COMPATIBILITY (iOS 14): an iteration count of `Infinity` was not properly handled.
253                 this._effect.iterationCount = Infinity;
254             }
255         }
256
257         if ("timingFunction" in this._effect)
258             this._effect.timingFunction = WI.CubicBezier.fromString(this._effect.timingFunction);
259
260         if ("keyframes" in this._effect) {
261             for (let keyframe of this._effect.keyframes) {
262                 if (keyframe.easing)
263                     keyframe.easing = WI.CubicBezier.fromString(keyframe.easing);
264
265                 if (keyframe.style)
266                     keyframe.style = keyframe.style.replaceAll(/;\s+/g, ";\n");
267             }
268         }
269
270         this.dispatchEventToListeners(WI.Animation.Event.EffectChanged);
271     }
272 };
273
274 WI.Animation._nextUniqueDisplayNameNumber = 1;
275
276 WI.Animation.Type = {
277     WebAnimation: "web-animation",
278     CSSAnimation: "css-animation",
279     CSSTransition: "css-transition",
280 };
281
282 WI.Animation.PlaybackDirection = {
283     Normal: "normal",
284     Reverse: "reverse",
285     Alternate: "alternate",
286     AlternateReverse: "alternate-reverse",
287 };
288
289 WI.Animation.FillMode = {
290     None: "none",
291     Forwards: "forwards",
292     Backwards: "backwards",
293     Both: "both",
294     Auto: "auto",
295 };
296
297 WI.Animation.Event = {
298     NameChanged: "animation-name-changed",
299     EffectChanged: "animation-effect-changed",
300     TargetChanged: "animation-target-changed",
301 };