// Configuration // --------------------------------------------------------------------------------- const config = { logoSrc: 'img/logo.png', // Path to the logo image logoHeight: 50, // Logo height backgroundColor: '#090909', // Canvas background color lineColor: '#159262', // Default dashed line color lineThickness: 3, // Dashed line thickness randomizeYellowLines: true, // Enable or disable random yellow lines yellowLineColor: '#9d921d', // Yellow line color avatarBackgroundColor: '#090909', // Background color behind the avatar borderThickness: 5, // Border thickness around avatars borderColor: '#1bec99', // Border color around avatars urlText: '[ Create your own at ccc.cyber.to ]', // Text to display on the canvas urlFont: '16px monospace', // Font for URL text urlColor: '#159262', // URL text color avatarScale: 75, // Avatar scale percentage drawMazeBackground: true, // Whether to draw a random maze background mazeColor: '#05130f', // Color of the maze lines mazeThickness: 2, // Thickness of the maze lines drawRedLine: true, // Enable or disable the red line redLineColor: '#9B283C', // Red line color consoleLogging: false, // Enable or disable console logging }; // --------------------------------------------------------------------------------- // Converts degrees to radians const toRad = (x) => { if (config.consoleLogging) console.log('Function call: toRad'); return x * (Math.PI / 180); }; const dist = [200, 330, 450]; // Distance of each user layer from the center const numb = [8, 15, 26]; // Number of users in each layer const radius = [64, 58, 50]; // Radius of avatars in each layer let userNum = 0; // Counter for the number of users rendered let remainingImg = 0; // Counter for remaining images to load let totalImg = 0; // Total images to load function render(users, selfUser) { if (config.consoleLogging) console.log('Function call: render'); userNum = 0; remainingImg = 0; // Get canvas element const canvas = document.getElementById("canvas"); if (!canvas) return; const ctx = canvas.getContext("2d"); // Get canvas context const width = canvas.width; const height = canvas.height; // Draw maze if enabled if (config.drawMazeBackground) { drawMazeBackground(ctx, width, height); } // Configure canvas background ctx.fillStyle = config.backgroundColor; ctx.globalCompositeOperation = 'destination-over'; ctx.fillRect(0, 0, width, height); ctx.globalCompositeOperation = 'source-over'; const userPositions = []; const tasks = []; const mainAvatarRadius = 110 * (config.avatarScale / 100); // Scale main avatar totalImg += 1; remainingImg += 1; loadImage( ctx, selfUser.avatar, (width / 2) - mainAvatarRadius, (height / 2) - mainAvatarRadius, mainAvatarRadius, null, tasks ); // Position and render user avatars for (let layerIndex = 0; layerIndex < 3; layerIndex++) { const angleSize = 360 / numb[layerIndex]; for (let i = 0; i < numb[layerIndex]; i++) { if (userNum >= users.length) break; totalImg += 1; remainingImg += 1; const offset = layerIndex * 30; const r = toRad(i * angleSize + offset); const centerX = Math.cos(r) * dist[layerIndex] + width / 2; const centerY = Math.sin(r) * dist[layerIndex] + height / 2; userPositions.push({ x: centerX, y: centerY }); // Render connecting lines ctx.beginPath(); ctx.setLineDash([10, 3]); ctx.moveTo(width / 2, height / 2); ctx.lineTo(centerX, centerY); ctx.strokeStyle = config.lineColor; ctx.lineWidth = config.lineThickness; ctx.stroke(); ctx.setLineDash([]); loadImage( ctx, users[userNum].avatar, centerX - radius[layerIndex] * (config.avatarScale / 100), centerY - radius[layerIndex] * (config.avatarScale / 100), radius[layerIndex] * (config.avatarScale / 100), null, tasks ); userNum++; } } // Draw random yellow lines if enabled if (config.randomizeYellowLines) { randomizeYellowLines(ctx, userPositions, 10); } // Draw red line if enabled if (config.drawRedLine) { drawRedLine(ctx, width, height, userPositions); } const logo = new Image(); logo.onload = function () { // Load and draw logo const aspectRatio = logo.width / logo.height; const logoWidth = config.logoHeight * aspectRatio; ctx.drawImage(logo, 10, 10, logoWidth, config.logoHeight); }; logo.src = config.logoSrc; // Draw URL text ctx.font = config.urlFont; ctx.fillStyle = config.urlColor; const textWidth = ctx.measureText(config.urlText).width; const textXPosition = width - textWidth - 10; const textYPosition = height - 10; ctx.fillText(config.urlText, textXPosition, textYPosition); } // Draws a maze background function drawMazeBackground(ctx, width, height) { if (config.consoleLogging) console.log('Function call: drawMazeBackground'); const cellSize = 20; ctx.strokeStyle = config.mazeColor; ctx.lineWidth = config.mazeThickness; for (let y = 0; y < height; y += cellSize) { for (let x = 0; x < width; x += cellSize) { if (Math.random() > 0.5) { ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + cellSize, y + cellSize); ctx.stroke(); } else { ctx.beginPath(); ctx.moveTo(x + cellSize, y); ctx.lineTo(x, y + cellSize); ctx.stroke(); } } } } // Draw random yellow lines function randomizeYellowLines(ctx, userPositions, maxLines) { if (config.consoleLogging) console.log('Function call: randomizeYellowLines'); const numberOfLines = Math.min(maxLines, Math.floor(Math.random() * userPositions.length)); for (let i = 0; i < numberOfLines; i++) { const index1 = Math.floor(Math.random() * userPositions.length); let index2; do { index2 = Math.floor(Math.random() * userPositions.length); } while (index2 === index1); const startPos = userPositions[index1]; const endPos = userPositions[index2]; ctx.beginPath(); ctx.setLineDash([10, 3]); ctx.moveTo(startPos.x, startPos.y); ctx.lineTo(endPos.x, endPos.y); ctx.strokeStyle = config.yellowLineColor; ctx.lineWidth = config.lineThickness; ctx.stroke(); ctx.setLineDash([]); } } // Draw a random red line function drawRedLine(ctx, width, height, userPositions) { if (config.consoleLogging) console.log('Function call: drawRedLine'); if (userPositions.length === 0) return; const selectedPosition = userPositions[Math.floor(Math.random() * userPositions.length)]; ctx.beginPath(); ctx.setLineDash([10, 3]); ctx.moveTo(width / 2, height / 2); ctx.lineTo(selectedPosition.x, selectedPosition.y); ctx.strokeStyle = config.redLineColor; ctx.lineWidth = config.lineThickness; ctx.stroke(); ctx.setLineDash([]); } // Load and draw user images function loadImage(ctx, url, x, y, r, name, tasks) { if (config.consoleLogging) console.log('Function call: loadImage'); let progress = document.getElementById("outInfo"); if (!progress) return; tasks.push(() => { /* No text addition here */ }); // Decrement image counter const decrementRemaining = () => { remainingImg -= 1; progress.innerText = `Loading avatars: ${totalImg - remainingImg}/${totalImg}`; if (remainingImg <= 0) { progress.innerText = ""; tasks.forEach((task) => task()); } }; const img = new Image(); // Handle image loading errors const drawImageWithFallback = (imageSrc) => { img.onload = function() { ctx.save(); ctx.beginPath(); ctx.arc(x + r, y + r, r, 0, Math.PI * 2, true); ctx.closePath(); ctx.clip(); ctx.fillStyle = config.avatarBackgroundColor; ctx.beginPath(); ctx.arc(x + r, y + r, r, 0, Math.PI * 2, true); ctx.fill(); ctx.closePath(); ctx.drawImage(img, x, y, r * 2, r * 2); ctx.beginPath(); ctx.arc(x + r, y + r, r, 0, Math.PI * 2, true); ctx.lineWidth = config.borderThickness; ctx.strokeStyle = config.borderColor; ctx.stroke(); ctx.closePath(); ctx.restore(); decrementRemaining(); }; img.onerror = function() { console.error(`IMG Error: ${imageSrc}`); if (imageSrc !== 'img/placeholder.png') { drawImageWithFallback('img/placeholder.png'); } else { decrementRemaining(); } }; img.src = imageSrc; }; drawImageWithFallback(url); }