facehash 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/next/index.js CHANGED
@@ -120,9 +120,9 @@ const FACE_SVG_DATA = {
120
120
  //#region src/next/image.tsx
121
121
  /**
122
122
  * Static Facehash image component for use with ImageResponse.
123
- * Uses only Satori-compatible CSS (flexbox, no transforms).
123
+ * Uses only Satori-compatible CSS (flexbox, position offsets for 3D effect).
124
124
  */
125
- function FacehashImage({ data, backgroundColor, size, variant, showInitial }) {
125
+ function FacehashImage({ data, backgroundColor, size, variant, showInitial, rotation }) {
126
126
  const { faceType, initial } = data;
127
127
  const svgData = FACE_SVG_DATA[faceType];
128
128
  const [, , vbWidth, vbHeight] = svgData.viewBox.split(" ").map(Number);
@@ -130,6 +130,9 @@ function FacehashImage({ data, backgroundColor, size, variant, showInitial }) {
130
130
  const faceWidth = size * .6;
131
131
  const faceHeight = faceWidth / aspectRatio;
132
132
  const fontSize = size * .26;
133
+ const offsetMagnitude = size * .05;
134
+ const offsetX = rotation.y * offsetMagnitude;
135
+ const offsetY = -rotation.x * offsetMagnitude;
133
136
  return /* @__PURE__ */ jsxs("div", {
134
137
  style: {
135
138
  width: size,
@@ -153,7 +156,9 @@ function FacehashImage({ data, backgroundColor, size, variant, showInitial }) {
153
156
  display: "flex",
154
157
  flexDirection: "column",
155
158
  alignItems: "center",
156
- justifyContent: "center"
159
+ justifyContent: "center",
160
+ marginLeft: offsetX,
161
+ marginTop: offsetY
157
162
  },
158
163
  children: [/* @__PURE__ */ jsx("svg", {
159
164
  "aria-label": "Avatar face",
@@ -269,6 +274,7 @@ function toFacehashHandler(options = {}) {
269
274
  return new ImageResponse(/* @__PURE__ */ jsx(FacehashImage, {
270
275
  backgroundColor,
271
276
  data,
277
+ rotation: data.rotation,
272
278
  showInitial,
273
279
  size,
274
280
  variant
package/next/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["FACE_TYPES: readonly FaceType[]","FACE_SVG_DATA: Record<\n\tFaceType,\n\t{\n\t\tviewBox: string;\n\t\tpaths: string[];\n\t}\n>","headers: Record<string, string>"],"sources":["../../src/core/facehash-data.ts","../../src/next/faces-svg.ts","../../src/next/image.tsx","../../src/next/handler.tsx"],"sourcesContent":["import { stringHash } from \"../utils/hash\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type Variant = \"gradient\" | \"solid\";\n\nexport type FaceType = \"round\" | \"cross\" | \"line\" | \"curved\";\n\nexport const FACE_TYPES: readonly FaceType[] = [\n\t\"round\",\n\t\"cross\",\n\t\"line\",\n\t\"curved\",\n] as const;\n\nexport type FacehashData = {\n\t/** The face type to render */\n\tfaceType: FaceType;\n\t/** Index into the colors array */\n\tcolorIndex: number;\n\t/** Rotation position for 3D effect (-1, 0, or 1 for each axis) */\n\trotation: { x: number; y: number };\n\t/** First letter of the name, uppercase */\n\tinitial: string;\n};\n\nexport type ComputeFacehashOptions = {\n\t/** String to generate face from */\n\tname: string;\n\t/** Number of colors available (for modulo) */\n\tcolorsLength?: number;\n};\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst SPHERE_POSITIONS = [\n\t{ x: -1, y: 1 }, // down-right\n\t{ x: 1, y: 1 }, // up-right\n\t{ x: 1, y: 0 }, // up\n\t{ x: 0, y: 1 }, // right\n\t{ x: -1, y: 0 }, // down\n\t{ x: 0, y: 0 }, // center\n\t{ x: 0, y: -1 }, // left\n\t{ x: -1, y: -1 }, // down-left\n\t{ x: 1, y: -1 }, // up-left\n] as const;\n\n/**\n * Default color palette using Tailwind CSS color values.\n */\nexport const DEFAULT_COLORS = [\n\t\"#ec4899\", // pink-500\n\t\"#f59e0b\", // amber-500\n\t\"#3b82f6\", // blue-500\n\t\"#f97316\", // orange-500\n\t\"#10b981\", // emerald-500\n] as const;\n\n// ============================================================================\n// Core Functions\n// ============================================================================\n\n/**\n * Computes deterministic face properties from a name string.\n * Pure function with no side effects or React dependencies.\n */\nexport function computeFacehash(options: ComputeFacehashOptions): FacehashData {\n\tconst { name, colorsLength = DEFAULT_COLORS.length } = options;\n\n\tconst hash = stringHash(name);\n\tconst faceIndex = hash % FACE_TYPES.length;\n\tconst colorIndex = hash % colorsLength;\n\tconst positionIndex = hash % SPHERE_POSITIONS.length;\n\tconst position = SPHERE_POSITIONS[positionIndex] ?? { x: 0, y: 0 };\n\n\treturn {\n\t\tfaceType: FACE_TYPES[faceIndex] ?? \"round\",\n\t\tcolorIndex,\n\t\trotation: position,\n\t\tinitial: name.charAt(0).toUpperCase(),\n\t};\n}\n\nconst FALLBACK_COLOR = \"#ec4899\"; // pink-500\n\n/**\n * Gets a color from an array by index, with fallback to default colors.\n */\nexport function getColor(\n\tcolors: readonly string[] | undefined,\n\tindex: number\n): string {\n\tconst palette = colors && colors.length > 0 ? colors : DEFAULT_COLORS;\n\treturn palette[index % palette.length] ?? FALLBACK_COLOR;\n}\n","import type { FaceType } from \"../core\";\n\n/**\n * SVG path data for each face type.\n * Used for static image generation with Satori/ImageResponse.\n */\nexport const FACE_SVG_DATA: Record<\n\tFaceType,\n\t{\n\t\tviewBox: string;\n\t\tpaths: string[];\n\t}\n> = {\n\tround: {\n\t\tviewBox: \"0 0 63 15\",\n\t\tpaths: [\n\t\t\t\"M62.4 7.2C62.4 11.1765 59.1765 14.4 55.2 14.4C51.2236 14.4 48 11.1765 48 7.2C48 3.22355 51.2236 0 55.2 0C59.1765 0 62.4 3.22355 62.4 7.2Z\",\n\t\t\t\"M14.4 7.2C14.4 11.1765 11.1765 14.4 7.2 14.4C3.22355 14.4 0 11.1765 0 7.2C0 3.22355 3.22355 0 7.2 0C11.1765 0 14.4 3.22355 14.4 7.2Z\",\n\t\t],\n\t},\n\tcross: {\n\t\tviewBox: \"0 0 71 23\",\n\t\tpaths: [\n\t\t\t\"M11.5 0C12.9411 0 13.6619 0.000460386 14.1748 0.354492C14.3742 0.49213 14.547 0.664882 14.6846 0.864258C15.0384 1.37711 15.0391 2.09739 15.0391 3.53809V7.96094H19.4619C20.9027 7.96094 21.6229 7.9615 22.1357 8.31543C22.3352 8.45308 22.5079 8.62578 22.6455 8.8252C22.9995 9.3381 23 10.0589 23 11.5C23 12.9408 22.9995 13.661 22.6455 14.1738C22.5079 14.3733 22.3352 14.5459 22.1357 14.6836C21.6229 15.0375 20.9027 15.0381 19.4619 15.0381H15.0391V19.4619C15.0391 20.9026 15.0384 21.6229 14.6846 22.1357C14.547 22.3351 14.3742 22.5079 14.1748 22.6455C13.6619 22.9995 12.9411 23 11.5 23C10.0592 23 9.33903 22.9994 8.82617 22.6455C8.62674 22.5079 8.45309 22.3352 8.31543 22.1357C7.96175 21.6229 7.96191 20.9024 7.96191 19.4619V15.0381H3.53809C2.0973 15.0381 1.37711 15.0375 0.864258 14.6836C0.664834 14.5459 0.492147 14.3733 0.354492 14.1738C0.000498831 13.661 -5.88036e-08 12.9408 0 11.5C6.2999e-08 10.0589 0.000460356 9.3381 0.354492 8.8252C0.492144 8.62578 0.664842 8.45308 0.864258 8.31543C1.37711 7.9615 2.09731 7.96094 3.53809 7.96094H7.96191V3.53809C7.96191 2.09765 7.96175 1.37709 8.31543 0.864258C8.45309 0.664828 8.62674 0.492149 8.82617 0.354492C9.33903 0.000555366 10.0592 1.62347e-09 11.5 0Z\",\n\t\t\t\"M58.7695 0C60.2107 0 60.9314 0.000460386 61.4443 0.354492C61.6437 0.49213 61.8165 0.664882 61.9541 0.864258C62.308 1.37711 62.3086 2.09739 62.3086 3.53809V7.96094H66.7314C68.1722 7.96094 68.8924 7.9615 69.4053 8.31543C69.6047 8.45308 69.7774 8.62578 69.915 8.8252C70.2691 9.3381 70.2695 10.0589 70.2695 11.5C70.2695 12.9408 70.269 13.661 69.915 14.1738C69.7774 14.3733 69.6047 14.5459 69.4053 14.6836C68.8924 15.0375 68.1722 15.0381 66.7314 15.0381H62.3086V19.4619C62.3086 20.9026 62.308 21.6229 61.9541 22.1357C61.8165 22.3351 61.6437 22.5079 61.4443 22.6455C60.9314 22.9995 60.2107 23 58.7695 23C57.3287 23 56.6086 22.9994 56.0957 22.6455C55.8963 22.5079 55.7226 22.3352 55.585 22.1357C55.2313 21.6229 55.2314 20.9024 55.2314 19.4619V15.0381H50.8076C49.3668 15.0381 48.6466 15.0375 48.1338 14.6836C47.9344 14.5459 47.7617 14.3733 47.624 14.1738C47.27 13.661 47.2695 12.9408 47.2695 11.5C47.2695 10.0589 47.27 9.3381 47.624 8.8252C47.7617 8.62578 47.9344 8.45308 48.1338 8.31543C48.6466 7.9615 49.3668 7.96094 50.8076 7.96094H55.2314V3.53809C55.2314 2.09765 55.2313 1.37709 55.585 0.864258C55.7226 0.664828 55.8963 0.492149 56.0957 0.354492C56.6086 0.000555366 57.3287 1.62347e-09 58.7695 0Z\",\n\t\t],\n\t},\n\tline: {\n\t\tviewBox: \"0 0 82 8\",\n\t\tpaths: [\n\t\t\t\"M3.53125 0.164063C4.90133 0.164063 5.58673 0.163893 6.08301 0.485352C6.31917 0.638428 6.52075 0.840012 6.67383 1.07617C6.99555 1.57252 6.99512 2.25826 6.99512 3.62891C6.99512 4.99911 6.99536 5.68438 6.67383 6.18066C6.52075 6.41682 6.31917 6.61841 6.08301 6.77148C5.58672 7.09305 4.90147 7.09277 3.53125 7.09277C2.16062 7.09277 1.47486 7.09319 0.978516 6.77148C0.742356 6.61841 0.540772 6.41682 0.387695 6.18066C0.0662401 5.68439 0.0664063 4.999 0.0664063 3.62891C0.0664063 2.25838 0.0660571 1.57251 0.387695 1.07617C0.540772 0.840012 0.742356 0.638428 0.978516 0.485352C1.47485 0.163744 2.16076 0.164063 3.53125 0.164063Z\",\n\t\t\t\"M25.1836 0.164063C26.5542 0.164063 27.24 0.163638 27.7363 0.485352C27.9724 0.638384 28.1731 0.8401 28.3262 1.07617C28.6479 1.57252 28.6484 2.25825 28.6484 3.62891C28.6484 4.99931 28.6478 5.68436 28.3262 6.18066C28.1731 6.41678 27.9724 6.61842 27.7363 6.77148C27.24 7.09321 26.5542 7.09277 25.1836 7.09277H11.3262C9.95557 7.09277 9.26978 7.09317 8.77344 6.77148C8.53728 6.61841 8.33569 6.41682 8.18262 6.18066C7.86115 5.68438 7.86133 4.99902 7.86133 3.62891C7.86133 2.25835 7.86096 1.57251 8.18262 1.07617C8.33569 0.840012 8.53728 0.638428 8.77344 0.485352C9.26977 0.163768 9.95572 0.164063 11.3262 0.164063H25.1836Z\",\n\t\t\t\"M78.2034 7.09325C76.8333 7.09325 76.1479 7.09342 75.6516 6.77197C75.4155 6.61889 75.2139 6.4173 75.0608 6.18114C74.7391 5.6848 74.7395 4.99905 74.7395 3.62841C74.7395 2.2582 74.7393 1.57294 75.0608 1.07665C75.2139 0.840493 75.4155 0.638909 75.6516 0.485832C76.1479 0.164271 76.8332 0.164543 78.2034 0.164543C79.574 0.164543 80.2598 0.164122 80.7561 0.485832C80.9923 0.638909 81.1939 0.840493 81.347 1.07665C81.6684 1.57293 81.6682 2.25831 81.6682 3.62841C81.6682 4.99894 81.6686 5.68481 81.347 6.18114C81.1939 6.4173 80.9923 6.61889 80.7561 6.77197C80.2598 7.09357 79.5739 7.09325 78.2034 7.09325Z\",\n\t\t\t\"M56.5511 7.09325C55.1804 7.09325 54.4947 7.09368 53.9983 6.77197C53.7622 6.61893 53.5615 6.41722 53.4085 6.18114C53.0868 5.6848 53.0862 4.99907 53.0862 3.62841C53.0862 2.258 53.0868 1.57296 53.4085 1.07665C53.5615 0.840539 53.7622 0.638898 53.9983 0.485832C54.4947 0.164105 55.1804 0.164543 56.5511 0.164543H70.4085C71.7791 0.164543 72.4649 0.164146 72.9612 0.485832C73.1974 0.638909 73.399 0.840493 73.552 1.07665C73.8735 1.57293 73.8733 2.25829 73.8733 3.62841C73.8733 4.99896 73.8737 5.68481 73.552 6.18114C73.399 6.4173 73.1974 6.61889 72.9612 6.77197C72.4649 7.09355 71.7789 7.09325 70.4085 7.09325H56.5511Z\",\n\t\t],\n\t},\n\tcurved: {\n\t\tviewBox: \"0 0 63 9\",\n\t\tpaths: [\n\t\t\t\"M0 5.06511C0 4.94513 0 4.88513 0.00771184 4.79757C0.0483059 4.33665 0.341025 3.76395 0.690821 3.46107C0.757274 3.40353 0.783996 3.38422 0.837439 3.34559C2.40699 2.21129 6.03888 0 10.5 0C14.9611 0 18.593 2.21129 20.1626 3.34559C20.216 3.38422 20.2427 3.40353 20.3092 3.46107C20.659 3.76395 20.9517 4.33665 20.9923 4.79757C21 4.88513 21 4.94513 21 5.06511C21 6.01683 21 6.4927 20.9657 6.6754C20.7241 7.96423 19.8033 8.55941 18.5289 8.25054C18.3483 8.20676 17.8198 7.96876 16.7627 7.49275C14.975 6.68767 12.7805 6 10.5 6C8.21954 6 6.02504 6.68767 4.23727 7.49275C3.18025 7.96876 2.65174 8.20676 2.47108 8.25054C1.19668 8.55941 0.275917 7.96423 0.0342566 6.6754C0 6.4927 0 6.01683 0 5.06511Z\",\n\t\t\t\"M42 5.06511C42 4.94513 42 4.88513 42.0077 4.79757C42.0483 4.33665 42.341 3.76395 42.6908 3.46107C42.7573 3.40353 42.784 3.38422 42.8374 3.34559C44.407 2.21129 48.0389 0 52.5 0C56.9611 0 60.593 2.21129 62.1626 3.34559C62.216 3.38422 62.2427 3.40353 62.3092 3.46107C62.659 3.76395 62.9517 4.33665 62.9923 4.79757C63 4.88513 63 4.94513 63 5.06511C63 6.01683 63 6.4927 62.9657 6.6754C62.7241 7.96423 61.8033 8.55941 60.5289 8.25054C60.3483 8.20676 59.8198 7.96876 58.7627 7.49275C56.975 6.68767 54.7805 6 52.5 6C50.2195 6 48.025 6.68767 46.2373 7.49275C45.1802 7.96876 44.6517 8.20676 44.4711 8.25054C43.1967 8.55941 42.2759 7.96423 42.0343 6.6754C42 6.4927 42 6.01683 42 5.06511Z\",\n\t\t],\n\t},\n};\n","import type { FacehashData, Variant } from \"../core\";\nimport { FACE_SVG_DATA } from \"./faces-svg\";\n\nexport type FacehashImageProps = {\n\t/** Computed facehash data */\n\tdata: FacehashData;\n\t/** Background color (hex) */\n\tbackgroundColor: string;\n\t/** Image size in pixels */\n\tsize: number;\n\t/** Background style variant */\n\tvariant: Variant;\n\t/** Show initial letter */\n\tshowInitial: boolean;\n};\n\n/**\n * Static Facehash image component for use with ImageResponse.\n * Uses only Satori-compatible CSS (flexbox, no transforms).\n */\nexport function FacehashImage({\n\tdata,\n\tbackgroundColor,\n\tsize,\n\tvariant,\n\tshowInitial,\n}: FacehashImageProps) {\n\tconst { faceType, initial } = data;\n\tconst svgData = FACE_SVG_DATA[faceType];\n\n\t// Calculate SVG dimensions based on viewBox\n\tconst [, , vbWidth, vbHeight] = svgData.viewBox.split(\" \").map(Number);\n\tconst aspectRatio = (vbWidth ?? 1) / (vbHeight ?? 1);\n\n\t// Face takes up ~60% of the container width\n\tconst faceWidth = size * 0.6;\n\tconst faceHeight = faceWidth / aspectRatio;\n\n\t// Font size for initial (26% of size, matching cqw from React component)\n\tconst fontSize = size * 0.26;\n\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\twidth: size,\n\t\t\t\theight: size,\n\t\t\t\tdisplay: \"flex\",\n\t\t\t\tflexDirection: \"column\",\n\t\t\t\talignItems: \"center\",\n\t\t\t\tjustifyContent: \"center\",\n\t\t\t\tbackgroundColor,\n\t\t\t\tposition: \"relative\",\n\t\t\t}}\n\t\t>\n\t\t\t{/* Gradient overlay */}\n\t\t\t{variant === \"gradient\" && (\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\ttop: 0,\n\t\t\t\t\t\tleft: 0,\n\t\t\t\t\t\tright: 0,\n\t\t\t\t\t\tbottom: 0,\n\t\t\t\t\t\tbackground:\n\t\t\t\t\t\t\t\"radial-gradient(ellipse 100% 100% at 50% 50%, rgba(255,255,255,0.15) 0%, transparent 60%)\",\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t)}\n\n\t\t\t{/* Face container */}\n\t\t\t<div\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{/* Face SVG */}\n\t\t\t\t<svg\n\t\t\t\t\taria-label=\"Avatar face\"\n\t\t\t\t\tfill=\"none\"\n\t\t\t\t\theight={faceHeight}\n\t\t\t\t\trole=\"img\"\n\t\t\t\t\tviewBox={svgData.viewBox}\n\t\t\t\t\twidth={faceWidth}\n\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t>\n\t\t\t\t\t{svgData.paths.map((d, i) => (\n\t\t\t\t\t\t<path d={d} fill=\"black\" key={i} />\n\t\t\t\t\t))}\n\t\t\t\t</svg>\n\n\t\t\t\t{/* Initial letter */}\n\t\t\t\t{showInitial && (\n\t\t\t\t\t<span\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tmarginTop: size * 0.08,\n\t\t\t\t\t\t\tfontSize,\n\t\t\t\t\t\t\tlineHeight: 1,\n\t\t\t\t\t\t\tfontFamily: \"monospace\",\n\t\t\t\t\t\t\tfontWeight: 700,\n\t\t\t\t\t\t\tcolor: \"black\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{initial}\n\t\t\t\t\t</span>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n","import { ImageResponse } from \"next/og\";\nimport type { NextRequest } from \"next/server\";\nimport {\n\tcomputeFacehash,\n\tDEFAULT_COLORS,\n\tgetColor,\n\ttype Variant,\n} from \"../core\";\nimport { FacehashImage } from \"./image\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type FacehashHandlerOptions = {\n\t/**\n\t * Default image size in pixels.\n\t * Can be overridden via `?size=` query param.\n\t * @default 400\n\t */\n\tsize?: number;\n\n\t/**\n\t * Default background style.\n\t * Can be overridden via `?variant=` query param.\n\t * @default \"gradient\"\n\t */\n\tvariant?: Variant;\n\n\t/**\n\t * Default for showing initial letter.\n\t * Can be overridden via `?showInitial=` query param.\n\t * @default true\n\t */\n\tshowInitial?: boolean;\n\n\t/**\n\t * Default color palette (hex colors).\n\t * Can be overridden via `?colors=` query param (comma-separated).\n\t * @default [\"#ec4899\", \"#f59e0b\", \"#3b82f6\", \"#f97316\", \"#10b981\"]\n\t */\n\tcolors?: string[];\n\n\t/**\n\t * Cache-Control header value.\n\t * Set to `null` to disable caching.\n\t * @default \"public, max-age=31536000, immutable\"\n\t */\n\tcacheControl?: string | null;\n};\n\nexport type FacehashHandler = {\n\tGET: (request: NextRequest) => Promise<ImageResponse>;\n};\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\nconst HEX_COLOR_REGEX = /^#[0-9A-Fa-f]{3,8}$/;\n\nfunction parseBoolean(value: string | null, defaultValue: boolean): boolean {\n\tif (value === null) {\n\t\treturn defaultValue;\n\t}\n\treturn value === \"true\" || value === \"1\";\n}\n\nfunction parseNumber(\n\tvalue: string | null,\n\tdefaultValue: number,\n\tmin = 1,\n\tmax = 2000\n): number {\n\tif (value === null) {\n\t\treturn defaultValue;\n\t}\n\tconst num = Number.parseInt(value, 10);\n\tif (Number.isNaN(num)) {\n\t\treturn defaultValue;\n\t}\n\treturn Math.min(Math.max(num, min), max);\n}\n\nfunction parseColors(value: string | null): string[] | undefined {\n\tif (!value) {\n\t\treturn;\n\t}\n\tconst colors = value\n\t\t.split(\",\")\n\t\t.map((c) => c.trim())\n\t\t.filter((c) => HEX_COLOR_REGEX.test(c));\n\treturn colors.length > 0 ? colors : undefined;\n}\n\nfunction parseVariant(value: string | null): Variant | undefined {\n\tif (value === \"gradient\" || value === \"solid\") {\n\t\treturn value;\n\t}\n\treturn;\n}\n\n// ============================================================================\n// Main Export\n// ============================================================================\n\n/**\n * Creates a Next.js route handler for generating Facehash avatar images.\n *\n * @example\n * ```ts\n * // app/api/avatar/route.ts\n * import { toFacehashHandler } from \"facehash/next\";\n *\n * export const { GET } = toFacehashHandler();\n * ```\n *\n * @example\n * ```ts\n * // With custom defaults\n * export const { GET } = toFacehashHandler({\n * size: 200,\n * variant: \"solid\",\n * colors: [\"#ff0000\", \"#00ff00\", \"#0000ff\"],\n * });\n * ```\n *\n * Query parameters:\n * - `name` (required): String to generate avatar from\n * - `size`: Image size in pixels (default: 400)\n * - `variant`: \"gradient\" or \"solid\" (default: \"gradient\")\n * - `showInitial`: \"true\" or \"false\" (default: \"true\")\n * - `colors`: Comma-separated hex colors (e.g., \"#ff0000,#00ff00\")\n */\nexport function toFacehashHandler(\n\toptions: FacehashHandlerOptions = {}\n): FacehashHandler {\n\tconst {\n\t\tsize: defaultSize = 400,\n\t\tvariant: defaultVariant = \"gradient\",\n\t\tshowInitial: defaultShowInitial = true,\n\t\tcolors: defaultColors = [...DEFAULT_COLORS],\n\t\tcacheControl = \"public, max-age=31536000, immutable\",\n\t} = options;\n\n\tasync function GET(request: NextRequest): Promise<ImageResponse> {\n\t\tconst searchParams = request.nextUrl.searchParams;\n\n\t\t// Parse name (required)\n\t\tconst name = searchParams.get(\"name\");\n\t\tif (!name) {\n\t\t\treturn new ImageResponse(\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\tbackgroundColor: \"#f3f4f6\",\n\t\t\t\t\t\tcolor: \"#6b7280\",\n\t\t\t\t\t\tfontSize: 24,\n\t\t\t\t\t\tfontFamily: \"sans-serif\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\tMissing ?name= parameter\n\t\t\t\t</div>,\n\t\t\t\t{\n\t\t\t\t\twidth: defaultSize,\n\t\t\t\t\theight: defaultSize,\n\t\t\t\t\tstatus: 400,\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t\"Content-Type\": \"image/png\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\n\t\t// Parse options from query params (override defaults)\n\t\tconst size = parseNumber(searchParams.get(\"size\"), defaultSize, 16, 2000);\n\t\tconst variant = parseVariant(searchParams.get(\"variant\")) ?? defaultVariant;\n\t\tconst showInitial = parseBoolean(\n\t\t\tsearchParams.get(\"showInitial\"),\n\t\t\tdefaultShowInitial\n\t\t);\n\t\tconst colors = parseColors(searchParams.get(\"colors\")) ?? defaultColors;\n\n\t\t// Compute facehash data\n\t\tconst data = computeFacehash({\n\t\t\tname,\n\t\t\tcolorsLength: colors.length,\n\t\t});\n\n\t\t// Get background color\n\t\tconst backgroundColor = getColor(colors, data.colorIndex);\n\n\t\t// Build response headers\n\t\tconst headers: Record<string, string> = {\n\t\t\t\"Content-Type\": \"image/png\",\n\t\t};\n\n\t\tif (cacheControl) {\n\t\t\theaders[\"Cache-Control\"] = cacheControl;\n\t\t}\n\n\t\t// Generate image\n\t\treturn new ImageResponse(\n\t\t\t<FacehashImage\n\t\t\t\tbackgroundColor={backgroundColor}\n\t\t\t\tdata={data}\n\t\t\t\tshowInitial={showInitial}\n\t\t\t\tsize={size}\n\t\t\t\tvariant={variant}\n\t\t\t/>,\n\t\t\t{\n\t\t\t\twidth: size,\n\t\t\t\theight: size,\n\t\t\t\theaders,\n\t\t\t}\n\t\t);\n\t}\n\n\treturn { GET };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["FACE_TYPES: readonly FaceType[]","FACE_SVG_DATA: Record<\n\tFaceType,\n\t{\n\t\tviewBox: string;\n\t\tpaths: string[];\n\t}\n>","headers: Record<string, string>"],"sources":["../../src/core/facehash-data.ts","../../src/next/faces-svg.ts","../../src/next/image.tsx","../../src/next/handler.tsx"],"sourcesContent":["import { stringHash } from \"../utils/hash\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type Variant = \"gradient\" | \"solid\";\n\nexport type FaceType = \"round\" | \"cross\" | \"line\" | \"curved\";\n\nexport const FACE_TYPES: readonly FaceType[] = [\n\t\"round\",\n\t\"cross\",\n\t\"line\",\n\t\"curved\",\n] as const;\n\nexport type FacehashData = {\n\t/** The face type to render */\n\tfaceType: FaceType;\n\t/** Index into the colors array */\n\tcolorIndex: number;\n\t/** Rotation position for 3D effect (-1, 0, or 1 for each axis) */\n\trotation: { x: number; y: number };\n\t/** First letter of the name, uppercase */\n\tinitial: string;\n};\n\nexport type ComputeFacehashOptions = {\n\t/** String to generate face from */\n\tname: string;\n\t/** Number of colors available (for modulo) */\n\tcolorsLength?: number;\n};\n\n// ============================================================================\n// Constants\n// ============================================================================\n\nconst SPHERE_POSITIONS = [\n\t{ x: -1, y: 1 }, // down-right\n\t{ x: 1, y: 1 }, // up-right\n\t{ x: 1, y: 0 }, // up\n\t{ x: 0, y: 1 }, // right\n\t{ x: -1, y: 0 }, // down\n\t{ x: 0, y: 0 }, // center\n\t{ x: 0, y: -1 }, // left\n\t{ x: -1, y: -1 }, // down-left\n\t{ x: 1, y: -1 }, // up-left\n] as const;\n\n/**\n * Default color palette using Tailwind CSS color values.\n */\nexport const DEFAULT_COLORS = [\n\t\"#ec4899\", // pink-500\n\t\"#f59e0b\", // amber-500\n\t\"#3b82f6\", // blue-500\n\t\"#f97316\", // orange-500\n\t\"#10b981\", // emerald-500\n] as const;\n\n// ============================================================================\n// Core Functions\n// ============================================================================\n\n/**\n * Computes deterministic face properties from a name string.\n * Pure function with no side effects or React dependencies.\n */\nexport function computeFacehash(options: ComputeFacehashOptions): FacehashData {\n\tconst { name, colorsLength = DEFAULT_COLORS.length } = options;\n\n\tconst hash = stringHash(name);\n\tconst faceIndex = hash % FACE_TYPES.length;\n\tconst colorIndex = hash % colorsLength;\n\tconst positionIndex = hash % SPHERE_POSITIONS.length;\n\tconst position = SPHERE_POSITIONS[positionIndex] ?? { x: 0, y: 0 };\n\n\treturn {\n\t\tfaceType: FACE_TYPES[faceIndex] ?? \"round\",\n\t\tcolorIndex,\n\t\trotation: position,\n\t\tinitial: name.charAt(0).toUpperCase(),\n\t};\n}\n\nconst FALLBACK_COLOR = \"#ec4899\"; // pink-500\n\n/**\n * Gets a color from an array by index, with fallback to default colors.\n */\nexport function getColor(\n\tcolors: readonly string[] | undefined,\n\tindex: number\n): string {\n\tconst palette = colors && colors.length > 0 ? colors : DEFAULT_COLORS;\n\treturn palette[index % palette.length] ?? FALLBACK_COLOR;\n}\n","import type { FaceType } from \"../core\";\n\n/**\n * SVG path data for each face type.\n * Used for static image generation with Satori/ImageResponse.\n */\nexport const FACE_SVG_DATA: Record<\n\tFaceType,\n\t{\n\t\tviewBox: string;\n\t\tpaths: string[];\n\t}\n> = {\n\tround: {\n\t\tviewBox: \"0 0 63 15\",\n\t\tpaths: [\n\t\t\t\"M62.4 7.2C62.4 11.1765 59.1765 14.4 55.2 14.4C51.2236 14.4 48 11.1765 48 7.2C48 3.22355 51.2236 0 55.2 0C59.1765 0 62.4 3.22355 62.4 7.2Z\",\n\t\t\t\"M14.4 7.2C14.4 11.1765 11.1765 14.4 7.2 14.4C3.22355 14.4 0 11.1765 0 7.2C0 3.22355 3.22355 0 7.2 0C11.1765 0 14.4 3.22355 14.4 7.2Z\",\n\t\t],\n\t},\n\tcross: {\n\t\tviewBox: \"0 0 71 23\",\n\t\tpaths: [\n\t\t\t\"M11.5 0C12.9411 0 13.6619 0.000460386 14.1748 0.354492C14.3742 0.49213 14.547 0.664882 14.6846 0.864258C15.0384 1.37711 15.0391 2.09739 15.0391 3.53809V7.96094H19.4619C20.9027 7.96094 21.6229 7.9615 22.1357 8.31543C22.3352 8.45308 22.5079 8.62578 22.6455 8.8252C22.9995 9.3381 23 10.0589 23 11.5C23 12.9408 22.9995 13.661 22.6455 14.1738C22.5079 14.3733 22.3352 14.5459 22.1357 14.6836C21.6229 15.0375 20.9027 15.0381 19.4619 15.0381H15.0391V19.4619C15.0391 20.9026 15.0384 21.6229 14.6846 22.1357C14.547 22.3351 14.3742 22.5079 14.1748 22.6455C13.6619 22.9995 12.9411 23 11.5 23C10.0592 23 9.33903 22.9994 8.82617 22.6455C8.62674 22.5079 8.45309 22.3352 8.31543 22.1357C7.96175 21.6229 7.96191 20.9024 7.96191 19.4619V15.0381H3.53809C2.0973 15.0381 1.37711 15.0375 0.864258 14.6836C0.664834 14.5459 0.492147 14.3733 0.354492 14.1738C0.000498831 13.661 -5.88036e-08 12.9408 0 11.5C6.2999e-08 10.0589 0.000460356 9.3381 0.354492 8.8252C0.492144 8.62578 0.664842 8.45308 0.864258 8.31543C1.37711 7.9615 2.09731 7.96094 3.53809 7.96094H7.96191V3.53809C7.96191 2.09765 7.96175 1.37709 8.31543 0.864258C8.45309 0.664828 8.62674 0.492149 8.82617 0.354492C9.33903 0.000555366 10.0592 1.62347e-09 11.5 0Z\",\n\t\t\t\"M58.7695 0C60.2107 0 60.9314 0.000460386 61.4443 0.354492C61.6437 0.49213 61.8165 0.664882 61.9541 0.864258C62.308 1.37711 62.3086 2.09739 62.3086 3.53809V7.96094H66.7314C68.1722 7.96094 68.8924 7.9615 69.4053 8.31543C69.6047 8.45308 69.7774 8.62578 69.915 8.8252C70.2691 9.3381 70.2695 10.0589 70.2695 11.5C70.2695 12.9408 70.269 13.661 69.915 14.1738C69.7774 14.3733 69.6047 14.5459 69.4053 14.6836C68.8924 15.0375 68.1722 15.0381 66.7314 15.0381H62.3086V19.4619C62.3086 20.9026 62.308 21.6229 61.9541 22.1357C61.8165 22.3351 61.6437 22.5079 61.4443 22.6455C60.9314 22.9995 60.2107 23 58.7695 23C57.3287 23 56.6086 22.9994 56.0957 22.6455C55.8963 22.5079 55.7226 22.3352 55.585 22.1357C55.2313 21.6229 55.2314 20.9024 55.2314 19.4619V15.0381H50.8076C49.3668 15.0381 48.6466 15.0375 48.1338 14.6836C47.9344 14.5459 47.7617 14.3733 47.624 14.1738C47.27 13.661 47.2695 12.9408 47.2695 11.5C47.2695 10.0589 47.27 9.3381 47.624 8.8252C47.7617 8.62578 47.9344 8.45308 48.1338 8.31543C48.6466 7.9615 49.3668 7.96094 50.8076 7.96094H55.2314V3.53809C55.2314 2.09765 55.2313 1.37709 55.585 0.864258C55.7226 0.664828 55.8963 0.492149 56.0957 0.354492C56.6086 0.000555366 57.3287 1.62347e-09 58.7695 0Z\",\n\t\t],\n\t},\n\tline: {\n\t\tviewBox: \"0 0 82 8\",\n\t\tpaths: [\n\t\t\t\"M3.53125 0.164063C4.90133 0.164063 5.58673 0.163893 6.08301 0.485352C6.31917 0.638428 6.52075 0.840012 6.67383 1.07617C6.99555 1.57252 6.99512 2.25826 6.99512 3.62891C6.99512 4.99911 6.99536 5.68438 6.67383 6.18066C6.52075 6.41682 6.31917 6.61841 6.08301 6.77148C5.58672 7.09305 4.90147 7.09277 3.53125 7.09277C2.16062 7.09277 1.47486 7.09319 0.978516 6.77148C0.742356 6.61841 0.540772 6.41682 0.387695 6.18066C0.0662401 5.68439 0.0664063 4.999 0.0664063 3.62891C0.0664063 2.25838 0.0660571 1.57251 0.387695 1.07617C0.540772 0.840012 0.742356 0.638428 0.978516 0.485352C1.47485 0.163744 2.16076 0.164063 3.53125 0.164063Z\",\n\t\t\t\"M25.1836 0.164063C26.5542 0.164063 27.24 0.163638 27.7363 0.485352C27.9724 0.638384 28.1731 0.8401 28.3262 1.07617C28.6479 1.57252 28.6484 2.25825 28.6484 3.62891C28.6484 4.99931 28.6478 5.68436 28.3262 6.18066C28.1731 6.41678 27.9724 6.61842 27.7363 6.77148C27.24 7.09321 26.5542 7.09277 25.1836 7.09277H11.3262C9.95557 7.09277 9.26978 7.09317 8.77344 6.77148C8.53728 6.61841 8.33569 6.41682 8.18262 6.18066C7.86115 5.68438 7.86133 4.99902 7.86133 3.62891C7.86133 2.25835 7.86096 1.57251 8.18262 1.07617C8.33569 0.840012 8.53728 0.638428 8.77344 0.485352C9.26977 0.163768 9.95572 0.164063 11.3262 0.164063H25.1836Z\",\n\t\t\t\"M78.2034 7.09325C76.8333 7.09325 76.1479 7.09342 75.6516 6.77197C75.4155 6.61889 75.2139 6.4173 75.0608 6.18114C74.7391 5.6848 74.7395 4.99905 74.7395 3.62841C74.7395 2.2582 74.7393 1.57294 75.0608 1.07665C75.2139 0.840493 75.4155 0.638909 75.6516 0.485832C76.1479 0.164271 76.8332 0.164543 78.2034 0.164543C79.574 0.164543 80.2598 0.164122 80.7561 0.485832C80.9923 0.638909 81.1939 0.840493 81.347 1.07665C81.6684 1.57293 81.6682 2.25831 81.6682 3.62841C81.6682 4.99894 81.6686 5.68481 81.347 6.18114C81.1939 6.4173 80.9923 6.61889 80.7561 6.77197C80.2598 7.09357 79.5739 7.09325 78.2034 7.09325Z\",\n\t\t\t\"M56.5511 7.09325C55.1804 7.09325 54.4947 7.09368 53.9983 6.77197C53.7622 6.61893 53.5615 6.41722 53.4085 6.18114C53.0868 5.6848 53.0862 4.99907 53.0862 3.62841C53.0862 2.258 53.0868 1.57296 53.4085 1.07665C53.5615 0.840539 53.7622 0.638898 53.9983 0.485832C54.4947 0.164105 55.1804 0.164543 56.5511 0.164543H70.4085C71.7791 0.164543 72.4649 0.164146 72.9612 0.485832C73.1974 0.638909 73.399 0.840493 73.552 1.07665C73.8735 1.57293 73.8733 2.25829 73.8733 3.62841C73.8733 4.99896 73.8737 5.68481 73.552 6.18114C73.399 6.4173 73.1974 6.61889 72.9612 6.77197C72.4649 7.09355 71.7789 7.09325 70.4085 7.09325H56.5511Z\",\n\t\t],\n\t},\n\tcurved: {\n\t\tviewBox: \"0 0 63 9\",\n\t\tpaths: [\n\t\t\t\"M0 5.06511C0 4.94513 0 4.88513 0.00771184 4.79757C0.0483059 4.33665 0.341025 3.76395 0.690821 3.46107C0.757274 3.40353 0.783996 3.38422 0.837439 3.34559C2.40699 2.21129 6.03888 0 10.5 0C14.9611 0 18.593 2.21129 20.1626 3.34559C20.216 3.38422 20.2427 3.40353 20.3092 3.46107C20.659 3.76395 20.9517 4.33665 20.9923 4.79757C21 4.88513 21 4.94513 21 5.06511C21 6.01683 21 6.4927 20.9657 6.6754C20.7241 7.96423 19.8033 8.55941 18.5289 8.25054C18.3483 8.20676 17.8198 7.96876 16.7627 7.49275C14.975 6.68767 12.7805 6 10.5 6C8.21954 6 6.02504 6.68767 4.23727 7.49275C3.18025 7.96876 2.65174 8.20676 2.47108 8.25054C1.19668 8.55941 0.275917 7.96423 0.0342566 6.6754C0 6.4927 0 6.01683 0 5.06511Z\",\n\t\t\t\"M42 5.06511C42 4.94513 42 4.88513 42.0077 4.79757C42.0483 4.33665 42.341 3.76395 42.6908 3.46107C42.7573 3.40353 42.784 3.38422 42.8374 3.34559C44.407 2.21129 48.0389 0 52.5 0C56.9611 0 60.593 2.21129 62.1626 3.34559C62.216 3.38422 62.2427 3.40353 62.3092 3.46107C62.659 3.76395 62.9517 4.33665 62.9923 4.79757C63 4.88513 63 4.94513 63 5.06511C63 6.01683 63 6.4927 62.9657 6.6754C62.7241 7.96423 61.8033 8.55941 60.5289 8.25054C60.3483 8.20676 59.8198 7.96876 58.7627 7.49275C56.975 6.68767 54.7805 6 52.5 6C50.2195 6 48.025 6.68767 46.2373 7.49275C45.1802 7.96876 44.6517 8.20676 44.4711 8.25054C43.1967 8.55941 42.2759 7.96423 42.0343 6.6754C42 6.4927 42 6.01683 42 5.06511Z\",\n\t\t],\n\t},\n};\n","import type { FacehashData, Variant } from \"../core\";\nimport { FACE_SVG_DATA } from \"./faces-svg\";\n\nexport type FacehashImageProps = {\n\t/** Computed facehash data */\n\tdata: FacehashData;\n\t/** Background color (hex) */\n\tbackgroundColor: string;\n\t/** Image size in pixels */\n\tsize: number;\n\t/** Background style variant */\n\tvariant: Variant;\n\t/** Show initial letter */\n\tshowInitial: boolean;\n\t/** Rotation for 3D effect simulation */\n\trotation: { x: number; y: number };\n};\n\n/**\n * Static Facehash image component for use with ImageResponse.\n * Uses only Satori-compatible CSS (flexbox, position offsets for 3D effect).\n */\nexport function FacehashImage({\n\tdata,\n\tbackgroundColor,\n\tsize,\n\tvariant,\n\tshowInitial,\n\trotation,\n}: FacehashImageProps) {\n\tconst { faceType, initial } = data;\n\tconst svgData = FACE_SVG_DATA[faceType];\n\n\t// Calculate SVG dimensions based on viewBox\n\tconst [, , vbWidth, vbHeight] = svgData.viewBox.split(\" \").map(Number);\n\tconst aspectRatio = (vbWidth ?? 1) / (vbHeight ?? 1);\n\n\t// Face takes up ~60% of the container width\n\tconst faceWidth = size * 0.6;\n\tconst faceHeight = faceWidth / aspectRatio;\n\n\t// Font size for initial (26% of size, matching cqw from React component)\n\tconst fontSize = size * 0.26;\n\n\t// Calculate 3D effect offset (simulate looking direction)\n\t// rotation.x: -1 = looking down, 0 = center, 1 = looking up\n\t// rotation.y: -1 = looking left, 0 = center, 1 = looking right\n\t// We offset the face in the opposite direction to simulate the \"looking\" effect\n\tconst offsetMagnitude = size * 0.05; // 5% of container size\n\tconst offsetX = rotation.y * offsetMagnitude; // horizontal offset (positive = right)\n\tconst offsetY = -rotation.x * offsetMagnitude; // vertical offset (positive = down, so negate)\n\n\treturn (\n\t\t<div\n\t\t\tstyle={{\n\t\t\t\twidth: size,\n\t\t\t\theight: size,\n\t\t\t\tdisplay: \"flex\",\n\t\t\t\tflexDirection: \"column\",\n\t\t\t\talignItems: \"center\",\n\t\t\t\tjustifyContent: \"center\",\n\t\t\t\tbackgroundColor,\n\t\t\t\tposition: \"relative\",\n\t\t\t}}\n\t\t>\n\t\t\t{/* Gradient overlay */}\n\t\t\t{variant === \"gradient\" && (\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\tposition: \"absolute\",\n\t\t\t\t\t\ttop: 0,\n\t\t\t\t\t\tleft: 0,\n\t\t\t\t\t\tright: 0,\n\t\t\t\t\t\tbottom: 0,\n\t\t\t\t\t\tbackground:\n\t\t\t\t\t\t\t\"radial-gradient(ellipse 100% 100% at 50% 50%, rgba(255,255,255,0.15) 0%, transparent 60%)\",\n\t\t\t\t\t}}\n\t\t\t\t/>\n\t\t\t)}\n\n\t\t\t{/* Face container with 3D position offset */}\n\t\t\t<div\n\t\t\t\tstyle={{\n\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\tflexDirection: \"column\",\n\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t// Apply position offset to simulate 3D \"looking direction\"\n\t\t\t\t\tmarginLeft: offsetX,\n\t\t\t\t\tmarginTop: offsetY,\n\t\t\t\t}}\n\t\t\t>\n\t\t\t\t{/* Face SVG */}\n\t\t\t\t<svg\n\t\t\t\t\taria-label=\"Avatar face\"\n\t\t\t\t\tfill=\"none\"\n\t\t\t\t\theight={faceHeight}\n\t\t\t\t\trole=\"img\"\n\t\t\t\t\tviewBox={svgData.viewBox}\n\t\t\t\t\twidth={faceWidth}\n\t\t\t\t\txmlns=\"http://www.w3.org/2000/svg\"\n\t\t\t\t>\n\t\t\t\t\t{svgData.paths.map((d, i) => (\n\t\t\t\t\t\t<path d={d} fill=\"black\" key={i} />\n\t\t\t\t\t))}\n\t\t\t\t</svg>\n\n\t\t\t\t{/* Initial letter */}\n\t\t\t\t{showInitial && (\n\t\t\t\t\t<span\n\t\t\t\t\t\tstyle={{\n\t\t\t\t\t\t\tmarginTop: size * 0.08,\n\t\t\t\t\t\t\tfontSize,\n\t\t\t\t\t\t\tlineHeight: 1,\n\t\t\t\t\t\t\tfontFamily: \"monospace\",\n\t\t\t\t\t\t\tfontWeight: 700,\n\t\t\t\t\t\t\tcolor: \"black\",\n\t\t\t\t\t\t}}\n\t\t\t\t\t>\n\t\t\t\t\t\t{initial}\n\t\t\t\t\t</span>\n\t\t\t\t)}\n\t\t\t</div>\n\t\t</div>\n\t);\n}\n","import { ImageResponse } from \"next/og\";\nimport type { NextRequest } from \"next/server\";\nimport {\n\tcomputeFacehash,\n\tDEFAULT_COLORS,\n\tgetColor,\n\ttype Variant,\n} from \"../core\";\nimport { FacehashImage } from \"./image\";\n\n// ============================================================================\n// Types\n// ============================================================================\n\nexport type FacehashHandlerOptions = {\n\t/**\n\t * Default image size in pixels.\n\t * Can be overridden via `?size=` query param.\n\t * @default 400\n\t */\n\tsize?: number;\n\n\t/**\n\t * Default background style.\n\t * Can be overridden via `?variant=` query param.\n\t * @default \"gradient\"\n\t */\n\tvariant?: Variant;\n\n\t/**\n\t * Default for showing initial letter.\n\t * Can be overridden via `?showInitial=` query param.\n\t * @default true\n\t */\n\tshowInitial?: boolean;\n\n\t/**\n\t * Default color palette (hex colors).\n\t * Can be overridden via `?colors=` query param (comma-separated).\n\t * @default [\"#ec4899\", \"#f59e0b\", \"#3b82f6\", \"#f97316\", \"#10b981\"]\n\t */\n\tcolors?: string[];\n\n\t/**\n\t * Cache-Control header value.\n\t * Set to `null` to disable caching.\n\t * @default \"public, max-age=31536000, immutable\"\n\t */\n\tcacheControl?: string | null;\n};\n\nexport type FacehashHandler = {\n\tGET: (request: NextRequest) => Promise<ImageResponse>;\n};\n\n// ============================================================================\n// Helper Functions\n// ============================================================================\n\nconst HEX_COLOR_REGEX = /^#[0-9A-Fa-f]{3,8}$/;\n\nfunction parseBoolean(value: string | null, defaultValue: boolean): boolean {\n\tif (value === null) {\n\t\treturn defaultValue;\n\t}\n\treturn value === \"true\" || value === \"1\";\n}\n\nfunction parseNumber(\n\tvalue: string | null,\n\tdefaultValue: number,\n\tmin = 1,\n\tmax = 2000\n): number {\n\tif (value === null) {\n\t\treturn defaultValue;\n\t}\n\tconst num = Number.parseInt(value, 10);\n\tif (Number.isNaN(num)) {\n\t\treturn defaultValue;\n\t}\n\treturn Math.min(Math.max(num, min), max);\n}\n\nfunction parseColors(value: string | null): string[] | undefined {\n\tif (!value) {\n\t\treturn;\n\t}\n\tconst colors = value\n\t\t.split(\",\")\n\t\t.map((c) => c.trim())\n\t\t.filter((c) => HEX_COLOR_REGEX.test(c));\n\treturn colors.length > 0 ? colors : undefined;\n}\n\nfunction parseVariant(value: string | null): Variant | undefined {\n\tif (value === \"gradient\" || value === \"solid\") {\n\t\treturn value;\n\t}\n\treturn;\n}\n\n// ============================================================================\n// Main Export\n// ============================================================================\n\n/**\n * Creates a Next.js route handler for generating Facehash avatar images.\n *\n * @example\n * ```ts\n * // app/api/avatar/route.ts\n * import { toFacehashHandler } from \"facehash/next\";\n *\n * export const { GET } = toFacehashHandler();\n * ```\n *\n * @example\n * ```ts\n * // With custom defaults\n * export const { GET } = toFacehashHandler({\n * size: 200,\n * variant: \"solid\",\n * colors: [\"#ff0000\", \"#00ff00\", \"#0000ff\"],\n * });\n * ```\n *\n * Query parameters:\n * - `name` (required): String to generate avatar from\n * - `size`: Image size in pixels (default: 400)\n * - `variant`: \"gradient\" or \"solid\" (default: \"gradient\")\n * - `showInitial`: \"true\" or \"false\" (default: \"true\")\n * - `colors`: Comma-separated hex colors (e.g., \"#ff0000,#00ff00\")\n */\nexport function toFacehashHandler(\n\toptions: FacehashHandlerOptions = {}\n): FacehashHandler {\n\tconst {\n\t\tsize: defaultSize = 400,\n\t\tvariant: defaultVariant = \"gradient\",\n\t\tshowInitial: defaultShowInitial = true,\n\t\tcolors: defaultColors = [...DEFAULT_COLORS],\n\t\tcacheControl = \"public, max-age=31536000, immutable\",\n\t} = options;\n\n\tasync function GET(request: NextRequest): Promise<ImageResponse> {\n\t\tconst searchParams = request.nextUrl.searchParams;\n\n\t\t// Parse name (required)\n\t\tconst name = searchParams.get(\"name\");\n\t\tif (!name) {\n\t\t\treturn new ImageResponse(\n\t\t\t\t<div\n\t\t\t\t\tstyle={{\n\t\t\t\t\t\twidth: \"100%\",\n\t\t\t\t\t\theight: \"100%\",\n\t\t\t\t\t\tdisplay: \"flex\",\n\t\t\t\t\t\talignItems: \"center\",\n\t\t\t\t\t\tjustifyContent: \"center\",\n\t\t\t\t\t\tbackgroundColor: \"#f3f4f6\",\n\t\t\t\t\t\tcolor: \"#6b7280\",\n\t\t\t\t\t\tfontSize: 24,\n\t\t\t\t\t\tfontFamily: \"sans-serif\",\n\t\t\t\t\t}}\n\t\t\t\t>\n\t\t\t\t\tMissing ?name= parameter\n\t\t\t\t</div>,\n\t\t\t\t{\n\t\t\t\t\twidth: defaultSize,\n\t\t\t\t\theight: defaultSize,\n\t\t\t\t\tstatus: 400,\n\t\t\t\t\theaders: {\n\t\t\t\t\t\t\"Content-Type\": \"image/png\",\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t);\n\t\t}\n\n\t\t// Parse options from query params (override defaults)\n\t\tconst size = parseNumber(searchParams.get(\"size\"), defaultSize, 16, 2000);\n\t\tconst variant = parseVariant(searchParams.get(\"variant\")) ?? defaultVariant;\n\t\tconst showInitial = parseBoolean(\n\t\t\tsearchParams.get(\"showInitial\"),\n\t\t\tdefaultShowInitial\n\t\t);\n\t\tconst colors = parseColors(searchParams.get(\"colors\")) ?? defaultColors;\n\n\t\t// Compute facehash data\n\t\tconst data = computeFacehash({\n\t\t\tname,\n\t\t\tcolorsLength: colors.length,\n\t\t});\n\n\t\t// Get background color\n\t\tconst backgroundColor = getColor(colors, data.colorIndex);\n\n\t\t// Build response headers\n\t\tconst headers: Record<string, string> = {\n\t\t\t\"Content-Type\": \"image/png\",\n\t\t};\n\n\t\tif (cacheControl) {\n\t\t\theaders[\"Cache-Control\"] = cacheControl;\n\t\t}\n\n\t\t// Generate image\n\t\treturn new ImageResponse(\n\t\t\t<FacehashImage\n\t\t\t\tbackgroundColor={backgroundColor}\n\t\t\t\tdata={data}\n\t\t\t\trotation={data.rotation}\n\t\t\t\tshowInitial={showInitial}\n\t\t\t\tsize={size}\n\t\t\t\tvariant={variant}\n\t\t\t/>,\n\t\t\t{\n\t\t\t\twidth: size,\n\t\t\t\theight: size,\n\t\t\t\theaders,\n\t\t\t}\n\t\t);\n\t}\n\n\treturn { GET };\n}\n"],"mappings":";;;;;AAUA,MAAaA,aAAkC;CAC9C;CACA;CACA;CACA;CACA;AAwBD,MAAM,mBAAmB;CACxB;EAAE,GAAG;EAAI,GAAG;EAAG;CACf;EAAE,GAAG;EAAG,GAAG;EAAG;CACd;EAAE,GAAG;EAAG,GAAG;EAAG;CACd;EAAE,GAAG;EAAG,GAAG;EAAG;CACd;EAAE,GAAG;EAAI,GAAG;EAAG;CACf;EAAE,GAAG;EAAG,GAAG;EAAG;CACd;EAAE,GAAG;EAAG,GAAG;EAAI;CACf;EAAE,GAAG;EAAI,GAAG;EAAI;CAChB;EAAE,GAAG;EAAG,GAAG;EAAI;CACf;;;;AAKD,MAAa,iBAAiB;CAC7B;CACA;CACA;CACA;CACA;CACA;;;;;AAUD,SAAgB,gBAAgB,SAA+C;CAC9E,MAAM,EAAE,MAAM,eAAe,eAAe,WAAW;CAEvD,MAAM,OAAO,WAAW,KAAK;CAC7B,MAAM,YAAY,OAAO,WAAW;CACpC,MAAM,aAAa,OAAO;CAE1B,MAAM,WAAW,iBADK,OAAO,iBAAiB,WACM;EAAE,GAAG;EAAG,GAAG;EAAG;AAElE,QAAO;EACN,UAAU,WAAW,cAAc;EACnC;EACA,UAAU;EACV,SAAS,KAAK,OAAO,EAAE,CAAC,aAAa;EACrC;;AAGF,MAAM,iBAAiB;;;;AAKvB,SAAgB,SACf,QACA,OACS;CACT,MAAM,UAAU,UAAU,OAAO,SAAS,IAAI,SAAS;AACvD,QAAO,QAAQ,QAAQ,QAAQ,WAAW;;;;;;;;;AC3F3C,MAAaC,gBAMT;CACH,OAAO;EACN,SAAS;EACT,OAAO,CACN,6IACA,uIACA;EACD;CACD,OAAO;EACN,SAAS;EACT,OAAO,CACN,grCACA,2qCACA;EACD;CACD,MAAM;EACL,SAAS;EACT,OAAO;GACN;GACA;GACA;GACA;GACA;EACD;CACD,QAAQ;EACP,SAAS;EACT,OAAO,CACN,mrBACA,uqBACA;EACD;CACD;;;;;;;;ACrBD,SAAgB,cAAc,EAC7B,MACA,iBACA,MACA,SACA,aACA,YACsB;CACtB,MAAM,EAAE,UAAU,YAAY;CAC9B,MAAM,UAAU,cAAc;CAG9B,MAAM,KAAK,SAAS,YAAY,QAAQ,QAAQ,MAAM,IAAI,CAAC,IAAI,OAAO;CACtE,MAAM,eAAe,WAAW,MAAM,YAAY;CAGlD,MAAM,YAAY,OAAO;CACzB,MAAM,aAAa,YAAY;CAG/B,MAAM,WAAW,OAAO;CAMxB,MAAM,kBAAkB,OAAO;CAC/B,MAAM,UAAU,SAAS,IAAI;CAC7B,MAAM,UAAU,CAAC,SAAS,IAAI;AAE9B,QACC,qBAAC;EACA,OAAO;GACN,OAAO;GACP,QAAQ;GACR,SAAS;GACT,eAAe;GACf,YAAY;GACZ,gBAAgB;GAChB;GACA,UAAU;GACV;aAGA,YAAY,cACZ,oBAAC,SACA,OAAO;GACN,UAAU;GACV,KAAK;GACL,MAAM;GACN,OAAO;GACP,QAAQ;GACR,YACC;GACD,GACA,EAIH,qBAAC;GACA,OAAO;IACN,SAAS;IACT,eAAe;IACf,YAAY;IACZ,gBAAgB;IAEhB,YAAY;IACZ,WAAW;IACX;cAGD,oBAAC;IACA,cAAW;IACX,MAAK;IACL,QAAQ;IACR,MAAK;IACL,SAAS,QAAQ;IACjB,OAAO;IACP,OAAM;cAEL,QAAQ,MAAM,KAAK,GAAG,MACtB,oBAAC;KAAQ;KAAG,MAAK;OAAa,EAAK,CAClC;KACG,EAGL,eACA,oBAAC;IACA,OAAO;KACN,WAAW,OAAO;KAClB;KACA,YAAY;KACZ,YAAY;KACZ,YAAY;KACZ,OAAO;KACP;cAEA;KACK;IAEH;GACD;;;;;AChER,MAAM,kBAAkB;AAExB,SAAS,aAAa,OAAsB,cAAgC;AAC3E,KAAI,UAAU,KACb,QAAO;AAER,QAAO,UAAU,UAAU,UAAU;;AAGtC,SAAS,YACR,OACA,cACA,MAAM,GACN,MAAM,KACG;AACT,KAAI,UAAU,KACb,QAAO;CAER,MAAM,MAAM,OAAO,SAAS,OAAO,GAAG;AACtC,KAAI,OAAO,MAAM,IAAI,CACpB,QAAO;AAER,QAAO,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,EAAE,IAAI;;AAGzC,SAAS,YAAY,OAA4C;AAChE,KAAI,CAAC,MACJ;CAED,MAAM,SAAS,MACb,MAAM,IAAI,CACV,KAAK,MAAM,EAAE,MAAM,CAAC,CACpB,QAAQ,MAAM,gBAAgB,KAAK,EAAE,CAAC;AACxC,QAAO,OAAO,SAAS,IAAI,SAAS;;AAGrC,SAAS,aAAa,OAA2C;AAChE,KAAI,UAAU,cAAc,UAAU,QACrC,QAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCT,SAAgB,kBACf,UAAkC,EAAE,EAClB;CAClB,MAAM,EACL,MAAM,cAAc,KACpB,SAAS,iBAAiB,YAC1B,aAAa,qBAAqB,MAClC,QAAQ,gBAAgB,CAAC,GAAG,eAAe,EAC3C,eAAe,0CACZ;CAEJ,eAAe,IAAI,SAA8C;EAChE,MAAM,eAAe,QAAQ,QAAQ;EAGrC,MAAM,OAAO,aAAa,IAAI,OAAO;AACrC,MAAI,CAAC,KACJ,QAAO,IAAI,cACV,oBAAC;GACA,OAAO;IACN,OAAO;IACP,QAAQ;IACR,SAAS;IACT,YAAY;IACZ,gBAAgB;IAChB,iBAAiB;IACjB,OAAO;IACP,UAAU;IACV,YAAY;IACZ;aACD;IAEK,EACN;GACC,OAAO;GACP,QAAQ;GACR,QAAQ;GACR,SAAS,EACR,gBAAgB,aAChB;GACD,CACD;EAIF,MAAM,OAAO,YAAY,aAAa,IAAI,OAAO,EAAE,aAAa,IAAI,IAAK;EACzE,MAAM,UAAU,aAAa,aAAa,IAAI,UAAU,CAAC,IAAI;EAC7D,MAAM,cAAc,aACnB,aAAa,IAAI,cAAc,EAC/B,mBACA;EACD,MAAM,SAAS,YAAY,aAAa,IAAI,SAAS,CAAC,IAAI;EAG1D,MAAM,OAAO,gBAAgB;GAC5B;GACA,cAAc,OAAO;GACrB,CAAC;EAGF,MAAM,kBAAkB,SAAS,QAAQ,KAAK,WAAW;EAGzD,MAAMC,UAAkC,EACvC,gBAAgB,aAChB;AAED,MAAI,aACH,SAAQ,mBAAmB;AAI5B,SAAO,IAAI,cACV,oBAAC;GACiB;GACX;GACN,UAAU,KAAK;GACF;GACP;GACG;IACR,EACF;GACC,OAAO;GACP,QAAQ;GACR;GACA,CACD;;AAGF,QAAO,EAAE,KAAK"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "facehash",
3
3
  "type": "module",
4
- "version": "0.0.4",
4
+ "version": "0.0.5",
5
5
  "private": false,
6
6
  "author": "Cossistant team",
7
7
  "description": "Deterministic avatar faces from any string. Lightweight, interactive, pure CSS. Works with any framework.",