получить границы сложного объекта на изображении с белым фоном (js) - PullRequest
0 голосов
/ 09 мая 2018

Какой хороший способ получить границы для изображения (не само изображение, а небелые пиксели)? Я использую javascript, поэтому постарайтесь сохранить алгоритмы в этой области, если можете.

Например, как мне получить список / два списка всех точек x и y, где для точек, которые существуют на границе этого (группы) объекта (ов):

enter image description here

Обратите внимание, что внутренняя часть должна быть включена, но нехватка цвета, которая полностью внутри (как отверстие) должна быть исключена.

Следовательно, результатом будут два списка, которые содержат точки x и y (для пикселей), которые будут создавать объект, подобный следующему:

enter image description here

Ниже показано, как я "добился" этого. Хотя он работает для всех вогнутых объектов, если вы попытаетесь использовать его для более сложных объектов с некоторыми выпуклыми сторонами, он неизбежно потерпит неудачу.

Успех с вогнутым объектом

сниппет:

var _PI = Math.PI, _HALF_PI = Math.PI / 2, _TWO_PI = 2 * Math.PI;
var _radius = 10, _damp = 75, _center = new THREE.Vector3(0, 0, 0);
var _phi = _PI / 2, _theta = _theta = _PI / 7;
var _sceneScreenshot = null, _dirty = true;

var _tmpCan = document.createElement("canvas"),
	_tmpCtx = _tmpCan.getContext("2d");

var scene = document.getElementById("scene"),
	sw = scene.width, sh = scene.height;
var _scene = new THREE.Scene();
var _renderer = new THREE.WebGLRenderer({ canvas: scene, alpha: true, antialias: true });
_renderer.setPixelRatio(window.devicePixelRatio);
_renderer.setSize(sw, sh);
var _camera = new THREE.PerspectiveCamera(35, sw / sh, .1, 1000);

_tmpCan.width = sw; _tmpCan.height = sh;

_scene.add(new THREE.HemisphereLight(0x999999, 0x555555, 1));
_scene.add(new THREE.AmbientLight(0x404040));
var _camLight = new THREE.PointLight(0xdfdfdf, 1.8, 300, 2);
_scene.add(_camLight);

var geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
var material = new THREE.MeshPhysicalMaterial( { color: 0x2378d3, opacity: .7 } );
var cube = new THREE.Mesh( geometry, material );
_scene.add( cube );

function initialize() {
	document.body.appendChild(_tmpCan);
	_tmpCan.style.position = "absolute";
	_tmpCan.style.left = "8px";
	_tmpCan.style.top = "8px";
	_tmpCan.style.pointerEvents = "none";
	addListeners();
	updateCamera();
	animate();
}

function addListeners() {
	/* mouse events */

	var scene = document.getElementById("scene");

	scene.oncontextmenu = function(e) {
		e.preventDefault();
	}

	scene.onmousedown = function(e) {
		e.preventDefault();
		mouseTouchDown(e.pageX, e.pageY, e.button);
	}

	scene.ontouchstart = function(e) {
		if (e.touches.length !== 1) {
			return;
		}
		e.preventDefault();
		mouseTouchDown(e.touches[0].pageX, e.touches[0].pageY, e.touches.length, true);
	}

	function mouseTouchDown(pageX, pageY, button, touch) {
		_mouseX = pageX; _mouseY = pageY;
		_button = button;
		if (touch) {
			document.ontouchmove = function(e) {
				if (e.touches.length !== 1) {
					return;
				}
				mouseTouchMove(e.touches[0].pageX, e.touches[0].pageY, e.touches.length, true);
			}
			document.ontouchend = function() {
				document.ontouchmove = null;
				document.ontouchend = null;
			}
		} else {
			document.onmousemove = function(e) {
				mouseTouchMove(e.pageX, e.pageY, _button);
			}
			document.onmouseup = function() {
				document.onmousemove = null;
				document.onmouseup = null;
			}
		}
	}

	function mouseTouchMove(pageX, pageY, button, touch) {
		var dx = pageX - _mouseX,
			dy = pageY - _mouseY;

		_phi += dx / _damp;
		// _theta += dy / _damp;

		_phi %= _TWO_PI;
		if (_phi < 0) {
			_phi += _TWO_PI;
		}

		// var maxTheta = _HALF_PI - _HALF_PI * .8,
		// 	minTheta = -_HALF_PI + _HALF_PI * .8;

		// if (_theta > maxTheta) {
		// 	_theta = maxTheta;
		// } else if (_theta < minTheta) {
		// 	_theta = minTheta;
		// }

		updateCamera();
		_dirty = true;
		// updateLabels();
		_mouseX = pageX;
		_mouseY = pageY;
	}
}

function updateCamera() {

	// var radius = _radius + (Math.sin(_theta % _PI)) * 10;
	var radius = _radius;
	var y = radius * Math.sin(_theta),
		phiR = radius * Math.cos(_theta);
	var z = phiR * Math.sin(_phi),
		x = phiR * Math.cos(_phi);

	_camera.position.set(x, y, z);
	_camLight.position.set(x, y, z);
	_camera.lookAt(_center);

}

function updateLabels() {
	if (_sceneScreenshot === null) {
		return;
	}

	var tmpImg = new Image();
	tmpImg.onload = function() {
		_tmpCtx.drawImage(tmpImg, 0, 0, sw, sh);

		var imgData = _tmpCtx.getImageData(0, 0, sw, sh);
		var data = imgData.data;

		var firstXs = [];
		var lastXs = [];
		for (var y = 0; y < sh; y++) {
			var firstX = -1;
			var lastX = -1;
			for (var x = 0; x < sw; x++) {
				var i = (x + y * sw) * 4;
				var sum = data[i] + data[i + 1] + data[i + 2];
				if (firstX === -1) {
					if (sum > 3) {
						firstX = x;
					}
				} else {
					if (sum > 3) {
						lastX = x;
					}
				}
			}
			if (lastX === -1 && firstX >= 0) {
				lastX = firstX;
			}
			firstXs.push(firstX);
			lastXs.push(lastX);
		}
		var firstYs = [];
		var lastYs = [];
		for (var x = 0; x < sw; x++) {
			var firstY = -1;
			var lastY = -1;
			for (var y = 0; y < sh; y++) {
				var i = (x + y * sw) * 4;
				var sum = data[i] + data[i + 1] + data[i + 2];
				if (firstY === -1) {
					if (sum < 759) {
						firstY = y;
					}
				} else {
					if (sum < 759) {
						lastY = y;
					}
				}
			}
			if (lastY === -1 && firstY >= 0) {
				lastY = firstY;
			}
			firstYs.push(firstY);
			lastYs.push(lastY);
		}
		postLoad(firstXs, lastXs, firstYs, lastYs);
	}
	tmpImg.src = _sceneScreenshot;


	function postLoad(firstXs, lastXs, firstYs, lastYs) {

		_tmpCtx.clearRect(0, 0, sw, sh);

		_tmpCtx.beginPath();
		for (var y = 0; y < sh; y++) {
			_tmpCtx.moveTo(firstXs[y], y);
			_tmpCtx.lineTo(lastXs[y], y);
		}
		/* TODO REMOVE BELOW TODO */
		_tmpCtx.strokeStyle = 'black';
		console.log(_tmpCtx.globalAlpha);
		_tmpCtx.stroke();
		/* TODO REMOVE ABOVE TODO */

		_tmpCtx.beginPath();
		for (var x = 0; x < sw; x++) {
			_tmpCtx.moveTo(x, firstYs[x]);
			_tmpCtx.lineTo(x, lastYs[x]);
		}
		/* TODO REMOVE BELOW TODO */
		_tmpCtx.strokeStyle = 'black';
		_tmpCtx.stroke();
		/* TODO REMOVE ABOVE TODO */

		var imgData = _tmpCtx.getImageData(0, 0, sw, sh);
		var data = imgData.data;

		for (var i = 0, iLen = data.length; i < iLen; i += 4) {
			if (data[i + 3] < 200) {
				data[i + 3] = 0;
			}
			/* TODO remove v TODO */
			else { data[i + 3] = 120; }
		}
		_tmpCtx.putImageData(imgData, 0, 0);
	}

}

function animate () {
	cube.rotation.x += 0.001;
	cube.rotation.y += 0.001;

	_renderer.render(_scene, _camera);
	if (_dirty) {
		_sceneScreenshot = _renderer.domElement.toDataURL();
		updateLabels();
		_dirty = false;
	}

	requestAnimationFrame( animate );
}

initialize();
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/92/three.js"></script>

<canvas id="scene" width="400" height="300"></canvas>

Сбой со сложным объектом

var _PI = Math.PI, _HALF_PI = Math.PI / 2, _TWO_PI = 2 * Math.PI;
var _radius = 10, _damp = 75, _center = new THREE.Vector3(0, 0, 0);
var _phi = _PI / 2, _theta = _theta = 0;
var _sceneScreenshot = null, _dirty = true;

var _tmpCan = document.createElement("canvas"),
	_tmpCtx = _tmpCan.getContext("2d");

var scene = document.getElementById("scene"),
	sw = scene.width, sh = scene.height;
var _scene = new THREE.Scene();
var _renderer = new THREE.WebGLRenderer({ canvas: scene, alpha: true, antialias: true });
_renderer.setPixelRatio(window.devicePixelRatio);
_renderer.setSize(sw, sh);
var _camera = new THREE.PerspectiveCamera(35, sw / sh, .1, 1000);

_tmpCan.width = sw; _tmpCan.height = sh;

_scene.add(new THREE.HemisphereLight(0x999999, 0x555555, 1));
_scene.add(new THREE.AmbientLight(0x404040));
var _camLight = new THREE.PointLight(0xdfdfdf, 1.8, 300, 2);
_scene.add(_camLight);

var geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
var material = new THREE.MeshPhysicalMaterial( { color: 0x2378d3, opacity: .7 } );
var cube = new THREE.Mesh( geometry, material );
_scene.add( cube );

var geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
var material = new THREE.MeshPhysicalMaterial( { color: 0xc36843, opacity: .7 } );
var cube2 = new THREE.Mesh( geometry, material );
cube2.position.x = -.75;
cube2.position.y = .75
_scene.add( cube2 );

var geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
var material = new THREE.MeshPhysicalMaterial( { color: 0x43f873, opacity: .7 } );
var cube3 = new THREE.Mesh( geometry, material );
cube3.position.x = -.25;
cube3.position.y = 1.5;
_scene.add( cube3 );


var geometry = new THREE.BoxBufferGeometry( 1, 1, 1 );
var material = new THREE.MeshPhysicalMaterial( { color: 0x253621, opacity: .7 } );
var cube4 = new THREE.Mesh( geometry, material );
cube4.position.x = 1;
cube4.position.y = .35;
_scene.add( cube4 );

function initialize() {
	document.body.appendChild(_tmpCan);
	_tmpCan.style.position = "absolute";
	_tmpCan.style.left = "200px";
	_tmpCan.style.top = "0px";
	_tmpCan.style.pointerEvents = "none";
	addListeners();
	updateCamera();
	animate();
}

function addListeners() {
	/* mouse events */

	var scene = document.getElementById("scene");

	scene.oncontextmenu = function(e) {
		e.preventDefault();
	}

	scene.onmousedown = function(e) {
		e.preventDefault();
		mouseTouchDown(e.pageX, e.pageY, e.button);
	}

	scene.ontouchstart = function(e) {
		if (e.touches.length !== 1) {
			return;
		}
		e.preventDefault();
		mouseTouchDown(e.touches[0].pageX, e.touches[0].pageY, e.touches.length, true);
	}

	function mouseTouchDown(pageX, pageY, button, touch) {
		_mouseX = pageX; _mouseY = pageY;
		_button = button;
		if (touch) {
			document.ontouchmove = function(e) {
				if (e.touches.length !== 1) {
					return;
				}
				mouseTouchMove(e.touches[0].pageX, e.touches[0].pageY, e.touches.length, true);
			}
			document.ontouchend = function() {
				document.ontouchmove = null;
				document.ontouchend = null;
			}
		} else {
			document.onmousemove = function(e) {
				mouseTouchMove(e.pageX, e.pageY, _button);
			}
			document.onmouseup = function() {
				document.onmousemove = null;
				document.onmouseup = null;
			}
		}
	}

	function mouseTouchMove(pageX, pageY, button, touch) {
		var dx = pageX - _mouseX,
			dy = pageY - _mouseY;

		_phi += dx / _damp;
		// _theta += dy / _damp;

		_phi %= _TWO_PI;
		if (_phi < 0) {
			_phi += _TWO_PI;
		}

		// var maxTheta = _HALF_PI - _HALF_PI * .8,
		// 	minTheta = -_HALF_PI + _HALF_PI * .8;

		// if (_theta > maxTheta) {
		// 	_theta = maxTheta;
		// } else if (_theta < minTheta) {
		// 	_theta = minTheta;
		// }

		updateCamera();
		_dirty = true;
		// updateLabels();
		_mouseX = pageX;
		_mouseY = pageY;
	}
}

function updateCamera() {

	// var radius = _radius + (Math.sin(_theta % _PI)) * 10;
	var radius = _radius;
	var y = radius * Math.sin(_theta),
		phiR = radius * Math.cos(_theta);
	var z = phiR * Math.sin(_phi),
		x = phiR * Math.cos(_phi);

	_camera.position.set(x, y, z);
	_camLight.position.set(x, y, z);
	_camera.lookAt(_center);

}

function updateLabels() {
	if (_sceneScreenshot === null) {
		return;
	}

	var tmpImg = new Image();
	tmpImg.onload = function() {
		_tmpCtx.drawImage(tmpImg, 0, 0, sw, sh);

		var imgData = _tmpCtx.getImageData(0, 0, sw, sh);
		var data = imgData.data;

		var firstXs = [];
		var lastXs = [];
		for (var y = 0; y < sh; y++) {
			var firstX = -1;
			var lastX = -1;
			for (var x = 0; x < sw; x++) {
				var i = (x + y * sw) * 4;
				var sum = data[i] + data[i + 1] + data[i + 2];
				if (firstX === -1) {
					if (sum > 3) {
						firstX = x;
					}
				} else {
					if (sum > 3) {
						lastX = x;
					}
				}
			}
			if (lastX === -1 && firstX >= 0) {
				lastX = firstX;
			}
			firstXs.push(firstX);
			lastXs.push(lastX);
		}
		var firstYs = [];
		var lastYs = [];
		for (var x = 0; x < sw; x++) {
			var firstY = -1;
			var lastY = -1;
			for (var y = 0; y < sh; y++) {
				var i = (x + y * sw) * 4;
				var sum = data[i] + data[i + 1] + data[i + 2];
				if (firstY === -1) {
					if (sum > 3) {
						firstY = y;
					}
				} else {
					if (sum > 3) {
						lastY = y;
					}
				}
			}
			if (lastY === -1 && firstY >= 0) {
				lastY = firstY;
			}
			firstYs.push(firstY);
			lastYs.push(lastY);
		}
		postLoad(firstXs, lastXs, firstYs, lastYs);
	}
	tmpImg.src = _sceneScreenshot;


	function postLoad(firstXs, lastXs, firstYs, lastYs) {

		_tmpCtx.clearRect(0, 0, sw, sh);

		_tmpCtx.beginPath();
		for (var y = 0; y < sh; y++) {
			_tmpCtx.moveTo(firstXs[y], y);
			_tmpCtx.lineTo(lastXs[y], y);
		}
		/* TODO REMOVE BELOW TODO */
		_tmpCtx.strokeStyle = 'black';
		console.log(_tmpCtx.globalAlpha);
		_tmpCtx.stroke();
		/* TODO REMOVE ABOVE TODO */

		_tmpCtx.beginPath();
		for (var x = 0; x < sw; x++) {
			_tmpCtx.moveTo(x, firstYs[x]);
			_tmpCtx.lineTo(x, lastYs[x]);
		}
		/* TODO REMOVE BELOW TODO */
		_tmpCtx.strokeStyle = 'black';
		_tmpCtx.stroke();
		/* TODO REMOVE ABOVE TODO */

		var imgData = _tmpCtx.getImageData(0, 0, sw, sh);
		var data = imgData.data;

		for (var i = 0, iLen = data.length; i < iLen; i += 4) {
			if (data[i + 3] < 200) {
				data[i + 3] = 0;
			}
			/* TODO remove v TODO */
			else { data[i + 3] = 120; }
		}
		_tmpCtx.putImageData(imgData, 0, 0);
	}

}

function animate () {
	cube.rotation.x += 0.001;
	cube.rotation.y += 0.001;
    
    cube2.rotation.x -= 0.001;
	cube2.rotation.y += 0.001;
    
    cube3.rotation.x += 0.001;
	cube3.rotation.y -= 0.001;
    
    cube4.rotation.x -= 0.001;
	cube4.rotation.y -= 0.001;

	_renderer.render(_scene, _camera);
	if (_dirty) {
		_sceneScreenshot = _renderer.domElement.toDataURL();
		updateLabels();
		_dirty = false;
	}

	requestAnimationFrame( animate );
}

initialize();
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/92/three.js"></script>
<canvas id="scene" width="400" height="300"></canvas>

Вы можете увидеть с помощью jsfiddle, что внутренняя часть этого сложного выпуклого изображения выходит из строя с внутренней стороны.

Вопрос

Таким образом, остается вопрос: каков хороший способ создания маски, если хотите, изображения (без учета отверстий), которое будет покрывать всю внешнюю поверхность любого сложного / выпуклого объекта, где фон белый и Компоненты изображения есть что-нибудь, кроме белого? спасибо

1 Ответ

0 голосов
/ 10 мая 2018

Вот решение, использующее алгоритм flood-fill для покрытия внешних областей белым, а остальные черным.
Имейте в виду, что это очень наивная реализация, потенциально возможна большая оптимизация (например, путем вычисления ограничивающего прямоугольника и только заполнения внутри него, другим будет использование 32-битных массивов сделать фактическое назначение пикселей при заполнении).
Следует также отметить, что заливка всегда начинается в верхнем левом углу, если объект в данный момент покрывает этот пиксель, он не будет работать (однако вы можете выбрать другой пиксель для начала).

Я удалил сенсорные обработчики и некоторые другие элементы, чтобы пример был коротким. Функция updateMask - это место, где создается маска.

function createCube(color, x, y){
    const geo = new THREE.BoxBufferGeometry( 1, 1, 1 );
    const mat = new THREE.MeshPhysicalMaterial( { color: color, opacity: 1 } );
    const mesh = new THREE.Mesh(geo, mat);
    mesh.position.x = x;
    mesh.position.y = y;
    return mesh;
}

const c_main = document.getElementById("main");
const c_mask = document.getElementById("mask");
const ctx_mask = c_mask.getContext("2d");
ctx_mask.fillStyle = "#000";
const cw = c_main.width, ch = c_main.height;

const TWO_PI = Math.PI * 2;
const damp = 75, radius = 10, animspeed = 0.001;
const center = new THREE.Vector3(0, 0, 0);
let x1 = 0;
let phi = Math.PI / 2;

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(35, cw / ch, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ canvas: c_main, alpha: true, antialias: false });
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(cw, ch);
const camLight = new THREE.PointLight(0xdfdfdf, 1.8, 300, 2);
scene.add(new THREE.HemisphereLight(0x999999, 0x555555, 1));
scene.add(new THREE.AmbientLight(0x404040));
scene.add(camLight);
const cubes = [];
cubes.push(createCube(0x2378d3, 0, 0));
cubes.push(createCube(0xc36843, -0.75, 0.75));
cubes.push(createCube(0x43f873, -0.25, 1.5));
cubes.push(createCube(0x253621, 1, 0.35));
scene.add(...cubes);

function initialize() {
    c_main.addEventListener("mousedown", mouseDown, false);
    updateCamera();
    animate();
}

function updateMask(){
    //First, fill the canvas with black
    ctx_mask.globalCompositeOperation = "source-over";
    ctx_mask.fillRect(0,0, cw, ch);
    //Then using the composite operation "destination-in" the canvas is made transparent EXCEPT where the new image is drawn.
    ctx_mask.globalCompositeOperation = "destination-in";
    ctx_mask.drawImage(c_main, 0, 0);

    //Now, use a flood fill algorithm of your choice to fill the outer transparent field with white.
    const idata = ctx_mask.getImageData(0,0, cw, ch);
    const array = idata.data;
    floodFill(array, 0, 0, cw, ch);
    ctx_mask.putImageData(idata, 0, 0);

    //The only transparency left are in the "holes", we make these black by using the composite operation "destination-over" to paint black behind everything.
    ctx_mask.globalCompositeOperation = "destination-over";
    ctx_mask.fillRect(0,0, cw, ch);
}

function mouseDown(e){
    e.preventDefault();
    x1 = e.pageX;
    const button = e.button;

    document.addEventListener("mousemove", mouseMove, false);
    document.addEventListener("mouseup", mouseUp, false);
}
function mouseUp(){
    document.removeEventListener("mousemove", mouseMove, false);
    document.removeEventListener("mouseup", mouseUp, false);
}
function mouseMove(e){
    const x2 = e.pageX;
    const dx = x2 - x1;

    phi += dx/damp;
    phi %= TWO_PI;
    if( phi < 0 ){
      phi += TWO_PI;
    }

    x1 = x2;
    updateCamera();
}


function updateCamera() {
    const x = radius * Math.cos(phi);
    const y = 0;
    const z = radius * Math.sin(phi);

    camera.position.set(x, y, z);
    camera.lookAt(center);
    camLight.position.set(x, y, z);
}
function animate(){
    cubes[0].rotation.x += animspeed;
    cubes[0].rotation.y += animspeed;
    cubes[1].rotation.x -= animspeed;
    cubes[1].rotation.y += animspeed;
    cubes[2].rotation.x += animspeed;
    cubes[2].rotation.y -= animspeed;
    cubes[3].rotation.x -= animspeed;
    cubes[3].rotation.y -= animspeed;

    renderer.render(scene, camera);
    updateMask();

    requestAnimationFrame(animate);
}

const FILL_THRESHOLD = 254;
//Quickly adapted flood fill from http://www.adammil.net/blog/v126_A_More_Efficient_Flood_Fill.html

function floodStart(array, x, y, width, height){
    const M = width * 4;
    while(true){
    let ox = x, oy = y;
    while(y !== 0 && array[(y-1)*M + x*4 + 3] < FILL_THRESHOLD){ y--; }
    while(x !== 0 && array[y*M + (x-1)*4 + 3] < FILL_THRESHOLD){ x--; }
    if(x === ox && y === oy){ break; }
  }

  floodFill(array, x, y, width, height);
}

function floodFill(array, x, y, width, height){
    const M = width * 4;

    let lastRowLength = 0;
  do{
    let rowLength = 0, sx = x;
    let idx = y*M + x*4 + 3;
    if(lastRowLength !== 0 && array[idx] >= FILL_THRESHOLD){
      do{
        if(--lastRowLength === 0){ return; }
      }
      while(array[ y*M + (++x)*4 + 3]);
      sx = x;
    }
    else{
      for(; x !== 0 && array[y*M + (x-1)*4 + 3] < FILL_THRESHOLD; rowLength++, lastRowLength++){
        const idx = y*M + (--x)*4;
        array[idx] = 255;
        array[idx + 1] = 255;
        array[idx + 2] = 255;
        array[idx + 3] = 255;
        if( y !== 0 && array[(y-1)*M + x*4 + 3] < FILL_THRESHOLD ){
          floodStart(array, x, y-1, width, height);
        }
        }
    }

    for(; sx < width && array[y*M + sx*4 + 3] < FILL_THRESHOLD; rowLength++, sx++){
        const idx = y*M + sx*4;
      array[idx] = 255;
      array[idx + 1] = 255;
      array[idx + 2] = 255;
      array[idx + 3] = 255;
    }
    if(rowLength < lastRowLength){
      for(let end=x+lastRowLength; ++sx < end; ){
        if(array[y*M + sx*4 + 3] < FILL_THRESHOLD){
            floodFill(array, sx, y, width, height);
        }
      }
    }
    else if(rowLength > lastRowLength && y !== 0){
      for(let ux=x+lastRowLength; ++ux<sx; ){
        if(array[(y-1)*M + ux*4 + 3] < FILL_THRESHOLD){
            floodStart(array, ux, y-1, width, height);
        }
      }
    }
    lastRowLength = rowLength;
  }
  while(lastRowLength !== 0 && ++y < height);
}

initialize();
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/92/three.js"></script>
<canvas id="main" width="300" height="200"></canvas>
<canvas id="mask" width="300" height="200"></canvas>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...