/**
* 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(
"resource://devtools/shared/event-emitter.js");
const {
PREDEFINED,
PRESETS,
DEFAULT_PRESET_CATEGORY,
} = require(
"resource://devtools/client/shared/widgets/CubicBezierPresets.js");
const {
InspectorCSSParserWrapper,
} = require(
"resource://devtools/shared/css/lexer.js");
const XHTML_NS =
"http://www.w3.org/1999/xhtml";
/**
* 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 new Error(
"No offsets were defined");
}
this.coordinates = coordinates.map(n => +n);
for (let i = 4; i--; ) {
const xy =
this.coordinates[i];
if (isNaN(xy) || (!(i % 2) && (xy < 0 || xy > 1))) {
throw new Error(`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() {
// Check first if current coords are one of css predefined functions
const predefName = Object.keys(PREDEFINED).find(key =>
coordsAreEqual(PREDEFINED[key],
this.coordinates)
);
return predefName ||
"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");
const 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() {
const 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(element) {
const w =
this.canvas.width,
h =
this.canvas.height;
// Convert padding percentage to actual padding
const p =
this.padding.map((a, i) => a * (i % 2 ? w : h));
return [
(parseFloat(element.style.left) - p[3]) / (w + p[1] + p[3]),
(h - parseFloat(element.style.top) - p[2]) / (h - p[0] - p[2]),
];
},
/**
* Draw the cubic bezier curve for the current coordinates
*/
plot(settings = {}) {
const xy =
this.bezier.coordinates;
const win =
this.canvas.ownerGlobal;
const computedStyle = win.getComputedStyle(win.document.documentElement);
const defaultSettings = {
handleColor: computedStyle.getPropertyValue(
"--timing-function-control-point-background"
),
handleThickness: 0.008,
diagonalThickness: 0.01,
diagonalColor: computedStyle.getPropertyValue(
"--bezier-diagonal-color"),
bezierColor: computedStyle.getPropertyValue(
"--timing-function-line-color"
),
bezierThickness: 0.015,
drawHandles:
true,
};
for (
const setting in settings) {
defaultSettings[setting] = settings[setting];
}
// Clear the canvas –making sure to clear the
// whole area by resetting the transform first.
this.ctx.save();
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
this.ctx.clearRect(0, 0,
this.canvas.width,
this.canvas.height);
this.ctx.restore();
if (defaultSettings.drawHandles) {
// Draw control handles
this.ctx.beginPath();
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();
// Draw diagonal between points
this.ctx.beginPath();
this.ctx.lineWidth = defaultSettings.diagonalThickness;
this.ctx.strokeStyle = defaultSettings.diagonalColor;
this.ctx.moveTo(0, 0);
this.ctx.lineTo(1, 1);
this.ctx.stroke();
this.ctx.closePath();
}
// 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 = PRESETS[
"ease-in"][
"ease-in-sine"]
) {
EventEmitter.decorate(
this);
this.parent = parent;
const { curve, p1, p2 } =
this._initMarkup();
this.curveBoundingBox = curve.getBoundingClientRect();
this.curve = curve;
this.p1 = p1;
this.p2 = p2;
// Create and plot the bezier curve
this.bezierCanvas =
new BezierCanvas(
this.curve,
new CubicBezier(coordinates),
[0.3, 0]
);
this.bezierCanvas.plot();
// Place the control points
const 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._onNewCoordinates =
this._onNewCoordinates.bind(
this);
this.onPrefersReducedMotionChange =
this.onPrefersReducedMotionChange.bind(
this);
// Add preset preview menu
this.presets =
new CubicBezierPresetWidget(parent);
// Add the timing function previewer
// if prefers-reduced-motion is not set
this.reducedMotion = parent.ownerGlobal.matchMedia(
"(prefers-reduced-motion)"
);
if (!
this.reducedMotion.matches) {
this.timingPreview =
new TimingFunctionPreviewWidget(parent);
}
// add event listener to change prefers-reduced-motion
// of the timing function preview during runtime
this.reducedMotion.addEventListener(
"change",
this.onPrefersReducedMotionChange
);
this._initEvents();
}
exports.CubicBezierWidget = CubicBezierWidget;
CubicBezierWidget.prototype = {
_initMarkup() {
const doc =
this.parent.ownerDocument;
const wrap = doc.createElementNS(XHTML_NS,
"div");
wrap.className =
"display-wrap";
const plane = doc.createElementNS(XHTML_NS,
"div");
plane.className =
"coordinate-plane";
const p1 = doc.createElementNS(XHTML_NS,
"button");
p1.className =
"control-point";
plane.appendChild(p1);
const p2 = doc.createElementNS(XHTML_NS,
"button");
p2.className =
"control-point";
plane.appendChild(p2);
const curve = doc.createElementNS(XHTML_NS,
"canvas");
curve.className =
"curve";
const parentComputedStyle =
this.parent.ownerGlobal.getComputedStyle(
this.parent
);
// We need to set the canvas dimension to the actual rendered dimension
// to avoid the canvas to scale. We can retrie the CSS variable values
// and striping their unit.
const dimensionRegex = /(?<size>\d+)px$/;
curve.setAttribute(
"width",
dimensionRegex.exec(
parentComputedStyle.getPropertyValue(
"--bezier-curve-width")
).groups.size
);
curve.setAttribute(
"height",
dimensionRegex.exec(
parentComputedStyle.getPropertyValue(
"--bezier-curve-height")
).groups.size
);
plane.appendChild(curve);
wrap.appendChild(plane);
this.parent.appendChild(wrap);
return {
p1,
p2,
curve,
};
},
onPrefersReducedMotionChange(event) {
// if prefers-reduced-motion is enabled destroy timing function preview
// else create it if it does not exist
if (event.matches) {
if (
this.timingPreview) {
this.timingPreview.destroy();
}
this.timingPreview = undefined;
}
else if (!
this.timingPreview) {
this.timingPreview =
new TimingFunctionPreviewWidget(
this.parent);
}
},
_removeMarkup() {
this.parent.querySelector(
".display-wrap").remove();
},
_initEvents() {
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);
this.presets.on(
"new-coordinates",
this._onNewCoordinates);
},
_removeEvents() {
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);
this.presets.off(
"new-coordinates",
this._onNewCoordinates);
},
_onPointMouseDown(event) {
// Updating the boundingbox in case it has changed
this.curveBoundingBox =
this.curve.getBoundingClientRect();
const point = event.target;
const doc = point.ownerDocument;
const self =
this;
doc.onmousemove =
function drag(e) {
let x = e.pageX;
const y = e.pageY;
const left = self.curveBoundingBox.left;
const 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(event) {
const point = event.target;
const code = event.keyCode;
if (code >= 37 && code <= 40) {
event.preventDefault();
// Arrow keys pressed
const left = parseInt(point.style.left, 10);
const top = parseInt(point.style.top, 10);
const 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(event) {
this.curveBoundingBox =
this.curve.getBoundingClientRect();
const left =
this.curveBoundingBox.left;
const top =
this.curveBoundingBox.top;
const x = event.pageX - left;
const y = event.pageY - top;
// Find which point is closer
const distP1 = distance(
x,
y,
parseInt(
this.p1.style.left, 10),
parseInt(
this.p1.style.top, 10)
);
const distP2 = distance(
x,
y,
parseInt(
this.p2.style.left, 10),
parseInt(
this.p2.style.top, 10)
);
const point = distP1 < distP2 ?
this.p1 :
this.p2;
point.style.left = x +
"px";
point.style.top = y +
"px";
this._updateFromPoints();
},
_onNewCoordinates(coordinates) {
this.coordinates = coordinates;
},
/**
* Get the current point coordinates and redraw the curve to match
*/
_updateFromPoints() {
// 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.presets.refreshMenu(coordinates);
this._redraw(coordinates);
},
/**
* Redraw the curve
* @param {Array} coordinates The array of control point coordinates
*/
_redraw(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);
if (
this.timingPreview) {
this.timingPreview.preview(
this.bezierCanvas.bezier.toString());
}
},
/**
* Set new coordinates for the control points and redraw the curve
* @param {Array} coordinates
*/
set coordinates(coordinates) {
this._redraw(coordinates);
// Move the points
const 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
const coordinates = parseTimingFunction(value);
this.presets.refreshMenu(coordinates);
this.coordinates = coordinates;
},
destroy() {
this._removeEvents();
this._removeMarkup();
// remove prefers-reduced-motion event listener
this.reducedMotion.removeEventListener(
"change",
this.onPrefersReducedMotionChange
);
this.reducedMotion =
null;
if (
this.timingPreview) {
this.timingPreview.destroy();
this.timingPreview =
null;
}
this.presets.destroy();
this.curve =
this.p1 =
this.p2 =
null;
},
};
/**
* CubicBezierPreset widget.
* Builds a menu of presets from CubicBezierPresets
* @param {DOMNode} parent The container where the preset panel should be
* created
*
* Emits "new-coordinate" event along with the coordinates
* whenever a preset is selected.
*/
function CubicBezierPresetWidget(parent) {
this.parent = parent;
const { presetPane, presets, categories } =
this._initMarkup();
this.presetPane = presetPane;
this.presets = presets;
this.categories = categories;
this._activeCategory =
null;
this._activePresetList =
null;
this._activePreset =
null;
this._onCategoryClick =
this._onCategoryClick.bind(
this);
this._onPresetClick =
this._onPresetClick.bind(
this);
EventEmitter.decorate(
this);
this._initEvents();
}
exports.CubicBezierPresetWidget = CubicBezierPresetWidget;
CubicBezierPresetWidget.prototype = {
/*
* Constructs a list of all preset categories and a list
* of presets for each category.
*
* High level markup:
* div .preset-pane
* div .preset-categories
* div .category
* div .category
* ...
* div .preset-container
* div .presetList
* div .preset
* ...
* div .presetList
* div .preset
* ...
*/
_initMarkup() {
const doc =
this.parent.ownerDocument;
const presetPane = doc.createElementNS(XHTML_NS,
"div");
presetPane.className =
"preset-pane";
const categoryList = doc.createElementNS(XHTML_NS,
"div");
categoryList.id =
"preset-categories";
const presetContainer = doc.createElementNS(XHTML_NS,
"div");
presetContainer.id =
"preset-container";
Object.keys(PRESETS).forEach(categoryLabel => {
const category =
this._createCategory(categoryLabel);
categoryList.appendChild(category);
const presetList =
this._createPresetList(categoryLabel);
presetContainer.appendChild(presetList);
});
presetPane.appendChild(categoryList);
presetPane.appendChild(presetContainer);
this.parent.appendChild(presetPane);
const allCategories = presetPane.querySelectorAll(
".category");
const allPresets = presetPane.querySelectorAll(
".preset");
return {
presetPane,
presets: allPresets,
categories: allCategories,
};
},
_createCategory(categoryLabel) {
const doc =
this.parent.ownerDocument;
const category = doc.createElementNS(XHTML_NS,
"div");
category.id = categoryLabel;
category.classList.add(
"category");
const categoryDisplayLabel =
this._normalizeCategoryLabel(categoryLabel);
category.textContent = categoryDisplayLabel;
category.setAttribute(
"title", categoryDisplayLabel);
return category;
},
_normalizeCategoryLabel(categoryLabel) {
return categoryLabel.replace(
"/-/g",
" ");
},
_createPresetList(categoryLabel) {
const doc =
this.parent.ownerDocument;
const presetList = doc.createElementNS(XHTML_NS,
"div");
presetList.id =
"preset-category-" + categoryLabel;
presetList.classList.add(
"preset-list");
Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => {
const preset =
this._createPreset(categoryLabel, presetLabel);
presetList.appendChild(preset);
});
return presetList;
},
_createPreset(categoryLabel, presetLabel) {
const doc =
this.parent.ownerDocument;
const preset = doc.createElementNS(XHTML_NS,
"div");
preset.classList.add(
"preset");
preset.id = presetLabel;
preset.coordinates = PRESETS[categoryLabel][presetLabel];
// Create preset preview
const curve = doc.createElementNS(XHTML_NS,
"canvas");
const bezier =
new CubicBezier(preset.coordinates);
curve.setAttribute(
"height", 50);
curve.setAttribute(
"width", 50);
preset.bezierCanvas =
new BezierCanvas(curve, bezier, [0.15, 0]);
preset.bezierCanvas.plot({
drawHandles:
false,
bezierThickness: 0.025,
});
preset.appendChild(curve);
// Create preset label
const presetLabelElem = doc.createElementNS(XHTML_NS,
"p");
const presetDisplayLabel =
this._normalizePresetLabel(
categoryLabel,
presetLabel
);
presetLabelElem.textContent = presetDisplayLabel;
preset.appendChild(presetLabelElem);
preset.setAttribute(
"title", presetDisplayLabel);
return preset;
},
_normalizePresetLabel(categoryLabel, presetLabel) {
return presetLabel.replace(categoryLabel +
"-",
"").replace(
"/-/g",
" ");
},
_initEvents() {
for (
const category of
this.categories) {
category.addEventListener(
"click",
this._onCategoryClick);
}
for (
const preset of
this.presets) {
preset.addEventListener(
"click",
this._onPresetClick);
}
},
_removeEvents() {
for (
const category of
this.categories) {
category.removeEventListener(
"click",
this._onCategoryClick);
}
for (
const preset of
this.presets) {
preset.removeEventListener(
"click",
this._onPresetClick);
}
},
_onPresetClick(event) {
this.emit(
"new-coordinates", event.currentTarget.coordinates);
this.activePreset = event.currentTarget;
},
_onCategoryClick(event) {
this.activeCategory = event.target;
},
_setActivePresetList(presetListId) {
const presetList =
this.presetPane.querySelector(
"#" + presetListId);
swapClassName(
"active-preset-list",
this._activePresetList, presetList);
this._activePresetList = presetList;
},
set activeCategory(category) {
swapClassName(
"active-category",
this._activeCategory, category);
this._activeCategory = category;
this._setActivePresetList(
"preset-category-" + category.id);
},
get activeCategory() {
return this._activeCategory;
},
set activePreset(preset) {
swapClassName(
"active-preset",
this._activePreset, preset);
this._activePreset = preset;
},
get activePreset() {
return this._activePreset;
},
/**
* Called by CubicBezierWidget onload and when
* the curve is modified via the canvas.
* Attempts to match the new user setting with an
* existing preset.
* @param {Array} coordinates new coords [i, j, k, l]
*/
refreshMenu(coordinates) {
// If we cannot find a matching preset, keep
// menu on last known preset category.
let category =
this._activeCategory;
// If we cannot find a matching preset
// deselect any selected preset.
let preset =
null;
// If a category has never been viewed before
// show the default category.
if (!category) {
category =
this.parent.querySelector(
"#" + DEFAULT_PRESET_CATEGORY);
}
// If the new coordinates do match a preset,
// set its category and preset button as active.
Object.keys(PRESETS).forEach(categoryLabel => {
Object.keys(PRESETS[categoryLabel]).forEach(presetLabel => {
if (coordsAreEqual(PRESETS[categoryLabel][presetLabel], coordinates)) {
category =
this.parent.querySelector(
"#" + categoryLabel);
preset =
this.parent.querySelector(
"#" + presetLabel);
}
});
});
this.activeCategory = category;
this.activePreset = preset;
},
destroy() {
this._removeEvents();
this.parent.querySelector(
".preset-pane").remove();
},
};
/**
* 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.parent = parent;
this._initMarkup();
}
TimingFunctionPreviewWidget.prototype = {
PREVIEW_DURATION: 1000,
_initMarkup() {
const doc =
this.parent.ownerDocument;
const container = doc.createElementNS(XHTML_NS,
"div");
container.className =
"timing-function-preview";
this.dot = doc.createElementNS(XHTML_NS,
"div");
this.dot.className =
"dot";
container.appendChild(
this.dot);
const scale = doc.createElementNS(XHTML_NS,
"div");
scale.className =
"scale";
container.appendChild(scale);
this.parent.appendChild(container);
},
destroy() {
this.dot.getAnimations().forEach(anim => anim.cancel());
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(value) {
// Don't restart the preview animation if the value is the same
if (value ===
this.previousValue) {
return;
}
if (parseTimingFunction(value)) {
this.restartAnimation(value);
}
this.previousValue = value;
},
/**
* Re-start the preview animation from the beginning.
* @param {String} timingFunction The value for the timing-function.
*/
restartAnimation(timingFunction) {
// Cancel the previous animation if there was any.
this.dot.getAnimations().forEach(anim => anim.cancel());
// And start the new one.
// The animation consists of a few keyframes that move the dot to the right of the
// container, and then move it back to the left.
// It also contains some pause where the dot is greyed-out, before it moves to
// the right, and once again, before it comes back to the left.
// The timing function passed to this function is applied to the keyframes that
// actually move the dot. This way it can be previewed in both direction, instead of
// being spread over the whole animation.
const translateStart =
"calc(var(--bezier-curve-width) / -2)";
const translateEnd =
"calc(var(--bezier-curve-width) / 2)";
const grayscaleFilter =
"grayscale(100%)";
this.dot.animate(
[
{ translate: translateStart, filter: grayscaleFilter, offset: 0 },
{
translate: translateStart,
filter: grayscaleFilter,
offset: 0.19,
},
{
translate: translateStart,
filter:
"none",
offset: 0.2,
easing: timingFunction,
},
{ translate: translateEnd, filter:
"none", offset: 0.5 },
{ translate: translateEnd, filter: grayscaleFilter, offset: 0.51 },
{ translate: translateEnd, filter: grayscaleFilter, offset: 0.7 },
{
translate: translateEnd,
filter:
"none",
offset: 0.71,
easing: timingFunction,
},
{ translate: translateStart, filter:
"none", offset: 1 },
],
{
duration:
this.PREVIEW_DURATION * 2,
iterations: Infinity,
}
);
},
};
// Helpers
function getPadding(padding) {
const 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));
}
/**
* Parse a string to see whether it is a valid timing function.
* If it is, return the coordinates as an array.
* Otherwise, return undefined.
* @param {String} value
* @return {Array} of coordinates, or undefined
*/
function parseTimingFunction(value) {
if (value in PREDEFINED) {
return PREDEFINED[value];
}
const tokenStream =
new InspectorCSSParserWrapper(value);
const getNextToken = () => {
while (
true) {
const token = tokenStream.nextToken();
if (
!token ||
(token.tokenType !==
"WhiteSpace" && token.tokenType !==
"Comment")
) {
return token;
}
}
};
let token = getNextToken();
if (token.tokenType !==
"Function" || token.value !==
"cubic-bezier") {
return undefined;
}
const result = [];
for (let i = 0; i < 4; ++i) {
token = getNextToken();
if (!token || token.tokenType !==
"Number") {
return undefined;
}
result.push(token.number);
token = getNextToken();
if (!token || token.tokenType !== (i == 3 ?
"CloseParenthesis" :
"Comma")) {
return undefined;
}
}
return result;
}
exports.parseTimingFunction = parseTimingFunction;
/**
* Removes a class from a node and adds it to another.
* @param {String} className the class to swap
* @param {DOMNode} from the node to remove the class from
* @param {DOMNode} to the node to add the class to
*/
function swapClassName(className, from, to) {
if (from !==
null) {
from.classList.remove(className);
}
if (to !==
null) {
to.classList.add(className);
}
}
/**
* Compares two arrays of coordinates [i, j, k, l]
* @param {Array} c1 first coordinate array to compare
* @param {Array} c2 second coordinate array to compare
* @return {Boolean}
*/
function coordsAreEqual(c1, c2) {
return c1.reduce((prev, curr, index) => prev && curr === c2[index],
true);
}