diff --git a/index.html b/index.html
index 4ad59ad..8fb4fab 100644
--- a/index.html
+++ b/index.html
@@ -5,16 +5,12 @@
Geometric Pattern Generator
-
+
+
diff --git a/src/Tile.ts b/src/Tile.ts
index b0c4b90..06af8ba 100644
--- a/src/Tile.ts
+++ b/src/Tile.ts
@@ -1,5 +1,24 @@
import SVG from "svg.js";
import { parseSVG as parsePath } from "svg-path-parser";
+import { getColorPallete, getRandomColor } from "./utils";
+
+type LineStartEnd = {
+ lineStart: { x: number; y: number };
+ lineEnd: { x: number; y: number };
+};
+
+interface TileParams {
+ element?: SVGElement;
+ showArcs?: boolean;
+ tileSize?: number;
+ numberOfLines?: number;
+ linesRandomDirection?: boolean;
+ rotation?: number;
+ outlineStrokeWeight?: number;
+ outlineStrokeColour?: string;
+ infillStrokeWeight?: number;
+ infillStrokeColour?: string;
+}
// Define the Tile class - arcs
class Tile {
@@ -7,21 +26,52 @@ class Tile {
showArcs: boolean;
tileSize: number;
numberOfLines: number;
+ linesRandomDirection: boolean;
+ rotation: number;
+ outlineStrokeWeight: number;
+ outlineStrokeColour: string;
+ infillStrokeWeight: number;
+ infillStrokeColour: string;
+ defaultStrokeWeight: number;
+ defaultStrokeColour: string;
- constructor(
- showArcs: boolean = true,
- tileSize: number = 100,
- numberOfLines: number = 10
- ) {
+ constructor({
+ showArcs = true,
+ tileSize = 100,
+ numberOfLines = 10,
+ linesRandomDirection = true,
+ rotation = 0,
+ outlineStrokeWeight = 1,
+ outlineStrokeColour = "black",
+ infillStrokeWeight = 0,
+ infillStrokeColour = "black",
+ }: TileParams) {
this.showArcs = showArcs;
this.tileSize = tileSize;
this.numberOfLines = numberOfLines;
+ this.linesRandomDirection = linesRandomDirection;
+ this.rotation = rotation;
+ this.outlineStrokeWeight = outlineStrokeWeight;
+ this.outlineStrokeColour = outlineStrokeColour;
+ this.infillStrokeWeight = infillStrokeWeight;
+ this.infillStrokeColour = infillStrokeColour;
this.element = this.createTileElement();
+ this.defaultStrokeWeight = 2;
+ this.defaultStrokeColour = "black";
}
createTileElement(): SVGElement {
- const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
- group.setAttribute("class", "tile");
+ const outlineGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ outlineGroup.setAttribute("class", "tile");
+
+ const infillGroup = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "g"
+ );
+ infillGroup.setAttribute("class", "tile");
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 100 100");
@@ -32,110 +82,227 @@ class Tile {
"http://www.w3.org/2000/svg",
"g"
);
- group.appendChild(linesGroup);
+ infillGroup.setAttribute("class", "tile");
+ // group.appendChild(linesGroup);
// Instead of manually specifying the points for each Radii (arcRadii) we can
// calculate that based on the size of the tile and the number of lines we want
- const calcRadii = (size: number, qty: number): number[] => {
+
+ // Experiment with an offset so we don't always fill the tile
+ const calcSpacing = (size: number, qty: number): number[] => {
const spacing = size / qty;
- console.log("CalcRadii, size", size, "quantity", qty, "spacing", spacing);
- let arcRadii = [];
- for (let i = 0; i <= qty; i++) {
- arcRadii.push(0 + i * spacing); // add each position to the array
+ console.log(
+ "Calc spacing, Tile size",
+ size,
+ "quantity of lines",
+ qty,
+ "gap between",
+ spacing,
+ "Last coord",
+ spacing * qty
+ );
+ let spacingArray = [];
+ for (let i = 1; i <= qty; i++) {
+ spacingArray.push(i * spacing); // add each position to the array
}
- return arcRadii;
+ return spacingArray;
};
+ let direction: "horizontal" | "vertical";
+ if (this.linesRandomDirection) {
+ direction = Math.random() < 0.5 ? "horizontal" : "vertical";
+ } else {
+ direction = "horizontal";
+ }
// const direction = Math.random() < 0.5 ? "horizontal" : "vertical";
- const direction = "horizontal";
+ // const direction = "horizontal";
+
+ // Spacing - Calculate the gap between lines for a given tile size and number of lines
+ const spacing = calcSpacing(this.tileSize, this.numberOfLines);
- const radii = calcRadii(this.tileSize, this.numberOfLines);
+ // Infill width should be the difference between the spacing and the outline stroke weight
+ const calculatedInfillStrokeWeight = spacing[1] / 2; //+ this.outlineStrokeWeight / 2;
+ console.log(
+ "Spacing[1]",
+ spacing[1],
+ "this.outlineStrokeWeight",
+ this.outlineStrokeWeight,
+ "Calculated infill stroke",
+ calculatedInfillStrokeWeight
+ );
- // Draw the lines and add them to the linesGroup
- for (const radius of radii) {
- const lines = this.createLine(radius, direction);
- linesGroup.appendChild(lines);
+ // Order matters! SVGs are layered, one thing over another will hide the thing below it.
+ // Colour infill lines - Add line that act as coloured in-fill (can't apply fill to an open path)
+ for (let i = 0; i < spacing.length; i++) {
+ console.log(
+ "Getting colour pallete",
+ i,
+ getColorPallete("ppP50")[i],
+ "direction",
+ direction,
+ "spacing",
+ spacing[i]
+ );
+ const line = this.createPath(
+ spacing[i] - calculatedInfillStrokeWeight / 2, // position
+ direction,
+ calculatedInfillStrokeWeight, // stroke weight
+ getColorPallete("ppP50")[i]
+ );
+ infillGroup.appendChild(line);
}
- // If we are showing arcs on this tile,
+ // Draw the 'solid' lines (the primary set of lines - not the in-fill) and add them to the linesGroup
+ // The first and last line should be half on or off the tile so things overlap correctly. That means
+ // there is one more line than the number specified. The in-fill lines will be the correct number
+ // for (const space of spacing) {
+ for (let i = 0; i <= spacing.length; i++) {
+ let space;
+ if (i >= spacing.length) {
+ space = 0;
+ console.log("Adding the single odd line at", space);
+ const line = this.createPath(
+ space,
+ direction,
+ this.outlineStrokeWeight,
+ this.outlineStrokeColour
+ );
+ outlineGroup.appendChild(line);
+ } else {
+ space = spacing[i];
+ console.log("Adding the next line at", space);
+ const line = this.createPath(
+ space + spacing[0] / 2,
+ direction,
+ this.outlineStrokeWeight,
+ this.outlineStrokeColour
+ );
+ outlineGroup.appendChild(line);
+ }
+
+ console.log("Number of outlines", outlineGroup.children.length);
+ }
+
+ // If we are showing arcs on this tile, this is where we add them
+ // but also where we shorten the straight lines where they intersect with the largest arc
if (this.showArcs) {
- const arcCenter = { x: 0, y: 0 };
- const largestRadius = radii[radii.length - 1];
+ const arcCenter = {
+ x: 0,
+ y: 0,
+ };
+ console.log("Spacing", spacing);
+ const largestSpace = spacing[spacing.length - 1];
// Convert the linesGroup.lines into an array
- Array.from(linesGroup.querySelectorAll("line")).forEach((line) => {
+ Array.from(outlineGroup.querySelectorAll("path")).forEach((line) => {
// Extract the line start and end for the current line
const lineStart = {
- x: parseFloat(line.getAttribute("x1") as string),
- y: parseFloat(line.getAttribute("y1") as string),
+ x: parseFloat(line.getAttribute("d")?.split(" ")[1] as string),
+ y: parseFloat(line.getAttribute("d")?.split(" ")[2] as string),
};
const lineEnd = {
- x: parseFloat(line.getAttribute("x2") as string),
- y: parseFloat(line.getAttribute("y2") as string),
+ x: parseFloat(line.getAttribute("d")?.split(" ")[4] as string),
+ y: parseFloat(line.getAttribute("d")?.split(" ")[5] as string),
};
+ // console.log("Line start:", lineStart, "Line end:", lineEnd);
// Determine if there is an intersection for the current line
const intersection = this.lineArcIntersection(
lineStart,
lineEnd,
- arcCenter,
- largestRadius
+ largestSpace
);
+ // If there is an intersection for the current line, modify it so the line starts at the intersection
if (intersection) {
- console.log("Intersection found", intersection);
- line.setAttribute("x1", String(intersection.x));
- line.setAttribute("y1", String(intersection.y));
- } else {
- console.log("No intersection found");
+ line.setAttribute(
+ "d",
+ `M ${intersection.x} ${intersection.y} ${line
+ .getAttribute("d")
+ ?.split(" ")
+ .slice(4, 6)
+ .join(" ")}`
+ );
}
});
- // for each radius in the radii array
- for (const radius of radii) {
- const arc = this.createArc(0, 0, radius, 0, 0 + 90);
- group.appendChild(arc);
+ // Arc (curves) in-fill (the coloured arcs as per the coloured in-fill lines)
+ for (let i = 0; i < spacing.length; i++) {
+ const arc = this.createArc(
+ 0,
+ 0,
+ spacing[i] - calculatedInfillStrokeWeight / 2,
+ 0,
+ 0 + 90,
+ this.infillStrokeWeight > calculatedInfillStrokeWeight
+ ? this.infillStrokeWeight
+ : calculatedInfillStrokeWeight,
+ getColorPallete("ppP50")[i] // We will want to switch between a pallete and a single colour at some point - how?
+ );
+ infillGroup.appendChild(arc);
+ }
+
+ // Arcs (curves) primary colour (the solid lines or outlines)
+ for (const space of spacing) {
+ const arc = this.createArc(
+ 0,
+ 0,
+ space - this.outlineStrokeWeight / 2,
+ 0,
+ 0 + 90,
+ this.outlineStrokeWeight,
+ this.outlineStrokeColour
+ );
+ outlineGroup.appendChild(arc);
}
}
- return group;
+
+ linesGroup.appendChild(infillGroup);
+ // linesGroup.appendChild(outlineGroup);
+ return linesGroup;
}
- createLine(radius: number, direction: "horizontal" | "vertical"): SVGElement {
+ // Maybe the line is causing problem? (I don't seem to be able to join a line to a path so make the lines paths?)
+ createPath(
+ length: number,
+ direction: "horizontal" | "vertical",
+ strokeWeight?: number,
+ strokeColour?: string
+ ): SVGElement {
console.log(
- "Creating a single line with direction",
+ "Creating a single path with direction",
direction,
"and length",
- radius
+ length,
+ "Stroke-wdith",
+ strokeWeight
);
- const lines = document.createElementNS("http://www.w3.org/2000/svg", "g");
- const lineOffset = 0;
-
- const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
+ const paths = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ const pathOffset = 0;
+ const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
// Horizontal Lines start at x1=0 y1=radius and finish at x2=100, y2=radius
// Vertical Lines start at x1=radius y1=0 and finish at x2=radius, y2=100
+ let d;
if (direction === "horizontal") {
- // Start of line
- line.setAttribute("x1", "0");
- line.setAttribute("y1", String(radius));
- // end of line
- line.setAttribute("x2", "100");
- line.setAttribute("y2", String(radius));
+ d = `M 0 ${length} L 100 ${length}`;
} else {
- // Start of line
- line.setAttribute("x1", String(radius));
- line.setAttribute("y1", "0");
- // end of line
- line.setAttribute("x2", String(radius));
- line.setAttribute("y2", "100");
+ d = `M ${length} 0 L ${length} 100`;
}
- line.setAttribute("stroke", "black");
- line.setAttribute("stroke-width", "1");
+ path.setAttribute("d", d);
+ path.setAttribute("stroke", strokeColour || this.defaultStrokeColour);
+ path.setAttribute(
+ "stroke-width",
+ String(strokeWeight) || String(this.defaultStrokeWeight)
+ );
+ // path.setAttribute("stroke-linecap", "round");
+ // path.setAttribute("stroke-linejoin", "round");
+ path.setAttribute("fill", "none");
- lines.appendChild(line);
+ paths.appendChild(path);
- return lines;
+ return paths;
}
// return an SVG arc for the given start coordinates and radius between the start and end angle
@@ -144,7 +311,9 @@ class Tile {
cy: number,
r: number,
startAngle: number,
- endAngle: number
+ endAngle: number,
+ strokeWeight?: number,
+ strokeColour?: string
): SVGElement {
const startPoint = this.polarToCartesian(cx, cy, r, startAngle);
const endPoint = this.polarToCartesian(cx, cy, r, endAngle);
@@ -168,8 +337,11 @@ class Tile {
arc.setAttribute("d", d);
arc.setAttribute("fill", "none");
- arc.setAttribute("stroke", "black");
- arc.setAttribute("stroke-width", "1");
+ arc.setAttribute("stroke", strokeColour || this.defaultStrokeColour);
+ arc.setAttribute(
+ "stroke-width",
+ String(strokeWeight) || String(this.defaultStrokeWeight)
+ );
return arc;
}
@@ -192,7 +364,6 @@ class Tile {
lineArcIntersection(
lineStart: { x: number; y: number },
lineEnd: { x: number; y: number },
- arcCenter: { x: number; y: number },
arcRadius: number
): { x: number; y: number } | null {
const dx = lineEnd.x - lineStart.x;
diff --git a/src/index.ts b/src/index.ts
index f8c4e95..6656a70 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,9 +1,10 @@
import Tile from "./Tile";
+import { getRandomColor, joinClosePaths } from "./utils";
// Create a grid of patterned tiles
const grid = document.getElementById("grid");
const numRows = 10;
-const numCols = 12;
+const numCols = 10;
const outerSVG = document.createElementNS("http://www.w3.org/2000/svg", "svg");
outerSVG.setAttribute("viewBox", `0 0 ${numCols * 100} ${numRows * 100}`);
@@ -12,30 +13,70 @@ outerSVG.setAttribute("height", "100%");
outerSVG.setAttribute("xmlns", "http://www.w3.org/2000/svg");
grid?.appendChild(outerSVG);
+// Create the background rect element
+const backgroundRect = document.createElementNS(
+ "http://www.w3.org/2000/svg",
+ "rect"
+);
+backgroundRect.setAttribute("width", "100%");
+backgroundRect.setAttribute("height", "100%");
+
+const backgroundColour = getRandomColor();
+backgroundRect.setAttribute("fill", "white");
+
+// Add the background rect to the SVG
+outerSVG.appendChild(backgroundRect);
+// outerSVG.setAttribute("")
+// const strokeColour = getRandomColor(backgroundColour);
+// const strokeWeight = Math.floor(Math.random() * 8) + 1;
// Loop to add tiles to a grid
for (let row = 0; row < numRows; row++) {
for (let col = 0; col < numCols; col++) {
// Chose a random rotation for the tile (constrained to 0, 90, 180 & 270)
const rotation = Math.floor(Math.random() * 4) * 90;
- // const rotation = 0;
+ // const rotation = 90;
// Randomly decide whether to show arcs (defaults to true)
const showArcs = Math.random() < 0.5;
+ // const showArcs = false;
// Add a tile to the grid (all the work is done in the Tile class)
- const tile = new Tile();
+ const tile = new Tile({
+ showArcs,
+ rotation,
+ linesRandomDirection: false,
+ numberOfLines: 7,
+ outlineStrokeWeight: 4,
+ outlineStrokeColour: "black",
+ infillStrokeWeight: 8,
+ });
// Define the SVG element using the tile content
const tileGroup = tile.element;
// Apply a grid offset and rotation to the tile
- tileGroup.setAttribute(
- "transform",
- `translate(${col * 100} ${row * 100}) rotate(${rotation} 50 50)`
- );
+ // tileGroup.setAttribute(
+ // "transform",
+ // `translate(${col * 100} ${row * 100}) rotate(${rotation} 50 50)`
+ // );
// Append the SVG element 'tileGroup' to the SVG
- outerSVG?.appendChild(tileGroup);
+ // outerSVG?.appendChild(tileGroup);
+ // Iterate through the children of tileGroup and append them directly to outerSVG
+ while (tileGroup.firstChild) {
+ const child = tileGroup.firstChild;
+ // Check if the child is an SVGElement
+ if (child instanceof SVGElement) {
+ // Apply a grid offset and rotation to the child element
+ child.setAttribute(
+ "transform",
+ `translate(${col * 100} ${row * 100}) rotate(${rotation} 50 50)`
+ );
+ }
+ outerSVG?.appendChild(child);
+ }
}
}
+// joinClosePaths(outerSVG, 5); // Add this line after creating the grid
+
// SVG Export (Save (Download) an SVG when the download button is clicked)
const downloadButton = document.getElementById("download-svg");
diff --git a/src/utils.ts b/src/utils.ts
new file mode 100644
index 0000000..a431066
--- /dev/null
+++ b/src/utils.ts
@@ -0,0 +1,112 @@
+export const joinClosePaths = (outerSVG: SVGSVGElement, threshold: number) => {
+ const paths = Array.from(outerSVG.querySelectorAll("path"));
+
+ for (let i = 0; i < paths.length; i++) {
+ // console.log("Picking a path");
+ for (let j = i + 1; j < paths.length; j++) {
+ // console.log("Comparing to all other paths");
+ const path1 = paths[i];
+ const path2 = paths[j];
+
+ const path1D = path1.getAttribute("d") as string;
+ const path2D = path2.getAttribute("d") as string;
+
+ const path1Start = path1D.split(" ").slice(1, 3);
+ const path1End = path1D.split(" ").slice(-2);
+
+ const path2Start = path2D.split(" ").slice(1, 3);
+ const path2End = path2D.split(" ").slice(-2);
+
+ const start1 = {
+ x: parseFloat(path1Start[0]),
+ y: parseFloat(path1Start[1]),
+ };
+ const end1 = { x: parseFloat(path1End[0]), y: parseFloat(path1End[1]) };
+ const start2 = {
+ x: parseFloat(path2Start[0]),
+ y: parseFloat(path2Start[1]),
+ };
+ const end2 = { x: parseFloat(path2End[0]), y: parseFloat(path2End[1]) };
+
+ const combinations = [
+ { a: start1, b: start2 },
+ { a: start1, b: end2 },
+ { a: end1, b: start2 },
+ { a: end1, b: end2 },
+ ];
+
+ for (const combination of combinations) {
+ // console.log("Comparing combinations...");
+ const dist = Math.hypot(
+ combination.a.x - combination.b.x,
+ combination.a.y - combination.b.y
+ );
+ if (threshold >= dist) {
+ console.log("too far to join");
+ }
+ if (dist < threshold) {
+ console.log("Found close path ends...");
+ path1.setAttribute(
+ "d",
+ `${path1D} L ${combination.b.x} ${combination.b.y} ${path2D
+ .split(" ")
+ .slice(3)
+ .join(" ")}`
+ );
+ outerSVG.removeChild(path2);
+ paths.splice(j, 1);
+ j--;
+ break;
+ }
+ }
+ }
+ }
+};
+
+export const getRandomColor = (exclude?: string): string => {
+ const filteredColors = exclude
+ ? getColorPallete("basic").filter((color) => color !== exclude)
+ : getColorPallete("basic");
+ const randomIndex = Math.floor(Math.random() * filteredColors.length);
+ return filteredColors[randomIndex];
+};
+
+type PaletteColors = {
+ [key: string]: string[];
+};
+
+const paletteColors: PaletteColors = {
+ basic: [
+ "black",
+ "navy",
+ "blue",
+ "teal",
+ "aqua",
+ "teal",
+ "blue",
+ "navy",
+ "black",
+ ],
+ ppP47: [
+ "rgb(166,217,226)",
+ "rgb(248,171,30)",
+ "rgb(250,210,219)",
+ "rgb(231,35,133)",
+ "rgb(250,210,219)",
+ "rgb(248,171,30)",
+ "rgb(166,217,226)",
+ ],
+ ppP50: [
+ "rgb(242,195,219)",
+ "rgb(218,228,151)",
+ "rgb(120,172,215)",
+ "rgb(248,171,30)",
+ "rgb(120,172,215)",
+ "rgb(218,228,151)",
+ "rgb(242,195,219)",
+ ],
+};
+
+export const getColorPallete = (name: string): string[] => {
+ return paletteColors[name];
+};
diff --git a/styles.css b/styles.css
index c59a7c0..7400628 100644
--- a/styles.css
+++ b/styles.css
@@ -1,26 +1,23 @@
-.tile-container {
- position: relative;
- }
-
- .tile {
- /* position: absolute; */
- /* width: 100px;
- height: 100px;
- */
- vertical-align: top;
- }
- .tile-overlay {
- position: absolute;
- width: 100px;
- height: 100px;
- }
+.tile {
+ /* position: absolute; */
+ /* width: 100px;
+ height: 100px;
+ */
+ /* vertical-align: top; */
+}
- #grid {
- /* display: grid; */
- width: 100%;
- height: 100%;
- grid-template-columns: repeat(auto-fill, 100px);
- grid-template-rows: repeat(auto-fill, 100px);
+#grid {
+ display: flexbox;
+ width: auto;
+ height: auto;
+ /* grid-template-columns: repeat(auto-fill, 100px); */
+ /* grid-template-rows: repeat(auto-fill, 100px); */
/* grid-auto-flow: row; */
- grid-gap: 0; /* Adjust the gap between tiles if needed */
+ /* grid-gap: 1; Adjust the gap between tiles if needed */
+}
+
+#controls {
+ padding-top: 0.5em;
+ padding-left: 0.5em;
+ padding-bottom: 2em;
}