From d3ba82be5d547bec7f889c93f378781c7dd6bb20 Mon Sep 17 00:00:00 2001
From: jdg <jd@infdj.com>
Date: Sun, 16 Jun 2024 08:51:01 +0000
Subject: [PATCH] First commit

---
 README.md        |   0
 index.html       |  47 ++++++++
 photoCalendar.js | 307 +++++++++++++++++++++++++++++++++++++++++++++++
 style.css        |  71 +++++++++++
 4 files changed, 425 insertions(+)
 create mode 100644 README.md
 create mode 100644 index.html
 create mode 100644 photoCalendar.js
 create mode 100644 style.css

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 @@
+<html>
+  <head>
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <meta name="mobile-web-app-capable" content="yes" />
+    <title>PhotoCalendar</title>
+    <script src="photoCalendar.js"></script>
+    <link rel="stylesheet" type="text/css" href="style.css" />
+  </head>
+  <body>
+    <div class="controls">
+      <div class="control select">
+        <label>Año:</label>
+        <input type="number" min="2024" step="1" value="2024" id="year" />
+      </div>
+      <div class="row">
+        <div class="col50">
+          <div class="control checkbox">
+            <input type="checkbox" id="showMonthSeparators" />
+            <label for="showMonthSeparators">Dividir meses</label>
+          </div>
+        </div>
+        <div class="col50">
+          <div class="control checkbox">
+            <input type="checkbox" id="centerMonths" />
+            <label for="centerMonths">Centrar meses</label>
+          </div>
+        </div>
+      </div>
+      <div class="row">
+        <div class="col50">
+          <div class="control file">
+            <label for="imageInput">Seleccionar Foto</label>
+            <input type="file" id="imageInput" accept="image/*" />
+          </div>
+        </div>
+        <div class="col50">
+          <div class="control button">
+            <button id="downloadButton">Descargar foto-calendario</button>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="preview">
+      <canvas id="calendarCanvas" width="1181" height="1771"></canvas>
+    </div>
+  </body>
+</html>
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%;
+  }
+}