Created
April 25, 2026 13:38
-
-
Save adam137016/001ed4bfa393a8f33ca8e7d020e4b69d to your computer and use it in GitHub Desktop.
Playlist Poster Generator for Sonic Dashboard
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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