Compare commits
No commits in common. "2a40d05cacae607694826521a9f7ba8f901758d6" and "4518e2e57e84adb0d6dd07d6ac64865c62079eea" have entirely different histories.
2a40d05cac
...
4518e2e57e
@ -5,7 +5,6 @@
|
||||
## 项目概述
|
||||
|
||||
一个精简的 React + TypeScript + Vite 入门项目。
|
||||
主要功能是在页面上显示数码风格的时钟。
|
||||
|
||||
- **Node 版本**: v24.14.0(在 `.nvmrc` 中指定)
|
||||
- **包管理器**: npm
|
||||
@ -67,8 +66,3 @@ src/
|
||||
## 构建输出
|
||||
|
||||
生产构建输出到 `dist/` 目录(ESLint 会忽略该目录)。
|
||||
|
||||
## 本地存储
|
||||
|
||||
本地存储使用localStorage,用于保存用户设置。
|
||||
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
# 翻页
|
||||
|
||||
这里是关于翻页的设计文档
|
||||
|
||||
翻页样式是模拟常见于机场的用于显示航班信息的老式机械翻页机。
|
||||
|
||||
基本样式就是上下两个方块,分别显示数字的上半部分和下半部分。
|
||||
|
||||
每次翻页时,上半部分会沿着上半部分的下边缘向下翻转,显示出后面背景的下一个数字的上半部分。然后,下半部分会沿着下半部分的上边缘向下翻转,显示出后面背景的下一个数字的下半部分。
|
||||
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
#!/bin/bash
|
||||
source ~/.nvm/nvm.sh
|
||||
nvm use
|
||||
|
||||
134
src/App.css
134
src/App.css
@ -8,11 +8,26 @@
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
width: 100%;
|
||||
padding-top: 40px;
|
||||
box-sizing: border-box;
|
||||
gap: 40px;
|
||||
}
|
||||
|
||||
.clock-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Digital clock style (default) */
|
||||
.digital-clock {
|
||||
font-family: 'DSEG7', monospace;
|
||||
font-size: 15vw;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
/* Style selector */
|
||||
@ -23,113 +38,6 @@
|
||||
padding: 8px;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
z-index: 10;
|
||||
flex-shrink: 0;
|
||||
margin-bottom: 60px;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.clock-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
padding: 0 20px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.digital-clock-outer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.digital-clock-side-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.digital-clock-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.digital-clock-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.digital-clock-header-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.digital-clock-period {
|
||||
font-family: Arial, sans-serif;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.digital-clock-date {
|
||||
font-family: Arial, sans-serif;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.format-toggle-side {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.format-toggle-side:hover {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.clock-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
max-width: 100vw;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.digital-clock {
|
||||
font-family: 'DSEG7', monospace;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
|
||||
letter-spacing: 0.05em;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.style-button {
|
||||
@ -156,6 +64,10 @@
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.digital-clock {
|
||||
font-size: 12vw;
|
||||
}
|
||||
|
||||
.style-selector {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
126
src/App.tsx
126
src/App.tsx
@ -5,65 +5,10 @@ import './App.css'
|
||||
|
||||
type ClockStyle = 'digital' | 'flip' | 'roll'
|
||||
|
||||
interface DateInfo {
|
||||
dateString: string
|
||||
weekday: string
|
||||
}
|
||||
|
||||
function formatDate(date: Date): DateInfo {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
|
||||
const weekday = weekdays[date.getDay()]
|
||||
|
||||
return {
|
||||
dateString: `${year}-${month}-${day}`,
|
||||
weekday
|
||||
}
|
||||
}
|
||||
|
||||
function DigitalClock({ time, size, period, is24Hour, onToggleFormat, dateInfo }: { time: string; size: number; period?: string; is24Hour: boolean; onToggleFormat: () => void; dateInfo: DateInfo }) {
|
||||
const fontSize = Math.floor(size * 0.95)
|
||||
const periodSize = Math.floor(size * 0.20)
|
||||
const dateSize = Math.floor(size * 0.20)
|
||||
|
||||
function DigitalClock({ time }: { time: string }) {
|
||||
return (
|
||||
<div className="digital-clock-outer">
|
||||
<div className="digital-clock-side-panel">
|
||||
<button
|
||||
className="format-toggle-side"
|
||||
onClick={onToggleFormat}
|
||||
>
|
||||
{is24Hour ? '24H' : '12H'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="digital-clock-wrapper">
|
||||
<div className="digital-clock-header">
|
||||
<div className="digital-clock-header-left">
|
||||
{!is24Hour && period && (
|
||||
<span
|
||||
className="digital-clock-period"
|
||||
style={{ fontSize: `${periodSize}px` }}
|
||||
>
|
||||
{period}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className="digital-clock-date"
|
||||
style={{ fontSize: `${dateSize}px` }}
|
||||
>
|
||||
{dateInfo.dateString} {dateInfo.weekday}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="digital-clock"
|
||||
style={{ fontSize: `${fontSize}px`, lineHeight: '1' }}
|
||||
>
|
||||
{time}
|
||||
</div>
|
||||
</div>
|
||||
<div className="digital-clock">
|
||||
{time}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -71,8 +16,6 @@ function DigitalClock({ time, size, period, is24Hour, onToggleFormat, dateInfo }
|
||||
function App() {
|
||||
const [time, setTime] = useState(new Date())
|
||||
const [clockStyle, setClockStyle] = useState<ClockStyle>('digital')
|
||||
const [clockSize, setClockSize] = useState(120)
|
||||
const [is24Hour, setIs24Hour] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
@ -81,67 +24,26 @@ function App() {
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
// Responsive clock size - fit within screen width
|
||||
useEffect(() => {
|
||||
const updateSize = () => {
|
||||
const width = window.innerWidth
|
||||
// More conservative for small screens
|
||||
const margin = width < 480 ? 40 : 60
|
||||
const availableWidth = width - margin
|
||||
// Use larger divisor for small screens to prevent overflow
|
||||
const divisor = width < 480 ? 7 : 6
|
||||
const newSize = Math.floor(availableWidth / divisor)
|
||||
setClockSize(Math.min(newSize, 200))
|
||||
}
|
||||
updateSize()
|
||||
window.addEventListener('resize', updateSize)
|
||||
return () => window.removeEventListener('resize', updateSize)
|
||||
}, [])
|
||||
|
||||
// Format time based on 12/24 hour setting
|
||||
const formatTime = () => {
|
||||
let hours = time.getHours()
|
||||
const minutes = String(time.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(time.getSeconds()).padStart(2, '0')
|
||||
let period: string | undefined
|
||||
|
||||
if (!is24Hour) {
|
||||
period = hours >= 12 ? 'PM' : 'AM'
|
||||
hours = hours % 12 || 12
|
||||
}
|
||||
|
||||
return {
|
||||
timeString: `${String(hours).padStart(2, '0')}:${minutes}:${seconds}`,
|
||||
period
|
||||
}
|
||||
}
|
||||
|
||||
const { timeString, period } = formatTime()
|
||||
const dateInfo = formatDate(time)
|
||||
const timeString = `${String(time.getHours()).padStart(2, '0')}:${String(time.getMinutes()).padStart(2, '0')}:${String(time.getSeconds()).padStart(2, '0')}`
|
||||
|
||||
const renderClock = () => {
|
||||
switch (clockStyle) {
|
||||
case 'flip':
|
||||
return <FlipClock time={timeString} size={clockSize} dateInfo={dateInfo} period={period} />
|
||||
return <FlipClock time={timeString} size={120} />
|
||||
case 'roll':
|
||||
return <RollClock time={timeString} size={clockSize} dateInfo={dateInfo} period={period} />
|
||||
return <RollClock time={timeString} size={120} />
|
||||
case 'digital':
|
||||
default:
|
||||
return (
|
||||
<DigitalClock
|
||||
time={timeString}
|
||||
size={clockSize}
|
||||
period={period}
|
||||
is24Hour={is24Hour}
|
||||
onToggleFormat={() => setIs24Hour(!is24Hour)}
|
||||
dateInfo={dateInfo}
|
||||
/>
|
||||
)
|
||||
return <DigitalClock time={timeString} />
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<div className="clock-container">
|
||||
{renderClock()}
|
||||
</div>
|
||||
|
||||
<div className="style-selector">
|
||||
<button
|
||||
className={`style-button ${clockStyle === 'digital' ? 'active' : ''}`}
|
||||
@ -162,12 +64,6 @@ function App() {
|
||||
滚动
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="clock-wrapper">
|
||||
<div className="clock-container">
|
||||
{renderClock()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,72 +1,3 @@
|
||||
.flip-clock-outer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.flip-clock-side-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.flip-clock-period {
|
||||
font-family: Arial, sans-serif;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.flip-format-toggle {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.flip-format-toggle:hover {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.flip-clock-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.flip-clock-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.flip-clock-header-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.flip-clock-period {
|
||||
font-family: Arial, sans-serif;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.flip-clock-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -74,15 +5,6 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.flip-clock-date {
|
||||
font-family: Arial, sans-serif;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.flip-clock-digit-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@ -1,97 +1,52 @@
|
||||
import { useState } from 'react';
|
||||
import { FlipDigit } from './FlipDigit';
|
||||
import './FlipClock.css';
|
||||
|
||||
interface DateInfo {
|
||||
dateString: string;
|
||||
weekday: string;
|
||||
}
|
||||
|
||||
interface FlipClockProps {
|
||||
time: string; // "HH:MM:SS" format
|
||||
size: number;
|
||||
dateInfo: DateInfo;
|
||||
period?: string;
|
||||
}
|
||||
|
||||
export function FlipClock({ time, size, dateInfo, period }: FlipClockProps) {
|
||||
const [is24Hour, setIs24Hour] = useState(true);
|
||||
|
||||
// Parse time - it's already in 12h format when period is provided
|
||||
const [hoursStr, minutes, seconds] = time.split(':');
|
||||
const displayHours = hoursStr;
|
||||
|
||||
const periodSize = size * 0.20;
|
||||
const dateSize = size * 0.20;
|
||||
export function FlipClock({ time, size }: FlipClockProps) {
|
||||
const [hours, minutes, seconds] = time.split(':');
|
||||
|
||||
return (
|
||||
<div className="flip-clock-outer">
|
||||
<div className="flip-clock-side-panel">
|
||||
<button
|
||||
className="flip-format-toggle"
|
||||
onClick={() => setIs24Hour(!is24Hour)}
|
||||
>
|
||||
{is24Hour ? '24H' : '12H'}
|
||||
</button>
|
||||
<div className="flip-clock-container">
|
||||
{/* Hours */}
|
||||
<div className="flip-clock-digit-group">
|
||||
<FlipDigit value={parseInt(hours[0], 10)} size={size} />
|
||||
<FlipDigit value={parseInt(hours[1], 10)} size={size} />
|
||||
</div>
|
||||
<div className="flip-clock-main">
|
||||
<div className="flip-clock-header">
|
||||
<div className="flip-clock-header-left">
|
||||
{period && (
|
||||
<span
|
||||
className="flip-clock-period"
|
||||
style={{ fontSize: `${periodSize}px` }}
|
||||
>
|
||||
{period}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className="flip-clock-date"
|
||||
style={{ fontSize: `${dateSize}px` }}
|
||||
>
|
||||
{dateInfo.dateString} {dateInfo.weekday}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flip-clock-container">
|
||||
{/* Hours */}
|
||||
<div className="flip-clock-digit-group">
|
||||
<FlipDigit value={parseInt(displayHours[0], 10)} size={size} />
|
||||
<FlipDigit value={parseInt(displayHours[1], 10)} size={size} />
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="flip-clock-separator"
|
||||
style={{
|
||||
fontSize: `${size * 0.5}px`,
|
||||
marginTop: `${-size * 0.08}px`
|
||||
}}
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
className="flip-clock-separator"
|
||||
style={{
|
||||
fontSize: `${size * 0.5}px`,
|
||||
marginTop: `${-size * 0.08}px`
|
||||
}}
|
||||
>
|
||||
:
|
||||
</span>
|
||||
|
||||
{/* Minutes */}
|
||||
<div className="flip-clock-digit-group">
|
||||
<FlipDigit value={parseInt(minutes[0], 10)} size={size} />
|
||||
<FlipDigit value={parseInt(minutes[1], 10)} size={size} />
|
||||
</div>
|
||||
{/* Minutes */}
|
||||
<div className="flip-clock-digit-group">
|
||||
<FlipDigit value={parseInt(minutes[0], 10)} size={size} />
|
||||
<FlipDigit value={parseInt(minutes[1], 10)} size={size} />
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="flip-clock-separator"
|
||||
style={{
|
||||
fontSize: `${size * 0.5}px`,
|
||||
marginTop: `${-size * 0.08}px`
|
||||
}}
|
||||
>
|
||||
:
|
||||
</span>
|
||||
<span
|
||||
className="flip-clock-separator"
|
||||
style={{
|
||||
fontSize: `${size * 0.5}px`,
|
||||
marginTop: `${-size * 0.08}px`
|
||||
}}
|
||||
>
|
||||
:
|
||||
</span>
|
||||
|
||||
{/* Seconds */}
|
||||
<div className="flip-clock-digit-group">
|
||||
<FlipDigit value={parseInt(seconds[0], 10)} size={size} />
|
||||
<FlipDigit value={parseInt(seconds[1], 10)} size={size} />
|
||||
</div>
|
||||
</div>
|
||||
{/* Seconds */}
|
||||
<div className="flip-clock-digit-group">
|
||||
<FlipDigit value={parseInt(seconds[0], 10)} size={size} />
|
||||
<FlipDigit value={parseInt(seconds[1], 10)} size={size} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -3,7 +3,6 @@
|
||||
border-radius: 4px;
|
||||
background-color: #1a1a1a;
|
||||
overflow: hidden;
|
||||
perspective: 1000px;
|
||||
}
|
||||
|
||||
.flip-digit-bg-top {
|
||||
@ -70,8 +69,7 @@
|
||||
border-top-left-radius: 4px;
|
||||
border-top-right-radius: 4px;
|
||||
backface-visibility: hidden;
|
||||
z-index: 11;
|
||||
/* 旋转轴在底部边缘(卡片中线位置) */
|
||||
z-index: 10;
|
||||
transform-origin: center bottom;
|
||||
}
|
||||
|
||||
@ -89,7 +87,6 @@
|
||||
border-bottom-right-radius: 4px;
|
||||
backface-visibility: hidden;
|
||||
z-index: 10;
|
||||
/* 旋转轴在顶部边缘(卡片中线位置) */
|
||||
transform-origin: center top;
|
||||
}
|
||||
|
||||
|
||||
@ -33,28 +33,23 @@ export function FlipDigit({ value, size }: FlipDigitProps) {
|
||||
|
||||
// Finish animation
|
||||
const finishAnimation = () => {
|
||||
setTopRotation(0);
|
||||
setBottomRotation(-90);
|
||||
setFlipBottomValue(null);
|
||||
setIsAnimating(false);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
setTopRotation(0);
|
||||
setBottomRotation(-90);
|
||||
setFlipBottomValue(null);
|
||||
animating.current = false;
|
||||
});
|
||||
animating.current = false;
|
||||
};
|
||||
|
||||
// Start bottom animation after top completes
|
||||
const startBottomAnimation = (targetValue: number) => {
|
||||
// 上半片翻转完成后,更新上半翻转片的值为新值
|
||||
// 这样当 finishAnimation 重置 rotation 时,上半翻转片显示正确的值
|
||||
setFlipTopValue(targetValue);
|
||||
|
||||
// 第二阶段:下半片从-90°向下翻转90°到0°
|
||||
// Animate bottom flap from -90deg to 0deg
|
||||
requestAnimationFrame(() => {
|
||||
setBottomRotation(0);
|
||||
});
|
||||
|
||||
// 下半片翻转完成后,背景下半更新为新值
|
||||
// After bottom animation completes, update bgBottom and finish
|
||||
setTimeout(() => {
|
||||
setBgBottomValue(targetValue);
|
||||
setTimeout(() => {
|
||||
@ -67,6 +62,7 @@ export function FlipDigit({ value, size }: FlipDigitProps) {
|
||||
useEffect(() => {
|
||||
if (value !== bgTopValue && !animating.current) {
|
||||
animating.current = true;
|
||||
setIsAnimating(true);
|
||||
|
||||
// Save current value as flip top value
|
||||
setFlipTopValue(bgTopValue);
|
||||
@ -75,23 +71,13 @@ export function FlipDigit({ value, size }: FlipDigitProps) {
|
||||
// Update bgTop immediately (revealed when top flips away)
|
||||
setBgTopValue(value);
|
||||
|
||||
// First, ensure transition is disabled before resetting rotation
|
||||
setIsAnimating(false);
|
||||
|
||||
// Reset rotation states instantly (without transition)
|
||||
// Reset rotation states
|
||||
setTopRotation(0);
|
||||
setBottomRotation(-90);
|
||||
|
||||
// Wait for browser to apply the reset (without transition)
|
||||
// Start top animation after a brief delay to ensure state is set
|
||||
requestAnimationFrame(() => {
|
||||
// Now enable transition for the actual animation
|
||||
setIsAnimating(true);
|
||||
|
||||
// Wait one more frame to ensure transition is enabled
|
||||
requestAnimationFrame(() => {
|
||||
// Start top animation: flip from 0° to 90°
|
||||
setTopRotation(90);
|
||||
});
|
||||
setTopRotation(90);
|
||||
});
|
||||
|
||||
// After top animation completes, start bottom animation
|
||||
@ -123,12 +109,14 @@ export function FlipDigit({ value, size }: FlipDigitProps) {
|
||||
|
||||
const topFlipStyle = {
|
||||
height: `${halfHeight}px`,
|
||||
transform: `rotateX(${topRotation}deg)`,
|
||||
transform: `perspective(1000px) translateY(${halfHeight * 0.5}px) rotateX(${topRotation}deg) translateY(${-halfHeight * 0.5}px)`,
|
||||
opacity: topRotation > 60 ? 0 : 1,
|
||||
};
|
||||
|
||||
const bottomFlipStyle = {
|
||||
height: `${halfHeight}px`,
|
||||
transform: `rotateX(${bottomRotation}deg)`,
|
||||
transform: `perspective(1000px) translateY(${-halfHeight * 0.5}px) rotateX(${bottomRotation}deg) translateY(${halfHeight * 0.5}px)`,
|
||||
opacity: bottomRotation > -60 ? 1 : 0,
|
||||
};
|
||||
|
||||
const centerLineStyle = {
|
||||
@ -157,7 +145,7 @@ export function FlipDigit({ value, size }: FlipDigitProps) {
|
||||
style={{
|
||||
...halfHeightStyle,
|
||||
...topFlipStyle,
|
||||
transition: isAnimating ? 'transform 300ms ease-in' : 'none',
|
||||
transition: isAnimating ? 'transform 300ms ease-in, opacity 300ms ease-in' : 'none',
|
||||
}}
|
||||
>
|
||||
<span className="flip-digit-text flip-digit-top-text" style={topTextStyle}>
|
||||
@ -172,7 +160,7 @@ export function FlipDigit({ value, size }: FlipDigitProps) {
|
||||
style={{
|
||||
...halfHeightStyle,
|
||||
...bottomFlipStyle,
|
||||
transition: isAnimating ? 'transform 300ms ease-out' : 'none',
|
||||
transition: isAnimating ? 'transform 300ms ease-out, opacity 300ms ease-out' : 'none',
|
||||
}}
|
||||
>
|
||||
<span className="flip-digit-text flip-digit-bottom-text" style={bottomTextStyle}>
|
||||
|
||||
@ -1,78 +1,9 @@
|
||||
.roll-clock-outer {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.roll-clock-side-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.roll-clock-period {
|
||||
font-family: Arial, sans-serif;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.roll-format-toggle {
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #888;
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.roll-format-toggle:hover {
|
||||
color: #fff;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.roll-clock-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.roll-clock-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.roll-clock-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.roll-clock-header-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.roll-clock-period {
|
||||
font-family: Arial, sans-serif;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.roll-clock-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
@ -80,15 +11,6 @@
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.roll-clock-date {
|
||||
font-family: Arial, sans-serif;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.roll-clock-digit-group {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
@ -2,30 +2,17 @@ import { useState } from 'react';
|
||||
import { RollDigit } from './RollDigit';
|
||||
import './RollClock.css';
|
||||
|
||||
interface DateInfo {
|
||||
dateString: string;
|
||||
weekday: string;
|
||||
}
|
||||
|
||||
interface RollClockProps {
|
||||
time: string; // "HH:MM:SS" format
|
||||
size: number;
|
||||
dateInfo: DateInfo;
|
||||
period?: string;
|
||||
}
|
||||
|
||||
export function RollClock({ time, size, dateInfo, period }: RollClockProps) {
|
||||
export function RollClock({ time, size }: RollClockProps) {
|
||||
// Roll direction: 'down' = roll down, 'up' = roll up
|
||||
const [direction, setDirection] = useState<'down' | 'up'>('down');
|
||||
const [is24Hour, setIs24Hour] = useState(true);
|
||||
|
||||
// Parse time - it's already in 12h format when period is provided
|
||||
const [hoursStr, minutes, seconds] = time.split(':');
|
||||
const displayHours = hoursStr;
|
||||
|
||||
const [hours, minutes, seconds] = time.split(':');
|
||||
const buttonSize = size * 0.25;
|
||||
const periodSize = size * 0.20;
|
||||
const dateSize = size * 0.20;
|
||||
|
||||
// Toggle direction
|
||||
const toggleDirection = () => {
|
||||
@ -33,95 +20,65 @@ export function RollClock({ time, size, dateInfo, period }: RollClockProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="roll-clock-outer">
|
||||
<div className="roll-clock-side-panel">
|
||||
<button
|
||||
className="roll-format-toggle"
|
||||
onClick={() => setIs24Hour(!is24Hour)}
|
||||
>
|
||||
{is24Hour ? '24H' : '12H'}
|
||||
</button>
|
||||
</div>
|
||||
<div className="roll-clock-wrapper">
|
||||
<div className="roll-clock-main">
|
||||
<div className="roll-clock-header">
|
||||
<div className="roll-clock-header-left">
|
||||
{!is24Hour && period && (
|
||||
<span
|
||||
className="roll-clock-period"
|
||||
style={{ fontSize: `${periodSize}px` }}
|
||||
>
|
||||
{period}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span
|
||||
className="roll-clock-date"
|
||||
style={{ fontSize: `${dateSize}px` }}
|
||||
>
|
||||
{dateInfo.dateString} {dateInfo.weekday}
|
||||
</span>
|
||||
</div>
|
||||
<div className="roll-clock-container">
|
||||
{/* Hours */}
|
||||
<div className="roll-clock-digit-group">
|
||||
<RollDigit value={parseInt(displayHours[0], 10)} size={size} direction={direction} />
|
||||
<RollDigit value={parseInt(displayHours[1], 10)} size={size} direction={direction} />
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="roll-clock-separator"
|
||||
style={{
|
||||
fontSize: `${size * 0.5}px`,
|
||||
marginTop: `${-size * 0.08}px`
|
||||
}}
|
||||
>
|
||||
:
|
||||
</span>
|
||||
|
||||
{/* Minutes */}
|
||||
<div className="roll-clock-digit-group">
|
||||
<RollDigit value={parseInt(minutes[0], 10)} size={size} direction={direction} />
|
||||
<RollDigit value={parseInt(minutes[1], 10)} size={size} direction={direction} />
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="roll-clock-separator"
|
||||
style={{
|
||||
fontSize: `${size * 0.5}px`,
|
||||
marginTop: `${-size * 0.08}px`
|
||||
}}
|
||||
>
|
||||
:
|
||||
</span>
|
||||
|
||||
{/* Seconds */}
|
||||
<div className="roll-clock-digit-group">
|
||||
<RollDigit value={parseInt(seconds[0], 10)} size={size} direction={direction} />
|
||||
<RollDigit value={parseInt(seconds[1], 10)} size={size} direction={direction} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="roll-clock-wrapper">
|
||||
<div className="roll-clock-container">
|
||||
{/* Hours */}
|
||||
<div className="roll-clock-digit-group">
|
||||
<RollDigit value={parseInt(hours[0], 10)} size={size} direction={direction} />
|
||||
<RollDigit value={parseInt(hours[1], 10)} size={size} direction={direction} />
|
||||
</div>
|
||||
|
||||
{/* Direction toggle button */}
|
||||
<button
|
||||
className="roll-clock-button"
|
||||
<span
|
||||
className="roll-clock-separator"
|
||||
style={{
|
||||
width: `${buttonSize}px`,
|
||||
height: `${buttonSize}px`,
|
||||
borderRadius: `${buttonSize / 2}px`,
|
||||
marginLeft: '8px',
|
||||
fontSize: `${size * 0.5}px`,
|
||||
marginTop: `${-size * 0.08}px`
|
||||
}}
|
||||
onClick={toggleDirection}
|
||||
>
|
||||
<span
|
||||
className="roll-clock-button-text"
|
||||
style={{ fontSize: `${buttonSize * 0.6}px` }}
|
||||
>
|
||||
{direction === 'down' ? '↓' : '↑'}
|
||||
</span>
|
||||
</button>
|
||||
:
|
||||
</span>
|
||||
|
||||
{/* Minutes */}
|
||||
<div className="roll-clock-digit-group">
|
||||
<RollDigit value={parseInt(minutes[0], 10)} size={size} direction={direction} />
|
||||
<RollDigit value={parseInt(minutes[1], 10)} size={size} direction={direction} />
|
||||
</div>
|
||||
|
||||
<span
|
||||
className="roll-clock-separator"
|
||||
style={{
|
||||
fontSize: `${size * 0.5}px`,
|
||||
marginTop: `${-size * 0.08}px`
|
||||
}}
|
||||
>
|
||||
:
|
||||
</span>
|
||||
|
||||
{/* Seconds */}
|
||||
<div className="roll-clock-digit-group">
|
||||
<RollDigit value={parseInt(seconds[0], 10)} size={size} direction={direction} />
|
||||
<RollDigit value={parseInt(seconds[1], 10)} size={size} direction={direction} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Direction toggle button */}
|
||||
<button
|
||||
className="roll-clock-button"
|
||||
style={{
|
||||
width: `${buttonSize}px`,
|
||||
height: `${buttonSize}px`,
|
||||
borderRadius: `${buttonSize / 2}px`,
|
||||
marginLeft: '8px',
|
||||
}}
|
||||
onClick={toggleDirection}
|
||||
>
|
||||
<span
|
||||
className="roll-clock-button-text"
|
||||
style={{ fontSize: `${buttonSize * 0.6}px` }}
|
||||
>
|
||||
{direction === 'down' ? '↓' : '↑'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -51,9 +51,11 @@
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
width: 1126px;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
border-inline: 1px solid var(--border);
|
||||
min-height: 100svh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user