Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/Pages/_Generate/GenerateTab.cshtml
Original file line number Diff line number Diff line change
Expand Up @@ -204,13 +204,14 @@
<div class="image_compare_stage image_compare_stage_side" id="image_compare_stage"></div>
</div>
<div class="imageview_popup_modal_undertext">
<div id="image_compare_transparency_row" style="display: none;">
<div id="image_compare_transparency_row" class="image_compare_transparency_row" style="display: none;">
<label class="translate" for="image_compare_transparency_slider">Overlay Opacity</label>
<div class="auto-slider-range-wrapper" style="--range-value: 50%;">
<input class="auto-slider-range" type="range" min="0" max="100" value="50" step="1" id="image_compare_transparency_slider" data-ispot="false" autocomplete="off" oninput="updateRangeStyle(this)" onchange="updateRangeStyle(this)">
</div>
<span id="image_compare_transparency_value">50%</span>
<span id="image_compare_transparency_value" class="image_compare_transparency_value">50%</span>
</div>
<div id="image_compare_scrub_row" class="image_compare_scrub_row"></div>
<div class="image_fullview_extra_buttons">
<button class="basic-button translate" data-compare-mode="side" type="button">Side by Side</button>
<button class="basic-button translate" data-compare-mode="slide_horizontal" type="button">Horizontal Slide</button>
Expand Down
29 changes: 26 additions & 3 deletions src/wwwroot/css/genpage.css
Original file line number Diff line number Diff line change
Expand Up @@ -837,23 +837,46 @@ body {
gap: 0.25rem;
padding: 0.25rem 0;
}
#image_compare_transparency_row {
.image_compare_transparency_row {
display: flex;
align-items: center;
gap: 0.5rem;
}
#image_compare_transparency_row .auto-slider-range-wrapper {
.image_compare_transparency_row .auto-slider-range-wrapper {
width: 14rem;
max-width: 60vw;
margin-top: 0;
margin-bottom: 0;
flex-shrink: 0;
}
#image_compare_transparency_value {
.image_compare_transparency_value {
min-width: 2.75rem;
text-align: left;
white-space: nowrap;
}
.image_compare_scrub_row {
display: none;
align-self: stretch;
padding: 0 1rem;
}
.image_compare_scrub_row.image_compare_scrub_row_active {
display: block;
}
.image_compare_scrub_row .image_compare_video_controls {
position: static;
opacity: 1;
background: none;
padding: 0;
}
.image_compare_scrub_row .image_compare_video_controls .video-time {
color: var(--text);
}
.image_compare_scrub_row .image_compare_video_controls button {
color: var(--text);
}
.image_compare_scrub_row .image_compare_video_controls .video-progress-inner {
background: var(--range-track-color);
}
.image_compare_modal .image_fullview_extra_buttons {
display: flex;
align-items: center;
Expand Down
44 changes: 36 additions & 8 deletions src/wwwroot/js/genpage/gentab/currentimagehandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -1457,6 +1457,8 @@ class ImageCompareHelper {
this.updateMetadataVisibility();
this.updateModeControls();
this.supportedTypes = ['image', 'video'];
this.scrubRow = getRequiredElementById('image_compare_scrub_row');
this.videoControls = null;
}

getImgOrContainer() {
Expand Down Expand Up @@ -1716,6 +1718,9 @@ class ImageCompareHelper {
}

showComparison(left, right) {
if (this.left?.src != left?.src || this.right?.src != right?.src) {
this.clearVideoControls();
}
this.left = left;
this.right = right;
let wasAlreadyOpen = this.isOpen();
Expand Down Expand Up @@ -1857,6 +1862,7 @@ class ImageCompareHelper {
this.showMetadata = false;
this.resetViewportState();
this.setStageContent('side', '');
this.clearVideoControls();
this.metadataDiv.innerHTML = '';
this.updateMetadataVisibility();
this.updateModeControls();
Expand Down Expand Up @@ -1919,8 +1925,11 @@ class ImageCompareHelper {
render() {
this.stopPanning(true);
this.updateModeControls();
let resumeTime = this.videoControls ? this.videoControls.media.currentTime : 0;
let resumePaused = this.videoControls ? this.videoControls.media.paused : false;
if (!this.hasSelection()) {
this.setStageContent('side', '');
this.clearVideoControls();
this.metadataDiv.innerHTML = '';
this.updateMetadataVisibility();
this.updateModeControls();
Expand All @@ -1930,14 +1939,15 @@ class ImageCompareHelper {
this.renderOverlay();
}
else if (ImageCompareHelper.modeDefinitions[this.mode].layout == 'single') {
this.setStageContent('single', `<div class="image_compare_slot">${this.renderMedia(this.left)}</div>`);
this.setStageContent('single', `<div class="image_compare_slot">${this.renderMedia(this.left, 'left')}</div>`);
}
else {
this.setStageContent('side', `
<div class="image_compare_slot">${this.renderMedia(this.left)}</div>
<div class="image_compare_slot">${this.renderMedia(this.right)}</div>`
<div class="image_compare_slot">${this.renderMedia(this.left, 'left')}</div>
<div class="image_compare_slot">${this.renderMedia(this.right, 'right')}</div>`
);
}
this.setupVideoControls(resumeTime, resumePaused);
let metadataHtml = this.renderMetadataTable();
this.metadataDiv.innerHTML = metadataHtml;
this.updateMetadataVisibility();
Expand All @@ -1957,14 +1967,32 @@ class ImageCompareHelper {
this.setStageContent('overlay', `
<div class="image_compare_slot">
<div class="${overlayClasses.join(' ')}" style="--image-compare-split:${this.overlaySplitPercent}%;--image-compare-transparency:${this.transparencyPercent / 100};">
<div class="image_compare_overlay_layer image_compare_overlay_layer_left">${this.renderMedia(this.left)}</div>
<div class="image_compare_overlay_layer image_compare_overlay_layer_right">${this.renderMedia(this.right)}</div>
<div class="image_compare_overlay_layer image_compare_overlay_layer_left">${this.renderMedia(this.left, 'left')}</div>
<div class="image_compare_overlay_layer image_compare_overlay_layer_right">${this.renderMedia(this.right, 'right')}</div>
${this.isSlideMode() ? '<div class="image_compare_overlay_divider"></div>' : ''}
</div>
</div>`
);
}

setupVideoControls(resumeTime, resumePaused) {
this.videoControls?.destroy();
this.videoControls = null;
let videos = [...this.stage.querySelectorAll('video.image_compare_media')];
this.scrubRow.classList.toggle('image_compare_scrub_row_active', videos.length > 0);
if (videos.length == 0) {
return;
}
let primary = videos.find(video => video.dataset.compareSide == 'left') ?? videos[0];
this.videoControls = new CompareVideoControls(videos, primary, this.scrubRow, resumeTime, resumePaused);
}

clearVideoControls() {
this.videoControls?.destroy();
this.videoControls = null;
this.scrubRow.classList.remove('image_compare_scrub_row_active');
}

updateOverlaySplitFromClientPosition(stage, clientX, clientY) {
let rect = stage.getBoundingClientRect();
let split;
Expand Down Expand Up @@ -2107,12 +2135,12 @@ class ImageCompareHelper {
this.applyView();
}

renderMedia(media) {
renderMedia(media, side) {
return this.renderMediaElement(
media.src,
'image_compare_media',
'alt="Compared media" onload="imageCompareHelper.onImgLoad()"',
'autoplay loop muted playsinline onloadedmetadata="imageCompareHelper.onImgLoad()"',
`alt="Compared media" data-compare-side="${side}" onload="imageCompareHelper.onImgLoad()"`,
`autoplay loop muted playsinline data-compare-side="${side}" onloadedmetadata="imageCompareHelper.onImgLoad()"`,
'',
false,
);
Expand Down
131 changes: 123 additions & 8 deletions src/wwwroot/js/genpage/helpers/mediacontrols.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,9 +181,8 @@ class VideoControls extends MediaControlsBase {
this.createControls();
}

/** Creates the controls UI for the video. */
createControls() {
let container = this.media.parentElement;
/** Builds the control bar UI (play, time, progress bar, volume). */
buildControlBar(container) {
let controls = createDiv(null, 'video-controls', `
<button data-action="play">▶</button>
<span class="video-time">0:00</span>
Expand All @@ -207,10 +206,19 @@ class VideoControls extends MediaControlsBase {
this.wirePlayAndVolumeHandlers();
this.media.addEventListener('loadedmetadata', () => this.updateDuration());
this.wirePlaybackProgressRafListeners();
container.addEventListener('mouseenter', () => { controls.style.opacity = 1; });
container.addEventListener('mouseleave', () => { controls.style.opacity = 0; });
this.progressBar.addEventListener('click', (e) => this.seek(e));
this.progressBar.addEventListener('mousedown', () => { this.isDragging = true; uiImprover.videoControlDragging = this; });
if (!this.media.paused) {
this.startProgressAnimation();
}
}

/** Creates the controls UI for the video. */
createControls() {
let container = this.media.parentElement;
this.buildControlBar(container);
container.addEventListener('mouseenter', () => { this.controls.style.opacity = 1; });
container.addEventListener('mouseleave', () => { this.controls.style.opacity = 0; });
this.media.draggable = true;
this.media.addEventListener('dragstart', (e) => {
let src = this.media.currentSrc || this.media.src;
Expand All @@ -220,9 +228,6 @@ class VideoControls extends MediaControlsBase {
e.dataTransfer.setData('text/uri-list', src);
}
});
if (!this.media.paused) {
this.startProgressAnimation();
}
}

/** Whether the progress bar element is still in the document (used by RAF loop in MediaControlsBase). */
Expand Down Expand Up @@ -591,3 +596,113 @@ class AudioControls extends MediaControlsBase {
this.redrawWaveform();
}
}

/** Video control bar for the image-compare modal: one bar drives every video in the compare stage, and only the primary (left-most) video emits audio. */
class CompareVideoControls extends VideoControls {
constructor(videos, primary, container, resumeTime = 0, resumePaused = false) {
super(primary);
this.videos = videos;
this.buildControlBar(container);
this.controls.classList.add('image_compare_video_controls');
for (let video of videos) {
video.muted = video != primary;
if (resumeTime > 0) {
video.currentTime = resumeTime;
}
if (resumePaused) {
video.pause();
}
video.addEventListener('loadedmetadata', () => {
this.updateDuration();
this.syncToPrimary();
});
}
this.updateDuration();
this.refreshProgressDisplay();
this.updateIcons();
}

createControls() {
}

/** Removes the control bar and stops its animation. */
destroy() {
this.stopProgressAnimation();
this.controls.remove();
}

/** Longest duration among the compared videos, ie the time range the scrub bar spans. */
spanDuration() {
let duration = 0;
for (let video of this.videos) {
if (isFinite(video.duration)) {
duration = Math.max(duration, video.duration);
}
}
return duration;
}

/** Seeks all videos to the given time. */
seekAll(time) {
for (let video of this.videos) {
video.currentTime = time;
}
}

/** Aligns secondary videos to the primary video's playback position. */
syncToPrimary() {
let time = this.media.currentTime;
for (let video of this.videos) {
if (video != this.media) {
video.currentTime = time;
}
}
}

/** Toggles play/pause for all videos together. */
togglePlay() {
if (this.media.paused) {
for (let video of this.videos) {
video.play().catch(() => {});
}
}
else {
for (let video of this.videos) {
video.pause();
}
}
this.updateIcons();
}

/** Seeks all videos from a click on the progress bar. */
seek(e) {
let p = MediaControlsBase.scrubFractionFromClientX(e.clientX, this.progressBar);
let d = this.spanDuration();
if (p == null || d <= 0) {
return;
}
this.seekAll(p * d);
}

/** Document-level mousemove while dragging the progress bar. */
drag(e) {
if (!this.isDragging) {
return;
}
this.seek(e);
this.refreshProgressDisplay();
}

/** Updates the progress bar and current-time label to match the primary video. */
refreshProgressDisplay() {
let d = this.spanDuration();
let percent = d > 0 ? (this.media.currentTime / d) * 100 : 0;
this.progressFilled.style.width = `${percent}%`;
this.currentTimeEl.textContent = this.formatTime(this.media.currentTime);
}

/** Updates the duration UI text to the spanned duration. */
updateDuration() {
this.durationEl.textContent = this.formatTime(this.spanDuration());
}
}
Loading