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

替代模拟 SVG 时钟

2023年3月14日

CPOL

7分钟阅读

viewsIcon

17069

downloadIcon

480

一个纯粹的Web浏览器应用程序,文章“一个SVG模拟时钟”的替代方案

 

Application

 

目录

引言
有什么有趣之处?
实现
SVG
时钟
对象类型比较
箭头旋转
性能优化
模拟时钟和数字时钟的主要区别
如何添加装饰?
CSS: 停靠布局
CSS: 适应SVG
服务器部分怎么样?

引言

有什么有趣之处?

有什么有趣之处?几乎没有!这是文章一个SVG模拟时钟中展示的应用程序的替代方案。我对一个时钟被过度设计的事实感到惊讶,并决定提供一个精简且接近最优的解决方案,我认为这更合适。

屏幕时钟的实际价值几乎为零,甚至可能是负数。这只是一个传统的编程练习,与编辑器、俄罗斯方块实现、分形图像渲染等类似。

然而,本文解释了几个对于初学者可能有用的细微之处。

实现

SVG

首先,让我们看看SVG是如何动态构建的。所有内容都以类SVG实现

class SVG {

    #implementation = {};

    constructor(viewBox, existingSvg) {
        const ns = "http://www.w3.org/2000/svg";
        this.#implementation.createNS = (name) =>
            document.createElementNS(ns, name);
        this.#implementation.attribute = (element, values) => {
            for (let attribute in values)
                element.setAttribute(attribute, values[attribute]);
            return element;
        }; //this.#implementation.attribute
        // defaults...
        this.#implementation.svg = existingSvg == null ?
            this.#implementation.createNS("svg") : existingSvg;
        if (viewBox != null) // it also covers undefined
            this.#implementation.svg.setAttribute("viewBox",
                `${viewBox.x.from} ${viewBox.y.from}
                 ${viewBox.x.to - viewBox.x.from}
                 ${viewBox.y.to - viewBox.y.from}`);
    } //constructor

    get element() { return this.#implementation.svg; }

    // ... get/set attributes, primitives: line, circle, ets.

    appendElement(element, group) {
        if (group)
            group.appendChild(element);
        else
            this.#implementation.svg.appendChild(element);
        return element;
    } //appendElement

    group() { return this.appendElement(this.#implementation.createNS("g")); }

} //class SVG

首先,请注意所有私有代码都隐藏在构造函数和私有成员this.#implementation中。

SVG元素可以使用document对象以与HTML元素略有不同的方式创建,因为SVG使用不同的命名空间。这在局部方法this.#implementation.createNS中实现。

SVG可以以两种不同的方式使用。如果指定了existingSvg,则该类可用于填充现有的<svg>元素,并且新内容可以添加到现有内容中。它可用于向嵌入在HTML中的<svg>元素添加内容。

时钟

函数createClock创建时钟实例。它也可以通过两种不同的方式创建。如果参数parent属于SVG类(如上所述),则时钟可以呈现在某个现有<svg>元素的顶部。否则,它是从头开始创建的。由于SVG.element的实例被附加到某个现有HTML元素,因此从头开始创建的整个时钟都将附加到该元素。该函数返回用于设置和渲染当前时钟时间的函数set

对于时钟表盘和箭头的渲染,Z轴顺序很重要。如果一个SVG元素稍后创建,它就会被放置在前景。例如,红色的秒针应该在最后创建,以使其在所有位置都清晰可见。

为了选择使用方式,代码需要按类型比较对象。让我们看看这个。

在应用程序中,时钟实例在“main.js”中的window.onload处理程序中初始化和使用。时钟在初始化时设置一次,然后定期在传递给setInterval()的函数中设置。

对象类型比较

在函数createClock中,我们有

if (parent.constructor != SVG)
    parent.appendChild(svg.element);

当然,在这一点上,我们假设对象parent不是undefined也不是null。需要时,可以使用if (parent != null)进行检查,因为这种比较也涵盖了undefined的情况。

这是一种直接比较类型的方式,不使用魔法字符串。我们可以在许多JavaScript代码示例中找到使用类型名称比较类型的情况。这是脏的,难以维护的,应该避免。

箭头旋转

时钟的核心部分是set方法。它旋转箭头

createClock = parent => {

    let currentTime = null;

    const set = time => {
        if (currentTime == time) return;
        currentTime = time;
        const date = new Date(time);
        let hour = date.getHours() % 12;
        let minute = date.getMinutes();
        let second = date.getSeconds();
        hour = (hour + minute / 60 + second / 3600) / 12;
        minute = (minute + second / 60) / 60;
        second = second / 60;
        arrowHour.style.transform = `rotate(${hour}turn)`;
        arrowMinute.style.transform = `rotate(${minute}turn)`;
        arrowSecond.style.transform = `rotate(${second}turn)`;
    }; //set

    return set;

}; //createClock

这里,arrowHourarrowMinutearrowSecond是SVG。为什么?这样做是为了泛化代码,并在需要渲染由多个SVG元素组成的复杂箭头形状时使其易于维护。

style.transform的另一个作用是避免三角函数计算,并使代码更易于维护。请注意,转换假定坐标点 (0, 0) 是旋转中心。

性能优化

在我们的案例中,时钟状态每秒更新一次。但是,如果我们每秒从传递给setInterval的处理程序中调用时钟的set函数,系统时钟事件将与此周期发生拍频,因此部分秒钟更新将会丢失。因此,为了看到所有秒钟更新,set应该每秒调用几次。在代码中,它平均每秒大约调用三次,看起来不错。但这样一些图形更新就会是多余的。

这个问题可以通过在本地存储currentTime并检查传递给set的参数time是否具有新值来解决,如set代码中所示

if (currentTime == time) return;
    currentTime = time;

这是可行的,因为参数time的值是通过Date.now()获取的。返回值类型是整数,并且此操作非常快,因为它基于内核OS功能,而且比较也非常快。另一方面,set函数的其余部分涉及昂贵的字符串操作和更昂贵的图形更新。因此,避免时钟状态的冗余更新非常重要。

模拟时钟和数字时钟的主要区别

计算箭头角度的最初想法是只使用小时、分钟和秒。这就是数字时钟所做的。这些值是整数。是的,它适用于数字时钟。

模拟时钟是不同的。说时针只显示小时是不正确的。一个时针应该显示秒、分钟和小时。同样,分针应该显示分钟和秒。只有精度不同。

这是错误

createClock = parent => {

    const set = time => {
        if (currentTime == time) return;
        currentTime = time;
        const date = new Date(time);
        let hour = date.getHours() % 12;
        let minute = date.getMinutes();
        let second = date.getSeconds();
        hour = hour  / 12; // wrong!
        minute = minute / 60; // wrong!
        second = second / 60;
        arrowHour.style.transform = `rotate(${hour}turn)`;
        arrowMinute.style.transform = `rotate(${minute}turn)`;
        arrowSecond.style.transform = `rotate(${second}turn)`;
    }; //set

    return set;

}; //createClock

如果这种计算方式与数字时钟相同,那么分针将停留在某个分钟刻度上,并每分钟跳到新的位置。时针将每小时跳动一个小时。相反,所有箭头都应按秒移动。这通过上面显示正确计算来实现。

如何添加装饰?

演示应用程序非常简单。如何添加所有那些阿拉伯或罗马数字标签、花哨的箭头或花哨的背景?

正确的方法是使用嵌入在HTML中的现有<svg>元素。脚本要控制的SVG元素可以添加在其顶部。当我需要做这样的事情时,我用一些矢量图形编辑器绘制适当的矢量图形并将其保存为SVG文件。我推荐Inkscape。文件可以嵌入到HTML中。通常,文件应该清除注释、冗余元数据和未使用的id属性。数字颜色最好替换为CSS颜色名称。

通常,向现有 SVG 添加一些矢量图形需要一个。可以通过其id属性或使用Document.querySelector()的其他方式获取对它的引用。

这里,最重要的是匹配坐标系和视口

CSS: 停靠布局

请注意,该应用程序的行为非常像一个“桌面应用程序”:当浏览器窗口大小调整时,<header><footer>元素始终保持在浏览器客户端区域的顶部和底部。渲染SVG的中央<main>区域会相应地缩放。至少,如果浏览器窗口不太小的话是这样的。

这是最适合单页应用程序的行为,但不仅仅是为此目的。

这是通过CSS Flexbox布局方法实现的。

flex的应用有许多细微之处。在应用程序中,CSS的相关部分被注释掉以展示正确的技术

/* flex: */
html, body { height: 100%; }
body { display: flex; flex-flow: column; }
main { flex: auto; overflow: auto; }
/* end flex */

CSS: 适应SVG

应用程序CSS的另一个棘手部分是将其SVG部分适配到其容器中并使其居中。它是通过使用相对单位实现的

section { text-align: center; }
svg { height: 80vmin; width: 80vmin; }

这样,矢量图形部分的实际大小由浏览器窗口客户端大小的宽度或高度决定,具体取决于当前的宽高比。

在这种情况下,图像比例为 1:1。如果不是,则widthheight属性值之一可以是auto。在某些情况下,可以使用属性max-contentmin-content

服务器部分怎么样?

问题是:为什么?也许这个问题可以向原始文章的作者提出,他提出了一个更大的两层应用程序。我认为使用服务器部分的Web应用程序是一回事,时钟组件是完全不同的事情,应该单独考虑。

最好的情况下,应用程序可以请求服务器部分更改时区,或者一些用于计时的时间参考点。这可以通过Ajax轻松实现。即使在这种情况下,也应使用关注点分离

© . All rights reserved.