1+ import * as PIXI from "https://cdn.skypack.dev/pixi.js" ;
2+ import { KawaseBlurFilter } from "https://cdn.skypack.dev/@pixi/filter-kawase-blur" ;
3+ import SimplexNoise from "https://cdn.skypack.dev/simplex-noise" ;
4+ import hsl from "https://cdn.skypack.dev/hsl-to-hex" ;
5+ import debounce from "https://cdn.skypack.dev/debounce" ;
6+
7+ // return a random number within a range
8+ function random ( min , max ) {
9+ return Math . random ( ) * ( max - min ) + min ;
10+ }
11+
12+ // map a number from 1 range to another
13+ function map ( n , start1 , end1 , start2 , end2 ) {
14+ return ( ( n - start1 ) / ( end1 - start1 ) ) * ( end2 - start2 ) + start2 ;
15+ }
16+
17+ // Create a new simplex noise instance
18+ const simplex = new SimplexNoise ( ) ;
19+
20+ // ColorPalette class
21+ class ColorPalette {
22+ constructor ( ) {
23+ this . setColors ( ) ;
24+ this . setCustomProperties ( ) ;
25+ }
26+
27+ setColors ( ) {
28+ // pick a random hue somewhere between 220 and 360
29+ this . hue = ~ ~ random ( 220 , 360 ) ;
30+ this . complimentaryHue1 = this . hue + 30 ;
31+ this . complimentaryHue2 = this . hue + 60 ;
32+ // define a fixed saturation and lightness
33+ this . saturation = 95 ;
34+ this . lightness = 50 ;
35+
36+ // define a base color
37+ this . baseColor = hsl ( this . hue , this . saturation , this . lightness ) ;
38+ // define a complimentary color, 30 degress away from the base
39+ this . complimentaryColor1 = hsl (
40+ this . complimentaryHue1 ,
41+ this . saturation ,
42+ this . lightness
43+ ) ;
44+ // define a second complimentary color, 60 degrees away from the base
45+ this . complimentaryColor2 = hsl (
46+ this . complimentaryHue2 ,
47+ this . saturation ,
48+ this . lightness
49+ ) ;
50+
51+ // store the color choices in an array so that a random one can be picked later
52+ this . colorChoices = [
53+ this . baseColor ,
54+ this . complimentaryColor1 ,
55+ this . complimentaryColor2
56+ ] ;
57+ }
58+
59+ randomColor ( ) {
60+ // pick a random color
61+ return this . colorChoices [ ~ ~ random ( 0 , this . colorChoices . length ) ] . replace (
62+ "#" ,
63+ "0x"
64+ ) ;
65+ }
66+
67+ setCustomProperties ( ) {
68+ // set CSS custom properties so that the colors defined here can be used throughout the UI
69+ document . documentElement . style . setProperty ( "--hue" , this . hue ) ;
70+ document . documentElement . style . setProperty (
71+ "--hue-complimentary1" ,
72+ this . complimentaryHue1
73+ ) ;
74+ document . documentElement . style . setProperty (
75+ "--hue-complimentary2" ,
76+ this . complimentaryHue2
77+ ) ;
78+ }
79+ }
80+
81+ // Orb class
82+ class Orb {
83+ // Pixi takes hex colors as hexidecimal literals (0x rather than a string with '#')
84+ constructor ( fill = 0x000000 ) {
85+ // bounds = the area an orb is "allowed" to move within
86+ this . bounds = this . setBounds ( ) ;
87+ // initialise the orb's { x, y } values to a random point within it's bounds
88+ this . x = random ( this . bounds [ "x" ] . min , this . bounds [ "x" ] . max ) ;
89+ this . y = random ( this . bounds [ "y" ] . min , this . bounds [ "y" ] . max ) ;
90+
91+ // how large the orb is vs it's original radius (this will modulate over time)
92+ this . scale = 1 ;
93+
94+ // what color is the orb?
95+ this . fill = fill ;
96+
97+ // the original radius of the orb, set relative to window height
98+ this . radius = random ( window . innerHeight / 6 , window . innerHeight / 3 ) ;
99+
100+ // starting points in "time" for the noise/self similar random values
101+ this . xOff = random ( 0 , 1000 ) ;
102+ this . yOff = random ( 0 , 1000 ) ;
103+ // how quickly the noise/self similar random values step through time
104+ this . inc = 0.002 ;
105+
106+ // PIXI.Graphics is used to draw 2d primitives (in this case a circle) to the canvas
107+ this . graphics = new PIXI . Graphics ( ) ;
108+ this . graphics . alpha = 0.825 ;
109+
110+ // 250ms after the last window resize event, recalculate orb positions.
111+ window . addEventListener (
112+ "resize" ,
113+ debounce ( ( ) => {
114+ this . bounds = this . setBounds ( ) ;
115+ } , 250 )
116+ ) ;
117+ }
118+
119+ setBounds ( ) {
120+ // how far from the { x, y } origin can each orb move
121+ const maxDist =
122+ window . innerWidth < 1000 ? window . innerWidth / 3 : window . innerWidth / 5 ;
123+ // the { x, y } origin for each orb (the bottom right of the screen)
124+ const originX = window . innerWidth / 1.25 ;
125+ const originY =
126+ window . innerWidth < 1000
127+ ? window . innerHeight
128+ : window . innerHeight / 1.375 ;
129+
130+ // allow each orb to move x distance away from it's x / y origin
131+ return {
132+ x : {
133+ min : originX - maxDist ,
134+ max : originX + maxDist
135+ } ,
136+ y : {
137+ min : originY - maxDist ,
138+ max : originY + maxDist
139+ }
140+ } ;
141+ }
142+
143+ update ( ) {
144+ // self similar "psuedo-random" or noise values at a given point in "time"
145+ const xNoise = simplex . noise2D ( this . xOff , this . xOff ) ;
146+ const yNoise = simplex . noise2D ( this . yOff , this . yOff ) ;
147+ const scaleNoise = simplex . noise2D ( this . xOff , this . yOff ) ;
148+
149+ // map the xNoise/yNoise values (between -1 and 1) to a point within the orb's bounds
150+ this . x = map ( xNoise , - 1 , 1 , this . bounds [ "x" ] . min , this . bounds [ "x" ] . max ) ;
151+ this . y = map ( yNoise , - 1 , 1 , this . bounds [ "y" ] . min , this . bounds [ "y" ] . max ) ;
152+ // map scaleNoise (between -1 and 1) to a scale value somewhere between half of the orb's original size, and 100% of it's original size
153+ this . scale = map ( scaleNoise , - 1 , 1 , 0.5 , 1 ) ;
154+
155+ // step through "time"
156+ this . xOff += this . inc ;
157+ this . yOff += this . inc ;
158+ }
159+
160+ render ( ) {
161+ // update the PIXI.Graphics position and scale values
162+ this . graphics . x = this . x ;
163+ this . graphics . y = this . y ;
164+ this . graphics . scale . set ( this . scale ) ;
165+
166+ // clear anything currently drawn to graphics
167+ this . graphics . clear ( ) ;
168+
169+ // tell graphics to fill any shapes drawn after this with the orb's fill color
170+ this . graphics . beginFill ( this . fill ) ;
171+ // draw a circle at { 0, 0 } with it's size set by this.radius
172+ this . graphics . drawCircle ( 0 , 0 , this . radius ) ;
173+ // let graphics know we won't be filling in any more shapes
174+ this . graphics . endFill ( ) ;
175+ }
176+ }
177+
178+ // Create PixiJS app
179+ const app = new PIXI . Application ( {
180+ // render to <canvas class="orb-canvas"></canvas>
181+ view : document . querySelector ( ".orb-canvas" ) ,
182+ // auto adjust size to fit the current window
183+ resizeTo : window ,
184+ // transparent background, we will be creating a gradient background later using CSS
185+ transparent : true
186+ } ) ;
187+
188+ // Create colour palette
189+ const colorPalette = new ColorPalette ( ) ;
190+
191+ app . stage . filters = [ new KawaseBlurFilter ( 30 , 10 , true ) ] ;
192+
193+ // Create orbs
194+ const orbs = [ ] ;
195+
196+ for ( let i = 0 ; i < 10 ; i ++ ) {
197+ const orb = new Orb ( colorPalette . randomColor ( ) ) ;
198+
199+ app . stage . addChild ( orb . graphics ) ;
200+
201+ orbs . push ( orb ) ;
202+ }
203+
204+ // Animate!
205+ if ( ! window . matchMedia ( "(prefers-reduced-motion: reduce)" ) . matches ) {
206+ app . ticker . add ( ( ) => {
207+ orbs . forEach ( ( orb ) => {
208+ orb . update ( ) ;
209+ orb . render ( ) ;
210+ } ) ;
211+ } ) ;
212+ } else {
213+ orbs . forEach ( ( orb ) => {
214+ orb . update ( ) ;
215+ orb . render ( ) ;
216+ } ) ;
217+ }
218+
219+ document
220+ . querySelector ( ".overlay__btn--colors" )
221+ . addEventListener ( "click" , ( ) => {
222+ colorPalette . setColors ( ) ;
223+ colorPalette . setCustomProperties ( ) ;
224+
225+ orbs . forEach ( ( orb ) => {
226+ orb . fill = colorPalette . randomColor ( ) ;
227+ } ) ;
228+ } ) ;
0 commit comments