Как создать эффект фонарика с помощью PixiJS?

Хотелось бы сделать подобный эффект фонарика как на сайте - https://awwwards-2020.tubikstudio.com/ Судя, по всему, там используется PixiJS.

Смог добиться этого эффекта с помощью CSS/JS, но хотелось бы увидеть решение на PixiJS.

Все, что получилось добиться с помощью PixiJS v6.5.2:

const createBackground = (path) => {
  const background = PIXI.Sprite.from(path);
  
  background.width = app.screen.width;
  background.height = app.screen.height;

  return background;
};

const app = new PIXI.Application({
  width: window.innerWidth,
  height: window.innerHeight,
  backgroundColor: 0x000000
});

document.body.appendChild(app.view);

// Функция для создания затемнённого фона с прозрачным кругом
function createDarkCoverWithHole(x, y) {
  const darkCover = new PIXI.Graphics();
  darkCover.clear();
  darkCover.beginFill(0x000000, 1);
  darkCover.drawRect(0, 0, app.screen.width, app.screen.height);
  darkCover.beginHole(); // Начинаем создание "дыры"
  darkCover.drawCircle(x, y, 300); // Рисуем круг в позиции курсора
  darkCover.endHole(); // Заканчиваем создание "дыры"
  darkCover.endFill();

  return darkCover;
}

app.stage.addChild(createBackground('background.jpg'));

let darkCover = createDarkCoverWithHole(app.screen.width / 2, app.screen.height / 2);
app.stage.addChild(darkCover);

// Обработчик движения мыши
app.renderer.plugins.interaction.on('mousemove', (event) => {
  const { x, y } = event.data.global;
  app.stage.removeChild(darkCover);
  darkCover = createDarkCoverWithHole(x, y);
  app.stage.addChild(darkCover);
});

Ответы (1 шт):

Автор решения: Александр

Нашел решение (PixiJS v6.5.10):

const app = new PIXI.Application({
  width: window.innerWidth,
  height: window.innerHeight,
  backgroundColor: 0x000000
});
document.body.appendChild(app.view);

const radius = 400;
const blurSize = 32;

app.loader.add('grass', 'background.jpg');
app.loader.load(setup);

let targetX = 0; // Целевая позиция X маски
let targetY = 0; // Целевая позиция Y маски
let currentX = 0; // Текущая позиция X маски
let currentY = 0; // Текущая позиция Y маски
const inertia = 0.1; // Коэффициент инерции (0 - немедленное перемещение, 1 - отсутствие перемещения)

function setup(loader, resources) {
  const background = new PIXI.Sprite(resources.grass.texture);
  app.stage.addChild(background);
  background.width = app.screen.width;
  background.height = app.screen.height;

  const circle = new PIXI.Graphics()
    .beginFill(0xFF0000)
    .drawCircle(radius + blurSize, radius + blurSize, radius)
    .endFill();
  circle.filters = [new PIXI.filters.BlurFilter(blurSize)];

  const bounds = new PIXI.Rectangle(0, 0, (radius + blurSize) * 2, (radius + blurSize) * 2);
  const texture = app.renderer.generateTexture(circle, PIXI.SCALE_MODES.NEAREST, 1, bounds);
  const focus = new PIXI.Sprite(texture);

  app.stage.addChild(focus);
  background.mask = focus;

  app.stage.interactive = true;
  app.stage.on('mousemove', pointerMove);

  app.ticker.add(() => {
    // Плавное обновление позиции маски с учетом инерции
    currentX += (targetX - currentX) * inertia;
    currentY += (targetY - currentY) * inertia;

    focus.position.x = currentX - focus.width / 2;
    focus.position.y = currentY - focus.height / 2;
  });

  function pointerMove(event) {
    // Обновляем целевую позицию маски согласно положению курсора
    targetX = event.data.global.x;
    targetY = event.data.global.y;
  }
}
→ Ссылка