Greasy Fork

Greasy Fork is available in English.

OWOP AutoPixel – [draggable + FR/EN]

Pose une image locale sur OWOP à partir (X,Y) avec blocs N×N, débit en px/s, réessais, passe de vérif. Queue: max 5000, refill sous 500. Panneau à droite, déplaçable, FR/EN.

当前为 2025-08-20 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         OWOP AutoPixel – [draggable + FR/EN]
// @namespace    owop-autopixel
// @version      2.0
// @description  Pose une image locale sur OWOP à partir (X,Y) avec blocs N×N, débit en px/s, réessais, passe de vérif. Queue: max 5000, refill sous 500. Panneau à droite, déplaçable, FR/EN.
// @match        *://ourworldofpixels.com/*
// @match        *://*.ourworldofpixels.com/*
// @run-at       document-idle
// @license      MIT
// @grant        none
// @noframes
// ==/UserScript==

(function () {
  'use strict';
  if (window.top !== window.self) return;

  // ---------- UI ----------
  const css = `
  #apxPanel{
    position:fixed;top:12px;right:12px;left:auto;
    z-index:2147483647;background:#111a;border:1px solid #333;border-radius:12px;
    padding:10px 12px;color:#eee;font:13px/1.35 system-ui,Segoe UI,Roboto,Arial;
    width:330px;box-shadow:0 8px 24px rgba(0,0,0,.35)
  }
  #apxTitle{
    margin:0 0 8px;font-size:14px;font-weight:700;
    cursor:move; user-select:none; -webkit-user-select:none;
    padding:4px 2px; border-bottom:1px solid #2a2a2a;
  }
  #apxPanel .row{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:8px}
  #apxPanel .wide{grid-column:1 / -1}
  #apxPanel input[type="number"],#apxPanel input[type="file"],#apxPanel button,#apxPanel input[type="range"]{
    width:100%;box-sizing:border-box;border-radius:8px;border:1px solid #444;background:#17181a;color:#eee;padding:6px 8px
  }
  #apxPanel label{display:flex;flex-direction:column;gap:6px}
  #apxPanel .small{color:#bbb;font-size:12px}
  #apxStatus{margin-top:6px;font-size:12px;color:#bbb}
  #apxLed{display:inline-block;width:8px;height:8px;border-radius:50%;margin-right:6px;background:#d33;vertical-align:middle}
  #apxLed.on{background:#2ecc71}
  `;
  const style = document.createElement('style'); style.textContent = css; document.head.append(style);

  const panel = document.createElement('div');
  panel.id = 'apxPanel';
  panel.innerHTML = `
    <h3 id="apxTitle">Image to Pixel By Agents_K</h3>
    <div class="row">
      <label class="wide">Image locale / Local image
        <input type="file" id="apxFile" accept="image/*">
      </label>
      <label>X (monde) / X (world)
        <input type="number" id="apxX" step="1" value="0">
      </label>
      <label>Y (monde) / Y (world)
        <input type="number" id="apxY" step="1" value="0">
      </label>
      <button id="apxSetMouse" class="wide" title="Prendre X/Y sous la souris / Take X/Y from mouse">Prendre X/Y depuis la souris / Take X/Y from mouse</button>
    </div>
    <div class="row">
      <label class="wide">Échelle de l'image (%) / Image scale (%)
        <input type="range" id="apxScale" min="10" max="400" step="10" value="100">
        <span class="small" id="apxScaleInfo">Échelle: 100% • Taille posée: — / Scale: 100% • Placed size: —</span>
      </label>
      <label class="wide">Taille du bloc (px) / Block size (px)
        <input type="range" id="apxBlock" min="1" max="15" step="2" value="3">
        <span class="small" id="apxBlockInfo">Bloc: 3×3 (9 px) / Block: 3×3 (9 px)</span>
      </label>
      <label class="wide">Débit (pixels / seconde) / Rate (pixels per second)
        <input type="range" id="apxRate" min="1" max="60" step="1" value="15">
        <span class="small" id="apxRateInfo">Débit: 15 px/s / Rate: 15 px/s</span>
      </label>
    </div>
    <div class="row">
      <button id="apxStart" style="grid-column:1/2">Démarrer / Start</button>
      <button id="apxStop"  style="grid-column:2/2">Arrêter / Stop</button>
      <button id="apxReset" class="wide">Réinitialiser progression / Reset progress</button>
    </div>
    <div id="apxStatus"><span id="apxLed"></span><span id="apxMsg">En attente d'une image… / Waiting for an image…</span></div>
  `;
  document.body.append(panel);

  // ---------- Draggable panel ----------
  (function makeDraggable(){
    const title = panel.querySelector('#apxTitle');
    let dragging = false, startX = 0, startY = 0, startLeft = 0, startTop = 0;

    try {
      const save = localStorage.getItem('apxPanelPos');
      if (save) {
        const {left, top} = JSON.parse(save);
        panel.style.left = left + 'px';
        panel.style.top  = top  + 'px';
        panel.style.right = 'auto';
      }
    } catch(_) {}

    const onMouseMove = (e) => {
      if (!dragging) return;
      const nx = startLeft + (e.clientX - startX);
      const ny = startTop  + (e.clientY - startY);
      panel.style.left = nx + 'px';
      panel.style.top  = ny + 'px';
      panel.style.right = 'auto';
    };
    const onMouseUp = () => {
      if (!dragging) return;
      dragging = false;
      window.removeEventListener('mousemove', onMouseMove);
      window.removeEventListener('mouseup', onMouseUp);
      const rect = panel.getBoundingClientRect();
      try { localStorage.setItem('apxPanelPos', JSON.stringify({ left: rect.left, top: rect.top })); } catch(_){}
    };

    title.addEventListener('mousedown', (e) => {
      dragging = true;
      const rect = panel.getBoundingClientRect();
      startX = e.clientX; startY = e.clientY;
      startLeft = rect.left; startTop = rect.top;
      window.addEventListener('mousemove', onMouseMove);
      window.addEventListener('mouseup', onMouseUp);
      e.preventDefault();
    });
  })();

  // ---------- Helpers & elements ----------
  const $ = (sel) => panel.querySelector(sel);
  const led = $('#apxLed'), msg = $('#apxMsg');
  const scaleEl = $('#apxScale'), scaleInfo = $('#apxScaleInfo');
  const blockEl = $('#apxBlock'), blockInfo = $('#apxBlockInfo');
  const rateEl  = $('#apxRate'),  rateInfo  = $('#apxRateInfo');

  const setStatus = (on, text) => { led.classList.toggle('on', !!on); msg.textContent = text || ''; };

  // ---------- Image state ----------
  let srcBmp = null, srcW = 0, srcH = 0;
  let scalePct = 100;
  let tgtW = 0, tgtH = 0;
  let imgData = null;

  const imgC = document.createElement('canvas');
  const imgCtx = imgC.getContext('2d', { willReadFrequently: true });

  // ---------- Parameters ----------
  const state = {
    x0: 0, y0: 0,
    block: 3,
    rate: 15,
    running: false,
    pass: 1, // 1 = scan/place, 2 = verify
  };

  // ---------- Queue with watermarks ----------
  // Task = { wx, wy, r, g, b, due, tries }
  const queue = [];
  const Q_MAX_FEED = 5000;        // hard cap
  const Q_LOW_WATERMARK = 500;    // refill threshold
  const BACKOFF_MS  = 600;        // <— déclaré UNE SEULE fois ici
  let tickTimer = null;

  function rgbEq(a,b){ return a && b && a[0]===b[0] && a[1]===b[1] && a[2]===b[2]; }
  function loadBitmap(file){ return file.arrayBuffer().then(buf => createImageBitmap(new Blob([buf]))); }

  function rebuildScaled() {
    if (!srcBmp) return;
    tgtW = Math.max(1, Math.round(srcW * scalePct / 100));
    tgtH = Math.max(1, Math.round(srcH * scalePct / 100));
    imgC.width = tgtW; imgC.height = tgtH;
    imgCtx.imageSmoothingEnabled = false;
    imgCtx.clearRect(0,0,tgtW,tgtH);
    imgCtx.drawImage(srcBmp, 0, 0, srcW, srcH, 0, 0, tgtW, tgtH);
    imgData = imgCtx.getImageData(0,0,tgtW,tgtH);
    scaleInfo.textContent = `Échelle: ${scalePct}% • Taille posée: ${tgtW}×${tgtH} / Scale: ${scalePct}% • Placed size: ${tgtW}×${tgtH}`;
  }

  function getImgRGB(ix, iy) {
    if (!imgData) return null;
    if (ix<0 || iy<0 || ix>=tgtW || iy>=tgtH) return null;
    const p = (iy*tgtW + ix) * 4, d = imgData.data;
    if (d[p+3] < 10) return null;
    return [d[p], d[p+1], d[p+2]];
  }

  // ---------- OWOP access ----------
  function waitForOWOP(maxMs = 30000) {
    return new Promise((resolve, reject) => {
      const t0 = Date.now();
      const iv = setInterval(() => {
        if (window.OWOP && OWOP.world) { clearInterval(iv); resolve(); }
        else if (Date.now() - t0 > maxMs) { clearInterval(iv); reject(new Error('OWOP timeout')); }
      }, 100);
    });
  }
  let placeMode = null;
  function placePixel(wx, wy, rgb) {
    try {
      if (placeMode === 'array') return OWOP.world.setPixel(wx, wy, rgb);
      if (placeMode === 'args')  return OWOP.world.setPixel(wx, wy, rgb[0], rgb[1], rgb[2]);
      try { const r1 = OWOP.world.setPixel(wx, wy, rgb); placeMode = 'array'; return r1; }
      catch(_) { const r2 = OWOP.world.setPixel(wx, wy, rgb[0], rgb[1], rgb[2]); placeMode = 'args'; return r2; }
    } catch(_) { return false; }
  }
  function getBoardPixel(wx, wy) {
    try {
      const p = OWOP.world.getPixel(wx, wy);
      if (!p) return null;
      if (Array.isArray(p)) return p;
      if (typeof p.r === 'number') return [p.r, p.g, p.b];
    } catch(_) {}
    return null;
  }

  // ---------- Incremental feeders with watermarks ----------
  // Pass 1 scanning pointers:
  let scanX = 0, scanY = 0, scannedAll = false;

  function feedQueue() {
    if (!imgData || scannedAll) return;
    while (queue.length < Q_MAX_FEED && !scannedAll) {
      const bx = state.block, by = state.block;
      for (let dy=0; dy<by; dy++) {
        for (let dx=0; dx<bx; dx++) {
          const ix = scanX + dx, iy = scanY + dy;
          if (ix>=tgtW || iy>=tgtH) continue;
          const rgb = getImgRGB(ix, iy);
          if (!rgb) continue;
          const wx = state.x0 + ix, wy = state.y0 + iy;
          queue.push({ wx, wy, r: rgb[0], g: rgb[1], b: rgb[2], due: 0, tries: 0 });
          if (queue.length >= Q_MAX_FEED) break;
        }
        if (queue.length >= Q_MAX_FEED) break;
      }
      scanX += state.block;
      if (scanX >= tgtW) { scanX = 0; scanY += state.block; }
      if (scanY >= tgtH) { scannedAll = true; }
    }
  }

  // Pass 2 verify pointers:
  let vScanX = 0, vScanY = 0, verifyDone = false;
  function beginVerify() { vScanX = 0; vScanY = 0; verifyDone = false; }

  function feedVerifyQueue() {
    if (!imgData || verifyDone) return;
    while (queue.length < Q_MAX_FEED && !verifyDone) {
      const rgb = getImgRGB(vScanX, vScanY);
      if (rgb) {
        const wx = state.x0 + vScanX, wy = state.y0 + vScanY;
        const cur = getBoardPixel(wx, wy);
        if (!rgbEq(cur, rgb)) {
          queue.push({ wx, wy, r: rgb[0], g: rgb[1], b: rgb[2], due: 0, tries: 0 });
        }
      }
      // advance verify pointer
      vScanX++;
      if (vScanX >= tgtW) { vScanX = 0; vScanY++; }
      if (vScanY >= tgtH) { verifyDone = true; }
    }
  }

  // ---------- Placement loop ----------
  function tick() {
    if (!state.running) return;

    const now = performance.now();
    let taskIdx = -1;
    for (let i = 0; i < queue.length; i++) {
      if (queue[i].due <= now) { taskIdx = i; break; }
    }
    if (taskIdx === -1) return;

    const t = queue.splice(taskIdx, 1)[0];
    const desired = [t.r, t.g, t.b];

    const cur = getBoardPixel(t.wx, t.wy);
    if (rgbEq(cur, desired)) { updateStatus(); return; }

    const ok = placePixel(t.wx, t.wy, desired);
    if (!ok) {
      t.tries++;
      t.due = now + BACKOFF_MS * Math.min(6, t.tries);
      queue.push(t);
    }
    updateStatus();
  }

  function updateStatus() {
    const progA = scannedAll ? 100 : Math.floor(((scanY*tgtW + scanX) / (tgtW*tgtH)) * 100);
    const passInfo = state.pass === 1 ? 'Passe 1/2 / Pass 1/2' : 'Passe 2/2 / Pass 2/2';
    setStatus(true, `${passInfo} • queue: ${queue.length} • scan: ${progA}%`);
  }

  function setRateTimer() {
    if (tickTimer) clearInterval(tickTimer);
    const interval = Math.max(10, Math.round(1000 / state.rate));
    tickTimer = setInterval(tick, interval);
  }

  // Watcher: transition between passes & refill under low watermark
  const endWatcher = setInterval(() => {
    if (!state.running) return;

    if (state.pass === 1) {
      if (!scannedAll && queue.length < Q_LOW_WATERMARK) feedQueue();
      if (queue.length === 0 && scannedAll) {
        state.pass = 2; beginVerify();
        setStatus(true, 'Vérification (passe 2/2)… / Verifying (pass 2/2)…');
        if (queue.length < Q_LOW_WATERMARK) feedVerifyQueue();
      }
    } else {
      if (!verifyDone && queue.length < Q_LOW_WATERMARK) feedVerifyQueue();
      if (verifyDone && queue.length === 0) {
        stopAll();
        setStatus(false, 'Terminé ✅ / Done ✅');
      }
    }
  }, 200);

  function startAll() {
    if (state.running) return;
    state.running = true;
    state.pass = 1;
    scanX = scanY = 0; scannedAll = false;
    beginVerify(); verifyDone = true;
    queue.length = 0;

    feedQueue();          // prime jusqu’à 5000
    setRateTimer();       // cadence px/s
    setStatus(true, 'Démarré (passe 1/2)… / Started (pass 1/2)…');
  }

  function stopAll() {
    state.running = false;
    if (tickTimer) clearInterval(tickTimer);
    setStatus(false, 'Arrêté. / Stopped.');
  }

  // ---------- Events UI ----------
  $('#apxFile').addEventListener('change', async (e) => {
    const f = e.target.files && e.target.files[0];
    if (!f) return;
    try {
      setStatus(false, 'Chargement image… / Loading image…');
      const bmp = await loadBitmap(f);
      srcBmp = bmp; srcW = bmp.width; srcH = bmp.height;
      rebuildScaled();
      setStatus(false, `Image ${srcW}×${srcH} chargée. Ajuste échelle/bloc/taux, mets X/Y, puis Démarrer. / Image ${srcW}×${srcH} loaded. Adjust scale/block/rate, set X/Y, then Start.`);
    } catch (err) {
      console.error('[AutoPixel] load error', err);
      setStatus(false, 'Échec chargement image. / Failed to load image.');
      alert('Échec du chargement de l’image (voir console). / Failed to load image (see console).');
    }
  });

  $('#apxSetMouse').addEventListener('click', () => {
    if (window.OWOP && OWOP.mouse) {
      $('#apxX').value = OWOP.mouse.tileX;
      $('#apxY').value = OWOP.mouse.tileY;
    } else {
      alert('OWOP pas prêt. Attends que la carte soit chargée. / OWOP not ready. Wait for the map to load.');
    }
  });

  $('#apxStart').addEventListener('click', async () => {
    try {
      await waitForOWOP();
      if (!imgData) { alert('Charge d’abord une image. / Please load an image first.'); return; }
      state.x0 = parseInt($('#apxX').value || '0', 10) || 0;
      state.y0 = parseInt($('#apxY').value || '0', 10) || 0;
      startAll();
    } catch(_) { alert('OWOP n’est pas prêt (timeout). Recharge la page. / OWOP not ready (timeout). Reload the page.'); }
  });

  $('#apxStop').addEventListener('click', stopAll);

  $('#apxReset').addEventListener('click', () => {
    stopAll();
    queue.length = 0;
    scanX = scanY = 0; scannedAll = false;
    beginVerify(); verifyDone = true;
    setStatus(false, 'Progression réinitialisée. / Progress reset.');
  });

  blockEl.addEventListener('input', () => {
    const v = parseInt(blockEl.value, 10) || 1;
    state.block = (v % 2 === 0) ? (v + 1) : v; // 1,3,5,...
    blockEl.value = String(state.block);
    blockInfo.textContent = `Bloc: ${state.block}×${state.block} (${state.block*state.block} px) / Block: ${state.block}×${state.block} (${state.block*state.block} px)`;
  });

  scaleEl.addEventListener('input', () => {
    scalePct = parseInt(scaleEl.value, 10) || 100;
    rebuildScaled();
  });

  rateEl.addEventListener('input', () => {
    state.rate = Math.max(1, parseInt(rateEl.value, 10) || 15);
    rateInfo.textContent = `Débit: ${state.rate} px/s / Rate: ${state.rate} px/s`;
    if (state.running) setRateTimer();
  });

  // init displays
  blockEl.dispatchEvent(new Event('input'));
  scaleEl.dispatchEvent(new Event('input'));
  rateEl.dispatchEvent(new Event('input'));

  console.log('[AutoPixel OWOP] ready ✅ (watermarks 500/5000, v1.4.3)');
})();