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; }