From a84cbfa4f0ecee98dd2418ff6e85edd306962caa Mon Sep 17 00:00:00 2001 From: openhands Date: Fri, 3 Jul 2026 16:55:59 +0200 Subject: [PATCH] feat: add all 191+ Habbo fonts from habbofont.net to logo generator --- .../media/logo-1783090011089-zdtspz.png | Bin 0 -> 1672 bytes .../media/logo-1783090023241-tuhvd8.png | Bin 0 -> 1219 bytes .../media/logo-1783090066849-0640kr.png | Bin 0 -> 1672 bytes src/components/public/habbo-fonts.ts | 269 +++++++++++++++ src/components/public/logo-generator.tsx | 320 +++++------------- 5 files changed, 363 insertions(+), 226 deletions(-) create mode 100644 public/assets/images/media/logo-1783090011089-zdtspz.png create mode 100644 public/assets/images/media/logo-1783090023241-tuhvd8.png create mode 100644 public/assets/images/media/logo-1783090066849-0640kr.png create mode 100644 src/components/public/habbo-fonts.ts diff --git a/public/assets/images/media/logo-1783090011089-zdtspz.png b/public/assets/images/media/logo-1783090011089-zdtspz.png new file mode 100644 index 0000000000000000000000000000000000000000..31eae3a7e086723863b5a41046e26f456069ca40 GIT binary patch literal 1672 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 - * and triggers a download via canvas.toBlob -> object URL -> . - * - * Pure browser code — no server modules (prisma/siteSettings) are imported. - */ - -type FontOption = { label: string; stack: string }; - -// ~8 fun, broadly-available stacks (web-safe + a Google-ish first pick). -const FONTS: FontOption[] = [ - { label: "Habbo pixel", stack: "var(--font-pixel), 'Courier New', monospace" }, - { label: "Poppins (rounded)", stack: "'Poppins', 'Trebuchet MS', sans-serif" }, - { label: "Comic Sans (playful)", stack: "'Comic Sans MS', 'Comic Sans', cursive" }, - { label: "Georgia (classic serif)", stack: "Georgia, 'Times New Roman', serif" }, - { label: "Impact (bold poster)", stack: "Impact, 'Haettenschweiler', sans-serif" }, - { label: "Trebuchet (clean)", stack: "'Trebuchet MS', Tahoma, sans-serif" }, - { label: "Monospace (retro)", stack: "'Courier New', monospace" }, -]; - -const MIN_SIZE = 16; -const MAX_SIZE = 160; +import { HABBO_FONT_GROUPS, buildFontUrl } from "@/components/public/habbo-fonts"; export default function LogoGenerator() { const [text, setText] = useState("Atom"); - const [fontStack, setFontStack] = useState(FONTS[0]!.stack); - const [textColor, setTextColor] = useState("#f59e0b"); - const [bgEnabled, setBgEnabled] = useState(false); - const [bgColor, setBgColor] = useState("#0f172a"); - const [size, setSize] = useState(72); - const [bold, setBold] = useState(true); - - const linkRef = useRef(null); + const [styleName, setStyleName] = useState("habbo"); + const [search, setSearch] = useState(""); const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "done" | "error">("idle"); const [, startTransition] = useTransition(); - const safeText = text.trim().length > 0 ? text : "Your hotel"; - const fontWeight = bold ? "bold" : "normal"; + const safeText = text.trim() || "Your hotel"; + const gifUrl = useMemo(() => buildFontUrl(styleName, safeText), [styleName, safeText]); - // Inline style for the live preview text, mirroring what the canvas will draw. - const previewTextStyle = useMemo( - () => ({ - fontFamily: fontStack, - fontSize: `${size}px`, - fontWeight, - color: textColor, - lineHeight: 1.1, - margin: 0, - padding: "0.1em 0.15em", - whiteSpace: "nowrap", - letterSpacing: "0.5px", - }), - [fontStack, size, fontWeight, textColor], + const currentFont = useMemo( + () => HABBO_FONT_GROUPS.flatMap((g) => g.fonts).find((f) => f.styleName === styleName), + [styleName], + ); + + const filteredGroups = useMemo( + () => + search + ? HABBO_FONT_GROUPS.map((g) => ({ + ...g, + fonts: g.fonts.filter( + (f) => + f.name.toLowerCase().includes(search.toLowerCase()) || + f.styleName.toLowerCase().includes(search.toLowerCase()), + ), + })).filter((g) => g.fonts.length > 0) + : HABBO_FONT_GROUPS, + [search], ); const handleDownload = useCallback(() => { - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - const fontSpec = `${fontWeight} ${size}px ${fontStack}`; - // Measure with the chosen font so the canvas fits the text snugly. - ctx.font = fontSpec; - const metrics = ctx.measureText(safeText); - const padX = Math.round(size * 0.4); - const padY = Math.round(size * 0.45); - const textWidth = Math.max(1, Math.ceil(metrics.width)); - const width = textWidth + padX * 2; - const height = Math.ceil(size * 1.35) + padY * 2; - - // Render crisply on high-DPI screens, then keep CSS-pixel coordinates below. - const dpr = Math.min(typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1, 3); - canvas.width = Math.round(width * dpr); - canvas.height = Math.round(height * dpr); - ctx.scale(dpr, dpr); - - if (bgEnabled) { - ctx.fillStyle = bgColor; - ctx.fillRect(0, 0, width, height); - } - // else: leave transparent (the default for a fresh canvas). - - ctx.font = fontSpec; - ctx.fillStyle = textColor; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(safeText, width / 2, height / 2); - - const fileBase = - safeText - .toLowerCase() - .replace(/[^a-z0-9]+/g, "-") - .replace(/^-+|-+$/g, "") || "logo"; - - canvas.toBlob((blob) => { - if (!blob) return; - const url = URL.createObjectURL(blob); - const a = linkRef.current ?? document.createElement("a"); - a.href = url; - a.download = `${fileBase}-logo.png`; - a.click(); - // Release the object URL once the download has been kicked off. - setTimeout(() => URL.revokeObjectURL(url), 1000); - }, "image/png"); - }, [bgColor, bgEnabled, fontStack, fontWeight, safeText, size, textColor]); - - const handleSave = useCallback(() => { - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - if (!ctx) return; - - const fontSpec = `${fontWeight} ${size}px ${fontStack}`; - ctx.font = fontSpec; - const metrics = ctx.measureText(safeText); - const padX = Math.round(size * 0.4); - const padY = Math.round(size * 0.45); - const textWidth = Math.max(1, Math.ceil(metrics.width)); - const width = textWidth + padX * 2; - const height = Math.ceil(size * 1.35) + padY * 2; - - const dpr = Math.min(typeof window !== "undefined" ? window.devicePixelRatio || 1 : 1, 3); - canvas.width = Math.round(width * dpr); - canvas.height = Math.round(height * dpr); - ctx.scale(dpr, dpr); - - if (bgEnabled) { - ctx.fillStyle = bgColor; - ctx.fillRect(0, 0, width, height); - } - - ctx.font = fontSpec; - ctx.fillStyle = textColor; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; - ctx.fillText(safeText, width / 2, height / 2); + window.open(gifUrl, "_blank"); + }, [gifUrl]); + const handleSave = useCallback(async () => { setSaveStatus("saving"); - canvas.toBlob(async (blob) => { - if (!blob) { setSaveStatus("error"); return; } - const file = new File([blob], "logo.png", { type: "image/png" }); + try { + const res = await fetch(gifUrl); + const blob = await res.blob(); + const ext = blob.type === "image/gif" ? "gif" : "png"; + const file = new File([blob], `logo.${ext}`, { type: blob.type }); const fd = new FormData(); fd.append("file", file); + startTransition(async () => { const result = await saveLogo(fd); setSaveStatus(result.success ? "done" : "error"); if (result.success) setTimeout(() => setSaveStatus("idle"), 3000); }); - }, "image/png"); - }, [bgColor, bgEnabled, fontStack, fontWeight, safeText, size, textColor, startTransition]); + } catch { + setSaveStatus("error"); + setTimeout(() => setSaveStatus("idle"), 3000); + } + }, [gifUrl, startTransition]); return (
- {/* ── Controls ──────────────────────────────── */} - +
+ + -
- - - -
- - - - - - {/* Reused, hidden anchor used to trigger the file download. */} -
- download -
- {/* ── Live preview ──────────────────────────── */} - +
-

{safeText}

+ {/* eslint-disable-next-line @next/next/no-img-element */} + {`Logo
-

- Everything runs in your browser — nothing is uploaded. The PNG keeps a - {" "} - transparent background unless you enable one. +

+ Generated via{" "} + + habbofont.net + + . The GIF is loaded from their server.