1/* 2 3 FILL IN THESE VARIABLES BASED ON THE GUIDE AT https://drawbox.nekoweb.org 4 5 IF YOU HAVE ANY QUESTION, SUGGESTIONS, OR NEED HELP, PLEASE EMAIL ME AT [email protected] OR @MONKEYBATION on DISCORD 6 7 /`·.¸ 8 /¸...¸`:· 9 ¸.·´ ¸ `·.¸.·´) 10 : © ):´; ¸ { 11 `·.¸ `· ¸.·´\`·¸) 12 `\\´´\¸.·´ 13 14*/ 15const GOOGLE_FORM_ID = "1FAIpQLSc7gk7Ot8_wF9SchGaDx5xf_Pc5v-XU0d4s3hNw7BBeK8ZSuw"; 16const IMAGE_ENTRY_ID = "entry.1369949347"; 17const NAME_ENTRY_ID = "entry.15412514"; 18const WEBSITE_ENTRY_ID = "entry.306295053"; // Replace with new name field entry ID 19const GOOGLE_SHEET_ID = "11w2LvZuljHliPFVvejbYIbyd_mXpcWefKuzUa8cVbnI"; 20const DISPLAY_IMAGES = true; 21 22/* 23 24 DONT EDIT BELOW THIS POINT IF YOU DONT KNOW WHAT YOU ARE DOING. 25 26*/ 27 28const CLIENT_ID = "b4fb95e0edc434c"; 29const GOOGLE_SHEET_URL = "https://docs.google.com/spreadsheets/d/" + GOOGLE_SHEET_ID + "/export?format=csv"; 30const GOOGLE_FORM_URL = "https://docs.google.com/forms/d/e/" + GOOGLE_FORM_ID + "/formResponse"; 31 32const HALFTONE_PATTERN = [ 33 [1,1,0,0,0,0], // xxoooo 34 [1,1,0,0,0,0], // xxoooo 35 [0,0,0,0,0,0], // oooooo 36 [0,0,0,0,0,0], // oooooo 37 [0,0,0,0,0,0], // oooooo 38 [0,0,0,1,1,0], // oooxxo 39 [0,0,0,1,1,0], // oooxxo 40 [0,0,0,0,0,0], // oooooo 41 [0,0,0,0,0,0], // oooooo 42 [0,0,0,0,0,0] // oooooo 43]; 44 45 46const HALFTONE_COLOR = "HALFTONE_PATTERN"; 47 48let canvas = document.getElementById("drawboxcanvas"); 49let context = canvas.getContext("2d"); 50context.fillStyle = "white"; 51context.fillRect(0, 0, canvas.width, canvas.height); 52 53let restore_array = []; 54let start_index = -1; 55let stroke_color = "black"; 56let stroke_width = "8"; /*brush starting size - should match "stroke_width = this.value" in html*/ 57let is_drawing = false; 58 59let lastX = 0; 60let lastY = 0; 61 62function change_color(element) { 63 stroke_color = element.style.background; 64 if (element.getAttribute('data-halftone') === 'true') { 65 stroke_color = HALFTONE_COLOR; 66 } 67} 68 69 70function updateBrushSize(value, source) { 71 stroke_width = value; 72 73 if (source === 'slider') { 74 document.getElementById('brush-input').value = value; 75 } else if (source === 'input') { 76 document.getElementById('brush-slider').value = value; 77 } 78} 79 80 81 82document.addEventListener("DOMContentLoaded", function() { 83 document.getElementById('brush-slider').addEventListener('input', function() { 84 updateBrushSize(this.value, 'slider'); 85 }); 86 87 document.getElementById('brush-input').addEventListener('input', function() { 88 updateBrushSize(this.value, 'input'); 89 }); 90 91 setDrawMode(); 92}); 93 94function start(event) { 95 is_drawing = true; 96 const x = getX(event); 97 const y = getY(event); 98 99 lastX = x; 100 lastY = y; 101 102 context.beginPath(); 103 context.moveTo(x, y); 104 event.preventDefault(); 105} 106 107 108 109 110function drawHalftonePath(x1, y1, x2, y2, size) { 111 const distance = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); 112 const step = Math.max(1, size / 10); 113 const steps = Math.ceil(distance / step); 114 115 context.fillStyle = "#000000"; 116 117 118 for (let i = 0; i <= steps; i++) { 119 const t = i / steps; 120 const centerX = x1 + (x2 - x1) * t; 121 const centerY = y1 + (y2 - y1) * t; 122 123 124 const radius = size / 2; 125 const startX = Math.floor(centerX - radius); 126 const endX = Math.ceil(centerX + radius); 127 const startY = Math.floor(centerY - radius); 128 const endY = Math.ceil(centerY + radius); 129 130 for (let py = startY; py <= endY; py++) { 131 for (let px = startX; px <= endX; px++) { 132 const dx = px - centerX; 133 const dy = py - centerY; 134 const distanceFromCenter = Math.sqrt(dx * dx + dy * dy); 135 136 if (distanceFromCenter <= radius && px >= 0 && py >= 0 && px < canvas.width && py < canvas.height) { 137 const patternX = ((px % 6) + 6) % 6; // replace these 3 with thewidth (6x10 would be x6, y10) 138 const patternY = ((py % 10) + 10) % 10;
138 // height 139 140 if (HALFTONE_PATTERN[patternY][patternX] === 1) { 141 context.fillRect(px, py, 1, 1); 142 } 143 } 144 } 145 } 146 } 147} 148 149 150 151 152 153function draw(event) { 154 if (!is_drawing) return; 155 156 const currentX = getX(event); 157 const currentY = getY(event); 158 159 if (is_erasing) { 160 context.globalCompositeOperation = "destination-out"; 161 context.strokeStyle = "rgba(0,0,0,1)"; 162 } else { 163 context.globalCompositeOperation = "source-over"; 164 context.strokeStyle = stroke_color; 165 } 166 167 context.lineWidth = stroke_width; 168 169 drawSquarePath(lastX, lastY, currentX, currentY, stroke_width); 170 171 172 lastX = currentX; 173 lastY = currentY; 174 175 event.preventDefault(); 176} 177 178function drawSquarePath(x1, y1, x2, y2, size) { 179 if (stroke_color === HALFTONE_COLOR) { 180 drawHalftonePath(x1, y1, x2, y2, size); 181 return; 182 } 183 184 185 const distance = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); 186 const maxGap = size * 0.6; 187 if (distance > maxGap) { 188 const steps = Math.ceil(distance / maxGap); 189 190 for (let i = 1; i < steps; i++) { 191 const t = i / steps; 192 const x = x1 + (x2 - x1) * t; 193 const y = y1 + (y2 - y1) * t; 194 195 if (is_erasing) { 196 context.clearRect(x - size/2, y - size/2, size, size); 197 } else { 198 context.fillStyle = stroke_color; 199 context.fillRect(x - size/2, y - size/2, size, size); 200 } 201 } 202 } 203 204 if (is_erasing) { 205 context.clearRect(x2 - size/2, y2 - size/2, size, size); 206 } else { 207 context.fillStyle = stroke_color; 208 context.fillRect(x2 - size/2, y2 - size/2, size, size); 209 } 210} 211 212function stop(event) { 213 if (!is_drawing) return; 214 context.stroke(); 215 context.closePath(); 216 is_drawing = false; 217 218 219 context.globalCompositeOperation = "source-over"; 220 221 restore_array.push(context.getImageData(0, 0, canvas.width, canvas.height)); 222 start_index++; 223 event.preventDefault(); 224} 225 226function getX(event) { 227 const rect = canvas.getBoundingClientRect(); 228 const scaleX = canvas.width / rect.width; 229 230 if (event.clientX !== undefined) { 231 return (event.clientX - rect.left) * scaleX; 232 } else { 233 return (event.targetTouches[0].clientX - rect.left) * scaleX; 234 } 235} 236 237function getY(event) { 238 const rect = canvas.getBoundingClientRect(); 239 const scaleY = canvas.height / rect.height; 240 241 if (event.clientY !== undefined) { 242 // Mouse event 243 return (event.clientY - rect.top) * scaleY; 244 } else { 245 // Touch event 246 return (event.targetTouches[0].clientY - rect.top) * scaleY; 247 } 248} 249 250canvas.addEventListener("touchstart", function(e) { 251 e.preventDefault(); 252 start(e); 253}, { passive: false }); 254 255canvas.addEventListener("touchmove", function(e) { 256 e.preventDefault(); 257 draw(e); 258}, { passive: false }); 259 260canvas.addEventListener("touchend", function(e) { 261 e.preventDefault(); 262 stop(e); 263}, { passive: false }); 264 265 266canvas.addEventListener("mousedown", start, false); 267canvas.addEventListener("mousemove", draw, false); 268canvas.addEventListener("mouseup", stop, false); 269canvas.addEventListener("mouseout", stop, false); 270 271function Restore() { 272 if (start_index <= 0) { 273 Clear(); 274 } else { 275 start_index--; 276 restore_array.pop(); 277 context.putImageData(restore_array[start_index], 0, 0); 278 } 279} 280 281function Clear() { 282 context.fillStyle = "white"; 283 context.clearRect(0, 0, canvas.width, canvas.height); 284 context.fillRect(0, 0, canvas.width, canvas.height); 285 restore_array = []; 286 start_index = -1; 287} 288 289context.drawImage = function() { 290 console.warn("noo >:("); 291}; 292 293// Add Ctrl+Z keyboard shortcut for undo 294document.addEventListener("keydown", function(event) { 295 // Check if Ctrl+Z is pressed (or Cmd+Z on Mac) 296 if ((event.ctrlKey || event.metaKey) && event.key === 'z' && !event.shiftKey) { 297 event.preventDefault(); // Prevent browser's default undo 298 Restore(); // Call the existing undo function 299 } 300}); 301 302document.getElementById("submit").addEventListener("click", async function () { 303 const submitButton = document.getElementById("submit"); 304 const statusText = document.getElementById("status"); 305 306 307 const artistName = document.getElementById("artist-name").value.trim(); 308 const artistWebsite = document.getElementById("artist-website").value.trim(); 309 const spamCheck = document.getElementById("spam-check").value.trim(); 310 311 312 if (!artistName) { 313 alert("Please enter your name!"); 314 return; 315 } 316
317 if (spamCheck.toLowerCase() !== "guestbook") { 318 alert("Please enter 'guestbook' in the spam blocker field!"); 319 return; 320 } 321 322 submitButton.disabled = true; 323 statusText.textContent = "Uploading..."; 324 325 const imageData = canvas.toDataURL("image/png"); 326 const blob = await (await fetch(imageData)).blob(); 327 const formData = new FormData(); 328 formData.append("image", blob, "drawing.png"); 329 330 try { 331 const response = await fetch("https://api.imgur.com/3/image", { 332 method: "POST", 333 headers: { Authorization: `Client-ID ${CLIENT_ID}` }, 334 body: formData, 335 }); 336 337 const data = await response.json(); 338 if (!data.success) throw new Error("Imgur upload failed"); 339 340 const imageUrl = data.data.link; 341 console.log("Uploaded image URL:", imageUrl); 342 343 344 let submissionData = imageUrl; 345 if (artistName) { 346 submissionData += " | " + artistName; 347 } 348 if (artistWebsite) { 349 submissionData += " | " + artistWebsite; 350 } 351 352 const googleFormData = new FormData(); 353 googleFormData.append(IMAGE_ENTRY_ID, imageUrl); 354 googleFormData.append(NAME_ENTRY_ID, artistName); 355 if (artistWebsite) { 356 googleFormData.append(WEBSITE_ENTRY_ID, artistWebsite); 357 } 358 359 await fetch(GOOGLE_FORM_URL, { 360 method: "POST", 361 body: googleFormData, 362 mode: "no-cors", 363 }); 364 365 statusText.textContent = "Upload successful!"; 366 alert("Image uploaded and submitted successfully â»"); 367 location.reload(); 368 } catch (error) { 369 console.error(error); 370 statusText.textContent = "Error uploading image."; 371 alert("Error uploading image or submitting to Google Form."); 372 } finally { 373 submitButton.disabled = false; 374 } 375}); 376 377async function fetchImages() { 378 if (!DISPLAY_IMAGES) { 379 console.log("Image display is disabled."); 380 return; 381 } 382 383 try { 384 console.log("Fetching from URL:", GOOGLE_SHEET_URL); 385 const response = await fetch(GOOGLE_SHEET_URL); 386 console.log("Response status:", response.status); 387 388 const csvText = await response.text(); 389 console.log("CSV data received:", csvText); 390 391 const rows = csvText.split("\n").slice(1); 392 console.log("Rows after splitting:", rows); 393 394 const gallery = document.getElementById("gallery"); 395 gallery.innerHTML = ""; 396 397 if (rows.length === 0 || (rows.length === 1 && rows[0].trim() === "")) { 398 console.log("No data rows found"); 399 gallery.innerHTML = "No images submitted yet."; 400 return; 401 } 402 403 404 405 // Limit to 12 most recent images 406 const approvedRows = rows.filter(row => { 407 const columns = row.split(","); 408 // Check if the "Approved" column (index 4) has any value (x, yes, approved, etc.) 409 const approvedStatus = columns[5] ? columns[5].trim().replace(/"/g, "") : ""; 410 return approvedStatus !== "" && approvedStatus.toLowerCase() !== "no"; 411 }); 412 413 const recentApprovedRows = approvedRows.reverse().slice(0, 12); 414 415 recentApprovedRows.forEach((row, index) => { 416 console.log(`Processing approved row ${index}:`, row); 417 418 const columns = row.split(","); 419 console.log("Columns:", columns); 420 421 if (columns.length < 2) { 422 console.log("Skipping row - not enough columns"); 423 return; 424 } 425 426 const rawTimestamp = columns[0].trim(); 427 // Timestap formatting "2025.08.26" 428 const date = new Date(rawTimestamp); 429 const year = date.getFullYear();
430 const month = String(date.getMonth() + 1).padStart(2, '0'); 431 const day = String(date.getDate()).padStart(2, '0'); 432 const timestamp = `${year}.${month}.${day}`; 433 434 const imgUrl = columns[1].trim().replace(/"/g, ""); 435 const artistName = columns[2] ? columns[2].trim().replace(/"/g, "") : "Anonymous"; 436 const artistWebsite = columns[3] ? columns[3].trim().replace(/"/g, "") : ""; 437 438 if (imgUrl.startsWith("http")) { 439 const div = document.createElement("div"); 440 div.classList.add("image-container"); 441 442 443 let artistLine = artistName; 444 if (artistWebsite && artistWebsite.startsWith("http")) { 445 artistLine += ` // <a href="${artistWebsite}" target="_blank">website</a>`; 446 } 447 448 div.innerHTML = ` 449 <img src="${imgUrl}" alt="drawing by ${artistName}"> 450 <p class="artist-line">${artistLine}</p> 451 <p class="timestamp">${timestamp}</p> 452 `; 453 gallery.appendChild(div); 454 console.log("Sent!"); 455 } 456 }); 457 458 459 460 461 const totalApproved = approvedRows.length; 462 463 const archiveLink = document.querySelector('a[href="/archive"]'); 464 if (archiveLink && totalApproved > 0) { 465 archiveLink.textContent = `View Archive (${totalApproved})`; 466 } 467 468 if (totalApproved > 12) { 469 const moreDiv = document.createElement("div"); 470 moreDiv.style.gridColumn = "1 / -1"; 471 moreDiv.style.textAlign = "center"; 472 moreDiv.style.marginTop = "40px"; 473 moreDiv.innerHTML = `<p><a href="/archive">View all ${totalApproved} drawings in the archive â</a></p>`; 474 gallery.appendChild(moreDiv); 475 } 476 477 478 } catch (error) { 479 console.error("Error fetching images:", error); 480 document.getElementById("gallery").textContent = "Failed to load images."; 481 } 482} 483 484 485fetchImages(); 486 487 488 489 490let is_erasing = false; 491 492function setDrawMode() { 493 is_erasing = false; 494 495 496 497 document.getElementById("draw-btn").classList.add("active-tool"); 498 document.getElementById("erase-btn").classList.remove("active-tool"); 499 500 501 canvas.style.cursor = "crosshair"; 502} 503 504function setEraseMode() { 505 is_erasing = true; 506 507 508 document.getElementById("draw-btn").classList.remove("active-tool"); 509 document.getElementById("erase-btn").classList.add("active-tool"); 510 511 512 canvas.style.cursor = "grab"; 513} 514 515 516document.addEventListener("DOMContentLoaded", function() { 517 setDrawMode(); 518}); 519 520 521 522 523 524 525 526 527 528 529
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.