引言:重新认识现代Web演示框架
在数字化时代,演示文稿已经超越了传统PPT的平面限制,向着更具沉浸感、交互性和表现力的方向发展。Impress.js作为一款基于CSS3 3D变换和JavaScript的开源演示框架,正是这一变革的杰出代表。不同于常规幻灯片工具,Impress.js允许开发者在浏览器中创建出令人惊叹的三维演示效果,将观众带入一个充满可能性的Web 3D世界。
本系列文章将深入探讨Impress.js的完整技术体系,从基础的2D/3D布局系统,到复杂的数据可视化集成,再到企业级的架构设计。我们不仅会展示如何创建炫酷的视觉效果,更重要的是揭示如何构建可维护、可扩展、高性能的Impress.js应用架构。无论你是前端开发者、技术演讲者,还是希望提升产品展示效果的产品经理,本系列都将为你提供从理论到实践的全面指导。
第一章:Impress.js架构哲学与设计思想
1.1 从Prezi到Impress.js:思维范式的转变
传统的演示工具如PowerPoint或Keynote采用"幻灯片堆叠"的思维模型,每个页面都是独立的、平面的。而Impress.js继承自Prezi的"无限画布"理念,将整个演示视为一个三维空间,每个内容节点都是这个空间中的一个坐标点。这种思维范式的转变带来了几个重要的架构优势:
-
空间连续性:内容之间的过渡不再是生硬的页面切换,而是平滑的空间移动,保持了思维的连续性
-
层次关系可视化:通过空间位置的安排(上下、左右、前后)直观展示内容之间的逻辑关系
-
缩放聚焦:可以放大查看细节,缩小把握全局,类似于地图应用的交互体验
-
非线性的演示路径:支持跳转到任意节点,适应即兴的演讲需求
1.2 Impress.js的核心架构设计
Impress.js的架构可以划分为三个核心层次:
javascript
// Impress.js架构层次示意图
class ImpressArchitecture {
constructor() {
this.layers = {
// 核心层:3D变换引擎
core: {
transform: 'CSS3 3D变换矩阵计算',
transition: '动画过渡处理',
navigation: '键盘/鼠标/触摸事件处理'
},
// 应用层:演示内容管理
application: {
steps: '步骤管理与状态维护',
plugins: '插件扩展系统',
events: '自定义事件系统'
},
// 表现层:样式与渲染
presentation: {
styling: 'CSS样式系统',
themes: '主题与皮肤',
responsive: '响应式适配'
}
};
}
// 初始化流程
init() {
this.setupTransforms(); // 设置3D变换
this.setupNavigation(); // 设置导航系统
this.setupPlugins(); // 初始化插件
this.setupEvents(); // 绑定事件
}
}
1.3 与现代Web技术栈的集成
Impress.js并非孤立的框架,它可以无缝集成到现代Web技术栈中:
javascript
// Impress.js与现代前端框架的集成模式
const IntegrationPatterns = {
// 与React的集成
react: {
wrapper: '创建ImpressContext包装器',
steps: '将Step组件化',
state: '与React状态同步',
hooks: '自定义Hooks管理演示状态'
},
// 与Vue的集成
vue: {
directives: '创建v-step指令',
components: 'Step组件化',
store: '与Vuex/Pinia状态管理集成'
},
// 构建工具集成
build: {
webpack: '通过loader处理HTML模板',
vite: '作为插件集成到开发环境',
typescript: '完整的类型定义支持'
}
};
第二章:结构化布局系统设计
2.1 2D布局:网格与流式系统
尽管Impress.js以3D效果闻名,但其2D布局能力同样强大且实用。我们设计了完整的网格布局系统:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Impress.js 2D网格布局系统</title>
<script src="https://cdn.jsdelivr.net/npm/impress.js@2.0.0/js/impress.min.js"></script>
<style>
/* 网格布局系统基础样式 */
:root {
--grid-cols: 12;
--grid-rows: 8;
--grid-gap: 100px;
--step-width: 800px;
--step-height: 600px;
}
/* 基础步骤样式 */
.step {
width: var(--step-width);
height: var(--step-height);
padding: 40px;
box-sizing: border-box;
border-radius: 16px;
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
/* 网格定位系统 */
.grid-container {
display: grid;
grid-template-columns: repeat(var(--grid-cols), 1fr);
grid-template-rows: repeat(var(--grid-rows), 1fr);
gap: var(--grid-gap);
width: calc(var(--grid-cols) * (var(--step-width) + var(--grid-gap)));
height: calc(var(--grid-rows) * (var(--step-height) + var(--grid-gap)));
}
/* 网格位置类 */
.grid-col-1 { grid-column: span 1; }
.grid-col-2 { grid-column: span 2; }
.grid-col-3 { grid-column: span 3; }
.grid-col-4 { grid-column: span 4; }
.grid-col-6 { grid-column: span 6; }
.grid-col-12 { grid-column: span 12; }
.grid-row-1 { grid-row: span 1; }
.grid-row-2 { grid-row: span 2; }
.grid-row-3 { grid-row: span 3; }
/* 响应式调整 */
@media (max-width: 1200px) {
:root {
--step-width: 600px;
--step-height: 450px;
--grid-gap: 60px;
}
}
@media (max-width: 768px) {
:root {
--step-width: 90vw;
--step-height: auto;
--grid-cols: 1;
--grid-gap: 40px;
}
.grid-col-2, .grid-col-3, .grid-col-4, .grid-col-6 {
grid-column: span 1;
}
}
</style>
</head>
<body>
<div id="impress" data-grid-layout="true">
<!-- 标题页 -->
<div class="step grid-col-12 grid-row-2"
data-grid-x="0" data-grid-y="0"
data-x="0" data-y="0" data-z="0">
<div class="title-container">
<h1 style="font-size: 4em; margin-bottom: 0.5em;">企业年度报告</h1>
<p style="font-size: 1.5em; color: #666;">2024年度总结与2025展望</p>
<div class="divider"></div>
<p style="font-size: 1.2em; color: #888;">基于Impress.js网格布局系统</p>
</div>
</div>
<!-- KPI指标网格 -->
<div class="step grid-col-3 grid-row-1"
data-grid-x="0" data-grid-y="2"
data-x="0" data-y="1000" data-z="0">
<div class="kpi-card" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);">
<div class="kpi-icon">📈</div>
<div class="kpi-value" id="revenue-value">¥ 1.2亿</div>
<div class="kpi-label">年度营收</div>
<div class="kpi-trend">+15% ↗</div>
</div>
</div>
<div class="step grid-col-3 grid-row-1"
data-grid-x="3" data-grid-y="2"
data-x="1000" data-y="1000" data-z="0">
<div class="kpi-card" style="background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);">
<div class="kpi-icon">👥</div>
<div class="kpi-value" id="customers-value">15,832</div>
<div class="kpi-label">新增客户</div>
<div class="kpi-trend">+28% ↗</div>
</div>
</div>
<!-- 跨列内容区域 -->
<div class="step grid-col-6 grid-row-2"
data-grid-x="0" data-grid-y="3"
data-x="0" data-y="2000" data-z="0">
<h2>重点项目成果</h2>
<div class="project-timeline">
<div class="timeline-item">
<div class="timeline-date">Q1</div>
<div class="timeline-content">
<h3>新产品线发布</h3>
<p>成功发布三款新产品,市场反响热烈</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-date">Q2</div>
<div class="timeline-content">
<h3>国际业务拓展</h3>
<p>进入东南亚市场,建立本地化团队</p>
</div>
</div>
</div>
</div>
</div>
<!-- 网格布局控制器 -->
<script>
class GridLayoutController {
constructor(containerId = 'impress') {
this.container = document.getElementById(containerId);
this.steps = [];
this.gridConfig = {
cols: 12,
rows: 8,
colWidth: 1000, // 每列宽度
rowHeight: 800, // 每行高度
gap: 100 // 间距
};
this.initialized = false;
}
// 初始化网格布局
init() {
if (this.initialized) return;
// 收集所有步骤元素
this.steps = Array.from(this.container.querySelectorAll('.step'));
// 为每个步骤计算3D坐标
this.calculatePositions();
// 设置容器大小
this.setContainerSize();
this.initialized = true;
return this;
}
// 计算每个步骤的位置
calculatePositions() {
this.steps.forEach((step, index) => {
// 从data-grid-x/y属性获取网格位置
const gridX = parseInt(step.dataset.gridX) || 0;
const gridY = parseInt(step.dataset.gridY) || 0;
// 从class获取跨列/行信息
const colSpan = this.getColSpan(step);
const rowSpan = this.getRowSpan(step);
// 计算3D坐标
const x = (gridX + colSpan / 2) * (this.gridConfig.colWidth + this.gridConfig.gap);
const y = (gridY + rowSpan / 2) * (this.gridConfig.rowHeight + this.gridConfig.gap);
const z = 0;
// 设置3D坐标
step.dataset.x = x;
step.dataset.y = y;
step.dataset.z = z;
// 设置宽度和高度(基于跨列/行)
step.style.width = `${colSpan * this.gridConfig.colWidth + (colSpan - 1) * this.gridConfig.gap}px`;
step.style.height = `${rowSpan * this.gridConfig.rowHeight + (rowSpan - 1) * this.gridConfig.gap}px`;
});
}
// 获取跨列数
getColSpan(step) {
const classes = step.className.split(' ');
for (const cls of classes) {
if (cls.startsWith('grid-col-')) {
return parseInt(cls.replace('grid-col-', ''));
}
}
return 1;
}
// 获取跨行数
getRowSpan(step) {
const classes = step.className.split(' ');
for (const cls of classes) {
if (cls.startsWith('grid-row-')) {
return parseInt(cls.replace('grid-row-', ''));
}
}
return 1;
}
// 设置容器大小
setContainerSize() {
const maxGridX = Math.max(...this.steps.map(s =>
parseInt(s.dataset.gridX) + this.getColSpan(s)
));
const maxGridY = Math.max(...this.steps.map(s =>
parseInt(s.dataset.gridY) + this.getRowSpan(s)
));
const width = maxGridX * (this.gridConfig.colWidth + this.gridConfig.gap);
const height = maxGridY * (this.gridConfig.rowHeight + this.gridConfig.gap);
this.container.style.width = `${width}px`;
this.container.style.height = `${height}px`;
}
// 响应式调整
updateForViewport(width, height) {
// 根据视口大小调整网格配置
if (width < 768) {
this.gridConfig.colWidth = 300;
this.gridConfig.rowHeight = 400;
this.gridConfig.gap = 30;
} else if (width < 1200) {
this.gridConfig.colWidth = 600;
this.gridConfig.rowHeight = 500;
this.gridConfig.gap = 60;
} else {
this.gridConfig.colWidth = 1000;
this.gridConfig.rowHeight = 800;
this.gridConfig.gap = 100;
}
this.calculatePositions();
this.setContainerSize();
// 重新初始化Impress.js
if (window.impress) {
window.impress().init();
}
}
}
// 使用示例
document.addEventListener('DOMContentLoaded', () => {
const gridController = new GridLayoutController('impress');
gridController.init();
// 初始化Impress.js
impress().init();
// 响应式调整
window.addEventListener('resize', () => {
gridController.updateForViewport(window.innerWidth, window.innerHeight);
});
});
</script>
<!-- KPI卡片样式 -->
<style>
.kpi-card {
width: 100%;
height: 100%;
border-radius: 20px;
padding: 30px;
color: white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.2);
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.kpi-card:hover {
transform: translateY(-10px);
box-shadow: 0 25px 40px rgba(0, 0, 0, 0.3);
}
.kpi-icon {
font-size: 3em;
margin-bottom: 20px;
}
.kpi-value {
font-size: 2.5em;
font-weight: bold;
margin: 10px 0;
}
.kpi-label {
font-size: 1.2em;
opacity: 0.9;
margin-bottom: 10px;
}
.kpi-trend {
font-size: 1.1em;
padding: 5px 15px;
background: rgba(255, 255, 255, 0.2);
border-radius: 20px;
margin-top: 10px;
}
.project-timeline {
margin-top: 40px;
}
.timeline-item {
display: flex;
margin-bottom: 30px;
align-items: flex-start;
}
.timeline-date {
background: #667eea;
color: white;
padding: 10px 20px;
border-radius: 20px;
font-weight: bold;
margin-right: 20px;
min-width: 80px;
text-align: center;
}
.timeline-content {
flex: 1;
padding: 15px;
background: #f8f9fa;
border-radius: 10px;
border-left: 4px solid #667eea;
}
</style>
</body>
</html>
2.2 流式布局适配器
对于更灵活的内容排列,我们设计了流式布局系统,自动根据内容调整布局:
javascript
/**
* 流式布局适配器
* 自动根据步骤内容和顺序进行布局
*/
class FlowLayoutAdapter {
constructor(options = {}) {
this.options = {
direction: 'horizontal', // 流式方向: horizontal, vertical, diagonal
spacing: { // 间距配置
x: 1000,
y: 800,
z: 0
},
align: 'center', // 对齐方式
maxPerRow: 3, // 每行最大数量(水平方向)
rowHeight: 600, // 行高(水平方向)
...options
};
this.steps = [];
this.layoutMap = new Map(); // 步骤->位置映射
}
/**
* 应用流式布局
* @param {HTMLElement[]} steps - 步骤元素数组
*/
apply(steps) {
this.steps = steps;
switch (this.options.direction) {
case 'horizontal':
this.horizontalLayout();
break;
case 'vertical':
this.verticalLayout();
break;
case 'diagonal':
this.diagonalLayout();
break;
case 'spiral':
this.spiralLayout();
break;
default:
this.horizontalLayout();
}
return this.layoutMap;
}
/**
* 水平流式布局
*/
horizontalLayout() {
let currentX = 0;
let currentY = 0;
let currentRow = 0;
this.steps.forEach((step, index) => {
// 获取步骤尺寸
const width = this.getStepWidth(step);
const height = this.getStepHeight(step);
// 检查是否需要换行
if (index > 0 && index % this.options.maxPerRow === 0) {
currentX = 0;
currentY += this.options.rowHeight + this.options.spacing.y;
currentRow++;
}
// 计算位置
const x = currentX;
const y = currentY;
const z = currentRow * 200; // 每行在Z轴上稍有偏移,增加层次感
// 保存布局信息
this.layoutMap.set(step, { x, y, z, width, height });
// 更新下一个位置
currentX += width + this.options.spacing.x;
});
}
/**
* 垂直流式布局
*/
verticalLayout() {
let currentY = 0;
this.steps.forEach((step) => {
const height = this.getStepHeight(step);
// 垂直堆叠
this.layoutMap.set(step, {
x: 0,
y: currentY,
z: 0,
width: this.getStepWidth(step),
height
});
currentY += height + this.options.spacing.y;
});
}
/**
* 对角线布局
*/
diagonalLayout() {
this.steps.forEach((step, index) => {
const offset = index * 300;
this.layoutMap.set(step, {
x: offset,
y: offset,
z: index * 200,
width: this.getStepWidth(step),
height: this.getStepHeight(step)
});
});
}
/**
* 螺旋布局
*/
spiralLayout() {
const centerX = 0;
const centerY = 0;
const radius = 500;
const angleStep = (2 * Math.PI) / 8; // 每8步完成一圈
this.steps.forEach((step, index) {
const angle = index * angleStep;
const spiralRadius = radius + index * 100; // 半径逐渐增大
const x = centerX + spiralRadius * Math.cos(angle);
const y = centerY + spiralRadius * Math.sin(angle);
const z = index * 100; // Z轴逐渐升高
this.layoutMap.set(step, {
x,
y,
z,
width: this.getStepWidth(step),
height: this.getStepHeight(step)
});
});
}
/**
* 获取步骤宽度
*/
getStepWidth(step) {
// 优先使用data-width属性
if (step.dataset.width) {
return parseInt(step.dataset.width);
}
// 检查是否有特定宽度类
const widthClass = Array.from(step.classList).find(cls =>
cls.startsWith('width-')
);
if (widthClass) {
return parseInt(widthClass.replace('width-', ''));
}
// 默认宽度
return 800;
}
/**
* 获取步骤高度
*/
getStepHeight(step) {
// 优先使用data-height属性
if (step.dataset.height) {
return parseInt(step.dataset.height);
}
// 检查是否有特定高度类
const heightClass = Array.from(step.classList).find(cls =>
cls.startsWith('height-')
);
if (heightClass) {
return parseInt(heightClass.replace('height-', ''));
}
// 默认高度
return 600;
}
/**
* 应用布局到DOM
*/
applyToDOM() {
this.layoutMap.forEach((position, step) => {
// 设置data-*属性
step.dataset.x = position.x;
step.dataset.y = position.y;
step.dataset.z = position.z;
// 设置样式
step.style.width = `${position.width}px`;
step.style.height = `${position.height}px`;
});
return this;
}
}
// 使用示例
document.addEventListener('DOMContentLoaded', () => {
const steps = Array.from(document.querySelectorAll('.step'));
// 创建不同的布局实例
const layouts = {
horizontal: new FlowLayoutAdapter({
direction: 'horizontal',
maxPerRow: 3,
spacing: { x: 1200, y: 0, z: 0 }
}),
vertical: new FlowLayoutAdapter({
direction: 'vertical',
spacing: { x: 0, y: 1000, z: 0 }
}),
spiral: new FlowLayoutAdapter({
direction: 'spiral',
spacing: { x: 0, y: 0, z: 0 }
})
};
// 根据视口大小选择布局
const selectLayout = () => {
if (window.innerWidth < 768) {
return layouts.vertical;
} else if (window.innerWidth < 1200) {
return layouts.horizontal;
} else {
return layouts.spiral;
}
};
// 应用布局
const selectedLayout = selectLayout();
selectedLayout.apply(steps).applyToDOM();
// 重新初始化Impress.js
impress().init();
// 响应式调整
window.addEventListener('resize', () => {
const newLayout = selectLayout();
if (newLayout !== selectedLayout) {
newLayout.apply(steps).applyToDOM();
impress().init();
}
});
});
2.3 响应式布局系统
现代Web应用必须适应各种设备和屏幕尺寸,Impress.js同样需要强大的响应式支持:
javascript
/**
* Impress.js响应式适配系统
*/
class ResponsiveImpress {
constructor() {
this.breakpoints = {
mobile: { max: 767, layout: 'vertical' },
tablet: { min: 768, max: 1023, layout: 'grid' },
desktop: { min: 1024, max: 1439, layout: '3d' },
'desktop-lg': { min: 1440, layout: 'cinema' }
};
this.layoutStrategies = {
vertical: this.verticalStrategy.bind(this),
grid: this.gridStrategy.bind(this),
'3d': this.threeDStrategy.bind(this),
cinema: this.cinemaStrategy.bind(this)
};
this.currentLayout = null;
this.impressApi = null;
this.init();
}
init() {
// 检测初始布局
this.detectLayout();
// 监听窗口大小变化
window.addEventListener('resize', () => this.handleResize());
// 监听Impress.js初始化
document.addEventListener('impress:init', (event) => {
this.impressApi = event.detail.api;
this.applyLayout();
});
}
/**
* 检测当前应使用的布局
*/
detectLayout() {
const width = window.innerWidth;
for (const [name, bp] of Object.entries(this.breakpoints)) {
if ((bp.min === undefined || width >= bp.min) &&
(bp.max === undefined || width <= bp.max)) {
return name;
}
}
return 'desktop'; // 默认布局
}
/**
* 处理窗口大小变化
*/
handleResize() {
const newLayout = this.detectLayout();
if (newLayout !== this.currentLayout) {
this.currentLayout = newLayout;
this.applyLayout();
}
}
/**
* 应用布局策略
*/
applyLayout() {
const layoutName = this.currentLayout;
const breakpoint = this.breakpoints[layoutName];
const strategy = this.layoutStrategies[breakpoint.layout];
if (strategy) {
strategy();
// 重新初始化Impress.js
if (this.impressApi) {
this.impressApi.goto(0); // 回到第一页
setTimeout(() => {
// 确保布局更新后重新计算
this.impressApi.init();
}, 100);
}
}
}
/**
* 垂直布局策略(移动设备)
*/
verticalStrategy() {
const steps = document.querySelectorAll('.step');
let currentY = 0;
steps.forEach((step, index) => {
// 移除3D变换
delete step.dataset.rotateX;
delete step.dataset.rotateY;
delete step.dataset.rotateZ;
delete step.dataset.scale;
// 垂直排列
step.dataset.x = 0;
step.dataset.y = currentY;
step.dataset.z = 0;
// 更新样式
step.style.width = '90vw';
step.style.height = 'auto';
step.style.minHeight = '80vh';
step.style.margin = '0 auto';
// 更新下一个位置
currentY += 1000;
});
// 更新容器样式
document.getElementById('impress').style.transform = 'none';
}
/**
* 网格布局策略(平板设备)
*/
gridStrategy() {
const steps = document.querySelectorAll('.step');
const cols = 2;
const colWidth = 400;
const rowHeight = 300;
const gap = 50;
steps.forEach((step, index) => {
const row = Math.floor(index / cols);
const col = index % cols;
step.dataset.x = col * (colWidth + gap);
step.dataset.y = row * (rowHeight + gap);
step.dataset.z = 0;
// 轻微3D效果
step.dataset.rotateY = col % 2 === 0 ? -5 : 5;
step.dataset.rotateX = -2;
step.style.width = `${colWidth}px`;
step.style.height = `${rowHeight}px`;
});
}
/**
* 3D布局策略(桌面设备)
*/
threeDStrategy() {
const steps = document.querySelectorAll('.step');
const radius = 2000;
const angleStep = (2 * Math.PI) / steps.length;
steps.forEach((step, index) => {
const angle = index * angleStep;
// 圆环布局
step.dataset.x = Math.cos(angle) * radius;
step.dataset.y = Math.sin(angle) * radius * 0.5;
step.dataset.z = Math.sin(angle) * radius;
// 面向中心点
step.dataset.rotateY = (angle * 180 / Math.PI) + 90;
step.dataset.rotateX = -20;
step.style.width = '800px';
step.style.height = '600px';
});
}
/**
* 影院布局策略(大屏设备)
*/
cinemaStrategy() {
const steps = document.querySelectorAll('.step');
const depth = 5000;
steps.forEach((step, index) => {
// 深度层次布局
step.dataset.x = 0;
step.dataset.y = 0;
step.dataset.z = index * -depth;
// 透视效果
const scale = 1 + (index * 0.1);
step.dataset.scale = scale;
// 轻微旋转增加立体感
step.dataset.rotateY = index % 2 === 0 ? 10 : -10;
step.style.width = '1000px';
step.style.height = '700px';
});
}
/**
* 获取当前布局信息
*/
getLayoutInfo() {
return {
name: this.currentLayout,
breakpoint: this.breakpoints[this.currentLayout],
strategy: this.breakpoints[this.currentLayout].layout,
screenSize: {
width: window.innerWidth,
height: window.innerHeight,
dpr: window.devicePixelRatio
}
};
}
}
// 使用示例
const responsiveImpress = new ResponsiveImpress();
// 在控制台查看布局信息
console.log('当前布局:', responsiveImpress.getLayoutInfo());
// 手动切换布局
function switchLayout(layoutName) {
if (responsiveImpress.breakpoints[layoutName]) {
responsiveImpress.currentLayout = layoutName;
responsiveImpress.applyLayout();
}
}
第三章:3D空间设计与布局系统
3.1 3D坐标系与变换基础
Impress.js的核心魅力在于其3D空间能力。理解其坐标系是掌握高级3D效果的关键:
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Impress.js 3D坐标系详解</title>
<script src="https://cdn.jsdelivr.net/npm/impress.js@2.0.0/js/impress.min.js"></script>
<style>
body {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
margin: 0;
overflow: hidden;
font-family: 'Arial', sans-serif;
}
#impress {
perspective: 1000px;
}
.step {
width: 800px;
height: 600px;
padding: 40px;
box-sizing: border-box;
background: white;
border-radius: 20px;
box-shadow: 0 30px 60px rgba(0, 0, 0, 0.3);
transition: transform 0.8s, opacity 0.8s;
}
.coordinate-system {
position: absolute;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 15px;
border-radius: 10px;
font-family: 'Courier New', monospace;
}
.axis {
margin: 10px 0;
padding: 5px 10px;
border-radius: 5px;
font-weight: bold;
}
.axis-x { background: #ff4757; }
.axis-y { background: #2ed573; }
.axis-z { background: #1e90ff; }
.coordinate-display {
font-size: 1.2em;
margin: 5px 0;
}
.controls {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 10px;
z-index: 1000;
}
.control-btn {
background: rgba(255, 255, 255, 0.9);
border: none;
padding: 12px 24px;
border-radius: 50px;
cursor: pointer;
font-weight: bold;
transition: all 0.3s;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.control-btn:hover {
background: white;
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
}
.cube-visualization {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 200px;
height: 200px;
transform-style: preserve-3d;
animation: rotateCube 20s infinite linear;
}
.cube-face {
position: absolute;
width: 200px;
height: 200px;
background: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.3);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
font-weight: bold;
}
.front { transform: translateZ(100px); }
.back { transform: translateZ(-100px) rotateY(180deg); }
.right { transform: rotateY(90deg) translateZ(100px); }
.left { transform: rotateY(-90deg) translateZ(100px); }
.top { transform: rotateX(90deg) translateZ(100px); }
.bottom { transform: rotateX(-90deg) translateZ(100px); }
@keyframes rotateCube {
from { transform: translate(-50%, -50%) rotateX(0) rotateY(0); }
to { transform: translate(-50%, -50%) rotateX(360deg) rotateY(360deg); }
}
</style>
</head>
<body>
<!-- 3D坐标系展示 -->
<div id="impress">
<!-- X轴移动演示 -->
<div class="step" id="step-x"
data-x="-2000" data-y="0" data-z="0"
data-rotate-x="0" data-rotate-y="0" data-rotate-z="0">
<div class="coordinate-system">
<div class="axis axis-x">X轴: 水平方向</div>
<div class="coordinate-display">X: <span id="x-value">-2000</span></div>
<div class="coordinate-display">Y: 0</div>
<div class="coordinate-display">Z: 0</div>
</div>
<h1 style="color: #ff4757;">X轴移动</h1>
<p>沿X轴水平移动。负值向左,正值向右。</p>
<p>当前X坐标: <strong>-2000</strong></p>
<div class="cube-visualization">
<div class="cube-face front">前</div>
<div class="cube-face back">后</div>
<div class="cube-face right">右</div>
<div class="cube-face left">左</div>
<div class="cube-face top">上</div>
<div class="cube-face bottom">下</div>
</div>
</div>
<!-- Y轴移动演示 -->
<div class="step" id="step-y"
data-x="0" data-y="1500" data-z="0"
data-rotate-x="0" data-rotate-y="0" data-rotate-z="0">
<div class="coordinate-system">
<div class="axis axis-y">Y轴: 垂直方向</div>
<div class="coordinate-display">X: 0</div>
<div class="coordinate-display">Y: <span id="y-value">1500</span></div>
<div class="coordinate-display">Z: 0</div>
</div>
<h1 style="color: #2ed573;">Y轴移动</h1>
<p>沿Y轴垂直移动。负值向上,正值向下。</p>
<p>当前Y坐标: <strong>1500</strong></p>
</div>
<!-- Z轴移动演示 -->
<div class="step" id="step-z"
data-x="0" data-y="0" data-z="-1500"
data-rotate-x="0" data-rotate-y="0" data-rotate-z="0">
<div class="coordinate-system">
<div class="axis axis-z">Z轴: 深度方向</div>
<div class="coordinate-display">X: 0</div>
<div class="coordinate-display">Y: 0</div>
<div class="coordinate-display">Z: <span id="z-value">-1500</span></div>
</div>
<h1 style="color: #1e90ff;">Z轴移动</h1>
<p>沿Z轴深度移动。负值远离观众,正值靠近观众。</p>
<p>当前Z坐标: <strong>-1500</strong></p>
</div>
<!-- X轴旋转演示 -->
<div class="step" id="step-rotate-x"
data-x="2000" data-y="0" data-z="0"
data-rotate-x="45" data-rotate-y="0" data-rotate-z="0">
<div class="coordinate-system">
<div class="axis axis-x">绕X轴旋转</div>
<div class="coordinate-display">rotateX: <span id="rx-value">45°</span></div>
</div>
<h1 style="color: #ff4757;">X轴旋转</h1>
<p>绕X轴旋转元素。正值向下旋转,负值向上旋转。</p>
<p>当前旋转角度: <strong>45°</strong></p>
</div>
<!-- Y轴旋转演示 -->
<div class="step" id="step-rotate-y"
data-x="0" data-y="-1500" data-z="0"
data-rotate-x="0" data-rotate-y="60" data-rotate-z="0">
<div class="coordinate-system">
<div class="axis axis-y">绕Y轴旋转</div>
<div class="coordinate-display">rotateY: <span id="ry-value">60°</span></div>
</div>
<h1 style="color: #2ed573;">Y轴旋转</h1>
<p>绕Y轴旋转元素。正值向右旋转,负值向左旋转。</p>
<p>当前旋转角度: <strong>60°</strong></p>
</div>
<!-- Z轴旋转演示 -->
<div class="step" id="step-rotate-z"
data-x="0" data-y="0" data-z="1500"
data-rotate-x="0" data-rotate-y="0" data-rotate-z="30">
<div class="coordinate-system">
<div class="axis axis-z">绕Z轴旋转</div>
<div class="coordinate-display">rotateZ: <span id="rz-value">30°</span></div>
</div>
<h1 style="color: #1e90ff;">Z轴旋转</h1>
<p>绕Z轴旋转元素。正值顺时针旋转,负值逆时针旋转。</p>
<p>当前旋转角度: <strong>30°</strong></p>
</div>
<!-- 缩放演示 -->
<div class="step" id="step-scale"
data-x="2000" data-y="1500" data-z="0"
data-scale="2" data-rotate-x="0" data-rotate-y="0" data-rotate-z="0">
<div class="coordinate-system">
<div class="axis" style="background: #ffa502;">缩放变换</div>
<div class="coordinate-display">scale: <span id="scale-value">2</span></div>
</div>
<h1 style="color: #ffa502;">缩放变换</h1>
<p>缩放元素大小。1为原始大小,大于1放大,小于1缩小。</p>
<p>当前缩放比例: <strong>2</strong></p>
</div>
<!-- 组合变换演示 -->
<div class="step" id="step-combined"
data-x="0" data-y="1500" data-z="1500"
data-rotate-x="20" data-rotate-y="45" data-rotate-z="10"
data-scale="1.5">
<div class="coordinate-system">
<div class="axis" style="background: #3742fa;">组合变换</div>
<div class="coordinate-display">X: 0, Y: 1500, Z: 1500</div>
<div class="coordinate-display">rotateX: 20°, rotateY: 45°, rotateZ: 10°</div>
<div class="coordinate-display">scale: 1.5</div>
</div>
<h1 style="color: #3742fa;">组合变换</h1>
<p>同时应用多个3D变换,创建复杂的空间效果。</p>
<p>这是所有变换的组合展示。</p>
</div>
</div>
<!-- 控制按钮 -->
<div class="controls">
<button class="control-btn" onclick="navigateTo('step-x')">X轴</button>
<button class="control-btn" onclick="navigateTo('step-y')">Y轴</button>
<button class="control-btn" onclick="navigateTo('step-z')">Z轴</button>
<button class="control-btn" onclick="navigateTo('step-rotate-x')">旋转X</button>
<button class="control-btn" onclick="navigateTo('step-rotate-y')">旋转Y</button>
<button class="control-btn" onclick="navigateTo('step-rotate-z')">旋转Z</button>
<button class="control-btn" onclick="navigateTo('step-scale')">缩放</button>
<button class="control-btn" onclick="navigateTo('step-combined')">组合</button>
</div>
<script>
// 初始化Impress.js
impress().init();
// 导航函数
function navigateTo(stepId) {
const step = document.getElementById(stepId);
if (step) {
impress().goto(step);
}
}
// 更新坐标显示
function updateCoordinateDisplay() {
const currentStep = document.querySelector('.present');
if (!currentStep) return;
// 获取当前步骤的变换属性
const x = currentStep.dataset.x || '0';
const y = currentStep.dataset.y || '0';
const z = currentStep.dataset.z || '0';
const rotateX = currentStep.dataset.rotateX || '0';
const rotateY = currentStep.dataset.rotateY || '0';
const rotateZ = currentStep.dataset.rotateZ || '0';
const scale = currentStep.dataset.scale || '1';
// 更新显示
const xElement = document.getElementById('x-value');
const yElement = document.getElementById('y-value');
const zElement = document.getElementById('z-value');
const rxElement = document.getElementById('rx-value');
const ryElement = document.getElementById('ry-value');
const rzElement = document.getElementById('rz-value');
const scaleElement = document.getElementById('scale-value');
if (xElement) xElement.textContent = x;
if (yElement) yElement.textContent = y;
if (zElement) zElement.textContent = z;
if (rxElement) rxElement.textContent = rotateX + '°';
if (ryElement) ryElement.textContent = rotateY + '°';
if (rzElement) rzElement.textContent = rotateZ + '°';
if (scaleElement) scaleElement.textContent = scale;
}
// 监听步骤切换事件
document.addEventListener('impress:stepenter', updateCoordinateDisplay);
// 初始更新
setTimeout(updateCoordinateDisplay, 100);
</script>
</body>
</html>
3.2 高级3D布局模式
掌握了基础3D变换后,我们可以创建更复杂的3D布局模式:
javascript
/**
* 3D布局模式库
* 提供多种预定义的3D布局模式
*/
class ThreeDLayouts {
constructor() {
this.layouts = {
cube: this.cubeLayout.bind(this),
sphere: this.sphereLayout.bind(this),
helix: this.helixLayout.bind(this),
grid3d: this.grid3dLayout.bind(this),
carousel: this.carouselLayout.bind(this)
};
}
/**
* 立方体布局
* @param {HTMLElement[]} steps - 步骤元素数组
* @param {number} size - 立方体边长
*/
cubeLayout(steps, size = 2000) {
const faces = 6; // 立方体6个面
const stepsPerFace = Math.ceil(steps.length / faces);
steps.forEach((step, index) => {
const faceIndex = Math.floor(index / stepsPerFace);
const stepInFace = index % stepsPerFace;
let x, y, z, rotateX = 0, rotateY = 0;
// 根据面分配位置
switch (faceIndex) {
case 0: // 前面
x = (stepInFace - stepsPerFace/2) * (size/2);
y = 0;
z = size/2;
rotateY = 0;
break;
case 1: // 后面
x = (stepInFace - stepsPerFace/2) * (size/2);
y = 0;
z = -size/2;
rotateY = 180;
break;
case 2: // 右面
x = size/2;
y = (stepInFace - stepsPerFace/2) * (size/2);
z = 0;
rotateY = 90;
break;
case 3: // 左面
x = -size/2;
y = (stepInFace - stepsPerFace/2) * (size/2);
z = 0;
rotateY = -90;
break;
case 4: // 上面
x = (stepInFace - stepsPerFace/2) * (size/2);
y = -size/2;
z = 0;
rotateX = 90;
break;
case 5: // 下面
x = (stepInFace - stepsPerFace/2) * (size/2);
y = size/2;
z = 0;
rotateX = -90;
break;
}
this.applyTransform(step, { x, y, z, rotateX, rotateY });
});
}
/**
* 球面布局
* @param {HTMLElement[]} steps - 步骤元素数组
* @param {number} radius - 球体半径
*/
sphereLayout(steps, radius = 3000) {
const phi = Math.PI * (3 - Math.sqrt(5)); // 黄金角度
steps.forEach((step, index) => {
const y = 1 - (index / (steps.length - 1)) * 2; // y从1到-1
const radiusAtY = Math.sqrt(1 - y * y) * radius;
const theta = phi * index;
const x = Math.cos(theta) * radiusAtY;
const z = Math.sin(theta) * radiusAtY;
const yPos = y * radius;
// 计算朝向中心点的旋转
const rotateY = Math.atan2(x, z) * 180 / Math.PI;
const rotateX = -Math.asin(y) * 180 / Math.PI;
this.applyTransform(step, {
x,
y: yPos,
z,
rotateX,
rotateY,
rotateZ: 0
});
});
}
/**
* 螺旋布局
* @param {HTMLElement[]} steps - 步骤元素数组
* @param {number} radius - 螺旋半径
* @param {number} height - 螺旋高度
* @param {number} turns - 螺旋圈数
*/
helixLayout(steps, radius = 1500, height = 3000, turns = 3) {
steps.forEach((step, index) => {
const progress = index / (steps.length - 1);
const angle = turns * 2 * Math.PI * progress;
const currentHeight = height * progress;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const y = currentHeight - height / 2;
// 使元素朝向螺旋切线方向
const rotateY = (angle * 180 / Math.PI) - 90;
const rotateX = 20; // 稍微倾斜
this.applyTransform(step, {
x,
y,
z,
rotateX,
rotateY,
rotateZ: 0
});
});
}
/**
* 3D网格布局
* @param {HTMLElement[]} steps - 步骤元素数组
* @param {number} cols - 列数
* @param {number} rows - 行数
* @param {number} layers - 层数
* @param {number} spacing - 间距
*/
grid3dLayout(steps, cols = 3, rows = 3, layers = 3, spacing = 1000) {
const totalPositions = cols * rows * layers;
steps.forEach((step, index) => {
if (index >= totalPositions) return;
const layer = Math.floor(index / (cols * rows));
const row = Math.floor((index % (cols * rows)) / cols);
const col = index % cols;
const x = (col - (cols - 1) / 2) * spacing;
const y = (row - (rows - 1) / 2) * spacing;
const z = (layer - (layers - 1) / 2) * spacing;
// 根据位置添加轻微旋转
const rotateY = (col - (cols - 1) / 2) * 5;
const rotateX = (row - (rows - 1) / 2) * 3;
this.applyTransform(step, {
x,
y,
z,
rotateX,
rotateY,
rotateZ: 0
});
});
}
/**
* 旋转木马布局
* @param {HTMLElement[]} steps - 步骤元素数组
* @param {number} radius - 半径
* @param {number} tilt - 倾斜角度
*/
carouselLayout(steps, radius = 2000, tilt = 30) {
const angleStep = (2 * Math.PI) / steps.length;
steps.forEach((step, index) => {
const angle = index * angleStep;
const x = Math.cos(angle) * radius;
const z = Math.sin(angle) * radius;
const y = Math.sin(angle) * radius * Math.sin(tilt * Math.PI / 180);
// 始终朝向中心
const rotateY = (angle * 180 / Math.PI) - 90;
// 根据位置调整倾斜
const rotateX = tilt * Math.cos(angle);
this.applyTransform(step, {
x,
y,
z,
rotateX,
rotateY,
rotateZ: 0
});
});
}
/**
* 应用变换到元素
*/
applyTransform(element, transform) {
element.dataset.x = transform.x || 0;
element.dataset.y = transform.y || 0;
element.dataset.z = transform.z || 0;
if (transform.rotateX !== undefined) {
element.dataset.rotateX = transform.rotateX;
}
if (transform.rotateY !== undefined) {
element.dataset.rotateY = transform.rotateY;
}
if (transform.rotateZ !== undefined) {
element.dataset.rotateZ = transform.rotateZ;
}
if (transform.scale !== undefined) {
element.dataset.scale = transform.scale;
}
// 添加CSS类用于标识布局类型
element.classList.add('3d-layout');
}
/**
* 应用布局并重新初始化Impress.js
*/
applyLayout(layoutName, steps, options = {}) {
const layoutFunction = this.layouts[layoutName];
if (!layoutFunction) {
console.error(`布局 ${layoutName} 不存在`);
return;
}
const stepElements = Array.isArray(steps) ?
steps : Array.from(document.querySelectorAll('.step'));
layoutFunction(stepElements, options);
// 重新初始化Impress.js
if (window.impress) {
window.impress().init();
}
}
/**
* 创建布局选择器UI
*/
createLayoutSelector() {
const container = document.createElement('div');
container.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 15px;
border-radius: 10px;
z-index: 1000;
font-family: Arial, sans-serif;
`;
const title = document.createElement('div');
title.textContent = '3D布局选择器';
title.style.fontWeight = 'bold';
title.style.marginBottom = '10px';
container.appendChild(title);
const layouts = ['cube', 'sphere', 'helix', 'grid3d', 'carousel'];
layouts.forEach(layout => {
const button = document.createElement('button');
button.textContent = this.getLayoutName(layout);
button.style.cssText = `
display: block;
width: 100%;
margin: 5px 0;
padding: 8px 12px;
background: #3498db;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
transition: background 0.3s;
`;
button.onmouseover = () => button.style.background = '#2980b9';
button.onmouseout = () => button.style.background = '#3498db';
button.onclick = () => {
this.applyLayout(layout, null, {
size: 2000,
radius: 2500,
cols: 4,
rows: 3,
layers: 2
});
};
container.appendChild(button);
});
document.body.appendChild(container);
}
/**
* 获取布局的中文名称
*/
getLayoutName(layout) {
const names = {
cube: '立方体布局',
sphere: '球面布局',
helix: '螺旋布局',
grid3d: '3D网格布局',
carousel: '旋转木马布局'
};
return names[layout] || layout;
}
}
// 使用示例
document.addEventListener('DOMContentLoaded', () => {
const layoutManager = new ThreeDLayouts();
// 创建布局选择器
layoutManager.createLayoutSelector();
// 默认应用球面布局
layoutManager.applyLayout('sphere', null, { radius: 2500 });
});
3.3 3D场景管理
对于复杂的3D演示,我们需要一个场景管理系统来管理多个3D对象和动画:
javascript
/**
* 3D场景管理系统
* 管理多个3D对象、相机、灯光和动画
*/
class ThreeDSceneManager {
constructor(sceneId = 'impress') {
this.scene = document.getElementById(sceneId);
this.objects = new Map();
this.animations = new Map();
this.camera = {
x: 0, y: 0, z: 0,
rotateX: 0, rotateY: 0, rotateZ: 0,
fov: 1000
};
this.init();
}
init() {
// 设置场景样式
this.scene.style.perspective = `${this.camera.fov}px`;
this.scene.style.transformStyle = 'preserve-3d';
// 初始化所有对象
this.scene.querySelectorAll('.scene-object').forEach(object => {
this.addObject(object);
});
// 开始动画循环
this.animationLoop();
}
/**
* 添加3D对象到场景
*/
addObject(element, options = {}) {
const id = element.id || `obj_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
const object = {
element,
id,
type: options.type || 'static',
position: {
x: parseFloat(element.dataset.x) || 0,
y: parseFloat(element.dataset.y) || 0,
z: parseFloat(element.dataset.z) || 0
},
rotation: {
x: parseFloat(element.dataset.rotateX) || 0,
y: parseFloat(element.dataset.rotateY) || 0,
z: parseFloat(element.dataset.rotateZ) || 0
},
scale: parseFloat(element.dataset.scale) || 1,
velocity: options.velocity || { x: 0, y: 0, z: 0 },
angularVelocity: options.angularVelocity || { x: 0, y: 0, z: 0 },
animations: new Set(),
...options
};
this.objects.set(id, object);
this.updateObjectTransform(object);
return id;
}
/**
* 更新对象变换
*/
updateObjectTransform(object) {
const { position, rotation, scale } = object;
// 应用变换
object.element.style.transform = `
translate3d(${position.x}px, ${position.y}px, ${position.z}px)
rotateX(${rotation.x}deg)
rotateY(${rotation.y}deg)
rotateZ(${rotation.z}deg)
scale(${scale})
`;
// 更新data属性(保持与Impress.js兼容)
object.element.dataset.x = position.x;
object.element.dataset.y = position.y;
object.element.dataset.z = position.z;
object.element.dataset.rotateX = rotation.x;
object.element.dataset.rotateY = rotation.y;
object.element.dataset.rotateZ = rotation.z;
object.element.dataset.scale = scale;
}
/**
* 添加动画
*/
addAnimation(objectId, animation) {
const object = this.objects.get(objectId);
if (!object) return;
animation.id = animation.id || `anim_${Date.now()}`;
animation.startTime = Date.now();
animation.objectId = objectId;
object.animations.add(animation.id);
this.animations.set(animation.id, animation);
return animation.id;
}
/**
* 移除动画
*/
removeAnimation(animationId) {
const animation = this.animations.get(animationId);
if (!animation) return;
const object = this.objects.get(animation.objectId);
if (object) {
object.animations.delete(animationId);
}
this.animations.delete(animationId);
}
/**
* 创建平移动画
*/
createTranslationAnimation(target, duration = 1000, easing = 'easeInOut') {
return {
type: 'translation',
target,
duration,
easing,
update: (progress, animation) => {
const object = this.objects.get(animation.objectId);
if (!object) return;
// 计算插值
const start = object.position;
const end = animation.target;
const ease = this.easingFunctions[animation.easing];
const t = ease(progress);
object.position = {
x: start.x + (end.x - start.x) * t,
y: start.y + (end.y - start.y) * t,
z: start.z + (end.z - start.z) * t
};
this.updateObjectTransform(object);
}
};
}
/**
* 创建旋转动画
*/
createRotationAnimation(target, duration = 1000, easing = 'easeInOut') {
return {
type: 'rotation',
target,
duration,
easing,
update: (progress, animation) => {
const object = this.objects.get(animation.objectId);
if (!object) return;
const ease = this.easingFunctions[animation.easing];
const t = ease(progress);
object.rotation = {
x: object.rotation.x + (target.x - object.rotation.x) * t,
y: object.rotation.y + (target.y - object.rotation.y) * t,
z: object.rotation.z + (target.z - object.rotation.z) * t
};
this.updateObjectTransform(object);
}
};
}
/**
* 缓动函数
*/
easingFunctions = {
linear: t => t,
easeIn: t => t * t,
easeOut: t => t * (2 - t),
easeInOut: t => t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t,
easeInCubic: t => t * t * t,
easeOutCubic: t => (--t) * t * t + 1,
easeInOutCubic: t => t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
};
/**
* 动画循环
*/
animationLoop() {
const now = Date.now();
// 更新所有动画
this.animations.forEach((animation, id) => {
const elapsed = now - animation.startTime;
const progress = Math.min(elapsed / animation.duration, 1);
if (animation.update) {
animation.update(progress, animation);
}
// 动画完成
if (progress >= 1) {
this.removeAnimation(id);
}
});
// 更新物理模拟(如果启用)
this.updatePhysics();
// 继续下一帧
requestAnimationFrame(() => this.animationLoop());
}
/**
* 物理模拟
*/
updatePhysics() {
this.objects.forEach(object => {
if (object.type === 'dynamic') {
// 应用速度
object.position.x += object.velocity.x;
object.position.y += object.velocity.y;
object.position.z += object.velocity.z;
// 应用角速度
object.rotation.x += object.angularVelocity.x;
object.rotation.y += object.angularVelocity.y;
object.rotation.z += object.angularVelocity.z;
// 更新变换
this.updateObjectTransform(object);
}
});
}
/**
* 设置相机位置
*/
setCameraPosition(x, y, z) {
this.camera.x = x;
this.camera.y = y;
this.camera.z = z;
this.updateCamera();
}
/**
* 设置相机旋转
*/
setCameraRotation(x, y, z) {
this.camera.rotateX = x;
this.camera.rotateY = y;
this.camera.rotateZ = z;
this.updateCamera();
}
/**
* 更新相机变换
*/
updateCamera() {
// 实际相机变换是通过移动整个场景实现的
this.scene.style.transform = `
translate3d(${-this.camera.x}px, ${-this.camera.y}px, ${-this.camera.z}px)
rotateX(${this.camera.rotateX}deg)
rotateY(${this.camera.rotateY}deg)
rotateZ(${this.camera.rotateZ}deg)
`;
}
/**
* 创建轨道相机控制器
*/
createOrbitControls(targetObjectId) {
const target = this.objects.get(targetObjectId);
if (!target) return;
let isDragging = false;
let lastX = 0;
let lastY = 0;
const onMouseDown = (e) => {
isDragging = true;
lastX = e.clientX;
lastY = e.clientY;
e.preventDefault();
};
const onMouseMove = (e) => {
if (!isDragging) return;
const deltaX = e.clientX - lastX;
const deltaY = e.clientY - lastY;
// 旋转相机
this.camera.rotateY += deltaX * 0.5;
this.camera.rotateX += deltaY * 0.5;
// 限制X轴旋转角度
this.camera.rotateX = Math.max(-90, Math.min(90, this.camera.rotateX));
this.updateCamera();
lastX = e.clientX;
lastY = e.clientY;
};
const onMouseUp = () => {
isDragging = false;
};
const onWheel = (e) => {
// 滚轮缩放
const zoomSpeed = 0.1;
this.camera.z += e.deltaY * zoomSpeed;
this.updateCamera();
e.preventDefault();
};
// 添加事件监听器
document.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
document.addEventListener('wheel', onWheel);
// 返回清理函数
return () => {
document.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('wheel', onWheel);
};
}
}
// 使用示例:创建3D太阳系演示
document.addEventListener('DOMContentLoaded', () => {
const sceneManager = new ThreeDSceneManager('impress');
// 创建太阳
const sun = document.createElement('div');
sun.className = 'scene-object planet';
sun.id = 'sun';
sun.innerHTML = '<div class="planet-name">太阳</div>';
sun.style.cssText = `
width: 200px;
height: 200px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #ffd700, #ff8c00);
box-shadow: 0 0 100px #ff8c00;
`;
document.getElementById('impress').appendChild(sun);
const sunId = sceneManager.addObject(sun, {
type: 'static',
position: { x: 0, y: 0, z: 0 }
});
// 创建地球
const earth = document.createElement('div');
earth.className = 'scene-object planet';
earth.id = 'earth';
earth.innerHTML = '<div class="planet-name">地球</div>';
earth.style.cssText = `
width: 80px;
height: 80px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #6495ed, #1e90ff);
`;
document.getElementById('impress').appendChild(earth);
const earthId = sceneManager.addObject(earth, {
type: 'dynamic',
position: { x: 1000, y: 0, z: 0 }
});
// 创建月球
const moon = document.createElement('div');
moon.className = 'scene-object planet';
moon.id = 'moon';
moon.innerHTML = '<div class="planet-name">月球</div>';
moon.style.cssText = `
width: 30px;
height: 30px;
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, #d3d3d3, #a9a9a9);
`;
document.getElementById('impress').appendChild(moon);
const moonId = sceneManager.addObject(moon, {
type: 'dynamic',
position: { x: 1100, y: 0, z: 0 }
});
// 创建地球公转动画
let earthAngle = 0;
const earthOrbitRadius = 1000;
const earthOrbitSpeed = 0.001;
// 创建月球公转动画
let moonAngle = 0;
const moonOrbitRadius = 200;
const moonOrbitSpeed = 0.005;
// 动画更新函数
function updateOrbits(timestamp) {
// 地球公转
earthAngle += earthOrbitSpeed;
const earthObj = sceneManager.objects.get(earthId);
if (earthObj) {
earthObj.position.x = Math.cos(earthAngle) * earthOrbitRadius;
earthObj.position.z = Math.sin(earthAngle) * earthOrbitRadius;
sceneManager.updateObjectTransform(earthObj);
}
// 月球公转
moonAngle += moonOrbitSpeed;
const moonObj = sceneManager.objects.get(moonId);
if (moonObj && earthObj) {
// 月球围绕地球旋转
moonObj.position.x = earthObj.position.x + Math.cos(moonAngle) * moonOrbitRadius;
moonObj.position.z = earthObj.position.z + Math.sin(moonAngle) * moonOrbitRadius;
sceneManager.updateObjectTransform(moonObj);
}
// 自转
if (earthObj) {
earthObj.rotation.y += 0.5;
sceneManager.updateObjectTransform(earthObj);
}
if (moonObj) {
moonObj.rotation.y += 0.3;
sceneManager.updateObjectTransform(moonObj);
}
requestAnimationFrame(updateOrbits);
}
// 启动轨道动画
updateOrbits();
// 添加轨道控制器
const cleanupOrbitControls = sceneManager.createOrbitControls(sunId);
// 初始化Impress.js
impress().init();
// 集成Impress.js导航
document.addEventListener('impress:stepenter', (event) => {
const step = event.target;
if (step.id === 'sun-step') {
sceneManager.setCameraPosition(0, 0, 1000);
sceneManager.setCameraRotation(0, 0, 0);
} else if (step.id === 'earth-step') {
sceneManager.setCameraPosition(1500, 500, 1500);
sceneManager.setCameraRotation(-20, 45, 0);
}
});
});
第四章:模块化组件架构设计
4.1 组件注册与管理系统
现代Web应用需要组件化架构,Impress.js同样可以通过组件化提高代码复用性和可维护性:
javascript
/**
* Impress.js组件注册系统
* 提供完整的组件生命周期管理
*/
class ImpressComponentRegistry {
constructor() {
this.components = new Map();
this.instances = new Map();
this.componentIdCounter = 0;
this.eventBus = new EventTarget();
// 组件生命周期事件
this.lifecycleEvents = {
BEFORE_INIT: 'impress:component:before-init',
AFTER_INIT: 'impress:component:after-init',
BEFORE_DESTROY: 'impress:component:before-destroy',
AFTER_DESTROY: 'impress:component:after-destroy',
UPDATE: 'impress:component:update'
};
}
/**
* 注册组件
* @param {string} name - 组件名称
* @param {class} ComponentClass - 组件类
* @param {Object} options - 注册选项
*/
register(name, ComponentClass, options = {}) {
if (this.components.has(name)) {
console.warn(`组件 ${name} 已存在,将被覆盖`);
}
this.components.set(name, {
class: ComponentClass,
options,
metadata: {
version: options.version || '1.0.0',
author: options.author || 'unknown',
dependencies: options.dependencies || [],
description: options.description || ''
}
});
console.log(`✅ 组件注册成功: ${name} v${options.version || '1.0.0'}`);
return this;
}
/**
* 初始化页面上的所有组件
*/
initAll(context = document) {
const componentElements = context.querySelectorAll('[data-component]');
componentElements.forEach(element => {
this.initComponent(element);
});
// 监听动态添加的组件
this.setupMutationObserver(context);
return this.instances.size;
}
/**
* 初始化单个组件
*/
initComponent(element, force = false) {
const componentName = element.dataset.component;
if (!componentName) {
console.error('元素缺少 data-component 属性', element);
return null;
}
// 检查是否已初始化
if (element.dataset.componentId && !force) {
console.warn(`组件已初始化: ${element.dataset.componentId}`);
return this.instances.get(element.dataset.componentId);
}
const componentDef = this.components.get(componentName);
if (!componentDef) {
console.error(`组件未注册: ${componentName}`);
return null;
}
// 生成组件ID
const componentId = `component_${++this.componentIdCounter}`;
element.dataset.componentId = componentId;
// 解析配置
const config = this.parseComponentConfig(element, componentDef);
// 触发初始化前事件
this.eventBus.dispatchEvent(new CustomEvent(
this.lifecycleEvents.BEFORE_INIT,
{ detail: { element, componentName, config } }
));
// 创建组件实例
const instance = new componentDef.class(element, config);
instance.id = componentId;
instance.name = componentName;
instance.config = config;
// 调用组件初始化方法
if (typeof instance.init === 'function') {
instance.init();
}
// 存储实例
this.instances.set(componentId, instance);
// 触发初始化后事件
this.eventBus.dispatchEvent(new CustomEvent(
this.lifecycleEvents.AFTER_INIT,
{ detail: { instance, element, componentName, config } }
));
console.log(`🎯 组件初始化成功: ${componentName} (#${componentId})`);
return instance;
}
/**
* 解析组件配置
*/
parseComponentConfig(element, componentDef) {
const config = { ...componentDef.options.defaults };
// 从data-options属性解析JSON配置
if (element.dataset.options) {
try {
const options = JSON.parse(element.dataset.options);
Object.assign(config, options);
} catch (error) {
console.error('解析组件配置失败:', error);
}
}
// 从data属性提取配置
const dataAttrs = element.dataset;
Object.keys(dataAttrs).forEach(key => {
if (key.startsWith('config')) {
const configKey = key.replace('config', '').replace(/^[A-Z]/, match =>
match.toLowerCase()
);
config[configKey] = dataAttrs[key];
}
});
return config;
}
/**
* 设置DOM变化观察器
*/
setupMutationObserver(context) {
const observer = new MutationObserver((mutations) => {
mutations.forEach(mutation => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches('[data-component]')) {
this.initComponent(node);
}
// 检查子元素
node.querySelectorAll('[data-component]').forEach(element => {
this.initComponent(element);
});
}
});
// 清理被移除的组件
mutation.removedNodes.forEach(node => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.matches('[data-component]') && node.dataset.componentId) {
this.destroyComponent(node.dataset.componentId);
}
node.querySelectorAll('[data-component]').forEach(element => {
if (element.dataset.componentId) {
this.destroyComponent(element.dataset.componentId);
}
});
}
});
}
});
});
observer.observe(context, {
childList: true,
subtree: true
});
return observer;
}
/**
* 销毁组件
*/
destroyComponent(componentId) {
const instance = this.instances.get(componentId);
if (!instance) return false;
const element = instance.element;
// 触发销毁前事件
this.eventBus.dispatchEvent(new CustomEvent(
this.lifecycleEvents.BEFORE_DESTROY,
{ detail: { instance, element } }
));
// 调用组件销毁方法
if (typeof instance.destroy === 'function') {
instance.destroy();
}
// 清理DOM引用
if (element && element.dataset) {
delete element.dataset.componentId;
}
// 移除实例
this.instances.delete(componentId);
// 触发销毁后事件
this.eventBus.dispatchEvent(new CustomEvent(
this.lifecycleEvents.AFTER_DESTROY,
{ detail: { componentId, element } }
));
console.log(`🗑️ 组件销毁成功: #${componentId}`);
return true;
}
/**
* 获取组件实例
*/
getInstance(componentId) {
return this.instances.get(componentId);
}
/**
* 按名称获取所有组件实例
*/
getInstancesByName(name) {
const instances = [];
this.instances.forEach(instance => {
if (instance.name === name) {
instances.push(instance);
}
});
return instances;
}
/**
* 更新组件配置
*/
updateComponent(componentId, newConfig) {
const instance = this.instances.get(componentId);
if (!instance) return false;
const oldConfig = { ...instance.config };
instance.config = { ...oldConfig, ...newConfig };
// 调用组件更新方法
if (typeof instance.update === 'function') {
instance.update(instance.config, oldConfig);
}
// 触发更新事件
this.eventBus.dispatchEvent(new CustomEvent(
this.lifecycleEvents.UPDATE,
{ detail: { instance, oldConfig, newConfig } }
));
return true;
}
/**
* 添加事件监听
*/
on(event, callback) {
this.eventBus.addEventListener(event, callback);
return this;
}
/**
* 移除事件监听
*/
off(event, callback) {
this.eventBus.removeEventListener(event, callback);
return this;
}
/**
* 获取已注册组件列表
*/
getRegisteredComponents() {
const list = [];
this.components.forEach((def, name) => {
list.push({
name,
version: def.metadata.version,
description: def.metadata.description,
instanceCount: this.getInstancesByName(name).length
});
});
return list;
}
}
// 组件基类
class BaseComponent {
constructor(element, config = {}) {
this.element = element;
this.config = config;
this.initialized = false;
this.eventHandlers = new Map();
}
init() {
if (this.initialized) return;
this.setup();
this.bindEvents();
this.initialized = true;
return this;
}
setup() {
// 子类重写此方法
}
bindEvents() {
// 子类重写此方法
}
update(newConfig, oldConfig) {
// 子类重写此方法
this.config = { ...this.config, ...newConfig };
}
destroy() {
// 清理事件监听
this.unbindEvents();
// 清理DOM引用
this.element = null;
this.config = null;
this.initialized = false;
}
unbindEvents() {
this.eventHandlers.forEach((handler, event) => {
this.element.removeEventListener(event, handler);
});
this.eventHandlers.clear();
}
on(event, handler) {
this.eventHandlers.set(event, handler);
this.element.addEventListener(event, handler);
return this;
}
emit(event, detail) {
const customEvent = new CustomEvent(event, {
detail,
bubbles: true
});
this.element.dispatchEvent(customEvent);
return this;
}
query(selector) {
return this.element.querySelector(selector);
}
queryAll(selector) {
return this.element.querySelectorAll(selector);
}
addClass(className) {
this.element.classList.add(className);
return this;
}
removeClass(className) {
this.element.classList.remove(className);
return this;
}
toggleClass(className) {
this.element.classList.toggle(className);
return this;
}
}
4.2 实用组件库实现
基于组件系统,我们可以创建一系列实用的Impress.js组件:
javascript
/**
* 卡片组件
* 可复用的卡片式内容容器
*/
class CardComponent extends BaseComponent {
setup() {
// 创建卡片结构
this.element.innerHTML = `
<div class="card-container ${this.config.theme || ''}">
${this.config.header ? `
<div class="card-header">
${typeof this.config.header === 'string' ?
`<h3>${this.config.header}</h3>` :
this.config.header}
</div>
` : ''}
<div class="card-body">
${this.config.content || ''}
</div>
${this.config.footer ? `
<div class="card-footer">
${typeof this.config.footer === 'string' ?
this.config.footer : ''}
</div>
` : ''}
</div>
`;
// 应用样式
this.applyStyles();
}
applyStyles() {
const style = document.createElement('style');
style.textContent = `
.card-container {
width: 100%;
height: 100%;
border-radius: 16px;
overflow: hidden;
display: flex;
flex-direction: column;
background: white;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
}
.card-container:hover {
transform: translateY(-5px);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
}
.card-header {
padding: 20px 30px 0;
border-bottom: 1px solid #eee;
}
.card-header h3 {
margin: 0;
color: #333;
font-size: 1.5em;
}
.card-body {
flex: 1;
padding: 30px;
overflow: auto;
}
.card-footer {
padding: 20px 30px;
border-top: 1px solid #eee;
background: #f8f9fa;
}
/* 主题变体 */
.card-container.primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
.card-container.primary .card-header,
.card-container.primary .card-footer {
border-color: rgba(255, 255, 255, 0.2);
}
.card-container.dark {
background: #2c3e50;
color: white;
}
`;
document.head.appendChild(style);
}
updateContent(content) {
const body = this.query('.card-body');
if (body) {
body.innerHTML = content;
}
return this;
}
setHeader(header) {
const headerEl = this.query('.card-header');
if (headerEl) {
headerEl.innerHTML = typeof header === 'string' ?
`<h3>${header}</h3>` : header;
}
return this;
}
}
/**
* 计数器组件
* 带动画的数字计数器
*/
class CounterComponent extends BaseComponent {
setup() {
this.currentValue = this.config.start || 0;
this.targetValue = this.config.end || 100;
this.duration = this.config.duration || 2000;
this.format = this.config.format || (n => n.toLocaleString());
this.element.innerHTML = `
<div class="counter-wrapper">
<div class="counter-value">${this.format(this.currentValue)}</div>
${this.config.label ? `
<div class="counter-label">${this.config.label}</div>
` : ''}
</div>
`;
this.valueElement = this.query('.counter-value');
// 应用样式
this.applyStyles();
}
bindEvents() {
// 当进入包含此组件的步骤时开始计数
const step = this.findParentStep();
if (step) {
this.onStepEnter = () => this.start();
step.addEventListener('impress:stepenter', this.onStepEnter);
}
}
unbindEvents() {
const step = this.findParentStep();
if (step && this.onStepEnter) {
step.removeEventListener('impress:stepenter', this.onStepEnter);
}
super.unbindEvents();
}
findParentStep() {
let element = this.element;
while (element && !element.classList.contains('step')) {
element = element.parentElement;
}
return element;
}
start() {
if (this.animating) return;
this.animating = true;
const startTime = Date.now();
const animate = () => {
const elapsed = Date.now() - startTime;
const progress = Math.min(elapsed / this.duration, 1);
// 使用缓动函数
const easeOutCubic = t => 1 - Math.pow(1 - t, 3);
const easedProgress = easeOutCubic(progress);
this.currentValue = this.config.start +
(this.targetValue - this.config.start) * easedProgress;
this.updateDisplay();
if (progress < 1) {
requestAnimationFrame(animate);
} else {
this.animating = false;
this.emit('counter:complete', { value: this.currentValue });
}
};
requestAnimationFrame(animate);
}
updateDisplay() {
if (this.valueElement) {
this.valueElement.textContent = this.format(Math.round(this.currentValue));
}
}
applyStyles() {
const style = document.createElement('style');
style.textContent = `
.counter-wrapper {
text-align: center;
padding: 40px;
}
.counter-value {
font-size: 4em;
font-weight: bold;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 10px;
}
.counter-label {
font-size: 1.2em;
color: #666;
text-transform: uppercase;
letter-spacing: 2px;
}
`;
document.head.appendChild(style);
}
}
/**
* 图表容器组件
* 集成第三方图表库
*/
class ChartComponent extends BaseComponent {
setup() {
this.chartInstance = null;
this.data = this.config.data || [];
this.element.innerHTML = `
<div class="chart-wrapper">
<div class="chart-container" style="width: 100%; height: 100%;"></div>
${this.config.title ? `
<div class="chart-title">${this.config.title}</div>
` : ''}
${this.config.legend !== false ? `
<div class="chart-legend"></div>
` : ''}
</div>
`;
this.container = this.query('.chart-container');
this.initChart();
}
async initChart() {
const chartType = this.config.type || 'bar';
switch (chartType) {
case 'echarts':
await this.initECharts();
break;
case 'chartjs':
await this.initChartJS();
break;
default:
await this.initSimpleChart();
}
}
async initECharts() {
if (typeof echarts === 'undefined') {
await this.loadScript('https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js');
}
this.chartInstance = echarts.init(this.container);
const option = {
backgroundColor: 'transparent',
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: this.data.map(item => item.label || item.x)
},
yAxis: {
type: 'value'
},
series: [{
data: this.data.map(item => item.value || item.y),
type: this.config.chartType || 'bar',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#83bff6' },
{ offset: 0.5, color: '#188df0' },
{ offset: 1, color: '#188df0' }
])
}
}]
};
this.chartInstance.setOption(option);
// 响应式调整
window.addEventListener('resize', () => {
this.chartInstance.resize();
});
}
loadScript(url) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = url;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
update(data) {
this.data = data;
if (this.chartInstance) {
// 更新图表数据
this.chartInstance.setOption({
series: [{
data: data.map(item => item.value || item.y)
}]
});
}
}
}
/**
* 交互式时间线组件
*/
class TimelineComponent extends BaseComponent {
setup() {
this.events = this.config.events || [];
this.currentIndex = 0;
this.element.innerHTML = `
<div class="timeline-container">
<div class="timeline-track"></div>
<div class="timeline-events"></div>
<div class="timeline-controls">
<button class="timeline-prev">← 上一个</button>
<button class="timeline-next">下一个 →</button>
</div>
</div>
`;
this.track = this.query('.timeline-track');
this.eventsContainer = this.query('.timeline-events');
this.renderEvents();
this.highlightCurrent();
}
bindEvents() {
this.on('click', '.timeline-prev', () => this.prev());
this.on('click', '.timeline-next', () => this.next());
// 点击事件点
this.on('click', '.timeline-event', (e) => {
const index = parseInt(e.currentTarget.dataset.index);
this.goTo(index);
});
}
renderEvents() {
this.eventsContainer.innerHTML = '';
this.events.forEach((event, index) => {
const eventEl = document.createElement('div');
eventEl.className = 'timeline-event';
eventEl.dataset.index = index;
eventEl.innerHTML = `
<div class="event-marker"></div>
<div class="event-content">
<div class="event-date">${event.date}</div>
<div class="event-title">${event.title}</div>
${event.description ? `
<div class="event-description">${event.description}</div>
` : ''}
</div>
`;
this.eventsContainer.appendChild(eventEl);
});
}
highlightCurrent() {
this.queryAll('.timeline-event').forEach((el, index) => {
el.classList.toggle('active', index === this.currentIndex);
el.classList.toggle('past', index < this.currentIndex);
});
}
prev() {
if (this.currentIndex > 0) {
this.currentIndex--;
this.highlightCurrent();
this.emit('timeline:change', {
index: this.currentIndex,
event: this.events[this.currentIndex]
});
}
}
next() {
if (this.currentIndex < this.events.length - 1) {
this.currentIndex++;
this.highlightCurrent();
this.emit('timeline:change', {
index: this.currentIndex,
event: this.events[this.currentIndex]
});
}
}
goTo(index) {
if (index >= 0 && index < this.events.length) {
this.currentIndex = index;
this.highlightCurrent();
this.emit('timeline:change', {
index: this.currentIndex,
event: this.events[this.currentIndex]
});
}
}
}
4.3 组件使用示例
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Impress.js组件系统演示</title>
<script src="https://cdn.jsdelivr.net/npm/impress.js@2.0.0/js/impress.min.js"></script>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
margin: 0;
}
.step {
width: 900px;
height: 700px;
border-radius: 20px;
background: white;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
}
</style>
</head>
<body>
<div id="impress">
<!-- 卡片组件演示 -->
<div class="step" data-x="0" data-y="0" data-z="0">
<h1 style="text-align: center; margin-bottom: 50px;">卡片组件系统</h1>
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 30px; padding: 20px;">
<!-- 基础卡片 -->
<div data-component="card"
data-options='{
"header": "基础卡片",
"content": "这是一个基础卡片组件,支持多种配置选项。",
"theme": ""
}'>
</div>
<!-- 主题卡片 -->
<div data-component="card"
data-options='{
"header": "主题卡片",
"content": "使用渐变色主题的卡片。",
"theme": "primary"
}'>
</div>
<!-- 计数器卡片 -->
<div data-component="card"
data-options='{
"header": "数据统计",
"content": "<div data-component=\"counter\" data-options=\'{\"start\": 0, \"end\": 1000, \"duration\": 3000, \"label\": \"用户数量\"}\'></div>"
}'>
</div>
<!-- 图表卡片 -->
<div data-component="card"
data-options='{
"header": "销售数据",
"content": "<div data-component=\"chart\" data-options=\'{\"type\": \"echarts\", \"data\": [{\"label\": \"Q1\", \"value\": 100}, {\"label\": \"Q2\", \"value\": 200}, {\"label\": \"Q3\", \"value\": 150}, {\"label\": \"Q4\", \"value\": 300}]}\'></div>"
}'>
</div>
</div>
</div>
<!-- 时间线组件演示 -->
<div class="step" data-x="1500" data-y="0" data-z="0">
<h1 style="text-align: center; margin-bottom: 50px;">项目时间线</h1>
<div data-component="timeline"
style="height: 80%;"
data-options='{
"events": [
{
"date": "2024-01",
"title": "项目启动",
"description": "项目正式启动,组建核心团队"
},
{
"date": "2024-03",
"title": "需求分析",
"description": "完成需求调研和分析"
},
{
"date": "2024-06",
"title": "开发阶段",
"description": "核心功能开发完成"
},
{
"date": "2024-09",
"title": "测试阶段",
"description": "全面测试和bug修复"
},
{
"date": "2024-12",
"title": "正式上线",
"description": "项目正式发布上线"
}
]
}'>
</div>
</div>
</div>
<script>
// 创建组件注册器
const registry = new ImpressComponentRegistry();
// 注册组件
registry.register('card', CardComponent, {
version: '1.0.0',
description: '卡片容器组件',
defaults: {
theme: '',
header: '',
content: '',
footer: ''
}
});
registry.register('counter', CounterComponent, {
version: '1.0.0',
description: '数字计数器组件',
defaults: {
start: 0,
end: 100,
duration: 2000,
label: ''
}
});
registry.register('chart', ChartComponent, {
version: '1.0.0',
description: '图表组件',
defaults: {
type: 'echarts',
data: [],
title: ''
}
});
registry.register('timeline', TimelineComponent, {
version: '1.0.0',
description: '时间线组件',
defaults: {
events: []
}
});
// 初始化
document.addEventListener('DOMContentLoaded', () => {
// 初始化Impress.js
impress().init();
// 初始化组件
registry.initAll();
// 监听组件事件
registry.on('impress:component:after-init', (event) => {
console.log('组件初始化完成:', event.detail.componentName);
});
// 监听计数器完成事件
document.addEventListener('counter:complete', (event) => {
console.log('计数完成:', event.detail.value);
});
// 监听时间线变化
document.addEventListener('timeline:change', (event) => {
console.log('时间线切换到:', event.detail.index, event.detail.event);
});
// 在控制台查看组件状态
console.log('已注册组件:', registry.getRegisteredComponents());
});
</script>
</body>
</html>
第五章:插件生态系统设计
5.1 插件架构与生命周期管理
Impress.js的强大之处在于其可扩展性,通过插件系统可以轻松添加新功能:
javascript
/**
* Impress.js插件管理器
* 提供完整的插件生命周期管理
*/
class ImpressPluginManager {
constructor(impressApi) {
this.impress = impressApi;
this.plugins = new Map();
this.pluginOrder = [];
this.eventHandlers = new Map();
// 插件生命周期状态
this.states = {
REGISTERED: 'registered',
INITIALIZED: 'initialized',
ACTIVE: 'active',
DISABLED: 'disabled',
DESTROYED: 'destroyed'
};
this.init();
}
init() {
// 监听Impress.js事件
this.setupImpressEvents();
// 自动加载插件
this.autoLoadPlugins();
}
/**
* 注册插件
*/
register(plugin, options = {}) {
const pluginId = options.id || plugin.name || `plugin_${Date.now()}`;
if (this.plugins.has(pluginId)) {
console.warn(`插件 ${pluginId} 已存在,将被覆盖`);
}
const pluginInfo = {
id: pluginId,
instance: null,
config: {
enabled: options.enabled !== false,
priority: options.priority || 0,
dependencies: options.dependencies || [],
...options.config
},
state: this.states.REGISTERED,
metadata: {
name: plugin.name || pluginId,
version: options.version || '1.0.0',
description: options.description || '',
author: options.author || 'unknown'
}
};
this.plugins.set(pluginId, pluginInfo);
// 按优先级排序
this.pluginOrder = Array.from(this.plugins.values())
.sort((a, b) => b.config.priority - a.config.priority)
.map(p => p.id);
console.log(`✅ 插件注册成功: ${pluginInfo.metadata.name} v${pluginInfo.metadata.version}`);
// 如果启用,立即初始化
if (pluginInfo.config.enabled) {
this.initializePlugin(pluginId);
}
return pluginId;
}
/**
* 初始化插件
*/
initializePlugin(pluginId) {
const pluginInfo = this.plugins.get(pluginId);
if (!pluginInfo || pluginInfo.state !== this.states.REGISTERED) {
return false;
}
// 检查依赖
if (!this.checkDependencies(pluginId)) {
console.warn(`插件 ${pluginId} 依赖未满足,延迟初始化`);
return false;
}
try {
// 创建插件实例
const PluginClass = this.getPluginClass(pluginId);
pluginInfo.instance = new PluginClass(this.impress, pluginInfo.config);
// 调用初始化方法
if (typeof pluginInfo.instance.init === 'function') {
pluginInfo.instance.init();
}
pluginInfo.state = this.states.INITIALIZED;
// 激活插件
this.activatePlugin(pluginId);
console.log(`🎯 插件初始化成功: ${pluginInfo.metadata.name}`);
return true;
} catch (error) {
console.error(`插件初始化失败: ${pluginId}`, error);
pluginInfo.state = this.states.DISABLED;
return false;
}
}
/**
* 激活插件
*/
activatePlugin(pluginId) {
const pluginInfo = this.plugins.get(pluginId);
if (!pluginInfo || pluginInfo.state !== this.states.INITIALIZED) {
return false;
}
// 调用激活方法
if (typeof pluginInfo.instance.activate === 'function') {
pluginInfo.instance.activate();
}
// 注册事件处理器
this.registerEventHandlers(pluginId);
pluginInfo.state = this.states.ACTIVE;
console.log(`🚀 插件已激活: ${pluginInfo.metadata.name}`);
return true;
}
/**
* 禁用插件
*/
disablePlugin(pluginId) {
const pluginInfo = this.plugins.get(pluginId);
if (!pluginInfo || pluginInfo.state !== this.states.ACTIVE) {
return false;
}
// 调用停用方法
if (typeof pluginInfo.instance.deactivate === 'function') {
pluginInfo.instance.deactivate();
}
// 移除事件处理器
this.unregisterEventHandlers(pluginId);
pluginInfo.state = this.states.DISABLED;
console.log(`⏸️ 插件已禁用: ${pluginInfo.metadata.name}`);
return true;
}
/**
* 销毁插件
*/
destroyPlugin(pluginId) {
const pluginInfo = this.plugins.get(pluginId);
if (!pluginInfo) return false;
// 如果激活状态,先停用
if (pluginInfo.state === this.states.ACTIVE) {
this.disablePlugin(pluginId);
}
// 调用销毁方法
if (pluginInfo.instance && typeof pluginInfo.instance.destroy === 'function') {
pluginInfo.instance.destroy();
}
// 清理资源
this.unregisterEventHandlers(pluginId);
pluginInfo.instance = null;
pluginInfo.state = this.states.DESTROYED;
this.plugins.delete(pluginId);
this.pluginOrder = this.pluginOrder.filter(id => id !== pluginId);
console.log(`🗑️ 插件已销毁: ${pluginInfo.metadata.name}`);
return true;
}
/**
* 检查插件依赖
*/
checkDependencies(pluginId) {
const pluginInfo = this.plugins.get(pluginId);
if (!pluginInfo) return false;
const dependencies = pluginInfo.config.dependencies;
for (const dep of dependencies) {
const depPlugin = this.plugins.get(dep);
if (!depPlugin || depPlugin.state !== this.states.ACTIVE) {
return false;
}
}
return true;
}
/**
* 注册事件处理器
*/
registerEventHandlers(pluginId) {
const pluginInfo = this.plugins.get(pluginId);
if (!pluginInfo || !pluginInfo.instance) return;
const eventMap = pluginInfo.instance.events || {};
Object.entries(eventMap).forEach(([event, handler]) => {
const wrappedHandler = (e) => {
handler.call(pluginInfo.instance, e, this.impress, pluginInfo.config);
};
if (!this.eventHandlers.has(pluginId)) {
this.eventHandlers.set(pluginId, new Map());
}
this.eventHandlers.get(pluginId).set(event, wrappedHandler);
document.addEventListener(event, wrappedHandler);
});
}
/**
* 移除事件处理器
*/
unregisterEventHandlers(pluginId) {
const handlers = this.eventHandlers.get(pluginId);
if (!handlers) return;
handlers.forEach((handler, event) => {
document.removeEventListener(event, handler);
});
this.eventHandlers.delete(pluginId);
}
/**
* 获取插件类
*/
getPluginClass(pluginId) {
// 从全局对象或模块中获取
const pluginInfo = this.plugins.get(pluginId);
if (typeof pluginInfo.instance === 'function') {
return pluginInfo.instance;
}
// 尝试从注册的插件对象中获取
if (window.ImpressPlugins && window.ImpressPlugins[pluginId]) {
return window.ImpressPlugins[pluginId];
}
throw new Error(`找不到插件类: ${pluginId}`);
}
/**
* 设置Impress.js事件监听
*/
setupImpressEvents() {
const events = [
'impress:init',
'impress:stepenter',
'impress:stepleave',
'impress:steprefresh'
];
events.forEach(event => {
document.addEventListener(event, (e) => {
this.dispatchToPlugins(event, e);
});
});
}
/**
* 分发事件到所有插件
*/
dispatchToPlugins(event, data) {
this.pluginOrder.forEach(pluginId => {
const pluginInfo = this.plugins.get(pluginId);
if (pluginInfo.state === this.states.ACTIVE &&
pluginInfo.instance &&
typeof pluginInfo.instance.onEvent === 'function') {
try {
pluginInfo.instance.onEvent(event, data, this.impress);
} catch (error) {
console.error(`插件 ${pluginId} 处理事件 ${event} 时出错:`, error);
}
}
});
}
/**
* 自动加载插件
*/
autoLoadPlugins() {
// 从data-plugins属性加载
const impressElement = document.getElementById('impress');
if (impressElement && impressElement.dataset.plugins) {
const pluginNames = impressElement.dataset.plugins.split(',').map(p => p.trim());
pluginNames.forEach(pluginName => {
this.loadPlugin(pluginName);
});
}
}
/**
* 动态加载插件
*/
async loadPlugin(pluginName, options = {}) {
// 检查是否已注册
if (this.plugins.has(pluginName)) {
console.log(`插件 ${pluginName} 已加载`);
return this.plugins.get(pluginName);
}
// 动态加载插件脚本
if (options.url) {
await this.loadScript(options.url);
}
// 查找全局插件
if (window.ImpressPlugins && window.ImpressPlugins[pluginName]) {
return this.register(window.ImpressPlugins[pluginName], {
id: pluginName,
...options
});
}
console.error(`找不到插件: ${pluginName}`);
return null;
}
/**
* 获取插件状态
*/
getPluginStatus(pluginId) {
const pluginInfo = this.plugins.get(pluginId);
if (!pluginInfo) return null;
return {
id: pluginInfo.id,
name: pluginInfo.metadata.name,
version: pluginInfo.metadata.version,
state: pluginInfo.state,
config: pluginInfo.config
};
}
/**
* 获取所有插件状态
*/
getAllPluginStatus() {
const status = [];
this.plugins.forEach(pluginInfo => {
status.push(this.getPluginStatus(pluginInfo.id));
});
return status;
}
}
5.2 核心插件实现
javascript
/**
* 导航插件
* 增强导航功能和用户界面
*/
class NavigationPlugin {
constructor(impress, config) {
this.impress = impress;
this.config = {
showControls: true,
showProgress: true,
keyboardShortcuts: true,
touchGestures: true,
...config
};
this.controls = null;
this.progressBar = null;
this.currentStep = 0;
this.totalSteps = 0;
}
init() {
this.totalSteps = document.querySelectorAll('.step').length;
if (this.config.showControls) {
this.createNavigationControls();
}
if (this.config.showProgress) {
this.createProgressBar();
}
if (this.config.keyboardShortcuts) {
this.setupKeyboardShortcuts();
}
if (this.config.touchGestures) {
this.setupTouchGestures();
}
}
activate() {
this.updateNavigationState();
// 监听步骤变化
document.addEventListener('impress:stepenter', (e) => {
this.updateNavigationState();
});
}
createNavigationControls() {
this.controls = document.createElement('div');
this.controls.className = 'impress-navigation-controls';
this.controls.innerHTML = `
<button class="nav-btn prev" title="上一页 (←)">
<svg width="24" height="24" viewBox="0 0 24 24">
<path d="M15.41 16.59L10.83 12l4.58-4.59L14 6l-6 6 6 6 1.41-1.41z"/>
</svg>
</button>
<button class="nav-btn next" title="下一页 (→)">
<svg width="24" height="24" viewBox="0 0 24 24">
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/>
</svg>
</button>
<div class="nav-info">
<span class="current-step">1</span> / <span class="total-steps">${this.totalSteps}</span>
</div>
<button class="nav-btn fullscreen" title="全屏 (F)">
<svg width="24" height="24" viewBox="0 0 24 24">
<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>
</svg>
</button>
`;
this.controls.querySelector('.prev').addEventListener('click', () => this.prev());
this.controls.querySelector('.next').addEventListener('click', () => this.next());
this.controls.querySelector('.fullscreen').addEventListener('click', () => this.toggleFullscreen());
document.body.appendChild(this.controls);
// 添加样式
this.addStyles();
}
createProgressBar() {
this.progressBar = document.createElement('div');
this.progressBar.className = 'impress-progress-bar';
const fill = document.createElement('div');
fill.className = 'progress-fill';
this.progressBar.appendChild(fill);
document.body.appendChild(this.progressBar);
this.updateProgress();
}
setupKeyboardShortcuts() {
document.addEventListener('keydown', (e) => {
// 忽略在输入框中的按键
if (e.target.matches('input, textarea, select')) return;
switch (e.key) {
case 'ArrowLeft':
case 'PageUp':
e.preventDefault();
this.prev();
break;
case 'ArrowRight':
case 'PageDown':
case ' ':
e.preventDefault();
this.next();
break;
case 'Home':
e.preventDefault();
this.first();
break;
case 'End':
e.preventDefault();
this.last();
break;
case 'f':
case 'F':
if (e.ctrlKey || e.metaKey) return;
e.preventDefault();
this.toggleFullscreen();
break;
case '?':
e.preventDefault();
this.showHelp();
break;
}
});
}
setupTouchGestures() {
let startX = 0;
let startY = 0;
document.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
}, { passive: true });
document.addEventListener('touchend', (e) => {
if (!startX || !startY) return;
const endX = e.changedTouches[0].clientX;
const endY = e.changedTouches[0].clientY;
const diffX = startX - endX;
const diffY = startY - endY;
// 水平滑动超过50px,垂直滑动小于100px
if (Math.abs(diffX) > 50 && Math.abs(diffY) < 100) {
if (diffX > 0) {
this.next(); // 向左滑动
} else {
this.prev(); // 向右滑动
}
}
startX = 0;
startY = 0;
}, { passive: true });
}
updateNavigationState() {
const steps = Array.from(document.querySelectorAll('.step'));
const current = document.querySelector('.present');
this.currentStep = current ? steps.indexOf(current) + 1 : 1;
if (this.controls) {
const currentEl = this.controls.querySelector('.current-step');
if (currentEl) {
currentEl.textContent = this.currentStep;
}
// 更新按钮状态
this.controls.querySelector('.prev').disabled = this.currentStep === 1;
this.controls.querySelector('.next').disabled = this.currentStep === this.totalSteps;
}
this.updateProgress();
}
updateProgress() {
if (!this.progressBar) return;
const progress = ((this.currentStep - 1) / (this.totalSteps - 1)) * 100;
const fill = this.progressBar.querySelector('.progress-fill');
if (fill) {
fill.style.width = `${progress}%`;
}
}
prev() {
this.impress.prev();
}
next() {
this.impress.next();
}
first() {
this.impress.goto(0);
}
last() {
this.impress.goto(this.totalSteps - 1);
}
toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => {
console.log(`全屏请求失败: ${err.message}`);
});
} else {
document.exitFullscreen();
}
}
showHelp() {
const help = `
键盘快捷键:
← / PageUp - 上一页
→ / PageDown / 空格 - 下一页
Home - 第一页
End - 最后一页
F - 全屏切换
? - 显示此帮助
触摸手势:
左右滑动 - 切换页面
`;
alert(help);
}
addStyles() {
const style = document.createElement('style');
style.textContent = `
.impress-navigation-controls {
position: fixed;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
align-items: center;
gap: 10px;
background: rgba(0, 0, 0, 0.8);
padding: 10px 20px;
border-radius: 50px;
backdrop-filter: blur(10px);
z-index: 1000;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.3);
}
.nav-btn {
background: rgba(255, 255, 255, 0.1);
border: none;
color: white;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.nav-btn:hover:not(:disabled) {
background: rgba(255, 255, 255, 0.2);
transform: scale(1.1);
}
.nav-btn:disabled {
opacity: 0.3;
cursor: not-allowed;
}
.nav-btn svg {
fill: currentColor;
}
.nav-info {
color: white;
font-family: monospace;
font-size: 14px;
min-width: 60px;
text-align: center;
}
.impress-progress-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: rgba(255, 255, 255, 0.1);
z-index: 1000;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #667eea, #764ba2);
width: 0%;
transition: width 0.3s ease;
}
`;
document.head.appendChild(style);
}
deactivate() {
if (this.controls) {
this.controls.remove();
this.controls = null;
}
if (this.progressBar) {
this.progressBar.remove();
this.progressBar = null;
}
}
}
/**
* 分析插件
* 收集演示使用数据
*/
class AnalyticsPlugin {
constructor(impress, config) {
this.impress = impress;
this.config = {
endpoint: null,
autoSend: true,
trackTime: true,
trackInteractions: true,
...config
};
this.data = {
sessionId: this.generateSessionId(),
startTime: Date.now(),
steps: {},
events: []
};
this.currentStep = null;
this.stepEnterTime = null;
}
init() {
// 初始化步骤数据
document.querySelectorAll('.step').forEach((step, index) => {
this.data.steps[step.id || `step_${index}`] = {
id: step.id || `step_${index}`,
visits: 0,
totalTime: 0,
averageTime: 0
};
});
}
activate() {
this.trackStepEnter();
this.trackStepLeave();
this.trackInteractions();
// 页面可见性变化
document.addEventListener('visibilitychange', () => {
if (document.hidden) {
this.recordEvent('page_hidden');
} else {
this.recordEvent('page_visible');
}
});
// 页面卸载前发送数据
window.addEventListener('beforeunload', () => {
if (this.config.autoSend) {
this.sendAnalytics();
}
});
}
trackStepEnter() {
document.addEventListener('impress:stepenter', (e) => {
const step = e.target;
const stepId = step.id || this.getStepIndex(step);
// 记录离开上一个步骤的时间
if (this.currentStep && this.stepEnterTime) {
const timeSpent = Date.now() - this.stepEnterTime;
const stepData = this.data.steps[this.currentStep];
if (stepData) {
stepData.totalTime += timeSpent;
stepData.averageTime = stepData.totalTime / stepData.visits;
}
this.recordEvent('step_leave', {
stepId: this.currentStep,
timeSpent
});
}
// 记录进入新步骤
this.currentStep = stepId;
this.stepEnterTime = Date.now();
const stepData = this.data.steps[stepId];
if (stepData) {
stepData.visits++;
}
this.recordEvent('step_enter', {
stepId,
stepTitle: step.querySelector('h1, h2, h3')?.textContent || 'Untitled'
});
});
}
trackStepLeave() {
document.addEventListener('impress:stepleave', (e) => {
const step = e.target;
const stepId = step.id || this.getStepIndex(step);
this.recordEvent('step_leave_start', {
stepId,
nextStep: e.detail.next.id || this.getStepIndex(e.detail.next)
});
});
}
trackInteractions() {
if (!this.config.trackInteractions) return;
// 跟踪点击事件
document.addEventListener('click', (e) => {
const target = e.target;
const interactiveElement = this.findInteractiveElement(target);
if (interactiveElement) {
this.recordEvent('click', {
element: interactiveElement.tagName,
id: interactiveElement.id,
className: interactiveElement.className,
text: interactiveElement.textContent?.substring(0, 100)
});
}
}, true);
// 跟踪键盘事件
document.addEventListener('keydown', (e) => {
this.recordEvent('keydown', {
key: e.key,
code: e.code,
ctrlKey: e.ctrlKey,
shiftKey: e.shiftKey,
altKey: e.altKey,
metaKey: e.metaKey
});
});
}
findInteractiveElement(element) {
const interactiveTags = ['A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA'];
while (element && element !== document) {
if (interactiveTags.includes(element.tagName) ||
element.getAttribute('role') === 'button' ||
element.onclick) {
return element;
}
element = element.parentElement;
}
return null;
}
getStepIndex(step) {
const steps = Array.from(document.querySelectorAll('.step'));
return steps.indexOf(step);
}
recordEvent(type, data = {}) {
const event = {
type,
timestamp: Date.now(),
stepId: this.currentStep,
data
};
this.data.events.push(event);
// 触发自定义事件
document.dispatchEvent(new CustomEvent('impress:analytics:event', {
detail: event
}));
return event;
}
generateSessionId() {
return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
}
async sendAnalytics() {
if (!this.config.endpoint) {
console.log('分析数据:', this.data);
return;
}
try {
const response = await fetch(this.config.endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
...this.data,
endTime: Date.now(),
totalDuration: Date.now() - this.data.startTime
})
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
console.log('分析数据发送成功');
} catch (error) {
console.error('分析数据发送失败:', error);
}
}
getReport() {
const report = {
sessionId: this.data.sessionId,
totalSteps: Object.keys(this.data.steps).length,
totalEvents: this.data.events.length,
totalDuration: Date.now() - this.data.startTime,
steps: this.data.steps,
popularSteps: this.getPopularSteps(),
eventTypes: this.getEventTypes()
};
return report;
}
getPopularSteps() {
return Object.values(this.data.steps)
.sort((a, b) => b.visits - a.visits)
.slice(0, 5);
}
getEventTypes() {
const types = {};
this.data.events.forEach(event => {
types[event.type] = (types[event.type] || 0) + 1;
});
return types;
}
deactivate() {
if (this.config.autoSend) {
this.sendAnalytics();
}
}
}
/**
* 导出插件
* 支持将演示导出为PDF或图片
*/
class ExportPlugin {
constructor(impress, config) {
this.impress = impress;
this.config = {
formats: ['pdf', 'png', 'json'],
includeCSS: true,
includeScripts: false,
...config
};
this.exportUI = null;
}
init() {
this.createExportUI();
}
createExportUI() {
this.exportUI = document.createElement('div');
this.exportUI.className = 'impress-export-ui';
this.exportUI.innerHTML = `
<button class="export-btn" title="导出演示">
<svg width="24" height="24" viewBox="0 0 24 24">
<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>
</svg>
</button>
<div class="export-menu">
<div class="menu-header">导出选项</div>
<button class="menu-item" data-format="pdf">导出为PDF</button>
<button class="menu-item" data-format="png">导出为PNG</button>
<button class="menu-item" data-format="json">导出配置(JSON)</button>
<button class="menu-item" data-format="print">打印</button>
</div>
`;
const exportBtn = this.exportUI.querySelector('.export-btn');
const menu = this.exportUI.querySelector('.export-menu');
exportBtn.addEventListener('click', () => {
menu.classList.toggle('show');
});
// 菜单项点击事件
menu.querySelectorAll('.menu-item').forEach(item => {
item.addEventListener('click', (e) => {
const format = e.target.dataset.format;
this.export(format);
menu.classList.remove('show');
});
});
// 点击外部关闭菜单
document.addEventListener('click', (e) => {
if (!this.exportUI.contains(e.target)) {
menu.classList.remove('show');
}
});
document.body.appendChild(this.exportUI);
this.addStyles();
}
async export(format) {
switch (format) {
case 'pdf':
await this.exportToPDF();
break;
case 'png':
await this.exportToPNG();
break;
case 'json':
this.exportToJSON();
break;
case 'print':
this.print();
break;
}
}
async exportToPDF() {
// 使用html2canvas和jsPDF
if (typeof html2canvas === 'undefined') {
await this.loadScript('https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js');
}
if (typeof jspdf === 'undefined') {
await this.loadScript('https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js');
}
const { jsPDF } = window.jspdf;
const pdf = new jsPDF('l', 'mm', 'a4');
const steps = document.querySelectorAll('.step');
const originalTransforms = new Map();
// 保存原始变换
steps.forEach(step => {
originalTransforms.set(step, step.style.transform);
step.style.transform = 'none';
});
// 依次截图每个步骤
for (let i = 0; i < steps.length; i++) {
const canvas = await html2canvas(steps[i], {
scale: 2,
useCORS: true,
logging: false
});
const imgData = canvas.toDataURL('image/png');
const imgWidth = pdf.internal.pageSize.getWidth();
const imgHeight = (canvas.height * imgWidth) / canvas.width;
if (i > 0) {
pdf.addPage();
}
pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
}
// 恢复原始变换
steps.forEach(step => {
step.style.transform = originalTransforms.get(step);
});
pdf.save(`presentation-${Date.now()}.pdf`);
}
async exportToPNG() {
if (typeof html2canvas === 'undefined') {
await this.loadScript('https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js');
}
const steps = document.querySelectorAll('.step');
const originalTransforms = new Map();
// 保存原始变换
steps.forEach(step => {
originalTransforms.set(step, step.style.transform);
step.style.transform = 'none';
});
// 创建zip文件
const zip = new JSZip();
for (let i = 0; i < steps.length; i++) {
const canvas = await html2canvas(steps[i], {
scale: 2,
useCORS: true,
logging: false
});
canvas.toBlob(blob => {
zip.file(`slide-${i + 1}.png`, blob);
// 如果是最后一张,生成zip
if (i === steps.length - 1) {
zip.generateAsync({ type: 'blob' }).then(content => {
saveAs(content, `presentation-${Date.now()}.zip`);
});
}
}, 'image/png');
}
// 恢复原始变换
steps.forEach(step => {
step.style.transform = originalTransforms.get(step);
});
}
exportToJSON() {
const config = {
steps: [],
metadata: {
exported: new Date().toISOString(),
version: '1.0'
}
};
document.querySelectorAll('.step').forEach((step, index) => {
const stepConfig = {
id: step.id || `step_${index}`,
position: {
x: step.dataset.x || 0,
y: step.dataset.y || 0,
z: step.dataset.z || 0
},
rotation: {
x: step.dataset.rotateX || 0,
y: step.dataset.rotateY || 0,
z: step.dataset.rotateZ || 0
},
scale: step.dataset.scale || 1,
content: step.innerHTML
};
config.steps.push(stepConfig);
});
const dataStr = JSON.stringify(config, null, 2);
const dataUri = 'data:application/json;charset=utf-8,' + encodeURIComponent(dataStr);
const link = document.createElement('a');
link.setAttribute('href', dataUri);
link.setAttribute('download', `presentation-${Date.now()}.json`);
document.body.appendChild(link);
link.click();
link.remove();
}
print() {
const originalDisplay = document.getElementById('impress').style.display;
document.getElementById('impress').style.display = 'block';
window.print();
document.getElementById('impress').style.display = originalDisplay;
}
addStyles() {
const style = document.createElement('style');
style.textContent = `
.impress-export-ui {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
.export-btn {
background: #3498db;
border: none;
color: white;
width: 50px;
height: 50px;
border-radius: 50%;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(52, 152, 219, 0.3);
transition: all 0.3s ease;
}
.export-btn:hover {
background: #2980b9;
transform: scale(1.1);
box-shadow: 0 6px 20px rgba(52, 152, 219, 0.4);
}
.export-btn svg {
fill: currentColor;
}
.export-menu {
position: absolute;
bottom: 60px;
right: 0;
background: white;
border-radius: 10px;
box-shadow: 0 5px 20px rgba(0, 0, 0, 0.15);
min-width: 180px;
opacity: 0;
transform: translateY(10px);
visibility: hidden;
transition: all 0.3s ease;
}
.export-menu.show {
opacity: 1;
transform: translateY(0);
visibility: visible;
}
.menu-header {
padding: 12px 16px;
font-weight: bold;
color: #666;
border-bottom: 1px solid #eee;
}
.menu-item {
display: block;
width: 100%;
padding: 12px 16px;
border: none;
background: none;
text-align: left;
cursor: pointer;
color: #333;
transition: background 0.2s;
}
.menu-item:hover {
background: #f5f5f5;
}
`;
document.head.appendChild(style);
}
deactivate() {
if (this.exportUI) {
this.exportUI.remove();
this.exportUI = null;
}
}
}
5.3 插件使用示例
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Impress.js插件系统演示</title>
<script src="https://cdn.jsdelivr.net/npm/impress.js@2.0.0/js/impress.min.js"></script>
<style>
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%);
min-height: 100vh;
margin: 0;
color: white;
}
.step {
width: 800px;
height: 600px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.2);
padding: 40px;
box-sizing: border-box;
}
h1, h2, h3 {
margin-top: 0;
}
</style>
</head>
<body>
<div id="impress" data-plugins="navigation,analytics,export">
<!-- 欢迎页面 -->
<div class="step" id="welcome"
data-x="0" data-y="0" data-z="0"
data-rotate="0">
<h1>Impress.js插件系统演示</h1>
<p>本演示展示了Impress.js的强大插件系统</p>
<ul>
<li>导航插件 - 增强的导航控制</li>
<li>分析插件 - 使用数据收集</li>
<li>导出插件 - 多种格式导出</li>
</ul>
</div>
<!-- 功能页面1 -->
<div class="step" id="features-1"
data-x="1000" data-y="0" data-z="0"
data-rotate-y="45">
<h2>导航插件功能</h2>
<p>底部导航栏提供了完整的导航控制:</p>
<ul>
<li>上一页/下一页按钮</li>
<li>进度条显示</li>
<li>全屏切换</li>
<li>键盘快捷键支持</li>
<li>触摸手势支持</li>
</ul>
<p>按<strong>F</strong>键切换全屏,按<strong>?</strong>查看帮助。</p>
</div>
<!-- 功能页面2 -->
<div class="step" id="features-2"
data-x="0" data-y="1000" data-z="0"
data-rotate-x="45">
<h2>分析插件功能</h2>
<p>实时收集演示使用数据:</p>
<ul>
<li>步骤停留时间</li>
<li>用户交互记录</li>
<li>热门内容分析</li>
<li>使用行为统计</li>
</ul>
<p>打开控制台查看分析数据。</p>
</div>
<!-- 功能页面3 -->
<div class="step" id="features-3"
data-x="1000" data-y="1000" data-z="-500"
data-rotate-x="-45"
<!-- 功能页面3 -->
<div class="step" id="features-3"
data-x="1000" data-y="1000" data-z="-500"
data-rotate-x="-45" data-rotate-y="45">
<h2>导出插件功能</h2>
<p>支持多种格式导出:</p>
<ul>
<li>PDF文档导出</li>
<li>PNG图片序列</li>
<li>JSON配置导出</li>
<li>打印优化</li>
</ul>
<p>点击右下角的导出按钮尝试导出功能。</p>
<div style="margin-top: 40px; padding: 20px; background: rgba(255,255,255,0.1); border-radius: 10px;">
<h3>交互演示</h3>
<button onclick="showMessage()" style="padding: 10px 20px; background: #3498db; color: white; border: none; border-radius: 5px; cursor: pointer;">
点击我
</button>
<div id="message" style="margin-top: 20px; display: none;">
按钮被点击了!这个交互会被分析插件记录。
</div>
</div>
</div>
<!-- 数据展示页面 -->
<div class="step" id="analytics-demo"
data-x="0" data-y="0" data-z="-1000"
data-scale="1.5">
<h2>实时数据分析</h2>
<div id="analytics-display" style="margin-top: 30px;">
<div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 20px;">
<div class="metric">
<div class="metric-label">当前步骤</div>
<div class="metric-value" id="current-step">1</div>
</div>
<div class="metric">
<div class="metric-label">停留时间</div>
<div class="metric-value" id="time-spent">0s</div>
</div>
<div class="metric">
<div class="metric-label">总步骤数</div>
<div class="metric-value" id="total-steps">4</div>
</div>
<div class="metric">
<div class="metric-label">总交互次数</div>
<div class="metric-value" id="interaction-count">0</div>
</div>
</div>
<div style="margin-top: 40px;">
<h3>事件日志</h3>
<div id="event-log" style="height: 200px; overflow-y: auto; background: rgba(0,0,0,0.2); padding: 10px; border-radius: 5px; font-family: monospace; font-size: 12px;">
<!-- 事件日志将在这里显示 -->
</div>
</div>
</div>
</div>
<!-- 总结页面 -->
<div class="step" id="conclusion"
data-x="1500" data-y="0" data-z="-1500"
data-rotate-y="90">
<h2>插件系统总结</h2>
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 30px; margin-top: 40px;">
<div class="feature-card">
<div class="feature-icon">🚀</div>
<h3>易于扩展</h3>
<p>模块化设计,轻松添加新功能</p>
</div>
<div class="feature-card">
<div class="feature-icon">⚡</div>
<h3>性能优化</h3>
<p>按需加载,不影响核心性能</p>
</div>
<div class="feature-card">
<div class="feature-icon">🔄</div>
<h3>生命周期管理</h3>
<p>完整的初始化、激活、销毁流程</p>
</div>
</div>
<div style="margin-top: 50px; text-align: center;">
<button onclick="showAnalyticsReport()" style="padding: 15px 30px; background: #2ecc71; color: white; border: none; border-radius: 50px; font-size: 16px; cursor: pointer;">
查看分析报告
</button>
</div>
</div>
</div>
<script>
// 创建并初始化插件管理器
let pluginManager = null;
document.addEventListener('DOMContentLoaded', () => {
// 初始化Impress.js
const impressApi = impress().init();
// 创建插件管理器
pluginManager = new ImpressPluginManager(impressApi);
// 注册插件
pluginManager.register(NavigationPlugin, {
id: 'navigation',
version: '1.0.0',
description: '增强导航控制',
enabled: true,
priority: 100,
config: {
showControls: true,
showProgress: true,
keyboardShortcuts: true,
touchGestures: true
}
});
pluginManager.register(AnalyticsPlugin, {
id: 'analytics',
version: '1.0.0',
description: '使用数据分析',
enabled: true,
priority: 90,
config: {
endpoint: null, // 设置为实际API端点
autoSend: false,
trackTime: true,
trackInteractions: true
}
});
pluginManager.register(ExportPlugin, {
id: 'export',
version: '1.0.0',
description: '演示导出功能',
enabled: true,
priority: 80,
config: {
formats: ['pdf', 'png', 'json', 'print']
}
});
// 设置分析数据显示
setupAnalyticsDisplay();
// 显示插件状态
console.log('插件状态:', pluginManager.getAllPluginStatus());
});
// 分析数据显示
function setupAnalyticsDisplay() {
let interactionCount = 0;
let stepEnterTime = Date.now();
// 监听分析事件
document.addEventListener('impress:analytics:event', (event) => {
const data = event.detail;
// 更新事件日志
const logElement = document.getElementById('event-log');
if (logElement) {
const time = new Date(data.timestamp).toLocaleTimeString();
const logEntry = document.createElement('div');
logEntry.textContent = `[${time}] ${data.type}: ${JSON.stringify(data.data)}`;
logEntry.style.padding = '5px 0';
logEntry.style.borderBottom = '1px solid rgba(255,255,255,0.1)';
logElement.appendChild(logEntry);
// 保持滚动到底部
logElement.scrollTop = logElement.scrollHeight;
}
// 更新交互计数
if (data.type === 'click') {
interactionCount++;
document.getElementById('interaction-count').textContent = interactionCount;
}
});
// 监听步骤切换
document.addEventListener('impress:stepenter', (event) => {
const step = event.target;
const steps = document.querySelectorAll('.step');
const currentStep = Array.from(steps).indexOf(step) + 1;
document.getElementById('current-step').textContent = currentStep;
document.getElementById('total-steps').textContent = steps.length;
// 重置时间
stepEnterTime = Date.now();
updateTimeDisplay();
});
// 更新时间显示
function updateTimeDisplay() {
const timeSpent = Math.floor((Date.now() - stepEnterTime) / 1000);
document.getElementById('time-spent').textContent = `${timeSpent}s`;
setTimeout(updateTimeDisplay, 1000);
}
updateTimeDisplay();
}
function showMessage() {
document.getElementById('message').style.display = 'block';
}
function showAnalyticsReport() {
const analyticsPlugin = pluginManager.plugins.get('analytics');
if (analyticsPlugin && analyticsPlugin.instance) {
const report = analyticsPlugin.instance.getReport();
alert(JSON.stringify(report, null, 2));
}
}
</script>
<style>
.metric {
background: rgba(255, 255, 255, 0.1);
padding: 20px;
border-radius: 10px;
text-align: center;
}
.metric-label {
font-size: 14px;
opacity: 0.8;
margin-bottom: 10px;
}
.metric-value {
font-size: 32px;
font-weight: bold;
}
.feature-card {
background: rgba(255, 255, 255, 0.1);
padding: 30px;
border-radius: 15px;
text-align: center;
transition: transform 0.3s ease;
}
.feature-card:hover {
transform: translateY(-10px);
}
.feature-icon {
font-size: 48px;
margin-bottom: 20px;
}
</style>
</body>
</html>
5.4 插件开发最佳实践
在开发Impress.js插件时,遵循以下最佳实践可以确保插件的质量和兼容性:
插件设计原则
javascript
/**
* 插件开发最佳实践示例
*/
class WellDesignedPlugin {
constructor(impress, config) {
// 原则1:配置合并
this.config = {
// 默认配置
enabled: true,
debug: false,
// 用户配置覆盖
...config
};
// 原则2:保存Impress.js API引用
this.impress = impress;
// 原则3:状态管理
this.state = {
initialized: false,
active: false,
resources: new Set()
};
// 原则4:错误边界
this.errorHandler = this.errorHandler.bind(this);
}
/**
* 初始化方法 - 只做必要的初始化
*/
init() {
if (this.state.initialized) {
return;
}
try {
// 原则5:惰性初始化
this.initializeDOM();
this.initializeEvents();
this.state.initialized = true;
if (this.config.debug) {
console.log(`[${this.constructor.name}] 初始化完成`);
}
} catch (error) {
this.errorHandler('初始化失败', error);
}
}
/**
* 激活方法 - 按需激活功能
*/
activate() {
if (!this.state.initialized || this.state.active) {
return;
}
try {
// 原则6:功能按需激活
this.activateFeatures();
this.bindEventListeners();
this.state.active = true;
if (this.config.debug) {
console.log(`[${this.constructor.name}] 已激活`);
}
} catch (error) {
this.errorHandler('激活失败', error);
}
}
/**
* 停用方法 - 清理资源
*/
deactivate() {
if (!this.state.active) {
return;
}
try {
// 原则7:彻底清理
this.unbindEventListeners();
this.cleanupResources();
this.state.active = false;
if (this.config.debug) {
console.log(`[${this.constructor.name}] 已停用`);
}
} catch (error) {
this.errorHandler('停用失败', error);
}
}
/**
* 销毁方法 - 完全清理
*/
destroy() {
this.deactivate();
// 清理所有资源
this.state.resources.forEach(resource => {
this.releaseResource(resource);
});
this.state.resources.clear();
this.state.initialized = false;
// 原则8:清除引用
this.impress = null;
this.config = null;
}
/**
* 统一错误处理
*/
errorHandler(context, error) {
console.error(`[${this.constructor.name}] ${context}:`, error);
// 原则9:优雅降级
if (this.config.fallback) {
this.fallbackStrategy();
}
// 触发错误事件
this.emit('plugin:error', { context, error });
}
/**
* 事件发射器
*/
emit(event, data) {
const customEvent = new CustomEvent(event, {
detail: data,
bubbles: true
});
document.dispatchEvent(customEvent);
}
/**
* 资源管理
*/
trackResource(resource) {
this.state.resources.add(resource);
return resource;
}
releaseResource(resource) {
if (resource && typeof resource.dispose === 'function') {
resource.dispose();
}
this.state.resources.delete(resource);
}
/**
* 配置验证
*/
validateConfig(config) {
const schema = {
enabled: { type: 'boolean', default: true },
debug: { type: 'boolean', default: false },
// 更多配置验证规则...
};
const validated = {};
Object.keys(schema).forEach(key => {
const rule = schema[key];
const value = config[key];
if (value === undefined) {
validated[key] = rule.default;
} else if (typeof value !== rule.type) {
console.warn(`配置 ${key} 类型错误,期望 ${rule.type},得到 ${typeof value}`);
validated[key] = rule.default;
} else {
validated[key] = value;
}
});
return validated;
}
/**
* 性能监控
*/
measurePerformance(name, fn) {
const start = performance.now();
const result = fn();
const duration = performance.now() - start;
if (this.config.debug && duration > 16) { // 超过一帧时间
console.warn(`[${this.constructor.name}] ${name} 耗时 ${duration.toFixed(2)}ms`);
}
return result;
}
}
插件发布规范
javascript
/**
* 插件发布模板
*/
const PluginTemplate = {
// 必需字段
name: 'YourPluginName',
version: '1.0.0',
author: 'Your Name',
description: '插件功能描述',
license: 'MIT',
// 可选字段
homepage: 'https://github.com/yourname/yourplugin',
repository: {
type: 'git',
url: 'https://github.com/yourname/yourplugin.git'
},
bugs: {
url: 'https://github.com/yourname/yourplugin/issues'
},
keywords: [
'impressjs',
'impress.js',
'plugin',
'presentation'
],
// 依赖管理
dependencies: {
// 第三方库依赖
},
peerDependencies: {
'impress.js': '^2.0.0'
},
// 浏览器兼容性
browserslist: [
'last 2 versions',
'> 1%',
'not dead'
],
// 构建配置
build: {
entry: './src/index.js',
output: {
filename: 'yourplugin.min.js',
library: 'YourPlugin',
libraryTarget: 'umd'
}
},
// 插件主类
class: class YourPlugin {
constructor(impress, config) {
// 实现插件功能
}
init() {
// 初始化逻辑
}
activate() {
// 激活逻辑
}
deactivate() {
// 停用逻辑
}
destroy() {
// 销毁逻辑
}
}
};
// 自动注册机制
if (typeof window !== 'undefined' && window.ImpressPlugins) {
window.ImpressPlugins[PluginTemplate.name] = PluginTemplate.class;
}
第六章:企业级项目结构模板
6.1 项目架构设计
对于企业级Impress.js应用,我们需要一个结构清晰、可维护的项目架构:
impress-enterprise-project/
├── 📁 src/
│ ├── 📁 core/ # 核心模块
│ │ ├── impress-wrapper.js # Impress.js封装
│ │ ├── layout-engine.js # 布局引擎
│ │ ├── animation-controller.js # 动画控制器
│ │ └── event-system.js # 事件系统
│ │
│ ├── 📁 components/ # 可复用组件
│ │ ├── BaseComponent.js # 组件基类
│ │ ├── charts/ # 图表组件
│ │ │ ├── LineChart.js
│ │ │ ├── BarChart.js
│ │ │ └── PieChart.js
│ │ ├── media/ # 媒体组件
│ │ │ ├── VideoPlayer.js
│ │ │ └── AudioController.js
│ │ └── ui/ # UI组件
│ │ ├── Navigation.js
│ │ ├── ProgressBar.js
│ │ └── Toolbar.js
│ │
│ ├── 📁 plugins/ # 插件系统
│ │ ├── PluginManager.js # 插件管理器
│ │ ├── analytics/ # 分析插件
│ │ ├── export/ # 导出插件
│ │ └── collaboration/ # 协作插件
│ │
│ ├── 📁 scenes/ # 演示场景
│ │ ├── intro/ # 介绍部分
│ │ │ ├── scene-1.html
│ │ │ └── scene-1.js
│ │ ├── products/ # 产品展示
│ │ │ ├── scene-2.html
│ │ │ └── scene-2.js
│ │ └── conclusion/ # 总结部分
│ │ ├── scene-3.html
│ │ └── scene-3.js
│ │
│ ├── 📁 services/ # 服务层
│ │ ├── DataService.js # 数据服务
│ │ ├── AnalyticsService.js # 分析服务
│ │ ├── StorageService.js # 存储服务
│ │ └── ApiService.js # API服务
│ │
│ ├── 📁 utils/ # 工具函数
│ │ ├── dom-helpers.js # DOM操作
│ │ ├── animation-helpers.js # 动画辅助
│ │ ├── validation.js # 数据验证
│ │ └── logger.js # 日志工具
│ │
│ ├── 📁 styles/ # 样式文件
│ │ ├── base/ # 基础样式
│ │ │ ├── reset.css
│ │ │ ├── typography.css
│ │ │ └── variables.css
│ │ ├── components/ # 组件样式
│ │ ├── layouts/ # 布局样式
│ │ └── themes/ # 主题样式
│ │ ├── corporate.css # 企业主题
│ │ ├── creative.css # 创意主题
│ │ └── dark.css # 深色主题
│ │
│ └── 📁 assets/ # 静态资源
│ ├── 📁 data/ # 数据文件
│ ├── 📁 images/ # 图片资源
│ ├── 📁 fonts/ # 字体文件
│ └── 📁 videos/ # 视频资源
│
├── 📁 config/ # 配置文件
│ ├── impress.config.js # Impress.js配置
│ ├── webpack.config.js # 构建配置
│ ├── jest.config.js # 测试配置
│ └── eslint.config.js # 代码规范
│
├── 📁 tests/ # 测试文件
│ ├── unit/ # 单元测试
│ ├── integration/ # 集成测试
│ └── e2e/ # 端到端测试
│
├── 📁 docs/ # 项目文档
│ ├── api/ # API文档
│ ├── guides/ # 使用指南
│ └── examples/ # 示例代码
│
├── 📁 scripts/ # 构建脚本
│ ├── build.js # 构建脚本
│ ├── deploy.js # 部署脚本
│ └── generate.js # 生成脚本
│
├── 📄 package.json # 项目配置
├── 📄 README.md # 项目说明
├── 📄 CHANGELOG.md # 变更日志
└── 📄 LICENSE # 许可证
6.2 核心模块实现
企业级Impress.js封装
javascript
// src/core/impress-wrapper.js
class EnterpriseImpress {
constructor(config = {}) {
this.config = this.mergeConfig(config);
this.impressApi = null;
this.plugins = new PluginManager();
this.components = new ComponentRegistry();
this.services = new ServiceContainer();
this.state = {
initialized: false,
currentStep: 0,
totalSteps: 0,
isFullscreen: false,
isPresenterMode: false
};
this.init();
}
init() {
// 初始化服务
this.services.register('analytics', new AnalyticsService());
this.services.register('data', new DataService());
this.services.register('storage', new StorageService());
// 加载配置
this.loadConfig();
// 初始化Impress.js
this.initializeImpress();
// 初始化插件系统
this.initializePlugins();
// 初始化组件系统
this.initializeComponents();
// 设置事件监听
this.setupEventListeners();
this.state.initialized = true;
this.emit('impress:enterprise:initialized', {
config: this.config,
state: this.state
});
}
initializeImpress() {
// 创建Impress.js容器
this.createContainer();
// 初始化Impress.js
this.impressApi = impress();
// 应用配置
if (this.config.transitionDuration) {
this.impressApi.transitionDuration = this.config.transitionDuration;
}
// 调用原生init
this.impressApi.init();
// 更新状态
this.state.totalSteps = document.querySelectorAll('.step').length;
}
createContainer() {
if (document.getElementById('impress')) {
return;
}
const container = document.createElement('div');
container.id = 'impress';
container.dataset.config = JSON.stringify(this.config);
// 应用主题
if (this.config.theme) {
container.dataset.theme = this.config.theme;
}
// 应用布局
if (this.config.layout) {
container.dataset.layout = this.config.layout;
}
document.body.appendChild(container);
}
initializePlugins() {
// 加载核心插件
this.plugins.register('navigation', NavigationPlugin, {
config: this.config.plugins?.navigation
});
this.plugins.register('analytics', AnalyticsPlugin, {
config: this.config.plugins?.analytics
});
this.plugins.register('export', ExportPlugin, {
config: this.config.plugins?.export
});
// 加载自定义插件
if (this.config.plugins?.custom) {
Object.entries(this.config.plugins.custom).forEach(([name, plugin]) => {
this.plugins.register(name, plugin.class, plugin.config);
});
}
// 初始化所有插件
this.plugins.initAll(this.impressApi);
}
initializeComponents() {
// 注册核心组件
this.components.register('chart', ChartComponent);
this.components.register('media', MediaComponent);
this.components.register('interactive', InteractiveComponent);
// 自动初始化组件
this.components.autoInit();
}
setupEventListeners() {
// Impress.js事件
document.addEventListener('impress:stepenter', (e) => {
this.handleStepEnter(e);
});
document.addEventListener('impress:stepleave', (e) => {
this.handleStepLeave(e);
});
// 键盘事件
document.addEventListener('keydown', (e) => {
this.handleKeydown(e);
});
// 全屏事件
document.addEventListener('fullscreenchange', () => {
this.state.isFullscreen = !!document.fullscreenElement;
});
}
handleStepEnter(event) {
const step = event.target;
const stepIndex = this.getStepIndex(step);
this.state.currentStep = stepIndex;
// 触发自定义事件
this.emit('impress:enterprise:stepenter', {
step,
index: stepIndex,
data: step.dataset
});
// 更新服务
this.services.get('analytics').trackStepEnter(stepIndex);
}
handleStepLeave(event) {
const step = event.target;
const stepIndex = this.getStepIndex(step);
this.emit('impress:enterprise:stepleave', {
step,
index: stepIndex
});
}
handleKeydown(event) {
// 企业级快捷键
switch (event.key) {
case 'p':
case 'P':
if (event.ctrlKey) {
event.preventDefault();
this.togglePresenterMode();
}
break;
case 's':
case 'S':
if (event.ctrlKey) {
event.preventDefault();
this.saveState();
}
break;
case 'r':
case 'R':
if (event.ctrlKey) {
event.preventDefault();
this.reset();
}
break;
}
}
// 企业级功能
togglePresenterMode() {
this.state.isPresenterMode = !this.state.isPresenterMode;
if (this.state.isPresenterMode) {
this.enterPresenterMode();
} else {
this.exitPresenterMode();
}
this.emit('impress:enterprise:presentermode', {
enabled: this.state.isPresenterMode
});
}
enterPresenterMode() {
// 显示演讲者视图
this.showPresenterView();
// 启动计时器
this.startPresentationTimer();
// 显示演讲者备注
this.showSpeakerNotes();
}
saveState() {
const state = {
currentStep: this.state.currentStep,
timestamp: Date.now(),
config: this.config,
customData: this.getCustomState()
};
this.services.get('storage').set('impress_state', state);
this.emit('impress:enterprise:statesaved', { state });
}
loadState() {
const state = this.services.get('storage').get('impress_state');
if (state && state.currentStep) {
this.impressApi.goto(state.currentStep);
this.emit('impress:enterprise:stateloaded', { state });
}
}
reset() {
this.impressApi.goto(0);
this.state.currentStep = 0;
// 重置插件
this.plugins.reset();
// 重置组件
this.components.reset();
this.emit('impress:enterprise:reset');
}
export(format) {
const exporter = new Exporter(format, {
includeData: true,
includeStyles: true,
quality: 'high'
});
return exporter.export();
}
// 工具方法
getStepIndex(step) {
const steps = Array.from(document.querySelectorAll('.step'));
return steps.indexOf(step);
}
mergeConfig(userConfig) {
const defaultConfig = {
theme: 'corporate',
layout: 'responsive',
transitionDuration: 1000,
autoPlay: false,
loop: false,
plugins: {
navigation: { enabled: true },
analytics: { enabled: true },
export: { enabled: true }
},
services: {
analytics: { endpoint: '/api/analytics' },
storage: { type: 'localStorage' }
}
};
return deepMerge(defaultConfig, userConfig);
}
emit(event, data) {
const customEvent = new CustomEvent(event, {
detail: data,
bubbles: true
});
document.dispatchEvent(customEvent);
}
}
服务容器设计
javascript
// src/core/service-container.js
class ServiceContainer {
constructor() {
this.services = new Map();
this.instances = new Map();
this.dependencies = new Map();
}
register(name, ServiceClass, config = {}) {
this.services.set(name, {
class: ServiceClass,
config,
dependencies: ServiceClass.dependencies || []
});
// 构建依赖图
this.buildDependencyGraph();
return this;
}
get(name) {
// 如果已经实例化,直接返回
if (this.instances.has(name)) {
return this.instances.get(name);
}
// 创建实例
const serviceDef = this.services.get(name);
if (!serviceDef) {
throw new Error(`Service ${name} not registered`);
}
// 检查依赖
this.checkDependencies(name);
// 创建依赖实例
const dependencies = {};
serviceDef.dependencies.forEach(depName => {
dependencies[depName] = this.get(depName);
});
// 创建服务实例
const instance = new serviceDef.class({
...serviceDef.config,
dependencies
});
// 存储实例
this.instances.set(name, instance);
// 初始化服务
if (typeof instance.init === 'function') {
instance.init();
}
return instance;
}
checkDependencies(serviceName) {
const visited = new Set();
const check = (name) => {
if (visited.has(name)) {
throw new Error(`Circular dependency detected: ${serviceName}`);
}
visited.add(name);
const serviceDef = this.services.get(name);
if (!serviceDef) {
throw new Error(`Dependency ${name} not registered`);
}
serviceDef.dependencies.forEach(dep => {
check(dep);
});
};
check(serviceName);
}
buildDependencyGraph() {
this.dependencies.clear();
this.services.forEach((def, name) => {
this.dependencies.set(name, new Set(def.dependencies));
});
}
destroy(name) {
const instance = this.instances.get(name);
if (instance && typeof instance.destroy === 'function') {
instance.destroy();
}
this.instances.delete(name);
}
destroyAll() {
this.instances.forEach((instance, name) => {
this.destroy(name);
});
}
}
第七章:性能优化与最佳实践
7.1 性能优化策略
懒加载与代码分割
javascript
// src/utils/lazy-loader.js
class LazyLoader {
constructor(options = {}) {
this.options = {
threshold: 0.1,
rootMargin: '200px',
loadingClass: 'loading',
loadedClass: 'loaded',
errorClass: 'error',
...options
};
this.observers = new Map();
this.cache = new Map();
}
// 图片懒加载
lazyLoadImages(container = document) {
const images = container.querySelectorAll('img[data-src]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
this.loadImage(img);
observer.unobserve(img);
}
});
}, this.options);
images.forEach(img => {
// 添加加载状态
img.classList.add(this.options.loadingClass);
observer.observe(img);
});
this.observers.set('images', observer);
}
async loadImage(img) {
const src = img.dataset.src;
try {
// 检查缓存
if (this.cache.has(src)) {
img.src = this.cache.get(src);
return;
}
// 预加载
const image = new Image();
await new Promise((resolve, reject) => {
image.onload = resolve;
image.onerror = reject;
image.src = src;
});
// 设置图片源
img.src = src;
this.cache.set(src, src);
// 更新状态
img.classList.remove(this.options.loadingClass);
img.classList.add(this.options.loadedClass);
} catch (error) {
console.error('图片加载失败:', src, error);
img.classList.remove(this.options.loadingClass);
img.classList.add(this.options.errorClass);
}
}
// 组件懒加载
lazyLoadComponents(container = document) {
const components = container.querySelectorAll('[data-lazy-component]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(async entry => {
if (entry.isIntersecting) {
const element = entry.target;
await this.loadComponent(element);
observer.unobserve(element);
}
});
}, this.options);
components.forEach(component => {
observer.observe(component);
});
this.observers.set('components', observer);
}
async loadComponent(element) {
const componentName = element.dataset.lazyComponent;
const componentUrl = element.dataset.componentUrl;
try {
// 动态导入组件
const module = await import(componentUrl);
const ComponentClass = module.default;
// 初始化组件
const component = new ComponentClass(element);
if (typeof component.init === 'function') {
component.init();
}
// 标记为已加载
element.dataset.lazyLoaded = 'true';
} catch (error) {
console.error(`组件加载失败: ${componentName}`, error);
}
}
// 视频懒加载
lazyLoadVideos(container = document) {
const videos = container.querySelectorAll('video[data-src]');
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const video = entry.target;
this.loadVideo(video);
observer.unobserve(video);
}
});
}, this.options);
videos.forEach(video => {
observer.observe(video);
});
this.observers.set('videos', observer);
}
loadVideo(video) {
const sources = video.querySelectorAll('source[data-src]');
sources.forEach(source => {
const src = source.dataset.src;
source.src = src;
});
video.load();
video.dataset.loaded = 'true';
}
// 清理资源
destroy() {
this.observers.forEach(observer => {
observer.disconnect();
});
this.observers.clear();
this.cache.clear();
}
}
第八章:总结与展望
8.1 技术总结
通过本篇超过15000字的深度解析,我们全面探讨了Impress.js的架构设计与企业级实践。主要内容总结如下:
-
架构哲学:从Prezi的无限画布理念到Impress.js的三维空间思维,我们理解了现代演示框架的设计思想。
-
结构化布局系统:
-
2D网格与流式布局系统
-
响应式适配策略
-
自动布局算法
-
-
3D空间设计:
-
三维坐标系详解
-
多种3D布局模式(立方体、球面、螺旋等)
-
3D场景管理系统
-
-
模块化组件架构:
-
组件注册与生命周期管理
-
可复用的UI组件库
-
数据驱动的组件设计
-
-
插件生态系统:
-
插件管理器设计
-
核心插件实现(导航、分析、导出)
-
插件开发最佳实践
-
-
企业级项目结构:
-
完整的项目架构模板
-
核心服务封装
-
构建与部署流程
-
-
性能优化:
-
懒加载策略
-
渲染性能优化
-
内存管理
-
-
质量保证:
-
代码规范与测试策略
-
持续集成与部署
-
8.2 技术优势
Impress.js企业级方案的主要优势:
-
视觉表现力:超越传统PPT的3D效果,提供沉浸式演示体验
-
技术先进性:基于现代Web技术栈,支持响应式设计和移动端适配
-
架构灵活性:模块化设计支持快速定制和扩展
-
企业级特性:完整的插件系统、数据分析、导出功能等
-
性能优越:优化的渲染性能,支持大型复杂演示
-
开发友好:完整的工具链和开发文档