Display renderer.domElement result downscaled multiple times

I have a website where I want to create canvasses with different sizes, and render a result from three.js on there.

Those newly created canvasses don’t have to be made with three,js, but I’m not against that either.
What is important is that they are ‘webgl’ so things stay fast.

Here an example of the different targets:

Screenshot 2023-11-20 161852

My problem so far was that downscaling looked terrible, here is an example:
downscale_threejs

Here is the same thing downscaled in photoshop:
downscaled

I tried a implementation with a offscreen_scale_factor but this didn’t really help.
I also tried a renderer that renderers the result on a plane, also this quality was bad.
As last I tried creating a webgl canvas that renderer the renderer.domElement as a texture, but nothing shows up.

I also saw there is CanvasRenderingContext2D: imageSmoothingQuality property - Web APIs | MDN, but this is not supported in firefox. Also I truly believe it should be possible to achieve this without imageSmoothingQuality I guess since things can be drawn with a shader?

Anyway, I hope someone can help me.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>HSLAB</title>
    <style>
        body {
            margin: 0;
            overflow: hidden;
        }
    </style>
</head>
<body>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/0.158.0/three.min.js"></script>
    <script src="hslab_logo.js"></script>
    <script src="hslab_logo_loader.js"></script>

    <div class="hslab_logo" style="width: 1024px; height: 256px; background-color: red;"></div>
    <div class="hslab_logo" style="width: 512px; height: 128px; background-color: green;"></div>
    <div class="hslab_logo" style="width: 256px; height: 64px; background-color: blue;"></div>
    <div class="hslab_logo" style="width: 128px; height: 32px; background-color: yellow;"></div>
    <div class="hslab_logo" style="width: 64px; height: 16px; background-color: magenta;"></div>
</body>
</html>

hslab_logo.js

"use strict";

let renderer;

(function(){

let scene, camera;
let texture1, texture2;
let mesh_zoom_area;


const OFFSCREEN_RENDER_WIDTH = 1024;
const OFFSCREEN_RENDER_HEIGHT = 1024;

// ---------------------------------------------------------

addEventListener("DOMContentLoaded", (event) => {
    init();
});

function init() {
   
    // Scene setup
    scene = new THREE.Scene();
    camera = new THREE.PerspectiveCamera(75, OFFSCREEN_RENDER_WIDTH / OFFSCREEN_RENDER_HEIGHT, 0.1, 1000);
    camera.position.z = 5;
    
    // Renderer setup
    renderer = new THREE.WebGLRenderer();
    renderer.setSize(OFFSCREEN_RENDER_WIDTH, OFFSCREEN_RENDER_HEIGHT);
    renderer.setClearColor(0xffffff);
    
    // Texture loading
    texture1 = new THREE.TextureLoader().load('HSL_Mask_0002.png');
    texture2 = new THREE.TextureLoader().load('HSL_RGB_0002.png');
    texture1.minFilter = THREE.NearestFilter;
    texture2.minFilter = THREE.NearestFilter;
    texture1.magFilter = THREE.NearestFilter;
    texture2.magFilter = THREE.NearestFilter;

    // Mesh setup
    const geometry = new THREE.PlaneGeometry(8, 8);
    const material1 = new THREE.MeshBasicMaterial({ map: texture1, transparent: true, alphaTest: 0.5 });
    const material2 = new THREE.MeshBasicMaterial({ map: texture2, transparent: true, alphaTest: 0.5 });
    const mesh1 = new THREE.Mesh(geometry, material1);
    const mesh2 = new THREE.Mesh(geometry, material2);
    mesh2.position.z = -0.025;

    // Zoom area setup
    const zoom_geometry = new THREE.PlaneGeometry(3.5, 1.1);
    const zoom_material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
    mesh_zoom_area = new THREE.Mesh(zoom_geometry, zoom_material);
    mesh_zoom_area.position.z = -0.035;

    // Finally...
    scene.add(mesh1);
    scene.add(mesh2);

    // window.addEventListener('resize', onWindowResize, false);

    // And start...
    // onWindowResize();
    adjustCameraFov(camera);

    animate();


}

/*
function onWindowResize() {

    const logo_container = logo_containers[0];
    const bounds = logo_container.getBoundingClientRect();

    // TODO
    bounds.width = 1024;
    bounds.height = 1024;


    // Adjust camera aspect ratio and update its projection matrix
    camera.aspect = bounds.width / bounds.height;
    camera.updateProjectionMatrix();

    // Update renderer size
    renderer.setSize(bounds.width, bounds.height);

    // Custom camera adjustments
    adjustCameraFov(camera);
}
*/

function adjustCameraFov(camera) {
    camera.fov = map(camera.aspect, 1, 5, 50, 30);
    if (camera.fov < 1) camera.fov = 1;
    fitCameraToCenteredObject(camera, mesh_zoom_area, 1, undefined);
}


function animate() {
    requestAnimationFrame(animate);

    const millis = performance.now();
    scene.rotation.y = Math.cos(millis * 0.0005) * Math.PI * 0.25;
    
    renderer.render(scene, camera);
}

// Utility functions
function map(value, fromMin, fromMax, toMin, toMax) {
    return (value - fromMin) / (fromMax - fromMin) * (toMax - toMin) + toMin;
}

function fitCameraToCenteredObject(camera, object, offset, orbitControls) {
    const boundingBox = new THREE.Box3();
    boundingBox.setFromObject( object );

    var middle = new THREE.Vector3();
    var size = new THREE.Vector3();
    boundingBox.getSize(size);

    // figure out how to fit the box in the view:
    // 1. figure out horizontal FOV (on non-1.0 aspects)
    // 2. figure out distance from the object in X and Y planes
    // 3. select the max distance (to fit both sides in)
    //
    // The reason is as follows:
    //
    // Imagine a bounding box (BB) is centered at (0,0,0).
    // Camera has vertical FOV (camera.fov) and horizontal FOV
    // (camera.fov scaled by aspect, see fovh below)
    //
    // Therefore if you want to put the entire object into the field of view,
    // you have to compute the distance as: z/2 (half of Z size of the BB
    // protruding towards us) plus for both X and Y size of BB you have to
    // figure out the distance created by the appropriate FOV.
    //
    // The FOV is always a triangle:
    //
    //  (size/2)
    // +--------+
    // |       /
    // |      /
    // |     /
    // | F° /
    // |   /
    // |  /
    // | /
    // |/
    //
    // F° is half of respective FOV, so to compute the distance (the length
    // of the straight line) one has to: `size/2 / Math.tan(F)`.
    //
    // FTR, from https://threejs.org/docs/#api/en/cameras/PerspectiveCamera
    // the camera.fov is the vertical FOV.

    const fov = camera.fov * ( Math.PI / 180 );
    const fovh = 2*Math.atan(Math.tan(fov/2) * camera.aspect);
    let dx = size.z / 2 + Math.abs( size.x / 2 / Math.tan( fovh / 2 ) );
    let dy = size.z / 2 + Math.abs( size.y / 2 / Math.tan( fov / 2 ) );
    let cameraZ = Math.max(dx, dy);

    // offset the camera, if desired (to avoid filling the whole canvas)
    if( offset !== undefined && offset !== 0 ) cameraZ *= offset;

    camera.position.set( 0, 0, cameraZ );

    // set the far plane of the camera so that it easily encompasses the whole object
    const minZ = boundingBox.min.z;
    const cameraToFarEdge = ( minZ < 0 ) ? -minZ + cameraZ : cameraZ - minZ;

    camera.far = cameraToFarEdge * 3;
    camera.updateProjectionMatrix();

    if ( orbitControls !== undefined ) {
        // set camera to rotate around the center
        orbitControls.target = new THREE.Vector3(0, 0, 0);

        // prevent camera from zooming out far enough to create far plane cutoff
        orbitControls.maxDistance = cameraToFarEdge * 2;
    }
}


})();

hslab_logo_loader.js

"use strict";

let logo_containers;

(function(){

addEventListener("DOMContentLoaded", (event) => {
    init();
});

function init() {
    onsole.log(renderer.domElement);

  
    logo_containers = document.querySelectorAll(".hslab_logo");

    if (logo_containers.length == 0) {
        console.log("[Warning] No div found with class=\"hslab_logo\"!");
        return;
    }
    
    logo_containers.forEach(container => {

    });


}

})();

I removed the non working code since I had to many variations for that.

Here are the textures:

This one is white with alpha!:

^^^

Hope someone can help

Keep in mind photoshop has method for image resizing. Theres a little dropdown menu you can select from when in the image resizer window. And theres a default method in preferences it uses when in layer view. You have to bring that function over to Canvas to get similar results. Otherwise its just crushing pixels down.