UNPKG

@flodlc/nebula

Version:

Including configurable Stars, Nebulas, Comets, Planets and Suns. Nebula comes with a vanilla JS and a React wrapper. Compatible with SSR

671 lines (670 loc) 20.6 kB
var __defProp = Object.defineProperty; var __getOwnPropSymbols = Object.getOwnPropertySymbols; var __hasOwnProp = Object.prototype.hasOwnProperty; var __propIsEnum = Object.prototype.propertyIsEnumerable; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __spreadValues = (a, b) => { for (var prop in b || (b = {})) if (__hasOwnProp.call(b, prop)) __defNormalProp(a, prop, b[prop]); if (__getOwnPropSymbols) for (var prop of __getOwnPropSymbols(b)) { if (__propIsEnum.call(b, prop)) __defNormalProp(a, prop, b[prop]); } return a; }; var __objRest = (source, exclude) => { var target = {}; for (var prop in source) if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0) target[prop] = source[prop]; if (source != null && __getOwnPropSymbols) for (var prop of __getOwnPropSymbols(source)) { if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop)) target[prop] = source[prop]; } return target; }; import React, { useRef, useLayoutEffect } from "react"; const DEFAULT_CONFIG = { starsCount: 400, starsColor: "#FFFFFF", starsRotationSpeed: 3, cometFrequence: 15, nebulasIntensity: 10, bgColor: "rgb(8,8,8)", sunScale: 1, planetsScale: 1, solarSystemOrbite: 65, solarSystemSpeedOrbit: 100 }; const fillConfig = (config) => { return Object.assign({}, DEFAULT_CONFIG, config); }; const parseColor = (color) => { const rgb = color.includes("#") ? hexToRGB(color) : color; const split = rgb.split(/[\(|,|\)]/); return [ parseInt(split[1], 10), parseInt(split[2], 10), parseInt(split[3], 10) ]; }; function hexToRGB(color) { let r = "0", g = "0", b = "0"; if (color.length <= 5) { r = "0x" + color[1] + color[1]; g = "0x" + color[2] + color[2]; b = "0x" + color[3] + color[3]; } else if (color.length >= 7) { r = "0x" + color[1] + color[2]; g = "0x" + color[3] + color[4]; b = "0x" + color[5] + color[6]; } return "rgb(" + +r + "," + +g + "," + +b + ")"; } const getRGB = (rgbChannels, opacity) => { return `rgba(${rgbChannels[0]}, ${rgbChannels[1]}, ${rgbChannels[2]}, ${opacity})`; }; class Drawable { constructor({ ctx }) { this.ctx = ctx; } getCanvasWidth() { return this.ctx.canvas.width; } getCanvasHeight() { return this.ctx.canvas.height; } get canvasMinSide() { return Math.min(this.getCanvasHeight(), this.getCanvasWidth()); } get canvasMaxSide() { return Math.max(this.getCanvasHeight(), this.getCanvasWidth()); } } class Astre extends Drawable { constructor({ ctx, width, speed, distance, rgb, origin, startAngle = Math.random() * 360 }) { super({ ctx }); this.relativeWidth = width; this.rgb = rgb; this.speed = speed; this.relativeDistance = distance; this.origin = origin; this.angle = Math.PI / 180 * (startAngle != null ? startAngle : 0); } rotate() { this.angle = (this.angle + Math.PI / 180 * this.speed) % 360; } get width() { return this.relativeWidth / 100 * this.canvasMinSide; } get distance() { return this.relativeDistance / 100 * this.canvasMinSide; } getAngle() { return this.angle; } getRefAngle() { var _a, _b; return this.getAngle() + ((_b = (_a = this.origin) == null ? void 0 : _a.getAngle()) != null ? _b : 0); } getWidth() { return this.width; } getOriginCoords() { if (!this.origin) { const orbitOriginCoords = [ this.getCanvasWidth() / 2, this.getCanvasHeight() / 2 ]; return [ orbitOriginCoords[0] + Math.cos(this.angle) * this.distance, orbitOriginCoords[1] + Math.sin(this.angle) * this.distance ]; } else { const orbitOriginCoords = this.origin.getOriginCoords(); return [ orbitOriginCoords[0] + Math.cos(this.origin.getAngle() + this.angle) * (this.distance + this.origin.getWidth()), orbitOriginCoords[1] + Math.sin(this.origin.getAngle() + this.angle) * (this.distance + this.origin.getWidth()) ]; } } } const roundCoords = (coords) => { return [Math.round(coords[0]), Math.round(coords[1])]; }; class Star extends Astre { constructor(_a) { var args = __objRest(_a, []); super(__spreadValues({}, args)); this.draw = () => { this.rotate(); this.ctx.shadowBlur = 0; this.ctx.beginPath(); const orginalCoords = roundCoords(this.getOriginCoords()); this.ctx.arc(...orginalCoords, Math.round(this.width), 0, Math.PI * 2); this.ctx.closePath(); this.ctx.fillStyle = `rgba(${this.rgb[0]}, ${this.rgb[1]}, ${this.rgb[2]}, 1)`; this.ctx.fill(); }; } } const Random = { between: (min, max) => min + Math.random() * (max - min), around: (value, tolerance, unit) => { if (unit === "%") { tolerance = tolerance * value; } return value - tolerance + Math.random() * tolerance * 2; }, positiveOrNegative: () => Math.random() > 0.5 ? 1 : -1, randomizeArray: (array) => { const newArray = array.slice(); for (let i = newArray.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); const temp = newArray[i]; newArray[i] = newArray[j]; newArray[j] = temp; } return newArray; } }; const generateStars = ({ stars, count, color, rotationSpeed, ctx }) => { let totalStars; const missingStars = count - stars.length; if (missingStars <= 0) { totalStars = stars.slice(0, count); } else { const newStars = new Array(missingStars).fill(0).map(() => new Star({ ctx, width: Random.between(0.03, 0.1), distance: 120 * Math.pow(Math.random() * Math.random(), 1 / 2), speed: Random.around(rotationSpeed * 0.015, 5e-3), rgb: parseColor(color) })); totalStars = stars.concat(newStars); } return totalStars.map((star) => { star.speed = Random.around(rotationSpeed * 0.015, 5e-3); return star; }); }; const drawOnCanvas = ({ canvas, drawings, bgColor, fps = 0 }) => { const width = canvas.width; const height = canvas.height; const ctx = canvas.getContext("2d"); if (!ctx) return () => void 0; ctx.save(); let animation; let lastTimestamp = 0; let timeStep = 1e3 / fps; const drawMainCanvas = () => { if (fps) { animation = requestAnimationFrame(drawMainCanvas); const timestamp = Date.now(); if (timestamp - lastTimestamp < timeStep) return; lastTimestamp = timestamp; } ctx.clearRect(0, 0, width, height); if (bgColor) { ctx.fillStyle = bgColor; ctx.fillRect(0, 0, width, height); } drawings.forEach((drawing) => drawing.draw()); }; drawMainCanvas(); return () => { ctx.restore(); if (animation) { cancelAnimationFrame(animation); } }; }; class Sun extends Astre { constructor(_b) { var args = __objRest(_b, []); super(__spreadValues({}, args)); this.draw = () => { this.rotate(); this.ctx.shadowBlur = 0; this.ctx.beginPath(); const orginalCoords = roundCoords(this.getOriginCoords()); this.ctx.arc(...orginalCoords, Math.round(this.width), 0, Math.PI * 2); this.ctx.fillStyle = "white"; this.ctx.fill(); this.ctx.closePath(); this.ctx.beginPath(); this.ctx.arc(...orginalCoords, Math.round(this.width * 5), 0, Math.PI * 2); this.ctx.closePath(); this.ctx.fillStyle = this.getGradiant(); this.ctx.fill(); }; } getGradiant() { const orginalCoords = roundCoords(this.getOriginCoords()); const gradient = this.ctx.createRadialGradient(...orginalCoords, 0, ...orginalCoords, Math.round(this.width * 5)); gradient.addColorStop(0, getRGB(this.rgb, 0.2)); gradient.addColorStop(0.1, getRGB(this.rgb, 0.3)); gradient.addColorStop(0.16, getRGB(this.rgb, 0.6)); gradient.addColorStop(0.2, getRGB(this.rgb, 1)); gradient.addColorStop(0.2, getRGB(this.rgb, 0.4)); gradient.addColorStop(0.23, getRGB(this.rgb, 0.08)); gradient.addColorStop(0.5, getRGB(this.rgb, 0.02)); gradient.addColorStop(0.9, getRGB(this.rgb, 5e-3)); gradient.addColorStop(1, getRGB(this.rgb, 0)); return gradient; } } class Planet extends Astre { constructor(_c) { var args = __objRest(_c, []); super(__spreadValues({}, args)); this.draw = () => { this.rotate(); this.ctx.shadowBlur = 0; this.ctx.beginPath(); const originalCoords = roundCoords(this.getOriginCoords()); this.ctx.arc(...originalCoords, Math.round(this.width), 0, Math.PI * 2); this.ctx.fillStyle = "black"; this.ctx.fill(); this.ctx.closePath(); const gradient = this.ctx.createRadialGradient(Math.round(originalCoords[0] - 0.4 * this.width * Math.cos(this.getRefAngle())), Math.round(originalCoords[1] - 0.4 * this.width * Math.sin(this.getRefAngle())), 0, ...originalCoords, Math.round(this.width)); gradient.addColorStop(0, getRGB(this.rgb, 1)); gradient.addColorStop(1, getRGB(this.rgb, 0.5)); this.ctx.fillStyle = gradient; this.ctx.fill(); }; } } const generateSolarSytem = ({ planets, sunScale, scale, rotationSpeed, distance, ctx }) => { const sun = new Sun({ ctx, width: 3.8 * 0.5 * sunScale, distance: distance / 2, startAngle: 0, speed: 33e-4 * rotationSpeed, rgb: parseColor("rgb(208,141,16)") }); const earth = new Planet({ ctx, width: 0.96 * 0.5 * scale, distance: 10 * 0.5 * scale, speed: 0.01 * rotationSpeed, rgb: parseColor("rgb(19,102,150)"), origin: sun }); return [ sun, new Planet({ ctx, width: 0.3 * 0.5 * scale, distance: 4.2, speed: 0.017 * rotationSpeed, rgb: parseColor("rgb(180, 144, 88)"), origin: sun }), earth, new Planet({ ctx, width: 0.24 * 0.5 * scale, distance: 3.2 * 0.5 * scale, speed: 0.0212 * rotationSpeed, rgb: parseColor("rgb(200, 200, 200)"), origin: earth }), new Planet({ ctx, width: 0.64 * 0.5 * scale, distance: 12.8 * 0.5 * scale, speed: 66e-4 * rotationSpeed, rgb: parseColor("rgb(233, 88, 26)"), origin: sun }), new Planet({ ctx, width: 1.44 * 0.5 * scale, distance: 17.6 * 0.5 * scale, speed: 46e-4 * rotationSpeed, rgb: parseColor("rgb(169, 109, 45)"), origin: sun }), new Planet({ ctx, width: 1.2 * 0.5 * scale, distance: 22 * 0.5 * scale, speed: 4e-3 * rotationSpeed, rgb: parseColor("rgb(164,127,84)"), origin: sun }), new Planet({ ctx, width: 0.76 * 0.5 * scale, distance: 25.2 * 0.5 * scale, speed: 37e-4 * rotationSpeed, rgb: parseColor("rgb(84,149,164)"), origin: sun }), new Planet({ ctx, width: 0.62 * 0.5 * scale, distance: 27.2 * 0.5 * scale, speed: 33e-4 * rotationSpeed, rgb: parseColor("rgb(36,82,154)"), origin: sun }) ]; }; const FPS = 40; const SPEED = 115; class Comet extends Drawable { constructor({ ctx, frequence }) { super({ ctx }); this.speed = SPEED; this.x = 0; this.y = 0; this.opacity = 0; this.draw = () => { this.move(); if (!this.showConfig) return; this.ctx.save(); this.ctx.ellipse(this.x, this.y, this.showConfig.width, 90, this.showConfig.direction + Math.PI / 2, 0, Math.PI * 2); this.ctx.globalAlpha = this.opacity; const gradiant = this.ctx.createRadialGradient(this.x, this.y, 0, this.x, this.y, 90); gradiant.addColorStop(0, getRGB(this.showConfig.rgb, 1)); gradiant.addColorStop(1, getRGB(this.showConfig.rgb, 0)); this.ctx.fillStyle = gradiant; this.ctx.fill(); this.ctx.restore(); }; this.ctx = ctx; this.frequence = frequence; } move() { if (this.showConfig) { this.x += this.speed * Math.cos(this.showConfig.direction); this.y += this.speed * Math.sin(this.showConfig.direction); const { x: startX, y: startY } = this.showConfig.startCoords; const distance = Math.sqrt(Math.pow(this.x - startX, 2) + Math.pow(this.y - startY, 2)); const showAvancement = distance / this.showConfig.distanceToTarget; this.opacity = Math.max(0.7, Math.min(showAvancement < 0.3 ? showAvancement : 1 - showAvancement, 1)); if (distance > this.showConfig.distanceToTarget) { this.showConfig = void 0; } return; } const shouldCreateANewShow = Math.random() > 1 - this.frequence / 100 / FPS; if (shouldCreateANewShow) { const fromAngle = Random.between(0, 2 * Math.PI); const maxSideSize = Math.max(this.getCanvasHeight(), this.getCanvasWidth()); this.showConfig = { startCoords: { x: Random.around(Math.cos(fromAngle) * maxSideSize / 3, 0.5, "%") + this.getCanvasWidth() / 2, y: Random.around(Math.sin(fromAngle) * maxSideSize / 3, 0.5, "%") + this.getCanvasHeight() / 2 }, direction: Random.between(fromAngle + Math.PI - Math.PI / 6, fromAngle + Math.PI + Math.PI / 6), distanceToTarget: Random.around(maxSideSize * 0.6, 0.3), speed: Random.around(SPEED, 0.15, "%"), rgb: parseColor("rgb(255,207,207)"), width: Random.between(0.2, 0.8), startOpacity: 0 }; this.x = this.showConfig.startCoords.x; this.y = this.showConfig.startCoords.y; } } } const generateComet = ({ ctx, frequence }) => { return new Array(1).fill(0).flatMap(() => { return [new Comet({ ctx, frequence })]; }); }; const INTENSITY_MULTIPLE = 0.025; const ITERATION_PER_COLOR = 3; const COLORS = ["rgb(6,2,122)", "rgb(6,66,18)", "#57046e"]; class NebulaColoration extends Drawable { constructor({ ctx, intensity }) { super({ ctx }); this.draw = () => { const imageData = this.ctx.getImageData(0, 0, this.getCanvasWidth(), this.getCanvasHeight()); const canvasWidth = this.getCanvasWidth(); const data = Array.from(imageData.data); for (let i = 0; i < data.length; i = i + 4) { const pixelIndex = i / 4; const x = pixelIndex % canvasWidth; const y = Math.floor(pixelIndex / canvasWidth); this.colorations.forEach((coloration) => { const opacity = getColorationOpacity(coloration, x, y) * this.intensity; if (opacity > 0) { for (let channel = 0; channel < 3; channel++) { data[i + channel] = opacity * coloration.rgb[channel] + data[channel + i] * (1 - opacity); } } }); for (let channel = 0; channel < 3; channel++) { const value = data[i + channel]; if (value > 0) { data[i + channel] = Math.round(value - 1 + Math.random()); } } } imageData.data.set(data); this.ctx.putImageData(imageData, 0, 0); }; this.intensity = intensity * INTENSITY_MULTIPLE; const grid = getGrid(COLORS.length * ITERATION_PER_COLOR); this.colorations = COLORS.flatMap((color) => { return new Array(ITERATION_PER_COLOR).fill(0).map(() => { const gridItem = grid.pop(); return { coords: { x: gridItem.x * this.getCanvasWidth(), y: gridItem.y * this.getCanvasHeight() }, rgb: parseColor(color), ratio: Random.around(Math.PI / 4, 0.2), width: Random.between(5, 8) * this.canvasMinSide * 0.08 }; }); }); } setIntensity(intensity) { this.intensity = intensity * INTENSITY_MULTIPLE; } } const getGrid = (length) => { const startAngle = Math.PI * 2 * Math.random(); const coords = new Array(length).fill(0).map((v, i) => { const angle = startAngle + Random.around(i * Math.PI * 2 / length, 0.32); const rayon = Random.between(0.8, 1.1); return { x: (Math.cos(angle) * rayon + 1) / 2, y: (Math.sin(angle) * rayon + 1) / 2 }; }); return Random.randomizeArray(coords); }; const getColorationOpacity = (coloration, x, y) => { const xDistance = x - coloration.coords.x; const yDistance = y - coloration.coords.y; const distanceToNebula = Math.sqrt(Math.pow(xDistance * Math.cos(coloration.ratio), 2) + Math.pow(yDistance * Math.sin(coloration.ratio), 2)); const relativeDistanceToNebula = (coloration.width - distanceToNebula) / coloration.width; return Math.max(relativeDistanceToNebula, 0); }; const generateNebulaColoration = ({ ctx, coloration, intensity }) => { if (coloration) { coloration.setIntensity(intensity); return coloration; } return new NebulaColoration({ ctx, intensity }); }; const CANVAS_STYLE = "width: 100%;height: 100%;position:absolute;will-change:transform;top: 0;left:0;"; class Nebula { constructor({ config, element }) { this.cancelAnimations = []; this.stars = []; this.comets = []; this.planets = []; this.onResize = () => { this.styleCanvas(); this.init(); }; this.styleCanvas = () => { this.bgCanvas.setAttribute("style", CANVAS_STYLE); this.bgCanvas.width = this.element.offsetWidth / 3; this.bgCanvas.height = this.element.offsetHeight / 3; this.canvas.setAttribute("style", CANVAS_STYLE); this.canvas.width = this.element.offsetWidth * 2; this.canvas.height = this.element.offsetHeight * 2; }; this.element = element; this.bgCanvas = document.createElement("CANVAS"); this.canvas = document.createElement("CANVAS"); element.append(this.bgCanvas); element.append(this.canvas); this.styleCanvas(); window.addEventListener("resize", this.onResize); this.config = fillConfig(config); this.setConfig(config); } setConfig(config) { this.config = fillConfig(config); this.coloration = generateNebulaColoration({ coloration: this.coloration, ctx: this.bgCanvas.getContext("2d"), intensity: this.config.nebulasIntensity }); this.stars = generateStars({ stars: this.stars, ctx: this.canvas.getContext("2d"), color: this.config.starsColor, count: this.config.starsCount, rotationSpeed: this.config.starsRotationSpeed }); this.planets = generateSolarSytem({ planets: this.planets, sunScale: this.config.sunScale, scale: this.config.planetsScale, ctx: this.canvas.getContext("2d"), rotationSpeed: this.config.solarSystemSpeedOrbit, distance: this.config.solarSystemOrbite }); this.comets = generateComet({ ctx: this.canvas.getContext("2d"), frequence: this.config.cometFrequence }); this.init(); } init() { this.draw(); } draw() { this.cancelAnimations.forEach((callback) => callback()); this.cancelAnimations = [ drawOnCanvas({ canvas: this.bgCanvas, drawings: [this.coloration], bgColor: this.config.bgColor }), drawOnCanvas({ canvas: this.canvas, drawings: [...this.stars, ...this.comets, ...this.planets], fps: FPS }) ]; } destroy() { var _a, _b; window.removeEventListener("resize", this.onResize); this.cancelAnimations.forEach((callback) => callback()); this.cancelAnimations = []; (_a = this.bgCanvas.parentElement) == null ? void 0 : _a.removeChild(this.bgCanvas); (_b = this.canvas.parentElement) == null ? void 0 : _b.removeChild(this.canvas); } } const ReactNebula = ({ config = {} }) => { const nebulaRef = useRef(); const wrapperRef = useRef(null); useLayoutEffect(() => { var _a; if (nebulaRef.current) { (_a = nebulaRef.current) == null ? void 0 : _a.setConfig(config); } }, [config]); useLayoutEffect(() => { if (!nebulaRef.current) { nebulaRef.current = new Nebula({ config, element: wrapperRef.current }); } return () => { var _a; (_a = nebulaRef.current) == null ? void 0 : _a.destroy(); nebulaRef.current = void 0; }; }, []); return /* @__PURE__ */ React.createElement("div", { ref: wrapperRef, style: { overflow: "hidden", background: "#0a1929", height: "100%", width: "100%", position: "absolute" } }); }; const createNebula = (element, config) => { return new Nebula({ config, element }); }; export { Nebula, ReactNebula, createNebula };