Web Inspector: DOM: provide a way to disable/breakpoint all event listeners for a...
[WebKit-https.git] / Source / WebInspectorUI / UserInterface / Views / BezierEditor.js
1 /*
2  * Copyright (C) 2015 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.BezierEditor = class BezierEditor extends WI.Object
27 {
28     constructor()
29     {
30         super();
31
32         this._element = document.createElement("div");
33         this._element.classList.add("bezier-editor");
34
35         var editorWidth = 184;
36         var editorHeight = 200;
37         this._padding = 25;
38         this._controlHandleRadius = 7;
39         this._bezierWidth = editorWidth - (this._controlHandleRadius * 2);
40         this._bezierHeight = editorHeight - (this._controlHandleRadius * 2) - (this._padding * 2);
41
42         this._bezierPreviewContainer = this._element.createChild("div", "bezier-preview");
43         this._bezierPreviewContainer.title = WI.UIString("Restart animation");
44         this._bezierPreviewContainer.addEventListener("mousedown", this._resetPreviewAnimation.bind(this));
45
46         this._bezierPreview = this._bezierPreviewContainer.createChild("div");
47
48         this._bezierPreviewTiming = this._element.createChild("div", "bezier-preview-timing");
49
50         this._bezierContainer = this._element.appendChild(createSVGElement("svg"));
51         this._bezierContainer.setAttribute("width", editorWidth);
52         this._bezierContainer.setAttribute("height", editorHeight);
53         this._bezierContainer.classList.add("bezier-container");
54
55         let svgGroup = this._bezierContainer.appendChild(createSVGElement("g"));
56         svgGroup.setAttribute("transform", "translate(0, " + this._padding + ")");
57
58         let linearCurve = svgGroup.appendChild(createSVGElement("line"));
59         linearCurve.classList.add("linear-curve");
60         linearCurve.setAttribute("x1", this._controlHandleRadius);
61         linearCurve.setAttribute("y1", this._bezierHeight + this._controlHandleRadius);
62         linearCurve.setAttribute("x2", this._bezierWidth + this._controlHandleRadius);
63         linearCurve.setAttribute("y2", this._controlHandleRadius);
64
65         this._bezierCurve = svgGroup.appendChild(createSVGElement("path"));
66         this._bezierCurve.classList.add("bezier-curve");
67
68         function createControl(x1, y1)
69         {
70             x1 += this._controlHandleRadius;
71             y1 += this._controlHandleRadius;
72
73             let line = svgGroup.appendChild(createSVGElement("line"));
74             line.classList.add("control-line");
75             line.setAttribute("x1", x1);
76             line.setAttribute("y1", y1);
77             line.setAttribute("x2", x1);
78             line.setAttribute("y2", y1);
79
80             let handle = svgGroup.appendChild(createSVGElement("circle"));
81             handle.classList.add("control-handle");
82
83             return {point: null, line, handle};
84         }
85
86         this._inControl = createControl.call(this, 0, this._bezierHeight);
87         this._outControl = createControl.call(this, this._bezierWidth, 0);
88
89         this._numberInputContainer = this._element.createChild("div", "number-input-container");
90
91         function createBezierInput(id, {min, max} = {})
92         {
93             let key = "_bezier" + id + "Input";
94             this[key] = this._numberInputContainer.createChild("input");
95             this[key].type = "number";
96             this[key].step = 0.01;
97
98             if (!isNaN(min))
99                 this[key].min = min;
100
101             if (!isNaN(max))
102                 this[key].max = max;
103
104             this[key].addEventListener("input", this._handleNumberInputInput.bind(this));
105             this[key].addEventListener("keydown", this._handleNumberInputKeydown.bind(this));
106         }
107
108         createBezierInput.call(this, "InX", {min: 0, max: 1});
109         createBezierInput.call(this, "InY");
110         createBezierInput.call(this, "OutX", {min: 0, max: 1});
111         createBezierInput.call(this, "OutY");
112
113         this._selectedControl = null;
114         this._mouseDownPosition = null;
115         this._bezierContainer.addEventListener("mousedown", this);
116
117         WI.addWindowKeydownListener(this);
118     }
119
120     // Public
121
122     get element()
123     {
124         return this._element;
125     }
126
127     set bezier(bezier)
128     {
129         if (!bezier)
130             return;
131
132         var isCubicBezier = bezier instanceof WI.CubicBezier;
133         console.assert(isCubicBezier);
134         if (!isCubicBezier)
135             return;
136
137         this._bezier = bezier;
138         this._updateBezierPreview();
139     }
140
141     get bezier()
142     {
143         return this._bezier;
144     }
145
146     removeListeners()
147     {
148         WI.removeWindowKeydownListener(this);
149     }
150
151     // Protected
152
153     handleEvent(event)
154     {
155         switch (event.type) {
156         case "mousedown":
157             this._handleMousedown(event);
158             break;
159         case "mousemove":
160             this._handleMousemove(event);
161             break;
162         case "mouseup":
163             this._handleMouseup(event);
164             break;
165         }
166     }
167
168     handleKeydownEvent(event)
169     {
170         if (!this._selectedControl || !this._element.parentNode)
171             return false;
172
173         let horizontal = 0;
174         let vertical = 0;
175         switch (event.keyCode) {
176         case WI.KeyboardShortcut.Key.Up.keyCode:
177             vertical = -1;
178             break;
179         case WI.KeyboardShortcut.Key.Right.keyCode:
180             horizontal = 1;
181             break;
182         case WI.KeyboardShortcut.Key.Down.keyCode:
183             vertical = 1;
184             break;
185         case WI.KeyboardShortcut.Key.Left.keyCode:
186             horizontal = -1;
187             break;
188         default:
189             return false;
190         }
191
192         if (event.shiftKey) {
193             horizontal *= 10;
194             vertical *= 10;
195         }
196
197         vertical *= this._bezierWidth / 100;
198         horizontal *= this._bezierHeight / 100;
199
200         this._selectedControl.point.x = Number.constrain(this._selectedControl.point.x + horizontal, 0, this._bezierWidth);
201         this._selectedControl.point.y += vertical;
202         this._updateControl(this._selectedControl);
203         this._updateValue();
204
205         return true;
206     }
207
208     // Private
209
210     _handleMousedown(event)
211     {
212         if (event.button !== 0)
213             return;
214
215         window.addEventListener("mousemove", this, true);
216         window.addEventListener("mouseup", this, true);
217
218         this._bezierPreviewContainer.classList.remove("animate");
219         this._bezierPreviewTiming.classList.remove("animate");
220
221         this._updateControlPointsForMouseEvent(event, true);
222     }
223
224     _handleMousemove(event)
225     {
226         this._updateControlPointsForMouseEvent(event);
227     }
228
229     _handleMouseup(event)
230     {
231         this._selectedControl.handle.classList.remove("selected");
232         this._mouseDownPosition = null;
233         this._triggerPreviewAnimation();
234
235         window.removeEventListener("mousemove", this, true);
236         window.removeEventListener("mouseup", this, true);
237     }
238
239     _updateControlPointsForMouseEvent(event, calculateSelectedControlPoint)
240     {
241         var point = WI.Point.fromEventInElement(event, this._bezierContainer);
242         point.x = Number.constrain(point.x - this._controlHandleRadius, 0, this._bezierWidth);
243         point.y -= this._controlHandleRadius + this._padding;
244
245         if (calculateSelectedControlPoint) {
246             this._mouseDownPosition = point;
247
248             if (this._inControl.point.distance(point) < this._outControl.point.distance(point))
249                 this._selectedControl = this._inControl;
250             else
251                 this._selectedControl = this._outControl;
252         }
253
254         if (event.shiftKey && this._mouseDownPosition) {
255             if (Math.abs(this._mouseDownPosition.x - point.x) > Math.abs(this._mouseDownPosition.y - point.y))
256                 point.y = this._mouseDownPosition.y;
257             else
258                 point.x = this._mouseDownPosition.x;
259         }
260
261         this._selectedControl.point = point;
262         this._selectedControl.handle.classList.add("selected");
263         this._updateValue();
264     }
265
266     _updateValue()
267     {
268         function round(num)
269         {
270             return Math.round(num * 100) / 100;
271         }
272
273         var inValueX = round(this._inControl.point.x / this._bezierWidth);
274         var inValueY = round(1 - (this._inControl.point.y / this._bezierHeight));
275
276         var outValueX = round(this._outControl.point.x / this._bezierWidth);
277         var outValueY = round(1 - (this._outControl.point.y / this._bezierHeight));
278
279         this._bezier = new WI.CubicBezier(inValueX, inValueY, outValueX, outValueY);
280         this._updateBezier();
281
282         this.dispatchEventToListeners(WI.BezierEditor.Event.BezierChanged, {bezier: this._bezier});
283     }
284
285     _updateBezier()
286     {
287         var r = this._controlHandleRadius;
288         var inControlX = this._inControl.point.x + r;
289         var inControlY = this._inControl.point.y + r;
290         var outControlX = this._outControl.point.x + r;
291         var outControlY = this._outControl.point.y + r;
292         var path = `M ${r} ${this._bezierHeight + r} C ${inControlX} ${inControlY} ${outControlX} ${outControlY} ${this._bezierWidth + r} ${r}`;
293         this._bezierCurve.setAttribute("d", path);
294         this._updateControl(this._inControl);
295         this._updateControl(this._outControl);
296
297         this._bezierInXInput.value = this._bezier.inPoint.x;
298         this._bezierInYInput.value = this._bezier.inPoint.y;
299         this._bezierOutXInput.value = this._bezier.outPoint.x;
300         this._bezierOutYInput.value = this._bezier.outPoint.y;
301     }
302
303     _updateControl(control)
304     {
305         control.handle.setAttribute("cx", control.point.x + this._controlHandleRadius);
306         control.handle.setAttribute("cy", control.point.y + this._controlHandleRadius);
307
308         control.line.setAttribute("x2", control.point.x + this._controlHandleRadius);
309         control.line.setAttribute("y2", control.point.y + this._controlHandleRadius);
310     }
311
312     _updateBezierPreview()
313     {
314         this._inControl.point = new WI.Point(this._bezier.inPoint.x * this._bezierWidth, (1 - this._bezier.inPoint.y) * this._bezierHeight);
315         this._outControl.point = new WI.Point(this._bezier.outPoint.x * this._bezierWidth, (1 - this._bezier.outPoint.y) * this._bezierHeight);
316
317         this._updateBezier();
318         this._triggerPreviewAnimation();
319     }
320
321     _triggerPreviewAnimation()
322     {
323         this._bezierPreview.style.animationTimingFunction = this._bezier.toString();
324         this._bezierPreviewContainer.classList.add("animate");
325         this._bezierPreviewTiming.classList.add("animate");
326     }
327
328     _resetPreviewAnimation()
329     {
330         var parent = this._bezierPreview.parentNode;
331         parent.removeChild(this._bezierPreview);
332         parent.appendChild(this._bezierPreview);
333
334         this._element.removeChild(this._bezierPreviewTiming);
335         this._element.appendChild(this._bezierPreviewTiming);
336     }
337
338     _handleNumberInputInput(event)
339     {
340         this._changeBezierForInput(event.target, event.target.value);
341     }
342
343     _handleNumberInputKeydown(event)
344     {
345         let shift = 0;
346         if (event.keyIdentifier === "Up")
347             shift = 0.01;
348          else if (event.keyIdentifier === "Down")
349             shift = -0.01;
350
351         if (!shift)
352             return;
353
354         if (event.shiftKey)
355             shift *= 10;
356
357         event.preventDefault();
358         this._changeBezierForInput(event.target, parseFloat(event.target.value) + shift);
359     }
360
361     _changeBezierForInput(target, value)
362     {
363         value = Math.round(value * 100) / 100;
364
365         switch (target) {
366         case this._bezierInXInput:
367             this._bezier.inPoint.x = Number.constrain(value, 0, 1);
368             break;
369         case this._bezierInYInput:
370             this._bezier.inPoint.y = value;
371             break;
372         case this._bezierOutXInput:
373             this._bezier.outPoint.x = Number.constrain(value, 0, 1);
374             break;
375         case this._bezierOutYInput:
376             this._bezier.outPoint.y = value;
377             break;
378         default:
379             return;
380         }
381
382         this._updateBezierPreview();
383
384         this.dispatchEventToListeners(WI.BezierEditor.Event.BezierChanged, {bezier: this._bezier});
385     }
386 };
387
388 WI.BezierEditor.Event = {
389     BezierChanged: "bezier-editor-bezier-changed"
390 };