基于HTML5 Canvas和WebGL实现图片的交互式几何变换

Photoshop中的自由变换工具,可以用来调整图片的几何形状,可以平移、旋转、缩放和斜切等,配合shift、ctrl和alt三键,使用起来十分灵活。一些成熟的在线PS,如Pixlr Editor,主要基于Flash平台技术实现,而目前,也出现了一些基于HTML5 Canvas实现的在线图像编辑工具,如CloudCanvas,它们都提供了自由变换工具。本人曾在自己的x项目中基于Flash平台技术实现了简单的图像自由几何变换功能,如图1所示。如今,本人对HTML5 Canvas和WebGL兴趣浓厚,于是想如何在Canvas上实现相似的功能。凭着项目得来的经验,经过一番研究,最终在无插件模式下,创建了如图2所示的Demo。


Demo在线演示,特来补上,建议使用Chrome浏览器打开(2014.10.23)。


基于HTML5 Canvas和WebGL实现图片的交互式几何变换_第1张图片

图1

基于HTML5 Canvas和WebGL实现图片的交互式几何变换_第2张图片

(a)

基于HTML5 Canvas和WebGL实现图片的交互式几何变换_第3张图片  基于HTML5 Canvas和WebGL实现图片的交互式几何变换_第4张图片

(b)                                                                                                    (c)

基于HTML5 Canvas和WebGL实现图片的交互式几何变换_第5张图片  基于HTML5 Canvas和WebGL实现图片的交互式几何变换_第6张图片

  (d)                                                                                                    (e)

基于HTML5 Canvas和WebGL实现图片的交互式几何变换_第7张图片

(f)

图2(b~f表现了初始、平移、缩放、旋转、镜像的不同状态)

这里为什么要谈到WebGL呢?因为性能,因为使用ImageData处理图像,负担全加在了JavaScript上,因为WebGL基于OpenGL ES 2.0,可以编写GLSL ES代码,可以使用GPU加速,从而减轻JavaScript的负担。如图2,实现了图像的底片效果 动画,窗口右上角显示的帧率始终保持在60FPS左右。为了降低开发难度,这里额外地使用了两个类库, CreateJS和 Three.js。Three.js封装了WebGL而不失灵活性,主要用来创建3D场景,而想绘制如图1所示的自由变换工具这么个有点复杂的2D图形,则不太方便、不太理想。所以,本人的做法是,图像作为纹理交给WebGL和Three.js处理,2D图形则使用HTML5 Canvas 2D和CreateJS(EaselJS)绘制,于是需要两个Canvas上下叠加在一起(可惜同一Canvas不能同时getContext("2d")又getContext("webgl"))。

下面贴出所有代码,没做什么注释。

index.html代码如下:


	
		ImageTool
		
		
		
		
		
		
        
	
	
		
		
	

index.js代码如下:
var stage=null,
    ctrlframe=null,
    needToUpdate=true,

    renderer=null,
    scene=null,
    camera=null,
    texture=null,
    picture=null,
    threshold=0,
    sign=1,

    stats=null;

$(function(){
    var winW=window.innerWidth;
    var winH=window.innerHeight;

    $(window).on("resize",onResize);
    //简单起见,canvas铺满整个窗口
    $("canvas").attr("width",winW).attr("height",winH).css("position","absolute");

    $("canvas:eq(1)").on("mousedown",onMouseDown);

    stats = initStats();

    renderer=new THREE.WebGLRenderer({canvas:$('canvas')[0]/*,antialias:true*/});
    renderer.setClearColor(0xEEEEEE, 1.0);
    renderer.setSize(winW, winH);

    scene = new THREE.Scene();

    camera = new THREE.OrthographicCamera(-winW/2,winW/2,winH/2,-winH/2);
    camera.position.set( 0, 0, 200 );
    scene.add( camera );

    texture=new THREE.ImageUtils.loadTexture("assets/imgs/girl.jpg");
    texture.magFilter=THREE.NearestFilter;
    texture.minFilter=THREE.NearestFilter;

    var geometry=new THREE.PlaneGeometry(256,256,1,1);
    var material=new THREE.ShaderMaterial({
        side:THREE.DoubleSide,
        uniforms:{
            map:{type:"t",value:texture},
            threshold:{type:"f",value:1.0}
        },
        vertexShader:[
            "varying vec2 vUV;",
            "void main(){",
                "vUV=uv;",
                "gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );",
            "}"
        ].join("\n"),
        fragmentShader:[
            "varying vec2 vUV;",
            "uniform sampler2D map;",
            "uniform float threshold;",
            "void main(void) {",
                "highp vec4 texColor = texture2D( map, vUV );",
                "if(vUV.s+vUV.t2){
        sign=-1.0;
        threshold.value=2;
    }else if(threshold.value<0){
        sign=1.0;
        threshold.value=0;
    }

    renderer.render(scene,camera);

    if(needToUpdate){//不必每一帧都更新上层画布,否则帧率会掉
        stage.update();
        needToUpdate=false;
    }

    stats.update();

    requestAnimationFrame(onUpdate);

}
function onMouseDown(evt){

    var state=ctrlframe.checkState(evt.clientX,evt.clientY);

    if(state!="still"){
        $("canvas:eq(1)").on("mouseup",onMouseUp);
        $("canvas:eq(1)").on("mousemove",onMouseMove);
    }

    console.log("state:"+state);

}

function onMouseUp(evt){

    $("canvas:eq(1)").off("mouseup",onMouseUp);
    $("canvas:eq(1)").off("mousemove",onMouseMove);

    ctrlframe.checkState(evt.clientX,evt.clientY);

}

function onMouseMove(evt){

    ctrlframe.update(evt.clientX,evt.clientY);

    needToUpdate=true;

}

function onResize(evt){

    var winW=window.innerWidth;
    var winH=window.innerHeight;

    $("canvas").attr("width",winW).attr("height",winH);

    renderer.setSize(winW, winH);
    camera.left=-winW/2;
    camera.right=winW/2;
    camera.top=winH/2;
    camera.bottom=-winH/2;
    camera.updateProjectionMatrix();

    ctrlframe.update();
    needToUpdate=true;

    stats.domElement.style.left = (winW-100)+'px';
}

function initStats() {

    var stats = new Stats();

    stats.setMode(0); // 0: fps, 1: ms

    // Align top-right
    stats.domElement.style.position = 'absolute';
    stats.domElement.style.left = (window.innerWidth-100)+'px';
    stats.domElement.style.top = '8px';

    $("body").append(stats.domElement);

    return stats;

}

enjolras.js代码如下:
createjs.CtrlFrame=function(bindObject){

	createjs.Shape.call(this);

	this.sx=1;
	this.sy=1;

	this.dx=0;
	this.dy=0;

	this.state="still";
	this.activeIndex=-1;

	this.circles=null;

	this.bindedObject=null;

	if(bindObject){
		this.bind(bindObject);
	}

}

createjs.CtrlFrame.prototype=Object.create(createjs.Shape.prototype);

createjs.CtrlFrame.prototype.constructor=createjs.CtrlFrame;

createjs.CtrlFrame.prototype.bind=function(bindObject){

	this.bindedObject=bindObject;

	var bounds=bindObject.getBounds();

	this.setBounds(bounds.x,bounds.y,bounds.width,bounds.height);

	this.x=bindObject.x;
	this.y=bindObject.y;
	this.sx=bindObject.scaleX;
	this.sy=bindObject.scaleY;
	this.rotation=bindObject.rotation;

	this.updateCircles();
	this.drawCircles();

};

createjs.CtrlFrame.prototype.updateBindedOject=function(){

	var bindedObject=this.bindedObject;

	bindedObject.x=this.x;
	bindedObject.y=this.y;
	bindedObject.scaleX=this.sx;
	bindedObject.scaleY=this.sy;
	bindedObject.rotation=this.rotation;

};

createjs.CtrlFrame.prototype.drawCircles=function(){

	var i=0,circle=null;
	var circles=this.circles;
	var colors=["red","orange","yellow","green","cyan","blue","purple","pink","lime"];

	var graphics=this.graphics;

	graphics.clear().setStrokeStyle(2).beginStroke("#0af");//beginStroke("#444");

	/*graphics.moveTo(circles[0].x,circles[0].y);
	graphics.lineTo(circles[2].x,circles[2].y);
	graphics.lineTo(circles[8].x,circles[8].y);
	graphics.lineTo(circles[6].x,circles[6].y);
	graphics.lineTo(circles[0].x,circles[0].y);*/
	graphics.drawRect(circles[0].x,circles[0].y,circles[8].x<<1,circles[8].y<<1);
	
	for(i=0;i>1)*this.sx;
	halfH=(bounds.height>>1)*this.sy;

	for(var i=0;i<9;i++){

		row=Math.floor(i/3);
		col=Math.floor(i%3);

		circles[i]=new createjs.Point((col-1)*halfW,(row-1)*halfH);

	}

	if(this.circles){

		this.circles.splice(0);

	}

	this.circles=circles;

};

createjs.CtrlFrame.prototype.decideActiveIndex=function(){

	var circles=this.circles;

	var bounds=this.getBounds();
	var theta=this.rotation*Math.PI/180;

	var dx=this.dx*Math.cos(-theta)-this.dy*Math.sin(-theta);
	var dy=this.dx*Math.sin(-theta)+this.dy*Math.cos(-theta);

	if(Math.abs(dx)>(bounds.width>>1)*Math.abs(this.sx)+10
		||Math.abs(dy)>(bounds.height>>1)*Math.abs(this.sy)+10){

		this.activeIndex=-1;

	}
	else{

		this.activeIndex=4;

		for(var i=0;i<9;i++){

			if(i==4){

				continue;

			}
			if(Math.abs(dx-circles[i].x)<10&&Math.abs(dy-circles[i].y)<10){

				this.activeIndex=i;

				break;

			}

		}
	}
};

createjs.CtrlFrame.prototype.checkState=function(x,y){

	this.dx=x-this.x;
	this.dy=y-this.y;

	this.decideActiveIndex();	

	switch(this.activeIndex){

		case 4://cyan
			this.state="translate";
			break;
		case 0://red
		case 2://yellow
		case 6://purple
		case 8://lime
		case 5://blue 横向
		case 7://pink 纵向
			this.state="scale";
			break;

		case 1://orange
		case 3://green
			this.state="rotate";
			break;

		default:
			this.state="still";

	}

	return this.state;

};

createjs.CtrlFrame.prototype.update=function(x,y){

	if(x==undefined || this.state=="still"){

		this.updateBindedOject();

		return;
	}

	if(this.state=="translate"){

		this.x=x-this.dx;
		this.y=y-this.dy;

	}else{

		this.dx=x-this.x;
		this.dy=y-this.y;

		if(this.state=="scale"){

			this.scale();

		}else{

			this.rotate();

		}

	}

	this.updateBindedOject();

};

createjs.CtrlFrame.prototype.scale=function(){

	var row=Math.floor(this.activeIndex/3);
	var col=Math.floor(this.activeIndex%3);

	var theta=this.rotation*Math.PI/180;
	var cos=Math.cos(-theta);
	var sin=Math.sin(-theta);

	var bounds=this.getBounds();

	var dx=this.dx*cos-this.dy*sin;
	var dy=this.dx*sin+this.dy*cos;

	if(col!=1){

		this.sx=dx*(col-1)/(bounds.width>>1);

	}
	if(row!=1){

		this.sy=dy*(row-1)/(bounds.height>>1);

	}
	
	this.updateCircles();
	this.drawCircles();

};

createjs.CtrlFrame.prototype.rotate=function(){

	var delta=0;

	if(this.activeIndex==1){

		delta=Math.PI*0.5*(this.sy<0?-1:1);

	}else{

		delta=Math.PI*(this.sx<0?0:1);

	}

	var theta=Math.atan2(this.dy,this.dx)+delta;

	this.rotation=theta*180/Math.PI;

};

var enjolras = enjolras || { };
//建立CreateJS与ThreeJS之间的绑定
enjolras.BindObject=function(picture,bounds){
	this._bounds=bounds;
	this._position=picture.position;
	this._rotation=picture.rotation;
	this._scale=picture.scale;

	console.log("Enjolras: Create a binding between CreateJS and ThreeJS");
};
//WebGL和Three.js采用右手坐标系,初始状态下原点落在Canvas中心
enjolras.BindObject.prototype={
	getBounds:function(){
		return this._bounds;
	},
	get x(){
		return this._position.x+(window.innerWidth>>1);
	},
	set x(value){
		this._position.x=(value-(window.innerWidth>>1));
	},
	get y(){
		return this._position.y+(window.innerHeight>>1);
	},
	set y(value){
		this._position.y=-(value-(window.innerHeight>>1));
	},
	get rotation(){
		return -this._rotation.z*180/Math.PI;
	},
	set rotation(value){
		this._rotation.z=-value*Math.PI/180;
	},
	get scaleX(){
		return this._scale.x;
	},
	set scaleX(value){
		this._scale.x=value;
	},
	get scaleY(){
		return this._scale.y;
	},
	set scaleY(value){
		this._scale.y=value;
	}
};

这里,平移、旋转、缩放都围绕中心点(浅蓝色的点)来进行,没有实现斜切功能,多处为硬编码,可以改进的地方还很多。

你可能感兴趣的:(基于HTML5 Canvas和WebGL实现图片的交互式几何变换)