Add Frost feature: implement CSS and JS for frost effects and visibility toggle
This commit is contained in:
74
Jellyfin.Plugin.Seasonals/Web/frost.css
Normal file
74
Jellyfin.Plugin.Seasonals/Web/frost.css
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
.frost-container {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 10;
|
||||||
|
overflow: hidden;
|
||||||
|
contain: strict;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frost-layer {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
/* A glowing white-blue gradient from edges */
|
||||||
|
background: radial-gradient(ellipse at center, transparent 60%, rgba(180, 220, 255, 0.4) 100%);
|
||||||
|
box-shadow: inset 0 0 60px rgba(200, 230, 255, 0.5), inset 0 0 120px rgba(255, 255, 255, 0.3);
|
||||||
|
|
||||||
|
filter: url('#frost-filter');
|
||||||
|
|
||||||
|
animation: frost-creep 4s ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle repeating star/crystal pattern */
|
||||||
|
.frost-crystals {
|
||||||
|
position: absolute;
|
||||||
|
top: -5%;
|
||||||
|
left: -5%;
|
||||||
|
width: 110%;
|
||||||
|
height: 110%;
|
||||||
|
/* Use multi-layered star patterns for a random, crystalline spread */
|
||||||
|
background-image:
|
||||||
|
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="60" height="60"><circle cx="10" cy="10" r="1.5" fill="rgba(255,255,255,0.2)"/><circle cx="40" cy="30" r="1" fill="rgba(255,255,255,0.15)"/><circle cx="20" cy="50" r="2" fill="rgba(255,255,255,0.1)"/><path d="M50 10 L51 15 L56 16 L51 17 L50 22 L49 17 L44 16 L49 15 Z" fill="rgba(255,255,255,0.2)"/></svg>'),
|
||||||
|
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40"><circle cx="5" cy="20" r="1" fill="rgba(255,255,255,0.15)"/><circle cx="25" cy="5" r="1.5" fill="rgba(255,255,255,0.1)"/><path d="M20 20 L21 23 L24 24 L21 25 L20 28 L19 25 L16 24 L19 23 Z" fill="rgba(255,255,255,0.15)"/></svg>'),
|
||||||
|
url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30"><circle cx="15" cy="15" r="1" fill="rgba(255,255,255,0.2)"/><circle cx="5" cy="5" r="0.8" fill="rgba(255,255,255,0.1)"/></svg>');
|
||||||
|
background-repeat: repeat;
|
||||||
|
background-size: 110px 110px, 60px 60px, 30px 30px;
|
||||||
|
background-position: 0 0, 15px 15px, 5px 10px;
|
||||||
|
mix-blend-mode: overlay;
|
||||||
|
|
||||||
|
/* Mask out the center so crystals only appear strongly on the edges */
|
||||||
|
-webkit-mask-image: radial-gradient(ellipse at center, transparent 50%, black 100%);
|
||||||
|
mask-image: radial-gradient(ellipse at center, transparent 50%, black 100%);
|
||||||
|
|
||||||
|
animation: frost-shimmer 6s infinite alternate ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes frost-creep {
|
||||||
|
0% {
|
||||||
|
opacity: 0;
|
||||||
|
box-shadow: inset 0 0 10px rgba(200, 230, 255, 0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
box-shadow: inset 0 0 60px rgba(200, 230, 255, 0.5), inset 0 0 120px rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes frost-shimmer {
|
||||||
|
0% {
|
||||||
|
opacity: 0.4;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
opacity: 0.8;
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
Jellyfin.Plugin.Seasonals/Web/frost.js
Normal file
75
Jellyfin.Plugin.Seasonals/Web/frost.js
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
const config = window.SeasonalsPluginConfig?.Frost || {};
|
||||||
|
|
||||||
|
const frost = config.EnableFrost !== undefined ? config.EnableFrost : true;
|
||||||
|
|
||||||
|
let msgPrinted = false;
|
||||||
|
|
||||||
|
function toggleFrost() {
|
||||||
|
const container = document.querySelector('.frost-container');
|
||||||
|
if (!container) 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) {
|
||||||
|
container.style.display = 'none';
|
||||||
|
if (!msgPrinted) {
|
||||||
|
console.log('Frost hidden');
|
||||||
|
msgPrinted = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.style.display = 'block';
|
||||||
|
if (msgPrinted) {
|
||||||
|
console.log('Frost visible');
|
||||||
|
msgPrinted = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(toggleFrost);
|
||||||
|
observer.observe(document.body, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
});
|
||||||
|
|
||||||
|
function createFrost(container) {
|
||||||
|
const frostLayer = document.createElement('div');
|
||||||
|
frostLayer.className = 'frost-layer';
|
||||||
|
|
||||||
|
const frostCrystals = document.createElement('div');
|
||||||
|
frostCrystals.className = 'frost-crystals';
|
||||||
|
|
||||||
|
// An SVG filter to make things look "frozen"/distorted around the edges
|
||||||
|
const svgFilter = document.createElement('div');
|
||||||
|
svgFilter.innerHTML = `
|
||||||
|
<svg style="display:none;">
|
||||||
|
<filter id="frost-filter">
|
||||||
|
<feTurbulence type="fractalNoise" baseFrequency="0.05" numOctaves="3" result="noise" />
|
||||||
|
<feDisplacementMap in="SourceGraphic" in2="noise" scale="5" xChannelSelector="R" yChannelSelector="G" />
|
||||||
|
</filter>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
frostLayer.appendChild(frostCrystals);
|
||||||
|
container.appendChild(frostLayer);
|
||||||
|
container.appendChild(svgFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeFrost() {
|
||||||
|
if (!frost) return;
|
||||||
|
|
||||||
|
const container = document.querySelector('.frost-container') || document.createElement("div");
|
||||||
|
|
||||||
|
if (!document.querySelector('.frost-container')) {
|
||||||
|
container.className = "frost-container";
|
||||||
|
container.setAttribute("aria-hidden", "true");
|
||||||
|
document.body.appendChild(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
createFrost(container);
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeFrost();
|
||||||
Reference in New Issue
Block a user