--- START OF FILE Paste January 29, 2026 - 9:19PM ---
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VisualLock AI Pro – Prompt Builder</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<!-- You might want to add Font Awesome for icons here if you plan to use them -->
<!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css"> -->
<style>
/* Capture One Dark Theme Color Palette - REFINED */
:root {
--bg-body: #1d1d1d; /* Your preferred darkest grey for overall page */
--bg-panel: #161616; /* Your preferred slightly lighter dark grey for panels */
--bg-input: #0c0c0c; /* A slightly lighter, yet still dark, grey for input fields */
--accent: #dec694; /* A professional, calming blue accent (Photoshop-esque) */
--accent-rgb: 94, 157, 219; /* RGB for translucent accent */
--accent-hover: #4a8cd0; /* Darker blue for hover */
--text-main: #e0e0e0; /* Slightly softer white for main text */
--text-muted: #aaaaaa; /* Medium grey for muted text - ensure good contrast */
--border: #1e1e1e; /* Darker grey for borders - clear separation */
--radius: 4px; /* Sharper corners */
--shadow: 0 2px 8px rgba(0, 0, 0, 0.5); /* Slightly stronger, deeper shadow */
--header-bg: #161616; /* Header background matches panels */
--header-border: #1e1e1e; /* Header border matches panel borders */
--panel-spacing: 16px; /* Consistent spacing - slightly increased for airiness */
--header-height: 60px;
}
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent; /* Remove tap highlight on mobile */
}
body {
margin: 0;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: var(--bg-body);
color: var(--text-main);
line-height: 1.5;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: flex-start; /* Align to top */
padding: 20px; /* Padding around the entire app */
font-size: 14px;
}
/* Scrollbar Styling */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: var(--bg-body); }
::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: var(--text-muted); }
/* Main App Wrapper */
.main-app-wrapper {
background: var(--bg-panel);
border-radius: var(--radius);
overflow: hidden;
max-width: 1600px; /* Increased max-width */
width: 100%;
box-shadow: var(--shadow);
display: flex;
flex-direction: column;
min-height: calc(100vh - 40px);
}
/* Header */
header {
background: var(--header-bg);
border-bottom: 1px solid var(--header-border);
padding: 10px 20px;
position: sticky;
top: 0;
z-index: 100;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
height: var(--header-height);
}
h1 {
margin: 0;
font-size: 18px;
letter-spacing: 0;
display: flex;
align-items: center;
color: var(--text-main); /* Ensure title stands out */
font-weight: 600; /* Slightly bolder */
}
h1 .ai {
color: var(--accent); /* Accent for AI */
margin: 0 3px;
font-weight: 700;
}
h1 .pro {
background: var(--accent); /* Maintain accent for PRO tag */
color: white;
font-weight: 700;
font-size: 9px;
padding: 1px 5px;
border-radius: 3px;
margin-left: 6px;
text-transform: uppercase;
letter-spacing: 0.8px;
}
.header-actions {
display: flex;
gap: 8px;
align-items: center;
}
/* App Container */
.app-container {
display: flex;
flex-wrap: nowrap;
width: 100%;
padding: var(--panel-spacing);
gap: var(--panel-spacing);
align-items: flex-start;
flex-grow: 1;
}
.left-column {
flex: 1 1 450px;
min-width: 380px;
display: flex;
flex-direction: column;
gap: var(--panel-spacing);
}
.right-column {
flex: 0 0 450px;
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--panel-spacing);
position: sticky;
top: calc(var(--header-height) + var(--panel-spacing));
max-height: calc(100vh - var(--header-height) - (var(--panel-spacing) * 2));
overflow-y: auto;
display: flex;
flex-direction: column;
gap: var(--panel-spacing);
box-shadow: var(--shadow);
}
/* Forms & Inputs */
.settingsBox {
background: var(--bg-panel);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: var(--panel-spacing);
box-shadow: var(--shadow);
flex-grow: 1;
}
.settingsBox.tabbed-panel {
padding-top: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
.settingsBox h3 {
margin: 0 0 10px 0;
color: var(--text-main);
font-size: 15px;
font-weight: 600;
border-bottom: 1px solid var(--border);
padding-bottom: 8px;
display: flex;
align-items: center;
}
label {
display: flex;
align-items: center;
margin-top: 14px; /* Slightly more space above labels */
margin-bottom: 6px; /* Slightly more space below labels */
font-size: 13px; /* Slightly larger labels for readability */
font-weight: 500; /* Medium weight */
color: var(--text-main); /* Labels are important, so make them visible */
text-transform: uppercase;
letter-spacing: 0.5px;
position: relative;
}
label:first-child { margin-top: 0; }
.section-number-circle {
display: none; /* Hide circles for C1 look */
}
label::before { /* Replace circle with a subtle indicator */
content: '';
display: inline-block;
width: 4px;
height: 12px;
background-color: var(--accent);
margin-right: 8px;
border-radius: 2px;
flex-shrink: 0;
}
.fieldDesc {
font-size: 12px; /* Keep reasonable size */
color: var(--text-muted); /* Good contrast with #161616 */
margin-bottom: 8px; /* More separation from input */
font-style: normal;
}
input:not([type="file"]), select, textarea {
width: 100%;
padding: 10px 12px; /* Slightly more padding for comfort */
background: var(--bg-input);
color: var(--text-main);
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 14px; /* Slightly larger input text */
transition: border-color 0.2s, box-shadow 0.2s;
font-family: inherit;
}
input:not([type="file"]):focus, select:focus, textarea:focus {
outline: none;
border-color: var(--accent);
box-shadow: 0 0 0 1px var(--accent); /* Thin accent border on focus */
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23aaaaaa%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13%205.1L146.2%20202.7%2018.5%2074.5a17.6%2017.6%200%200%200-25.3%2023.3l137.9%20137.8c6.8%206.7%2017.7%206.7%2024.5%200l137.9-137.8c10.1-10.1%202.9-26.3-12.9-26.3z%22%2F%3E%3C%2Fsvg%3E');
background-repeat: no-repeat;
background-position: right 10px center; /* Adjusted for larger padding */
background-size: 10px;
padding-right: 30px; /* Make space for the arrow */
}
.input-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px; /* Slightly more gap */
}
/* File Upload Styling */
input[type="file"] {
padding: 10px 12px;
}
input[type="file"]::file-selector-button {
background: var(--border);
color: var(--text-main);
border: none;
padding: 6px 10px;
border-radius: var(--radius);
cursor: pointer;
margin-right: 10px;
font-size: 13px;
transition: background 0.2s, color 0.2s;
}
input[type="file"]::file-selector-button:hover {
background: var(--text-muted);
color: var(--text-main);
}
/* Output Section */
#photoPreviews {
display: flex;
flex-wrap: wrap;
gap: 10px; /* Slightly more gap */
min-height: 90px; /* Increased min-height */
background: var(--bg-input);
border-radius: var(--radius);
border: 1px dashed var(--border);
padding: 10px;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 12px;
position: relative;
}
#photoPreviews:empty::before {
content: "No images selected";
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
white-space: nowrap;
}
.previewImgContainer {
position: relative;
width: 75px; /* Larger preview images */
height: 75px;
border-radius: var(--radius);
border: 1px solid var(--border);
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
img.previewImg {
width: 100%;
height: 100%;
object-fit: cover;
}
.removePreview {
position: absolute;
top: 4px; /* Adjusted position */
right: 4px; /* Adjusted position */
background: rgba(0,0,0,0.7); /* Slightly darker background */
color: white;
border-radius: 50%;
width: 20px; /* Slightly larger remove button */
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
cursor: pointer;
line-height: 1;
transition: background 0.2s;
}
.removePreview:hover {
background: var(--accent);
}
#promptOutput {
background: var(--bg-input);
border: 1px solid var(--border);
padding: 14px; /* Increased padding */
border-radius: var(--radius);
min-height: 160px; /* Increased min-height */
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
font-size: 13px; /* Slightly larger font */
line-height: 1.6; /* Increased line-height for readability */
color: var(--text-main);
white-space: pre-wrap;
box-shadow: inset 0 2px 4px rgba(0,0,0,0.3); /* Darker inset shadow */
word-break: break-word;
overflow-y: auto;
}
/* Buttons */
.action-btn {
width: 100%;
padding: 12px; /* More padding */
border: none;
border-radius: var(--radius);
font-weight: 600;
font-size: 14px; /* Slightly larger button text */
cursor: pointer;
transition: transform 0.1s, background 0.2s, box-shadow 0.2s;
}
#generatePrompt {
background: var(--accent); /* Solid accent color */
color: white;
margin-bottom: 10px; /* Slightly more margin */
box-shadow: 0 2px 8px rgba(var(--accent-rgb), 0.4); /* Stronger shadow */
}
#generatePrompt:hover {
transform: translateY(-1px);
background: var(--accent-hover);
box-shadow: 0 4px 12px rgba(var(--accent-rgb), 0.5);
}
#generatePrompt:disabled {
background: var(--bg-input);
color: var(--text-muted);
border: 1px solid var(--border);
box-shadow: none;
transform: none;
cursor: not-allowed;
}
.secondary {
background: var(--bg-input); /* Secondary button matches input background */
border: 1px solid var(--border);
color: var(--text-muted);
}
.secondary:hover {
border-color: var(--accent);
color: var(--accent);
background: rgba(var(--accent-rgb), 0.1); /* Subtle hover background */
}
.secondary:disabled { opacity: 0.6; cursor: not-allowed; background: var(--bg-input); color: var(--text-muted); }
.separator { height: 1px; background: var(--border); margin: 10px 0; } /* Slightly more margin */
/* AI Output Area */
#aiModelsContainer {
display: flex;
flex-direction: column;
gap: 12px; /* Slightly more gap */
}
#aiModelsContainer div {
background: var(--bg-input);
padding: 12px; /* Increased padding */
border-radius: var(--radius);
border-left: 3px solid var(--accent); /* Keep accent for emphasis */
font-size: 12px;
color: var(--text-main);
word-break: break-word;
overflow-y: auto;
}
#aiModelsContainer strong {
color: var(--accent); /* Accent for AI model names */
}
/* Toast */
.toast {
position: fixed; bottom: 20px; right: 20px;
background: var(--accent); color: white;
font-weight: 600;
padding: 12px 22px; /* Slightly more padding */
border-radius: 4px;
box-shadow: 0 4px 10px rgba(var(--accent-rgb), 0.6); /* Stronger shadow */
opacity: 0; transform: translateY(20px);
transition: all 0.3s cubic-bezier(0.68, -0.55, 0.27, 1.55);
z-index: 2000;
font-size: 14px;
}
.toast.show { opacity: 1; transform: translateY(0); }
/* Info link styling - Tooltips */
.info-link {
margin-left: 8px; /* Slightly more margin */
font-size: 0.8em;
color: var(--accent);
cursor: pointer;
text-decoration: none;
font-weight: normal;
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
width: 18px; /* Slightly larger hit area */
height: 18px;
border-radius: 50%;
background: rgba(var(--accent-rgb), 0.15); /* Lighter tint of accent */
color: var(--accent);
font-weight: bold;
transition: background 0.2s;
}
.info-link:hover {
background: var(--accent);
color: white;
}
.info-tooltip {
position: absolute;
top: 100%;
left: 50%;
transform: translateX(-50%);
background: var(--bg-input); /* Consistent input background */
border: 1px solid var(--border);
padding: 10px 14px; /* Increased padding */
border-radius: var(--radius);
font-size: 12px;
color: var(--text-main);
white-space: normal;
min-width: 220px; /* Increased min-width */
max-width: 320px; /* Increased max-width */
z-index: 1001;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
box-shadow: var(--shadow);
margin-top: 6px; /* Small gap below '?' */
}
.info-link:hover .info-tooltip {
opacity: 1;
pointer-events: auto;
transform: translateX(-50%) translateY(0);
}
/* Input group for mood and randomize button */
.mood-input-group {
display: flex;
gap: 10px; /* Slightly more gap */
align-items: flex-end;
}
.mood-input-group > div {
flex-grow: 1;
}
.mood-input-group button {
padding: 10px 14px; /* Increased padding */
font-size: 13px;
border-radius: var(--radius);
white-space: nowrap;
height: 38px; /* Match input height + padding */
background: var(--accent);
color: white;
border: none;
cursor: pointer;
transition: background 0.2s;
}
.mood-input-group button:hover {
background: var(--accent-hover);
}
/* Additional Instructions Specific Styling */
.additional-instructions-group {
display: flex;
flex-direction: column;
gap: 10px; /* Slightly more gap */
}
.additional-instructions-options {
display: flex;
flex-wrap: wrap;
gap: 8px; /* Slightly more gap */
}
.additional-instructions-options button {
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-muted);
padding: 8px 12px; /* Increased padding */
border-radius: var(--radius);
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
}
.additional-instructions-options button:hover {
border-color: var(--accent);
color: var(--accent);
background: rgba(var(--accent-rgb), 0.1);
}
.additional-instructions-options button.selected {
background: var(--accent);
color: white;
border-color: var(--accent);
}
/* Target AIs Select */
#targetAIs {
min-height: 90px; /* Ensure visibility for multiple select */
height: auto;
padding: 10px 12px;
}
#targetAIs option {
padding: 6px 0;
}
#targetAIs option:checked {
background-color: var(--accent);
color: white;
}
/* Ensure header buttons are styled correctly */
.header-actions button {
background: var(--bg-input);
border: 1px solid var(--border);
color: var(--text-muted);
padding: 8px 12px;
border-radius: var(--radius);
font-size: 13px;
height: auto;
white-space: nowrap;
}
.header-actions button:hover {
border-color: var(--accent);
color: var(--accent);
background: rgba(var(--accent-rgb), 0.1);
}
/* Tabbed Interface for Left Column */
.tab-bar {
display: flex;
border-bottom: 1px solid var(--border);
margin-bottom: 0;
overflow-x: auto;
padding-bottom: 5px;
background: var(--bg-panel);
border-top-left-radius: var(--radius);
border-top-right-radius: var(--radius);
padding-left: var(--panel-spacing);
padding-right: var(--panel-spacing);
}
.tab-button {
flex-shrink: 0;
padding: 12px 18px; /* Increased padding */
border: 1px solid transparent;
border-bottom: none;
background: transparent;
color: var(--text-muted);
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border-radius: var(--radius) var(--radius) 0 0;
margin-bottom: -1px;
display: flex; /* For icon alignment */
align-items: center;
gap: 6px; /* Space between icon and text */
}
.tab-button:hover {
color: var(--text-main);
background: var(--bg-input);
}
.tab-button.active {
color: var(--accent); /* Accent for active tab */
border-color: var(--border);
border-bottom-color: var(--bg-panel);
background: var(--bg-panel);
font-weight: 600; /* Bolder for active */
}
.tab-content {
display: none;
flex-direction: column;
gap: var(--panel-spacing);
padding: var(--panel-spacing);
border: 1px solid var(--border);
border-top: none;
border-bottom-left-radius: var(--radius);
border-bottom-right-radius: var(--radius);
background: var(--bg-panel);
}
.tab-content.active {
display: flex;
}
/* Accordion Styling for Advanced Sections */
.accordion-item {
border-bottom: 1px solid var(--border);
padding-bottom: 8px; /* Increased padding */
}
.accordion-item:last-of-type {
border-bottom: none;
padding-bottom: 0;
}
.accordion-header {
display: flex;
align-items: center;
cursor: pointer;
padding: 12px 0; /* Increased padding */
font-size: 16px; /* Slightly larger for clarity */
font-weight: 600;
color: var(--text-main);
transition: color 0.2s;
}
.accordion-header:hover {
color: var(--accent);
}
.accordion-header.expanded {
color: var(--accent); /* Accent when expanded */
}
.accordion-header .arrow {
margin-left: auto;
transition: transform 0.2s ease;
font-size: 1.2em;
}
.accordion-header.expanded .arrow {
transform: rotate(90deg);
}
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease-out;
padding-top: 0;
display: flex;
flex-direction: column;
gap: 12px; /* Increased spacing for elements within accordion */
}
.accordion-content.expanded {
padding-top: 12px; /* Add padding when expanded */
}
.accordion-content .input-row {
margin-top: 0;
}
.accordion-content label { margin-top: 0; }
.accordion-content label:first-of-type { margin-top: 14px; } /* First label in content gets top margin */
.accordion-header::before { /* Consistent accent line */
content: '';
display: inline-block;
width: 4px;
height: 16px;
background-color: var(--accent);
margin-right: 8px;
border-radius: 2px;
flex-shrink: 0;
}
.suggest-button {
background: #50664d; /* Muted, professional green/grey */
color: white;
border: none;
padding: 10px 14px;
border-radius: var(--radius);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
margin-top: 12px; /* Increased margin */
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
}
.suggest-button:hover {
background: #6a8067;
}
/* Dark Rustic Preset Button - Adjusted to new dark theme earthy tone */
#darkRusticPresetBtn {
background: #50664d !important; /* Muted, professional green/grey */
color: white !important;
border: none !important;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
}
#darkRusticPresetBtn:hover {
background: #6a8067 !important;
}
/* Mobile Responsive */
@media (max-width: 900px) {
body { padding: 0; }
.main-app-wrapper {
margin: 0;
border-radius: 0;
min-height: 100vh;
}
.app-container {
flex-direction: column;
padding: 10px;
gap: 10px;
}
.left-column { min-width: auto; width: 100%; }
.right-column {
width: 100%;
flex: 1 1 auto;
position: static;
max-height: none;
}
header {
flex-direction: column;
gap: 8px;
padding: 10px;
height: auto;
}
h1 { margin-bottom: 5px; }
.header-actions {
width: 100%;
justify-content: center;
}
.header-actions button {
flex-grow: 1;
max-width: 120px;
}
.tab-bar { justify-content: flex-start; }
.tab-button { padding: 10px 12px; font-size: 13px; gap: 4px; } /* Adjust tab button size for mobile */
.tab-content {
border-left: none;
border-right: none;
border-radius: 0;
}
.settingsBox {
border-radius: 0;
border-left: none;
border-right: none;
}
}
/* New Styling for Photography Type Selector */
.photography-type-selector {
margin-bottom: var(--panel-spacing);
padding-bottom: var(--panel-spacing);
border-bottom: 1px solid var(--border);
}
.photography-type-selector label {
margin-top: 0; /* Override default label margin for this specific label */
}
.photography-type-selector select {
width: auto; /* Allow select to shrink to content */
max-width: 100%;
}
</style>
</head>
<body>
<div class="main-app-wrapper">
<header>
<h1>VisualLock <span class="ai">AI</span><span class="pro">PRO</span></h1>
<div class="header-actions">
<button id="savePresetBtn" class="secondary">💾 Save Preset</button>
<input type="file" id="loadPresetInput" accept=".json" style="display: none;">
<button id="loadPresetBtn" class="secondary">📂 Load Preset</button>
<button id="supportBtn" class="secondary">Donate</button>
</div>
</header>
<div class="app-container">
<!-- LEFT COLUMN: INPUTS -->
<div class="left-column">
<!-- Photography Type Selector -->
<div class="settingsBox photography-type-selector">
<label for="photographyType">Photography Type</label>
<select id="photographyType">
<option value="product">Product Photography</option>
<option value="portrait">Portrait Photography</option>
</select>
</div>
<!-- Consolidated Settings Box for Tabs -->
<div class="settingsBox tabbed-panel">
<div class="tab-bar">
<!-- Added unicode icons for a Photoshop-like feel. For Font Awesome, use <i class="fas fa-star"></i> -->
<button class="tab-button active" data-tab="basicSettings">⭐ Basic</button>
<button class="tab-button" data-tab="advancedSettings">⚙️ Advanced</button>
<button class="tab-button" data-tab="stylingSettings">🎨 Styling</button>
<button class="tab-button" data-tab="outputSettings">📄 Output</button>
</div>
<!-- Product Photography Settings -->
<div id="productSettings" class="tab-content-group active">
<div id="basicSettings" class="tab-content active">
<label>Subject & Reference</label>
<div class="fieldDesc">Upload up to 3 reference images.</div>
<input type="file" id="referencePhotos" accept="image/*" multiple>
<div id="photoPreviews"></div>
<label>Product Description</label>
<input type="text" id="description" placeholder="e.g. Tactical handgun, matte black finish">
<div class="input-row">
<div>
<label>Height</label>
<input type="text" id="productHeight" placeholder="e.g. 1 ft 4 in">
</div>
<div>
<label>Width</label>
<input type="text" id="productWidth" placeholder="e.g. 0 ft 6 in">
</div>
</div>
<label>Environment</label>
<select id="environment">
<option value="studio neutral">Studio Neutral</option>
<option value="indoor lifestyle">Indoor Lifestyle</option>
<option value="outdoor natural">Outdoor Natural</option>
<option value="controlled tactical">Controlled Tactical</option>
<option value="futuristic urban">Futuristic Urban</option>
<option value="rustic countryside">Rustic Countryside</option>
</select>
<label>Lighting Base</label>
<select id="lighting">
<option value="soft studio lighting">Soft Studio</option>
<option value="directional window lighting">Window Light</option>
<option value="overcast outdoor lighting">Overcast</option>
<option value="controlled dramatic lighting">Dramatic</option>
<option value="spotlight">Spotlight</option>
<option value="high-key">High-Key</option>
<option value="low-key">Low-Key</option>
<option value="golden hour">Golden Hour</option>
</select>
<div class="input-row">
<div>
<label>Angle</label>
<select id="cameraPerspective">
<option value="product-level">Product-Level</option>
<option value="eye-level">Eye-Level</option>
<option value="slight low-angle">Low Angle</option>
<option value="high-angle">High Angle</option>
<option value="flat lay (overhead)">Flat Lay</option>
</select>
</div>
<div>
<label>Aspect Ratio</label>
<select id="aspectRatio">
<option value="1:1">1:1 (Square)</option>
<option value="4:3">4:3 (Standard)</option>
<option value="3:2">3:2 (Classic)</option>
<option value="16:9">16:9 (Wide)</option>
<option value="9:16">9:16 (Vertical)</option>
</select>
</div>
</div>
<label>Background Detail</label>
<input type="text" id="background" placeholder="e.g. matte black gradient, concrete texture">
</div>
<div id="advancedSettings" class="tab-content">
<!-- Lighting Refinement (from popup) -->
<div class="accordion-item">
<div class="accordion-header" data-accordion="lightingDetails">💡 Refine Lighting <span class="arrow">▶</span></div>
<div class="accordion-content" id="lightingDetailsAccordion">
<div class="input-row">
<div><label>Source</label><input type="text" id="lightSource" placeholder="e.g. strobe"></div>
<div><label>Direction</label><input type="text" id="lightDirection" placeholder="e.g. side"></div>
</div>
<label>Intent / Mood</label><input type="text" id="lightIntent" placeholder="e.g. dramatic">
<div class="input-row">
<div><div><label>Count</label><input type="text" id="lightCount" placeholder="e.g. 3-point"></div></div>
<div><div><label>Modifiers</label><input type="text" id="lightModifiers" placeholder="e.g. softbox"></div></div>
</div>
<label>Reflections</label><input type="text" id="reflectionsGlares" placeholder="e.g. specular highlights">
</div>
</div>
<!-- Advanced Camera (from popup) -->
<div class="accordion-item">
<div class="accordion-header" data-accordion="cameraDetails">📷 Advanced Camera <span class="arrow">▶</span></div>
<div class="accordion-content" id="cameraDetailsAccordion">
<div class="input-row">
<div><label>Aperture <a href="#" class="info-link" data-info="aperture">? <span class="info-tooltip">Aperture (f-stop): Controls how much light enters the camera and the depth of field (how much of the image is in focus). Lower f-numbers (like f/2.8) mean more light and a shallower depth of field (blurry background).</span></a></label><input type="text" id="aperture" placeholder="2.8"></div>
<div><label>Shutter <a href="#" class="info-link" data-info="shutterSpeed">? <span class="info-tooltip">Shutter Speed: Determines how long the camera's sensor is exposed to light. Faster speeds (like 1/1000s) freeze motion; slower speeds (like 1/30s) can create motion blur.</span></a></label><input type="text" id="shutterSpeed" placeholder="1/125"></div>
</div>
<div class="input-row">
<div><label>ISO <a href="#" class="info-link" data-info="iso">? <span class="info-tooltip">ISO: Measures the camera sensor's sensitivity to light. Higher ISO allows for shooting in darker conditions but can introduce 'noise' (graininess) into the image.</span></a></label><input type="text" id="iso" placeholder="100"></div>
<div><label>WB (K) <a href="#" class="info-link" data-info="whiteBalance">? <span class="info-tooltip">White Balance (WB K): Adjusts the color temperature of your photos to make whites appear truly white, compensating for different light sources (e.g., warm indoor light vs. cool outdoor light). Measured in Kelvin (K).</span></a></label><input type="text" id="whiteBalance" placeholder="5600"></div>
</div>
<div class="input-row">
<div><label>Focal Length <a href="#" class="info-link" data-info="focalLength">? <span class="info-tooltip">Focal Length (mm): Refers to the lens's ability to magnify the scene. Shorter focal lengths (e.g., 24mm) offer a wide-angle view, while longer ones (e.g., 200mm) provide a telephoto (zoomed-in) view.</span></a></label><input type="text" id="focalLength" placeholder="85mm"></div>
<div><label>Lens Type</label><input type="text" id="lensType" placeholder="Macro"></div>
</div>
<label>Focus Method</label><input type="text" id="focusMethod" placeholder="e.g. focus stacking">
</div>
</div>
<!-- Props (from popup) -->
<div class="accordion-item">
<div class="accordion-header" data-accordion="propsDetails">📦 Add Props <span class="arrow">▶</span></div>
<div class="accordion-content" id="propsDetailsAccordion">
<button id="suggestPropComboBtn" class="suggest-button">Suggest Combo</button>
<div class="input-row">
<div><label>Prop 1</label><input type="text" id="prop1"></div>
<div><label>Dims</label><input type="text" id="prop1Dim"></div>
</div>
<div class="input-row">
<div><label>Prop 2</label><input type="text" id="prop2"></div>
<div><label>Dims</label><input type="text" id="prop2Dim"></div>
</div>
<div class="input-row">
<div><label>Prop 3</label><input type="text" id="prop3"></div>
<div><label>Dims</label><input type="text" id="prop3Dim"></div>
</div>
<div class="input-row">
<div><label>Prop 4</label><input type="text" id="prop4"></div>
<div><label>Dims</label><input type="text" id="prop4Dim"></div>
</div>
</div>
</div>
</div>
<div id="stylingSettings" class="tab-content">
<!-- Surface & Mood (existing) -->
<div class="accordion-item">
<div class="accordion-header" data-accordion="surfaceMood">✨ Surface & Mood <span class="arrow">▶</span></div>
<div class="accordion-content expanded" id="surfaceMoodAccordion">
<div class="input-row">
<div>
<label>Material</label>
<input type="text" id="surfaceMaterial" placeholder="e.g. polished marble">
</div>
<div>
<label>Color</label>
<input type="text" id="surfaceColor" placeholder="e.g. pure white">
</div>
</div>
<label>Atmosphere</label>
<div class="mood-input-group">
<div>
<input type="text" id="overallMood" placeholder="e.g. luxurious, industrial, serene">
</div>
<button id="randomizeMoodBtn">Randomize Mood</button>
</div>
</div>
</div>
<!-- Composition (from popup) -->
<div class="accordion-item">
<div class="accordion-header" data-accordion="compositionDetails">📐 Composition <span class="arrow">▶</span></div>
<div class="accordion-content" id="compositionDetailsAccordion">
<label>Rule</label><input type="text" id="compRule" placeholder="e.g. rule of thirds">
<div class="input-row">
<div><label>Framing</label><input type="text" id="framing" placeholder="e.g. medium shot"></div>
<div><label>Neg. Space</label><input type="text" id="negativeSpace" placeholder="e.g. ample"></div>
</div>
<label>Depth of Field</label><input type="text" id="dofDesc" placeholder="e.g. creamy bokeh">
</div>
</div>
<!-- Post Processing (from popup) -->
<div class="accordion-item">
<div class="accordion-header" data-accordion="postProcessingDetails">🎨 Post-Processing <span class="arrow">▶</span></div>
<div class="accordion-content" id="postProcessingDetailsAccordion">
<div class="input-row">
<div><label>Grading</label><input type="text" id="colorGrading" placeholder="e.g. vibrant"></div>
<div><label>Contrast</label><input type="text" id="contrast" placeholder="e.g. high"></div>
</div>
<div class="input-row">
<div><label>Sharpness</label><input type="text" id="sharpness" placeholder="e.g. sharp"></div>
<div><label>Grain</label><input type="text" id="grainNoise" placeholder="e.g. minimal"></div>
</div>
<div class="input-row">
<div><label>Retouch</label><input type="text" id="retouching" placeholder="e.g. flawless"></div>
<div><label>Vignette</label><input type="text" id="vignetting" placeholder="e.g. subtle"></div>
</div>
</div>
</div>
</div>
<div id="outputSettings" class="tab-content">
<div class="accordion-item">
<div class="accordion-header" data-accordion="additionalInstructions">📄 Additional Instructions <span class="arrow">▶</span></div>
<div class="accordion-content expanded" id="additionalInstructionsAccordion">
<div class="additional-instructions-group">
<div class="additional-instructions-options">
<button type="button" data-value="Clean, dry, polished black metal surface, no smudges, no oil, matte finish">Clean Matte Finish</button>
<button type="button" data-value="Pristine gun frame, perfectly clean, no residue, smooth, even surface">Pristine Surface</button>
<button type="button" data-value="Black firearm finish, free of imperfections, dry, no fingerprints, studio quality">Studio Quality Finish</button>
</div>
<textarea id="additional" rows="3" placeholder="Any specific details or exclusions..."></textarea>
</div>
</div>
</div>
<!-- Preset Button - Moved here for logical grouping with "instructions" for output -->
<button id="darkRusticPresetBtn" class="action-btn secondary">🍂 Dark Rustic Preset</button>
</div>
</div> <!-- End of productSettings -->
<!-- Portrait Photography Settings -->
<div id="portraitSettings" class="tab-content-group" style="display: none;">
<div id="basicSettingsPortrait" class="tab-content active">
<label>Subject Description</label>
<input type="text" id="portraitSubjectDescription" placeholder="e.g. A young woman with long wavy hair">
<label>Age & Gender</label>
<div class="input-row">
<div><input type="text" id="portraitAge" placeholder="e.g. 20s"></div>
<div><input type="text" id="portraitGender" placeholder="e.g. Female"></div>
</div>
<label>Ethnicity / Background</label>
<input type="text" id="portraitEthnicity" placeholder="e.g. Caucasian, Asian, African-American">
<label>Expression & Pose</label>
<input type="text" id="portraitExpressionPose" placeholder="e.g. smiling gently, looking directly at camera, hands clasped">
<label>Attire / Wardrobe</label>
<input type="text" id="portraitAttire" placeholder="e.g. elegant evening gown, casual streetwear">
<label>Location / Background</label>
<select id="portraitEnvironment">
<option value="studio seamless background">Studio Seamless Background</option>
<option value="urban cityscape">Urban Cityscape</option>
<option value="natural outdoor park">Natural Outdoor Park</option>
<option value="cozy indoor cafe">Cozy Indoor Cafe</option>
<option value="historic building interior">Historic Building Interior</option>
<option value="dramatic natural landscape">Dramatic Natural Landscape</option>
</select>
<label>Lighting Base</label>
<select id="portraitLighting">
<option value="natural soft window light">Natural Soft Window Light</option>
<option value="studio butterfly lighting">Studio Butterfly Lighting</option>
<option value="rim light, dramatic">Rim Light, Dramatic</option>
<option value="golden hour backlight">Golden Hour Backlight</option>
<option value="high-key, bright">High-Key, Bright</option>
<option value="low-key, moody">Low-Key, Moody</option>
</select>
<div class="input-row">
<div>
<label>Shot Type</label>
<select id="portraitShotType">
<option value="headshot">Headshot</option>
<option value="bust shot">Bust Shot</option>
<option value="half-body shot">Half-Body Shot</option>
<option value="full-body shot">Full-Body Shot</option>
<option value="environmental portrait">Environmental Portrait</option>
</select>
</div>
<div>
<label>Aspect Ratio</label>
<select id="portraitAspectRatio">
<option value="1:1">1:1 (Square)</option>
<option value="4:3">4:3 (Standard)</option>
<option value="3:2">3:2 (Classic)</option>
<option value="16:9">16:9 (Wide)</option>
<option value="9:16">9:16 (Vertical)</option>
</select>
</div>
</div>
</div>
<div id="advancedSettingsPortrait" class="tab-content">
<!-- Lighting Refinement (adapted for portrait) -->
<div class="accordion-item">
<div class="accordion-header" data-accordion="lightingDetailsPortrait">💡 Refine Lighting <span class="arrow">▶</span></div>
<div class="accordion-content" id="lightingDetailsAccordionPortrait">
<div class="input-row">
<div><label>Source</label><input type="text" id="lightSourcePortrait" placeholder="e.g. large softbox"></div>
<div><label>Direction</label><input type="text" id="lightDirectionPortrait" placeholder="e.g. 45 degrees left"></div>
</div>
<label>Intent / Mood</label><input type="text" id="lightIntentPortrait" placeholder="e.g. ethereal, dramatic">
<div class="input-row">
<div><div><label>Count</label><input type="text" id="lightCountPortrait" placeholder="e.g. 2-light setup"></div></div>
<div><div><label>Modifiers</label><input type="text" id="lightModifiersPortrait" placeholder="e.g. beauty dish, reflector"></div></div>
</div>
<label>Catchlights</label><input type="text" id="catchlightsPortrait" placeholder="e.g. natural, sparkling">
</div>
</div>
<!-- Advanced Camera (shared or slightly adapted) -->
<div class="accordion-item">
<div class="accordion-header" data-accordion="cameraDetailsPortrait">📷 Advanced Camera <span class="arrow">▶</span></div>
<div class="accordion-content" id="cameraDetailsAccordionPortrait">
<div class="input-row">
<div><label>Aperture <a href="#" class="info-link" data-info="aperture">? <span class="info-tooltip">Aperture (f-stop): Controls how much light enters the camera and the depth of field (how much of the image is in focus). Lower f-numbers (like f/2.8) mean more light and a shallower depth of field (blurry background).</span></a></label><input type="text" id="aperturePortrait" placeholder="1.8"></div>
<div><label>Shutter <a href="#" class="info-link" data-info="shutterSpeed">? <span class="info-tooltip">Shutter Speed: Determines how long the camera's sensor is exposed to light. Faster speeds (like 1/1000s) freeze motion; slower speeds (like 1/30s) can create motion blur.</span></a></label><input type="text" id="shutterSpeedPortrait" placeholder="1/160"></div>
</div>
<div class="input-row">
<div><label>ISO <a href="#" class="info-link" data-info="iso">? <span class="info-tooltip">ISO: Measures the camera sensor's sensitivity to light. Higher ISO allows for shooting in darker conditions but can introduce 'noise' (graininess) into the image.</span></a></label><input type="text" id="isoPortrait" placeholder="200"></div>
<div><label>WB (K) <a href="#" class="info-link" data-info="whiteBalance">? <span class="info-tooltip">White Balance (WB K): Adjusts the color temperature of your photos to make whites appear truly white, compensating for different light sources (e.g., warm indoor light vs. cool outdoor light). Measured in Kelvin (K).</span></a></label><input type="text" id="whiteBalancePortrait" placeholder="5200"></div>
</div>
<div class="input-row">
<div><label>Focal Length <a href="#" class="info-link" data-info="focalLength">? <span class="info-tooltip">Focal Length (mm): Refers to the lens's ability to magnify the scene. Shorter focal lengths (e.g., 24mm) offer a wide-angle view, while longer ones (e.g., 200mm) provide a telephoto (zoomed-in) view.</span></a></label><input type="text" id="focalLengthPortrait" placeholder="50mm"></div>
<div><label>Lens Type</label><input type="text" id="lensTypePortrait" placeholder="Prime"></div>
</div>
<label>Focus Point</label><input type="text" id="focusMethodPortrait" placeholder="e.g. on the eyes">
</div>
</div>
<!-- Hair & Makeup / Accessories (new for portrait) -->
<div class="accordion-item">
<div class="accordion-header" data-accordion="hairMakeupDetails">💄 Hair & Makeup / Accessories <span class="arrow">▶</span></div>
<div class="accordion-content" id="hairMakeupDetailsAccordion">
<label>Hair Style</label><input type="text" id="hairStyle" placeholder="e.g. flowing curls, slicked back">
<label>Makeup Style</label><input type="text" id="makeupStyle" placeholder="e.g. natural, smoky eyes, bold lips">
<label>Accessories</label><input type="text" id="accessories" placeholder="e.g. delicate necklace, statement earrings, hat">
</div>
</div>
</div>
<div id="stylingSettingsPortrait" class="tab-content">
<!-- Skin Texture & Mood (adapted for portrait) -->
<div class="accordion-item">
<div class="accordion-header" data-accordion="skinMoodPortrait">✨ Skin & Mood <span class="arrow">▶</span></div>
<div class="accordion-content expanded" id="skinMoodAccordionPortrait">
<label>Skin Texture</label>
<input type="text" id="skinTexturePortrait" placeholder="e.g. smooth, radiant, natural, glowing">
<label>Overall Mood</label>
<div class="mood-input-group">
<div>
<input type="text" id="overallMoodPortrait" placeholder="e.g. ethereal, powerful, intimate, joyful">
</div>
<button id="randomizeMoodBtnPortrait">Randomize Mood</button>
</div>
</div>
</div>
<!-- Composition (adapted for portrait) -->
<div class="accordion-item">
<div class="accordion-header" data-accordion="compositionDetailsPortrait">📐 Composition <span class="arrow">▶</span></div>
<div class="accordion-content" id="compositionDetailsAccordionPortrait">
<label>Rule</label><input type="text" id="compRulePortrait" placeholder="e.g. rule of thirds, leading lines">
<div class="input-row">
<div><label>Framing</label><input type="text" id="framingPortrait" placeholder="e.g. close-up, wide shot"></div>
<div><label>Neg. Space</label><input type="text" id="negativeSpacePortrait" placeholder="e.g. dramatic, balanced"></div>
</div>
<label>Depth of Field</label><input type="text" id="dofDescPortrait" placeholder="e.g. creamy bokeh, sharp background">
</div>
</div>
<!-- Post Processing (adapted for portrait) -->
<div class="accordion-item">
<div class="accordion-header" data-accordion="postProcessingDetailsPortrait">🎨 Post-Processing <span class="arrow">▶</span></div>
<div class="accordion-content" id="postProcessingDetailsAccordionPortrait">
<div class="input-row">
<div><label>Grading</label><input type="text" id="colorGradingPortrait" placeholder="e.g. warm, cinematic, monochrome"></div>
<div><label>Contrast</label><input type="text" id="contrastPortrait" placeholder="e.g. soft, strong"></div>
</div>
<div class="input-row">
<div><label>Sharpness</label><input type="text" id="sharpnessPortrait" placeholder="e.g. crisp, soft focus"></div>
<div><label>Grain</label><input type="text" id="grainNoisePortrait" placeholder="e.g. subtle film grain, clean"></div>
</div>
<div class="input-row">
<div><label>Retouch</label><input type="text" id="retouchingPortrait" placeholder="e.g. subtle skin softening, blemish removal"></div>
<div><label>Vignette</label><input type="text" id="vignettingPortrait" placeholder="e.g. strong, subtle, none"></div>
</div>
</div>
</div>
</div>
<div id="outputSettingsPortrait" class="tab-content">
<div class="accordion-item">
<div class="accordion-header" data-accordion="additionalInstructionsPortrait">📄 Additional Instructions <span class="arrow">▶</span></div>
<div class="accordion-content expanded" id="additionalInstructionsAccordionPortrait">
<div class="additional-instructions-group">
<div class="additional-instructions-options" id="additionalInstructionsOptionsPortrait">
<button type="button" data-value="Natural skin texture, no excessive smoothing or plastic look">Natural Skin</button>
<button type="button" data-value="Eyes sharp and in focus, catchlights present">Sharp Eyes</button>
<button type="button" data-value="Flattering angles, genuine expression">Flattering Portrait</button>
<button type="button" data-value="Avoid distorted features, maintain realistic proportions">Realistic Features</button>
</div>
<textarea id="additionalPortrait" rows="3" placeholder="Any specific details or exclusions..."></textarea>
</div>
</div>
</div>
</div>
</div> <!-- End of portraitSettings -->
</div> <!-- End of settingsBox -->
</div>
<!-- RIGHT COLUMN: OUTPUT (STICKY) -->
<div class="right-column">
<label>Generated Prompt</label>
<div id="promptOutput">Prompt will appear here...</div>
<button id="generatePrompt" class="action-btn">✨ Build Prompt</button>
<button id="copyPrompt" class="action-btn secondary" disabled>Copy to Clipboard</button>
<div style="border-top: 1px solid var(--border); margin-top: 20px; padding-top: 15px;">
<label>AI Optimization Target</label>
<select id="targetAIs" multiple>
<option value="midjourney">MidJourney v6</option>
<option value="dalle">DALL·E 3 / Bing</option>
<option value="gpt">Stable Diffusion / XL</option>
</select>
<div id="aiModelsContainer"></div>
</div>
</div>
</div>
</div>
<div id="toast" class="toast"></div>
<script>
// ---------------------------
// Toast
// ---------------------------
function toast(msg){
const t = document.getElementById("toast");
t.textContent = msg;
t.classList.add("show");
setTimeout(()=>t.classList.remove("show"),2000);
}
// ---------------------------
// Params & Templates - EXPANDED LockAIParams
// ---------------------------
class LockAIParams {
constructor(){
this.photographyType = "product"; // New property
this.subjectType = "product"; // Retained for compatibility with product description
this.description = "";
this.referenceImages = [];
this.environment = "studio neutral";
// EXPANDED LIGHTING
this.lighting = {
style:"soft studio lighting",
source:null,
direction:null,
intent:null,
count: null,
modifiers: null,
reflectionsGlares: null,
catchlights: null // New for portrait
};
// EXPANDED CAMERA
this.camera = {
perspective:"product-level", // For product
shotType:null, // For portrait
aperture:null,
shutterSpeed:null,
ISO:null,
whiteBalance:null,
focalLength: null,
lensType: null,
focusMethod: null, // For product
focusPoint: null // For portrait
};
this.aspectRatio = "1:1";
this.background = null;
this.props = [];
this.additional = "";
this.productHeight = "";
this.productWidth = "";
this.propDims = [];
// NEW PROPERTIES
this.surfaceMaterial = ""; // For product
this.surfaceColor = ""; // For product
this.skinTexture = ""; // For portrait
this.overallMood = "";
// NEW COMPOSITION
this.composition = {
rule: null,
framing: null,
negativeSpace: null,
dofDesc: null
};
// NEW POST-PROCESSING
this.postProcessing = {
colorGrading: null,
contrast: null,
sharpness: null,
grainNoise: null,
retouching: null,
vignetting: null
};
// PORTRAIT-SPECIFIC FIELDS
this.portrait = {
subjectDescription: "",
age: "",
gender: "",
ethnicity: "",
expressionPose: "",
attire: "",
accessories: "",
hairStyle: "",
makeupStyle: ""
};
}
}
// UPDATED promptTemplates for more natural language
const promptTemplates = {
product: `A professional, photorealistic image of the uploaded {{description}}, shown from a {{cameraPerspective}} angle.
Dimensions are approximately {{productHeight}} tall and {{productWidth}} wide.
Crucially, do not alter the original positioning or orientation of the main subject; maintain its exact geometry, proportions, materials, finishes, textures, and markings.
The scene is set in a {{environment}} with {{lightingStyle}} lighting{{lightingDetails}}.
The background features: {{background}}.
Camera settings: {{cameraDetails}}.
Aspect ratio: {{aspectRatio}}.
{{surfaceDetails}}
{{compositionDetails}}
{{props}}{{propDims}}
{{postProcessingDetails}}
{{additional}}
This image should be strictly photorealistic and suitable for commercial use.
`,
portrait: `A photorealistic {{shotType}} portrait of {{portraitSubjectDescription}} ({{portraitAge}}, {{portraitGender}}, {{portraitEthnicity}}).
The subject is in a {{portraitExpressionPose}} pose, wearing {{portraitAttire}}{{accessories}}.
The location is a {{portraitEnvironment}} with {{lightingStyle}} lighting{{lightingDetails}}.
Hair style: {{hairStyle}}. Makeup style: {{makeupStyle}}.
Camera settings: {{cameraDetails}}.
Aspect ratio: {{aspectRatio}}.
{{skinDetails}}
{{compositionDetails}}
{{postProcessingDetails}}
{{additional}}
This image should be strictly photorealistic and suitable for commercial use.
`
};
// UPDATED buildPrompt function to include new parameters and improved cleanup
function buildPrompt(params, template, index=1){
const replacements = {
index,
photographyType: params.photographyType,
description: params.description || params.subjectType, // for product
productHeight: params.productHeight || "unspecified",
productWidth: params.productWidth || "unspecified",
environment: params.environment, // for product
// Portrait specific fields
portraitSubjectDescription: params.portrait.subjectDescription || "a person",
portraitAge: params.portrait.age || "unspecified age",
portraitGender: params.portrait.gender || "unspecified gender",
portraitEthnicity: params.portrait.ethnicity || "unspecified ethnicity",
portraitExpressionPose: params.portrait.expressionPose || "neutral expression",
portraitAttire: params.portrait.attire || "casual clothing",
portraitEnvironment: params.portrait.environment || "a studio seamless background",
shotType: params.camera.shotType || "portrait",
accessories: params.portrait.accessories ? ` with ${params.portrait.accessories}` : "",
hairStyle: params.portrait.hairStyle || "natural hair",
makeupStyle: params.portrait.makeupStyle || "natural makeup",
// Lighting Details
lightingStyle: params.lighting.style,
lightingDetails: (() => {
const parts = [];
if (params.lighting.source) parts.push(`a ${params.lighting.source} light source`);
if (params.lighting.direction) parts.push(`light coming from the ${params.lighting.direction}`);
if (params.lighting.intent) parts.push(`to achieve a ${params.lighting.intent} mood`);
if (params.lighting.count) parts.push(`${params.lighting.count} setup`);
if (params.lighting.modifiers) parts.push(`using ${params.lighting.modifiers}`);
if (params.photographyType === "product" && params.lighting.reflectionsGlares) parts.push(`with ${params.lighting.reflectionsGlares}`);
if (params.photographyType === "portrait" && params.lighting.catchlights) parts.push(`with ${params.lighting.catchlights} catchlights`);
return parts.length > 0 ? ` (${parts.join(", ")})` : "";
})(),
// Camera Details
cameraPerspective: params.camera.perspective, // For product
cameraDetails: (() => {
const parts = [];
if (params.camera.aperture) parts.push(`aperture f/${params.camera.aperture}`);
if (params.camera.shutterSpeed) parts.push(`shutter speed ${params.camera.shutterSpeed}s`);
if (params.camera.ISO) parts.push(`ISO ${params.camera.ISO}`);
if (params.camera.whiteBalance) parts.push(`white balance ${params.camera.whiteBalance}K`);
if (params.camera.focalLength) parts.push(`focal length ${params.camera.focalLength}`);
if (params.camera.lensType) parts.push(`a ${params.camera.lensType} lens`);
if (params.photographyType === "product" && params.camera.focusMethod) parts.push(`focus method: ${params.camera.focusMethod}`);
if (params.photographyType === "portrait" && params.camera.focusPoint) parts.push(`focus point: ${params.camera.focusPoint}`);
return parts.length > 0 ? `${parts.join(", ")}` : "standard camera settings";
})(),
aspectRatio: params.aspectRatio,
background: params.background || "a neutral studio background",
// NEW Surface Details (Product) / Skin Details (Portrait)
surfaceDetails: (() => {
const parts = [];
if (params.surfaceMaterial) parts.push(`The surface is ${params.surfaceMaterial}`);
if (params.surfaceColor) parts.push(`in a ${params.surfaceColor} color`);
if (params.overallMood && params.photographyType === "product") parts.push(`creating a ${params.overallMood} atmosphere`);
return parts.length > 0 ? `${parts.join(" and ")}.` : "";
})(),
skinDetails: (() => {
const parts = [];
if (params.skinTexture) parts.push(`Skin texture is ${params.skinTexture}`);
if (params.overallMood && params.photographyType === "portrait") parts.push(`creating a ${params.overallMood} mood`);
return parts.length > 0 ? `${parts.join(" and ")}.` : "";
})(),
// NEW Composition Details
compositionDetails: (() => {
const parts = [];
if (params.composition.rule) parts.push(`utilizing the ${params.composition.rule} rule`);
if (params.composition.framing) parts.push(`with a ${params.composition.framing} shot`);
if (params.composition.negativeSpace) parts.push(`featuring ${params.composition.negativeSpace} negative space`);
if (params.composition.dofDesc) parts.push(`and ${params.composition.dofDesc}`);
return parts.length > 0 ? `Composition details: ${parts.join(", ")}.` : "";
})(),
props: params.props.length > 0 ? `Props included are: ${params.props.join(", ")}` : "No specific props are added",
propDims: params.propDims.length > 0 ? ` (with dimensions: ${params.propDims.join(", ")})` : "",
// NEW Post-Processing Details
postProcessingDetails: (() => {
const parts = [];
if (params.postProcessing.colorGrading) parts.push(`${params.postProcessing.colorGrading} color grading`);
if (params.postProcessing.contrast) parts.push(`${params.postProcessing.contrast} contrast`);
if (params.postProcessing.sharpness) parts.push(`${params.postProcessing.sharpness} sharpness`);
if (params.postProcessing.grainNoise) parts.push(`${params.postProcessing.grainNoise} grain/noise`);
if (params.postProcessing.retouching) parts.push(`${params.postProcessing.retouching} retouching`);
if (params.postProcessing.vignetting) parts.push(`${params.postProcessing.vignetting} vignetting`);
return parts.length > 0 ? `Post-processing includes: ${parts.join(", ")}.` : "";
})(),
additional: params.additional ? `Additional notes: ${params.additional}.` : ""
};
let prompt = template;
Object.entries(replacements).forEach(([k,v])=>{
prompt = prompt.replace(new RegExp(`{{${k}}}`,"g"), v);
});
// --- Advanced cleanup for natural language prompts ---
// Remove lines that are left empty or contain only default/empty phrases
prompt = prompt.split('\n').filter(line => {
const trimmedLine = line.trim();
if (!trimmedLine) return false; // Remove entirely empty lines
if (trimmedLine === "Dimensions are approximately unspecified tall and unspecified wide.") return false;
if (trimmedLine === "The scene is set in a studio neutral with soft studio lighting lighting.") return false; // If environment/lighting are defaults and no details
if (trimmedLine === "The background features: a neutral studio background.") return false; // If background is default
if (trimmedLine === "Camera settings: standard camera settings.") return false; // If camera details are default
if (trimmedLine === "No specific props are added (with dimensions: ).") return false; // If props are empty
if (trimmedLine === "No specific props are added.") return false; // If props are empty
if (trimmedLine === "Composition details:.") return false; // If composition is empty
if (trimmedLine === "Post-processing includes:.") return false; // If post-processing is empty
if (trimmedLine === "Additional notes:.") return false; // If additional is empty
if (trimmedLine === "The surface is.") return false; // If surface details are empty (product)
if (trimmedLine === "Skin texture is.") return false; // If skin details are empty (portrait)
if (trimmedLine === "Hair style: natural hair. Makeup style: natural makeup.") return false; // If portrait specifics are default
if (trimmedLine.match(/A photorealistic (.*?) portrait of a person \(unspecified age, unspecified gender, unspecified ethnicity\)\./)) return false; // Default portrait line
if (trimmedLine.match(/The subject is in a neutral expression pose, wearing casual clothing\./)) return false; // Default portrait subject details
// More robust check for lighting details - if it only contains the style and then an empty parentheses group
if (trimmedLine.match(/with (.*?) lighting \(\)/)) {
const lightingStyleMatch = trimmedLine.match(/with (.*?) lighting/);
if (lightingStyleMatch && lightingStyleMatch[1] === params.lighting.style) {
const currentLightingDetails = replacements.lightingDetails.trim();
if (currentLightingDetails === "()") return false; // If only "()" is left from lightingDetails
}
}
return true;
}).join('\n');
// Remove any remaining multiple spaces or leading/trailing punctuation from the cleanup
prompt = prompt.replace(/\s\s+/g, " "); // Replace multiple spaces with single space
prompt = prompt.replace(/,\s*\./g, "."); // Remove commas before periods
prompt = prompt.replace(/:\s*\./g, "."); // Remove colons before periods
prompt = prompt.replace(/\.\s*\./g, "."); // Remove double periods
prompt = prompt.replace(/( and )+\./g, "."); // Remove "and ." pattern
prompt = prompt.replace(/,\s*$/gm, ""); // Remove trailing commas at end of lines
prompt = prompt.replace(/\s*,\s*\./g, "."); // Catch cases like "item, ." -> "item."
prompt = prompt.replace(/\s\.\s*$/gm, "."); // Ensure a single period at the end of a sentence if it was modified
return prompt.trim();
}
function generateLockAIPrompts(params, targetAIs=[]){
const template = promptTemplates[params.photographyType];
if(!template) throw new Error(`No template for photography type ${params.photographyType}`);
const basePrompts = params.referenceImages.map((_,i)=>buildPrompt(params,template,i+1));
const optimizedPrompts = {};
targetAIs.forEach(ai=>{
optimizedPrompts[ai] = basePrompts.map(p=>{
if(ai==="midjourney") {
const arValue = params.aspectRatio.replace(":", "_");
// MidJourney specific modifications, simplified as per the new natural language prompt structure
let mjPrompt = p;
mjPrompt = mjPrompt.replace(/Environment: (.*?), Lighting: (.*?)\./g, 'Environment: $1, Lighting: $2');
mjPrompt = mjPrompt.replace(/Camera settings: (.*?)\./g, 'Camera: $1.');
mjPrompt = mjPrompt.replace(/Props included are: (.*?)( \(with dimensions: .*?\))?\./g, 'Props: $1$2.');
mjPrompt = mjPrompt.replace(/Post-processing includes: (.*?)\./g, 'Post-processing: $1.');
// Add --ar parameter only if not 1:1, as 1:1 is MJ default
return `${mjPrompt} --v 6 ${params.aspectRatio !== "1:1" ? `--ar ${arValue}` : ''}`.trim();
}
if(ai==="dalle" || ai==="gpt") {
// DALL-E/GPT are more flexible with natural language, so the base prompt often works well
}
return p;
});
});
return { basePrompts, optimizedPrompts };
}
// ---------------------------
// UI Elements
// ---------------------------
const photographyTypeSelector = document.getElementById("photographyType");
const productSettingsGroup = document.getElementById("productSettings");
const portraitSettingsGroup = document.getElementById("portraitSettings");
const referencePhotos = document.getElementById("referencePhotos");
const photoPreviews = document.getElementById("photoPreviews");
const generatePromptBtn = document.getElementById("generatePrompt");
const copyPromptBtn = document.getElementById("copyPrompt");
const promptOutput = document.getElementById("promptOutput");
const targetAIs = document.getElementById("targetAIs");
const aiContainer = document.getElementById("aiModelsContainer");
const randomizeMoodBtnProduct = document.getElementById("randomizeMoodBtn");
const overallMoodInputProduct = document.getElementById("overallMood");
const randomizeMoodBtnPortrait = document.getElementById("randomizeMoodBtnPortrait");
const overallMoodInputPortrait = document.getElementById("overallMoodPortrait");
const suggestPropComboBtn = document.getElementById("suggestPropComboBtn");
const additionalInstructionsTextareaProduct = document.getElementById("additional");
const additionalInstructionsOptionsProduct = document.querySelector(".additional-instructions-options"); // For product
const additionalInstructionsTextareaPortrait = document.getElementById("additionalPortrait");
const additionalInstructionsOptionsPortrait = document.getElementById("additionalInstructionsOptionsPortrait"); // For portrait
const darkRusticPresetBtn = document.getElementById("darkRusticPresetBtn");
const savePresetBtn = document.getElementById("savePresetBtn");
const loadPresetBtn = document.getElementById("loadPresetBtn");
const loadPresetInput = document.getElementById("loadPresetInput");
// ---------------------------
// Photography Type Switcher Logic
// ---------------------------
function switchPhotographyType() {
const selectedType = photographyTypeSelector.value;
if (selectedType === "product") {
productSettingsGroup.style.display = 'flex';
portraitSettingsGroup.style.display = 'none';
// Re-activate product tabs if portrait was active
document.getElementById('basicSettings').classList.add('active');
document.getElementById('basicSettingsPortrait').classList.remove('active');
document.querySelector('.tab-bar .tab-button[data-tab="basicSettings"]').classList.add('active');
// The Dark Rustic Preset button is part of product output settings
darkRusticPresetBtn.style.display = 'block';
} else if (selectedType === "portrait") {
productSettingsGroup.style.display = 'none';
portraitSettingsGroup.style.display = 'flex';
// Re-activate portrait tabs if product was active
document.getElementById('basicSettingsPortrait').classList.add('active');
document.getElementById('basicSettings').classList.remove('active');
document.querySelector('.tab-bar .tab-button[data-tab="basicSettings"]').classList.add('active');
// Hide Dark Rustic Preset button for portrait mode
darkRusticPresetBtn.style.display = 'none';
}
updateGeneratedPrompts();
initializeAccordionHeights(); // Re-initialize accordion heights for the visible group
}
photographyTypeSelector.addEventListener("change", switchPhotographyType);
// ---------------------------
// Image Upload & Preview
// ---------------------------
let uploadedImages = []; // Store image data URLs
referencePhotos.addEventListener("change", (e)=>{
const files = Array.from(e.target.files);
// Filter out files that are not images or if over limit
const newImageFiles = files.filter(file => file.type.startsWith('image/') && uploadedImages.length < 3);
if (newImageFiles.length === 0 && files.length > 0) {
if (uploadedImages.length >= 3) {
toast("Maximum 3 reference images reached.");
} else {
toast("Please select image files (JPG, PNG, GIF, etc.).");
}
referencePhotos.value = ""; // Clear file input
return;
}
let loadedCount = 0;
newImageFiles.forEach(file => {
const reader = new FileReader();
reader.onload = e => {
uploadedImages.push(e.target.result);
renderPreviews();
loadedCount++;
if (loadedCount === newImageFiles.length) {
updateGeneratedPrompts();
}
};
reader.readAsDataURL(file);
});
referencePhotos.value = ""; // Clear file input
});
function renderPreviews() {
photoPreviews.innerHTML = "";
if (uploadedImages.length === 0) {
// No specific content needed, CSS :empty::before handles it.
} else {
uploadedImages.forEach((src, index) => {
const container = document.createElement("div");
container.className = "previewImgContainer";
const img = document.createElement("img");
img.src = src;
img.className = "previewImg";
const removeBtn = document.createElement("span");
removeBtn.className = "removePreview";
removeBtn.textContent = "×";
removeBtn.addEventListener("click", () => {
uploadedImages.splice(index, 1);
renderPreviews();
updateGeneratedPrompts(); // Update prompt after removing image
});
container.appendChild(img);
container.appendChild(removeBtn);
photoPreviews.appendChild(container);
});
}
}
// ---------------------------
// Refactored Prompt Generation Function
// This function is now called by both the button and the select change event
// ---------------------------
function updateGeneratedPrompts() {
const params = new LockAIParams();
params.photographyType = photographyTypeSelector.value;
const descriptionIsEmpty = (params.photographyType === "product" && !document.getElementById("description").value.trim()) ||
(params.photographyType === "portrait" && !document.getElementById("portraitSubjectDescription").value.trim());
if (uploadedImages.length === 0 && descriptionIsEmpty) {
promptOutput.textContent = "Enter a description or upload an image to generate prompts.";
aiContainer.innerHTML = "";
copyPromptBtn.disabled = true;
generatePromptBtn.textContent = "Build Prompt";
generatePromptBtn.disabled = false;
return;
}
generatePromptBtn.textContent="Building Prompt…";
generatePromptBtn.disabled=true;
copyPromptBtn.disabled=true;
setTimeout(()=>{
params.referenceImages = uploadedImages.map(src => "image"); // Just placeholder for count
if (params.photographyType === "product") {
params.description = document.getElementById("description").value.trim() || "product";
params.environment = document.getElementById("environment").value;
// Lighting
params.lighting.style = document.getElementById("lighting").value;
params.lighting.source = document.getElementById("lightSource").value.trim() || null;
params.lighting.direction = document.getElementById("lightDirection").value.trim() || null;
params.lighting.intent = document.getElementById("lightIntent").value.trim() || null;
params.lighting.count = document.getElementById("lightCount").value.trim() || null;
params.lighting.modifiers = document.getElementById("lightModifiers").value.trim() || null;
params.lighting.reflectionsGlares = document.getElementById("reflectionsGlares").value.trim() || null;
// Camera
params.camera.perspective = document.getElementById("cameraPerspective").value;
params.camera.aperture = document.getElementById("aperture").value.trim() || null;
params.camera.shutterSpeed = document.getElementById("shutterSpeed").value.trim() || null;
params.camera.ISO = document.getElementById("iso").value.trim() || null;
params.camera.whiteBalance = document.getElementById("whiteBalance").value.trim() || null;
params.camera.focalLength = document.getElementById("focalLength").value.trim() || null;
params.camera.lensType = document.getElementById("lensType").value.trim() || null;
params.camera.focusMethod = document.getElementById("focusMethod").value.trim() || null;
params.aspectRatio = document.getElementById("aspectRatio").value;
params.background = document.getElementById("background").value.trim() || null;
// Product dimensions
params.productHeight = document.getElementById("productHeight").value.trim() || "";
params.productWidth = document.getElementById("productWidth").value.trim() || "";
params.props = [document.getElementById("prop1").value.trim(),
document.getElementById("prop2").value.trim(),
document.getElementById("prop3").value.trim(),
document.getElementById("prop4").value.trim()].filter(Boolean);
params.propDims = [document.getElementById("prop1Dim").value.trim(),
document.getElementById("prop2Dim").value.trim(),
document.getElementById("prop3Dim").value.trim(),
document.getElementById("prop4Dim").value.trim()].filter(Boolean);
params.additional = additionalInstructionsTextareaProduct.value.trim();
// Styling & Surface
params.surfaceMaterial = document.getElementById("surfaceMaterial").value.trim() || "";
params.surfaceColor = document.getElementById("surfaceColor").value.trim() || "";
params.overallMood = document.getElementById("overallMood").value.trim() || "";
// Composition
params.composition.rule = document.getElementById("compRule").value.trim() || null;
params.composition.framing = document.getElementById("framing").value.trim() || null;
params.composition.negativeSpace = document.getElementById("negativeSpace").value.trim() || null;
params.composition.dofDesc = document.getElementById("dofDesc").value.trim() || null;
// Post-Processing
params.postProcessing.colorGrading = document.getElementById("colorGrading").value.trim() || null;
params.postProcessing.contrast = document.getElementById("contrast").value.trim() || null;
params.postProcessing.sharpness = document.getElementById("sharpness").value.trim() || null;
params.postProcessing.grainNoise = document.getElementById("grainNoise").value.trim() || null;
params.postProcessing.retouching = document.getElementById("retouching").value.trim() || null;
params.postProcessing.vignetting = document.getElementById("vignetting").value.trim() || null;
} else if (params.photographyType === "portrait") {
params.portrait.subjectDescription = document.getElementById("portraitSubjectDescription").value.trim() || "a person";
params.portrait.age = document.getElementById("portraitAge").value.trim() || null;
params.portrait.gender = document.getElementById("portraitGender").value.trim() || null;
params.portrait.ethnicity = document.getElementById("portraitEthnicity").value.trim() || null;
params.portrait.expressionPose = document.getElementById("portraitExpressionPose").value.trim() || null;
params.portrait.attire = document.getElementById("portraitAttire").value.trim() || null;
params.portrait.accessories = document.getElementById("accessories").value.trim() || null;
params.portrait.hairStyle = document.getElementById("hairStyle").value.trim() || null;
params.portrait.makeupStyle = document.getElementById("makeupStyle").value.trim() || null;
params.portrait.environment = document.getElementById("portraitEnvironment").value;
params.lighting.style = document.getElementById("portraitLighting").value;
// Lighting Refinement Portrait
params.lighting.source = document.getElementById("lightSourcePortrait").value.trim() || null;
params.lighting.direction = document.getElementById("lightDirectionPortrait").value.trim() || null;
params.lighting.intent = document.getElementById("lightIntentPortrait").value.trim() || null;
params.lighting.count = document.getElementById("lightCountPortrait").value.trim() || null;
params.lighting.modifiers = document.getElementById("lightModifiersPortrait").value.trim() || null;
params.lighting.catchlights = document.getElementById("catchlightsPortrait").value.trim() || null;
params.camera.shotType = document.getElementById("portraitShotType").value;
params.aspectRatio = document.getElementById("portraitAspectRatio").value;
// Camera Advanced Portrait
params.camera.aperture = document.getElementById("aperturePortrait").value.trim() || null;
params.camera.shutterSpeed = document.getElementById("shutterSpeedPortrait").value.trim() || null;
params.camera.ISO = document.getElementById("isoPortrait").value.trim() || null;
params.camera.whiteBalance = document.getElementById("whiteBalancePortrait").value.trim() || null;
params.camera.focalLength = document.getElementById("focalLengthPortrait").value.trim() || null;
params.camera.lensType = document.getElementById("lensTypePortrait").value.trim() || null;
params.camera.focusPoint = document.getElementById("focusMethodPortrait").value.trim() || null; // Using focusMethodPortrait for focusPoint
// Skin & Mood Portrait
params.skinTexture = document.getElementById("skinTexturePortrait").value.trim() || "";
params.overallMood = document.getElementById("overallMoodPortrait").value.trim() || "";
// Composition Portrait
params.composition.rule = document.getElementById("compRulePortrait").value.trim() || null;
params.composition.framing = document.getElementById("framingPortrait").value.trim() || null;
params.composition.negativeSpace = document.getElementById("negativeSpacePortrait").value.trim() || null;
params.composition.dofDesc = document.getElementById("dofDescPortrait").value.trim() || null;
// Post-Processing Portrait
params.postProcessing.colorGrading = document.getElementById("colorGradingPortrait").value.trim() || null;
params.postProcessing.contrast = document.getElementById("contrastPortrait").value.trim() || null;
params.postProcessing.sharpness = document.getElementById("sharpnessPortrait").value.trim() || null;
params.postProcessing.grainNoise = document.getElementById("grainNoisePortrait").value.trim() || null;
params.postProcessing.retouching = document.getElementById("retouchingPortrait").value.trim() || null;
params.postProcessing.vignetting = document.getElementById("vignettingPortrait").value.trim() || null;
params.additional = additionalInstructionsTextareaPortrait.value.trim();
}
const selectedAIs = Array.from(targetAIs.selectedOptions).map(o=>o.value);
const { basePrompts, optimizedPrompts } = generateLockAIPrompts(params, selectedAIs);
promptOutput.textContent = basePrompts.join("\n\n");
aiContainer.innerHTML = "";
Object.entries(optimizedPrompts).forEach(([ai, prompts])=>{
const div = document.createElement("div");
div.innerHTML = `<strong>${ai.toUpperCase()} Optimized:</strong><div style="white-space:pre-wrap; margin-top:5px;">${prompts.join("\n\n")}</div>`;
aiContainer.appendChild(div);
});
copyPromptBtn.disabled=false;
generatePromptBtn.textContent="Build Prompt";
generatePromptBtn.disabled=false;
},200); // Small delay for "Building Prompt..." message visibility
}
// ---------------------------
// Generate Prompt Button Event Listener
// ---------------------------
generatePromptBtn.addEventListener("click", updateGeneratedPrompts);
// ---------------------------
// Auto-Update Prompts on AI Target Change Event Listener
// ---------------------------
targetAIs.addEventListener("change", updateGeneratedPrompts);
// ---------------------------
// Dynamic Prompt Updates for all relevant input fields
// ---------------------------
document.querySelectorAll('.left-column input, .left-column select, .left-column textarea').forEach(element => {
// Exclude file inputs as they have their own change listener
if (element.type !== 'file') {
element.addEventListener('input', updateGeneratedPrompts);
element.addEventListener('change', updateGeneratedPrompts); // For select elements
}
});
// ---------------------------
// Copy
// ---------------------------
copyPromptBtn.addEventListener("click", ()=>{
navigator.clipboard.writeText(promptOutput.textContent);
toast("Prompt copied!");
});
// ---------------------------
// Support Button
// ---------------------------
document.getElementById("supportBtn").addEventListener("click",()=>{
window.open("https://paypal.me/visuallockai","_blank");
});
// ---------------------------
// NEW: Randomize Mood (Product)
// ---------------------------
const moods = [
"luxurious", "industrial", "serene", "energetic", "calm", "dramatic", "minimalist",
"futuristic", "vintage", "rustic", "cozy", "sleek", "vibrant", "moody", "clean", "grungy",
"elegant", "bold", "playful", "sophisticated"
];
randomizeMoodBtnProduct.addEventListener("click", () => {
const randomIndex = Math.floor(Math.random() * moods.length);
overallMoodInputProduct.value = moods[randomIndex];
updateGeneratedPrompts();
});
// ---------------------------
// NEW: Randomize Mood (Portrait)
// ---------------------------
const portraitMoods = [
"ethereal", "powerful", "intimate", "joyful", "melancholy", "confident", "mysterious",
"dreamy", "candid", "glamorous", "raw", "vulnerable", "playful", "serious"
];
randomizeMoodBtnPortrait.addEventListener("click", () => {
const randomIndex = Math.floor(Math.random() * portraitMoods.length);
overallMoodInputPortrait.value = portraitMoods[randomIndex];
updateGeneratedPrompts();
});
// ---------------------------
// NEW: Prop Combos
// ---------------------------
const propCombos = [
["vintage camera", "leather journal"],
["coffee cup", "open book"],
["succulent plant", "geometric vase"],
["wooden block", "dried flower"],
["minimalist watch", "small sculpture"],
["spectacles", "quill pen"],
["sci-fi gadget", "glowing orb"],
["aged map", "compass"],
["velvet cloth", "gold jewelry"],
["ceramic bowl", "fresh fruit"]
];
suggestPropComboBtn.addEventListener("click", () => {
const randomIndex = Math.floor(Math.random() * propCombos.length);
const selectedCombo = propCombos[randomIndex];
document.getElementById("prop1").value = selectedCombo[0];
document.getElementById("prop2").value = selectedCombo[1];
document.getElementById("prop3").value = "";
document.getElementById("prop4").value = "";
document.getElementById("prop1Dim").value = "";
document.getElementById("prop2Dim").value = "";
document.getElementById("prop3Dim").value = "";
document.getElementById("prop4Dim").value = "";
updateGeneratedPrompts();
});
// ---------------------------
// NEW: Additional Instructions Buttons (Product)
// ---------------------------
additionalInstructionsOptionsProduct.querySelectorAll('button').forEach(button => {
button.addEventListener('click', () => {
// Toggle selected class
button.classList.toggle('selected');
// Get all selected values
const selectedValues = Array.from(additionalInstructionsOptionsProduct.querySelectorAll('button.selected'))
.map(btn => btn.dataset.value);
// Update the textarea with selected values, joined by a comma and space
additionalInstructionsTextareaProduct.value = selectedValues.join(', ');
updateGeneratedPrompts();
});
});
// ---------------------------
// NEW: Additional Instructions Buttons (Portrait)
// ---------------------------
additionalInstructionsOptionsPortrait.querySelectorAll('button').forEach(button => {
button.addEventListener('click', () => {
// Toggle selected class
button.classList.toggle('selected');
// Get all selected values
const selectedValues = Array.from(additionalInstructionsOptionsPortrait.querySelectorAll('button.selected'))
.map(btn => btn.dataset.value);
// Update the textarea with selected values, joined by a comma and space
additionalInstructionsTextareaPortrait.value = selectedValues.join(', ');
updateGeneratedPrompts();
});
});
// ---------------------------
// NEW: Dark Rustic Preset Functionality
// ---------------------------
darkRusticPresetBtn.addEventListener("click", () => {
// Ensure product photography is selected first
photographyTypeSelector.value = "product";
switchPhotographyType(); // Apply display changes
// Basic fields
document.getElementById("description").value = "Tactical handgun, matte black finish";
document.getElementById("productHeight").value = "0 ft 8 in";
document.getElementById("productWidth").value = "0 ft 4 in";
document.getElementById("environment").value = "controlled tactical";
document.getElementById("lighting").value = "controlled dramatic lighting";
// Lighting accordion fields
document.getElementById("lightSource").value = "strobe";
document.getElementById("lightDirection").value = "side";
document.getElementById("lightIntent").value = "moody, atmospheric";
document.getElementById("lightCount").value = "1-point";
document.getElementById("lightModifiers").value = "snoot, grid";
document.getElementById("reflectionsGlares").value = "subtle specular highlights, minimal glare";
document.getElementById("cameraPerspective").value = "product-level";
document.getElementById("aspectRatio").value = "4:3";
document.getElementById("background").value = "Dark, rustic wooden table with leather accents, Still life on distressed wood, with a dark, moody background and soft, directional lighting, Vintage Western-style backdrop with old leather, wooden surfaces, and subtle smoke, Dark, atmospheric photography setting with natural elements like wood, leather, and foliage";
// Camera accordion fields
document.getElementById("aperture").value = "2.8";
document.getElementById("shutterSpeed").value = "1/100";
document.getElementById("iso").value = "200";
document.getElementById("whiteBalance").value = "3200";
document.getElementById("focalLength").value = "100mm";
document.getElementById("lensType").value = "macro";
document.getElementById("focusMethod").value = "single point focus, sharpest details on product";
document.getElementById("surfaceMaterial").value = "distressed wood, aged leather";
document.getElementById("surfaceColor").value = "dark brown, deep red";
document.getElementById("overallMood").value = "rustic, moody, vintage, atmospheric";
// Props
document.getElementById("prop1").value = "vintage bullets";
document.getElementById("prop2").value = "leather holster";
document.getElementById("prop3").value = "";
document.getElementById("prop4").value = "";
document.getElementById("prop1Dim").value = "";
document.getElementById("prop2Dim").value = "";
document.getElementById("prop3Dim").value = "";
document.getElementById("prop4Dim").value = "";
// Composition accordion fields
document.getElementById("compRule").value = "rule of thirds, leading lines";
document.getElementById("framing").value = "close-up, medium shot";
document.getElementById("negativeSpace").value = "minimal, controlled negative space";
document.getElementById("dofDesc").value = "creamy bokeh, shallow depth of field";
// Post-Processing accordion fields
document.getElementById("colorGrading").value = "dark, desaturated, warm tones, cinematic grade";
document.getElementById("contrast").value = "high";
document.getElementById("sharpness").value = "sharp, focused on product";
document.getElementById("grainNoise").value = "subtle film grain";
document.getElementById("retouching").value = "minimal, natural imperfections preserved";
document.getElementById("vignetting").value = "strong, dark corners";
// Clear and set additional instructions
additionalInstructionsTextareaProduct.value = "Clean, dry, polished black metal surface, no smudges, no oil, matte finish, do not alter the rotation of the subject, sharp focus, high detail.";
// Deselect all additional instruction buttons, then re-select relevant ones
additionalInstructionsOptionsProduct.querySelectorAll('button').forEach(btn => btn.classList.remove('selected'));
// Simulate clicks on the relevant buttons for the preset
const presetInstructions = [
"Clean, dry, polished black metal surface, no smudges, no oil, matte finish",
"do not alter the rotation of the subject"
];
additionalInstructionsOptionsProduct.querySelectorAll('button').forEach(btn => {
if (btn.dataset.value === "Clean, dry, polished black metal surface, no smudges, no oil, matte finish" ||
btn.dataset.value === "do not alter the rotation of the subject") { // Assuming this is an instruction, or add it as a new button
btn.classList.add('selected');
}
});
// Clear any uploaded images for the preset
uploadedImages = [];
renderPreviews();
updateGeneratedPrompts();
toast("Dark Rustic Preset Applied!");
});
// ---------------------------
// NEW: Save Preset Functionality
// ---------------------------
savePresetBtn.addEventListener("click", () => {
const presetData = {
photographyType: photographyTypeSelector.value,
// Product fields
description: document.getElementById("description").value,
productHeight: document.getElementById("productHeight").value,
productWidth: document.getElementById("productWidth").value,
environment: document.getElementById("environment").value,
lighting: {
style: document.getElementById("lighting").value,
source: document.getElementById("lightSource").value,
direction: document.getElementById("lightDirection").value,
intent: document.getElementById("lightIntent").value,
count: document.getElementById("lightCount").value,
modifiers: document.getElementById("lightModifiers").value,
reflectionsGlares: document.getElementById("reflectionsGlares").value,
catchlights: "" // Ensure this is saved for portrait
},
camera: {
perspective: document.getElementById("cameraPerspective").value,
aperture: document.getElementById("aperture").value,
shutterSpeed: document.getElementById("shutterSpeed").value,
ISO: document.getElementById("iso").value,
whiteBalance: document.getElementById("whiteBalance").value,
focalLength: document.getElementById("focalLength").value,
lensType: document.getElementById("lensType").value,
focusMethod: document.getElementById("focusMethod").value,
shotType: "", // Ensure this is saved for portrait
focusPoint: "" // Ensure this is saved for portrait
},
aspectRatio: document.getElementById("aspectRatio").value,
background: document.getElementById("background").value,
surfaceMaterial: document.getElementById("surfaceMaterial").value,
surfaceColor: document.getElementById("surfaceColor").value,
overallMood: document.getElementById("overallMood").value,
props: [
document.getElementById("prop1").value,
document.getElementById("prop2").value,
document.getElementById("prop3").value,
document.getElementById("prop4").value
],
propDims: [
document.getElementById("prop1Dim").value,
document.getElementById("prop2Dim").value,
document.getElementById("prop3Dim").value,
document.getElementById("prop4Dim").value
],
additional: additionalInstructionsTextareaProduct.value,
composition: {
rule: document.getElementById("compRule").value,
framing: document.getElementById("framing").value,
negativeSpace: document.getElementById("negativeSpace").value,
dofDesc: document.getElementById("dofDesc").value
},
postProcessing: {
colorGrading: document.getElementById("colorGrading").value,
contrast: document.getElementById("contrast").value,
sharpness: document.getElementById("sharpness").value,
grainNoise: document.getElementById("grainNoise").value,
retouching: document.getElementById("retouching").value,
vignetting: document.getElementById("vignetting").value
},
selectedAdditionalInstructionsProduct: Array.from(additionalInstructionsOptionsProduct.querySelectorAll('button.selected')).map(btn => btn.dataset.value),
// Portrait fields
portrait: {
subjectDescription: document.getElementById("portraitSubjectDescription").value,
age: document.getElementById("portraitAge").value,
gender: document.getElementById("portraitGender").value,
ethnicity: document.getElementById("portraitEthnicity").value,
expressionPose: document.getElementById("portraitExpressionPose").value,
attire: document.getElementById("portraitAttire").value,
accessories: document.getElementById("accessories").value,
hairStyle: document.getElementById("hairStyle").value,
makeupStyle: document.getElementById("makeupStyle").value
},
portraitEnvironment: document.getElementById("portraitEnvironment").value,
portraitLighting: {
style: document.getElementById("portraitLighting").value,
source: document.getElementById("lightSourcePortrait").value,
direction: document.getElementById("lightDirectionPortrait").value,
intent: document.getElementById("lightIntentPortrait").value,
count: document.getElementById("lightCountPortrait").value,
modifiers: document.getElementById("lightModifiersPortrait").value,
catchlights: document.getElementById("catchlightsPortrait").value,
reflectionsGlares: "" // Ensure this is saved for product
},
portraitCamera: {
shotType: document.getElementById("portraitShotType").value,
aperture: document.getElementById("aperturePortrait").value,
shutterSpeed: document.getElementById("shutterSpeedPortrait").value,
ISO: document.getElementById("isoPortrait").value,
whiteBalance: document.getElementById("whiteBalancePortrait").value,
focalLength: document.getElementById("focalLengthPortrait").value,
lensType: document.getElementById("lensTypePortrait").value,
focusPoint: document.getElementById("focusMethodPortrait").value,
perspective: "" // Ensure this is saved for product
},
portraitAspectRatio: document.getElementById("portraitAspectRatio").value,
skinTexture: document.getElementById("skinTexturePortrait").value,
overallMoodPortrait: document.getElementById("overallMoodPortrait").value,
portraitComposition: {
rule: document.getElementById("compRulePortrait").value,
framing: document.getElementById("framingPortrait").value,
negativeSpace: document.getElementById("negativeSpacePortrait").value,
dofDesc: document.getElementById("dofDescPortrait").value
},
portraitPostProcessing: {
colorGrading: document.getElementById("colorGradingPortrait").value,
contrast: document.getElementById("contrastPortrait").value,
sharpness: document.getElementById("sharpnessPortrait").value,
grainNoise: document.getElementById("grainNoisePortrait").value,
retouching: document.getElementById("retouchingPortrait").value,
vignetting: document.getElementById("vignettingPortrait").value
},
selectedAdditionalInstructionsPortrait: Array.from(additionalInstructionsOptionsPortrait.querySelectorAll('button.selected')).map(btn => btn.dataset.value)
};
const filename = `VisualLock_Preset_${new Date().toISOString().slice(0,10)}.json`;
const dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(presetData, null, 2));
const downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", filename);
document.body.appendChild(downloadAnchorNode);
downloadAnchorNode.remove();
toast("Preset saved!");
});
// ---------------------------
// NEW: Load Preset Functionality
// ---------------------------
loadPresetBtn.addEventListener("click", () => {
loadPresetInput.click(); // Trigger the hidden file input
});
loadPresetInput.addEventListener("change", (event) => {
const file = event.target.files[0];
if (!file) {
return;
}
const reader = new FileReader();
reader.onload = (e) => {
try {
const loadedPreset = JSON.parse(e.target.result);
applyPreset(loadedPreset);
toast("Preset loaded successfully!");
} catch (error) {
console.error("Error parsing preset file:", error);
toast("Error loading preset: Invalid file format.");
}
};
reader.readAsText(file);
// Clear the file input so the same file can be loaded again if needed
loadPresetInput.value = '';
});
function applyPreset(preset) {
// Set photography type first
photographyTypeSelector.value = preset.photographyType || "product";
switchPhotographyType(); // Update UI based on type
// Product fields
document.getElementById("description").value = preset.description || "";
document.getElementById("productHeight").value = preset.productHeight || "";
document.getElementById("productWidth").value = preset.productWidth || "";
document.getElementById("environment").value = preset.environment || "studio neutral";
document.getElementById("lighting").value = preset.lighting?.style || "soft studio lighting";
document.getElementById("cameraPerspective").value = preset.camera?.perspective || "product-level";
document.getElementById("aspectRatio").value = preset.aspectRatio || "1:1";
document.getElementById("background").value = preset.background || "";
document.getElementById("surfaceMaterial").value = preset.surfaceMaterial || "";
document.getElementById("surfaceColor").value = preset.surfaceColor || "";
document.getElementById("overallMood").value = preset.overallMood || "";
additionalInstructionsTextareaProduct.value = preset.additional || "";
// Lighting accordion fields (Product)
document.getElementById("lightSource").value = preset.lighting?.source || "";
document.getElementById("lightDirection").value = preset.lighting?.direction || "";
document.getElementById("lightIntent").value = preset.lighting?.intent || "";
document.getElementById("lightCount").value = preset.lighting?.count || "";
document.getElementById("lightModifiers").value = preset.lighting?.modifiers || "";
document.getElementById("reflectionsGlares").value = preset.lighting?.reflectionsGlares || "";
// Camera accordion fields (Product)
document.getElementById("aperture").value = preset.camera?.aperture || "";
document.getElementById("shutterSpeed").value = preset.camera?.shutterSpeed || "";
document.getElementById("iso").value = preset.camera?.ISO || "";
document.getElementById("whiteBalance").value = preset.camera?.whiteBalance || "";
document.getElementById("focalLength").value = preset.camera?.focalLength || "";
document.getElementById("lensType").value = preset.camera?.lensType || "";
document.getElementById("focusMethod").value = preset.camera?.focusMethod || "";
// Props accordion fields (Product)
document.getElementById("prop1").value = preset.props?.[0] || "";
document.getElementById("prop2").value = preset.props?.[1] || "";
document.getElementById("prop3").value = preset.props?.[2] || "";
document.getElementById("prop4").value = preset.props?.[3] || "";
document.getElementById("prop1Dim").value = preset.propDims?.[0] || "";
document.getElementById("prop2Dim").value = preset.propDims?.[1] || "";
document.getElementById("prop3Dim").value = preset.propDims?.[2] || "";
document.getElementById("prop4Dim").value = preset.propDims?.[3] || "";
// Composition accordion fields (Product)
document.getElementById("compRule").value = preset.composition?.rule || "";
document.getElementById("framing").value = preset.composition?.framing || "";
document.getElementById("negativeSpace").value = preset.composition?.negativeSpace || "";
document.getElementById("dofDesc").value = preset.composition?.dofDesc || "";
// Post-Processing accordion fields (Product)
document.getElementById("colorGrading").value = preset.postProcessing?.colorGrading || "";
document.getElementById("contrast").value = preset.postProcessing?.contrast || "";
document.getElementById("sharpness").value = preset.postProcessing?.sharpness || "";
document.getElementById("grainNoise").value = preset.postProcessing?.grainNoise || "";
document.getElementById("retouching").value = preset.postProcessing?.retouching || "";
document.getElementById("vignetting").value = preset.postProcessing?.vignetting || "";
// Update the state of additional instructions buttons (Product)
additionalInstructionsOptionsProduct.querySelectorAll('button').forEach(btn => {
const isSelected = preset.selectedAdditionalInstructionsProduct && preset.selectedAdditionalInstructionsProduct.includes(btn.dataset.value);
if (isSelected) {
btn.classList.add('selected');
} else {
btn.classList.remove('selected');
}
});
// Portrait fields
document.getElementById("portraitSubjectDescription").value = preset.portrait?.subjectDescription || "";
document.getElementById("portraitAge").value = preset.portrait?.age || "";
document.getElementById("portraitGender").value = preset.portrait?.gender || "";
document.getElementById("portraitEthnicity").value = preset.portrait?.ethnicity || "";
document.getElementById("portraitExpressionPose").value = preset.portrait?.expressionPose || "";
document.getElementById("portraitAttire").value = preset.portrait?.attire || "";
document.getElementById("accessories").value = preset.portrait?.accessories || "";
document.getElementById("hairStyle").value = preset.portrait?.hairStyle || "";
document.getElementById("makeupStyle").value = preset.portrait?.makeupStyle || "";
document.getElementById("portraitEnvironment").value = preset.portraitEnvironment || "studio seamless background";
document.getElementById("portraitLighting").value = preset.portraitLighting?.style || "natural soft window light";
document.getElementById("portraitShotType").value = preset.portraitCamera?.shotType || "headshot";
document.getElementById("portraitAspectRatio").value = preset.portraitAspectRatio || "1:1";
document.getElementById("skinTexturePortrait").value = preset.skinTexture || "";
document.getElementById("overallMoodPortrait").value = preset.overallMoodPortrait || "";
additionalInstructionsTextareaPortrait.value = preset.additionalPortrait || ""; // Assuming a separate additional field for portrait
// Lighting Refinement Portrait
document.getElementById("lightSourcePortrait").value = preset.portraitLighting?.source || "";
document.getElementById("lightDirectionPortrait").value = preset.portraitLighting?.direction || "";
document.getElementById("lightIntentPortrait").value = preset.portraitLighting?.intent || "";
document.getElementById("lightCountPortrait").value = preset.portraitLighting?.count || "";
document.getElementById("lightModifiersPortrait").value = preset.portraitLighting?.modifiers || "";
document.getElementById("catchlightsPortrait").value = preset.portraitLighting?.catchlights || "";
// Camera Advanced Portrait
document.getElementById("aperturePortrait").value = preset.portraitCamera?.aperture || "";
document.getElementById("shutterSpeedPortrait").value = preset.portraitCamera?.shutterSpeed || "";
document.getElementById("isoPortrait").value = preset.portraitCamera?.ISO || "";
document.getElementById("whiteBalancePortrait").value = preset.portraitCamera?.whiteBalance || "";
document.getElementById("focalLengthPortrait").value = preset.portraitCamera?.focalLength || "";
document.getElementById("lensTypePortrait").value = preset.portraitCamera?.lensType || "";
document.getElementById("focusMethodPortrait").value = preset.portraitCamera?.focusPoint || "";
// Composition Portrait
document.getElementById("compRulePortrait").value = preset.portraitComposition?.rule || "";
document.getElementById("framingPortrait").value = preset.portraitComposition?.framing || "";
document.getElementById("negativeSpacePortrait").value = preset.portraitComposition?.negativeSpace || "";
document.getElementById("dofDescPortrait").value = preset.portraitComposition?.dofDesc || "";
// Post-Processing Portrait
document.getElementById("colorGradingPortrait").value = preset.portraitPostProcessing?.colorGrading || "";
document.getElementById("contrastPortrait").value = preset.portraitPostProcessing?.contrast || "";
document.getElementById("sharpnessPortrait").value = preset.portraitPostProcessing?.sharpness || "";
document.getElementById("grainNoisePortrait").value = preset.portraitPostProcessing?.grainNoise || "";
document.getElementById("retouchingPortrait").value = preset.portraitPostProcessing?.retouching || "";
document.getElementById("vignettingPortrait").value = preset.portraitPostProcessing?.vignetting || "";
// Update the state of additional instructions buttons (Portrait)
additionalInstructionsOptionsPortrait.querySelectorAll('button').forEach(btn => {
const isSelected = preset.selectedAdditionalInstructionsPortrait && preset.selectedAdditionalInstructionsPortrait.includes(btn.dataset.value);
if (isSelected) {
btn.classList.add('selected');
} else {
btn.classList.remove('selected');
}
});
// Clear and re-render reference images if any were loaded (from local storage for example, not direct file upload)
// For simplicity, we'll just clear current and assume user will re-upload if needed.
uploadedImages = [];
renderPreviews();
// After applying the preset, update the generated prompts
updateGeneratedPrompts();
}
// ---------------------------
// NEW: Tabbed Interface Logic
// ---------------------------
document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', () => {
const selectedType = photographyTypeSelector.value;
let currentTabContentGroup = (selectedType === "product") ? productSettingsGroup : portraitSettingsGroup;
// Deactivate all tab buttons and hide all tab contents within the active group
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
currentTabContentGroup.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
// Activate clicked button and show corresponding content
button.classList.add('active');
// Determine which specific tab content div to show based on selectedType and tab data-tab
const targetTabId = button.dataset.tab + (selectedType === "portrait" ? "Portrait" : "");
document.getElementById(targetTabId).classList.add('active');
// Force a prompt update in case switching tabs affects input visibility for reading values
updateGeneratedPrompts();
});
});
// ---------------------------
// NEW: Accordion Logic
// ---------------------------
function toggleAccordion(header) {
const content = document.getElementById(header.dataset.accordion + ((photographyTypeSelector.value === "portrait") ? "PortraitAccordion" : "Accordion"));
const isExpanded = header.classList.toggle('expanded');
content.classList.toggle('expanded');
if (isExpanded) {
content.style.maxHeight = content.scrollHeight + "px";
} else {
content.style.maxHeight = "0";
}
updateGeneratedPrompts(); // Update prompt after opening/closing
}
document.querySelectorAll('.accordion-header').forEach(header => {
header.addEventListener('click', () => toggleAccordion(header));
});
// Function to set initial heights for expanded accordions
function initializeAccordionHeights() {
const selectedType = photographyTypeSelector.value;
let currentTabContentGroup = (selectedType === "product") ? productSettingsGroup : portraitSettingsGroup;
currentTabContentGroup.querySelectorAll('.accordion-content.expanded').forEach(content => {
content.style.maxHeight = content.scrollHeight + "px";
});
}
// Recalculate accordion heights on window resize
window.addEventListener('resize', () => {
initializeAccordionHeights(); // Call the unified function
});
// Initial setup on load
document.addEventListener("DOMContentLoaded", () => {
renderPreviews();
switchPhotographyType(); // Ensure correct initial display
initializeAccordionHeights(); // Set initial heights for accordions
});
</script>
</body>
</html>
Contact
APXpmg
Peter grillone. Rancho vista Lane
Eureka Springs AR
USA
Tel. four seven nine . four zero five . 7770
If you are a client in need of assistance, please contact our customer support. They can help you with product and service related questions.
© 2026 All Rights Reserved, LUSANTIS
PeterG
Quiet strength applied to visual craft
Directed by Peter.G
