js拖拽、碰撞与重力实现代码

2026-05-02 12:25:38 191
分类:canvas

Unity3D物理系统3大组件:碰撞体(Collider),触发器(Trigger),刚体(RigidBody)

今天做了个刚体(RigidBody),模拟重力、碰撞、弹跳。

html部分

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>js拖拽、碰撞与重力实现代码</title>
    <style>
        body {
            width: 100%;
            height: 100%;
        }

        .rigidBody {
            position: absolute;
            width: 50px;
            height: 50px;
            background-color: red;
        }
    </style>
    <script src="js/test1.js"></script>
</head>
<body>
<div id="div1"></div>
<div id="div2"></div>
<script>
    new RigidBody('div1', {useGravity: false});
    new RigidBody('div2');
</script>
</body>
</html>

js部分

//刚体
var RigidBody = function (element, options) {
    this.element = document.getElementById(element); //碰撞体元素

    var defaults = {
        canDrag: true,          //是否允许拖动
        useGravity: true,       //启用重力
        acc: 2,                 //加速度(这里使用的匀速加速度)
        bounce: 0.8,            //弹力系数,每次碰撞后减少运动(摩擦力未实现,暂时忽略摩擦力)
        // mass: 1,             //质量
        // drag: 1,             //阻力
        // angularDrag: 0.05,   //旋转阻力
    }
    this.options = Object.assign({}, defaults, options);

    this.timer = 0;     //计时器
    this.speedX = 0;    //运动位置
    this.speedY = 0;

    this.init();
}
RigidBody.prototype = {
    constructor: RigidBody,

    init: function () {
        var self = this;
        self.fallMotion();
        self.bintEvent();
    },

    //自由落体运动
    fallMotion: function () {
        var self = this;
        if (!self.options.useGravity) return false;
        var element = self.element;
        var clientWidth = document.documentElement.clientWidth,
            clientHeight = document.documentElement.clientHeight;
        var offsetWidth = element.offsetWidth,
            offsetHeight = element.offsetHeight;

        update();

        function update() {
            self.timer = requestAnimationFrame(update);

            self.speedY += self.options.acc;
            var t = element.offsetTop + self.speedY,
                l = element.offsetLeft + self.speedX;

            //当碰撞上下边界
            if (t >= clientHeight - offsetHeight) {
                self.speedY *= -1 * self.options.bounce;
                self.speedX *= self.options.bounce;
                t = clientHeight - offsetHeight;
            }
            else if (t <= 0) {
                self.speedY *= -1;
                self.speedX *= self.options.bounce;
                t = 0;
            }

            //当碰撞左右边界
            if (l >= clientWidth - offsetWidth) {
                self.speedX *= -1 * self.options.bounce;
                l = clientWidth - offsetWidth;
            }
            else if (l <= 0) {
                self.speedX *= -1 * self.options.bounce;
                l = 0;
            }

            if (Math.abs(self.speedX) < 1) {
                self.speedX = 0;
            }
            if (Math.abs(self.speedY) < 1) {
                self.speedY = 0;
            }

            if (self.speedX == 0 && self.speedY == 0 && t == clientHeight - offsetHeight) {
                window.cancelAnimationFrame(self.timer);
            }
            else {
                element.style.top = t + 'px';
                element.style.left = l + 'px';
            }
        }
    },

    bintEvent() {
        var self = this;
        if (self.options.canDrag) {
            self.dragEvent();
        }
    },

    //拖拽事件
    dragEvent() {
        var self = this;
        var disX, disY;
        var lastX = 0, lastY = 0;

        self.element.addEventListener('mousedown', onMouseDown, false);

        function onMouseDown(event) {
            event.preventDefault();
            window.cancelAnimationFrame(self.timer);

            disX = event.clientX - self.element.offsetLeft;
            disY = event.clientY - self.element.offsetTop;

            document.addEventListener('mousemove', onMouseMove);
            document.addEventListener('mouseup', onMouseUp);
        }

        function onMouseMove(event) {
            var l = event.clientX - disX;
            var t = event.clientY - disY;
            self.element.style.left = l + 'px';

            self.element.style.top = t + 'px';
            self.speedX = l - lastX;
            self.speedY = t - lastY;
            lastX = l;
            lastY = t;
        }

        function onMouseUp() {
            self.fallMotion();
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
        }
    }
}

2018/07/20做了一次改进,用canvas来实现。

这次实现了两个球的独立运动,互不干扰。但是小球之间的碰撞还未实现。

html部分

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>CANVAS</title>
    <style>
 html, body {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
            overflow: hidden;
        }

        #container {
            width: 100%;
            height: 100%;
        }
    </style>
    <script src="../js/libs/stats.min.js"></script>
    <script src="canvas.js"></script>
</head>
<body>
<div id="container"></div>

<script>
 var container = document.getElementById('container');
    var stats;
    var camera, scene, renderer;
    var raycaster, intersectObj;
    var mouse;
    var speedX = 0, speedY = 0;

    init();

    function init() {
        stats = new Stats();
        container.appendChild(stats.dom);
        //相机
 camera = new CANVAS.Camera(70, container.offsetWidth / container.offsetHeight, 1, 1000);

        //场景
 scene = new CANVAS.Scene();

        //渲染器
 renderer = new CANVAS.Renderer();
        renderer.setSize(container.offsetWidth, container.offsetHeight);

        container.appendChild(renderer.canvas);

        //几何体
 var rect1 = new CANVAS.Rect(10, 10);
        rect1.pattern = 'stroke';
        var rect2 = new CANVAS.Rect(100, 10, 50, 50);
        rect2.canDrag = true;
        rect2.isRigidBody = true;
        rect2.useGravity = true;

        var circle1 = new CANVAS.Circle(35, 100);
        circle1.pattern = 'stroke';
        var circle2 = new CANVAS.Circle(125, 100);
        circle2.x = 100;
        circle2.y = 300;
        circle2.canDrag = true;
        circle2.isRigidBody = true;
        circle2.useGravity = true;

        scene.add(rect1);
        scene.add(rect2);
        scene.add(circle1);
        scene.add(circle2);

        raycaster = new CANVAS.Raycaster();
        mouse = new CANVAS.Vector2();

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

        update();
        dragEvent();
    }

    function update() {
        requestAnimationFrame(update);
        renderer.render(scene, camera);
        stats.update();
    }

    function dragEvent() {
        var disX, disY;
        var lastX = 0, lastY = 0;

        container.addEventListener('mousedown', onMouseDown, false);

        //鼠标按下
 function onMouseDown(event) {
            event.preventDefault();

            mouse.x = event.pageX;
            mouse.y = event.pageY;

            raycaster.setFromCamera(mouse, camera);
            intersectObj = raycaster.intersectObjects(scene.children);

            if (intersectObj && intersectObj.canDrag) {
                intersectObj.updateZIndex();
                intersectObj.useGravity = false;
                disX = event.pageX - intersectObj.x;
                disY = event.pageY - intersectObj.y;

                document.addEventListener('mousemove', onMouseMove);
                document.addEventListener('mouseup', onMouseUp);
            }
        }

        function onMouseMove(event) {
            var l = event.clientX - disX;
            var t = event.clientY - disY;

            intersectObj.x = l;
            intersectObj.y = t;

            intersectObj.speedVector.x = l - lastX;
            intersectObj.speedVector.y = t - lastY;
            lastX = l;
            lastY = t;
        }

        function onMouseUp() {
            intersectObj.useGravity = true;
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
        }
    }

    //自适应
 function onWindowResize() {
        renderer.setSize(window.innerWidth, window.innerHeight);
    }

</script>
</body>
</html>

js部分

"use strict";
(function () {
    Array.prototype.remove = function (val) {
        let index = this.indexOf(val);
        if (index > -1) {
            this.splice(index, 1);
        }
    }

    let utils = {
        /*
        * 对象克隆(深拷贝)
        */
        clone: function (myObj) {
            if (typeof (myObj) !== 'object' || myObj === null) return myObj;
            let newObj = {};
            for (let i in myObj) {
                newObj[i] = utils.clone(myObj[i]);
            }
            return newObj;
        },

        /*
        * 对象合并(多级合并,浅拷贝)
        * @param obj1 被合并对象
        * @param obj2 要合并对象
        * @returns object 合并后的对象
        */
        assign: function (obj1, obj2) {
            Object.keys(obj1).forEach((key) => {
                if (typeof obj1[key] === 'object') {
                    obj2[key] = utils.assign(obj1[key], obj2[key]);
                }
            });
            return Object.assign(obj1, obj2);
        },

        ascSort: function (a, b, sortName) {
            return a[sortName] - b[sortName];
        },

        descSort: function (a, b, sortName) {
            return b[sortName] - a[sortName]
        },
    };

    const _Math = {
        DEG2RAD: Math.PI / 180,
        RAD2DEG: 180 / Math.PI,

        generateUUID: (function () {
            var lut = [];
            for (var i = 0; i < 256; i++) {
                lut[i] = (i < 16 ? '0' : '') + (i).toString(16);
            }
            return function generateUUID() {
                var d0 = Math.random() * 0xffffffff | 0;
                var d1 = Math.random() * 0xffffffff | 0;
                var d2 = Math.random() * 0xffffffff | 0;
                var d3 = Math.random() * 0xffffffff | 0;
                var uuid = lut[d0 & 0xff] + lut[d0 >> 8 & 0xff] + lut[d0 >> 16 & 0xff] + lut[d0 >> 24 & 0xff] + '-' +
                    lut[d1 & 0xff] + lut[d1 >> 8 & 0xff] + '-' + lut[d1 >> 16 & 0x0f | 0x40] + lut[d1 >> 24 & 0xff] + '-' +
                    lut[d2 & 0x3f | 0x80] + lut[d2 >> 8 & 0xff] + '-' + lut[d2 >> 16 & 0xff] + lut[d2 >> 24 & 0xff] +
                    lut[d3 & 0xff] + lut[d3 >> 8 & 0xff] + lut[d3 >> 16 & 0xff] + lut[d3 >> 24 & 0xff];
                // .toUpperCase() here flattens concatenated strings to save heap memory space.
                return uuid.toUpperCase();
            };
        })(),

        //角度转弧度
        degToRad: function (degrees) {
            return degrees * _Math.DEG2RAD;
        },

        //弧度转角度
        radToDeg: function (radians) {
            return radians * _Math.RAD2DEG;
        },

        //获取两点的距离
        getDistance: function (p1, p2) {
            return Math.sqrt(Math.pow(Math.abs(p1.x - p2.x), 2) + Math.pow(Math.abs(p2.y - p2.y), 2));
        }
    }

    //2维向量
    class Vector2 {
        constructor(x, y) {
            this.x = x || 0;
            this.y = y || 0;
        }
    }

    //3维向量
    class Vector3 {
        constructor(x, y, z) {
            this.x = x || 0;
            this.y = y || 0;
            this.z = z || 0;
        }
    }

    //欧拉角
    class Euler {
        constructor(x, y, z, order) {
            this.defaultOrder = 'XYZ';
            this.rotationOrder = ['XYZ', 'YZX', 'ZXY', 'XZY', 'YXZ', 'ZYX'];
            this.x = x || 0;
            this.y = y || 0;
            this.z = z || 0;
            this.order = order || this.defaultOrder;
        }
    }

    //四元素
    class Quaternion {
        constructor(x, y, z, w) {
            this.x = x || 0;
            this.y = y || 0;
            this.z = z || 0;
            this.w = w || 1;
        }

        //欧拉角转四元素
        setFromEuler(euler, update) {
            if (!euler) {
                throw new Error('setFromEuler() now expects an Euler rotation rather than a Vector3 and order.');
            }

            var x = euler.x, y = euler.y, z = euler.z, order = euler._order;

            var cos = Math.cos;
            var sin = Math.sin;

            var c1 = cos(x / 2);
            var c2 = cos(y / 2);
            var c3 = cos(z / 2);

            var s1 = sin(x / 2);
            var s2 = sin(y / 2);
            var s3 = sin(z / 2);

            if (order === 'XYZ') {
                this.x = s1 * c2 * c3 + c1 * s2 * s3;
                this.y = c1 * s2 * c3 - s1 * c2 * s3;
                this.z = c1 * c2 * s3 + s1 * s2 * c3;
                this.w = c1 * c2 * c3 - s1 * s2 * s3;
            } else if (order === 'YXZ') {
                this.x = s1 * c2 * c3 + c1 * s2 * s3;
                this.y = c1 * s2 * c3 - s1 * c2 * s3;
                this.z = c1 * c2 * s3 - s1 * s2 * c3;
                this.w = c1 * c2 * c3 + s1 * s2 * s3;
            } else if (order === 'ZXY') {
                this.x = s1 * c2 * c3 - c1 * s2 * s3;
                this.y = c1 * s2 * c3 + s1 * c2 * s3;
                this.z = c1 * c2 * s3 + s1 * s2 * c3;
                this.w = c1 * c2 * c3 - s1 * s2 * s3;
            } else if (order === 'ZYX') {
                this.x = s1 * c2 * c3 - c1 * s2 * s3;
                this.y = c1 * s2 * c3 + s1 * c2 * s3;
                this.z = c1 * c2 * s3 - s1 * s2 * c3;
                this.w = c1 * c2 * c3 + s1 * s2 * s3;
            } else if (order === 'YZX') {
                this.x = s1 * c2 * c3 + c1 * s2 * s3;
                this.y = c1 * s2 * c3 + s1 * c2 * s3;
                this.z = c1 * c2 * s3 - s1 * s2 * c3;
                this.w = c1 * c2 * c3 - s1 * s2 * s3;
            } else if (order === 'XZY') {
                this.x = s1 * c2 * c3 - c1 * s2 * s3;
                this.y = c1 * s2 * c3 - s1 * c2 * s3;
                this.z = c1 * c2 * s3 + s1 * s2 * c3;
                this.w = c1 * c2 * c3 + s1 * s2 * s3;
            }

            if (update) this.onChangeCallback();

            return this;
        }
    }

    //颜色
    class Color {
        constructor(r, g, b, a) {
            this.r = r || 0;
            this.g = g || 0;
            this.b = b || 0;
            this.a = a || 1;
        }
    }

    //颜色(HSL表现方式)
    class HSL {
        /*
        * Hue(色调)。0(或360)表示红色,120表示绿色,240表示蓝色,也可取其他数值来指定颜色。取值为:0 - 360
        * Saturation(饱和度)。取值为:0.0% - 100.0%
        * Lightness(亮度)。取值为:0.0% - 100.0%
        */
        constructor(h, s, l) {
            this.h = h || 0;
            this.s = s || '50%';
            this.l = l || '50%';
        }
    }

    let canvas = undefined;   //canvas画布
    let ctx = undefined;
    let sceneObj = [];
    let elementIndex = 0;     //创建序号
    let elementzIndex = 0;    //层级

    //射线
    class Raycaster {
        constructor() {
            this.interactionObject = [];
            this.coords = undefined;
            this.camera = undefined;
        }

        setFromCamera(coords, camera) {
            this.coords = coords;
            this.camera = camera;
        }

        intersectObject(object) {
            let self = this;
            self.interactionObject = [];
            self.interactionObject.push(object);
            return self.raycast(self.coords, self.camera);
        }

        intersectObjects(objects) {
            let self = this;
            self.interactionObject = [];
            if (Array.isArray(objects) === false) {
                console.error('CANVAS.Raycaster.intersectObjects: objects is not an Array.')
            }
            else {
                objects.forEach(function (value) {
                    self.interactionObject.push(value);
                });
            }

            return self.raycast(self.coords, self.camera);
        }

        raycast(coords) {
            var self = this;

            let point = {
                x: coords.x - canvas.getBoundingClientRect().left,
                y: coords.y - canvas.getBoundingClientRect().top
            }

            let objects = [];
            for (let obj of self.interactionObject) {
                if (obj.type === 'rect') {
                    if (point.x > obj.x && point.x < (obj.x + obj.width) && point.y > obj.y && point.y < (obj.y + obj.height)) {
                        objects.push(obj);
                    }
                }
                else if (obj.type === 'circle') {
                    if (obj.r > _Math.getDistance(obj, point)) {
                        objects.push(obj);
                    }
                }
            }
            objects.sort(function (a, b) {
                return utils.descSort(a, b, 'zIndex');
            });
            if (objects.length > 0) {
                return objects[0];
            }
        }
    }

    //刚体
    class RigidBody {
        constructor() {
            this.canDrag = false;       //是否能拖拽
            this.isRigidBody = false;   //是否是刚体
            this.useGravity = false;    //使用重力
            this.acc = 1;               //加速度
            this.bounce = 0.8;          //弹性系数

            this.speedVector = new Vector2();//速度向量
        }

        //自由落体运动
        fallMotion() {
            var self = this;
            if (!self.isRigidBody || !self.useGravity) {
                return;
            }
            var clientWidth = canvas.width,
                clientHeight = canvas.height;
            var offsetWidth = self.width,
                offsetHeight = self.height;
            if (self.type === 'circle') {
                offsetWidth = self.r;
                offsetHeight = self.r;
            }

            self.speedVector.y += self.acc * 2;
            var y = self.y + self.speedVector.y,
                x = self.x + self.speedVector.x;

            //当碰撞上下边界
            if (y >= clientHeight - offsetHeight) {
                self.speedVector.y *= -1 * self.bounce;
                self.speedVector.x *= self.bounce;
                y = clientHeight - offsetHeight;
            }
            else if (y <= 0) {
                self.speedVector.y *= -1;
                self.speedVector.x *= self.bounce;
                y = 0;
            }

            //当碰撞左右边界
            if (x >= clientWidth - offsetWidth) {
                self.speedVector.x *= -1 * self.bounce;
                x = clientWidth - offsetWidth;
            }
            else if (x <= 0) {
                self.speedVector.x *= -1 * self.bounce;
                x = 0;
            }

            for (let i in sceneObj) {
                if (sceneObj[i].isRigidBody && self.uuid != sceneObj[i].uuid) {

                }
            }

            if (Math.abs(self.speedVector.x) < 1) {
                self.speedVector.x = 0;
            }
            if (Math.abs(self.speedVector.y) < 1) {
                self.speedVector.y = 0;
            }

            self.x = x;
            self.y = y;
        }
    }

    //平面几何
    class PlaneGeometry extends RigidBody {
        constructor() {
            super();
            elementIndex++;
            elementzIndex++;

            this.uuid = _Math.generateUUID();
            this.index = elementIndex;
            this.zIndex = elementzIndex;
            this.type = undefined;  //几何类型
            this.pattern = 'fill';  //绘制方式(stroke:线条,fill:填充)
            this._color = undefined;
            this.needUpdate = true;
        }

        setColor(color) {
            if (color instanceof Color) {
                this._color = 'rgba(' + color.r + ',' + color.g + ',' + color.b + ',' + color.a + ')';
            }
            else if (color instanceof HSL) {
                this._color = 'hsl(' + color.h + ', ' + color.s + ',' + color.l + ')';
            }
            else {
                this._color = color;
            }
        }

        updateZIndex() {
            elementzIndex++;
            this.zIndex = elementzIndex;
            sceneObj.sort(function (a, b) {
                return utils.ascSort(a, b, 'zIndex');
            })
        }

        update() {
            var self = this;
            if (self.needUpdate) {
                self.fallMotion();
            }
            self.draw();
        }
    }

    //矩形
    class Rect extends PlaneGeometry {
        constructor(x, y, width, height, color) {
            super();
            this.type = 'rect';
            this.x = x || 0;
            this.y = y || 0;
            this.width = width || 50;
            this.height = width || 50;
            this.color = color || new Color(Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255), 1);
        }

        draw() {
            if (this.pattern === 'fill') {
                ctx.beginPath();
                this.setColor(this.color);
                ctx.fillStyle = this._color;
                ctx.rect(this.x, this.y, this.width, this.height);
                ctx.fill();
            }
            else if (this.pattern === 'stroke') {
                ctx.beginPath();
                this.setColor(this.color);
                ctx.strokeStyle = this._color;
                ctx.rect(this.x, this.y, this.width, this.height);
                ctx.stroke();
            }
        }
    }

    //圆形
    class Circle extends PlaneGeometry {
        constructor(x, y, r, color) {
            super();
            this.type = 'circle';
            this.x = x || 0;
            this.y = y || 0;
            this.r = r || 25;
            this.color = color || new Color(Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255), 1);
        }

        draw() {
            if (this.pattern === 'fill') {
                ctx.beginPath();
                this.setColor(this.color);
                ctx.fillStyle = this._color;
                ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
                ctx.fill();
            }
            else if (this.pattern === 'stroke') {
                ctx.beginPath();
                this.setColor(this.color);
                ctx.strokeStyle = this._color;
                ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI);
                ctx.stroke();
            }
        }
    }

    class Camera {
        constructor(fov, aspect, near, far) {
            this.position = new Vector3();
            this.rotation = new Quaternion();
            this.scale = new Vector3(1, 1, 1);
            this.fov = fov || 70;
            this.aspect = aspect || 1;
            this.near = near || 1;
            this.far = far || 1000;
        }
    }

    //场景
    class Scene {
        constructor() {
            this.children = [];
            sceneObj = this.children;
        }

        add(obj) {
            this.children.push(obj);
            obj.parent = this;
        }

        remove(obj) {
            this.children.remove(obj);
            obj.parent = null;
        }
    }

    //渲染器
    class Renderer {
        constructor() {
            canvas = document.createElement('canvas');
            ctx = canvas.getContext('2d');
            this.fillStyle = '#ffffff';
            this.canvas = canvas;
        }

        //设置背景色
        setClearColor(color) {
            this.fillStyle = color;
        }

        //设置尺寸
        setSize(width, height) {
            canvas.width = width;
            canvas.height = height;
        }

        //渲染场景
        render(scene, camera) {
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            ctx.fillStyle = this.fillStyle;
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            scene.children.forEach(function (value) {
                value.update();
            });
        }
    }

    let CANVAS = {
        utils: utils,
        Math: _Math,
        Vector2: Vector2,
        Vector3: Vector3,
        Euler: Euler,
        Quaternion: Quaternion,
        Color: Color,
        HSL: HSL,
        Raycaster: Raycaster,
        Rect: Rect,
        Circle: Circle,
        Camera: Camera,
        Scene: Scene,
        Renderer: Renderer,
    };
    window.CANVAS = CANVAS;
})();