pi-emergence
Version:
Various emergent phenomena
391 lines (293 loc) • 14.2 kB
JavaScript
const ANCHOR_TYPE_HEAD = 1;
const ANCHOR_TYPE_TAIL = 2;
const ANCHOR_TYPE_NONE = 0;
const DEFAULT_SEGMENT_LENGTH = 20;
const DEFAULT_SEGMENT_MAX_VELOCITY = 10;
const DEFAULT_SEGMENT_MASS = 1.0;
const G = 9.8 * (1);
class TentacleSegment {
static colors = ["green"]; // ["#00820077", "green"];
static UI = P5Ui;
/**
*
* @param {Tentacle} tentacle - The tentacle that this segment is a part of
* @param {TentacleSegment} prevSegment - The previous segment in the tentacle
* @param {number} length - The length of this segment
*/
constructor(tentacle, prevSegment = null, options = null) {
// Check to see if the params were switched
if (tentacle === null || !(tentacle instanceof Tentacle))
throw new Error("tentacle must be an instance of Tentacle: " + JSON.stringify(tentacle, null, 4));
if (prevSegment !== null && !(prevSegment instanceof TentacleSegment))
throw new Error("prevSegment must be an instance of TentacleSegment: " + JSON.stringify(prevSegment, null, 4));
if (options === null || typeof options !== "object")
options = {};
// Identifiers
this.id = options.id || Math.floor(Math.random() * 9999999999).toString(36) + (new Date()).getTime().toString();
this.anchorType = typeof options.anchorType === "number" ? options.anchorType : ANCHOR_TYPE_TAIL;
// Visual Properties
this.color = options.color || "yellow";
// Physical Properties
this.length = options.length;
if (typeof this.length !== "number") this.length = DEFAULT_SEGMENT_LENGTH;
this.angle = options.angle || (options.localAngle || 0);
if (typeof this.angle !== "number") this.angle = 0;
this.localAngle = this.angle - (prevSegment?.angle || 0);
this.angleVelocity = 0;
this.angleAcceleration = 0;
this.angleSpring = options.angleSpring;
if (typeof this.angleSpring !== "number") this.angleSpring = 0.0;
this.angleDamping = options.angleDamping;
if (typeof this.angleDamping !== "number") this.angleDamping = 0.9;
this.velocity = createVector(0, 0);
this.acceleration = createVector(0, 0);
this.maxVelocity = options.maxVelocity;
this.mass = options.mass;
this.forces = createVector(0, 0);
if (typeof this.maxVelocity !== "number") this.maxVelocity = DEFAULT_SEGMENT_MAX_VELOCITY;
if (typeof this.mass !== "number") this.mass = DEFAULT_SEGMENT_MASS;
// Geometric Properties
const basePos = !!prevSegment ? prevSegment.tip.position : tentacle.position;
this.base = new TentacleSegmentEndpoint(basePos.copy());
this.tip = new TentacleSegmentEndpoint(basePos.copy().add(createVector(this.length, 0)));
// References
this.tentacle = tentacle;
this.prevSegment = prevSegment;
this.nextSegment = null;
this.totalLength = (prevSegment?.totalLength || 0) + this.length; // Length from the head of the entire tentacle to this end position
if (!prevSegment) this.anchorType = ANCHOR_TYPE_HEAD; // Head/anchor base
else {
this.index = prevSegment.index + 1;
if (!prevSegment.prevSegment) prevSegment.anchorType = ANCHOR_TYPE_HEAD;
else prevSegment.anchorType = ANCHOR_TYPE_NONE;
}
if (typeof this.length !== "number") this.length = 20;
if (this.length <= 0) this.length = 20;
this.onTentacleGrabbed = (tp, t) => {
return 1;
};
this.updateTipAngles(0, true);
}
calculateAngle() {
const a = this.angle;
return createVector(this.length * Math.cos(a), this.length * Math.sin(a));
}
resetState() {
this.clearForces();
}
/**
* Calculates the angle of the segment from the base to the tip.
* @returns {p5.Vector} - The vector from the base of this segment to the tip of this segment
*/
calculateClamAngle() {
const a = -this.angle;
const len = -this.length;
return createVector(len * Math.cos(a), len * Math.sin(a));
}
/**
* Appends a segment to the end of this segment.
* @param options {object} - Segment options
*/
appendSegment(options = {}) {
if (!options) options = {};
if (!!options.color) options.color = this.color;
this.nextSegment = new TentacleSegment(this.tentacle, this, options);
return this.nextSegment;
}
getLocalAngle() {
return this.angle - (this.prevSegment?.angle || 0);
}
clearForces() {
this.tip.forces.set(0, 0);
this.base.forces.set(0, 0);
}
updateAngleBy(angleDelta) {
if (typeof angleDelta !== "number") return;
this.angle += angleDelta;
this.nextSegment?.updateAngleBy(angleDelta);
}
/**
* Adds any forces to this segment, including gravity
*/
updateForces() {
const gravity = createVector(0, G / this.mass);
if (this.prevSegment)
this.base.addForce(gravity);
if (this.nextSegment) {
this.tip.addForce(gravity);
}
}
/**
* Do all calculations (update forces and angles) for this segment and its children. Update positions after this
*/
updatePhysics(isAuto) {
if (!isAuto) {
this.forces = createVector(0, 0);
return;
}
this.updateForces();
if (!!this.nextSegment)
this.nextSegment.updatePhysics(isAuto);
}
/**
* Updates the angles of the two endpoints of this segment (base and tip) - this.angle MUST be set before calling this method.
* @param {number} positionAnchor - 0 = Base: Position is updated relative to the base endpoint, 1 = Tip: Position is updated relative to the tip endpoint
*/
updateTipAngles(positionAnchor = 0, updatePositions = true) {
this.base.angle = this.angle;
this.tip.angle = this.angle - PI;
}
/**
* This sets (recursively) the base of the current segment to the tip of the previous segment (Only position values are updated -- No angles or forces)
* @param {number} parentAngleDiff - The difference in angle between this segment and its parent
*/
updatePositions(parentAngleDiff = 0) {
const isHeadMovable = this.anchorType !== ANCHOR_TYPE_TAIL;
const isTipMovable = this.anchorType !== ANCHOR_TYPE_HEAD;
this.base.updatePositions();
this.tip.updatePositions();
if (!!this.prevSegment)
this.base.position.set(this.prevSegment.tip.position);
this.angle = this.tip.position.sub(this.base.position).heading();
const dir = this.calculateAngle(); //createVector(len * Math.cos(a), len * Math.sin(a));
this.tip.position.set(this.base.position.copy().add(dir));
this.updateTipAngles();
if (!!this.nextSegment)
this.nextSegment.updatePositions(parentAngleDiff);
}
/**
* This sets (recursively, starting from tail going back down to head) the base of the current segment to the tip of the previous segment (Only position values are updated -- No angles or forces)
* @param {number} parentAngleDiff - The difference in angle between this segment and its parent
*/
updatePositionsBackward(parentAngleDiff = 0) {
if (!!this.nextSegment) {
this.tip.position.set(this.nextSegment.base.position);
}
console.log("Updating Positions Backward with Angle: " + (this.angle * DEGREE_RATIO).toFixed(1));
this.updateTipAngles(1, true);
if (!!this.prevSegment) this.prevSegment.updatePositionsBackward(parentAngleDiff);
}
drawLabels(distance = 170) {
const debugLevel = this.tentacle.agent.debugLevel || 0;
const isSelected = this.tentacle.selectedSegment === this || debugLevel > 1;
strokeWeight(1);
if (debugLevel <= 0) {
if (isSelected) TentacleSegment.UI.drawCircleAt(this.base.position, "red", 24, 1);
return;
}
if (!isSelected) return;
const tipPos = this.tip.position;
const basePos = this.base.position;
//const m = basePos.y < this.tentacle.agent.position.y ? -1 : 1;
const m = basePos.y < tipPos.y ? -1 : 1;
const baseLabelPos = basePos.copy().add(createVector(0, distance * m)); //.setHeading(a));
const tipLabelPos = tipPos.copy().add(createVector(0, distance * -m)); //.setHeading(a + offset));
const baseColor = "rgba(0, 255, 255, 1)";
const baseDimColor = "rgba(0, 255, 255, 0.5)";
const tipColor = "rgba(255, 0, 255, 1)";
const tipDimColor = "rgba(255, 0, 255, 0.5)";
noFill();
stroke(baseDimColor);
textAlign(LEFT, TOP);
line(basePos.x, basePos.y, baseLabelPos.x, baseLabelPos.y);
line(baseLabelPos.x + 1, baseLabelPos.y, baseLabelPos.x + 5, baseLabelPos.y);
text(this.base.getDescription(), baseLabelPos.x, baseLabelPos.y);
stroke(tipDimColor);
line(tipPos.x, tipPos.y, tipLabelPos.x, tipLabelPos.y);
line(tipLabelPos.x + 1, tipLabelPos.y, tipLabelPos.x + 5, tipLabelPos.y);
text(this.tip.getDescription(), tipLabelPos.x, tipLabelPos.y);
// ----- Big wheel circles
//noStroke();
// Draw white tip circle (large)
//TentacleSegment.UI.drawCircleAt(tipPos, colorName, this.length);
// Draw base circle (large)
//TentacleSegment.UI.drawCircleAt(basePos, colorName, this.length);
// White Horizontal Line in the base circle
//stroke(colorName);
//line(basePos.x, basePos.y, basePos.x + this.length/2, basePos.y)
// White Horizontal Line in the tip circle
//stroke(colorName);
//line(tipPos.x, tipPos.y, tipPos.x + this.length/2, tipPos.y)
// ------ End Big wheel circles
if (this.tentacle.agent.debugLevel > 1) {
// Note this: The following turns the tip into a knee, and the line drawn is the tibia
stroke("rgba(255, 255, 255, 0.5)"); // Set color to cyan transluscent
const tibiaPos = p5.Vector.sub(tipPos, this.calculateClamAngle()); // Get thet position for the tip of the "tibia"
line(tipPos.x, tipPos.y, tibiaPos.x, tibiaPos.y); // Draw the line from the "knee" (this.tip) to the tip of the "tibia" (new tibiaPos)
}
TentacleSegment.UI.drawCircleAt(this.base.position, "#FF0000", 24, 1);
TentacleSegment.UI.drawCircleAt(this.tip.position, this.color, 8, 2);
strokeWeight(1);
// const grav = createVector(0, G / this.mass);
// const gravityForce = this.addForce(grav).mult(10);
const gravityForce = p5.Vector.mult(this.forces, 10);
const lineLen = (this.length * 2);
const arcLen = (this.length / 2.0);
textAlign(LEFT, CENTER);
// Base End Horizontal Line and angle arc
stroke(baseDimColor);
line(basePos.x - lineLen, basePos.y, basePos.x + lineLen, basePos.y);
text("BASE: " + this.id, basePos.x + lineLen + 10, basePos.y);
let ae = this.getArcEndpoints(this.base.angle);
arc(basePos.x, basePos.y, arcLen, arcLen, ae.start, ae.end, OPEN); // Base angle visual
// Tip End Horizontal Line and angle arc
stroke(tipDimColor);
line(tipPos.x - lineLen, tipPos.y, tipPos.x + lineLen, tipPos.y);
text("TIP: " + this.id, tipPos.x + lineLen + 10, tipPos.y);
ae = this.getArcEndpoints(this.tip.angle);
arc(tipPos.x, tipPos.y, arcLen, arcLen, ae.start, ae.end, OPEN); // Tip angle visual
// Draw Forces/Gravity
noFill();
strokeWeight(1);
// Base Forces
const xx = gravityForce.x / 2;
const yy = gravityForce.y / 2;
stroke(baseDimColor);
line(tipPos.x, tipPos.y, tipPos.x + xx, tipPos.y + yy);
ellipse(tipPos.x + xx, tipPos.y + yy, 6, 6);
// X/Y Gravity
// stroke(baseDimColor);
// line(tipPos.x, tipPos.y, tipPos.x + gravityBase.x, tipPos.y);
// line(tipPos.x, tipPos.y, tipPos.x, tipPos.y + gravityBase.y);
// ellipse(tipPos.x + gravityBase.x, tipPos.y, 6, 6);
// ellipse(tipPos.x, tipPos.y + gravityBase.y, 6, 6);
// Tip Forces
const os = 1;
stroke(tipDimColor);
line(basePos.x, basePos.y, basePos.x + xx, basePos.y + yy);
ellipse(basePos.x + xx, basePos.y + yy, 6, 6);
// X/Y Gravity
// stroke(tipDimColor);
// line(basePos.x, basePos.y, basePos.x + gravityBase.x, basePos.y);
// line(basePos.x, basePos.y, basePos.x, basePos.y + gravityBase.y);
// ellipse(basePos.x + gravityBase.x, basePos.y, 6, 6);
// ellipse(basePos.x, basePos.y + gravityBase.y, 6, 6);
}
getArcEndpoints(angle) {
let arcStart = 0;
let arcEnd = angle;
if (arcEnd < 0) {
let temp = arcStart;
arcStart = arcEnd;
arcEnd = temp;
}
return { start: arcStart, end: arcEnd };
}
draw(selectedId, colorOverride = null) {
const tipPosition = this.tip.position;
if (!tipPosition) throw new Error("Failed to draw segment. No tipPosition was set.");
const color = colorOverride || (this.color || "#999999");
const isSelected = (selectedId === this.id);
const dimColor = !!selectedId ? "#FFFFFF17" : color; //"#FFFFFFAA";
stroke(isSelected ? color : dimColor);
strokeWeight(3);
const basePos = this.base.position;
const tipPos = this.tip.position;
line(basePos.x, basePos.y, tipPos.x, tipPos.y);
// Dot/ball joint
ellipse(tipPosition.x, tipPosition.y, 6, 6);
if (!!this.nextSegment)
this.nextSegment.draw(false, colorOverride);
this.drawLabels();
}
}