From a05627e4563630f2cb6c391579f9c8cafc05ddd6 Mon Sep 17 00:00:00 2001 From: Sam <34695753+SamEyeBam@users.noreply.github.com> Date: Fri, 11 Apr 2025 20:40:23 +1200 Subject: [PATCH] Rays WTF --- Larry the snail/larry_photos/legs0.png | Bin 0 -> 5815 bytes Larry the snail/larry_photos/legs1.png | Bin 0 -> 5814 bytes Larry the snail/larry_photos/legs2.png | Bin 0 -> 5815 bytes Larry the snail/larry_photos/legs4.png | Bin 0 -> 5815 bytes docs/index.html | 1 + docs/js/helper.js | 89 +++++-- docs/js/index.js | 33 ++- docs/js/objects.js | 306 ++++++++++++++++++++++++- 8 files changed, 402 insertions(+), 27 deletions(-) create mode 100644 Larry the snail/larry_photos/legs0.png create mode 100644 Larry the snail/larry_photos/legs1.png create mode 100644 Larry the snail/larry_photos/legs2.png create mode 100644 Larry the snail/larry_photos/legs4.png diff --git a/Larry the snail/larry_photos/legs0.png b/Larry the snail/larry_photos/legs0.png new file mode 100644 index 0000000000000000000000000000000000000000..6d742412e9fdf9d0acb239b61a93c41b8635590d GIT binary patch literal 5815 zcmeHKeQ*<H8sG5IVu3nU3ba$`HXTmBHv5sU&C;}%B#;1U(-N?;pl){GBukTQ%x;^| z5kxo`fnh*9)q+@IMEpA7oH<Wa9CV~}z#TVtBI6uzSP(3p2=!#9G9bNoH|duana=4S zchk*m_I;m^-}5}b_j%vh-RP;9KRzoz3qg?aE~mX3J|Bd4#+VHFFNInTz{j;lZ;f2d zCeT=13<zO>$}3|41xX=*Aj#8Ps%oQ0h%vXiW;Bi(H~EDx=&j9->#Vmc!p|C-XXYMD zP98JWdHVCu4_vRGY_52Ch2C7;@*a~|Gw<Yc=k;H8er%|Iy6_3@=27eJJ4@L*+7q20 zTKrq*|J(PZF>Bi#&%5`3ka@V|#<cwS&3{gx;B3WDPAs$+pZ84A@7sO!(5XL#&Mc^` z>#BM^+!kMw1aF)wI7kX@rSI4-*MY(<$Cq7p?EKbu{BK9z=Zlg#FU-6|JD98E?%nzQ z8fyC5&#E?6b<Qf%9JfyWmyCVa5xqL)2evzJ)+%ILUc+14%W_sAh-STDvw2)LTTkh* z;GL_UndAJ}Ghsvb-c8!v%&`+6Kk9k$O8dzA*-K`xUH{3_(5a6u?!`l&Y}<2?&e}gZ zvvBj0%t^uJQ<jY`@6=q-(KFt2ZtVEY`ibP!gJ-vPw{~}&lQg@l*f;)>JKB4Ks{Xd6 z<;1>C)&reIzb*QUf3AOTs}m2j`yTE3)LlR6yzktqCzl?6)|}UV|HhXWnP*=Ze_^|O zVaK7$0)CpSsPuuU=F4l3Y+d&J^3`7~lWW?ymlrqfcp&?}&28Dw&1q>K*PPrz|L(Q? zOJjY{=l->J=cxr--oAS5(aA^aW(oUG=9d>NbfWj<P%EatJYsClX=C-GJ(o+qJYU#w z%N0L%apt;bIL(sRwiT3pUwEr^pRZ+y+%$sRb+he{W%HhDUB1ir&NtV%GtpM$cCdXx z-OV@0KX$e0dSmU_Le0oCAGX!kHg&D}^Az;@o`oAOI#)kDcQ<nArN5gmt@)^Smw4jE z+OG><z4yTlfw!L+Sx_*7a~yx9E13{G&ffk;IB_j=&XsuAr<G;RutysN*o!spMGPlK zbSy9Wfi4+|!6rqJlCors<?4Wp`aw{LTD8~SKdeOs-l~1b=qB7T8wd%`#yD8qSmEUw z>o^OqEi26`Niq;10%R6VM#52vNm{ikE(6Dk7}uhziCkyZ*0?>WO^gH7pfl(Q%#jot zC~avLS`z02OtpPp8UoI&+K?>A7#vR|61s$5C&q&~X|Y&vg2E{ZgBF;yGAgr4EGiW% z5Gf2hkhr)IlLaw~DwwQate34?E!3ku`9)%GcQ1TYO0xj@z>{nYCv^lKiQxS`B-zmb zLDC5g>LGdImd2}rB-Y0{;Aj9*xwt<B&-MDp>f>Rx9G=5L7(}3{1f!AzLe6)&J-r?Z z1wkPaQ@tSB11x1B&_~vQ+!T#kPX9!pdoS()>z>+GV`$}eGj@@ySJHFYty(2M!;752 zGwP6{^?>9F5;N;b7Bc{n#w-L)U<T4}0)PTEZ(;jExuTNHMmeBBL2#V_aX2Hd=S`Fm zqf8`+8O$aMqX`R%akPmrkrW3EB;609GA_WbWW)WlQlNMUWwH1HVFV_O<t!9t;9)i_ zWu!2DfTxWxL9@kAs!%+~%oF1g7A~g{VS@mVMT4qBA)G1qxU5=ANAyWNVO9=62e=1> zC@&_YK9yI9fW<Pa@JX8WX0u688A;kqnGC5=D&kTQmtZd{s3f7&n^bpYVHj8rq?T1W z6#}SoSPf%~16CH}UQrBNwF(5Pc=oE@a6|E|%-UHQKv04*G6cy`MlY#n2!b(~W~qZd zdyy9cEAM8lY#+2FwdhVkg7H_XqST&R3?5I-Qq!=YZYC5}w*|v;sS+f%0q|;Eh?P=t zAvPKW@cc+OY>!+Rq#68lfHwO51V+<F*hMBii}^VVjRBrCuq3M|{3IBNE{Op-!Nx&( z5b_AQf(@#2g-%Z|)r^7mi4aiM0aAv+w(X_NNNVwPvAA+#^i*4d|Adbc)u7)I1N~Am zcyz&&5br$-(|jq1&aZf-*Wp);0HKG242j=ix`yc*5(7gr9`3GTx`xESkc@}B>ldRd ztM6k9MBy7y0)8qz?QKfJ&#Vmp!ufWjTlww!;MFE5xhLlINeH4VR&Gu5m9hpX%#>Yj zN9I?WJOt5Km;T-dMR~HLMz)C&<((W+Ulak4CImTzDldu;J(WEPqUX5m<z6~RlaX=b zh6XwfxU_FA>K-8YADW!2{KHS`xm)_91~JHovM&db=fVJ2dgMbTN=Xj-Oi25G-Gg(d z=U~PBT+G4j?}Xi7K4ixFJ1b+DltSXoAIsD8Y?#=0chITg9~CtM+TXPUq0bM{S+V`E n^Z&WrhIPXX{2wy_w^6oIp6|2bi6-Z2lQMQWD(w5`KDPEhMLHJn literal 0 HcmV?d00001 diff --git a/Larry the snail/larry_photos/legs1.png b/Larry the snail/larry_photos/legs1.png new file mode 100644 index 0000000000000000000000000000000000000000..f1302205790341128ff117875a21dfdb7760f514 GIT binary patch literal 5814 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 literal 0 HcmV?d00001 diff --git a/Larry the snail/larry_photos/legs2.png b/Larry the snail/larry_photos/legs2.png new file mode 100644 index 0000000000000000000000000000000000000000..a8702022f181fd2f1fa5a245256b13faf7e3d078 GIT binary patch literal 5815 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 literal 0 HcmV?d00001 diff --git a/Larry the snail/larry_photos/legs4.png b/Larry the snail/larry_photos/legs4.png new file mode 100644 index 0000000000000000000000000000000000000000..a798939dbfdc62ed3aa23e17cdb360e9a8452500 GIT binary patch literal 5815 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 UcD24lUY6UgoYjs4%QtTQFN0qyvH$=8 literal 0 HcmV?d00001 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