Hi there, I’m making a t-shirt configurator with three and fabric.js, Bit there is one problem, When i click on the t-shirt model to place an image on the mouse position it is not being perfectly place, Can anyone help me in this issue?
here is the model:
Tshirt.glb (1.2 MB)
here is the code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<style>
* {
margin: 0;
padding: 0;
background: rgb(0, 0, 0);
}
#wiki {
display: none;
}
#canvas_container {
display: none;
}
</style>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/90/three.min.js"></script>
<!-- Include other Three.js loaders -->
<script src="GLTFLoader.js"></script>
<script src="OrbitControls.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dat-gui/0.7.1/dat.gui.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.2.0/fabric.min.js"></script>
<script type="text/javascript"
src="https://custom.speedjersey.com/designer/assets/javascripts/three/OBJLoader2.js"></script>
<script type="text/javascript"
src="https://custom.speedjersey.com/designer/assets/javascripts/three/SVGLoader.js"></script>
<canvas style="display: none;" id="canvas" height="1024" width="1024"></canvas>
<img crossorigin="anonymous" id="wiki"
src="https://fiverr-res.cloudinary.com/t_profile_original,q_auto,f_auto/attachments/profile/photo/19d960fe641d49e4c77288d4b5e90283-1690885352403/9c2d1c91-ea9a-4f74-822f-3f27f1b6f0c7.jpg"
width="100" height="100" />
<img crossorigin="anonymous" id="wiki1" src="https://www.imgacademy.com/sites/default/files/2009-stadium-about.jpg"
width="20" height="20" />
<div id="renderer">
</div>
<br />
<div id="canvas_container">
<canvas id="cnvs"></canvas>
</div>
<script>
/**
* FabricJS
**/
/**
* FabricJS
**/
var canvas = new fabric.Canvas("cnvs", { width: window.innerWidth, height: window.innerHeight });
window.addEventListener('resize', function () {
canvas.setDimensions({ width: window.innerWidth, height: window.innerHeight });
});
canvas.backgroundColor = "white";
canvas.on("after:render", function () {
if (model) {
canvasTexture.needsUpdate = true;
}
});
var text = new fabric.IText('Three.js\n', {
fontSize: 40,
textAlign: 'center',
fontWeight: 'bold',
left: 128,
top: 128,
angle: 0,
originX: 'center',
originY: 'center',
shadow: 'blue -5px 6px 5px'
});
canvas.add(text);
var imgElement = document.getElementById("wiki1");
var imageinstance = new fabric.Image(imgElement, {
angle: 0,
left: 0,
opacity: 1,
cornerSize: 10,
});
canvas.add(imageinstance);
/**
* ThreeJS
**/
var containerHeight = "512";
var containerWidth = "512";
var camera, renderer, container, scene, texture, material, geometry;
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 1000);
camera.position.set(0, 0, 10);
scene.background = new THREE.Color("#4C489B");
/* Raycaster */
var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
var onClickPosition = new THREE.Vector2();
var isMobile = false;
/**
** Renderer
**/
container = document.getElementById("renderer");
renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
camera.updateProjectionMatrix();
container.appendChild(renderer.domElement);
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
var width = window.innerWidth;
var height = window.innerHeight;
var canvasPixelWidth = canvas.width / window.devicePixelRatio;
var canvasPixelHeight = canvas.height / window.devicePixelRatio;
const needResize = canvasPixelWidth !== width || canvasPixelHeight !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
/*
* End Renderer
*/
var controls = new THREE.OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // an animation loop is required when either damping or auto-rotation are enabled
controls.dampingFactor = 0.5;
controls.screenSpacePanning = false;
controls.maxPolarAngle = Math.PI / 2;
var light = new THREE.DirectionalLight(0xffffff, 0.5);
light.position.setScalar(10);
scene.add(light);
scene.add(new THREE.AmbientLight(0xffffff, 1));
var canvasTexture = new THREE.CanvasTexture(cnvs);
canvasTexture.flipY = false;
canvasTexture.wrapT = THREE.RepeatWrapping;
canvasTexture.minFilter = THREE.LinearFilter;
canvasTexture.generateMipmaps = false;
canvasTexture.wrapT = THREE.ClampToEdgeWrapping;
canvasTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
canvasTexture.magFilter = THREE.NearestFilter;
canvasTexture.anisotropy = renderer.capabilities.getMaxAnisotropy();
canvasTexture.needsUpdate = true;
var newMaterial = new THREE.MeshPhysicalMaterial({});
var loader = new THREE.GLTFLoader();
var model;
loader.load('Tshirt.glb', function (gltf) {
let initialAngle = scene.rotation.y;
model = gltf.scene;
// Set the canvas texture to the material of the 3D model
model.traverse(function (child) {
if (child.isMesh) {
// Check if the mesh has a name that corresponds to the type
if (child.name.includes(scene.name)) {
// Ensure the material is MeshBasicMaterial for texture mapping
if (child.material instanceof THREE.MeshStandardMaterial) {
child.material.map = canvasTexture;
child.material.side = THREE.DoubleSide; // Ensure both sides of the geometry are rendered
child.material.needsUpdate = true;
} else {
// If the material is not MeshBasicMaterial, create a new one
const newMaterial = new THREE.MeshStandardMaterial({
map: canvasTexture,
side: THREE.DoubleSide,
});
child.material = newMaterial;
}
// Assuming the UV mapping is correct, no additional adjustments needed
child.nameID = scene.name; // Set a new property to identify this object
}
}
});
model.scale.set(6, 6, 6);
model.position.set(0, -8, 0)
// Add the loaded 3D model to the scene
scene.add(model);
});
// var geometry = new THREE.PlaneGeometry(20, 20, 30, 30);
// // geometry.vertices.forEach(v => {
// // v.z = Math.cos(v.x) * Math.sin(-v.y * 1) * 1;
// // });
// // geometry.computeFaceNormals();
// // geometry.computeVertexNormals();
// var mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({
// map: canvasTexture,
// metalness: 0.25,
// roughness: 0.25,
// magFilter: THREE.NearestFilter, // Use NearestFilter for magnification
// minFilter: THREE.LinearMipmapLinearFilter, // Use LinearMipmapLinearFilter for minification
// anisotropy: renderer.capabilities.getMaxAnisotropy(), // Anisotropic filtering
// }));
// scene.add(mesh);
function animateRandom() {
var randomX = THREE.Math.randInt(50, 206);
var randomY = THREE.Math.randInt(10, 206);
}
animateRandom();
setInterval(animateRandom, 1000);
var clock = new THREE.Clock();
var time = 0;
// controls.update();
function render() {
requestAnimationFrame(render);
time += clock.getDelta();
renderer.render(scene, camera);
}
render();
/**
* Fabric.js patch
*/
fabric.Canvas.prototype.getPointer = function (e, ignoreZoom) {
if (this._absolutePointer && !ignoreZoom) {
return this._absolutePointer;
}
if (this._pointer && ignoreZoom) {
return this._pointer;
}
var simEvt;
if (e.touches != undefined) {
simEvt = new MouseEvent({
touchstart: "mousedown",
touchmove: "mousemove",
touchend: "mouseup"
}[e.type], {
bubbles: true,
cancelable: true,
view: window,
detail: 1,
screenX: Math.round(e.changedTouches[0].screenX),
screenY: Math.round(e.changedTouches[0].screenY),
clientX: Math.round(e.changedTouches[0].clientX),
clientY: Math.round(e.changedTouches[0].clientY),
ctrlKey: false,
altKey: false,
shiftKey: false,
metaKey: false,
button: 0,
relatedTarget: null
});
var pointer = fabric.util.getPointer(simEvt),
upperCanvasEl = this.upperCanvasEl,
bounds = upperCanvasEl.getBoundingClientRect(),
boundsWidth = bounds.width || 0,
boundsHeight = bounds.height || 0,
cssScale;
} else {
var pointer = fabric.util.getPointer(e),
upperCanvasEl = this.upperCanvasEl,
bounds = upperCanvasEl.getBoundingClientRect(),
boundsWidth = bounds.width || 0,
boundsHeight = bounds.height || 0,
cssScale;
}
if (!boundsWidth || !boundsHeight) {
if ('top' in bounds && 'bottom' in bounds) {
boundsHeight = Math.abs(bounds.top - bounds.bottom);
}
if ('right' in bounds && 'left' in bounds) {
boundsWidth = Math.abs(bounds.right - bounds.left);
}
}
this.calcOffset();
pointer.x = pointer.x - this._offset.left;
pointer.y = pointer.y - this._offset.top;
/* BEGIN PATCH CODE */
if (e.target !== this.upperCanvasEl) {
var positionOnScene;
if (isMobile == true) {
positionOnScene = getPositionOnSceneTouch(container, e);
if (positionOnScene) {
console.log(positionOnScene);
pointer.x = positionOnScene.x;
pointer.y = positionOnScene.y;
}
} else {
positionOnScene = getPositionOnScene(container, e);
if (positionOnScene) {
console.log(positionOnScene);
pointer.x = positionOnScene.x;
pointer.y = positionOnScene.y;
}
}
}
/* END PATCH CODE */
if (!ignoreZoom) {
pointer = this.restorePointerVpt(pointer);
}
if (boundsWidth === 0 || boundsHeight === 0) {
cssScale = { width: 1, height: 1 };
}
else {
cssScale = {
width: upperCanvasEl.width / boundsWidth,
height: upperCanvasEl.height / boundsHeight
};
}
return {
x: pointer.x * cssScale.width,
y: pointer.y * cssScale.height
};
}
/**
* Listeners
*/
container.addEventListener("mousedown", onMouseEvt, false);
if (
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|ipad|iris|kindle|Android|Silk|lge |maemo|midp|mmp|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows (ce|phone)|xda|xiino/i.test(
navigator.userAgent,
) ||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
navigator.userAgent.substr(0, 4),
)
) {
isMobile = true;
container.addEventListener("touchstart", onTouch, false);
}
/**
* Event handler
*/
function onTouch(evt) {
evt.preventDefault();
const positionOnScene = getPositionOnSceneTouch(container, evt);
if (positionOnScene) {
const canvasRect = canvas._offset;
const simEvt = new MouseEvent(evt.type, {
clientX: canvasRect.left + positionOnScene.x,
clientY: canvasRect.top + positionOnScene.y,
});
canvas.upperCanvasEl.dispatchEvent(simEvt);
}
}
function getPositionOnScene(sceneContainer, evt) {
var array = getMousePosition(container, evt.clientX, evt.clientY);
onClickPosition.fromArray(array);
var intersects = getIntersects(onClickPosition, scene.children);
if (intersects.length > 0 && intersects[0].uv) {
var uv = intersects[0].uv;
intersects[0].object.material.map.transformUv(uv);
// Get the canvas position based on UV coordinates
var canvasX = Math.round(uv.x * canvas.width);
var canvasY = Math.round(uv.y * canvas.height);
// Get the intersection point on the canvas using the raycaster
var intersectionPoint = intersects[0].point;
var canvasPosition = convertWorldToCanvas(intersectionPoint);
return {
x: canvasPosition.x,
y: canvasPosition.y,
};
}
return null;
}
function convertWorldToCanvas(worldPosition) {
var vector = worldPosition.clone();
vector.project(camera);
vector.x = Math.round((vector.x + 1) / 2 * canvas.width);
vector.y = Math.round((-vector.y + 1) / 2 * canvas.height);
return vector;
}
function onMouseEvt(evt) {
evt.preventDefault();
const positionOnScene = getPositionOnScene(container, evt)
if (positionOnScene) {
const canvasRect = canvas._offset;
const simEvt = new MouseEvent(evt.type, {
clientX: canvasRect.left + positionOnScene.x,
clientY: canvasRect.top + positionOnScene.y
});
console.log(simEvt);
canvas.upperCanvasEl.dispatchEvent(simEvt);
}
}
/**
* Add a flag to track whether an image is being dragged
*/
let isDragging = false;
/**
* Event handler
*/
container.addEventListener("mousedown", onMouseDown, false);
/**
* Modified onMouseDown function
*/
// Use Vector2 for onClickPosition
var onClickPosition = new THREE.Vector2();
function getRealPosition(axis, value) {
let CORRECTION_VALUE = axis === "x" ? 512 / 2 : 512 / 2;
return Math.round(value * 512) - CORRECTION_VALUE;
}
function onMouseDown(evt) {
evt.preventDefault();
const positionOnScene = getPositionOnScene(container, evt);
if (positionOnScene && model) {
const imgElement = document.getElementById("wiki");
const newImageInstance = new fabric.Image(imgElement, {
angle: 0,
left: positionOnScene.x,
top: positionOnScene.y,
opacity: 1,
cornerSize: 30,
});
// Convert screen coordinates to 3D world coordinates
const vector = new THREE.Vector3(positionOnScene.x, positionOnScene.y, 0.5);
vector.unproject(camera);
// Raycast to find intersection with the model
const raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
const intersects = raycaster.intersectObject(model, true);
if (intersects.length > 0) {
// Get the intersection point in world coordinates
const intersectionPoint = intersects[0].point;
// Project the 3D intersection point back to screen coordinates
const screenPosition = intersectionPoint.clone().project(camera);
// Calculate the offset between screen and fabric coordinates
const offset = {
x: positionOnScene.x - screenPosition.x * window.innerWidth,
y: positionOnScene.y - screenPosition.y * window.innerHeight,
};
// Set fabric image position considering the offset
newImageInstance.left = positionOnScene.x - offset.x;
newImageInstance.top = positionOnScene.y - offset.y;
newImageInstance.setCoords();
model.add(newImageInstance);
}
}
}
/**
* Event handler for mouseup
*/
container.addEventListener("mouseup", onMouseUp, false);
/**
* Modified onMouseUp function
*/
function onMouseUp(evt) {
// Reset the flag when the mouse button is released
isDragging = false;
}
/**
* Event handler for object:selected in Fabric.js
*/
canvas.on('object:selected', function (options) {
if (options.target && options.target.type === 'image') {
// Set the flag to indicate that an image is being dragged
isDragging = true;
}
});
/**
* Event handler for object:modified in Fabric.js
*/
canvas.on('object:modified', function (options) {
// Reset the flag after the image has been modified (including drag)
isDragging = false;
});
/**
* Modified onTouch function
*/
function onTouch(evt) {
// Check the flag before processing touch events
if (isDragging) {
return;
}
// ... (Your existing touch event handling code)
}
/**
* Three.js Helper functions
*/
function getPositionOnScene(sceneContainer, evt) {
console.log("Getting position on scene...");
var array = getMousePosition(sceneContainer, evt.clientX, evt.clientY);
onClickPosition.fromArray(array);
// Get all objects in the scene (including the 3D model and other elements)
var objects = [];
scene.traverse(function (child) {
if (child.isMesh) {
objects.push(child);
}
});
console.log("Number of objects in the scene:", objects.length);
var intersects = getIntersects(onClickPosition, objects);
if (intersects.length > 0) {
console.log("Intersection found:", intersects[0].point);
return intersects[0].point;
}
console.log("No intersection found.");
return null;
}
function getRealPosition(axis, value) {
let CORRECTION_VALUE = axis === "x" ? 4.5 : 5.5;
return Math.round(value * 512) - CORRECTION_VALUE;
}
var getMousePosition = function (dom, x, y) {
var rect = dom.getBoundingClientRect();
return [(x - rect.left) / rect.width, (y - rect.top) / rect.height];
};
var getIntersects = function (point, objects) {
mouse.set(point.x * 2 - 1, -(point.y * 2) + 1);
raycaster.setFromCamera(mouse, camera);
return raycaster.intersectObjects(objects);
};
</script>
</body>
</html>`