Hey guys i was just going through this three.js example HERE and after playing around with the example for sometime , i saw the below function:
// NOTE :: the below function, helps in positioning the 5 static red dots
function updateHUDSprites () {
var width = window.innerWidth / 2;
var height = window.innerHeight / 2;
var material = spriteTL.material;
var imageWidth = material.map.image.width / 2;
var imageHeight = material.map.image.height / 2;
// NOTE :: the below lines, helps in positioning the 5 static red dots
spriteTL.position.set( - width + imageWidth, height - imageHeight, 1 ); // top left
spriteTR.position.set( width - imageWidth, height - imageHeight, 1 ); // top right
spriteBL.position.set( - width + imageWidth, - height + imageHeight, 1 ); // bottom left
spriteBR.position.set( width - imageWidth, - height + imageHeight, 1 ); // bottom right
spriteC.position.set( 0, 0, 1 ); // center
}
This function is basically helping position all the 5 pngs , i have played around with the above code quite a bit , but i still don't understand how the centering works , for example the below line:
spriteC.position.set( 0, 0, 1 ); // center
By playing around with the example a bit , i realized that the above line of code was actually positioning the center image , i just don't understand the values being passed to the position.set() function. if somebody could just explain how does spriteC.position.set( 0, 0, 1 ); actually center the image , it would be great.
it depends on how camera is positioned
sprites are rendered using the orthographic camera renderer.render( sceneOrtho, cameraOrtho ); and it has position (0,0,10), looking at the origin (0,0,0)
anything at the origin or along a line parallel with z axis (our sprite with (0,0,1)) will appear to be in the center, negative x coordinate will move the object to the left, positive to the right and same thing with y
Related
I am facing the challenge of figuring out the visible view width and height of a ThreeJS mesh object in pixel units.
In the screenshot below you can see objects floating in 3D space, on mouse click I need to be able to figure out what view width and height they are occupying in pixels
As I am rather new to ThreeJS it is taking me rather long to find a solution, so I would welcome any kind of assistance.
The below function shows what kind of approaches I have been trying.
getObjectSizeInViewSpace(object){
const size = new THREE.Vector3()
const box = new THREE.Box3().setFromObject(object).getSize(size)
size.project(this.camera)
let halfWidth = window.innerWidth / 2;
let halfHeight = window.innerHeight / 2;
size.x = (size.x*halfWidth)
size.y = (size.y*halfHeight)
return new THREE.Vector2(size.x,size.y)
}
You're looking for Vector3.project(). This basically takes world-space (3D) coordinates, and uses the camera's viewport to convert into normalized device coordinates, which range from [-1, 1]. For example x: -1 is the left side of the screen, and x: 1 is the right side. So you'll have to take the 4 vectors (top left, top right, bottom left, bottom right)` of your plane to calculate their pixel dimensions in your browser:
// Get 3D positions of top left corner (assuming they're not rotated)
vec3 topLeft = new Vector3(
plane.position.x - planeWidth / 2,
plane.position.y - planeHeight / 2,
plane.positon.z
);
// This converts x, y, z to the [-1, 1] range
topLeft.project(camera);
// This converts from [-1, 1] to [0, windowWidth]
const topLeftX = (1 + topLeft.x) / 2 * window.innerWidth;
const topLeftY = (1 - topLeft.y) / 2 * window.innerHeight;
Notice the topLeftY value is inverted, since -y in 3D space goes +y in pixel coordinates. Do this 4 times (once for each corner), and then you can subtract (right - left) to get the width, and the same for the height.
There is endlessly moving sprite "green block" from top to bottom and it works. Is it possible to show sprite moving like "around" the stage show at the top as much as hide in bottom. I don't know exactly how this effect can be called, but I mean when green block is starting to move down the scene border, then start showing it again at the top. How can it be done and can you, please, show how to do this?
const WIDTH = 500;
const HEIGHT = 500;
const app = new PIXI.Application({
width: WIDTH,
height: HEIGHT,
backgroundColor: 0x000000
});
document.body.appendChild(app.view);
const sprite = PIXI.Sprite.from('https://i.ibb.co/b3Sjn6M/greeenblock.png');
sprite.width = 100;
sprite.height = 100;
// Center
sprite.anchor.set(0.5);
sprite.x = app.screen.width / 2;
sprite.y = app.screen.height / 2;
app.stage.addChild(sprite);
// Listen for animate update
app.ticker.add((delta) => {
// Move from topto bottom
sprite.position.y += delta * 2;
if (sprite.position.y > HEIGHT + sprite.height / 2) {
sprite.position.y = -sprite.height / 2;
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/5.3.3/pixi.min.js"></script>
Solution (with flickering) provided by #Blindman67:
const WIDTH = 500;
const HEIGHT = 500;
const app = new PIXI.Application({
width: WIDTH,
height: HEIGHT,
backgroundColor: 0x000000
});
document.body.appendChild(app.view);
const sprite = PIXI.Sprite.from('https://i.ibb.co/b3Sjn6M/greeenblock.png');
const spriteReverse = PIXI.Sprite.from('https://i.ibb.co/b3Sjn6M/greeenblock.png');
sprite.width = 100;
sprite.height = 100;
spriteReverse.width = 100;
spriteReverse.height = 100;
// Center
sprite.anchor.set(0.5);
sprite.x = app.screen.width / 2;
sprite.y = app.screen.height / 2;
spriteReverse.anchor.set(0.5);
spriteReverse.x = app.screen.width / 2;
spriteReverse.y = app.screen.height / 2;
app.stage.addChild(sprite);
app.stage.addChild(spriteReverse);
let y = 0;
// Euqlidian modulo
const modAbs = (value, modulo) => (value % modulo + modulo) % modulo;
// Listen for animate update
app.ticker.add((delta) => {
// Move from topto bottom
y += delta * 2;
if (y > HEIGHT + sprite.height / 2) {
y = -sprite.height / 2;
}
// use modulo to warp
y = modAbs(y, HEIGHT);
// check if sprite overlaps the screen edge
spriteReverse.visible = false;
if (y + sprite.height > HEIGHT) { // is crossing then
spriteReverse.visible = true;
spriteReverse.position.y = (y - HEIGHT) // ... draw a copy at opposite edge.
}
sprite.position.y = y
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/5.3.3/pixi.min.js"></script>
If I understand you: you have one box which you wish to move in an infinite loop from the top to the bottom. Once it hits the bottom it should start showing at the top.
The easiest way I can think of would be to have two identical boxes.
Both starts at the top and only one moves down. Once it hits the bottom the other box can start moving down.
When the first box is completely off-screen you reset it's position.
And repeat.
% Remainder operator
This can be done using the remainder operator %
For example if the screen is 1000 pixels wide and you have a coordinate of 1500, that is the object has warped around the screen 1.5 times, using the remainder operator 1500 % 1000 = 500.
If only moving in a positive direction then this is all that is needed (apart from popping)
x = x % screenWidth;
// and/or for y
y = y % screenHeight;
Negative space
However there is a problem if the object moves in the other direction as the remainder operation keeps the sign of the number -1500 % 1000 === -500, and even worse if you use Math.abs on the result you still get the wrong value Math.abs(-1200 % 1000) === 200 which should be 800
You can fix this using a slightly more complex function. You can add it to the Math object or use a stand alone function as follows.
const modAbs = (value, modulo) => (value % modulo + modulo) % modulo;
With the above function negative values are correctly moved into positive space.
So if you have a coordinate x, y to make it warp the screen with
x = modAbs(x, screenWidth);
y = modAbs(y, screenHeight);
That seams easy, but unfortunately there are still some problems to overcome.
Popping
Using the above function to warp across the screen does not consider the size of the sprite, and because you are rendering only one copy when the sprite is move across the playfield edge it will not appear at the other side until the coordinate crossed the edge.
This causes the sprite to pop in and or out depending on the direction of movement and the position of the sprites origin.
There are two solutions.
Extend the playfield
If you make the playfield larger than the view (Viewable area) by 2 times the size of the sprite and warp using the larger playfield then the sprite will not warp until it has completely disappeared from view. This prevents the ugly popping in and out when warping and is most suited to NPC type sprites. For player (focused) sprites this is not a good options as the sprite will not be completely visible as it crosses the screen edges.
Render extra copies.
To keep the sprite fully visible at all times you need to render it more than once when it is crossing the screen. Example pseudo code
// use modulo to warp
x = modAbs(x, screenWidth);
// check if sprite overlaps the screen edge
if (x + spriteWidth > screenWidth) { // is crossing then
drawSprite(x - screenWidth, // ... draw a copy at opposite edge.
If you are only warping between top and bottom (or left and right) this is all that is needed.
If you are warping in all directions you will need to render the sprite up to 4 times. Twice when crossing top bottom or left right. 4 times if crossing in a corner.
As your question only indicates up and down warps I assume you don't need the extra code.
I have a canvas which is 500*250 (w*h) and applied this code to it;
var cvs = document.getElementById('Dugong');
var ctx = cvs.getContext('2d');
var cvsHeight = cvs.height; // 250
var cvsWidth = cvs.width; // 500
var loadUpFlappy = "Flappy";
var loadUpDugong = "Dugong";
ctx.fillStyle = "#ECA20F";
ctx.fillRect(5, (cvsHeight / 2) + 1, ctx.measureText(loadUpFlappy).width + 10, 2); // across, down, width, height
ctx.fillRect(cvsWidth - (ctx.measureText(loadUpDugong).width + 5), cvsHeight / 2 + 1, ctx.measureText(loadUpDugong).width + 10, 2);
ctx.fillStyle = "#DADFE1";
ctx.font = "40px Century Schoolbook";
ctx.fillText(loadUpFlappy, 10, cvsHeight / 2);
ctx.fillText(loadUpDugong, cvsWidth - (ctx.measureText(loadUpDugong).width + 10), cvsHeight / 2);
To my knowledge, this code should ouput;
One #ECA20F line which has a height of 2, is 5 away from the left canvas border, is 1 below the canvas middle line and has the width of Flappy plus 10.
Another #ECA20F line which has a height of 2, is 5 away from the right canvas border, is 1 below the canvas middle line and has the width of Dugong plus 10.
AND
The word Flappy in #DADFE1 which is 10 away from the left border of the canvas and is in the middle the canvas.
The word Dugong also in #DADFE1 which is 10 away from the right border of the canvas and is also in the middle of the canvas.
My problem is that the lines are not matching up with the text (they should span the width of the text plus 5px on either side). Instead they do this;
View Image
Sorry if there are any glaringly obvious answers to this issue as I am a 'noob' when it comes to javascript and canvases.
Edit: I thought that it'd not be possible to take the text length. So I'm a bit wrong.
There's no easy way to place the lines because you don't know the font size in the text. What you can do is to align the text in center (ctx.textAlign = 'center') and place it on a certain point which will be its center. Then you set the line width according to the text length and size. Put the line always below to this point and set the same X as the point, subtracting it with the width you got for the line divided by 2. But the line width wouldn't be exact in this case, but it helps smaller words.
I wrote some code to zoom in my image, but when I scroll at the very beginning this picture jumps a little. How to fix the problem?
Full page view.
Editor view.
HTML
<canvas id="canvas"></canvas>
JS
function draw(scroll) {
scroll = (window.scrollY || window.pageYOffset) / (document.body.clientHeight - window.innerHeight) * 3000;
canvas.setAttribute('width', window.innerWidth);
canvas.setAttribute('height', window.innerHeight);
//The main formula that draws and zooms the picture
drawImageProp(ctx, forest, 0, (-scroll * 3.9) / 4, canvas.width, canvas.height + (scroll * 3.9) / 2);
}
Not a bug fix
I had a look at the Codepen example and it does jump at the top (sometimes). I have a fix for you but I did not have the time to locate the source of your code problem. I did notice that the jump involved a aspect change so it must be in the scaling that your error is. (look out for negatives)
GPU is a better clipper
Also your code is actually doing unnecessary work, because you are calculating the image clipping region. Canvas context does the clipping for you and is especially good at clipping images. Even though you provide the clip area the image will still go through clip as that is part of the render pipeline. The only time you should be concerned about the clipped display of an image is whether or not any part of the image is visible so that you don't send a draw call, and it only really matters if you are pushing the image render count (ie game sprite counts 500+)
Code example
Anyway I digress. Below is my code. You can add the checks and balances. (argument vetting, scaling max min, etc).
Calling function.
// get a normalised scale 0-1 from the scroll postion
var scale = (window.scrollY || window.pageYOffset) / (document.body.clientHeight - window.innerHeight);
// call the draw function
// scale 0-1 where 0 is min scale and 1 is max scale (min max determined in function
// X and y offset are clamped but are ranged
// 0 - image.width and 0 - image.height
// where 0,0 shows top left and width,height show bottom right
drawImage(ctx, forest, scale, xOffset, yOffset);
The function.
The comments should cover what you need to know. You will notice that all I am concerned with is how big the image should be and where the top left corner will be. The GPU will do the clipping for you, and will not cost you processing time (even for unaccelerated displays). I personally like to work with normalised values 0-1, it is a little extra work but my brain likes the simplicity, it also reduces the need for magic numbers (magics number are a sign that code is not adaptable) . Function will work for any size display and any size image. Oh and I like divide rather than multiply, (a bad coding habit that comes from a good math habit) replacing the / 2 and needed brackets with * 0.5 will make it more readable.
function drawImage(ctx, img, scale, x, y){
const MAX_SCALE = 4;
const MIN_SCALE = 1;
var w = canvas.width; // set vars just for source clarity
var h = canvas.height;
var iw = img.width;
var ih = img.height;
var fit = Math.max(w / iw, h / ih); // get the scale to fill the avalible display area
// Scale is a normalised value from 0-1 as input arg Convert to range
scale = (MAX_SCALE - MIN_SCALE) * scale + MIN_SCALE;
var idw = iw * fit * scale; // get image total display size;
var idh = ih * fit * scale;
x /= iw; // normalise offsets
y /= ih; //
x = - (idw - w) * x; // transform offsets to display coords
y = - (idh - h) * y;
x = Math.min( 0, Math.max( - (idw - w), x) ); // clamp image to display area
y = Math.min( 0, Math.max( - (idh - h), y) );
// use set transform to scale and translate
ctx.setTransform(scale, 0, 0, scale, idw / 2 + x, idh / 2 + y);
// display the image to fit;
ctx.drawImage(img, ( - iw / 2 ) * fit, (- ih / 2 ) * fit);
// restore transform.
ctx.setTransform(1, 0, 0, 1, 0, 0)
}
Sorry I did not solve the problem directly, but hopefully this will help you redesign your approch.
I recently added a similar answer involving zooming and panning (and rotation) with the mouse which you may be interested in How to pan the canvas? Its a bit messy still "note to self (my clean it up)" and has no bounds clamping. But shows how to set a zoom origin, and convert from screen space to world space. (find where a screen pixel is on a pan/scale/rotated display).
Good luck with your project.
I am using the three.js framework and have the renderer in a div that takes up the right 50% of the screen and has 87.5% height. I am then trying to place a sphere wherever is clicked, however the coordinates that are calculated are not accurate within the div and the spheres appear in various positions away from the mouse. How can I accurately calculate the coordinates within this div? Thanks and here's the code
function onDocumentMouseMove( event ) {
event.preventDefault();
mouseX = (event.clientX / container.offsetWidth) * 2 - 1;
mouseY = -(event.clientY / container.offsetHeight) * 2 - 1;
}
function onDocumentMouseDown(event){
event.preventDefault()
alert("X: " + mouseX + " Y: " + mouseY);
var vector = new THREE.Vector3( mouseX, mouseY, 1 );
var sphere = new THREE.Mesh(new THREE.SphereGeometry(size / 4), new THREE.MeshLambertMaterial(intensity));
sphere.position.x = vector.x;
sphere.position.y = vector.y;
sphere.position.z = vector.z;
scene.add(sphere);
spheres.push(sphere);
}
Ok, so first of all - you have to set properly you camera inside your div:
camera = new THREE.PerspectiveCamera( 45, referenceToYourDiv.width / referenceToYourDiv.height, 1, 1000 );
Then, all you have to do is to calculate your mouse event relative to you div's position.
The thing is that in THREE.js you deal with absolute center of your container to get (0,0) coordinates in 3D space, but in HTML - your (0,0) is located in the corner of your screen. So just "move" that (0,0) point to your container's center and you're good to go!
If you would have fullscreen rendering, than your code would be:
mouseX = event.clientX - windowHalfWidth;
mouseY = event.clientY - windowHalfHeight;
But since you deal with custom size container, try this out:
mouseX = (event.clientX - (window.innerWidth*0.5))-(container.width*0.5);
mouseY = (event.clientY - (window.innerHeight*0.875))-(container.height*0.5);
For window width 1920 and container's width 50% - it will produce mouse values from -480 to +480, regarding to your container's width center.
For window height 1200 and container's height 87.5% - it will produce mouse values from -525 to +525, regarding to your container's height center.
Well, at least it should - I didn't try this code, but that is the basic idea of what you have to do.
Hope that helps.
UPDATE: Here is an example of what you are trying to achieve: http://jsfiddle.net/PwWbT/