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

เพื่อนสายรุ้งวิ่งผ่านแผงพร้อมลากสายรุ้งเป็นคลื่น: ตัวละคร 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 แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
ต้นฉบับมีตัวการ์ตูนที่มีลิขสิทธิ์ (ชินจัง โดเรมอน พาวเวอร์พัฟ ฯลฯ) ฉบับนี้ใช้ตัวละครที่วาดขึ้นใหม่ทั้งหมด แอนิเมชันหยุดเองหลังประมาณ 5 นาที
hooks/register.tsx 228 lines1/**
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