You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为容器添加WebGL悬浮效果并实现图片object-fit适配?

解决方案:为容器添加WebGL悬浮效果并实现object-fit: cover适配

要实现目标效果,我们需要用WebGL渲染图片来实现悬浮变形,同时确保图片按object-fit: cover规则适配右侧52vw宽的容器。以下是完整实现步骤:

1. 调整基础布局与样式

先修正结构,隐藏原始图片,用WebGL画布承载渲染效果:

.casestart {
  height: 100vh;
  width: 100vw;
  position: relative;
  overflow: hidden;
}

.imageside {
  width: 52vw;
  height: 100vh;
  position: absolute;
  right: 0;
  background: #000;
  overflow: hidden; /* 限制内容不溢出容器 */
}

canvas{
  width: 100%;
  height: 100%;
  display: block;
}

#hoverImage {
  display: none; /* 仅作为WebGL纹理源,不显示 */
}
<div class="casestart">
  <div class="imageside">
    <canvas id="webglCanvas"></canvas>
    <img id="hoverImage" src="https://www.datocms-assets.com/92457/1676542990-mvsm_logitechquake2_explode01_v001_b.jpg?fit=max&fm=jpg&h=1920&w=1920" alt="" draggable="false">
  </div>
</div>

2. WebGL核心实现代码

以下JS代码实现鼠标悬浮变形效果,同时通过着色器计算实现object-fit: cover的适配逻辑:

const canvas = document.getElementById('webglCanvas');
const gl = canvas.getContext('webgl');
const image = document.getElementById('hoverImage');

// 适配容器尺寸
function resizeCanvas() {
  const container = document.querySelector('.imageside');
  canvas.width = container.clientWidth;
  canvas.height = container.clientHeight;
  gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();

// 顶点着色器
const vertexShaderSource = `
attribute vec2 a_position;
attribute vec2 a_texCoord;
varying vec2 v_texCoord;
void main() {
  gl_Position = vec4(a_position, 0.0, 1.0);
  v_texCoord = a_texCoord;
}
`;

// 片段着色器:实现cover适配+悬浮变形
const fragmentShaderSource = `
precision mediump float;
uniform sampler2D u_image;
uniform vec2 u_resolution;
uniform vec2 u_mouse;
uniform float u_time;

// 计算cover模式的纹理坐标
vec2 getCoverTexCoord(vec2 texCoord) {
  vec2 imageSize = vec2(textureSize(u_image, 0));
  vec2 containerSize = u_resolution;
  
  float imageRatio = imageSize.x / imageSize.y;
  float containerRatio = containerSize.x / containerSize.y;
  
  vec2 scale = vec2(1.0);
  if (imageRatio > containerRatio) {
    scale.y = imageRatio / containerRatio;
    texCoord.y = (texCoord.y - 0.5) / scale.y + 0.5;
  } else {
    scale.x = containerRatio / imageRatio;
    texCoord.x = (texCoord.x - 0.5) / scale.x + 0.5;
  }
  return texCoord;
}

void main() {
  vec2 texCoord = getCoverTexCoord(v_texCoord);
  
  // 鼠标距离计算
  vec2 mousePos = u_mouse / u_resolution;
  float distance = length(mousePos - texCoord);
  float strength = smoothstep(0.3, 0.0, distance);
  
  // 流动变形效果
  vec2 offset = vec2(
    sin(texCoord.y * 10.0 + u_time) * strength * 0.02,
    cos(texCoord.x * 10.0 + u_time) * strength * 0.02
  );
  
  vec4 color = texture2D(u_image, texCoord + offset);
  gl_FragColor = color;
}
`;

// 创建着色器工具函数
function createShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error(gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    return null;
  }
  return shader;
}

// 初始化WebGL程序
const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
  console.error(gl.getProgramInfoLog(program));
  return;
}
gl.useProgram(program);

// 设置顶点数据
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
const positions = [
  -1, -1,
  1, -1,
  -1, 1,
  1, 1,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
const positionLocation = gl.getAttribLocation(program, 'a_position');
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);

// 设置纹理坐标
const texCoordBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, texCoordBuffer);
const texCoords = [
  0, 0,
  1, 0,
  0, 1,
  1, 1,
];
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(texCoords), gl.STATIC_DRAW);
const texCoordLocation = gl.getAttribLocation(program, 'a_texCoord');
gl.enableVertexAttribArray(texCoordLocation);
gl.vertexAttribPointer(texCoordLocation, 2, gl.FLOAT, false, 0, 0);

// 加载纹理
const texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

// 获取Uniform变量位置
const resolutionLocation = gl.getUniformLocation(program, 'u_resolution');
const mouseLocation = gl.getUniformLocation(program, 'u_mouse');
const timeLocation = gl.getUniformLocation(program, 'u_time');
gl.uniform2f(resolutionLocation, canvas.width, canvas.height);

// 跟踪鼠标位置
let mouseX = 0;
let mouseY = 0;
canvas.addEventListener('mousemove', (e) => {
  const rect = canvas.getBoundingClientRect();
  mouseX = e.clientX - rect.left;
  mouseY = rect.bottom - e.clientY; // 适配WebGL左下角原点坐标
});

// 动画循环
function animate(time) {
  gl.uniform1f(timeLocation, time * 0.001);
  gl.uniform2f(mouseLocation, mouseX, mouseY);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
  requestAnimationFrame(animate);
}

// 图片加载完成后启动动画
image.onload = () => {
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
  animate(0);
};

关键说明

  • object-fit: cover 实现:在片段着色器中通过计算图片与容器的宽高比,动态调整纹理坐标,确保图片按cover规则填充容器,不会拉伸变形。
  • 悬浮变形效果:通过计算鼠标与纹理坐标的距离,生成动态偏移量,实现类似流动的变形效果,可调整smoothstep参数和偏移量数值来控制效果强度。
  • 响应式适配:监听窗口resize事件,实时调整画布尺寸和WebGL视口,保证容器大小变化时效果正常。

内容的提问来源于stack exchange,提问作者TOCJ

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 04:34:56