├── README.md ├── demo └── tool-overview.gif └── sketch_atomic_clusters_explorer ├── atomsui.pde ├── attractors_manager.pde ├── config.pde ├── data ├── PTSerif-Regular.ttf ├── RobotoMono-Regular.ttf ├── atoms.png ├── controls.png ├── keyboard.png └── pause.png ├── pan_zoom_manager.pde ├── panel_atoms.pde ├── panel_charges.pde ├── panel_keyboard.pde ├── panel_slider.pde ├── particle.pde ├── particle_attractors.pde ├── particles_manager.pde ├── sketch_atomic_clusters_explorer.pde ├── toolbar.pde └── utils.pde /README.md: -------------------------------------------------------------------------------- 1 | # Atomic Clusters explorer [Processing] 2 | 3 | *This project was released under the MIT License* 4 | 5 | * Instagram: [https://instagram.com/ciphrd](https://instagram.com/ciphrd) 6 | * Blog: [https://ciphered.xyz](https://ciphered.xyz) 7 | * Atomic Clusters article: [https://ciphered.xyz/2020/06/01/atomic-clusters,-a-molecular-particle-based-simulation/](https://ciphered.xyz/2020/06/01/atomic-clusters,-a-molecular-particle-based-simulation/) 8 | 9 | Atomic Clusters is a particle-based simulation inspired by the work **Clusters** from *Jeffrey Ventrella*. I wrote [an article](https://ciphered.xyz/2020/06/01/atomic-clusters,-a-molecular-particle-based-simulation/) to describe both the system and its implementation on my blog. Feel free to check it out if you want an insight on this system. 10 | 11 | ![Atomic Clusters explorer demo](demo/tool-overview.gif) 12 | 13 | This simulation is an attraction-repulsion system, where particles can have [0; 4] attractors. Attractors of the same color are attracted, and repelled by attractors of some different colors. These forces generates torque on the particles, creating the angular rotation we can observe. 14 | 15 | This tool was made to explore Atomic Clusters with more precision than just throwing random particles all arround. It is still under development and the code is not 100% clean. Like the UI architecture is garbage, but it was enough for this first version. There is also a lot of room for optimisation, especially when it comes to the particles interactions. This is my first project using Processing so if you want to improve this tool feel free to contribute. I made a quick video to explain how the tool works: 16 | 17 | [![Link to the presentation of the tool](https://img.youtube.com/vi/2viKdYow9LM/0.jpg)](https://www.youtube.com/watch?v=2viKdYow9LM) 18 | 19 | ## How to run 20 | 21 | * [Download Processing](https://processing.org/download/) and install it 22 | * Clone this repo 23 | * Open the `sketch_atomic_clusters_explorer.pde` file with Processing 24 | * Run 25 | 26 | ## How to use 27 | 28 | * You can interact with the system by adding, deleting, rotating and moving particles 29 | * Adjust the simulation settings on the bottom right 30 | * **The list of controls can be seen by clicking on the keyboard icon of the bottom left toolbar** 31 | * New atoms can be created and added to the toolbar using available subatomic charges (colors) 32 | * The number of different subatomic charges can be modified by clicking on the top-right icon. 33 | 34 | ## Exploration of Atomic Clusters with more particles 35 | 36 | I explored this system using compute shaders to have more particles, I posted some of my results on my instagram: [@ciphrd](https://instagram.com/ciphrd) 37 | 38 | ## Todo 39 | 40 | * refacto of some components - **required** before next update 41 | * optimization of particle-particle interactions 42 | * add interaction tools (such as mouse repelling particles, random particle spawn, eraser?) 43 | * selected particle should have more data displayed + a way to modify its settings -------------------------------------------------------------------------------- /demo/tool-overview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciphrd/AtomicClustersExplorer/b5300eb7e59af26a895f5df52842edaee951a98e/demo/tool-overview.gif -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/atomsui.pde: -------------------------------------------------------------------------------- 1 | /** 2 | * The AtomsUI can be used to display atoms on an interface 3 | **/ 4 | 5 | int ATOMS_BAR_HEIGHT = 80; 6 | 7 | class AtomsUI { 8 | // the attractors list to display 9 | Particle[] particles; 10 | public int selected; 11 | int hovered; 12 | boolean subatomHovered; 13 | 14 | public AtomsUI () { 15 | updateDisplayParticles(); 16 | selected = 0; 17 | } 18 | 19 | public void updateDisplayParticles () { 20 | particles = createParticlesFromAttractors(); 21 | } 22 | 23 | private Particle[] createParticlesFromAttractors () { 24 | Particle[] pts = new Particle[attractorsManager.attractors.length]; 25 | 26 | // loop through the attractors to create UI particles from the data 27 | for (int i = 0; i < attractorsManager.attractors.length; i++) { 28 | pts[i] = new Particle( 29 | i * 42 + 28, 30 | 30, 31 | 0, // angle 32 | 0.5, // mass 33 | 18, // radius 34 | 12, // magnetic force of the attraction points 35 | attractorsManager.attractors[i] 36 | ); 37 | } 38 | 39 | return pts; 40 | } 41 | 42 | public void update () { 43 | hovered = -1; 44 | subatomHovered = false; 45 | // we add some events here because this is called on every frame 46 | if (mouse.y < 80 && activeUIEvents < 1) { 47 | activeUIEvents = 1; 48 | for (int i = 0; i < particles.length; i++) { 49 | float dmouse = particles[i].position.copy().sub(mouse).mag(); 50 | boolean in = dmouse < particles[i].radius; 51 | if (in) { 52 | hovered = i; 53 | break; 54 | } 55 | } 56 | 57 | // we try the detection on the right icon 58 | if (hovered == -1) { 59 | if (mouse.x > width-120) { 60 | subatomHovered = true; 61 | } 62 | } 63 | } 64 | } 65 | 66 | public void mousePressed () { 67 | if (activeUIEvents == 1) { 68 | if (hovered != -1) { 69 | selected = hovered; 70 | } else if (subatomHovered) { 71 | activeUI = activeUI != 4 ? 4 : 0; 72 | } 73 | } 74 | } 75 | 76 | public void draw () { 77 | // the background 78 | noStroke(); 79 | fill(0, 0, 0, 120); 80 | rect(0, 0, width, ATOMS_BAR_HEIGHT); 81 | 82 | // the line 83 | strokeWeight(1); 84 | stroke(255, 255, 255, 50); 85 | line(0, ATOMS_BAR_HEIGHT, width, ATOMS_BAR_HEIGHT); 86 | 87 | // the particles 88 | for (int i = 0; i < particles.length; i++) { 89 | particles[i].draw(hovered == i, selected == i, false, false); 90 | } 91 | 92 | // infos 93 | textSize(14); 94 | textLeading(18); 95 | textAlign(LEFT, BOTTOM); 96 | text("Possible configurations", 10, ATOMS_BAR_HEIGHT - 3); 97 | textAlign(RIGHT, BOTTOM); 98 | if (activeUI == 4) { 99 | fill(0, 255, 0); 100 | } 101 | text("Subatomic particles", width - 10, ATOMS_BAR_HEIGHT - 3); 102 | 103 | // atoms types 104 | if (activeUI == 4) { 105 | stroke(0, 255, 0); 106 | } else if (subatomHovered) { 107 | stroke(255, 255, 0); 108 | } else { 109 | stroke(100); 110 | } 111 | noFill(); 112 | pushMatrix(); 113 | translate(width - 85, 34); 114 | strokeWeight(3); 115 | circle(0, 0, 40); 116 | strokeWeight(1); 117 | float dAngle = TWO_PI / attractorsManager.nbTypes; 118 | for (int i = 0; i < attractorsManager.nbTypes; i++) { 119 | int[] col = getColorFromType(i); 120 | fill(col[0], col[1], col[2]); 121 | circle( 122 | cos(-dAngle*i) * 20, 123 | sin(-dAngle*i) * 20, 124 | 12 125 | ); 126 | } 127 | popMatrix(); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/attractors_manager.pde: -------------------------------------------------------------------------------- 1 | /** 2 | * The attractors manager handles the available attractors, is responsible for the creation aswell 3 | **/ 4 | 5 | 6 | // This is where the default atoms are defined (defining their attractors is a way to define an atom 7 | ParticleAttractors attrs1 = new ParticleAttractors(new int[]{ 0, 1 }); 8 | ParticleAttractors attrs2 = new ParticleAttractors(new int[]{ 1, 2 }); 9 | ParticleAttractors attrs3 = new ParticleAttractors(new int[]{ 2, 3 }); 10 | ParticleAttractors attrs4 = new ParticleAttractors(new int[]{ 3, 0 }); 11 | ParticleAttractors attrs5 = new ParticleAttractors(new int[]{ 0, 0, 2 }); 12 | ParticleAttractors attrs6 = new ParticleAttractors(new int[]{ 1, 0, 0 }); 13 | ParticleAttractors attrs7 = new ParticleAttractors(new int[]{ 2, 2, 1 }); 14 | ParticleAttractors attrs8 = new ParticleAttractors(new int[]{ 3, 3 }); 15 | 16 | ParticleAttractors[] defaultAttractors = new ParticleAttractors[]{ attrs1, attrs2, attrs3, attrs4, attrs5, attrs6, attrs7, attrs8 }; 17 | 18 | 19 | 20 | 21 | class AttractorsManager { 22 | public ParticleAttractors[] attractors; 23 | public int nbTypes; 24 | 25 | public AttractorsManager () { 26 | // we initialize the attractors with the default ones 27 | attractors = defaultAttractors; 28 | 29 | // find the number of attractors by finding the maximum value in the default attractors 30 | int maxt = 0; 31 | for (int i = 0; i < attractors.length; i++) { 32 | for (int j = 0; j < attractors[i].types.length; j++) { 33 | maxt = max(maxt, attractors[i].types[j]+1); 34 | } 35 | } 36 | nbTypes = maxt; 37 | } 38 | 39 | // returns true if the types are adjacent on the repulsion circle 40 | public boolean areTypesRepelled (int t1, int t2) { 41 | if (t1 == t2) return false; 42 | if (t1 == 0 && t2 == nbTypes-1) return true; 43 | if (t2 == 0 && t1 == nbTypes-1) return true; 44 | return abs(t1-t2) == 1; 45 | } 46 | 47 | public void addAttractor (ParticleAttractors pa) { 48 | ParticleAttractors[] pas2 = new ParticleAttractors[attractors.length+1]; 49 | for (int i = 0; i < attractors.length; i++) { 50 | pas2[i] = attractors[i]; 51 | } 52 | pas2[attractors.length] = pa; 53 | attractors = pas2; 54 | atomsUI.updateDisplayParticles(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/config.pde: -------------------------------------------------------------------------------- 1 | /** 2 | * Easy way to have sort of a global accessible config 3 | **/ 4 | 5 | // ui config 6 | int UI_PADDING = 7; 7 | int[][] CHARGE_COLORS = new int[][]{ 8 | new int[]{ 255, 0, 0 }, 9 | new int[]{ 0, 0, 255 }, 10 | new int[]{ 0, 255, 0 }, 11 | new int[]{ 255, 255, 0 }, 12 | new int[]{ 0, 255, 255 }, 13 | new int[]{ 255, 0, 255 }, 14 | new int[]{ 0, 128, 255 }, 15 | new int[]{ 80, 255, 128 }, 16 | }; 17 | 18 | // sim config 19 | int NB_PARTICLES_START = 30; // the number of particles at the start of the simulation 20 | int MAX_CHARGES = 8; // maximum types of subatomic charges allowed 21 | float MAX_COL_RESPONSE = 8; // max magnitude of the collisions response 22 | 23 | 24 | class Config { 25 | public float maxSpeed = 5.0; 26 | public float magneticStrength = 1.0; 27 | public float attractionRange = 4.0; 28 | public float colTransferEnergy = 0.5; 29 | public float attractionStrength = 1; 30 | public float repulsionStrength = .3; 31 | public float friction = 0.1; 32 | public float spawnRadius = 12; 33 | public float distanceCenter = 0.6; 34 | } 35 | 36 | Config config = new Config(); 37 | -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/data/PTSerif-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciphrd/AtomicClustersExplorer/b5300eb7e59af26a895f5df52842edaee951a98e/sketch_atomic_clusters_explorer/data/PTSerif-Regular.ttf -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/data/RobotoMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciphrd/AtomicClustersExplorer/b5300eb7e59af26a895f5df52842edaee951a98e/sketch_atomic_clusters_explorer/data/RobotoMono-Regular.ttf -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/data/atoms.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciphrd/AtomicClustersExplorer/b5300eb7e59af26a895f5df52842edaee951a98e/sketch_atomic_clusters_explorer/data/atoms.png -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/data/controls.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciphrd/AtomicClustersExplorer/b5300eb7e59af26a895f5df52842edaee951a98e/sketch_atomic_clusters_explorer/data/controls.png -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/data/keyboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciphrd/AtomicClustersExplorer/b5300eb7e59af26a895f5df52842edaee951a98e/sketch_atomic_clusters_explorer/data/keyboard.png -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/data/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ciphrd/AtomicClustersExplorer/b5300eb7e59af26a895f5df52842edaee951a98e/sketch_atomic_clusters_explorer/data/pause.png -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/pan_zoom_manager.pde: -------------------------------------------------------------------------------- 1 | import processing.core.PApplet; 2 | import processing.core.PConstants; 3 | import processing.core.PVector; 4 | 5 | /** 6 | * Pan-Zoom Controller. 7 | * 8 | * Allows to move and scale a drawing using mouse and keyboard. Mouse wheel 9 | * changes the scale, mouse drag or keyboard arrows change the panning 10 | * (movement). 11 | * 12 | * @author Bohumir Zamecnik 13 | * @license MIT 14 | * 15 | * Inspired by "Pan And Zoom" by Dan Thompson, licensed under Creative Commons 16 | * Attribution-Share Alike 3.0 and GNU GPL license. Work: 17 | * http://openprocessing.org/visuals/?visualID= 46964 18 | */ 19 | public class PanZoomController { 20 | private final PVector DIR_UP = new PVector(0, -1); 21 | private final PVector DIR_DOWN = new PVector(0, 1); 22 | private final PVector DIR_LEFT = new PVector(-1, 0); 23 | private final PVector DIR_RIGHT = new PVector(1, 0); 24 | 25 | private float panVelocity = 40; 26 | private float scaleVelocity = 0.04f; 27 | private float minLogScale = -5; 28 | private float maxLogScale = 5; 29 | 30 | private float logScale = 0; 31 | private float scale = 1; 32 | private PVector pan = new PVector(); 33 | 34 | private PApplet p; 35 | 36 | public PanZoomController(PApplet p) { 37 | this.p = p; 38 | } 39 | 40 | public void mouseDragged() { 41 | PVector mouse = new PVector(p.mouseX, p.mouseY); 42 | PVector pmouse = new PVector(p.pmouseX, p.pmouseY); 43 | pan.add(PVector.sub(mouse, pmouse)); 44 | } 45 | 46 | public void mouseWheel (int step) { 47 | logScale = PApplet.constrain(logScale + step * scaleVelocity, 48 | minLogScale, 49 | maxLogScale); 50 | float prevScale = scale; 51 | scale = (float) Math.pow(2, logScale); 52 | 53 | PVector mouse = new PVector(p.mouseX, p.mouseY); 54 | pan = PVector.add(mouse, 55 | PVector.mult(PVector.sub(pan, mouse), scale / prevScale)); 56 | } 57 | 58 | // @author ciphrd 59 | // given 2d screen coordinates, returns corresponding world space coordinates 60 | public PVector screenToWorld (PVector coord) { 61 | return coord.copy().sub(pan).div(scale); 62 | } 63 | 64 | public float getScale() { 65 | return scale; 66 | } 67 | 68 | public void setScale(float scale) { 69 | this.scale = scale; 70 | } 71 | 72 | public PVector getPan() { 73 | return pan; 74 | } 75 | 76 | public void setPan(PVector pan) { 77 | this.pan = pan; 78 | } 79 | 80 | public void setPanVelocity(float panVelocity) { 81 | this.panVelocity = panVelocity; 82 | } 83 | 84 | public void setScaleVelocity(float scaleVelocity) { 85 | this.scaleVelocity = scaleVelocity; 86 | } 87 | 88 | public void setMinLogScale(float minLogScale) { 89 | this.minLogScale = minLogScale; 90 | } 91 | 92 | public void setMaxLogScale(float maxLogScale) { 93 | this.maxLogScale = maxLogScale; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/panel_atoms.pde: -------------------------------------------------------------------------------- 1 | /** 2 | * UI Layer ID: 5 3 | * 4 | **/ 5 | 6 | int PANEL_ATOM_WIDTH = 400; 7 | int PANEL_ATOM_HEIGHT = 400; 8 | int PANEL_ATOM_RADIUS = 90; 9 | int PANEL_ATOM_NB_H = 60; 10 | int PANEL_ATOM_BUTTON_H = 50; 11 | int PANEL_ATOM_CHARGES_SEL_W = 120; 12 | int PANEL_ATOM_CHARGES_SEL_RAD = 10; 13 | 14 | ParticleAttractors PANEL_ATOM_DEF_ATTR = new ParticleAttractors(new int[]{ 0, 0, 0 }); 15 | 16 | 17 | class PanelAtoms { 18 | PVector position; 19 | ParticleAttractors currentAtom; 20 | PVector atomCenter; 21 | int nChargesHovered; 22 | int atomChargeHovered; 23 | int atomChargeSelected = 0; 24 | int chargeSelHovered; 25 | boolean buttonHovered; 26 | 27 | public PanelAtoms () { 28 | position = new PVector( 29 | (width-PANEL_ATOM_WIDTH) / 2, 30 | (height-PANEL_ATOM_HEIGHT) / 2 31 | ); 32 | currentAtom = PANEL_ATOM_DEF_ATTR; 33 | atomCenter = new PVector( 34 | PANEL_ATOM_RADIUS + UI_PADDING*4, 35 | (PANEL_ATOM_HEIGHT-20-PANEL_ATOM_BUTTON_H-PANEL_ATOM_NB_H) / 2 + 20 36 | ); 37 | } 38 | 39 | // updates the current atom so that its attractors are 40 | private void updateCurrentAtom (int nb) { 41 | // we build the types array 42 | int[] types = new int[nb]; 43 | for (int i = 0; i < nb; i++) { 44 | types[i] = currentAtom.nb > i ? currentAtom.types[i] : 0; 45 | } 46 | 47 | ParticleAttractors nAtom = new ParticleAttractors(types); 48 | currentAtom = nAtom; 49 | 50 | if (atomChargeSelected+1 > nAtom.nb) { 51 | atomChargeSelected = nAtom.nb - 1; 52 | } 53 | } 54 | 55 | public void update () { 56 | nChargesHovered = -1; 57 | atomChargeHovered = -1; 58 | chargeSelHovered = -1; 59 | buttonHovered = false; 60 | 61 | if (activeUI == 5) { 62 | PVector mousePanel = mouse.copy().sub(new PVector(position.x, position.y)); 63 | 64 | if (activeUIEvents < 5 && mousePanel.x > 0 && mousePanel.x < PANEL_ATOM_WIDTH 65 | && mousePanel.y > 0 && mousePanel.y < PANEL_ATOM_HEIGHT) { 66 | activeUIEvents = 5; 67 | 68 | // we detect in which part of the popin we are 69 | if (mousePanel.y > PANEL_ATOM_HEIGHT - PANEL_ATOM_BUTTON_H - PANEL_ATOM_NB_H && mousePanel.y < PANEL_ATOM_HEIGHT - PANEL_ATOM_BUTTON_H 70 | && mousePanel.x < PANEL_ATOM_WIDTH - 100) { 71 | // we are in the charges section 72 | int cw = (PANEL_ATOM_WIDTH-100) / 4; 73 | for (int i = 0; i < 4; i++) { 74 | if (mousePanel.x > cw * i && mousePanel.x < cw * (i+1)) { 75 | nChargesHovered = i; 76 | break; 77 | } 78 | } 79 | } 80 | // test for the atom && the charges selection 81 | else if (mousePanel.y > 20 && mousePanel.y < PANEL_ATOM_HEIGHT - PANEL_ATOM_BUTTON_H - PANEL_ATOM_NB_H) { 82 | // x can tell if we're on the atom or on the charges 83 | if (mousePanel.x < PANEL_ATOM_WIDTH - PANEL_ATOM_CHARGES_SEL_W) { // atom 84 | // now we can finally detect in which section of the circle we are, to know which charge is hovered 85 | PVector dir = mousePanel.copy().sub(atomCenter); 86 | if (dir.mag() < PANEL_ATOM_RADIUS + 20) { 87 | float da = TWO_PI / currentAtom.nb; 88 | float a = (atan2(dir.y, dir.x) + TWO_PI + da/2) % TWO_PI; 89 | atomChargeHovered = int(a/da); 90 | } 91 | } 92 | else { // cursor is over the charges area 93 | mousePanel.sub(new PVector(PANEL_ATOM_WIDTH - PANEL_ATOM_CHARGES_SEL_W/2, 65)); 94 | 95 | // now test the collision with the circles 96 | for (int i = 0; i < attractorsManager.nbTypes; i++) { 97 | float y = i*(PANEL_ATOM_CHARGES_SEL_RAD*2+5); 98 | PVector pos = new PVector(0, y); 99 | pos.sub(mousePanel); 100 | if (pos.mag() <= PANEL_ATOM_CHARGES_SEL_RAD) { 101 | chargeSelHovered = i; 102 | break; 103 | } 104 | } 105 | } 106 | } 107 | // test for the button 108 | else if (mousePanel.y > PANEL_ATOM_HEIGHT - PANEL_ATOM_BUTTON_H) { 109 | buttonHovered = true; 110 | } 111 | } 112 | } 113 | } 114 | 115 | public void mousePressed () { 116 | if (activeUIEvents == 5) { 117 | if (nChargesHovered != -1) { 118 | if (nChargesHovered+1 != currentAtom.nb) { 119 | // update of the current atom 120 | updateCurrentAtom(nChargesHovered+1); 121 | } 122 | } else if (atomChargeHovered != -1) { 123 | atomChargeSelected = atomChargeHovered; 124 | } else if (chargeSelHovered != -1) { 125 | currentAtom.types[atomChargeSelected] = chargeSelHovered; 126 | } else if (buttonHovered) { 127 | int[] cp = new int[currentAtom.types.length]; 128 | for (int i = 0; i < cp.length; i++) 129 | cp[i] = currentAtom.types[i]; 130 | 131 | ParticleAttractors newPa = new ParticleAttractors(cp); 132 | attractorsManager.addAttractor(newPa); 133 | } 134 | } 135 | } 136 | 137 | public void draw () { 138 | if (activeUI == 5) { 139 | pushMatrix(); 140 | translate(position.x, position.y); 141 | 142 | fill(0, 0, 0, 210); 143 | stroke(255, 255, 255, 120); 144 | rect(0, 0, PANEL_ATOM_WIDTH, PANEL_ATOM_HEIGHT); 145 | 146 | fill(255); 147 | textAlign(LEFT, TOP); 148 | text("Create a new atom and add it to the toolbar", UI_PADDING, 3); 149 | 150 | drawAtom(); 151 | drawChargesNumber(); 152 | drawChargeSelection(); 153 | drawButton(); 154 | 155 | popMatrix(); 156 | } 157 | } 158 | 159 | private void drawAtom () { 160 | pushMatrix(); 161 | translate(atomCenter.x, atomCenter.y); 162 | 163 | noFill(); 164 | stroke(128); 165 | strokeWeight(4); 166 | circle(0, 0, PANEL_ATOM_RADIUS*2); 167 | 168 | // now the attractors 169 | PVector pos = new PVector(0, 0); 170 | int[] col; 171 | for (int i = 0; i < currentAtom.nb; i++) { 172 | float a = currentAtom.getAttractorAngle(i); 173 | col = getColorFromType(currentAtom.types[i]); 174 | pos.set( 175 | cos(a) * PANEL_ATOM_RADIUS, 176 | sin(a) * PANEL_ATOM_RADIUS 177 | ); 178 | // draw the line 179 | strokeWeight(7); 180 | stroke(col[0], col[1], col[2]); 181 | line(0, 0, pos.x, pos.y); 182 | 183 | strokeWeight(4); 184 | if (atomChargeSelected == i) { 185 | stroke(0, 255, 0); 186 | } else if (atomChargeHovered == i) { 187 | stroke(255, 255, 0); 188 | } else { 189 | stroke(128); 190 | } 191 | fill(col[0], col[1], col[2]); 192 | circle( 193 | pos.x, 194 | pos.y, 195 | 30 196 | ); 197 | if (atomChargeSelected == i) { 198 | noFill(); 199 | stroke(0); 200 | strokeWeight(1); 201 | circle( 202 | pos.x, 203 | pos.y, 204 | 30 205 | ); 206 | } 207 | } 208 | 209 | // and finally the center 210 | noStroke(); 211 | fill(255); 212 | circle(0, 0, 40); 213 | 214 | popMatrix(); 215 | } 216 | 217 | private void drawCharge (int idx, boolean active, boolean hovered) { 218 | pushMatrix(); 219 | translate((PANEL_ATOM_WIDTH-100) / 4 * (idx+.5), PANEL_ATOM_NB_H / 2); 220 | 221 | noFill(); 222 | strokeWeight(2); 223 | if (active) 224 | stroke(0, 255, 0); 225 | else if (hovered) 226 | stroke(255, 255, 0); 227 | else 228 | stroke(128); 229 | circle(0, 0, 40); 230 | 231 | // now we draw the charges 232 | float da = TWO_PI / (idx+1); 233 | noStroke(); 234 | if (active) 235 | fill(0, 255, 0); 236 | else if (hovered) 237 | fill(255, 255, 0); 238 | else 239 | fill(128); 240 | for (int i = 0; i < idx+1; i++) { 241 | float a = da * i; 242 | circle(cos(a)*20, sin(a)*20, 11); 243 | } 244 | 245 | popMatrix(); 246 | } 247 | 248 | private void drawChargesNumber () { 249 | pushMatrix(); 250 | translate(0, PANEL_ATOM_HEIGHT - PANEL_ATOM_BUTTON_H - PANEL_ATOM_NB_H); 251 | 252 | stroke(50); 253 | strokeWeight(1); 254 | line(0, -UI_PADDING, PANEL_ATOM_WIDTH, -UI_PADDING); 255 | 256 | for (int i = 0; i < 4; i++) { 257 | drawCharge(i, i == currentAtom.nb-1, i == nChargesHovered); 258 | } 259 | 260 | fill(255); 261 | textAlign(LEFT, CENTER); 262 | text("Number of\ncharges", PANEL_ATOM_WIDTH-90, PANEL_ATOM_NB_H/2); 263 | 264 | popMatrix(); 265 | } 266 | 267 | private void drawChargeSelection () { 268 | pushMatrix(); 269 | translate(PANEL_ATOM_WIDTH - PANEL_ATOM_CHARGES_SEL_W/2, 65); 270 | 271 | int[] col; 272 | for (int i = 0; i < 8; i++) { 273 | col = getColorFromType(i); 274 | int alpha = 255; 275 | strokeWeight(3); 276 | if (i+1 > attractorsManager.nbTypes) { 277 | strokeWeight(0); 278 | alpha = 30; 279 | } 280 | if (i == currentAtom.types[atomChargeSelected]) { 281 | stroke(0, 255, 0); 282 | } else { 283 | stroke(128, 128, 128, alpha); 284 | } 285 | fill(col[0], col[1], col[2], alpha); 286 | circle(0, i*(PANEL_ATOM_CHARGES_SEL_RAD*2+5), PANEL_ATOM_CHARGES_SEL_RAD*2); 287 | if (i == currentAtom.types[atomChargeSelected]) { 288 | noFill(); 289 | stroke(0); 290 | strokeWeight(1); 291 | circle( 292 | 0, 293 | i*(PANEL_ATOM_CHARGES_SEL_RAD*2+5), 294 | PANEL_ATOM_CHARGES_SEL_RAD*2 295 | ); 296 | } 297 | } 298 | 299 | popMatrix(); 300 | } 301 | 302 | private void drawButton () { 303 | if (buttonHovered) { 304 | fill(0, 200, 80); 305 | } else { 306 | fill(0, 128, 50); 307 | } 308 | noStroke(); 309 | rect(0, PANEL_ATOM_HEIGHT - PANEL_ATOM_BUTTON_H + UI_PADDING, PANEL_ATOM_WIDTH, PANEL_ATOM_BUTTON_H - UI_PADDING); 310 | 311 | fill(255); 312 | textAlign(CENTER, CENTER); 313 | text("Add ATOM to the atoms bar", PANEL_ATOM_WIDTH/2, PANEL_ATOM_HEIGHT - (PANEL_ATOM_BUTTON_H - UI_PADDING/2) / 2); 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/panel_charges.pde: -------------------------------------------------------------------------------- 1 | /** 2 | * UI Layer ID: 4 3 | * Can edit the number of charges of the universe 4 | * 5 | * Could be reworked into sub classes but hey 6 | **/ 7 | 8 | 9 | int CHARGES_PANEL_WIDTH = 400; 10 | int CHARGES_PANEL_HEIGHT = 100; 11 | int CHARGES_PANEL_C_RADIUS = 25; 12 | int CHARGES_PANEL_PADDING = 25; 13 | 14 | int[] CHARGES_PANEL_C1 = new int[]{ 255, 255, 255 }; 15 | int[] CHARGES_PANEL_C2 = new int[]{ 0, 255, 0 }; 16 | 17 | 18 | class ChargesPanel { 19 | PVector position; 20 | int chargeWidth; 21 | int hovered; 22 | 23 | public ChargesPanel () { 24 | position = new PVector( 25 | (width - CHARGES_PANEL_WIDTH) / 2, 26 | (height - CHARGES_PANEL_HEIGHT) / 2 27 | ); 28 | chargeWidth = (CHARGES_PANEL_WIDTH-CHARGES_PANEL_PADDING) / MAX_CHARGES; 29 | } 30 | 31 | private void drawCharge (int idx) { 32 | PVector center = new PVector( 33 | chargeWidth * idx + CHARGES_PANEL_PADDING + CHARGES_PANEL_C_RADIUS/2, 34 | 68 35 | ); 36 | int[] col = getColorFromType(idx); 37 | fill(col[0], col[1], col[2]); 38 | noStroke(); 39 | circle(center.x, center.y, CHARGES_PANEL_C_RADIUS); 40 | } 41 | 42 | private void drawRange (int nb, int[] col) { 43 | stroke(col[0], col[1], col[2]); 44 | noFill(); 45 | rect( 46 | 10, 47 | 45, 48 | chargeWidth * nb + 5, 49 | 45 50 | ); 51 | } 52 | 53 | public void update () { 54 | hovered = -1; 55 | if (activeUI == 4) { 56 | // at first we detect if the mouse is in the toolbar 57 | if (activeUIEvents < 4 && mouse.x > position.x && mouse.x < position.x + CHARGES_PANEL_WIDTH 58 | && mouse.y > position.y && mouse.y < position.y + CHARGES_PANEL_HEIGHT) { 59 | activeUIEvents = 4; 60 | 61 | // we try fo find which one is hovered 62 | PVector pos = new PVector(); 63 | for (int i = 0; i < MAX_CHARGES; i++) { 64 | fill(255, 0, 0); 65 | pos.set( 66 | position.x + i * chargeWidth + 14, 67 | position.y + 45 68 | ); 69 | if (mouse.x > pos.x && mouse.x < pos.x + chargeWidth +1 && mouse.y > pos.y && mouse.y < pos.y + 45) { 70 | hovered = i; 71 | break; 72 | } 73 | } 74 | } 75 | } 76 | } 77 | 78 | public void draw () { 79 | if (activeUI == 4) { // if it is selected in the menu 80 | pushMatrix(); 81 | translate(position.x, position.y); 82 | 83 | // the background/border of the popin 84 | fill(0, 0, 0, 210); 85 | stroke(255, 255, 255, 120); 86 | rect(0, 0, CHARGES_PANEL_WIDTH, CHARGES_PANEL_HEIGHT); 87 | 88 | fill(255); 89 | textAlign(LEFT, TOP); 90 | text("Subatomic particles\nSet the number available in this universe", UI_PADDING, 3); 91 | 92 | for (int i = 0; i < MAX_CHARGES; i++) { 93 | drawCharge(i); 94 | } 95 | // the current number of charges displayed 96 | drawRange(attractorsManager.nbTypes, CHARGES_PANEL_C1); 97 | if (hovered != -1) drawRange(hovered+1, CHARGES_PANEL_C2); 98 | 99 | popMatrix(); 100 | } 101 | } 102 | 103 | public void mousePressed () { 104 | if (activeUIEvents == 4 && hovered != -1) { 105 | attractorsManager.nbTypes = hovered + 1; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/panel_keyboard.pde: -------------------------------------------------------------------------------- 1 | /** 2 | * UI Layer ID: 6 3 | * Displays the keyboard shortcuts 4 | **/ 5 | 6 | 7 | int PANEL_KB_WIDTH = 700; 8 | int PANEL_KB_ENTRY_H = 30; 9 | int PANEL_NB_COL1_W = 130; 10 | 11 | String[] PANEL_KB_SHORTCUTS = new String[]{ 12 | "MOUSE_LEFT", 13 | "MOUSE_RIGHT", 14 | "MOUSE_WHEEL", 15 | "SPACE", 16 | "DEL", 17 | "ENTER", 18 | "a", 19 | "d", 20 | "r", 21 | }; 22 | 23 | String[] PANEL_KB_INTEL = new String[]{ 24 | "On atom: select / Otherwise: add selected toolbar atom", 25 | "Move atom under cursor, left click to release", 26 | "Adjust zoom level", 27 | "Pause/Start simulation", 28 | "Delete selected atom", 29 | "Delete all atoms", 30 | "Add many selected toolbar atoms", 31 | "Deselect the selected atom", 32 | "Rotate selected atom", 33 | }; 34 | 35 | 36 | class PanelKeyboard { 37 | int panelHeight; 38 | PVector position; 39 | 40 | public PanelKeyboard () { 41 | panelHeight = 40 + PANEL_KB_ENTRY_H * PANEL_KB_SHORTCUTS.length; 42 | position = new PVector( 43 | (width - PANEL_KB_WIDTH) / 2, 44 | (height - panelHeight) / 2 45 | ); 46 | } 47 | 48 | public void update () { 49 | if (activeUI == 6) { 50 | if (activeUIEvents < 6 && mouse.x > position.x && mouse.x < position.x + PANEL_KB_WIDTH 51 | && mouse.y > position.y && mouse.y < position.y + panelHeight) { 52 | activeUIEvents = 6; 53 | } 54 | } 55 | } 56 | 57 | public void draw () { 58 | if (activeUI == 6) { 59 | pushMatrix(); 60 | translate(position.x, position.y); 61 | 62 | fill(0, 0, 0, 210); 63 | stroke(255, 255, 255, 120); 64 | rect(0, 0, PANEL_KB_WIDTH, panelHeight); 65 | 66 | fill(255); 67 | textAlign(CENTER, TOP); 68 | text("Keyboard / mouse controls", PANEL_KB_WIDTH / 2, 10); 69 | 70 | // now we can draw the controls 71 | translate(0, 40); 72 | textAlign(LEFT, CENTER); 73 | strokeWeight(1); 74 | stroke(50); 75 | for (int i = 0; i < PANEL_KB_SHORTCUTS.length; i++) { 76 | text(PANEL_KB_SHORTCUTS[i], UI_PADDING, (i+.5) * PANEL_KB_ENTRY_H); 77 | text(PANEL_KB_INTEL[i] , UI_PADDING + PANEL_NB_COL1_W, (i+.5) * PANEL_KB_ENTRY_H); 78 | line(1, i * PANEL_KB_ENTRY_H, PANEL_KB_WIDTH-2, i * PANEL_KB_ENTRY_H); 79 | } 80 | 81 | // vertical separator 82 | line( 83 | UI_PADDING + PANEL_NB_COL1_W - UI_PADDING, 84 | 0, 85 | UI_PADDING + PANEL_NB_COL1_W - UI_PADDING, 86 | panelHeight - 40 87 | ); 88 | 89 | popMatrix(); 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/panel_slider.pde: -------------------------------------------------------------------------------- 1 | /** 2 | * UI Layer ID: 3 3 | **/ 4 | 5 | boolean drawAttractionRange = false; 6 | 7 | int SLIDERS_PANEL_WIDTH = 250; 8 | int SLIDER_HEIGHT = 45; 9 | int SLIDER_PADDING = 5; 10 | 11 | 12 | String[] SLIDERS_TEXT = new String[]{ 13 | "Max speed", 14 | "Magnetic strength", 15 | "Attraction range", 16 | "Col transfer energy", 17 | "Attraction strength", 18 | "RepulsionStrength", 19 | "Friction", 20 | "Spawn radius", 21 | "Charges distance center", 22 | }; 23 | 24 | float[] SLIDERS_MINS = new float[]{ 25 | 1.0, // max speed, 26 | 0.0, // magnetic strength 27 | 2.0, // attraction range 28 | 0.0, // collision transfer energy 29 | 0.0, // attraction strength, 30 | 0.0, // repulsion strength 31 | 0.0, // friction 32 | 1, // spawn radius 33 | 0, // charges distance center 34 | }; 35 | 36 | float[] SLIDERS_MAXS = new float[]{ 37 | 20.0, // max speed 38 | 4.0, // magnetic strength 39 | 10.0, // attraction range 40 | 1.2, // collision transfer energy 41 | 4.0, // attraction strength 42 | 4.0, // repulsion strength 43 | 1.0, // friction 44 | 50, // spawn radius 45 | 1.5, // charges distance center 46 | }; 47 | 48 | 49 | class SlidersPanel { 50 | int panelHeight; 51 | PVector position; 52 | PVector sliderSize; 53 | int hovered; // if a slider is hovered 54 | 55 | public SlidersPanel () { 56 | panelHeight = SLIDER_HEIGHT * SLIDERS_TEXT.length; 57 | position = new PVector( 58 | width - SLIDERS_PANEL_WIDTH, 59 | height - toolbar.size.y - panelHeight - 2 60 | ); 61 | sliderSize = new PVector( 62 | SLIDERS_PANEL_WIDTH - SLIDER_PADDING*2 - 50, 63 | (SLIDER_HEIGHT-SLIDER_PADDING*2) / 2 64 | ); 65 | } 66 | 67 | void updateConfigValue (int idx, float value) { 68 | switch (idx) { 69 | case 0: // max speed 70 | config.maxSpeed = value; 71 | break; 72 | case 1: // magnetic strength 73 | config.magneticStrength = value; 74 | break; 75 | case 2: // attraction range 76 | config.attractionRange = value; 77 | break; 78 | case 3: // collision transfer energy 79 | config.colTransferEnergy = value; 80 | break; 81 | case 4: // attraction strength 82 | config.attractionStrength = value; 83 | break; 84 | case 5: // repulsion strength 85 | config.repulsionStrength = value; 86 | break; 87 | case 6: // friction 88 | config.friction = value; 89 | break; 90 | case 7: // spawn radius 91 | config.spawnRadius = value; 92 | break; 93 | case 8: // spawn radius 94 | config.distanceCenter = value; 95 | break; 96 | } 97 | } 98 | 99 | float getParameterValue (int idx, float t) { 100 | return lerp(SLIDERS_MINS[idx], SLIDERS_MAXS[idx], t); 101 | } 102 | 103 | float getConfigValue (int idx) { 104 | switch (idx) { 105 | case 0: // max speed 106 | return config.maxSpeed; 107 | case 1: // magnetic strength 108 | return config.magneticStrength; 109 | case 2: // attraction range 110 | return config.attractionRange; 111 | case 3: // collision transfer energy 112 | return config.colTransferEnergy; 113 | case 4: // attraction strength 114 | return config.attractionStrength; 115 | case 5: // repulsion strength 116 | return config.repulsionStrength; 117 | case 6: // friction 118 | return config.friction; 119 | case 7: // spawn radius 120 | return config.spawnRadius; 121 | case 8: // spawn radius 122 | return config.distanceCenter; 123 | } 124 | 125 | return 0; 126 | } 127 | 128 | public void update () { 129 | drawAttractionRange = false; 130 | 131 | if (activeUI == 3) { 132 | hovered = -1; 133 | 134 | // check if the mouse is over the panel first 135 | if (activeUIEvents < 3 && mouse.x > position.x && mouse.y > position.y && mouse.y < position.y + panelHeight) { 136 | activeUIEvents = 3; 137 | 138 | // now we can check if the mouse is inside a slider 139 | for (int i = 0; i < SLIDERS_TEXT.length; i++) { 140 | PVector containerPos = new PVector( 141 | position.x + SLIDER_PADDING, 142 | position.y + i * SLIDER_HEIGHT + SLIDER_PADDING 143 | ); 144 | 145 | if (mouse.x > containerPos.x && mouse.x < containerPos.x + sliderSize.x 146 | && mouse.y > containerPos.y + sliderSize.y && mouse.y < containerPos.y + 2 * sliderSize.y) { 147 | hovered = i; 148 | break; 149 | } 150 | } 151 | } 152 | 153 | if (hovered == 2) drawAttractionRange = true; 154 | 155 | // if mouse is down, we can update the slider value 156 | if (mousePressed && hovered != -1) { 157 | PVector containerPos = new PVector( 158 | position.x + SLIDER_PADDING, 159 | position.y + hovered * SLIDER_HEIGHT + SLIDER_PADDING 160 | ); 161 | float t = (mouse.x-containerPos.x) / sliderSize.x; 162 | updateConfigValue(hovered, getParameterValue(hovered, t)); 163 | } 164 | } 165 | } 166 | 167 | public void draw () { 168 | if (activeUI == 3) { 169 | fill(0, 0, 0, 180); 170 | stroke(255, 255, 255, 120); 171 | rect(position.x, position.y, SLIDERS_PANEL_WIDTH, panelHeight); 172 | 173 | // we draw all the sliders + text one by one 174 | for (int i = 0; i < SLIDERS_TEXT.length; i++) { 175 | PVector containerPos = new PVector( 176 | position.x + SLIDER_PADDING, 177 | position.y + i * SLIDER_HEIGHT + SLIDER_PADDING 178 | ); 179 | fill(255, 255, 255); 180 | textAlign(LEFT, TOP); 181 | text(SLIDERS_TEXT[i], containerPos.x, containerPos.y-4); 182 | 183 | // draw the slider 184 | noStroke(); 185 | fill(128, 128, 128); 186 | rect(containerPos.x, containerPos.y + sliderSize.y, sliderSize.x, sliderSize.y); 187 | 188 | // inner 189 | fill(0, 255, 0); 190 | float cfgValue = getConfigValue(i); 191 | float t = (cfgValue - SLIDERS_MINS[i]) / (SLIDERS_MAXS[i]-SLIDERS_MINS[i]); 192 | rect(containerPos.x, containerPos.y + sliderSize.y, sliderSize.x * t, sliderSize.y); 193 | 194 | // value 195 | fill(255); 196 | String cfgValueStr = String.valueOf(cfgValue); 197 | cfgValueStr = cfgValueStr.substring(0, min(4, cfgValueStr.length())); 198 | text(cfgValueStr, containerPos.x + sliderSize.x + 5, containerPos.y + sliderSize.y); 199 | } 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/particle.pde: -------------------------------------------------------------------------------- 1 | 2 | int[] getColorFromType (int type) { 3 | return CHARGE_COLORS[type]; 4 | } 5 | 6 | public class Particle { 7 | PVector position; 8 | PVector velocity; 9 | public float angle; // the rotation arround the z axis, rad 10 | float angVelocity; // the angular velocity 11 | float radius; 12 | float mass; 13 | float magn; // magnetique strength 14 | ParticleAttractors attractors; 15 | 16 | public Particle (float x, float y, float ang, float m, float rad, float magnetic, ParticleAttractors attrctrs) { 17 | position = new PVector(x, y); 18 | velocity = new PVector(0f, 0f); 19 | 20 | angle = ang; 21 | angVelocity = 0f; 22 | 23 | radius = rad; 24 | mass = m; 25 | magn = magnetic; 26 | 27 | attractors = attrctrs; 28 | } 29 | 30 | // attractor world position 31 | public PVector getAttrWPos (int idx) { 32 | PVector ret = getAttrPos(idx); 33 | ret.add(position); 34 | return ret; 35 | } 36 | 37 | // attractor position relative to the center 38 | private PVector getAttrPos (int idx) { 39 | float attrAngle = attractors.getAttractorAngle(idx); 40 | 41 | return new PVector( 42 | cos(angle + attrAngle) * radius * config.distanceCenter, 43 | sin(angle + attrAngle) * radius * config.distanceCenter 44 | ); 45 | } 46 | 47 | public void update () { 48 | angle+= angVelocity; 49 | PVector npos = position.copy().add(velocity); 50 | 51 | // TO BE REMOVED - smoothening of the position to prevent flickering 52 | npos.set(lerp(position.x, npos.x, .8), lerp(position.y, npos.y, .8)); 53 | position = npos; 54 | } 55 | 56 | // applies the friction over the angular and linear velocity 57 | public void friction () { 58 | angVelocity*= 1 - config.friction; 59 | velocity.mult(1 - config.friction); 60 | } 61 | 62 | public void draw (boolean hovered, boolean selected, boolean drawRange, boolean drawForces) { 63 | noFill(); 64 | 65 | // translate to the center 66 | pushMatrix(); 67 | translate(position.x, position.y); 68 | 69 | // draw its axis 70 | stroke(50); 71 | strokeWeight(1); 72 | line(cos(angle)*radius, sin(angle)*radius, cos(angle+PI)*radius, sin(angle+PI)*radius); 73 | 74 | // draw the attraction point 75 | // draw the attraction points 76 | noStroke(); 77 | for (int i = 0; i < attractors.nb; i++) { 78 | int[] col = getColorFromType(attractors.types[i]); 79 | fill(col[0], col[1], col[2]); 80 | stroke(col[0], col[1], col[2]); 81 | strokeWeight(4); 82 | PVector attrPos = getAttrPos(i); 83 | line(0, 0, attrPos.x, attrPos.y); 84 | circle(attrPos.x, attrPos.y, 4); 85 | } 86 | 87 | 88 | // draw the center 89 | fill(255); 90 | noStroke(); 91 | circle(0, 0, 6); 92 | 93 | 94 | // draw the circle 95 | noFill(); 96 | if (selected) { 97 | stroke(0, 255, 0); 98 | } else if (hovered) { 99 | stroke(200, 200, 0); 100 | } else { 101 | stroke(50); 102 | } 103 | strokeWeight(selected ? 3 : 2); 104 | circle(0, 0, radius*2f); 105 | 106 | // draw the attraction 107 | if (drawAttractionRange && drawRange) { 108 | stroke(255, 0, 0, 50); 109 | circle(0, 0, radius*config.attractionRange*2); 110 | } 111 | 112 | // we draw the velocity 113 | if (selected && drawForces) drawVector(velocity.copy().div(config.maxSpeed).mult(10), 0, 0, new int[]{255, 255, 0}); 114 | 115 | popMatrix(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/particle_attractors.pde: -------------------------------------------------------------------------------- 1 | /** 2 | * The attractions is simply a list of groups. 3 | * Particles can have up to 5 attraction points, distrubted evenly along a circle of a radius smaller than the particle radius. 4 | * Each attraction point belongs to a certain type 5 | **/ 6 | 7 | class ParticleAttractors { 8 | public int nb; 9 | public int[] types; 10 | float deltaAngle; // the angle distance between the points 11 | 12 | public ParticleAttractors (int[] _types) { 13 | types = _types; 14 | update(); 15 | } 16 | 17 | public void update () { 18 | nb = types.length; 19 | deltaAngle = TWO_PI / nb; 20 | } 21 | 22 | /** 23 | * returns the angle of the attractor at a given index 24 | **/ 25 | public float getAttractorAngle (int idx) { 26 | return idx * deltaAngle; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/particles_manager.pde: -------------------------------------------------------------------------------- 1 | /** 2 | * This class is responsible for handling the particles, storing them, adding, deleting, 3 | * computing the forces and updating 4 | **/ 5 | 6 | class ParticlesManager { 7 | public Particle[] particles; 8 | 9 | public ParticlesManager () { 10 | particles = new Particle[NB_PARTICLES_START]; 11 | 12 | // initialize the particles 13 | for (int i = 0; i < particles.length; i++) { 14 | particles[i] = new Particle( 15 | random(312) + (width-312)/2, //random(400.0)+312, 16 | random(312) + (height-312)/2, //random(400.0)+312, 17 | random(PI*2f), 18 | 0.5, // mass 19 | config.spawnRadius, // radius 20 | 8, // magnetic force of the attraction points 21 | attractorsManager.attractors[floor(random(attractorsManager.attractors.length))] // we give it a random ParticleAttractors 22 | ); 23 | } 24 | } 25 | 26 | public void computeVelocities () { 27 | // the space transform to allow for pan/zoom 28 | PVector pan = panZoomManager.getPan(); 29 | float scale = panZoomManager.getScale(); 30 | pushMatrix(); 31 | translate(pan.x, pan.y); 32 | scale(scale); 33 | 34 | for (int i = 0; i < particles.length; i++) { 35 | 36 | if (!paused && i != moved) { 37 | for (int j = 0; j < particles.length; j++) { 38 | if (i != j) { // ignore self to self 39 | Particle p1 = particles[i]; 40 | Particle p2 = particles[j]; 41 | 42 | if (p1.position.copy().sub(p2.position).mag() < p1.radius * config.attractionRange) { 43 | 44 | // we go through the attraction points of the both particles 45 | for (int ai = 0; ai < p1.attractors.nb; ai++) { 46 | for (int aj = 0; aj < p2.attractors.nb; aj++) { 47 | boolean repelled = attractorsManager.areTypesRepelled(p1.attractors.types[ai], p2.attractors.types[aj]); 48 | 49 | // if the attractors are of the same type 50 | if (p1.attractors.types[ai] == p2.attractors.types[aj] || repelled) { 51 | // first we find the force between the attraction points 52 | PVector p1attr = p1.getAttrWPos(ai); 53 | PVector F = p2.getAttrWPos(aj).sub(p1attr); 54 | float d = F.mag(); 55 | F.normalize(); 56 | if (i == selected) { 57 | drawVector(F.copy().mult(50 * (repelled ? -1 : 1)), p1attr.x, p1attr.y, getColorFromType(p2.attractors.types[aj])); 58 | } 59 | F.mult((p1.magn*p2.magn)/(d*d)); 60 | F.mult(repelled ? -config.repulsionStrength : config.attractionStrength); 61 | F.mult(config.magneticStrength); 62 | 63 | // we compute the torque 64 | float theta = atan2(F.y, F.x) + PI - (p1.angle + p1.attractors.getAttractorAngle(ai)); 65 | float torq = (config.distanceCenter * p1.radius) * F.mag() * sin(-theta); 66 | 67 | // from which we can find the angular momentum 68 | float am = 2.0 * torq / (p1.mass*p1.radius*p1.radius); 69 | p1.angVelocity+= am; 70 | 71 | // now we update the translation motion 72 | PVector ca = p2.getAttrWPos(aj).sub(p1attr); 73 | ca.normalize(); 74 | ca.mult(F.mag() - abs(F.mag() * sin(-theta))); 75 | ca.mult(repelled ? -config.repulsionStrength : config.attractionStrength); 76 | p1.velocity.add(ca); 77 | } 78 | } 79 | } 80 | } 81 | 82 | // just add a bit of repulsion between particles 83 | /* 84 | PVector dir = p1.position.copy().sub(p2.position); 85 | float d = dir.mag(); 86 | dir.normalize(); 87 | 88 | float repF = 0; 89 | if (d < 40) { 90 | repF = (1 - d/40) * 1.5; 91 | } 92 | PVector rep = dir.mult(repF); 93 | p1.velocity.add(rep); 94 | */ 95 | } 96 | } 97 | 98 | // small attraction to the center 99 | // particles[i].velocity.add(particles[i].position.copy().sub(new PVector(width/2, height/2)).mult(-0.0005)); 100 | 101 | // if the velocity of a particle is greater than a threshold we cut it 102 | if (particles[i].velocity.mag() > config.maxSpeed) { 103 | particles[i].velocity.normalize(); 104 | particles[i].velocity.mult(config.maxSpeed); 105 | } 106 | 107 | // same for the angular velocity 108 | if (abs(particles[i].angVelocity) > 0.1) { 109 | particles[i].angVelocity = 0.1 * Math.signum(particles[i].angVelocity); 110 | } 111 | } // end pause block 112 | 113 | // now the tests for the tooling 114 | if (mouse.y > 80 && activeUIEvents == 0 && hovered == -1) { 115 | float dmouse = particles[i].position.copy().sub(worldMouse).mag(); 116 | boolean in = dmouse < particles[i].radius; 117 | if (in) { 118 | hovered = i; 119 | } 120 | } 121 | 122 | if (i == moved) { 123 | particles[i].position.set(worldMouse.x, worldMouse.y); 124 | particles[i].velocity.mult(0); 125 | } 126 | } 127 | 128 | popMatrix(); 129 | } 130 | 131 | public void applyCollisions () { 132 | for (int i = 0; i < particles.length; i++) { 133 | for (int j = 0; j < particles.length; j++) { 134 | // first, we compute the positions after the application of the velocity 135 | PVector pos1end = particles[i].position.copy().add(particles[i].velocity); 136 | PVector dirend = pos1end.copy().sub(particles[j].position); 137 | float dend = dirend.mag(); // distance at the end of position update 138 | dirend.normalize(); 139 | 140 | if (i != j) { // prevents self to self collision 141 | // is there a collision between the 2 particles after the update ? 142 | if (dend < particles[i].radius + particles[j].radius) { 143 | // for how much distance is the particle within the other ? 144 | float din = particles[i].radius + particles[j].radius - dend; 145 | 146 | // dirend is the vector between the particles center 147 | PVector vmove = dirend.copy().mult(din/* * particles[i].velocity.mag()*/); 148 | if (vmove.mag() > MAX_COL_RESPONSE) { 149 | vmove.normalize(); 150 | vmove.mult(MAX_COL_RESPONSE); 151 | } 152 | particles[i].velocity.add(vmove); 153 | 154 | // this kinda emulates the transfer of energy 155 | particles[j].velocity.add(vmove.mult(-config.colTransferEnergy)); 156 | } 157 | } 158 | } 159 | } 160 | } 161 | 162 | // updates the particles position and angle, and draws them 163 | public void update () { 164 | // space transform to allow for pan / zoom 165 | PVector pan = panZoomManager.getPan(); 166 | pushMatrix(); 167 | translate(pan.x, pan.y); 168 | scale(panZoomManager.getScale()); 169 | 170 | // now we can update 171 | for (int i = 0; i < particles.length; i++) { 172 | if (!paused) { 173 | particles[i].update(); 174 | particles[i].friction(); 175 | } 176 | particles[i].draw(hovered == i, selected == i, true, true); 177 | } 178 | 179 | popMatrix(); 180 | } 181 | 182 | public void addParticle (float x, float y, ParticleAttractors attractors) { 183 | Particle[] nPts = new Particle[particles.length+1]; 184 | for (int i = 0; i < particles.length; i++) { 185 | nPts[i] = particles[i]; 186 | } 187 | nPts[nPts.length-1] = new Particle( 188 | x, 189 | y, 190 | random(PI*2f), 191 | 0.5, // mass 192 | config.spawnRadius, // radius 193 | 8, // magnetic force of the attraction points 194 | attractors 195 | ); 196 | particles = nPts; 197 | } 198 | 199 | public void deleteParticle (int idx) { 200 | Particle[] nPts = new Particle[particles.length-1]; 201 | int c = 0; 202 | for (int i = 0; i < particles.length; i++) { 203 | if (idx != i) { 204 | nPts[c++] = particles[i]; 205 | } 206 | } 207 | particles = nPts; 208 | } 209 | 210 | // just removes all the particles currently active 211 | public void reset () { 212 | particles = new Particle[0]; 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/sketch_atomic_clusters_explorer.pde: -------------------------------------------------------------------------------- 1 | /** 2 | * @author ciphrd 3 | * @license MIT 4 | * 5 | * Hey, just a quick note, if you fork this project and improve the tool, feel free to add your @pseudo 6 | * to the title of the window, in the setup() function. Let's have the community leave a trace :) 7 | * 8 | * This software was made to explore the Atomic Clusters system. 9 | * I wrote an article to explain the idea, the rules and the implementation behind this system: 10 | * 11 | * The architecture of this software is... approximative. Let's say it serves it purpose: just a fun 12 | * tool to see how the system works and what can be done with it. 13 | * 14 | * TODO: zoom (des push matrix consécustifs) 15 | **/ 16 | 17 | import java.util.Map; 18 | 19 | 20 | 21 | AttractorsManager attractorsManager; 22 | ParticlesManager pManager; 23 | AtomsUI atomsUI; 24 | Toolbar toolbar; 25 | SlidersPanel slidersPanel; 26 | ChargesPanel chargesPanel; 27 | PanelAtoms panelAtoms; 28 | PanelKeyboard panelKeyboard; 29 | 30 | PanZoomController panZoomManager; 31 | 32 | 33 | // so the events works in "cascade" 34 | // when the mouse is detected over a block, it sets this value to the ID that corresponds to the block 35 | // detection is applied from the UI the most in front to the last 36 | // if the mouse is detected at a point in the cascade, elements below won't perform their events 37 | // 0 corresponds to the element the most in the back 38 | // ------ 39 | // MAP of the UI 40 | // 0 - Particles world layer 41 | // 1 - Atoms bar on top 42 | // 2 - Bottom toolbar 43 | // 3 - Sliders layers 44 | // 4 - Charges editor 45 | // 5 - Atoms editor 46 | // 6 - Keyboard shortcuts 47 | int activeUI = 0; 48 | int activeUIEvents = 0; 49 | 50 | // to store informations used by the tooling 51 | int hovered = -1; 52 | int selected = -1; 53 | boolean paused = false; 54 | PImage pauseImg; 55 | int moved = -1; 56 | 57 | // useful global vars 58 | PVector mouse = new PVector(); 59 | PVector worldMouse = new PVector(); 60 | 61 | // the position of the camera 62 | PVector camera = new PVector(0, 0); 63 | 64 | void setup () { 65 | attractorsManager = new AttractorsManager(); 66 | pManager = new ParticlesManager(); 67 | 68 | panZoomManager = new PanZoomController(this); 69 | 70 | // we create the UI components 71 | atomsUI = new AtomsUI(); 72 | toolbar = new Toolbar(); 73 | slidersPanel = new SlidersPanel(); 74 | chargesPanel = new ChargesPanel(); 75 | panelAtoms = new PanelAtoms(); 76 | panelKeyboard = new PanelKeyboard(); 77 | 78 | 79 | // we setup the UI struff 80 | pauseImg = loadImage("pause.png"); 81 | PFont PTsans; 82 | PTsans = createFont("RobotoMono-Regular.ttf", 32); 83 | textFont(PTsans, 18); 84 | 85 | size(1280, 720); 86 | surface.setTitle("Atomic Clusters explorer - @ciphrd"); 87 | //surface.setResizable(true); 88 | } 89 | 90 | void draw () { 91 | // we reset 92 | background(0); 93 | activeUIEvents = 0; 94 | hovered = -1; 95 | 96 | mouse.set(mouseX, mouseY); 97 | worldMouse = panZoomManager.screenToWorld(mouse); 98 | handleEvents(); 99 | 100 | pManager.computeVelocities(); 101 | 102 | // we compute the collisions and then we update/draw the particles 103 | if (!paused) { 104 | // collision detection 105 | pManager.applyCollisions(); 106 | } 107 | pManager.update(); 108 | 109 | // we add rotation to the selected particle if r is pressed 110 | if (selected != -1 && keyPressed && key == 'r') { 111 | pManager.particles[selected].angle+= 0.2; 112 | } 113 | 114 | drawUI(); 115 | } 116 | 117 | 118 | void handleEvents () { 119 | // we need to execute this in the right order, from front in UI to back 120 | panelKeyboard.update(); 121 | panelAtoms.update(); 122 | chargesPanel.update(); 123 | slidersPanel.update(); 124 | toolbar.update(); 125 | atomsUI.update(); 126 | 127 | if (keyPressed && key == 'a' && atomsUI.selected != -1) { 128 | pManager.addParticle(worldMouse.x, worldMouse.y, attractorsManager.attractors[atomsUI.selected]); 129 | } 130 | } 131 | 132 | 133 | void drawUI () { 134 | atomsUI.draw(); 135 | toolbar.draw(); 136 | slidersPanel.draw(); 137 | chargesPanel.draw(); 138 | panelAtoms.draw(); 139 | panelKeyboard.draw(); 140 | if (paused) image(pauseImg, 15, height - 45, 32, 32); 141 | else { 142 | fill(255); 143 | textAlign(LEFT, BOTTOM); 144 | String fr = String.valueOf(frameRate); 145 | if (fr.length() > 4) fr = fr.substring(0, 4); 146 | text("v0.0.1", UI_PADDING, height-UI_PADDING); 147 | text(fr + " fps", UI_PADDING, height-UI_PADDING-18); 148 | text(pManager.particles.length + " atoms", UI_PADDING, height-UI_PADDING-36); 149 | } 150 | } 151 | 152 | 153 | 154 | void keyPressed () { 155 | if (key == ' ') { 156 | paused = !paused; 157 | } 158 | if (key == DELETE && selected != -1) { 159 | pManager.deleteParticle(selected); 160 | selected = -1; 161 | } 162 | if (key == ENTER) { 163 | pManager.reset(); 164 | } 165 | if (key == 'd') { 166 | selected = -1; 167 | } 168 | } 169 | 170 | void mousePressed () { 171 | // if a particle is being moved, we stop it 172 | moved = -1; 173 | 174 | chargesPanel.mousePressed(); 175 | toolbar.mousePressed(); 176 | atomsUI.mousePressed(); 177 | panelAtoms.mousePressed(); 178 | 179 | if (mouseButton == RIGHT && moved == -1) { 180 | moved = hovered; 181 | } 182 | } 183 | 184 | void mouseClicked () { 185 | if (activeUIEvents == 0) { 186 | if (moved == -1) { 187 | selected = hovered; 188 | } 189 | if (hovered == -1 && atomsUI.selected != -1) { 190 | pManager.addParticle(worldMouse.x, worldMouse.y, attractorsManager.attractors[atomsUI.selected]); 191 | } 192 | } 193 | } 194 | 195 | void mouseDragged () { 196 | if (activeUIEvents == 0) { 197 | panZoomManager.mouseDragged(); 198 | } 199 | } 200 | 201 | void mouseWheel (MouseEvent event) { 202 | panZoomManager.mouseWheel(-event.getCount()); 203 | } 204 | -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/toolbar.pde: -------------------------------------------------------------------------------- 1 | /** 2 | * UI Layer ID: 2 3 | * The toolbar is the component displayed in the bottom. 4 | * The buttons can open associated panels. 5 | * 6 | **/ 7 | 8 | 9 | PImage slidersImg; 10 | PImage atomsImg; 11 | PImage keyboardImg; 12 | PImage[] icons; 13 | 14 | int TOOLBAR_ITEM_WIDTH = 32; 15 | int TOOLBAR_MARGIN_ITEMS = 7; 16 | int TOOLBAR_BORDER_MARGIN = 10; 17 | 18 | // mapping of idx => ui layer id 19 | int[] TOOLBAR_LAYER_ID_MAP = new int[]{ 3, 5, 6 }; 20 | 21 | String[] TOOLBAR_INFOTEXT = new String[]{ 22 | "Simulation settings", 23 | "Create a new atom", 24 | "Keyboard shortcuts" 25 | }; 26 | 27 | 28 | class Toolbar { 29 | public int active; 30 | public PVector size; 31 | int hovered = -1; 32 | 33 | public Toolbar () { 34 | // we first load the icons 35 | slidersImg = loadImage("controls.png"); 36 | atomsImg = loadImage("atoms.png"); 37 | keyboardImg = loadImage("keyboard.png"); 38 | icons = new PImage[]{ slidersImg, atomsImg, keyboardImg }; 39 | active = -1; 40 | size = getToolbarSize(); 41 | } 42 | 43 | PVector getCircleCoord (int idx) { 44 | return new PVector( 45 | width - TOOLBAR_ITEM_WIDTH / 2 - TOOLBAR_BORDER_MARGIN - (TOOLBAR_ITEM_WIDTH + TOOLBAR_BORDER_MARGIN)*idx, 46 | height - TOOLBAR_ITEM_WIDTH / 2 - TOOLBAR_BORDER_MARGIN 47 | ); 48 | } 49 | 50 | PVector getIconCoord (int idx) { 51 | return new PVector( 52 | width - TOOLBAR_BORDER_MARGIN - 26 - (TOOLBAR_ITEM_WIDTH + TOOLBAR_BORDER_MARGIN)*idx, // because image is 20px, 32-20 = 12, to center 6 53 | height - TOOLBAR_BORDER_MARGIN - 26 54 | ); 55 | } 56 | 57 | PVector getToolbarSize () { 58 | return new PVector( 59 | (TOOLBAR_ITEM_WIDTH + TOOLBAR_MARGIN_ITEMS) * icons.length + TOOLBAR_BORDER_MARGIN * 2, 60 | TOOLBAR_ITEM_WIDTH + TOOLBAR_BORDER_MARGIN * 2 61 | ); 62 | } 63 | 64 | void mousePressed () { 65 | if (hovered != -1) { 66 | activeUI = activeUI == TOOLBAR_LAYER_ID_MAP[hovered] ? -1 : TOOLBAR_LAYER_ID_MAP[hovered]; 67 | } 68 | } 69 | 70 | public void update () { 71 | // reset stuff 72 | hovered = -1; 73 | 74 | // at first we detect if the mouse is in the toolbar 75 | if (activeUIEvents < 2 && mouse.x > width - size.x && mouse.y > height - size.y) { 76 | activeUIEvents = 2; 77 | // here we can test if the mouse is inside an item 78 | for (int i = 0; i < icons.length; i++) { 79 | PVector coord = getCircleCoord(i); 80 | if (mouse.copy().sub(coord).mag() <= TOOLBAR_ITEM_WIDTH/2 + 2) { 81 | hovered = i; 82 | break; 83 | } 84 | } 85 | } 86 | } 87 | 88 | public void draw () { 89 | fill(0, 0, 0, 180); 90 | 91 | // background 92 | stroke(255, 255, 255, 120); 93 | rect(width - size.x, height - size.y, size.x, size.y); 94 | 95 | if (hovered != -1) { 96 | fill(255); 97 | textAlign(RIGHT, BOTTOM); 98 | text(TOOLBAR_INFOTEXT[hovered], width - UI_PADDING, height - size.y - 3); 99 | } 100 | 101 | strokeWeight(2); 102 | noFill(); 103 | for (int i = 0; i < icons.length; i++) { 104 | PVector coord = getCircleCoord(i); 105 | 106 | // we set the color based on the state of the item 107 | if (TOOLBAR_LAYER_ID_MAP[i] == activeUI) stroke(0, 255, 0, 180); 108 | else if (hovered == i) stroke(255, 255, 0, 180); 109 | else stroke(255, 255, 255, 180); 110 | 111 | circle(coord.x, coord.y, TOOLBAR_ITEM_WIDTH); 112 | coord = getIconCoord(i); 113 | image(icons[i], coord.x, coord.y); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /sketch_atomic_clusters_explorer/utils.pde: -------------------------------------------------------------------------------- 1 | // just some utility functions 2 | 3 | void drawVector (PVector V, float x, float y, int[] col) { 4 | stroke(col[0], col[1], col[2], 90); 5 | strokeWeight(2); 6 | pushMatrix(); 7 | translate(x, y); 8 | float ang = atan2(V.y, V.x) + PI; 9 | line(0, 0, V.x, V.y); 10 | line(V.x, V.y, V.x + cos(ang+PI*.2) * 10, V.y + sin(ang+PI*.2) * 10); 11 | line(V.x, V.y, V.x + cos(ang-PI*.2) * 10, V.y + sin(ang-PI*.2) * 10); 12 | popMatrix(); 13 | } 14 | --------------------------------------------------------------------------------