tools.js 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518
  1. /*
  2. Pixel Studio - tool framework and built-in tools
  3. Left toolbar, options bar, pointer event pipeline, brush engine,
  4. shape drawing, selection tools, move / zoom / hand.
  5. */
  6. "use strict";
  7. /* ---------- default tool options (persisted in prefs) ---------- */
  8. PS.toolOpts = {
  9. brush: { size: 24, hardness: 0.8, opacity: 1, flow: 0.7, type: "round" },
  10. pencil: { size: 3, opacity: 1 },
  11. eraser: { size: 30, opacity: 1, type: "round" },
  12. fill: { tolerance: 32, contiguous: true },
  13. gradient: { preset: "fg-bg", style: "linear", reverse: false, opacity: 1, stops: [] },
  14. wand: { tolerance: 32, contiguous: true, smart: true, edgeThreshold: 60 },
  15. marquee: { feather: 0 },
  16. move: { showBounds: false },
  17. shape: { kind: "rect", mode: "both", strokeWidth: 6, radius: 12, points: 5 },
  18. text: { font: "Arial", size: 48, bold: false, italic: false },
  19. zoom: {}
  20. };
  21. /* ---------- toolbar grouping (fly-out submenus) ---------- */
  22. // Toolbar entries: single tools, tool groups (one button + right-click
  23. // fly-out), and the shape picker (fly-out chooses the shape kind visually).
  24. PS.toolbarLayout = [
  25. { kind: "single", tool: "move" },
  26. { kind: "group", id: "select", tools: ["marquee-rect", "marquee-ellipse", "lasso", "lasso-poly"] },
  27. { kind: "single", tool: "wand" },
  28. { kind: "group", id: "paint", tools: ["brush", "pencil"] },
  29. { kind: "single", tool: "eraser" },
  30. { kind: "group", id: "bucket", tools: ["fill", "gradient"] },
  31. { kind: "single", tool: "eyedropper" },
  32. { kind: "single", tool: "text" },
  33. { kind: "shape" },
  34. { kind: "single", tool: "hand" },
  35. { kind: "single", tool: "zoom" }
  36. ];
  37. // Last-selected member shown on each group's toolbar button.
  38. PS.groupRep = { select: "marquee-rect", paint: "brush", bucket: "fill" };
  39. // Per-shape-kind icons for the shape fly-out and toolbar button.
  40. PS.shapeIcons = {
  41. rect: '<svg viewBox="0 0 24 24" stroke-width="1.6"><rect x="4" y="6" width="16" height="12"/></svg>',
  42. rounded: '<svg viewBox="0 0 24 24" stroke-width="1.6"><rect x="4" y="6" width="16" height="12" rx="3.5"/></svg>',
  43. ellipse: '<svg viewBox="0 0 24 24" stroke-width="1.6"><ellipse cx="12" cy="12" rx="8" ry="6"/></svg>',
  44. line: '<svg viewBox="0 0 24 24" stroke-width="1.6"><path d="M5 19 19 5"/></svg>',
  45. arrow: '<svg viewBox="0 0 24 24" stroke-width="1.6"><path d="M4 20 20 4M20 4h-6M20 4v6"/></svg>',
  46. triangle: '<svg viewBox="0 0 24 24" stroke-width="1.6"><path d="M12 5 20 19H4z"/></svg>',
  47. star: '<svg viewBox="0 0 24 24" stroke-width="1.6"><path d="M12 3.5l2.5 5.6 6.1.6-4.6 4 1.4 6-5.4-3.2L6.1 19.7l1.4-6L2.9 9.7l6.1-.6z"/></svg>'
  48. };
  49. /* ---------- framework ---------- */
  50. PS.registerTool = function (id, def) {
  51. def.id = id;
  52. PS.tools[id] = def;
  53. };
  54. PS.setTool = function (id) {
  55. if (!PS.tools[id]) { return; }
  56. if (PS.commitTextEdit) { PS.commitTextEdit(); }
  57. PS.closeToolFlyout();
  58. var old = PS.tools[PS.tool];
  59. if (old && old.deactivate) { old.deactivate(); }
  60. PS.tool = id;
  61. // remember this tool as its toolbar group's representative
  62. PS.toolbarLayout.forEach(function (entry) {
  63. if (entry.kind === "group" && entry.tools.indexOf(id) >= 0) {
  64. PS.groupRep[entry.id] = id;
  65. }
  66. });
  67. PS.renderToolbar();
  68. PS.renderOptionsBar();
  69. var ws = PS.el("workspace");
  70. ws.style.cursor = PS.tools[id].cursor || "crosshair";
  71. PS.savePrefsDebounced();
  72. };
  73. PS.renderToolbar = function () {
  74. var host = PS.el("toolbar-buttons");
  75. host.innerHTML = "";
  76. PS.toolbarLayout.forEach(function (entry) {
  77. if (entry.kind === "single") {
  78. host.appendChild(PS._singleToolBtn(entry.tool));
  79. } else if (entry.kind === "group") {
  80. host.appendChild(PS._groupToolBtn(entry));
  81. } else if (entry.kind === "shape") {
  82. host.appendChild(PS._shapeToolBtn());
  83. }
  84. });
  85. };
  86. // build the base toolbar button (icon, active state, optional fly-out triangle)
  87. PS._toolBtn = function (icon, title, active, hasFlyout) {
  88. var btn = document.createElement("button");
  89. btn.className = "tool-btn" + (active ? " active" : "");
  90. btn.title = title;
  91. btn.innerHTML = icon;
  92. if (hasFlyout) {
  93. var tri = document.createElement("span");
  94. tri.className = "flyout-tri";
  95. btn.appendChild(tri);
  96. }
  97. return btn;
  98. };
  99. PS._singleToolBtn = function (id) {
  100. var def = PS.tools[id];
  101. var btn = PS._toolBtn(def.icon,
  102. def.name + (def.key ? " (" + def.key.toUpperCase() + ")" : ""),
  103. PS.tool === id, false);
  104. btn.addEventListener("click", function () { PS.setTool(id); });
  105. return btn;
  106. };
  107. PS._groupToolBtn = function (entry) {
  108. var inGroup = entry.tools.indexOf(PS.tool) >= 0;
  109. var rep = inGroup ? PS.tool : PS.groupRep[entry.id];
  110. if (entry.tools.indexOf(rep) < 0) { rep = entry.tools[0]; }
  111. var def = PS.tools[rep];
  112. var btn = PS._toolBtn(def.icon,
  113. def.name + " — right-click for more", inGroup, true);
  114. btn.addEventListener("click", function () { PS.setTool(rep); });
  115. btn.addEventListener("contextmenu", function (e) {
  116. e.preventDefault();
  117. PS.openToolFlyout(btn, entry.tools.map(function (t) {
  118. var d = PS.tools[t];
  119. return {
  120. icon: d.icon, label: d.name, active: PS.tool === t,
  121. onSelect: function () { PS.setTool(t); }
  122. };
  123. }));
  124. });
  125. return btn;
  126. };
  127. PS._shapeToolBtn = function () {
  128. var kind = PS.toolOpts.shape.kind;
  129. var icon = PS.shapeIcons[kind] || PS.tools.shape.icon;
  130. var btn = PS._toolBtn(icon, "Shape — right-click to pick a shape",
  131. PS.tool === "shape", true);
  132. btn.addEventListener("click", function () { PS.setTool("shape"); });
  133. btn.addEventListener("contextmenu", function (e) {
  134. e.preventDefault();
  135. PS.openToolFlyout(btn, PS.shapeKinds.map(function (k) {
  136. return {
  137. icon: PS.shapeIcons[k.v] || PS.tools.shape.icon,
  138. label: k.l,
  139. active: PS.tool === "shape" && PS.toolOpts.shape.kind === k.v,
  140. onSelect: function () {
  141. PS.toolOpts.shape.kind = k.v;
  142. PS.setTool("shape");
  143. PS.savePrefsDebounced();
  144. }
  145. };
  146. }));
  147. });
  148. return btn;
  149. };
  150. /* ---------- tool fly-out submenu ---------- */
  151. PS._toolFlyout = null;
  152. PS.openToolFlyout = function (btn, items) {
  153. PS.closeToolFlyout();
  154. var fly = document.createElement("div");
  155. fly.className = "tool-flyout";
  156. items.forEach(function (it) {
  157. var row = document.createElement("div");
  158. row.className = "tool-flyout-item" + (it.active ? " active" : "");
  159. var ic = document.createElement("span");
  160. ic.className = "tfi-icon";
  161. ic.innerHTML = it.icon;
  162. var lb = document.createElement("span");
  163. lb.className = "tfi-label";
  164. lb.textContent = it.label;
  165. row.appendChild(ic);
  166. row.appendChild(lb);
  167. row.addEventListener("click", function (e) {
  168. e.stopPropagation();
  169. PS.closeToolFlyout();
  170. it.onSelect();
  171. });
  172. fly.appendChild(row);
  173. });
  174. document.body.appendChild(fly);
  175. // open to the right of the button, top edge aligned with the button top
  176. var r = btn.getBoundingClientRect();
  177. fly.style.left = Math.round(r.right + 2) + "px";
  178. fly.style.top = Math.round(r.top) + "px";
  179. var fr = fly.getBoundingClientRect();
  180. if (fr.bottom > window.innerHeight - 4) {
  181. fly.style.top = Math.max(4, window.innerHeight - fr.height - 4) + "px";
  182. }
  183. PS._toolFlyout = fly;
  184. setTimeout(function () {
  185. document.addEventListener("pointerdown", PS._flyoutOutside, true);
  186. document.addEventListener("keydown", PS._flyoutKey, true);
  187. }, 0);
  188. };
  189. PS.closeToolFlyout = function () {
  190. if (PS._toolFlyout) { PS._toolFlyout.remove(); PS._toolFlyout = null; }
  191. document.removeEventListener("pointerdown", PS._flyoutOutside, true);
  192. document.removeEventListener("keydown", PS._flyoutKey, true);
  193. };
  194. PS._flyoutOutside = function (e) {
  195. if (PS._toolFlyout && !PS._toolFlyout.contains(e.target)) { PS.closeToolFlyout(); }
  196. };
  197. PS._flyoutKey = function (e) {
  198. if (e.key === "Escape") { PS.closeToolFlyout(); }
  199. };
  200. PS.renderOptionsBar = function () {
  201. var host = PS.el("optionsbar");
  202. host.innerHTML = "";
  203. var def = PS.tools[PS.tool];
  204. if (!def) { return; }
  205. var name = document.createElement("span");
  206. name.className = "tool-name";
  207. name.textContent = def.name;
  208. host.appendChild(name);
  209. if (def.options) { def.options(host); }
  210. };
  211. /* ---------- pointer event pipeline ---------- */
  212. PS._pointer = { down: false, panning: false, panStart: null };
  213. PS.bindWorkspaceEvents = function () {
  214. var ws = PS.el("workspace");
  215. ws.addEventListener("contextmenu", function (e) { e.preventDefault(); });
  216. ws.addEventListener("pointerdown", function (e) {
  217. if (!PS.doc) { return; }
  218. if (e.button === 2) { return; }
  219. // ignore presses on the workspace scrollbars
  220. var wsRect = ws.getBoundingClientRect();
  221. if (e.clientX - wsRect.left > ws.clientWidth ||
  222. e.clientY - wsRect.top > ws.clientHeight) {
  223. return;
  224. }
  225. ws.setPointerCapture(e.pointerId);
  226. PS._pointer.down = true;
  227. if (e.button === 1 || PS.spacePan || PS.tool === "hand") {
  228. PS._pointer.panning = true;
  229. PS._pointer.panStart = {
  230. x: e.clientX, y: e.clientY,
  231. sl: ws.scrollLeft, st: ws.scrollTop
  232. };
  233. ws.style.cursor = "grabbing";
  234. e.preventDefault();
  235. return;
  236. }
  237. var raw = PS.eventToDoc(e);
  238. var pt = PS.snapDocPoint(raw);
  239. if (PS.selTransform.onDown(pt, e)) {
  240. e.preventDefault();
  241. return;
  242. }
  243. // grab an existing guide (Move tool) before handing off to the tool
  244. if (PS.guideDragStart(raw)) {
  245. e.preventDefault();
  246. return;
  247. }
  248. var def = PS.tools[PS.tool];
  249. if (def && def.onDown) { def.onDown(pt, e); }
  250. e.preventDefault();
  251. });
  252. ws.addEventListener("pointermove", function (e) {
  253. if (!PS.doc) { return; }
  254. var raw = PS.eventToDoc(e);
  255. PS.cursorPos = PS.snapDocPoint(raw);
  256. PS.updateCursorStatus();
  257. if (PS._pointer.panning) {
  258. var p = PS._pointer.panStart;
  259. ws.scrollLeft = p.sl - (e.clientX - p.x);
  260. ws.scrollTop = p.st - (e.clientY - p.y);
  261. return;
  262. }
  263. if (PS.guidesDragging()) {
  264. PS.guideDragMove(raw);
  265. return;
  266. }
  267. if (PS.selTransform.dragging) {
  268. PS.selTransform.onMove(PS.cursorPos);
  269. return;
  270. }
  271. // Update cursor for handle / guide hover (only when not mid-stroke/drag)
  272. if (!PS._pointer.down) {
  273. var tCursor = PS.selTransform.getCursor(PS.cursorPos) || PS.moveBoundsCursor(PS.cursorPos);
  274. if (!tCursor) {
  275. var gh = PS.guideHitTest(raw);
  276. if (gh) { tCursor = (gh.orient === "h") ? "row-resize" : "col-resize"; }
  277. }
  278. ws.style.cursor = tCursor || (PS.tools[PS.tool] || {}).cursor || "crosshair";
  279. }
  280. var def = PS.tools[PS.tool];
  281. if (def && def.onMove) { def.onMove(PS.cursorPos, e); }
  282. });
  283. function finish(e) {
  284. if (!PS.doc) { return; }
  285. if (PS._pointer.panning) {
  286. PS._pointer.panning = false;
  287. ws.style.cursor = (PS.tools[PS.tool] || {}).cursor || "crosshair";
  288. } else if (PS.guidesDragging()) {
  289. PS.guideDragEnd(PS.eventToDoc(e));
  290. ws.style.cursor = (PS.tools[PS.tool] || {}).cursor || "crosshair";
  291. } else if (PS.selTransform.dragging) {
  292. PS.selTransform.onUp();
  293. ws.style.cursor = (PS.tools[PS.tool] || {}).cursor || "crosshair";
  294. } else if (PS._pointer.down) {
  295. var def = PS.tools[PS.tool];
  296. if (def && def.onUp) { def.onUp(PS.snapDocPoint(PS.eventToDoc(e)), e); }
  297. }
  298. PS._pointer.down = false;
  299. }
  300. ws.addEventListener("pointerup", finish);
  301. ws.addEventListener("pointercancel", finish);
  302. ws.addEventListener("dblclick", function (e) {
  303. var def = PS.tools[PS.tool];
  304. if (def && def.onDblClick) { def.onDblClick(PS.eventToDoc(e), e); }
  305. });
  306. // Ctrl+wheel zoom at pointer, plain wheel scrolls (default)
  307. ws.addEventListener("wheel", function (e) {
  308. if (!PS.doc) { return; }
  309. if (e.ctrlKey) {
  310. e.preventDefault();
  311. var pt = PS.eventToDoc(e);
  312. PS.setZoom(PS.zoom * (e.deltaY < 0 ? 1.15 : 1 / 1.15), pt);
  313. }
  314. }, { passive: false });
  315. ws.addEventListener("pointerleave", function () {
  316. PS.cursorPos = null;
  317. PS.updateCursorStatus();
  318. });
  319. };
  320. /* ---------- shared helpers ---------- */
  321. // selection combine mode from modifier keys
  322. PS.selModeFromEvent = function (e) {
  323. if (e.shiftKey && e.altKey) { return "intersect"; }
  324. if (e.shiftKey) { return "add"; }
  325. if (e.altKey) { return "subtract"; }
  326. return "replace";
  327. };
  328. PS.requirePaintableLayer = function () {
  329. var layer = PS.activeLayer();
  330. if (!layer) { return null; }
  331. if (layer.type === "text") {
  332. PS.toast("Text layer: rasterize it first (Layer menu) to paint on it", true);
  333. return null;
  334. }
  335. if (!layer.visible) {
  336. PS.toast("Layer is hidden", true);
  337. return null;
  338. }
  339. return layer;
  340. };
  341. PS.sampleColorAt = function (pt, comp, toBg) {
  342. var x = Math.floor(pt.x), y = Math.floor(pt.y);
  343. if (x < 0 || y < 0 || x >= PS.doc.width || y >= PS.doc.height) { return; }
  344. var d = comp.getContext("2d").getImageData(x, y, 1, 1).data;
  345. if (d[3] === 0) { return; }
  346. var hex = PS.rgbToHex(d[0], d[1], d[2], d[3]);
  347. if (toBg) { PS.setBg(hex); } else { PS.setFg(hex); }
  348. };
  349. // draw helper used by tool overlays: transform overlay ctx into doc space
  350. PS.overlayDocSpace = function (ctx, fn) {
  351. var origin = PS.docToOverlay(0, 0);
  352. ctx.save();
  353. ctx.translate(origin.x, origin.y);
  354. ctx.scale(PS.zoom, PS.zoom);
  355. fn(1 / PS.zoom); // pass screen-pixel size in doc units
  356. ctx.restore();
  357. };
  358. /* ============================================================
  359. BRUSH ENGINE (brush / pencil / eraser)
  360. Stamps are accumulated at full opacity into a stroke buffer which
  361. is composited onto the layer with the stroke opacity on release,
  362. clipped by the selection. This gives Photoshop-style opacity
  363. semantics (self-overlapping strokes do not darken).
  364. ============================================================ */
  365. PS.strokeTypes = [
  366. { v: "round", l: "Round" },
  367. { v: "soft", l: "Soft Round" },
  368. { v: "calligraphy", l: "Calligraphy" },
  369. { v: "marker", l: "Marker" },
  370. { v: "spray", l: "Airbrush / Spray" }
  371. ];
  372. PS._stroke = null;
  373. PS.beginStroke = function (kind, pt, e) {
  374. if (e.altKey && kind !== "eraser") {
  375. PS.sampleColorAt(pt, PS.compositeToCanvas(), false);
  376. return;
  377. }
  378. var layer = PS.requirePaintableLayer();
  379. if (!layer) { return; }
  380. var opts = PS.toolOpts[kind];
  381. // Stamps are drawn opaque into the buffer; the foreground color's alpha is
  382. // applied (with the stroke opacity) at composite time so self-overlap
  383. // within a stroke does not darken.
  384. var rgb = PS.hexToRgb(PS.fg) || { r: 0, g: 0, b: 0, a: 255 };
  385. var colorAlpha = (kind === "eraser") ? 1 : (rgb.a === undefined ? 1 : rgb.a / 255);
  386. PS._stroke = {
  387. kind: kind,
  388. layer: layer,
  389. opts: opts,
  390. color: (kind === "eraser") ? "#000000" : PS.rgbToHex(rgb.r, rgb.g, rgb.b),
  391. colorAlpha: colorAlpha,
  392. before: PS.snapshotLayer(layer),
  393. canvas: PS.createCanvas(PS.doc.width, PS.doc.height),
  394. last: pt,
  395. rest: 0
  396. };
  397. PS._stroke.ctx = PS._stroke.canvas.getContext("2d");
  398. PS.strokePreview = {
  399. layer: layer,
  400. canvas: PS._stroke.canvas,
  401. opacity: opts.opacity * colorAlpha,
  402. erase: kind === "eraser"
  403. };
  404. PS.stampSegment(pt, pt);
  405. PS.requestRender();
  406. };
  407. PS.continueStroke = function (pt) {
  408. if (!PS._stroke) { return; }
  409. PS.stampSegment(PS._stroke.last, pt);
  410. PS._stroke.last = pt;
  411. PS.requestRender();
  412. };
  413. PS.endStroke = function () {
  414. var s = PS._stroke;
  415. if (!s) { return; }
  416. var buf = s.canvas;
  417. if (PS.doc.selection) {
  418. var masked = PS.cloneCanvas(buf);
  419. var mctx = masked.getContext("2d");
  420. mctx.globalCompositeOperation = "destination-in";
  421. mctx.drawImage(PS.doc.selection.mask, 0, 0);
  422. buf = masked;
  423. }
  424. var ctx = s.layer.canvas.getContext("2d");
  425. ctx.globalAlpha = s.opts.opacity * (s.colorAlpha === undefined ? 1 : s.colorAlpha);
  426. ctx.globalCompositeOperation = (s.kind === "eraser") ? "destination-out" : "source-over";
  427. ctx.drawImage(buf, 0, 0);
  428. ctx.globalAlpha = 1;
  429. ctx.globalCompositeOperation = "source-over";
  430. PS.strokePreview = null;
  431. PS._stroke = null;
  432. var labels = { brush: "Brush Stroke", pencil: "Pencil", eraser: "Eraser" };
  433. PS.commitLayerCanvas(labels[s.kind], s.layer, s.before);
  434. PS.requestRender();
  435. };
  436. PS.stampSegment = function (from, to) {
  437. var s = PS._stroke;
  438. var opts = s.opts;
  439. var ctx = s.ctx;
  440. var size = opts.size;
  441. var color = s.color;
  442. if (s.kind === "pencil") {
  443. // crisp pixel stamps along the line
  444. ctx.fillStyle = color;
  445. var dist = Math.hypot(to.x - from.x, to.y - from.y);
  446. var steps = Math.max(1, Math.ceil(dist));
  447. for (var i = 0; i <= steps; i++) {
  448. var t = i / steps;
  449. var x = from.x + (to.x - from.x) * t;
  450. var y = from.y + (to.y - from.y) * t;
  451. ctx.fillRect(Math.round(x - size / 2), Math.round(y - size / 2), size, size);
  452. }
  453. return;
  454. }
  455. var type = (s.kind === "eraser") ? opts.type : opts.type;
  456. if (type === "round") {
  457. // continuous segments give the smoothest hard-round result
  458. ctx.strokeStyle = color;
  459. ctx.fillStyle = color;
  460. ctx.lineWidth = size;
  461. ctx.lineCap = "round";
  462. ctx.lineJoin = "round";
  463. if (from.x === to.x && from.y === to.y) {
  464. ctx.beginPath();
  465. ctx.arc(to.x, to.y, size / 2, 0, Math.PI * 2);
  466. ctx.fill();
  467. } else {
  468. ctx.beginPath();
  469. ctx.moveTo(from.x, from.y);
  470. ctx.lineTo(to.x, to.y);
  471. ctx.stroke();
  472. }
  473. return;
  474. }
  475. // spaced stamps for textured stroke types
  476. var spacingByType = { soft: 0.15, calligraphy: 0.08, marker: 0.25, spray: 0.3 };
  477. var spacing = Math.max(1, size * (spacingByType[type] || 0.2));
  478. var dx = to.x - from.x, dy = to.y - from.y;
  479. var dist2 = Math.hypot(dx, dy);
  480. if (dist2 === 0) {
  481. PS.stampAt(ctx, to.x, to.y, type, size, color, opts);
  482. return;
  483. }
  484. var travelled = s.rest;
  485. while (travelled <= dist2) {
  486. var tt = travelled / dist2;
  487. PS.stampAt(ctx, from.x + dx * tt, from.y + dy * tt, type, size, color, opts);
  488. travelled += spacing;
  489. }
  490. s.rest = travelled - dist2;
  491. };
  492. PS.stampAt = function (ctx, x, y, type, size, color, opts) {
  493. var r = size / 2;
  494. var rgb = PS.hexToRgb(color) || { r: 0, g: 0, b: 0 };
  495. if (type === "soft") {
  496. var g = ctx.createRadialGradient(x, y, 0, x, y, r);
  497. var solid = "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + ",1)";
  498. var clear = "rgba(" + rgb.r + "," + rgb.g + "," + rgb.b + ",0)";
  499. var hard = PS.clamp(opts.hardness !== undefined ? opts.hardness : 0.5, 0, 0.99);
  500. g.addColorStop(0, solid);
  501. g.addColorStop(hard, solid);
  502. g.addColorStop(1, clear);
  503. ctx.globalAlpha = opts.flow !== undefined ? opts.flow : 0.6;
  504. ctx.fillStyle = g;
  505. ctx.beginPath();
  506. ctx.arc(x, y, r, 0, Math.PI * 2);
  507. ctx.fill();
  508. ctx.globalAlpha = 1;
  509. } else if (type === "calligraphy") {
  510. ctx.save();
  511. ctx.translate(x, y);
  512. ctx.rotate(-Math.PI / 4);
  513. ctx.scale(1, 0.3);
  514. ctx.fillStyle = color;
  515. ctx.beginPath();
  516. ctx.arc(0, 0, r, 0, Math.PI * 2);
  517. ctx.fill();
  518. ctx.restore();
  519. } else if (type === "marker") {
  520. ctx.globalAlpha = (opts.flow !== undefined ? opts.flow : 0.6) * 0.6;
  521. ctx.fillStyle = color;
  522. ctx.fillRect(x - r, y - r, size, size);
  523. ctx.globalAlpha = 1;
  524. } else if (type === "spray") {
  525. ctx.fillStyle = color;
  526. var dots = Math.max(6, Math.round(size * 0.8));
  527. for (var i = 0; i < dots; i++) {
  528. var ang = Math.random() * Math.PI * 2;
  529. var rad = Math.sqrt(Math.random()) * r;
  530. var dr = 0.5 + Math.random();
  531. ctx.globalAlpha = 0.25;
  532. ctx.beginPath();
  533. ctx.arc(x + Math.cos(ang) * rad, y + Math.sin(ang) * rad, dr, 0, Math.PI * 2);
  534. ctx.fill();
  535. }
  536. ctx.globalAlpha = 1;
  537. }
  538. };
  539. // circular size cursor for paint tools
  540. PS.paintCursorOverlay = function (size, square) {
  541. return function (ctx) {
  542. if (!PS.cursorPos || PS._pointer.panning) { return; }
  543. var p = PS.docToOverlay(PS.cursorPos.x, PS.cursorPos.y);
  544. var r = (typeof size === "function" ? size() : size) * PS.zoom / 2;
  545. ctx.strokeStyle = "rgba(255,255,255,0.85)";
  546. ctx.lineWidth = 1;
  547. ctx.beginPath();
  548. if (square) {
  549. ctx.rect(p.x - r, p.y - r, r * 2, r * 2);
  550. } else {
  551. ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
  552. }
  553. ctx.stroke();
  554. ctx.strokeStyle = "rgba(0,0,0,0.6)";
  555. ctx.beginPath();
  556. if (square) {
  557. ctx.rect(p.x - r - 1, p.y - r - 1, r * 2 + 2, r * 2 + 2);
  558. } else {
  559. ctx.arc(p.x, p.y, r + 1, 0, Math.PI * 2);
  560. }
  561. ctx.stroke();
  562. };
  563. };
  564. /* ============================================================
  565. TOOL DEFINITIONS
  566. ============================================================ */
  567. /* ----- Move (V) ----- */
  568. (function () {
  569. var drag = null;
  570. var HANDLE_PX = 8; // corner handle square size, screen pixels
  571. var PAD_PX = 6; // gap between content bounds and the drawn box, screen pixels
  572. // matches the diagonal resize cursors selTransform uses for its own
  573. // corner handles, so the affordance reads the same even though clicking
  574. // here switches tools instead of resizing in place
  575. var CORNER_CURSOR = { nw: "nwse-resize", ne: "nesw-resize", se: "nwse-resize", sw: "nesw-resize" };
  576. // 4 corner positions (doc coords) of a content-bounds box, padded the
  577. // same way the box itself is drawn
  578. function cornerPositions(b) {
  579. var pad = PAD_PX / PS.zoom;
  580. var x = b.x - pad, y = b.y - pad, r = b.x + b.w + pad, bot = b.y + b.h + pad;
  581. return { nw: { x: x, y: y }, ne: { x: r, y: y }, se: { x: r, y: bot }, sw: { x: x, y: bot } };
  582. }
  583. // returns the corner id under doc point pt, or null
  584. function hitCorner(pt, b) {
  585. var corners = cornerPositions(b);
  586. var hitR = (HANDLE_PX / 2 + 3) / PS.zoom;
  587. for (var id in corners) {
  588. var c = corners[id];
  589. if (Math.abs(pt.x - c.x) <= hitR && Math.abs(pt.y - c.y) <= hitR) { return id; }
  590. }
  591. return null;
  592. }
  593. // cursor hint for the central pointer pipeline (hover, not dragging)
  594. PS.moveBoundsCursor = function (pt) {
  595. if (!pt || PS.tool !== "move" || !PS.toolOpts.move.showBounds) { return null; }
  596. var layer = PS.activeLayer();
  597. var b = layer && PS.layerContentBounds(layer);
  598. var id = b && hitCorner(pt, b);
  599. return id ? CORNER_CURSOR[id] : null;
  600. };
  601. PS.registerTool("move", {
  602. name: "Move",
  603. key: "v",
  604. cursor: "move",
  605. icon: '<svg viewBox="0 0 24 24" stroke-width="1.6"><path d="M12 2v20M2 12h20M12 2l-3 3M12 2l3 3M12 22l-3-3M12 22l3-3M2 12l3-3M2 12l3 3M22 12l-3-3M22 12l-3 3"/></svg>',
  606. options: function (host) {
  607. var o = PS.toolOpts.move;
  608. PS.ui.checkbox(host, "Show selection box", o.showBounds, function (v) {
  609. o.showBounds = v;
  610. PS.savePrefsDebounced();
  611. });
  612. PS.ui.label(host, "Drag to move the active layer; with a selection, drags the selected pixels. Arrow keys nudge. " +
  613. "With the box shown, click a corner to switch to the rectangular marquee.");
  614. },
  615. onDown: function (pt, e) {
  616. var layer = PS.activeLayer();
  617. if (!layer) { return; }
  618. if (PS.toolOpts.move.showBounds) {
  619. var cb = PS.layerContentBounds(layer);
  620. var corner = cb && hitCorner(pt, cb);
  621. if (corner) {
  622. // Switch to the rectangular marquee and immediately begin a
  623. // resize-handle drag, so dragging the corner *scales the
  624. // layer content* (not just draws a new selection). We seed a
  625. // selection over the content bounds so the transform engine
  626. // has a handle at the clicked corner to grab; the scale runs
  627. // on this first drag, matching the marquee's own handles.
  628. var mask = PS.maskFromRect(cb.x, cb.y, cb.w, cb.h, false);
  629. PS.setSelection(mask, "replace", "Select Layer Bounds");
  630. PS.setTool("marquee-rect");
  631. PS.selTransform.onDown(pt, e, corner);
  632. return;
  633. }
  634. }
  635. if (layer.type === "text") {
  636. drag = { mode: "text", layer: layer, start: pt, ox: layer.text.x, oy: layer.text.y };
  637. return;
  638. }
  639. var sel = PS.doc.selection;
  640. var inSel = sel && PS.pointInSelection(pt);
  641. var base = PS.cloneCanvas(layer.canvas);
  642. var float;
  643. if (inSel) {
  644. float = PS.getSelectedPixels(layer.canvas).canvas;
  645. var bctx = base.getContext("2d");
  646. bctx.globalCompositeOperation = "destination-out";
  647. bctx.drawImage(sel.mask, 0, 0);
  648. } else {
  649. float = PS.cloneCanvas(layer.canvas);
  650. base.getContext("2d").clearRect(0, 0, base.width, base.height);
  651. }
  652. drag = {
  653. mode: "raster",
  654. layer: layer,
  655. start: pt,
  656. before: PS.snapshotLayer(layer),
  657. beforeSel: sel,
  658. base: base,
  659. float: float,
  660. withSel: !!inSel,
  661. preview: PS.createCanvas(PS.doc.width, PS.doc.height),
  662. dx: 0, dy: 0
  663. };
  664. },
  665. onMove: function (pt) {
  666. if (!drag) { return; }
  667. if (drag.mode === "text") {
  668. drag.layer.text.x = drag.ox + (pt.x - drag.start.x);
  669. drag.layer.text.y = drag.oy + (pt.y - drag.start.y);
  670. PS.renderTextLayer(drag.layer);
  671. PS.requestRender();
  672. return;
  673. }
  674. drag.dx = Math.round(pt.x - drag.start.x);
  675. drag.dy = Math.round(pt.y - drag.start.y);
  676. var pctx = drag.preview.getContext("2d");
  677. pctx.clearRect(0, 0, drag.preview.width, drag.preview.height);
  678. pctx.drawImage(drag.base, 0, 0);
  679. pctx.drawImage(drag.float, drag.dx, drag.dy);
  680. PS.layerOverride = { layer: drag.layer, canvas: drag.preview };
  681. PS.requestRender();
  682. },
  683. onUp: function () {
  684. if (!drag) { return; }
  685. if (drag.mode === "text") {
  686. var layer = drag.layer, ox = drag.ox, oy = drag.oy;
  687. var nx = layer.text.x, ny = layer.text.y;
  688. if (nx !== ox || ny !== oy) {
  689. PS.pushHistory("Move Text",
  690. function () { layer.text.x = ox; layer.text.y = oy; PS.renderTextLayer(layer); },
  691. function () { layer.text.x = nx; layer.text.y = ny; PS.renderTextLayer(layer); });
  692. }
  693. drag = null;
  694. return;
  695. }
  696. PS.layerOverride = null;
  697. if (drag.dx !== 0 || drag.dy !== 0) {
  698. var lyr = drag.layer;
  699. var ctx = lyr.canvas.getContext("2d");
  700. ctx.clearRect(0, 0, lyr.canvas.width, lyr.canvas.height);
  701. ctx.drawImage(drag.base, 0, 0);
  702. ctx.drawImage(drag.float, drag.dx, drag.dy);
  703. var beforeCanvas = drag.before;
  704. var afterCanvas = PS.cloneCanvas(lyr.canvas);
  705. var beforeSel = drag.beforeSel;
  706. var afterSel = beforeSel;
  707. if (drag.withSel) {
  708. PS.translateSelection(drag.dx, drag.dy);
  709. afterSel = PS.doc.selection;
  710. }
  711. PS.pushHistory("Move",
  712. function () {
  713. PS.restoreLayerCanvas(lyr, beforeCanvas);
  714. PS.doc.selection = beforeSel;
  715. },
  716. function () {
  717. PS.restoreLayerCanvas(lyr, afterCanvas);
  718. PS.doc.selection = afterSel;
  719. });
  720. }
  721. drag = null;
  722. PS.requestRender();
  723. },
  724. overlay: function (ctx) {
  725. if (!PS.toolOpts.move.showBounds) { return; }
  726. var layer = drag ? drag.layer : PS.activeLayer();
  727. if (!layer) { return; }
  728. var b = PS.layerContentBounds(layer);
  729. if (!b) { return; }
  730. if (drag && (drag.dx || drag.dy)) { b = { x: b.x + drag.dx, y: b.y + drag.dy, w: b.w, h: b.h }; }
  731. var z = PS.zoom;
  732. var origin = PS.docToOverlay(0, 0);
  733. var pad = PAD_PX;
  734. var sx = origin.x + b.x * z - pad, sy = origin.y + b.y * z - pad;
  735. var sw = b.w * z + pad * 2, sh = b.h * z + pad * 2;
  736. ctx.save();
  737. ctx.strokeStyle = "rgba(100,160,255,0.85)";
  738. ctx.lineWidth = 1;
  739. ctx.setLineDash([4, 3]);
  740. ctx.strokeRect(Math.round(sx) + 0.5, Math.round(sy) + 0.5, Math.round(sw), Math.round(sh));
  741. ctx.setLineDash([]);
  742. var hs = HANDLE_PX, hh = hs / 2;
  743. [{ x: sx, y: sy }, { x: sx + sw, y: sy }, { x: sx + sw, y: sy + sh }, { x: sx, y: sy + sh }]
  744. .forEach(function (hp) {
  745. var hx = Math.round(hp.x), hy = Math.round(hp.y);
  746. ctx.fillStyle = "rgba(30,30,30,0.75)";
  747. ctx.fillRect(hx - hh - 1, hy - hh - 1, hs + 2, hs + 2);
  748. ctx.fillStyle = "#ffffff";
  749. ctx.fillRect(hx - hh, hy - hh, hs, hs);
  750. });
  751. // dimension label
  752. var label = Math.round(b.w) + " x " + Math.round(b.h) + " px";
  753. ctx.font = "11px sans-serif";
  754. var tw = ctx.measureText(label).width;
  755. var ly = sy - 8;
  756. ctx.fillStyle = "rgba(30,30,30,0.85)";
  757. ctx.fillRect(sx, ly - 12, tw + 8, 16);
  758. ctx.fillStyle = "#ffffff";
  759. ctx.textBaseline = "middle";
  760. ctx.fillText(label, sx + 4, ly - 4);
  761. ctx.restore();
  762. }
  763. });
  764. // arrow-key nudge (called from hotkeys)
  765. PS.nudgeMove = function (dx, dy) {
  766. var layer = PS.activeLayer();
  767. if (!layer) { return; }
  768. if (layer.type === "text") {
  769. layer.text.x += dx; layer.text.y += dy;
  770. PS.renderTextLayer(layer);
  771. PS.requestRender();
  772. PS.markDirty();
  773. return;
  774. }
  775. var before = PS.snapshotLayer(layer);
  776. var moved = PS.createCanvas(PS.doc.width, PS.doc.height);
  777. moved.getContext("2d").drawImage(layer.canvas, dx, dy);
  778. var ctx = layer.canvas.getContext("2d");
  779. ctx.clearRect(0, 0, layer.canvas.width, layer.canvas.height);
  780. ctx.drawImage(moved, 0, 0);
  781. if (PS.doc.selection) { PS.translateSelection(dx, dy); }
  782. PS.commitLayerCanvas("Nudge", layer, before);
  783. PS.requestRender();
  784. };
  785. PS.pointInSelection = function (pt) {
  786. var sel = PS.doc.selection;
  787. if (!sel) { return false; }
  788. var x = Math.floor(pt.x), y = Math.floor(pt.y);
  789. if (x < 0 || y < 0 || x >= PS.doc.width || y >= PS.doc.height) { return false; }
  790. var a = sel.mask.getContext("2d").getImageData(x, y, 1, 1).data[3];
  791. return a >= 128;
  792. };
  793. })();
  794. /* ----- Marquee selections (M) ----- */
  795. (function () {
  796. function makeMarquee(id, name, ellipse, icon) {
  797. var drag = null;
  798. PS.registerTool(id, {
  799. name: name,
  800. key: "m",
  801. group: "marquee",
  802. cursor: "crosshair",
  803. icon: icon,
  804. options: function (host) {
  805. PS.ui.slider(host, "Feather", PS.toolOpts.marquee.feather, 0, 50, 1, function (v) {
  806. PS.toolOpts.marquee.feather = v;
  807. PS.savePrefsDebounced();
  808. }, function (v) { return v + "px"; });
  809. PS.ui.label(host, "Shift adds, Alt subtracts from a selection");
  810. },
  811. onDown: function (pt, e) {
  812. drag = { start: pt, cur: pt, mode: PS.selModeFromEvent(e), constrain: false };
  813. },
  814. onMove: function (pt, e) {
  815. if (!drag) { return; }
  816. drag.cur = pt;
  817. drag.constrain = e.shiftKey && drag.mode === "replace";
  818. },
  819. onUp: function (pt) {
  820. if (!drag) { return; }
  821. var r = normRect(drag.start, drag.cur, drag.constrain);
  822. var mode = drag.mode;
  823. drag = null;
  824. if (r.w < 2 && r.h < 2) {
  825. if (mode === "replace") { PS.deselect(); }
  826. return;
  827. }
  828. var mask = PS.maskFromRect(r.x, r.y, r.w, r.h, ellipse);
  829. var feather = PS.toolOpts.marquee.feather;
  830. if (feather > 0) {
  831. var soft = PS.makeMaskCanvas();
  832. var sctx = soft.getContext("2d");
  833. sctx.filter = "blur(" + feather + "px)";
  834. sctx.drawImage(mask, 0, 0);
  835. sctx.filter = "none";
  836. mask = soft;
  837. }
  838. PS.setSelection(mask, mode, name);
  839. },
  840. overlay: function (ctx) {
  841. if (!drag) { return; }
  842. var r = normRect(drag.start, drag.cur, drag.constrain);
  843. PS.overlayDocSpace(ctx, function (px) {
  844. ctx.lineWidth = px;
  845. ctx.setLineDash([4 * px, 4 * px]);
  846. ctx.strokeStyle = "#fff";
  847. ctx.beginPath();
  848. if (ellipse) {
  849. ctx.ellipse(r.x + r.w / 2, r.y + r.h / 2, r.w / 2, r.h / 2, 0, 0, Math.PI * 2);
  850. } else {
  851. ctx.rect(r.x, r.y, r.w, r.h);
  852. }
  853. ctx.stroke();
  854. ctx.strokeStyle = "#000";
  855. ctx.lineDashOffset = 4 * px;
  856. ctx.stroke();
  857. });
  858. }
  859. });
  860. }
  861. function normRect(a, b, constrain) {
  862. var w = b.x - a.x, h = b.y - a.y;
  863. if (constrain) {
  864. var m = Math.max(Math.abs(w), Math.abs(h));
  865. w = (w < 0 ? -m : m);
  866. h = (h < 0 ? -m : m);
  867. }
  868. return {
  869. x: Math.min(a.x, a.x + w),
  870. y: Math.min(a.y, a.y + h),
  871. w: Math.abs(w),
  872. h: Math.abs(h)
  873. };
  874. }
  875. makeMarquee("marquee-rect", "Rectangular Marquee", false,
  876. '<svg viewBox="0 0 24 24" stroke-width="1.6"><rect x="4" y="6" width="16" height="12" stroke-dasharray="3 2.5"/></svg>');
  877. makeMarquee("marquee-ellipse", "Elliptical Marquee", true,
  878. '<svg viewBox="0 0 24 24" stroke-width="1.6"><ellipse cx="12" cy="12" rx="8" ry="6" stroke-dasharray="3 2.5"/></svg>');
  879. })();
  880. /* ----- Lasso tools (L) ----- */
  881. (function () {
  882. // freehand lasso
  883. var path = null;
  884. PS.registerTool("lasso", {
  885. name: "Lasso",
  886. key: "l",
  887. group: "lasso",
  888. cursor: "crosshair",
  889. icon: '<svg viewBox="0 0 24 24" stroke-width="1.6"><path d="M5 10c0-3.5 3.5-6 7.5-6S20 6.5 20 10s-3.5 6-7.5 6c-1.2 0-2.4-.2-3.4-.6M8 14.5c-1 2.5-2.5 4-4.5 4.5M8 14.5c.6 1.4.2 2.8-1 3.4"/></svg>',
  890. options: function (host) {
  891. PS.ui.label(host, "Drag a freehand selection. Shift adds, Alt subtracts.");
  892. },
  893. onDown: function (pt, e) {
  894. path = { points: [pt], mode: PS.selModeFromEvent(e) };
  895. },
  896. onMove: function (pt) {
  897. if (!path) { return; }
  898. var last = path.points[path.points.length - 1];
  899. if (Math.hypot(pt.x - last.x, pt.y - last.y) >= 1.5) {
  900. path.points.push(pt);
  901. }
  902. },
  903. onUp: function () {
  904. if (!path) { return; }
  905. var pts = path.points, mode = path.mode;
  906. path = null;
  907. if (pts.length < 3) {
  908. if (mode === "replace") { PS.deselect(); }
  909. return;
  910. }
  911. PS.setSelection(PS.maskFromPolygon(pts), mode, "Lasso");
  912. },
  913. overlay: function (ctx) {
  914. if (!path || path.points.length < 2) { return; }
  915. drawPolyOverlay(ctx, path.points, false);
  916. }
  917. });
  918. // polygonal lasso
  919. var poly = null;
  920. PS.registerTool("lasso-poly", {
  921. name: "Polygonal Lasso",
  922. key: "l",
  923. group: "lasso",
  924. cursor: "crosshair",
  925. icon: '<svg viewBox="0 0 24 24" stroke-width="1.6"><path d="M4 16 9 5l7 2 4 7-6 5z" stroke-dasharray="3 2"/></svg>',
  926. options: function (host) {
  927. PS.ui.label(host, "Click to add points; double-click, Enter, or click the first point to close. Esc cancels.");
  928. },
  929. onDown: function (pt, e) {
  930. if (!poly) {
  931. poly = { points: [pt], mode: PS.selModeFromEvent(e) };
  932. return;
  933. }
  934. // close if clicking near the starting point
  935. var first = poly.points[0];
  936. if (Math.hypot(pt.x - first.x, pt.y - first.y) * PS.zoom < 9 && poly.points.length >= 3) {
  937. PS.finishPolyLasso();
  938. return;
  939. }
  940. poly.points.push(pt);
  941. },
  942. onDblClick: function () { PS.finishPolyLasso(); },
  943. onKey: function (e) {
  944. if (e.key === "Enter") { PS.finishPolyLasso(); return true; }
  945. if (e.key === "Escape") { poly = null; return true; }
  946. return false;
  947. },
  948. deactivate: function () { poly = null; },
  949. overlay: function (ctx) {
  950. if (!poly) { return; }
  951. var pts = poly.points.slice();
  952. if (PS.cursorPos) { pts.push(PS.cursorPos); }
  953. drawPolyOverlay(ctx, pts, true);
  954. }
  955. });
  956. PS.finishPolyLasso = function () {
  957. if (!poly || poly.points.length < 3) { poly = null; return; }
  958. var pts = poly.points, mode = poly.mode;
  959. poly = null;
  960. PS.setSelection(PS.maskFromPolygon(pts), mode, "Polygonal Lasso");
  961. };
  962. function drawPolyOverlay(ctx, pts, markStart) {
  963. PS.overlayDocSpace(ctx, function (px) {
  964. ctx.lineWidth = px;
  965. ctx.strokeStyle = "#fff";
  966. ctx.setLineDash([4 * px, 4 * px]);
  967. ctx.beginPath();
  968. ctx.moveTo(pts[0].x, pts[0].y);
  969. for (var i = 1; i < pts.length; i++) { ctx.lineTo(pts[i].x, pts[i].y); }
  970. ctx.stroke();
  971. ctx.strokeStyle = "#000";
  972. ctx.lineDashOffset = 4 * px;
  973. ctx.stroke();
  974. if (markStart) {
  975. ctx.setLineDash([]);
  976. ctx.fillStyle = "#fff";
  977. ctx.fillRect(pts[0].x - 3 * px, pts[0].y - 3 * px, 6 * px, 6 * px);
  978. }
  979. });
  980. }
  981. })();
  982. /* ----- Magic wand / smart select (W) ----- */
  983. PS.registerTool("wand", {
  984. name: "Magic Wand",
  985. key: "w",
  986. cursor: "crosshair",
  987. icon: '<svg viewBox="0 0 24 24" stroke-width="1.6"><path d="M6 18 15 9M13 4l.7 2.2M19.8 10.3 22 11M14.5 13.5l2 2M18.5 4.5l-2 2"/></svg>',
  988. options: function (host) {
  989. var o = PS.toolOpts.wand;
  990. PS.ui.slider(host, "Tolerance", o.tolerance, 0, 150, 1, function (v) {
  991. o.tolerance = v; PS.savePrefsDebounced();
  992. });
  993. PS.ui.checkbox(host, "Contiguous", o.contiguous, function (v) {
  994. o.contiguous = v; PS.savePrefsDebounced();
  995. });
  996. PS.ui.sep(host);
  997. PS.ui.checkbox(host, "Smart edges (edge detection)", o.smart, function (v) {
  998. o.smart = v; PS.savePrefsDebounced();
  999. });
  1000. PS.ui.slider(host, "Edge sensitivity", o.edgeThreshold, 10, 200, 1, function (v) {
  1001. o.edgeThreshold = v; PS.savePrefsDebounced();
  1002. });
  1003. },
  1004. onDown: function (pt, e) {
  1005. var o = PS.toolOpts.wand;
  1006. var mask = PS.magicWandMask(pt.x, pt.y, {
  1007. tolerance: o.tolerance,
  1008. contiguous: o.contiguous,
  1009. smart: o.smart,
  1010. edgeThreshold: o.edgeThreshold
  1011. });
  1012. if (!mask) { return; }
  1013. PS.setSelection(mask, PS.selModeFromEvent(e), o.smart ? "Smart Select" : "Magic Wand");
  1014. }
  1015. });
  1016. /* ----- Brush (B), Pencil, Eraser (E) ----- */
  1017. (function () {
  1018. function paintToolOptions(kind, host) {
  1019. var o = PS.toolOpts[kind];
  1020. PS.ui.slider(host, "Size", o.size, 1, 300, 1, function (v) {
  1021. o.size = v; PS.savePrefsDebounced();
  1022. }, function (v) { return v + "px"; });
  1023. PS.ui.slider(host, "Opacity", Math.round(o.opacity * 100), 1, 100, 1, function (v) {
  1024. o.opacity = v / 100; PS.savePrefsDebounced();
  1025. }, function (v) { return v + "%"; });
  1026. if (kind === "brush") {
  1027. PS.ui.select(host, "Stroke", PS.strokeTypes, o.type, function (v) {
  1028. o.type = v; PS.savePrefsDebounced();
  1029. });
  1030. PS.ui.slider(host, "Hardness", Math.round((o.hardness || 0.8) * 100), 0, 99, 1, function (v) {
  1031. o.hardness = v / 100; PS.savePrefsDebounced();
  1032. }, function (v) { return v + "%"; });
  1033. PS.ui.slider(host, "Flow", Math.round((o.flow || 0.7) * 100), 5, 100, 1, function (v) {
  1034. o.flow = v / 100; PS.savePrefsDebounced();
  1035. }, function (v) { return v + "%"; });
  1036. }
  1037. if (kind === "eraser") {
  1038. PS.ui.select(host, "Type", [
  1039. { v: "round", l: "Hard Round" },
  1040. { v: "soft", l: "Soft Round" }
  1041. ], o.type, function (v) { o.type = v; PS.savePrefsDebounced(); });
  1042. }
  1043. PS.ui.label(host, "[ and ] change size" + (kind !== "eraser" ? ", Alt-click samples color" : ""));
  1044. }
  1045. function registerPaintTool(id, name, key, icon) {
  1046. PS.registerTool(id, {
  1047. name: name,
  1048. key: key,
  1049. group: (id === "eraser") ? undefined : "paint",
  1050. cursor: "crosshair",
  1051. icon: icon,
  1052. options: function (host) { paintToolOptions(id, host); },
  1053. onDown: function (pt, e) { PS.beginStroke(id, pt, e); },
  1054. onMove: function (pt) { PS.continueStroke(pt); },
  1055. onUp: function () { PS.endStroke(); },
  1056. overlay: PS.paintCursorOverlay(function () { return PS.toolOpts[id].size; }, id === "pencil")
  1057. });
  1058. }
  1059. registerPaintTool("brush", "Brush", "b",
  1060. '<svg viewBox="0 0 24 24" stroke-width="1.6"><path d="M20 4c-4 1-9 5.5-11 9l2 2c3.5-2 8-7 9-11zM9 13c-2 .3-3.4 1.6-3.8 4.2-.1.8-.8 1.4-1.7 1.6 1.3 1.4 4.6 1.6 6.3-.1 1.2-1.2 1.4-2.7.7-4.2z"/></svg>');
  1061. registerPaintTool("pencil", "Pencil", "b",
  1062. '<svg viewBox="0 0 24 24" stroke-width="1.6"><path d="M4 20l1-4L16 5l3 3L8 19zM14 7l3 3M4 20l4-1"/></svg>');
  1063. registerPaintTool("eraser", "Eraser", "e",
  1064. '<svg viewBox="0 0 24 24" stroke-width="1.6"><path d="M9 19 4 14a2 2 0 0 1 0-2.8l7.2-7.2a2 2 0 0 1 2.8 0L20 10a2 2 0 0 1 0 2.8L13.8 19zM9 19h11M7 9l7 7"/></svg>');
  1065. })();
  1066. /* ----- Paint bucket / fill (G) ----- */
  1067. PS.registerTool("fill", {
  1068. name: "Paint Bucket",
  1069. key: "g",
  1070. cursor: "crosshair",
  1071. icon: '<svg viewBox="0 0 24 24" stroke-width="1.6"><path d="M10 3 5 8l7 7 7-5.5L10 3zM5 8l-1.5 1.5M19 15c.8 1.3 1.5 2.6 1.5 3.5a1.7 1.7 0 0 1-3.4 0c0-.9.9-2.2 1.9-3.5z"/></svg>',
  1072. options: function (host) {
  1073. var o = PS.toolOpts.fill;
  1074. PS.ui.slider(host, "Tolerance", o.tolerance, 0, 150, 1, function (v) {
  1075. o.tolerance = v; PS.savePrefsDebounced();
  1076. });
  1077. PS.ui.checkbox(host, "Contiguous", o.contiguous, function (v) {
  1078. o.contiguous = v; PS.savePrefsDebounced();
  1079. });
  1080. PS.ui.label(host, "Alt-click samples color");
  1081. },
  1082. onDown: function (pt, e) {
  1083. if (e.altKey) {
  1084. PS.sampleColorAt(pt, PS.compositeToCanvas(), false);
  1085. return;
  1086. }
  1087. var layer = PS.requirePaintableLayer();
  1088. if (!layer) { return; }
  1089. var before = PS.snapshotLayer(layer);
  1090. if (PS.floodFillLayer(layer, pt, PS.fg, PS.toolOpts.fill)) {
  1091. PS.commitLayerCanvas("Paint Bucket", layer, before);
  1092. PS.requestRender();
  1093. }
  1094. }
  1095. });
  1096. // flood fill on the layer's own pixels, honoring the selection mask
  1097. PS.floodFillLayer = function (layer, pt, hex, opts) {
  1098. var d = PS.doc;
  1099. var w = d.width, h = d.height;
  1100. var x = Math.floor(pt.x), y = Math.floor(pt.y);
  1101. if (x < 0 || y < 0 || x >= w || y >= h) { return false; }
  1102. var ctx = layer.canvas.getContext("2d");
  1103. var img = ctx.getImageData(0, 0, w, h);
  1104. var px = img.data;
  1105. var maskData = null;
  1106. if (d.selection) {
  1107. maskData = d.selection.mask.getContext("2d").getImageData(0, 0, w, h).data;
  1108. if (maskData[(y * w + x) * 4 + 3] < 128) { return false; }
  1109. }
  1110. var rgb = PS.hexToRgb(hex);
  1111. var i0 = (y * w + x) * 4;
  1112. var sr = px[i0], sg = px[i0 + 1], sb = px[i0 + 2], sa = px[i0 + 3];
  1113. var tol = opts.tolerance;
  1114. if (sr === rgb.r && sg === rgb.g && sb === rgb.b && sa === 255 && tol < 255) {
  1115. return false; // already that color
  1116. }
  1117. function matches(i) {
  1118. if (maskData && maskData[i + 3] < 128) { return false; }
  1119. return Math.abs(px[i] - sr) <= tol && Math.abs(px[i + 1] - sg) <= tol &&
  1120. Math.abs(px[i + 2] - sb) <= tol && Math.abs(px[i + 3] - sa) <= tol;
  1121. }
  1122. var fillA = (rgb.a === undefined) ? 255 : rgb.a;
  1123. function paint(i) {
  1124. px[i] = rgb.r; px[i + 1] = rgb.g; px[i + 2] = rgb.b; px[i + 3] = fillA;
  1125. }
  1126. var visited = new Uint8Array(w * h);
  1127. if (!opts.contiguous) {
  1128. for (var p = 0; p < w * h; p++) {
  1129. if (matches(p * 4)) { paint(p * 4); }
  1130. }
  1131. } else {
  1132. var stack = [[x, y]];
  1133. visited[y * w + x] = 1;
  1134. while (stack.length) {
  1135. var cur = stack.pop();
  1136. var cx = cur[0], cy = cur[1];
  1137. var left = cx;
  1138. while (left > 0 && !visited[cy * w + left - 1] && matches((cy * w + left - 1) * 4)) {
  1139. left--; visited[cy * w + left] = 1;
  1140. }
  1141. var right = cx;
  1142. while (right < w - 1 && !visited[cy * w + right + 1] && matches((cy * w + right + 1) * 4)) {
  1143. right++; visited[cy * w + right] = 1;
  1144. }
  1145. for (var sx = left; sx <= right; sx++) {
  1146. paint((cy * w + sx) * 4);
  1147. if (cy > 0 && !visited[(cy - 1) * w + sx] && matches(((cy - 1) * w + sx) * 4)) {
  1148. visited[(cy - 1) * w + sx] = 1;
  1149. stack.push([sx, cy - 1]);
  1150. }
  1151. if (cy < h - 1 && !visited[(cy + 1) * w + sx] && matches(((cy + 1) * w + sx) * 4)) {
  1152. visited[(cy + 1) * w + sx] = 1;
  1153. stack.push([sx, cy + 1]);
  1154. }
  1155. }
  1156. }
  1157. }
  1158. ctx.putImageData(img, 0, 0);
  1159. return true;
  1160. };
  1161. /* ----- Eyedropper (I) ----- */
  1162. (function () {
  1163. var sampling = null;
  1164. PS.registerTool("eyedropper", {
  1165. name: "Eyedropper",
  1166. key: "i",
  1167. cursor: "crosshair",
  1168. icon: '<svg viewBox="0 0 24 24" stroke-width="1.6"><path d="m13 8 3 3-7.5 7.5c-.6.6-1.4 1-2.2 1.1l-2.3.4.4-2.3c.1-.8.5-1.6 1.1-2.2zM13 8l2-2M16 11l2-2M14 3.5 20.5 10M17.5 3.5c1.5-1 3.5 1 2.5 2.5"/></svg>',
  1169. options: function (host) {
  1170. PS.ui.label(host, "Click to set foreground color, Alt-click to set background color");
  1171. },
  1172. onDown: function (pt, e) {
  1173. sampling = { comp: PS.compositeToCanvas(), toBg: e.altKey };
  1174. PS.sampleColorAt(pt, sampling.comp, sampling.toBg);
  1175. },
  1176. onMove: function (pt) {
  1177. if (sampling) { PS.sampleColorAt(pt, sampling.comp, sampling.toBg); }
  1178. },
  1179. onUp: function () { sampling = null; }
  1180. });
  1181. })();
  1182. /* ----- Shape tool (U) ----- */
  1183. (function () {
  1184. var drag = null;
  1185. PS.shapeKinds = [
  1186. { v: "rect", l: "Rectangle" },
  1187. { v: "rounded", l: "Rounded Rectangle" },
  1188. { v: "ellipse", l: "Ellipse" },
  1189. { v: "line", l: "Line" },
  1190. { v: "arrow", l: "Arrow" },
  1191. { v: "triangle", l: "Triangle" },
  1192. { v: "star", l: "Star" }
  1193. ];
  1194. PS.buildShapePath = function (ctx, kind, r, opts) {
  1195. ctx.beginPath();
  1196. if (kind === "rect") {
  1197. ctx.rect(r.x, r.y, r.w, r.h);
  1198. } else if (kind === "rounded") {
  1199. var rad = Math.min(opts.radius, r.w / 2, r.h / 2);
  1200. ctx.moveTo(r.x + rad, r.y);
  1201. ctx.arcTo(r.x + r.w, r.y, r.x + r.w, r.y + r.h, rad);
  1202. ctx.arcTo(r.x + r.w, r.y + r.h, r.x, r.y + r.h, rad);
  1203. ctx.arcTo(r.x, r.y + r.h, r.x, r.y, rad);
  1204. ctx.arcTo(r.x, r.y, r.x + r.w, r.y, rad);
  1205. ctx.closePath();
  1206. } else if (kind === "ellipse") {
  1207. ctx.ellipse(r.x + r.w / 2, r.y + r.h / 2, r.w / 2, r.h / 2, 0, 0, Math.PI * 2);
  1208. } else if (kind === "line") {
  1209. ctx.moveTo(r.x0, r.y0);
  1210. ctx.lineTo(r.x1, r.y1);
  1211. } else if (kind === "arrow") {
  1212. var ang = Math.atan2(r.y1 - r.y0, r.x1 - r.x0);
  1213. var len = Math.hypot(r.x1 - r.x0, r.y1 - r.y0);
  1214. var head = Math.min(len * 0.35, Math.max(12, opts.strokeWidth * 3));
  1215. ctx.moveTo(r.x0, r.y0);
  1216. ctx.lineTo(r.x1, r.y1);
  1217. ctx.moveTo(r.x1, r.y1);
  1218. ctx.lineTo(r.x1 - head * Math.cos(ang - 0.45), r.y1 - head * Math.sin(ang - 0.45));
  1219. ctx.moveTo(r.x1, r.y1);
  1220. ctx.lineTo(r.x1 - head * Math.cos(ang + 0.45), r.y1 - head * Math.sin(ang + 0.45));
  1221. } else if (kind === "triangle") {
  1222. ctx.moveTo(r.x + r.w / 2, r.y);
  1223. ctx.lineTo(r.x + r.w, r.y + r.h);
  1224. ctx.lineTo(r.x, r.y + r.h);
  1225. ctx.closePath();
  1226. } else if (kind === "star") {
  1227. var n = PS.clamp(opts.points || 5, 3, 12);
  1228. var cx = r.x + r.w / 2, cy = r.y + r.h / 2;
  1229. var R = Math.min(r.w, r.h) / 2;
  1230. var rr = R * 0.45;
  1231. for (var i = 0; i < n * 2; i++) {
  1232. var rad2 = (i % 2 === 0) ? R : rr;
  1233. var a = -Math.PI / 2 + i * Math.PI / n;
  1234. var X = cx + rad2 * Math.cos(a), Y = cy + rad2 * Math.sin(a);
  1235. if (i === 0) { ctx.moveTo(X, Y); } else { ctx.lineTo(X, Y); }
  1236. }
  1237. ctx.closePath();
  1238. }
  1239. };
  1240. function geom(drag) {
  1241. var a = drag.start, b = drag.cur;
  1242. var w = b.x - a.x, h = b.y - a.y;
  1243. if (drag.constrain) {
  1244. var kind = PS.toolOpts.shape.kind;
  1245. if (kind === "line" || kind === "arrow") {
  1246. // snap to 45 degree increments
  1247. var ang = Math.atan2(h, w);
  1248. var len = Math.hypot(w, h);
  1249. var snap = Math.round(ang / (Math.PI / 4)) * (Math.PI / 4);
  1250. w = len * Math.cos(snap);
  1251. h = len * Math.sin(snap);
  1252. } else {
  1253. var m = Math.max(Math.abs(w), Math.abs(h));
  1254. w = w < 0 ? -m : m;
  1255. h = h < 0 ? -m : m;
  1256. }
  1257. }
  1258. return {
  1259. x: Math.min(a.x, a.x + w), y: Math.min(a.y, a.y + h),
  1260. w: Math.abs(w), h: Math.abs(h),
  1261. x0: a.x, y0: a.y, x1: a.x + w, y1: a.y + h
  1262. };
  1263. }
  1264. function renderShape(ctx, r) {
  1265. var o = PS.toolOpts.shape;
  1266. PS.buildShapePath(ctx, o.kind, r, o);
  1267. var lineOnly = (o.kind === "line" || o.kind === "arrow");
  1268. if (!lineOnly && (o.mode === "fill" || o.mode === "both")) {
  1269. ctx.fillStyle = PS.fg;
  1270. ctx.fill();
  1271. }
  1272. if (lineOnly || o.mode === "stroke" || o.mode === "both") {
  1273. ctx.strokeStyle = lineOnly ? PS.fg : (o.mode === "both" ? PS.bg : PS.fg);
  1274. ctx.lineWidth = o.strokeWidth;
  1275. ctx.lineCap = "round";
  1276. ctx.lineJoin = "round";
  1277. ctx.stroke();
  1278. }
  1279. }
  1280. PS.registerTool("shape", {
  1281. name: "Shape",
  1282. key: "u",
  1283. cursor: "crosshair",
  1284. icon: '<svg viewBox="0 0 24 24" stroke-width="1.6"><rect x="3" y="3" width="12" height="12" rx="1"/><circle cx="16" cy="16" r="5.5"/></svg>',
  1285. options: function (host) {
  1286. var o = PS.toolOpts.shape;
  1287. var kindLabel = o.kind;
  1288. PS.shapeKinds.forEach(function (k) { if (k.v === o.kind) { kindLabel = k.l; } });
  1289. PS.ui.label(host, "Shape: " + kindLabel + " (right-click the Shape tool to change)");
  1290. if (o.kind !== "line" && o.kind !== "arrow") {
  1291. PS.ui.select(host, "Mode", [
  1292. { v: "fill", l: "Fill (FG)" },
  1293. { v: "stroke", l: "Stroke (FG)" },
  1294. { v: "both", l: "Fill FG + Stroke BG" }
  1295. ], o.mode, function (v) { o.mode = v; PS.savePrefsDebounced(); });
  1296. }
  1297. PS.ui.slider(host, "Stroke width", o.strokeWidth, 1, 60, 1, function (v) {
  1298. o.strokeWidth = v; PS.savePrefsDebounced();
  1299. }, function (v) { return v + "px"; });
  1300. if (o.kind === "rounded") {
  1301. PS.ui.slider(host, "Corner radius", o.radius, 1, 100, 1, function (v) {
  1302. o.radius = v; PS.savePrefsDebounced();
  1303. });
  1304. }
  1305. if (o.kind === "star") {
  1306. PS.ui.slider(host, "Points", o.points, 3, 12, 1, function (v) {
  1307. o.points = v; PS.savePrefsDebounced();
  1308. });
  1309. }
  1310. PS.ui.label(host, "Shift constrains proportions");
  1311. },
  1312. onDown: function (pt, e) {
  1313. if (!PS.requirePaintableLayer()) { return; }
  1314. drag = { start: pt, cur: pt, constrain: e.shiftKey };
  1315. },
  1316. onMove: function (pt, e) {
  1317. if (!drag) { return; }
  1318. drag.cur = pt;
  1319. drag.constrain = e.shiftKey;
  1320. },
  1321. onUp: function () {
  1322. if (!drag) { return; }
  1323. var r = geom(drag);
  1324. drag = null;
  1325. if (r.w < 2 && r.h < 2) { return; }
  1326. var layer = PS.requirePaintableLayer();
  1327. if (!layer) { return; }
  1328. var before = PS.snapshotLayer(layer);
  1329. PS.maskedDraw(layer, function (ctx) { renderShape(ctx, r); });
  1330. PS.commitLayerCanvas("Shape: " + PS.toolOpts.shape.kind, layer, before);
  1331. PS.requestRender();
  1332. },
  1333. overlay: function (ctx) {
  1334. if (!drag) { return; }
  1335. var r = geom(drag);
  1336. PS.overlayDocSpace(ctx, function () {
  1337. ctx.globalAlpha = 0.8;
  1338. renderShape(ctx, r);
  1339. ctx.globalAlpha = 1;
  1340. });
  1341. }
  1342. });
  1343. })();
  1344. /* ----- Hand (H) and Zoom (Z) ----- */
  1345. PS.registerTool("hand", {
  1346. name: "Hand",
  1347. key: "h",
  1348. cursor: "grab",
  1349. icon: '<svg viewBox="0 0 24 24" stroke-width="1.6"><path d="M7 11V5.5a1.5 1.5 0 0 1 3 0V10m0-5.5v-1a1.5 1.5 0 0 1 3 0V10m0-5a1.5 1.5 0 0 1 3 0V11m0-3.5a1.5 1.5 0 0 1 3 0V15a6 6 0 0 1-6 6h-1.8a6 6 0 0 1-4.6-2.2L4 15.6c-1.4-1.7.6-3.8 2.2-2.4L7 14z"/></svg>',
  1350. options: function (host) {
  1351. PS.ui.label(host, "Drag to pan. Hold Space with any tool for temporary pan.");
  1352. }
  1353. // panning handled by the pointer pipeline
  1354. });
  1355. (function () {
  1356. var drag = null;
  1357. PS.registerTool("zoom", {
  1358. name: "Zoom",
  1359. key: "z",
  1360. cursor: "zoom-in",
  1361. icon: '<svg viewBox="0 0 24 24" stroke-width="1.6"><circle cx="10.5" cy="10.5" r="6.5"/><path d="m15.5 15.5 5 5M8 10.5h5M10.5 8v5"/></svg>',
  1362. options: function (host) {
  1363. PS.ui.label(host, "Click to zoom in, Alt-click to zoom out, drag a box to zoom to area");
  1364. PS.ui.button(host, "Fit", function () { PS.zoomFit(); });
  1365. PS.ui.button(host, "100%", function () { PS.zoomActual(); });
  1366. },
  1367. onDown: function (pt) { drag = { start: pt, cur: pt }; },
  1368. onMove: function (pt) { if (drag) { drag.cur = pt; } },
  1369. onUp: function (pt, e) {
  1370. if (!drag) { return; }
  1371. var r = {
  1372. x: Math.min(drag.start.x, drag.cur.x),
  1373. y: Math.min(drag.start.y, drag.cur.y),
  1374. w: Math.abs(drag.cur.x - drag.start.x),
  1375. h: Math.abs(drag.cur.y - drag.start.y)
  1376. };
  1377. drag = null;
  1378. if (r.w * PS.zoom > 12 && r.h * PS.zoom > 12) {
  1379. var holder = PS.el("workspace-holder");
  1380. var z = Math.min((holder.clientWidth - 40) / r.w, (holder.clientHeight - 40) / r.h);
  1381. PS.setZoom(z, { x: r.x + r.w / 2, y: r.y + r.h / 2 });
  1382. } else {
  1383. PS.setZoom(PS.zoom * (e.altKey ? 1 / 1.5 : 1.5), pt);
  1384. }
  1385. },
  1386. overlay: function (ctx) {
  1387. if (!drag) { return; }
  1388. PS.overlayDocSpace(ctx, function (px) {
  1389. ctx.lineWidth = px;
  1390. ctx.strokeStyle = "#4a90d9";
  1391. ctx.setLineDash([4 * px, 3 * px]);
  1392. ctx.strokeRect(
  1393. Math.min(drag.start.x, drag.cur.x), Math.min(drag.start.y, drag.cur.y),
  1394. Math.abs(drag.cur.x - drag.start.x), Math.abs(drag.cur.y - drag.start.y));
  1395. });
  1396. }
  1397. });
  1398. })();