first commit

This commit is contained in:
2026-05-25 19:07:12 +07:00
commit f29c67bff4
62 changed files with 13196 additions and 0 deletions
File diff suppressed because one or more lines are too long
+333
View File
@@ -0,0 +1,333 @@
export const STRIP_BASE_SIZE = { width: 600, height: 1800 }
export const DEFAULT_STRIP_THEME_ID = 'neon-night'
const STACKED_SLOTS = [
{ x: 80, y: 160, width: 440, height: 290, radius: 24 },
{ x: 80, y: 615, width: 440, height: 290, radius: 24 },
{ x: 80, y: 1070, width: 440, height: 290, radius: 24 },
]
function svgToDataUri(svg) {
const cleaned = svg.replace(/\s+/g, ' ').trim()
// encodeURIComponent leaves characters like () and ' unescaped, which can break CSS url(...) parsing.
const encoded = encodeURIComponent(cleaned)
.replace(/\(/g, '%28')
.replace(/\)/g, '%29')
.replace(/'/g, '%27')
return `data:image/svg+xml,${encoded}`
}
function slotGuidesSvg({ stroke = 'rgba(255,255,255,0.18)', dash = '10 10' } = {}) {
return STACKED_SLOTS.map((s, i) => {
const innerX = s.x + 8
const innerY = s.y + 8
const innerW = s.width - 16
const innerH = s.height - 16
const labelY = innerY + innerH / 2 + 14
return `
<rect x="${innerX}" y="${innerY}" width="${innerW}" height="${innerH}" rx="${Math.max(
(s.radius || 0) - 4,
0,
)}" fill="rgba(255,255,255,0.04)" stroke="${stroke}" stroke-width="2" stroke-dasharray="${dash}" />
<text x="${innerX + innerW / 2}" y="${labelY}" text-anchor="middle" font-family="ui-sans-serif, system-ui" font-size="28" fill="${stroke}">PHOTO ${
i + 1
}</text>
`
}).join('')
}
function ticketClipPath(w, h, cornerR = 22, notchR = 28) {
const mid = h / 2
return [
`M${cornerR} 0`,
`H${w - cornerR}`,
`Q${w} 0 ${w} ${cornerR}`,
`V${mid - notchR}`,
`C${w - notchR} ${mid - notchR} ${w - notchR} ${mid + notchR} ${w} ${mid + notchR}`,
`V${h - cornerR}`,
`Q${w} ${h} ${w - cornerR} ${h}`,
`H${cornerR}`,
`Q0 ${h} 0 ${h - cornerR}`,
`V${mid + notchR}`,
`C${notchR} ${mid + notchR} ${notchR} ${mid - notchR} 0 ${mid - notchR}`,
`V${cornerR}`,
`Q0 0 ${cornerR} 0`,
'Z',
].join(' ')
}
const TICKET_SLOTS = STACKED_SLOTS.map((s) => ({
...s,
radius: 22,
clipPath: ticketClipPath(s.width - 16, s.height - 16, 22, 28),
}))
const NEON_NIGHT = svgToDataUri(`
<svg width="600" height="1800" viewBox="0 0 600 1800" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#0a0320"/>
<stop offset="1" stop-color="#1a0b3d"/>
</linearGradient>
<radialGradient id="glow" cx="0.5" cy="0.2" r="0.8">
<stop offset="0" stop-color="#ff4fd8" stop-opacity="0.35"/>
<stop offset="1" stop-color="#ff4fd8" stop-opacity="0"/>
</radialGradient>
<pattern id="stars" width="120" height="120" patternUnits="userSpaceOnUse">
<circle cx="18" cy="20" r="2" fill="#ffffff" opacity="0.55"/>
<circle cx="88" cy="28" r="1.5" fill="#a7b7ff" opacity="0.55"/>
<circle cx="64" cy="82" r="1.2" fill="#ffffff" opacity="0.35"/>
<circle cx="102" cy="96" r="1.8" fill="#7ff8ff" opacity="0.45"/>
</pattern>
</defs>
<rect width="600" height="1800" fill="url(#bg)"/>
<rect width="600" height="1800" fill="url(#glow)"/>
<rect width="600" height="1800" fill="url(#stars)" opacity="0.5"/>
<path d="M-40 360 C120 260 260 440 420 340 C520 280 620 320 700 260" fill="none" stroke="#7ff8ff" stroke-width="10" opacity="0.35"/>
<path d="M-60 520 C140 420 240 610 420 510 C540 444 640 520 720 440" fill="none" stroke="#ff4fd8" stroke-width="12" opacity="0.22"/>
<text x="300" y="120" text-anchor="middle" font-family="ui-sans-serif, system-ui" font-size="34" fill="#ffffff" opacity="0.85" letter-spacing="6">AFTERDARK</text>
<text x="300" y="160" text-anchor="middle" font-family="ui-sans-serif, system-ui" font-size="16" fill="#c9c2ff" opacity="0.9" letter-spacing="4">LUMI PHOTO STRIP</text>
${slotGuidesSvg({ stroke: 'rgba(255,255,255,0.22)' })}
<rect x="90" y="1670" width="420" height="70" rx="24" fill="rgba(255,255,255,0.06)" stroke="rgba(255,255,255,0.2)"/>
<text x="300" y="1716" text-anchor="middle" font-family="ui-monospace, SFMono-Regular" font-size="18" fill="#ffffff" opacity="0.65">SAY CHEESE • 3 SHOTS</text>
</svg>
`)
const SAKURA_WAVE = svgToDataUri(`
<svg width="600" height="1800" viewBox="0 0 600 1800" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#fff3f8"/>
<stop offset="0.55" stop-color="#f7f1ff"/>
<stop offset="1" stop-color="#e7f7ff"/>
</linearGradient>
<pattern id="dots" width="26" height="26" patternUnits="userSpaceOnUse">
<circle cx="4" cy="4" r="2" fill="#ff7aa2" opacity="0.25"/>
</pattern>
</defs>
<rect width="600" height="1800" fill="url(#bg)"/>
<rect width="600" height="1800" fill="url(#dots)" opacity="0.6"/>
<path d="M0 240 C120 320 240 150 360 230 C470 304 540 260 600 230 L600 0 L0 0 Z" fill="#ffb3cc" opacity="0.55"/>
<path d="M0 300 C140 380 260 220 380 300 C480 365 560 330 600 300" fill="none" stroke="#ff7aa2" stroke-width="10" opacity="0.35"/>
<path d="M0 1600 C120 1520 220 1700 340 1610 C460 1520 530 1620 600 1550 L600 1800 L0 1800 Z" fill="#bfe7ff" opacity="0.55"/>
<path d="M40 1540 C160 1460 250 1610 360 1520 C470 1450 540 1500 560 1485" fill="none" stroke="#7ab7ff" stroke-width="10" opacity="0.35"/>
<text x="300" y="140" text-anchor="middle" font-family="ui-sans-serif, system-ui" font-size="30" fill="#7c3b54" opacity="0.9" letter-spacing="4">SAKURA WAVE</text>
${slotGuidesSvg({ stroke: 'rgba(124,59,84,0.28)', dash: '8 8' })}
<g opacity="0.55">
<path d="M86 330 C96 312 122 312 132 330 C122 348 96 348 86 330 Z" fill="#ff7aa2"/>
<path d="M132 330 C150 320 160 340 146 352 C134 352 126 340 132 330 Z" fill="#ff9bb8"/>
<path d="M86 330 C70 320 62 342 76 352 C88 352 98 340 86 330 Z" fill="#ff9bb8"/>
</g>
</svg>
`)
const RETRO_FILM = svgToDataUri(`
<svg width="600" height="1800" viewBox="0 0 600 1800" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="paper" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#fff8ea"/>
<stop offset="1" stop-color="#fff0d6"/>
</linearGradient>
<pattern id="grain" width="90" height="90" patternUnits="userSpaceOnUse">
<circle cx="16" cy="20" r="1.8" fill="#000" opacity="0.06"/>
<circle cx="52" cy="46" r="1.3" fill="#000" opacity="0.05"/>
<circle cx="78" cy="18" r="1.2" fill="#000" opacity="0.04"/>
<circle cx="32" cy="76" r="1.6" fill="#000" opacity="0.045"/>
</pattern>
</defs>
<rect width="600" height="1800" fill="url(#paper)"/>
<rect width="600" height="1800" fill="url(#grain)"/>
<!-- film rails -->
<rect x="34" y="0" width="80" height="1800" rx="24" fill="#1b1b1b" opacity="0.92"/>
<rect x="486" y="0" width="80" height="1800" rx="24" fill="#1b1b1b" opacity="0.92"/>
<!-- sprockets -->
<g fill="#fff" opacity="0.75">
${Array.from({ length: 18 })
.map((_, i) => {
const y = 70 + i * 96
return `<rect x="58" y="${y}" width="32" height="54" rx="10"/><rect x="510" y="${y}" width="32" height="54" rx="10"/>`
})
.join('')}
</g>
<text x="300" y="128" text-anchor="middle" font-family="ui-monospace, SFMono-Regular" font-size="22" fill="#1b1b1b" opacity="0.75" letter-spacing="4">RETRO FILM</text>
<text x="300" y="160" text-anchor="middle" font-family="ui-monospace, SFMono-Regular" font-size="14" fill="#1b1b1b" opacity="0.55" letter-spacing="3">FRAME 03 • TAKE 01</text>
${slotGuidesSvg({ stroke: 'rgba(27,27,27,0.22)', dash: '12 8' })}
<path d="M120 1705 H480" stroke="#1b1b1b" stroke-width="3" opacity="0.25"/>
<text x="300" y="1740" text-anchor="middle" font-family="ui-monospace, SFMono-Regular" font-size="14" fill="#1b1b1b" opacity="0.6">LUMI LAB • DEVELOPED DIGITALLY</text>
</svg>
`)
const COMIC_POP = svgToDataUri(`
<svg width="600" height="1800" viewBox="0 0 600 1800" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stop-color="#fff3a7"/>
<stop offset="1" stop-color="#ffd1e8"/>
</linearGradient>
<pattern id="halftone" width="18" height="18" patternUnits="userSpaceOnUse">
<circle cx="4" cy="4" r="3" fill="#000" opacity="0.07"/>
</pattern>
</defs>
<rect width="600" height="1800" fill="url(#bg)"/>
<rect width="600" height="1800" fill="url(#halftone)"/>
<path d="M0 220 L600 80" stroke="#111" stroke-width="8" opacity="0.14"/>
<path d="M0 360 L600 220" stroke="#111" stroke-width="8" opacity="0.12"/>
<path d="M0 500 L600 360" stroke="#111" stroke-width="8" opacity="0.1"/>
<g>
<path d="M90 90 H390 Q420 90 432 112 L470 176 Q482 198 462 208 H140 Q112 208 104 184 L78 120 Q70 98 90 90 Z" fill="#ffffff" opacity="0.9" stroke="#111" stroke-width="6"/>
<text x="270" y="160" text-anchor="middle" font-family="ui-sans-serif, system-ui" font-size="34" fill="#111" letter-spacing="3">SNAP!</text>
</g>
${slotGuidesSvg({ stroke: 'rgba(17,17,17,0.2)', dash: '6 10' })}
<text x="300" y="1740" text-anchor="middle" font-family="ui-sans-serif, system-ui" font-size="18" fill="#111" opacity="0.6" letter-spacing="3">THREE PANEL STORY</text>
</svg>
`)
const KRAFT_MINIMAL = svgToDataUri(`
<svg width="600" height="1800" viewBox="0 0 600 1800" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="kraft" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#d6b892"/>
<stop offset="1" stop-color="#caa87a"/>
</linearGradient>
<pattern id="fibers" width="80" height="80" patternUnits="userSpaceOnUse">
<path d="M6 16 C24 8 32 28 52 18" stroke="#000" stroke-width="2" opacity="0.05" fill="none"/>
<path d="M10 52 C22 44 38 66 62 56" stroke="#000" stroke-width="2" opacity="0.05" fill="none"/>
<circle cx="62" cy="18" r="2" fill="#000" opacity="0.05"/>
</pattern>
</defs>
<rect width="600" height="1800" fill="url(#kraft)"/>
<rect width="600" height="1800" fill="url(#fibers)"/>
<text x="300" y="130" text-anchor="middle" font-family="ui-sans-serif, system-ui" font-size="28" fill="#3a2a1d" opacity="0.8" letter-spacing="6">KRAFT</text>
<text x="300" y="164" text-anchor="middle" font-family="ui-sans-serif, system-ui" font-size="14" fill="#3a2a1d" opacity="0.55" letter-spacing="4">MINIMAL STRIP</text>
${slotGuidesSvg({ stroke: 'rgba(58,42,29,0.22)', dash: '14 10' })}
<rect x="86" y="1540" width="428" height="190" rx="26" fill="rgba(255,255,255,0.35)" stroke="rgba(58,42,29,0.18)" stroke-width="3"/>
<text x="110" y="1600" font-family="ui-monospace, SFMono-Regular" font-size="14" fill="#3a2a1d" opacity="0.65">NOTES:</text>
<path d="M110 1630 H490" stroke="#3a2a1d" opacity="0.22" stroke-width="3"/>
<path d="M110 1668 H490" stroke="#3a2a1d" opacity="0.18" stroke-width="3"/>
<path d="M110 1706 H490" stroke="#3a2a1d" opacity="0.14" stroke-width="3"/>
</svg>
`)
const CINEMA_TICKET = svgToDataUri(`
<svg width="600" height="1800" viewBox="0 0 600 1800" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#111018"/>
<stop offset="1" stop-color="#22122a"/>
</linearGradient>
<pattern id="specks" width="70" height="70" patternUnits="userSpaceOnUse">
<circle cx="10" cy="18" r="1.4" fill="#fff" opacity="0.10"/>
<circle cx="44" cy="40" r="1.8" fill="#fff" opacity="0.08"/>
<circle cx="60" cy="16" r="1.2" fill="#fff" opacity="0.07"/>
<circle cx="22" cy="58" r="1.5" fill="#fff" opacity="0.06"/>
</pattern>
</defs>
<rect width="600" height="1800" fill="url(#bg)"/>
<rect width="600" height="1800" fill="url(#specks)"/>
<text x="300" y="128" text-anchor="middle" font-family="ui-monospace, SFMono-Regular" font-size="24" fill="#f7d26a" opacity="0.9" letter-spacing="6">CINEMA TICKET</text>
<text x="300" y="162" text-anchor="middle" font-family="ui-monospace, SFMono-Regular" font-size="12" fill="#ffffff" opacity="0.55" letter-spacing="5">ADMIT ONE • 3 PHOTOS</text>
${slotGuidesSvg({ stroke: 'rgba(247,210,106,0.28)', dash: '10 12' })}
<!-- perforation vibe -->
<g opacity="0.35" fill="#f7d26a">
${Array.from({ length: 30 })
.map((_, i) => {
const y = 220 + i * 48
return `<circle cx="34" cy="${y}" r="5"/><circle cx="566" cy="${y}" r="5"/>`
})
.join('')}
</g>
<rect x="110" y="1670" width="380" height="86" rx="24" fill="rgba(255,255,255,0.06)" stroke="rgba(247,210,106,0.25)" stroke-width="2"/>
<text x="300" y="1722" text-anchor="middle" font-family="ui-monospace, SFMono-Regular" font-size="16" fill="#ffffff" opacity="0.72">SEAT: A-03 • SHOW: 19:30</text>
</svg>
`)
export const STRIP_THEMES = [
{
id: 'neon-night',
label: 'Neon Night',
image: NEON_NIGHT,
borderColor: '#ff4fd8',
size: STRIP_BASE_SIZE,
layout: 'stacked',
slots: STACKED_SLOTS,
},
{
id: 'sakura-wave',
label: 'Sakura Wave',
image: SAKURA_WAVE,
borderColor: '#7c3b54',
size: STRIP_BASE_SIZE,
layout: 'stacked',
slots: STACKED_SLOTS,
},
{
id: 'retro-film',
label: 'Retro Film',
image: RETRO_FILM,
borderColor: '#1b1b1b',
size: STRIP_BASE_SIZE,
layout: 'stacked',
slots: STACKED_SLOTS,
},
{
id: 'comic-pop',
label: 'Comic Pop',
image: COMIC_POP,
borderColor: '#111111',
size: STRIP_BASE_SIZE,
layout: 'stacked',
slots: STACKED_SLOTS,
},
{
id: 'kraft-minimal',
label: 'Kraft Minimal',
image: KRAFT_MINIMAL,
borderColor: '#3a2a1d',
size: STRIP_BASE_SIZE,
layout: 'stacked',
slots: STACKED_SLOTS,
},
{
id: 'cinema-ticket',
label: 'Cinema Ticket',
image: CINEMA_TICKET,
borderColor: '#f7d26a',
size: STRIP_BASE_SIZE,
layout: 'stacked',
slots: TICKET_SLOTS,
},
]
export const STRIP_THEME_MAP = Object.fromEntries(STRIP_THEMES.map((theme) => [theme.id, theme]))