Matilda
В html блок в Zero

<link rel='stylesheet' href='https://cdn.jsdelivr.net/gh/alphardex/aqua.css/dist/aqua.min.css'>

<div class="relative w-screen h-screen">
  <div class="bouncy-balloon w-full h-full bg-black bg-white-grad-1 overflow-hidden"></div>
</div>

В T123 ниже

<script id="rendered-js" type="module">
import * as THREE from "https://cdn.skypack.dev/three@0.124.0";
import ky from "https://cdn.skypack.dev/kyouka@1.2.5";
import { OrbitControls } from "https://cdn.skypack.dev/three@0.124.0/examples/jsm/controls/OrbitControls";
import Stats from "https://cdn.skypack.dev/three@0.124.0/examples/jsm/libs/stats.module";
import * as CANNON from "https://cdn.skypack.dev/cannon-es@0.18.0";
import { EffectComposer, RenderPass, NormalPass, SSAOEffect, EffectPass } from "https://cdn.skypack.dev/postprocessing@6.22.2";
const calcAspect = (el) => el.clientWidth / el.clientHeight;
const getNormalizedMousePos = (e) => {
    return {
        x: (e.clientX / window.innerWidth) * 2 - 1,
        y: -(e.clientY / window.innerHeight) * 2 + 1
    };
};
class MouseTracker {
    constructor() {
        this.mousePos = new THREE.Vector2(0, 0);
        this.mouseSpeed = 0;
    }
    // 追踪鼠标位置
    trackMousePos() {
        window.addEventListener("mousemove", (e) => {
            this.setMousePos(e);
        });
        window.addEventListener("touchstart", (e) => {
            this.setMousePos(e.touches[0]);
        }, { passive: false });
        window.addEventListener("touchmove", (e) => {
            this.setMousePos(e.touches[0]);
        });
    }
    // 设置鼠标位置
    setMousePos(e) {
        const { x, y } = getNormalizedMousePos(e);
        this.mousePos.x = x;
        this.mousePos.y = y;
    }
    // 追踪鼠标速度
    trackMouseSpeed() {
        // https://stackoverflow.com/questions/6417036/track-mouse-speed-with-js
        let lastMouseX = -1;
        let lastMouseY = -1;
        let mouseSpeed = 0;
        window.addEventListener("mousemove", (e) => {
            const mousex = e.pageX;
            const mousey = e.pageY;
            if (lastMouseX > -1) {
                mouseSpeed = Math.max(Math.abs(mousex - lastMouseX), Math.abs(mousey - lastMouseY));
                this.mouseSpeed = mouseSpeed / 100;
            }
            lastMouseX = mousex;
            lastMouseY = mousey;
        });
        document.addEventListener("mouseleave", () => {
            this.mouseSpeed = 0;
        });
    }
}
// 三维点
class Point {
    constructor(p) {
        this.x = p.x;
        this.y = p.y;
        this.z = p.z;
    }
}
// 数组转化为点
const array2Point = (arr) => new Point({ x: arr[0], y: arr[1], z: arr[2] });
// 点转化为数组
const point2Array = (point) => [point.x, point.y, point.z];
// 多个数组转化为多个点
const arrays2Point = (arrs) => arrs.map((item) => array2Point(item));
// 点转化为Three.js的向量
const point2ThreeVector = (point) => new THREE.Vector3(point.x, point.y, point.z);
// 点转化为Cannon.js的向量
const point2CannonVec = (point) => new CANNON.Vec3(point.x, point.y, point.z);
// 点转化为Three.js的Euler
const point2ThreeEuler = (point) => new THREE.Euler(point.x, point.y, point.z);
// 获取viewport
const getViewport = (camera) => {
    const position = new THREE.Vector3();
    const target = new THREE.Vector3();
    const distance = camera.getWorldPosition(position).distanceTo(target);
    const fov = (camera.fov * Math.PI) / 180; // convert vertical fov to radians
    const h = 2 * Math.tan(fov / 2) * distance; // visible height
    const w = h * (window.innerWidth / window.innerHeight);
    const viewport = { width: w, height: h };
    return viewport;
};
class MeshPhysicsObject {
    constructor(mesh, body, copyPosition = true, copyQuaternion = true) {
        this.mesh = mesh;
        this.body = body;
        this.copyPosition = copyPosition;
        this.copyQuaternion = copyQuaternion;
    }
}
class Base {
    constructor(sel, debug = false) {
        this.debug = debug;
        this.container = document.querySelector(sel);
        this.perspectiveCameraParams = {
            fov: 75,
            near: 0.1,
            far: 100
        };
        this.orthographicCameraParams = {
            zoom: 2,
            near: -100,
            far: 1000
        };
        this.cameraPosition = new THREE.Vector3(0, 3, 10);
        this.lookAtPosition = new THREE.Vector3(0, 0, 0);
        this.rendererParams = {
            alpha: true,
            antialias: true
        };
        this.mouseTracker = new MouseTracker();
    }
    // 初始化
    init() {
        this.createScene();
        this.createPerspectiveCamera();
        this.createRenderer();
        this.createMesh({});
        this.createLight();
        this.createOrbitControls();
        this.createDebugUI();
        this.addListeners();
        this.setLoop();
    }
    // 创建场景
    createScene() {
        const scene = new THREE.Scene();
        this.scene = scene;
    }
    // 创建透视相机
    createPerspectiveCamera() {
        const { perspectiveCameraParams, cameraPosition, lookAtPosition } = this;
        const { fov, near, far } = perspectiveCameraParams;
        const aspect = calcAspect(this.container);
        const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
        camera.position.copy(cameraPosition);
        camera.lookAt(lookAtPosition);
        this.camera = camera;
    }
    // 创建正交相机
    createOrthographicCamera() {
        const { orthographicCameraParams, cameraPosition, lookAtPosition } = this;
        const { left, right, top, bottom, near, far } = orthographicCameraParams;
        const camera = new THREE.OrthographicCamera(left, right, top, bottom, near, far);
        camera.position.copy(cameraPosition);
        camera.lookAt(lookAtPosition);
        this.camera = camera;
    }
    // 更新正交相机参数
    updateOrthographicCameraParams() {
        const { container } = this;
        const { zoom, near, far } = this.orthographicCameraParams;
        const aspect = calcAspect(container);
        this.orthographicCameraParams = {
            left: -zoom * aspect,
            right: zoom * aspect,
            top: zoom,
            bottom: -zoom,
            near,
            far,
            zoom
        };
    }
    // 创建渲染
    createRenderer() {
        const { rendererParams } = this;
        const renderer = new THREE.WebGLRenderer(rendererParams);
        renderer.setClearColor(0x000000, 0);
        this.container.appendChild(renderer.domElement);
        this.renderer = renderer;
        this.resizeRendererToDisplaySize();
    }
    // 调整渲染器尺寸
    resizeRendererToDisplaySize() {
        const { renderer } = this;
        renderer.setSize(this.container.clientWidth, this.container.clientHeight);
        renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    }
    // 创建网格
    createMesh(meshObject, container = this.scene) {
        const { geometry = new THREE.BoxGeometry(1, 1, 1), material = new THREE.MeshStandardMaterial({
            color: new THREE.Color("#d9dfc8")
        }), position = new THREE.Vector3(0, 0, 0) } = meshObject;
        const mesh = new THREE.Mesh(geometry, material);
        mesh.position.copy(position);
        container.add(mesh);
        return mesh;
    }
    // 创建光源
    createLight() {
        const dirLight = new THREE.DirectionalLight(new THREE.Color("#ffffff"), 0.5);
        dirLight.position.set(0, 50, 0);
        this.scene.add(dirLight);
        const ambiLight = new THREE.AmbientLight(new THREE.Color("#ffffff"), 0.4);
        this.scene.add(ambiLight);
    }
    // 创建轨道控制
    createOrbitControls() {
        const controls = new OrbitControls(this.camera, this.renderer.domElement);
        const { lookAtPosition } = this;
        controls.target.copy(lookAtPosition);
        controls.update();
        this.controls = controls;
    }
    // 创建调试UI
    createDebugUI() {
        const axisHelper = new THREE.AxesHelper();
        this.scene.add(axisHelper);
        const stats = Stats();
        this.container.appendChild(stats.dom);
        this.stats = stats;
    }
    // 监听事件
    addListeners() {
        this.onResize();
    }
    // 监听画面缩放
    onResize() {
        window.addEventListener("resize", (e) => {
            const aspect = calcAspect(this.container);
            const camera = this.camera;
            camera.aspect = aspect;
            camera.updateProjectionMatrix();
            this.resizeRendererToDisplaySize();
            if (this.shaderMaterial) {
                this.shaderMaterial.uniforms.uResolution.value.x = window.innerWidth;
                this.shaderMaterial.uniforms.uResolution.value.y = window.innerHeight;
            }
        });
    }
    // 动画
    update() {
        console.log("animation");
    }
    // 渲染
    setLoop() {
        this.renderer.setAnimationLoop(() => {
            this.update();
            if (this.controls) {
                this.controls.update();
            }
            if (this.stats) {
                this.stats.update();
            }
            if (this.composer) {
                this.composer.render();
            }
            else {
                this.renderer.render(this.scene, this.camera);
            }
        });
    }
}
class PhysicsBase extends Base {
    constructor(sel, debug = false) {
        super(sel, debug);
        this.gravity = new CANNON.Vec3(0, -9.82, 0);
        this.meshPhysicsObjs = [];
    }
    // 创建物理世界
    createWorld() {
        const { gravity } = this;
        const world = new CANNON.World();
        world.gravity.copy(gravity);
        this.world = world;
    }
    // 创建物理物体
    createBody(shape, body, bodyOffset = new CANNON.Vec3(0, 0, 0), orientation = new CANNON.Quaternion(0, 0, 0)) {
        body.addShape(shape, bodyOffset, orientation);
        this.world.addBody(body);
        return body;
    }
    // 动画
    update() {
        this.sync();
        this.world.step(1 / 60);
    }
    // 同步物理和渲染
    sync() {
        this.meshPhysicsObjs.forEach((obj) => {
            const { mesh, body, copyPosition, copyQuaternion } = obj;
            if (copyPosition) {
                mesh.position.copy(body.position);
            }
            if (copyQuaternion) {
                mesh.quaternion.copy(body.quaternion);
            }
        });
    }
}
class BouncyBalloon extends PhysicsBase {
    constructor(sel, debug) {
        super(sel, debug);
        this.perspectiveCameraParams = {
            fov: 35,
            near: 10,
            far: 40
        }; // 透视相机相关参数
        this.cameraPosition = new THREE.Vector3(0, 0, 20); // 相机位置
        this.gravity = new CANNON.Vec3(0, 0, 0); // 重力
        this.ballMat = null; // 球的材质
        this.balls = []; // 一堆小球
        this.planes = []; // 4个平面隔板
        this.mouseFollowBall = null; // 跟随鼠标的大球
        this.params = {
            ballColor: "#002f93" // 球的颜色
        };
    }
    // 初始化
    init() {
        this.createWorld();
        this.createScene();
        this.createPerspectiveCamera();
        this.viewport = getViewport(this.camera);
        this.createRenderer();
        this.changeRendererParams();
        this.createBallMaterial();
        this.createBalls();
        this.createFourPlanes();
        this.addBallsDamping();
        this.createMouseFollowBall();
        this.hideSomeObjs();
        this.createLight();
        this.createPostprocessingEffect();
        this.mouseTracker.trackMousePos();
        this.addListeners();
        this.setLoop();
    }
    // 创建球材质
    createBallMaterial() {
        const { ballColor } = this.params;
        const ballMat = new THREE.MeshLambertMaterial({
            color: new THREE.Color("silver"),
            emissive: new THREE.Color(ballColor)
        });
        this.ballMat = ballMat;
    }
    // 创建球
    createBall({ position = new Point({ x: 0, y: 0, z: 0 }), scale = 1 }) {
        // 在three.js中创建渲染物体
        const geo = new THREE.SphereBufferGeometry(1, 64, 64);
        const mat = this.ballMat;
        const mesh = new THREE.Mesh(geo, mat);
        mesh.position.copy(point2ThreeVector(position));
        mesh.scale.copy(new THREE.Vector3(scale, scale, scale));
        this.scene.add(mesh);
        // 在cannon.js中创建物理物体
        const body = new CANNON.Body({
            mass: 1,
            shape: new CANNON.Box(new CANNON.Vec3(scale, scale, scale)),
            position: point2CannonVec(position)
        });
        this.world.addBody(body);
        // 将两物体的数据同步
        const obj = new MeshPhysicsObject(mesh, body);
        this.meshPhysicsObjs.push(obj);
        return obj;
    }
    // 创建一堆球
    createBalls(count = 64) {
        const balls = [...Array(count).keys()].map(() => {
            const scale = ky.randomNumberInRange(0.5, 1);
            const ball = this.createBall({
                scale
            });
            return ball;
        });
        this.balls = balls;
    }
    // 创建平面
    createPlane({ position = new Point({ x: 0, y: 0, z: 0 }), rotation = new Point({ x: 0, y: 0, z: 0 }) }) {
        // 在three.js中创建渲染物体
        const geo = new THREE.PlaneGeometry(1, 1);
        const mat = new THREE.MeshBasicMaterial({
            color: new THREE.Color("red"),
            side: THREE.DoubleSide
        });
        const mesh = new THREE.Mesh(geo, mat);
        mesh.position.copy(point2ThreeVector(position));
        mesh.rotation.copy(point2ThreeEuler(rotation));
        this.scene.add(mesh);
        // 在cannon.js中创建物理物体
        const body = new CANNON.Body({
            shape: new CANNON.Plane(),
            position: point2CannonVec(position),
            quaternion: new CANNON.Quaternion().setFromEuler(rotation.x, rotation.y, rotation.z)
        });
        this.world.addBody(body);
        // 将两物体的数据同步
        const obj = new MeshPhysicsObject(mesh, body);
        this.meshPhysicsObjs.push(obj);
        return obj;
    }
    // 创建4个平面隔板
    createFourPlanes() {
        const planePositions = arrays2Point([
            [0, 0, 0],
            [0, 0, 8],
            [0, -4, 0],
            [0, 4, 0]
        ]);
        const planeRotations = arrays2Point([
            [0, 0, 0],
            [0, ky.deg2rad(-180), 0],
            [ky.deg2rad(-90), 0, 0],
            [ky.deg2rad(90), 0, 0]
        ]);
        const planes = [];
        for (let i = 0; i < 4; i++) {
            const position = planePositions[i];
            const rotation = planeRotations[i];
            const plane = this.createPlane({ position, rotation });
            planes.push(plane);
        }
        this.planes = planes;
    }
    // 给球添加阻尼
    addBallsDamping() {
        this.balls.forEach((ball) => {
            ball.body.angularDamping = 0.2;
            ball.body.linearDamping = 0.95;
        });
    }
    // 动画
    update() {
        this.sync();
        this.world.step(1 / 60);
        this.applyForce2Balls();
        this.mouseBallFollow();
    }
    // 给球施加力
    applyForce2Balls() {
        this.balls.forEach((obj) => {
            const force = new THREE.Vector3()
                .copy(obj.body.position)
                .normalize()
                .multiplyScalar(-36);
            obj.body.applyForce(force);
        });
    }
    // 创建跟踪鼠标的球
    createMouseFollowBall(scale = 2) {
        // 在three.js中创建渲染物体
        const geo = new THREE.SphereBufferGeometry(1, 64, 64);
        const mat = this.ballMat;
        const mesh = new THREE.Mesh(geo, mat);
        mesh.scale.copy(new THREE.Vector3(scale, scale, scale));
        this.scene.add(mesh);
        // 在cannon.js中创建物理物体
        const body = new CANNON.Body({
            mass: 1,
            shape: new CANNON.Sphere(2),
            type: CANNON.Body.KINEMATIC
        });
        this.world.addBody(body);
        // 将两物体的数据同步
        const mouseFollowBall = new MeshPhysicsObject(mesh, body);
        this.meshPhysicsObjs.push(mouseFollowBall);
        this.mouseFollowBall = mouseFollowBall;
    }
    // 大球跟踪鼠标
    mouseBallFollow() {
        const mousePos = this.mouseTracker.mousePos;
        const x = (mousePos.x * this.viewport.width) / 2;
        const y = (mousePos.y * this.viewport.height) / 2;
        const pos = new CANNON.Vec3(x, y, 2.5);
        this.mouseFollowBall.body.position.copy(pos);
    }
    // 隐藏部分物体
    hideSomeObjs() {
        this.planes.forEach((plane) => (plane.mesh.visible = false));
        this.mouseFollowBall.mesh.visible = false;
    }
    // 更改渲染器参数
    changeRendererParams() {
        this.renderer.outputEncoding = THREE.sRGBEncoding;
        this.renderer.toneMapping = THREE.CineonToneMapping;
        this.renderer.toneMappingExposure = 1.5;
    }
    // 创建光源
    createLight() {
        const { ballColor } = this.params;
        // 环境光
        const ambiLight = new THREE.AmbientLight(new THREE.Color("white"), 0.75);
        this.scene.add(ambiLight);
        // 平行光
        const dirLight1 = new THREE.DirectionalLight(new THREE.Color("white"), 4);
        dirLight1.position.set(0, 5, -5);
        this.scene.add(dirLight1);
        // 聚光
        const spotLight = new THREE.SpotLight(new THREE.Color(ballColor));
        spotLight.position.set(20, 20, 25);
        this.scene.add(spotLight);
    }
    // 创建后期处理特效
    createPostprocessingEffect() {
        const { ballColor } = this.params;
        const composer = new EffectComposer(this.renderer);
        // 渲染通道
        const renderPass = new RenderPass(this.scene, this.camera);
        composer.addPass(renderPass);
        // 法线通道
        const normalPass = new NormalPass(this.scene, this.camera);
        composer.addPass(normalPass);
        // SSAO特效
        const ssaoConfig = {
            rangeThreshold: 0.5,
            rangeFalloff: 0.1,
            bias: 0.5
        };
        const ssaoEffect1 = new SSAOEffect(this.camera, normalPass.renderTarget.texture, Object.assign(Object.assign({}, ssaoConfig), { color: ballColor, samples: 9, radius: 30, intensity: 30 }));
        const ssaoEffect2 = new SSAOEffect(this.camera, normalPass.renderTarget.texture, Object.assign(Object.assign({}, ssaoConfig), { color: ballColor, samples: 18, radius: 5, intensity: 30 }));
        // 特效通道
        const effectPass = new EffectPass(this.camera, ssaoEffect1, ssaoEffect2);
        effectPass.renderToScreen = true;
        composer.addPass(effectPass);
        this.composer = composer;
    }
}
const start = () => {
    const bouncyBalloon = new BouncyBalloon(".bouncy-balloon", false);
    bouncyBalloon.init();
};
start();
//# sourceURL=pen.js
    </script>
В T123 ниже

<style>


:root {
  --white-grad-1: linear-gradient(to top, #e6e9f0 0%, #eef1f5 100%);
}

.bg-white-grad-1 {
  background: var(--white-grad-1);
}
</style>
Made on
Tilda