From ee1ecbfb27c31509983574061a117aaceb16aa14 Mon Sep 17 00:00:00 2001 From: Hananoshika Yomaru Date: Tue, 19 May 2026 02:05:58 -0700 Subject: [PATCH] Add three.js dependency in package.json and bun.lock, implement particle background in main.tsx, and update SASS and HTML templates for background integration. --- bun.lock | 3 + frontend/src/lib/particle-background.ts | 489 ++ frontend/src/main.tsx | 7 + package.json | 3 +- .../sass/_particle-background.scss | 61 + themes/openfreebooks/sass/main.scss | 1 + themes/openfreebooks/static/js/bundle.js | 2 +- .../js/hero-ascii-background-HquA4z6h.js | 1 + .../static/js/hero-particle-field-8QmKXPyD.js | 4143 ++++++++++++++++ .../static/js/hero-particle-field-B-SRNfT7.js | 4191 ++++++++++++++++ .../static/js/hero-particle-field-wJNbLIBU.js | 4369 +++++++++++++++++ .../static/js/particle-background-0Y1WVKeg.js | 4202 ++++++++++++++++ .../static/js/particle-background-BPzVSB-3.js | 4199 ++++++++++++++++ .../static/js/particle-background-CxKm2vyT.js | 4199 ++++++++++++++++ .../static/js/particle-background-DJNEFg7N.js | 4198 ++++++++++++++++ .../static/js/particle-background-D_7YLNEY.js | 4202 ++++++++++++++++ .../static/js/particle-background-DxknAzri.js | 4202 ++++++++++++++++ .../static/js/quadratic-explorer-C1eXzzfV.js | 1 + themes/openfreebooks/templates/base.html | 2 + themes/openfreebooks/templates/index.html | 4 + 20 files changed, 38477 insertions(+), 2 deletions(-) create mode 100644 frontend/src/lib/particle-background.ts create mode 100644 themes/openfreebooks/sass/_particle-background.scss create mode 100644 themes/openfreebooks/static/js/hero-ascii-background-HquA4z6h.js create mode 100644 themes/openfreebooks/static/js/hero-particle-field-8QmKXPyD.js create mode 100644 themes/openfreebooks/static/js/hero-particle-field-B-SRNfT7.js create mode 100644 themes/openfreebooks/static/js/hero-particle-field-wJNbLIBU.js create mode 100644 themes/openfreebooks/static/js/particle-background-0Y1WVKeg.js create mode 100644 themes/openfreebooks/static/js/particle-background-BPzVSB-3.js create mode 100644 themes/openfreebooks/static/js/particle-background-CxKm2vyT.js create mode 100644 themes/openfreebooks/static/js/particle-background-DJNEFg7N.js create mode 100644 themes/openfreebooks/static/js/particle-background-D_7YLNEY.js create mode 100644 themes/openfreebooks/static/js/particle-background-DxknAzri.js create mode 100644 themes/openfreebooks/static/js/quadratic-explorer-C1eXzzfV.js diff --git a/bun.lock b/bun.lock index 8d1c6ed..e9ac6c2 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "katex": "^0.16.47", "solid-js": "^1.9.5", + "three": "^0.184.0", }, "devDependencies": { "typescript": "^5.8.3", @@ -250,6 +251,8 @@ "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "three": ["three@0.184.0", "", {}, "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg=="], + "tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], diff --git a/frontend/src/lib/particle-background.ts b/frontend/src/lib/particle-background.ts new file mode 100644 index 0000000..09bb80f --- /dev/null +++ b/frontend/src/lib/particle-background.ts @@ -0,0 +1,489 @@ +import { + AdditiveBlending, + BufferAttribute, + BufferGeometry, + Clock, + Color, + NormalBlending, + PerspectiveCamera, + Plane, + Points, + Raycaster, + Scene, + ShaderMaterial, + Vector2, + Vector3, + WebGLRenderer, +} from "three"; +import { resolveTheme, type ThemeChoice } from "../theme"; + +const DESKTOP_COUNT = 60000; +const MOBILE_COUNT = 20000; +const SHAPE_COUNT = 4; +const MORPH_MAX = 4; +const MORPH_LOOP_EPS = 0.001; +const SHAPE_SCALE = 0.8; + +type ShapeColors = [number, number, number]; + +type ThemePalette = { + layers: ShapeColors; + sphere: ShapeColors; + cube: ShapeColors; + helix: ShapeColors; + clear: string; + glow: number; + particleAlpha: number; + colorBoost: number; + pointScale: number; + colorJitter: number; +}; + +const LIGHT_PALETTE: ThemePalette = { + layers: [0.44, 0.28, 0.05], + sphere: [0.58, 0.34, 0.02], + cube: [0.03, 0.34, 0.16], + helix: [0.34, 0.44, 0.06], + clear: "#f5f4f1", + glow: 0.72, + particleAlpha: 1, + colorBoost: 1.45, + pointScale: 1.35, + colorJitter: 0.04, +}; + +const DARK_PALETTE: ThemePalette = { + layers: [0.82, 0.7, 0.48], + sphere: [0.85, 0.58, 0.22], + cube: [0.28, 0.78, 0.46], + helix: [0.62, 0.82, 0.34], + clear: "#1a1917", + glow: 0.82, + particleAlpha: 0.78, + colorBoost: 1, + pointScale: 1, + colorJitter: 0.12, +}; + +function prefersReducedMotion() { + return window.matchMedia("(prefers-reduced-motion: reduce)").matches; +} + +function isMobileTier() { + return window.matchMedia("(max-width: 768px)").matches; +} + +function hasFinePointer() { + return window.matchMedia("(pointer: fine)").matches; +} + +function getPalette(theme: ThemeChoice): ThemePalette { + return theme === "dark" ? DARK_PALETTE : LIGHT_PALETTE; +} + +function getPaperColor(): Color { + const value = getComputedStyle(document.documentElement).getPropertyValue("--paper").trim(); + return new Color(value || LIGHT_PALETTE.clear); +} + +function setColor(arr: Float32Array, index: number, rgb: ShapeColors, jitter = 0.12) { + arr[index * 3] = rgb[0] + (Math.random() - 0.5) * jitter; + arr[index * 3 + 1] = rgb[1] + (Math.random() - 0.5) * jitter; + arr[index * 3 + 2] = rgb[2] + (Math.random() - 0.5) * jitter; +} + +function scalePositions(positions: Float32Array, scale: number) { + for (let i = 0; i < positions.length; i += 1) { + positions[i] *= scale; + } +} + +function generateLayers(count: number, positions: Float32Array, colors: Float32Array, rgb: ShapeColors) { + const layerCount = 14; + for (let i = 0; i < count; i += 1) { + const layerIndex = Math.floor(Math.random() * layerCount); + const layerY = (layerIndex / (layerCount - 1)) * 12 - 6; + positions[i * 3] = (Math.random() - 0.5) * 9; + positions[i * 3 + 1] = layerY + (Math.random() - 0.5) * 0.18; + positions[i * 3 + 2] = (Math.random() - 0.5) * 6; + setColor(colors, i, rgb, 0.06); + } + scalePositions(positions, SHAPE_SCALE); +} + +function generateSphere(count: number, positions: Float32Array, colors: Float32Array, rgb: ShapeColors) { + for (let i = 0; i < count; i += 1) { + const phi = Math.acos(-1 + (2 * i) / count); + const theta = Math.sqrt(count * Math.PI) * phi; + const radius = 5 + (Math.random() - 0.5) * 0.5; + positions[i * 3] = radius * Math.cos(theta) * Math.sin(phi); + positions[i * 3 + 1] = radius * Math.sin(theta) * Math.sin(phi); + positions[i * 3 + 2] = radius * Math.cos(phi); + setColor(colors, i, rgb, 0.1); + } + scalePositions(positions, SHAPE_SCALE); +} + +function generateCube(count: number, positions: Float32Array, colors: Float32Array, rgb: ShapeColors) { + for (let i = 0; i < count; i += 1) { + let x = (Math.random() - 0.5) * 10; + let y = (Math.random() - 0.5) * 10; + let z = (Math.random() - 0.5) * 10; + const axis = Math.floor(Math.random() * 3); + const sign = Math.random() > 0.5 ? 1 : -1; + if (axis === 0) x = 5 * sign; + else if (axis === 1) y = 5 * sign; + else z = 5 * sign; + positions[i * 3] = x; + positions[i * 3 + 1] = y; + positions[i * 3 + 2] = z; + setColor(colors, i, rgb, 0.1); + } + scalePositions(positions, SHAPE_SCALE); +} + +function generateHelix(count: number, positions: Float32Array, colors: Float32Array, rgb: ShapeColors) { + for (let i = 0; i < count; i += 1) { + const t = (i / count) * Math.PI * 20; + const radius = 3; + const offset = (Math.random() - 0.5) * 1.5; + const arm = i % 2 === 0 ? 0 : Math.PI; + positions[i * 3] = (radius + offset) * Math.cos(t + arm); + positions[i * 3 + 1] = (t / (Math.PI * 20)) * 14 - 7; + positions[i * 3 + 2] = (radius + offset) * Math.sin(t + arm); + setColor(colors, i, rgb, 0.1); + } + scalePositions(positions, SHAPE_SCALE); +} + +function fillShapeColors( + geometry: BufferGeometry, + palette: ThemePalette, + count: number +) { + const col0 = geometry.getAttribute("col0") as BufferAttribute; + const col1 = geometry.getAttribute("col1") as BufferAttribute; + const col2 = geometry.getAttribute("col2") as BufferAttribute; + const col3 = geometry.getAttribute("col3") as BufferAttribute; + + for (let i = 0; i < count; i += 1) { + setColor(col0.array as Float32Array, i, palette.layers, palette.colorJitter); + setColor(col1.array as Float32Array, i, palette.sphere, palette.colorJitter); + setColor(col2.array as Float32Array, i, palette.cube, palette.colorJitter); + setColor(col3.array as Float32Array, i, palette.helix, palette.colorJitter); + } + + col0.needsUpdate = true; + col1.needsUpdate = true; + col2.needsUpdate = true; + col3.needsUpdate = true; +} + +const vertexShader = ` + attribute vec3 pos0; + attribute vec3 col0; + attribute vec3 pos1; + attribute vec3 col1; + attribute vec3 pos2; + attribute vec3 col2; + attribute vec3 pos3; + attribute vec3 col3; + + uniform float uMorph; + uniform float uTime; + uniform vec3 uMouse; + uniform float uPixelRatio; + uniform float uMouseEnabled; + uniform float uGlow; + uniform float uPointScale; + + varying vec3 vColor; + varying float vAlpha; + + void main() { + float m = mod(uMorph, ${SHAPE_COUNT}.0); + float t = fract(m); + float ease = t < 0.5 ? 4.0 * t * t * t : 1.0 - pow(-2.0 * t + 2.0, 3.0) / 2.0; + + vec3 finalPos; + vec3 finalCol; + + if (m < 1.0) { + finalPos = mix(pos0, pos1, ease); + finalCol = mix(col0, col1, ease); + } else if (m < 2.0) { + finalPos = mix(pos1, pos2, ease); + finalCol = mix(col1, col2, ease); + } else if (m < 3.0) { + finalPos = mix(pos2, pos3, ease); + finalCol = mix(col2, col3, ease); + } else { + finalPos = mix(pos3, pos0, ease); + finalCol = mix(col3, col0, ease); + } + + float sphereInfluence = max(0.0, 1.0 - abs(m - 1.0)); + if (sphereInfluence > 0.0) { + float breathing = sin(uTime * 1.5) * 0.5; + float shockTime = fract(uTime * 0.15); + float shockRadius = shockTime * 20.0; + float distFromCenter = length(finalPos); + float shock = smoothstep(2.0, 0.0, abs(distFromCenter - shockRadius)); + vec3 dir = normalize(finalPos + vec3(0.0001)); + finalPos += dir * (breathing + shock * 4.0) * sphereInfluence; + finalCol += vec3(0.4, 0.7, 1.0) * shock * sphereInfluence * uGlow; + } + + if (uMouseEnabled > 0.5) { + float mouseDist = distance(finalPos, uMouse); + float radius = 3.5; + if (mouseDist < radius) { + vec3 pushDir = normalize(finalPos - uMouse); + float force = pow(1.0 - (mouseDist / radius), 2.0); + finalPos += pushDir * force * 1.5; + } + } + + finalPos.y += sin(finalPos.x * 2.0 + uTime * 2.0) * 0.1; + finalPos.x += cos(finalPos.y * 2.0 + uTime * 2.0) * 0.1; + finalPos.z += sin(finalPos.x * 1.5 + finalPos.y * 1.5 + uTime * 1.6) * 0.06; + + vec4 mvPosition = modelViewMatrix * vec4(finalPos, 1.0); + gl_PointSize = (13.0 * uPixelRatio * uPointScale) * (1.0 / -mvPosition.z); + gl_Position = projectionMatrix * mvPosition; + vColor = finalCol; + vAlpha = 1.0; + } +`; + +const fragmentShader = ` + uniform float uParticleAlpha; + uniform float uColorBoost; + + varying vec3 vColor; + varying float vAlpha; + + void main() { + vec2 center = vec2(0.5, 0.5); + float dist = distance(gl_PointCoord, center); + if (dist > 0.5) discard; + float alpha = smoothstep(0.5, 0.08, dist); + vec3 color = clamp(vColor * uColorBoost, 0.0, 1.0); + gl_FragColor = vec4(color, alpha * uParticleAlpha * vAlpha); + } +`; + +function clampMorph(morph: number) { + if (morph <= 0) return 0; + if (morph >= MORPH_MAX - MORPH_LOOP_EPS) return MORPH_MAX - MORPH_LOOP_EPS; + return morph; +} + +function getLayersHold(morph: number) { + const clamped = clampMorph(morph); + if (clamped <= MORPH_LOOP_EPS || clamped >= MORPH_MAX - MORPH_LOOP_EPS) { + return 1; + } + const m = clamped % SHAPE_COUNT; + if (m < 0.5) return 1 - m / 0.5; + if (m > 3.5) return (SHAPE_COUNT - m) / 0.5; + return 0; +} + +export function initParticleBackground(container: HTMLElement): () => void { + if (prefersReducedMotion()) { + return () => {}; + } + + document.body.classList.add("has-particle-background"); + + const particleCount = isMobileTier() ? MOBILE_COUNT : DESKTOP_COUNT; + const mouseEnabled = hasFinePointer(); + const palette = getPalette(resolveTheme()); + + const scene = new Scene(); + const camera = new PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); + camera.position.z = 10; + + const renderer = new WebGLRenderer({ + alpha: false, + antialias: false, + powerPreference: "high-performance", + }); + renderer.setSize(window.innerWidth, window.innerHeight); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + container.appendChild(renderer.domElement); + + const geometry = new BufferGeometry(); + const pos0 = new Float32Array(particleCount * 3); + const col0 = new Float32Array(particleCount * 3); + const pos1 = new Float32Array(particleCount * 3); + const col1 = new Float32Array(particleCount * 3); + const pos2 = new Float32Array(particleCount * 3); + const col2 = new Float32Array(particleCount * 3); + const pos3 = new Float32Array(particleCount * 3); + const col3 = new Float32Array(particleCount * 3); + + generateLayers(particleCount, pos0, col0, palette.layers); + generateSphere(particleCount, pos1, col1, palette.sphere); + generateCube(particleCount, pos2, col2, palette.cube); + generateHelix(particleCount, pos3, col3, palette.helix); + + geometry.setAttribute("pos0", new BufferAttribute(pos0, 3)); + geometry.setAttribute("col0", new BufferAttribute(col0, 3)); + geometry.setAttribute("pos1", new BufferAttribute(pos1, 3)); + geometry.setAttribute("col1", new BufferAttribute(col1, 3)); + geometry.setAttribute("pos2", new BufferAttribute(pos2, 3)); + geometry.setAttribute("col2", new BufferAttribute(col2, 3)); + geometry.setAttribute("pos3", new BufferAttribute(pos3, 3)); + geometry.setAttribute("col3", new BufferAttribute(col3, 3)); + geometry.setAttribute("position", new BufferAttribute(pos0, 3)); + + const material = new ShaderMaterial({ + vertexShader, + fragmentShader, + uniforms: { + uMorph: { value: 0 }, + uTime: { value: 0 }, + uMouse: { value: new Vector3(999, 999, 999) }, + uPixelRatio: { value: Math.min(window.devicePixelRatio, 2) }, + uMouseEnabled: { value: mouseEnabled ? 1 : 0 }, + uGlow: { value: palette.glow }, + uParticleAlpha: { value: palette.particleAlpha }, + uColorBoost: { value: palette.colorBoost }, + uPointScale: { value: palette.pointScale }, + }, + transparent: true, + depthWrite: false, + blending: resolveTheme() === "dark" ? AdditiveBlending : NormalBlending, + }); + + const points = new Points(geometry, material); + scene.add(points); + + let targetMorph = 0; + let currentMorph = 0; + let spinY = 0; + let spinX = 0; + const clock = new Clock(); + let frameId = 0; + let disposed = false; + + const plane = new Plane(new Vector3(0, 0, 1), 0); + const raycaster = new Raycaster(); + const mouseNdc = new Vector2(-999, -999); + const targetMouseWorld = new Vector3(); + + function applyThemePalette(theme: ThemeChoice) { + const nextPalette = getPalette(theme); + fillShapeColors(geometry, nextPalette, particleCount); + material.uniforms.uGlow.value = nextPalette.glow; + material.uniforms.uParticleAlpha.value = nextPalette.particleAlpha; + material.uniforms.uColorBoost.value = nextPalette.colorBoost; + material.uniforms.uPointScale.value = nextPalette.pointScale; + material.blending = theme === "dark" ? AdditiveBlending : NormalBlending; + material.needsUpdate = true; + renderer.setClearColor(getPaperColor(), 1); + } + + applyThemePalette(resolveTheme()); + + function onScroll() { + const maxScroll = document.body.scrollHeight - window.innerHeight; + targetMorph = maxScroll > 0 ? clampMorph((window.scrollY / maxScroll) * MORPH_MAX) : 0; + } + + function onMouseMove(event: MouseEvent) { + if (!mouseEnabled) return; + mouseNdc.x = (event.clientX / window.innerWidth) * 2 - 1; + mouseNdc.y = -(event.clientY / window.innerHeight) * 2 + 1; + } + + function onResize() { + camera.aspect = window.innerWidth / window.innerHeight; + camera.updateProjectionMatrix(); + renderer.setSize(window.innerWidth, window.innerHeight); + material.uniforms.uPixelRatio.value = Math.min(window.devicePixelRatio, 2); + renderer.setClearColor(getPaperColor(), 1); + } + + const themeObserver = new MutationObserver(() => { + applyThemePalette(resolveTheme()); + }); + + const darkModeQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const onSystemThemeChange = () => { + applyThemePalette(resolveTheme()); + }; + + themeObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-theme"], + }); + darkModeQuery.addEventListener("change", onSystemThemeChange); + + window.addEventListener("scroll", onScroll, { passive: true }); + window.addEventListener("mousemove", onMouseMove, { passive: true }); + window.addEventListener("resize", onResize); + + onScroll(); + + function smoothstep(value: number) { + return value * value * (3 - 2 * value); + } + + function animate() { + if (disposed) return; + frameId = window.requestAnimationFrame(animate); + + const delta = clock.getDelta(); + const time = clock.getElapsedTime(); + material.uniforms.uTime.value = time; + currentMorph += (targetMorph - currentMorph) * 0.05; + currentMorph = clampMorph(currentMorph); + material.uniforms.uMorph.value = currentMorph; + + const layersHold = getLayersHold(currentMorph); + const spinWeight = 1 - layersHold; + + if (layersHold < 0.99) { + spinY += delta * 0.1; + spinX += delta * 0.05; + } else { + spinY = 0; + spinX = 0; + } + + const easedSpin = smoothstep(spinWeight); + points.rotation.y = spinY * easedSpin; + points.rotation.x = spinX * easedSpin; + + if (mouseEnabled) { + raycaster.setFromCamera(mouseNdc, camera); + raycaster.ray.intersectPlane(plane, targetMouseWorld); + const localMouse = targetMouseWorld.clone(); + points.worldToLocal(localMouse); + material.uniforms.uMouse.value.lerp(localMouse, 0.1); + } + + renderer.setClearColor(getPaperColor(), 1); + renderer.render(scene, camera); + } + + animate(); + + return () => { + disposed = true; + window.cancelAnimationFrame(frameId); + window.removeEventListener("scroll", onScroll); + window.removeEventListener("mousemove", onMouseMove); + window.removeEventListener("resize", onResize); + darkModeQuery.removeEventListener("change", onSystemThemeChange); + themeObserver.disconnect(); + document.body.classList.remove("has-particle-background"); + geometry.dispose(); + material.dispose(); + renderer.dispose(); + renderer.domElement.remove(); + }; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index 4250d74..ae27a01 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -59,6 +59,13 @@ if (marqueeRoot) { render(() => , document.body); render(() => , document.body); +const particleRoot = document.getElementById("webgl-background"); +if (particleRoot && !window.matchMedia("(prefers-reduced-motion: reduce)").matches) { + void import("./lib/particle-background").then(({ initParticleBackground }) => { + initParticleBackground(particleRoot); + }); +} + const quadraticRoot = document.getElementById("quadratic-explorer"); if (quadraticRoot) { void Promise.all([ diff --git a/package.json b/package.json index e561ba0..0b11423 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ }, "dependencies": { "katex": "^0.16.47", - "solid-js": "^1.9.5" + "solid-js": "^1.9.5", + "three": "^0.184.0" }, "devDependencies": { "typescript": "^5.8.3", diff --git a/themes/openfreebooks/sass/_particle-background.scss b/themes/openfreebooks/sass/_particle-background.scss new file mode 100644 index 0000000..c384d62 --- /dev/null +++ b/themes/openfreebooks/sass/_particle-background.scss @@ -0,0 +1,61 @@ +#webgl-background { + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; +} + +#webgl-background canvas { + display: block; + width: 100%; + height: 100%; +} + +body.has-particle-background { + background: transparent; +} + +body.has-particle-background main { + position: relative; + z-index: 1; +} + +body.has-particle-background main::before { + content: ""; + position: fixed; + inset: 0; + z-index: -1; + pointer-events: none; + background: linear-gradient( + to bottom, + color-mix(in srgb, var(--paper) 34%, transparent), + color-mix(in srgb, var(--paper) 56%, transparent) 42%, + color-mix(in srgb, var(--paper) 82%, transparent) 100% + ); +} + +body.has-particle-background .footer { + position: relative; + z-index: 1; + background: color-mix(in srgb, var(--paper) 94%, transparent); +} + +[data-theme="dark"] body.has-particle-background main::before { + background: linear-gradient( + to bottom, + color-mix(in srgb, var(--paper) 48%, transparent), + color-mix(in srgb, var(--paper) 68%, transparent) 42%, + color-mix(in srgb, var(--paper) 88%, transparent) 100% + ); +} + +@media (prefers-color-scheme: dark) { + :root:not([data-theme="light"]) body.has-particle-background main::before { + background: linear-gradient( + to bottom, + color-mix(in srgb, var(--paper) 48%, transparent), + color-mix(in srgb, var(--paper) 68%, transparent) 42%, + color-mix(in srgb, var(--paper) 88%, transparent) 100% + ); + } +} diff --git a/themes/openfreebooks/sass/main.scss b/themes/openfreebooks/sass/main.scss index 3804623..c62b932 100644 --- a/themes/openfreebooks/sass/main.scss +++ b/themes/openfreebooks/sass/main.scss @@ -1,4 +1,5 @@ @import "tokens"; +@import "particle-background"; @import "grain"; @import "layout"; @import "components"; diff --git a/themes/openfreebooks/static/js/bundle.js b/themes/openfreebooks/static/js/bundle.js index 8fbd4bf..bb91db4 100644 --- a/themes/openfreebooks/static/js/bundle.js +++ b/themes/openfreebooks/static/js/bundle.js @@ -1,2 +1,2 @@ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["js/quadratic-explorer-BEDvXl1S.js","js/katex-Dc8nsIP1.js","js/render-math-H0D_R-AC.js","js/katex.css"])))=>i.map(i=>d[i]); -const Se="modulepreload",Ae=function(e){return"/"+e},z={},ee=function(t,n,s){let r=Promise.resolve();if(n&&n.length>0){let l=function(u){return Promise.all(u.map(f=>Promise.resolve(f).then(a=>({status:"fulfilled",value:a}),a=>({status:"rejected",reason:a}))))};document.getElementsByTagName("link");const o=document.querySelector("meta[property=csp-nonce]"),c=(o==null?void 0:o.nonce)||(o==null?void 0:o.getAttribute("nonce"));r=l(n.map(u=>{if(u=Ae(u),u in z)return;z[u]=!0;const f=u.endsWith(".css"),a=f?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${u}"]${a}`))return;const m=document.createElement("link");if(m.rel=f?"stylesheet":Se,f||(m.as="script"),m.crossOrigin="",m.href=u,c&&m.setAttribute("nonce",c),document.head.appendChild(m),f)return new Promise((b,v)=>{m.addEventListener("load",b),m.addEventListener("error",()=>v(new Error(`Unable to preload CSS for ${u}`)))})}))}function i(l){const o=new Event("vite:preloadError",{cancelable:!0});if(o.payload=l,window.dispatchEvent(o),!o.defaultPrevented)throw l}return r.then(l=>{for(const o of l||[])o.status==="rejected"&&i(o.reason);return t().catch(i)})};const ke=(e,t)=>e===t,Ce=Symbol("solid-track"),j={equals:ke};let ue=ge;const L=1,V=2,fe={owned:null,cleanups:null,context:null,owner:null};var g=null;let J=null,$e=null,h=null,w=null,$=null,Y=0;function N(e,t){const n=h,s=g,r=e.length===0,i=t===void 0?s:t,l=r?fe:{owned:null,cleanups:null,context:i?i.context:null,owner:i},o=r?e:()=>e(()=>x(()=>D(l)));g=l,h=null;try{return P(o,!0)}finally{h=n,g=s}}function G(e,t){t=t?Object.assign({},j,t):j;const n={value:e,observers:null,observerSlots:null,comparator:t.equals||void 0},s=r=>(typeof r=="function"&&(r=r(n.value)),he(n,r));return[de.bind(n),s]}function B(e,t,n){const s=Z(e,t,!1,L);U(s)}function Te(e,t,n){ue=Le;const s=Z(e,t,!1,L);s.user=!0,$?$.push(s):U(s)}function R(e,t,n){n=n?Object.assign({},j,n):j;const s=Z(e,t,!0,0);return s.observers=null,s.observerSlots=null,s.comparator=n.equals||void 0,U(s),de.bind(s)}function x(e){if(h===null)return e();const t=h;h=null;try{return e()}finally{h=t}}function W(e){Te(()=>x(e))}function X(e){return g===null||(g.cleanups===null?g.cleanups=[e]:g.cleanups.push(e)),e}function de(){if(this.sources&&this.state)if(this.state===L)U(this);else{const e=w;w=null,P(()=>F(this),!1),w=e}if(h){const e=this.observers;if(!e||e[e.length-1]!==h){const t=e?e.length:0;h.sources?(h.sources.push(this),h.sourceSlots.push(t)):(h.sources=[this],h.sourceSlots=[t]),e?(e.push(h),this.observerSlots.push(h.sources.length-1)):(this.observers=[h],this.observerSlots=[h.sources.length-1])}}return this.value}function he(e,t,n){let s=e.value;return(!e.comparator||!e.comparator(s,t))&&(e.value=t,e.observers&&e.observers.length&&P(()=>{for(let r=0;r1e6)throw w=[],new Error},!1)),t}function U(e){if(!e.fn)return;D(e);const t=Y;_e(e,e.value,t)}function _e(e,t,n){let s;const r=g,i=h;h=g=e;try{s=e.fn(t)}catch(l){return e.pure&&(e.state=L,e.owned&&e.owned.forEach(D),e.owned=null),e.updatedAt=n+1,be(l)}finally{h=i,g=r}(!e.updatedAt||e.updatedAt<=n)&&(e.updatedAt!=null&&"observers"in e?he(e,s):e.value=s,e.updatedAt=n)}function Z(e,t,n,s=L,r){const i={fn:e,state:s,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:t,owner:g,context:g?g.context:null,pure:n};return g===null||g!==fe&&(g.owned?g.owned.push(i):g.owned=[i]),i}function H(e){if(e.state===0)return;if(e.state===V)return F(e);if(e.suspense&&x(e.suspense.inFallback))return e.suspense.effects.push(e);const t=[e];for(;(e=e.owner)&&(!e.updatedAt||e.updatedAt=0;n--)if(e=t[n],e.state===L)U(e);else if(e.state===V){const s=w;w=null,P(()=>F(e,t[0]),!1),w=s}}function P(e,t){if(w)return e();let n=!1;t||(w=[]),$?n=!0:$=[],Y++;try{const s=e();return xe(n),s}catch(s){n||($=null),w=null,be(s)}}function xe(e){if(w&&(ge(w),w=null),e)return;const t=$;$=null,t.length&&P(()=>ue(t),!1)}function ge(e){for(let t=0;t=0;t--)D(e.tOwned[t]);delete e.tOwned}if(e.owned){for(t=e.owned.length-1;t>=0;t--)D(e.owned[t]);e.owned=null}if(e.cleanups){for(t=e.cleanups.length-1;t>=0;t--)e.cleanups[t]();e.cleanups=null}e.state=0}function Be(e){return e instanceof Error?e:new Error(typeof e=="string"?e:"Unknown error",{cause:e})}function be(e,t=g){throw Be(e)}const Ie=Symbol("fallback");function te(e){for(let t=0;t1?[]:null;return X(()=>te(i)),()=>{let c=e()||[],u=c.length,f,a;return c[Ce],x(()=>{let b,v,k,d,p,y,E,S,C;if(u===0)l!==0&&(te(i),i=[],s=[],r=[],l=0,o&&(o=[])),n.fallback&&(s=[Ie],r[0]=N(Ee=>(i[0]=Ee,n.fallback())),l=1);else if(l===0){for(r=new Array(u),a=0;a=y&&S>=y&&s[E]===c[S];E--,S--)k[S]=r[E],d[S]=i[E],o&&(p[S]=o[E]);for(b=new Map,v=new Array(S+1),a=S;a>=y;a--)C=c[a],f=b.get(C),v[a]=f===void 0?-1:f,b.set(C,a);for(f=y;f<=E;f++)C=s[f],a=b.get(C),a!==void 0&&a!==-1?(k[a]=r[f],d[a]=i[f],o&&(p[a]=o[f]),a=v[a],b.set(C,a)):i[f]();for(a=y;ae(t||{}))}const qe=e=>`Stale read from <${e}>.`;function ne(e){const t="fallback"in e&&{fallback:()=>e.fallback};return R(Oe(()=>e.each,e.children,t||void 0))}function De(e){const t=e.keyed,n=R(()=>e.when,void 0,void 0),s=t?n:R(n,void 0,{equals:(r,i)=>!r==!i});return R(()=>{const r=s();if(r){const i=e.children;return typeof i=="function"&&i.length>0?x(()=>i(t?r:()=>{if(!x(s))throw qe("Show");return n()})):i}return e.fallback},void 0,void 0)}function Ue(e,t,n){let s=n.length,r=t.length,i=s,l=0,o=0,c=t[r-1].nextSibling,u=null;for(;lf-o){const v=t[l];for(;o{r=i,t===document?e():A(t,e(),t.firstChild?null:void 0,n)},s.owner),()=>{r(),t.textContent=""}}function I(e,t,n,s){let r;const i=()=>{const o=document.createElement("template");return o.innerHTML=e,o.content.firstChild},l=()=>(r||(r=i())).cloneNode(!0);return l.cloneNode=l,l}function pe(e,t=window.document){const n=t[se]||(t[se]=new Set);for(let s=0,r=e.length;se(t,n))}function A(e,t,n,s){if(n!==void 0&&!s&&(s=[]),typeof t!="function")return K(e,t,s,n);B(r=>K(e,t(),r,n),s)}function re(e,t,n){const s=t.trim().split(/\s+/);for(let r=0,i=s.length;rObject.defineProperty(e,"target",{configurable:!0,value:c}),l=()=>{const c=t[n];if(c&&!t.disabled){const u=t[`${n}Data`];if(u!==void 0?c.call(t,u,e):c.call(t,e),e.cancelBubble)return}return t.host&&typeof t.host!="string"&&!t.host._$host&&t.contains(e.target)&&i(t.host),!0},o=()=>{for(;l()&&(t=t._$host||t.parentNode||t.host););};if(Object.defineProperty(e,"currentTarget",{configurable:!0,get(){return t||document}}),e.composedPath){const c=e.composedPath();i(c[0]);for(let u=0;u{let o=t();for(;typeof o=="function";)o=o();n=K(e,o,n,s)}),()=>n;if(Array.isArray(t)){const o=[],c=n&&Array.isArray(n);if(Q(o,t,n,r))return B(()=>n=K(e,o,n,s,!0)),()=>n;if(o.length===0){if(n=O(e,n,s),l)return n}else c?n.length===0?oe(e,o,s):Ue(e,n,o):(n&&O(e),oe(e,o));n=o}else if(t.nodeType){if(Array.isArray(n)){if(l)return n=O(e,n,s,t);O(e,n,null,t)}else n==null||n===""||!e.firstChild?e.appendChild(t):e.replaceChild(t,e.firstChild);n=t}}return n}function Q(e,t,n,s){let r=!1;for(let i=0,l=t.length;i=0;l--){const o=t[l];if(r!==o){const c=o.parentNode===e;!i&&!l?c?e.replaceChild(r,o):e.insertBefore(r,n):c&&o.remove()}else i=!0}}else e.insertBefore(r,n);return[r]}const we="openfreebooks-theme";function Ne(){return window.matchMedia("(prefers-color-scheme: dark)").matches?"dark":"light"}function Re(){const e=localStorage.getItem(we);return e==="light"||e==="dark"?e:null}function ve(){return Re()??Ne()}function ye(e){document.documentElement.setAttribute("data-theme",e)}function je(e){localStorage.setItem(we,e),ye(e)}function Ve(){ye(ve())}var Ge=I("