HTML+JS+SVG实现HMM模型动画演示

简述

使用html实现HMM模型的动画演示,直接存储为txt文件更改后辍名为html即可运行,该效果只展示了状态转移。

使用参数如下

状态:[“下雨”,“晴天”]
观察结果:[“步行”、“购物”、“清洁”]

转移概率:
“下雨”:{“下雨”:0.7,“晴天”:0.3}
“晴天”:{“下雨”:0.4,“晴天”:0.6}

发射概率:
“雨天”:{“步行”:0.1,“商店”:0.4,“清洁”:0.5}
“晴天”:{“步行”:0.6,“购物”:0.3,“清洁”:0.1}

可视化解释

红点所在为当前状态,箭头为转移方向,灰色为前一个状态。

实现效果

Rainy转移状态Rainy:

Rainy转移状态Rainy

Rainy状态转移Sunny:

Rainy状态转移Sunny

完整代码

<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>HMM算法可视化</title>
    <style>
        .container {
            width: 800px;
            margin: 20px auto;
            text-align: center;
        }
        svg {
            border: 1px solid #ccc;
        }
        button {
            padding: 10px 20px;
            margin-top: 20px;
            cursor: pointer;
        }
        .state-node {
            cursor: pointer;
            transition: fill 0.3s ease;
        }
        .status {
            font-size: 20px;
            margin-top: 20px;
            font-weight: bold;
        }
        .arrow {
            stroke: black;
            stroke-width: 2;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>HMM算法可视化</h1>
        <svg id="hmmCanvas" width="800" height="400"></svg>
        <button onclick="startAnimation()">开始动画</button>
        <button onclick="stopAnimation()">停止动画</button>
        <!-- 显示当前状态 -->
        <div class="status" id="currentStatus">当前状态: Rainy</div>
    </div>

    <script>
        const svg = document.getElementById('hmmCanvas');
        const statusDiv = document.getElementById('currentStatus'); // 获取状态显示区域
        let isAnimating = false;
        let currentState = 'Rainy'; // 初始状态
        const states = ['Rainy', 'Sunny'];
        let animationTimeouts = [];  // 存储所有setTimeout的ID,以便清除

        // HMM参数设置
        const hmm = {
            states: ['Rainy', 'Sunny'],
            observations: ['Walk', 'Shop', 'Clean'],
            transitionProb: {
                'Rainy': {'Rainy': 0.7, 'Sunny': 0.3},
                'Sunny': {'Rainy': 0.4, 'Sunny': 0.6}
            },
            emissionProb: {
                'Rainy': {'Walk': 0.1, 'Shop': 0.4, 'Clean': 0.5},
                'Sunny': {'Walk': 0.6, 'Shop': 0.3, 'Clean': 0.1}
            }
        };

        // 初始化SVG绘图
        function initializeSVG() {
            svg.innerHTML = '';
            hmm.states.forEach((state, i) => {
                const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
                g.setAttribute('transform', `translate(${200 + i * 300}, 200)`);

                const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
                circle.setAttribute('r', '40');
                circle.setAttribute('fill', state === 'Rainy' ? '#42a5f5' : '#ffd600');
                circle.classList.add('state-node');
                circle.setAttribute('data-state', state);

                const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
                text.setAttribute('text-anchor', 'middle');
                text.setAttribute('dy', '5');
                text.textContent = state;

                g.appendChild(circle);
                g.appendChild(text);
                svg.appendChild(g);
            });

            // 创建红色圆圈表示当前状态
            createCurrentStateIndicator();
        }

        // 创建一个红色圆圈表示当前状态
        function createCurrentStateIndicator() {
            const indicator = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
            indicator.setAttribute('r', '10');
            indicator.setAttribute('fill', 'red');
            indicator.setAttribute('class', 'current-state-indicator');
            svg.appendChild(indicator);
            updateCurrentStateIndicator();
        }

        // 更新当前状态的红色圆圈位置
        function updateCurrentStateIndicator() {
            const stateIndex = hmm.states.indexOf(currentState);
            const x = 200 + stateIndex * 300; // 计算红色圆圈X位置
            const y = 240; // 红色圆圈放在状态球下方
            const indicator = svg.querySelector('.current-state-indicator');
            if (indicator) {
                indicator.setAttribute('cx', x);
                indicator.setAttribute('cy', y);
            }
        }

        // 计算下一个状态
        function getNextState(current) {
            const rand = Math.random();
            const prob = hmm.transitionProb[current];
            return rand < prob['Rainy'] ? 'Rainy' : 'Sunny';
        }

        function calculateArrowPositions(fromState, toState) {
    		const fromIndex = hmm.states.indexOf(fromState);
    		const toIndex = hmm.states.indexOf(toState);
    
    		// 判断箭头方向,确保箭头的长度一致
    		if (fromIndex > toIndex) { 
        		// 如果箭头从右向左(从 Sunny 到 Rainy)
        		const fromX = 200 + fromIndex * 300 - 40;  // 半径偏移量,圆的右侧
	    		const fromY = 200;
	    		const toX = 200 + toIndex * 300 + 40;  // 半径偏移量,圆的左侧
	    		const toY = 200;
       		return {
            		fromX: fromX,
            		fromY: fromY,
            		toX: toX,
            		toY: toY,
		        };
		    } else {
		        // 如果箭头从左向右(从 Rainy 到 Sunny)
		        const fromX = 200 + fromIndex * 300 + 40;  // 半径偏移量,圆的右侧
	    		const fromY = 200;
	    		const toX = 200 + toIndex * 300 - 40;  // 半径偏移量,圆的左侧
	    		const toY = 200;
		        return {
		            fromX: fromX,
		            fromY: fromY,
		            toX: toX,
		            toY: toY,
		        };
		    }
		}

        // 状态转移动画
        function animateTransition(fromState, toState) {
            const fromNode = svg.querySelector(`circle[data-state='${fromState}']`);
            const toNode = svg.querySelector(`circle[data-state='${toState}']`);

            // 在状态转换之间添加箭头
            const { fromX, fromY, toX, toY } = calculateArrowPositions(fromState, toState);

            // 画箭头
            const arrow = document.createElementNS('http://www.w3.org/2000/svg', 'line');
            arrow.setAttribute('x1', fromX);
            arrow.setAttribute('y1', fromY);
            arrow.setAttribute('x2', toX);
            arrow.setAttribute('y2', toY);
            arrow.setAttribute('class', 'arrow');
            svg.appendChild(arrow);

            // 添加箭头尾部(三角形)指示方向
            const angle = Math.atan2(toY - fromY, toX - fromX);
            const arrowHeadSize = 10;
            const arrowHeadX = toX - arrowHeadSize * Math.cos(angle - Math.PI / 6);
            const arrowHeadY = toY - arrowHeadSize * Math.sin(angle - Math.PI / 6);

            const arrowHead = document.createElementNS('http://www.w3.org/2000/svg', 'polygon');
            arrowHead.setAttribute('points', `${toX},${toY} ${arrowHeadX},${arrowHeadY} ${toX - arrowHeadSize * Math.cos(angle + Math.PI / 6)},${toY - arrowHeadSize * Math.sin(angle + Math.PI / 6)}`);
            arrowHead.setAttribute('fill', 'black');
            svg.appendChild(arrowHead);

            if (fromNode) fromNode.setAttribute('fill', 'gray'); // 旧状态变灰
            setTimeout(() => {
                if (fromNode) fromNode.setAttribute('fill', fromState === 'Rainy' ? '#42a5f5' : '#ffd600');
                if (toNode) toNode.setAttribute('fill', toState === 'Rainy' ? '#42a5f5' : '#ffd600');
            }, 3500);

            // 移除箭头和箭头尾部
            setTimeout(() => {
                svg.removeChild(arrow);
                svg.removeChild(arrowHead);
            }, 3500);
        }

        // 生成观测
        function displayObservation(state) {
            const obsProb = hmm.emissionProb[state];
            const rand = Math.random();
            let chosenObs = 'Walk'; // 默认值

            let cumulative = 0;
            for (let obs in obsProb) {
                cumulative += obsProb[obs];
                if (rand < cumulative) {
                    chosenObs = obs;
                    break;
                }
            }

            // 获取对应状态节点的位置
            const stateNode = svg.querySelector(`circle[data-state='${state}']`);
            if (!stateNode) return;

            const obsText = document.createElementNS('http://www.w3.org/2000/svg', 'text');
            obsText.setAttribute('x', 350);
            obsText.setAttribute('y', 100);
            obsText.setAttribute('fill', '#2e7d32');
            obsText.textContent = chosenObs;
            obsText.setAttribute('opacity', 0);
            svg.appendChild(obsText);

            // 淡入效果
            obsText.style.transition = 'opacity 0.5s ease-in';
            setTimeout(() => obsText.setAttribute('opacity', 1), 100);

            // 2秒后移除
            setTimeout(() => svg.removeChild(obsText), 4000);
        }

        // 更新当前状态
        function updateCurrentStatus(state) {
            statusDiv.textContent = `当前状态: ${state}`; // 更新状态显示
            currentState = state;
            updateCurrentStateIndicator(); // 更新红色圆圈位置
        }

        // 启动动画
        function startAnimation() {
            if (isAnimating) return;
            isAnimating = true;

            function step() {
                const nextState = getNextState(currentState);
                animateTransition(currentState, nextState);
                displayObservation(nextState);
                updateCurrentStatus(nextState); // 更新当前状态显示
                currentState = nextState;

                if (isAnimating) {
                    const timeoutId = setTimeout(step, 4000); // 2秒后继续
                    animationTimeouts.push(timeoutId);  // 保存timeout ID
                }
            }

            step();
        }

        // 停止动画
        function stopAnimation() {
            isAnimating = false;

            // 清除所有未执行的 setTimeout
            animationTimeouts.forEach(timeoutId => clearTimeout(timeoutId));
            animationTimeouts = []; // 清空 timeout ID 列表
        }

        initializeSVG();
    </script>
</body>
</html>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值