Discuss Scratch

Umhead22
Scratcher
100+ posts

Stage effects

I've made stage extension, it works but the problem is that pielation doesn't work so i removed it from menus. I need help with pixelation effect

// you can use this extension however you like, credits aren't required.
// simple extension for applying visual effects to the Scratch stage
// Pixelated effect is implemented via overlay canvas as CSS filter doesn't support it, it crashes extension.
// all credits goes to me, Umhead.
// all credit goes to Umhead for creating this extension. (aka me)
// version 0.1 2025.10.29
(function (Scratch) {
  'use strict';
  if (!Scratch.extensions || !Scratch.extensions.unsandboxed) {
    alert("Extension MUST be unsandboxed to work properly. Please enable 'Unsandboxed extensions' before loading it");
    throw new Error('Unsandboxed mode required.');
  }
  const vm = Scratch.vm;
  const renderer = vm && vm.renderer;
  let stageCanvas = null;
  let usedCssFilter = false;
  // Overlay fallback vars
  let overlayCanvas = null;
  let overlayCtx = null;
  let overlayTempCanvas = null; // offscreen temp canvas for pixelation, not working with main overlay.
  let overlayTempCtx = null;
  let currentMode = 'none'; // 'css', 'overlay'
  let rafId = null;
  let activeEffects = [];
  function log(...args) {
    console.log('[StageFX]', ...args);
  }
  function findStageCanvas() {
    try {
      if (renderer && renderer.canvas) return renderer.canvas;
      const candidates = Array.from(document.querySelectorAll('canvas'));
      if (!candidates.length) return null;
      if (candidates.length === 1) return candidates[0];
      for (const c of candidates) {
        const r = c.getBoundingClientRect();
        if (r.width >= 200 && r.height >= 120) return c;
      }
      return candidates[0];
    } catch (e) {
      log('Error finding stage canvas:', e);
      return null;
    }
  }
  function composeCssFilterFromActive() {
    return activeEffects
      .filter(e => e.name !== 'pixelate')
      .map(e => {
        switch (e.name) {
          case 'blur': return `blur(${e.value}px)`;
          case 'brightness': return `brightness(${e.value}%)`;
          case 'contrast': return `contrast(${e.value}%)`;
          case 'grayscale': return `grayscale(${e.value}%)`;
          case 'sepia': return `sepia(${e.value}%)`;
          case 'invert': return `invert(${e.value}%)`;
          case 'hue-rotate': return `hue-rotate(${e.value}deg)`;
          case 'saturate': return `saturate(${e.value}%)`;
          default: return '';
        }
      })
      .filter(Boolean)
      .join(' ');
  }
  function applyCssFilterToCanvas(filter) {
    try {
      stageCanvas = findStageCanvas();
      if (!stageCanvas) {
        log('No stage canvas found for CSS filter.');
        return false;
      }
      stageCanvas.style.filter = filter;
      stageCanvas.style.pointerEvents = 'auto';
      usedCssFilter = true;
      currentMode = 'css';
      log('Applied CSS filter to stage canvas:', filter);
      return true;
    } catch (e) {
      log('CSS filter failed:', e);
      return false;
    }
  }
  function clearCssFilterFromCanvas() {
    try {
      stageCanvas = findStageCanvas();
      if (stageCanvas) {
        stageCanvas.style.filter = '';
        log('Cleared CSS filter from canvas');
      }
    } catch (e) {
      log('Error clearing CSS filter:', e);
    }
    usedCssFilter = false;
  }
  function setupOverlay() {
    try {
      stageCanvas = findStageCanvas();
      if (!stageCanvas) {
        log('No stage canvas found for overlay fallback.');
        return false;
      }
      if (!overlayCanvas) {
        overlayCanvas = document.createElement('canvas');
        overlayCanvas.style.position = 'fixed';
        overlayCanvas.style.zIndex = 999999;
        overlayCanvas.style.pointerEvents = 'none';
        overlayCanvas.style.imageRendering = 'pixelated';
        document.body.appendChild(overlayCanvas);
        overlayCtx = overlayCanvas.getContext('2d');
        overlayCtx.imageSmoothingEnabled = false;
        log('Overlay created');
      }
      updateOverlayPositionAndSize();
      return true;
    } catch (e) {
      log('setupOverlay error:', e);
      return false;
    }
  }
  function updateOverlayPositionAndSize() {
    if (!overlayCanvas) return;
    stageCanvas = findStageCanvas();
    if (!stageCanvas) return;
    const rect = stageCanvas.getBoundingClientRect();
    overlayCanvas.style.left = rect.left + 'px';
    overlayCanvas.style.top = rect.top + 'px';
    overlayCanvas.style.width = rect.width + 'px';
    overlayCanvas.style.height = rect.height + 'px';
    overlayCanvas.width = stageCanvas.width || Math.round(rect.width);
    overlayCanvas.height = stageCanvas.height || Math.round(rect.height);
  }
  function drawOverlayFilter() {
    if (!overlayCtx || !stageCanvas) return;
    try {
      overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
      const hasPixel = activeEffects.some(e => e.name === 'pixelate');
      if (hasPixel) {
        const eff = activeEffects.find(e => e.name === 'pixelate');
        const scale = Math.max(1, Math.round(eff.value) || 4);
        const w = Math.max(1, Math.floor(overlayCanvas.width / scale));
        const h = Math.max(1, Math.floor(overlayCanvas.height / scale));
        if (!overlayTempCanvas) {
          overlayTempCanvas = document.createElement('canvas');
          overlayTempCtx = overlayTempCanvas.getContext('2d');
        }
        if (overlayTempCanvas.width !== w || overlayTempCanvas.height !== h) {
          overlayTempCanvas.width = w;
          overlayTempCanvas.height = h;
        }
        overlayTempCtx.imageSmoothingEnabled = false;
        overlayTempCtx.clearRect(0, 0, w, h);
        overlayTempCtx.drawImage(stageCanvas, 0, 0, stageCanvas.width || overlayCanvas.width, stageCanvas.height || overlayCanvas.height, 0, 0, w, h);
        overlayCtx.imageSmoothingEnabled = false;
        overlayCtx.drawImage(overlayTempCanvas, 0, 0, w, h, 0, 0, overlayCanvas.width, overlayCanvas.height);
        return;
      }
      const filterString = activeEffects
        .filter(e => e.name !== 'pixelate')
        .map(e => {
          switch (e.name) {
            case 'blur': return `blur(${e.value}px)`;
            case 'brightness': return `brightness(${e.value}%)`;
            case 'contrast': return `contrast(${e.value}%)`;
            case 'grayscale': return `grayscale(${e.value}%)`;
            case 'sepia': return `sepia(${e.value}%)`;
            case 'invert': return `invert(${e.value}%)`;
            case 'hue-rotate': return `hue-rotate(${e.value}deg)`;
            case 'saturate': return `saturate(${e.value}%)`;
            default: return '';
          }
        })
        .filter(Boolean)
        .join(' ');
      overlayCtx.filter = filterString || 'none';
      overlayCtx.imageSmoothingEnabled = true;
      overlayCtx.drawImage(stageCanvas, 0, 0, overlayCanvas.width, overlayCanvas.height);
    } catch (e) {
      log('drawOverlayFilter error:', e);
    }
  }
  function startOverlayLoop() {
    if (!setupOverlay()) return false;
    if (rafId) return true;
    const loop = () => {
      updateOverlayPositionAndSize();
      drawOverlayFilter();
      rafId = requestAnimationFrame(loop);
    };
    rafId = requestAnimationFrame(loop);
    currentMode = 'overlay';
    log('Overlay loop started');
    return true;
  }
  function stopOverlayLoop() {
    if (rafId) cancelAnimationFrame(rafId);
    rafId = null;
    if (overlayCtx && overlayCanvas) overlayCtx.clearRect(0, 0, overlayCanvas.width, overlayCanvas.height);
    if (overlayCanvas && overlayCanvas.parentNode) overlayCanvas.parentNode.removeChild(overlayCanvas);
    overlayCanvas = null;
    overlayCtx = null;
    overlayTempCanvas = null;
    overlayTempCtx = null;
    currentMode = 'none';
    log('Overlay stopped');
  }
  // --- Auto-fix for disappearing effects ---
  function handleCanvasChange() {
    stageCanvas = findStageCanvas();
    if (!stageCanvas) return;
    if (activeEffects.length > 0) {
      const css = composeCssFilterFromActive();
      if (css) applyCssFilterToCanvas(css);
      else startOverlayLoop();
    }
    if (overlayCanvas) updateOverlayPositionAndSize();
  }
  window.addEventListener('resize', handleCanvasChange);
  window.addEventListener('scroll', handleCanvasChange, true);
  document.addEventListener('fullscreenchange', handleCanvasChange);
  const observer = new MutationObserver(() => {
    handleCanvasChange();
  });
  function startObservingStage() {
    const stageWrapper = document.querySelector('.stage-wrapper, .stage_canvas_wrapper, .stage');
    if (stageWrapper) {
      observer.observe(stageWrapper, { childList: true, subtree: true });
      log('Stage canvas observer attached.');
    } else {
      setTimeout(startObservingStage, 1000);
    }
  }
  startObservingStage();
  class StageEffects {
    getInfo() {
      return {
        id: 'stagefx',
        name: 'Stage Effects',
        color1: '#4b6ef1',
        blocks: [
          {
            opcode: 'applyEffect',
            blockType: Scratch.BlockType.COMMAND,
            text: 'apply [EFFECT] strength [VALUE]',
            arguments: {
              EFFECT: { type: Scratch.ArgumentType.STRING, menu: 'effects' },
              VALUE: { type: Scratch.ArgumentType.NUMBER, defaultValue: 5 }
            }
          },
          {
            opcode: 'clearEffects',
            blockType: Scratch.BlockType.COMMAND,
            text: 'clear effects'
          }
        ],
        menus: {
          effects: { acceptReporters: true, items: ['blur','brightness','contrast','grayscale','sepia','invert','hue-rotate','saturate'] }
        }
      };
    }
    applyEffect(args) {
      const eff = (args.EFFECT || '').toString().toLowerCase();
      const val = Number(args.VALUE) || 0;
      log('applyEffect requested:', eff, val);
      const name = eff === 'pixel' ? 'pixelate' : eff;
      const existingIndex = activeEffects.findIndex(e => e.name === name);
      if (existingIndex >= 0) activeEffects[existingIndex].value = val;
      else activeEffects.push({ name, value: val });
      const hasPixel = activeEffects.some(e => e.name === 'pixelate');
      const css = composeCssFilterFromActive();
      if (hasPixel) {
        stageCanvas = findStageCanvas();
        if (stageCanvas) stageCanvas.style.filter = '';
        startOverlayLoop();
        return;
      }
      if (css) {
        const ok = applyCssFilterToCanvas(css);
        if (ok) {
          if (overlayCanvas) stopOverlayLoop();
          log('Using CSS filter path.');
          return;
        }
      }
      startOverlayLoop();
    }
    clearEffects() {
      activeEffects = [];
      clearCssFilterFromCanvas();
      stopOverlayLoop();
      log('clearEffects executed');
    }
  }
  Scratch.extensions.register(new StageEffects());
  log('Stage Effects extension loaded.');
})(Scratch);
It adds two blocks:
apply (blur v) strenght (5) :: motion

clear effects :: motion

My browser / operating system: Windows NT 10.0, Chrome 141.0.0.0, No Flash versions detected
Umhead22
Scratcher
100+ posts

Stage effects

I don't expect you to view whole code, i expect to give small solution how i can make pixel effect

Powered by DjangoBB