├── 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 | 
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 |
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 |
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 |
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 | `
`
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 |
--------------------------------------------------------------------------------