23_canvasJs.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501
  1. const canvas = document.getElementById("canvas");
  2. const ctx = canvas.getContext("2d");
  3. const canvWidth = canvas.width;
  4. const canvHeight = canvas.height;
  5. let current;
  6. let selection = [];
  7. const tools = {
  8. graffity: {
  9. mousemove(e) {
  10. //e.buttons 0b00000x11 & 0b00000100 == x
  11. e.buttons & 1 &&
  12. // new Circle(e.layerX, e.layerY, +size.value, color.value);
  13. new Ellipse(
  14. e.layerX,
  15. e.layerY,
  16. +size.value,
  17. +size.value,
  18. color.value
  19. );
  20. },
  21. },
  22. // ------- а круг - это частный случай элипса :) - просто зажми клавишу Ctrl
  23. //
  24. // circle: {
  25. // mousedown(e) {
  26. // current = new Circle(e.layerX, e.layerY, 1, color.value);
  27. // },
  28. // mousemove(e) {
  29. // if (!current) return;
  30. // current.radius = current.distanceTo(e.layerX, e.layerY);
  31. // Drawable.drawAll();
  32. // },
  33. // mouseup(e) {
  34. // current = null;
  35. // },
  36. // },
  37. ellipse: {
  38. mousedown(e) {
  39. current = new Ellipse(e.layerX, e.layerY, 1, 1, color.value);
  40. },
  41. mousemove(e) {
  42. if (!current) return;
  43. current.radiusX = current.distanceTo(e.layerX, current.y);
  44. current.radiusY = current.distanceTo(current.x, e.layerY);
  45. if (e.ctrlKey) {
  46. current.radiusX = current.radiusY = Math.max(
  47. current.radiusX,
  48. current.radiusY
  49. );
  50. }
  51. Drawable.drawAll();
  52. },
  53. mouseup(e) {
  54. current = null;
  55. },
  56. },
  57. line: {
  58. mousedown(e) {
  59. current = new Line(
  60. e.layerX,
  61. e.layerY,
  62. 0,
  63. 0,
  64. color.value,
  65. +size.value
  66. );
  67. },
  68. mousemove(e) {
  69. if (!current) return;
  70. current.width = e.layerX - current.x;
  71. current.height = e.layerY - current.y;
  72. Drawable.drawAll();
  73. },
  74. mouseup(e) {
  75. current = null;
  76. },
  77. },
  78. rectangle: {
  79. mousedown(e) {
  80. current = new Rectangle(e.layerX, e.layerY, 1, 1, color.value);
  81. },
  82. mousemove(e) {
  83. if (!current) return;
  84. let deltaX = current.distanceTo(e.layerX, current.y);
  85. let deltaY = current.distanceTo(current.x, e.layerY);
  86. current.width = e.layerX > current.x ? deltaX : -deltaX;
  87. current.height = e.layerY > current.y ? deltaY : -deltaY;
  88. if (e.ctrlKey) {
  89. current.width = current.height = Math.max(
  90. current.width,
  91. current.height
  92. );
  93. }
  94. Drawable.drawAll();
  95. },
  96. mouseup(e) {
  97. current = null;
  98. },
  99. },
  100. select: {
  101. itWasMousemove: false,
  102. click(e) {
  103. let found = [];
  104. if (!tools.select.itWasMousemove) {
  105. found = Drawable.instances.filter(
  106. (c) => c.in && c.in(e.layerX, e.layerY)
  107. );
  108. // ------ такая логика мне кажется более естественной ------
  109. // но это уже из вопросов "идеологии партии" (программного продукта)
  110. //
  111. if (found[0]) {
  112. found = [found.pop()];
  113. }
  114. if (!e.ctrlKey) {
  115. selection = [];
  116. }
  117. selection = selection.concat(found);
  118. Drawable.drawAll(selection);
  119. }
  120. tools.select.itWasMousemove = false;
  121. },
  122. mousedown(e) {
  123. current = new Rectangle(e.layerX, e.layerY, 1, 1, "#8F8F8F", true);
  124. },
  125. mousemove(e) {
  126. if (!current) return;
  127. tools.select.itWasMousemove = true;
  128. let deltaX = current.distanceTo(e.layerX, current.y);
  129. let deltaY = current.distanceTo(current.x, e.layerY);
  130. current.width = e.layerX > current.x ? deltaX : -deltaX;
  131. current.height = e.layerY > current.y ? deltaY : -deltaY;
  132. Drawable.drawAll();
  133. },
  134. mouseup(e) {
  135. Drawable.instances.pop();
  136. let found = Drawable.instances.filter(
  137. (c) =>
  138. c.inBounds &&
  139. c.inBounds(
  140. current.x,
  141. current.y,
  142. current.width,
  143. current.height
  144. )
  145. );
  146. if (!e.ctrlKey) {
  147. selection = [];
  148. }
  149. selection = selection.concat(found);
  150. Drawable.drawAll(selection);
  151. current = null;
  152. },
  153. },
  154. };
  155. function superHandler(evt) {
  156. let t = tools[tool.value];
  157. // надо же еще и проверить вообще наличие такой опции
  158. if (t && typeof t[evt.type] === "function") {
  159. t[evt.type].call(this, evt);
  160. }
  161. }
  162. canvas.onmousemove = superHandler;
  163. canvas.onmouseup = superHandler;
  164. canvas.onmousedown = superHandler;
  165. canvas.onclick = superHandler;
  166. ////
  167. function Drawable() {
  168. Drawable.addInstance(this);
  169. }
  170. const distance = (x1, y1, x2, y2) => ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5;
  171. Drawable.prototype.draw = function () {};
  172. Drawable.prototype.distanceTo = function (x, y) {
  173. if (typeof this.x !== "number" || typeof this.y !== "number") {
  174. return NaN;
  175. }
  176. return distance(this.x, this.y, x, y);
  177. };
  178. Drawable.instances = [];
  179. Drawable.addInstance = function (item) {
  180. Drawable.instances.push(item);
  181. };
  182. Drawable.drawAll = function (selection = []) {
  183. ctx.clearRect(0, 0, canvWidth, canvHeight);
  184. Drawable.forAll((item) => item.draw());
  185. selection.forEach((item) => item.draw(true));
  186. };
  187. Drawable.forAll = function (callback) {
  188. for (var i = 0; i < Drawable.instances.length; i++) {
  189. callback(Drawable.instances[i]);
  190. }
  191. };
  192. // ---- Круг - это частный случай элипса ----
  193. //
  194. // class Circle extends Drawable {
  195. // constructor(x, y, radius, color) {
  196. // super();
  197. // this.x = x;
  198. // this.y = y;
  199. // this.radius = radius;
  200. // this.color = color;
  201. // this.draw();
  202. // }
  203. // draw(selected) {
  204. // ctx.beginPath();
  205. // ctx.arc(this.x, this.y, this.radius, 0, 2 * Math.PI);
  206. // ctx.closePath();
  207. // ctx.fillStyle = this.color;
  208. // if (selected) {
  209. // ctx.lineWidth = 5;
  210. // // проба чтоб цвет контура отличался от цвета заливки
  211. // // но надо дорабатывать логику
  212. // ctx.strokeStyle =
  213. // "#" +
  214. // (
  215. // (0x7fffff + parseInt("0x" + this.color.slice(1))) %
  216. // 0xffffff
  217. // ).toString(16);
  218. // ctx.stroke();
  219. // }
  220. // ctx.fill();
  221. // }
  222. // in(x, y) {
  223. // return this.distanceTo(x, y) < this.radius;
  224. // }
  225. // inBounds(x, y, w, h) {
  226. // // x = 100, this.x = 102, w = 5
  227. // return this.x >= x && this.x <= x + w && this.y >= y && this.y <= y + h;
  228. // }
  229. // }
  230. class Ellipse extends Drawable {
  231. constructor(x, y, radiusX, radiusY, color) {
  232. super();
  233. this.x = x;
  234. this.y = y;
  235. this.radiusX = radiusX;
  236. this.radiusY = radiusY;
  237. this.color = color;
  238. this.draw();
  239. }
  240. draw(selected) {
  241. ctx.beginPath();
  242. ctx.ellipse(
  243. this.x,
  244. this.y,
  245. this.radiusX,
  246. this.radiusY,
  247. 0,
  248. 0,
  249. 2 * Math.PI
  250. );
  251. ctx.closePath();
  252. ctx.fillStyle = this.color;
  253. if (selected) {
  254. ctx.lineWidth = 5;
  255. // проба чтоб цвет контура отличался от цвета заливки
  256. // но надо дорабатывать логику
  257. // prettier-ignore
  258. ctx.strokeStyle =
  259. "#" + ((0x7fffff + parseInt("0x" + this.color.slice(1))) % 0xffffff).toString(16);
  260. ctx.stroke();
  261. }
  262. ctx.fill();
  263. }
  264. in(x, y) {
  265. return (
  266. (x - this.x) ** 2 / this.radiusX ** 2 +
  267. (y - this.y) ** 2 / this.radiusY ** 2 <=
  268. 1
  269. );
  270. // (x**2)/a**2 + (y**2)/b**2 = 1 - формула элипса
  271. }
  272. inBounds(x, y, width, height) {
  273. let minX = Math.min(x, x + width);
  274. let minY = Math.min(y, y + height);
  275. let maxX = Math.max(x, x + width);
  276. let maxY = Math.max(y, y + height);
  277. return (
  278. Math.min(this.x - this.radiusX, this.x + this.radiusX) >= minX &&
  279. Math.max(this.x - this.radiusX, this.x + this.radiusX) <= maxX &&
  280. Math.min(this.y - this.radiusY, this.y + this.radiusY) >= minY &&
  281. Math.max(this.y - this.radiusY, this.y + this.radiusY) <= maxY
  282. );
  283. }
  284. }
  285. class Line extends Drawable {
  286. constructor(x, y, width, height, color, lineWidth) {
  287. super();
  288. this.x = x;
  289. this.y = y;
  290. this.width = width;
  291. this.height = height;
  292. this.color = color;
  293. this.lineWidth = lineWidth;
  294. this.draw();
  295. }
  296. draw(selected) {
  297. ctx.beginPath();
  298. ctx.moveTo(this.x, this.y);
  299. ctx.lineTo(this.x + this.width, this.y + this.height);
  300. ctx.closePath();
  301. // prettier-ignore
  302. ctx.strokeStyle = selected ?
  303. "#" + ((0x7fffff + parseInt("0x" + this.color.slice(1))) % 0xffffff ).toString(16)
  304. : this.color;
  305. ctx.lineWidth = this.lineWidth;
  306. ctx.stroke();
  307. }
  308. in(x, y) {
  309. // prettier-ignore
  310. if (
  311. !(x > Math.min(this.x - this.lineWidth, this.x + this.width + this.lineWidth) &&
  312. x < Math.max(this.x - this.lineWidth, this.x + this.width + this.lineWidth) &&
  313. y > Math.min(this.y - this.lineWidth, this.y + this.height + this.lineWidth) &&
  314. y < Math.max(this.y - this.lineWidth, this.y + this.height + this.lineWidth)
  315. )
  316. ) {
  317. return false;
  318. }
  319. // prettier-ignore
  320. let aureoleArr = [
  321. Math.atan((y + this.lineWidth / 2 - this.y) / (x - this.lineWidth / 2 - this.x)),
  322. Math.atan((y + this.lineWidth / 2 - this.y) / (x + this.lineWidth / 2 - this.x)),
  323. Math.atan((y - this.lineWidth / 2 - this.y) / (x + this.lineWidth / 2 - this.x)),
  324. Math.atan((y - this.lineWidth / 2 - this.y) / (x - this.lineWidth / 2 - this.x)),
  325. ];
  326. let ownAngle = Math.atan(this.height / this.width);
  327. let maxAngle = Math.max(...aureoleArr);
  328. let minAngle = Math.min(...aureoleArr);
  329. if (maxAngle - minAngle > Math.PI / 2) {
  330. if (maxAngle > Math.PI) {
  331. minAngle += Math.PI;
  332. } else {
  333. maxAngle -= Math.PI;
  334. }
  335. [maxAngle, minAngle] = [minAngle, maxAngle];
  336. if (this.width * this.height > 0) {
  337. ownAngle = ownAngle * -1;
  338. }
  339. }
  340. return minAngle <= ownAngle && ownAngle <= maxAngle;
  341. }
  342. inBounds(x, y, width, height) {
  343. let minX = Math.min(x, x + width);
  344. let minY = Math.min(y, y + height);
  345. let maxX = Math.max(x, x + width);
  346. let maxY = Math.max(y, y + height);
  347. return (
  348. Math.min(this.x, this.x + this.width) >= minX &&
  349. Math.max(this.x, this.x + this.width) <= maxX &&
  350. Math.min(this.y, this.y + this.height) >= minY &&
  351. Math.max(this.y, this.y + this.height) <= maxY
  352. );
  353. }
  354. }
  355. class Rectangle extends Drawable {
  356. constructor(x, y, width, height, color, isSelectTool = false) {
  357. super();
  358. this.x = x;
  359. this.y = y;
  360. this.width = width;
  361. this.height = height;
  362. this.color = color;
  363. this.draw();
  364. this.isSelectTool = isSelectTool;
  365. }
  366. draw(selected) {
  367. ctx.beginPath();
  368. ctx.rect(this.x, this.y, this.width, this.height);
  369. ctx.closePath();
  370. ctx.fillStyle = this.color;
  371. ctx.lineWidth = this.isSelectTool ? 1 : 5;
  372. // проба чтоб цвет контура отличался от цвета заливки
  373. // но надо дорабатывать логику (подобрать цвет)
  374. // prettier-ignore
  375. if (selected) {
  376. ctx.strokeStyle =
  377. "#" + ((0x7fffff + parseInt("0x" + this.color.slice(1))) % 0xffffff).toString(16);
  378. } else {
  379. ctx.strokeStyle = this.color;
  380. }
  381. if (this.isSelectTool || selected) {
  382. ctx.stroke();
  383. }
  384. if (!this.isSelectTool) {
  385. ctx.fill();
  386. }
  387. }
  388. in(x, y) {
  389. return (
  390. Math.min(this.x, this.x + this.width) <= x &&
  391. x <= Math.max(this.x, this.x + this.width) &&
  392. Math.min(this.y, this.y + this.height) <= y &&
  393. y <= Math.max(this.y, this.y + this.height)
  394. );
  395. }
  396. inBounds(x, y, width, height) {
  397. let minX = Math.min(x, x + width);
  398. let minY = Math.min(y, y + height);
  399. let maxX = Math.max(x, x + width);
  400. let maxY = Math.max(y, y + height);
  401. return (
  402. Math.min(this.x, this.x + this.width) >= minX &&
  403. Math.max(this.x, this.x + this.width) <= maxX &&
  404. Math.min(this.y, this.y + this.height) >= minY &&
  405. Math.max(this.y, this.y + this.height) <= maxY
  406. );
  407. }
  408. }
  409. // c oninput повеселее
  410. color.oninput = () => {
  411. selection.forEach((c) => (c.color = color.value));
  412. Drawable.drawAll(selection);
  413. };
  414. document.getElementById("delete").onclick = () => {
  415. Drawable.instances = Drawable.instances.filter(
  416. (item) => !selection.includes(item)
  417. );
  418. selection = [];
  419. Drawable.drawAll();
  420. };
  421. document.getElementById("undo").onclick = () => {
  422. Drawable.instances.pop();
  423. selection = [];
  424. Drawable.drawAll();
  425. };
  426. clear.onclick = () => {
  427. selection = [];
  428. Drawable.instances = [];
  429. ctx.clearRect(0, 0, canvWidth, canvHeight);
  430. };
  431. tool.onchange = () => {
  432. comment.innerText = "";
  433. if (tool.value === "circle" || tool.value === "ellipse") {
  434. comment.innerText = "Круг - этот тот же элипс, но +CTRL";
  435. }
  436. if (tool.value === "rectangle") {
  437. comment.innerText = "Квадрат = прямоугольник + CTRL";
  438. }
  439. };