mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-31 01:42:27 +00:00
Add ambient glow to video modal
This commit is contained in:
1 parent
a08f21a9bd
commit
cb88a79590
4 files changed
+405
No files matched your search
@@ -40,6 +40,9 @@
|
||||
</div>
|
||||
|
||||
<div class="player-modal__content video-modal__panel flex flex-col gap-6">
|
||||
<div class="video-modal__ambient" aria-hidden="true">
|
||||
<canvas id="ambientCanvas" class="video-modal__ambient-canvas"></canvas>
|
||||
</div>
|
||||
<div class="video-modal__layout">
|
||||
<div class="video-modal__primary flex flex-col gap-6">
|
||||
<div class="watch-container">
|
||||
|
||||
@@ -1410,6 +1410,31 @@
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.video-modal__panel > :not(.video-modal__ambient) {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.video-modal__ambient {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
background: var(--color-overlay-strong);
|
||||
}
|
||||
|
||||
.video-modal__ambient-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
transform: scale(1.08);
|
||||
transform-origin: center;
|
||||
filter: blur(40px) saturate(1.25) brightness(0.8);
|
||||
}
|
||||
|
||||
#playerModal .watch-container {
|
||||
width: 100%;
|
||||
max-width: var(--player-modal-inline-max, var(--layout-player-max-width));
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
// js/ui/ambientBackground.js
|
||||
|
||||
const DEFAULT_THROTTLE_MS = 66;
|
||||
|
||||
function getGlobalFromCanvas(canvas) {
|
||||
const doc = canvas?.ownerDocument || (typeof document !== "undefined" ? document : null);
|
||||
const win = doc?.defaultView || (typeof window !== "undefined" ? window : null);
|
||||
return { doc, win };
|
||||
}
|
||||
|
||||
function getFallbackFillColor(canvas) {
|
||||
const { doc, win } = getGlobalFromCanvas(canvas);
|
||||
const candidates = ["--color-overlay-strong", "--color-overlay", "--color-surface", "--color-bg"];
|
||||
let styles = null;
|
||||
if (doc && typeof doc.documentElement !== "undefined") {
|
||||
const root = doc.documentElement;
|
||||
if (win && typeof win.getComputedStyle === "function") {
|
||||
styles = win.getComputedStyle(root);
|
||||
}
|
||||
}
|
||||
|
||||
for (const token of candidates) {
|
||||
if (!styles) {
|
||||
break;
|
||||
}
|
||||
const value = styles.getPropertyValue(token);
|
||||
if (value && value.trim()) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
return "#000000";
|
||||
}
|
||||
|
||||
function resizeCanvas(canvas, video) {
|
||||
if (!canvas) {
|
||||
return { width: 0, height: 0 };
|
||||
}
|
||||
const { doc, win } = getGlobalFromCanvas(canvas);
|
||||
const host = canvas.parentElement || canvas;
|
||||
const rect = host?.getBoundingClientRect?.();
|
||||
if (!rect || rect.width <= 0 || rect.height <= 0) {
|
||||
return { width: canvas.width || 0, height: canvas.height || 0 };
|
||||
}
|
||||
|
||||
const dpr = (win && win.devicePixelRatio) || 1;
|
||||
const targetWidth = Math.max(1, Math.round(rect.width * dpr));
|
||||
const targetHeight = Math.max(1, Math.round(rect.height * dpr));
|
||||
|
||||
if (canvas.width !== targetWidth || canvas.height !== targetHeight) {
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = targetHeight;
|
||||
}
|
||||
|
||||
if (video) {
|
||||
const readyState = Number.isFinite(video.readyState) ? video.readyState : 0;
|
||||
if (readyState >= 1 && video.videoWidth && video.videoHeight) {
|
||||
return { width: canvas.width, height: canvas.height };
|
||||
}
|
||||
}
|
||||
|
||||
return { width: canvas.width, height: canvas.height };
|
||||
}
|
||||
|
||||
function drawCover(ctx, video, canvasWidth, canvasHeight) {
|
||||
if (!ctx || !video || !canvasWidth || !canvasHeight) {
|
||||
return;
|
||||
}
|
||||
const videoWidth = video.videoWidth;
|
||||
const videoHeight = video.videoHeight;
|
||||
if (!videoWidth || !videoHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const videoRatio = videoWidth / videoHeight;
|
||||
const canvasRatio = canvasWidth / canvasHeight;
|
||||
|
||||
let sx = 0;
|
||||
let sy = 0;
|
||||
let sw = videoWidth;
|
||||
let sh = videoHeight;
|
||||
|
||||
if (videoRatio > canvasRatio) {
|
||||
sh = videoHeight;
|
||||
sw = Math.round(videoHeight * canvasRatio);
|
||||
sx = Math.max(0, Math.round((videoWidth - sw) / 2));
|
||||
sy = 0;
|
||||
} else {
|
||||
sw = videoWidth;
|
||||
sh = Math.round(videoWidth / canvasRatio);
|
||||
sx = 0;
|
||||
sy = Math.max(0, Math.round((videoHeight - sh) / 2));
|
||||
}
|
||||
|
||||
ctx.drawImage(video, sx, sy, sw, sh, 0, 0, canvasWidth, canvasHeight);
|
||||
}
|
||||
|
||||
export function attachAmbientBackground(videoElement, canvasElement, options = {}) {
|
||||
if (!videoElement || !canvasElement) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const ctx = canvasElement.getContext("2d", { alpha: false });
|
||||
if (!ctx) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const { doc, win } = getGlobalFromCanvas(canvasElement);
|
||||
const throttleMs = Number.isFinite(options.throttleMs)
|
||||
? Math.max(0, options.throttleMs)
|
||||
: DEFAULT_THROTTLE_MS;
|
||||
|
||||
const requestFrame = win?.requestAnimationFrame?.bind(win) || ((cb) => setTimeout(() => cb(Date.now()), throttleMs));
|
||||
const cancelFrame = win?.cancelAnimationFrame?.bind(win) || ((id) => clearTimeout(id));
|
||||
|
||||
let rafId = 0;
|
||||
let running = false;
|
||||
let destroyed = false;
|
||||
let lastTime = 0;
|
||||
let tainted = false;
|
||||
let resizeBound = false;
|
||||
|
||||
const clearCanvas = () => {
|
||||
if (!canvasElement) {
|
||||
return;
|
||||
}
|
||||
const fillColor = getFallbackFillColor(canvasElement);
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.fillRect(0, 0, canvasElement.width || 0, canvasElement.height || 0);
|
||||
};
|
||||
|
||||
const stop = () => {
|
||||
running = false;
|
||||
if (rafId) {
|
||||
cancelFrame(rafId);
|
||||
rafId = 0;
|
||||
}
|
||||
if (resizeBound && win) {
|
||||
win.removeEventListener("resize", handleResize);
|
||||
resizeBound = false;
|
||||
}
|
||||
};
|
||||
|
||||
const schedule = () => {
|
||||
if (!running || destroyed) {
|
||||
return;
|
||||
}
|
||||
rafId = requestFrame(step);
|
||||
};
|
||||
|
||||
const step = (timestamp = (win?.performance?.now?.() ?? Date.now())) => {
|
||||
if (!running || destroyed) {
|
||||
return;
|
||||
}
|
||||
if (timestamp - lastTime < throttleMs) {
|
||||
schedule();
|
||||
return;
|
||||
}
|
||||
lastTime = timestamp;
|
||||
|
||||
try {
|
||||
const { width, height } = resizeCanvas(canvasElement, videoElement);
|
||||
if (!width || !height) {
|
||||
schedule();
|
||||
return;
|
||||
}
|
||||
drawCover(ctx, videoElement, width, height);
|
||||
} catch (error) {
|
||||
tainted = true;
|
||||
stop();
|
||||
clearCanvas();
|
||||
return;
|
||||
}
|
||||
|
||||
schedule();
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
if (destroyed || tainted || running) {
|
||||
return;
|
||||
}
|
||||
if (doc?.hidden) {
|
||||
running = false;
|
||||
return;
|
||||
}
|
||||
running = true;
|
||||
lastTime = 0;
|
||||
resizeCanvas(canvasElement, videoElement);
|
||||
if (win && !resizeBound) {
|
||||
win.addEventListener("resize", handleResize);
|
||||
resizeBound = true;
|
||||
}
|
||||
step();
|
||||
};
|
||||
|
||||
const handleResize = () => {
|
||||
if (destroyed) {
|
||||
return;
|
||||
}
|
||||
resizeCanvas(canvasElement, videoElement);
|
||||
};
|
||||
|
||||
const handleVisibility = () => {
|
||||
if (!doc) {
|
||||
return;
|
||||
}
|
||||
if (doc.hidden) {
|
||||
stop();
|
||||
return;
|
||||
}
|
||||
resizeCanvas(canvasElement, videoElement);
|
||||
if (!running && !videoElement.paused && !videoElement.ended) {
|
||||
start();
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlay = () => {
|
||||
if (destroyed || tainted) {
|
||||
return;
|
||||
}
|
||||
start();
|
||||
};
|
||||
|
||||
const handlePause = () => {
|
||||
if (destroyed) {
|
||||
return;
|
||||
}
|
||||
stop();
|
||||
};
|
||||
|
||||
const handleEnded = () => {
|
||||
if (destroyed) {
|
||||
return;
|
||||
}
|
||||
stop();
|
||||
};
|
||||
|
||||
const handleLoadedMetadata = () => {
|
||||
if (destroyed || tainted) {
|
||||
return;
|
||||
}
|
||||
resizeCanvas(canvasElement, videoElement);
|
||||
if (!videoElement.paused && !videoElement.ended) {
|
||||
start();
|
||||
}
|
||||
};
|
||||
|
||||
videoElement.addEventListener("play", handlePlay);
|
||||
videoElement.addEventListener("pause", handlePause);
|
||||
videoElement.addEventListener("ended", handleEnded);
|
||||
videoElement.addEventListener("emptied", handlePause);
|
||||
videoElement.addEventListener("loadedmetadata", handleLoadedMetadata);
|
||||
|
||||
if (doc) {
|
||||
doc.addEventListener("visibilitychange", handleVisibility);
|
||||
}
|
||||
|
||||
resizeCanvas(canvasElement, videoElement);
|
||||
if (!videoElement.paused && !videoElement.ended) {
|
||||
start();
|
||||
}
|
||||
|
||||
return ({ clear = true } = {}) => {
|
||||
if (destroyed) {
|
||||
return;
|
||||
}
|
||||
destroyed = true;
|
||||
stop();
|
||||
videoElement.removeEventListener("play", handlePlay);
|
||||
videoElement.removeEventListener("pause", handlePause);
|
||||
videoElement.removeEventListener("ended", handleEnded);
|
||||
videoElement.removeEventListener("emptied", handlePause);
|
||||
videoElement.removeEventListener("loadedmetadata", handleLoadedMetadata);
|
||||
if (doc) {
|
||||
doc.removeEventListener("visibilitychange", handleVisibility);
|
||||
}
|
||||
if (clear) {
|
||||
clearCanvas();
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { createModalAccessibility } from "./modalAccessibility.js";
|
||||
import createPopover from "../overlay/popoverEngine.js";
|
||||
import { createVideoMoreMenuPanel } from "./videoMenuRenderers.js";
|
||||
import { attachAmbientBackground } from "../ambientBackground.js";
|
||||
import { applyDesignSystemAttributes } from "../../designSystem.js";
|
||||
import { devLogger } from "../../utils/logger.js";
|
||||
|
||||
@@ -94,6 +95,8 @@ export class VideoModal {
|
||||
|
||||
this.modalPosterCleanup = null;
|
||||
this.videoEventCleanup = null;
|
||||
this.ambientCanvas = null;
|
||||
this.detachAmbientBackground = null;
|
||||
|
||||
this.activeVideo = null;
|
||||
|
||||
@@ -147,6 +150,7 @@ export class VideoModal {
|
||||
}
|
||||
|
||||
setVideoElement(videoElement) {
|
||||
this.teardownAmbientGlow({ clear: false });
|
||||
this.detachVideoEvents();
|
||||
this.clearPosterCleanup();
|
||||
|
||||
@@ -161,6 +165,8 @@ export class VideoModal {
|
||||
this.modalVideo = null;
|
||||
}
|
||||
|
||||
this.attachAmbientGlow();
|
||||
|
||||
return this.modalVideo;
|
||||
}
|
||||
|
||||
@@ -250,6 +256,8 @@ export class VideoModal {
|
||||
this.modalMorePopover = null;
|
||||
this.modalMoreMenuPanel = null;
|
||||
|
||||
this.teardownAmbientGlow({ clear: false });
|
||||
|
||||
const previousScrollRegion = this.scrollRegion;
|
||||
if (previousScrollRegion && this.modalNavScrollHandler) {
|
||||
previousScrollRegion.removeEventListener(
|
||||
@@ -288,6 +296,8 @@ export class VideoModal {
|
||||
this.modalZapBtn = playerModal.querySelector("#modalZapBtn") || null;
|
||||
this.modalMoreBtn = playerModal.querySelector("#modalMoreBtn") || null;
|
||||
|
||||
this.ambientCanvas = playerModal.querySelector("#ambientCanvas") || null;
|
||||
|
||||
this.modalZapDialog = playerModal.querySelector("#modalZapDialog") || null;
|
||||
this.modalZapForm = playerModal.querySelector("#modalZapForm") || null;
|
||||
this.modalZapAmountInput =
|
||||
@@ -356,6 +366,8 @@ export class VideoModal {
|
||||
this.setShareEnabled(false);
|
||||
this.resetStats();
|
||||
|
||||
this.attachAmbientGlow();
|
||||
|
||||
if (!this.activeVideo && this.playerModal) {
|
||||
this.playerModal.classList.add("hidden");
|
||||
this.playerModal.setAttribute("hidden", "");
|
||||
@@ -407,6 +419,90 @@ export class VideoModal {
|
||||
this.videoEventCleanup = null;
|
||||
}
|
||||
|
||||
getAmbientFallbackColor() {
|
||||
const canvas = this.ambientCanvas;
|
||||
const doc =
|
||||
canvas?.ownerDocument ||
|
||||
this.document ||
|
||||
(typeof document !== "undefined" ? document : null);
|
||||
const win = doc?.defaultView || (typeof window !== "undefined" ? window : null);
|
||||
|
||||
if (doc && win && typeof win.getComputedStyle === "function") {
|
||||
const styles = win.getComputedStyle(doc.documentElement);
|
||||
const tokens = [
|
||||
"--color-overlay-strong",
|
||||
"--color-overlay",
|
||||
"--color-surface",
|
||||
"--color-bg",
|
||||
];
|
||||
|
||||
for (const token of tokens) {
|
||||
const value = styles.getPropertyValue(token);
|
||||
if (value && value.trim()) {
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "#000000";
|
||||
}
|
||||
|
||||
clearAmbientCanvas() {
|
||||
const canvas = this.ambientCanvas;
|
||||
if (!canvas || typeof canvas.getContext !== "function") {
|
||||
return;
|
||||
}
|
||||
const context = canvas.getContext("2d");
|
||||
if (!context) {
|
||||
return;
|
||||
}
|
||||
|
||||
const width = canvas.width || canvas.clientWidth || 0;
|
||||
const height = canvas.height || canvas.clientHeight || 0;
|
||||
if (!width || !height) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.fillStyle = this.getAmbientFallbackColor();
|
||||
context.fillRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
teardownAmbientGlow({ clear = true } = {}) {
|
||||
const detach = this.detachAmbientBackground;
|
||||
this.detachAmbientBackground = null;
|
||||
|
||||
if (typeof detach === "function") {
|
||||
try {
|
||||
detach({ clear });
|
||||
return;
|
||||
} catch (error) {
|
||||
this.log("[VideoModal] Failed to teardown ambient background", error);
|
||||
}
|
||||
}
|
||||
|
||||
if (clear) {
|
||||
this.clearAmbientCanvas();
|
||||
}
|
||||
}
|
||||
|
||||
attachAmbientGlow() {
|
||||
if (!this.modalVideo || !this.ambientCanvas) {
|
||||
this.clearAmbientCanvas();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.detachAmbientBackground = attachAmbientBackground(
|
||||
this.modalVideo,
|
||||
this.ambientCanvas,
|
||||
);
|
||||
} catch (error) {
|
||||
this.detachAmbientBackground = null;
|
||||
this.log("[VideoModal] Failed to attach ambient background", error);
|
||||
this.clearAmbientCanvas();
|
||||
}
|
||||
}
|
||||
|
||||
bindActionButtons() {
|
||||
if (this.copyMagnetBtn) {
|
||||
this.copyMagnetBtn.addEventListener("click", this.handleCopyRequest);
|
||||
|
||||
Reference in new issue
Block a user