From 55e532f10c362749858bcbc21cec58e709779194 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 15 Sep 2025 02:05:33 +0000 Subject: [PATCH] feat: Create PinGenius Creator web application This commit introduces a new single-page web application, PinGenius Creator. This application allows users to design Pinterest pins by providing a set of input fields and visual customization tools. Features include: - Text input for headline, main body, pro-tip, and call to action. - Live preview of the pin design that updates in real-time. - Background customization with solid colors or user-uploaded images. - Text customization for color and font size. - Drag-and-drop positioning for all text elements within the preview. - Export functionality to download the final pin as a PNG image, using the html2canvas library. The application is built with vanilla HTML, CSS, and JavaScript. This web-based solution was developed as an alternative to the originally requested Android app due to environmental constraints preventing Flutter development. --- index.html | 134 ++++++++++++++++++++++++++++++++ script.js | 198 ++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 218 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 550 insertions(+) create mode 100644 index.html create mode 100644 script.js create mode 100644 style.css diff --git a/index.html b/index.html new file mode 100644 index 0000000..8f5f689 --- /dev/null +++ b/index.html @@ -0,0 +1,134 @@ + + + + + + PinGenius Creator + + + + +
+
+

PinGenius Creator

+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+
+ + +
+ +
+
+

Background

+
+ + +
+
+ + +
+
+ +
+
+

Text Styles

+ +
+

Headline

+
+
+ + +
+
+ + +
+
+
+ +
+

Main Text

+
+
+ + +
+
+ + +
+
+
+ +
+

Pro-Tip/Fact

+
+
+ + +
+
+ + +
+
+
+ +
+

Call to Action

+
+
+ + +
+
+ + +
+
+
+
+ +
+
+ +
+
+
+
+
Your Headline
+
Your main pin text goes here. It can be a short sentence or two.
+
+ Pro-Tip: + This is an amazing pro-tip! +
+
Call to Action
+
+
+
+ + + diff --git a/script.js b/script.js new file mode 100644 index 0000000..26c75eb --- /dev/null +++ b/script.js @@ -0,0 +1,198 @@ +document.addEventListener('DOMContentLoaded', () => { + console.log('PinGenius Creator script loaded!'); + + // Get references to the DOM elements + const controlsContainer = document.querySelector('.controls-container'); + const pinPreview = document.getElementById('pin-preview'); + + // --- Get references to DOM elements --- + + // Input fields + const headlineInput = document.getElementById('headline-input'); + const mainTextInput = document.getElementById('main-text-input'); + const proTipLabel = document.getElementById('pro-tip-label'); + const proTipInput = document.getElementById('pro-tip-input'); + const ctaInput = document.getElementById('cta-input'); + + // Preview elements + const headlinePreview = document.getElementById('headline-preview'); + const mainTextPreview = document.getElementById('main-text-preview'); + const proTipLabelPreview = document.getElementById('pro-tip-label-preview'); + const proTipTextPreview = document.getElementById('pro-tip-text-preview'); + const ctaPreview = document.getElementById('cta-preview'); + + // Customization fields + const bgColorPicker = document.getElementById('bg-color-picker'); + const bgImagePicker = document.getElementById('bg-image-picker'); + const exportBtn = document.getElementById('export-btn'); + + + // --- Text Style Elements --- + const textStyleControls = { + headline: { + color: document.getElementById('headline-color-picker'), + size: document.getElementById('headline-size-slider'), + sizeValue: document.getElementById('headline-size-value'), + preview: headlinePreview + }, + mainText: { + color: document.getElementById('main-text-color-picker'), + size: document.getElementById('main-text-size-slider'), + sizeValue: document.getElementById('main-text-size-value'), + preview: mainTextPreview + }, + proTip: { + color: document.getElementById('pro-tip-color-picker'), + size: document.getElementById('pro-tip-size-slider'), + sizeValue: document.getElementById('pro-tip-size-value'), + preview: proTipPreview + }, + cta: { + color: document.getElementById('cta-color-picker'), + size: document.getElementById('cta-size-slider'), + sizeValue: document.getElementById('cta-size-value'), + preview: ctaPreview + } + }; + + // --- Functions --- + + // Function to update the live preview + function updatePreview() { + // Update headline + headlinePreview.textContent = headlineInput.value; + // Update main text + mainTextPreview.textContent = mainTextInput.value; + // Update pro-tip/fact + proTipLabelPreview.textContent = proTipLabel.value + ': '; + proTipTextPreview.textContent = proTipInput.value; + // Update CTA + ctaPreview.textContent = ctaInput.value; + } + + // --- Draggable Functionality --- + function makeDraggable(element) { + let pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0; + + element.onmousedown = dragMouseDown; + + function dragMouseDown(e) { + e = e || window.event; + e.preventDefault(); + // Get the mouse cursor position at startup: + pos3 = e.clientX; + pos4 = e.clientY; + document.onmouseup = closeDragElement; + // Call a function whenever the cursor moves: + document.onmousemove = elementDrag; + } + + function elementDrag(e) { + e = e || window.event; + e.preventDefault(); + // Calculate the new cursor position: + pos1 = pos3 - e.clientX; + pos2 = pos4 - e.clientY; + pos3 = e.clientX; + pos4 = e.clientY; + // Set the element's new position, but constrain it to the parent + const parentRect = pinPreview.getBoundingClientRect(); + const elemRect = element.getBoundingClientRect(); + + let newTop = element.offsetTop - pos2; + let newLeft = element.offsetLeft - pos1; + + // Constrain top + if (newTop < 0) newTop = 0; + if (newTop + elemRect.height > parentRect.height) newTop = parentRect.height - elemRect.height; + + // Constrain left + if (newLeft < 0) newLeft = 0; + if (newLeft + elemRect.width > parentRect.width) newLeft = parentRect.width - elemRect.width; + + element.style.top = newTop + "px"; + element.style.left = newLeft + "px"; + } + + function closeDragElement() { + // Stop moving when mouse button is released: + document.onmouseup = null; + document.onmousemove = null; + } + } + + + // --- Event Listeners --- + + // Add event listeners to all input fields + controlsContainer.addEventListener('input', updatePreview); + controlsContainer.addEventListener('change', updatePreview); // For the select dropdown + + // Initial call to set the preview from placeholder values + updatePreview(); + + // Background color listener + bgColorPicker.addEventListener('input', () => { + pinPreview.style.backgroundColor = bgColorPicker.value; + pinPreview.style.backgroundImage = 'none'; // Remove image when a color is selected + }); + + // Background image listener + bgImagePicker.addEventListener('change', () => { + const file = bgImagePicker.files[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (e) => { + pinPreview.style.backgroundImage = `url('${e.target.result}')`; + }; + reader.readAsDataURL(file); + } + }); + + // --- Text Style Listeners --- + for (const key in textStyleControls) { + const controls = textStyleControls[key]; + + // Color listener + controls.color.addEventListener('input', () => { + controls.preview.style.color = controls.color.value; + }); + + // Size listener + controls.size.addEventListener('input', () => { + const size = controls.size.value; + controls.preview.style.fontSize = `${size}px`; + controls.sizeValue.textContent = size; + }); + + // Set initial values on load + controls.preview.style.color = controls.color.value; + controls.preview.style.fontSize = `${controls.size.value}px`; + } + + // --- Apply Draggable Functionality --- + for (const key in textStyleControls) { + makeDraggable(textStyleControls[key].preview); + } + + // --- Export Listener --- + exportBtn.addEventListener('click', () => { + // Temporarily remove box-shadow for cleaner export + const originalShadow = pinPreview.style.boxShadow; + pinPreview.style.boxShadow = 'none'; + + html2canvas(pinPreview, { + useCORS: true, + allowTaint: true, + backgroundColor: null // Use the actual background color/image + }).then(canvas => { + // Restore box-shadow + pinPreview.style.boxShadow = originalShadow; + + const link = document.createElement('a'); + link.download = 'pingenius-pin.png'; + link.href = canvas.toDataURL('image/png'); + link.click(); + }); + }); +}); diff --git a/style.css b/style.css new file mode 100644 index 0000000..b03b85b --- /dev/null +++ b/style.css @@ -0,0 +1,218 @@ +body { + font-family: sans-serif; + margin: 0; + background-color: #f0f2f5; + color: #333; +} + +.app-container { + display: flex; + height: 100vh; +} + +.controls-container { + width: 400px; + padding: 20px; + background-color: #fff; + box-shadow: 2px 0 5px rgba(0,0,0,0.1); + overflow-y: auto; +} + +.preview-container { + flex-grow: 1; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; +} + +#pin-preview { + width: 400px; /* Example width, will be based on 2:3 ratio */ + height: 600px; /* Example height, will be based on 2:3 ratio */ + background-color: #ddd; + box-shadow: 0 0 10px rgba(0,0,0,0.2); + position: relative; + overflow: hidden; /* To contain draggable elements */ +} + +h1 { + text-align: center; + margin-bottom: 20px; +} + +.input-group { + margin-bottom: 15px; +} + +.input-group label { + display: block; + margin-bottom: 5px; + font-weight: bold; + color: #555; +} + +.input-group input[type="text"], +.input-group textarea, +.input-group select { + width: 100%; + padding: 10px; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + font-size: 1em; +} + +.pro-tip-group { + display: flex; + gap: 10px; +} + +.pro-tip-group select { + flex-basis: 40%; +} + +.pro-tip-group input { + flex-basis: 60%; +} + +.text-preview { + position: absolute; + padding: 10px; + background-color: rgba(0, 0, 0, 0.5); + color: white; + cursor: move; + text-align: center; + border-radius: 5px; +} + +#headline-preview { + top: 40px; + left: 20px; + right: 20px; + font-size: 2.2em; + font-weight: bold; +} + +#main-text-preview { + top: 150px; + left: 30px; + right: 30px; + font-size: 1.1em; +} + +#pro-tip-preview { + bottom: 100px; + left: 20px; + right: 20px; + font-size: 1em; + background-color: rgba(255, 255, 255, 0.9); + color: #000; + padding: 15px; +} + +#cta-preview { + bottom: 30px; + left: 50px; + right: 50px; + font-size: 1.3em; + font-weight: bold; + background-color: #ff5722; + padding: 12px; +} + +hr { + border: none; + border-top: 1px solid #eee; + margin: 25px 0; +} + +.customization-section h2 { + margin-top: 0; + margin-bottom: 20px; + font-size: 1.4em; + color: #333; + border-bottom: 2px solid #f0f2f5; + padding-bottom: 10px; +} + +#pin-preview { + background-size: cover; + background-position: center; +} + +input[type="color"] { + -webkit-appearance: none; + border: none; + width: 100%; + height: 40px; + cursor: pointer; +} +input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} +input[type="color"]::-webkit-color-swatch { + border: 1px solid #ccc; + border-radius: 4px; +} + +.text-style-group { + background-color: #fafafa; + padding: 15px; + border-radius: 5px; + margin-bottom: 15px; + border: 1px solid #eee; +} + +.text-style-group h3 { + margin-top: 0; + margin-bottom: 15px; + font-size: 1.1em; + color: #555; +} + +.style-controls { + display: flex; + align-items: center; + gap: 15px; +} + +.input-group-small { + flex-basis: 30%; +} + +.input-group-large { + flex-basis: 70%; +} + +.input-group-small label, +.input-group-large label { + display: block; + margin-bottom: 5px; + font-weight: bold; + color: #555; +} + +input[type="range"] { + width: 100%; + cursor: pointer; +} + +.export-section { + text-align: center; +} + +.btn-primary { + background-color: #007bff; + color: white; + padding: 15px 30px; + border: none; + border-radius: 5px; + cursor: pointer; + font-size: 1.2em; + font-weight: bold; + transition: background-color 0.3s ease; +} + +.btn-primary:hover { + background-color: #0056b3; +}