| |
| |
| |
| |
| |
| import Box from "@mui/material/Box"; |
|
|
| export const COMIC_INK = "#101018"; |
| export const CREAM = "#faf8f2"; |
| export const COMIC_ORANGE = "#FF7A2F"; |
| export const COMIC_YELLOW = "#FFD23F"; |
| |
| |
| export const ACID_MAGENTA = "#FF2FA8"; |
| export const ACID_CYAN = "#2FF0E6"; |
| export const ANTON = "'Anton', 'Arial Narrow', Impact, sans-serif"; |
|
|
| |
| |
| |
| const SCHEMES = { |
| orange: { bg: COMIC_ORANGE, text: COMIC_INK, edge: COMIC_INK, shadow: COMIC_INK, shadowOnDark: ACID_MAGENTA }, |
| yellow: { bg: COMIC_YELLOW, text: COMIC_INK, edge: COMIC_INK, shadow: COMIC_INK, shadowOnDark: ACID_CYAN }, |
| ink: { bg: COMIC_INK, text: CREAM, edge: COMIC_INK, shadow: COMIC_ORANGE, shadowOnDark: COMIC_ORANGE }, |
| paper: { bg: CREAM, text: COMIC_INK, edge: COMIC_INK, shadow: COMIC_INK, shadowOnDark: COMIC_ORANGE }, |
| }; |
|
|
| |
| |
| const METRICS = { |
| xs: { fontSize: "0.78rem", padding: "7px 14px", border: 2, shadow: 4, hoverShadow: 6 }, |
| small: { fontSize: "0.92rem", padding: "9px 20px", border: 3, shadow: 5, hoverShadow: 7 }, |
| medium: { fontSize: "1.15rem", padding: "13px 30px", border: 4, shadow: 7, hoverShadow: 10 }, |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function ComicButton({ |
| children, |
| href, |
| target, |
| rel, |
| onClick, |
| variant = "filled", |
| scheme = "orange", |
| size = "small", |
| onDark = false, |
| sx, |
| ...rest |
| }) { |
| const colors = SCHEMES[scheme]; |
| const metrics = METRICS[size]; |
| const plate = onDark ? colors.shadowOnDark : colors.shadow; |
| const isOutline = variant === "outline"; |
|
|
| const restShadow = `${metrics.shadow}px ${metrics.shadow}px 0 ${plate}`; |
| const hoverShadow = `${metrics.hoverShadow}px ${metrics.hoverShadow}px 0 ${plate}`; |
| const pressedShadow = `0px 0px 0 ${plate}`; |
|
|
| const rootSx = { |
| position: "relative", |
| display: "inline-flex", |
| alignItems: "center", |
| justifyContent: "center", |
| gap: "0.5em", |
| boxSizing: "border-box", |
| fontFamily: ANTON, |
| fontWeight: 400, |
| fontSize: metrics.fontSize, |
| lineHeight: 1.15, |
| whiteSpace: "nowrap", |
| letterSpacing: "0.07em", |
| textTransform: "uppercase", |
| textDecoration: "none", |
| textAlign: "center", |
| padding: metrics.padding, |
| border: `${metrics.border}px solid`, |
| borderRadius: 0, |
| cursor: "pointer", |
| WebkitTapHighlightColor: "transparent", |
| userSelect: "none", |
| transition: |
| "transform 0.12s ease, box-shadow 0.12s ease, background-color 0.12s ease, color 0.12s ease", |
| ...(isOutline |
| ? { |
| background: "transparent", |
| color: colors.bg, |
| borderColor: colors.bg, |
| boxShadow: "none", |
| "&:hover": { |
| background: colors.bg, |
| color: colors.text, |
| borderColor: colors.edge, |
| transform: "translate(-2px, -2px)", |
| boxShadow: restShadow, |
| }, |
| "&:active": { |
| background: colors.bg, |
| color: colors.text, |
| borderColor: colors.edge, |
| transform: "translate(2px, 2px)", |
| boxShadow: pressedShadow, |
| }, |
| } |
| : { |
| background: colors.bg, |
| color: colors.text, |
| borderColor: colors.edge, |
| boxShadow: restShadow, |
| "&:hover": { |
| transform: "translate(-2px, -2px)", |
| boxShadow: hoverShadow, |
| }, |
| "&:active": { |
| transform: `translate(${metrics.shadow}px, ${metrics.shadow}px)`, |
| boxShadow: pressedShadow, |
| }, |
| }), |
| "&:focus-visible": { |
| outline: `3px dashed ${onDark ? CREAM : COMIC_INK}`, |
| outlineOffset: "5px", |
| }, |
| "@media (prefers-reduced-motion: reduce)": { transition: "none" }, |
| }; |
|
|
| const combinedSx = [rootSx, ...(Array.isArray(sx) ? sx : sx ? [sx] : [])]; |
|
|
| return href ? ( |
| <Box component="a" href={href} target={target} rel={rel} onClick={onClick} sx={combinedSx} {...rest}> |
| {children} |
| </Box> |
| ) : ( |
| <Box component="button" type="button" onClick={onClick} sx={combinedSx} {...rest}> |
| {children} |
| </Box> |
| ); |
| } |
|
|
| |
| const ROTATIONS = [-1.5, 0.75, -1]; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function ComicTitle({ component = "h2", tone = "light", accent = COMIC_ORANGE, fontSize = "2.4rem", lines, sx }) { |
| const list = lines.map((l) => (typeof l === "string" ? { text: l } : l)); |
| const lineSx = (line, i) => { |
| const rotate = ROTATIONS[i % ROTATIONS.length]; |
| const outline = line.variant === "outline"; |
| if (tone === "dark") { |
| return outline |
| ? { |
| color: "transparent", |
| WebkitTextStroke: `0.032em ${line.color ?? "#ffffff"}`, |
| textShadow: `-0.03em 0 0 ${ACID_MAGENTA}`, |
| transform: `rotate(${rotate}deg)`, |
| } |
| : { |
| color: line.color ?? accent, |
| textShadow: `0.06em 0.06em 0 ${COMIC_INK}, -0.035em 0 0 ${ACID_CYAN}, 0.035em 0 0 ${ACID_MAGENTA}`, |
| transform: `rotate(${rotate}deg)`, |
| }; |
| } |
| return outline |
| ? { |
| color: "transparent", |
| WebkitTextStroke: `0.03em ${line.color ?? COMIC_INK}`, |
| transform: `rotate(${rotate}deg)`, |
| } |
| : { |
| color: line.color ?? COMIC_INK, |
| textShadow: `0.05em 0.05em 0 ${accent}`, |
| transform: `rotate(${rotate}deg)`, |
| }; |
| }; |
| return ( |
| <Box |
| component={component} |
| sx={[ |
| { |
| m: 0, |
| fontFamily: ANTON, |
| fontWeight: 400, |
| lineHeight: 0.95, |
| letterSpacing: "normal", |
| textTransform: "uppercase", |
| fontSize, |
| }, |
| ...(Array.isArray(sx) ? sx : sx ? [sx] : []), |
| ]} |
| > |
| {list.map((line, i) => ( |
| <Box |
| key={i} |
| component="span" |
| className="comic-title-line" |
| sx={{ |
| display: "block", |
| ...(line.scale ? { fontSize: `${line.scale}em` } : {}), |
| ...lineSx(line, i), |
| }} |
| > |
| {line.text} |
| </Box> |
| ))} |
| </Box> |
| ); |
| } |
|
|
| |
| |
| const RAMP_DIRECTIONS = { |
| top: "to bottom", |
| bottom: "to top", |
| "top-left": "to bottom right", |
| "top-right": "to bottom left", |
| "bottom-left": "to top right", |
| "bottom-right": "to top left", |
| }; |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function HalftoneRamp({ |
| color = "rgba(16, 16, 24, 0.06)", |
| size = 14, |
| corner = "bottom", |
| reach = 55, |
| dots = [3.1, 2.55, 2.05, 1.6, 1.15, 0.7], |
| sx, |
| }) { |
| const towards = RAMP_DIRECTIONS[corner]; |
| const band = reach / dots.length; |
| return ( |
| <Box |
| aria-hidden |
| sx={[ |
| { position: "absolute", inset: 0, pointerEvents: "none", overflow: "hidden" }, |
| ...(Array.isArray(sx) ? sx : sx ? [sx] : []), |
| ]} |
| > |
| {dots.map((radius, i) => { |
| const centre = band * i; |
| const mask = |
| i === 0 |
| ? `linear-gradient(${towards}, #000 0%, transparent ${centre + band}%)` |
| : `linear-gradient(${towards}, transparent ${centre - band}%, #000 ${centre}%, transparent ${centre + band}%)`; |
| return ( |
| <Box |
| key={radius} |
| sx={{ |
| position: "absolute", |
| inset: 0, |
| backgroundImage: `radial-gradient(${color} ${radius}px, transparent ${radius + 0.7}px)`, |
| backgroundSize: `${size}px ${size}px`, |
| maskImage: mask, |
| WebkitMaskImage: mask, |
| }} |
| /> |
| ); |
| })} |
| </Box> |
| ); |
| } |
|
|