First commit
This commit is contained in:
commit
d3ba82be5d
47
index.html
Normal file
47
index.html
Normal file
@ -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>
|
307
photoCalendar.js
Normal file
307
photoCalendar.js
Normal file
@ -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();
|
||||||
|
});
|
71
style.css
Normal file
71
style.css
Normal file
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user