当前位置: 首页 > news >正文

大屏可视化动图渲染

大屏可视化项目中我们常用到动图,一般情况下是UI提供gif动图来实现,也有一些情况下并不适合这种情况。比如当前做的收费站数字化管理平台使用到了半透明的动图,这个时候UI导出gif动图时,会出现明显的锯齿,没有达到预期效果。这个时候就需要采用png序列帧的方式,本文主要分享的是动图展示方案。

实际效果

在这里插入图片描述

左侧是通过png 序列帧的方式实现效果,右侧是直接加载gif动图

在这里插入图片描述

上面是背景为透明状态下的gif,可以明显地看到锯齿效果

概念介绍

PNG序列帧:一种通过连续播放多张独立的PNG图片来实现动画效果的技术。

  • 核心优势

    1. 高质量透明背景支持:相比GIF格式,PNG支持Alpha通道,能够完美呈现半透明或全透明区域且无边缘锯齿问题。这使得它在需要精细抠图的场景(如UI元素、logo动画)中表现更优。
    2. 色彩还原度高:采用无损压缩算法,颜色偏差极小,尤其适合包含渐变色或复杂细节的图形。例如,火焰、雨水等粒子特效能更真实地展现。
    3. 开发可控性强:开发人员可灵活调整播放速度、循环次数及交互逻辑。
  • 优点

    1. 兼容性好:几乎所浏览器都支持,可直接对接开发流程。
    2. 效果还原度高:逐帧渲染避免压缩损失,动态细节完整保留。
    3. 灵活编辑:每张图片独立存在,便于后期修改某一帧而不影响整体。
  • 缺点

    1. 源占用大:复杂动画可能产生成百上千张图片,导致加载时间长和内存消耗高。例如,某汽车小程序的加载动画使用了近200张PNG,对性能造成压力。
    2. 性能瓶颈:大量图片同时加载易引发卡顿,需权衡画质与流畅度。
PNG 序列图、GIF方案比对
特性时序图/代码方案GIF方案
文件大小✅ 小(矢量或压缩算法优化)❌ 大(尤其长时长动画)
交互支持✅ 可绑定事件、动态控制❌ 仅循环播放无交互
分辨率适配✅ 矢量无损缩放❌ 位图放大模糊/锯齿
性能开销✅ 硬件加速渲染❌ CPU频繁解码
维护成本✅ 代码可读性强、易于调试❌ 二进制格式难以修改
动态数据集成✅ 支持实时数据驱动❌ 静态内容无法变化
实战代码
SequencePlayer组件,基于setInterval
<template><div class="sequence-player"><img:src="currentFrame":style="imageStyle"alt="序列动画"class="sequence-img"ref="imgElement"/></div>
</template><script>
export default {name: "SequencePlayer",components: {},props: {// 📝 核心配置项frames: {type: Array,required: true, // 必须传入图片数组validator(value) {return value.every((item) => typeof item === "string");},},frameRate: {type: Number,default: 20, // 默认20FPS},autoStart: {type: Boolean,default: true, // 默认自动播放},imageStyle: {type: Object,default() {return { width: "90px", height: "auto" }; // 默认图片样式},},},data() {return {currentIndex: 0,intervalId: null,isPlaying: false,};},watch: {frames: {immediate: true,handler(newVal) {if (!newVal || newVal.length === 0) {console.warn("⚠️ 警告:未检测到有效图片序列");}},},},computed: {currentFrame() {return this.frames[this.currentIndex];},},methods: {start() {if (this.intervalId || !this.frames?.length) return;this.isPlaying = true;this.intervalId = setInterval(() => {this.currentIndex = (this.currentIndex + 1) % this.frames.length;}, 1000 / this.frameRate);},stop() {clearInterval(this.intervalId);this.intervalId = null;this.isPlaying = false;},reset() {this.stop();this.currentIndex = 0;},jumpTo(index) {if (index >= 0 && index < this.frames.length) {this.currentIndex = index;}},},created() {},mounted() {if (this.autoStart) this.start();},beforeDestroy() {this.stop();},
};
</script><style lang="less" scoped>
.sequence-player {display: flex;justify-content: center;align-items: center;height: 100%;width: 100%;
}
.sequence-img {image-rendering: high-quality;border-radius: 8px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);transition: opacity 0.3s ease; /* 可选:添加淡入淡出效果 */
}
</style>
使用方式
<template><div class="simple-sequence"><div class="demo"><SequencePlayer :frame-rate="20" :frames="baseFrames" /></div></div>
</template><script>
import SequencePlayer from "../SequencePlayer.vue";export default {name: "FrameDemo",components: {SequencePlayer,},data() {return {baseFrames: [require("../image/RSU_00000.png"),require("../image/RSU_00001.png"),require("../image/RSU_00002.png"),require("../image/RSU_00003.png"),require("../image/RSU_00004.png"),require("../image/RSU_00005.png"),require("../image/RSU_00006.png"),require("../image/RSU_00007.png"),require("../image/RSU_00008.png"),require("../image/RSU_00009.png"),require("../image/RSU_00010.png"),require("../image/RSU_00011.png"),require("../image/RSU_00012.png"),require("../image/RSU_00013.png"),require("../image/RSU_00014.png"),require("../image/RSU_00015.png"),require("../image/RSU_00016.png"),require("../image/RSU_00017.png"),require("../image/RSU_00018.png"),require("../image/RSU_00019.png"),],};},
};
</script><style lang="less" scoped>
.simple-sequence {display: flex;justify-content: center;align-items: center;height: 100%;width: 100%;
}
</style>

在这里插入图片描述

左侧是基于setInterval实现的,右侧是基于requestAnimationFrame实现的

SequencePlayerCopy组件,基于requestAnimationFrame

改进点

  1. 移除 setInterval 依赖
    改用 requestAnimationFrame API 实现高性能动画循环。
  2. 精准计时控制
    通过时间戳差值计算实际经过的时间,确保按指定帧率播放。
  3. 自动启停管理
    在组件生命周期内正确处理动画状态(启动/暂停/重置)。
  4. 性能优化
    避免累积误差导致的动画加速问题。
<template><div class="sequence-player"><img:src="currentFrame":style="imageStyle"alt="序列动画"class="sequence-img"ref="imgElement"/></div>
</template><script>
export default {name: "SequencePlayer",components: {},props: {// &#128221; 核心配置项frames: {type: Array,required: true, // 必须传入图片数组validator(value) {return value.every((item) => typeof item === "string");},},frameRate: {type: Number,default: 20, // 默认20FPS},autoStart: {type: Boolean,default: true, // 默认自动播放},imageStyle: {type: Object,default() {return { width: "90px", height: "auto" }; // 默认图片样式},},},data() {return {currentIndex: 0,animationId: null, // 存储RAF IDlastTimestamp: null, // 上一帧时间戳isPlaying: false, // 播放状态标记accumulatedTime: 0, // 累计等待时间(用于稳定帧率)};},watch: {frames: {immediate: true,handler(newVal) {if (!newVal || newVal.length === 0) {console.warn("&#9888;️ 警告:未检测到有效图片序列");}},},},computed: {currentFrame() {return this.frames[this.currentIndex];},// 根据目标帧率计算每帧应持续的时间(毫秒)targetFrameDuration() {return 1000 / this.frameRate;},},methods: {/*** 动画主循环 - 使用 requestAnimationFrame 实现* @param {number} timestamp - 当前时间戳(由RAF自动传入)*/animate(timestamp) {if (!this.lastTimestamp) {this.lastTimestamp = timestamp;requestAnimationFrame(this.animate.bind(this));return;}const elapsed = timestamp - this.lastTimestamp;const waitTime = Math.max(0, this.targetFrameDuration - elapsed);if (waitTime > 0) {// 如果未达到目标间隔,设置超时回调继续等待setTimeout(() => {requestAnimationFrame(this.animate.bind(this));}, waitTime);} else {// 时间足够时更新帧并重置计时器this.updateFrame();this.lastTimestamp = performance.now();requestAnimationFrame(this.animate.bind(this));}},/*** 更新当前显示的帧索引*/updateFrame() {this.currentIndex = (this.currentIndex + 1) % this.frames.length;},/*** 启动动画*/start() {if (this.animationId !== null || !this.frames?.length) return;this.isPlaying = true;this.lastTimestamp = performance.now();requestAnimationFrame(this.animate.bind(this));},/*** 停止动画*/stop() {cancelAnimationFrame(this.animationId);this.animationId = null;this.isPlaying = false;this.lastTimestamp = null;},/*** 重置到第一帧并停止播放*/reset() {this.stop();this.currentIndex = 0;},/*** 跳转到指定索引的帧* @param {number} index - 目标帧索引*/jumpTo(index) {if (index >= 0 && index < this.frames.length) {this.currentIndex = index;}},},mounted() {if (this.autoStart) this.start();},beforeDestroy() {this.stop();},
};
</script><style lang="less" scoped>
.sequence-player {display: flex;justify-content: center;align-items: center;height: 100%;width: 100%;
}
.sequence-img {image-rendering: high-quality;border-radius: 8px;box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);transition: opacity 0.3s ease; /* 可选:添加淡入淡出效果 */
}
</style>
使用
<template><div class="simple-sequence"><div class="demo"><SequencePlayerCopy :frame-rate="20" :frames="baseFrames" /></div></div>
</template><script>
import SequencePlayer from "../SequencePlayer.vue";
import SequencePlayerCopy from "../SequencePlayerCopy.vue";export default {name: "FrameDemo",components: {SequencePlayer,SequencePlayerCopy,},data() {return {baseFrames: [require("../image/RSU_00000.png"),require("../image/RSU_00001.png"),require("../image/RSU_00002.png"),require("../image/RSU_00003.png"),require("../image/RSU_00004.png"),require("../image/RSU_00005.png"),require("../image/RSU_00006.png"),require("../image/RSU_00007.png"),require("../image/RSU_00008.png"),require("../image/RSU_00009.png"),require("../image/RSU_00010.png"),require("../image/RSU_00011.png"),require("../image/RSU_00012.png"),require("../image/RSU_00013.png"),require("../image/RSU_00014.png"),require("../image/RSU_00015.png"),require("../image/RSU_00016.png"),require("../image/RSU_00017.png"),require("../image/RSU_00018.png"),require("../image/RSU_00019.png"),],};},
};
</script><style lang="less" scoped>
.simple-sequence {display: flex;justify-content: center;align-items: center;height: 100%;width: 100%;
}
.demo {height: 90px;width: 90px;margin-top: 100px;margin-left: 100px;
}
</style>

文章转载自:

http://cIg4gfTj.pLfrk.cn
http://hNhvgYLv.pLfrk.cn
http://CD257zQi.pLfrk.cn
http://RUWnkBxE.pLfrk.cn
http://UhI9re4r.pLfrk.cn
http://ZMyJkfEA.pLfrk.cn
http://G2xk3V9x.pLfrk.cn
http://HyXYX6vy.pLfrk.cn
http://C5NQPCnD.pLfrk.cn
http://3CFqGhbK.pLfrk.cn
http://bMjWPopF.pLfrk.cn
http://Z69emgWe.pLfrk.cn
http://vDen2WgO.pLfrk.cn
http://9FWpFwE5.pLfrk.cn
http://2CkHVWJC.pLfrk.cn
http://D1R0fwC9.pLfrk.cn
http://0N4Te16n.pLfrk.cn
http://6dJnGTxF.pLfrk.cn
http://BHwxqy4M.pLfrk.cn
http://oi0BKF3g.pLfrk.cn
http://wJShqHiI.pLfrk.cn
http://RmGAeg56.pLfrk.cn
http://kTZWYw0h.pLfrk.cn
http://P9kFEDGQ.pLfrk.cn
http://c71FMM4f.pLfrk.cn
http://IimfbYqg.pLfrk.cn
http://E5vakClD.pLfrk.cn
http://xfSLEIFE.pLfrk.cn
http://wsrH6IMG.pLfrk.cn
http://5kP8HlO6.pLfrk.cn
http://www.dtcms.com/a/385128.html

相关文章:

  • Claude Code生态、实战
  • 系统架构设计师备考第24天——需求工程
  • 逻辑回归模型:基于鸢尾花数据集的多分类任务全流程
  • 实战适配器模式
  • OpenLayers数据源集成 -- 章节十三:EsriJSON图层详解
  • RabbitMQ—运维篇
  • 一站式YOLO目标检测解决方案:训练、测试、推理与数据集转换全流程指南
  • lamp脚本部署
  • 前端构建工具有哪些?常用前端构建工具对比、前端构建工具推荐与最佳实践分析
  • Charles日志分析与流量监控技巧 高效排查接口与性能问题
  • ACM模式的输入部分——input().strip() 与 sys.stdin.readline() 的不同
  • ““.equals(studentID)
  • 【Lesson 3】CUDA 编程模型:线程、块、网格 —— Ubuntu 22.04 + RTX 30/40 系列实战
  • [Windows]C盘瘦身 --- 软件搬家
  • CLIP 完全上手指南:从安装、下载加速、文本/图像编码到图文匹配,一篇全搞定!
  • BKY莱德因:5大黑科技逆转时光
  • 开源嵌入模型推荐与选型指南
  • 科普:Python中为什么“from .utils” 不能写成 “from ./utils”?
  • 客户粘性提升策略是什么?系统化策略提升客户粘性指南
  • Spring 框架从入门到精通(第一篇)—— 框架核心与 IOC 容器实践
  • 通过DSL生成Jenkins流水线
  • 构建AI智能体:三十四、LangChain SQLDatabaseToolkit终极指南:架构、优势与最佳实践
  • 算法 --- 字符串
  • PDF 文件创建时间属性怎样批量修改详细教程
  • hutool DesensitizedUtil
  • train.py代码学习 自学
  • 安全与效率的平衡术:安全空间
  • 【Unity】事件分发系统的使用示例
  • dinov3 源码 笔记1
  • 飞书项目,再交卷中国智造