Sample Page

sketch

function setup() {
    createCanvas(windowWidth, windowHeight);
    //  createCanvas(960,600); // nexus 7
    frameRate(30);

    // set display parameters
    bit = min(width, height) / 80
    strokeWeight(0.5 * bit);
    textSize(4 * bit);
    textAlign(RIGHT);

    // initialize view and objects
    streaks = new Streaklines();
    var p1 = createVector(0.25 * width, 0.5 * height);
    var p2 = createVector(0.75 * width, 0.75 * height);
    vline = new VortexLine(p1, p2, 8);
    background(255);
}

function draw() {
    // background
    fill(255, 50);
    noStroke();
    rect(0, 0, width, height);

    // update vortex line
    vline.update();

    // draw objects
    streaks.draw();
    vline.draw()

    // draw text
    fill(0);
    noStroke();
    text("angle of attack: " + degrees(vline.aoa).toFixed(1) + "\u00B0", width - bit, 5 * bit)
    text("maximum camber: " + (vline.cam * 100).toFixed(1) + "%", width - bit, 10 * bit)
    text("lift coefficient: " + vline.lift.toFixed(2), width - bit, 15 * bit)
    text("center of effort: " + vline.pcen.x.toFixed(0) + "%", width - bit, 20 * bit)
}

function mousePressed() {
    vline.mousePressed();
}

// velocity function including free-stream
function velocity(x) {
    var v = vline.velocity(x);
    v.x += 1;
    return v;
}

// Vortex line class
function VortexLine(x0, x1, num) {
    // init control points
    this.control = [x0, p5.Vector.lerp(x0, x1, 1 / 3), p5.Vector.lerp(x0, x1, 2 / 3), x1];

    // init points and panels
    this.num = num;
    this.points = [];
    this.panels = [];
    for (var i = 0; i < this.num; i++) this.panels[i] = new VortexPanel(x0, x1);

    // upate vortex line geometry
    this.geom = function() {
        //update points and panels
        for (var i = 0; i <= this.num; i++) {
            var x = bezierPoint(this.control[0].x, this.control[1].x,
                    this.control[2].x, this.control[3].x, i / this.num),
                y = bezierPoint(this.control[0].y, this.control[1].y,
                    this.control[2].y, this.control[3].y, i / this.num);
            this.points[i] = createVector(x, y);
            if (i > 0) this.panels[i - 1].set(this.points[i - 1], this.points[i]);
        }

        // compute parameters
        var chord = this.control[3].copy().sub(this.control[0]);
        this.l = chord.mag();
        this.aoa = chord.heading()
        this.cam = 0;
        for (var i = 1; i < this.num; i++) {
            var c = this.control[3].copy().sub(this.points[i]).rotate(-this.aoa).y;
            if (abs(c) > abs(this.cam)) this.cam = c;
        }
        this.cam /= this.l;
    }
    this.geom();

    // Determine if a point is being moved
    this.mousePressed = function() {
        this.pressPoint = createVector(mouseX, mouseY);
        this.moving = -1;
        for (var i = 0; i < 4 & this.moving < 0; i++) {
            var d = dist(mouseX, mouseY, this.control[i].x, this.control[i].y)
            if (d < 3 * bit) this.moving = i;
        }
    }

    // Update vortex line
    this.update = function() {
        if (mouseIsPressed & this.moving >= 0) {
            var i = this.moving;

            // highlight moving point
            noStroke();
            fill(0, 20);
            ellipse(this.control[i].x, this.control[i].y, 8 * bit, 8 * bit);

            // move point
            this.control[i].add(createVector(mouseX, mouseY).sub(this.pressPoint));
            this.pressPoint = createVector(mouseX, mouseY);

            this.geom();
        }

        // update gamma with under-relaxed gauss-seidel:
        // ... Note: the upwash IS the residual, and
        // ... I use a[i,i] = upwash(i,gamma[i]=1)-upwash(i,gamma[i]=0)
        for (var i = this.num - 1; i >= 0; i--) {
            var g = this.panels[i].gamma[0],
                u0 = this.upwash(i, 0),
                u1 = this.upwash(i, 1);
            this.gamma_set(i, (g - u0 / (u1 - u0)) / 2);
        }

        // compute lift and center of effort
        var s = 0;
        this.lift = 0;
        this.cen = createVector(0, 0);
        for (var i = 0; i < this.num; i++) {
            var g0 = this.panels[i].gamma[0],
                g1 = this.panels[i].gamma[1],
                c0 = p5.Vector.lerp(this.panels[i].x0, this.panels[i].x1, 1 / 3),
                c1 = p5.Vector.lerp(this.panels[i].x0, this.panels[i].x1, 2 / 3);
            s += g0 + g1;
            this.lift += (g0 + g1) / 2 * this.panels[i].l;
            this.cen.add(c0.mult(g0)).add(c1.mult(g1));
        }
        this.cen.div(s);
        this.lift /= -0.5 * this.l;
        this.pcen = this.cen.copy().sub(this.control[0]).rotate(-this.aoa).mult(100 / this.l);
    }

    // set gamma at point i
    this.gamma_set = function(i, a) {
        this.panels[i].gamma[0] = a;
        if (i > 0) this.panels[i - 1].gamma[1] = a;
    }

    // compute velocity through panels[i] when gamma[i]=gi
    this.upwash = function(i, gi) {
        this.gamma_set(i, gi);
        var x = p5.Vector.lerp(this.points[i], this.points[i + 1], 0.5),
            v = velocity(x); // includes free-stream!
        v.rotate(-this.panels[i].a)
        return v.y
    }

    this.velocity = function(x) {
        v = createVector(0, 0);
        for (var i = 0; i < this.num; i++) v.add(this.panels[i].velocity(x));
        return v
    }

    this.draw = function() {
        // chord line 
        stroke(150, 50);
        line(this.points[0].x, this.points[0].y, this.points[this.num].x, this.points[this.num].y)

        // control points
        fill(255);
        stroke(255, 155, 50, 50);
        line(this.control[0].x, this.control[0].y, this.control[1].x, this.control[1].y, bit, bit);
        line(this.control[2].x, this.control[2].y, this.control[3].x, this.control[3].y, bit, bit);
        ellipse(this.control[1].x, this.control[1].y, 2 * bit, 2 * bit);
        ellipse(this.control[2].x, this.control[2].y, 2 * bit, 2 * bit);

        // vortex panels and end points
        stroke(255, 100, 100);
        for (var i = 0; i < this.num; i++) this.panels[i].draw();
        ellipse(this.control[0].x, this.control[0].y, 2 * bit, 2 * bit);
        ellipse(this.control[3].x, this.control[3].y, 2 * bit, 2 * bit);


        // center of effort
        stroke(100, 255, 100, 200);
        ellipse(this.cen.x, this.cen.y, 2 * bit, 2 * bit);
    }
}

// VortexPanel class
function VortexPanel(x0, x1) {
    this.set = function(x0, x1) {
        this.x0 = x0.copy();
        this.x1 = x1.copy();
        var s = p5.Vector.sub(x1, x0);
        this.l = s.mag();
        this.a = s.heading();
    }
    this.gamma = [0, 0];
    this.set(x0, x1);

    // Induced velocity
    this.velocity = function(x) {
        // transform to panel-frame
        var xp = p5.Vector.sub(x, this.x0); // translate
        xp.rotate(-this.a);

        // compute velocity and rotate back
        var up = this.pVelocity(xp.x, xp.y);
        return up.rotate(this.a);
    }

    // Induced velocity in panel-frame
    this.pVelocity = function(x, y) {
        // r-squared and theta from panels ends to point
        var xp = this.l - x,
            r0 = sq(x) + sq(y),
            t0 = atan2(y, x),
            r1 = sq(xp) + sq(y),
            t1 = atan2(y, -xp);

        // check for r==0 and precompute deltas
        if (r0 < 1e-6 || r1 < 1e-6) return 0;
        var lr = log(r1 / r0) / 2,
            dt = t1 - t0;

        // compute velocity contributions
        var u0 = this.gamma[0] * (-y * lr + xp * dt),
            u1 = this.gamma[1] * (y * lr + x * dt),
            v0 = this.gamma[0] * (xp * lr + y * dt - this.l),
            v1 = this.gamma[1] * (x * lr - y * dt + this.l);
        return createVector((u0 + u1) / TWO_PI / this.l, (v0 + v1) / TWO_PI / this.l);
    }

    this.draw = function() {
        line(this.x0.x, this.x0.y, this.x1.x, this.x1.y);
    }
}

// Streakline class
function Streaklines() {
    this.particles = [];
    for (var i = 0; i < 500; i++) this.particles[i] = new Particle();

    this.draw = function() {
        stroke(0, 100, 255);
        for (var i = 0; i < this.particles.length; i++) {
            this.particles[i].draw();
            this.particles[i].update();
        }
    };
}

// Tracer particle class
function Particle() {
    // Random reset
    this.reset = function() {
        this.age = 0;
        this.life = int(random(200));
        this.p = createVector(random(-100, width), random(1, height));
        this.p0 = createVector(this.p.x, this.p.y);
    }
    this.reset();

    this.draw = function() {
        line(this.p0.x, this.p0.y, this.p.x, this.p.y);
    }

    // Update position using velocity function
    this.update = function() {
        // two-step Runge-Kutta integration
        var h = bit,
            k1 = velocity(this.p),
            k2 = velocity(this.p.copy().add(k1.copy().mult(0.6666 * h))),
            dp = ((k1.mult(0.25)).add(k2.mult(0.75))).mult(h);

        // move particle
        if (dp.mag() < 5 * h) {
            this.p0 = this.p.copy();
            this.p.add(dp);
            this.age++;
            if (this.p.y > 0 & this.p.x < width && this.p.y < height && this.age < this.life) return;
        }
        this.reset();
    }
}