Skill: jiafar/horse-sticker-maker (version 1.0.0)

--- name: horse-sticker-maker description: Create and deploy a festive Chinese New Year (Year of the Horse 2026) animated GIF sticker maker web app. Use when the user wants to generate custom horse-themed blessing stickers, deploy a sticker generator H5 page, or create WeChat-compatible animated GIF stickers with gold horse animation on red background. Supports custom text input, 6-frame gold horse galloping animation, Canvas-based client-side GIF rendering via gif.js, and Vercel deployment. --- # Horse Sticker Maker Generate a web app that creates custom animated GIF stickers for Chinese New Year (Year of the Horse 2026). ## What It Does - Users input custom blessing text (1-8 chars) - Client-side Canvas renders a 240px animated GIF with: - Red gradient background with gold sparkle particles - 6-frame gold horse galloping animation (transparent PNG) - User's custom text in gold calligraphy at top - "立马加薪" bottom text with color-cycling effect - Output is WeChat sticker compatible (≤500KB, 240px) ## Quick Start 1. Copy the template project: ```bash cp -r <skill_dir>/assets/horse-blessing-template ./horse-blessing cd horse-blessing npm install ``` 2. Run locally: ```bash npm run dev # Open http://localhost:3000/sticker ``` 3. Deploy to Vercel: ```bash vercel --prod --yes ``` ## Project Structure ``` horse-blessing/ ├── app/ │ ├── page.tsx # Main page (AI-generated blessing with poem) │ ├── sticker/page.tsx # Sticker maker (Canvas GIF generator) │ ├── api/generate/ # AI poem + image generation API │ ├── api/sticker/ # Sticker API │ └── layout.tsx # Root layout (red theme) ├── public/ │ ├── horse-frame-[1-6].png # 6-frame transparent gold horse │ ├── horse-transparent.png # Static horse fallback │ └── gif.worker.js # gif.js web worker ├── package.json └── tailwind.config.ts ``` ## Key Technical Details ### GIF Generation (Client-Side) - Uses `gif.js` loaded via CDN (`Script` from next/script) - 18 frames (6 horse frames × 3 cycles), 85ms delay per frame - Canvas size: 240×240px for WeChat sticker compatibility - Horse frames loaded as `Image` elements, drawn via `drawImage` ### Horse Frame Assets - 6 transparent PNG frames in `public/horse-frame-[1-6].png` - Generated via AI image model, backgrounds removed with `sharp` - Removal technique: pixels with R>210, G>210, B>210 → alpha=0 ### Customization Points - **Bottom text**: Edit `'立马加薪'` in `sticker/page.tsx` - **GIF size**: Change `const size = 240` (keep ≤240 for WeChat) - **Frame count**: Change `const frames = 18` - **Horse images**: Replace `public/horse-frame-*.png` - **Background**: Modify the radial gradient colors - **Sparkle count**: Adjust sparkle array size (default 30) ### WeChat Sticker Compatibility - Max 500KB file size - Max 240px recommended dimension - GIF format required - Save → WeChat chat → emoji panel → "+" → select from gallery ## Dependencies ```json { "next": "^14.0.0", "react": "^18.2.0", "react-dom": "^18.2.0", "sharp": "latest", "gif-encoder-2": "^1.0.5", "html2canvas-pro": "^1.6.7" } ``` External CDN: `gif.js@0.2.0` (loaded at runtime for client-side GIF encoding)

path: assets/horse-blessing-template/app/api/generate/route.ts
content: import { NextRequest, NextResponse } from 'next/server'

const [REDACTED_SECRET]
const BASE = process.env.GOOGLE_API_BASE || 'https://aiplatform.googleapis.com/v1/publishers/google/models'

async function geminiGenerate(prompt: string): Promise<string> {
  const res = await fetch(`${BASE}/gemini-2.5-flash:generateContent?key=${API_KEY}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      contents: [{ role: 'user', parts: [{ text: prompt }] }],
      generationConfig: { temperature: 0.9, maxOutputTokens: 2048, thinkingConfig: { thinkingBudget: 0 } },
    }),
  })
  const data = await res.json()
  if (data.error) throw new Error(data.error.message || 'Gemini API error')
  // Handle streaming response (array) or single response
  const candidates = Array.isArray(data) ? data[data.length - 1]?.candidates : data.candidates
  return candidates?.[0]?.content?.parts?.map((p: any) => p.text).join('') || ''
}

async function imagenGenerate(prompt: string): Promise<string> {
  const res = await fetch(`${BASE}/imagen-4.0-generate-001:predict?key=${API_KEY}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      instances: [{ prompt }],
      parameters: { sampleCount: 1, aspectRatio: '9:16' },
    }),
  })
  const data = await res.json()
  if (data.error) throw new Error(data.error.message || 'Imagen API error')
  return data.predictions?.[0]?.bytesBase64Encoded || ''
}

export async function POST(req: NextRequest) {
  try {
    const { name } = await req.json()
    if (!name || typeof name !== 'string' || name.trim().length === 0 || name.trim().length > 10) {
      return NextResponse.json({ error: '请输入有效的名字（1-10个字）' }, { status: 400 })
    }

    const trimmed = name.trim()
    const chars = Array.from(trimmed)

    // 1. Generate poem + blessing via Gemini
    // Build acrostic constraint: pad to 4 chars for 七绝 (4 lines)
    const padded = chars.length >= 4 ? chars.slice(0, 4) : [...chars, ...['马', '到', '成', '功'].slice(0, 4 - chars.length)]

    const textPrompt = `你是一位精通格律的诗词大师+创意文案高手。请为"${trimmed}"创作2026马年专属祝福。

【藏头诗·七言绝句】严格要求（违反任何一条都不合格）：
1. 必须恰好四句，不能多不能少
2. 每句恰好7个汉字（不含标点），句末用中文逗号或句号
3. 四句的第一个字依次是：「${padded[0]}」「${padded[1]}」「${padded[2]}」「${padded[3]}」
4. 押韵：第一、二、四句的最后一个字必须押韵（韵母相同或相近）
5. 第三句最后一个字用仄声（不押韵），形成"AABA"韵律
6. 内容要有画面感：骏马奔腾、春风破晓、星辰大海等意象，避免空洞说教
7. 禁止使用以下俗套词组：宏图大展、前程似锦、鹏程万里、蒸蒸日上

自检清单（生成后逐条验证）：
- [ ] 共四句？每句数汉字=7？
- [ ] 第1/2/3/4句首字分别是${padded.join('/')}？
- [ ] 第1、2、4句尾字押韵？第3句尾字不押韵？

【创意祝福语】要求：
- 40字以内，必须含"${trimmed}"
- 风格：像好朋友发的微信，不是官方贺卡
- 可以幽默、可以温暖、可以谐音梗，要真诚有个性
- 参考风格："2026了，${trimmed}骑上快马冲就完了！你的好运配速：每秒一个小目标 🏇"

【运势标签】要求：
- 8字以内，必须含"马"字
- 要有趣味和记忆点，像社交媒体爆款标签
- 好的例子：一马平川躺赢版、桃花快马加鞭来、财运万马奔腾中
- 坏的例子：马到成功（太俗）、万事如意（没有马）

严格按JSON返回，不要用markdown代码块包裹：
{"poem":"四句七绝用\\n分隔","blessing":"创意祝福语","fortune":"运势标签"}`

    const textRaw = await geminiGenerate(textPrompt)
    
    // Extract JSON from response (handle potential markdown wrapping)
    const jsonMatch = textRaw.match(/\{[\s\S]*\}/)
    if (!jsonMatch) throw new Error('文本生成格式错误')
    const textResult = JSON.parse(jsonMatch[0])

    // 2. Generate horse image via Imagen
    const imagePrompt = `A magnificent golden horse galloping through auspicious red clouds, Chinese New Year celebration style, traditional Chinese art fusion with modern illustration, gold and red color scheme, festive lanterns and fireworks in background, lucky symbols, elegant and joyful atmosphere, high quality digital art`

    const imageBase64 = await imagenGenerate(imagePrompt)

    return NextResponse.json({
      poem: textResult.poem,
      blessing: textResult.blessing,
      fortune: textResult.fortune || '马到成功',
      image: imageBase64 ? `data:image/png;base64,${imageBase64}` : null,
    })
  } catch (e: any) {
    console.error('Generate error:', e)
    return NextResponse.json({ error: '生成失败，请重试: ' + e.message }, { status: 500 })
  }
}


path: assets/horse-blessing-template/app/api/sticker/route.ts
content: import { NextRequest, NextResponse } from 'next/server'

const GEMINI_API_KEY = process.env.GEMINI_API_KEY!
const GEMINI_MODEL = 'gemini-3-pro-image-preview'

const STYLE_PROMPTS: Record<string, string> = {
  rich: '金色骏马被金币和红包环绕，富贵发财主题',
  epic: '威武霸气的骏马昂首嘶鸣，火焰鬃毛，英雄史诗风格',
  cute: '超萌Q版小马驹，大眼睛水汪汪，卡哇伊粉嫩风格',
  lucky: '骏马踏祥云，周围环绕吉祥如意符号，传统中国祝福风格',
  party: '欢乐的马在庆祝，周围有烟花彩带，派对狂欢风格',
  zen: '一匹马悠闲躺着冥想，旁边有香炉青烟，佛系搞笑风格',
}

async function geminiGenerateImage(prompt: string): Promise<string> {
  const url = `https://generativelanguage.googleapis.com/v1beta/models/${GEMINI_MODEL}:generateContent?key=${GEMINI_API_KEY}`
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      contents: [{ parts: [{ text: prompt }] }],
      generationConfig: {
        responseModalities: ['TEXT', 'IMAGE'],
      },
    }),
  })
  const data = await res.json()
  if (data.error) throw new Error(data.error.message || 'Gemini API error')

  const parts = data.candidates?.[0]?.content?.parts || []
  for (const part of parts) {
    if (part.inlineData) {
      return `data:${part.inlineData.mimeType};base64,${part.inlineData.data}`
    }
  }
  throw new Error('未生成图片')
}

export async function POST(req: NextRequest) {
  try {
    const { text, styleId } = await req.json()
    if (!text || typeof text !== 'string' || text.trim().length === 0 || text.trim().length > 8) {
      return NextResponse.json({ error: '请输入1-8个字的表情文字' }, { status: 400 })
    }

    const trimmed = text.trim()
    const styleDesc = STYLE_PROMPTS[styleId] || STYLE_PROMPTS.rich

    const prompt = `生成一张正方形微信表情包图片：

- 纯红色喜庆背景，有金色光效和闪烁粒子（单一背景，不要图中图）
- 画面中心是一匹金色骏马，${styleDesc}
- 马头上戴着一个金色发箍，发箍正面清晰写着"${trimmed}"（金色醒目大字，必须在发箍上）
- 马的周围有飞舞旋转的金币和红包（要有动感，像在旋转飞舞的感觉）
- 画面底部用大号醒目金色艺术字写"2026，财源滚滚来"（字要大、要醒目、要有质感）
- 整体风格：喜庆、炫酷、大气，正方形构图

重要：发箍上只写"${trimmed}"，底部大字"2026，财源滚滚来"要非常醒目。金币红包要有旋转飞舞的动感效果。`

    const imageDataUrl = await geminiGenerateImage(prompt)

    return NextResponse.json({ image: imageDataUrl })
  } catch (e: any) {
    console.error('Sticker generate error:', e)
    return NextResponse.json({ error: '生成失败: ' + e.message }, { status: 500 })
  }
}


path: assets/horse-blessing-template/app/globals.css
content: @tailwind base;
@tailwind components;
@tailwind utilities;

body {
  background: linear-gradient(135deg, #8B0000 0%, #DC143C 30%, #FFD700 100%);
  min-height: 100vh;
  font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
}

@keyframes gallop {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-12px); }
}

.animate-gallop {
  animation: gallop 0.6s ease-in-out infinite;
}

.blessing-card {
  background: linear-gradient(180deg, #FFF8E7 0%, #FFFDF5 100%);
  border: 3px solid #D4AF37;
  box-shadow: 0 8px 32px rgba(139, 0, 0, 0.3);
}

.poem-text {
  font-family: 'STKaiti', 'KaiTi', serif;
  line-height: 2;
}


path: assets/horse-blessing-template/app/layout.tsx
content: import './globals.css'
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: '马上有福 🐴 AI专属马年祝福',
  description: '输入你的名字，AI为你写一首专属马年祝福',
}

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="zh-CN">
      <body className="min-h-screen">{children}</body>
    </html>
  )
}


path: assets/horse-blessing-template/app/page.tsx
content: 'use client'
import { useState, useRef, useCallback } from 'react'
import html2canvas from 'html2canvas-pro'

type Result = { poem: string; blessing: string; fortune: string; image: string | null }

export default function Home() {
  const [name, setName] = useState('')
  const [loading, setLoading] = useState(false)
  const [result, setResult] = useState<Result | null>(null)
  const [savedImage, setSavedImage] = useState<string | null>(null)
  const [saving, setSaving] = useState(false)
  const cardRef = useRef<HTMLDivElement>(null)

  const saveAsImage = useCallback(async () => {
    if (!cardRef.current || saving) return
    setSaving(true)
    try {
      const canvas = await html2canvas(cardRef.current, {
        scale: 3,
        useCORS: true,
        backgroundColor: '#7f1d1d',
        logging: false,
      })
      const dataUrl = canvas.toDataURL('image/png')
      setSavedImage(dataUrl)
    } catch (e) {
      alert('图片生成失败，请长按截图保存')
    } finally {
      setSaving(false)
    }
  }, [saving])

  async function generate() {
    if (!name.trim() || loading) return
    setLoading(true)
    setResult(null)
    try {
      const res = await fetch('/api/generate', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ name: name.trim() }),
      })
      const data = await res.json()
      if (data.error) throw new Error(data.error)
      setResult(data)
    } catch (e: any) {
      alert('生成失败: ' + e.message)
    } finally {
      setLoading(false)
    }
  }

  if (loading) {
    return (
      <div className="min-h-screen flex flex-col items-center justify-center px-6">
        <div className="text-7xl animate-bounce mb-6">🐴</div>
        <p className="text-white text-xl font-medium">
          AI 正在为 <span className="text-yellow-300 font-bold">{name}</span> 专属创作...
        </p>
        <div className="flex gap-2 mt-4">
          <span className="text-white/60 text-sm">✍️ 写藏头诗</span>
          <span className="text-white/40">→</span>
          <span className="text-white/60 text-sm">🎨 画骏马</span>
          <span className="text-white/40">→</span>
          <span className="text-white/60 text-sm">🎁 生成祝福卡</span>
        </div>
      </div>
    )
  }

  if (result) {
    return (
      <div className="min-h-screen flex flex-col items-center justify-center px-4 py-8">
        {/* 祝福卡 */}
        <div ref={cardRef} className="blessing-card rounded-2xl overflow-hidden max-w-sm w-full mx-auto shadow-2xl">
          {/* AI 生成的马图 */}
          {result.image && (
            <div className="relative">
              <img src={result.image} alt="AI生成骏马图" className="w-full h-48 object-cover" />
              <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-red-900/80 to-transparent h-16" />
            </div>
          )}
          
          <div className="p-6 text-center bg-gradient-to-b from-red-800 to-red-900">
            {/* 运势标签 */}
            <div className="inline-block bg-yellow-400 text-red-900 text-xs font-bold px-3 py-1 rounded-full mb-3">
              🐴 {result.fortune}
            </div>
            
            <h2 className="text-yellow-300 text-2xl font-bold mb-4">{name} 的马年祝福</h2>
            
            {/* 藏头诗 */}
            <div className="bg-red-950/50 rounded-xl p-5 mb-4 border border-yellow-400/20">
              {result.poem.split('\n').map((line, i) => (
                <p key={i} className="text-yellow-100 text-lg leading-relaxed font-serif">
                  <span className="text-yellow-400 font-bold">{line[0]}</span>
                  {line.slice(1)}
                </p>
              ))}
            </div>
            
            {/* 祝福语 */}
            <p className="text-yellow-100/90 text-sm leading-relaxed mb-4">{result.blessing}</p>
            
            <div className="border-t border-yellow-400/20 pt-3 mt-3">
              <p className="text-xs text-yellow-200/50">🐴 马上有福 · AI 专属祝福 · 2026马年</p>
            </div>
          </div>
        </div>

        {/* 生成的图片预览 */}
        {savedImage && (
          <div className="mt-4 max-w-sm w-full mx-auto">
            <p className="text-yellow-300 text-sm text-center mb-2">✅ 图片已生成！长按下方图片保存</p>
            <img src={savedImage} alt="祝福卡" className="w-full rounded-xl shadow-lg" />
          </div>
        )}

        <div className="flex gap-3 mt-4">
          {!savedImage ? (
            <button
              onClick={saveAsImage}
              disabled={saving}
              className="bg-yellow-500 hover:bg-yellow-400 disabled:opacity-50 text-red-900 font-bold px-6 py-3 rounded-full shadow-lg transition active:scale-95"
            >
              {saving ? '⏳ 生成中...' : '📸 生成图片保存'}
            </button>
          ) : (
            <a
              href={savedImage}
              download={`${name}-马年祝福.png`}
              className="bg-green-500 hover:bg-green-400 text-white font-bold px-6 py-3 rounded-full shadow-lg transition active:scale-95"
            >
              💾 下载图片
            </a>
          )}
          
          <button
            onClick={() => { setResult(null); setName(''); setSavedImage(null) }}
            className="bg-white/20 hover:bg-white/30 text-white px-6 py-3 rounded-full backdrop-blur transition"
          >
            🐴 再来一个
          </button>
        </div>
      </div>
    )
  }

  return (
    <div className="min-h-screen flex flex-col items-center justify-center px-6">
      <div className="text-center mb-8">
        <div className="text-8xl mb-4">🐴</div>
        <h1 className="text-white text-4xl font-bold mb-2">马上有福</h1>
        <p className="text-white/80 text-lg">AI 为你写一首专属马年藏头诗</p>
        <p className="text-yellow-300/70 text-sm mt-1">每个人的祝福都独一无二 ✨</p>
      </div>

      <div className="w-full max-w-xs">
        <input
          type="text"
          value={name}
          onChange={(e) => setName(e.target.value)}
          onKeyDown={(e) => e.key === 'Enter' && generate()}
          placeholder="输入你的名字"
          maxLength={10}
          className="w-full px-5 py-4 rounded-2xl text-center text-xl bg-white/95 text-gray-800 placeholder-gray-400 outline-none shadow-lg focus:ring-2 focus:ring-yellow-400"
        />
        
        <button
          onClick={generate}
          disabled={!name.trim()}
          className="w-full mt-4 bg-yellow-500 hover:bg-yellow-400 disabled:opacity-50 text-red-900 font-bold text-lg px-6 py-4 rounded-2xl shadow-lg transition active:scale-95"
        >
          ✨ 生成我的马年祝福
        </button>
      </div>

      <a href="/sticker" className="text-yellow-300/70 text-sm mt-6 hover:text-yellow-300 transition">
        🤪 去做马年表情包 →
      </a>
      <p className="text-white/40 text-xs mt-4">AI 藏头诗 + AI 骏马图 · 千人千面</p>
    </div>
  )
}


path: assets/horse-blessing-template/app/sticker/page.tsx
content: 'use client'
import { useState, useCallback } from 'react'
import Script from 'next/script'

type StickerResult = { image: string; text: string }

function createAnimatedSticker(
  horseFrames: HTMLImageElement[],
  stickerText: string,
  onComplete: (gifDataUrl: string) => void
) {
  const size = 240  // WeChat sticker max recommended size
  const frames = 18  // 6 horse frames × 3 cycles
  const canvas = document.createElement('canvas')
  canvas.width = size
  canvas.height = size
  const ctx = canvas.getContext('2d')!

  // @ts-ignore
  const gif = new GIF({
    workers: 2,
    quality: 8,
    width: size,
    height: size,
    workerScript: '/gif.worker.js',
  })

  // Pre-calculate sparkle positions
  const sparkles = Array.from({ length: 30 }, () => ({
    x: Math.random() * size,
    y: Math.random() * size,
    r: Math.random() * 2 + 0.5,
    phase: Math.random() * Math.PI * 2,
  }))

  for (let f = 0; f < frames; f++) {
    const t = f / frames

    // === RED GRADIENT BACKGROUND ===
    const grad = ctx.createRadialGradient(size * 0.4, size * 0.3, 30, size / 2, size / 2, size * 0.8)
    grad.addColorStop(0, '#FF1A1A')
    grad.addColorStop(0.5, '#CC0000')
    grad.addColorStop(1, '#8B0000')
    ctx.fillStyle = grad
    ctx.fillRect(0, 0, size, size)

    // === GOLD SPARKLES ===
    for (const sp of sparkles) {
      const alpha = 0.15 + 0.85 * Math.abs(Math.sin(t * Math.PI * 2 + sp.phase))
      ctx.beginPath()
      ctx.arc(sp.x, sp.y, sp.r * (0.8 + 0.4 * alpha), 0, Math.PI * 2)
      ctx.fillStyle = `rgba(255, 215, 0, ${alpha})`
      ctx.fill()
    }

    // === TOP: USER INPUT TEXT — big gold calligraphy ===
    const mainFontSize = stickerText.length <= 2 ? 48 : stickerText.length <= 4 ? 38 : 28
    ctx.save()
    ctx.shadowColor = 'rgba(255, 180, 0, 0.8)'
    ctx.shadowBlur = 12 + 6 * Math.sin(t * Math.PI * 2)
    ctx.font = `bold ${mainFontSize}px "PingFang SC", "Microsoft YaHei", "STHeiti", sans-serif`
    ctx.textAlign = 'center'
    ctx.textBaseline = 'middle'
    ctx.strokeStyle = '#8B4513'
    ctx.lineWidth = 3
    ctx.lineJoin = 'round'
    ctx.strokeText(stickerText, size / 2, 30)
    const textGrad = ctx.createLinearGradient(size * 0.2, 10, size * 0.8, 50)
    textGrad.addColorStop(0, '#FFD700')
    textGrad.addColorStop(0.4, '#FFF8DC')
    textGrad.addColorStop(0.7, '#FFA500')
    textGrad.addColorStop(1, '#FFD700')
    ctx.fillStyle = textGrad
    ctx.fillText(stickerText, size / 2, 30)
    ctx.restore()

    // === CENTER: BIG RUNNING HORSE — 6-frame real animation ===
    const horseW = 160
    const horseH = 160
    const frameIdx = f % horseFrames.length
    const currentHorse = horseFrames[frameIdx]
    const gallopBounce = Math.sin(t * Math.PI * 4) * 3
    const horseX = (size - horseW) / 2
    const horseY = 50 + gallopBounce
    const tilt = Math.sin(t * Math.PI * 4) * 0.02

    ctx.save()
    ctx.shadowColor = 'rgba(255, 180, 0, 0.6)'
    ctx.shadowBlur = 18
    ctx.translate(horseX + horseW / 2, horseY + horseH / 2)
    ctx.rotate(tilt)
    ctx.drawImage(currentHorse, -horseW / 2, -horseH / 2, horseW, horseH)
    ctx.restore()

    // === BOTTOM: "立马加薪" — centered, big, gold ===
    ctx.save()
    ctx.shadowColor = 'rgba(255, 180, 0, 0.7)'
    ctx.shadowBlur = 10 + 5 * Math.sin(t * Math.PI * 2)
    ctx.font = 'bold 28px "PingFang SC", "Microsoft YaHei", sans-serif'
    ctx.textAlign = 'center'
    ctx.textBaseline = 'middle'
    ctx.strokeStyle = '#5C1010'
    ctx.lineWidth = 3
    ctx.lineJoin = 'round'
    ctx.strokeText('立马加薪', size / 2, size - 18)
    const goldColors = ['#FFD700', '#FFA500', '#FFEC8B', '#FFD700', '#FF8C00', '#FFD700']
    ctx.fillStyle = goldColors[f % goldColors.length]
    ctx.fillText('立马加薪', size / 2, size - 18)
    ctx.restore()

    gif.addFrame(ctx, { copy: true, delay: 85 })
  }

  gif.on('finished', (blob: Blob) => {
    const reader = new FileReader()
    reader.onload = () => onComplete(reader.result as string)
    reader.readAsDataURL(blob)
  })

  gif.render()
}

export default function StickerPage() {
  const [text, setText] = useState('')
  const [loading, setLoading] = useState(false)
  const [results, setResults] = useState<StickerResult[]>([])
  const [gifReady, setGifReady] = useState(false)

  async function generate() {
    if (loading || !gifReady) return
    const trimmed = text.trim()
    if (!trimmed || trimmed.length > 8) {
      alert('请输入1-8个字')
      return
    }
    setLoading(true)

    // Load 6 horse frames
    const frameCount = 6
    const frames: HTMLImageElement[] = []
    let loaded = 0
    let errored = false
    for (let i = 1; i <= frameCount; i++) {
      const img = new Image()
      img.crossOrigin = 'anonymous'
      img.onload = () => {
        loaded++
        if (loaded === frameCount && !errored) {
          createAnimatedSticker(frames, trimmed, (gifDataUrl) => {
            setResults(prev => [{ image: gifDataUrl, text: trimmed }, ...prev])
            setLoading(false)
          })
        }
      }
      img.onerror = () => {
        if (!errored) { errored = true; alert('素材加载失败'); setLoading(false) }
      }
      img.src = `/horse-frame-${i}.png`
      frames.push(img)
    }
  }

  const saveSticker = useCallback((dataUrl: string, stickerText: string) => {
    const link = document.createElement('a')
    link.download = `马年祝福-${stickerText}.gif`
    link.href = dataUrl
    link.click()
  }, [])

  if (loading) {
    return (
      <div className="min-h-screen flex flex-col items-center justify-center px-6">
        <div className="text-7xl animate-bounce mb-6">🐴</div>
        <p className="text-white text-xl font-medium">✨ 动态表情包生成中...</p>
        <p className="text-white/60 text-sm mt-2">合成 GIF 动画，稍等几秒</p>
      </div>
    )
  }

  return (
    <>
      <Script
        src="https://cdn.jsdelivr.net/npm/gif.js@0.2.0/dist/gif.js"
        onLoad={() => setGifReady(true)}
      />
      <div className="min-h-screen px-4 py-8">
        <div className="max-w-md mx-auto">
          <div className="text-center mb-6">
            <div className="text-6xl mb-2">🐴🔥</div>
            <h1 className="text-white text-3xl font-bold">马年表情包工厂</h1>
            <p className="text-white/70 text-sm mt-1">输入祝福语 → 秒出动态 GIF 表情包</p>
          </div>

          <div className="mb-4">
            <label className="text-yellow-300 text-sm font-medium mb-1 block">祝福文字（1-8字）</label>
            <input
              type="text"
              value={text}
              onChange={(e) => setText(e.target.value)}
              onKeyDown={(e) => e.key === 'Enter' && generate()}
              placeholder="新年快乐、恭喜发财、马到成功..."
              maxLength={8}
              className="w-full px-4 py-3 rounded-xl text-center text-lg bg-white/95 text-gray-800 placeholder-gray-400 outline-none focus:ring-2 focus:ring-yellow-400"
            />
          </div>

          <button
            onClick={generate}
            disabled={!text.trim() || !gifReady}
            className="w-full bg-yellow-500 hover:bg-yellow-400 disabled:opacity-50 text-red-900 font-bold text-lg px-6 py-4 rounded-2xl shadow-lg transition active:scale-95 mb-6"
          >
            {gifReady ? '✨ 生成动态表情包' : '⏳ 加载中...'}
          </button>

          {results.length > 0 && (
            <div>
              <h2 className="text-yellow-300 font-bold mb-3">📦 我的表情包</h2>
              <div className="grid grid-cols-2 gap-3">
                {results.map((r, i) => (
                  <div key={i} className="bg-white/10 rounded-xl p-2 text-center">
                    <img src={r.image} alt={r.text} className="w-full rounded-lg mb-2" />
                    <button
                      onClick={() => saveSticker(r.image, r.text)}
                      className="text-xs bg-green-500 hover:bg-green-400 text-white px-3 py-1 rounded-full"
                    >
                      💾 下载GIF
                    </button>
                  </div>
                ))}
              </div>
              <p className="text-white/50 text-xs text-center mt-3">💡 保存后添加到微信自定义表情即可使用</p>
            </div>
          )}

          <div className="text-center mt-6">
            <a href="/" className="text-white/50 text-sm hover:text-white/80 transition">← 回到藏头诗祝福</a>
          </div>
        </div>
      </div>
    </>
  )
}


path: assets/horse-blessing-template/next-env.d.ts
content: /// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.


path: assets/horse-blessing-template/next.config.js
content: /** @type {import('next').NextConfig} */
const nextConfig = {}
module.exports = nextConfig


path: assets/horse-blessing-template/package.json
content: {
  "name": "horse-blessing",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start"
  },
  "dependencies": {
    "gif-encoder-2": "^1.0.5",
    "html2canvas-pro": "^1.6.7",
    "next": "^14.0.0",
    "png-js": "^1.0.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "sharp": "^0.34.5"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "@types/react": "^18.2.0",
    "autoprefixer": "^10.4.0",
    "postcss": "^8.4.0",
    "tailwindcss": "^3.4.0",
    "typescript": "^5.0.0"
  }
}


path: assets/horse-blessing-template/postcss.config.js
content: module.exports = { plugins: { tailwindcss: {}, autoprefixer: {} } }


path: assets/horse-blessing-template/public/gif.worker.js
content: // gif.worker.js 0.2.0 - https://github.com/jnordberg/gif.js
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){var NeuQuant=require("./TypedNeuQuant.js");var LZWEncoder=require("./LZWEncoder.js");function ByteArray(){this.page=-1;this.pages=[];this.newPage()}ByteArray.pageSize=4096;ByteArray.charMap={};for(var i=0;i<256;i++)ByteArray.charMap[i]=String.fromCharCode(i);ByteArray.prototype.newPage=function(){this.pages[++this.page]=new Uint8Array(ByteArray.pageSize);this.cursor=0};ByteArray.prototype.getData=function(){var rv="";for(var p=0;p<this.pages.length;p++){for(var i=0;i<ByteArray.pageSize;i++){rv+=ByteArray.charMap[this.pages[p][i]]}}return rv};ByteArray.prototype.writeByte=function(val){if(this.cursor>=ByteArray.pageSize)this.newPage();this.pages[this.page][this.cursor++]=val};ByteArray.prototype.writeUTFBytes=function(string){for(var l=string.length,i=0;i<l;i++)this.writeByte(string.charCodeAt(i))};ByteArray.prototype.writeBytes=function(array,offset,length){for(var l=length||array.length,i=offset||0;i<l;i++)this.writeByte(array[i])};function GIFEncoder(width,height){this.width=~~width;this.height=~~height;this.transparent=null;this.transIndex=0;this.repeat=-1;this.delay=0;this.image=null;this.pixels=null;this.indexedPixels=null;this.colorDepth=null;this.colorTab=null;this.neuQuant=null;this.usedEntry=new Array;this.palSize=7;this.dispose=-1;this.firstFrame=true;this.sample=10;this.dither=false;this.globalPalette=false;this.out=new ByteArray}GIFEncoder.prototype.setDelay=function(milliseconds){this.delay=Math.round(milliseconds/10)};GIFEncoder.prototype.setFrameRate=function(fps){this.delay=Math.round(100/fps)};GIFEncoder.prototype.setDispose=function(disposalCode){if(disposalCode>=0)this.dispose=disposalCode};GIFEncoder.prototype.setRepeat=function(repeat){this.repeat=repeat};GIFEncoder.prototype.setTransparent=function(color){this.transparent=color};GIFEncoder.prototype.addFrame=function(imageData){this.image=imageData;this.colorTab=this.globalPalette&&this.globalPalette.slice?this.globalPalette:null;this.getImagePixels();this.analyzePixels();if(this.globalPalette===true)this.globalPalette=this.colorTab;if(this.firstFrame){this.writeLSD();this.writePalette();if(this.repeat>=0){this.writeNetscapeExt()}}this.writeGraphicCtrlExt();this.writeImageDesc();if(!this.firstFrame&&!this.globalPalette)this.writePalette();this.writePixels();this.firstFrame=false};GIFEncoder.prototype.finish=function(){this.out.writeByte(59)};GIFEncoder.prototype.setQuality=function(quality){if(quality<1)quality=1;this.sample=quality};GIFEncoder.prototype.setDither=function(dither){if(dither===true)dither="FloydSteinberg";this.dither=dither};GIFEncoder.prototype.setGlobalPalette=function(palette){this.globalPalette=palette};GIFEncoder.prototype.getGlobalPalette=function(){return this.globalPalette&&this.globalPalette.slice&&this.globalPalette.slice(0)||this.globalPalette};GIFEncoder.prototype.writeHeader=function(){this.out.writeUTFBytes("GIF89a")};GIFEncoder.prototype.analyzePixels=function(){if(!this.colorTab){this.neuQuant=new NeuQuant(this.pixels,this.sample);this.neuQuant.buildColormap();this.colorTab=this.neuQuant.getColormap()}if(this.dither){this.ditherPixels(this.dither.replace("-serpentine",""),this.dither.match(/-serpentine/)!==null)}else{this.indexPixels()}this.pixels=null;this.colorDepth=8;this.palSize=7;if(this.transparent!==null){this.transIndex=this.findClosest(this.transparent,true)}};GIFEncoder.prototype.indexPixels=function(imgq){var nPix=this.pixels.length/3;this.indexedPixels=new Uint8Array(nPix);var k=0;for(var j=0;j<nPix;j++){var index=this.findClosestRGB(this.pixels[k++]&255,this.pixels[k++]&255,this.pixels[k++]&255);this.usedEntry[index]=true;this.indexedPixels[j]=index}};GIFEncoder.prototype.ditherPixels=function(kernel,serpentine){var kernels={FalseFloydSteinberg:[[3/8,1,0],[3/8,0,1],[2/8,1,1]],FloydSteinberg:[[7/16,1,0],[3/16,-1,1],[5/16,0,1],[1/16,1,1]],Stucki:[[8/42,1,0],[4/42,2,0],[2/42,-2,1],[4/42,-1,1],[8/42,0,1],[4/42,1,1],[2/42,2,1],[1/42,-2,2],[2/42,-1,2],[4/42,0,2],[2/42,1,2],[1/42,2,2]],Atkinson:[[1/8,1,0],[1/8,2,0],[1/8,-1,1],[1/8,0,1],[1/8,1,1],[1/8,0,2]]};if(!kernel||!kernels[kernel]){throw"Unknown dithering kernel: "+kernel}var ds=kernels[kernel];var index=0,height=this.height,width=this.width,data=this.pixels;var direction=serpentine?-1:1;this.indexedPixels=new Uint8Array(this.pixels.length/3);for(var y=0;y<height;y++){if(serpentine)direction=direction*-1;for(var x=direction==1?0:width-1,xend=direction==1?width:0;x!==xend;x+=direction){index=y*width+x;var idx=index*3;var r1=data[idx];var g1=data[idx+1];var b1=data[idx+2];idx=this.findClosestRGB(r1,g1,b1);this.usedEntry[idx]=true;this.indexedPixels[index]=idx;idx*=3;var r2=this.colorTab[idx];var g2=this.colorTab[idx+1];var b2=this.colorTab[idx+2];var er=r1-r2;var eg=g1-g2;var eb=b1-b2;for(var i=direction==1?0:ds.length-1,end=direction==1?ds.length:0;i!==end;i+=direction){var x1=ds[i][1];var y1=ds[i][2];if(x1+x>=0&&x1+x<width&&y1+y>=0&&y1+y<height){var d=ds[i][0];idx=index+x1+y1*width;idx*=3;data[idx]=Math.max(0,Math.min(255,data[idx]+er*d));data[idx+1]=Math.max(0,Math.min(255,data[idx+1]+eg*d));data[idx+2]=Math.max(0,Math.min(255,data[idx+2]+eb*d))}}}}};GIFEncoder.prototype.findClosest=function(c,used){return this.findClosestRGB((c&16711680)>>16,(c&65280)>>8,c&255,used)};GIFEncoder.prototype.findClosestRGB=function(r,g,b,used){if(this.colorTab===null)return-1;if(this.neuQuant&&!used){return this.neuQuant.lookupRGB(r,g,b)}var c=b|g<<8|r<<16;var minpos=0;var dmin=256*256*256;var len=this.colorTab.length;for(var i=0,index=0;i<len;index++){var dr=r-(this.colorTab[i++]&255);var dg=g-(this.colorTab[i++]&255);var db=b-(this.colorTab[i++]&255);var d=dr*dr+dg*dg+db*db;if((!used||this.usedEntry[index])&&d<dmin){dmin=d;minpos=index}}return minpos};GIFEncoder.prototype.getImagePixels=function(){var w=this.width;var h=this.height;this.pixels=new Uint8Array(w*h*3);var data=this.image;var srcPos=0;var count=0;for(var i=0;i<h;i++){for(var j=0;j<w;j++){this.pixels[count++]=data[srcPos++];this.pixels[count++]=data[srcPos++];this.pixels[count++]=data[srcPos++];srcPos++}}};GIFEncoder.prototype.writeGraphicCtrlExt=function(){this.out.writeByte(33);this.out.writeByte(249);this.out.writeByte(4);var transp,disp;if(this.transparent===null){transp=0;disp=0}else{transp=1;disp=2}if(this.dispose>=0){disp=dispose&7}disp<<=2;this.out.writeByte(0|disp|0|transp);this.writeShort(this.delay);this.out.writeByte(this.transIndex);this.out.writeByte(0)};GIFEncoder.prototype.writeImageDesc=function(){this.out.writeByte(44);this.writeShort(0);this.writeShort(0);this.writeShort(this.width);this.writeShort(this.height);if(this.firstFrame||this.globalPalette){this.out.writeByte(0)}else{this.out.writeByte(128|0|0|0|this.palSize)}};GIFEncoder.prototype.writeLSD=function(){this.writeShort(this.width);this.writeShort(this.height);this.out.writeByte(128|112|0|this.palSize);this.out.writeByte(0);this.out.writeByte(0)};GIFEncoder.prototype.writeNetscapeExt=function(){this.out.writeByte(33);this.out.writeByte(255);this.out.writeByte(11);this.out.writeUTFBytes("NETSCAPE2.0");this.out.writeByte(3);this.out.writeByte(1);this.writeShort(this.repeat);this.out.writeByte(0)};GIFEncoder.prototype.writePalette=function(){this.out.writeBytes(this.colorTab);var n=3*256-this.colorTab.length;for(var i=0;i<n;i++)this.out.writeByte(0)};GIFEncoder.prototype.writeShort=function(pValue){this.out.writeByte(pValue&255);this.out.writeByte(pValue>>8&255)};GIFEncoder.prototype.writePixels=function(){var enc=new LZWEncoder(this.width,this.height,this.indexedPixels,this.colorDepth);enc.encode(this.out)};GIFEncoder.prototype.stream=function(){return this.out};module.exports=GIFEncoder},{"./LZWEncoder.js":2,"./TypedNeuQuant.js":3}],2:[function(require,module,exports){var EOF=-1;var BITS=12;var HSIZE=5003;var masks=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function LZWEncoder(width,height,pixels,colorDepth){var initCodeSize=Math.max(2,colorDepth);var accum=new Uint8Array(256);var htab=new Int32Array(HSIZE);var codetab=new Int32Array(HSIZE);var cur_accum,cur_bits=0;var a_count;var free_ent=0;var maxcode;var clear_flg=false;var g_init_bits,ClearCode,EOFCode;function char_out(c,outs){accum[a_count++]=c;if(a_count>=254)flush_char(outs)}function cl_block(outs){cl_hash(HSIZE);free_ent=ClearCode+2;clear_flg=true;output(ClearCode,outs)}function cl_hash(hsize){for(var i=0;i<hsize;++i)htab[i]=-1}function compress(init_bits,outs){var fcode,c,i,ent,disp,hsize_reg,hshift;g_init_bits=init_bits;clear_flg=false;n_bits=g_init_bits;maxcode=MAXCODE(n_bits);ClearCode=1<<init_bits-1;EOFCode=ClearCode+1;free_ent=ClearCode+2;a_count=0;ent=nextPixel();hshift=0;for(fcode=HSIZE;fcode<65536;fcode*=2)++hshift;hshift=8-hshift;hsize_reg=HSIZE;cl_hash(hsize_reg);output(ClearCode,outs);outer_loop:while((c=nextPixel())!=EOF){fcode=(c<<BITS)+ent;i=c<<hshift^ent;if(htab[i]===fcode){ent=codetab[i];continue}else if(htab[i]>=0){disp=hsize_reg-i;if(i===0)disp=1;do{if((i-=disp)<0)i+=hsize_reg;if(htab[i]===fcode){ent=codetab[i];continue outer_loop}}while(htab[i]>=0)}output(ent,outs);ent=c;if(free_ent<1<<BITS){codetab[i]=free_ent++;htab[i]=fcode}else{cl_block(outs)}}output(ent,outs);output(EOFCode,outs)}function encode(outs){outs.writeByte(initCodeSize);remaining=width*height;curPixel=0;compress(initCodeSize+1,outs);outs.writeByte(0)}function flush_char(outs){if(a_count>0){outs.writeByte(a_count);outs.writeBytes(accum,0,a_count);a_count=0}}function MAXCODE(n_bits){return(1<<n_bits)-1}function nextPixel(){if(remaining===0)return EOF;--remaining;var pix=pixels[curPixel++];return pix&255}function output(code,outs){cur_accum&=masks[cur_bits];if(cur_bits>0)cur_accum|=code<<cur_bits;else cur_accum=code;cur_bits+=n_bits;while(cur_bits>=8){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}if(free_ent>maxcode||clear_flg){if(clear_flg){maxcode=MAXCODE(n_bits=g_init_bits);clear_flg=false}else{++n_bits;if(n_bits==BITS)maxcode=1<<BITS;else maxcode=MAXCODE(n_bits)}}if(code==EOFCode){while(cur_bits>0){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}flush_char(outs)}}this.encode=encode}module.exports=LZWEncoder},{}],3:[function(require,module,exports){var ncycles=100;var netsize=256;var maxnetpos=netsize-1;var netbiasshift=4;var intbiasshift=16;var intbias=1<<intbiasshift;var gammashift=10;var gamma=1<<gammashift;var betashift=10;var beta=intbias>>betashift;var betagamma=intbias<<gammashift-betashift;var initrad=netsize>>3;var radiusbiasshift=6;var radiusbias=1<<radiusbiasshift;var initradius=initrad*radiusbias;var radiusdec=30;var alphabiasshift=10;var initalpha=1<<alphabiasshift;var alphadec;var radbiasshift=8;var radbias=1<<radbiasshift;var alpharadbshift=alphabiasshift+radbiasshift;var alpharadbias=1<<alpharadbshift;var prime1=499;var prime2=491;var prime3=487;var prime4=503;var minpicturebytes=3*prime4;function NeuQuant(pixels,samplefac){var network;var netindex;var bias;var freq;var radpower;function init(){network=[];netindex=new Int32Array(256);bias=new Int32Array(netsize);freq=new Int32Array(netsize);radpower=new Int32Array(netsize>>3);var i,v;for(i=0;i<netsize;i++){v=(i<<netbiasshift+8)/netsize;network[i]=new Float64Array([v,v,v,0]);freq[i]=intbias/netsize;bias[i]=0}}function unbiasnet(){for(var i=0;i<netsize;i++){network[i][0]>>=netbiasshift;network[i][1]>>=netbiasshift;network[i][2]>>=netbiasshift;network[i][3]=i}}function altersingle(alpha,i,b,g,r){network[i][0]-=alpha*(network[i][0]-b)/initalpha;network[i][1]-=alpha*(network[i][1]-g)/initalpha;network[i][2]-=alpha*(network[i][2]-r)/initalpha}function alterneigh(radius,i,b,g,r){var lo=Math.abs(i-radius);var hi=Math.min(i+radius,netsize);var j=i+1;var k=i-1;var m=1;var p,a;while(j<hi||k>lo){a=radpower[m++];if(j<hi){p=network[j++];p[0]-=a*(p[0]-b)/alpharadbias;p[1]-=a*(p[1]-g)/alpharadbias;p[2]-=a*(p[2]-r)/alpharadbias}if(k>lo){p=network[k--];p[0]-=a*(p[0]-b)/alpharadbias;p[1]-=a*(p[1]-g)/alpharadbias;p[2]-=a*(p[2]-r)/alpharadbias}}}function contest(b,g,r){var bestd=~(1<<31);var bestbiasd=bestd;var bestpos=-1;var bestbiaspos=bestpos;var i,n,dist,biasdist,betafreq;for(i=0;i<netsize;i++){n=network[i];dist=Math.abs(n[0]-b)+Math.abs(n[1]-g)+Math.abs(n[2]-r);if(dist<bestd){bestd=dist;bestpos=i}biasdist=dist-(bias[i]>>intbiasshift-netbiasshift);if(biasdist<bestbiasd){bestbiasd=biasdist;bestbiaspos=i}betafreq=freq[i]>>betashift;freq[i]-=betafreq;bias[i]+=betafreq<<gammashift}freq[bestpos]+=beta;bias[bestpos]-=betagamma;return bestbiaspos}function inxbuild(){var i,j,p,q,smallpos,smallval,previouscol=0,startpos=0;for(i=0;i<netsize;i++){p=network[i];smallpos=i;smallval=p[1];for(j=i+1;j<netsize;j++){q=network[j];if(q[1]<smallval){smallpos=j;smallval=q[1]}}q=network[smallpos];if(i!=smallpos){j=q[0];q[0]=p[0];p[0]=j;j=q[1];q[1]=p[1];p[1]=j;j=q[2];q[2]=p[2];p[2]=j;j=q[3];q[3]=p[3];p[3]=j}if(smallval!=previouscol){netindex[previouscol]=startpos+i>>1;for(j=previouscol+1;j<smallval;j++)netindex[j]=i;previouscol=smallval;startpos=i}}netindex[previouscol]=startpos+maxnetpos>>1;for(j=previouscol+1;j<256;j++)netindex[j]=maxnetpos}function inxsearch(b,g,r){var a,p,dist;var bestd=1e3;var best=-1;var i=netindex[g];var j=i-1;while(i<netsize||j>=0){if(i<netsize){p=network[i];dist=p[1]-g;if(dist>=bestd)i=netsize;else{i++;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist<bestd){a=p[2]-r;if(a<0)a=-a;dist+=a;if(dist<bestd){bestd=dist;best=p[3]}}}}if(j>=0){p=network[j];dist=g-p[1];if(dist>=bestd)j=-1;else{j--;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist<bestd){a=p[2]-r;if(a<0)a=-a;dist+=a;if(dist<bestd){bestd=dist;best=p[3]}}}}}return best}function learn(){var i;var lengthcount=pixels.length;var alphadec=30+(samplefac-1)/3;var samplepixels=lengthcount/(3*samplefac);var delta=~~(samplepixels/ncycles);var alpha=initalpha;var radius=initradius;var rad=radius>>radiusbiasshift;if(rad<=1)rad=0;for(i=0;i<rad;i++)radpower[i]=alpha*((rad*rad-i*i)*radbias/(rad*rad));var step;if(lengthcount<minpicturebytes){samplefac=1;step=3}else if(lengthcount%prime1!==0){step=3*prime1}else if(lengthcount%prime2!==0){step=3*prime2}else if(lengthcount%prime3!==0){step=3*prime3}else{step=3*prime4}var b,g,r,j;var pix=0;i=0;while(i<samplepixels){b=(pixels[pix]&255)<<netbiasshift;g=(pixels[pix+1]&255)<<netbiasshift;r=(pixels[pix+2]&255)<<netbiasshift;j=contest(b,g,r);altersingle(alpha,j,b,g,r);if(rad!==0)alterneigh(rad,j,b,g,r);pix+=step;if(pix>=lengthcount)pix-=lengthcount;i++;if(delta===0)delta=1;if(i%delta===0){alpha-=alpha/alphadec;radius-=radius/radiusdec;rad=radius>>radiusbiasshift;if(rad<=1)rad=0;for(j=0;j<rad;j++)radpower[j]=alpha*((rad*rad-j*j)*radbias/(rad*rad))}}}function buildColormap(){init();learn();unbiasnet();inxbuild()}this.buildColormap=buildColormap;function getColormap(){var map=[];var index=[];for(var i=0;i<netsize;i++)index[network[i][3]]=i;var k=0;for(var l=0;l<netsize;l++){var j=index[l];map[k++]=network[j][0];map[k++]=network[j][1];map[k++]=network[j][2]}return map}this.getColormap=getColormap;this.lookupRGB=inxsearch}module.exports=NeuQuant},{}],4:[function(require,module,exports){var GIFEncoder,renderFrame;GIFEncoder=require("./GIFEncoder.js");renderFrame=function(frame){var encoder,page,stream,transfer;encoder=new GIFEncoder(frame.width,frame.height);if(frame.index===0){encoder.writeHeader()}else{encoder.firstFrame=false}encoder.setTransparent(frame.transparent);encoder.setRepeat(frame.repeat);encoder.setDelay(frame.delay);encoder.setQuality(frame.quality);encoder.setDither(frame.dither);encoder.setGlobalPalette(frame.globalPalette);encoder.addFrame(frame.data);if(frame.last){encoder.finish()}if(frame.globalPalette===true){frame.globalPalette=encoder.getGlobalPalette()}stream=encoder.stream();frame.data=stream.pages;frame.cursor=stream.cursor;frame.pageSize=stream.constructor.pageSize;if(frame.canTransfer){transfer=function(){var i,len,ref,results;ref=frame.data;results=[];for(i=0,len=ref.length;i<len;i++){page=ref[i];results.push(page.buffer)}return results}();return self.postMessage(frame,transfer)}else{return self.postMessage(frame)}};self.onmessage=function(event){return renderFrame(event.data)}},{"./GIFEncoder.js":1}]},{},[4]);
//# sourceMappingURL=gif.worker.js.map


path: assets/horse-blessing-template/tailwind.config.ts
content: import type { Config } from 'tailwindcss'
const config: Config = {
  content: ['./app/**/*.{js,ts,jsx,tsx}'],
  theme: { extend: {} },
  plugins: [],
}
export default config


path: assets/horse-blessing-template/tsconfig.json
content: {
  "compilerOptions": {
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": [
        "./*"
      ]
    }
  },
  "include": [
    "next-env.d.ts",
    "**/*.ts",
    "**/*.tsx",
    ".next/types/**/*.ts"
  ],
  "exclude": [
    "node_modules"
  ]
}


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: suspicious | SkillSpector: suspicious (HIGH)