Newer
Older
fractals / src / FractalSVG.ts
@peter peter on 3 Oct 2019 5 KB Added hexagonal base shape.
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),
    );
}