UNPKG

@omnedia/ngx-starry-sky

Version:

A simple component library to create a container with an animated background.

271 lines (266 loc) 14.4 kB
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