Upload an image and automatically build it on the r/place canvas at specified coordinates
Size
25.0 KB
Version
1.1.1
Created
Dec 14, 2025
Updated
2 days ago
1// ==UserScript==
2// @name r/place Image Builder
3// @description Upload an image and automatically build it on the r/place canvas at specified coordinates
4// @version 1.1.1
5// @match https://*.rplace.live/*
6// @icon https://rplace.live/assets/favicon-C9XfoVL2.png
7// @grant GM.getValue
8// @grant GM.setValue
9// ==/UserScript==
10(function() {
11 'use strict';
12
13 console.log('r/place Image Builder extension loaded');
14
15 // Color palette for r/place (32 colors based on standard r/place palette)
16 const PALETTE = [
17 '#6D001A', '#BE0039', '#FF4500', '#FFA800', '#FFD635',
18 '#FFF8B8', '#00A368', '#00CC78', '#7EED56', '#00756F',
19 '#009EAA', '#00CCC0', '#2450A4', '#3690EA', '#51E9F4',
20 '#493AC1', '#6A5CFF', '#94B3FF', '#811E9F', '#B44AC0',
21 '#E4ABFF', '#DE107F', '#FF3881', '#FF99AA', '#6D482F',
22 '#9C6926', '#FFB470', '#000000', '#515252', '#898D90',
23 '#D4D7D9', '#FFFFFF'
24 ];
25
26 // Convert hex color to RGB
27 function hexToRgb(hex) {
28 const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
29 return result ? {
30 r: parseInt(result[1], 16),
31 g: parseInt(result[2], 16),
32 b: parseInt(result[3], 16)
33 } : null;
34 }
35
36 // Find closest color in palette
37 function findClosestColor(r, g, b) {
38 let minDistance = Infinity;
39 let closestIndex = 0;
40
41 PALETTE.forEach((color, index) => {
42 const rgb = hexToRgb(color);
43 const distance = Math.sqrt(
44 Math.pow(r - rgb.r, 2) +
45 Math.pow(g - rgb.g, 2) +
46 Math.pow(b - rgb.b, 2)
47 );
48 if (distance < minDistance) {
49 minDistance = distance;
50 closestIndex = index;
51 }
52 });
53
54 return closestIndex;
55 }
56
57 // Process image and convert to pixel data
58 async function processImage(file, maxWidth = 100, maxHeight = 100) {
59 return new Promise((resolve, reject) => {
60 const reader = new FileReader();
61 reader.onload = (e) => {
62 const img = new Image();
63 img.onload = () => {
64 // Create canvas to process image
65 const canvas = document.createElement('canvas');
66 const ctx = canvas.getContext('2d');
67
68 // Calculate dimensions maintaining aspect ratio
69 let width = img.width;
70 let height = img.height;
71
72 if (width > maxWidth) {
73 height = (height * maxWidth) / width;
74 width = maxWidth;
75 }
76 if (height > maxHeight) {
77 width = (width * maxHeight) / height;
78 height = maxHeight;
79 }
80
81 canvas.width = Math.floor(width);
82 canvas.height = Math.floor(height);
83
84 // Draw and get pixel data
85 ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
86 const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
87 const pixels = [];
88
89 // Convert to palette colors
90 for (let y = 0; y < canvas.height; y++) {
91 for (let x = 0; x < canvas.width; x++) {
92 const i = (y * canvas.width + x) * 4;
93 const r = imageData.data[i];
94 const g = imageData.data[i + 1];
95 const b = imageData.data[i + 2];
96 const a = imageData.data[i + 3];
97
98 // Skip transparent pixels
99 if (a < 128) continue;
100
101 const colorIndex = findClosestColor(r, g, b);
102 pixels.push({ x, y, colorIndex, color: PALETTE[colorIndex] });
103 }
104 }
105
106 resolve({
107 width: canvas.width,
108 height: canvas.height,
109 pixels: pixels,
110 preview: canvas.toDataURL()
111 });
112 };
113 img.onerror = reject;
114 img.src = e.target.result;
115 };
116 reader.onerror = reject;
117 reader.readAsDataURL(file);
118 });
119 }
120
121 // Create overlay to show where image will be built
122 function createOverlay(imageData, startX, startY) {
123 // Remove existing overlay
124 const existingOverlay = document.getElementById('rplace-builder-overlay');
125 if (existingOverlay) {
126 existingOverlay.remove();
127 }
128
129 const canvas = document.getElementById('canvas');
130 if (!canvas) return;
131
132 const overlay = document.createElement('canvas');
133 overlay.id = 'rplace-builder-overlay';
134 overlay.style.position = 'absolute';
135 overlay.style.pointerEvents = 'none';
136 overlay.style.zIndex = '9999';
137 overlay.style.opacity = '0.6';
138
139 // Match canvas size and position
140 const rect = canvas.getBoundingClientRect();
141 overlay.width = canvas.width;
142 overlay.height = canvas.height;
143 overlay.style.left = rect.left + 'px';
144 overlay.style.top = rect.top + 'px';
145 overlay.style.width = rect.width + 'px';
146 overlay.style.height = rect.height + 'px';
147
148 document.body.appendChild(overlay);
149
150 const ctx = overlay.getContext('2d');
151
152 // Draw the image preview on overlay
153 imageData.pixels.forEach(pixel => {
154 ctx.fillStyle = pixel.color;
155 ctx.fillRect(startX + pixel.x, startY + pixel.y, 1, 1);
156 });
157
158 console.log('Overlay created showing image preview');
159 return overlay;
160 }
161
162 // Place a single pixel by clicking the place button and selecting color
163 async function placePixel(x, y, colorIndex) {
164 return new Promise((resolve) => {
165 try {
166 console.log(`Attempting to place pixel at (${x}, ${y}) with color index ${colorIndex}`);
167
168 // First, click the "place" button to initiate pixel placement
169 const placeButton = document.getElementById('place');
170 if (!placeButton) {
171 console.error('Place button not found');
172 resolve(false);
173 return;
174 }
175
176 // Check if button is disabled (cooldown)
177 if (placeButton.disabled) {
178 console.log('Place button is disabled (cooldown active)');
179 resolve(false);
180 return;
181 }
182
183 console.log('Clicking place button...');
184 placeButton.click();
185
186 // Wait for palette to appear
187 setTimeout(() => {
188 const palette = document.getElementById('palette');
189 const paletteStyle = window.getComputedStyle(palette);
190
191 if (!palette || paletteStyle.display === 'none' || paletteStyle.opacity === '0') {
192 console.log('Palette not visible after clicking place button');
193 resolve(false);
194 return;
195 }
196
197 console.log('Palette is visible, selecting color...');
198
199 // Select the color
200 const colorDiv = document.querySelector(`#colours div[data-index="${colorIndex}"]`);
201 if (!colorDiv) {
202 console.error(`Color ${colorIndex} not found in palette`);
203 // Cancel the placement
204 const cancelBtn = document.getElementById('pcancel');
205 if (cancelBtn) cancelBtn.click();
206 resolve(false);
207 return;
208 }
209
210 colorDiv.click();
211 console.log(`Selected color ${colorIndex}`);
212
213 // Wait a bit then click on canvas at the target position
214 setTimeout(() => {
215 const canvas = document.getElementById('canvas');
216 if (!canvas) {
217 console.error('Canvas not found');
218 resolve(false);
219 return;
220 }
221
222 // Click at the specific coordinates
223 const rect = canvas.getBoundingClientRect();
224 const clickX = rect.left + x;
225 const clickY = rect.top + y;
226
227 console.log(`Clicking canvas at screen position (${clickX}, ${clickY}) for canvas coords (${x}, ${y})`);
228
229 const clickEvent = new MouseEvent('click', {
230 bubbles: true,
231 cancelable: true,
232 view: window,
233 clientX: clickX,
234 clientY: clickY
235 });
236 canvas.dispatchEvent(clickEvent);
237
238 // Wait for confirmation button
239 setTimeout(() => {
240 const confirmBtn = document.getElementById('pok');
241 if (!confirmBtn) {
242 console.error('Confirm button not found');
243 resolve(false);
244 return;
245 }
246
247 console.log('Clicking confirm button...');
248 confirmBtn.click();
249 console.log('Pixel placed successfully!');
250 resolve(true);
251 }, 500);
252 }, 300);
253 }, 500);
254 } catch (error) {
255 console.error('Error placing pixel:', error);
256 resolve(false);
257 }
258 });
259 }
260
261 // Main building state
262 let buildState = {
263 isBuilding: false,
264 isPaused: false,
265 currentPixelIndex: 0,
266 imageData: null,
267 startX: 0,
268 startY: 0,
269 totalPixels: 0,
270 placedPixels: 0,
271 overlay: null
272 };
273
274 // Update overlay to show current progress
275 function updateOverlay() {
276 if (!buildState.overlay || !buildState.imageData) return;
277
278 const ctx = buildState.overlay.getContext('2d');
279 ctx.clearRect(0, 0, buildState.overlay.width, buildState.overlay.height);
280
281 // Draw all pixels
282 buildState.imageData.pixels.forEach((pixel, index) => {
283 if (index < buildState.currentPixelIndex) {
284 // Already placed - show with green border
285 ctx.fillStyle = pixel.color;
286 ctx.fillRect(buildState.startX + pixel.x, buildState.startY + pixel.y, 1, 1);
287 ctx.strokeStyle = '#00ff00';
288 ctx.strokeRect(buildState.startX + pixel.x, buildState.startY + pixel.y, 1, 1);
289 } else if (index === buildState.currentPixelIndex) {
290 // Current pixel - highlight with yellow
291 ctx.fillStyle = '#ffff00';
292 ctx.fillRect(buildState.startX + pixel.x, buildState.startY + pixel.y, 1, 1);
293 } else {
294 // Not yet placed - show semi-transparent
295 ctx.fillStyle = pixel.color;
296 ctx.globalAlpha = 0.3;
297 ctx.fillRect(buildState.startX + pixel.x, buildState.startY + pixel.y, 1, 1);
298 ctx.globalAlpha = 1.0;
299 }
300 });
301 }
302
303 // Build image on canvas
304 async function buildImage() {
305 if (!buildState.imageData || buildState.isPaused) return;
306
307 buildState.isBuilding = true;
308 updateProgress();
309
310 while (buildState.currentPixelIndex < buildState.imageData.pixels.length && buildState.isBuilding && !buildState.isPaused) {
311 const pixel = buildState.imageData.pixels[buildState.currentPixelIndex];
312 const targetX = buildState.startX + pixel.x;
313 const targetY = buildState.startY + pixel.y;
314
315 console.log(`Building pixel ${buildState.currentPixelIndex + 1}/${buildState.totalPixels} at (${targetX}, ${targetY})`);
316
317 updateOverlay();
318
319 const success = await placePixel(targetX, targetY, pixel.colorIndex);
320
321 if (success) {
322 buildState.placedPixels++;
323 buildState.currentPixelIndex++;
324 updateProgress();
325 updateOverlay();
326
327 // Save progress
328 await GM.setValue('buildProgress', {
329 currentPixelIndex: buildState.currentPixelIndex,
330 placedPixels: buildState.placedPixels
331 });
332
333 // Wait between placements (cooldown - adjust based on site's cooldown)
334 console.log('Waiting 5 seconds before next pixel...');
335 await new Promise(resolve => setTimeout(resolve, 5000));
336 } else {
337 console.log('Failed to place pixel, retrying in 10 seconds...');
338 await new Promise(resolve => setTimeout(resolve, 10000));
339 }
340 }
341
342 if (buildState.currentPixelIndex >= buildState.totalPixels) {
343 console.log('Image building complete!');
344 buildState.isBuilding = false;
345 alert('Image building complete!');
346 updateProgress();
347 }
348 }
349
350 // Update progress display
351 function updateProgress() {
352 const progressText = document.getElementById('rplace-builder-progress');
353 const progressBar = document.getElementById('rplace-builder-progress-bar');
354
355 if (progressText && buildState.imageData) {
356 const percentage = Math.floor((buildState.placedPixels / buildState.totalPixels) * 100);
357 progressText.textContent = `Progress: ${buildState.placedPixels}/${buildState.totalPixels} pixels (${percentage}%)`;
358
359 if (progressBar) {
360 progressBar.style.width = percentage + '%';
361 }
362 }
363
364 // Update button states
365 const startBtn = document.getElementById('rplace-builder-start');
366 const pauseBtn = document.getElementById('rplace-builder-pause');
367 const stopBtn = document.getElementById('rplace-builder-stop');
368
369 if (startBtn) startBtn.disabled = buildState.isBuilding && !buildState.isPaused;
370 if (pauseBtn) {
371 pauseBtn.disabled = !buildState.isBuilding;
372 pauseBtn.textContent = buildState.isPaused ? 'Resume' : 'Pause';
373 }
374 if (stopBtn) stopBtn.disabled = !buildState.isBuilding && !buildState.isPaused;
375 }
376
377 // Create UI panel
378 function createUI() {
379 const panel = document.createElement('div');
380 panel.id = 'rplace-builder-panel';
381 panel.innerHTML = `
382 <div style="position: fixed; top: 10px; right: 10px; background: #1a1a1b; color: #d7dadc; padding: 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.5); z-index: 10000; width: 320px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;">
383 <h3 style="margin: 0 0 15px 0; font-size: 18px; font-weight: 600; color: #ffffff;">r/place Image Builder</h3>
384
385 <div style="margin-bottom: 15px;">
386 <label style="display: block; margin-bottom: 5px; font-size: 14px; font-weight: 500;">Upload Image:</label>
387 <input type="file" id="rplace-builder-file" accept="image/*" style="width: 100%; padding: 8px; background: #272729; border: 1px solid #343536; border-radius: 4px; color: #d7dadc; font-size: 13px;">
388 </div>
389
390 <div style="margin-bottom: 15px; display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
391 <div>
392 <label style="display: block; margin-bottom: 5px; font-size: 14px; font-weight: 500;">Start X:</label>
393 <input type="number" id="rplace-builder-x" value="100" min="0" style="width: 100%; padding: 8px; background: #272729; border: 1px solid #343536; border-radius: 4px; color: #d7dadc; font-size: 13px;">
394 </div>
395 <div>
396 <label style="display: block; margin-bottom: 5px; font-size: 14px; font-weight: 500;">Start Y:</label>
397 <input type="number" id="rplace-builder-y" value="100" min="0" style="width: 100%; padding: 8px; background: #272729; border: 1px solid #343536; border-radius: 4px; color: #d7dadc; font-size: 13px;">
398 </div>
399 </div>
400
401 <div style="margin-bottom: 15px; display: grid; grid-template-columns: 1fr 1fr; gap: 10px;">
402 <div>
403 <label style="display: block; margin-bottom: 5px; font-size: 14px; font-weight: 500;">Max Width:</label>
404 <input type="number" id="rplace-builder-width" value="50" min="1" max="200" style="width: 100%; padding: 8px; background: #272729; border: 1px solid #343536; border-radius: 4px; color: #d7dadc; font-size: 13px;">
405 </div>
406 <div>
407 <label style="display: block; margin-bottom: 5px; font-size: 14px; font-weight: 500;">Max Height:</label>
408 <input type="number" id="rplace-builder-height" value="50" min="1" max="200" style="width: 100%; padding: 8px; background: #272729; border: 1px solid #343536; border-radius: 4px; color: #d7dadc; font-size: 13px;">
409 </div>
410 </div>
411
412 <button id="rplace-builder-process" style="width: 100%; padding: 10px; background: #0079d3; color: white; border: none; border-radius: 4px; font-size: 14px; font-weight: 600; cursor: pointer; margin-bottom: 10px;">
413 Process Image
414 </button>
415
416 <div id="rplace-builder-preview" style="margin-bottom: 15px; display: none;">
417 <label style="display: block; margin-bottom: 5px; font-size: 14px; font-weight: 500;">Preview:</label>
418 <img id="rplace-builder-preview-img" style="width: 100%; border: 1px solid #343536; border-radius: 4px; background: #272729;">
419 <p id="rplace-builder-info" style="margin: 5px 0; font-size: 12px; color: #818384;"></p>
420 </div>
421
422 <button id="rplace-builder-show-overlay" style="width: 100%; padding: 8px; background: #7c3aed; color: white; border: none; border-radius: 4px; font-size: 13px; font-weight: 600; cursor: pointer; margin-bottom: 10px; display: none;">
423 Show Preview on Canvas
424 </button>
425
426 <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin-bottom: 10px;">
427 <button id="rplace-builder-start" style="padding: 10px; background: #46d160; color: white; border: none; border-radius: 4px; font-size: 13px; font-weight: 600; cursor: pointer;">
428 Start
429 </button>
430 <button id="rplace-builder-pause" style="padding: 10px; background: #ffa500; color: white; border: none; border-radius: 4px; font-size: 13px; font-weight: 600; cursor: pointer;" disabled>
431 Pause
432 </button>
433 <button id="rplace-builder-stop" style="padding: 10px; background: #ea0027; color: white; border: none; border-radius: 4px; font-size: 13px; font-weight: 600; cursor: pointer;" disabled>
434 Stop
435 </button>
436 </div>
437
438 <div style="margin-bottom: 10px;">
439 <div style="background: #272729; border-radius: 4px; height: 20px; overflow: hidden;">
440 <div id="rplace-builder-progress-bar" style="background: #46d160; height: 100%; width: 0%; transition: width 0.3s;"></div>
441 </div>
442 <p id="rplace-builder-progress" style="margin: 5px 0 0 0; font-size: 12px; color: #818384; text-align: center;">Ready to build</p>
443 </div>
444
445 <button id="rplace-builder-close" style="position: absolute; top: 10px; right: 10px; background: transparent; color: #818384; border: none; font-size: 20px; cursor: pointer; padding: 0; width: 24px; height: 24px; line-height: 24px;">×</button>
446 </div>
447 `;
448
449 document.body.appendChild(panel);
450
451 // Event listeners
452 document.getElementById('rplace-builder-close').addEventListener('click', () => {
453 panel.style.display = 'none';
454 });
455
456 document.getElementById('rplace-builder-process').addEventListener('click', async () => {
457 const fileInput = document.getElementById('rplace-builder-file');
458 const maxWidth = parseInt(document.getElementById('rplace-builder-width').value);
459 const maxHeight = parseInt(document.getElementById('rplace-builder-height').value);
460
461 if (!fileInput.files[0]) {
462 alert('Please select an image first!');
463 return;
464 }
465
466 try {
467 console.log('Processing image...');
468 const imageData = await processImage(fileInput.files[0], maxWidth, maxHeight);
469
470 buildState.imageData = imageData;
471 buildState.totalPixels = imageData.pixels.length;
472 buildState.currentPixelIndex = 0;
473 buildState.placedPixels = 0;
474
475 // Show preview
476 const previewDiv = document.getElementById('rplace-builder-preview');
477 const previewImg = document.getElementById('rplace-builder-preview-img');
478 const infoText = document.getElementById('rplace-builder-info');
479 const showOverlayBtn = document.getElementById('rplace-builder-show-overlay');
480
481 previewImg.src = imageData.preview;
482 infoText.textContent = `Size: ${imageData.width}x${imageData.height} (${imageData.pixels.length} pixels)`;
483 previewDiv.style.display = 'block';
484 showOverlayBtn.style.display = 'block';
485
486 console.log('Image processed successfully:', imageData);
487 alert('Image processed! Click "Show Preview on Canvas" to see where it will be built, then click Start.');
488 } catch (error) {
489 console.error('Error processing image:', error);
490 alert('Error processing image. Please try again.');
491 }
492 });
493
494 document.getElementById('rplace-builder-show-overlay').addEventListener('click', () => {
495 if (!buildState.imageData) {
496 alert('Please process an image first!');
497 return;
498 }
499
500 const startX = parseInt(document.getElementById('rplace-builder-x').value);
501 const startY = parseInt(document.getElementById('rplace-builder-y').value);
502
503 buildState.startX = startX;
504 buildState.startY = startY;
505 buildState.overlay = createOverlay(buildState.imageData, startX, startY);
506 });
507
508 document.getElementById('rplace-builder-start').addEventListener('click', async () => {
509 if (!buildState.imageData) {
510 alert('Please process an image first!');
511 return;
512 }
513
514 buildState.startX = parseInt(document.getElementById('rplace-builder-x').value);
515 buildState.startY = parseInt(document.getElementById('rplace-builder-y').value);
516 buildState.isPaused = false;
517
518 // Create overlay if not exists
519 if (!buildState.overlay) {
520 buildState.overlay = createOverlay(buildState.imageData, buildState.startX, buildState.startY);
521 }
522
523 console.log(`Starting build at (${buildState.startX}, ${buildState.startY})`);
524 buildImage();
525 });
526
527 document.getElementById('rplace-builder-pause').addEventListener('click', () => {
528 buildState.isPaused = !buildState.isPaused;
529 updateProgress();
530
531 if (!buildState.isPaused) {
532 buildImage();
533 }
534 });
535
536 document.getElementById('rplace-builder-stop').addEventListener('click', async () => {
537 buildState.isBuilding = false;
538 buildState.isPaused = false;
539 buildState.currentPixelIndex = 0;
540 buildState.placedPixels = 0;
541
542 // Remove overlay
543 if (buildState.overlay) {
544 buildState.overlay.remove();
545 buildState.overlay = null;
546 }
547
548 await GM.setValue('buildProgress', null);
549 updateProgress();
550 console.log('Build stopped');
551 });
552
553 console.log('UI panel created');
554 }
555
556 // Initialize
557 function init() {
558 // Wait for page to load
559 if (document.readyState === 'loading') {
560 document.addEventListener('DOMContentLoaded', createUI);
561 } else {
562 createUI();
563 }
564 }
565
566 init();
567})();