react-envelope-graph
Version:
A drag-and-drop-enabled, responsive, envelope graph that allows to shape a wave with attack, decay, sustain and release
655 lines (578 loc) • 21.2 kB
JavaScript
'use strict';
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var React = _interopDefault(require('react'));
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
}
function _defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
function _createClass(Constructor, protoProps, staticProps) {
if (protoProps) _defineProperties(Constructor.prototype, protoProps);
if (staticProps) _defineProperties(Constructor, staticProps);
return Constructor;
}
function _inherits(subClass, superClass) {
if (typeof superClass !== "function" && superClass !== null) {
throw new TypeError("Super expression must either be null or a function");
}
subClass.prototype = Object.create(superClass && superClass.prototype, {
constructor: {
value: subClass,
writable: true,
configurable: true
}
});
if (superClass) _setPrototypeOf(subClass, superClass);
}
function _getPrototypeOf(o) {
_getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
return o.__proto__ || Object.getPrototypeOf(o);
};
return _getPrototypeOf(o);
}
function _setPrototypeOf(o, p) {
_setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
o.__proto__ = p;
return o;
};
return _setPrototypeOf(o, p);
}
function _assertThisInitialized(self) {
if (self === void 0) {
throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
}
return self;
}
function _possibleConstructorReturn(self, call) {
if (call && (typeof call === "object" || typeof call === "function")) {
return call;
}
return _assertThisInitialized(self);
}
function _slicedToArray(arr, i) {
return _arrayWithHoles(arr) || _iterableToArrayLimit(arr, i) || _nonIterableRest();
}
function _arrayWithHoles(arr) {
if (Array.isArray(arr)) return arr;
}
function _iterableToArrayLimit(arr, i) {
if (!(Symbol.iterator in Object(arr) || Object.prototype.toString.call(arr) === "[object Arguments]")) {
return;
}
var _arr = [];
var _n = true;
var _d = false;
var _e = undefined;
try {
for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) {
_arr.push(_s.value);
if (i && _arr.length === i) break;
}
} catch (err) {
_d = true;
_e = err;
} finally {
try {
if (!_n && _i["return"] != null) _i["return"]();
} finally {
if (_d) throw _e;
}
}
return _arr;
}
function _nonIterableRest() {
throw new TypeError("Invalid attempt to destructure non-iterable instance");
}
function createCommonjsModule(fn, module) {
return module = { exports: {} }, fn(module, module.exports), module.exports;
}
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
var ReactPropTypesSecret = 'SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED';
var ReactPropTypesSecret_1 = ReactPropTypesSecret;
function emptyFunction() {}
function emptyFunctionWithReset() {}
emptyFunctionWithReset.resetWarningCache = emptyFunction;
var factoryWithThrowingShims = function() {
function shim(props, propName, componentName, location, propFullName, secret) {
if (secret === ReactPropTypesSecret_1) {
// It is still safe when called from React.
return;
}
var err = new Error(
'Calling PropTypes validators directly is not supported by the `prop-types` package. ' +
'Use PropTypes.checkPropTypes() to call them. ' +
'Read more at http://fb.me/use-check-prop-types'
);
err.name = 'Invariant Violation';
throw err;
} shim.isRequired = shim;
function getShim() {
return shim;
} // Important!
// Keep this list in sync with production version in `./factoryWithTypeCheckers.js`.
var ReactPropTypes = {
array: shim,
bool: shim,
func: shim,
number: shim,
object: shim,
string: shim,
symbol: shim,
any: shim,
arrayOf: getShim,
element: shim,
elementType: shim,
instanceOf: getShim,
node: shim,
objectOf: getShim,
oneOf: getShim,
oneOfType: getShim,
shape: getShim,
exact: getShim,
checkPropTypes: emptyFunctionWithReset,
resetWarningCache: emptyFunction
};
ReactPropTypes.PropTypes = ReactPropTypes;
return ReactPropTypes;
};
var propTypes = createCommonjsModule(function (module) {
/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
{
// By explicitly using `prop-types` you are opting into new production behavior.
// http://fb.me/prop-types-in-prod
module.exports = factoryWithThrowingShims();
}
});
var styles = {
line: {
fill: "none",
stroke: "rgb(221, 226, 232)",
strokeWidth: "2"
},
dndBox: {
fill: "none",
stroke: "white",
strokeWidth: 0.1,
height: 0.75,
width: 0.75
},
dndBoxActive: {
fill: "none",
stroke: "white",
strokeWidth: 0.1
},
corners: {
strokeWidth: 0.25,
length: 1,
stroke: "white"
}
};
var viewBox = {
width: 100,
height: 20,
marginTop: 2 * styles.corners.strokeWidth + styles.dndBox.height / 2 + styles.dndBox.strokeWidth,
marginRight: 2 * styles.corners.strokeWidth + styles.dndBox.width / 2 + styles.dndBox.strokeWidth,
marginBottom: 2 * styles.corners.strokeWidth + styles.dndBox.height / 2 + styles.dndBox.strokeWidth,
marginLeft: 2 * styles.corners.strokeWidth + styles.dndBox.width / 2 + styles.dndBox.strokeWidth
};
var EnvelopeGraph =
/*#__PURE__*/
function (_React$Component) {
_inherits(EnvelopeGraph, _React$Component);
function EnvelopeGraph(props) {
var _this;
_classCallCheck(this, EnvelopeGraph);
_this = _possibleConstructorReturn(this, _getPrototypeOf(EnvelopeGraph).call(this, props));
_this.state = {};
if (props.ratio && typeof props.ratio.xa === "number" && typeof props.ratio.xd === "number" && typeof props.ratio.xr === "number") {
_this.state.ratio = props.ratio;
} else if (!props.ratio) {
_this.state.ratio = {
xa: 0.25,
xd: 0.25,
xr: 0.25
};
} else if (typeof props.ratio.xs === "number") {
throw new Error("Configuring ratio with parameter 'xs' is not supported.");
} else {
throw new Error("ratio needs to have values of type 'number': xa, xd, xr");
}
_this.state = Object.assign(_this.state, {
xa: props.defaultXa * viewBox.width * _this.state.ratio.xa,
xd: props.defaultXd * viewBox.width * _this.state.ratio.xd,
xr: props.defaultXr * viewBox.width * _this.state.ratio.xr,
// NOTE: Dragging attack in y direction is currently not implemented.
ya: props.defaultYa,
ys: props.defaultYs,
drag: null,
svgRatio: 0
});
_this.onWindowResize = _this.onWindowResize.bind(_assertThisInitialized(_this));
styles = Object.assign(styles, props.styles);
return _this;
}
_createClass(EnvelopeGraph, [{
key: "componentDidMount",
value: function componentDidMount() {
var _this2 = this;
window.addEventListener("resize", this.onWindowResize); // NOTE: We call this initially, to set the width and height values.
this.onWindowResize();
window.addEventListener("mouseup", function () {
return _this2.setState({
drag: null
});
});
}
}, {
key: "onWindowResize",
value: function onWindowResize() {
var _this$computeStyles = this.computeStyles(),
width = _this$computeStyles.width,
height = _this$computeStyles.height; // NOTE: As the svg preserves it's aspect ratio, we have to calculate only
// one value that accounts for both width and height ratios.
this.setState({
svgRatio: {
width: width / viewBox.width,
height: height / viewBox.height
}
});
}
}, {
key: "getPhaseLengths",
value: function getPhaseLengths() {
var _this$state = this.state,
xa = _this$state.xa,
xd = _this$state.xd,
xr = _this$state.xr; // NOTE: We're subtracting 1/4 of the width to reserve space for release.
var absoluteS = viewBox.width - xa - xd - 0.25 * viewBox.width;
return [xa, xd, absoluteS, xr];
}
/**
* Returns a string to be used as 'd' attribute on an svg path that resembles
* an envelope shape given its parameters
* @return {String}
*/
}, {
key: "generatePath",
value: function generatePath() {
var _this$state2 = this.state,
ys = _this$state2.ys,
ya = _this$state2.ya;
var _this$getPhaseLengths = this.getPhaseLengths(),
_this$getPhaseLengths2 = _slicedToArray(_this$getPhaseLengths, 4),
attackWidth = _this$getPhaseLengths2[0],
decayWidth = _this$getPhaseLengths2[1],
sustainWidth = _this$getPhaseLengths2[2],
releaseWidth = _this$getPhaseLengths2[3];
var strokes = [];
strokes.push("M 0 " + viewBox.height);
strokes.push(this.exponentialStrokeTo(attackWidth, -viewBox.height));
strokes.push(this.exponentialStrokeTo(decayWidth, viewBox.height * (1 - ys)));
strokes.push(this.linearStrokeTo(sustainWidth, 0));
strokes.push(this.exponentialStrokeTo(releaseWidth, viewBox.height * ys));
return strokes.join(" ");
}
/**
* Constructs a command for an svg path that resembles an exponential curve
* @param {Number} dx
* @param {Number} dy
* @return {String} command
*/
}, {
key: "exponentialStrokeTo",
value: function exponentialStrokeTo(dx, dy) {
return ["c", dx / 5, dy / 2, dx / 2, dy, dx, dy].join(" ");
}
/**
* Constructs a line command for an svg path
* @param {Number} dx
* @param {Number} dy
* @return {String} command
*/
}, {
key: "linearStrokeTo",
value: function linearStrokeTo(dx, dy) {
return "l ".concat(dx, " ").concat(dy);
}
}, {
key: "renderCorners",
value: function renderCorners() {
var marginTop = viewBox.marginTop,
marginRight = viewBox.marginRight,
marginBottom = viewBox.marginBottom,
marginLeft = viewBox.marginLeft;
var _styles$corners = styles.corners,
length = _styles$corners.length,
stroke = _styles$corners.stroke,
strokeWidth = _styles$corners.strokeWidth; // NOTE: We draw the paths clockwise.
return [React.createElement("path", {
key: "top-left-corner",
fill: "none",
stroke: stroke,
strokeWidth: strokeWidth,
d: "M ".concat(strokeWidth, ",").concat(strokeWidth + length, " V ").concat(strokeWidth, " H ").concat(strokeWidth + length)
}), React.createElement("path", {
key: "top-right-corner",
fill: "none",
stroke: stroke,
strokeWidth: strokeWidth,
d: "M ".concat(viewBox.width + marginLeft + marginRight - length - strokeWidth, ",").concat(strokeWidth, " H ").concat(viewBox.width + marginLeft + marginRight - strokeWidth, " V ").concat(strokeWidth + length)
}), React.createElement("path", {
key: "bottom-right-corner",
fill: "none",
stroke: stroke,
strokeWidth: strokeWidth,
d: "M ".concat(viewBox.width + marginLeft + marginRight - strokeWidth, ",").concat(viewBox.height + marginTop + marginBottom - strokeWidth - length, " V ").concat(viewBox.height + marginTop + marginBottom - strokeWidth, " H ").concat(viewBox.width + marginLeft + marginRight - length - strokeWidth)
}), React.createElement("path", {
key: "bottom-left-corner",
fill: "none",
stroke: stroke,
strokeWidth: strokeWidth,
d: "M ".concat(length + strokeWidth, ",").concat(viewBox.height + marginTop + marginBottom - strokeWidth, " H ").concat(strokeWidth, " V ").concat(viewBox.height + marginTop + marginBottom - length - strokeWidth)
})];
}
}, {
key: "render",
value: function render() {
var _this$props = this.props,
corners = _this$props.corners,
style = _this$props.style;
var marginTop = viewBox.marginTop,
marginRight = viewBox.marginRight,
marginBottom = viewBox.marginBottom,
marginLeft = viewBox.marginLeft;
var drag = this.state.drag;
var w = viewBox.width + marginLeft + marginRight;
var h = viewBox.height + marginTop + marginBottom;
var vb = "0 0 ".concat(w, " ").concat(h);
return React.createElement("svg", {
style: style,
onDragStart: function onDragStart() {
return false;
},
viewBox: vb,
ref: "box"
}, React.createElement("path", {
transform: "translate(".concat(marginLeft, ", ").concat(marginTop, ")"),
d: this.generatePath(),
style: Object.assign({}, styles.line),
vectorEffect: "non-scaling-stroke"
}), corners ? this.renderCorners() : null, this.renderDnDRect("attack"), this.renderDnDRect("decaysustain"), this.renderDnDRect("release"));
}
}, {
key: "renderDnDRect",
value: function renderDnDRect(type) {
var _this3 = this;
var _this$getPhaseLengths3 = this.getPhaseLengths(),
_this$getPhaseLengths4 = _slicedToArray(_this$getPhaseLengths3, 4),
attackWidth = _this$getPhaseLengths4[0],
decayWidth = _this$getPhaseLengths4[1],
sustainWidth = _this$getPhaseLengths4[2],
releaseWidth = _this$getPhaseLengths4[3];
var marginTop = viewBox.marginTop,
marginLeft = viewBox.marginLeft;
var _this$state3 = this.state,
ys = _this$state3.ys,
drag = _this$state3.drag;
var rHeight = styles.dndBox.height;
var rWidth = styles.dndBox.width;
var x, y;
if (type === "attack") {
x = marginLeft + attackWidth - rWidth / 2;
y = marginTop - rHeight / 2;
} else if (type === "decaysustain") {
x = marginLeft + attackWidth + decayWidth - rWidth / 2;
y = marginTop + viewBox.height * (1 - ys) - rHeight / 2;
} else if (type === "release") {
x = marginLeft + attackWidth + decayWidth + sustainWidth + releaseWidth - rWidth / 2;
y = marginTop + viewBox.height - rHeight / 2;
} else {
throw new Error("Invalid type for DnDRect");
}
return React.createElement("rect", {
onMouseDown: function onMouseDown() {
return _this3.setState({
drag: type
});
},
x: x,
y: y,
width: rWidth,
height: rHeight,
style: {
pointerEvents: "all",
fill: drag === type ? styles.dndBoxActive.fill : styles.dndBox.fill,
stroke: drag === type ? styles.dndBoxActive.stroke : styles.dndBox.stroke,
strokeWidth: styles.dndBox.strokeWidth
}
});
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps, prevState) {
var _this$state4 = this.state,
drag = _this$state4.drag,
ratio = _this$state4.ratio;
var _this$props2 = this.props,
defaultXa = _this$props2.defaultXa,
defaultXd = _this$props2.defaultXd,
defaultYs = _this$props2.defaultYs,
defaultXr = _this$props2.defaultXr;
if (prevState.drag !== drag) {
window.addEventListener("mousemove", this.moveDnDRect(drag));
}
if (prevProps.defaultXa !== defaultXa || prevProps.defaultXd !== defaultXd || prevProps.defaultYs !== defaultYs || prevProps.defaultXr !== defaultXr) {
Object.assign(this.state, {
xa: defaultXa * viewBox.width * ratio.xa,
xd: defaultXd * viewBox.width * ratio.xd,
ys: defaultYs,
xr: defaultXr * viewBox.width * ratio.xr
});
this.setState(this.state);
} else {
this.notifyChanges(prevState);
}
}
}, {
key: "notifyChanges",
value: function notifyChanges(prevState) {
var _this$state5 = this.state,
xa = _this$state5.xa,
ya = _this$state5.ya,
xd = _this$state5.xd,
ys = _this$state5.ys,
xr = _this$state5.xr,
ratio = _this$state5.ratio,
graph = _this$state5.graph;
var onChange = this.props.onChange;
if (prevState.xa !== xa || prevState.xd !== xd || prevState.ys !== ys || prevState.xr !== xr && onChange) {
var relationXa = xa / viewBox.width * 1 / ratio.xa;
var relationXd = xd / viewBox.width * 1 / ratio.xd;
var relationXr = xr / viewBox.width * 1 / ratio.xr;
onChange({
xa: relationXa,
ya: ya,
xd: relationXd,
ys: ys,
xr: relationXr
});
}
}
}, {
key: "computeStyles",
value: function computeStyles() {
var computedStyle = window.getComputedStyle(this.refs.box);
var styles = {};
["paddingTop", "paddingRight", "paddingBottom", "paddingLeft", "height", "width"].map(function (key) {
return styles[key] = parseFloat(computedStyle[key]);
});
return styles;
}
}, {
key: "moveDnDRect",
value: function moveDnDRect(type) {
var _this4 = this;
return function (event) {
event.stopPropagation();
var _this4$getPhaseLength = _this4.getPhaseLengths(),
_this4$getPhaseLength2 = _slicedToArray(_this4$getPhaseLength, 4),
attackWidth = _this4$getPhaseLength2[0],
decayWidth = _this4$getPhaseLength2[1],
sustainWidth = _this4$getPhaseLength2[2],
releaseWidth = _this4$getPhaseLength2[3];
var _this4$computeStyles = _this4.computeStyles(),
paddingTop = _this4$computeStyles.paddingTop,
paddingRight = _this4$computeStyles.paddingRight,
paddingBottom = _this4$computeStyles.paddingBottom,
paddingLeft = _this4$computeStyles.paddingLeft;
var marginLeft = viewBox.marginLeft;
var _this4$state = _this4.state,
drag = _this4$state.drag,
xa = _this4$state.xa,
xd = _this4$state.xd,
xr = _this4$state.xr,
ratio = _this4$state.ratio,
svgRatio = _this4$state.svgRatio;
var styles = _this4.props.styles;
if (drag === type) {
var rect = _this4.refs.box.getBoundingClientRect();
if (type === "attack") {
var xaNew = (event.clientX - paddingLeft - rect.left) / svgRatio.width - marginLeft;
var newState = {};
if (xaNew <= ratio.xa * viewBox.width && xaNew >= 0) {
newState.xa = xaNew;
}
_this4.setState(newState);
} else if (type === "decaysustain") {
// NOTE: ys is defined as a percentage and not as an absolute value in
// user units.
var ysNew = 1 - (event.clientY - paddingTop - rect.top) / svgRatio.height / viewBox.height;
var _newState = {};
if (ysNew >= 0 && ysNew <= 1) {
_newState.ys = ysNew;
}
var xdNew = (event.clientX - paddingLeft - rect.left - attackWidth * svgRatio.width) / svgRatio.width;
if (xdNew >= 0 && xdNew <= ratio.xd * viewBox.width) {
_newState.xd = xdNew;
}
_this4.setState(_newState);
} else if (type == "release") {
var xrNew = (event.clientX - paddingLeft - rect.left - (attackWidth + decayWidth + sustainWidth) * svgRatio.width) / svgRatio.width;
if (xrNew >= 0 && xrNew <= ratio.xr * viewBox.width) {
_this4.setState({
xr: xrNew
});
}
}
}
};
}
}]);
return EnvelopeGraph;
}(React.Component);
EnvelopeGraph.propTypes = {
defaultXa: propTypes.number.isRequired,
defaultXd: propTypes.number.isRequired,
defaultXr: propTypes.number.isRequired,
defaultYa: propTypes.number.isRequired,
defaultYs: propTypes.number.isRequired,
ratio: propTypes.shape({
xa: propTypes.number,
xd: propTypes.number,
xr: propTypes.number
}),
dndBox: propTypes.shape({
height: propTypes.number,
width: propTypes.number
}),
onChange: propTypes.func,
style: propTypes.object,
styles: propTypes.object,
corners: propTypes.bool
};
EnvelopeGraph.defaultProps = {
corners: true,
// TODO: Remove when ya implemented.
defaultYa: 1
};
module.exports = EnvelopeGraph;
//# sourceMappingURL=react-envelope-graph.cjs.js.map