UNPKG

@fluritheme/fluri

Version:

Interactive snowfall for websites: DOM-aware snow accumulation, mouse interactions, snow physics, Canvas 2D rendering, and Christmas effects.

147 lines • 5.87 kB
const paper = { r: 255, g: 255, b: 255, a: 1 }; /** Small cached backdrop map. No hit testing or computed-style reads occur in the particle loop. */ export class SnowContrast { doc; columns = 8; rows = 5; cells = new Float32Array(this.columns * this.rows); support = new WeakMap(); colors = new Map(); parser; lastRefresh = -Infinity; dirty = true; width = 1; height = 1; observer; pending; constructor(doc, invalidate) { this.doc = doc; const canvas = doc.createElement('canvas'); canvas.width = canvas.height = 1; this.parser = canvas.getContext('2d', { willReadFrequently: true }); const win = doc.defaultView; if (win) { this.observer = new win.MutationObserver((records) => { if (records.some((record) => !(record.target instanceof win.Element) || !record.target.closest('[data-fluri-root]'))) { this.dirty = true; if (!this.pending) this.pending = setTimeout(() => { this.pending = undefined; invalidate(); }, Math.max(0, 185 - (performance.now() - this.lastRefresh))); } }); this.observer.observe(doc.documentElement, { attributes: true, subtree: true, attributeFilter: ['style', 'class', 'data-theme', 'data-color-scheme'], }); } } invalidate() { this.dirty = true; } color(css) { const known = this.colors.get(css); if (known) return known; const ctx = this.parser; if (!ctx) return paper; ctx.clearRect(0, 0, 1, 1); ctx.fillStyle = 'transparent'; ctx.fillStyle = css; ctx.fillRect(0, 0, 1, 1); const pixel = ctx.getImageData(0, 0, 1, 1).data; const value = { r: pixel[0] ?? 0, g: pixel[1] ?? 0, b: pixel[2] ?? 0, a: (pixel[3] ?? 0) / 255, }; if (this.colors.size >= 128) this.colors.clear(); this.colors.set(css, value); return value; } lightness(color) { const linear = (value) => value <= 10.31475 ? value / 3294.6 : ((value / 255 + 0.055) / 1.055) ** 2.4; return 0.2126 * linear(color.r) + 0.7152 * linear(color.g) + 0.0722 * linear(color.b); } strength(color) { return Math.max(0, Math.min(1, (this.lightness(color) - 0.56) / 0.32)); } isPale(css) { const color = this.color(css); return (this.lightness(color) > 0.7 && Math.max(color.r, color.g, color.b) - Math.min(color.r, color.g, color.b) < 65); } refresh(surfaces, width, height) { const now = performance.now(); const resized = width !== this.width || height !== this.height; if (!resized && now - this.lastRefresh < (this.dirty ? 180 : 650)) return; this.lastRefresh = now; this.dirty = false; this.width = Math.max(1, width); this.height = Math.max(1, height); const win = this.doc.defaultView; if (!win) return; const raw = new WeakMap(); const composed = new WeakMap(); const own = (element) => { let value = raw.get(element); if (!value) { value = this.color(win.getComputedStyle(element).backgroundColor); raw.set(element, value); } return value; }; const background = (element) => { if (!element) return paper; const cached = composed.get(element); if (cached) return cached; const foreground = own(element); const behind = foreground.a >= 0.999 ? foreground : background(element.parentElement); const value = foreground.a >= 0.999 ? foreground : { r: foreground.r * foreground.a + behind.r * (1 - foreground.a), g: foreground.g * foreground.a + behind.g * (1 - foreground.a), b: foreground.b * foreground.a + behind.b * (1 - foreground.a), a: 1, }; composed.set(element, value); return value; }; for (let row = 0; row < this.rows; row++) for (let column = 0; column < this.columns; column++) { const x = ((column + 0.5) * this.width) / this.columns, y = ((row + 0.5) * this.height) / this.rows; const elements = this.doc.elementsFromPoint(x, y); const underlying = elements.find((element) => !element.closest('[data-fluri-root]') && own(element).a > 0.01); this.cells[row * this.columns + column] = this.strength(background(underlying ?? this.doc.body)); } this.support = new WeakMap(); for (const surface of surfaces) this.support.set(surface, Math.max(this.strength(background(surface.element)), this.strength(background(surface.element.parentElement)))); } at(x, y) { const column = Math.max(0, Math.min(this.columns - 1, Math.floor((x * this.columns) / this.width))); const row = Math.max(0, Math.min(this.rows - 1, Math.floor((y * this.rows) / this.height))); return this.cells[row * this.columns + column] ?? 0; } forSurface(surface) { return this.support.get(surface) ?? this.at(surface.x + surface.width / 2, surface.y - 3); } destroy() { clearTimeout(this.pending); this.observer?.disconnect(); this.colors.clear(); this.parser = null; } } //# sourceMappingURL=Contrast.js.map