Add Film Noir feature: implement CSS and JS for film noir effects and toggle functionality
This commit is contained in:
86
Jellyfin.Plugin.Seasonals/Web/filmnoir.css
Normal file
86
Jellyfin.Plugin.Seasonals/Web/filmnoir.css
Normal file
@@ -0,0 +1,86 @@
|
||||
.filmnoir-tint {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 10000;
|
||||
background-color: #8c7355;
|
||||
mix-blend-mode: color;
|
||||
}
|
||||
|
||||
.filmnoir-effects {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
pointer-events: none;
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
/* Film grain */
|
||||
.filmnoir-grain {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200"><filter id="n"><feTurbulence type="fractalNoise" baseFrequency="0.8" numOctaves="3" stitchTiles="stitch"/></filter><rect width="200" height="200" filter="url(%23n)" opacity="0.4"/></svg>');
|
||||
animation: grain-dance 0.2s steps(4) infinite;
|
||||
pointer-events: none;
|
||||
mix-blend-mode: overlay;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
/* Vignette */
|
||||
.filmnoir-vignette {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(circle at center, transparent 50%, rgba(0,0,0,0.8) 120%);
|
||||
box-shadow: inset 0 0 100px rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
/* Occasional flicker and scratch */
|
||||
.filmnoir-scratches {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(to right, transparent 50%, rgba(255,255,255,0.2) 51%, transparent 52%);
|
||||
background-size: 200% 100%;
|
||||
animation: scratch 4s infinite linear, flicker 6s infinite alternate;
|
||||
opacity: 0.2;
|
||||
mix-blend-mode: screen;
|
||||
}
|
||||
|
||||
@keyframes grain-dance {
|
||||
0% { transform: translate(0,0); }
|
||||
25% { transform: translate(-5%,-5%); }
|
||||
50% { transform: translate(-10%,5%); }
|
||||
75% { transform: translate(5%,-10%); }
|
||||
100% { transform: translate(0,0); }
|
||||
}
|
||||
|
||||
@keyframes scratch {
|
||||
0% { background-position: -200% 0; }
|
||||
10% { background-position: 200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
|
||||
@keyframes flicker {
|
||||
0% { opacity: 0.2; }
|
||||
5% { opacity: 0.1; }
|
||||
10% { opacity: 0.3; }
|
||||
15% { opacity: 0.2; }
|
||||
50% { opacity: 0.15; }
|
||||
55% { opacity: 0.25; }
|
||||
100% { opacity: 0.2; }
|
||||
}
|
||||
79
Jellyfin.Plugin.Seasonals/Web/filmnoir.js
Normal file
79
Jellyfin.Plugin.Seasonals/Web/filmnoir.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const config = window.SeasonalsPluginConfig?.FilmNoir || {};
|
||||
const filmnoir = config.EnableFilmNoir !== undefined ? config.EnableFilmNoir : true;
|
||||
|
||||
let msgPrinted = false;
|
||||
|
||||
function toggleFilmNoir() {
|
||||
const tint = document.querySelector('.filmnoir-tint');
|
||||
const effects = document.querySelector('.filmnoir-effects');
|
||||
if (!tint || !effects) return;
|
||||
|
||||
const videoPlayer = document.querySelector('.videoPlayerContainer');
|
||||
const trailerPlayer = document.querySelector('.youtubePlayerContainer');
|
||||
const isDashboard = document.body.classList.contains('dashboardDocument');
|
||||
const hasUserMenu = document.querySelector('#app-user-menu');
|
||||
|
||||
if (videoPlayer || trailerPlayer || isDashboard || hasUserMenu) {
|
||||
tint.style.display = 'none';
|
||||
effects.style.display = 'none';
|
||||
if (!msgPrinted) {
|
||||
console.log('FilmNoir hidden');
|
||||
msgPrinted = true;
|
||||
}
|
||||
} else {
|
||||
tint.style.display = 'block';
|
||||
effects.style.display = 'block';
|
||||
if (msgPrinted) {
|
||||
console.log('FilmNoir visible');
|
||||
msgPrinted = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const observer = new MutationObserver(toggleFilmNoir);
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true
|
||||
});
|
||||
|
||||
|
||||
function createFilmNoir() {
|
||||
if (!document.querySelector('.filmnoir-tint')) {
|
||||
const tint = document.createElement('div');
|
||||
tint.className = 'filmnoir-tint';
|
||||
tint.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(tint);
|
||||
}
|
||||
|
||||
let effects = document.querySelector('.filmnoir-effects');
|
||||
if (!effects) {
|
||||
effects = document.createElement('div');
|
||||
effects.className = 'filmnoir-effects';
|
||||
effects.setAttribute('aria-hidden', 'true');
|
||||
document.body.appendChild(effects);
|
||||
|
||||
const vignette = document.createElement('div');
|
||||
vignette.className = 'filmnoir-vignette';
|
||||
|
||||
const grain = document.createElement('div');
|
||||
grain.className = 'filmnoir-grain';
|
||||
|
||||
const scratches = document.createElement('div');
|
||||
scratches.className = 'filmnoir-scratches';
|
||||
|
||||
effects.appendChild(grain);
|
||||
effects.appendChild(scratches);
|
||||
effects.appendChild(vignette);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function initializeFilmNoir() {
|
||||
if (!filmnoir) return;
|
||||
|
||||
createFilmNoir();
|
||||
toggleFilmNoir();
|
||||
}
|
||||
|
||||
initializeFilmNoir();
|
||||
Reference in New Issue
Block a user