Files
animate/docs/js/objects.js
Sam 1fe4d3763e Add spiralProngs to Phyllotaxis and update config
Introduces a new 'spiralProngs' property to the Phyllotaxis class and its configuration, allowing control over spiral prong count. Also expands the 'width' range for NewWave in the config. Updates the drawSpiral method to use spiralProngs for angle calculation.
2025-09-01 18:42:23 +12:00

1211 lines
40 KiB
JavaScript

class BaseShape {
constructor() {
this.controls = []; // Keep track of created elements and event listeners
this.speedMultiplier = 100;
}
initialise(config) {
for (let item of config) {
const { element, listener, filtersDiv } = addControl(item, this);
this.controls.push({ element, listener, });
if (item.type === "range" && item.property !== "rays") {
// Initialize rangeFilter array for this control
const controlIndex = this.controls.length - 1;
this.controls[controlIndex].rangeFilters = [];
let addFilterButton = document.createElement("button");
addFilterButton.innerText = "Add Filter";
addFilterButton.className = "add-filter-button";
// Store the control index in the click handler closure
addFilterButton.addEventListener("click", () => {
const { filterDiv, eventListener, min, max, rate } = createFilter(item);
filtersDiv.appendChild(filterDiv);
// Use the stored control index
if (this.controls[controlIndex] && this.controls[controlIndex].rangeFilters) {
this.controls[controlIndex].rangeFilters.push({
element: filterDiv,
listener: eventListener,
min: min,
max: max,
rate: rate,
});
} else {
console.error("Control or rangeFilters not found for index:", controlIndex);
}
});
filtersDiv.appendChild(addFilterButton);
}
}
const { element, listener } = addControl({ type: "range", min: 1, max: 500, defaultValue: 100, property: "speedMultiplier" }, this);
this.controls.push({ element, listener });
}
remove() {
this.controls.forEach(({ element, listener }) => {
if (element && listener) {
element.removeEventListener("input", listener);
element.removeEventListener("click", listener);
element.removeEventListener("change", listener);
}
else {
console.log("Element or listener not found for removal:", element, listener);
}
// Find and remove the container div instead of individual elements
if (element && element.id) {
// Handle header elements which don't have container
if (element.className === "header") {
if (element.parentElement) {
element.parentElement.removeChild(element);
}
} else {
// For regular controls, find and remove the container
const containerDiv = element.closest(".control-container");
if (containerDiv && containerDiv.parentElement) {
containerDiv.parentElement.removeChild(containerDiv);
}
}
}
});
this.controls = [];
}
updateFilters(elapsed) {
for (let i = 0; i < this.controls.length; i++) {
const control = this.controls[i];
if (control.rangeFilters?.length > 0) {
let newValue = 0;
for (let j = 0; j < control.rangeFilters.length; j++) {
const filter = control.rangeFilters[j];
// const value = parseFloat(filter.element.value);
const min = parseFloat(filter.min.value);
const max = parseFloat(filter.max.value);
const rate = parseFloat(filter.rate.value);
const halfRange = (max - min) / 2;
const filterValue = min + halfRange + Math.sin(elapsed * (1 / rate)) * halfRange; // Calculate the new value based on the range
if (filterValue >= min && filterValue <= max) {
// console.log(newValue, min, max)
newValue += filterValue;
console.log("New Value:", newValue, filterValue, min, max);
}
}
control.element.value = newValue;
const event = new Event('input', { bubbles: true });
control.element.dispatchEvent(event);
}
}
}
draw() {
throw new Error("Draw function not implemented");
}
}
class PolyTwistColourWidth extends BaseShape {
constructor(sides, width, line_width, depth, rotation, speedMultiplier, colour1, colour2) {
super();
this.sides = sides;
this.width = width;
this.line_width = line_width;
this.depth = depth;
this.rotation = rotation;
this.speedMultiplier = speedMultiplier;
this.colour1 = colour1;
this.colour2 = colour2;
}
draw(rotation) {
rotation *= (this.speedMultiplier / 100)
let out_angle = 0;
const innerAngle = 180 - ((this.sides - 2) * 180) / this.sides;
const scopeAngle = rotation - (innerAngle * Math.floor(rotation / innerAngle));
if (scopeAngle < innerAngle / 2) {
out_angle = innerAngle / (2 * Math.cos((2 * Math.PI * scopeAngle) / (3 * innerAngle))) - innerAngle / 2;
} else {
out_angle = -innerAngle / (2 * Math.cos(((2 * Math.PI) / 3) - ((2 * Math.PI * scopeAngle) / (3 * innerAngle)))) + (innerAngle * 3) / 2;
}
let minWidth = Math.sin(rad(innerAngle / 2)) * (0.5 / Math.tan(rad(innerAngle / 2))) * 2;
let widthMultiplier = minWidth / Math.sin(Math.PI / 180 * (90 + innerAngle / 2 - out_angle + innerAngle * Math.floor(out_angle / innerAngle)));
for (let i = 0; i < this.depth; i++) {
const fraction = i / this.depth;
const ncolour = LerpHex(this.colour1, this.colour2, fraction);
DrawPolygon(this.sides, this.width * widthMultiplier ** i, out_angle * i + this.rotation, ncolour, this.line_width);
}
}
}
class FloralPhyllo extends BaseShape {
constructor(width, depth, start, colour1, colour2) {
super();
this.width = width;
this.depth = depth;
this.start = start;
this.colour1 = colour1;
this.colour2 = colour2;
this.speedMultiplier = 500;
}
draw(rotation) {
rotation *= (this.speedMultiplier / 500)
rotation += this.start
// var c = 24; //something to do with width. but not width
var c = 1; //something to do with width. but not width
//dont make larger than 270 unless altering the number of colours in lerpedColours
for (let n = this.depth; n > 0; n -= 1) {
let colVal = waveNormal(n, this.depth)
let ncolour = LerpHex(this.colour1, this.colour2, n / this.depth);
const a = n * rotation / 1000; //137.5;
const r = c * Math.sqrt(n);
const x = r * Math.cos(a) + centerX;
const y = r * Math.sin(a) + centerY;
drawEyelid(n * 2.4 + 40, x, y, ncolour);
}
}
}
class Spiral1 extends BaseShape {
constructor(sides, width, colour) {
super();
this.sides = sides;
this.width = width;
this.colour = colour;
}
draw(rotation) {
rotation *= (this.speedMultiplier / 100)
var rot = Math.round((this.sides - 2) * 180 / this.sides * 2)
var piv = 360 / this.sides;
var stt = 0.5 * Math.PI - rad(rot) //+ rad(rotation);
var end = 0;
var n = this.width / ((this.width / 10) * (this.width / 10)) //pixel correction for mid leaf
for (let i = 1; i < this.sides + 1; i++) {
end = stt + rad(rot);
ctx.lineWidth = 5
ctx.beginPath();
ctx.arc(centerX + Math.cos(rad(90 + piv * i + rotation)) * this.width, centerY + Math.sin(rad(90 + piv * i + rotation)) * this.width, this.width, stt + rad(rotation) - (stt - end) / 2, end + rad(rotation) + rad(n), 0);
ctx.strokeStyle = this.colour;
ctx.stroke();
ctx.beginPath();
ctx.arc(centerX + Math.cos(rad(90 + piv * i - rotation)) * this.width, centerY + Math.sin(rad(90 + piv * i - rotation)) * this.width, this.width, stt - rad(rotation), end - (end - stt) / 2 + rad(n) - rad(rotation), 0);
ctx.strokeStyle = this.colour;
ctx.stroke();
stt = end + -(rad(rot - piv)) //+rad(30);
}
}
}
class FloralAccident extends BaseShape {
constructor(sides, width, colour) {
super();
this.sides = sides;
this.width = width;
this.colour = colour;
}
draw(rotation) {
rotation *= (this.speedMultiplier / 100)
var rot = Math.round((this.sides - 2) * 180 / this.sides * 2)
var piv = 360 / this.sides;
var stt = 0.5 * Math.PI - rad(rot) //+ rad(rotation);
var end = 0;
var n = this.width / ((this.width / 10) * (this.width / 10)) //pixel correction for mid leaf
for (let i = 1; i < this.sides + 1; i++) {
end = stt + rad(rot);
ctx.beginPath();
ctx.arc(centerX + Math.cos(rad(90 + piv * i + rotation)) * this.width, centerY + Math.sin(rad(90 + piv * i + rotation)) * this.width, this.width, stt - (stt - end + rad(rotation)) / 2, end + rad(n), 0);
ctx.strokeStyle = this.colour;
ctx.stroke();
ctx.beginPath();
ctx.arc(centerX + Math.cos(rad(90 + piv * i - rotation)) * this.width, centerY + Math.sin(rad(90 + piv * i - rotation)) * this.width, this.width, stt, end - (end - stt - rad(rotation)) / 2 + rad(n), 0);
ctx.strokeStyle = this.colour;
ctx.stroke();
stt = end + -(rad(rot - piv)) //+rad(30);
}
}
}
class FloralPhyllo_Accident extends BaseShape {
constructor(sides, width, colour1, colour2) {
super();
this.sides = sides;
this.width = width;
this.colour1 = colour1;
this.colour2 = colour2;
}
draw(rotation) {
rotation *= (this.speedMultiplier / 100)
var c = 24; //something to do with width. but not width
for (let n = 0; n < 300; n += 1) {
let ncolour = LerpHex(this.colour1, this.colour2, Math.cos(rad(n / 2)));
let a = n * (rotation / 1000 + 100); //137.5;
let r = c * Math.sqrt(n);
let x = r * Math.cos(a) + centerX;
let y = r * Math.sin(a) + centerY;
drawEyelidAccident(x, y);
}
}
}
class Nodal_expanding extends BaseShape {
constructor(expand, points, start, line_width, colour1, colour2, colour_change) {
super();
this.expand = expand;
this.points = points;
this.start = start;
this.line_width = line_width;
this.colour1 = colour1;
this.colour2 = colour2;
this.colour_change = colour_change
}
draw(rotation) {
rotation *= (this.speedMultiplier / 1000)
var angle = (360 / 3000 * rotation) + this.start //2000 controls speed
var length = this.expand;
for (let z = 1; z <= this.points; z++) { //why specifically 2500
ctx.beginPath();
let ncolour = LerpHex(this.colour1, this.colour2, z / this.points);
ctx.moveTo(centerX + (Math.cos(rad(angle * (z - 1) + 0)) * (length - this.expand)), centerY + (Math.sin(rad(angle * (z - 1) + 0)) * (length - this.expand)));
ctx.lineTo(centerX + (Math.cos(rad(angle * z + 0)) * length), centerY + (Math.sin(rad(angle * z + 0)) * length));
length += this.expand;
ctx.lineWidth = this.line_width;//try 1
ctx.strokeStyle = ncolour;
ctx.lineCap = "round"
// ctx.strokeStyle = colourToText(ncolour);
console.log(ncolour)
ctx.stroke();
}
}
}
class Phyllotaxis extends BaseShape {
constructor(width, size, sizeMin, start, nMax, wave, spiralProngs, colour1, colour2) {
super();
this.width = width;
this.size = size;
this.sizeMin = sizeMin;
this.start = start;
this.nMax = nMax;
this.wave = wave;
this.spiralProngs = spiralProngs;
this.colour1 = colour1;
this.colour2 = colour2;
}
drawWave(angle) {
angle /= 1000
const startColor = [45, 129, 252];
const endColor = [252, 3, 98];
const distanceMultiplier = 3;
const maxIterations = this.nMax;
// angle=0;
for (let n = 0; n < maxIterations; n++) {
ctx.beginPath();
const nColor = lerpRGB(startColor, endColor, Math.cos(rad(n / 2)));
// const nAngle = n* angle ;
// const nAngle = n*angle+ Math.sin(rad(n*1+angle*4000))/1 ;
const nAngle = n * angle + Math.sin(rad(n * 1 + angle * 40000)) / 2;
const radius = distanceMultiplier * n;
const xCoord = radius * Math.cos(nAngle) + centerX;
const yCoord = radius * Math.sin(nAngle) + centerY;
ctx.arc(xCoord, yCoord, this.size, 0, 2 * Math.PI);
ctx.fillStyle = colourToText(nColor);
ctx.fill();
}
}
drawSpiral(angle) {
angle /= 5000
const startColor = [45, 129, 252];
const endColor = [252, 3, 98];
const distanceMultiplier = 2;
const maxIterations = 1000;
ctx.beginPath();
ctx.moveTo(centerX, centerY); // Start at the center
for (let n = 0; n < maxIterations; n++) {
const nColor = lerpRGB(startColor, endColor, Math.cos(rad(n / 2)));
const nAngle = n * angle + Math.sin(angle * n * this.spiralProngs);
const radius = distanceMultiplier * n;
const xCoord = radius * Math.cos(nAngle) + centerX;
const yCoord = radius * Math.sin(nAngle) + centerY;
// ctx.arc(xCoord, yCoord, 8, 0, 2 * Math.PI);
ctx.lineTo(xCoord, yCoord);
// ctx.fillStyle = colourToText(nColor);
// ctx.fill();
}
ctx.stroke();
}
// Draw_nodal(300, 100, 31, rotation, "blue");
draw(rotation) {
rotation *= (this.speedMultiplier / 300)
rotation += this.start
const sizeMultiplier = this.nMax / this.size + (5 - 3)
if (this.wave === 1) {
this.drawWave(rotation)
}
else if (this.wave === 2) {
this.drawSpiral(rotation)
}
else {
for (let n = 0; n < this.nMax; n += 1) {
const ncolour = LerpHex(this.colour1, this.colour2, n / this.nMax);
// const ncolour = LerpHex(this.colour1, this.colour2, (n/this.nMax)**2);
const a = n * (rotation / 1000)//137.5;
const r = this.width * Math.sqrt(n);
const x = r * Math.cos(a) + centerX;
const y = r * Math.sin(a) + centerY;
ctx.beginPath();
ctx.arc(x, y, (n / sizeMultiplier) + this.sizeMin, 0, 2 * Math.PI);
ctx.fillStyle = ncolour;
// ctx.fillStyle = colourToText(ncolour);
ctx.fill();
// console.log(this.c)
}
}
}
}
class SquareTwist_angle extends BaseShape {
constructor(width, line_width, colour1) {
super();
this.width = width;
this.line_width = line_width;
this.colour1 = colour1;
}
drawSquare(angle, size, colour) {
ctx.save();
ctx.translate(centerX, centerY)//-(Math.sin(rad(angle)) *centerX));
ctx.rotate(rad(angle + 180));
ctx.beginPath();
ctx.strokeStyle = colour;
ctx.lineWidth = this.line_width;
ctx.rect(-size / 2, -size / 2, size, size);
ctx.stroke();
ctx.restore();
}
// DrawSquareTwist_angle(400,0,rotation,"red")
draw(rotation) {
rotation *= (this.speedMultiplier / 100)
let out_angle = rotation;
let widthMultiplier = 1 / (2 * Math.sin(Math.PI / 180 * (130 - out_angle + 90 * Math.floor(out_angle / 90)))) + 0.5
for (let i = 0; i < 25; i++) {
this.drawSquare(rotation * i, this.width * widthMultiplier ** i, this.colour1)
}
}
}
class CircleExpand extends BaseShape {
constructor(nCircles, gap, linear, heart, colour1, colour2) {
super();
this.nCircles = nCircles;
this.gap = gap;
this.linear = linear;
this.heart = heart;
this.colour1 = colour1;
this.colour2 = colour2
}
lerpColor(a, b, amount) {
var ah = +a.replace('#', '0x'),
ar = ah >> 16, ag = ah >> 8 & 0xff, ab = ah & 0xff,
bh = +b.replace('#', '0x'),
br = bh >> 16, bg = bh >> 8 & 0xff, bb = bh & 0xff,
rr = ar + amount * (br - ar),
rg = ag + amount * (bg - ag),
rb = ab + amount * (bb - ab);
return '#' + ((1 << 24) + (rr << 16) + (rg << 8) + rb | 0).toString(16).slice(1);
}
arraySort(x, y) {
if (x.r > y.r) {
return 1;
}
if (x.r < y.r) {
return -1;
}
return 0;
}
drawHeart(w, colour) {
// var w = 200
ctx.strokeStyle = "black";
ctx.fillStyle = colour;
ctx.lineWidth = 1;
var x = centerX - w / 2;
let y = centerY - w / 2
ctx.beginPath();
ctx.moveTo(x, y + w / 4);
ctx.quadraticCurveTo(x, y, x + w / 4, y);
ctx.quadraticCurveTo(x + w / 2, y, x + w / 2, y + w / 5);
ctx.quadraticCurveTo(x + w / 2, y, x + w * 3 / 4, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + w / 4);
ctx.quadraticCurveTo(x + w, y + w / 2, x + w * 3 / 4, y + w * 3 / 4);
ctx.lineTo(x + w / 2, y + w);
ctx.lineTo(x + w / 4, y + w * 3 / 4);
ctx.quadraticCurveTo(x, y + w / 2, x, y + w / 4);
ctx.stroke();
ctx.fill();
}
draw(rotation) {
rotation *= (0.9)
ctx.strokeWeight = 1;
ctx.lineWidth = 1;
let arrOfWidths = []
let arrOfco = []
let intRot;
if (this.linear) {
intRot = Math.floor(rotation * 30) / 100
}
else {
intRot = Math.sin(rad(Math.floor(rotation * 30) / 4)) + rotation / 4
}
for (let i = 0; i < this.nCircles; i++) {
const width = this.gap * ((intRot + i) % this.nCircles);
const colour = (Math.sin(rad(i * (360 / this.nCircles) - 90)) + 1) / 2
arrOfWidths.push({ r: width, c: colour });
}
let newArr = arrOfWidths.sort(this.arraySort)
for (let i = this.nCircles - 1; i >= 0; i--) {
let newColour = this.lerpColor(this.colour1, this.colour2, newArr[i].c)
if (this.heart) {
this.drawHeart(newArr[i].r, newColour)
}
else {
ctx.beginPath();
ctx.arc(centerX, centerY, newArr[i].r, 0, 2 * Math.PI);
ctx.fillStyle = newColour;
ctx.fill();
ctx.stokeStyle = "black";
ctx.stroke();
}
}
}
}
class EyePrototype extends BaseShape {
constructor(x, y, rotate, flip, width, blink_speed, draw_spiral, spiral_full, draw_pupil, draw_expand, draw_hypno, line_width, colourPupil, colourSpiral, colourExpand) {
super();
this.x = x;
this.y = y;
this.rotate = rotate;
this.flip = flip
this.width = width;
this.blink_speed = blink_speed;
this.line_width = line_width;
this.step = 0;
this.opening = true;
this.counter = 0;
this.cooldown = 0;
this.draw_spiral = draw_spiral;
this.spiral_full = spiral_full;
this.draw_pupil = draw_pupil;
this.draw_expand = draw_expand;
this.draw_hypno = draw_hypno;
this.colourPupil = colourPupil;
this.colourSpiral = colourSpiral;
this.colourExpand = colourExpand;
this.centerPulse = new CircleExpand(10, 30, 1, 0, "#2D81FC", "#FC0362")
}
drawEyelid(rotation) {
ctx.strokeStyle = "orange";
let relCenterX = centerX + this.x;
let relCenterY = centerY + this.y;
rotation *= (this.speedMultiplier / 100)
ctx.lineWidth = 1;
ctx.beginPath();
let newPoint = 0
let newPoint1 = 0
let addedRotate = this.flip ? 90 : 0
newPoint = rotatePoint(- this.width / 2, 0, this.rotate + addedRotate)
ctx.moveTo(relCenterX + newPoint[0], relCenterY + newPoint[1]);
newPoint = rotatePoint(0, - rotation / 400 * this.width, this.rotate + addedRotate)
newPoint1 = rotatePoint(this.width / 2, 0, this.rotate + addedRotate)
ctx.quadraticCurveTo(relCenterX + newPoint[0], relCenterY + newPoint[1], relCenterX + newPoint1[0], relCenterY + newPoint1[1]);
newPoint = rotatePoint(- this.width / 2, 0, this.rotate + addedRotate)
ctx.moveTo(relCenterX + newPoint[0], relCenterY + newPoint[1]);
newPoint = rotatePoint(0, + rotation / 400 * this.width, this.rotate + addedRotate)
newPoint1 = rotatePoint(this.width / 2, 0, this.rotate + addedRotate)
ctx.quadraticCurveTo(relCenterX + newPoint[0], relCenterY + newPoint[1], relCenterX + newPoint1[0], relCenterY + newPoint1[1]);
ctx.stroke();
}
eyelidCut(rotation) {
let relCenterX = centerX + this.x;
let relCenterY = centerY + this.y;
let newPoint = 0
let newPoint1 = 0
let addedRotate = this.flip ? 90 : 0
// ctx.lineWidth = 1;
let squarePath = new Path2D();
newPoint = rotatePoint(- this.width / 2, 0, this.rotate + addedRotate)
squarePath.moveTo(relCenterX + newPoint[0], relCenterY + newPoint[1]);
newPoint = rotatePoint(0, - rotation / 400 * this.width, this.rotate + addedRotate)
newPoint1 = rotatePoint(this.width / 2, 0, this.rotate + addedRotate)
squarePath.quadraticCurveTo(relCenterX + newPoint[0], relCenterY + newPoint[1], relCenterX + newPoint1[0], relCenterY + newPoint1[1]);
newPoint = rotatePoint(- this.width / 2, 0, this.rotate + addedRotate)
squarePath.moveTo(relCenterX + newPoint[0], relCenterY + newPoint[1]);
newPoint = rotatePoint(0, + rotation / 400 * this.width, this.rotate + addedRotate)
newPoint1 = rotatePoint(this.width / 2, 0, this.rotate + addedRotate)
squarePath.quadraticCurveTo(relCenterX + newPoint[0], relCenterY + newPoint[1], relCenterX + newPoint1[0], relCenterY + newPoint1[1]);
ctx.clip(squarePath);
}
drawGrowEye(step) {
// console.log(step)
ctx.strokeStyle = this.colourExpand
ctx.beginPath();
ctx.lineWidth = 5;
ctx.arc(centerX + this.x, centerY + this.y, step, 0, 2 * Math.PI);
ctx.stroke();
}
drawCircle(step) {
ctx.strokeStyle = this.colourPupil
ctx.beginPath();
ctx.lineWidth = 5;
ctx.arc(centerX + this.x, centerY + this.y, step, 0, 2 * Math.PI);
ctx.stroke();
}
drawSpiral(step) {
ctx.strokeStyle = this.colourSpiral;
let a = 1
let b = 5
ctx.moveTo(centerX, centerY);
ctx.beginPath();
let max = this.spiral_full ? this.width : this.width / 2
for (let i = 0; i < max; i++) {
let angle = 0.1 * i;
let x = centerX + (a + b * angle) * Math.cos(angle + step / 2);
let y = centerY + (a + b * angle) * Math.sin(angle + step / 2);
ctx.lineTo(x + this.x, y + this.y);
}
ctx.lineWidth = 3;
ctx.stroke();
}
stepFunc() {
if (this.cooldown != 0) {
this.cooldown--;
} else {
if (this.opening == true) {
if (this.step >= 200) {
this.cooldown = 200;
this.opening = false;
this.step -= this.blink_speed;
} else {
this.step += this.blink_speed;
}
} else {
if (this.step <= 0) {
this.opening = true;
this.step += this.blink_speed;
} else {
this.step -= this.blink_speed;
}
}
}
}
draw(rotation) {
let speedMult = 50
console.log(this.blink_speed)
let waitTime = this.blink_speed
let cap = 200
let d = waitTime * speedMult * 10
let a = cap * 2 + d
let outputRotation = Math.min(Math.abs((Math.floor(rotation * speedMult) % a) - a / 2 - d / 2), cap)
ctx.fillStyle = "black";
ctx.save();
this.drawEyelid(outputRotation);
// squareCut();
this.eyelidCut(outputRotation);
// console.log(Math.floor(this.counter % this.width / 2))
if (Math.floor(this.counter % (this.width / 4)) === 0) {
this.counter = 0;
}
ctx.fillStyle = "black";
ctx.fillRect(this.x - this.width / 2 + centerX, 0, this.width, ctx.canvas.height);
if (this.draw_expand) {
this.drawGrowEye(this.width / 4 + this.counter);
}
if (this.draw_hypno) {
this.centerPulse.draw(rotation)
}
if (this.draw_spiral) {
this.drawSpiral(rotation)
}
if (this.draw_pupil) {
this.drawCircle(this.width / 4);
}
ctx.restore();
this.stepFunc();
this.counter++;
}
}
class MaryFace extends BaseShape {
constructor(x1, y1, rotate1, width1, x2, y2, rotate2, width2) {
super();
this.x1 = x1;
this.y1 = y1;
this.rotate1 = rotate1;
this.width1 = width1;
this.x2 = x2;
this.y2 = y2;
this.rotate2 = rotate2;
this.width2 = width2;
this.eye1 = new EyePrototype(x1, y1, rotate1, 0, width1, 10, 1, 1, 0, 0, 0, 1, "#00fffb", "#00fffb", "#00fffb")
this.eye2 = new EyePrototype(x2, y2, rotate2, 0, width2, 10, 1, 1, 0, 0, 0, 1, "#00fffb", "#00fffb", "#00fffb")
// this.eye3 = new EyePrototype(112, -280, rotate2+2,1, width2, 10, 1, 1, 0, 0, 1, "#00fffb", "#00fffb", "#00fffb")
this.eye3 = new EyePrototype(110, -280, rotate2 + 2, 1, width2, 10, 1, 1, 0, 0, 0, 1, "#00fffb", "#00fffb", "#00fffb")//maybe
}
draw(rotation) {
let img = new Image();
img.src = "maryFace.png";
ctx.drawImage(img, centerX - img.width / 2, centerY - img.height / 2);
this.eye1.draw(rotation);
this.eye2.draw(rotation);
this.eye3.draw(rotation);
}
}
class NewWave extends BaseShape {
constructor(width, sides, step, lineWidth, limiter) {
super();
this.width = width
this.sides = sides;
this.step = step;
this.lineWidth = lineWidth;
this.limiter = limiter;
}
draw(rotation) {
rotation *= this.speedMultiplier / 400
this.updateFilters(rotation);
ctx.lineWidth = this.lineWidth
for (let j = 0; j < this.sides; j++) {
const radRotation = rad(360 / this.sides * j)
const inverter = 1 - (j % 2) * 2
let lastX = centerX
let lastY = centerY
for (let i = 0; i < this.width; i += this.step) {
ctx.beginPath();
ctx.moveTo(lastX, lastY);
ctx.strokeStyle = colourToText(lerpRGB([255, 51, 170], [51, 170, 255], i / this.width))
const x = i
const y = (Math.sin(-i * inverter / 30 + rotation * inverter) * i / (this.limiter / 100))
const xRotated = x * Math.cos(radRotation) - y * Math.sin(radRotation)
const yRotated = x * Math.sin(radRotation) + y * Math.cos(radRotation)
lastX = centerX + xRotated;
lastY = centerY + yRotated;
ctx.lineTo(centerX + xRotated, centerY + yRotated);
ctx.stroke();
}
}
}
}
class Countdown extends BaseShape {
constructor(milestone) {
super();
this.milestone = milestone;
}
secondsUntilDate(targetDate) {
const now = new Date();
// Create the target date specifically in New Zealand timezone (NZST/NZDT)
// Format: YYYY-MM-DDThh:mm:ss+12:00 (NZ standard time)
// The '+12:00' part makes it explicit that we're using NZ timezone
const nzTimeString = targetDate.replace('T', 'T').concat('+12:00');
const target = new Date(nzTimeString);
// Get difference in milliseconds and convert to seconds
const difference = target.getTime() - now.getTime();
return Math.round(difference / 1000);
}
drawProgressBar(progress) {
const colourBackground = "#0c2f69";
const colourProgress = "#4287f5";
const barWidth = ctx.canvas.width;
const barHeight = 60;
// const barX = centerX - barWidth / 2;
const barX = 0;
// const barY = centerY + 350 - barHeight / 2;
const barY = ctx.canvas.height - barHeight;
ctx.fillStyle = colourBackground;
ctx.beginPath();
ctx.rect(barX, barY, barWidth, barHeight)
ctx.fill();
ctx.fillStyle = colourProgress;
ctx.beginPath();
ctx.rect(barX, barY, (barWidth / 100) * progress, barHeight)
ctx.fill();;
}
draw(elapsedTime) {
// elapsedTime *= this.speedMultiplier / 400
let fontSize = 48;
if (ctx.canvas.width < 1000) {
fontSize = 24;
}
ctx.font = fontSize + "px serif";
ctx.fillStyle = "white"
ctx.textAlign = "center";
const futureDate = '2025-06-01T04:30:00';
const seconds = this.secondsUntilDate(futureDate);
const minutes = Math.floor(seconds / 60);
const hours = Math.floor(seconds / 3600);
const percentRounded = (((elapsedTime / 1000) / seconds) * 100).toFixed(8);
ctx.fillText(seconds + " Seconds", centerX, centerY - 200);
ctx.fillText(minutes + " Minues", centerX, centerY - 100);
ctx.fillText(hours + " Hours", centerX, centerY);
ctx.fillText(percentRounded + "% Closer", centerX, centerY + 300);
const milestoneSeconds = this.milestone;
const target = new Date(futureDate + '+12:00'); // Add NZ timezone offset here too
const milestoneDate = new Date(target.getTime() - milestoneSeconds * 1000).toLocaleString()
ctx.fillText(milestoneDate, centerX, centerY + 100);
ctx.fillText("^-- " + milestoneSeconds + " milestone", centerX, centerY + 200);
// Calculate when a single pixel will appear on the progress bar
const canvasWidth = ctx.canvas.width;
// Calculate how many seconds are needed to move a single pixel
// Each pixel represents (100/canvasWidth) percent of the total progress
// We need to know how many seconds that percentage represents
const secondsPerPixel = (seconds / canvasWidth);
console.log(elapsedTime)
const secondsUntilFirstPixel = secondsPerPixel - (elapsedTime / 10);
// console.log(secondsUntilFirstPixel)
ctx.fillText("Time until first pixel: " + Math.round(secondsUntilFirstPixel) + " seconds", centerX, centerY + 350);
this.drawProgressBar(percentRounded);
}
}
class RaysInShape extends BaseShape {
constructor(rays, speed, doesWave, speedVertRate, speedHorrRate, speedVert, speedHorr, boxSize, trailLength = 50, lineWidth, fade, colourFree, colourContained, boxVisible,) {
super();
this.rays = rays;
this.speed = speed;
this.speedVert = speedVert;
this.speedHorr = speedHorr;
this.boxSize = boxSize;
this.trailLength = trailLength;
this.rayObjects = [];
this.centerRays = []; // New array for rays heading to center
this.lineWidth = lineWidth;
this.boxVisible = boxVisible;
this.doesWave = doesWave;
this.colourFree = colourFree;
this.colourContained = colourContained;
this.speedHorrRate = speedHorrRate;
this.speedVertRate = speedVertRate;
this.fade = fade;
}
initialise(config) { //is overide
for (let item of config) {
const { element, listener, filtersDiv } = addControl(item, this);
this.controls.push({ element, listener, });
if (item.type === "range" && item.property !== "rays") {
// Initialize rangeFilter array for this control
const controlIndex = this.controls.length - 1;
this.controls[controlIndex].rangeFilters = [];
let addFilterButton = document.createElement("button");
addFilterButton.innerText = "Add Filter";
addFilterButton.className = "add-filter-button";
// Store the control index in the click handler closure
addFilterButton.addEventListener("click", () => {
const { filterDiv, eventListener, min, max, rate } = createFilter(item);
filtersDiv.appendChild(filterDiv);
// Use the stored control index
if (this.controls[controlIndex] && this.controls[controlIndex].rangeFilters) {
this.controls[controlIndex].rangeFilters.push({
element: filterDiv,
listener: eventListener,
min: min,
max: max,
rate: rate,
});
} else {
console.error("Control or rangeFilters not found for index:", controlIndex);
}
});
filtersDiv.appendChild(addFilterButton);
}
}
// Add controls for speed multiplier and trail length
const { element: speedElement, listener: speedListener } = addControl({
type: "range", min: 1, max: 500, defaultValue: 100, property: "speedMultiplier"
}, this);
this.controls.push({ element: speedElement, listener: speedListener });
// const { element: trailElement, listener: trailListener } = addControl({
// type: "range", min: 5, max: 200, defaultValue: this.trailLength, property: "trailLength"
// }, this);
// this.controls.push({ element: trailElement, listener: trailListener });
// Prepare rayObjects for the first draw
this.prepareRayObjects();
}
prepareRayObjects() {
this.rayObjects = [];
for (let i = 0; i < this.rays; i++) {
const angle = (360 / this.rays) * i;
this.rayObjects.push({
angle: angle,
lastX: centerX,
lastY: centerY,
positions: [{ x: centerX, y: centerY, angle: angle }]
});
}
this.centerRays = []; // Initialize centerRays array
}
createCenterRay(x, y) {
// Calculate angle towards center
const dx = centerX - x;
const dy = centerY - y;
const angleToCenter = Math.atan2(dy, dx) * 180 / Math.PI;
// Create new center-bound ray
this.centerRays.push({
positions: [{ x: x, y: y }],
angle: angleToCenter,
reachedCenter: false
});
}
updateCenterRays(deltaTime) {
const centerThreshold = 5; // Distance threshold to consider "reached center"
const maxDistance = 2000;
// Process each center-bound ray
for (let i = 0; i < this.centerRays.length; i++) {
const ray = this.centerRays[i];
// Skip rays that have reached the center
if (ray.reachedCenter) {
// Remove the oldest position from the trail
if (ray.positions.length > 0) {
ray.positions.shift();
}
// Remove ray if trail is empty
if (ray.positions.length <= 1) {
this.centerRays.splice(i, 1);
i--;
continue;
}
} else {
// Get current position
const currentPos = ray.positions[ray.positions.length - 1];
// Calculate new position
const dx = (this.speedHorr / 100) * this.speed * Math.cos(rad(ray.angle));
const dy = (this.speedVert / 100) * this.speed * Math.sin(rad(ray.angle));
const newX = currentPos.x + dx;
const newY = currentPos.y + dy;
// Check if ray has gone too far from origin
const distFromOrigin = Math.sqrt(
Math.pow(newX - centerX, 2) + Math.pow(newY - centerY, 2)
);
// Remove rays that have gone too far
if (distFromOrigin > maxDistance) {
this.centerRays.splice(i, 1);
i--;
continue;
}
// Add new position to ray
ray.positions.push({ x: newX, y: newY });
// Check if ray has reached center
const distToCenter = Math.sqrt(
Math.pow(newX - centerX, 2) + Math.pow(newY - centerY, 2)
);
if (distToCenter <= centerThreshold) {
ray.reachedCenter = true;
}
// Remove positions beyond trail length
while (ray.positions.length > this.trailLength) {
ray.positions.shift();
}
}
// Draw segments
for (let j = 1; j < ray.positions.length; j++) {
const prev = ray.positions[j - 1];
const curr = ray.positions[j];
// Fade alpha (newer = brighter)
let alpha = 1;
if (this.fade) {
alpha = (j / ray.positions.length) * 0.8 + 0.2;
}
ctx.beginPath();
ctx.moveTo(prev.x, prev.y);
ctx.lineTo(curr.x, curr.y);
// Contained rays
const col = hexToRgb(this.colourFree);
ctx.strokeStyle = `rgba(${col.r}, ${col.g}, ${col.b}, ${alpha})`;
ctx.stroke();
}
}
}
setRays(newValue) {
this.rays = newValue;
this.prepareRayObjects(); // Reinitialize rayObjects with the new number of rays
}
draw(elapsed, deltaTime) {
deltaTime *= this.speedMultiplier / 100;
this.updateFilters(elapsed);
if (this.doesWave) {
const vertRate = this.speedVertRate / 100;
const horrRate = this.speedHorrRate / 100;
this.speedVert = Math.sin(elapsed / 10 * vertRate) * 85 + 100;
this.speedHorr = Math.sin(elapsed / 10 * horrRate) * 85 + 100;
updateControlInput(this.speedVert, "speedVert");
updateControlInput(this.speedHorr, "speedHorr");
}
// Define the box boundaries
const boxLeft = centerX - this.boxSize / 2;
const boxRight = centerX + this.boxSize / 2;
const boxTop = centerY - this.boxSize / 2;
const boxBottom = centerY + this.boxSize / 2;
// Draw the box boundary for visualization
if (this.boxVisible) {
ctx.strokeStyle = "white";
ctx.lineWidth = 1;
ctx.strokeRect(boxLeft, boxTop, this.boxSize, this.boxSize);
}
ctx.lineWidth = this.lineWidth;
// Process ray movements and collisions
for (let j = 0; j < this.rayObjects.length; j++) {
const ray = this.rayObjects[j];
// Get current position
const currentPos = ray.positions[ray.positions.length - 1];
// Calculate potential new position
let dx = (this.speedHorr / 100) * this.speed * Math.cos(rad(ray.angle));
let dy = (this.speedVert / 100) * this.speed * Math.sin(rad(ray.angle));
let newX = currentPos.x + dx;
let newY = currentPos.y + dy;
let collisionType = null;
const oldAngle = ray.angle;
// Check for horizontal collision
if (newX < boxLeft || newX > boxRight) {
// Calculate exact collision point with horizontal wall
const collisionX = newX < boxLeft ? boxLeft : boxRight;
const collisionRatio = (collisionX - currentPos.x) / dx;
const collisionY = currentPos.y + dy * collisionRatio;
// Add collision point to positions array
ray.positions.push({
x: collisionX,
y: collisionY,
angle: oldAngle,
collision: 'horizontal'
});
// Create a center-bound ray at the collision point
this.createCenterRay(collisionX, collisionY);
// Reflect horizontally
ray.angle = 180 - ray.angle;
// Normalize angle
ray.angle = ((ray.angle % 360) + 360) % 360;
// Calculate remaining movement after collision
const remainingRatio = 1 - collisionRatio;
dx = remainingRatio * (this.speedHorr / 100) * this.speed * Math.cos(rad(ray.angle));
dy = remainingRatio * (this.speedVert / 100) * this.speed * Math.sin(rad(ray.angle));
newX = collisionX + dx;
newY = collisionY + dy;
collisionType = 'horizontal';
}
// Check for vertical collision
if (newY < boxTop || newY > boxBottom) {
if (collisionType === null) {
// Calculate exact collision point with vertical wall
const collisionY = newY < boxTop ? boxTop : boxBottom;
const collisionRatio = (collisionY - currentPos.y) / dy;
const collisionX = currentPos.x + dx * collisionRatio;
// Add collision point to positions array
ray.positions.push({
x: collisionX,
y: collisionY,
angle: oldAngle,
collision: 'vertical'
});
// Create a center-bound ray at the collision point
this.createCenterRay(collisionX, collisionY);
// Reflect vertically
ray.angle = 360 - ray.angle;
// Normalize angle
ray.angle = ((ray.angle % 360) + 360) % 360;
// Calculate remaining movement after collision
const remainingRatio = 1 - collisionRatio;
dx = remainingRatio * (this.speedHorr / 100) * this.speed * Math.cos(rad(ray.angle));
dy = remainingRatio * (this.speedVert / 100) * this.speed * Math.sin(rad(ray.angle));
newX = collisionX + dx;
newY = collisionY + dy;
} else {
// Second collision in the same frame (corner case)
// Simply ensure we stay inside the box
newX = Math.max(boxLeft, Math.min(newX, boxRight));
newY = Math.max(boxTop, Math.min(newY, boxBottom));
ray.positions.push({
x: newX,
y: newY,
angle: ray.angle,
collision: 'corner'
});
// Create a center-bound ray at the collision point (corner)
this.createCenterRay(newX, newY);
}
}
// Ensure rays stay inside the box
newX = Math.max(boxLeft, Math.min(newX, boxRight));
newY = Math.max(boxTop, Math.min(newY, boxBottom));
// Add new position to history if there was no collision yet
if (collisionType === null) {
ray.positions.push({
x: newX,
y: newY,
angle: ray.angle
});
}
// Limit positions array to trail length
while (ray.positions.length > this.trailLength) {
ray.positions.shift();
}
// Draw the trail
// Draw all segments of the trail
for (let i = 1; i < ray.positions.length; i++) {
const prev = ray.positions[i - 1];
const curr = ray.positions[i];
// Fade color based on position in trail (newer = brighter)
let alpha = 1;
if (this.fade) {
alpha = (i / ray.positions.length) * 0.8 + 0.2;
}
ctx.beginPath();
ctx.moveTo(prev.x, prev.y);
ctx.lineTo(curr.x, curr.y);
// Highlight collision points with different color
if (curr.collision) {
ctx.strokeStyle = `rgba(255, 255, 0, ${alpha})`;
} else {
const col = hexToRgb(this.colourContained);
ctx.strokeStyle = `rgba(${col.r}, ${col.g}, ${col.b}, ${alpha})`;
// ctx.strokeStyle = `rgba(50, 50, 50, ${alpha})`; // Changed from pink to gray
}
ctx.stroke();
}
}
// Update and draw center-bound rays
this.updateCenterRays(deltaTime);
}
}