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
+
+ Topic (Internal Tagging/Organization)
+
+
+
+ Headline
+
+
+
+ Main Pin Text
+
+
+
+
+ Call to Action (CTA)
+
+
+
+
+
+
Background
+
+ Background Color
+
+
+
+ Background Image
+
+
+
+
+
+
+
Text Styles
+
+
+
+
+
+
+
+
+
+
+
+
+ Export as PNG
+
+
+
+
+
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;
+}