CurveEditor.hx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728
  1. package hide.comp;
  2. typedef CurveKey = hrt.prefab.Curve.CurveKey;
  3. class CurveEditor extends Component {
  4. public var xScale = 200.;
  5. public var yScale = 30.;
  6. public var xOffset = 0.;
  7. public var yOffset = 0.;
  8. public var curve(default, set) : hrt.prefab.Curve;
  9. public var undo : hide.ui.UndoHistory;
  10. public var lockViewX = false;
  11. public var lockViewY = false;
  12. public var lockKeyX = false;
  13. public var maxLength = 0.0;
  14. public var minValue : Float = 0.;
  15. public var maxValue : Float = 0.;
  16. var svg : hide.comp.SVG;
  17. var width = 0;
  18. var height = 0;
  19. var gridGroup : Element;
  20. var graphGroup : Element;
  21. var selectGroup : Element;
  22. var refreshTimer : haxe.Timer = null;
  23. var lastValue : Dynamic;
  24. var selectedKeys: Array<CurveKey> = [];
  25. var previewKeys: Array<CurveKey> = [];
  26. public function new(undo, ?parent) {
  27. super(parent,null);
  28. this.undo = undo;
  29. element.addClass("hide-curve-editor");
  30. element.attr({ tabindex: "1" });
  31. element.css({ width: "100%", height: "100%" });
  32. svg = new hide.comp.SVG(element);
  33. var div = this.element;
  34. var root = svg.element;
  35. height = Math.round(svg.element.height());
  36. if(height == 0 && parent != null)
  37. height = Math.round(parent.height());
  38. width = Math.round(svg.element.width());
  39. gridGroup = svg.group(root, "grid");
  40. graphGroup = svg.group(root, "graph");
  41. selectGroup = svg.group(root, "selection-overlay");
  42. root.resize((e) -> refresh());
  43. root.addClass("hide-curve-editor");
  44. root.mousedown(function(e) {
  45. var offset = root.offset();
  46. var px = e.clientX - offset.left;
  47. var py = e.clientY - offset.top;
  48. e.preventDefault();
  49. e.stopPropagation();
  50. div.focus();
  51. if(e.which == 1) {
  52. if(e.ctrlKey) {
  53. addKey(ixt(px), iyt(py));
  54. }
  55. else {
  56. startSelectRect(px, py);
  57. }
  58. }
  59. else if(e.which == 2) {
  60. // Pan
  61. startPan(e);
  62. }
  63. });
  64. element.keydown(function(e) {
  65. if(e.key == "z") {
  66. zoomAll();
  67. refresh();
  68. }
  69. });
  70. root.contextmenu(function(e) {
  71. e.preventDefault();
  72. return false;
  73. });
  74. root.on("mousewheel", function(e : js.jquery.Event) {
  75. var step = (e:Dynamic).originalEvent.wheelDelta > 0 ? 1.0 : -1.0;
  76. var changed = false;
  77. if(e.shiftKey) {
  78. if(!lockViewY) {
  79. yScale *= Math.pow(1.125, step);
  80. changed = true;
  81. }
  82. }
  83. else {
  84. if(!lockViewX) {
  85. xScale *= Math.pow(1.125, step);
  86. changed = true;
  87. }
  88. }
  89. if(changed) {
  90. e.preventDefault();
  91. e.stopPropagation();
  92. refresh();
  93. }
  94. });
  95. div.keydown(function(e) {
  96. if(curve == null) return;
  97. if(e.keyCode == 46) {
  98. beforeChange();
  99. var newVal = [for(k in curve.keys) if(selectedKeys.indexOf(k) < 0) k];
  100. curve.keys = newVal;
  101. selectedKeys = [];
  102. e.preventDefault();
  103. e.stopPropagation();
  104. afterChange();
  105. }
  106. if(e.key == "z") {
  107. zoomAll();
  108. }
  109. });
  110. }
  111. public dynamic function onChange(anim: Bool) {
  112. }
  113. public dynamic function onKeyMove(key: CurveKey, prevTime: Float, prevVal: Float) {
  114. }
  115. function set_curve(curve: hrt.prefab.Curve) {
  116. this.curve = curve;
  117. maxLength = curve.maxTime;
  118. lastValue = haxe.Json.parse(haxe.Json.stringify(curve.save()));
  119. var view = getDisplayState("view");
  120. if(view != null) {
  121. if(!lockViewX) {
  122. xOffset = view.xOffset;
  123. xScale = view.xScale;
  124. }
  125. if(!lockViewY) {
  126. yOffset = view.yOffset;
  127. yScale = view.yScale;
  128. }
  129. }
  130. else {
  131. zoomAll();
  132. }
  133. refresh();
  134. return curve;
  135. }
  136. function addKey(time: Float, ?val: Float) {
  137. beforeChange();
  138. if(minValue < maxValue)
  139. val = hxd.Math.clamp(val, minValue, maxValue);
  140. curve.addKey(time, val, curve.keyMode);
  141. afterChange();
  142. }
  143. function addPreviewKey(time: Float, ?val: Float) {
  144. beforeChange();
  145. if(minValue < maxValue)
  146. val = hxd.Math.clamp(val, minValue, maxValue);
  147. curve.addPreviewKey(time, val);
  148. afterChange();
  149. }
  150. function fixKey(key : CurveKey) {
  151. var index = curve.keys.indexOf(key);
  152. var prev = curve.keys[index-1];
  153. var next = curve.keys[index+1];
  154. inline function addPrevH() {
  155. if(key.prevHandle == null)
  156. key.prevHandle = new hrt.prefab.Curve.CurveHandle(prev != null ? (prev.time - key.time) / 3 : -0.5, 0);
  157. }
  158. inline function addNextH() {
  159. if(key.nextHandle == null)
  160. key.nextHandle = new hrt.prefab.Curve.CurveHandle(next != null ? (next.time - key.time) / 3 : -0.5, 0);
  161. }
  162. switch(key.mode) {
  163. case Aligned:
  164. addPrevH();
  165. addNextH();
  166. var pa = hxd.Math.atan2(key.prevHandle.dv, key.prevHandle.dt);
  167. var na = hxd.Math.atan2(key.nextHandle.dv, key.nextHandle.dt);
  168. if(hxd.Math.abs(hxd.Math.angle(pa - na)) < Math.PI - (1./180.)) {
  169. key.nextHandle.dt = -key.prevHandle.dt;
  170. key.nextHandle.dv = -key.prevHandle.dv;
  171. }
  172. case Free:
  173. addPrevH();
  174. addNextH();
  175. case Linear:
  176. key.nextHandle = null;
  177. key.prevHandle = null;
  178. case Constant:
  179. key.nextHandle = null;
  180. key.prevHandle = null;
  181. }
  182. if(key.time < 0)
  183. key.time = 0;
  184. if(maxLength > 0 && key.time > maxLength)
  185. key.time = maxLength;
  186. if(prev != null && key.time < prev.time)
  187. key.time = prev.time + 0.01;
  188. if(next != null && key.time > next.time)
  189. key.time = next.time - 0.01;
  190. if(minValue < maxValue)
  191. key.value = hxd.Math.clamp(key.value, minValue, maxValue);
  192. if(false) {
  193. // TODO: This sorta works but is annoying.
  194. // Doesn't yet prevent backwards handles
  195. if(next != null && key.nextHandle != null) {
  196. var slope = key.nextHandle.dv / key.nextHandle.dt;
  197. slope = hxd.Math.clamp(slope, -1000, 1000);
  198. if(key.nextHandle.dt + key.time > next.time) {
  199. key.nextHandle.dt = next.time - key.time;
  200. key.nextHandle.dv = slope * key.nextHandle.dt;
  201. }
  202. }
  203. if(prev != null && key.prevHandle != null) {
  204. var slope = key.prevHandle.dv / key.prevHandle.dt;
  205. slope = hxd.Math.clamp(slope, -1000, 1000);
  206. if(key.prevHandle.dt + key.time < prev.time) {
  207. key.prevHandle.dt = prev.time - key.time;
  208. key.prevHandle.dv = slope * key.prevHandle.dt;
  209. }
  210. }
  211. }
  212. }
  213. function startSelectRect(p1x: Float, p1y: Float) {
  214. var offset = element.offset();
  215. var selX = p1x;
  216. var selY = p1y;
  217. var selW = 0.;
  218. var selH = 0.;
  219. startDrag(function(e) {
  220. var p2x = e.clientX - offset.left;
  221. var p2y = e.clientY - offset.top;
  222. selX = hxd.Math.min(p1x, p2x);
  223. selY = hxd.Math.min(p1y, p2y);
  224. selW = hxd.Math.abs(p2x-p1x);
  225. selH = hxd.Math.abs(p2y-p1y);
  226. selectGroup.empty();
  227. svg.rect(selectGroup, selX, selY, selW, selH);
  228. }, function(e) {
  229. selectGroup.empty();
  230. var minT = ixt(selX);
  231. var minV = iyt(selY + selH);
  232. var maxT = ixt(selX + selW);
  233. var maxV = iyt(selY);
  234. selectedKeys = [for(key in curve.keys)
  235. if(key.time >= minT && key.time <= maxT && key.value >= minV && key.value <= maxV) key];
  236. refreshGraph();
  237. });
  238. }
  239. function saveView() {
  240. saveDisplayState("view", {
  241. xOffset: xOffset,
  242. yOffset: yOffset,
  243. xScale: xScale,
  244. yScale: yScale
  245. });
  246. }
  247. function startPan(e) {
  248. var lastX = e.clientX;
  249. var lastY = e.clientY;
  250. startDrag(function(e) {
  251. var dt = (e.clientX - lastX) / xScale;
  252. var dv = (e.clientY - lastY) / yScale;
  253. if(!lockViewX)
  254. xOffset -= dt;
  255. if(!lockViewY)
  256. yOffset += dv;
  257. lastX = e.clientX;
  258. lastY = e.clientY;
  259. setPan(xOffset, yOffset);
  260. }, function(e) {
  261. saveView();
  262. });
  263. }
  264. public function setPan(xoff, yoff) {
  265. xOffset = xoff;
  266. yOffset = yoff;
  267. refreshGrid();
  268. graphGroup.attr({transform: 'translate(${xt(0)},${yt(0)})'});
  269. }
  270. public function setYZoom(yMin: Float, yMax: Float) {
  271. var margin = 20.0;
  272. yScale = (height - margin * 2.0) / (yMax - yMin);
  273. yOffset = (yMax + yMin) * 0.5;
  274. }
  275. public function setXZoom(xMin: Float, xMax: Float) {
  276. var margin = 10.0;
  277. xScale = (width - margin * 2.0) / (xMax - xMin);
  278. xOffset = xMin;
  279. }
  280. public function zoomAll() {
  281. var bounds = curve.getBounds();
  282. if(bounds.width <= 0) {
  283. bounds.xMin = 0.0;
  284. bounds.xMax = 1.0;
  285. }
  286. if(bounds.height <= 0) {
  287. if(minValue < maxValue) {
  288. bounds.yMin = minValue;
  289. bounds.yMax = maxValue;
  290. }
  291. else {
  292. bounds.yMin = -1.0;
  293. bounds.yMax = 1.0;
  294. }
  295. }
  296. if(!lockViewY) {
  297. setYZoom(bounds.yMin, bounds.yMax);
  298. }
  299. if(!lockViewX) {
  300. setXZoom(bounds.xMin, bounds.xMax);
  301. }
  302. saveView();
  303. }
  304. inline function xt(x: Float) return Math.round((x - xOffset) * xScale);
  305. inline function yt(y: Float) return Math.round((-y + yOffset) * yScale + height/2);
  306. inline function ixt(px: Float) return px / xScale + xOffset;
  307. inline function iyt(py: Float) return -(py - height/2) / yScale + yOffset;
  308. function startDrag(onMove: js.jquery.Event->Void, onStop: js.jquery.Event->Void) {
  309. var el = new Element(element[0].ownerDocument.body);
  310. el.on("mousemove.curveeditor", onMove);
  311. el.on("mouseup.curveeditor", function(e: js.jquery.Event) {
  312. el.off("mousemove.curveeditor");
  313. el.off("mouseup.curveeditor");
  314. e.preventDefault();
  315. e.stopPropagation();
  316. onStop(e);
  317. });
  318. }
  319. function copyKey(key: CurveKey): CurveKey {
  320. return cast haxe.Json.parse(haxe.Json.stringify(key));
  321. }
  322. function beforeChange() {
  323. lastValue = haxe.Json.parse(haxe.Json.stringify(curve.save()));
  324. }
  325. function afterChange() {
  326. var newVal = haxe.Json.parse(haxe.Json.stringify(curve.save()));
  327. var oldVal = lastValue;
  328. undo.change(Custom(function(undo) {
  329. if(undo) {
  330. curve.load(oldVal);
  331. }
  332. else {
  333. curve.load(newVal);
  334. }
  335. lastValue = haxe.Json.parse(haxe.Json.stringify(curve.save()));
  336. selectedKeys = [];
  337. refresh();
  338. onChange(false);
  339. }));
  340. refresh();
  341. onChange(false);
  342. }
  343. public function refresh(?anim: Bool) {
  344. refreshGrid();
  345. refreshGraph(anim);
  346. if(!anim)
  347. saveView();
  348. }
  349. public function refreshGrid() {
  350. width = Math.round(svg.element.width());
  351. height = Math.round(svg.element.height());
  352. gridGroup.empty();
  353. var minX = Math.floor(ixt(0));
  354. var maxX = Math.ceil(ixt(width));
  355. var hgrid = svg.group(gridGroup, "hgrid");
  356. for(ix in minX...(maxX+1)) {
  357. var l = svg.line(hgrid, xt(ix), 0, xt(ix), height).attr({
  358. "shape-rendering": "crispEdges"
  359. });
  360. if(ix == 0)
  361. l.addClass("axis");
  362. }
  363. var minY = Math.floor(iyt(height));
  364. var maxY = Math.ceil(iyt(0));
  365. var vgrid = svg.group(gridGroup, "vgrid");
  366. var vstep = 0.1;
  367. while((maxY - minY) / vstep > 20)
  368. vstep *= 10;
  369. inline function hline(iy) {
  370. return svg.line(vgrid, 0, yt(iy), width, yt(iy)).attr({
  371. "shape-rendering": "crispEdges"
  372. });
  373. }
  374. inline function hlabel(str, iy) {
  375. svg.text(vgrid, 1, yt(iy), str);
  376. }
  377. var minS = Math.floor(minY / vstep);
  378. var maxS = Math.ceil(maxY / vstep);
  379. for(i in minS...(maxS+1)) {
  380. var iy = i * vstep;
  381. var l = hline(iy);
  382. if(iy == 0)
  383. l.addClass("axis");
  384. hlabel("" + hxd.Math.fmt(iy), iy);
  385. }
  386. if(maxLength > 0)
  387. svg.rect(gridGroup, xt(maxLength), 0, width - xt(maxLength), height, { opacity: 0.4});
  388. }
  389. public function refreshGraph(?anim: Bool = false, ?animKey: CurveKey) {
  390. if(curve == null)
  391. return;
  392. graphGroup.empty();
  393. var graphOffX = xt(0);
  394. var graphOffY = yt(0);
  395. graphGroup.attr({transform: 'translate($graphOffX, $graphOffY)'});
  396. var curveGroup = svg.group(graphGroup, "curve");
  397. var vectorsGroup = svg.group(graphGroup, "vectors");
  398. var handlesGroup = svg.group(graphGroup, "handles");
  399. var tangentsHandles = svg.group(handlesGroup, "tangents");
  400. var keyHandles = svg.group(handlesGroup, "keys");
  401. var selection = svg.group(graphGroup, "selection");
  402. var size = 7;
  403. // Draw curve
  404. if(curve.keys.length > 0) {
  405. var keys = curve.keys;
  406. if(false) { // Bezier draw, faster but less accurate
  407. var lines = ['M ${xScale*(keys[0].time)},${-yScale*(keys[0].value)}'];
  408. for(ik in 1...keys.length) {
  409. var prev = keys[ik-1];
  410. var cur = keys[ik];
  411. if(prev.mode == Constant) {
  412. lines.push('L ${xScale*(prev.time)} ${-yScale*(prev.value)}
  413. L ${xScale*(cur.time)} ${-yScale*(prev.value)}
  414. L ${xScale*(cur.time)} ${-yScale*(cur.value)}');
  415. }
  416. else {
  417. lines.push('C
  418. ${xScale*(prev.time + (prev.nextHandle != null ? prev.nextHandle.dt : 0.))},${-yScale*(prev.value + (prev.nextHandle != null ? prev.nextHandle.dv : 0.))}
  419. ${xScale*(cur.time + (cur.prevHandle != null ? cur.prevHandle.dt : 0.))}, ${-yScale*(cur.value + (cur.prevHandle != null ? cur.prevHandle.dv : 0.))}
  420. ${xScale*(cur.time)}, ${-yScale*(cur.value)} ');
  421. }
  422. }
  423. svg.make(curveGroup, "path", {d: lines.join("")});
  424. }
  425. else {
  426. var pts = curve.sample(200);
  427. var poly = [];
  428. for(i in 0...pts.length) {
  429. var x = xScale * (curve.duration * i / (pts.length - 1));
  430. var y = yScale * (-pts[i]);
  431. poly.push(new h2d.col.Point(x, y));
  432. }
  433. svg.polygon(curveGroup, poly);
  434. }
  435. }
  436. function addRect(group, x: Float, y: Float) {
  437. return svg.rect(group, x - Math.floor(size/2), y - Math.floor(size/2), size, size).attr({
  438. "shape-rendering": "crispEdges"
  439. });
  440. }
  441. function editPopup(key: CurveKey, top: Float, left: Float) {
  442. var popup = new Element('<div class="keyPopup">
  443. <div class="line"><label>Time</label><input class="x" type="number" value="0" step="0.1"/></div>
  444. <div class="line"><label>Value</label><input class="y" type="number" value="0" step="0.1"/></div>
  445. <div class="line">
  446. <label>Mode</label>
  447. <select>
  448. <option value="0">Aligned</option>
  449. <option value="1">Free</option>
  450. <option value="2">Linear</option>
  451. <option value="3">Constant</option>
  452. </select>
  453. </div>
  454. </div>').appendTo(element);
  455. popup.css({top: top, left: left});
  456. popup.focusout(function(e) {
  457. haxe.Timer.delay(function() {
  458. if(popup.find(':focus').length == 0)
  459. popup.remove();
  460. }, 0);
  461. });
  462. function setMode(m: hrt.prefab.Curve.CurveKeyMode) {
  463. key.mode = m;
  464. curve.keyMode = m;
  465. fixKey(key);
  466. refreshGraph();
  467. }
  468. var select = popup.find("select");
  469. select.val(Std.string(key.mode));
  470. select.change(function(val) {
  471. setMode(cast Std.parseInt(select.val()));
  472. });
  473. function afterEdit() {
  474. refreshGraph(false);
  475. onChange(false);
  476. }
  477. var xel = popup.find(".x");
  478. xel.val(hxd.Math.fmt(key.time));
  479. xel.change(function(e) {
  480. var f = Std.parseFloat(xel.val());
  481. if(f != null) {
  482. undo.change(Field(key, "time", key.time), afterEdit);
  483. key.time = f;
  484. afterEdit();
  485. }
  486. });
  487. var yel = popup.find(".y");
  488. yel.val(hxd.Math.fmt(key.value));
  489. yel.change(function(e) {
  490. var f = Std.parseFloat(yel.val());
  491. if(f != null) {
  492. undo.change(Field(key, "value", key.value), afterEdit);
  493. key.value = f;
  494. afterEdit();
  495. }
  496. });
  497. popup.find("input").first().focus();
  498. popup.focus();
  499. return popup;
  500. }
  501. for(key in curve.previewKeys) {
  502. var kx = xScale*(key.time);
  503. var ky = -yScale*(key.value);
  504. var keyHandle = addRect(keyHandles, kx, ky);
  505. keyHandle.addClass("preview");
  506. }
  507. for(key in curve.keys) {
  508. var kx = xScale*(key.time);
  509. var ky = -yScale*(key.value);
  510. var keyHandle = addRect(keyHandles, kx, ky);
  511. var selected = selectedKeys.indexOf(key) >= 0;
  512. if(selected)
  513. keyHandle.addClass("selected");
  514. if(!anim) {
  515. keyHandle.mousedown(function(e) {
  516. if(e.which != 1) return;
  517. e.preventDefault();
  518. e.stopPropagation();
  519. var offset = element.offset();
  520. beforeChange();
  521. var startT = key.time;
  522. var startV = key.value;
  523. startDrag(function(e) {
  524. var lx = e.clientX - offset.left;
  525. var ly = e.clientY - offset.top;
  526. var nkx = ixt(lx);
  527. var nky = iyt(ly);
  528. var prevTime = key.time;
  529. var prevVal = key.value;
  530. key.time = nkx;
  531. key.value = nky;
  532. if(e.ctrlKey) {
  533. key.time = Math.round(key.time * 10) / 10.;
  534. key.value = Math.round(key.value * 10) / 10.;
  535. }
  536. if(lockKeyX || e.shiftKey)
  537. key.time = startT;
  538. if(e.altKey)
  539. key.value = startV;
  540. fixKey(key);
  541. refreshGraph(true, key);
  542. onKeyMove(key, prevTime, prevVal);
  543. onChange(true);
  544. }, function(e) {
  545. selectedKeys = [key];
  546. fixKey(key);
  547. afterChange();
  548. });
  549. selectedKeys = [key];
  550. refreshGraph();
  551. });
  552. keyHandle.contextmenu(function(e) {
  553. var offset = element.offset();
  554. var popup = editPopup(key, e.clientY - offset.top - 50, e.clientX - offset.left);
  555. e.preventDefault();
  556. return false;
  557. });
  558. }
  559. function addHandle(next: Bool) {
  560. var handle = next ? key.nextHandle : key.prevHandle;
  561. var other = next ? key.prevHandle : key.nextHandle;
  562. if(handle == null) return null;
  563. var px = xScale*(key.time + handle.dt);
  564. var py = -yScale*(key.value + handle.dv);
  565. var line = svg.line(vectorsGroup, kx, ky, px, py);
  566. var circle = svg.circle(tangentsHandles, px, py, size/2);
  567. if(selected) {
  568. line.addClass("selected");
  569. circle.addClass("selected");
  570. }
  571. if(anim)
  572. return circle;
  573. circle.mousedown(function(e) {
  574. if(e.which != 1) return;
  575. e.preventDefault();
  576. e.stopPropagation();
  577. var offset = element.offset();
  578. var otherLen = hxd.Math.distance(other.dt * xScale, other.dv * yScale);
  579. beforeChange();
  580. startDrag(function(e) {
  581. var lx = e.clientX - offset.left;
  582. var ly = e.clientY - offset.top;
  583. var abskx = xt(key.time);
  584. var absky = yt(key.value);
  585. if(next && lx < abskx || !next && lx > abskx)
  586. lx = kx;
  587. var ndt = ixt(lx) - key.time;
  588. var ndv = iyt(ly) - key.value;
  589. handle.dt = ndt;
  590. handle.dv = ndv;
  591. if(key.mode == Aligned) {
  592. var angle = Math.atan2(absky - ly, lx - abskx);
  593. other.dt = Math.cos(angle + Math.PI) * otherLen / xScale;
  594. other.dv = Math.sin(angle + Math.PI) * otherLen / yScale;
  595. }
  596. fixKey(key);
  597. refreshGraph(true, key);
  598. onChange(true);
  599. }, function(e) {
  600. afterChange();
  601. });
  602. });
  603. return circle;
  604. }
  605. if(!anim || animKey == key) {
  606. var pHandle = addHandle(false);
  607. var nHandle = addHandle(true);
  608. }
  609. }
  610. if(selectedKeys.length > 1) {
  611. var bounds = new h2d.col.Bounds();
  612. for(key in selectedKeys)
  613. bounds.addPoint(new h2d.col.Point(xScale*(key.time), -yScale*(key.value)));
  614. var margin = 12.5;
  615. bounds.xMin -= margin;
  616. bounds.yMin -= margin;
  617. bounds.xMax += margin;
  618. bounds.yMax += margin;
  619. var rect = svg.rect(selection, bounds.x, bounds.y, bounds.width, bounds.height).attr({
  620. "shape-rendering": "crispEdges"
  621. });
  622. if(!anim) {
  623. beforeChange();
  624. rect.mousedown(function(e) {
  625. if(e.which != 1) return;
  626. e.preventDefault();
  627. e.stopPropagation();
  628. var deltaX = 0;
  629. var deltaY = 0;
  630. var lastX = e.clientX;
  631. var lastY = e.clientY;
  632. startDrag(function(e) {
  633. var dx = e.clientX - lastX;
  634. var dy = e.clientY - lastY;
  635. if(lockKeyX || e.shiftKey)
  636. dx = 0;
  637. if(e.altKey)
  638. dy = 0;
  639. for(key in selectedKeys) {
  640. key.time += dx / xScale;
  641. if(lockKeyX || e.shiftKey)
  642. key.time -= deltaX / xScale;
  643. key.value -= dy / yScale;
  644. if(e.altKey)
  645. key.value += deltaY / yScale;
  646. }
  647. deltaX += dx;
  648. deltaY += dy;
  649. if(lockKeyX || e.shiftKey) {
  650. lastX -= deltaX;
  651. deltaX = 0;
  652. }
  653. else
  654. lastX = e.clientX;
  655. if(e.altKey) {
  656. lastY -= deltaY;
  657. deltaY = 0;
  658. }
  659. else
  660. lastY = e.clientY;
  661. refreshGraph(true);
  662. onChange(true);
  663. }, function(e) {
  664. afterChange();
  665. });
  666. refreshGraph();
  667. });
  668. }
  669. }
  670. }
  671. }