/** * ChronoCanvas — Calendar Wallpaper Generator * Shared utilities and index.html logic */ // ========================================== // Device Detection & Presets // ========================================== const DEVICE_PRESETS = { // iPhone iphone15pm: { width: 1290, height: 2796, platform: 'ios', name: 'iPhone 15 Pro Max' }, iphone15p: { width: 1179, height: 2556, platform: 'ios', name: 'iPhone 15 Pro' }, iphone15: { width: 1179, height: 2556, platform: 'ios', name: 'iPhone 15' }, iphone14pm: { width: 1290, height: 2796, platform: 'ios', name: 'iPhone 14 Pro Max' }, iphone14p: { width: 1179, height: 2556, platform: 'ios', name: 'iPhone 14 Pro' }, iphone13: { width: 1170, height: 2532, platform: 'ios', name: 'iPhone 13/14' }, iphonese: { width: 750, height: 1334, platform: 'ios', name: 'iPhone SE' }, // Android pixel8p: { width: 1344, height: 2992, platform: 'android', name: 'Pixel 8 Pro' }, pixel8: { width: 1080, height: 2400, platform: 'android', name: 'Pixel 8' }, s24u: { width: 1440, height: 3120, platform: 'android', name: 'Galaxy S24 Ultra' }, s24: { width: 1080, height: 2340, platform: 'android', name: 'Galaxy S24' }, // Desktop macbook14: { width: 3024, height: 1964, platform: 'desktop', name: 'MacBook 14"' }, macbook16: { width: 3456, height: 2234, platform: 'desktop', name: 'MacBook 16"' }, imac: { width: 5120, height: 2880, platform: 'desktop', name: 'iMac 5K' }, fhd: { width: 1920, height: 1080, platform: 'desktop', name: 'Full HD' }, '4k': { width: 3840, height: 2160, platform: 'desktop', name: '4K UHD' } }; const ACCENT_COLORS = { primary: { 500: '#0ea5e9', 600: '#0284c7' }, secondary: { 500: '#d946ef', 600: '#c026d3' }, emerald: { 500: '#10b981', 600: '#059669' }, amber: { 500: '#f59e0b', 600: '#d97706' }, rose: { 500: '#f43f5e', 600: '#e11d48' }, violet: { 500: '#8b5cf6', 600: '#7c3aed' } }; // ========================================== // Utility Functions // ========================================== function detectPlatform() { const ua = navigator.userAgent.toLowerCase(); if (/iphone|ipad|ipod/.test(ua)) return 'ios'; if (/android/.test(ua)) return 'android'; return 'desktop'; } function detectResolution() { const dpr = window.devicePixelRatio || 1; // Use screen dimensions for wallpaper generation // For mobile, we want the native resolution const width = Math.round(window.screen.width * dpr); const height = Math.round(window.screen.height * dpr); return { width, height, dpr }; } function findBestPreset(width, height, platform) { // Try to find matching preset for (const [key, preset] of Object.entries(DEVICE_PRESETS)) { if (preset.platform === platform && Math.abs(preset.width - width) < 100 && Math.abs(preset.height - height) < 100) { return { key, ...preset }; } } return null; } function getMonthData(year, month) { const firstDay = new Date(year, month, 1); const lastDay = new Date(year, month + 1, 0); const daysInMonth = lastDay.getDate(); const startDayOfWeek = firstDay.getDay(); // 0 = Sunday const weeks = []; let currentWeek = []; // Fill leading empty days for (let i = 0; i < startDayOfWeek; i++) { currentWeek.push(null); } // Fill days for (let day = 1; day <= daysInMonth; day++) { currentWeek.push(day); if (currentWeek.length === 7) { weeks.push(currentWeek); currentWeek = []; } } // Fill trailing empty days if (currentWeek.length > 0) { while (currentWeek.length < 7) { currentWeek.push(null); } weeks.push(currentWeek); } return { weeks, daysInMonth, startDayOfWeek }; } // ========================================== // Canvas Rendering Engine // ========================================== function renderCalendarWallpaper(canvas, options) { const { width, height, theme = 'dark', accent = 'primary', showSeconds = false, showWeekday = true, showYear = true, date = new Date() } = options; const ctx = canvas.getContext('2d'); const colors = ACCENT_COLORS[accent] || ACCENT_COLORS.primary; // Set canvas size canvas.width = width; canvas.height = height; // Theme colors const isDark = theme === 'dark'; const bgColor = isDark ? '#0f172a' : '#ffffff'; const textColor = isDark ? '#f8fafc' : '#1e293b'; const mutedColor = isDark ? '#94a3b8' : '#64748b'; const surfaceColor = isDark ? 'rgba(30, 41, 59, 0.6)' : 'rgba(241, 245, 249, 0.8)'; const gridColor = isDark ? 'rgba(148, 163, 184, 0.1)' : 'rgba(100, 116, 139, 0.1)'; // Background ctx.fillStyle = bgColor; ctx.fillRect(0, 0, width, height); // Subtle gradient overlay const gradient = ctx.createLinearGradient(0, 0, width, height); if (isDark) { gradient.addColorStop(0, 'rgba(14, 165, 233, 0.08)'); gradient.addColorStop(0.5, 'transparent'); gradient.addColorStop(1, 'rgba(217, 70, 239, 0.05)'); } else { gradient.addColorStop(0, 'rgba(14, 165, 233, 0.05)'); gradient.addColorStop(0.5, 'transparent'); gradient.addColorStop(1, 'rgba(217, 70, 239, 0.03)'); } ctx.fillStyle = gradient; ctx.fillRect(0, 0, width, height); // Calculate layout based on aspect ratio const isPortrait = height > width; const padding = Math.min(width, height) * 0.08; const availableWidth = width - padding * 2; const availableHeight = height - padding * 2; // Scale factor based on resolution const baseScale = Math.min(width, height) / 1000; const scale = Math.max(baseScale, 1); let currentY = padding; // Date display const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; const weekdayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']; const currentMonth = date.getMonth(); const currentYear = date.getFullYear(); const currentDay = date.getDate(); const currentWeekday = date.getDay(); // Header section if (showWeekday) { ctx.font = `300 ${24 * scale}px system-ui, -apple-system, sans-serif`; ctx.fillStyle = colors[500]; ctx.textAlign = 'left'; ctx.fillText(weekdayNames[currentWeekday], padding, currentY + 24 * scale); currentY += 40 * scale; } // Large date ctx.font = `200 ${120 * scale}px system-ui, -apple-system, sans-serif`; ctx.fillStyle = textColor; ctx.fillText(String(currentDay).padStart(2, '0'), padding, currentY + 100 * scale); currentY += 130 * scale; // Month and year ctx.font = `400 ${28 * scale}px system-ui, -apple-system, sans-serif`; ctx.fillStyle = mutedColor; const yearText = showYear ? ` ${currentYear}` : ''; ctx.fillText(`${monthNames[currentMonth]}${yearText}`, padding, currentY); currentY += 60 * scale; // Time (optional) const hours = date.getHours(); const minutes = date.getMinutes(); const seconds = date.getSeconds(); const ampm = hours >= 12 ? 'PM' : 'AM'; const displayHours = hours % 12 || 12; const timeString = `${displayHours}:${String(minutes).padStart(2, '0')}${showSeconds ? ':' + String(seconds).padStart(2, '0') : ''} ${ampm}`; ctx.font = `300 ${20 * scale}px system-ui, -apple-system, sans-serif`; ctx.fillStyle = mutedColor; ctx.fillText(timeString, padding, currentY); currentY += 50 * scale; // Calendar grid const { weeks } = getMonthData(currentYear, currentMonth); const dayNames = ['S', 'M', 'T', 'W', 'T', 'F', 'S']; // Calendar container const calendarTop = currentY; const cellSize = Math.min(availableWidth / 7, (availableHeight - currentY + padding) / (weeks.length + 2)); const calendarWidth = cellSize * 7; const startX = (width - calendarWidth) / 2; // Day headers ctx.font = `500 ${14 * scale}px system-ui, -apple-system, sans-serif`; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; dayNames.forEach((day, i) => { const x = startX + i * cellSize + cellSize / 2; const y = calendarTop + cellSize / 2; ctx.fillStyle = i === 0 || i === 6 ? colors[500] : mutedColor; ctx.fillText(day, x, y); }); // Calendar days const today = new Date(); const isCurrentMonth = today.getMonth() === currentMonth && today.getFullYear() === currentYear; weeks.forEach((week, weekIndex) => { const rowY = calendarTop + (weekIndex + 1.5) * cellSize; week.forEach((day, dayIndex) => { if (!day) return; const x = startX + dayIndex * cellSize + cellSize / 2; const y = rowY; const isToday = isCurrentMonth && day === currentDay; // Today highlight if (isToday) { ctx.beginPath(); ctx.arc(x, y, cellSize * 0.35, 0, Math.PI * 2); ctx.fillStyle = colors[500]; ctx.fill(); // Glow effect ctx.beginPath(); ctx.arc(x, y, cellSize * 0.45, 0, Math.PI * 2); ctx.fillStyle = colors[500] + '20'; ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = `600 ${16 * scale}px system-ui, -apple-system, sans-serif`; } else { ctx.fillStyle = textColor; ctx.font = `400 ${16 * scale}px system-ui, -apple-system, sans-serif`; } ctx.fillText(String(day), x, y); }); }); // Decorative elements // Subtle grid lines ctx.strokeStyle = gridColor; ctx.lineWidth = 1; for (let i = 0; i <= 7; i++) { const x = startX + i * cellSize; ctx.beginPath(); ctx.moveTo(x, calendarTop + cellSize); ctx.lineTo(x, calendarTop + (weeks.length + 1) * cellSize); ctx.stroke(); } for (let i = 0; i <= weeks.length + 1; i++) { const y = calendarTop + i * cellSize; ctx.beginPath(); ctx.moveTo(startX, y); ctx.lineTo(startX + calendarWidth, y); ctx.stroke(); } // Bottom branding (subtle) ctx.font = `300 ${12 * scale}px system-ui, -apple-system, sans-serif`; ctx.fillStyle = isDark ? 'rgba(148, 163, 184, 0.3)' : 'rgba(100, 116, 139, 0.3)'; ctx.textAlign = 'center'; ctx.fillText('ChronoCanvas', width / 2, height - padding / 2); } // ========================================== // URL Parameter Handling // ========================================== function buildRenderUrl(params) { const url = new URL('render.html', window.location.href); Object.entries(params).forEach(([key, value]) => { if (value !== undefined && value !== null && value !== '') { url.searchParams.set(key, String(value)); } }); return url.toString(); } function parseRenderUrl(urlString) { const url = new URL(urlString); const params = {}; url.searchParams.forEach((value, key) => { params[key] = value; }); return params; } // ========================================== // Index Page Logic // ========================================== function initIndexPage() { // DOM Elements const platformInput = document.getElementById('platform'); const widthInput = document.getElementById('width'); const heightInput = document.getElementById('height'); const themeInput = document.getElementById('theme'); const accentInput = document.getElementById('accent'); const presetSelect = document.getElementById('preset'); const deviceLabelInput = document.getElementById('device-label'); const showSecondsInput = document.getElementById('show-seconds'); const showWeekdayInput = document.getElementById('show-weekday'); const showYearInput = document.getElementById('show-year'); const previewCanvas = document.getElementById('preview-canvas'); const previewLoading = document.getElementById('preview-loading'); const generatedUrlInput = document.getElementById('generated-url'); const generateBtn = document.getElementById('generate-btn'); const copyBtn = document.getElementById('copy-btn'); const openRenderBtn = document.getElementById('open-render'); const detectedDeviceEl = document.getElementById('detected-device'); const toast = document.getElementById('toast'); const toastMessage = document.getElementById('toast-message'); // Platform buttons const platformBtns = document.querySelectorAll('.platform-btn'); const themeBtns = document.querySelectorAll('.theme-btn'); const accentBtns = document.querySelectorAll('.accent-btn'); // State let currentParams = {}; // Auto-detect device function autoDetect() { const platform = detectPlatform(); const { width, height, dpr } = detectResolution(); // Find best preset const preset = findBestPreset(width, height, platform); // Update UI platformInput.value = platform; updatePlatformButtons(); if (preset) { widthInput.value = preset.width; heightInput.value = preset.height; presetSelect.value = preset.key; deviceLabelInput.value = preset.name; detectedDeviceEl.textContent = `${preset.name} (${width}×${height} @ ${dpr}x)`; } else { // Use detected resolution directly widthInput.value = width; heightInput.value = height; presetSelect.value = ''; deviceLabelInput.value = ''; detectedDeviceEl.textContent = `${platform} (${width}×${height} @ ${dpr}x)`; } updatePreview(); } function updatePlatformButtons() { const value = platformInput.value; platformBtns.forEach(btn => { btn.classList.toggle('active', btn.dataset.value === value); }); } function updateThemeButtons() { const value = themeInput.value; themeBtns.forEach(btn => { btn.classList.toggle('active', btn.dataset.value === value); }); } function updateAccentButtons() { const value = accentInput.value; accentBtns.forEach(btn => { btn.classList.toggle('active', btn.dataset.value === value); }); } function getCurrentParams() { return { width: parseInt(widthInput.value) || 1179, height: parseInt(heightInput.value) || 2556, platform: platformInput.value, theme: themeInput.value, accent: accentInput.value, label: deviceLabelInput.value, seconds: showSecondsInput.checked ? '1' : '0', weekday: showWeekdayInput.checked ? '1' : '0', year: showYearInput.checked ? '1' : '0' }; } function updatePreview() { const params = getCurrentParams(); currentParams = params; // Generate preview at smaller size const previewWidth = 400; const aspectRatio = params.height / params.width; const previewHeight = Math.round(previewWidth * aspectRatio); previewCanvas.width = previewWidth; previewCanvas.height = previewHeight; renderCalendarWallpaper(previewCanvas, { width: previewWidth, height: previewHeight, theme: params.theme, accent: params.accent, showSeconds: params.seconds === '1', showWeekday: params.weekday === '1', showYear: params.year === '1', date: new Date() }); previewLoading.classList.add('hidden'); // Update URL const renderUrl = buildRenderUrl(params); generatedUrlInput.value = renderUrl; openRenderBtn.href = renderUrl; } function showToast(message) { toastMessage.textContent = message; toast.classList.remove('translate-y-20', 'opacity-0'); setTimeout(() => { toast.classList.add('translate-y-20', 'opacity-0'); }, 2000); } // Event Listeners platformBtns.forEach(btn => { btn.addEventListener('click', () => { platformInput.value = btn.dataset.value; updatePlatformButtons(); updatePreview(); }); }); themeBtns.forEach(btn => { btn.addEventListener('click', () => { themeInput.value = btn.dataset.value; updateThemeButtons(); updatePreview(); }); }); accentBtns.forEach(btn => { btn.addEventListener('click', () => { accentInput.value = btn.dataset.value; updateAccentButtons(); updatePreview(); }); }); presetSelect.addEventListener('change', () => { const key = presetSelect.value; if (key && DEVICE_PRESETS[key]) { const preset = DEVICE_PRESETS[key]; widthInput.value = preset.width; heightInput.value = preset.height; platformInput.value = preset.platform; deviceLabelInput.value = preset.name; updatePlatformButtons(); } updatePreview(); }); [widthInput, heightInput, deviceLabelInput].forEach(el => { el.addEventListener('input', updatePreview); }); [showSecondsInput, showWeekdayInput, showYearInput].forEach(el => { el.addEventListener('change', updatePreview); }); generateBtn.addEventListener('click', updatePreview); copyBtn.addEventListener('click', async () => { try { await navigator.clipboard.writeText(generatedUrlInput.value); showToast('Copied to clipboard!'); } catch (err) { // Fallback generatedUrlInput.select(); document.execCommand('copy'); showToast('Copied to clipboard!'); } }); // Initialize updateThemeButtons(); updateAccentButtons(); autoDetect(); } // ========================================== // Render Page Logic // ========================================== function initRenderPage() { const canvas = document.getElementById('wallpaper-canvas'); const loading = document.getElementById('loading'); // Parse URL parameters const urlParams = new URLSearchParams(window.location.search); // Get parameters with fallbacks const getParam = (name, defaultValue, parser = String) => { const value = urlParams.get(name); return value !== null ? parser(value) : defaultValue; }; // Auto-detect if needed const { width: detectedWidth, height: detectedHeight } = detectResolution(); const detectedPlatform = detectPlatform(); const params = { width: getParam('width', detectedWidth, parseInt), height: getParam('height', detectedHeight, parseInt), theme: getParam('theme', 'dark'), accent: getParam('accent', 'primary'), showSeconds: getParam('seconds', '0') === '1', showWeekday: getParam('weekday', '1') === '1', showYear: getParam('year', '1') === '1' }; // Validate dimensions params.width = Math.max(320, Math.min(8192, params.width)); params.height = Math.max(320, Math.min(8192, params.height)); // Render function render() { renderCalendarWallpaper(canvas, { width: params.width, height: params.height, theme: params.theme, accent: params.accent, showSeconds: params.showSeconds, showWeekday: params.showWeekday, showYear: params.showYear, date: new Date() }); // Hide loading loading.classList.add('hidden'); // For automation: update title with timestamp document.title = `ChronoCanvas — ${new Date().toLocaleString()}`; } // Initial render render(); // Auto-refresh for clock if showing seconds if (params.showSeconds) { setInterval(render, 1000); } else { // Refresh every minute to update time setInterval(render, 60000); } } // ========================================== // Initialize // ========================================== document.addEventListener('DOMContentLoaded', () => { // Detect which page we're on const isRenderPage = document.getElementById('wallpaper-canvas') !== null; if (isRenderPage) { initRenderPage(); } else { initIndexPage(); } });