267 lines
7.9 KiB
JavaScript
267 lines
7.9 KiB
JavaScript
import React from 'react'
|
|
|
|
const RESOLUTION_TIERS = [
|
|
{ label: '8K', width: 7680, height: 4320, tone: 'amber' },
|
|
{ label: '5K', width: 5120, height: 2880, tone: 'violet' },
|
|
{ label: '4K', width: 3840, height: 2160, tone: 'sky' },
|
|
{ label: 'QHD', width: 2560, height: 1440, tone: 'emerald' },
|
|
{ label: 'Full HD', width: 1920, height: 1080, tone: 'cyan' },
|
|
{ label: 'HD', width: 1280, height: 720, tone: 'slate' },
|
|
]
|
|
|
|
const ASPECT_RATIOS = [
|
|
{ label: '21:9', ratio: 21 / 9 },
|
|
{ label: '16:10', ratio: 16 / 10 },
|
|
{ label: '16:9', ratio: 16 / 9 },
|
|
{ label: '3:2', ratio: 3 / 2 },
|
|
{ label: '4:3', ratio: 4 / 3 },
|
|
{ label: '1:1', ratio: 1 },
|
|
{ label: '4:5', ratio: 4 / 5 },
|
|
{ label: '3:4', ratio: 3 / 4 },
|
|
{ label: '9:16', ratio: 9 / 16 },
|
|
]
|
|
|
|
const TONE_CLASSES = {
|
|
amber: 'border-amber-400/25 bg-amber-400/10 text-amber-100',
|
|
violet: 'border-violet-400/25 bg-violet-400/10 text-violet-100',
|
|
sky: 'border-sky-400/25 bg-sky-400/10 text-sky-100',
|
|
emerald: 'border-emerald-400/25 bg-emerald-400/10 text-emerald-100',
|
|
cyan: 'border-cyan-400/25 bg-cyan-400/10 text-cyan-100',
|
|
slate: 'border-white/10 bg-white/[0.04] text-white/80',
|
|
}
|
|
|
|
function ScreenIcon({ className }) {
|
|
return (
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.8} stroke="currentColor" className={className}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 17.25h6m-3 0v2.25m-7.5-15h15A1.5 1.5 0 0 1 21 6v9A1.5 1.5 0 0 1 19.5 16.5h-15A1.5 1.5 0 0 1 3 15V6A1.5 1.5 0 0 1 4.5 4.5Z" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
function RatioIcon({ className }) {
|
|
return (
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.8} stroke="currentColor" className={className}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 8.25V6A1.5 1.5 0 0 1 6 4.5h2.25m7.5 0H18A1.5 1.5 0 0 1 19.5 6v2.25m0 7.5V18A1.5 1.5 0 0 1 18 19.5h-2.25m-7.5 0H6A1.5 1.5 0 0 1 4.5 18v-2.25" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 12h7.5" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
function FormatIcon({ className, variant }) {
|
|
if (variant === 'ultrawide') {
|
|
return (
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.8} stroke="currentColor" className={className}>
|
|
<rect x="3.75" y="7.5" width="16.5" height="9" rx="2.25" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M7.5 12h9" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
if (variant === 'vertical') {
|
|
return (
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.8} stroke="currentColor" className={className}>
|
|
<rect x="7.25" y="3.75" width="9.5" height="16.5" rx="2.25" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 7.5v9" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.8} stroke="currentColor" className={className}>
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 18c2.5-5.5 5-8.25 7.5-8.25S17 12.5 19.5 18" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M7.5 6.75h9" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M9 4.5h6v4.5H9z" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
function OrientationIcon({ className, orientation }) {
|
|
if (orientation === 'square') {
|
|
return (
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.8} stroke="currentColor" className={className}>
|
|
<rect x="5.5" y="5.5" width="13" height="13" rx="2.25" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
if (orientation === 'portrait') {
|
|
return (
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.8} stroke="currentColor" className={className}>
|
|
<rect x="7.25" y="4.5" width="9.5" height="15" rx="2.25" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8.25v7.5" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.8} stroke="currentColor" className={className}>
|
|
<rect x="4.5" y="7.25" width="15" height="9.5" rx="2.25" />
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M8.25 12h7.5" />
|
|
</svg>
|
|
)
|
|
}
|
|
|
|
function Badge({ label, tone, icon }) {
|
|
return (
|
|
<span className={`inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] ${TONE_CLASSES[tone] || TONE_CLASSES.slate}`}>
|
|
<span className="text-current/90">{icon}</span>
|
|
<span>{label}</span>
|
|
</span>
|
|
)
|
|
}
|
|
|
|
function pickResolutionTier(width, height) {
|
|
const longSide = Math.max(width, height)
|
|
const shortSide = Math.min(width, height)
|
|
|
|
for (const tier of RESOLUTION_TIERS) {
|
|
if (longSide >= tier.width && shortSide >= tier.height) {
|
|
return tier
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
function pickOrientation(width, height) {
|
|
if (width === height) {
|
|
return {
|
|
key: 'orientation-square',
|
|
label: 'Square',
|
|
tone: 'amber',
|
|
icon: <OrientationIcon className="h-3.5 w-3.5" orientation="square" />,
|
|
isSquare: true,
|
|
}
|
|
}
|
|
|
|
if (width > height) {
|
|
return {
|
|
key: 'orientation-landscape',
|
|
label: 'Landscape',
|
|
tone: 'emerald',
|
|
icon: <OrientationIcon className="h-3.5 w-3.5" orientation="landscape" />,
|
|
isSquare: false,
|
|
}
|
|
}
|
|
|
|
return {
|
|
key: 'orientation-portrait',
|
|
label: 'Portrait',
|
|
tone: 'violet',
|
|
icon: <OrientationIcon className="h-3.5 w-3.5" orientation="portrait" />,
|
|
isSquare: false,
|
|
}
|
|
}
|
|
|
|
function pickAspectRatio(width, height) {
|
|
const ratio = width / height
|
|
let best = null
|
|
|
|
for (const candidate of ASPECT_RATIOS) {
|
|
const delta = Math.abs(ratio - candidate.ratio) / candidate.ratio
|
|
|
|
if (delta > 0.03) {
|
|
continue
|
|
}
|
|
|
|
if (best === null || delta < best.delta) {
|
|
best = { ...candidate, delta }
|
|
}
|
|
}
|
|
|
|
return best
|
|
}
|
|
|
|
function pickSemanticFormat(width, height, aspectRatio, orientation) {
|
|
if (!orientation || orientation.isSquare) {
|
|
return null
|
|
}
|
|
|
|
const ratio = width / height
|
|
|
|
if (ratio >= 2.1) {
|
|
return {
|
|
key: 'semantic-ultrawide',
|
|
label: 'Ultrawide',
|
|
tone: 'sky',
|
|
icon: <FormatIcon className="h-3.5 w-3.5" variant="ultrawide" />,
|
|
}
|
|
}
|
|
|
|
if (ratio <= 0.75) {
|
|
return {
|
|
key: 'semantic-vertical',
|
|
label: 'Vertical',
|
|
tone: 'violet',
|
|
icon: <FormatIcon className="h-3.5 w-3.5" variant="vertical" />,
|
|
}
|
|
}
|
|
|
|
if (aspectRatio && ['4:3', '3:2', '16:10'].includes(aspectRatio.label)) {
|
|
return {
|
|
key: 'semantic-classic',
|
|
label: 'Classic',
|
|
tone: 'amber',
|
|
icon: <FormatIcon className="h-3.5 w-3.5" variant="classic" />,
|
|
}
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
export function getArtworkFormatBadges(width, height) {
|
|
if (!(width > 0 && height > 0)) {
|
|
return []
|
|
}
|
|
|
|
const badges = []
|
|
const orientation = pickOrientation(width, height)
|
|
|
|
const resolutionTier = pickResolutionTier(width, height)
|
|
if (resolutionTier) {
|
|
badges.push({
|
|
key: `resolution-${resolutionTier.label}`,
|
|
label: resolutionTier.label,
|
|
tone: resolutionTier.tone,
|
|
icon: <ScreenIcon className="h-3.5 w-3.5" />,
|
|
})
|
|
}
|
|
|
|
if (orientation) {
|
|
badges.push(orientation)
|
|
}
|
|
|
|
const aspectRatio = pickAspectRatio(width, height)
|
|
const semanticFormat = pickSemanticFormat(width, height, aspectRatio, orientation)
|
|
|
|
if (semanticFormat) {
|
|
badges.push(semanticFormat)
|
|
}
|
|
|
|
if (aspectRatio && !orientation?.isSquare) {
|
|
badges.push({
|
|
key: `ratio-${aspectRatio.label}`,
|
|
label: aspectRatio.label,
|
|
tone: 'slate',
|
|
icon: <RatioIcon className="h-3.5 w-3.5" />,
|
|
})
|
|
}
|
|
|
|
return badges
|
|
}
|
|
|
|
export default function ArtworkFormatBadges({ width, height, className = '' }) {
|
|
const badges = getArtworkFormatBadges(width, height)
|
|
|
|
if (badges.length === 0) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<div className={`flex flex-wrap gap-2 ${className}`.trim()}>
|
|
{badges.map((badge) => (
|
|
<Badge key={badge.key} label={badge.label} tone={badge.tone} icon={badge.icon} />
|
|
))}
|
|
</div>
|
|
)
|
|
} |