Dopasowywanie fragmentu generowanego proceduralnie świata do fragmentu innego świata

18

Czy czytałeś Kroniki bursztynu Rogera Zelaznego?

Wyobraź sobie, że grasz w trzeciej grze MMO. Odradzasz się na świecie i zaczynasz błąkać. Po pewnym czasie, gdy myślisz, że nauczyłeś się mapy, zdajesz sobie sprawę, że jesteś w miejscu, którego nigdy wcześniej nie widziałeś. Wracasz do ostatniego miejsca, które na pewno znasz i nadal tam jest. Ale reszta świata się zmieniła i nawet nie zauważyłeś, jak to się stało.

Czytałem o proceduralnym generowaniu świata. Czytałem o hałasie i oktawach Perlina, hałasie Simplex, algorytmie kwadratu diamentowego, o symulacji płyt tektonicznych i erozji wodnej. Uważam, że mam niejasne zrozumienie ogólnego podejścia w proceduralnym generowaniu świata.

I z tą wiedzą nie mam pojęcia, jak możesz zrobić coś takiego, jak napisano powyżej. Każdy pomysł, który przychodzi mi do głowy, napotyka pewne teoretyczne problemy. Oto kilka pomysłów, które mogę wymyślić:

1) „Odwracalna” generacja świata z numerem początkowym jako danymi wejściowymi i pewną w pełni opisującą liczbą fragmentów

Wątpię, czy jest to możliwe, ale wyobrażam sobie funkcję, która otrzyma ziarno i wytworzy macierz liczb, na której zbudowane są fragmenty. I dla każdego unikalnego numeru jest unikalny fragment. I druga funkcja, która pobiera ten unikalny numer porcji i generuje ziarno, które zawiera tę liczbę. Próbowałem zrobić schemat na poniższym obrazku:

wprowadź opis zdjęcia tutaj

2) Tworzenie kawałków całkowicie losowych i przejście między nimi.

Jak sugerował Aracthor . Zaletą tego podejścia jest to, że jest to możliwe i nie wymaga magicznej funkcji :)

Wadą tego podejścia jest moim zdaniem to, że prawdopodobnie nie można mieć zróżnicowanego świata. Jeśli powiedzmy, że zarówno archipelag, jak i kontynent reprezentowany przez tylko jedną liczbę i przylegające do niego fragmenty, to wielkość fragmentu nie byłaby równa kontynentowi. I wątpię, aby można było dobrze przechodzić między kawałkami. Czy coś brakuje?

Innymi słowy, rozwijasz MMO ze światem generowanym proceduralnie. Ale zamiast jednego świata masz ich wiele . Jakie podejście przyjęlibyście do generowania światów i jak wprowadzilibyście przejście gracza z jednego świata do drugiego, nie zauważając przejścia gracza

W każdym razie uważam, że masz ogólny pomysł. Jak byś to zrobił?

Netaholic
źródło
Mam więc problemy z odpowiedziami tutaj. @Aracthor Wcześniej rozmawiałem z tobą o płynnych rozmaitościach, ten rodzaj dotyczy tutaj. Są jednak 2 dość wysokie odpowiedzi, więc zastanawiam się, czy jest sens ...
Alec Teal
@AlecTeal, jeśli masz coś do dodania, zrób. Z przyjemnością usłyszę wszelkie pomysły i sugestie.
netaholic

Odpowiedzi:

23

Użyj wycinka szumu wyższego rzędu. Jeśli wcześniej używałeś szumu 2d do mapy wysokości, użyj szumu 3D z ustaloną ostatnią współrzędną. Teraz możesz powoli zmieniać pozycję w ostatnim wymiarze, aby zmodyfikować teren. Ponieważ szum Perlina jest ciągły we wszystkich wymiarach, przejścia będą płynne, dopóki płynnie zmienisz pozycję, w której próbkujesz funkcję szumu.

Jeśli chcesz zmienić teren daleko od odległości do gracza jako przesunięcie, na przykład. Możesz także zapisać przesunięcie dla każdej współrzędnej na mapie i zwiększać, ale nigdy nie zmniejszać. W ten sposób mapa staje się nowsza, ale nigdy się nie starzeje.

Ten pomysł działa również, jeśli już używasz szumu 3D, po prostu próbkuj wtedy z 4D. Spójrz także na hałas Simplex. To ulepszona wersja hałasu Perlina i działa lepiej dla większej liczby wymiarów.

danijar
źródło
2
To jest interesujące. Czy rozumiem poprawnie, że sugerujesz generowanie szumu 3d, użyj wycinka xy w pewnym jego z jako mapy wysokości i wykonuj płynne przejście do innego wycinka, zmieniając współrzędną z wraz ze wzrostem odległości od gracza?
netaholic
@netaholic Dokładnie. Opisanie go jako plastra jest bardzo dobrą intuicją. Ponadto możesz śledzić najwyższą wartość ostatniej współrzędnej wszędzie na mapie i tylko ją zwiększać, ale nigdy jej nie zmniejszać.
danijar
1
To genialny pomysł. Zasadniczo mapa terenu byłaby parabolicznym (lub inną krzywą) przekrojem przez objętość 3D.
Fałszywe imię
To naprawdę sprytny pomysł.
user253751
5

Twój pomysł na podzielenie świata na kilka części nie jest zły. To jest po prostu niekompletne.

Jedynym problemem są skrzyżowania między porcjami. Na przykład, jeśli użyjesz szumu perlin, aby wygenerować ulgę, i nasiona inne dla każdej części i ryzykujesz, że tak się stanie:

Błąd usuwania fragmentów

Rozwiązaniem byłoby wygenerowanie odciążenia kawałków nie tylko z nasion szumu Perlina, ale także z innych kawałków wokół niego.

Algorytm Perlina wykorzystuje wartości losowej mapy wokół siebie, aby się „wygładzić”. Jeśli użyją wspólnej mapy, zostaną wygładzone razem.

Jedyny problem polega na tym, że jeśli zmienisz ziarno, aby było inaczej, gdy gracz się odsunie, będziesz musiał ponownie załadować kawałki, ponieważ ich granice również powinny się zmienić.

Nie zmieniłoby to wielkości fragmentów, ale zwiększyłoby minimalną odległość od gracza do załadowania / rozładowania, ponieważ fragment musi zostać załadowany, gdy gracz go zobaczy, a przy tej metodzie, ponieważ sąsiednie fragmenty muszą być zbyt .

AKTUALIZACJA:

Jeśli każda część twojego świata jest innego typu, problem rośnie. Nie chodzi tylko o ulgę. Kosztownym rozwiązaniem byłoby:

Kawałki cięte

Załóżmy, że zielone fragmenty to światy leśne, archipelagi niebieskie, a żółte płaskie pustynie.
Rozwiązaniem tutaj jest utworzenie stref „przejściowych”, w których twoja ulga i natura ziemi (a także obiekty uziemione lub cokolwiek innego chcesz) stopniowo zmieniałyby się z jednego rodzaju na inny.

I jak widać na tym obrazie, piekła część do kodowania będą małe kwadraty w rogach porcji: muszą utworzyć połączenie między 4 porcjami, potencjalnie różnymi naturami.

Dlatego na tym poziomie złożoności uważam, że nie można używać klasycznych generacji światów 2D, takich jak Perlin2D . Polecam ci odpowiedź na @danijar.

Araktor
źródło
Czy sugerujesz wygenerować „środek” kawałka z nasion, a jego krawędzie „wygładzić” w oparciu o sąsiednie kawałki? Ma to sens, ale zwiększy rozmiar fragmentu, ponieważ powinien to być rozmiar obszaru, który gracz może obserwować plus podwójna szerokość obszaru przejścia do sąsiednich fragmentów. Obszar jest coraz większy, im bardziej różnorodny jest świat.
netaholic
@netaholic To nie byłoby większe, ale w pewnym sensie. Dodałem do tego akapit.
Aracthor
Zaktualizowałem moje pytanie. Próbowałem opisać niektóre moje pomysły
netaholic
Tak więc inna odpowiedź tutaj wykorzystuje (w pewnym sensie, niezupełnie) trzeci wymiar jako wykresy. Również postrzegasz samolot jako różnorodny i podobają mi się twoje pomysły. Aby rozszerzyć go nieco dalej, naprawdę potrzebujesz płynnego rozdzielacza. Musisz upewnić się, że przejścia są płynne. Możesz wtedy zastosować rozmycie lub szum, a odpowiedź byłaby idealna.
Alec Teal
0

Chociaż pomysł Danijara jest dość solidny, możesz skończyć z przechowywaniem dużej ilości danych, jeśli chcesz mieć taki sam obszar lokalny i przesunięcie odległości. I żądając coraz większej liczby coraz bardziej złożonych dźwięków. Możesz je wszystkie uzyskać w bardziej standardowy sposób 2D.

Opracowałem algorytm do generowania proceduralnego losowego szumu fraktalnego, częściowo oparty na algorytmie kwadratu diamentowego, który ustaliłem jako nieskończony i deterministyczny. Diament-kwadrat może więc stworzyć nieskończony krajobraz, a także mój własny dość blokowy algorytm.

Pomysł jest w zasadzie taki sam. Ale zamiast próbkować szumy o wyższych wymiarach, możesz iterować wartości na różnych poziomach iteracji.

Więc nadal przechowujesz wartości, o które wcześniej prosiłeś, i buforujesz je (ten schemat można by niezależnie wykorzystać do przyspieszenia już bardzo szybkiego algorytmu). A kiedy żądany jest nowy obszar, tworzony jest z nową wartością y. a każdy obszar, który nie jest wymagany w tym żądaniu, jest usuwany.

Zamiast szukać w różnych przestrzeniach w dodatkowych wymiarach. Przechowujemy dodatkowy bit danych monotonicznych w celu ich mieszania w różnych (stopniowo coraz większych ilościach na różnych poziomach).

Jeśli użytkownik porusza się w kierunku, wartości są odpowiednio przesuwane (i na każdym poziomie), a nowe wartości są generowane na nowych krawędziach. Jeśli zostanie zmienione najwyższe iteracyjne ziarno, cały świat ulegnie drastycznej zmianie. Jeśli końcowa iteracja otrzyma inny wynik, wówczas kwota zmiany będzie bardzo niewielka + 1 blok mniej więcej. Ale wzgórze nadal tam będzie, dolina itp., Ale zakątki i zakamarki zmienią się. Chyba że pójdziesz wystarczająco daleko, a wtedy wzgórze zniknie.

Więc jeśli przechowujemy 100x100 fragmentu wartości dla każdej iteracji. Wtedy nic nie może się zmienić w odtwarzaczu przy 100 x 100. Ale przy 200 x 200 rzeczy mogą się zmienić o 1 blok. Przy 400 x 400 rzeczy mogą się zmienić o 2 bloki. W odległości 800 x 800 rzeczy będą mogły się zmienić o 4 bloki. Więc rzeczy się zmienią i będą się zmieniać coraz bardziej, im dalej pójdziesz. Jeśli wrócisz, będą się różnić, jeśli pójdziesz za daleko, zostaną całkowicie zmienione i całkowicie utracone, ponieważ wszystkie nasiona zostaną porzucone.

Dodanie innego wymiaru, aby zapewnić ten efekt stabilizujący, z pewnością zadziałałoby, przesuwając y na odległość, ale będziesz przechowywać dużo danych dla bardzo wielu bloków, kiedy nie powinieneś. W deterministycznych algorytmach szumu fraktalnego można uzyskać ten sam efekt, dodając zmieniającą się wartość (w innej wysokości), gdy pozycja przesuwa się poza określony punkt.

https://jsfiddle.net/rkdzau7o/

var SCALE_FACTOR = 2;
//The scale factor is kind of arbitrary, but the code is only consistent for 2 currently. Gives noise for other scale but not location proper.
var BLUR_EDGE = 2; //extra pixels are needed for the blur (3 - 1).
var buildbuffer = BLUR_EDGE + SCALE_FACTOR;

canvas = document.getElementById('canvas');
ctx = canvas.getContext("2d");
var stride = canvas.width + buildbuffer;
var colorvalues = new Array(stride * (canvas.height + buildbuffer));
var iterations = 7;
var xpos = 0;
var ypos = 0;
var singlecolor = true;


/**
 * Function adds all the required ints into the ints array.
 * Note that the scanline should not actually equal the width.
 * It should be larger as per the getRequiredDim function.
 *
 * @param iterations Number of iterations to perform.
 * @param ints       pixel array to be used to insert values. (Pass by reference)
 * @param stride     distance in the array to the next requestedY value.
 * @param x          requested X location.
 * @param y          requested Y location.
 * @param width      width of the image.
 * @param height     height of the image.
 */

function fieldOlsenNoise(iterations, ints, stride, x, y, width, height) {
  olsennoise(ints, stride, x, y, width, height, iterations); //Calls the main routine.
  //applyMask(ints, stride, width, height, 0xFF000000);
}

function applyMask(pixels, stride, width, height, mask) {
  var index;
  index = 0;
  for (var k = 0, n = height - 1; k <= n; k++, index += stride) {
    for (var j = 0, m = width - 1; j <= m; j++) {
      pixels[index + j] |= mask;
    }
  }
}

/**
 * Converts a dimension into the dimension required by the algorithm.
 * Due to the blurring, to get valid data the array must be slightly larger.
 * Due to the interpixel location at lowest levels it needs to be bigger by
 * the max value that can be. (SCALE_FACTOR)
 *
 * @param dim
 * @return
 */

function getRequiredDim(dim) {
  return dim + BLUR_EDGE + SCALE_FACTOR;
}

//Function inserts the values into the given ints array (pass by reference)
//The results will be within 0-255 assuming the requested iterations are 7.
function olsennoise(ints, stride, x_within_field, y_within_field, width, height, iteration) {
  if (iteration == 0) {
    //Base case. If we are at the bottom. Do not run the rest of the function. Return random values.
    clearValues(ints, stride, width, height); //base case needs zero, apply Noise will not eat garbage.
    applyNoise(ints, stride, x_within_field, y_within_field, width, height, iteration);
    return;
  }

  var x_remainder = x_within_field & 1; //Adjust the x_remainder so we know how much more into the pixel are.
  var y_remainder = y_within_field & 1; //Math.abs(y_within_field % SCALE_FACTOR) - Would be assumed for larger scalefactors.

  /*
  Pass the ints, and the stride for that set of ints.
  Recurse the call to the function moving the x_within_field forward if we actaully want half a pixel at the start.
  Same for the requestedY.
  The width should expanded by the x_remainder, and then half the size, with enough extra to store the extra ints from the blur.
  If the width is too long, it'll just run more stuff than it needs to.
  */

  olsennoise(ints, stride,
    (Math.floor((x_within_field + x_remainder) / SCALE_FACTOR)) - x_remainder,
    (Math.floor((y_within_field + y_remainder) / SCALE_FACTOR)) - y_remainder,
    (Math.floor((width + x_remainder) / SCALE_FACTOR)) + BLUR_EDGE,
    (Math.floor((height + y_remainder) / SCALE_FACTOR)) + BLUR_EDGE, iteration - 1);

  //This will scale the image from half the width and half the height. bounds.
  //The scale function assumes you have at least width/2 and height/2 good ints.
  //We requested those from olsennoise above, so we should have that.

  applyScaleShift(ints, stride, width + BLUR_EDGE, height + BLUR_EDGE, SCALE_FACTOR, x_remainder, y_remainder);

  //This applies the blur and uses the given bounds.
  //Since the blur loses two at the edge, this will result
  //in us having width requestedX height of good ints and required
  // width + blurEdge of good ints. height + blurEdge of good ints.
  applyBlur(ints, stride, width + BLUR_EDGE, height + BLUR_EDGE);

  //Applies noise to all the given ints. Does not require more or less than ints. Just offsets them all randomly.
  applyNoise(ints, stride, x_within_field, y_within_field, width, height, iteration);
}



function applyNoise(pixels, stride, x_within_field, y_within_field, width, height, iteration) {
  var bitmask = 0b00000001000000010000000100000001 << (7 - iteration);
  var index = 0;
  for (var k = 0, n = height - 1; k <= n; k++, index += stride) { //iterate the requestedY positions. Offsetting the index by stride each time.
    for (var j = 0, m = width - 1; j <= m; j++) { //iterate the requestedX positions through width.
      var current = index + j; // The current position of the pixel is the index which will have added stride each, requestedY iteration
      pixels[current] += hashrandom(j + x_within_field, k + y_within_field, iteration) & bitmask;
      //add on to this pixel the hash function with the set reduction.
      //It simply must scale down with the larger number of iterations.
    }
  }
}

function applyScaleShift(pixels, stride, width, height, factor, shiftX, shiftY) {
  var index = (height - 1) * stride; //We must iteration backwards to scale so index starts at last Y position.
  for (var k = 0, n = height - 1; k <= n; n--, index -= stride) { // we iterate the requestedY, removing stride from index.
    for (var j = 0, m = width - 1; j <= m; m--) { // iterate the requestedX positions from width to 0.
      var pos = index + m; //current position is the index (position of that scanline of Y) plus our current iteration in scale.
      var lower = (Math.floor((n + shiftY) / factor) * stride) + Math.floor((m + shiftX) / factor); //We find the position that is half that size. From where we scale them out.
      pixels[pos] = pixels[lower]; // Set the outer position to the inner position. Applying the scale.
    }
  }
}

function clearValues(pixels, stride, width, height) {
  var index;
  index = 0;
  for (var k = 0, n = height - 1; k <= n; k++, index += stride) { //iterate the requestedY values.
    for (var j = 0, m = width - 1; j <= m; j++) { //iterate the requestedX values.
      pixels[index + j] = 0; //clears those values.
    }
  }
}

//Applies the blur.
//loopunrolled box blur 3x3 in each color.
function applyBlur(pixels, stride, width, height) {
  var index = 0;
  var v0;
  var v1;
  var v2;

  var r;
  var g;
  var b;

  for (var j = 0; j < height; j++, index += stride) {
    for (var k = 0; k < width; k++) {
      var pos = index + k;

      v0 = pixels[pos];
      v1 = pixels[pos + 1];
      v2 = pixels[pos + 2];

      r = ((v0 >> 16) & 0xFF) + ((v1 >> 16) & 0xFF) + ((v2 >> 16) & 0xFF);
      g = ((v0 >> 8) & 0xFF) + ((v1 >> 8) & 0xFF) + ((v2 >> 8) & 0xFF);
      b = ((v0) & 0xFF) + ((v1) & 0xFF) + ((v2) & 0xFF);
      r = Math.floor(r / 3);
      g = Math.floor(g / 3);
      b = Math.floor(b / 3);
      pixels[pos] = r << 16 | g << 8 | b;
    }
  }
  index = 0;
  for (var j = 0; j < height; j++, index += stride) {
    for (var k = 0; k < width; k++) {
      var pos = index + k;
      v0 = pixels[pos];
      v1 = pixels[pos + stride];
      v2 = pixels[pos + (stride << 1)];

      r = ((v0 >> 16) & 0xFF) + ((v1 >> 16) & 0xFF) + ((v2 >> 16) & 0xFF);
      g = ((v0 >> 8) & 0xFF) + ((v1 >> 8) & 0xFF) + ((v2 >> 8) & 0xFF);
      b = ((v0) & 0xFF) + ((v1) & 0xFF) + ((v2) & 0xFF);
      r = Math.floor(r / 3);
      g = Math.floor(g / 3);
      b = Math.floor(b / 3);
      pixels[pos] = r << 16 | g << 8 | b;
    }
  }
}


function hashrandom(v0, v1, v2) {
  var hash = 0;
  hash ^= v0;
  hash = hashsingle(hash);
  hash ^= v1;
  hash = hashsingle(hash);
  hash ^= v2;
  hash = hashsingle(hash);
  return hash;
}

function hashsingle(v) {
  var hash = v;
  var h = hash;

  switch (hash & 3) {
    case 3:
      hash += h;
      hash ^= hash << 32;
      hash ^= h << 36;
      hash += hash >> 22;
      break;
    case 2:
      hash += h;
      hash ^= hash << 22;
      hash += hash >> 34;
      break;
    case 1:
      hash += h;
      hash ^= hash << 20;
      hash += hash >> 2;
  }
  hash ^= hash << 6;
  hash += hash >> 10;
  hash ^= hash << 8;
  hash += hash >> 34;
  hash ^= hash << 50;
  hash += hash >> 12;
  return hash;
}


//END, OLSEN NOSE.



//Nuts and bolts code.

function MoveMap(dx, dy) {
  xpos -= dx;
  ypos -= dy;
  drawMap();
}

function drawMap() {
  //int iterations, int[] ints, int stride, int x, int y, int width, int height
  console.log("Here.");
  fieldOlsenNoise(iterations, colorvalues, stride, xpos, ypos, canvas.width, canvas.height);
  var img = ctx.createImageData(canvas.width, canvas.height);

  for (var y = 0, h = canvas.height; y < h; y++) {
    for (var x = 0, w = canvas.width; x < w; x++) {
      var standardShade = colorvalues[(y * stride) + x];
      var pData = ((y * w) + x) * 4;
      if (singlecolor) {
        img.data[pData] = standardShade & 0xFF;
        img.data[pData + 1] = standardShade & 0xFF;
        img.data[pData + 2] = standardShade & 0xFF;
      } else {
        img.data[pData] = standardShade & 0xFF;
        img.data[pData + 1] = (standardShade >> 8) & 0xFF;
        img.data[pData + 2] = (standardShade >> 16) & 0xFF;
      }
      img.data[pData + 3] = 255;
    }
  }
  ctx.putImageData(img, 0, 0);
}

$("#update").click(function(e) {
  iterations = parseInt($("iterations").val());
  drawMap();
})
$("#colors").click(function(e) {
  singlecolor = !singlecolor;
  drawMap();
})

var m = this;
m.map = document.getElementById("canvas");
m.width = canvas.width;
m.height = canvas.height;

m.hoverCursor = "auto";
m.dragCursor = "url(data:image/vnd.microsoft.icon;base64,AAACAAEAICACAAcABQAwAQAAFgAAACgAAAAgAAAAQAAAAAEAAQAAAAAAAAEAAAAAAAAAAAAAAgAAAAAAAAAAAAAA////AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8AAAA/AAAAfwAAAP+AAAH/gAAB/8AAAH/AAAB/wAAA/0AAANsAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//////////////////////////////////////////////////////////////////////////////////////gH///4B///8Af//+AD///AA///wAH//+AB///wAf//4AH//+AD///yT/////////////////////////////8=), default";
m.scrollTime = 300;

m.mousePosition = new Coordinate;
m.mouseLocations = [];
m.velocity = new Coordinate;
m.mouseDown = false;
m.timerId = -1;
m.timerCount = 0;

m.viewingBox = document.createElement("div");
m.viewingBox.style.cursor = m.hoverCursor;

m.map.parentNode.replaceChild(m.viewingBox, m.map);
m.viewingBox.appendChild(m.map);
m.viewingBox.style.overflow = "hidden";
m.viewingBox.style.width = m.width + "px";
m.viewingBox.style.height = m.height + "px";
m.viewingBox.style.position = "relative";
m.map.style.position = "absolute";

function AddListener(element, event, f) {
  if (element.attachEvent) {
    element["e" + event + f] = f;
    element[event + f] = function() {
      element["e" + event + f](window.event);
    };
    element.attachEvent("on" + event, element[event + f]);
  } else
    element.addEventListener(event, f, false);
}

function Coordinate(startX, startY) {
  this.x = startX;
  this.y = startY;
}

var MouseMove = function(b) {
  var e = b.clientX - m.mousePosition.x;
  var d = b.clientY - m.mousePosition.y;
  MoveMap(e, d);
  m.mousePosition.x = b.clientX;
  m.mousePosition.y = b.clientY;
};

/**
 * mousedown event handler
 */
AddListener(m.viewingBox, "mousedown", function(e) {
  m.viewingBox.style.cursor = m.dragCursor;

  // Save the current mouse position so we can later find how far the
  // mouse has moved in order to scroll that distance
  m.mousePosition.x = e.clientX;
  m.mousePosition.y = e.clientY;

  // Start paying attention to when the mouse moves
  AddListener(document, "mousemove", MouseMove);
  m.mouseDown = true;

  event.preventDefault ? event.preventDefault() : event.returnValue = false;
});

/**
 * mouseup event handler
 */
AddListener(document, "mouseup", function() {
  if (m.mouseDown) {
    var handler = MouseMove;
    if (document.detachEvent) {
      document.detachEvent("onmousemove", document["mousemove" + handler]);
      document["mousemove" + handler] = null;
    } else {
      document.removeEventListener("mousemove", handler, false);
    }

    m.mouseDown = false;

    if (m.mouseLocations.length > 0) {
      var clickCount = m.mouseLocations.length;
      m.velocity.x = (m.mouseLocations[clickCount - 1].x - m.mouseLocations[0].x) / clickCount;
      m.velocity.y = (m.mouseLocations[clickCount - 1].y - m.mouseLocations[0].y) / clickCount;
      m.mouseLocations.length = 0;
    }
  }

  m.viewingBox.style.cursor = m.hoverCursor;
});

drawMap();
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas" width="500" height="500">
</canvas>
<fieldset>
  <legend>Height Map Properties</legend>
  <input type="text" name="iterations" id="iterations">
  <label for="iterations">
    Iterations(7)
  </label>
  <label>
    <input type="checkbox" id="colors" />Rainbow</label>
</fieldset>

Tatarize
źródło