mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-12 20:45:52 +00:00
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.
This commit is contained in:
parent
3f4a7a9e73
commit
ee1ecbfb27
20 changed files with 38477 additions and 2 deletions
3
bun.lock
3
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=="],
|
||||
|
|
|
|||
489
frontend/src/lib/particle-background.ts
Normal file
489
frontend/src/lib/particle-background.ts
Normal file
|
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
|
@ -59,6 +59,13 @@ if (marqueeRoot) {
|
|||
render(() => <ScrollRevealBootstrap />, document.body);
|
||||
render(() => <ContributorsDialogBootstrap />, 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([
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
61
themes/openfreebooks/sass/_particle-background.scss
Normal file
61
themes/openfreebooks/sass/_particle-background.scss
Normal file
|
|
@ -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%
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
@import "tokens";
|
||||
@import "particle-background";
|
||||
@import "grain";
|
||||
@import "layout";
|
||||
@import "components";
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{j as J,t as K,u as N,o as O}from"./bundle.js";var Q=K("<canvas class=hero-ascii-canvas aria-hidden=true>");const W=" .·:;-=+*#%@";function U(d){const e=Math.max(0,Math.min(1,d)),n=Math.floor(e*(W.length-1));return W[n]??" "}function tt(){let d;return J(()=>{const e=d;if(!e)return;const n=e.closest(".hero");if(!n)return;const o=e.getContext("2d",{alpha:!0});if(!o)return;const R=window.matchMedia("(prefers-reduced-motion: reduce)"),k=window.matchMedia("(pointer: coarse)"),C=window.matchMedia("(max-width: 768px)"),S=()=>R.matches||k.matches||C.matches;let u=S()?"static":"animated",p=0,g=0,$=-9999,B=-9999,r=0,c=0,v=0,w=0,x=10,y=6,L=12;const H=()=>{const t=getComputedStyle(n);return{ink:t.getPropertyValue("--ink").trim()||"#1a1a1a",soft:t.getPropertyValue("--soft-ink").trim()||"#555",mono:t.getPropertyValue("--mono").trim()||'"IBM Plex Mono", Menlo, monospace'}};let E=H();const X=()=>{E=H();const t=n.getBoundingClientRect(),a=Math.min(window.devicePixelRatio||1,2);r=t.width,c=t.height,e.width=Math.max(1,Math.floor(r*a)),e.height=Math.max(1,Math.floor(c*a)),e.style.width=`${r}px`,e.style.height=`${c}px`,o.setTransform(a,0,0,a,0,0);const i=S();x=i?12:10,L=x*1.18,y=x*.6,v=Math.max(8,Math.floor(r/y)),w=Math.max(6,Math.floor(c/L)),i&&(v=Math.max(8,Math.floor(v*.7)),w=Math.max(6,Math.floor(w*.7))),o.font=`${x}px ${E.mono}`,o.textBaseline="top"},j=(t,a,i)=>{const l=r*.72,f=c*.48,M=t-l,s=a-f,V=Math.hypot(M,s),q=Math.atan2(s,M)+i*.45,P=Math.sin(V*.09-q*2.8)*.5+.5,A=Math.sin(t*.018+i*.9)*Math.cos(a*.014-i*.65)*.22,m=t/Math.max(r,1),b=m*m*(3-2*m);return(P*.82+A)*(b*.92+.06)},Y=()=>{o.clearRect(0,0,r,c);const t=Math.min(r,c)*.22,a=t*t;for(let i=0;i<w;i+=1)for(let l=0;l<v;l+=1){const f=(l+.5)*y,M=(i+.5)*L;let s=j(f,M,g);if(u==="animated"){const m=f-$,b=M-B,D=Math.exp(-(m*m+b*b)/a);s=Math.min(1,s+D*.42)}if(s<.07)continue;const V=f/Math.max(r,1),q=U(s),P=Math.min(1,V*1.15+s*.25),A=(l*17+i*31+Math.floor(g*5))%113;P>.55&&s>.62&&A<2?(o.fillStyle=A===0?"rgba(180, 60, 70, 0.55)":"rgba(60, 90, 180, 0.5)",o.globalAlpha=1):P>.5?(o.fillStyle=E.ink,o.globalAlpha=.18+s*.72):(o.fillStyle=E.soft,o.globalAlpha=.08+s*.35),o.fillText(q,l*y,i*L),o.globalAlpha=1}},_=()=>{g+=.016,Y(),p=window.requestAnimationFrame(_)},z=()=>{u=S()?"static":"animated",X(),u==="animated"?p=window.requestAnimationFrame(_):(g=0,Y())},F=()=>{p&&(window.cancelAnimationFrame(p),p=0)},G=t=>{if(u!=="animated")return;const a=n.getBoundingClientRect();$=t.clientX-a.left,B=t.clientY-a.top},I=()=>{$=-9999,B=-9999},T=()=>{F(),z()},h=()=>{F(),z()};z(),n.addEventListener("pointermove",G),n.addEventListener("pointerleave",I),window.addEventListener("resize",T),R.addEventListener("change",h),k.addEventListener("change",h),C.addEventListener("change",h),O(()=>{F(),n.removeEventListener("pointermove",G),n.removeEventListener("pointerleave",I),window.removeEventListener("resize",T),R.removeEventListener("change",h),k.removeEventListener("change",h),C.removeEventListener("change",h)})}),(()=>{var e=Q(),n=d;return typeof n=="function"?N(n,e):d=e,e})()}export{tt as HeroAsciiBackground};
|
||||
4143
themes/openfreebooks/static/js/hero-particle-field-8QmKXPyD.js
Normal file
4143
themes/openfreebooks/static/js/hero-particle-field-8QmKXPyD.js
Normal file
File diff suppressed because one or more lines are too long
4191
themes/openfreebooks/static/js/hero-particle-field-B-SRNfT7.js
Normal file
4191
themes/openfreebooks/static/js/hero-particle-field-B-SRNfT7.js
Normal file
File diff suppressed because one or more lines are too long
4369
themes/openfreebooks/static/js/hero-particle-field-wJNbLIBU.js
Normal file
4369
themes/openfreebooks/static/js/hero-particle-field-wJNbLIBU.js
Normal file
File diff suppressed because one or more lines are too long
4202
themes/openfreebooks/static/js/particle-background-0Y1WVKeg.js
Normal file
4202
themes/openfreebooks/static/js/particle-background-0Y1WVKeg.js
Normal file
File diff suppressed because one or more lines are too long
4199
themes/openfreebooks/static/js/particle-background-BPzVSB-3.js
Normal file
4199
themes/openfreebooks/static/js/particle-background-BPzVSB-3.js
Normal file
File diff suppressed because one or more lines are too long
4199
themes/openfreebooks/static/js/particle-background-CxKm2vyT.js
Normal file
4199
themes/openfreebooks/static/js/particle-background-CxKm2vyT.js
Normal file
File diff suppressed because one or more lines are too long
4198
themes/openfreebooks/static/js/particle-background-DJNEFg7N.js
Normal file
4198
themes/openfreebooks/static/js/particle-background-DJNEFg7N.js
Normal file
File diff suppressed because one or more lines are too long
4202
themes/openfreebooks/static/js/particle-background-D_7YLNEY.js
Normal file
4202
themes/openfreebooks/static/js/particle-background-D_7YLNEY.js
Normal file
File diff suppressed because one or more lines are too long
4202
themes/openfreebooks/static/js/particle-background-DxknAzri.js
Normal file
4202
themes/openfreebooks/static/js/particle-background-DxknAzri.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -35,6 +35,8 @@
|
|||
</script>
|
||||
<div id="site-header"></div>
|
||||
|
||||
{% block page_background %}{% endblock page_background %}
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock content %}
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@
|
|||
<meta name="description" content="{{ section.description | default(value=config.description) }}" />
|
||||
{% endblock meta_description %}
|
||||
|
||||
{% block page_background %}
|
||||
<div id="webgl-background" aria-hidden="true"></div>
|
||||
{% endblock page_background %}
|
||||
|
||||
{% block content %}
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue