Add starfield.js in the landing page. (#1833)

This commit is contained in:
Yudong Jin
2025-12-27 05:33:45 +08:00
committed by GitHub
parent da230312cd
commit 2db8f741a8
6 changed files with 502 additions and 4 deletions

View File

@@ -1,7 +1,10 @@
<!-- Section: hero -->
<section data-md-color-scheme="slate" data-md-color-primary="grey" class="home-div" style="height: min(100vh, 120vw); position: relative; margin-top:-2.4rem; padding: 0; overflow: hidden;">
<!-- hero image -->
<img src="assets/hero/universe_bg.png" class="hero-bg" alt="">
<!-- <img alt="" class="hero-bg" src="assets/hero/universe_bg.png" /> -->
<div class="starfield">
<div class="starfield-origin"></div>
</div>
<div class="hero-div">
<img src="assets/hero/ground.png" alt="" style="position: absolute; width: auto; height: 26.445%; left: 28.211%; top: 54.145%;">
<img src="assets/hero/links.png" alt="" style="position: absolute; width: auto; height: 78.751%; left: 10.545%; top: 7.326%;">

View File

@@ -1,7 +1,10 @@
<!-- Section: hero -->
<section data-md-color-scheme="slate" data-md-color-primary="grey" class="home-div" style="height: min(100vh, 120vw); position: relative; margin-top:-2.4rem; padding: 0; overflow: hidden;">
<!-- hero image -->
<img src="../assets/hero/universe_bg.png" class="hero-bg" alt="">
<!-- <img alt="" class="hero-bg" src="assets/hero/universe_bg.png" /> -->
<div class="starfield">
<div class="starfield-origin"></div>
</div>
<div class="hero-div">
<img src="../assets/hero/ground.png" alt="" style="position: absolute; width: auto; height: 26.445%; left: 28.211%; top: 54.145%;">
<img src="../assets/hero/links.png" alt="" style="position: absolute; width: auto; height: 78.751%; left: 10.545%; top: 7.326%;">

View File

@@ -1,7 +1,10 @@
<!-- Section: hero -->
<section data-md-color-scheme="slate" data-md-color-primary="grey" class="home-div" style="height: min(100vh, 120vw); position: relative; margin-top:-2.4rem; padding: 0; overflow: hidden;">
<!-- hero image -->
<img src="../assets/hero/universe_bg.png" class="hero-bg" alt="">
<!-- <img alt="" class="hero-bg" src="assets/hero/universe_bg.png" /> -->
<div class="starfield">
<div class="starfield-origin"></div>
</div>
<div class="hero-div">
<img src="../assets/hero/ground.png" alt="" style="position: absolute; width: auto; height: 26.445%; left: 28.211%; top: 54.145%;">
<img src="../assets/hero/links.png" alt="" style="position: absolute; width: auto; height: 78.751%; left: 10.545%; top: 7.326%;">

View File

@@ -0,0 +1,471 @@
/*
* starfield.js
*
* Version: 1.5.0
* Description: Interactive starfield background
*
* Usage:
* Starfield.setup({
* // options
* });
*/
(function (root, factory) {
if (typeof define === "function" && define.amd) {
define([], factory);
} else if (typeof module === "object" && module.exports) {
module.exports = factory();
} else {
root.Starfield = factory();
}
})(this, function () {
const Starfield = {};
const config = {
numStars: 250, // Number of stars
baseSpeed: 1, // Base speed of stars (will affect acceleration)
trailLength: 0.8, // Length of star trail (0-1)
starColor: "rgb(255, 255, 255)", // Color of stars (only rgb)
canvasColor: "rgb(0, 0, 0)", // Canvas background color (only rgb)
hueJitter: 0, // Maximum hue variation in degrees (0-360)
maxAcceleration: 10, // Maximum acceleration
accelerationRate: 0.2, // Rate of acceleration
decelerationRate: 0.2, // Rate of deceleration
minSpawnRadius: 80, // Minimum spawn distance from origin
maxSpawnRadius: 500, // Maximum spawn distance from origin
auto: true,
originX: null,
originY: null,
container: null,
originElement: null,
};
let stars = [];
let accelerate = false;
let accelerationFactor = 0;
let originX = 0;
let originY = 0;
let prevOriginX = 0;
let prevOriginY = 0;
let canvas, ctx;
let width, height;
let lastTimestamp = 0;
let canvasRGB = [0, 0, 0];
let lastCanvasColor = config.canvasColor;
let origin;
let container;
const mouseEnterHandler = () => (accelerate = true);
const mouseLeaveHandler = () => (accelerate = false);
const resizeHandler = () => windowResized(container, origin);
function visibilityHandler() {
if (document.visibilityState === "visible") {
lastTimestamp = performance.now();
}
}
function getOriginY(origin, container) {
const originRect = origin.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
return originRect.top - containerRect.top + originRect.height / 2;
}
function getOriginX(origin, container) {
const originRect = origin.getBoundingClientRect();
const containerRect = container.getBoundingClientRect();
return originRect.left - containerRect.left + originRect.width / 2;
}
/**
* Set up and start the starfield animation.
* @param {Object} userConfig Configuration options.
*/
function setup(userConfig = {}) {
Object.assign(config, userConfig);
container = config.container || document.querySelector(".starfield");
if (!container) {
throw new Error("Starfield: No container element found.");
}
// container.style.position = "relative";
width = container.clientWidth;
height = container.clientHeight;
canvas = document.createElement("canvas");
canvas.width = width;
canvas.height = height;
canvas.style.position = "absolute";
canvas.style.top = "0";
canvas.style.left = "0";
canvas.style.width = "100%";
canvas.style.height = "100%";
canvas.style.zIndex = "-1";
canvasRGB = parseRGBA(config.canvasColor);
container.appendChild(canvas);
ctx = canvas.getContext("2d");
if (config.auto) {
origin =
config.originElement || document.querySelector(".starfield-origin");
if (!origin) {
throw new Error("Starfield: No origin element found.");
}
originX = getOriginX(origin, container);
originY = getOriginY(origin, container);
origin.addEventListener("mouseenter", mouseEnterHandler);
origin.addEventListener("mouseleave", mouseLeaveHandler);
window.addEventListener("resize", resizeHandler);
} else {
originX = config.originX !== null ? config.originX : width / 2;
originY = config.originY !== null ? config.originY : height / 2;
}
for (let i = 0; i < config.numStars; i++) {
const star = createRandomStar();
stars.push(star);
}
document.addEventListener("visibilitychange", visibilityHandler);
requestAnimationFrame(draw);
}
function windowResized(container, origin) {
width = container.clientWidth;
height = container.clientHeight;
canvas.width = width;
canvas.height = height;
originX = getOriginX(origin, container);
originY = getOriginY(origin, container);
stars.forEach((star) => star.reset());
}
function createRandomStar() {
const angle = random(0, Math.PI * 2);
const radius = random(config.minSpawnRadius, config.maxSpawnRadius);
const x = originX + Math.cos(angle) * radius;
const y = originY + Math.sin(angle) * radius;
return new Star(x, y);
}
class Star {
constructor(x, y) {
this.pos = {
x: x,
y: y,
};
this.prevpos = {
x: x,
y: y,
};
this.vel = {
x: 0,
y: 0,
};
this.angle = Math.atan2(y - originY, x - originX);
this.baseSpeed = random(config.baseSpeed * 0.5, config.baseSpeed * 1.5);
this.hueOffset = random(-config.hueJitter, config.hueJitter);
}
reset() {
const newStar = createRandomStar();
this.pos.x = newStar.pos.x;
this.pos.y = newStar.pos.y;
this.prevpos.x = this.pos.x;
this.prevpos.y = this.pos.y;
this.vel.x = 0;
this.vel.y = 0;
this.angle = Math.atan2(this.pos.y - originY, this.pos.x - originX);
this.baseSpeed = random(config.baseSpeed * 0.5, config.baseSpeed * 1.5);
this.hueOffset = random(-config.hueJitter, config.hueJitter);
}
update(acc, deltaTime) {
const adjustedAcc = acc * this.baseSpeed;
this.vel.x += Math.cos(this.angle) * adjustedAcc * deltaTime;
this.vel.y += Math.sin(this.angle) * adjustedAcc * deltaTime;
this.prevpos.x = this.pos.x;
this.prevpos.y = this.pos.y;
this.pos.x += this.vel.x * deltaTime;
this.pos.y += this.vel.y * deltaTime;
}
draw() {
let velMag = Math.sqrt(
this.vel.x * this.vel.x + this.vel.y * this.vel.y
);
velMag = velMag * 3;
const alpha = map(velMag, 0, 10, 0, 1);
const weight = map(velMag, 0, 10, 1, 3);
ctx.lineWidth = weight;
const [r, g, b] = parseRGBA(config.starColor);
const [h, s, l] = rgbToHsl(r, g, b);
const adjustedH = (h + this.hueOffset + 360) % 360;
const [newR, newG, newB] = hslToRgb(adjustedH, s, l).map((v) =>
Math.round(v)
);
ctx.strokeStyle = `rgba(${newR}, ${newG}, ${newB}, ${alpha})`;
ctx.beginPath();
ctx.moveTo(this.prevpos.x, this.prevpos.y);
ctx.lineTo(this.pos.x, this.pos.y);
ctx.stroke();
}
isActive() {
return onScreen(this.pos.x, this.pos.y);
}
updateAngle() {
this.angle = Math.atan2(this.pos.y - originY, this.pos.x - originX);
}
}
function draw(timestamp) {
if (!lastTimestamp) lastTimestamp = timestamp;
const deltaTime = (timestamp - lastTimestamp) / 16.67;
lastTimestamp = timestamp;
if (config.auto) {
originX = getOriginX(origin, container);
originY = getOriginY(origin, container);
if (originX !== prevOriginX || originY !== prevOriginY) {
stars.forEach((star) => {
star.updateAngle();
});
prevOriginX = originX;
prevOriginY = originY;
}
}
if (lastCanvasColor !== config.canvasColor) {
canvasRGB = parseRGBA(config.canvasColor);
lastCanvasColor = config.canvasColor;
}
const [bgR, bgG, bgB] = canvasRGB;
ctx.fillStyle = `rgba(${bgR}, ${bgG}, ${bgB}, ${1 - config.trailLength})`;
ctx.fillRect(0, 0, width, height);
if (accelerate) {
accelerationFactor = Math.min(
accelerationFactor + config.accelerationRate * deltaTime,
config.maxAcceleration
);
} else {
accelerationFactor = Math.max(
accelerationFactor - config.decelerationRate * deltaTime,
0
);
}
const baseAcc = 0.01;
const currentAcc = baseAcc * (1 + accelerationFactor * 10);
for (let star of stars) {
star.update(currentAcc, deltaTime);
star.draw();
if (!star.isActive()) {
star.reset();
}
}
requestAnimationFrame(draw);
}
function onScreen(x, y) {
return x >= 0 && x <= width && y >= 0 && y <= height;
}
function random(min, max) {
return Math.random() * (max - min) + min;
}
// https://gist.github.com/mjackson/5311256
function rgbToHsl(r, g, b) {
r /= 255;
g /= 255;
b /= 255;
const max = Math.max(r, g, b),
min = Math.min(r, g, b);
let h,
s,
l = (max + min) / 2;
if (max === min) {
h = s = 0;
} else {
const d = max - min;
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
switch (max) {
case r:
h = (g - b) / d + (g < b ? 6 : 0);
break;
case g:
h = (b - r) / d + 2;
break;
case b:
h = (r - g) / d + 4;
break;
}
h /= 6;
}
return [h * 360, s, l];
}
// https://gist.github.com/mjackson/5311256
function hslToRgb(h, s, l) {
let r, g, b;
h = h / 360;
if (s === 0) {
r = g = b = l;
} else {
const hue2rgb = (p, q, t) => {
if (t < 0) t += 1;
if (t > 1) t -= 1;
if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1 / 2) return q;
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p;
};
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q;
r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1 / 3);
}
return [r * 255, g * 255, b * 255];
}
function parseRGBA(color) {
const rgbaRegex = /rgba?\((\d+),\s*(\d+),\s*(\d+)/;
const match = color.match(rgbaRegex);
if (match) {
return [
parseInt(match[1], 10),
parseInt(match[2], 10),
parseInt(match[3], 10),
];
}
return [255, 255, 255];
}
function map(value, start1, stop1, start2, stop2) {
return ((value - start1) / (stop1 - start1)) * (stop2 - start2) + start2;
}
/**
* Set the acceleration state of the starfield.
* @param {boolean} state The acceleration state.
*/
function setAccelerate(state) {
accelerate = state;
}
/**
* Set the x-coordinate of the origin of the starfield.
* @param {number} x The x-coordinate of the origin.
*/
function setOriginX(x) {
originX = x;
stars.forEach((star) => {
star.angle = Math.atan2(star.pos.y - originY, star.pos.x - originX);
});
}
/**
* Set the y-coordinate of the origin of the starfield.
* @param {number} y The y-coordinate of the origin.
*/
function setOriginY(y) {
originY = y;
stars.forEach((star) => {
star.angle = Math.atan2(star.pos.y - originY, star.pos.x - originX);
});
}
/**
* Set the origin of the starfield to a specific point.
* @param {number} x The x-coordinate of the origin.
* @param {number} y The y-coordinate of the origin.
*/
function setOrigin(x, y) {
originX = x;
originY = y;
stars.forEach((star) => {
star.angle = Math.atan2(star.pos.y - originY, star.pos.x - originX);
});
}
/**
* Resize the starfield to a new width and height.
* @param {number} newWidth The new width of the starfield.
* @param {number} newHeight The new height of the starfield.
*/
function resize(newWidth, newHeight) {
width = newWidth;
height = newHeight;
canvas.width = width;
canvas.height = height;
if (config.originY !== null) {
originY = config.originY;
} else {
originY = height / 2;
}
stars.forEach((star) => star.reset());
}
function cleanup() {
if (origin) {
origin.removeEventListener("mouseenter", mouseEnterHandler);
origin.removeEventListener("mouseleave", mouseLeaveHandler);
}
window.removeEventListener("resize", resizeHandler);
document.removeEventListener("visibilitychange", visibilityHandler);
if (canvas && canvas.parentNode) {
canvas.parentNode.removeChild(canvas);
}
stars = [];
accelerate = false;
accelerationFactor = 0;
originX = 0;
originY = 0;
prevOriginX = 0;
prevOriginY = 0;
lastTimestamp = 0;
}
Starfield.setup = setup;
Starfield.setAccelerate = setAccelerate;
Starfield.setOrigin = setOrigin;
Starfield.setOriginX = setOriginX;
Starfield.setOriginY = setOriginY;
Starfield.resize = resize;
Starfield.config = config;
Starfield.cleanup = cleanup;
return Starfield;
});

View File

@@ -552,3 +552,18 @@ a:hover .text-button span {
width: 100%;
height: 100%;
}
/* starfield */
.starfield {
position: absolute;
width: 100%;
height: 100%;
z-index: 0;
}
.starfield-origin {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}

View File

@@ -1,7 +1,10 @@
<!-- Section: hero -->
<section data-md-color-scheme="slate" data-md-color-primary="grey" class="home-div" style="height: min(100vh, 120vw); position: relative; margin-top:-2.4rem; padding: 0; overflow: hidden;">
<!-- hero image -->
<img src="../assets/hero/universe_bg.png" class="hero-bg" alt="">
<!-- <img alt="" class="hero-bg" src="assets/hero/universe_bg.png" /> -->
<div class="starfield">
<div class="starfield-origin"></div>
</div>
<div class="hero-div">
<img src="../assets/hero/ground.png" alt="" style="position: absolute; width: auto; height: 26.445%; left: 28.211%; top: 54.145%;">
<img src="../assets/hero/links.png" alt="" style="position: absolute; width: auto; height: 78.751%; left: 10.545%; top: 7.326%;">