1113 lines
36 KiB
JavaScript
1113 lines
36 KiB
JavaScript
/*!
|
|
* HDR Web Plugin v0.6.2
|
|
* Drop-in pure-black HDR media stage for web pages.
|
|
*/
|
|
(function attach(root, factory) {
|
|
const api = factory();
|
|
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = api;
|
|
return;
|
|
}
|
|
|
|
root.HDREnhancer = api;
|
|
|
|
if (!root.document) return;
|
|
|
|
const script = root.document.currentScript;
|
|
const shouldAutoRun = api.shouldAutoRun(script);
|
|
if (shouldAutoRun) {
|
|
const scriptOptions = api.readScriptOptions(script);
|
|
api.ready(() => api.install({ ...scriptOptions, window: root }));
|
|
}
|
|
})(typeof globalThis !== 'undefined' ? globalThis : this, function createHDREnhancer() {
|
|
const PLUGIN_NAME = 'HDR Web Plugin';
|
|
const VERSION = '0.6.2';
|
|
const STYLE_ID = 'hdr-enhancer-style';
|
|
const OVERLAY_ID = 'hdr-enhancer-overlay';
|
|
const BADGE_ID = 'hdr-enhancer-badge';
|
|
const SCAN_ID = 'hdr-enhancer-scan';
|
|
const CANVAS_ID = 'hdr-enhancer-canvas';
|
|
const VIDEO_ID = 'hdr-enhancer-video';
|
|
const IMAGE_ID = 'hdr-enhancer-image';
|
|
const DEFAULT_HDR_VIDEO = 'media/hdr-galaxy-fireworks.mp4';
|
|
const DEFAULT_HDR_WEBM = 'media/hdr-galaxy-fireworks.webm';
|
|
const DEFAULT_HDR_IMAGE = 'media/hdr-landing-prism.png';
|
|
|
|
function getWindow(input) {
|
|
if (input && input.window) return input.window;
|
|
if (input && input.document) return input;
|
|
if (typeof window !== 'undefined') return window;
|
|
return null;
|
|
}
|
|
|
|
function mediaMatches(target, query) {
|
|
try {
|
|
return Boolean(target && target.matchMedia && target.matchMedia(query).matches);
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function cssSupports(target, property, value) {
|
|
try {
|
|
return Boolean(target && target.CSS && target.CSS.supports(property, value));
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function detectCapabilities(input) {
|
|
const target = getWindow(input) || input || {};
|
|
const rec2020 = mediaMatches(target, '(color-gamut: rec2020)');
|
|
const p3 = mediaMatches(target, '(color-gamut: p3)');
|
|
|
|
return {
|
|
dynamicRange: mediaMatches(target, '(dynamic-range: high)') ? 'high' : 'standard',
|
|
videoDynamicRange: mediaMatches(target, '(video-dynamic-range: high)') ? 'high' : 'standard',
|
|
colorGamut: rec2020 ? 'rec2020' : p3 ? 'p3' : 'srgb',
|
|
supportsDisplayP3: cssSupports(target, 'color', 'color(display-p3 1 0 0)'),
|
|
supportsRec2020: cssSupports(target, 'color', 'color(rec2020 1 0 0)'),
|
|
supportsRelativeColor: cssSupports(target, 'color', 'color(from white srgb r g b)'),
|
|
supportsDynamicRangeLimit: cssSupports(target, 'dynamic-range-limit', 'no-limit'),
|
|
supportsHdrColor: cssSupports(target, 'color', 'hdr-color(color(display-p3 1 1 1) 0, color(rec2100-pq 1 1 1) 2)'),
|
|
prefersReducedMotion: mediaMatches(target, '(prefers-reduced-motion: reduce)')
|
|
};
|
|
}
|
|
|
|
function clamp(value, min, max) {
|
|
const number = Number(value);
|
|
if (!Number.isFinite(number)) return min;
|
|
return Math.max(min, Math.min(max, number));
|
|
}
|
|
|
|
function readDatasetValue(script, name) {
|
|
if (!script || !script.dataset) return undefined;
|
|
return script.dataset[name];
|
|
}
|
|
|
|
function parseBoolean(value) {
|
|
if (value === undefined) return undefined;
|
|
if (value === true || value === 'true' || value === '') return true;
|
|
if (value === false || value === 'false') return false;
|
|
return undefined;
|
|
}
|
|
|
|
function parseNumber(value) {
|
|
if (value === undefined || value === '') return undefined;
|
|
const number = Number(value);
|
|
return Number.isFinite(number) ? number : undefined;
|
|
}
|
|
|
|
function normalizeQuality(value) {
|
|
if (value === 'low' || value === 'balanced' || value === 'high') return value;
|
|
return undefined;
|
|
}
|
|
|
|
function normalizeMotion(value) {
|
|
if (value === 'static' || value === 'burst' || value === 'ambient') return value;
|
|
return undefined;
|
|
}
|
|
|
|
function normalizeMediaType(value) {
|
|
if (value === 'video' || value === 'image') return value;
|
|
return undefined;
|
|
}
|
|
|
|
function scriptAssetBase(script) {
|
|
if (!script || !script.src || typeof URL === 'undefined') return undefined;
|
|
try {
|
|
return new URL('../', script.src).href;
|
|
} catch (_) {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
function readScriptOptions(script) {
|
|
const options = {};
|
|
const intensity = parseNumber(readDatasetValue(script, 'hdrIntensity'));
|
|
const animated = parseBoolean(readDatasetValue(script, 'hdrAnimated'));
|
|
const overlay = parseBoolean(readDatasetValue(script, 'hdrOverlay'));
|
|
const scanlines = parseBoolean(readDatasetValue(script, 'hdrScanlines'));
|
|
const badge = parseBoolean(readDatasetValue(script, 'hdrBadge'));
|
|
const effects = parseBoolean(readDatasetValue(script, 'hdrEffects'));
|
|
const effect = readDatasetValue(script, 'hdrEffect');
|
|
const quality = normalizeQuality(readDatasetValue(script, 'hdrQuality'));
|
|
const fps = parseNumber(readDatasetValue(script, 'hdrFps'));
|
|
const maxDpr = parseNumber(readDatasetValue(script, 'hdrMaxDpr'));
|
|
const motion = normalizeMotion(readDatasetValue(script, 'hdrMotion'));
|
|
const media = parseBoolean(readDatasetValue(script, 'hdrMedia'));
|
|
const blackout = parseBoolean(readDatasetValue(script, 'hdrBlackout'));
|
|
const text = parseBoolean(readDatasetValue(script, 'hdrText'));
|
|
const textBoost = parseNumber(readDatasetValue(script, 'hdrTextBoost'));
|
|
const videoSrc = readDatasetValue(script, 'hdrVideoSrc');
|
|
const webmSrc = readDatasetValue(script, 'hdrWebmSrc');
|
|
const imageSrc = readDatasetValue(script, 'hdrImageSrc');
|
|
const textSrc = readDatasetValue(script, 'hdrTextSrc');
|
|
const mediaType = normalizeMediaType(readDatasetValue(script, 'hdrMediaType'));
|
|
const assetBase = scriptAssetBase(script);
|
|
|
|
if (intensity !== undefined) options.intensity = intensity;
|
|
if (animated !== undefined) options.animated = animated;
|
|
if (overlay !== undefined) options.overlay = overlay;
|
|
if (scanlines !== undefined) options.scanlines = scanlines;
|
|
if (badge !== undefined) options.badge = badge;
|
|
if (effects !== undefined) options.effects = effects;
|
|
if (effect !== undefined && effect !== '') options.effect = effect;
|
|
if (quality !== undefined) options.quality = quality;
|
|
if (fps !== undefined) options.fps = fps;
|
|
if (maxDpr !== undefined) options.maxDpr = maxDpr;
|
|
if (motion !== undefined) options.motion = motion;
|
|
if (media !== undefined) options.media = media;
|
|
if (blackout !== undefined) options.blackout = blackout;
|
|
if (text !== undefined) options.text = text;
|
|
if (textBoost !== undefined) options.textBoost = textBoost;
|
|
if (videoSrc !== undefined && videoSrc !== '') options.videoSrc = videoSrc;
|
|
if (webmSrc !== undefined && webmSrc !== '') options.webmSrc = webmSrc;
|
|
if (imageSrc !== undefined && imageSrc !== '') options.imageSrc = imageSrc;
|
|
if (textSrc !== undefined && textSrc !== '') options.textSrc = textSrc;
|
|
if (mediaType !== undefined) options.mediaType = mediaType;
|
|
if (assetBase !== undefined) options.assetBase = assetBase;
|
|
|
|
return options;
|
|
}
|
|
|
|
function shouldAutoRun(script) {
|
|
const hdrAuto = parseBoolean(readDatasetValue(script, 'hdrAuto'));
|
|
const legacyAuto = parseBoolean(readDatasetValue(script, 'auto'));
|
|
if (hdrAuto !== undefined) return hdrAuto;
|
|
if (legacyAuto !== undefined) return legacyAuto;
|
|
return true;
|
|
}
|
|
|
|
function resolveAssetUrl(path, base) {
|
|
if (!path) return path;
|
|
if (typeof URL === 'undefined' || !base) return path;
|
|
try {
|
|
return new URL(path, base).href;
|
|
} catch (_) {
|
|
return path;
|
|
}
|
|
}
|
|
|
|
function cssUrl(value) {
|
|
if (!value) return 'none';
|
|
return `url("${String(value).replace(/["\\\n\r\f]/g, '\\$&')}")`;
|
|
}
|
|
|
|
function createMediaConfig(options, capabilities) {
|
|
const settings = {
|
|
media: true,
|
|
mediaType: 'video',
|
|
videoSrc: '',
|
|
webmSrc: '',
|
|
imageSrc: '',
|
|
assetBase: '',
|
|
...options
|
|
};
|
|
const caps = capabilities || detectCapabilities();
|
|
const enabled = settings.media !== false && caps.dynamicRange === 'high';
|
|
const mediaType = normalizeMediaType(settings.mediaType) || 'video';
|
|
const src = resolveAssetUrl(settings.videoSrc || DEFAULT_HDR_VIDEO, settings.assetBase);
|
|
const webm = resolveAssetUrl(settings.webmSrc || DEFAULT_HDR_WEBM, settings.assetBase);
|
|
const image = resolveAssetUrl(settings.imageSrc || DEFAULT_HDR_IMAGE, settings.assetBase);
|
|
|
|
if (mediaType === 'image') {
|
|
return {
|
|
enabled,
|
|
type: 'image',
|
|
src: image,
|
|
sources: [
|
|
{
|
|
src: image,
|
|
type: 'image/png'
|
|
}
|
|
],
|
|
reason: enabled ? 'hdr-capable' : settings.media === false ? 'disabled' : 'standard-dynamic-range'
|
|
};
|
|
}
|
|
|
|
return {
|
|
enabled,
|
|
type: 'video',
|
|
src,
|
|
sources: [
|
|
{
|
|
src,
|
|
type: 'video/mp4; codecs="hvc1"'
|
|
},
|
|
{
|
|
src: webm,
|
|
type: 'video/webm; codecs="vp09.02.10.10.01.09.16.09.01"'
|
|
}
|
|
],
|
|
reason: enabled ? 'hdr-capable' : settings.media === false ? 'disabled' : 'standard-dynamic-range'
|
|
};
|
|
}
|
|
|
|
function createEffectConfig(options, capabilities) {
|
|
const settings = {
|
|
intensity: 1,
|
|
animated: true,
|
|
effects: true,
|
|
effect: 'cosmic-fireworks',
|
|
quality: 'low',
|
|
motion: 'static',
|
|
...options
|
|
};
|
|
const caps = capabilities || detectCapabilities();
|
|
const intensity = clamp(settings.intensity, 0.35, 1.85);
|
|
const reduced = Boolean(caps.prefersReducedMotion) || settings.animated === false;
|
|
const quality = normalizeQuality(settings.quality) || 'balanced';
|
|
const profiles = {
|
|
low: {
|
|
dprCap: 1,
|
|
targetFps: 12,
|
|
starBase: 30,
|
|
galaxyBase: 36,
|
|
burstBase: 14,
|
|
maxSparks: 54,
|
|
maxComets: 1,
|
|
shadowBlur: 6
|
|
},
|
|
balanced: {
|
|
dprCap: 1.15,
|
|
targetFps: 24,
|
|
starBase: 54,
|
|
galaxyBase: 68,
|
|
burstBase: 20,
|
|
maxSparks: 90,
|
|
maxComets: 2,
|
|
shadowBlur: 10
|
|
},
|
|
high: {
|
|
dprCap: 1.35,
|
|
targetFps: 30,
|
|
starBase: 82,
|
|
galaxyBase: 98,
|
|
burstBase: 30,
|
|
maxSparks: 150,
|
|
maxComets: 3,
|
|
shadowBlur: 14
|
|
}
|
|
};
|
|
const profile = profiles[quality];
|
|
const motion = normalizeMotion(settings.motion) || 'static';
|
|
const shouldAnimate = motion === 'ambient' && !reduced;
|
|
const targetFps = Math.round(clamp(settings.fps || profile.targetFps, 12, 60));
|
|
const dprCap = clamp(settings.maxDpr || profile.dprCap, 1, 1.75);
|
|
const motionScale = reduced ? 0.18 : 1;
|
|
const burstParticleCount = motion === 'static' ? 0 : Math.round(profile.burstBase + intensity * 10);
|
|
const maxSparks = motion === 'static' ? 0 : profile.maxSparks;
|
|
|
|
return {
|
|
enabled: settings.effects !== false,
|
|
effect: settings.effect || 'cosmic-fireworks',
|
|
quality,
|
|
motion,
|
|
shouldAnimate,
|
|
intensity,
|
|
galaxy: settings.effect !== 'fireworks-only',
|
|
fireworkBursts: settings.effect !== 'galaxy-only',
|
|
stars: true,
|
|
motionScale,
|
|
targetFps: shouldAnimate ? targetFps : 1,
|
|
dprCap: reduced ? 1 : dprCap,
|
|
starCount: Math.round(profile.starBase + intensity * 10),
|
|
galaxyCount: Math.round(profile.galaxyBase + intensity * 14),
|
|
burstParticleCount,
|
|
maxSparks,
|
|
maxComets: motion === 'static' ? 0 : profile.maxComets,
|
|
shadowBlur: profile.shadowBlur,
|
|
colorSpace: caps.colorGamut === 'p3' || caps.colorGamut === 'rec2020' ? 'p3' : 'srgb'
|
|
};
|
|
}
|
|
|
|
function createStyleText(options, capabilities) {
|
|
const settings = {
|
|
intensity: 1,
|
|
animated: true,
|
|
text: true,
|
|
...options
|
|
};
|
|
const caps = capabilities || detectCapabilities();
|
|
const intensity = clamp(settings.intensity, 0.35, 1.65);
|
|
const textBoost = clamp(settings.textBoost || 1.9, 1, 4);
|
|
const bloom = (0.65 * intensity).toFixed(2);
|
|
const glow = Math.round(34 * intensity);
|
|
const longGlow = Math.round(96 * intensity);
|
|
const saturation = (1.08 + intensity * 0.2).toFixed(2);
|
|
const contrast = (1.02 + intensity * 0.12).toFixed(2);
|
|
const motionState = settings.animated && !caps.prefersReducedMotion ? 'running' : 'paused';
|
|
|
|
return `
|
|
:root {
|
|
--hdr-fallback-cyan: #38faff;
|
|
--hdr-fallback-magenta: #ff3df2;
|
|
--hdr-fallback-lime: #baff37;
|
|
--hdr-fallback-amber: #ffd95a;
|
|
--hdr-fallback-blue: #4d7cff;
|
|
--hdr-black: #000000;
|
|
--hdr-panel: rgba(0, 0, 0, 0.78);
|
|
--hdr-line: rgba(96, 255, 255, 0.26);
|
|
--hdr-cyan: var(--hdr-fallback-cyan);
|
|
--hdr-magenta: var(--hdr-fallback-magenta);
|
|
--hdr-lime: var(--hdr-fallback-lime);
|
|
--hdr-amber: var(--hdr-fallback-amber);
|
|
--hdr-blue: var(--hdr-fallback-blue);
|
|
--hdr-bloom: ${bloom};
|
|
--hdr-glow: ${glow}px;
|
|
--hdr-long-glow: ${longGlow}px;
|
|
--hdr-text-boost: ${textBoost.toFixed(2)};
|
|
--hdr-texture-url: none;
|
|
}
|
|
|
|
@media (color-gamut: p3) {
|
|
:root {
|
|
--hdr-cyan: color(display-p3 0.08 0.98 1);
|
|
--hdr-magenta: color(display-p3 1 0.12 0.92);
|
|
--hdr-lime: color(display-p3 0.72 1 0.1);
|
|
--hdr-amber: color(display-p3 1 0.82 0.22);
|
|
--hdr-blue: color(display-p3 0.22 0.42 1);
|
|
--hdr-line: color(display-p3 0.3 1 1 / 0.32);
|
|
}
|
|
}
|
|
|
|
@media (color-gamut: rec2020) {
|
|
:root {
|
|
--hdr-cyan: color(rec2020 0.12 0.95 1);
|
|
--hdr-magenta: color(rec2020 0.95 0.1 0.9);
|
|
--hdr-lime: color(rec2020 0.7 1 0.08);
|
|
--hdr-amber: color(rec2020 1 0.78 0.1);
|
|
--hdr-blue: color(rec2020 0.18 0.34 1);
|
|
}
|
|
}
|
|
|
|
@supports (dynamic-range-limit: no-limit) {
|
|
html[data-hdr-boosted="true"],
|
|
body[data-hdr-enhanced="true"],
|
|
#${VIDEO_ID},
|
|
#${IMAGE_ID},
|
|
body[data-hdr-enhanced="true"] :is(.hdr-title, [data-hdr-title], [data-hdr-text="true"]) {
|
|
dynamic-range-limit: no-limit;
|
|
}
|
|
}
|
|
|
|
html[data-hdr-boosted="true"] {
|
|
background: #000000 !important;
|
|
color-scheme: dark;
|
|
}
|
|
|
|
body[data-hdr-enhanced="true"] {
|
|
background: #000000 !important;
|
|
color: #eefcff !important;
|
|
color-scheme: dark;
|
|
}
|
|
|
|
body[data-hdr-enhanced="true"]::before,
|
|
body[data-hdr-enhanced="true"]::after {
|
|
content: none !important;
|
|
display: none !important;
|
|
background: none !important;
|
|
box-shadow: none !important;
|
|
filter: none !important;
|
|
}
|
|
|
|
body[data-hdr-enhanced="true"] :is(h1, h2, h3, .hdr-title, [data-hdr-title]) {
|
|
color: #f8feff !important;
|
|
text-shadow:
|
|
0 0 0.5px #fff,
|
|
0 0 calc(var(--hdr-glow) * 0.55) var(--hdr-cyan),
|
|
0 0 var(--hdr-long-glow) color-mix(in srgb, var(--hdr-magenta), transparent 30%);
|
|
}
|
|
|
|
body[data-hdr-enhanced="true"][data-hdr-text="true"] :is(h1, h2, h3, .hdr-title, [data-hdr-title], [data-hdr-text="true"]) {
|
|
filter: brightness(var(--hdr-text-boost)) saturate(1.24) contrast(1.08);
|
|
transform: translateZ(0);
|
|
}
|
|
|
|
@supports (color: hdr-color(color(display-p3 1 1 1) 0, color(rec2100-pq 1 1 1) 2)) {
|
|
body[data-hdr-enhanced="true"][data-hdr-text="true"] :is(h1, h2, h3, .hdr-title, [data-hdr-title], [data-hdr-text="true"]) {
|
|
color: hdr-color(color(display-p3 1 1 1) 0, color(rec2100-pq 1 1 1) 2) !important;
|
|
}
|
|
}
|
|
|
|
@supports ((-webkit-background-clip: text) or (background-clip: text)) {
|
|
body[data-hdr-enhanced="true"][data-hdr-text="true"] :is(.hdr-title, [data-hdr-text="true"]) {
|
|
background-image: var(--hdr-texture-url);
|
|
background-position: center;
|
|
background-repeat: no-repeat;
|
|
background-size: cover;
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
color: transparent !important;
|
|
-webkit-text-fill-color: transparent;
|
|
text-shadow:
|
|
0 0 1px rgba(255, 255, 255, 0.9),
|
|
0 0 calc(var(--hdr-glow) * 0.72) color-mix(in srgb, var(--hdr-cyan), transparent 8%),
|
|
0 0 calc(var(--hdr-long-glow) * 1.12) color-mix(in srgb, var(--hdr-magenta), transparent 12%);
|
|
}
|
|
}
|
|
|
|
body[data-hdr-enhanced="true"] :is(a, button, .button, [role="button"], input[type="submit"]) {
|
|
border-color: color-mix(in srgb, var(--hdr-cyan), transparent 30%) !important;
|
|
box-shadow:
|
|
0 0 calc(var(--hdr-glow) * 0.35) color-mix(in srgb, var(--hdr-cyan), transparent 28%),
|
|
inset 0 0 18px rgba(255, 255, 255, 0.08);
|
|
}
|
|
|
|
body[data-hdr-enhanced="true"] :is(a, button, .button, [role="button"], input[type="submit"]):hover {
|
|
color: #ffffff !important;
|
|
text-shadow: 0 0 18px var(--hdr-cyan);
|
|
box-shadow:
|
|
0 0 var(--hdr-glow) var(--hdr-cyan),
|
|
0 0 var(--hdr-long-glow) color-mix(in srgb, var(--hdr-magenta), transparent 36%);
|
|
}
|
|
|
|
body[data-hdr-enhanced="true"] :is(article, section, main, aside, header, footer, nav, .card, [class*="card"], [class*="panel"]) {
|
|
border-color: color-mix(in srgb, var(--hdr-cyan), transparent 76%) !important;
|
|
}
|
|
|
|
body[data-hdr-enhanced="true"] ::selection {
|
|
background: var(--hdr-cyan);
|
|
color: #00050a;
|
|
}
|
|
|
|
#${OVERLAY_ID},
|
|
#${SCAN_ID},
|
|
#${CANVAS_ID},
|
|
#${VIDEO_ID},
|
|
#${IMAGE_ID} {
|
|
pointer-events: none;
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 2147483000;
|
|
}
|
|
|
|
#${VIDEO_ID},
|
|
#${IMAGE_ID} {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
z-index: 0;
|
|
object-fit: cover;
|
|
background: #000000;
|
|
opacity: 1;
|
|
mix-blend-mode: normal;
|
|
}
|
|
|
|
:where(body[data-hdr-enhanced="true"] > :not(#${OVERLAY_ID}):not(#${SCAN_ID}):not(#${CANVAS_ID}):not(#${VIDEO_ID}):not(#${IMAGE_ID}):not(#${BADGE_ID})) {
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
#${CANVAS_ID} {
|
|
width: 100vw;
|
|
height: 100vh;
|
|
z-index: 2147482999;
|
|
opacity: ${Math.min(0.84, 0.48 + intensity * 0.16).toFixed(2)};
|
|
mix-blend-mode: screen;
|
|
}
|
|
|
|
#${OVERLAY_ID} {
|
|
mix-blend-mode: screen;
|
|
opacity: ${Math.min(0.72, 0.34 + intensity * 0.17).toFixed(2)};
|
|
background:
|
|
radial-gradient(circle at var(--hdr-orb-x, 20%) var(--hdr-orb-y, 20%), var(--hdr-cyan), transparent 18rem),
|
|
radial-gradient(circle at calc(100% - var(--hdr-orb-x, 20%)) 30%, var(--hdr-magenta), transparent 20rem),
|
|
linear-gradient(115deg, transparent 0%, rgba(255,255,255,0.08) 48%, transparent 58%);
|
|
filter: blur(18px) saturate(1.25);
|
|
animation: hdrAurora 8s ease-in-out infinite alternate;
|
|
animation-play-state: ${motionState};
|
|
}
|
|
|
|
#${SCAN_ID} {
|
|
opacity: ${Math.min(0.38, 0.1 + intensity * 0.12).toFixed(2)};
|
|
background:
|
|
repeating-linear-gradient(
|
|
180deg,
|
|
rgba(255,255,255,0.06) 0,
|
|
rgba(255,255,255,0.06) 1px,
|
|
transparent 1px,
|
|
transparent 7px
|
|
);
|
|
mix-blend-mode: soft-light;
|
|
animation: hdrScan 5s linear infinite;
|
|
animation-play-state: ${motionState};
|
|
}
|
|
|
|
#${BADGE_ID} {
|
|
position: fixed;
|
|
right: 18px;
|
|
bottom: 18px;
|
|
z-index: 2147483001;
|
|
border: 1px solid color-mix(in srgb, var(--hdr-cyan), transparent 30%);
|
|
border-radius: 999px;
|
|
padding: 8px 12px;
|
|
background: rgba(1, 2, 10, 0.72);
|
|
box-shadow:
|
|
0 0 22px color-mix(in srgb, var(--hdr-cyan), transparent 30%),
|
|
inset 0 0 18px rgba(255, 255, 255, 0.08);
|
|
color: #f8feff;
|
|
font: 700 11px/1.2 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
backdrop-filter: blur(14px);
|
|
}
|
|
|
|
#${BADGE_ID} strong {
|
|
color: var(--hdr-cyan);
|
|
text-shadow: 0 0 16px var(--hdr-cyan);
|
|
}
|
|
|
|
@media (dynamic-range: high) {
|
|
#${OVERLAY_ID} {
|
|
opacity: ${Math.min(0.86, 0.44 + intensity * 0.2).toFixed(2)};
|
|
filter: blur(15px) saturate(1.55) brightness(1.18);
|
|
}
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
#${OVERLAY_ID},
|
|
#${SCAN_ID},
|
|
body[data-hdr-enhanced="true"] * {
|
|
animation-duration: 1ms !important;
|
|
animation-iteration-count: 1 !important;
|
|
scroll-behavior: auto !important;
|
|
}
|
|
}
|
|
|
|
@keyframes hdrAurora {
|
|
0% {
|
|
--hdr-orb-x: 12%;
|
|
--hdr-orb-y: 22%;
|
|
transform: translate3d(-1.5%, -1%, 0) scale(1);
|
|
}
|
|
45% {
|
|
--hdr-orb-x: 62%;
|
|
--hdr-orb-y: 14%;
|
|
transform: translate3d(1%, 0.5%, 0) scale(1.04);
|
|
}
|
|
100% {
|
|
--hdr-orb-x: 38%;
|
|
--hdr-orb-y: 72%;
|
|
transform: translate3d(0.8%, 1.4%, 0) scale(1.08);
|
|
}
|
|
}
|
|
|
|
@keyframes hdrScan {
|
|
from { transform: translateY(-7px); }
|
|
to { transform: translateY(7px); }
|
|
}
|
|
`.trim();
|
|
}
|
|
|
|
function ready(callback) {
|
|
const win = getWindow();
|
|
if (!win || !win.document) return;
|
|
if (win.document.readyState === 'loading') {
|
|
win.document.addEventListener('DOMContentLoaded', callback, { once: true });
|
|
return;
|
|
}
|
|
callback();
|
|
}
|
|
|
|
function appendNode(document, id, className, tagName) {
|
|
const existing = document.getElementById(id);
|
|
if (existing) return existing;
|
|
|
|
const node = document.createElement(tagName || 'div');
|
|
node.id = id;
|
|
if (className) node.className = className;
|
|
node.setAttribute('aria-hidden', 'true');
|
|
document.body.appendChild(node);
|
|
return node;
|
|
}
|
|
|
|
function appendHdrVideo(document, config) {
|
|
const video = appendNode(document, VIDEO_ID, 'hdr-enhancer-video', 'video');
|
|
video.dataset.hdrMedia = 'true';
|
|
video.dataset.hdrType = config.type;
|
|
video.dataset.hdrSource = config.src;
|
|
video.muted = true;
|
|
video.loop = true;
|
|
video.autoplay = true;
|
|
video.playsInline = true;
|
|
video.preload = 'auto';
|
|
video.innerHTML = '';
|
|
if (typeof video.removeAttribute === 'function') video.removeAttribute('src');
|
|
for (const sourceConfig of config.sources) {
|
|
const source = document.createElement('source');
|
|
source.src = sourceConfig.src;
|
|
source.type = sourceConfig.type;
|
|
source.setAttribute('src', sourceConfig.src);
|
|
source.setAttribute('type', sourceConfig.type);
|
|
video.appendChild(source);
|
|
}
|
|
video.setAttribute('muted', '');
|
|
video.setAttribute('loop', '');
|
|
video.setAttribute('autoplay', '');
|
|
video.setAttribute('playsinline', '');
|
|
video.setAttribute('preload', 'auto');
|
|
video.setAttribute('poster', '');
|
|
|
|
if (typeof video.load === 'function') {
|
|
try {
|
|
video.load();
|
|
} catch (_) {
|
|
// Older embedded browsers may defer loading until playback starts.
|
|
}
|
|
}
|
|
|
|
if (typeof video.play === 'function') {
|
|
try {
|
|
const playback = video.play();
|
|
if (playback && typeof playback.catch === 'function') playback.catch(() => {});
|
|
} catch (_) {
|
|
// Autoplay can still be denied in unusual browser policies; muted video is retried by the browser.
|
|
}
|
|
}
|
|
|
|
return video;
|
|
}
|
|
|
|
function appendHdrImage(document, config) {
|
|
const image = appendNode(document, IMAGE_ID, 'hdr-enhancer-image', 'img');
|
|
image.dataset.hdrMedia = 'true';
|
|
image.dataset.hdrType = config.type;
|
|
image.dataset.hdrSource = config.src;
|
|
image.src = config.src;
|
|
image.alt = '';
|
|
image.decoding = 'async';
|
|
image.loading = 'eager';
|
|
image.setAttribute('src', config.src);
|
|
image.setAttribute('alt', '');
|
|
image.setAttribute('decoding', 'async');
|
|
image.setAttribute('loading', 'eager');
|
|
return image;
|
|
}
|
|
|
|
function stopVisualEffects(win) {
|
|
const state = win && win.__hdrEnhancerFx;
|
|
if (!state) return;
|
|
if (state.frame && win.cancelAnimationFrame) {
|
|
win.cancelAnimationFrame(state.frame);
|
|
}
|
|
if (state.timer && win.clearTimeout) {
|
|
win.clearTimeout(state.timer);
|
|
}
|
|
if (state.resize && win.removeEventListener) {
|
|
win.removeEventListener('resize', state.resize);
|
|
}
|
|
if (state.visibility && win.document && win.document.removeEventListener) {
|
|
win.document.removeEventListener('visibilitychange', state.visibility);
|
|
}
|
|
delete win.__hdrEnhancerFx;
|
|
}
|
|
|
|
function startVisualEffects(win, canvas, config) {
|
|
if (!win || !canvas || !config.enabled || typeof canvas.getContext !== 'function') return;
|
|
|
|
stopVisualEffects(win);
|
|
|
|
let context;
|
|
try {
|
|
context = canvas.getContext('2d', {
|
|
alpha: true,
|
|
colorSpace: config.colorSpace === 'p3' ? 'display-p3' : 'srgb'
|
|
});
|
|
} catch (_) {
|
|
context = null;
|
|
}
|
|
const ctx = context || canvas.getContext('2d', { alpha: true });
|
|
if (!ctx) return;
|
|
|
|
const random = () => Math.random();
|
|
const palette = [
|
|
[56, 250, 255],
|
|
[255, 61, 242],
|
|
[186, 255, 55],
|
|
[255, 217, 90],
|
|
[77, 124, 255],
|
|
[255, 255, 255]
|
|
];
|
|
const color = (rgb, alpha) => `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, ${alpha})`;
|
|
const state = {
|
|
frame: 0,
|
|
timer: 0,
|
|
width: 0,
|
|
height: 0,
|
|
ratio: 1,
|
|
ticks: 0,
|
|
stars: [],
|
|
galaxy: [],
|
|
sparks: [],
|
|
comets: []
|
|
};
|
|
state.frameInterval = 1000 / config.targetFps;
|
|
state.lastRender = 0;
|
|
state.animate = config.shouldAnimate;
|
|
state.paused = false;
|
|
|
|
function resize() {
|
|
const width = Math.max(1, win.innerWidth || canvas.clientWidth || 1280);
|
|
const height = Math.max(1, win.innerHeight || canvas.clientHeight || 720);
|
|
const ratio = Math.min(config.dprCap, Math.max(1, win.devicePixelRatio || 1));
|
|
state.width = width;
|
|
state.height = height;
|
|
state.ratio = ratio;
|
|
canvas.width = Math.round(width * ratio);
|
|
canvas.height = Math.round(height * ratio);
|
|
canvas.style.width = `${width}px`;
|
|
canvas.style.height = `${height}px`;
|
|
canvas.dataset.hdrQuality = config.quality;
|
|
canvas.dataset.hdrMotion = config.motion;
|
|
canvas.dataset.hdrFps = String(config.targetFps);
|
|
canvas.dataset.hdrDpr = String(ratio);
|
|
canvas.dataset.hdrStars = String(config.starCount);
|
|
canvas.dataset.hdrGalaxy = String(config.galaxyCount);
|
|
ctx.setTransform(ratio, 0, 0, ratio, 0, 0);
|
|
|
|
state.stars = Array.from({ length: config.starCount }, () => ({
|
|
x: random() * width,
|
|
y: random() * height,
|
|
r: random() * 1.7 + 0.35,
|
|
twinkle: random() * Math.PI * 2,
|
|
rgb: palette[Math.floor(random() * palette.length)]
|
|
}));
|
|
state.galaxy = Array.from({ length: config.galaxyCount }, () => ({
|
|
radius: random() ** 0.55 * Math.min(width, height) * 0.48,
|
|
angle: random() * Math.PI * 2,
|
|
lane: random() > 0.5 ? 1 : -1,
|
|
size: random() * 2.2 + 0.4,
|
|
speed: (random() * 0.0007 + 0.00025) * (random() > 0.5 ? 1 : -1),
|
|
rgb: palette[Math.floor(random() * (palette.length - 1))]
|
|
}));
|
|
}
|
|
|
|
function spawnBurst(forceX, forceY) {
|
|
if (!config.fireworkBursts) return;
|
|
if (state.sparks.length > config.maxSparks * 0.72) return;
|
|
const width = state.width;
|
|
const height = state.height;
|
|
const x = forceX || width * (0.16 + random() * 0.68);
|
|
const y = forceY || height * (0.12 + random() * 0.45);
|
|
const base = palette[Math.floor(random() * palette.length)];
|
|
const count = Math.min(
|
|
config.maxSparks - state.sparks.length,
|
|
Math.round(config.burstParticleCount * (0.82 + random() * 0.38))
|
|
);
|
|
for (let i = 0; i < count; i += 1) {
|
|
const angle = (Math.PI * 2 * i) / count + random() * 0.22;
|
|
const speed = (1.4 + random() * 5.4) * config.motionScale * config.intensity;
|
|
state.sparks.push({
|
|
x,
|
|
y,
|
|
lastX: x,
|
|
lastY: y,
|
|
vx: Math.cos(angle) * speed,
|
|
vy: Math.sin(angle) * speed,
|
|
life: 64 + random() * 54,
|
|
age: 0,
|
|
size: 1.1 + random() * 2.4,
|
|
rgb: random() > 0.28 ? base : palette[Math.floor(random() * palette.length)]
|
|
});
|
|
}
|
|
}
|
|
|
|
function spawnComet() {
|
|
if (state.comets.length >= config.maxComets) return;
|
|
const height = state.height;
|
|
state.comets.push({
|
|
x: -80,
|
|
y: height * (0.08 + random() * 0.42),
|
|
vx: (10 + random() * 8) * config.motionScale,
|
|
vy: (2.4 + random() * 2.8) * config.motionScale,
|
|
life: 90,
|
|
age: 0,
|
|
rgb: palette[Math.floor(random() * palette.length)]
|
|
});
|
|
}
|
|
|
|
function drawGlowCircle(x, y, radius, rgb, alpha) {
|
|
const gradient = ctx.createRadialGradient(x, y, 0, x, y, radius);
|
|
gradient.addColorStop(0, color(rgb, alpha));
|
|
gradient.addColorStop(0.35, color(rgb, alpha * 0.32));
|
|
gradient.addColorStop(1, color(rgb, 0));
|
|
ctx.fillStyle = gradient;
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
function drawBloomPoint(x, y, radius, rgb, alpha, blur) {
|
|
ctx.shadowColor = color(rgb, Math.min(0.9, alpha + 0.22));
|
|
ctx.shadowBlur = blur;
|
|
ctx.fillStyle = color(rgb, alpha);
|
|
ctx.beginPath();
|
|
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
ctx.fill();
|
|
}
|
|
|
|
function drawFrame(now) {
|
|
const timestamp = typeof now === 'number' ? now : Date.now();
|
|
const raf = win.requestAnimationFrame || ((callback) => win.setTimeout(callback, 16));
|
|
if (state.lastRender && timestamp - state.lastRender < state.frameInterval) {
|
|
state.frame = raf(drawFrame);
|
|
return;
|
|
}
|
|
state.lastRender = timestamp;
|
|
|
|
if (state.paused) {
|
|
state.frame = raf(drawFrame);
|
|
return;
|
|
}
|
|
|
|
const width = state.width;
|
|
const height = state.height;
|
|
state.ticks += config.motionScale;
|
|
|
|
ctx.globalCompositeOperation = 'source-over';
|
|
ctx.clearRect(0, 0, width, height);
|
|
ctx.fillStyle = 'rgba(0, 0, 12, 0.12)';
|
|
ctx.fillRect(0, 0, width, height);
|
|
|
|
ctx.globalCompositeOperation = 'lighter';
|
|
|
|
if (config.stars) {
|
|
for (const star of state.stars) {
|
|
const pulse = 0.42 + Math.sin(state.ticks * 0.045 + star.twinkle) * 0.36;
|
|
drawBloomPoint(
|
|
star.x,
|
|
star.y,
|
|
star.r,
|
|
star.rgb,
|
|
Math.max(0.26, pulse * 0.7),
|
|
config.shadowBlur * 0.45
|
|
);
|
|
}
|
|
}
|
|
|
|
if (config.galaxy) {
|
|
const cx = width * 0.58;
|
|
const cy = height * 0.5;
|
|
for (const dust of state.galaxy) {
|
|
dust.angle += dust.speed * state.ticks * config.motionScale;
|
|
const arm = dust.angle + dust.lane * dust.radius * 0.011 + state.ticks * 0.0025;
|
|
const x = cx + Math.cos(arm) * dust.radius * 1.34;
|
|
const y = cy + Math.sin(arm) * dust.radius * 0.38;
|
|
const alpha = 0.2 + (1 - dust.radius / (Math.min(width, height) * 0.5)) * 0.46;
|
|
drawBloomPoint(x, y, dust.size, dust.rgb, alpha * 0.78, config.shadowBlur);
|
|
}
|
|
drawGlowCircle(cx, cy, Math.min(width, height) * 0.22, [255, 255, 255], 0.22 * config.intensity);
|
|
drawGlowCircle(cx, cy, Math.min(width, height) * 0.36, [255, 61, 242], 0.11 * config.intensity);
|
|
}
|
|
|
|
if (config.fireworkBursts && state.ticks % Math.max(22, 54 - config.intensity * 15) < 1) {
|
|
spawnBurst();
|
|
}
|
|
if (state.ticks % 130 < 1) {
|
|
spawnComet();
|
|
}
|
|
|
|
ctx.lineCap = 'round';
|
|
for (let i = state.sparks.length - 1; i >= 0; i -= 1) {
|
|
const spark = state.sparks[i];
|
|
spark.age += 1;
|
|
spark.lastX = spark.x;
|
|
spark.lastY = spark.y;
|
|
spark.x += spark.vx;
|
|
spark.y += spark.vy;
|
|
spark.vx *= 0.986;
|
|
spark.vy = spark.vy * 0.986 + 0.038 * config.motionScale;
|
|
const left = 1 - spark.age / spark.life;
|
|
if (left <= 0) {
|
|
state.sparks.splice(i, 1);
|
|
continue;
|
|
}
|
|
ctx.strokeStyle = color(spark.rgb, left * 0.72);
|
|
ctx.lineWidth = spark.size * (0.8 + left);
|
|
ctx.shadowColor = color(spark.rgb, 0.85);
|
|
ctx.shadowBlur = config.shadowBlur * 0.95;
|
|
ctx.beginPath();
|
|
ctx.moveTo(spark.lastX, spark.lastY);
|
|
ctx.lineTo(spark.x, spark.y);
|
|
ctx.stroke();
|
|
drawBloomPoint(spark.x, spark.y, spark.size * 1.2, spark.rgb, left * 0.66, config.shadowBlur);
|
|
}
|
|
|
|
for (let i = state.comets.length - 1; i >= 0; i -= 1) {
|
|
const comet = state.comets[i];
|
|
comet.age += 1;
|
|
comet.x += comet.vx;
|
|
comet.y += comet.vy;
|
|
const left = 1 - comet.age / comet.life;
|
|
if (left <= 0 || comet.x > width + 220) {
|
|
state.comets.splice(i, 1);
|
|
continue;
|
|
}
|
|
ctx.strokeStyle = color(comet.rgb, 0.64 * left);
|
|
ctx.lineWidth = 3.8;
|
|
ctx.shadowColor = color(comet.rgb, 0.95);
|
|
ctx.shadowBlur = config.shadowBlur * 1.35;
|
|
ctx.beginPath();
|
|
ctx.moveTo(comet.x - 150, comet.y - 36);
|
|
ctx.lineTo(comet.x, comet.y);
|
|
ctx.stroke();
|
|
drawGlowCircle(comet.x, comet.y, 18 + 18 * config.intensity, comet.rgb, 0.52 * left);
|
|
}
|
|
|
|
ctx.shadowBlur = 0;
|
|
if (state.animate) {
|
|
state.frame = raf(drawFrame);
|
|
} else {
|
|
state.frame = 0;
|
|
}
|
|
}
|
|
|
|
resize();
|
|
if (config.motion !== 'static') {
|
|
spawnBurst(state.width * 0.32, state.height * 0.34);
|
|
}
|
|
state.resize = resize;
|
|
if (win.addEventListener) win.addEventListener('resize', resize);
|
|
state.visibility = () => {
|
|
state.paused = Boolean(win.document && win.document.hidden);
|
|
if (!state.paused && state.animate && !state.frame) {
|
|
state.frame = (win.requestAnimationFrame || ((callback) => win.setTimeout(callback, 16)))(drawFrame);
|
|
}
|
|
};
|
|
if (win.document && win.document.addEventListener) {
|
|
win.document.addEventListener('visibilitychange', state.visibility);
|
|
}
|
|
win.__hdrEnhancerFx = state;
|
|
drawFrame();
|
|
}
|
|
|
|
function enhance(options) {
|
|
const settings = {
|
|
intensity: 1,
|
|
animated: true,
|
|
overlay: false,
|
|
scanlines: false,
|
|
badge: true,
|
|
effects: false,
|
|
effect: 'cosmic-fireworks',
|
|
media: true,
|
|
blackout: true,
|
|
text: true,
|
|
...options
|
|
};
|
|
const win = getWindow(settings);
|
|
const document = win && win.document;
|
|
|
|
if (!document || !document.head || !document.body) {
|
|
return { active: false, reason: 'missing-document' };
|
|
}
|
|
|
|
const capabilities = detectCapabilities(win);
|
|
const mediaConfig = createMediaConfig(settings, capabilities);
|
|
let reused = true;
|
|
let style = document.getElementById(STYLE_ID);
|
|
if (!style) {
|
|
reused = false;
|
|
style = document.createElement('style');
|
|
style.id = STYLE_ID;
|
|
style.textContent = createStyleText(settings, capabilities);
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
document.documentElement.dataset.hdrBoosted = 'true';
|
|
document.documentElement.style.setProperty('--hdr-runtime-gamut', capabilities.colorGamut);
|
|
document.documentElement.style.setProperty(
|
|
'--hdr-texture-url',
|
|
cssUrl(resolveAssetUrl(settings.textSrc, settings.assetBase) || mediaConfig.src)
|
|
);
|
|
document.body.dataset.hdrEnhanced = 'true';
|
|
document.body.dataset.hdrMode = capabilities.colorGamut;
|
|
document.body.dataset.hdrEffect = settings.effect;
|
|
document.body.dataset.hdrMedia = mediaConfig.enabled ? 'true' : 'false';
|
|
document.body.dataset.hdrText = settings.text === false ? 'false' : 'true';
|
|
|
|
if (mediaConfig.enabled && mediaConfig.type === 'image') {
|
|
appendHdrImage(document, mediaConfig);
|
|
}
|
|
if (mediaConfig.enabled && mediaConfig.type === 'video') {
|
|
appendHdrVideo(document, mediaConfig);
|
|
}
|
|
if (settings.effects !== false && !mediaConfig.enabled) {
|
|
const canvas = appendNode(document, CANVAS_ID, 'hdr-enhancer-canvas', 'canvas');
|
|
startVisualEffects(win, canvas, createEffectConfig(settings, capabilities));
|
|
}
|
|
if (settings.overlay) {
|
|
appendNode(document, OVERLAY_ID, 'hdr-enhancer-overlay');
|
|
}
|
|
if (settings.scanlines) {
|
|
appendNode(document, SCAN_ID, 'hdr-enhancer-scan');
|
|
}
|
|
if (settings.badge) {
|
|
const badge = appendNode(document, BADGE_ID, 'hdr-enhancer-badge');
|
|
const mediaLabel = mediaConfig.enabled ? 'MEDIA' : 'CAPABLE';
|
|
badge.innerHTML = `<strong>HDR ${mediaLabel}</strong> ${capabilities.dynamicRange.toUpperCase()} / ${capabilities.colorGamut.toUpperCase()}`;
|
|
}
|
|
|
|
return { active: true, reused, capabilities, media: mediaConfig };
|
|
}
|
|
|
|
function install(options) {
|
|
return enhance(options);
|
|
}
|
|
|
|
function disable(options) {
|
|
const win = getWindow(options || {});
|
|
const document = win && win.document;
|
|
if (!document) return { active: false, reason: 'missing-document' };
|
|
|
|
stopVisualEffects(win);
|
|
|
|
[STYLE_ID, OVERLAY_ID, SCAN_ID, BADGE_ID, CANVAS_ID, VIDEO_ID, IMAGE_ID].forEach((id) => {
|
|
const node = document.getElementById(id);
|
|
if (node && node.remove) node.remove();
|
|
});
|
|
|
|
if (document.body) {
|
|
document.body.removeAttribute('data-hdr-enhanced');
|
|
document.body.removeAttribute('data-hdr-mode');
|
|
document.body.removeAttribute('data-hdr-effect');
|
|
document.body.removeAttribute('data-hdr-media');
|
|
document.body.removeAttribute('data-hdr-text');
|
|
}
|
|
if (document.documentElement) {
|
|
delete document.documentElement.dataset.hdrBoosted;
|
|
if (document.documentElement.style) {
|
|
document.documentElement.style.removeProperty('--hdr-runtime-gamut');
|
|
document.documentElement.style.removeProperty('--hdr-texture-url');
|
|
}
|
|
}
|
|
|
|
return { active: false };
|
|
}
|
|
|
|
function toggle(options) {
|
|
const win = getWindow(options || {});
|
|
const document = win && win.document;
|
|
if (!document || !document.body) return enhance(options);
|
|
return document.body.dataset.hdrEnhanced === 'true' ? disable({ window: win }) : enhance(options);
|
|
}
|
|
|
|
return {
|
|
pluginName: PLUGIN_NAME,
|
|
version: VERSION,
|
|
detectCapabilities,
|
|
readScriptOptions,
|
|
shouldAutoRun,
|
|
createMediaConfig,
|
|
createEffectConfig,
|
|
createStyleText,
|
|
install,
|
|
enhance,
|
|
disable,
|
|
toggle,
|
|
ready
|
|
};
|
|
});
|