Giant refactor. added layers. ui overhaul. added save/load and we now got presets
This commit is contained in:
Sam
2025-12-28 03:21:25 +13:00
parent f01076df57
commit 14ec23237f
90 changed files with 4971 additions and 22901 deletions

View File

@@ -0,0 +1,99 @@
/**
* CircleExpand - Expanding concentric circles/hearts animation
*/
class CircleExpand extends BaseShape {
static config = [
{ type: 'range', min: 1, max: 70, defaultValue: 21, property: 'nCircles' },
{ type: 'range', min: 50, max: 150, defaultValue: 150, property: 'gap' },
{ type: 'range', min: 0, max: 1, defaultValue: 1, property: 'linear' },
{ type: 'range', min: 0, max: 1, defaultValue: 1, property: 'heart' },
{ type: 'color', defaultValue: '#fc03cf', property: 'colour1' },
{ type: 'color', defaultValue: '#00fffb', property: 'colour2' },
];
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) {
const ah = +a.replace('#', '0x');
const ar = ah >> 16, ag = ah >> 8 & 0xff, ab = ah & 0xff;
const bh = +b.replace('#', '0x');
const br = bh >> 16, bg = bh >> 8 & 0xff, bb = bh & 0xff;
const rr = ar + amount * (br - ar);
const rg = ag + amount * (bg - ag);
const 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) {
ctx.strokeStyle = "black";
ctx.fillStyle = colour;
ctx.lineWidth = 1;
const x = centerX - w / 2;
const 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(elapsed) {
const rotation = elapsed * 0.9;
ctx.strokeWeight = 1;
ctx.lineWidth = 1;
const arrOfWidths = [];
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 });
}
const newArr = arrOfWidths.sort(this.arraySort);
for (let i = this.nCircles - 1; i >= 0; i--) {
const 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.strokeStyle = "black";
ctx.stroke();
}
}
}
}
shapeRegistry.register('CircleExpand', CircleExpand);

View File

@@ -0,0 +1,78 @@
/**
* Countdown - Countdown timer display with progress bar
*/
class Countdown extends BaseShape {
static config = [
{ type: 'range', min: 8000, max: 2000000, defaultValue: 2000000, property: 'milestone' },
];
constructor(milestone) {
super();
this.milestone = milestone;
}
secondsUntilDate(targetDate) {
const now = new Date();
const nzTimeString = targetDate.replace('T', 'T').concat('+12:00');
const target = new Date(nzTimeString);
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 = 0;
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(elapsed) {
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 = (((elapsed / 1000) / seconds) * 100).toFixed(8);
ctx.fillText(seconds + " Seconds", centerX, centerY - 200);
ctx.fillText(minutes + " Minutes", 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');
const milestoneDate = new Date(target.getTime() - milestoneSeconds * 1000).toLocaleString();
ctx.fillText(milestoneDate, centerX, centerY + 100);
ctx.fillText("^-- " + milestoneSeconds + " milestone", centerX, centerY + 200);
const canvasWidth = ctx.canvas.width;
const secondsPerPixel = (seconds / canvasWidth);
const secondsUntilFirstPixel = secondsPerPixel - (elapsed / 10);
ctx.fillText("Time until first pixel: " + Math.round(secondsUntilFirstPixel) + " seconds", centerX, centerY + 350);
this.drawProgressBar(percentRounded);
}
}
shapeRegistry.register('Countdown', Countdown);

View File

@@ -0,0 +1,196 @@
/**
* EyePrototype - Animated eye with blinking, spiral, and hypnotic effects
*/
class EyePrototype extends BaseShape {
static config = [
{ type: 'range', min: -400, max: 400, defaultValue: 0, property: 'x' },
{ type: 'range', min: -400, max: 400, defaultValue: 0, property: 'y' },
{ type: 'range', min: -180, max: 180, defaultValue: 0, property: 'rotate' },
{ type: 'range', min: 0, max: 1, defaultValue: 1, property: 'flip' },
{ type: 'range', min: 1, max: 800, defaultValue: 400, property: 'width' },
{ type: 'range', min: 1, max: 100, defaultValue: 5, property: 'blink_speed' },
{ type: 'range', min: 0, max: 1, defaultValue: 0, property: 'draw_spiral' },
{ type: 'range', min: 0, max: 1, defaultValue: 1, property: 'spiral_full' },
{ type: 'range', min: 0, max: 1, defaultValue: 0, property: 'draw_pupil' },
{ type: 'range', min: 0, max: 1, defaultValue: 0, property: 'draw_expand' },
{ type: 'range', min: 0, max: 1, defaultValue: 1, property: 'draw_hypno' },
{ type: 'range', min: 1, max: 10, defaultValue: 1, property: 'line_width' },
{ type: 'color', defaultValue: '#00fffb', property: 'colourPupil' },
{ type: 'color', defaultValue: '#ff0000', property: 'colourSpiral' },
{ type: 'color', defaultValue: '#00fffb', property: 'colourExpand' },
{ type: 'range', min: 0, max: 1, defaultValue: 1, property: 'draw_eyelid' },
];
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";
const relCenterX = centerX + this.x;
const relCenterY = centerY + this.y;
rotation *= (this.speedMultiplier / 100);
ctx.lineWidth = 1;
ctx.beginPath();
let newPoint = 0;
let newPoint1 = 0;
const 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) {
const relCenterX = centerX + this.x;
const relCenterY = centerY + this.y;
let newPoint = 0;
let newPoint1 = 0;
const addedRotate = this.flip ? 90 : 0;
const 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) {
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;
const a = 1;
const b = 5;
ctx.moveTo(centerX, centerY);
ctx.beginPath();
const max = this.spiral_full ? this.width : this.width / 2;
for (let i = 0; i < max; i++) {
const angle = 0.1 * i;
const x = centerX + (a + b * angle) * Math.cos(angle + step / 2);
const 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(elapsed) {
const speedMult = 50;
const waitTime = this.blink_speed;
const cap = 200;
const d = waitTime * speedMult * 10;
const a = cap * 2 + d;
const outputRotation = Math.min(Math.abs((Math.floor(elapsed * speedMult) % a) - a / 2 - d / 2), cap);
ctx.fillStyle = "black";
ctx.save();
this.drawEyelid(outputRotation);
this.eyelidCut(outputRotation);
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(elapsed);
}
if (this.draw_spiral) {
this.drawSpiral(elapsed);
}
if (this.draw_pupil) {
this.drawCircle(this.width / 4);
}
ctx.restore();
this.stepFunc();
this.counter++;
}
}
shapeRegistry.register('EyePrototype', EyePrototype);

View File

@@ -0,0 +1,59 @@
/**
* FloralAccident - Accidental floral spiral pattern variant
*/
class FloralAccident extends BaseShape {
static config = [
{ type: 'range', min: 1, max: 50, defaultValue: 20, property: 'sides' },
{ type: 'range', min: 1, max: 600, defaultValue: 240, property: 'width' },
{ type: 'color', defaultValue: '#4287f5', property: 'colour' },
];
constructor(sides, width, colour) {
super();
this.sides = sides;
this.width = width;
this.colour = colour;
}
draw(elapsed) {
this.updateFilters(elapsed);
const rotation = elapsed * (this.speedMultiplier / 100);
const rot = Math.round((this.sides - 2) * 180 / this.sides * 2);
const piv = 360 / this.sides;
let stt = 0.5 * Math.PI - rad(rot);
let end = 0;
const n = this.width / ((this.width / 10) * (this.width / 10));
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));
}
}
}
shapeRegistry.register('FloralAccident', FloralAccident);

View File

@@ -0,0 +1,40 @@
/**
* FloralPhyllo - Phyllotaxis-based floral pattern with eyelid shapes
*/
class FloralPhyllo extends BaseShape {
static config = [
{ type: 'range', min: 1, max: 600, defaultValue: 300, property: 'width' },
{ type: 'range', min: 1, max: 300, defaultValue: 150, property: 'depth' },
{ type: 'range', min: 0, max: 3141, defaultValue: 0, property: 'start' },
{ type: 'color', defaultValue: '#4287f5', property: 'colour1' },
{ type: 'color', defaultValue: '#FC0362', property: 'colour2' },
];
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(elapsed) {
this.updateFilters(elapsed);
const rotation = elapsed * (this.speedMultiplier / 500) + this.start;
const c = 1;
for (let n = this.depth; n > 0; n -= 1) {
const ncolour = LerpHex(this.colour1, this.colour2, n / this.depth);
const a = n * rotation / 1000;
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);
}
}
}
shapeRegistry.register('FloralPhyllo', FloralPhyllo);

View File

@@ -0,0 +1,37 @@
/**
* FloralPhyllo_Accident - Phyllotaxis pattern with accidental eyelid variation
*/
class FloralPhyllo_Accident extends BaseShape {
static config = [
{ type: 'range', min: 1, max: 50, defaultValue: 20, property: 'sides' },
{ type: 'range', min: 1, max: 600, defaultValue: 240, property: 'width' },
{ type: 'color', defaultValue: '#2D81FC', property: 'colour1' },
{ type: 'color', defaultValue: '#FC0362', property: 'colour2' },
];
constructor(sides, width, colour1, colour2) {
super();
this.sides = sides;
this.width = width;
this.colour1 = colour1;
this.colour2 = colour2;
}
draw(elapsed) {
this.updateFilters(elapsed);
const rotation = elapsed * (this.speedMultiplier / 100);
const c = 24;
for (let n = 0; n < 300; n += 1) {
const ncolour = LerpHex(this.colour1, this.colour2, Math.cos(rad(n / 2)));
const a = n * (rotation / 1000 + 100);
const r = c * Math.sqrt(n);
const x = r * Math.cos(a) + centerX;
const y = r * Math.sin(a) + centerY;
drawEyelidAccident(x, y);
}
}
}
shapeRegistry.register('FloralPhyllo_Accident', FloralPhyllo_Accident);

View File

@@ -0,0 +1,42 @@
/**
* MaryFace - Face overlay with animated eyes
*/
class MaryFace extends BaseShape {
static config = [
{ type: 'range', min: -400, max: 400, defaultValue: -110, property: 'x1' },
{ type: 'range', min: -400, max: 400, defaultValue: -140, property: 'y1' },
{ type: 'range', min: -180, max: 180, defaultValue: 18, property: 'rotate1' },
{ type: 'range', min: 0, max: 400, defaultValue: 160, property: 'width1' },
{ type: 'range', min: -400, max: 400, defaultValue: 195, property: 'x2' },
{ type: 'range', min: -400, max: 400, defaultValue: -30, property: 'y2' },
{ type: 'range', min: -180, max: 180, defaultValue: 18, property: 'rotate2' },
{ type: 'range', min: 0, max: 400, defaultValue: 160, property: 'width2' },
];
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(110, -280, rotate2 + 2, 1, width2, 10, 1, 1, 0, 0, 0, 1, "#00fffb", "#00fffb", "#00fffb");
}
draw(elapsed) {
const img = new Image();
img.src = "maryFace.png";
ctx.drawImage(img, centerX - img.width / 2, centerY - img.height / 2);
this.eye1.draw(elapsed);
this.eye2.draw(elapsed);
this.eye3.draw(elapsed);
}
}
shapeRegistry.register('MaryFace', MaryFace);

52
docs/js/shapes/NewWave.js Normal file
View File

@@ -0,0 +1,52 @@
/**
* NewWave - Sine wave pattern radiating from center
*/
class NewWave extends BaseShape {
static config = [
{ type: 'range', min: 300, max: 1600, defaultValue: 342, property: 'width' },
{ type: 'range', min: 2, max: 40, defaultValue: 4, property: 'sides' },
{ type: 'range', min: 1, max: 100, defaultValue: 1, property: 'step' },
{ type: 'range', min: 1, max: 10, defaultValue: 4, property: 'lineWidth' },
{ type: 'range', min: 100, max: 1000, defaultValue: 100, property: 'limiter' },
];
constructor(width, sides, step, lineWidth, limiter) {
super();
this.width = width;
this.sides = sides;
this.step = step;
this.lineWidth = lineWidth;
this.limiter = limiter;
}
draw(elapsed) {
this.updateFilters(elapsed);
const rotation = elapsed * this.speedMultiplier / 400;
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();
}
}
}
}
shapeRegistry.register('NewWave', NewWave);

View File

@@ -0,0 +1,53 @@
/**
* Nodal_expanding - Expanding nodal pattern with color gradient
*/
class Nodal_expanding extends BaseShape {
static config = [
{ type: 'range', min: 1, max: 100, defaultValue: 5, property: 'expand' },
{ type: 'range', min: 1, max: 1000, defaultValue: 150, property: 'points' },
{ type: 'range', min: 1, max: 360, defaultValue: 0, property: 'start' },
{ type: 'range', min: 1, max: 10, defaultValue: 6, property: 'line_width' },
{ type: 'color', defaultValue: '#2D81FC', property: 'colour1' },
{ type: 'color', defaultValue: '#FC0362', property: 'colour2' },
{ type: 'range', min: 0, max: 10, defaultValue: 5, property: 'colour_change' },
];
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(elapsed) {
this.updateFilters(elapsed);
const rotation = elapsed * (this.speedMultiplier / 1000);
const angle = (360 / 3000 * rotation) + this.start;
let length = this.expand;
for (let z = 1; z <= this.points; z++) {
ctx.beginPath();
const 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;
ctx.strokeStyle = ncolour;
ctx.lineCap = "round";
ctx.stroke();
}
}
}
shapeRegistry.register('Nodal_expanding', Nodal_expanding);

View File

@@ -0,0 +1,95 @@
/**
* Phyllotaxis - Classic phyllotaxis pattern with multiple wave modes
*/
class Phyllotaxis extends BaseShape {
static config = [
{ type: 'range', min: 1, max: 40, defaultValue: 24, property: 'width' },
{ type: 'range', min: 1, max: 40, defaultValue: 10, property: 'size' },
{ type: 'range', min: 1, max: 40, defaultValue: 4, property: 'sizeMin' },
{ type: 'range', min: 0, max: 3141, defaultValue: 0, property: 'start' },
{ type: 'range', min: 1, max: 10000, defaultValue: 300, property: 'nMax' },
{ type: 'range', min: 0, max: 2, defaultValue: 0, property: 'wave' },
{ type: 'range', min: 1, max: 12, defaultValue: 2, property: 'spiralProngs' },
{ type: 'color', defaultValue: '#2D81FC', property: 'colour1' },
{ type: 'color', defaultValue: '#FC0362', property: 'colour2' },
];
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;
for (let n = 0; n < maxIterations; n++) {
ctx.beginPath();
const nColor = lerpRGB(startColor, endColor, Math.cos(rad(n / 2)));
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);
for (let n = 0; n < maxIterations; n++) {
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.lineTo(xCoord, yCoord);
}
ctx.stroke();
}
draw(elapsed) {
this.updateFilters(elapsed);
const rotation = elapsed * (this.speedMultiplier / 300) + 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 a = n * (rotation / 1000);
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.fill();
}
}
}
}
shapeRegistry.register('Phyllotaxis', Phyllotaxis);

View File

@@ -0,0 +1,50 @@
/**
* PolyTwistColourWidth - Twisted polygon with color gradient
*/
class PolyTwistColourWidth extends BaseShape {
static config = [
{ type: 'range', min: 3, max: 10, defaultValue: 5, property: 'sides' },
{ type: 'range', min: 400, max: 2000, defaultValue: 400, property: 'width' },
{ type: 'range', min: 2, max: 5, defaultValue: 5, property: 'line_width' },
{ type: 'range', min: 1, max: 100, defaultValue: 50, property: 'depth' },
{ type: 'range', min: -180, max: 180, defaultValue: -90, property: 'rotation' },
{ type: 'color', defaultValue: '#4287f5', property: 'colour1' },
{ type: 'color', defaultValue: '#42f57b', property: 'colour2' },
];
constructor(sides, width, line_width, depth, rotation, colour1, colour2) {
super();
this.sides = sides;
this.width = width;
this.line_width = line_width;
this.depth = depth;
this.rotation = rotation;
this.colour1 = colour1;
this.colour2 = colour2;
}
draw(elapsed) {
this.updateFilters(elapsed);
const rotation = elapsed * (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;
}
const minWidth = Math.sin(rad(innerAngle / 2)) * (0.5 / Math.tan(rad(innerAngle / 2))) * 2;
const 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);
}
}
}
shapeRegistry.register('PolyTwistColourWidth', PolyTwistColourWidth);

View File

@@ -0,0 +1,289 @@
/**
* RaysInShape - Rays bouncing within a box with center-returning trails
*/
class RaysInShape extends BaseShape {
static config = [
{ type: 'range', min: 50, max: 1000, defaultValue: 500, property: 'rays', callback: (instance, newValue) => instance.setRays(newValue) },
{ type: 'range', min: 1, max: 30, defaultValue: 2, property: 'speed' },
{ type: 'checkbox', defaultValue: true, property: 'doesWave' },
{ type: 'range', min: 1, max: 200, defaultValue: 100, property: 'speedVertRate' },
{ type: 'range', min: 1, max: 200, defaultValue: 100, property: 'speedHorrRate' },
{ type: 'range', min: 1, max: 200, defaultValue: 100, property: 'speedVert' },
{ type: 'range', min: 1, max: 200, defaultValue: 100, property: 'speedHorr' },
{ type: 'range', min: 10, max: 2000, defaultValue: 800, property: 'boxSize' },
{ type: 'range', min: 1, max: 80, defaultValue: 5, property: 'trailLength' },
{ type: 'range', min: 1, max: 500, defaultValue: 5, property: 'lineWidth' },
{ type: 'checkbox', defaultValue: false, property: 'fade' },
{ type: 'color', defaultValue: '#43dbad', property: 'colourFree' },
{ type: 'color', defaultValue: '#f05c79', property: 'colourContained' },
{ type: 'header', text: '--CollisionBox---' },
{ type: 'checkbox', defaultValue: false, property: 'boxVisible' },
];
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 = [];
this.lineWidth = lineWidth;
this.boxVisible = boxVisible;
this.doesWave = doesWave;
this.colourFree = colourFree;
this.colourContained = colourContained;
this.speedHorrRate = speedHorrRate;
this.speedVertRate = speedVertRate;
this.fade = fade;
}
initializeControls(controlManager) {
super.initializeControls(controlManager);
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 = [];
}
createCenterRay(x, y) {
const dx = centerX - x;
const dy = centerY - y;
const angleToCenter = Math.atan2(dy, dx) * 180 / Math.PI;
this.centerRays.push({
positions: [{ x: x, y: y }],
angle: angleToCenter,
reachedCenter: false
});
}
updateCenterRays(deltaTime) {
const centerThreshold = 5;
const maxDistance = 2000;
for (let i = 0; i < this.centerRays.length; i++) {
const ray = this.centerRays[i];
if (ray.reachedCenter) {
if (ray.positions.length > 0) {
ray.positions.shift();
}
if (ray.positions.length <= 1) {
this.centerRays.splice(i, 1);
i--;
continue;
}
} else {
const currentPos = ray.positions[ray.positions.length - 1];
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;
const distFromOrigin = Math.sqrt(
Math.pow(newX - centerX, 2) + Math.pow(newY - centerY, 2)
);
if (distFromOrigin > maxDistance) {
this.centerRays.splice(i, 1);
i--;
continue;
}
ray.positions.push({ x: newX, y: newY });
const distToCenter = Math.sqrt(
Math.pow(newX - centerX, 2) + Math.pow(newY - centerY, 2)
);
if (distToCenter <= centerThreshold) {
ray.reachedCenter = true;
}
while (ray.positions.length > this.trailLength) {
ray.positions.shift();
}
}
for (let j = 1; j < ray.positions.length; j++) {
const prev = ray.positions[j - 1];
const curr = ray.positions[j];
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);
const col = hexToRgb(this.colourFree);
ctx.strokeStyle = `rgba(${col.r}, ${col.g}, ${col.b}, ${alpha})`;
ctx.stroke();
}
}
}
setRays(newValue) {
this.rays = newValue;
this.prepareRayObjects();
}
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");
}
const boxLeft = centerX - this.boxSize / 2;
const boxRight = centerX + this.boxSize / 2;
const boxTop = centerY - this.boxSize / 2;
const boxBottom = centerY + this.boxSize / 2;
if (this.boxVisible) {
ctx.strokeStyle = "white";
ctx.lineWidth = 1;
ctx.strokeRect(boxLeft, boxTop, this.boxSize, this.boxSize);
}
ctx.lineWidth = this.lineWidth;
for (let j = 0; j < this.rayObjects.length; j++) {
const ray = this.rayObjects[j];
const currentPos = ray.positions[ray.positions.length - 1];
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;
if (newX < boxLeft || newX > boxRight) {
const collisionX = newX < boxLeft ? boxLeft : boxRight;
const collisionRatio = (collisionX - currentPos.x) / dx;
const collisionY = currentPos.y + dy * collisionRatio;
ray.positions.push({
x: collisionX,
y: collisionY,
angle: oldAngle,
collision: 'horizontal'
});
this.createCenterRay(collisionX, collisionY);
ray.angle = 180 - ray.angle;
ray.angle = ((ray.angle % 360) + 360) % 360;
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';
}
if (newY < boxTop || newY > boxBottom) {
if (collisionType === null) {
const collisionY = newY < boxTop ? boxTop : boxBottom;
const collisionRatio = (collisionY - currentPos.y) / dy;
const collisionX = currentPos.x + dx * collisionRatio;
ray.positions.push({
x: collisionX,
y: collisionY,
angle: oldAngle,
collision: 'vertical'
});
this.createCenterRay(collisionX, collisionY);
ray.angle = 360 - ray.angle;
ray.angle = ((ray.angle % 360) + 360) % 360;
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 {
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'
});
this.createCenterRay(newX, newY);
}
}
newX = Math.max(boxLeft, Math.min(newX, boxRight));
newY = Math.max(boxTop, Math.min(newY, boxBottom));
if (collisionType === null) {
ray.positions.push({
x: newX,
y: newY,
angle: ray.angle
});
}
while (ray.positions.length > this.trailLength) {
ray.positions.shift();
}
for (let i = 1; i < ray.positions.length; i++) {
const prev = ray.positions[i - 1];
const curr = ray.positions[i];
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);
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.stroke();
}
}
this.updateCenterRays(deltaTime);
}
}
shapeRegistry.register('RaysInShape', RaysInShape);

60
docs/js/shapes/Spiral1.js Normal file
View File

@@ -0,0 +1,60 @@
/**
* Spiral1 - Dual-direction spiral pattern
*/
class Spiral1 extends BaseShape {
static config = [
{ type: 'range', min: 1, max: 50, defaultValue: 20, property: 'sides' },
{ type: 'range', min: 1, max: 600, defaultValue: 240, property: 'width' },
{ type: 'color', defaultValue: '#4287f5', property: 'colour' },
];
constructor(sides, width, colour) {
super();
this.sides = sides;
this.width = width;
this.colour = colour;
}
draw(elapsed) {
this.updateFilters(elapsed);
const rotation = elapsed * (this.speedMultiplier / 100);
const rot = Math.round((this.sides - 2) * 180 / this.sides * 2);
const piv = 360 / this.sides;
let stt = 0.5 * Math.PI - rad(rot);
let end = 0;
const n = this.width / ((this.width / 10) * (this.width / 10));
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));
}
}
}
shapeRegistry.register('Spiral1', Spiral1);

View File

@@ -0,0 +1,42 @@
/**
* SquareTwist_angle - Twisted square pattern with angle-based scaling
*/
class SquareTwist_angle extends BaseShape {
static config = [
{ type: 'range', min: 1, max: 800, defaultValue: 400, property: 'width' },
{ type: 'range', min: 1, max: 10, defaultValue: 1, property: 'line_width' },
{ type: 'color', defaultValue: '#2D81FC', property: 'colour1' },
];
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);
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();
}
draw(elapsed) {
this.updateFilters(elapsed);
const rotation = elapsed * (this.speedMultiplier / 100);
const out_angle = rotation;
const 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);
}
}
}
shapeRegistry.register('SquareTwist_angle', SquareTwist_angle);