Smoothing a line and creating a gradient - html5-canvas

I'm making this for a class and I have managed to create a drawing app in which the stroke reacts to the speed of the mouse and the colour of the stroke changes depending on the angle.
Link here.
It looks really rough though, and I'd like to understand how I could smoothen the lines by connecting the strokes (so it would appear like a single stroke that is changing width) and if it would be possible to create a gradient, going from red to green. I tried to get some help from the teacher but we have approximately 10 minutes a week dedicated to getting individual assistance for our projects and it's quite hard to ask all these question and understand what is happening in the code...
Please note that I had a lot of help doing this. I previously did something similar using paper.js, but my teacher prefers me to use "pure" canvas for this. I have a background in webdesign, but it's far from programming, I just know markup language and working with html and css, occasionally using some jquery slider. So I'm entirely confused by even the simplest tutorials, I tried to follow this but I don't even understand WHERE to put everything and it just didn't work.
I'd be really glad if someone could give me some help with this... ELI5, please. I'm a fast learner but I'm still in confusion mode, overwhelmed by all these lines of code I don't (yet) understand, but would really like to.
Thank you in advance!

Several hints:
Set context.lineCap='round'. This rounds the beginning & end of each line. That rounding helps visually merge one line into the next line.
Limit your lineWidth to a small range. This makes your line visually flow better because there isn't a huge jump in size when the user makes rapid speed changes.
Here's refactored code and a Demo:
// cache a reference to the canvas element & its context
// because they are used often
var canvas=$('#canvas')[0];
var context = canvas.getContext('2d');
//full-screen
canvas.width = 500;
canvas.height = 500;
// style lines with rounded end-caps & rounded joins
context.lineJoin='round';
context.lineCap='round';
var mouseX = undefined;
var mouseY = undefined;
var mouseIsDown = false;
var PI2=Math.PI*2;
var speed=-1000;
(function(){Math.clamp=function(a,b,c){return Math.max(b,Math.min(c,a));}})();
onMouseDown = function( event ){
// tell the browser we're handling this event
event.preventDefault();
event.stopPropagation();
mouseIsDown = true;
}
onMouseMove = function( event ){
// tell the browser we're handling this event
event.preventDefault();
event.stopPropagation();
var previousMouseX = mouseX;
var previousMouseY = mouseY;
mouseX = event.pageX;
mouseY = event.pageY;
var dx = Math.abs( mouseX - previousMouseX );
var dy = Math.abs( mouseY - previousMouseY );
var speed = Math.sqrt( dx * dx + dy * dy );
// limit the min/max width of the line
speed=Math.clamp(speed,2,12);
var angle = 2 * Math.PI * Math.acos( dx / ( speed + 0.00001 ) );
if( angle < Math.PI ){
context.strokeStyle = 'rgb( 0, 255, 0 )';
}
else{
context.strokeStyle = 'rgb( 255, 0, 0 )';
}
if( mouseIsDown ){
context.lineWidth=speed;
context.beginPath();
context.moveTo( previousMouseX, previousMouseY );
context.lineTo( mouseX, mouseY );
context.stroke();
}
}
//drawing only is the mouse is down
onMouseUp = function( event ){
// tell the browser we're handling this event
event.preventDefault();
event.stopPropagation();
mouseIsDown = false;
}
$( '#canvas' ).on( 'mousedown', onMouseDown );
$( '#canvas' ).on( 'mousemove', onMouseMove );
$( '#canvas' ).on( 'mouseup', onMouseUp );
$( '#canvas' ).on( 'mouseout', onMouseUp );
canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<canvas id="canvas" width=500 height=500></canvas>
Applying a gradient is much harder. You will have to:
Save each mouse point
Interpolate points between each mouse point so that each new point is 1px distant from the previous.
Clear the canvas
Redraw a line between each mouse point with each line's strokeStyle changing according to your desired gradient.

Related

Rotate webGL canvas to appear landscape-oriented on a portrait-oriented mobile phone

I’m using a-frame and trying to accomplish this task - force the canvas to be rendered as “landscape” when a mobile device is in portrait orientation (ie. device-width = 414px and device-height = 736px).
I have successfully accomplished this with the following steps
camera.aspect = 736 / 414;
camera.updateProjectionMatrix();
renderer.setSize(736, 414);
In css...
.a-canvas {
transform: rotate(90deg) translate(161px, 161px);
height: 414px !important;
width: 736px !important;
}
This all works great except for one major thing…I have 3D buttons in my scene and when I go to click them they don’t line up with the rotated canvas, instead their clickable position remains in the same place as before the canvas was rotated.
I’ve tried to set matrixWorldNeedsUpdate = true on the scene’s object3D along with updateWorldMatrix() with no luck. I tried calling refreshObjects on the raycaster with no luck. I tried rotating the scene and the camera with no luck.
I’m not sure what else to do. Any help would be greatly appreciated!
ANSWER:
Thanks to Marquizzo and gman for the help. Here's the updated a-frame source code (v1.0.4) to make the raycaster handle this forced landscape canvas properly
// line: 66884
onMouseMove: (function () {
var direction = new THREE.Vector3();
var mouse = new THREE.Vector2();
var origin = new THREE.Vector3();
var rayCasterConfig = {origin: origin, direction: direction};
return function (evt) {
var bounds = this.canvasBounds;
var camera = this.el.sceneEl.camera;
var left;
var point;
var top;
camera.parent.updateMatrixWorld();
// Calculate mouse position based on the canvas element
if (evt.type === 'touchmove' || evt.type === 'touchstart') {
// Track the first touch for simplicity.
point = evt.touches.item(0);
} else {
point = evt;
}
left = point.clientX - bounds.left;
top = point.clientY - bounds.top;
// mouse.x = (left / bounds.width) * 2 - 1;
// mouse.y = -(top / bounds.height) * 2 + 1;
// HAYDEN's CODE: flipping x and y coordinates to force landscape
// --------------------------------------------------------------
let clickX = (left / bounds.width) * 2 - 1;
let clickY = - (top / bounds.height) * 2 + 1;
mouse.x = -clickY;
mouse.y = clickX;
// --------------------------------------------------------------
origin.setFromMatrixPosition(camera.matrixWorld);
direction.set(mouse.x, mouse.y, 0.5).unproject(camera).sub(origin).normalize();
this.el.setAttribute('raycaster', rayCasterConfig);
if (evt.type === 'touchmove') { evt.preventDefault(); }
};
})(),
A-Frame uses a Raycaster internally to determine if the spot you clicked has hit an object. You can see in the Three.js documentation the raycaster needs the mouse x&y coordinates to determine where you clicked. Here's a working demo of that concept. However with your setup, x, y turns into -y, x.
I think you'll have to write your own Raycaster function to trigger on the click event instead of relying on the built-in AFrame functionality, and then swap the x&y values:
function onClick() {
let clickX = ( event.clientX / window.innerWidth ) * 2 - 1;
let clickY = - ( event.clientY / window.innerHeight ) * 2 + 1;
mouse.x = -clickY;
mouse.y = clickX;
// Then continue with raycaster.setFromCamera(), etc...
}
window.addEventListener( 'click', onClick, false );

Rotate an object like OrbitControls but only the object itself

We love three.js! And here is a page we built using it a few years ago.
https://www.jgprolock.com
We are in the process of revising the animations on this site.
Once the page loads, the user has the ability to drag and rotate the object. But it is really a trick. We are using orbit controls to rotate the camera around our scene, and thus our main object which is centered in the scene (positions x,y,z all equal to 0). If we did not place the object in the center, it starts to look uneven in its rotation as the camera now is rotating around a center that the object doesn't have.
In order to make it look like the object is on the left side, we ended up moving the canvas to the left and then we bring it back to the right or left as the animation continues after scrolling.
So, my question is .. does anyone have an example how to achieve this functionality just by rotating the actual object itself, instead of rotating the camera around the entire scene using the orbit controls plugin?
Or is there away to modify the orbit controls to rotate around an object and not the entire scene?
I've been searching for this for a while but right after asking this question I came across this link, which actually has an example of what we are trying to do.
https://jsfiddle.net/n6u6asza/1205/
The key to making this work as copied from the link: (although I am not 100% sure what this all means)
/* */
var isDragging = false;
var previousMousePosition = {
x: 0,
y: 0
};
$(renderer.domElement).on('mousedown', function(e) {
isDragging = true;
})
.on('mousemove', function(e) {
//console.log(e);
var deltaMove = {
x: e.offsetX-previousMousePosition.x,
y: e.offsetY-previousMousePosition.y
};
if(isDragging) {
var deltaRotationQuaternion = new three.Quaternion()
.setFromEuler(new three.Euler(
toRadians(deltaMove.y * 1),
toRadians(deltaMove.x * 1),
0,
'XYZ'
));
cube.quaternion.multiplyQuaternions(deltaRotationQuaternion, cube.quaternion);
}
previousMousePosition = {
x: e.offsetX,
y: e.offsetY
};
});
/* */
If you want an article on how to achieve this without the use of unnecessary jquery dependencies you can have a look here
This uses the eventListener to find a mousemove event whilst a mousedown event is occurring, and then passes the coordinates to a custom function.
var mouseDown = false,
mouseX = 0,
mouseY = 0;
var canvas = renderer.domElement
canvas.addEventListener('mousemove', function (evt) {
if (!mouseDown) {return}
//console.log('drag')
evt.preventDefault();
var deltaX = evt.clientX - mouseX,
deltaY = evt.clientY - mouseY;
mouseX = evt.clientX;
mouseY = evt.clientY;
// DO SOMETHING HERE WITH X and Y
object.rotation.x += deltaX
}, false);
canvas.addEventListener('mousedown', function (evt) {
evt.preventDefault();
mouseDown = true;
mouseX = evt.clientX;
mouseY = evt.clientY;
}, false);
canvas.addEventListener('mouseup', function (evt) {
evt.preventDefault();
mouseDown = false;
}, false);
}
But not that this will not work if you have OrbitControls or DragControls imported!

Clickable points on Three.js globe

I have looked into using three.js but could not find any examples specific to what I need.
I am trying to create a 3D Map (on a globe) that has 3 different pin points around the world. I'd like to be able to detect which pinpoint was clicked on, in order to display information elsewhere on the page.
Is anyone aware of a way to do this? Any demos or tutorials would be very helpful.
Thank you.
Use an event listener to fire off a raycaster on mouse click. I quickly cut and pasted some code from one of my projects so that it does a raycast from the mouse in screenspace to an object in 3D space and updates the color of the material to red.
This should at least get you started in the right direction:
var mouse = new THREE.Vector2(0,0);
var raygun = new THREE.Raycaster();
var useRaycast = true;
// Raycast when we click the mouse
function onClick() {
// Get mouse position in screen space
mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1;
mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1;
// Only raycast if not panning (optimization)
var hits;
if (useRaycast) {
raygun.setFromCamera(mouse, camera);
// Raycast to single object
hits = raygun.intersectObject(myTargetObect, false);
// Raycast to multiple objects
// hits = raygun.intersectObjects([myTargetObect, myTargetObect2]);
}
// Run if we have intersections
if (hits.length > 0) {
for (var i = 0; i < hits.length; i++ ) {
// Change material color of item we clicked on
hits[i].object.material.color.set(0xff0000);
}
renderer.render(scene, camera);
}
}
window.addEventListener('click', onClick, false);
MrDoob also has an example on github with a web demo.

Tween camera target in three.js

I have this code that works well:
function onMouseMove( event ) {
window.onmousedown = function() {
var canvasPosition = renderer.domElement.getBoundingClientRect();
var mouseX = event.clientX - canvasPosition.left;
var mouseY = event.clientY - canvasPosition.top;
var mouseVector = new THREE.Vector3 (
2 * (mouseX / window.innerWidth) - 1,
1 - 2 * (mouseY / window.innerHeight), 1);
mouseVector.unproject( camera );
var dir = mouseVector.sub( camera.position ).normalize();
var distance = - camera.position.z / dir.z;
var pos = camera.position.clone().add( dir.multiplyScalar( distance ) );
camera.getWorldDirection();
camera.lookAt( pos );
// camera.updateMatrixWorld(true);
console.log(mouseVector);
console.log(mouseX);
console.log(mouseY);
// render();
}
}
But I would like to smooth the movement. So I found the following code from the tween example, but not sure how to use it. In the above code, I get current camera lookat from one place, one format, and put the new camera look at in camera.lookat in a different format - neither of which seem to be standard x,y,z.
In the below code, the tween would have me change an properties (x,y,z) on a single item. which the unprojecting and normalizing of the camera do not accommodate:
new TWEEN.Tween( intersects[ 0 ].object.position )
.to( {
x: Math.random() * 800 - 400,
y: Math.random() * 800 - 400,
z: Math.random() * 800 - 400
}, 2000 )
.easing( TWEEN.Easing.Elastic.Out)
.start();
If there is a breakdown or something I can read, or actually work out problems to understand, I'd be grateful. I've read camera tutorials and matrix tutorials over and over for years, but my brain just can't comprehend it.
I've been digging around here quite a bit, but nothing addresses a camera tween - at least for a valid version of threejs
Thank you!
I recommend you get acquainted with linear interpolation, or more commonly known as "lerp". The THREE.Vector3 class has a lerp function that you could use to interpolate between a starting point and an ending point:
var camPos = new THREE.Vector3(0, 0, 0); // Holds current camera position
var targetPos = new THREE.Vector3(10, 10, -10);// Target position
var origin = new THREE.Vector3(0, 0, 0); // Optional origin
function animate(){
// Interpolate camPos toward targetPos
camPos.lerp(targetPos, 0.05);
// Apply new camPos to your camera
camera.position.copy(camPos);
// (Optional) have camera look at the origin after it's been moved
camera.lookAt(origin);
// render();
}
In the above example, your animate() function is called once per frame, and the camera will travel 5% towards targetPos per frame.
If you change targetPos, the camera will animate towards its new target value.
I recommend you first get acquainted with lerping before you start bringing in third-party libraries like TWEEN.js or others.
just for smoothing the movement, this might already help you:
// keep this outside of the event-handler
var lookAtPosition = new THREE.Vector3();
var lookAtTween = new TWEEN.Tween(lookAtPosition);
// as lookAt is not a property we can assign to we need to
// call it every time the tween was updated:
lookAtTween.onUpdate(function() {
camera.lookAt(lookAtPosition);
});
window.onmousedown = function() {
// do your thing to compute pos
// instead of `camera.lookAt(pos)`, do this:
lookAtTween
.stop() // just in case it's still animating
.to(pos, 500) // set destination and duration
.start(); // start the tween
};

zooming based on the position of cursor in mousewheel

I am currently working on a THREE.js project.While using trackball control zooming it is zooming relative to the center of the model.But i want to zoom according to the position of cursor with mouse wheel.Any one who is familiar please help me over this.
Thank you in advance.
In mousewheel add this code which helps you in zooming based on the cursor position and not relative to the model.
If you are using trackball controls,you could use that for panning and rotating.
So set trackball controls enabled to false in mousewheel.
renderer.domElement.addEventListener('mousewheel',
function (e) {
mousewheel(e);
},
false);
function mousewheel(event) {
trackBallControls.noZoom = true;
event.preventDefault();
var factor = 50;
var mX = (event.clientX / jQuery(container).width()) * 2 - 1;
var mY = -(event.clientY / jQuery(container).height()) * 2 + 1;
var vector = new THREE.Vector3(mX, mY, 0.5);
//console.log(vector);
vector.unproject(camera);
vector.sub(camera.position);
if (event.deltaY < 0) {
camera.position.addVectors(camera.position,
vector.setLength(factor));
trackBallControls.target.addVectors(trackBallControls.target,
vector.setLength(factor));
camera.updateProjectionMatrix();
} else {
camera.position.subVectors(camera.position,
vector.setLength(factor));
trackBallControls.target.subVectors(trackBallControls.target,
vector.setLength(factor));
camera.updateProjectionMatrix();
}
}

Resources