SolarumAsteridion's picture
Update templates/index.html
8897096 verified
raw
history blame
92.5 kB
<!DOCTYPE html>
<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.
*/
'use strict';
// 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>