发布于 2026-01-05 2 阅读
0

使用 React Hooks 制作霓虹时钟

使用 React Hooks 制作霓虹时钟

一点背景介绍

TL;DR:一些关于我制作这个钟表的动机的略带黑色幽默

大约两年前,也就是2017年9月,当时我在德里首都区的Zomato公司担任软件工程师,不幸感染了严重的戊型肝炎。我不得不请假,回到位于奥里萨邦沿海小镇(我的家乡)的父母家休养。戊型肝炎的康复过程漫长而痛苦,我花了整整20天。那时,我家附近的网络覆盖很差,网速慢得令人抓狂(比2G略好一些),电视频道也寥寥无几(而且大部分都是本地新闻频道)。就这样,整整20天,我几乎与世隔绝,除了休息和康复之外,也没什么其他事情可操心。当然,我的房间里也放着一些好书(大多是小说),但一本书能读的次数毕竟有限。总而言之,这种生活方式与大都市快速发展的创业公司的喧嚣截然不同。

那20天里,我一直在读书、看时间、再读书、再看时间、查看是否该吃药、再读书、再看时间,如此反复……有句话说,当你希望时间过得快一些时,时间反而过得很慢,那段时间就是如此。

最终,在康复/隔离几天后,我想,如果我必须花半辈子时间看钟,告诉自己现在是几点几分,为什么不自己写个小钟呢?我可以用传统的 HTML、CSS 和原生 JavaScript 来写,不用上网,也不用通过 npm 来获取所有现成的资源。于是我就这么做了。

两年后,也就是2019年9月,我重新审视了我之前做的那个小钟,并用React Hooks重写了它。那么,让我们一起来看看这个霓虹灯钟的制作过程吧。

钟表制作

这就是它的样子(我们将在本文中构建的时钟)。

霓虹钟

要求

  1. 它应该与系统时钟同步,每秒滴答一声。
  2. 它应该将当前时间转换为一个对象,该对象指定如何用标准英语读出该时间。
  3. 它应该从一系列词语中突出显示与当前时间相关的短语。
  4. 它应该每隔15分钟报时一次。
  5. 时钟应该有动画效果。

在 Pug 中搭建页面脚手架

因为我们将使用 React 来渲染时钟,所以现在不需要编写大量的 HTML 代码;我们只需要链接 JS 库和样式表,并创建一个div带有 id 的容器root,React 会在这个容器中渲染我们的应用程序。让我们用 Pug 快速实现一下。

我们将使用Google FontsJulius Sans One中的字体,因为这很酷。

html
  head
    title Neon Clock (Using React Hooks)
    meta(name='viewport', content='initial-scale=2.0')
    link(rel='stylesheet', href='https://fonts.googleapis.com/css?family=Julius+Sans+One')
    link(rel='styleheet', href='/style.css')

  body
    #root
    script(type='text/javascript', src='https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js')
    script(type='text/javascript', src='https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js')
Enter fullscreen mode Exit fullscreen mode

使用 Javascript 编写应用程序

抽出时间把它读出来

我们先来看一些用来读时间的短语。以下是一些以24小时制表示时间的例子,以及相应的读法:

  • 00:00- 现在是午夜
  • 00:05现在是午夜过后五点
  • 00:10现在是午夜过后十点
  • 01:00现在是凌晨一点
  • 01:15现在是凌晨一点一刻
  • 13:30现在是下午一点半
  • 11:45现在是中午12点45分。
  • 17:55现在是下午五点到六点。
  • 20:25现在是晚上八点二十五分。
  • 20:26现在是晚上八点二十五分左右。
  • 20:24现在已经快晚上八点二十五分了。
  • ... 等等

如果我们查看所有符合这种格式的字符串,就会发现它们可以按顺序由以下短语列表构成:

const phrases = [
  'IT IS',
  'ABOUT',
  'NEARLY',
  'TEN',
  'QUARTER',
  'TWENTY',
  'FIVE',
  'HALF',
  'PAST',
  'TO',
  'ONE',
  'TWO',
  'THREE',
  'FOUR',
  'FIVE',
  'SIX',
  'SEVEN',
  'EIGHT',
  'NINE',
  'TEN',
  'ELEVEN',
  'NOON',
  'MIDNIGHT',
  'O\' CLOCK',
  'IN THE',
  'MORNING',
  'AFTERNOON',
  'EVENING',
];
Enter fullscreen mode Exit fullscreen mode

请注意,“five和”ten在列表中出现了两次。这是因为这些短语在时间读出中可以出现两次(一次在分钟部分,一次在小时部分,例如 17:25、04:55 或 10:10 等)。

现在让我们编写一个函数,该函数将获取当前时间并提取小时、分钟和秒值,以及描述当前日期和当前时间的区域设置字符串。

function getNow () {
  const now = new Date(Date.now());
  const hour = now.getHours();
  const minute = now.getMinutes();
  const second = now.getSeconds();
  const display = now.toLocaleString();

  return {
    hour,
    minute,
    second,
    display,
  };
}
Enter fullscreen mode Exit fullscreen mode

现在我们有了一个函数,可以获取当前时间的简单对象,接下来让我们编写一个函数来分析这个当前时间对象,并弄清楚如何读取它。

我们可以分两步完成:

  1. 创建一个配置对象,用于描述任何给定时间的读取过程的某些方面。
  2. 生成实际时间读取字符串。

第一步,让我们考虑几个问题,在决定如何读取给定的时间值之前,我们需要回答这些问题。

  1. 我们需要秒数吗?(绝对不需要)
  2. 分钟5 的倍数吗?换句话说,分针是否直接指向钟面上的某个数字?
  3. 分钟值是否略小于 5 的倍数?换句话说,分针是否略微快于表盘上的某个数字?
  4. 分钟值是否略大于 5 的倍数?换句话说,分针是否略微落后于表盘上的某个数字?
  5. 从分针上看,最接近5的倍数是多少?
  6. 是确切的时间吗?是几点钟、中午十二点还是午夜?
  7. 现在是过了某个小时几分钟吗?
  8. 距离某个时间点还不到30分钟吗?
  9. 钟面上时针指向的数值最接近哪个数字?
  10. 现在是早上、下午还是晚上?

现在我们可以编写一个函数,该函数接受一个包含小时和分钟值的简单时间对象,并为我们回答这些问题。

function getReadoutConfig ({ hour, minute }) {
  const lastMinuteMark = Math.floor(minute / 5) * 5;
  const distFromLast = minute - lastMinuteMark;
  const isExact = distFromLast === 0;
  const isNearly = !isExact && distFromLast > 2;
  const isAbout = !isExact && !isNearly;
  const nearestMinuteMark = isNearly
    ? (lastMinuteMark + 5) % 60
    : lastMinuteMark;
  const isOClock = nearestMinuteMark === 0;
  const isPast = !isOClock && nearestMinuteMark <= 30;
  const isTo = !isOClock && !isPast;
  const minuteMark = (isPast || isOClock)
    ? nearestMinuteMark
    : 60 - nearestMinuteMark;

  const nearestHour = (isTo || (isOClock && isNearly)) ? (hour + 1) % 24 : hour;
  const nearestHour12 = nearestHour > 12
    ? nearestHour - 12
    : nearestHour;
  const isNoon = nearestHour === 12;
  const isMidnight = nearestHour === 0;
  const isMorning = !isMidnight && nearestHour < 12;
  const isAfternoon = nearestHour > 12 && nearestHour <= 18;
  const isEvening = nearestHour > 18;

  return {
    isExact,
    isAbout,
    isNearly,

    minute: minuteMark,
    isOClock: isOClock && !isNoon && !isMidnight,
    isPast,
    isTo,

    hour: nearestHour12,
    isNoon,
    isMidnight,
    isMorning,
    isAfternoon,
    isEvening,
  };
}
Enter fullscreen mode Exit fullscreen mode

在步骤 2 中,我们获取上述函数返回的配置对象,并检查需要高亮显示哪些短语才能读出给定的时间。我们将返回一个布尔值数组(true 或 false),指示是否要高亮显示短语数组中的某个短语。

function getHighlights (config) {
  return [
    true, // IT IS
    config.isAbout, // ABOUT
    config.isNearly, // NEARLY
    config.minute === 10, // TEN
    config.minute === 15, // QUARTER
    config.minute === 20 || config.minute === 25, // TWENTY
    config.minute === 5 || config.minute === 25, // FIVE
    config.minute === 30, // HALF
    config.isPast, // PAST
    config.isTo, // TO
    config.hour === 1, // ONE
    config.hour === 2, // TWO
    config.hour === 3, // THREE
    config.hour === 4, // FOUR
    config.hour === 5, // FIVE
    config.hour === 6, // SIX
    config.hour === 7, // SEVEN
    config.hour === 8, // EIGHT
    config.hour === 9, // NINE
    config.hour === 10, // TEN
    config.hour === 11, // ELEVEN
    config.isNoon, // NOON
    config.isMidnight, // MIDNIGHT
    config.isOClock, // O' CLOCK
    config.isMorning || config.isAfternoon || config.isEvening, // IN THE
    config.isMorning, // MORNING
    config.isAfternoon, // AFTERNOON
    config.isEvening, // EVENING
  ];
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以通过连接短语数组中高亮显示的短语来获得实际的时间读数字符串:

const readoutConfig = getReadoutConfig(time);
const highlighted = getHighlights(readoutConfig);
const readoutString = phrases.filter((phrase, index) => highlighted[index]).join(' ');
Enter fullscreen mode Exit fullscreen mode

钩子useClock

现在我们有了获取和读取当前时间的函数,我们需要某种方法来确保这些函数每秒都与系统时钟同步使用。我们可以通过以下方式实现这一点:

  1. 现在看看时间
  2. 决定下一秒何时开始。
  3. 当下一秒开始时,记录一个 1000 毫秒(1 秒)的间隔。
  4. 每次时间间隔到达时,更新我们应用程序中的当前时间。

我们来为此编写一个React Hook,useClock并将其命名为 `setTimeout` 。首先,我们需要一个名为 `time` 的状态值time来跟踪当前时间。然后,我们需要另一个名为 `setInterval` 的状态值timer来跟踪我们是否设置了间隔。

我们的钩子会检查是否timer已设置或间隔,如果没有,则会设置间隔。这段逻辑可以使用 `useEffect` 编写,它会在应用程序首次渲染时运行一次。除非我们清除间隔并将 `useEffect` 设置为 `true` useEffect,否则此效果不需要在每次后续渲染时都运行timernull

每次时间间隔到来时,我们将状态设置time为当前时间。

因为钩子的用户useClock不应该time自己设置值,而只能读取值,所以我们只会timeuseClock钩子返回。

function useClock () {
  const [timer, setTimer] = React.useState(null);
  const [time, setTime] = React.useState(getNow());

  // this effect will run when our app renders for the first time
  React.useEffect(() => {
    // When this effect runs, initialize the timer / interval
    if (!timer) initTimer();

    // This returned function will clear the interval when our app unmounts
    return (() => (timer && window.clearInterval(timer) && setTimer(null));

  }, [timer]); // This hook will run only when the value of timer is set or unset

  function initTimer () {
    const now = Date.now();
    const nextSec = (Math.floor(now / 1000) + 1) * 1000;
    const timeLeft = nextSec - now;

    // Register an interval beginning next second
    window.setTimeout(() => {
      // on each second update the state time
      const interval = window.setInterval(() => setTime(getNow()), 1000);

      // now our timer / interval is set
      setTimer(interval);
    }, timeLeft);
  }

  return time;
}
Enter fullscreen mode Exit fullscreen mode

渲染时钟和读出组件

现在一切就绪,让我们编写一些组件来渲染我们的应用程序。首先,我们需要一个应用程序组件,它将渲染到root div我们在 Pug 文件中创建的容器内。它将包含一个标准的模拟时钟组件和一个时间读取组件。

function NeonClock () {
  const time = useClock();
  return (
    <div className='clock'>
      <StandardClock time={time} />
      <TimeReadout time={time} />
    </div>
  );
}

const root = document.getElementById('root');
ReactDOM.render(<NeonClock />, root);
Enter fullscreen mode Exit fullscreen mode

我们StandardClock先来构建这个组件。它看起来像一个模拟时钟,并且会带有动画效果。为了模拟模拟时钟,它会有一个表盘,表盘上有 12 个罗马数字和 60 个小线段。这 60 个小线段中,每隔 5 个线段需要稍长一些。ticks为了方便起见,我们把这些小线段称为“小线段”。时钟当然会有 3 根指针,它们会以各自的速度旋转。

如图所示,这台钟唯一会动的部件就是三根指针。我们可以通过设置 CSS 来控制它们的旋转运动transform: rotate(xx.x deg)

function StandardClock ({ time }) {
  const clockMarks = [ 'XII', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX', 'X', 'XI' ];

  // Calculate the angles of rotation of each hand (in degrees)
  const hourAngle = ((time.hour % 12) * 60 + time.minute) / 2;
  const minuteAngle = (time.minute * 60 + time.second) / 10;
  const secondAngle = time.second * 6;

  return (
    <div className='standard-clock'>
      <div>
        { clockMarks.map(mark => <span className='mark'>{mark}</span>) }
      </div>
      <div>
        { Array(60).fill(1).map(tick => <span className='tick' />) }
      </div>
      <div className='inner-circle' />
      <div className='inner-circle-2' />
      <div className='hour-hand' style={{ transform: `rotate(${hourAngle}deg)` }} />
      <div className='minute-hand' style={{ transform: `rotate(${minuteAngle}deg)` }} />
      <div className='second-hand' style={{ transform: `rotate(${secondAngle}deg)` }} />
      <div className='center' />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

接下来,我们来构建时间读出组件。该组件当然会包含各种短语,其中一些会被高亮显示。此外,它还会包含一个语音播报组件,该组件会使用window.speechSynthesisAPI每隔 15 分钟播报一次时间。

为了以一种炫酷的方式显示读出文本,我们将以静音方式显示所有短语,并glow为应该突出显示的短语添加一个类。

function TimeReadout ({ time }) {
  // get the highlighted phrases and the readout string
  const readoutConfig = getReadoutConfig(time);
  const highlighted = getHighlights(readoutConfig);
  const timeText = phrases.filter((phrase, index) => highlighted[index]).join(' ') + '.';

  // speak out the time only on the first second of each 15 minutes
  const shouldSpeak = time.second === 0 && time.minute % 15 === 0;

  return (
    <div className='readout'>
      <p className='phrases'>
        { phrases.map((phrase, index) => (
          <span className={highlighted[index] ? 'glow' : ''}>
            {phrase}
          </span>
        ))}
      </p>
      <p className='timer'>{time.display}</p>
      <Speaker active={shouldSpeak} text={timeText} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

完成这些之后,我们来构建Speaker组件。首先,我们需要一个函数,能够用标准的英式口音朗读任何给定的文本(因为英国人说英语的方式才是正确的,那就是幽默风趣。而且据说英语最初就是他们发明的,真为他们高兴!)

要朗读文本,首先需要为文本创建一个话语对象,并设置语速(朗读速度)、音调(声音的音高)、音量和语音模板(我们将使用第一个出现的语音en-GB)。然后,我们可以将此话语对象传递给speechSynthesis.speak函数,使其实际朗读出来。

function speak (text) {
  const synth = window.speechSynthesis;
  const rate = 0.7;
  const pitch = 0.6;
  const voice = synth.getVoices().filter(v => v.lang === 'en-GB')[0];
  const utterance = new SpeechSynthesisUtterance(text);
  utterance.voice = voice;
  utterance.pitch = pitch;
  utterance.rate = rate;
  synth.speak(utterance);
}
Enter fullscreen mode Exit fullscreen mode

现在我们可以创建一个Speaker组件,它不会渲染任何内容,但会使用一个钩子在 prop设置为 trueuseEffect时调用 speak 函数activetrue

function Speaker ({ active, text }) {
  React.useEffect (() => {
    if (active) speak(text);
  });
  return null;
}
Enter fullscreen mode Exit fullscreen mode

组件样式

所有组件和逻辑都已就绪,接下来我们使用 LESS 来设置组件样式。本节将简要介绍样式设置中的一些要点,具体细节请参考本文的示例代码。

柔和而闪亮的短语

柔和文字效果是通过使用柔和的深红色和 2 像素的模糊效果实现的。发光效果是通过使用更亮(接近白色)的红色和text-shadow20 像素扩展范围的红色来实现的。此外,font-weight发光文字的亮度也进行了设置,bold使其看起来更醒目、更明亮。

span {
  color: @muted-red;
  margin: 0 10px;
  transition: all 0.5s cubic-bezier(0.6, -0.51, 0.5, 1.51);
  vertical-align: middle;
  filter: blur(2px);

  &.glow {
    color: @glowing-red;
    text-shadow: 0 0 20px @shadow-red;
    font-weight: bold;
    filter: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

表盘上的罗马数字

时钟的表盘(圆盘)尺寸为300px * 300px。每个罗马数字都采用绝对定位,10px位于外圆内,并相对于外圆水平居中。transform-origin包含数字的跨度与圆心重合。

.mark {
  position: absolute;
  display: inline-block;
  top: 10px;
  left: 115px;
  width: 50px;
  height: 30px;
  font-size: 30px;
  text-align: center;
  color: @glowing-red;
  filter: none;
  transform-origin: 25px 140px;
}
Enter fullscreen mode Exit fullscreen mode

然后,将包含数字的每个跨度以 30 度为增量旋转。我们使用 LESS 递归 mixin 生成旋转,并span通过nth-child选择器将其应用于这些跨度。

.generate-mark-rotation (@n) when (@n = 13) {}
.generate-mark-rotation (@n) when (@n < 13) {
  span.mark:nth-child(@{n}) {
    transform: rotate((@n - 1) * 30deg);
  }
  .generate-mark-rotation(@n + 1);
}
.generate-mark-rotation(2);
Enter fullscreen mode Exit fullscreen mode

采用相同的方法将拨盘上的 60 个线段放置到位。

手的放置和旋转

首先将双手放置在指定00:00:00位置,使用绝对定位,使每只手的底部与圆心重合。然后,将transform-origin双手的顶部也设置为与圆心重合。

transform:rotate(xx.x deg)React 组件在指针上设置该值时div,指针会相对于时钟中心旋转。

使其响应式

为简化起见,我们将小屏幕设备的上限设定为[此处应填写具体数值] 960px。对于更小的屏幕,我们使用越来越font sizes小的时钟组件尺寸。这样可以确保时钟在所有设备上都能得到良好的响应。

这就是本文中描述的所有物品所在的笔。

希望您喜欢阅读这篇关于这个小项目的文章,并从中有所收获。您可以在gnsp.in
上找到更多关于我的信息

感谢阅读!

文章来源:https://dev.to/gnsp/making-a-neon-clock-using-react-hooks-mei