Skip to content

Instantly share code, notes, and snippets.

@adam137016
Created April 25, 2026 13:38
Show Gist options
  • Select an option

  • Save adam137016/001ed4bfa393a8f33ca8e7d020e4b69d to your computer and use it in GitHub Desktop.

Select an option

Save adam137016/001ed4bfa393a8f33ca8e7d020e4b69d to your computer and use it in GitHub Desktop.
Playlist Poster Generator for Sonic Dashboard
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Plex Sonic Auto Playlist Poster Generator</title>
<!--
═══════════════════════════════════════════════════════════════════════════════
PLEX SONIC AUTO PLAYLIST β€” POSTER GENERATOR
Companion tool for sonic_dashboard.py
https://www.reddit.com/r/plexamp/comments/1sjs669/
═══════════════════════════════════════════════════════════════════════════════
WHAT THIS DOES:
Generates custom retro-style poster art for each of your Auto: playlists
so they display a unique image instead of the random album collage Plex
generates by default.
HOW TO USE:
1. Open this file in Chrome or Edge (Firefox may block multi-downloads).
2. Click "Download All Posters" β€” Chrome will ask permission for multiple
downloads the first time. Click Allow.
3. All PNG files will download to your Downloads folder.
4. Create a folder for them, e.g.:
Windows: C:\Users\YourName\Desktop\playlist_posters\
Mac/Linux: ~/playlist_posters/
5. Move all the PNG files into that folder.
APPLYING POSTERS IN PLEX (manual):
Music β†’ Playlists β†’ click Β·Β·Β· on a playlist β†’ Edit
β†’ click the thumbnail β†’ Upload Poster β†’ select the matching PNG.
APPLYING POSTERS AUTOMATICALLY (recommended):
Use the POSTER_ENABLED option in sonic_dashboard.py.
Set POSTER_ENABLED = True and POSTER_FOLDER to your folder path.
The dashboard will re-apply all posters automatically after every run,
so they never revert to the random album collage.
CUSTOMIZING:
- Each playlist entry in the PLAYLISTS array has a "enabled" flag.
Set enabled:false for genres you don't use β€” they'll still generate
but appear in a separate "Disabled" section for future use.
- "Regenerate All" creates fresh variations of each poster (slight
randomness is baked into each design).
- Posters are 600Γ—600px PNG, ideal for Plex playlist art.
POSTER FILENAMES:
Files are named like: Auto_Punk_Hardcore.png
The sonic_dashboard.py script matches these to playlists automatically
using fuzzy name matching, so minor differences are fine.
═══════════════════════════════════════════════════════════════════════════════
-->
<style>
@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Playfair+Display:ital,wght@0,700;1,400&family=Special+Elite&family=Oswald:wght@300;700&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: #111008;
color: #e8d5a3;
font-family: 'Special Elite', monospace;
min-height: 100vh;
padding: 30px 20px 60px;
}
h1 {
font-family: 'Bebas Neue', sans-serif;
font-size: 3rem;
letter-spacing: 0.15em;
text-align: center;
color: #f5c842;
text-shadow: 3px 3px 0 #7a3d00, 6px 6px 0 #3a1a00;
margin-bottom: 6px;
}
.subtitle {
text-align: center;
font-size: 0.8rem;
color: #a08860;
letter-spacing: 0.2em;
text-transform: uppercase;
margin-bottom: 8px;
}
.info-bar {
text-align: center;
font-size: 0.75rem;
color: #6a5830;
margin-bottom: 28px;
font-family: 'Special Elite', monospace;
}
.top-controls {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 28px;
}
button {
font-family: 'Bebas Neue', sans-serif;
letter-spacing: 0.12em;
font-size: 1rem;
border: 2px solid #f5c842;
background: transparent;
color: #f5c842;
padding: 10px 22px;
cursor: pointer;
transition: all 0.15s;
}
button:hover { background: #f5c842; color: #1a1208; }
button.primary { background: #f5c842; color: #1a1208; }
button.primary:hover { background: #ffd700; border-color: #ffd700; }
.section-label {
font-family: 'Bebas Neue', sans-serif;
letter-spacing: 0.2em;
font-size: 1.1rem;
color: #f5c842;
border-bottom: 1px solid #3a2a00;
padding-bottom: 8px;
margin: 0 auto 20px;
max-width: 1400px;
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
max-width: 1400px;
margin: 0 auto 40px;
}
.poster-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.poster-wrap {
position: relative;
width: 280px;
height: 280px;
cursor: pointer;
transition: transform 0.2s;
}
.poster-wrap:hover { transform: scale(1.03); }
canvas {
width: 280px;
height: 280px;
display: block;
box-shadow: 0 8px 32px rgba(0,0,0,0.7), 0 0 0 1px #3a2a00;
}
.poster-label {
font-family: 'Bebas Neue', sans-serif;
letter-spacing: 0.08em;
color: #c8aa78;
font-size: 0.9rem;
text-align: center;
}
.dl-btn {
font-family: 'Special Elite', monospace;
font-size: 0.72rem;
border: 1px solid #5a4020;
background: #1a1008;
color: #c8aa78;
padding: 5px 14px;
cursor: pointer;
letter-spacing: 0.06em;
}
.dl-btn:hover { border-color: #f5c842; color: #f5c842; }
.instructions {
max-width: 900px;
margin: 10px auto 0;
padding: 22px 26px;
border: 1px solid #3a2a00;
background: #0c0900;
font-size: 0.8rem;
line-height: 1.9;
color: #a08860;
}
.instructions strong { color: #f5c842; }
.instructions code {
background: #1a1208;
border: 1px solid #3a2a00;
padding: 2px 6px;
font-family: 'Courier Prime', monospace;
color: #c8f0a0;
font-size: 0.78rem;
}
</style>
</head>
<body>
<h1>Playlist Poster Generator</h1>
<p class="subtitle">Sonic Auto Playlists β€” Retro Vintage Series</p>
<p class="info-bar">32 genres Β· Click any poster to download individually Β· Posters are 600Γ—600px PNG Β· Use Chrome or Edge for best download support</p>
<div class="top-controls">
<button class="primary" onclick="renderAll()">🎨 Regenerate All</button>
<button onclick="downloadAll()">⬇ Download All 32 Posters</button>
<button onclick="downloadEnabled()">⬇ Download Enabled Only</button>
</div>
<div class="section-label">ENABLED GENRES</div>
<div class="grid" id="grid-enabled"></div>
<div class="section-label">DISABLED GENRES β€” Generate now, use later if you enable them</div>
<div class="grid" id="grid-disabled"></div>
<div class="instructions">
<strong>Step 1 β€” Download posters:</strong>
Click "Download All 32 Posters". If Chrome asks about multiple downloads, click <strong>Allow</strong>. All PNGs will save to your Downloads folder.<br><br>
<strong>Step 2 β€” Create a folder:</strong>
Move all the PNG files into a dedicated folder, e.g. <code>C:\Users\YourName\Desktop\playlist_posters\</code><br><br>
<strong>Step 3 β€” Apply to Plex (manual):</strong>
Music β†’ Playlists β†’ click <strong>Β·Β·Β·</strong> on a playlist β†’ <strong>Edit</strong> β†’ click the thumbnail β†’ <strong>Upload Poster</strong> β†’ select the matching PNG.<br><br>
<strong>Step 3 β€” Apply automatically (recommended):</strong>
In <code>sonic_dashboard.py</code>, set <code>POSTER_ENABLED = True</code> and set <code>POSTER_FOLDER</code> to your folder path.
The dashboard will re-apply all posters after every run so they never reset to random album art.<br><br>
<strong>Note:</strong> Plex resets playlist art every time a playlist is regenerated. The automatic option in the dashboard fixes this permanently.
</div>
<script>
const SIZE = 600;
// ── GENRE LIST ──────────────────────────────────────────────────────────────
// Set enabled:true/false to match which genres you have active in the dashboard.
// Disabled genres still generate posters β€” useful if you enable them later.
const PLAYLISTS = [
// ENABLED
{ name:"Punk & Hardcore", sub:"Fast, Loud & No Apologies", enabled:true, border:'torn', palette:{bg:['#0a0400','#181000','#221800'], accent:'#dd6600', gold:'#ff9900', light:'#ffcc66', stripe:'#884400'}, icon:'punk' },
{ name:"Classic & Hard Rock", sub:"Arena Power & Electric Riffs", enabled:true, border:'lightning', palette:{bg:['#0a0008','#18001a','#200020'], accent:'#cc0033', gold:'#ff4466', light:'#ffaacc', stripe:'#880022'}, icon:'rock' },
{ name:"Psychedelic & Art Rock", sub:"Mind-Bending Sonic Landscapes", enabled:true, border:'ornate', palette:{bg:['#04000a','#0a0018','#120022'], accent:'#8800cc', gold:'#cc44ff', light:'#eeb8ff', stripe:'#440066'}, icon:'psychedelic' },
{ name:"Progressive & Neo-Prog", sub:"Epic Structures & Shifting Time", enabled:true, border:'grid', palette:{bg:['#000408','#000c18','#001020'], accent:'#0066cc', gold:'#2299ff', light:'#88ccff', stripe:'#003366'}, icon:'prog' },
{ name:"Rockabilly & Retro Rock", sub:"Grease, Twang & Hot Rods", enabled:true, border:'rope', palette:{bg:['#080400','#140a00','#1e1200'], accent:'#cc5500', gold:'#ff8800', light:'#ffbb66', stripe:'#662200'}, icon:'rockabilly' },
{ name:"Surf & California Pop", sub:"Sun, Waves & Highway 1", enabled:true, border:'worn', palette:{bg:['#000a10','#001522','#001e2e'], accent:'#0088aa', gold:'#00ccee', light:'#88eeff', stripe:'#004455'}, icon:'surf' },
{ name:"Metal", sub:"Heavy Riffs & Crushing Power", enabled:true, border:'lightning', palette:{bg:['#050000','#100000','#1a0000'], accent:'#aa0000', gold:'#cc2200', light:'#ff6633', stripe:'#550000'}, icon:'metal' },
{ name:"Extreme & Underground Metal",sub:"Brutal, Relentless & Uncompromising", enabled:true, border:'torn', palette:{bg:['#020000','#080000','#100000'], accent:'#880000', gold:'#aa1100', light:'#dd3300', stripe:'#440000'}, icon:'extreme' },
{ name:"Alternative & Grunge", sub:"Noise, Distortion & Attitude", enabled:true, border:'torn', palette:{bg:['#020404','#060c0c','#0a1414'], accent:'#208050', gold:'#40b070', light:'#80d0a0', stripe:'#104030'}, icon:'alt' },
{ name:"New Wave & Post-Punk", sub:"Cold Synths & Angular Guitars", enabled:true, border:'grid', palette:{bg:['#000408','#00080f','#000c14'], accent:'#006699', gold:'#0099cc', light:'#66ccee', stripe:'#003344'}, icon:'newwave' },
{ name:"Jam Bands & Experimental", sub:"Live Improvisation & Sonic Freedom", enabled:true, border:'ornate', palette:{bg:['#04080a','#081014','#0c181e'], accent:'#226644', gold:'#44aa77', light:'#88ddbb', stripe:'#113322'}, icon:'jam' },
{ name:"Hip-Hop & Rap", sub:"Rhythm, Flow & Street Poetry", enabled:true, border:'grid', palette:{bg:['#040008','#0a0018','#100024'], accent:'#8800cc', gold:'#aa22ff', light:'#dd88ff', stripe:'#440066'}, icon:'hiphop' },
{ name:"Pop", sub:"Hook-Driven Radio Gold", enabled:true, border:'worn', palette:{bg:['#080004','#140008','#1e000e'], accent:'#cc0066', gold:'#ff2299', light:'#ff99cc', stripe:'#660033'}, icon:'pop' },
{ name:"Electronic & Dance", sub:"Circuits, Beats & Basslines", enabled:true, border:'grid', palette:{bg:['#00080d','#001520','#002030'], accent:'#00aacc', gold:'#00ddff', light:'#aaeeff', stripe:'#005577'}, icon:'electronic' },
{ name:"Soul, R&B & Funk", sub:"Deep Grooves & Soulful Fire", enabled:true, border:'worn', palette:{bg:['#080200','#140600','#1e0c00'], accent:'#cc4400', gold:'#ff7700', light:'#ffaa55', stripe:'#662200'}, icon:'soul' },
{ name:"Folk, Americana & Bluegrass",sub:"Handcrafted Songs & Root Stories", enabled:true, border:'rope', palette:{bg:['#080a00','#121500','#1c2000'], accent:'#6a8c20', gold:'#a0b840', light:'#d0e080', stripe:'#3a5010'}, icon:'folk' },
{ name:"Country & Americana", sub:"Dusty Roads & Wide Open Plains", enabled:true, border:'rope', palette:{bg:['#0d0800','#1e1200','#2a1a00'], accent:'#c87820', gold:'#e8b040', light:'#f5d890', stripe:'#6b4000'}, icon:'country' },
{ name:"Blues", sub:"Raw Soul & Delta Roots", enabled:true, border:'worn', palette:{bg:['#1a0a00','#2d1200','#3d1800'], accent:'#c84b00', gold:'#e8a030', light:'#f5d080', stripe:'#8b2200'}, icon:'blues' },
{ name:"Jazz", sub:"Late Nights & Smoky Rooms", enabled:true, border:'ornate', palette:{bg:['#000a08','#001512','#001e1a'], accent:'#00886a', gold:'#00b890', light:'#80e8d0', stripe:'#004433'}, icon:'jazz' },
{ name:"Swing & Big Band", sub:"Brass, Swing & Dance Floors", enabled:true, border:'ornate', palette:{bg:['#080600','#140f00','#1e1800'], accent:'#bb8800', gold:'#ddaa00', light:'#ffe066', stripe:'#775500'}, icon:'swing' },
{ name:"Reggae & Ska", sub:"Island Vibes & Upstroke Rhythm", enabled:true, border:'worn', palette:{bg:['#000800','#001200','#001c00'], accent:'#228800', gold:'#44cc00', light:'#88ff44', stripe:'#114400'}, icon:'reggae' },
{ name:"Yacht Rock", sub:"Smooth Sailing & Soft Grooves", enabled:true, border:'worn', palette:{bg:['#000810','#001020','#001830'], accent:'#005588', gold:'#0088cc', light:'#66ccff', stripe:'#002244'}, icon:'yacht' },
{ name:"Indie", sub:"DIY Spirit & Bedroom Dreams", enabled:true, border:'ornate', palette:{bg:['#060008','#100015','#18001e'], accent:'#cc44aa', gold:'#ee66cc', light:'#ffaaee', stripe:'#660055'}, icon:'indie' },
// DISABLED β€” set enabled:true if you turn these on in the dashboard
{ name:"Goth & Dark Rock", sub:"Shadows, Velvet & Decay", enabled:false, border:'ornate', palette:{bg:['#000005','#08000f','#100018'], accent:'#6600aa', gold:'#9933dd', light:'#cc88ff', stripe:'#330066'}, icon:'goth' },
{ name:"World Music", sub:"Global Traditions & Rhythms", enabled:false, border:'worn', palette:{bg:['#080400','#120a00','#1c1200'], accent:'#bb6600', gold:'#dd9900', light:'#ffcc66', stripe:'#664400'}, icon:'world' },
{ name:"Latin", sub:"Fire, Rhythm & Tropical Heat", enabled:false, border:'worn', palette:{bg:['#080000','#150000','#200800'], accent:'#cc2200', gold:'#ff6600', light:'#ffaa44', stripe:'#660000'}, icon:'latin' },
{ name:"Classic Vocal & Standards", sub:"Timeless Voices & Great American Songs",enabled:false,border:'ornate', palette:{bg:['#060408','#0e0c12','#161018'], accent:'#9944aa', gold:'#cc77dd', light:'#eeb8ff', stripe:'#441155'}, icon:'vocal' },
{ name:"Trip-Hop & Downtempo", sub:"Hazy Beats & Dark Atmosphere", enabled:false, border:'grid', palette:{bg:['#040408','#080810','#0c0c18'], accent:'#334488', gold:'#5566bb', light:'#aabbee', stripe:'#112244'}, icon:'triphop' },
{ name:"Power Pop & Twee", sub:"Sugary Hooks & Jangly Guitars", enabled:false, border:'worn', palette:{bg:['#080004','#120008','#1c000e'], accent:'#cc2255', gold:'#ee4477', light:'#ffaacc', stripe:'#660022'}, icon:'powerpop' },
{ name:"Gospel", sub:"Spirit, Soul & Sacred Fire", enabled:false, border:'ornate', palette:{bg:['#080600','#120e00','#1c1800'], accent:'#cc9900', gold:'#ffcc00', light:'#ffe680', stripe:'#775500'}, icon:'gospel' },
{ name:"Comedy & Novelty", sub:"Laughs, Parody & Musical Wit", enabled:false, border:'torn', palette:{bg:['#060800','#0e1200','#161c00'], accent:'#88aa00', gold:'#bbdd00', light:'#eeff66', stripe:'#445500'}, icon:'comedy' },
{ name:"Soundtrack & Score", sub:"Cinema, Drama & Epic Themes", enabled:false, border:'ornate', palette:{bg:['#060408','#0c0810','#120c18'], accent:'#775599', gold:'#aa88cc', light:'#ddbbff', stripe:'#332244'}, icon:'soundtrack' },
];
// ── ICONS ───────────────────────────────────────────────────────────────────
const ICONS = {
punk(ctx,p) {
const spikes=7;
for(let i=0;i<spikes;i++){
const x=SIZE/2-90+i*30, h=80+Math.sin(i*1.1)*50;
ctx.fillStyle=i%2===0?p.gold:p.accent;
ctx.shadowColor=ctx.fillStyle; ctx.shadowBlur=14;
ctx.beginPath(); ctx.moveTo(x-11,SIZE/2+30); ctx.lineTo(x,SIZE/2+30-h); ctx.lineTo(x+11,SIZE/2+30); ctx.closePath(); ctx.fill();
}
ctx.shadowBlur=0;
ctx.strokeStyle=p.light+'cc'; ctx.lineWidth=3;
ctx.beginPath(); ctx.moveTo(SIZE/2-30,SIZE/2+55); ctx.lineTo(SIZE/2+30,SIZE/2+100); ctx.stroke();
ctx.beginPath(); ctx.moveTo(SIZE/2+30,SIZE/2+55); ctx.lineTo(SIZE/2-30,SIZE/2+100); ctx.stroke();
},
rock(ctx,p) {
ctx.fillStyle=p.gold; ctx.shadowColor=p.gold; ctx.shadowBlur=22;
ctx.beginPath(); ctx.moveTo(SIZE/2+20,135); ctx.lineTo(SIZE/2-35,SIZE/2+15); ctx.lineTo(SIZE/2+12,SIZE/2+15); ctx.lineTo(SIZE/2-20,SIZE-135); ctx.lineTo(SIZE/2+35,SIZE/2-15); ctx.lineTo(SIZE/2-12,SIZE/2-15); ctx.closePath(); ctx.fill();
ctx.shadowBlur=0;
},
psychedelic(ctx,p) {
for(let r=20;r<200;r+=22){
ctx.strokeStyle=`hsla(${r*2},100%,65%,0.5)`; ctx.lineWidth=2;
ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,r,0,Math.PI*2); ctx.stroke();
}
ctx.strokeStyle=p.gold+'bb'; ctx.lineWidth=2; ctx.beginPath();
for(let t=0;t<Math.PI*8;t+=0.05){ const r=t*14,x=SIZE/2+r*Math.cos(t),y=SIZE/2+r*Math.sin(t); t===0?ctx.moveTo(x,y):ctx.lineTo(x,y); }
ctx.stroke();
},
prog(ctx,p) {
ctx.strokeStyle=p.gold+'cc'; ctx.lineWidth=2;
[[0],[Math.PI/4],[Math.PI/2],[Math.PI*3/4]].forEach(([a])=>{ ctx.save(); ctx.translate(SIZE/2,SIZE/2); ctx.rotate(a); ctx.strokeRect(-70,-70,140,140); ctx.restore(); });
ctx.strokeStyle=p.accent+'cc'; ctx.lineWidth=1.5;
ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,100,0,Math.PI*2); ctx.stroke();
ctx.fillStyle=p.gold; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,12,0,Math.PI*2); ctx.fill();
},
rockabilly(ctx,p) {
for(let i=0;i<5;i++){
const x=SIZE/2-80+i*40;
ctx.fillStyle=i%2===0?p.gold:p.accent; ctx.shadowColor=ctx.fillStyle; ctx.shadowBlur=12;
ctx.beginPath(); ctx.moveTo(x,SIZE/2+60); ctx.bezierCurveTo(x-20,SIZE/2+20,x-10,SIZE/2-40,x,SIZE/2-70); ctx.bezierCurveTo(x+10,SIZE/2-40,x+20,SIZE/2+20,x,SIZE/2+60); ctx.closePath(); ctx.fill();
}
ctx.shadowBlur=0;
},
surf(ctx,p) {
for(let w=0;w<3;w++){
ctx.beginPath(); ctx.strokeStyle=(w===1?p.gold:p.accent)+(w===1?'cc':'88'); ctx.lineWidth=w===1?3:1.5;
for(let x=60;x<SIZE-60;x+=2){ const y=SIZE/2-40+w*40+Math.sin((x-60)*0.022+w)*55; x===60?ctx.moveTo(x,y):ctx.lineTo(x,y); }
ctx.stroke();
}
ctx.fillStyle=p.gold; ctx.shadowColor=p.gold; ctx.shadowBlur=14;
ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2-80,26,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
},
metal(ctx,p) {
const pts=[];
for(let i=0;i<5;i++){ const a=(i*4/5)*Math.PI*2-Math.PI/2; pts.push([SIZE/2+110*Math.cos(a),SIZE/2+110*Math.sin(a)]); }
ctx.strokeStyle=p.accent+'cc'; ctx.lineWidth=2.5;
ctx.beginPath(); pts.forEach((pt,i)=>i===0?ctx.moveTo(pt[0],pt[1]):ctx.lineTo(pt[0],pt[1])); ctx.closePath(); ctx.stroke();
ctx.strokeStyle=p.gold+'88'; ctx.lineWidth=1.5; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,110,0,Math.PI*2); ctx.stroke();
ctx.fillStyle=p.accent; ctx.shadowColor=p.accent; ctx.shadowBlur=16; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,16,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
},
extreme(ctx,p) {
ctx.fillStyle=p.accent+'cc'; ctx.beginPath();
for(let i=0;i<24;i++){ const a=(i/24)*Math.PI*2,r=i%2===0?120:60; i===0?ctx.moveTo(SIZE/2+r*Math.cos(a),SIZE/2+r*Math.sin(a)):ctx.lineTo(SIZE/2+r*Math.cos(a),SIZE/2+r*Math.sin(a)); }
ctx.closePath(); ctx.fill();
ctx.fillStyle=p.bg[0]; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,40,0,Math.PI*2); ctx.fill();
},
alt(ctx,p) {
for(let i=0;i<12;i++){ const a=(i/12)*Math.PI*2,r=80+(i%3)*22; ctx.strokeStyle=i%3===0?p.gold:p.accent+'88'; ctx.lineWidth=i%3===0?2.5:1; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,r,a,a+Math.PI*2/12-0.1); ctx.stroke(); }
ctx.fillStyle=p.gold; ctx.shadowColor=p.gold; ctx.shadowBlur=12; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,14,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
},
newwave(ctx,p) {
for(let i=0;i<8;i++){ ctx.strokeStyle=p.accent+'66'; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(60+i*70,60); ctx.lineTo(60+i*70,SIZE-60); ctx.stroke(); ctx.beginPath(); ctx.moveTo(60,60+i*70); ctx.lineTo(SIZE-60,60+i*70); ctx.stroke(); }
ctx.strokeStyle=p.gold+'cc'; ctx.lineWidth=3; ctx.beginPath(); ctx.moveTo(60,SIZE-60); ctx.lineTo(SIZE-60,60); ctx.stroke();
ctx.fillStyle=p.gold; ctx.shadowColor=p.gold; ctx.shadowBlur=14; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,16,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
},
jam(ctx,p) {
for(let t=0;t<Math.PI*6;t+=0.2){ const r=t*14,x=SIZE/2+r*Math.cos(t),y=SIZE/2+r*Math.sin(t)*0.7,sz=3+Math.sin(t)*2; ctx.fillStyle=t%2<1?p.gold:p.accent; ctx.globalAlpha=0.7; ctx.beginPath(); ctx.arc(x,y,sz,0,Math.PI*2); ctx.fill(); }
ctx.globalAlpha=1;
},
hiphop(ctx,p) {
for(let s=20;s<170;s+=28){ ctx.strokeStyle=s%56===20?p.gold+'cc':p.accent+'55'; ctx.lineWidth=s%56===20?2.5:1; ctx.beginPath(); ctx.moveTo(SIZE/2,SIZE/2-s); ctx.lineTo(SIZE/2+s,SIZE/2); ctx.lineTo(SIZE/2,SIZE/2+s); ctx.lineTo(SIZE/2-s,SIZE/2); ctx.closePath(); ctx.stroke(); }
ctx.fillStyle=p.gold; ctx.shadowColor=p.gold; ctx.shadowBlur=16; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,16,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
},
pop(ctx,p) {
for(let i=0;i<8;i++){ const a=(i/8)*Math.PI*2; ctx.strokeStyle=i%2===0?p.gold+'cc':p.accent+'88'; ctx.lineWidth=i%2===0?3:1.5; ctx.beginPath(); ctx.moveTo(SIZE/2+22*Math.cos(a),SIZE/2+22*Math.sin(a)); ctx.lineTo(SIZE/2+140*Math.cos(a),SIZE/2+140*Math.sin(a)); ctx.stroke(); }
ctx.fillStyle=p.gold; ctx.shadowColor=p.gold; ctx.shadowBlur=22; drawStar(ctx,SIZE/2,SIZE/2,5,55,22); ctx.shadowBlur=0;
},
electronic(ctx,p) {
for(let r=40;r<190;r+=30){ ctx.strokeStyle=r%60===40?p.gold+'cc':p.accent+'55'; ctx.lineWidth=r%60===40?2:1; ctx.setLineDash([8,4]); ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,r,0,Math.PI*2); ctx.stroke(); ctx.setLineDash([]); }
ctx.fillStyle=p.gold; ctx.shadowColor=p.gold; ctx.shadowBlur=20; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,18,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
ctx.strokeStyle=p.light+'cc'; ctx.lineWidth=2.5; ctx.beginPath();
for(let x=60;x<SIZE-60;x++){ const y=SIZE/2+Math.sin((x-60)*0.045*Math.PI)*60; x===60?ctx.moveTo(x,y):ctx.lineTo(x,y); } ctx.stroke();
},
soul(ctx,p) {
for(let i=0;i<6;i++){ const x=SIZE/2-75+i*30,h=90+Math.sin(i*0.9)*40; ctx.fillStyle=i%2===0?p.gold:p.accent; ctx.globalAlpha=0.7; ctx.shadowColor=ctx.fillStyle; ctx.shadowBlur=10; ctx.beginPath(); ctx.moveTo(x,SIZE/2+50); ctx.bezierCurveTo(x-18,SIZE/2+10,x-8,SIZE/2-h+30,x,SIZE/2-h); ctx.bezierCurveTo(x+8,SIZE/2-h+30,x+18,SIZE/2+10,x,SIZE/2+50); ctx.closePath(); ctx.fill(); }
ctx.globalAlpha=1; ctx.shadowBlur=0;
},
folk(ctx,p) {
ctx.fillStyle=p.accent+'44'; ctx.beginPath(); ctx.moveTo(SIZE/2,130); ctx.lineTo(SIZE/2+115,SIZE/2+75); ctx.lineTo(SIZE/2-115,SIZE/2+75); ctx.closePath(); ctx.fill();
ctx.fillStyle=p.gold+'aa'; ctx.beginPath(); ctx.moveTo(SIZE/2,105); ctx.lineTo(SIZE/2+82,SIZE/2+25); ctx.lineTo(SIZE/2-82,SIZE/2+25); ctx.closePath(); ctx.fill();
ctx.fillStyle=p.stripe+'cc'; ctx.fillRect(SIZE/2-14,SIZE/2+75,28,58);
},
country(ctx,p) {
ctx.fillStyle=p.accent+'33'; drawStar(ctx,SIZE/2,SIZE/2,5,135,57);
ctx.fillStyle=p.gold; ctx.globalAlpha=0.9; drawStar(ctx,SIZE/2,SIZE/2,5,92,38); ctx.globalAlpha=1;
ctx.fillStyle=p.accent+'44'; ctx.beginPath(); ctx.ellipse(SIZE/2,SIZE/2+65,125,22,0,0,Math.PI*2); ctx.fill();
},
blues(ctx,p) {
for(let line=0;line<5;line++){ const baseY=200+line*22; ctx.strokeStyle=p.gold+'44'; ctx.lineWidth=1; ctx.beginPath(); for(let x=60;x<SIZE-60;x+=2){ const y=baseY+Math.sin((x+line*30)*0.045)*5; x===60?ctx.moveTo(x,y):ctx.lineTo(x,y); } ctx.stroke(); }
ctx.fillStyle=p.accent+'55'; ctx.beginPath(); ctx.ellipse(SIZE/2,SIZE/2+30,75,95,0,0,Math.PI*2); ctx.fill();
for(let i=0;i<5;i++){ ctx.strokeStyle=p.gold+'cc'; ctx.lineWidth=2; const y=SIZE/2-80+i*32; ctx.beginPath(); ctx.moveTo(SIZE/2-20,y); ctx.lineTo(SIZE/2+20,y); ctx.stroke(); }
},
jazz(ctx,p) {
ctx.strokeStyle=p.gold+'aa'; ctx.lineWidth=2; ctx.beginPath();
for(let t=0;t<Math.PI*4;t+=0.05){ const r=18+t*18,x=SIZE/2+r*Math.cos(t),y=SIZE/2+r*Math.sin(t)*0.5; t===0?ctx.moveTo(x,y):ctx.lineTo(x,y); } ctx.stroke();
ctx.fillStyle=p.gold; ctx.font='bold 64px serif'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('β™ͺ',SIZE/2-40,SIZE/2-30);
ctx.fillStyle=p.light; ctx.font='bold 46px serif'; ctx.fillText('β™©',SIZE/2+52,SIZE/2+52);
},
swing(ctx,p) {
for(let i=0;i<5;i++){ ctx.strokeStyle=p.gold+'55'; ctx.lineWidth=1; ctx.beginPath(); ctx.moveTo(60,SIZE/2-40+i*20); ctx.lineTo(SIZE-60,SIZE/2-40+i*20); ctx.stroke(); }
ctx.fillStyle=p.gold; ctx.font='bold 80px serif'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('π„ž',SIZE/2,SIZE/2+10);
ctx.fillStyle=p.accent+'88'; ctx.beginPath(); ctx.arc(SIZE/2-90,SIZE/2+30,12,0,Math.PI*2); ctx.fill();
ctx.fillStyle=p.gold+'88'; ctx.beginPath(); ctx.arc(SIZE/2+60,SIZE/2+10,10,0,Math.PI*2); ctx.fill();
},
reggae(ctx,p) {
for(let r=40;r<145;r+=28){ ctx.strokeStyle=r%56===40?p.gold+'cc':p.accent+'55'; ctx.lineWidth=r%56===40?3:1; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,r,0,Math.PI*2); ctx.stroke(); }
ctx.strokeStyle=p.gold; ctx.lineWidth=3;
ctx.beginPath(); ctx.moveTo(SIZE/2,SIZE/2-110); ctx.lineTo(SIZE/2,SIZE/2+110); ctx.stroke();
ctx.beginPath(); ctx.moveTo(SIZE/2,SIZE/2); ctx.lineTo(SIZE/2-75,SIZE/2+75); ctx.stroke();
ctx.beginPath(); ctx.moveTo(SIZE/2,SIZE/2); ctx.lineTo(SIZE/2+75,SIZE/2+75); ctx.stroke();
},
yacht(ctx,p) {
ctx.strokeStyle=p.gold+'cc'; ctx.lineWidth=2.5;
ctx.beginPath(); ctx.moveTo(SIZE/2,SIZE/2+80); ctx.lineTo(SIZE/2-110,SIZE/2+80); ctx.bezierCurveTo(SIZE/2-80,SIZE/2+80,SIZE/2-40,SIZE/2+110,SIZE/2,SIZE/2+110); ctx.bezierCurveTo(SIZE/2+40,SIZE/2+110,SIZE/2+80,SIZE/2+80,SIZE/2+110,SIZE/2+80); ctx.closePath(); ctx.stroke();
ctx.beginPath(); ctx.moveTo(SIZE/2,SIZE/2-90); ctx.lineTo(SIZE/2,SIZE/2+80); ctx.stroke();
ctx.fillStyle=p.gold+'aa'; ctx.beginPath(); ctx.moveTo(SIZE/2,SIZE/2-80); ctx.lineTo(SIZE/2-80,SIZE/2+40); ctx.lineTo(SIZE/2,SIZE/2+40); ctx.closePath(); ctx.fill();
for(let w=0;w<3;w++){ ctx.strokeStyle=p.accent+'55'; ctx.lineWidth=1; ctx.beginPath(); for(let x=60;x<SIZE-60;x+=2){ const y=SIZE/2-80+w*30+Math.sin(x*0.03+w)*10; x===60?ctx.moveTo(x,y):ctx.lineTo(x,y); } ctx.stroke(); }
},
indie(ctx,p) {
for(let i=0;i<5;i++){ const cx=SIZE/2+Math.cos(i*1.25)*80,cy=SIZE/2+Math.sin(i*1.25)*60,r=40+i*12; const grad=ctx.createRadialGradient(cx,cy,0,cx,cy,r); grad.addColorStop(0,p.gold+'55'); grad.addColorStop(1,p.accent+'00'); ctx.fillStyle=grad; ctx.beginPath(); ctx.arc(cx,cy,r,0,Math.PI*2); ctx.fill(); }
ctx.fillStyle=p.light+'cc'; ctx.shadowColor=p.light; ctx.shadowBlur=22; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,24,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
},
goth(ctx,p) {
ctx.strokeStyle=p.accent+'bb'; ctx.lineWidth=2; ctx.beginPath(); ctx.moveTo(SIZE/2-80,SIZE/2+100); ctx.lineTo(SIZE/2-80,SIZE/2-40); ctx.bezierCurveTo(SIZE/2-80,SIZE/2-120,SIZE/2+80,SIZE/2-120,SIZE/2+80,SIZE/2-40); ctx.lineTo(SIZE/2+80,SIZE/2+100); ctx.stroke();
ctx.fillStyle=p.light+'cc'; ctx.shadowColor=p.light; ctx.shadowBlur=18; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2-75,40,0,Math.PI*2); ctx.fill();
ctx.fillStyle=p.bg[1]; ctx.shadowBlur=0; ctx.beginPath(); ctx.arc(SIZE/2+18,SIZE/2-86,34,0,Math.PI*2); ctx.fill();
},
world(ctx,p) {
ctx.strokeStyle=p.gold+'88'; ctx.lineWidth=1.5; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,110,0,Math.PI*2); ctx.stroke();
for(let i=1;i<4;i++){ const r=110*Math.sin((i/4)*Math.PI); ctx.beginPath(); ctx.ellipse(SIZE/2,SIZE/2,r,30,0,0,Math.PI*2); ctx.stroke(); }
for(let i=0;i<6;i++){ const a=(i/6)*Math.PI; ctx.beginPath(); ctx.moveTo(SIZE/2+110*Math.cos(a),SIZE/2+110*Math.sin(a)); ctx.lineTo(SIZE/2-110*Math.cos(a),SIZE/2-110*Math.sin(a)); ctx.stroke(); }
ctx.fillStyle=p.gold; ctx.shadowColor=p.gold; ctx.shadowBlur=12; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,14,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
},
latin(ctx,p) {
for(let i=0;i<16;i++){ const a=(i/16)*Math.PI*2; ctx.strokeStyle=i%2===0?p.gold+'cc':p.accent+'88'; ctx.lineWidth=i%2===0?3:1.5; ctx.beginPath(); ctx.moveTo(SIZE/2+28*Math.cos(a),SIZE/2+28*Math.sin(a)); ctx.lineTo(SIZE/2+145*Math.cos(a),SIZE/2+145*Math.sin(a)); ctx.stroke(); }
ctx.fillStyle=p.gold; ctx.shadowColor=p.gold; ctx.shadowBlur=25; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,28,0,Math.PI*2); ctx.fill(); ctx.shadowBlur=0;
},
vocal(ctx,p) {
ctx.strokeStyle=p.gold+'88'; ctx.lineWidth=2;
for(let i=0;i<5;i++){ ctx.beginPath(); ctx.moveTo(60,SIZE/2-30+i*18); ctx.lineTo(SIZE-60,SIZE/2-30+i*18); ctx.stroke(); }
ctx.fillStyle=p.gold; ctx.font='bold 70px serif'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('π„ž',SIZE/2,SIZE/2+15);
},
triphop(ctx,p) {
for(let i=0;i<3;i++){ const grad=ctx.createRadialGradient(SIZE/2,SIZE/2,0,SIZE/2,SIZE/2,80+i*40); grad.addColorStop(0,p.accent+'44'); grad.addColorStop(1,'transparent'); ctx.fillStyle=grad; ctx.fillRect(0,0,SIZE,SIZE); }
ctx.strokeStyle=p.gold+'66'; ctx.lineWidth=1.5; ctx.setLineDash([4,8]); ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,90,0,Math.PI*2); ctx.stroke(); ctx.setLineDash([]);
ctx.fillStyle=p.gold+'cc'; ctx.beginPath(); ctx.arc(SIZE/2,SIZE/2,20,0,Math.PI*2); ctx.fill();
},
powerpop(ctx,p) {
for(let i=0;i<6;i++){ const a=(i/6)*Math.PI*2; ctx.fillStyle=i%2===0?p.gold:p.accent; drawStar(ctx,SIZE/2+90*Math.cos(a),SIZE/2+90*Math.sin(a),5,18,8); }
ctx.fillStyle=p.gold; ctx.shadowColor=p.gold; ctx.shadowBlur=18; drawStar(ctx,SIZE/2,SIZE/2,5,45,18); ctx.shadowBlur=0;
},
gospel(ctx,p) {
for(let i=0;i<12;i++){ const a=(i/12)*Math.PI*2; ctx.strokeStyle=p.gold+(i%3===0?'cc':'66'); ctx.lineWidth=i%3===0?3:1; ctx.beginPath(); ctx.moveTo(SIZE/2+25*Math.cos(a),SIZE/2+25*Math.sin(a)); ctx.lineTo(SIZE/2+140*Math.cos(a),SIZE/2+140*Math.sin(a)); ctx.stroke(); }
ctx.strokeStyle=p.gold; ctx.lineWidth=8; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(SIZE/2,SIZE/2-80); ctx.lineTo(SIZE/2,SIZE/2+80); ctx.stroke();
ctx.beginPath(); ctx.moveTo(SIZE/2-55,SIZE/2-20); ctx.lineTo(SIZE/2+55,SIZE/2-20); ctx.stroke();
ctx.lineCap='butt';
},
comedy(ctx,p) {
ctx.fillStyle=p.gold+'cc'; ctx.font='bold 120px serif'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('πŸ˜„',SIZE/2,SIZE/2-20);
ctx.fillStyle=p.accent+'88'; ctx.font='bold 50px serif'; ctx.fillText('β™ͺ',SIZE/2-80,SIZE/2+70); ctx.fillText('β™ͺ',SIZE/2+60,SIZE/2+80);
},
soundtrack(ctx,p) {
ctx.fillStyle=p.stripe+'88'; ctx.fillRect(SIZE/2-80,80,160,SIZE-160);
ctx.strokeStyle=p.gold+'88'; ctx.lineWidth=1;
for(let y=90;y<SIZE-90;y+=22){ ctx.strokeRect(SIZE/2-70,y,20,14); ctx.strokeRect(SIZE/2+50,y,20,14); }
ctx.fillStyle=p.gold; ctx.font='bold 54px serif'; ctx.textAlign='center'; ctx.textBaseline='middle'; ctx.fillText('β˜…',SIZE/2,SIZE/2);
},
};
function drawStar(ctx,cx,cy,pts,outerR,innerR){
ctx.beginPath();
for(let i=0;i<pts*2;i++){ const r=i%2===0?outerR:innerR,a=(i/pts)*Math.PI-Math.PI/2; i===0?ctx.moveTo(cx+r*Math.cos(a),cy+r*Math.sin(a)):ctx.lineTo(cx+r*Math.cos(a),cy+r*Math.sin(a)); }
ctx.closePath(); ctx.fill();
}
// ── BORDERS ─────────────────────────────────────────────────────────────────
const BORDERS = {
worn(ctx,p){ ctx.strokeStyle=p.gold; ctx.lineWidth=8; ctx.strokeRect(18,18,SIZE-36,SIZE-36); ctx.strokeStyle=p.accent+'88'; ctx.lineWidth=2; ctx.strokeRect(28,28,SIZE-56,SIZE-56); [[24,24],[SIZE-24,24],[24,SIZE-24],[SIZE-24,SIZE-24]].forEach(([x,y])=>{ ctx.fillStyle=p.gold; ctx.beginPath(); ctx.arc(x,y,9,0,Math.PI*2); ctx.fill(); }); },
ornate(ctx,p){ ctx.strokeStyle=p.gold; ctx.lineWidth=6; ctx.strokeRect(14,14,SIZE-28,SIZE-28); ctx.strokeStyle=p.light+'44'; ctx.lineWidth=2; ctx.strokeRect(22,22,SIZE-44,SIZE-44); ctx.strokeStyle=p.gold+'aa'; ctx.lineWidth=1; ctx.strokeRect(30,30,SIZE-60,SIZE-60); [[16,16,1,1],[SIZE-16,16,-1,1],[16,SIZE-16,1,-1],[SIZE-16,SIZE-16,-1,-1]].forEach(([x,y,dx,dy])=>{ ctx.strokeStyle=p.gold; ctx.lineWidth=3; ctx.beginPath(); ctx.moveTo(x,y+dy*32); ctx.lineTo(x,y); ctx.lineTo(x+dx*32,y); ctx.stroke(); }); },
torn(ctx,p){ ctx.strokeStyle=p.accent; ctx.lineWidth=5; ctx.setLineDash([8,4,2,4]); ctx.strokeRect(18,18,SIZE-36,SIZE-36); ctx.setLineDash([]); ctx.strokeStyle=p.gold+'66'; ctx.lineWidth=2; ctx.strokeRect(26,26,SIZE-52,SIZE-52); },
grid(ctx,p){ ctx.strokeStyle=p.gold+'aa'; ctx.lineWidth=4; ctx.strokeRect(16,16,SIZE-32,SIZE-32); for(let i=0;i<=10;i++){ const t=16+(i/10)*(SIZE-32); [[[t,12],[t,20]],[[t,SIZE-20],[t,SIZE-12]],[[12,t],[20,t]],[[SIZE-20,t],[SIZE-12,t]]].forEach(([[x1,y1],[x2,y2]])=>{ ctx.strokeStyle=p.accent; ctx.lineWidth=1.5; ctx.beginPath(); ctx.moveTo(x1,y1); ctx.lineTo(x2,y2); ctx.stroke(); }); } },
rope(ctx,p){ ctx.strokeStyle=p.gold; ctx.lineWidth=7; ctx.setLineDash([14,6]); ctx.strokeRect(16,16,SIZE-32,SIZE-32); ctx.setLineDash([]); ctx.strokeStyle=p.accent+'88'; ctx.lineWidth=2; ctx.strokeRect(26,26,SIZE-52,SIZE-52); },
lightning(ctx,p){ ctx.strokeStyle=p.gold; ctx.lineWidth=6; ctx.strokeRect(16,16,SIZE-32,SIZE-32); ctx.strokeStyle=p.accent+'aa'; ctx.lineWidth=2; const zig=14; ctx.beginPath(); for(let i=26;i<SIZE-26;i+=zig){ ctx.lineTo(i,26+(Math.floor(i/zig)%2===0?6:-6)); } ctx.stroke(); },
};
function drawBackground(ctx,p){
const grad=ctx.createLinearGradient(0,0,SIZE,SIZE); grad.addColorStop(0,p.bg[0]); grad.addColorStop(0.5,p.bg[1]); grad.addColorStop(1,p.bg[2]);
ctx.fillStyle=grad; ctx.fillRect(0,0,SIZE,SIZE);
ctx.globalAlpha=0.04; for(let i=0;i<5000;i++){ ctx.fillStyle=Math.random()>0.5?'#fff':'#000'; ctx.fillRect(Math.random()*SIZE,Math.random()*SIZE,1,1); } ctx.globalAlpha=1;
ctx.globalAlpha=0.025; ctx.fillStyle='#000'; for(let y=0;y<SIZE;y+=3){ ctx.fillRect(0,y,SIZE,1); } ctx.globalAlpha=1;
}
function drawText(ctx,pl,p){
ctx.fillStyle=p.stripe+'cc'; ctx.fillRect(0,46,SIZE,58);
ctx.strokeStyle=p.gold+'aa'; ctx.lineWidth=1.5;
ctx.beginPath(); ctx.moveTo(0,46); ctx.lineTo(SIZE,46); ctx.stroke();
ctx.beginPath(); ctx.moveTo(0,104); ctx.lineTo(SIZE,104); ctx.stroke();
ctx.fillStyle=p.light; ctx.font='500 17px "Oswald",sans-serif'; ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText('AUTO PLAYLIST',SIZE/2,75);
const shortName=pl.name.toUpperCase();
ctx.fillStyle=p.gold; ctx.shadowColor=p.gold+'88'; ctx.shadowBlur=8;
let fs=60; ctx.font=`bold ${fs}px "Bebas Neue",sans-serif`;
while(ctx.measureText(shortName).width>SIZE-72&&fs>18){ fs-=2; ctx.font=`bold ${fs}px "Bebas Neue",sans-serif`; }
ctx.textBaseline='middle'; ctx.fillText(shortName,SIZE/2,SIZE-108); ctx.shadowBlur=0;
ctx.fillStyle=p.light+'cc'; ctx.font=`italic 16px "Playfair Display",serif`; ctx.textBaseline='middle';
ctx.fillText(pl.sub,SIZE/2,SIZE-72);
ctx.fillStyle=p.stripe+'cc'; ctx.fillRect(0,SIZE-52,SIZE,52);
ctx.strokeStyle=p.gold+'aa'; ctx.lineWidth=1.5; ctx.beginPath(); ctx.moveTo(0,SIZE-52); ctx.lineTo(SIZE,SIZE-52); ctx.stroke();
ctx.fillStyle=p.light+'88'; ctx.font='11px "Special Elite",monospace'; ctx.textBaseline='middle';
ctx.fillText('PLEX Β· SONIC AUTO PLAYLIST',SIZE/2,SIZE-26);
}
const canvasMap = {};
function renderPoster(pl,key){
if(!canvasMap[key]){ const c=document.createElement('canvas'); c.width=SIZE; c.height=SIZE; canvasMap[key]=c; }
const canvas=canvasMap[key], ctx=canvas.getContext('2d'), p=pl.palette;
ctx.clearRect(0,0,SIZE,SIZE);
drawBackground(ctx,p);
(BORDERS[pl.border]||BORDERS.worn)(ctx,p);
ctx.save(); ctx.translate((Math.random()-0.5)*4,(Math.random()-0.5)*4);
(ICONS[pl.icon]||ICONS.alt)(ctx,p);
ctx.restore();
drawText(ctx,pl,p);
return canvas;
}
function makeCard(pl,key){
const card=document.createElement('div'); card.className='poster-card';
const wrap=document.createElement('div'); wrap.className='poster-wrap'; wrap.title='Click to download';
const canvas=renderPoster(pl,key);
canvas.style.width='280px'; canvas.style.height='280px'; canvas.style.cursor='pointer';
canvas.addEventListener('click',()=>downloadOne(canvas,pl.name));
wrap.appendChild(canvas);
const label=document.createElement('div'); label.className='poster-label'; label.textContent=pl.name;
const dlBtn=document.createElement('button'); dlBtn.className='dl-btn'; dlBtn.textContent='⬇ Download';
dlBtn.addEventListener('click',()=>downloadOne(canvas,pl.name));
card.append(wrap,label,dlBtn);
return card;
}
function renderAll(){
const ge=document.getElementById('grid-enabled'), gd=document.getElementById('grid-disabled');
ge.innerHTML=''; gd.innerHTML='';
PLAYLISTS.forEach((pl,i)=>{
const key='pl_'+i;
(pl.enabled?ge:gd).appendChild(makeCard(pl,key));
});
}
function sanitize(n){ return n.replace(/[^a-z0-9]+/gi,'_').replace(/^_+|_+$/g,''); }
function downloadOne(canvas,name){ const a=document.createElement('a'); a.download='Auto_'+sanitize(name)+'.png'; a.href=canvas.toDataURL('image/png'); a.click(); }
function downloadAll(){ Object.values(canvasMap).forEach((canvas,i)=>{ setTimeout(()=>downloadOne(canvas,PLAYLISTS[i].name),i*180); }); }
function downloadEnabled(){
PLAYLISTS.filter(p=>p.enabled).forEach((pl,i)=>{
const key='pl_'+PLAYLISTS.indexOf(pl);
const canvas=canvasMap[key];
if(canvas) setTimeout(()=>downloadOne(canvas,pl.name),i*180);
});
}
window.addEventListener('load', renderAll);
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment