admin 发表于 3 天前

多语言轮盘时钟

这是一个使用 HTML 和 CSS 制作的动态时钟组件,支持展示多层时间刻度(包括年份、月份、日期、星期、小时、分钟和秒),并带有多语言切换功能。页面采用响应式设计,适配不同屏幕宽度,同时运用了 JavaScript 进行动画和交互。
https://s1.img.fenx.top/s/68c41ee2821e7.png

HTML在线运行工具:https://tool.fenx.top/beeseek/HTML/

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <title>多语言轮盘时钟</title>
    <style>
      *,
      ::before,
      ::after {
      box-sizing: border-box;
      }
      :root {
      --clr-bg: rgb(3 3 3);
      --clock-size: 800px;
      --clock-clr: rgb(12, 74, 110);
      }
      body {
      margin: 0;
      min-height: 100svh;
      display: grid;
      place-content: center;
      font-family: system-ui;
      background-color: var(--clr-bg);
      background-image: radial-gradient(rgb(8, 47, 73), rgb(8, 47, 60));
      background-blend-mode: difference;
      }

      .clock {
      position: fixed;
      inset: 0;
      margin: auto;
      width: var(--clock-size);
      height: var(--clock-size);
      aspect-ratio: 1;
      place-content: center;
      background: var(--clock-clr);
      border-radius: 50%;
      }
      @media (width < 800px) {
      .clock {
          left: 0;
          right: auto;
          translate: calc((50% - 2rem) * -1) 0;
      }
      }
      /* semi transparent mask to hide not active time data*/
      .clock::before {
      content: '';
      position: absolute;
      inset: 1px;
      margin: auto;
      background-color: rgba(0 0 0 / 0.85);
      clip-path: polygon(
          0 0,
          100% 0,
          100% 48%,
          50% 48%,
          50% 52%,
          100% 52%,
          100% 100%,
          0 100%
      );
      border-radius: 50%;
      z-index: 20;
      }

      .clock > div {
      position: absolute;
      inset: 0;
      margin: auto;
      width: var(--clock-d);
      height: var(--clock-d);
      font-size: var(--f-size, 0.9rem);
      aspect-ratio: 1;
      isolation: isolate;
      aspect-ratio: 1;
      border-radius: 50%;
      }

      /* years */
      .clock > div:nth-of-type(1) {
      --clock-d: calc(var(--clock-size) - 20px);
      }
      /* seconds */
      .clock > div:nth-of-type(2) {
      --clock-d: calc(var(--clock-size) - 130px);
      }
      /* minutes */
      .clock > div:nth-child(3) {
      --clock-d: calc(var(--clock-size) - 195px);
      }

      /* hours */
      .clock > div:nth-child(4) {
      --clock-d: calc(var(--clock-size) - 260px);
      }
      /* day numbers */
      .clock > div:nth-child(5) {
      --clock-d: calc(var(--clock-size) - 350px);
      }
      /* month names*/
      .clock > div:nth-child(6) {
      --clock-d: calc(var(--clock-size) - 470px);
      }
      /* day names*/
      .clock > div:nth-child(7) {
      --clock-d: calc(var(--clock-size) - 600px);
      }

      .clock-face {
      position: relative;
      width: 100%;
      height: 100%;
      aspect-ratio: 1;
      border-radius: 50%;
      transition: 300ms linear;
      }
      .clock-face > * {
      position: absolute;
      transform-origin: center;
      white-space: nowrap;
      color: white;
      opacity: 0.75;

      &.active {
          opacity: 1;
      }
      }

      .clock > .current-lang-display {
      position: absolute;
      inset: 0;
      margin: auto;
      z-index: 100;
      display: grid;
      place-content: center;
      background-color: var(--clock-clr);
      border: 1px solid rgba(255 255 255 / 0.25);
      color: white;
      border-radius: 50%;
      width: 40px;
      height: 40px;
      aspect-ratio: 1/1;
      cursor: pointer;
      transition: 300ms ease-in-out;
      font-size: 1.5rem;
      outline: none;
      &:focus-visible,
      &:hover {
          background-color: white;
      }
      }
      /* time seperators */
      .current-lang-display::before,
      .current-lang-display::after {
      content: ': ';
      color: white;
      position: absolute;
      z-index: 199;
      top: 50%;
      right: 0;
      font-size: 0.9rem;
      translate: 283px -10px;
      }
      .current-lang-display::after {
      font-size: 0.9rem;
      translate: 250px -10px;
      }

      /* Dialog styles */
      dialog {
      width: min(calc(100% - 2rem), 380px);
      padding: 1rem;
      border: none;
      border-radius: 999px;
      background: rgba(0 0 0 / 0.25);
      text-align: center;
      aspect-ratio: 1;
      overflow: visible;

      @starting-style {
          opacity: 0;
          scale: 0;
      }
      transition: opacity 500ms ease-in,
          scale 500ms cubic-bezier(0.28, -0.55, 0.27, 1.55);
      }
      /* Backdrop styling */
      dialog::backdrop {
      background-color: rgba(from black r g b / 0.5);
      backdrop-filter: blur(3px);
      opacity: 1;
      @starting-style {
          opacity: 0;
      }
      transition: opacity 1000ms ease-in; /* this doesn't appear to be working */
      }

      dialog .btn-dialog-close {
      position: absolute;
      top: 0rem;
      right: 25%;
      aspect-ratio: 1;
      width: 40px;
      height: 40px;
      border-radius: 50%;
      background-color: black;
      font-size: 1.2rem;
      color: white;
      border: none;
      outline: none;
      cursor: pointer;
      transition: rotate 300ms ease-in-out;
      z-index: 11;
      &:focus-visible,
      &:hover {
          rotate: 90deg;
      }
      }

      .language-options {
      position: absolute;
      inset: 0;
      margin: auto;
      border-radius: 50%;
      aspect-ratio: 1/1;
      overflow: hidden;
      }
      .language-options > label {
      position: absolute;
      transform: translate(-50%, -50%);
      cursor: pointer;
      font-size: 0.9rem;
      aspect-ratio: 1/1;
      border-radius: 50%;
      width: 36px;
      height: 36px;
      transition: 300ms ease-in-out;
      display: grid;
      place-content: center;
      transform-origin: center;

      &.active {
          color: white;
          background: var(--clock-clr);
      }

      &:focus-visible,
      &:hover {
          scale: 1.1;
          z-index: 2;
      }
      }
      .language-title {
      position: absolute;
      left: 50%;
      top: 50%;
      transform: translate(-50%, -50%);
      pointer-events: none;
      color: white;
      font-size: 1.2rem;
      @starting-style {
          opacity: 0;
      }
      transition: opacity 300ms ease-in-out;
      }
      .flag-icon {
      font-size: 1.5rem;
      display: grid;
      place-content: center;
      }
      .language-options input {
      position: absolute;
      width: 1px;
      height: 1px;
      margin: -1px;
      padding: 0;
      border: 0;
      clip: rect(0, 0, 0, 0);
      overflow: hidden;
      }
    </style>
</head>
<body>
    <!-- partial:index.partial.html -->
    <div class="clock" data-date="2024-12-25">
      <div>
      <div data-clock="years" data-numbers="101" class="clock-face"></div>
      </div>
      <div>
      <div data-clock="seconds" data-numbers="60" class="clock-face"></div>
      </div>
      <div>
      <div data-clock="minutes" data-numbers="60" class="clock-face"></div>
      </div>
      <div>
      <div data-clock="hours" data-numbers="24" class="clock-face"></div>
      </div>
      <div>
      <div data-clock="days" data-numbers="31" class="clock-face"></div>
      </div>
      <div>
      <div data-clock="months" data-numbers="12" class="clock-face"></div>
      </div>

      <div>
      <div data-clock="day-names" data-numbers="7" class="clock-face"></div>
      </div>
      <button type="button" id="current-lang" class="current-lang-display">
      en
      </button>
    </div>

    <dialog id="language-dialog">
      <button
      type="button"
      id="btn-dialog-close"
      class="btn-dialog-close"
      autofocus
      >
      ✕
      </button>
      <div id="language-options" class="language-options"></div>
    </dialog>
    <!-- partial -->
    <script>
      console.clear();

      // array of JavaScript supported languages for local dates (not definitive)
      const languageFlags = [
      { code: 'ar-SA', name: 'Arabic (Saudi Arabia)', flag: '🇸🇦' },
      { code: 'cs-CZ', name: 'Czech (Czech Republic)', flag: '🇨🇿' },
      { code: 'da-DK', name: 'Danish (Denmark)', flag: '🇩🇰' },
      { code: 'de-DE', name: 'German (Germany)', flag: '🇩🇪' },
      { code: 'el-GR', name: 'Greek (Greece)', flag: '🇬🇷' },
      { code: 'en-US', name: 'English (US)', flag: '🇺🇸' },
      { code: 'en-GB', name: 'English (UK)', flag: '🇬🇧' },
      { code: 'es-ES', name: 'Spanish (Spain)', flag: '🇪🇸' },
      { code: 'es-MX', name: 'Spanish (Mexico)', flag: '🇲🇽' },
      { code: 'fi-FI', name: 'Finnish (Finland)', flag: '🇫🇮' },
      { code: 'fr-CA', name: 'French (Canada)', flag: '🇨🇦' },
      { code: 'fr-FR', name: 'French (France)', flag: '🇫🇷' },
      { code: 'he-IL', name: 'Hebrew (Israel)', flag: '🇮🇱' },
      { code: 'hi-IN', name: 'Hindi (India)', flag: '🇮🇳' },
      { code: 'hu-HU', name: 'Hungarian (Hungary)', flag: '🇭🇺' },
      { code: 'it-IT', name: 'Italian (Italy)', flag: '🇮🇹' },
      { code: 'ja-JP', name: 'Japanese (Japan)', flag: '🇯🇵' },
      { code: 'ko-KR', name: 'Korean (South Korea)', flag: '🇰🇷' },
      { code: 'nl-NL', name: 'Dutch (Netherlands)', flag: '🇳🇱' },
      { code: 'no-NO', name: 'Norwegian (Norway)', flag: '🇳🇴' },
      { code: 'pl-PL', name: 'Polish (Poland)', flag: '🇵🇱' },
      { code: 'pt-BR', name: 'Portuguese (Brazil)', flag: '🇧🇷' },
      { code: 'pt-PT', name: 'Portuguese (Portugal)', flag: '🇵🇹' },
      { code: 'ro-RO', name: 'Romanian (Romania)', flag: '🇷🇴' },
      { code: 'ru-RU', name: 'Russian (Russia)', flag: '🇷🇺' },
      { code: 'sv-SE', name: 'Swedish (Sweden)', flag: '🇸🇪' },
      { code: 'th-TH', name: 'Thai (Thailand)', flag: '🇹🇭' },
      { code: 'tr-TR', name: 'Turkish (Turkey)', flag: '🇹🇷' },
      { code: 'vi-VN', name: 'Vietnamese (Vietnam)', flag: '🇻🇳' },
      { code: 'zh-CN', name: 'Chinese (Simplified, China)', flag: '🇨🇳' },
      ];

      const RADIUS = 140; // Radius of the circle for flag buttons
      /*

{ code: 'zh-TW', name: 'Chinese (Traditional, Taiwan)', flag: '🇹🇼' },
*/

      // map for default regions based on languageFlags
      const defaultRegions = languageFlags.reduce((map, lang) => {
      const baseLang = lang.code.split('-'); // Extract the base language (e.g., 'en' from 'en-US')
      if (!map) {
          map = lang.code;
      }
      return map;
      }, {});

      function getLocale() {
      // cet the primary language from navigator.languages or fallback to navigator.language
      let language =
          (navigator.languages && navigator.languages) ||
          navigator.language ||
          'en-US';

      // not all browsers return the complete lang code so we have to add it from the mapped values
      if (language.length === 2) {
          language =
            defaultRegions || `${language}-${language.toUpperCase()}`;
      }
      return language;
      }

      let locale = getLocale();

      const currentLangDisplay = document.getElementById('current-lang');
      const languageDialog = document.getElementById('language-dialog');
      const languageOptionsContainer =
      document.getElementById('language-options');
      const closeButton = document.getElementById('btn-dialog-close');

      function drawClockFaces() {
      const clockFaces = document.querySelectorAll('.clock-face');

      // Get the current date details
      const currentDate = new Date();
      const currentDay = currentDate.getDate();
      const currentMonth = currentDate.getMonth();
      const currentYear = currentDate.getFullYear();
      const currentWeekday = currentDate.getDay();
      const currentHours = currentDate.getHours();
      const currentMinutes = currentDate.getMinutes();
      const currentSeconds = currentDate.getSeconds();
      const totalDaysInMonth = new Date(
          currentYear,
          currentMonth + 1,
          0
      ).getDate();

      const weekdayNames = Array.from({ length: 7 }, (_, i) =>
          new Intl.DateTimeFormat(locale, { weekday: 'long' }).format(
            new Date(2021, 0, i + 3)
          )
      );
      const monthNames = Array.from({ length: 12 }, (_, i) =>
          new Intl.DateTimeFormat(locale, { month: 'long' }).format(
            new Date(2021, i)
          )
      );

      clockFaces.forEach((clockFace) => {
          clockFace.innerHTML = '';

          const clockType = clockFace.getAttribute('data-clock');
          const numbers = parseInt(clockFace.getAttribute('data-numbers'), 10);
          const RADIUS = clockFace.offsetWidth / 2 - 20; // Adjusted for padding
          const center = clockFace.offsetWidth / 2;

          let valueSet;
          let currentValue;

          // define the value set and current value for each clock type
          switch (clockType) {
            case 'seconds':
            valueSet = Array.from({ length: 60 }, (_, i) =>
                String(i).padStart(2, '0')
            );
            currentValue = String(currentSeconds).padStart(2, '0');
            break;
            case 'minutes':
            valueSet = Array.from({ length: 60 }, (_, i) =>
                String(i).padStart(2, '0')
            );
            currentValue = String(currentMinutes).padStart(2, '0');
            break;
            case 'hours':
            valueSet = Array.from({ length: 24 }, (_, i) =>
                String(i).padStart(2, '0')
            );
            currentValue = String(currentHours).padStart(2, '0');
            break;
            case 'days':
            valueSet = Array.from(
                { length: totalDaysInMonth },
                (_, i) => i + 1
            );
            currentValue = currentDay;
            break;
            case 'months':
            valueSet = monthNames;
            currentValue = currentMonth;
            break;
            case 'years':
            valueSet = Array.from({ length: 101 }, (_, i) => 2000 + i);
            currentValue = currentYear;
            break;
            case 'day-names':
            valueSet = weekdayNames;
            currentValue = currentWeekday;
            break;
            default:
            return;
          }

          // create and position elements on the clock face
          valueSet.forEach((value, i) => {
            const angle = i * (360 / numbers);
            const x = center + RADIUS * Math.cos((angle * Math.PI) / 180);
            const y = center + RADIUS * Math.sin((angle * Math.PI) / 180);

            const element = document.createElement('span');
            element.classList.add('number');
            element.textContent = value;
            element.style.left = `${x}px`;
            element.style.top = `${y}px`;
            element.style.transform = `translate(-50%, -50%) rotate(${angle}deg)`;

            clockFace.appendChild(element);
          });

          // rotation to align the current value to the 3 o'clock position
          const currentIndex = valueSet.indexOf(
            typeof valueSet === 'string'
            ? String(currentValue)
            : currentValue
          );
          const rotationAngle = -((currentIndex / numbers) * 360);
          clockFace.style.transform = `rotate(${rotationAngle}deg)`;
      });
      }

      function rotateClockFaces() {
      const clockFaces = document.querySelectorAll('.clock-face');

      const lastAngles = {};
      function updateRotations() {
          const now = new Date();
          const currentSecond = now.getSeconds();
          const currentMinute = now.getMinutes();
          const currentHour = now.getHours();
          const currentDay = now.getDate();
          const currentMonth = now.getMonth(); // 0-indexed
          const currentYear = now.getFullYear();
          const currentWeekday = now.getDay(); // 0 = Sunday, 6 = Saturday

          clockFaces.forEach((clockFace) => {
            const clockType = clockFace.getAttribute('data-clock');
            const totalNumbers = parseInt(
            clockFace.getAttribute('data-numbers'),
            10
            );

            let currentValue;
            switch (clockType) {
            case 'seconds':
                currentValue = currentSecond;
                break;
            case 'minutes':
                currentValue = currentMinute;
                break;
            case 'hours':
                currentValue = currentHour;
                break;
            case 'days':
                currentValue = currentDay - 1;
                break;
            case 'months':
                currentValue = currentMonth;
                break;
            case 'years':
                currentValue = currentYear - 2000;
                break;
            case 'day-names':
                currentValue = currentWeekday; // 0 = Sunday
                break;
            default:
                return;
            }

            const targetAngle = (360 / totalNumbers) * currentValue;

            // Retrieve the last angle for this clock face
            const clockId = clockFace.id || clockType;
            const lastAngle = lastAngles || 0;

            // Adjust for shortest rotation direction
            const delta = targetAngle - lastAngle;
            const shortestDelta = ((delta + 540) % 360) - 180; // Normalize between -180 and 180

            // update the clock face rotation
            const newAngle = lastAngle + shortestDelta;
            clockFace.style.transform = `rotate(${newAngle * -1}deg)`;

            // store the new angle
            lastAngles = newAngle;

            // "active" class
            const numbers = clockFace.querySelectorAll('.number');
            numbers.forEach((number, index) => {
            if (index === currentValue) {
                number.classList.add('active');
            } else {
                number.classList.remove('active');
            }
            });
          });
          // request next frame
          requestAnimationFrame(updateRotations);
      }

      updateRotations();
      }

      // create language options
      function createLanguageOptions() {
      const centerX = languageOptionsContainer.offsetWidth / 2;
      const centerY = languageOptionsContainer.offsetHeight / 2;

      languageFlags.forEach((lang, index, arr) => {
          const angle = (index / arr.length) * 2 * Math.PI;
          const x = centerX + RADIUS * Math.cos(angle);
          const y = centerY + RADIUS * Math.sin(angle);

          const radioWrapper = document.createElement('label');
          radioWrapper.title = lang.name;
          radioWrapper.style.left = `${x}px`;
          radioWrapper.style.top = `${y}px`;

          const radioInput = document.createElement('input');
          radioInput.type = 'radio';
          radioInput.name = 'language';
          radioInput.value = lang.code;

          if (lang.code === locale) {
            radioInput.checked = true;
            radioWrapper.classList.add('active');
          }

          const flag = document.createElement('span');
          flag.classList.add('flag-icon');
          flag.innerText = lang.flag;

          radioWrapper.appendChild(radioInput);
          radioWrapper.appendChild(flag);
          languageOptionsContainer.appendChild(radioWrapper);

          // Handle hover: display language name in the center of the parent container
          radioWrapper.addEventListener('mouseover', () =>
            showTitle(lang.name, radioWrapper)
          );
          radioWrapper.addEventListener('mouseleave', () => hideTitle());

          radioInput.addEventListener('change', () => {
            locale = radioInput.value;
            setCurrentLangDisplay(lang);
            drawClockFaces();
            document.querySelector('label.active')?.classList.remove('active');
            radioWrapper.classList.add('active');
            closeDialog();
          });
      });
      }

      // Show title (language name) in the center
      let titleDisplay = null; // Declare titleDisplay globally for reuse
      function showTitle(languageName) {
      if (titleDisplay) {
          titleDisplay.remove();
      }
      titleDisplay = document.createElement('div');
      titleDisplay.classList.add('language-title');
      titleDisplay.textContent = languageName; // Update the title with the language name
      languageOptionsContainer.appendChild(titleDisplay);
      }
      function hideTitle() {
      if (titleDisplay) {
          titleDisplay.textContent = '';
      }
      }
      // Set current language display button flag and title
      function setCurrentLangDisplay(lang) {
      currentLangDisplay.textContent = lang.flag;
      currentLangDisplay.title = lang.name;
      showTitle(lang.name);
      }
      function openDialog() {
      languageDialog.showModal();
      createLanguageOptions();
      addDialogCloseListener();
      }
      function closeDialog() {
      languageDialog.close();
      removeLanguageOptions();
      removeDialogCloseListener();
      }
      function removeLanguageOptions() {
      languageOptionsContainer.innerHTML = '';
      }
      function addDialogCloseListener() {
      languageDialog.addEventListener('click', closeDialogOnClickOutside);
      }
      function removeDialogCloseListener() {
      languageDialog.removeEventListener('click', closeDialogOnClickOutside);
      }
      function closeDialogOnClickOutside(e) {
      if (e.target === languageDialog) {
          closeDialog();
      }
      }

      // dialog close button event
      closeButton.addEventListener('click', closeDialog);

      // current language display - open dialog with lang buttons
      currentLangDisplay.addEventListener('click', openDialog);
      //console.log(locale);

      // initalize
      drawClockFaces();
      rotateClockFaces();
      setCurrentLangDisplay(languageFlags.find((lang) => lang.code === locale));
    </script>
</body>
</html>


页: [1]
查看完整版本: 多语言轮盘时钟