Add playbackRate video option, improve menu widget options

-Add option for playback rate (speed)
-Add hotkeys for changing playback rate ( , and . )
-Add labeled number input widget
-Inherit labeled widget types from base class
-Add option for onchange to above base class
-Clamp numbers when setting numeric values on videos
-Add some helper functions to video elements
-Remove bPropagateOnChange, roll functionality into forEachElement delegate
-Add widgetType parameter to inform the widget type to be created
-Removed unnecessary and incorrect comments
This commit is contained in:
JaredTherriault
2024-04-18 17:58:33 -07:00
parent a514999008
commit b1a44cf0cc
5 changed files with 161 additions and 54 deletions
+35 -13
View File
@@ -6,7 +6,7 @@ import { getPngMetadata } from "/scripts/pnginfo.js";
import {
getMaxZIndex, createDarkContainer, copyToClipboard,
isValid, getCurrentSecondsFromEpoch
isValid, getCurrentSecondsFromEpoch, clamp
} from "../common/Utilities.js";
import { getLastMousePosition, isPointerDown } from "../common/EventManager.js";
@@ -574,8 +574,27 @@ export async function createImageElementFromFileInfo(fileInfo) {
}
});
imageElement.forceLoad = function () {
img.forceLoad = function () {
img.src = img.dataSrc;
if (bIsVideoFormat) {
img.setPlaybackRate(setting_VideoPlaybackOptions.value.defaultPlaybackRate); // This gets reset when src is reset
}
}
img.initVideo = function () {
img.type = fileInfo.file?.format || undefined;
img.autoplay = false; // Start false, will autoplay via observer
img.loop = setting_VideoPlaybackOptions.value.loop;
img.controls = setting_VideoPlaybackOptions.value.controls;
img.muted = setting_VideoPlaybackOptions.value.muted;
img.setVolume(setting_VideoPlaybackOptions.value.defaultVolume);
img.setPlaybackRate(setting_VideoPlaybackOptions.value.defaultPlaybackRate);
}
imageElement.forceLoad = function () {
img.forceLoad();
}
if (fileInfo.bShouldForceLoad) {
@@ -654,15 +673,6 @@ export async function createImageElementFromFileInfo(fileInfo) {
if (bIsVideoFormat) {
img.type = fileInfo.file?.format || undefined;
img.autoplay = false; // Start false, will autoplay via observer
img.loop = setting_VideoPlaybackOptions.value.loop;
img.controls = setting_VideoPlaybackOptions.value.controls;
img.muted = setting_VideoPlaybackOptions.value.muted;
img.volume = setting_VideoPlaybackOptions.value.defaultVolume / 100; // Slider is 1-100, volume is 0-1
imageElement.bIsVideoFormat = bIsVideoFormat;
img.togglePlayback = async function () {
if (!img.pause || !img.play || !('paused' in img)) { return; }
@@ -679,6 +689,10 @@ export async function createImageElementFromFileInfo(fileInfo) {
}
}
img.setVolume = function (percentage) {
img.volume = clamp(percentage / 100, 0.0, 1.0); // Slider is 1-100, volume is 0-1
}
img.toggleFullscreen = function () {
if (!document.fullscreenElement) {
img.requestFullscreen();
@@ -687,15 +701,19 @@ export async function createImageElementFromFileInfo(fileInfo) {
}
}
img.setPlaybackRate = function (rate) {
img.playbackRate = clamp(rate, 0.05, 100.00);
}
img.seekVideo = function (delta) {
if (!img.duration || !img.currentTime) { return; }
const maxTime = img.duration;
const currentTime = img.currentTime;
const seekStep = Math.min(10, Math.max(1, maxTime / 100)); // Seek multiplier
const seekStep = clamp(maxTime / 100, 1, 10); // Seek multiplier: 1 sec min / 1% of video / 10 sec max
let newTime = currentTime + (delta * seekStep);
newTime = Math.max(0, Math.min(maxTime, newTime)); // Clamp within valid range
newTime = clamp(newTime, 0, maxTime); // Clamp within valid range
img.currentTime = newTime; // Seek the video to the new time
};
@@ -714,6 +732,10 @@ export async function createImageElementFromFileInfo(fileInfo) {
img.seekVideo(scrollDelta);
}
});
img.initVideo();
imageElement.bIsVideoFormat = bIsVideoFormat;
}
aElement.appendChild(img);
+18 -2
View File
@@ -63,7 +63,7 @@ document.addEventListener("keydown", async (event) => {
if (elementUnderPointer && elementUnderPointer.toggleMute) {
event.preventDefault();
elementUnderPointer.toggleMute()
elementUnderPointer.toggleMute();
}
} else if (event.key == 'f') { // Toggle fullscreen
const elementUnderPointer = getElementUnderPointer();
@@ -71,7 +71,23 @@ document.addEventListener("keydown", async (event) => {
if (elementUnderPointer && elementUnderPointer.toggleFullscreen) {
event.preventDefault();
elementUnderPointer.toggleFullscreen()
elementUnderPointer.toggleFullscreen();
}
} else if (event.key == ',') { // Decrease playback rate
const elementUnderPointer = getElementUnderPointer();
if (elementUnderPointer && elementUnderPointer.playbackRate && elementUnderPointer.setPlaybackRate) {
event.preventDefault();
elementUnderPointer.setPlaybackRate(elementUnderPointer.playbackRate - 0.05);
}
} else if (event.key == '.') { // Increase playback rate
const elementUnderPointer = getElementUnderPointer();
if (elementUnderPointer && elementUnderPointer.playbackRate && elementUnderPointer.setPlaybackRate) {
event.preventDefault();
elementUnderPointer.setPlaybackRate(elementUnderPointer.playbackRate + 0.05);
}
}
});
+1 -1
View File
@@ -67,7 +67,7 @@ const imageAndVideoObserver = new IntersectionObserver((entries) => {
// Check if the video is intersecting with the viewport
if (entry.isIntersecting) {
if (!element.src || element.src === '') {
element.src = element.dataSrc;
element.forceLoad();
if (element.tagName !== 'VIDEO') {
unobserveVisualElement(element);
+84 -24
View File
@@ -323,17 +323,10 @@ export function createVideoPlaybackOptionsMenuWidgets(menu) {
const infos = new info_VideoPlaybackOptions();
function setGenericSettingOnVisualElementsInImageList(propertyName, propertyValue, info) {
function callForEachCallbackOnEachElementInImageList(propertyName, propertyValue, info) {
for (let child of getImageListChildren()) {
for (let element of getVideoElements(child)) {
if (info.bPropagateOnChange) {
element[propertyName] = propertyValue;
}
if (info.forEachElement) {
info.forEachElement(element, propertyValue);
}
info.forEachElement(element, propertyName, propertyValue);
}
}
}
@@ -345,8 +338,8 @@ export function createVideoPlaybackOptionsMenuWidgets(menu) {
// If requested, set similarly named properties on image list
const info = infos[propertyName];
if (info.bPropagateOnChange || info.forEachElement) {
setGenericSettingOnVisualElementsInImageList(propertyName, newValue, info);
if (info.forEachElement) {
callForEachCallbackOnEachElementInImageList(propertyName, newValue, info);
}
}
@@ -354,8 +347,9 @@ export function createVideoPlaybackOptionsMenuWidgets(menu) {
Object.entries(new options_VideoPlayback()).forEach(([propertyName, propertyValue]) => {
// In case we're using an old serialization, use the serialized value or default
const propertyValueToUse = propertyName in setting_VideoPlaybackOptions.value ? setting_VideoPlaybackOptions.value[propertyName] : propertyValue;
const info = infos[propertyName];
let widget;
if (typeof propertyValueToUse === 'boolean') {
if (info.widgetType === 'checkbox') {
let options = new options_LabeledCheckboxToggle();
options.id = `VideoPlaybackOptions.${propertyName}`
options.checked = propertyValueToUse;
@@ -366,18 +360,35 @@ export function createVideoPlaybackOptionsMenuWidgets(menu) {
oninput(propertyName, newValue);
}
widget = createLabeledCheckboxToggle(options);
} else if (typeof propertyValueToUse === 'number') {
} else if (info.widgetType === 'range') {
let options = new options_LabeledSliderRange();
options.id = `VideoPlaybackOptions.${propertyName}`
options.value = propertyValueToUse;
options.labelTextContent = propertyName;
options.bIncludeValueLabel = false;
options.min = info?.min ? info.min : options.min;
options.max = info?.max ? info.max : options.max;
options.step = info?.step ? info.step : options.step;
options.oninput = (e) => {
// This config setting is a class instance, so we need to take a few extra steps to serialize it
const newValue = e.target.valueAsNumber;
oninput(propertyName, newValue);
}
widget = createLabeledSliderRange(options);
} else if (info.widgetType === 'number') {
let options = new options_LabeledNumberInput();
options.id = `VideoPlaybackOptions.${propertyName}`
options.value = propertyValueToUse;
options.labelTextContent = propertyName;
options.min = info?.min ? info.min : options.min;
options.max = info?.max ? info.max : options.max;
options.step = info?.step ? info.step : options.step;
options.onchange = (e) => { // Use onchange here because the user can type a value in, onchange requires commit
// This config setting is a class instance, so we need to take a few extra steps to serialize it
const newValue = e.target.valueAsNumber;
oninput(propertyName, newValue);
}
widget = createLabeledNumberInput(options);
}
widget.title = infos[propertyName]?.tooltip || '';
menu.appendChild(widget);
@@ -394,17 +405,33 @@ export function createVideoPlaybackOptionsFlyout() {
return flyout;
}
export class options_LabeledSliderRange {
class options_BaseLabeledWidget {
id = undefined;
labelTextContent = undefined;
oninput = undefined;
onchange = undefined;
bindEvents(widget) {
if (this.oninput) {
widget.oninput = this.oninput;
}
if (this.onchange) {
widget.onchange = this.onchange;
}
return widget;
}
}
export class options_LabeledSliderRange extends options_BaseLabeledWidget {
bIncludeValueLabel = true;
bPrependValueLabel = false;
valueLabelFractionalDigits = 0;
id = undefined;
value = 0;
min = 0;
max = 100
step = 1;
oninput = undefined;
}
export function createLabeledSliderRange(options = new options_LabeledSliderRange()) {
@@ -441,10 +468,11 @@ export function createLabeledSliderRange(options = new options_LabeledSliderRang
value: options.value,
min: options.min,
max: options.max,
step: options.step,
oninput: options.oninput
step: options.step
});
options.bindEvents(MainElement);
let OuterElement = $el('div', {
style: {
display: 'flex',
@@ -467,11 +495,42 @@ export function createLabeledSliderRange(options = new options_LabeledSliderRang
return OuterElement;
}
export class options_LabeledCheckboxToggle {
labelTextContent = undefined;
id = undefined;
export class options_LabeledNumberInput extends options_BaseLabeledWidget {
value = 0;
min = 0;
max = 100
step = 1;
}
export function createLabeledNumberInput(options = new options_LabeledNumberInput()) {
let MainElement = $el('input', {
id: options.id,
type: 'number',
value: options.value,
min: options.min,
max: options.max,
step: options.step
});
options.bindEvents(MainElement);
let OuterElement = $el('div', {
style: {
display: 'flex',
alignItems: 'center',
gap: '5%'
}
});
OuterElement.appendChild($el('label', { textContent: options.labelTextContent }));
OuterElement.appendChild(MainElement);
return OuterElement;
}
export class options_LabeledCheckboxToggle extends options_BaseLabeledWidget {
checked = false;
oninput = undefined;
}
export function createLabeledCheckboxToggle(options = new options_LabeledCheckboxToggle()) {
@@ -479,10 +538,11 @@ export function createLabeledCheckboxToggle(options = new options_LabeledCheckbo
let MainElement = $el('input', {
id: options.id,
type: 'checkbox',
checked: options.checked,
oninput: options.oninput
checked: options.checked
});
options.bindEvents(MainElement);
let OuterElement = $el('div', {
style: {
display: 'flex',
+23 -14
View File
@@ -1,20 +1,21 @@
export class options_VideoPlayback {
autoplay = false; loop = true; controls = true; muted = true; useWheelSeek = true; invertWheelSeek = true; defaultVolume = 50;
autoplay = false; loop = true; controls = true; muted = true; useWheelSeek = true; invertWheelSeek = true; defaultVolume = 50; defaultPlaybackRate = 1.00;
};
export class info_VideoPlaybackOptions {
autoplay = { tooltip: 'Should the videos automatically play as they scroll into view? Does not apply to animated images, only video types.',
bPropagateOnChange: true, forEachElement: forEachElement_Autoplay };
loop = { tooltip: 'Should the videos automatically play again when the end is reached?', bPropagateOnChange: true, forEachElement: null };
controls = { tooltip: 'Should controls such as play/pause, volume, etc be displayed on the video?', bPropagateOnChange: true, forEachElement: null };
muted = { tooltip: 'Should videos play without audio?', bPropagateOnChange: true, forEachElement: null };
useWheelSeek = { tooltip: 'Should videos seek when mousing over a video and using the scroll wheel?', bPropagateOnChange: false, forEachElement: null };
forEachElement: forEachElement_Autoplay, widgetType: 'checkbox' };
loop = { tooltip: 'Should the videos automatically play again when the end is reached?', forEachElement: forEachElement_genericPropagatation, widgetType: 'checkbox' };
controls = { tooltip: 'Should controls such as play/pause, volume, etc be displayed on the video?', forEachElement: forEachElement_genericPropagatation, widgetType: 'checkbox' };
muted = { tooltip: 'Should videos play without audio?', forEachElement: forEachElement_genericPropagatation, widgetType: 'checkbox' };
useWheelSeek = { tooltip: 'Should videos seek when mousing over a video and using the scroll wheel?', forEachElement: forEachElement_genericPropagatation, widgetType: 'checkbox' };
invertWheelSeek = { tooltip: 'Should scroll wheel seeking be inverted? (If true, scrolling up will seek forward. If false, will seek backward)',
bPropagateOnChange: false, forEachElement: null };
defaultVolume = { tooltip: 'The default volume at which videos should play if not muted.', bPropagateOnChange: false, forEachElement: forEachElement_Volume };
forEachElement: forEachElement_genericPropagatation, widgetType: 'checkbox' };
defaultVolume = { tooltip: 'The default volume at which videos should play if not muted', forEachElement: forEachElement_Volume, widgetType: 'range' };
defaultPlaybackRate = { tooltip: 'The default rate at which videos should play', forEachElement: forEachElement_playbackRate, widgetType: 'number', min: 0.01, max: 100.00, step: 0.01 };
};
function forEachElement_Autoplay(element, propertyValue) {
function forEachElement_Autoplay(element, propertyName, propertyValue) {
// Pause anything currently playing if autoplay is false
// We don't want to play just anything, we'll let observer take care of that
if (!propertyValue && element.pause && 'paused' in element && !element.paused) {
@@ -22,10 +23,18 @@ function forEachElement_Autoplay(element, propertyValue) {
}
}
function forEachElement_Volume(element, propertyValue) {
// Pause anything currently playing if autoplay is false
// We don't want to play just anything, we'll let observer take care of that
if (propertyValue && 'volume' in element) {
element.volume = propertyValue / 100; // Slider is 1-100, volume is 0-1
function forEachElement_genericPropagatation(element, propertyName, propertyValue) {
element[propertyName] = propertyValue;
}
function forEachElement_Volume(element, propertyName, propertyValue) {
if (propertyValue && element.setVolume) {
element.setVolume(propertyValue);
}
}
function forEachElement_playbackRate(element, propertyName, propertyValue) {
if (propertyValue && element.setPlaybackRate) {
element.setPlaybackRate(propertyValue);
}
}