Refactor build process in package.json to clean up CSS files before building, enhance particle background implementation with new tree structure and color settings, and add new styles for tree call-to-action components in SASS files.

This commit is contained in:
Hananoshika Yomaru 2026-05-19 03:22:22 -07:00
parent ee1ecbfb27
commit 07a76333bd
16 changed files with 42854 additions and 27 deletions

View file

@ -17,12 +17,12 @@ import {
} from "three";
import { resolveTheme, type ThemeChoice } from "../theme";
const DESKTOP_COUNT = 60000;
const MOBILE_COUNT = 20000;
const SHAPE_COUNT = 4;
const DESKTOP_COUNT = 150000;
const MOBILE_COUNT = 45000;
const MORPH_MAX = 4;
const MORPH_LOOP_EPS = 0.001;
const SHAPE_SCALE = 0.8;
const GROUND_Y = -4;
const BRANCH_MIN_Y = -1.25;
type ShapeColors = [number, number, number];
@ -31,6 +31,8 @@ type ThemePalette = {
sphere: ShapeColors;
cube: ShapeColors;
helix: ShapeColors;
treeTrunk: ShapeColors;
treeFoliage: ShapeColors;
clear: string;
glow: number;
particleAlpha: number;
@ -44,6 +46,8 @@ const LIGHT_PALETTE: ThemePalette = {
sphere: [0.58, 0.34, 0.02],
cube: [0.03, 0.34, 0.16],
helix: [0.34, 0.44, 0.06],
treeTrunk: [0.4, 0.26, 0.08],
treeFoliage: [0.06, 0.36, 0.18],
clear: "#f5f4f1",
glow: 0.72,
particleAlpha: 1,
@ -57,6 +61,8 @@ const DARK_PALETTE: ThemePalette = {
sphere: [0.85, 0.58, 0.22],
cube: [0.28, 0.78, 0.46],
helix: [0.62, 0.82, 0.34],
treeTrunk: [0.62, 0.42, 0.16],
treeFoliage: [0.18, 0.62, 0.32],
clear: "#1a1917",
glow: 0.82,
particleAlpha: 0.78,
@ -156,6 +162,388 @@ function generateHelix(count: number, positions: Float32Array, colors: Float32Ar
scalePositions(positions, SHAPE_SCALE);
}
function setColorExact(arr: Float32Array, index: number, r: number, g: number, b: number) {
arr[index * 3] = r;
arr[index * 3 + 1] = g;
arr[index * 3 + 2] = b;
}
type TreeBranch = {
sx: number;
sy: number;
sz: number;
cx: number;
cy: number;
cz: number;
ex: number;
ey: number;
ez: number;
thickStart: number;
thickEnd: number;
isTerminal: boolean;
};
type RootSegment = {
sx: number;
sz: number;
ex: number;
ez: number;
bend: number;
thickStart: number;
thickEnd: number;
depth: number;
};
function evalBezier(
sx: number,
sy: number,
sz: number,
cx: number,
cy: number,
cz: number,
ex: number,
ey: number,
ez: number,
t: number
) {
const omt = 1 - t;
return {
x: omt * omt * sx + 2 * omt * t * cx + t * t * ex,
y: omt * omt * sy + 2 * omt * t * cy + t * t * ey,
z: omt * omt * sz + 2 * omt * t * cz + t * t * ez,
};
}
function trunkSurface(h: number, theta: number) {
const y = GROUND_Y + h * 4.5;
const baseR = 0.8;
const flare = Math.exp(-h * 15) * 2;
const taper = 1 - h * 0.6;
let radius = (baseR + flare) * taper;
const ridges = Math.abs(Math.sin(theta * 8)) * 0.12 + Math.sin(theta * 15) * 0.05;
radius += ridges * (1 - Math.min(1, flare * 0.5));
const bendX = Math.sin(y * 0.6) * 0.3;
const bendZ = Math.cos(y * 0.4) * 0.3;
return {
x: bendX + radius * Math.cos(theta),
y,
z: bendZ + radius * Math.sin(theta),
radius,
bendX,
bendZ,
};
}
function depositTrunkParticle(
positions: Float32Array,
colors: Float32Array,
isFlower: Float32Array,
index: number,
h: number,
theta: number,
palette: ThemePalette,
radialJitter = 1
) {
const surface = trunkSurface(h, theta);
const jitter = (Math.random() - 0.5) * 0.08 * radialJitter;
positions[index * 3] = surface.x + Math.cos(theta) * jitter;
positions[index * 3 + 1] = surface.y + (Math.random() - 0.5) * 0.04;
positions[index * 3 + 2] = surface.z + Math.sin(theta) * jitter;
setColor(colors, index, palette.treeTrunk, 0.06);
isFlower[index] = 0;
}
function generateDetailedTree(
count: number,
positions: Float32Array,
colors: Float32Array,
isFlower: Float32Array,
palette: ThemePalette
) {
const landCount = Math.floor(count * (25000 / 150000));
const rootCount = Math.floor(count * (15000 / 150000));
const trunkCount = Math.floor(count * (3000 / 150000));
const branchParticleCount = Math.floor(count * (55000 / 150000));
const junctionBudget = Math.floor(count * (4000 / 150000));
const flowerPlotCount = Math.max(120, Math.floor(count * (1000 / 150000)));
let pIdx = 0;
for (let i = 0; i < landCount && pIdx < count; i += 1, pIdx += 1) {
const r = 10 * Math.sqrt(Math.random());
const theta = Math.random() * Math.PI * 2;
positions[pIdx * 3] = r * Math.cos(theta);
positions[pIdx * 3 + 1] = GROUND_Y;
positions[pIdx * 3 + 2] = r * Math.sin(theta);
setColor(colors, pIdx, palette.treeFoliage, 0.03);
isFlower[pIdx] = 0;
}
const rootSegments: RootSegment[] = [];
function addRootSegment(
sx: number,
sz: number,
angle: number,
length: number,
thickStart: number,
depth: number,
maxDepth: number
) {
const curve = (Math.random() - 0.5) * length * (0.18 + depth * 0.04);
const ex = sx + Math.cos(angle) * length;
const ez = sz + Math.sin(angle) * length;
const thickEnd = Math.max(0.018, thickStart * (0.42 + Math.random() * 0.15));
rootSegments.push({ sx, sz, ex, ez, bend: curve, thickStart, thickEnd, depth });
if (depth >= maxDepth) return;
const childCount = depth === 1 ? 2 + Math.floor(Math.random() * 2) : 1 + Math.floor(Math.random() * 2);
for (let child = 0; child < childCount; child += 1) {
const spawnT = 0.42 + Math.random() * 0.46;
const mx = sx + (ex - sx) * spawnT;
const mz = sz + (ez - sz) * spawnT;
const childAngle = angle + (child - (childCount - 1) / 2) * (0.38 + Math.random() * 0.42);
addRootSegment(
mx,
mz,
childAngle,
length * (0.45 + Math.random() * 0.34),
thickStart * (1 - spawnT) + thickEnd * spawnT,
depth + 1,
maxDepth
);
}
}
const primaryRoots = 26;
for (let i = 0; i < primaryRoots; i += 1) {
const angle = (i / primaryRoots) * Math.PI * 2 + (Math.random() - 0.5) * 0.16;
const rootFlare = 0.28 + Math.random() * 0.28;
addRootSegment(
Math.cos(angle) * rootFlare,
Math.sin(angle) * rootFlare,
angle,
4.8 + Math.random() * 3.8,
0.42 + Math.random() * 0.24,
1,
3 + Math.floor(Math.random() * 2)
);
}
for (let i = 0; i < rootCount && pIdx < count; i += 1, pIdx += 1) {
const segment = rootSegments[Math.floor(Math.random() * rootSegments.length)];
const t = Math.pow(Math.random(), 0.82);
const omt = 1 - t;
const midX = (segment.sx + segment.ex) * 0.5 + Math.cos(Math.atan2(segment.ez - segment.sz, segment.ex - segment.sx) + Math.PI / 2) * segment.bend;
const midZ = (segment.sz + segment.ez) * 0.5 + Math.sin(Math.atan2(segment.ez - segment.sz, segment.ex - segment.sx) + Math.PI / 2) * segment.bend;
const centerX = omt * omt * segment.sx + 2 * omt * t * midX + t * t * segment.ex;
const centerZ = omt * omt * segment.sz + 2 * omt * t * midZ + t * t * segment.ez;
const tangentX = 2 * omt * (midX - segment.sx) + 2 * t * (segment.ex - midX);
const tangentZ = 2 * omt * (midZ - segment.sz) + 2 * t * (segment.ez - midZ);
const tangentLen = Math.sqrt(tangentX * tangentX + tangentZ * tangentZ) || 1;
const normalX = -tangentZ / tangentLen;
const normalZ = tangentX / tangentLen;
const thick = (segment.thickStart * (1 - t) + segment.thickEnd * t) * (1 - Math.random() * 0.35);
const spread = (Math.random() - 0.5) * thick;
positions[pIdx * 3] = centerX + normalX * spread;
positions[pIdx * 3 + 1] = GROUND_Y + 0.012;
positions[pIdx * 3 + 2] = centerZ + normalZ * spread;
setColor(colors, pIdx, palette.treeTrunk, 0.05);
isFlower[pIdx] = 0;
}
for (let i = 0; i < flowerPlotCount && pIdx < count - 10; i += 1) {
const r = 2.5 + Math.random() * 7;
const theta = Math.random() * Math.PI * 2;
const fx = r * Math.cos(theta);
const fz = r * Math.sin(theta);
const fh = 0.35 + Math.random() * 0.55;
const magenta = Math.random() > 0.5;
const stemSegments = 8;
for (let j = 0; j < stemSegments; j += 1) {
positions[pIdx * 3] = fx;
positions[pIdx * 3 + 1] = GROUND_Y + (j / stemSegments) * fh;
positions[pIdx * 3 + 2] = fz;
setColor(colors, pIdx, palette.treeFoliage, 0.03);
isFlower[pIdx] = 0;
pIdx += 1;
}
positions[pIdx * 3] = fx;
positions[pIdx * 3 + 1] = GROUND_Y + fh;
positions[pIdx * 3 + 2] = fz;
if (magenta) {
setColorExact(colors, pIdx, 1, 0.2, 0.8);
} else {
setColorExact(colors, pIdx, 1, 0.9, 0.2);
}
isFlower[pIdx] = 1;
pIdx += 1;
}
for (let i = 0; i < trunkCount && pIdx < count; i += 1, pIdx += 1) {
const h = Math.random();
const theta = Math.random() * Math.PI * 2;
depositTrunkParticle(positions, colors, isFlower, pIdx, h, theta, palette);
}
const branches: TreeBranch[] = [];
const terminalBranches: TreeBranch[] = [];
const junctions: Array<{ h: number; theta: number; thick: number }> = [];
function generateBranch(
sx: number,
sy: number,
sz: number,
dirX: number,
dirY: number,
dirZ: number,
length: number,
thickStart: number,
depth: number,
maxDepth: number
) {
const isTerminal = depth >= maxDepth;
const droop = Math.max(0, depth - 1) * 0.06;
const ex = sx + dirX * length;
const ey = Math.max(sy + (dirY - droop) * length, BRANCH_MIN_Y + depth * 0.08);
const ez = sz + dirZ * length;
const cx = sx + dirX * length * 0.45;
const cy = Math.max(sy + dirY * length * 0.45 + 0.5 / depth, Math.max(sy, ey) + 0.12);
const cz = sz + dirZ * length * 0.45;
const thickEnd = Math.max(0.04, thickStart * 0.42);
const branch: TreeBranch = {
sx,
sy,
sz,
cx,
cy,
cz,
ex,
ey,
ez,
thickStart,
thickEnd,
isTerminal,
};
branches.push(branch);
if (isTerminal) terminalBranches.push(branch);
if (!isTerminal) {
const numChildren = depth === 1 ? 2 + Math.floor(Math.random() * 2) : 1 + Math.floor(Math.random() * 3);
for (let c = 0; c < numChildren; c += 1) {
const spawnT = 0.35 + Math.random() * 0.55;
const pos = evalBezier(sx, sy, sz, cx, cy, cz, ex, ey, ez, spawnT);
const omt = 1 - spawnT;
let tanX = 2 * omt * (cx - sx) + 2 * spawnT * (ex - cx);
let tanY = 2 * omt * (cy - sy) + 2 * spawnT * (ey - cy);
let tanZ = 2 * omt * (cz - sz) + 2 * spawnT * (ez - cz);
const tanLen = Math.sqrt(tanX * tanX + tanY * tanY + tanZ * tanZ) || 1;
tanX /= tanLen;
tanY /= tanLen;
tanZ /= tanLen;
const spread = 0.55 + depth * 0.25;
let cDirX = tanX + (Math.random() - 0.5) * spread;
let cDirY = tanY + (Math.random() - 0.5) * spread;
let cDirZ = tanZ + (Math.random() - 0.5) * spread;
cDirY = Math.max(cDirY, depth <= 2 ? 0.18 : -0.05);
const cLen = Math.sqrt(cDirX * cDirX + cDirY * cDirY + cDirZ * cDirZ);
cDirX /= cLen;
cDirY /= cLen;
cDirZ /= cLen;
const cLength = length * (0.62 + Math.random() * 0.28);
const cThick = thickStart * (1 - spawnT) + thickEnd * spawnT;
generateBranch(pos.x, pos.y, pos.z, cDirX, cDirY, cDirZ, cLength, cThick * 0.88, depth + 1, maxDepth);
}
}
}
const numMainBranches = 12 + Math.floor(Math.random() * 6);
for (let i = 0; i < numMainBranches; i += 1) {
const theta = (i / numMainBranches) * Math.PI * 2 + (Math.random() - 0.5) * 0.35;
const h = 0.62 + Math.random() * 0.34;
const surface = trunkSurface(h, theta);
const normalX = Math.cos(theta);
const normalZ = Math.sin(theta);
const embeddedDepth = Math.min(surface.radius * 0.5, 0.38);
const sx = surface.x - normalX * embeddedDepth;
const sy = surface.y;
const sz = surface.z - normalZ * embeddedDepth;
let dirX = normalX * 1.15;
let dirY = 0.78 + Math.random() * 0.5;
let dirZ = normalZ * 1.15;
const dirLen = Math.sqrt(dirX * dirX + dirY * dirY + dirZ * dirZ) || 1;
dirX /= dirLen;
dirY /= dirLen;
dirZ /= dirLen;
const length = 2.0 + Math.random() * 2.1;
const thickStart = Math.max(0.2, surface.radius * 0.9);
junctions.push({ h, theta, thick: thickStart });
const targetMaxDepth = 2 + Math.floor(Math.random() * 3);
generateBranch(sx, sy, sz, dirX, dirY, dirZ, length, thickStart, 1, targetMaxDepth);
}
const junctionParticles = Math.max(8, Math.floor(junctionBudget / Math.max(junctions.length, 1)));
for (const junction of junctions) {
for (let j = 0; j < junctionParticles && pIdx < count; j += 1, pIdx += 1) {
const h = junction.h + (Math.random() - 0.5) * 0.055;
const theta = junction.theta + (Math.random() - 0.5) * 0.72;
const surface = trunkSurface(h, theta);
const normalX = Math.cos(junction.theta);
const normalZ = Math.sin(junction.theta);
const collar = (Math.random() - 0.5) * junction.thick;
const bridge = Math.random() * Math.min(surface.radius * 0.85, 0.75);
positions[pIdx * 3] = surface.x - normalX * bridge + Math.cos(theta) * collar * 0.45;
positions[pIdx * 3 + 1] = surface.y + (Math.random() - 0.5) * junction.thick * 0.45;
positions[pIdx * 3 + 2] = surface.z - normalZ * bridge + Math.sin(theta) * collar * 0.45;
setColor(colors, pIdx, palette.treeTrunk, 0.06);
isFlower[pIdx] = 0;
}
}
const branchOrange: ShapeColors = palette.helix;
const branchOrangeTip: ShapeColors = palette.sphere;
for (let i = 0; i < branchParticleCount && pIdx < count; i += 1, pIdx += 1) {
const b = branches[Math.floor(Math.random() * branches.length)];
const t =
Math.random() < 0.45
? Math.pow(Math.random(), 2.2) * 0.35
: 0.15 + Math.random() * 0.85;
const pos = evalBezier(b.sx, b.sy, b.sz, b.cx, b.cy, b.cz, b.ex, b.ey, b.ez, t);
const thick = b.thickStart * (1 - t) + b.thickEnd * t;
const ringTheta = Math.random() * Math.PI * 2;
positions[pIdx * 3] = pos.x + thick * Math.cos(ringTheta);
positions[pIdx * 3 + 1] = pos.y + thick * Math.sin(ringTheta) * 0.35;
positions[pIdx * 3 + 2] = pos.z + thick * Math.sin(ringTheta);
setColor(colors, pIdx, b.isTerminal ? branchOrangeTip : branchOrange, 0.08);
isFlower[pIdx] = 0;
}
while (pIdx < count) {
if (terminalBranches.length === 0) break;
const b = terminalBranches[Math.floor(Math.random() * terminalBranches.length)];
const t = Math.pow(Math.random(), 0.6);
const pos = evalBezier(b.sx, b.sy, b.sz, b.cx, b.cy, b.cz, b.ex, b.ey, b.ez, t);
const angle = Math.random() * Math.PI * 2;
const spread = Math.random() * 1.8;
const droop = spread * (0.15 + Math.random() * 0.15) + Math.random() * 0.1;
positions[pIdx * 3] = pos.x + Math.cos(angle) * spread;
positions[pIdx * 3 + 1] = pos.y - droop;
positions[pIdx * 3 + 2] = pos.z + Math.sin(angle) * spread;
setColor(colors, pIdx, palette.treeFoliage, 0.1);
isFlower[pIdx] = 0;
pIdx += 1;
}
scalePositions(positions, SHAPE_SCALE);
}
function fillShapeColors(
geometry: BufferGeometry,
palette: ThemePalette,
@ -188,6 +576,9 @@ const vertexShader = `
attribute vec3 col2;
attribute vec3 pos3;
attribute vec3 col3;
attribute vec3 pos4;
attribute vec3 col4;
attribute float aIsFlower;
uniform float uMorph;
uniform float uTime;
@ -201,7 +592,7 @@ const vertexShader = `
varying float vAlpha;
void main() {
float m = mod(uMorph, ${SHAPE_COUNT}.0);
float m = clamp(uMorph, 0.0, ${MORPH_MAX}.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;
@ -212,14 +603,37 @@ const vertexShader = `
finalPos = mix(pos0, pos1, ease);
finalCol = mix(col0, col1, ease);
} else if (m < 2.0) {
t = m - 1.0;
ease = t < 0.5 ? 4.0 * t * t * t : 1.0 - pow(-2.0 * t + 2.0, 3.0) / 2.0;
finalPos = mix(pos1, pos2, ease);
finalCol = mix(col1, col2, ease);
} else if (m < 3.0) {
t = m - 2.0;
ease = t < 0.5 ? 4.0 * t * t * t : 1.0 - pow(-2.0 * t + 2.0, 3.0) / 2.0;
finalPos = mix(pos2, pos3, ease);
finalCol = mix(col2, col3, ease);
} else {
finalPos = mix(pos3, pos0, ease);
finalCol = mix(col3, col0, ease);
t = clamp(m - 3.0, 0.0, 1.0);
ease = t < 0.5 ? 4.0 * t * t * t : 1.0 - pow(-2.0 * t + 2.0, 3.0) / 2.0;
finalPos = mix(pos3, pos4, ease);
finalCol = mix(col3, col4, ease);
}
float treeInfluence = 0.0;
if (m > 3.0) treeInfluence = clamp(m - 3.0, 0.0, 1.0);
if (treeInfluence > 0.0) {
if (finalPos.y > -2.0) {
float swayInfluence = clamp((finalPos.y + 2.0) / 8.0, 0.0, 1.0) * treeInfluence;
finalPos.x += sin(uTime * 1.2 + finalPos.y) * 0.25 * swayInfluence;
finalPos.z += cos(uTime * 1.5 + finalPos.x) * 0.25 * swayInfluence;
}
if (aIsFlower > 0.5) {
float blink = 0.5 + 0.5 * sin(uTime * 5.0 + pos4.x * 12.0 + pos4.z * 10.0);
finalPos.y += blink * 0.35 * treeInfluence;
finalCol += finalCol * blink * 2.5 * treeInfluence;
}
}
float sphereInfluence = max(0.0, 1.0 - abs(m - 1.0));
@ -244,9 +658,16 @@ const vertexShader = `
}
}
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;
float idleWobble = 1.0;
if (m > 3.0 && finalPos.y < -3.0) {
idleWobble = 0.0;
} else if (m > 3.0) {
idleWobble = 1.0 - treeInfluence;
}
finalPos.y += sin(finalPos.x * 2.0 + uTime * 2.0) * 0.1 * idleWobble;
finalPos.x += cos(finalPos.y * 2.0 + uTime * 2.0) * 0.1 * idleWobble;
finalPos.z += sin(finalPos.x * 1.5 + finalPos.y * 1.5 + uTime * 1.6) * 0.06 * idleWobble;
vec4 mvPosition = modelViewMatrix * vec4(finalPos, 1.0);
gl_PointSize = (13.0 * uPixelRatio * uPointScale) * (1.0 / -mvPosition.z);
@ -275,19 +696,20 @@ const fragmentShader = `
function clampMorph(morph: number) {
if (morph <= 0) return 0;
if (morph >= MORPH_MAX - MORPH_LOOP_EPS) return MORPH_MAX - MORPH_LOOP_EPS;
if (morph >= MORPH_MAX) return MORPH_MAX;
return morph;
}
function getTreeHold(morph: number) {
const m = clampMorph(morph);
if (m < 3.5) return 0;
return Math.min(1, (m - 3.5) / 0.5);
}
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;
const m = clampMorph(morph);
if (m >= 0.5) return 0;
return 1 - m / 0.5;
}
export function initParticleBackground(container: HTMLElement): () => void {
@ -323,11 +745,15 @@ export function initParticleBackground(container: HTMLElement): () => void {
const col2 = new Float32Array(particleCount * 3);
const pos3 = new Float32Array(particleCount * 3);
const col3 = new Float32Array(particleCount * 3);
const pos4 = new Float32Array(particleCount * 3);
const col4 = new Float32Array(particleCount * 3);
const isFlower = new Float32Array(particleCount);
generateLayers(particleCount, pos0, col0, palette.layers);
generateSphere(particleCount, pos1, col1, palette.sphere);
generateCube(particleCount, pos2, col2, palette.cube);
generateHelix(particleCount, pos3, col3, palette.helix);
generateDetailedTree(particleCount, pos4, col4, isFlower, palette);
geometry.setAttribute("pos0", new BufferAttribute(pos0, 3));
geometry.setAttribute("col0", new BufferAttribute(col0, 3));
@ -337,6 +763,9 @@ export function initParticleBackground(container: HTMLElement): () => void {
geometry.setAttribute("col2", new BufferAttribute(col2, 3));
geometry.setAttribute("pos3", new BufferAttribute(pos3, 3));
geometry.setAttribute("col3", new BufferAttribute(col3, 3));
geometry.setAttribute("pos4", new BufferAttribute(pos4, 3));
geometry.setAttribute("col4", new BufferAttribute(col4, 3));
geometry.setAttribute("aIsFlower", new BufferAttribute(isFlower, 1));
geometry.setAttribute("position", new BufferAttribute(pos0, 3));
const material = new ShaderMaterial({
@ -359,6 +788,7 @@ export function initParticleBackground(container: HTMLElement): () => void {
});
const points = new Points(geometry, material);
points.position.y = -0.65 * SHAPE_SCALE;
scene.add(points);
let targetMorph = 0;
@ -377,6 +807,10 @@ export function initParticleBackground(container: HTMLElement): () => void {
function applyThemePalette(theme: ThemeChoice) {
const nextPalette = getPalette(theme);
fillShapeColors(geometry, nextPalette, particleCount);
generateDetailedTree(particleCount, pos4, col4, isFlower, nextPalette);
(geometry.getAttribute("col4") as BufferAttribute).needsUpdate = true;
(geometry.getAttribute("pos4") as BufferAttribute).needsUpdate = true;
(geometry.getAttribute("aIsFlower") as BufferAttribute).needsUpdate = true;
material.uniforms.uGlow.value = nextPalette.glow;
material.uniforms.uParticleAlpha.value = nextPalette.particleAlpha;
material.uniforms.uColorBoost.value = nextPalette.colorBoost;
@ -388,9 +822,21 @@ export function initParticleBackground(container: HTMLElement): () => void {
applyThemePalette(resolveTheme());
function getScrollProgress() {
const scrollHeight = Math.max(
document.documentElement.scrollHeight,
document.body.scrollHeight
);
const maxScroll = Math.max(0, scrollHeight - window.innerHeight);
if (maxScroll <= 0) return 0;
const scrollY = Math.min(Math.max(0, window.scrollY), maxScroll);
const progress = scrollY / maxScroll;
// Lock morph at the end so elastic overscroll / sub-pixel gaps do not flicker.
return progress >= 0.992 ? 1 : progress;
}
function onScroll() {
const maxScroll = document.body.scrollHeight - window.innerHeight;
targetMorph = maxScroll > 0 ? clampMorph((window.scrollY / maxScroll) * MORPH_MAX) : 0;
targetMorph = clampMorph(getScrollProgress() * MORPH_MAX);
}
function onMouseMove(event: MouseEvent) {
@ -405,6 +851,7 @@ export function initParticleBackground(container: HTMLElement): () => void {
renderer.setSize(window.innerWidth, window.innerHeight);
material.uniforms.uPixelRatio.value = Math.min(window.devicePixelRatio, 2);
renderer.setClearColor(getPaperColor(), 1);
onScroll();
}
const themeObserver = new MutationObserver(() => {
@ -444,11 +891,13 @@ export function initParticleBackground(container: HTMLElement): () => void {
material.uniforms.uMorph.value = currentMorph;
const layersHold = getLayersHold(currentMorph);
const treeHold = getTreeHold(currentMorph);
const spinWeight = 1 - layersHold;
const treeSettle = smoothstep(treeHold);
if (layersHold < 0.99) {
spinY += delta * 0.1;
spinX += delta * 0.05;
spinY += delta * (0.1 + 0.05 * treeSettle);
spinX += delta * 0.05 * (1 - treeSettle);
} else {
spinY = 0;
spinX = 0;
@ -456,7 +905,7 @@ export function initParticleBackground(container: HTMLElement): () => void {
const easedSpin = smoothstep(spinWeight);
points.rotation.y = spinY * easedSpin;
points.rotation.x = spinX * easedSpin;
points.rotation.x = spinX * easedSpin * (1 - treeSettle);
if (mouseEnabled) {
raycaster.setFromCamera(mouseNdc, camera);

View file

@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"build:js": "vite build",
"build": "bun run build:js && zola build",
"build": "rm -f themes/openfreebooks/static/main.css public/main.css && bun run build:js && zola build",
"dev:js": "vite build --watch",
"scan:size": "bun scripts/scan-output-size.ts"
},

View file

@ -10,4 +10,6 @@ not markdown, html based. but in every page, we should have a copy as md button
we don't i18n. i18n will be solved in future by big companies.
search is supported by pagefind.
search is supported by pagefind.
we have a catalog of subjects. Each subject will have chapters. Each chapters can be labelled (multiple) cirriculum. E.g. DSE, IB, A level. so that user knows if a chapter is needed in a cirriculum. Each subject should show all chapters by default (all cirriculum). chapters should be sequential in a tree structure. users can filter by themselves.

View file

@ -345,6 +345,45 @@ h3 {
margin-bottom: 12px;
}
.tree-cta {
min-height: clamp(680px, 92vh, 980px);
display: flex;
align-items: center;
text-align: center;
padding: clamp(180px, 24vh, 280px) 0 clamp(40px, 12vh, 280px);
}
.tree-cta__inner {
max-width: 820px;
margin-inline: auto;
}
.tree-cta__title {
max-width: 11ch;
margin-inline: auto;
margin: 0 0 24px;
font-size: clamp(3rem, 8vw, 7.5rem);
line-height: 0.92;
letter-spacing: -0.04em;
}
.tree-cta__lead {
max-width: 520px;
margin: 0 auto;
color: var(--soft-ink);
font-size: clamp(1.1rem, 2vw, 1.45rem);
line-height: 1.6;
}
.tree-cta__actions {
display: flex;
flex-wrap: wrap;
gap: 18px;
justify-content: center;
align-items: center;
margin-top: 34px;
}
.marquee-shell {
margin: 0;
padding: 0;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -96,4 +96,16 @@
<p class="section-note">Want to list a book? Open a pull request or start a discussion on GitHub.</p>
</div>
</section>
<section class="tree-cta" aria-labelledby="tree-cta-title">
<div class="container tree-cta__inner">
<p class="section-kicker">For Every Learner</p>
<h2 id="tree-cta-title" class="tree-cta__title">The next generation is the future.</h2>
<p class="tree-cta__lead">Let's provide good, free education that anyone can read, share, and improve.</p>
<div class="tree-cta__actions">
<a class="button button-primary" href="{{ config.extra.browse_url | safe }}">Start reading</a>
<a class="button button-secondary" href="{{ config.extra.github_url | safe }}">Help grow the library</a>
</div>
</div>
</section>
{% endblock content %}