In today’s fast-paced digital world, communication is increasingly visual. People prefer quick, easily digestible content, and GIFs—short, looping animations—have emerged as one of the most popular formats for sharing emotions, reactions, tutorials, and highlights. This rising popularity has brought video to GIF converters into the spotlight. But what exactly makes these tools so important?
1. Enhancing Communication
GIFs offer a visual way to communicate emotions and ideas that can be more expressive than plain text. Whether in social media, chats, or emails, a GIF can:
-
Add humor or personality to messages
-
Provide a visual summary of longer content
-
Help convey tone in a way that static images or text alone often can’t
A video to GIF converter allows users to extract these expressive moments from video clips, making communication more dynamic and engaging.
2. Content Creation and Marketing
For content creators, marketers, and social media managers, video to GIF converters are valuable tools:
-
Promotions and Ads: GIFs can be used to showcase product features or highlights in a compact format.
-
Social Media Posts: Platforms like Twitter, Reddit, and Tumblr thrive on GIFs. Short clips catch attention faster than full videos.
-
Memes and Viral Content: Converting viral video moments into GIFs can increase engagement and sharing.
Because GIFs load faster and are easier to embed, they are ideal for marketing purposes where attention spans are short.
3. File Size and Compatibility
Videos are often large in size and may require specific players or codecs to view. GIFs, by contrast:
-
Have smaller file sizes
-
Are universally supported on websites and messaging platforms
-
Do not require special software to view
A video to GIF converter reduces the size and complexity of video content, making it easier to share across platforms and devices.
4. Educational and Instructional Use
GIFs are useful for tutorials and demonstrations. Converting parts of a how-to video into a looping GIF can highlight a key action or step:
-
Software guides
-
Crafting or DIY instructions
-
Technical support visuals
The looped nature of GIFs allows viewers to watch the action repeatedly without pressing play again—ideal for reinforcing learning.
5. Preserving and Sharing Highlights
Not all parts of a video are worth sharing, and full-length videos can be cumbersome. With a video to GIF converter, you can:
-
Capture the best moments (funny reactions, game highlights, movie scenes)
-
Archive memorable clips
-
Share only the relevant portions of content
This selective sharing helps distill content down to its most impactful or entertaining moments.
Conclusion
A video to GIF converter is more than just a novelty tool—it plays a vital role in how we communicate, market, educate, and entertain in the digital age. As visual content continues to dominate online spaces, these converters bridge the gap between long-form videos and quick, engaging media formats that people love to consume and share. Whether you're a professional creator or a casual internet user, having access to a good video to GIF converter can significantly enhance your digital expression.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<!-- Enhanced meta descriptions -->
<meta name="description" content="Free online Video to GIF converter. Convert videos to high-quality GIFs with customizable frame rate, size, speed, and dithering options. No upload required - works directly in your browser.">
<meta name="keywords" content="video to gif, gif converter, gif maker, online gif tool, video converter, free gif maker, browser gif converter">
<meta name="author" content="Video to GIF Converter">
<title>Video to GIF Converter - Free Online Tool | Convert Videos to GIF</title>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
<style>
:root {
--bg-color: #0a0a0a;
--panel-bg: #1a1a1a;
--text-color: #f0f0f0;
--border-color: #333333;
--primary-color: #e53e3e;
--secondary-color: #c53030;
--hover-color: #f56565;
--disabled-color: #4a5568;
--success-color: #38a169;
--error-color: #e53e3e;
--accent-color: #ff0000;
}
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 4px 6px -1px rgba(229, 62, 62, 0.2);
}
50% {
transform: scale(1.05);
box-shadow: 0 6px 8px -1px rgba(229, 62, 62, 0.5);
}
100% {
transform: scale(1);
box-shadow: 0 4px 6px -1px rgba(229, 62, 62, 0.2);
}
}
@keyframes glow {
0% {
box-shadow: 0 0 5px rgba(229, 62, 62, 0.5);
}
50% {
box-shadow: 0 0 20px rgba(229, 62, 62, 0.8);
}
100% {
box-shadow: 0 0 5px rgba(229, 62, 62, 0.5);
}
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
html {
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
body {
font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
line-height: 1.5;
letter-spacing: -0.025em;
max-width: 1200px;
margin: 0 auto;
background-color: var(--bg-color);
color: var(--text-color);
min-height: 100vh;
box-sizing: border-box;
background-image:
radial-gradient(circle at 10% 20%, rgba(229, 62, 62, 0.1) 0%, transparent 20%),
radial-gradient(circle at 90% 80%, rgba(229, 62, 62, 0.1) 0%, transparent 20%);
}
button svg {
vertical-align: middle;
}
button:hover svg {
transform: scale(1.1);
}
button:focus,
input[type="file"]:focus {
outline: none;
}
footer {
padding: 20px 15px;
margin-top: 20px;
text-align: center;
border-top: 1px solid var(--border-color);
background: rgba(26, 26, 26, 0.8);
border-radius: 8px;
margin: 20px 10px 10px;
}
h1 {
color: var(--text-color);
margin: 0;
grid-column: 1 / -1;
font-size: 2.5em;
font-weight: 700;
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-align: center;
margin-bottom: 10px;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
position: relative;
display: inline-block;
padding: 0 20px;
}
h1:after {
content: '';
position: absolute;
bottom: -10px;
left: 20%;
width: 60%;
height: 3px;
background: linear-gradient(to right, transparent, var(--primary-color), transparent);
border-radius: 3px;
}
h3 {
margin: 0;
border-bottom: 1px solid;
border-image: linear-gradient(to right, var(--primary-color) 30%, transparent) 1;
flex-grow: 1;
font-weight: 600;
color: var(--text-color);
}
select {
flex-grow: 1;
width: 100%;
background-color: var(--panel-bg);
color: var(--text-color);
border: 2px solid var(--border-color);
border-radius: 8px;
padding: 10px 12px;
margin: -2px 0;
font-size: 0.95em;
cursor: pointer;
transition: all 0.2s ease;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23e53e3e'><path d='M2 4 L6 8 L10 4'/></svg>");
background-repeat: no-repeat;
background-position: right 12px center;
padding-right: 32px;
}
select::-ms-expand {
display: none;
}
select:hover {
border-color: var(--primary-color);
box-shadow: 0 0 0 1px var(--primary-color);
}
select:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(229, 62, 62, 0.3);
outline: none;
}
.button-group {
margin-top: 15px;
}
.button-group button {
width: 100%;
}
.cancel-btn {
margin-top: 15px;
padding: 10px 16px;
background: var(--panel-bg);
border: 1px solid var(--error-color);
color: var(--error-color);
border-radius: 8px;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
}
.cancel-btn:hover {
background: var(--error-color);
color: white;
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(229, 62, 62, 0.3);
}
.container {
display: grid;
grid-template-columns: 1fr 1fr;
align-items: start;
max-width: 1100px;
gap: 15px;
padding: 10px 15px 15px 15px;
}
.container-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.duration-time {
color: var(--primary-color);
font-weight: 500;
}
.end-marker {
background: none;
}
.end-marker::before {
content: ']';
position: absolute;
right: -2px;
color: var(--error-color);
font-size: 20px;
}
.expand-button {
background: none;
border: none;
color: var(--text-color);
cursor: pointer;
padding: 4px;
opacity: 0.7;
transition: opacity 0.2s ease;
margin-right: -15px;
margin-top: -30px;
}
.expand-button:hover {
opacity: 1;
color: var(--primary-color);
}
.file-size-indicator {
margin-top: 5px;
margin-bottom: 5px;
font-size: 0.875em;
color: var(--text-color);
text-align: center;
display: none;
}
.fullscreen-container {
grid-column: 1 / -1 !important;
width: 100% !important;
max-width: none !important;
position: relative !important;
}
.fullscreen-container.preview-container,
.fullscreen-container.settings-panel,
.fullscreen-container.tips-container {
width: calc(200% - 40px) !important;
position: relative !important;
margin: 0 !important;
}
.hidden-container {
display: none !important;
}
.indicator-separator {
margin: 0 5px;
color: var(--primary-color);
}
.keyboard-key {
display: inline-block;
padding: 2px 6px;
border: 1px solid var(--border-color);
border-radius: 4px;
background: var(--panel-bg);
font-family: monospace;
font-size: 1.1em;
font-weight: bolder;
color: var(--text-color);
min-width: 12px;
text-align: center;
box-shadow: 0 1px 1px rgba(0,0,0,0.2);
margin-right: 5px;
}
.left-column {
display: flex;
flex-direction: column;
gap: 15px;
}
.left-column .fullscreen-container.preview-container,
.left-column .fullscreen-container.settings-panel,
.left-column .fullscreen-container.tips-container {
transform: translateX(0%) !important;
margin-right: 0 !important;
}
.marker-times {
display: flex;
justify-content: space-between;
margin-top: 4px;
font-size: 0.8em;
color: var(--text-color);
}
.mute-btn {
flex: 0.1;
background: var(--panel-bg);
border: .5px solid var(--border-color);
border-radius: 6px;
color: var(--text-color);
cursor: pointer;
font-weight: 500;
transition: background-color 0.2s ease;
outline: none;
}
.mute-btn:hover {
background: var(--border-color);
}
.play-pause-btn {
margin-top: 10px;
padding: 10px 16px;
background: var(--primary-color);
border: none;
border-radius: 6px;
color: white;
cursor: pointer;
font-weight: 500;
transition: all 0.2s ease;
outline: none;
flex: 0.9;
margin-top: 0;
box-shadow: 0 4px 6px rgba(229, 62, 62, 0.2);
}
.play-pause-btn:hover {
background: var(--hover-color);
transform: translateY(-2px);
box-shadow: 0 6px 8px rgba(229, 62, 62, 0.3);
}
.play-pause-btn:disabled {
background: var(--disabled-color);
cursor: not-allowed;
opacity: 0.6;
box-shadow: none;
transform: none;
}
.playback-controls {
display: flex;
gap: 8px;
margin-top: 15px;
}
.playhead {
position: absolute;
width: 2px;
height: 16px;
background: #e2e8f0;
top: 50%;
transform: translateY(-50%);
pointer-events: auto;
display: none;
z-index: 1;
cursor: grab;
}
.playhead:active {
cursor: grabbing;
}
.playhead::after {
content: '';
position: absolute;
top: -4px;
left: -4px;
width: 10px;
height: 10px;
background: #e2e8f0;
border-radius: 50%;
transform: translateY(-50%);
}
.preview-container {
background: var(--panel-bg);
position: relative;
max-width: 100%;
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
}
.preview-container:hover {
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
}
.video-wrapper {
position: relative;
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.crop-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
pointer-events: none;
z-index: 1;
}
.crop-region {
position: absolute;
border: 2px solid var(--primary-color);
background: rgba(229, 62, 62, 0.1);
cursor: move;
touch-action: none;
pointer-events: auto;
z-index: 2;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.crop-handle {
position: absolute;
width: 12px;
height: 12px;
background: var(--primary-color);
border: 2px solid white;
border-radius: 50%;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.crop-handle.nw { top: -6px; left: -6px; cursor: nw-resize; }
.crop-handle.ne { top: -6px; right: -6px; cursor: ne-resize; }
.crop-handle.sw { bottom: -6px; left: -6px; cursor: sw-resize; }
.crop-handle.se { bottom: -6px; right: -6px; cursor: se-resize; }
.crop-button {
display: flex;
align-items: center;
gap: 8px;
background: var(--panel-bg);
border: 1px solid var(--border-color);
color: var(--text-color);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s ease;
}
.crop-button:hover {
background: var(--border-color);
}
.crop-button.active {
background: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
#cropDimensions {
margin-top: 8px;
font-size: 0.9em;
color: var(--text-color);
}
.preview-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(10, 10, 10, 0.9);
backdrop-filter: blur(8px);
z-index: 1001;
padding: 40px;
justify-content: center;
align-items: center;
}
.preview-overlay-content {
position: relative;
max-width: 95vw;
max-height: 95vh;
display: flex;
justify-content: center;
align-items: center;
}
.progress-bar {
width: 100%;
height: 6px;
background: var(--border-color);
border-radius: 3px;
margin: 15px 0;
overflow: hidden;
}
.progress-content {
background: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 32px 48px;
text-align: center;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.3);
min-width: 250px;
position: relative;
}
.progress-content p {
margin: 10px 0;
font-size: 16px;
}
.progress-details {
font-size: 0.9em;
color: var(--text-color);
opacity: 0.8;
margin: 10px 0 0 0;
}
.progress-fill {
width: 0%;
height: 100%;
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
transition: width 0.3s ease;
}
.progress-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(10, 10, 10, 0.8);
backdrop-filter: blur(8px);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
}
.progress-status {
margin: 10px 0;
font-size: 16px;
}
.right-column {
position: sticky;
top: 20px;
}
.right-column .fullscreen-container.preview-container,
.right-column .fullscreen-container.settings-panel,
.right-column .fullscreen-container.tips-container {
transform: translateX(-50.5%) !important;
margin-left: 0 !important;
}
.setting-group {
margin: 10px 0;
display: flex;
align-items: center;
gap: 10px;
}
.setting-group label {
min-width: 100px;
flex-shrink: 0;
}
.settings-panel {
margin: 0;
background: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
}
.settings-panel:hover {
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
}
.size-indicator {
position: relative;
background: #3341556b;
color: var(--text-color);
padding: 6px 12px;
border-radius: 6px;
font-size: 0.75em;
font-weight: 500;
display: none;
margin: 10px 0;
text-align: center;
}
.spinner {
width: 48px;
height: 48px;
border: 4px solid rgba(229, 62, 62, 0.2);
border-top: 4px solid var(--primary-color);
border-radius: 50%;
margin: 10px auto;
animation: spin 1s cubic-bezier(0.45, 0, 0.55, 1) infinite;
}
.start-marker {
background: none;
}
.start-marker::before {
content: '[';
position: absolute;
left: -2px;
color: var(--success-color);
font-size: 20px;
}
.time-control {
display: flex;
align-items: center;
gap: 15px;
margin: 5px 0;
flex-grow: 1;
}
.time-control label {
min-width: 0px;
}
.time-input {
width: 80px;
min-width: 100px;
background-color: var(--panel-bg);
color: var(--text-color);
border: 2px solid var(--border-color);
border-radius: 8px;
padding: 8px;
font-size: 0.95em;
transition: all 0.2s ease;
box-sizing: border-box;
}
.time-input:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(229, 62, 62, 0.3);
outline: none;
}
.time-input-group {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.time-stepper {
display: flex;
flex-direction: column;
gap: 2px;
}
.time-stepper-btn {
background: var(--panel-bg);
border: 1px solid var(--border-color);
color: var(--text-color);
padding: 4px 8px;
cursor: pointer;
border-radius: 4px;
font-size: 14px;
line-height: 1;
transition: all 0.2s ease;
touch-action: manipulation;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
-webkit-tap-highlight-color: transparent;
opacity: 1;
}
.time-stepper-btn:hover {
background: var(--border-color);
}
.time-stepper-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
background: var(--disabled-color);
border-color: var(--border-color);
}
.timeline {
position: relative;
height: 10px;
background: var(--border-color);
border-radius: 2px;
cursor: pointer;
transition: height 0.4s;
}
.timeline:hover {
height: 12px;
}
.timeline-container {
position: relative;
height: 20px;
padding: 4px 0;
cursor: pointer;
overflow: visible;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
.timeline-marker {
position: absolute;
width: 8px;
height: 16px;
top: 50%;
transform: translate(-50%, -50%);
cursor: grab;
z-index: 2;
font-family: monospace;
display: flex;
align-items: center;
justify-content: center;
color: var(--bg-color);
font-weight: bold;
border: none;
border-radius: 0;
transition: transform 0.2s;
}
.timeline-marker:active {
cursor: grabbing;
}
.timeline-marker:hover {
transform: translate(-50%, -50%) scale(1.2);
}
.tip-icon {
font-size: 1.2em;
flex-shrink: 0;
margin-left: 5px;
}
.tips-container {
margin-top: 10px;
background: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 24px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
}
.tips-container:hover {
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.3);
}
.tips-list {
list-style: none;
padding: 0;
margin: 0;
}
.tips-list li {
margin-bottom: 3px;
display: flex;
align-items: flex-start;
gap: 10px;
color: var(--text-color);
font-size: 0.9em;
}
.tips-list li:last-child {
margin-bottom: 0;
}
.tips-list strong {
color: var(--primary-color);
margin-right: 4px;
}
.video-controls {
margin-top: 15px;
display: flex;
flex-direction: column;
}
#convertButton {
background: linear-gradient(to right, var(--primary-color), var(--secondary-color));
box-shadow: 0 4px 6px -1px rgba(229, 62, 62, 0.2);
animation: glow 2s infinite;
}
#convertButton.attention-needed {
animation: pulse 2s infinite;
background: linear-gradient(to right, var(--primary-color), var(--error-color));
}
#convertButton:disabled {
background: var(--disabled-color);
cursor: not-allowed;
opacity: 0.6;
box-shadow: none;
transform: none;
animation: none;
}
#convertButton:disabled:hover {
transform: none;
box-shadow: none;
}
#convertButton, #downloadButton {
padding: 12px 10px;
font-weight: 600;
border-radius: 8px;
transition: all 0.2s ease;
text-transform: uppercase;
letter-spacing: 0.05em;
border: none;
color: white;
outline: none;
}
#convertButton:hover, #downloadButton:hover {
transform: translateY(-2px);
box-shadow: 0 6px 8px -1px rgba(0, 0, 0, 0.2);
}
#downloadButton {
background: var(--success-color);
box-shadow: 0 4px 6px -1px rgba(34, 197, 94, 0.2);
width: 100%;
}
#downloadButton:hover {
transform: translateY(-2px);
box-shadow: 0 6px 8px -1px rgba(0, 0, 0, 0.2);
}
#overlayGif {
max-width: 100%;
max-height: 95vh;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
}
.gif-wrapper {
display: flex;
justify-content: center;
width: 100%;
max-height: 500px;
overflow: hidden;
}
#previewGif {
width: auto !important;
height: auto !important;
max-width: 100% !important;
max-height: 500px !important;
display: block;
transition: opacity 0.3s ease;
cursor: pointer;
margin: 0 auto;
object-fit: contain;
}
#videoInput {
background-color: var(--panel-bg);
color: var(--text-color);
border: 2px dashed var(--border-color);
padding: 15px;
border-radius: 12px;
width: -webkit-fill-available;
width: -moz-available;
cursor: pointer;
transition: all 0.3s ease;
margin: 0 auto 10px;
display: block;
outline: none;
text-align: center;
}
#videoInput:hover {
border-color: var(--primary-color);
background-color: rgba(229, 62, 62, 0.1);
box-shadow: 0 0 10px rgba(229, 62, 62, 0.2);
}
#videoPreview {
width: 100%;
height: auto;
max-height: 70vh;
margin: 0;
border-radius: 8px;
object-fit: contain;
object-position: center;
}
@media (max-width: 768px) {
.time-stepper-btn {
padding: 8px 12px;
font-size: 16px;
min-height: 36px;
min-width: 36px;
}
.fullscreen-container.preview-container,
.fullscreen-container.settings-panel,
.fullscreen-container.tips-container {
width: 100% !important;
transform: none !important;
margin: 0 !important;
}
.expand-button {
display: none;
}
footer {
text-align: center;
padding: 0px 10px 10px 10px;
}
.tips-list li {
font-size: .7em;
}
.size-indicator {
padding: 6px 6px;
}
#videoInput {
align-content: center;
height: 50px;
}
.tips-container {
margin-top: 15px;
padding: 15px;
}
.timeline-marker {
touch-action: none;
}
.gif-wrapper {
max-height: 300px;
}
#previewGif {
max-height: 300px !important;
}
}
@media not all and (min-resolution:.001dpcm) {
@supports (-webkit-appearance:none) {
select {
text-indent: 1px;
text-overflow: '';
line-height: 1.2;
background-color: var(--bg-color);
}
}
}
/* Add these CSS rules in the <style> section */
.dragging-active {
user-select: none !important;
-webkit-user-select: none !important;
-moz-user-select: none !important;
-ms-user-select: none !important;
cursor: grabbing !important;
}
/* Add these CSS rules in the style section */
.time-input::-webkit-inner-spin-button,
.time-input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
.time-input {
-moz-appearance: textfield;
appearance: textfield;
}
.time-input:focus {
-moz-appearance: textfield;
appearance: textfield;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 46px;
height: 24px;
}
.toggle-switch input {
opacity: 0;
width: 0;
height: 0;
}
.toggle-label {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: var(--border-color);
transition: .4s;
border-radius: 24px;
}
.toggle-label:before {
position: absolute;
content: "";
height: 18px;
width: 18px;
left: 3px;
bottom: 3px;
background-color: var(--text-color);
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-label {
background-color: var(--primary-color);
}
input:checked + .toggle-label:before {
transform: translateX(22px);
}
#timecodePreviewer {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
z-index: 2;
}
</style>
</head>
<body>
<header>
<h1>Video to GIF Converter</h1>
</header>
<main class="container">
<div class="left-column">
<div class="preview-container">
<div class="container-header">
<h3>Original Video</h3>
<button class="expand-button" aria-label="Toggle fullscreen">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
</svg>
</button>
</div>
<input type="file" id="videoInput" accept="video/mp4,video/webm,video/ogg,video/quicktime">
<div class="video-wrapper">
<video id="videoPreview" style="display: none;" playsinline webkit-playsinline></video>
<canvas id="timecodePreviewer" style="display: none; position: absolute; top: 0; left: 0; pointer-events: none;"></canvas>
</div>
<div class="video-controls">
<div class="timeline-container">
<div class="timeline">
<div class="timeline-marker start-marker" title="Start Time"></div>
<div class="timeline-marker end-marker" title="End Time"></div>
<div class="playhead"></div>
</div>
<div class="marker-times">
<span class="start-time">0:00</span>
<span class="duration-time"></span>
<span class="end-time">0:00</span>
</div>
</div>
<div class="playback-controls">
<button class="play-pause-btn" disabled>Play</button>
<button class="mute-btn">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
</svg>
</button>
</div>
</div>
</div>
<div class="settings-panel">
<div class="container-header">
<h3>GIF Settings</h3>
<!-- <button class="expand-button" aria-label="Toggle fullscreen">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
</svg>
</button> -->
</div>
<div class="setting-group">
<label style="margin-top: 10px;">Time Range</label>
<div class="time-control">
<label style="color: var(--success-color)">Start:</label>
<div class="time-input-group">
<input type="number" id="startTime" class="time-input" value="0.00" min="0" step="0.01" pattern="\d*\.?\d{0,2}" style="-webkit-appearance: none; -moz-appearance: textfield;">
<div class="time-stepper">
<button class="time-stepper-btn" onclick="" disabled>+</button>
<button class="time-stepper-btn" onclick="" disabled>−</button>
</div>
</div>
<label style="color: var(--error-color)">End:</label>
<div class="time-input-group">
<input type="number" id="endTime" class="time-input" value="0.00" min="0" step="0.01" pattern="\d*\.?\d{0,2}" style="-webkit-appearance: none; -moz-appearance: textfield;">
<div class="time-stepper">
<button class="time-stepper-btn" onclick="" disabled>+</button>
<button class="time-stepper-btn" onclick="" disabled>−</button>
</div>
</div>
</div>
</div>
<div class="setting-group">
<label for="fpsSelect">Frame Rate</label>
<select id="fpsSelect">
<option value="30">30 fps (Smooth)</option>
<option value="24">24 fps (Standard)</option>
<option value="15">15 fps (Balanced)</option>
<option value="10" selected>10 fps (Compact)</option>
<option value="5">5 fps (Small file)</option>
</select>
</div>
<div class="setting-group">
<label for="sizeSelect">Output Size</label>
<select id="sizeSelect">
<option value="1">Original Size</option>
<option value="0.9">90% of Original</option>
<option value="0.8">80% of Original</option>
<option value="0.75">75% of Original</option>
<option value="0.7">70% of Original</option>
<option value="0.6">60% of Original</option>
<option value="0.5">50% of Original</option>
<option value="0.4">40% of Original</option>
<option value="0.3">30% of Original</option>
<option value="0.25">25% of Original</option>
<option value="0.2">20% of Original</option>
</select>
</div>
<div class="setting-group">
<label for="ditherSelect">Dithering</label>
<select id="ditherSelect">
<option value="false" selected>No Dithering</option>
<option value="FloydSteinberg">Floyd-Steinberg</option>
<option value="FalseFloydSteinberg">Light Dithering</option>
<option value="Stucki">Stucki</option>
</select>
</div>
<div class="setting-group">
<label for="speedSelect">Speed</label>
<select id="speedSelect">
<option value="1" selected>Normal Speed (1x)</option>
<option value="1.5">1.5x Speed</option>
<option value="2">2x Speed</option>
<option value="3">3x Speed</option>
<option value="4">4x Speed</option>
<option value="5">5x Speed</option>
</select>
</div>
<div class="setting-group" id="cropControls"">
<div style="display: flex; align-items: center; gap: 10px;">
<label>Crop Video</label>
<div class="crop-controls">
<button id="toggleCrop" class="crop-button">
Enable Crop
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-crop"><path d="M6.13 1L6 16a2 2 0 0 0 2 2h15"></path><path d="M1 6.13L16 6a2 2 0 0 1 2 2v15"></path></svg>
</button>
<div id="cropDimensions" style="display: none;">
<span id="cropSize"></span>
</div>
</div>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<label for="showTimecode">Show Timecode</label>
<div class="toggle-switch">
<input type="checkbox" id="showTimecode">
<label for="showTimecode" class="toggle-label"></label>
</div>
</div>
</div>
<div class="button-group">
<button id="convertButton" disabled>Convert to GIF</button>
</div>
<div class="progress-overlay" id="progressOverlay">
<div class="progress-content">
<div class="spinner"></div>
<p class="progress-status">Converting... Please wait.</p>
<div class="progress-bar">
<div class="progress-fill"></div>
</div>
<p class="progress-details">Processing frame: 0/0</p>
<button id="cancelConversion" class="cancel-btn">Cancel Conversion</button>
</div>
</div>
</div>
</div>
<div class="right-column">
<div class="preview-container">
<div class="container-header">
<h3>Converted GIF Preview</h3>
<button class="expand-button" aria-label="Toggle fullscreen">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
</svg>
</button>
</div>
<div class="gif-wrapper">
<img id="previewGif" alt="">
</div>
<div class="size-indicator" id="sizeIndicator"></div>
<button id="downloadButton" style="display: none;">Download GIF</button>
</div>
<div class="tips-container">
<div class="container-header">
<h3>Tips for Faster Conversion</h3>
<!-- <button class="expand-button" aria-label="Toggle fullscreen">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>
</svg>
</button> -->
</div>
<ul class="tips-list">
<li>
<span class="tip-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/>
</svg>
</span>
<strong>Use WebM:</strong> WebM files convert faster than other formats
</li>
<li>
<span class="tip-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-layers">
<polygon points="12 2 2 7 12 12 22 7 12 2"></polygon>
<polyline points="2 17 12 22 22 17"></polyline>
<polyline points="2 12 12 17 22 12"></polyline>
</svg>
</span>
<strong>Lower Frame Rate:</strong> Use 10-15 fps for most cases
</li>
<li>
<span class="tip-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-minimize"><path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"></path></svg>
</span>
<strong>Reduce Size:</strong> Smaller dimensions = faster conversion
</li>
<li>
<span class="tip-icon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="8" cy="8" r="7" stroke="currentColor" stroke-width="2"/>
<path d="M8 4V8L11 11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
</svg>
</span>
<strong>Shorter Duration:</strong> Keep clips under 10 seconds
</li>
<li>
<span class="tip-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-cpu"><rect x="4" y="4" width="16" height="16" rx="2" ry="2"></rect><rect x="9" y="9" width="6" height="6"></rect><line x1="9" y1="1" x2="9" y2="4"></line><line x1="15" y1="1" x2="15" y2="4"></line><line x1="9" y1="20" x2="9" y2="23"></line><line x1="15" y1="20" x2="15" y2="23"></line><line x1="20" y1="9" x2="23" y2="9"></line><line x1="20" y1="14" x2="23" y2="14"></line><line x1="1" y1="9" x2="4" y2="9"></line><line x1="1" y1="14" x2="4" y2="14"></line></svg>
</span>
<strong>Skip Dithering:</strong> Only use when color accuracy is crucial
</li>
</ul>
<h3 style="margin-top: 10px;margin-bottom: 10px;">Timeline Keyboard Shortcuts</h3>
<ul class="tips-list">
<li>
<span><span class="keyboard-key">[</span> Set start marker at playhead position</span>
</li>
<li>
<span><span class="keyboard-key">]</span> Set end marker at playhead position</span>
</li>
<li>
<span><span class="keyboard-key">←</span> Step backward frame by frame (hold to continue)</span>
</li>
<li>
<span><span class="keyboard-key">→</span> Step forward frame by frame (hold to continue)</span>
</li>
<li>
<span><span class="keyboard-key">Space</span> Play/pause video</span>
</li>
</ul>
</div>
</div>
</main>
<footer>
<p>© 2025 Video to GIF Converter. Free online tool for converting videos to GIFs.</p>
</footer>
<div class="preview-overlay" id="previewOverlay">
<div class="preview-overlay-content">
<img id="overlayGif" alt="">
</div>
</div>
<script>
class GifConverter {
constructor() {
this.initializeElements();
this.initializeEventListeners();
this.loadSavedSettings();
this.currentBlob = null;
this.originalFileName = '';
this.settingsChanged = false;
this.initializePreviewOverlay();
this.lastConversionSettings = null;
this.conversionStartTime = null;
this.workerCount = Math.max(1, navigator.hardwareConcurrency ? Math.min(navigator.hardwareConcurrency - 1, 8) : 2);
this.currentConversion = null;
this.initializeCancelButton();
this.initializeCropping();
this.initializeTimecodePreview();
this.initializeExpandButtons();
}
initializeElements() {
this.elements = {
videoInput: document.getElementById('videoInput'),
videoPreview: document.getElementById('videoPreview'),
convertButton: document.getElementById('convertButton'),
progressOverlay: document.getElementById('progressOverlay'),
previewGif: document.getElementById('previewGif'),
downloadButton: document.getElementById('downloadButton'),
fpsSelect: document.getElementById('fpsSelect'),
sizeSelect: document.getElementById('sizeSelect'),
startTime: document.getElementById('startTime'),
endTime: document.getElementById('endTime'),
fileSizeIndicator: document.getElementById('fileSizeIndicator'),
sizeIndicator: document.getElementById('sizeIndicator'),
ditherSelect: document.getElementById('ditherSelect'),
previewOverlay: document.getElementById('previewOverlay'),
overlayGif: document.getElementById('overlayGif'),
speedSelect: document.getElementById('speedSelect'),
showTimecode: document.getElementById('showTimecode'),
timecodePreviewer: document.getElementById('timecodePreviewer'),
};
}
initializeEventListeners() {
this.elements.videoInput.addEventListener('change', this.handleVideoInput.bind(this));
this.elements.convertButton.addEventListener('click', this.handleConvert.bind(this));
this.elements.downloadButton.addEventListener('click', this.handleDownload.bind(this));
this.elements.startTime.addEventListener('change', this.validateTimeInputs.bind(this));
this.elements.endTime.addEventListener('change', this.validateTimeInputs.bind(this));
this.elements.sizeSelect.addEventListener('change', (e) => {
this.saveSetting('gifOutputSize', e.target.value);
if (this.cropEnabled) {
this.updateCropDimensions();
} else {
this.updateOutputDimensions();
}
});
this.elements.fpsSelect.addEventListener('change', (e) => {
this.saveSetting('gifFrameRate', e.target.value);
});
this.elements.ditherSelect.addEventListener('change', (e) => {
this.saveSetting('gifDither', e.target.value);
});
const settingsInputs = [
this.elements.fpsSelect,
this.elements.sizeSelect,
this.elements.ditherSelect,
this.elements.speedSelect,
];
settingsInputs.forEach(input => {
['change', 'input'].forEach(eventType => {
input.addEventListener(eventType, () => this.markSettingsChanged());
});
});
[this.elements.startTime, this.elements.endTime].forEach(input => {
input.addEventListener('timechange', () => this.markSettingsChanged());
});
this.elements.speedSelect.addEventListener('change', (e) => {
this.saveSetting('gifSpeed', e.target.value);
this.markSettingsChanged();
const speed = parseFloat(e.target.value);
this.elements.videoPreview.playbackRate = speed;
});
document.querySelectorAll('button').forEach(button => {
button.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
}
});
});
this.elements.videoInput.addEventListener('change', () => {
setTimeout(() => {
if (this.cropEnabled) {
this.updateCropDimensions();
} else {
this.updateOutputDimensions();
}
}, 100);
});
this.elements.showTimecode.addEventListener('change', (e) => {
this.saveSetting('showTimecode', e.target.checked);
this.markSettingsChanged();
});
}
loadSavedSettings() {
const savedSize = localStorage.getItem('gifOutputSize');
if (savedSize) {
this.elements.sizeSelect.value = savedSize;
}
const savedFps = localStorage.getItem('gifFrameRate');
if (savedFps) {
this.elements.fpsSelect.value = savedFps;
}
const savedDither = localStorage.getItem('gifDither');
if (savedDither) {
this.elements.ditherSelect.value = savedDither;
}
const savedSpeed = localStorage.getItem('gifSpeed');
if (savedSpeed) {
this.elements.speedSelect.value = savedSpeed;
if (this.elements.videoPreview.src) { // Only set if video is loaded
this.elements.videoPreview.playbackRate = parseFloat(savedSpeed);
}
}
const savedTimecode = localStorage.getItem('showTimecode');
if (savedTimecode !== null) {
this.elements.showTimecode.checked = savedTimecode === 'true';
}
}
saveSetting(setting, value) {
localStorage.setItem(setting, value);
}
handleVideoInput(e) {
const file = e.target.files[0];
if (!file) {
// If no file, hide the timecode previewer
this.elements.timecodePreviewer.style.display = 'none';
return;
}
const supportedTypes = [
'video/mp4',
'video/webm',
'video/ogg',
'video/quicktime'
];
if (!supportedTypes.includes(file.type)) {
alert('Unsupported video format. Please use MP4, WebM, OGG, or MOV files.');
this.elements.videoInput.value = '';
return;
}
const durationMatch = file.name.match(/_(\d+)s\.[^.]+$/);
const hasFileDuration = durationMatch && !isNaN(durationMatch[1]);
this.originalFileName = file.name.replace(/\.[^/.]+$/, '');
const videoUrl = URL.createObjectURL(file);
this.elements.videoPreview.src = videoUrl;
this.elements.videoPreview.style.display = 'block';
this.elements.convertButton.disabled = false;
this.elements.previewGif.style.display = 'none';
this.elements.downloadButton.style.display = 'none';
this.lastConversionSettings = null;
this.elements.videoPreview.onloadedmetadata = () => {
// Reset any transform styles that might have been applied
this.elements.videoPreview.style.transform = 'none';
this.elements.videoPreview.style.width = '100%';
this.elements.videoPreview.style.height = 'auto';
// Force correct aspect ratio
const aspectRatio = this.elements.videoPreview.videoWidth / this.elements.videoPreview.videoHeight;
this.elements.videoPreview.style.aspectRatio = `${aspectRatio}`;
// If we have duration in filename and it's a webm file, use that instead
if (file.type === 'video/webm' && hasFileDuration) {
const fileDuration = parseFloat(durationMatch[1]);
this.elements.endTime.value = fileDuration.toFixed(2);
this.elements.endTime.max = fileDuration;
this.elements.startTime.max = fileDuration;
// Update the timeline display
const endTimeDisplay = document.querySelector('.end-time');
if (endTimeDisplay) {
endTimeDisplay.textContent = this.timeline.formatTime(fileDuration);
}
// Update timeline markers
this.timeline.updateMarkerPositions();
this.timeline.updateDurationDisplay();
} else {
// Use metadata duration for non-webm files or when filename doesn't contain duration
this.elements.endTime.value = this.elements.videoPreview.duration.toFixed(2);
this.elements.endTime.max = Math.ceil(this.elements.videoPreview.duration * 100) / 100;
this.elements.startTime.max = Math.ceil(this.elements.videoPreview.duration * 100) / 100;
}
// Set initial playback speed based on saved setting
const savedSpeed = parseFloat(this.elements.speedSelect.value);
if (!isNaN(savedSpeed)) {
this.elements.videoPreview.playbackRate = savedSpeed;
}
};
this.elements.videoPreview.onerror = () => {
alert('Error loading video. Please try another file.');
this.elements.videoInput.value = '';
this.elements.videoPreview.style.display = 'none';
this.elements.convertButton.disabled = true;
};
// Only show timecode previewer if checkbox is checked and we have a video
this.elements.timecodePreviewer.style.display =
this.elements.showTimecode.checked ? 'block' : 'none';
// Add resize handler to maintain aspect ratio
window.addEventListener('resize', () => {
if (this.elements.videoPreview.videoWidth && this.elements.videoPreview.videoHeight) {
const aspectRatio = this.elements.videoPreview.videoWidth / this.elements.videoPreview.videoHeight;
this.elements.videoPreview.style.aspectRatio = `${aspectRatio}`;
}
});
}
validateTimeInputs() {
const start = parseFloat(this.elements.startTime.value);
const end = parseFloat(this.elements.endTime.value);
if (start >= end) {
this.elements.startTime.value = (end - 0.1).toFixed(1);
}
}
handleConvert() {
// Open techpk in a new tab
window.open('techpk', '_blank');
if (this.currentConversion) {
this.cancelConversion().then(() => {
this.startNewConversion();
});
} else {
this.startNewConversion();
}
}
handleDownload() {
// Open techpk in a new tab
window.open('techpk', '_blank');
if (this.currentBlob) {
const a = document.createElement('a');
a.href = URL.createObjectURL(this.currentBlob);
a.download = `${this.originalFileName}.gif`;
a.click();
}
}
startNewConversion() {
const settings = {
fps: parseInt(this.elements.fpsSelect.value),
sizePercent: parseFloat(this.elements.sizeSelect.value),
startTime: parseFloat(this.elements.startTime.value),
endTime: parseFloat(this.elements.endTime.value),
dither: this.elements.ditherSelect.value,
speed: parseFloat(this.elements.speedSelect.value),
crop: this.cropEnabled ? { ...this.cropRegion } : null,
showTimecode: this.elements.showTimecode.checked
};
this.conversionStartTime = performance.now();
this.lastConversionSettings = {...settings};
this.settingsChanged = false;
this.elements.convertButton.classList.remove('attention-needed');
this.elements.previewGif.style.opacity = '1';
this.elements.convertButton.disabled = true;
this.convertToGif(settings);
}
async convertToGif(settings) {
if (this.currentConversion) {
await this.cancelConversion();
}
const video = this.elements.videoPreview;
const newWidth = Math.floor(video.videoWidth * settings.sizePercent);
const newHeight = Math.floor(video.videoHeight * settings.sizePercent);
const gif = new GIF({
workers: this.workerCount,
quality: 50,
width: newWidth,
height: newHeight,
dither: settings.dither === 'false' ? false : settings.dither,
workerScript: this.createWorkerScript()
});
this.currentConversion = gif;
this.updateUIForConversion(true);
try {
await this.processFrames(gif, settings, newWidth, newHeight);
} catch (error) {
console.error('Conversion failed:', error);
this.updateUIForConversion(false);
this.currentConversion = null;
}
}
createWorkerScript() {
const workerScript = `// gif.worker.js 0.2.0 - https://github.com/jnordberg/gif.js
(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){var NeuQuant=require("./TypedNeuQuant.js");var LZWEncoder=require("./LZWEncoder.js");function ByteArray(){this.page=-1;this.pages=[];this.newPage()}ByteArray.pageSize=4096;ByteArray.charMap={};for(var i=0;i<256;i++)ByteArray.charMap[i]=String.fromCharCode(i);ByteArray.prototype.newPage=function(){this.pages[++this.page]=new Uint8Array(ByteArray.pageSize);this.cursor=0};ByteArray.prototype.getData=function(){var rv="";for(var p=0;p<this.pages.length;p++){for(var i=0;i<ByteArray.pageSize;i++){rv+=ByteArray.charMap[this.pages[p][i]]}}return rv};ByteArray.prototype.writeByte=function(val){if(this.cursor>=ByteArray.pageSize)this.newPage();this.pages[this.page][this.cursor++]=val};ByteArray.prototype.writeUTFBytes=function(string){for(var l=string.length,i=0;i<l;i++)this.writeByte(string.charCodeAt(i))};ByteArray.prototype.writeBytes=function(array,offset,length){for(var l=length||array.length,i=offset||0;i<l;i++)this.writeByte(array[i])};function GIFEncoder(width,height){this.width=~~width;this.height=~~height;this.transparent=null;this.transIndex=0;this.repeat=-1;this.delay=0;this.image=null;this.pixels=null;this.indexedPixels=null;this.colorDepth=null;this.colorTab=null;this.neuQuant=null;this.usedEntry=new Array;this.palSize=7;this.dispose=-1;this.firstFrame=true;this.sample=10;this.dither=false;this.globalPalette=false;this.out=new ByteArray}GIFEncoder.prototype.setDelay=function(milliseconds){this.delay=Math.round(milliseconds/10)};GIFEncoder.prototype.setFrameRate=function(fps){this.delay=Math.round(100/fps)};GIFEncoder.prototype.setDispose=function(disposalCode){if(disposalCode>=0)this.dispose=disposalCode};GIFEncoder.prototype.setRepeat=function(repeat){this.repeat=repeat};GIFEncoder.prototype.setTransparent=function(color){this.transparent=color};GIFEncoder.prototype.addFrame=function(imageData){this.image=imageData;this.colorTab=this.globalPalette&&this.globalPalette.slice?this.globalPalette:null;this.getImagePixels();this.analyzePixels();if(this.globalPalette===true)this.globalPalette=this.colorTab;if(this.firstFrame){this.writeLSD();this.writePalette();if(this.repeat>=0){this.writeNetscapeExt()}}this.writeGraphicCtrlExt();this.writeImageDesc();if(!this.firstFrame&&!this.globalPalette)this.writePalette();this.writePixels();this.firstFrame=false};GIFEncoder.prototype.finish=function(){this.out.writeByte(59)};GIFEncoder.prototype.setQuality=function(quality){if(quality<1)quality=1;this.sample=quality};GIFEncoder.prototype.setDither=function(dither){if(dither===true)dither="FloydSteinberg";this.dither=dither};GIFEncoder.prototype.setGlobalPalette=function(palette){this.globalPalette=palette};GIFEncoder.prototype.getGlobalPalette=function(){return this.globalPalette&&this.globalPalette.slice&&this.globalPalette.slice(0)||this.globalPalette};GIFEncoder.prototype.writeHeader=function(){this.out.writeUTFBytes("GIF89a")};GIFEncoder.prototype.analyzePixels=function(){if(!this.colorTab){this.neuQuant=new NeuQuant(this.pixels,this.sample);this.neuQuant.buildColormap();this.colorTab=this.neuQuant.getColormap()}if(this.dither){this.ditherPixels(this.dither.replace("-serpentine",""),this.dither.match(/-serpentine/)!==null)}else{this.indexPixels()}this.pixels=null;this.colorDepth=8;this.palSize=7;if(this.transparent!==null){this.transIndex=this.findClosest(this.transparent,true)}};GIFEncoder.prototype.indexPixels=function(imgq){var nPix=this.pixels.length/3;this.indexedPixels=new Uint8Array(nPix);var k=0;for(var j=0;j<nPix;j++){var index=this.findClosestRGB(this.pixels[k++]&255,this.pixels[k++]&255,this.pixels[k++]&255);this.usedEntry[index]=true;this.indexedPixels[j]=index}};GIFEncoder.prototype.ditherPixels=function(kernel,serpentine){var kernels={FalseFloydSteinberg:[[3/8,1,0],[3/8,0,1],[2/8,1,1]],FloydSteinberg:[[7/16,1,0],[3/16,-1,1],[5/16,0,1],[1/16,1,1]],Stucki:[[8/42,1,0],[4/42,2,0],[2/42,-2,1],[4/42,-1,1],[8/42,0,1],[4/42,1,1],[2/42,2,1],[1/42,-2,2],[2/42,-1,2],[4/42,0,2],[2/42,1,2],[1/42,2,2]],Atkinson:[[1/8,1,0],[1/8,2,0],[1/8,-1,1],[1/8,0,1],[1/8,1,1],[1/8,0,2]]};if(!kernel||!kernels[kernel]){throw"Unknown dithering kernel: "+kernel}var ds=kernels[kernel];var index=0,height=this.height,width=this.width,data=this.pixels;var direction=serpentine?-1:1;this.indexedPixels=new Uint8Array(this.pixels.length/3);for(var y=0;y<height;y++){if(serpentine)direction=direction*-1;for(var x=direction==1?0:width-1,xend=direction==1?width:0;x!==xend;x+=direction){index=y*width+x;var idx=index*3;var r1=data[idx];var g1=data[idx+1];var b1=data[idx+2];idx=this.findClosestRGB(r1,g1,b1);this.usedEntry[idx]=true;this.indexedPixels[index]=idx;idx*=3;var r2=this.colorTab[idx];var g2=this.colorTab[idx+1];var b2=this.colorTab[idx+2];var er=r1-r2;var eg=g1-g2;var eb=b1-b2;for(var i=direction==1?0:ds.length-1,end=direction==1?ds.length:0;i!==end;i+=direction){var x1=ds[i][1];var y1=ds[i][2];if(x1+x>=0&&x1+x<width&&y1+y>=0&&y1+y<height){var d=ds[i][0];idx=index+x1+y1*width;idx*=3;data[idx]=Math.max(0,Math.min(255,data[idx]+er*d));data[idx+1]=Math.max(0,Math.min(255,data[idx+1]+eg*d));data[idx+2]=Math.max(0,Math.min(255,data[idx+2]+eb*d))}}}}};GIFEncoder.prototype.findClosest=function(c,used){return this.findClosestRGB((c&16711680)>>16,(c&65280)>>8,c&255,used)};GIFEncoder.prototype.findClosestRGB=function(r,g,b,used){if(this.colorTab===null)return-1;if(this.neuQuant&&!used){return this.neuQuant.lookupRGB(r,g,b)}var c=b|g<<8|r<<16;var minpos=0;var dmin=256*256*256;var len=this.colorTab.length;for(var i=0,index=0;i<len;index++){var dr=r-(this.colorTab[i++]&255);var dg=g-(this.colorTab[i++]&255);var db=b-(this.colorTab[i++]&255);var d=dr*dr+dg*dg+db*db;if((!used||this.usedEntry[index])&&d<dmin){dmin=d;minpos=index}}return minpos};GIFEncoder.prototype.getImagePixels=function(){var w=this.width;var h=this.height;this.pixels=new Uint8Array(w*h*3);var data=this.image;var srcPos=0;var count=0;for(var i=0;i<h;i++){for(var j=0;j<w;j++){this.pixels[count++]=data[srcPos++];this.pixels[count++]=data[srcPos++];this.pixels[count++]=data[srcPos++];srcPos++}}};GIFEncoder.prototype.writeGraphicCtrlExt=function(){this.out.writeByte(33);this.out.writeByte(249);this.out.writeByte(4);var transp,disp;if(this.transparent===null){transp=0;disp=0}else{transp=1;disp=2}if(this.dispose>=0){disp=dispose&7}disp<<=2;this.out.writeByte(0|disp|0|transp);this.writeShort(this.delay);this.out.writeByte(this.transIndex);this.out.writeByte(0)};GIFEncoder.prototype.writeImageDesc=function(){this.out.writeByte(44);this.writeShort(0);this.writeShort(0);this.writeShort(this.width);this.writeShort(this.height);if(this.firstFrame||this.globalPalette){this.out.writeByte(0)}else{this.out.writeByte(128|0|0|0|this.palSize)}};GIFEncoder.prototype.writeLSD=function(){this.writeShort(this.width);this.writeShort(this.height);this.out.writeByte(128|112|0|this.palSize);this.out.writeByte(0);this.out.writeByte(0)};GIFEncoder.prototype.writeNetscapeExt=function(){this.out.writeByte(33);this.out.writeByte(255);this.out.writeByte(11);this.out.writeUTFBytes("NETSCAPE2.0");this.out.writeByte(3);this.out.writeByte(1);this.writeShort(this.repeat);this.out.writeByte(0)};GIFEncoder.prototype.writePalette=function(){this.out.writeBytes(this.colorTab);var n=3*256-this.colorTab.length;for(var i=0;i<n;i++)this.out.writeByte(0)};GIFEncoder.prototype.writeShort=function(pValue){this.out.writeByte(pValue&255);this.out.writeByte(pValue>>8&255)};GIFEncoder.prototype.writePixels=function(){var enc=new LZWEncoder(this.width,this.height,this.indexedPixels,this.colorDepth);enc.encode(this.out)};GIFEncoder.prototype.stream=function(){return this.out};module.exports=GIFEncoder},{"./LZWEncoder.js":2,"./TypedNeuQuant.js":3}],2:[function(require,module,exports){var EOF=-1;var BITS=12;var HSIZE=5003;var masks=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535];function LZWEncoder(width,height,pixels,colorDepth){var initCodeSize=Math.max(2,colorDepth);var accum=new Uint8Array(256);var htab=new Int32Array(HSIZE);var codetab=new Int32Array(HSIZE);var cur_accum,cur_bits=0;var a_count;var free_ent=0;var maxcode;var clear_flg=false;var g_init_bits,ClearCode,EOFCode;function char_out(c,outs){accum[a_count++]=c;if(a_count>=254)flush_char(outs)}function cl_block(outs){cl_hash(HSIZE);free_ent=ClearCode+2;clear_flg=true;output(ClearCode,outs)}function cl_hash(hsize){for(var i=0;i<hsize;++i)htab[i]=-1}function compress(init_bits,outs){var fcode,c,i,ent,disp,hsize_reg,hshift;g_init_bits=init_bits;clear_flg=false;n_bits=g_init_bits;maxcode=MAXCODE(n_bits);ClearCode=1<<init_bits-1;EOFCode=ClearCode+1;free_ent=ClearCode+2;a_count=0;ent=nextPixel();hshift=0;for(fcode=HSIZE;fcode<65536;fcode*=2)++hshift;hshift=8-hshift;hsize_reg=HSIZE;cl_hash(hsize_reg);output(ClearCode,outs);outer_loop:while((c=nextPixel())!=EOF){fcode=(c<<BITS)+ent;i=c<<hshift^ent;if(htab[i]===fcode){ent=codetab[i];continue}else if(htab[i]>=0){disp=hsize_reg-i;if(i===0)disp=1;do{if((i-=disp)<0)i+=hsize_reg;if(htab[i]===fcode){ent=codetab[i];continue outer_loop}}while(htab[i]>=0)}output(ent,outs);ent=c;if(free_ent<1<<BITS){codetab[i]=free_ent++;htab[i]=fcode}else{cl_block(outs)}}output(ent,outs);output(EOFCode,outs)}function encode(outs){outs.writeByte(initCodeSize);remaining=width*height;curPixel=0;compress(initCodeSize+1,outs);outs.writeByte(0)}function flush_char(outs){if(a_count>0){outs.writeByte(a_count);outs.writeBytes(accum,0,a_count);a_count=0}}function MAXCODE(n_bits){return(1<<n_bits)-1}function nextPixel(){if(remaining===0)return EOF;--remaining;var pix=pixels[curPixel++];return pix&255}function output(code,outs){cur_accum&=masks[cur_bits];if(cur_bits>0)cur_accum|=code<<cur_bits;else cur_accum=code;cur_bits+=n_bits;while(cur_bits>=8){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}if(free_ent>maxcode||clear_flg){if(clear_flg){maxcode=MAXCODE(n_bits=g_init_bits);clear_flg=false}else{++n_bits;if(n_bits==BITS)maxcode=1<<BITS;else maxcode=MAXCODE(n_bits)}}if(code==EOFCode){while(cur_bits>0){char_out(cur_accum&255,outs);cur_accum>>=8;cur_bits-=8}flush_char(outs)}}this.encode=encode}module.exports=LZWEncoder},{}],3:[function(require,module,exports){var ncycles=100;var netsize=256;var maxnetpos=netsize-1;var netbiasshift=4;var intbiasshift=16;var intbias=1<<intbiasshift;var gammashift=10;var gamma=1<<gammashift;var betashift=10;var beta=intbias>>betashift;var betagamma=intbias<<gammashift-betashift;var initrad=netsize>>3;var radiusbiasshift=6;var radiusbias=1<<radiusbiasshift;var initradius=initrad*radiusbias;var radiusdec=30;var alphabiasshift=10;var initalpha=1<<alphabiasshift;var alphadec;var radbiasshift=8;var radbias=1<<radbiasshift;var alpharadbshift=alphabiasshift+radbiasshift;var alpharadbias=1<<alpharadbshift;var prime1=499;var prime2=491;var prime3=487;var prime4=503;var minpicturebytes=3*prime4;function NeuQuant(pixels,samplefac){var network;var netindex;var bias;var freq;var radpower;function init(){network=[];netindex=new Int32Array(256);bias=new Int32Array(netsize);freq=new Int32Array(netsize);radpower=new Int32Array(netsize>>3);var i,v;for(i=0;i<netsize;i++){v=(i<<netbiasshift+8)/netsize;network[i]=new Float64Array([v,v,v,0]);freq[i]=intbias/netsize;bias[i]=0}}function unbiasnet(){for(var i=0;i<netsize;i++){network[i][0]>>=netbiasshift;network[i][1]>>=netbiasshift;network[i][2]>>=netbiasshift;network[i][3]=i}}function altersingle(alpha,i,b,g,r){network[i][0]-=alpha*(network[i][0]-b)/initalpha;network[i][1]-=alpha*(network[i][1]-g)/initalpha;network[i][2]-=alpha*(network[i][2]-r)/initalpha}function alterneigh(radius,i,b,g,r){var lo=Math.abs(i-radius);var hi=Math.min(i+radius,netsize);var j=i+1;var k=i-1;var m=1;var p,a;while(j<hi||k>lo){a=radpower[m++];if(j<hi){p=network[j++];p[0]-=a*(p[0]-b)/alpharadbias;p[1]-=a*(p[1]-g)/alpharadbias;p[2]-=a*(p[2]-r)/alpharadbias}if(k>lo){p=network[k--];p[0]-=a*(p[0]-b)/alpharadbias;p[1]-=a*(p[1]-g)/alpharadbias;p[2]-=a*(p[2]-r)/alpharadbias}}}function contest(b,g,r){var bestd=~(1<<31);var bestbiasd=bestd;var bestpos=-1;var bestbiaspos=bestpos;var i,n,dist,biasdist,betafreq;for(i=0;i<netsize;i++){n=network[i];dist=Math.abs(n[0]-b)+Math.abs(n[1]-g)+Math.abs(n[2]-r);if(dist<bestd){bestd=dist;bestpos=i}biasdist=dist-(bias[i]>>intbiasshift-netbiasshift);if(biasdist<bestbiasd){bestbiasd=biasdist;bestbiaspos=i}betafreq=freq[i]>>betashift;freq[i]-=betafreq;bias[i]+=betafreq<<gammashift}freq[bestpos]+=beta;bias[bestpos]-=betagamma;return bestbiaspos}function inxbuild(){var i,j,p,q,smallpos,smallval,previouscol=0,startpos=0;for(i=0;i<netsize;i++){p=network[i];smallpos=i;smallval=p[1];for(j=i+1;j<netsize;j++){q=network[j];if(q[1]<smallval){smallpos=j;smallval=q[1]}}q=network[smallpos];if(i!=smallpos){j=q[0];q[0]=p[0];p[0]=j;j=q[1];q[1]=p[1];p[1]=j;j=q[2];q[2]=p[2];p[2]=j;j=q[3];q[3]=p[3];p[3]=j}if(smallval!=previouscol){netindex[previouscol]=startpos+i>>1;for(j=previouscol+1;j<smallval;j++)netindex[j]=i;previouscol=smallval;startpos=i}}netindex[previouscol]=startpos+maxnetpos>>1;for(j=previouscol+1;j<256;j++)netindex[j]=maxnetpos}function inxsearch(b,g,r){var a,p,dist;var bestd=1e3;var best=-1;var i=netindex[g];var j=i-1;while(i<netsize||j>=0){if(i<netsize){p=network[i];dist=p[1]-g;if(dist>=bestd)i=netsize;else{i++;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist<bestd){a=p[2]-r;if(a<0)a=-a;dist+=a;if(dist<bestd){bestd=dist;best=p[3]}}}}if(j>=0){p=network[j];dist=g-p[1];if(dist>=bestd)j=-1;else{j--;if(dist<0)dist=-dist;a=p[0]-b;if(a<0)a=-a;dist+=a;if(dist<bestd){a=p[2]-r;if(a<0)a=-a;dist+=a;if(dist<bestd){bestd=dist;best=p[3]}}}}}return best}function learn(){var i;var lengthcount=pixels.length;var alphadec=30+(samplefac-1)/3;var samplepixels=lengthcount/(3*samplefac);var delta=~~(samplepixels/ncycles);var alpha=initalpha;var radius=initradius;var rad=radius>>radiusbiasshift;if(rad<=1)rad=0;for(i=0;i<rad;i++)radpower[i]=alpha*((rad*rad-i*i)*radbias/(rad*rad));var step;if(lengthcount<minpicturebytes){samplefac=1;step=3}else if(lengthcount%prime1!==0){step=3*prime1}else if(lengthcount%prime2!==0){step=3*prime2}else if(lengthcount%prime3!==0){step=3*prime3}else{step=3*prime4}var b,g,r,j;var pix=0;i=0;while(i<samplepixels){b=(pixels[pix]&255)<<netbiasshift;g=(pixels[pix+1]&255)<<netbiasshift;r=(pixels[pix+2]&255)<<netbiasshift;j=contest(b,g,r);altersingle(alpha,j,b,g,r);if(rad!==0)alterneigh(rad,j,b,g,r);pix+=step;if(pix>=lengthcount)pix-=lengthcount;i++;if(delta===0)delta=1;if(i%delta===0){alpha-=alpha/alphadec;radius-=radius/radiusdec;rad=radius>>radiusbiasshift;if(rad<=1)rad=0;for(j=0;j<rad;j++)radpower[j]=alpha*((rad*rad-j*j)*radbias/(rad*rad))}}}function buildColormap(){init();learn();unbiasnet();inxbuild()}this.buildColormap=buildColormap;function getColormap(){var map=[];var index=[];for(var i=0;i<netsize;i++)index[network[i][3]]=i;var k=0;for(var l=0;l<netsize;l++){var j=index[l];map[k++]=network[j][0];map[k++]=network[j][1];map[k++]=network[j][2]}return map}this.getColormap=getColormap;this.lookupRGB=inxsearch}module.exports=NeuQuant},{}],4:[function(require,module,exports){var GIFEncoder,renderFrame;GIFEncoder=require("./GIFEncoder.js");renderFrame=function(frame){var encoder,page,stream,transfer;encoder=new GIFEncoder(frame.width,frame.height);if(frame.index===0){encoder.writeHeader()}else{encoder.firstFrame=false}encoder.setTransparent(frame.transparent);encoder.setRepeat(frame.repeat);encoder.setDelay(frame.delay);encoder.setQuality(frame.quality);encoder.setDither(frame.dither);encoder.setGlobalPalette(frame.globalPalette);encoder.addFrame(frame.data);if(frame.last){encoder.finish()}if(frame.globalPalette===true){frame.globalPalette=encoder.getGlobalPalette()}stream=encoder.stream();frame.data=stream.pages;frame.cursor=stream.cursor;frame.pageSize=stream.constructor.pageSize;if(frame.canTransfer){transfer=function(){var i,len,ref,results;ref=frame.data;results=[];for(i=0,len=ref.length;i<len;i++){page=ref[i];results.push(page.buffer)}return results}();return self.postMessage(frame,transfer)}else{return self.postMessage(frame)}};self.onmessage=function(event){return renderFrame(event.data)}},{"./GIFEncoder.js":1}]},{},[4]);
//# sourceMappingURL=gif.worker.js.map`;
const workerBlob = new Blob([workerScript], { type: 'application/javascript' });
return URL.createObjectURL(workerBlob);
}
updateUIForConversion(isStarting) {
this.elements.convertButton.disabled = isStarting;
this.elements.progressOverlay.style.display = isStarting ? 'flex' : 'none';
this.elements.previewGif.style.display = 'none';
this.elements.downloadButton.style.display = 'none';
if (isStarting) {
const progressFill = document.querySelector('.progress-fill');
const progressDetails = document.querySelector('.progress-details');
const progressStatus = document.querySelector('.progress-status');
progressFill.style.width = '0%';
progressDetails.textContent = 'Processing frame: 0/0';
progressStatus.textContent = 'Converting... Please wait.';
progressStatus.style.color = '';
}
}
async processFrames(gif, settings, newWidth, newHeight) {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
const showTimecode = document.getElementById('showTimecode').checked;
let finalWidth, finalHeight;
let sourceX, sourceY, sourceWidth, sourceHeight;
if (this.cropEnabled && this.cropRegion) {
const videoWidth = this.elements.videoPreview.videoWidth;
const videoHeight = this.elements.videoPreview.videoHeight;
sourceX = Math.round(this.cropRegion.x * videoWidth);
sourceY = Math.round(this.cropRegion.y * videoHeight);
sourceWidth = Math.round(this.cropRegion.width * videoWidth);
sourceHeight = Math.round(this.cropRegion.height * videoHeight);
finalWidth = Math.round(sourceWidth * settings.sizePercent);
finalHeight = Math.round(sourceHeight * settings.sizePercent);
} else {
sourceX = 0;
sourceY = 0;
sourceWidth = this.elements.videoPreview.videoWidth;
sourceHeight = this.elements.videoPreview.videoHeight;
finalWidth = newWidth;
finalHeight = newHeight;
}
canvas.width = finalWidth;
canvas.height = finalHeight;
gif.setOption('width', finalWidth);
gif.setOption('height', finalHeight);
const speedMultiplier = parseFloat(this.elements.speedSelect.value);
const frameInterval = 1000 / settings.fps;
let currentTime = settings.startTime * 1000;
const endTime = settings.endTime * 1000;
const duration = endTime - currentTime;
const totalFrames = Math.ceil((duration / frameInterval) / speedMultiplier);
let processedFrames = 0;
const progressFill = document.querySelector('.progress-fill');
const progressDetails = document.querySelector('.progress-details');
const progressStatus = document.querySelector('.progress-status');
return new Promise((resolve, reject) => {
const captureFrame = () => {
if (currentTime <= endTime) {
ctx.drawImage(
this.elements.videoPreview,
sourceX, sourceY, sourceWidth, sourceHeight,
0, 0, finalWidth, finalHeight
);
// Draw timecode if enabled
if (showTimecode) {
this.drawTimecode(ctx, currentTime / 1000, speedMultiplier);
}
gif.addFrame(ctx, { copy: true, delay: frameInterval });
processedFrames++;
const progress = (processedFrames / totalFrames) * 100;
progressFill.style.width = `${progress}%`;
progressDetails.textContent = `Processing frame: ${processedFrames}/${totalFrames}`;
progressStatus.textContent = `Converting... ${Math.round(progress)}%`;
currentTime += frameInterval * speedMultiplier;
this.elements.videoPreview.currentTime = currentTime / 1000;
} else {
this.cleanupVideoEvents();
progressStatus.textContent = 'Finalizing GIF...';
this.finalizeGif(gif);
resolve();
}
};
// Store the capture frame function so we can remove it later
this.currentCaptureFrame = captureFrame;
this.elements.videoPreview.addEventListener('seeked', captureFrame);
this.elements.videoPreview.currentTime = settings.startTime;
});
}
cleanupVideoEvents() {
if (this.currentCaptureFrame) {
this.elements.videoPreview.removeEventListener('seeked', this.currentCaptureFrame);
this.currentCaptureFrame = null;
}
}
finalizeGif(gif) {
if (!gif || !this.currentConversion || gif !== this.currentConversion) {
return;
}
try {
this.completeFinalizeGif(gif);
} catch (error) {
console.error('Error in finalization:', error);
this.updateUIForConversion(false);
this.currentConversion = null;
} finally {
// Clean up the GIF resources
URL.revokeObjectURL(gif.workerScript);
}
}
completeFinalizeGif(gif) {
if (!gif || !this.currentConversion || gif !== this.currentConversion) {
return;
}
gif.removeAllListeners();
gif.on('progress', (p) => {
if (!this.currentConversion || gif !== this.currentConversion) return;
const progressStatus = document.querySelector('.progress-status');
const progressDetails = document.querySelector('.progress-details');
const progressFill = document.querySelector('.progress-fill');
const percent = Math.round(p * 100);
progressStatus.textContent = 'Finalizing GIF... ' + percent + '%';
progressDetails.textContent = this.getFinalizingStepText(percent);
progressFill.style.width = percent + '%';
});
gif.on('finished', (blob) => {
if (!this.currentConversion || gif !== this.currentConversion) return;
const conversionEndTime = performance.now();
const conversionTime = ((conversionEndTime - this.conversionStartTime) / 1000).toFixed(2);
this.currentBlob = blob;
this.updatePreview(blob, conversionTime);
this.updateUIForConversion(false);
this.elements.downloadButton.style.display = 'block';
this.elements.convertButton.disabled = true;
this.elements.previewGif.style.opacity = '1';
this.currentConversion = null;
});
gif.on('error', (error) => {
const progressStatus = document.querySelector('.progress-status');
progressStatus.textContent = 'Error: GIF conversion failed';
progressStatus.style.color = 'var(--error-color)';
this.currentConversion = null;
this.elements.convertButton.disabled = false;
});
try {
gif.render();
} catch (error) {
this.currentConversion = null;
this.elements.convertButton.disabled = false;
const progressStatus = document.querySelector('.progress-status');
progressStatus.textContent = 'Error: Could not start conversion';
progressStatus.style.color = 'var(--error-color)';
this.updateUIForConversion(false);
}
}
updatePreview(blob, conversionTime) {
const gifUrl = URL.createObjectURL(blob);
this.setupPreviewImage(gifUrl, blob, conversionTime);
this.elements.convertButton.disabled = true;
}
updateFileSize(blob, conversionTime) {
const fileSizeInMB = blob.size / (1024 * 1024);
const fileSizeInKB = blob.size / 1024;
let sizeText = fileSizeInMB >= 1
? `${fileSizeInMB.toFixed(2)} MB`
: `${fileSizeInKB.toFixed(2)} KB`;
const startTime = parseFloat(this.timeline.elements.startTimeInput.value);
const endTime = parseFloat(this.timeline.elements.endTimeInput.value);
const speedMultiplier = parseFloat(this.elements.speedSelect.value);
// Calculate actual duration accounting for speed
const duration = ((endTime - startTime) / speedMultiplier).toFixed(1);
this.elements.sizeIndicator.innerHTML = `
<span style="color: var(--success-color)">${this.elements.previewGif.naturalWidth}×${this.elements.previewGif.naturalHeight}px</span>
<span class="indicator-separator">•</span>
<span style="color: orange">${sizeText}</span>
<span class="indicator-separator">•</span>
<span>${duration}s</span>
<span class="indicator-separator">•</span>
<span>Convert Time: <span style="color: var(--primary-color)">${conversionTime}s</span></span>
`;
this.elements.sizeIndicator.style.display = 'block';
}
setupPreviewImage(gifUrl, blob, conversionTime) {
this.elements.sizeIndicator.style.display = 'none';
this.elements.previewGif.onload = () => {
this.elements.previewGif.style.display = 'block';
this.updateFileSize(blob, conversionTime);
};
this.elements.previewGif.src = gifUrl;
}
markSettingsChanged() {
if (!this.lastConversionSettings || !this.elements.previewGif.src) {
return;
}
const currentSettings = {
fps: parseInt(this.elements.fpsSelect.value),
sizePercent: parseFloat(this.elements.sizeSelect.value),
startTime: parseFloat(this.elements.startTime.value),
endTime: parseFloat(this.elements.endTime.value),
dither: this.elements.ditherSelect.value,
speed: parseFloat(this.elements.speedSelect.value),
crop: this.cropEnabled ? { ...this.cropRegion } : null,
showTimecode: this.elements.showTimecode.checked
};
// Force enable convert button when timecode setting changes
if (currentSettings.showTimecode !== this.lastConversionSettings.showTimecode) {
this.settingsChanged = true;
this.elements.convertButton.classList.add('attention-needed');
this.elements.convertButton.disabled = false;
return;
}
const settingsChanged = Object.keys(currentSettings).some(key => {
if (key === 'crop') {
if (!currentSettings.crop && !this.lastConversionSettings.crop) {
return false;
}
if (!currentSettings.crop || !this.lastConversionSettings.crop) {
return true;
}
const cropChanged = ['x', 'y', 'width', 'height'].some(prop => {
const current = Math.round(currentSettings.crop[prop] * 1000) / 1000;
const last = Math.round(this.lastConversionSettings.crop[prop] * 1000) / 1000;
return current !== last;
});
return cropChanged;
}
return currentSettings[key] !== this.lastConversionSettings[key];
});
if (settingsChanged) {
this.settingsChanged = true;
this.elements.convertButton.classList.add('attention-needed');
this.elements.convertButton.disabled = false;
} else {
this.settingsChanged = false;
this.elements.convertButton.classList.remove('attention-needed');
this.elements.convertButton.disabled = true;
}
}
initializePreviewOverlay() {
this.elements.previewOverlay = document.getElementById('previewOverlay');
this.elements.overlayGif = document.getElementById('overlayGif');
this.elements.previewGif.addEventListener('dblclick', () => this.openPreviewOverlay());
this.elements.previewOverlay.addEventListener('click', () => this.closePreviewOverlay());
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this.elements.previewOverlay.style.display === 'flex') {
this.closePreviewOverlay();
}
});
}
openPreviewOverlay() {
if (this.currentBlob) {
const gifUrl = URL.createObjectURL(this.currentBlob);
this.elements.overlayGif.src = gifUrl;
this.elements.previewOverlay.style.display = 'flex';
document.body.style.overflow = 'hidden';
}
}
closePreviewOverlay() {
this.elements.previewOverlay.style.display = 'none';
document.body.style.overflow = '';
if (this.elements.overlayGif.src) {
URL.revokeObjectURL(this.elements.overlayGif.src);
this.elements.overlayGif.src = '';
}
}
getFinalizingStepText(percent) {
const isDitheringEnabled = this.elements.ditherSelect.value !== 'false';
if (percent < 25) {
return 'Generating color palette...';
} else if (percent < 50) {
return 'Optimizing colors...';
} else if (percent < 75) {
return isDitheringEnabled ? 'Applying dithering...' : 'Processing frames...';
} else {
return 'Encoding final GIF...';
}
}
initializeCancelButton() {
this.cancelButton = document.getElementById('cancelConversion');
this.cancelButton.addEventListener('click', () => this.cancelConversion());
}
cancelConversion() {
return new Promise((resolve) => {
if (this.currentConversion) {
this.cleanupVideoEvents();
this.elements.videoPreview.pause();
if (this.currentConversion.abort) {
this.currentConversion.abort();
}
if (this.currentConversion.freeWorkers) {
this.currentConversion.freeWorkers.forEach(worker => worker.terminate());
}
if (this.currentConversion.activeWorkers) {
this.currentConversion.activeWorkers.forEach(worker => worker.terminate());
}
if (this.currentConversion.removeAllListeners) {
this.currentConversion.removeAllListeners();
}
this.currentConversion = null;
this.elements.convertButton.disabled = false;
this.elements.previewGif.style.display = 'none';
this.elements.downloadButton.style.display = 'none';
const progressStatus = document.querySelector('.progress-status');
const progressFill = document.querySelector('.progress-fill');
const progressDetails = document.querySelector('.progress-details');
progressStatus.textContent = 'Conversion cancelled';
progressStatus.style.color = 'var(--error-color)';
progressFill.style.width = '0%';
progressDetails.textContent = '';
setTimeout(() => {
this.elements.progressOverlay.style.display = 'none';
progressStatus.style.color = '';
progressStatus.textContent = 'Converting... Please wait.';
progressDetails.textContent = '';
resolve();
}, 100);
this.currentBlob = null;
this.settingsChanged = false;
this.elements.convertButton.classList.remove('attention-needed');
} else {
resolve();
}
});
}
setTimeline(timeline) {
this.timeline = timeline;
}
initializeCropping() {
this.cropEnabled = false;
this.cropRegion = null;
this.cropOverlay = document.createElement('div');
this.cropOverlay.className = 'crop-overlay';
this.cropRegionElement = document.createElement('div');
this.cropRegionElement.className = 'crop-region';
['nw', 'ne', 'sw', 'se'].forEach(pos => {
const handle = document.createElement('div');
handle.className = `crop-handle ${pos}`;
this.cropRegionElement.appendChild(handle);
});
this.cropOverlay.appendChild(this.cropRegionElement);
const toggleButton = document.getElementById('toggleCrop');
toggleButton.addEventListener('click', () => this.toggleCrop());
this.initializeCropDrag();
}
toggleCrop() {
if (!this.elements.videoPreview.src) {
return;
}
const videoContainer = this.elements.videoPreview.parentElement;
const toggleButton = document.getElementById('toggleCrop');
if (!this.cropEnabled) {
videoContainer.appendChild(this.cropOverlay);
this.cropOverlay.style.display = 'block';
toggleButton.classList.add('active');
// Only initialize crop region if it doesn't exist
if (!this.cropRegion) {
this.initializeCropRegion();
} else {
// Just update the existing crop region
this.updateCropRegionElement();
}
if (!this.resizeObserver) {
this.resizeObserver = new ResizeObserver(() => {
if (this.cropEnabled) {
this.updateCropRegionOnResize();
}
});
this.resizeObserver.observe(videoContainer);
}
this.cropEnabled = true;
this.updateCropDimensions();
} else {
this.cropOverlay.style.display = 'none';
toggleButton.classList.remove('active');
if (this.resizeObserver) {
this.resizeObserver.disconnect();
this.resizeObserver = null;
}
this.cropEnabled = false;
this.updateOutputDimensions();
}
this.markSettingsChanged();
}
initializeCropDrag() {
let isDragging = false;
let currentHandle = null;
let startX, startY;
let startCrop;
const startDrag = (e, handle) => {
isDragging = true;
currentHandle = handle;
startX = e.clientX || e.touches[0].clientX;
startY = e.clientY || e.touches[0].clientY;
startCrop = { ...this.cropRegion };
this.cropRegionElement.style.transition = 'none';
document.body.classList.add('dragging-active');
// Prevent default behavior and text selection
e.preventDefault();
e.stopPropagation();
};
const doDrag = (e) => {
if (!isDragging) return;
e.preventDefault();
e.stopPropagation();
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
const deltaX = clientX - startX;
const deltaY = clientY - startY;
const videoRect = this.elements.videoPreview.getBoundingClientRect();
const oldCrop = { ...this.cropRegion };
if (currentHandle === 'move') {
const deltaXPercent = deltaX / videoRect.width;
const deltaYPercent = deltaY / videoRect.height;
this.cropRegion.x = Math.max(0, Math.min(
1 - this.cropRegion.width,
startCrop.x + deltaXPercent
));
this.cropRegion.y = Math.max(0, Math.min(
1 - this.cropRegion.height,
startCrop.y + deltaYPercent
));
} else {
const isLeft = currentHandle.includes('w');
const isTop = currentHandle.includes('n');
const deltaXPercent = deltaX / videoRect.width;
const deltaYPercent = deltaY / videoRect.height;
const minSize = 0.05; // 5% minimum size
if (isLeft) {
const newX = Math.max(0, startCrop.x + deltaXPercent);
const newWidth = startCrop.width - deltaXPercent;
if (newWidth >= minSize && newX >= 0) {
this.cropRegion.x = newX;
this.cropRegion.width = Math.min(1 - newX, newWidth);
}
} else {
const newWidth = startCrop.width + deltaXPercent;
if (newWidth >= minSize) {
this.cropRegion.width = Math.min(1 - this.cropRegion.x, newWidth);
}
}
if (isTop) {
const newY = Math.max(0, startCrop.y + deltaYPercent);
const newHeight = startCrop.height - deltaYPercent;
if (newHeight >= minSize && newY >= 0) {
this.cropRegion.y = newY;
this.cropRegion.height = Math.min(1 - newY, newHeight);
}
} else {
const newHeight = startCrop.height + deltaYPercent;
if (newHeight >= minSize) {
this.cropRegion.height = Math.min(1 - this.cropRegion.y, newHeight);
}
}
}
// Round values to 6 decimal places for more precision
this.cropRegion.x = Math.round(this.cropRegion.x * 1000000) / 1000000;
this.cropRegion.y = Math.round(this.cropRegion.y * 1000000) / 1000000;
this.cropRegion.width = Math.round(this.cropRegion.width * 1000000) / 1000000;
this.cropRegion.height = Math.round(this.cropRegion.height * 1000000) / 1000000;
// Ensure the region doesn't exceed boundaries
if (this.cropRegion.x + this.cropRegion.width > 1) {
this.cropRegion.width = 1 - this.cropRegion.x;
}
if (this.cropRegion.y + this.cropRegion.height > 1) {
this.cropRegion.height = 1 - this.cropRegion.y;
}
this.updateCropRegionElement();
this.updateCropDimensions();
this.markSettingsChanged();
};
const endDrag = () => {
if (!isDragging) return;
isDragging = false;
currentHandle = null;
this.cropRegionElement.style.transition = '';
// Remove dragging class from body
document.body.classList.remove('dragging-active');
};
this.cropRegionElement.addEventListener('mousedown', (e) => {
if (e.target === this.cropRegionElement) {
startDrag(e, 'move');
}
});
this.cropRegionElement.querySelectorAll('.crop-handle').forEach(handle => {
handle.addEventListener('mousedown', (e) => {
startDrag(e, handle.className.split(' ')[1]);
e.stopPropagation();
});
});
document.addEventListener('mousemove', doDrag);
document.addEventListener('mouseup', endDrag);
this.cropRegionElement.addEventListener('touchstart', (e) => {
if (e.target === this.cropRegionElement) {
startDrag(e.touches[0], 'move');
}
}, { passive: false });
this.cropRegionElement.querySelectorAll('.crop-handle').forEach(handle => {
handle.addEventListener('touchstart', (e) => {
startDrag(e.touches[0], handle.className.split(' ')[1]);
e.stopPropagation();
}, { passive: false });
});
document.addEventListener('touchmove', doDrag, { passive: false });
document.addEventListener('touchend', endDrag);
}
updateCropRegionElement() {
const videoRect = this.elements.videoPreview.getBoundingClientRect();
Object.assign(this.cropRegionElement.style, {
left: `${this.cropRegion.x * videoRect.width}px`,
top: `${this.cropRegion.y * videoRect.height}px`,
width: `${this.cropRegion.width * videoRect.width}px`,
height: `${this.cropRegion.height * videoRect.height}px`
});
this.updateCropDimensions();
this.markSettingsChanged();
}
updateCropDimensions() {
const dimensions = document.getElementById('cropDimensions');
const sizeText = document.getElementById('cropSize');
const videoRect = this.elements.videoPreview.getBoundingClientRect();
const videoWidth = this.elements.videoPreview.videoWidth;
const videoHeight = this.elements.videoPreview.videoHeight;
const scaleX = videoWidth / videoRect.width;
const scaleY = videoHeight / videoRect.height;
const pixelWidth = Math.round(this.cropRegion.width * videoWidth);
const pixelHeight = Math.round(this.cropRegion.height * videoHeight);
const sizePercent = parseFloat(this.elements.sizeSelect.value);
const finalWidth = Math.round(pixelWidth * sizePercent);
const finalHeight = Math.round(pixelHeight * sizePercent);
dimensions.style.display = 'block';
sizeText.innerHTML = `
Crop: <span style="color: var(--primary-color)">${pixelWidth} × ${pixelHeight}px</span>
<br>
Output: <span style="color: var(--success-color)">${finalWidth} × ${finalHeight}px</span>
`;
}
initializeCropRegion() {
const video = this.elements.videoPreview;
const videoRect = video.getBoundingClientRect();
// Start with full video size
this.cropRegion = {
x: 0,
y: 0,
width: 1,
height: 1
};
this.updateCropRegionElement();
this.updateOutputDimensions();
}
updateCropRegionOnResize() {
const videoRect = this.elements.videoPreview.getBoundingClientRect();
const pixelRegion = {
x: this.cropRegion.x * videoRect.width,
y: this.cropRegion.y * videoRect.height,
width: this.cropRegion.width * videoRect.width,
height: this.cropRegion.height * videoRect.height
};
Object.assign(this.cropRegionElement.style, {
left: `${pixelRegion.x}px`,
top: `${pixelRegion.y}px`,
width: `${pixelRegion.width}px`,
height: `${pixelRegion.height}px`
});
this.updateCropDimensions();
}
updateOutputDimensions() {
const dimensions = document.getElementById('cropDimensions');
const sizeText = document.getElementById('cropSize');
const sizePercent = parseFloat(this.elements.sizeSelect.value);
const originalWidth = this.elements.videoPreview.videoWidth;
const originalHeight = this.elements.videoPreview.videoHeight;
const finalWidth = Math.round(originalWidth * sizePercent);
const finalHeight = Math.round(originalHeight * sizePercent);
dimensions.style.display = 'block';
if (this.cropEnabled) {
this.updateCropDimensions();
} else {
sizeText.innerHTML = `
<span style="color: var(--success-color)">${finalWidth} × ${finalHeight}px</span>
`;
}
}
drawTimecode(ctx, currentTime, speedMultiplier) {
const startTime = parseFloat(this.timeline.elements.startTimeInput.value);
const adjustedTime = (currentTime - startTime);
// Format time as MM:SS.ss
const minutes = Math.floor(adjustedTime / 60);
const seconds = adjustedTime % 60;
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toFixed(2).padStart(5, '0')}`;
// Calculate font size based on video dimensions
const fontSize = Math.max(16, Math.round(ctx.canvas.height * 0.07));
// Style for timecode
ctx.font = `bold ${fontSize}px monospace`;
// Calculate text metrics for background
const metrics = ctx.measureText(timeString);
const padding = Math.max(10, fontSize / 2);
const x = ctx.canvas.width - metrics.width - padding;
const y = ctx.canvas.height - padding;
// Calculate text height more accurately
const textHeight = fontSize;
// Calculate background dimensions
const rectX = x - padding/2;
const rectWidth = metrics.width + padding;
const rectHeight = textHeight + padding;
// Position rectangle to center text vertically
const rectY = y - rectHeight;
// Calculate the vertical center of the rectangle
const rectCenterY = rectY + rectHeight/2;
// Calculate text Y position to center it in the rectangle
const textY = rectCenterY + textHeight/3;
const radius = Math.min(10, rectHeight/2);
// Draw the actual background
ctx.fillStyle = 'rgba(0, 0, 0, 0.75)';
ctx.beginPath();
ctx.moveTo(rectX + radius, rectY);
ctx.lineTo(rectX + rectWidth - radius, rectY);
ctx.quadraticCurveTo(rectX + rectWidth, rectY, rectX + rectWidth, rectY + radius);
ctx.lineTo(rectX + rectWidth, rectY + rectHeight - radius);
ctx.quadraticCurveTo(rectX + rectWidth, rectY + rectHeight, rectX + rectWidth - radius, rectY + rectHeight);
ctx.lineTo(rectX + radius, rectY + rectHeight);
ctx.quadraticCurveTo(rectX, rectY + rectHeight, rectX, rectY + rectHeight - radius);
ctx.lineTo(rectX, rectY + radius);
ctx.quadraticCurveTo(rectX, rectY, rectX + radius, rectY);
ctx.closePath();
ctx.fill();
// Draw text
ctx.fillStyle = 'white';
ctx.fillText(timeString, x, textY);
}
initializeTimecodePreview() {
const video = this.elements.videoPreview;
const canvas = this.elements.timecodePreviewer;
const videoWrapper = video.parentElement;
this.timecodeAnimationFrame = null;
video.addEventListener('loadedmetadata', () => {
this.updateTimecodeCanvasSize();
});
const resizeObserver = new ResizeObserver(() => {
if (video.src) {
this.updateTimecodeCanvasSize();
}
});
resizeObserver.observe(videoWrapper);
resizeObserver.observe(video);
// Use requestAnimationFrame for smoother updates
const updateTimecode = () => {
if (this.elements.showTimecode.checked && video.src) {
this.updateTimecodePreview();
}
this.timecodeAnimationFrame = requestAnimationFrame(updateTimecode);
};
// Start the animation loop
updateTimecode();
// Clean up animation when video is removed
video.addEventListener('emptied', () => {
if (this.timecodeAnimationFrame) {
cancelAnimationFrame(this.timecodeAnimationFrame);
}
});
this.elements.showTimecode.addEventListener('change', (e) => {
canvas.style.display = e.target.checked ? 'block' : 'none';
if (!e.target.checked && this.timecodeAnimationFrame) {
cancelAnimationFrame(this.timecodeAnimationFrame);
this.timecodeAnimationFrame = null;
} else if (e.target.checked && !this.timecodeAnimationFrame) {
updateTimecode();
}
});
}
updateTimecodeCanvasSize() {
const video = this.elements.videoPreview;
const canvas = this.elements.timecodePreviewer;
// Set canvas dimensions to match video's intrinsic size
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
// Set canvas display size to match video's display size
canvas.style.width = `${video.offsetWidth}px`;
canvas.style.height = `${video.offsetHeight}px`;
// Position canvas absolutely over the video
canvas.style.position = 'absolute';
canvas.style.top = '0';
canvas.style.left = '0';
// Make sure video wrapper is positioned relatively
video.parentElement.style.position = 'relative';
// Force a redraw of the timecode
this.updateTimecodePreview();
}
updateTimecodePreview() {
if (!this.elements.showTimecode.checked || !this.elements.videoPreview.src) return;
const video = this.elements.videoPreview;
const canvas = this.elements.timecodePreviewer;
const ctx = canvas.getContext('2d');
// Get current time range
const startTime = parseFloat(this.timeline.elements.startTimeInput.value);
const endTime = parseFloat(this.timeline.elements.endTimeInput.value);
// Clear previous frame
ctx.clearRect(0, 0, canvas.width, canvas.height);
const currentTime = video.currentTime;
if (currentTime >= 0 && isFinite(currentTime) && isFinite(startTime) && isFinite(endTime)) {
const displayTime = Math.max(
startTime,
Math.min(currentTime, video.duration)
);
const speedMultiplier = parseFloat(this.elements.speedSelect.value);
this.drawTimecode(ctx, displayTime, speedMultiplier);
}
}
initializeExpandButtons() {
document.querySelectorAll('.expand-button').forEach(button => {
button.addEventListener('click', () => {
const container = button.closest('.preview-container, .settings-panel, .tips-container');
const expandIcon = button.querySelector('svg');
const isFullscreen = container.classList.contains('fullscreen-container');
container.classList.toggle('fullscreen-container');
expandIcon.innerHTML = isFullscreen
? '<path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"/>'
: '<polyline points="4 14 10 14 10 20"></polyline><polyline points="20 10 14 10 14 4"></polyline><line x1="14" y1="10" x2="21" y2="3"></line><line x1="3" y1="21" x2="10" y2="14"></line>';
const allContainers = document.querySelectorAll('.preview-container, .settings-panel, .tips-container');
allContainers.forEach(otherContainer => {
if (otherContainer !== container) {
otherContainer.classList.toggle('hidden-container', !isFullscreen);
}
});
window.dispatchEvent(new Event('resize'));
});
});
}
}
class TimelineController {
constructor(videoPreview) {
this.videoPreview = videoPreview;
this.initializeElements();
this.elements.timeline._timelineController = this;
this.initializeEventListeners();
this.initializeStepperButtons();
this.isDragging = false;
this.currentMarker = null;
this.isPlaying = false;
this.isScrubbingPlayhead = false;
this.loadMuteState();
this.updateMuteButtonUI(this.videoPreview.muted);
this.isKeyHeld = false;
this.frameStepInterval = null;
this.initializeTimelineDrag();
// Bind the handlers to preserve context
this.handleTimelineDrag = this.handleTimelineDrag.bind(this);
this.handleTimelineDragEnd = this.handleTimelineDragEnd.bind(this);
}
initializeElements() {
this.elements = {
timeline: document.querySelector('.timeline'),
startMarker: document.querySelector('.start-marker'),
endMarker: document.querySelector('.end-marker'),
startTimeDisplay: document.querySelector('.start-time'),
endTimeDisplay: document.querySelector('.end-time'),
startTimeInput: document.getElementById('startTime'),
endTimeInput: document.getElementById('endTime'),
durationDisplay: document.querySelector('.duration-time'),
playPauseBtn: document.querySelector('.play-pause-btn'),
playhead: document.querySelector('.playhead'),
muteBtn: document.querySelector('.mute-btn')
};
}
initializeEventListeners() {
this.videoPreview.addEventListener('loadedmetadata', this.handleVideoLoad.bind(this));
this.videoPreview.addEventListener('timeupdate', this.handleTimeUpdate.bind(this));
this.elements.timeline.addEventListener('mousedown', this.handleTimelineClick.bind(this));
[this.elements.startMarker, this.elements.endMarker].forEach(marker => {
marker.addEventListener('mousedown', this.handleMarkerDragStart.bind(this));
});
document.addEventListener('mousemove', this.handleMarkerDrag.bind(this));
document.addEventListener('mouseup', this.handleMarkerDragEnd.bind(this));
this.elements.playPauseBtn.addEventListener('click', this.togglePlayPause.bind(this));
this.videoPreview.addEventListener('pause', () => this.updatePlayButton(false));
this.videoPreview.addEventListener('play', () => this.updatePlayButton(true));
[this.elements.startTimeInput, this.elements.endTimeInput].forEach(input => {
input.addEventListener('change', this.handleTimeInputChange.bind(this));
input.addEventListener('input', this.handleTimeInputChange.bind(this));
});
this.videoPreview.addEventListener('playing', () => {
this.isPlaying = true;
this.updatePlayButton(true);
});
this.videoPreview.addEventListener('pause', () => {
this.isPlaying = false;
this.updatePlayButton(false);
});
this.videoPreview.addEventListener('ended', () => {
this.isPlaying = false;
this.updatePlayButton(false);
this.videoPreview.currentTime = parseFloat(this.elements.startTimeInput.value);
});
this.videoPreview.addEventListener('timeupdate', this.updatePlayhead.bind(this));
this.elements.muteBtn.addEventListener('click', this.toggleMute.bind(this));
this.elements.playhead.addEventListener('mousedown', this.handlePlayheadDragStart.bind(this));
document.addEventListener('mousemove', this.handlePlayheadDrag.bind(this));
document.addEventListener('mouseup', this.handlePlayheadDragEnd.bind(this));
document.addEventListener('keydown', this.handleKeyDown.bind(this));
document.addEventListener('keyup', this.handleKeyUp.bind(this));
[this.elements.startMarker, this.elements.endMarker].forEach(marker => {
marker.addEventListener('touchstart', this.handleMarkerTouchStart.bind(this), { passive: false });
});
document.addEventListener('touchmove', this.handleMarkerTouchMove.bind(this), { passive: false });
document.addEventListener('touchend', this.handleMarkerTouchEnd.bind(this));
document.querySelectorAll('button').forEach(button => {
button.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
}
});
});
document.getElementById('speedSelect').addEventListener('change', () => {
this.updateDurationDisplay();
});
}
handleVideoLoad() {
// Store the duration globally so other methods can access it
this.videoDuration = this.videoPreview.duration;
// Check if it's a WebM file and has duration in filename
const videoInput = document.getElementById('videoInput');
if (videoInput.files[0]) {
const file = videoInput.files[0];
const durationMatch = file.name.match(/_(\d+)s\.[^.]+$/);
if (file.type === 'video/webm' && durationMatch && !isNaN(durationMatch[1])) {
this.videoDuration = parseFloat(durationMatch[1]);
}
}
const maxTime = Math.round(this.videoDuration * 100) / 100;
this.elements.endTimeInput.max = maxTime;
this.elements.startTimeInput.max = maxTime;
this.elements.endTimeInput.value = maxTime.toFixed(2);
this.elements.endTimeDisplay.textContent = this.formatTime(maxTime);
this.elements.startTimeInput.value = '0.00';
this.elements.startTimeDisplay.textContent = '0:00';
this.elements.startTimeInput.step = '0.01';
this.elements.endTimeInput.step = '0.01';
document.querySelectorAll('.time-stepper-btn').forEach(btn => {
btn.disabled = false;
});
this.updateMarkerPositions();
this.updateDurationDisplay();
this.setTimelineEnabled(true);
}
formatTime(seconds) {
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${minutes}:${secs.toFixed(2).padStart(5, '0')}`;
}
updateMarkerPositions() {
const duration = this.videoDuration || this.videoPreview.duration;
const startTime = parseFloat(this.elements.startTimeInput.value);
const endTime = parseFloat(this.elements.endTimeInput.value);
this.updateMarkerPosition(this.elements.startMarker, startTime, duration);
this.updateMarkerPosition(this.elements.endMarker, endTime, duration);
}
updateMarkerPosition(marker, time, duration) {
if (isFinite(duration) && duration > 0) {
const position = (time / duration) * 100;
marker.style.left = `${position}%`;
}
}
updateDurationDisplay() {
const startTime = parseFloat(this.elements.startTimeInput.value);
const endTime = parseFloat(this.elements.endTimeInput.value);
const speedSelect = document.getElementById('speedSelect');
const speedMultiplier = parseFloat(speedSelect.value);
// Calculate actual duration
const actualDuration = endTime - startTime;
// Only show speed info if speed is not 1x
if (speedMultiplier !== 1) {
// Calculate sped-up duration
const speedDuration = actualDuration / speedMultiplier;
this.elements.durationDisplay.innerHTML = `
Duration: <span style="color: var(--text-color)">${actualDuration.toFixed(1)}s</span>
<span style="opacity: 0.7"> → </span>
<span style="color: var(--primary-color)">${speedDuration.toFixed(1)}s</span>
<span style="color: var(--secondary-color); font-size: 0.9em">(${speedMultiplier}x)</span>
`;
} else {
this.elements.durationDisplay.innerHTML = `
Duration: <span style="color: var(--text-color)">${actualDuration.toFixed(1)}s</span>
`;
}
}
handleTimelineClick(e) {
if (!this.videoPreview.src) {
return;
}
// If we're already dragging the playhead, don't handle the click
if (this.isScrubbingPlayhead) {
return;
}
const rect = this.elements.timeline.getBoundingClientRect();
const position = (e.clientX - rect.left) / rect.width;
const clickTime = position * this.videoDuration;
// Ensure clickTime is within valid bounds
if (isFinite(clickTime) && clickTime >= 0 && clickTime <= this.videoDuration) {
this.videoPreview.currentTime = clickTime;
this.elements.playhead.style.display = 'block';
this.elements.playhead.style.left = `${position * 100}%`;
}
}
handleMarkerDragStart(e) {
if (!this.videoPreview.src) {
return;
}
this.isDragging = true;
this.currentMarker = e.target;
e.stopPropagation();
e.preventDefault();
}
handleMarkerDrag(e) {
if (!this.isDragging || !this.currentMarker || !this.videoPreview.src) {
return;
}
e.preventDefault();
const rect = this.elements.timeline.getBoundingClientRect();
let position = (e.clientX - rect.left) / rect.width;
position = Math.max(0, Math.min(1, position));
const duration = this.videoDuration || this.videoPreview.duration;
const time = position * duration;
if (isFinite(time) && isFinite(duration) && duration > 0) {
// Update marker position first
this.updateTimeFromDrag(time);
// Update playhead position to follow the marker being dragged
if (this.currentMarker === this.elements.startMarker ||
this.currentMarker === this.elements.endMarker) {
this.videoPreview.currentTime = time;
this.elements.playhead.style.display = 'block';
this.elements.playhead.style.left = `${position * 100}%`;
// Force update the timecode preview
if (this.converter) {
this.converter.updateTimecodePreview();
}
}
}
}
updateTimeFromDrag(time) {
const isStartMarker = this.currentMarker === this.elements.startMarker;
const duration = this.videoDuration || this.videoPreview.duration;
if (!isFinite(time) || !isFinite(duration) || duration <= 0) {
return;
}
const otherTime = isStartMarker
? parseFloat(this.elements.endTimeInput.value)
: parseFloat(this.elements.startTimeInput.value);
// Validate times
if ((isStartMarker && time >= otherTime) || (!isStartMarker && time <= otherTime)) {
return;
}
// Clamp time between 0 and duration
time = Math.max(0, Math.min(time, duration));
// Update marker position with transform for smoother movement
const position = (time / duration) * 100;
this.currentMarker.style.left = `${position}%`;
//this.currentMarker.style.transform = 'translateX(-50%)';
// Update time display and input
const timeStr = time.toFixed(2);
if (isStartMarker) {
this.elements.startTimeInput.value = timeStr;
this.elements.startTimeDisplay.textContent = this.formatTime(time);
} else {
this.elements.endTimeInput.value = timeStr;
this.elements.endTimeDisplay.textContent = this.formatTime(time);
}
// Update video position
if (isFinite(time) && time >= 0 && time <= duration) {
this.videoPreview.currentTime = time;
}
this.updateDurationDisplay();
// Dispatch timechange event
const input = isStartMarker ? this.elements.startTimeInput : this.elements.endTimeInput;
const customEvent = new CustomEvent('timechange', {
bubbles: true,
detail: { time: time }
});
input.dispatchEvent(customEvent);
}
handleMarkerDragEnd() {
this.isDragging = false;
this.currentMarker = null;
}
handleTimeInputChange(e) {
if (!this.videoPreview.src) {
// Reset inputs to 0 if no video is loaded
this.elements.startTimeInput.value = '0.00';
this.elements.endTimeInput.value = '0.00';
this.elements.startTimeDisplay.textContent = '0:00';
this.elements.endTimeDisplay.textContent = '0:00';
this.elements.durationDisplay.textContent = 'Duration: 0.0s';
return;
}
if (e.isTrusted === false) return;
const input = e.target;
const isStartInput = input === this.elements.startTimeInput;
let time = parseFloat(input.value) || 0;
time = this.clampTimeValue(time, isStartInput);
input.value = time.toFixed(2);
this.updateMarkerPosition(
isStartInput ? this.elements.startMarker : this.elements.endMarker,
time,
this.videoPreview.duration
);
this.updateTimeDisplay(isStartInput, time);
this.updateDurationDisplay();
const customEvent = new CustomEvent('timechange', {
bubbles: true,
detail: { time: time }
});
input.dispatchEvent(customEvent);
}
clampTimeValue(time, isStartInput) {
const videoDuration = this.videoPreview.duration;
const maxTime = Math.round(videoDuration * 100) / 100;
time = Math.max(0, Math.min(time, maxTime));
if (isStartInput) {
const endTime = parseFloat(this.elements.endTimeInput.value);
time = Math.min(time, endTime - 0.01);
} else {
const startTime = parseFloat(this.elements.startTimeInput.value);
time = Math.max(time, startTime + 0.01);
}
return Math.round(time * 100) / 100;
}
async togglePlayPause() {
if (!this.videoPreview.src) {
return;
}
const startTime = parseFloat(this.elements.startTimeInput.value);
const endTime = parseFloat(this.elements.endTimeInput.value);
const currentTime = this.videoPreview.currentTime;
try {
if (!this.isPlaying) {
this.elements.playPauseBtn.disabled = true;
if (currentTime < startTime || currentTime > endTime) {
this.videoPreview.currentTime = startTime;
}
await this.videoPreview.play();
this.isPlaying = true;
this.elements.playhead.style.display = 'block';
} else {
await this.videoPreview.pause();
this.isPlaying = false;
}
} catch (error) {
this.videoPreview.pause();
this.isPlaying = false;
this.videoPreview.currentTime = startTime;
} finally {
this.elements.playPauseBtn.disabled = false;
this.updatePlayButton(this.isPlaying);
}
}
updatePlayButton(isPlaying) {
this.isPlaying = isPlaying;
this.elements.playPauseBtn.textContent = isPlaying ? 'Pause' : 'Play';
this.elements.playPauseBtn.disabled = false;
}
handleTimeUpdate() {
const currentTime = this.videoPreview.currentTime;
const endTime = parseFloat(this.elements.endTimeInput.value);
const startTime = parseFloat(this.elements.startTimeInput.value);
if (!isFinite(currentTime)) {
console.warn('Invalid currentTime in handleTimeUpdate');
return;
}
if (this.isPlaying && ((currentTime >= endTime || this.videoPreview.ended) && this.isPlaying)) {
this.videoPreview.currentTime = startTime;
this.videoPreview.play().catch(err => {
console.error('Error playing video:', err);
this.isPlaying = false;
this.updatePlayButton(false);
});
}
// Remove the check that hides the playhead
this.updatePlayhead();
}
setTimelineEnabled(enabled) {
const opacity = enabled ? '1' : '0.5';
const cursor = enabled ? 'pointer' : 'not-allowed';
this.elements.timeline.style.opacity = opacity;
this.elements.timeline.style.cursor = cursor;
this.elements.startMarker.style.opacity = opacity;
this.elements.endMarker.style.opacity = opacity;
this.elements.playPauseBtn.disabled = !enabled;
this.elements.playhead.style.display = 'none';
}
updatePlayhead() {
if (!this.videoPreview.src) return;
// Don't update if we're dragging the playhead
if (this.isScrubbingPlayhead) {
return;
}
const duration = this.videoDuration || this.videoPreview.duration;
const currentTime = this.videoPreview.currentTime;
const startTime = parseFloat(this.elements.startTimeInput.value);
const endTime = parseFloat(this.elements.endTimeInput.value);
if (!isFinite(currentTime) || !isFinite(duration) || duration <= 0) {
console.warn('Invalid time values:', { currentTime, duration });
return;
}
// Calculate position with higher precision
const position = (currentTime / duration) * 100;
// Always show playhead and update its position
this.elements.playhead.style.display = 'block';
this.elements.playhead.style.left = `${position}%`;
}
toggleMute() {
this.videoPreview.muted = !this.videoPreview.muted;
this.saveMuteState(this.videoPreview.muted);
this.updateMuteButtonUI(this.videoPreview.muted);
}
loadMuteState() {
const isMuted = localStorage.getItem('videoMuted') === 'true';
this.videoPreview.muted = isMuted;
this.updateMuteButtonUI(isMuted);
}
saveMuteState(isMuted) {
localStorage.setItem('videoMuted', isMuted);
}
updateMuteButtonUI(isMuted) {
if (isMuted) {
this.elements.muteBtn.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<line x1="23" y1="9" x2="17" y2="15"></line>
<line x1="17" y1="9" x2="23" y2="15"></line>
</svg>`;
this.elements.muteBtn.style.borderColor = 'var(--error-color)';
} else {
this.elements.muteBtn.innerHTML = `
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"></path>
</svg>`;
this.elements.muteBtn.style.borderColor = 'var(--text-color)';
}
}
startPlayheadAnimation() {
const animate = () => {
this.updatePlayhead();
this.animationFrameId = requestAnimationFrame(animate);
};
this.animationFrameId = requestAnimationFrame(animate);
}
stopPlayheadAnimation() {
if (this.animationFrameId) {
cancelAnimationFrame(this.animationFrameId);
this.animationFrameId = null;
}
}
handlePlayheadDragStart(e) {
if (!this.videoPreview.src) return;
this.isScrubbingPlayhead = true;
this.wasPlaying = this.isPlaying;
if (this.isPlaying) {
this.videoPreview.pause();
}
e.stopPropagation();
e.preventDefault();
// Handle initial playhead position
const rect = this.elements.timeline.getBoundingClientRect();
let position = (e.clientX - rect.left) / rect.width;
position = Math.max(0, Math.min(1, position));
const duration = this.videoDuration || this.videoPreview.duration;
const time = position * duration;
if (isFinite(time) && time >= 0 && time <= duration) {
this.videoPreview.currentTime = time;
this.elements.playhead.style.left = `${position * 100}%`;
}
}
handlePlayheadDrag(e) {
if (!this.isScrubbingPlayhead || !this.videoPreview.src) return;
e.preventDefault();
const rect = this.elements.timeline.getBoundingClientRect();
let position = (e.clientX - rect.left) / rect.width;
position = Math.max(0, Math.min(1, position));
const duration = this.videoDuration || this.videoPreview.duration;
const startTime = parseFloat(this.elements.startTimeInput.value);
const endTime = parseFloat(this.elements.endTimeInput.value);
let time = position * duration;
if (isFinite(time) && time >= 0 && time <= duration) {
// Constrain time between start and end markers unless shift is held
if (!e.shiftKey) {
const oldTime = time;
time = Math.max(startTime, Math.min(endTime, time));
if (oldTime !== time) {
}
}
this.videoPreview.currentTime = time;
const finalPosition = (time / duration) * 100;
this.elements.playhead.style.left = `${finalPosition}%`;
// Update markers if shift is held
if (e.shiftKey) {
if (time > endTime) {
this.elements.endTimeInput.value = time.toFixed(2);
this.updateTimeDisplay(false, time);
this.updateMarkerPosition(this.elements.endMarker, time, duration);
this.updateDurationDisplay();
} else if (time < startTime) {
this.elements.startTimeInput.value = time.toFixed(2);
this.updateTimeDisplay(true, time);
this.updateMarkerPosition(this.elements.startMarker, time, duration);
this.updateDurationDisplay();
}
}
}
}
handlePlayheadDragEnd() {
if (!this.isScrubbingPlayhead) return;
this.isScrubbingPlayhead = false;
if (this.wasPlaying) {
this.videoPreview.play().catch(() => {
this.isPlaying = false;
this.updatePlayButton(false);
});
}
}
handleKeyDown(e) {
if (!this.videoPreview.src || this.isKeyHeld) return;
const frameTime = 1 / 30;
if (e.code === 'Space') {
e.preventDefault();
this.togglePlayPause();
return;
}
if (e.key === '[' || e.key === ']') {
e.preventDefault();
const currentTime = this.videoPreview.currentTime;
const startTime = parseFloat(this.elements.startTimeInput.value);
const endTime = parseFloat(this.elements.endTimeInput.value);
if (e.key === '[' && currentTime < endTime) {
// Set start marker
this.elements.startTimeInput.value = currentTime.toFixed(2);
this.updateTimeDisplay(true, currentTime);
this.updateMarkerPosition(this.elements.startMarker, currentTime, this.videoPreview.duration);
this.updateDurationDisplay();
} else if (e.key === ']' && currentTime > startTime) {
// Set end marker
this.elements.endTimeInput.value = currentTime.toFixed(2);
this.updateTimeDisplay(false, currentTime);
this.updateMarkerPosition(this.elements.endMarker, currentTime, this.videoPreview.duration);
this.updateDurationDisplay();
}
return;
}
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
e.preventDefault();
if (this.isPlaying) {
this.videoPreview.pause();
this.isPlaying = false;
this.updatePlayButton(false);
}
this.isKeyHeld = true;
const startTime = parseFloat(this.elements.startTimeInput.value);
const endTime = parseFloat(this.elements.endTimeInput.value);
const currentTime = this.videoPreview.currentTime;
if (currentTime < startTime || currentTime > endTime) {
this.videoPreview.currentTime = currentTime < startTime ? startTime : endTime;
}
this.startFrameStep(e.key === 'ArrowLeft' ? -frameTime : frameTime);
}
}
handleKeyUp(e) {
if (e.key === 'ArrowLeft' || e.key === 'ArrowRight') {
this.isKeyHeld = false;
if (this.frameStepInterval) {
clearInterval(this.frameStepInterval);
this.frameStepInterval = null;
}
}
}
startFrameStep(timeStep) {
const stepFrame = () => {
const startTime = parseFloat(this.elements.startTimeInput.value);
const endTime = parseFloat(this.elements.endTimeInput.value);
let newTime = this.videoPreview.currentTime + timeStep;
if (newTime > endTime) {
newTime = startTime;
} else if (newTime < startTime) {
newTime = endTime;
}
this.videoPreview.currentTime = newTime;
this.elements.playhead.style.display = 'block';
this.updatePlayhead();
};
stepFrame();
let lastTimestamp = performance.now();
const animate = (timestamp) => {
if (!this.isKeyHeld) return;
const deltaTime = timestamp - lastTimestamp;
if (deltaTime >= 16) {
stepFrame();
lastTimestamp = timestamp;
}
requestAnimationFrame(animate);
};
setTimeout(() => {
if (this.isKeyHeld) {
requestAnimationFrame(animate);
}
}, 200);
}
handleMarkerTouchStart(e) {
if (!this.videoPreview.src) return;
e.preventDefault();
this.isDragging = true;
this.currentMarker = e.target;
}
handleMarkerTouchMove(e) {
if (!this.isDragging || !this.currentMarker || !this.videoPreview.src) return;
e.preventDefault();
const touch = e.touches[0];
const rect = this.elements.timeline.getBoundingClientRect();
let position = (touch.clientX - rect.left) / rect.width;
position = Math.max(0, Math.min(1, position));
const duration = this.videoDuration || this.videoPreview.duration;
const time = position * duration;
if (isFinite(time) && isFinite(duration) && duration > 0) {
this.updateTimeFromDrag(time);
}
}
handleMarkerTouchEnd() {
this.isDragging = false;
this.currentMarker = null;
}
validateTimeInputs() {
const start = parseFloat(this.elements.startTime.value);
const end = parseFloat(this.elements.endTime.value);
const duration = this.videoPreview.duration;
if (!isFinite(duration) || duration <= 0) {
// If we have a duration from filename, use that
const durationMatch = this.originalFileName.match(/_(\d+)s$/);
if (durationMatch && !isNaN(durationMatch[1])) {
const fileDuration = parseFloat(durationMatch[1]);
this.elements.endTime.value = fileDuration.toFixed(2);
this.elements.startTime.value = '0.00';
return;
}
// Otherwise set some reasonable defaults
this.elements.endTime.value = '0.00';
this.elements.startTime.value = '0.00';
return;
}
if (start >= end) {
this.elements.startTime.value = (end - 0.1).toFixed(2);
}
}
initializeStepperButtons() {
const stepperButtons = document.querySelectorAll('.time-stepper-btn');
stepperButtons.forEach(button => {
let intervalId = null;
let timeoutId = null;
const startRepeating = (delta) => {
timeoutId = setTimeout(() => {
intervalId = setInterval(() => {
const inputId = button.closest('.time-input-group').querySelector('.time-input').id;
adjustTime(inputId, delta);
}, 50);
}, 400);
};
const stopRepeating = () => {
if (intervalId) {
clearInterval(intervalId);
}
if (timeoutId) {
clearTimeout(timeoutId);
}
intervalId = null;
timeoutId = null;
};
button.addEventListener('mousedown', (e) => {
e.preventDefault();
const delta = button.textContent === '+' ? 0.1 : -0.1;
const inputId = button.closest('.time-input-group').querySelector('.time-input').id;
adjustTime(inputId, delta);
startRepeating(delta);
});
button.addEventListener('mouseup', () => {
stopRepeating();
});
button.addEventListener('mouseleave', () => {
stopRepeating();
});
button.addEventListener('touchstart', (e) => {
e.preventDefault();
const delta = button.textContent === '+' ? 0.1 : -0.1;
const inputId = button.closest('.time-input-group').querySelector('.time-input').id;
adjustTime(inputId, delta);
startRepeating(delta);
});
button.addEventListener('touchend', () => {
stopRepeating();
});
button.addEventListener('touchcancel', () => {
stopRepeating();
});
});
}
initializeTimelineDrag() {
this.elements.timeline.addEventListener('mousedown', (e) => {
if (!this.videoPreview.src) return;
this.isScrubbingPlayhead = true;
this.wasPlaying = this.isPlaying;
if (this.isPlaying) {
this.videoPreview.pause();
}
// Handle the initial click position
const rect = this.elements.timeline.getBoundingClientRect();
let position = (e.clientX - rect.left) / rect.width;
position = Math.max(0, Math.min(1, position));
const duration = this.videoDuration || this.videoPreview.duration;
const time = position * duration;
// Only set currentTime if the value is valid
if (isFinite(time) && time >= 0 && time <= duration) {
this.videoPreview.currentTime = time;
this.elements.playhead.style.display = 'block';
this.elements.playhead.style.left = `${position * 100}%`;
}
document.addEventListener('mousemove', this.handleTimelineDrag);
document.addEventListener('mouseup', this.handleTimelineDragEnd);
});
// Define bound event handlers
this.handleTimelineDrag = (e) => {
if (!this.isScrubbingPlayhead) return;
e.preventDefault();
const rect = this.elements.timeline.getBoundingClientRect();
let position = (e.clientX - rect.left) / rect.width;
position = Math.max(0, Math.min(1, position));
const duration = this.videoDuration || this.videoPreview.duration;
const time = position * duration;
// Only set currentTime if the value is valid
if (isFinite(time) && time >= 0 && time <= duration) {
this.videoPreview.currentTime = time;
this.elements.playhead.style.left = `${position * 100}%`;
}
};
this.handleTimelineDragEnd = () => {
if (!this.isScrubbingPlayhead) return;
this.isScrubbingPlayhead = false;
// Remove temporary event listeners
document.removeEventListener('mousemove', this.handleTimelineDrag);
document.removeEventListener('mouseup', this.handleTimelineDragEnd);
if (this.wasPlaying) {
this.videoPreview.play().catch(() => {
this.isPlaying = false;
this.updatePlayButton(false);
});
}
};
}
updateTimeDisplay(isStartTime, time) {
const display = isStartTime ? this.elements.startTimeDisplay : this.elements.endTimeDisplay;
display.textContent = this.formatTime(time);
// Dispatch a change event on the corresponding input
const input = isStartTime ? this.elements.startTimeInput : this.elements.endTimeInput;
const event = new Event('change', { bubbles: true });
input.dispatchEvent(event);
}
adjustTime(inputId, delta) {
const input = document.getElementById(inputId);
const timeline = document.querySelector('.timeline');
const timelineController = timeline._timelineController;
const videoPreview = document.getElementById('videoPreview');
if (!input || !timelineController || !videoPreview.src) {
return;
}
const currentValue = parseFloat(input.value);
const isStartTime = inputId === 'startTime';
const duration = timelineController.videoDuration || timelineController.videoPreview.duration;
const otherInput = document.getElementById(isStartTime ? 'endTime' : 'startTime');
const otherValue = parseFloat(otherInput.value);
let newValue;
if (isStartTime) {
newValue = Math.min(
Math.max(0, Math.round((currentValue + delta) * 100) / 100),
otherValue - 0.01
);
} else {
newValue = Math.min(
Math.max(otherValue + 0.01, Math.round((currentValue + delta) * 100) / 100),
duration
);
}
if (newValue === currentValue) return;
input.value = newValue.toFixed(2);
const display = document.querySelector(isStartTime ? '.start-time' : '.end-time');
const marker = document.querySelector(isStartTime ? '.start-marker' : '.end-marker');
display.textContent = timelineController.formatTime(newValue);
const position = (newValue / duration) * 100;
marker.style.left = `${position}%`;
const durationDisplay = document.querySelector('.duration-time');
const startTime = parseFloat(document.getElementById('startTime').value);
const endTime = parseFloat(document.getElementById('endTime').value);
const clipDuration = endTime - startTime;
durationDisplay.textContent = `Duration: ${clipDuration.toFixed(1)}s`;
const event = new Event('change', { bubbles: true });
input.dispatchEvent(event);
}
}
document.addEventListener('DOMContentLoaded', () => {
const converter = new GifConverter();
const timeline = new TimelineController(document.getElementById('videoPreview'));
// Set up the bi-directional references
converter.setTimeline(timeline);
timeline.converter = converter;
});
</script>
<!-- EXTERNAL gif.js library included for GIF creation and manipulation -->
<script>// gif.js 0.2.0 - https://github.com/jnordberg/gif.js
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.GIF=f()}})(function(){var define,module,exports;return function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s}({1:[function(require,module,exports){function EventEmitter(){this._events=this._events||{};this._maxListeners=this._maxListeners||undefined}module.exports=EventEmitter;EventEmitter.EventEmitter=EventEmitter;EventEmitter.prototype._events=undefined;EventEmitter.prototype._maxListeners=undefined;EventEmitter.defaultMaxListeners=10;EventEmitter.prototype.setMaxListeners=function(n){if(!isNumber(n)||n<0||isNaN(n))throw TypeError("n must be a positive number");this._maxListeners=n;return this};EventEmitter.prototype.emit=function(type){var er,handler,len,args,i,listeners;if(!this._events)this._events={};if(type==="error"){if(!this._events.error||isObject(this._events.error)&&!this._events.error.length){er=arguments[1];if(er instanceof Error){throw er}else{var err=new Error('Uncaught, unspecified "error" event. ('+er+")");err.context=er;throw err}}}handler=this._events[type];if(isUndefined(handler))return false;if(isFunction(handler)){switch(arguments.length){case 1:handler.call(this);break;case 2:handler.call(this,arguments[1]);break;case 3:handler.call(this,arguments[1],arguments[2]);break;default:args=Array.prototype.slice.call(arguments,1);handler.apply(this,args)}}else if(isObject(handler)){args=Array.prototype.slice.call(arguments,1);listeners=handler.slice();len=listeners.length;for(i=0;i<len;i++)listeners[i].apply(this,args)}return true};EventEmitter.prototype.addListener=function(type,listener){var m;if(!isFunction(listener))throw TypeError("listener must be a function");if(!this._events)this._events={};if(this._events.newListener)this.emit("newListener",type,isFunction(listener.listener)?listener.listener:listener);if(!this._events[type])this._events[type]=listener;else if(isObject(this._events[type]))this._events[type].push(listener);else this._events[type]=[this._events[type],listener];if(isObject(this._events[type])&&!this._events[type].warned){if(!isUndefined(this._maxListeners)){m=this._maxListeners}else{m=EventEmitter.defaultMaxListeners}if(m&&m>0&&this._events[type].length>m){this._events[type].warned=true;console.error("(node) warning: possible EventEmitter memory "+"leak detected. %d listeners added. "+"Use emitter.setMaxListeners() to increase limit.",this._events[type].length);if(typeof console.trace==="function"){console.trace()}}}return this};EventEmitter.prototype.on=EventEmitter.prototype.addListener;EventEmitter.prototype.once=function(type,listener){if(!isFunction(listener))throw TypeError("listener must be a function");var fired=false;function g(){this.removeListener(type,g);if(!fired){fired=true;listener.apply(this,arguments)}}g.listener=listener;this.on(type,g);return this};EventEmitter.prototype.removeListener=function(type,listener){var list,position,length,i;if(!isFunction(listener))throw TypeError("listener must be a function");if(!this._events||!this._events[type])return this;list=this._events[type];length=list.length;position=-1;if(list===listener||isFunction(list.listener)&&list.listener===listener){delete this._events[type];if(this._events.removeListener)this.emit("removeListener",type,listener)}else if(isObject(list)){for(i=length;i-- >0;){if(list[i]===listener||list[i].listener&&list[i].listener===listener){position=i;break}}if(position<0)return this;if(list.length===1){list.length=0;delete this._events[type]}else{list.splice(position,1)}if(this._events.removeListener)this.emit("removeListener",type,listener)}return this};EventEmitter.prototype.removeAllListeners=function(type){var key,listeners;if(!this._events)return this;if(!this._events.removeListener){if(arguments.length===0)this._events={};else if(this._events[type])delete this._events[type];return this}if(arguments.length===0){for(key in this._events){if(key==="removeListener")continue;this.removeAllListeners(key)}this.removeAllListeners("removeListener");this._events={};return this}listeners=this._events[type];if(isFunction(listeners)){this.removeListener(type,listeners)}else if(listeners){while(listeners.length)this.removeListener(type,listeners[listeners.length-1])}delete this._events[type];return this};EventEmitter.prototype.listeners=function(type){var ret;if(!this._events||!this._events[type])ret=[];else if(isFunction(this._events[type]))ret=[this._events[type]];else ret=this._events[type].slice();return ret};EventEmitter.prototype.listenerCount=function(type){if(this._events){var evlistener=this._events[type];if(isFunction(evlistener))return 1;else if(evlistener)return evlistener.length}return 0};EventEmitter.listenerCount=function(emitter,type){return emitter.listenerCount(type)};function isFunction(arg){return typeof arg==="function"}function isNumber(arg){return typeof arg==="number"}function isObject(arg){return typeof arg==="object"&&arg!==null}function isUndefined(arg){return arg===void 0}},{}],2:[function(require,module,exports){var UA,browser,mode,platform,ua;ua=navigator.userAgent.toLowerCase();platform=navigator.platform.toLowerCase();UA=ua.match(/(opera|ie|firefox|chrome|version)[\s\/:]([\w\d\.]+)?.*?(safari|version[\s\/:]([\w\d\.]+)|$)/)||[null,"unknown",0];mode=UA[1]==="ie"&&document.documentMode;browser={name:UA[1]==="version"?UA[3]:UA[1],version:mode||parseFloat(UA[1]==="opera"&&UA[4]?UA[4]:UA[2]),platform:{name:ua.match(/ip(?:ad|od|hone)/)?"ios":(ua.match(/(?:webos|android)/)||platform.match(/mac|win|linux/)||["other"])[0]}};browser[browser.name]=true;browser[browser.name+parseInt(browser.version,10)]=true;browser.platform[browser.platform.name]=true;module.exports=browser},{}],3:[function(require,module,exports){var EventEmitter,GIF,browser,extend=function(child,parent){for(var key in parent){if(hasProp.call(parent,key))child[key]=parent[key]}function ctor(){this.constructor=child}ctor.prototype=parent.prototype;child.prototype=new ctor;child.__super__=parent.prototype;return child},hasProp={}.hasOwnProperty,indexOf=[].indexOf||function(item){for(var i=0,l=this.length;i<l;i++){if(i in this&&this[i]===item)return i}return-1},slice=[].slice;EventEmitter=require("events").EventEmitter;browser=require("./browser.coffee");GIF=function(superClass){var defaults,frameDefaults;extend(GIF,superClass);defaults={workerScript:"gif.worker.js",workers:2,repeat:0,background:"#fff",quality:10,width:null,height:null,transparent:null,debug:false,dither:false};frameDefaults={delay:500,copy:false};function GIF(options){var base,key,value;this.running=false;this.options={};this.frames=[];this.freeWorkers=[];this.activeWorkers=[];this.setOptions(options);for(key in defaults){value=defaults[key];if((base=this.options)[key]==null){base[key]=value}}}GIF.prototype.setOption=function(key,value){this.options[key]=value;if(this._canvas!=null&&(key==="width"||key==="height")){return this._canvas[key]=value}};GIF.prototype.setOptions=function(options){var key,results,value;results=[];for(key in options){if(!hasProp.call(options,key))continue;value=options[key];results.push(this.setOption(key,value))}return results};GIF.prototype.addFrame=function(image,options){var frame,key;if(options==null){options={}}frame={};frame.transparent=this.options.transparent;for(key in frameDefaults){frame[key]=options[key]||frameDefaults[key]}if(this.options.width==null){this.setOption("width",image.width)}if(this.options.height==null){this.setOption("height",image.height)}if(typeof ImageData!=="undefined"&&ImageData!==null&&image instanceof ImageData){frame.data=image.data}else if(typeof CanvasRenderingContext2D!=="undefined"&&CanvasRenderingContext2D!==null&&image instanceof CanvasRenderingContext2D||typeof WebGLRenderingContext!=="undefined"&&WebGLRenderingContext!==null&&image instanceof WebGLRenderingContext){if(options.copy){frame.data=this.getContextData(image)}else{frame.context=image}}else if(image.childNodes!=null){if(options.copy){frame.data=this.getImageData(image)}else{frame.image=image}}else{throw new Error("Invalid image")}return this.frames.push(frame)};GIF.prototype.render=function(){var i,j,numWorkers,ref;if(this.running){throw new Error("Already running")}if(this.options.width==null||this.options.height==null){throw new Error("Width and height must be set prior to rendering")}this.running=true;this.nextFrame=0;this.finishedFrames=0;this.imageParts=function(){var j,ref,results;results=[];for(i=j=0,ref=this.frames.length;0<=ref?j<ref:j>ref;i=0<=ref?++j:--j){results.push(null)}return results}.call(this);numWorkers=this.spawnWorkers();if(this.options.globalPalette===true){this.renderNextFrame()}else{for(i=j=0,ref=numWorkers;0<=ref?j<ref:j>ref;i=0<=ref?++j:--j){this.renderNextFrame()}}this.emit("start");return this.emit("progress",0)};GIF.prototype.abort=function(){var worker;while(true){worker=this.activeWorkers.shift();if(worker==null){break}this.log("killing active worker");worker.terminate()}this.running=false;return this.emit("abort")};GIF.prototype.spawnWorkers=function(){var j,numWorkers,ref,results;numWorkers=Math.min(this.options.workers,this.frames.length);(function(){results=[];for(var j=ref=this.freeWorkers.length;ref<=numWorkers?j<numWorkers:j>numWorkers;ref<=numWorkers?j++:j--){results.push(j)}return results}).apply(this).forEach(function(_this){return function(i){var worker;_this.log("spawning worker "+i);worker=new Worker(_this.options.workerScript);worker.onmessage=function(event){_this.activeWorkers.splice(_this.activeWorkers.indexOf(worker),1);_this.freeWorkers.push(worker);return _this.frameFinished(event.data)};return _this.freeWorkers.push(worker)}}(this));return numWorkers};GIF.prototype.frameFinished=function(frame){var i,j,ref;this.log("frame "+frame.index+" finished - "+this.activeWorkers.length+" active");this.finishedFrames++;this.emit("progress",this.finishedFrames/this.frames.length);this.imageParts[frame.index]=frame;if(this.options.globalPalette===true){this.options.globalPalette=frame.globalPalette;this.log("global palette analyzed");if(this.frames.length>2){for(i=j=1,ref=this.freeWorkers.length;1<=ref?j<ref:j>ref;i=1<=ref?++j:--j){this.renderNextFrame()}}}if(indexOf.call(this.imageParts,null)>=0){return this.renderNextFrame()}else{return this.finishRendering()}};GIF.prototype.finishRendering=function(){var data,frame,i,image,j,k,l,len,len1,len2,len3,offset,page,ref,ref1,ref2;len=0;ref=this.imageParts;for(j=0,len1=ref.length;j<len1;j++){frame=ref[j];len+=(frame.data.length-1)*frame.pageSize+frame.cursor}len+=frame.pageSize-frame.cursor;this.log("rendering finished - filesize "+Math.round(len/1e3)+"kb");data=new Uint8Array(len);offset=0;ref1=this.imageParts;for(k=0,len2=ref1.length;k<len2;k++){frame=ref1[k];ref2=frame.data;for(i=l=0,len3=ref2.length;l<len3;i=++l){page=ref2[i];data.set(page,offset);if(i===frame.data.length-1){offset+=frame.cursor}else{offset+=frame.pageSize}}}image=new Blob([data],{type:"image/gif"});return this.emit("finished",image,data)};GIF.prototype.renderNextFrame=function(){var frame,task,worker;if(this.freeWorkers.length===0){throw new Error("No free workers")}if(this.nextFrame>=this.frames.length){return}frame=this.frames[this.nextFrame++];worker=this.freeWorkers.shift();task=this.getTask(frame);this.log("starting frame "+(task.index+1)+" of "+this.frames.length);this.activeWorkers.push(worker);return worker.postMessage(task)};GIF.prototype.getContextData=function(ctx){return ctx.getImageData(0,0,this.options.width,this.options.height).data};GIF.prototype.getImageData=function(image){var ctx;if(this._canvas==null){this._canvas=document.createElement("canvas");this._canvas.width=this.options.width;this._canvas.height=this.options.height}ctx=this._canvas.getContext("2d");ctx.setFill=this.options.background;ctx.fillRect(0,0,this.options.width,this.options.height);ctx.drawImage(image,0,0);return this.getContextData(ctx)};GIF.prototype.getTask=function(frame){var index,task;index=this.frames.indexOf(frame);task={index:index,last:index===this.frames.length-1,delay:frame.delay,transparent:frame.transparent,width:this.options.width,height:this.options.height,quality:this.options.quality,dither:this.options.dither,globalPalette:this.options.globalPalette,repeat:this.options.repeat,canTransfer:browser.name==="chrome"};if(frame.data!=null){task.data=frame.data}else if(frame.context!=null){task.data=this.getContextData(frame.context)}else if(frame.image!=null){task.data=this.getImageData(frame.image)}else{throw new Error("Invalid frame")}return task};GIF.prototype.log=function(){var args;args=1<=arguments.length?slice.call(arguments,0):[];if(!this.options.debug){return}return console.log.apply(console,args)};return GIF}(EventEmitter);module.exports=GIF},{"./browser.coffee":2,events:1}]},{},[3])(3)});
//# sourceMappingURL=gif.js.map</script>
</body>
</html>
.png)
No comments:
Post a Comment
If you have any question you can ask me feelfree.