diff --git a/examples/simple/src/App.tsx b/examples/simple/src/App.tsx index 0c6b61ad..3302bf6f 100644 --- a/examples/simple/src/App.tsx +++ b/examples/simple/src/App.tsx @@ -24,13 +24,18 @@ import './App.css' const Clock = () => { return ( - + {(date: Date) => ( <> - + )} diff --git a/examples/storybook/playwright.config.ts b/examples/storybook/playwright.config.ts index e49e1f05..9fa17043 100644 --- a/examples/storybook/playwright.config.ts +++ b/examples/storybook/playwright.config.ts @@ -29,10 +29,10 @@ export default defineConfig({ // maxDiffPixels: 1, // }, - // toMatchSnapshot: { - // // An acceptable ratio of pixels that are different to the total amount of pixels, between 0 and 1. - // maxDiffPixelRatio: 0.0125, - // }, + toMatchSnapshot: { + // An acceptable ratio of pixels that are different to the total amount of pixels, between 0 and 1. + maxDiffPixelRatio: 0.001, + }, }, use: { diff --git a/examples/storybook/tests/example.spec.ts-snapshots/clock-image-chromium-darwin.png b/examples/storybook/tests/example.spec.ts-snapshots/clock-image-chromium-darwin.png index d1615d16..1e446db8 100644 Binary files a/examples/storybook/tests/example.spec.ts-snapshots/clock-image-chromium-darwin.png and b/examples/storybook/tests/example.spec.ts-snapshots/clock-image-chromium-darwin.png differ diff --git a/packages/clock/src/__snapshots__/test.tsx.snap b/packages/clock/src/__snapshots__/test.tsx.snap index e2b7de16..e74d210f 100644 --- a/packages/clock/src/__snapshots__/test.tsx.snap +++ b/packages/clock/src/__snapshots__/test.tsx.snap @@ -2,60 +2,774 @@ exports[`render AnalogClock 1`] = ` - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 12 + + + 1 + + + 2 + + + 3 + + + 4 + + + 5 + + + 6 + + + 7 + + + 8 + + + 9 + + + 10 + + + 11 + + + + + + `; exports[`render DigitalClock 1`] = ` -
+
09:00:00 AM (Asia/Tokyo)
diff --git a/packages/clock/src/analog.tsx b/packages/clock/src/analog.tsx index 2f547193..5bf25010 100644 --- a/packages/clock/src/analog.tsx +++ b/packages/clock/src/analog.tsx @@ -1,76 +1,380 @@ -import { DateTime } from 'luxon' import React from 'react' +import { computeFace, FaceType } from './faces' +import { calcHMS, StepStyle } from './step' import { ClockProps } from './types' -export const AnalogClock: React.FunctionComponent = ( - props -): JSX.Element => { +export interface AnalogClockStyle { + width: number + height: number + step: StepStyle + face: FaceType + bigHand: HandStyle + smallHand: HandStyle + secondHand: HandStyle + frame: FrameStyle + centerPoint: CenterPointStyle + hourLines: LinesStyle + minuteLines: LinesStyle +} + +export interface HandStyle { + width: number + length: number + color: string +} + +export interface FrameStyle { + size: number + width: number + color: string + backgroundColor: string +} + +export interface CenterPointStyle { + size: number + color: string +} + +export interface LinesStyle { + width: number + length: number + color: string +} + +export const defaultAnalogClockStyle: AnalogClockStyle = { + width: 100, + height: 100, + step: 'tick', + face: 'arabic', + bigHand: { + width: 3, + length: 30, + color: 'black', + }, + smallHand: { + width: 2, + length: 40, + color: 'black', + }, + secondHand: { + width: 1, + length: 45, + color: 'red', + }, + frame: { + size: 49, + width: 1, + color: 'black', + backgroundColor: 'white', + }, + centerPoint: { + size: 2, + color: 'black', + }, + hourLines: { + width: 1, + length: 4, + color: 'black', + }, + minuteLines: { + width: 1, + length: 2, + color: 'black', + }, +} + +interface AnalogClockCustomize { + step?: StepStyle + face?: FaceType + width?: number + height?: number + bigHand?: Partial + smallHand?: Partial + secondHand?: Partial + frame?: Partial + centerPoint?: Partial + hourLines?: Partial + minuteLines?: Partial +} + +type AnalogClockProps = ClockProps & AnalogClockCustomize + +export const AnalogClock: React.FC = (props): JSX.Element => { const { timezone, date } = props - const datetime = DateTime.fromJSDate(date) - const dt = datetime.setZone(timezone) - const hour = dt.hour - const minute = dt.minute - const second = dt.second - const bigHandColor = 'black' - const smallHandColor = 'black' - const secondHandColor = 'red' - const bigHandWidth = 2 - const smallHandWidth = 2 - const secondHandWidth = 2 - const bigHandLength = 30 - const secondHandLength = 40 - const minuteHandLength = 40 - const centerX = 50 - const centerY = 50 + const { + width, + height, + bigHand, + smallHand, + secondHand, + frame, + step, + face, + centerPoint, + hourLines, + minuteLines, + } = customizeClockProps(props) + const { hour, minute, second } = calcHMS(date, timezone, step) + const centerX = width / 2 + const centerY = height / 2 return ( - - - - - + + + + + + + + + + +
+ ) +} + +export const Hand: React.FC< + { + centerX: number + centerY: number + degree: number + } & HandStyle +> = (props) => { + const { centerX, centerY, degree, length, width, color } = props + const radDegree = degreeToRadian(degree) + const x = centerX + length * Math.sin(radDegree) + const y = centerY - length * Math.cos(radDegree) // clock coordinate + return ( + + ) +} + +const HourLines = (props: { + centerX: number + centerY: number + radius: number + length: number + width: number + color: string +}) => { + const { centerX, centerY, radius, width, color, length } = props + return ( + + ) +} + +const MinutesLines = (props: { + centerX: number + centerY: number + radius: number + length: number + width: number + color: string +}) => { + const { centerX, centerY, radius, width, color, length } = props + return ( + + ) +} + +const Lines = (props: { + centerX: number + centerY: number + radius: number + length: number + count: number + width: number + color: string +}) => { + const { centerX, centerY, radius, length, count, width, color } = props + const lines = [] + for (let i = 0; i < count; i++) { + const degree = (360 / count) * i + const radDegree = degreeToRadian(degree) + const x1 = centerX + radius * Math.sin(radDegree) + const y1 = centerY + radius * Math.cos(radDegree) + const x2 = x1 + length * Math.sin(radDegree) + const y2 = y1 + length * Math.cos(radDegree) + lines.push( - - ) + ) + } + return <>{lines} +} + +const Faces = (props: { + centerX: number + centerY: number + radius: number + faceType: FaceType +}) => { + const { centerX, centerY, radius } = props + const faces = [] + const textSize = radius / 5 + const radius2 = radius - textSize + const textStyle = { + textAnchor: 'middle', + dominantBaseline: 'central', + fontSize: textSize, + fontFamily: 'monospace', + fill: 'black', + } + for (let i = 0; i < 12; i++) { + const degree = (360 / 12) * i + const rad = degreeToRadian(degree) + const x = centerX + radius2 * Math.sin(rad) + const y = centerY - radius2 * Math.cos(rad) // clock coordinate + faces.push( + + {computeFace(i, props.faceType)} + + ) + } + return <>{faces} +} + +function customizeClockProps( + customize: AnalogClockCustomize +): AnalogClockStyle { + const { + width, + height, + step, + face, + bigHand, + smallHand, + secondHand, + frame, + centerPoint, + hourLines, + minuteLines, + } = { + ...defaultAnalogClockStyle, + ...customize, + } + return { + width, + height, + step, + face, + bigHand: { + ...defaultAnalogClockStyle.bigHand, + ...bigHand, + }, + smallHand: { + ...defaultAnalogClockStyle.smallHand, + ...smallHand, + }, + secondHand: { + ...defaultAnalogClockStyle.secondHand, + ...secondHand, + }, + frame: { + ...defaultAnalogClockStyle.frame, + ...frame, + }, + centerPoint: { + ...defaultAnalogClockStyle.centerPoint, + ...centerPoint, + }, + hourLines: { + ...defaultAnalogClockStyle.hourLines, + ...hourLines, + }, + minuteLines: { + ...defaultAnalogClockStyle.minuteLines, + ...minuteLines, + }, + } +} + +function degreeToRadian(degree: number): number { + return (degree * Math.PI) / 180 } diff --git a/packages/clock/src/digital.tsx b/packages/clock/src/digital.tsx index 32cffc61..31401cf8 100644 --- a/packages/clock/src/digital.tsx +++ b/packages/clock/src/digital.tsx @@ -15,7 +15,12 @@ export const DigitalClock: React.FunctionComponent = ( const secondStr = dt.second < 10 ? `0${dt.second}` : dt.second const ampm = dt.hour < 12 ? 'AM' : 'PM' return ( -
+
{hourStr}:{minuteStr}:{secondStr} {ampm} ({timezone})
) diff --git a/packages/clock/src/faces/arabic/index.ts b/packages/clock/src/faces/arabic/index.ts new file mode 100644 index 00000000..d72ad332 --- /dev/null +++ b/packages/clock/src/faces/arabic/index.ts @@ -0,0 +1,6 @@ +export function face(num: number): string { + if (num === 0) { + return '12' + } + return `${num}` +} diff --git a/packages/clock/src/faces/arabic/test.ts b/packages/clock/src/faces/arabic/test.ts new file mode 100644 index 00000000..10456d4c --- /dev/null +++ b/packages/clock/src/faces/arabic/test.ts @@ -0,0 +1,16 @@ +import { face } from '.' + +test('arabic/face returns face', () => { + expect(face(0)).toBe('12') // clock starts at 12 + expect(face(1)).toBe('1') + expect(face(2)).toBe('2') + expect(face(3)).toBe('3') + expect(face(4)).toBe('4') + expect(face(5)).toBe('5') + expect(face(6)).toBe('6') + expect(face(7)).toBe('7') + expect(face(8)).toBe('8') + expect(face(9)).toBe('9') + expect(face(10)).toBe('10') + expect(face(11)).toBe('11') +}) diff --git a/packages/clock/src/faces/index.ts b/packages/clock/src/faces/index.ts new file mode 100644 index 00000000..4d6dc123 --- /dev/null +++ b/packages/clock/src/faces/index.ts @@ -0,0 +1,12 @@ +import { face as arabicFace } from './arabic' +import { face as romanFace } from './roman' +export type FaceType = 'roman' | 'arabic' + +export function computeFace(num: number, faceType: FaceType): string { + switch (faceType) { + case 'arabic': + return arabicFace(num) + case 'roman': + return romanFace(num) + } +} diff --git a/packages/clock/src/faces/roman/index.ts b/packages/clock/src/faces/roman/index.ts new file mode 100644 index 00000000..6254f48a --- /dev/null +++ b/packages/clock/src/faces/roman/index.ts @@ -0,0 +1,29 @@ +export function face(num: number): string { + switch (num) { + case 0: + return 'Ⅻ' + case 1: + return 'Ⅰ' + case 2: + return 'Ⅱ' + case 3: + return 'Ⅲ' + case 4: + return 'Ⅳ' + case 5: + return `Ⅴ` + case 6: + return 'Ⅵ' + case 7: + return 'Ⅶ' + case 8: + return 'Ⅷ' + case 9: + return 'Ⅸ' + case 10: + return 'Ⅹ' + case 11: + return 'Ⅺ' + } + return `${num}` // unreachable +} diff --git a/packages/clock/src/faces/roman/test.ts b/packages/clock/src/faces/roman/test.ts new file mode 100644 index 00000000..c5846740 --- /dev/null +++ b/packages/clock/src/faces/roman/test.ts @@ -0,0 +1,17 @@ +import { face } from '.' + +test('roman/face returns face', () => { + expect(face(0)).toBe('Ⅻ') + expect(face(1)).toBe('Ⅰ') + expect(face(2)).toBe('Ⅱ') + expect(face(3)).toBe('Ⅲ') + expect(face(4)).toBe('Ⅳ') + expect(face(5)).toBe('Ⅴ') + expect(face(6)).toBe('Ⅵ') + expect(face(7)).toBe('Ⅶ') + expect(face(8)).toBe('Ⅷ') + expect(face(9)).toBe('Ⅸ') + expect(face(10)).toBe('Ⅹ') + expect(face(11)).toBe('Ⅺ') + expect(face(12)).toBe('12') // invalid +}) diff --git a/packages/clock/src/faces/test.ts b/packages/clock/src/faces/test.ts new file mode 100644 index 00000000..76756ef9 --- /dev/null +++ b/packages/clock/src/faces/test.ts @@ -0,0 +1,9 @@ +import { face as arabicFace } from './arabic' +import { face as romanFace } from './roman' + +import { computeFace } from '.' + +test('computeFace returns face', () => { + expect(computeFace(0, 'roman')).toBe(romanFace(0)) + expect(computeFace(0, 'arabic')).toBe(arabicFace(0)) +}) diff --git a/packages/clock/src/step/index.ts b/packages/clock/src/step/index.ts new file mode 100644 index 00000000..7b73656d --- /dev/null +++ b/packages/clock/src/step/index.ts @@ -0,0 +1,46 @@ +import { DateTime } from 'luxon' + +export type StepStyle = 'tick' | 'sweep' + +interface HMS { + hour: number + minute: number + second: number +} + +export function calcHMS( + date: Date, + timezone: string, + stepStyle: StepStyle +): HMS { + switch (stepStyle) { + case 'tick': + return tickHMS(date, timezone) + case 'sweep': + return sweepHMS(date, timezone) + } +} + +/** + * tick represents a clock hand that moves in discrete steps + */ +export function tickHMS(date: Date, timezone: string): HMS { + const datetime = DateTime.fromJSDate(date) + const dt = datetime.setZone(timezone) + const hour = dt.hour + const minute = dt.minute + const second = dt.second + return { hour, minute, second } +} + +/** + * sweep represents a clock hand that moves smoothly + */ +export function sweepHMS(date: Date, timezone: string): HMS { + const datetime = DateTime.fromJSDate(date) + const dt = datetime.setZone(timezone) + const hour = dt.hour + dt.minute / 60 + dt.second / 3600 + const minute = dt.minute + dt.second / 60 + const second = dt.second + dt.millisecond / 1000 + return { hour, minute, second } +} diff --git a/packages/clock/src/step/test.ts b/packages/clock/src/step/test.ts new file mode 100644 index 00000000..5c3b85ab --- /dev/null +++ b/packages/clock/src/step/test.ts @@ -0,0 +1,32 @@ +import { calcHMS, tickHMS, sweepHMS } from '.' + +test('tickHMS returns HMS', () => { + const dt = new Date('2021-01-01T00:00:00Z') + expect(tickHMS(dt, 'Asia/Tokyo')).toEqual({ + hour: 9, + minute: 0, + second: 0, + }) + + const dt2 = new Date('2021-01-01T10:08:14Z') + expect(tickHMS(dt2, 'Asia/Tokyo')).toEqual({ + hour: 19, + minute: 8, + second: 14, + }) +}) + +test('sweepHMS returns HMS', () => { + const dt2 = new Date('2021-01-01T10:08:14.2000Z') + expect(sweepHMS(dt2, 'Asia/Tokyo')).toEqual({ + hour: 19.13722222222222, + minute: 8.233333333333333, + second: 14.2, + }) +}) + +test('calcHMS returns HMS', () => { + const dt = new Date('2021-01-01T10:08:14.2000Z') + expect(calcHMS(dt, 'Asia/Tokyo', 'tick')).toEqual(tickHMS(dt, 'Asia/Tokyo')) + expect(calcHMS(dt, 'Asia/Tokyo', 'sweep')).toEqual(sweepHMS(dt, 'Asia/Tokyo')) +}) diff --git a/packages/stopwatch/src/__snapshots__/test.tsx.snap b/packages/stopwatch/src/__snapshots__/test.tsx.snap index f9cbdcfe..c8f50a32 100644 --- a/packages/stopwatch/src/__snapshots__/test.tsx.snap +++ b/packages/stopwatch/src/__snapshots__/test.tsx.snap @@ -3,7 +3,7 @@ exports[`render MinimalStopwatch 1`] = `
diff --git a/packages/stopwatch/src/minimal.tsx b/packages/stopwatch/src/minimal.tsx index 269487ee..c8ac2eb2 100644 --- a/packages/stopwatch/src/minimal.tsx +++ b/packages/stopwatch/src/minimal.tsx @@ -28,8 +28,8 @@ export const MinimalStopwatch: React.FunctionComponent = ( return (
diff --git a/packages/timer/src/__snapshots__/test.tsx.snap b/packages/timer/src/__snapshots__/test.tsx.snap index 3994b6a2..55706662 100644 --- a/packages/timer/src/__snapshots__/test.tsx.snap +++ b/packages/timer/src/__snapshots__/test.tsx.snap @@ -3,7 +3,7 @@ exports[`render MinimalTimer 1`] = `
diff --git a/packages/timer/src/minimal.tsx b/packages/timer/src/minimal.tsx index 58a2d6bd..ef1226a7 100644 --- a/packages/timer/src/minimal.tsx +++ b/packages/timer/src/minimal.tsx @@ -28,8 +28,8 @@ export const MinimalTimer: React.FunctionComponent = ( return (