mirror of
https://github.com/HananoshikaYomaru/openfreebooks.git
synced 2026-11-12 20:45:52 +00:00
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:
parent
ee1ecbfb27
commit
07a76333bd
16 changed files with 42854 additions and 27 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
4
spec.md
4
spec.md
|
|
@ -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.
|
||||
|
|
@ -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
4235
themes/openfreebooks/static/js/particle-background-BIfD0E0p.js
Normal file
4235
themes/openfreebooks/static/js/particle-background-BIfD0E0p.js
Normal file
File diff suppressed because one or more lines are too long
4235
themes/openfreebooks/static/js/particle-background-Bs9rbJB8.js
Normal file
4235
themes/openfreebooks/static/js/particle-background-Bs9rbJB8.js
Normal file
File diff suppressed because one or more lines are too long
4210
themes/openfreebooks/static/js/particle-background-BvCC-uK-.js
Normal file
4210
themes/openfreebooks/static/js/particle-background-BvCC-uK-.js
Normal file
File diff suppressed because one or more lines are too long
4235
themes/openfreebooks/static/js/particle-background-C1I-pKlL.js
Normal file
4235
themes/openfreebooks/static/js/particle-background-C1I-pKlL.js
Normal file
File diff suppressed because one or more lines are too long
4235
themes/openfreebooks/static/js/particle-background-CNFSCvn8.js
Normal file
4235
themes/openfreebooks/static/js/particle-background-CNFSCvn8.js
Normal file
File diff suppressed because one or more lines are too long
4235
themes/openfreebooks/static/js/particle-background-CgsT97LC.js
Normal file
4235
themes/openfreebooks/static/js/particle-background-CgsT97LC.js
Normal file
File diff suppressed because one or more lines are too long
4235
themes/openfreebooks/static/js/particle-background-DE20wQLx.js
Normal file
4235
themes/openfreebooks/static/js/particle-background-DE20wQLx.js
Normal file
File diff suppressed because one or more lines are too long
4235
themes/openfreebooks/static/js/particle-background-FPd0A9Hn.js
Normal file
4235
themes/openfreebooks/static/js/particle-background-FPd0A9Hn.js
Normal file
File diff suppressed because one or more lines are too long
4235
themes/openfreebooks/static/js/particle-background-NU84UE0i.js
Normal file
4235
themes/openfreebooks/static/js/particle-background-NU84UE0i.js
Normal file
File diff suppressed because one or more lines are too long
4235
themes/openfreebooks/static/js/particle-background-jF071Y02.js
Normal file
4235
themes/openfreebooks/static/js/particle-background-jF071Y02.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -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 %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue