feat: add adaptive text color for publication date over images

- Install fast-average-color library for image color detection
- Create useAdaptiveTextColor hook to analyze top-right image corner
- Update ReaderHeader to dynamically adjust date text/shadow colors
- Ensures publication date is readable on both light and dark backgrounds
This commit is contained in:
Gigi
2025-10-15 15:40:57 +02:00
parent eaa590b8e2
commit 99d7705404
4 changed files with 115 additions and 2 deletions

10
package-lock.json generated
View File

@@ -22,6 +22,7 @@
"applesauce-react": "^4.0.0",
"applesauce-relay": "^4.0.0",
"date-fns": "^4.1.0",
"fast-average-color": "^9.5.0",
"nostr-tools": "^2.4.0",
"prismjs": "^1.30.0",
"react": "^18.2.0",
@@ -6086,6 +6087,15 @@
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"license": "MIT"
},
"node_modules/fast-average-color": {
"version": "9.5.0",
"resolved": "https://registry.npmjs.org/fast-average-color/-/fast-average-color-9.5.0.tgz",
"integrity": "sha512-nC6x2YIlJ9xxgkMFMd1BNoM1ctMjNoRKfRliPmiEWW3S6rLTHiQcy9g3pt/xiKv/D0NAAkhb9VyV+WJFvTqMGg==",
"license": "MIT",
"engines": {
"node": ">= 12"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",

View File

@@ -25,6 +25,7 @@
"applesauce-react": "^4.0.0",
"applesauce-relay": "^4.0.0",
"date-fns": "^4.1.0",
"fast-average-color": "^9.5.0",
"nostr-tools": "^2.4.0",
"prismjs": "^1.30.0",
"react": "^18.2.0",

View File

@@ -3,6 +3,7 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import { faHighlighter, faClock, faNewspaper } from '@fortawesome/free-solid-svg-icons'
import { format } from 'date-fns'
import { useImageCache } from '../hooks/useImageCache'
import { useAdaptiveTextColor } from '../hooks/useAdaptiveTextColor'
import { UserSettings } from '../services/settingsService'
import { Highlight, HighlightLevel } from '../types/highlights'
import { HighlightVisibility } from './HighlightsPanel'
@@ -34,6 +35,7 @@ const ReaderHeader: React.FC<ReaderHeaderProps> = ({
highlightVisibility = { nostrverse: true, friends: true, mine: true }
}) => {
const cachedImage = useImageCache(image)
const { textColor, shadowColor } = useAdaptiveTextColor(cachedImage)
const formattedDate = published ? format(new Date(published * 1000), 'MMM d, yyyy') : null
const isLongSummary = summary && summary.length > 150
@@ -83,7 +85,13 @@ const ReaderHeader: React.FC<ReaderHeaderProps> = ({
</div>
)}
{formattedDate && (
<div className="publish-date-topright">
<div
className="publish-date-topright"
style={{
color: textColor,
textShadow: `0 2px 4px ${shadowColor}`
}}
>
{formattedDate}
</div>
)}
@@ -125,7 +133,13 @@ const ReaderHeader: React.FC<ReaderHeaderProps> = ({
{title && (
<div className="reader-header">
{formattedDate && (
<div className="publish-date-topright">
<div
className="publish-date-topright"
style={{
color: textColor,
textShadow: `0 2px 4px ${shadowColor}`
}}
>
{formattedDate}
</div>
)}

View File

@@ -0,0 +1,88 @@
import { useEffect, useState } from 'react'
import FastAverageColor from 'fast-average-color'
interface AdaptiveTextColor {
textColor: string
shadowColor: string
}
/**
* Hook to determine optimal text and shadow colors based on image background
* Samples the top-right corner of the image to ensure publication date is readable
*
* @param imageUrl - The URL of the image to analyze
* @returns Object containing textColor and shadowColor for optimal contrast
*/
export function useAdaptiveTextColor(imageUrl: string | undefined): AdaptiveTextColor {
const [colors, setColors] = useState<AdaptiveTextColor>({
textColor: '#ffffff',
shadowColor: 'rgba(0, 0, 0, 0.5)'
})
useEffect(() => {
if (!imageUrl) {
// No image, use default white text
setColors({
textColor: '#ffffff',
shadowColor: 'rgba(0, 0, 0, 0.5)'
})
return
}
const fac = new FastAverageColor()
const img = new Image()
img.crossOrigin = 'anonymous'
img.onload = async () => {
try {
const width = img.naturalWidth
const height = img.naturalHeight
// Sample top-right corner (last 25% width, first 25% height)
const color = await fac.getColor(img, {
left: Math.floor(width * 0.75),
top: 0,
width: Math.floor(width * 0.25),
height: Math.floor(height * 0.25)
})
// Use library's built-in isLight check for optimal contrast
if (color.isLight) {
setColors({
textColor: '#000000',
shadowColor: 'rgba(255, 255, 255, 0.5)'
})
} else {
setColors({
textColor: '#ffffff',
shadowColor: 'rgba(0, 0, 0, 0.5)'
})
}
} catch (error) {
// Fallback to default on error
console.error('Error analyzing image color:', error)
setColors({
textColor: '#ffffff',
shadowColor: 'rgba(0, 0, 0, 0.5)'
})
}
}
img.onerror = () => {
// Fallback to default if image fails to load
setColors({
textColor: '#ffffff',
shadowColor: 'rgba(0, 0, 0, 0.5)'
})
}
img.src = imageUrl
return () => {
fac.destroy()
}
}, [imageUrl])
return colors
}