Menyesuaikan sepotong dunia yang dihasilkan secara prosedural dengan sebagian dunia lainnya

18

Sudahkah Anda membaca The Chronicles of Amber oleh Roger Zelazny?

Bayangkan diri Anda bermain di game MMO orang ketiga. Anda muncul di dunia dan mulai berkeliaran. Setelah beberapa waktu, ketika Anda berpikir, bahwa Anda telah mempelajari peta, Anda menyadari, bahwa Anda berada di suatu tempat, yang belum pernah Anda lihat sebelumnya. Anda kembali ke tempat terakhir yang Anda yakin tahu dan masih ada di sana. Tetapi seluruh dunia telah berubah dan Anda bahkan tidak memperhatikan bagaimana itu terjadi.

Saya sudah membaca tentang generasi dunia prosedural. Saya telah membaca tentang Perlin noise dan oktaf, Simplex noise, algoritma Diamond-square, tentang simulasi lempeng tektonik dan erosi air. Saya percaya saya memiliki pemahaman yang kabur tentang pendekatan umum dalam generasi dunia prosedural.

Dan dengan pengetahuan ini saya tidak tahu bagaimana Anda bisa melakukan sesuatu seperti yang ditulis di atas. Setiap gagasan yang muncul di benak saya menemui beberapa masalah teoretis. Berikut adalah beberapa ide yang dapat saya pikirkan:

1) Generasi dunia "reversibel" dengan sejumlah seed sebagai input dan beberapa menggambarkan-a-chunk-number

Saya ragu bahwa itu bahkan mungkin, tetapi saya membayangkan sebuah fungsi, yang akan menerima sebuah seed dan menghasilkan matriks angka, yang di atasnya potongan dibangun. Dan untuk setiap nomor unik ada potongan unik. Dan fungsi kedua, yang mendapatkan nomor chunk yang unik ini dan menghasilkan seed, yang berisi angka ini. Saya sudah mencoba membuat skema pada gambar di bawah ini:

masukkan deskripsi gambar di sini

2) Membuat potongan sepenuhnya acak dan membuat transisi di antara mereka.

Seperti yang disarankan Aracthor . Manfaat dari pendekatan ini adalah memungkinkan dan tidak memerlukan fungsi sihir :)

Kontra pendekatan ini menurut saya, adalah bahwa kemungkinan tidak mungkin untuk memiliki dunia yang beragam. Jika Anda katakan saja kepulauan dan benua yang diwakili oleh hanya satu angka dan bongkahan yang berdekatan, maka ukuran bongkahannya tidak akan sama dengan benua. Dan saya ragu bahwa adalah mungkin untuk melakukan transisi yang cantik antara potongan-potongan. Apakah saya melewatkan sesuatu?

Jadi, dengan kata lain, Anda sedang mengembangkan MMO dengan dunia yang dihasilkan secara prosedural. Tetapi alih-alih memiliki satu dunia, Anda memiliki banyak dunia . Pendekatan apa yang akan Anda ambil untuk menghasilkan dunia dan bagaimana Anda menerapkan transisi pemain dari satu dunia ke dunia lain tanpa pemain memperhatikan transisi.

Ngomong-ngomong, saya yakin Anda punya ide umum. Bagaimana Anda akan melakukannya?

netaholik
sumber
Jadi saya punya beberapa masalah dengan jawabannya di sini. @ Aracthor Saya sudah bicara dengan Anda tentang manifold halus sebelumnya, hal semacam itu berlaku di sini. Namun ada 2 jawaban yang cukup tinggi jadi saya bertanya-tanya apakah ada benarnya ...
Alec Teal
@AlecTeal jika Anda memiliki sesuatu untuk ditambahkan, silakan lakukan. Saya akan senang mendengar ide dan saran.
netaholic

Jawaban:

23

Gunakan irisan tingkat tinggi. Jika Anda menggunakan noise 2d untuk peta ketinggian sebelumnya, gunakan noise 3D dengan koordinat terakhir yang diperbaiki. Sekarang Anda dapat perlahan mengubah posisi di dimensi terakhir untuk memodifikasi medan. Karena noise Perlin kontinu dalam semua dimensi, Anda akan mendapatkan transisi yang lancar selama Anda dengan lancar mengubah posisi tempat Anda mencicipi fungsi noise.

Jika Anda hanya ingin mengubah medan yang jauh dari jarak ke pemain seperti offset misalnya. Anda juga bisa menyimpan offset untuk setiap koordinat di peta dan hanya menambah tetapi tidak pernah menguranginya. Dengan cara ini peta hanya menjadi lebih baru tetapi tidak pernah lebih tua.

Gagasan ini juga berfungsi jika Anda sudah menggunakan noise 3D, cukup sampel dari 4D lalu. Juga, lihatlah kebisingan Simplex. Ini adalah versi peningkatan kebisingan Perlin dan berfungsi lebih baik untuk dimensi yang lebih banyak.

danijar
sumber
2
Ini menarik. Apakah saya mengerti dengan benar, bahwa Anda menyarankan untuk menghasilkan suara 3d, menggunakan xy-slice pada z tertentu sebagai heightmap dan melakukan transisi yang mulus ke irisan lain dengan mengubah z - koordinat ketika jarak dari pemain bertambah?
netaholic
@netaholic Tepat. Menggambarkannya sebagai irisan adalah intuisi yang sangat bagus. Selain itu, Anda dapat melacak nilai tertinggi untuk koordinat terakhir di mana saja di peta dan hanya menambahnya tetapi tidak pernah menguranginya.
danijar
1
Ini ide yang cemerlang. Pada dasarnya, peta medan Anda akan berupa irisan parabola (atau kurva lainnya) melalui volume 3D.
Nama Palsu
Ini ide yang sangat pintar.
user253751
5

Ide Anda untuk membagi dunia menjadi beberapa bidak tidak buruk. Itu tidak lengkap.

Satu-satunya masalah adalah persimpangan antara potongan. Misalnya, jika Anda menggunakan perlin noise untuk menghasilkan bantuan, dan seed berbeda untuk setiap chunk, dan risiko ini terjadi:

Bug bantuan potongan

Sebuah solusi akan menghasilkan bantuan potongan tidak hanya dari benih kebisingan Perlin, tetapi juga dari potongan lain di sekitarnya.

Algoritma Perlin menggunakan nilai peta acak di sekitarnya untuk "memuluskan" diri mereka sendiri. Jika mereka menggunakan peta umum, akan dihaluskan bersama.

Satu-satunya masalah adalah jika Anda mengubah biji chunk untuk membuatnya berbeda ketika pemain surut, Anda harus memuat ulang bongkahan juga, karena perbatasan mereka juga harus berubah.

Ini tidak akan mengubah ukuran bongkahan, tetapi itu akan meningkatkan jarak minimal dari pemain menjadi bongkar / muat, karena bongkahan harus dimuat ketika pemain melihatnya, dan, dengan metode ini, bongkahan yang berdekatan harus terlalu .

MEMPERBARUI:

Jika setiap potongan dunia Anda memiliki tipe yang berbeda, masalahnya akan tumbuh. Ini bukan hanya tentang kelegaan. Solusi mahal adalah sebagai berikut:

Potongan daging

Anggaplah bongkahan hijau adalah dunia hutan, kepulauan yang biru dan gurun yang datar.
Solusinya di sini adalah untuk menciptakan zona "transisi", di mana relief dan sifat dasar Anda (serta objek yang di-ground-kan, atau apa pun yang Anda inginkan) akan semakin beralih dari satu jenis ke jenis lainnya.

Dan seperti yang Anda lihat pada gambar ini, bagian kode yang akan menjadi kotak kecil di sudut-sudut chunk: mereka harus membuat tautan antara 4 potongan, berpotensi sifat berbeda.

Jadi untuk tingkat kompleksitas ini, saya pikir generasi dunia 2D klasik seperti Perlin2D tidak dapat digunakan. Saya merujuk Anda ke jawaban @danijar untuk itu.

Aracthor
sumber
Apakah Anda menyarankan untuk menghasilkan "pusat" potongan dari biji dan ujungnya "dihaluskan" berdasarkan potongan yang berdekatan? Masuk akal, tetapi itu akan meningkatkan ukuran bongkahan, karena itu harus menjadi ukuran daerah, bahwa pemain dapat mengamati dan menggandakan lebar area transisi ke bongkahan yang berdekatan. Dan area chunk menjadi lebih besar semakin beragam dunia.
netaholic
@netaholic Ini tidak akan lebih besar, tapi agak. Saya menambahkan paragraf di atasnya.
Aracthor
Saya telah memperbarui pertanyaan saya. Mencoba menggambarkan beberapa ide yang saya miliki
netaholic
Jadi jawaban lain di sini menggunakan (semacam, tidak cukup) dimensi ketiga sebagai bagan. Anda juga melihat pesawat sebagai bermacam-macam, dan saya menyukai ide-ide Anda. Untuk memperluasnya sedikit lebih jauh, Anda benar-benar ingin berlipat ganda. Anda perlu memastikan transisi Anda lancar. Anda kemudian dapat menerapkan blur atau noise untuk ini dan jawabannya akan sempurna.
Alec Teal
0

Meskipun ide danijar cukup solid, Anda bisa menyimpan banyak data, jika Anda ingin memiliki area lokal yang sama dan perubahan jarak. Dan meminta semakin banyak irisan kebisingan yang semakin kompleks. Anda bisa mendapatkan semua ini dengan mode 2d yang lebih standar.

Saya mengembangkan sebuah algoritma untuk secara prosedural menghasilkan noise fraktal acak, sebagian berdasarkan pada algoritma square diamond yang saya tetapkan untuk menjadi tak terbatas, dan juga deterministik. Jadi diamond-square dapat membuat lansekap tak terbatas, serta algoritma saya sendiri yang agak gumpal.

Idenya pada dasarnya sama. Tetapi, alih-alih mengambil sampel derau dimensi yang lebih tinggi, Anda dapat mengulangi nilai pada tingkat berulang yang berbeda.

Jadi, Anda masih menyimpan nilai yang Anda minta sebelumnya, dan menyimpannya (skema ini secara mandiri dapat digunakan untuk mempercepat algoritma yang sudah super cepat). Dan ketika area baru diminta, itu dibuat dengan nilai y baru. dan area apa pun yang tidak diminta dalam permintaan itu dihapus.

Jadi, alih-alih menjelajahi ruang yang berbeda dalam dimensi tambahan. Kami menyimpan sedikit data monotonik untuk dicampur dalam jumlah yang berbeda (dengan jumlah yang semakin besar pada tingkat yang berbeda).

Jika pengguna bergerak ke suatu arah, nilai-nilai dipindahkan sesuai (dan di setiap tingkat) dan nilai-nilai baru dihasilkan di tepi yang baru. Jika benih iteratif teratas diubah, seluruh dunia akan berubah secara drastis. Jika iterasi akhir diberikan hasil yang berbeda, maka jumlah perubahan akan sangat kecil + -1 blok atau lebih. Tapi, bukit akan tetap ada di sana dan lembah dll, tetapi celah dan celah akan berubah. Kecuali Anda melangkah cukup jauh, maka bukit itu akan hilang.

Jadi jika kita menyimpan 100x100 potongan nilai setiap iterasi. Maka tidak ada yang bisa berubah pada 100x100 dari pemain. Tapi, pada 200x200 hal bisa berubah 1 blok. Pada 400x400, berbagai hal dapat berubah sebanyak 2 blok. Pada 800x800 hal-hal akan dapat berubah sebanyak 4 blok. Jadi segalanya akan berubah dan mereka akan berubah semakin banyak semakin jauh Anda melangkah. Jika Anda kembali mereka akan berbeda, jika Anda pergi terlalu jauh mereka akan benar-benar berubah dan benar-benar hilang karena semua benih akan ditinggalkan.

Menambahkan dimensi yang berbeda untuk memberikan efek stabilisasi ini, tentu saja akan berhasil, menggeser y dari kejauhan, tetapi Anda akan menyimpan banyak data untuk banyak blok besar ketika Anda tidak harus melakukannya. Dalam algoritme fraktal noise deterministik Anda bisa mendapatkan efek yang sama dengan menambahkan nilai yang berubah (pada jumlah yang berbeda) ketika posisi bergerak melampaui titik tertentu.

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
sumber