Posted: 18th Oct 2021 17:14
I do mostly JS development these days and came across this neat particle effect library and wondered how easy it would be to recreate. Staring at it for a minute it just looks like it matches to a nearest neighbor (kind of like voronoi tessellation). So here's my lunch break snippet. It'd look nicer with anti-aliasing. I suppose I could do this with sprites, wouldn't be too difficult.


+ Code Snippet
#CONSTANT SCREEN_WIDTH = 1280
#CONSTANT SCREEN_HEIGHT = 720
#CONSTANT PARTICLE_COUNT = 100

SetWindowSize(SCREEN_WIDTH, SCREEN_HEIGHT, 0 )
setVirtualResolution(SCREEN_WIDTH, SCREEN_HEIGHT)
 

 
// Coin properties
Type ParticleUDT
    x as float
    y as float
    dx as float
    dy as float
    alpha as float
EndType
 
// Coin array
particles as ParticleUDT[PARTICLE_COUNT]
 
for i = 0 to PARTICLE_COUNT-1
	particles[i].x = random(0, SCREEN_WIDTH)
	particles[i].y = random(0, SCREEN_HEIGHT)
	a = random(0, 360)
	particles[i].dx = cos(a)
	particles[i].dy = sin(a)
next i


// particle movement speed
speed as float = 0.4


// Foreground and background colors need components specified
// to mimic alpha blending through linear interpolation of the
// two colors

// background color
br = 3
bg = 1
bb = 38

// foreground color
fr = 143
fg = 141
fb = 184

c = makeColor(fr, fg, fb)
setClearColor(br, bg, bb)




repeat


	// Loop through all coins
	for i = 0 to PARTICLE_COUNT-1
		
		particles[i].x = particles[i].x + particles[i].dx*speed
		particles[i].y = particles[i].y + particles[i].dy*speed
		
		drawEllipse(particles[i].x, particles[i].y, 2, 2, c, c, 1)
		
		
		// Connect particles to neighbors
		for j = 0 to PARTICLE_COUNT-1
			if i <> j
				
				// Join particles if within 150px
				d = (particles[i].x - particles[j].x) ^ 2 + (particles[i].y - particles[j].y)^2
				if d <= 22500
					
					// Fade the line between particles. 
					// Gets brighter the closer they are.
					t# = d / 22500.0
					if t# > 1 then t# = 1
					if t# < 0 then t# = 0
						
					r = fr + (br-fr)*t#
					g = fg + (bg-fg)*t#
					b = fb + (bb-fb)*t#
					ac = makeColor(r, g, b)

					drawLine(particles[i].x, particles[i].y, particles[j].x, particles[j].y, ac, ac)
				endif
			endif
		next j
		
		
		// Screen boundaries
		if particles[i].x < 0 or particles[i].x > SCREEN_WIDTH or particles[i].y < 0 or particles[i].y > SCREEN_HEIGHT
			particles[i].x = random(0, 1) * SCREEN_WIDTH
			particles[i].y = random(0, 1) * SCREEN_HEIGHT
			a = random(0, 360)
			particles[i].dx = cos(a)
			particles[i].dy = sin(a)
			
		endif
		
	next i

 
    sync()
until getRawKeyPressed(27) = 1  // Press ESC to exit
end
 
 
Posted: 18th Oct 2021 18:19
purdy

kinda like watching constellations "dance"
Posted: 19th Oct 2021 4:55
It looks like a screensaver. A bit hypnotic, I think.
I wonder what it would look like if the individual particles had mass and attracted each other accordingly. .
If two particles collide, the masses should add up.
Would it then be a simulation of the genesis? (just kidding)
Posted: 19th Oct 2021 18:47
I like the idea of weighted particles. Bigger ones could reach farther.

It occurred to me that this actually draws twice as many lines as necessary, because each of the connecting particles will draw a line to the other. So A draws to B but B will also see A within range and draw another line. Since there's no telling how many lines will be drawn from a particle I don't see an easy way to fix this short of having an array of children for each particle. But then trying to look up each particle to see if it's a child of another already might be even less efficient.
Posted: 21st Oct 2021 5:07
It occurred to me that this actually draws twice as many lines as necessary, because each of the connecting particles will draw a line to the other. So A draws to B but B will also see A within range and draw another line. Since there's no telling how many lines will be drawn from a particle I don't see an easy way to fix this short of having an array of children for each particle. But then trying to look up each particle to see if it's a child of another already might be even less efficient.


I think I have the solution here, or at least an approach.
I have changed your code a little bit. So that now the determining and drawing of the lines happens recursively in a function.
I don't know if the result is the same. But it saves about half of the lines.
Original version around just over 600 lines.
Recursive version around 320 lines.

+ Code Snippet
#CONSTANT SCREEN_WIDTH = 1280
#CONSTANT SCREEN_HEIGHT = 720
#CONSTANT PARTICLE_COUNT = 100

SetWindowSize(SCREEN_WIDTH, SCREEN_HEIGHT, 0 )
setVirtualResolution(SCREEN_WIDTH, SCREEN_HEIGHT)
 

 
// Coin properties
Type ParticleUDT
    x as float
    y as float
    dx as float
    dy as float
    alpha as float
    
    // processing flag
    proc as integer
EndType
 
// Coin array
global particles as ParticleUDT[PARTICLE_COUNT]
 
for i = 0 to PARTICLE_COUNT-1
	particles[i].x = random(0, SCREEN_WIDTH)
	particles[i].y = random(0, SCREEN_HEIGHT)
	a = random(0, 360)
	particles[i].dx = cos(a)
	particles[i].dy = sin(a)
next i


// particle movement speed
speed as float = 0.4
// line count
global lcount as integer = 0
// global processing flag
global proc_flag = 1

// Foreground and background colors need components specified
// to mimic alpha blending through linear interpolation of the
// two colors

// background color
global br = 3
global bg = 1
global bb = 38

// foreground color
global fr = 143
global fg = 141
global fb = 184

global c
c = makeColor(fr, fg, fb)
setClearColor(br, bg, bb)




repeat
	// reset line count
	lcount = 0

	// Loop through all coins
	for i = 0 to PARTICLE_COUNT-1
		
		particles[i].x = particles[i].x + particles[i].dx*speed
		particles[i].y = particles[i].y + particles[i].dy*speed
		
		drawEllipse(particles[i].x, particles[i].y, 2, 2, c, c, 1)
		
		// If the particle has not been processed, it must be processed.
		if particles[i].proc <> proc_flag then connect_particles(particles[i])
		
/*		// Connect particles to neighbors
		for j = 0 to PARTICLE_COUNT-1
			if i <> j
				
				// Join particles if within 150px
				d = (particles[i].x - particles[j].x) ^ 2 + (particles[i].y - particles[j].y)^2
				if d <= 22500
					
					// Fade the line between particles. 
					// Gets brighter the closer they are.
					t# = d / 22500.0
					if t# > 1 then t# = 1
					if t# < 0 then t# = 0
						
					r = fr + (br-fr)*t#
					g = fg + (bg-fg)*t#
					b = fb + (bb-fb)*t#
					ac = makeColor(r, g, b)

					drawLine(particles[i].x, particles[i].y, particles[j].x, particles[j].y, ac, ac)
					inc lcount
				endif
			endif
		next j
*/		
		
		// Screen boundaries
		if particles[i].x < 0 or particles[i].x > SCREEN_WIDTH or particles[i].y < 0 or particles[i].y > SCREEN_HEIGHT
			particles[i].x = random(0, 1) * SCREEN_WIDTH
			particles[i].y = random(0, 1) * SCREEN_HEIGHT
			a = random(0, 360)
			particles[i].dx = cos(a)
			particles[i].dy = sin(a)
			
		endif
		
	next i

	print("Lines:Particles = "+str(lcount)+":"+str(PARTICLE_COUNT))
	
	// reverse the processing flag, to not reset the entire array of particles.
	proc_flag = 1-proc_flag
    sync()
until getRawKeyPressed(27) = 1  // Press ESC to exit
end
 
 
function connect_particles(p ref as ParticleUDT)
	// set processing flag to prevent endless loop
	p.proc = proc_flag
	for j = 0 to PARTICLE_COUNT-1
		// If the particle has already been processed, continue with the next one.
		if particles[j].proc = proc_flag then continue
		
		// prevent draw line to it self or to particles on the same place
		if p.x <> particles[j].x OR p.y <> particles[j].y
			
			// Join particles if within 150px
			d = (p.x - particles[j].x) ^ 2 + (p.y - particles[j].y)^2
			if d <= 22500
				
				// Fade the line between particles. 
				// Gets brighter the closer they are.
				t# = d / 22500.0
				if t# > 1 then t# = 1
				if t# < 0 then t# = 0
					
				r = fr + (br-fr)*t#
				g = fg + (bg-fg)*t#
				b = fb + (bb-fb)*t#
				ac = makeColor(r, g, b)

				drawLine(p.x, p.y, particles[j].x, particles[j].y, ac, ac)
				// increment the line counter
				inc lcount
			endif
		endif
	next j
endfunction


EDIT: OOps, The function is not called recursively at all. But it still saves lines.
Posted: 24th Oct 2021 17:55
Nice. On mine it maxed at about 180fps, yours hit around 240. It might be higher, but right now I'm really taxing my cpu with a multi-threaded decryption program. I was actually thinking about using a flag last night as a possible solution. At first glance I'm still not sure how it works lol.
Posted: 25th Oct 2021 21:34
Hehe, I just noticed that you only have to change two lines in your code. then it's faster than mine.

Change line 59 to
+ Code Snippet
for i = 0 to PARTICLE_COUNT-2
(not absolutely necessary)

Change line 68 to
+ Code Snippet
for j = i+1 to PARTICLE_COUNT-1
Posted: 28th Oct 2021 5:15
Ahhh, you sir are a genius! It seems so obvious now. Now I peak at 320 fps. (my cpu is also taxed at 90% for 5 days straight now running decryption) Oops, back down to 280fps after making some other changes.

Particles will grow after colliding, the smaller one is destroyed. Each particle contains its own color. I've also attempted to fade the particles out as they near the edge of the screen, but there still seems to be some flicker right as they go off the edge.



+ Code Snippet
REM ***********************************************
REM Title: Gamespace
REM Author: Phaelax
REM Downloaded from: http://dbcc.zimnox.com/
REM ***********************************************

#CONSTANT SCREEN_WIDTH = 1280
#CONSTANT SCREEN_HEIGHT = 720
#CONSTANT PARTICLE_COUNT = 100
#CONSTANT BOUNDARY_THRESHOLD = 50.0

SetWindowSize(SCREEN_WIDTH, SCREEN_HEIGHT, 0 )
setVirtualResolution(SCREEN_WIDTH, SCREEN_HEIGHT)
SetSyncRate(0, 0)

 
// Coin properties
Type ParticleUDT
    x as float
    y as float
    dx as float
    dy as float
    alpha as float
    speed as float
    size as float
    process as integer
    baseColor as integer
    color as integer
EndType
 
// particle array
global particles as ParticleUDT[PARTICLE_COUNT]
 
for i = 0 to PARTICLE_COUNT-1
	particles[i].x = random(0, SCREEN_WIDTH)
	particles[i].y = random(0, SCREEN_HEIGHT)
	a = random(0, 360)
	particles[i].dx = cos(a)
	particles[i].dy = sin(a)
	particles[i].speed = random(1,6) / 10.0
	particles[i].size = 2
	//particles[i].baseColor = makeColor(random(64,255),random(64,255),random(64,255))
	if random(0,1) = 1
		particles[i].baseColor = makeColor(216,9,219)
	else
		particles[i].baseColor = makeColor(5,151,230)
	endif
next i


// particle movement speed
speed as float = 0.4


// Foreground and background colors need components specified
// to mimic alpha blending through linear interpolation of the
// two colors

// background color
br = 3
bg = 1
bb = 38


setClearColor(br, bg, bb)

white = makeColor(255,255,255)


repeat
	lineCount = 0

	// Loop through all coins
	for i = 0 to PARTICLE_COUNT-2
		
		particles[i].x = particles[i].x + particles[i].dx*particles[i].speed
		particles[i].y = particles[i].y + particles[i].dy*particles[i].speed
		
		
		// If particle is near edge of screen, fade it out
		w# = 1
		if particles[i].x <= BOUNDARY_THRESHOLD then w# = 1.0 - particles[i].x / BOUNDARY_THRESHOLD
		if particles[i].x >= SCREEN_WIDTH - BOUNDARY_THRESHOLD then w# = (particles[i].x - (SCREEN_WIDTH - BOUNDARY_THRESHOLD)) / BOUNDARY_THRESHOLD
		
		if particles[i].y <= BOUNDARY_THRESHOLD then w#  = min(w#, 1.0 - particles[i].y / BOUNDARY_THRESHOLD)
		if particles[i].y >= SCREEN_HEIGHT - BOUNDARY_THRESHOLD then w# = min(w#, (particles[i].y - (SCREEN_HEIGHT - BOUNDARY_THRESHOLD)) / BOUNDARY_THRESHOLD)
		
		if w# < 0 then w# = 0

		if w# < 1

			pr = GetColorRed(particles[i].baseColor)
			pg = GetColorGreen(particles[i].baseColor)
			pb = GetColorBlue(particles[i].baseColor)
		
			r = pr + (br-pr)*w#
			g = pg + (bg-pg)*w#
			b = pb + (bb-pb)*w#
			particles[i].color = makeColor(r,g,b)
		else
			particles[i].color = particles[i].baseColor
		endif
		
		
		drawEllipse(particles[i].x, particles[i].y, particles[i].size, particles[i].size, particles[i].color, particles[i].color, 1)
		
		if getRawKeyState(32) = 1 then drawEllipse(particles[i].x, particles[i].y, particles[i].size, particles[i].size, white, white, 0)
		
		
		killParticle = 0
		// Connect particles to neighbors
		for j = i+1 to PARTICLE_COUNT-1
			if i <> j and particles[j].process <> processFlag
				
				// Join particles if within 150px
				d = (particles[i].x - particles[j].x) ^ 2 + (particles[i].y - particles[j].y)^2
				if d <= 22500
					
					// Merge particles if collide
					if d <= (particles[i].size + particles[j].size)^2
						
						if particles[i].size > particles[j].size
							particles[i].size = particles[i].size + 2
							killParticle(particles[j])
						else
							particles[j].size = particles[j].size + 2
							killParticle(particles[i])
						endif
						
						//exit
					else
						// Fade the line between particles. 
						// Gets brighter the closer they are.
						t# = d / 22500.0
						if t# > 1 then t# = 1
						if t# < 0 then t# = 0
						
						drawParticleJoint(particles[i], particles[j], t#, br, bg, bb)

						inc lineCount
					
					endif
					
					
				endif
				
			endif
		next j
		
		particles[i].process = processFlag
		
		if (particles[i].x < 0 or particles[i].x > SCREEN_WIDTH or particles[i].y < 0 or particles[i].y > SCREEN_HEIGHT)
			killParticle(particles[i])
		endif

	next i

	processFlag = 1 - processFlag

	print(screenfps())
	//print("Lines: "+str(lineCount))
 
    sync()
until getRawKeyPressed(27) = 1  // Press ESC to exit
end




function killParticle(p ref as ParticleUDT)
	// If 1, create particle on left or right edge of screen
	if random(0,1) = 1
		p.x = random(0, 1) * SCREEN_WIDTH
		p.y = random(1, SCREEN_HEIGHT)
	else 
		// create particle on top or bottom of screen
		p.x = random(1, SCREEN_WIDTH)
		p.y = random(0, 1) * SCREEN_HEIGHT
	endif	
	a = random(0, 360)
	p.dx = cos(a)
	p.dy = sin(a)
	p.speed = random(1,6) / 10.0
	p.size = 2
	//p.baseColor = makeColor(random(64,255),random(64,255),random(64,255))
	
endfunction




function min(a as float, b as float)
	if a < b then exitfunction a
endfunction b




function drawParticleJoint(ap as ParticleUDT, bp as ParticleUDT, t as float, br as integer, bg as integer, bb as integer)
	
	ir = GetColorRed(ap.color)
	ig = GetColorGreen(ap.color)
	ib = GetColorBlue(ap.color)

	jr = GetColorRed(bp.color)
	jg = GetColorGreen(bp.color)
	jb = GetColorBlue(bp.color)
		
	iar = ir + (br-ir)*t
	iag = ig + (bg-ig)*t
	iab = ib + (bb-ib)*t
	
	jar = jr + (br-jr)*t
	jag = jg + (bg-jg)*t
	jab = jb + (bb-jb)*t
	
	
	ic = makeColor(iar, iag, iab)
	jc = makeColor(jar, jag, jab)

	drawLine(ap.x, ap.y, bp.x, bp.y, ic, jc)
endfunction