Skip to content

Commit fbb4ecc

Browse files
committed
feat: accent color transition
1 parent 212eaea commit fbb4ecc

File tree

2 files changed

+125
-9
lines changed

2 files changed

+125
-9
lines changed

src/lib/color.ts

+45
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,48 @@ export function hexToHsl(hex: string) {
123123
// Return the HSL values as a string
124124
return [h, s, Math.round(l * 100)]
125125
}
126+
127+
export function generateTransitionColors(
128+
startColor: string,
129+
targetColor: string,
130+
step: number,
131+
): string[] {
132+
// Convert startColor and targetColor to RGB values
133+
const startRed = parseInt(startColor.substring(1, 3), 16)
134+
const startGreen = parseInt(startColor.substring(3, 5), 16)
135+
const startBlue = parseInt(startColor.substring(5, 7), 16)
136+
137+
const targetRed = parseInt(targetColor.substring(1, 3), 16)
138+
const targetGreen = parseInt(targetColor.substring(3, 5), 16)
139+
const targetBlue = parseInt(targetColor.substring(5, 7), 16)
140+
141+
// Calculate increments for each color channel
142+
const redIncrement = (targetRed - startRed) / step
143+
const greenIncrement = (targetGreen - startGreen) / step
144+
const blueIncrement = (targetBlue - startBlue) / step
145+
146+
const transitionColors: string[] = []
147+
148+
// Generate transition colors
149+
for (let i = 0; i < step; i++) {
150+
// Calculate transition color values
151+
const transitionRed = Math.round(startRed + redIncrement * i)
152+
const transitionGreen = Math.round(startGreen + greenIncrement * i)
153+
const transitionBlue = Math.round(startBlue + blueIncrement * i)
154+
155+
// Convert RGB values to hex format
156+
const hexColor = `#${(
157+
(1 << 24) |
158+
(transitionRed << 16) |
159+
(transitionGreen << 8) |
160+
transitionBlue
161+
)
162+
.toString(16)
163+
.slice(1)}`
164+
165+
// Add transition color to the result array
166+
transitionColors.push(hexColor)
167+
}
168+
169+
return Array.from(new Set(transitionColors))
170+
}

src/providers/root/accent-color-provider.tsx

+80-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
import { useEffect, useRef, useState } from 'react'
12
import { useServerInsertedHTML } from 'next/navigation'
23
import type { AccentColor } from '~/app/config'
34
import type { PropsWithChildren } from 'react'
45

5-
import { sample } from '~/lib/_'
6-
import { hexToHsl } from '~/lib/color'
6+
import { generateTransitionColors, hexToHsl } from '~/lib/color'
77
import { noopObj } from '~/lib/noop'
88

99
import { useAppConfigSelector } from './aggregation-data-provider'
@@ -30,20 +30,91 @@ const accentColorDark = [
3030
export const AccentColorProvider = ({ children }: PropsWithChildren) => {
3131
const { light, dark } =
3232
useAppConfigSelector((config) => config.color) || (noopObj as AccentColor)
33-
useServerInsertedHTML(() => {
34-
const accentColorL = sample(light ?? accentColorLight)
35-
const accentColorD = sample(dark ?? accentColorDark)
3633

37-
const lightHsl = hexToHsl(accentColorL)
38-
const darkHsl = hexToHsl(accentColorD)
34+
const Length = Math.max(light?.length ?? 0, dark?.length ?? 0)
35+
const randomSeedRef = useRef((Math.random() * Length) | 0)
36+
37+
const lightColors = light ?? accentColorLight
38+
const darkColors = dark ?? accentColorDark
39+
const currentAccentColorLRef = useRef(lightColors[randomSeedRef.current])
40+
const currentAccentColorDRef = useRef(darkColors[randomSeedRef.current])
41+
42+
const [u, update] = useState(0)
43+
useEffect(() => {
44+
const $style = document.createElement('style')
45+
46+
const $originColor = document.getElementById('accent-color-style')
47+
48+
const nextSeed = (randomSeedRef.current + 1) % Length
49+
const nextColorD = darkColors[nextSeed]
50+
const nextColorL = lightColors[nextSeed]
51+
const STEP = 60
52+
const INTERVAL = 100
53+
const colorsD = generateTransitionColors(
54+
currentAccentColorDRef.current,
55+
nextColorD,
56+
STEP,
57+
)
58+
const colorsL = generateTransitionColors(
59+
currentAccentColorLRef.current,
60+
nextColorL,
61+
STEP,
62+
)
63+
64+
const timer = setTimeout(function updateAccent() {
65+
const colorD = colorsD.shift()
66+
const colorL = colorsL.shift()
67+
if (colorD && colorL) {
68+
currentAccentColorDRef.current = colorD
69+
currentAccentColorLRef.current = colorL
70+
setTimeout(updateAccent, INTERVAL)
71+
} else {
72+
randomSeedRef.current = nextSeed
73+
currentAccentColorDRef.current = nextColorD
74+
currentAccentColorLRef.current = nextColorL
75+
update(u + 1)
76+
}
77+
78+
const lightHsl = hexToHsl(currentAccentColorLRef.current)
79+
const darkHsl = hexToHsl(currentAccentColorDRef.current)
80+
81+
const [hl, sl, ll] = lightHsl
82+
const [hd, sd, ld] = darkHsl
83+
84+
$style.innerHTML = `html[data-theme='light'] {
85+
--a: ${`${hl} ${sl}% ${ll}%`};
86+
--af: ${`${hl} ${sl}% ${ll + 6}%`};
87+
}
88+
html[data-theme='dark'] {
89+
--a: ${`${hd} ${sd}% ${ld}%`};
90+
--af: ${`${hd} ${sd}% ${ld - 6}%`};
91+
}
92+
`
93+
}, INTERVAL)
94+
document.head.appendChild($style)
95+
// FIXME should remove origin color, if not will not override origin color
96+
$originColor?.remove()
97+
return () => {
98+
clearTimeout(timer)
99+
100+
setTimeout(() => {
101+
document.head.removeChild($style)
102+
}, 1000)
103+
}
104+
}, [Length, darkColors, lightColors, u])
105+
106+
useServerInsertedHTML(() => {
107+
const lightHsl = hexToHsl(currentAccentColorLRef.current)
108+
const darkHsl = hexToHsl(currentAccentColorDRef.current)
39109

40110
const [hl, sl, ll] = lightHsl
41111
const [hd, sd, ld] = darkHsl
42112

43113
return (
44114
<style
45-
data-light={accentColorL}
46-
data-dark={accentColorD}
115+
id="accent-color-style"
116+
data-light={currentAccentColorLRef.current}
117+
data-dark={currentAccentColorDRef.current}
47118
dangerouslySetInnerHTML={{
48119
__html: `html[data-theme='light'] {
49120
--a: ${`${hl} ${sl}% ${ll}%`};

0 commit comments

Comments
 (0)