1import { r as reactExports, R as React, j as jsxRuntimeExports } from "./vendor-react-Bay9Y7Lb.js"; 2import { a as defaultBackgroundPresets } from "./StagePuppetEngine-BtPqoUG7.js"; 3const SOUTHPARK_STAGE_CALIBRATIONS = { 4 // 1. Pavilion / Outdoor stages with smaller or raised platform 5 anime: { scale: 1.46, offsetX: -1, offsetY: 3, origin: "50% 50%", backY: 435, frontY: 600, backScale: 0.69, frontScale: 1.02, horizonY: 260 }, 6 occasions: { scale: 1.84, offsetX: 0, offsetY: -11, origin: "50% 50%", backY: 405, frontY: 555, backScale: 0.55, frontScale: 0.98, horizonY: 250 }, 7 reggae: { scale: 1.45, offsetX: -1, offsetY: -5, origin: "50% 50%", backY: 385, frontY: 565, backScale: 0.58, frontScale: 0.98, horizonY: 260 }, 8 techno: { scale: 1.5, offsetX: 0, offsetY: -10, origin: "50% 50%", backY: 410, frontY: 550, backScale: 0.52, frontScale: 1, horizonY: 240 }, 9 trap: { scale: 1.28, offsetX: -1, offsetY: -11, origin: "50% 50%", backY: 340, frontY: 540, backScale: 0.51, frontScale: 0.98, horizonY: 250 }, 10 // 2. Large concert & theater stages with elevated horizons 11 rock: { scale: 2.5, offsetX: -4, offsetY: -28, origin: "50% 50%", backY: 375, frontY: 540, backScale: 0.54, frontScale: 1, horizonY: 250 }, 12 classical: { scale: 1.37, offsetX: 0, offsetY: -12, origin: "50% 50%", backY: 435, frontY: 520, backScale: 0.55, frontScale: 0.82, horizonY: 250 }, 13 pop: { scale: 1.02, offsetX: -1, offsetY: 3, origin: "50% 50%", backY: 365, frontY: 540, backScale: 0.56, frontScale: 0.98, horizonY: 255 }, 14 jazz: { scale: 1.52, offsetX: 0, offsetY: -10, origin: "50% 50%", backY: 345, frontY: 455, backScale: 0.55, frontScale: 0.95, horizonY: 275 }, 15 metal: { scale: 1.22, offsetX: -1, offsetY: -11, origin: "50% 50%", backY: 370, frontY: 520, backScale: 0.55, frontScale: 0.98, horizonY: 245 }, 16 folk: { scale: 1.3, offsetX: 2, offsetY: -4, origin: "50% 50%", backY: 370, frontY: 565, backScale: 0.57, frontScale: 0.86, horizonY: 260 }, 17 game_music: { scale: 1.19, offsetX: 0, offsetY: -8, origin: "50% 50%", backY: 375, frontY: 485, backScale: 0.55, frontScale: 0.98, horizonY: 250 }, 18 "sci-fi": { scale: 1.2, offsetX: 1, offsetY: -5, origin: "50% 50%", backY: 380, frontY: 560, backScale: 0.56, frontScale: 0.98, horizonY: 255 }, 19 scifi: { scale: 1.2, offsetX: 1, offsetY: -5, origin: "50% 50%", backY: 380, frontY: 560, backScale: 0.56, frontScale: 0.98, horizonY: 255 }, 20 country: { scale: 1.29, offsetX: 0, offsetY: -4, origin: "50% 50%", backY: 430, frontY: 520, backScale: 0.58, frontScale: 0.96, horizonY: 265 }, 21 world: { scale: 1.22, offsetX: 0, offsetY: -4, origin: "50% 50%", backY: 375, frontY: 560, backScale: 0.54, frontScale: 0.98, horizonY: 250 }, 22 indie: { scale: 1.12, offsetX: 0, offsetY: 0, origin: "50% 50%", backY: 380, frontY: 485, backScale: 0.58, frontScale: 0.96, horizonY: 260 }, 23 // 3. Flat floor / expansive ground stages (subtle 1.15-1.18x zoom for stage immersion) 24 alt_rock: { scale: 1.15, offsetX: -1, offsetY: 0, origin: "50% 50%", backY: 420, frontY: 480, backScale: 0.6, frontScale: 0.98, horizonY: 275 }, 25 national: { scale: 1.44, offsetX: 0, offsetY: -11, origin: "50% 50%", backY: 405, frontY: 545, backScale: 0.56, frontScale: 0.98, horizonY: 255 }, 26 religious: { scale: 1.68, offsetX: 0, offsetY: -21, origin: "50% 50%", backY: 385, frontY: 565, backScale: 0.52, frontScale: 0.98, horizonY: 240 }, 27 original: { scale: 1, offsetX: 2, offsetY: -11, origin: "50% 50%", backY: 350, frontY: 545, backScale: 0.55, frontScale: 0.94, horizonY: 250 }, 28 hip_hop: { scale: 1.24, offsetX: 0, offsetY: -7, origin: "50% 50%", backY: 385, frontY: 525, backScale: 0.6, frontScale: 0.98, horizonY: 270 }, 29 karaoke: { scale: 1.15, offsetX: -1, offsetY: -6, origin: "50% 50%", backY: 405, frontY: 545, backScale: 0.64, frontScale: 0.96, horizonY: 280 }, 30 latin: { scale: 1.15, offsetX: 0, offsetY: -5, origin: "50% 50%", backY: 390, frontY: 540, backScale: 0.58, frontScale: 0.98, horizonY: 260 }, 31 reggaeton: { scale: 1.15, offsetX: -2, offsetY: -6, origin: "50% 50%", backY: 380, frontY: 545, backScale: 0.52, frontScale: 1, horizonY: 260 }, 32 rnb: { scale: 1.58, offsetX: 2, offsetY: -5, origin: "50% 50%", backY: 380, frontY: 590, backScale: 0.6, frontScale: 0.96, horizonY: 275 } 33}; 34const DEFAULT_ALIGNMENT = { 35 scale: 1.05, 36 offsetX: 0, 37 offsetY: 0, 38 origin: "50% 50%", 39 backY: 380, 40 frontY: 475, 41 backScale: 0.58, 42 frontScale: 0.98, 43 horizonY: 260 44}; 45function getSouthparkStageAlignment(imageUrlOrId) { 46 if (!imageUrlOrId) return DEFAULT_ALIGNMENT; 47 const directKey = imageUrlOrId.toLowerCase().trim(); 48 if (SOUTHPARK_STAGE_CALIBRATIONS[directKey]) { 49 return SOUTHPARK_STAGE_CALIBRATIONS[directKey]; 50 } 51 const match = imageUrlOrId.match(/\/([^/]+)\.(jpg|png|webp|svg)/i); 52 if (match && match[1]) { 53 const fileKey = match[1].toLowerCase(); 54 if (SOUTHPARK_STAGE_CALIBRATIONS[fileKey]) { 55 return SOUTHPARK_STAGE_CALIBRATIONS[fileKey]; 56 } 57 } 58 for (const key of Object.keys(SOUTHPARK_STAGE_CALIBRATIONS)) { 59 if (directKey.includes(key)) { 60 return SOUTHPARK_STAGE_CALIBRATIONS[key]; 61 } 62 } 63 return DEFAULT_ALIGNMENT; 64} 65const defaultSouthparkStageConfig = { 66 type: "image", 67 color: "#0c0c14", 68 gradient: { 69 type: "linear", 70 angle: 135, 71 stops: [] 72 }, 73 image: { 74 url: "/assets/visualizers/orchestra/bkg/classical.jpg", 75 size: "cover", 76 position: "center center", 77 repeat: "no-repeat", 78 blur: 0 79 }, 80 particles: { 81 enabled: false, 82 type: "stars", 83 count: 50, 84 speed: 1, 85 color: "#ffffff", 86 size: 2 87 }, 88 overlay: { 89 enabled: true, 90 color: "#000000", 91 opacity: 0.15 92 } 93};
94function useSouthparkStageBackground(track, shuffleSeed) { 95 const [spBackgroundConfig, setSpBackgroundConfig] = reactExports.useState(() => { 96 if (typeof window !== "undefined") { 97 try { 98 const cached = localStorage.getItem("southpark_cached_bg_config"); 99 if (cached) return JSON.parse(cached); 100 } catch { 101 } 102 } 103 return null; 104 }); 105 const [spBackgroundPresets, setSpBackgroundPresets] = reactExports.useState(() => { 106 if (typeof window !== "undefined") { 107 try { 108 const cached = localStorage.getItem("southpark_cached_bg_presets"); 109 if (cached) return JSON.parse(cached); 110 } catch { 111 } 112 } 113 return null; 114 }); 115 reactExports.useEffect(() => { 116 let isMounted = true; 117 const loadConfigs = async () => { 118 var _a, _b; 119 try { 120 const [spBgRes, spPresetsRes] = await Promise.all([ 121 fetch("/api/settings/southpark_background_config"), 122 fetch("/api/settings/southpark_background_presets") 123 ]); 124 if (isMounted) { 125 if (spBgRes.ok) { 126 const data = await spBgRes.json(); 127 if (data == null ? void 0 : data.value) { 128 const val = data.value; 129 if ((_b = (_a = val.image) == null ? void 0 : _a.url) == null ? void 0 : _b.includes("background-forest")) { 130 val.image.url = "/assets/visualizers/orchestra/bkg/classical.jpg"; 131 } 132 setSpBackgroundConfig(val); 133 try { 134 localStorage.setItem("southpark_cached_bg_config", JSON.stringify(val)); 135 } catch { 136 } 137 } 138 } 139 if (spPresetsRes.ok) { 140 const data = await spPresetsRes.json(); 141 if ((data == null ? void 0 : data.value) && Array.isArray(data.value) && data.value.length > 0) { 142 const cleaned = data.value.filter( 143 (p) => { 144 var _a2, _b2, _c; 145 return !((_c = (_b2 = (_a2 = p.config) == null ? void 0 : _a2.image) == null ? void 0 : _b2.url) == null ? void 0 : _c.includes("background-forest")); 146 } 147 ); 148 setSpBackgroundPresets(cleaned); 149 try { 150 localStorage.setItem("southpark_cached_bg_presets", JSON.stringify(cleaned)); 151 } catch { 152 } 153 } 154 } 155 } 156 } catch (e) { 157 console.log("Using default background configs in Southpark visualizer", e); 158 } 159 }; 160 loadConfigs(); 161 return () => { 162 isMounted = false; 163 }; 164 }, []); 165 const resolvedBackgroundConfig = reactExports.useMemo(() => { 166 var _a; 167 if (track.backgroundImageUrl) { 168 return { 169 type: "image", 170 color: "#050505", 171 gradient: { 172 type: "linear", 173 angle: 180, 174 stops: [] 175 }, 176 image: { 177 url: track.backgroundImageUrl, 178 size: "cover", 179 position: "center center", 180 repeat: "no-repeat", 181 blur: 0 182 }, 183 overlay: { 184 enabled: true, 185 color: "#000000", 186 opacity: 0.15 187 }, 188 particles: { 189 enabled: false, 190 type: "stars", 191 count: 50, 192 speed: 1, 193 color: "#ffffff", 194 size: 2 195 } 196 }; 197 } 198 const enrichPresetConfig = (preset) => { 199 var _a2, _b, _c; 200 const url = (_b = (_a2 = preset.config) == null ? void 0 : _a2.image) == null ? void 0 : _b.url; 201 const calib2 = getSouthparkStageAlignment(url); 202 const userImg = (_c = preset.config) == null ? void 0 : _c.image; 203 return { 204 ...preset, 205 config: { 206 ...preset.config, 207 image: { 208 url: (userImg == null ? void 0 : userImg.url) || url || "", 209 size: (userImg == null ? void 0 : userImg.size) || "cover", 210 position: (userImg == null ? void 0 : userImg.position) || "center center", 211 repeat: (userImg == null ? void 0 : userImg.repeat) || "no-repeat", 212 blur: (userImg == null ? void 0 : userImg.blur) || 0, 213 scale: (userImg == null ? void 0 : userImg.scale) !== void 0 ? userImg.scale : calib2.scale, 214 offsetX: (userImg == null ? void 0 : userImg.offsetX) !== void 0 ? userImg.offsetX : calib2.offsetX || 0, 215 offsetY: (userImg == null ? void 0 : userImg.offsetY) !== void 0 ? userImg.offsetY : calib2.offsetY, 216 backY: (userImg == null ? void 0 : userImg.backY) !== void 0 ? userImg.backY : calib2.backY, 217 frontY: (userImg == null ? void 0 : userImg.frontY) !== void 0 ? userImg.frontY : calib2.frontY, 218 backScale: (userImg == null ? void 0 : userImg.backScale) !== void 0 ? userImg.backScale : calib2.backScale, 219 frontScale: (userImg == null ? void 0 : userImg.frontScale) !== void 0 ? userImg.frontScale : calib2.frontScale, 220 horizonY: (userImg == null ? void 0 : userImg.horizonY) !== void 0 ? userImg.horizonY : calib2.horizonY 221 } 222 } 223 }; 224 };
225 const basePool = defaultBackgroundPresets.filter((p) => { 226 var _a2, _b, _c; 227 return !((_c = (_b = (_a2 = p.config) == null ? void 0 : _a2.image) == null ? void 0 : _b.url) == null ? void 0 : _c.includes("background-forest")); 228 }); 229 const mergedPresets = basePool.map((base) => { 230 const spOverride = (spBackgroundPresets || []).find((p) => p.id === base.id); 231 const raw = spOverride ? spOverride : base; 232 return enrichPresetConfig(raw); 233 }); 234 const baseIds = new Set(basePool.map((b) => b.id)); 235 const extraCustom = (spBackgroundPresets || []).filter((p) => { 236 var _a2, _b, _c; 237 return !baseIds.has(p.id) && !((_c = (_b = (_a2 = p.config) == null ? void 0 : _a2.image) == null ? void 0 : _b.url) == null ? void 0 : _c.includes("background-forest")); 238 }).map(enrichPresetConfig); 239 const fullPool = [...mergedPresets, ...extraCustom]; 240 if (shuffleSeed !== void 0 && fullPool.length > 0) { 241 const seedIndex = Math.abs(shuffleSeed) % fullPool.length; 242 return fullPool[seedIndex].config; 243 } 244 const matchPresets = (presets) => { 245 var _a2, _b; 246 if (!Array.isArray(track.tags) || track.tags.length === 0) return null; 247 const tagNames = track.tags.map((t) => { 248 if (typeof t === "string") return t.toLowerCase(); 249 if (t && typeof t === "object" && "name" in t) return t.name.toLowerCase(); 250 return ""; 251 }).filter(Boolean); 252 for (const preset of presets) { 253 if (Array.isArray(preset.tags) && preset.tags.length > 0) { 254 const matches = preset.tags.some((tag) => { 255 const cleanTag = tag.toLowerCase().trim(); 256 if (!cleanTag) return false; 257 return tagNames.some((tn) => { 258 if (tn.includes(cleanTag) || cleanTag.includes(tn)) return true; 259 const tnWords = tn.split(/[\s,\-_]+/); 260 const tagWords = cleanTag.split(/[\s,\-_]+/); 261 return tagWords.some((tw) => tw.length > 2 && tnWords.includes(tw)); 262 }); 263 }); 264 if (matches && preset.config && !((_b = (_a2 = preset.config.image) == null ? void 0 : _a2.url) == null ? void 0 : _b.includes("background-forest"))) { 265 return preset.config; 266 } 267 } 268 } 269 return null; 270 }; 271 const matchedPresetConfig = matchPresets(fullPool); 272 if (matchedPresetConfig) return matchedPresetConfig; 273 if (fullPool.length > 0) { 274 const trackStr = `${track.id || ""}_${track.title || "default"}`; 275 let hash = 0; 276 for (let i = 0; i < trackStr.length; i++) { 277 hash = (hash << 5) - hash + trackStr.charCodeAt(i); 278 hash |= 0; 279 } 280 const index = Math.abs(hash) % fullPool.length; 281 return fullPool[index].config; 282 } 283 const baseDefault = spBackgroundConfig || defaultSouthparkStageConfig; 284 const calib = getSouthparkStageAlignment((_a = baseDefault.image) == null ? void 0 : _a.url); 285 const defImg = baseDefault.image; 286 return { 287 ...baseDefault, 288 image: { 289 url: (defImg == null ? void 0 : defImg.url) || "/assets/visualizers/orchestra/bkg/classical.jpg", 290 size: (defImg == null ? void 0 : defImg.size) || "cover", 291 position: (defImg == null ? void 0 : defImg.position) || "center center", 292 repeat: (defImg == null ? void 0 : defImg.repeat) || "no-repeat", 293 blur: (defImg == null ? void 0 : defImg.blur) || 0, 294 scale: (defImg == null ? void 0 : defImg.scale) !== void 0 ? defImg.scale : calib.scale, 295 offsetX: (defImg == null ? void 0 : defImg.offsetX) !== void 0 ? defImg.offsetX : calib.offsetX || 0, 296 offsetY: (defImg == null ? void 0 : defImg.offsetY) !== void 0 ? defImg.offsetY : calib.offsetY, 297 backY: (defImg == null ? void 0 : defImg.backY) !== void 0 ? defImg.backY : calib.backY, 298 frontY: (defImg == null ? void 0 : defImg.frontY) !== void 0 ? defImg.frontY : calib.frontY, 299 backScale: (defImg == null ? void 0 : defImg.backScale) !== void 0 ? defImg.backScale : calib.backScale, 300 frontScale: (defImg == null ? void 0 : defImg.frontScale) !== void 0 ? defImg.frontScale : calib.frontScale, 301 horizonY: (defImg == null ? void 0 : defImg.horizonY) !== void 0 ? defImg.horizonY : calib.horizonY 302 } 303 }; 304 }, [ 305 track.id, 306 track.title, 307 track.backgroundImageUrl, 308 track.tags, 309 shuffleSeed,
310 spBackgroundConfig, 311 spBackgroundPresets 312 ]); 313 return resolvedBackgroundConfig; 314} 315const FAMILY_SORT_WEIGHTS = { 316 // 1. Basses (Low end foundation - Left stage) 317 4: 10, 318 // Basses (Acoustic, Electric, Synth Bass) 319 // 2. Guitars 320 3: 20, 321 // Guitars (Nylon, Steel, Clean, Distortion) 322 // 3. Drums & Percussion (Rhythm section, anchored in back) 323 "-1": 30, 324 // Ch 9 Drums / Rhythm kit 325 14: 35, 326 // Percussive 327 15: 38, 328 // Sound Effects 329 // 4. Keyboards 330 0: 40, 331 // Pianos (Acoustic Grand, Electric Piano) 332 2: 45, 333 // Organs (Hammond, Church, Accordion) 334 // 5. Synthesizers 335 10: 50, 336 // Synth Lead 337 11: 55, 338 // Synth Pad 339 12: 58, 340 // Synth FX 341 // 6. Woodwinds 342 9: 60, 343 // Pipes / Flutes (Flute, Pan Flute, Piccolo) 344 8: 65, 345 // Reeds (Saxophones, Oboe, Clarinet, Bassoon) 346 // 7. Brass 347 7: 70, 348 // Brass (Trumpet, Trombone, French Horn, Tuba) 349 // 8. Strings & Ensembles 350 5: 80, 351 // Strings (Violin, Viola, Cello, Contrabass, Harp) 352 6: 85, 353 // Ensembles (String Ensembles, Choirs) 354 1: 88, 355 // Chromatic Percussion (Vibraphone, Marimba, Glockenspiel) 356 // 9. Ethnic & World 357 13: 90 358 // Ethnic (Sitar, Banjo, Koto, Bagpipe) 359}; 360const PERC_ROLE_WEIGHTS = { 361 drums: 30, 362 electronic: 31, 363 congas: 32, 364 bongos: 33, 365 tambourine: 34, 366 cowbell: 35, 367 triangle: 36, 368 shaker: 37, 369 claps: 38 370}; 371function getPerspectiveScale(y, baseScale = 1, depthConfig) { 372 const backY = (depthConfig == null ? void 0 : depthConfig.backY) ?? 380; 373 const frontY = (depthConfig == null ? void 0 : depthConfig.frontY) ?? 475; 374 const backScale = (depthConfig == null ? void 0 : depthConfig.backScale) ?? 0.58; 375 const frontScale = (depthConfig == null ? void 0 : depthConfig.frontScale) ?? 0.98; 376 const effectiveBase = (depthConfig == null ? void 0 : depthConfig.baseScale) ?? baseScale; 377 const clampedY = Math.max(backY - 30, Math.min(frontY + 35, y)); 378 const rangeY = Math.max(20, frontY - backY); 379 const progress = (clampedY - backY) / rangeY; 380 const scale = backScale + progress * (frontScale - backScale); 381 return Math.max(0.35, Math.min(1.25, scale)) * effectiveBase; 382} 383function calculateEnsemblePlacement(musicians, options = {}) { 384 const count = musicians.length; 385 if (count === 0) return []; 386 const { 387 stageConfig, 388 customPositions = {}, 389 enableDensityScaling = true, 390 stageWidth = 1200, 391 stageHeight = 650 392 } = options; 393 const baseScale = (stageConfig == null ? void 0 : stageConfig.baseScale) ?? 1; 394 const globalYOffset = (stageConfig == null ? void 0 : stageConfig.yOffset) ?? 0; 395 const stageArcMultiplier = (stageConfig == null ? void 0 : stageConfig.arcFactor) ?? 1; 396 const densityScale = enableDensityScaling && count > 8 ? Math.max(0.75, 1 - (count - 8) * 0.022) : 1; 397 const prepared = musicians.map((m) => { 398 let weight = 5e3; 399 const prog = m.originalProgram !== void 0 && m.originalProgram !== null ? m.originalProgram : m.program; 400 const fam = m.originalFamilyIndex !== void 0 && m.originalFamilyIndex !== null ? m.originalFamilyIndex : m.isPercussion ? -1 : Math.floor(prog / 8); 401 if (m.isPercussion) { 402 const roleWeight = m.percRole ? PERC_ROLE_WEIGHTS[m.percRole] || 30 : 30; 403 weight = roleWeight * 1e3; 404 } else { 405 const base = FAMILY_SORT_WEIGHTS[fam] !== void 0 ? FAMILY_SORT_WEIGHTS[fam] : 50; 406 weight = base * 1e3 + prog % 128; 407 } 408 return { 409 musician: m, 410 fam, 411 sortWeight: weight 412 }; 413 }); 414 prepared.sort((a, b) => a.sortWeight - b.sortWeight); 415 const backY = (stageConfig == null ? void 0 : stageConfig.backY) ?? 380; 416 const frontY = (stageConfig == null ? void 0 : stageConfig.frontY) ?? 475; 417 const middleY = Math.round((backY + frontY) / 2); 418 prepared.some((p) => p.musician.isPercussion || p.fam === -1 || p.fam === 14); 419 let backGroup = []; 420 let middleGroup = []; 421 let frontGroup = []; 422 if (count <= 3) { 423 const hasPerc = prepared.some((p) => p.musician.isPercussion || p.fam === -1 || p.fam === 14); 424 if (hasPerc) { 425 prepared.forEach((p) => { 426 if (p.musician.isPercussion || p.fam === -1 || p.fam === 14) backGroup.push(p); 427 else frontGroup.push(p); 428 }); 429 } else { 430 frontGroup = [...prepared]; 431 } 432 } else if (count <= 7) { 433 prepared.forEach((p) => { 434 const isBack = p.musician.isPercussion || p.fam === -1 || p.fam === 14 || p.fam === 11 || p.fam === 12; 435 const isMiddle = p.fam === 7 || p.fam === 8 || p.fam === 9 || p.fam === 0 || p.fam === 2 || p.fam === 1 || p.fam === 6; 436 if (isBack) { 437 backGroup.push(p); 438 } else if (isMiddle) { 439 middleGroup.push(p); 440 } else { 441 frontGroup.push(p); 442 } 443 }); 444 if (frontGroup.length >= 4 && middleGroup.length === 0) { 445 middleGroup.push(frontGroup.splice(Math.floor(frontGroup.length / 2), 1)[0]); 446 } 447 if (frontGroup.length > 3 && middleGroup.length === 1 && backGroup.length === 0) { 448 backGroup.push(frontGroup.shift()); 449 } 450 } else { 451 prepared.forEach((p) => { 452 const isBack = p.musician.isPercussion || p.fam === -1 || p.fam === 14 || p.fam === 11 || p.fam === 12; 453 const isMiddle = p.fam === 7 || p.fam === 8 || p.fam === 9 || p.fam === 0 || p.fam === 2 || p.fam === 6 || p.fam === 1; 454 if (isBack) { 455 backGroup.push(p); 456 } else if (isMiddle) { 457 middleGroup.push(p); 458 } else { 459 frontGroup.push(p); 460 } 461 }); 462 const targetPerRow = Math.ceil(count / 3); 463 while (frontGroup.length > targetPerRow + 1 && middleGroup.length < targetPerRow) { 464 middleGroup.push(frontGroup.splice(Math.floor(frontGroup.length / 2), 1)[0]); 465 } 466 while (middleGroup.length > targetPerRow + 1 && backGroup.length < targetPerRow) { 467 backGroup.push(middleGroup.splice(Math.floor(middleGroup.length / 2), 1)[0]); 468 } 469 } 470 const frontMargin = count >= 16 ? 110 : count >= 12 ? 130 : count >= 8 ? 150 : count >= 6 ? 160 : 200; 471 const middleMargin = frontMargin + 25; 472 const backMargin = frontMargin + 45; 473 function placeRow(group, rowName, baseYVal, margin) { 474 const n = group.length; 475 if (n === 0) return []; 476 const avail = stageWidth - margin * 2; 477 if (n === 1) { 478 return [{ 479 m: group[0].musician, 480 rowName, 481 rowIndex: rowName === "back" ? 0 : rowName === "middle" ? 1 : 2, 482 defaultX: Math.round(stageWidth / 2),
483 defaultY: baseYVal 484 }]; 485 } 486 const step = avail / (n - 1); 487 return group.map((p, i) => ({ 488 m: p.musician, 489 rowName, 490 rowIndex: rowName === "back" ? 0 : rowName === "middle" ? 1 : 2, 491 defaultX: Math.round(margin + i * step), 492 defaultY: baseYVal 493 })); 494 } 495 const placedFront = placeRow(frontGroup, "front", frontY, frontMargin); 496 const placedMiddle = placeRow(middleGroup, "middle", middleY, middleMargin); 497 const placedBack = placeRow(backGroup, "back", backY, backMargin); 498 const allPlaced = [...placedBack, ...placedMiddle, ...placedFront]; 499 if (count > 1) { 500 let minGap = 160; 501 if (count <= 2) minGap = 260; 502 else if (count <= 4) minGap = 200; 503 else if (count === 5) minGap = 175; 504 else if (count === 6) minGap = 155; 505 else if (count === 7) minGap = 140; 506 else if (count <= 10) minGap = 110; 507 else if (count <= 14) minGap = 85; 508 else minGap = 70; 509 const leftBound = frontMargin; 510 const rightBound = stageWidth - frontMargin; 511 for (let pass = 0; pass < 25; pass++) { 512 allPlaced.sort((a, b) => a.defaultX - b.defaultX); 513 let maxOverlap = 0; 514 for (let i = 0; i < allPlaced.length - 1; i++) { 515 const curr = allPlaced[i]; 516 const next = allPlaced[i + 1]; 517 const dist = next.defaultX - curr.defaultX; 518 const overlap = minGap - dist; 519 if (overlap > 0) { 520 if (overlap > maxOverlap) maxOverlap = overlap; 521 const shift = Math.ceil(overlap / 2); 522 curr.defaultX = Math.max(leftBound, curr.defaultX - shift); 523 next.defaultX = Math.min(rightBound, next.defaultX + shift); 524 } 525 } 526 if (maxOverlap <= 1) break; 527 } 528 allPlaced.sort((a, b) => a.defaultX - b.defaultX); 529 const ensembleLeft = allPlaced[0].defaultX; 530 const ensembleRight = allPlaced[allPlaced.length - 1].defaultX; 531 const ensembleCenter = (ensembleLeft + ensembleRight) / 2; 532 const centerOffset = Math.round(stageWidth / 2 - ensembleCenter); 533 if (Math.abs(centerOffset) > 2) { 534 allPlaced.forEach((m) => { 535 m.defaultX = Math.max(leftBound, Math.min(rightBound, m.defaultX + centerOffset)); 536 }); 537 } 538 allPlaced.forEach((m) => { 539 const arcProgress = (m.defaultX - leftBound) / Math.max(1, rightBound - leftBound); 540 const arcFactor = Math.sin(Math.max(0, Math.min(1, arcProgress)) * Math.PI); 541 const baseArcNudge = m.rowName === "front" ? 24 : m.rowName === "middle" ? 16 : 10; 542 const arcOffsetY = -arcFactor * baseArcNudge * stageArcMultiplier; 543 m.defaultY = Math.min(stageHeight - 145, Math.round(m.defaultY + globalYOffset + arcOffsetY)); 544 }); 545 } else if (allPlaced.length === 1) { 546 allPlaced[0].defaultY = Math.min(stageHeight - 145, Math.round(allPlaced[0].defaultY + globalYOffset)); 547 } 548 const tempPlaced = allPlaced; 549 const result = tempPlaced.map((item) => { 550 const m = item.m; 551 const stableKey = m.stableKey || (typeof m.index === "number" ? `track-${m.index}${m.percRole ? "-" + m.percRole : ""}` : void 0); 552 const customPos = (stableKey ? customPositions[stableKey] : void 0) || (m.trackId !== void 0 ? customPositions[`trackId-${m.trackId}`] : void 0) || customPositions[m.id]; 553 const posX = (customPos == null ? void 0 : customPos.x) ?? item.defaultX; 554 const posY = (customPos == null ? void 0 : customPos.y) ?? item.defaultY; 555 const basePerspectiveScale = getPerspectiveScale(posY, baseScale, stageConfig); 556 const musicianScale = basePerspectiveScale * densityScale * (m.scaleMultiplier ?? 1); 557 return { 558 m, 559 defaultX: item.defaultX, 560 defaultY: item.defaultY, 561 posX, 562 posY, 563 musicianScale, 564 rowName: item.rowName, 565 rowIndex: item.rowIndex 566 }; 567 }); 568 return result.sort((a, b) => a.posY - b.posY); 569} 570const LIGHTING_PROFILES = { 571 rock: { 572 id: "rock", 573 name: "Arena Rock Rig", 574 style: "searchlights", 575 baseIntensity: 0.55, 576 peakIntensity: 1, 577 beatPulseAmount: 0.35, 578 sweepSpeedMultiplier: 1.4, 579 trussColor: "#ef4444", 580 ambientColors: ["rgba(239, 68, 68, 0.25)", "rgba(59, 130, 246, 0.2)"], 581 blinderColor: "#ffffff", 582 beamColors: ["#ef4444", "#f97316", "#eab308", "#3b82f6", "#ec4899"], 583 beamCount: 6, 584 maxBeamSpreadAngle: 36, 585 hasStrobes: true, 586 hasBlinders: true, 587 floorGlowOpacity: 0.45 588 }, 589 techno: { 590 id: "techno", 591 name: "Cyber Rave Matrix", 592 style: "lasers", 593 baseIntensity: 0.6, 594 peakIntensity: 1, 595 beatPulseAmount: 0.4, 596 sweepSpeedMultiplier: 2, 597 trussColor: "#06b6d4", 598 ambientColors: ["rgba(6, 182, 212, 0.3)", "rgba(168, 85, 247, 0.25)"], 599 blinderColor: "#00f6ff", 600 beamColors: ["#06b6d4", "#ec4899", "#3b82f6", "#a855f7", "#10b981"], 601 beamCount: 8, 602 maxBeamSpreadAngle: 55, 603 hasStrobes: true, 604 hasBlinders: true, 605 floorGlowOpacity: 0.5 606 }, 607 classical: { 608 id: "classical", 609 name: "Concert Hall Chandelier", 610 style: "chandelier", 611 baseIntensity: 0.3, 612 peakIntensity: 0.65, 613 beatPulseAmount: 0.15, 614 sweepSpeedMultiplier: 0.6, 615 trussColor: "#f59e0b", 616 ambientColors: ["rgba(245, 158, 11, 0.18)", "rgba(217, 119, 6, 0.12)"], 617 blinderColor: "#fffbeb", 618 beamColors: ["#fde68a", "#fef3c7", "#f59e0b", "#fbbf24"], 619 beamCount: 4, 620 maxBeamSpreadAngle: 24, 621 hasStrobes: false, 622 hasBlinders: false, 623 floorGlowOpacity: 0.25 624 }, 625 jazz: { 626 id: "jazz", 627 name: "Smoky Cabaret Pin-Spot", 628 style: "cabaret", 629 baseIntensity: 0.25, 630 peakIntensity: 0.6, 631 beatPulseAmount: 0.2, 632 sweepSpeedMultiplier: 0.7, 633 trussColor: "#6366f1", 634 ambientColors: ["rgba(49, 46, 129, 0.35)", "rgba(88, 28, 135, 0.2)"], 635 blinderColor: "#fef08a", 636 beamColors: ["#818cf8", "#a855f7", "#fde047", "#60a5fa"], 637 beamCount: 3, 638 maxBeamSpreadAngle: 22, 639 hasStrobes: false, 640 hasBlinders: false, 641 floorGlowOpacity: 0.22 642 }, 643 anime: { 644 id: "anime", 645 name: "Celestial God Rays", 646 style: "godrays", 647 baseIntensity: 0.4, 648 peakIntensity: 0.78, 649 beatPulseAmount: 0.22, 650 sweepSpeedMultiplier: 0.85, 651 trussColor: "#c084fc", 652 ambientColors: ["rgba(192, 132, 252, 0.25)", "rgba(56, 189, 248, 0.2)"], 653 blinderColor: "#ffffff", 654 beamColors: ["#f472b6", "#38bdf8", "#c084fc", "#fb7185", "#fef08a"], 655 beamCount: 5, 656 maxBeamSpreadAngle: 32, 657 hasStrobes: false, 658 hasBlinders: true, 659 floorGlowOpacity: 0.35 660 }, 661 folk: { 662 id: "folk", 663 name: "Golden Hour Lanterns", 664 style: "lanterns", 665 baseIntensity: 0.32, 666 peakIntensity: 0.68, 667 beatPulseAmount: 0.18, 668 sweepSpeedMultiplier: 0.65, 669 trussColor: "#ea580c", 670 ambientColors: ["rgba(234, 88, 12, 0.2)", "rgba(202, 138, 4, 0.15)"], 671 blinderColor: "#fed7aa", 672 beamColors: ["#fb923c", "#facc15", "#f97316", "#fdba74"], 673 beamCount: 4, 674 maxBeamSpreadAngle: 26, 675 hasStrobes: false, 676 hasBlinders: false, 677 floorGlowOpacity: 0.28 678 }, 679 pop: { 680 id: "pop", 681 name: "Broadway Disco Pins", 682 style: "disco", 683 baseIntensity: 0.5, 684 peakIntensity: 0.9, 685 beatPulseAmount: 0.3, 686 sweepSpeedMultiplier: 1.3, 687 trussColor: "#ec4899", 688 ambientColors: ["rgba(236, 72, 153, 0.25)", "rgba(234, 179, 8, 0.2)"], 689 blinderColor: "#ffffff", 690 beamColors: ["#ec4899", "#06b6d4", "#eab308", "#a855f7", "#f43f5e"], 691 beamCount: 6, 692 maxBeamSpreadAngle: 42, 693 hasStrobes: true, 694 hasBlinders: true, 695 floorGlowOpacity: 0.4 696 }, 697 reggae: { 698 id: "reggae", 699 name: "Sunset Horizon Spectrum", 700 style: "sunset", 701 baseIntensity: 0.4, 702 peakIntensity: 0.75, 703 beatPulseAmount: 0.25, 704 sweepSpeedMultiplier: 0.8, 705 trussColor: "#10b981", 706 ambientColors: ["rgba(16, 185, 129, 0.25)", "rgba(245, 158, 11, 0.2)"], 707 blinderColor: "#fef08a", 708 beamColors: ["#10b981", "#f59e0b", "#ef4444", "#84cc16"], 709 beamCount: 5, 710 maxBeamSpreadAngle: 35, 711 hasStrobes: false, 712 hasBlinders: false, 713 floorGlowOpacity: 0.35 714 }, 715 cathedral: { 716 id: "cathedral", 717 name: "Cathedral Pillar Shafts", 718 style: "cathedral", 719 baseIntensity: 0.3, 720 peakIntensity: 0.7, 721 beatPulseAmount: 0.12, 722 sweepSpeedMultiplier: 0.5, 723 trussColor: "#38bdf8", 724 ambientColors: ["rgba(56, 189, 248, 0.18)", "rgba(250, 204, 21, 0.12)"], 725 blinderColor: "#ffffff", 726 beamColors: ["#ffffff", "#bae6fd", "#fef08a", "#e0f2fe"], 727 beamCount: 4, 728 maxBeamSpreadAngle: 18, 729 hasStrobes: false, 730 hasBlinders: true, 731 floorGlowOpacity: 0.25 732 }, 733 standard: { 734 id: "standard", 735 name: "Theatrical Stage Rig", 736 style: "standard", 737 baseIntensity: 0.42, 738 peakIntensity: 0.8, 739 beatPulseAmount: 0.25, 740 sweepSpeedMultiplier: 1, 741 trussColor: "#818cf8", 742 ambientColors: ["rgba(129, 140, 248, 0.22)", "rgba(245, 158, 11, 0.12)"], 743 blinderColor: "#ffffff", 744 beamColors: ["#818cf8", "#f59e0b", "#38bdf8", "#c084fc"], 745 beamCount: 5, 746 maxBeamSpreadAngle: 30, 747 hasStrobes: false, 748 hasBlinders: true, 749 floorGlowOpacity: 0.32 750 } 751}; 752function resolveLightingProfile(bg, tags) { 753 var _a; 754 const url = (((_a = bg == null ? void 0 : bg.image) == null ? void 0 : _a.url) || "").toLowerCase(); 755 if (url.includes("rock") || url.includes("metal")) return LIGHTING_PROFILES.rock; 756 if (url.includes("techno") || url.includes("scifi") || url.includes("sci-fi") || url.includes("trap") || url.includes("electronic")) return LIGHTING_PROFILES.techno; 757 if (url.includes("classical")) return LIGHTING_PROFILES.classical; 758 if (url.includes("jazz") || url.includes("rnb")) return LIGHTING_PROFILES.jazz; 759 if (url.includes("anime") || url.includes("game_music")) return LIGHTING_PROFILES.anime; 760 if (url.includes("folk") || url.includes("country") || url.includes("world") || url.includes("original")) return LIGHTING_PROFILES.folk; 761 if (url.includes("pop") || url.includes("karaoke") || url.includes("occasions")) return LIGHTING_PROFILES.pop; 762 if (url.includes("reggae") || url.includes("latin") || url.includes("reggaeton")) return LIGHTING_PROFILES.reggae; 763 if (url.includes("religious") || url.includes("national")) return LIGHTING_PROFILES.cathedral; 764 if (Array.isArray(tags) && tags.length > 0) { 765 const tagStrings = tags.map((t) => typeof t === "string" ? t.toLowerCase() : ((t == null ? void 0 : t.name) || "").toLowerCase()).join(" "); 766 if (tagStrings.includes("rock") || tagStrings.includes("metal")) return LIGHTING_PROFILES.rock; 767 if (tagStrings.includes("techno") || tagStrings.includes("edm") || tagStrings.includes("dance") || tagStrings.includes("synth")) return LIGHTING_PROFILES.techno; 768 if (tagStrings.includes("classical") || tagStrings.includes("orchestral") || tagStrings.includes("piano")) return LIGHTING_PROFILES.classical; 769 if (tagStrings.includes("jazz") || tagStrings.includes("blues") || tagStrings.includes("r&b") || tagStrings.includes("rnb")) return LIGHTING_PROFILES.jazz; 770 if (tagStrings.includes("anime") || tagStrings.includes("game") || tagStrings.includes("chiptune")) return LIGHTING_PROFILES.anime; 771 if (tagStrings.includes("folk") || tagStrings.includes("country") || tagStrings.includes("acoustic")) return LIGHTING_PROFILES.folk; 772 if (tagStrings.includes("pop") || tagStrings.includes("karaoke") || tagStrings.includes("broadway")) return LIGHTING_PROFILES.pop; 773 if (tagStrings.includes("reggae") || tagStrings.includes("latin") || tagStrings.includes("salsa")) return LIGHTING_PROFILES.reggae; 774 if (tagStrings.includes("hymn") || tagStrings.includes("religious") || tagStrings.includes("anthem")) return LIGHTING_PROFILES.cathedral; 775 } 776 return LIGHTING_PROFILES.standard; 777} 778const SouthparkStageLightingComponent = ({ 779 track,
780 currentTime = 0, 781 isPlaying, 782 bpm = 120, 783 bps = 2, 784 backgroundConfig, 785 lightingMode = "dynamic", 786 musicians = [], 787 musicianPositions = {}, 788 activeNotes = {}, 789 soloedIndex = null, 790 selectedTrackIndex = null, 791 globalIntensity = 0, 792 stageWidth = 1200, 793 stageHeight = 650 794}) => { 795 var _a, _b, _c; 796 if (lightingMode === "off") { 797 return null; 798 } 799 const profile = reactExports.useMemo(() => { 800 return resolveLightingProfile(backgroundConfig, track == null ? void 0 : track.tags); 801 }, [(_a = backgroundConfig == null ? void 0 : backgroundConfig.image) == null ? void 0 : _a.url, track == null ? void 0 : track.tags]); 802 const safeTime = typeof currentTime === "number" && !isNaN(currentTime) && isFinite(currentTime) ? Math.max(0, currentTime) : 0; 803 const safeBpm = typeof bpm === "number" && !isNaN(bpm) && isFinite(bpm) && bpm > 0 ? bpm : 120; 804 const safeBps = typeof bps === "number" && !isNaN(bps) && isFinite(bps) && bps > 0 ? bps : safeBpm / 60; 805 const effectiveBps = safeBps > 0 ? safeBps : 2; 806 const beatInterval = 1 / effectiveBps; 807 const rawBeatProg = safeTime % beatInterval / beatInterval; 808 const beatProgress = !isNaN(rawBeatProg) && isFinite(rawBeatProg) ? Math.max(0, Math.min(1, rawBeatProg)) : 0; 809 const rawBeatPulse = Math.exp(-5.5 * beatProgress); 810 const beatPulse = !isNaN(rawBeatPulse) && isFinite(rawBeatPulse) ? rawBeatPulse : 0; 811 const rawBarProg = safeTime % (beatInterval * 4) / (beatInterval * 4); 812 const barProgress = !isNaN(rawBarProg) && isFinite(rawBarProg) ? Math.max(0, Math.min(1, rawBarProg)) : 0; 813 const rawDownbeat = Math.exp(-6.5 * barProgress); 814 const downbeatPulse = !isNaN(rawDownbeat) && isFinite(rawDownbeat) ? rawDownbeat : 0; 815 let hasClimax = false; 816 let hasAttack = false; 817 let maxMusicianVel = 0; 818 for (const key in activeNotes) { 819 const n = activeNotes[key]; 820 if (n) { 821 if (n.velocity > maxMusicianVel) maxMusicianVel = n.velocity; 822 if ((_b = n.features) == null ? void 0 : _b.isClimaxing) hasClimax = true; 823 if ((_c = n.features) == null ? void 0 : _c.isPhraseAttack) hasAttack = true; 824 } 825 } 826 const maxVelNorm = maxMusicianVel / 127; 827 const modeMultiplier = lightingMode === "subtle" ? 0.6 : 1; 828 const speedMultiplier = lightingMode === "subtle" ? 0.5 : 1; 829 const dynamicRange = profile.peakIntensity - profile.baseIntensity; 830 const musicEnergy = (globalIntensity || 0) * 0.55 + maxVelNorm * 0.45; 831 const climaxBoost = (hasClimax ? 0.28 : 0) + (hasAttack ? 0.15 : 0); 832 const rawIntensity = (profile.baseIntensity + musicEnergy * dynamicRange + beatPulse * profile.beatPulseAmount + climaxBoost) * modeMultiplier; 833 const effectiveIntensity = Math.max(0, Math.min(1, !isNaN(rawIntensity) && isFinite(rawIntensity) ? rawIntensity : profile.baseIntensity)); 834 const trussY = 15; 835 const halfWidth = (stageWidth || 1200) / 2; 836 const beamCount = profile.beamCount || 5; 837 const rigBeams = reactExports.useMemo(() => { 838 const beams = []; 839 const time = isPlaying && safeTime ? safeTime : 0; 840 const sweepRate = effectiveBps * 0.6 * profile.sweepSpeedMultiplier * speedMultiplier; 841 for (let i = 0; i < beamCount; i++) { 842 const spreadRatio = i / (beamCount - 1 || 1) - 0.5; 843 const originX = halfWidth + spreadRatio * stageWidth * 0.75; 844 const originY = trussY; 845 const phaseOffset = i * (Math.PI / (beamCount * 0.75)); 846 const primarySway = Math.sin(time * sweepRate + phaseOffset); 847 const harmonicSway = Math.sin(time * sweepRate * 1.5 + phaseOffset * 1.2) * 0.35; 848 const combinedSway = primarySway + harmonicSway; 849 const maxSweepPx = profile.maxBeamSpreadAngle / 45 * 220; 850 const targetX = originX + combinedSway * maxSweepPx; 851 const targetY = stageHeight - 20; 852 const beamColor = profile.beamColors[i % profile.beamColors.length]; 853 const baseBeamOpacity = 0.18 + effectiveIntensity * 0.38 + downbeatPulse * 0.18; 854 beams.push({ 855 id: `rig-beam-${i}`, 856 originX, 857 originY, 858 targetX, 859 targetY, 860 widthTop: 14 + effectiveIntensity * 8, 861 widthBottom: 85 + effectiveIntensity * 55 + (hasClimax ? 40 : 0), 862 color: beamColor, 863 opacity: Math.min(0.85, baseBeamOpacity * modeMultiplier) 864 }); 865 } 866 return beams; 867 }, [ 868 beamCount, 869 halfWidth, 870 stageWidth, 871 stageHeight, 872 trussY, 873 isPlaying, 874 safeTime, 875 effectiveBps, 876 profile, 877 speedMultiplier, 878 effectiveIntensity, 879 downbeatPulse, 880 hasClimax, 881 modeMultiplier 882 ]); 883 const characterSpotlights = reactExports.useMemo(() => { 884 const spots = []; 885 musicians.forEach((m) => { 886 const pos = musicianPositions[m.uniqueName]; 887 if (!pos) return; 888 const noteData = activeNotes[m.uniqueName]; 889 const vel = (noteData == null ? void 0 : noteData.velocity) || 0; 890 const isActive = vel > 0; 891 const isSoloed = soloedIndex === m.index; 892 const isSelected = selectedTrackIndex === m.index; 893 if (!isActive && !isSoloed && !isSelected) return; 894 const velNorm = Math.max(0.2, vel / 127); 895 const trackColor = pos.trackColor || profile.trussColor; 896 const originOffset = (pos.posX - halfWidth) * 0.55; 897 const beamOriginX = halfWidth + originOffset; 898 const beamOriginY = trussY; 899 const soloBoost = isSoloed || isSelected ? 0.35 : 0; 900 const spotOpacity = Math.min(0.95, (0.35 + velNorm * 0.45 + soloBo
900ost) * modeMultiplier); 901 const coneOpacity = Math.min(0.75, (0.18 + velNorm * 0.32 + soloBoost) * modeMultiplier); 902 const baseRadiusX = (48 + velNorm * 28) * pos.scale; 903 const baseRadiusY = (16 + velNorm * 10) * pos.scale; 904 const feetY = pos.posY + 78 * pos.scale; 905 spots.push({ 906 musicianKey: m.uniqueName, 907 posX: pos.posX, 908 posY: pos.posY, 909 feetY, 910 scale: pos.scale, 911 color: trackColor, 912 velocity: vel, 913 isSoloed, 914 isSelected, 915 spotOpacity, 916 coneOpacity, 917 puddleRadiusX: baseRadiusX * (isSoloed ? 1.25 : 1), 918 puddleRadiusY: baseRadiusY * (isSoloed ? 1.25 : 1), 919 beamOriginX, 920 beamOriginY 921 }); 922 }); 923 return spots; 924 }, [musicians, musicianPositions, activeNotes, soloedIndex, selectedTrackIndex, profile.trussColor, halfWidth, trussY, modeMultiplier]); 925 const showBlinder = profile.hasBlinders && (hasClimax || profile.hasStrobes && downbeatPulse > 0.85); 926 const blinderOpacity = hasClimax ? 0.35 : downbeatPulse * 0.22; 927 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { className: "southpark-stage-lighting pointer-events-none select-none", children: [ 928 /* @__PURE__ */ jsxRuntimeExports.jsxs("defs", { children: [
929 /* @__PURE__ */ jsxRuntimeExports.jsxs("linearGradient", { id: "spRigBeamGradSoft", x1: "0%", y1: "0%", x2: "0%", y2: "100%", children: [ 930 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "#ffffff", stopOpacity: "0.75" }), 931 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "25%", stopColor: "#ffffff", stopOpacity: "0.38" }), 932 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "55%", stopColor: "#ffffff", stopOpacity: "0.16" }), 933 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "82%", stopColor: "#ffffff", stopOpacity: "0.04" }), 934 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: "#ffffff", stopOpacity: "0.0" }) 935 ] }), 936 /* @__PURE__ */ jsxRuntimeExports.jsxs("linearGradient", { id: "spBeamGradSoft", x1: "0%", y1: "0%", x2: "0%", y2: "100%", children: [ 937 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "currentColor", stopOpacity: "0.65" }), 938 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "25%", stopColor: "currentColor", stopOpacity: "0.38" }),
939 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "55%", stopColor: "currentColor", stopOpacity: "0.16" }), 940 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "82%", stopColor: "currentColor", stopOpacity: "0.04" }), 941 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: "currentColor", stopOpacity: "0.0" }) 942 ] }), 943 /* @__PURE__ */ jsxRuntimeExports.jsxs("linearGradient", { id: "spRigBeamGrad", x1: "0%", y1: "0%", x2: "0%", y2: "100%", children: [ 944 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "#ffffff", stopOpacity: "0.75" }), 945 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "25%", stopColor: "#ffffff", stopOpacity: "0.35" }), 946 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "70%", stopColor: "#ffffff", stopOpacity: "0.1" }), 947 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: "#ffffff", stopOpacity: "0.0" }) 948 ] }), 949 /* @__PURE__ */ jsxRuntimeExports.jsxs("radialGradient", { id: "spTrussWashGrad", cx: "50%", cy: "50%", r: "50%", children: [
950 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: profile.ambientColors[0], stopOpacity: "0.65" }), 951 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "55%", stopColor: profile.ambientColors[0], stopOpacity: "0.25" }), 952 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: profile.ambientColors[0], stopOpacity: "0" }) 953 ] }), 954 /* @__PURE__ */ jsxRuntimeExports.jsxs("radialGradient", { id: "spCrossStageGrad", cx: "50%", cy: "50%", r: "50%", children: [ 955 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: profile.ambientColors[1], stopOpacity: "0.45" }), 956 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "60%", stopColor: profile.ambientColors[1], stopOpacity: "0.15" }), 957 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: profile.ambientColors[1], stopOpacity: "0" }) 958 ] }), 959 /* @__PURE__ */ jsxRuntimeExports.jsxs("radialGradient", { id: "spFloorPuddleGrad", cx: "50%", cy: "50%", r: "50%", children: [
960 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "#ffffff", stopOpacity: "0.85" }), 961 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "35%", stopColor: "#ffffff", stopOpacity: "0.35" }), 962 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "70%", stopColor: "#ffffff", stopOpacity: "0.08" }), 963 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: "#ffffff", stopOpacity: "0" }) 964 ] }), 965 characterSpotlights.map((spot) => { 966 const keySafe = spot.musicianKey.replace(/[^a-zA-Z0-9_-]/g, "_"); 967 return /* @__PURE__ */ jsxRuntimeExports.jsxs(React.Fragment, { children: [ 968 /* @__PURE__ */ jsxRuntimeExports.jsxs( 969 "linearGradient", 970 { 971 id: `spFollowBeamGrad-${keySafe}`, 972 x1: "0%", 973 y1: "0%", 974 x2: "0%", 975 y2: "100%", 976 children: [ 977 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: spot.color, stopOpacity: "0.80" }),
978 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "25%", stopColor: spot.color, stopOpacity: "0.55" }), 979 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "55%", stopColor: spot.color, stopOpacity: "0.28" }), 980 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "80%", stopColor: spot.color, stopOpacity: "0.10" }), 981 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "94%", stopColor: spot.color, stopOpacity: "0.02" }), 982 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: spot.color, stopOpacity: "0.0" }) 983 ] 984 } 985 ), 986 /* @__PURE__ */ jsxRuntimeExports.jsxs( 987 "radialGradient", 988 { 989 id: `spFloorSpotGrad-${keySafe}`, 990 cx: "50%", 991 cy: "50%", 992 r: "50%", 993 children: [ 994 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: spot.color, stopOpacity: "0.85" }),
995 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "35%", stopColor: spot.color, stopOpacity: "0.55" }), 996 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "68%", stopColor: spot.color, stopOpacity: "0.22" }), 997 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "88%", stopColor: spot.color, stopOpacity: "0.05" }), 998 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: spot.color, stopOpacity: "0.0" }) 999 ] 1000 } 1001 ) 1002 ] }, `defs-spot-${keySafe}`); 1003 }) 1004 ] }), 1005 /* @__PURE__ */ jsxRuntimeExports.jsx( 1006 "ellipse", 1007 { 1008 cx: halfWidth, 1009 cy: trussY + 40, 1010 rx: stageWidth * 0.55, 1011 ry: 90, 1012 fill: "url(#spTrussWashGrad)", 1013 opacity: 0.65 + effectiveIntensity * 0.35 1014 } 1015 ), 1016 /* @__PURE__ */ jsxRuntimeExports.jsx( 1017 "ellipse", 1018 { 1019 cx: halfWidth, 1020 cy: stageHeight * 0.65, 1021 rx: stageWidth * 0.65, 1022 ry: 140, 1023 fill: "url(#spCrossStageGrad)", 1024 opacity: 0.45 + effectiveIntensity * 0.35 1025 } 1026 ), 1027 /* @__PURE__ */ jsxRuntimeExports.jsx("g", { className: "overhead-truss-fixtures", opacity: 0.7 + effectiveIntensity * 0.3, children: rigBeams.map((beam, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { transform: `translate(${beam.originX}, ${beam.originY})`, children: [
1028 /* @__PURE__ */ jsxRuntimeExports.jsx( 1029 "polygon", 1030 { 1031 points: "-12,-8 12,-8 9,4 -9,4", 1032 fill: "#18181b", 1033 stroke: "#27272a", 1034 strokeWidth: "1.5" 1035 } 1036 ), 1037 /* @__PURE__ */ jsxRuntimeExports.jsx( 1038 "circle", 1039 { 1040 cx: "0", 1041 cy: "2", 1042 r: Math.max(1, 6 + (beatPulse || 0) * 2), 1043 fill: beam.color, 1044 fillOpacity: 0.25 1045 } 1046 ), 1047 /* @__PURE__ */ jsxRuntimeExports.jsx( 1048 "circle", 1049 { 1050 cx: "0", 1051 cy: "2", 1052 r: Math.max(1, 3 + (beatPulse || 0) * 1.5), 1053 fill: beam.color 1054 } 1055 ) 1056 ] }, `fixture-${idx}`)) }), 1057 /* @__PURE__ */ jsxRuntimeExports.jsx("g", { className: "rig-volumetric-beams", children: rigBeams.map((beam) => { 1058 const topHalf = beam.widthTop / 2; 1059 const botHalf = beam.widthBottom / 2; 1060 const points = [ 1061 `${beam.originX - topHalf},${beam.originY}`, 1062 `${beam.originX + topHalf},${beam.originY}`, 1063 `${beam.targetX + botHalf},${beam.targetY}`, 1064 `${beam.targetX - botHalf},${beam.targetY}` 1065 ].join(" "); 1066 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { opacity: beam.opacity, children: [
1067 /* @__PURE__ */ jsxRuntimeExports.jsx( 1068 "polygon", 1069 { 1070 points, 1071 color: beam.color, 1072 fill: "url(#spBeamGradSoft)", 1073 opacity: 0.8 1074 } 1075 ), 1076 /* @__PURE__ */ jsxRuntimeExports.jsx( 1077 "polygon", 1078 { 1079 points, 1080 fill: "url(#spRigBeamGradSoft)", 1081 opacity: 0.65 1082 } 1083 ) 1084 ] }, beam.id); 1085 }) }), 1086 /* @__PURE__ */ jsxRuntimeExports.jsx("g", { className: "character-follow-spots", children: characterSpotlights.map((spot) => { 1087 const keySafe = spot.musicianKey.replace(/[^a-zA-Z0-9_-]/g, "_"); 1088 const topW = 10 * spot.scale; 1089 const botW = spot.puddleRadiusX * 1.35; 1090 const coneBottomY = spot.feetY + 6 * spot.scale; 1091 const conePoints = [ 1092 `${spot.beamOriginX - topW},${spot.beamOriginY}`, 1093 `${spot.beamOriginX + topW},${spot.beamOriginY}`, 1094 `${spot.posX + botW},${coneBottomY}`, 1095 `${spot.posX - botW},${coneBottomY}` 1096 ].join(" "); 1097 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1098 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 1099 /* @__PURE__ */ jsxRuntimeExports.jsx( 1100 "polygon", 1101 { 1102 points: conePoints, 1103 fill: `url(#spFollowBeamGrad-${keySafe})`, 1104 opacity: spot.coneOpacity * 0.85 1105 } 1106 ), 1107 /* @__PURE__ */ jsxRuntimeExports.jsx( 1108 "polygon", 1109 { 1110 points: conePoints, 1111 fill: "url(#spRigBeamGradSoft)", 1112 opacity: spot.coneOpacity * 0.45 1113 } 1114 ) 1115 ] }), 1116 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 1117 /* @__PURE__ */ jsxRuntimeExports.jsx( 1118 "ellipse", 1119 { 1120 cx: spot.posX, 1121 cy: spot.feetY, 1122 rx: spot.puddleRadiusX, 1123 ry: spot.puddleRadiusY, 1124 fill: `url(#spFloorSpotGrad-${keySafe})`, 1125 opacity: spot.spotOpacity * 0.85 1126 } 1127 ), 1128 /* @__PURE__ */ jsxRuntimeExports.jsx( 1129 "ellipse", 1130 { 1131 cx: spot.posX, 1132 cy: spot.feetY, 1133 rx: spot.puddleRadiusX * 0.6, 1134 ry: spot.puddleRadiusY * 0.6, 1135 fill: "url(#spFloorPuddleGrad)", 1136 opacity: spot.spotOpacity * 0.55 1137 } 1138 ) 1139 ] }), 1140 (spot.isSoloed || spot.isSelected) && /* @__PURE__ */ jsxRuntimeExports.jsx("g", { transform: `translate(${spot.posX}, ${spot.posY - 65 * spot.scale})`, children: /* @__PURE__ */ jsxRuntimeExports.jsx( 1141 "circle", 1142 { 1143 cx: "0", 1144 cy: "0", 1145 r: 42 * spot.scale, 1146 fill: "none", 1147 stroke: spot.color, 1148 strokeWidth: "2.5", 1149 strokeDasharray: "5 3", 1150 opacity: 0.8, 1151 className: "animate-spin", 1152 style: { animationDuration: "8s" } 1153 } 1154 ) }) 1155 ] }, `spot-${spot.musicianKey}`); 1156 }) }), 1157 showBlinder && /* @__PURE__ */ jsxRuntimeExports.jsx( 1158 "rect", 1159 { 1160 x: "0", 1161 y: "0", 1162 width: stageWidth, 1163 height: stageHeight, 1164 fill: profile.blinderColor, 1165 opacity: blinderOpacity, 1166 style: { transition: "opacity 0.08s ease-out" } 1167 } 1168 ) 1169 ] }); 1170}; 1171const SouthparkStageLighting = React.memo(SouthparkStageLightingComponent); 1172const BANNER_THEMES = { 1173 // 1. Classical Opera House (Arch proscenium with red velvet & gilded carvings) 1174 classical: { 1175 themeId: "classical", 1176 hangLeft: { x: 380, y: 12 }, 1177 hangRight: { x: 820, y: 12 }, 1178 bannerX: 320, 1179 bannerY: 98, 1180 bannerWidth: 560, 1181 bannerHeight: 104, 1182 hardwareType: "silk_cords", 1183 cardStyle: { 1184 fill: "#4a0e17", 1185 stroke: "#d4af37", 1186 strokeWidth: 3.5, 1187 rx: 14, 1188 titleColor: "#fff5ea", 1189 artistColor: "#e0c068", 1190 fontFamily: 'serif, "Times New Roman", Georgia', 1191 accentColor: "#d4af37", 1192 badgeText: "OPERA HALL" 1193 } 1194 }, 1195 // 2. Stadium Concert Arena (Overhead heavy concert box truss) 1196 rock: { 1197 themeId: "rock", 1198 hangLeft: { x: 360, y: 0 }, 1199 hangRight: { x: 840, y: 0 }, 1200 bannerX: 310, 1201 bannerY: 86, 1202 bannerWidth: 580, 1203 bannerHeight: 100, 1204 hardwareType: "steel_cables", 1205 cardStyle: { 1206 fill: "#18181b", 1207 stroke: "#71717a", 1208 strokeWidth: 3, 1209 rx: 6, 1210 titleColor: "#fafafa", 1211 artistColor: "#f59e0b", 1212 fontFamily: 'Impact, "Arial Black", sans-serif', 1213 accentColor: "#e11d48", 1214 badgeText: "LIVE TOUR" 1215 } 1216 }, 1217 // 3. Country Barn (Overhead horizontal rustic timber beam) 1218 country: { 1219 themeId: "country", 1220 hangLeft: { x: 375, y: 38 }, 1221 hangRight: { x: 825, y: 38 }, 1222 bannerX: 325, 1223 bannerY: 108, 1224 bannerWidth: 550, 1225 bannerHeight: 104, 1226 hardwareType: "hemp_rope", 1227 cardStyle: { 1228 fill: "#3e2723", 1229 stroke: "#8d6e63", 1230 strokeWidth: 4, 1231 rx: 8, 1232 titleColor: "#fff8e1", 1233 artistColor: "#ffcc80", 1234 fontFamily: '"Courier New", Courier, monospace', 1235 accentColor: "#bcaaa4", 1236 badgeText: "BARN STAGE" 1237 } 1238 }, 1239 // 4. Vintage Jazz & Blues Club (Lighting pipe in front of warm brick wall) 1240 jazz: { 1241 themeId: "jazz", 1242 hangLeft: { x: 390, y: 16 }, 1243 hangRight: { x: 810, y: 16 }, 1244 bannerX: 335, 1245 bannerY: 96, 1246 bannerWidth: 530, 1247 bannerHeight: 98, 1248 hardwareType: "brass_chains", 1249 cardStyle: { 1250 fill: "#1c1917", 1251 stroke: "#b45309", 1252 strokeWidth: 3, 1253 rx: 16, 1254 titleColor: "#fef3c7", 1255 artistColor: "#f59e0b", 1256 fontFamily: '"Trebuchet MS", "Lucida Grande", sans-serif', 1257 accentColor: "#d97706", 1258 badgeText: "BLUE NOTE" 1259 } 1260 }, 1261 // 5. Techno & EDM (Spaceship bridge bulkhead ceiling looking into cosmos) 1262 techno: { 1263 themeId: "techno", 1264 hangLeft: { x: 370, y: 0 }, 1265 hangRight: { x: 830, y: 0 }, 1266 bannerX: 315, 1267 bannerY: 82, 1268 bannerWidth: 570, 1269 bannerHeight: 96, 1270 hardwareType: "laser_conduit", 1271 cardStyle: { 1272 fill: "#080e1a", 1273 stroke: "#06b6d4", 1274 strokeWidth: 2.5, 1275 rx: 8, 1276 titleColor: "#67e8f9", 1277 artistColor: "#a855f7", 1278 fontFamily: "system-ui, -apple-system, sans-serif", 1279 accentColor: "#06b6d4", 1280 badgeText: "HOLONET // 01" 1281 } 1282 }, 1283 // 6. Anime & Animation (Park pavilion bandstand arch with blue sky) 1284 anime: { 1285 themeId: "anime", 1286 hangLeft: { x: 390, y: 55 },
1287 hangRight: { x: 810, y: 55 }, 1288 bannerX: 335, 1289 bannerY: 115, 1290 bannerWidth: 530, 1291 bannerHeight: 100, 1292 hardwareType: "ribbons", 1293 cardStyle: { 1294 fill: "#fef9c3", 1295 stroke: "#38bdf8", 1296 strokeWidth: 4, 1297 rx: 24, 1298 titleColor: "#0f172a", 1299 artistColor: "#ec4899", 1300 fontFamily: '"Comic Sans MS", "Chalkboard SE", sans-serif', 1301 accentColor: "#f43f5e", 1302 badgeText: "â FESTIVAL â " 1303 } 1304 }, 1305 // 7. Underground Trap (Concrete warehouse cross-trusses & industrial beams) 1306 trap: { 1307 themeId: "trap", 1308 hangLeft: { x: 360, y: 20 }, 1309 hangRight: { x: 840, y: 20 }, 1310 bannerX: 315, 1311 bannerY: 94, 1312 bannerWidth: 570, 1313 bannerHeight: 102, 1314 hardwareType: "rusted_chains", 1315 cardStyle: { 1316 fill: "#121214", 1317 stroke: "#eab308", 1318 strokeWidth: 3, 1319 rx: 4, 1320 titleColor: "#f43f5e", 1321 artistColor: "#facc15", 1322 fontFamily: "Impact, sans-serif", 1323 accentColor: "#eab308", 1324 badgeText: "UNDERGROUND" 1325 } 1326 }, 1327 // 8. Heavy Metal (Industrial proscenium arch with stage cans & smoke) 1328 metal: { 1329 themeId: "metal", 1330 hangLeft: { x: 370, y: 5 }, 1331 hangRight: { x: 830, y: 5 }, 1332 bannerX: 310, 1333 bannerY: 88, 1334 bannerWidth: 580, 1335 bannerHeight: 104, 1336 hardwareType: "spiked_chains", 1337 cardStyle: { 1338 fill: "#09090b", 1339 stroke: "#dc2626", 1340 strokeWidth: 3.5, 1341 rx: 2, 1342 titleColor: "#ffffff", 1343 artistColor: "#ef4444", 1344 fontFamily: 'Impact, "Arial Black", sans-serif', 1345 accentColor: "#dc2626", 1346 badgeText: "METAL FEST" 1347 } 1348 }, 1349 // 9. Tropical Reggae (Beach boardwalk posts & festoon lights) 1350 reggae: { 1351 themeId: "reggae", 1352 hangLeft: { x: 330, y: 75 }, 1353 hangRight: { x: 870, y: 75 }, 1354 bannerX: 325, 1355 bannerY: 130, 1356 bannerWidth: 550, 1357 bannerHeight: 105, 1358 hardwareType: "hemp_rope", 1359 cardStyle: { 1360 fill: "#152417", 1361 stroke: "#16a34a", 1362 strokeWidth: 4, 1363 rx: 12, 1364 titleColor: "#fef08a", 1365 artistColor: "#ef4444", 1366 fontFamily: '"Trebuchet MS", sans-serif', 1367 accentColor: "#eab308", 1368 badgeText: "SUNSET ROOTS" 1369 } 1370 }, 1371 // 10. Woodland Folk (Pine forest clearing canopy trees) 1372 folk: { 1373 themeId: "folk", 1374 hangLeft: { x: 290, y: 30 }, 1375 hangRight: { x: 910, y: 30 }, 1376 bannerX: 330, 1377 bannerY: 108, 1378 bannerWidth: 540, 1379 bannerHeight: 102, 1380 hardwareType: "clothespins", 1381 cardStyle: { 1382 fill: "#292524", 1383 stroke: "#a8a29e", 1384 strokeWidth: 3.5, 1385 rx: 10, 1386 titleColor: "#f5f5f4", 1387 artistColor: "#d6d3d1", 1388 fontFamily: "Georgia, serif", 1389 accentColor: "#84cc16", 1390 badgeText: "CAMPFIRE SESSIONS" 1391 } 1392 }, 1393 // 11. Cathedral Religious (Gothic stone vaulted arches) 1394 religious: { 1395 themeId: "religious", 1396 hangLeft: { x: 390, y: 8 }, 1397 hangRight: { x: 810, y: 8 }, 1398 bannerX: 330, 1399 bannerY: 92, 1400 bannerWidth: 540, 1401 bannerHeight: 112, 1402 hardwareType: "gold_bullion", 1403 cardStyle: { 1404 fill: "#1e1b4b", 1405 stroke: "#eab308", 1406 strokeWidth: 3.5, 1407 rx: 14, 1408 titleColor: "#ffffff", 1409 artistColor: "#fef08a", 1410 fontFamily: '"Times New Roman", serif', 1411 accentColor: "#facc15", 1412 badgeText: "SACRED CHOIR" 1413 } 1414 }, 1415 // 12. Broadway Occasions (Velvet valance arch with sparkling fairy lights) 1416 occasions: { 1417 themeId: "occasions", 1418 hangLeft: { x: 380, y: 22 }, 1419 hangRight: { x: 820, y: 22 }, 1420 bannerX: 315, 1421 bannerY: 96, 1422 bannerWidth: 570, 1423 bannerHeight: 106, 1424 hardwareType: "silk_cords", 1425 cardStyle: { 1426 fill: "#31102b", 1427 stroke: "#f59e0b", 1428 strokeWidth: 4, 1429 rx: 18, 1430 titleColor: "#fffbeb", 1431 artistColor: "#fde68a", 1432 fontFamily: "Georgia, serif", 1433 accentColor: "#fbbf24", 1434 badgeText: "â GALA NIGHT â " 1435 } 1436 }, 1437 // 13. Hip Hop (Midnight alley utility power cables) 1438 hip_hop: { 1439 themeId: "hip_hop", 1440 hangLeft: { x: 340, y: 38 }, 1441 hangRight: { x: 860, y: 52 }, 1442 bannerX: 320, 1443 bannerY: 108, 1444 bannerWidth: 560, 1445 bannerHeight: 102, 1446 hardwareType: "power_lines", 1447 cardStyle: { 1448 fill: "#18181b", 1449 stroke: "#a855f7", 1450 strokeWidth: 3.5, 1451 rx: 4, 1452 titleColor: "#38bdf8", 1453 artistColor: "#f43f5e", 1454 fontFamily: 'Impact, "Arial Black", sans-serif', 1455 accentColor: "#a855f7", 1456 badgeText: "ALLEY CYPHER" 1457 } 1458 }, 1459 // 14. Pop Arena (Overhead sleek concert lighting truss) 1460 pop: { 1461 themeId: "pop", 1462 hangLeft: { x: 370, y: 0 }, 1463 hangRight: { x: 830, y: 0 }, 1464 bannerX: 320, 1465 bannerY: 86, 1466 bannerWidth: 560, 1467 bannerHeight: 98, 1468 hardwareType: "tension_wires", 1469 cardStyle: {
1470 fill: "#0f172a", 1471 stroke: "#ec4899", 1472 strokeWidth: 3, 1473 rx: 18, 1474 titleColor: "#fdf2f8", 1475 artistColor: "#38bdf8", 1476 fontFamily: "system-ui, -apple-system, sans-serif", 1477 accentColor: "#ec4899", 1478 badgeText: "POP SPECTACULAR" 1479 } 1480 }, 1481 // 15. Alternative Rock (Brick wall lighting truss) 1482 alt_rock: { 1483 themeId: "alt_rock", 1484 hangLeft: { x: 380, y: 20 }, 1485 hangRight: { x: 820, y: 20 }, 1486 bannerX: 325, 1487 bannerY: 94, 1488 bannerWidth: 550, 1489 bannerHeight: 98, 1490 hardwareType: "turnbuckles", 1491 cardStyle: { 1492 fill: "#09090b", 1493 stroke: "#8b5cf6", 1494 strokeWidth: 3, 1495 rx: 8, 1496 titleColor: "#ede9fe", 1497 artistColor: "#a78bfa", 1498 fontFamily: '"Trebuchet MS", sans-serif', 1499 accentColor: "#8b5cf6", 1500 badgeText: "INDIE ROCK" 1501 } 1502 }, 1503 // 16. Karaoke Disco (Overhead disco lighting bar) 1504 karaoke: { 1505 themeId: "karaoke", 1506 hangLeft: { x: 360, y: 15 }, 1507 hangRight: { x: 840, y: 15 }, 1508 bannerX: 320, 1509 bannerY: 90, 1510 bannerWidth: 560, 1511 bannerHeight: 104, 1512 hardwareType: "disco_streamers", 1513 cardStyle: { 1514 fill: "#1e1b4b", 1515 stroke: "#f43f5e", 1516 strokeWidth: 3.5, 1517 rx: 20, 1518 titleColor: "#fef08a", 1519 artistColor: "#38bdf8", 1520 fontFamily: '"Arial Black", sans-serif', 1521 accentColor: "#f43f5e", 1522 badgeText: "DISCO LOUNGE" 1523 } 1524 }, 1525 // 17. Latin Plaza (Colonial street balcony festoon light line) 1526 latin: { 1527 themeId: "latin", 1528 hangLeft: { x: 310, y: 50 }, 1529 hangRight: { x: 890, y: 50 }, 1530 bannerX: 325, 1531 bannerY: 118, 1532 bannerWidth: 550, 1533 bannerHeight: 105, 1534 hardwareType: "bunting", 1535 cardStyle: { 1536 fill: "#7c2d12", 1537 stroke: "#fb923c", 1538 strokeWidth: 4, 1539 rx: 16, 1540 titleColor: "#fff7ed", 1541 artistColor: "#fed7aa", 1542 fontFamily: "Georgia, serif", 1543 accentColor: "#ea580c", 1544 badgeText: "GRAN FIESTA" 1545 } 1546 }, 1547 // 18. 16-Bit Game Music (Cavern stalactite crags) 1548 game_music: { 1549 themeId: "game_music", 1550 hangLeft: { x: 345, y: 15 }, 1551 hangRight: { x: 855, y: 15 }, 1552 bannerX: 325, 1553 bannerY: 92, 1554 bannerWidth: 550, 1555 bannerHeight: 100, 1556 hardwareType: "pixel_chains", 1557 cardStyle: { 1558 fill: "#022c22", 1559 stroke: "#10b981", 1560 strokeWidth: 3.5, 1561 rx: 0, 1562 titleColor: "#a7f3d0", 1563 artistColor: "#6ee7b7", 1564 fontFamily: '"Courier New", monospace', 1565 accentColor: "#34d399", 1566 badgeText: "QUEST // LVL 99" 1567 } 1568 }, 1569 // 19. Indie Wildflower Meadow (Tree branches framing clearing) 1570 indie: { 1571 themeId: "indie", 1572 hangLeft: { x: 300, y: 45 }, 1573 hangRight: { x: 900, y: 45 }, 1574 bannerX: 330, 1575 bannerY: 114, 1576 bannerWidth: 540, 1577 bannerHeight: 100, 1578 hardwareType: "clothespins", 1579 cardStyle: { 1580 fill: "#fffbeb", 1581 stroke: "#d97706", 1582 strokeWidth: 3, 1583 rx: 12, 1584 titleColor: "#451a03", 1585 artistColor: "#b45309", 1586 fontFamily: "Georgia, serif", 1587 accentColor: "#f59e0b", 1588 badgeText: "ACOUSTIC MEADOW" 1589 } 1590 }, 1591 // 20. Sci-Fi Bioluminescent Forest (Ancient glowing tree branches) 1592 "sci-fi": { 1593 themeId: "sci-fi", 1594 hangLeft: { x: 330, y: 15 }, 1595 hangRight: { x: 870, y: 15 }, 1596 bannerX: 320, 1597 bannerY: 92, 1598 bannerWidth: 560, 1599 bannerHeight: 100, 1600 hardwareType: "vines", 1601 cardStyle: { 1602 fill: "#042f2e", 1603 stroke: "#2dd4bf", 1604 strokeWidth: 3, 1605 rx: 16, 1606 titleColor: "#ccfbf1", 1607 artistColor: "#5eead4", 1608 fontFamily: "system-ui, sans-serif", 1609 accentColor: "#2dd4bf", 1610 badgeText: "MYSTIC GROVE" 1611 } 1612 }, 1613 // 21. Ancient World Temple (Carved teakwood lintel beam) 1614 world: { 1615 themeId: "world", 1616 hangLeft: { x: 370, y: 35 }, 1617 hangRight: { x: 830, y: 35 }, 1618 bannerX: 325, 1619 bannerY: 108, 1620 bannerWidth: 550, 1621 bannerHeight: 105, 1622 hardwareType: "temple_cords", 1623 cardStyle: { 1624 fill: "#431407", 1625 stroke: "#f59e0b", 1626 strokeWidth: 3.5, 1627 rx: 8, 1628 titleColor: "#fef3c7", 1629 artistColor: "#fde68a", 1630 fontFamily: "serif", 1631 accentColor: "#d97706", 1632 badgeText: "TEMPLE COURTYARD" 1633 } 1634 }, 1635 // 22. National Ceremonial Hall (Top horizontal lighting truss) 1636 national: { 1637 themeId: "national", 1638 hangLeft: { x: 380, y: 0 }, 1639 hangRight: { x: 820, y: 0 }, 1640 bannerX: 320, 1641 bannerY: 88, 1642 bannerWidth: 560, 1643 bannerHeight: 105, 1644 hardwareType: "gold_bullion", 1645 cardStyle: {
1646 fill: "#0f172a", 1647 stroke: "#eab308", 1648 strokeWidth: 4, 1649 rx: 12, 1650 titleColor: "#f8fafc", 1651 artistColor: "#fef08a", 1652 fontFamily: '"Times New Roman", serif', 1653 accentColor: "#facc15", 1654 badgeText: "CONCERT JUBILEE" 1655 } 1656 }, 1657 // 23. Original Geometric Studio (Ceiling soffit beam) 1658 original: { 1659 themeId: "original", 1660 hangLeft: { x: 390, y: 5 }, 1661 hangRight: { x: 810, y: 5 }, 1662 bannerX: 325, 1663 bannerY: 86, 1664 bannerWidth: 550, 1665 bannerHeight: 96, 1666 hardwareType: "tension_wires", 1667 cardStyle: { 1668 fill: "#09090b", 1669 stroke: "#a1a1aa", 1670 strokeWidth: 2, 1671 rx: 6, 1672 titleColor: "#f4f4f5", 1673 artistColor: "#a1a1aa", 1674 fontFamily: "system-ui, -apple-system, sans-serif", 1675 accentColor: "#e4e4e7", 1676 badgeText: "STUDIO MASTER" 1677 } 1678 }, 1679 // 24. Midnight R&B Lounge (Velvet canopy valance) 1680 rnb: { 1681 themeId: "rnb", 1682 hangLeft: { x: 380, y: 38 }, 1683 hangRight: { x: 820, y: 38 }, 1684 bannerX: 330, 1685 bannerY: 106, 1686 bannerWidth: 540, 1687 bannerHeight: 100, 1688 hardwareType: "brass_chains", 1689 cardStyle: { 1690 fill: "#2e1065", 1691 stroke: "#c084fc", 1692 strokeWidth: 3, 1693 rx: 14, 1694 titleColor: "#faf5ff", 1695 artistColor: "#e9d5ff", 1696 fontFamily: "Georgia, serif", 1697 accentColor: "#c084fc", 1698 badgeText: "MIDNIGHT LOUNGE" 1699 } 1700 }, 1701 // 25. Reggaeton Neon Club (Warehouse ceiling truss) 1702 reggaeton: { 1703 themeId: "reggaeton", 1704 hangLeft: { x: 370, y: 20 }, 1705 hangRight: { x: 830, y: 20 }, 1706 bannerX: 315, 1707 bannerY: 96, 1708 bannerWidth: 570, 1709 bannerHeight: 102, 1710 hardwareType: "neon_cables", 1711 cardStyle: { 1712 fill: "#18181b", 1713 stroke: "#06b6d4", 1714 strokeWidth: 3.5, 1715 rx: 10, 1716 titleColor: "#f43f5e", 1717 artistColor: "#22d3ee", 1718 fontFamily: "Impact, sans-serif", 1719 accentColor: "#f43f5e", 1720 badgeText: "RITMO URBANO" 1721 } 1722 }, 1723 // Default Fallback 1724 default: { 1725 themeId: "default", 1726 hangLeft: { x: 380, y: 0 }, 1727 hangRight: { x: 820, y: 0 }, 1728 bannerX: 325, 1729 bannerY: 92, 1730 bannerWidth: 550, 1731 bannerHeight: 102, 1732 hardwareType: "hemp_rope", 1733 cardStyle: { 1734 fill: "#1c1917", 1735 stroke: "#eab308", 1736 strokeWidth: 3.5, 1737 rx: 12, 1738 titleColor: "#fef08a", 1739 artistColor: "#fde047", 1740 fontFamily: '"Trebuchet MS", sans-serif', 1741 accentColor: "#eab308", 1742 badgeText: "HAMIENET STAGE" 1743 } 1744 } 1745}; 1746function getBannerThemeId(imageUrlOrId) { 1747 if (!imageUrlOrId) return "default"; 1748 const clean = imageUrlOrId.toLowerCase().trim(); 1749 const match = clean.match(/\/([^/]+)\.(jpg|png|webp|svg)/i); 1750 const fileKey = match && match[1] ? match[1].toLowerCase() : clean; 1751 const keys = [ 1752 "classical", 1753 "rock", 1754 "country", 1755 "jazz", 1756 "techno", 1757 "anime", 1758 "trap", 1759 "metal", 1760 "reggae", 1761 "folk", 1762 "religious", 1763 "occasions", 1764 "hip_hop", 1765 "pop", 1766 "alt_rock", 1767 "karaoke", 1768 "latin", 1769 "game_music", 1770 "indie", 1771 "sci-fi", 1772 "world", 1773 "national", 1774 "original", 1775 "rnb", 1776 "reggaeton" 1777 ]; 1778 for (const k of keys) { 1779 if (fileKey.includes(k)) return k; 1780 } 1781 return "default"; 1782} 1783function renderHardwareStrand(anchor, target, type, accentColor = "#f59e0b") { 1784 const dx = target.x - anchor.x; 1785 const dy = target.y - anchor.y; 1786 const dist = Math.sqrt(dx * dx + dy * dy); 1787 if (dist <= 0) return null; 1788 switch (type) { 1789 case "steel_cables": 1790 case "tension_wires": 1791 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1792 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: anchor.x - 5, y: anchor.y, width: 10, height: 6, rx: 2, fill: "#3f3f46", stroke: "#71717a", strokeWidth: 1 }), 1793 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y + 6, x2: target.x, y2: target.y - 6, stroke: "#a1a1aa", strokeWidth: 2.5 }), 1794 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y + 6, x2: target.x, y2: target.y - 6, stroke: "#ffffff", strokeWidth: 1, strokeDasharray: "3 3", opacity: 0.6 }), 1795 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: target.x - 4, y: target.y - 12, width: 8, height: 10, rx: 2, fill: "#71717a", stroke: "#27272a", strokeWidth: 1 }), 1796 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 5, fill: "none", stroke: "#d4d4d8", strokeWidth: 2 }) 1797 ] }, `cable-${anchor.x}-${target.x}`); 1798 case "turnbuckles": 1799 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 1800 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x, cy: anchor.y + 2, r: 4, fill: "#52525b" }),
1801 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y + 4, x2: target.x, y2: target.y - 14, stroke: "#71717a", strokeWidth: 2 }), 1802 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: target.x - 4.5, y: target.y - 14, width: 9, height: 12, rx: 2, fill: "#27272a", stroke: "#a1a1aa", strokeWidth: 1.5 }), 1803 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 4.5, fill: "none", stroke: "#a855f7", strokeWidth: 2 }) 1804 ] }, `turnbuckle-${anchor.x}-${target.x}`); 1805 case "brass_chains": 1806 case "spiked_chains": 1807 case "rusted_chains": { 1808 const isSpiked = type === "spiked_chains"; 1809 const linkColor = type === "brass_chains" ? "#d97706" : isSpiked ? "#451a03" : "#78716c"; 1810 const highlightColor = type === "brass_chains" ? "#fde68a" : isSpiked ? "#dc2626" : "#d6d3d1"; 1811 const numLinks = Math.max(3, Math.floor(dist / 14)); 1812 const links = []; 1813 for (let i = 0; i < numLinks; i++) { 1814 const t = i / (numLinks - 1); 1815 const lx = anchor.x + dx * t; 1816 const ly = anchor.y + dy * t; 1817 const isAlt = i % 2 === 0; 1818 links.push(
1819 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 1820 /* @__PURE__ */ jsxRuntimeExports.jsx( 1821 "ellipse", 1822 { 1823 cx: lx, 1824 cy: ly, 1825 rx: isAlt ? 3.5 : 2, 1826 ry: isAlt ? 6.5 : 8, 1827 fill: "none", 1828 stroke: linkColor, 1829 strokeWidth: 2 1830 } 1831 ), 1832 /* @__PURE__ */ jsxRuntimeExports.jsx( 1833 "ellipse", 1834 { 1835 cx: lx, 1836 cy: ly, 1837 rx: isAlt ? 2 : 1, 1838 ry: isAlt ? 4.5 : 5.5, 1839 fill: "none", 1840 stroke: highlightColor, 1841 strokeWidth: 0.7, 1842 opacity: 0.7 1843 } 1844 ), 1845 isSpiked && isAlt && /* @__PURE__ */ jsxRuntimeExports.jsx( 1846 "polygon", 1847 { 1848 points: `${lx - 5},${ly} ${lx},${ly - 4} ${lx + 5},${ly} ${lx},${ly + 4}`, 1849 fill: "#b91c1c" 1850 } 1851 ) 1852 ] }, `link-${i}`) 1853 ); 1854 } 1855 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1856 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x, cy: anchor.y, r: 4.5, fill: linkColor }), 1857 links, 1858 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 5.5, fill: "none", stroke: linkColor, strokeWidth: 2.5 }) 1859 ] }, `chain-${anchor.x}-${target.x}`); 1860 } 1861 case "silk_cords": 1862 case "gold_bullion": { 1863 const cordColor = type === "silk_cords" ? "#eab308" : "#fbbf24"; 1864 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 1865 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x, cy: anchor.y, r: 6, fill: cordColor }), 1866 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x, cy: anchor.y, r: 3, fill: "#78350f" }), 1867 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x - 1.5, y1: anchor.y, x2: target.x - 1.5, y2: target.y, stroke: cordColor, strokeWidth: 2 }), 1868 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x + 1.5, y1: anchor.y, x2: target.x + 1.5, y2: target.y, stroke: cordColor, strokeWidth: 2 }),
1869 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#78350f", strokeWidth: 1, strokeDasharray: "3 3" }), 1870 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 6, fill: "none", stroke: cordColor, strokeWidth: 2.5 }), 1871 /* @__PURE__ */ jsxRuntimeExports.jsx( 1872 "path", 1873 { 1874 d: `M ${target.x - 3.5} ${target.y + 4} L ${target.x + 3.5} ${target.y + 4} L ${target.x + 5} ${target.y + 22} L ${target.x - 5} ${target.y + 22} Z`, 1875 fill: cordColor 1876 } 1877 ), 1878 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: target.x - 3, y1: target.y + 22, x2: target.x - 3, y2: target.y + 27, stroke: cordColor, strokeWidth: 1.5 }), 1879 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: target.x, y1: target.y + 22, x2: target.x, y2: target.y + 28, stroke: cordColor, strokeWidth: 1.5 }), 1880 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: target.x + 3, y1: target.y + 22, x2: target.x + 3, y2: target.y + 27, stroke: cordColor, strokeWidth: 1.5 }) 1881 ] }, `silk-${anchor.x}-${target.x}`); 1882 } 1883 case "ribbons": 1884 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1885 /* @__PURE__ */ jsxRuntimeExports.jsx( 1886 "path", 1887 { 1888 d: `M ${anchor.x} ${anchor.y} C ${anchor.x - 16} ${anchor.y - 12}, ${anchor.x - 16} ${anchor.y + 12}, ${anchor.x} ${anchor.y} C ${anchor.x + 16} ${anchor.y - 12}, ${anchor.x + 16} ${anchor.y + 12}, ${anchor.x} ${anchor.y}`, 1889 fill: "#f472b6", 1890 stroke: "#ec4899", 1891 strokeWidth: 1.5 1892 } 1893 ), 1894 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x, cy: anchor.y, r: 3.5, fill: "#fbcfe8" }), 1895 /* @__PURE__ */ jsxRuntimeExports.jsx( 1896 "path", 1897 { 1898 d: `M ${anchor.x} ${anchor.y} Q ${anchor.x + (target.x > anchor.x ? 8 : -8)} ${(anchor.y + target.y) / 2} ${target.x} ${target.y}`, 1899 fill: "none", 1900 stroke: "#f472b6", 1901 strokeWidth: 3.5 1902 } 1903 ), 1904 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 4.5, fill: "#fb7185" }) 1905 ] }, `ribbon-${anchor.x}-${target.x}`); 1906 case "laser_conduit": 1907 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1908 /* @__PURE__ */ jsxRuntimeExports.jsx( 1909 "polygon", 1910 { 1911 points: `${anchor.x - 6},${anchor.y} ${anchor.x + 6},${anchor.y} ${anchor.x},${anchor.y + 8}`, 1912 fill: "#06b6d4" 1913 } 1914 ), 1915 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y + 6, x2: target.x, y2: target.y, stroke: "#06b6d4", strokeWidth: 5, opacity: 0.3 }), 1916 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y + 6, x2: target.x, y2: target.y, stroke: "#a5f3fc", strokeWidth: 2 }), 1917 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: (anchor.x + target.x) / 2, cy: (anchor.y + target.y) / 2, r: 3.5, fill: "#38bdf8" }), 1918 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 5, fill: "#0891b2", stroke: "#67e8f9", strokeWidth: 1.5 }) 1919 ] }, `laser-${anchor.x}-${target.x}`); 1920 case "vines": 1921 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 1922 /* @__PURE__ */ jsxRuntimeExports.jsx( 1923 "path", 1924 { 1925 d: `M ${anchor.x} ${anchor.y} Q ${anchor.x - 12} ${(anchor.y + target.y) * 0.4} ${anchor.x + 6} ${(anchor.y + target.y) * 0.7} T ${target.x} ${target.y}`, 1926 fill: "none", 1927 stroke: "#0d9488", 1928 strokeWidth: 3 1929 } 1930 ),
1931 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x - 8, cy: (anchor.y + target.y) * 0.38, r: 2.5, fill: "#5eead4" }), 1932 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x + 4, cy: (anchor.y + target.y) * 0.68, r: 3, fill: "#99f6e4" }), 1933 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 4.5, fill: "#14b8a6" }) 1934 ] }, `vines-${anchor.x}-${target.x}`); 1935 case "power_lines": 1936 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 1937 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#27272a", strokeWidth: 3.5 }), 1938 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: target.x - 6, y: target.y - 12, width: 12, height: 8, rx: 1, fill: "#a1a1aa", transform: `rotate(-12 ${target.x} ${target.y - 8})` }), 1939 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 4, fill: "#52525b" }) 1940 ] }, `power-${anchor.x}-${target.x}`); 1941 case "disco_streamers": 1942 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
1943 /* @__PURE__ */ jsxRuntimeExports.jsx( 1944 "path", 1945 { 1946 d: `M ${anchor.x} ${anchor.y} Q ${anchor.x + 10} ${(anchor.y + target.y) * 0.4} ${anchor.x - 8} ${(anchor.y + target.y) * 0.7} T ${target.x} ${target.y}`, 1947 fill: "none", 1948 stroke: "#ec4899", 1949 strokeWidth: 2.5, 1950 strokeDasharray: "4 2" 1951 } 1952 ), 1953 /* @__PURE__ */ jsxRuntimeExports.jsx( 1954 "path", 1955 { 1956 d: `M ${anchor.x + 4} ${anchor.y} Q ${anchor.x - 6} ${(anchor.y + target.y) * 0.5} ${target.x + 4} ${target.y}`, 1957 fill: "none", 1958 stroke: "#38bdf8", 1959 strokeWidth: 2 1960 } 1961 ), 1962 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 5, fill: "#f43f5e" }) 1963 ] }, `streamers-${anchor.x}-${target.x}`); 1964 case "clothespins": 1965 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 1966 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#a8a29e", strokeWidth: 2, strokeDasharray: "3 1" }),
1967 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: target.x - 3, y: target.y - 10, width: 6, height: 16, rx: 1.5, fill: "#d6d3d1", stroke: "#78716c", strokeWidth: 1 }), 1968 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: target.x - 2, y1: target.y - 3, x2: target.x + 2, y2: target.y - 3, stroke: "#57534e", strokeWidth: 1 }) 1969 ] }, `peg-${anchor.x}-${target.x}`); 1970 case "bunting": 1971 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 1972 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#fb923c", strokeWidth: 2.5 }), 1973 /* @__PURE__ */ jsxRuntimeExports.jsx( 1974 "polygon", 1975 { 1976 points: `${(anchor.x + target.x) / 2 - 6},${(anchor.y + target.y) / 2} ${(anchor.x + target.x) / 2 + 6},${(anchor.y + target.y) / 2} ${(anchor.x + target.x) / 2},${(anchor.y + target.y) / 2 + 10}`, 1977 fill: "#f43f5e" 1978 } 1979 ), 1980 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 4, fill: "#ea580c" }) 1981 ] }, `bunting-${anchor.x}-${target.x}`); 1982 case "pixel_chains": { 1983 const numSteps = Math.max(3, Math.floor(dist / 14)); 1984 const steps = []; 1985 for (let i = 0; i < numSteps; i++) { 1986 const t = i / (numSteps - 1); 1987 const px = Math.round(anchor.x + dx * t); 1988 const py = Math.round(anchor.y + dy * t); 1989 steps.push(
1990 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: px - 2, y: py - 3, width: 5, height: 7, fill: "#10b981", stroke: "#064e3b", strokeWidth: 1 }, `pixel-${i}`) 1991 ); 1992 } 1993 return /* @__PURE__ */ jsxRuntimeExports.jsx("g", { children: steps }, `pxchain-${anchor.x}-${target.x}`); 1994 } 1995 case "temple_cords": 1996 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 1997 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#d97706", strokeWidth: 3, strokeDasharray: "4 2" }), 1998 /* @__PURE__ */ jsxRuntimeExports.jsx( 1999 "path", 2000 { 2001 d: `M ${target.x - 4} ${target.y - 4} Q ${target.x} ${target.y - 8} ${target.x + 4} ${target.y - 4} L ${target.x + 5} ${target.y + 4} L ${target.x - 5} ${target.y + 4} Z`, 2002 fill: "#f59e0b", 2003 stroke: "#78350f", 2004 strokeWidth: 1 2005 } 2006 ), 2007 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y + 6, r: 1.5, fill: "#451a03" }) 2008 ] }, `temple-${anchor.x}-${target.x}`); 2009 case "neon_cables": 2010 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
2011 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#06b6d4", strokeWidth: 4, opacity: 0.4 }), 2012 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#f43f5e", strokeWidth: 2 }), 2013 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 5, fill: "#06b6d4" }) 2014 ] }, `neoncable-${anchor.x}-${target.x}`); 2015 case "hemp_rope": 2016 default: 2017 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 2018 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: anchor.x, cy: anchor.y, r: 5, fill: "#78350f", stroke: "#d97706", strokeWidth: 1.5 }), 2019 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#b45309", strokeWidth: 3.5 }), 2020 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: anchor.x, y1: anchor.y, x2: target.x, y2: target.y, stroke: "#fef3c7", strokeWidth: 1, strokeDasharray: "3 3", opacity: 0.7 }),
2021 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 6, fill: "#451a03", stroke: "#f59e0b", strokeWidth: 2 }), 2022 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: target.x, cy: target.y, r: 2.5, fill: "#18181b" }) 2023 ] }, `rope-${anchor.x}-${target.x}`); 2024 } 2025} 2026const SouthparkStageBannerComponent = ({ 2027 track, 2028 backgroundUrl, 2029 currentTime = 0, 2030 bpm = 120, 2031 isPlaying = false, 2032 globalIntensity = 0, 2033 showBanner = true 2034}) => { 2035 const themeKey = reactExports.useMemo(() => getBannerThemeId(backgroundUrl), [backgroundUrl]); 2036 const preset = reactExports.useMemo(() => BANNER_THEMES[themeKey] || BANNER_THEMES.default, [themeKey]); 2037 const rawTitle = ((track == null ? void 0 : track.title) || "HamieNET Orchestra").trim(); 2038 const rawArtist = ((track == null ? void 0 : track.artist) || "").trim(); 2039 const isPlaceholderArtist = !rawArtist || rawArtist.toLowerCase() === "unknown" || rawArtist.toLowerCase() === "unknown artist" || rawArtist.toLowerCase() === "null" || rawArtist.toLowerCase() === "undefined"; 2040 const cleanArtist = isPlaceholderArtist ? null : rawArtist; 2041 let displayTitle = rawTitle; 2042 let titleFontSize = 21; 2043 if (displayTitle.length > 38) { 2044 displayTitle = displayTitle.slice(0, 36) + "â¦"; 2045 titleFontSize = 16; 2046 } else if (displayTitle.length > 26) { 2047 titleFontSize = 18; 2048 } else if (displayTitle.length > 18) { 2049 titleFontSize = 19.5; 2050 } 2051 let displayArtist = cleanArtist; 2052 let artistFontSize = 13.5; 2053 if (displayArtist && displayArtist.length > 34) { 2054 displayArtist = displayArtist.slice(0, 32) + "â¦"; 2055 artistFontSize = 12; 2056 } 2057 const { bannerX, bannerY, bannerWidth, bannerHeight, cardStyle, hangLeft, hangRight, hardwareType } = preset; 2058 const centerX = bannerX + bannerWidth / 2; 2059 const currentBannerY = bannerY; 2060 const attachInset = 28; 2061 const attachLeft = { x: bannerX + attachInset, y: currentBannerY + 10 }; 2062 const attachRight = { x: bannerX + bannerWidth - attachInset, y: currentBannerY + 10 }; 2063 if (!showBanner) return null; 2064 const swayDuration = Math.max(1.5, 60 / (bpm || 120) * 2); 2065 return /* @__PURE__ */ jsxRuntimeExports.jsxs( 2066 "g", 2067 { 2068 className: "southpark-stage-banner select-none pointer-events-none", 2069 style: { 2070 transformOrigin: `${centerX}px ${Math.min(hangLeft.y, hangRight.y)}px`, 2071 animation: isPlaying ? `spBannerSwayAnim ${swayDuration.toFixed(2)}s ease-in-out infinite alternate` : "none", 2072 willChange: "transform" 2073 }, 2074 children: [
2075 /* @__PURE__ */ jsxRuntimeExports.jsxs("defs", { children: [ 2076 /* @__PURE__ */ jsxRuntimeExports.jsx("style", { children: ` 2077 @keyframes spBannerSwayAnim { 2078 0% { transform: rotate(-0.85deg); } 2079 100% { transform: rotate(0.85deg); } 2080 } 2081 ` }), 2082 /* @__PURE__ */ jsxRuntimeExports.jsxs("linearGradient", { id: "spBannerGoldGrad", x1: "0%", y1: "0%", x2: "100%", y2: "100%", children: [ 2083 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "0%", stopColor: "#fef08a" }), 2084 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "50%", stopColor: "#eab308" }), 2085 /* @__PURE__ */ jsxRuntimeExports.jsx("stop", { offset: "100%", stopColor: "#ca8a04" }) 2086 ] }) 2087 ] }), 2088 renderHardwareStrand(hangLeft, attachLeft, hardwareType, cardStyle.accentColor), 2089 renderHardwareStrand(hangRight, attachRight, hardwareType, cardStyle.accentColor), 2090 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [
2091 /* @__PURE__ */ jsxRuntimeExports.jsx( 2092 "rect", 2093 { 2094 x: bannerX, 2095 y: currentBannerY + 5, 2096 width: bannerWidth, 2097 height: bannerHeight, 2098 rx: cardStyle.rx, 2099 fill: "rgba(0, 0, 0, 0.4)" 2100 } 2101 ), 2102 /* @__PURE__ */ jsxRuntimeExports.jsx( 2103 "rect", 2104 { 2105 x: bannerX, 2106 y: currentBannerY, 2107 width: bannerWidth, 2108 height: bannerHeight, 2109 rx: cardStyle.rx, 2110 fill: cardStyle.fill, 2111 stroke: cardStyle.stroke, 2112 strokeWidth: cardStyle.strokeWidth, 2113 opacity: 0.96 2114 } 2115 ), 2116 /* @__PURE__ */ jsxRuntimeExports.jsx( 2117 "rect", 2118 { 2119 x: bannerX + 5, 2120 y: currentBannerY + 5, 2121 width: bannerWidth - 10, 2122 height: bannerHeight - 10, 2123 rx: Math.max(2, cardStyle.rx - 4), 2124 fill: "none", 2125 stroke: cardStyle.stroke, 2126 strokeWidth: 1, 2127 strokeDasharray: themeKey === "folk" || themeKey === "country" ? "4 3" : void 0, 2128 opacity: 0.4 2129 } 2130 ), 2131 themeKey === "occasions" && // Broadway Marquee Lights around the perimeter
2132 /* @__PURE__ */ jsxRuntimeExports.jsx("g", { children: [...Array(16)].map((_, i) => { 2133 const bulbX = bannerX + 16 + i * ((bannerWidth - 32) / 15); 2134 return /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 2135 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bulbX, cy: currentBannerY + 7, r: 2.5, fill: "#fef08a" }), 2136 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bulbX, cy: currentBannerY + 7, r: 4.5, fill: "#fef08a", opacity: 0.3 }), 2137 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bulbX, cy: currentBannerY + bannerHeight - 7, r: 2.5, fill: "#fef08a" }), 2138 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bulbX, cy: currentBannerY + bannerHeight - 7, r: 4.5, fill: "#fef08a", opacity: 0.3 }) 2139 ] }, `bulb-top-${i}`); 2140 }) }), 2141 themeKey === "anime" && // Cute pastel decorative stars on corners 2142 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { fill: "#38bdf8", children: [ 2143 /* @__P
2143URE__ */ jsxRuntimeExports.jsx("path", { d: `M ${bannerX + 18} ${currentBannerY + 16} l 2 4 l 4 1 l -3 3 l 1 4 l -4 -2 l -4 2 l 1 -4 l -3 -3 l 4 -1 Z` }), 2144 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: `M ${bannerX + bannerWidth - 18} ${currentBannerY + 16} l 2 4 l 4 1 l -3 3 l 1 4 l -4 -2 l -4 2 l 1 -4 l -3 -3 l 4 -1 Z` }) 2145 ] }), 2146 themeKey === "trap" && // Yellow/Black Caution Hazard Stripes along edges 2147 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 2148 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: bannerX + 4, y: currentBannerY + 4, width: bannerWidth - 8, height: 5, fill: "#eab308" }), 2149 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: bannerX + 4, y: currentBannerY + bannerHeight - 9, width: bannerWidth - 8, height: 5, fill: "#eab308" }) 2150 ] }), 2151 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bannerX + 12, cy: currentBannerY + 12, r: 3, fill: cardStyle.stroke, opacity: 0.8 }), 2152 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bannerX + bannerWidth - 12, cy: currentBannerY + 12, r: 3, fill: cardStyle.stroke, opacity: 0.8 }),
2153 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bannerX + 12, cy: currentBannerY + bannerHeight - 12, r: 3, fill: cardStyle.stroke, opacity: 0.8 }), 2154 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: bannerX + bannerWidth - 12, cy: currentBannerY + bannerHeight - 12, r: 3, fill: cardStyle.stroke, opacity: 0.8 }), 2155 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { id: "hamienet-banner-crest", children: [ 2156 /* @__PURE__ */ jsxRuntimeExports.jsx( 2157 "circle", 2158 { 2159 cx: centerX, 2160 cy: currentBannerY - 14, 2161 r: 18, 2162 fill: cardStyle.fill, 2163 stroke: cardStyle.stroke, 2164 strokeWidth: 2 2165 } 2166 ), 2167 /* @__PURE__ */ jsxRuntimeExports.jsx( 2168 "circle", 2169 { 2170 cx: centerX, 2171 cy: currentBannerY - 14, 2172 r: 15.5, 2173 fill: "none", 2174 stroke: cardStyle.stroke, 2175 strokeWidth: 0.75, 2176 strokeDasharray: "2 1.5", 2177 opacity: 0.8 2178 } 2179 ),
2180 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { transform: `translate(${centerX - 13.5}, ${currentBannerY - 26}) scale(1.15)`, children: [ 2181 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M4.5 14C4.5 12.067 6.067 10.5 8 10.5H16C17.933 10.5 19.5 12.067 19.5 14V17.5C19.5 18.8807 18.3807 20 17 20H7C5.61929 20 4.5 18.8807 4.5 17.5V14Z", fill: "#EAD8C1" }), 2182 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M8 10.5C8 8.01472 10.0147 6 12.5 6H11.5C9.01472 6 7 8.01472 7 10.5C7 11.3284 7.67157 12 8.5 12H8C7.44772 12 7 11.5523 7 11V10.5C7 9.11929 8.11929 8 9.5 8H14.5C15.8807 8 17 9.11929 17 10.5V11C17 11.5523 16.5523 12 16 12H15.5C16.3284 12 17 11.3284 17 10.5C17 8.01472 14.9853 6 12.5 6H11.5C9.01472 6 7 8.01472 7 10.5", fill: "#D1B897" }), 2183 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "9.5", cy: "14.5", r: "1.1", fill: "black" }), 2184 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: "14.5", cy: "14.5", r: "1.1", fill: "black" }), 2185 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M12 16.5C12.5 17.5 11.5 17.5 12 16.5Z", stroke: "black", strokeWidth: "0.5", strokeLinecap: "round" }),
2186 /* @__PURE__ */ jsxRuntimeExports.jsx("path", { d: "M2 11L12 7L22 11L12 15L2 11Z", fill: "#1E293B" }), 2187 /* @__PURE__ */ jsxRuntimeExports.jsx("rect", { x: "11", y: "4", width: "2", height: "3", rx: "0.5", fill: "#1E293B" }), 2188 /* @__PURE__ */ jsxRuntimeExports.jsx("line", { x1: "21", y1: "11", x2: "23", y2: "11", stroke: "#FBBF24", strokeWidth: "1.5", strokeLinecap: "round" }) 2189 ] }), 2190 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 2191 /* @__PURE__ */ jsxRuntimeExports.jsx( 2192 "rect", 2193 { 2194 x: centerX - (cardStyle.badgeText ? 82 : 55), 2195 y: currentBannerY - 1, 2196 width: cardStyle.badgeText ? 164 : 110, 2197 height: 17, 2198 rx: 4, 2199 fill: cardStyle.fill, 2200 stroke: cardStyle.stroke, 2201 strokeWidth: 1.5 2202 } 2203 ), 2204 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: centerX - (cardStyle.badgeText ? 75 : 48), cy: currentBannerY + 7.5, r: 1.5, fill: cardStyle.stroke }),
2205 /* @__PURE__ */ jsxRuntimeExports.jsx("circle", { cx: centerX + (cardStyle.badgeText ? 75 : 48), cy: currentBannerY + 7.5, r: 1.5, fill: cardStyle.stroke }), 2206 /* @__PURE__ */ jsxRuntimeExports.jsx( 2207 "text", 2208 { 2209 x: centerX, 2210 y: currentBannerY + 11, 2211 textAnchor: "middle", 2212 fill: cardStyle.accentColor || cardStyle.stroke, 2213 fontSize: "8.5", 2214 fontWeight: "900", 2215 letterSpacing: "1.2", 2216 fontFamily: "system-ui, sans-serif", 2217 children: cardStyle.badgeText ? `HAMIENET ⢠${cardStyle.badgeText}` : "HAMIENET" 2218 } 2219 ) 2220 ] }) 2221 ] }), 2222 cleanArtist ? ( 2223 // Song Title + Artist Layout 2224 /* @__PURE__ */ jsxRuntimeExports.jsxs("g", { children: [ 2225 /* @__PURE__ */ jsxRuntimeExports.jsx( 2226 "text", 2227 { 2228 x: centerX, 2229 y: currentBannerY + bannerHeight * 0.44, 2230 textAnchor: "middle", 2231 dominantBaseline: "central", 2232 fill: cardStyle.titleColor, 2233 fontSize: titleFontSize, 2234 fontWeight: "800", 2235 letterSpacing: themeKey === "techno" ? "1.5" : "0.5", 2236 fontFamily: cardStyle.fontFamily, 2237 style: { textShadow: "0 2px 4px rgba(0,0,0,0.5)" }, 2238 children: displayTitle 2239 } 2240 ),
2241 /* @__PURE__ */ jsxRuntimeExports.jsx( 2242 "line", 2243 { 2244 x1: centerX - 80, 2245 y1: currentBannerY + bannerHeight * 0.63, 2246 x2: centerX + 80, 2247 y2: currentBannerY + bannerHeight * 0.63, 2248 stroke: cardStyle.stroke, 2249 strokeWidth: 1, 2250 opacity: 0.5 2251 } 2252 ), 2253 /* @__PURE__ */ jsxRuntimeExports.jsx( 2254 "text", 2255 { 2256 x: centerX, 2257 y: currentBannerY + bannerHeight * 0.79, 2258 textAnchor: "middle", 2259 dominantBaseline: "central", 2260 fill: cardStyle.artistColor, 2261 fontSize: artistFontSize, 2262 fontWeight: "600", 2263 letterSpacing: "0.5", 2264 fontFamily: cardStyle.fontFamily, 2265 opacity: 0.9, 2266 style: { textShadow: "0 1px 3px rgba(0,0,0,0.4)" }, 2267 children: displayArtist 2268 } 2269 ) 2270 ] }) 2271 ) : ( 2272 // Song Title Only (centered vertically when no artist is available)
2273 /* @__PURE__ */ jsxRuntimeExports.jsx("g", { children: /* @__PURE__ */ jsxRuntimeExports.jsx( 2274 "text", 2275 { 2276 x: centerX, 2277 y: currentBannerY + bannerHeight * 0.54, 2278 textAnchor: "middle", 2279 dominantBaseline: "central", 2280 fill: cardStyle.titleColor, 2281 fontSize: titleFontSize * 1.15, 2282 fontWeight: "800", 2283 letterSpacing: themeKey === "techno" ? "1.5" : "0.5", 2284 fontFamily: cardStyle.fontFamily, 2285 style: { textShadow: "0 2px 5px rgba(0,0,0,0.6)" }, 2286 children: displayTitle 2287 } 2288 ) }) 2289 ) 2290 ] }) 2291 ] 2292 } 2293 ); 2294}; 2295const SouthparkStageBanner = React.memo(SouthparkStageBannerComponent); 2296export { 2297 SouthparkStageBanner as S, 2298 SouthparkStageLighting as a, 2299 calculateEnsemblePlacement as c, 2300 defaultSouthparkStageConfig as d, 2301 getSouthparkStageAlignment as g, 2302 resolveLightingProfile as r, 2303 useSouthparkStageBackground as u 2304};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.