PixiJS 313,967 (MIT, 8.21.0, github.com/pixijs/pixijs (https://github.com/pixijs/pixijs 48,232 ), about 48,200 stars) renders 2D sprites, vector graphics, text and filters through a scene graph, batching everything that shares textures into few draw calls. PixiJS ran it on WebGPU; here preference: 'webgl' asks for its WebGL 2 renderer, and the covers are image sprites with a built-in filter graying all but one:
<script src="https://cdn.jsdelivr.net/npm/pixi.js@8.21.0/dist/pixi.min.js"></script>
<script type="module">
const app = new PIXI.Application();
await app.init({ preference: 'webgl', width: 600, height: 210, background: '#ece8de',
antialias: true, resolution: 2, autoDensity: true });
document.body.appendChild(app.canvas);
const urls = [1, 2, 3, 4, 5, 6].map((n) => `booknest-cover-${n}.png`);
const textures = await PIXI.Assets.load(urls); // loaded and uploaded
const gray = new PIXI.ColorMatrixFilter(); gray.desaturate();
urls.forEach((url, i) => app.stage.addChild(new PIXI.Sprite({ texture: textures[url],
anchor: 0.5, x: 70 + i * 92, y: 105, scale: i === 2 ? 0.45 : 0.36,
rotation: (i - 2.5) * 0.08, filters: i === 2 ? [] : [gray] })));
app.stage.addChild(app.stage.getChildAt(2)); // featured cover on top
app.render();
window.__done = true;
</script>
app.renderer.name reported webgl. With preference: 'webgpu' the same code runs on WebGPU and falls back to WebGL without an adapter. Filters are render-to-texture passes (Post-Processing and Particles), so put one filter on a container rather than one per sprite.