音乐播放器
项目效果
音乐播放器的简述
“背景图片”
图片 首先运用canvas的方式 将图片通过drawimage的方法画到canvas上,然后运用canvas的一个方法 getimagedata 获取图片每一个像素点的 rgba的像素 就是
获取data里面rgba的数据 在网上查到高斯模糊的算法 就是好像对data里的rgba的数据进行数学方式的处理 处理之后将data输出 canvas的 putImageData()
将图像数据放回画布 重新放进canvas里 然后利用canvas的todataurl方法转化成base的方式 就是这个编码的格式 来当作他背景图片的src
“移动端的兼容”
html文件里用了meta标签
<meta name="viewport" content="width=device-width initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
背景图片考虑到不同手机的分辨率 不定宽 用 "background-size:cover; "尽可能把容器覆盖上
“页面的结构”
就是分成了两个模块,采用模块的方式写的,页面主要分为两个大的部分 一个是音频管理对象 用来管理播放器本身的一些功能就比如 播放 暂停 上一首 下一首点赞
另一个就是控制对象 controlmanager 控制对象主要是针对用户来控制播放器的一些功能和方法
“获取信息”
我们将歌曲的信息,歌曲的图片,歌曲的播放时长,作者,专辑信息之类的利用json的形式封装到后台 进行模拟后台的数据
用了ajax的方式进行获取 利用回调函数来处理以及渲染页面
音频管理对象(audioManager.js)
首先我们定义一个构造函数,在构造函数里面定义一些方法以及事件,方法:
音频播放,音频暂停,加载音频资源,修改音频当前播放时间;
渲染当前歌曲总时长,当前时长以及进度条的位置,进度条停止和开始,
歌单的渲染,歌单的隐藏和显示,
渲染当前歌曲信息,背景图片,播放按钮样式
事件:ended,当当前音频播放结束触发ended事件,触发点击下一首事件
音频控制播放器
绑定了一些点击事件以及拖拽事件,
点击事件:点击上一首后下一首按钮时,修改当前歌曲索引值,加载歌单资源,修改当前页面样式;
点击播放/暂停按钮,触发音频播放/暂停,渲染播放按钮样式;
点击显示歌单按钮,渲染歌单,点击关闭,将歌单隐藏;
点击歌单中某一首歌曲的时候,重新渲染歌单,加载歌曲资源,歌曲播放
拖拽事件:
在body上面绑定touchstart,touchmove,touchend事件,在touchstart事件触发的时候,进度条的动画停止,记录停止的时间;
touchmove,计算拖拽的百分比 更新我们的当前时间和进度条;
touchend,修改音频当前播放时间继续播放,开启进度条动画
进度条
由两个div实现,通过定位,将后一个div放在前一个div上,最开始上面的div设置为translate(-100%),然后根据当前时长,歌曲总时长,计算百分比,修改translate的值,实现进度条动画,动画效果是通过requestAnimitionFrame实现的,判断进度条的百分比是否达到1,没有则继续渲染,有则触发点击下一首事件
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link rel="stylesheet" href="../css/index.css">
</head>
<body>
<div class="wrapper">
<div class="song-img">
<div class="img-wrapper">
<img src="" alt="">
</div>
</div>
<div class="song-info">
</div>
<div class="pro">
<div class="cur-time">00:00</div>
<div class="pro-wrapper">
<div class="pro-bottom"></div>
<div class="pro-top">
<div class="slider-point"></div>
</div>
</div>
<div class="all-time">04:10</div>
</div>
<div class="control">
<div class="btn-wrapper like-btn"></div>
<div class="btn-wrapper prev-btn"></div>
<div class="btn-wrapper play-btn"></div>
<div class="btn-wrapper next-btn"></div>
<div class="btn-wrapper list-btn"></div>
</div>
</div>
<script src = "../js/zepto.min.js"></script>
<script src = "../js/gaussBlur.js"></script>
<script src = "../js/render.js"></script>
<script src = "../js/controlManager.js"></script>
<script src = "../js/audioManager.js"></script>
<script src = "../js/processor.js"></script>
<script src = "../js/playlist.js"></script>
<script src = "../js/index.js"></script>
</body>
</html>
gulp.js
var gulp = require("gulp");
var imagemin = require("gulp-imagemin") //图片压缩
var htmlclean = require("gulp-htmlclean"); //去掉html空格,进行压缩
var uglify = require("gulp-uglify"); //js压缩
var stripDebug = require("gulp-strip-debug"); //去掉调试语句
var concat = require("gulp-concat"); //没使用
var deporder = require("gulp-deporder"); //没使用
var less = require("gulp-less"); //将less转成css
var postcss = require("gulp-postcss"); //cssnano,autoprefixer的依赖
var autoprefixer = require("autoprefixer"); //兼容css3属性,添加前缀
var cssnano = require("cssnano"); //css代码压缩
var connect = require("gulp-connect");
var folder = {
src : "src/",
dist : "dist/"
}
var devMode = process.env.NODE_ENV !== "production";
//流操作 task running
gulp.task("html",function(){
var page = gulp.src(folder.src + "html/index.html")
.pipe(connect.reload());
if(!devMode){
page.pipe(htmlclean());
}
page.pipe(gulp.dest(folder.dist + "html/"))
})
gulp.task("images",function(){
gulp.src(folder.src + "images/*")
.pipe(imagemin())
.pipe(gulp.dest(folder.dist+"images/"))
})
gulp.task("js",function(){
var js = gulp.src(folder.src+"js/*")
.pipe(connect.reload());
if(!devMode){ //开发模式不压缩,生产模式压缩
js.pipe(uglify()) //代码压缩
.pipe(stripDebug()) //去掉调试语句
}
js.pipe(gulp.dest(folder.dist+"js/"))
})
gulp.task("css",function(){
var css = gulp.src(folder.src+"css/*")
.pipe(connect.reload())
.pipe(less()); //将less转成css
var options = [autoprefixer()]; //添加前缀,css3一些属性需要兼容,添加前缀
if(!devMode){
options.push(cssnano()) //css代码压缩
}
css.pipe(postcss(options))
.pipe(gulp.dest(folder.dist + "css/"))
})
gulp.task("watch",function(){
gulp.watch(folder.src + "html/*",["html"]);
gulp.watch(folder.src + "images/*",["images"]);
gulp.watch(folder.src + "js/*",["js"]);
gulp.watch(folder.src + "css/*",["css"]);
})
gulp.task("server",function(){
connect.server({
port : "8081",
livereload : true
});
})
gulp.task("default",["html","images","js","css","watch","server"]);
index.js
获取数据,绑定点击事件,绑定touch事件
var $ = window.Zepto;
var root = window.player;
var $scope = $(document.body);
var songList; //存获取来的data
var controlmanager; //创建的改变索引值的实例
var audio = new root.audioManager(); //创建一个音频实例
function bindClick(){
$scope.on("play:change",function(event,index,flag){
audio.setAudioSource(songList[index].audio);
if(audio.status == "play"||flag){ //如果当前的状态是播放的话,加载完新的音频资源,播放
audio.play();
root.processor.start(); //
}
root.processor.renderAllTime(songList[index].duration) //渲染歌曲时间
root.render(songList[index]); //渲染页面
root.processor.updata(0); //渲染curtime,以及进度条的位置
})
//移动端click有300ms延迟
$scope.on("click",".prev-btn",function(){
var index = controlmanager.prev();
$scope.trigger("play:change",index); //渲染整个页面
})
$scope.on("click",".next-btn",function(){
var index = controlmanager.next();
$scope.trigger("play:change",index);
})
$scope.on("click",".play-btn",function(){
if(audio.status == "play"){
audio.pause();
root.processor.stop();
}else{
root.processor.start();
audio.play();
}
$(this).toggleClass("playing"); //改变play-btn的样式
})
$scope.on("click",".list-btn",function(){
root.playList.show(controlmanager);
})
}
function bindTouch(){
var $slidePoint = $scope.find(".slider-point");
var offset = $scope.find(".pro-wrapper").offset();
var left = offset.left;
var width = offset.width;
//绑定拖拽事件 开始拖拽 : 取消进度条渲染
$slidePoint.on("touchstart",function(){
root.processor.stop();
}).on("touchmove",function(e){
//计算拖拽的百分比 更新我们的当前时间和进度条
var x = e.changedTouches[0].clientX;
var percent = (x - left) / width;
if(percent > 1 || percent < 0){
percent = 0;
}
root.processor.updata(percent)
}).on("touchend",function(e){
//计算百分百 跳转播放 重新开始进度条渲染
var x = e.changedTouches[0].clientX;
var percent = (x - left) / width;
if(percent > 1 || percent < 0){
percent = 0;
}
var curDuration = songList[controlmanager.index].duration;
var curTime = curDuration * percent;
audio.jumpToplay(curTime);
root.processor.start(percent);
$scope.find(".play-btn").addClass("playing");
})
}
function getData(url){ //请求数据,请求到数据之后,进行img渲染,imgInfo,背景图片,时间
$.ajax({
type : "GET",
url : url,
success : function(data){
bindClick();
bindTouch();
root.playList.renderList(data);
controlmanager = new root.controlManager(data.length);
songList = data;
$scope.trigger("play:change",0);
},
error : function(){
console.log("error")
}
})
}
getData("../mock/data.json")
gaussBlur.js 高斯函数
渲染背景图皮
/* requires:
zepto.min.js
*/
(function ($, root) {
'use strict';
function gaussBlur(imgData) {
var pixes = imgData.data;
var width = imgData.width;
var height = imgData.height;
var gaussMatrix = [],
gaussSum = 0,
x, y,
r, g, b, a,
i, j, k, len;
var radius = 10;
var sigma = 5;
a = 1 / (Math.sqrt(2 * Math.PI) * sigma);
b = -1 / (2 * sigma * sigma);
//生成高斯矩阵
for (i = 0, x = -radius; x <= radius; x++, i++) {
g = a * Math.exp(b * x * x);
gaussMatrix[i] = g;
gaussSum += g;
}
//归一化, 保证高斯矩阵的值在[0,1]之间
for (i = 0, len = gaussMatrix.length; i < len; i++) {
gaussMatrix[i] /= gaussSum;
}
//x 方向一维高斯运算
for (y = 0; y < height; y++) {
for (x = 0; x < width; x++) {
r = g = b = a = 0;
gaussSum = 0;
for (j = -radius; j <= radius; j++) {
k = x + j;
if (k >= 0 && k < width) {//确保 k 没超出 x 的范围
//r,g,b,a 四个一组
i = (y * width + k) * 4;
r += pixes[i] * gaussMatrix[j + radius];
g += pixes[i + 1] * gaussMatrix[j + radius];
b += pixes[i + 2] * gaussMatrix[j + radius];
// a += pixes[i + 3] * gaussMatrix[j];
gaussSum += gaussMatrix[j + radius];
}
}
i = (y * width + x) * 4;
// 除以 gaussSum 是为了消除处于边缘的像素, 高斯运算不足的问题
// console.log(gaussSum)
pixes[i] = r / gaussSum;
pixes[i + 1] = g / gaussSum;
pixes[i + 2] = b / gaussSum;
// pixes[i + 3] = a ;
}
}
//y 方向一维高斯运算
for (x = 0; x < width; x++) {
for (y = 0; y < height; y++) {
r = g = b = a = 0;
gaussSum = 0;
for (j = -radius; j <= radius; j++) {
k = y + j;
if (k >= 0 && k < height) {//确保 k 没超出 y 的范围
i = (k * width + x) * 4;
r += pixes[i] * gaussMatrix[j + radius];
g += pixes[i + 1] * gaussMatrix[j + radius];
b += pixes[i + 2] * gaussMatrix[j + radius];
// a += pixes[i + 3] * gaussMatrix[j];
gaussSum += gaussMatrix[j + radius];
}
}
i = (y * width + x) * 4;
pixes[i] = r / gaussSum;
pixes[i + 1] = g / gaussSum;
pixes[i + 2] = b / gaussSum;
}
}
//end
return imgData;
}
// 模糊图片
function blurImg(img, ele) {
var w = img.width,
h = img.height,
canvasW = 40,
canvasH = 40;
var canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d');
canvas.width = canvasW;
canvas.height = canvasH;
ctx.drawImage(img, 0, 0, w, h, 0, 0, canvasW, canvasH);
var pixel = ctx.getImageData(0, 0, canvasH, canvasH);
gaussBlur(pixel);
ctx.putImageData(pixel, 0, 0);
var imageData = canvas.toDataURL();
ele.css('background-image', 'url(' + imageData + ')');
}
root.blurImg = blurImg;
})(window.Zepto, window.player || (window.player = {}));
controlManage.js
索引值改变
(function($,root){ //控制索引
function controlManager(len){
this.index = 0;
this.len = len;
}
controlManager.prototype = {
prev : function(){
return this.getIndex(-1);
},
next : function(){
return this.getIndex(1);
},
getIndex : function(val){
var index = this.index;
var len = this.len;
var curIndex = (index + val + len) % len;
this.index = curIndex;
return curIndex;
}
}
root.controlManager = controlManager;
})(window.Zepto,window.player || (window.player = {}));
render.js 渲染页面
渲染img,bgImg,info,喜欢的样式
(function($,root){
var $scope = $(document.body);
console.log(root)
//渲染当前这首歌的信息
function renderInfo(info){
var html = '<div class="song-name">'+info.song+'</div>'+
'<div class="singer-name">'+info.singer+'</div>'+
'<div class="album-name">'+info.album+'</div>';
$scope.find(".song-info").html(html)
}
//渲染当前这首歌的图片
function renderImg(src){
var img = new Image();
img.onload = function(){
root.blurImg(img,$scope); //bgimg 高斯模糊
$scope.find(".song-img img").attr("src",src)
}
img.src = src;
}
function renderIsLike(isLike){
if(isLike){ //心的样式
$scope.find(".like-btn").addClass("liking");
}else{
$scope.find(".like-btn").removeClass("liking");
}
}
root.render = function(data){
renderInfo(data);
renderImg(data.image);
renderIsLike(data.isLike)
}
})(window.Zepto,window.player || (window.player = {}))
audioManage.js
音频播放,暂停,监听音频是否ended,如果结束,触发下一首按钮的点击事件,修改当前播放音乐的位置
(function($,root){
var $scope = $(document.body);
function audioManager(){
this.audio = new Audio();
this.status = "pause";
// this.bindEvent();
}
audioManager.prototype = {
//绑定监听歌曲是否播放完成事件,如果播放完成,触发下一首的点击事件
bindEvent:function(){
$(this.audio).on("ended",function(){
$scope.find(".next-btn").trigger("click");
})
},
play : function(){
this.audio.play();
this.status = "play";
},
pause : function(){
this.audio.pause();
this.status = "pause";
},
setAudioSource : function(src){
this.audio.src = src;
this.audio.load(); //将音频加载进来
},
jumpToplay : function(time){
this.audio.currentTime = time; //将音频的currentTime修改,继续播放
this.play();
}
}
root.audioManager = audioManager;
})(window.Zepto,window.player || (window.player ={}))
processor.js
渲染歌曲的时间,当前的播放时间,将时间转化成minute:second,更新进度条位置,进度条开始走,进度条结束
(function($,root){
var $scope = $(document.body); //进度条渲染,拖拽
var curDuration;
var frameId;
var lastPercent = 0;
var startTime;
//把秒转换成分和秒
function formatTime(duration){
duration = Math.round(duration); //取整
var minute = Math.floor(duration / 60); //向下取整,分钟
var second = duration - minute * 60; //秒数
if(minute < 10){
minute = "0" + minute;
}
if(second < 10){
second = "0" + second;
}
return minute + ":" +second;
}
function renderAllTime(duration){ //渲染歌曲总时长
lastPercent = 0;
curDuration = duration; //记录歌曲的时长
var allTime = formatTime(duration);
$scope.find(".all-time").html(allTime);
}
function updata(precent){ //渲染歌曲当前时长,以及进度条的位置
var curTime = precent * curDuration; //计算歌曲当前的时长
curTime = formatTime(curTime); //将当前事件装成minute:second
$scope.find(".cur-time").html(curTime); //渲染时间
var percentage = (precent - 1) * 100 + "%"; //控制进度条的位置
$scope.find(".pro-top").css({
transform : "translateX("+percentage+")"
})
}
function start(precentage){
lastPercent = precentage === undefined ? lastPercent : precentage;
cancelAnimationFrame(frameId); //清理定时器
startTime = new Date().getTime();
function frame(){
var curTime = new Date().getTime();
var precent = lastPercent + (curTime - startTime) / (curDuration * 1000); //
if(precent < 1){
frameId = requestAnimationFrame(frame); //创建定时器
updata(precent);
}else{ //表示进度条到了最右面,歌曲播放结束,触发下一首的按钮的点击事件
cancelAnimationFrame(frameId);
$scope.find(".next-btn").trigger("click");
}
}
frame()
}
function stop(){ //进度条停止,记录当前停止的位置
var stopTime = new Date().getTime();
lastPercent = lastPercent + (stopTime - startTime) / (curDuration * 1000);
cancelAnimationFrame(frameId);
}
root.processor = {
renderAllTime : renderAllTime,
start : start,
stop : stop,
updata : updata
}
})(window.Zepto,window.player || (window.player));
playlist.js
渲染播放列表,添加点击事件,渲染当前播放歌曲的样式
(function($,root){ //播放列表
var $scope = $(document.body);
var control;
var $playList = $("<div class = 'play-list'>"+
"<div class='play-header'>播放列表</div>" +
"<ul class = 'list-wrapper'></ul>" +
"<div class='close-btn'>关闭</div>"+
"</div>")
//渲染我们的播放列表dom
function renderList(songList){
var html = '';
for(var i = 0;i < songList.length;i++){
html += "<li><h3 >"+songList[i].song+"-<span>"+songList[i].singer+"</span></h3></li>"
}
$playList.find("ul").html(html);
$scope.append($playList);
bindEvent();
}
function show(controlmanager){
control = controlmanager;
$playList.addClass("show"); //将歌单展示出来
signSong(control.index); //将index的歌曲渲染不同的样式
}
function bindEvent(){
$playList.on("click",".close-btn",function(){
$playList.removeClass("show")
})
$playList.find("li").on("click",function(){
var index = $(this).index(); //获取当前点击的歌曲的索引
signSong(index); //渲染样式
control.index = index; //改变索引
$scope.trigger("play:change",[index,true]);
$scope.find(".play-btn").addClass("playing");
setTimeout(function(){
$playList.removeClass("show")
}, 200);
})
}
function signSong(index){ //给当前点击的li添加sign样式,原来的删除
$playList.find(".sign").removeClass("sign");
$playList.find("ul li").eq(index).addClass("sign");
}
root.playList = {
renderList : renderList,
show : show
}
})(window.Zepto,window.player || (window.player = {}))