原生js实现canvas气泡冒泡效果

时间:2021-10-24 11:07:18

说明:

本文章主要分为ES5和ES6两个版本

ES5版本是早期版本,后面用ES6重写优化的,建议使用ES6版本。

1, 原生js实现canvas气泡冒泡效果的插件,api丰富,使用简单
2, 只需引入JumpBubble.js一个js文件即可

项目源码地址: https://github.com/roonly/JumpBubble

== 使用demo:

ES6版本的使用demo:

const bubble = new JumpBubble(document.getElementById(`cavs`));

bubble.create('/img/fish.png');

ES5代码的时候demo:

var demo = new JumpBubble({
  elCanvas : document.getElementById("canvasIdName")
});
demo.create({
  elImg : document.getElementById("imgIdName")
});

== 效果:

原生js实现canvas气泡冒泡效果

== html 代码:

ES6版本:

//html内容
<canvas id="cavs" width="130" height="400" style="border:1px solid #fff;">您的浏览器不支持canvas标签~</canvas> //index.js内容:
let list = [
'http://p4.cdn.btime.com/t01e430315c854b44d2.png',
'http://p5.qhimg.com/t017f9904d4be818a87.png',
'http://p5.qhimg.com/t015ec16e404a442dd4.png',
'/img/fish.png', //注:路径是相对html的路径,因为该路径最终会放到img标签的src上
];
const bubble = new JumpBubble(document.getElementById(`cavs`));
setInterval(() => {
if(s > list.length - 1){
s = 0;
}
bubble.create(list[s]);
s++
},250);

ES5版本:

<!DOCTYPE HTML>
<html>
<meta charset="utf-8">
<meta name="renderer" content="webkit">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<title>原生js实现canvas气泡冒泡效果</title>
<body>
<p style="display:none">
<img id="img1" src="http://p8.qhimg.com/t01053ab4d4d6510abd.png" alt="">
<img id="img2" src="http://p5.qhimg.com/t017f9904d4be818a87.png" alt="">
<img id="img3" src="http://p5.qhimg.com/t015ec16e404a442dd4.png" alt="">
<img id="img4" src="http://p6.qhimg.com/t017895dcd6312beacb.png" alt="">
<img id="img5" src="http://p2.qhimg.com/t01f70bccf10e16addd.png" alt="">
<img id="img6" src="http://p3.qhimg.com/t016d419cab67d819ac.png" alt="">
</p>
<h2>小贼说:原生js实现canvas气泡冒泡效果的插件,api丰富,使用简单</h2>
<canvas id="myCanvas" width="250" height="430" style="border:1px solid #eee">您的浏览器不支持canvas标签~</canvas>
<canvas id="myCanvas2" width="200" height="330" style="border:1px solid #eee">您的浏览器不支持canvas标签~</canvas>
<script src="JumpBubble.js"></script>
<script type="text/javascript">
window.onload = function(){
// 使用demo
var demo = new JumpBubble({
elCanvas : document.getElementById("myCanvas")
});
clearInterval(setDemo1);
var setDemo1 = setInterval(function(){
var idName = "img" + Math.ceil(Math.random()*6);
demo.create({
elImg : document.getElementById(idName)
});
},150); // demo2
var demo2 = new JumpBubble({
elCanvas : document.getElementById("myCanvas2"),
config : {
alpha : 0.5,
width : 30
},
callback : function(a,b,c){}
});
clearInterval(setDemo2);
var setDemo2 = setInterval(function(){
demo2.create({
elImg : document.getElementById("img1")
});
},150); } </script>
</body>
</html>

== javascript 代码:(分为ES6版 和 ES5版)

ES6版:

 /*
** @param canvasNode [必传] <DOM> canvan标签元素
** @param config [选传] <Obj> 可选配置项
** - effect <Str> 气泡的y轴浮动效果,可选2个值:linear、ease,默认'ease'
** - speed <Str> 气泡的Y轴移动速度,可选3个值 slow default fast, 默认'default'
** - isToAlapha <Boo> 气泡是否逐渐增加透明度 默认true
** - alpha <Num> 初始气泡的透明度, 默认0.9
** - max <Num> //画布上最多同时存在多少气泡,默认30,请根据气泡出现频道调整此值,已达到最优的视觉效果
** - diffWidth <Num> 初始冒泡时气泡宽度与指定宽度(width属性值)的差值,默认15,差值越大,冒泡时气泡从小变大的效果越明显
** - left <Num> 冒泡位置,距离画布左侧像素数,默认在画布中间靠左15像素位置
** - top <Num> 冒泡位置,距离画布顶部像素数,默认距离画布顶部为画布高度减30像素
** - width <Num> 气泡的宽度, 默认30,为保证气泡不变形,高度随宽度改变
** @param callback [选传] <Func> 态度气泡实例初始化后的回调
*/
export default class JumpBubble{
constructor(canvasNode, config = {}, callback ){
if(!canvasNode || !canvasNode.getContext){
console.warn("jumpBuffle,启用失败,canvas传参错误 或 浏览器不支持canvas");
this.error = true;
return false;
}
const t = this,
{ width, height } = canvasNode;
const _config = { //配置气泡冒泡设置
width: 30, //可自定义气泡宽度,高度随宽度变化
left : width/2 - 15, //距离左侧距离
top : height - 30, //距离顶部距离
alpha : 0.9, // 透明度设置
effect: 'ease',
speed: 'default',
max: 30, //画布上最多同时存在多少气泡, 默认30
isToAlapha: true,
diffWidth: 15, // 初始冒泡时气泡宽度与正常宽度的差值,默认15
cavHeight: height, // [非配置项] canvan标签的高度,设置气泡在不同高度有不同的浮动速度时会用到
cavWidth: width, // [非配置项] canvan标签的高度,设置气泡在左右晃动,触壁反弹时会用到
};
Object.assign(t, {
canvasInfo: {
canvas : canvasNode,
width,
height
},
config: Object.assign(_config, config),
ctx: canvasNode.getContext("2d"),
bubbleArr: [],//用来存储所有的气泡
allImg: { //缓存创建的img标签
lists: [], //src的列表
doms: [], //缓存list对应的创建的dom
loadState: [], //是否img标签已经加载完毕,如果完毕再直接返回img标签的dom,如果没有加载完毕则放到img.load函数内返回imgdom
}
});
callback && callback(t);
}
/*
** 冒泡的生命周期
** before: 气泡开始冒泡前
** after: 单个气泡消失后
*/
create(imgsrc, before, after){
const t = this,
{ctx, error} = t;
if(!ctx || error){
console.warn("jumpBuffle:create时,ctx错误");
return false;
}
const { bubbleArr, canvasInfo } = t,
{ width: imgwidth, max } = t.config;
t.createImg(imgsrc).then(imgNode => {
const imgInfo = {
el : imgNode,
width : imgwidth || imgNode.width,
height : imgwidth && imgNode.height*(imgwidth/imgNode.width) || imgNode.height
};
if(bubbleArr.length > max){
return;
}
bubbleArr.push(new DrawImg(ctx, imgInfo, t.config));
//每添加一个气泡触发一次的回调函数, 生命周期为气泡开始冒泡前
before && before(canvasInfo.canvas, imgNode, bubbleArr);
if(!t.setInter){
t.setInterFn(after);
}
});
return this;
}
createImg(imgsrc){
return new Promise(res => {
const { lists, doms, loadState } = this.allImg;
const i = lists.indexOf(imgsrc);
if(i > -1 && loadState[i]){
res(doms[i]);
return ;
}
const img = document.createElement('img');
img.src = imgsrc;
img.setAttribute('style', 'display:none;');
document.body.appendChild(img);
lists.push(imgsrc);
doms.push(img);
img.onload = () => {
loadState.push(true);
res(img);
}
})
}
setInterFn(after){
const t = this,
{ ctx, canvasInfo } = t,
{ width, height } = canvasInfo;
t.setInter = setInterval(function(){
try{
ctx.clearRect(0, 0, width, height);
t.bubbleArr = t.bubbleArr.filter(function(val){
val.addCtx();
val.updateCtx();
if(val.y < 10){
after && after();
return false;
}else{
return true;
}
});
if(t.bubbleArr.length === 0){
clearInterval(t.setInter);
t.setInter = null;
ctx.clearRect(0, 0, width, height);
}
}catch(e){
console.warn('创建态度气泡出错',e);
clearInterval(t.setInter);
}
},25);
}
} class DrawImg{
constructor(ctx, imgInfo, { left, top, alpha, speed, cavWidth, cavHeight, effect, isToAlapha, diffWidth }){
Object.assign(this, {
whichUnit: null, // 标识气泡在画布中的位置 ,canvan画布分成3个部分,2:中上、1:中、0:中下
ctx,
originWidth: imgInfo.width,
img: imgInfo.el,
imgWidth: imgInfo.width - diffWidth, //气泡初始大小与指定气泡大小的差值
imgHeight: imgInfo.height - diffWidth,
x: left, //气泡在x轴的位置 相对左侧
y: top, //气泡在y轴位置 相对顶部
alpha,
speed,
effect,
isToAlapha,
cavWidth,
oneUnit: cavHeight/4, //将canvan画布分成3个部分,中上、中、中下(中下占2/4,其他各占1/4)
toRight: (Math.random() > 0.5 ? false : true), //在气泡左右晃动效果时,该属性标识气泡是向左晃动还是向右晃动。
xPx: Math.random()*2.5, //x轴气泡每次位移像素数
yPx: null, //缓存气泡在画布y轴上每次位移的距离
yPxArr: null, //缓存y轴每次位移像素数的3个阶段的值的数组
diffAlapa: null, //缓存气泡在变为透明时每次增加的透明度
});
this.updateCtx = this.updateCtx.bind(this);
this.effectCommon = this.effectCommon.bind(this);
}
getSpeed(type = 'default'){
switch(type){
case 'slow':
return 0.6;
case 'fast':
return 1.4;
default:
return 1;
}
}
addCtx(){
const p = this,
{ ctx } = p;
ctx.save();
ctx.globalAlpha = p.alpha;
ctx.drawImage(p.img, p.x, p.y, p.imgWidth, p.imgHeight);
ctx.restore();
}
setImgWidth(){
const { originWidth, imgWidth } = this;
if(imgWidth < originWidth){//差值diffWidth的初始小气泡,逐渐变大为指定气泡的大小
this.imgWidth += 1;
this.imgHeight += 1;
}
}
setAlapa(){
const p = this,
{ y, isToAlapha, whichUnit } = p;
if(!isToAlapha)return false; //可自行配置,是否逐渐增加透明度
let diffAlapa = p.diffAlapa;
if(whichUnit === 2){ //气泡在画布中上部分时
if(!diffAlapa){
p.diffAlapa = diffAlapa = p.countDiffAlapa();
}
if(p.alpha <= diffAlapa){
p.alpha = 0;
}else{
p.alpha -= diffAlapa;
}
}
}
countDiffAlapa(){
const { alpha, oneUnit, yPx } = this;
return (alpha + 0.1)/(oneUnit/yPx);
}
setYpx(){
const p = this,
{ y, oneUnit } = p;
let i;
// 根据态度气泡在容器内到达高度不同,设置不同的速度
switch(true){
case y < oneUnit: //气泡在画布中上部分时
i = 2;
break;
case y > oneUnit && y < oneUnit*2: //气泡在画布的中部分时
i = 1;
break;
default: //气泡在画布的中下部分
i = 0;
}
if(p.whichUnit !== i){
p.yPx = p.yPxArr[i];
p.whichUnit = i;
}
p.y -= p.yPxArr[i];
}
setYpxArr(){
if(this.yPxArr)return false;
this.yPxArr = this.countYpxArr(this.effect, this.getSpeed(this.speed));
}
countYpxArr(effect, speedNum = 1){//根据effect不同,设置气泡在y轴上位移距离,数组包含3个值,分别表示在y轴的3个阶段的位移距离
const basePx = 2;
const easeArr = [basePx-0.5, basePx, basePx+0.5];
const linearArr = [basePx, basePx, basePx];
switch(effect){
case 'ease':
return mlt(easeArr);
case 'linear':
return mlt(linearArr);
default:
return mlt(easeArr);
}
function mlt(arr){
return arr.map(v => v*speedNum);
}
}
pullback(){//在x轴上,触壁反弹效果,晃动的上升,
const p = this,
{ y, cavWidth, xPx, originWidth } = p;
// 控制气泡左右晃动,触壁反弹效果
switch(true){
case (p.x + originWidth >= cavWidth): //气泡触右侧壁
p.toRight = false;
p.x -= xPx;
break;
case p.x <= 2: //气泡触左侧壁
p.toRight = true;
p.x += xPx;
break;
case p.toRight:
p.x += xPx;
break;
default:
p.x -= xPx;
}
this.effectCommon();
}
effectCommon(){
this.setYpxArr();
this.setYpx();
this.setAlapa();
this.setImgWidth();
}
updateCtx(){
this.pullback();
}
}

ES5版:

;(function(window){
function JumpBubble(opt){
var t = this,
canvas = opt.elCanvas,
canvasW = canvas.width,
canvasH = canvas.height;
if(!canvas){
console.warn("jumpBuffle:new 实例时,canvas传参错误");
return;
}
t.canvasInfo = {
canvas : canvas,
width : canvasW,
height : canvasH
};
var canvas = t.canvasInfo.canvas;
if(!canvas.getContext){
console.warn("jumpBuffle,启用失败,浏览器不支持canvas");
return;
}
var config = { //配置气泡冒泡设置
left : canvasW/2 - 15, //距离左侧距离
top : canvasH - 30, //距离顶部距离
alpha : 0.9 // 透明度设置
// width : 30 // 默认使用传入图片的实际宽高,可自定义气泡宽度,高度随宽度变化
};
t.callback = opt.callback; //每添加一个气泡触发一次的回调函数
t.config = hrExtend(config,opt.config);
t.ctx = canvas.getContext("2d");
t.bubbleArr = []; //用来存储所有的气泡
}; JumpBubble.prototype.create = function(opt){
var t = this,
bubbleArr = t.bubbleArr,
ctx = t.ctx,
img = opt.elImg,
config = t.config,
cfgImgWidth = config.width,
convasInfo = t.canvasInfo,
callback = t.callback;
if(!ctx){
console.warn("jumpBuffle:create时,ctx错误");
return;
}
var imgInfo = {
el : img,
width : cfgImgWidth || img.width,
height : cfgImgWidth && img.height*(cfgImgWidth/img.width) || img.height
};
if(bubbleArr.length>30){
return false;
}
bubbleArr.push(new drawImg(ctx,imgInfo,t.config,convasInfo));
//每添加一个气泡触发一次的回调函数,
// 参数1:canvas元素;参数2:传入的图片元素;参数3:当前存在的气泡数组
callback && callback(convasInfo.canvas,img,bubbleArr);
if(!t.setInter){
t.setInterFn();
}
}; JumpBubble.prototype.setInterFn = function(){
var t = this,
ctx = t.ctx,
convasInfo = t.canvasInfo,
canvasW = convasInfo.width,
canvasH = convasInfo.height;
t.setInter = setInterval(function(){
ctx.clearRect(0,0,canvasW,canvasH);
t.bubbleArr = t.bubbleArr.filter(function(val){
val.addCtx();
val.updateCtx();
if(val.y < 10){
return false;
}else{
return true;
}
});
if(t.bubbleArr.length === 0){
clearInterval(t.setInter);
t.setInter = null;
ctx.clearRect(0,0,canvasW,canvasH);
}
},25);
}; function drawImg(ctx,imgInfo,config ,canvasInfo){
var p = this;
p.ctx = ctx;
p.imgInfo = imgInfo,
p.img = imgInfo.el;
p.imgWidth = imgInfo.width - 10;
p.imgHeight = imgInfo.height - 10;
p.x = config.left;
p.y = config.top;
p.alpha = config.alpha;
p.canvasInfo = canvasInfo;
p.ranX = (Math.random()*5 - 2.5)/2;
}
drawImg.prototype.addCtx = function(){
var p = this,
ctx = p.ctx;
ctx.save();
ctx.globalAlpha = p.alpha;
ctx.drawImage(p.img,p.x,p.y,p.imgWidth, p.imgHeight);
ctx.restore();
}
drawImg.prototype.updateCtx = function(){
var p = this,
canvasInfo = p.canvasInfo,
afterRoad = canvasInfo.height/4,
ranX = p.ranX;
if(p.y < afterRoad){
if(Math.random() > 0.5){
p.x += ranX/2;
}
p.y -= 2.5;
if(p.alpha <= 0.02){
p.alpha = 0;
}else{
p.alpha -= 0.02;
}
}else if(p.y > afterRoad && p.y < afterRoad*2){
p.x += ranX/2;
p.y -= 3;
p.alpha -= 0.01;
}else{
p.x += ranX;
p.y -= 4;
}
if(p.imgWidth < p.imgInfo.width){
p.imgWidth += 1;
p.imgHeight += 1;
}
} function deepCopy(p,c){
/*@param p [必选] [对象] 被克隆对象
**c :[可选] p对象被克隆到c身上,c被改变
**返回值为深度克隆后的c*/
var c= c || {};
for(var i in p){
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
arguments.callee(p[i],c[i]);
} else {
c[i] = p[i];
}
}
return c;
};
// 至少传入2个参数,传入的参数都将会被深度复制,不会影响原对象,然后返回扩展后的新对象
function hrExtend() { //扩展对象
var args = arguments;
if (args.length < 2) return;
var temp = deepCopy(args[0]); //调用复制对象方法
for (var n = 1; n < args.length; n++) {
for (var i in args[n]) {
temp[i] = args[n][i];
}
}
return temp;
}
window.JumpBubble = JumpBubble;
})(window);