I am drawing shapes on a canvas every frame and I want to move a shape between 2 positions with a constant speed.
I am using a lerp function I found:
const lerp = (start: number, end: number, amt: number) =>
(1 - amt) * start + amt * end;
const onFrame = () => {
const newPositionY = lerp(currentPositionY, destinationY, 0.01);
// use new position to move shape
}
window.requestAnimationFrame(onFrame);
However this causes my animation to "smooth" because the closer it gets to the destination the "slower" it moves to it (because of the speed aspect).
How can I change this to be a constant speed?
In Unity I would multiply my speed by Time.deltaTime but when I reproduce that in JavaScript by tracking delta time as a variable my shape "jumps" pretty much immediately to the destination:
const lerp = (start: number, end: number, amt: number) =>
(1 - amt) * start + amt * end;
let deltaTime
let lastFrameTimestamp
const onFrame = () => {
deltaTime = (Date.now() - lastFrameTimestamp) / 1000;
lastFrameTimestamp = Date.now();
const newPositionY = lerp(currentPositionY, destinationY, 0.01 * deltaTime);
}
window.requestAnimationFrame(onFrame);
const lerp = (start, end, amt) => (1 - amt) * start + amt * end;
const startPosition = { x: 100, y: 100 };
const endPosition = { x: 300, y: 200 };
const speed = 100; // pixels per second
let startTime = null;
const onFrame = (timestamp) => {
if (!startTime) {
startTime = timestamp;
}
const time = (timestamp - startTime) / 1000; // time in seconds
const distance = Math.sqrt(
(endPosition.x - startPosition.x) ** 2 + (endPosition.y - startPosition.y) ** 2
);
const duration = distance / speed;
const interpolation = Math.min(time / duration, 1); // interpolation amount capped at 1
const newPosition = {
x: lerp(startPosition.x, endPosition.x, interpolation),
y: lerp(startPosition.y, endPosition.y, interpolation),
};
// use newPosition to move shape
if (interpolation < 1) {
window.requestAnimationFrame(onFrame);
}
};
window.requestAnimationFrame(onFrame);
Related
I'm trying to create a 3D map in p5.js.
I took the code in mouseDragged() and mouseWheel() from prototype.orbitControl function in order to modify it based on the needs of the map.
For some reason, when I tilt the camera to the center and then try to mouse drag in order to move, it would get restricted left to right.
What I need help from is regardless of the tilt and rotation, I'd still be able to move the camera on top of the plane just like google earth.
let canvas, camera;
const sensitivityZ = 0.1;
const scaleFactor = 100;
const sensitivityX = 1;
const sensitivityY = 1;
function setup() {
canvas = createCanvas(windowWidth, 500, WEBGL);
camera = createCamera();
camera._orbit(0, 1, 0); // initial tilt
debugMode(); // show grid
// cursor: suppress right-click context menu
document.oncontextmenu = () => false;
}
function draw() {
background(170);
fill(50);
box(100);
}
function mouseDragged() {
// rotating
if (mouseButton === RIGHT) {
const deltaTheta = (-sensitivityX * (mouseX - pmouseX)) / scaleFactor;
const deltaPhi = (sensitivityY * (mouseY - pmouseY)) / scaleFactor;
camera._orbit(deltaTheta, deltaPhi, 0);
}
// moving
if (mouseButton === LEFT) {
const sensitivityTouch = 1;
const local = camera._getLocalAxes();
// normalize portions along X/Z axes
const xmag = Math.sqrt(local.x[0] * local.x[0] + local.x[2] * local.x[2]);
if (xmag !== 0) {
local.x[0] /= xmag;
local.x[2] /= xmag;
}
// normalize portions along X/Z axes
const ymag = Math.sqrt(local.y[0] * local.y[0] + local.y[2] * local.y[2]);
if (ymag !== 0) {
local.y[0] /= ymag;
local.y[2] /= ymag;
}
// move along those vectors by amount controlled by mouseX, pmouseY
const dx = -1 * sensitivityTouch * (mouseX - pmouseX);
const dz = -1 * sensitivityTouch * (mouseY - pmouseY);
// restrict movement to XZ plane in world space
camera.setPosition(
camera.eyeX + dx * local.x[0] + dz * local.z[0],
camera.eyeY,
camera.eyeZ + dx * local.x[2] + dz * local.z[2]
);
}
}
function mouseWheel(event) {
if (event.delta > 0) {
camera._orbit(0, 0, sensitivityZ * scaleFactor);
} else {
camera._orbit(0, 0, -sensitivityZ * scaleFactor);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.min.js" integrity="sha512-WJXVjqeINVpi5XXJ2jn0BSCfp0y80IKrYh731gLRnkAS9TKc5KNt/OfLtu+fCueqdWniouJ1ubM+VI/hbo7POQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
Hi I am trying to create isometric graphic app with React, mostly base on the code here.
I achieved most of the functions (ie. zoom and scroll).
But hovering tiles after zooming gives me wrong mouse position (hover position).
You can see what I mean here.
You can zoom with scrolling vertically.
When it is not zoomed in or out, hovering tile works correctly (tile color changes where the mouse positions).
But after zooming out/in it is not working right.
Does anyone know how to get the mouse position or tile index correctly after zooming in/out?
Implemented code can be found on my Github repo here
Code snippet for getting target tile is below:
const handleHover = (x: number, y: number) => {
const { e: xPos, f: yPos } = ctx.getTransform()
const mouse_x = mouseRef.current.x - x - xPos
const mouse_y = mouseRef.current.y - y - yPos
const hoverTileX =
Math.floor(
mouse_y / Tile.TILE_HEIGHT + mouse_x / Tile.TILE_WIDTH
) - 1
const hoverTileY = Math.floor(
-mouse_x / Tile.TILE_WIDTH + mouse_y / Tile.TILE_HEIGHT
)
if (
hoverTileX >= 0 &&
hoverTileY >= 0 &&
hoverTileX < gridSize &&
hoverTileY < gridSize
) {
const renderX =
x + (hoverTileX - hoverTileY) * Tile.TILE_HALF_WIDTH
const renderY =
y + (hoverTileX + hoverTileY) * Tile.TILE_HALF_HEIGHT
renderTileHover(ctx)(renderX, renderY + Tile.TILE_HEIGHT)
}
}
I am not good at maths so I really need help...
Thank you.
I figured out how to achieve this.
I will leave it here so anyone who has the same issue wont waste a lot of time for this kind of issue.
My code is like this:
/**
* #param context canvas context 2d
* #param inputX mouse/touch input position x (ie. clientX)
* #param inputY mouse/touch input position y (ie. clientY)
* #returns {x, y} x and y position of inputX/Y which map scale and position are taken into account
*/
export const getTransformedPoint = (context: CanvasRenderingContext2D, inputX: number, inputY: number) => {
const transform = context.getTransform()
const invertedScaleX = DEFAULT_MAP_SCALE / transform.a
const invertedScaleY = DEFAULT_MAP_SCALE / transform.d
const transformedX = invertedScaleX * inputX - invertedScaleX * transform.e
const transformedY = invertedScaleY * inputY - invertedScaleY * transform.f
return { x: transformedX, y: transformedY }
}
/**
*
* #param startPosition position where map start rendered (Position2D has {x: number, y: number} type)
* #param inputX mouse/touch input position x (ie. clientX)
* #param inputY mouse/touch input position x (ie. clientY)
* #returns positionX, positionY: tile position x, y axis
*/
export const getTilePosition = (
startPosition: Position2D,
inputX: number,
inputY: number
): { positionX: number; positionY: number } => {
const positionX =
Math.floor((inputY - startPosition.y) / TILE_HEIGHT + (inputX - startPosition.x) / TILE_WIDTH) - 1
const positionY = Math.floor(
(inputY - startPosition.y) / TILE_HEIGHT - (inputX - startPosition.x) / TILE_WIDTH
)
return { positionX, positionY }
}
// usage
const onClick = (e: MouseEvent) => {
const { x: mouseX, y: mouseY } = getTransformedPoint(ctx, e.clientX, e.clientY)
const { positionX, positionY } = getTilePosition(startPositionRef.current, mouseX, mouseY)
// Do something with positionX and positionY...
// ie.
if (return positionX >= 0 && positionY >= 0 && positionX < GRID_SIZE && positionY < GRID_SIZE) {
// code when a user clicks a tile within the map
}
}
I referenced this for calculating the mouse position when the map is zoomed out/in.
Try below
const handleHover = (x: number, y: number) => {
// use the current scale of the canvas context
const { a: scale, e: xPos, f: yPos } = ctx.getTransform()
const mouse_x = (mouseRef.current.x - x - xPos) * scale
const mouse_y = (mouseRef.current.y - y - yPos) * scale
// rest of the code...
}
this.up = function () {
this.velocity += this.lift;
};
this.update = function () {
this.velocity += this.gravity;
this.velocity *= 0.9;
this.y += this.velocity;
}
Hi, so I have this code in my canvas draw loop to make a shape move up and down, the thing is so its frame rate dependent so im trying to apply delta time to these functions. Anyone know how I'd go about doing that?
Help would be appreciated
requestAnimationFrame calls its callback with a timestamp. This allows you to track the time passed between two frames:
let t = null;
const frame = dt => {
// Update and render your canvas
};
const loop = ts => {
const dt = t === null ? 0 : ts - t;
t = ts;
frame(dt / 1000);
requestAnimationFrame(loop);
};
This example calls frame with the elapsed time in seconds. This means you can express your velocity and acceleration in pixels/second.
You'll get:
position in px
velocity in px/s
acceleration in px/s/s
Updating the three becomes something like:
px += vx * dt;
vx += ax * dt;
In the example below I've implemented a quick random animation. Try increasing the entity count using the number input.
For small number of entities, we're able to render at 60fps and using the real elapsed time doesn't matter much.
But as you increase the number of entities, you'll see stuff starting to move really slowly if we don't take the elapsed time in to account!
const G = 300; // px / s / s
const cvs = document.createElement("canvas");
cvs.width = 300;
cvs.height = 300;
cvs.style.border = "1px solid red";
const ctx = cvs.getContext("2d");
const Ball = (x, y, vx = 0, vy = 0, ax = 0, ay = G, color = "black") => {
const render = () => {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
};
const update = (dt) => {
return Ball(
x + dt * vx,
y + dt * vy,
vx + dt * ax,
vy + dt * ay,
ax,
ay,
color
);
}
return { render, update };
}
const randomBalls = n => Array.from(
{ length: n },
() => Ball(
Math.random() * 300,
Math.random() * 300,
-100 + Math.random() * 200,
Math.random() * -200,
0,
G,
`#${[
(Math.floor(Math.random() * 255)).toString(16),
(Math.floor(Math.random() * 255)).toString(16),
(Math.floor(Math.random() * 255)).toString(16)
].join("")}`
)
);
// Game
let balls = randomBalls(100);
let t = null;
let useFrameTime = true;
const frame = dt => {
if (!useFrameTime) dt = 1 / 60;
// Update entities
balls = balls.map(b => b.update(dt));
// Clear window
ctx.clearRect(0, 0, cvs.width, cvs.height);
// Draw new rects
balls.forEach(b => b.render());
}
const loop = ts => {
const dt = t === null ? 0 : ts - t;
t = ts;
frame(dt / 1000);
requestAnimationFrame(loop);
}
document.body.append(cvs);
requestAnimationFrame(loop);
document.querySelector("button").addEventListener("click", () => {
const n = +document.querySelector("input[type=number]").value;
balls = randomBalls(n);
});
document.querySelector("input[type=checkbox]").addEventListener("change", (e) => {
useFrameTime = e.target.checked;
});
body { display: flex; }
<div style="width: 300px">
<p>Increase the count here. Time for all balls to exit the frame should stay roughly the same, but you should see the frame rate drop as you increase the number of entities to render.
</p>
<input type="number" value="100">
<button>Go</button>
<br>
<label><input type="checkbox" checked> Use frame time in render loop</label>
</div>
I am trying to rotate a cone from its apex, rather than its centre, so that the apex remains in the same position.
I've found the example below from the following link:
https://groups.google.com/forum/#!topic/cesium-dev/f9ZiSWPMgus
But it only shows how to rotate the cone by 90 degrees, if you choose a different value for roll, like 45 or 30 degrees, it gets skewed, and the apex ends up in the wrong place.
I know its something to do with the offset, but can't make any progress from there. Is there some way to calculate the correct offset for any value of roll?
I'd also like to extend the length of the cone when its rotated, so that when its rotated 30 degrees for example, the bottom of the cone will still reach the ground in that direction, while the apex still remains in its original place, I don't know how feasible that is though.
Here's a glitch of the code sample below:
https://glitch.com/edit/#!/cesium-cone-rotation
var viewer = new Cesium.Viewer('cesiumContainer');
var position = Cesium.Cartesian3.fromDegrees(-75, 40, 90);
//Original, non-rotated cone for comparison.
viewer.entities.add(new Cesium.Entity({
position: position,
point: {
color: Cesium.Color.YELLOW,
show: true,
pixelSize: 20
},
cylinder: {
topRadius: 0,
bottomRadius: 45,
length: 180,
material: Cesium.Color.YELLOW.withAlpha(0.5)
}
}));
var heading = Cesium.Math.toRadians(0.0);
var pitch = Cesium.Math.toRadians(0.0);
var roll = Cesium.Math.toRadians(90.0);
var hpr = new Cesium.HeadingPitchRoll(heading, pitch, roll);
//Create a rotation
var orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr);
// offset the rotation so it's rotating from the apex of the cone, instead of the centre
var offset = new Cesium.Cartesian3(0, 90, 90);
//Create a transform for the offset.
var enuTransform = Cesium.Transforms.eastNorthUpToFixedFrame(position);
//Transform the offset
Cesium.Matrix4.multiplyByPointAsVector(enuTransform, offset, offset);
//Add the offset to the original position to get the final value.
Cesium.Cartesian3.add(position, offset, position);
viewer.entities.add(new Cesium.Entity({
position: position,
orientation: orientation,
point: {
color: Cesium.Color.YELLOW,
show: true,
pixelSize: 20
},
cylinder: {
topRadius: 0,
bottomRadius: 45,
length: 180,
material: Cesium.Color.YELLOW.withAlpha(0.5)
}
}));
viewer.zoomTo(viewer.entities);
Here's what I came up with to rotate and translate a cylinder when you want to point it in a specific orientation specified by an azimuth and elevation.
/**
* Calculate the position and orientation needed for the beam entity.
* #param {Cesium.Cartesian3} position - The position of the desired origin.
* #param {Number} az - The azimuth of the beam center in radians.
* #param {Number} el - The elevation of the beam center in radians.
* #param {Number} range - The range of the beam in meters.
*
* #returns {[Cesium.Cartesian3, Cesium.Quaternion]} Array of the position and
* orientation to use for the beam.
*/
calculateBeam(position, az, el, range) {
// The origin of Cesium Cylinder entities is the center of the cylinder.
// They are also pointed straight down towards the local East-North plane. The
// math below rotates and translates the cylinder so that its origin is the tip
// of the cylinder and its orientation is pointed in the direction specified by
// the az/el.
let heading = az - Cesium.Math.toRadians(90);
let pitch = Cesium.Math.toRadians(90) + el;
let hpr = new Cesium.HeadingPitchRoll(heading, pitch, 0.0);
let x = range/2.0 * Math.sin(pitch) * Math.cos(heading);
let y = -range/2.0 * Math.sin(heading) * Math.sin(pitch);
let z = -range/2.0 * Math.cos(pitch);
var offset = new Cesium.Cartesian3(x, y, z);
let enuTransform = Cesium.Transforms.eastNorthUpToFixedFrame(position);
Cesium.Matrix4.multiplyByPointAsVector(enuTransform, offset, offset);
let newPosition = Cesium.Cartesian3.add(position, offset, new Cesium.Cartesian3());
let orientation = Cesium.Transforms.headingPitchRollQuaternion(position, hpr);
return [newPosition, orientation];
}
This will give you the position/orientation to use when you create the cylinder entity. It will place the cylinder such that the tip of the cylinder is located at 'position' and it is pointed in the direction specified by the azimuth and elevation. Azimuth is relative to North with positive angles towards East. Elevation is relative to the North-East plane, with positive angles up. Range is the length of the cylinder.
This doesn't get you the behavior you wanted as far as lengthening the cylinder as you rotate it, but hopefully it helps.
I have the same problem as you. This is my reference code. This is a function of computing the matrix of a cone at any angle.
computedModelMatrix(Cartesian3: any, attitude: any, length: any) {
//锥体距离卫星的高度
let oldLength = length / 2;
let centerCartesian3 = new Cesium.Cartesian3(Cartesian3.x, Cartesian3.y, Cartesian3.z);
let oldX = 0, oldY = 0, oldZ = -oldLength, newX = 0, newY = 0, newZ = 0;
let heading = attitude.heading;
//规定顺时针为正旋转,正东方向为0度
if (heading < 0) {
heading = heading + 360;
}
let roll = attitude.roll;
let pitch = attitude.pitch;
let headingRadians = Cesium.Math.toRadians(heading);
let pitchRadians = Cesium.Math.toRadians(pitch);
let rollRadians = Cesium.Math.toRadians(roll);
let hpr = new Cesium.HeadingPitchRoll(headingRadians, pitchRadians, rollRadians);
let orientation = Cesium.Transforms.headingPitchRollQuaternion(centerCartesian3, hpr);
//旋转roll
newY = oldY + oldLength * Math.sin(rollRadians);
newZ = oldZ + oldLength - oldLength * Math.cos(rollRadians);
let pitchTouying = oldLength * Math.cos(rollRadians);//进行pitch变化时在Y轴和Z轴组成的平面的投影
//旋转pitch
newX = oldX + pitchTouying * Math.sin(pitchRadians);
newZ = newZ + (pitchTouying - pitchTouying * Math.cos(pitchRadians));
if (heading != 0) {
let headingTouying = Math.sqrt(Math.pow(Math.abs(newX), 2) + Math.pow(Math.abs(newY), 2));//进行heading变化时在Y轴和X轴组成的平面的投影
//旋转heading
let Xdeg = Cesium.Math.toDegrees(Math.acos(Math.abs(newX) / Math.abs(headingTouying)));//现有投影线与X轴的夹角
let newXdeg = 0;//旋转heading后与X轴的夹角
let newXRadians = 0;//旋转heading后与X轴的夹角弧度
if (newX >= 0 && newY >= 0) {
newXdeg = heading - Xdeg;
} else if (newX > 0 && newY < 0) {
newXdeg = heading + Xdeg;
} else if (newX < 0 && newY > 0) {
newXdeg = heading + (180 + Xdeg);
} else {
newXdeg = heading + (180 - Xdeg)
}
if (newXdeg >= 360) {
newXdeg = 360 - newXdeg;
}
if (newXdeg >= 0 && newXdeg <= 90) {
newXRadians = Cesium.Math.toRadians(newXdeg);
newY = -headingTouying * Math.sin(newXRadians);
newX = headingTouying * Math.cos(newXRadians);
} else if (newXdeg > 90 && newXdeg <= 180) {
newXRadians = Cesium.Math.toRadians(180 - newXdeg);
newY = -headingTouying * Math.sin(newXRadians);
newX = -headingTouying * Math.cos(newXRadians)
} else if (newXdeg > 180 && newXdeg <= 270) {
newXRadians = Cesium.Math.toRadians(newXdeg - 180);
newY = headingTouying * Math.sin(newXRadians);
newX = -(headingTouying * Math.cos(newXRadians))
} else {
newXRadians = Cesium.Math.toRadians(360 - newXdeg);
newY = headingTouying * Math.sin(newXRadians);
newX = headingTouying * Math.cos(newXRadians)
}
}
let offset = new Cesium.Cartesian3(newX, newY, newZ);
let newPosition = this.computeOffset(centerCartesian3, offset);
return Cesium.Matrix4.fromTranslationQuaternionRotationScale(newPosition, orientation, new Cesium.Cartesian3(1, 1, 1))
}
computeOffset(Cartesian3: any, offset: any) {
let enuTransform = Cesium.Transforms.eastNorthUpToFixedFrame(Cartesian3);
Cesium.Matrix4.multiplyByPointAsVector(enuTransform, offset, offset);
return Cesium.Cartesian3.add(Cartesian3, offset, new Cesium.Cartesian3());
}
paintForegroundBars(hours: Array<number>) {
let barColor = "#b3bec9";
let numBars = hours.length;
let barWidth = Math.floor((this.canvasWidth / numBars) - this.barsSpacing);
let maxBarHeight = this.canvasHeight - (this.timesHeight + (this.timesSpacing * 2));
let barLeft = 0 + (this.barsSpacing / 2);
this.canvasContext.fillStyle = barColor;
this.canvasContext.strokeStyle = barColor;
this.canvasContext.lineJoin = "round";
this.canvasContext.lineWidth = this.cornerRadius;
for (let i = 0; i < numBars; i++) {
let barHeight = Math.round(maxBarHeight * hours[i]);
if (barHeight > 0) {
let barTop = maxBarHeight - barHeight;
let roundedBarTop = barTop + (this.cornerRadius / 2);
let roundedBarLeft = barLeft + (this.cornerRadius / 2);
let roundedBarWidth = barWidth - this.cornerRadius;
let roundedBarHeight = barHeight - this.cornerRadius;
this.canvasContext.strokeRect(roundedBarLeft, roundedBarTop, roundedBarWidth, roundedBarHeight);
this.canvasContext.fillRect(roundedBarLeft, roundedBarTop, roundedBarWidth, roundedBarHeight);
}
barLeft = Math.floor(barLeft + barWidth) + (this.barsSpacing);
}
}
At the moment I am drawing the height of a bar chart with the below code:
this.canvasContext.strokeRect(roundedBarLeft, roundedBarTop, roundedBarWidth, roundedBarHeight);
this.canvasContext.fillRect(roundedBarLeft, roundedBarTop, roundedBarWidth, roundedBarHeight);
Instead of when this runs it just being a fixed height I want it to animate from 0 to the height that has been calculated in my JS. How do you go about doing this?
Many thanks
Below is a simple example of how this kind of animation works. The thing you are looking for is the easing value - once you have that, you are set! In this case I store the start time inside the start variable, and then you simply take the current time, remove the start time and divide it by the time you want to pass. This will give you a number between 0 and 1, and multiplying any other number by that will give you the number in the range 0 to n. If you want to add a base value to this, your general formula is basically this:
fromValue + (nowTime - sinceTime) / duration * (toValue - fromValue);
The reason the easing value is so important is that it allows tweening. For example, you can create a smooth curve by multiplying this easing value by itself:
var easing = (nowTime - sinceTime) / duration;
easing = easing * easing;
fromValue + easing * (toValue - fromValue);
Use a graphing application to learn more about these curves :)
var canvas = document.querySelector('canvas');
var ctx = canvas.getContext('2d');
var start = Date.now();
var duration = 5000;
var animationFrame = false;
canvas.width = 40;
canvas.height = 400;
function drawBar(){
var progress = (Date.now() - start) / duration;
if( progress >= 1 ){
// Final state before stopping any drawing function
ctx.clearRect( 0, 0, canvas.width, canvas.height );
ctx.fillRect( 0, 0, canvas.width, canvas.height );
window.cancelAnimationFrame( drawBar );
} else {
// In progress
ctx.clearRect( 0, 0, canvas.width, canvas.height );
ctx.fillRect( 0, 0, canvas.width, canvas.height * progress );
window.requestAnimationFrame( drawBar );
}
}
animationFrame = setInterval( drawBar, 1000 / 60 );
document.body.addEventListener('click', function( event ){
start = Date.now();
window.cancelAnimationFrame( drawBar );
window.requestAnimationFrame( drawBar );
});
<canvas></canvas>