commit d3ba82be5d547bec7f889c93f378781c7dd6bb20 Author: jdg Date: Sun Jun 16 08:51:01 2024 +0000 First commit diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/index.html b/index.html new file mode 100644 index 0000000..af0f8ec --- /dev/null +++ b/index.html @@ -0,0 +1,47 @@ + + + + + PhotoCalendar + + + + +
+
+ + +
+
+
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+ +
+
+
+
+
+ +
+ + diff --git a/photoCalendar.js b/photoCalendar.js new file mode 100644 index 0000000..d6ecf15 --- /dev/null +++ b/photoCalendar.js @@ -0,0 +1,307 @@ +class PhotoCalendar { + dragOptions = { + isDragging: false, + lastZoom: 1, + MAX_ZOOM: 5, + MIN_ZOOM: 0.1, + SCROLL_SENSITIVITY: 0.0005, + startX: null, + startY: null, + x: null, + y: null, + dragging: false, + mouseOver: false, + }; + + options = { + calendarStartY: 690, + divideMonths: false, + fillStyle: "white", + img: null, + photoOffsetX: 0, + photoOffsetY: 0, + year: 2024, + centerMonths: false, + }; + + constructor() { + this.initControls(); + this.onYearChange(); + } + + overPhotoImg(x, y) { + this.mouseOver = y >= 0 && y <= this.options.calendarStartY; + return this.mouseOver; + } + 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 }; + } + } + onPointerDown(e) { + this.dragOptions.isDragging = true; + let x = this.getEventLocation(e).x; + let y = this.getEventLocation(e).y; + + if (this.overPhotoImg(x, y)) { + this.dragOptions.x = x - this.options.photoOffsetX; + this.dragOptions.y = y - this.options.photoOffsetY; + this.renderPhoto(); + return; + } + + this.dragOptions.x = x; + this.dragOptions.y = y; + } + dragStop() { + this.dragOptions.isDragging = false; + } + onPointerUp(e) { + this.dragStop(); + } + + 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; + } + handleTouch(e, singleTouchHandler) { + if (e.touches.length <= 1) { + singleTouchHandler(e); + + if (e.type == "touchend") { + e.preventDefault(); + this.dragOptions.mouseOver = false; + } + } + + + if (e.type == "touchmove" && e.touches.length == 2) + { + this.dragStop(); + } + } + + initControls() { + const self = this; + 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()); + + 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); + } + + renderMonth(calendar, month, canvas, x, y, w) { + const ctx = canvas.getContext("2d"); + const monthNames = ["Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"]; + const dayInitials = ["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); + } + } + } +} + +document.addEventListener("DOMContentLoaded", () => { + const app = new PhotoCalendar(); +}); diff --git a/style.css b/style.css new file mode 100644 index 0000000..c7d6116 --- /dev/null +++ b/style.css @@ -0,0 +1,71 @@ +body { + overflow: hidden; + font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; + font-size: 1rem; +} +.control { + padding-bottom: 0.5rem; +} +.preview canvas { + border: 1px solid black; +} + +.control.file input[type="file"] { + display: none; +} +.control.file label { + border-radius: 4px; + border: 1px solid black; + -display: block; + padding: 0.25rem; + text-align: center; +} + +.control.button button { + border-radius: 4px; + border: 1px solid black; + display: block; + padding: 0.25rem; + font-size: 1rem; +} + +@media (min-aspect-ratio: 1/1) { + /* Horizontal */ + .preview canvas { + position: absolute; + height: 100%; + top: 0; + right: 0; + } + + .row { + display: block; + } + .col50 { + width: 50%; + } +} + +@media (max-aspect-ratio: 1/1) { + /* Vertical */ + .preview canvas { + position: absolute; + width: 100%; + bottom: 0; + left: 0; + } + .control.file label { + width: 90%; + } + .control.button button { + width: 90%; + } + + .row { + width: 100%; + display: flex; + } + .col50 { + width: 50%; + } +}