Circle Vortex

This project was made as an exercise to create generated Art. With this, I tried at first to mimic a soundwave-like circle, but with the many parameters, many different shapes are possible.

Log in to post a comment.

const randomSeed = 0; //min=0 max=1000 step=1

const circlePoints = 350; //min=3 max=1000 step=1
const circleRingNumber = 4; //min=1 max=10 step=1
const circleDistance = 20; //min=10 max=50 step=0.01
const circleDistanceChange = 3; //min=-10 max=10 step=0.1
const circleStartDistance = 10; //min=10 max=50 step=0.1
const circleThicknessMax = 4; //min=0 max=20 step=1

const subringsMax = 20; //min=0 max=50 step=1
const subringsRotationVariation = 0.0 //min=0 max=1 step=0.001
const subringsScaleVariation = 10 //min=0 max=10 step=0.001
const subringsJitterVariation = 0.0 //min=0 max=10 step=0.01

const noiseAmplitude = 8; //min=-50 max=50 step=0.01
const noiseFrequency = 6; //min=0.01 max=10 step=0.001
const noisePhase = 0; //min=0 max=6.2831 step=0.001

const amplitudeNoiseFrequency = 1; //min=1 max=10 step=0.001
const amplitudeNoisePhase = 0; //min=0 max=6.2831 step=0.001
const amplitudeNoiseOffset = -0.25; //min=-1 max=0 step=0.01
const amplitudeStart = 0.0; //min=0 max=3 step=0.01
const amplitudeEnd = 2.0; //min=0 max=3 step=0.01

Canvas.setpenopacity(1);

// Global code will be evaluated once.
const turtle = new Turtle();
turtle.penup();

const randomOffsets = getCircleOffsets(noisePhase);


for (let i=0; i < circleRingNumber; i++) {
    const cd = circleStartDistance + (circleDistance + (circleDistanceChange * i) * i);
    const circle = getCirclePointsPolarCoordinates(cd);
    const circleAmplitude = mix(amplitudeStart, amplitudeEnd, i / (circleRingNumber));
    const jitteredCircle = jitterCircle(circle, circleAmplitude);
    const thickness = Math.max(Math.floor(rand(i, circleThicknessMax)), 1);
    const thickCircle = getThickRing(jitteredCircle, thickness);
    renderPolarCircle(thickCircle);
    
    const subCircles = generateSubRings(jitteredCircle, i);
    subCircles.map(circle => {
        renderPolarCircle(circle);
    })
}

function generateSubRings(inputCircle, seed) {
    const numberOfRings = Math.floor(rand(seed, subringsMax));
    const outputCircles = [];
    for (let i = 0; i < numberOfRings; i++) {
        const scaleNoise = (noise(i) + 0.25) * subringsScaleVariation;
        const scaledCircle = inputCircle.map((vertex, index) => {
           return shiftRadius(vertex, scaleNoise); 
        });
        const rotationNoise = (noise(i) + 0.25)* subringsRotationVariation;
        const rotatedCircle = scaledCircle.map((vertex, index) => {
            return shiftAngle(vertex, rotationNoise);
        });
        const distortedCircle = rotatedCircle.map((vertex, index) => {
            return {
                r: vertex.r + noise(vertex.r) * subringsJitterVariation,
                angle: vertex.angle
            }
        });
        outputCircles.push(distortedCircle);
    }
    return outputCircles;
}

function renderPolarCircle(circle) {
    const cartesianCircle = circle.map((vertex) => {
        return toCartesianCoordinates(vertex)
    });
    renderPoints(cartesianCircle);
}

function renderPoints(points) {
    const startPoint = points[0]
    turtle.goto(startPoint.x, startPoint.y);
    turtle.pendown();
    for (const point of points) {
        turtle.goto(point.x, point.y);
    }
    
    // close the loop
    turtle.goto(startPoint.x, startPoint.y);
    turtle.penup();
}

function toCartesianCoordinates(input) {
    return {
        x: input.r * Math.cos(input.angle),
        y: input.r * Math.sin(input.angle)
    }
}

function getCirclePointsPolarCoordinates(r) {
    const output = []
    for (let i=0; i<circlePoints; i++) {
        const angle = i / circlePoints * Math.PI * 2;
        output.push({r,angle});
    }
    return output;
}

function shiftRadius(vertex, amount) {
    return {
        r: vertex.r + amount,
        angle: vertex.angle
    }
}

function shiftAngle(vertex, amount) {
    return {
        r: vertex.r,
        angle: vertex.angle + amount
    }
}

function jitterCircle(circle, amount) {
    return circle.map((vertex, index) => {
        return shiftRadius(vertex, randomOffsets[index] * noiseAmplitude * amount);
    });
}

function getThickRing(input, thickness=1) {
    const startIndex = thickness * (-1);
    const endIndex = thickness;
    const output = [];
    for (let i = startIndex; i < endIndex; i++) {
        newInput = input.map((vertex) => {
            return shiftRadius(vertex, i * 0.25)
        });
        output.push(...newInput);
    }
    return output;
}

function getCircleOffsets(phase) {
    const output = []
    for (let i=0; i<circlePoints; i++) {
        const t = i / (circlePoints);
        const angle = t * Math.PI * 2;
        const nx = Math.cos(angle) * noiseFrequency + phase;
        const ny = Math.sin(angle) * noiseFrequency + phase;
        
        const ampNx = Math.cos(angle) * amplitudeNoiseFrequency + amplitudeNoisePhase;
        const ampNy = Math.sin(angle) * amplitudeNoiseFrequency + amplitudeNoisePhase;

        const offset = smoothStep(0, 1, noise2D(nx, ny) + amplitudeNoiseOffset);
        const amplitudeInfluence = noise2D(ampNx, ampNy);
        output.push((offset - 0.5) * 2 * amplitudeInfluence);
    }
    return output;
}


function fbm(x, octaves = 4) {
    let value = 0;
    let amplitude = 0.5;
    let frequency = 1;

    for (let i = 0; i < octaves; i++) {
        value += noise(x * frequency) * amplitude;
        frequency *= 2;
        amplitude *= 0.5;
    }

    return value;
}


function noise(x) {
    const i = Math.floor(x);
    const f = x - i;
    const y = mix(rand(i), rand(i + 1.0), smoothStep(0, 1, f));
    return y;
}

function noise2D(x, y) {
    const ix = Math.floor(x);
    const iy = Math.floor(y);

    const fx = x - ix;
    const fy = y - iy;

    const a = rand(ix + iy * 57);
    const b = rand(ix + 1 + iy * 57);
    const c = rand(ix + (iy + 1) * 57);
    const d = rand(ix + 1 + (iy + 1) * 57);

    const ux = smoothStep(0, 1, fx);
    const uy = smoothStep(0, 1, fy);

    const lerpX1 = mix(a, b, ux);
    const lerpX2 = mix(c, d, ux);

    return mix(lerpX1, lerpX2, uy);
}
 


function smoothStep(start, stop, t) {
    const clampedT = clamp((t - start) / (stop - start), 0, 1)
    
    return clampedT * clampedT * (3.0 - 2.0 * clampedT);
}


function clamp(x, min, max) {
    return Math.min(Math.max(min, x), max);
}


function rand(input, factor = 1, bidirectional = false) {
    const r = hash(input + randomSeed);
    return bidirectional ? (r - 0.5) * 2 * factor : r * factor;
}

function hash(x) {
    const s = Math.sin(x * 127.1) * 437578.5453;
    return s - Math.floor(s);
}


function mix(inputA, inputB, t) {
    return (1 - t) * inputA + t * inputB
}