สร้าง mockup จริงจากโลโก้ของคุณ: เสื้อ แก้ว ถุงผ้า กล่อง มือถือ แล็ปท็อป เป็นไฟล์ SVG และทำใบสั่งงานส่งร้านสกรีน

สร้าง mockup จริงจากโลโก้ของคุณ (ไฟล์ PNG/JPG/SVG หรือข้อความ) บนสินค้า 8 แบบ: เสื้อยืด แก้วมัค แก้วเก็บความเย็น ถุงผ้า กล่องคราฟท์ ถุงกาแฟ มือถือ แล็ปท็อป ได้เป็นไฟล์ SVG ที่เปิดดูหรือส่งต่อได้ทันที เลือกสีสินค้าได้ รวมทุกแบบในหน้าเดียวได้ และทำใบสั่งงานไว้ส่งร้านสกรีน
| คำสั่ง | ทำอะไร |
|---|---|
/mockup logo <ไฟล์> | ใส่โลโก้ (png jpg svg webp ไม่เกิน 2 MB) |
/mockup text <ข้อความ> | ใช้ข้อความแทนโลโก้ |
/mockup make <สินค้า> [สี] | สร้าง mockup เช่น /mockup make shirt black |
/mockup all | ทุกแบบในหน้าเดียว |
/mockup shop <ชื่อร้าน> [ติดต่อ] | ร้านที่จะส่งงาน |
/mockup price <สินค้า> <ราคา> | ราคาต่อชิ้นที่ร้านแจ้ง |
/mockup order <สินค้า> <จำนวน> [รายละเอียด] | เพิ่มรายการสั่ง |
/mockup order / order export / order clear | ดู / ส่งออก / ล้างใบสั่งงาน |
/mockup reset confirm | ล้างข้อมูลทั้งหมด |
/plugin install mockup-th --marketplace tksoft11/claude-code-mods-th
ต้องตั้ง CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 ก่อนเปิด claude (ดู README หลัก)
เปิด hooks/register.tsx แล้วอ่านตามบทที่เขียนไว้ในคอมเมนต์:
Mod ไม่ได้สั่งซื้อหรือจ่ายเงินให้ร้านไหน ราคาไม่มีค่าเริ่มต้น ใส่เองตามที่ร้านแจ้ง mockup เป็นภาพประกอบเพื่อให้เห็นตำแหน่งโลโก้ ไม่ใช่ภาพถ่ายจริง
hooks/register.tsx 294 lines1/**
2 * ============================================================================
3 * MOD 40: Mockup TH - สร้าง mockup จริงจากโลโก้ของคุณ (ฉบับสอนทำ)
4 * ============================================================================
5 * แรงบันดาลใจจาก: ร้านสกรีนเสื้อ แก้ว ถุงผ้า ที่ต้องส่ง mockup ให้ลูกค้าดูก่อนสั่ง
6 *
7 * หน้าที่ของ Mod นี้:
8 * 1. ใส่โลโก้ (ไฟล์ PNG/JPG/SVG) หรือข้อความ แล้วสร้าง mockup เป็นไฟล์ SVG ที่เปิดดูหรือส่งต่อได้ทันที
9 * 2. สินค้า 8 แบบ: เสื้อยืด, แก้วมัค, แก้วเก็บความเย็น, ถุงผ้า, กล่อง, ถุงกาแฟ, มือถือ, แล็ปท็อป
10 * 3. เลือกสีสินค้าได้, สร้างทุกแบบรวมในหน้าเดียว (contact sheet)
11 * 4. ทำใบสั่งงาน: ตั้งชื่อร้าน ราคาที่ร้านแจ้ง เพิ่มรายการ แล้วส่งออกเป็นไฟล์ไว้ส่งให้ร้าน
12 *
13 * หมายเหตุ: Mod ไม่ได้สั่งซื้อหรือจ่ายเงินให้ร้านไหน ราคาใส่เองตามที่ร้านแจ้ง (ไม่มีราคาเดาให้)
14 */
15
16import type { EngineInterface, Register } from 'claude-code'
17
18interface Logo {
19 kind: 'image' | 'text'
20 data: string // data URI ของรูป หรือข้อความ
21 name: string
22}
23interface OrderLine {
24 product: string
25 qty: number
26 detail: string
27}
28interface Shop {
29 name: string
30 contact: string
31 prices: Record<string, number>
32}
33
34// ==========================================================================
35// บทที่ 1: แบบสินค้า - วาดด้วย SVG path + กำหนดกรอบที่วางโลโก้
36// ==========================================================================
37interface Product {
38 name: string
39 w: number
40 h: number
41 color: string // สีเริ่มต้น
42 shape: (c: string) => string
43 logo: { x: number; y: number; w: number; h: number }
44}
45
46const shade = 'rgba(0,0,0,0.12)'
47const PRODUCTS: Record<string, Product> = {
48 shirt: {
49 name: 'เสื้อยืด', w: 400, h: 420, color: '#ffffff',
50 shape: c => `<path d="M130 40 L95 55 L30 110 L70 165 L105 140 L105 390 L295 390 L295 140 L330 165 L370 110 L305 55 L270 40 Q200 80 130 40 Z" fill="${c}" stroke="${shade}" stroke-width="3"/><path d="M160 46 Q200 75 240 46" fill="none" stroke="${shade}" stroke-width="3"/>`,
51 logo: { x: 150, y: 130, w: 100, h: 100 },
52 },
53 mug: {
54 name: 'แก้วมัค', w: 400, h: 340, color: '#ffffff',
55 shape: c => `<rect x="80" y="60" width="200" height="230" rx="18" fill="${c}" stroke="${shade}" stroke-width="3"/><path d="M280 110 Q350 110 350 175 Q350 240 280 240" fill="none" stroke="${shade}" stroke-width="22"/><ellipse cx="180" cy="62" rx="100" ry="14" fill="${shade}"/>`,
56 logo: { x: 120, y: 120, w: 120, h: 120 },
57 },
58 tumbler: {
59 name: 'แก้วเก็บความเย็น', w: 300, h: 460, color: '#1f2933',
60 shape: c => `<rect x="85" y="40" width="130" height="30" rx="8" fill="#555"/><path d="M80 70 L220 70 L200 420 Q150 435 100 420 Z" fill="${c}" stroke="${shade}" stroke-width="3"/>`,
61 logo: { x: 105, y: 170, w: 90, h: 90 },
62 },
63 tote: {
64 name: 'ถุงผ้า', w: 400, h: 460, color: '#f3ead8',
65 shape: c => `<path d="M140 130 Q140 40 200 40 Q260 40 260 130" fill="none" stroke="#c9b58f" stroke-width="12"/><rect x="70" y="130" width="260" height="300" fill="${c}" stroke="${shade}" stroke-width="3"/>`,
66 logo: { x: 130, y: 210, w: 140, h: 140 },
67 },
68 box: {
69 name: 'กล่องคราฟท์', w: 420, h: 360, color: '#c8a26b',
70 shape: c => `<path d="M70 120 L210 60 L350 120 L210 180 Z" fill="${c}" stroke="${shade}" stroke-width="3"/><path d="M70 120 L70 270 L210 330 L210 180 Z" fill="${c}" stroke="${shade}" stroke-width="3" opacity="0.85"/><path d="M350 120 L350 270 L210 330 L210 180 Z" fill="${c}" stroke="${shade}" stroke-width="3" opacity="0.7"/>`,
71 logo: { x: 160, y: 85, w: 100, h: 70 },
72 },
73 coffee: {
74 name: 'ถุงกาแฟ', w: 320, h: 460, color: '#1b1b1b',
75 shape: c => `<path d="M70 60 L250 60 L265 420 L55 420 Z" fill="${c}" stroke="${shade}" stroke-width="3"/><rect x="70" y="60" width="180" height="26" fill="#333"/><circle cx="160" cy="130" r="10" fill="#444"/>`,
76 logo: { x: 100, y: 190, w: 120, h: 120 },
77 },
78 phone: {
79 name: 'มือถือ', w: 280, h: 540, color: '#111111',
80 shape: c => `<rect x="30" y="20" width="220" height="480" rx="34" fill="${c}"/><rect x="42" y="34" width="196" height="452" rx="24" fill="#ffffff"/><rect x="110" y="44" width="60" height="16" rx="8" fill="${c}"/>`,
81 logo: { x: 80, y: 180, w: 120, h: 120 },
82 },
83 laptop: {
84 name: 'แล็ปท็อป', w: 520, h: 360, color: '#9aa3ad',
85 shape: c => `<rect x="80" y="30" width="360" height="240" rx="14" fill="#222"/><rect x="94" y="44" width="332" height="212" fill="#ffffff"/><path d="M40 280 L480 280 L460 310 L60 310 Z" fill="${c}"/>`,
86 logo: { x: 200, y: 90, w: 120, h: 120 },
87 },
88}
89
90let logo: Logo | undefined
91let order: OrderLine[] = []
92let shop: Shop = { name: '', contact: '', prices: {} }
93let projectRoot = ''
94
95async function save($: EngineInterface): Promise<void> {
96 await $.store.set('mockup', { logo, order, shop })
97}
98
99function esc(s: string): string {
100 return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"')
101}
102
103// ==========================================================================
104// บทที่ 2: ประกอบ SVG - วางโลโก้ในกรอบของสินค้า (รูป = <image>, ข้อความ = <text>)
105// ==========================================================================
106function logoSvg(l: Logo, box: Product['logo'], onDark: boolean): string {
107 if (l.kind === 'image') return `<image href="${l.data}" x="${box.x}" y="${box.y}" width="${box.w}" height="${box.h}" preserveAspectRatio="xMidYMid meet"/>`
108 const size = Math.max(12, Math.min(40, Math.floor((box.w * 1.6) / Math.max(1, [...l.data].length))))
109 return `<text x="${box.x + box.w / 2}" y="${box.y + box.h / 2}" font-size="${size}" font-family="Sarabun, 'Noto Sans Thai', sans-serif" font-weight="700" fill="${onDark ? '#ffffff' : '#111111'}" text-anchor="middle" dominant-baseline="middle">${esc(l.data)}</text>`
110}
111
112function isDark(hex: string): boolean {
113 const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex)
114 if (!m) return false
115 const [r, g, b] = [m[1], m[2], m[3]].map(x => parseInt(x ?? '0', 16))
116 return (r ?? 0) * 0.299 + (g ?? 0) * 0.587 + (b ?? 0) * 0.114 < 128
117}
118
119function productSvg(key: string, color?: string): string {
120 const p = PRODUCTS[key]
121 if (!p || !logo) return ''
122 const c = color ?? p.color
123 return `<svg xmlns="http://www.w3.org/2000/svg" width="${p.w}" height="${p.h}" viewBox="0 0 ${p.w} ${p.h}"><rect width="100%" height="100%" fill="#eef0f3"/>${p.shape(c)}${logoSvg(logo, p.logo, isDark(c) && key !== 'phone' && key !== 'laptop')}</svg>`
124}
125
126// contact sheet: ทุกสินค้าในหน้าเดียว เรียงเป็นตาราง 4 คอลัมน์
127function sheetSvg(): string {
128 const keys = Object.keys(PRODUCTS)
129 const cell = 300
130 const cols = 4
131 const rows = Math.ceil(keys.length / cols)
132 const items = keys
133 .map((k, i) => {
134 const p = PRODUCTS[k]!
135 const scale = Math.min((cell - 20) / p.w, (cell - 50) / p.h)
136 const x = (i % cols) * cell
137 const y = Math.floor(i / cols) * cell
138 const inner = productSvg(k).replace(/^<svg[^>]*>/, '').replace(/<\/svg>$/, '').replace(/<rect width="100%" height="100%" fill="#eef0f3"\/>/, '')
139 return `<g transform="translate(${x + 10},${y + 10}) scale(${scale.toFixed(3)})">${inner}</g><text x="${x + cell / 2}" y="${y + cell - 12}" font-size="16" font-family="sans-serif" text-anchor="middle" fill="#333">${esc(p.name)}</text>`
140 })
141 .join('')
142 return `<svg xmlns="http://www.w3.org/2000/svg" width="${cols * cell}" height="${rows * cell}" viewBox="0 0 ${cols * cell} ${rows * cell}"><rect width="100%" height="100%" fill="#eef0f3"/>${items}</svg>`
143}
144
145// ==========================================================================
146// บทที่ 3: ใบสั่งงาน - ใช้ราคาที่ผู้ใช้ใส่เองเท่านั้น
147// ==========================================================================
148function orderText(now: number): string {
149 const lines = order.map((o, i) => {
150 const price = shop.prices[o.product]
151 return `${i + 1}. ${PRODUCTS[o.product]?.name ?? o.product} × ${o.qty}${o.detail ? ` (${o.detail})` : ''}${price ? ` = ฿${(price * o.qty).toLocaleString('en-US')}` : ' (ยังไม่ใส่ราคา)'}`
152 })
153 const total = order.reduce((s, o) => s + (shop.prices[o.product] ?? 0) * o.qty, 0)
154 const unpriced = order.filter(o => !shop.prices[o.product]).length
155 return [
156 `# ใบสั่งงานสกรีน${shop.name ? ` - ถึง ${shop.name}` : ''}`,
157 `วันที่ ${new Date(now).toLocaleDateString('th-TH')}${shop.contact ? ` · ติดต่อร้าน: ${shop.contact}` : ''}`,
158 `โลโก้: ${logo ? logo.name : '(ยังไม่ได้ใส่)'}`,
159 '',
160 ...lines,
161 '',
162 total ? `รวมโดยประมาณ ฿${total.toLocaleString('en-US')}${unpriced ? ` (ยังไม่รวม ${unpriced} รายการที่ยังไม่มีราคา)` : ''} ตามราคาที่ร้านแจ้ง ยืนยันกับร้านอีกครั้ง` : 'ราคา: รอร้านแจ้ง',
163 'ไฟล์ mockup แนบ: mockup-*.svg',
164 ].join('\n')
165}
166
167const HELP = `🎨 Mockup TH (Mod 40)
168========================================
169ใส่โลโก้:
170 /mockup logo <ไฟล์ .png .jpg .svg> เช่น /mockup logo logo.png
171 /mockup text <ข้อความ> ใช้ข้อความแทนโลโก้
172สร้าง:
173 /mockup make <สินค้า> [สี] สินค้า: ${Object.entries(PRODUCTS).map(([k, p]) => `${k}=${p.name}`).join(' ')}
174 สี: ชื่อสีหรือ hex เช่น black, #ff6b35
175 /mockup all สร้างทุกแบบในหน้าเดียว (mockup-all.svg)
176ใบสั่งงาน:
177 /mockup shop <ชื่อร้าน> [ช่องทางติดต่อ]
178 /mockup price <สินค้า> <ราคาต่อชิ้น> ตามที่ร้านแจ้ง
179 /mockup order <สินค้า> <จำนวน> [รายละเอียด เช่น ไซส์ สี]
180 /mockup order | order clear | order export
181 /mockup reset confirm ล้างข้อมูลทั้งหมด`
182
183export const register: Register = on => {
184 on('session.start', async ($, e, next) => {
185 projectRoot = e.cwd.replace(/[\\/]$/, '')
186 const saved = (await $.store.get('mockup')) as { logo?: Logo; order?: OrderLine[]; shop?: Shop } | undefined
187 logo = saved?.logo
188 order = saved?.order ?? []
189 shop = saved?.shop ?? { name: '', contact: '', prices: {} }
190 await $.command.register({ name: 'mockup', description: 'สร้าง mockup จากโลโก้: เสื้อ แก้ว ถุงผ้า กล่อง มือถือ แล็ปท็อป + ใบสั่งงาน (/mockup help)', argumentHint: 'logo|text|make|all|shop|price|order' })
191 return next(e)
192 })
193
194 // ==========================================================================
195 // บทที่ 4: คำสั่ง /mockup
196 // ==========================================================================
197 on('command.run', { command: 'mockup' }, async ($, e) => {
198 const [sub = 'help', ...rest] = e.args.trim().split(/\s+/).filter(Boolean)
199 const sep = projectRoot.includes('\\') ? '\\' : '/'
200 const now = await $.clock.now()
201 switch (sub) {
202 case 'logo': {
203 const file = rest.join(' ')
204 const ext = /\.(png|jpe?g|svg|webp)$/i.exec(file)?.[1]?.toLowerCase()
205 if (!file || !ext) return { text: 'วิธีใช้: /mockup logo logo.png (รองรับ png jpg svg webp)' }
206 const path = /^([A-Za-z]:|[\\/])/.test(file) ? file : `${projectRoot}${sep}${file}`
207 const st = await $.fs.stat(path).catch(() => undefined)
208 if (!st) return { text: `❌ ไม่พบไฟล์ ${file}` }
209 if (st.size > 2 * 1024 * 1024) return { text: '❌ ไฟล์ใหญ่เกิน 2 MB ย่อรูปก่อน' }
210 const { base64 } = await $.fs.read(path, { as: 'bytes' })
211 const mime = ext === 'svg' ? 'image/svg+xml' : ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : `image/${ext}`
212 logo = { kind: 'image', data: `data:${mime};base64,${base64}`, name: file.split(/[\\/]/).pop() ?? file }
213 await save($)
214 return { text: `🖼️ ใช้โลโก้ ${logo.name} แล้ว ลอง /mockup all` }
215 }
216 case 'text': {
217 const text = rest.join(' ')
218 if (!text) return { text: 'วิธีใช้: /mockup text ร้านกาแฟบ้านฉัน' }
219 logo = { kind: 'text', data: text.slice(0, 40), name: `ข้อความ "${text.slice(0, 40)}"` }
220 await save($)
221 return { text: `🔤 ใช้ข้อความ "${logo.data}" เป็นโลโก้แล้ว ลอง /mockup all` }
222 }
223 case 'make': {
224 if (!logo) return { text: 'ใส่โลโก้ก่อน: /mockup logo logo.png หรือ /mockup text ชื่อร้าน' }
225 const [key = '', color] = rest
226 if (!PRODUCTS[key]) return { text: `สินค้าที่มี: ${Object.keys(PRODUCTS).join(', ')}` }
227 if (color && !/^(#[0-9a-f]{3,8}|[a-z]+)$/i.test(color)) return { text: '❌ สีต้องเป็นชื่อสีภาษาอังกฤษหรือ hex เช่น black, #ff6b35' }
228 const path = `${projectRoot}${sep}mockup-${key}${color ? `-${color.replace('#', '')}` : ''}.svg`
229 await $.fs.write(path, productSvg(key, color))
230 return { text: `✅ สร้าง ${PRODUCTS[key]?.name} แล้ว: ${path}\nเปิดใน browser เพื่อดู หรือส่งไฟล์ให้ลูกค้า/ร้านได้เลย` }
231 }
232 case 'all': {
233 if (!logo) return { text: 'ใส่โลโก้ก่อน: /mockup logo logo.png หรือ /mockup text ชื่อร้าน' }
234 const path = `${projectRoot}${sep}mockup-all.svg`
235 await $.fs.write(path, sheetSvg())
236 return { text: `✅ สร้าง mockup ${Object.keys(PRODUCTS).length} แบบในหน้าเดียว: ${path}` }
237 }
238 case 'shop': {
239 const [name = '', ...contact] = rest
240 if (!name) return { text: shop.name ? `🏪 ${shop.name}${shop.contact ? ` · ${shop.contact}` : ''}` : 'วิธีใช้: /mockup shop ชื่อร้าน [ไลน์/เบอร์]' }
241 shop = { ...shop, name, contact: contact.join(' ') }
242 await save($)
243 return { text: `🏪 ส่งใบสั่งงานถึง ${name}` }
244 }
245 case 'price': {
246 const [key = '', priceText] = rest
247 const price = Number(priceText)
248 if (!PRODUCTS[key] || !(price > 0)) return { text: 'วิธีใช้: /mockup price shirt 220 (ราคาต่อชิ้นที่ร้านแจ้ง)' }
249 shop.prices[key] = price
250 await save($)
251 return { text: `💵 ${PRODUCTS[key]?.name} ชิ้นละ ฿${price}` }
252 }
253 case 'order': {
254 const [first = '', qtyText, ...detail] = rest
255 if (first === 'clear') {
256 order = []
257 await save($)
258 return { text: '🧹 ล้างใบสั่งงานแล้ว' }
259 }
260 if (first === 'export') {
261 const path = `${projectRoot}${sep}order-${new Date(now).toISOString().slice(0, 10)}.md`
262 await $.fs.write(path, orderText(now) + '\n')
263 return { text: `💾 บันทึกใบสั่งงาน: ${path}\nส่งไฟล์นี้พร้อม mockup ให้ร้านได้เลย` }
264 }
265 if (first && PRODUCTS[first]) {
266 const qty = Number(qtyText)
267 if (!Number.isInteger(qty) || qty < 1) return { text: 'วิธีใช้: /mockup order shirt 50 ไซส์ M 30 L 20 สีขาว' }
268 order.push({ product: first, qty, detail: detail.join(' ') })
269 await save($)
270 }
271 return { text: order.length ? orderText(now) : 'ใบสั่งงานว่าง /mockup order shirt 50 ไซส์ M' }
272 }
273 case 'reset':
274 if (rest[0] !== 'confirm') return { text: '⚠️ จะลบโลโก้ ร้าน ราคา และใบสั่งงานทั้งหมด ยืนยันด้วย /mockup reset confirm' }
275 logo = undefined
276 order = []
277 shop = { name: '', contact: '', prices: {} }
278 await save($)
279 return { text: '🧹 ล้างข้อมูลทั้งหมดแล้ว' }
280 default:
281 return { text: HELP }
282 }
283 })
284}
285
286/**
287 * ========================================================================
288 * การบ้าน / ต่อยอด
289 * ========================================================================
290 * 1. แปลง SVG เป็น PNG ด้วย $.process.run เรียกโปรแกรมอย่าง Inkscape หรือ ImageMagick
291 * 2. ส่ง mockup ให้ลูกค้าผ่าน LINE Flex Message (Mod 12) พร้อม QR มัดจำ (Mod 14)
292 * 3. เพิ่มสินค้าใหม่: ใส่ shape และกรอบโลโก้ใน PRODUCTS เช่น หมวก เคสโทรศัพท์
293 */
294