@omnedia/ngx-starry-sky
Version:
A simple component library to create a container with an animated background.
271 lines (266 loc) • 14.4 kB
JavaScript
import * as i1 from '@angular/common';
import { isPlatformBrowser, CommonModule } from '@angular/common';
import * as i0 from '@angular/core';
import { signal, PLATFORM_ID, Input, ViewChild, Inject, ChangeDetectionStrategy, Component } from '@angular/core';
class NgxStarrySkyComponent {
platformId;
canvasRef;
svgRef;
wrapperRef;
styleClass;
disableShootingStars = false;
set skyColor(color) {
this.style["--om-starry-sky-color"] = color;
}
set starsBackgroundPropsValue(props) {
this.starsBackgroundProps = { ...this.starsBackgroundProps, ...props };
}
starsBackgroundProps = {
starDensity: 0.00015,
allStarsTwinkle: true,
twinkleProbability: 0.7,
minTwinkleSpeed: 0.5,
maxTwinkleSpeed: 1,
};
set shootingStarsPropsValue(props) {
this.shootingStarsProps = { ...this.shootingStarsProps, ...props };
}
shootingStarsProps = {
minSpeed: 10,
maxSpeed: 30,
minDelay: 1200,
maxDelay: 4200,
starColor: "#cd8ef8",
trailColor: "#80dffa",
starWidth: 10,
starHeight: 1,
};
shootingStar = signal(undefined);
style = {};
stars = [];
isInView = signal(false);
isAnimating = signal(false);
animationFrameIdSky;
animationFrameIdShootingStar;
intersectionObserver;
constructor(platformId) {
this.platformId = platformId;
}
ngAfterViewInit() {
this.initStarSky();
this.initShootingStars();
if (isPlatformBrowser(this.platformId)) {
this.intersectionObserver = new IntersectionObserver(([entry]) => {
this.renderContents(entry.isIntersecting);
});
this.intersectionObserver.observe(this.canvasRef.nativeElement);
}
}
ngOnDestroy() {
window.removeEventListener("resize", () => this.setCanvasSize());
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
}
if (this.animationFrameIdSky) {
cancelAnimationFrame(this.animationFrameIdSky);
}
if (this.animationFrameIdShootingStar) {
cancelAnimationFrame(this.animationFrameIdShootingStar);
}
}
renderContents(isIntersecting) {
if (isIntersecting && !this.isInView()) {
this.isInView.set(true);
if (!this.isAnimating()) {
this.animationFrameIdSky = requestAnimationFrame(() => this.renderStarSky());
this.animationFrameIdShootingStar = requestAnimationFrame(() => this.moveShootingStar());
}
}
else if (!isIntersecting) {
this.isInView.set(false);
}
}
initStarSky() {
window.addEventListener("resize", () => this.setCanvasSize());
this.setCanvasSize();
this.updateStars();
this.renderStarSky();
}
initShootingStars() {
if (this.disableShootingStars) {
return;
}
this.createShootingStar();
this.moveShootingStar();
}
renderStarSky() {
if (!this.isInView()) {
this.isAnimating.set(false);
return;
}
this.isAnimating.set(true);
const context = this.canvasRef.nativeElement.getContext("2d");
if (!context) {
return;
}
context.clearRect(0, 0, this.canvasRef.nativeElement.width, this.canvasRef.nativeElement.height);
this.stars.forEach((star) => {
context.beginPath();
context.arc(star.x, star.y, star.radius, 0, Math.PI * 2);
context.fillStyle = `rgba(255, 255, 255, ${star.opacity})`;
context.fill();
if (star.twinkleSpeed !== null) {
star.opacity =
0.5 +
Math.abs(Math.sin((Date.now() * 0.001) / star.twinkleSpeed) * 0.5);
}
});
this.animationFrameIdSky = requestAnimationFrame(() => this.renderStarSky());
}
updateStars() {
const context = this.canvasRef.nativeElement.getContext("2d");
if (!context) {
return;
}
const { width, height } = this.wrapperRef.nativeElement.getBoundingClientRect();
this.stars = this.generateStars(width, height);
}
generateStars(width, height) {
const area = width * height;
const numStars = Math.floor(area * (this.starsBackgroundProps.starDensity ?? 0.00015));
return Array.from({ length: numStars }, () => {
const shouldTwinkle = this.starsBackgroundProps.allStarsTwinkle ||
Math.random() < (this.starsBackgroundProps.twinkleProbability ?? 0.7);
return {
x: Math.random() * width,
y: Math.random() * height,
radius: Math.random() * 0.05 + 0.5,
opacity: Math.random() * 0.5 + 0.5,
twinkleSpeed: shouldTwinkle
? (this.starsBackgroundProps.minTwinkleSpeed ?? 0.5) +
Math.random() *
((this.starsBackgroundProps.maxTwinkleSpeed ?? 1) -
(this.starsBackgroundProps.minTwinkleSpeed ?? 0.5))
: null,
};
});
}
setCanvasSize() {
this.canvasRef.nativeElement.width =
this.wrapperRef.nativeElement.getBoundingClientRect().width;
this.canvasRef.nativeElement.height =
this.wrapperRef.nativeElement.getBoundingClientRect().height;
}
createShootingStar() {
if (this.disableShootingStars) {
return;
}
const { x, y, angle } = this.getRandomStartPoint();
const shootingStar = {
id: Date.now(),
x,
y,
angle,
scale: 1,
speed: Math.random() *
((this.shootingStarsProps.maxSpeed ?? 30) -
(this.shootingStarsProps.minSpeed ?? 10)) +
(this.shootingStarsProps.minSpeed ?? 10),
distance: 0,
};
this.shootingStar.set(shootingStar);
const randomDelay = Math.random() *
((this.shootingStarsProps.maxDelay ?? 4200) -
(this.shootingStarsProps.minDelay ?? 1200)) +
(this.shootingStarsProps.minDelay ?? 1200);
setTimeout(() => this.createShootingStar(), randomDelay);
}
moveShootingStar() {
if (this.disableShootingStars) {
return;
}
if (!this.isInView()) {
this.isAnimating.set(false);
return;
}
this.isAnimating.set(true);
this.animationFrameIdShootingStar = requestAnimationFrame(() => this.moveShootingStar());
if (!this.shootingStar()) {
return;
}
const prevStar = Object.assign({}, this.shootingStar());
const newX = prevStar.x + prevStar.speed * Math.cos((prevStar.angle * Math.PI) / 180);
const newY = prevStar.y + prevStar.speed * Math.sin((prevStar.angle * Math.PI) / 180);
const newDistance = prevStar.distance + prevStar.speed;
const newScale = 1 + newDistance / 100;
if (newX < -20 ||
newX > this.wrapperRef.nativeElement.offsetWidth + 20 ||
newY < -20 ||
newY > this.wrapperRef.nativeElement.offsetHeight + 20) {
this.shootingStar.set(undefined);
return;
}
prevStar.x = newX;
prevStar.y = newY;
prevStar.distance = newDistance;
prevStar.scale = newScale;
this.shootingStar.set(prevStar);
}
getRandomStartPoint() {
const side = Math.floor(Math.random() * 4);
const offset = Math.random() * this.wrapperRef.nativeElement.offsetWidth;
switch (side) {
case 0:
return { x: offset, y: 0, angle: 45 };
case 1:
return { x: this.wrapperRef.nativeElement.offsetWidth, y: offset, angle: 135 };
case 2:
return { x: offset, y: this.wrapperRef.nativeElement.offsetHeight, angle: 225 };
case 3:
return { x: 0, y: offset, angle: 315 };
default:
return { x: 0, y: 0, angle: 45 };
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NgxStarrySkyComponent, deps: [{ token: PLATFORM_ID }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.0.3", type: NgxStarrySkyComponent, isStandalone: true, selector: "om-starry-sky", inputs: { styleClass: "styleClass", disableShootingStars: "disableShootingStars", skyColor: "skyColor", starsBackgroundPropsValue: ["starsBackgroundConfig", "starsBackgroundPropsValue"], shootingStarsPropsValue: ["shootingStarsConfig", "shootingStarsPropsValue"] }, viewQueries: [{ propertyName: "canvasRef", first: true, predicate: ["OmStarrySkyCanvas"], descendants: true }, { propertyName: "svgRef", first: true, predicate: ["OmStarrySkySvg"], descendants: true }, { propertyName: "wrapperRef", first: true, predicate: ["OmStarrySkyWrapper"], descendants: true }], ngImport: i0, template: "<div class=\"om-starry-sky\" [ngStyle]=\"style\" [ngClass]=\"styleClass\" #OmStarrySkyWrapper>\r\n <div class=\"om-starry-sky-background\">\r\n <svg #OmStarrySkySvg>\r\n @if (shootingStar(); as star) {\r\n <rect [attr.key]=\"star.id\" [attr.x]=\"star.x\" [attr.y]=\"star.y\"\r\n [attr.width]=\"(shootingStarsProps.starWidth ?? 1) * (star.scale)\"\r\n [attr.height]=\"shootingStarsProps.starHeight ?? 10\" [attr.fill]=\"'url(#gradient)'\"\r\n [attr.transform]=\"'rotate(' + star.angle + ', ' + (star.x + (shootingStarsProps.starWidth ?? 1) * star.scale / 2) + ', ' + (star.y + (shootingStarsProps.starHeight ?? 10) / 2) + ')'\">\r\n </rect>\r\n <defs>\r\n <linearGradient id=\"gradient\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">\r\n <stop offset=\"0%\" [style.stopColor]=\"shootingStarsProps.trailColor\" [style.stopOpacity]=\"'0'\"/>\r\n <stop offset=\"100%\" [style.stopColor]=\"shootingStarsProps.starColor\" [style.stopOpacity]=\"'1'\"/>\r\n </linearGradient>\r\n </defs>\r\n }\r\n </svg>\r\n <canvas #OmStarrySkyCanvas></canvas>\r\n </div>\r\n\r\n <ng-content></ng-content>\r\n</div>\r\n", styles: [".om-starry-sky{--om-starry-sky-color: rgb(23 23 23/1);position:relative;width:100%;height:100%}.om-starry-sky .om-starry-sky-background{position:absolute;width:100%;height:100%;background-color:var(--om-starry-sky-color);pointer-events:none}.om-starry-sky .om-starry-sky-background canvas,.om-starry-sky .om-starry-sky-background svg{position:absolute;width:100%;height:100%;inset:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: NgxStarrySkyComponent, decorators: [{
type: Component,
args: [{ selector: "om-starry-sky", standalone: true, imports: [CommonModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"om-starry-sky\" [ngStyle]=\"style\" [ngClass]=\"styleClass\" #OmStarrySkyWrapper>\r\n <div class=\"om-starry-sky-background\">\r\n <svg #OmStarrySkySvg>\r\n @if (shootingStar(); as star) {\r\n <rect [attr.key]=\"star.id\" [attr.x]=\"star.x\" [attr.y]=\"star.y\"\r\n [attr.width]=\"(shootingStarsProps.starWidth ?? 1) * (star.scale)\"\r\n [attr.height]=\"shootingStarsProps.starHeight ?? 10\" [attr.fill]=\"'url(#gradient)'\"\r\n [attr.transform]=\"'rotate(' + star.angle + ', ' + (star.x + (shootingStarsProps.starWidth ?? 1) * star.scale / 2) + ', ' + (star.y + (shootingStarsProps.starHeight ?? 10) / 2) + ')'\">\r\n </rect>\r\n <defs>\r\n <linearGradient id=\"gradient\" x1=\"0%\" y1=\"0%\" x2=\"100%\" y2=\"100%\">\r\n <stop offset=\"0%\" [style.stopColor]=\"shootingStarsProps.trailColor\" [style.stopOpacity]=\"'0'\"/>\r\n <stop offset=\"100%\" [style.stopColor]=\"shootingStarsProps.starColor\" [style.stopOpacity]=\"'1'\"/>\r\n </linearGradient>\r\n </defs>\r\n }\r\n </svg>\r\n <canvas #OmStarrySkyCanvas></canvas>\r\n </div>\r\n\r\n <ng-content></ng-content>\r\n</div>\r\n", styles: [".om-starry-sky{--om-starry-sky-color: rgb(23 23 23/1);position:relative;width:100%;height:100%}.om-starry-sky .om-starry-sky-background{position:absolute;width:100%;height:100%;background-color:var(--om-starry-sky-color);pointer-events:none}.om-starry-sky .om-starry-sky-background canvas,.om-starry-sky .om-starry-sky-background svg{position:absolute;width:100%;height:100%;inset:0}\n"] }]
}], ctorParameters: () => [{ type: undefined, decorators: [{
type: Inject,
args: [PLATFORM_ID]
}] }], propDecorators: { canvasRef: [{
type: ViewChild,
args: ["OmStarrySkyCanvas"]
}], svgRef: [{
type: ViewChild,
args: ["OmStarrySkySvg"]
}], wrapperRef: [{
type: ViewChild,
args: ["OmStarrySkyWrapper"]
}], styleClass: [{
type: Input,
args: ["styleClass"]
}], disableShootingStars: [{
type: Input,
args: ["disableShootingStars"]
}], skyColor: [{
type: Input,
args: ["skyColor"]
}], starsBackgroundPropsValue: [{
type: Input,
args: ["starsBackgroundConfig"]
}], shootingStarsPropsValue: [{
type: Input,
args: ["shootingStarsConfig"]
}] } });
/*
* Public API Surface of ngx-starry-sky
*/
/**
* Generated bundle index. Do not edit.
*/
export { NgxStarrySkyComponent };
//# sourceMappingURL=omnedia-ngx-starry-sky.mjs.map