r/place Image Builder

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})();
r/place Image Builder | Robomonkey