ThreeJS: Tweaking projection / raycasting - math
there is a very rough ThreeJS sketch with a cube at the Vector3(0.0, 0.0, 0.0) rotated with one edge to a viewer. Code gets some screen points from left/right edges, transforms them to 3D world coordinates and transpose further for their projections on the cube. By now I have set them by hand, but it could be done with THREE.Raycaster and the result is the same.
let m0 = new THREE.Vector3(0.0, edges.wtl.y, 100.0);
let m1 = new THREE.Vector3(0.0, edges.wtl.y, -100.0);
let ray0 = new THREE.Raycaster();
let dir = m1.clone().sub(m0.clone()).normalize();
ray0.set(m0, dir);
The initial setup looks fine, but if you rotate scene with OrbitControls you would notice that straight white lines don't match with red ones. Despite the fact that the red lines are built correctly based on the camera FOV distortion I need to tweak red dots in a way illustrated below.
Any ideas? Maybe I need to find screen coordinates for cube left/right edges and find its intersections with whose I am using just in the beginning of calculateEdges() and transform them back to world ones? It's a very clumsy solution and could be use as a last resort only.
THREE.OrbitControls = function ( object, domElement ) {
this.object = object;
this.domElement = ( domElement !== undefined ) ? domElement : document;
// API
this.enabled = true;
this.center = new THREE.Vector3();
this.userZoom = true;
this.userZoomSpeed = 1.0;
this.userRotate = true;
this.userRotateSpeed = 1.0;
this.userPan = true;
this.userPanSpeed = 2.0;
this.autoRotate = false;
this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60
this.minPolarAngle = 0; // radians
this.maxPolarAngle = Math.PI; // radians
this.minDistance = 0;
this.maxDistance = Infinity;
// 65 /*A*/, 83 /*S*/, 68 /*D*/
this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40, ROTATE: 65, ZOOM: 83, PAN: 68 };
// internals
var scope = this;
var EPS = 0.000001;
var PIXELS_PER_ROUND = 1800;
var rotateStart = new THREE.Vector2();
var rotateEnd = new THREE.Vector2();
var rotateDelta = new THREE.Vector2();
var zoomStart = new THREE.Vector2();
var zoomEnd = new THREE.Vector2();
var zoomDelta = new THREE.Vector2();
var phiDelta = 0;
var thetaDelta = 0;
var scale = 1;
var lastPosition = new THREE.Vector3();
var STATE = { NONE: -1, ROTATE: 0, ZOOM: 1, PAN: 2 };
var state = STATE.NONE;
// events
var changeEvent = { type: 'change' };
this.rotateLeft = function ( angle ) {
if ( angle === undefined ) {
angle = getAutoRotationAngle();
}
thetaDelta -= angle;
};
this.rotateRight = function ( angle ) {
if ( angle === undefined ) {
angle = getAutoRotationAngle();
}
thetaDelta += angle;
};
this.rotateUp = function ( angle ) {
if ( angle === undefined ) {
angle = getAutoRotationAngle();
}
phiDelta -= angle;
};
this.rotateDown = function ( angle ) {
if ( angle === undefined ) {
angle = getAutoRotationAngle();
}
phiDelta += angle;
};
this.zoomIn = function ( zoomScale ) {
if ( zoomScale === undefined ) {
zoomScale = getZoomScale();
}
scale /= zoomScale;
};
this.zoomOut = function ( zoomScale ) {
if ( zoomScale === undefined ) {
zoomScale = getZoomScale();
}
scale *= zoomScale;
};
this.pan = function ( distance ) {
distance.transformDirection( this.object.matrix );
distance.multiplyScalar( scope.userPanSpeed );
this.object.position.add( distance );
this.center.add( distance );
};
this.update = function () {
var position = this.object.position;
var offset = position.clone().sub( this.center );
// angle from z-axis around y-axis
var theta = Math.atan2( offset.x, offset.z );
// angle from y-axis
var phi = Math.atan2( Math.sqrt( offset.x * offset.x + offset.z * offset.z ), offset.y );
if ( this.autoRotate ) {
this.rotateLeft( getAutoRotationAngle() );
}
theta += thetaDelta;
phi += phiDelta;
// restrict phi to be between desired limits
phi = Math.max( this.minPolarAngle, Math.min( this.maxPolarAngle, phi ) );
// restrict phi to be betwee EPS and PI-EPS
phi = Math.max( EPS, Math.min( Math.PI - EPS, phi ) );
var radius = offset.length() * scale;
// restrict radius to be between desired limits
radius = Math.max( this.minDistance, Math.min( this.maxDistance, radius ) );
offset.x = radius * Math.sin( phi ) * Math.sin( theta );
offset.y = radius * Math.cos( phi );
offset.z = radius * Math.sin( phi ) * Math.cos( theta );
position.copy( this.center ).add( offset );
this.object.lookAt( this.center );
thetaDelta = 0;
phiDelta = 0;
scale = 1;
if ( lastPosition.distanceTo( this.object.position ) > 0 ) {
this.dispatchEvent( changeEvent );
lastPosition.copy( this.object.position );
}
};
function getAutoRotationAngle() {
return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed;
}
function getZoomScale() {
return Math.pow( 0.95, scope.userZoomSpeed );
}
function onMouseDown( event ) {
if ( scope.enabled === false ) return;
if ( scope.userRotate === false ) return;
event.preventDefault();
if ( state === STATE.NONE )
{
if ( event.button === 0 )
state = STATE.ROTATE;
if ( event.button === 1 )
state = STATE.ZOOM;
if ( event.button === 2 )
state = STATE.PAN;
}
if ( state === STATE.ROTATE ) {
//state = STATE.ROTATE;
rotateStart.set( event.clientX, event.clientY );
} else if ( state === STATE.ZOOM ) {
//state = STATE.ZOOM;
zoomStart.set( event.clientX, event.clientY );
} else if ( state === STATE.PAN ) {
//state = STATE.PAN;
}
document.addEventListener( 'mousemove', onMouseMove, false );
document.addEventListener( 'mouseup', onMouseUp, false );
}
function onMouseMove( event ) {
if ( scope.enabled === false ) return;
event.preventDefault();
if ( state === STATE.ROTATE ) {
rotateEnd.set( event.clientX, event.clientY );
rotateDelta.subVectors( rotateEnd, rotateStart );
scope.rotateLeft( 2 * Math.PI * rotateDelta.x / PIXELS_PER_ROUND * scope.userRotateSpeed );
scope.rotateUp( 2 * Math.PI * rotateDelta.y / PIXELS_PER_ROUND * scope.userRotateSpeed );
rotateStart.copy( rotateEnd );
} else if ( state === STATE.ZOOM ) {
zoomEnd.set( event.clientX, event.clientY );
zoomDelta.subVectors( zoomEnd, zoomStart );
if ( zoomDelta.y > 0 ) {
scope.zoomIn();
} else {
scope.zoomOut();
}
zoomStart.copy( zoomEnd );
} else if ( state === STATE.PAN ) {
var movementX = event.movementX || event.mozMovementX || event.webkitMovementX || 0;
var movementY = event.movementY || event.mozMovementY || event.webkitMovementY || 0;
scope.pan( new THREE.Vector3( - movementX, movementY, 0 ) );
}
}
function onMouseUp( event ) {
if ( scope.enabled === false ) return;
if ( scope.userRotate === false ) return;
document.removeEventListener( 'mousemove', onMouseMove, false );
document.removeEventListener( 'mouseup', onMouseUp, false );
state = STATE.NONE;
}
function onMouseWheel( event ) {
if ( scope.enabled === false ) return;
if ( scope.userZoom === false ) return;
var delta = 0;
if ( event.wheelDelta ) { // WebKit / Opera / Explorer 9
delta = event.wheelDelta;
} else if ( event.detail ) { // Firefox
delta = - event.detail;
}
if ( delta > 0 ) {
scope.zoomOut();
} else {
scope.zoomIn();
}
}
function onKeyDown( event ) {
if ( scope.enabled === false ) return;
if ( scope.userPan === false ) return;
switch ( event.keyCode ) {
/*case scope.keys.UP:
scope.pan( new THREE.Vector3( 0, 1, 0 ) );
break;
case scope.keys.BOTTOM:
scope.pan( new THREE.Vector3( 0, - 1, 0 ) );
break;
case scope.keys.LEFT:
scope.pan( new THREE.Vector3( - 1, 0, 0 ) );
break;
case scope.keys.RIGHT:
scope.pan( new THREE.Vector3( 1, 0, 0 ) );
break;
*/
case scope.keys.ROTATE:
state = STATE.ROTATE;
break;
case scope.keys.ZOOM:
state = STATE.ZOOM;
break;
case scope.keys.PAN:
state = STATE.PAN;
break;
}
}
function onKeyUp( event ) {
switch ( event.keyCode ) {
case scope.keys.ROTATE:
case scope.keys.ZOOM:
case scope.keys.PAN:
state = STATE.NONE;
break;
}
}
this.domElement.addEventListener( 'contextmenu', function ( event ) { event.preventDefault(); }, false );
this.domElement.addEventListener( 'mousedown', onMouseDown, false );
this.domElement.addEventListener( 'mousewheel', onMouseWheel, false );
this.domElement.addEventListener( 'DOMMouseScroll', onMouseWheel, false ); // firefox
window.addEventListener( 'keydown', onKeyDown, false );
window.addEventListener( 'keyup', onKeyUp, false );
};
THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype );
let camera, scene, renderer, raycaster, controls, edges = {}, line0, line1, plane;
let windowHalfX = window.innerWidth / 2;
let windowHalfY = window.innerHeight / 2;
init();
animate();
function init() {
const container = document.createElement('div');
document.body.appendChild(container);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 1, 1024);
camera.position.x = 0;
camera.position.y = 0;
camera.position.z = 64;
scene = new THREE.Scene();
edges.tl = new THREE.Vector3(0.0, 0.0, 0.0);
edges.tr = new THREE.Vector3(0.0, 0.0, 0.0);
edges.bl = new THREE.Vector3(0.0, 0.0, 0.0);
edges.br = new THREE.Vector3(0.0, 0.0, 0.0);
edges.wtl = new THREE.Vector3(0.0, 0.0, 0.0);
edges.wtr = new THREE.Vector3(0.0, 0.0, 0.0);
edges.wbl = new THREE.Vector3(0.0, 0.0, 0.0);
edges.wbr = new THREE.Vector3(0.0, 0.0, 0.0);
edges.width = new THREE.Vector3(0.0, 0.0, 0.0);
edges.wwidth = new THREE.Vector3(0.0, 0.0, 0.0);
const ambientLight = new THREE.AmbientLight(0xCCCCCC, 0.4);
scene.add(ambientLight);
const pointLight = new THREE.PointLight(0xFFFFFF, 0.8);
camera.add(pointLight);
scene.add(camera);
renderer = new THREE.WebGLRenderer();
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
container.appendChild(renderer.domElement);
controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.minPolarAngle = Math.PI / 2.0 -0.15;
controls.maxPolarAngle = Math.PI / 2.0 + 0.15;
controls.minAzimuthAngle = -0.15;
controls.maxAzimuthAngle = 0.15;
controls.minDistance = 42.0; //.75;
controls.maxDistance = 69.0;
//cube
let geometry = new THREE.BoxGeometry(32, 32, 32);
let material = new THREE.MeshPhongMaterial( {color: 0x00FFFF} );
const cube = new THREE.Mesh(geometry, material);
cube.rotation.set(0.0, -Math.PI / 4.0, 0.0);
cube.name = "cube";
cube.updateMatrixWorld();
scene.add(cube);
//window.addEventListener('resize', onWindowResize);
}
function animate() {
requestAnimationFrame(animate);
render();
}
function render() {
controls.update();
calculateEdges()
renderer.render(scene, camera);
}
function calculateEdges(){
let toRemove = ["line0", "line1", "topLine", "bottomLine", "frame", "pointTM", "pointBM", "point00", "point01", "point10", "point11", "point20","point21", "point30", "point31", "point40", "point41"];
toRemove.forEach((name_) => { if(scene.getObjectByName(name_) != undefined) { scene.remove(scene.getObjectByName(name_)); } })
let distance = 0.0, w = 50;
edges.tl.x = -1.0;
edges.tl.y = -((windowHalfY - w) / window.innerHeight) * 2 + 1;
edges.tl.z = 0.0;
edges.width.x = ((2.0 * w) / window.innerWidth) * 2 - 1;
edges.width.y = -((windowHalfY - w) / window.innerHeight) * 2 + 1;
edges.width.z = 0.0;
edges.tr.x = (windowHalfX * 2.0 / window.innerWidth) * 2 - 1;
edges.tr.y = -((windowHalfY - w) / window.innerHeight) * 2 + 1;
edges.tr.z = 0.0;
edges.bl.x = -1.0;
edges.bl.y = -((windowHalfY + w) / window.innerHeight) * 2 + 1;
edges.bl.z = 0.0;
edges.br.x = (windowHalfX * 2.0 / window.innerWidth) * 2 - 1;
edges.br.y = -((windowHalfY + w) / window.innerHeight) * 2 + 1;
edges.br.z = 0.0;
edges.tl.unproject(camera);
edges.tl.sub(camera.position).normalize();
distance = -camera.position.z / edges.tl.z;
edges.wtl = edges.wtl.copy(camera.position).add(edges.tl.multiplyScalar(distance));
edges.width.unproject(camera);
edges.width.sub(camera.position).normalize();
distance = -camera.position.z / edges.width.z;
edges.wwidth = edges.wwidth.copy(camera.position).add(edges.width.multiplyScalar(distance));
edges.tr.unproject(camera);
edges.tr.sub(camera.position).normalize();
distance = -camera.position.z / edges.tr.z;
edges.wtr = edges.wtr.copy(camera.position).add(edges.tr.multiplyScalar(distance));
edges.bl.unproject(camera);
edges.bl.sub(camera.position).normalize();
distance = -camera.position.z / edges.bl.z;
edges.wbl = edges.wbl.copy(camera.position).add(edges.bl.multiplyScalar(distance));
edges.br.unproject(camera);
edges.br.sub(camera.position).normalize();
distance = -camera.position.z / edges.br.z;
edges.wbr = edges.wbr.copy(camera.position).add(edges.br.multiplyScalar(distance));
const material = new THREE.LineBasicMaterial({ color: 0x0FFFFFF });
const points0 = [edges.wtl, edges.wtr];
let geometry = new THREE.BufferGeometry().setFromPoints(points0);
line0 = new THREE.Line(geometry, material);
line0.name = "line0";
scene.add(line0);
const points1 = [edges.wbl, edges.wbr];
geometry = new THREE.BufferGeometry().setFromPoints(points1);
line1 = new THREE.Line(geometry, material);
line1.name = "line1";
scene.add(line1);
const sphereGeometry = new THREE.SphereGeometry(1.0, 8, 8);
const sphereMaterial = new THREE.MeshBasicMaterial( { color: 0xFFFFFF } );
const sphereMaterial2 = new THREE.MeshBasicMaterial( { color: 0xFF0000 } );
let p00 = new THREE.Vector3(edges.wtl.x, edges.wtl.y, 0.0);
let p01 = new THREE.Vector3(edges.wbl.x, edges.wbl.y, 0.0);
let p10 = new THREE.Vector3(-Math.sqrt(2.0) * 16.0, edges.wtl.y, 0.0);
let p11 = new THREE.Vector3(-Math.sqrt(2.0) * 16.0, edges.wbl.y, 0.0);
let p20 = new THREE.Vector3(0.0, edges.wtl.y, Math.sqrt(2.0) * 16.0);
let p21 = new THREE.Vector3(0.0, edges.wbl.y, Math.sqrt(2.0) * 16.0);
let p30 = new THREE.Vector3(Math.sqrt(2.0) * 16.0, edges.wtl.y, 0.0);
let p31 = new THREE.Vector3(Math.sqrt(2.0) * 16.0, edges.wbl.y, 0.0);
let p40 = new THREE.Vector3(edges.wtr.x, edges.wtr.y, 0.0);
let p41 = new THREE.Vector3(edges.wbr.x, edges.wbr.y, 0.0);
let sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(p00.x, p00.y, p00.z);
sphere.name = "point00";
scene.add(sphere);
sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(p01.x, p01.y, p01.z);
sphere.name = "point01";
scene.add(sphere);
sphere = new THREE.Mesh(sphereGeometry, sphereMaterial2);
sphere.position.set(p10.x, p10.y, p10.z);
sphere.name = "point10";
scene.add(sphere);
sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(p20.x, p20.y, p20.z);
sphere.name = "point20";
scene.add(sphere);
sphere = new THREE.Mesh(sphereGeometry, sphereMaterial2);
sphere.position.set(p30.x, p30.y, p30.z);
sphere.name = "point30";
scene.add(sphere);
sphere = new THREE.Mesh(sphereGeometry, sphereMaterial2);
sphere.position.set(p11.x, p11.y, p11.z);
sphere.name = "point11";
scene.add(sphere);
sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(p21.x, p21.y, p21.z);
sphere.name = "point21";
scene.add(sphere);
sphere = new THREE.Mesh(sphereGeometry, sphereMaterial2);
sphere.position.set(p31.x, p31.y, p31.z);
sphere.name = "point31";
scene.add(sphere);
sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(p40.x, p40.y, p40.z);
sphere.name = "point40";
scene.add(sphere);
sphere = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphere.position.set(p41.x, p41.y, p41.z);
sphere.name = "point41";
scene.add(sphere);
const material2 = new THREE.LineBasicMaterial({ color: 0x0FF0000 });
let points = [p00, p10, p20, p30, p40];
geometry = new THREE.BufferGeometry().setFromPoints(points);
let topLine = new THREE.Line(geometry, material2);
topLine.name = "topLine";
scene.add(topLine);
points = [p01, p11, p21, p31, p41];
geometry = new THREE.BufferGeometry().setFromPoints(points);
let bottomLine = new THREE.Line(geometry, material2);
bottomLine.name = "bottomLine";
scene.add(bottomLine);
let pf0 = new THREE.Vector3(edges.wtl.x + edges.wtl.distanceTo(edges.wwidth), p00.y, p00.z);
let pf1 = new THREE.Vector3(edges.wbl.x + edges.wtl.distanceTo(edges.wwidth), p01.y, p01.z);
//let pf1 = new THREE.Vector3(edges.wwidth.x * 2, p01.y, p01.z);
points = [p00, pf0, pf1, p01, p00];
geometry = new THREE.BufferGeometry().setFromPoints(points);
let frameLine = new THREE.Line(geometry, material);
frameLine.name = "frame";
scene.add(frameLine);
}
body { margin: 0; }
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/106/three.min.js"></script>
As is in the figure below,
consider a Plane made by camera.position and both end-points(P00 and P40)
and move the points(P10 and P30) to the intersection points of the plane and the edges.
Then, the points would align straight on the screen as expected.
For example, by using ray.intersectPlane(),
let p00 = new THREE.Vector3(edges.wtl.x, edges.wtl.y, 0.0);
let p01 = new THREE.Vector3(edges.wbl.x, edges.wbl.y, 0.0);
//let p10 = new THREE.Vector3(-Math.sqrt(2.0) * 16.0, edges.wtl.y, 0.0);
//let p11 = new THREE.Vector3(-Math.sqrt(2.0) * 16.0, edges.wbl.y, 0.0);
let p10 = new THREE.Vector3(-Math.sqrt(2.0) * 16.0, 16.0, 0.0);
let p11 = new THREE.Vector3(-Math.sqrt(2.0) * 16.0, -16.0, 0.0);
let p20 = new THREE.Vector3(0.0, edges.wtl.y, Math.sqrt(2.0) * 16.0);
let p21 = new THREE.Vector3(0.0, edges.wbl.y, Math.sqrt(2.0) * 16.0);
//let p30 = new THREE.Vector3(Math.sqrt(2.0) * 16.0, edges.wtl.y, 0.0);
//let p31 = new THREE.Vector3(Math.sqrt(2.0) * 16.0, edges.wbl.y, 0.0);
let p30 = new THREE.Vector3(Math.sqrt(2.0) * 16.0, 16.0, 0.0);
let p31 = new THREE.Vector3(Math.sqrt(2.0) * 16.0, -16.0, 0.0);
let p40 = new THREE.Vector3(edges.wtr.x, edges.wtr.y, 0.0);
let p41 = new THREE.Vector3(edges.wbr.x, edges.wbr.y, 0.0);
let nwt = p00.clone().sub(camera.position).cross(p40.clone().sub(camera.position)).normalize();
let nwb = p01.clone().sub(camera.position).cross(p41.clone().sub(camera.position)).normalize();
let planewt = new THREE.Plane(nwt, -nwt.dot(camera.position));
let planewb = new THREE.Plane(nwb, -nwb.dot(camera.position));
let r10 = new THREE.Ray(p10.clone(), p11.clone().sub(p10).normalize());
let r11 = new THREE.Ray(p11.clone(), p10.clone().sub(p11).normalize());
r10.intersectPlane(planewt, p10);
r11.intersectPlane(planewb, p11);
let r30 = new THREE.Ray(p30.clone(), p31.clone().sub(p30).normalize());
let r31 = new THREE.Ray(p31.clone(), p30.clone().sub(p31).normalize());
r30.intersectPlane(planewt, p30);
r31.intersectPlane(planewb, p31);
Related
Rotation in react application
I am trying to make a game where I have to move the plane in circular motion. However, when it reaches 90 degree or certain angle it still points in same direction as it is still image. I want to update the position on a circle with the state of react but image should also move and not point in same direction. aeroplane move circular Currently the head is in east direction, at 0 degree. Now if the react state has value 90 degree i want it to move on this circle 90 degree but also now point upwards in north direction . Anyhelp? Yes I tried it but does not seem to be working. I was using canvas earlier..................... class Louie extends Component { constructor(props) { super(props); this.update = this.update.bind(this); this.rotateImage = this.rotateImage.bind(this); } componentDidMount() { this.props.dispatch(getStats()); this.canvas = this.refs.canvas; this.ctx = this.canvas.getContext("2d"); this.image = new Image(); this.image.src = require("../../Images/louie-front.png"); this.image.id = "lpl-head"; this.image.style.transform = "rotate(90deg)"; this.camera = {}; this.camera.x = 0; this.camera.y = 0; this.scale = 1.0; this.obj = []; this.t = {}; this.t.angle = Math.random() * Math.PI * 2; //start angle this.t.radius = document.getElementById("louie-canvas").offsetHeight - 100; this.t.x = Math.cos(this.t.angle) * this.t.radius; // start position x this.t.y = Math.sin(this.t.angle) * this.t.radius; //start position y this.t.circumference = this.t.radius * 2 * Math.PI; //curcumfrence this.t.start = Date.now(); this.obj.push(this.t); this.draw(); } draw = () => { this.update(); this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.save(); this.ctx.translate( 0 - (this.camera.x - this.canvas.width / 2), 0 - (this.camera.y - this.canvas.height / 2) ); this.ctx.scale(this.scale, this.scale); this.ctx.fillStyle = "blue"; for (var i = 0; i < this.obj.length; i++) { this.ctx.beginPath(); this.ctx.arc(0, 0, this.obj[i].radius, 0, Math.PI * 2); this.ctx.lineWidth = 5; this.ctx.strokeStyle = "#949494"; this.ctx.shadowBlur = 15; this.ctx.shadowColor = "#080808"; this.ctx.shadowOffsetX = 10; this.ctx.shadowOffsetY = 5; this.ctx.stroke(); this.ctx.drawImage( this.image, this.obj[i].x - (window.innerWidth * 4) / 100, this.obj[i].y - (window.innerWidth * 3) / 100, (window.innerWidth * 8) / 100, (window.innerHeight * 10) / 100 ); } this.ctx.fillStyle = "#ab377a"; this.ctx.beginPath(); this.ctx.arc(0, 0, 10, 0, 2 * Math.PI); this.ctx.closePath(); this.ctx.fill(); this.ctx.restore(); requestAnimationFrame(this.draw); }; update = () => { for (var i = 0; i < this.obj.length; i++) { var angle = this.props.headDegree; this.obj[i].x = this.obj[i].radius * Math.cos((angle * Math.PI) / 180); this.obj[i].y = this.obj[i].radius * Math.sin((angle * Math.PI) / 180); } }; rotateImage = (angle) => { this.image.style.transform = `rotateZ(${angle * 3}deg)`; };
I just know how to use for to draw the tree, but now I want to use recursion to draw the tree
I just know how to use for to draw a tree (the tree data is the picture one, the result is picture two), but now I want to use recursion to draw the tree. Please tell me how change writing style from for to recursive first input point //input point const line_point =[0, 0, 0, 2, 151, 2, 2, 151, 2, -62, 283, 63, 2, 151, 2, 62, 297, -58, -62, 283, 63, -104, 334, 74, -62, 283, 63, -58, 338, 45, 62, 297, -58, 67, 403, -55, 62, 297, -58, 105, 365, -86]; take out star point and end point const star_line_x= new Array(); const star_line_y= new Array(); const star_line_z= new Array(); const end_line_x= new Array(); const end_line_y= new Array(); const end_line_z= new Array(); for (var q=0; q < line_point.length; q+=6){ star_line_x.push(line_point[q]); } for (var r=1; r < line_point.length; r+=6){ star_line_y.push(line_point[r]); } for (var s=2; s < line_point.length; s+=6){ star_line_z.push(line_point[s]); } for (var t=3; t < line_point.length; t+=6){ end_line_x.push(line_point[t]); } for (var u=4; u < line_point.length; u+=6){ end_line_y.push(line_point[u]); } for (var v=5; v < line_point.length; v+=6){ end_line_z.push(line_point[v]); } var cylinder_star_point = new Array(); var cylinder_end_point = new Array(); //star_point end_point for (var w=0; w < line_point.length/6; w++){ var star_point = new THREE.Vector3 (star_line_x[w],star_line_y[w],star_line_z[w]); var end_point = new THREE.Vector3 (end_line_x[w],end_line_y[w],end_line_z[w]); cylinder_star_point.push( star_point); cylinder_end_point.push( end_point); } calculation cylinder high //calculation cylinder high var line_len = new Array(); for (var dd=0; dd < line_point.length/6; dd++){ var len_x = Math.pow(end_line_x[dd]-star_line_x[dd],2); var len_y = Math.pow(end_line_y[dd]-star_line_y[dd],2); var len_z = Math.pow(end_line_z[dd]-star_line_z[dd],2); var len_direction = Math.sqrt(len_x+len_y+len_z); line_len.push(len_direction);//Cylinder high } calculation center point //center_point const cylinder_center_point= new Array(); for (var bb=0; bb< cylinder_end_point.length; bb++){ var star_set_point = cylinder_star_point[bb]; var end_set_point = cylinder_end_point[bb]; var center_point = end_set_point.clone().add(star_set_point).divideScalar(2); cylinder_center_point.push(center_point); } calculation cylinder direction vector //cylinder direction const cylinder_direction= new Array(); for (var cc=0; cc < cylinder_end_point.length; cc++){ var star_direction = cylinder_star_point[cc]; var end_direction = cylinder_end_point[cc]; var center_direction = end_direction.clone().sub(star_direction); cylinder_direction.push(center_direction); } draw cylinder for (var dd=0; dd <cylinder_direction.length;dd++){ var material = new THREE.MeshPhongMaterial({color:'#ff0000'}); let upVector = new THREE.Vector3(0, 1, 0); var geometry = new THREE.CylinderGeometry(5, 5, line_len[dd], 20, 1, false); var mesh = new THREE.Mesh(geometry, material); mesh.position.set(0, line_len[dd]/2, 0); var group = new THREE.Group(); group.position.set(star_line_x[dd],star_line_y[dd],star_line_z[dd]); group.add(mesh); let targetVector =cylinder_direction[dd]; let quaternion = new THREE.Quaternion().setFromUnitVectors(upVector, targetVector.normalize()); group.setRotationFromQuaternion(quaternion) scene.add(group) } picture two: use for to draw the tree
For a tree the simplest method is to start with just a tree depth and assume 2 children. The function makes one branch and if depth > 0 then it recursively calls itself to make 2 more branches. const numBranches = 2; const spread = 1.5; const branchShrinkFactor = 0.8; const branchSpreadFactor = 0.8; function addBranch(parent, depth, offset, angle, branchLength, spread) { const material = new THREE.MeshPhongMaterial({color:'#ff0000'}); const geometry = new THREE.CylinderBufferGeometry(5, 5, branchLength, 20, 1, false); geometry.translate(0, branchLength / 2, 0); const mesh = new THREE.Mesh(geometry, material); mesh.position.y = offset; mesh.rotation.z = angle; parent.add(mesh); if (depth > 1) { for (let i = 0; i < numBranches; ++i) { const a = i / (numBranches - 1) - 0.5; addBranch(mesh, depth - 1, branchLength, a * spread, branchLength * branchShrinkFactor, spread * branchSpreadFactor) } } } addBranch(scene, 5, 0, 0, 100, 1.5); body { margin: 0; } #c { width: 100vw; height: 100vh; display: block; } <canvas id="c"></canvas> <script type="module"> import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.module.js'; function main() { const canvas = document.querySelector('#c'); const renderer = new THREE.WebGLRenderer({canvas}); const fov = 75; const aspect = 2; // the canvas default const near = 1; const far = 1000; const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); camera.position.set(0, 150, 300); const scene = new THREE.Scene(); scene.background = new THREE.Color('lightskyblue'); { const color = 0xFFFFFF; const intensity = 1; const light = new THREE.DirectionalLight(color, intensity); light.position.set(-1, 2, 4); scene.add(light); } const numBranches = 2; const spread = 1.5; const branchShrinkFactor = 0.8; const branchSpreadFactor = 0.8; function addBranch(parent, depth, offset, angle, branchLength, spread) { const material = new THREE.MeshPhongMaterial({color:'#ff0000'}); const geometry = new THREE.CylinderBufferGeometry(5, 5, branchLength, 20, 1, false); geometry.translate(0, branchLength / 2, 0); const mesh = new THREE.Mesh(geometry, material); mesh.position.y = offset; mesh.rotation.z = angle; parent.add(mesh); if (depth > 1) { for (let i = 0; i < numBranches; ++i) { const a = i / (numBranches - 1) - 0.5; addBranch(mesh, depth - 1, branchLength, a * spread, branchLength * branchShrinkFactor, spread * branchSpreadFactor) } } } addBranch(scene, 5, 0, 0, 100, 1.5); 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; } function render(time) { time *= 0.001; if (resizeRendererToDisplaySize(renderer)) { const canvas = renderer.domElement; camera.aspect = canvas.clientWidth / canvas.clientHeight; camera.updateProjectionMatrix(); } renderer.render(scene, camera); // requestAnimationFrame(render); } requestAnimationFrame(render); } main(); </script> If you want specific data for each branch then you need to pass that in. For example const tree = [ { length: 100, angle: 0, branches: 2 }, // root { length: 40, angle: -1, branches: 3 }, // first branch { length: 50, angle: 0.8, branches: 0 }, // 1st child branch { length: 40, angle: 0.3, branches: 0 }, // 2nd child branch { length: 30, angle: -0.3, branches: 0 }, // 3rd child branch { length: 50, angle: 0.8, branches: 2 }, // second branch { length: 50, angle: 0.5, branches: 0 }, // 1st child branch { length: 40, angle: -0.6, branches: 2 }, // 2nd child branch { length: 40, angle: -0.3, branches: 0 }, // 1st grandchild branch { length: 95, angle: 0.3, branches: 0 }, // 2st grandchild branch ]; and then walk the tree description, if a branches for a particular branch is > 0 then it recursively calls itself to add those branches. Each branches consumes a row in the array of branches so we pass back ndx so we can tell how many rows were consumed. function addBranch(parent, offset, tree, ndx = 0) { const {length, angle, branches} = tree[ndx]; const material = new THREE.MeshPhongMaterial({color:'#ff0000'}); const geometry = new THREE.CylinderGeometry(5, 5, length, 20, 1, false); geometry.translate(0, length / 2, 0); const mesh = new THREE.Mesh(geometry, material); mesh.position.y = offset; mesh.rotation.z = angle; parent.add(mesh); for (let i = 0; i < branches; ++i) { ndx = addBranch(mesh, length, tree, ++ndx); } return ndx; } addBranch(scene, 0, tree); body { margin: 0; } #c { width: 100vw; height: 100vh; display: block; } <canvas id="c"></canvas> <script type="module"> import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.module.js'; function main() { const canvas = document.querySelector('#c'); const renderer = new THREE.WebGLRenderer({canvas}); const fov = 75; const aspect = 2; // the canvas default const near = 1; const far = 1000; const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); camera.position.set(0, 150, 300); const scene = new THREE.Scene(); scene.background = new THREE.Color('lightskyblue'); { const color = 0xFFFFFF; const intensity = 1; const light = new THREE.DirectionalLight(color, intensity); light.position.set(-1, 2, 4); scene.add(light); } const tree = [ { length: 100, angle: 0, branches: 2 }, // root { length: 40, angle: -1, branches: 3 }, // first branch { length: 50, angle: 0.8, branches: 0 }, // 1st child branch { length: 40, angle: 0.3, branches: 0 }, // 2nd child branch { length: 30, angle: -0.3, branches: 0 }, // 3rd child branch { length: 50, angle: 0.8, branches: 2 }, // second branch { length: 50, angle: 0.5, branches: 0 }, // 1st child branch { length: 40, angle: -0.6, branches: 2 }, // 2nd child branch { length: 40, angle: -0.3, branches: 0 }, // 1st grandchild branch { length: 95, angle: 0.3, branches: 0 }, // 2st grandchild branch ]; function addBranch(parent, offset, tree, ndx = 0) { const {length, angle, branches} = tree[ndx]; const material = new THREE.MeshPhongMaterial({color:'#ff0000'}); const geometry = new THREE.CylinderGeometry(5, 5, length, 20, 1, false); geometry.translate(0, length / 2, 0); const mesh = new THREE.Mesh(geometry, material); mesh.position.y = offset; mesh.rotation.z = angle; parent.add(mesh); for (let i = 0; i < branches; ++i) { ndx = addBranch(mesh, length, tree, ++ndx); } return ndx; } addBranch(scene, 0, tree); 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; } function render(time) { time *= 0.001; if (resizeRendererToDisplaySize(renderer)) { const canvas = renderer.domElement; camera.aspect = canvas.clientWidth / canvas.clientHeight; camera.updateProjectionMatrix(); } renderer.render(scene, camera); // requestAnimationFrame(render); } requestAnimationFrame(render); } main(); </script> It's not clear to me what your input data is. Your tree has a depth of 3 and 2 branches per level so this data would work const endPoints = [ [ 0, 0, 0], // A [ 2, 151, 2], // B [ -62, 283, 63], // C [-104, 334, 74], // E [ -58, 338, 45], // F [ 62, 296, -58], // D [ 67, 403, -55], // G [ 105, 365, -86], // H ]; using this code // assumes there are 2 branches per function addBranch(parent, depth, offset, tree, parentNdx = 0, childNdx = 1) { const start = tree[parentNdx]; const end = tree[childNdx]; const length = start.distanceTo(end); const material = new THREE.MeshPhongMaterial({color:'#ff0000'}); const geometry = new THREE.CylinderGeometry(5, 5, length, 20, 1, false); geometry.translate(0, length / 2, 0); geometry.rotateX(Math.PI / 2); const mesh = new THREE.Mesh(geometry, material); mesh.position.z = offset; parent.add(mesh); mesh.lookAt(end); let ndx = childNdx + 1; if (depth > 1) { const numBranches = 2; for (let i = 0; i < numBranches; ++i) { ndx = addBranch(mesh, depth - 1, length, tree, childNdx, ndx); } } return ndx; } addBranch(scene, 3, 0, tree); I pointed the cylinders in the positive Z direction which means I can use lookAt to point the cylinder from its start to its end point. body { margin: 0; } #c { width: 100vw; height: 100vh; display: block; } <canvas id="c"></canvas> <script type="module"> import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.module.js'; function main() { const canvas = document.querySelector('#c'); const renderer = new THREE.WebGLRenderer({canvas}); const fov = 75; const aspect = 2; // the canvas default const near = 1; const far = 1000; const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); camera.position.set(250, 170, 250); camera.lookAt(0, 170, 0); const scene = new THREE.Scene(); scene.background = new THREE.Color('lightskyblue'); { const color = 0xFFFFFF; const intensity = 1; const light = new THREE.DirectionalLight(color, intensity); light.position.set(-1, 2, 4); scene.add(light); } const tree = [ [ 0, 0, 0], // A [ 2, 151, 2], // B [ -62, 283, 63], // C [-104, 334, 74], // E [ -58, 338, 45], // F [ 62, 296, -58], // D [ 67, 403, -55], // G [ 105, 365, -86], // H ].map(v => new THREE.Vector3().fromArray(v)); // assumes there are 2 branches per function addBranch(parent, depth, offset, tree, parentNdx = 0, childNdx = 1) { const start = tree[parentNdx]; const end = tree[childNdx]; const length = start.distanceTo(end); const material = new THREE.MeshPhongMaterial({color:'#ff0000'}); const geometry = new THREE.CylinderGeometry(5, 5, length, 20, 1, false); geometry.translate(0, length / 2, 0); geometry.rotateX(Math.PI / 2); const mesh = new THREE.Mesh(geometry, material); mesh.position.z = offset; parent.add(mesh); mesh.lookAt(end); let ndx = childNdx + 1; if (depth > 1) { const numBranches = 2; for (let i = 0; i < numBranches; ++i) { ndx = addBranch(mesh, depth - 1, length, tree, childNdx, ndx); } } return ndx; } addBranch(scene, 3, 0, tree); 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; } function render(time) { time *= 0.001; if (resizeRendererToDisplaySize(renderer)) { const canvas = renderer.domElement; camera.aspect = canvas.clientWidth / canvas.clientHeight; camera.updateProjectionMatrix(); } renderer.render(scene, camera); // requestAnimationFrame(render); } requestAnimationFrame(render); } main(); </script> note: this only one of infinite ways to create the tree recursively. Rather than an array in depth first order you could also create a tree structure to pass into the algorithm const E = { pos: [-104, 334, 74], }; const F = { pos: [ -58, 338, 45], }; const C = { pos: [ -62, 283, 63], children: [E, F], }; const G = { pos: [ 67, 403, -55], }; const H = { pos: [ 105, 365, -86], }; const D = { pos: [ 62, 296, -58], children: [G, H], }; const B = { pos: [ 2, 151, 2], children: [C, D], }; const A = { pos: [0, 0, 0], children: [B], }; function addBranch(parent, branch, offset = 0) { const {pos, children} = branch; const start = new THREE.Vector3().fromArray(pos); for (const child of children) { const end = new THREE.Vector3().fromArray(child.pos); const length = start.distanceTo(end); const geometry = new THREE.CylinderGeometry(5, 5, length, 20, 1, false); geometry.translate(0, length / 2, 0); geometry.rotateX(Math.PI / 2); const material = new THREE.MeshPhongMaterial({color: 'red'}); const mesh = new THREE.Mesh(geometry, material); mesh.position.z = offset; parent.add(mesh); mesh.lookAt(end); if (child.children) { addBranch(mesh, child, length); } } } addBranch(scene, A); body { margin: 0; } #c { width: 100vw; height: 100vh; display: block; } <canvas id="c"></canvas> <script type="module"> import * as THREE from 'https://threejsfundamentals.org/threejs/resources/threejs/r115/build/three.module.js'; function main() { const canvas = document.querySelector('#c'); const renderer = new THREE.WebGLRenderer({canvas}); const fov = 75; const aspect = 2; // the canvas default const near = 1; const far = 1000; const camera = new THREE.PerspectiveCamera(fov, aspect, near, far); camera.position.set(250, 170, 250); camera.lookAt(0, 170, 0); const scene = new THREE.Scene(); scene.background = new THREE.Color('lightskyblue'); { const color = 0xFFFFFF; const intensity = 1; const light = new THREE.DirectionalLight(color, intensity); light.position.set(-1, 2, 4); scene.add(light); } const E = { pos: [-104, 334, 74], }; const F = { pos: [ -58, 338, 45], }; const C = { pos: [ -62, 283, 63], children: [E, F], }; const G = { pos: [ 67, 403, -55], }; const H = { pos: [ 105, 365, -86], }; const D = { pos: [ 62, 296, -58], children: [G, H], }; const B = { pos: [ 2, 151, 2], children: [C, D], }; const A = { pos: [0, 0, 0], children: [B], }; function addBranch(parent, branch, offset = 0) { const {pos, children} = branch; const start = new THREE.Vector3().fromArray(pos); for (const child of children) { const end = new THREE.Vector3().fromArray(child.pos); const length = start.distanceTo(end); const geometry = new THREE.CylinderGeometry(5, 5, length, 20, 1, false); geometry.translate(0, length / 2, 0); geometry.rotateX(Math.PI / 2); const material = new THREE.MeshPhongMaterial({color: 'red'}); const mesh = new THREE.Mesh(geometry, material); mesh.position.z = offset; parent.add(mesh); mesh.lookAt(end); if (child.children) { addBranch(mesh, child, length); } } } addBranch(scene, A); 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; } function render(time) { time *= 0.001; if (resizeRendererToDisplaySize(renderer)) { const canvas = renderer.domElement; camera.aspect = canvas.clientWidth / canvas.clientHeight; camera.updateProjectionMatrix(); } renderer.render(scene, camera); // requestAnimationFrame(render); } requestAnimationFrame(render); } main(); </script>
how to make custom class in fabric js using fabric.Textbox Class override?
I am using FabricJS version : 3.6.3 I want to make new FabricJS class called : Button So that I have extend one class called Textbox from fabric js, which will Draw a Rectangle behind Text and it looking like a button. But Problem is that, I can't set height to that Button because height is not allow in Texbox object. I want to set Height and Width to Button object. Width is working Properly due to Textbox. it will also warp Text if width keep smaller then text width, and can be editable by double clicking on it. But only problem is that can't set Height to an object it should be Text vertically center when Height is increase. In short I want to make this kind of functionality in fabric js using object customization. Expected Output : but Actual Output : Here Is my Code That Create button : // fabric js custom button class (function (fabric) { "use strict"; // var fabric = global.fabric || (global.fabric = {}); fabric.Button = fabric.util.createClass(fabric.Textbox, { type: "button", stateProperties: fabric.Object.prototype.stateProperties.concat( "buttonRx", "buttonRy", "buttonFill", "buttonPadding", "buttonStrokeColor", "buttonStrokeWidth" ), buttonRx: 0, buttonRy: 0, buttonFill: "#ffffff00", buttonPadding: 0, buttonHeight: 0, buttonWidth: 0, textAlign: "center", buttonStrokeColor: "#000000", buttonStrokeWidth: 0, _dimensionAffectingProps: fabric.Text.prototype._dimensionAffectingProps.concat( "width", "fontSize" ), cacheProperties: fabric.Object.prototype.cacheProperties.concat( "buttonRx", "buttonRy", "buttonFill", "buttonPadding", "buttonStrokeColor", "buttonStrokeWidth" ), initialize: function (text, options) { this.text = text; this.callSuper("initialize", text, options); /* this.on("scaling", function () { console.log('scaling', this.getScaledHeight()); this.set({ height: this.getScaledHeight(), scaleY: 1, }); }); */ this._initRxRy(); }, _initRxRy: function () { if (this.buttonRx && !this.buttonRy) { this.buttonRy = this.buttonRx; } else if (this.buttonRy && !this.buttonRx) { this.buttonRx = this.buttonRy; } }, /* _setCenter(){ }, */ _render: function (ctx) { // 1x1 case (used in spray brush) optimization was removed because // with caching and higher zoom level this makes more damage than help // this.width = this.width * this.scaleX; // this.height = this.height * this.scaleY; // (this.scaleX = 1), (this.scaleY = 1); var rx = this.buttonRx ? Math.min(this.buttonRx, this.width / 2) : 0, ry = this.buttonRy ? Math.min(this.buttonRy, this.height / 2) : 0, w = this.width + this.buttonPadding, h = this.height + this.buttonPadding, x = -this.width / 2 - this.buttonPadding / 2, y = -this.height / 2 - this.buttonPadding / 2, isRounded = rx !== 0 || ry !== 0, /* "magic number" for bezier approximations of arcs (http://itc.ktu.lt/itc354/Riskus354.pdf) */ k = 1 - 0.5522847498; ctx.beginPath(); ctx.moveTo(x + rx, y); ctx.lineTo(x + w - rx, y); isRounded && ctx.bezierCurveTo(x + w - k * rx, y, x + w, y + k * ry, x + w, y + ry); ctx.lineTo(x + w, y + h - ry); isRounded && ctx.bezierCurveTo( x + w, y + h - k * ry, x + w - k * rx, y + h, x + w - rx, y + h ); ctx.lineTo(x + rx, y + h); isRounded && ctx.bezierCurveTo(x + k * rx, y + h, x, y + h - k * ry, x, y + h - ry); ctx.lineTo(x, y + ry); isRounded && ctx.bezierCurveTo(x, y + k * ry, x + k * rx, y, x + rx, y); ctx.closePath(); ctx.save(); if (this.buttonFill) { ctx.fillStyle = this.buttonFill; if (this.fillRule === "evenodd") { ctx.fill("evenodd"); } else { ctx.fill(); } } if (this.buttonStrokeWidth > 0) { if (this.strokeUniform) { ctx.scale(1 / this.scaleX, 1 / this.scaleY); } if (this.shadow && !this.shadow.affectStroke) { this._removeShadow(ctx); } if (this.buttonStrokeColor) { ctx.lineWidth = this.buttonStrokeWidth; ctx.strokeStyle = this.buttonStrokeColor; ctx.stroke(); } else { ctx.lineWidth = this.buttonStrokeWidth; ctx.stroke(); } } ctx.restore(); this.clearContextTop(); this._clearCache(); this.height = this.calcTextHeight(); this.saveState({ propertySet: "_dimensionAffectingProps" }); // this._renderPaintInOrder(ctx); this._setTextStyles(ctx); this._renderTextLinesBackground(ctx); this._renderTextDecoration(ctx, "underline"); this._renderText(ctx); this._renderTextDecoration(ctx, "overline"); this._renderTextDecoration(ctx, "linethrough"); this.initDimensions(); // this.callSuper('render', ctx); }, toObject: function (propertiesToInclude) { return this.callSuper( "toObject", [ "buttonRx", "buttonRy", "buttonFill", "buttonPadding", "buttonStrokeColor", "buttonStrokeWidth", "objectCaching", ].concat(propertiesToInclude) ); }, }); fabric.Button.fromObject = function (object, callback) { return fabric.Object._fromObject("Button", object, callback, "text"); }; })(fabric); // fabric js class finish here var canvas = []; var cotainer = document.getElementById("canvas-container"); for (let i = 0; i < 1; i++) { var width = 500, height = 500; var canvasEl = document.createElement("canvas"); canvasEl.id = "canvas-" + i; cotainer.append(canvasEl); var fabCanvas = new fabric.Canvas(canvasEl, {}); fabCanvas.setHeight(height); fabCanvas.setWidth(width); canvas.push(fabCanvas); } canvas.forEach((c) => { var button = new fabric.Button("Click Me", { text: "Click Me", buttonStrokeColor: "#f00", buttonStrokeWidth: 2, width: 110, fill: "#f00", fontSize: 50, width: 400, buttonFill: "#42A5F5", buttonRx: 15, buttonRy: 15, objectCaching: false, fontFamily: "verdana", }); c.add(button); c.renderAll(); }); canvas{ border: 1px solid black } <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.6.2/fabric.js"></script> <div id="canvas-container"> </div>
The solution will be to set the scaleX and scaleY of the button text to 1 when you scale the Button Object and also set the font size of the text equal to its scale.
var tbox = new fabric.Button(v.textDisp, { left: v.posX, top: v.posY, boxHeight: v.length // new create }); fabric.Button = fabric.util.createClass(fabric.Textbox, { type: "button", stateProperties: fabric.Object.prototype.stateProperties.concat( "buttonRx", "buttonRy", "buttonFill", "buttonPadding", "buttonStrokeColor", "buttonStrokeWidth", "boxHeight" ), buttonRx: 0, buttonRy: 0, buttonPadding: 0, buttonHeight: 0, buttonWidth: 0, buttonStrokeColor: "#000000", buttonStrokeWidth: 0, _dimensionAffectingProps: fabric.Text.prototype._dimensionAffectingProps.concat( "width", "fontSize" ), cacheProperties: fabric.Object.prototype.cacheProperties.concat( "buttonRx", "buttonRy", "buttonFill", "buttonPadding", "buttonStrokeColor", "buttonStrokeWidth", "boxHeight" ), initialize: function (text, options) { this.text = text; this.callSuper("initialize", text, options); /* this.on("scaling", function () { console.log('scaling', this.getScaledHeight()); this.set({ height: this.getScaledHeight(), scaleY: 1, }); }); */ this._initRxRy(); }, _initRxRy: function () { if (this.buttonRx && !this.buttonRy) { this.buttonRy = this.buttonRx; } else if (this.buttonRy && !this.buttonRx) { this.buttonRx = this.buttonRy; } }, /* _setCenter(){ }, */ _render: function (ctx) { // 1x1 case (used in spray brush) optimization was removed because // with caching and higher zoom level this makes more damage than help // this.width = this.width * this.scaleX; // this.height = this.height * this.scaleY; // (this.scaleX = 1), (this.scaleY = 1); var rx = this.buttonRx ? Math.min(this.buttonRx, this.width / 2) : 0, ry = this.buttonRy ? Math.min(this.buttonRy, this.height / 2) : 0, w = this.width + this.buttonPadding, h = this.height + this.buttonPadding, x = -this.width / 2 - this.buttonPadding / 2, y = -this.height / 2 - this.buttonPadding / 2, hh = this.boxHeight * this.scaleY, isRounded = rx !== 0 || ry !== 0, k = 1 - 0.5522847498; ctx.beginPath(); ctx.moveTo(x + rx, y); ctx.lineTo(x + w - rx, y); isRounded && ctx.bezierCurveTo(x + w - k * rx, y, x + w, y + k * ry, x + w, y + ry); ctx.lineTo(x + w, y + hh - ry); isRounded && ctx.bezierCurveTo( x + w, y + hh - k * ry, x + w - k * rx, y + hh, x + w - rx, y + hh ); ctx.lineTo(x + rx, y + hh); isRounded && ctx.bezierCurveTo(x + k * rx, y + hh, x, y + hh - k * ry, x, y + hh - ry); ctx.lineTo(x, y + ry); isRounded && ctx.bezierCurveTo(x, y + k * ry, x + k * rx, y, x + rx, y); ctx.closePath(); ctx.save(); if (this.buttonFill) { ctx.fillStyle = this.buttonFill; if (this.fillRule === "evenodd") { ctx.fill("evenodd"); } else { ctx.fill(); } } if (this.buttonStrokeWidth > 0) { if (this.strokeUniform) { ctx.scale(1 / this.scaleX, 1 / this.scaleY); } if (this.shadow && !this.shadow.affectStroke) { this._removeShadow(ctx); } if (this.buttonStrokeColor) { ctx.lineWidth = this.buttonStrokeWidth; ctx.strokeStyle = this.buttonStrokeColor; ctx.stroke(); } else { ctx.lineWidth = this.buttonStrokeWidth; ctx.stroke(); } } ctx.restore(); this.clearContextTop(); this._clearCache(); this.height = this.calcTextHeight(); this.saveState({ propertySet: "_dimensionAffectingProps" }); // this._renderPaintInOrder(ctx); this._setTextStyles(ctx); this._renderTextLinesBackground(ctx); this._renderTextDecoration(ctx, "underline"); this._renderText(ctx); this._renderTextDecoration(ctx, "overline"); this._renderTextDecoration(ctx, "linethrough"); this.initDimensions(); // this.callSuper('render', ctx); }, toObject: function (propertiesToInclude) { return this.callSuper( "toObject", [ "buttonRx", "buttonRy", "buttonFill", "buttonPadding", "buttonStrokeColor", "buttonStrokeWidth", "objectCaching", "boxHeight" ].concat(propertiesToInclude) ); }, }); After adding boxHeight, declare a variable as 'hh' instead of'h' in "_render" When drawing, change to'hh' instead of'h'
how to dragging Threejs point
I'm trying to huge graph visualization with threejs r86(latest master version), for showing 600,000 nodes I found a way to draw them faster than using mesh with THREE.points but know I need to make them draggable, after many searches I found raycast to found closest object to mouse point but I have a problem becouse all of taht points are just an object and can not be changed seperately. function Graph3(Nodes, Edges) { this.renderer = new THREE.WebGLRenderer({ alpha: true}); var width = window.innerWidth , height = window.innerHeight; this.renderer.setSize(width, height, false); document.body.appendChild(this.renderer.domElement); this.scene = new THREE.Scene(), this.camera = new THREE.PerspectiveCamera(100, width / height, 0.1, 3000), this.controls = new THREE.OrbitControls(this.camera); this.controls.enableKeys = true; this.controls.enableRotate = false; var material, geometry; self = this; material = new THREE.LineBasicMaterial({color: '#ccc'}); geometry = new THREE.Geometry(); geometry.vertices = Nodes.map(function(item){return new THREE.Vector3(item.pos.x,item.pos.y,item.pos.z);}); // this.vertices = geometry.vertices; this.line = new THREE.LineSegments(geometry, material); this.scene.add(this.line); var Node = new THREE.Group(); material = new THREE.PointsMaterial( { color:0x000060 ,size:1 } ); this.particles = new THREE.Mesh(geometry,material) this.particles = new THREE.Points( geometry, material); this.scene.add( this.particles ); dragControls = new THREE.DragControls([this.particles], this.camera/*,this.scene*/, this.renderer.domElement); this.camera.position.z = 200; var raycaster = new THREE.Raycaster(); var mouse = new THREE.Vector2(); document.addEventListener( 'click', function ( event ) { // calculate mouse position in normalized device coordinates // (-1 to +1) for both components mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1; mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; console.log(mouse); }, false ); stats = new Stats(); document.body.appendChild(stats.dom); this.animate = function() { raycaster.setFromCamera( mouse, self.camera ); var intersections = raycaster.intersectObject( self.particles ); intersection = ( intersections.length ) > 0 ? intersections[ 0 ] : null; if ( intersection !== null) { console.log(intersection); } requestAnimationFrame( self.animate ); stats.update(); self.renderer.render(self.scene, self.camera); } this.animate();} I had able to change all the points with dragControls but can't move them seperatly I had found EventsControls.js file which help us to handle events but I couldn't use it
Here you can check how to target individual parts of a buffer geometry with a raycaster: https://github.com/mrdoob/three.js/blob/master/examples/webgl_interactive_buffergeometry.html As for moving them, refer to this question and answer: How to quickly update a large BufferGeometry?
Thanks for helping me in previous question. I am making my points in 2d plane (z = 0) and I could making them with bufferGeometry and RawShaderMaterial but now I have another problem in dragging them, how raycaster do? it need vec3 positions but I have changed it for performance purpose. var Geo = new THREE.BufferGeometry(); var position = new Float32Array( NodeCount * 2 ); var colors = new Float32Array( NodeCount * 3 ); var sizes = new Float32Array( NodeCount ); for ( var i = 0; i < NodeCount; i++ ) { position[ 2*i ] = (Math.random() - 0.5) * 10; position[ 2*i + 1 ] = (Math.random() - 0.5) * 10; colors[ 3*i ] = Math.random(); colors[3*i+1] = Math.random(); colors[3*i+2] = Math.random(); // sizes sizes[i] = Math.random() * 5 ; } Geo.addAttribute( 'position', new THREE.BufferAttribute( position, 2 ) ); Geo.addAttribute( 'color', new THREE.BufferAttribute( colors, 3 ) ); Geo.addAttribute( 'size', new THREE.BufferAttribute( sizes, 1 ) ); points = new THREE.Points( Geo, new THREE.RawShaderMaterial({ vertexShader:` precision highp float; uniform mat4 modelViewMatrix; uniform mat4 projectionMatrix; uniform vec3 cameraPosition; attribute vec2 position; /// reason of problem varying vec3 vColor; attribute vec3 color; attribute float size; void main() { vColor = color; gl_PointSize = size; gl_Position = projectionMatrix * modelViewMatrix * vec4( position , 0, 1 ); }`, fragmentShader:` precision highp float; varying vec3 vColor; void main() { gl_FragColor = vec4( vColor, 1.0 ) ; }` }) ); scene.add( points ); and my using of raycaster: function mouseDown(e) { e.preventDefault(); var mouse = new THREE.Vector2(); mouse.x = ( event.clientX / window.innerWidth ) * 2 - 1; mouse.y = - ( event.clientY / window.innerHeight ) * 2 + 1; // mouse.z = 0; raycaster.setFromCamera(mouse, camera); raycaster.far = camera.position.z + 3; const intersect = raycaster.intersectObject(points); console.log(intersect); if (intersect.length > 0) { controls.enabled = false; console.log(intersect); selection = intersect[0].index; } } function mouseUp(e) { controls.enabled = true; var vector = new THREE.Vector3(); vector.x = (( event.clientX / window.innerWidth ) * 2 - 1); vector.y = (- ( event.clientY / window.innerHeight ) * 2 + 1); vector.z = 1.0; console.log(camera.position.z); vector.unproject( camera ); var dir = vector.sub( camera.position ).normalize(); var distance = - camera.position.z / dir.z; var temp = camera.position.clone().add( dir.multiplyScalar( distance ) ); var pos = points.geometry.attributes.position; pos.setXY(selection, temp.x, temp.y); pos.updateRange.offset = selection; // where to start updating pos.updateRange.count = 1; // how many vertices to update pos.needsUpdate = true; selection = undefined; }
three.js and confetti
I'm trying to make a confetti explosion and I'm having issues with projecting the confetti out. My idea is to have a fast explosion outwards in all directions (1 sec) then the confetti floats to the ground. I'm sure my math is wrong because I'm not getting it to expand. I've taken three.js code and made some mods: http://givememypatientinfo.com/ParticleBlocksConfetti.html Any suggestions are welcome. I'm a noob at the three.js... but love the library! Code: var container, stats; var camera, controls, scene, projector, renderer; var objects = [], plane; var vel = 1; var vel2 = 0.01; var accel = .3; var accel2 = -.9; var force = 1; var frame = 0; var mouse = new THREE.Vector2(), offset = new THREE.Vector3(), INTERSECTED, SELECTED; init(); animate(); function init() { container = document.createElement( 'div' ); document.body.appendChild( container ); camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 10000 ); camera.position.z = 1000; /*//controls = new THREE.TrackballControls( camera ); controls.rotateSpeed = 1.0; controls.zoomSpeed = 1.2; controls.panSpeed = 0.8; controls.noZoom = false; controls.noPan = false; controls.staticMoving = true; controls.dynamicDampingFactor = 0.3;*/ scene = new THREE.Scene(); scene.add( new THREE.AmbientLight( 0x505050 ) ); var light = new THREE.SpotLight( 0xffffff, 1.5 ); light.position.set( 0, 500, 2000 ); light.castShadow = true; light.shadowCameraNear = 200; light.shadowCameraFar = camera.far; light.shadowCameraFov = 50; light.shadowBias = -0.00022; light.shadowDarkness = 0.5; light.shadowMapWidth = 2048; light.shadowMapHeight = 2048; scene.add( light ); var geometry = new THREE.CubeGeometry( 40, 40, 40 ); //make confetti for particle system for ( var i = 0; i < 100; i ++ ) { var object = new THREE.Mesh( geometry, new THREE.MeshLambertMaterial( { color: Math.random() * 0xffffff } ) ); //object.material.ambient = object.material.color; /*object.position.x = Math.random() * 500 - 100; object.position.y = Math.random() * 500 - 100; object.position.z = 300;*/ object.position.x = Math.random() * 100 - 100; object.position.y = Math.random() * 100 - 100; object.position.z = 300; object.rotation.x = Math.random() * 2 * Math.PI; object.rotation.y = Math.random() * 2 * Math.PI; object.rotation.z = Math.random() * 2 * Math.PI; object.scale.x = .1; object.scale.y = Math.random() * .8 + .1; object.scale.z = Math.random() * .5 + .1; object.castShadow = false; object.receiveShadow = true; scene.add( object ); objects.push( object ); } plane = new THREE.Mesh( new THREE.PlaneGeometry( 2000, 2000, 8, 8 ), new THREE.MeshBasicMaterial( { color: 0x000000, opacity: 0.25, transparent: true, wireframe: true } ) ); plane.visible = false; scene.add( plane ); projector = new THREE.Projector(); renderer = new THREE.WebGLRenderer( { antialias: true } ); renderer.sortObjects = false; renderer.setSize( window.innerWidth, window.innerHeight ); renderer.shadowMapEnabled = true; renderer.shadowMapType = THREE.PCFShadowMap; container.appendChild( renderer.domElement ); var info = document.createElement( 'div' ); info.style.position = 'absolute'; info.style.top = '10px'; info.style.width = '100%'; info.style.textAlign = 'center'; info.innerHTML = 'three.js webgl - draggable cubes'; container.appendChild( info ); } function animate_particles(frame) { //will update each particle if (frame < 50){ var pCount = objects.length-1; if (frame < 40){ vel += accel*2; }else { vel = vel + accel2; } while(pCount > -1) { if (frame < 30){ objects[pCount].position.y += vel; }else{ objects[pCount].position.y -= vel; } //objects[pCount].rotation.x += Math.random()*.7; //objects[pCount].rotation.z += Math.random()*.01; //objects[pCount].rotation.y += Math.random()*.01; pCount--; } } } function animate() { requestAnimationFrame( animate ); animate_particles(frame); render(); //stats.update(); } function render() { renderer.render( scene, camera ); frame++; } </script>
This could be what you were trying to archieve. I modified your code a little bit and commented the changes. Basically I just added a random direction vector, normalized it and added a random speed to the particles. In the animate_particles function, I am moving the confetti along the random direction vector at the random speed. var container, stats; var camera, controls, scene, projector, renderer; var objects = [], plane; var vel = 1; var vel2 = 0.01; var accel = .3; var accel2 = -.9; var force = 1; var frame = 0; var mouse = new THREE.Vector2(), offset = new THREE.Vector3(), INTERSECTED, SELECTED; init(); animate(); function init() { container = document.createElement( 'div' ); document.body.appendChild( container ); camera = new THREE.PerspectiveCamera( 70, window.innerWidth / window.innerHeight, 1, 10000 ); camera.position.z = 1000; /*//controls = new THREE.TrackballControls( camera ); controls.rotateSpeed = 1.0; controls.zoomSpeed = 1.2; controls.panSpeed = 0.8; controls.noZoom = false; controls.noPan = false; controls.staticMoving = true; controls.dynamicDampingFactor = 0.3;*/ scene = new THREE.Scene(); scene.add( new THREE.AmbientLight( 0x505050 ) ); var light = new THREE.SpotLight( 0xffffff, 1.5 ); light.position.set( 0, 500, 2000 ); light.castShadow = true; light.shadowCameraNear = 200; light.shadowCameraFar = camera.far; light.shadowCameraFov = 50; light.shadowBias = -0.00022; light.shadowDarkness = 0.5; light.shadowMapWidth = 2048; light.shadowMapHeight = 2048; scene.add( light ); var geometry = new THREE.CubeGeometry( 40, 40, 40 ); //make confetti for particle system for ( var i = 0; i < 100; i ++ ) { var object = new THREE.Mesh( geometry, new THREE.MeshLambertMaterial( { color: Math.random() * 0xffffff } ) ); //object.material.ambient = object.material.color; /*object.position.x = Math.random() * 500 - 100; object.position.y = Math.random() * 500 - 100; object.position.z = 300;*/ object.position.x = Math.random() * 100 - 100; object.position.y = Math.random() * 100 - 100; object.position.z = 300; object.rotation.x = Math.random() * 2 * Math.PI; object.rotation.y = Math.random() * 2 * Math.PI; object.rotation.z = Math.random() * 2 * Math.PI; object.scale.x = .1; object.scale.y = Math.random() * .8 + .1; object.scale.z = Math.random() * .5 + .1; // give every "particle" a random expanding direction vector and normalize it to receive a length of 1. object.directionVector = new THREE.Vector3( Math.random() - .5, Math.random() - .5, Math.random() - .5 ) object.directionVector.normalize(); // and a random expanding Speed object.expandingSpeed = Math.random() * 100; object.castShadow = false; object.receiveShadow = true; scene.add( object ); objects.push( object ); } plane = new THREE.Mesh( new THREE.PlaneGeometry( 2000, 2000, 8, 8 ), new THREE.MeshBasicMaterial( { color: 0x000000, opacity: 0.25, transparent: true, wireframe: true } ) ); plane.visible = false; scene.add( plane ); projector = new THREE.Projector(); renderer = new THREE.WebGLRenderer( { antialias: true } ); renderer.sortObjects = false; renderer.setSize( window.innerWidth, window.innerHeight ); renderer.shadowMapEnabled = true; renderer.shadowMapType = THREE.PCFShadowMap; container.appendChild( renderer.domElement ); var info = document.createElement( 'div' ); info.style.position = 'absolute'; info.style.top = '10px'; info.style.width = '100%'; info.style.textAlign = 'center'; info.innerHTML = 'three.js webgl - draggable cubes'; container.appendChild( info ); } function animate_particles(frame) { //will update each particle if (frame < 50){ var pCount = objects.length-1; if (frame < 40){ vel += accel*2; }else { vel = vel + accel2; } while(pCount > -1) { if (frame < 30){ // commented that out. not sure why you put it there. //objects[pCount].position.y += vel; // move objects along random direction vector at the individual random speed. objects[pCount].position.x += objects[pCount].directionVector.x * objects[pCount].expandingSpeed; objects[pCount].position.y += objects[pCount].directionVector.y * objects[pCount].expandingSpeed; objects[pCount].position.z += objects[pCount].directionVector.z * objects[pCount].expandingSpeed; }else{ objects[pCount].position.y -= vel; } //objects[pCount].rotation.x += Math.random()*.7; //objects[pCount].rotation.z += Math.random()*.01; //objects[pCount].rotation.y += Math.random()*.01; pCount--; } } } function animate() { requestAnimationFrame( animate ); animate_particles(frame); render(); //stats.update(); } function render() { renderer.render( scene, camera ); frame++; }
I've just made a plugin for create Confetti Effect with THREE.js: https://github.com/mrgoonie/three.confetti.explosion.js Things would be easier with: var confetti = new ExplosionConfetti({ rate: 1, // percent of explosion in every tick - smaller is fewer - be careful, larger than 10 may crash your browser! amount: 200, // max amount particle of an explosion radius: 800, // max radius of an explosion areaWidth: 500, // width of the area areaHeight: 500, // height of the area fallingHeight: 500, // start exploding from Y position fallingSpeed: 1, // max falling speed colors: [0xffffff, 0xff0000, 0xffff00] // random colors }); scene.add( confetti.object ); Cheers,