private class Line {
private final PVector origin;
private final PVector vector;
-
+
Line(PVector pt, PVector v) {
origin = pt;
vector = v.get();
vector.normalize();
}
-
+
PVector getPoint() {
return origin;
}
-
+
PVector getVector() {
return vector;
}
-
- PVector getPointAt(float t) {
- PVector pt = PVector.mult(vector, t);
- pt.add(origin);
- return pt;
+
+ PVector getPointAt(final float t) {
+ return PVector.add(origin, PVector.mult(vector, t));
}
-
- boolean isColinear(PVector pt) {
- PVector projected = projected(pt);
+
+ boolean isColinear(final PVector pt) {
+ PVector projected = projectPoint(pt);
return projected.x==pt.x && projected.y==pt.y && projected.z==pt.z;
}
-
- float getTValue(PVector pt) {
+
+ float getTValue(final PVector pt) {
PVector subtraction = PVector.sub(pt, origin);
return subtraction.dot(vector);
- }
-
- PVector projected(PVector pt) {
+ }
+
+ PVector projectPoint(final PVector pt) {
return getPointAt(getTValue(pt));
}
-
- PVector rotatePoint(PVector pt, float rads) {
- Vec3D axisVec3D = new Vec3D(vector.x, vector.y, vector.z);
- Matrix4x4 mat = new Matrix4x4();
- mat.rotateAroundAxis(axisVec3D, rads);
- Vec3D ptVec3D = new Vec3D(pt.x, pt.y, pt.z);
- Vec3D rotatedPt = mat.applyTo(ptVec3D);
- return new PVector(rotatedPt.x, rotatedPt.y, rotatedPt.z);
+
+ PVector rotatePoint(final PVector p, final float t) {
+ final PVector o = origin;
+ final PVector v = vector;
+
+ final float cost = cos(t);
+ final float sint = sin(t);
+
+ float x = (o.x*(v.y*v.y + v.z*v.z) - v.x*(o.y*v.y + o.z*v.z - v.x*p.x - v.y*p.y - v.z*p.z))*(1 - cost) + p.x*cost + (-o.z*v.y + o.y*v.z - v.z*p.y + v.y*p.z)*sint;
+ float y = (o.y*(v.x*v.x + v.z*v.z) - v.y*(o.x*v.x + o.z*v.z - v.x*p.x - v.y*p.y - v.z*p.z))*(1 - cost) + p.y*cost + (o.z*v.x - o.x*v.z + v.z*p.x - v.x*p.z)*sint;
+ float z = (o.z*(v.x*v.x + v.y*v.y) - v.z*(o.x*v.x + o.y*v.y - v.x*p.x - v.y*p.y - v.z*p.z))*(1 - cost) + p.z*cost + (-o.y*v.x + o.x*v.y - v.y*p.x + v.x*p.y)*sint;
+ return new PVector(x, y, z);
}
}
private class Helix {
private final Line axis;
- private final float period;
- private final float rotationPeriod;
- private final float radius;
- private final float girth;
+ private final float period; // period of coil
+ private final float rotationPeriod; // animation period
+ private final float radius; // radius of coil
+ private final float girth; // girth of coil
private final PVector referencePoint;
private float phase;
private PVector phaseNormal;
}
this.referencePoint = pt;
- this.phase = phase;
- setPhaseNormalFromPhase();
- }
-
- private void setPhaseNormalFromPhase() {
- phaseNormal = axis.getVector().cross(axis.rotatePoint(referencePoint, phase));
+ // The normal is calculated by the cross product of the axis
+ // and a random point that is not colinear with it.
+ phaseNormal = axis.getVector().cross(referencePoint);
phaseNormal.normalize();
phaseNormal.mult(radius);
}
-
- private void setPhase(float phase) {
- this.phase = phase;
- setPhaseNormalFromPhase();
+
+ Line getAxis() {
+ return axis;
}
- void step(int deltaMs) {
- setPhase(phase + (deltaMs / rotationPeriod) * TWO_PI);
+ PVector getPhaseNormal() {
+ return phaseNormal;
}
+ float getPhase() {
+ return phase;
+ }
+
+ void step(double deltaMs) {
+ // Rotate
+ if (rotationPeriod != 0) {
+ this.phase = (phase + ((float)deltaMs / (float)rotationPeriod) * TWO_PI);
+ }
+ }
+
PVector pointOnToroidalAxis(float t) {
PVector p = axis.getPointAt(t);
PVector middle = PVector.add(p, phaseNormal);
- return axis.rotatePoint(middle, (t / period) * TWO_PI);
+ return axis.rotatePoint(middle, (t / period) * TWO_PI + phase);
}
- color colorOfPoint(PVector p) {
- // Calculate the projection of this point to the axis.
- PVector projectedPoint = axis.projected(p);
-
+ private float myDist(PVector p1, PVector p2) {
+ final float x = p2.x-p1.x;
+ final float y = p2.y-p1.y;
+ final float z = p2.z-p1.z;
+ return sqrt(x*x + y*y + z*z);
+ }
+
+ color colorOfPoint(final PVector p) {
+ final float t = axis.getTValue(p);
+ final PVector axisPoint = axis.getPointAt(t);
+
+ // For performance reasons, cut out points that are outside of
+ // the tube where the toroidal coil lives.
+ if (abs(myDist(p, axisPoint) - radius) > girth*.5f) {
+ return lx.hsb(0,0,0);
+ }
+
// Find the appropriate point for the current rotation
// of the helix.
- float t = axis.getTValue(projectedPoint);
- PVector toroidPoint = pointOnToroidalAxis(t);
-
+ PVector toroidPoint = axisPoint;
+ toroidPoint.add(phaseNormal);
+ toroidPoint = axis.rotatePoint(toroidPoint, (t / period) * TWO_PI + phase);
+
// The rotated point represents the middle of the girth of
// the helix. Figure out if the current point is inside that
// region.
- float d = PVector.dist(p, toroidPoint);
- boolean inToroid = d < girth;
-
- // Soften edges by fading brightness
+ float d = myDist(p, toroidPoint);
+
+ // Soften edges by fading brightness.
float b = constrain(100*(1 - ((d-.5*girth)/(girth*.5))), 0, 100);
- return color((lx.getBaseHuef() + (360*(phase / TWO_PI)))%360, (inToroid ? 80 : 0), b);
+ return lx.hsb((lx.getBaseHuef() + (360*(phase / TWO_PI)))%360, 80, b);
+ }
+ }
+
+ private class BasePairInfo {
+ Line line;
+ float colorPhase1;
+ float colorPhase2;
+
+ BasePairInfo(Line line, float colorPhase1, float colorPhase2) {
+ this.line = line;
+ this.colorPhase1 = colorPhase1;
+ this.colorPhase2 = colorPhase2;
}
}
private final Helix h1;
private final Helix h2;
-
+ private final BasePairInfo[] basePairs;
+
private final BasicParameter helix1On = new BasicParameter("H1ON", 1);
private final BasicParameter helix2On = new BasicParameter("H2ON", 1);
+ private final BasicParameter basePairsOn = new BasicParameter("BPON", 1);
+
+ private static final float helixCoilPeriod = 100;
+ private static final float helixCoilRadius = 50;
+ private static final float helixCoilGirth = 30;
+ private static final float helixCoilRotationPeriod = 5000;
+
+ private static final float spokePeriod = 40;
+ private static final float spokeGirth = 20;
+ private static final float spokePhase = 10;
+ private static final float spokeRadius = helixCoilRadius - helixCoilGirth*.5f;
+ private static final float tMin = -200;
+ private static final float tMax = 200;
+
public HelixPattern(GLucose glucose) {
super(glucose);
-
+
addParameter(helix1On);
addParameter(helix2On);
-
+ addParameter(basePairsOn);
+
+ PVector origin = new PVector(100, 50, 55);
+ PVector axis = new PVector(1,0,0);
+
h1 = new Helix(
- new Line(new PVector(100, 50, 70), new PVector(1,0,0)),
- 700, // period
- 50, // radius
- 30, // girth
- 0, // phase
- 10000); // rotation period (ms)
+ new Line(origin, axis),
+ helixCoilPeriod,
+ helixCoilRadius,
+ helixCoilGirth,
+ 0,
+ helixCoilRotationPeriod);
h2 = new Helix(
- new Line(new PVector(100, 50, 70), new PVector(1,0,0)),
- 700,
- 50,
- 30,
+ new Line(origin, axis),
+ helixCoilPeriod,
+ helixCoilRadius,
+ helixCoilGirth,
PI,
- 10000);
+ helixCoilRotationPeriod);
+
+ basePairs = new BasePairInfo[(int)floor((tMax - tMin)/spokePeriod)];
+ }
- // TODO(shaheen) calculate line segments between
- // toroidal points selected by stepping the
- // parameterized t value. select base pairs and
- // associated colors. lerp between colors for each
- // base pair to produce a DNA effect.
-
+ private void calculateSpokes() {
+ float colorPhase = PI/6;
+ for (float t = tMin + spokePhase; t < tMax; t += spokePeriod) {
+ int spokeIndex = (int)floor((t - tMin)/spokePeriod);
+ PVector h1point = h1.pointOnToroidalAxis(t);
+ PVector spokeCenter = h1.getAxis().getPointAt(t);
+ PVector spokeVector = PVector.sub(h1point, spokeCenter);
+ Line spokeLine = new Line(spokeCenter, spokeVector);
+ basePairs[spokeIndex] = new BasePairInfo(spokeLine, colorPhase * spokeIndex, colorPhase * (spokeIndex + 1));
+ }
}
- void run(int deltaMs) {
+ private color calculateSpokeColor(final PVector pt) {
+ // Find the closest spoke's t-value and calculate its
+ // axis. Until everything animates in the model reference
+ // frame, this has to be calculated at every step because
+ // the helices rotate.
+ Line axis = h1.getAxis();
+ float t = axis.getTValue(pt) + spokePhase;
+ int spokeIndex = (int)floor((t - tMin + spokePeriod/2) / spokePeriod);
+ if (spokeIndex < 0 || spokeIndex >= basePairs.length) {
+ return lx.hsb(0,0,0);
+ }
+ BasePairInfo basePair = basePairs[spokeIndex];
+ Line spokeLine = basePair.line;
+ PVector pointOnSpoke = spokeLine.projectPoint(pt);
+ float d = PVector.dist(pt, pointOnSpoke);
+ float b = (PVector.dist(pointOnSpoke, spokeLine.getPoint()) < spokeRadius) ? constrain(100*(1 - ((d-.5*spokeGirth)/(spokeGirth*.5))), 0, 100) : 0.f;
+ float phase = spokeLine.getTValue(pointOnSpoke) < 0 ? basePair.colorPhase1 : basePair.colorPhase2;
+ return lx.hsb((lx.getBaseHuef() + (360*(phase / TWO_PI)))%360, 80.f, b);
+ }
+
+ void run(double deltaMs) {
boolean h1on = helix1On.getValue() > 0.5;
boolean h2on = helix2On.getValue() > 0.5;
-
+ boolean spokesOn = (float)basePairsOn.getValue() > 0.5;
+
h1.step(deltaMs);
h2.step(deltaMs);
-
- for (Point p : model.points) {
- color h1c = color(0,0,0);
- color h2c = color(0,0,0);
-
- if (h1on) {
- h1c = h1.colorOfPoint(new PVector(p.x,p.y,p.z));
+ calculateSpokes();
+
+ for (LXPoint p : model.points) {
+ PVector pt = new PVector(p.x,p.y,p.z);
+ color h1c = h1.colorOfPoint(pt);
+ color h2c = h2.colorOfPoint(pt);
+ color spokeColor = calculateSpokeColor(pt);
+
+ if (!h1on) {
+ h1c = lx.hsb(0,0,0);
}
-
- if (h2on) {
- h2c = h2.colorOfPoint(new PVector(p.x,p.y,p.z));
+
+ if (!h2on) {
+ h2c = lx.hsb(0,0,0);
}
-
+
+ if (!spokesOn) {
+ spokeColor = lx.hsb(0,0,0);
+ }
+
// The helices are positioned to not overlap. If that changes,
// a better blending formula is probably needed.
- colors[p.index] = blendColor(h1c, h2c, ADD);
+ colors[p.index] = blendColor(blendColor(h1c, h2c, ADD), spokeColor, ADD);
}
}
}