diff --git a/src/main/java/dev/bytevibe/hyperpoint/Controller.java b/src/main/java/dev/bytevibe/hyperpoint/Controller.java
index b49316f..aa5659c 100644
--- a/src/main/java/dev/bytevibe/hyperpoint/Controller.java
+++ b/src/main/java/dev/bytevibe/hyperpoint/Controller.java
@@ -470,6 +470,86 @@ public class Controller implements Initializable {
}
}
+ /**
+ * 开始演示(默认淡出淡入效果)
+ */
+ @FXML
+ public void onStartPresentation(ActionEvent actionEvent) {
+ if (currentSlide == null || currentSlide.getPages().isEmpty()) {
+ showWarning("无幻灯片", "请先创建或打开一个幻灯片。");
+ return;
+ }
+ startPresentation(TransitionEffect.FADE);
+ }
+
+ /**
+ * 使用淡出淡入效果播放演示
+ */
+ @FXML
+ public void onPresentationWithFade(ActionEvent actionEvent) {
+ if (currentSlide == null || currentSlide.getPages().isEmpty()) {
+ showWarning("无幻灯片", "请先创建或打开一个幻灯片。");
+ return;
+ }
+ startPresentation(TransitionEffect.FADE);
+ }
+
+ /**
+ * 使用从右推入效果播放演示
+ */
+ @FXML
+ public void onPresentationWithPushLeft(ActionEvent actionEvent) {
+ if (currentSlide == null || currentSlide.getPages().isEmpty()) {
+ showWarning("无幻灯片", "请先创建或打开一个幻灯片。");
+ return;
+ }
+ startPresentation(TransitionEffect.PUSH_LEFT);
+ }
+
+ /**
+ * 使用从左推入效果播放演示
+ */
+ @FXML
+ public void onPresentationWithPushRight(ActionEvent actionEvent) {
+ if (currentSlide == null || currentSlide.getPages().isEmpty()) {
+ showWarning("无幻灯片", "请先创建或打开一个幻灯片。");
+ return;
+ }
+ startPresentation(TransitionEffect.PUSH_RIGHT);
+ }
+
+ /**
+ * 使用缩放放大效果播放演示
+ */
+ @FXML
+ public void onPresentationWithZoomIn(ActionEvent actionEvent) {
+ if (currentSlide == null || currentSlide.getPages().isEmpty()) {
+ showWarning("无幻灯片", "请先创建或打开一个幻灯片。");
+ return;
+ }
+ startPresentation(TransitionEffect.ZOOM_IN);
+ }
+
+ /**
+ * 使用旋转翻页效果播放演示
+ */
+ @FXML
+ public void onPresentationWithRotate(ActionEvent actionEvent) {
+ if (currentSlide == null || currentSlide.getPages().isEmpty()) {
+ showWarning("无幻灯片", "请先创建或打开一个幻灯片。");
+ return;
+ }
+ startPresentation(TransitionEffect.ROTATE);
+ }
+
+ /**
+ * 启动演示
+ */
+ private void startPresentation(TransitionEffect transition) {
+ PresentationWindow presentationWindow = new PresentationWindow(currentSlide, transition);
+ presentationWindow.show();
+ }
+
/**
* 显示警告对话框
*/
diff --git a/src/main/java/dev/bytevibe/hyperpoint/PresentationWindow.java b/src/main/java/dev/bytevibe/hyperpoint/PresentationWindow.java
new file mode 100644
index 0000000..cc782a4
--- /dev/null
+++ b/src/main/java/dev/bytevibe/hyperpoint/PresentationWindow.java
@@ -0,0 +1,677 @@
+package dev.bytevibe.hyperpoint;
+
+import javafx.animation.*;
+import javafx.geometry.Bounds;
+import javafx.scene.Scene;
+import javafx.scene.canvas.Canvas;
+import javafx.scene.canvas.GraphicsContext;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.StackPane;
+import javafx.scene.paint.Color;
+import javafx.scene.transform.Rotate;
+import javafx.stage.Stage;
+import javafx.stage.StageStyle;
+import javafx.util.Duration;
+
+/**
+ * 全屏演示窗口,支持翻页和动画效果
+ */
+public class PresentationWindow {
+ private final Slide slide;
+ private final Stage stage;
+ private final Canvas canvas;
+ private final StackPane root;
+ private int currentPageIndex = 0;
+ private TransitionEffect currentTransition = TransitionEffect.FADE;
+ private boolean isTransitioning = false;
+
+ public PresentationWindow(Slide slide, TransitionEffect transition) {
+ this.slide = slide;
+ this.currentTransition = transition;
+ this.stage = new Stage();
+ this.canvas = new Canvas();
+ this.root = new StackPane();
+
+ initializeStage();
+ }
+
+ private void initializeStage() {
+ // 页面标准宽高比 (4:3)
+ final double PAGE_WIDTH = 1024;
+ final double PAGE_HEIGHT = 768;
+ final double ASPECT_RATIO = PAGE_WIDTH / PAGE_HEIGHT; // 4:3 = 1.333...
+
+ // 设置全屏,获取屏幕分辨率
+ stage.setFullScreen(true);
+ stage.setFullScreenExitHint("");
+ stage.initStyle(StageStyle.UNDECORATED);
+
+ // 绑定Canvas大小到root,Canvas会根据root大小自动调整
+ canvas.widthProperty().bind(root.widthProperty());
+ canvas.heightProperty().bind(root.heightProperty());
+
+ // 添加键盘和鼠标事件处理
+ root.setOnKeyPressed(this::handleKeyEvent);
+ root.setOnMouseClicked(this::handleMouseClick);
+
+ root.getChildren().add(canvas);
+ root.setStyle("-fx-background-color: #000000;");
+ root.setFocusTraversable(true);
+
+ Scene scene = new Scene(root);
+ stage.setScene(scene);
+
+ // 监听Scene大小变化,计算缩放因子
+ scene.widthProperty().addListener((obs, oldVal, newVal) -> drawCurrentPage(null));
+ scene.heightProperty().addListener((obs, oldVal, newVal) -> drawCurrentPage(null));
+
+ // 显示第一页
+ drawCurrentPage(null);
+ }
+
+ /**
+ * 显示演示窗口
+ */
+ public void show() {
+ stage.show();
+ root.requestFocus();
+ }
+
+ /**
+ * 处理键盘事件
+ */
+ private void handleKeyEvent(KeyEvent event) {
+ if (event.getCode() == KeyCode.RIGHT || event.getCode() == KeyCode.SPACE) {
+ nextPage();
+ } else if (event.getCode() == KeyCode.LEFT) {
+ previousPage();
+ } else if (event.getCode() == KeyCode.ESCAPE) {
+ exitPresentation();
+ }
+ }
+
+ /**
+ * 处理鼠标点击事件
+ */
+ private void handleMouseClick(MouseEvent event) {
+ if (event.getClickCount() == 1) {
+ nextPage();
+ }
+ }
+
+ /**
+ * 下一页
+ */
+ private void nextPage() {
+ if (isTransitioning || currentPageIndex >= slide.getPages().size() - 1) {
+ return;
+ }
+ currentPageIndex++;
+ playTransition(currentTransition);
+ }
+
+ /**
+ * 上一页
+ */
+ private void previousPage() {
+ if (isTransitioning || currentPageIndex <= 0) {
+ return;
+ }
+ currentPageIndex--;
+ playTransition(currentTransition);
+ }
+
+ /**
+ * 播放过渡动画
+ */
+ private void playTransition(TransitionEffect effect) {
+ isTransitioning = true;
+
+ switch (effect) {
+ case FADE:
+ playFadeTransition();
+ break;
+ case PUSH_LEFT:
+ playPushTransition(-canvas.getWidth(), 0);
+ break;
+ case PUSH_RIGHT:
+ playPushTransition(canvas.getWidth(), 0);
+ break;
+ case PUSH_DOWN:
+ playPushTransition(0, canvas.getHeight());
+ break;
+ case PUSH_UP:
+ playPushTransition(0, -canvas.getHeight());
+ break;
+ case ZOOM_IN:
+ playZoomTransition(0.5, 1.0);
+ break;
+ case ZOOM_OUT:
+ playZoomTransition(1.0, 0.5);
+ break;
+ case ROTATE:
+ playRotateTransition();
+ break;
+ default:
+ drawCurrentPage(null);
+ isTransitioning = false;
+ }
+ }
+
+ /**
+ * 淡出淡入动画
+ */
+ private void playFadeTransition() {
+ int duration = currentTransition.getDuration();
+ int halfDuration = duration / 2;
+
+ // 保存当前页面索引
+ int previousPageIndex = currentPageIndex - 1;
+ if (previousPageIndex < 0) previousPageIndex = slide.getPages().size() - 1;
+
+ // 淡出旧页面,显示黑屏,淡入新页面
+ AnimationTimer animationTimer = new AnimationTimer() {
+ private long startTime = System.currentTimeMillis();
+ private final long fadeOutDuration = halfDuration;
+ private final long fadeInDuration = halfDuration;
+
+ @Override
+ public void handle(long now) {
+ long elapsed = System.currentTimeMillis() - startTime;
+
+ if (elapsed < fadeOutDuration) {
+ // 淡出阶段
+ double progress = (double) elapsed / fadeOutDuration;
+ drawFadeTransition(progress, true);
+ } else if (elapsed < fadeOutDuration + fadeInDuration) {
+ // 淡入阶段
+ double progress = (double) (elapsed - fadeOutDuration) / fadeInDuration;
+ drawFadeTransition(progress, false);
+ } else {
+ // 动画完成
+ drawCurrentPage(null);
+ isTransitioning = false;
+ stop();
+ }
+ }
+ };
+
+ animationTimer.start();
+ }
+
+ /**
+ * 绘制淡出淡入过渡
+ */
+ private void drawFadeTransition(double progress, boolean fadeOut) {
+ GraphicsContext gc = canvas.getGraphicsContext2D();
+ double canvasWidth = canvas.getWidth();
+ double canvasHeight = canvas.getHeight();
+
+ final double PAGE_WIDTH = 1024;
+ final double PAGE_HEIGHT = 768;
+
+ double scaleX = canvasWidth / PAGE_WIDTH;
+ double scaleY = canvasHeight / PAGE_HEIGHT;
+ double scale = Math.min(scaleX, scaleY);
+
+ double scaledWidth = PAGE_WIDTH * scale;
+ double scaledHeight = PAGE_HEIGHT * scale;
+
+ double offsetX = (canvasWidth - scaledWidth) / 2;
+ double offsetY = (canvasHeight - scaledHeight) / 2;
+
+ if (fadeOut) {
+ // 绘制当前页面并淡出
+ drawCurrentPage(null);
+ gc.setFill(Color.BLACK.interpolate(Color.TRANSPARENT, 1 - progress));
+ gc.fillRect(0, 0, canvasWidth, canvasHeight);
+ } else {
+ // 绘制黑屏并淡入下一页
+ gc.setFill(Color.BLACK);
+ gc.fillRect(0, 0, canvasWidth, canvasHeight);
+
+ gc.setFill(Color.WHITE);
+ gc.fillRect(offsetX, offsetY, scaledWidth, scaledHeight);
+
+ gc.save();
+ gc.translate(offsetX, offsetY);
+ gc.scale(scale, scale);
+
+ SlidePage page = slide.getPages().get(currentPageIndex);
+ for (DrawableObject obj : page.getPageContent().getDrawableObjects()) {
+ drawObject(gc, obj);
+ }
+
+ gc.restore();
+
+ // 淡入效果
+ gc.setFill(Color.color(1, 1, 1, 1 - progress));
+ gc.fillRect(0, 0, canvasWidth, canvasHeight);
+ }
+ }
+
+ /**
+ * 推送动画
+ */
+ private void playPushTransition(double offsetX, double offsetY) {
+ int duration = currentTransition.getDuration();
+
+ AnimationTimer animationTimer = new AnimationTimer() {
+ private long startTime = System.currentTimeMillis();
+
+ @Override
+ public void handle(long now) {
+ long elapsed = System.currentTimeMillis() - startTime;
+
+ if (elapsed < duration) {
+ double progress = (double) elapsed / duration;
+ drawPushTransition(progress, offsetX, offsetY);
+ } else {
+ drawCurrentPage(null);
+ isTransitioning = false;
+ stop();
+ }
+ }
+ };
+
+ animationTimer.start();
+ }
+
+ /**
+ * 绘制推送过渡
+ */
+ private void drawPushTransition(double progress, double offsetX, double offsetY) {
+ GraphicsContext gc = canvas.getGraphicsContext2D();
+ double canvasWidth = canvas.getWidth();
+ double canvasHeight = canvas.getHeight();
+
+ final double PAGE_WIDTH = 1024;
+ final double PAGE_HEIGHT = 768;
+
+ double scaleX = canvasWidth / PAGE_WIDTH;
+ double scaleY = canvasHeight / PAGE_HEIGHT;
+ double scale = Math.min(scaleX, scaleY);
+
+ double scaledWidth = PAGE_WIDTH * scale;
+ double scaledHeight = PAGE_HEIGHT * scale;
+
+ double centerOffsetX = (canvasWidth - scaledWidth) / 2;
+ double centerOffsetY = (canvasHeight - scaledHeight) / 2;
+
+ // 清空背景
+ gc.setFill(Color.BLACK);
+ gc.fillRect(0, 0, canvasWidth, canvasHeight);
+
+ // 获取前一页索引(确保有效)
+ int prevPageIndex = currentPageIndex - 1;
+ if (prevPageIndex < 0) {
+ prevPageIndex = slide.getPages().size() - 1; // 循环到最后一页
+ }
+
+ // 绘制旧页面
+ SlidePage prevPage = slide.getPages().get(prevPageIndex);
+ double slideOffsetX = offsetX * progress;
+ double slideOffsetY = offsetY * progress;
+
+ gc.save();
+ gc.translate(centerOffsetX + slideOffsetX, centerOffsetY + slideOffsetY);
+ gc.scale(scale, scale);
+ gc.setFill(Color.WHITE);
+ gc.fillRect(0, 0, PAGE_WIDTH, PAGE_HEIGHT);
+ for (DrawableObject obj : prevPage.getPageContent().getDrawableObjects()) {
+ drawObject(gc, obj);
+ }
+ gc.restore();
+
+ // 绘制新页面推入
+ SlidePage newPage = slide.getPages().get(currentPageIndex);
+ double newPageOffsetX = centerOffsetX - offsetX * (1 - progress);
+ double newPageOffsetY = centerOffsetY - offsetY * (1 - progress);
+
+ gc.save();
+ gc.translate(newPageOffsetX, newPageOffsetY);
+ gc.scale(scale, scale);
+ gc.setFill(Color.WHITE);
+ gc.fillRect(0, 0, PAGE_WIDTH, PAGE_HEIGHT);
+ for (DrawableObject obj : newPage.getPageContent().getDrawableObjects()) {
+ drawObject(gc, obj);
+ }
+ gc.restore();
+ }
+
+ /**
+ * 缩放动画
+ */
+ private void playZoomTransition(double startScale, double endScale) {
+ int duration = currentTransition.getDuration();
+
+ AnimationTimer animationTimer = new AnimationTimer() {
+ private long startTime = System.currentTimeMillis();
+
+ @Override
+ public void handle(long now) {
+ long elapsed = System.currentTimeMillis() - startTime;
+
+ if (elapsed < duration) {
+ double progress = (double) elapsed / duration;
+ double currentScale = startScale + (endScale - startScale) * progress;
+ drawZoomTransition(currentScale, progress);
+ } else {
+ drawCurrentPage(null);
+ isTransitioning = false;
+ stop();
+ }
+ }
+ };
+
+ animationTimer.start();
+ }
+
+ /**
+ * 绘制缩放过渡
+ */
+ private void drawZoomTransition(double zoomScale, double progress) {
+ GraphicsContext gc = canvas.getGraphicsContext2D();
+ double canvasWidth = canvas.getWidth();
+ double canvasHeight = canvas.getHeight();
+
+ final double PAGE_WIDTH = 1024;
+ final double PAGE_HEIGHT = 768;
+
+ double scaleX = canvasWidth / PAGE_WIDTH;
+ double scaleY = canvasHeight / PAGE_HEIGHT;
+ double scale = Math.min(scaleX, scaleY);
+
+ double scaledWidth = PAGE_WIDTH * scale;
+ double scaledHeight = PAGE_HEIGHT * scale;
+
+ double offsetX = (canvasWidth - scaledWidth) / 2;
+ double offsetY = (canvasHeight - scaledHeight) / 2;
+
+ // 清空背景
+ gc.setFill(Color.BLACK);
+ gc.fillRect(0, 0, canvasWidth, canvasHeight);
+
+ // 获取前一页索引(确保有效)
+ int prevPageIndex = currentPageIndex - 1;
+ if (prevPageIndex < 0) {
+ prevPageIndex = slide.getPages().size() - 1; // 循环到最后一页
+ }
+
+ // 绘制旧页面,逐渐缩小
+ SlidePage prevPage = slide.getPages().get(prevPageIndex);
+ double oldPageScale = scale * (1 - progress * 0.3); // 缩小30%
+ double oldPageWidth = PAGE_WIDTH * oldPageScale;
+ double oldPageHeight = PAGE_HEIGHT * oldPageScale;
+ double oldPageOffsetX = (canvasWidth - oldPageWidth) / 2;
+ double oldPageOffsetY = (canvasHeight - oldPageHeight) / 2;
+
+ gc.save();
+ gc.translate(oldPageOffsetX, oldPageOffsetY);
+ gc.scale(oldPageScale / scale, oldPageScale / scale);
+ gc.setFill(Color.WHITE);
+ gc.fillRect(0, 0, PAGE_WIDTH, PAGE_HEIGHT);
+ for (DrawableObject obj : prevPage.getPageContent().getDrawableObjects()) {
+ drawObject(gc, obj);
+ }
+ gc.restore();
+
+ // 绘制新页面,逐渐放大
+ SlidePage newPage = slide.getPages().get(currentPageIndex);
+ double newPageScale = scale * (0.7 + progress * 0.3); // 从70%放大到100%
+ double newPageWidth = PAGE_WIDTH * newPageScale;
+ double newPageHeight = PAGE_HEIGHT * newPageScale;
+ double newPageOffsetX = (canvasWidth - newPageWidth) / 2;
+ double newPageOffsetY = (canvasHeight - newPageHeight) / 2;
+
+ gc.save();
+ gc.translate(newPageOffsetX, newPageOffsetY);
+ gc.scale(newPageScale / scale, newPageScale / scale);
+ gc.setFill(Color.WHITE);
+ gc.fillRect(0, 0, PAGE_WIDTH, PAGE_HEIGHT);
+ for (DrawableObject obj : newPage.getPageContent().getDrawableObjects()) {
+ drawObject(gc, obj);
+ }
+ gc.restore();
+ }
+
+ /**
+ * 旋转动画
+ */
+ private void playRotateTransition() {
+ int duration = currentTransition.getDuration();
+
+ AnimationTimer animationTimer = new AnimationTimer() {
+ private long startTime = System.currentTimeMillis();
+
+ @Override
+ public void handle(long now) {
+ long elapsed = System.currentTimeMillis() - startTime;
+
+ if (elapsed < duration) {
+ double progress = (double) elapsed / duration;
+ double rotation = progress * 360; // 360度旋转
+ drawRotateTransition(rotation, progress);
+ } else {
+ drawCurrentPage(null);
+ isTransitioning = false;
+ stop();
+ }
+ }
+ };
+
+ animationTimer.start();
+ }
+
+ /**
+ * 绘制旋转过渡
+ */
+ private void drawRotateTransition(double rotation, double progress) {
+ GraphicsContext gc = canvas.getGraphicsContext2D();
+ double canvasWidth = canvas.getWidth();
+ double canvasHeight = canvas.getHeight();
+
+ final double PAGE_WIDTH = 1024;
+ final double PAGE_HEIGHT = 768;
+
+ double scaleX = canvasWidth / PAGE_WIDTH;
+ double scaleY = canvasHeight / PAGE_HEIGHT;
+ double scale = Math.min(scaleX, scaleY);
+
+ double scaledWidth = PAGE_WIDTH * scale;
+ double scaledHeight = PAGE_HEIGHT * scale;
+
+ double offsetX = (canvasWidth - scaledWidth) / 2;
+ double offsetY = (canvasHeight - scaledHeight) / 2;
+
+ // 清空背景
+ gc.setFill(Color.BLACK);
+ gc.fillRect(0, 0, canvasWidth, canvasHeight);
+
+ // 绘制旋转的页面
+ SlidePage newPage = slide.getPages().get(currentPageIndex);
+
+ gc.save();
+ // 移到中心,旋转,再移回
+ double centerX = offsetX + scaledWidth / 2;
+ double centerY = offsetY + scaledHeight / 2;
+
+ gc.translate(centerX, centerY);
+ gc.rotate(rotation);
+ gc.translate(-scaledWidth / 2, -scaledHeight / 2);
+ gc.scale(scale, scale);
+
+ gc.setFill(Color.WHITE);
+ gc.fillRect(0, 0, PAGE_WIDTH, PAGE_HEIGHT);
+ for (DrawableObject obj : newPage.getPageContent().getDrawableObjects()) {
+ drawObject(gc, obj);
+ }
+
+ gc.restore();
+ }
+
+ /**
+ * 绘制当前页面
+ */
+ private void drawCurrentPage(Animation animation) {
+ if (currentPageIndex < 0 || currentPageIndex >= slide.getPages().size()) {
+ return;
+ }
+
+ GraphicsContext gc = canvas.getGraphicsContext2D();
+ double canvasWidth = canvas.getWidth();
+ double canvasHeight = canvas.getHeight();
+
+ // 页面标准宽高比
+ final double PAGE_WIDTH = 1024;
+ final double PAGE_HEIGHT = 768;
+
+ // 计算缩放因子,保持宽高比
+ double scaleX = canvasWidth / PAGE_WIDTH;
+ double scaleY = canvasHeight / PAGE_HEIGHT;
+ double scale = Math.min(scaleX, scaleY);
+
+ // 计算内容区域的实际宽高
+ double scaledWidth = PAGE_WIDTH * scale;
+ double scaledHeight = PAGE_HEIGHT * scale;
+
+ // 计算居中偏移
+ double offsetX = (canvasWidth - scaledWidth) / 2;
+ double offsetY = (canvasHeight - scaledHeight) / 2;
+
+ // 清空背景(黑色)
+ gc.setFill(Color.BLACK);
+ gc.fillRect(0, 0, canvasWidth, canvasHeight);
+
+ // 白色背景区域
+ gc.setFill(Color.WHITE);
+ gc.fillRect(offsetX, offsetY, scaledWidth, scaledHeight);
+
+ SlidePage page = slide.getPages().get(currentPageIndex);
+
+ // 保存绘图上下文
+ gc.save();
+
+ // 应用缩放和平移变换
+ gc.translate(offsetX, offsetY);
+ gc.scale(scale, scale);
+
+ // 绘制所有对象
+ for (DrawableObject obj : page.getPageContent().getDrawableObjects()) {
+ drawObject(gc, obj);
+ }
+
+ gc.restore();
+
+ // 绘制页码(不受缩放影响)
+ drawPageNumber(gc, canvasWidth, canvasHeight);
+ }
+
+ /**
+ * 绘制单个对象
+ */
+ private void drawObject(GraphicsContext gc, DrawableObject obj) {
+ if (obj instanceof TextObject) {
+ drawTextObject(gc, (TextObject) obj);
+ } else if (obj instanceof ShapeObject) {
+ drawShapeObject(gc, (ShapeObject) obj);
+ } else if (obj instanceof ImageObject) {
+ drawImageObject(gc, (ImageObject) obj);
+ }
+ }
+
+ /**
+ * 绘制文本对象
+ */
+ private void drawTextObject(GraphicsContext gc, TextObject textObj) {
+ gc.setFill(textObj.getTextColorAsColor());
+ gc.setFont(new javafx.scene.text.Font(textObj.getFontFamily(), textObj.getFontSize()));
+
+ double lineHeight = textObj.getFontSize() * 1.2;
+ double currentY = textObj.getY();
+
+ for (String line : textObj.getLayoutLines()) {
+ currentY += lineHeight;
+ gc.fillText(line, textObj.getX() + 5, currentY);
+ }
+ }
+
+ /**
+ * 绘制形状对象
+ */
+ private void drawShapeObject(GraphicsContext gc, ShapeObject shapeObj) {
+ gc.setFill(Color.web("#" + shapeObj.getFillColor()));
+ gc.setStroke(Color.web("#" + shapeObj.getStrokeColor()));
+ gc.setLineWidth(shapeObj.getStrokeWidth());
+
+ switch (shapeObj.getShapeType()) {
+ case RECTANGLE:
+ gc.fillRect(shapeObj.getX(), shapeObj.getY(), shapeObj.getWidth(), shapeObj.getHeight());
+ gc.strokeRect(shapeObj.getX(), shapeObj.getY(), shapeObj.getWidth(), shapeObj.getHeight());
+ break;
+ case CIRCLE:
+ double radius = Math.min(shapeObj.getWidth(), shapeObj.getHeight()) / 2;
+ gc.fillOval(shapeObj.getX(), shapeObj.getY(), shapeObj.getWidth(), shapeObj.getHeight());
+ gc.strokeOval(shapeObj.getX(), shapeObj.getY(), shapeObj.getWidth(), shapeObj.getHeight());
+ break;
+ case ELLIPSE:
+ gc.fillOval(shapeObj.getX(), shapeObj.getY(), shapeObj.getWidth(), shapeObj.getHeight());
+ gc.strokeOval(shapeObj.getX(), shapeObj.getY(), shapeObj.getWidth(), shapeObj.getHeight());
+ break;
+ case LINE:
+ gc.strokeLine(shapeObj.getX(), shapeObj.getY(),
+ shapeObj.getX() + shapeObj.getWidth(), shapeObj.getY() + shapeObj.getHeight());
+ break;
+ }
+ }
+
+ /**
+ * 绘制图片对象
+ */
+ private void drawImageObject(GraphicsContext gc, ImageObject imgObj) {
+ try {
+ javafx.scene.image.Image image = new javafx.scene.image.Image("file:" + imgObj.getImagePath());
+ gc.drawImage(image, imgObj.getX(), imgObj.getY(), imgObj.getWidth(), imgObj.getHeight());
+ } catch (Exception e) {
+ // 图片加载失败,显示占位符
+ gc.setFill(Color.LIGHTGRAY);
+ gc.fillRect(imgObj.getX(), imgObj.getY(), imgObj.getWidth(), imgObj.getHeight());
+ gc.setFill(Color.BLACK);
+ gc.fillText("图片加载失败", imgObj.getX() + 10, imgObj.getY() + 20);
+ }
+ }
+
+ /**
+ * 绘制页码(不受缩放影响)
+ */
+ private void drawPageNumber(GraphicsContext gc, double canvasWidth, double canvasHeight) {
+ gc.setFill(Color.BLACK);
+ gc.setFont(new javafx.scene.text.Font(16));
+ String pageNum = String.format("%d / %d", currentPageIndex + 1, slide.getPages().size());
+ gc.fillText(pageNum, canvasWidth - 100, canvasHeight - 20);
+ }
+
+ /**
+ * 设置翻页动画
+ */
+ public void setTransitionEffect(TransitionEffect effect) {
+ this.currentTransition = effect;
+ }
+
+ /**
+ * 退出演示
+ */
+ private void exitPresentation() {
+ stage.close();
+ }
+
+ /**
+ * 获取舞台
+ */
+ public Stage getStage() {
+ return stage;
+ }
+}
+
diff --git a/src/main/java/dev/bytevibe/hyperpoint/TransitionEffect.java b/src/main/java/dev/bytevibe/hyperpoint/TransitionEffect.java
new file mode 100644
index 0000000..80f1d60
--- /dev/null
+++ b/src/main/java/dev/bytevibe/hyperpoint/TransitionEffect.java
@@ -0,0 +1,68 @@
+package dev.bytevibe.hyperpoint;
+
+/**
+ * 演示/播放模式下支持的翻页动画效果
+ */
+public enum TransitionEffect {
+ /**
+ * 无动画,直接切换
+ */
+ NONE("无动画", 0),
+
+ /**
+ * 淡出/淡入效果
+ */
+ FADE("淡出淡入", 500),
+
+ /**
+ * 从右向左推送效果
+ */
+ PUSH_LEFT("从右推入", 400),
+
+ /**
+ * 从左向右推送效果
+ */
+ PUSH_RIGHT("从左推入", 400),
+
+ /**
+ * 从上向下推送效果
+ */
+ PUSH_DOWN("从上推入", 400),
+
+ /**
+ * 从下向上推送效果
+ */
+ PUSH_UP("从下推入", 400),
+
+ /**
+ * 缩放效果 - 从小到大
+ */
+ ZOOM_IN("缩放放大", 500),
+
+ /**
+ * 缩放效果 - 从大到小
+ */
+ ZOOM_OUT("缩放缩小", 500),
+
+ /**
+ * 旋转效果
+ */
+ ROTATE("旋转翻页", 600);
+
+ private final String displayName;
+ private final int duration; // 毫秒
+
+ TransitionEffect(String displayName, int duration) {
+ this.displayName = displayName;
+ this.duration = duration;
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public int getDuration() {
+ return duration;
+ }
+}
+
diff --git a/src/main/resources/dev/bytevibe/hyperpoint/main.fxml b/src/main/resources/dev/bytevibe/hyperpoint/main.fxml
index 086a7ab..d1cb44c 100644
--- a/src/main/resources/dev/bytevibe/hyperpoint/main.fxml
+++ b/src/main/resources/dev/bytevibe/hyperpoint/main.fxml
@@ -47,6 +47,18 @@
+
+
+
+
+
+
+
+
+
+
+
+