| /* | 
 |  * Copyright (C) 2015 Apple Inc. All rights reserved. | 
 |  * | 
 |  * Redistribution and use in source and binary forms, with or without | 
 |  * modification, are permitted provided that the following conditions | 
 |  * are met: | 
 |  * 1. Redistributions of source code must retain the above copyright | 
 |  *    notice, this list of conditions and the following disclaimer. | 
 |  * 2. Redistributions in binary form must reproduce the above copyright | 
 |  *    notice, this list of conditions and the following disclaimer in the | 
 |  *    documentation and/or other materials provided with the distribution. | 
 |  * | 
 |  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' | 
 |  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, | 
 |  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR | 
 |  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS | 
 |  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR | 
 |  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF | 
 |  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS | 
 |  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN | 
 |  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) | 
 |  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF | 
 |  * THE POSSIBILITY OF SUCH DAMAGE. | 
 |  */ | 
 |  | 
 | WI.BezierEditor = class BezierEditor extends WI.Object | 
 | { | 
 |     constructor() | 
 |     { | 
 |         super(); | 
 |  | 
 |         this._element = document.createElement("div"); | 
 |         this._element.classList.add("bezier-editor"); | 
 |         this._element.dir = "ltr"; | 
 |  | 
 |         var editorWidth = 184; | 
 |         var editorHeight = 200; | 
 |         this._padding = 25; | 
 |         this._controlHandleRadius = 7; | 
 |         this._bezierWidth = editorWidth - (this._controlHandleRadius * 2); | 
 |         this._bezierHeight = editorHeight - (this._controlHandleRadius * 2) - (this._padding * 2); | 
 |  | 
 |         this._bezierPreviewContainer = this._element.createChild("div", "bezier-preview"); | 
 |         this._bezierPreviewContainer.title = WI.UIString("Restart animation"); | 
 |         this._bezierPreviewContainer.addEventListener("mousedown", this._resetPreviewAnimation.bind(this)); | 
 |  | 
 |         this._bezierPreview = this._bezierPreviewContainer.createChild("div"); | 
 |  | 
 |         this._bezierPreviewTiming = this._element.createChild("div", "bezier-preview-timing"); | 
 |  | 
 |         this._bezierContainer = this._element.appendChild(createSVGElement("svg")); | 
 |         this._bezierContainer.setAttribute("width", editorWidth); | 
 |         this._bezierContainer.setAttribute("height", editorHeight); | 
 |         this._bezierContainer.classList.add("bezier-container"); | 
 |  | 
 |         let svgGroup = this._bezierContainer.appendChild(createSVGElement("g")); | 
 |         svgGroup.setAttribute("transform", "translate(0, " + this._padding + ")"); | 
 |  | 
 |         let linearCurve = svgGroup.appendChild(createSVGElement("line")); | 
 |         linearCurve.classList.add("linear-curve"); | 
 |         linearCurve.setAttribute("x1", this._controlHandleRadius); | 
 |         linearCurve.setAttribute("y1", this._bezierHeight + this._controlHandleRadius); | 
 |         linearCurve.setAttribute("x2", this._bezierWidth + this._controlHandleRadius); | 
 |         linearCurve.setAttribute("y2", this._controlHandleRadius); | 
 |  | 
 |         this._bezierCurve = svgGroup.appendChild(createSVGElement("path")); | 
 |         this._bezierCurve.classList.add("bezier-curve"); | 
 |  | 
 |         function createControl(x1, y1) | 
 |         { | 
 |             x1 += this._controlHandleRadius; | 
 |             y1 += this._controlHandleRadius; | 
 |  | 
 |             let line = svgGroup.appendChild(createSVGElement("line")); | 
 |             line.classList.add("control-line"); | 
 |             line.setAttribute("x1", x1); | 
 |             line.setAttribute("y1", y1); | 
 |             line.setAttribute("x2", x1); | 
 |             line.setAttribute("y2", y1); | 
 |  | 
 |             let handle = svgGroup.appendChild(createSVGElement("circle")); | 
 |             handle.classList.add("control-handle"); | 
 |  | 
 |             return {point: null, line, handle}; | 
 |         } | 
 |  | 
 |         this._inControl = createControl.call(this, 0, this._bezierHeight); | 
 |         this._outControl = createControl.call(this, this._bezierWidth, 0); | 
 |  | 
 |         this._numberInputContainer = this._element.createChild("div", "number-input-container"); | 
 |  | 
 |         function createBezierInput(id, {min, max} = {}) | 
 |         { | 
 |             let key = "_bezier" + id + "Input"; | 
 |             this[key] = this._numberInputContainer.createChild("input"); | 
 |             this[key].type = "number"; | 
 |             this[key].step = 0.01; | 
 |  | 
 |             if (!isNaN(min)) | 
 |                 this[key].min = min; | 
 |  | 
 |             if (!isNaN(max)) | 
 |                 this[key].max = max; | 
 |  | 
 |             this[key].addEventListener("input", this._handleNumberInputInput.bind(this)); | 
 |             this[key].addEventListener("keydown", this._handleNumberInputKeydown.bind(this)); | 
 |         } | 
 |  | 
 |         createBezierInput.call(this, "InX", {min: 0, max: 1}); | 
 |         createBezierInput.call(this, "InY"); | 
 |         createBezierInput.call(this, "OutX", {min: 0, max: 1}); | 
 |         createBezierInput.call(this, "OutY"); | 
 |  | 
 |         this._selectedControl = null; | 
 |         this._mouseDownPosition = null; | 
 |         this._bezierContainer.addEventListener("mousedown", this); | 
 |  | 
 |         WI.addWindowKeydownListener(this); | 
 |     } | 
 |  | 
 |     // Public | 
 |  | 
 |     get element() | 
 |     { | 
 |         return this._element; | 
 |     } | 
 |  | 
 |     set bezier(bezier) | 
 |     { | 
 |         if (!bezier) | 
 |             return; | 
 |  | 
 |         var isCubicBezier = bezier instanceof WI.CubicBezier; | 
 |         console.assert(isCubicBezier); | 
 |         if (!isCubicBezier) | 
 |             return; | 
 |  | 
 |         this._bezier = bezier; | 
 |         this._updateBezierPreview(); | 
 |     } | 
 |  | 
 |     get bezier() | 
 |     { | 
 |         return this._bezier; | 
 |     } | 
 |  | 
 |     removeListeners() | 
 |     { | 
 |         WI.removeWindowKeydownListener(this); | 
 |     } | 
 |  | 
 |     // Protected | 
 |  | 
 |     handleEvent(event) | 
 |     { | 
 |         switch (event.type) { | 
 |         case "mousedown": | 
 |             this._handleMousedown(event); | 
 |             break; | 
 |         case "mousemove": | 
 |             this._handleMousemove(event); | 
 |             break; | 
 |         case "mouseup": | 
 |             this._handleMouseup(event); | 
 |             break; | 
 |         } | 
 |     } | 
 |  | 
 |     handleKeydownEvent(event) | 
 |     { | 
 |         if (!this._selectedControl || !this._element.parentNode) | 
 |             return false; | 
 |  | 
 |         let horizontal = 0; | 
 |         let vertical = 0; | 
 |         switch (event.keyCode) { | 
 |         case WI.KeyboardShortcut.Key.Up.keyCode: | 
 |             vertical = -1; | 
 |             break; | 
 |         case WI.KeyboardShortcut.Key.Right.keyCode: | 
 |             horizontal = 1; | 
 |             break; | 
 |         case WI.KeyboardShortcut.Key.Down.keyCode: | 
 |             vertical = 1; | 
 |             break; | 
 |         case WI.KeyboardShortcut.Key.Left.keyCode: | 
 |             horizontal = -1; | 
 |             break; | 
 |         default: | 
 |             return false; | 
 |         } | 
 |  | 
 |         if (event.shiftKey) { | 
 |             horizontal *= 10; | 
 |             vertical *= 10; | 
 |         } | 
 |  | 
 |         vertical *= this._bezierWidth / 100; | 
 |         horizontal *= this._bezierHeight / 100; | 
 |  | 
 |         this._selectedControl.point.x = Number.constrain(this._selectedControl.point.x + horizontal, 0, this._bezierWidth); | 
 |         this._selectedControl.point.y += vertical; | 
 |         this._updateControl(this._selectedControl); | 
 |         this._updateValue(); | 
 |  | 
 |         return true; | 
 |     } | 
 |  | 
 |     // Private | 
 |  | 
 |     _handleMousedown(event) | 
 |     { | 
 |         if (event.button !== 0) | 
 |             return; | 
 |  | 
 |         event.stop(); | 
 |         window.addEventListener("mousemove", this, true); | 
 |         window.addEventListener("mouseup", this, true); | 
 |  | 
 |         this._bezierPreviewContainer.classList.remove("animate"); | 
 |         this._bezierPreviewTiming.classList.remove("animate"); | 
 |  | 
 |         this._updateControlPointsForMouseEvent(event, true); | 
 |     } | 
 |  | 
 |     _handleMousemove(event) | 
 |     { | 
 |         this._updateControlPointsForMouseEvent(event); | 
 |     } | 
 |  | 
 |     _handleMouseup(event) | 
 |     { | 
 |         this._selectedControl.handle.classList.remove("selected"); | 
 |         this._mouseDownPosition = null; | 
 |         this._triggerPreviewAnimation(); | 
 |  | 
 |         window.removeEventListener("mousemove", this, true); | 
 |         window.removeEventListener("mouseup", this, true); | 
 |     } | 
 |  | 
 |     _updateControlPointsForMouseEvent(event, calculateSelectedControlPoint) | 
 |     { | 
 |         var point = WI.Point.fromEventInElement(event, this._bezierContainer); | 
 |         point.x = Number.constrain(point.x - this._controlHandleRadius, 0, this._bezierWidth); | 
 |         point.y -= this._controlHandleRadius + this._padding; | 
 |  | 
 |         if (calculateSelectedControlPoint) { | 
 |             this._mouseDownPosition = point; | 
 |  | 
 |             if (this._inControl.point.distance(point) < this._outControl.point.distance(point)) | 
 |                 this._selectedControl = this._inControl; | 
 |             else | 
 |                 this._selectedControl = this._outControl; | 
 |         } | 
 |  | 
 |         if (event.shiftKey && this._mouseDownPosition) { | 
 |             if (Math.abs(this._mouseDownPosition.x - point.x) > Math.abs(this._mouseDownPosition.y - point.y)) | 
 |                 point.y = this._mouseDownPosition.y; | 
 |             else | 
 |                 point.x = this._mouseDownPosition.x; | 
 |         } | 
 |  | 
 |         this._selectedControl.point = point; | 
 |         this._selectedControl.handle.classList.add("selected"); | 
 |         this._updateValue(); | 
 |     } | 
 |  | 
 |     _updateValue() | 
 |     { | 
 |         function round(num) | 
 |         { | 
 |             return Math.round(num * 100) / 100; | 
 |         } | 
 |  | 
 |         var inValueX = round(this._inControl.point.x / this._bezierWidth); | 
 |         var inValueY = round(1 - (this._inControl.point.y / this._bezierHeight)); | 
 |  | 
 |         var outValueX = round(this._outControl.point.x / this._bezierWidth); | 
 |         var outValueY = round(1 - (this._outControl.point.y / this._bezierHeight)); | 
 |  | 
 |         this._bezier = new WI.CubicBezier(inValueX, inValueY, outValueX, outValueY); | 
 |         this._updateBezier(); | 
 |  | 
 |         this.dispatchEventToListeners(WI.BezierEditor.Event.BezierChanged, {bezier: this._bezier}); | 
 |     } | 
 |  | 
 |     _updateBezier() | 
 |     { | 
 |         var r = this._controlHandleRadius; | 
 |         var inControlX = this._inControl.point.x + r; | 
 |         var inControlY = this._inControl.point.y + r; | 
 |         var outControlX = this._outControl.point.x + r; | 
 |         var outControlY = this._outControl.point.y + r; | 
 |         var path = `M ${r} ${this._bezierHeight + r} C ${inControlX} ${inControlY} ${outControlX} ${outControlY} ${this._bezierWidth + r} ${r}`; | 
 |         this._bezierCurve.setAttribute("d", path); | 
 |         this._updateControl(this._inControl); | 
 |         this._updateControl(this._outControl); | 
 |  | 
 |         this._bezierInXInput.value = this._bezier.inPoint.x; | 
 |         this._bezierInYInput.value = this._bezier.inPoint.y; | 
 |         this._bezierOutXInput.value = this._bezier.outPoint.x; | 
 |         this._bezierOutYInput.value = this._bezier.outPoint.y; | 
 |     } | 
 |  | 
 |     _updateControl(control) | 
 |     { | 
 |         control.handle.setAttribute("cx", control.point.x + this._controlHandleRadius); | 
 |         control.handle.setAttribute("cy", control.point.y + this._controlHandleRadius); | 
 |  | 
 |         control.line.setAttribute("x2", control.point.x + this._controlHandleRadius); | 
 |         control.line.setAttribute("y2", control.point.y + this._controlHandleRadius); | 
 |     } | 
 |  | 
 |     _updateBezierPreview() | 
 |     { | 
 |         this._inControl.point = new WI.Point(this._bezier.inPoint.x * this._bezierWidth, (1 - this._bezier.inPoint.y) * this._bezierHeight); | 
 |         this._outControl.point = new WI.Point(this._bezier.outPoint.x * this._bezierWidth, (1 - this._bezier.outPoint.y) * this._bezierHeight); | 
 |  | 
 |         this._updateBezier(); | 
 |         this._triggerPreviewAnimation(); | 
 |     } | 
 |  | 
 |     _triggerPreviewAnimation() | 
 |     { | 
 |         this._bezierPreview.style.animationTimingFunction = this._bezier.toString(); | 
 |         this._bezierPreviewContainer.classList.add("animate"); | 
 |         this._bezierPreviewTiming.classList.add("animate"); | 
 |     } | 
 |  | 
 |     _resetPreviewAnimation() | 
 |     { | 
 |         var parent = this._bezierPreview.parentNode; | 
 |         parent.removeChild(this._bezierPreview); | 
 |         parent.appendChild(this._bezierPreview); | 
 |  | 
 |         this._element.removeChild(this._bezierPreviewTiming); | 
 |         this._element.appendChild(this._bezierPreviewTiming); | 
 |     } | 
 |  | 
 |     _handleNumberInputInput(event) | 
 |     { | 
 |         this._changeBezierForInput(event.target, event.target.value); | 
 |     } | 
 |  | 
 |     _handleNumberInputKeydown(event) | 
 |     { | 
 |         let shift = 0; | 
 |         if (event.keyIdentifier === "Up") | 
 |             shift = 0.01; | 
 |          else if (event.keyIdentifier === "Down") | 
 |             shift = -0.01; | 
 |  | 
 |         if (!shift) | 
 |             return; | 
 |  | 
 |         if (event.shiftKey) | 
 |             shift *= 10; | 
 |  | 
 |         event.preventDefault(); | 
 |         this._changeBezierForInput(event.target, parseFloat(event.target.value) + shift); | 
 |     } | 
 |  | 
 |     _changeBezierForInput(target, value) | 
 |     { | 
 |         value = Math.round(value * 100) / 100; | 
 |  | 
 |         switch (target) { | 
 |         case this._bezierInXInput: | 
 |             this._bezier.inPoint.x = Number.constrain(value, 0, 1); | 
 |             break; | 
 |         case this._bezierInYInput: | 
 |             this._bezier.inPoint.y = value; | 
 |             break; | 
 |         case this._bezierOutXInput: | 
 |             this._bezier.outPoint.x = Number.constrain(value, 0, 1); | 
 |             break; | 
 |         case this._bezierOutYInput: | 
 |             this._bezier.outPoint.y = value; | 
 |             break; | 
 |         default: | 
 |             return; | 
 |         } | 
 |  | 
 |         this._updateBezierPreview(); | 
 |  | 
 |         this.dispatchEventToListeners(WI.BezierEditor.Event.BezierChanged, {bezier: this._bezier}); | 
 |     } | 
 | }; | 
 |  | 
 | WI.BezierEditor.Event = { | 
 |     BezierChanged: "bezier-editor-bezier-changed" | 
 | }; |