SLOPSHOPPER

rainbow-friends-th

เพื่อนสายรุ้ง ASCII ที่ออกแบบใหม่ทั้งหมด: แมวสายรุ้ง ยูนิคอร์น ช้างน้อย ปลาทอง วิ่งในแผงเป็นแอนิเมชันสีรุ้ง

newpanecommandtimer
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · rainbow-friends-th
│ ┃ 🌈 Rainbow Friends ✕ › fix the failing auth test and add an audit log call │ ┃ │ ┃ ⏺ Read(src/auth.ts) │ ┃ ✦ ⎿ Read 6 lines │ ┃ ⏺ Update(src/auth.ts) │ ┃ ✦ ⎿ Added 2 lines, removed 1 line │ ┃ ⏺ Bash(bun test) │ ┃ ✦ ⎿ 3 pass, 1 fail │ ┃ │ ┃ ✦ ● Done. refresh now rejects expired claims and logs an audit event. │ ┃ │ ┃ แมวสายรุ้ง ✻ Worked for 42s · done 4:20 PM │ │ › /rainbow │ ⎿ rainbow-friends-th: 🌈 แมวสายรุ้ง มาแล้ว! │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · 🌈 Rainbow Friends
✦ ✦ ✦ ✦ แมวสายรุ้ง
README

Mod 48: Rainbow Friends TH

เพื่อนสายรุ้งวิ่งผ่านแผงพร้อมลากสายรุ้งเป็นคลื่น: ตัวละคร ASCII ที่ออกแบบขึ้นเอง 5 ตัว (แมว ยูนิคอร์น ช้างน้อย ปลาทอง เมฆปุย) เดินขบวนพร้อมกันได้ และข้อความสีรุ้งไล่สีวิ่ง

คำสั่ง

คำสั่งทำอะไร
/rainbow [ตัว]ให้เพื่อนวิ่งผ่าน: cat unicorn elephant fish cloud
/rainbow paradeทุกตัวเดินขบวน
/rainbow text <ข้อความ>ข้อความสีรุ้ง
/rainbow listดูหน้าตาทุกตัว
/rainbow stopหยุด

ติดตั้ง

/plugin install rainbow-friends-th --marketplace tksoft11/claude-code-mods-th

ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)

โค้ดสอนอะไร

เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:

  1. ตัวละคร 2 เฟรม
  2. วาดเลน: สายรุ้งเป็นคลื่น + วางตัวละครทับ และรวมช่องสีเดียวกัน
  3. แผงและ timer ที่หยุดเมื่อปิดแผง
  4. คำสั่ง /rainbow

หมายเหตุ

ต้นฉบับมีตัวการ์ตูนที่มีลิขสิทธิ์ (ชินจัง โดเรมอน พาวเวอร์พัฟ ฯลฯ) ฉบับนี้ใช้ตัวละครที่วาดขึ้นใหม่ทั้งหมด แอนิเมชันหยุดเองหลังประมาณ 5 นาที

Source 1 files
hooks/register.tsx 228 lines
1/**
2 * ============================================================================
3 * MOD 48: Rainbow Friends TH - เพื่อนสายรุ้งวิ่งผ่านจอ (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: มีมแมวบินลากสายรุ้งยุคอินเทอร์เน็ตแรกๆ
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. ตัวละคร ASCII ที่ออกแบบเอง 5 ตัว (แมว ยูนิคอร์น ช้างน้อย ปลาทอง เมฆ)
9 *    วิ่งผ่านแผงพร้อมลากสายรุ้งเป็นคลื่น
10 * 2. /rainbow parade - ทุกตัวเดินขบวนพร้อมกัน
11 * 3. /rainbow text <ข้อความ> - ข้อความสีรุ้งไล่สีวิ่ง
12 *
13 * หมายเหตุ: ต้นฉบับมีตัวการ์ตูนดัง (ชินจัง โดเรมอน พาวเวอร์พัฟ ฯลฯ) ซึ่งมีลิขสิทธิ์
14 * ฉบับนี้ใช้ตัวละครที่วาดขึ้นใหม่ทั้งหมด
15 */
16
17import type { EngineInterface, Register, Timer } from 'claude-code'
18
19interface Sprite {
20  name: string
21  color: string
22  frames: [string[], string[]]
23}
24
25// ==========================================================================
26// บทที่ 1: ตัวละคร - 2 เฟรมต่อตัว สลับกันเหมือนกำลังวิ่ง/ว่าย
27// ==========================================================================
28const SPRITES: Record<string, Sprite> = {
29  cat: {
30    name: 'แมวสายรุ้ง',
31    color: '#bdbdbd',
32    frames: [
33      [' /\\_/\\ ', '( ^.^ )', ' /   \\ ', '(_)-(_)'],
34      [' /\\_/\\ ', '( ^.^ )', ' \\   / ', ' (_)(_)'],
35    ],
36  },
37  unicorn: {
38    name: 'ยูนิคอร์น',
39    color: '#f8bbd0',
40    frames: [
41      ['     \\  ', '  ,,_/o>', ' (    ) ', ' /|  |\\ '],
42      ['     \\  ', '  ,,_/o>', ' (    ) ', '  |\\/|  '],
43    ],
44  },
45  elephant: {
46    name: 'ช้างน้อย',
47    color: '#90a4ae',
48    frames: [
49      ['  ___   ', ' (o  )_ ', ' (    )\\', '  || || '],
50      ['  ___   ', ' (o  )_ ', ' (    ) |', '  |\\ /| '],
51    ],
52  },
53  fish: {
54    name: 'ปลาทอง',
55    color: '#ffa726',
56    frames: [
57      ['   __    ', '><(  o>  ', '   ~~  o '],
58      ['   __    ', '>=(  o>  ', '   ~~ o  '],
59    ],
60  },
61  cloud: {
62    name: 'เมฆปุย',
63    color: '#e3f2fd',
64    frames: [
65      ['  .--.   ', ' (    ). ', '(_______)', '  \' \' \'  '],
66      ['  .--.   ', ' (    ). ', '(_______)', ' \' \' \'   '],
67    ],
68  },
69}
70
71const RAINBOW = ['#e53935', '#fb8c00', '#fdd835', '#43a047', '#1e88e5', '#8e24aa']
72const PANE = 'rainbow-friends-th'
73const MAX_FRAMES = 2000 // ประมาณ 5 นาทีแล้วพัก (ไม่กินเครื่องตอนลืมปิดแผง)
74
75let cast: string[] = ['cat']
76let banner = ''
77let frame = 0
78let ticker: Timer | undefined
79
80interface Cell {
81  text: string
82  color?: string
83}
84
85// ==========================================================================
86// บทที่ 2: วาดหนึ่งเลน - สายรุ้งเป็นคลื่นด้านหลัง แล้ววางตัวละครทับ
87// ==========================================================================
88function lane(sprite: Sprite, offset: number, width: number): Cell[][] {
89  const art = sprite.frames[frame % 4 < 2 ? 0 : 1]
90  const spriteWidth = Math.max(...art.map(l => l.length))
91  const travel = width + spriteWidth
92  const x = ((Math.floor(frame / 2) + offset) % travel) - spriteWidth
93  return art.map((row, y) => {
94    const cells: Cell[] = []
95    for (let col = 0; col < width; col++) {
96      const inSprite = col >= x && col < x + spriteWidth
97      const ch = inSprite ? row[col - x] ?? ' ' : ' '
98      if (inSprite) cells.push(ch === ' ' ? { text: ' ' } : { text: ch, color: sprite.color })
99      else if (col < x && col >= x - 30) {
100        // คลื่น: แต่ละช่วง 3 คอลัมน์ยกขึ้นลงสลับกัน
101        const wave = Math.floor((col + Math.floor(frame / 2)) / 3) % 2
102        const band = Math.floor(((y + wave) * RAINBOW.length) / (art.length + 1))
103        cells.push({ text: '█', color: RAINBOW[band % RAINBOW.length] })
104      } else cells.push({ text: (col * 7 + y * 13 + frame) % 97 === 0 ? '✦' : ' ', color: '#fff59d' })
105    }
106    return cells
107  })
108}
109
110// รวมช่องสีเดียวกันที่ติดกันเป็นก้อนเดียว (Text น้อยลง วาดเร็วขึ้น)
111function merge(cells: Cell[]): Cell[] {
112  const out: Cell[] = []
113  for (const c of cells) {
114    const prev = out.at(-1)
115    if (prev && prev.color === c.color) prev.text += c.text
116    else out.push({ ...c })
117  }
118  return out
119}
120
121function rainbowText(text: string): Cell[] {
122  return [...text].map((ch, i) => ({ text: ch, color: RAINBOW[(i + Math.floor(frame / 2)) % RAINBOW.length] }))
123}
124
125function start($: EngineInterface): void {
126  frame = 0
127  ticker ??= $.clock.every(150, () => {
128    frame++
129    if (frame >= MAX_FRAMES) stop()
130    $.ui.invalidate('ui.render')
131  })
132}
133
134function stop(): void {
135  ticker?.cancel()
136  ticker = undefined
137}
138
139const HELP = `🌈 Rainbow Friends TH (Mod 48)
140========================================
141/rainbow [ตัว]          ให้เพื่อนวิ่งผ่านแผง: ${Object.entries(SPRITES).map(([k, s]) => `${k}=${s.name}`).join(' ')}
142/rainbow parade         ทุกตัวเดินขบวน
143/rainbow text <ข้อความ>  ข้อความสีรุ้ง
144/rainbow list           ดูหน้าตาทุกตัว
145/rainbow stop           หยุด`
146
147export const register: Register = on => {
148  on('session.start', async ($, e, next) => {
149    await $.command.register({ name: 'rainbow', description: 'เพื่อนสายรุ้งวิ่งผ่านจอ แมว ยูนิคอร์น ช้าง ปลาทอง เมฆ (/rainbow help)', argumentHint: '[ตัว]|parade|text <ข้อความ>|list|stop' })
150    return next(e)
151  })
152
153  on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
154    const { Box, Text } = $.ui.resolve(e)
155    const width = Math.max(30, Math.min(80, e.props.bodyColumns - 2))
156    const lanes = cast.map(key => ({ key, sprite: SPRITES[key] })).filter((l): l is { key: string; sprite: Sprite } => !!l.sprite)
157    return (
158      <Box flexDirection="column">
159        {banner && (
160          <Text bold>
161            {merge(rainbowText(banner)).map(c => (
162              <Text color={c.color}>{c.text}</Text>
163            ))}
164          </Text>
165        )}
166        {lanes.map((l, i) => (
167          <Box flexDirection="column" marginTop={1}>
168            {lane(l.sprite, i * 17, width).map(row => (
169              <Text>
170                {merge(row).map(c => (c.color ? <Text color={c.color}>{c.text}</Text> : c.text))}
171              </Text>
172            ))}
173            <Text dimColor>{l.sprite.name}</Text>
174          </Box>
175        ))}
176      </Box>
177    )
178  })
179
180  on('ui.close', async ($, e, next) => {
181    if (e.id === PANE) stop()
182    return next(e)
183  })
184
185  on('command.run', { command: 'rainbow' }, async ($, e) => {
186    const [sub = 'cat', ...rest] = e.args.trim().split(/\s+/).filter(Boolean)
187    const show = async (msg: string) => {
188      await $.ui.open({ id: PANE, title: '🌈 Rainbow Friends' })
189      start($)
190      $.ui.invalidate('ui.render')
191      return { text: msg }
192    }
193    switch (sub) {
194      case 'help':
195        return { text: HELP }
196      case 'stop':
197        stop()
198        await $.ui.close({ id: PANE })
199        return { text: '👋 เพื่อนๆ กลับบ้านแล้ว' }
200      case 'list':
201        return { text: Object.entries(SPRITES).map(([k, s]) => `${s.name} (${k})\n${s.frames[0].join('\n')}`).join('\n\n') }
202      case 'parade':
203        cast = Object.keys(SPRITES)
204        banner = ''
205        return show('🌈 ขบวนพาเหรดสายรุ้งมาแล้ว!')
206      case 'text': {
207        const text = rest.join(' ').slice(0, 60)
208        if (!text) return { text: 'วิธีใช้: /rainbow text สุขสันต์วันเกิด!' }
209        banner = text
210        return show(`🌈 ${text}`)
211      }
212      default:
213        if (!SPRITES[sub]) return { text: `ไม่มีตัว "${sub}"\n${HELP}` }
214        cast = [sub]
215        return show(`🌈 ${SPRITES[sub].name} มาแล้ว!`)
216    }
217  })
218}
219
220/**
221 * ========================================================================
222 * การบ้าน / ต่อยอด
223 * ========================================================================
224 * 1. ให้เพื่อนวิ่งผ่านเองทุกครั้งที่ Claude ทำงานเสร็จ (ดัก turn.complete)
225 * 2. วาดตัวละครของคุณเองแล้วเพิ่มลงใน SPRITES
226 * 3. เปลี่ยนชุดสีตามเทศกาล: สงกรานต์ ลอยกระทง ปีใหม่
227 */
228