0t1 steckt noch in den Kinderschuhen.

Dynamisches Sandkörnerspiel

Ein dynamisches Sandkörnerspiel wird mit Hilfe einer JavaScript Bibliothek in p5.js erstellt werden. Dabei soll durch die Leertaste die Zeichenfarbe der Sandkörner geändert werden können und über die Pfeiltasten die Richtung, in die der Sand fällt.

Sandkörnerspiel

Editor starten

Als erstes starten wir den p5-Editor, welchen du hier öffnen kannst: https://editor.p5js.org/. Dort wird dir folgender Code angezeigt:

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);
}

In der Funktion setup() wird deine Zeichenfläche erzeugt, auf welcher wir später unsere Sandkörner zeichnen und bewegen können. Hier werden Umgebungseigenschaften, wie Bildschirmgröße und Hintergrundfarbe definiert. Diese Funktion wird beim Programmstart einmalig aufgerufen.

Die Funktion draw() hingegen wird wiederholt ausgeführt bis das Programm gestoppt wird. In unserem Fall soll über diese Funktion kontinuierlich die Positionierung der Sandkörner in der Welt und die Position des Mauszeigers geprüft werden. Dazu aber später mehr.

Globale Variablen

let n = 64; //number of the collumns and rows
let s; //size of the cells in px
let world; //two-dimensional array that contains the color information of each individual cell
let palette; //color palette
let col = 0; //index of the current color
let backCol; //background color of the cells
let xDirection = 0; //direction of motion (horizontal)
let yDirection = 0; //direction of motion (vertical)

Die in den Kommentaren beschriebenen Funktionenen/ Werte werden teilweise erst im späteren Verlauf des Programmes zugewiesen.

initParams()-Funktion

//creates the color palette and the world
function initParams() {
  let palettes = [
["#751212", "#ba821e", "#a55200", "#a33d3d", "#ffd385", "#fcf4ae", "#ffec00"],
["#1a1a1a", "#4d4d4d", "#808080", "#b3b3b3", "#cccccc", "#f2f2f2", "#ffffff"],
["#0b1a1a", "#560719", "#a4161b", "#e5473b", "#b1a6a5", "#f5f3f4", "#ffffff"],
["#89b574", "#d4e56e", "#c5ffe6", "#a0d1f9", "#bdc5ff", "#ffff8d", "#c8faa5"],
["#ffc5fa", "#bdcfff", "#a0dfff", "#b636c1", "#667ded", "#aa16df", "#ea75d4"],
["#ffb3b3", "#ffc5fa", "#c5ffe6", "#bdc5ff", "#ffd386", "#d4e56e", "#fcf4ae"],
["#04162d", "#6b140e", "#8c0217", "#d00000", "#e76d03", "#fba418", "#feba17"],
["#132924", "#21461c", "#4e884d", "#90a955", "#ecf28e", "#edf0d5", "#f7f7fc"]
];
  
  let c = round(random(0,7));
  palette = palettes[c]; //randomly picks a color palette from the array palettes
  shuffle(palette, true); //randomizes the order of the colors in the chosen palette
  
  backCol = palette.pop();
  world = [];
  for (let i = 0; i < n; i++) {
    let line = [];
    for (let j = 0; j < n; j++) {
      line.push(backCol);
    }
    world.push(line);
  }
}

Hier wird die Farbpalette, die Hintergrundfabe und die Zeichenfläche initialisiert. Im Feld palettes sind die verschiedenen Farbpaletten enthalten. Die Variable c, welche zufällig generiert wird gibt die aus palettes ausgewählte Farbpalette in die Variable palette. Die shuffle()-Funktion ordnet die Elemente im Feld palette in einer zufälligen Reihenfolge an. Daraufhin wird in der Variable blackCol die Farbinformation der Hintergrundfarbe gespeichert. Durch die Funktion pop() wird dafür die letzte Farbe in der sortierten Palette verwendet. Zuletzt wird die Welt definiert. Bei der Welt handelt es sich um ein zweidimensionales Feld, welches durch die for-Schleife mit der Hintergrundfarbe befüllt wird. Die Größe des Feldes world wird dabei durch n beschrieben.

setup()-Funktion

//creates canvas
function setup() {
  createCanvas(500, 500);
  noStroke();
  s = width / n;
  initParams();
}

Die Zeichenfläche soll 500 x 500 px groß sein. noStroke() wird hinzugefügt, da sonst jede Zelle eine Umrandung hätte. s beschreibt die Größe jeder Zelle in Pixel und kann erst definiert werden, wenn die Größe des Canvas definiert wurde, deshalb wird dies hier definiert und nicht bei den globale Variablen. Die initPrams()-Funktion wird aufgerufen. Somit ist unsere Zeichenfläche vorbereitet, nun geht es ans Zeichnen.

oneStep()-Funktion

function oneStep() {
  
  //if-loop goes through the world from the top right to bottom left and shifts the grains of sand down/left
  if (yDirection === 1 || xDirection === -1) {
    for (let i = n - 1; i > -1; i--) {
      for (let j = 0; j < n; j++) {
        let v = world[i][j];
        if (v != backCol) {
          let i_new = i + yDirection;
          let j_new = j + xDirection;
          if (
            i_new < n &&
            i_new > -1 &&
            j_new < n &&
            j_new > -1 &&
            world[i_new][j_new] == backCol
          ) {
            world[i][j] = backCol;
            world[i_new][j_new] = v;
          }
        }
      }
    }
    
  //else-loop goes through the world from the bottom left to top right and shifts the grains of sand up/right
  } else {
    // Richtung nach oben oder rechts
    for (let i = 0; i < n; i++) {
      for (let j = n - 1; j > -1; j--) {
        let v = world[i][j];
        if (v != backCol) {
          let i_new = i + yDirection;
          let j_new = j + xDirection;
          if (
            i_new < n &&
            i_new > -1 &&
            j_new < n &&
            j_new > -1 &&
            world[i_new][j_new] == backCol
          ) {
            world[i][j] = backCol;
            world[i_new][j_new] = v;
          }
        }
      }
    }
  }
}

Zur einfacheren Erklärung dieser Funktion gehen wir davon aus, dass die Sandkörner von oben nach unten fallen sollen (yDirection === 1). Allgemein gesagt geht diese Funktion nun die Welt von oben nach unten durch und verschiebt die gezeichneten Sandkörner, wenn möglich, nach unten. Sie überprüft jede einzelne Zelle auf ihre Farbinformation v und gleicht diese mit der Hintergrundfarbe der Zeichenfläche backCol ab. Stimmt die Farbe überein, so wird die Zählschleife weitergezählt und die nächste Zelle überprüft. Entspricht die Farbe allerdings nicht der Hintergrundfarbe, so wird die darunterliegende Zelle auf ihre Farbinformation geprüft. Entspricht diese ebenfalls nicht der Hintergrundfarbe kann das Sandkorn nicht weiter herunterfallen, weil es schon am „Boden“ angekommen ist. Ist die darunterliegende Zelle jedoch noch in der Hintergrundfarbe eingefärbt, so soll das Sandkorn nach unten fallen, bis es entweder auf die Begrenzung der Zeichenfläche oder ein anderes gezeichnetes Sandkorn fällt.

Im nächsten Schritt wird also die Farbinformation der oberen Zelle wieder auf die Hintergrundfarbe backCol gesetzt. Die untere Zelle bekommt zu Farbinformation v der oberen Zelle zugewiesen. Die if-Schleife geht die Welt von oben rechts nach unten links durch und verschiebt die Sandkörner nach unten/ links, wenn möglich. Die else-Schleife hingegen geht die Welt von unten links nach oben rechts durch und verschiebt die Sandkörner nach oben/ rechts, wenn möglich.

drawWorld()-Funktion

//draws the individual cells in the world
function drawWorld() {
  for (let i = 0; i < n; i++) {
    for (let j = 0; j < n; j++) {
      fill(world[i][j]);
      square(j * s, i * s, s);
    }
  }
}

Bisher weisen wir durch die oneStep()-Funktion jeder Zelle wiederholt ihre Farbinformationen zu. Diese ist jedoch noch nicht auf der Zeichenfläche sichtbar. Hier kommt die drawWorld()-Funktion ins Spiel. Diese zeichnet die einzelnen Zellen in der Welt und geht die Welt von oben links nach unten rechts durch und zeichnet n^2 viele Quadrate auf der Zeichenfläche. Dabei betrachtet das Programm jede Zelle einzeln, überprüft sie auf ihre Farbinformation und setzt in dieser Farbe ein Quadrat.

draw()-Funktion

function draw() {
  oneStep();  
  drawWorld();

  if ( //checks if the mouse pointer is on the drawing area
    mouseX > -1 &&
    mouseY > -1 &&
    mouseX < width &&
    mouseY < height &&
    !keyIsPressed
  ) {
    let i = floor((n * mouseY) / height); 
    let j = floor((n * mouseX) / width);
    
    //by a mouse click the color of the cell, that was clicked on changed
    if (mouseIsPressed) world[i][j] = palette[col % palette.length]; 
    
    //visual indicator of the mouse position on the canvas
    fill(palette[col % palette.length]);
    square(j * s, i * s, s);
  }
}

Diese Funktion ruft die oneStep()-Funktion auf und gibt jeder Zelle ihre Farbinformation. Sie ruft die draw-World()-Funktion auf und zeichnet jede Zelle mit der in oneStep() zugewiesenen Farbfunktion. Anschließend wird überprüft, ob sich die Maus auf dem Canvas befindet. Die Bedingung !keyIsPressed verhindert, dass während Farb- oder Richtungsänderungen gezeichnet werden kann. Die Zeichenfunktion ist somit erst nach Abschluss dieser Änderungen wieder verfügbar. Somit ist es zu jeder Zeit eindeutig, mit welcher Farbe und in welche Richtung gezeichnet werden soll. Anschließend wird die genaue Position der Maus auf der Zeichenfläche ermittelt. i bestimmt den Index der Zeile, und j den Index der Spalte in der sich die Maus befindet. Durch einen Mausklick wird die Farbe der Zelle auf die geklickt wurde geändert. Um einen visuellen Indikator für die Mausposition auf dem Canvas zu erhalten wird mit square() ein temporärers Quadrat an der aktuellen Mausposition mit der aktuellen Füllfarbe, welche durch fill() gesetzt wird, gezeichnet. Dieser Indikator ist wichtig, um zum einen die Orientierung auf dem Canvas zu gewährleisten und zum anderen die Funktionsweise der Anwendung verständlich zu visualisieren.

keyPressed()-Funktion

function keyPressed() {
  if (key == " ") { //changes the drawing color
    col++;
  }
  if (keyCode == DELETE) {
    n = round(random(10,60)); //randomizes the number of the collumns and rows
    setup();
  }
  if (keyCode == UP_ARROW) { //grains move upwards
    yDirection = -1;
    xDirection = 0;
  }
  if (keyCode == DOWN_ARROW) { //grains move downwards
    yDirection = 1;
    xDirection = 0;
  }
  if (keyCode == LEFT_ARROW) { //grains move left
    yDirection = 0;
    xDirection = -1;
  }
  if (keyCode == RIGHT_ARROW) { //grains move right
    yDirection = 0;
    xDirection = 1;
  }
  if(key === "s"){ //saves the canvas as a jpg
    save("myCanvas.jpg");
  }
}

Diese Funktion macht die Anwendung interaktiv und flexibel. Die Leertaste zählt col, also den Index der aktuellen Farbpalette hoch und wechselt somit die Zeichenfarbe. DELETE gibt n einen zufälligen Wert und ruft die setup()-Funktion auf. Dadurch wird ein neuer Canvas erstellt mit einem neuen Wert für die Variable n . Da in der setup()-Funktion die initParams()-Funktion enthalten ist wird auch die Farbpalette neu ausgewählt. Die letzten vier Schleifen ändern jeweils die Bewegungsrichtung der Sandkörner. Je nachdem welche Werte xDirection und yDirection besitzen fallen die Sandkörner durch die oneStep()-Funktion in die gwünschte Richtung. Anschließend kann man die Zeichenfläche als jpg-Datei speichern.

Zusatztipp

Setzt du bei den globalen Variablen sowohl xDirection als auch yDirection auf 0 kannst du zu Beginn die Sandkörner frei auf der Zeichenfläche platzieren und anschließend mit Drücken der Pfeiltasten in sich zusammenfallen lassen. So kannst du noch interessantere Bilder kreieren und es ist auch sehr spannend anzusehen.

Quellcode

Hier findest du unseren Quellcode und unsere Inspiration:

https://editor.p5js.org/schindler/sketches/fcFB9p2Nu

https://openprocessing.org/sketch/1442701

Viel Spaß beim Zeichnen!


© 0t1

Cookies

0t1 mag keine Kekse (und kein Tracking). Wir verwenden lediglich notwendige Cookies für essentielle Funktionen.

Wir verwenden Schriftarten von Adobe Fonts. Dafür stellt dein Browser eine Verbindung zu den Servern von Adobe in den USA her. Wenn du unsere Seite nutzen möchtest, musst du dich damit einverstanden erklären.

Weitere Informationen in unserer Datenschutzerklärung.