596 lines
30 KiB
JavaScript
596 lines
30 KiB
JavaScript
// sports.js — Sports venue background themes
|
|
// Variants: stadium, raceTrack, court, field, arena, pool, gym, skatepark
|
|
|
|
(function(engine) {
|
|
if (!engine) return;
|
|
var D = engine.Draw, H = engine.helpers;
|
|
|
|
engine.registerTheme('sports', {
|
|
|
|
stadium: {
|
|
meta: { id: 'sports_stadium', mood: 'exciting', colors: ['#1a3a1a', '#2a5a2a', '#88cc44', '#ffffff'], tags: ['stadium', 'football', 'soccer', 'crowd'], description: 'Large outdoor stadium with green pitch, bright floodlights, and packed stands', compatibleMusicMoods: ['exciting', 'energetic', 'upbeat'], recommendedFor: ['sports', 'action', 'trivia'] },
|
|
colors: ['#1a3a1a', '#2a5a2a', '#88cc44', '#ffffff'],
|
|
renderBase: function(ctx, w, h, c, Draw, t) {
|
|
// Night sky
|
|
Draw.fillGradient(ctx, w, h, ['#0a0a1e', '#1a1a2e'], 90);
|
|
// Pitch
|
|
Draw.rect(ctx, w * 0.08, h * 0.4, w * 0.84, h * 0.45, c[1]);
|
|
// Mowed stripes on pitch
|
|
for (var s = 0; s < 8; s++) {
|
|
var sx = w * 0.08 + s * w * 0.105;
|
|
Draw.rect(ctx, sx, h * 0.4, w * 0.105, h * 0.45, s % 2 === 0 ? c[1] : H.lighten(c[1], 0.08));
|
|
}
|
|
},
|
|
renderMid: function(ctx, w, h, c, Draw, t) {
|
|
var rng = H.seededRandom(1001);
|
|
// Field markings
|
|
ctx.strokeStyle = H.rgba(c[3], 0.6); ctx.lineWidth = 2;
|
|
ctx.strokeRect(w * 0.1, h * 0.42, w * 0.8, h * 0.41);
|
|
// Center line
|
|
ctx.beginPath(); ctx.moveTo(w * 0.5, h * 0.42); ctx.lineTo(w * 0.5, h * 0.83); ctx.stroke();
|
|
// Center circle
|
|
ctx.beginPath(); ctx.arc(w * 0.5, h * 0.625, h * 0.08, 0, Math.PI * 2); ctx.stroke();
|
|
Draw.circle(ctx, w * 0.5, h * 0.625, 3, H.rgba(c[3], 0.6));
|
|
// Goal boxes
|
|
ctx.strokeRect(w * 0.1, h * 0.52, w * 0.08, h * 0.21);
|
|
ctx.strokeRect(w * 0.82, h * 0.52, w * 0.08, h * 0.21);
|
|
// Stands
|
|
for (var row = 0; row < 5; row++) {
|
|
var ry = h * 0.08 + row * h * 0.06;
|
|
Draw.rect(ctx, w * 0.02, ry, w * 0.96, h * 0.05, H.rgba('#444466', 0.6 - row * 0.08));
|
|
// Crowd dots
|
|
for (var c2 = 0; c2 < 40; c2++) {
|
|
var cx = w * 0.04 + rng() * w * 0.92;
|
|
var crowdColor = H.pick(['#cc3333', '#3333cc', '#ffffff', '#ffcc00', '#33cc33', '#ff6600']);
|
|
var bob = Math.sin(t * 2 + c2 * 0.3 + row) * 1;
|
|
Draw.circle(ctx, cx, ry + h * 0.025 + bob, 2, H.rgba(crowdColor, 0.5));
|
|
}
|
|
}
|
|
// Bottom stands
|
|
for (var row2 = 0; row2 < 3; row2++) {
|
|
var ry2 = h * 0.87 + row2 * h * 0.04;
|
|
Draw.rect(ctx, w * 0.02, ry2, w * 0.96, h * 0.035, H.rgba('#444466', 0.5 - row2 * 0.1));
|
|
for (var c3 = 0; c3 < 30; c3++) {
|
|
var cx2 = w * 0.04 + rng() * w * 0.92;
|
|
Draw.circle(ctx, cx2, ry2 + h * 0.017, 2, H.rgba(H.pick(['#cc3333', '#3333cc', '#ffffff', '#ffcc00']), 0.4));
|
|
}
|
|
}
|
|
// Floodlights
|
|
for (var fl = 0; fl < 4; fl++) {
|
|
var fx = w * 0.05 + fl * w * 0.3;
|
|
Draw.rect(ctx, fx, h * 0.0, 4, h * 0.12, '#666');
|
|
Draw.rect(ctx, fx - 8, 0, 20, 8, '#888');
|
|
var lightPulse = 0.15 + Math.sin(t * 0.3 + fl * 0.5) * 0.03;
|
|
Draw.glow(ctx, fx + 2, h * 0.02, 50, H.rgba('#FFFFFF', lightPulse));
|
|
// Light cones on pitch
|
|
ctx.fillStyle = H.rgba('#FFFFFF', 0.02 + Math.sin(t * 0.2 + fl) * 0.005);
|
|
ctx.beginPath(); ctx.moveTo(fx, h * 0.08); ctx.lineTo(fx - w * 0.15, h * 0.85);
|
|
ctx.lineTo(fx + w * 0.15, h * 0.85); ctx.closePath(); ctx.fill();
|
|
}
|
|
},
|
|
particles: { type: 'sparkles', count: 12, color: '#FFFFFF' }
|
|
},
|
|
|
|
raceTrack: {
|
|
meta: { id: 'sports_racetrack', mood: 'fast', colors: ['#333333', '#555555', '#cc0000', '#ffffff'], tags: ['racing', 'speed', 'track', 'cars'], description: 'Asphalt race track with lane markings, kerbing, and grandstand', compatibleMusicMoods: ['energetic', 'fast', 'intense'], recommendedFor: ['racing', 'action', 'sports'] },
|
|
colors: ['#333333', '#555555', '#cc0000', '#ffffff'],
|
|
renderBase: function(ctx, w, h, c, Draw, t) {
|
|
// Sky
|
|
Draw.fillGradient(ctx, w, h, ['#4488bb', '#88bbdd'], 90);
|
|
// Green surroundings
|
|
Draw.rect(ctx, 0, h * 0.3, w, h * 0.7, '#3a6a2a');
|
|
},
|
|
renderMid: function(ctx, w, h, c, Draw, t) {
|
|
var rng = H.seededRandom(1101);
|
|
// Track surface (oval perspective)
|
|
ctx.fillStyle = c[0];
|
|
ctx.beginPath();
|
|
ctx.moveTo(w * 0.05, h * 0.85); ctx.lineTo(w * 0.2, h * 0.4);
|
|
ctx.lineTo(w * 0.8, h * 0.4); ctx.lineTo(w * 0.95, h * 0.85);
|
|
ctx.closePath(); ctx.fill();
|
|
// Inner grass
|
|
ctx.fillStyle = '#4a7a3a';
|
|
ctx.beginPath();
|
|
ctx.moveTo(w * 0.2, h * 0.82); ctx.lineTo(w * 0.3, h * 0.48);
|
|
ctx.lineTo(w * 0.7, h * 0.48); ctx.lineTo(w * 0.8, h * 0.82);
|
|
ctx.closePath(); ctx.fill();
|
|
// Lane markings
|
|
ctx.strokeStyle = H.rgba(c[3], 0.6); ctx.lineWidth = 2; ctx.setLineDash([15, 15]);
|
|
for (var l = 1; l < 4; l++) {
|
|
var t1 = l / 4;
|
|
ctx.beginPath();
|
|
ctx.moveTo(H.lerp(w * 0.05, w * 0.2, t1), H.lerp(h * 0.85, h * 0.82, t1));
|
|
ctx.lineTo(H.lerp(w * 0.2, w * 0.3, t1), H.lerp(h * 0.4, h * 0.48, t1));
|
|
ctx.lineTo(H.lerp(w * 0.8, w * 0.7, t1), H.lerp(h * 0.4, h * 0.48, t1));
|
|
ctx.lineTo(H.lerp(w * 0.95, w * 0.8, t1), H.lerp(h * 0.85, h * 0.82, t1));
|
|
ctx.stroke();
|
|
}
|
|
ctx.setLineDash([]);
|
|
// Kerbing (red-white stripes)
|
|
for (var k = 0; k < 20; k++) {
|
|
var kx1 = H.lerp(w * 0.2, w * 0.8, k / 20);
|
|
var kx2 = H.lerp(w * 0.2, w * 0.8, (k + 1) / 20);
|
|
Draw.rect(ctx, kx1, h * 0.39, kx2 - kx1, h * 0.02, k % 2 === 0 ? c[2] : c[3]);
|
|
}
|
|
// Start/finish line
|
|
for (var sq = 0; sq < 10; sq++) {
|
|
for (var sr = 0; sr < 3; sr++) {
|
|
var checker = (sq + sr) % 2 === 0;
|
|
Draw.rect(ctx, w * 0.48 + sq * 4, h * 0.65 + sr * 4, 4, 4, checker ? '#000' : '#fff');
|
|
}
|
|
}
|
|
// Grandstand
|
|
Draw.rect(ctx, w * 0.3, h * 0.25, w * 0.4, h * 0.12, '#556677');
|
|
for (var gr = 0; gr < 4; gr++) {
|
|
var gy = h * 0.26 + gr * h * 0.025;
|
|
for (var gs = 0; gs < 25; gs++) {
|
|
var gx = w * 0.32 + rng() * w * 0.36;
|
|
Draw.circle(ctx, gx, gy + h * 0.012, 1.5, H.rgba(H.pick(['#ff3333', '#3355cc', '#ffcc00', '#ffffff']), 0.5));
|
|
}
|
|
}
|
|
// Clouds
|
|
Draw.clouds(ctx, w, h, h * 0.08, 4, '#FFFFFF', 1102);
|
|
// Tire marks with animation
|
|
var skidOffset = (t * 40) % w;
|
|
ctx.strokeStyle = H.rgba('#222', 0.15); ctx.lineWidth = 3;
|
|
ctx.beginPath(); ctx.moveTo(w * 0.35, h * 0.75);
|
|
ctx.quadraticCurveTo(w * 0.45, h * 0.7, w * 0.55, h * 0.72); ctx.stroke();
|
|
},
|
|
particles: { type: 'dust', count: 15, color: '#AAAAAA' }
|
|
},
|
|
|
|
court: {
|
|
meta: { id: 'sports_court', mood: 'competitive', colors: ['#c47a34', '#e8944a', '#ffffff', '#2255aa'], tags: ['basketball', 'court', 'indoor', 'wood'], description: 'Indoor basketball court with polished hardwood, court lines, and arena lighting', compatibleMusicMoods: ['energetic', 'upbeat', 'competitive'], recommendedFor: ['sports', 'action', 'trivia'] },
|
|
colors: ['#c47a34', '#e8944a', '#ffffff', '#2255aa'],
|
|
renderBase: function(ctx, w, h, c, Draw, t) {
|
|
// Arena dark upper
|
|
Draw.fillGradient(ctx, w, h, ['#1a1a2a', '#2a2a3a'], 90);
|
|
// Hardwood floor
|
|
var floorY = h * 0.35;
|
|
Draw.rect(ctx, w * 0.03, floorY, w * 0.94, h * 0.55, c[0]);
|
|
// Wood grain lines
|
|
var rng = H.seededRandom(1201);
|
|
for (var g = 0; g < 20; g++) {
|
|
var gx = w * 0.03 + g * (w * 0.94 / 20);
|
|
ctx.strokeStyle = H.rgba(H.darken(c[0], 0.15), 0.3); ctx.lineWidth = 0.5;
|
|
ctx.beginPath(); ctx.moveTo(gx, floorY); ctx.lineTo(gx, floorY + h * 0.55); ctx.stroke();
|
|
}
|
|
},
|
|
renderMid: function(ctx, w, h, c, Draw, t) {
|
|
var rng = H.seededRandom(1202);
|
|
var floorY = h * 0.35;
|
|
// Court lines
|
|
ctx.strokeStyle = c[2]; ctx.lineWidth = 2;
|
|
ctx.strokeRect(w * 0.08, floorY + h * 0.03, w * 0.84, h * 0.49);
|
|
// Center line
|
|
ctx.beginPath(); ctx.moveTo(w * 0.5, floorY + h * 0.03); ctx.lineTo(w * 0.5, floorY + h * 0.52); ctx.stroke();
|
|
// Center circle
|
|
ctx.beginPath(); ctx.arc(w * 0.5, floorY + h * 0.275, h * 0.08, 0, Math.PI * 2); ctx.stroke();
|
|
// Three-point arcs
|
|
ctx.beginPath(); ctx.arc(w * 0.1, floorY + h * 0.275, h * 0.2, -0.9, 0.9); ctx.stroke();
|
|
ctx.beginPath(); ctx.arc(w * 0.9, floorY + h * 0.275, h * 0.2, Math.PI - 0.9, Math.PI + 0.9); ctx.stroke();
|
|
// Free throw lanes (paint)
|
|
ctx.fillStyle = H.rgba(c[3], 0.15);
|
|
ctx.fillRect(w * 0.08, floorY + h * 0.14, w * 0.12, h * 0.27);
|
|
ctx.fillRect(w * 0.8, floorY + h * 0.14, w * 0.12, h * 0.27);
|
|
ctx.strokeRect(w * 0.08, floorY + h * 0.14, w * 0.12, h * 0.27);
|
|
ctx.strokeRect(w * 0.8, floorY + h * 0.14, w * 0.12, h * 0.27);
|
|
// Backboards and hoops
|
|
for (var side = 0; side < 2; side++) {
|
|
var hx = side === 0 ? w * 0.09 : w * 0.91;
|
|
Draw.rect(ctx, hx - 8, floorY + h * 0.2, 16, h * 0.06, '#FFFFFF');
|
|
ctx.strokeStyle = '#FF4400'; ctx.lineWidth = 2;
|
|
ctx.beginPath(); ctx.arc(hx, floorY + h * 0.275, 8, 0, Math.PI * 2); ctx.stroke();
|
|
}
|
|
// Scoreboard
|
|
Draw.rect(ctx, w * 0.35, h * 0.04, w * 0.3, h * 0.08, '#111');
|
|
var scoreGlow = 0.7 + Math.sin(t * 0.5) * 0.15;
|
|
ctx.fillStyle = H.rgba('#ff3333', scoreGlow);
|
|
ctx.font = 'bold 14px monospace';
|
|
ctx.fillText('HOME', w * 0.38, h * 0.08);
|
|
ctx.fillText('AWAY', w * 0.55, h * 0.08);
|
|
ctx.fillStyle = H.rgba('#00ff44', scoreGlow);
|
|
ctx.fillText('88', w * 0.42, h * 0.1);
|
|
ctx.fillText('85', w * 0.58, h * 0.1);
|
|
// Arena lights
|
|
for (var lt = 0; lt < 6; lt++) {
|
|
var lx = w * 0.1 + lt * w * 0.16;
|
|
Draw.rect(ctx, lx, h * 0.13, w * 0.06, 4, '#ddd');
|
|
var intensity = 0.06 + Math.sin(t * 0.2 + lt * 0.4) * 0.015;
|
|
Draw.glow(ctx, lx + w * 0.03, h * 0.14, 40, H.rgba('#FFFFFF', intensity));
|
|
}
|
|
// Crowd in upper stands
|
|
for (var cr = 0; cr < 3; cr++) {
|
|
var cry = h * 0.15 + cr * h * 0.06;
|
|
for (var ci = 0; ci < 25; ci++) {
|
|
var ccx = w * 0.06 + rng() * w * 0.88;
|
|
Draw.circle(ctx, ccx, cry, 1.5, H.rgba(H.pick(['#cc3333', '#3355cc', '#ffffff', '#ffaa00']), 0.35));
|
|
}
|
|
}
|
|
// Floor reflection shimmer
|
|
var shimmer = 0.03 + Math.sin(t * 0.4) * 0.01;
|
|
ctx.fillStyle = H.rgba('#FFFFFF', shimmer);
|
|
ctx.fillRect(w * 0.03, floorY, w * 0.94, h * 0.55);
|
|
},
|
|
particles: { type: 'sparkles', count: 8, color: '#FFFFFF' }
|
|
},
|
|
|
|
field: {
|
|
meta: { id: 'sports_field', mood: 'fresh', colors: ['#2a7a2a', '#3a9a3a', '#ffffff', '#88cc44'], tags: ['soccer', 'field', 'outdoor', 'grass'], description: 'Open soccer field under blue sky with goals, corner flags, and freshly cut grass', compatibleMusicMoods: ['upbeat', 'cheerful', 'energetic'], recommendedFor: ['sports', 'action', 'pet'] },
|
|
colors: ['#2a7a2a', '#3a9a3a', '#ffffff', '#88cc44'],
|
|
renderBase: function(ctx, w, h, c, Draw, t) {
|
|
// Sky
|
|
Draw.fillGradient(ctx, w, h, ['#3388cc', '#88ccee', '#aaddee'], 90);
|
|
// Field with mowed stripes
|
|
for (var s = 0; s < 10; s++) {
|
|
var sx = s * (w / 10);
|
|
Draw.rect(ctx, sx, h * 0.45, w / 10, h * 0.55, s % 2 === 0 ? c[0] : c[1]);
|
|
}
|
|
},
|
|
renderMid: function(ctx, w, h, c, Draw, t) {
|
|
var rng = H.seededRandom(1301);
|
|
// Field markings
|
|
ctx.strokeStyle = c[2]; ctx.lineWidth = 2;
|
|
// Boundary
|
|
ctx.strokeRect(w * 0.05, h * 0.47, w * 0.9, h * 0.48);
|
|
// Halfway line
|
|
ctx.beginPath(); ctx.moveTo(w * 0.5, h * 0.47); ctx.lineTo(w * 0.5, h * 0.95); ctx.stroke();
|
|
// Center circle
|
|
ctx.beginPath(); ctx.arc(w * 0.5, h * 0.71, h * 0.08, 0, Math.PI * 2); ctx.stroke();
|
|
Draw.circle(ctx, w * 0.5, h * 0.71, 3, c[2]);
|
|
// Penalty areas
|
|
ctx.strokeRect(w * 0.05, h * 0.57, w * 0.1, h * 0.28);
|
|
ctx.strokeRect(w * 0.85, h * 0.57, w * 0.1, h * 0.28);
|
|
// Goal areas
|
|
ctx.strokeRect(w * 0.05, h * 0.63, w * 0.04, h * 0.16);
|
|
ctx.strokeRect(w * 0.91, h * 0.63, w * 0.04, h * 0.16);
|
|
// Goals (nets)
|
|
for (var g = 0; g < 2; g++) {
|
|
var gx = g === 0 ? w * 0.02 : w * 0.95;
|
|
var gw = w * 0.03;
|
|
Draw.rect(ctx, gx, h * 0.63, gw, h * 0.16, H.rgba('#FFFFFF', 0.15));
|
|
ctx.strokeStyle = '#FFFFFF'; ctx.lineWidth = 2;
|
|
ctx.strokeRect(gx, h * 0.63, gw, h * 0.16);
|
|
// Net mesh
|
|
ctx.strokeStyle = H.rgba('#FFFFFF', 0.2); ctx.lineWidth = 0.5;
|
|
for (var n = 0; n < 5; n++) {
|
|
ctx.beginPath(); ctx.moveTo(gx, h * 0.63 + n * h * 0.032); ctx.lineTo(gx + gw, h * 0.63 + n * h * 0.032); ctx.stroke();
|
|
ctx.beginPath(); ctx.moveTo(gx + n * gw / 5, h * 0.63); ctx.lineTo(gx + n * gw / 5, h * 0.79); ctx.stroke();
|
|
}
|
|
}
|
|
// Corner flags
|
|
for (var cf = 0; cf < 4; cf++) {
|
|
var cfx = cf < 2 ? w * 0.05 : w * 0.95;
|
|
var cfy = cf % 2 === 0 ? h * 0.47 : h * 0.95;
|
|
Draw.rect(ctx, cfx - 1, cfy - 16, 2, 16, '#888');
|
|
var flagWave = Math.sin(t * 3 + cf) * 3;
|
|
ctx.fillStyle = '#ff4444';
|
|
ctx.beginPath(); ctx.moveTo(cfx, cfy - 16); ctx.lineTo(cfx + 8 + flagWave, cfy - 13);
|
|
ctx.lineTo(cfx, cfy - 10); ctx.closePath(); ctx.fill();
|
|
}
|
|
// Clouds
|
|
Draw.clouds(ctx, w, h, h * 0.08, 5, '#FFFFFF', 1302);
|
|
// Sun
|
|
var sunPulse = 0.8 + Math.sin(t * 0.2) * 0.1;
|
|
Draw.circle(ctx, w * 0.85, h * 0.1, 20, H.rgba('#ffdd44', sunPulse));
|
|
Draw.glow(ctx, w * 0.85, h * 0.1, 50, '#ffdd44');
|
|
// Trees along side
|
|
Draw.trees(ctx, w, h, h * 0.45, 8, '#5a3a1a', '#2a6a2a', 1303);
|
|
},
|
|
particles: { type: 'leaves', count: 8, color: '#88CC44' }
|
|
},
|
|
|
|
arena: {
|
|
meta: { id: 'sports_arena', mood: 'intense', colors: ['#1a1a2e', '#2a2a44', '#ff4444', '#ffaa00'], tags: ['arena', 'boxing', 'mma', 'spotlight'], description: 'Dark fighting arena with dramatic spotlights, ropes, and smoky atmosphere', compatibleMusicMoods: ['intense', 'dramatic', 'energetic'], recommendedFor: ['action', 'sports', 'rpg'] },
|
|
colors: ['#1a1a2e', '#2a2a44', '#ff4444', '#ffaa00'],
|
|
renderBase: function(ctx, w, h, c, Draw, t) {
|
|
Draw.fillGradient(ctx, w, h, [c[0], c[1]], 90);
|
|
// Dark floor
|
|
Draw.rect(ctx, 0, h * 0.7, w, h * 0.3, H.darken(c[0], 0.3));
|
|
},
|
|
renderMid: function(ctx, w, h, c, Draw, t) {
|
|
var rng = H.seededRandom(1401);
|
|
// Ring platform
|
|
ctx.fillStyle = '#333344';
|
|
ctx.beginPath();
|
|
ctx.moveTo(w * 0.15, h * 0.7); ctx.lineTo(w * 0.25, h * 0.5);
|
|
ctx.lineTo(w * 0.75, h * 0.5); ctx.lineTo(w * 0.85, h * 0.7);
|
|
ctx.closePath(); ctx.fill();
|
|
// Ring canvas (mat)
|
|
ctx.fillStyle = '#e8e0d0';
|
|
ctx.beginPath();
|
|
ctx.moveTo(w * 0.22, h * 0.52); ctx.lineTo(w * 0.27, h * 0.48);
|
|
ctx.lineTo(w * 0.73, h * 0.48); ctx.lineTo(w * 0.78, h * 0.52);
|
|
ctx.closePath(); ctx.fill();
|
|
// Ring mat surface
|
|
ctx.fillStyle = H.rgba(c[2], 0.08);
|
|
ctx.beginPath();
|
|
ctx.moveTo(w * 0.22, h * 0.52); ctx.lineTo(w * 0.27, h * 0.48);
|
|
ctx.lineTo(w * 0.73, h * 0.48); ctx.lineTo(w * 0.78, h * 0.52);
|
|
ctx.closePath(); ctx.fill();
|
|
// Corner posts
|
|
var posts = [[w * 0.27, h * 0.48], [w * 0.73, h * 0.48], [w * 0.22, h * 0.52], [w * 0.78, h * 0.52]];
|
|
for (var p = 0; p < 4; p++) {
|
|
Draw.rect(ctx, posts[p][0] - 3, posts[p][1] - h * 0.12, 6, h * 0.12, '#888');
|
|
// Turnbuckle
|
|
Draw.circle(ctx, posts[p][0], posts[p][1] - h * 0.12, 4, p < 2 ? c[2] : c[3]);
|
|
}
|
|
// Ropes
|
|
for (var r = 0; r < 3; r++) {
|
|
var ropeY = h * 0.48 - (r + 1) * h * 0.03;
|
|
var sag = Math.sin(t * 0.5 + r) * 2;
|
|
ctx.strokeStyle = c[2]; ctx.lineWidth = 2;
|
|
// Top ropes
|
|
ctx.beginPath(); ctx.moveTo(w * 0.27, ropeY);
|
|
ctx.quadraticCurveTo(w * 0.5, ropeY + sag + 3, w * 0.73, ropeY); ctx.stroke();
|
|
// Side ropes (perspective)
|
|
var sideRopeY = ropeY + h * 0.02;
|
|
ctx.beginPath(); ctx.moveTo(w * 0.27, ropeY);
|
|
ctx.quadraticCurveTo(w * 0.245, sideRopeY + sag, w * 0.22, sideRopeY + h * 0.04); ctx.stroke();
|
|
ctx.beginPath(); ctx.moveTo(w * 0.73, ropeY);
|
|
ctx.quadraticCurveTo(w * 0.755, sideRopeY + sag, w * 0.78, sideRopeY + h * 0.04); ctx.stroke();
|
|
}
|
|
// Spotlights
|
|
for (var sp = 0; sp < 3; sp++) {
|
|
var spx = w * 0.3 + sp * w * 0.2;
|
|
var pulse = 0.12 + Math.sin(t * 0.6 + sp * 1.5) * 0.04;
|
|
Draw.glow(ctx, spx, 0, 30, H.rgba(c[3], 0.4));
|
|
ctx.fillStyle = H.rgba(c[3], pulse);
|
|
ctx.beginPath(); ctx.moveTo(spx - 15, 0); ctx.lineTo(spx + 15, 0);
|
|
ctx.lineTo(spx + w * 0.1, h * 0.7); ctx.lineTo(spx - w * 0.1, h * 0.7);
|
|
ctx.closePath(); ctx.fill();
|
|
}
|
|
// Crowd silhouettes
|
|
for (var cr = 0; cr < 50; cr++) {
|
|
var ccx = rng() * w;
|
|
var ccy = h * 0.7 + rng() * h * 0.15;
|
|
var bob = Math.sin(t * 1.5 + cr * 0.5) * 1.5;
|
|
Draw.circle(ctx, ccx, ccy + bob, 3 + rng() * 2, H.rgba('#333344', 0.7));
|
|
}
|
|
// Smoke effect
|
|
for (var sm = 0; sm < 4; sm++) {
|
|
var smx = w * 0.3 + rng() * w * 0.4 + Math.sin(t * 0.2 + sm) * 15;
|
|
var smy = h * 0.3 + rng() * h * 0.15;
|
|
ctx.fillStyle = H.rgba('#aaaacc', 0.03);
|
|
ctx.beginPath(); ctx.ellipse(smx, smy, 60 + rng() * 40, 20, 0, 0, Math.PI * 2); ctx.fill();
|
|
}
|
|
},
|
|
particles: { type: 'dust', count: 20, color: '#CCBBAA' }
|
|
},
|
|
|
|
pool: {
|
|
meta: { id: 'sports_pool', mood: 'calm', colors: ['#1a6088', '#2288aa', '#44bbdd', '#88eeff'], tags: ['swimming', 'water', 'pool', 'blue'], description: 'Olympic swimming pool with lane dividers, rippling water, and tiled edges', compatibleMusicMoods: ['calm', 'peaceful', 'ambient'], recommendedFor: ['sports', 'racing', 'puzzle'] },
|
|
colors: ['#1a6088', '#2288aa', '#44bbdd', '#88eeff'],
|
|
renderBase: function(ctx, w, h, c, Draw, t) {
|
|
// Indoor ceiling
|
|
Draw.fillGradient(ctx, w, h, ['#e8e4dc', '#d8d4cc'], 90);
|
|
// Pool water
|
|
var waterStart = h * 0.35;
|
|
ctx.fillStyle = Draw.linearGradient(ctx, 0, waterStart, 0, h, [
|
|
[0, c[1]], [0.5, c[2]], [1, c[0]]
|
|
]);
|
|
ctx.fillRect(w * 0.05, waterStart, w * 0.9, h * 0.55);
|
|
},
|
|
renderMid: function(ctx, w, h, c, Draw, t) {
|
|
var rng = H.seededRandom(1501);
|
|
var waterStart = h * 0.35;
|
|
var poolW = w * 0.9;
|
|
var poolX = w * 0.05;
|
|
// Lane dividers
|
|
var lanes = 8;
|
|
for (var l = 1; l < lanes; l++) {
|
|
var lx = poolX + l * (poolW / lanes);
|
|
ctx.strokeStyle = H.rgba('#FFFFFF', 0.3); ctx.lineWidth = 1;
|
|
ctx.beginPath(); ctx.moveTo(lx, waterStart); ctx.lineTo(lx, waterStart + h * 0.55); ctx.stroke();
|
|
// Lane rope beads
|
|
for (var b = 0; b < 15; b++) {
|
|
var by = waterStart + b * (h * 0.55 / 15);
|
|
var beadColor = b % 3 === 0 ? c[3] : (b % 3 === 1 ? '#ff4444' : '#ffffff');
|
|
Draw.circle(ctx, lx, by, 2, H.rgba(beadColor, 0.5));
|
|
}
|
|
}
|
|
// Water caustics (animated light patterns)
|
|
for (var ca = 0; ca < 25; ca++) {
|
|
var cx = poolX + rng() * poolW;
|
|
var cy = waterStart + rng() * h * 0.5;
|
|
var caSize = 15 + rng() * 30;
|
|
var caAlpha = 0.03 + Math.sin(t * 1.5 + ca * 0.7) * 0.02;
|
|
ctx.fillStyle = H.rgba(c[3], caAlpha);
|
|
ctx.beginPath();
|
|
ctx.moveTo(cx, cy - caSize * 0.3);
|
|
ctx.quadraticCurveTo(cx + caSize * 0.4, cy, cx, cy + caSize * 0.3);
|
|
ctx.quadraticCurveTo(cx - caSize * 0.4, cy, cx, cy - caSize * 0.3);
|
|
ctx.fill();
|
|
}
|
|
// Water surface ripples
|
|
for (var rp = 0; rp < 12; rp++) {
|
|
var ry = waterStart + rp * 2;
|
|
ctx.strokeStyle = H.rgba('#FFFFFF', 0.06);
|
|
ctx.lineWidth = 0.5;
|
|
ctx.beginPath();
|
|
for (var rx = poolX; rx < poolX + poolW; rx += 3) {
|
|
ctx.lineTo(rx, ry + Math.sin(rx * 0.03 + t * 2 + rp * 0.5) * 2);
|
|
}
|
|
ctx.stroke();
|
|
}
|
|
// Pool edge tiles
|
|
Draw.rect(ctx, poolX - 5, waterStart - 5, poolW + 10, 8, '#ddd');
|
|
Draw.rect(ctx, poolX - 5, waterStart + h * 0.55 - 3, poolW + 10, 8, '#ddd');
|
|
// Tile pattern on edge
|
|
for (var ti = 0; ti < 30; ti++) {
|
|
var tix = poolX + ti * (poolW / 30);
|
|
ctx.strokeStyle = H.rgba('#bbb', 0.4); ctx.lineWidth = 0.5;
|
|
ctx.beginPath(); ctx.moveTo(tix, waterStart - 5); ctx.lineTo(tix, waterStart + 3); ctx.stroke();
|
|
}
|
|
// Starting blocks
|
|
for (var sb = 0; sb < lanes; sb++) {
|
|
var sbx = poolX + sb * (poolW / lanes) + poolW / lanes / 2 - 8;
|
|
Draw.rect(ctx, sbx, waterStart - 12, 16, 8, '#888');
|
|
Draw.rect(ctx, sbx + 2, waterStart - 16, 12, 5, '#aaa');
|
|
}
|
|
// Lane numbers on bottom
|
|
ctx.font = '12px monospace'; ctx.fillStyle = H.rgba('#000', 0.15);
|
|
for (var ln = 0; ln < lanes; ln++) {
|
|
var lnx = poolX + ln * (poolW / lanes) + poolW / lanes / 2 - 3;
|
|
ctx.fillText(String(ln + 1), lnx, waterStart + h * 0.5);
|
|
}
|
|
// Ceiling lights reflected in water
|
|
for (var cl = 0; cl < 5; cl++) {
|
|
var clx = w * 0.15 + cl * w * 0.18;
|
|
Draw.rect(ctx, clx, h * 0.05, w * 0.08, 4, '#eee');
|
|
var refAlpha = 0.04 + Math.sin(t * 0.4 + cl) * 0.02;
|
|
Draw.glow(ctx, clx + w * 0.04, waterStart + h * 0.2, 40, H.rgba(c[3], refAlpha));
|
|
}
|
|
},
|
|
particles: { type: 'sparkles', count: 15, color: '#88EEFF' }
|
|
},
|
|
|
|
gym: {
|
|
meta: { id: 'sports_gym', mood: 'competitive', colors: ['#ddc8a0', '#c4a870', '#ffffff', '#cc3333'], tags: ['gymnastics', 'gym', 'mats', 'indoor'], description: 'Gymnastics arena with spring floor, apparatus silhouettes, and competition banners', compatibleMusicMoods: ['upbeat', 'focused', 'energetic'], recommendedFor: ['sports', 'action', 'creative'] },
|
|
colors: ['#ddc8a0', '#c4a870', '#ffffff', '#cc3333'],
|
|
renderBase: function(ctx, w, h, c, Draw, t) {
|
|
// Arena interior
|
|
Draw.fillGradient(ctx, w, h, ['#2a2a3a', '#3a3a4a'], 90);
|
|
// Spring floor
|
|
Draw.rect(ctx, w * 0.1, h * 0.6, w * 0.8, h * 0.3, c[0]);
|
|
Draw.rect(ctx, w * 0.1, h * 0.6, w * 0.8, h * 0.3, H.rgba(c[2], 0.05));
|
|
},
|
|
renderMid: function(ctx, w, h, c, Draw, t) {
|
|
var rng = H.seededRandom(1601);
|
|
// Floor exercise boundary
|
|
ctx.strokeStyle = c[2]; ctx.lineWidth = 2;
|
|
ctx.strokeRect(w * 0.12, h * 0.62, w * 0.76, h * 0.26);
|
|
// Diagonal line on floor
|
|
ctx.strokeStyle = H.rgba(c[3], 0.2); ctx.lineWidth = 1;
|
|
ctx.beginPath(); ctx.moveTo(w * 0.12, h * 0.62); ctx.lineTo(w * 0.88, h * 0.88); ctx.stroke();
|
|
ctx.beginPath(); ctx.moveTo(w * 0.88, h * 0.62); ctx.lineTo(w * 0.12, h * 0.88); ctx.stroke();
|
|
// Balance beam
|
|
Draw.rect(ctx, w * 0.03, h * 0.55, w * 0.18, h * 0.015, c[1]);
|
|
Draw.rect(ctx, w * 0.06, h * 0.565, 4, h * 0.12, '#666');
|
|
Draw.rect(ctx, w * 0.17, h * 0.565, 4, h * 0.12, '#666');
|
|
// Vault table
|
|
Draw.rect(ctx, w * 0.78, h * 0.54, w * 0.08, h * 0.03, '#888');
|
|
Draw.rect(ctx, w * 0.8, h * 0.57, 4, h * 0.1, '#666');
|
|
Draw.rect(ctx, w * 0.84, h * 0.57, 4, h * 0.1, '#666');
|
|
// Runway mat
|
|
Draw.rect(ctx, w * 0.88, h * 0.56, w * 0.1, h * 0.015, '#4488dd');
|
|
// Rings (hanging)
|
|
for (var ring = 0; ring < 2; ring++) {
|
|
var rx = w * 0.4 + ring * w * 0.06;
|
|
ctx.strokeStyle = '#888'; ctx.lineWidth = 1;
|
|
ctx.beginPath(); ctx.moveTo(rx, 0); ctx.lineTo(rx, h * 0.25); ctx.stroke();
|
|
var sway = Math.sin(t * 0.3 + ring * Math.PI) * 3;
|
|
ctx.strokeStyle = c[1]; ctx.lineWidth = 3;
|
|
ctx.beginPath(); ctx.arc(rx + sway, h * 0.28, 8, 0, Math.PI * 2); ctx.stroke();
|
|
}
|
|
// Parallel bars
|
|
Draw.rect(ctx, w * 0.55, h * 0.45, w * 0.15, 3, '#aa8855');
|
|
Draw.rect(ctx, w * 0.55, h * 0.5, w * 0.15, 3, '#aa8855');
|
|
Draw.rect(ctx, w * 0.58, h * 0.5, 3, h * 0.17, '#777');
|
|
Draw.rect(ctx, w * 0.67, h * 0.5, 3, h * 0.17, '#777');
|
|
// Scoreboard
|
|
Draw.rect(ctx, w * 0.3, h * 0.05, w * 0.4, h * 0.08, '#111');
|
|
ctx.strokeStyle = '#333'; ctx.lineWidth = 2;
|
|
ctx.strokeRect(w * 0.3, h * 0.05, w * 0.4, h * 0.08);
|
|
var scoreGlow = 0.7 + Math.sin(t * 0.6) * 0.2;
|
|
ctx.fillStyle = H.rgba('#00ff66', scoreGlow);
|
|
ctx.font = 'bold 12px monospace';
|
|
ctx.fillText('FLOOR 9.450', w * 0.35, h * 0.09);
|
|
// Competition banners
|
|
for (var bn = 0; bn < 5; bn++) {
|
|
var bx = w * 0.1 + bn * w * 0.18;
|
|
var bannerColor = H.pick([c[3], '#3355cc', '#ffcc00', '#33aa55', '#aa33aa']);
|
|
Draw.rect(ctx, bx, h * 0.14, 18, h * 0.08, bannerColor);
|
|
// Banner point
|
|
ctx.fillStyle = bannerColor;
|
|
ctx.beginPath(); ctx.moveTo(bx, h * 0.22); ctx.lineTo(bx + 9, h * 0.24); ctx.lineTo(bx + 18, h * 0.22); ctx.closePath(); ctx.fill();
|
|
}
|
|
// Arena lights
|
|
for (var al = 0; al < 4; al++) {
|
|
var alx = w * 0.15 + al * w * 0.22;
|
|
var lightAlpha = 0.06 + Math.sin(t * 0.25 + al * 0.6) * 0.015;
|
|
Draw.glow(ctx, alx, h * 0.14, 35, H.rgba('#FFFFFF', lightAlpha));
|
|
}
|
|
},
|
|
particles: { type: 'dust', count: 10, color: '#DDCC99' }
|
|
},
|
|
|
|
skatepark: {
|
|
meta: { id: 'sports_skatepark', mood: 'cool', colors: ['#555566', '#777788', '#ff6600', '#ffcc00'], tags: ['skate', 'urban', 'concrete', 'graffiti'], description: 'Urban skatepark with concrete ramps, half-pipes, graffiti, and warm sunset light', compatibleMusicMoods: ['cool', 'energetic', 'retro', 'upbeat'], recommendedFor: ['action', 'sports', 'racing', 'creative'] },
|
|
colors: ['#555566', '#777788', '#ff6600', '#ffcc00'],
|
|
renderBase: function(ctx, w, h, c, Draw, t) {
|
|
// Warm sunset sky
|
|
Draw.fillGradient(ctx, w, h, ['#dd6633', '#ee9955', '#ffcc88'], 90);
|
|
// Concrete ground
|
|
Draw.rect(ctx, 0, h * 0.55, w, h * 0.45, c[0]);
|
|
// Concrete texture
|
|
var rng = H.seededRandom(1700);
|
|
for (var sp = 0; sp < 30; sp++) {
|
|
Draw.circle(ctx, rng() * w, h * 0.55 + rng() * h * 0.45, 1, H.rgba('#444', 0.15));
|
|
}
|
|
},
|
|
renderMid: function(ctx, w, h, c, Draw, t) {
|
|
var rng = H.seededRandom(1701);
|
|
// Half-pipe
|
|
ctx.fillStyle = c[1];
|
|
ctx.beginPath();
|
|
ctx.moveTo(w * 0.05, h * 0.55); ctx.quadraticCurveTo(w * 0.05, h * 0.9, w * 0.2, h * 0.9);
|
|
ctx.lineTo(w * 0.2, h * 0.55); ctx.closePath(); ctx.fill();
|
|
// Coping on half-pipe
|
|
Draw.rect(ctx, w * 0.04, h * 0.54, w * 0.17, 3, '#999');
|
|
// Quarter pipe on right
|
|
ctx.fillStyle = c[1];
|
|
ctx.beginPath();
|
|
ctx.moveTo(w * 0.75, h * 0.9); ctx.quadraticCurveTo(w * 0.95, h * 0.9, w * 0.95, h * 0.6);
|
|
ctx.lineTo(w * 0.95, h * 0.55); ctx.lineTo(w * 0.75, h * 0.55); ctx.closePath(); ctx.fill();
|
|
Draw.rect(ctx, w * 0.74, h * 0.54, w * 0.22, 3, '#999');
|
|
// Fun box / pyramid
|
|
ctx.fillStyle = H.darken(c[0], 0.1);
|
|
ctx.beginPath();
|
|
ctx.moveTo(w * 0.35, h * 0.85); ctx.lineTo(w * 0.4, h * 0.72);
|
|
ctx.lineTo(w * 0.6, h * 0.72); ctx.lineTo(w * 0.65, h * 0.85);
|
|
ctx.closePath(); ctx.fill();
|
|
// Rail on top
|
|
Draw.rect(ctx, w * 0.4, h * 0.715, w * 0.2, 3, '#aaa');
|
|
// Grind rail
|
|
ctx.strokeStyle = '#bbb'; ctx.lineWidth = 3;
|
|
ctx.beginPath(); ctx.moveTo(w * 0.25, h * 0.8); ctx.lineTo(w * 0.4, h * 0.8); ctx.stroke();
|
|
Draw.rect(ctx, w * 0.25, h * 0.8, 3, h * 0.1, '#888');
|
|
Draw.rect(ctx, w * 0.4, h * 0.8, 3, h * 0.1, '#888');
|
|
// Graffiti on walls/ramps
|
|
var graffitiColors = ['#ff3366', '#33ccff', '#ffcc00', '#44ff44', '#ff6600', '#cc44ff'];
|
|
// Back wall
|
|
Draw.rect(ctx, 0, h * 0.2, w, h * 0.35, H.rgba(c[0], 0.5));
|
|
// Graffiti blobs
|
|
for (var g = 0; g < 8; g++) {
|
|
var gx = rng() * w * 0.9 + w * 0.05;
|
|
var gy = h * 0.25 + rng() * h * 0.22;
|
|
var gc = graffitiColors[Math.floor(rng() * graffitiColors.length)];
|
|
ctx.fillStyle = H.rgba(gc, 0.2 + rng() * 0.15);
|
|
ctx.beginPath();
|
|
ctx.ellipse(gx, gy, 15 + rng() * 25, 8 + rng() * 12, rng() * Math.PI, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
// Street light
|
|
Draw.rect(ctx, w * 0.7, h * 0.15, 3, h * 0.4, '#666');
|
|
Draw.rect(ctx, w * 0.68, h * 0.14, 20, 6, '#777');
|
|
var lampGlow = 0.3 + Math.sin(t * 0.4) * 0.1;
|
|
Draw.circle(ctx, w * 0.69, h * 0.17, 5, H.rgba(c[3], lampGlow));
|
|
Draw.glow(ctx, w * 0.69, h * 0.17, 40, H.rgba(c[3], lampGlow * 0.5));
|
|
// Fence in background
|
|
for (var f = 0; f < 15; f++) {
|
|
var fx = w * 0.02 + f * w * 0.07;
|
|
Draw.rect(ctx, fx, h * 0.35, 2, h * 0.2, H.rgba('#888', 0.3));
|
|
}
|
|
ctx.strokeStyle = H.rgba('#888', 0.25); ctx.lineWidth = 1;
|
|
ctx.beginPath(); ctx.moveTo(0, h * 0.4); ctx.lineTo(w, h * 0.4); ctx.stroke();
|
|
ctx.beginPath(); ctx.moveTo(0, h * 0.48); ctx.lineTo(w, h * 0.48); ctx.stroke();
|
|
// Warm sun glow
|
|
Draw.glow(ctx, w * 0.15, h * 0.08, 60, '#ffaa44');
|
|
Draw.circle(ctx, w * 0.15, h * 0.08, 18, H.rgba('#ffcc66', 0.8));
|
|
},
|
|
particles: { type: 'leaves', count: 10, color: '#CC8833' }
|
|
}
|
|
});
|
|
})(window.BackgroundEngine);
|