Kawaii Helper & Drawing Bot for Gartic.io

Helper for Gartic.io with auto-guess, drawing assistance, and drawing bot

目前为 2025-03-11 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Kawaii Helper & Drawing Bot for Gartic.io
  3. // @namespace http://tampermonkey.net/
  4. // @version 2025-03-12
  5. // @description Helper for Gartic.io with auto-guess, drawing assistance, and drawing bot
  6. // @author anonimbiri & Gartic-Developers
  7. // @license MIT
  8. // @match https://gartic.io/*
  9. // @icon https://www.google.com/s2/favicons?sz=64&domain=gartic.io
  10. // @run-at document-start
  11. // @grant none
  12. // ==/UserScript==
  13.  
  14. // I used the word list from 'https://github.com/Gartic-Developers/Gartic-WordList/'.
  15. // Thanks to Gartic Developers for providing this resource. Also, thanks to Qwyua!
  16.  
  17. (function() {
  18. 'use strict';
  19.  
  20. // Script interception (unchanged)
  21. Node.prototype.appendChild = new Proxy(Node.prototype.appendChild, {
  22. apply: function(target, thisArg, argumentsList) {
  23. const node = argumentsList[0];
  24. if (node.nodeName.toLowerCase() === 'script' && node.src && node.src.includes('room')) {
  25. console.log('Target script detected:', node.src);
  26. fetch(node.src)
  27. .then(response => response.text())
  28. .then(scriptContent => {
  29. let modifiedContent = scriptContent
  30. .replace(
  31. 'r.created||c?Rt("input",{type:"text",name:"chat",className:"mousetrap",autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",value:i,placeholder:this._lang.chatHere,maxLength:100,enterKeyHint:"send",onChange:this.handleText,ref:this._ref}):Rt("input",{type:"text",name:"chat",className:"mousetrap",autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",value:this._lang.loginChat,maxLength:100,ref:this._ref,disabled:!0})',
  32. 'Rt("input",{type:"text",name:"chat",className:"mousetrap",autoComplete:"off",autoCorrect:"off",autoCapitalize:"off",value:i,placeholder:this._lang.chatHere,maxLength:100,enterKeyHint:"send",onChange:this.handleText,ref:this._ref})'
  33. )
  34. .replace(
  35. 'this._timerAtivo=setInterval((function(){Date.now()-e._ativo>15e4&&(O(Object(f.a)(n.prototype),"emit",e).call(e,"avisoInativo"),e._ativo=Date.now())}),1e3)',
  36. 'this._timerAtivo=setInterval((function(){Date.now()-e._ativo>15e4&&e.active()}),1e3)'
  37. )
  38. .replace(
  39. 'e.unlock()}',
  40. 'e.unlock();window.game=e;setInterval(()=>{window.game=e},1000);e.on("votekick",(t,i,o)=>{if(i.id===e.me.id){e.votekick(t.id,true);}});}'
  41. );
  42. let blob = new Blob([modifiedContent], { type: 'application/javascript' });
  43. let blobUrl = URL.createObjectURL(blob);
  44. node.src = blobUrl;
  45. node.textContent = '';
  46. return target.apply(thisArg, [node]);
  47. })
  48. .catch(error => console.error('Failed to fetch/modify script:', error));
  49. return node;
  50. }
  51. return target.apply(thisArg, argumentsList);
  52. }
  53. });
  54.  
  55. // Load fonts
  56. const fontLink = document.createElement('link');
  57. fontLink.rel = 'stylesheet';
  58. fontLink.href = 'https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c:wght@400;700&display=swap';
  59. document.head.appendChild(fontLink);
  60.  
  61. // Inject HTML
  62. const kawaiiHTML = `
  63. <div class="kawaii-cheat" id="kawaiiCheat">
  64. <div class="kawaii-header" id="kawaiiHeader">
  65. <img src="https://i.imgur.com/ptRhAHj.png" alt="Anime Girl" class="header-icon">
  66. <h2>✧ Kawaii Helper ✧</h2>
  67. <button class="minimize-btn" id="minimizeBtn">▼</button>
  68. </div>
  69. <div class="kawaii-body" id="kawaiiBody">
  70. <div class="kawaii-tabs">
  71. <button class="kawaii-tab active" data-tab="guessing">Guessing</button>
  72. <button class="kawaii-tab" data-tab="drawing">Drawing</button>
  73. </div>
  74. <div class="kawaii-content" id="guessing-tab">
  75. <div class="checkbox-container">
  76. <input type="checkbox" id="autoGuess">
  77. <label for="autoGuess">Auto Guess</label>
  78. </div>
  79. <div class="slider-container" id="speedContainer" style="display: none;">
  80. <div class="slider-label">Speed</div>
  81. <div class="custom-slider">
  82. <input type="range" id="guessSpeed" min="100" max="5000" value="1000" step="100">
  83. <div class="slider-track"></div>
  84. <span id="speedValue">1s</span>
  85. </div>
  86. </div>
  87. <div class="checkbox-container">
  88. <input type="checkbox" id="customWords">
  89. <label for="customWords">Custom Words</label>
  90. </div>
  91. <div class="dropzone-container" id="wordListContainer" style="display: none;">
  92. <div class="dropzone" id="wordListDropzone">
  93. <input type="file" id="wordList" accept=".txt">
  94. <div class="dropzone-content">
  95. <div class="dropzone-icon">❀</div>
  96. <p>Drop word list here or click to upload</p>
  97. </div>
  98. </div>
  99. </div>
  100. <div class="input-container">
  101. <input type="text" id="guessPattern" placeholder="Enter pattern (e.g., ___e___)">
  102. </div>
  103. <div class="hit-list" id="hitList">
  104. <div class="message">Type a pattern to see matches ✧</div>
  105. </div>
  106. </div>
  107. <div class="kawaii-content" id="drawing-tab" style="display: none;">
  108. <div class="dropzone-container">
  109. <div class="dropzone" id="imageDropzone">
  110. <input type="file" id="imageUpload" accept="image/*">
  111. <div class="dropzone-content">
  112. <div class="dropzone-icon">✎</div>
  113. <p>Drop image here or click to upload</p>
  114. </div>
  115. </div>
  116. <div class="image-preview" id="imagePreview" style="display: none;">
  117. <img id="previewImg">
  118. <div class="preview-controls">
  119. <button class="cancel-btn" id="cancelImage">✕</button>
  120. </div>
  121. </div>
  122. </div>
  123. <div class="slider-container">
  124. <div class="slider-label">Draw Speed</div>
  125. <div class="custom-slider">
  126. <input type="range" id="drawSpeed" min="1" max="50" value="20" step="1">
  127. <div class="slider-track"></div>
  128. <span id="drawSpeedValue">20</span>
  129. </div>
  130. </div>
  131. <div class="slider-container">
  132. <div class="slider-label">Max Colors</div>
  133. <div class="custom-slider">
  134. <input type="range" id="maxColors" min="3" max="100" value="20" step="1">
  135. <div class="slider-track"></div>
  136. <span id="maxColorsValue">20</span>
  137. </div>
  138. </div>
  139. <button class="draw-btn" id="sendDraw" disabled>Draw Now ✧</button>
  140. </div>
  141. <div class="kawaii-footer">
  142. <span class="credit-text">Made with by Anonimbiri & Gartic-Developers</span>
  143. </div>
  144. </div>
  145. </div>
  146. `;
  147.  
  148. const waitForBody = setInterval(() => {
  149. if (document.body) {
  150. clearInterval(waitForBody);
  151. document.body.insertAdjacentHTML('beforeend', kawaiiHTML);
  152. addStylesAndBehavior();
  153. }
  154. }, 100);
  155.  
  156. function addStylesAndBehavior() {
  157. const style = document.createElement('style');
  158. style.textContent = `
  159. :root {
  160. --primary-color: #FF69B4;
  161. --primary-dark: #FF1493;
  162. --primary-light: #FFC0CB;
  163. --bg-color: #FFB6C1;
  164. --text-color: #5d004f;
  165. --panel-bg: rgba(255, 182, 193, 0.95);
  166. --panel-border: #FF69B4;
  167. --element-bg: rgba(255, 240, 245, 0.7);
  168. --element-hover: rgba(255, 240, 245, 0.9);
  169. --element-active: #FF69B4;
  170. --element-active-text: #FFF0F5;
  171. }
  172.  
  173. .kawaii-cheat {
  174. position: fixed;
  175. top: 20px;
  176. right: 20px;
  177. width: 280px;
  178. background: var(--panel-bg);
  179. border-radius: 15px;
  180. box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
  181. padding: 10px;
  182. display: flex;
  183. flex-direction: column;
  184. gap: 10px;
  185. color: var(--text-color);
  186. user-select: none;
  187. z-index: 1000;
  188. font-family: 'M PLUS Rounded 1c', sans-serif;
  189. border: 2px solid var(--panel-border);
  190. transition: all 0.4s ease-in-out;
  191. max-height: calc(100vh - 40px);
  192. overflow: hidden;
  193. }
  194.  
  195. .kawaii-cheat.minimized {
  196. height: 50px;
  197. opacity: 0.9;
  198. transform: scale(0.95);
  199. overflow: hidden;
  200. }
  201.  
  202. .kawaii-cheat:not(.minimized) {
  203. opacity: 1;
  204. transform: scale(1);
  205. }
  206.  
  207. .kawaii-cheat.minimized .kawaii-body {
  208. opacity: 0;
  209. max-height: 0;
  210. overflow: hidden;
  211. transition: opacity 0.2s ease-in-out, max-height 0.4s ease-in-out;
  212. }
  213.  
  214. .kawaii-cheat:not(.minimized) .kawaii-body {
  215. opacity: 1;
  216. max-height: 500px;
  217. transition: opacity 0.2s ease-in-out 0.2s, max-height 0.4s ease-in-out;
  218. }
  219.  
  220. .kawaii-cheat.dragging {
  221. opacity: 0.8;
  222. transition: none; /* Sürükleme sırasında animasyonu devre dışı bırak */
  223. }
  224.  
  225. .kawaii-header {
  226. display: flex;
  227. justify-content: space-between;
  228. align-items: center;
  229. padding: 5px 10px;
  230. cursor: move;
  231. background: var(--element-bg);
  232. border-radius: 10px;
  233. border: 2px solid var(--primary-color);
  234. }
  235.  
  236. .header-icon {
  237. width: 30px;
  238. height: 30px;
  239. border-radius: 50%;
  240. margin-right: 10px;
  241. border: 1px dashed var(--primary-color);
  242. }
  243.  
  244. .kawaii-header h2 {
  245. margin: 0;
  246. font-size: 18px;
  247. font-weight: 700;
  248. color: var(--primary-dark);
  249. text-shadow: 1px 1px 2px var(--primary-light);
  250. }
  251.  
  252. .minimize-btn {
  253. background: transparent;
  254. border: 2px solid var(--primary-dark);
  255. border-radius: 6px;
  256. width: 24px;
  257. height: 24px;
  258. color: var(--primary-dark);
  259. font-size: 16px;
  260. line-height: 20px;
  261. text-align: center;
  262. cursor: pointer;
  263. transition: all 0.3s ease;
  264. }
  265.  
  266. .minimize-btn:hover {
  267. background: var(--primary-color);
  268. color: var(--element-active-text);
  269. border-color: var(--primary-color);
  270. transform: rotate(180deg);
  271. }
  272.  
  273. .kawaii-tabs {
  274. display: flex;
  275. gap: 8px;
  276. padding: 5px 0;
  277. }
  278.  
  279. .kawaii-tab {
  280. flex: 1;
  281. background: var(--element-bg);
  282. border: 1px dashed var(--primary-color);
  283. padding: 6px;
  284. border-radius: 10px;
  285. font-size: 12px;
  286. font-weight: 700;
  287. color: var(--text-color);
  288. cursor: pointer;
  289. transition: background 0.3s ease, transform 0.3s ease;
  290. text-align: center;
  291. }
  292.  
  293. .kawaii-tab.active {
  294. background: var(--primary-color);
  295. color: var(--element-active-text);
  296. border-color: var(--primary-dark);
  297. }
  298.  
  299. .kawaii-tab:hover:not(.active) {
  300. background: var(--element-hover);
  301. transform: scale(1.05);
  302. }
  303.  
  304. .kawaii-content {
  305. display: flex;
  306. flex-direction: column;
  307. gap: 10px;
  308. max-height: 55vh;
  309. overflow-y: auto;
  310. padding: 5px;
  311. }
  312.  
  313. .checkbox-container {
  314. display: flex;
  315. align-items: center;
  316. gap: 8px;
  317. background: var(--element-bg);
  318. padding: 8px;
  319. border-radius: 10px;
  320. border: 1px dashed var(--primary-color);
  321. cursor: pointer;
  322. transition: background 0.3s ease;
  323. }
  324.  
  325. .checkbox-container:hover {
  326. background: var(--element-hover);
  327. }
  328.  
  329. .checkbox-container input[type="checkbox"] {
  330. appearance: none;
  331. width: 18px;
  332. height: 18px;
  333. background: var(--element-active-text);
  334. border: 1px dashed var(--primary-color);
  335. border-radius: 50%;
  336. cursor: pointer;
  337. position: relative;
  338. }
  339.  
  340. .checkbox-container input[type="checkbox"]:checked {
  341. background: var(--primary-color);
  342. border-color: var(--primary-dark);
  343. }
  344.  
  345. .checkbox-container input[type="checkbox"]:checked::after {
  346. content: "♥";
  347. position: absolute;
  348. top: 50%;
  349. left: 50%;
  350. transform: translate(-50%, -50%);
  351. color: var(--element-active-text);
  352. font-size: 12px;
  353. }
  354.  
  355. .checkbox-container label {
  356. font-size: 12px;
  357. font-weight: 700;
  358. color: var(--text-color);
  359. cursor: pointer;
  360. }
  361.  
  362. .input-container {
  363. background: var(--element-bg);
  364. padding: 8px;
  365. border-radius: 10px;
  366. border: 1px dashed var(--primary-color);
  367. }
  368.  
  369. .input-container input[type="text"] {
  370. width: 100%;
  371. background: var(--element-active-text);
  372. border: 1px dashed var(--primary-light);
  373. border-radius: 8px;
  374. padding: 6px 10px;
  375. color: var(--text-color);
  376. font-size: 12px;
  377. font-weight: 500;
  378. box-sizing: border-box;
  379. transition: border-color 0.3s ease;
  380. outline: none;
  381. }
  382.  
  383. .input-container input[type="text"]:focus {
  384. border-color: var(--primary-dark);
  385. }
  386.  
  387. .dropzone-container {
  388. display: flex;
  389. flex-direction: column;
  390. gap: 10px;
  391. }
  392.  
  393. .dropzone {
  394. position: relative;
  395. background: var(--element-bg);
  396. border: 1px dashed var(--primary-color);
  397. border-radius: 10px;
  398. padding: 15px;
  399. display: flex;
  400. flex-direction: column;
  401. align-items: center;
  402. justify-content: center;
  403. cursor: pointer;
  404. transition: background 0.3s ease, border-color 0.3s ease;
  405. min-height: 80px;
  406. }
  407.  
  408. .dropzone:hover, .dropzone.drag-over {
  409. background: var(--element-hover);
  410. border-color: var(--primary-dark);
  411. }
  412.  
  413. .dropzone input[type="file"] {
  414. position: absolute;
  415. top: 0;
  416. left: 0;
  417. width: 100%;
  418. height: 100%;
  419. opacity: 0;
  420. cursor: pointer;
  421. }
  422.  
  423. .dropzone-content {
  424. display: flex;
  425. flex-direction: column;
  426. align-items: center;
  427. gap: 8px;
  428. text-align: center;
  429. pointer-events: none;
  430. }
  431.  
  432. .dropzone-icon {
  433. font-size: 24px;
  434. color: var(--primary-color);
  435. animation: pulse 1.5s infinite ease-in-out;
  436. }
  437.  
  438. @keyframes pulse {
  439. 0%, 100% { transform: scale(1); }
  440. 50% { transform: scale(1.1); }
  441. }
  442.  
  443. .dropzone-content p {
  444. margin: 0;
  445. color: var(--text-color);
  446. font-size: 12px;
  447. font-weight: 500;
  448. }
  449.  
  450. .slider-container {
  451. display: flex;
  452. flex-direction: column;
  453. gap: 6px;
  454. background: var(--element-bg);
  455. padding: 8px;
  456. border-radius: 10px;
  457. border: 1px dashed var(--primary-color);
  458. }
  459.  
  460. .slider-label {
  461. font-size: 12px;
  462. color: var(--text-color);
  463. font-weight: 700;
  464. text-align: center;
  465. }
  466.  
  467. .custom-slider {
  468. position: relative;
  469. height: 25px;
  470. padding: 0 8px;
  471. }
  472.  
  473. .custom-slider input[type="range"] {
  474. -webkit-appearance: none;
  475. width: 100%;
  476. height: 6px;
  477. background: transparent;
  478. position: absolute;
  479. top: 50%;
  480. left: 0;
  481. transform: translateY(-50%);
  482. z-index: 2;
  483. }
  484.  
  485. .custom-slider .slider-track {
  486. position: absolute;
  487. top: 50%;
  488. left: 0;
  489. width: 100%;
  490. height: 6px;
  491. background: linear-gradient(to right, var(--primary-dark) 0%, var(--primary-dark) var(--slider-progress), var(--primary-light) var(--slider-progress), var(--primary-light) 100%);
  492. border-radius: 3px;
  493. transform: translateY(-50%);
  494. z-index: 1;
  495. }
  496.  
  497. .custom-slider input[type="range"]::-webkit-slider-thumb {
  498. -webkit-appearance: none;
  499. width: 16px;
  500. height: 16px;
  501. background: var(--primary-color);
  502. border-radius: 50%;
  503. border: 1px dashed var(--element-active-text);
  504. cursor: pointer;
  505. transition: transform 0.3s ease;
  506. }
  507.  
  508. .custom-slider input[type="range"]::-webkit-slider-thumb:hover {
  509. transform: scale(1.2);
  510. }
  511.  
  512. .custom-slider span {
  513. position: absolute;
  514. bottom: -15px;
  515. left: 50%;
  516. transform: translateX(-50%);
  517. font-size: 10px;
  518. color: var(--text-color);
  519. background: var(--element-active-text);
  520. padding: 2px 6px;
  521. border-radius: 8px;
  522. border: 1px dashed var(--primary-color);
  523. white-space: nowrap;
  524. }
  525.  
  526. .hit-list {
  527. max-height: 180px;
  528. overflow-y: scroll;
  529. background: var(--element-bg);
  530. border: 1px dashed var(--primary-color);
  531. border-radius: 10px;
  532. padding: 8px;
  533. display: flex;
  534. flex-direction: column;
  535. gap: 6px;
  536. scrollbar-width: thin;
  537. scrollbar-color: var(--primary-color) var(--element-bg);
  538. }
  539.  
  540. .hit-list::-webkit-scrollbar {
  541. width: 6px;
  542. }
  543.  
  544. .hit-list::-webkit-scrollbar-thumb {
  545. background-color: var(--primary-color);
  546. border-radius: 10px;
  547. }
  548.  
  549. .hit-list::-webkit-scrollbar-track {
  550. background: var(--element-bg);
  551. }
  552.  
  553. .hit-list button {
  554. background: rgba(255, 240, 245, 0.8);
  555. border: 1px dashed var(--primary-color);
  556. padding: 6px 10px;
  557. border-radius: 8px;
  558. color: var(--text-color);
  559. font-size: 12px;
  560. font-weight: 700;
  561. cursor: pointer;
  562. transition: background 0.3s ease, transform 0.3s ease;
  563. text-align: left;
  564. }
  565.  
  566. .hit-list button:hover:not(.tried) {
  567. background: var(--primary-color);
  568. color: var(--element-active-text);
  569. transform: scale(1.03);
  570. }
  571.  
  572. .hit-list button.tried {
  573. background: rgba(255, 182, 193, 0.6);
  574. border-color: var(--primary-light);
  575. color: var(--primary-dark);
  576. opacity: 0.7;
  577. cursor: not-allowed;
  578. }
  579.  
  580. .hit-list .tried-label {
  581. font-size: 10px;
  582. color: var(--primary-dark);
  583. text-align: center;
  584. padding: 4px;
  585. background: var(--element-active-text);
  586. border-radius: 8px;
  587. border: 1px dashed var(--primary-color);
  588. }
  589.  
  590. .hit-list .message {
  591. font-size: 12px;
  592. color: var(--text-color);
  593. text-align: center;
  594. padding: 8px;
  595. }
  596.  
  597. .image-preview {
  598. position: relative;
  599. margin-top: 10px;
  600. background: var(--element-bg);
  601. padding: 8px;
  602. border-radius: 10px;
  603. border: 1px dashed var(--primary-color);
  604. }
  605.  
  606. .image-preview img {
  607. max-width: 100%;
  608. max-height: 120px;
  609. border-radius: 8px;
  610. display: block;
  611. margin: 0 auto;
  612. }
  613.  
  614. .preview-controls {
  615. position: absolute;
  616. top: 12px;
  617. right: 12px;
  618. display: flex;
  619. gap: 6px;
  620. }
  621.  
  622. .cancel-btn {
  623. background: transparent;
  624. border: 2px solid var(--primary-dark);
  625. border-radius: 6px;
  626. width: 24px;
  627. height: 24px;
  628. color: var(--primary-dark);
  629. font-size: 16px;
  630. line-height: 20px;
  631. text-align: center;
  632. cursor: pointer;
  633. transition: all 0.3s ease;
  634. }
  635.  
  636. .cancel-btn:hover {
  637. background: var(--primary-dark);
  638. color: var(--element-active-text);
  639. transform: scale(1.1);
  640. }
  641.  
  642. .draw-btn {
  643. background: var(--primary-color);
  644. border: 1px dashed var(--primary-dark);
  645. padding: 8px;
  646. border-radius: 10px;
  647. color: var(--element-active-text);
  648. font-size: 14px;
  649. font-weight: 700;
  650. cursor: pointer;
  651. transition: background 0.3s ease, transform 0.3s ease;
  652. text-align: center;
  653. }
  654.  
  655. .draw-btn:hover:not(:disabled) {
  656. background: var(--primary-dark);
  657. transform: scale(1.05);
  658. }
  659.  
  660. .draw-btn:disabled {
  661. background: rgba(255, 105, 180, 0.5);
  662. cursor: not-allowed;
  663. }
  664.  
  665. .kawaii-footer {
  666. display: flex;
  667. justify-content: center;
  668. align-items: center;
  669. margin-top: 10px;
  670. padding: 6px;
  671. background: var(--element-bg);
  672. border-radius: 10px;
  673. border: 2px solid var(--primary-color);
  674. }
  675.  
  676. .credit-text {
  677. font-size: 10px;
  678. color: var(--text-color);
  679. font-weight: 700;
  680. }
  681. `;
  682. document.head.appendChild(style);
  683.  
  684. // DOM Elements
  685. const kawaiiCheat = document.getElementById('kawaiiCheat');
  686. const kawaiiHeader = document.getElementById('kawaiiHeader');
  687. const minimizeBtn = document.getElementById('minimizeBtn');
  688. const tabButtons = document.querySelectorAll('.kawaii-tab');
  689. const tabContents = document.querySelectorAll('.kawaii-content');
  690. const autoGuessCheckbox = document.getElementById('autoGuess');
  691. const speedContainer = document.getElementById('speedContainer');
  692. const guessSpeed = document.getElementById('guessSpeed');
  693. const speedValue = document.getElementById('speedValue');
  694. const customWordsCheckbox = document.getElementById('customWords');
  695. const wordListContainer = document.getElementById('wordListContainer');
  696. const wordListDropzone = document.getElementById('wordListDropzone');
  697. const wordListInput = document.getElementById('wordList');
  698. const guessPattern = document.getElementById('guessPattern');
  699. const hitList = document.getElementById('hitList');
  700. const imageDropzone = document.getElementById('imageDropzone');
  701. const imageUpload = document.getElementById('imageUpload');
  702. const imagePreview = document.getElementById('imagePreview');
  703. const previewImg = document.getElementById('previewImg');
  704. const cancelImage = document.getElementById('cancelImage');
  705. const drawSpeed = document.getElementById('drawSpeed');
  706. const drawSpeedValue = document.getElementById('drawSpeedValue');
  707. const maxColors = document.getElementById('maxColors');
  708. const maxColorsValue = document.getElementById('maxColorsValue');
  709. const sendDraw = document.getElementById('sendDraw');
  710.  
  711. // Variables
  712. let isDragging = false;
  713. let initialX, initialY;
  714. let xOffset = 0, yOffset = 0;
  715. let rafId = null; // requestAnimationFrame ID
  716. let autoGuessInterval = null;
  717. let wordList = { "Custom": [] };
  718. let triedLabelAdded = false;
  719.  
  720. const wordListURLs = {
  721. "General (en)": "https://cdn.jsdelivr.net/gh/Gartic-Developers/Gartic-WordList@master/languages/English/general.json",
  722. "General (tr)": "https://cdn.jsdelivr.net/gh/Gartic-Developers/Gartic-WordList@master/languages/Turkish/general.json"
  723. };
  724.  
  725. // Utility Functions
  726. function updateSliderTrack(slider) {
  727. const min = parseInt(slider.min);
  728. const max = parseInt(slider.max);
  729. const value = parseInt(slider.value);
  730. const progress = ((value - min) / (max - min)) * 100;
  731. slider.parentElement.querySelector('.slider-track').style.setProperty('--slider-progress', `${progress}%`);
  732. }
  733.  
  734. function preventDefaults(e) {
  735. e.preventDefault();
  736. e.stopPropagation();
  737. }
  738.  
  739. // Initial Setup
  740. updateSliderTrack(guessSpeed);
  741. updateSliderTrack(drawSpeed);
  742. updateSliderTrack(maxColors);
  743.  
  744. // Dragging Functionality with Optimization
  745. kawaiiHeader.addEventListener('mousedown', (e) => {
  746. if (e.target !== minimizeBtn) {
  747. initialX = e.clientX - xOffset;
  748. initialY = e.clientY - yOffset;
  749. isDragging = true;
  750. kawaiiCheat.classList.add('dragging');
  751. if (rafId) cancelAnimationFrame(rafId); // Önceki frame'i iptal et
  752. }
  753. });
  754.  
  755. document.addEventListener('mousemove', (e) => {
  756. if (isDragging) {
  757. e.preventDefault();
  758. const newX = e.clientX - initialX;
  759. const newY = e.clientY - initialY;
  760.  
  761. if (rafId) cancelAnimationFrame(rafId); // Tekrarlanan frame'leri önle
  762. rafId = requestAnimationFrame(() => {
  763. kawaiiCheat.style.transform = `translate(${newX}px, ${newY}px)`;
  764. xOffset = newX;
  765. yOffset = newY;
  766. });
  767. }
  768. });
  769.  
  770. document.addEventListener('mouseup', () => {
  771. if (isDragging) {
  772. isDragging = false;
  773. kawaiiCheat.classList.remove('dragging');
  774. if (rafId) cancelAnimationFrame(rafId); // Son frame'i temizle
  775. }
  776. });
  777.  
  778. // Minimize Button
  779. minimizeBtn.addEventListener('click', () => {
  780. kawaiiCheat.classList.toggle('minimized');
  781. minimizeBtn.textContent = kawaiiCheat.classList.contains('minimized') ? '▲' : '▼';
  782. });
  783.  
  784. // Tab Switching
  785. tabButtons.forEach(btn => {
  786. btn.addEventListener('click', () => {
  787. tabButtons.forEach(b => b.classList.remove('active'));
  788. tabContents.forEach(c => c.style.display = 'none');
  789. btn.classList.add('active');
  790. document.getElementById(`${btn.dataset.tab}-tab`).style.display = 'flex';
  791. });
  792. });
  793.  
  794. // Checkbox Container Click
  795. document.querySelectorAll('.checkbox-container').forEach(container => {
  796. container.addEventListener('click', (e) => {
  797. const checkbox = container.querySelector('input[type="checkbox"]');
  798. if (e.target !== checkbox) {
  799. checkbox.checked = !checkbox.checked;
  800. checkbox.dispatchEvent(new Event('change'));
  801. }
  802. });
  803. });
  804.  
  805. // Auto Guess Checkbox
  806. autoGuessCheckbox.addEventListener('change', (e) => {
  807. speedContainer.style.display = e.target.checked ? 'flex' : 'none';
  808. if (!e.target.checked) stopAutoGuess();
  809. else if (guessPattern.value) startAutoGuess();
  810. });
  811.  
  812. // Guess Speed Slider
  813. guessSpeed.addEventListener('input', (e) => {
  814. updateSliderTrack(e.target);
  815. speedValue.textContent = e.target.value >= 1000 ? `${e.target.value / 1000}s` : `${e.target.value}ms`;
  816. if (autoGuessCheckbox.checked && autoGuessInterval) {
  817. stopAutoGuess();
  818. startAutoGuess();
  819. }
  820. });
  821.  
  822. // Custom Words Checkbox
  823. customWordsCheckbox.addEventListener('change', (e) => {
  824. wordListContainer.style.display = e.target.checked ? 'block' : 'none';
  825. updateHitList(guessPattern.value.trim());
  826. });
  827.  
  828. // Word List Dropzone
  829. ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
  830. wordListDropzone.addEventListener(eventName, preventDefaults, false);
  831. });
  832.  
  833. wordListDropzone.addEventListener('dragenter', () => wordListDropzone.classList.add('drag-over'));
  834. wordListDropzone.addEventListener('dragover', () => wordListDropzone.classList.add('drag-over'));
  835. wordListDropzone.addEventListener('dragleave', () => wordListDropzone.classList.remove('drag-over'));
  836. wordListDropzone.addEventListener('drop', (e) => {
  837. wordListDropzone.classList.remove('drag-over');
  838. const file = e.dataTransfer.files[0];
  839. if (file && file.type === 'text/plain') handleWordListFile(file);
  840. });
  841.  
  842. wordListInput.addEventListener('change', (e) => {
  843. const file = e.target.files[0];
  844. if (file) {
  845. handleWordListFile(file);
  846. e.target.value = '';
  847. }
  848. });
  849.  
  850. function handleWordListFile(file) {
  851. const reader = new FileReader();
  852. reader.onload = function(event) {
  853. wordList["Custom"] = event.target.result.split('\n').map(word => word.trim()).filter(word => word.length > 0);
  854. alert(`Loaded ${wordList["Custom"].length} words from ${file.name}`);
  855. updateHitList(guessPattern.value.trim());
  856. };
  857. reader.readAsText(file);
  858. }
  859.  
  860. // Guess Pattern Input
  861. guessPattern.addEventListener('input', (e) => updateHitList(e.target.value.trim()));
  862.  
  863. // Hit List Functionality
  864. hitList.addEventListener('click', (e) => {
  865. if (e.target.tagName === 'BUTTON' && !e.target.classList.contains('tried')) {
  866. const button = e.target;
  867. button.classList.add('tried');
  868. if (!triedLabelAdded && hitList.querySelectorAll('button.tried').length === 1) {
  869. const triedLabel = document.createElement('div');
  870. triedLabel.classList.add('tried-label');
  871. triedLabel.textContent = 'Tried Words';
  872. hitList.appendChild(triedLabel);
  873. triedLabelAdded = true;
  874. }
  875. if (window.game && window.game._socket) {
  876. window.game._socket.emit(13, window.game._codigo, button.textContent);
  877. }
  878. hitList.appendChild(button);
  879. }
  880. });
  881.  
  882. function startAutoGuess() {
  883. if (!autoGuessCheckbox.checked) return;
  884. stopAutoGuess();
  885. const speed = parseInt(guessSpeed.value);
  886. autoGuessInterval = setInterval(() => {
  887. const buttons = hitList.querySelectorAll('button:not(.tried)');
  888. if (buttons.length > 0 && window.game && window.game._socket) {
  889. const word = buttons[0].textContent;
  890. buttons[0].classList.add('tried');
  891. window.game._socket.emit(13, window.game._codigo, word);
  892. if (!triedLabelAdded && hitList.querySelectorAll('button.tried').length === 1) {
  893. const triedLabel = document.createElement('div');
  894. triedLabel.classList.add('tried-label');
  895. triedLabel.textContent = 'Tried Words';
  896. hitList.appendChild(triedLabel);
  897. triedLabelAdded = true;
  898. }
  899. hitList.appendChild(buttons[0]);
  900. }
  901. }, speed);
  902. }
  903.  
  904. function stopAutoGuess() {
  905. if (autoGuessInterval) {
  906. clearInterval(autoGuessInterval);
  907. autoGuessInterval = null;
  908. }
  909. }
  910.  
  911. function updateHitList(pattern) {
  912. hitList.innerHTML = '';
  913. triedLabelAdded = false;
  914. const activeTheme = customWordsCheckbox.checked || !window.game || !window.game._dadosSala || !window.game._dadosSala.tema
  915. ? "Custom" : window.game._dadosSala.tema;
  916. const activeList = wordList[activeTheme] || [];
  917.  
  918. if (!pattern) {
  919. if (activeList.length === 0) {
  920. hitList.innerHTML = `<div class="message">${customWordsCheckbox.checked ? 'Upload a custom word list ✧' : 'No words available ✧'}</div>`;
  921. } else {
  922. activeList.forEach(word => {
  923. const button = document.createElement('button');
  924. button.textContent = word;
  925. hitList.appendChild(button);
  926. });
  927. }
  928. return;
  929. }
  930.  
  931. const regex = new RegExp(`^${pattern.split('').map(char => char === '_' ? '.' : char).join('')}$`, 'i');
  932. const matches = activeList.filter(word => regex.test(word));
  933.  
  934. if (matches.length === 0) {
  935. hitList.innerHTML = '<div class="message">No matches found ✧</div>';
  936. } else {
  937. matches.forEach(word => {
  938. const button = document.createElement('button');
  939. button.textContent = word;
  940. hitList.appendChild(button);
  941. });
  942. }
  943. }
  944.  
  945. async function fetchWordList(theme) {
  946. if (!wordList[theme] && wordListURLs[theme]) {
  947. try {
  948. const response = await fetch(wordListURLs[theme]);
  949. if (!response.ok) throw new Error(`Failed to fetch ${theme} word list`);
  950. const data = await response.json();
  951. wordList[theme] = data.words || data;
  952. console.log(`Loaded ${wordList[theme].length} words for ${theme}`);
  953. } catch (error) {
  954. console.error(`Error fetching word list for ${theme}:`, error);
  955. wordList[theme] = [];
  956. }
  957. }
  958. }
  959.  
  960. // Image Upload
  961. ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
  962. imageDropzone.addEventListener(eventName, preventDefaults, false);
  963. });
  964.  
  965. imageDropzone.addEventListener('dragenter', () => imageDropzone.classList.add('drag-over'));
  966. imageDropzone.addEventListener('dragover', () => imageDropzone.classList.add('drag-over'));
  967. imageDropzone.addEventListener('dragleave', () => imageDropzone.classList.remove('drag-over'));
  968. imageDropzone.addEventListener('drop', (e) => {
  969. imageDropzone.classList.remove('drag-over');
  970. const file = e.dataTransfer.files[0];
  971. if (file && file.type.startsWith('image/')) handleImageFile(file);
  972. });
  973.  
  974. imageUpload.addEventListener('change', (e) => {
  975. const file = e.target.files[0];
  976. if (file) {
  977. handleImageFile(file);
  978. e.target.value = '';
  979. }
  980. });
  981.  
  982. function handleImageFile(file) {
  983. const reader = new FileReader();
  984. reader.onload = function(event) {
  985. previewImg.src = event.target.result;
  986. imageDropzone.style.display = 'none';
  987. imagePreview.style.display = 'block';
  988. sendDraw.disabled = false;
  989. };
  990. reader.readAsDataURL(file);
  991. }
  992.  
  993. cancelImage.addEventListener('click', () => {
  994. previewImg.src = '';
  995. imageDropzone.style.display = 'flex';
  996. imagePreview.style.display = 'none';
  997. sendDraw.disabled = true;
  998. imageUpload.value = '';
  999. });
  1000.  
  1001. drawSpeed.addEventListener('input', (e) => {
  1002. updateSliderTrack(e.target);
  1003. drawSpeedValue.textContent = e.target.value;
  1004. });
  1005.  
  1006. maxColors.addEventListener('input', (e) => {
  1007. updateSliderTrack(e.target);
  1008. maxColorsValue.textContent = e.target.value;
  1009. });
  1010.  
  1011. sendDraw.addEventListener('click', () => {
  1012. if (previewImg.src) {
  1013. if (!window.game || !window.game.turn) {
  1014. alert('Not your turn or game not loaded! ✧');
  1015. return;
  1016. }
  1017. sendDraw.disabled = true;
  1018. processAndDrawImage(previewImg.src);
  1019. }
  1020. });
  1021.  
  1022. // Socket Integration
  1023. const checkGame = setInterval(() => {
  1024. if (window.game && window.game._socket) {
  1025. clearInterval(checkGame);
  1026. const currentTheme = window.game._dadosSala.tema || "Custom";
  1027. if (currentTheme !== "Custom") {
  1028. fetchWordList(currentTheme).then(() => updateHitList(guessPattern.value.trim()));
  1029. }
  1030.  
  1031. window.game._socket.on(30, (hint) => {
  1032. hint = String(hint).replace(/,/g, '');
  1033. guessPattern.value = hint;
  1034. updateHitList(hint);
  1035. if (autoGuessCheckbox.checked) startAutoGuess();
  1036. });
  1037.  
  1038. window.game._socket.on(19, () => {
  1039. guessPattern.value = '';
  1040. stopAutoGuess();
  1041. updateHitList('');
  1042. });
  1043.  
  1044. window.game._socket.on(15, (playerId) => {
  1045. if (playerId === window.game.me.id) {
  1046. guessPattern.value = '';
  1047. stopAutoGuess();
  1048. updateHitList('');
  1049. }
  1050. });
  1051.  
  1052. let lastTheme = currentTheme;
  1053. setInterval(() => {
  1054. const newTheme = window.game._dadosSala.tema || "Custom";
  1055. if (newTheme !== lastTheme && newTheme !== "Custom") {
  1056. lastTheme = newTheme;
  1057. fetchWordList(newTheme).then(() => updateHitList(guessPattern.value.trim()));
  1058. }
  1059. }, 1000);
  1060. }
  1061. }, 100);
  1062. }
  1063.  
  1064. function processAndDrawImage(imageSrc) {
  1065. if (!window.game || !window.game._socket || !window.game._desenho || !window.game.turn) {
  1066. alert('Game not ready or not your turn! ✧');
  1067. return;
  1068. }
  1069.  
  1070. const img = new Image();
  1071. img.crossOrigin = "Anonymous";
  1072. img.onload = async function() {
  1073. const gameCanvas = window.game._desenho._canvas.canvas;
  1074. if (!gameCanvas || !gameCanvas.width || !gameCanvas.height) {
  1075. alert('Canvas not accessible! ✧');
  1076. sendDraw.disabled = false;
  1077. return;
  1078. }
  1079.  
  1080. const ctx = gameCanvas.getContext('2d');
  1081. if (!ctx) {
  1082. alert('Canvas context not available! ✧');
  1083. sendDraw.disabled = false;
  1084. return;
  1085. }
  1086.  
  1087. const canvasWidth = Math.floor(gameCanvas.width);
  1088. const canvasHeight = Math.floor(gameCanvas.height);
  1089.  
  1090. const tempCanvas = document.createElement('canvas');
  1091. const tempCtx = tempCanvas.getContext('2d');
  1092. if (!tempCtx) {
  1093. alert('Temp canvas context failed! ✧');
  1094. sendDraw.disabled = false;
  1095. return;
  1096. }
  1097.  
  1098. const scale = Math.min(canvasWidth / img.width, canvasHeight / img.height);
  1099. const newWidth = Math.floor(img.width * scale);
  1100. const newHeight = Math.floor(img.height * scale);
  1101.  
  1102. tempCanvas.width = canvasWidth;
  1103. tempCanvas.height = canvasHeight;
  1104.  
  1105. const offsetX = Math.floor((canvasWidth - newWidth) / 2);
  1106. const offsetY = Math.floor((canvasHeight - newHeight) / 2);
  1107.  
  1108. tempCtx.drawImage(img, offsetX, offsetY, newWidth, newHeight);
  1109.  
  1110. let imageData;
  1111. try {
  1112. imageData = tempCtx.getImageData(0, 0, canvasWidth, canvasHeight);
  1113. } catch (e) {
  1114. alert('Image data error: ' + e.message + ' ✧');
  1115. sendDraw.disabled = false;
  1116. return;
  1117. }
  1118.  
  1119. const data = imageData.data;
  1120. const drawSpeedValue = Math.max(300, parseInt(drawSpeed.value) || 500);
  1121.  
  1122. // Get maxColors from menu (fixed from drawSpeed to maxColorsSelect)
  1123. const maxColorsValue = parseInt(maxColors.value) || 20;
  1124.  
  1125. // Image bounds
  1126. const imgLeft = offsetX;
  1127. const imgRight = offsetX + newWidth - 1;
  1128. const imgTop = offsetY;
  1129. const imgBottom = offsetY + newHeight - 1;
  1130.  
  1131. // Background detection
  1132. const colorCounts = new Map();
  1133. let backgroundColor = [255, 255, 255];
  1134. const sampleStep = Math.max(1, Math.floor(newWidth / 50));
  1135.  
  1136. for (let x = imgLeft; x <= imgRight; x += sampleStep) {
  1137. for (let y = imgTop; y <= imgBottom; y += sampleStep) {
  1138. const index = (y * canvasWidth + x) * 4;
  1139. const r = Math.round(data[index] / 20) * 20;
  1140. const g = Math.round(data[index+1] / 20) * 20;
  1141. const b = Math.round(data[index+2] / 20) * 20;
  1142. const key = `${r},${g},${b}`;
  1143. colorCounts.set(key, (colorCounts.get(key) || 0) + 1);
  1144. }
  1145. }
  1146.  
  1147. let maxCount = 0;
  1148. for (const [key, count] of colorCounts) {
  1149. if (count > maxCount) {
  1150. maxCount = count;
  1151. backgroundColor = key.split(',').map(Number);
  1152. }
  1153. }
  1154.  
  1155. const bgHex = 'x' + backgroundColor.map(c =>
  1156. c.toString(16).padStart(2, '0').toUpperCase()
  1157. ).join('');
  1158.  
  1159. // Clear and set background (both socket and local)
  1160. window.game._socket.emit(10, window.game._codigo, [4]);
  1161. ctx.clearRect(0, 0, canvasWidth, canvasHeight);
  1162. await new Promise(resolve => setTimeout(resolve, drawSpeedValue));
  1163.  
  1164. window.game._socket.emit(10, window.game._codigo, [5, bgHex]);
  1165. ctx.fillStyle = `#${bgHex.slice(1)}`;
  1166. await new Promise(resolve => setTimeout(resolve, drawSpeedValue));
  1167.  
  1168. window.game._socket.emit(10, window.game._codigo, [3, 0, 0, canvasWidth, canvasHeight]);
  1169. ctx.fillRect(0, 0, canvasWidth, canvasHeight);
  1170. await new Promise(resolve => setTimeout(resolve, drawSpeedValue));
  1171.  
  1172. // Color clustering for foreground
  1173. const colorClusters = new Map();
  1174. for (let y = imgTop; y < imgBottom; y += sampleStep) {
  1175. for (let x = imgLeft; x < imgRight; x += sampleStep) {
  1176. const index = (y * canvasWidth + x) * 4;
  1177. const r = Math.round(data[index] / 20) * 20;
  1178. const g = Math.round(data[index+1] / 20) * 20;
  1179. const b = Math.round(data[index+2] / 20) * 20;
  1180. const key = `${r},${g},${b}`;
  1181.  
  1182. if (colorDistance([r, g, b], backgroundColor) > 60) {
  1183. colorClusters.set(key, (colorClusters.get(key) || 0) + 1);
  1184. }
  1185. }
  1186. }
  1187.  
  1188. const topColors = [...colorClusters.entries()]
  1189. .sort((a, b) => b[1] - a[1])
  1190. .slice(0, maxColorsValue)
  1191. .map(([key]) => ({
  1192. rgb: key.split(',').map(Number),
  1193. hex: 'x' + key.split(',').map(c =>
  1194. Number(c).toString(16).padStart(2, '0').toUpperCase()
  1195. ).join('')
  1196. }));
  1197.  
  1198. // Fill regions by color
  1199. const fillsByColor = {};
  1200. const visited = new Set();
  1201. const stripHeight = 2;
  1202. const minStripWidth = 10;
  1203.  
  1204. for (let y = imgTop; y < imgBottom; y += stripHeight) {
  1205. let startX = null;
  1206. let currentColor = null;
  1207. let stripWidth = 0;
  1208.  
  1209. for (let x = imgLeft; x < imgRight; x += 1) {
  1210. const index = (y * canvasWidth + x) * 4;
  1211. const pixelColor = [data[index], data[index+1], data[index+2]];
  1212. const bgDist = colorDistance(pixelColor, backgroundColor);
  1213.  
  1214. if (bgDist > 60 && !visited.has(`${x},${y}`)) {
  1215. const nearestColor = topColors.reduce((prev, curr) =>
  1216. colorDistance(pixelColor, prev.rgb) < colorDistance(pixelColor, curr.rgb) ? prev : curr
  1217. );
  1218.  
  1219. if (startX === null || currentColor?.hex !== nearestColor.hex) {
  1220. if (startX !== null && stripWidth >= minStripWidth) {
  1221. if (!fillsByColor[currentColor.hex]) fillsByColor[currentColor.hex] = [];
  1222. fillsByColor[currentColor.hex].push([startX, y, stripWidth, stripHeight]);
  1223. for (let dx = 0; dx < stripWidth; dx++) {
  1224. visited.add(`${startX + dx},${y}`);
  1225. }
  1226. }
  1227. startX = x;
  1228. currentColor = nearestColor;
  1229. stripWidth = 1;
  1230. } else {
  1231. stripWidth++;
  1232. }
  1233. } else if (startX !== null && bgDist <= 60) {
  1234. if (stripWidth >= minStripWidth) {
  1235. if (!fillsByColor[currentColor.hex]) fillsByColor[currentColor.hex] = [];
  1236. fillsByColor[currentColor.hex].push([startX, y, stripWidth, stripHeight]);
  1237. for (let dx = 0; dx < stripWidth; dx++) {
  1238. visited.add(`${startX + dx},${y}`);
  1239. }
  1240. }
  1241. startX = null;
  1242. currentColor = null;
  1243. stripWidth = 0;
  1244. }
  1245. }
  1246.  
  1247. if (startX !== null && stripWidth >= minStripWidth) {
  1248. if (!fillsByColor[currentColor.hex]) fillsByColor[currentColor.hex] = [];
  1249. fillsByColor[currentColor.hex].push([startX, y, stripWidth, stripHeight]);
  1250. for (let dx = 0; dx < stripWidth; dx++) {
  1251. visited.add(`${startX + dx},${y}`);
  1252. }
  1253. }
  1254. }
  1255.  
  1256. // Draw fills grouped by color (socket and local)
  1257. for (const color in fillsByColor) {
  1258. const fillCommand = [3];
  1259. fillsByColor[color].forEach(([x, y, width, height]) => {
  1260. fillCommand.push(x, y, width, height);
  1261. });
  1262.  
  1263. window.game._socket.emit(10, window.game._codigo, [5, color]);
  1264. ctx.fillStyle = `#${color.slice(1)}`;
  1265. await new Promise(resolve => setTimeout(resolve, drawSpeedValue));
  1266.  
  1267. window.game._socket.emit(10, window.game._codigo, fillCommand);
  1268. fillsByColor[color].forEach(([x, y, width, height]) => {
  1269. ctx.fillRect(x, y, width, height);
  1270. });
  1271. await new Promise(resolve => setTimeout(resolve, drawSpeedValue));
  1272. }
  1273.  
  1274. // Straight lines for remaining details
  1275. const lines = [];
  1276. const lineStep = 6; // Larger step for efficiency
  1277. const minLineLength = 10; // Minimum length for a line to be drawn
  1278.  
  1279. for (let y = imgTop; y < imgBottom; y += lineStep) {
  1280. for (let x = imgLeft; x < imgRight; x += lineStep) {
  1281. if (visited.has(`${x},${y}`)) continue;
  1282.  
  1283. const index = (y * canvasWidth + x) * 4;
  1284. const pixelColor = [data[index], data[index+1], data[index+2]];
  1285. if (colorDistance(pixelColor, backgroundColor) <= 60) continue;
  1286.  
  1287. const nearestColor = topColors.reduce((prev, curr) =>
  1288. colorDistance(pixelColor, prev.rgb) < colorDistance(pixelColor, curr.rgb) ? prev : curr
  1289. );
  1290.  
  1291. // Check horizontal vs vertical preference based on neighboring pixels
  1292. let horizontalScore = 0;
  1293. let verticalScore = 0;
  1294.  
  1295. // Check right (horizontal)
  1296. for (let dx = 1; dx <= minLineLength; dx++) {
  1297. const nx = x + dx;
  1298. if (nx >= imgRight) break;
  1299. const ni = (y * canvasWidth + nx) * 4;
  1300. const nextColor = [data[ni], data[ni+1], data[ni+2]];
  1301. if (colorDistance(nextColor, pixelColor) < 50 && !visited.has(`${nx},${y}`)) {
  1302. horizontalScore++;
  1303. } else {
  1304. break;
  1305. }
  1306. }
  1307.  
  1308. // Check down (vertical)
  1309. for (let dy = 1; dy <= minLineLength; dy++) {
  1310. const ny = y + dy;
  1311. if (ny >= imgBottom) break;
  1312. const ni = (ny * canvasWidth + x) * 4;
  1313. const nextColor = [data[ni], data[ni+1], data[ni+2]];
  1314. if (colorDistance(nextColor, pixelColor) < 50 && !visited.has(`${x},${ny}`)) {
  1315. verticalScore++;
  1316. } else {
  1317. break;
  1318. }
  1319. }
  1320.  
  1321. // Decide direction and length
  1322. if (horizontalScore >= verticalScore && horizontalScore >= minLineLength / 2) {
  1323. const lineLength = Math.min(horizontalScore, imgRight - x);
  1324. if (lineLength >= minLineLength) {
  1325. lines.push({
  1326. points: [[x, y], [x + lineLength - 1, y]],
  1327. color: nearestColor.hex
  1328. });
  1329. for (let dx = 0; dx < lineLength; dx++) {
  1330. visited.add(`${x + dx},${y}`);
  1331. }
  1332. }
  1333. } else if (verticalScore >= minLineLength / 2) {
  1334. const lineLength = Math.min(verticalScore, imgBottom - y);
  1335. if (lineLength >= minLineLength) {
  1336. lines.push({
  1337. points: [[x, y], [x, y + lineLength - 1]],
  1338. color: nearestColor.hex
  1339. });
  1340. for (let dy = 0; dy < lineLength; dy++) {
  1341. visited.add(`${x},${y + dy}`);
  1342. }
  1343. }
  1344. }
  1345. }
  1346. }
  1347.  
  1348. // Draw straight lines (socket and local)
  1349. for (const { points, color } of lines) {
  1350. window.game._socket.emit(10, window.game._codigo, [5, color]);
  1351. ctx.strokeStyle = `#${color.slice(1)}`;
  1352. await new Promise(resolve => setTimeout(resolve, drawSpeedValue));
  1353.  
  1354. window.game._socket.emit(10, window.game._codigo, [6, 4]);
  1355. ctx.lineWidth = 4;
  1356. await new Promise(resolve => setTimeout(resolve, drawSpeedValue));
  1357.  
  1358. const drawCommand = [1, 6, points[0][0], points[0][1], points[1][0], points[1][1]];
  1359. window.game._socket.emit(10, window.game._codigo, drawCommand);
  1360.  
  1361. ctx.beginPath();
  1362. ctx.moveTo(points[0][0], points[0][1]);
  1363. ctx.lineTo(points[1][0], points[1][1]);
  1364. ctx.stroke();
  1365. await new Promise(resolve => setTimeout(resolve, drawSpeedValue));
  1366. }
  1367.  
  1368. alert('Drawing completed! ✧');
  1369. sendDraw.disabled = false;
  1370. };
  1371. img.onerror = function() {
  1372. alert('Failed to load image! ✧');
  1373. sendDraw.disabled = false;
  1374. };
  1375. img.src = imageSrc;
  1376. }
  1377.  
  1378. // Color distance helper
  1379. function colorDistance(color1, color2) {
  1380. return Math.sqrt(
  1381. Math.pow(color1[0] - color2[0], 2) +
  1382. Math.pow(color1[1] - color2[1], 2) +
  1383. Math.pow(color1[2] - color2[2], 2)
  1384. );
  1385. }
  1386. })();