基于gsap的椭圆卫星拖拽

0 阅读2分钟

效果图

chrome-capture-2025-4-20.gif

核心代码

 const svg = document.querySelector('svg');
        const ellipseGroup = document.getElementById('ellipseGroup');
        const ellipse = document.getElementById('ellipse');
        const nodesGroup = document.getElementById('nodesGroup');
        const addNodeBtn = document.getElementById('addNode');
        const resetBtn = document.getElementById('resetNodes');

        // 获取输入元素
        const cxInput = document.getElementById('cx');
        const cyInput = document.getElementById('cy');
        const rxInput = document.getElementById('rx');
        const ryInput = document.getElementById('ry');
        const rotateInput = document.getElementById('rotate');

        // 初始化参数(从输入框获取初始值)
        let ellipseCX = parseInt(cxInput.value);
        let ellipseCY = parseInt(cyInput.value);
        let ellipseRX = parseInt(rxInput.value);
        let ellipseRY = parseInt(ryInput.value);
        let ellipseRotate = parseInt(rotateInput.value);

        let nodes = []; // 存储所有节点
        let draggingNode = null; // 当前拖拽的节点
        let initialAngle = 0; // 初始拖拽角度
        let startAngle = 0; // 起始角度

        // 初始设置椭圆旋转角度
        ellipseGroup.setAttribute('transform', `rotate(${ellipseRotate} ${ellipseCX} ${ellipseCY})`);

        // 输入变化时更新椭圆参数
        cxInput.addEventListener('input', updateEllipseParams);
        cyInput.addEventListener('input', updateEllipseParams);
        rxInput.addEventListener('input', updateEllipseParams);
        ryInput.addEventListener('input', updateEllipseParams);
        rotateInput.addEventListener('input', updateEllipseParams);

        // 初始化现有节点
        document.querySelectorAll('.node').forEach(node => {
            nodes.push(node);
            setupDraggable(node);
        });

        // 添加新节点
        addNodeBtn.addEventListener('click', () => {
            const count = nodes.length + 1;
            const currentYear = new Date().getFullYear() + count - 1;

            // 创建节点组
            const nodeGroup = document.createElementNS("http://www.w3.org/2000/svg", "g");
            nodeGroup.setAttribute("id", `node-group-${count}`);

            // 创建节点圆
            const node = document.createElementNS("http://www.w3.org/2000/svg", "circle");
            node.setAttribute("class", "node");
            node.setAttribute("r", 7);
            node.setAttribute("fill", "#3f6ffc");
            node.setAttribute("id", `node-${count}`);
            nodeGroup.appendChild(node);

            // 创建节点文本
            const nodeText = document.createElementNS("http://www.w3.org/2000/svg", "text");
            nodeText.setAttribute("class", "node-text");
            nodeText.textContent = currentYear;
            nodeGroup.appendChild(nodeText);

            // 添加到SVG
            nodesGroup.appendChild(nodeGroup);

            nodes.push(node);
            setupDraggable(node);
            updateNodePositions();
        });

        // 重置节点
        resetBtn.addEventListener('click', () => {
            // 移除新增的节点
            while (nodes.length > 1) {
                const node = nodes.pop();
                const nodeGroup = node.parentNode;
                nodesGroup.removeChild(nodeGroup);
            }
            updateNodePositions();
        });

        function updateEllipseParams() {
            // 从输入框获取最新值
            ellipseCX = parseInt(cxInput.value);
            ellipseCY = parseInt(cyInput.value);
            ellipseRX = parseInt(rxInput.value);
            ellipseRY = parseInt(ryInput.value);
            ellipseRotate = parseInt(rotateInput.value);

            // 更新椭圆元素
            gsap.to(ellipse, {
                duration: 0.3,
                attr: {
                    cx: ellipseCX,
                    cy: ellipseCY,
                    rx: ellipseRX,
                    ry: ellipseRY
                }
            });

            // 更新椭圆组的旋转
            gsap.to(ellipseGroup, {
                duration: 0.3,
                rotation: ellipseRotate,
                transformOrigin: `${ellipseCX}px ${ellipseCY}px`
            });

            // 重新计算节点位置
            updateNodePositions();
        }

        // 设置节点拖拽功能
        function setupDraggable(node) {
            node.addEventListener('mousedown', (e) => {
                e.stopPropagation();
                draggingNode = node;

                // 获取节点在未旋转坐标系中的位置
                const pt = svg.createSVGPoint();
                pt.x = node.cx.baseVal.value;
                pt.y = node.cy.baseVal.value;

                // 应用逆旋转矩阵
                const rotateRad = -ellipseRotate * Math.PI / 180;
                const cos = Math.cos(rotateRad);
                const sin = Math.sin(rotateRad);

                // 相对于椭圆中心的坐标
                const relX = pt.x - ellipseCX;
                const relY = pt.y - ellipseCY;

                // 逆旋转后的坐标
                const unrotatedX = relX * cos - relY * sin;
                const unrotatedY = relX * sin + relY * cos;

                // 计算未旋转坐标系中的角度
                initialAngle = Math.atan2(unrotatedY, unrotatedX);

                // 记录开始拖拽时的角度
                const mousePt = svg.createSVGPoint();
                mousePt.x = e.clientX;
                mousePt.y = e.clientY;
                const cursorPt = mousePt.matrixTransform(svg.getScreenCTM().inverse());

                // 鼠标相对于椭圆中心的坐标
                const mouseRelX = cursorPt.x - ellipseCX;
                const mouseRelY = cursorPt.y - ellipseCY;

                // 应用逆旋转
                const mouseUnrotatedX = mouseRelX * cos - mouseRelY * sin;
                const mouseUnrotatedY = mouseRelX * sin + mouseRelY * cos;

                startAngle = Math.atan2(mouseUnrotatedY, mouseUnrotatedX);

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

        function onMouseMove(e) {
            if (!draggingNode) return;

            const pt = svg.createSVGPoint();
            pt.x = e.clientX;
            pt.y = e.clientY;
            const cursorPt = pt.matrixTransform(svg.getScreenCTM().inverse());

            // 鼠标相对于椭圆中心的坐标
            const mouseRelX = cursorPt.x - ellipseCX;
            const mouseRelY = cursorPt.y - ellipseCY;

            // 应用逆旋转矩阵
            const rotateRad = -ellipseRotate * Math.PI / 180;
            const cos = Math.cos(rotateRad);
            const sin = Math.sin(rotateRad);

            // 逆旋转后的坐标
            const mouseUnrotatedX = mouseRelX * cos - mouseRelY * sin;
            const mouseUnrotatedY = mouseRelX * sin + mouseRelY * cos;

            // 计算鼠标当前角度(在未旋转坐标系中)
            const currentAngle = Math.atan2(mouseUnrotatedY, mouseUnrotatedX);

            // 计算角度差
            let angleDiff = currentAngle - startAngle;

            // 处理角度跳跃问题
            if (angleDiff > Math.PI) angleDiff -= 2 * Math.PI;
            if (angleDiff < -Math.PI) angleDiff += 2 * Math.PI;

            // 新的基准角度(在未旋转坐标系中)
            const newBaseAngle = initialAngle + angleDiff;

            // 更新所有节点位置
            updateNodePositions(newBaseAngle);
        }

        function onMouseUp() {
            draggingNode = null;
            document.removeEventListener('mousemove', onMouseMove);
            document.removeEventListener('mouseup', onMouseUp);
        }

        // 更新所有节点位置,使它们在椭圆上均匀分布
        function updateNodePositions(baseAngle = 0) {
            const count = nodes.length;
            if (count === 0) return;

            // 计算相邻节点之间的角度间隔(弧度)
            const angleStep = (Math.PI * 2) / count;

            // 转换为弧度
            const rotateRad = ellipseRotate * Math.PI / 180;
            const cos = Math.cos(rotateRad);
            const sin = Math.sin(rotateRad);

            nodes.forEach((node, index) => {
                // 计算当前节点的角度(在未旋转坐标系中)
                const angle = baseAngle + (angleStep * index);

                // 在未旋转坐标系中的位置
                const unrotatedX = ellipseRX * Math.cos(angle);
                const unrotatedY = ellipseRY * Math.sin(angle);

                // 应用旋转变换
                const rotatedX = unrotatedX * cos - unrotatedY * sin;
                const rotatedY = unrotatedX * sin + unrotatedY * cos;

                // 相对于椭圆中心的最终位置
                const x = ellipseCX + rotatedX;
                const y = ellipseCY + rotatedY;

                // 使用GSAP平滑移动到新位置
                gsap.to(node, {
                    duration: 0.1,
                    attr: {
                        cx: x,
                        cy: y
                    }
                });

                // 更新对应文本位置
                const nodeGroup = node.parentNode;
                const nodeText = nodeGroup.querySelector('text');
                if (nodeText) {
                    gsap.to(nodeText, {
                        duration: 0.1,
                        attr: {
                            x: x,
                            y: y
                        }
                    });
                }
            });
        }

完整代码

gitee.com/south-mount…