diff --git a/Larry the snail/larry_photos/legs0.png b/Larry the snail/larry_photos/legs0.png new file mode 100644 index 0000000..6d74241 Binary files /dev/null and b/Larry the snail/larry_photos/legs0.png differ diff --git a/Larry the snail/larry_photos/legs1.png b/Larry the snail/larry_photos/legs1.png new file mode 100644 index 0000000..f130220 Binary files /dev/null and b/Larry the snail/larry_photos/legs1.png differ diff --git a/Larry the snail/larry_photos/legs2.png b/Larry the snail/larry_photos/legs2.png new file mode 100644 index 0000000..a870202 Binary files /dev/null and b/Larry the snail/larry_photos/legs2.png differ diff --git a/Larry the snail/larry_photos/legs4.png b/Larry the snail/larry_photos/legs4.png new file mode 100644 index 0000000..a798939 Binary files /dev/null and b/Larry the snail/larry_photos/legs4.png differ diff --git a/docs/index.html b/docs/index.html index 477eae6..5181340 100644 --- a/docs/index.html +++ b/docs/index.html @@ -12,6 +12,7 @@ <div id="toolbar"> <br> <select id="shape-selector"> + <option value="RaysInShape">Rays</option> <option value="NewWave">NewWave</option> <option value="EyePrototype">EyePrototype</option> <option value="Nodal_expanding">Nodal_expanding</option> diff --git a/docs/js/helper.js b/docs/js/helper.js index 7ab3e95..6168753 100644 --- a/docs/js/helper.js +++ b/docs/js/helper.js @@ -99,44 +99,97 @@ async function fetchConfig(className) { { type: "range", min: 1, max: 10, defaultValue: 4, property: "lineWidth" }, { type: "range", min: 100, max: 1000, defaultValue: 100, property: "limiter" }, ], + RaysInShape: [ + { type: "range", min: 50, max: 1000, defaultValue: 300, property: "rays" }, + { type: "range", min: 1, max: 1000, defaultValue: 2, property: "speed" }, + { 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: 10, max: 200, defaultValue: 50, property: "trailLength" }, + + ], }; return config[className]; } function addControl(item, instance) { - // console.log(item); let parentDiv = document.getElementById("custom"); let title = document.createElement("p"); title.innerText = item.property + ": " + item.defaultValue; title.id = "elText" + item.property; - let control = document.createElement("input"); - control.type = item.type; + let control; if (item.type === "range") { + control = document.createElement("input"); + control.type = "range"; control.min = item.min; control.max = item.max; + control.value = item.defaultValue; + control.addEventListener("input", (event) => { + const newValue = parseInt(event.target.value, 10); + instance[item.property] = newValue; + title.innerText = item.property + ": " + newValue; + + if (item.callback) { + item.callback(instance, newValue); + } + }); + } else if (item.type === "button") { + control = document.createElement("button"); + control.innerText = item.label; + control.addEventListener("click", () => { + instance[item.method](); + }); + } else if (item.type === "dropdown") { + control = document.createElement("select"); + item.options.forEach(option => { + let optionElement = document.createElement("option"); + optionElement.value = option.value; + optionElement.innerText = option.label; + control.appendChild(optionElement); + }); + control.value = item.defaultValue; + control.addEventListener("change", (event) => { + const newValue = event.target.value; + instance[item.property] = newValue; + title.innerText = item.property + ": " + newValue; + + if (item.callback) { + item.callback(instance, newValue); + } + }); + } else if (item.type === "header") { + control = document.createElement("p"); + control.innerText = item.text; + control.className = "header"; + control.id = "elHeader" + item.text.replace(/\s+/g, ''); + } + else if (item.type === "color") { + control = document.createElement("input"); + control.type = item.type; + control.value = item.defaultValue; + control.id = "el" + item.property; + control.addEventListener("input", (event) => { + const newValue = event.target.value; + instance[item.property] = newValue; + title.innerText = item.property + ": " + newValue; + }) + } - control.value = item.defaultValue; - control.className = "control"; - control.id = "el" + item.property; + if (item.type != "header") { + control.className = "control"; + control.id = "el" + item.property; + } - const listener = (event) => { - const newValue = event.target.value; - instance[item.property] = - item.type === "range" ? parseInt(newValue, 10) : newValue; - - title.innerText = item.property + ": " + newValue; - }; - - control.addEventListener("input", listener); - - parentDiv.appendChild(title); + if (item.type != "button" && item.type != "header") { + parentDiv.appendChild(title); + } parentDiv.appendChild(control); - return { element: control, listener }; + return { element: control }; } function drawEyelid(width, x1, y1, colour) { diff --git a/docs/js/index.js b/docs/js/index.js index c8c4d75..d373345 100644 --- a/docs/js/index.js +++ b/docs/js/index.js @@ -12,13 +12,16 @@ let targetFps = 60; let frameDuration = 1000 / targetFps; let rotation = 0; //was = j = angle -let paused = true; +let paused = false; +let elapsedTime = 0; +let lastTimestamp = 0; render_clear(); let drawObj = null; function createInstance(className, args) { const classMap = { NewWave: NewWave, + RaysInShape: RaysInShape, PolyTwistColourWidth: PolyTwistColourWidth, FloralPhyllo: FloralPhyllo, Spiral1: Spiral1, @@ -65,15 +68,31 @@ updateDrawObj(); function render() { setTimeout(() => { - requestAnimationFrame(() => { - render_clear(); - if (drawObj) { - drawObj.draw(rotation); - } - + requestAnimationFrame((timestamp) => { + if (!lastTimestamp) lastTimestamp = timestamp; + const deltaTime = timestamp - lastTimestamp; + const adjustedElapsed = elapsedTime / 100; // Convert to seconds + lastTimestamp = timestamp; + let adjustedDeltaTime; if (!paused) { rotation += deg_per_sec / targetFps; + elapsedTime += deltaTime; + adjustedDeltaTime = deltaTime / 100; // Convert to seconds + // console.log(adjustedDeltaTime) } + // console.log(deltaTime) + // console.log(elapsedTime) + render_clear(); + if (drawObj) { + // drawObj.draw(rotation); + + drawObj.draw(adjustedElapsed, adjustedDeltaTime); + + } + + ctx.font = "48px serif"; + ctx.fillStyle = "white" + ctx.fillText(Math.floor(elapsedTime) + "ms", centerX - 100, centerY + 400); // drawCenter(300) }); render(); diff --git a/docs/js/objects.js b/docs/js/objects.js index 7d27e41..23b7483 100644 --- a/docs/js/objects.js +++ b/docs/js/objects.js @@ -10,7 +10,7 @@ class BaseShape { this.controls.push({ element, listener }); } - const { element, listener } = addControl({ type: "range", min: 1, max: 500, defaultValue: 100, property: "speedMultiplier", }, this); + const { element, listener } = addControl({ type: "range", min: 1, max: 500, defaultValue: 100, property: "speedMultiplier" }, this); this.controls.push({ element, listener }); } @@ -18,11 +18,14 @@ class BaseShape { this.controls.forEach(({ element, listener }) => { if (element && listener) { element.removeEventListener("input", listener); + element.removeEventListener("click", listener); } if (element && element.parentElement) { element.parentElement.removeChild(element); const titleElement = document.getElementById("elText" + element.id.slice(2)); - titleElement.parentElement.removeChild(titleElement); + if (titleElement) { + titleElement.parentElement.removeChild(titleElement); + } } }); this.controls = []; @@ -672,3 +675,302 @@ class NewWave extends BaseShape { } } +class RaysInShape extends BaseShape { + constructor(rays, speed, speedVert, speedHorr, boxSize, trailLength = 50) { + 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 + } + + initialise(config) { + for (let item of config) { + const { element, listener } = addControl(item, this); + this.controls.push({ element, listener }); + } + + // 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 all segments of the trail + ctx.lineWidth = 3; + for (let j = 1; j < ray.positions.length; j++) { + const prev = ray.positions[j - 1]; + const curr = ray.positions[j]; + + // Fade color based on position in trail (newer = brighter) + const alpha = (j / ray.positions.length) * 0.8 + 0.2; + + ctx.beginPath(); + ctx.moveTo(prev.x, prev.y); + ctx.lineTo(curr.x, curr.y); + + // Center-bound rays are pink + ctx.strokeStyle = `rgba(255, 51, 170, ${alpha})`; + ctx.stroke(); + } + } + } + + draw(elapsed, deltaTime) { + deltaTime *= this.speedMultiplier / 100; + + // 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 + ctx.strokeStyle = "white"; + ctx.lineWidth = 1; + ctx.strokeRect(boxLeft, boxTop, this.boxSize, this.boxSize); + + // 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 + ctx.lineWidth = 3; + + // 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) + const 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 { + ctx.strokeStyle = `rgba(50, 50, 50, ${alpha})`; // Changed from pink to gray + } + + ctx.stroke(); + } + } + + // Update and draw center-bound rays + this.updateCenterRays(deltaTime); + } +} \ No newline at end of file