PageSourceSearch

https://left0vers.github.io/drawbox.js

js left0vers.github.io collected 2026-10-03 09:30:03 UTC 15,096 bytes, 529 lines download raw bytes

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.