├── LICENSE ├── README.md ├── _config.yml ├── legoflow.json ├── package.json └── src ├── _index.html ├── assets ├── img │ ├── gift-lit │ │ ├── level1.png │ │ ├── level2.png │ │ ├── level3.png │ │ ├── level4.png │ │ └── level5.png │ └── like │ │ └── heart │ │ ├── audience │ │ ├── like01.png │ │ ├── like02.png │ │ ├── like03.png │ │ ├── like04.png │ │ ├── like05.png │ │ ├── like06.png │ │ └── like07.png │ │ └── user │ │ ├── like01.png │ │ ├── like02.png │ │ └── like03.png ├── js │ └── svga │ │ └── svga.min.js └── svga │ ├── angel.svga │ ├── girl.svga │ ├── kingset.svga │ ├── posche.svga │ ├── rainbowrose.svga │ ├── rose.svga │ └── sample.svga ├── css └── main.css ├── js ├── component │ ├── danmaku │ │ ├── index.js │ │ ├── style │ │ │ └── index.scss │ │ └── tpl │ │ │ └── index.js │ ├── download-app │ │ └── index.js │ ├── gift │ │ ├── gift-large │ │ │ ├── config.js │ │ │ ├── index.js │ │ │ └── style │ │ │ │ └── index.scss │ │ └── gift-lit │ │ │ ├── config.js │ │ │ ├── index.js │ │ │ ├── module │ │ │ └── combo.js │ │ │ ├── style │ │ │ └── index.scss │ │ │ └── tpl │ │ │ └── index.js │ ├── header │ │ ├── index.js │ │ ├── style │ │ │ └── index.scss │ │ └── tpl │ │ │ └── index.js │ ├── layout │ │ ├── index.js │ │ ├── style │ │ │ └── index.scss │ │ └── tpl │ │ │ └── index.js │ ├── like │ │ ├── config.js │ │ ├── index.js │ │ ├── module │ │ │ ├── anim.js │ │ │ ├── anim │ │ │ │ ├── anim.js │ │ │ │ ├── anim.like.js │ │ │ │ ├── random.js │ │ │ │ └── ticker.js │ │ │ └── frequent.js │ │ └── style │ │ │ └── index.scss │ ├── message │ │ ├── config.js │ │ ├── img │ │ │ ├── chat.png │ │ │ ├── gift.png │ │ │ ├── live.png │ │ │ └── share.png │ │ ├── index.js │ │ ├── style │ │ │ └── index.scss │ │ └── tpl │ │ │ └── index.js │ ├── player │ │ ├── config.js │ │ ├── img │ │ │ ├── icon-play.png │ │ │ └── loading.svg │ │ ├── index.js │ │ ├── style │ │ │ ├── live-end.scss │ │ │ └── live-in.scss │ │ └── tpl │ │ │ ├── live-end.js │ │ │ └── live-in.js │ ├── recommend-list │ │ ├── img │ │ │ └── ico-empty.png │ │ ├── index.js │ │ ├── style │ │ │ └── index.scss │ │ └── tpl │ │ │ └── index.js │ └── top-bar │ │ ├── img │ │ ├── app-logo.png │ │ └── icon-share.png │ │ ├── index.js │ │ ├── style │ │ └── index.scss │ │ └── tpl │ │ └── index.js ├── lib │ ├── anim │ │ ├── anim.js │ │ ├── anim.like.js │ │ ├── custom.js │ │ └── modules │ │ │ ├── random.js │ │ │ └── ticker.js │ ├── classlist-polyfill │ │ └── index.js │ ├── delegate │ │ └── index.js │ ├── es6-promise │ │ └── index.js │ ├── event │ │ └── index.js │ ├── fastclick │ │ └── index.js │ ├── jsonp │ │ └── index.js │ ├── lazy-load-img │ │ └── index.js │ ├── queue │ │ └── index.js │ ├── scrollTo │ │ └── index.js │ ├── socket.io │ │ └── index.js │ ├── sprite │ │ └── index.js │ ├── svga │ │ ├── svga-db.min.js │ │ ├── svga-worker.min.js │ │ └── svga.min.js │ └── swipe │ │ └── index.js ├── main.js └── public │ ├── api │ └── index.js │ ├── config │ └── index.js │ ├── index.js │ └── util │ ├── delegate-event.js │ ├── detect-android.js │ ├── get-url-param.js │ ├── index.js │ ├── preload-script.js │ ├── scroll-to.js │ ├── sleep.js │ └── sprite.js └── sass ├── _img.scss ├── base ├── _base.scss ├── _flexible.scss ├── _normalize.scss ├── _orientation.scss ├── _reset.scss └── _tool.scss └── main.scss /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 YY UED 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 为了提升品牌影响的影响力和推广产品,H5直播间已经成为了一款直播产品的必备要素。在网络直播盛行的这个风口浪尖,直播产品的模式越来越成熟,一个直播间所需要的元素和交互已经形成了稳定的模式。于是,你没理由完全要自己手动搭建一个H5直播间,因为H5Live提供了一套成熟稳定的H5直播间方案: 2 | 3 | 1. 组件化、配置化搭建,5分钟快速上手; 4 | 2. 涵盖公屏、大礼物、小礼物、弹幕、点赞等功能模块; 5 | 3. 配备高性能的礼物动画方案; 6 | 7 | ---- 8 | 9 | ![h5live](https://user-images.githubusercontent.com/1295348/31772948-c3eb733e-b4a6-11e7-83a8-2fbce55a62f8.png) 10 | 11 | 12 | ## 入门 13 | 14 | 为了加快开发效率,推荐你使用开箱即用的[LegoFlow](https://legoflow.com/)进行开发。当然,你也可以自己配置webpack进行开发,只需要支持ES6编译和SASS loader即可。 15 | 16 | ### 目录结构 17 | 18 | ```` 19 | js/ 20 | ├── component //组件 21 | |   ├── palyer 22 | |   ├── ... 23 | │   └── top-bar 24 | ├── lib //第三方库 25 | ├── public //公共,例如api,全局配置,工具等 26 | └── main.js //主入口 27 | ```` 28 | 29 | ## Issues 30 | 31 | 如有使用问题或建议,欢迎提issues 32 | 33 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /legoflow.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1.0.43", 3 | "type": "mobile", 4 | "assets": "", 5 | "es6": true, 6 | "webp": false, 7 | "legolib": false, 8 | "https": false, 9 | "hot": false, 10 | "isUglifyJs": true, 11 | "isPackVueStyle": false, 12 | "vue@2.1": false, 13 | "watch": [], 14 | "publicPath": "", 15 | "packImgSize": "", 16 | "packCommon": "", 17 | "other": "", 18 | "dist": "", 19 | "output": "", 20 | "shell": "", 21 | "alias": { 22 | }, 23 | "global": { 24 | }, 25 | "externals": {}, 26 | "proxy": {}, 27 | "ts": {}, 28 | "cache": "timestamp", 29 | "user.dev.args": {}, 30 | "user.build.args": {}, 31 | "env": {}, 32 | "build.move": [], 33 | "build.env": "", 34 | "build.zip": "", 35 | "img.folder": "", 36 | "html.folder": "", 37 | "js.folder": "", 38 | "export.folder": "" 39 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "H5Live", 3 | "version": "1.1.0", 4 | "repository": {}, 5 | "author": "UED.David", 6 | "license": "MIT" 7 | } -------------------------------------------------------------------------------- /src/_index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | H5Live 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/assets/img/gift-lit/level1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/gift-lit/level1.png -------------------------------------------------------------------------------- /src/assets/img/gift-lit/level2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/gift-lit/level2.png -------------------------------------------------------------------------------- /src/assets/img/gift-lit/level3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/gift-lit/level3.png -------------------------------------------------------------------------------- /src/assets/img/gift-lit/level4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/gift-lit/level4.png -------------------------------------------------------------------------------- /src/assets/img/gift-lit/level5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/gift-lit/level5.png -------------------------------------------------------------------------------- /src/assets/img/like/heart/audience/like01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/like/heart/audience/like01.png -------------------------------------------------------------------------------- /src/assets/img/like/heart/audience/like02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/like/heart/audience/like02.png -------------------------------------------------------------------------------- /src/assets/img/like/heart/audience/like03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/like/heart/audience/like03.png -------------------------------------------------------------------------------- /src/assets/img/like/heart/audience/like04.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/like/heart/audience/like04.png -------------------------------------------------------------------------------- /src/assets/img/like/heart/audience/like05.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/like/heart/audience/like05.png -------------------------------------------------------------------------------- /src/assets/img/like/heart/audience/like06.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/like/heart/audience/like06.png -------------------------------------------------------------------------------- /src/assets/img/like/heart/audience/like07.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/like/heart/audience/like07.png -------------------------------------------------------------------------------- /src/assets/img/like/heart/user/like01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/like/heart/user/like01.png -------------------------------------------------------------------------------- /src/assets/img/like/heart/user/like02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/like/heart/user/like02.png -------------------------------------------------------------------------------- /src/assets/img/like/heart/user/like03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/img/like/heart/user/like03.png -------------------------------------------------------------------------------- /src/assets/svga/angel.svga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/svga/angel.svga -------------------------------------------------------------------------------- /src/assets/svga/girl.svga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/svga/girl.svga -------------------------------------------------------------------------------- /src/assets/svga/kingset.svga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/svga/kingset.svga -------------------------------------------------------------------------------- /src/assets/svga/posche.svga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/svga/posche.svga -------------------------------------------------------------------------------- /src/assets/svga/rainbowrose.svga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/svga/rainbowrose.svga -------------------------------------------------------------------------------- /src/assets/svga/rose.svga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/svga/rose.svga -------------------------------------------------------------------------------- /src/assets/svga/sample.svga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/assets/svga/sample.svga -------------------------------------------------------------------------------- /src/js/component/danmaku/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component 弹幕模块 3 | * @author wilson 4 | */ 5 | 6 | import style from'./style/index.scss'; 7 | import createTpl from './tpl/index.js'; 8 | 9 | const danmakuElement = document.querySelector('[data-component="danmaku"]'); 10 | danmakuElement.innerHTML = ``; 11 | 12 | let main = { 13 | // 记录插入的位置 14 | slotArr: [0, 0], 15 | 16 | // 选择下一条弹幕插入的位置 17 | chooseMinPos: (slotArr) => { 18 | let minNum = Math.min.apply(Math, slotArr); 19 | let minPos = slotArr.indexOf(minNum); 20 | if(minPos < 0) return 0; 21 | return minPos; 22 | }, 23 | 24 | // 准备下一条弹幕 25 | followUp: (wrapDomTemp, minPos, danmaku) => { 26 | let slotArr = main.slotArr; 27 | let minPosTemp = slotArr[minPos]; 28 | if (minPosTemp === 0) { 29 | return; 30 | }else if (minPosTemp < 0) { 31 | minPosTemp = 0 32 | } else if(minPosTemp === 1) { 33 | minPosTemp -= 1; 34 | if(danmaku.paused) { 35 | danmaku.resume(); 36 | } 37 | } 38 | slotArr[minPos] = minPosTemp; 39 | main.slotArr = slotArr; 40 | }, 41 | 42 | // 创建弹幕 43 | create: (data) => { 44 | let danmakuItem = document.createElement('div'); 45 | danmakuItem.setAttribute('class', 'live-danmaku__item'); 46 | const tpl = createTpl(data); 47 | danmakuItem.innerHTML = tpl; 48 | danmakuElement.appendChild(danmakuItem); 49 | return danmakuItem; 50 | }, 51 | 52 | // 初始化队列 53 | initQueue: () => { 54 | const speed = 96; 55 | let wrapDom = danmakuElement; 56 | let danmaku = queue.queue(function (task, callback) { 57 | let danmakuItem; 58 | let danmakuItemWidth; 59 | let danmakuItemHeight; 60 | let translateWidth; 61 | let durationTime; 62 | let slotArr = main.slotArr; 63 | let minPos = main.chooseMinPos(slotArr); 64 | if(slotArr[minPos] === 0) { 65 | slotArr[minPos] += 1; 66 | danmakuItem = main.create(task); 67 | danmakuItemWidth = danmakuItem.offsetWidth; 68 | danmakuItemHeight = danmakuItem.offsetHeight; 69 | translateWidth = document.body.clientWidth + danmakuItemWidth + 8; 70 | durationTime = (translateWidth / speed); 71 | let topPos = minPos * (danmakuItemHeight + 4); 72 | danmakuItem.style.top = `${topPos}px`; 73 | danmakuItem.style.webkitTransform = `translate(${-translateWidth}px, 0)`; 74 | danmakuItem.style.transform = `translate(${-translateWidth}px, 0)`; 75 | danmakuItem.style.webkitTransitionDuration = `${durationTime}s`; 76 | danmakuItem.style.transitionDuration = `${durationTime}s`; 77 | } 78 | 79 | main.slotArr = slotArr; 80 | util.sleep(Math.floor(durationTime * 1000 / 1.3)).then(() => { 81 | main.followUp(wrapDom, minPos, danmaku) 82 | }) 83 | callback(danmakuItem, slotArr, durationTime); 84 | }, 2); 85 | 86 | return danmaku; 87 | }, 88 | 89 | // 添加队列 90 | addQueue: (danmakuItemObj, danmaku, isLast) => { 91 | if(isLast) { 92 | // 插到队列尾部 93 | danmaku.push(danmakuItemObj, function (danmakuItem, slotArr, durationTime) { 94 | if(slotArr[0] == 1 && slotArr[1] == 1) { 95 | if(!danmaku.paused) { 96 | danmaku.pause(); 97 | } 98 | } 99 | 100 | util.sleep(Math.ceil(durationTime * 1000)).then(() => { 101 | danmakuElement.removeChild(danmakuItem) 102 | }); 103 | }); 104 | } else { 105 | // 插到队列首部 106 | danmaku.unshift(danmakuItemObj, function (danmakuItem, slotArr, durationTime) { 107 | if(slotArr[0] == 1 && slotArr[1] == 1) { 108 | if(!danmaku.paused) { 109 | danmaku.pause(); 110 | } 111 | } 112 | 113 | util.sleep(Math.ceil(durationTime * 1000)).then(() => { 114 | danmakuElement.removeChild(danmakuItem) 115 | }); 116 | }); 117 | } 118 | } 119 | } 120 | 121 | export default { 122 | init(socket) { 123 | let danmaku = main.initQueue(); 124 | socket.ioSocket.on(socket.socketName, (data) => { 125 | let {nick, avatar, content} = data.data; 126 | let danmakuItemObj = { 127 | "nick": nick, 128 | "avatar": avatar, 129 | "content": content 130 | } 131 | main.addQueue(danmakuItemObj, danmaku, true); 132 | }); 133 | 134 | // 用户发弹幕优先显示 135 | // this.unShiftQueue = (danmakuItemObj) => { 136 | // main.addQueue(danmakuItemObj, danmaku, false); 137 | // } 138 | 139 | // 销毁弹幕 140 | this.destroy = () => { 141 | danmakuElement.style['display'] = 'none'; 142 | danmaku.tasks.length = 0; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/js/component/danmaku/style/index.scss: -------------------------------------------------------------------------------- 1 | .live-danmaku { 2 | position: absolute; 3 | left: 0; 4 | top: 3rem; 5 | width: 100%; 6 | height: 1.2rem; 7 | word-break: normal; 8 | word-wrap: normal; 9 | white-space:nowrap; 10 | pointer-events: none; 11 | // HACK: camera pc mode 12 | .camera-pc ~ & { 13 | top: 1.3rem; 14 | } 15 | } 16 | 17 | // .live-danmaku__row { 18 | // position: absolute; 19 | // left: 100%; 20 | // height: 0.54rem; 21 | // word-break: normal; 22 | // word-wrap: normal; 23 | // white-space:nowrap; 24 | // &.ext-row1 { 25 | // bottom: 0; 26 | // } 27 | // &.ext-row2 { 28 | // top: 0; 29 | // } 30 | // } 31 | 32 | // .live-danmaku__rowInner { 33 | // height: 0.54rem; 34 | // } 35 | 36 | .live-danmaku__item { 37 | display: inline-block; 38 | position: absolute; 39 | height: 0.54rem; 40 | left: 100%; 41 | padding-right: 0.24rem; 42 | margin-right: 0.24rem; 43 | background-color: rgba(46, 46, 46, 0.6); 44 | border-radius: 0.26rem; 45 | vertical-align: top; 46 | font-size: 0; 47 | transition: transform 8s linear; 48 | } 49 | 50 | .live-danmaku__avatar { 51 | position: absolute; 52 | left: 0.03rem; 53 | top: 0.05rem; 54 | width: 0.44rem; 55 | height: 0.44rem; 56 | border-radius: 50%; 57 | } 58 | 59 | .live-danmaku__nick { 60 | display: inline-block; 61 | vertical-align: top; 62 | max-width: 3rem; 63 | height: 0.54rem; 64 | padding: 0 0.18rem 0 0.54rem; 65 | font-size: 0.32rem; 66 | color: #e0b66c; 67 | font-weight: 500; 68 | line-height: 0.56rem; 69 | white-space: nowrap; 70 | word-wrap: normal; 71 | overflow: hidden; 72 | text-overflow: ellipsis; 73 | text-align: left; 74 | } 75 | 76 | .live-danmaku__content { 77 | display: inline-block; 78 | vertical-align: top; 79 | font-size: 0.32rem; 80 | color: #fff; 81 | font-weight: 500; 82 | line-height: 0.56rem; 83 | } 84 | -------------------------------------------------------------------------------- /src/js/component/danmaku/tpl/index.js: -------------------------------------------------------------------------------- 1 | let renderTpl = (data)=>{ 2 | let tpl = ` 3 | ${data.nick} 4 |

${data.nick}

5 |

${data.content}

6 | `; 7 | return tpl; 8 | } 9 | 10 | export default renderTpl; 11 | -------------------------------------------------------------------------------- /src/js/component/download-app/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component 下载逻辑 3 | * @description 点击下载按钮后的逻辑 4 | * @author wuweishuan 5 | */ 6 | /** 7 | * 状态类名 8 | * @type {String} 9 | */ 10 | const active = 'is-active'; 11 | /** 12 | * 按钮选择器 13 | * @type {String} 14 | */ 15 | const selector = '[data-role="download-app"]'; 16 | 17 | import delegate from 'delegate'; 18 | import downloadTips from 'component/download-tips/index.js'; // 下载弹窗 19 | 20 | document.addEventListener('touchstart', delegate(selector, function ( event ) { 21 | this.classList.add(active); 22 | }), false); 23 | 24 | document.addEventListener('touchend', delegate(selector, function ( event ) { 25 | this.classList.remove(active); 26 | }), false); 27 | 28 | document.addEventListener('touchcancel', delegate(selector, function ( event ) { 29 | this.classList.remove(active); 30 | }), false); 31 | 32 | document.addEventListener('click', delegate(selector, function ( event ) { 33 | if(window.CONFIG.downloadLink) { 34 | window.location.href = window.CONFIG.downloadLink; 35 | } else { 36 | // 如果没有下载链接就打开下载窗 37 | downloadTips.show(); 38 | } 39 | }), false); 40 | -------------------------------------------------------------------------------- /src/js/component/gift/gift-large/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 大礼物配置 3 | */ 4 | 5 | 6 | let config = { 7 | 'use-svga' : true, //大礼物动画使用SVGA(如果使用svga播放,svga文件需和页面同域) 8 | urlSVGA : '/assets/js/svga/svga.min.js', // svga库路径 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /src/js/component/gift/gift-large/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component 大礼物播放器 3 | * @author lixinliang 4 | */ 5 | 6 | import Event from '../../../lib/event/index.js'; 7 | import style from'./style/index.scss'; 8 | import config from'./config.js'; 9 | 10 | const componentSVGAPlayer = document.querySelector('[data-component="SVGAPlayer"]'); 11 | 12 | const giftLargeEvent = new Event; 13 | 14 | let cache = {}; 15 | let giftFromWeb = []; 16 | let giftFromUser = []; 17 | let readyToPlay = Promise.resolve(); 18 | 19 | let onSendGift = function () {}; 20 | 21 | let loadSVGA; 22 | 23 | export default { 24 | init ( giftSocket, giftMapObj ) { 25 | loadSVGA = Promise.all([ 26 | util.preloadScript(config.urlSVGA), 27 | // util.preloadScript(config.urlSVGADB), 28 | ]); 29 | return new Promise(( resolve ) => { 30 | componentSVGAPlayer.innerHTML = ``; 31 | this.send = ( svga ) => { 32 | giftFromUser.push(svga); 33 | // console.log('push'); 34 | readyToPlay = readyToPlay.then(play); 35 | }; 36 | // TODO: dev 37 | giftSocket.ioSocket.on(giftSocket.socketName, ( responese ) => { 38 | let data = responese.data; 39 | let svga = giftMapObj[data.propId].svga; 40 | if (svga) { 41 | giftFromWeb.push(svga); 42 | readyToPlay = readyToPlay.then(play); 43 | onSendGift({ 44 | uid: '', 45 | nickname: data.nick, 46 | name: giftMapObj[data.propId].name, 47 | }); 48 | } 49 | }); 50 | // TODO: dev 51 | this.on = giftLargeEvent.on.bind(giftLargeEvent); 52 | resolve(); 53 | }).catch((err) => console.log(err)); 54 | }, 55 | onSendGift ( callback ) { 56 | onSendGift = callback; 57 | }, 58 | setPosition () { 59 | let offsetTop = componentSVGAPlayer.offsetTop; 60 | let style = componentSVGAPlayer.style; 61 | style['transform'] = style['-webkit-transform'] = `translateY(${ -offsetTop/2 }px)`; 62 | }, 63 | destroy () { 64 | return new Promise(( resolve ) => { 65 | componentSVGAPlayer.style.display = 'none'; 66 | resolve(); 67 | }).catch((err) => console.log(err)); 68 | }, 69 | }; 70 | 71 | function play () { 72 | return new Promise(( resolve ) => { 73 | let url = giftFromUser.shift() || giftFromWeb.shift(); 74 | if (url) { 75 | getSVGA(url).then(playSVGA).then(resolve); 76 | } else { 77 | resolve(); 78 | } 79 | }).catch((err) => console.log(err)); 80 | } 81 | 82 | function getSVGA ( url ) { 83 | return new Promise(( resolve ) => { 84 | loadSVGA.then(() => { 85 | if (cache[url]) { 86 | resolve(cache[url]); 87 | } else { 88 | let a = document.createElement('a'); 89 | a.href = url; 90 | let canvas = document.createElement('canvas'); 91 | canvas.id = `svga-player__${ Object.keys(cache).length }`; 92 | canvas.width = canvas.height = 500; 93 | canvas.style.width = `400px`; 94 | canvas.style.height = `400px`; 95 | canvas.clearsAfterStop = true; 96 | componentSVGAPlayer.appendChild(canvas); 97 | 98 | var player = new SVGA.Player(`#${ canvas.id }`); 99 | var parser = new SVGA.Parser(); 100 | player.loops = 1; 101 | parser.load(url, function(videoItem) { 102 | player.canvas = canvas; 103 | componentSVGAPlayer.removeChild(canvas); 104 | canvas.classList.add('is-active'); 105 | resolve({ 106 | player, 107 | videoItem, 108 | }); 109 | }) 110 | } 111 | }); 112 | }).catch((err) => console.log(err)); 113 | } 114 | 115 | function playSVGA ( options ) { 116 | let {player, videoItem} = options; 117 | return new Promise(( resolve ) => { 118 | componentSVGAPlayer.appendChild(player.canvas); 119 | player.onFinished(() => { 120 | componentSVGAPlayer.removeChild(player.canvas); 121 | resolve(); 122 | }); 123 | player.setVideoItem(videoItem); 124 | player.startAnimation(); 125 | }).catch((err) => console.log(err)); 126 | } 127 | -------------------------------------------------------------------------------- /src/js/component/gift/gift-large/style/index.scss: -------------------------------------------------------------------------------- 1 | .live-SVGAPlayer { 2 | position: absolute; 3 | left: 0; 4 | box-sizing: content-box; 5 | height: 0; 6 | width: 100%; 7 | padding-top: 100%; 8 | // top: 50%; 9 | // transform: translateY(-50%); 10 | bottom: 3.3rem + 0.9; 11 | // bottom: 3.3rem + .9rem + .46rem; 12 | // margin-top: (-3.3rem - .9rem - .46rem)/2; 13 | // margin-top: -1.7rem; 14 | pointer-events: none; 15 | canvas { 16 | position: absolute; 17 | display: block; 18 | width: 100%; 19 | height: 100%; 20 | top: 0; 21 | left: 0; 22 | opacity: 0; 23 | &.is-active { 24 | opacity: 1; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/js/component/gift/gift-lit/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 礼物配置 3 | */ 4 | 5 | 6 | let config = { 7 | // 小礼物连送时显示的等级图片 8 | levelMap: [ 9 | { 10 | "levelNum": 1, 11 | "levelImg": "https://legox.org/assets/project/h5live/img/gift-lit/level1.png" 12 | }, 13 | { 14 | "levelNum": 10, 15 | "levelImg": "https://legox.org/assets/project/h5live/img/gift-lit/level2.png" 16 | }, 17 | { 18 | "levelNum": 20, 19 | "levelImg": "https://legox.org/assets/project/h5live/img/gift-lit/level3.png" 20 | }, 21 | { 22 | "levelNum": 30, 23 | "levelImg": "https://legox.org/assets/project/h5live/img/gift-lit/level4.png" 24 | }, 25 | { 26 | "levelNum": 40, 27 | "levelImg": "https://legox.org/assets/project/h5live/img/gift-lit/level5.png" 28 | } 29 | ], 30 | 31 | // 礼物显示时间ms 32 | duration: 3000 33 | } 34 | 35 | export default config; 36 | -------------------------------------------------------------------------------- /src/js/component/gift/gift-lit/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component 礼物模块 3 | * @author wilson 4 | */ 5 | 6 | import sprite from '../../../lib/sprite/index.js'; // 礼物动画 7 | import combo from './module/combo.js'; // 礼物连击 8 | import style from'./style/index.scss'; 9 | import createTpl from './tpl/index.js'; 10 | 11 | const componentGift = document.querySelector('[data-component="gift"]'); 12 | componentGift.innerHTML = ``; 13 | 14 | let giftMap = null; 15 | 16 | let main = { 17 | // 创建dom 18 | create: (data, position) => { 19 | main.comboId[`line${position}`] = data.comboId; 20 | let giftItem = document.createElement('div'); 21 | giftItem.setAttribute('class', `live-gift__item ext-line${position} gift-ani`); 22 | const tpl = createTpl(data); 23 | giftItem.innerHTML = tpl; 24 | componentGift.appendChild(giftItem); 25 | }, 26 | 27 | // 礼物连送ID 28 | comboId: { 29 | 'line1': '', 30 | 'line2': '', 31 | }, 32 | 33 | // 礼物连送对象 34 | comboObj: { 35 | 'line1': null, 36 | 'line2': null, 37 | }, 38 | 39 | // 礼物位置数 40 | giftQueueNum: 0, 41 | 42 | // 礼物推送 43 | pushGift: (queue, position) => { 44 | // 创建礼物dom,并指定显示位置 45 | main.create(queue.task, position); 46 | let currentGiftElement = document.querySelector(`.ext-line${position}`); 47 | let giftImgDom = currentGiftElement.querySelector('.live-gift__giftImg'); // 礼物图片动画 48 | let aniDom = currentGiftElement.querySelector('.live-gift__giftNum'); 49 | let comboImgDom = aniDom.children[1]; 50 | let originalDom = aniDom.children[2]; 51 | 52 | sprite(giftImgDom, giftMap[queue.task.propId].spriteImage, giftMap[queue.task.propId].spriteDuration); 53 | giftImgDom.dataset.status = 'play'; 54 | 55 | main.comboObj[`line${position}`] = new combo({ 56 | endNum: queue.task.giftNum, 57 | aniDom: aniDom, 58 | comboImgDom: comboImgDom, 59 | numDom: originalDom, 60 | cb: function(aniDom) { 61 | currentGiftElement.classList.add('is-hide'); 62 | 63 | //重置 64 | main.comboObj[`line${position}`] = null; 65 | main.comboId[`line${position}`] = ''; 66 | 67 | setTimeout(() => { 68 | componentGift.removeChild(currentGiftElement); 69 | queue.callback(queue.task); 70 | }, 500); 71 | } 72 | }) 73 | }, 74 | 75 | // 初始化队列 76 | initQueue: () => { 77 | let timer = null; 78 | //礼物消息队列 79 | let giftLit = queue.queue(function (task, callback) { 80 | let extPos1Dom = document.querySelector('.ext-line1'); 81 | let extPos2Dom = document.querySelector('.ext-line2'); 82 | let currentQuene = { 83 | task, 84 | callback, 85 | }; 86 | 87 | main.giftQueueNum++; 88 | if(main.giftQueueNum <= 2) { 89 | if(extPos1Dom) { 90 | main.pushGift(currentQuene, 2); 91 | } else { 92 | main.pushGift(currentQuene, 1); 93 | } 94 | } else { 95 | // 超出队列显示,队列暂停 96 | giftLit.pause(); 97 | } 98 | }, 2); 99 | 100 | return giftLit; 101 | }, 102 | 103 | // 加入队列 104 | addQueue: (data, giftLitle, isLast) => { 105 | let {nick, avatar, propId, propCount, comboId} = data.data; 106 | let giftItemObj = { 107 | "propId": propId, 108 | "nick":nick, 109 | "avatar": avatar, 110 | "giftNum": propCount, 111 | "giftName": giftMap[propId].name, 112 | "giftImg": giftMap[propId].thumb, 113 | "comboId": comboId 114 | }; 115 | 116 | if(isLast) { 117 | // 插到队列尾部 118 | giftLitle.push(giftItemObj, main.queueCallback(giftLitle)); 119 | } else { 120 | // 插到队列首部 121 | giftLitle.unshift(giftItemObj, main.queueCallback(giftLitle)); 122 | } 123 | 124 | }, 125 | 126 | // 队列回调 127 | queueCallback: (giftLitle) => { 128 | if(giftLitle.tasks.length > 0) { 129 | let tempArr = giftLitle.tasks, 130 | tempArrLen = tempArr.length; 131 | let comboArr = []; 132 | let comboTotal = 0; 133 | let beLeftArr = []; 134 | let firstTaskComboid = tempArr[0].data.comboId; 135 | for(let i = 0; i < tempArrLen;i++) { 136 | if(tempArr[i].data.comboId == firstTaskComboid) { 137 | comboArr.push(tempArr[i]); 138 | comboTotal += tempArr[i].data.giftNum; 139 | } else { 140 | beLeftArr.push(tempArr[i]); 141 | } 142 | } 143 | 144 | if(comboArr.length > 1) { 145 | // 累积comboid相同的礼物 146 | comboArr[0].data.giftNum = comboTotal; 147 | comboArr.length = 1; 148 | } 149 | 150 | // 拼接队列 151 | giftLitle.tasks = comboArr.concat(beLeftArr, giftLitle.tasks.slice(tempArrLen)); 152 | } 153 | main.giftQueueNum--; 154 | // 恢复队列 155 | if(giftLitle.paused) { 156 | giftLitle.resume(); 157 | } 158 | }, 159 | 160 | // 礼物推送判断 161 | giftHandle: (data, giftLit, callback) => { 162 | if(giftMap[data.propId]) { 163 | let svga = giftMap[data.propId].svga; 164 | if(!svga) { 165 | let comboId = data.comboId; 166 | if(comboId == main.comboId['line1']) { 167 | main.comboObj['line1'].update(data.propCount); 168 | } else if(comboId == main.comboId['line2']) { 169 | main.comboObj['line2'].update(data.propCount); 170 | } else { 171 | callback(data, giftMap, giftLit); 172 | } 173 | } 174 | } else { 175 | console.log('没有匹配到礼物!') 176 | } 177 | } 178 | } 179 | 180 | let onSendGift = function () {}; 181 | 182 | export default { 183 | // 初始化 184 | init(socket, giftMapObj) { 185 | giftMap = giftMapObj; 186 | if (componentGift.matches('.camera-pc ~ [data-component="gift"]')) { 187 | // HACK: camera pc mode 188 | const videoHeight = document.querySelector('[data-component="player"]').dataset.videoHeight; 189 | componentGift.style['top'] = `${ videoHeight }px`; 190 | componentGift.style['bottom'] = `0`; 191 | componentGift.style['transform'] = componentGift.style['-webkit-transform'] = `translateY(-100%)`; 192 | // HACK: image padding 193 | componentGift.style['margin-top'] = `-10px`; 194 | } 195 | 196 | let giftLit = main.initQueue(); 197 | 198 | socket.ioSocket.on(socket.socketName, (data) => { 199 | main.giftHandle(data.data, giftLit, function() { 200 | let {uid, nick, propId} = data.data; 201 | onSendGift({ 202 | uid: uid, 203 | nickname: nick, 204 | name: giftMap[propId].name, 205 | }); 206 | main.addQueue(data, giftLit, true); 207 | }) 208 | }); 209 | 210 | // 礼物面板送礼优先显示 211 | // this.unShiftQueue = (data) => { 212 | // main.giftHandle(data, giftLit, function() { 213 | // main.addQueue(data, giftLit, false); 214 | // }) 215 | // }; 216 | }, 217 | destroy () { 218 | componentGift.style['display'] = 'none'; 219 | }, 220 | onSendGift ( callback ) { 221 | onSendGift = callback; 222 | }, 223 | } 224 | -------------------------------------------------------------------------------- /src/js/component/gift/gift-lit/module/combo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component 礼物连击 3 | * @author wilson 4 | */ 5 | 6 | import config from '../config.js'; // 连击参数配置 7 | 8 | let levelMap = config.levelMap; // 礼物连击图片映射数据 9 | 10 | 11 | let combo = function(option) { 12 | this.curNum = 0; 13 | this.endNum = option.endNum; 14 | this.aniDom = option.aniDom; 15 | this.comboImgDom = option.comboImgDom; 16 | this.numDom = option.numDom; 17 | this.cb = option.cb; 18 | 19 | this.hasCritModel = false; // 是否启动了暴击模式 20 | this.start(); 21 | } 22 | 23 | combo.prototype = { 24 | start() { 25 | this.countUp(); 26 | }, 27 | 28 | // 查询连击图片更换 29 | changeComboImg(curNum) { 30 | let levelMapLen = levelMap.length; 31 | for(let i = 0;i < levelMapLen;i++) { 32 | if(curNum == levelMap[i].levelNum) { 33 | return i; 34 | } else if(curNum > levelMap[i].levelNum) { 35 | if((i + 1) < levelMapLen && curNum < levelMap[i + 1].levelNum) { 36 | return i; 37 | } else if((i + 1) == levelMapLen) { 38 | return i; 39 | } 40 | } 41 | } 42 | 43 | return (levelMapLen - 1); 44 | }, 45 | 46 | // 计数 47 | countUp() { 48 | if(this.curNum < this.endNum) { // 累加 49 | 50 | this.remainNum = this.endNum - this.curNum; 51 | if(this.remainNum > 10) { 52 | this.curNum += 10; 53 | this.hasCritModel = true; 54 | } else { 55 | if(this.hasCritModel) { 56 | this.curNum += this.remainNum; 57 | } else { 58 | this.curNum++; 59 | } 60 | } 61 | this.numDom.textContent = this.curNum; 62 | setTimeout(() => { 63 | this.aniDom.classList.add('is-ani'); 64 | if(this.aniDom.classList.contains('is-ani')) { 65 | // 判断是否需要更换连击图片 66 | let level = this.changeComboImg(this.curNum); 67 | this.comboImgDom.style.backgroundImage = `url(${levelMap[level].levelImg})`; 68 | } 69 | }, 0); 70 | 71 | setTimeout(() => { 72 | this.aniDom.classList.remove('is-ani'); 73 | this.comboImgDom.style.backgroundImage = ''; 74 | setTimeout(() => { 75 | this.countUp(); 76 | }, 100) 77 | }, 250) 78 | } else { // 连击结束 79 | let checkTimer = setInterval(() => { 80 | if(this.curNum < this.endNum) { 81 | this.countUp(); 82 | clearTimeout(overTimer); 83 | clearInterval(checkTimer); 84 | } 85 | }, 200); 86 | 87 | let overTimer = setTimeout(() => { 88 | this.cb(this.aniDom); 89 | clearInterval(checkTimer); 90 | }, config.duration); 91 | } 92 | }, 93 | 94 | // 更新 95 | update(offsetNum) { 96 | this.endNum += offsetNum; 97 | } 98 | } 99 | 100 | export default combo; 101 | -------------------------------------------------------------------------------- /src/js/component/gift/gift-lit/style/index.scss: -------------------------------------------------------------------------------- 1 | .live-gift { 2 | position: absolute; 3 | left: 0.2rem; 4 | bottom: 4.3rem; 5 | height: 1.64rem; 6 | pointer-events: none; 7 | } 8 | 9 | .live-gift__item { 10 | position: relative; 11 | width: 3.84rem; 12 | height: 0.64rem; 13 | transform: translateX(-4.5rem); 14 | background-color: rgba(46, 46, 46, 0.6); 15 | border-radius: 0.32rem; 16 | transition: opacity 0.3s ease-out 0.2s; 17 | &.ext-line1 { 18 | position: absolute; 19 | left: 0; 20 | bottom: 0; 21 | } 22 | &.ext-line2 { 23 | position: absolute; 24 | left: 0; 25 | top: 0; 26 | } 27 | &.gift-ani { 28 | animation-name: giftAni; 29 | animation-duration: 0.3s; 30 | animation-timing-function: ease-out; 31 | animation-fill-mode: forwards; 32 | } 33 | 34 | &.is-hide { 35 | opacity: 0; 36 | } 37 | } 38 | 39 | @keyframes giftAni{ 40 | 0% { 41 | transform: translateX(-4.5rem); 42 | } 43 | 100% { 44 | transform: translateX(0); 45 | } 46 | } 47 | 48 | .live-gift__userImg { 49 | position: absolute; 50 | left: 0.02rem; 51 | top: 0.02rem; 52 | width: 0.6rem; 53 | height: 0.6rem; 54 | border-radius: 50%; 55 | } 56 | 57 | .live-gift__userName { 58 | width: 2.44rem; 59 | font-size: 0.24rem; 60 | line-height: 0.3rem; 61 | color: #e0b66c; 62 | font-weight: 500; 63 | padding: 0.05rem 0 0 0.76rem; 64 | white-space: nowrap; 65 | word-wrap: normal; 66 | overflow: hidden; 67 | text-overflow: ellipsis; 68 | text-align: left; 69 | } 70 | 71 | .live-gift__giftName { 72 | width: 2.44rem; 73 | font-size: 0.2rem; 74 | color: #fff; 75 | line-height: 0.26rem; 76 | padding-left: 0.76rem; 77 | white-space: nowrap; 78 | word-wrap: normal; 79 | overflow: hidden; 80 | text-overflow: ellipsis; 81 | text-align: left; 82 | font-weight: 400; 83 | span { 84 | margin-left: 0.08rem; 85 | color: #ffdda0; 86 | font-weight: 500; 87 | } 88 | } 89 | 90 | .live-gift__giftImg { 91 | position: absolute; 92 | left: 2.5rem; 93 | top: -0.15rem; 94 | width: 0.9rem; 95 | height: 0.9rem; 96 | } 97 | 98 | .live-gift__giftNum { 99 | position: absolute; 100 | left: 3.5rem; 101 | top: 0.09rem; 102 | width: 2rem; 103 | height: 0.44rem; 104 | line-height: 0.44rem; 105 | height: .44rem; 106 | font-size: 0.42rem; 107 | color: #ffc40f; 108 | font-weight: bold; 109 | -webkit-text-stroke: 0.02rem #fff; 110 | span { 111 | &:nth-of-type(1) { 112 | position: absolute; 113 | top: 0; 114 | left: 0; 115 | } 116 | &:nth-of-type(2) { 117 | position: absolute; 118 | top: -.65rem; 119 | left: -.4rem; 120 | width: 1.9rem; 121 | height: 1.86rem; 122 | text-align: center; 123 | line-height: 2.34rem; 124 | // background-image: url(http://page.yy.com/channel/assets/images/combo-effect/level1.png); 125 | background-size: 11.4rem 1.86rem; 126 | } 127 | &:nth-of-type(3) { 128 | position: absolute; 129 | top: -.9rem; 130 | left: .3rem; 131 | // width: 2.4rem; 132 | height: 2.34rem; 133 | // text-align: center; 134 | line-height: 2.34rem; 135 | } 136 | } 137 | &.is-ani { 138 | span { 139 | &:nth-of-type(1) { 140 | animation: txtani 0.25s ease-out 0s; 141 | animation-fill-mode: forwards; 142 | } 143 | &:nth-of-type(2) { 144 | animation: giftani 0.25s steps(6,end) 0s; 145 | } 146 | 147 | &:nth-of-type(3) { 148 | animation: txtani 0.25s ease-out 0s; 149 | animation-fill-mode: forwards; 150 | } 151 | } 152 | } 153 | } 154 | 155 | @keyframes giftani { 156 | from{ 157 | background-position: 0 0; 158 | } 159 | to { 160 | background-position: -11.4rem 0; 161 | } 162 | } 163 | 164 | @keyframes txtani { 165 | from{ 166 | opacity: 0; 167 | transform: scale(2.4); 168 | } 169 | to { 170 | opacity: 1; 171 | transform: scale(1); 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /src/js/component/gift/gift-lit/tpl/index.js: -------------------------------------------------------------------------------- 1 | let renderTpl = (data)=>{ 2 | let tpl = ` 3 | 4 |

${data.nick}

5 |

送了${data.giftName}

6 |
7 |

x

8 | `; 9 | return tpl; 10 | } 11 | 12 | export default renderTpl; 13 | -------------------------------------------------------------------------------- /src/js/component/header/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component 头部主播名片 3 | * @author David 4 | */ 5 | 6 | import Event from '../../lib/event/index.js'; 7 | import style from './style/index.scss'; 8 | import renderTpl from './tpl/index.js'; 9 | 10 | const componentHeader = document.querySelector('[data-component="header"]'); 11 | 12 | const headerEvent = new Event; 13 | 14 | export default { 15 | init ( response ) { 16 | let tpl = renderTpl(response.data); 17 | componentHeader.innerHTML = `${ tpl }`; 18 | document.addEventListener('click', window.util.delegate('[data-role="follow"]', function () { 19 | headerEvent.trigger('follow', ( handler ) => { 20 | handler.apply(this); 21 | }); 22 | }, 1), false); 23 | this.on = headerEvent.on.bind(headerEvent); 24 | }, 25 | destroy () { 26 | componentHeader.innerHTML = ''; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /src/js/component/header/style/index.scss: -------------------------------------------------------------------------------- 1 | @mixin ellipsis($width: auto) { 2 | @if($width) { 3 | max-width: $width; 4 | } 5 | white-space: nowrap; 6 | word-wrap: normal; 7 | overflow: hidden; 8 | -o-text-overflow: ellipsis; 9 | text-overflow: ellipsis; 10 | text-align: left; 11 | } 12 | 13 | .live-header { 14 | position: absolute; 15 | // top: .4rem + 1.1rem; 16 | top: .15rem; 17 | left: 0; 18 | width: 100%; 19 | padding: 0 .2rem; 20 | } 21 | .live-hostInfo { 22 | float: left; 23 | position: relative; 24 | padding: .09rem .1rem; 25 | // padding-right: .06rem + .72rem + .14rem; 26 | left: .08rem; 27 | // height: 0.62rem; 28 | // padding-right: .92rem; 29 | // padding-left: .1rem; 30 | border-radius: .1rem; 31 | background: rgba(0, 0, 0, 0.2); 32 | color: #fff; 33 | font-size: 0; 34 | } 35 | .live-hostInfo__avatar { 36 | display: inline-block; 37 | vertical-align: top; 38 | width: .56rem; 39 | height: .56rem; 40 | border-radius: 50%; 41 | background-size: 100% 100%; 42 | background-repeat: no-repeat; 43 | position: relative; 44 | // top: 50%; 45 | // transform: translateY(-50%); 46 | } 47 | .live-hostInfo__cnt { 48 | margin-left: .1rem; 49 | display: inline-block; 50 | vertical-align: top; 51 | // font-size: .20rem; 52 | font-size: .24rem; 53 | line-height: .28rem; 54 | position: relative; 55 | // top: 50%; 56 | // transform: translateY(-50%); 57 | // margin-top: .04rem; 58 | } 59 | .live-hostInfo__nick { 60 | @include ellipsis(1.5rem); 61 | font-weight: 700; 62 | } 63 | .live-hostInfo__online { 64 | @include ellipsis(1.5rem); 65 | } 66 | .live-hostInfo__follow { 67 | // position: absolute; 68 | // top: 50%; 69 | // right: .06rem; 70 | // transform: translate(0, -50%); 71 | display: inline-block; 72 | margin-left: .08rem; 73 | width: .72rem; 74 | height: .56rem; 75 | line-height: .56rem; 76 | text-align: center; 77 | border-radius: .1rem; 78 | font-size: .24rem; 79 | background-color: rgba(256, 256, 256, 1); 80 | color: #2e2e2e; 81 | font-weight: 500; 82 | } 83 | .live-uid { 84 | position: absolute; 85 | // top: .05rem; 86 | top: .03rem; 87 | right: .4rem; 88 | right: .3rem; 89 | font-size: .24rem; 90 | color: #fff; 91 | // text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2); 92 | text-shadow: 0 0 1px rgba(0, 0, 0, 1); 93 | } 94 | -------------------------------------------------------------------------------- /src/js/component/header/tpl/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file header主播信息 3 | */ 4 | 5 | let renderTpl = (data)=>{ 6 | let tpl = ` 7 |
8 |
9 |
10 |

${data.anchorNick}

11 |

${data.users}人

12 |
13 |
关注
14 |
15 |
主播ID: ${data.anchorId}
16 | `; 17 | return tpl; 18 | } 19 | 20 | export default renderTpl; 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/js/component/layout/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component UI布局 3 | */ 4 | 5 | import renderTpl from './tpl/index.js'; 6 | import style from './style/index.scss'; 7 | 8 | let wrapSelector = window.wrapSelector?window.wrapSelector:'body'; 9 | document.querySelector(wrapSelector).innerHTML = `${renderTpl()}`; 10 | -------------------------------------------------------------------------------- /src/js/component/layout/style/index.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | min-width: 320px; 3 | max-width: 750px; 4 | } 5 | 6 | .live-content { 7 | width: 100%; 8 | position: relative; 9 | // padding-top: 1.1rem; 10 | background: #2e2e2e; 11 | overflow: hidden; 12 | user-select: none; 13 | } 14 | .live-player__uid, 15 | .live-player__nick, 16 | .live-player__avatar, 17 | .live-player__status, 18 | .live-player__playBtn { 19 | z-index: 2;// 覆盖遮罩或者边框等UI元素 20 | } 21 | .live-chatBar__switch { 22 | span { 23 | z-index: 2;// 覆盖遮罩或者边框等UI元素 24 | } 25 | } 26 | .live-giftPanel__cover, 27 | .live-giftPanel__border { 28 | z-index: 2;// 覆盖遮罩或者边框等UI元素 29 | } 30 | .live-giftPanel__present { 31 | z-index: 6;// 涉及动画的层级 32 | } 33 | .live-giftPanel__comboWrap { 34 | z-index: 5;// 涉及动画的层级 35 | } 36 | .live-giftPanel__comboButton { 37 | z-index: 7;// 涉及动画的层级 38 | } 39 | .live-topBar { 40 | z-index: 9;// 保持层级置顶 41 | } 42 | .live-downloadTips { 43 | z-index: 9;// 保持层级置顶 44 | } 45 | -------------------------------------------------------------------------------- /src/js/component/layout/tpl/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file UI布局 3 | */ 4 | 5 | let renderTpl = (data)=>{ 6 | let tpl = ` 7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | `; 20 | return tpl; 21 | } 22 | 23 | export default renderTpl; 24 | -------------------------------------------------------------------------------- /src/js/component/like/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 点赞配置 3 | */ 4 | 5 | let config = { 6 | // 点赞图片基础路径 7 | likeImageBaseUrl: '', 8 | 9 | // 点赞区域宽度 10 | wrapperWidth: 60, 11 | 12 | // 点赞区域高度 13 | wrapperHeight: 280, 14 | 15 | // 点赞显示的图片 16 | likeImg: { 17 | user01 : '/assets/img/like/heart/user/like01.png', 18 | user02 : '/assets/img/like/heart/user/like02.png', 19 | user03 : '/assets/img/like/heart/user/like03.png', 20 | audience01 : '/assets/img/like/heart/audience/like01.png', 21 | audience02 : '/assets/img/like/heart/audience/like02.png', 22 | audience03 : '/assets/img/like/heart/audience/like03.png', 23 | audience04 : '/assets/img/like/heart/audience/like04.png', 24 | audience05 : '/assets/img/like/heart/audience/like05.png', 25 | audience06 : '/assets/img/like/heart/audience/like06.png', 26 | audience07 : '/assets/img/like/heart/audience/like07.png', 27 | }, 28 | 29 | // 自动显示点赞的图片数量 30 | likeImgUserNum: 3, 31 | 32 | // 用户点击显示点赞的图片数量 33 | likeImgAudienceNum: 7, 34 | } 35 | 36 | export default config; 37 | -------------------------------------------------------------------------------- /src/js/component/like/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component 点赞 3 | * @author lixinliang 4 | */ 5 | 6 | import style from './style/index.scss'; 7 | import anim from './module/anim.js'; //点赞动画 8 | import frequent from './module/frequent.js'; 9 | 10 | const componentLike = document.querySelector('[data-component="like"]'); 11 | 12 | export default { 13 | init () { 14 | componentLike.innerHTML = ``; 15 | componentLike.appendChild(anim.canvas); 16 | // 根据在线人数模拟点赞量 17 | let online = document.querySelector('.live-hostInfo__online'); 18 | this.autoLike = true; 19 | let autoLike = () => { 20 | let viewer = (parseInt(online ? online.innerText : 0) + '').length; 21 | let delay = 50 + 2520 / (viewer > 6 ? 6 : viewer); 22 | frequent(() => { 23 | if (this.autoLike) { 24 | anim.show('audience'); 25 | autoLike(); 26 | } 27 | }, delay) 28 | }; 29 | autoLike(); 30 | this.delegate = delegate('.live-content', function ( event ) { 31 | anim.show('user'); 32 | }); 33 | document.addEventListener('click', this.delegate, false); 34 | }, 35 | destroy () { 36 | this.autoLike = false; 37 | document.removeEventListener('click', this.delegate, false); 38 | componentLike.innerHTML = ''; 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /src/js/component/like/module/anim.js: -------------------------------------------------------------------------------- 1 | import Anim from './anim/anim.js'; // 创建一个canvas 2 | import Like from './anim/anim.like.js'; // 点赞效果的逻辑代码 3 | import config from '../config.js'; 4 | 5 | // 把点赞引擎添加 animjs 上 6 | Anim.setup('like', Like); 7 | // 创建点赞实例 8 | let anim = new Anim('like'); 9 | // 设置尺寸 10 | anim.width(60).height(280); 11 | // 设置资源集合 与 映射分组 12 | anim.mainfest(mainfest()).group(mainfest.group); 13 | 14 | export default anim; 15 | 16 | function mainfest () { 17 | let result = {}; 18 | let user = append.call(result, 'user', config.likeImgUserNum); 19 | let audience = append.call(result, 'audience', config.likeImgAudienceNum); 20 | mainfest.group = { user, audience }; 21 | return result; 22 | } 23 | 24 | function append ( directory, length ) { 25 | return Array.apply(null, { length }).map((value, index) => fixed(index + 1 + '')).map((id) => { 26 | let key = directory + id; 27 | this[key] = config.likeImg[key]; 28 | return key; 29 | }); 30 | } 31 | 32 | function fixed ( value ) { 33 | return value.length == 1 ? '0' + value : value; 34 | } 35 | -------------------------------------------------------------------------------- /src/js/component/like/module/anim/anim.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component animjs 包含以下功能:点赞图片预加载、点赞图片分组、定义点赞的canvas样式 3 | * @author lixinliang 4 | */ 5 | 6 | import Ticker from './ticker.js'; 7 | 8 | const DPR = window.devicePixelRatio || 1; 9 | 10 | let closure = {}; 11 | let storage = []; 12 | 13 | // animjs 不涉及动画的实现 调用show方法时 把图片集合交给 动画引擎处理 14 | class Anim { 15 | 16 | /** 17 | * new的时候决定该实例使用那种动画引擎 18 | * @type {String} name 动画引擎 19 | */ 20 | constructor ( name = '' ) { 21 | let canvas = document.createElement('canvas'); 22 | let context = canvas.getContext('2d'); 23 | this.canvas = canvas; 24 | this.context = context; 25 | this.id = storage.length; 26 | this.name = name; 27 | let imageSet = {}; 28 | let imageMap = {}; 29 | storage.push({ imageSet, imageMap }); 30 | let ticker = new Ticker(); 31 | ticker.on(() => { 32 | context.clearRect(0, 0, canvas.width, canvas.height); 33 | }); 34 | this.ticker = ticker; 35 | this.dpr = DPR; 36 | if (DPR > 1) { 37 | this.canvas.style['transform'] = this.canvas.style['-webkit-transform'] = `scale(${ 1/DPR })`; 38 | } 39 | } 40 | 41 | /** 42 | * 传入一组图片集合 进行预加载 43 | * @param {Object} imageSet 资源集合 { 图片id : 图片url } 44 | * @return {Anim} this anim 45 | */ 46 | mainfest ( imageSet ) { 47 | let mySet = storage[this.id].imageSet; 48 | for (let key in imageSet) { 49 | let value = imageSet[key]; 50 | let image = { 51 | url : value, 52 | status : false 53 | }; 54 | image.img = loadImage(image.url, () => { 55 | image.status = true; 56 | }); 57 | mySet[key] = image; 58 | } 59 | return this 60 | } 61 | 62 | /** 63 | * 传入图片分组的映射关系 64 | * @param {Object} imageMap 资源分组映射 { 分组id : [图片id,...] } 65 | * @return {Anim} this anim 66 | */ 67 | group ( imageMap ) { 68 | let myMap = storage[this.id].imageMap; 69 | for (let key in imageMap) { 70 | myMap[key] = imageMap[key].slice(); 71 | } 72 | return this 73 | } 74 | 75 | /** 76 | * 随机播放动画 77 | * @param {String} groupName 资源分组名 78 | * @return {Anim} this anim 79 | */ 80 | show ( groupName ) { 81 | let { imageSet, imageMap } = storage[this.id]; 82 | let images = []; 83 | imageMap[groupName].forEach((imageName) => { 84 | let image = imageSet[imageName]; 85 | if (image.status) { 86 | images.push(image.img); 87 | } 88 | }); 89 | if (this.name) { 90 | return new closure[this.name](this, images); 91 | } 92 | return this 93 | } 94 | 95 | /** 96 | * 设置宽度 97 | * @param {Number} canvasWidth 画布宽度 98 | * @return {Anim} this anim 99 | */ 100 | width ( canvasWidth ) { 101 | let result = correct(canvasWidth); 102 | if (result) { 103 | let { num, str } = result; 104 | this.canvas.width = num; 105 | this.canvas.style.width = str; 106 | } 107 | return this 108 | } 109 | 110 | /** 111 | * 设置高度 112 | * @param {Number} canvasHeight 画布高度 113 | * @return {Anim} this anim 114 | */ 115 | height ( canvasHeight ) { 116 | 117 | let result = correct(canvasHeight); 118 | if (result) { 119 | let { num, str } = result; 120 | this.canvas.height = num; 121 | this.canvas.style.height = str; 122 | } 123 | 124 | return this 125 | } 126 | } 127 | 128 | export default Anim; 129 | 130 | /** 131 | * 加载图片 132 | * @param {String} url 图片地址 133 | * @param {Function} cb 加载回调 134 | * @return {HTMLImageElement} img 图片标签 135 | */ 136 | function loadImage ( url, cb ) { 137 | let img = new Image; 138 | img.onload = cb; 139 | img.src = url; 140 | return img; 141 | }; 142 | 143 | /** 144 | * 对canvas的宽高修正 在hidpi屏上 尺寸需要加倍 145 | * @param {Number} value 尺寸 146 | * @return {Number} value 尺寸 147 | */ 148 | function correct ( value ) { 149 | value *= DPR; 150 | let num; 151 | let str; 152 | if (typeof value == 'number') { 153 | num = value; 154 | str = value + 'px'; 155 | } else if (typeof value == 'string') { 156 | str = value; 157 | num = parseInt(value); 158 | } 159 | if (num && str) { 160 | return { num, str } 161 | } else { 162 | return null 163 | } 164 | }; 165 | 166 | /** 167 | * 定义动画引擎 168 | * @param {String} name 动画引擎名 169 | * @param {Class} plugin 动画引擎 170 | */ 171 | Anim.setup = function ( name, plugin ) { 172 | closure[name] = plugin; 173 | }; 174 | -------------------------------------------------------------------------------- /src/js/component/like/module/anim/anim.like.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component anim.like 基于 animjs 的点赞动画引擎 3 | * @author lixinliang 4 | */ 5 | 6 | import { 7 | getRandomByMedian, 8 | getRandomBetweenInt, 9 | getRandomWithBinomial, 10 | } from './random.js'; 11 | 12 | // 点赞动画引擎 13 | class Like { 14 | /** 15 | * 调用 new Like 创建一个赞的动画 16 | * @param {Anim} anim Anim实例 17 | * @param {Array} images 素材集合 18 | * @return {Like} like 点赞实例 19 | */ 20 | constructor ( anim, images ) { 21 | // 从图片集合 随机抽取一张 22 | let image = images[getRandomBetweenInt(0, images.length - 1)]; 23 | 24 | this.image = image; 25 | this.canvas = anim.canvas; 26 | 27 | let DPR = this.dpr = anim.dpr; 28 | 29 | let imageWidth = image.width; 30 | let imageHeight = image.height; 31 | 32 | // @return 当前动画进度 0 ~ 100 33 | let getImageTimeline = this.createImageTimeline(); 34 | // @return 旋转后的图片 canvas 35 | let getImageData = this.createImageData(); 36 | // @return 获取当前缩放曲线 number 37 | let getImageScaleSize = this.createImageScaleSize(); 38 | // @return 获取当前透明度曲线 number 39 | let getImageOpacityValue = this.createImageOpacityValue(); 40 | // @return 获取当前位移曲线 { x, y } 41 | let getImagePathPosition = this.createImagePathPosition(); 42 | 43 | let handler = () => { 44 | let percentage = getImageTimeline(); 45 | let { imageCanvas, imageContext } = getImageData(); 46 | let scaleSize = getImageScaleSize(percentage); 47 | let globalAlpha = getImageOpacityValue(percentage); 48 | let { x, y } = getImagePathPosition(percentage); 49 | 50 | // 最后渲染的时候 再把 dpr 计算进去 51 | if (scaleSize == 1 && globalAlpha == 1) { 52 | anim.context.drawImage(imageCanvas, x * DPR, y * DPR, imageWidth * DPR, imageHeight * DPR); 53 | } else { 54 | if (globalAlpha == 1) { 55 | let scaleWidth = imageWidth * scaleSize; 56 | let deltaWidth = (imageWidth - scaleWidth) / 2; 57 | let scaleHeight = imageHeight * scaleSize; 58 | let deltaHeight = (imageHeight - scaleHeight) / 2; 59 | anim.context.drawImage(imageCanvas, (x + deltaWidth) * DPR, (y + deltaHeight) * DPR, scaleWidth * DPR, scaleHeight * DPR); 60 | } else { 61 | anim.context.globalAlpha = globalAlpha; 62 | anim.context.drawImage(imageCanvas, x * DPR, y * DPR, imageWidth * DPR, imageHeight * DPR); 63 | anim.context.globalAlpha = 1; 64 | } 65 | } 66 | if (percentage == 100) { 67 | getImageTimeline = null; 68 | getImageData = null; 69 | getImageScaleSize = null; 70 | getImageOpacityValue = null; 71 | getImagePathPosition = null; 72 | anim.ticker.off(handler); 73 | } 74 | }; 75 | anim.ticker.on(handler); 76 | } 77 | 78 | /** 79 | * 创建每一个动画的独立时间线 80 | * @return {Function} getImageTimeline 获得该动画当前时间线的函数 81 | */ 82 | createImageTimeline () { 83 | // 动画总时基数 3000ms 浮动系数 20% 84 | let originDuration = 3000; 85 | let range = 0.2; 86 | 87 | let duration = getRandomByMedian(originDuration, range); 88 | let times = Math.round(duration / 16.7); 89 | let now = 0; 90 | return () => { 91 | now++; 92 | let percentage = parseInt(now * 1000 / times) / 10; 93 | return percentage > 100 ? 100 : percentage 94 | } 95 | } 96 | 97 | /** 98 | * 创建每一个动画的独立形态 99 | * @return {Function} getImageData 获得该动画形态的对象的函数 100 | */ 101 | createImageData () { 102 | // 图像旋转幅度 -30 ~ 30deg 103 | let minRotate = -30; 104 | let maxRotate = 30; 105 | // 缩放系数的二项分布 70 ~ 120 出现95的概率最高 106 | let minScale = 70; 107 | let maxScale = 120; 108 | // 缩放最大值为105 超出105的概率以105为对称轴 添加到 90 ~ 105 的区间 109 | let maxLimitScale = 105; 110 | 111 | let image = this.image; 112 | let { width, height } = image; 113 | let rotate = getRandomBetweenInt(minRotate, maxRotate); 114 | let scale = getRandomWithBinomial(minScale, maxScale); 115 | if (scale > maxLimitScale) { 116 | scale -= (maxScale - maxLimitScale); 117 | } 118 | scale = scale / 100; 119 | 120 | let imageCanvas = document.createElement('canvas'); 121 | let imageContext = imageCanvas.getContext('2d'); 122 | 123 | // 使用离屏canvas保存旋转后的图片并多次复用 为了避免旋转后图片被切割 画布的尺寸是图片的2倍 124 | imageCanvas.width = width * 2; 125 | imageCanvas.height = height * 2; 126 | 127 | imageContext.translate(width, height); 128 | imageContext.rotate(rotate * Math.PI / 180); 129 | imageContext.scale(scale, scale); 130 | imageContext.translate(-width, -height); 131 | 132 | imageContext.drawImage(image, width / 2, height / 2, width, height); 133 | 134 | return () => ({ imageCanvas, imageContext }) 135 | } 136 | 137 | /** 138 | * 创建每一个动画的独立放大曲线 139 | * @return {Function} getImageScaleSize 获得该动画当前尺寸的函数 140 | */ 141 | createImageScaleSize () { 142 | // 完成缩放动画的百分比阶段 9 ~ 11 143 | let minAppearStage = 9; 144 | let maxAppearStage = 11; 145 | 146 | let appearStage = getRandomBetweenInt(minAppearStage, maxAppearStage); 147 | return ( percentage ) => { 148 | return percentage >= appearStage ? 1 : ((parseInt(100 * percentage / appearStage) / 100) || 0.1) 149 | } 150 | } 151 | 152 | /** 153 | * 创建每一个动画的独立透明曲线 154 | * @return {Function} getImageOpacityValue 获得该动画当前透明度的函数 155 | */ 156 | 157 | createImageOpacityValue () { 158 | // 开始淡出动画的百分比阶段 82 ~ 86 159 | let minDisappearStage = 82; 160 | let maxDisappearStage = 86; 161 | 162 | let disappearStage = getRandomBetweenInt(minDisappearStage, maxDisappearStage); 163 | let area = 100 - disappearStage; 164 | return ( percentage ) => { 165 | if (percentage < disappearStage) { 166 | return 1; 167 | } else { 168 | let opacity = ((percentage - disappearStage) / area).toFixed(2) - 0; 169 | return 1 - opacity; 170 | } 171 | } 172 | } 173 | 174 | /** 175 | * 创建每一个动画的独立位移曲线 176 | * @return {Function} getImagePathPosition 获得该动画当前位移值的函数 177 | */ 178 | createImagePathPosition () { 179 | // 以下计算均以 dpr 为 1作为参考值 180 | // 动画初始位置的横向定位的浮动系数 10% 181 | let startX = 10; 182 | // 动画纵向位移距离delta y是总距离的 95% 浮动系数 5% 183 | let deltaY = 0.95; 184 | let deltaYRange = 1 - deltaY; 185 | // 动画横向位移距离符合正弦曲线 正弦波峰最小值10% 最大值15% 186 | let minCrest = 10; 187 | let maxCrest = 15; 188 | // 正弦波长系数 最小值16 最大值32 189 | let minWavelength = 16; 190 | let maxWavelength = 32; 191 | 192 | let DPR = this.dpr; 193 | // @2x 图片要先砍半 194 | let imageWidth = this.image.width / 2; 195 | let imageHeight = this.image.height / 2; 196 | // 画布尺寸根据dpr计算 197 | let canvasWidth = this.canvas.width / DPR; 198 | let canvasHeight = this.canvas.height / DPR; 199 | // console.log(imageWidth, imageHeight, canvasWidth, canvasHeight); 200 | let originX = canvasWidth * (0.5 + getRandomBetweenInt(-startX, startX) / 100); 201 | let originY = canvasHeight; 202 | // 初始值居中是画布总长度减去图片长度后的一半 203 | // 再减去旋转的偏移量 也是图片尺寸的一半 204 | // 刚好减去一倍的图片尺寸 205 | originX -= imageWidth; 206 | originY -= imageHeight; 207 | 208 | let translateX = canvasWidth * getRandomBetweenInt(minCrest, maxCrest) / 100; 209 | let translateY = getRandomByMedian(originY * deltaY, deltaYRange); 210 | 211 | let wavelength = getRandomBetweenInt(minWavelength, maxWavelength); 212 | 213 | // 方向是 -1/1 214 | let direction = getRandomBetweenInt(0, 1) * 2 - 1; 215 | 216 | return ( percentage ) => { 217 | let x = originX + translateX * Math.sin(percentage / wavelength) * direction; 218 | let y = originY - translateY * percentage / 100; 219 | return { x, y } 220 | } 221 | } 222 | } 223 | 224 | export default Like; 225 | -------------------------------------------------------------------------------- /src/js/component/like/module/anim/random.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 在区间 [min, max] 上的等概率随机整数 4 | * @param {Number} min 最小值 5 | * @param {Number} max 最大值 6 | * @return {Number} value 随机数 7 | */ 8 | let getRandomBetweenInt = function ( min, max ) { 9 | min = parseInt(min) || 0; 10 | max = parseInt(max) || 0; 11 | if (min > max) { 12 | [min, max] = [max, min]; 13 | } 14 | let value = min + Math.round(Math.random() * (max + 1 - min)); 15 | return value == max + 1 ? min : value; 16 | }; 17 | 18 | /** 19 | * 以 value 为中数上下浮动 percentage 的区间上的等概率随机整数 20 | * @param {Number} value 中数 21 | * @param {Number} percentage 浮动系数 22 | * @return {Number} value 随机数 23 | */ 24 | let getRandomByMedian = function ( value, percentage ) { 25 | value = parseInt(value) || 0; 26 | if (percentage < 0) { 27 | percentage = 0; 28 | } 29 | if (percentage > 1) { 30 | percentage = 1; 31 | } 32 | return getRandomBetweenInt(value * (1 - percentage), value * (1 + percentage)); 33 | }; 34 | 35 | /** 36 | * 在区间 [min, max] 上的遵循二项分布的随机整数 其中中数概率最高 37 | * @param {Number} min 最小值 38 | * @param {Number} max 最大值 39 | * @return {Number} value 随机数 40 | */ 41 | let getRandomWithBinomial = function ( min, max ) { 42 | min = parseInt(min) || 0; 43 | max = parseInt(max) || 0; 44 | if (min > max) { 45 | [min, max] = [max, min]; 46 | } 47 | let median = parseInt((max - min) / 2); 48 | return min + getRandomBetweenInt(0, median) + getRandomBetweenInt(0, median); 49 | }; 50 | 51 | export { 52 | getRandomByMedian, 53 | getRandomBetweenInt, 54 | getRandomWithBinomial, 55 | }; 56 | -------------------------------------------------------------------------------- /src/js/component/like/module/anim/ticker.js: -------------------------------------------------------------------------------- 1 | const TICK = 'tick'; 2 | const TICKER = new Event(TICK); 3 | 4 | let elements = []; 5 | 6 | let requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || ((fn) => setTimeout(fn, 16)); 7 | 8 | let looper = () => { 9 | elements.forEach((elem) => elem.dispatchEvent(TICKER)); 10 | requestAnimationFrame(looper); 11 | }; 12 | 13 | requestAnimationFrame(looper); 14 | 15 | class Ticker { 16 | constructor () { 17 | this.elem = document.createElement('div'); 18 | elements.push(this.elem); 19 | } 20 | on ( fn ) { 21 | this.elem.addEventListener(TICK, fn, false); 22 | return this 23 | } 24 | off ( fn ) { 25 | this.elem.removeEventListener(TICK, fn, false); 26 | return this 27 | } 28 | } 29 | 30 | export default Ticker; 31 | -------------------------------------------------------------------------------- /src/js/component/like/module/frequent.js: -------------------------------------------------------------------------------- 1 | let requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || ((fn) => setTimeout(fn, 16)); 2 | 3 | let frequent = (fn, delay) => { 4 | let counter = parseInt(delay / 16.7); 5 | requestAnimationFrame(function looper () { 6 | if (!--counter) { 7 | fn(); 8 | } else { 9 | requestAnimationFrame(looper); 10 | } 11 | }); 12 | } 13 | export default frequent; 14 | -------------------------------------------------------------------------------- /src/js/component/like/style/index.scss: -------------------------------------------------------------------------------- 1 | .live-like { 2 | canvas { 3 | transform-origin : 100% 100%; 4 | pointer-events: none; 5 | position: absolute; 6 | bottom: 0.46rem; 7 | right : 0; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/js/component/message/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 配置文件 3 | */ 4 | 5 | let config = { 6 | // 消息上限 7 | limit: 50, 8 | 9 | // 固定时消息新增上限 10 | limitWhenFixed: 30, 11 | 12 | // 消息更新间隔 13 | interval: 800, 14 | 15 | // 有消息自动滚到底部 16 | autoScrollToBottom: true, 17 | 18 | // 系统消息 19 | systemMessage: '欢迎来到ME星球,玩得开心。', 20 | } 21 | 22 | export default config; 23 | -------------------------------------------------------------------------------- /src/js/component/message/img/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/js/component/message/img/chat.png -------------------------------------------------------------------------------- /src/js/component/message/img/gift.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/js/component/message/img/gift.png -------------------------------------------------------------------------------- /src/js/component/message/img/live.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/js/component/message/img/live.png -------------------------------------------------------------------------------- /src/js/component/message/img/share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/js/component/message/img/share.png -------------------------------------------------------------------------------- /src/js/component/message/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component 消息流 3 | * @description 聊天公屏消息流 4 | * @author lixinliang 5 | */ 6 | 7 | import scrollTo from '../../lib/scrollTo/index.js'; 8 | import style from './style/index.scss'; 9 | import createTpl from './tpl/index.js'; 10 | import config from './config'; 11 | 12 | const componentMessage = document.querySelector('[data-component="message"]'); 13 | 14 | // 消息上限 15 | let {limit} = config; 16 | // 固定时消息新增上限 17 | let {limitWhenFixed} = config; 18 | let length = 0; 19 | // 消息更新间隔 20 | let {interval} = config; 21 | let scrollingToBottom = false; 22 | let {autoScrollToBottom} = config; 23 | let tipsShowing = false; 24 | let messages = document.createDocumentFragment(); 25 | 26 | export default { 27 | init (socket) { 28 | componentMessage.innerHTML = `${ createTpl() }`; 29 | this.messageList = componentMessage.querySelector('.live-message__list'); 30 | this.messageScreen = componentMessage.querySelector('.live-message__screen'); 31 | if (componentMessage.matches('.camera-pc ~ [data-component="message"]')) { 32 | // HACK: camera pc mode 33 | const videoHeight = document.querySelector('[data-component="player"]').dataset.videoHeight; 34 | this.messageScreen.style['max-height'] = `${ videoHeight }px`; 35 | this.messageScreen.style['-webkit-mask-size'] = `100% ${ videoHeight }px`; 36 | } 37 | this.sid = setInterval(() => { 38 | this.update(); 39 | }, interval); 40 | const selector = '.live-message__icon'; 41 | const active = 'is-active'; 42 | let fadeOutId = 0; 43 | let fadeOutFn = delegate(selector, function ( event ) { 44 | clearTimeout(fadeOutId); 45 | fadeOutId = setTimeout(() => { 46 | this.classList.remove(active); 47 | }, 100); 48 | }); 49 | document.addEventListener('touchstart', delegate(selector, function ( event ) { 50 | clearTimeout(fadeOutId); 51 | this.classList.add(active); 52 | }), false); 53 | document.addEventListener('touchend', fadeOutFn, false); 54 | document.addEventListener('touchcancel', fadeOutFn, false); 55 | this.messageScreen.addEventListener('scroll', () => { 56 | let { scrollTop, scrollHeight, offsetHeight } = this.messageScreen; 57 | if ((scrollTop + offsetHeight) >= (scrollHeight - 1)) { 58 | autoScrollToBottom = true; 59 | if (tipsShowing) { 60 | tipsShowing = false; 61 | componentMessage.classList.remove('has-tips'); 62 | } 63 | } else { 64 | autoScrollToBottom = false; 65 | } 66 | }, false); 67 | 68 | let button = componentMessage.querySelector('.live-message__tips'); 69 | button.addEventListener('click', () => { 70 | scrollingToBottom = true; 71 | scrollTo('bottom', { 72 | elem : this.messageScreen, 73 | // transition : false, 74 | }).then(() => { 75 | componentMessage.classList.remove('has-tips'); 76 | scrollingToBottom = false; 77 | autoScrollToBottom = true; 78 | }); 79 | }, false); 80 | 81 | this.addSystem(config.systemMessage); 82 | socket.ioSocket.on(socket.socketName, ( data ) => { 83 | let {nick, content} = data.data; 84 | this.addChat(nick, content); 85 | }); 86 | }, 87 | add ( text, insertBefore ) { 88 | let message = document.createElement('p'); 89 | message.classList.add('live-message__item'); 90 | message.innerHTML = text; 91 | if (insertBefore) { 92 | messages.insertBefore(message, messages.firstElementChild); 93 | } else { 94 | messages.appendChild(message); 95 | } 96 | if (insertBefore) { 97 | this.update(); 98 | } 99 | }, 100 | update () { 101 | if (scrollingToBottom) { 102 | return; 103 | } 104 | if (!messages.children.length) { 105 | return; 106 | } 107 | length++; 108 | if (!autoScrollToBottom) { 109 | if (length > (limit + limitWhenFixed)) { 110 | autoScrollToBottom = true; 111 | tipsShowing = false; 112 | componentMessage.classList.remove('has-tips'); 113 | for (let i = 0; i < limitWhenFixed; i++) { 114 | this.messageList.removeChild(this.messageList.firstElementChild); 115 | } 116 | length -= limitWhenFixed; 117 | } 118 | } 119 | if (autoScrollToBottom) { 120 | if (length > limit) { 121 | this.messageList.removeChild(this.messageList.firstElementChild); 122 | length--; 123 | } 124 | } 125 | this.messageList.appendChild(messages.firstElementChild); 126 | if (autoScrollToBottom) { 127 | scrollTo('bottom', { 128 | elem : this.messageScreen, 129 | transition : false, 130 | }); 131 | } else { 132 | if (!tipsShowing) { 133 | tipsShowing = true; 134 | componentMessage.classList.add('has-tips'); 135 | } 136 | } 137 | }, 138 | addSystem ( text ) { 139 | this.add(`系统通知 ${ text }`); 140 | }, 141 | addShare ( nickname ) { 142 | this.add(`${ nickname } 分享了这个直播`); 143 | }, 144 | addChat ( nickname, text, insertBefore ) { 145 | this.add(`${ nickname } ${ text }`, insertBefore); 146 | }, 147 | addPresent ( nickname, gift, insertBefore ) { 148 | this.add(`${ nickname } 送了 ${ gift }`, insertBefore); 149 | }, 150 | addFollow ( nickname, anchorName ) { 151 | this.add(`${ nickname } 关注了 ${ anchorName }`, true); 152 | }, 153 | destroy () { 154 | clearInterval(this.sid); 155 | componentMessage.innerHTML = ''; 156 | }, 157 | show () { 158 | componentMessage.classList.remove('fade-out'); 159 | }, 160 | hide () { 161 | componentMessage.classList.add('fade-out'); 162 | }, 163 | }; 164 | -------------------------------------------------------------------------------- /src/js/component/message/style/index.scss: -------------------------------------------------------------------------------- 1 | .live-message { 2 | position: absolute; 3 | // left: 0.2rem; 4 | left: 0; 5 | bottom: .2rem; 6 | // height: .56rem * 5 + .68; 7 | // padding-bottom: .68rem; 8 | // width: 8rem; 9 | width: 100%; 10 | // display: none; 11 | transition: opacity 200ms ease-in; 12 | &.fade-out { 13 | pointer-events: none; 14 | opacity: 0; 15 | } 16 | &.has-tips { 17 | .live-message__tips { 18 | display: inline-block; 19 | } 20 | } 21 | } 22 | .live-message__screen { 23 | max-height: .56rem * 5; 24 | margin-bottom: 0.2rem; 25 | overflow-y: scroll; 26 | -webkit-overflow-scrolling: touch; 27 | -webkit-mask-image: linear-gradient(to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 15%, rgba(0, 0, 0, 1)); 28 | -webkit-mask-repeat: no-repeat; 29 | -webkit-mask-size: 100% .56rem * 5; 30 | -webkit-mask-position: 100% 100%; 31 | // position: relative; 32 | &::-webkit-scrollbar { 33 | display: none; 34 | } 35 | } 36 | .live-message__list { 37 | // position: absolute; 38 | // left: 0; 39 | // bottom: 0; 40 | width: 6rem; 41 | position: relative; 42 | left: 0.2rem; 43 | .is-system { 44 | color: #76ae15; 45 | } 46 | .is-share { 47 | color: #ffdda0; 48 | } 49 | .is-nickname { 50 | color: #e0b66c; 51 | } 52 | .is-present { 53 | color: #e0b66c; 54 | } 55 | .is-follow { 56 | color: #e0b66c; 57 | } 58 | } 59 | .live-message__item { 60 | line-height: .56rem; 61 | font-size: 0.32rem; 62 | color: #fff; 63 | // text-shadow: 0 0 1px rgba(0, 0, 0, .6); 64 | // text-shadow: 0 0 1px rgba(0, 0, 0, 1); 65 | text-shadow: 1px 0 3px rgba(0, 0, 0, 1); 66 | &, 67 | * { 68 | font-weight: 500; 69 | } 70 | } 71 | .live-message__action { 72 | height: 0.68rem; 73 | width: 0.68rem * 4 + 0.2 * 3; 74 | margin-left: 0.2rem; 75 | // position: absolute; 76 | // left: 0; 77 | // bottom: 0; 78 | &::after { 79 | content: ""; 80 | clear: both; 81 | display: table; 82 | } 83 | } 84 | .live-message__icon { 85 | float: left; 86 | width: 0.68rem; 87 | height: 0.68rem; 88 | margin-left: 0.2rem; 89 | background-repeat: no-repeat; 90 | background-size: 100% 100%; 91 | // box-shadow: 0 0 3px 1px rgba(0, 0, 0, .4); 92 | border-radius: 50%; 93 | cursor: pointer; 94 | &:first-child { 95 | margin-left: 0; 96 | } 97 | &.is-chat { 98 | background-image: url(../img/chat.png); 99 | } 100 | &.is-share { 101 | background-image: url(../img/share.png); 102 | } 103 | &.is-live { 104 | background-image: url(../img/live.png); 105 | } 106 | &.is-gift { 107 | background-image: url(../img/gift.png); 108 | } 109 | &.is-active { 110 | filter: brightness(.7); 111 | } 112 | } 113 | .live-message__tips { 114 | display: none; 115 | position: absolute; 116 | background-color: #ffd893; 117 | padding-left: 0.2rem; 118 | padding-right: 0.2rem + 0.25; 119 | height: 0.64rem; 120 | line-height: 0.64rem; 121 | font-size: 0.26rem; 122 | color: #261300; 123 | left: 0.2rem; 124 | bottom: 0rem + 0.68 + 0.2 + 0.1; 125 | border-radius: 0.12rem; 126 | cursor: pointer; 127 | i { 128 | position: absolute; 129 | top: 50%; 130 | width: 0; 131 | height: 0; 132 | right: 0.35rem; 133 | margin-top: -0.07rem; 134 | &::before { 135 | content: ""; 136 | position: absolute; 137 | top: 0; 138 | left: 0; 139 | border-top: 4px solid #261300; 140 | border-left: 4px solid transparent; 141 | border-right: 4px solid transparent; 142 | } 143 | &::after { 144 | content: ""; 145 | position: absolute; 146 | left: 1px; 147 | top: 0px; 148 | border-top: 3px solid #ffd893; 149 | border-left: 3px solid transparent; 150 | border-right: 3px solid transparent; 151 | } 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/js/component/message/tpl/index.js: -------------------------------------------------------------------------------- 1 | let renderTpl = (data)=>{ 2 | let tpl = ` 3 |
4 |
5 |
6 |
7 | 8 | 9 | 10 | 11 |
12 |
有新消息
13 | `; 14 | return tpl; 15 | } 16 | 17 | export default renderTpl; 18 | -------------------------------------------------------------------------------- /src/js/component/player/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 配置文件 3 | */ 4 | 5 | let config = { 6 | checkLiveStatusSpeek: 1500, // 检查直播状态的速度频率(毫秒) 7 | } 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /src/js/component/player/img/icon-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/js/component/player/img/icon-play.png -------------------------------------------------------------------------------- /src/js/component/player/img/loading.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 11 | 12 | 13 | 17 | 21 | 22 | 23 | 27 | 31 | 32 | 33 | 37 | 41 | 42 | 43 | 47 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/js/component/player/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component 播放器 3 | * @description 根据不同状态渲染不同的UI界面 4 | * @author David 5 | */ 6 | 7 | import config from './config.js'; 8 | import livingStyle from './style/live-in.scss'; 9 | import liveEndStyle from './style/live-end.scss'; 10 | 11 | import livingTpl from './tpl/live-in.js'; // 直播中状态 12 | import liveEndTpl from './tpl/live-end.js'; // 直播结束状态 13 | 14 | let componentPlayer = document.querySelector('[data-component="player"]'); 15 | let video = null; 16 | 17 | export default { 18 | 19 | init({data}) { 20 | this.baseLiveData = data; 21 | return new Promise(( resolve, reject ) => { 22 | // 是否在开播 23 | if(data.living) { 24 | this.renderLiving(data).then(()=>{ 25 | resolve(); 26 | }); 27 | } 28 | else { 29 | this.renderLiveEnd(data); 30 | } 31 | 32 | }).catch((err) => { 33 | console.log(err); 34 | throw err; 35 | }); 36 | }, 37 | 38 | // 监听直播状态 39 | listenLiveStatus(callback) { 40 | RequqestApi.getLiveStatus().then(({data})=>{ 41 | if (data.status === 1 && !this.scopeLiveStatus) { 42 | this.renderLiving(this.baseLiveData, true); 43 | this.scopeLiveStatus = 1; 44 | } 45 | else if (data.status !== 1 && this.scopeLiveStatus) { 46 | this.renderLiveEnd(this.baseLiveData); 47 | this.scopeLiveStatus = 0; 48 | } 49 | callback && callback(data.status); 50 | }); 51 | setTimeout(()=>{ 52 | this.listenLiveStatus(callback); 53 | }, config.checkLiveStatusSpeek); 54 | }, 55 | 56 | // 初始化视频 57 | createVideo(data) { 58 | return new Promise(( resolve, reject ) => { 59 | componentPlayer.classList.add('is-waiting'); 60 | window.util.sleep(1000).then(()=>{ 61 | if (!video) { 62 | let newVideo = document.createElement('video'); 63 | document.querySelector('#player').appendChild(newVideo); 64 | setTimeout(resolve, 10); 65 | video = newVideo; 66 | video.src = data.liveUrl; 67 | video.setAttribute('playsinline', true); 68 | video.removeAttribute('poster'); 69 | } 70 | video.play(); 71 | this.scopeLiveStatus = 1; 72 | }) 73 | }); 74 | }, 75 | 76 | // 渲染直播中状态UI 77 | renderLiving(data, autoplay) { 78 | return new Promise(( resolve, reject ) => { 79 | let isAutoPlay = autoplay?autoplay:(Number(window.util.getUrlParam('autoplay')) || 0); 80 | componentPlayer.innerHTML = `${ livingTpl(data) }`; 81 | componentPlayer.classList.add('has-poster', 'has-button'); 82 | // 自动播放 83 | isAutoPlay && this.createVideo(data).then(resolve); 84 | 85 | document.addEventListener('click', window.util.delegate('[data-role="play-video"]', () => { 86 | this.createVideo(data).then(resolve); 87 | }), false); 88 | }) 89 | .then(this.bindEvent) 90 | .then(() => { 91 | this.setPlayerSize('mobile'); 92 | }); 93 | }, 94 | 95 | // 渲染开播结束状态UI 96 | renderLiveEnd(data) { 97 | const posterURL = data.cover; 98 | componentPlayer.style['background-image'] = `url(${ posterURL })`; 99 | componentPlayer.innerHTML = `${ liveEndTpl(data) }`; 100 | componentPlayer.classList.add('has-poster'); 101 | video = null; 102 | }, 103 | 104 | // 动态计算视频窗口 105 | setPlayerSize( type ) { 106 | const winHeight = window.innerHeight; 107 | const topBarHeight = document.querySelector('[data-component="topBar"]').offsetHeight; 108 | if (type == 'mobile') { 109 | const playerHeight = winHeight - topBarHeight + 1; 110 | const videoHeight = '120%'; 111 | const videoTop = '-10%'; 112 | video.style['height'] = videoHeight; 113 | video.style['top'] = videoTop; 114 | componentPlayer.classList.remove('has-poster', 'has-button', 'is-waiting'); 115 | componentPlayer.classList.add('has-live'); 116 | return new Promise(( resolve ) => { 117 | componentPlayer.addEventListener('transitionend', resolve, false); 118 | componentPlayer.style['height'] = `${ playerHeight }px`; 119 | }); 120 | } 121 | if (type == 'pc') { 122 | const winHeight = window.innerHeight; 123 | const topBarHeight = document.querySelector('[data-component="topBar"]').offsetHeight; 124 | // HACK: message bottom + message action height + message screen margin bottom 125 | const messageActionZone = Math.round((0.2 + 0.68 + 0.2) * parseFloat(getComputedStyle(document.documentElement)['font-size'])); 126 | const playerHeight = winHeight - topBarHeight + 1; 127 | const videoHeight = (playerHeight - messageActionZone) * 0.5; 128 | video.style['height'] = '50%'; 129 | video.style['width'] = `${ videoHeight * 16 / 9 }px`; 130 | document.querySelector('#player').style['padding-bottom'] = `${ messageActionZone }px`; 131 | componentPlayer.classList.remove('has-poster', 'has-button', 'is-waiting'); 132 | componentPlayer.classList.add('has-live', 'camera-pc'); 133 | componentPlayer.dataset.videoHeight = videoHeight; 134 | return new Promise(( resolve ) => { 135 | componentPlayer.addEventListener('transitionend', resolve, false); 136 | componentPlayer.style['height'] = `${ playerHeight }px`; 137 | }); 138 | } 139 | }, 140 | 141 | // 添加视频事件 142 | bindEvent() { 143 | // TODO: 暴露回调 其他组件可以获取直播状态 144 | video.addEventListener('pause', () => { 145 | componentPlayer.classList.add('has-button'); 146 | this.scopeLiveStatus = 0; 147 | }, false); 148 | video.addEventListener('play', () => { 149 | componentPlayer.classList.remove('has-button'); 150 | }, false) 151 | }, 152 | 153 | } 154 | 155 | -------------------------------------------------------------------------------- /src/js/component/player/style/live-end.scss: -------------------------------------------------------------------------------- 1 | .live-player { 2 | position: relative; 3 | height: 6.87rem; 4 | color: #fff; 5 | overflow: hidden; 6 | text-align: center; 7 | background-size: cover; 8 | background-position: center; 9 | background-repeat: no-repeat; 10 | pointer-events: none; 11 | &:after { 12 | content: ''; 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | width: 100%; 17 | height: 100%; 18 | background: rgba(0, 0, 0, .5); 19 | } 20 | } 21 | .live-player__status, 22 | .live-player__avatar, 23 | .live-player__nick, 24 | .live-player__uid { 25 | position: relative; 26 | } 27 | .live-player__status { 28 | // padding-top: .9rem; 29 | font-size: .4rem; 30 | margin-top: .9rem; 31 | } 32 | .live-player__avatar { 33 | // padding-top: .52rem; 34 | margin: .52rem auto 0; 35 | height: 1.48rem; 36 | width: 1.48rem; 37 | // border-radius: 1.48rem; 38 | border-radius: 50%; 39 | overflow: hidden; 40 | background-size: 100% 100%; 41 | background-repeat: no-repeat; 42 | // img { 43 | // width: 1.48rem; 44 | // height: 1.48rem; 45 | // border-radius: 1.48rem; 46 | // } 47 | } 48 | .live-player__nick { 49 | // padding-top: .05rem; 50 | margin-top: .05rem; 51 | font-size: .34rem; 52 | } 53 | .live-player__uid { 54 | // padding-top: .1rem; 55 | margin-top: .1rem; 56 | font-size: .24rem; 57 | } 58 | -------------------------------------------------------------------------------- /src/js/component/player/style/live-in.scss: -------------------------------------------------------------------------------- 1 | %center { 2 | position: absolute; 3 | left: 50%; 4 | top: 50%; 5 | transform: translate(-50%, -50%); 6 | } 7 | .live-player { 8 | width: 100%; 9 | height: 6.87rem; 10 | overflow: hidden; 11 | position: relative; 12 | background-size: 0 0; 13 | background-position: center; 14 | background-repeat: no-repeat; 15 | font-size: 0.8rem; 16 | line-height: 5rem; 17 | transition: height .4s linear 0s; 18 | color: #fff; 19 | text-align: center; 20 | pointer-events: none; 21 | #player { 22 | position: relative; 23 | width: 100%; 24 | height: 100%; 25 | // top: 50%; 26 | // transform: translateY(-50%); 27 | } 28 | video { 29 | width: 100%; 30 | height: 100%; 31 | display: block; 32 | font-size: 0.28rem; 33 | position: relative; 34 | } 35 | #player { 36 | opacity: 0; 37 | } 38 | &.has-poster { 39 | background-size: cover; 40 | &:after { 41 | content: ''; 42 | position: absolute; 43 | top: 0; 44 | left: 0; 45 | width: 100%; 46 | height: 100%; 47 | background: rgba(0, 0, 0, .5); 48 | } 49 | } 50 | &.has-live { 51 | #player { 52 | opacity: 1; 53 | } 54 | } 55 | &.camera-pc { 56 | #player { 57 | video { 58 | left: 50%; 59 | transform: translateX(-50%); 60 | } 61 | } 62 | } 63 | &.has-button { 64 | pointer-events: auto; 65 | } 66 | &.has-button.is-waiting { 67 | .live-player__playBtn { 68 | &:after { 69 | background-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTM1IiBoZWlnaHQ9IjE0MCIgdmlld0JveD0iMCAwIDEzNSAxNDAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgZmlsbD0iI2ZmZiI+ICAgIDxyZWN0IHk9IjEwIiB3aWR0aD0iMTUiIGhlaWdodD0iMTIwIiByeD0iNiI+ICAgICAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJoZWlnaHQiICAgICAgICAgICAgIGJlZ2luPSIwLjVzIiBkdXI9IjFzIiAgICAgICAgICAgICB2YWx1ZXM9IjEyMDsxMTA7MTAwOzkwOzgwOzcwOzYwOzUwOzQwOzE0MDsxMjAiIGNhbGNNb2RlPSJsaW5lYXIiICAgICAgICAgICAgIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPiAgICAgICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0ieSIgICAgICAgICAgICAgYmVnaW49IjAuNXMiIGR1cj0iMXMiICAgICAgICAgICAgIHZhbHVlcz0iMTA7MTU7MjA7MjU7MzA7MzU7NDA7NDU7NTA7MDsxMCIgY2FsY01vZGU9ImxpbmVhciIgICAgICAgICAgICAgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIC8+ICAgIDwvcmVjdD4gICAgPHJlY3QgeD0iMzAiIHk9IjEwIiB3aWR0aD0iMTUiIGhlaWdodD0iMTIwIiByeD0iNiI+ICAgICAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJoZWlnaHQiICAgICAgICAgICAgIGJlZ2luPSIwLjI1cyIgZHVyPSIxcyIgICAgICAgICAgICAgdmFsdWVzPSIxMjA7MTEwOzEwMDs5MDs4MDs3MDs2MDs1MDs0MDsxNDA7MTIwIiBjYWxjTW9kZT0ibGluZWFyIiAgICAgICAgICAgICByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4gICAgICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9InkiICAgICAgICAgICAgIGJlZ2luPSIwLjI1cyIgZHVyPSIxcyIgICAgICAgICAgICAgdmFsdWVzPSIxMDsxNTsyMDsyNTszMDszNTs0MDs0NTs1MDswOzEwIiBjYWxjTW9kZT0ibGluZWFyIiAgICAgICAgICAgICByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4gICAgPC9yZWN0PiAgICA8cmVjdCB4PSI2MCIgd2lkdGg9IjE1IiBoZWlnaHQ9IjE0MCIgcng9IjYiPiAgICAgICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0iaGVpZ2h0IiAgICAgICAgICAgICBiZWdpbj0iMHMiIGR1cj0iMXMiICAgICAgICAgICAgIHZhbHVlcz0iMTIwOzExMDsxMDA7OTA7ODA7NzA7NjA7NTA7NDA7MTQwOzEyMCIgY2FsY01vZGU9ImxpbmVhciIgICAgICAgICAgICAgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIC8+ICAgICAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJ5IiAgICAgICAgICAgICBiZWdpbj0iMHMiIGR1cj0iMXMiICAgICAgICAgICAgIHZhbHVlcz0iMTA7MTU7MjA7MjU7MzA7MzU7NDA7NDU7NTA7MDsxMCIgY2FsY01vZGU9ImxpbmVhciIgICAgICAgICAgICAgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIC8+ICAgIDwvcmVjdD4gICAgPHJlY3QgeD0iOTAiIHk9IjEwIiB3aWR0aD0iMTUiIGhlaWdodD0iMTIwIiByeD0iNiI+ICAgICAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJoZWlnaHQiICAgICAgICAgICAgIGJlZ2luPSIwLjI1cyIgZHVyPSIxcyIgICAgICAgICAgICAgdmFsdWVzPSIxMjA7MTEwOzEwMDs5MDs4MDs3MDs2MDs1MDs0MDsxNDA7MTIwIiBjYWxjTW9kZT0ibGluZWFyIiAgICAgICAgICAgICByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4gICAgICAgIDxhbmltYXRlIGF0dHJpYnV0ZU5hbWU9InkiICAgICAgICAgICAgIGJlZ2luPSIwLjI1cyIgZHVyPSIxcyIgICAgICAgICAgICAgdmFsdWVzPSIxMDsxNTsyMDsyNTszMDszNTs0MDs0NTs1MDswOzEwIiBjYWxjTW9kZT0ibGluZWFyIiAgICAgICAgICAgICByZXBlYXRDb3VudD0iaW5kZWZpbml0ZSIgLz4gICAgPC9yZWN0PiAgICA8cmVjdCB4PSIxMjAiIHk9IjEwIiB3aWR0aD0iMTUiIGhlaWdodD0iMTIwIiByeD0iNiI+ICAgICAgICA8YW5pbWF0ZSBhdHRyaWJ1dGVOYW1lPSJoZWlnaHQiICAgICAgICAgICAgIGJlZ2luPSIwLjVzIiBkdXI9IjFzIiAgICAgICAgICAgICB2YWx1ZXM9IjEyMDsxMTA7MTAwOzkwOzgwOzcwOzYwOzUwOzQwOzE0MDsxMjAiIGNhbGNNb2RlPSJsaW5lYXIiICAgICAgICAgICAgIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPiAgICAgICAgPGFuaW1hdGUgYXR0cmlidXRlTmFtZT0ieSIgICAgICAgICAgICAgYmVnaW49IjAuNXMiIGR1cj0iMXMiICAgICAgICAgICAgIHZhbHVlcz0iMTA7MTU7MjA7MjU7MzA7MzU7NDA7NDU7NTA7MDsxMCIgY2FsY01vZGU9ImxpbmVhciIgICAgICAgICAgICAgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIC8+ICAgIDwvcmVjdD48L3N2Zz4=); 70 | } 71 | } 72 | } 73 | } 74 | 75 | .live-player__playBtn { 76 | // width: 1.84rem; 77 | display: none; 78 | width: 90px; 79 | height: 0; 80 | padding-top: 65px; 81 | // padding-top: 1.3rem; 82 | overflow: hidden; 83 | background-color: #000; 84 | border-radius: .18rem; 85 | cursor: pointer; 86 | @extend %center; 87 | &:after { 88 | content: ''; 89 | width: .48rem; 90 | height: .6rem; 91 | width: .48rem; 92 | height: .6rem; 93 | background: url(../img/icon-play.png) 0 0 no-repeat; 94 | background-size: cover; 95 | @extend %center; 96 | } 97 | .live-player.has-button & { 98 | display: block; 99 | } 100 | } 101 | 102 | @keyframes loading{ 103 | 0%{ 104 | background-position: 0px 0; 105 | } 106 | 100%{ 107 | background-position: 0px -630px; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/js/component/player/tpl/live-end.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 直播结束UI 3 | */ 4 | 5 | let renderTpl = (data)=>{ 6 | let tpl = ` 7 |

直播已结束

8 |

9 |

${data.anchorNick}

10 |

ME号:${data.anchorId}

11 | `; 12 | return tpl; 13 | } 14 | 15 | export default renderTpl; 16 | 17 | -------------------------------------------------------------------------------- /src/js/component/player/tpl/live-in.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 直播中UI 3 | */ 4 | 5 | let renderTpl = (data)=>{ 6 | let tpl = ` 7 | 12 |
13 |
播放
14 | `; 15 | return tpl; 16 | } 17 | 18 | export default renderTpl; 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/js/component/recommend-list/img/ico-empty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/js/component/recommend-list/img/ico-empty.png -------------------------------------------------------------------------------- /src/js/component/recommend-list/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component 直播推荐列表(大家都在看) 3 | * @author lixinliang 4 | */ 5 | 6 | import style from './style/index.scss'; 7 | import renderTpl from './tpl/index.js'; 8 | import LazyloadImg from '../../lib/lazy-load-img/index.js'; 9 | 10 | let liveContent = document.querySelector('[data-role="liveContent"]'); 11 | let componentRecommendList = document.createElement('div'); 12 | componentRecommendList.setAttribute('data-component', 'recommendList'); 13 | componentRecommendList.classList.add('live-recommendList'); 14 | liveContent.parentNode.insertBefore(componentRecommendList, liveContent.nextSibling); 15 | 16 | export default { 17 | init() { 18 | RequqestApi.getRecommendList().then((response)=>{ 19 | let list = response.data; 20 | componentRecommendList.innerHTML = `${ renderTpl(list) }`; 21 | new LazyloadImg({ 22 | el: componentRecommendList.querySelector('.live-recommendList__content'), 23 | mode: 'diy', 24 | position: { 25 | top: -150, 26 | right: -150, 27 | bottom: -150, 28 | left: -150, 29 | }, 30 | diy: { 31 | backgroundSize: 'cover', 32 | backgroundRepeat: 'no-repeat', 33 | backgroundPosition: 'center center', 34 | }, 35 | }).start(); 36 | }) 37 | 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /src/js/component/recommend-list/style/index.scss: -------------------------------------------------------------------------------- 1 | .live-recommendList { 2 | overflow: hidden; 3 | background: #ededed; 4 | &::after { 5 | content: ""; 6 | clear: both; 7 | display: table; 8 | } 9 | .live-recommendList__head { 10 | font-size: 0.28rem; 11 | color: #2e2e2e; 12 | height: .88rem; 13 | line-height: .88rem; 14 | padding: 0 .20rem; 15 | overflow: hidden; 16 | user-select: none; 17 | } 18 | .live-recommendList__item { 19 | float: left; 20 | width: 50%; 21 | a { 22 | display: block; 23 | background: #f7f7f7; 24 | } 25 | &:nth-child(2n+1) { 26 | padding-right: 2px; 27 | } 28 | &:nth-child(2n) { 29 | padding-left: 2px; 30 | } 31 | } 32 | .live-recommendList__img { 33 | position: relative; 34 | padding-bottom: 100%; 35 | background-color: #f5f5f5; 36 | img { 37 | position: absolute; 38 | top: 0; 39 | left: 0; 40 | width: 100%; 41 | height: 100%; 42 | font-size: 0.28rem; 43 | } 44 | } 45 | .live-recommendList__nick { 46 | font-size: 0.24rem; 47 | height: 0.76rem; 48 | padding: 0 0.18rem 0.16rem; 49 | line-height: 0.60rem; 50 | overflow: hidden; 51 | word-break: normal; 52 | word-wrap: normal; 53 | white-space: nowrap; 54 | text-overflow: ellipsis; 55 | // &::after { 56 | // content: ""; 57 | // clear: both; 58 | // display: table; 59 | // } 60 | } 61 | .live-recommendList__count { 62 | font-size: 0.20rem; 63 | color: #484848; 64 | height: 0.60rem; 65 | line-height: 0.60rem; 66 | float: right; 67 | margin-left: 0.1rem; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/js/component/recommend-list/tpl/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 推荐列表 3 | */ 4 | 5 | let renderTpl = (list)=>{ 6 | let tpl = ` 7 |
大家都在看
8 |
9 | ${list.map(item => 10 | `
11 | 12 |
13 |

${item.users}人${item.nick}

14 |
15 |
` 16 | ).join('')} 17 |
18 | `; 19 | return tpl; 20 | } 21 | 22 | export default renderTpl; 23 | -------------------------------------------------------------------------------- /src/js/component/top-bar/img/app-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/js/component/top-bar/img/app-logo.png -------------------------------------------------------------------------------- /src/js/component/top-bar/img/icon-share.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/js/component/top-bar/img/icon-share.png -------------------------------------------------------------------------------- /src/js/component/top-bar/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component 顶部工具条 3 | * @author David 4 | */ 5 | 6 | import style from './style/index.scss'; 7 | import renderTpl from './tpl/index.js'; 8 | 9 | export default { 10 | init () { 11 | let componentTopBar = document.querySelector('[data-component="topBar"]'); 12 | componentTopBar.innerHTML = `${ renderTpl() }`; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/js/component/top-bar/style/index.scss: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 1.05rem; 3 | } 4 | 5 | .live-topBar { 6 | position: fixed; 7 | left: 0; 8 | top: 0; 9 | width: 100%; 10 | height: 1.05rem; 11 | overflow: hidden; 12 | user-select: none; 13 | // padding: .13rem 0; 14 | // border-bottom: .02rem solid #7b7360; 15 | background: #2e2e2e; 16 | // background: rgba(46, 46, 46, 0.8); 17 | &::before { 18 | position: absolute; 19 | left: .28rem; 20 | top: 50%; 21 | transform: translate(0, -50%); 22 | // margin-top: .01rem; 23 | content: ''; 24 | width: .76rem; 25 | height: .76rem; 26 | background: url(../img/app-logo.png) 0 0 no-repeat; 27 | background-size: cover; 28 | } 29 | &::after { 30 | content: ""; 31 | position: absolute; 32 | width: 100%; 33 | height: 1px; 34 | left: 0; 35 | bottom: 0; 36 | background-color: #7b7360; 37 | transform: scaleY(.5); 38 | } 39 | } 40 | .live-topBar__cnt { 41 | padding-left: 1.33rem; 42 | font-size: .24rem; 43 | height: .24rem; 44 | line-height: .24rem; 45 | color: #fff; 46 | &:first-of-type { 47 | margin-top: .25rem; 48 | margin-bottom: .1rem; 49 | } 50 | } 51 | .live-topBar__btn { 52 | position: absolute; 53 | right: .25rem; 54 | top: 50%; 55 | transform: translate(0, -50%); 56 | width: 2.43rem; 57 | height: .66rem; 58 | // line-height: .66rem; 59 | // text-align: center; 60 | 61 | // text-align: center; 62 | // border: .03rem solid #7d7051; 63 | border: 1px solid #7d7051; 64 | border-radius: .15rem; 65 | background: #524c3e; 66 | // outline: none; 67 | cursor: pointer; 68 | &.is-active { 69 | background: #736b50; 70 | } 71 | &::after { 72 | content: "立即打开"; 73 | position: absolute; 74 | top: 50%; 75 | left: 50%; 76 | transform: translateX(-50%) translateY(-50%); 77 | font-size: .28rem; 78 | height: .28rem; 79 | line-height: .28rem; 80 | color: #e0c37e; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/js/component/top-bar/tpl/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file top-bar 3 | */ 4 | 5 | let renderTpl = (data)=>{ 6 | let tpl = ` 7 |

下载ME立即与TA

8 |

零距离实时互动

9 |
10 | `; 11 | return tpl; 12 | } 13 | 14 | export default renderTpl; 15 | 16 | -------------------------------------------------------------------------------- /src/js/lib/anim/anim.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component animjs 包含以下功能:点赞图片预加载、点赞图片分组、定义点赞的canvas样式 3 | * @author lixinliang 4 | */ 5 | 6 | import Ticker from './modules/ticker.js'; 7 | 8 | const DPR = window.devicePixelRatio || 1; 9 | 10 | let closure = {}; 11 | let storage = []; 12 | 13 | // animjs 不涉及动画的实现 调用show方法时 把图片集合交给 动画引擎处理 14 | class Anim { 15 | 16 | /** 17 | * new的时候决定该实例使用那种动画引擎 18 | * @type {String} name 动画引擎 19 | */ 20 | constructor ( name = '' ) { 21 | let canvas = document.createElement('canvas'); 22 | let context = canvas.getContext('2d'); 23 | this.canvas = canvas; 24 | this.context = context; 25 | this.id = storage.length; 26 | this.name = name; 27 | let imageSet = {}; 28 | let imageMap = {}; 29 | storage.push({ imageSet, imageMap }); 30 | let ticker = new Ticker(); 31 | ticker.on(() => { 32 | context.clearRect(0, 0, canvas.width, canvas.height); 33 | }); 34 | this.ticker = ticker; 35 | this.dpr = DPR; 36 | if (DPR > 1) { 37 | this.canvas.style['transform'] = this.canvas.style['-webkit-transform'] = `scale(${ 1/DPR })`; 38 | } 39 | } 40 | 41 | /** 42 | * 传入一组图片集合 进行预加载 43 | * @param {Object} imageSet 资源集合 { 图片id : 图片url } 44 | * @return {Anim} this anim 45 | */ 46 | mainfest ( imageSet ) { 47 | let mySet = storage[this.id].imageSet; 48 | for (let key in imageSet) { 49 | let value = imageSet[key]; 50 | let image = { 51 | url : value, 52 | status : false 53 | }; 54 | image.img = loadImage(image.url, () => { 55 | image.status = true; 56 | }); 57 | mySet[key] = image; 58 | } 59 | return this 60 | } 61 | 62 | /** 63 | * 传入图片分组的映射关系 64 | * @param {Object} imageMap 资源分组映射 { 分组id : [图片id,...] } 65 | * @return {Anim} this anim 66 | */ 67 | group ( imageMap ) { 68 | let myMap = storage[this.id].imageMap; 69 | for (let key in imageMap) { 70 | myMap[key] = imageMap[key].slice(); 71 | } 72 | return this 73 | } 74 | 75 | /** 76 | * 随机播放动画 77 | * @param {String} groupName 资源分组名 78 | * @return {Anim} this anim 79 | */ 80 | show ( groupName ) { 81 | let { imageSet, imageMap } = storage[this.id]; 82 | let images = []; 83 | imageMap[groupName].forEach((imageName) => { 84 | let image = imageSet[imageName]; 85 | if (image.status) { 86 | images.push(image.img); 87 | } 88 | }); 89 | if (this.name) { 90 | return new closure[this.name](this, images); 91 | } 92 | return this 93 | } 94 | 95 | /** 96 | * 设置宽度 97 | * @param {Number} canvasWidth 画布宽度 98 | * @return {Anim} this anim 99 | */ 100 | width ( canvasWidth ) { 101 | let result = correct(canvasWidth); 102 | if (result) { 103 | let { num, str } = result; 104 | this.canvas.width = num; 105 | this.canvas.style.width = str; 106 | } 107 | return this 108 | } 109 | 110 | /** 111 | * 设置高度 112 | * @param {Number} canvasHeight 画布高度 113 | * @return {Anim} this anim 114 | */ 115 | height ( canvasHeight ) { 116 | 117 | let result = correct(canvasHeight); 118 | if (result) { 119 | let { num, str } = result; 120 | this.canvas.height = num; 121 | this.canvas.style.height = str; 122 | } 123 | 124 | return this 125 | } 126 | } 127 | 128 | export default Anim; 129 | 130 | /** 131 | * 加载图片 132 | * @param {String} url 图片地址 133 | * @param {Function} cb 加载回调 134 | * @return {HTMLImageElement} img 图片标签 135 | */ 136 | function loadImage ( url, cb ) { 137 | let img = new Image; 138 | img.onload = cb; 139 | img.src = url; 140 | return img; 141 | }; 142 | 143 | /** 144 | * 对canvas的宽高修正 在hidpi屏上 尺寸需要加倍 145 | * @param {Number} value 尺寸 146 | * @return {Number} value 尺寸 147 | */ 148 | function correct ( value ) { 149 | value *= DPR; 150 | let num; 151 | let str; 152 | if (typeof value == 'number') { 153 | num = value; 154 | str = value + 'px'; 155 | } else if (typeof value == 'string') { 156 | str = value; 157 | num = parseInt(value); 158 | } 159 | if (num && str) { 160 | return { num, str } 161 | } else { 162 | return null 163 | } 164 | }; 165 | 166 | /** 167 | * 定义动画引擎 168 | * @param {String} name 动画引擎名 169 | * @param {Class} plugin 动画引擎 170 | */ 171 | Anim.setup = function ( name, plugin ) { 172 | closure[name] = plugin; 173 | }; 174 | -------------------------------------------------------------------------------- /src/js/lib/anim/anim.like.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component anim.like 基于 animjs 的点赞动画引擎 3 | * @author lixinliang 4 | */ 5 | 6 | import { 7 | getRandomByMedian, 8 | getRandomBetweenInt, 9 | getRandomWithBinomial, 10 | } from './modules/random.js'; 11 | 12 | // 点赞动画引擎 13 | class Like { 14 | /** 15 | * 调用 new Like 创建一个赞的动画 16 | * @param {Anim} anim Anim实例 17 | * @param {Array} images 素材集合 18 | * @return {Like} like 点赞实例 19 | */ 20 | constructor ( anim, images ) { 21 | // 从图片集合 随机抽取一张 22 | let image = images[getRandomBetweenInt(0, images.length - 1)]; 23 | 24 | this.image = image; 25 | this.canvas = anim.canvas; 26 | 27 | let DPR = this.dpr = anim.dpr; 28 | 29 | let imageWidth = image.width; 30 | let imageHeight = image.height; 31 | 32 | // @return 当前动画进度 0 ~ 100 33 | let getImageTimeline = this.createImageTimeline(); 34 | // @return 旋转后的图片 canvas 35 | let getImageData = this.createImageData(); 36 | // @return 获取当前缩放曲线 number 37 | let getImageScaleSize = this.createImageScaleSize(); 38 | // @return 获取当前透明度曲线 number 39 | let getImageOpacityValue = this.createImageOpacityValue(); 40 | // @return 获取当前位移曲线 { x, y } 41 | let getImagePathPosition = this.createImagePathPosition(); 42 | 43 | let handler = () => { 44 | let percentage = getImageTimeline(); 45 | let { imageCanvas, imageContext } = getImageData(); 46 | let scaleSize = getImageScaleSize(percentage); 47 | let globalAlpha = getImageOpacityValue(percentage); 48 | let { x, y } = getImagePathPosition(percentage); 49 | 50 | // 最后渲染的时候 再把 dpr 计算进去 51 | if (scaleSize == 1 && globalAlpha == 1) { 52 | anim.context.drawImage(imageCanvas, x * DPR, y * DPR, imageWidth * DPR, imageHeight * DPR); 53 | } else { 54 | if (globalAlpha == 1) { 55 | let scaleWidth = imageWidth * scaleSize; 56 | let deltaWidth = (imageWidth - scaleWidth) / 2; 57 | let scaleHeight = imageHeight * scaleSize; 58 | let deltaHeight = (imageHeight - scaleHeight) / 2; 59 | anim.context.drawImage(imageCanvas, (x + deltaWidth) * DPR, (y + deltaHeight) * DPR, scaleWidth * DPR, scaleHeight * DPR); 60 | } else { 61 | anim.context.globalAlpha = globalAlpha; 62 | anim.context.drawImage(imageCanvas, x * DPR, y * DPR, imageWidth * DPR, imageHeight * DPR); 63 | anim.context.globalAlpha = 1; 64 | } 65 | } 66 | if (percentage == 100) { 67 | getImageTimeline = null; 68 | getImageData = null; 69 | getImageScaleSize = null; 70 | getImageOpacityValue = null; 71 | getImagePathPosition = null; 72 | anim.ticker.off(handler); 73 | } 74 | }; 75 | anim.ticker.on(handler); 76 | } 77 | 78 | /** 79 | * 创建每一个动画的独立时间线 80 | * @return {Function} getImageTimeline 获得该动画当前时间线的函数 81 | */ 82 | createImageTimeline () { 83 | // 动画总时基数 3000ms 浮动系数 20% 84 | let originDuration = 3000; 85 | let range = 0.2; 86 | 87 | let duration = getRandomByMedian(originDuration, range); 88 | let times = Math.round(duration / 16.7); 89 | let now = 0; 90 | return () => { 91 | now++; 92 | let percentage = parseInt(now * 1000 / times) / 10; 93 | return percentage > 100 ? 100 : percentage 94 | } 95 | } 96 | 97 | /** 98 | * 创建每一个动画的独立形态 99 | * @return {Function} getImageData 获得该动画形态的对象的函数 100 | */ 101 | createImageData () { 102 | // 图像旋转幅度 -30 ~ 30deg 103 | let minRotate = -30; 104 | let maxRotate = 30; 105 | // 缩放系数的二项分布 70 ~ 120 出现95的概率最高 106 | let minScale = 70; 107 | let maxScale = 120; 108 | // 缩放最大值为105 超出105的概率以105为对称轴 添加到 90 ~ 105 的区间 109 | let maxLimitScale = 105; 110 | 111 | let image = this.image; 112 | let { width, height } = image; 113 | let rotate = getRandomBetweenInt(minRotate, maxRotate); 114 | let scale = getRandomWithBinomial(minScale, maxScale); 115 | if (scale > maxLimitScale) { 116 | scale -= (maxScale - maxLimitScale); 117 | } 118 | scale = scale / 100; 119 | 120 | let imageCanvas = document.createElement('canvas'); 121 | let imageContext = imageCanvas.getContext('2d'); 122 | 123 | // 使用离屏canvas保存旋转后的图片并多次复用 为了避免旋转后图片被切割 画布的尺寸是图片的2倍 124 | imageCanvas.width = width * 2; 125 | imageCanvas.height = height * 2; 126 | 127 | imageContext.translate(width, height); 128 | imageContext.rotate(rotate * Math.PI / 180); 129 | imageContext.scale(scale, scale); 130 | imageContext.translate(-width, -height); 131 | 132 | imageContext.drawImage(image, width / 2, height / 2, width, height); 133 | 134 | return () => ({ imageCanvas, imageContext }) 135 | } 136 | 137 | /** 138 | * 创建每一个动画的独立放大曲线 139 | * @return {Function} getImageScaleSize 获得该动画当前尺寸的函数 140 | */ 141 | createImageScaleSize () { 142 | // 完成缩放动画的百分比阶段 9 ~ 11 143 | let minAppearStage = 9; 144 | let maxAppearStage = 11; 145 | 146 | let appearStage = getRandomBetweenInt(minAppearStage, maxAppearStage); 147 | return ( percentage ) => { 148 | return percentage >= appearStage ? 1 : ((parseInt(100 * percentage / appearStage) / 100) || 0.1) 149 | } 150 | } 151 | 152 | /** 153 | * 创建每一个动画的独立透明曲线 154 | * @return {Function} getImageOpacityValue 获得该动画当前透明度的函数 155 | */ 156 | 157 | createImageOpacityValue () { 158 | // 开始淡出动画的百分比阶段 82 ~ 86 159 | let minDisappearStage = 82; 160 | let maxDisappearStage = 86; 161 | 162 | let disappearStage = getRandomBetweenInt(minDisappearStage, maxDisappearStage); 163 | let area = 100 - disappearStage; 164 | return ( percentage ) => { 165 | if (percentage < disappearStage) { 166 | return 1; 167 | } else { 168 | let opacity = ((percentage - disappearStage) / area).toFixed(2) - 0; 169 | return 1 - opacity; 170 | } 171 | } 172 | } 173 | 174 | /** 175 | * 创建每一个动画的独立位移曲线 176 | * @return {Function} getImagePathPosition 获得该动画当前位移值的函数 177 | */ 178 | createImagePathPosition () { 179 | // 以下计算均以 dpr 为 1作为参考值 180 | // 动画初始位置的横向定位的浮动系数 10% 181 | let startX = 10; 182 | // 动画纵向位移距离delta y是总距离的 95% 浮动系数 5% 183 | let deltaY = 0.95; 184 | let deltaYRange = 1 - deltaY; 185 | // 动画横向位移距离符合正弦曲线 正弦波峰最小值10% 最大值15% 186 | let minCrest = 10; 187 | let maxCrest = 15; 188 | // 正弦波长系数 最小值16 最大值32 189 | let minWavelength = 16; 190 | let maxWavelength = 32; 191 | 192 | let DPR = this.dpr; 193 | // @2x 图片要先砍半 194 | let imageWidth = this.image.width / 2; 195 | let imageHeight = this.image.height / 2; 196 | // 画布尺寸根据dpr计算 197 | let canvasWidth = this.canvas.width / DPR; 198 | let canvasHeight = this.canvas.height / DPR; 199 | // console.log(imageWidth, imageHeight, canvasWidth, canvasHeight); 200 | let originX = canvasWidth * (0.5 + getRandomBetweenInt(-startX, startX) / 100); 201 | let originY = canvasHeight; 202 | // 初始值居中是画布总长度减去图片长度后的一半 203 | // 再减去旋转的偏移量 也是图片尺寸的一半 204 | // 刚好减去一倍的图片尺寸 205 | originX -= imageWidth; 206 | originY -= imageHeight; 207 | 208 | let translateX = canvasWidth * getRandomBetweenInt(minCrest, maxCrest) / 100; 209 | let translateY = getRandomByMedian(originY * deltaY, deltaYRange); 210 | 211 | let wavelength = getRandomBetweenInt(minWavelength, maxWavelength); 212 | 213 | // 方向是 -1/1 214 | let direction = getRandomBetweenInt(0, 1) * 2 - 1; 215 | 216 | return ( percentage ) => { 217 | let x = originX + translateX * Math.sin(percentage / wavelength) * direction; 218 | let y = originY - translateY * percentage / 100; 219 | return { x, y } 220 | } 221 | } 222 | } 223 | 224 | export default Like; 225 | -------------------------------------------------------------------------------- /src/js/lib/anim/custom.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @desc 把点赞源码打包成一个js 可以提供给其他团队快速使用 3 | * @version 订制版 开发者自定义点赞图片 4 | * @author lixinliang 5 | * @doc 使用文档等待补充 6 | */ 7 | 8 | import Anim from 'anim'; 9 | import Like from 'anim.like'; 10 | 11 | // 把点赞引擎添加 animjs 上 12 | Anim.setup('like', Like); 13 | // 创建实例 14 | let anim = new Anim('like'); 15 | // 设置尺寸 16 | anim.width(60).height(280); 17 | // 设置资源 18 | let base64 = { 19 | user01 : require('../../../assets/like/heart/user/like01.png'), 20 | user02 : require('../../../assets/like/heart/user/like02.png'), 21 | user03 : require('../../../assets/like/heart/user/like03.png'), 22 | audience01 : require('../../../assets/like/heart/audience/like01.png'), 23 | audience02 : require('../../../assets/like/heart/audience/like02.png'), 24 | audience03 : require('../../../assets/like/heart/audience/like03.png'), 25 | audience04 : require('../../../assets/like/heart/audience/like04.png'), 26 | audience05 : require('../../../assets/like/heart/audience/like05.png'), 27 | audience06 : require('../../../assets/like/heart/audience/like06.png'), 28 | audience07 : require('../../../assets/like/heart/audience/like07.png'), 29 | }; 30 | // 设置资源集合 与 映射分组 31 | anim.mainfest(mainfest()).group(mainfest.group); 32 | 33 | export default anim; 34 | 35 | function mainfest () { 36 | let result = {}; 37 | let user = append.call(result, 'user', 3); 38 | let audience = append.call(result, 'audience', 7); 39 | // let user = result::append('user', 3); 40 | // let audience = result::append('audience', 7); 41 | // let user = mainfest::append('user', 6); 42 | // let audience = user; 43 | mainfest.group = { user, audience }; 44 | return result; 45 | } 46 | 47 | function append ( directory, length ) { 48 | const baseImageUrl = './assets/like/heart/'; 49 | // const baseImageUrl = './assets/like/luhan/'; 50 | const defaultFileName = 'like'; 51 | const defaultFileType = 'png'; 52 | return Array.apply(null, { length }).map((value, index) => fixed(index + 1 + '')).map((id) => { 53 | let key = directory + id; 54 | // this[key] = [baseImageUrl, directory, '/', defaultFileName, id, '.', defaultFileType].join(''); 55 | this[key] = base64[key]; 56 | return key; 57 | }); 58 | } 59 | 60 | function fixed ( value ) { 61 | return value.length == 1 ? '0' + value : value; 62 | } 63 | 64 | export default anim; 65 | -------------------------------------------------------------------------------- /src/js/lib/anim/modules/random.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * 在区间 [min, max] 上的等概率随机整数 4 | * @param {Number} min 最小值 5 | * @param {Number} max 最大值 6 | * @return {Number} value 随机数 7 | */ 8 | let getRandomBetweenInt = function ( min, max ) { 9 | min = parseInt(min) || 0; 10 | max = parseInt(max) || 0; 11 | if (min > max) { 12 | [min, max] = [max, min]; 13 | } 14 | let value = min + Math.round(Math.random() * (max + 1 - min)); 15 | return value == max + 1 ? min : value; 16 | }; 17 | 18 | /** 19 | * 以 value 为中数上下浮动 percentage 的区间上的等概率随机整数 20 | * @param {Number} value 中数 21 | * @param {Number} percentage 浮动系数 22 | * @return {Number} value 随机数 23 | */ 24 | let getRandomByMedian = function ( value, percentage ) { 25 | value = parseInt(value) || 0; 26 | if (percentage < 0) { 27 | percentage = 0; 28 | } 29 | if (percentage > 1) { 30 | percentage = 1; 31 | } 32 | return getRandomBetweenInt(value * (1 - percentage), value * (1 + percentage)); 33 | }; 34 | 35 | /** 36 | * 在区间 [min, max] 上的遵循二项分布的随机整数 其中中数概率最高 37 | * @param {Number} min 最小值 38 | * @param {Number} max 最大值 39 | * @return {Number} value 随机数 40 | */ 41 | let getRandomWithBinomial = function ( min, max ) { 42 | min = parseInt(min) || 0; 43 | max = parseInt(max) || 0; 44 | if (min > max) { 45 | [min, max] = [max, min]; 46 | } 47 | let median = parseInt((max - min) / 2); 48 | return min + getRandomBetweenInt(0, median) + getRandomBetweenInt(0, median); 49 | }; 50 | 51 | export { 52 | getRandomByMedian, 53 | getRandomBetweenInt, 54 | getRandomWithBinomial, 55 | }; 56 | -------------------------------------------------------------------------------- /src/js/lib/anim/modules/ticker.js: -------------------------------------------------------------------------------- 1 | const TICK = 'tick'; 2 | const TICKER = new Event(TICK); 3 | 4 | let elements = []; 5 | 6 | let requestAnimationFrame = window.requestAnimationFrame || window.webkitRequestAnimationFrame || ((fn) => setTimeout(fn, 16)); 7 | 8 | let looper = () => { 9 | elements.forEach((elem) => elem.dispatchEvent(TICKER)); 10 | requestAnimationFrame(looper); 11 | }; 12 | 13 | requestAnimationFrame(looper); 14 | 15 | class Ticker { 16 | constructor () { 17 | this.elem = document.createElement('div'); 18 | elements.push(this.elem); 19 | } 20 | on ( fn ) { 21 | this.elem.addEventListener(TICK, fn, false); 22 | return this 23 | } 24 | off ( fn ) { 25 | this.elem.removeEventListener(TICK, fn, false); 26 | return this 27 | } 28 | } 29 | 30 | export default Ticker; 31 | -------------------------------------------------------------------------------- /src/js/lib/classlist-polyfill/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * classList.js: Cross-browser full element.classList implementation. 3 | * 1.1.20170427 4 | * 5 | * By Eli Grey, http://eligrey.com 6 | * License: Dedicated to the public domain. 7 | * See https://github.com/eligrey/classList.js/blob/master/LICENSE.md 8 | */ 9 | 10 | /*global self, document, DOMException */ 11 | 12 | /*! @source http://purl.eligrey.com/github/classList.js/blob/master/classList.js */ 13 | 14 | if ("document" in window.self) { 15 | 16 | // Full polyfill for browsers with no classList support 17 | // Including IE < Edge missing SVGElement.classList 18 | if (!("classList" in document.createElement("_")) 19 | || document.createElementNS && !("classList" in document.createElementNS("http://www.w3.org/2000/svg","g"))) { 20 | 21 | (function (view) { 22 | 23 | "use strict"; 24 | 25 | if (!('Element' in view)) return; 26 | 27 | var 28 | classListProp = "classList" 29 | , protoProp = "prototype" 30 | , elemCtrProto = view.Element[protoProp] 31 | , objCtr = Object 32 | , strTrim = String[protoProp].trim || function () { 33 | return this.replace(/^\s+|\s+$/g, ""); 34 | } 35 | , arrIndexOf = Array[protoProp].indexOf || function (item) { 36 | var 37 | i = 0 38 | , len = this.length 39 | ; 40 | for (; i < len; i++) { 41 | if (i in this && this[i] === item) { 42 | return i; 43 | } 44 | } 45 | return -1; 46 | } 47 | // Vendors: please allow content code to instantiate DOMExceptions 48 | , DOMEx = function (type, message) { 49 | this.name = type; 50 | this.code = DOMException[type]; 51 | this.message = message; 52 | } 53 | , checkTokenAndGetIndex = function (classList, token) { 54 | if (token === "") { 55 | throw new DOMEx( 56 | "SYNTAX_ERR" 57 | , "An invalid or illegal string was specified" 58 | ); 59 | } 60 | if (/\s/.test(token)) { 61 | throw new DOMEx( 62 | "INVALID_CHARACTER_ERR" 63 | , "String contains an invalid character" 64 | ); 65 | } 66 | return arrIndexOf.call(classList, token); 67 | } 68 | , ClassList = function (elem) { 69 | var 70 | trimmedClasses = strTrim.call(elem.getAttribute("class") || "") 71 | , classes = trimmedClasses ? trimmedClasses.split(/\s+/) : [] 72 | , i = 0 73 | , len = classes.length 74 | ; 75 | for (; i < len; i++) { 76 | this.push(classes[i]); 77 | } 78 | this._updateClassName = function () { 79 | elem.setAttribute("class", this.toString()); 80 | }; 81 | } 82 | , classListProto = ClassList[protoProp] = [] 83 | , classListGetter = function () { 84 | return new ClassList(this); 85 | } 86 | ; 87 | // Most DOMException implementations don't allow calling DOMException's toString() 88 | // on non-DOMExceptions. Error's toString() is sufficient here. 89 | DOMEx[protoProp] = Error[protoProp]; 90 | classListProto.item = function (i) { 91 | return this[i] || null; 92 | }; 93 | classListProto.contains = function (token) { 94 | token += ""; 95 | return checkTokenAndGetIndex(this, token) !== -1; 96 | }; 97 | classListProto.add = function () { 98 | var 99 | tokens = arguments 100 | , i = 0 101 | , l = tokens.length 102 | , token 103 | , updated = false 104 | ; 105 | do { 106 | token = tokens[i] + ""; 107 | if (checkTokenAndGetIndex(this, token) === -1) { 108 | this.push(token); 109 | updated = true; 110 | } 111 | } 112 | while (++i < l); 113 | 114 | if (updated) { 115 | this._updateClassName(); 116 | } 117 | }; 118 | classListProto.remove = function () { 119 | var 120 | tokens = arguments 121 | , i = 0 122 | , l = tokens.length 123 | , token 124 | , updated = false 125 | , index 126 | ; 127 | do { 128 | token = tokens[i] + ""; 129 | index = checkTokenAndGetIndex(this, token); 130 | while (index !== -1) { 131 | this.splice(index, 1); 132 | updated = true; 133 | index = checkTokenAndGetIndex(this, token); 134 | } 135 | } 136 | while (++i < l); 137 | 138 | if (updated) { 139 | this._updateClassName(); 140 | } 141 | }; 142 | classListProto.toggle = function (token, force) { 143 | token += ""; 144 | 145 | var 146 | result = this.contains(token) 147 | , method = result ? 148 | force !== true && "remove" 149 | : 150 | force !== false && "add" 151 | ; 152 | 153 | if (method) { 154 | this[method](token); 155 | } 156 | 157 | if (force === true || force === false) { 158 | return force; 159 | } else { 160 | return !result; 161 | } 162 | }; 163 | classListProto.toString = function () { 164 | return this.join(" "); 165 | }; 166 | 167 | if (objCtr.defineProperty) { 168 | var classListPropDesc = { 169 | get: classListGetter 170 | , enumerable: true 171 | , configurable: true 172 | }; 173 | try { 174 | objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); 175 | } catch (ex) { // IE 8 doesn't support enumerable:true 176 | // adding undefined to fight this issue https://github.com/eligrey/classList.js/issues/36 177 | // modernie IE8-MSW7 machine has IE8 8.0.6001.18702 and is affected 178 | if (ex.number === undefined || ex.number === -0x7FF5EC54) { 179 | classListPropDesc.enumerable = false; 180 | objCtr.defineProperty(elemCtrProto, classListProp, classListPropDesc); 181 | } 182 | } 183 | } else if (objCtr[protoProp].__defineGetter__) { 184 | elemCtrProto.__defineGetter__(classListProp, classListGetter); 185 | } 186 | 187 | }(window.self)); 188 | 189 | } 190 | 191 | // There is full or partial native classList support, so just check if we need 192 | // to normalize the add/remove and toggle APIs. 193 | 194 | (function () { 195 | "use strict"; 196 | 197 | var testElement = document.createElement("_"); 198 | 199 | testElement.classList.add("c1", "c2"); 200 | 201 | // Polyfill for IE 10/11 and Firefox <26, where classList.add and 202 | // classList.remove exist but support only one argument at a time. 203 | if (!testElement.classList.contains("c2")) { 204 | var createMethod = function(method) { 205 | var original = DOMTokenList.prototype[method]; 206 | 207 | DOMTokenList.prototype[method] = function(token) { 208 | var i, len = arguments.length; 209 | 210 | for (i = 0; i < len; i++) { 211 | token = arguments[i]; 212 | original.call(this, token); 213 | } 214 | }; 215 | }; 216 | createMethod('add'); 217 | createMethod('remove'); 218 | } 219 | 220 | testElement.classList.toggle("c3", false); 221 | 222 | // Polyfill for IE 10 and Firefox <24, where classList.toggle does not 223 | // support the second argument. 224 | if (testElement.classList.contains("c3")) { 225 | var _toggle = DOMTokenList.prototype.toggle; 226 | 227 | DOMTokenList.prototype.toggle = function(token, force) { 228 | if (1 in arguments && !this.contains(token) === !force) { 229 | return force; 230 | } else { 231 | return _toggle.call(this, token); 232 | } 233 | }; 234 | 235 | } 236 | 237 | testElement = null; 238 | }()); 239 | 240 | } 241 | -------------------------------------------------------------------------------- /src/js/lib/delegate/index.js: -------------------------------------------------------------------------------- 1 | if (!Element.prototype.matches) { 2 | Element.prototype.matches = 3 | Element.prototype.matchesSelector || 4 | Element.prototype.webkitMatchesSelector || 5 | function(s) { 6 | var matches = (this.document || this.ownerDocument).querySelectorAll(s), 7 | i = matches.length; 8 | while (--i >= 0 && matches.item(i) !== this) {} 9 | return i > -1; 10 | }; 11 | } 12 | 13 | export default function delegate ( selector, handler, counter ) { 14 | if (counter) { 15 | let callback = function ( event ) { 16 | for (let target = event.target; target && target != this; target = target.parentNode) { 17 | if (target.matches(selector)) { 18 | counter--; 19 | if (!counter) { 20 | this.removeEventListener(event.type, callback, false); 21 | } 22 | return handler.apply(target, arguments); 23 | } 24 | } 25 | }; 26 | return callback; 27 | } 28 | return function ( event ) { 29 | for (let target = event.target; target && target != this; target = target.parentNode) { 30 | if (target.matches(selector)) { 31 | return handler.apply(target, arguments); 32 | } 33 | } 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/js/lib/event/index.js: -------------------------------------------------------------------------------- 1 | export default class Event { 2 | constructor() { 3 | this.handlers = {}; 4 | } 5 | on ( type, handler ) { 6 | let handlers = this.handlers; 7 | if (handlers[type]) { 8 | handlers[type].push(handler); 9 | } else { 10 | handlers[type] = [handler]; 11 | } 12 | return this; 13 | } 14 | trigger ( type, callback ) { 15 | let handlers = this.handlers; 16 | if (handlers[type] instanceof Array) { 17 | handlers[type].forEach(( handler ) => { 18 | callback(handler); 19 | }); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/js/lib/jsonp/index.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var JSONP, computedUrl, createElement, encode, noop, objectToURI, random, randomString; 3 | 4 | createElement = function(tag) { 5 | return window.document.createElement(tag); 6 | }; 7 | 8 | encode = window.encodeURIComponent; 9 | 10 | random = Math.random; 11 | 12 | JSONP = function(options) { 13 | var callback, callbackFunc, callbackName, done, head, params, script; 14 | if (options == null) { 15 | options = {}; 16 | } 17 | params = { 18 | data: options.data || {}, 19 | error: options.error || noop, 20 | success: options.success || noop, 21 | beforeSend: options.beforeSend || noop, 22 | complete: options.complete || noop, 23 | url: options.url || '' 24 | }; 25 | params.computedUrl = computedUrl(params); 26 | if (params.url.length === 0) { 27 | throw new Error('MissingUrl'); 28 | } 29 | done = false; 30 | if (params.beforeSend({}, params) !== false) { 31 | callbackName = options.callbackName || 'callback'; 32 | callbackFunc = options.callbackFunc || 'jsonp_' + randomString(15); 33 | callback = params.data[callbackName] = callbackFunc; 34 | window[callback] = function(data) { 35 | window[callback] = null; 36 | params.success(data, params); 37 | return params.complete(data, params); 38 | }; 39 | script = createElement('script'); 40 | script.src = computedUrl(params); 41 | script.async = true; 42 | 43 | script.onerror = function(evt) { 44 | params.error({ 45 | url: script.src, 46 | event: evt 47 | }); 48 | return params.complete({ 49 | url: script.src, 50 | event: evt 51 | }, params); 52 | }; 53 | script.onload = script.onreadystatechange = function() { 54 | var ref, ref1; 55 | 56 | if (done) { 57 | return; 58 | } 59 | done = true; 60 | if (script) { 61 | script.onload = script.onreadystatechange = null; 62 | if ((ref1 = script.parentNode) != null) { 63 | ref1.removeChild(script); 64 | } 65 | return script = null; 66 | } 67 | }; 68 | head = window.document.getElementsByTagName('head')[0] || window.document.documentElement; 69 | head.insertBefore(script, head.firstChild); 70 | } 71 | return { 72 | abort: function() { 73 | window[callback] = function() { 74 | return window[callback] = null; 75 | }; 76 | done = true; 77 | if (script != null ? script.parentNode : void 0) { 78 | script.onload = script.onreadystatechange = null; 79 | script.parentNode.removeChild(script); 80 | return script = null; 81 | } 82 | } 83 | }; 84 | }; 85 | 86 | noop = function() { 87 | return void 0; 88 | }; 89 | 90 | computedUrl = function(params) { 91 | var url; 92 | url = params.url; 93 | url += params.url.indexOf('?') < 0 ? '?' : '&'; 94 | url += objectToURI(params.data); 95 | return url; 96 | }; 97 | 98 | randomString = function(length) { 99 | var str; 100 | str = ''; 101 | while (str.length < length) { 102 | str += random().toString(36).slice(2, 3); 103 | } 104 | return str; 105 | }; 106 | 107 | objectToURI = function(obj) { 108 | var data, key, value; 109 | data = (function() { 110 | var results; 111 | results = []; 112 | for (key in obj) { 113 | value = obj[key]; 114 | results.push(encode(key) + '=' + encode(value)); 115 | } 116 | return results; 117 | })(); 118 | return data.join('&'); 119 | }; 120 | 121 | if (typeof define !== "undefined" && define !== null ? define.amd : void 0) { 122 | define(function() { 123 | return JSONP; 124 | }); 125 | } else if (typeof module !== "undefined" && module !== null ? module.exports : void 0) { 126 | module.exports = JSONP; 127 | } else { 128 | this.JSONP = JSONP; 129 | } 130 | 131 | }).call(this); 132 | -------------------------------------------------------------------------------- /src/js/lib/lazy-load-img/index.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * version: 3.0.6 3 | * github:https://github.com/lzxb/lazy-load-img 4 | */ 5 | 6 | (function (global, factory) { 7 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 8 | typeof define === 'function' && define.amd ? define(factory) : 9 | (global.LazyLoadImg = factory()); 10 | }(this, (function () { 'use strict'; 11 | 12 | var testMeet = function (el) { 13 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 14 | 15 | // 取得元素在可视区的位置(相对浏览器视窗)左右上下 16 | var bcr = el.getBoundingClientRect 17 | // padding+border+width 18 | ();var mw = el.offsetWidth; // 元素自身宽度 19 | var mh = el.offsetHeight; // 元素自身的高度 20 | // 包含了导航栏 21 | var w = window.innerWidth; // 视窗的宽度 22 | var h = window.innerHeight; // 视窗的高度 23 | 24 | var boolX = !(bcr.right - options.left <= 0 && bcr.left + mw - options.left <= 0) && !(bcr.left + options.right >= w && bcr.right + options.right >= mw + w); // 左右符合条件 25 | var boolY = !(bcr.bottom - options.top <= 0 && bcr.top + mh - options.top <= 0) && !(bcr.top + options.bottom >= h && bcr.bottom + options.bottom >= mh + h); // 上下符合条件 26 | return el.width !== 0 && el.height !== 0 && boolX && boolY; 27 | }; 28 | 29 | var canvas = document.createElement('canvas'); 30 | canvas.getContext('2d').globalAlpha = 0.0; 31 | var images = {}; 32 | 33 | var getTransparent = function (src, w, h) { 34 | if (images[src]) return images[src]; 35 | canvas.width = w; 36 | canvas.height = h; 37 | var data = canvas.toDataURL('image/png'); 38 | images[src] = data; 39 | return data; 40 | }; 41 | 42 | var classCallCheck = function (instance, Constructor) { 43 | if (!(instance instanceof Constructor)) { 44 | throw new TypeError("Cannot call a class as a function"); 45 | } 46 | }; 47 | 48 | var createClass = function () { 49 | function defineProperties(target, props) { 50 | for (var i = 0; i < props.length; i++) { 51 | var descriptor = props[i]; 52 | descriptor.enumerable = descriptor.enumerable || false; 53 | descriptor.configurable = true; 54 | if ("value" in descriptor) descriptor.writable = true; 55 | Object.defineProperty(target, descriptor.key, descriptor); 56 | } 57 | } 58 | 59 | return function (Constructor, protoProps, staticProps) { 60 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 61 | if (staticProps) defineProperties(Constructor, staticProps); 62 | return Constructor; 63 | }; 64 | }(); 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | var _extends = Object.assign || function (target) { 73 | for (var i = 1; i < arguments.length; i++) { 74 | var source = arguments[i]; 75 | 76 | for (var key in source) { 77 | if (Object.prototype.hasOwnProperty.call(source, key)) { 78 | target[key] = source[key]; 79 | } 80 | } 81 | } 82 | 83 | return target; 84 | }; 85 | 86 | var _win = window; 87 | 88 | var LazyLoadImg = function () { 89 | // 构造函数 初始化参数 90 | function LazyLoadImg() { 91 | var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; 92 | classCallCheck(this, LazyLoadImg); 93 | 94 | this.options = { 95 | el: document.querySelector('body'), // 选择的元素 96 | mode: 'default', // 默认模式,将显示原图,diy模式,将自定义剪切,默认剪切居中部分 97 | time: 300, // 设置一个检测时间间隔 98 | done: true, // 页面内所有数据图片加载完成后,是否自己销毁程序,true默认销毁,false不销毁:FALSE应用场景:页面异步不断获取数据的情况下 需要实时监听则不销毁 99 | diy: { // 此属性,只有在设置diy 模式时才生效 100 | backgroundSize: 'cover', 101 | backgroundRepeat: 'no-repeat', 102 | backgroundPosition: 'center center' 103 | }, 104 | position: { // 只要其中一个位置符合条件,都会触发加载机制 105 | top: 0, // 元素距离顶部 106 | right: 0, // 元素距离右边 107 | bottom: 0, // 元素距离下面 108 | left: 0 // 元素距离左边 109 | }, 110 | before: function before(el) {// 图片加载之前,执行钩子函数 111 | 112 | }, 113 | success: function success(el) {// 图片加载成功,执行钩子函数 114 | 115 | }, 116 | error: function error(el) {// 图片加载失败,执行的钩子函数 117 | 118 | } 119 | }; 120 | // 深拷贝 如果都有 则右面的值 option.position会覆盖this.options.position 121 | options.position = _extends({}, this.options.position, options.position); 122 | options.diy = _extends({}, this.options.diy, options.diy); 123 | _extends(this.options, options); 124 | this.start(); 125 | } 126 | 127 | createClass(LazyLoadImg, [{ 128 | key: 'start', 129 | value: function start() { 130 | this._timer = true; 131 | this._start(); 132 | } 133 | }, { 134 | key: '_start', 135 | value: function _start() { 136 | var _this = this; 137 | 138 | var options = this.options; 139 | 140 | clearTimeout(this._timer // 清除定时器 141 | );if (!this._timer) return; 142 | this._timer = setTimeout(function () { 143 | var list = Array.prototype.slice.apply(options.el.querySelectorAll('[data-src]')); 144 | if (!list.length && options.done) return clearTimeout(_this._timer); 145 | list.forEach(function (el) { 146 | if (!el.dataset.LazyLoadImgState && testMeet(el, options.position)) { 147 | _this.loadImg(el); 148 | } 149 | }); 150 | _this._start(); 151 | }, options.time); 152 | } 153 | }, { 154 | key: 'loadImg', 155 | value: function loadImg(el) { 156 | var _this2 = this; 157 | 158 | // 加载图片 159 | var options = this.options; 160 | 161 | el.dataset.LazyLoadImgState = 'start'; 162 | options.before.call(this, el); 163 | var img = new _win.Image(); 164 | img.src = el.dataset.src; 165 | // 图片加载成功 166 | img.addEventListener('load', function () { 167 | if (options.mode === 'diy') { 168 | el.src = getTransparent(el.src, el.width, el.height); 169 | options.diy.backgroundImage = 'url(' + img.src + ')'; 170 | _extends(el.style, options.diy); 171 | } else { 172 | el.src = img.src; 173 | } 174 | delete el.dataset.src; 175 | delete el.dataset.LazyLoadImgState; 176 | return options.success.call(_this2, el); 177 | }, false 178 | 179 | // 图片加载失败 180 | );img.addEventListener('error', function () { 181 | delete el.dataset.src; 182 | delete el.dataset.LazyLoadImgState; 183 | options.error.call(_this2, el); 184 | }, false); 185 | } 186 | }, { 187 | key: 'destroy', 188 | value: function destroy() { 189 | // 解除事件绑定 190 | delete this._timer; 191 | } 192 | }]); 193 | return LazyLoadImg; 194 | }(); 195 | 196 | return LazyLoadImg; 197 | 198 | }))); 199 | 200 | -------------------------------------------------------------------------------- /src/js/lib/queue/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @component 队列 3 | * @author wilson 4 | */ 5 | 6 | (function () { 7 | 8 | var queue = {}; 9 | function noop() {} 10 | 11 | var previous_async; 12 | 13 | var root = typeof self === 'object' && self.self === self && self || 14 | typeof global === 'object' && global.global === global && global || 15 | this; 16 | 17 | if (root != null) { 18 | previous_async = root.queue; 19 | } 20 | 21 | function only_once(fn) { 22 | return function() { 23 | if (fn === null) throw new Error("Callback was already called."); 24 | fn.apply(this, arguments); 25 | fn = null; 26 | }; 27 | } 28 | 29 | var _toString = Object.prototype.toString; 30 | 31 | var _isArray = Array.isArray || function (obj) { 32 | return _toString.call(obj) === '[object Array]'; 33 | }; 34 | 35 | function _arrayEach(arr, iterator) { 36 | var index = -1, 37 | length = arr.length; 38 | 39 | while (++index < length) { 40 | iterator(arr[index], index, arr); 41 | } 42 | } 43 | 44 | function _map(arr, iterator) { 45 | var index = -1, 46 | length = arr.length, 47 | result = Array(length); 48 | 49 | while (++index < length) { 50 | result[index] = iterator(arr[index], index, arr); 51 | } 52 | return result; 53 | } 54 | 55 | var _setImmediate = typeof setImmediate === 'function' && setImmediate; 56 | 57 | var _delay = _setImmediate ? function(fn) { 58 | // not a direct alias for IE10 compatibility 59 | _setImmediate(fn); 60 | } : function(fn) { 61 | setTimeout(fn, 0); 62 | }; 63 | 64 | queue.setImmediate = _delay; 65 | 66 | function _queue(worker, concurrency, payload) { 67 | if (concurrency == null) { 68 | concurrency = 1; 69 | } 70 | else if(concurrency === 0) { 71 | throw new Error('Concurrency must not be zero'); 72 | } 73 | function _insert(q, data, pos, callback) { 74 | if (callback != null && typeof callback !== "function") { 75 | throw new Error("task callback must be a function"); 76 | } 77 | q.started = true; 78 | if (!_isArray(data)) { 79 | data = [data]; 80 | } 81 | if(data.length === 0 && q.idle()) { 82 | // call drain immediately if there are no tasks 83 | return queue.setImmediate(function() { 84 | q.drain(); 85 | }); 86 | } 87 | _arrayEach(data, function(task) { 88 | var item = { 89 | data: task, 90 | callback: callback || noop 91 | }; 92 | 93 | if (pos) { 94 | q.tasks.unshift(item); 95 | } else { 96 | q.tasks.push(item); 97 | } 98 | 99 | if (q.tasks.length === q.concurrency) { 100 | q.saturated(); 101 | } 102 | }); 103 | queue.setImmediate(q.process); 104 | } 105 | function _next(q, tasks) { 106 | return function(){ 107 | workers -= 1; 108 | 109 | var removed = false; 110 | var args = arguments; 111 | _arrayEach(tasks, function (task) { 112 | _arrayEach(workersList, function (worker, index) { 113 | if (worker === task && !removed) { 114 | workersList.splice(index, 1); 115 | removed = true; 116 | } 117 | }); 118 | 119 | task.callback.apply(task, args); 120 | }); 121 | if (q.tasks.length + workers === 0) { 122 | q.drain(); 123 | } 124 | q.process(); 125 | }; 126 | } 127 | 128 | var workers = 0; 129 | var workersList = []; 130 | var q = { 131 | tasks: [], 132 | concurrency: concurrency, 133 | payload: payload, 134 | saturated: noop, 135 | empty: noop, 136 | drain: noop, 137 | started: false, 138 | paused: false, 139 | push: function (data, callback) { 140 | _insert(q, data, false, callback); 141 | }, 142 | unshift: function (data, callback) { 143 | _insert(q, data, true, callback); 144 | }, 145 | process: function () { 146 | while(!q.paused && workers < q.concurrency && q.tasks.length){ 147 | 148 | var tasks = q.payload ? 149 | q.tasks.splice(0, q.payload) : 150 | q.tasks.splice(0, q.tasks.length); 151 | 152 | var data = _map(tasks, function (task) { 153 | return task.data; 154 | }); 155 | 156 | if (q.tasks.length === 0) { 157 | q.empty(); 158 | } 159 | workers += 1; 160 | workersList.push(tasks[0]); 161 | var cb = only_once(_next(q, tasks)); 162 | worker(data, cb); 163 | } 164 | }, 165 | length: function () { 166 | return q.tasks.length; 167 | }, 168 | running: function () { 169 | return workers; 170 | }, 171 | workersList: function () { 172 | return workersList; 173 | }, 174 | idle: function() { 175 | return q.tasks.length + workers === 0; 176 | }, 177 | pause: function () { 178 | q.paused = true; 179 | }, 180 | resume: function () { 181 | if (q.paused === false) { return; } 182 | q.paused = false; 183 | var resumeCount = Math.min(q.concurrency, q.tasks.length); 184 | // Need to call q.process once per concurrent 185 | // worker to preserve full concurrency after pause 186 | for (var w = 1; w <= resumeCount; w++) { 187 | queue.setImmediate(q.process); 188 | } 189 | } 190 | }; 191 | return q; 192 | } 193 | 194 | queue.queue = function (worker, concurrency) { 195 | var q = _queue(function (items, cb) { 196 | worker(items[0], cb); 197 | }, concurrency, 1); 198 | 199 | return q; 200 | }; 201 | 202 | module.exports = queue; 203 | 204 | }()); 205 | -------------------------------------------------------------------------------- /src/js/lib/scrollTo/index.js: -------------------------------------------------------------------------------- 1 | export default function scrollTo ( direction, config ) { 2 | let { elem, speed = 20, interval = 16, transition = true } = config; 3 | return new Promise(( resolve ) => { 4 | if (transition) { 5 | let scrolling = true; 6 | let now = elem.scrollTop; 7 | let next = () => { 8 | let destination; 9 | if (direction == 'top') { 10 | destination = 0; 11 | now -= speed; 12 | if (now < destination) { 13 | now = destination; 14 | scrolling = false; 15 | } 16 | } else { 17 | destination = elem.scrollHeight; 18 | now += speed; 19 | if (now > destination) { 20 | now = destination; 21 | scrolling = false; 22 | } 23 | } 24 | elem.scrollTop = now; 25 | if (scrolling) { 26 | setTimeout(next, interval); 27 | } else { 28 | resolve(); 29 | } 30 | }; 31 | next(); 32 | } else { 33 | if (direction == 'top') { 34 | elem.scrollTop = 0; 35 | } else { 36 | elem.scrollTop = elem.scrollHeight; 37 | } 38 | resolve(); 39 | } 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/js/lib/sprite/index.js: -------------------------------------------------------------------------------- 1 | // 从时序图尺寸 动态生成动画函数 2 | 3 | let record = {}; 4 | 5 | export default function ( elem, src, duration ) { 6 | return new Promise(( resolve ) => { 7 | let img = new Image; 8 | img.onload = onload; 9 | img.src = src; 10 | elem.dataset.status = 'pause'; 11 | function onload () { 12 | let frame; 13 | let direction; 14 | if (this.width > this.height) { 15 | direction = 'x'; 16 | frame = Math.round(this.width / this.height); 17 | // elem.style['background-size'] = `auto ${ getComputedStyle(elem).width }`; 18 | elem.style['background-size'] = `auto 100%`; 19 | } else { 20 | direction = 'y'; 21 | frame = Math.round(this.height / this.width); 22 | // elem.style['background-size'] = `${ getComputedStyle(elem).height } auto`; 23 | elem.style['background-size'] = `100% auto`; 24 | } 25 | elem.dataset.frame = frame; 26 | elem.dataset.direction = direction; 27 | elem.style['background-image'] = `url(${ src })`; 28 | let unique = direction + frame; 29 | if (!record[unique]) { 30 | record[unique] = true; 31 | let style = document.createElement('style'); 32 | style.innerHTML = ` 33 | [data-status="play"][data-frame="${ frame }"][data-direction="${ direction }"] { 34 | animation-duration: ${ duration * frame }ms; 35 | -webkit-animation-duration: ${ duration * frame }ms; 36 | animation-name: spirit-animation-${ unique }; 37 | -webkit-animation-name: spirit-animation-${ unique }; 38 | animation-timing-function: steps(${ frame }); 39 | -webkit-animation-timing-function: steps(${ frame }); 40 | animation-iteration-count: infinite; 41 | -webkit-animation-iteration-count: infinite; 42 | } 43 | @keyframes spirit-animation-${ unique } { 44 | 0% { 45 | background-position: 0 0; 46 | } 47 | 100% { 48 | background-position: ${ direction == 'x' ? `-${ frame }00% 0` : `0 -${ frame }00%` }; 49 | } 50 | } 51 | @-webkit-keyframes spirit-animation-${ unique } { 52 | 0% { 53 | background-position: 0 0; 54 | } 55 | 100% { 56 | background-position: ${ direction == 'x' ? `-${ frame }00% 0` : `0 -${ frame }00%` }; 57 | } 58 | } 59 | `; 60 | document.head.appendChild(style); 61 | } 62 | resolve(); 63 | } 64 | }); 65 | }; 66 | -------------------------------------------------------------------------------- /src/js/lib/svga/svga-db.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * @project : svga-web-canvas 3 | * @version : 0.0.1 4 | * @author : UED.lijialiang 5 | * @update : 2016-10-21 9:35:38 am 6 | * @email : lijialiang@yy.com 7 | */ !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.SvgaDb=t():e.SvgaDb=t()}(this,function(){return function(e){function t(a){if(n[a])return n[a].exports;var r=n[a]={exports:{},id:a,loaded:!1};return e[a].call(r.exports,r,r.exports,t),r.loaded=!0,r.exports}var n={};return t.m=e,t.c=n,t.p="",t(0)}([function(e,t){function n(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var a,r,o=function(){function e(e,t){for(var n=0;n0?n(JSON.parse(t.rows.item(0).images),JSON.parse(t.rows.item(0).movie),!1):n("","",!0)},a._errorCallBack)})}},{key:"clear",value:function(e){"string"==typeof e?this._clearByUrl(e):"number"==typeof e&&this._clearByDate(e)}},{key:"_clearByUrl",value:function(t){var n=this;e.db.transaction(function(a){a.executeSql("DELETE FROM "+e.table.name+" WHERE url LIKE ?",[t],function(e,t){console.info("[Svga Web Canvas DB]: clear success")},n._errorCallBack)})}},{key:"_clearByDate",value:function(t){var n=this,a=new Date;e.db.transaction(function(r){r.executeSql("SELECT * FROM "+e.table.name,[],function(e,r){for(var o=r.rows.length,i=0;i=t&&n._clearByUrl(c.url)}},n._errorCallBack)})}}]),e}(),a.table={name:"svga",attribute:"url, movie, images, time"},r)}])}); -------------------------------------------------------------------------------- /src/js/lib/swipe/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Swipe 2.0 3 | * 4 | * Brad Birdsall 5 | * Copyright 2013, MIT License 6 | * 7 | */ 8 | 9 | function Swipe(container, options) { 10 | 11 | "use strict"; 12 | 13 | // utilities 14 | var noop = function() {}; // simple no operation function 15 | var offloadFn = function(fn) { setTimeout(fn || noop, 0) }; // offload a functions execution 16 | 17 | // check browser capabilities 18 | var browser = { 19 | addEventListener: !!window.addEventListener, 20 | touch: ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch, 21 | transitions: (function(temp) { 22 | var props = ['transitionProperty', 'WebkitTransition', 'MozTransition', 'OTransition', 'msTransition']; 23 | for ( var i in props ) if (temp.style[ props[i] ] !== undefined) return true; 24 | return false; 25 | })(document.createElement('swipe')) 26 | }; 27 | 28 | // quit if no root element 29 | if (!container) return; 30 | var element = container.children[0]; 31 | var slides, slidePos, width, length; 32 | options = options || {}; 33 | var index = parseInt(options.startSlide, 10) || 0; 34 | var speed = options.speed || 300; 35 | options.continuous = options.continuous !== undefined ? options.continuous : true; 36 | 37 | function setup() { 38 | 39 | // cache slides 40 | slides = element.children; 41 | length = slides.length; 42 | 43 | // set continuous to false if only one slide 44 | if (slides.length < 2) options.continuous = false; 45 | 46 | //special case if two slides 47 | if (browser.transitions && options.continuous && slides.length < 3) { 48 | element.appendChild(slides[0].cloneNode(true)); 49 | element.appendChild(element.children[1].cloneNode(true)); 50 | slides = element.children; 51 | } 52 | 53 | // create an array to store current positions of each slide 54 | slidePos = new Array(slides.length); 55 | 56 | // determine width of each slide 57 | width = container.getBoundingClientRect().width || container.offsetWidth; 58 | 59 | element.style.width = (slides.length * width) + 'px'; 60 | 61 | // stack elements 62 | var pos = slides.length; 63 | while(pos--) { 64 | 65 | var slide = slides[pos]; 66 | 67 | slide.style.width = width + 'px'; 68 | slide.setAttribute('data-index', pos); 69 | 70 | if (browser.transitions) { 71 | slide.style.left = (pos * -width) + 'px'; 72 | move(pos, index > pos ? -width : (index < pos ? width : 0), 0); 73 | } 74 | 75 | } 76 | 77 | // reposition elements before and after index 78 | if (options.continuous && browser.transitions) { 79 | move(circle(index-1), -width, 0); 80 | move(circle(index+1), width, 0); 81 | } 82 | 83 | if (!browser.transitions) element.style.left = (index * -width) + 'px'; 84 | 85 | container.style.visibility = 'visible'; 86 | 87 | } 88 | 89 | function prev() { 90 | 91 | if (options.continuous) slide(index-1); 92 | else if (index) slide(index-1); 93 | 94 | } 95 | 96 | function next() { 97 | 98 | if (options.continuous) slide(index+1); 99 | else if (index < slides.length - 1) slide(index+1); 100 | 101 | } 102 | 103 | function circle(index) { 104 | 105 | // a simple positive modulo using slides.length 106 | return (slides.length + (index % slides.length)) % slides.length; 107 | 108 | } 109 | 110 | function slide(to, slideSpeed) { 111 | 112 | // do nothing if already on requested slide 113 | if (index == to) return; 114 | 115 | if (browser.transitions) { 116 | 117 | var direction = Math.abs(index-to) / (index-to); // 1: backward, -1: forward 118 | 119 | // get the actual position of the slide 120 | if (options.continuous) { 121 | var natural_direction = direction; 122 | direction = -slidePos[circle(to)] / width; 123 | 124 | // if going forward but to < index, use to = slides.length + to 125 | // if going backward but to > index, use to = -slides.length + to 126 | if (direction !== natural_direction) to = -direction * slides.length + to; 127 | 128 | } 129 | 130 | var diff = Math.abs(index-to) - 1; 131 | 132 | // move all the slides between index and to in the right direction 133 | while (diff--) move( circle((to > index ? to : index) - diff - 1), width * direction, 0); 134 | 135 | to = circle(to); 136 | 137 | move(index, width * direction, slideSpeed || speed); 138 | move(to, 0, slideSpeed || speed); 139 | 140 | if (options.continuous) move(circle(to - direction), -(width * direction), 0); // we need to get the next in place 141 | 142 | } else { 143 | 144 | to = circle(to); 145 | animate(index * -width, to * -width, slideSpeed || speed); 146 | //no fallback for a circular continuous if the browser does not accept transitions 147 | } 148 | 149 | index = to; 150 | offloadFn(options.callback && options.callback(index, slides[index])); 151 | } 152 | 153 | function move(index, dist, speed) { 154 | 155 | translate(index, dist, speed); 156 | slidePos[index] = dist; 157 | 158 | } 159 | 160 | function translate(index, dist, speed) { 161 | 162 | var slide = slides[index]; 163 | var style = slide && slide.style; 164 | 165 | if (!style) return; 166 | 167 | style.webkitTransitionDuration = 168 | style.MozTransitionDuration = 169 | style.msTransitionDuration = 170 | style.OTransitionDuration = 171 | style.transitionDuration = speed + 'ms'; 172 | 173 | style.webkitTransform = 'translate(' + dist + 'px,0)' + 'translateZ(0)'; 174 | style.msTransform = 175 | style.MozTransform = 176 | style.OTransform = 'translateX(' + dist + 'px)'; 177 | 178 | } 179 | 180 | function animate(from, to, speed) { 181 | 182 | // if not an animation, just reposition 183 | if (!speed) { 184 | 185 | element.style.left = to + 'px'; 186 | return; 187 | 188 | } 189 | 190 | var start = +new Date; 191 | 192 | var timer = setInterval(function() { 193 | 194 | var timeElap = +new Date - start; 195 | 196 | if (timeElap > speed) { 197 | 198 | element.style.left = to + 'px'; 199 | 200 | if (delay) begin(); 201 | 202 | options.transitionEnd && options.transitionEnd.call(event, index, slides[index]); 203 | 204 | clearInterval(timer); 205 | return; 206 | 207 | } 208 | 209 | element.style.left = (( (to - from) * (Math.floor((timeElap / speed) * 100) / 100) ) + from) + 'px'; 210 | 211 | }, 4); 212 | 213 | } 214 | 215 | // setup auto slideshow 216 | var delay = options.auto || 0; 217 | var interval; 218 | 219 | function begin() { 220 | 221 | interval = setTimeout(next, delay); 222 | 223 | } 224 | 225 | function stop() { 226 | 227 | delay = 0; 228 | clearTimeout(interval); 229 | 230 | } 231 | 232 | 233 | // setup initial vars 234 | var start = {}; 235 | var delta = {}; 236 | var isScrolling; 237 | 238 | // setup event capturing 239 | var events = { 240 | 241 | handleEvent: function(event) { 242 | 243 | switch (event.type) { 244 | case 'touchstart': this.start(event); break; 245 | case 'touchmove': this.move(event); break; 246 | case 'touchend': offloadFn(this.end(event)); break; 247 | case 'webkitTransitionEnd': 248 | case 'msTransitionEnd': 249 | case 'oTransitionEnd': 250 | case 'otransitionend': 251 | case 'transitionend': offloadFn(this.transitionEnd(event)); break; 252 | case 'resize': offloadFn(setup); break; 253 | } 254 | 255 | if (options.stopPropagation) event.stopPropagation(); 256 | 257 | }, 258 | start: function(event) { 259 | 260 | var touches = event.touches[0]; 261 | 262 | // measure start values 263 | start = { 264 | 265 | // get initial touch coords 266 | x: touches.pageX, 267 | y: touches.pageY, 268 | 269 | // store time to determine touch duration 270 | time: +new Date 271 | 272 | }; 273 | 274 | // used for testing first move event 275 | isScrolling = undefined; 276 | 277 | // reset delta and end measurements 278 | delta = {}; 279 | 280 | // attach touchmove and touchend listeners 281 | element.addEventListener('touchmove', this, false); 282 | element.addEventListener('touchend', this, false); 283 | 284 | }, 285 | move: function(event) { 286 | 287 | // ensure swiping with one touch and not pinching 288 | if ( event.touches.length > 1 || event.scale && event.scale !== 1) return 289 | 290 | if (options.disableScroll) event.preventDefault(); 291 | 292 | var touches = event.touches[0]; 293 | 294 | // measure change in x and y 295 | delta = { 296 | x: touches.pageX - start.x, 297 | y: touches.pageY - start.y 298 | } 299 | 300 | // determine if scrolling test has run - one time test 301 | if ( typeof isScrolling == 'undefined') { 302 | isScrolling = !!( isScrolling || Math.abs(delta.x) < Math.abs(delta.y) ); 303 | } 304 | 305 | // if user is not trying to scroll vertically 306 | if (!isScrolling) { 307 | 308 | // prevent native scrolling 309 | event.preventDefault(); 310 | 311 | // stop slideshow 312 | stop(); 313 | 314 | // increase resistance if first or last slide 315 | if (options.continuous) { // we don't add resistance at the end 316 | 317 | translate(circle(index-1), delta.x + slidePos[circle(index-1)], 0); 318 | translate(index, delta.x + slidePos[index], 0); 319 | translate(circle(index+1), delta.x + slidePos[circle(index+1)], 0); 320 | 321 | } else { 322 | 323 | delta.x = 324 | delta.x / 325 | ( (!index && delta.x > 0 // if first slide and sliding left 326 | || index == slides.length - 1 // or if last slide and sliding right 327 | && delta.x < 0 // and if sliding at all 328 | ) ? 329 | ( Math.abs(delta.x) / width + 1 ) // determine resistance level 330 | : 1 ); // no resistance if false 331 | 332 | // translate 1:1 333 | translate(index-1, delta.x + slidePos[index-1], 0); 334 | translate(index, delta.x + slidePos[index], 0); 335 | translate(index+1, delta.x + slidePos[index+1], 0); 336 | } 337 | 338 | } 339 | 340 | }, 341 | end: function(event) { 342 | 343 | // measure duration 344 | var duration = +new Date - start.time; 345 | 346 | // determine if slide attempt triggers next/prev slide 347 | var isValidSlide = 348 | Number(duration) < 250 // if slide duration is less than 250ms 349 | && Math.abs(delta.x) > 20 // and if slide amt is greater than 20px 350 | || Math.abs(delta.x) > width/2; // or if slide amt is greater than half the width 351 | 352 | // determine if slide attempt is past start and end 353 | var isPastBounds = 354 | !index && delta.x > 0 // if first slide and slide amt is greater than 0 355 | || index == slides.length - 1 && delta.x < 0; // or if last slide and slide amt is less than 0 356 | 357 | if (options.continuous) isPastBounds = false; 358 | 359 | // determine direction of swipe (true:right, false:left) 360 | var direction = delta.x < 0; 361 | 362 | // if not scrolling vertically 363 | if (!isScrolling) { 364 | 365 | if (isValidSlide && !isPastBounds) { 366 | 367 | if (direction) { 368 | 369 | if (options.continuous) { // we need to get the next in this direction in place 370 | 371 | move(circle(index-1), -width, 0); 372 | move(circle(index+2), width, 0); 373 | 374 | } else { 375 | move(index-1, -width, 0); 376 | } 377 | 378 | move(index, slidePos[index]-width, speed); 379 | move(circle(index+1), slidePos[circle(index+1)]-width, speed); 380 | index = circle(index+1); 381 | 382 | } else { 383 | if (options.continuous) { // we need to get the next in this direction in place 384 | 385 | move(circle(index+1), width, 0); 386 | move(circle(index-2), -width, 0); 387 | 388 | } else { 389 | move(index+1, width, 0); 390 | } 391 | 392 | move(index, slidePos[index]+width, speed); 393 | move(circle(index-1), slidePos[circle(index-1)]+width, speed); 394 | index = circle(index-1); 395 | 396 | } 397 | 398 | options.callback && options.callback(index, slides[index]); 399 | 400 | } else { 401 | 402 | if (options.continuous) { 403 | 404 | move(circle(index-1), -width, speed); 405 | move(index, 0, speed); 406 | move(circle(index+1), width, speed); 407 | 408 | } else { 409 | 410 | move(index-1, -width, speed); 411 | move(index, 0, speed); 412 | move(index+1, width, speed); 413 | } 414 | 415 | } 416 | 417 | } 418 | 419 | // kill touchmove and touchend event listeners until touchstart called again 420 | element.removeEventListener('touchmove', events, false) 421 | element.removeEventListener('touchend', events, false) 422 | 423 | }, 424 | transitionEnd: function(event) { 425 | 426 | if (parseInt(event.target.getAttribute('data-index'), 10) == index) { 427 | 428 | if (delay) begin(); 429 | 430 | options.transitionEnd && options.transitionEnd.call(event, index, slides[index]); 431 | 432 | } 433 | 434 | } 435 | 436 | } 437 | 438 | // trigger setup 439 | setup(); 440 | 441 | // start auto slideshow if applicable 442 | if (delay) begin(); 443 | 444 | 445 | // add event listeners 446 | if (browser.addEventListener) { 447 | 448 | // set touchstart event on element 449 | if (browser.touch) element.addEventListener('touchstart', events, false); 450 | 451 | if (browser.transitions) { 452 | element.addEventListener('webkitTransitionEnd', events, false); 453 | element.addEventListener('msTransitionEnd', events, false); 454 | element.addEventListener('oTransitionEnd', events, false); 455 | element.addEventListener('otransitionend', events, false); 456 | element.addEventListener('transitionend', events, false); 457 | } 458 | 459 | // set resize event on window 460 | window.addEventListener('resize', events, false); 461 | 462 | } else { 463 | 464 | window.onresize = function () { setup() }; // to play nice with old IE 465 | 466 | } 467 | 468 | // expose the Swipe API 469 | return { 470 | setup: function() { 471 | 472 | setup(); 473 | 474 | }, 475 | slide: function(to, speed) { 476 | 477 | // cancel slideshow 478 | stop(); 479 | 480 | slide(to, speed); 481 | 482 | }, 483 | prev: function() { 484 | 485 | // cancel slideshow 486 | stop(); 487 | 488 | prev(); 489 | 490 | }, 491 | next: function() { 492 | 493 | // cancel slideshow 494 | stop(); 495 | 496 | next(); 497 | 498 | }, 499 | stop: function() { 500 | 501 | // cancel slideshow 502 | stop(); 503 | 504 | }, 505 | getPos: function() { 506 | 507 | // return current index position 508 | return index; 509 | 510 | }, 511 | getNumSlides: function() { 512 | 513 | // return total number of slides 514 | return length; 515 | }, 516 | kill: function() { 517 | 518 | // cancel slideshow 519 | stop(); 520 | 521 | // reset element 522 | element.style.width = ''; 523 | element.style.left = ''; 524 | 525 | // reset slides 526 | var pos = slides.length; 527 | while(pos--) { 528 | 529 | var slide = slides[pos]; 530 | slide.style.width = ''; 531 | slide.style.left = ''; 532 | 533 | if (browser.transitions) translate(pos, 0, 0); 534 | 535 | } 536 | 537 | // removed event listeners 538 | if (browser.addEventListener) { 539 | 540 | // remove current event listeners 541 | element.removeEventListener('touchstart', events, false); 542 | element.removeEventListener('webkitTransitionEnd', events, false); 543 | element.removeEventListener('msTransitionEnd', events, false); 544 | element.removeEventListener('oTransitionEnd', events, false); 545 | element.removeEventListener('otransitionend', events, false); 546 | element.removeEventListener('transitionend', events, false); 547 | window.removeEventListener('resize', events, false); 548 | 549 | } 550 | else { 551 | 552 | window.onresize = null; 553 | 554 | } 555 | 556 | } 557 | } 558 | 559 | } 560 | 561 | 562 | // if ( window.jQuery || window.Zepto ) { 563 | // (function($) { 564 | // $.fn.Swipe = function(params) { 565 | // return this.each(function() { 566 | // $(this).data('Swipe', new Swipe($(this)[0], params)); 567 | // }); 568 | // } 569 | // })( window.jQuery || window.Zepto ) 570 | // } 571 | 572 | module.exports = Swipe; 573 | -------------------------------------------------------------------------------- /src/js/main.js: -------------------------------------------------------------------------------- 1 | import './public/index.js'; 2 | import './component/layout/index.js'; 3 | import player from './component/player/index.js'; 4 | 5 | import topBar from './component/top-bar/index.js'; 6 | import header from './component/header/index.js'; 7 | import recommendList from './component/recommend-list/index.js'; 8 | 9 | import danmaku from './component/danmaku/index.js'; 10 | import message from './component/message/index.js'; 11 | import giftLit from './component/gift/gift-lit/index.js'; 12 | import giftLarge from './component/gift/gift-large/index.js'; 13 | import like from './component/like/index.js'; 14 | 15 | // 直播间基础信息准备就绪 16 | RequqestApi.getLiveData().then((liveData)=>{ 17 | 18 | player.init(liveData).then(()=>{ 19 | 20 | // 弹幕初始化 21 | RequqestApi.getDanmaku().then(( socket ) => { 22 | danmaku.init(socket); 23 | }); 24 | 25 | // 公屏初始化 26 | RequqestApi.getMessage().then((socket) => { 27 | message.init(socket); 28 | }) 29 | 30 | // 请求礼物映射 31 | let giftMapReady = RequqestApi.getGiftMap(); 32 | 33 | // 小礼物初始化 34 | Promise.all([RequqestApi.getGiftLit(), giftMapReady]).then(([ giftSocket, giftMapObj ]) => { 35 | giftLit.init(giftSocket, giftMapObj); 36 | }); 37 | 38 | // 大礼物初始化 39 | Promise.all([RequqestApi.getGiftLarge(), giftMapReady]).then(([ giftSocket, giftMapObj ]) => { 40 | giftLarge.init(giftSocket, giftMapObj); 41 | }); 42 | 43 | // 点赞 44 | like.init(); 45 | 46 | 47 | player.listenLiveStatus((status)=>{ 48 | if (status) return; 49 | header.destroy(); 50 | danmaku.destroy(); 51 | message.destroy(); 52 | giftLit.destroy(); 53 | giftLarge.destroy(); 54 | like.destroy(); 55 | console.log('直播结束'); 56 | }); 57 | }); 58 | 59 | (typeof topBar !== "undefined") && topBar.init(); 60 | (typeof header !== "undefined") && header.init(liveData); 61 | (typeof recommendList !== "undefined") && recommendList.init(); 62 | }) 63 | -------------------------------------------------------------------------------- /src/js/public/api/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file API接口 3 | * 4 | */ 5 | 6 | let jsonpPromise = (url) => { 7 | return new Promise(( resolve, reject ) => { 8 | window.util.jsonp({ 9 | url: url, 10 | success: resolve, 11 | error: reject, 12 | }); 13 | }).catch((err) => { 14 | console.log(err); 15 | throw err; 16 | }); 17 | } 18 | 19 | let socketPromise = (key, socketName) => { 20 | let ioUrl = 'http://legox.org:5353'; 21 | return new Promise(( resolve ) => { 22 | let ioSocket = io.connect(ioUrl); 23 | ioSocket.emit(socketName,{ key: key, time: 8000 }); 24 | resolve({ 25 | ioSocket, 26 | socketName, 27 | }); 28 | }); 29 | }; 30 | 31 | window.RequqestApi = { 32 | 33 | // 获取直播间基础信息 34 | getLiveData(){ 35 | return jsonpPromise('https://legox.org/mock/e7507320-b270-11e7-ba75-c111f832c2e3'); 36 | }, 37 | 38 | // 获取视频推荐列表 39 | getRecommendList(){ 40 | return jsonpPromise('https://legox.org/mock/7d57faa0-b3c0-11e7-ba75-c111f832c2e3'); 41 | }, 42 | 43 | // 获取直播状态 44 | getLiveStatus(){ 45 | return jsonpPromise('https://legox.org/mock/a3e67a40-863c-11e7-9085-0ba4558c07dc'); 46 | // return jsonpPromise('https://legox.org/mock/fbec5f60-b3c9-11e7-ba75-c111f832c2e3'); 47 | }, 48 | 49 | // 获取登录用户信息 50 | getUserInfo(){ 51 | return jsonpPromise('http://uedfe.yypm.com/mock/api/344'); 52 | }, 53 | 54 | // 获取弹幕 55 | getDanmaku() { 56 | return socketPromise('64af4540-b4c3-11e7-ba75-c111f832c2e3', 'mock'); 57 | }, 58 | 59 | // 获取公屏 60 | getMessage() { 61 | return socketPromise('4a2691b0-b4c8-11e7-ba75-c111f832c2e3', 'mock'); 62 | }, 63 | 64 | // 获取礼物映射 65 | getGiftMap() { 66 | return jsonpPromise('https://legox.org/mock/10e15760-b53f-11e7-ba75-c111f832c2e3'); 67 | }, 68 | 69 | // 获取小礼物 70 | getGiftLit() { 71 | return socketPromise('52741350-b541-11e7-ba75-c111f832c2e3', 'mock'); 72 | }, 73 | 74 | // 获取大礼物 75 | getGiftLarge() { 76 | return socketPromise('a11dce30-b56c-11e7-ba75-c111f832c2e3', 'mock'); 77 | }, 78 | } 79 | -------------------------------------------------------------------------------- /src/js/public/config/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @description 全局配置 3 | * @author wuweishuan 4 | */ 5 | 6 | window.CONFIG = { 7 | 8 | type: 'mobile', //终端类型 mobile||pc 9 | 10 | wrapSelector: 'body', //最外层的选择器 11 | 12 | // 组件引入 13 | components: { 14 | 'message' : true, //公屏消息 15 | 'gift-danmaku' : true, //弹幕礼物 16 | 'gift-big' : true, //大礼物 17 | 'gift-little' : true, //下礼物 18 | 'like' : true, //点赞 19 | 'top-bar' : true, //顶部条 20 | 'download-tips' : true, //下载提示 21 | 'recommend-list' : true, //视频推荐列表 22 | }, 23 | 24 | // 功能启用 25 | function: { 26 | 'send-message' : false, //发送公屏消息 27 | 'send-gift' : false, //发送礼物 28 | 'use-svga' : true, //大礼物动画使用SVGA 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /src/js/public/index.js: -------------------------------------------------------------------------------- 1 | import '../lib/es6-promise/index.js'; 2 | import '../lib/classlist-polyfill/index.js'; 3 | import io from '../lib/socket.io/index.js'; 4 | import fastclick from '../lib/fastclick/index.js'; 5 | import queue from '../lib/queue/index.js'; 6 | import delegate from '../lib/delegate/index.js'; 7 | import './util/index.js'; 8 | import './config/index.js'; 9 | import './api/index.js'; 10 | 11 | document.addEventListener("DOMContentLoaded", function(event) { 12 | fastclick.attach(document.body); 13 | }); 14 | window.io = window.io ? window.io : io; 15 | window.queue = window.queue ? window.queue : queue; 16 | window.delegate = window.delegate ? window.delegate : delegate; 17 | -------------------------------------------------------------------------------- /src/js/public/util/delegate-event.js: -------------------------------------------------------------------------------- 1 | if (!Element.prototype.matches) { 2 | Element.prototype.matches = 3 | Element.prototype.matchesSelector || 4 | Element.prototype.webkitMatchesSelector || 5 | function(s) { 6 | var matches = (this.document || this.ownerDocument).querySelectorAll(s), 7 | i = matches.length; 8 | while (--i >= 0 && matches.item(i) !== this) {} 9 | return i > -1; 10 | }; 11 | } 12 | function delegate ( selector, handler, counter ) { 13 | if (counter) { 14 | let callback = function ( event ) { 15 | for (let target = event.target; target && target != this; target = target.parentNode) { 16 | if (target.matches(selector)) { 17 | counter--; 18 | if (!counter) { 19 | this.removeEventListener(event.type, callback, false); 20 | } 21 | return handler.apply(target, arguments); 22 | } 23 | } 24 | }; 25 | return callback; 26 | } 27 | return function ( event ) { 28 | for (let target = event.target; target && target != this; target = target.parentNode) { 29 | if (target.matches(selector)) { 30 | return handler.apply(target, arguments); 31 | } 32 | } 33 | }; 34 | }; 35 | 36 | export default delegate; 37 | -------------------------------------------------------------------------------- /src/js/public/util/detect-android.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 检测当前环境是否为Android 3 | * 4 | * @return {boolean} true|false 5 | */ 6 | 7 | 8 | function isAndroid() { 9 | let userAgent = window.navigator.userAgent; 10 | return /Android/i.test(userAgent); 11 | } 12 | 13 | export default isAndroid 14 | 15 | -------------------------------------------------------------------------------- /src/js/public/util/get-url-param.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取url中指定参数的值 3 | * @param {string} name 需要获取的参数名 4 | * @param {string} url 需要被处理的url,默认为当前url 5 | * @return {string} 对应的参数值 6 | */ 7 | 8 | function getUrlParam(name, url) { 9 | var paramReg = new RegExp("[\\?&#]" + name + "=([^&#]+)","gi"); 10 | var paramMatch = decodeURIComponent(url || location.href).match(paramReg); 11 | var paramResult = []; 12 | 13 | if (paramMatch && paramMatch.length > 0) { 14 | paramResult = (paramMatch[paramMatch.length-1]).split("="); 15 | if (paramResult && paramResult.length > 1) { 16 | return paramResult[1]; 17 | } 18 | return '' 19 | } 20 | return ''; 21 | } 22 | 23 | export default getUrlParam; 24 | -------------------------------------------------------------------------------- /src/js/public/util/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file util 3 | * 4 | */ 5 | 6 | import jsonp from '../../lib/jsonp/index.js'; 7 | import delegate from './delegate-event.js'; 8 | import sleep from './sleep.js'; 9 | import scrollTo from './scroll-to.js'; 10 | import sprite from './sprite.js'; 11 | import getUrlParam from './get-url-param.js'; 12 | import isAndroid from './detect-android.js'; 13 | import preloadScript from './preload-script.js'; 14 | 15 | window.util = { 16 | jsonp, 17 | delegate, 18 | sleep, 19 | scrollTo, 20 | sprite, 21 | getUrlParam, 22 | isAndroid, 23 | preloadScript, 24 | } 25 | -------------------------------------------------------------------------------- /src/js/public/util/preload-script.js: -------------------------------------------------------------------------------- 1 | function preloadScript(url) { 2 | new Promise(( resolve ) => { 3 | let script = document.createElement('script'); 4 | script.src = url; 5 | script.onload = () => { 6 | document.head.removeChild(script); 7 | resolve(); 8 | }; 9 | document.head.appendChild(script); 10 | }); 11 | } 12 | 13 | export default preloadScript; 14 | -------------------------------------------------------------------------------- /src/js/public/util/scroll-to.js: -------------------------------------------------------------------------------- 1 | function scrollTo ( direction, config ) { 2 | let { elem, speed = 20, interval = 16, transition = true } = config; 3 | return new Promise(( resolve ) => { 4 | if (transition) { 5 | let scrolling = true; 6 | let now = elem.scrollTop; 7 | let next = () => { 8 | let destination; 9 | if (direction == 'top') { 10 | destination = 0; 11 | now -= speed; 12 | if (now < destination) { 13 | now = destination; 14 | scrolling = false; 15 | } 16 | } else { 17 | destination = elem.scrollHeight; 18 | now += speed; 19 | if (now > destination) { 20 | now = destination; 21 | scrolling = false; 22 | } 23 | } 24 | elem.scrollTop = now; 25 | if (scrolling) { 26 | setTimeout(next, interval); 27 | } else { 28 | resolve(); 29 | } 30 | }; 31 | next(); 32 | } else { 33 | if (direction == 'top') { 34 | elem.scrollTop = 0; 35 | } else { 36 | elem.scrollTop = elem.scrollHeight; 37 | } 38 | resolve(); 39 | } 40 | }); 41 | } 42 | 43 | export default scrollTo; 44 | -------------------------------------------------------------------------------- /src/js/public/util/sleep.js: -------------------------------------------------------------------------------- 1 | function sleep(delay) { 2 | return new Promise(( resolve ) => { 3 | setTimeout(resolve, delay); 4 | }); 5 | } 6 | 7 | export default sleep; 8 | -------------------------------------------------------------------------------- /src/js/public/util/sprite.js: -------------------------------------------------------------------------------- 1 | // 从时序图尺寸 动态生成动画函数 2 | 3 | let record = {}; 4 | function sprite( elem, src, duration ) { 5 | return new Promise(( resolve ) => { 6 | let img = new Image; 7 | img.onload = onload; 8 | img.src = src; 9 | elem.dataset.status = 'pause'; 10 | function onload () { 11 | let frame; 12 | let direction; 13 | if (this.width > this.height) { 14 | direction = 'x'; 15 | frame = Math.round(this.width / this.height); 16 | // elem.style['background-size'] = `auto ${ getComputedStyle(elem).width }`; 17 | elem.style['background-size'] = `auto 100%`; 18 | } else { 19 | direction = 'y'; 20 | frame = Math.round(this.height / this.width); 21 | // elem.style['background-size'] = `${ getComputedStyle(elem).height } auto`; 22 | elem.style['background-size'] = `100% auto`; 23 | } 24 | elem.dataset.frame = frame; 25 | elem.dataset.direction = direction; 26 | elem.style['background-image'] = `url(${ src })`; 27 | let unique = direction + frame; 28 | if (!record[unique]) { 29 | record[unique] = true; 30 | let style = document.createElement('style'); 31 | style.innerHTML = ` 32 | [data-status="play"][data-frame="${ frame }"][data-direction="${ direction }"] { 33 | animation-duration: ${ duration * frame }ms; 34 | -webkit-animation-duration: ${ duration * frame }ms; 35 | animation-name: spirit-animation-${ unique }; 36 | -webkit-animation-name: spirit-animation-${ unique }; 37 | animation-timing-function: steps(${ frame }); 38 | -webkit-animation-timing-function: steps(${ frame }); 39 | animation-iteration-count: infinite; 40 | -webkit-animation-iteration-count: infinite; 41 | } 42 | @keyframes spirit-animation-${ unique } { 43 | 0% { 44 | background-position: 0 0; 45 | } 46 | 100% { 47 | background-position: ${ direction == 'x' ? `-${ frame }00% 0` : `0 -${ frame }00%` }; 48 | } 49 | } 50 | @-webkit-keyframes spirit-animation-${ unique } { 51 | 0% { 52 | background-position: 0 0; 53 | } 54 | 100% { 55 | background-position: ${ direction == 'x' ? `-${ frame }00% 0` : `0 -${ frame }00%` }; 56 | } 57 | } 58 | `; 59 | document.head.appendChild(style); 60 | } 61 | resolve(); 62 | } 63 | }); 64 | } 65 | export default sprite; 66 | -------------------------------------------------------------------------------- /src/sass/_img.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yyued/H5Live/20d407f361d56190ca82496c22f6e159af8c7d29/src/sass/_img.scss -------------------------------------------------------------------------------- /src/sass/base/_base.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * 基础模块 & 全局变量配置 3 | * 4 | */ 5 | 6 | // 文本配置 7 | $b-font-size : 14px !default; 8 | $b-font-family : "San Francisco", "Helvetica Neue", Helvetica, sans-serif !default; 9 | $b-font-weight : 300 !default; 10 | $b-line-height : 1.5 !default; 11 | $b-link-decoration : underline !default; 12 | 13 | // 颜色配置 14 | $c-text : #333 !default; 15 | $c-background : #fff !default; 16 | $c-link : #333 #fa0 !default; 17 | 18 | // 基础模块 19 | @import "normalize"; 20 | @import "reset"; 21 | @import "flexible"; 22 | // @import "orientation"; 23 | @import "tool"; 24 | 25 | // todo: 横屏提示在安卓弹出键盘时被触发 -------------------------------------------------------------------------------- /src/sass/base/_flexible.scss: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | 纯 CSS 自适应方案 4 | 5 | 1,设计稿尺寸 6 | 7 | 默认设计稿宽带 750px,可通过变量 $psd-size 进行修改 8 | 9 | 2,对应的 meta 标签写法: 10 | 11 | 12 | 13 | 3,px to rem 需要用到小学算术 14 | 15 | 100px = 1rem / 1px = 0.01rem 16 | 17 | Notes: 18 | 19 | 断点参考: 20 | http://screensiz.es/phone 21 | http://mydevice.io/devices/#sortSmartphones 22 | 23 | 横屏断点:384px, 480px, 533px, 568px, 640px, 667px, 699px, 736px 24 | 某些手机厂商系统的虚拟 SmartBar 会改变 Screen 的值,又是坑,建议统一横屏提示规避 25 | 26 | 避免使用 min-device-width 27 | https://developers.google.com/web/fundamentals/design-and-ui/responsive/fundamentals/use-media-queries?hl=zh-cn#min-device-width- 28 | 29 | Changelog: 30 | 31 | 2016.05.04:添加 393px 699px 断点,小米note 1080 屏幕,DPR=2.75 32 | 2016.11.11: 参考 http://mydevice.io/devices/#sortSmartphones 添加部分安卓手机宽度 33 | 34 | */ 35 | 36 | // 定义基准数 37 | $base-fontSize: 100px !default; 38 | 39 | // 定义设计稿尺寸 40 | $psd-size: 750px !default; 41 | 42 | // 定义竖屏断点 43 | $responsives: 320px, 346px, 360px, 375px, 384px, 390px, 393px, 400px, 412px, 414px, 432px; 44 | 45 | // 定义根元素font-size 46 | @mixin rem($values){ 47 | font-size: $values * $base-fontSize / $psd-size; 48 | } 49 | 50 | // 遍历输出断点 51 | @each $responsive in $responsives{ 52 | @media only screen and (min-width: #{$responsive}) { 53 | html{ @include rem($responsive);} 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/sass/base/_normalize.scss: -------------------------------------------------------------------------------- 1 | /* normalize.css v5.0.0 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /** 4 | * 1. Change the default font family in all browsers (opinionated). 5 | * 2. Correct the line height in all browsers. 6 | * 3. Prevent adjustments of font size after orientation changes in 7 | * IE on Windows Phone and in iOS. 8 | */ 9 | 10 | /* Document 11 | ========================================================================== */ 12 | 13 | html { 14 | font-family: sans-serif; /* 1 */ 15 | line-height: 1.15; /* 2 */ 16 | -ms-text-size-adjust: 100%; /* 3 */ 17 | -webkit-text-size-adjust: 100%; /* 3 */ 18 | } 19 | 20 | /* Sections 21 | ========================================================================== */ 22 | 23 | /** 24 | * Remove the margin in all browsers (opinionated). 25 | */ 26 | 27 | body { 28 | margin: 0; 29 | } 30 | 31 | /** 32 | * Add the correct display in IE 9-. 33 | */ 34 | 35 | article, 36 | aside, 37 | footer, 38 | header, 39 | nav, 40 | section { 41 | display: block; 42 | } 43 | 44 | /** 45 | * Correct the font size and margin on `h1` elements within `section` and 46 | * `article` contexts in Chrome, Firefox, and Safari. 47 | */ 48 | 49 | h1 { 50 | font-size: 2em; 51 | margin: 0.67em 0; 52 | } 53 | 54 | /* Grouping content 55 | ========================================================================== */ 56 | 57 | /** 58 | * Add the correct display in IE 9-. 59 | * 1. Add the correct display in IE. 60 | */ 61 | 62 | figcaption, 63 | figure, 64 | main { /* 1 */ 65 | display: block; 66 | } 67 | 68 | /** 69 | * Add the correct margin in IE 8. 70 | */ 71 | 72 | figure { 73 | margin: 1em 40px; 74 | } 75 | 76 | /** 77 | * 1. Add the correct box sizing in Firefox. 78 | * 2. Show the overflow in Edge and IE. 79 | */ 80 | 81 | hr { 82 | box-sizing: content-box; /* 1 */ 83 | height: 0; /* 1 */ 84 | overflow: visible; /* 2 */ 85 | } 86 | 87 | /** 88 | * 1. Correct the inheritance and scaling of font size in all browsers. 89 | * 2. Correct the odd `em` font sizing in all browsers. 90 | */ 91 | 92 | pre { 93 | font-family: monospace, monospace; /* 1 */ 94 | font-size: 1em; /* 2 */ 95 | } 96 | 97 | /* Text-level semantics 98 | ========================================================================== */ 99 | 100 | /** 101 | * 1. Remove the gray background on active links in IE 10. 102 | * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. 103 | */ 104 | 105 | a { 106 | background-color: transparent; /* 1 */ 107 | -webkit-text-decoration-skip: objects; /* 2 */ 108 | } 109 | 110 | /** 111 | * Remove the outline on focused links when they are also active or hovered 112 | * in all browsers (opinionated). 113 | */ 114 | 115 | a:active, 116 | a:hover { 117 | outline-width: 0; 118 | } 119 | 120 | /** 121 | * 1. Remove the bottom border in Firefox 39-. 122 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 123 | */ 124 | 125 | abbr[title] { 126 | border-bottom: none; /* 1 */ 127 | text-decoration: underline; /* 2 */ 128 | text-decoration: underline dotted; /* 2 */ 129 | } 130 | 131 | /** 132 | * Prevent the duplicate application of `bolder` by the next rule in Safari 6. 133 | */ 134 | 135 | b, 136 | strong { 137 | font-weight: inherit; 138 | } 139 | 140 | /** 141 | * Add the correct font weight in Chrome, Edge, and Safari. 142 | */ 143 | 144 | b, 145 | strong { 146 | font-weight: bolder; 147 | } 148 | 149 | /** 150 | * 1. Correct the inheritance and scaling of font size in all browsers. 151 | * 2. Correct the odd `em` font sizing in all browsers. 152 | */ 153 | 154 | code, 155 | kbd, 156 | samp { 157 | font-family: monospace, monospace; /* 1 */ 158 | font-size: 1em; /* 2 */ 159 | } 160 | 161 | /** 162 | * Add the correct font style in Android 4.3-. 163 | */ 164 | 165 | dfn { 166 | font-style: italic; 167 | } 168 | 169 | /** 170 | * Add the correct background and color in IE 9-. 171 | */ 172 | 173 | mark { 174 | background-color: #ff0; 175 | color: #000; 176 | } 177 | 178 | /** 179 | * Add the correct font size in all browsers. 180 | */ 181 | 182 | small { 183 | font-size: 80%; 184 | } 185 | 186 | /** 187 | * Prevent `sub` and `sup` elements from affecting the line height in 188 | * all browsers. 189 | */ 190 | 191 | sub, 192 | sup { 193 | font-size: 75%; 194 | line-height: 0; 195 | position: relative; 196 | vertical-align: baseline; 197 | } 198 | 199 | sub { 200 | bottom: -0.25em; 201 | } 202 | 203 | sup { 204 | top: -0.5em; 205 | } 206 | 207 | /* Embedded content 208 | ========================================================================== */ 209 | 210 | /** 211 | * Add the correct display in IE 9-. 212 | */ 213 | 214 | audio, 215 | video { 216 | display: inline-block; 217 | } 218 | 219 | /** 220 | * Add the correct display in iOS 4-7. 221 | */ 222 | 223 | audio:not([controls]) { 224 | display: none; 225 | height: 0; 226 | } 227 | 228 | /** 229 | * Remove the border on images inside links in IE 10-. 230 | */ 231 | 232 | img { 233 | border-style: none; 234 | } 235 | 236 | /** 237 | * Hide the overflow in IE. 238 | */ 239 | 240 | svg:not(:root) { 241 | overflow: hidden; 242 | } 243 | 244 | /* Forms 245 | ========================================================================== */ 246 | 247 | /** 248 | * 1. Change the font styles in all browsers (opinionated). 249 | * 2. Remove the margin in Firefox and Safari. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | font-family: sans-serif; /* 1 */ 258 | font-size: 100%; /* 1 */ 259 | line-height: 1.15; /* 1 */ 260 | margin: 0; /* 2 */ 261 | } 262 | 263 | /** 264 | * Show the overflow in IE. 265 | * 1. Show the overflow in Edge. 266 | */ 267 | 268 | button, 269 | input { /* 1 */ 270 | overflow: visible; 271 | } 272 | 273 | /** 274 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 275 | * 1. Remove the inheritance of text transform in Firefox. 276 | */ 277 | 278 | button, 279 | select { /* 1 */ 280 | text-transform: none; 281 | } 282 | 283 | /** 284 | * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` 285 | * controls in Android 4. 286 | * 2. Correct the inability to style clickable types in iOS and Safari. 287 | */ 288 | 289 | button, 290 | html [type="button"], /* 1 */ 291 | [type="reset"], 292 | [type="submit"] { 293 | -webkit-appearance: button; /* 2 */ 294 | } 295 | 296 | /** 297 | * Remove the inner border and padding in Firefox. 298 | */ 299 | 300 | button::-moz-focus-inner, 301 | [type="button"]::-moz-focus-inner, 302 | [type="reset"]::-moz-focus-inner, 303 | [type="submit"]::-moz-focus-inner { 304 | border-style: none; 305 | padding: 0; 306 | } 307 | 308 | /** 309 | * Restore the focus styles unset by the previous rule. 310 | */ 311 | 312 | button:-moz-focusring, 313 | [type="button"]:-moz-focusring, 314 | [type="reset"]:-moz-focusring, 315 | [type="submit"]:-moz-focusring { 316 | outline: 1px dotted ButtonText; 317 | } 318 | 319 | /** 320 | * Change the border, margin, and padding in all browsers (opinionated). 321 | */ 322 | 323 | fieldset { 324 | border: 1px solid #c0c0c0; 325 | margin: 0 2px; 326 | padding: 0.35em 0.625em 0.75em; 327 | } 328 | 329 | /** 330 | * 1. Correct the text wrapping in Edge and IE. 331 | * 2. Correct the color inheritance from `fieldset` elements in IE. 332 | * 3. Remove the padding so developers are not caught out when they zero out 333 | * `fieldset` elements in all browsers. 334 | */ 335 | 336 | legend { 337 | box-sizing: border-box; /* 1 */ 338 | color: inherit; /* 2 */ 339 | display: table; /* 1 */ 340 | max-width: 100%; /* 1 */ 341 | padding: 0; /* 3 */ 342 | white-space: normal; /* 1 */ 343 | } 344 | 345 | /** 346 | * 1. Add the correct display in IE 9-. 347 | * 2. Add the correct vertical alignment in Chrome, Firefox, and Opera. 348 | */ 349 | 350 | progress { 351 | display: inline-block; /* 1 */ 352 | vertical-align: baseline; /* 2 */ 353 | } 354 | 355 | /** 356 | * Remove the default vertical scrollbar in IE. 357 | */ 358 | 359 | textarea { 360 | overflow: auto; 361 | } 362 | 363 | /** 364 | * 1. Add the correct box sizing in IE 10-. 365 | * 2. Remove the padding in IE 10-. 366 | */ 367 | 368 | [type="checkbox"], 369 | [type="radio"] { 370 | box-sizing: border-box; /* 1 */ 371 | padding: 0; /* 2 */ 372 | } 373 | 374 | /** 375 | * Correct the cursor style of increment and decrement buttons in Chrome. 376 | */ 377 | 378 | [type="number"]::-webkit-inner-spin-button, 379 | [type="number"]::-webkit-outer-spin-button { 380 | height: auto; 381 | } 382 | 383 | /** 384 | * 1. Correct the odd appearance in Chrome and Safari. 385 | * 2. Correct the outline style in Safari. 386 | */ 387 | 388 | [type="search"] { 389 | -webkit-appearance: textfield; /* 1 */ 390 | outline-offset: -2px; /* 2 */ 391 | } 392 | 393 | /** 394 | * Remove the inner padding and cancel buttons in Chrome and Safari on macOS. 395 | */ 396 | 397 | [type="search"]::-webkit-search-cancel-button, 398 | [type="search"]::-webkit-search-decoration { 399 | -webkit-appearance: none; 400 | } 401 | 402 | /** 403 | * 1. Correct the inability to style clickable types in iOS and Safari. 404 | * 2. Change font properties to `inherit` in Safari. 405 | */ 406 | 407 | ::-webkit-file-upload-button { 408 | -webkit-appearance: button; /* 1 */ 409 | font: inherit; /* 2 */ 410 | } 411 | 412 | /* Interactive 413 | ========================================================================== */ 414 | 415 | /* 416 | * Add the correct display in IE 9-. 417 | * 1. Add the correct display in Edge, IE, and Firefox. 418 | */ 419 | 420 | details, /* 1 */ 421 | menu { 422 | display: block; 423 | } 424 | 425 | /* 426 | * Add the correct display in all browsers. 427 | */ 428 | 429 | summary { 430 | display: list-item; 431 | } 432 | 433 | /* Scripting 434 | ========================================================================== */ 435 | 436 | /** 437 | * Add the correct display in IE 9-. 438 | */ 439 | 440 | canvas { 441 | display: inline-block; 442 | } 443 | 444 | /** 445 | * Add the correct display in IE. 446 | */ 447 | 448 | template { 449 | display: none; 450 | } 451 | 452 | /* Hidden 453 | ========================================================================== */ 454 | 455 | /** 456 | * Add the correct display in IE 10-. 457 | */ 458 | 459 | [hidden] { 460 | display: none; 461 | } -------------------------------------------------------------------------------- /src/sass/base/_orientation.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | 旋转提示 tips 方案 3 | */ 4 | 5 | body{ 6 | &:after{ 7 | position: fixed; 8 | top: 0; 9 | left: 0; 10 | width: 1px; 11 | height: 1px; 12 | overflow: hidden; 13 | content: ""; 14 | background-color: rgba(0,0,0,.8); 15 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAuIAAAAlCAYAAAAHkYwaAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA3hpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNS1jMDIxIDc5LjE1NTc3MiwgMjAxNC8wMS8xMy0xOTo0NDowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD0ieG1wLmRpZDphOTg5NjIyMC01YzQxLTQwZmItOGE4Ni03MGFlZjk0Zjg4YmMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6QTVCOUNFRkFGMDE3MTFFNUEyNTc4QUU5QTBBRThGREMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6QTVCOUNFRjlGMDE3MTFFNUEyNTc4QUU5QTBBRThGREMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTQgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo0NGM1ZWU0Ny1jNzYzLTQ1ZDAtYjAwZi1hZGJkNDZlMWMyZjQiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6YTk4OTYyMjAtNWM0MS00MGZiLThhODYtNzBhZWY5NGY4OGJjIi8+IDwvcmRmOkRlc2NyaXB0aW9uPiA8L3JkZjpSREY+IDwveDp4bXBtZXRhPiA8P3hwYWNrZXQgZW5kPSJyIj8+sZvI1QAAELlJREFUeNrsnc1xq8wShttf3QS4CbiKEwIOAS0cgE4IaOW1tLxLtPYKhSAC8EKEYEIwVU7gIwRd4ZouWmOG+WH4kdRvFWUfHyHBTKN5uqen5+l8PgOL9aj6/ngLLz+ao74cJbfIpGrafX05DqL9TZSIc3Jx3q0quByR+L1Y8DUm4nebPmL1K74cp8tRXY4/d36foXhWO23n+fW9+Q5Oxf/nl39XbB6sR9M/3ASsB9daDIopN8UsbZ+K9reBw1j8vGUl4r5P4n6WqKPoH9s+YvULHbDqAZ7vzOC7ddu8hiGc9aj6DzcBi6UEpaGwt/d8TXNA226E+5CBJL8TG2gi2yazKoGAjxraqPPSouKZsLWS9FXzt83E17EUB2Dl8b0CYi864YzdEFUzQf9ad59iRvIRnBIWi0Gc9bgSU58NSDRTn6bpDGsP0Lvn1u8FjPXCQdzWBkxhDQF/J94fP2cpMJ6Ja2wchb/ib5/QpqlMCePxHdo+OqC1oQ0Ona0b05nuu+5AAHZu0BacFshiEGex7liRA+jk4vUYvWwGFBOIx9ePqSkG1rGj7zRattRoWG5oMwmYRy1pNBzzrmPxtyWAOIXwFemblbCJqWF85cGOfbzPGCB+C1Fg3ffASmG3saGTHTKIsxjEWSxWlw4SNOKiI93gmXWcz+qGV5OBeik2YHIvJiCdQRsNRxjHiHgyo90013QU14EQTuGolGA8Ev8eewFnsbD3GaoQ7FJT9o5O90/eteifOZ4x09mumEGc9ejixZosljmQ6SLdEYEyTkvpH6TDGSFhDLgyEaahyLMraCspzLMINYA28tkF4TKM18LWT3D7i2an1hSpGBG06Swb8BN5b/r9iRw7zfONaSmlSXs8v74XbBqsRxVHxFksMxDfQhuxVA0uGQErXnykFnVWMDUjsoBZ+tNENQyLNGM+e9f7IITrQAIjzgBtNBzIuXthY0eYNo2CAnUfhMswfhLnfhmcw/ptL2O1FzpV+HzN4ejGUgCjz/Z+7O77460vyFFbrO1hsRjEWaw7VC3gKRNH15R8JgaWCjgarhuk5fxRGsFzeR+dioEgHovrO/SAlU6YkpIr4GgvHBTMF5/Chrak3Rsw3BgCIsL4Udz/J0yzbgFryLvItvqKjzr1aYeDGRGbOnn+7IA4VQX0R63HlCotBasDVRKwB5rnvwBO9WMxiLNYDy+ay5vC9WI1jJZjlYmx82ZTuN2651vSnhVxdAoNYETk9aH0e6lp86HRR0y/qHpAvNTcM6akbHqcvb/Q1rQfGsXX3U8G1wtmbe22ud8XaCPjCJ2bEe0/APcFxLbn+UiViHo+t68soetnYzCghLbazdRKiMNZSW2RwfUmRmvihNaKNkp46GExiLNYLFQDGXIZt5TApWlE8VFFo9h7yck5aAb3TJyD0bMDcUp05/sAKhWIBwSkVddO83VrDfztxOszGCeHHjdZCUg/uEZOawHjKXE2EMbHzPktLa7ZtmrK2iP8yVV3KFjuNM+HC4Rj+tQUi2j72g86nsdE+nuIzvXz63tnX35/vMUM4iwGcRaLRVXBdeUI3MIZIWuqfMw56gL70FZqS1sQLuA6NSEXEBiPDOJ9sB32/B86EGABp3sCbAjjPqA2FG21Jtfry2Z3oj9T8TknaKt9jAGELm1SWNqaD6lgtFA8v6GjbaZwXXJyLggPxbNYdbS3DOgqYGexHk5cNYXFslNJBlEcOFcLG1BCB6DAdIVopGvagnu0by3avepwjCpoqzSMpbgH5lQ7A1IIR9s4GR6lOCeA69rdQ9r+E65zd/94dhyb630hbdR85hdwRFPlUEKPLZWW70khfM4ZuS1xJuVnIYC2bj51SnI2CxaDOIvFsgHcZoGanJ99BPdFZL61FgB0tITTVIJHnxqyyRFCtgpecvI6GOnaAdRRxq6IuAzhG2jzm00OrDOO95Y5tl8ibAHLImIUHPPBG8g/ezpO0M4YYVUYdO6+4D53yHR16EqNLbkGCOaEcJzFwZKFmJL1SZ4F+qz+7Mvw/PrO1aVYDy9OTWGx9MKoU0Kgay8gCzdBOUI75TxnTdxCDISRgLedxf0BjLNjIuYkVw6woduhTy4tOUbfm8AT/j+tRHIg7ZlbgFIF7eLNjECNzQLOteRUHeB32cSxhGXzMmjTt06g3oXxUZx4XalL01KYS1RG7uFT4SgUxEEE4LQUFotBnMXqUQDtwq1IAhpaJ3wl4AtTL2Ix4Pgof+YijHp+imui1Ul0g+ge/EfVsOQcloA8OvRB38YgFXE8ohGuX5V6Qv+v7nAM5B0RK3CrLY8bsqwt7Sknn6lyDsesV47R8Rjaahm+ALN5z7PlOeeZv090O03GPXZ2C6rFgZF5GhmXnWSc4eJNfFgsBnEWqxP+ThJ8YyRSBbV7AmAUyFNoKydMmQuJeexbaOueq7QlAOu77jCtD+5STg1zS3ULUw/QRo43I9iDDsRLyVb+gN/I897RqVstAOwK0R4+c/hrC4erL79f5XiFI7SD7czKremv4tlJ4PdMzgZ4R1YWqwXx7483bgXWLHp+fZ8STMEQjrAiQyRBdG1wHlYzwShQSH73OS1Pr+WkAci+jUMoqPS9l0vkFPODEaQLsM8TNp3CPkCb4+57V1OTiHhf/6icnyZC+qLply206U4uYL+k6Grt+Xk2tcmzpQ3T1CKf0kXEu0A9MQDWkPzUrSPIJ7YJuoNu1/fx9sIfh8s4UC/BQJmFWLOB+OX4HzcDawZNZneqOrU9oqXXbKuI1OR83AAoAL/TsJUCpE1gGzQg4BNgI3Df4Q8dmYMhxO2hrWe98XwfKpBESCp64CvsABGst73WgFmsABnWbWlNnttK84yW0nmxhZ3qHIhyQhDHBdqqdQ3U4dlrggl3OS6xWAziLFY7OMmL2PD3f6GtYOECQzn4TUnpGph2PQNzonkNKjV8nW274o6NLtpaQihdtOkzKo4OyknTftiGOPNBZwTkSGROQDvXOAC8QdTtS7fgWLWQMzdw4Gm1Et3M0dTR8ADUC4TxeU2/P95U1VPCGfqKeYg1PYhPmB7AYi1RfYuGMMq6hmVEJSPFNaoG/8QQZlPD19nKdtt0OoiHYDeVXpP+SsHfFt+uUTncFTPpsB+0t+bvG01fc3m34QA8t3RpKar/PxjeI4L4UmZOaLnSvOf52AlnVfe8TpK6wizEmg3EuQlYLKVw58ZIgOFSoOhWoqQuAygdxG3B4kDANwY/6UBPhq87K+wH1wjspf7DUo6q69Rt/mKjuSuGPHl+vwDs061sUjx8Q3hg8Bpffb0E0Wh4pXlef9ZLNNvZX0C4uPHvOxaLQZzFUqn5onc8FYEpcRwofQ2u0UDAvaVBHHPDbQdgjIpn4niZua0KYj9yacUc2oo1so3QUnYcEe9+Fk6W55xmulYs59dc8yf8TnOju+Dewy6TutxwuZThRvTNFricIYtBnMW6aw0diLE0oa18RQMDMrDbgsAtAVZKgNpFzSCPEXHTDY18wzd0AHcs9UUBbfWUvcLp8gUmpjaYEYdztUD7qC3bBHOva3Bz6nyoEg4hLk5MhS1g2hZN4bgHBxt3caW54bgweU2c7B81UfDvj7efikqaqDiLxSDOYt24bv0LPrIEhNAzUIwtXNwIMHyxZRNl+xKQU8K8kUYVcBekXwOpn3RboY9lX6ZrCmifTWlfB7Crp36EtmrN3PnTuFkW7sT7Jex0Te7t1oUboOFMTkrgG7+LDoq2oeU6WSwGcRbr3vT8+t4Z4buh2rFyVYUY+ksOxuQ800h+3+tstld3EebiFx6gqYGAnXjPDPyXbcOIu8kCuQLaMpgyuBakH4uOvpsSSnDBrkmlDtyopbGXFUw/60JnO0qDZ2Aps0JNu74IGI+g3WW2gvsIFBzJdw7dQbd3PwYRFW/aoImKB0upK85iMYizWNMBbg3ukT3chr2AcfN55SoadNdK3f2ZblCSaiBiLBDHBY1NH2AVkZPGIUlAvQAP83Bxh9OjAMZ6QNvH5GdAPsdEtHrKQQPic+SHo2NRG95TLfohEG07ZS5+QJy2k7CXXHFP6PgsCXIxVQXTgPBvU88u+JacdoW58aazUT+pLAzhLAZxFuuB9P3xdhIA4lonHAf8TAyi/x0RPuSd93Rb0iNUHwyAzqSO+FgDZAJtSsqGXKtucW3fVuQIyk0e7he00TqbvOctAfkusC4sIKOUnCn57zGxP13N6TFsyyUlaANt1ZcT9O8S6lO16MdMPHtHYeNyveqp29Gl3em1olOx1DUdkcIhpvrj6jxeAPweFqqyWAziLJalSmjr8LqC+BQDfleUtK/+OR00dwYQPVYdcVsIzKXrVrVFrLn/SoK2T2gjuDY59rTdMcrnsqiuUDgXuOto2dHXUwEZbjpUOvT/X9G2kXifzUTXXIvPxgXUONuwE1BO892XCHi4ZgDtHKvnnMR9LTFNxWRWzfcMTggsFoM4i3XXwjq2IfRvN94HklPUALaFs4Rc05KnerE6StAB3ntNe5jmkpfQRhpt2iIn8F0Z2EFf/6AD1eUMdJWyqycCSARYBFtXID7BPJvKoPOWCZvAKHlg4KzOpQiu6+TjPRzhOt1maYs35ecndARlPK8wfO2UTimLNYv+4SZgParEtsq5BLu2IIOgNUVE3HTw0u3ktyTtYfxIqktNckw9MYnymVS0aYD1SfOaKfuNrjHYgHs0k6ZIpTD9bpbNda/IPdBFzEuDcJwBkmcg8B7QRjNwK5U6pnbiGvE4WN53IpyML+hfZM5iMYizWA+mQoIgFxAfM3oVQxsZ0gEaLp4LxOB+CyXR7mlxVq0BVp2m2mGRAuHeA/jTsoJof1Ora8ajcQz+hXZx59wQfoJ2xmMjXW/z+wtpxzmcGt+O3lbYw7/QzlpUFk5xdIffESzWL3FqCuvRhdvYY5qJKZTgDpBjl/WjcFYbQDgOXhvu2snkI697yrQUBEJc5Gh7nxH5GZCf1A6n2hAI88ETAm0baHd4xPKdW2hnrqauLU8hHJ9Nla3gc2ta0nCuPGpsR7rgOCJHKDkZOZiVxvyR2An5x6aeX985NYXFIM5i3aouX+gJ6CN0NbTTp6YDG01LSQze3wXWAzBbdIaL5ehAzxtjTAuCaAtoG2uHvqag3CVd3WyTz0gJ8BdwXW+ewnRE/q57JjDKiZsXYZ3v/YhtHos2ppFuuXrKgbxuDb9r6uM1VzBeLe8uCNc5ATZO9Nggvurp86rHZrE9baoLUQg/GnzvsVgM4izWDWgN5lO8MdhPB0egn/Z2rcFNo3y5YhDeSo7AEhd63YOCDocrIsBNQS4A97SCvnOHpnxQWwngetOVPmHaB4WvEtS1znEr92Kg40Aj8AEBcNnJzUFdehFhcEO+CzDdq+vZrcn9llIb2NZ2x7KZ4cBnM1Q4dmGHEziXStJGxv1+ge6sJ5BhWteexWIQZ7EWLOPp0BFVDzxPVTKPpqI0A+Bf4AoDY/ahasYEQa8mUDIGQFQenoUtXOdTdwFn199NRTdS2oJbNRbqOMSKdnCJtubk9aEE5HGHIyTD74vD9Q+FcLzfVGObczvfrnXkS8X9/DhXYkE9i3XXejqfz9wKrIfVDWxxn4B6104sd4Zl0IYAD8D4pedw45Kf754B15rCsE2YXEVzoSkkMSz8bqcEzGrY6+xlS9q4BPsylDbCqDtN06GOwMrh/Y4wPKUIvweCHgfjJm3wAtr8tLAeXv8XYADZiQpkoYclwAAAAABJRU5ErkJggg=='); 16 | background-repeat: no-repeat; 17 | background-position: center; 18 | background-size: 738/2 + px 37/2 + px; 19 | display: none; 20 | z-index: 99999; 21 | } 22 | } 23 | 24 | @media all and (orientation : landscape) { 25 | body{ 26 | &:after{ 27 | width: 100%; 28 | height: 100%; 29 | display: block; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/sass/base/_reset.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * @file: 重置样式 3 | */ 4 | 5 | /* 全局设置 */ 6 | *, 7 | *:before, 8 | *:after{ 9 | font-family: "San Francisco", "Helvetica Neue", Helvetica, sans-serif; 10 | font-weight: 300; 11 | box-sizing: border-box; 12 | appearance: none; 13 | -webkit-tap-highlight-color: rgba(0,0,0,0); 14 | } 15 | 16 | /* 设置基本html,body样式 */ 17 | html,body{ 18 | margin: 0 auto; 19 | padding: 0; 20 | } 21 | 22 | /* 设置图片最大宽度 */ 23 | img{ 24 | max-width: 100%; 25 | } 26 | 27 | /* 清除默认按钮表现形式 */ 28 | button{ 29 | -webkit-appearance: none; 30 | appearance: none; 31 | } 32 | 33 | /* 设置表格默认样式 */ 34 | table { 35 | border-collapse: collapse; 36 | border-spacing: 0; 37 | } 38 | 39 | /* 文本域禁止拖拉放大 */ 40 | textarea { 41 | resize: none; 42 | } 43 | 44 | a { 45 | text-decoration: none; 46 | color: currentColor; 47 | } 48 | 49 | img { 50 | vertical-align: top; 51 | } 52 | 53 | i, em { 54 | font-style: normal; 55 | } 56 | 57 | ol, ul, li, menu { 58 | list-style: none outside none; 59 | } 60 | 61 | fieldset, iframe, abbr, acronym { 62 | border: 0 none; 63 | } 64 | 65 | dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fieldset, legend, input, button, textarea, p, blockquote, th, td, hr, 66 | article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { 67 | margin: 0; 68 | padding: 0; 69 | } 70 | 71 | del { 72 | text-decoration:line-through; 73 | } 74 | -------------------------------------------------------------------------------- /src/sass/base/_tool.scss: -------------------------------------------------------------------------------- 1 | /* @name: 转为REM单位fn 2 | * @ps: 默认宽度为750px 3 | */ 4 | @function toRem($px , $width: 750px) { 5 | // 动态尺寸单位 6 | $ppr: $width / 16 / 1rem; 7 | @return ($px / $ppr); 8 | } 9 | 10 | /* @name: 上下左右居中 (未知高度) 11 | * @ps: 需要设置父元素 display:table 12 | */ 13 | @mixin centers{ 14 | display:table-cell; 15 | vertical-align:middle; 16 | } 17 | 18 | /* @name: 上下左右居中 (确定高度)*/ 19 | @mixin centersHight{ 20 | position: absolute; 21 | left: 0; 22 | right: 0; 23 | top: 0; 24 | bottom: 0; 25 | margin: auto; 26 | } 27 | 28 | /* 29 | * @name: 文字溢出显示省略号 30 | * @ps: 需要设置宽度 31 | */ 32 | @mixin ellipsis{ 33 | white-space: nowrap; 34 | word-wrap: normal; 35 | overflow: hidden; 36 | text-overflow: ellipsis; 37 | text-align: left; 38 | } 39 | 40 | /* @name: 强制不换行 */ 41 | @mixin nowrap{ 42 | white-space: nowrap; 43 | word-wrap: normal; 44 | word-break: keep-all; 45 | } 46 | 47 | /* @name: 清除浮动 */ 48 | @mixin clearFix{ 49 | overflow: hidden; 50 | clear: both; 51 | } 52 | 53 | /* 54 | * @name: 三角形 55 | * @param: $size 大小 56 | * @param: $color 颜色 57 | * @param: $type {string} 类型 (up || down || left || right || topleft || topright || bottomleft || bottomright) 58 | */ 59 | @mixin triangle( 60 | $size: 50px, 61 | $type: up, 62 | $color: #6699FF 63 | ){ 64 | width: 0; 65 | height: 0; 66 | @if $type == up { 67 | border-left: $size/2 solid transparent; 68 | border-right: $size/2 solid transparent; 69 | border-bottom: $size solid $color; 70 | }@else if $type == down { 71 | border-left: $size/2 solid transparent; 72 | border-right: $size/2 solid transparent; 73 | border-top: $size solid $color; 74 | }@else if $type == left { 75 | border-top: $size/2 solid transparent; 76 | border-right: $size/2 solid $color; 77 | border-bottom: $size solid transparent; 78 | }@else if $type == right { 79 | border-top: $size/2 solid transparent; 80 | border-left: $size/2 solid $color; 81 | border-bottom: $size solid transparent; 82 | }@else if $type == topleft { 83 | border-top: $size solid $color; 84 | border-right: $size solid transparent; 85 | }@else if $type == topright { 86 | border-top: $size solid $color; 87 | border-left: $size solid transparent; 88 | }@else if $type == bottomleft { 89 | border-bottom: $size solid $color; 90 | border-right: $size solid transparent; 91 | }@else if $type == bottomright { 92 | border-bottom: $size solid $color; 93 | border-left: $size solid transparent; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/sass/main.scss: -------------------------------------------------------------------------------- 1 | @charset "UTF-8"; 2 | 3 | @import "base/base"; 4 | @import "img"; 5 | 6 | body { 7 | font: $b-font-weight #{$b-font-size}/#{$b-line-height} $b-font-family; 8 | color: $c-text; 9 | a { 10 | text-decoration: none; 11 | color: nth($c-link, 1); 12 | &:hover { 13 | text-decoration: underline; 14 | color: nth($c-link, 2); 15 | } 16 | } 17 | } 18 | --------------------------------------------------------------------------------