Log in to post a comment.

Canvas.setpenopacity(1);

const spacing        = 100; // min=1,   max=200, step=1
const brustRadius    =  8; // min=5,   max=100, step=1
const areolaRadius   =  4; // min=1,   max=100, step=1
const nippleRadius   =  1; // min=1,   max=100, step=1
const height         =  0; // min=-80,   max=80,  step=1
const droop          =   1; // min=0,   max=100, step=1
const areolaNoise    =   0; // min=0,   max=10,  step=0.1
const areolaFreq     =   8; // min=1,   max=60,  step=1
const nippleNoise    =   0; // min=0,   max=10,  step=0.1
const nippleFreq     =   8; // min=1,   max=60,  step=1

const clampedAreola = Math.min(areolaRadius, brustRadius);
const clampedNipple = Math.min(nippleRadius, clampedAreola);

const maxAreolaDisplace = brustRadius - clampedAreola;
const maxNippleDisplace = clampedAreola - clampedNipple;

function makeRng(seed) {
    let s = seed >>> 0;
    return function() {
        s += 0x6D2B79F5;
        let t = Math.imul(s ^ (s >>> 15), 1 | s);
        t ^= t + Math.imul(t ^ (t >>> 7), 61 | t);
        return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
}

function buildNoiseMap(count, freq, amplitude, rng) {
    if (amplitude === 0) return new Array(count).fill(0);
    const anchors = [];
    for (let i = 0; i < freq; i++) {
        anchors.push((rng() - 0.5) * 2 * amplitude);
    }
    return Array.from({ length: count }, (_, i) => {
        const t  = (i / count) * freq;
        const i0 = Math.floor(t) % freq;
        const i1 = (i0 + 1) % freq;
        const f  = t - Math.floor(t);
        const mu = (1 - Math.cos(f * Math.PI)) / 2;
        return anchors[i0] * (1 - mu) + anchors[i1] * mu;
    });
}

function applyNoise(points, amplitude, freq, maxOutward, rng) {
    if (amplitude === 0) return points;
    const cx = points.reduce((s, p) => s + p.x, 0) / points.length;
    const cy = points.reduce((s, p) => s + p.y, 0) / points.length;
    const noiseMap = buildNoiseMap(points.length, freq, amplitude, rng);
    return points.map((p, i) => {
        const dx  = p.x - cx;
        const dy  = p.y - cy;
        const len = Math.sqrt(dx * dx + dy * dy) || 1;
        const offset = Math.min(noiseMap[i], maxOutward);
        return {
            x: p.x + (dx / len) * offset,
            y: p.y + (dy / len) * offset
        };
    });
}

function drawPoints(t, points) {
    t.penup();
    t.goto(points[0].x, points[0].y);
    t.pendown();
    for (let i = 1; i < points.length; i++) {
        t.goto(points[i].x, points[i].y);
    }
}

function buildCirclePoints(cx, cy, r) {
    const pts = [];
    const steps = 180;
    for (let i = 0; i <= steps; i++) {
        const angle = -Math.PI / 2 + (i / steps) * 2 * Math.PI;
        pts.push({ x: cx + r * Math.cos(angle), y: cy + r * Math.sin(angle) });
    }
    return pts;
}

function buildSemiPoints(cx, arcCY, r) {
    const pts = [];
    const steps = 90;
    for (let i = 0; i <= steps; i++) {
        const angle = Math.PI - (i / steps) * Math.PI;
        pts.push({ x: cx + r * Math.cos(angle), y: arcCY + r * Math.sin(angle) });
    }
    return pts;
}

function drawCircle(cx, cy, r, amplitude, freq, maxOutward, rng) {
    const t = new Turtle();
    drawPoints(t, applyNoise(buildCirclePoints(cx, cy, r), amplitude, freq, maxOutward, rng));
}

function drawBreast(cx, cy) {
    const arcCY = cy + height;
    const leftX  = cx - brustRadius;
    const rightX = cx + brustRadius;

    if (droop === 0) {
        const t = new Turtle();
        drawPoints(t, buildCirclePoints(cx, arcCY, brustRadius));
        return;
    }

    const t = new Turtle();
    const leftLine = [
        { x: leftX, y: arcCY },
        { x: leftX, y: arcCY - droop * 0.33 },
        { x: leftX, y: arcCY - droop * 0.66 },
        { x: leftX, y: arcCY - droop }
    ];
    drawPoints(t, leftLine);

    const rightLine = [
        { x: rightX, y: arcCY },
        { x: rightX, y: arcCY - droop * 0.33 },
        { x: rightX, y: arcCY - droop * 0.66 },
        { x: rightX, y: arcCY - droop }
    ];
    drawPoints(t, rightLine);

    drawPoints(t, buildSemiPoints(cx, arcCY, brustRadius));
}

const groupWidth = spacing + brustRadius * 4;
const offsetX    = -groupWidth / 2 + brustRadius;
const cy         = 0;

drawBreast(offsetX, cy);
drawBreast(offsetX + spacing + brustRadius * 2, cy);

const leftCX  = offsetX;
const rightCX = offsetX + spacing + brustRadius * 2;

const rngAreola = makeRng(1);
const rngNipple = makeRng(2);

drawCircle(leftCX,  cy + height, clampedAreola, areolaNoise, areolaFreq, maxAreolaDisplace, rngAreola);
drawCircle(rightCX, cy + height, clampedAreola, areolaNoise, areolaFreq, maxAreolaDisplace, rngAreola);

drawCircle(leftCX,  cy + height, clampedNipple, nippleNoise, nippleFreq, maxNippleDisplace, rngNipple);
drawCircle(rightCX, cy + height, clampedNipple, nippleNoise, nippleFreq, maxNippleDisplace, rngNipple);