前端实现扩散动画

以圆为例,当时为了性能和美观要求做了很多版,记录下来

除了canvas和svg当然dom用trasition也能做,比较简单,这里就不讨论了

canvas扩散圆

普通版

思路:不断重绘一个不同半径的圆,达到扩散效果

function Circle(obj){
    this.x = obj.x;	//圆心位置
    this.y = obj.y;
    this.ctx = obj.ctx;	//canvas.getContext
    this.circles = obj.circles;	//存储Circle,以便于后续删除
    this.color = obj.color;	//圆的颜色
    this.type = 'ca';
    this.globalAlph = 1;
    this.targetRadius = 1;	//圆半径

    Circle.prototype.draw = function(index){
    	if( this.targetRadius < 50 && this.targetRadius >0) {  //150
        	this.globalAlpha>0?this.globalAlpha-=0.015:'';	//随着不断扩散透明度逐渐降低
            this.targetRadius += 1;   //不断增加的半径,对应不停扩散的圆的半径
        } else {	//扩散到最大删除该圆
            this.circles.splice(index,1);	//删除
            return ;
        }
        let grd = this.ctx.createRadialGradient(this.x, this.y,0,this.x, this.y,this.r);
        grd.addColorStop(0,'transparent');
        grd.addColorStop(1,this.color);
        
        this.ctx.beginPath();
        this.ctx.globalAlpha=this.globalAlpha;
        this.ctx.fillStyle = grd;
        this.ctx.arc( this.x, this.y, this.targetRadius, 0, Math.PI * 2 );
        this.ctx.strokeStyle = this.color;
        this.ctx.stroke();
    }
}

使用

circles.push(new Circle(x,y,ctx,circles,color));

animate(){ 
   requestAnimationFrame( animate);//
   this.ctx.globalCompositeOperation = 'destination-over';
   this.ctx.fillStyle = 'rgba(0, 0, 0, 0)';
   this.ctx.fillRect( 0, 0, width, height );	//清理画布
   this.ctx.globalCompositeOperation = 'lighter';
   
  
   let i = this.circles.length;
   while(i--){
      this.circles[i].draw();
   } 
},
离屏版扩散圆

界面需要显示大量同类型动画,频繁重绘性能压力增大。
一般使用离屏渲染动画都是固定形状颜色的图形,其位置不断变化,例如扔飞盘,飞盘本身未变化只是其位置变化。

画离屏圆
/**
 * 
 * @param {String} color 颜色 
 * @param {String} r 半径 
 * @param {Float} alpha 透明度 
 */
function Circle(color,r,alpha){
    this.canvas = document.createElement('canvas'); // 创建一个新的canvas
    this.width = this.canvas.width = (r+2) * 2; // 创建一个正好包裹住一个圆的方形canvas,+2包含线宽和阴影,否则圆的一部分会被切掉
    this.height = this.canvas.height = (r+2) * 2;
    this.ctx = this.canvas.getContext('2d');
    this.ctx.lineWidth = 1;
    this.ctx.globalAlpha=alpha||1;
    this.opacity = 1;
    this.initR = 0;
    this.color = color;
    this.x = this.width / 2;
    this.y = this.height / 2;
    this.r = r; // 半径
    Circle.prototype.draw = function(){    //将canvas保存为图片,显示时将canvas再画到对应的画布上
        this.ctx.save();

        let grd = this.ctx.createRadialGradient(this.x, this.y,0,this.x, this.y,this.r);
        grd.addColorStop(0,'transparent');
        grd.addColorStop(1,this.color);	
        this.ctx.fillStyle = grd;

        this.ctx.beginPath();
        this.ctx.arc( this.x, this.y, this.r, 0, Math.PI * 2 ); //绕想,x,y半径r画圆,绘制圆弧的起始位置,绘制圆弧的终止位置。0-2π就是画个正圆
        this.ctx.strokeStyle = this.color;
        
        this.ctx.stroke();
        // this.ctx.shadowColor = this.color;	//shadow比较耗性能,酌情添加
        // this.ctx.shadowBlur = 10;

        this.ctx.fill();

        this.ctx.restore();
    }
    /**
	 * 
	 * @param {Object} ctx 需要显示该圆的canvas.getContext("2d")
	 * @param {Float} x,y需要显示的位置
	 * @param {Float} scale缩放范围0-1
	 */
    Circle.prototype.move = function(ctx,x, y ,scale){	//将画好的图片移动到指定位置
        let s = scale||1;
        let w = this.width*s;
        let h = this.height*s;
        // 将这个新创建的canvas画到真正显示的canvas上。圆是在图片中心,但是将图片载入画布的位置是图片的左上角,所以需要将其调整到中心位置
        ctx.drawImage(this.canvas, x-(this.r*s+2), y-(this.r*s+2),w,h); 
    }
}
移动离屏
思路1

将扩散到最大的圆画到canvas存起来,需要的时候通过drawImage()去绘制到指定位置,并缩放。

缩放移动画好的图

// 移动、缩放
function drawScale(x,y,cir,circles ,ctx){
    this.x = x;    //终点坐标
    this.y = y;
    this.cir = cir; //最大扩散圆图,new Circle()
    this.circles = circles ;   //动画个数数组,用来将完成的动画从画面上删除
    this.percent = 1;
    this.alpha = 1;
    this.len = 50;
    this.ctx = ctx;
    drawScale.prototype.draw = function(){
        this.cir.move(this.ctx,this.x,this.y,this.percent/this.len,this.alpha)
        if(this.percent>=this.len-1){
            this.circles .splice(0,1);	//删除
            return;
        }
        this.alpha -= 0.015
        this.percent++;
    }
}

使用同普通版,animate不变

cirs[color] = new Circle(color,r,1);	//将每种颜色的圆存起来

circles.push(new drawScale(x,y,cirs[color],circles,ctx));

思路2

根据MDN提示:不要在用drawImage时缩放图像,可以使用这一种

一个扩散圆分解可以分解为由小到大的多个圆,先生成多个半径递增的离屏圆图片,再将其移动到相应的位置上从小到大显示该圆就形成了一个离屏圆动画,这样可以修改圆的透明度让他逐渐消失(canvas透明度是需要修改整体透明度的,需要在beginpath之前设置,所以drawScale不适用)
这样会导致每个圆都需要存储几十个帧,才能有很好的动画效果。这只适用于圆的颜色需求不是很多的情况下,如果要求每一次画圆颜色不同当然还是直接画比较理想

vue项目中使用,需要先初始化各颜色圆的每一帧

// 初始化扩散圆的每一帧
init(){
	let c = {
	     high: #DE3C4C,
	     medium: #FFA169,
	     low: #FFD24D,
	 };
	 this.color = c;
	 let r=1,percent=0,alpha=1;
	 for(let i in c){	//根据颜色画不同的扩散圆,每组里是每一帧,cir存储扩散圆每一帧的图
	     this.cir[c[i]] =[];
	 }
	 while(r){
	     for(let i in c){
	         let d = new func.Circle2(c[i],r,alpha);	//每一帧
	         d.draw();
	         this.cir[c[i]].push(d);
	     }
	     if(r>15&&alpha>=0.03){
	         alpha-=0.03;    //  透明度逐渐降低
	     }
	     if( r < 50 && r >0) {  //150
	         r += 1.2;   //2.4
	     } else {
	         r = 0;
	     }
	 }
}
animate(){ //渲染
    requestAnimFrame( this.animate);
     this.ctx.globalCompositeOperation = 'destination-over';
     this.ctx.fillStyle = 'rgba(0, 0, 0, 0)';
     this.ctx.fillRect( 0, 0, this.width, this.height );	//清屏
     this.ctx.globalCompositeOperation = 'lighter';
    
     // 当前需要渲染的圆
     var i1 = this.circles.length;
     while( i1-- ) {
         this.circles[ i1].init( i1);
     }   
 },
createCircle( target,color) {	//target目的点
	//需要画的圆放入circle,然后在animate中渲染
  this.circles.push(new drawCir(target.x,target.y,this.cir[color],this.circles,this.ctx)):''
},



//js中drawCir
// 移动离屏
function drawCir(x,y,cir,circles ,ctx){
    this.x = x;    //终点坐标
    this.y = y;
    this.cir = cir; //一个动画的每帧图片的组合数组
    this.circles = circles ;   //动画个数数组,用来将完成的动画从画面上删除
    this.percent = 0;
    this.len = cir.length;
    this.ctx = ctx;
    drawCir.prototype.init = function(){
        this.cir[this.percent].move(this.ctx,this.x,this.y)
        if(this.percent>=this.len-1){
            this.circles.splice(0,1);
            return;
        }
        this.percent++;
    }
}

简化,将init提到方法中来

function drawCirFrame(ctx,colors){	//colors一个对象存储了各类型的颜色,例:{a: "#fff",b:"#000"}
    this.colorCir = {}; //一个动画的每帧图片的组合数组
    this.circles = [];   //动画个数数组,用来将完成的动画从画面上删除
    this.ctx = ctx;
    let r=1,percent=0,alpha=1;
    let isHex = (color)=>{
        let reg = /^#([0-9a-fA-f]{3}|[0-9a-fA-f]{6})$/;
        return reg.test(color)
    },
    while(r){	//生成各颜色,各透明度,各半径的圆图
        for(let i in colors){
            if(isHex(colors[i])){  //如果是"#fff"类型颜色,auth.isHex是一个自定义的判断方法
                let d = new Circle(colors[i],r,alpha);
                d.draw();
                this.colorCir[colors[i]] = this.colorCir[colors[i]]||[]
                this.colorCir[colors[i]].push(d);
            }
        }
        if(r>15&&alpha>=0.03){
            alpha-=0.03;    //  透明度逐渐降低
        }
        if( r < 50 && r >0) {  
            r += 1.2;  //半径逐渐增加
        } else {
            r = 0;
        }
    } 
    drawCirFrame.prototype.add = function(x,y,color){
        this.circles.push({
            x: x,
            y: y,
            color: color,
            percent: 0,
            len:  this.colorCir[color].length,
            circles: this.colorCir[color],
        }) 
    }
    drawCirFrame.prototype.draw = function(){
        this.circles.forEach((a,ind)=>{
            a.circles[a.percent].move(this.ctx,a.x,a.y);
            if(a.percent>=a.len-1){
                this.cirs.splice(ind,1);    
                return;
            }
            a.percent++;
        })
    }
}

使用

//初始化canva后
cirFram = new drawCirFrame(ctx,colors)
//需要圆时
cirFram.add(x,y,color); 
//animate中(animate同上,清理画布方法不变,circles遍历取消改为下方)
cirFram.draw();	//将circles的遍历挪到了drawCirFrame中

d3(v5版)+svg

因为本来使用的项目就有d3+svg所以没有增加额外的依赖,如果你没有的话就需要考虑一下要不要增加额外的依赖去实现这个功能了,总体上这个方式是比canvas更简单的

vue的环境

//methods
createCircle(p,color){	//p位置
   if(!this.circleColor[color]){
        this.initColor(color)
    }
    let circle = this.svg.select(".circles").append("circle").attr('class',color);
    circle.attr("cx",p.x)
        .attr("cy",p.y)
        .attr("fill",`url(#radial${color})`)
        // .attr("filter",`url(#filter${color})`)
        .attr("style",`stroke: ${color}; stroke-width: 1;`)

        .attr("r","0")
        .attr("opacity","1")
        .transition()
        .ease(d3.easeLinear)
        .duration(500)
        .attr("r","25")	//扩散到一半开始扩散+消失
        .transition()
        .duration(500)
        .attr("r","50")
        .attr("opacity","0")
        .remove();
        // .attr("r","0")	//直接扩散+消失,导致扩散到一半就已经透明度很低了,效果不是很好,看着比较模糊
        // .attr("opacity","1")
        // .transition()
        // .duration(1500)
        // .attr("r","50")
        // .attr("opacity","0")
        // .remove();
        
},
// 初始化渐变色
initColor(color){
	// 填充色
    let radial = this.svg.select(".color").append("radialGradient")
        .attr('id',`radial${color}`)
        .attr('cx','50%')
        .attr('cy','50%')
        .attr('fx','50%')
        .attr('fy','50%')
        .attr('r','50%')
    radial.append("stop")
        .attr('offset','0%')
        .attr('style','stop-color:transparent;stop-opacity:0');
    radial.append("stop")
        .attr('offset','100%')
        .attr('style',`stop-color:${color};stop-opacity:0.4`);    //rgba(0,35,76,1)
     //滤镜色,比如加阴影,酌情添加
    /* let filter = this.svg.select(".color").append("filter")
        .attr('id',`filter${color}`)
        .attr('x','0')
        .attr('y','0')
        .attr('width','100%')
        .attr('height','100%')
    filter.append("feOffset")
        .attr('result','offOut')
        .attr('in','SourceAlpha')
        .attr('dx','0')
        .attr('dy','0')
    filter.append("feGaussianBlur")
        .attr('in','offOut')
        .attr('stdDeviation',`3`)
        .attr('result','blurOut');
    filter.append("feBlend")
        .attr('in','SourceGraphic')
        .attr('in2','blurOut')
        .attr('mode',`normal`); */
    
    this.circleColor[color] =` radial${color}`
},

threejs:3d版

使用起来也很简单,总结起来就是画个你想要的图形,通过在animate中不断缩放即可实现扩散动画。
其他部分同canvas的写法,在扩散上转为scale去处理。

如果要做逐渐消失的效果,可以直接修改material的透明度,或者直接dispose原来的material在新建。就效果来说新建会比直接修改效果更好。往往直接修改其透明度它会延迟转变,效果不是很好

circle.scale.set(s,s,1);//s表示缩放程度
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值