65.9K
CodeProject 正在变化。 阅读更多。
Home

Web 演示文稿,单文件应用程序,现已支持视频

2020 年 11 月 23 日

CC (ASA 3U)

11分钟阅读

viewsIcon

41696

downloadIcon

1179

所有办公演示应用程序的跨平台替代品,都在一个文件中

(此演示使用了一个 AV1 视频,该视频与几乎所有浏览器兼容,但不包括 Microsoft Edge)

presentation.h

目录

引言
动机
用法
演示数据
演示属性
视频属性
图像文件类型
动画
实现细节
保留动画
加载、开始、暂停和停止视频
触摸屏支持
RTL 支持
版本
许可说明

引言

本文是关于 Web 演示产品的小系列文章的第一篇

动机

在我上次进行的演示中,我已经收集了一系列 SVG 图像,主要代表一些架构解决方案。在开发架构和设计解决方案时,您总是会遇到一些问题,因为 SVG 是最通用的矢量格式。虽然可以通过合适的图像查看器轻松以演示方式显示照片,但 SVG 通常需要额外的努力,而那些办公演示应用程序可能会使事情变得一团糟,这可能是最糟糕的选择。为什么不直接显示图像本身呢?毕竟,它们是可伸缩的,并且与所有不麻烦的 Web 浏览器兼容。

因此,我很快将这些图像组合到一个 JavaScript 文件中,并添加了最简单的脚本来在一张图像移到另一张图像。多么轻松!谁还需要其他东西?

在演示中,我添加了一些动画 SVG、静态 WebP 照片以及一个从相机直接导出的 WebP 动画视频,在后续版本中还添加了一个真实的 WebM 视频样本。这些文件和功能与演示产品本身无关,它们只是表明我们所需的一切都已可用,并且仅使用开源产品即可创建。同时,我们可以拥有那些臃肿的办公软件应用程序所期望的所有功能,而且方式更简单。

用法

基本用法可能非常简单。 在线演示也提供了演示系统的介绍并展示了其功能。

基本上,演示者创建一组代表演示帧的文件,按所需顺序在演示数据文件中列出它们的路径,并在浏览器中加载“presentation.h”,通过 URL 查询字符串传递演示数据文件。演示开始。

还有一些最小化的高级属性;其中每一个都是可选的。

演示数据

除了图像文件,还需要描述演示文稿。首先,它需要一个图像文件相对路径列表,以定义图像数据和演示中文档的顺序。所有其他属性都可以省略,然后使用默认值。例如,假设我们将其放入一个名为“demo/presentation.js”的文件中,路径相对于“presentation.html”。

const presentation = {
    images: [ // relative to presentation.html
        "demo/1.webp",
        "demo/1.svg",
        "demo/2.webp",
    ],
    title: "Presentation Demo",
    hideHelpOnStart: false,
    colors: {
        background: "white",
        text: {
            background: "azure",
            foreground: "black",
            border: "lightBlue",
        },
    },
    rtl: false,
};

v. 2 以来的扩展语法允许添加视频元素。示例如下

const presentation = {
    images: [ // relative to presentation.html
        "demo/1.webp",
        image("demo/1.svg"), // same as "demo/1.svg"
        video("demo/myClip.webm", {
            title: "My concert",
            poster: "demo/our-concert-hall.webp"
        }),
        video("demo/myTrip.webm", {
            title: "On the road",
            play: true, // auto-play
        }),
    ],
}

这样,演示帧列表可以是多态的,是图像和视频元素的顺序混合。请参见 下面的视频属性

可以通过在 URL 的查询字符串中使用演示文件的路径来在网页上加载此演示文稿。最简单的方法是有一个单独的特定于演示文稿的 HTML 文件。假设这是文件“demo/index.html”,那么它的内容可能是

<!doctype HTML>
<html>
    <head>
        <meta http-equiv="refresh" content="0; url=
        ../presentation.html?demo/presentation.js" />
    </head>
</html>

演示属性

所有路径都相对于“presentation.html”的位置。

presentation.images:一个图像文件名数组,路径相对于“presentation.html”。此属性是唯一必需的。如果缺少,或者这是一个空列表,应用程序将报告一个错误。

可选属性

title:演示标题

hideHelpOnStart:布尔值(true/false),默认值:false,也就是说,默认情况下,帮助文本在开始时显示。

colors:演示背景和文本数据渲染的颜色。

colors.background:演示背景。它也应用于所有支持透明度或 Alpha 通道的图像的背景。

colors.text:文本数据渲染的颜色,一个具有三个不言自明属性的对象。

rtl:适用于 从右到左的书写文化 的选项,详细说明 下方,默认值:false

视频属性

每个视频元素都以这种形式添加

video("path_to_video_source_file", // relative to presentation.html
    { /* video properties... */ } // optional
)

属性

title:String,<video> 元素的标题。

poster:String,在视频开始前显示在视频元素上的光栅图形文件的路径。

playtrue/false:自动播放;如果为 true,视频将在其演示帧变为活动状态时开始播放。

请注意,同时使用 posterplay 意义不大,因为如果视频自动开始播放,观众将没有足够的时间看到海报。

图像文件类型

对于演示帧,所有 Web 标准化的 MIME 类型都可以接受。对于矢量图形,它是 SVG,image/svg+xml。对于光栅图形,可接受的类型包括:image/apng、image/avif、image/gif、image/jpeg、image/png 和 image/webp。

特定浏览器接受但未标准化的 Web 图像文件类型,如 .bmp、.ico 或 .tif,最好避免使用。

对于光栅图像,最实用和推荐的格式是 WebP。它比旧的图像类型提供更好的压缩,支持渐进式渲染,独立的预设优化用于图片、图标、照片、绘图和文本,以及动画。其他支持动画的光栅图形类型包括 APNGAVIFGIF

然而,对于演示目的,最重要的一种动画是矢量动画 SVG,因为人们广泛使用各种过渡效果。即使这些效果会分散对演示的注意力,而不是帮助理解材料,它们也被认为是必须的。

动画

尽管图像文件的创建是用户的唯一责任,但我只想评论一下仅使用 Web 标准化的图像文件类型和仅使用开源产品和标准来创建动画。有很多用于创建动画的工具。

例如,SVG 可以使用 Inkscape 创建。关于 SVG 动画的文档很多,尤其是 SMIL,这可能是最适合演示目的的方法:每个过渡效果只需要一行简短的 XML 代码,并且效果可以组合在同一元素上。此外,还有一些动画插件可用。

对于光栅图形,最好的方法可能是 WebP。我使用了两个工具来创建动画。首先,动画可以由 GIMP 中的一组单独帧组成。要做到这一点,需要将每一帧放在一个单独的图层中,执行动画优化([主菜单] => Filters => Animation => Optimize (Difference)),然后将结果保存为 WebP 图像。在保存时,GIMP 会提供一个创建动画的选项。

此外,FFMpeg 可以将可用的视频文件转换为 WebP 动画。命令行示例

ffmpeg -i input_file -vcodec libwebp -filter:v fps=fps=20 -lossless 0 -compression_level 6 -an -vsync 0 output_file.webp

有关更多详细信息,请参阅 FFMpeg 文档。

我提到的所有软件工具不仅是开源的,而且在大多数平台上都可以使用。

实现细节

实现的实现足够简单,无需过多深入。本文的目的是提供一个功能齐全的演示工具,而不是教任何东西。如果这项工作能教任何东西,那就是对所有商业臃肿产品的极简主义和清醒的实际看法。

因此,我只会涉及一些不那么明显的问题。

保留动画

动画通过不在初始化阶段加载所有图像来保留。相反,只有一个 img 元素,它只通过为其 src 属性赋值来加载源,仅用于演示文稿的第一张图像。所有其他图像在显示时加载。

const move = backward => {
    // ...
    // undefined: initialization, boolean: backward/forward:
    if (backward != undefined) {
        if (backward)
            if (current > 0) --current; else current = presentation.images.length - 1;
        else
            if (current < presentation.images.length - 1) ++current; else current = 0;
    image.src = presentation.images[current];
    // ...
    if (isVideo) {
        // ...
        videoSource.src = item.source;
        // ...
    } else {
        image.src = item;
        resize(image);
    }
    show(video, isVideo);
    show(image, !isVideo);
};

这样,每一次 image.src 属性赋值都会启动动画。因此,每次显示同一图像时,动画都会重新开始。在 v. 2 中添加了视频功能后,<img><video> 元素会显示/隐藏,具体取决于 images 列表中当前元素的类型。与图形动画不同,视频的启动是通过用户明确命令(按键“P”,播放/暂停)或自动启动的,当包含视频的帧变为活动状态时 — 这由图像列表中可选的布尔属性 play 控制。

加载、开始、暂停和停止视频

视频 API 中有几个令人困惑的部分,因此我想就此提供一些有用的说明。

我必须从设计所需方面开始。视频内部导航和演示帧的“外部”导航之间可能存在冲突。我决定在以下设计中解决它:让我们考虑将 ← 和 → 键专门用于演示帧导航。在演示过程中,很少需要跳转到视频内的位置,即使需要,也可以使用鼠标/触摸屏/触摸板。此外,我将“P”键指定用于视频播放/暂停。当视频播放到结束时,它也起到了视频重新开始的作用,因为这是 HTMLVideoElement.play() 的工作方式。

现在,当活动帧需要加载视频时,应该卸载先前显示的视频或图形图像。与 img 元素不同,将字符串值赋给 HTMLSourceElement.src 属性是不够的;还必须调用 HTMLVideoElement.load()

所有这些都在此处予以考虑

const move = backward => {
    video.pause();
    document.exitFullscreen();
    videoSource.src = undefined;
    image.src = undefined;
    // ...
    if (isVideo) {
        if (item.source) {
            video.poster = item.poster;
            video.title = item.title;
            videoSource.src = item.source;
            video.onplay = event => event.target.requestFullscreen();
            video.onended = event => document.exitFullscreen();
            video.load();
            if (item.play)
                video.play();
        } else
            video.title = "Video file not specified";
    } else {
        image.src = item;
        resize(image);
    } //if
    // ...
}
document.body.onkeydown = event => {
    switch (event.code) {
        // ...
        case "KeyP":
            if (videoSource.src)
                if (video.paused) video.play(); else video.pause();
    }
};

我得说,这里最令人困惑的部分是 document.exitFullscreen()。我使用全屏 API 在视频开始播放时以全屏模式显示视频,并且理所当然地,当活动演示帧更改时,我们需要恢复到以前的模式。但如果用户也使用浏览器的全屏模式怎么办?出乎意料的是,一切正常。尽管函数名称如此,document.exitFullscreen() 仍将恢复到播放视频之前的浏览器全屏模式或窗口模式。视频全屏和浏览器窗口全屏是不同的,并且不冲突。

触摸屏支持

let touchStart = undefined;
addEventListener("touchstart", event => {
    touchStart = { x: event.changedTouches[0].clientX,
                   y: event.changedTouches[0].clientY };
}, false);
addEventListener("touchend", event => { touchStart = undefined; }, false);
addEventListener("touchmove", event => {
    if (touchStart == undefined) return;
    const vector = { x: event.changedTouches[0].clientX - touchStart.x,
                    y: event.changedTouches[0].clientY - touchStart.y };
    const horizontal = Math.abs(vector.x) > Math.abs(vector.y);
    let back = horizontal ? vector.x > 0 : vector.y > 0;
    if (horizontal && presentation.rtl) back = !back;
    move(back);
    touchStart = undefined;
}, false);

此行为还取决于下面解释的 presentation.rtl 属性。

RTL 支持

基于 从右到左 系统的书写文化也会略微改变人们的看法。这种文化元素会影响人们看待 时间箭头 的方式:在西方文化中,人们将时间想象成从左到右流动的东西,而其他人可能以不同的方式思考。在这种情况下,将演示文稿的流程视为从右到左移动可能更自然。对于这些人,提供了 rtl 属性。它只改变左右箭头和触摸屏左右滑动手势的使用:方向变为相反。它与 CSS direction 无关,但基于相似的考虑。

实际上,它只影响人们如何处理箭头键“←”和“→”以及触摸屏滑动的方向。在西方文化中,人们认为“←”表示“上一个”,“→”表示“下一个”,执行这些操作的人将当前帧视为一个窗口,显示一系列帧,代表时间线。对于触摸屏滑动,一个人“移动”的不是窗口,而是帧序列本身,因此“←”表示“下一个”而“→”表示“上一个”。在 RTL 文化中,所有这四个操作都朝相反的方向进行。同时,上下方向的含义不取决于文化。

因此,由于 move 函数接受一个布尔参数,其含义是“转到上一帧”,箭头键的处理取决于 presentation.rtl,但仅限于水平方向。

switch (event.code) {
    case "Space":
    case "ArrowDown": move(false); break;
    case "Backspace":
    case "ArrowUp": move(true); break;
    case "ArrowRight": move(presentation.rtl); break;
    case "ArrowLeft": move(!presentation.rtl); break;
    //...
}

同样,在 触摸屏滑动 手势的实现中,仅对水平方向使用反向方向。

版本

1.1.0

2020 年 11 月 22 日

首次生产发布,仅支持与 <img> 元素兼容的媒体:矢量/光栅图形,以及动画矢量/光栅图形。

2.1.0

2020 年 11 月 24 日

准备好视频的版本。除了图像,还可以添加视频。媒体列表的语法已扩展为包含图形和视频元素的混合多态列表,同时保留了原始语法。

3.0.0

2020 年 12 月 6 日

视频的样式改进。根据功能请求(CodeProject 会员 gunamoi1),视频元素始终居中或处于全屏模式。

4.0.0

2020 年 12 月 21 日

小修正。在此版本中,添加了一个替代产品,昵称为“另一种方式”。它在 单独的文章 中进行了描述。

4.2.0

2021 年 1 月 12 日

帮助元素的关闭框被平台无关的 SVG 替代。

4.3.0

2021 年 1 月 17 日

修复了在 Gecko 和 Goanna 引擎中出现的关闭框大小问题。

4.4.0

2023 年 9 月 4 日

修复了两个演示产品中的拼写错误。

致谢

Nelek 在两个演示产品中发现了一个 拼写错误。已在 v. 4.4.0 中修复。

许可说明

演示中使用的所有照片、视频和图形材料均由

© . All rights reserved.