HTML SVG路径动画

<template>
    <!-- <div id="inputContainer" class="inputContainer">
        <input type="number" id="positionInput" value="1100" style="border: 1px solid black;font-size: 24px;"
            placeholder="输入水平位置" />
        <button class="styled-button" @click="method.refresh">机器人移动</button>
    </div> -->
    <div>
        <h2 style="text-align: center;display: inline;">{{ data.titleContent }}</h2>
        <span style="float: right;">{{ data.refreshDatetime }}</span>
        <span style="float: right;">
            <span class="legend" style="background-color:burlywood;"></span>
            <span>准备中</span>
            <span class="legend" style="background-color: blue;"></span>
            <span>请求上料</span>
            <span class="legend" style="background-color: green;"></span>
            <span>处理中</span>
            <span class="legend" style="background-color: red;"></span>
            <span>完成</span>
            <span>——</span>
        </span>
    </div>

    <div style="background-color: white;width: 1440px;height: 840px;position: relative;">
        <canvas id="pathCanvas" class="pathStyle"></canvas>
        <svg class="path" viewBox="0 0 1440 840">
            <path id="linePath" stroke="aqua" stroke-width="2" fill="none" />
            <image class="arrow" xlink:href="../../../assets/img/Rotor.png" transform="translate(-20,-20)">
                <animateMotion repeatCount="indefinite" dur="5s" keyPoints="0;1" keyTimes="0;1">
                    <mpath href="#linePath" />
                </animateMotion>
            </image>
        </svg>
        <img id="background" class="background" src="../../../assets/img/P6144BackImage.png" alt="背景" />
        <div class="grid-container">
            <div v-for="(row, rowIndex) in data.grid" :key="rowIndex" class="grid-cell">
                <div v-for="(cell, cellIndex) in row" :key="cellIndex" class="grid-cell"
                    @mouseover="method.showCoordinates(rowIndex, cellIndex)" @mouseleave="method.hideCoordinates"
                    :class="{
                        'preparing-status-cell': method.setPreparingStatus(rowIndex, cellIndex),
                        'request-loading-status-cell': method.setRequestLoadingStatus(rowIndex, cellIndex),
                        'processing-status-cell': method.setProcessingStatus(rowIndex, cellIndex),
                        'finished-status-cell': method.setFinishedStatus(rowIndex, cellIndex),
                    }">
                    <div class="container">
                        <div class="station-font">{{ method.getStationName(rowIndex, cellIndex) }}</div>
                    </div>
                </div>
            </div>
        </div>
        <div class="track"></div>
        <img id="kuka" class="kuka" src="../../../assets/img/kuka.png" alt="机器人" />
        <!-- 坐标显示元素 -->
        <div v-if="data.showCoordinates" class="coordinates-display">
            {{ data.currentCoordinates }}
        </div>

    </div>

</template>

<script lang="ts" setup>
import { ref, reactive, onMounted } from 'vue'
import * as signalR from '@microsoft/signalr';
import { P6144StationVO } from '../../../types/WCS/Status'

const SERVER_URL = `${import.meta.env.VITE_BASE_PATH}:${import.meta.env.VITE_SERVER_PORT}`

interface FromTo {
    From: string;
    To: string;
    Action: string;
}

interface Point {
    x: number;
    y: number;
}

interface StatusScreenVO {
    Type: string;
    Content: string;
}

interface PublishTaskContentVO {
    Title: string;
    AnimationX: number;
}

enum StationStatusEnum {
    Preparing,
    RequestLoading,
    Processing,
    Finished,
}

enum Direction {
    Right,
    Left,
    Down,
    Up
}

const data = reactive({
    refreshDatetime: '',
    titleContent: 'title',
    grid: ref(Array.from({ length: 24 }, () => Array(28))),
    positionInput: ref<HTMLInputElement | null>(null),
    imageElement: ref<HTMLImageElement | null>(null),
    connection: ref<signalR.HubConnection | null>(null),
    hubConnection: new signalR.HubConnectionBuilder()
        .withUrl(SERVER_URL + '/TaskAnimationHub')
        .configureLogging(signalR.LogLevel.Information)
        .withServerTimeout(3600 * 1 * 1000)
        .build(),
    stationName: ref<{ x: number; y: number, station_code: string, station_name: string }[]>([
        { x: 2, y: 2, station_code: 'OP010', station_name: '' },
    ]),
    preparingLocation: ref<{ x: number; y: number, station_name: string }[]>([
        { x: 2, y: 2, station_name: 'OP010' },
    ]),
    requestLoadingLocation: ref<{ x: number; y: number, station_name: string }[]>([
        { x: 2, y: 8, station_name: 'OP040' },
    ]),
    processingLocation: ref<{ x: number; y: number, station_name: string }[]>([
        { x: 12, y: 2, station_name: 'OP070' },
    ]),
    finishedLocation: ref<{ x: number; y: number, station_name: string }[]>([
        { x: 12, y: 8, station_name: 'OP100' },
    ]),
    showCoordinates: false,
    currentCoordinates: '',

    lineWidth: 4,
    lineColor: 'aqua',
    triangleLength: 12,
    spaceBetween: 40,
    animationInProgress: false,
})

// 定义方法对象
const method = reactive({
    refresh: () => {
        if (data.positionInput) {
            const position = parseInt(data.positionInput.value, 14)
            if (!isNaN(position) && data.imageElement) {
                data.imageElement.style.left = position + 'px'
            } else {
                alert('请输入一个有效的数字')
            }
        }
    },
    drawTriangle: (ctx: CanvasRenderingContext2D, point: Point, direction: Direction) => {
        const length = data.triangleLength;

        ctx.strokeStyle = data.lineColor;
        ctx.lineWidth = data.lineWidth;
        ctx.beginPath();

        switch (direction) {
            case Direction.Right:
                ctx.moveTo(point.y - length, point.x - length);
                ctx.lineTo(point.y, point.x);
                ctx.lineTo(point.y - length, point.x + length);
                break;
            case Direction.Left:
                ctx.moveTo(point.y + length, point.x - length);
                ctx.lineTo(point.y, point.x);
                ctx.lineTo(point.y + length, point.x + length);
                break;
            case Direction.Down:
                ctx.moveTo(point.y - length, point.x - length);
                ctx.lineTo(point.y, point.x);
                ctx.lineTo(point.y + length, point.x - length);
                break;
            case Direction.Up:
                ctx.moveTo(point.y + length, point.x + length);
                ctx.lineTo(point.y, point.x);
                ctx.lineTo(point.y - length, point.x + length);
                break;
        }

        //ctx.closePath();
        ctx.stroke();
    },
    drawPath: (from: string, to: string,action:string) => {
        //找from to的坐标
        const height = 30;
        const width = 60;
        const trackHeight = 410;
        const fromX: number = data.stationName.find(station => station.station_code === from)?.x as number;
        const fromY: number = data.stationName.find(station => station.station_code === from)?.y as number;
        const toX: number = data.stationName.find(station => station.station_code === to)?.x as number;
        const toY: number = data.stationName.find(station => station.station_code === to)?.y as number;
        const takePoint: Point = { x: (fromX) * height + height / 2, y: (fromY) * width + width / 2 };
        const takeTrack: Point = { x: trackHeight, y: fromY * width + width / 2 };
        const putTrack: Point = { x: trackHeight, y: toY * width + width / 2 };
        const putPoint: Point = { x: toX * height + height / 2, y: toY * width + width / 2 };


        const canvas = document.getElementById('pathCanvas') as HTMLCanvasElement;
        const ctx = canvas.getContext('2d');

        const pathElement = document.getElementById('linePath') as unknown as SVGPathElement;

        if(action==="Finished" && ctx){
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            pathElement.setAttribute('d', '');
            return;
        }

        
        if (pathElement) {
            const d = `
            M${takePoint.y},${takePoint.x}
            L${takeTrack.y},${takeTrack.x}
            L${putTrack.y},${putTrack.x}
            L${putPoint.y},${putPoint.x}`;
            pathElement.setAttribute('d', d);
        }

        if (ctx) {
            ctx.clearRect(0, 0, canvas.width, canvas.height); // 清除之前绘制的路径

            ctx.lineWidth = data.lineWidth;
            ctx.strokeStyle = data.lineColor;

            ctx.beginPath();
            ctx.moveTo(takePoint.y, takePoint.x); // 起始点  
            ctx.lineTo(takeTrack.y, takeTrack.x); // 终点  
            ctx.stroke(); // 执行绘制 

            ctx.beginPath();
            ctx.moveTo(takeTrack.y, takeTrack.x); // 起始点  
            ctx.lineTo(putTrack.y, putTrack.x); // 终点  
            ctx.stroke(); // 执行绘制 

            ctx.beginPath();
            ctx.moveTo(putTrack.y, putTrack.x); // 起始点  
            ctx.lineTo(putPoint.y, putPoint.x); // 终点  
            ctx.stroke(); // 执行绘制 

            // 绘制箭头 takePoint水平方向小于takeTrack,则箭头向下
            if (takePoint.x < takeTrack.x) {
                method.drawTriangle(ctx, takeTrack, Direction.Down);
            } else {
                method.drawTriangle(ctx, takeTrack, Direction.Up);
            }
            // 绘制箭头takeTrack垂直方向小于putTrack,则箭头向右
            if (takeTrack.y < putTrack.y) {
                // while (takeTrack.y < putTrack.y) {
                //     method.drawTriangle(ctx, takeTrack, Direction.Right);
                //     takeTrack.y += data.spaceBetween;
                // }
                method.drawTriangle(ctx, putTrack, Direction.Right);
            } else {
                // while (putTrack.y < takeTrack.y) {
                //     method.drawTriangle(ctx, putTrack, Direction.Left);
                //     putTrack.y += data.spaceBetween;
                // }
                method.drawTriangle(ctx, putTrack, Direction.Left);
            }
            //绘制箭头 putTrack水平方向小于putPoint,则箭头向下
            if (putTrack.x < putPoint.x) {
                method.drawTriangle(ctx, putPoint, Direction.Down);
            } else {
                method.drawTriangle(ctx, putPoint, Direction.Up);
            }
        }
    },
    signalR_Listen: () => {
        data.hubConnection.on('ReciveMsg', (message: string) => {
            console.log('Received message:', message);
            const parsedData = JSON.parse(message);
            const statusScreen: StatusScreenVO = parsedData as StatusScreenVO;
            var type = statusScreen.Type;
            if (type === "RobotAnimation") {
                const robotAnimation = JSON.parse(statusScreen.Content) as PublishTaskContentVO;
                var title = JSON.parse(robotAnimation.Title) as FromTo;
                data.titleContent = "从:" + title.From + " 到:" + title.To + " 动作:" + title.Action;
                //console.log('Received Content:', taskInfo.Title);
                if (data.imageElement) {
                    //console.log('AnimationX:', robotAnimation.AnimationX);
                    data.imageElement.style.left = robotAnimation.AnimationX + 'px';
                }
                method.drawPath(title.From, title.To,title.Action);
            };
            if (type === "StationStatus") {
                const stationStatus = JSON.parse(statusScreen.Content) as P6144StationVO[];
                // console.log('StationStatus:', stationStatus);
                //data.preparingLocation.length = 0;
                data.stationName = stationStatus.map(item => ({ x: item.status_x, y: item.status_y, station_code: item.station_code, station_name: item.station_name }));
                data.preparingLocation = stationStatus
                    .filter(item => item.status === StationStatusEnum[StationStatusEnum.Preparing])
                    .map(item => ({ x: item.status_x, y: item.status_y, station_name: item.station_code }));
                // console.log('StationStatusEnum Preparing:', StationStatusEnum[StationStatusEnum.Preparing]);
                // console.log('preparingLocation Content:', data.preparingLocation);
                //data.requestLoadingLocation.length = 0;
                data.requestLoadingLocation = stationStatus
                    .filter(item => item.status === StationStatusEnum[StationStatusEnum.RequestLoading])
                    .map(item => ({ x: item.status_x, y: item.status_y, station_name: item.station_code }));
                //data.processingLocation.length = 0;
                data.processingLocation = stationStatus
                    .filter(item => item.status === StationStatusEnum[StationStatusEnum.Processing])
                    .map(item => ({ x: item.status_x, y: item.status_y, station_name: item.station_code }));
                //data.finishedLocation.length = 0;
                data.finishedLocation = stationStatus
                    .filter(item => item.status === StationStatusEnum[StationStatusEnum.Finished])
                    .map(item => ({ x: item.status_x, y: item.status_y, station_name: item.station_code }));
            };
            data.refreshDatetime = "刷新时间:" + (new Date()).toLocaleString('cn-ZH', {
                year: 'numeric',
                month: '2-digit',
                day: '2-digit',
                hour: '2-digit',
                minute: '2-digit',
                second: '2-digit'
            });
        });
    },
    signalRConnect: async () => {
        try {
            await data.hubConnection.start();
            console.log('Connected!');
            data.connection = data.hubConnection; // 保存连接实例以便后续使用
        } catch (err) {
            console.error('SignalR Connection Error:', err);
        }
    },
    setPreparingStatus: (row: number, col: number) => {
        return data.preparingLocation.some(coord => coord.x === col && coord.y === row);
    },
    setRequestLoadingStatus: (row: number, col: number) => {
        return data.requestLoadingLocation.some(coord => coord.x === col && coord.y === row);
    },
    setProcessingStatus: (row: number, col: number) => {
        return data.processingLocation.some(coord => coord.x === col && coord.y === row);
    },
    setFinishedStatus: (row: number, col: number) => {
        return data.finishedLocation.some(coord => coord.x === col && coord.y === row);
    },
    getStationName: (row, col) => {
        const coordinate = data.stationName.find(coord => coord.x === col && coord.y === row);
        return coordinate ? coordinate.station_code : ""; // 如果找到匹配的坐标,则返回其status,否则返回空字符串  
    },
    showCoordinates: (row, col) => {
        var stationName = data.stationName.find(coord => coord.x === col && coord.y === row)?.station_name;
        data.showCoordinates = true;
        data.currentCoordinates = `${stationName},Row: ${row}, Col: ${col}`;
        //console.log(stationName);
    },
    hideCoordinates: () => {
        data.showCoordinates = false;
        data.currentCoordinates = '';
    }
})

const MAX_RECONNECT_ATTEMPTS = 1200; // 最大重连尝试次数  
let reconnectAttempts = 0; // 当前重连尝试次数  

// 添加一个用于重连的方法  
const reconnect = () => {
    if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
        reconnectAttempts++;
        console.log(`Attempting to reconnect... (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})`);
        setTimeout(async () => {
            try {
                await data.hubConnection.start();
                console.log('Reconnected!');
                reconnectAttempts = 0; // 重置重连尝试次数  
                method.signalR_Listen(); // 重新绑定事件监听器  
            } catch (err) {
                reconnect(); // 如果连接失败,则递归尝试重新连接  
            }
        }, 2000); // 等待2秒后重试连接  
    } else {
        console.error('Max reconnect attempts reached.');
    }
};

// 使用 onMounted 来获取 DOM 元素引用
onMounted(() => {
    data.positionInput = document.getElementById('positionInput') as HTMLInputElement
    data.imageElement = document.getElementById('kuka') as HTMLImageElement
    method.signalRConnect();
    method.signalR_Listen();
    // 监听连接断开事件,并尝试重新连接  
    data.hubConnection.onclose(async (error) => {
        if (error) {
            console.error('Connection closed with error:', error);
        }
        reconnect(); // 当连接断开时尝试重新连接  
    });

    const canvas = document.getElementById('pathCanvas') as HTMLCanvasElement;
    const ctx = canvas.getContext('2d');
    if (ctx) {
        const scaleFactor = 1;
        const scaledWidth = 1440 * scaleFactor;
        const scaledHeight = 840 * scaleFactor;
        canvas.width = scaledWidth;
        canvas.height = scaledHeight;
        ctx.scale(scaleFactor, scaleFactor);
        ctx.lineWidth = 1;
        ctx.strokeStyle = 'green';
    }
})
</script>

<style lang="less" scoped>
.kuka {
    position: absolute;
    top: 350px;
    left: 300px;
    transform: translateX(0%);
    z-index: 30;
    width: 100px;
    height: auto;
    transition: left 3s ease-in-out;
}

.track {
    position: absolute;
    top: 350px;
    transform: translateX(0%);
    z-index: 20;
    width: 890px;
    height: 120px;
    background-color: rgb(190, 190, 114);
    opacity: 0.9;
    margin-left: 300px;
    margin-right: 250px;
}

.background {
    z-index: 9;
    position: absolute;
    top: 0px;
    left: 0%;
    transform: translateX(0%);
    width: 1440px;
    height: 840px;
}

.inputContainer {
    margin-bottom: 20px;
}

.grid-container {
    display: grid;
    grid-template-columns: repeat(24, 60px);
    grid-template-rows: repeat(28, 30px);
    width: 100%;
    height: 100%;
    grid-gap: 0px;
    background-color: white;
    z-index: 10;
    //position: absolute;
}

.grid-cell {
    width: 60px;
    height: 30px;
    background-color: white;
    margin: 0;
    padding: 0;
    //border-radius:30%;
    position: relative;
    // opacity: 0.5;
    // z-index: 12; 
}

.container {
    //position: relative;
    height: 100%;
    display: flex;
    justify-content: center;
    align-items: center;
    //animation: blink 2s infinite;
}

@keyframes blink {

    /* 从0%(完全不透明)到100%(完全透明),然后再回到0% */
    0%,
    100% {
        opacity: 1;
    }

    50% {
        opacity: 0.8;
    }
}

.legend {
    width: 12px;
    /* 圆点的宽度 */
    height: 12px;
    /* 圆点的高度 */
    // border-radius: 50%;
    /* 将矩形元素转换为圆形 */
    display: inline-block;
}

.preparing-status-cell {
    background-color: burlywood;
    border: 1px solid black;
    z-index: 16;
    /* 提高 z-index 值 */
    position: relative;
}

.request-loading-status-cell {
    background-color: blue;
    border: 1px solid black;
    z-index: 16;
    /* 提高 z-index 值 */
    position: relative;
}

.processing-status-cell {
    background-color: green;
    border: 1px solid black;
    z-index: 16;
    /* 提高 z-index 值 */
    position: relative;
}

.finished-status-cell {
    background-color: red;
    border: 1px solid black;
    z-index: 16;
    /* 提高 z-index 值 */
    position: relative;
}

.station-font {
    display: flex;
    align-items: flex-end;
    justify-content: center;
    height: 70%;
    color: white;
    font-size: 10px;
    font-weight: bold;
}

.coordinates-display {
    position: absolute;
    top: 10px;
    left: 10px;
    padding: 5px;
    background-color: white;
    color: black;
    border-radius: 5px;
    z-index: 50;
}

.pathStyle {
    position: absolute;
    z-index: 25;
    pointer-events: none;
    top: 0;
    left: 0;
}

body {
    display: flex;
    justify-content: center;
    align-items: center;
    height: 100vh;
    background-color: #f0f0f0;
}

.path {
    position: absolute;
    pointer-events: none;
    width: 1440px;
    height: 840px;
    z-index: 25;
    top: 0;
}

.arrow {
    fill: lightgray;
    width: 40px;
    height: 40px;
    animation: moveAlongPath 4s linear infinite alternate;
    z-index: 25;
    // border: black 1px solid;
}

@keyframes moveAlongPath {
    0% {
        offset-distance: 0%;
    }
    100% {
        offset-distance: 100%;
    }
}
</style>
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值