Files
SkinbaseNova/resources/js/components/artwork/ArtworkFormatBadges.jsx
2026-04-18 17:02:56 +02:00

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>
)
}