class PhotoCalendar { dragOptions = { isDragging: false, x: null, y: null, dragging: false, }; options = { calendarStartY: 690, divideMonths: false, fillStyle: "white", img: null, photoOffsetX: 0, photoOffsetY: 0, year: null, centerMonths: false, }; constructor() { this.initDarkModeSwitch(); this.initControls(); this.onYearChange(); } initDarkModeSwitch() { function setDarkMode(darkModeSelected) { if (darkModeSelected) { document.documentElement.classList.add("darkMode"); } else { document.documentElement.classList.remove("darkMode"); } } const darkModekCheckbox = document.getElementById("darkModeSwitch_checkbox"); darkModekCheckbox.checked = localStorage.getItem("darkMode") || false; setDarkMode(darkModekCheckbox.checked); darkModekCheckbox.addEventListener("change", (e) => { localStorage.setItem("darkMode", e.target.checked); setDarkMode(e.target.checked); }); } overPhotoImg(x, y) { return y >= 0 && y <= this.options.calendarStartY; } getEventLocation(e) { if (e.touches && e.touches.length == 1) { return { x: e.touches[0].clientX, y: e.touches[0].clientY }; } else if (e.clientX && e.clientY) { return { x: e.clientX, y: e.clientY }; } } getMousePositionInCanvas(x, y) { const canvas = this.getCanvas(); const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const mouseX = (x - rect.left) * scaleX; const mouseY = (y - rect.top) * scaleY; return { x: mouseX, y: mouseY }; } onPointerDown(e) { let x = this.getEventLocation(e).x; let y = this.getEventLocation(e).y; const mousePosition = this.getMousePositionInCanvas(x, y); if (this.overPhotoImg(mousePosition.x, mousePosition.y)) { this.dragOptions.isDragging = true; this.dragOptions.x = x - this.options.photoOffsetX; this.dragOptions.y = y - this.options.photoOffsetY; this.renderPhoto(); return; } this.dragStop(); this.dragOptions.x = x; this.dragOptions.y = y; } onPointerMove(e) { if (this.dragOptions.isDragging) { // this.options.photoOffsetX = this.getEventLocation(e).x - this.dragOptions.x; this.options.photoOffsetY = this.getEventLocation(e).y - this.dragOptions.y; this.renderPhoto(); return; } // mouse over let x = this.getEventLocation(e).x; let y = this.getEventLocation(e).y; } onPointerUp(e) { this.dragStop(); } dragStop() { this.dragOptions.isDragging = false; } handleTouch(e, singleTouchHandler) { if (e.touches.length <= 1) { singleTouchHandler(e); if (e.type == "touchend") { e.preventDefault(); this.dragStop(); return; } } if (e.type == "touchmove" && e.touches.length == 2) { this.dragStop(); } } initControls() { const self = this; this.options.year = new Date().getFullYear() + 1; document.getElementById("year").value = this.options.year; document.getElementById("year").addEventListener("change", (e) => { self.options.year = e.target.value; self.onYearChange(); }); document.getElementById("showMonthSeparators").addEventListener("click", (e) => { self.options.divideMonths = e.target.checked; self.onYearChange(); }); document.getElementById("centerMonths").addEventListener("click", (e) => { self.options.centerMonths = e.target.checked; self.onYearChange(); }); document.getElementById("imageInput").addEventListener("change", (e) => self.handleImageUpload(e)); document.getElementById("downloadButton").addEventListener("click", (e) => self.downloadCanvas()); // Handle photo position const canvas = this.getCanvas(); canvas.addEventListener("mousedown", (e) => self.onPointerDown(e)); canvas.addEventListener("touchstart", (e) => self.handleTouch(e, (e) => self.onPointerDown(e))); canvas.addEventListener("mouseup", (e) => self.onPointerUp(e)); canvas.addEventListener("touchend", (e) => self.handleTouch(e, (e) => self.onPointerUp(e))); canvas.addEventListener("mousemove", (e) => self.onPointerMove(e)); canvas.addEventListener("touchmove", (e) => self.handleTouch(e, (e) => self.onPointerMove(e))); } handleImageUpload(event) { const self = this; const canvas = self.getCanvas(); const ctx = canvas.getContext("2d"); const file = event.target.files[0]; const reader = new FileReader(); reader.onload = function (e) { const img = new Image(); img.onload = function () { self.options.img = img; self.renderPhoto(); }; img.src = e.target.result; }; reader.readAsDataURL(file); } onYearChange() { const calendar = this.createCalendar(this.options.year); this.renderCalendar(calendar); } createCalendar(year) { const calendar = []; const daysInWeek = 7; const monthsInYear = 12; // Helper function to determine the number of days in a given month and year function getDaysInMonth(month, year) { return new Date(year, month + 1, 0).getDate(); } // Helper function to get the day of the week (0 is Monday, 6 is Sunday) function getDayOfWeek(year, month, day) { const date = new Date(year, month, day); const dayOfWeek = date.getDay(); return (dayOfWeek + 6) % 7; // Adjust so Monday is 0 and Sunday is 6 } for (let month = 0; month < monthsInYear; month++) { const daysInMonth = getDaysInMonth(month, year); const monthArray = []; let weekArray = Array(daysInWeek).fill(null); for (let day = 1; day <= daysInMonth; day++) { const dayOfWeek = getDayOfWeek(year, month, day); weekArray[dayOfWeek] = day; if (dayOfWeek === 6 || day === daysInMonth) { // End of the week or month monthArray.push(weekArray); weekArray = Array(daysInWeek).fill(null); } } calendar.push(monthArray); } return calendar; } getCanvas() { const canvas = document.getElementById("calendarCanvas"); return canvas; } renderPhoto() { const canvas = this.getCanvas(); const ctx = canvas.getContext("2d"); ctx.fillStyle = this.options.fillStyle; ctx.fillRect(0, 0, canvas.width, this.options.calendarStartY - 1); // Clear canvas if (this.options.img == null) { return; } // Calculate the aspect ratio and new height const aspectRatio = this.options.img.width / this.options.img.height; const newWidth = canvas.width; const newHeight = newWidth / aspectRatio; ctx.save(); ctx.beginPath(); ctx.rect(0, 0, canvas.width, this.options.calendarStartY - 1); ctx.clip(); ctx.drawImage(this.options.img, this.options.photoOffsetX, this.options.photoOffsetY, newWidth, newHeight); ctx.restore(); } downloadCanvas() { const canvas = this.getCanvas(); const link = document.createElement("a"); link.href = canvas.toDataURL("image/png"); link.download = "calendar.png"; link.click(); } renderCalendar(calendar) { const canvas = this.getCanvas(); const ctx = canvas.getContext("2d"); ctx.fillStyle = this.options.fillStyle; ctx.fillRect(0, 0, canvas.width, canvas.height); this.renderPhoto(); ctx.fillStyle = this.options.fillStyle; ctx.fillRect(0, this.options.calendarStartY, canvas.width, canvas.height); const monthWidth = canvas.width / 3; const monthHeigth = 30 + 30 * 8; for (let i = 0; i < 12; i++) { this.renderMonth(calendar, i, canvas, (i % 3) * monthWidth, parseInt(i / 3) * monthHeigth + this.options.calendarStartY, monthWidth); } } drawText(ctx, text, x, y) { ctx.textAlign = "left"; ctx.textBaseline = "middle"; ctx.fillText(text, x, y); } drawTextCentered(ctx, text, x, y, cellWidth) { const centerX = x + cellWidth / 2; ctx.textAlign = "center"; ctx.textBaseline = "middle"; ctx.fillText(text, centerX, y); } getMonthNames() { return Array.from(document.querySelectorAll("[data-months] span")).map((x) => x.innerHTML); } getWeekdayNames() { return Array.from(document.querySelectorAll("[data-weekdays] span")).map((x) => x.innerHTML); } renderMonth(calendar, month, canvas, x, y, w) { const ctx = canvas.getContext("2d"); const monthNames = this.getMonthNames(); //["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"]; const dayInitials = this.getWeekdayNames(); //["L", "M", "X", "J", "V", "S", "D"]; const cellWidth = w / 8; const cellHeight = 30; const headerHeight = 30; if (this.options.divideMonths) { ctx.beginPath(); ctx.rect(x, y, w, 30 + 30 * 8); ctx.stroke(); } x += cellWidth / 2; y += headerHeight; // Clear the area where the month will be rendered ctx.fillStyle = this.options.fillStyle; ctx.fillRect(x, y, 7 * cellWidth, calendar[month].length * cellHeight + headerHeight * 2); // Draw the month name ctx.fillStyle = "black"; ctx.font = "bold 28px Arial"; if (this.options.centerMonths) { this.drawTextCentered(ctx, monthNames[month], x, y, cellWidth * 7); } else { this.drawText(ctx, monthNames[month], x + cellWidth / 3, y); } // Draw the day initials header ctx.font = "24px Arial"; for (let i = 0; i < dayInitials.length; i++) { // ctx.fillText(dayInitials[i], x + i * cellWidth, y + headerHeight); this.drawTextCentered(ctx, dayInitials[i], x + i * cellWidth, y + headerHeight, cellWidth); } // Draw the days of the month ctx.font = "22px Arial"; const monthData = calendar[month]; for (let week = 0; week < monthData.length; week++) { for (let day = 0; day < monthData[week].length; day++) { const dayNumber = monthData[week][day]; const dayText = dayNumber !== null ? dayNumber : ""; // Set color to red for Saturday (5) and Sunday (6), else default color if (day === 5 || day === 6) { ctx.fillStyle = "red"; } else { ctx.fillStyle = "black"; } // ctx.fillText(dayText, x + day * cellWidth, y + headerHeight * 2 + week * cellHeight); this.drawTextCentered(ctx, dayText, x + day * cellWidth, y + headerHeight * 2 + week * cellHeight, cellWidth); } } } } function translate(lang, firstTime = false) { const en = { "Año:": "Year:", "Dividir meses": "Split months", "Centrar meses": "Center months", "Seleccionar Foto": "Select image", "Descargar foto-calendario": "Download photo-calendar", "Buy me a coffe": "Buy me a coffe", "Support my work": "Support my work", L: "M", M: "T", X: "W", J: "T", V: "F", S: "S", D: "S", Enero: "January", Febrero: "February", Marzo: "March", Abril: "April", Mayo: "May", June: "June", Julio: "July", Agosto: "August", Septiembre: "September", Octubre: "October", Noviembre: "November", Diciembre: "December", }; Array.from(document.querySelectorAll("[data-translate]")).forEach((x) => { if (firstTime) { x.dataset.translate = x.innerHTML; } if (en.hasOwnProperty(x.dataset.translate)) { if (lang == "es") { x.innerHTML = x.dataset.translate; } else { x.innerHTML = en[x.dataset.translate]; } } }); const languageSelector = document.getElementById("language"); languageSelector.value = lang; } document.addEventListener("DOMContentLoaded", () => { const app = new PhotoCalendar(); const userLanguage = navigator.language || navigator.userLanguage; const languageCode = userLanguage.split("-")[0]; if (languageCode != "es") { console.log("Language Code", languageCode); translate(languageCode, true); app.onYearChange(); } document.getElementById("language").addEventListener("change", (e) => { translate(e.target.value); app.onYearChange(); }); });