~patrickhaussmann/truchet.app.5ls.de

7b730b9339131809acd8d74c7a8f605ee9c9bb90 — patrickhaussmann 2 years ago 96fec5c
convert to OOP
2 files changed, 277 insertions(+), 199 deletions(-)

M index.html
A lib.js
M index.html => index.html +58 -199
@@ 98,11 98,13 @@
    <div id="container" style="height: 200vh"></div>

    <script src="/crel.min.js"></script>
    <script src="/lib.js"></script>
    <script>
      var width = document.body.scrollWidth;
      var height = document.body.scrollHeight;
      const seed = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
      const rand = mulberry32(seed);
      var tiles = new TileList();

      const container = crel("#container");



@@ 111,43 113,23 @@
      function createRow(rowIndex) {
        const row = crel.div({ class: "row" });
        const numTiles = width / size;
        if (!tilesGrid[rowIndex]) tilesGrid[rowIndex] = [];
        for (let colIndex = 0; colIndex < numTiles; colIndex += 1) {
          const orientation = randomOrientation(rand());
          const el = crel.div(
            {
              class: "truchet-" + orientation,
              style: {
                width: size + "px",
                height: size + "px",
              },
            },
            (segmentA = crel.div({ class: "a" }, (thisEl) => {
              if (rand() < 0.5) thisEl.style.zIndex = "1";
            })),
            (segmentB = crel.div({ class: "b" }))
          );
          el.rowIndex = rowIndex;
          el.colIndex = colIndex;
          const tile = {
            el: el,
            orientation: orientation,
            r: rand(),
            rowIndex: rowIndex,
            colIndex: colIndex,
            segments: {
              a: segmentA,
              b: segmentB,
            },
          };
          tilesGrid[rowIndex][colIndex] = tile;
          tilesSorted.push(tile);
          row.append(el);
          const tile = new Tile(rowIndex, colIndex, orientation);
          tiles.add(tile);

          row.append(tile.element);

          if (rand() < 0.003) {
            let segment = rand() < 0.5 ? "a" : "b";
            const type = rand() < 0.5 ? "a" : "b";
            const segment = tile.segments[type];

            setTimeout(() => {
              createcoloredLoop(tile, segment, true);
              const loop = new Loop(segment, tiles);
              if (loop.isSafe(loopMaxLength, tiles)) {
                loop.color();
                loops.push(loop);
              }
            }, 0); // delay after all tiles are created
          }
        }


@@ 155,172 137,46 @@
        return row;
      }

      var tilesSorted = [];
      var tilesGrid = [];
      var lastRow = 0;
      var size;
      var size = Math.round(width / Math.round(width / 60)); // find divisor of with near 60px
      size += size % 2; // make sure it's even

      var maxTiles;
      function createTiles(startRow = 0) {
        if (maxTiles && tilesSorted.length > maxTiles) {
        if (maxTiles && tiles.length > maxTiles) {
          // avoid excessive memory usage
          window.location.reload();
        }

        if (!size) {
          size = Math.round(width / Math.round(width / 60)); // find divisor of with near 60px
          size += size % 2; // make sure it's even
        }
        const numTiles = height / size;
        for (var rowIndex = startRow; rowIndex < numTiles; rowIndex += 1) {
          container.append(createRow(rowIndex));
        }
        lastRow = rowIndex;
        tilesSorted.sort((a, b) => a.r - b.r);

        if (startRow === 0) {
          maxTiles = tilesSorted.length * 5;
          maxTiles = tiles.length * 5;
        }
      }

      createTiles();

      // https://github.com/bryc/code/blob/master/jshash/PRNGs.md#mulberry32
      function mulberry32(a) {
        const seed = a;
        const rand = function () {
          var t = (a += 0x6d2b79f5);
          t = Math.imul(t ^ (t >>> 15), t | 1);
          t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
          return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
        };
        rand.reset = function () {
          a = seed;
        };
        return rand;
      }

      // https://stackoverflow.com/a/41956372
      // Return 0 <= i <= array.length such that !pred(array[i - 1]) && pred(array[i])
      function binarySearch(array, pred) {
        let lo = -1,
          hi = array.length;
        while (1 + lo < hi) {
          const mi = lo + ((hi - lo) >> 1);
          if (pred(array[mi])) {
            hi = mi;
          } else {
            lo = mi;
          }
        }
        return hi;
      }

      // inclusive min and max
      const findTilesRangeOfR = (min, max) =>
        tilesSorted.slice(
          binarySearch(tilesSorted, (tile) => tile.r >= min), // lowerBound
          binarySearch(tilesSorted, (tile) => tile.r > max) // upperBound
        );

      function setOrientation(tile, orientation) {
        tile.orientation = orientation;
        tile.el.className = "truchet-" + orientation;
      }

      const directionMap = {
        l: { rowOffset: 0, colOffset: -1 }, // left
        r: { rowOffset: 0, colOffset: 1 }, // right
        t: { rowOffset: -1, colOffset: 0 }, // top
        b: { rowOffset: 1, colOffset: 0 }, // bottom
      };

      const invertDirection = { l: "r", r: "l", t: "b", b: "t" };
      const connections = [
        { l: "a", r: "b", t: "b", b: "a" }, // orientation 0
        { l: "a", r: "b", t: "a", b: "b" }, // orientation 1
        { l: "a", r: "a", t: "b", b: "b" }, // orientation 2
      ];

      const getConnections = (orientation, segment) =>
        Object.entries(connections[orientation]).reduce((acc, [key, value]) => {
          if (value == segment) acc.push(key);
          return acc;
        }, []);

      const findNeighbors = (tile, segment) =>
        getConnections(tile.orientation, segment).flatMap((connection) => {
          const { rowOffset, colOffset } = directionMap[connection];
          const neighbor =
            tilesGrid[tile.rowIndex + rowOffset]?.[tile.colIndex + colOffset];
          if (!neighbor) return [];
          return [
            {
              tile: neighbor,
              segment:
                connections[neighbor.orientation][invertDirection[connection]],
            },
          ];
        });

      const getNextInLoop = (loop, tile, segment) =>
        findNeighbors(tile, segment).filter(
          (neighbor) =>
            !loop.some(
              (t) => t.tile == neighbor.tile && t.segment == neighbor.segment
            )
        )[0];

      function findLoop(tile, segment) {
        const directNeighbors = findNeighbors(tile, segment);
        var loop = [{ tile, segment }, ...directNeighbors];
        directNeighbors.forEach((directNeighbor) => {
          var nextInLoop = getNextInLoop(
            loop,
            directNeighbor.tile,
            directNeighbor.segment
          );
          while (nextInLoop) {
            loop.push(nextInLoop);
            nextInLoop = getNextInLoop(
              loop,
              nextInLoop.tile,
              nextInLoop.segment
            );
          }
        });
        return loop;
      }

      const colorLoop = (loop) =>
        loop.forEach(({ tile: t, segment }) => {
          t.segments[segment].classList.toggle("colored");
        });

      const redrawLoops = () =>
        coloredLoops.forEach((coloredLoop) => {
          colorLoop(coloredLoop.loop); // remove color
          coloredLoop.loop = findLoop(
            coloredLoop.start.tile,
            coloredLoop.start.segment
          );
          colorLoop(coloredLoop.loop); // add color
        loops.forEach((loop) => {
          loop.redraw();
        });

      const step = 0.01;
      var a = 0;
      setInterval(() => {
        findTilesRangeOfR(a, a + step).forEach((tile) => {
          if (
            tile.segments.a.classList.contains("colored") ||
            tile.segments.b.classList.contains("colored")
          )
            return;
          setOrientation(tile, randomOrientation(Math.random()));
        tiles.getRangeOfR(a, a + step).forEach((tile) => {
          if (tile.isColored()) return;
          tile.setOrientation(randomOrientation(Math.random()));
        });
        a = (a + step) % 1;
      }, 500);

      window.addEventListener("resize", () => {
      /* window.addEventListener("resize", () => {
        width = document.body.scrollWidth;
        height = document.body.scrollHeight;
        tilesSorted = [];


@@ 330,8 186,8 @@
        size = null;
        lastRow = 0;
        createTiles();
        coloredLoops = [];
      });
        loops = [];
      }); */

      window.addEventListener(
        "scroll",


@@ 344,7 200,7 @@
              container.style.height = currentHeight + "px";
              height = currentHeight;
              createTiles(lastRow);
              redrawLoops();
              //redrawLoops();
            }
          };
        })()


@@ 352,39 208,42 @@

      const loopMaxLength =
        (window.innerHeight * window.innerWidth) / size / 100;

      function createcoloredLoop(tile, segment, limited = false) {
        const loop = findLoop(tile, segment);

        // skip if loop is too long or ends at the bottom
        if (
          limited &&
          (loop.length > loopMaxLength ||
            loop.some(
              ({ tile: t, segment: s }) =>
                t.rowIndex == lastRow - 1 &&
                getConnections(t.orientation, s).includes("b")
            ))
        )
          return;

        colorLoop(loop);
        coloredLoops.push({ start: { tile, segment }, loop });
        return loop;
      }

      var coloredLoops = [];
      var loops = [];
      window.addEventListener("click", (e) => {
        const segment = Array.from(e.target.classList).filter(
        const type = Array.from(e.target.classList).filter(
          (a) => a != "colored"
        )[0];
        if (!["a", "b"].includes(segment)) return; // not a truchet tile

        const tile =
          tilesGrid[e.target.parentNode.rowIndex][e.target.parentNode.colIndex];
        createcoloredLoop(tile, segment);
        const segment =
          tiles.grid[e.target.parentNode.rowIndex]?.[
            e.target.parentNode.colIndex
          ]?.segments[type];
        if (!segment) return; // not a truchet tile
        const loop = new Loop(segment, tiles);
        loop.color();
        loops.push(loop);
      });

      /**
       * It takes a seed and returns a function that returns a random number between 0 and 1
       * @param a - the seed
       * @returns A function that returns a random number between 0 and 1.
       */
      // https://github.com/bryc/code/blob/master/jshash/PRNGs.md#mulberry32
      function mulberry32(a) {
        const seed = a;
        const rand = function () {
          var t = (a += 0x6d2b79f5);
          t = Math.imul(t ^ (t >>> 15), t | 1);
          t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
          return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
        };
        rand.reset = function () {
          a = seed;
        };
        return rand;
      }

      setInterval(() => window.scrollBy(0, 1), 30);
    </script>
  </body>

A lib.js => lib.js +219 -0
@@ 0,0 1,219 @@
const directionMap = {
  l: { rowOffset: 0, colOffset: -1 }, // left
  r: { rowOffset: 0, colOffset: 1 }, // right
  t: { rowOffset: -1, colOffset: 0 }, // top
  b: { rowOffset: 1, colOffset: 0 }, // bottom
};
const invertDirection = { l: "r", r: "l", t: "b", b: "t" };
const connections = [
  { l: "a", r: "b", t: "b", b: "a" }, // orientation 0
  { l: "a", r: "b", t: "a", b: "b" }, // orientation 1
  { l: "a", r: "a", t: "b", b: "b" }, // orientation 2
];

const getConnections = (orientation, type) =>
  Object.entries(connections[orientation]).reduce((acc, [key, value]) => {
    if (value == type) acc.push(key);
    return acc;
  }, []);

//#region Tile
/**
 * It creates a tile with two segments, and it has a random number
 * @param rowIndex - The row index of the tile.
 * @param colIndex - The column index of the tile.
 * @param orientation - The orientation of the tile.
 */
function Tile(rowIndex, colIndex, orientation) {
  this.segments = {
    a: new Segment(this, "a"),
    b: new Segment(this, "b"),
  };
  this.element = crel.div(
    {
      class: "truchet-" + orientation,
      style: {
        width: size + "px",
        height: size + "px",
      },
    },
    this.segments.a.element,
    this.segments.b.element
  );
  this.element.rowIndex = rowIndex;
  this.element.colIndex = colIndex;
  this.orientation = orientation;
  this.rowIndex = rowIndex;
  this.colIndex = colIndex;
  this.r = rand();
}
Tile.prototype.setOrientation = function (orientation) {
  this.orientation = orientation;
  this.element.className = "truchet-" + orientation;
};
Tile.prototype.isColored = function () {
  return this.segments.a.isColored() || this.segments.b.isColored();
};
//#endregion

//#region Segment
/**
 * It creates a new segment object, which is a div element with a class of either "a" or "b", and it
 * has a color property that can be set to true or false
 * @param parent - The parent segment.
 * @param type - The type of segment. This can be "a" or "b".
 */
function Segment(parent, type) {
  this.parent = parent;
  this.type = type;
  this.element = crel.div({ class: type }, (thisEl) => {
    if (type == "a" && rand() < 0.5) thisEl.style.zIndex = "1";
  });
  this.colored = false;
}
Segment.prototype.isColored = function () {
  return this.colored;
};
Segment.prototype.color = function (setColor = true) {
  if (setColor) {
    this.colored = true;
    this.element.classList.add("colored");
  } else {
    this.colored = false;
    this.element.classList.remove("colored");
  }
};
Object.defineProperty(Segment.prototype, "rowIndex", {
  get: function () {
    return this.parent.rowIndex;
  },
});
Object.defineProperty(Segment.prototype, "colIndex", {
  get: function () {
    return this.parent.colIndex;
  },
});
Object.defineProperty(Segment.prototype, "orientation", {
  get: function () {
    return this.parent.orientation;
  },
});
//#endregion

//#region TileList
/**
 * It's a list of tiles that can be sorted by r, and can be queried for a range of r
 */
function TileList() {
  this.grid = [];
  this.list = [];
  this.sorted = true;
}
TileList.prototype.add = function (tile) {
  if (!this.grid[tile.rowIndex]) this.grid[tile.rowIndex] = [];
  this.grid[tile.rowIndex][tile.colIndex] = tile;
  this.list.push(tile);
  this.sorted = false;
};
TileList.prototype.sort = function () {
  if (this.sorted) return;
  this.list.sort((tileA, tileB) => tileA.r - tileB.r);
  this.sorted = true;
};
TileList.prototype.getRangeOfR = function (min, max) {
  function binarySearch(array, pred) {
    // https://stackoverflow.com/a/41956372
    // Return 0 <= i <= array.length such that !pred(array[i - 1]) && pred(array[i])
    let lo = -1,
      hi = array.length;
    while (1 + lo < hi) {
      const mi = lo + ((hi - lo) >> 1);
      if (pred(array[mi])) {
        hi = mi;
      } else {
        lo = mi;
      }
    }
    return hi;
  }

  this.sort();
  // inclusive min and max
  return this.list.slice(
    binarySearch(this.list, (tile) => tile.r >= min), // lowerBound
    binarySearch(this.list, (tile) => tile.r > max) // upperBound
  );
};
Object.defineProperty(TileList.prototype, "length", {
  get: function () {
    return this.list.length;
  },
});
//#endregion

//#region Loop
/**
 * It finds all the segments that are connected to the start segment, and returns them as an array
 * @param startSegment - the segment that the loop starts at
 * @param tiles - the tiles object
 */
function Loop(startSegment, tiles) {
  this.startSegment = startSegment;
  this.tiles = tiles;
  this.loop = this.findLoop(this.startSegment);
}
Loop.prototype.findLoop = function (segment) {
  const findNeighborSegment = (segment) =>
    getConnections(segment.orientation, segment.type).flatMap((connection) => {
      const { rowOffset, colOffset } = directionMap[connection];
      const neighbor =
        this.tiles.grid[segment.rowIndex + rowOffset]?.[
          segment.colIndex + colOffset
        ];
      if (!neighbor) return [];
      return [
        neighbor.segments[
          connections[neighbor.orientation][invertDirection[connection]]
        ],
      ];
    });

  const getNextInLoop = (segment) =>
    findNeighborSegment(segment).filter(
      (neighbor) => !loop.some((s) => s == neighbor)
    )[0];

  const directNeighbors = findNeighborSegment(segment);
  var loop = [segment, ...directNeighbors];

  directNeighbors.forEach((directNeighbor) => {
    var nextInLoop = getNextInLoop(directNeighbor);
    while (nextInLoop) {
      loop.push(nextInLoop);
      nextInLoop = getNextInLoop(nextInLoop);
    }
  });
  return loop;
};
Loop.prototype.color = function (setColor) {
  this.loop.forEach((segment) => {
    segment.color(setColor);
  });
};
Loop.prototype.redraw = function () {
  this.color(false); // remove color
  this.loop = this.findLoop(this.startSegment);
  this.color(); // add color
};
Loop.prototype.isSafe = function (loopMaxLength, tiles) {
  // check if loop is too long or ends at the bottom
  return (
    this.loop.length < loopMaxLength &&
    !this.loop.some(
      (segment) =>
        segment.rowIndex == tiles.grid.length - 1 &&
        getConnections(segment.orientation, segment.type).includes("b")
    )
  );
};
//#endregion