SVG continuous inwards square spiral animation with pure CSS/JS - css

I need some help with this kinda specific animation. It is a square spiral pattern that keeps going inwards until it's fully complete. I somewhat did manage to get it going but I don't know how to stop the animation properly, and I'm not sure if the math behind it is mostly efficient/correct.
Here's what I have for now:
function createSquareSpiralPath(
strokeWidth,
width,
height,
) {
const maxIterations = Math.trunc(Math.min(width, height) / 2 / strokeWidth); // ???
let path = '';
for (let i = 0; i <= maxIterations; i++) {
const step = strokeWidth * i;
const computed = [
`${step},${height - step}`,
`${step},${step}`,
`${width - step - strokeWidth},${step}`,
`${width - step - strokeWidth},${height - step - strokeWidth} `,
];
path += computed.join(' ');
}
return path.trim();
}
.spiral {
stroke-dasharray: 6130;
stroke-dashoffset: 6130;
animation: moveToTheEnd 5s linear forwards;
}
#keyframes moveToTheEnd {
to {
stroke-dashoffset: 0;
}
}
<svg viewBox="-10 -10 350 350" height="350" width="350">
<polyline class="spiral" points="
0,350 0,0 330,0 330,330 20,330 20,20 310,20 310,310 40,310 40,40 290,40 290,290 60,290 60,60 270,60 270,270 80,270 80,80 250,80 250,250 100,250 100,100 230,100 230,230 120,230 120,120 210,120 210,210 140,210 140,140 190,140 190,190 160,190 160,160 170,160 170,170"
style="fill:transparent;stroke:black;stroke-width:20" />
Sorry, your browser does not support inline SVG.
</svg>
I added the js function just to demonstrate how I'm generating the points. As you can see the animation plays exactly how I want, I just can't find a way to wrap it up properly. Also, I'm unsure if this function would generate correct points for varying width/height/strokeWidth.
I appreciate any help! Thanks in advance. :)
PS.: I could not find a mathematical term for this pattern (square-ish spiral) so I'm more than happy to learn how to call it properly.
Edit
Based on #enxaneta answers (thank you!) it seems I'm incorrectly calculating the max number of iterations. This can be seen whenever width !== height. I'll do some research on how I'm producing this value, maybe this formula isn't adequate to properly "stop" the animation without any blank space.

I guess you also need to check if your current drawing position has already reached a maximum x/y (close to you center).
The calculation for the loops iterations works fine.
Currently you're drawing 4 new points in each step.
Depending on your stroke-width you might need to stop drawing e.g after the 2. or 3. point when you're close to the center X/Y coordinates.
let spiral1 = createSquareSpiralPath(50, 500, 1000);
let spiral1_2 = createSquareSpiralPath(20, 1000, 500);
let spiral2 = createSquareSpiralPath(150, 300, 300);
function createSquareSpiralPath(strokeWidth, width, height) {
let maxIterations = Math.trunc(Math.min(width, height) / 2 / strokeWidth);
let coords = [];
//calculate max X/Y coordinates according to stroke-width
let strokeToWidthRatio = width * 1 / strokeWidth;
let strokeToHeightRatio = height * 1 / strokeWidth;
let maxX = (width - strokeWidth / strokeToWidthRatio) / 2;
let maxY = (height - strokeWidth / strokeToHeightRatio) / 2;
for (let i = 0; i <= maxIterations; i++) {
const step = strokeWidth * i;
// calculate points in iteration
let [x1, y1] = [step, (height - step)];
let [x2, y2] = [step, step];
let [x3, y3] = [(width - step - strokeWidth), step];
let [x4, y4] = [(width - step - strokeWidth), (height - step - strokeWidth)];
//stop drawing if max X/Y coordinates are reached
if (x1 <= maxX && y1 >= maxY) {
coords.push(x1, y1)
}
if (x2 <= maxX && y2 <= maxY) {
coords.push(x2, y2)
}
if (x3 >= maxX && y3 <= maxY) {
coords.push(x3, y3)
}
if (x4 >= maxX && y4 >= maxY) {
coords.push(x4, y4)
}
}
let points = coords.join(' ');
//calc pathLength from coordinates
let pathLength = 0;
for (let i = 0; i < coords.length - 2; i += 2) {
let x1 = coords[i];
let y1 = coords[i + 1];
let x2 = coords[i + 2];
let y2 = coords[i + 3];
let length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
pathLength += length;
}
//optional: render svg
renderSpiralSVG(points, pathLength, width, height, strokeWidth);
return [points, pathLength];
}
function renderSpiralSVG(points, pathLength, width, height, strokeWidth) {
const ns = "http://www.w3.org/2000/svg";
let svgTmp = document.createElementNS(ns, "svg");
svgTmp.setAttribute(
"viewBox", [-strokeWidth / 2, -strokeWidth / 2, width, height].join(" ")
);
let newPolyline = document.createElementNS(ns, "polyline");
newPolyline.classList.add("spiral");
newPolyline.setAttribute("points", points);
svgTmp.appendChild(newPolyline);
document.body.appendChild(svgTmp);
newPolyline.setAttribute(
"style",
`fill:transparent;
stroke:black;
stroke-linecap: square;
stroke-width:${strokeWidth};
stroke-dashoffset: ${pathLength};
stroke-dasharray: ${pathLength};`
);
}
svg {
border: 1px solid red;
}
svg {
display: inline-block;
height: 20vw;
}
.spiral {
stroke-width: 1;
animation: moveToTheEnd 1s linear forwards;
}
.spiral:hover {
stroke-width: 1!important;
}
#keyframes moveToTheEnd {
to {
stroke-dashoffset: 0;
}
}
<p> Hover to see spiral lines</p>

To control the animation, instead of CSS, use the Web Animations API
https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API
Wrap all in a standard Web Component <svg-spiral> with shadowDOM, so you can have multiple components on screen without any global CSS conflicts.
set a pathLenght="100" on the polygon, so you don't have to do calculations
stroke-dasharray must be written as: strokeDasharray in WAAPI
The animation triggers an onfinish function
clicking an <svg-spiral> in the SO snippet below will restart the animation
<div style="display:grid;grid:1fr/repeat(4,1fr)">
<svg-spiral></svg-spiral>
<svg-spiral stroke="rebeccapurple" width="1000" strokewidth="10"></svg-spiral>
<svg-spiral stroke="blue" duration="10000"></svg-spiral>
<svg-spiral stroke="red" width="6000" duration="1e4"></svg-spiral>
</div>
<script>
customElements.define("svg-spiral", class extends HTMLElement {
connectedCallback() {
let strokewidth = this.getAttribute("strokewidth") || 30;
let width = this.getAttribute("width") || 500; let height = this.getAttribute("height") || width;
let points = '';
for (let i = 0; i <= ~~(Math.min(width, height) / 2 / strokewidth); i++) {
const step = strokewidth * i;
points += `${step},${height - step} ${step},${step} ${width - step - strokewidth},${step} ${width - step - strokewidth},${height - step - strokewidth} `;
}
this.attachShadow({mode:"open"}).innerHTML = `<svg viewBox="-${strokewidth/2}-${strokewidth/2} ${width} ${height}"><polyline class="spiral" pathLength="100" points="${points}z"
fill="transparent" stroke-width="${strokewidth}" /></svg>`;
this.onclick = (e) => this.animate();
this.animate();
}
animate() {
let spiral = this.shadowRoot.querySelector(".spiral");
spiral.setAttribute("stroke", this.getAttribute("stroke") || "black");
let player = spiral.animate(
[{ strokeDashoffset: 100, strokeDasharray: 100, opacity: 0 },
{ strokeDashoffset: 0, strokeDasharray: 100, opacity: 1 }],
{
duration: ~~(this.getAttribute("duration") || 5000),
iterations: 1
});
player.onfinish = (e) => { spiral.setAttribute("stroke", "green") }
}
})
</script>

Related

Deconstructing Google maps smarty pins animation

Updates
Updated fiddle to simplify what is going on:
added four buttons to move the stick, each button increments the value by 30 in the direction
plotted x and y axis
red line is the stick, with bottom end coordinates at (ax,ay) and top end coordinates at (bx,by)
green line is (presumably) previous position of the stick, with bottom end coordinates at (ax, ay) and top end coordinates at (bx0, by0)
So, after having my ninja moments. I'm still nowhere near understanding the sorcery behind unknownFunctionA and unknownFunctionB
For the sake of everyone (all two of you) here is what I've sort of learnt so far
function unknownFunctionB(e) {
var t = e.b.x - e.a.x
, n = e.b.y - e.a.y
, a = t * t + n * n;
if (a > 0) {
if (a == e.lengthSq)
return;
var o = Math.sqrt(a)
, i = (o - e.length) / o
, s = .5;
e.b.x -= t * i * .5 * s,
e.b.y -= n * i * .5 * s
}
}
In the unknownFunctionB above, variable o is length of the red sitck.
Still don't understand
What is variable i and how is (bx,by) calculated? essentially:
bx = bx - (bx - ax) * 0.5 * 0.5
by = by - (by - ay) * 0.5 * 0.5
In unknownFunctionA what are those magic numbers 1.825 and 0.825?
Below is irrelevant
I'm trying to deconstruct marker drag animation used on smartypins
I've managed to get the relevant code for marker move animation but I'm struggling to learn how it all works, especially 2 functions (that I've named unknownFunctionA and unknownFunctionB)
Heres the StickModel class used on smartypins website, unminified to best of my knowledge
function unknownFunctionA(e) {
var t = 1.825
, n = .825
, a = t * e.x - n * e.x0
, o = t * e.y - n * e.y0 - 5;
e.x0 = e.x,
e.y0 = e.y,
e.x = a,
e.y = o;
}
function unknownFunctionB(e) {
var t = e.b.x - e.a.x
, n = e.b.y - e.a.y
, a = t * t + n * n;
if (a > 0) {
if (a == e.lengthSq)
return;
var o = Math.sqrt(a)
, i = (o - e.length) / o
, s = .5;
e.b.x -= t * i * .5 * s,
e.b.y -= n * i * .5 * s
}
}
function StickModel() {
this._props = function(e) {
return {
length: e,
lengthSq: e * e,
a: {
x: 0,
y: 0
},
b: {
x: 0,
y: 0 - e,
x0: 0,
y0: 0 - e
},
angle: 0
}
}
(60)
}
var radianToDegrees = 180 / Math.PI;
StickModel.prototype = {
pos: {
x: 0,
y: 0
},
angle: function() {
return this._props.angle
},
reset: function(e, t) {
var n = e - this._props.a.x
, a = t - this._props.a.y;
this._props.a.x += n,
this._props.a.y += a,
this._props.b.x += n,
this._props.b.y += a,
this._props.b.x0 += n,
this._props.b.y0 += a
},
move: function(e, t) {
this._props.a.x = e,
this._props.a.y = t
},
update: function() {
unknownFunctionA(this._props.b),
unknownFunctionB(this._props),
this.pos.x = this._props.a.x,
this.pos.y = this._props.a.y;
var e = this._props.b.x - this._props.a.x
, t = this._props.b.y - this._props.a.y
, o = Math.atan2(t, e);
this._props.angle = o * radianToDegrees;
}
}
StickModel.prototype.constructor = StickModel;
Fiddle link with sample implementation on canvas: http://jsfiddle.net/vff1w82w/3/
Again, Everything works as expected, I'm just really curious to learn the following:
What could be the ideal names for unknownFunctionA and unknownFunctionB and an explanation of their functionality
What are those magic numbers in unknownFunctionA (1.825 and .825) and .5 in unknownFunctionB.
Variable o in unknownFunctionB appears to be hypotenuse. If that's the case, then what exactly is i = (o - e.length) / o in other words, i = (hypotenuse - stickLength) / hypotenuse?
First thing I'd recommend is renaming all those variables and methods until they start making sense. I also removed unused code.
oscillator
adds wobble to the Stick model by creating new position values for the Stick that follows the mouse
Exaggerates its movement by multiplying its new position by 1.825 and also subtracting the position of an "echo" of its previous position multiplied by 0.825. Sort of looking for a middle point between them. Helium makes the stick sit upright.
overshooter minus undershooter must equal 1 or you will have orientation problems with your stick. overshooter values above 2.1 tend to make it never settle.
seekerUpdate
updates the seeker according to mouse positions.
The distance_to_cover variable measures the length of the total movement. You were right: hypothenuse (variable o).
The ratio variable calculates the ratio of the distance that can be covered subtracting the size of the stick. The ratio is then used to limit the adjustment of the update on the seeker in both directions (x and y). That's how much of the update should be applied to prevent overshooting the target.
easing slows down the correct updates.
There are lots of interesting info related to vectors on the book The nature of code.
function oscillator(seeker) {
var overshooter = 1.825;
var undershooter = .825;
var helium = -5;
var new_seeker_x = overshooter * seeker.x - undershooter * seeker.echo_x;
var new_seeker_y = overshooter * seeker.y - undershooter * seeker.echo_y + helium;
seeker.echo_x = seeker.x;
seeker.echo_y = seeker.y;
seeker.x = new_seeker_x;
seeker.y = new_seeker_y;
}
function seekerUpdate(stick) {
var dX = stick.seeker.x - stick.mouse_pos.x;
var dY = stick.seeker.y - stick.mouse_pos.y;
var distance_to_cover = Math.sqrt(dX * dX + dY * dY);
var ratio = (distance_to_cover - stick.length) / distance_to_cover;
var easing = .25;
stick.seeker.x -= dX * ratio * easing;
stick.seeker.y -= dY * ratio * easing;
}
function StickModel() {
this._props = function(length) {
return {
length: length,
lengthSq: length * length,
mouse_pos: {
x: 0,
y: 0
},
seeker: {
x: 0,
y: 0 - length,
echo_x: 0,
echo_y: 0 - length
}
}
}(60)
}
StickModel.prototype = {
move: function(x, y) {
this._props.mouse_pos.x = x;
this._props.mouse_pos.y = y;
},
update: function() {
oscillator(this._props.seeker);
seekerUpdate(this._props);
}
};
StickModel.prototype.constructor = StickModel;
// Canvas to draw stick model coordinates
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
canvas.width = window.outerWidth;
canvas.height = window.outerHeight;
var canvasCenterX = Math.floor(canvas.width / 2);
var canvasCenterY = Math.floor(canvas.height / 2);
context.translate(canvasCenterX, canvasCenterY);
var stickModel = new StickModel();
draw();
setInterval(function() {
stickModel.update();
draw();
}, 16);
$(window).mousemove(function(e) {
var mouseX = (e.pageX - canvasCenterX);
var mouseY = (e.pageY - canvasCenterY);
stickModel.move(mouseX, mouseY);
stickModel.update();
draw();
});
function draw() {
context.clearRect(-canvas.width, -canvas.height, canvas.width * 2, canvas.height * 2);
// red line from (ax, ay) to (bx, by)
context.beginPath();
context.strokeStyle = "#ff0000";
context.moveTo(stickModel._props.mouse_pos.x, stickModel._props.mouse_pos.y);
context.lineTo(stickModel._props.seeker.x, stickModel._props.seeker.y);
context.fillText('mouse_pos x:' + stickModel._props.mouse_pos.x + ' y: ' + stickModel._props.mouse_pos.y, stickModel._props.mouse_pos.x, stickModel._props.mouse_pos.y);
context.fillText('seeker x:' + stickModel._props.seeker.x + ' y: ' + stickModel._props.seeker.y, stickModel._props.seeker.x - 30, stickModel._props.seeker.y);
context.lineWidth = 1;
context.stroke();
context.closePath();
// green line from (ax, ay) to (bx0, by0)
context.beginPath();
context.strokeStyle = "#00ff00";
context.moveTo(stickModel._props.mouse_pos.x, stickModel._props.mouse_pos.y);
context.lineTo(stickModel._props.seeker.echo_x, stickModel._props.seeker.echo_y);
context.fillText('echo x:' + stickModel._props.seeker.echo_x + ' y: ' + stickModel._props.seeker.echo_y, stickModel._props.seeker.echo_x, stickModel._props.seeker.echo_y - 20);
context.lineWidth = 1;
context.stroke();
context.closePath();
// blue line from (bx0, by0) to (bx, by)
context.beginPath();
context.strokeStyle = "#0000ff";
context.moveTo(stickModel._props.seeker.echo_x, stickModel._props.seeker.echo_y);
context.lineTo(stickModel._props.seeker.x, stickModel._props.seeker.y);
context.stroke();
context.closePath();
}
body {
margin: 0px;
padding: 0px;
}
canvas {
display: block;
}
p {
position: absolute;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js"></script>
<p>Move your mouse to see the stick (colored red) follow</p>
<canvas id="myCanvas"></canvas>

WebGL Walkthrough, Move around the 3D scene

I'm new to WebGL, and I'm trying to create a walk-through for a website, I have taken my Maya model into WebGL with the help of inka3D, but when I apply the following code for the movement, it doesn't work as it explains. Only the left arrow works fine.
function resize()
{
var width = canvas.offsetWidth;
var height = canvas.offsetHeight;
canvas.width = width;
canvas.height = height;
aspect = width / height;
}
var cameraTargetX = 37.2878151;
var cameraTargetY = 12.846137;
var cameraTargetZ = 7.17901707;
var dx = 5;
var dy = 5;
window.addEventListener('keydown',doKeyDown,true);
function doKeyDown(evt){
switch (evt.keyCode) {
case 38: /* Up arrow was pressed */
if (cameraTargetY - dy > 0){
cameraTargetY -= dy;
}
break;
case 40: /* Down arrow was pressed */
if (cameraTargetY + dy < height){
cameraTargetY += dy;
}
break;
case 37: /* Left arrow was pressed Fine*/
if (cameraTargetX - dx > 0){
cameraTargetX -= dx;
}
break;
case 39: /* Right arrow was pressed */
if (cameraTargetX + dx < width){
cameraTargetX += dx;
}
break;
}
}
};
If only the left arrow works this means that difference of (cameraTargetX - dx ) > 0. Thats why you can translate. The reason is cameraTargetX is 37 diff of 5 make it 32 and on key press you can visualize this in 5X7(loop). Key is pressed 7 times until the value become lesser than zero
But when var cameraTargetY = 12.846137; and dy is 5 it take only 5x2(loop) just a fraction and the value become lesser than zero and you can visualize the diff.
Solution is as stated dx and dy are delta values means this should be very small as variable convection so try with
var dx = 0.05;
var dy = 0.05;
You will get answer. If any doubt feel free to ask

Radius of projected sphere in screen space

I'm trying to find the visible size of a sphere in pixels, after projection to screen space. The sphere is centered at the origin with the camera looking right at it. Thus the projected sphere should be a perfect circle in two dimensions. I am aware of this 1 existing question. However, the formula given there doesn't seem to produce the result I want. It is too small by a few percent. I assume this is because it is not correctly taking perspective into account. After projecting to screen space you do not see half the sphere but significantly less, due to perspective foreshortening (you see just a cap of the sphere instead of the full hemisphere 2).
How can I derive an exact 2D bounding circle?
Indeed, with a perspective projection you need to compute the height of the sphere "horizon" from the eye / center of the camera (this "horizon" is determined by rays from the eye tangent to the sphere).
Notations:
d: distance between the eye and the center of the sphere
r: radius of the sphere
l: distance between the eye and a point on the sphere "horizon", l = sqrt(d^2 - r^2)
h: height / radius of the sphere "horizon"
theta: (half-)angle of the "horizon" cone from the eye
phi: complementary angle of theta
h / l = cos(phi)
but:
r / d = cos(phi)
so, in the end:
h = l * r / d = sqrt(d^2 - r^2) * r / d
Then once you have h, simply apply the standard formula (the one from the question you linked) to get the projected radius pr in the normalized viewport:
pr = cot(fovy / 2) * h / z
with z the distance from the eye to the plane of the sphere "horizon":
z = l * cos(theta) = sqrt(d^2 - r^2) * h / r
so:
pr = cot(fovy / 2) * r / sqrt(d^2 - r^2)
And finally, multiply pr by height / 2 to get the actual screen radius in pixels.
What follows is a small demo done with three.js. The sphere distance, radius and the vertical field of view of the camera can be changed by using respectively the n / f, m / p and s / w pairs of keys. A yellow line segment rendered in screen-space shows the result of the computation of the radius of the sphere in screen-space. This computation is done in the function computeProjectedRadius().
projected-sphere.js:
"use strict";
function computeProjectedRadius(fovy, d, r) {
var fov;
fov = fovy / 2 * Math.PI / 180.0;
//return 1.0 / Math.tan(fov) * r / d; // Wrong
return 1.0 / Math.tan(fov) * r / Math.sqrt(d * d - r * r); // Right
}
function Demo() {
this.width = 0;
this.height = 0;
this.scene = null;
this.mesh = null;
this.camera = null;
this.screenLine = null;
this.screenScene = null;
this.screenCamera = null;
this.renderer = null;
this.fovy = 60.0;
this.d = 10.0;
this.r = 1.0;
this.pr = computeProjectedRadius(this.fovy, this.d, this.r);
}
Demo.prototype.init = function() {
var aspect;
var light;
var container;
this.width = window.innerWidth;
this.height = window.innerHeight;
// World scene
aspect = this.width / this.height;
this.camera = new THREE.PerspectiveCamera(this.fovy, aspect, 0.1, 100.0);
this.scene = new THREE.Scene();
this.scene.add(THREE.AmbientLight(0x1F1F1F));
light = new THREE.DirectionalLight(0xFFFFFF);
light.position.set(1.0, 1.0, 1.0).normalize();
this.scene.add(light);
// Screen scene
this.screenCamera = new THREE.OrthographicCamera(-aspect, aspect,
-1.0, 1.0,
0.1, 100.0);
this.screenScene = new THREE.Scene();
this.updateScenes();
this.renderer = new THREE.WebGLRenderer({
antialias: true
});
this.renderer.setSize(this.width, this.height);
this.renderer.domElement.style.position = "relative";
this.renderer.autoClear = false;
container = document.createElement('div');
container.appendChild(this.renderer.domElement);
document.body.appendChild(container);
}
Demo.prototype.render = function() {
this.renderer.clear();
this.renderer.setViewport(0, 0, this.width, this.height);
this.renderer.render(this.scene, this.camera);
this.renderer.render(this.screenScene, this.screenCamera);
}
Demo.prototype.updateScenes = function() {
var geometry;
this.camera.fov = this.fovy;
this.camera.updateProjectionMatrix();
if (this.mesh) {
this.scene.remove(this.mesh);
}
this.mesh = new THREE.Mesh(
new THREE.SphereGeometry(this.r, 16, 16),
new THREE.MeshLambertMaterial({
color: 0xFF0000
})
);
this.mesh.position.z = -this.d;
this.scene.add(this.mesh);
this.pr = computeProjectedRadius(this.fovy, this.d, this.r);
if (this.screenLine) {
this.screenScene.remove(this.screenLine);
}
geometry = new THREE.Geometry();
geometry.vertices.push(new THREE.Vector3(0.0, 0.0, -1.0));
geometry.vertices.push(new THREE.Vector3(0.0, -this.pr, -1.0));
this.screenLine = new THREE.Line(
geometry,
new THREE.LineBasicMaterial({
color: 0xFFFF00
})
);
this.screenScene = new THREE.Scene();
this.screenScene.add(this.screenLine);
}
Demo.prototype.onKeyDown = function(event) {
console.log(event.keyCode)
switch (event.keyCode) {
case 78: // 'n'
this.d /= 1.1;
this.updateScenes();
break;
case 70: // 'f'
this.d *= 1.1;
this.updateScenes();
break;
case 77: // 'm'
this.r /= 1.1;
this.updateScenes();
break;
case 80: // 'p'
this.r *= 1.1;
this.updateScenes();
break;
case 83: // 's'
this.fovy /= 1.1;
this.updateScenes();
break;
case 87: // 'w'
this.fovy *= 1.1;
this.updateScenes();
break;
}
}
Demo.prototype.onResize = function(event) {
var aspect;
this.width = window.innerWidth;
this.height = window.innerHeight;
this.renderer.setSize(this.width, this.height);
aspect = this.width / this.height;
this.camera.aspect = aspect;
this.camera.updateProjectionMatrix();
this.screenCamera.left = -aspect;
this.screenCamera.right = aspect;
this.screenCamera.updateProjectionMatrix();
}
function onLoad() {
var demo;
demo = new Demo();
demo.init();
function animationLoop() {
demo.render();
window.requestAnimationFrame(animationLoop);
}
function onResizeHandler(event) {
demo.onResize(event);
}
function onKeyDownHandler(event) {
demo.onKeyDown(event);
}
window.addEventListener('resize', onResizeHandler, false);
window.addEventListener('keydown', onKeyDownHandler, false);
window.requestAnimationFrame(animationLoop);
}
index.html:
<!DOCTYPE html>
<html>
<head>
<title>Projected sphere</title>
<style>
body {
background-color: #000000;
}
</style>
<script src="http://cdnjs.cloudflare.com/ajax/libs/three.js/r61/three.min.js"></script>
<script src="projected-sphere.js"></script>
</head>
<body onLoad="onLoad()">
<div id="container"></div>
</body>
</html>
Let the sphere have radius r and be seen at a distance d from the observer. The projection plane is at distance f from the observer.
The sphere is seen under the half angle asin(r/d), so the apparent radius is f.tan(asin(r/d)), which can be written as f . r / sqrt(d^2 - r^2). [The wrong formula being f . r / d.]
The illustrated accepted answer above is excellent, but I needed a solution without knowing the field of view, just a matrix to transform between world and screen space, so I had to adapt the solution.
Reusing some variable names from the other answer, calculate the start point of the spherical cap (the point where line h meets line d):
capOffset = cos(asin(l / d)) * r
capCenter = sphereCenter + ( sphereNormal * capOffset )
where capCenter and sphereCenter are points in world space, and sphereNormal is a normalized vector pointing along d, from the sphere center towards the camera.
Transform the point to screen space:
capCenter2 = matrix.transform(capCenter)
Add 1 (or any amount) to the x pixel coordinate:
capCenter2.x += 1
Transform it back to world space:
capCenter2 = matrix.inverse().transform(capCenter2)
Measure the distance between the original and new points in world space, and divide into the amount you added to get a scale factor:
scaleFactor = 1 / capCenter.distance(capCenter2)
Multiply that scale factor by the cap radius h to get the visible screen radius in pixels:
screenRadius = h * scaleFactor

Processing2 2D Physics Collision at 90 degree corners

so this is my first post ever on asking a question about programming, so please be patient :)
For a little project in school I made a little physics class, handling collision. Although it worked out fine I still have a bug I couldn't figure out after some hours of searching and I still don't really know where the problem lies in.
For the implementation we used the on Java based language Processing which is used for an introduction to programming and prototyping.
With the a left mouseclick I can spawn some balls which collide pixel-wise with a certain color on the screen. When colliding with a 90 degree corner they just fall through the obstacle. Sadly I can't post a screenshot because of my lack in reputation.
So my question is about what the problem is. Someone I asked said it could be a problem with the dot product I use for calculating the new mirrored velocity, but I couldn't find anything in that direction. I suspect the error lies somewhere in the part where the new velocity is calculated, in the update method of the PhysicsEntity class.
So thanks to everyone who is answering, I am grateful for every useful hint :)
Here is my code, it consists of three classes. I am going to post everything so you can run the code yourself. If you don't have processing you'll need to download it from http://processing.org/ in order to run the code sample below.
Main.pde NOTE: This part is only an example for using my physics class.
ArrayList<PhysicsEntity> entities = new ArrayList<PhysicsEntity>();
boolean mouseClicked = false;
boolean paused = false;
void setup()
{
size(800, 600);
background(0);
frameRate(60);
}
void draw()
{
if (!paused)
{
clear();
float gameTime = 1 / frameRate;
loadPixels();
for (int x = 0; x < width; ++x)
{
for (int y = height - 100; y < height; ++y)
{
pixels[x + y * width] = color(0, 200, 0, 128);
}
}
for (int x = 0; x < width; ++x)
{
for (int y = 0; y < 20; ++y)
{
pixels[x + y * width] = color(0, 200, 0, 128);
}
}
for (int x = 0; x < 100; ++x)
{
for (int y = 0; y < height; ++y)
{
pixels[x + y * width] = color(0, 200, 0, 128);
}
}
for (int x = width - 100; x < width; ++x)
{
for (int y = 0; y < height; ++y)
{
pixels[x + y * width] = color(0, 200, 0, 128);
}
}
updatePixels();
if (mousePressed)
{
entities.add(new PhysicsEntity(new Vector2(width / 2, height / 2), new Vector2(random(-100, 100), random(-100, 100)), new Vector2(0.0f, 250.0f)));
}
for (int i = 0; i < entities.size(); ++i)
{
entities.get(i).update(gameTime);
entities.get(i).show();
}
}
}
Vector2.pde NOTE: This class is just necessary for calculting things in the physics class.
class Vector2
{
float a;
float b;
Vector2()
{
a = 0.0f;
b = 0.0f;
}
Vector2(float _a, float _b)
{
a = _a;
b = _b;
}
/* Return exact copy of the vector */
Vector2 Copy()
{
return new Vector2(a, b);
}
Vector2 Add(Vector2 vecB)
{
return new Vector2(a + vecB.a, b + vecB.b);
}
Vector2 Substract(Vector2 vecB)
{
return new Vector2(a - vecB.a, b - vecB.b);
}
/* Scale the vector by a scalar x */
Vector2 Scale(float x)
{
return new Vector2(a * x, b * x);
}
Vector2 Divide(float x)
{
return new Vector2(a / x, b / x);
}
float Dot(Vector2 vecB)
{
return (a * vecB.a + b * vecB.b);
}
float SqrLength()
{
return (pow(a, 2) + pow(b, 2));
}
float Length()
{
return sqrt(SqrLength());
}
boolean Equals(Vector2 vecB)
{
return (a != vecB.a || b != vecB.b) ? false : true;
}
}
Vector2 ZeroVector()
{
return new Vector2(0.0f, 0.0f);
}
PhysicsEntity.pde NOTE: That's the class where actually failed.
class PhysicsEntity
{
Vector2 m_Pos;
Vector2 m_PrevPos;
Vector2 m_Vel;
Vector2 m_Acc;
/* bouncyness in case of collision; gets multiplied with the velocity */
float m_fBouncyness = 1.0f;
color collisionKey = color(0, 200, 0, 128);
public PhysicsEntity(Vector2 _pos, Vector2 _vel, Vector2 _acc)
{
if (_vel == null)
_vel = new Vector2(0.0f, 0.0f);
m_Pos = new Vector2(_pos.a, _pos.b);
m_PrevPos = m_Pos;
m_Vel = _vel;
m_Acc = _acc;
}
public void update(float dt)
{
/* Euler Integration more accurate Version */
/* x = x + vt + 0.5*at^2 */
m_Pos = m_Pos.Add(m_Vel.Scale(dt)).Add(m_Acc.Scale(pow(dt, 2)).Scale(0.5));
/* v = v + at */
m_Vel = m_Vel.Add(m_Acc.Scale(dt));
/* Collision based on color key */
if (isCollidable(m_Pos.a, m_Pos.b, collisionKey))
{
float speed = m_Vel.Length();
if (speed > 0.0f)
{
/* normalized vector of velocity */
Vector2 velNorm = m_Vel.Divide(speed);
/* getting the floor normal */
Vector2 floorNorm = interp(m_Pos, m_PrevPos);
if (!floorNorm.Equals(ZeroVector()))
{
/* mirror velocity on floor normal vector */
/* C = A - (2 * B * (A dot B)) where A is original vector, B the mirror, C result. */
Vector2 mirVel = velNorm.Substract(floorNorm.Scale(2.0f).Scale(velNorm.Dot(floorNorm)));
/* caculate new velocity */
m_Vel = mirVel.Scale(speed).Scale(m_fBouncyness);
/* add to position to move out of collision */
m_Pos = m_Pos.Add(m_Vel.Scale(dt));
}
}
}
m_PrevPos = m_Pos;
}
public void show()
{
ellipse(m_Pos.a, m_Pos.b, 10, 10);
}
public Vector2 interp(Vector2 pos, Vector2 PrevPos)
{
/* Vector from previous position to current position */
Vector2 line = pos.Substract(PrevPos);
float iLength = line.Length();
Vector2 lineFraction = ZeroVector();
/* checks if there the is vectorlength greater zero that connects the current and the previous position */
if (iLength > 0.0f)
lineFraction = line.Divide(iLength);
/* loop from through positions between previous position and current position */
for (int i = 0; i <= iLength; ++i)
{
Vector2 normVec = getNormal(PrevPos.Add(lineFraction.Scale(i)), collisionKey);
if (!normVec.Equals(ZeroVector()))
return normVec;
}
return ZeroVector();
}
}
/* returns normal vector of a 2d landscape in a certain area */
public Vector2 getNormal(Vector2 pos, color col)
{
int area = 10;
/* prevent coordinates from being out of the window */
if (pos.a <= area || pos.a >= width - area || pos.b <= area || pos.b >= height - area)
return ZeroVector();
Vector2 avg = new Vector2();
float loops = 0;
/* loop through an area of pixels */
for (int x = -area; x <= area; ++x)
{
for (int y = -area; y <= area; ++y)
{
if (x*x + y*y <= area*area)
{
float sumX = pos.a + float(x);
float sumY = pos.b + float(y);
/* count collidable pixels in area */
if (isCollidable(sumX, sumY, col))
{
/* add up positions of these pixels */
avg.a += sumX;
avg.b += sumY;
++loops;
}
}
}
}
if (loops == 0)
return ZeroVector();
/* calculate average position */
avg = avg.Divide(loops);
/* calculate length of the vector from initial position to average position */
float avgLength = dist(avg.a, avg.b, pos.a, pos.b);
/* check if avgLenth is zero or in other words: if avg is equals to pos */
if (avgLength == 0.0f)
return ZeroVector();
/* calculate vector(connection vector) from initial position to average position */
Vector2 conVec = pos.Substract(avg);
/* return normalized connection vector */
return conVec.Divide(avgLength);
}
/* method to check if pixel on a certain position is collidable */
public boolean isCollidable(float pixelX, float pixelY, color col)
{
if (pixelX >= width || pixelX < 0 || pixelY >= height || pixelY < 0)
return false;
return pixels[int(pixelX) + int(pixelY) * width] == col;
}
Edit1:
So thanks to the friendly first replay I stripped my code by a few lines :) If there is still a problem with my post let me know!
I cant analyze correctness of your whole physic calculation but in my opinion problem is with calculation of new velocity and :
/* caculate new velocity */
m_Vel = mirVel.Scale(speed).Scale(m_fBouncyness);
/* add to position to move out of collision */
m_Pos = m_Pos.Add(m_Vel.Scale(dt));
Because if you change m_fBouncyness to real value simulating some gravitation (0.8f or less) your problem will never occur but if you change it to some unreal value like 2.0f you will lose all your balls after few bounces.
This indicate problem in algorithm. Your approach consist (in simple) of this steps in loop:
update position of ball
calculate new position
correct position depending on bounce
draw ball
Here can be problem because you calculate new position of ball - this position is out of black box so you calculate average position then new velocity and correct new position. Then draw ball and repeat but what if this new position is also out of the black box? This ball will bounce out of border ... this happens in corner because of calculation of average position (in corner you got far away from black box then at classic border (when you set m_fBouncyness to some bigger value this will happen even on normal border not only in corner!))
Hope this could help you to find your problem.
So finally I've got a solution.
It appears that the answer of Majlik was very helpful. According to his answer I did a few changes which I will explain now.
First of all I put the if-statement if (speed > 0.0f) way up, over the whole movement code so nothing happens anymore if the speed is too low. Of course you can define a certain treshold which works for you.
In addition to that I introduced an else-case, for the if(colliding) statement, in which the movement code is handled, so if the ball is currently colliding it doesn't move at all apart from the collision handling code.
Finally I thought of a new way to move the ball out of the collision. The suggestion of Maljik proved to be right. My previous method didn't move the ball out of the collision at all.
For that I made a while loop which loops as long as the ball is still in collision. In every runthrough the ball gets moved by a normalized vector with the same direction as my mirrored velocity vector. For safety reasons I still got an iterator incrementing every time, so it doesn't end in an infinite loop.
After all the solution was very obvious. But thanks to those who answered.
Below the new changed code:
public void update(float dt)
{
float speed = m_Vel.Length();
if (speed > 0.0f)
{
/* Collision based on color key */
if (isCollidable(m_Pos.a, m_Pos.b, collisionKey))
{
/* normalized vector of velocity */
Vector2 velNorm = m_Vel.Divide(speed);
/* getting the floor normal */
Vector2 floorNorm = interp(m_Pos, m_PrevPos);
if (!floorNorm.Equals(ZeroVector()))
{
/* mirror velocity on floor normal vector */
/* C = A - (2 * B * (A dot B)) where A is original vector, B the mirror, C result. */
Vector2 mirVel = velNorm.Substract(floorNorm.Scale(2.0f).Scale(velNorm.Dot(floorNorm)));
/* caculate new velocity */
m_Vel = mirVel.Scale(speed).Scale(m_fBouncyness);
int it = 0;
Vector2 normMirVel = mirVel.Divide(mirVel.Length());
while (isCollidable(m_Pos.a, m_Pos.b, collisionKey) && it < 100)
{
/* add to position to move out of collision */
m_Pos = m_Pos.Add(normMirVel);
++it;
}
}
}
else
{
/* Euler Integration more accurate Version */
/* x = x + vt + 0.5*at^2 */
m_Pos = m_Pos.Add(m_Vel.Scale(dt)).Add(m_Acc.Scale(pow(dt, 2)).Scale(0.5));
/* v = v + at */
m_Vel = m_Vel.Add(m_Acc.Scale(dt));
}
}
m_PrevPos = m_Pos;
}
Edit: I might that this is not an ideal soluation since the ball gets moved further than it should in this frame. Maybe you should only calculate the necessary distance to move out of collision and add the actual velocity step by step. Also you could compare the current velocity direction to the direction where it should go. If it's already moving in the right direction there is no interference needed.

Make background follow the cursor

I'm trying to make the background-position follow the cursor within the relative dimensions of the 'figure'. JSFiddle here: http://jsfiddle.net/LJCkj/
It's off by a few pixels and I'm not sure how to take the scale into account.
The figure has an initial 180% background size and then on hover a 115% background size.
jQuery(function($) {
toScale = 1.15; // The 115% on hover
$('figure').on('mousemove', function(e) {
el = $(this);
w = el.width() * toScale;
h = el.height() * toScale;
x = e.pageX - el.offset().left - w / 2;
y = e.pageY - el.offset().top - h / 2;
if ((x >= toScale && x <= w) && (y >= toScale && y <= h))
el.css({
backgroundPosition: x+'px '+y+'px'
});
});
});
is what I've figured so far. But it's off by a good amount. Any ideas?
I think you are doing the multiplication by toScale at the wrong time.
Also, you are checking if x and y are greater than toScale, which is 1.15, so you can never move the picture back into the corner again.
Thirdly, because you are checking if both x and y are valid, it is very hard to move it back to the corner, because as soon as any of the values if out of bounds, you stop moving.
Your adjusted javascript could look like this:
function Between(a, min, max)
{
// return a, but bound by min and max.
return a<min?min:a>max?max:a;
}
jQuery(function($) {
toScale = 1.15; // The 115% on hover
$('figure').on('mousemove', function(e) {
el = $(this);
w = el.width();
h = el.height();
x = (e.pageX - el.offset().left - w / 2) * toScale;
y = (e.pageY - el.offset().top - h / 2) * toScale;
x = Between(x, 0, w);
y = Between(y, 0, h);
el.css({
backgroundPosition: x+'px '+y+'px'
});
$('span').text(x + ',' + y);
});
});
Your fiddle. Note I've added a span to view the coordinates. It might help you as well in the further development of your code.
http://jsfiddle.net/LJCkj/2

Resources