summaryrefslogtreecommitdiff
path: root/toolkit/devtools/shared/widgets/CubicBezierWidget.js
diff options
context:
space:
mode:
authorMatt A. Tobin <email@mattatobin.com>2016-10-16 19:34:53 -0400
committerMatt A. Tobin <email@mattatobin.com>2016-10-16 19:34:53 -0400
commit81805ce3f63e2e4a799bd54f174083c58a9b5640 (patch)
tree6e13374b213ac9b2ae74c25d8aac875faf71fdd0 /toolkit/devtools/shared/widgets/CubicBezierWidget.js
parent28c8da71bf521bb3ee76f27b8a241919e24b7cd5 (diff)
downloadpalemoon-gre-81805ce3f63e2e4a799bd54f174083c58a9b5640.tar.gz
Move Mozilla DevTools to Platform - Part 3: Merge the browser/devtools and toolkit/devtools adjusting for directory collisions
Diffstat (limited to 'toolkit/devtools/shared/widgets/CubicBezierWidget.js')
-rw-r--r--toolkit/devtools/shared/widgets/CubicBezierWidget.js556
1 files changed, 556 insertions, 0 deletions
diff --git a/toolkit/devtools/shared/widgets/CubicBezierWidget.js b/toolkit/devtools/shared/widgets/CubicBezierWidget.js
new file mode 100644
index 000000000..9177253b8
--- /dev/null
+++ b/toolkit/devtools/shared/widgets/CubicBezierWidget.js
@@ -0,0 +1,556 @@
+/**
+ * Copyright (c) 2013 Lea Verou. All rights reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+// Based on www.cubic-bezier.com by Lea Verou
+// See https://github.com/LeaVerou/cubic-bezier
+
+"use strict";
+
+const EventEmitter = require("devtools/toolkit/event-emitter");
+const {setTimeout, clearTimeout} = require("sdk/timers");
+
+const PREDEFINED = exports.PREDEFINED = {
+ "ease": [.25, .1, .25, 1],
+ "linear": [0, 0, 1, 1],
+ "ease-in": [.42, 0, 1, 1],
+ "ease-out": [0, 0, .58, 1],
+ "ease-in-out": [.42, 0, .58, 1]
+};
+
+/**
+ * CubicBezier data structure helper
+ * Accepts an array of coordinates and exposes a few useful getters
+ * @param {Array} coordinates i.e. [.42, 0, .58, 1]
+ */
+function CubicBezier(coordinates) {
+ if (!coordinates) {
+ throw "No offsets were defined";
+ }
+
+ this.coordinates = coordinates.map(n => +n);
+
+ for (let i = 4; i--;) {
+ let xy = this.coordinates[i];
+ if (isNaN(xy) || (!(i%2) && (xy < 0 || xy > 1))) {
+ throw "Wrong coordinate at " + i + "(" + xy + ")";
+ }
+ }
+
+ this.coordinates.toString = function() {
+ return this.map(n => {
+ return (Math.round(n * 100)/100 + '').replace(/^0\./, '.');
+ }) + "";
+ }
+}
+
+exports.CubicBezier = CubicBezier;
+
+CubicBezier.prototype = {
+ get P1() {
+ return this.coordinates.slice(0, 2);
+ },
+
+ get P2() {
+ return this.coordinates.slice(2);
+ },
+
+ toString: function() {
+ return 'cubic-bezier(' + this.coordinates + ')';
+ }
+};
+
+/**
+ * Bezier curve canvas plotting class
+ * @param {DOMNode} canvas
+ * @param {CubicBezier} bezier
+ * @param {Array} padding Amount of horizontal,vertical padding around the graph
+ */
+function BezierCanvas(canvas, bezier, padding) {
+ this.canvas = canvas;
+ this.bezier = bezier;
+ this.padding = getPadding(padding);
+
+ // Convert to a cartesian coordinate system with axes from 0 to 1
+ this.ctx = this.canvas.getContext('2d');
+ let p = this.padding;
+
+ this.ctx.scale(canvas.width * (1 - p[1] - p[3]),
+ -canvas.height * (1 - p[0] - p[2]));
+ this.ctx.translate(p[3] / (1 - p[1] - p[3]),
+ -1 - p[0] / (1 - p[0] - p[2]));
+};
+
+exports.BezierCanvas = BezierCanvas;
+
+BezierCanvas.prototype = {
+ /**
+ * Get P1 and P2 current top/left offsets so they can be positioned
+ * @return {Array} Returns an array of 2 {top:String,left:String} objects
+ */
+ get offsets() {
+ let p = this.padding, w = this.canvas.width, h = this.canvas.height;
+
+ return [{
+ left: w * (this.bezier.coordinates[0] * (1 - p[3] - p[1]) - p[3]) + 'px',
+ top: h * (1 - this.bezier.coordinates[1] * (1 - p[0] - p[2]) - p[0]) + 'px'
+ }, {
+ left: w * (this.bezier.coordinates[2] * (1 - p[3] - p[1]) - p[3]) + 'px',
+ top: h * (1 - this.bezier.coordinates[3] * (1 - p[0] - p[2]) - p[0]) + 'px'
+ }]
+ },
+
+ /**
+ * Convert an element's left/top offsets into coordinates
+ */
+ offsetsToCoordinates: function(element) {
+ let p = this.padding, w = this.canvas.width, h = this.canvas.height;
+
+ // Convert padding percentage to actual padding
+ p = p.map(function(a, i) { return a * (i % 2? w : h)});
+
+ return [
+ (parseInt(element.style.left) - p[3]) / (w + p[1] + p[3]),
+ (h - parseInt(element.style.top) - p[2]) / (h - p[0] - p[2])
+ ];
+ },
+
+ /**
+ * Draw the cubic bezier curve for the current coordinates
+ */
+ plot: function(settings={}) {
+ let xy = this.bezier.coordinates;
+
+ let defaultSettings = {
+ handleColor: '#666',
+ handleThickness: .008,
+ bezierColor: '#4C9ED9',
+ bezierThickness: .015
+ };
+
+ for (let setting in settings) {
+ defaultSettings[setting] = settings[setting];
+ }
+
+ this.ctx.clearRect(-.5,-.5, 2, 2);
+
+ // Draw control handles
+ this.ctx.beginPath();
+ this.ctx.fillStyle = defaultSettings.handleColor;
+ this.ctx.lineWidth = defaultSettings.handleThickness;
+ this.ctx.strokeStyle = defaultSettings.handleColor;
+
+ this.ctx.moveTo(0, 0);
+ this.ctx.lineTo(xy[0], xy[1]);
+ this.ctx.moveTo(1,1);
+ this.ctx.lineTo(xy[2], xy[3]);
+
+ this.ctx.stroke();
+ this.ctx.closePath();
+
+ function circle(ctx, cx, cy, r) {
+ return ctx.beginPath();
+ ctx.arc(cx, cy, r, 0, 2*Math.PI, !1);
+ ctx.closePath();
+ }
+
+ circle(this.ctx, xy[0], xy[1], 1.5 * defaultSettings.handleThickness);
+ this.ctx.fill();
+ circle(this.ctx, xy[2], xy[3], 1.5 * defaultSettings.handleThickness);
+ this.ctx.fill();
+
+ // Draw bezier curve
+ this.ctx.beginPath();
+ this.ctx.lineWidth = defaultSettings.bezierThickness;
+ this.ctx.strokeStyle = defaultSettings.bezierColor;
+ this.ctx.moveTo(0,0);
+ this.ctx.bezierCurveTo(xy[0], xy[1], xy[2], xy[3], 1,1);
+ this.ctx.stroke();
+ this.ctx.closePath();
+ }
+};
+
+/**
+ * Cubic-bezier widget. Uses the BezierCanvas class to draw the curve and
+ * adds the control points and user interaction
+ * @param {DOMNode} parent The container where the graph should be created
+ * @param {Array} coordinates Coordinates of the curve to be drawn
+ *
+ * Emits "updated" events whenever the curve is changed. Along with the event is
+ * sent a CubicBezier object
+ */
+function CubicBezierWidget(parent, coordinates=PREDEFINED["ease-in-out"]) {
+ this.parent = parent;
+ let {curve, p1, p2} = this._initMarkup();
+
+ this.curve = curve;
+ this.curveBoundingBox = curve.getBoundingClientRect();
+ this.p1 = p1;
+ this.p2 = p2;
+
+ // Create and plot the bezier curve
+ this.bezierCanvas = new BezierCanvas(this.curve,
+ new CubicBezier(coordinates), [.25, 0]);
+ this.bezierCanvas.plot();
+
+ // Place the control points
+ let offsets = this.bezierCanvas.offsets;
+ this.p1.style.left = offsets[0].left;
+ this.p1.style.top = offsets[0].top;
+ this.p2.style.left = offsets[1].left;
+ this.p2.style.top = offsets[1].top;
+
+ this._onPointMouseDown = this._onPointMouseDown.bind(this);
+ this._onPointKeyDown = this._onPointKeyDown.bind(this);
+ this._onCurveClick = this._onCurveClick.bind(this);
+ this._initEvents();
+
+ // Add the timing function previewer
+ this.timingPreview = new TimingFunctionPreviewWidget(parent);
+
+ EventEmitter.decorate(this);
+}
+
+exports.CubicBezierWidget = CubicBezierWidget;
+
+CubicBezierWidget.prototype = {
+ _initMarkup: function() {
+ let doc = this.parent.ownerDocument;
+
+ let plane = doc.createElement("div");
+ plane.className = "coordinate-plane";
+
+ let p1 = doc.createElement("button");
+ p1.className = "control-point";
+ p1.id = "P1";
+ plane.appendChild(p1);
+
+ let p2 = doc.createElement("button");
+ p2.className = "control-point";
+ p2.id = "P2";
+ plane.appendChild(p2);
+
+ let curve = doc.createElement("canvas");
+ curve.setAttribute("height", "400");
+ curve.setAttribute("width", "200");
+ curve.id = "curve";
+ plane.appendChild(curve);
+
+ this.parent.appendChild(plane);
+
+ return {
+ p1: p1,
+ p2: p2,
+ curve: curve
+ }
+ },
+
+ _removeMarkup: function() {
+ this.parent.ownerDocument.querySelector(".coordinate-plane").remove();
+ },
+
+ _initEvents: function() {
+ this.p1.addEventListener("mousedown", this._onPointMouseDown);
+ this.p2.addEventListener("mousedown", this._onPointMouseDown);
+
+ this.p1.addEventListener("keydown", this._onPointKeyDown);
+ this.p2.addEventListener("keydown", this._onPointKeyDown);
+
+ this.curve.addEventListener("click", this._onCurveClick);
+ },
+
+ _removeEvents: function() {
+ this.p1.removeEventListener("mousedown", this._onPointMouseDown);
+ this.p2.removeEventListener("mousedown", this._onPointMouseDown);
+
+ this.p1.removeEventListener("keydown", this._onPointKeyDown);
+ this.p2.removeEventListener("keydown", this._onPointKeyDown);
+
+ this.curve.removeEventListener("click", this._onCurveClick);
+ },
+
+ _onPointMouseDown: function(event) {
+ // Updating the boundingbox in case it has changed
+ this.curveBoundingBox = this.curve.getBoundingClientRect();
+
+ let point = event.target;
+ let doc = point.ownerDocument;
+ let self = this;
+
+ doc.onmousemove = function drag(e) {
+ let x = e.pageX;
+ let y = e.pageY;
+ let left = self.curveBoundingBox.left;
+ let top = self.curveBoundingBox.top;
+
+ if (x === 0 && y == 0) {
+ return;
+ }
+
+ // Constrain x
+ x = Math.min(Math.max(left, x), left + self.curveBoundingBox.width);
+
+ point.style.left = x - left + "px";
+ point.style.top = y - top + "px";
+
+ self._updateFromPoints();
+ };
+
+ doc.onmouseup = function () {
+ point.focus();
+ doc.onmousemove = doc.onmouseup = null;
+ }
+ },
+
+ _onPointKeyDown: function(event) {
+ let point = event.target;
+ let code = event.keyCode;
+
+ if (code >= 37 && code <= 40) {
+ event.preventDefault();
+
+ // Arrow keys pressed
+ let left = parseInt(point.style.left);
+ let top = parseInt(point.style.top);
+ let offset = 3 * (event.shiftKey ? 10 : 1);
+
+ switch (code) {
+ case 37: point.style.left = left - offset + 'px'; break;
+ case 38: point.style.top = top - offset + 'px'; break;
+ case 39: point.style.left = left + offset + 'px'; break;
+ case 40: point.style.top = top + offset + 'px'; break;
+ }
+
+ this._updateFromPoints();
+ }
+ },
+
+ _onCurveClick: function(event) {
+ let left = this.curveBoundingBox.left;
+ let top = this.curveBoundingBox.top;
+ let x = event.pageX - left;
+ let y = event.pageY - top;
+
+ // Find which point is closer
+ let distP1 = distance(x, y,
+ parseInt(this.p1.style.left), parseInt(this.p1.style.top));
+ let distP2 = distance(x, y,
+ parseInt(this.p2.style.left), parseInt(this.p2.style.top));
+
+ let point = distP1 < distP2 ? this.p1 : this.p2;
+ point.style.left = x + "px";
+ point.style.top = y + "px";
+
+ this._updateFromPoints();
+ },
+
+ /**
+ * Get the current point coordinates and redraw the curve to match
+ */
+ _updateFromPoints: function() {
+ // Get the new coordinates from the point's offsets
+ let coordinates = this.bezierCanvas.offsetsToCoordinates(this.p1)
+ coordinates = coordinates.concat(this.bezierCanvas.offsetsToCoordinates(this.p2));
+
+ this._redraw(coordinates);
+ },
+
+ /**
+ * Redraw the curve
+ * @param {Array} coordinates The array of control point coordinates
+ */
+ _redraw: function(coordinates) {
+ // Provide a new CubicBezier to the canvas and plot the curve
+ this.bezierCanvas.bezier = new CubicBezier(coordinates);
+ this.bezierCanvas.plot();
+ this.emit("updated", this.bezierCanvas.bezier);
+
+ this.timingPreview.preview(this.bezierCanvas.bezier + "");
+ },
+
+ /**
+ * Set new coordinates for the control points and redraw the curve
+ * @param {Array} coordinates
+ */
+ set coordinates(coordinates) {
+ this._redraw(coordinates)
+
+ // Move the points
+ let offsets = this.bezierCanvas.offsets;
+ this.p1.style.left = offsets[0].left;
+ this.p1.style.top = offsets[0].top;
+ this.p2.style.left = offsets[1].left;
+ this.p2.style.top = offsets[1].top;
+ },
+
+ /**
+ * Set new coordinates for the control point and redraw the curve
+ * @param {String} value A string value. E.g. "linear", "cubic-bezier(0,0,1,1)"
+ */
+ set cssCubicBezierValue(value) {
+ if (!value) {
+ return;
+ }
+
+ value = value.trim();
+
+ // Try with one of the predefined values
+ let coordinates = PREDEFINED[value];
+
+ // Otherwise parse the coordinates from the cubic-bezier function
+ if (!coordinates && value.startsWith("cubic-bezier")) {
+ coordinates = value.replace(/cubic-bezier|\(|\)/g, "").split(",").map(parseFloat);
+ }
+
+ this.coordinates = coordinates;
+ },
+
+ destroy: function() {
+ this._removeEvents();
+ this._removeMarkup();
+
+ this.timingPreview.destroy();
+
+ this.curve = this.p1 = this.p2 = null;
+ }
+};
+
+/**
+ * The TimingFunctionPreviewWidget animates a dot on a scale with a given
+ * timing-function
+ * @param {DOMNode} parent The container where this widget should go
+ */
+function TimingFunctionPreviewWidget(parent) {
+ this.previousValue = null;
+ this.autoRestartAnimation = null;
+
+ this.parent = parent;
+ this._initMarkup();
+}
+
+TimingFunctionPreviewWidget.prototype = {
+ PREVIEW_DURATION: 1000,
+
+ _initMarkup: function() {
+ let doc = this.parent.ownerDocument;
+
+ let container = doc.createElement("div");
+ container.className = "timing-function-preview";
+
+ this.dot = doc.createElement("div");
+ this.dot.className = "dot";
+ container.appendChild(this.dot);
+
+ let scale = doc.createElement("div");
+ scale.className = "scale";
+ container.appendChild(scale);
+
+ this.parent.appendChild(container);
+ },
+
+ destroy: function() {
+ clearTimeout(this.autoRestartAnimation);
+ this.parent.querySelector(".timing-function-preview").remove();
+ this.parent = this.dot = null;
+ },
+
+ /**
+ * Preview a new timing function. The current preview will only be stopped if
+ * the supplied function value is different from the previous one. If the
+ * supplied function is invalid, the preview will stop.
+ * @param {String} value
+ */
+ preview: function(value) {
+ // Don't restart the preview animation if the value is the same
+ if (value === this.previousValue) {
+ return false;
+ }
+
+ clearTimeout(this.autoRestartAnimation);
+
+ if (isValidTimingFunction(value)) {
+ this.dot.style.animationTimingFunction = value;
+ this.restartAnimation();
+ }
+
+ this.previousValue = value;
+ },
+
+ /**
+ * Re-start the preview animation from the beginning
+ */
+ restartAnimation: function() {
+ // Reset the animation duration in case it was changed
+ this.dot.style.animationDuration = (this.PREVIEW_DURATION * 2) + "ms";
+
+ // Just toggling the class won't do it unless there's a sync reflow
+ this.dot.classList.remove("animate");
+ let w = this.dot.offsetWidth;
+ this.dot.classList.add("animate");
+
+ // Restart it again after a while
+ this.autoRestartAnimation = setTimeout(this.restartAnimation.bind(this),
+ this.PREVIEW_DURATION * 2);
+ }
+};
+
+// Helpers
+
+function getPadding(padding) {
+ let p = typeof padding === 'number'? [padding] : padding;
+
+ if (p.length === 1) {
+ p[1] = p[0];
+ }
+
+ if (p.length === 2) {
+ p[2] = p[0];
+ }
+
+ if (p.length === 3) {
+ p[3] = p[1];
+ }
+
+ return p;
+}
+
+function distance(x1, y1, x2, y2) {
+ return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2));
+}
+
+/**
+ * Checks whether a string is a valid timing-function value
+ * @param {String} value
+ * @return {Boolean}
+ */
+function isValidTimingFunction(value) {
+ // Either it's a predefined value
+ if (value in PREDEFINED) {
+ return true;
+ }
+
+ // Or it has to match a cubic-bezier expression
+ if (value.match(/^cubic-bezier\(([0-9.\- ]+,){3}[0-9.\- ]+\)/)) {
+ return true;
+ }
+
+ return false;
+}