@@ 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>
@@ 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