Files
HDR/dist/hdr-web-plugin.js

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
};
});