UNPKG

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
'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