Part of the Advent of Grok {Shan, Shui}*. See blog post for intro and table of contents.

« Day 20 | Day 22 »

“OK,” I am thinking, looking at the state of this tedious investigation, “If we’ll consider it an advent, after all, I have just 4 more days to spend somehow.” How should I spend them?

I am considering a few options, like, maybe, just do more of the same: rewrite/understand one more tree, or one more architectural element, or that man-on-a-boat, or flatMount or just a roof? Or maybe go clean up the functions I already handled, make them at least to have similar code style and consistent usage of my “core prototype” extensions? Maybe add docs/comments, or try to adjust to consistency at least all function signatures at once (long parameter names, hash deconstruction, etc.)? All of ideas has its merits, but all mostly feel “more of the same”—all in all, the project was about trying to understand the intuitions behind the beautiful picture generation, and, without much self-consciousness, I might say that by this point I understand most of what happening on all levels from mountain-with-trees-and-towers to single uneven “hand-painted” line. It obviously doesn’t bring me closer to having the artistic talent to design something of comparable beauty, but at least I see now how the design might be implemented.

In addition to those doubts, I also have travelling day tomorrow, and not much time on my hands today… So, I eventually set for the rough schedule:

  • spend 23rd and 24th for the polishing and wrap-up of the diary that emerged through the previous days (while simultaneously writing a diary about how I do it)—making sure it is convenient to follow with cross-links, cleaner texts and overall structure
  • which leaves today and tomorrow for some more code investigation, and today’s one would be short and dedicated to the one layer I haven’t looked into yet: the highest one which combines all of those chunks together and makes the picture infinite.

So… It seems everything interesting highest-level happens in the function update(), which calls chunkloader()/chunkrender() which we already saw (and somewhat cleaned up). And all of it starts with the MEM global state variable and its various properties like xmax and cwid. So, let’s rename those, shouldn’t we?

I am starting from renaming MEM to STATE and then looking at its definition:

STATE = {
  canv: "",
  chunks: [],
  xmin: 0,
  xmax: 0,
  cwid: 512,
  cursx: 0,
  lasttick: 0,
  windx: 3000,
  windy: 800,
  occupied_x: null,
};

Now, grepping throught the code for the usages, we can clarify these names a bit:

STATE = {
  // those are actually immutable settings:

  chunkWidth: 512,
  windowWidth: 3000,
  windowHeight: 800,

  // that's really state (of the drawing)

  canvas: "",
  chunks: [],

  xmin: 0, // minimum x current STATE.canvasas is drawn starting from (can be negative)
  xmax: 0, // maximum x current STATE.canvasas is drawn to

  cursorX: 0, // start of currently visible part of the picture

  occupiedXs: null, // global list of x columns already having some mountains, initialized in mountplanner

  lasttick: 0, // debug
};

One more cleanup/splitting:

CONFIG = {
  chunkWidth: 512,
  windowWidth: 3000,
  windowHeight: 800,
}

STATE = {
  canvas: "",
  chunks: [],

  xmin: 0, // minimum x current STATE.canvasas is drawn starting from (can be negative)
  xmax: 0, // maximum x current STATE.canvasas is drawn to

  cursorX: 0, // start of currently visible part of the picture

  occupiedXs: null, // global list of x columns already having some mountains, initialized in mountplanner
};

Now, looking at how variables are used, I notice that STATE.canvas is not actually a global state. It is reset in each call of the chunkrender:

function chunkrender(xmin, xmax) {
  STATE.canvas = "";

  for (var i = 0; i < STATE.chunks.length; i++) {
    if (
      xmin - CONFIG.chunkWidth < STATE.chunks[i].x &&
      STATE.chunks[i].x < xmax + CONFIG.chunkWidth
    ) {
      STATE.canvas += STATE.chunks[i].canv;
    }
  }
}

…which is called by the update(), and then it uses the canvas. So, we can make it less global (and also simplify the chunkrender):

function chunkrender(xmin, xmax) {
  return STATE.chunks
         .filter( chunk => xmin - CONFIG.chunkWidth < chunk.x && chunk.x < xmax + CONFIG.chunkWidth )
         .map( chunk => chunk.canv )
         .join()
}

…and now we have a bit cleaned up update and one pretend-global-but-not-really variable less:

function update() {
  self.chunkloader(STATE.cursorX, STATE.cursorX + CONFIG.windowWidth);
  canvas = self.chunkrender(STATE.cursorX, STATE.cursorX + CONFIG.windowWidth);

  document.getElementById("BG").innerHTML =
    "<svg id='SVG' xmlns='http://www.w3.org/2000/svg' " +
      `width='${CONFIG.windowWidth}' height='${CONFIG.windowHeight}' style='mix-blend-mode:multiply;'` +
      `viewBox = '${calcViewBox()}'><g id='G' transform='translate(0, 0)'>` +
      canvas +
    "</g></svg>";
}

The last function to look into once more, is chunkloader. With final understanding of what’s what, I did this to it:

function chunkloader(xmin, xmax) {
  while (xmax > STATE.xmax - CONFIG.chunkWidth || xmin < STATE.xmin + CONFIG.chunkWidth) {
    var plan;
    if (xmax > STATE.xmax - CONFIG.chunkWidth) {
      plan = mountplanner(STATE.xmax, STATE.xmax + CONFIG.chunkWidth);
      STATE.xmax += CONFIG.chunkWidth;
    } else {
      plan = mountplanner(STATE.xmin - CONFIG.chunkWidth, STATE.xmin);
      STATE.xmin -= CONFIG.chunkWidth;
    }

    plan.forEach( ({tag, x, y}, i) => {
      switch(tag) {
      case "mount":
        STATE.chunks.put({tag, x, y,            canvas: Mount.mountain(x, y, rand(i * 2), {})});
        STATE.chunks.put({tag, x, y: y - 10000, canvas: water(x, y, i * 2)});
        break
      case "flatmount":
        STATE.chunks.put({tag, x, y,
          canvas: Mount.flatMount(x, y, rand(2 * Math.PI), {wid: rand(600, 1000), hei: 100, cho: rand(0.5, 0.7)}),
        });
        break
      case "distmount":
        STATE.chunks.put({tag, x, y,
           canvas: Mount.distMount(x, y, rand(100), {hei: 150, len: randChoice([500, 1000, 1500])})
         });
        break
      case "boat":
        STATE.chunks.put({tag, x, y,
          canvas: Arch.boat01(x, y, Math.random(), {sca: y / 800, fli: randChoice([true, false])})
        });
        break
      default:
        throw `weird tag: ${tag}`
      }
    })
  }
}

With STATE.chunks.put defined just under the STATE:

STATE.chunks.put = function(newChunk) {
  if (this.length == 0 || newChunk.y >= this.last.y) { this.push(newChunk) }
  else if (newChunk.y <= this.first.y)               { this.unshift(newChunk)}
  else {
    this.eachCons(2).
         findIndex( ([chunk1, chunk2], i) => newChunk.y >= chunk1.y && newChunk.y <= chunk2.y ).
         and_then( idx => this.splice(idx + 1, 0, newChunk) )
  }
};

That would do the “overall structure” for me! (Even though though a temptation, say, to turn while into some sequential enumerator is high, I will resist it.) This would do the day’s work, too. Tomorrow, the new day. One thing to see to on the “global” level is how the smoothness of the transitions is achieved when chunks are put next to each other. Should be something simple yet ingenious.

« Day 20 | Day 22 »