9 C
New York
Tuesday, March 11, 2025

Replicating CSS Object-Slot in WebGL: Optimized Methods for Picture Scaling and Positioning


For those who’ve ever labored with pictures on the net, you’re in all probability accustomed to the CSS property object-fit: cowl. This property ensures that a picture fully covers its container whereas preserving its side ratio, which is essential for responsive layouts. Nevertheless, in the case of WebGL, replicating this impact isn’t as simple. In contrast to conventional 2D pictures, WebGL includes making use of textures to 3D meshes, and this brings with it a set of efficiency challenges.

On this article, we’ll discover a number of strategies to realize the object-fit: cowl; impact in WebGL. I first got here throughout a very elegant technique in this text on Codrops, written by Luis Bizarro (thanks, Luis!). His method cleverly calculates the side ratio immediately throughout the fragment shader:

// https://tympanus.web/codrops/2021/01/05/creating-an-infinite-auto-scrolling-gallery-using-webgl-with-ogl-and-glsl-shaders/

precision highp float;
 
uniform vec2 uImageSizes;
uniform vec2 uPlaneSizes;
uniform sampler2D tMap;
 
various vec2 vUv;
 
void principal() {
  vec2 ratio = vec2(
    min((uPlaneSizes.x / uPlaneSizes.y) / (uImageSizes.x / uImageSizes.y), 1.0),
    min((uPlaneSizes.y / uPlaneSizes.x) / (uImageSizes.y / uImageSizes.x), 1.0)
  );
 
  vec2 uv = vec2(
    vUv.x * ratio.x + (1.0 - ratio.x) * 0.5,
    vUv.y * ratio.y + (1.0 - ratio.y) * 0.5
  );
 
  gl_FragColor.rgb = texture2D(tMap, uv).rgb;
  gl_FragColor.a = 1.0;
}

I received’t go into the specifics of organising a 3D atmosphere right here, as the main focus is on the logic and strategies for picture scaling and positioning. The code I’ll present is meant to elucidate the core idea, which may then be tailored to varied WebGL setups.

The method we’re utilizing includes calculating the side ratios of each the airplane and the picture, then adjusting the UV coordinates by multiplying these ratios to proportionally zoom within the picture, guaranteeing it doesn’t go under a scale of 1.0.

Whereas this method is efficient, it may be computationally costly as a result of the calculations are carried out for every fragment of the mesh and picture constantly. Nevertheless, in our case, we are able to optimize this by calculating these values as soon as and passing them to the shader as uniforms. This technique permits us to take care of full management over scaling and positioning whereas considerably lowering computational overhead.

That mentioned, the shader-based method stays extremely helpful, notably in eventualities the place dynamic recalculation is required.

Important Logic: Calculating Picture and Mesh Ratios

Now let’s transfer on to the primary logic, which permits us to calculate whether or not our mesh is bigger or smaller than our picture after which modify accordingly. After defining the dimensions of our mesh as described above, we have to calculate its ratio, in addition to the ratio of our picture.

If the ratio of our picture is bigger than the ratio of our mesh, it means the picture is wider, so we should modify the width by multiplying it by the ratio between the picture’s ratio and the mesh’s ratio. This ensures that the picture fully covers the mesh in width with out distortion, whereas the peak stays unchanged.

Equally, if the ratio of our picture is smaller than that of our mesh, we modify the peak to increase it to fully fill the peak of the mesh.

If each circumstances are met, it implies that the ratio is identical between the 2.

fitImage(){
    const imageRatio = picture.width / picture.peak;
    const meshSizesAspect = mesh.scale.x / mesh.scale.y;

    let scaleWidth, scaleHeight;

    if (imageRatio > meshSizesAspect) {
        scaleWidth = imageRatio / meshSizesAspect;
        scaleHeight = 1;
    } else if (imageRatio < meshSizesAspect) {
        scaleWidth = 1;
        scaleHeight = meshSizesAspect / imageRatio;
    } else {
        scaleWidth = 1;
        scaleHeight = 1;
    }

    this.program.uniforms.uPlaneSizes.worth = [scaleWidth, scaleHeight];
}

After calculating this, we now want to regulate it with our UVs.

// vertex.glsl

precision highp float;

attribute vec3 place;
attribute vec2 uv;

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

uniform vec2 uImagePosition;
uniform vec2 uPlaneSizes;

various vec2 vUv;

void principal() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4(place, 1.0);
    vUv = uImagePosition + .5 + (uv - .5) / uPlaneSizes;
}
// fragment.glsl

precision highp float;
various vec2 vUv;
uniform sampler2D tMap;

void principal() {
    gl_FragColor = texture2D(tMap, vUv);
}

So, after normalizing the UVs, we divide them by the vec2 that we calculated beforehand to regulate the UVs in order that the picture is mapped accurately onto the mesh whereas respecting the proportions of uPlaneSizes.

Now, we are able to merely compute the values of uPlaneSizes inside a operate to initially calculate them on every resize. That is additionally helpful after we need to deform the size of our mesh, for instance, if we need to shut a picture whereas sustaining a visually aesthetic ratio.

Animation Examples

Instance 1: Primary Scaling With out fitImage()

const begin = {
    x: this.mesh.scale.x,
}

new Replace({
    period: 1500,
    ease: [0.75, 0.30, 0.20, 1],
    replace: (t) => {
        this.mesh.scale.x = Lerp(begin.x, begin.x * 2, t.ease);
    },
}).begin();

Instance 2: Integrating fitImage() within the Replace Loop

By recalculating ratios throughout animations, we guarantee constant scaling:

const begin = {
    x: this.mesh.scale.x,
}

new Replace({
    period: 1500,
    ease: [0.75, 0.30, 0.20, 1],
    replace: (t) => {
        this.mesh.scale.x = Lerp(begin.x, begin.x * 2, t.ease);
        this.fitImage(); // Execute the tactic solely through the animation
    },
}).begin();

Vertical Translation with uImagePosition

Modifying the uImagePosition uniform permits for offsetting the feel coordinates, mimicking a div with overflow: hidden:

new Replace({
    period: 1500,
    ease: [0.1, 0.7, 0.2, 1],
    replace: (t) => {
        this.program.uniforms.uImagePosition.worth[1] = Lerp(0, 1, t.ease);
    },
}).begin();

This shifts the picture vertically throughout the mesh, just like translating a background picture.

Combining Mesh Scaling and Inside Positioning

To animate a mesh closing vertically whereas sustaining alignment (like transform-origin: prime in CSS):

const begin = {
    y: this.mesh.scale.y,
    x: this.mesh.scale.x,
    place: this.mesh.place.y,
}

new Replace({
    period: 1500,
    ease: [0.1, 0.7, 0.2, 1],
    replace: (t) => {
        const ease = t.ease;
        this.mesh.scale.y = Lerp(begin.y, 0, ease);
        this.program.uniforms.uImagePosition.worth[1] = Lerp(0, .25, ease);
        this.fitImage();
    },
}).begin();

Adjusting Remodel Origin

By default, the mesh will shrink towards the middle. To maintain it anchored to the highest (equal to CSS’s transform-origin: prime), modify the mesh’s place by half the dimensions worth throughout transformations:

// Add this line within the replace technique
this.mesh.place.y = Lerp(begin.place, begin.y / 2, ease);

Combining Mesh Scaling and Uniform Changes

By modifying each the mesh dimension and uniforms, we are able to create fascinating visible results. For instance, right here’s an thought for a vertical slider with two pictures:

Thanks for studying! I hope this information helps you optimize picture manipulation in WebGL. When you have any questions, ideas, or simply need to share your ideas, be happy to succeed in out.



Supply hyperlink

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles