gsap
Version:
GSAP is a framework-agnostic JavaScript animation library that turns developers into animation superheroes. Build high-performance animations that work in **every** major browser. Animate CSS, SVG, canvas, React, Vue, WebGL, colors, strings, motion paths,
1,631 lines (1,284 loc) • 54.9 kB
JavaScript
/*!
* PathEditor 3.13.0
* https://gsap.com
*
* Copyright 2008-2025, GreenSock. All rights reserved.
* Subject to the terms at https://gsap.com/standard-license
* @author: Jack Doyle, jack@greensock.com
*/
/* eslint-disable */
import { stringToRawPath, rawPathToString, bezierToPoints, simplifyPoints, pointsToSegment, subdivideSegment, getClosestData, copyRawPath, transformRawPath } from "./paths.js";
import { getGlobalMatrix, Matrix2D } from "./matrix.js";
var _numbersExp = /(?:(-)?\d*\.?\d*(?:e[\-+]?\d+)?)[0-9]/ig,
_doc,
_supportsPointer,
_win,
_body,
gsap,
_context,
_selectionColor = "#4e7fff",
_minimumMovement = 1,
_DEG2RAD = Math.PI / 180,
_getTime = Date.now || function () {
return new Date().getTime();
},
_lastInteraction = 0,
_isPressed = 0,
_emptyFunc = function _emptyFunc() {
return false;
},
_interacted = function _interacted() {
return _lastInteraction = _getTime();
},
_CTRL,
_ALT,
_SHIFT,
_CMD,
_recentlyAddedAnchor,
_editingAxis = {},
//stores the x/y of the most recently-selected anchor point's x and y axis. We tap into this for snapping horizontally and vertically.
_history = [],
_point = {},
//reuse to minimize memory and maximize performance (mostly for snapping)
_temp = [],
//reuse this in places like getNormalizedSVG() to conserve memory
_comma = ",",
_selectedPaths = [],
_preventDefault = function _preventDefault(event) {
if (event.preventDefault) {
event.preventDefault();
if (event.preventManipulation) {
event.preventManipulation(); //for some Microsoft browsers
}
}
},
_createElement = function _createElement(type) {
return _doc.createElementNS ? _doc.createElementNS("http://www.w3.org/1999/xhtml", type) : _doc.createElement(type);
},
_createSVG = function _createSVG(type, container, attributes) {
var element = _doc.createElementNS("http://www.w3.org/2000/svg", type),
reg = /([a-z])([A-Z])/g,
p;
attributes = attributes || {};
attributes["class"] = attributes["class"] || "path-editor";
for (p in attributes) {
if (element.style[p] !== undefined) {
element.style[p] = attributes[p];
} else {
element.setAttributeNS(null, p.replace(reg, "$1-$2").toLowerCase(), attributes[p]);
}
}
container.appendChild(element);
return element;
},
_identityMatrixObject = {
matrix: new Matrix2D()
},
_getConsolidatedMatrix = function _getConsolidatedMatrix(target) {
return (target.transform && target.transform.baseVal.consolidate() || _identityMatrixObject).matrix;
},
_getConcatenatedTransforms = function _getConcatenatedTransforms(target) {
var m = _getConsolidatedMatrix(target),
owner = target.ownerSVGElement;
while ((target = target.parentNode) && target.ownerSVGElement === owner) {
m.multiply(_getConsolidatedMatrix(target));
}
return "matrix(" + m.a + "," + m.b + "," + m.c + "," + m.d + "," + m.e + "," + m.f + ")";
},
_addHistory = function _addHistory(pathEditor) {
var selectedIndexes = [],
a = pathEditor._selectedAnchors,
i;
for (i = 0; i < a.length; i++) {
selectedIndexes[i] = a[i].i;
}
_history.unshift({
path: pathEditor,
d: pathEditor.path.getAttribute("d"),
transform: pathEditor.path.getAttribute("transform") || "",
selectedIndexes: selectedIndexes
});
if (_history.length > 30) {
_history.length = 30;
}
},
_round = function _round(value) {
return ~~(value * 1000 + (value < 0 ? -.5 : .5)) / 1000;
},
_getSquarePathData = function _getSquarePathData(size) {
size = _round(size);
return ["M-" + size, -size, size, -size, size, size, -size, size + "z"].join(_comma);
},
_getCirclePathData = function _getCirclePathData(size) {
var circ = 0.552284749831,
rcirc = _round(size * circ);
size = _round(size);
return "M" + size + ",0C" + [size, rcirc, rcirc, size, 0, size, -rcirc, size, -size, rcirc, -size, 0, -size, -rcirc, -rcirc, -size, 0, -size, rcirc, -size, size, -rcirc, size, 0].join(_comma) + "z";
},
_checkDeselect = function _checkDeselect(e) {
if (!e.target._gsSelection && !_isPressed && _getTime() - _lastInteraction > 100) {
var i = _selectedPaths.length;
while (--i > -1) {
_selectedPaths[i].deselect();
}
_selectedPaths.length = 0;
}
},
_tempDiv,
_touchEventLookup,
_isMultiTouching = 0,
_addListener = function _addListener(element, type, func, capture) {
if (element.addEventListener) {
var touchType = _touchEventLookup[type];
capture = capture || {
passive: false
};
element.addEventListener(touchType || type, func, capture);
if (touchType && type !== touchType && touchType.substr(0, 7) !== "pointer") {
//some browsers actually support both, so must we. But pointer events cover all.
element.addEventListener(type, func, capture);
}
} else if (element.attachEvent) {
element.attachEvent("on" + type, func);
}
},
_removeListener = function _removeListener(element, type, func) {
if (element.removeEventListener) {
var touchType = _touchEventLookup[type];
element.removeEventListener(touchType || type, func);
if (touchType && type !== touchType && touchType.substr(0, 7) !== "pointer") {
element.removeEventListener(type, func);
}
} else if (element.detachEvent) {
element.detachEvent("on" + type, func);
}
},
_hasTouchID = function _hasTouchID(list, ID) {
var i = list.length;
while (--i > -1) {
if (list[i].identifier === ID) {
return true;
}
}
return false;
},
_onMultiTouchDocumentEnd = function _onMultiTouchDocumentEnd(e) {
_isMultiTouching = e.touches && _dragCount < e.touches.length;
_removeListener(e.target, "touchend", _onMultiTouchDocumentEnd);
},
_onMultiTouchDocument = function _onMultiTouchDocument(e) {
_isMultiTouching = e.touches && _dragCount < e.touches.length;
_addListener(e.target, "touchend", _onMultiTouchDocumentEnd);
},
_bind = function _bind(func, scope) {
return function (e) {
return func.call(scope, e);
};
},
_callback = function _callback(type, self, param) {
var callback = self.vars[type];
if (callback) {
callback.call(self.vars.callbackScope || self, param || self);
}
return self;
},
_copyElement,
_resetSelection = function _resetSelection() {
_copyElement.style.display = "block";
_copyElement.select();
_copyElement.style.display = "none";
},
_coreInitted,
_initCore = function _initCore(core) {
_doc = document;
_win = window;
_body = _doc.body;
gsap = gsap || core || _win.gsap || console.warn("Please gsap.registerPlugin(PathEditor)");
_context = gsap && gsap.core.context || function () {};
_tempDiv = _createElement("div");
_copyElement = _createElement("textarea");
_copyElement.style.display = "none";
_body && _body.appendChild(_copyElement);
_touchEventLookup = function (types) {
//we create an object that makes it easy to translate touch event types into their "pointer" counterparts if we're in a browser that uses those instead. Like IE10 uses "MSPointerDown" instead of "touchstart", for example.
var standard = types.split(","),
converted = (_tempDiv.onpointerdown !== undefined ? "pointerdown,pointermove,pointerup,pointercancel" : _tempDiv.onmspointerdown !== undefined ? "MSPointerDown,MSPointerMove,MSPointerUp,MSPointerCancel" : types).split(","),
obj = {},
i = 4;
while (--i > -1) {
obj[standard[i]] = converted[i];
obj[converted[i]] = standard[i];
}
return obj;
}("touchstart,touchmove,touchend,touchcancel");
SVGElement.prototype.getTransformToElement = SVGElement.prototype.getTransformToElement || function (e) {
//adds Chrome support
return e.getScreenCTM().inverse().multiply(this.getScreenCTM());
};
_doc.addEventListener("keydown", function (e) {
var key = e.keyCode || e.which,
keyString = e.key || key,
i,
state,
a,
path;
if (keyString === "Shift" || key === 16) {
_SHIFT = true;
} else if (keyString === "Control" || key === 17) {
_CTRL = true;
} else if (keyString === "Meta" || key === 91) {
_CMD = true;
} else if (keyString === "Alt" || key === 18) {
_ALT = true;
i = _selectedPaths.length;
while (--i > -1) {
_selectedPaths[i]._onPressAlt();
}
} else if ((keyString === "z" || key === 90) && (_CTRL || _CMD) && _history.length > 1) {
//UNDO
_history.shift();
state = _history[0];
if (state) {
path = state.path;
path.path.setAttribute("d", state.d);
path.path.setAttribute("transform", state.transform);
path.init();
a = path._anchors;
for (i = 0; i < a.length; i++) {
if (state.selectedIndexes.indexOf(a[i].i) !== -1) {
path._selectedAnchors.push(a[i]);
}
}
path._updateAnchors();
path.update();
if (path.vars.onUndo) {
path.vars.onUndo.call(path);
}
}
} else if (keyString === "Delete" || keyString === "Backspace" || key === 8 || key === 46 || key === 63272 || key === "d" && (_CTRL || _CMD)) {
//DELETE
i = _selectedPaths.length;
while (--i > -1) {
_selectedPaths[i]._deleteSelectedAnchors();
}
} else if ((keyString === "a" || key === 65) && (_CMD || _CTRL)) {
//SELECT ALL
i = _selectedPaths.length;
while (--i > -1) {
_selectedPaths[i].select(true);
}
}
}, true);
_doc.addEventListener("keyup", function (e) {
var key = e.key || e.keyCode || e.which;
if (key === "Shift" || key === 16) {
_SHIFT = false;
} else if (key === "Control" || key === 17) {
_CTRL = false;
} else if (key === "Meta" || key === 91) {
_CMD = false;
} else if (key === "Alt" || key === 18) {
_ALT = false;
var i = _selectedPaths.length;
while (--i > -1) {
_selectedPaths[i]._onReleaseAlt();
}
}
}, true);
_supportsPointer = !!_win.PointerEvent;
_addListener(_doc, "mouseup", _checkDeselect);
_addListener(_doc, "touchend", _checkDeselect);
_addListener(_doc, "touchcancel", _emptyFunc); //some older Android devices intermittently stop dispatching "touchmove" events if we don't listen for "touchcancel" on the document. Very strange indeed.
_addListener(_win, "touchmove", _emptyFunc); //works around Safari bugs that still allow the page to scroll even when we preventDefault() on the touchmove event.
_body && _body.addEventListener("touchstart", _emptyFunc); //works around Safari bug: https://gsap.com/forums/topic/21450-draggable-in-iframe-on-mobile-is-buggy/
_coreInitted = 1;
},
_onPress = function _onPress(e) {
var self = this,
ctm = getGlobalMatrix(self.target.parentNode, true),
//previously used self.target.parentNode.getScreenCTM().inverse() but there's a major bug in Firefox that prevents it from working properly when there's an ancestor with a transform applied, so we bootstrapped our own solution that seems to work great across all browsers.
touchEventTarget,
temp;
this._matrix = this.target.transform.baseVal.getItem(0).matrix;
this._ctm = ctm;
if (_touchEventLookup[e.type]) {
//note: on iOS, BOTH touchmove and mousemove are dispatched, but the mousemove has pageY and pageX of 0 which would mess up the calculations and needlessly hurt performance.
touchEventTarget = e.type.indexOf("touch") !== -1 ? e.currentTarget || e.target : _doc; //pointer-based touches (for Microsoft browsers) don't remain locked to the original target like other browsers, so we must use the document instead. The event type would be "MSPointerDown" or "pointerdown".
_addListener(touchEventTarget, "touchend", self._onRelease);
_addListener(touchEventTarget, "touchmove", self._onMove);
_addListener(touchEventTarget, "touchcancel", self._onRelease);
_addListener(_doc, "touchstart", _onMultiTouchDocument);
_addListener(_win, "touchforcechange", _preventDefault); //otherwise iOS will scroll when dragging.
} else {
touchEventTarget = null;
_addListener(_doc, "mousemove", self._onMove); //attach these to the document instead of the box itself so that if the user's mouse moves too quickly (and off of the box), things still work.
}
if (!_supportsPointer) {
_addListener(_doc, "mouseup", self._onRelease);
}
_preventDefault(e);
_resetSelection(); // when a PathEditor is in an iframe in an environment like codepen, this helps avoid situations where the DELETE key won't actually work because the parent frame is intercepting the event.
if (e.changedTouches) {
//touch events store the data slightly differently
e = self.touch = e.changedTouches[0];
self.touchID = e.identifier;
} else if (e.pointerId) {
self.touchID = e.pointerId; //for some Microsoft browsers
} else {
self.touch = self.touchID = null;
}
self._startPointerY = self.pointerY = e.pageY; //record the starting x and y so that we can calculate the movement from the original in _onMouseMove
self._startPointerX = self.pointerX = e.pageX;
self._startElementX = self._matrix.e;
self._startElementY = self._matrix.f;
if (this._ctm.a === 1 && this._ctm.b === 0 && this._ctm.c === 0 && this._ctm.d === 1) {
this._ctm = null;
} else {
temp = self._startPointerX * this._ctm.a + self._startPointerY * this._ctm.c + this._ctm.e;
self._startPointerY = self._startPointerX * this._ctm.b + self._startPointerY * this._ctm.d + this._ctm.f;
self._startPointerX = temp;
}
self.isPressed = _isPressed = true;
self.touchEventTarget = touchEventTarget;
if (self.vars.onPress) {
self.vars.onPress.call(self.vars.callbackScope || self, self.pointerEvent);
}
},
_onMove = function _onMove(e) {
var self = this,
originalEvent = e,
touches,
i;
if (!self._enabled || _isMultiTouching || !self.isPressed || !e) {
return;
}
self.pointerEvent = e;
touches = e.changedTouches;
if (touches) {
//touch events store the data slightly differently
e = touches[0];
if (e !== self.touch && e.identifier !== self.touchID) {
//Usually changedTouches[0] will be what we're looking for, but in case it's not, look through the rest of the array...(and Android browsers don't reuse the event like iOS)
i = touches.length;
while (--i > -1 && (e = touches[i]).identifier !== self.touchID) {}
if (i < 0) {
return;
}
}
} else if (e.pointerId && self.touchID && e.pointerId !== self.touchID) {
//for some Microsoft browsers, we must attach the listener to the doc rather than the trigger so that when the finger moves outside the bounds of the trigger, things still work. So if the event we're receiving has a pointerId that doesn't match the touchID, ignore it (for multi-touch)
return;
}
_preventDefault(originalEvent);
self.setPointerPosition(e.pageX, e.pageY);
if (self.vars.onDrag) {
self.vars.onDrag.call(self.vars.callbackScope || self, self.pointerEvent);
}
},
_onRelease = function _onRelease(e, force) {
var self = this;
if (!self._enabled || !self.isPressed || e && self.touchID != null && !force && (e.pointerId && e.pointerId !== self.touchID || e.changedTouches && !_hasTouchID(e.changedTouches, self.touchID))) {
//for some Microsoft browsers, we must attach the listener to the doc rather than the trigger so that when the finger moves outside the bounds of the trigger, things still work. So if the event we're receiving has a pointerId that doesn't match the touchID, ignore it (for multi-touch)
return;
}
_interacted();
self.isPressed = _isPressed = false; //TODO: if we want to accommodate multi-touch, we'd need to introduce a counter to track how many touches there are and only toggle this when they're all off.
var originalEvent = e,
wasDragging = self.isDragging,
touchEventTarget = self.touchEventTarget,
touches,
i;
if (touchEventTarget) {
_removeListener(touchEventTarget, "touchend", self._onRelease);
_removeListener(touchEventTarget, "touchmove", self._onMove);
_removeListener(touchEventTarget, "touchcancel", self._onRelease);
_removeListener(_doc, "touchstart", _onMultiTouchDocument);
} else {
_removeListener(_doc, "mousemove", self._onMove);
}
if (!_supportsPointer) {
_removeListener(_doc, "mouseup", self._onRelease);
if (e && e.target) {
_removeListener(e.target, "mouseup", self._onRelease);
}
}
if (wasDragging) {
self.isDragging = false;
} else if (self.vars.onClick) {
self.vars.onClick.call(self.vars.callbackScope || self, originalEvent);
}
if (e) {
touches = e.changedTouches;
if (touches) {
//touch events store the data slightly differently
e = touches[0];
if (e !== self.touch && e.identifier !== self.touchID) {
//Usually changedTouches[0] will be what we're looking for, but in case it's not, look through the rest of the array...(and Android browsers don't reuse the event like iOS)
i = touches.length;
while (--i > -1 && (e = touches[i]).identifier !== self.touchID) {}
if (i < 0) {
return;
}
}
}
self.pointerEvent = originalEvent;
self.pointerX = e.pageX;
self.pointerY = e.pageY;
}
if (originalEvent && !wasDragging && self.vars.onDragRelease) {
self.vars.onDragRelease.call(self, self.pointerEvent);
} else {
if (originalEvent) {
_preventDefault(originalEvent);
}
if (self.vars.onRelease) {
self.vars.onRelease.call(self.vars.callbackScope || self, self.pointerEvent);
}
}
if (wasDragging && self.vars.onDragEnd) {
self.vars.onDragEnd.call(self.vars.callbackScope || self, self.pointerEvent);
}
return true;
},
_createSegmentAnchors = function _createSegmentAnchors(rawPath, j, editor, vars) {
var segment = rawPath[j],
l = segment.length - (segment.closed ? 6 : 0),
a = [],
i;
for (i = 0; i < l; i += 6) {
a.push(new Anchor(editor, rawPath, j, i, vars));
}
segment.closed && (a[0].isClosedStart = true);
return a;
},
_getLength = function _getLength(segment, i, i2) {
//i is the starting index, and it'll return the length to the next x/y pair. So if you're looking for the length to handle1, you'd feed in the index of the handle control point x whereas if you're looking for the length to handle2, i would be the x of the anchor.
var x = segment[i2] - segment[i],
y = segment[i2 + 1] - segment[i + 1];
return Math.sqrt(x * x + y * y);
};
var DraggableSVG = /*#__PURE__*/function () {
function DraggableSVG(target, vars) {
this.target = typeof target === "string" ? _doc.querySelectorAll(target)[0] : target;
this.vars = vars || {};
this._onPress = _bind(_onPress, this);
this._onMove = _bind(_onMove, this);
this._onRelease = _bind(_onRelease, this);
this.target.setAttribute("transform", (this.target.getAttribute("transform") || "") + " translate(0,0)");
this._matrix = _getConsolidatedMatrix(this.target);
this.x = this._matrix.e;
this.y = this._matrix.f;
this.snap = vars.snap;
if (!isNaN(vars.maxX) || !isNaN(vars.minX)) {
this._bounds = 1;
this.maxX = +vars.maxX;
this.minX = +vars.minX;
} else {
this._bounds = 0;
}
this.enabled(true);
}
var _proto = DraggableSVG.prototype;
_proto.setPointerPosition = function setPointerPosition(pointerX, pointerY) {
var rnd = 1000,
xChange,
yChange,
x,
y,
temp;
this.pointerX = pointerX;
this.pointerY = pointerY;
if (this._ctm) {
temp = pointerX * this._ctm.a + pointerY * this._ctm.c + this._ctm.e;
pointerY = pointerX * this._ctm.b + pointerY * this._ctm.d + this._ctm.f;
pointerX = temp;
}
yChange = pointerY - this._startPointerY;
xChange = pointerX - this._startPointerX;
if (yChange < _minimumMovement && yChange > -_minimumMovement) {
yChange = 0;
}
if (xChange < _minimumMovement && xChange > -_minimumMovement) {
xChange = 0;
}
x = ((this._startElementX + xChange) * rnd | 0) / rnd;
y = ((this._startElementY + yChange) * rnd | 0) / rnd;
if (this.snap && !_SHIFT) {
_point.x = x;
_point.y = y;
this.snap.call(this, _point);
x = _point.x;
y = _point.y;
}
if (this.x !== x || this.y !== y) {
this._matrix.f = this.y = y;
this._matrix.e = this.x = x;
if (!this.isDragging && this.isPressed) {
this.isDragging = true;
_callback("onDragStart", this, this.pointerEvent);
}
}
};
_proto.enabled = function enabled(_enabled) {
if (!arguments.length) {
return this._enabled;
}
var dragging;
this._enabled = _enabled;
if (_enabled) {
if (!_supportsPointer) {
_addListener(this.target, "mousedown", this._onPress);
}
_addListener(this.target, "touchstart", this._onPress);
_addListener(this.target, "click", this._onClick, true); //note: used to pass true for capture but it prevented click-to-play-video functionality in Firefox.
} else {
dragging = this.isDragging;
_removeListener(this.target, "mousedown", this._onPress);
_removeListener(this.target, "touchstart", this._onPress);
_removeListener(_win, "touchforcechange", _preventDefault);
_removeListener(this.target, "click", this._onClick);
if (this.touchEventTarget) {
_removeListener(this.touchEventTarget, "touchcancel", this._onRelease);
_removeListener(this.touchEventTarget, "touchend", this._onRelease);
_removeListener(this.touchEventTarget, "touchmove", this._onMove);
}
_removeListener(_doc, "mouseup", this._onRelease);
_removeListener(_doc, "mousemove", this._onMove);
this.isDragging = this.isPressed = false;
if (dragging) {
_callback("onDragEnd", this, this.pointerEvent);
}
}
return this;
};
_proto.endDrag = function endDrag(e) {
this._onRelease(e);
};
return DraggableSVG;
}();
var Anchor = /*#__PURE__*/function () {
function Anchor(editor, rawPath, j, i, vars) {
this.editor = editor;
this.element = _createSVG("path", editor._selection, {
fill: _selectionColor,
stroke: _selectionColor,
strokeWidth: 2,
vectorEffect: "non-scaling-stroke"
});
this.update(rawPath, j, i);
this.element._gsSelection = true;
this.vars = vars || {};
this._draggable = new DraggableSVG(this.element, {
callbackScope: this,
onDrag: this.onDrag,
snap: this.vars.snap,
onPress: this.onPress,
onRelease: this.onRelease,
onClick: this.onClick,
onDragEnd: this.onDragEnd
});
}
var _proto2 = Anchor.prototype;
_proto2.onPress = function onPress() {
_callback("onPress", this);
};
_proto2.onClick = function onClick() {
_callback("onClick", this);
};
_proto2.onDrag = function onDrag() {
var s = this.segment;
this.vars.onDrag.call(this.vars.callbackScope || this, this, this._draggable.x - s[this.i], this._draggable.y - s[this.i + 1]);
};
_proto2.onDragEnd = function onDragEnd() {
_callback("onDragEnd", this);
};
_proto2.onRelease = function onRelease() {
_callback("onRelease", this);
};
_proto2.update = function update(rawPath, j, i) {
if (rawPath) {
this.rawPath = rawPath;
}
if (arguments.length <= 1) {
j = this.j;
i = this.i;
} else {
this.j = j;
this.i = i;
}
var prevSmooth = this.smooth,
segment = this.rawPath[j],
pi = i === 0 && segment.closed ? segment.length - 4 : i - 2;
this.segment = segment;
this.smooth = i > 0 && i < segment.length - 2 && Math.abs(Math.atan2(segment[i + 1] - segment[pi + 1], segment[i] - segment[pi]) - Math.atan2(segment[i + 3] - segment[i + 1], segment[i + 2] - segment[i])) < 0.09 ? 2 : 0; //0: corner, 1: smooth but not mirrored, 2: smooth and mirrored.
if (this.smooth !== prevSmooth) {
this.element.setAttribute("d", this.smooth ? this.editor._circleHandle : this.editor._squareHandle);
}
this.element.setAttribute("transform", "translate(" + segment[i] + "," + segment[i + 1] + ")");
};
return Anchor;
}();
export var PathEditor = /*#__PURE__*/function () {
function PathEditor(target, vars) {
vars = vars || {};
_coreInitted || _initCore();
this.vars = vars;
this.path = typeof target === "string" ? _doc.querySelectorAll(target)[0] : target;
this._g = _createSVG("g", this.path.ownerSVGElement, {
"class": "path-editor-g path-editor"
});
this._selectionHittest = _createSVG("path", this._g, {
stroke: "transparent",
strokeWidth: 16,
fill: "none",
vectorEffect: "non-scaling-stroke"
});
this._selection = vars._selection || _createSVG("g", this._g, {
"class": "path-editor-selection path-editor"
});
this._selectionPath = _createSVG("path", this._selection, {
stroke: _selectionColor,
strokeWidth: 2,
fill: "none",
vectorEffect: "non-scaling-stroke"
});
this._selectedAnchors = [];
this._line1 = _createSVG("polyline", this._selection, {
stroke: _selectionColor,
strokeWidth: 2,
vectorEffect: "non-scaling-stroke"
});
this._line2 = _createSVG("polyline", this._selection, {
stroke: _selectionColor,
strokeWidth: 2,
vectorEffect: "non-scaling-stroke"
});
this._line1.style.pointerEvents = this._line2.style.pointerEvents = this._selectionPath.style.pointerEvents = "none";
this._enabled = true;
var ctm = this.path.parentNode.getScreenCTM().inverse(),
size = (ctm.a + ctm.d) / 2 * (vars.handleSize || 5);
this._squareHandle = _getSquarePathData(size);
this._circleHandle = _getCirclePathData(size * 1.15);
this._handle1 = _createSVG("path", this._selection, {
d: this._squareHandle,
fill: _selectionColor,
stroke: "transparent",
strokeWidth: 6
});
this._handle2 = _createSVG("path", this._selection, {
d: this._squareHandle,
fill: _selectionColor,
stroke: "transparent",
strokeWidth: 6
});
this._handle1._draggable = new DraggableSVG(this._handle1, {
onDrag: this._onDragHandle1,
callbackScope: this,
onPress: this._onPressHandle1,
onRelease: this._onReleaseHandle,
onClick: this._onClickHandle1,
snap: vars.handleSnap
});
this._handle2._draggable = new DraggableSVG(this._handle2, {
onDrag: this._onDragHandle2,
callbackScope: this,
onPress: this._onPressHandle2,
onRelease: this._onReleaseHandle,
onClick: this._onClickHandle2,
snap: vars.handleSnap
});
this._handle1.style.visibility = this._handle2.style.visibility = "hidden";
var selectionItems = [this._handle1, this._handle2, this._line1, this._line2, this._selection, this._selectionPath, this._selectionHittest],
i = selectionItems.length;
while (--i > -1) {
selectionItems[i]._gsSelection = true; //just a flag we can check in the _checkDeselect() method to detect clicks on things that are selection-related.
}
if (vars.draggable !== false) {
this._draggable = new DraggableSVG(this._selectionHittest, {
callbackScope: this,
onPress: this.select,
onRelease: this._onRelease,
onDrag: this._onDragPath,
onDragEnd: this._saveState,
maxX: this.vars.maxX,
minX: this.vars.minX
});
}
this.init();
this._selection.style.visibility = vars.selected === false ? "hidden" : "visible";
if (vars.selected !== false) {
this.path._gsSelection = true;
_selectedPaths.push(this);
}
this._saveState();
if (!_supportsPointer) {
_addListener(this._selectionHittest, "mousedown", _bind(this._onClickSelectionPath, this));
_addListener(this._selectionHittest, "mouseup", _bind(this._onRelease, this));
}
_addListener(this._selectionHittest, "touchstart", _bind(this._onClickSelectionPath, this));
_addListener(this._selectionHittest, "touchend", _bind(this._onRelease, this));
_context(this);
}
var _proto3 = PathEditor.prototype;
_proto3._onRelease = function _onRelease(e) {
var anchor = this._editingAnchor;
if (anchor) {
_editingAxis.x = anchor.segment[anchor.i];
_editingAxis.y = anchor.segment[anchor.i + 1];
}
_removeListener(_win, "touchforcechange", _preventDefault); //otherwise iOS will scroll when dragging.
_callback("onRelease", this, e);
};
_proto3.init = function init() {
var pathData = this.path.getAttribute("d"),
rawPath = stringToRawPath(pathData),
transform = this.path.getAttribute("transform") || "translate(0,0)",
createAnchors = !this._rawPath || rawPath.totalPoints !== this._rawPath.totalPoints || rawPath.length !== this._rawPath.length,
anchorVars = {
callbackScope: this,
snap: this.vars.anchorSnap,
onDrag: this._onDragAnchor,
onPress: this._onPressAnchor,
onRelease: this._onRelease,
onClick: this._onClickAnchor,
onDragEnd: this._onDragEndAnchor,
maxX: this.vars.maxX,
minX: this.vars.minX
},
l,
i;
if (createAnchors && this._anchors && this._anchors.length) {
for (i = 0; i < this._anchors.length; i++) {
this._anchors[i].element.parentNode.removeChild(this._anchors[i].element);
this._anchors[i]._draggable.enabled(false);
}
this._selectedAnchors.length = 0;
}
this._rawPath = rawPath;
if (createAnchors) {
this._anchors = _createSegmentAnchors(rawPath, 0, this, anchorVars);
l = rawPath.length;
if (l > 1) {
for (i = 1; i < l; i++) {
this._anchors = this._anchors.concat(_createSegmentAnchors(rawPath, i, this, anchorVars));
}
}
} else {
i = this._anchors.length;
while (--i > -1) {
this._anchors[i].update(rawPath);
}
}
this._selection.appendChild(this._handle1); //for stacking order (handles should always be on top)
this._selection.appendChild(this._handle2); // this._selectedAnchors.length = 0;
this._selectionPath.setAttribute("d", pathData);
this._selectionHittest.setAttribute("d", pathData);
this._g.setAttribute("transform", _getConcatenatedTransforms(this.path.parentNode) || "translate(0,0)");
this._selection.setAttribute("transform", transform);
this._selectionHittest.setAttribute("transform", transform);
this._updateAnchors();
return this;
};
_proto3._saveState = function _saveState() {
_addHistory(this);
};
_proto3._onClickSelectionPath = function _onClickSelectionPath(e) {
if (this._selection.style.visibility === "hidden") {
this.select();
} else if (_ALT || e && e.altKey) {
var anchorVars = {
callbackScope: this,
snap: this.vars.anchorSnap,
onDrag: this._onDragAnchor,
onPress: this._onPressAnchor,
onRelease: this._onRelease,
onClick: this._onClickAnchor,
onDragEnd: this._onDragEndAnchor,
maxX: this.vars.maxX,
minX: this.vars.minX
},
ctm = this._selection.getScreenCTM().inverse(),
newIndex,
_i,
anchor,
x,
y,
closestData;
if (this._draggable) {
this._draggable._onRelease(e); //otherwise, ALT-click/dragging on a path would create a new anchor AND drag the entire path.
}
if (ctm) {
x = e.clientX * ctm.a + e.clientY * ctm.c + ctm.e;
y = e.clientX * ctm.b + e.clientY * ctm.d + ctm.f;
} //DEBUG: _createSVG("circle", this._selection, {fill:"red", r:5, cx:x, cy:y});
closestData = getClosestData(this._rawPath, x, y);
subdivideSegment(this._rawPath[closestData.j], closestData.i, closestData.t);
newIndex = closestData.i + 6;
for (_i = 0; _i < this._anchors.length; _i++) {
if (this._anchors[_i].i >= newIndex && this._anchors[_i].j === closestData.j) {
this._anchors[_i].i += 6;
}
}
anchor = new Anchor(this, this._rawPath, closestData.j, newIndex, anchorVars);
this._selection.appendChild(this._handle1); //for stacking order (handles should always be on top)
this._selection.appendChild(this._handle2);
anchor._draggable._onPress(e);
_recentlyAddedAnchor = anchor;
this._anchors.push(anchor);
this._selectedAnchors.length = 0;
this._selectedAnchors.push(anchor);
this._updateAnchors();
this.update();
this._saveState();
}
_resetSelection();
_addListener(_win, "touchforcechange", _preventDefault); //otherwise iOS will scroll when dragging.
_callback("onPress", this);
};
_proto3._onClickHandle1 = function _onClickHandle1() {
var anchor = this._editingAnchor,
i = anchor.i,
s = anchor.segment,
pi = anchor.isClosedStart ? s.length - 4 : i - 2;
if (_ALT && Math.abs(s[i] - s[pi]) < 5 && Math.abs(s[i + 1] - s[pi + 1]) < 5) {
this._onClickAnchor(anchor);
}
};
_proto3._onClickHandle2 = function _onClickHandle2() {
var anchor = this._editingAnchor,
i = anchor.i,
s = anchor.segment;
if (_ALT && Math.abs(s[i] - s[i + 2]) < 5 && Math.abs(s[i + 1] - s[i + 3]) < 5) {
this._onClickAnchor(anchor);
}
};
_proto3._onDragEndAnchor = function _onDragEndAnchor(e) {
_recentlyAddedAnchor = null;
this._saveState();
};
_proto3.isSelected = function isSelected() {
return this._selectedAnchors.length > 0 || this._selection.style.visibility === "visible";
};
_proto3.select = function select(allAnchors) {
this._selection.style.visibility = "visible";
this._editingAnchor = null;
this.path._gsSelection = true;
if (allAnchors === true) {
var _i2 = this._anchors.length;
while (--_i2 > -1) {
this._selectedAnchors[_i2] = this._anchors[_i2];
}
}
if (_selectedPaths.indexOf(this) === -1) {
_selectedPaths.push(this);
}
this._updateAnchors();
return this;
};
_proto3.deselect = function deselect() {
this._selection.style.visibility = "hidden";
this._selectedAnchors.length = 0;
this._editingAnchor = null;
this.path._gsSelection = false;
_selectedPaths.splice(_selectedPaths.indexOf(this), 1);
this._updateAnchors();
return this;
};
_proto3._onDragPath = function _onDragPath(e) {
var transform = this._selectionHittest.getAttribute("transform") || "translate(0,0)";
this._selection.setAttribute("transform", transform);
this.path.setAttribute("transform", transform);
};
_proto3._onPressAnchor = function _onPressAnchor(anchor) {
if (this._selectedAnchors.indexOf(anchor) === -1) {
//if it isn't already selected...
if (!_SHIFT) {
this._selectedAnchors.length = 0;
}
this._selectedAnchors.push(anchor);
} else if (_SHIFT) {
this._selectedAnchors.splice(this._selectedAnchors.indexOf(anchor), 1);
anchor._draggable.endDrag();
}
_editingAxis.x = anchor.segment[anchor.i];
_editingAxis.y = anchor.segment[anchor.i + 1];
this._updateAnchors();
_callback("onPress", this);
};
_proto3._deleteSelectedAnchors = function _deleteSelectedAnchors() {
var anchors = this._selectedAnchors,
i = anchors.length,
anchor,
index,
j,
jIndex;
while (--i > -1) {
anchor = anchors[i];
anchor.element.parentNode.removeChild(anchor.element);
anchor._draggable.enabled(false);
index = anchor.i;
jIndex = anchor.j;
if (!index) {
//first
anchor.segment.splice(index, 6);
} else if (index < anchor.segment.length - 2) {
anchor.segment.splice(index - 2, 6);
} else {
//last
anchor.segment.splice(index - 4, 6);
}
anchors.splice(i, 1);
this._anchors.splice(this._anchors.indexOf(anchor), 1);
for (j = 0; j < this._anchors.length; j++) {
if (this._anchors[j].i >= index && this._anchors[j].j === jIndex) {
this._anchors[j].i -= 6;
}
}
}
this._updateAnchors();
this.update();
this._saveState();
if (this.vars.onDeleteAnchor) {
this.vars.onDeleteAnchor.call(this.vars.callbackScope || this);
}
};
_proto3._onClickAnchor = function _onClickAnchor(anchor) {
var i = anchor.i,
segment = anchor.segment,
pi = anchor.isClosedStart ? segment.length - 4 : i - 2,
rnd = 1000,
isEnd = !i || i >= segment.length - 2,
angle1,
angle2,
length1,
length2,
sin,
cos;
if (_ALT && _recentlyAddedAnchor !== anchor && this._editingAnchor) {
anchor.smooth = !anchor.smooth;
if (isEnd && !anchor.isClosedStart) {
//the very ends can't be "smooth"
anchor.smooth = false;
}
anchor.element.setAttribute("d", anchor.smooth ? this._circleHandle : this._squareHandle);
if (anchor.smooth && (!isEnd || anchor.isClosedStart)) {
angle1 = Math.atan2(segment[i + 1] - segment[pi + 1], segment[i] - segment[pi]);
angle2 = Math.atan2(segment[i + 3] - segment[i + 1], segment[i + 2] - segment[i]);
angle1 = (angle1 + angle2) / 2;
length1 = _getLength(segment, pi, i);
length2 = _getLength(segment, i, i + 2);
if (length1 < 0.2) {
length1 = _getLength(segment, i, pi - 4) / 4;
angle1 = angle2 || Math.atan2(segment[i + 7] - segment[pi - 3], segment[i + 6] - segment[pi - 4]);
}
if (length2 < 0.2) {
length2 = _getLength(segment, i, i + 6) / 4;
angle2 = angle1 || Math.atan2(segment[i + 7] - segment[pi - 3], segment[i + 6] - segment[pi - 4]);
}
sin = Math.sin(angle1);
cos = Math.cos(angle1);
if (Math.abs(angle2 - angle1) < Math.PI / 2) {
sin = -sin;
cos = -cos;
}
segment[pi] = ((segment[i] + cos * length1) * rnd | 0) / rnd;
segment[pi + 1] = ((segment[i + 1] + sin * length1) * rnd | 0) / rnd;
segment[i + 2] = ((segment[i] - cos * length2) * rnd | 0) / rnd;
segment[i + 3] = ((segment[i + 1] - sin * length2) * rnd | 0) / rnd;
this._updateAnchors();
this.update();
this._saveState();
} else if (!anchor.smooth && (!isEnd || anchor.isClosedStart)) {
if (i || anchor.isClosedStart) {
segment[pi] = segment[i];
segment[pi + 1] = segment[i + 1];
}
if (i < segment.length - 2) {
segment[i + 2] = segment[i];
segment[i + 3] = segment[i + 1];
}
this._updateAnchors();
this.update();
this._saveState();
}
} else if (!_SHIFT) {
this._selectedAnchors.length = 0;
this._selectedAnchors.push(anchor);
}
_recentlyAddedAnchor = null;
this._updateAnchors();
};
_proto3._updateAnchors = function _updateAnchors() {
var anchor = this._selectedAnchors.length === 1 ? this._selectedAnchors[0] : null,
segment = anchor ? anchor.segment : null,
i,
x,
y;
this._editingAnchor = anchor;
for (i = 0; i < this._anchors.length; i++) {
this._anchors[i].element.style.fill = this._selectedAnchors.indexOf(this._anchors[i]) !== -1 ? _selectionColor : "white"; //this._anchors[i].element.setAttribute("fill", (this._selectedAnchors.indexOf(this._anchors[i]) !== -1) ? _selectionColor : "white");
}
if (anchor) {
this._handle1.setAttribute("d", anchor.smooth ? this._circleHandle : this._squareHandle);
this._handle2.setAttribute("d", anchor.smooth ? this._circleHandle : this._squareHandle);
}
i = anchor ? anchor.i : 0;
if (anchor && (i || anchor.isClosedStart)) {
x = anchor.isClosedStart ? segment[segment.length - 4] : segment[i - 2];
y = anchor.isClosedStart ? segment[segment.length - 3] : segment[i - 1]; //TODO: if they equal the anchor coordinates, just hide it.
this._handle1.style.visibility = this._line1.style.visibility = !_ALT && x === segment[i] && y === segment[i + 1] ? "hidden" : "visible";
this._handle1.setAttribute("transform", "translate(" + x + _comma + y + ")");
this._line1.setAttribute("points", x + _comma + y + _comma + segment[i] + _comma + segment[i + 1]);
} else {
this._handle1.style.visibility = this._line1.style.visibility = "hidden";
}
if (anchor && i < segment.length - 2) {
x = segment[i + 2];
y = segment[i + 3];
this._handle2.style.visibility = this._line2.style.visibility = !_ALT && x === segment[i] && y === segment[i + 1] ? "hidden" : "visible";
this._handle2.setAttribute("transform", "translate(" + x + _comma + y + ")");
this._line2.setAttribute("points", segment[i] + _comma + segment[i + 1] + _comma + x + _comma + y);
} else {
this._handle2.style.visibility = this._line2.style.visibility = "hidden";
}
};
_proto3._onPressAlt = function _onPressAlt() {
var anchor = this._editingAnchor;
if (anchor) {
if (anchor.i || anchor.isClosedStart) {
this._handle1.style.visibility = this._line1.style.visibility = "visible";
}
if (anchor.i < anchor.segment.length - 2) {
this._handle2.style.visibility = this._line2.style.visibility = "visible";
}
}
};
_proto3._onReleaseAlt = function _onReleaseAlt() {
var anchor = this._editingAnchor,
s,
i,
pi;
if (anchor) {
s = anchor.segment;
i = anchor.i;
pi = anchor.isClosedStart ? s.length - 4 : i - 2;
if (s[i] === s[pi] && s[i + 1] === s[pi + 1]) {
this._handle1.style.visibility = this._line1.style.visibility = "hidden";
}
if (s[i] === s[i + 2] && s[i + 1] === s[i + 3]) {
this._handle2.style.visibility = this._line2.style.visibility = "hidden";
}
}
};
_proto3._onPressHandle1 = function _onPressHandle1() {
if (this._editingAnchor.smooth) {
this._oppositeHandleLength = _getLength(this._editingAnchor.segment, this._editingAnchor.i, this._editingAnchor.i + 2);
}
_callback("onPress", this);
};
_proto3._onPressHandle2 = function _onPressHandle2() {
if (this._editingAnchor.smooth) {
this._oppositeHandleLength = _getLength(this._editingAnchor.segment, this._editingAnchor.isClosedStart ? this._editingAnchor.segment.length - 4 : this._editingAnchor.i - 2, this._editingAnchor.i);
}
_callback("onPress", this);
};
_proto3._onReleaseHandle = function _onReleaseHandle(e) {
this._onRelease(e);
this._saveState();
};
_proto3._onDragHandle1 = function _onDragHandle1() {
var anchor = this._editingAnchor,
s = anchor.segment,
i = anchor.i,
pi = anchor.isClosedStart ? s.length - 4 : i - 2,
rnd = 1000,
x = this._handle1._draggable.x,
y = this._handle1._draggable.y,
angle;
s[pi] = x = (x * rnd | 0) / rnd;
s[pi + 1] = y = (y * rnd | 0) / rnd;
if (anchor.smooth) {
if (_ALT) {
anchor.smooth = false;
anchor.element.setAttribute("d", this._squareHandle);
this._handle1.setAttribute("d", this._squareHandle);
this._handle2.setAttribute("d", this._squareHandle);
} else {
angle = Math.atan2(s[i + 1] - y, s[i] - x);
x = this._oppositeHandleLength * Math.cos(angle);
y = this._oppositeHandleLength * Math.sin(angle);
s[i + 2] = ((s[i] + x) * rnd | 0) / rnd;
s[i + 3] = ((s[i + 1] + y) * rnd | 0) / rnd;
}
}
this.update();
};
_proto3._onDragHandle2 = function _onDragHandle2() {
var anchor = this._editingAnchor,
s = anchor.segment,
i = anchor.i,
pi = anchor.isClosedStart ? s.length - 4 : i - 2,
rnd = 1000,
x = this._handle2._draggable.x,
y = this._handle2._draggable.y,
angle;
s[i + 2] = x = (x * rnd | 0) / rnd;
s[i + 3] = y = (y * rnd | 0) / rnd;
if (anchor.smooth) {
if (_ALT) {
anchor.smooth = false;
anchor.element.setAttribute("d", this._squareHandle);
this._handle1.setAttribute("d", this._squareHandle);
this._handle2.setAttribute("d", this._squareHandle);
} else {
angle = Math.atan2(s[i + 1] - y, s[i] - x);
x = this._oppositeHandleLength * Math.cos(angle);
y = this._oppositeHandleLength * Math.sin(angle);
s[pi] = ((s[i] + x) * rnd | 0) / rnd;
s[pi + 1] = ((s[i + 1] + y) * rnd | 0) / rnd;
}
}
this.update();
};
_proto3._onDragAnchor = function _onDragAnchor(anchor, changeX, changeY) {
var anchors = this._selectedAnchors,
l = anchors.length,
rnd = 1000,
i,
j,
s,
a,
pi;
for (j = 0; j < l; j++) {
a = anchors[j];
i = a.i;
s = a.segment;
if (i) {
s[i - 2] = ((s[i - 2] + changeX) * rnd | 0) / rnd;
s[i - 1] = ((s[i - 1] + changeY) * rnd | 0) / rnd;
} else if (a.isClosedStart) {
pi = s.length - 2;
s[pi] = _round(s[pi] + changeX);
s[pi + 1] = _round(s[pi + 1] + changeY);
s[pi - 2] = _round(s[pi - 2] + changeX);
s[pi - 1] = _round(s[pi - 1] + changeY);
}
s[i] = ((s[i] + changeX) * rnd | 0) / rnd;
s[i + 1] = ((s[i + 1] + changeY) * rnd | 0) / rnd;
if (i < s.length - 2) {
s[i + 2] = ((s[i + 2] + changeX) * rnd | 0) / rnd;
s[i + 3] = ((s[i + 3] + changeY) * rnd | 0) / rnd;
}
if (a !== anchor) {
a.element.setAttribute("transform", "translate(" + s[i] + _comma + s[i + 1] + ")");
}
}
this.update();
};
_proto3.enabled = function enabled(_enabled2) {
if (!arguments.length) {
return this._enabled;
}
var i = this._anchors.length;
while (--i > -1) {
this._anchors[i]._draggable.enabled(_enabled2);
}
this._enabled = _enabled2;
this._handle1._draggable.enabled(_enabled2);
this._handle2._draggable.enabled(_enabled2);
if (this._draggable) {
this._draggable.enabled(_enabled2);
}
if (!_enabled2) {
this.deselect();
this._selectionHittest.parentNode && this._selectionHittest.parentNode.removeChild(this._selectionHittest);
this._selection.parentNode && this._selection.parentNode.removeChild(this._selection);
} else if (!this._selection.parentNode) {
this.path.ownerSVGElement.appendChild(this._selectionHittest);
this.path.ownerSVGElement.appendChild(this._selection);
this.init();
this._saveState();
}
this._updateAnchors();
return this.update();
};
_proto3.update = function update(readPath) {
var d = "",
anchor = this._editingAnchor,
i,
s,
x,
y,
pi;
if (readPath) {
this.init();
}
if (anchor) {
i = anchor.i;
s = anchor.segment;
if (i || anchor.isClosedStart) {
pi = anchor.isClosedStart ? s.length - 4 : i - 2;
x = s[pi];
y = s[pi + 1];
this._handle1.setAttribute("transform", "translate(" + x + _comma + y + ")");
this._line1.setAttribute("points", x + _comma + y + _comma + s[i] + _comma + s[i + 1]);
}
if (i < s.length - 2) {
x = s[i + 2];
y = s[i + 3];
this._handle2.setAttribute("transform", "translate(" + x + _comma + y + ")");
this._line2.setAttribute("points", s[i] + _comma + s[i + 1] + _comma + x + _comma + y);
}
}
if (readPath) {
d = this.path.getAttribute("d");
} else {
for (i = 0; i < this._rawPath.length; i++) {
s = this._rawPath[i];
if (s.length > 7) {
d += "M" + s[0] + _comma + s[1] + "C" + s.slice(2).join(_comma);
}
}
this.path.setAttribute("d", d);
this._selectionPath.setAttribute("d", d);
this._selectionHittest.setAttribute("d", d);
}
if (this.vars.onUpdate && this._enabled) {
_callback("onUpdate", this, d);
}
return this;
};
_proto3.getRawPath = function getRawPath(applyTransforms, offsetX, offsetY) {
if (applyTransforms) {
var m = _getConsolidatedMatrix(this.path);
return transformRawPath(copyRawPath(this._rawPath), 1, 0, 0, 1, m.e + (offsetX || 0), m.f + (offsetY || 0));
}
return this._rawPath;
};
_proto3.getString = function getString(applyTransforms, offsetX, offsetY) {
if (applyTransforms) {
var m = _getConsolidatedMatrix(this.path);
return rawPathToString(transformRawPath(copyRawPath(this._rawPath), 1, 0, 0, 1, m.e + (offsetX || 0), m.f + (offsetY || 0)));
}
return this.path.getAttribute("d");
};
_proto3.getNormalizedSVG = function getNormalizedSVG(height, originY, shorten, onEaseError) {
var s = this._rawPath[0],
tx = s[0] * -1,
ty = originY === 0 ? 0 : -(originY || s[1]),
l = s.length,
sx = 1 / (s[l - 2] + tx),
sy = -height || s[l - 1] + ty,
rnd = 1000,
points,
i,
x1,
y1,
x2,
y2;
_temp.length = 0;
if (sy) {
//typically y ends at 1 (so that the end values are reached)
sy = 1 / sy;
} else {
//in case the ease returns to its beginning value, scale eve