import { Polygon, Point } from "./Polygon"; /** * Concatenates two arrays component wise. * a[i], b[i+offset] => [a[i], b[i+offset]] */ function zip<S,T>(a:S[], b:T[], offset:number = 0): Array<[S, T]> { if(offset < 0) { throw new Error("The offset is negative."); } if(offset % 1 !== 0) { throw new Error("The offset is not an integer."); } if(a.length !== b.length) { throw new Error("Input arrays have different lengths."); } const output:Array<[S, T]> = []; const length = a.length; for(let i=0;i<length;i++) { output.push([ a[i], b[(i+offset)%length], ]); } return output; } function midPoint(a:Point, b:Point, scalar:number): Point { const cx = scalar * (b.x - a.x) + a.x; const cy = scalar * (b.y - a.y) + a.y; return new Point(cx, cy); } function clamp(value:number, lowerBound:number|null = 0, upperBound:number|null = 1): number { if(lowerBound != null) { value = Math.max(lowerBound, value); } if(upperBound != null) { value = Math.min(upperBound, value); } return value; } function randomizeScalar( baseValue:number, randomizeAmplitude:number, lowerBound:number | null, upperBound:number | null, ): number { if(randomizeAmplitude !== 0) { baseValue += 2 * randomizeAmplitude * (Math.random() - 0.5); } return clamp(baseValue, lowerBound, upperBound); } function inwardSpiral(input:Polygon, scalar:number, randomizeShrinkRate:number): Polygon { const lines = zip(input, input, 1); const newPoints = lines.map(([a,b]) => midPoint(a, b, randomizeScalar(scalar, randomizeShrinkRate, 0, 0.9))); return new Polygon(...newPoints); } export function createInwardSpiral( startShape:Polygon, maxiterations:number, shrinkRate:number, randomizeShrinkRate:number, ): Polygon[] { const cycle:Polygon[] = [startShape]; let pNext:Polygon; for(let i=0;i<maxiterations;i++) { pNext = inwardSpiral(startShape, shrinkRate, randomizeShrinkRate); cycle.push(pNext); startShape = pNext; } return cycle; } function polygonToSvg( svgRootElement:SVGSVGElement, polygon:Polygon, { fill }:{fill:string | null}, ): SVGPolygonElement { // Note: it is not possible to create any svg element without the namespace. const polygonSvg = document.createElementNS("http://www.w3.org/2000/svg", "polygon"); polygonSvg.style.fill = fill; for(const corner of polygon) { // This is the only way to create an SVGPoint: const point = svgRootElement.createSVGPoint(); point.x = corner.x; point.y = corner.y; polygonSvg.points.appendItem(point); } return polygonSvg; } function polygonStyle(strength:number) { return { fill: `rgb(0,0,${Math.floor(strength)})`, }; } export function fractalToSvg(fractal:Polygon[][], sideLength:number): SVGSVGElement { // Note: it is not possible to create any svg element without the namespace. const el = document.createElementNS("http://www.w3.org/2000/svg", "svg") as SVGSVGElement; el.width.baseVal.newValueSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX, sideLength); el.height.baseVal.newValueSpecifiedUnits(SVGLength.SVG_LENGTHTYPE_PX, sideLength); el.style.fill = "none"; el.style.stroke = "purple"; el.style.strokeWidth = "1"; let colorStrength = 0; const initialColorStrength = 20; for(const segment of fractal) { colorStrength = initialColorStrength; for(const iteration of segment) { const p = polygonToSvg(el, iteration, polygonStyle(colorStrength)); el.appendChild(p); colorStrength += (255-initialColorStrength)/segment.length; } } return el; } export function createSquare(side:number): Polygon { const square = new Polygon(); square.push(new Point(0,0)); square.push(new Point(0,side)); square.push(new Point(side,side)); square.push(new Point(side,0)); return square; } function createHexagonInSquare(sideLength:number): Polygon { return new Polygon(...[ new Point(sideLength, sideLength/2), new Point(3 * sideLength / 4, 0), new Point(sideLength / 4, 0), new Point(0, sideLength / 2), new Point(sideLength/4, sideLength), new Point(3*sideLength/4, sideLength), ]); } export function createHexagonalBase(sideLength:number): Polygon[] { const center = new Point(sideLength / 2, sideLength / 2); const hexagon = createHexagonInSquare(sideLength); const lines = zip(hexagon, hexagon, 1); const base = lines.map(([a,b]) => { return new Polygon(...[ a, b, center, ]); }); const corner1 = new Point(sideLength, 0); const corner2 = new Point(0,0); const corner3 = new Point(0, sideLength); const corner4 = new Point(sideLength, sideLength); base.push(new Polygon(...[hexagon[0], corner1, hexagon[1]])); base.push(new Polygon(...[hexagon[2], corner2, hexagon[3]])); base.push(new Polygon(...[hexagon[3], corner3, hexagon[4]])); base.push(new Polygon(...[hexagon[5], corner4, hexagon[0]])); return base; } export function inwardSpiralFractal(shape:Polygon, iterations:number, shrinkRate:number, randomize:number): Polygon[][] { const fractal = [ createInwardSpiral(shape, iterations, shrinkRate, randomize), ]; return fractal; } export function createTriangle(sideLength:number): Polygon { return new Polygon( new Point(sideLength/2, 0), new Point(0,sideLength), new Point(sideLength, sideLength), ); }