Three.js scene is distorted until the mouse is moved - javascript

My three.js scene is completely distorted until I move the mouse somewhere on the site.
You can see the nature of the distortion on the image below:
When I move the mouse, the scene suddenly pops and everything is fine. It doesn't seem to matter where exactly the cursor is within the site, it doesn't have to be over the canvas where my scene is rendered.
This is how the scene looks after moving the mouse:
The following three.js related dependencies are used:
"three": "^0.108.0"
"three-orbitcontrols": "^2.102.2"
"three.meshline": "^1.2.0"
I tried updating three to the latest version (0.116.1), but that didn't solve the issue either. I managed to replicate this issue on Firefox and Edge, but not on Chrome.
Some extra context: we use OffscreenCanvas for better performance, the mouse positions are sent from the main thread to the web worker on mousemove event, we use that information to slightly move the camera and the background (with offsets). I temporarily removed to mousemove handler logic from the web worker code and the issue still popped up, so it's probably unrelated. We use tween.js to make the camera animations smooth.
Relevant code snippets:
Scene setup:
const {scene, camera} = makeScene(elem, cameraPosX, 0, 60, 45);
const supportsWebp = (browser !== 'Safari');
imageLoader.load(backgroundImage, mapImage => {
const texture = new THREE.CanvasTexture(mapImage);
texture.anisotropy = renderer.capabilities.getMaxAnisotropy();
texture.minFilter = THREE.LinearFilter;
// Repeat background so we don't run out of it during offset changes on mousemove
texture.wrapS = THREE.MirroredRepeatWrapping;
texture.wrapT = THREE.MirroredRepeatWrapping;
scene.background = texture;
});
// Creating objects in the scene
let orbitingPlanet = getPlanet(0xffffff, true, 1 * mobilePlanetSizeAdjustment);
scene.add(orbitingPlanet);
// Ellipse class, which extends the virtual base class Curve
let curveMain = new THREE.EllipseCurve(
0, 0, // ax, aY
80, 30, // xRadius, yRadius
0, 2 * Math.PI, // aStartAngle, aEndAngle
false, // aClockwise
0.2 // aRotation
);
let ellipseMainGeometry = new THREE.Path(curveMain.getPoints(100)).createPointsGeometry(100);
let ellipseMainMaterial = new MeshLine.MeshLineMaterial({
color: new THREE.Color(0xffffff),
opacity: 0.2,
transparent: true,
});
let ellipseMain = new MeshLine.MeshLine();
ellipseMain.setGeometry(ellipseMainGeometry, function(p) {
return 0.2; // Line width
});
const ellipseMainMesh = new THREE.Mesh(ellipseMain.geometry, ellipseMainMaterial );
scene.add(ellipseMainMesh);
// Create a halfish curve on which one of the orbiting planets will move
let curveMainCut = new THREE.EllipseCurve(
0, 0, // ax, aY
80, 30, // xRadius, yRadius
0.5 * Math.PI, 1.15 * Math.PI, // aStartAngle, aEndAngle
false, // aClockwise
0.2 // aRotation
);
let lastTweenRendered = Date.now();
let startRotation = new THREE.Vector3(
camera.rotation.x,
camera.rotation.y,
camera.rotation.z);
let tweenie;
return (time, rect) => {
camera.aspect = state.width / state.height;
camera.updateProjectionMatrix();
let pt1 = curveMainCut.getPointAt(t_top_faster);
orbitingPlanet.position.set(pt1.x, pt1.y, 1);
t_top_faster = (t_top_faster >= 1) ? 0 : t_top_faster += 0.001;
// Slightly rotate the background on mouse move
if (scene && scene.background) {
// The rotation mush be between 0 and 0.01
scene.background.rotation =
Math.max(-0.001,Math.min(0.01, scene.background.rotation + 0.00005 * target.x));
let offsetX = scene.background.offset.x + 0.00015 * target.x;
let offsetY = scene.background.offset.y + 0.00015 * target.y;
scene.background.offset = new THREE.Vector2(
(offsetX > -0.05 && offsetX < 0.05) ? offsetX : scene.background.offset.x,
(offsetY > -0.05 && offsetY < 0.05) ? offsetY : scene.background.offset.y);
}
lastTweenRendered = tweenAnimateCamera(lastTweenRendered, tweenie, camera, startRotation, 200);
renderer.render(scene, camera);
};
makeScene function:
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(fieldOfView, state.width / state.height, 0.1, 100000000);
camera.position.set(camPosX, camPosY, camPosZ);
camera.lookAt(0, 0, 0);
scene.add(camera);
return {scene, camera};
Camera animation based on mouse positions:
function tweenAnimateCamera(lastTweenRendered, tween, camera, startRotation, period) {
target.x = (1 - mouse.x) * 0.002;
target.y = (1 - mouse.y) * 0.002;
let now = Date.now();
if ((
// Don't let the camera go too far
startRotation.x > -0.01 && startRotation.x < 0.01) &&
now - lastTweenRendered > (period / 2)) {
if (tween) {
tween.stop();
}
lastTweenRendered = now;
let endRotation = new THREE.Vector3(
camera.rotation.x + 0.005 * (target.y - camera.rotation.x),
camera.rotation.y + 0.005 * (target.x - camera.rotation.y),
camera.rotation.z);
tween = new TWEEN.Tween(startRotation)
.to(endRotation, period * 2)
.easing(TWEEN.Easing.Quadratic.InOut)
.onUpdate(function (v) {
camera.rotation.set(v.x, v.y, v.z);
})
.onComplete(function(v) {
startRotation = v.clone();
});
tween.start();
}
TWEEN.update();
return lastTweenRendered
}
Mouse position receiver logic:
if (e.data.type === 'mousePosUpdate') {
if (e.data.x !== -100000 && e.data.y !== -100000) {
mouse.x = ( e.data.x - state.width / 2 );
mouse.y = ( e.data.y - state.height / 2 );
target.x = ( 1 - mouse.x ) * 0.002;
target.y = ( 1 - mouse.y ) * 0.002;
}
}
Render loop:
function render(time) {
time *= 0.001;
for (const {elem, fn, ctx} of sceneElements) {
// get the viewport relative position of this element
canvasesUpdatedPos.forEach( canvasUpdate => {
if (canvasUpdate.id === elem.id) {
elem.rect = canvasUpdate.rect;
}
});
const rect = elem.rect;
const bottom = rect.bottom;
const height = rect.height;
const left = rect.left;
const right = rect.right;
const top = rect.top;
const width = rect.width;
const rendererCanvas = renderer.domElement;
const isOffscreen =
bottom < 0 ||
top > state.height ||
right < 0 ||
left > state.width;
if (!isOffscreen && width !== 0 && height !== 0) {
// make sure the renderer's canvas is big enough
let isResize = resizeRendererToDisplaySize(renderer, height, width);
// make sure the canvas for this area is the same size as the area
if (ctx.canvas.width !== width || ctx.canvas.height !== height) {
ctx.canvas.width = width;
ctx.canvas.height = height;
state.width = width;
state.height = height;
}
renderer.setScissor(0, 0, width, height);
renderer.setViewport(0, 0, width, height);
fn(time, rect);
// copy the rendered scene to this element's canvas
ctx.globalCompositeOperation = 'copy';
ctx.drawImage(
rendererCanvas,
0, rendererCanvas.height - height, width, height, // src rect
0, 0, width, height); // dst rect
}
}
// Limiting to 35 FPS.
setTimeout(function() {
if (!stopAnimating) {
requestAnimationFrame(render);
}
}, 1000 / 35);
}

I don't see where you're initiating target and mouse anywhere. My best guess is that target.x, target.y or mouse.x, mouse.y are undefined or 0, and it's probably causing a division by 0, or a calculation that returns NaN, which is giving you that infinitely stretched texture. You should be able to fix this if you initiate those vectors:
var target = new THREE.Vector2();
var mouse = new THREE.Vector2();

First of anyone running into similar issues please take a look at Marquizzo's answer and Ethan Hermsey's comment (on the question) as well, since they provided a good possible cause for this issue, although the issue was different in my case.
The distortion was related to the OffscreenCanvas in our case. When the application starts we send the OffscreenCanvas along with it's size to the web worker:
const rect = element.getBoundingClientRect();
const offScreenCanvas = element.transferControlToOffscreen();
worker.post({
type: 'canvas',
newCanvas: offScreenCanvas,
width: rect.width,
height: rect.height
}, [offScreenCanvas]);
The cause of the issue was the height, which was incorrect in certain cases, 1px to be precise in the example pictured in the question. The incorrect height popped up because of a race condition, in a separate script we used to set up the height of certain canvas container elements with the following script:
$(".fullscreen-block").css({ height: var wh = $(window).height() });
However, we usually sent the size of the canvas to the worker before this happened. Substituting this JS code with a simple CSS rule solved this issue:
.fullscreen-block {
height: 100vh;
}
So, in the end, the issue was not related to the mouse event's handled by us, I can only guess why moving the mouse fixed the distortion. I'd say Firefox probably revalidates/recalculates the size of DOM elements when the mouse is moved and we were notified about size changes, causing the animation to pop to the correct size and state.

Related

Three.js OrbitControls Rotating Around a ring

I have an OrbitControls camera which rotates around the dot: target = player.position.x, y and z. It's pretty ok, but the player is right in the centre of the screen. I need a camera to rotate around a ring...
first picture: target is in the centre, second: target is always placed to the left of the centre
Let me explain:
Here is my code:
controls.target.x = player.position.x;
controls.target.y = player.position.y+3;
controls.target.z = player.position.z;
So we rotate around a fixed dot.
But I need a dot which changes its position depending on controls.getAzimuthalAngle() or something else.
The camera must be like in GTA V or other games, where a player is not right in the centre of the screen, but a little to the left, so it's more convenient for, for example, aiming, the player does not interfere with the aim in the centre.
Help...
Generally for a 3rd person camera you constantly target the camera at the player (or some offset from the player, or some object attached to the player)
You then have the camera follow the player over time.
In the code below there is a cameraRig that follows the player. The camera rig is there so we don't have to do any math to keep the camera above the ground. Similarly there is a camTarget which is attached to the player. This is so the camera looks at about shoulder height.
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const keys = {};
const scene = new THREE.Scene();
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 500;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
// put the camera 5 units above the rig
const cameraRig = new THREE.Object3D();
cameraRig.add(camera);
camera.position.y = 5;
scene.add(cameraRig);
cameraRig.position.z = 5;
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
{
const size = 200;
const divisions = 100;
const gridHelper = new THREE.GridHelper(size, divisions);
scene.add(gridHelper);
}
const boxWidth = 0.5;
const boxHeight = 2;
const boxDepth = 0.5;
const geometry = new THREE.CylinderGeometry(boxWidth, boxDepth, boxHeight);
const material = new THREE.MeshPhongMaterial({color:'red'});
const cube = new THREE.Mesh(geometry, material);
const player = new THREE.Object3D();
const camTarget = new THREE.Object3D();
cube.position.y = boxHeight / 2; // move cube above ground
player.add(cube);
camTarget.position.y = boxHeight * 3 / 2; // target 2/3ds up the player
player.add(camTarget);
scene.add(player);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
const moveDir = new THREE.Vector3();
const camTargetPos = new THREE.Vector3();
let then = 0;
function render(now) {
now *= 0.001;
deltaTime = now - then;
then = now;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
// left, right, a, d
const dx = ((keys[37] || keys[65]) ? 1 : 0) +
((keys[39] || keys[68]) ? -1 : 0);
// up, down, w, s
const dy = ((keys[38] || keys[87]) ? 1 : 0) +
((keys[40] || keys[83]) ? -1 : 0);
const playerMoveSpeed = 10; // units per second
camera.getWorldDirection(moveDir);
moveDir.y = 0; // no up down movement
moveDir.normalize();
// move player forward/back
player.position.addScaledVector(moveDir, dy * playerMoveSpeed * deltaTime);
// rotate direction 90 degrees
const t = moveDir.x;
moveDir.x = moveDir.z;
moveDir.z = -t;
// move player left/right
player.position.addScaledVector(moveDir, dx * playerMoveSpeed * deltaTime);
// if the cameraRig is too far from
// player then move it
const maxDistance = 6;
const maxCamMoveSpeed = 0.015;
const distance = cameraRig.position.distanceTo(player.position);
if (distance > maxDistance) {
const amount = maxCamMoveSpeed;
cameraRig.position.lerp(player.position, amount);
}
camTarget.getWorldPosition(camTargetPos);
camera.lookAt(camTargetPos);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
window.addEventListener('keydown', (e) => {
e.preventDefault();
keys[e.keyCode] = true;
});
window.addEventListener('keyup', (e) => {
e.preventDefault();
keys[e.keyCode] = false;
});
}
main();
body {
margin: 0;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r113/build/three.min.js"></script>
<canvas id="c" tabindex="0"></canvas>
3rd person cameras can be hard. The one above is too simple. You can think of it as if the cameraRig is being dragged on a string behind the player. If you back up the camera doesn't move. It only moves if the the camera is maxDistance away.
It's common to instead put the camera on a stick so if you walk backward the stick pushed the camera back.
function main() {
const canvas = document.querySelector('#c');
const renderer = new THREE.WebGLRenderer({canvas});
const keys = {};
const scene = new THREE.Scene();
const fov = 75;
const aspect = 2; // the canvas default
const near = 0.1;
const far = 500;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
// put the camera 5 units above the rig
const cameraRig = new THREE.Object3D();
cameraRig.add(camera);
camera.position.y = 5;
scene.add(cameraRig);
cameraRig.position.z = 5;
{
const color = 0xFFFFFF;
const intensity = 1;
const light = new THREE.DirectionalLight(color, intensity);
light.position.set(-1, 2, 4);
scene.add(light);
}
{
const size = 200;
const divisions = 100;
const gridHelper = new THREE.GridHelper(size, divisions);
scene.add(gridHelper);
}
const boxWidth = 0.5;
const boxHeight = 2;
const boxDepth = 0.5;
const geometry = new THREE.CylinderGeometry(boxWidth, boxDepth, boxHeight);
const material = new THREE.MeshPhongMaterial({color:'red'});
const cube = new THREE.Mesh(geometry, material);
const player = new THREE.Object3D();
const camTarget = new THREE.Object3D();
cube.position.y = boxHeight / 2; // move cube above ground
player.add(cube);
camTarget.position.y = boxHeight * 3 / 2; // target 2/3ds up the player
player.add(camTarget);
scene.add(player);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
const moveDir = new THREE.Vector3();
const camTargetPos = new THREE.Vector3();
const rigTargetPos = new THREE.Vector3();
function moveCameraToDistance(desiredDistance) {
const maxCamMoveSpeed = 0.02;
// delta from player to rig
rigTargetPos.subVectors(cameraRig.position, player.position);
// remove up/down
rigTargetPos.y = 0; // no up/down
// make unit vector
rigTargetPos.normalize();
// make desiredDistance long
rigTargetPos.multiplyScalar(desiredDistance);
// add player position
rigTargetPos.add(player.position);
// move rig toward that position
const curDistance = cameraRig.position.distanceTo(player.position);
cameraRig.position.lerp(
rigTargetPos,
THREE.MathUtils.lerp(
maxCamMoveSpeed,
1,
THREE.MathUtils.clamp(1 - curDistance / desiredDistance, 0, 1)));
}
let then = 0;
function render(now) {
now *= 0.001;
deltaTime = now - then;
then = now;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
// left, right, a, d
const dx = ((keys[37] || keys[65]) ? 1 : 0) +
((keys[39] || keys[68]) ? -1 : 0);
// up, down, w, s
const dy = ((keys[38] || keys[87]) ? 1 : 0) +
((keys[40] || keys[83]) ? -1 : 0);
const playerMoveSpeed = 10; // units per second
camera.getWorldDirection(moveDir);
moveDir.y = 0; // no up down movement
moveDir.normalize();
// move player forward/back
player.position.addScaledVector(moveDir, dy * playerMoveSpeed * deltaTime);
// rotate direction 90 degrees
const t = moveDir.x;
moveDir.x = moveDir.z;
moveDir.z = -t;
// move player left/right
player.position.addScaledVector(moveDir, dx * playerMoveSpeed * deltaTime);
// keep camera at distance
const minDistance = 4;
const maxDistance = 6;
const distance = cameraRig.position.distanceTo(player.position);
if (distance > maxDistance) {
moveCameraToDistance(maxDistance);
} else if (distance < minDistance) {
moveCameraToDistance(minDistance);
}
// compute point from player in direction of rig
// at desired distance
camTarget.getWorldPosition(camTargetPos);
camera.lookAt(camTargetPos);
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
window.addEventListener('keydown', (e) => {
e.preventDefault();
keys[e.keyCode] = true;
});
window.addEventListener('keyup', (e) => {
e.preventDefault();
keys[e.keyCode] = false;
});
}
main();
body {
margin: 0;
}
#c {
width: 100vw;
height: 100vh;
display: block;
}
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r113/build/three.js"></script>
<canvas id="c" tabindex="0"></canvas>
You then need to deal with things like stuff getting between the camera and the player. (some games fade that stuff out). You also need to deal with say walking through a door. For a while player will be on one side of the door and the camera on the other (or rather opposite sides of the doorway so does the camera come down so it can see through the door? Does the wall fade out? Does the camera jump through the door and start looking from some other positions. Rumor is in Mario 64 one programmer worked on the camera an entire year and nothing else.)
Note that some of the code above only works in the special case that someObject.position is a world space postion (the object has no parent objects, or if it does all the parents have position = 0,0,0 rotation = 0,0,0, scale = 1,1,1). If the object does have parents then you'd need to get the world position with
const wp = new THREE.Vector3();
someObject.getWorldPosition(wp);
And if you wanted to apply a world position you'd need to do more work to make the position relative of the parent. For now, to keep it simple, I just used objects who's world position is their position.

Smooth camera transition when mouse re-enters canvas - Three.js

I am attempting to move an object with Three.js by having the object track the mouse movements of a user. Whenever the mouse leaves the canvas and re-enters the canvas in a different spot, the object will jump to the position of the mouse in the new spot.
I am trying to avoid this jerky object movement and have it smoothly transition to the new location of the mouse when a user moves their cursor off screen and has it re-enter in a new position.
var scene = new THREE.Scene();
var camera = new THREE.PerspectiveCamera(60, 1, 1, 1000);
camera.position.set(0, 0, 20);
var renderer = new THREE.WebGLRenderer({
antialias: true
});
var canvas = renderer.domElement;
document.body.appendChild(canvas);
var box = new THREE.Mesh(new THREE.BoxBufferGeometry(), new THREE.MeshNormalMaterial());
box.geometry.translate(0, 0, 0.5);
box.scale.set(1, 1, 3);
scene.add(box);
var plane = new THREE.Plane(new THREE.Vector3(0, 0, 1), -10);
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
var pointOfIntersection = new THREE.Vector3();
canvas.addEventListener("mousemove", onMouseMove, false);
//Move object view based on user mouse position
function onMouseMove(event){
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
raycaster.ray.intersectPlane(plane, pointOfIntersection);
box.lookAt(pointOfIntersection);
}
renderer.setAnimationLoop(() => {
if (resize(renderer)) {
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
});
function resize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
Example of jump when moving mouse in canvas, then move mouse out of canvas to new position, and finally re-entering canvas: https://codepen.io/prisoner849/pen/yGMWNg
You will want to store the box look x y position in addition to the current mouse x y position coordinates.
Then inside the loop call an update function. Inside the update function you will ease the look position towards the current mouse position.
You then use lookPosition instead of mouse in raycaster.setFromCamera.
The easing can look like this:
const easeAmount = 8;
function update(){
lookPosition.x += ( mouse.x - lookPosition.x ) / easeAmount;
lookPosition.y += ( mouse.y - lookPosition.y ) / easeAmount;
}
Working solution:
https://codepen.io/cdigital/pen/aboEeGp

Getting relative mouse position on canvas after scaling

Problem: I'm working with an HTML canvas. My canvas has a background image that multiple people can draw over in real-time (via socket.io), but drawing breaks if you've zoomed in.
Cause: To calculate where to start and end a line, I normalize input upon capture to be between 0 and 1 inclusive, like so:
// Pseudocode
line.x = mousePosition.x / canvas.width;
line.y = mousePosition.y / canvas.height;
Because of this, the canvas can be of any size and in any position.
To implement a zoom-on-scroll functionality, I simply translate based on the current mouse position, scale the canvas by a factor of 2, then translate back the negative value of the current mouse position (as recommended here).
Here's where the problem lies
When I zoom, the canvas doesn't seem to have a notion of it's original size.
For instance, let's say I have a 1000px square canvas. Using my normalized x and y above, the upper left corner is 0, 0 and the lower right is 1, 1.
I then zoom into the center through scaling by a factor of 2. I would expect that my new upper left would be 0.5, 0.5 and my lower right would be 0.75, 0.75, but it isn't. Even when I zoom in, the upper left is still 0, 0 and the lower right is 1, 1.
The result is that when I zoom in and draw, the lines appear where they would as if I were not zoomed at all. If I zoomed into the center and "drew" in the upper left, I'd see nothing until I scrolled out to see that the line was actually getting drawn on the original upper left.
What I need to know: When zoomed, is there a way to get a read on what your new origin is relative to the un-zoomed canvas, or what amount of the canvas is hidden? Either of these would let me zoom in and draw and have it track correctly.
If I'm totally off base here and there's a better way to approach this, I'm all ears. If you need additional information, I'll provide what I can.
It's not clear to me what you mean by "zoomed".
Zoomed =
made the canvas a different size?
changed the transform on the canvas
used CSS transform?
used CSS zoom?
I'm going to assume it's transform on the canvas in which case it's something like
function getElementRelativeMousePosition(e) {
return [e.offsetX, e.offsetY];
}
function getCanvasRelativeMousePosition(e) {
const pos = getElementRelativeMousePosition(e);
pos[0] = pos[0] * ctx.canvas.width / ctx.canvas.clientWidth;
pos[1] = pos[1] * ctx.canvas.height / ctx.canvas.clientHeight;
return pos;
}
function getComputedMousePosition(e) {
const pos = getCanvasRelativeMousePosition(e);
const p = new DOMPoint(...pos);
const point = inverseOriginTransform.transformPoint(p);
return [point.x, point.y];
}
Where inverseOriginTransform is the inverse of whatever transform you're using to zoom and scroll the contents of the canvas.
const settings = {
zoom: 1,
xoffset: 0,
yoffset: 0,
};
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const lines = [
[[100, 10], [200, 30]],
[[50, 50], [100, 30]],
];
let newStart;
let newEnd;
let originTransform = new DOMMatrix();
let inverseOriginTransform = new DOMMatrix();
function setZoomAndOffsetTransform() {
originTransform = new DOMMatrix();
originTransform.translateSelf(settings.xoffset, settings.yoffset);
originTransform.scaleSelf(settings.zoom, settings.zoom);
inverseOriginTransform = originTransform.inverse();
}
const ui = document.querySelector('#ui')
addSlider(settings, 'zoom', ui, 0.25, 3, draw);
addSlider(settings, 'xoffset', ui, -100, +100, draw);
addSlider(settings, 'yoffset', ui, -100, +100, draw);
draw();
function updateAndDraw() {
draw();
}
function getElementRelativeMousePosition(e) {
return [e.offsetX, e.offsetY];
}
function getCanvasRelativeMousePosition(e) {
const pos = getElementRelativeMousePosition(e);
pos[0] = pos[0] * ctx.canvas.width / ctx.canvas.clientWidth;
pos[1] = pos[1] * ctx.canvas.height / ctx.canvas.clientHeight;
return pos;
}
function getTransformRelativeMousePosition(e) {
const pos = getCanvasRelativeMousePosition(e);
const p = new DOMPoint(...pos);
const point = inverseOriginTransform.transformPoint(p);
return [point.x, point.y];
}
canvas.addEventListener('mousedown', (e) => {
const pos = getTransformRelativeMousePosition(e);
if (newStart) {
} else {
newStart = pos;
newEnd = pos;
}
});
canvas.addEventListener('mousemove', (e) => {
if (newStart) {
newEnd = getTransformRelativeMousePosition(e);
draw();
}
});
canvas.addEventListener('mouseup', (e) => {
if (newStart) {
lines.push([newStart, newEnd]);
newStart = undefined;
}
});
function draw() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.save();
setZoomAndOffsetTransform();
ctx.setTransform(
originTransform.a,
originTransform.b,
originTransform.c,
originTransform.d,
originTransform.e,
originTransform.f);
ctx.beginPath();
for (const line of lines) {
ctx.moveTo(...line[0]);
ctx.lineTo(...line[1]);
}
if (newStart) {
ctx.moveTo(...newStart);
ctx.lineTo(...newEnd);
}
ctx.stroke();
ctx.restore();
}
function addSlider(obj, prop, parent, min, max, callback) {
const valueRange = max - min;
const sliderRange = 100;
const div = document.createElement('div');
div.class = 'range';
const input = document.createElement('input');
input.type = 'range';
input.min = 0;
input.max = sliderRange;
const label = document.createElement('span');
label.textContent = `${prop}: `;
const valueElem = document.createElement('span');
function setInputValue(v) {
input.value = (v - min) * sliderRange / valueRange;
}
input.addEventListener('input', (e) => {
const v = parseFloat(input.value) * valueRange / sliderRange + min;
valueElem.textContent = v.toFixed(1);
obj[prop] = v;
callback();
});
const v = obj[prop];
valueElem.textContent = v.toFixed(1);
setInputValue(v);
div.appendChild(input);
div.appendChild(label);
div.appendChild(valueElem);
parent.appendChild(div);
}
canvas { border: 1px solid black; }
#app { display: flex; }
<div id="app"><canvas></canvas><div id="ui"></div>
Note: I didn't bother making zoom always zoom from the center. To do so would require adjusting xoffset and yoffset as the zoom changes.
Use HTMLElement.prototype.getBoundingClientRect() to get displayed size and position of canvas in DOM. From the displayed size and origin size, calculates the scale of the canvas.
Example:
canvas.addEventListener("click", function (event) {
var b = canvas.getBoundingClientRect();
var scale = canvas.width / parseFloat(b.width);
var x = (event.clientX - b.left) * scale;
var y = (event.clientY - b.top) * scale;
// Marks mouse position
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.stroke();
});

ThreeJS Highlight/Projector

How would I go about adding a 2d sprite under a 3d object on top of a plane to show that my object(s) are highlighted/selected?
Also, how would I do this on uneven terrain?
I'm attaching a sample image to better explain my question:
I'm not sure that using a sprite is a good idea.
As you said about uneven terrain, it's better to use something not flat. For example, a sphere or a cylinder with a material with .alphaMap.
We need something ring-like for our effect of selection.
Let's suppose, you chose sphere, then you can set its material's alphaMap from a file or a dynamically created texture from canvas:
// alpha texture
var canvas = document.createElement("canvas");
canvas.width = 128;
canvas.height = 128;
var ctx = canvas.getContext("2d");
var gradient = ctx.createLinearGradient(0, 0, 0, 128);
gradient.addColorStop(0.35, "black");
gradient.addColorStop(0.475, "white");
gradient.addColorStop(0.525, "white");
gradient.addColorStop(0.65, "black");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 128, 128);
var alphaTexture = new THREE.Texture(canvas);
alphaTexture.needsUpdate = true;
About .alphaMap:
The alpha map is a grayscale texture that controls the opacity across the surface (black: fully transparent; white: fully opaque). Default is null.
Only the color of the texture is used, ignoring the alpha channel if one exists. For RGB and RGBA textures, the WebGL renderer will use the green channel when sampling this texture due to the extra bit of precision provided for green in DXT-compressed and uncompressed RGB 565 formats. Luminance-only and luminance/alpha textures will also still work as expected.
That's why we have there black and white only.
Let's create an object of the simpliest NPC with a yellow ring which indicating that our NPC is selected:
var npc = function() {
var geom = new THREE.SphereGeometry(4, 4, 2);
geom.translate(0, 4, 0);
var mesh = new THREE.Mesh(geom, new THREE.MeshLambertMaterial({
color: Math.random() * 0xffffff
}));
// highlighter
geom.computeBoundingSphere();
var sphereGeom = new THREE.SphereGeometry(geom.boundingSphere.radius, 32, 24);
var sphereMat = new THREE.MeshBasicMaterial({
color: "yellow", // yellow ring
transparent: true, // to make our alphaMap work, we have to set this parameter to `true`
alphaMap: alphaTexture
});
var sphere = new THREE.Mesh(sphereGeom, sphereMat);
sphere.visible = false;
mesh.add(sphere);
mesh.userData.direction = new THREE.Vector3(Math.random() - 0.5, 0, Math.random() - 0.5).normalize();
mesh.userData.speed = Math.random() * 5 + 5;
mesh.position.set(
Math.random() * (worldWidth - 10) - (worldWidth - 10) * 0.5,
10,
Math.random() * (worldDepth - 10) - (worldDepth - 10) * 0.5
);
scene.add(mesh);
return mesh;
}
The rest is not so difficult.
We need an array of our NPCs which we'll check for intersection
var npcs = [];
for (var i = 0; i < 10; i++) {
npcs.push(npc());
}
and then on mousedown event we'll select our an NPC (taken from the interactive cubes example and modified for our needs):
window.addEventListener('mousedown', onMouseDown, false);
function onMouseDown(event) {
if (event.button != 2) return;
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
selector.setFromCamera( mouse, camera );
var intersects = selector.intersectObjects( npcs );
if ( intersects.length > 0 ) {
if ( INTERSECTED != intersects[ 0 ].object ) {
if ( INTERSECTED ) INTERSECTED.children[0].visible = INTERSECTED.selected;
INTERSECTED = intersects[ 0 ].object;
INTERSECTED.selected = INTERSECTED.children[0].visible;
INTERSECTED.children[0].visible = true;
}
} else {
if ( INTERSECTED ) INTERSECTED.children[0].visible = INTERSECTED.selected;
INTERSECTED = null;
}
}
jsfiddle example. Here you can select objects with the right mouse button.

raphael viewbox animated zoom

I'm build a kind of javascript map with javascript and the Raphael lib.
I'm able to zoom on an object when clicked, but I want it to be animated (like slowly diving in and so on). Is there a way to do so without reinventing the wheel?
There is no reason that the viewbox of an svg object cannot be animated -- Raphael simply doesn't provide such functionality out of the box. Creating a plugin is reasonably straightforward, however. For instance:
Raphael.fn.animateViewBox = function animateViewBox( x, y, w, h, duration, easing_function, callback )
{
var cx = this._viewBox ? this._viewBox[0] : 0,
dx = x - cx,
cy = this._viewBox ? this._viewBox[1] : 0,
dy = y - cy,
cw = this._viewBox ? this._viewBox[2] : this.width,
dw = w - cw,
ch = this._viewBox ? this._viewBox[3] : this.height,
dh = h - ch,
self = this;;
easing_function = easing_function || "linear";
var interval = 25;
var steps = duration / interval;
var current_step = 0;
var easing_formula = Raphael.easing_formulas[easing_function];
var intervalID = setInterval( function()
{
var ratio = current_step / steps;
self.setViewBox( cx + dx * easing_formula( ratio ),
cy + dy * easing_formula( ratio ),
cw + dw * easing_formula( ratio ),
ch + dh * easing_formula( ratio ), false );
if ( current_step++ >= steps )
{
clearInterval( intervalID );
callback && callback();
}
}, interval );
}
Any paper instantiated after this plugin is installed can use animateViewBox in exactly the same method Raphael's built-in animate method works. For instance...
var paper = Raphael( 0, 0, 640, 480 );
paper.animateViewBox( 100, 100, 320, 240, 5000, '<>', function()
{
alert("View box updated! What's next?" );
} );
Demonstration staged here.
Raphael animations work by animating element attributes. When you call element.animate, you provide the final object parameters, the time it takes to get there and possibly an easing function if you don't want it to be linear.
For example, to scale up/down an circle you might consider this example: http://jsfiddle.net/eUfCg/
// Creates canvas 320 × 200 at 10, 50
var paper = Raphael(10, 50, 320, 200);
// Creates circle at x = 50, y = 40, with radius 10
var circle = paper.circle(50, 40, 10);
// Sets the fill attribute of the circle to red (#f00)
circle.attr("fill", "#f00");
// Sets the stroke attribute of the circle to white
circle.attr("stroke", "#fff");
var zoomed = false;
circle.click(function () {
if (zoomed) {
this.animate({ transform: "s1" }, 500);
zoomed = false;
} else {
this.animate({ transform: "s4" }, 500);
zoomed = true;
}
});​
Which animates the transform property of the circle. To scale your map you should put all of the elements inside a group, and animate the transform property of the group, considering the scale and translation that you want to end up with.
See http://raphaeljs.com/reference.html#Element.transform for more information on the transform property.

Categories