Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>AVE - AI Video Editor</title> | |
<link rel="preconnect" href="https://fonts.googleapis.com"> | |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
<link href="https://fonts.googleapis.com/css2?family=Crimson+Text:ital,wght@0,400;0,600;0,700;1,400;1,600;1,700&display=swap" rel="stylesheet"> | |
<link href="https://fonts.googleapis.com/css2?family=Faculty+Glyphic&family=Funnel+Sans:ital,wght@0,300..800;1,300..800&display=swap" rel="stylesheet"> | |
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" integrity="sha512-SnH5WK+bZxgPHs44uWIX+LLJAJ9/2PkPKZ5QiAj6Ta86w+fsb2TkcmfRyVX3pBnMFcV7oQPJkl9QevSCWr3W6A==" crossorigin="anonymous" referrerpolicy="no-referrer" /> | |
<style> | |
:root { | |
--primary-color: #20c997; | |
--secondary-color: #0dcaf0; | |
--dark-bg: #12141c; | |
--card-bg: #252a41; | |
--card-bg-transparent: rgba(37, 42, 65, 0.9); | |
--input-bg: rgba(0, 0, 0, 0.2); | |
--input-focus-bg: rgba(0, 0, 0, 0.3); | |
--input-border: rgba(255, 255, 255, 0.1); | |
--input-focus-border: var(--secondary-color); | |
--text-primary: #ffffff; | |
--text-secondary: #e5e7eb; | |
--text-muted: #9ca3af; | |
--success-color: #4ade80; | |
--success-bg: rgba(74, 222, 128, 0.1); | |
--error-color: #f87171; | |
--error-bg: rgba(248, 113, 113, 0.1); | |
--gradient-start: #0d6efd; | |
--gradient-mid: var(--primary-color); | |
--gradient-end: var(--secondary-color); | |
--secondary-color-rgb: 13, 202, 240; | |
--switch-bg-off: #4b5563; | |
--switch-bg-on: linear-gradient(135deg, var(--gradient-start), var(--gradient-mid)); | |
--border-radius-lg: 16px; | |
--border-radius-md: 12px; | |
--border-radius-sm: 8px; | |
--border-radius-pill: 34px; | |
} | |
body { font-family: "Faculty Glyphic", serif; margin: 0; padding: 0; background-color: var(--dark-bg); color: var(--text-secondary); line-height: 1.7; overflow-x: hidden; overflow-y: auto; position: relative; } | |
#fluid-canvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: -1; } | |
.container { background-color: var(--card-bg-transparent); padding: 25px 30px; border-radius: var(--border-radius-lg); box-shadow: 0 15px 35px rgba(0, 0, 0, 0.4); width: 100%; max-width: 1500px; box-sizing: border-box; position: relative; overflow: hidden; z-index: 1; margin: 40px auto; } | |
.container::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 6px; background: linear-gradient(to right, var(--gradient-start), var(--gradient-mid), var(--gradient-end)); border-radius: var(--border-radius-lg) var(--border-radius-lg) 0 0; opacity: 0.8; z-index: 2; } | |
.content-wrapper { position: relative; z-index: 1; } | |
h1, h2 { text-align: center; color: var(--text-primary); margin-bottom: 35px; font-family: "Faculty Glyphic", sans-serif; font-weight: 700; text-shadow: 0 2px 5px rgba(0, 0, 0, 0.3); } | |
h1 { font-size: 2.4rem; margin-bottom: 45px; } | |
h2 { font-size: 1.8rem; margin-top: 45px; } | |
h1 i { margin-right: 12px; background: linear-gradient(to right, var(--gradient-start), var(--gradient-mid), var(--gradient-end)); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent; text-shadow: none; font-size: 0.9em; vertical-align: middle; } | |
.main-card { background: linear-gradient(135deg, rgba(13, 110, 253, 0.1), rgba(32, 201, 151, 0.1), rgba(13, 202, 240, 0.1)); padding: 25px 35px; border-radius: var(--border-radius-md); margin-bottom: 45px; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15); position: relative; overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.08); text-align: center; } | |
.main-title { color: var(--text-primary); font-size: 1.6rem; margin-bottom: 10px; font-family: "Funnel Sans", sans-serif; font-weight: 700; } | |
.main-subtitle { color: var(--text-secondary); font-size: 1.1rem; margin-bottom: 0; font-family: "Faculty Glyphic", serif; } | |
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 20px; margin-bottom: 35px; } | |
.form-group { margin-bottom: 0; background-color: rgba(255, 255, 255, 0.04); padding: 18px 18px 12px 18px; border-radius: var(--border-radius-md); transition: background-color 0.3s ease, transform 0.2s ease, box-shadow 0.3s ease; border: 1px solid transparent; box-sizing: border-box; display: flex; flex-direction: column; justify-content: space-between; min-height: 170px; } | |
.form-group-span-full { grid-column: 1 / -1; min-height: auto; margin-bottom: 20px; } | |
.form-group-span-full:last-of-type { margin-bottom: 0; } | |
.form-group:has(input[type="number"]), .form-group:has(select) { justify-content: center; text-align: center; min-height: auto; } | |
.form-group:focus-within:not(.switch-group-box) { background-color: rgba(255, 255, 255, 0.07); border-color: rgba(var(--secondary-color-rgb), 0.3); box-shadow: 0 0 15px rgba(var(--secondary-color-rgb), 0.1); } | |
.form-group:has(input[type="number"]) label, .form-group:has(select) label { justify-content: center; margin-bottom: 10px; } | |
.form-group:has(input[type="file"]) label:not(.file-input-label) { justify-content: flex-start; } | |
.form-group label:not(.switch-text-label):not(.file-input-label) { display: flex; align-items: center; margin-bottom: 12px; font-weight: 700; color: var(--text-secondary); font-size: 0.95rem; flex-shrink: 0; font-family: "Faculty Glyphic", serif; } | |
.form-group label:not(.switch-text-label):not(.file-input-label) i { margin-right: 10px; color: var(--text-muted); width: 1.1em; text-align: center; transition: color 0.3s ease; font-size: 1em; } | |
.form-group:focus-within:not(.switch-group-box) label:not(.switch-text-label):not(.file-input-label) i { color: var(--secondary-color); } | |
.form-group input[type="text"], .form-group input[type="number"], .form-group select, .form-group textarea { width: 100%; padding: 12px 14px; border: 1px solid var(--input-border); border-radius: var(--border-radius-sm); box-sizing: border-box; font-size: 1rem; transition: border-color 0.3s ease, box-shadow 0.3s ease, background-color 0.3s ease; background-color: var(--input-bg); color: var(--text-primary); flex-grow: 0; margin-bottom: 10px; font-family: "Faculty Glyphic", serif; } | |
.form-group input[type="number"] { text-align: center; } | |
.form-group select { 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%23cccccc%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.5-12.8z%22%2F%3E%3C/svg%3E'); background-repeat: no-repeat; background-position: right 14px center; background-size: 11px auto; padding-right: 40px; } | |
.form-group:has(select) select { text-align: center; padding-left: 40px; } | |
.form-group input::placeholder, .form-group textarea::placeholder { color: var(--text-muted); opacity: 0.7; font-family: "Faculty Glyphic", serif; } | |
.form-group input[type="text"]:focus, .form-group input[type="number"]:focus, .form-group select:focus, .form-group textarea:focus { border-color: var(--input-focus-border); outline: none; box-shadow: 0 0 0 3px rgba(var(--secondary-color-rgb), 0.25); background-color: var(--input-focus-bg); } | |
.form-group input:focus, .form-group select:focus { box-shadow: 0 0 8px rgba(var(--secondary-color-rgb), 0.8); } | |
.form-group textarea { resize: vertical; flex-grow: 1; min-height: 70px; } | |
.form-group input[type="file"] { opacity: 0; position: absolute; z-index: -1; left: 0; top: 0; width: 1px; height: 1px; } | |
.file-input-label { display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 15px; border: 1px dashed var(--input-border); border-radius: var(--border-radius-sm); background-color: var(--input-bg); color: var(--text-muted); cursor: pointer; transition: all 0.3s ease; font-size: 0.9rem; margin-bottom: 10px; text-align: center; width: 100%; box-sizing: border-box; min-height: 75px; flex-grow: 1; font-family: "Faculty Glyphic", serif; } | |
.file-input-label i { margin-right: 0; margin-bottom: 8px; font-size: 1.2em; } | |
.file-input-label span { display: block; } | |
.file-input-label:hover { border-color: var(--secondary-color); color: var(--secondary-color); background-color: var(--input-focus-bg); } | |
.file-name-display { font-size: 0.85rem; color: var(--text-muted); margin-top: 0px; margin-bottom: 10px; display: block; min-height: 1.2em; word-break: break-all; text-align: center; font-family: "Faculty Glyphic", serif; } | |
.form-group small { font-size: 0.8rem; color: var(--text-muted); display: block; margin-top: auto; padding-top: 8px; font-family: 'Faculty Glyphic', serif; font-style: italic; opacity: 0.8; flex-shrink: 0; text-align: left; line-height: 1.3; } | |
.form-group:has(input[type="number"]) small, .form-group:has(select) small, .form-group:has(input[type="file"]) small { text-align: center; } | |
.form-group:has(.switch-wrapper) { background-color: rgba(255, 255, 255, 0.04); padding: 15px 20px; border-radius: var(--border-radius-md); border: 1px solid transparent; transition: background-color 0.3s ease, border-color 0.3s ease; min-height: auto; display: flex; flex-direction: column; gap: 10px; justify-content: center; } | |
.form-group:has(.switch-wrapper):focus-within { background-color: rgba(255, 255, 255, 0.07); border-color: rgba(var(--secondary-color-rgb), 0.3); } | |
.switch-wrapper { display: flex; align-items: center; justify-content: space-between; width: 100%; margin-bottom: 0; } | |
.switch-text-label { font-weight: 700; color: var(--text-secondary); font-size: 0.95rem; cursor: pointer; user-select: none; margin-right: 15px; font-family: "Faculty Glyphic", serif; } | |
.switch { position: relative; display: inline-block; width: 50px; height: 26px; flex-shrink: 0; } | |
.switch input { opacity: 0; width: 0; height: 0; } | |
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: var(--switch-bg-off); transition: .4s; border-radius: var(--border-radius-pill); } | |
.slider:before { position: absolute; content: ""; height: 18px; width: 18px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; } | |
.switch input:checked + .slider { background: var(--switch-bg-on); } | |
.switch input:focus + .slider { box-shadow: 0 0 0 3px rgba(var(--secondary-color-rgb), 0.25); } | |
.switch input:checked + .slider:before { transform: translateX(24px); } | |
.button { display: flex; align-items: center; justify-content: center; width: 100%; margin-top: 35px; background: linear-gradient(135deg, var(--gradient-start), var(--gradient-mid), var(--gradient-end)); color: white; padding: 16px 28px; border: none; border-radius: var(--border-radius-sm); cursor: pointer; font-size: 1.15rem; font-family: "Funnel Sans", sans-serif; font-weight: 700; transition: all 0.3s ease; box-shadow: 0 5px 15px rgba(13, 110, 253, 0.3); position: relative; overflow: hidden; text-transform: uppercase; letter-spacing: 0.5px; } | |
.button::after { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient(circle, rgba(255, 255, 255, 0.3) 0%, rgba(255, 255, 255, 0) 70%); transform: rotate(45deg); transition: opacity 0.5s ease; opacity: 0; } | |
.button:hover { transform: translateY(-5px) scale(1.05); box-shadow: 0 10px 25px rgba(13, 110, 253, 0.5); } | |
.button:hover::after { opacity: 1; } | |
.button:active { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(13, 110, 253, 0.3); } | |
.button i { margin-right: 12px; font-size: 1em; } | |
.button:disabled { background: #4b5563; cursor: not-allowed; transform: none; box-shadow: none; color: var(--text-muted); } | |
.button:disabled::after { display: none; } | |
.button:disabled i { color: var(--text-muted); } | |
.message { margin-top: 30px; padding: 20px 28px; border-radius: var(--border-radius-md); display: flex; align-items: center; animation: fadeIn 0.4s ease-out; border: 1px solid transparent; font-size: 1.05rem; font-family: "Faculty Glyphic", serif; position: relative; z-index: 2; } | |
@keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } } | |
.message i { margin-right: 18px; font-size: 1.3em; flex-shrink: 0; } | |
.error { background-color: var(--error-bg); color: var(--error-color); border-color: rgba(248, 113, 113, 0.3); } | |
.error i { color: var(--error-color); } | |
.success { background-color: var(--success-bg); color: var(--success-color); border-color: rgba(74, 222, 128, 0.3); } | |
.success i { color: var(--success-color); } | |
#loading-indicator { display: none; text-align: center; margin: 25px 0 0 0; font-weight: 500; color: var(--text-muted); font-size: 0.95rem; font-family: "Faculty Glyphic", serif; position: relative; z-index: 2; } | |
#loading-indicator .spinner { display: inline-block; vertical-align: middle; border: 3px solid rgba(255, 255, 255, 0.1); border-top-color: var(--secondary-color); border-radius: 50%; width: 20px; height: 20px; animation: spin 0.8s linear infinite; margin-right: 10px; } | |
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } | |
#progress-area { display: none; margin-top: 35px; padding: 28px 35px; background-color: rgba(0, 0, 0, 0.2); border-radius: var(--border-radius-md); border: 1px solid var(--input-border); animation: fadeIn 0.5s ease-out; text-align: center; position: relative; z-index: 2; } | |
#progress-area .progress-spinner { display: inline-block; vertical-align: middle; border: 4px solid rgba(255, 255, 255, 0.1); border-top-color: var(--gradient-start); border-right-color: var(--gradient-mid); border-bottom-color: var(--gradient-end); border-left-color: transparent; border-radius: 50%; width: 32px; height: 32px; animation: spin 0.8s linear infinite; margin-bottom: 18px; } | |
#progress-stage { display: block; font-weight: 700; color: var(--text-primary); font-size: 1.2rem; margin-bottom: 10px; font-family: "Funnel Sans", sans-serif; } | |
#progress-message { display: block; color: var(--text-secondary); font-size: 1rem; min-height: 1.2em; font-family: "Faculty Glyphic", serif; } | |
#video-modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; overflow: auto; background-color: rgba(0, 0, 0, 0.7); align-items: center; justify-content: center; padding: 20px; box-sizing: border-box; animation: fadeIn 0.3s ease-out; } | |
.modal-content { background-color: var(--card-bg); margin: auto; padding: 30px 35px; border-radius: var(--border-radius-lg); box-shadow: 0 10px 30px rgba(0, 0, 0, 0.4); width: 90%; max-width: 800px; position: relative; border-top: 6px solid; border-image: linear-gradient(to right, var(--gradient-start), var(--gradient-mid), var(--gradient-end)) 1; text-align: center; } | |
.modal-close { color: var(--text-muted); position: absolute; top: 15px; right: 25px; font-size: 28px; font-weight: bold; transition: color 0.3s ease; } | |
.modal-close:hover, .modal-close:focus { color: var(--text-primary); text-decoration: none; cursor: pointer; } | |
.modal-content h2 { font-size: 1.6rem; margin-top: 0; margin-bottom: 25px; color: var(--text-primary); font-family: "Funnel Sans", sans-serif; } | |
.modal-video-container { margin-bottom: 30px; background-color: rgba(0, 0, 0, 0.25); padding: 15px; border-radius: var(--border-radius-md); box-shadow: inset 0 1px 5px rgba(0, 0, 0, 0.2); border: 1px solid var(--input-border); } | |
.modal-video-container video { max-width: 100%; height: auto; display: block; border-radius: var(--border-radius-sm); background-color: #000; } | |
.modal-download-link { display: inline-flex; align-items: center; justify-content: center; margin-top: 15px; background: linear-gradient(135deg, var(--secondary-color), var(--primary-color)); color: white; padding: 14px 24px; text-decoration: none; border-radius: var(--border-radius-sm); font-weight: 700; transition: all 0.3s ease; box-shadow: 0 5px 15px rgba(var(--secondary-color-rgb), 0.3); position: relative; overflow: hidden; font-size: 1.05rem; text-transform: uppercase; letter-spacing: 0.5px; font-family: "Funnel Sans", sans-serif; } | |
.modal-download-link::after { content: ''; position: absolute; top: -50%; left: -50%; width: 200%; height: 200%; background: radial-gradient(circle, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0) 70%); transform: rotate(45deg); transition: opacity 0.5s ease; opacity: 0; } | |
.modal-download-link:hover { transform: translateY(-3px); box-shadow: 0 8px 20px rgba(var(--secondary-color-rgb), 0.4); } | |
.modal-download-link:hover::after { opacity: 1; } | |
.modal-download-link:active { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(var(--secondary-color-rgb), 0.3); } | |
.modal-download-link i { margin-right: 12px; font-size: 1em; } | |
#modal-hq-confirmation { display: none; margin-top: 30px; padding: 22px 28px; background-color: rgba(var(--secondary-color-rgb), 0.1); border: 1px solid rgba(var(--secondary-color-rgb), 0.3); border-radius: var(--border-radius-md); text-align: center; animation: fadeIn 0.4s ease-out; } | |
#modal-hq-confirmation p { margin: 0 0 18px 0; color: var(--text-secondary); font-size: 1rem; font-family: "Faculty Glyphic", serif; } | |
#modal-hq-confirmation .popup-buttons button { padding: 9px 20px; border-radius: var(--border-radius-sm); border: none; cursor: pointer; font-weight: 700; font-size: 0.95rem; transition: all 0.2s ease; margin: 0 10px; min-width: 90px; font-family: "Funnel Sans", sans-serif; } | |
#modal-hq-confirm-yes { background-color: var(--success-color); color: var(--dark-bg); } | |
#modal-hq-confirm-yes:hover { background-color: #6adf9a; transform: translateY(-1px); } | |
#modal-hq-confirm-no { background-color: var(--text-muted); color: var(--dark-bg); } | |
#modal-hq-confirm-no:hover { background-color: #b0b7c3; transform: translateY(-1px); } | |
</style> | |
</head> | |
<body> | |
<canvas id="fluid-canvas"></canvas> | |
<div class="container"> | |
<div class="content-wrapper"> | |
<h1><i class="fas fa-magic"></i>AI Video Editor</h1> | |
<div class="main-card"> | |
<div class="main-title">Craft Your Vision Instantly</div> | |
<div class="main-subtitle">Let AI handle the edits. Just upload your files and describe the style.</div> | |
</div> | |
<form id="video-form"> | |
<div class="form-grid"> | |
<div class="form-group"> | |
<label for="videos"><i class="fas fa-folder-open"></i>Source Videos</label> | |
<label for="videos" class="file-input-label"><i class="fas fa-upload"></i><span>Choose Files...</span></label> | |
<input type="file" id="videos" name="videos[]" accept="video/*" multiple required data-display-target="videos-display"> | |
<span class="file-name-display" id="videos-display">No files selected</span> | |
<small>Select one or more video clips.</small> | |
</div> | |
<div class="form-group"> | |
<label for="audios"><i class="fas fa-music"></i>Source Audios</label> | |
<label for="audios" class="file-input-label"><i class="fas fa-upload"></i><span>Choose Files...</span></label> | |
<input type="file" id="audios" name="audios[]" accept="audio/*" multiple data-display-target="audios-display"> | |
<span class="file-name-display" id="audios-display">No files selected</span> | |
<small>Optional background audio tracks.</small> | |
</div> | |
<div class="form-group"> | |
<label for="style_sample"><i class="fas fa-palette"></i>Style Sample</label> | |
<label for="style_sample" class="file-input-label"><i class="fas fa-upload"></i><span>Choose File...</span></label> | |
<input type="file" id="style_sample" name="style_sample" accept="video/*" data-display-target="style-sample-display"> | |
<span class="file-name-display" id="style-sample-display">No file selected</span> | |
<small>Optional video to mimic style.</small> | |
</div> | |
<div class="form-group"> | |
<label for="duration"><i class="fas fa-clock"></i>Target Duration (s)</label> | |
<input type="number" id="duration" name="duration" step="1" min="1" required placeholder="e.g., 30"> | |
<small>Desired final video length.</small> | |
</div> | |
<div class="form-group"> | |
<label for="variations"><i class="fas fa-random"></i>Variations</label> | |
<select id="variations" name="variations"> | |
<option value="1" selected>1 Plan</option> | |
<option value="2">2 Plans</option> | |
<option value="3">3 Plans</option> | |
<option value="4">4 Plans</option> | |
</select> | |
<small>Number of edit plans to generate.</small> | |
</div> | |
<div class="form-group"> | |
<div class="switch-wrapper"> | |
<label for="mute_audio" class="switch-text-label">Mute Audio</label> | |
<label class="switch"><input type="checkbox" id="mute_audio" name="mute_audio"><span class="slider"></span></label> | |
</div> | |
<div class="switch-wrapper"> | |
<label for="generate_preview" class="switch-text-label">Generate Preview</label> | |
<label class="switch"><input type="checkbox" id="generate_preview" name="generate_preview"><span class="slider"></span></label> | |
</div> | |
<small>Mute source audio / Generate low-res preview first.</small> | |
</div> | |
<div class="form-group"> | |
<label for="model_name"><i class="fas fa-brain"></i>AI Model</label> | |
<select id="model_name" name="model_name"> | |
<option value="gemini-2.5-flash" {% if default_model == 'gemini-2.5-flash' %}selected{% endif %}>gemini-2.5-flash</option> | |
<option value="gemini-2.5-pro" {% if default_model == 'gemini-2.5-pro' %}selected{% endif %}>gemini-2.5-pro</option> | |
</select> | |
<small>Choose the AI editing model.</small> | |
</div> | |
<div class="form-group form-group-span-full"> | |
<label for="style_desc"><i class="fas fa-pen-alt"></i>Style Description</label> | |
<textarea id="style_desc" name="style_desc" rows="2" required placeholder="Describe desired style, pacing, mood, effects, etc. Defaults to Instagram Reel style if left blank.">{{ default_style_desc | escape }}</textarea> | |
<small>Describe the desired look and feel (e.g., "fast-paced, energetic, like a travel vlog").</small> | |
</div> | |
<div class="form-group form-group-span-full"> | |
<label for="output"><i class="fas fa-file-video"></i>Output File Name</label> | |
<input type="text" id="output" name="output" value="ai_edited_video.mp4" required> | |
<small>Name for the final generated video file.</small> | |
</div> | |
</div> | |
<button type="submit" id="submit-button" class="button"><i class="fas fa-cogs"></i>Generate Video</button> | |
</form> | |
<div id="loading-indicator" style="display: none;"><div class="spinner"></div><span>Submitting request...</span></div> | |
<div id="progress-area" style="display: none;"><div class="progress-spinner"></div><span id="progress-stage">Starting...</span><span id="progress-message">Please wait while we process your request.</span></div> | |
<div id="message-area"></div> | |
</div> | |
</div> | |
<div id="video-modal"> | |
<div class="modal-content"> | |
<span class="modal-close" id="modal-close-button">×</span> | |
<h2 id="modal-title">Your Generated Video</h2> | |
<div class="modal-video-container"> | |
<video id="modal-video" controls controlsList="nodownload">Your browser does not support the video tag.</video> | |
</div> | |
<a id="modal-download-link" href="#" class="modal-download-link" download><i class="fas fa-download"></i>Download Video</a> | |
<div id="modal-hq-confirmation" style="display: none;"> | |
<p id="modal-hq-popup-message">Preview generated! Generate the high-quality version now?</p> | |
<div class="popup-buttons"> | |
<button id="modal-hq-confirm-yes">Yes, Generate HQ (Plan 1)</button> | |
<button id="modal-hq-confirm-no">No, Thanks</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script>/* | |
MIT License | |
Copyright (c) 2017 Pavel Dobryakov | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. | |
*/ | |
; | |
// Simulation section | |
const canvas = document.getElementsByTagName('canvas')[0]; | |
resizeCanvas(); | |
let config = { | |
SIM_RESOLUTION: 64, | |
DYE_RESOLUTION: 256, | |
CAPTURE_RESOLUTION: 32, | |
DENSITY_DISSIPATION: 1, | |
VELOCITY_DISSIPATION: 0.2, | |
PRESSURE: 0.8, | |
PRESSURE_ITERATIONS: 20, | |
CURL: 30, | |
SPLAT_RADIUS: 0.25, | |
SPLAT_FORCE: 6000, | |
SHADING: true, | |
COLORFUL: true, | |
COLOR_UPDATE_SPEED: 10, | |
PAUSED: false, | |
BACK_COLOR: { r: 0, g: 0, b: 0 }, | |
TRANSPARENT: false, | |
BLOOM: false, | |
BLOOM_ITERATIONS: 8, | |
BLOOM_RESOLUTION: 256, | |
BLOOM_INTENSITY: 0.8, | |
BLOOM_THRESHOLD: 0.6, | |
BLOOM_SOFT_KNEE: 0.7, | |
SUNRAYS: false, | |
SUNRAYS_RESOLUTION: 196, | |
SUNRAYS_WEIGHT: 1.0, | |
} | |
function pointerPrototype () { | |
this.id = -1; | |
this.texcoordX = 0; | |
this.texcoordY = 0; | |
this.prevTexcoordX = 0; | |
this.prevTexcoordY = 0; | |
this.deltaX = 0; | |
this.deltaY = 0; | |
this.down = false; | |
this.moved = false; | |
this.color = [30, 0, 300]; | |
} | |
let pointers = []; | |
let splatStack = []; | |
pointers.push(new pointerPrototype()); | |
const { gl, ext } = getWebGLContext(canvas); | |
if (isMobile()) { | |
config.DYE_RESOLUTION = 512; | |
} | |
if (!ext.supportLinearFiltering) { | |
config.DYE_RESOLUTION = 512; | |
config.SHADING = false; | |
config.BLOOM = false; | |
config.SUNRAYS = false; | |
} | |
startGUI(); | |
function getWebGLContext (canvas) { | |
const params = { alpha: true, depth: false, stencil: false, antialias: false, preserveDrawingBuffer: false }; | |
let gl = canvas.getContext('webgl2', params); | |
const isWebGL2 = !!gl; | |
if (!isWebGL2) | |
gl = canvas.getContext('webgl', params) || canvas.getContext('experimental-webgl', params); | |
let halfFloat; | |
let supportLinearFiltering; | |
if (isWebGL2) { | |
gl.getExtension('EXT_color_buffer_float'); | |
supportLinearFiltering = gl.getExtension('OES_texture_float_linear'); | |
} else { | |
halfFloat = gl.getExtension('OES_texture_half_float'); | |
supportLinearFiltering = gl.getExtension('OES_texture_half_float_linear'); | |
} | |
gl.clearColor(0.0, 0.0, 0.0, 1.0); | |
const halfFloatTexType = isWebGL2 ? gl.HALF_FLOAT : halfFloat.HALF_FLOAT_OES; | |
let formatRGBA; | |
let formatRG; | |
let formatR; | |
if (isWebGL2) | |
{ | |
formatRGBA = getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, halfFloatTexType); | |
formatRG = getSupportedFormat(gl, gl.RG16F, gl.RG, halfFloatTexType); | |
formatR = getSupportedFormat(gl, gl.R16F, gl.RED, halfFloatTexType); | |
} | |
else | |
{ | |
formatRGBA = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); | |
formatRG = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); | |
formatR = getSupportedFormat(gl, gl.RGBA, gl.RGBA, halfFloatTexType); | |
} | |
return { | |
gl, | |
ext: { | |
formatRGBA, | |
formatRG, | |
formatR, | |
halfFloatTexType, | |
supportLinearFiltering | |
} | |
}; | |
} | |
function getSupportedFormat (gl, internalFormat, format, type) | |
{ | |
if (!supportRenderTextureFormat(gl, internalFormat, format, type)) | |
{ | |
switch (internalFormat) | |
{ | |
case gl.R16F: | |
return getSupportedFormat(gl, gl.RG16F, gl.RG, type); | |
case gl.RG16F: | |
return getSupportedFormat(gl, gl.RGBA16F, gl.RGBA, type); | |
default: | |
return null; | |
} | |
} | |
return { | |
internalFormat, | |
format | |
} | |
} | |
function supportRenderTextureFormat (gl, internalFormat, format, type) { | |
let texture = gl.createTexture(); | |
gl.bindTexture(gl.TEXTURE_2D, texture); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, 4, 4, 0, format, type, null); | |
let fbo = gl.createFramebuffer(); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); | |
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); | |
let status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); | |
return status == gl.FRAMEBUFFER_COMPLETE; | |
} | |
function startGUI () { | |
} | |
function isMobile () { | |
return /Mobi|Android/i.test(navigator.userAgent); | |
} | |
function captureScreenshot () { | |
let res = getResolution(config.CAPTURE_RESOLUTION); | |
let target = createFBO(res.width, res.height, ext.formatRGBA.internalFormat, ext.formatRGBA.format, ext.halfFloatTexType, gl.NEAREST); | |
render(target); | |
let texture = framebufferToTexture(target); | |
texture = normalizeTexture(texture, target.width, target.height); | |
let captureCanvas = textureToCanvas(texture, target.width, target.height); | |
let datauri = captureCanvas.toDataURL(); | |
downloadURI('fluid.png', datauri); | |
URL.revokeObjectURL(datauri); | |
} | |
function framebufferToTexture (target) { | |
gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo); | |
let length = target.width * target.height * 4; | |
let texture = new Float32Array(length); | |
gl.readPixels(0, 0, target.width, target.height, gl.RGBA, gl.FLOAT, texture); | |
return texture; | |
} | |
function normalizeTexture (texture, width, height) { | |
let result = new Uint8Array(texture.length); | |
let id = 0; | |
for (let i = height - 1; i >= 0; i--) { | |
for (let j = 0; j < width; j++) { | |
let nid = i * width * 4 + j * 4; | |
result[nid + 0] = clamp01(texture[id + 0]) * 255; | |
result[nid + 1] = clamp01(texture[id + 1]) * 255; | |
result[nid + 2] = clamp01(texture[id + 2]) * 255; | |
result[nid + 3] = clamp01(texture[id + 3]) * 255; | |
id += 4; | |
} | |
} | |
return result; | |
} | |
function clamp01 (input) { | |
return Math.min(Math.max(input, 0), 1); | |
} | |
function textureToCanvas (texture, width, height) { | |
let captureCanvas = document.createElement('canvas'); | |
let ctx = captureCanvas.getContext('2d'); | |
captureCanvas.width = width; | |
captureCanvas.height = height; | |
let imageData = ctx.createImageData(width, height); | |
imageData.data.set(texture); | |
ctx.putImageData(imageData, 0, 0); | |
return captureCanvas; | |
} | |
function downloadURI (filename, uri) { | |
let link = document.createElement('a'); | |
link.download = filename; | |
link.href = uri; | |
document.body.appendChild(link); | |
link.click(); | |
document.body.removeChild(link); | |
} | |
class Material { | |
constructor (vertexShader, fragmentShaderSource) { | |
this.vertexShader = vertexShader; | |
this.fragmentShaderSource = fragmentShaderSource; | |
this.programs = []; | |
this.activeProgram = null; | |
this.uniforms = []; | |
} | |
setKeywords (keywords) { | |
let hash = 0; | |
for (let i = 0; i < keywords.length; i++) | |
hash += hashCode(keywords[i]); | |
let program = this.programs[hash]; | |
if (program == null) | |
{ | |
let fragmentShader = compileShader(gl.FRAGMENT_SHADER, this.fragmentShaderSource, keywords); | |
program = createProgram(this.vertexShader, fragmentShader); | |
this.programs[hash] = program; | |
} | |
if (program == this.activeProgram) return; | |
this.uniforms = getUniforms(program); | |
this.activeProgram = program; | |
} | |
bind () { | |
gl.useProgram(this.activeProgram); | |
} | |
} | |
class Program { | |
constructor (vertexShader, fragmentShader) { | |
this.uniforms = {}; | |
this.program = createProgram(vertexShader, fragmentShader); | |
this.uniforms = getUniforms(this.program); | |
} | |
bind () { | |
gl.useProgram(this.program); | |
} | |
} | |
function createProgram (vertexShader, fragmentShader) { | |
let program = gl.createProgram(); | |
gl.attachShader(program, vertexShader); | |
gl.attachShader(program, fragmentShader); | |
gl.linkProgram(program); | |
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) | |
console.trace(gl.getProgramInfoLog(program)); | |
return program; | |
} | |
function getUniforms (program) { | |
let uniforms = []; | |
let uniformCount = gl.getProgramParameter(program, gl.ACTIVE_UNIFORMS); | |
for (let i = 0; i < uniformCount; i++) { | |
let uniformName = gl.getActiveUniform(program, i).name; | |
uniforms[uniformName] = gl.getUniformLocation(program, uniformName); | |
} | |
return uniforms; | |
} | |
function compileShader (type, source, keywords) { | |
source = addKeywords(source, keywords); | |
const shader = gl.createShader(type); | |
gl.shaderSource(shader, source); | |
gl.compileShader(shader); | |
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) | |
console.trace(gl.getShaderInfoLog(shader)); | |
return shader; | |
}; | |
function addKeywords (source, keywords) { | |
if (keywords == null) return source; | |
let keywordsString = ''; | |
keywords.forEach(keyword => { | |
keywordsString += '#define ' + keyword + '\n'; | |
}); | |
return keywordsString + source; | |
} | |
const baseVertexShader = compileShader(gl.VERTEX_SHADER, ` | |
precision highp float; | |
attribute vec2 aPosition; | |
varying vec2 vUv; | |
varying vec2 vL; | |
varying vec2 vR; | |
varying vec2 vT; | |
varying vec2 vB; | |
uniform vec2 texelSize; | |
void main () { | |
vUv = aPosition * 0.5 + 0.5; | |
vL = vUv - vec2(texelSize.x, 0.0); | |
vR = vUv + vec2(texelSize.x, 0.0); | |
vT = vUv + vec2(0.0, texelSize.y); | |
vB = vUv - vec2(0.0, texelSize.y); | |
gl_Position = vec4(aPosition, 0.0, 1.0); | |
} | |
`); | |
const blurVertexShader = compileShader(gl.VERTEX_SHADER, ` | |
precision highp float; | |
attribute vec2 aPosition; | |
varying vec2 vUv; | |
varying vec2 vL; | |
varying vec2 vR; | |
uniform vec2 texelSize; | |
void main () { | |
vUv = aPosition * 0.5 + 0.5; | |
float offset = 1.33333333; | |
vL = vUv - texelSize * offset; | |
vR = vUv + texelSize * offset; | |
gl_Position = vec4(aPosition, 0.0, 1.0); | |
} | |
`); | |
const blurShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision mediump float; | |
precision mediump sampler2D; | |
varying vec2 vUv; | |
varying vec2 vL; | |
varying vec2 vR; | |
uniform sampler2D uTexture; | |
void main () { | |
vec4 sum = texture2D(uTexture, vUv) * 0.29411764; | |
sum += texture2D(uTexture, vL) * 0.35294117; | |
sum += texture2D(uTexture, vR) * 0.35294117; | |
gl_FragColor = sum; | |
} | |
`); | |
const copyShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision mediump float; | |
precision mediump sampler2D; | |
varying highp vec2 vUv; | |
uniform sampler2D uTexture; | |
void main () { | |
gl_FragColor = texture2D(uTexture, vUv); | |
} | |
`); | |
const clearShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision mediump float; | |
precision mediump sampler2D; | |
varying highp vec2 vUv; | |
uniform sampler2D uTexture; | |
uniform float value; | |
void main () { | |
gl_FragColor = value * texture2D(uTexture, vUv); | |
} | |
`); | |
const colorShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision mediump float; | |
uniform vec4 color; | |
void main () { | |
gl_FragColor = color; | |
} | |
`); | |
const checkerboardShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision highp float; | |
precision highp sampler2D; | |
varying vec2 vUv; | |
uniform sampler2D uTexture; | |
uniform float aspectRatio; | |
#define SCALE 25.0 | |
void main () { | |
vec2 uv = floor(vUv * SCALE * vec2(aspectRatio, 1.0)); | |
float v = mod(uv.x + uv.y, 2.0); | |
v = v * 0.1 + 0.8; | |
gl_FragColor = vec4(vec3(v), 1.0); | |
} | |
`); | |
const displayShaderSource = ` | |
precision highp float; | |
precision highp sampler2D; | |
varying vec2 vUv; | |
varying vec2 vL; | |
varying vec2 vR; | |
varying vec2 vT; | |
varying vec2 vB; | |
uniform sampler2D uTexture; | |
uniform sampler2D uBloom; | |
uniform sampler2D uSunrays; | |
uniform sampler2D uDithering; | |
uniform vec2 ditherScale; | |
uniform vec2 texelSize; | |
vec3 linearToGamma (vec3 color) { | |
color = max(color, vec3(0)); | |
return max(1.055 * pow(color, vec3(0.416666667)) - 0.055, vec3(0)); | |
} | |
void main () { | |
vec3 c = texture2D(uTexture, vUv).rgb; | |
#ifdef SHADING | |
vec3 lc = texture2D(uTexture, vL).rgb; | |
vec3 rc = texture2D(uTexture, vR).rgb; | |
vec3 tc = texture2D(uTexture, vT).rgb; | |
vec3 bc = texture2D(uTexture, vB).rgb; | |
float dx = length(rc) - length(lc); | |
float dy = length(tc) - length(bc); | |
vec3 n = normalize(vec3(dx, dy, length(texelSize))); | |
vec3 l = vec3(0.0, 0.0, 1.0); | |
float diffuse = clamp(dot(n, l) + 0.7, 0.7, 1.0); | |
c *= diffuse; | |
#endif | |
#ifdef BLOOM | |
vec3 bloom = texture2D(uBloom, vUv).rgb; | |
#endif | |
#ifdef SUNRAYS | |
float sunrays = texture2D(uSunrays, vUv).r; | |
c *= sunrays; | |
#ifdef BLOOM | |
bloom *= sunrays; | |
#endif | |
#endif | |
#ifdef BLOOM | |
float noise = texture2D(uDithering, vUv * ditherScale).r; | |
noise = noise * 2.0 - 1.0; | |
bloom += noise / 255.0; | |
bloom = linearToGamma(bloom); | |
c += bloom; | |
#endif | |
float a = max(c.r, max(c.g, c.b)); | |
gl_FragColor = vec4(c, a); | |
} | |
`; | |
const bloomPrefilterShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision mediump float; | |
precision mediump sampler2D; | |
varying vec2 vUv; | |
uniform sampler2D uTexture; | |
uniform vec3 curve; | |
uniform float threshold; | |
void main () { | |
vec3 c = texture2D(uTexture, vUv).rgb; | |
float br = max(c.r, max(c.g, c.b)); | |
float rq = clamp(br - curve.x, 0.0, curve.y); | |
rq = curve.z * rq * rq; | |
c *= max(rq, br - threshold) / max(br, 0.0001); | |
gl_FragColor = vec4(c, 0.0); | |
} | |
`); | |
const bloomBlurShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision mediump float; | |
precision mediump sampler2D; | |
varying vec2 vL; | |
varying vec2 vR; | |
varying vec2 vT; | |
varying vec2 vB; | |
uniform sampler2D uTexture; | |
void main () { | |
vec4 sum = vec4(0.0); | |
sum += texture2D(uTexture, vL); | |
sum += texture2D(uTexture, vR); | |
sum += texture2D(uTexture, vT); | |
sum += texture2D(uTexture, vB); | |
sum *= 0.25; | |
gl_FragColor = sum; | |
} | |
`); | |
const bloomFinalShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision mediump float; | |
precision mediump sampler2D; | |
varying vec2 vL; | |
varying vec2 vR; | |
varying vec2 vT; | |
varying vec2 vB; | |
uniform sampler2D uTexture; | |
uniform float intensity; | |
void main () { | |
vec4 sum = vec4(0.0); | |
sum += texture2D(uTexture, vL); | |
sum += texture2D(uTexture, vR); | |
sum += texture2D(uTexture, vT); | |
sum += texture2D(uTexture, vB); | |
sum *= 0.25; | |
gl_FragColor = sum * intensity; | |
} | |
`); | |
const sunraysMaskShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision highp float; | |
precision highp sampler2D; | |
varying vec2 vUv; | |
uniform sampler2D uTexture; | |
void main () { | |
vec4 c = texture2D(uTexture, vUv); | |
float br = max(c.r, max(c.g, c.b)); | |
c.a = 1.0 - min(max(br * 20.0, 0.0), 0.8); | |
gl_FragColor = c; | |
} | |
`); | |
const sunraysShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision highp float; | |
precision highp sampler2D; | |
varying vec2 vUv; | |
uniform sampler2D uTexture; | |
uniform float weight; | |
#define ITERATIONS 16 | |
void main () { | |
float Density = 0.3; | |
float Decay = 0.95; | |
float Exposure = 0.7; | |
vec2 coord = vUv; | |
vec2 dir = vUv - 0.5; | |
dir *= 1.0 / float(ITERATIONS) * Density; | |
float illuminationDecay = 1.0; | |
float color = texture2D(uTexture, vUv).a; | |
for (int i = 0; i < ITERATIONS; i++) | |
{ | |
coord -= dir; | |
float col = texture2D(uTexture, coord).a; | |
color += col * illuminationDecay * weight; | |
illuminationDecay *= Decay; | |
} | |
gl_FragColor = vec4(color * Exposure, 0.0, 0.0, 1.0); | |
} | |
`); | |
const splatShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision highp float; | |
precision highp sampler2D; | |
varying vec2 vUv; | |
uniform sampler2D uTarget; | |
uniform float aspectRatio; | |
uniform vec3 color; | |
uniform vec2 point; | |
uniform float radius; | |
void main () { | |
vec2 p = vUv - point.xy; | |
p.x *= aspectRatio; | |
vec3 splat = exp(-dot(p, p) / radius) * color; | |
vec3 base = texture2D(uTarget, vUv).xyz; | |
gl_FragColor = vec4(base + splat, 1.0); | |
} | |
`); | |
const advectionShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision highp float; | |
precision highp sampler2D; | |
varying vec2 vUv; | |
uniform sampler2D uVelocity; | |
uniform sampler2D uSource; | |
uniform vec2 texelSize; | |
uniform vec2 dyeTexelSize; | |
uniform float dt; | |
uniform float dissipation; | |
vec4 bilerp (sampler2D sam, vec2 uv, vec2 tsize) { | |
vec2 st = uv / tsize - 0.5; | |
vec2 iuv = floor(st); | |
vec2 fuv = fract(st); | |
vec4 a = texture2D(sam, (iuv + vec2(0.5, 0.5)) * tsize); | |
vec4 b = texture2D(sam, (iuv + vec2(1.5, 0.5)) * tsize); | |
vec4 c = texture2D(sam, (iuv + vec2(0.5, 1.5)) * tsize); | |
vec4 d = texture2D(sam, (iuv + vec2(1.5, 1.5)) * tsize); | |
return mix(mix(a, b, fuv.x), mix(c, d, fuv.x), fuv.y); | |
} | |
void main () { | |
#ifdef MANUAL_FILTERING | |
vec2 coord = vUv - dt * bilerp(uVelocity, vUv, texelSize).xy * texelSize; | |
vec4 result = bilerp(uSource, coord, dyeTexelSize); | |
#else | |
vec2 coord = vUv - dt * texture2D(uVelocity, vUv).xy * texelSize; | |
vec4 result = texture2D(uSource, coord); | |
#endif | |
float decay = 1.0 + dissipation * dt; | |
gl_FragColor = result / decay; | |
}`, | |
ext.supportLinearFiltering ? null : ['MANUAL_FILTERING'] | |
); | |
const divergenceShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision mediump float; | |
precision mediump sampler2D; | |
varying highp vec2 vUv; | |
varying highp vec2 vL; | |
varying highp vec2 vR; | |
varying highp vec2 vT; | |
varying highp vec2 vB; | |
uniform sampler2D uVelocity; | |
void main () { | |
float L = texture2D(uVelocity, vL).x; | |
float R = texture2D(uVelocity, vR).x; | |
float T = texture2D(uVelocity, vT).y; | |
float B = texture2D(uVelocity, vB).y; | |
vec2 C = texture2D(uVelocity, vUv).xy; | |
if (vL.x < 0.0) { L = -C.x; } | |
if (vR.x > 1.0) { R = -C.x; } | |
if (vT.y > 1.0) { T = -C.y; } | |
if (vB.y < 0.0) { B = -C.y; } | |
float div = 0.5 * (R - L + T - B); | |
gl_FragColor = vec4(div, 0.0, 0.0, 1.0); | |
} | |
`); | |
const curlShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision mediump float; | |
precision mediump sampler2D; | |
varying highp vec2 vUv; | |
varying highp vec2 vL; | |
varying highp vec2 vR; | |
varying highp vec2 vT; | |
varying highp vec2 vB; | |
uniform sampler2D uVelocity; | |
void main () { | |
float L = texture2D(uVelocity, vL).y; | |
float R = texture2D(uVelocity, vR).y; | |
float T = texture2D(uVelocity, vT).x; | |
float B = texture2D(uVelocity, vB).x; | |
float vorticity = R - L - T + B; | |
gl_FragColor = vec4(0.5 * vorticity, 0.0, 0.0, 1.0); | |
} | |
`); | |
const vorticityShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision highp float; | |
precision highp sampler2D; | |
varying vec2 vUv; | |
varying vec2 vL; | |
varying vec2 vR; | |
varying vec2 vT; | |
varying vec2 vB; | |
uniform sampler2D uVelocity; | |
uniform sampler2D uCurl; | |
uniform float curl; | |
uniform float dt; | |
void main () { | |
float L = texture2D(uCurl, vL).x; | |
float R = texture2D(uCurl, vR).x; | |
float T = texture2D(uCurl, vT).x; | |
float B = texture2D(uCurl, vB).x; | |
float C = texture2D(uCurl, vUv).x; | |
vec2 force = 0.5 * vec2(abs(T) - abs(B), abs(R) - abs(L)); | |
force /= length(force) + 0.0001; | |
force *= curl * C; | |
force.y *= -1.0; | |
vec2 velocity = texture2D(uVelocity, vUv).xy; | |
velocity += force * dt; | |
velocity = min(max(velocity, -1000.0), 1000.0); | |
gl_FragColor = vec4(velocity, 0.0, 1.0); | |
} | |
`); | |
const pressureShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision mediump float; | |
precision mediump sampler2D; | |
varying highp vec2 vUv; | |
varying highp vec2 vL; | |
varying highp vec2 vR; | |
varying highp vec2 vT; | |
varying highp vec2 vB; | |
uniform sampler2D uPressure; | |
uniform sampler2D uDivergence; | |
void main () { | |
float L = texture2D(uPressure, vL).x; | |
float R = texture2D(uPressure, vR).x; | |
float T = texture2D(uPressure, vT).x; | |
float B = texture2D(uPressure, vB).x; | |
float C = texture2D(uPressure, vUv).x; | |
float divergence = texture2D(uDivergence, vUv).x; | |
float pressure = (L + R + B + T - divergence) * 0.25; | |
gl_FragColor = vec4(pressure, 0.0, 0.0, 1.0); | |
} | |
`); | |
const gradientSubtractShader = compileShader(gl.FRAGMENT_SHADER, ` | |
precision mediump float; | |
precision mediump sampler2D; | |
varying highp vec2 vUv; | |
varying highp vec2 vL; | |
varying highp vec2 vR; | |
varying highp vec2 vT; | |
varying highp vec2 vB; | |
uniform sampler2D uPressure; | |
uniform sampler2D uVelocity; | |
void main () { | |
float L = texture2D(uPressure, vL).x; | |
float R = texture2D(uPressure, vR).x; | |
float T = texture2D(uPressure, vT).x; | |
float B = texture2D(uPressure, vB).x; | |
vec2 velocity = texture2D(uVelocity, vUv).xy; | |
velocity.xy -= vec2(R - L, T - B); | |
gl_FragColor = vec4(velocity, 0.0, 1.0); | |
} | |
`); | |
const blit = (() => { | |
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer()); | |
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, -1, 1, 1, 1, 1, -1]), gl.STATIC_DRAW); | |
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer()); | |
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array([0, 1, 2, 0, 2, 3]), gl.STATIC_DRAW); | |
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); | |
gl.enableVertexAttribArray(0); | |
return (target, clear = false) => { | |
if (target == null) | |
{ | |
gl.viewport(0, 0, gl.drawingBufferWidth, gl.drawingBufferHeight); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, null); | |
} | |
else | |
{ | |
gl.viewport(0, 0, target.width, target.height); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, target.fbo); | |
} | |
if (clear) | |
{ | |
gl.clearColor(0.0, 0.0, 0.0, 1.0); | |
gl.clear(gl.COLOR_BUFFER_BIT); | |
} | |
// CHECK_FRAMEBUFFER_STATUS(); | |
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0); | |
} | |
})(); | |
function CHECK_FRAMEBUFFER_STATUS () { | |
let status = gl.checkFramebufferStatus(gl.FRAMEBUFFER); | |
if (status != gl.FRAMEBUFFER_COMPLETE) | |
console.trace("Framebuffer error: " + status); | |
} | |
let dye; | |
let velocity; | |
let divergence; | |
let curl; | |
let pressure; | |
let bloom; | |
let bloomFramebuffers = []; | |
let sunrays; | |
let sunraysTemp; | |
let ditheringTexture = createTextureAsync('LDR_LLL1_0.png'); | |
const blurProgram = new Program(blurVertexShader, blurShader); | |
const copyProgram = new Program(baseVertexShader, copyShader); | |
const clearProgram = new Program(baseVertexShader, clearShader); | |
const colorProgram = new Program(baseVertexShader, colorShader); | |
const checkerboardProgram = new Program(baseVertexShader, checkerboardShader); | |
const bloomPrefilterProgram = new Program(baseVertexShader, bloomPrefilterShader); | |
const bloomBlurProgram = new Program(baseVertexShader, bloomBlurShader); | |
const bloomFinalProgram = new Program(baseVertexShader, bloomFinalShader); | |
const sunraysMaskProgram = new Program(baseVertexShader, sunraysMaskShader); | |
const sunraysProgram = new Program(baseVertexShader, sunraysShader); | |
const splatProgram = new Program(baseVertexShader, splatShader); | |
const advectionProgram = new Program(baseVertexShader, advectionShader); | |
const divergenceProgram = new Program(baseVertexShader, divergenceShader); | |
const curlProgram = new Program(baseVertexShader, curlShader); | |
const vorticityProgram = new Program(baseVertexShader, vorticityShader); | |
const pressureProgram = new Program(baseVertexShader, pressureShader); | |
const gradienSubtractProgram = new Program(baseVertexShader, gradientSubtractShader); | |
const displayMaterial = new Material(baseVertexShader, displayShaderSource); | |
function initFramebuffers () { | |
let simRes = getResolution(config.SIM_RESOLUTION); | |
let dyeRes = getResolution(config.DYE_RESOLUTION); | |
const texType = ext.halfFloatTexType; | |
const rgba = ext.formatRGBA; | |
const rg = ext.formatRG; | |
const r = ext.formatR; | |
const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; | |
gl.disable(gl.BLEND); | |
if (dye == null) | |
dye = createDoubleFBO(dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering); | |
else | |
dye = resizeDoubleFBO(dye, dyeRes.width, dyeRes.height, rgba.internalFormat, rgba.format, texType, filtering); | |
if (velocity == null) | |
velocity = createDoubleFBO(simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering); | |
else | |
velocity = resizeDoubleFBO(velocity, simRes.width, simRes.height, rg.internalFormat, rg.format, texType, filtering); | |
divergence = createFBO (simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); | |
curl = createFBO (simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); | |
pressure = createDoubleFBO(simRes.width, simRes.height, r.internalFormat, r.format, texType, gl.NEAREST); | |
initBloomFramebuffers(); | |
initSunraysFramebuffers(); | |
} | |
function initBloomFramebuffers () { | |
let res = getResolution(config.BLOOM_RESOLUTION); | |
const texType = ext.halfFloatTexType; | |
const rgba = ext.formatRGBA; | |
const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; | |
bloom = createFBO(res.width, res.height, rgba.internalFormat, rgba.format, texType, filtering); | |
bloomFramebuffers.length = 0; | |
for (let i = 0; i < config.BLOOM_ITERATIONS; i++) | |
{ | |
let width = res.width >> (i + 1); | |
let height = res.height >> (i + 1); | |
if (width < 2 || height < 2) break; | |
let fbo = createFBO(width, height, rgba.internalFormat, rgba.format, texType, filtering); | |
bloomFramebuffers.push(fbo); | |
} | |
} | |
function initSunraysFramebuffers () { | |
let res = getResolution(config.SUNRAYS_RESOLUTION); | |
const texType = ext.halfFloatTexType; | |
const r = ext.formatR; | |
const filtering = ext.supportLinearFiltering ? gl.LINEAR : gl.NEAREST; | |
sunrays = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering); | |
sunraysTemp = createFBO(res.width, res.height, r.internalFormat, r.format, texType, filtering); | |
} | |
function createFBO (w, h, internalFormat, format, type, param) { | |
gl.activeTexture(gl.TEXTURE0); | |
let texture = gl.createTexture(); | |
gl.bindTexture(gl.TEXTURE_2D, texture); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, param); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, param); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); | |
gl.texImage2D(gl.TEXTURE_2D, 0, internalFormat, w, h, 0, format, type, null); | |
let fbo = gl.createFramebuffer(); | |
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); | |
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0); | |
gl.viewport(0, 0, w, h); | |
gl.clear(gl.COLOR_BUFFER_BIT); | |
let texelSizeX = 1.0 / w; | |
let texelSizeY = 1.0 / h; | |
return { | |
texture, | |
fbo, | |
width: w, | |
height: h, | |
texelSizeX, | |
texelSizeY, | |
attach (id) { | |
gl.activeTexture(gl.TEXTURE0 + id); | |
gl.bindTexture(gl.TEXTURE_2D, texture); | |
return id; | |
} | |
}; | |
} | |
function createDoubleFBO (w, h, internalFormat, format, type, param) { | |
let fbo1 = createFBO(w, h, internalFormat, format, type, param); | |
let fbo2 = createFBO(w, h, internalFormat, format, type, param); | |
return { | |
width: w, | |
height: h, | |
texelSizeX: fbo1.texelSizeX, | |
texelSizeY: fbo1.texelSizeY, | |
get read () { | |
return fbo1; | |
}, | |
set read (value) { | |
fbo1 = value; | |
}, | |
get write () { | |
return fbo2; | |
}, | |
set write (value) { | |
fbo2 = value; | |
}, | |
swap () { | |
let temp = fbo1; | |
fbo1 = fbo2; | |
fbo2 = temp; | |
} | |
} | |
} | |
function resizeFBO (target, w, h, internalFormat, format, type, param) { | |
let newFBO = createFBO(w, h, internalFormat, format, type, param); | |
copyProgram.bind(); | |
gl.uniform1i(copyProgram.uniforms.uTexture, target.attach(0)); | |
blit(newFBO); | |
return newFBO; | |
} | |
function resizeDoubleFBO (target, w, h, internalFormat, format, type, param) { | |
if (target.width == w && target.height == h) | |
return target; | |
target.read = resizeFBO(target.read, w, h, internalFormat, format, type, param); | |
target.write = createFBO(w, h, internalFormat, format, type, param); | |
target.width = w; | |
target.height = h; | |
target.texelSizeX = 1.0 / w; | |
target.texelSizeY = 1.0 / h; | |
return target; | |
} | |
function createTextureAsync (url) { | |
let texture = gl.createTexture(); | |
gl.bindTexture(gl.TEXTURE_2D, texture); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT); | |
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT); | |
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, 1, 1, 0, gl.RGB, gl.UNSIGNED_BYTE, new Uint8Array([255, 255, 255])); | |
let obj = { | |
texture, | |
width: 1, | |
height: 1, | |
attach (id) { | |
gl.activeTexture(gl.TEXTURE0 + id); | |
gl.bindTexture(gl.TEXTURE_2D, texture); | |
return id; | |
} | |
}; | |
let image = new Image(); | |
image.onload = () => { | |
obj.width = image.width; | |
obj.height = image.height; | |
gl.bindTexture(gl.TEXTURE_2D, texture); | |
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGB, gl.RGB, gl.UNSIGNED_BYTE, image); | |
}; | |
image.src = url; | |
return obj; | |
} | |
function updateKeywords () { | |
let displayKeywords = []; | |
if (config.SHADING) displayKeywords.push("SHADING"); | |
if (config.BLOOM) displayKeywords.push("BLOOM"); | |
if (config.SUNRAYS) displayKeywords.push("SUNRAYS"); | |
displayMaterial.setKeywords(displayKeywords); | |
} | |
updateKeywords(); | |
initFramebuffers(); | |
multipleSplats(parseInt(Math.random() * 10) + 5, 0.8); // Initial burst with slightly | |
// Configuration for ambient splats | |
const AMBIENT_INTERVAL = 1000; // Time between ambient splats in milliseconds (e.g., 2 seconds) | |
const MAX_AMBIENT_SPLATS = 5; // Maximum number of random splats per interval (e.g., 1 or 2) | |
const AMBIENT_INTENSITY = 0.3; // How strong the ambient splats are (e.g., 20% of normal) | |
setInterval(() => { | |
// Only add ambient splats if the simulation isn't paused by the user (P key) | |
if (!config.PAUSED) { | |
// Generate a small number of splats (1 to MAX_AMBIENT_SPLATS) | |
const numSplats = Math.floor(Math.random() * MAX_AMBIENT_SPLATS) + 1; | |
// Call multipleSplats with the low ambient intensity | |
multipleSplats(numSplats, AMBIENT_INTENSITY); | |
} | |
}, AMBIENT_INTERVAL); | |
let lastUpdateTime = Date.now(); | |
let colorUpdateTimer = 0.0; | |
update(); | |
function update () { | |
const dt = calcDeltaTime(); | |
if (resizeCanvas()) | |
initFramebuffers(); | |
updateColors(dt); | |
applyInputs(); | |
if (!config.PAUSED) | |
step(dt); | |
render(null); | |
requestAnimationFrame(update); | |
} | |
function calcDeltaTime () { | |
let now = Date.now(); | |
let dt = (now - lastUpdateTime) / 1000; | |
dt = Math.min(dt, 0.016666); | |
lastUpdateTime = now; | |
return dt; | |
} | |
function resizeCanvas () { | |
let width = scaleByPixelRatio(canvas.clientWidth); | |
let height = scaleByPixelRatio(canvas.clientHeight); | |
if (canvas.width != width || canvas.height != height) { | |
canvas.width = width; | |
canvas.height = height; | |
return true; | |
} | |
return false; | |
} | |
function updateColors (dt) { | |
if (!config.COLORFUL) return; | |
colorUpdateTimer += dt * config.COLOR_UPDATE_SPEED; | |
if (colorUpdateTimer >= 1) { | |
colorUpdateTimer = wrap(colorUpdateTimer, 0, 1); | |
pointers.forEach(p => { | |
p.color = generateColor(); | |
}); | |
} | |
} | |
function applyInputs () { | |
if (splatStack.length > 0) | |
multipleSplats(splatStack.pop()); | |
pointers.forEach(p => { | |
if (p.moved) { | |
p.moved = false; | |
splatPointer(p); | |
} | |
}); | |
} | |
function step (dt) { | |
gl.disable(gl.BLEND); | |
curlProgram.bind(); | |
gl.uniform2f(curlProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); | |
gl.uniform1i(curlProgram.uniforms.uVelocity, velocity.read.attach(0)); | |
blit(curl); | |
vorticityProgram.bind(); | |
gl.uniform2f(vorticityProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); | |
gl.uniform1i(vorticityProgram.uniforms.uVelocity, velocity.read.attach(0)); | |
gl.uniform1i(vorticityProgram.uniforms.uCurl, curl.attach(1)); | |
gl.uniform1f(vorticityProgram.uniforms.curl, config.CURL); | |
gl.uniform1f(vorticityProgram.uniforms.dt, dt); | |
blit(velocity.write); | |
velocity.swap(); | |
divergenceProgram.bind(); | |
gl.uniform2f(divergenceProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); | |
gl.uniform1i(divergenceProgram.uniforms.uVelocity, velocity.read.attach(0)); | |
blit(divergence); | |
clearProgram.bind(); | |
gl.uniform1i(clearProgram.uniforms.uTexture, pressure.read.attach(0)); | |
gl.uniform1f(clearProgram.uniforms.value, config.PRESSURE); | |
blit(pressure.write); | |
pressure.swap(); | |
pressureProgram.bind(); | |
gl.uniform2f(pressureProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); | |
gl.uniform1i(pressureProgram.uniforms.uDivergence, divergence.attach(0)); | |
for (let i = 0; i < config.PRESSURE_ITERATIONS; i++) { | |
gl.uniform1i(pressureProgram.uniforms.uPressure, pressure.read.attach(1)); | |
blit(pressure.write); | |
pressure.swap(); | |
} | |
gradienSubtractProgram.bind(); | |
gl.uniform2f(gradienSubtractProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); | |
gl.uniform1i(gradienSubtractProgram.uniforms.uPressure, pressure.read.attach(0)); | |
gl.uniform1i(gradienSubtractProgram.uniforms.uVelocity, velocity.read.attach(1)); | |
blit(velocity.write); | |
velocity.swap(); | |
advectionProgram.bind(); | |
gl.uniform2f(advectionProgram.uniforms.texelSize, velocity.texelSizeX, velocity.texelSizeY); | |
if (!ext.supportLinearFiltering) | |
gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, velocity.texelSizeX, velocity.texelSizeY); | |
let velocityId = velocity.read.attach(0); | |
gl.uniform1i(advectionProgram.uniforms.uVelocity, velocityId); | |
gl.uniform1i(advectionProgram.uniforms.uSource, velocityId); | |
gl.uniform1f(advectionProgram.uniforms.dt, dt); | |
gl.uniform1f(advectionProgram.uniforms.dissipation, config.VELOCITY_DISSIPATION); | |
blit(velocity.write); | |
velocity.swap(); | |
if (!ext.supportLinearFiltering) | |
gl.uniform2f(advectionProgram.uniforms.dyeTexelSize, dye.texelSizeX, dye.texelSizeY); | |
gl.uniform1i(advectionProgram.uniforms.uVelocity, velocity.read.attach(0)); | |
gl.uniform1i(advectionProgram.uniforms.uSource, dye.read.attach(1)); | |
gl.uniform1f(advectionProgram.uniforms.dissipation, config.DENSITY_DISSIPATION); | |
blit(dye.write); | |
dye.swap(); | |
} | |
function render (target) { | |
if (config.BLOOM) | |
applyBloom(dye.read, bloom); | |
if (config.SUNRAYS) { | |
applySunrays(dye.read, dye.write, sunrays); | |
blur(sunrays, sunraysTemp, 1); | |
} | |
if (target == null || !config.TRANSPARENT) { | |
gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA); | |
gl.enable(gl.BLEND); | |
} | |
else { | |
gl.disable(gl.BLEND); | |
} | |
if (!config.TRANSPARENT) | |
drawColor(target, normalizeColor(config.BACK_COLOR)); | |
if (target == null && config.TRANSPARENT) | |
drawCheckerboard(target); | |
drawDisplay(target); | |
} | |
function drawColor (target, color) { | |
colorProgram.bind(); | |
gl.uniform4f(colorProgram.uniforms.color, color.r, color.g, color.b, 1); | |
blit(target); | |
} | |
function drawCheckerboard (target) { | |
checkerboardProgram.bind(); | |
gl.uniform1f(checkerboardProgram.uniforms.aspectRatio, canvas.width / canvas.height); | |
blit(target); | |
} | |
function drawDisplay (target) { | |
let width = target == null ? gl.drawingBufferWidth : target.width; | |
let height = target == null ? gl.drawingBufferHeight : target.height; | |
displayMaterial.bind(); | |
if (config.SHADING) | |
gl.uniform2f(displayMaterial.uniforms.texelSize, 1.0 / width, 1.0 / height); | |
gl.uniform1i(displayMaterial.uniforms.uTexture, dye.read.attach(0)); | |
if (config.BLOOM) { | |
gl.uniform1i(displayMaterial.uniforms.uBloom, bloom.attach(1)); | |
gl.uniform1i(displayMaterial.uniforms.uDithering, ditheringTexture.attach(2)); | |
let scale = getTextureScale(ditheringTexture, width, height); | |
gl.uniform2f(displayMaterial.uniforms.ditherScale, scale.x, scale.y); | |
} | |
if (config.SUNRAYS) | |
gl.uniform1i(displayMaterial.uniforms.uSunrays, sunrays.attach(3)); | |
blit(target); | |
} | |
function applyBloom (source, destination) { | |
if (bloomFramebuffers.length < 2) | |
return; | |
let last = destination; | |
gl.disable(gl.BLEND); | |
bloomPrefilterProgram.bind(); | |
let knee = config.BLOOM_THRESHOLD * config.BLOOM_SOFT_KNEE + 0.0001; | |
let curve0 = config.BLOOM_THRESHOLD - knee; | |
let curve1 = knee * 2; | |
let curve2 = 0.25 / knee; | |
gl.uniform3f(bloomPrefilterProgram.uniforms.curve, curve0, curve1, curve2); | |
gl.uniform1f(bloomPrefilterProgram.uniforms.threshold, config.BLOOM_THRESHOLD); | |
gl.uniform1i(bloomPrefilterProgram.uniforms.uTexture, source.attach(0)); | |
blit(last); | |
bloomBlurProgram.bind(); | |
for (let i = 0; i < bloomFramebuffers.length; i++) { | |
let dest = bloomFramebuffers[i]; | |
gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); | |
gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0)); | |
blit(dest); | |
last = dest; | |
} | |
gl.blendFunc(gl.ONE, gl.ONE); | |
gl.enable(gl.BLEND); | |
for (let i = bloomFramebuffers.length - 2; i >= 0; i--) { | |
let baseTex = bloomFramebuffers[i]; | |
gl.uniform2f(bloomBlurProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); | |
gl.uniform1i(bloomBlurProgram.uniforms.uTexture, last.attach(0)); | |
gl.viewport(0, 0, baseTex.width, baseTex.height); | |
blit(baseTex); | |
last = baseTex; | |
} | |
gl.disable(gl.BLEND); | |
bloomFinalProgram.bind(); | |
gl.uniform2f(bloomFinalProgram.uniforms.texelSize, last.texelSizeX, last.texelSizeY); | |
gl.uniform1i(bloomFinalProgram.uniforms.uTexture, last.attach(0)); | |
gl.uniform1f(bloomFinalProgram.uniforms.intensity, config.BLOOM_INTENSITY); | |
blit(destination); | |
} | |
function applySunrays (source, mask, destination) { | |
gl.disable(gl.BLEND); | |
sunraysMaskProgram.bind(); | |
gl.uniform1i(sunraysMaskProgram.uniforms.uTexture, source.attach(0)); | |
blit(mask); | |
sunraysProgram.bind(); | |
gl.uniform1f(sunraysProgram.uniforms.weight, config.SUNRAYS_WEIGHT); | |
gl.uniform1i(sunraysProgram.uniforms.uTexture, mask.attach(0)); | |
blit(destination); | |
} | |
function blur (target, temp, iterations) { | |
blurProgram.bind(); | |
for (let i = 0; i < iterations; i++) { | |
gl.uniform2f(blurProgram.uniforms.texelSize, target.texelSizeX, 0.0); | |
gl.uniform1i(blurProgram.uniforms.uTexture, target.attach(0)); | |
blit(temp); | |
gl.uniform2f(blurProgram.uniforms.texelSize, 0.0, target.texelSizeY); | |
gl.uniform1i(blurProgram.uniforms.uTexture, temp.attach(0)); | |
blit(target); | |
} | |
} | |
function splatPointer (pointer) { | |
let dx = pointer.deltaX * config.SPLAT_FORCE; | |
let dy = pointer.deltaY * config.SPLAT_FORCE; | |
splat(pointer.texcoordX, pointer.texcoordY, dx, dy, pointer.color); | |
} | |
function multipleSplats (amount, intensityMultiplier = 1.0) { // Added intensityMultiplier | |
for (let i = 0; i < amount; i++) { | |
const color = generateColor(); | |
// Scale color intensity based on the multiplier | |
color.r *= 10.0 * intensityMultiplier; | |
color.g *= 10.0 * intensityMultiplier; | |
color.b *= 10.0 * intensityMultiplier; | |
const x = Math.random(); | |
const y = Math.random(); | |
// Scale velocity intensity based on the multiplier | |
const dx = 1000 * (Math.random() - 0.5) * intensityMultiplier; | |
const dy = 1000 * (Math.random() - 0.5) * intensityMultiplier; | |
splat(x, y, dx, dy, color); | |
} | |
} | |
function splat (x, y, dx, dy, color) { | |
splatProgram.bind(); | |
gl.uniform1i(splatProgram.uniforms.uTarget, velocity.read.attach(0)); | |
gl.uniform1f(splatProgram.uniforms.aspectRatio, canvas.width / canvas.height); | |
gl.uniform2f(splatProgram.uniforms.point, x, y); | |
gl.uniform3f(splatProgram.uniforms.color, dx, dy, 0.0); | |
gl.uniform1f(splatProgram.uniforms.radius, correctRadius(config.SPLAT_RADIUS / 100.0)); | |
blit(velocity.write); | |
velocity.swap(); | |
gl.uniform1i(splatProgram.uniforms.uTarget, dye.read.attach(0)); | |
gl.uniform3f(splatProgram.uniforms.color, color.r, color.g, color.b); | |
blit(dye.write); | |
dye.swap(); | |
} | |
function correctRadius (radius) { | |
let aspectRatio = canvas.width / canvas.height; | |
if (aspectRatio > 1) | |
radius *= aspectRatio; | |
return radius; | |
} | |
// Get reference to container to check event targets | |
const containerElement = document.querySelector('.container'); | |
window.addEventListener('mousedown', e => { | |
// Check if the click started inside the container/form | |
if (containerElement && containerElement.contains(e.target)) { | |
// If the target (or its ancestor) is the container, ignore for fluid | |
return; | |
} | |
const rect = canvas.getBoundingClientRect(); | |
// Calculate position relative to the canvas origin | |
let posX = scaleByPixelRatio(e.clientX - rect.left); | |
let posY = scaleByPixelRatio(e.clientY - rect.top); | |
let pointer = pointers.find(p => p.id == -1); | |
if (pointer == null) | |
pointer = new pointerPrototype(); | |
// Pass the SCALED PIXEL coordinates relative to the canvas | |
updatePointerDownData(pointer, -1, posX, posY); | |
}); | |
window.addEventListener('mousemove', e => { | |
let pointer = pointers[0]; // Assuming the first pointer is for mouse | |
if (!pointer.down) return; // Only track if mouse is down | |
const rect = canvas.getBoundingClientRect(); | |
// Calculate position relative to the canvas origin | |
let posX = scaleByPixelRatio(e.clientX - rect.left); | |
let posY = scaleByPixelRatio(e.clientY - rect.top); | |
// Pass the SCALED PIXEL coordinates relative to the canvas | |
updatePointerMoveData(pointer, posX, posY); | |
}); | |
// window.addEventListener('mouseup', ...) // Keep this listener as is | |
window.addEventListener('touchstart', e => { | |
// Note: We generally avoid preventDefault on window touchstart/move | |
// as it can break scrolling. Let's see if it works without it. | |
const touches = e.targetTouches; | |
const rect = canvas.getBoundingClientRect(); | |
let didProcessTouchOutside = false; | |
for (let i = 0; i < touches.length; i++) { | |
// Check if the touch started inside the container/form | |
if (containerElement && containerElement.contains(touches[i].target)) { | |
continue; // Ignore this specific touch for fluid | |
} | |
didProcessTouchOutside = true; // Mark that at least one touch outside occurred | |
// Ensure pointers array is large enough | |
// Use pointers.length directly, as pointers[0] is mouse | |
while (pointers.length <= touches[i].identifier + 1) | |
pointers.push(new pointerPrototype()); | |
// Calculate position relative to the canvas origin | |
let relativeX = touches[i].clientX - rect.left; | |
let relativeY = touches[i].clientY - rect.top; | |
let posX = scaleByPixelRatio(relativeX); | |
let posY = scaleByPixelRatio(relativeY); | |
// Find the correct pointer slot or reuse an inactive one if needed | |
// For simplicity, let's just assign based on identifier + 1 for now | |
// (assuming identifier 0 is first touch, 1 is second etc.) | |
let pointerIndex = touches[i].identifier + 1; | |
if(pointerIndex >= pointers.length) pointerIndex = pointers.length -1; // Safety check | |
// Pass the SCALED PIXEL coordinates relative to the canvas | |
updatePointerDownData(pointers[pointerIndex], touches[i].identifier, posX, posY); | |
} | |
// if (didProcessTouchOutside) { e.preventDefault(); } // Avoid if possible | |
}); | |
window.addEventListener('touchmove', e => { | |
const touches = e.targetTouches; | |
const rect = canvas.getBoundingClientRect(); | |
for (let i = 0; i < touches.length; i++) { | |
// Find the pointer associated with this touch ID | |
let pointer = pointers.find(p => p.id == touches[i].identifier); | |
if (!pointer || !pointer.down) continue; // Ignore if not tracked or not down | |
// Calculate position relative to the canvas origin | |
let relativeX = touches[i].clientX - rect.left; | |
let relativeY = touches[i].clientY - rect.top; | |
let posX = scaleByPixelRatio(relativeX); | |
let posY = scaleByPixelRatio(relativeY); | |
// Pass the SCALED PIXEL coordinates relative to the canvas | |
updatePointerMoveData(pointer, posX, posY); | |
} | |
}, false); // UseCapture = false is default, but good to be explicit | |
window.addEventListener('touchend', e => { | |
const touches = e.changedTouches; | |
for (let i = 0; i < touches.length; i++) | |
{ | |
let pointer = pointers.find(p => p.id == touches[i].identifier); | |
if (pointer == null) continue; | |
updatePointerUpData(pointer); | |
} | |
}); | |
window.addEventListener('keydown', e => { | |
if (e.code === 'KeyP') | |
config.PAUSED = !config.PAUSED; | |
if (e.key === ' ') | |
splatStack.push(parseInt(Math.random() * 20) + 5); | |
}); | |
function updatePointerDownData (pointer, id, posX, posY) { | |
pointer.id = id; | |
pointer.down = true; | |
pointer.moved = false; | |
pointer.texcoordX = posX / canvas.width; | |
pointer.texcoordY = 1.0 - posY / canvas.height; | |
pointer.prevTexcoordX = pointer.texcoordX; | |
pointer.prevTexcoordY = pointer.texcoordY; | |
pointer.deltaX = 0; | |
pointer.deltaY = 0; | |
pointer.color = generateColor(); | |
} | |
function updatePointerMoveData (pointer, posX, posY) { | |
pointer.prevTexcoordX = pointer.texcoordX; | |
pointer.prevTexcoordY = pointer.texcoordY; | |
pointer.texcoordX = posX / canvas.width; | |
pointer.texcoordY = 1.0 - posY / canvas.height; | |
pointer.deltaX = correctDeltaX(pointer.texcoordX - pointer.prevTexcoordX); | |
pointer.deltaY = correctDeltaY(pointer.texcoordY - pointer.prevTexcoordY); | |
pointer.moved = Math.abs(pointer.deltaX) > 0 || Math.abs(pointer.deltaY) > 0; | |
} | |
function updatePointerUpData (pointer) { | |
pointer.down = false; | |
} | |
function correctDeltaX (delta) { | |
let aspectRatio = canvas.width / canvas.height; | |
if (aspectRatio < 1) delta *= aspectRatio; | |
return delta; | |
} | |
function correctDeltaY (delta) { | |
let aspectRatio = canvas.width / canvas.height; | |
if (aspectRatio > 1) delta /= aspectRatio; | |
return delta; | |
} | |
function generateColor () { | |
let c = HSVtoRGB(Math.random(), 1.0, 1.0); | |
c.r *= 0.15; | |
c.g *= 0.15; | |
c.b *= 0.15; | |
return c; | |
} | |
function HSVtoRGB (h, s, v) { | |
let r, g, b, i, f, p, q, t; | |
i = Math.floor(h * 6); | |
f = h * 6 - i; | |
p = v * (1 - s); | |
q = v * (1 - f * s); | |
t = v * (1 - (1 - f) * s); | |
switch (i % 6) { | |
case 0: r = v, g = t, b = p; break; | |
case 1: r = q, g = v, b = p; break; | |
case 2: r = p, g = v, b = t; break; | |
case 3: r = p, g = q, b = v; break; | |
case 4: r = t, g = p, b = v; break; | |
case 5: r = v, g = p, b = q; break; | |
} | |
return { | |
r, | |
g, | |
b | |
}; | |
} | |
function normalizeColor (input) { | |
let output = { | |
r: input.r / 255, | |
g: input.g / 255, | |
b: input.b / 255 | |
}; | |
return output; | |
} | |
function wrap (value, min, max) { | |
let range = max - min; | |
if (range == 0) return min; | |
return (value - min) % range + min; | |
} | |
function getResolution (resolution) { | |
let aspectRatio = gl.drawingBufferWidth / gl.drawingBufferHeight; | |
if (aspectRatio < 1) | |
aspectRatio = 1.0 / aspectRatio; | |
let min = Math.round(resolution); | |
let max = Math.round(resolution * aspectRatio); | |
if (gl.drawingBufferWidth > gl.drawingBufferHeight) | |
return { width: max, height: min }; | |
else | |
return { width: min, height: max }; | |
} | |
function getTextureScale (texture, width, height) { | |
return { | |
x: width / texture.width, | |
y: height / texture.height | |
}; | |
} | |
function scaleByPixelRatio (input) { | |
let pixelRatio = window.devicePixelRatio || 1; | |
return Math.floor(input * pixelRatio); | |
} | |
function hashCode (s) { | |
if (s.length == 0) return 0; | |
let hash = 0; | |
for (let i = 0; i < s.length; i++) { | |
hash = (hash << 5) - hash + s.charCodeAt(i); | |
hash |= 0; // Convert to 32bit integer | |
} | |
return hash; | |
};</script> | |
<script> | |
const form = document.getElementById('video-form'); | |
const submitButton = document.getElementById('submit-button'); | |
const loadingIndicator = document.getElementById('loading-indicator'); | |
const progressArea = document.getElementById('progress-area'); | |
const progressStage = document.getElementById('progress-stage'); | |
const progressMessage = document.getElementById('progress-message'); | |
const messageArea = document.getElementById('message-area'); | |
const fileInputs = document.querySelectorAll('input[type="file"]'); | |
const videoModal = document.getElementById('video-modal'); | |
const modalTitle = document.getElementById('modal-title'); | |
const modalVideo = document.getElementById('modal-video'); | |
const modalDownloadLink = document.getElementById('modal-download-link'); | |
const modalCloseButton = document.getElementById('modal-close-button'); | |
const modalHqPopup = document.getElementById('modal-hq-confirmation'); | |
const modalHqPopupMessage = document.getElementById('modal-hq-popup-message'); | |
const modalHqConfirmYes = document.getElementById('modal-hq-confirm-yes'); | |
const modalHqConfirmNo = document.getElementById('modal-hq-confirm-no'); | |
const variationsSelect = document.getElementById('variations'); | |
const modelSelect = document.getElementById('model_name'); | |
const fluidCanvasElement = document.getElementById('fluid-canvas'); | |
document.addEventListener('DOMContentLoaded', () => { | |
let currentRequestId = null; | |
let lastPreviewRequestId = null; | |
let numPlansGeneratedForLastPreview = 1; | |
let progressInterval = null; | |
const POLLING_INTERVAL = 2000; | |
function showMessage(type, text, area = messageArea) { | |
const iconClass = type === 'success' ? 'fa-check-circle' : 'fa-exclamation-triangle'; | |
area.innerHTML = `<div class="message ${type}"><i class="fas ${iconClass}"></i>${text}</div>`; | |
area.style.display = 'block'; | |
area.scrollIntoView({ behavior: 'smooth', block: 'center' }); | |
} | |
function hideModal() { | |
videoModal.style.display = 'none'; | |
modalVideo.pause(); modalVideo.src = ''; modalDownloadLink.href = '#'; modalDownloadLink.removeAttribute('download'); modalHqPopup.style.display = 'none'; | |
} | |
function clearState(clearForm = true) { | |
messageArea.innerHTML = ''; messageArea.style.display = 'none'; hideModal(); progressArea.style.display = 'none'; loadingIndicator.style.display = 'none'; | |
if (progressInterval) { clearInterval(progressInterval); progressInterval = null; } | |
currentRequestId = null; lastPreviewRequestId = null; numPlansGeneratedForLastPreview = 1; | |
submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-cogs"></i> Generate Video'; | |
if (clearForm) { | |
form.reset(); | |
fileInputs.forEach(input => { | |
const displayTargetId = input.dataset.displayTarget; const displaySpan = document.getElementById(displayTargetId); | |
if (displaySpan) { displaySpan.textContent = input.multiple ? 'No files selected' : 'No file selected'; displaySpan.style.color = 'var(--text-muted)'; } | |
}); | |
const styleDescTextarea = document.getElementById('style_desc'); const tempEl = document.createElement('textarea'); tempEl.innerHTML = `{{ default_style_desc | escape }}`; const defaultDesc = tempEl.value; if (styleDescTextarea) styleDescTextarea.value = defaultDesc; | |
const outputInput = document.getElementById('output'); if (outputInput) outputInput.value = 'ai_edited_video.mp4'; | |
if (variationsSelect) variationsSelect.value = '1'; | |
tempEl.innerHTML = `{{ default_model | escape }}`; const defaultModelValue = tempEl.value; if (modelSelect) modelSelect.value = defaultModelValue; | |
document.getElementById('mute_audio').checked = false; document.getElementById('generate_preview').checked = false; | |
} | |
} | |
function updateProgressDisplay(stage, message) { | |
progressStage.textContent = stage.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); progressMessage.textContent = message || 'Processing...'; progressArea.style.display = 'block'; loadingIndicator.style.display = 'none'; hideModal(); | |
} | |
fileInputs.forEach(input => { | |
input.addEventListener('change', (event) => { | |
const displayTargetId = input.dataset.displayTarget; const displaySpan = document.getElementById(displayTargetId); | |
if (displaySpan) { | |
const files = input.files; | |
if (files.length === 1) { displaySpan.textContent = files[0].name; displaySpan.style.color = 'var(--text-secondary)'; } | |
else if (files.length > 1) { displaySpan.textContent = `${files.length} files selected`; displaySpan.style.color = 'var(--text-secondary)'; } | |
else { displaySpan.textContent = input.multiple ? 'No files selected' : 'No file selected'; displaySpan.style.color = 'var(--text-muted)'; } | |
} | |
}); | |
}); | |
async function triggerHqGeneration(previewRequestId) { | |
if (!previewRequestId) { showMessage('error', 'Cannot generate HQ: Original preview request ID is missing.'); submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-cogs"></i> Generate Video'; return; } | |
console.log(`Requesting HQ generation based on preview ID: ${previewRequestId}`); hideModal(); messageArea.innerHTML = ''; messageArea.style.display = 'none'; updateProgressDisplay('STARTING_HQ', 'Initializing High-Quality generation (using Plan 1)...'); submitButton.disabled = true; submitButton.innerHTML = '<i class="fas fa-hourglass-half"></i> Processing HQ...'; | |
try { | |
const response = await fetch(`/generate-hq/${previewRequestId}`, { method: 'POST' }); const result = await response.json(); | |
if (!response.ok) { throw new Error(result.message || `Server error: ${response.status}`); } | |
if (result.status === 'processing_started' && result.request_id) { | |
currentRequestId = result.request_id; lastPreviewRequestId = null; numPlansGeneratedForLastPreview = 1; console.log("HQ Processing started with Request ID:", currentRequestId); updateProgressDisplay('RECEIVED_HQ', result.message || 'HQ Processing started...'); if (progressInterval) clearInterval(progressInterval); progressInterval = setInterval(pollProgress, POLLING_INTERVAL); | |
} else { throw new Error(result.message || 'Received an unexpected response when starting HQ generation.'); } | |
} catch (error) { | |
console.error('HQ Generation Start Error:', error); progressArea.style.display = 'none'; showMessage('error', `Failed to start HQ generation: ${error.message}`); submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-cogs"></i> Generate Video'; | |
} | |
} | |
async function pollProgress() { | |
if (!currentRequestId) { console.warn("Polling stopped: No active request ID."); if (progressInterval) clearInterval(progressInterval); progressInterval = null; return; } | |
try { | |
const response = await fetch(`/progress/${currentRequestId}`); | |
if (!response.ok) { | |
if (response.status === 404) { console.error(`Polling failed: Request ID ${currentRequestId} not found.`); showMessage('error', `Polling error: Request ID not found or expired. Please start a new request.`); } | |
else { console.error(`Polling failed with status: ${response.status}`); let errorMsg = `Polling error: Server returned status ${response.status}. Please try again later.`; try { const errorData = await response.json(); errorMsg = errorData.message || errorMsg; } catch (e) { } showMessage('error', errorMsg); } | |
if (progressInterval) clearInterval(progressInterval); progressInterval = null; progressArea.style.display = 'none'; submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-cogs"></i> Generate Video'; return; | |
} | |
const data = await response.json(); updateProgressDisplay(data.stage, data.message); | |
if (data.stage === 'COMPLETED') { | |
clearInterval(progressInterval); progressInterval = null; progressArea.style.display = 'none'; const resultData = data.result || {}; const isPreview = resultData.is_preview; const successMsg = data.message || `Video ${isPreview ? 'preview ' : ''}generated successfully!`; showMessage('success', successMsg, messageArea); | |
if (resultData.video_url) { | |
modalTitle.textContent = `Your Generated Video ${isPreview ? '(Preview)' : '(HQ)'}`; modalVideo.src = resultData.video_url; modalDownloadLink.href = resultData.video_url; modalDownloadLink.download = resultData.output_filename || `ai_edited_video${isPreview ? '_preview' : '_hq'}.mp4`; modalVideo.load(); | |
if (isPreview && resultData.request_id) { | |
lastPreviewRequestId = resultData.request_id; numPlansGeneratedForLastPreview = resultData.num_plans_generated || 1; | |
if (numPlansGeneratedForLastPreview > 1) { modalHqPopupMessage.textContent = `Preview generated using Plan 1/${numPlansGeneratedForLastPreview}. Generate the high-quality version using Plan 1?`; modalHqConfirmYes.textContent = `Yes, Generate HQ (Plan 1)`; } | |
else { modalHqPopupMessage.textContent = `Preview generated! Generate the high-quality version now?`; modalHqConfirmYes.textContent = `Yes, Generate HQ`; } | |
modalHqPopup.style.display = 'block'; submitButton.disabled = true; submitButton.innerHTML = '<i class="fas fa-check"></i> Preview Ready'; | |
} else { modalHqPopup.style.display = 'none'; submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-cogs"></i> Generate Video'; } | |
videoModal.style.display = 'flex'; | |
} else { showMessage('error', `Processing completed, but no video URL was returned.${isPreview ? ' (Preview)' : ''}`); submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-cogs"></i> Generate Video'; } | |
} else if (data.stage === 'FAILED') { | |
clearInterval(progressInterval); progressInterval = null; progressArea.style.display = 'none'; showMessage('error', data.error || 'An unknown error occurred during processing.'); submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-cogs"></i> Generate Video'; | |
} else if (data.stage === 'UNKNOWN') { | |
clearInterval(progressInterval); progressInterval = null; progressArea.style.display = 'none'; showMessage('error', data.message || 'Request status is unknown or has expired.'); submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-cogs"></i> Generate Video'; | |
} | |
} catch (error) { | |
console.error('Polling Fetch Error:', error); if (progressInterval) clearInterval(progressInterval); progressInterval = null; progressArea.style.display = 'none'; showMessage('error', `Polling connection error: ${error.message}. Please check connection.`); submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-cogs"></i> Generate Video'; | |
} | |
} | |
form.addEventListener('submit', async (event) => { | |
event.preventDefault(); clearState(false); | |
const videosInput = document.getElementById('videos'); const durationInput = document.getElementById('duration'); const styleDescInput = document.getElementById('style_desc'); const outputInput = document.getElementById('output'); | |
if (!videosInput.files || videosInput.files.length === 0) { showMessage('error', 'Please select at least one source video file.'); return; } | |
if (!durationInput.value || durationInput.value <= 0) { showMessage('error', 'Please enter a valid positive target duration.'); return; } | |
if (!styleDescInput.value.trim()) { showMessage('error', 'Please provide a style description.'); return; } | |
if (!outputInput.value.trim()) { showMessage('error', 'Please provide an output file name.'); return; } | |
loadingIndicator.style.display = 'block'; submitButton.disabled = true; submitButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Submitting...'; const formData = new FormData(form); | |
try { | |
const response = await fetch('/generate', { method: 'POST', body: formData, }); const result = await response.json(); | |
if (!response.ok) { throw new Error(result.message || `Server error: ${response.status}`); } | |
if (result.status === 'processing_started' && result.request_id) { | |
currentRequestId = result.request_id; lastPreviewRequestId = null; numPlansGeneratedForLastPreview = 1; console.log("Processing started with Request ID:", currentRequestId); loadingIndicator.style.display = 'none'; updateProgressDisplay('RECEIVED', result.message || 'Processing started...'); if (progressInterval) clearInterval(progressInterval); progressInterval = setInterval(pollProgress, POLLING_INTERVAL); submitButton.innerHTML = '<i class="fas fa-hourglass-half"></i> Processing...'; | |
} else { throw new Error(result.message || 'Received an unexpected response from the server.'); } | |
} catch (error) { | |
console.error('Form Submission Error:', error); showMessage('error', `Submission Failed: ${error.message || 'Could not connect.'}`); submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-cogs"></i> Generate Video'; loadingIndicator.style.display = 'none'; | |
} | |
}); | |
modalCloseButton.addEventListener('click', () => { hideModal(); if (lastPreviewRequestId) { submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-cogs"></i> Generate Video'; } }); | |
videoModal.addEventListener('click', (event) => { if (event.target === videoModal) { hideModal(); if (lastPreviewRequestId) { submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-cogs"></i> Generate Video'; } } }); | |
modalHqConfirmYes.addEventListener('click', () => { triggerHqGeneration(lastPreviewRequestId); }); | |
modalHqConfirmNo.addEventListener('click', () => { hideModal(); submitButton.disabled = false; submitButton.innerHTML = '<i class="fas fa-cogs"></i> Generate Video'; console.log("User declined HQ generation."); lastPreviewRequestId = null; }); | |
}); | |
</script> | |
</body> | |
</html> |