canvas.js 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. const canvas = document.querySelector(".canvas canvas");
  2. const ctx = canvas.getContext("2d", { willReadFrequently: true });
  3. ctx.imageSmoothingEnabled = false;
  4. let drawing = false;
  5. let lastX, lastY;
  6. let strokeSize = 8;
  7. let strokeColor = "black";
  8. function drawPixelLine(x0, y0, x1, y1) {
  9. const dx = Math.abs(x1 - x0);
  10. const dy = Math.abs(y1 - y0);
  11. const sx = x0 < x1 ? 1 : -1;
  12. const sy = y0 < y1 ? 1 : -1;
  13. let err = dx - dy;
  14. while (true) {
  15. ctx.fillRect(x0, y0, strokeSize, strokeSize);
  16. if (x0 === x1 && y0 === y1) break;
  17. const e2 = 2 * err;
  18. if (e2 > -dy) {
  19. err -= dy;
  20. x0 += sx;
  21. }
  22. if (e2 < dx) {
  23. err += dx;
  24. y0 += sy;
  25. }
  26. }
  27. }
  28. // Disable dragging for all images on the page
  29. document.querySelectorAll('img').forEach(img => {
  30. img.addEventListener('dragstart', (e) => e.preventDefault());
  31. });
  32. canvas.addEventListener("mousedown", start);
  33. canvas.addEventListener("mouseup", () => (drawing = false));
  34. canvas.addEventListener("mouseleave", () => (drawing = false));
  35. canvas.addEventListener("mousemove", move);
  36. function start(e) {
  37. drawing = true;
  38. const rect = canvas.getBoundingClientRect();
  39. const scaleX = canvas.width / rect.width;
  40. const scaleY = canvas.height / rect.height;
  41. lastX = Math.floor((e.clientX - rect.left) * scaleX);
  42. lastY = Math.floor((e.clientY - rect.top) * scaleY);
  43. }
  44. function move(e) {
  45. if (!drawing) return;
  46. const rect = canvas.getBoundingClientRect();
  47. const scaleX = canvas.width / rect.width;
  48. const scaleY = canvas.height / rect.height;
  49. const x = Math.floor((e.clientX - rect.left) * scaleX);
  50. const y = Math.floor((e.clientY - rect.top) * scaleY);
  51. ctx.fillStyle = strokeColor;
  52. drawPixelLine(lastX, lastY, x, y);
  53. lastX = x;
  54. lastY = y;
  55. }
  56. canvas.addEventListener("touchstart", (e) => {
  57. e.preventDefault();
  58. const touch = e.touches[0];
  59. start(touch);
  60. });
  61. canvas.addEventListener("touchmove", (e) => {
  62. e.preventDefault();
  63. const touch = e.touches[0];
  64. move(touch);
  65. });
  66. canvas.addEventListener("touchend", (e) => {
  67. e.preventDefault();
  68. drawing = false;
  69. });
  70. /* BUTTONS */
  71. let pencilButton = document.querySelector("button#pencil");
  72. let eraserButton = document.querySelector("button#eraser");
  73. let smallButton = document.querySelector("button#small");
  74. let mediumButton = document.querySelector("button#medium");
  75. let clearButton = document.querySelector("button#clear");
  76. pencilButton.addEventListener("click", (e) => {
  77. e.preventDefault();
  78. strokeColor = "black";
  79. pencilButton.classList.add("active-tool");
  80. eraserButton.classList.remove("active-tool");
  81. });
  82. eraserButton.addEventListener("click", (e) => {
  83. e.preventDefault();
  84. strokeColor = "white";
  85. eraserButton.classList.add("active-tool");
  86. pencilButton.classList.remove("active-tool");
  87. });
  88. smallButton.addEventListener("click", (e) => {
  89. e.preventDefault();
  90. strokeSize = 4;
  91. smallButton.classList.add("active-tool");
  92. mediumButton.classList.remove("active-tool");
  93. });
  94. mediumButton.addEventListener("click", (e) => {
  95. e.preventDefault();
  96. strokeSize = 8;
  97. mediumButton.classList.add("active-tool");
  98. smallButton.classList.remove("active-tool");
  99. });
  100. window.resetCanvas = () => {
  101. const BASE_HEIGHT = 312;
  102. window.currentHeightMultiplier = 1;
  103. canvas.height = BASE_HEIGHT;
  104. canvas.parentElement.style.aspectRatio = `856 / 312`;
  105. ctx.clearRect(0, 0, canvas.width, canvas.height);
  106. ctx.lineWidth = strokeSize;
  107. ctx.lineCap = "round";
  108. ctx.strokeStyle = strokeColor;
  109. };
  110. clearButton.addEventListener("click", () => {
  111. window.resetCanvas();
  112. window.clearCurrentText();
  113. });
  114. // Extend Canvas Button
  115. const extendCanvasBtn = document.getElementById("extend-canvas-btn");
  116. if (extendCanvasBtn) {
  117. extendCanvasBtn.addEventListener("click", (e) => {
  118. e.preventDefault();
  119. if ((window.currentHeightMultiplier || 1) >= 3) return;
  120. const BASE_HEIGHT = 312;
  121. window.currentHeightMultiplier = (window.currentHeightMultiplier || 1) + 1;
  122. const newHeight = BASE_HEIGHT * window.currentHeightMultiplier;
  123. // Create temporary canvas to hold current drawing
  124. const tempCanvas = document.createElement('canvas');
  125. tempCanvas.width = canvas.width;
  126. tempCanvas.height = canvas.height;
  127. tempCanvas.getContext('2d').drawImage(canvas, 0, 0);
  128. canvas.height = newHeight;
  129. canvas.parentElement.style.aspectRatio = `856 / ${newHeight}`;
  130. // Restore drawing
  131. ctx.clearRect(0, 0, canvas.width, canvas.height);
  132. ctx.drawImage(tempCanvas, 0, 0);
  133. });
  134. }
  135. copyButton = document.querySelector("button#copy");
  136. copyButton.addEventListener("click", (e) => {
  137. e.preventDefault();
  138. // copy image
  139. const images = document.querySelectorAll(
  140. "#chat-log .message-box .canvas img"
  141. );
  142. const lastImage = images[0];
  143. if (lastImage) {
  144. const img = new Image();
  145. img.onload = function () {
  146. ctx.clearRect(0, 0, canvas.width, canvas.height);
  147. ctx.drawImage(img, 0, 0);
  148. };
  149. img.src = lastImage.src;
  150. }
  151. // copy text
  152. const texts = document.querySelectorAll(
  153. "#chat-log .message-box .canvas .message-text"
  154. );
  155. const lastText = texts[0];
  156. if (lastText) {
  157. const text = lastText.innerHTML.replace(/<br>/g, "\n");
  158. window.setCurrentText(text);
  159. }
  160. });
  161. // Color swaths
  162. const swaths = document.querySelectorAll(".swatch");
  163. swaths.forEach(swatch => {
  164. swatch.addEventListener("click", (e) => {
  165. e.preventDefault();
  166. const color = swatch.getAttribute("data-color");
  167. strokeColor = color;
  168. swaths.forEach(s => s.classList.remove("active-swatch"));
  169. swatch.classList.add("active-swatch");
  170. // Update user's theme color to match
  171. if (window.displayUserColor) {
  172. window.userColor = color;
  173. window.displayUserColor(color);
  174. }
  175. // Unset eraser if active
  176. pencilButton.classList.add("active-tool");
  177. eraserButton.classList.remove("active-tool");
  178. });
  179. });
  180. // Sync custom color picker with stroke color
  181. const picker = document.getElementById("user-color-picker");
  182. if (picker) {
  183. picker.addEventListener("input", (e) => {
  184. strokeColor = e.target.value;
  185. swaths.forEach(s => s.classList.remove("active-swatch"));
  186. // We could add an active class to the picker container but it's okay
  187. pencilButton.classList.add("active-tool");
  188. eraserButton.classList.remove("active-tool");
  189. });
  190. }
  191. // Expand Button
  192. const expandBtn = document.getElementById("expand-btn");
  193. if (expandBtn) {
  194. expandBtn.addEventListener("click", (e) => {
  195. e.preventDefault();
  196. document.body.classList.toggle("fullscreen-mode");
  197. if (document.body.classList.contains("fullscreen-mode")) {
  198. expandBtn.innerHTML = "shrink ⤡";
  199. } else {
  200. expandBtn.innerHTML = "expand ⤢";
  201. }
  202. });
  203. }
  204. // Randomize Color Button
  205. const randomColorBtn = document.getElementById("random-color-btn");
  206. if (randomColorBtn) {
  207. randomColorBtn.addEventListener("click", (e) => {
  208. e.preventDefault();
  209. const newColor = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');
  210. strokeColor = newColor;
  211. swaths.forEach(s => s.classList.remove("active-swatch"));
  212. if (picker) picker.value = newColor;
  213. if (window.displayUserColor) {
  214. window.userColor = newColor;
  215. window.displayUserColor(newColor);
  216. }
  217. pencilButton.classList.add("active-tool");
  218. eraserButton.classList.remove("active-tool");
  219. });
  220. }
  221. // Theme Toggle Button
  222. const themeBtn = document.getElementById("theme-btn");
  223. if (themeBtn) {
  224. themeBtn.addEventListener("click", (e) => {
  225. e.preventDefault();
  226. document.body.classList.toggle("light-mode");
  227. if (document.body.classList.contains("light-mode")) {
  228. themeBtn.innerHTML = "dark mode 🌙";
  229. } else {
  230. themeBtn.innerHTML = "light mode ☀";
  231. }
  232. });
  233. }
  234. // Download Archive Button
  235. const downloadBtn = document.getElementById("download-btn");
  236. if (downloadBtn) {
  237. downloadBtn.addEventListener("click", (e) => {
  238. e.preventDefault();
  239. window.location.href = "/api/download_images";
  240. });
  241. }
  242. // Palette Toggle for portrait mode
  243. const paletteToggleBtn = document.getElementById("palette-toggle");
  244. const colorPalette = document.getElementById("color-palette");
  245. if (paletteToggleBtn && colorPalette) {
  246. paletteToggleBtn.addEventListener("click", (e) => {
  247. e.preventDefault();
  248. colorPalette.classList.toggle("show-palette");
  249. });
  250. }