// State
let texts = [];
let panels = [];
let currentPanelIndex = 0;
let backgroundImage = null;
let originalImage = null;
let settings = {
fontSize: 18,
fontFamily: "Arial",
textColor: "#ffffff",
alignment: "left",
sidePadding: 10,
centerOffset: 0,
bgBrightness: 100,
bgContrast: 100,
};
// Crop state
let cropBox = {
x: 0,
y: 0,
width: 0,
height: 0,
};
let isDragging = false;
let isResizing = false;
let resizeHandle = null;
let dragStart = { x: 0, y: 0 };
let cropBoxStart = { x: 0, y: 0, width: 0, height: 0 };
// Initialize
document.addEventListener("DOMContentLoaded", () => {
initializeTheme();
initializeEventListeners();
loadDefaultBackground();
addDefaultTexts();
});
// Theme Management
function initializeTheme() {
const savedTheme = localStorage.getItem("theme") || "light";
document.documentElement.setAttribute("data-theme", savedTheme);
}
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute("data-theme");
const newTheme = currentTheme === "light" ? "dark" : "light";
document.documentElement.setAttribute("data-theme", newTheme);
localStorage.setItem("theme", newTheme);
}
// Event Listeners
function initializeEventListeners() {
// Theme toggle
document.getElementById("themeToggle").addEventListener("click", toggleTheme);
// Add text
document.getElementById("addTextBtn").addEventListener("click", addText);
document.getElementById("panelTextInput").addEventListener("keypress", (e) => {
if (e.key === "Enter") addText();
});
// Settings
document.getElementById("fontSize").addEventListener("input", (e) => {
settings.fontSize = parseInt(e.target.value);
document.getElementById("fontSizeValue").textContent = e.target.value;
updateAllPanels();
});
document.getElementById("fontFamily").addEventListener("change", (e) => {
settings.fontFamily = e.target.value;
updateAllPanels();
});
document.getElementById("textColor").addEventListener("input", (e) => {
settings.textColor = e.target.value;
document.getElementById("textColorValue").textContent = e.target.value;
updateAllPanels();
});
document.querySelectorAll(".align-btn").forEach((btn) => {
btn.addEventListener("click", (e) => {
document.querySelectorAll(".align-btn").forEach((b) => b.classList.remove("active"));
btn.classList.add("active");
settings.alignment = btn.dataset.align;
updateAllPanels();
});
});
document.getElementById("sidePadding").addEventListener("input", (e) => {
settings.sidePadding = parseInt(e.target.value);
document.getElementById("sidePaddingValue").textContent = e.target.value;
updateAllPanels();
});
document.getElementById("centerOffset").addEventListener("input", (e) => {
settings.centerOffset = parseInt(e.target.value);
document.getElementById("centerOffsetValue").textContent = e.target.value;
updateAllPanels();
});
// Background controls
document.getElementById("uploadBgBtn").addEventListener("click", () => {
document.getElementById("bgImageInput").click();
});
document.getElementById("bgImageInput").addEventListener("change", handleBackgroundUpload);
document.getElementById("bgBrightness").addEventListener("input", (e) => {
settings.bgBrightness = parseInt(e.target.value);
document.getElementById("bgBrightnessValue").textContent = e.target.value;
drawCropCanvas();
updateAllPanels();
});
document.getElementById("bgContrast").addEventListener("input", (e) => {
settings.bgContrast = parseInt(e.target.value);
document.getElementById("bgContrastValue").textContent = e.target.value;
drawCropCanvas();
updateAllPanels();
});
document.getElementById("resetCropBtn").addEventListener("click", resetCrop);
// Crop canvas events
const canvas = document.getElementById("cropCanvas");
canvas.addEventListener("mousedown", handleCropMouseDown);
canvas.addEventListener("mousemove", handleCropMouseMove);
canvas.addEventListener("mouseup", handleCropMouseUp);
canvas.addEventListener("mouseleave", handleCropMouseUp);
// Panel navigation
document.getElementById("prevPanel").addEventListener("click", () => {
if (currentPanelIndex > 0) {
currentPanelIndex--;
displayCurrentPanel();
}
});
document.getElementById("nextPanel").addEventListener("click", () => {
if (currentPanelIndex < panels.length - 1) {
currentPanelIndex++;
displayCurrentPanel();
}
});
document.getElementById("downloadCurrentBtn").addEventListener("click", downloadCurrentPanel);
document.getElementById("downloadAllBtn").addEventListener("click", downloadAllPanels);
}
// Load default background
function loadDefaultBackground() {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => {
originalImage = img;
backgroundImage = img;
initializeCropBox();
drawCropCanvas();
updateAllPanels();
};
img.src = "https://images.unsplash.com/photo-1579546929518-9e396f3cc809?w=800&h=250&fit=crop";
}
// Add default texts
function addDefaultTexts() {
const defaultTexts = ["links", "about me", "projects"];
defaultTexts.forEach((text) => {
texts.push({ id: Date.now() + Math.random(), text });
});
renderTexts();
updateAllPanels();
}
// Add text
function addText() {
const input = document.getElementById("panelTextInput");
const text = input.value.trim();
if (!text) return;
texts.push({ id: Date.now(), text });
input.value = "";
renderTexts();
updateAllPanels();
}
// Render texts list
function renderTexts() {
const container = document.getElementById("textsList");
if (texts.length === 0) {
container.innerHTML = `
`;
return;
}
container.innerHTML = texts
.map(
(item) => `
`,
)
.join("");
}
// Update text
window.updateText = function (id, newText) {
const item = texts.find((t) => t.id === id);
if (item) {
item.text = newText;
updateAllPanels();
}
};
// Delete text
window.deleteText = function (id) {
texts = texts.filter((t) => t.id !== id);
renderTexts();
updateAllPanels();
};
// Handle background upload
function handleBackgroundUpload(e) {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
originalImage = img;
backgroundImage = img;
initializeCropBox();
drawCropCanvas();
updateAllPanels();
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
}
// Initialize crop box
function initializeCropBox() {
const canvas = document.getElementById("cropCanvas");
const container = document.getElementById("cropCanvasContainer");
const rect = container.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
// Set crop box to full canvas initially
cropBox = {
x: 0,
y: 0,
width: canvas.width,
height: canvas.height,
};
updateCropBoxElement();
document.getElementById("cropBox").classList.add("active");
}
// Draw crop canvas
function drawCropCanvas() {
const canvas = document.getElementById("cropCanvas");
const ctx = canvas.getContext("2d");
if (!originalImage) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Apply filters
ctx.filter = `brightness(${settings.bgBrightness}%) contrast(${settings.bgContrast}%)`;
// Draw image to fit canvas
const scale = Math.max(canvas.width / originalImage.width, canvas.height / originalImage.height);
const scaledWidth = originalImage.width * scale;
const scaledHeight = originalImage.height * scale;
const x = (canvas.width - scaledWidth) / 2;
const y = (canvas.height - scaledHeight) / 2;
ctx.drawImage(originalImage, x, y, scaledWidth, scaledHeight);
ctx.filter = "none";
}
// Update crop box element
function updateCropBoxElement() {
const element = document.getElementById("cropBox");
element.style.left = cropBox.x + "px";
element.style.top = cropBox.y + "px";
element.style.width = cropBox.width + "px";
element.style.height = cropBox.height + "px";
}
// Crop mouse handlers
function handleCropMouseDown(e) {
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
// Check if clicking on a handle
const handles = document.querySelectorAll(".crop-handle");
let clickedHandle = null;
handles.forEach((handle) => {
const handleRect = handle.getBoundingClientRect();
const handleX = handleRect.left - rect.left + handleRect.width / 2;
const handleY = handleRect.top - rect.top + handleRect.height / 2;
if (Math.abs(x - handleX) < 10 && Math.abs(y - handleY) < 10) {
clickedHandle = handle.classList[1]; // Get handle class (nw, ne, etc.)
}
});
if (clickedHandle) {
isResizing = true;
resizeHandle = clickedHandle;
} else if (
x >= cropBox.x &&
x <= cropBox.x + cropBox.width &&
y >= cropBox.y &&
y <= cropBox.y + cropBox.height
) {
isDragging = true;
}
dragStart = { x, y };
cropBoxStart = { ...cropBox };
}
function handleCropMouseMove(e) {
if (!isDragging && !isResizing) return;
const rect = e.target.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const dx = x - dragStart.x;
const dy = y - dragStart.y;
if (isDragging) {
cropBox.x = Math.max(0, Math.min(cropBoxStart.x + dx, rect.width - cropBox.width));
cropBox.y = Math.max(0, Math.min(cropBoxStart.y + dy, rect.height - cropBox.height));
} else if (isResizing) {
const minSize = 50;
switch (resizeHandle) {
case "nw":
cropBox.x = Math.max(
0,
Math.min(cropBoxStart.x + dx, cropBoxStart.x + cropBoxStart.width - minSize),
);
cropBox.y = Math.max(
0,
Math.min(cropBoxStart.y + dy, cropBoxStart.y + cropBoxStart.height - minSize),
);
cropBox.width = cropBoxStart.width - (cropBox.x - cropBoxStart.x);
cropBox.height = cropBoxStart.height - (cropBox.y - cropBoxStart.y);
break;
case "ne":
cropBox.y = Math.max(
0,
Math.min(cropBoxStart.y + dy, cropBoxStart.y + cropBoxStart.height - minSize),
);
cropBox.width = Math.max(
minSize,
Math.min(cropBoxStart.width + dx, rect.width - cropBoxStart.x),
);
cropBox.height = cropBoxStart.height - (cropBox.y - cropBoxStart.y);
break;
case "sw":
cropBox.x = Math.max(
0,
Math.min(cropBoxStart.x + dx, cropBoxStart.x + cropBoxStart.width - minSize),
);
cropBox.width = cropBoxStart.width - (cropBox.x - cropBoxStart.x);
cropBox.height = Math.max(
minSize,
Math.min(cropBoxStart.height + dy, rect.height - cropBoxStart.y),
);
break;
case "se":
cropBox.width = Math.max(
minSize,
Math.min(cropBoxStart.width + dx, rect.width - cropBoxStart.x),
);
cropBox.height = Math.max(
minSize,
Math.min(cropBoxStart.height + dy, rect.height - cropBoxStart.y),
);
break;
case "n":
cropBox.y = Math.max(
0,
Math.min(cropBoxStart.y + dy, cropBoxStart.y + cropBoxStart.height - minSize),
);
cropBox.height = cropBoxStart.height - (cropBox.y - cropBoxStart.y);
break;
case "s":
cropBox.height = Math.max(
minSize,
Math.min(cropBoxStart.height + dy, rect.height - cropBoxStart.y),
);
break;
case "w":
cropBox.x = Math.max(
0,
Math.min(cropBoxStart.x + dx, cropBoxStart.x + cropBoxStart.width - minSize),
);
cropBox.width = cropBoxStart.width - (cropBox.x - cropBoxStart.x);
break;
case "e":
cropBox.width = Math.max(
minSize,
Math.min(cropBoxStart.width + dx, rect.width - cropBoxStart.x),
);
break;
}
}
updateCropBoxElement();
}
function handleCropMouseUp() {
if (isDragging || isResizing) {
applyCrop();
}
isDragging = false;
isResizing = false;
resizeHandle = null;
}
// Apply crop
function applyCrop() {
if (!originalImage) return;
const canvas = document.getElementById("cropCanvas");
const tempCanvas = document.createElement("canvas");
const tempCtx = tempCanvas.getContext("2d");
// Calculate scale factor
const scale = Math.max(canvas.width / originalImage.width, canvas.height / originalImage.height);
const scaledWidth = originalImage.width * scale;
const scaledHeight = originalImage.height * scale;
const offsetX = (canvas.width - scaledWidth) / 2;
const offsetY = (canvas.height - scaledHeight) / 2;
// Calculate crop area in original image coordinates
const cropX = (cropBox.x - offsetX) / scale;
const cropY = (cropBox.y - offsetY) / scale;
const cropWidth = cropBox.width / scale;
const cropHeight = cropBox.height / scale;
tempCanvas.width = cropWidth;
tempCanvas.height = cropHeight;
tempCtx.drawImage(
originalImage,
cropX,
cropY,
cropWidth,
cropHeight,
0,
0,
cropWidth,
cropHeight,
);
const img = new Image();
img.onload = () => {
backgroundImage = img;
updateAllPanels();
};
img.src = tempCanvas.toDataURL();
}
// Reset crop
function resetCrop() {
loadDefaultBackground();
}
// Update all panels
function updateAllPanels() {
panels = texts.map((item) => ({
id: item.id,
text: item.text,
}));
currentPanelIndex = Math.min(currentPanelIndex, Math.max(0, panels.length - 1));
displayCurrentPanel();
updatePanelNavigation();
}
// Display current panel
function displayCurrentPanel() {
const container = document.getElementById("panelDisplay");
if (panels.length === 0) {
container.innerHTML = `
Добавьте тексты для создания панелей
`;
return;
}
const panel = panels[currentPanelIndex];
container.innerHTML = ``;
setTimeout(() => {
drawPanel(panel, "currentPanelCanvas");
}, 0);
}
// Update panel navigation
function updatePanelNavigation() {
document.getElementById("panelCount").textContent = panels.length;
document.getElementById("panelIndicator").textContent =
panels.length > 0 ? `${currentPanelIndex + 1} / ${panels.length}` : "0 / 0";
document.getElementById("prevPanel").disabled = currentPanelIndex === 0 || panels.length === 0;
document.getElementById("nextPanel").disabled =
currentPanelIndex >= panels.length - 1 || panels.length === 0;
document.getElementById("downloadCurrentBtn").disabled = panels.length === 0;
document.getElementById("downloadAllBtn").disabled = panels.length === 0;
}
// Draw panel
function drawPanel(panel, canvasId) {
const canvas = document.getElementById(canvasId);
if (!canvas) return;
const ctx = canvas.getContext("2d");
canvas.width = 320;
canvas.height = 100;
// Draw background
if (backgroundImage) {
ctx.filter = `brightness(${settings.bgBrightness}%) contrast(${settings.bgContrast}%)`;
const scale = Math.max(
canvas.width / backgroundImage.width,
canvas.height / backgroundImage.height,
);
const scaledWidth = backgroundImage.width * scale;
const scaledHeight = backgroundImage.height * scale;
const x = (canvas.width - scaledWidth) / 2;
const y = (canvas.height - scaledHeight) / 2;
ctx.drawImage(backgroundImage, x, y, scaledWidth, scaledHeight);
ctx.filter = "none";
} else {
ctx.fillStyle = "#667eea";
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Draw text
ctx.font = `${settings.fontSize}px ${settings.fontFamily}`;
ctx.fillStyle = settings.textColor;
ctx.textBaseline = "middle";
const text = panel.text;
let x;
if (settings.alignment === "left") {
x = settings.sidePadding;
ctx.textAlign = "left";
} else if (settings.alignment === "center") {
x = canvas.width / 2 + settings.centerOffset;
ctx.textAlign = "center";
} else {
x = canvas.width - settings.sidePadding;
ctx.textAlign = "right";
}
const y = canvas.height / 2;
ctx.fillText(text, x, y);
}
// Download current panel
function downloadCurrentPanel() {
if (panels.length === 0) return;
const canvas = document.getElementById("currentPanelCanvas");
const panel = panels[currentPanelIndex];
if (canvas && panel) {
const link = document.createElement("a");
link.download = `${panel.text}.png`;
link.href = canvas.toDataURL();
link.click();
}
}
// Download all panels
function downloadAllPanels() {
panels.forEach((panel, index) => {
setTimeout(() => {
const tempCanvas = document.createElement("canvas");
tempCanvas.id = `temp-canvas-${panel.id}`;
document.body.appendChild(tempCanvas);
drawPanel(panel, tempCanvas.id);
const link = document.createElement("a");
link.download = `${panel.text}.png`;
link.href = tempCanvas.toDataURL();
link.click();
document.body.removeChild(tempCanvas);
}, 100 * index);
});
}