<div id="canvasLeftId">
      <h3>Three js Canvas</h3>
      <button id="drawing-mode" class="btn btn-info">Cancel drawing mode</button>
      <div id="canvasForDrawingThreejsScene_Id"></div>
    </div>

    <div id="canvasRightId">
      <h3>Fabric js Canvas</h3>
      <canvas id="canvasForDrawingFabricjs_Id" width="256" height="256"></canvas>
    </div>

    <div id="div2Id">
      <h3>Hidden Canvas for Fabric js lower and Upper Canvases</h3>
      <canvas id="offscreenCanvasForDrawingTheFabricjsUpperAndLowerCanvas_Id" width="256" height="256"></canvas>
    </div>
 
html, body {
    width: 100%;
    height: 100%;
}

body {
    background: #FFF7F4;
}

#canvasLeftId {
    position: relative;
    display: inline-block;
    height: 100%;
    width: 50%;
}

#canvasRightId {
    float: right;
    position: relative;
    display: inline-block;
    height: 100%;
    width: 50%;
}

#canvasForDrawingThreejsScene_Id {
    position: relative;
    height: 256px;
    width: 256px;
}

#canvasForDrawingFabricjs_Id {
    height: 100%;
    width: 100%;
}

#div2Id {
    display: none;
}
/**
 * Fabric.js
 */

// https://stackoverflow.com/questions/60350747/fabric-js-patterns-how-create-pattern-for-canvas-background-from-rects-without
console.log('fabric.devicePixelRatio', fabric.devicePixelRatio);
// fabric.devicePixelRatio = Math.max(Math.floor(fabric.devicePixelRatio), 1);
fabric.devicePixelRatio = 1;
console.log('fabric.devicePixelRatio2', fabric.devicePixelRatio);

var fabricCanvas = new fabric.Canvas('canvasForDrawingFabricjs_Id');
fabricCanvas.backgroundColor = '#FFBE9F';
fabricCanvas.freeDrawingBrush = new fabric['PencilBrush'](fabricCanvas);
fabricCanvas.isDrawingMode = true;
fabricCanvas.setDimensions({width:256, height:256});
console.log('fabricCanvas.lowerCanvasEl.width', fabricCanvas.lowerCanvasEl.width);
console.log('fabricCanvas.upperCanvasEl.width', fabricCanvas.upperCanvasEl.width);
console.log('fabricCanvas.upperCanvasEl.height', fabricCanvas.upperCanvasEl.height);
   
var rectangle = new fabric.Rect({
    top: 100,
    left: 100,
    fill: '#FF6E27',
    width: 100,
    height: 100,
    transparentCorners: false,
    centeredScaling: true,
});

fabricCanvas.add(rectangle);

/**
 * Threejs
 */

var camera, renderer, canvasForDrawingThreejsSceneEl, scene, texture, material, geometry, cube;

var raycaster = new THREE.Raycaster();
var mouse = new THREE.Vector2();
var clickedPointCoords = new THREE.Vector2();

init();
animate();

/**
 * Configurator init function
 */

var drawingModeEl = document.getElementById('drawing-mode');
drawingModeEl.onclick = function() {
    fabricCanvas.isDrawingMode = !fabricCanvas.isDrawingMode;
    if (fabricCanvas.isDrawingMode) {
        drawingModeEl.innerHTML = 'Cancel drawing mode';
    }
    else {
        drawingModeEl.innerHTML = 'Enter drawing mode';
    }
};

function init() {
    /**
   * Camera
   */

    camera = new THREE.PerspectiveCamera(
        30,
        window.innerWidth / window.innerHeight,
        0.01,
        100
    );
    camera.position.set(0, 0, 3.5);

    /**
   * Canvas0
   */

    canvasForDrawingThreejsSceneEl = document.getElementById('canvasForDrawingThreejsScene_Id');
    
    renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(window.devicePixelRatio);

    renderer.setSize(canvasForDrawingThreejsSceneEl.clientWidth, canvasForDrawingThreejsSceneEl.clientHeight);
    camera.aspect = canvasForDrawingThreejsSceneEl.clientWidth / canvasForDrawingThreejsSceneEl.clientHeight;

    camera.updateProjectionMatrix();
    canvasForDrawingThreejsSceneEl.appendChild(renderer.domElement);

    /**
   * Scene
   */

    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x000000);

    /**
   * Texture and material
   */

    texture = new THREE.Texture(document.getElementById('canvasForDrawingFabricjs_Id'));
    texture.anisotropy = renderer.capabilities.getMaxAnisotropy();

    material = new THREE.MeshBasicMaterial({ map: texture });

    /**
   * Model
   */

    geometry = new THREE.BoxGeometry(1, 1, 1);
    cube = new THREE.Mesh(geometry, material);
    cube.rotation.x = 0.5;
    cube.rotation.y = 0.3;
    scene.add(cube);
}

/**
 * Configurator frame render function
 */

function animate() {
    requestAnimationFrame(animate);

    cube.rotation.x += 0.004;
    cube.rotation.y += 0.001;
    // cube.rotation.x += 0.000;
    // cube.rotation.y += 0.000;
    texture.needsUpdate = true;

    // console.log('scene.children[0].material.map.image', scene.children[0].material.map.image);
    renderer.render(scene, camera);
}

/**
 * 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 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 = getPositionOnScene(canvasForDrawingThreejsSceneEl, e);
        // console.log('positionOnScene:', positionOnScene);
        pointer.x = positionOnScene.x;
        pointer.y = positionOnScene.y;
        // console.log('pointer', pointer);
    }
    /* END PATCH CODE */

    // console.log('pointer1:', pointer);
    if (!ignoreZoom) {
        pointer = this.restorePointerVpt(pointer);
    }

    if (boundsWidth === 0 || boundsHeight === 0) {
        // console.log('bar2');
        cssScale = { width: 1, height: 1 };
    }
    else {
        // console.log('bar3');
        cssScale = {
            width: upperCanvasEl.width / boundsWidth,
            height: upperCanvasEl.height / boundsHeight
        };
    }

    return {
        x: pointer.x * cssScale.width,
        y: pointer.y * cssScale.height
    };
};

fabric.Object.prototype._drawControl =    function(control, ctx, methodName, left, top, styleOverride) {
    // console.log('bar1');
    // console.log('control', control);
    styleOverride = styleOverride || {};
    if (!this.isControlVisible(control)) {
        return;
    }
    // console.log('control', control);
    // console.log('bar2');
    var size = this.cornerSize, stroke = !this.transparentCorners && this.cornerStrokeColor;
    switch (styleOverride.cornerStyle || this.cornerStyle) {
        case 'rect':
            if(control == this.__corner){
                ctx.save();
                // console.log('set control to red');
        	ctx.strokeStyle = ctx.fillStyle='red';
            }
            else {
            // console.log('set control to black');
          	ctx.strokeStyle = ctx.fillStyle='black';
              
            }
            ctx.beginPath();
            ctx.arc(left + size / 2, top + size / 2, size / 2, 0, 2 * Math.PI, false);
            ctx[methodName]();
            if (stroke) {
                ctx.stroke();
            }
            if(control == this.__corner){
                ctx.restore();
        	
            }
            break;
        default:
            this.transparentCorners || ctx.clearRect(left, top, size, size);
            ctx[methodName + 'Rect'](left, top, size, size);
            if (stroke) {
                ctx.strokeRect(left, top, size, size);
            }
    }
}; 

/**
 * Listeners
 */

canvasForDrawingThreejsSceneEl.addEventListener('mousedown', onMouseDown111, false);
canvasForDrawingThreejsSceneEl.addEventListener('touchstart', onTouchStart111, false);

var offscreenCanvasEl;

// cRightEl = document.getElementById('canvasRightId');
let cRightEl = document;

cRightEl.addEventListener('mousedown', onMouseDown222, true);
cRightEl.addEventListener('touchstart', onTouchStart222, false);

function drawOffscreen(){
    var upperCanvas1 = fabricCanvas.upperCanvasEl;
    var lowerCanvas1 = fabricCanvas.lowerCanvasEl;

    // grab the context from your destination canvas
    var destCtx = offscreenCanvasEl.getContext('2d');

    // call its drawImage() function passing it the source canvas directly
    destCtx.drawImage(lowerCanvas1, 0, 0);
    destCtx.drawImage(upperCanvas1, 0, 0);
}


// changes texture pointer
function onMouseDown222(ev){
    cRightEl.addEventListener('mouseup', onMouseUp222, true);
    if(fabricCanvas.isDrawingMode){
        cRightEl.addEventListener('mousemove', onMouseMove222, true);
        offscreenCanvasEl = document.getElementById('offscreenCanvasForDrawingTheFabricjsUpperAndLowerCanvas_Id');
        offscreenCanvasEl.width = fabricCanvas.width;
        offscreenCanvasEl.height = fabricCanvas.height;
        // console.log('offscreenCanvasEl.width2', offscreenCanvasEl.width);
        material.map.image = offscreenCanvasEl;
    }
}

function onMouseMove222(ev){
    if(fabricCanvas.isDrawingMode){
	    drawOffscreen();
    }
}

// returns original canvas pointer
function onMouseUp222(ev){
    cRightEl.removeEventListener('mouseup', onMouseUp222, true);
    if(fabricCanvas.isDrawingMode){
        let gg = document.getElementById('canvasForDrawingFabricjs_Id');
        material.map.image = gg;
        cRightEl.removeEventListener('mousemove', onMouseMove222, true);
    }
}

function onTouchStart222(ev){
    cRightEl.addEventListener('touchend', onTouchEnd222, true);
    if(fabricCanvas.isDrawingMode){
        cRightEl.addEventListener('touchmove', onTouchMove222, true);
        offscreenCanvasEl = document.getElementById('offscreenCanvasForDrawingTheFabricjsUpperAndLowerCanvas_Id');
        offscreenCanvasEl.width = fabricCanvas.width;
        offscreenCanvasEl.height = fabricCanvas.height;
        console.log('offscreenCanvasEl.width2', offscreenCanvasEl.width);
        material.map.image = offscreenCanvasEl;
    }
}

function onTouchMove222(ev){
    if(fabricCanvas.isDrawingMode){
	    drawOffscreen();
    }
}

function onTouchEnd222(ev){
    cRightEl.removeEventListener('touchend', onTouchEnd222, true);
    if(fabricCanvas.isDrawingMode){
        let gg = document.getElementById('canvasForDrawingFabricjs_Id');
        material.map.image = gg;
        cRightEl.removeEventListener('touchmove', onTouchMove222, true);
    }
}

/**
 * Event handler
 */

function onMouseDown111(evt) {
    evt.preventDefault();
    const positionOnScene = getPositionOnScene(canvasForDrawingThreejsSceneEl, evt);
    if (positionOnScene) {
        const canvasRect = fabricCanvas._offset;
        const simEvt = new MouseEvent(evt.type, {
            clientX: canvasRect.left + positionOnScene.x,
            clientY: canvasRect.top + positionOnScene.y
        });
        fabricCanvas.upperCanvasEl.dispatchEvent(simEvt);
    }
}

function onTouchStart111(evt) {
    evt.preventDefault();
    const positionOnScene = getPositionOnScene(canvasForDrawingThreejsSceneEl, evt);
    if (positionOnScene) {
        const canvasRect = fabricCanvas._offset;
        const simEvt = new TouchEvent(evt.type, {
            clientX: canvasRect.left + positionOnScene.x,
            clientY: canvasRect.top + positionOnScene.y
        });
        fabricCanvas.upperCanvasEl.dispatchEvent(simEvt);
    }
}

/**
 * Three.js Helper functions
 */
function getPositionOnScene(sceneContainer, evt) {
    
    // console.log('evt', evt);
    let clientX = 0;
    let clientY = 0;
    if(evt instanceof MouseEvent) {
        clientX = evt.clientX;
        clientY = evt.clientY;
    }
    else if(evt instanceof TouchEvent) {
        clientX = event.changedTouches[0].clientX;
        clientY = event.changedTouches[0].clientY;
    }
    else{
        throw new Error('event type is not supported');
    }

    // console.log('clientX', clientX);
    // console.log('clientY', clientY);

    var array = getMousePosition(canvasForDrawingThreejsSceneEl, clientX, clientY);
    clickedPointCoords.fromArray(array);
    // console.log('clickedPointCoords', clickedPointCoords);

    var intersects = getIntersects(clickedPointCoords, scene.children);
    if (intersects.length > 0 && intersects[0].uv) {
        var uv = intersects[0].uv;
        // console.log('intersects[0].uv: ', intersects[0].uv);
        // console.log('uv: ', uv);
        intersects[0].object.material.map.transformUv(uv);
        // console.log('uv.x', uv.x);
        // console.log('uv.y', uv.y);
        let x1 = getRealPosition('x', uv.x);
        let y1 = getRealPosition('y', uv.y); 
        // console.log('x1', x1);
        // console.log('y1', y1);

        // let x2 = uv.x * canvasForDrawingThreejsSceneEl.clientHeight;
        // let y2 = uv.y * canvasForDrawingThreejsSceneEl.clientWidth;
        // console.log('x2', x2);
        // console.log('y2', y2);

        return {
            x: x1,
            y: y1
        };
    }
    return null;
}

function getRealPosition(axis, value) {
    let CORRECTION_VALUE = axis === 'x' ? 4.5 : 5.5;

    return Math.round(value * canvasForDrawingThreejsSceneEl.clientHeight) - CORRECTION_VALUE;
}

var getMousePosition = function(dom, x, y) {
    var rect = dom.getBoundingClientRect();
    // console.log('rect', rect);
    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);
};
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/three.js/97/three.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.0/fabric.js