├── src ├── data │ └── .gitkeep ├── scss │ ├── .gitkeep │ ├── util │ │ ├── main.scss │ │ ├── _variables.scss │ │ ├── variables │ │ │ ├── _font.scss │ │ │ ├── _color.scss │ │ │ └── _breakpoint.scss │ │ ├── _functions.scss │ │ ├── _keyframe.scss │ │ └── _mixins.scss │ ├── components │ │ ├── _clock.scss │ │ ├── _space-invader.scss │ │ ├── _author.scss │ │ ├── _trivial.scss │ │ ├── _login-guide.scss │ │ ├── _guest-list.scss │ │ ├── _login.scss │ │ ├── _input-block.scss │ │ ├── _curtain-call.scss │ │ ├── _music.scss │ │ ├── _chat-block.scss │ │ └── _modal.scss │ ├── main.scss │ ├── layout │ │ └── _layout.scss │ └── reset.scss ├── ts │ ├── .gitkeep │ ├── dom │ │ ├── index.ts │ │ ├── space-inavader.ts │ │ ├── clock.ts │ │ ├── chat.ts │ │ └── music.ts │ ├── mesh │ │ ├── index.ts │ │ ├── dom-cube.ts │ │ └── cube.ts │ ├── util │ │ ├── index.ts │ │ ├── ticker.ts │ │ ├── sizer.ts │ │ ├── soundcloud-service.ts │ │ ├── widget.ts │ │ ├── function.ts │ │ ├── event-emitter.ts │ │ ├── service.ts │ │ └── waveform.ts │ ├── interface │ │ ├── showScreenTarget.ts │ │ ├── index.ts │ │ ├── meshType.ts │ │ └── faceType.ts │ ├── resource │ │ ├── textures.ts │ │ └── index.ts │ ├── class │ │ ├── env.ts │ │ ├── camera.ts │ │ ├── playground.ts │ │ ├── renderer.ts │ │ └── base.ts │ └── main.ts ├── static │ └── .gitkeep ├── template │ ├── .gitkeep │ ├── modal.ejs │ ├── footer.ejs │ ├── header.ejs │ ├── ga.ejs │ └── head.ejs ├── assets │ ├── fonts │ │ └── .gitkeep │ └── images │ │ ├── .gitkeep │ │ ├── chat.png │ │ ├── avatar.png │ │ ├── matcap │ │ ├── Orb.png │ │ ├── Sil.png │ │ ├── Skin.png │ │ ├── nikel.png │ │ ├── Carpaint.png │ │ ├── BlackRough.png │ │ ├── Carpaint 3.png │ │ ├── Carpaint 4.png │ │ ├── carpaint 2.png │ │ ├── porcelain.png │ │ └── GoldScratched.png │ │ ├── not-found.jpg │ │ ├── og-image.jpg │ │ ├── rotation-lock.png │ │ ├── space-invader.png │ │ ├── soundcloud-credit.png │ │ ├── cubemap │ │ ├── elegant │ │ │ ├── nx.png │ │ │ ├── ny.png │ │ │ ├── nz.png │ │ │ ├── px.png │ │ │ ├── py.png │ │ │ └── pz.png │ │ ├── gs-gradient │ │ │ ├── nx.png │ │ │ ├── ny.png │ │ │ ├── nz.png │ │ │ ├── px.png │ │ │ ├── py.png │ │ │ └── pz.png │ │ └── rb-gradient │ │ │ ├── nx.png │ │ │ ├── ny.png │ │ │ ├── nz.png │ │ │ ├── px.png │ │ │ ├── py.png │ │ │ └── pz.png │ │ ├── arrow.svg │ │ ├── play.svg │ │ ├── search.svg │ │ ├── play-alpha.svg │ │ ├── twitter.svg │ │ ├── pause-alpha.svg │ │ ├── github.svg │ │ ├── headphone.svg │ │ ├── rotation-lock-n.svg │ │ ├── logout.svg │ │ ├── ripple.svg │ │ ├── logo.svg │ │ └── no-result.svg └── pages │ └── index.main.ejs ├── tsconfig-for-webpack-config.json ├── .vscode └── settings.json ├── tsconfig.json ├── chat ├── package.json ├── fly.toml └── index.js ├── README(zh-tw).md ├── README.md ├── archive ├── login-guide.txt └── grid.txt ├── .gitignore ├── package.json └── webpack.config.ts /src/data/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/scss/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/static/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/template/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/template/modal.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/fonts/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/template/footer.ejs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ts/dom/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chat'; -------------------------------------------------------------------------------- /src/ts/mesh/index.ts: -------------------------------------------------------------------------------- 1 | export * from './cube'; 2 | -------------------------------------------------------------------------------- /src/template/header.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/images/chat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/chat.png -------------------------------------------------------------------------------- /src/assets/images/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/avatar.png -------------------------------------------------------------------------------- /src/ts/util/index.ts: -------------------------------------------------------------------------------- 1 | export * from './event-emitter'; 2 | export * from './ticker'; 3 | export * from './sizer'; -------------------------------------------------------------------------------- /src/assets/images/matcap/Orb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/matcap/Orb.png -------------------------------------------------------------------------------- /src/assets/images/matcap/Sil.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/matcap/Sil.png -------------------------------------------------------------------------------- /src/assets/images/not-found.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/not-found.jpg -------------------------------------------------------------------------------- /src/assets/images/og-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/og-image.jpg -------------------------------------------------------------------------------- /src/ts/interface/showScreenTarget.ts: -------------------------------------------------------------------------------- 1 | export type ShowScreenTargets = ('chatMainInner' | 'loginGuide' | 'guestList') -------------------------------------------------------------------------------- /src/assets/images/matcap/Skin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/matcap/Skin.png -------------------------------------------------------------------------------- /src/assets/images/matcap/nikel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/matcap/nikel.png -------------------------------------------------------------------------------- /src/scss/util/main.scss: -------------------------------------------------------------------------------- 1 | @import "./variables"; 2 | @import "./functions"; 3 | @import "./mixins"; 4 | @import "./keyframe"; -------------------------------------------------------------------------------- /src/assets/images/matcap/Carpaint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/matcap/Carpaint.png -------------------------------------------------------------------------------- /src/assets/images/rotation-lock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/rotation-lock.png -------------------------------------------------------------------------------- /src/assets/images/space-invader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/space-invader.png -------------------------------------------------------------------------------- /src/ts/interface/index.ts: -------------------------------------------------------------------------------- 1 | export * from './meshType'; 2 | export * from './faceType'; 3 | export * from './showScreenTarget'; -------------------------------------------------------------------------------- /src/assets/images/matcap/BlackRough.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/matcap/BlackRough.png -------------------------------------------------------------------------------- /src/assets/images/matcap/Carpaint 3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/matcap/Carpaint 3.png -------------------------------------------------------------------------------- /src/assets/images/matcap/Carpaint 4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/matcap/Carpaint 4.png -------------------------------------------------------------------------------- /src/assets/images/matcap/carpaint 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/matcap/carpaint 2.png -------------------------------------------------------------------------------- /src/assets/images/matcap/porcelain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/matcap/porcelain.png -------------------------------------------------------------------------------- /src/assets/images/soundcloud-credit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/soundcloud-credit.png -------------------------------------------------------------------------------- /src/ts/interface/meshType.ts: -------------------------------------------------------------------------------- 1 | export interface MeshType { 2 | setModel: () => void, 3 | update: (delta: number) => void, 4 | } -------------------------------------------------------------------------------- /src/assets/images/cubemap/elegant/nx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/elegant/nx.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/elegant/ny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/elegant/ny.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/elegant/nz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/elegant/nz.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/elegant/px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/elegant/px.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/elegant/py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/elegant/py.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/elegant/pz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/elegant/pz.png -------------------------------------------------------------------------------- /src/assets/images/matcap/GoldScratched.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/matcap/GoldScratched.png -------------------------------------------------------------------------------- /src/ts/interface/faceType.ts: -------------------------------------------------------------------------------- 1 | export interface FaceType { 2 | setElement: () => void, 3 | update: (delta: number) => void, 4 | } -------------------------------------------------------------------------------- /src/assets/images/cubemap/gs-gradient/nx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/gs-gradient/nx.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/gs-gradient/ny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/gs-gradient/ny.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/gs-gradient/nz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/gs-gradient/nz.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/gs-gradient/px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/gs-gradient/px.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/gs-gradient/py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/gs-gradient/py.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/gs-gradient/pz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/gs-gradient/pz.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/rb-gradient/nx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/rb-gradient/nx.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/rb-gradient/ny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/rb-gradient/ny.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/rb-gradient/nz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/rb-gradient/nz.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/rb-gradient/px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/rb-gradient/px.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/rb-gradient/py.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/rb-gradient/py.png -------------------------------------------------------------------------------- /src/assets/images/cubemap/rb-gradient/pz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mizok/3d-Cube-Chat/HEAD/src/assets/images/cubemap/rb-gradient/pz.png -------------------------------------------------------------------------------- /tsconfig-for-webpack-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "esModuleInterop": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "workbench.colorCustomizations": { 3 | "activityBar.background": "#021791", 4 | "titleBar.activeBackground": "#0320CB", 5 | "titleBar.activeForeground": "#FAFBFF" 6 | } 7 | } -------------------------------------------------------------------------------- /src/assets/images/arrow.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/scss/util/_variables.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file contains all application-wide Sass variables. 3 | // ----------------------------------------------------------------------------- 4 | 5 | @import 'variables/color'; 6 | @import 'variables/font'; 7 | @import 'variables/breakpoint'; -------------------------------------------------------------------------------- /src/template/ga.ejs: -------------------------------------------------------------------------------- 1 | 2 | <%const id="G-36SHCV2K3K" %> 3 | 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist/", 4 | "noImplicitAny": true, 5 | "module": "commonjs", 6 | "target": "es5", 7 | "jsx": "react", 8 | "allowJs": true, 9 | "moduleResolution": "node", 10 | "declaration": true, 11 | "experimentalDecorators": true, 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "dist", 16 | "./webpack.config.ts" 17 | ] 18 | } -------------------------------------------------------------------------------- /src/scss/components/_clock.scss: -------------------------------------------------------------------------------- 1 | .clock { 2 | @include hasHover() { 3 | &:hover { 4 | opacity: 0.75; 5 | } 6 | } 7 | 8 | 9 | * { 10 | font-family: 'Silkscreen'; 11 | color: $white; 12 | } 13 | 14 | &__date { 15 | font-size: 2rem; 16 | display: flex; 17 | } 18 | 19 | &__time { 20 | font-size: 4.5rem; 21 | display: flex; 22 | } 23 | } -------------------------------------------------------------------------------- /src/scss/components/_space-invader.scss: -------------------------------------------------------------------------------- 1 | .space-invader { 2 | width: 391px; 3 | height: 294px; 4 | background-image: url(~@img/space-invader.png); 5 | -webkit-animation: play 1s steps(6) infinite alternate; 6 | animation: play 3s steps(6) infinite alternate; 7 | } 8 | 9 | @keyframes play { 10 | from { 11 | background-position: 0px; 12 | } 13 | 14 | to { 15 | background-position: -2346px; 16 | } 17 | } -------------------------------------------------------------------------------- /src/scss/util/variables/_font.scss: -------------------------------------------------------------------------------- 1 | /// Regular font family 2 | /// @type List 3 | $text-font-stack: "微軟正黑體", 4 | "Microsoft JhengHei", 5 | Arial, 6 | "文泉驛正黑", 7 | "WenQuanYi Zen Hei", 8 | "蘋方-繁", 9 | "PingFang TC", 10 | "黑體-繁", 11 | "Heiti TC", 12 | "儷黑 Pro", 13 | "LiHei Pro", 14 | sans-serif !default; 15 | 16 | /// Code (monospace) font family 17 | /// @type List 18 | $code-font-stack: 'Courier New', 19 | 'DejaVu Sans Mono', 20 | 'Bitstream Vera Sans Mono', 21 | 'Monaco', 22 | monospace !default; -------------------------------------------------------------------------------- /chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node index.js", 9 | "dev":"npx nodemon index.js" 10 | }, 11 | "author": "mizok", 12 | "license": "ISC", 13 | "dependencies": { 14 | "socket.io": "^4.5.2" 15 | }, 16 | "devDependencies": { 17 | "nodemon": "^2.0.20", 18 | "ts-node": "^10.9.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/assets/images/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/scss/util/variables/_color.scss: -------------------------------------------------------------------------------- 1 | $blue:#2196f3 !default; 2 | $pink:#ec407a !default; 3 | $red:#ef5350 !default; 4 | $purple:#ab47bc !default; 5 | $indigo:#5c6bc0 !default; 6 | $teal:#26a69a !default; 7 | $green:#66bb6a !default; 8 | $yellow:#ffee58 !default; 9 | $orange:#ffa726 !default; 10 | $brown:#8d6e63 !default; 11 | $grey:#bdbdbd !default; 12 | $white:#ffffff !default; 13 | $black:#000000 !default; 14 | $lightblack:#495057 !default; 15 | 16 | 17 | //functional 18 | $text-color:$black !default; 19 | $link-color:$blue !default; -------------------------------------------------------------------------------- /src/ts/util/ticker.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from './event-emitter' 2 | import { Clock } from 'three'; 3 | 4 | export class Ticker extends EventEmitter { 5 | private clock: Clock = new Clock(); 6 | constructor() { 7 | super() 8 | window.requestAnimationFrame(() => { 9 | this.tick() 10 | }) 11 | } 12 | 13 | private tick() { 14 | this.trigger('tick', [this.clock]) 15 | 16 | window.requestAnimationFrame(() => { 17 | this.tick() 18 | }) 19 | } 20 | } -------------------------------------------------------------------------------- /src/assets/images/play-alpha.svg: -------------------------------------------------------------------------------- 1 | 資產 1 -------------------------------------------------------------------------------- /src/assets/images/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /README(zh-tw).md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3D-Cube-Chat 3 | date: 4 | author: Mizok 5 | version: 0.0.1 6 | tags: 7 | --- 8 | 9 | ![](https://i.imgur.com/IlGZNOp.gif) 10 | 11 | [README(英文)](https://github.com/mizok/3d-cube-chat/blob/main/README.md) 12 | 13 | ## 介紹 14 | 15 | 這是一個具備下列機能的專案 16 | 17 | - 音樂面板 : 該面板中的音樂來源是來自於`Soundcloud API`,本人不持有這些音樂的版權。 18 | - 聊天室面板 : 這是一個基於`Socket.io`所打造出來的線上聊天室。 19 | - 時鐘面板 : 如你所見,就是個時鐘。 20 | 21 | Github Page: [https://mizok.github.io/3d-Cube-Chat/](https://mizok.github.io/3d-Cube-Chat/) 22 | 23 | ## 這個專案可以被應用在商業用途嗎? 24 | 25 | 不好意思,但**不行**,這個專案目前的憑證為 [創用CC4.0](https://creativecommons.org/licenses/by-nc-sa/4.0/), 您不可以也不應該把它直接使用在任何商業用途上。 -------------------------------------------------------------------------------- /src/assets/images/pause-alpha.svg: -------------------------------------------------------------------------------- 1 | 資產 1 -------------------------------------------------------------------------------- /src/scss/main.scss: -------------------------------------------------------------------------------- 1 | @import 'util/main'; 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | 7 | .wrapper { 8 | height: 100%; 9 | display: flex; 10 | flex-direction: column; 11 | } 12 | } 13 | 14 | 15 | @import './layout/layout'; 16 | @import './components/curtain-call'; 17 | @import './components/modal'; 18 | @import './components/input-block'; 19 | @import './components/author'; 20 | @import './components/chat-block'; 21 | @import './components/space-invader'; 22 | @import './components/clock'; 23 | @import './components/login'; 24 | @import './components/login-guide'; 25 | @import './components/guest-list'; 26 | @import './components/music'; 27 | @import './components/trivial'; -------------------------------------------------------------------------------- /src/scss/util/variables/_breakpoint.scss: -------------------------------------------------------------------------------- 1 | /// Breakpoints map 2 | /// 網頁的breakpoint 其實不只有固定的1024/768/375 那幾種, 現行前端界常用的breakpoint 大多都是來自於知名框架的定義或主流device的長寬度 3 | /// 因此有些非主流廠牌的device就常有可能會被忽略掉,這邊我們紀錄的是目前前端界常見的一些breakpoint 4 | 5 | $screen-pad-pro-landscape: 1366px !default; //ipad pro 的橫式寬度 6 | $screen-bs-extra-large-desktop: 1200px !default; //Bootstrap 3定義的超大型桌上裝置寬度 7 | $screen-pad-landscape: 1024px !default; //一般ipad的橫式寬度 8 | $screen-bs-large: 992px !default; //Bootstrap 3定義的大型桌上裝置寬度 9 | $screen-pad-portrait: 768px !default; //一般ipad的直式寬度 10 | $screen-bs-phone-landscape: 576px !default; //Bootstrap 3定義的一般手機橫式寬度 11 | $screen-normal-phone-portrait: 480px !default; // 公司內部自行定義的平均手機寬度值 12 | $screen-small-phone: 320px !default; // iphone 5的直式寬度 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/ts/util/sizer.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from './event-emitter' 2 | 3 | export class Sizer extends EventEmitter { 4 | width: number; 5 | height: number; 6 | pixelRatio = Math.min(window.devicePixelRatio, 2); 7 | 8 | constructor(public canvas: HTMLCanvasElement) { 9 | super() 10 | this.initSizingMechanic(); 11 | } 12 | 13 | private initSizingMechanic() { 14 | this.sizing(); 15 | window.addEventListener('resize', this.sizing.bind(this)) 16 | } 17 | sizing() { 18 | const rect = this.canvas.parentElement.getBoundingClientRect(); 19 | this.width = rect.width; 20 | this.height = rect.height 21 | this.trigger('resize', [this.width, this.height]) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/scss/components/_author.scss: -------------------------------------------------------------------------------- 1 | .author { 2 | display: flex; 3 | align-items: center; 4 | color: transparentize($white, 0.5); 5 | padding: 10px 20PX; 6 | opacity: 0.75; 7 | 8 | &__former { 9 | flex: 1; 10 | } 11 | 12 | &__latter { 13 | flex: none; 14 | display: flex; 15 | align-items: center; 16 | } 17 | 18 | &__link { 19 | width: 20px; 20 | height: 20px; 21 | opacity: 0.5; 22 | transition: opacity 500ms; 23 | 24 | img { 25 | width: 100%; 26 | } 27 | } 28 | 29 | @include hasHover() { 30 | &__link:hover { 31 | opacity: 1; 32 | } 33 | } 34 | 35 | 36 | &__link+&__link { 37 | margin-left: 10px; 38 | } 39 | } -------------------------------------------------------------------------------- /src/scss/components/_trivial.scss: -------------------------------------------------------------------------------- 1 | .no-result { 2 | color: rgba(255, 255, 255, 1); 3 | opacity: 0.1; 4 | 5 | * { 6 | font-weight: 100; 7 | } 8 | 9 | &__img { 10 | width: 250px; 11 | position: relative; 12 | margin: 0 auto; 13 | 14 | @include rwd($screen-pad-portrait) { 15 | width: 150px; 16 | } 17 | 18 | img { 19 | width: 100%; 20 | } 21 | } 22 | 23 | &__img+&__title { 24 | margin-top: 50px; 25 | } 26 | 27 | &__title { 28 | font-size: 30px; 29 | line-height: 1.5; 30 | } 31 | 32 | &__title+&__descrp { 33 | margin-top: 30px; 34 | } 35 | 36 | &__descrp { 37 | font-size: 14px; 38 | line-height: 1.5; 39 | } 40 | } -------------------------------------------------------------------------------- /src/assets/images/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ts/resource/textures.ts: -------------------------------------------------------------------------------- 1 | interface Source { 2 | name: string, 3 | type: string, 4 | paths?: string[], 5 | path?: string 6 | } 7 | 8 | export const textureSources: Source[] = [ 9 | { 10 | name: 'gradientCubeTexture', 11 | type: 'cubeTexture', 12 | paths: [ 13 | './assets/images/cubemap/gs-gradient/px.png', 14 | './assets/images/cubemap/gs-gradient/nx.png', 15 | './assets/images/cubemap/gs-gradient/py.png', 16 | './assets/images/cubemap/gs-gradient/ny.png', 17 | './assets/images/cubemap/gs-gradient/pz.png', 18 | './assets/images/cubemap/gs-gradient/nz.png' 19 | ] 20 | }, 21 | { 22 | name: 'cubeMatcap', 23 | type: 'texture', 24 | path: './assets/images/matcap/BlackRough.png' 25 | } 26 | ] 27 | 28 | -------------------------------------------------------------------------------- /src/scss/components/_login-guide.scss: -------------------------------------------------------------------------------- 1 | .login-guide { 2 | position: relative; 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | justify-content: center; 7 | 8 | * { 9 | font-family: 'Silkscreen'; 10 | color: $white; 11 | } 12 | 13 | 14 | &__title { 15 | font-size: 50px; 16 | animation: flickr 1s steps(3) infinite forwards; 17 | } 18 | 19 | &__button { 20 | font-size: 20px; 21 | border-radius: 99px; 22 | padding: 15px; 23 | margin-top: 30px; 24 | background-color: transparent; 25 | border: 3px solid $white; 26 | opacity: 0.5; 27 | cursor: pointer; 28 | 29 | @include hasHover() { 30 | &:hover { 31 | opacity: 0.75; 32 | } 33 | } 34 | 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 3D-Cube-Chat 3 | date: 4 | author: Mizok 5 | version: 0.0.1 6 | tags: 7 | --- 8 | 9 | # 3D-Cube-Chat 10 | 11 | ![](https://i.imgur.com/IlGZNOp.gif) 12 | 13 | [README(zh-tw)](https://github.com/mizok/3d-Cube-Chat/blob/master/README(zh-tw).md) 14 | ## Introduction 15 | 16 | It's basically a project which has: 17 | 18 | - Music panel : The music sources are from Soundcloud API, I don't have the copyright of them. 19 | - Chat panel : It's a chat that I built using `socket.io`. 20 | - Clock panel : well, just a clock. 21 | 22 | Github Page: [https://mizok.github.io/3d-Cube-Chat/](https://mizok.github.io/3d-Cube-Chat/) 23 | 24 | ## Can I take this for commercial use? 25 | 26 | Sorry, but NO, this repository is under [Attribution-NonCommercial-ShareAlike 4.0 International](https://creativecommons.org/licenses/by-nc-sa/4.0/), so you shall not use this in ANY commercial case. 27 | -------------------------------------------------------------------------------- /chat/fly.toml: -------------------------------------------------------------------------------- 1 | # fly.toml file generated for 3d-cube-chat on 2022-10-12T05:59:03+08:00 2 | 3 | app = "3d-cube-chat" 4 | kill_signal = "SIGINT" 5 | kill_timeout = 5 6 | processes = [] 7 | 8 | [build] 9 | builder = "heroku/buildpacks:20" 10 | 11 | [env] 12 | PORT = "8080" 13 | 14 | [experimental] 15 | allowed_public_ports = [] 16 | auto_rollback = true 17 | 18 | [[services]] 19 | http_checks = [] 20 | internal_port = 5500 21 | processes = ["app"] 22 | protocol = "tcp" 23 | script_checks = [] 24 | [services.concurrency] 25 | hard_limit = 25 26 | soft_limit = 20 27 | type = "connections" 28 | 29 | [[services.ports]] 30 | force_https = true 31 | handlers = ["http"] 32 | port = 80 33 | 34 | [[services.ports]] 35 | handlers = ["tls", "http"] 36 | port = 443 37 | 38 | [[services.tcp_checks]] 39 | grace_period = "1s" 40 | interval = "15s" 41 | restart_limit = 0 42 | timeout = "2s" 43 | -------------------------------------------------------------------------------- /src/assets/images/headphone.svg: -------------------------------------------------------------------------------- 1 | 資產 1 -------------------------------------------------------------------------------- /src/assets/images/rotation-lock-n.svg: -------------------------------------------------------------------------------- 1 | 資產 1 -------------------------------------------------------------------------------- /src/ts/util/soundcloud-service.ts: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | export class SoundCloudService { 4 | private config: { clientId: string } 5 | constructor(config: { clientId: string }) { 6 | this.config = config; 7 | } 8 | 9 | search(params: { q: string, limit?: number, client_id?: string }) { 10 | return new Promise((resolve, reject) => { 11 | const type = '/search/tracks'; 12 | const cors = `https://cors-proxy-server.onrender.com/`; 13 | params.client_id = params.client_id ? params.client_id : this.config.clientId; 14 | const urlParameters = Object.entries(params) 15 | .map((e) => e.join("=")) 16 | .join("&"); 17 | 18 | const url = `${cors}https://api-v2.soundcloud.com${type}?${urlParameters}`; 19 | 20 | axios({ 21 | url: url 22 | }) 23 | .then((res: any) => resolve(res.data)) 24 | .catch((err: any) => reject(err)); 25 | }); 26 | } 27 | } -------------------------------------------------------------------------------- /src/assets/images/logout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 10 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/ts/dom/space-inavader.ts: -------------------------------------------------------------------------------- 1 | import { Object3D, Vector3, Matrix4 } from "three"; 2 | import { CSS3DObject } from "three/examples/jsm/renderers/CSS3DRenderer"; 3 | import { Base } from "../class/base"; 4 | import { FaceType } from "../interface"; 5 | import { updateOcclude } from "../util/function"; 6 | 7 | export class SpaceInvader implements FaceType { 8 | object: Object3D 9 | element: HTMLElement; 10 | private offset = 1.7; 11 | private pos = new Vector3(0, 0, -this.offset); 12 | private normal = new Vector3(0, 0, -1); 13 | private cNormal = new Vector3(); 14 | private cPos = new Vector3(); 15 | private m4 = new Matrix4(); 16 | constructor(private base: Base) { 17 | this.setElement(); 18 | } 19 | setElement() { 20 | this.element = this.base.domBundle.querySelector('#space-invader'); 21 | this.object = new CSS3DObject(this.element); 22 | this.object.position.set(0, 0, -this.offset); 23 | this.object.rotation.y = - Math.PI; 24 | this.object.scale.set(1 / 160, 1 / 160, 1); 25 | } 26 | 27 | update() { 28 | 29 | updateOcclude(this); 30 | } 31 | } -------------------------------------------------------------------------------- /src/assets/images/ripple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/scss/components/_guest-list.scss: -------------------------------------------------------------------------------- 1 | .guest-list { 2 | * { 3 | color: $white; 4 | font-family: 'Silkscreen'; 5 | } 6 | 7 | &__inner { 8 | height: 100%; 9 | display: flex; 10 | flex-direction: column; 11 | overflow: hidden; 12 | } 13 | 14 | &__title { 15 | font-size: 3rem; 16 | text-align: center; 17 | margin-bottom: 1rem; 18 | } 19 | 20 | &__header { 21 | padding: 10px; 22 | border-bottom: 3px solid $white; 23 | flex: none; 24 | } 25 | 26 | &__number { 27 | font-size: 1rem; 28 | text-align: center; 29 | } 30 | 31 | &__ul { 32 | flex: 1; 33 | padding: 0 10px; 34 | margin-top: 10px; 35 | margin-bottom: 10px; 36 | overflow: auto; 37 | @include custom-scroll(); 38 | } 39 | 40 | &__li+&__li { 41 | border-top: 3px dotted $white; 42 | } 43 | 44 | 45 | &__li { 46 | opacity: 0; 47 | animation: fadeIndownSmall 500ms forwards; 48 | 49 | } 50 | } 51 | 52 | .guest { 53 | padding: 20px; 54 | 55 | &__name { 56 | font-size: 1rem; 57 | } 58 | } -------------------------------------------------------------------------------- /archive/login-guide.txt: -------------------------------------------------------------------------------- 1 | import { 2 | Scene, 3 | WebGLRenderer, 4 | PerspectiveCamera, 5 | Color 6 | } from "three"; 7 | 8 | import { 9 | InfiniteGridHelper 10 | } from './grid' 11 | 12 | export function loginGuide(canvas: HTMLCanvasElement) { 13 | const scene = new Scene(); 14 | const renderer = new WebGLRenderer({ 15 | antialias: true, 16 | canvas: canvas 17 | }); 18 | 19 | const rect = canvas.getBoundingClientRect(); 20 | 21 | 22 | renderer.setSize(rect.width, rect.height); 23 | renderer.setViewport(0, 0, rect.width, rect.height); 24 | 25 | renderer.setClearColor(new Color(0, 0, 0)); 26 | 27 | const camera = new PerspectiveCamera( 28 | 75, 29 | rect.width / rect.height, 30 | 0.1, 31 | 1000 32 | ); 33 | scene.add(camera); 34 | 35 | const grid = new InfiniteGridHelper(); 36 | 37 | scene.add(grid); 38 | 39 | 40 | camera.position.z = 5; 41 | 42 | let time = 0; 43 | const loop = (time: number) => { 44 | renderer.render(scene, camera); 45 | requestAnimationFrame((time) => { 46 | loop(time); 47 | }); 48 | }; 49 | 50 | loop(time); 51 | } 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/ts/class/env.ts: -------------------------------------------------------------------------------- 1 | import { Base } from './base'; 2 | import { AmbientLight, Clock, CubeTextureLoader, DirectionalLight } from 'three'; 3 | 4 | export class Env { 5 | ambientLight: AmbientLight; 6 | directionalLight: DirectionalLight; 7 | constructor(private base: Base) { 8 | this.setLights(); 9 | } 10 | 11 | private setLights() { 12 | this.setAmbientLight(); 13 | this.setDirectionalLight(); 14 | this.setBackground(); 15 | } 16 | 17 | private setDirectionalLight() { 18 | this.directionalLight = new DirectionalLight(0xffffff, 1); 19 | this.directionalLight.castShadow = true 20 | this.directionalLight.shadow.mapSize.set(2048, 2048) 21 | this.directionalLight.shadow.normalBias = 0.05 22 | this.directionalLight.position.set(3.5, 2, - 1.25) 23 | this.base.scene.add(this.directionalLight) 24 | } 25 | 26 | private setAmbientLight() { 27 | this.ambientLight = new AmbientLight(0xffffff, 1); 28 | this.base.scene.add(this.ambientLight) 29 | } 30 | 31 | private setBackground() { 32 | this.base.scene.background = this.base.resources.gradientCubeTexture 33 | 34 | } 35 | 36 | 37 | update(delta: number) { 38 | 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/ts/class/camera.ts: -------------------------------------------------------------------------------- 1 | import { PerspectiveCamera, Vector3 } from 'three'; 2 | import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js'; 3 | import { Base } from './base'; 4 | 5 | export class Camera { 6 | instance: PerspectiveCamera; 7 | controls: OrbitControls; 8 | bestPoint = new Vector3(-8, 1, 8); 9 | minimunDrawbackAngle = Math.PI / 3; 10 | private sizer = this.base.sizer; 11 | private canvas = this.base.domCanvas; 12 | private scene = this.base.scene; 13 | 14 | constructor( 15 | private base: Base 16 | ) { 17 | this.setInstance() 18 | this.setControls() 19 | } 20 | 21 | private setInstance() { 22 | const camera = new PerspectiveCamera(35, this.sizer.width / this.sizer.height, 0.1, 100); 23 | camera.position.set(this.bestPoint.x, this.bestPoint.y, this.bestPoint.z); 24 | this.instance = camera; 25 | this.scene.add(this.instance) 26 | // //@ts-ignore 27 | // window.cm = camera; 28 | 29 | } 30 | 31 | private setControls() { 32 | this.controls = new OrbitControls(this.instance, this.canvas) 33 | this.controls.enableDamping = true; 34 | this.controls.enableZoom = false; 35 | this.controls.enablePan = false; 36 | } 37 | 38 | resize() { 39 | this.instance.aspect = this.sizer.width / this.sizer.height 40 | this.instance.updateProjectionMatrix() 41 | } 42 | 43 | update() { 44 | this.controls.update() 45 | } 46 | } -------------------------------------------------------------------------------- /src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/scss/util/_functions.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file contains all application-wide Sass functions. 3 | // ----------------------------------------------------------------------------- 4 | 5 | //平方根 6 | @function sqrt($r) { 7 | $x0: 1; // initial value 8 | $solution: false; 9 | 10 | @for $i from 1 through 10 { 11 | @if abs(2 * $x0) < 0.00000000000001 { 12 | // Don't want to divide by a number smaller than this 13 | $solution: false; 14 | } 15 | 16 | $x1: $x0 - ($x0 * $x0 - abs($r)) / (2 * $x0) !global; 17 | 18 | @if (abs($x1 - $x0) / abs($x1)) < 0.0000001 { 19 | // 7 digit accuracy is desired 20 | $solution: true; 21 | } 22 | 23 | $x0: $x1; 24 | } 25 | 26 | @if $solution==true { 27 | 28 | // If $r is negative, then the output will be multiplied with i = √-1 29 | // (√xy = √x√y) => √-$r = √-1√$r 30 | @if $r < 0 { 31 | @return $x1 #{i}; 32 | } 33 | 34 | @else { 35 | @return $x1; 36 | } 37 | } 38 | 39 | @else { 40 | @return "No solution"; 41 | } 42 | } 43 | 44 | //白化顏色用的function 45 | @function diluter($color, $rate) { 46 | $color-0: mix($color, #fff, $rate); 47 | @return $color-0; 48 | } 49 | 50 | //px轉換單位成rem用的function 51 | @function pxToRem($size, $base:16px) { 52 | $remSize: $size / $base; 53 | //Default font size on html element is 100%, equivalent to 16px; 54 | @return #{$remSize}rem; 55 | } -------------------------------------------------------------------------------- /src/scss/components/_login.scss: -------------------------------------------------------------------------------- 1 | .login { 2 | padding: 40px; 3 | background: linear-gradient(180deg, rgb(42, 42, 45) 0%, rgb(19, 19, 19) 100%); 4 | transition: opacity 500ms; 5 | 6 | &--logined { 7 | opacity: 0; 8 | pointer-events: none; 9 | } 10 | 11 | &__title { 12 | margin: 0 0 30px; 13 | padding: 0; 14 | color: #fff; 15 | text-align: center; 16 | font-family: 'Silkscreen'; 17 | } 18 | 19 | &__user-box { 20 | position: relative; 21 | 22 | .input-block { 23 | padding-left: 0; 24 | padding-right: 0; 25 | padding-bottom: 0.5rem; 26 | padding-top: 1.5rem; 27 | } 28 | 29 | label { 30 | position: absolute; 31 | top: 0; 32 | left: 0; 33 | padding: 10px 0; 34 | color: #fff; 35 | pointer-events: none; 36 | transition: .5s; 37 | top: -10px; 38 | left: 0; 39 | color: #03e9f4; 40 | font-size: 12px; 41 | font-family: 'Silkscreen'; 42 | } 43 | 44 | } 45 | 46 | 47 | &__button { 48 | 49 | @include gradient-border(); 50 | width: 100%; 51 | color: $white; 52 | margin-top: 30px; 53 | transition: box-shadow 500ms; 54 | cursor: pointer; 55 | font-family: 'Silkscreen'; 56 | 57 | @include hasHover() { 58 | &:hover { 59 | box-shadow: 10px 10px 30px 0px rgba(0, 0, 0, 0.75); 60 | 61 | } 62 | } 63 | 64 | } 65 | } -------------------------------------------------------------------------------- /src/scss/components/_input-block.scss: -------------------------------------------------------------------------------- 1 | // colors 2 | $input-background: rgba(38, 38, 40, 0); 3 | $input-text-inactive: #7881A1; 4 | $input-text-active: #BFD2FF; 5 | 6 | 7 | .input-block { 8 | position: relative; 9 | display: flex; 10 | align-items: center; 11 | width: 100%; 12 | margin: 0 auto; 13 | border-radius: 2px; 14 | padding: 0.7rem 1.5rem 0.8rem 1rem; 15 | background: $input-background; 16 | 17 | &:after { 18 | content: ""; 19 | position: absolute; 20 | left: 0px; 21 | right: 0px; 22 | bottom: 0px; 23 | z-index: 999; 24 | height: 2px; 25 | border-bottom-left-radius: 2px; 26 | border-bottom-right-radius: 2px; 27 | background-position: 0% 0%; 28 | background: linear-gradient(to right, #B294FF, #57E6E6, #FEFFB8, #57E6E6, #B294FF, #57E6E6); 29 | background-size: 500% auto; 30 | animation: gradient 3s linear infinite; 31 | z-index: 1; 32 | } 33 | 34 | &__input { 35 | flex-grow: 1; 36 | color: $input-text-active; 37 | font-size: 1rem; 38 | line-height: 2.4rem; 39 | vertical-align: middle; 40 | border-style: none; 41 | background: transparent; 42 | outline: none; 43 | 44 | &::-webkit-input-placeholder { 45 | color: $input-text-inactive; 46 | } 47 | } 48 | 49 | &__button { 50 | vertical-align: middle; 51 | transition: color .25s; 52 | padding: 0; 53 | background: none; 54 | border: none; 55 | outline: none; 56 | opacity: 0.5; 57 | transition: opacity 500ms; 58 | cursor: pointer; 59 | font-size: 0; 60 | 61 | @include hasHover() { 62 | &:hover { 63 | opacity: 0.75; 64 | } 65 | } 66 | 67 | } 68 | } -------------------------------------------------------------------------------- /src/ts/class/playground.ts: -------------------------------------------------------------------------------- 1 | import { Env } from "./env"; 2 | import { Base } from "./base"; 3 | import { Cube } from "../mesh"; 4 | import { DomCube } from "../mesh/dom-cube"; 5 | import { EventEmitter } from "../util"; 6 | 7 | export class Playground extends EventEmitter { 8 | env: Env; 9 | cube: Cube; 10 | domCube: DomCube; 11 | ready = false; 12 | private initTimeout: any; 13 | private initTimeoutDuration = 1000; 14 | constructor(private base: Base) { 15 | super(); 16 | this.init(); 17 | } 18 | private init() { 19 | this.base.getResources().then(() => { 20 | this.env = new Env(this.base); 21 | this.trigger('env-ready'); 22 | clearTimeout(this.initTimeout) 23 | this.initTimeout = setTimeout(() => { 24 | this.cube = new Cube(this.base); 25 | this.domCube = new DomCube(this.base); 26 | this.ready = true; 27 | this.trigger('ready'); 28 | }, this.initTimeoutDuration) 29 | }) 30 | } 31 | 32 | showChat() { 33 | this.cube.showChat() 34 | this.domCube.showChat() 35 | if (this.base.getLoginStatus()) { 36 | this.domCube.chat.showScreen('guestList'); 37 | } 38 | else { 39 | this.domCube.chat.showScreen('loginGuide'); 40 | } 41 | } 42 | 43 | showMusic() { 44 | this.cube.showMusic() 45 | this.domCube.showMusic() 46 | } 47 | 48 | update(delta: number) { 49 | if (this.ready) { 50 | this.env.update(delta); 51 | this.cube.update(delta); 52 | this.domCube.update(delta); 53 | } 54 | 55 | } 56 | } -------------------------------------------------------------------------------- /src/ts/resource/index.ts: -------------------------------------------------------------------------------- 1 | import { textureSources } from './textures'; 2 | import { TextureLoader, CubeTextureLoader } from 'three'; 3 | 4 | interface SourceObj { 5 | name: string, 6 | content: any 7 | } 8 | 9 | 10 | const textureLoader = new TextureLoader(); 11 | const cubeTextureLoader = new CubeTextureLoader(); 12 | 13 | const getTexture = (source: any) => { 14 | const prm: Promise = new Promise((res, rej) => { 15 | textureLoader.load( 16 | source.path, 17 | (texture) => { 18 | res({ 19 | name: source.name, 20 | content: texture 21 | }); 22 | }, 23 | null, 24 | rej 25 | ); 26 | }); 27 | return prm; 28 | }; 29 | 30 | const getCubeTexture = (source: any) => { 31 | const prm: Promise = new Promise((res, rej) => { 32 | cubeTextureLoader.load( 33 | source.paths, 34 | (texture) => { 35 | res({ 36 | name: source.name, 37 | content: texture 38 | }); 39 | }, 40 | null, 41 | (error) => { console.log(error) } 42 | ); 43 | }); 44 | return prm; 45 | } 46 | 47 | 48 | 49 | export const getResources = () => { 50 | 51 | const promiseArr: Promise[] = []; 52 | 53 | 54 | for (let textureSource of textureSources) { 55 | switch (textureSource.type) { 56 | case 'cubeTexture': 57 | promiseArr.push(getCubeTexture(textureSource)); 58 | break; 59 | case 'texture': 60 | promiseArr.push(getTexture(textureSource)); 61 | break; 62 | } 63 | 64 | } 65 | 66 | 67 | return Promise.all(promiseArr).then((values) => { 68 | const result: { 69 | [key: string]: any 70 | } = {}; 71 | values.forEach((sourceObj) => { 72 | result[sourceObj.name] = sourceObj.content; 73 | }) 74 | return result; 75 | }) 76 | } 77 | 78 | 79 | -------------------------------------------------------------------------------- /src/ts/util/widget.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "../util"; 2 | const SoundcloudWidget = require('soundcloud-widget'); 3 | const iframeEle = document.querySelector('#soundcloud-iframe') as HTMLIFrameElement; 4 | class SCWidget extends EventEmitter { 5 | instance: typeof SoundcloudWidget; 6 | constructor(iframeEle: HTMLIFrameElement) { 7 | super(); 8 | this.instance = new SoundcloudWidget(iframeEle); 9 | this.init(); 10 | } 11 | private init() { 12 | this.instance.on(SoundcloudWidget.events.PLAY, () => { 13 | this.trigger('play') 14 | }) 15 | this.instance.on(SoundcloudWidget.events.PLAY_PROGRESS, (ev: any) => { 16 | const progressPercent = ev.relativePosition * 100; 17 | this.trigger('play-progress', [progressPercent]) 18 | }) 19 | this.instance.on(SoundcloudWidget.events.PAUSE, () => { 20 | this.trigger('pause') 21 | }) 22 | this.instance.on(SoundcloudWidget.events.SEEK, (ev: any) => { 23 | this.trigger('seek') 24 | }) 25 | } 26 | play() { 27 | this.instance.play(); 28 | } 29 | pause() { 30 | this.instance.pause(); 31 | } 32 | toggle() { 33 | this.instance.toggle(); 34 | } 35 | seek(millisecond: number) { 36 | this.instance.seekTo(millisecond) 37 | } 38 | load(url: string) { 39 | return this.instance.load(url, { auto_play: false }).then(async () => { 40 | const soundGetter = this.instance.getCurrentSound(); 41 | await soundGetter.then((soundObject: any) => { 42 | this.trigger('load', [soundObject]) 43 | }) 44 | console.log('sound loaded') 45 | }) 46 | } 47 | } 48 | 49 | export const widget = new SCWidget(iframeEle); 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /src/scss/util/_keyframe.scss: -------------------------------------------------------------------------------- 1 | // gradient animation 2 | @keyframes gradient { 3 | 0% { 4 | background-position: 0 0 5 | } 6 | 7 | 100% { 8 | background-position: 100% 0 9 | } 10 | } 11 | 12 | 13 | // bubble animation 14 | @keyframes bubble { 15 | 0% { 16 | transform: scale(0); 17 | } 18 | 19 | 100% { 20 | transform: scale(1); 21 | } 22 | } 23 | 24 | 25 | @keyframes unlock { 26 | 0% { 27 | opacity: 0; 28 | transform: rotation(-90deg); 29 | } 30 | 31 | 100% { 32 | opacity: 0.5; 33 | transform: rotation(0deg); 34 | } 35 | } 36 | 37 | 38 | 39 | @keyframes spin { 40 | 0% { 41 | transform: rotate(0deg) 42 | } 43 | 44 | 100% { 45 | transform: rotate(360deg) 46 | } 47 | } 48 | 49 | @keyframes flickr { 50 | 0% { 51 | opacity: 0; 52 | } 53 | 54 | 50% { 55 | opacity: 1; 56 | } 57 | 58 | 100% { 59 | opacity: 0; 60 | } 61 | } 62 | 63 | @keyframes fadeIndownSmall { 64 | 0% { 65 | opacity: 0; 66 | transform: translateY(-20%) 67 | } 68 | 69 | 100% { 70 | opacity: 1; 71 | transform: translateY(0%) 72 | } 73 | } 74 | 75 | 76 | @keyframes fadeIndownMedium { 77 | 0% { 78 | opacity: 0; 79 | transform: translateY(-50%) 80 | } 81 | 82 | 100% { 83 | opacity: 1; 84 | transform: translateY(0%) 85 | } 86 | } 87 | 88 | 89 | @keyframes marquee { 90 | 0% { 91 | opacity: 0; 92 | width: auto; 93 | transform: translateX(0%); 94 | } 95 | 96 | 10% { 97 | width: auto; 98 | opacity: 1; 99 | } 100 | 101 | 100% { 102 | width: auto; 103 | transform: translateX(-100%); 104 | } 105 | } 106 | 107 | @keyframes fadeIn { 108 | 0% { 109 | opacity: 0; 110 | } 111 | 112 | 100% { 113 | opacity: 1; 114 | } 115 | } -------------------------------------------------------------------------------- /src/ts/class/renderer.ts: -------------------------------------------------------------------------------- 1 | import { WebGLRenderer, PCFSoftShadowMap } from 'three'; 2 | import { CSS3DRenderer } from 'three/examples/jsm/renderers/CSS3DRenderer'; 3 | 4 | import { Base } from './base'; 5 | 6 | export class Renderer { 7 | instance: WebGLRenderer; 8 | instance2: CSS3DRenderer; 9 | private sizer = this.base.sizer; 10 | private canvas = this.base.canvas; 11 | private domCanvas = this.base.domCanvas; 12 | private scene = this.base.scene; 13 | private scene2 = this.base.scene2; 14 | private camera = this.base.camera; 15 | constructor( 16 | private base: Base 17 | ) { 18 | this.setInstances() 19 | } 20 | 21 | private setInstances() { 22 | //instance 23 | this.instance = new WebGLRenderer({ 24 | canvas: this.canvas, 25 | antialias: true, 26 | preserveDrawingBuffer: true 27 | }) 28 | this.instance.physicallyCorrectLights = true 29 | this.instance.toneMappingExposure = 1.75 30 | this.instance.shadowMap.enabled = true 31 | this.instance.shadowMap.type = PCFSoftShadowMap 32 | this.instance.setClearColor(0xffffff) 33 | //instance2 34 | this.instance2 = new CSS3DRenderer({ 35 | element: this.domCanvas 36 | }); 37 | this.sizing(); 38 | } 39 | 40 | resize() { 41 | this.sizing(); 42 | } 43 | 44 | private sizing() { 45 | //instance 46 | this.instance.setSize(this.sizer.width, this.sizer.height); 47 | this.instance.setPixelRatio(this.sizer.pixelRatio); 48 | //instance2 49 | this.instance2.setSize(this.sizer.width, this.sizer.height); 50 | } 51 | 52 | update() { 53 | this.instance.render(this.scene, this.camera.instance); 54 | this.instance2.render(this.scene2, this.camera.instance); 55 | } 56 | 57 | } -------------------------------------------------------------------------------- /src/ts/util/function.ts: -------------------------------------------------------------------------------- 1 | export function updateOcclude(faceType: any) { 2 | const box = faceType.base.playground.cube.mesh; 3 | 4 | faceType.cNormal.copy(faceType.normal).applyMatrix3(box.normalMatrix); 5 | 6 | faceType.cPos.copy(faceType.pos).applyMatrix4(faceType.m4.multiplyMatrices(faceType.base.camera.instance.matrixWorldInverse, box.matrixWorld)); 7 | 8 | let d = faceType.cPos.negate().dot(faceType.cNormal); 9 | 10 | faceType.element.style.visibility = d < 0 ? "hidden" : "visible"; 11 | } 12 | 13 | export function pad(n: number, l: number) { 14 | for (var r = n.toString(); r.length < l; r = 0 + r); 15 | return r; 16 | } 17 | 18 | export const cubeLikeConfig = { 19 | initialScale: [0, 0, 0] as const, 20 | initialRotation: [Math.PI / 3, -Math.PI / 3, Math.PI / 3] as const, 21 | startAnimationInnerRotationConfig: { 22 | x: 0, 23 | y: Math.PI / 4, 24 | z: 0, 25 | duration: 2 26 | }, 27 | startAnimationInnerScalingConfig: { 28 | x: 1, 29 | y: 1, 30 | z: 1, 31 | duration: 2 32 | }, 33 | showChatAnimationInnerRotationConfig: { 34 | x: 0, 35 | y: -Math.PI / 4, 36 | z: 0, 37 | duration: 2 38 | }, 39 | showMusicAnimationInnerRotationConfig: { 40 | x: 0, 41 | y: -Math.PI / 4, 42 | z: 0, 43 | duration: 2 44 | }, 45 | showChatAnimationOuterRotationConfig: { 46 | x: 0, 47 | y: 0, 48 | z: 0, 49 | duration: 2 50 | }, 51 | updateParameter: 5 52 | } 53 | 54 | 55 | export function createElementFromHTML(htmlString: string) { 56 | const div = document.createElement('div'); 57 | div.innerHTML = htmlString.trim(); 58 | 59 | // Change this to div.childNodes to support multiple top-level nodes. 60 | return div.firstChild; 61 | } 62 | 63 | export function getTargetAngle(angle: number) { 64 | const remain = angle % (2 * Math.PI); 65 | const minor = angle - remain - 0.25 * Math.PI; 66 | const major = angle + ((7 * Math.PI / 4) - remain) 67 | const newAngle = remain < Math.PI ? minor : major; 68 | return newAngle; 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/ts/dom/clock.ts: -------------------------------------------------------------------------------- 1 | import { Matrix4, Object3D, Vector3 } from "three"; 2 | import { CSS3DObject } from "three/examples/jsm/renderers/CSS3DRenderer"; 3 | import { Base } from "../class/base"; 4 | import { FaceType } from "../interface"; 5 | import { pad, updateOcclude } from "../util/function"; 6 | 7 | export class Clock implements FaceType { 8 | object: Object3D 9 | element: HTMLElement 10 | private offset = 1.7; 11 | private pos = new Vector3(-this.offset, 0, 0); 12 | private normal = new Vector3(-1, 0, 0); 13 | private cNormal = new Vector3(); 14 | private cPos = new Vector3(); 15 | private m4 = new Matrix4(); 16 | private timer: any; 17 | constructor(private base: Base) { 18 | this.setElement(); 19 | } 20 | setElement() { 21 | this.element = this.base.domBundle.querySelector('#clock'); 22 | this.object = new CSS3DObject(this.element); 23 | this.object.position.set(-this.offset, 0, 0); 24 | this.object.rotation.y = - Math.PI / 2; 25 | this.object.scale.set(1 / 160, 1 / 160, 1); 26 | this.initClock(); 27 | } 28 | 29 | private updateClock() { 30 | const now = new Date(); 31 | const 32 | sec = now.getSeconds(), 33 | min = now.getMinutes(), 34 | hou = now.getHours(), 35 | mo = now.getMonth(), 36 | dy = now.getDate(), 37 | yr = now.getFullYear(); 38 | const months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; 39 | const tags = ["mon", "d", "y", "h", "m", "s"], 40 | corr = [months[mo], dy, yr, pad(hou, 2), pad(min, 2), pad(sec, 2)]; 41 | for (let i = 0; i < tags.length; i++) 42 | this.element.querySelector(`#${tags[i]}`).innerHTML = corr[i].toString(); 43 | } 44 | 45 | private initClock() { 46 | this.updateClock();//先執行一次避免開場有破綻 47 | this.timer = setInterval(this.updateClock.bind(this), 1000); 48 | } 49 | 50 | update() { 51 | updateOcclude(this); 52 | } 53 | } -------------------------------------------------------------------------------- /src/scss/components/_curtain-call.scss: -------------------------------------------------------------------------------- 1 | .curtain-call { 2 | position: fixed; 3 | width: 100%; 4 | height: 100%; 5 | top: 0; 6 | left: 0; 7 | z-index: 999; 8 | background-color: rgb(43, 43, 43); 9 | opacity: 1; 10 | transition: opacity 500ms 0.5s; 11 | display: flex; 12 | 13 | * { 14 | user-select: none; 15 | } 16 | 17 | &--hide { 18 | opacity: 0; 19 | pointer-events: none; 20 | } 21 | 22 | &__inner { 23 | flex: none; 24 | width: 100%; 25 | height: 100%; 26 | display: flex; 27 | align-items: center; 28 | justify-content: center; 29 | flex-direction: column; 30 | } 31 | 32 | &__title { 33 | font-size: 1.5rem; 34 | font-family: 'Roboto Condensed'; 35 | color: $white; 36 | opacity: 0.5; 37 | transition: opacity 500ms; 38 | 39 | @include hasHover() { 40 | &:hover { 41 | opacity: 0.75; 42 | } 43 | } 44 | 45 | } 46 | 47 | &__img { 48 | width: 100px; 49 | 50 | img { 51 | width: 100%; 52 | user-select: none; 53 | } 54 | } 55 | 56 | &__img~&__title { 57 | margin-top: 20px; 58 | } 59 | 60 | &__img~&__loading { 61 | margin-top: 30px; 62 | } 63 | 64 | &__loading { 65 | width: 200px; 66 | height: 10px; 67 | border-radius: 20px; 68 | background-color: rgb(36, 36, 36); 69 | overflow: hidden; 70 | position: relative; 71 | 72 | &:before { 73 | content: ''; 74 | display: block; 75 | position: absolute; 76 | width: 100%; 77 | left: 0; 78 | top: 0; 79 | height: 100%; 80 | background-color: transparentize($white, 0.5); 81 | transform: scaleX(0); 82 | transition: transform 0.5s; 83 | transform-origin: 0% 50%; 84 | } 85 | } 86 | 87 | &--hide &__loading::before { 88 | transform: scaleX(1); 89 | } 90 | } -------------------------------------------------------------------------------- /src/scss/layout/_layout.scss: -------------------------------------------------------------------------------- 1 | $chatWidth: 400px; 2 | $chatWidthMobile: 300px; 3 | 4 | .wrapper { 5 | height: 100%; 6 | width: 100%; 7 | overflow: hidden; 8 | position: fixed; 9 | z-index: 5; 10 | left: 0; 11 | top: 0; 12 | 13 | &__inner { 14 | display: flex; 15 | height: 100%; 16 | width: 100%; 17 | transform: none; 18 | transition: transform 500ms; 19 | } 20 | 21 | &--active &__inner { 22 | transform: translateX(-#{$chatWidth}); 23 | 24 | @include rwd($screen-pad-portrait) { 25 | transform: translateX(-#{$chatWidthMobile}); 26 | } 27 | } 28 | 29 | &__canvas-block { 30 | width: 100%; 31 | height: 100%; 32 | flex: none; 33 | position: relative; 34 | z-index: 2; 35 | overflow: hidden; 36 | } 37 | 38 | &--active &__canvas { 39 | transform: translateX(#{$chatWidth/2}); 40 | 41 | @include rwd($screen-pad-portrait) { 42 | transform: translateX(#{$chatWidthMobile/2}); 43 | } 44 | } 45 | 46 | &--active &__dom-canvas { 47 | transform: translateX(#{$chatWidth/2}); 48 | 49 | @include rwd($screen-pad-portrait) { 50 | transform: translateX(#{$chatWidthMobile/2}); 51 | } 52 | } 53 | 54 | &__canvas { 55 | height: 100%; 56 | width: 100%; 57 | position: absolute; 58 | top: 0; 59 | transition: transform 500ms; 60 | } 61 | 62 | &__dom-canvas { 63 | transition: transform 500ms; 64 | position: relative; 65 | z-index: 2; 66 | 67 | .music-player { 68 | z-index: 4; 69 | } 70 | 71 | .chat-main { 72 | z-index: 3; 73 | } 74 | 75 | .clock { 76 | z-index: 2; 77 | } 78 | 79 | .space-invader { 80 | z-index: 1; 81 | } 82 | 83 | } 84 | 85 | &__chat-block { 86 | flex: none; 87 | width: $chatWidth; 88 | height: 100%; 89 | box-shadow: 19px -1px 30px -17px rgba(0, 0, 0, 0.5) inset; 90 | 91 | @include rwd($screen-pad-portrait) { 92 | width: $chatWidthMobile; 93 | } 94 | } 95 | } 96 | 97 | .dom-bundle { 98 | position: absolute; 99 | left: -1000vw !important; 100 | transform: translateZ(0); 101 | opacity: 0 !important; 102 | visibility: hidden !important; 103 | pointer-events: none !important; 104 | transform: translateZ(0) !important; 105 | } -------------------------------------------------------------------------------- /src/template/head.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= title%> 6 | 7 | <% const 8 | description='"3D-Cube-Chat" is a chat app which based on three.js and socket.io, and the users can even listen music via this app.' 9 | %> 10 | <% const url='https://mizok.github.io/3d-Cube-Chat/' %> 11 | <% const author='Mizok.H' %> 12 | <% const openGraphImageUrl='https://mizok.github.io/3d-Cube-Chat/assets/images/og-image.jpg' %> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | <%= title%> 37 | 38 | 39 | 40 | 43 | <%- include('./ga.ejs')%> 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Microbundle cache 58 | .rpt2_cache/ 59 | .rts2_cache_cjs/ 60 | .rts2_cache_es/ 61 | .rts2_cache_umd/ 62 | 63 | # Optional REPL history 64 | .node_repl_history 65 | 66 | # Output of 'npm pack' 67 | *.tgz 68 | 69 | # Yarn Integrity file 70 | .yarn-integrity 71 | 72 | # dotenv environment variables file 73 | .env 74 | .env.test 75 | .env.production 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | .parcel-cache 80 | 81 | # Next.js build output 82 | .next 83 | out 84 | 85 | # Nuxt.js build / generate output 86 | .nuxt 87 | dist 88 | 89 | # Gatsby files 90 | .cache/ 91 | # Comment in the public line in if your project uses Gatsby and not Next.js 92 | # https://nextjs.org/blog/next-9-1#public-directory-support 93 | # public 94 | 95 | # vuepress build output 96 | .vuepress/dist 97 | 98 | # Serverless directories 99 | .serverless/ 100 | 101 | # FuseBox cache 102 | .fusebox/ 103 | 104 | # DynamoDB Local files 105 | .dynamodb/ 106 | 107 | # TernJS port file 108 | .tern-port 109 | 110 | # Stores VSCode versions used for testing VSCode extensions 111 | .vscode-test 112 | 113 | # yarn v2 114 | .yarn/cache 115 | .yarn/unplugged 116 | .yarn/build-state.yml 117 | .yarn/install-state.gz 118 | .pnp.* 119 | 120 | # .DS_Store 121 | .DS_Store 122 | 123 | dist/** 124 | -------------------------------------------------------------------------------- /chat/index.js: -------------------------------------------------------------------------------- 1 | const {createServer} = require('http'); 2 | const {Server} = require("socket.io"); 3 | 4 | //app.js 5 | /*建立http服務*/ 6 | const app = createServer() 7 | /*引入socket.io*/ 8 | const io = new Server(app, { 9 | cors: { 10 | origin: ["https://mizok.github.io", "http://localhost:8080", "http://192.168.1.101:8080"], 11 | allowedHeaders: ["custom-header"], 12 | credentials: true, 13 | methods: ["GET", "POST"] 14 | } 15 | }) 16 | /*自訂監聽端口*/ 17 | const port = 5500; 18 | app.listen(port); 19 | 20 | console.log('app listen at ' + port) 21 | 22 | 23 | 24 | 25 | 26 | /*用戶陣列*/ 27 | let users= []; 28 | 29 | io.on('connection', (socket) => { 30 | /*是否為新用戶*/ 31 | let isNewPerson = true; 32 | /*當前登入用戶*/ 33 | let username= null; 34 | 35 | //監聽登入 36 | socket.on('login', (data) => { 37 | for (var i = 0; i < users.length; i++) { 38 | isNewPerson = (users[i].username === data.username) ? false : true; 39 | } 40 | if (isNewPerson) { 41 | username = data.username 42 | users.push({ 43 | username: data.username, 44 | id: socket.id 45 | }) 46 | data.userCount = users.length 47 | data.users = users; 48 | /*發送 登入成功 事件*/ 49 | socket.emit('loginSuccess', data) 50 | /*向所有連接的用戶廣播 add 事件*/ 51 | io.sockets.emit('add', data); 52 | } else { 53 | /*發送 登入失敗 事件*/ 54 | socket.emit('loginFail', ''); 55 | socket.disconnect(); 56 | } 57 | }) 58 | 59 | //監聽登出 60 | socket.on('logout', (data) => { 61 | /* 發送 離開成功 事件 */ 62 | 63 | socket.emit('leaveSuccess') 64 | /* 向所有連接的用戶廣播 有人登出 */ 65 | users = users.filter((val) => { 66 | return (val.username !== data.username) 67 | }) 68 | io.sockets.emit('leave', { username: data.username, userCount: users.length,users:users }); 69 | socket.disconnect(); 70 | }) 71 | 72 | socket.on('disconnect', () => { 73 | socket.emit('leaveSuccess') 74 | 75 | const userLeft = users.filter((val) => { 76 | return (val.id === socket.id) 77 | })[0]?.username 78 | 79 | users = users.filter((val) => { 80 | return (val.id !== socket.id) 81 | }) 82 | 83 | io.sockets.emit('leave', { username: userLeft, userCount: users.length,users:users }) 84 | }) 85 | 86 | socket.on('sendMessage', function (data) { 87 | /*發送receiveMessage事件*/ 88 | io.sockets.emit('receiveMessage', data) 89 | }) 90 | }) 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webpack_playground", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "npx cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack-config.json\" webpack --config webpack.config.ts --mode production", 9 | "dev": "npx cross-env TS_NODE_PROJECT=\"tsconfig-for-webpack-config.json\" webpack serve --config webpack.config.ts --mode development", 10 | "deploy": "npm run build && npx gh-pages -d dist" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/mizok/webpack_playground.git" 15 | }, 16 | "author": "", 17 | "license": "ISC", 18 | "bugs": { 19 | "url": "https://github.com/mizok/webpack_playground/issues" 20 | }, 21 | "homepage": "https://github.com/mizok/webpack_playground#readme", 22 | "devDependencies": { 23 | "@babel/core": "^7.19.3", 24 | "@babel/plugin-syntax-top-level-await": "^7.14.5", 25 | "@types/gsap": "^3.0.0", 26 | "@types/lodash": "^4.14.186", 27 | "@types/three": "^0.144.0", 28 | "@types/webpack-dev-server": "^4.7.2", 29 | "assert": "^2.0.0", 30 | "babel-loader": "^8.2.5", 31 | "browserify-zlib": "^0.2.0", 32 | "buffer": "^6.0.3", 33 | "copy-webpack-plugin": "^9.0.1", 34 | "cross-env": "^7.0.3", 35 | "css-loader": "^5.2.1", 36 | "css-minimizer-webpack-plugin": "^4.0.0", 37 | "ejs": "^3.1.6", 38 | "gh-pages": "^3.2.3", 39 | "html-loader": "^3.0.1", 40 | "html-webpack-plugin": "^5.3.1", 41 | "https-browserify": "^1.0.0", 42 | "mini-css-extract-plugin": "^1.5.0", 43 | "postcss-loader": "^5.2.0", 44 | "postcss-preset-env": "^6.7.0", 45 | "process": "^0.11.10", 46 | "sass": "^1.33.0", 47 | "sass-loader": "^12.0.0", 48 | "stream-browserify": "^3.0.0", 49 | "stream-http": "^3.2.0", 50 | "style-loader": "^2.0.0", 51 | "template-ejs-loader": "latest", 52 | "terser-webpack-plugin": "^5.3.1", 53 | "ts-loader": "^9.2.6", 54 | "ts-node": "^10.4.0", 55 | "tsconfig-paths": "^3.12.0", 56 | "typescript": "^4.5.5", 57 | "url": "^0.11.0", 58 | "webpack": "^5.31.2", 59 | "webpack-cli": "^4.9.1", 60 | "webpack-dev-server": "^4.7.2" 61 | }, 62 | "dependencies": { 63 | "axios": "^1.1.3", 64 | "es6-promise": "^4.2.8", 65 | "gsap": "^3.11.3", 66 | "is-mobile": "^3.1.1", 67 | "lodash": "^4.17.21", 68 | "socket.io-client": "^4.5.2", 69 | "soundcloud-key-fetch": "^1.0.13", 70 | "soundcloud-widget": "^0.2.1", 71 | "three": "^0.145.0" 72 | }, 73 | "browserslist": [ 74 | "last 2 version", 75 | "> 1%", 76 | "IE 10" 77 | ] 78 | } 79 | -------------------------------------------------------------------------------- /src/ts/mesh/dom-cube.ts: -------------------------------------------------------------------------------- 1 | import { Group } from "three"; 2 | import { Base } from "../class/base"; 3 | import { Chat } from '../dom'; 4 | import { SpaceInvader } from '../dom/space-inavader'; 5 | import gsap from "gsap"; 6 | import { Clock } from "../dom/clock"; 7 | import { cubeLikeConfig, getTargetAngle } from "../util/function"; 8 | import { Music } from "../dom/music"; 9 | 10 | export class DomCube { 11 | chat: Chat; 12 | clock: Clock; 13 | music: Music; 14 | spaceInvader: SpaceInvader; 15 | groupOuter = new Group(); 16 | groupInner = new Group(); 17 | constructor(private base: Base) { 18 | this.init(); 19 | } 20 | 21 | 22 | init() { 23 | this.chat = new Chat(this.base); 24 | this.clock = new Clock(this.base); 25 | this.music = new Music(this.base); 26 | this.spaceInvader = new SpaceInvader(this.base); 27 | this.groupInner.scale.set(...cubeLikeConfig.initialScale); 28 | this.groupInner.rotation.set(...cubeLikeConfig.initialRotation); 29 | this.groupInner.add(this.chat.object); 30 | this.groupInner.add(this.clock.object); 31 | this.groupInner.add(this.music.object); 32 | this.groupInner.add(this.spaceInvader.object); 33 | this.groupOuter.add(this.groupInner); 34 | this.base.scene2.add(this.groupOuter); 35 | this.doAnimation(); 36 | } 37 | 38 | private doAnimation() { 39 | gsap.to(this.groupInner.rotation, cubeLikeConfig.startAnimationInnerRotationConfig) 40 | gsap.to(this.groupInner.scale, cubeLikeConfig.startAnimationInnerScalingConfig) 41 | } 42 | 43 | showChat() { 44 | gsap.to(this.groupInner.rotation, cubeLikeConfig.showChatAnimationInnerRotationConfig); 45 | const angle = getTargetAngle(this.groupOuter.rotation.y) 46 | gsap.to(this.groupOuter.rotation, { 47 | x: 0, 48 | y: angle, 49 | z: 0, 50 | duration: 2 51 | }) 52 | } 53 | showMusic() { 54 | gsap.to(this.groupInner.rotation, cubeLikeConfig.showMusicAnimationInnerRotationConfig); 55 | const angle = getTargetAngle(this.groupOuter.rotation.y) 56 | gsap.to(this.groupOuter.rotation, { 57 | x: 0, 58 | y: angle - Math.PI / 4, 59 | z: 0, 60 | duration: 2 61 | }) 62 | } 63 | 64 | update(delta: number) { 65 | if (!this.base.touched && !this.base.getRotationLockStatus()) { 66 | this.groupOuter.rotation.y += delta / cubeLikeConfig.updateParameter; 67 | } 68 | this.chat.update(); 69 | this.clock.update(); 70 | this.music.update(); 71 | this.spaceInvader.update(); 72 | } 73 | } -------------------------------------------------------------------------------- /src/assets/images/no-result.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/ts/class/base.ts: -------------------------------------------------------------------------------- 1 | import { Renderer } from './renderer'; 2 | import { Camera } from './camera'; 3 | import { Ticker, Sizer } from '../util'; 4 | import { Scene, Clock, Vector2 } from 'three'; 5 | import { getResources } from '../resource'; 6 | import { Playground } from './playground'; 7 | 8 | let rotationLocked: boolean = false; 9 | let loginStatus: boolean = false; 10 | 11 | export class Base { 12 | sizer = new Sizer(this.canvas) 13 | scene = new Scene(); 14 | scene2 = new Scene(); 15 | ticker = new Ticker(); 16 | camera = new Camera(this); 17 | renderer = new Renderer(this); 18 | playground = new Playground(this); 19 | touched = false; 20 | resources: { 21 | [key: string]: any 22 | } 23 | private touchedReactDelay = 2000; 24 | 25 | 26 | constructor(public canvas: HTMLCanvasElement, public domCanvas: HTMLElement, public domBundle: HTMLElement) { 27 | this.initResizeMechanic(); 28 | this.initTickMechanic(); 29 | this.initTouchMechanic(); 30 | } 31 | 32 | private initResizeMechanic() { 33 | this.sizer.on('resize', () => { 34 | this.renderer.resize(); 35 | this.camera.resize(); 36 | }) 37 | } 38 | 39 | private initTickMechanic() { 40 | this.ticker.on('tick', (clock: Clock) => { 41 | const delta = clock.getDelta(); 42 | this.renderer.update(); 43 | this.camera.update(); 44 | this.playground.update(delta); 45 | }) 46 | } 47 | 48 | 49 | private initTouchMechanic() { 50 | let startLocation = new Vector2(); 51 | let endLocation = new Vector2(); 52 | let timeout: any; 53 | const cbStart = (e: MouseEvent) => { 54 | startLocation.x = e.clientX; 55 | startLocation.y = e.clientY; 56 | this.touched = true; 57 | } 58 | const cbEnd = (e: MouseEvent) => { 59 | endLocation.x = e.clientX; 60 | endLocation.y = e.clientY; 61 | const delay = startLocation.distanceTo(endLocation) > 5 ? this.touchedReactDelay : 0; 62 | clearTimeout(timeout) 63 | timeout = setTimeout(() => { 64 | this.touched = false; 65 | }, delay) 66 | } 67 | this.domCanvas.addEventListener('mousedown', cbStart) 68 | this.domCanvas.addEventListener('touchstart', cbStart) 69 | this.domCanvas.addEventListener('mouseup', cbEnd) 70 | this.domCanvas.addEventListener('touchend', cbEnd) 71 | this.domCanvas.addEventListener('mouseleave', cbEnd) 72 | } 73 | 74 | setRotationLock(status: boolean) { 75 | rotationLocked = status; 76 | } 77 | 78 | 79 | getRotationLockStatus() { 80 | return rotationLocked; 81 | } 82 | 83 | setLoginStatus(status: boolean) { 84 | loginStatus = status; 85 | } 86 | 87 | getLoginStatus() { 88 | return loginStatus; 89 | } 90 | 91 | async getResources() { 92 | this.resources = await getResources() 93 | } 94 | } -------------------------------------------------------------------------------- /src/ts/dom/chat.ts: -------------------------------------------------------------------------------- 1 | import { Matrix4, Object3D, Scene, Vector3 } from "three"; 2 | import { CSS3DObject } from "three/examples/jsm/renderers/CSS3DRenderer"; 3 | import { Base } from "../class/base"; 4 | import { FaceType, ShowScreenTargets } from "../interface"; 5 | import { EventEmitter } from "../util"; 6 | import { updateOcclude } from "../util/function"; 7 | 8 | 9 | export class Chat extends EventEmitter implements FaceType { 10 | object: Object3D 11 | element: HTMLElement; 12 | chatMainInner: HTMLElement; 13 | loginGuide: HTMLElement; 14 | guestList: HTMLElement; 15 | private loginGuideBtn: HTMLElement; 16 | private offset = 1.7; 17 | private pos = new Vector3(0, 0, this.offset); 18 | private normal = new Vector3(0, 0, 1); 19 | private cNormal = new Vector3(); 20 | private cPos = new Vector3(); 21 | private m4 = new Matrix4(); 22 | private timer: any; 23 | constructor(private base: Base) { 24 | super(); 25 | this.setElement(); 26 | } 27 | setElement() { 28 | this.element = this.base.domBundle.querySelector('#chat-main-cube'); 29 | this.chatMainInner = this.element.querySelector('#chat-main-inner'); 30 | this.loginGuide = this.element.querySelector('#login-guide'); 31 | this.guestList = this.element.querySelector('#guest-list'); 32 | this.loginGuideBtn = this.element.querySelector('#login-guide-button'); 33 | this.object = new CSS3DObject(this.element); 34 | this.object.position.set(0, 0, this.offset); 35 | this.object.scale.set(1 / 160, 1 / 160, 1); 36 | this.bindElementEvents(); 37 | } 38 | 39 | showScreen(target: ShowScreenTargets) { 40 | const hideClass = 'chat-main__inner--hide'; 41 | const targets = ['chatMainInner', 'loginGuide', 'guestList']; 42 | this[target].classList.remove(hideClass); 43 | targets.filter((val) => val !== target).forEach((unchosen) => { 44 | (this[unchosen as keyof this] as (HTMLElement | any))?.classList.add(hideClass) 45 | }) 46 | } 47 | 48 | refreshGuestList(list: { username: string, id: string }[]) { 49 | const containerEle = this.guestList.querySelector('#guest-list-container'); 50 | containerEle.innerHTML = ''; 51 | let total = ''; 52 | const transitionGap = 200; 53 | list.forEach((guest, index) => { 54 | const item = 55 | ` 56 |
  • 57 |
    ${guest.username}
    58 |
  • 59 | ` 60 | total += item; 61 | }) 62 | containerEle.innerHTML = total; 63 | } 64 | 65 | private bindElementEvents() { 66 | this.loginGuideBtn.addEventListener('pointerdown', () => { 67 | this.trigger('cube-login-button-click'); 68 | }) 69 | } 70 | 71 | setGuestNumber(data: number) { 72 | const containerEle = this.guestList.querySelector('#guest-number'); 73 | containerEle.innerHTML = `NOW ONLINE: ${data}` 74 | } 75 | 76 | 77 | update() { 78 | updateOcclude(this); 79 | } 80 | } -------------------------------------------------------------------------------- /archive/grid.txt: -------------------------------------------------------------------------------- 1 | import * as THREE from "three"; 2 | 3 | export class InfiniteGridHelper extends THREE.Mesh { 4 | 5 | constructor ( size1, size2, color, distance, axes = 'xzy' ) { 6 | 7 | 8 | color = color || new THREE.Color( 'white' ); 9 | size1 = size1 || 10; 10 | size2 = size2 || 100; 11 | 12 | distance = distance || 8000; 13 | 14 | 15 | 16 | const planeAxes = axes.substr( 0, 2 ); 17 | 18 | const geometry = new THREE.PlaneBufferGeometry( 2, 2, 1, 1 ); 19 | 20 | const material = new THREE.ShaderMaterial( { 21 | 22 | side: THREE.DoubleSide, 23 | 24 | uniforms: { 25 | uSize1: { 26 | value: size1 27 | }, 28 | uSize2: { 29 | value: size2 30 | }, 31 | uColor: { 32 | value: color 33 | }, 34 | uDistance: { 35 | value: distance 36 | } 37 | }, 38 | transparent: true, 39 | vertexShader: ` 40 | 41 | varying vec3 worldPosition; 42 | 43 | uniform float uDistance; 44 | 45 | void main() { 46 | 47 | vec3 pos = position.${axes} * uDistance; 48 | pos.${planeAxes} += cameraPosition.${planeAxes}; 49 | 50 | worldPosition = pos; 51 | 52 | gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0); 53 | 54 | } 55 | `, 56 | 57 | 58 | fragmentShader: ` 59 | 60 | varying vec3 worldPosition; 61 | 62 | uniform float uSize1; 63 | uniform float uSize2; 64 | uniform vec3 uColor; 65 | uniform float uDistance; 66 | 67 | 68 | 69 | float getGrid(float size) { 70 | 71 | vec2 r = worldPosition.${planeAxes} / size; 72 | 73 | 74 | vec2 grid = abs(fract(r - 0.5) - 0.5) / fwidth(r); 75 | float line = min(grid.x, grid.y); 76 | 77 | 78 | return 1.0 - min(line, 1.0); 79 | } 80 | 81 | void main() { 82 | 83 | 84 | float d = 1.0 - min(distance(cameraPosition.${planeAxes}, worldPosition.${planeAxes}) / uDistance, 1.0); 85 | 86 | float g1 = getGrid(uSize1); 87 | float g2 = getGrid(uSize2); 88 | 89 | 90 | gl_FragColor = vec4(uColor.rgb, mix(g2, g1, g1) * pow(d, 3.0)); 91 | gl_FragColor.a = mix(0.5 * gl_FragColor.a, gl_FragColor.a, g2); 92 | 93 | if ( gl_FragColor.a <= 0.0 ) discard; 94 | 95 | 96 | } 97 | 98 | `, 99 | 100 | extensions: { 101 | derivatives: true 102 | } 103 | 104 | } ); 105 | 106 | super( geometry, material ); 107 | 108 | this.frustumCulled = false; 109 | 110 | } 111 | 112 | } 113 | 114 | 115 | -------------------------------------------------------------------------------- /src/ts/mesh/cube.ts: -------------------------------------------------------------------------------- 1 | import gsap from "gsap"; 2 | import { ExtrudeGeometry, Group, Mesh, MeshMatcapMaterial, Shape } from "three"; 3 | import { Base } from "../class/base"; 4 | import { cubeLikeConfig, getTargetAngle } from "../util/function"; 5 | import { MeshType } from "../interface"; 6 | 7 | export class Cube implements MeshType { 8 | mesh: Mesh; 9 | group: Group; 10 | constructor(private base: Base) { 11 | this.setModel(); 12 | } 13 | 14 | private createRoundedBoxGeo(width: number, height: number, depth: number, radius0: number, smoothness: number) { 15 | let shape = new Shape(); 16 | let eps = 0.00001; 17 | let radius = radius0 - eps; 18 | let faceRadius = 0.25; 19 | shape.absarc(eps, eps, faceRadius, -Math.PI / 2, -Math.PI, true); 20 | shape.absarc(eps, height - radius * 2, faceRadius, Math.PI, Math.PI / 2, true); 21 | shape.absarc(width - radius * 2, height - radius * 2, faceRadius, Math.PI / 2, 0, true); 22 | shape.absarc(width - radius * 2, eps, faceRadius, 0, -Math.PI / 2, true); 23 | let geometry = new ExtrudeGeometry(shape, { 24 | depth: depth - radius0, 25 | bevelEnabled: true, 26 | bevelSegments: smoothness * 2, 27 | steps: 1, 28 | bevelSize: radius, 29 | bevelThickness: radius0, 30 | curveSegments: smoothness 31 | }); 32 | 33 | geometry.center(); 34 | 35 | return geometry; 36 | } 37 | 38 | setModel() { 39 | const geo = this.createRoundedBoxGeo(3, 3, 3, 0.4, 20); 40 | const mat = new MeshMatcapMaterial({ 41 | matcap: this.base.resources.cubeMatcap 42 | }) 43 | this.group = new Group(); 44 | 45 | this.mesh = new Mesh(geo, mat); 46 | this.mesh.scale.set(...cubeLikeConfig.initialScale); 47 | this.mesh.rotation.set(...cubeLikeConfig.initialRotation); 48 | 49 | this.group.add(this.mesh); 50 | 51 | this.base.scene.add(this.group); 52 | 53 | this.doAnimation(); 54 | } 55 | 56 | private doAnimation() { 57 | gsap.to(this.mesh.rotation, cubeLikeConfig.startAnimationInnerRotationConfig) 58 | gsap.to(this.mesh.scale, cubeLikeConfig.startAnimationInnerScalingConfig) 59 | } 60 | 61 | showChat() { 62 | gsap.to(this.base.camera.instance.position, { 63 | x: this.base.camera.bestPoint.x, 64 | y: this.base.camera.bestPoint.y, 65 | z: this.base.camera.bestPoint.z, 66 | duration: 4, 67 | onStart: () => { 68 | this.base.camera.controls.enabled = false; 69 | this.base.camera.controls.enableDamping = false; 70 | }, 71 | onComplete: () => { 72 | this.base.camera.controls.enabled = true; 73 | this.base.camera.controls.enableDamping = true; 74 | } 75 | }) 76 | gsap.to(this.mesh.rotation, cubeLikeConfig.showChatAnimationInnerRotationConfig) 77 | const angle = getTargetAngle(this.group.rotation.y) 78 | gsap.to(this.group.rotation, { 79 | x: 0, 80 | y: angle, 81 | z: 0, 82 | duration: 2 83 | }) 84 | 85 | 86 | 87 | } 88 | 89 | showMusic() { 90 | gsap.to(this.base.camera.instance.position, { 91 | x: this.base.camera.bestPoint.x, 92 | y: this.base.camera.bestPoint.y, 93 | z: this.base.camera.bestPoint.z, 94 | duration: 4, 95 | onStart: () => { 96 | this.base.camera.controls.enabled = false; 97 | this.base.camera.controls.enableDamping = false; 98 | }, 99 | onComplete: () => { 100 | this.base.camera.controls.enabled = true; 101 | this.base.camera.controls.enableDamping = true; 102 | } 103 | }) 104 | gsap.to(this.mesh.rotation, cubeLikeConfig.showMusicAnimationInnerRotationConfig) 105 | const angle = getTargetAngle(this.group.rotation.y) 106 | gsap.to(this.group.rotation, { 107 | x: 0, 108 | y: angle - Math.PI / 4, 109 | z: 0, 110 | duration: 2 111 | }) 112 | 113 | } 114 | 115 | update(delta: number) { 116 | if (!this.base.touched && !this.base.getRotationLockStatus()) { 117 | this.group.rotation.y += delta / cubeLikeConfig.updateParameter; 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /src/scss/components/_music.scss: -------------------------------------------------------------------------------- 1 | .music-iframe { 2 | position: absolute; 3 | left: -1000vw !important; 4 | will-change: transform; 5 | transform: translateZ(0); 6 | 7 | iframe { 8 | opacity: 0 !important; 9 | visibility: hidden !important; 10 | pointer-events: none !important; 11 | transform: translateZ(0) !important; 12 | } 13 | } 14 | 15 | .music-player { 16 | width: 380px; 17 | height: 380px; 18 | border-radius: 5px; 19 | overflow: hidden; 20 | opacity: 0.35; 21 | transition: opacity 500ms; 22 | border: 3px solid #ff5500; 23 | display: flex; 24 | 25 | @include hasHover() { 26 | &:hover { 27 | opacity: 0.5; 28 | } 29 | } 30 | 31 | &__inner { 32 | position: relative; 33 | z-index: 2; 34 | padding: 10px; 35 | flex: none; 36 | width: 100%; 37 | height: 100%; 38 | flex-direction: column; 39 | display: flex; 40 | } 41 | 42 | &__head { 43 | display: flex; 44 | align-items: center; 45 | flex: none; 46 | } 47 | 48 | &__body { 49 | flex: 1; 50 | display: flex; 51 | justify-content: flex-end; 52 | flex-direction: column; 53 | 54 | } 55 | 56 | 57 | &__head &__former { 58 | flex: none; 59 | } 60 | 61 | &__head &__latter { 62 | flex: 1; 63 | overflow: hidden; 64 | } 65 | 66 | &__head &__former+&__latter { 67 | margin-left: 10px; 68 | } 69 | 70 | &__img { 71 | width: 100%; 72 | height: 100%; 73 | position: absolute; 74 | background-size: cover; 75 | background-position: center; 76 | top: 0; 77 | left: 0; 78 | z-index: 1; 79 | user-select: none; 80 | 81 | 82 | &:before, 83 | &:after { 84 | content: ''; 85 | display: block; 86 | height: 100px; 87 | width: 100%; 88 | position: absolute; 89 | top: 0; 90 | background: linear-gradient(0deg, rgba(78, 73, 152, 0) 0%, rgba(0, 0, 0, 1) 100%); 91 | z-index: 2; 92 | } 93 | 94 | &:after { 95 | top: auto; 96 | bottom: 0; 97 | background: linear-gradient(180deg, rgba(78, 73, 152, 0) 0%, rgba(0, 0, 0, 1) 100%); 98 | } 99 | 100 | img { 101 | user-select: none; 102 | pointer-events: none; 103 | } 104 | } 105 | 106 | &__btn { 107 | width: 50px; 108 | height: 50px; 109 | border-radius: 50%; 110 | background-size: cover; 111 | cursor: pointer; 112 | transition: box-shadow 1s, opacity 500ms; 113 | opacity: 0.75; 114 | background-color: transparent; 115 | border: none; 116 | position: relative; 117 | 118 | &:before, 119 | &:after { 120 | content: ''; 121 | display: block; 122 | position: absolute; 123 | top: 0; 124 | width: 100%; 125 | height: 100%; 126 | border-radius: 50%; 127 | background-size: cover; 128 | transition: opacity 500ms; 129 | } 130 | 131 | &:before { 132 | background-image: url(~@img/play-alpha.svg); 133 | } 134 | 135 | &:after { 136 | background-image: url(~@img/pause-alpha.svg); 137 | opacity: 0; 138 | } 139 | 140 | @include hasHover() { 141 | &:hover { 142 | box-shadow: 143 | 6.7px 6.7px 5.3px rgba(0, 0, 0, 0.1); 144 | opacity: 1; 145 | } 146 | } 147 | 148 | &--pause::before { 149 | opacity: 0; 150 | } 151 | 152 | &--pause::after { 153 | opacity: 1; 154 | } 155 | } 156 | 157 | &__metadata { 158 | * { 159 | font-family: 'Roboto Condensed'; 160 | color: $white; 161 | } 162 | } 163 | 164 | &__title { 165 | display: block; 166 | font-size: 1rem; 167 | white-space: nowrap; 168 | 169 | span { 170 | @include text-ellipsis(); 171 | display: inline-block; 172 | width: 100%; 173 | } 174 | 175 | @include hasHover() { 176 | &:hover span { 177 | animation: marquee 10s linear infinite; 178 | animation-delay: 1s; 179 | } 180 | } 181 | } 182 | 183 | &__artist { 184 | display: block; 185 | font-size: 0.75rem; 186 | white-space: nowrap; 187 | 188 | span { 189 | display: inline-block; 190 | } 191 | } 192 | 193 | &__title+&__descrp { 194 | margin-top: 5px; 195 | } 196 | 197 | &__waveform { 198 | cursor: pointer; 199 | } 200 | 201 | &__body &__waveform { 202 | height: 100px; 203 | width: 100%; 204 | flex: none; 205 | } 206 | 207 | &__waveform+&__credit-bar { 208 | margin-top: 5px; 209 | border-top: 1px solid transparentize($white, 0.5); 210 | } 211 | 212 | &__credit-bar { 213 | padding-top: 5px; 214 | display: flex; 215 | justify-content: space-between; 216 | align-items: center; 217 | } 218 | 219 | &__body &__brand { 220 | font-size: 0.75rem; 221 | color: $white; 222 | } 223 | 224 | &__body &__credit { 225 | width: 100px; 226 | 227 | img { 228 | width: 100%; 229 | } 230 | } 231 | } -------------------------------------------------------------------------------- /src/scss/reset.scss: -------------------------------------------------------------------------------- 1 | @import './main'; 2 | 3 | 4 | * { 5 | padding: 0; 6 | margin: 0; 7 | font-family: $text-font-stack; 8 | box-sizing: border-box; 9 | -webkit-overflow-scrolling: touch; 10 | -webkit-appearance: none; 11 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 12 | 13 | &:focus { 14 | outline: 0; 15 | } 16 | } 17 | 18 | *, 19 | *::before, 20 | *::after { 21 | box-sizing: border-box; 22 | } 23 | 24 | /********************* global reset End*********************/ 25 | 26 | /********************* main HTML Structure reset Start*********************/ 27 | 28 | html { 29 | line-height: 1.15; 30 | /* 1 */ 31 | -webkit-text-size-adjust: none; 32 | /* 2 */ 33 | -ms-text-size-adjust: none; 34 | /* 3 */ 35 | } 36 | 37 | body { 38 | line-height: 1; 39 | color: black; 40 | touch-action: pan-x pan-y; 41 | } 42 | 43 | html, 44 | body { 45 | height: 100%; 46 | box-sizing: border-box; 47 | -ms-overflow-style: scrollbar; 48 | } 49 | 50 | main { 51 | display: block; 52 | } 53 | 54 | article, 55 | aside, 56 | footer, 57 | header, 58 | nav, 59 | section, 60 | figcaption, 61 | figure { 62 | display: block; 63 | } 64 | 65 | /********************* main HTML Structure reset End*********************/ 66 | 67 | /********************* Secondary HTML Element reset Start*********************/ 68 | 69 | /* tables still need 'cellspacing="0"' in the markup */ 70 | 71 | table { 72 | border-collapse: collapse; 73 | border-spacing: 0; 74 | 75 | caption, 76 | th, 77 | td { 78 | text-align: left; 79 | font-weight: normal; 80 | } 81 | 82 | th, 83 | td { 84 | vertical-align: top; 85 | } 86 | } 87 | 88 | // ordered-list,unordered-list reset 89 | 90 | ol, 91 | ul { 92 | list-style: none; 93 | } 94 | 95 | // paragraph reset 96 | 97 | p { 98 | color: $text-color; 99 | } 100 | 101 | // pre-render reset 102 | 103 | pre { 104 | font-family: $text-font-stack; 105 | /* 1 */ 106 | font-size: 1em; 107 | /* 2 */ 108 | white-space: pre-wrap; 109 | word-break: break-word; 110 | } 111 | 112 | /** 113 | * 1. Add the correct box sizing in Firefox. 114 | * 2. Show the overflow in Edge and IE. 115 | */ 116 | 117 | hr { 118 | box-sizing: content-box; 119 | /* 1 */ 120 | height: 0; 121 | /* 1 */ 122 | overflow: visible; 123 | /* 2 */ 124 | } 125 | 126 | /** 127 | * Remove the gray background on active links in IE 10. 128 | * Set display : inline-block as default. 129 | */ 130 | 131 | a { 132 | background-color: transparent; 133 | display: inline-block; 134 | color: $link-color; 135 | text-decoration: none; 136 | -webkit-text-decoration-skip: objects; 137 | 138 | /* 2 */ 139 | @include on-event { 140 | color: $text-color; 141 | text-decoration: underline; 142 | } 143 | 144 | &[href] { 145 | cursor: pointer; 146 | } 147 | } 148 | 149 | /** 150 | * Add the correct font weight in Chrome, Edge, and Safari. 151 | */ 152 | 153 | b, 154 | strong { 155 | font-weight: bolder; 156 | } 157 | 158 | blockquote, 159 | q { 160 | quotes: none; 161 | 162 | &:before, 163 | &:after { 164 | content: ""; 165 | content: none; 166 | } 167 | } 168 | 169 | /** 170 | * Remove the border on images inside links in IE 10. 171 | */ 172 | 173 | img { 174 | border-style: none; 175 | display: inline-block; 176 | width: 100%; 177 | } 178 | 179 | /** 180 | * Hide the overflow in IE. 181 | */ 182 | 183 | svg:not(:root) { 184 | overflow: hidden; 185 | } 186 | 187 | /** 188 | * 1. Change the font styles in all browsers. 189 | * 2. Remove the margin in Firefox and Safari. 190 | */ 191 | 192 | /********************* Secondary HTML Element reset End*********************/ 193 | 194 | /******************** Form Element Reset Start********************/ 195 | 196 | button, 197 | input, 198 | optgroup, 199 | select, 200 | textarea { 201 | font-family: inherit; 202 | /* 1 */ 203 | font-size: 100%; 204 | /* 1 */ 205 | line-height: 1.15; 206 | /* 1 */ 207 | margin: 0; 208 | /* 2 */ 209 | text-transform: none; 210 | } 211 | 212 | textarea { 213 | overflow: auto; 214 | resize: none; 215 | 216 | &:invalid { 217 | box-shadow: none; 218 | } 219 | } 220 | 221 | select { 222 | -webkit-appearance: none; 223 | appearance: none; 224 | border-radius: 0; 225 | } 226 | 227 | input { 228 | 229 | &:invalid { 230 | box-shadow: none; 231 | } 232 | 233 | &[type="password"]::-ms-reveal, 234 | &[type=""]::-ms-clear { 235 | display: none; 236 | } 237 | 238 | &:-webkit-input-placeholder, 239 | &:-ms-input-placeholder, 240 | &::-ms-input-placeholder, 241 | &::placeholder { 242 | font: inherit; 243 | } 244 | 245 | &[type="checkbox"], 246 | &[type="radio"] { 247 | box-sizing: border-box; 248 | /* 1 */ 249 | padding: 0; 250 | /* 2 */ 251 | } 252 | 253 | &[type="number"]::-webkit-inner-spin-button, 254 | &[type="number"]::-webkit-outer-spin-button { 255 | height: auto; 256 | } 257 | 258 | &[type="search"]::-webkit-search-decoration { 259 | -webkit-appearance: none; 260 | } 261 | } 262 | 263 | [type="button"], 264 | /* 1 */ 265 | [type="reset"], 266 | [type="submit"] { 267 | -webkit-appearance: button; 268 | /* 2 */ 269 | } 270 | 271 | label[for] { 272 | cursor: pointer; 273 | } 274 | 275 | legend { 276 | color: inherit; 277 | /* 2 */ 278 | display: table; 279 | /* 1 */ 280 | max-width: 100%; 281 | /* 1 */ 282 | padding: 0; 283 | /* 3 */ 284 | white-space: normal; 285 | /* 1 */ 286 | } 287 | 288 | /******************** Form Element Reset End********************/ 289 | 290 | template { 291 | display: none; 292 | } 293 | 294 | 295 | /********************* global reset Start*********************/ -------------------------------------------------------------------------------- /src/ts/dom/music.ts: -------------------------------------------------------------------------------- 1 | import { Matrix4, Object3D, Vector3 } from "three"; 2 | import { CSS3DObject } from "three/examples/jsm/renderers/CSS3DRenderer"; 3 | import { Base } from "../class/base"; 4 | import { FaceType } from "../interface"; 5 | import { updateOcclude } from "../util/function"; 6 | import { widget } from '../util/widget'; 7 | import { Waveform, EVENT_CLICK } from '../util/waveform' 8 | const axios = require('axios'); 9 | 10 | 11 | 12 | export class Music implements FaceType { 13 | object: Object3D 14 | element: HTMLElement 15 | private offset = 1.7; 16 | private pos = new Vector3(this.offset, 0, 0); 17 | private normal = new Vector3(1, 0, 0); 18 | private cNormal = new Vector3(); 19 | private cPos = new Vector3(); 20 | private m4 = new Matrix4(); 21 | private timer: any; 22 | private defaultSource = 'https%3A//api.soundcloud.com/tracks/327386009'; 23 | private playStatus = false; 24 | private waveformInstance: Waveform; 25 | constructor(private base: Base) { 26 | this.setElement(); 27 | } 28 | setElement() { 29 | this.element = this.base.domBundle.querySelector('#music-player'); 30 | this.object = new CSS3DObject(this.element); 31 | this.object.position.set(this.offset, 0, 0); 32 | this.object.rotation.y = Math.PI / 2; 33 | this.object.scale.set(1 / 160, 1 / 160, 1); 34 | this.init(); 35 | } 36 | 37 | private bindPlayerUIEvent() { 38 | const playButton = this.element.querySelector('#music-player-button') as HTMLElement; 39 | const banner = this.element.querySelector('#music-player-img img') as HTMLImageElement; 40 | const title = this.element.querySelector('#music-player-title span'); 41 | const artist = this.element.querySelector('#music-player-artist span'); 42 | const permaLink = this.element.querySelector('#music-player-perma-link') as HTMLAnchorElement; 43 | const waveform = this.element.querySelector('#music-player-waveform') as HTMLElement; 44 | const waveformRect = waveform.getBoundingClientRect(); 45 | const contrastFactor = 5; 46 | const contrastOffset = 128; 47 | 48 | playButton.addEventListener('pointerdown', () => { 49 | if (!this.playStatus) { 50 | widget.play() 51 | } 52 | else { 53 | widget.pause() 54 | } 55 | this.playStatus = !this.playStatus; 56 | }) 57 | 58 | widget.on('play', () => { 59 | playButton.classList.add('music-player__btn--pause') 60 | this.playStatus = true; 61 | }) 62 | 63 | widget.on('pause', () => { 64 | if (!this.waveformInstance) return; 65 | playButton.classList.remove('music-player__btn--pause') 66 | this.playStatus = false; 67 | this.waveformInstance.pause(); 68 | }) 69 | 70 | permaLink.addEventListener('pointerdown', (e) => { 71 | const href = (e.currentTarget as HTMLAnchorElement).href; 72 | window.open(href, '_blank'); 73 | }) 74 | 75 | 76 | widget.on('play-progress', (percent: number) => { 77 | if (!this.waveformInstance) return; 78 | this.waveformInstance.playProgress(percent); 79 | }) 80 | 81 | 82 | widget.on('load', (soundObject: any) => { 83 | let picUrl = ''; 84 | if (!!soundObject.artwork_url) { 85 | picUrl = soundObject.artwork_url.replace(/(.*)(-large)(\.[a-z0-9]{3}[a-z0-9]?)$/, '$1-t500x500$3') 86 | } 87 | else { 88 | picUrl = './assets/images/not-found.jpg'; 89 | } 90 | //image 91 | banner.src = picUrl 92 | //title 93 | title.innerHTML = soundObject?.title 94 | //artist 95 | artist.innerHTML = soundObject?.publisher_metadata?.artist || 'Unknown'; 96 | //soundcloud link 97 | permaLink.href = soundObject?.permalink_url; 98 | 99 | //get waveform data and render waveform diagram 100 | axios({ 101 | url: soundObject.waveform_url 102 | }) 103 | .then((res: any) => { 104 | waveform.innerHTML = ''; 105 | // some contrast calculation 106 | const data = res.data.samples.map((val: number) => ((val - contrastOffset) / 100) * contrastFactor + contrastOffset / 100); 107 | this.waveformInstance = new Waveform( 108 | { 109 | container: waveform, 110 | data: data, 111 | width: waveformRect.width, 112 | height: waveformRect.height, 113 | trackLength: data.length, // for wonder wizkid 114 | reflection: 0.3, 115 | waveWidth: 2, 116 | interpolate: true, 117 | fadeOpacity: 0.888, 118 | bindResize: false // to make the waveform bind to the resize event of the window!! 119 | } 120 | ); 121 | this.waveformInstance.on(EVENT_CLICK, (percent: number) => { 122 | const porpotion = percent / 100; 123 | widget.seek(porpotion * soundObject?.duration); 124 | widget.play() 125 | this.waveformInstance.playProgress(percent); 126 | 127 | }); 128 | }) 129 | .catch((err: any) => { throw err }); 130 | 131 | 132 | }) 133 | 134 | 135 | } 136 | 137 | private init() { 138 | this.bindPlayerUIEvent(); 139 | widget.load(this.defaultSource) 140 | } 141 | 142 | update() { 143 | updateOcclude(this); 144 | } 145 | } -------------------------------------------------------------------------------- /src/ts/util/event-emitter.ts: -------------------------------------------------------------------------------- 1 | interface Name { 2 | original: string 3 | value: string 4 | namespace: string 5 | } 6 | 7 | export class EventEmitter { 8 | callbacks: any = {} 9 | 10 | constructor() { 11 | this.callbacks = {} 12 | this.callbacks.base = {} 13 | } 14 | 15 | on(_names: string, callback: Function) { 16 | // Errors 17 | if (typeof _names === 'undefined' || _names === '') { 18 | console.warn('wrong names') 19 | return false 20 | } 21 | 22 | if (typeof callback === 'undefined') { 23 | console.warn('wrong callback') 24 | return false 25 | } 26 | 27 | // Resolve names 28 | const names = this.resolveNames(_names) 29 | 30 | // Each name 31 | names.forEach((_name) => { 32 | // Resolve name 33 | const name = this.resolveName(_name) 34 | 35 | // Create namespace if not exist 36 | if (!(this.callbacks[name.namespace] instanceof Object)) this.callbacks[name.namespace] = {} 37 | 38 | // Create callback if not exist 39 | if (!(this.callbacks[name.namespace][name.value] instanceof Array)) 40 | this.callbacks[name.namespace][name.value] = [] 41 | 42 | // Add callback 43 | this.callbacks[name.namespace][name.value].push(callback) 44 | }) 45 | 46 | return this 47 | } 48 | 49 | off(_names: string) { 50 | // Errors 51 | if (typeof _names === 'undefined' || _names === '') { 52 | console.warn('wrong name') 53 | return false 54 | } 55 | 56 | // Resolve names 57 | const names = this.resolveNames(_names) 58 | 59 | // Each name 60 | names.forEach((_name) => { 61 | // Resolve name 62 | const name = this.resolveName(_name) 63 | 64 | // Remove namespace 65 | if (name.namespace !== 'base' && name.value === '') { 66 | delete this.callbacks[name.namespace] 67 | } 68 | 69 | // Remove specific callback in namespace 70 | else { 71 | // Default 72 | if (name.namespace === 'base') { 73 | // Try to remove from each namespace 74 | for (const namespace in this.callbacks) { 75 | if ( 76 | this.callbacks[namespace] instanceof Object && 77 | this.callbacks[namespace][name.value] instanceof Array 78 | ) { 79 | delete this.callbacks[namespace][name.value] 80 | 81 | // Remove namespace if empty 82 | if (Object.keys(this.callbacks[namespace]).length === 0) delete this.callbacks[namespace] 83 | } 84 | } 85 | } 86 | 87 | // Specified namespace 88 | else if ( 89 | this.callbacks[name.namespace] instanceof Object && 90 | this.callbacks[name.namespace][name.value] instanceof Array 91 | ) { 92 | delete this.callbacks[name.namespace][name.value] 93 | 94 | // Remove namespace if empty 95 | if (Object.keys(this.callbacks[name.namespace]).length === 0) delete this.callbacks[name.namespace] 96 | } 97 | } 98 | }) 99 | 100 | return this 101 | } 102 | 103 | trigger(_name: string, _args?: any[]) { 104 | // Errors 105 | if (typeof _name === 'undefined' || _name === '') { 106 | console.warn('wrong name') 107 | return false 108 | } 109 | 110 | let finalResult: any = null 111 | let result = null 112 | 113 | // Default args 114 | const args = !(_args instanceof Array) ? [] : _args 115 | 116 | // Resolve names (should on have one event) 117 | let nameArray = this.resolveNames(_name) 118 | 119 | // Resolve name 120 | let name = this.resolveName(nameArray[0]) 121 | 122 | // Default namespace 123 | if (name.namespace === 'base') { 124 | // Try to find callback in each namespace 125 | for (const namespace in this.callbacks) { 126 | if ( 127 | this.callbacks[namespace] instanceof Object && 128 | this.callbacks[namespace][name.value] instanceof Array 129 | ) { 130 | this.callbacks[namespace][name.value].forEach((callback: Function) => { 131 | result = callback.apply(this, args) 132 | 133 | if (typeof finalResult === 'undefined') { 134 | finalResult = result 135 | } 136 | }) 137 | } 138 | } 139 | } 140 | 141 | // Specified namespace 142 | else if (this.callbacks[name.namespace] instanceof Object) { 143 | if (name.value === '') { 144 | console.warn('wrong name') 145 | return this 146 | } 147 | 148 | this.callbacks[name.namespace][name.value].forEach((callback: Function) => { 149 | result = callback.apply(this, args) 150 | 151 | if (typeof finalResult === 'undefined') finalResult = result 152 | }) 153 | } 154 | 155 | return finalResult 156 | } 157 | 158 | resolveNames(_names: string) { 159 | let names: string | string[] = _names 160 | names = names.replace(/[^a-zA-Z0-9 ,/.]/g, '') 161 | names = names.replace(/[,/]+/g, ' ') 162 | names = names.split(' ') 163 | 164 | return names 165 | } 166 | 167 | resolveName(name: string) { 168 | const newName: Partial = {} 169 | const parts = name.split('.') 170 | 171 | newName.original = name 172 | newName.value = parts[0] 173 | newName.namespace = 'base' // Base namespace 174 | 175 | // Specified namespace 176 | if (parts.length > 1 && parts[1] !== '') { 177 | newName.namespace = parts[1] 178 | } 179 | 180 | return newName as Name 181 | } 182 | } -------------------------------------------------------------------------------- /src/ts/util/service.ts: -------------------------------------------------------------------------------- 1 | import { widget } from '../util/widget' 2 | import { SoundCloudService } from './soundcloud-service'; 3 | const isMobile = require('is-mobile') 4 | const soundCloudService = new SoundCloudService({ clientId: '8m4K5d2x4mNmUHLhLmsGq9vxE3dDkxCm' }) 5 | 6 | export const pointerEvent = isMobile() ? 'touchstart' : 'click'; 7 | 8 | export function doConfirm(template: string) { 9 | const frame = document.querySelector('#modal-confirm'); 10 | const activeClass = 'modal-prompt--active' 11 | 12 | let cusRes: any; 13 | const prm = new Promise((res) => { 14 | cusRes = res; 15 | }) 16 | let confirm = false; 17 | const cb = (e: MouseEvent) => { 18 | e.preventDefault(); 19 | switch ((e.target as HTMLElement).closest('[action]').getAttribute('action')) { 20 | case 'yes': 21 | confirm = true; 22 | frame.classList.remove(activeClass); 23 | break; 24 | case 'no': 25 | confirm = false; 26 | frame.classList.remove(activeClass); 27 | break; 28 | case 'close': 29 | frame.classList.remove(activeClass); 30 | break; 31 | } 32 | e.currentTarget.removeEventListener(pointerEvent, cb); 33 | cusRes(confirm) 34 | } 35 | 36 | frame.querySelector('p').innerHTML = template; 37 | //show frame; 38 | frame.classList.add(activeClass); 39 | frame.addEventListener(pointerEvent, cb); 40 | return prm; 41 | } 42 | 43 | export function doAlert(template: string) { 44 | const frame = document.querySelector('#modal-alert'); 45 | const activeClass = 'modal-prompt--active' 46 | 47 | const cb = (e: MouseEvent) => { 48 | e.preventDefault(); 49 | const action = (e.target as HTMLElement).closest('[action]').getAttribute('action'); 50 | if (action === 'close') { 51 | e.currentTarget.removeEventListener(pointerEvent, cb); 52 | frame.classList.remove(activeClass); 53 | } 54 | 55 | } 56 | 57 | frame.querySelector('p').innerHTML = template; 58 | //show frame; 59 | frame.classList.add(activeClass); 60 | frame.addEventListener(pointerEvent, cb); 61 | } 62 | 63 | async function renderSearchView(frame: Element, keywordInput: HTMLInputElement) { 64 | if (!soundCloudService) return; 65 | const container = frame.querySelector('#search-music-result'); 66 | const keyword = keywordInput.value; 67 | if (!keyword) return; 68 | container.innerHTML = ` 69 | 72 | ` 73 | const searchResult: any = await soundCloudService.search({ 74 | q: keyword, 75 | limit: 100 76 | }).then((searchResult) => { 77 | widget.trigger('search', [searchResult]) 78 | return searchResult; 79 | }); 80 | 81 | const embedableTracks = searchResult?.collection.filter((o: any) => { 82 | return o.embeddable_by === 'all' && !!o.title 83 | }) 84 | const htmlBundle = embedableTracks.map((o: any, index: number) => { 85 | return ` 86 | 108 | ` 109 | }).join(''); 110 | container.scrollTop = 0; 111 | container.innerHTML = htmlBundle; 112 | } 113 | 114 | export function searchMusic(): Promise { 115 | const frame = document.querySelector('#modal-music-search'); 116 | const activeClass = 'modal-music-search--active'; 117 | const keywordInput = frame.querySelector('#music-search-input') as HTMLInputElement; 118 | let cusRes: (value: (false | string)) => void; 119 | let chosenId: string; 120 | const prm = new Promise((res: (value: (false | string)) => void, rej) => { 121 | cusRes = res; 122 | }) 123 | 124 | const keydownCallback = (evt: KeyboardEvent) => { 125 | if (evt.keyCode == 13) { 126 | if (keywordInput.value && document.activeElement === keywordInput) { 127 | renderSearchView(frame, keywordInput); 128 | } 129 | } 130 | } 131 | 132 | const cb = async (e: MouseEvent) => { 133 | const actionTarget = (e.target as HTMLElement).closest('[action]') || (e.target as HTMLElement); 134 | const action = actionTarget.getAttribute('action'); 135 | if (action === 'close') { 136 | e.currentTarget.removeEventListener(pointerEvent, cb); 137 | document.removeEventListener('keydown', keydownCallback); 138 | frame.classList.remove(activeClass); 139 | cusRes(false); 140 | } 141 | else if (action === 'search') { 142 | renderSearchView(frame, keywordInput); 143 | } 144 | else if (action === 'choose') { 145 | chosenId = actionTarget.getAttribute('data-id') || ''; 146 | e.currentTarget.removeEventListener(pointerEvent, cb); 147 | document.removeEventListener('keydown', keydownCallback); 148 | frame.classList.remove(activeClass); 149 | cusRes(chosenId); 150 | } 151 | } 152 | 153 | frame.classList.add(activeClass); 154 | frame.addEventListener(pointerEvent, cb); 155 | document.addEventListener('keydown', keydownCallback); 156 | return prm; 157 | } 158 | 159 | export function playViaIframe(trackId: string) { 160 | const source = `https%3A//api.soundcloud.com/tracks/${trackId}` 161 | return widget.load(source).then( 162 | () => { 163 | widget.play(); 164 | } 165 | ) 166 | } 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const { resolve } = require('path'); 3 | const CopyPlugin = require('copy-webpack-plugin'); 4 | const TerserPlugin = require("terser-webpack-plugin"); 5 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 6 | const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); 7 | import * as webpack from 'webpack'; 8 | import 'webpack-dev-server'; // dont remove this import, it's for webpack-dev-server type 9 | import HtmlWebpackPlugin from 'html-webpack-plugin'; 10 | const COMPRESS = true; 11 | 12 | const getEntriesByParsingTemplateNames = (templatesFolderName, atRoot = true) => { 13 | const folderPath = resolve(__dirname, `./src/${templatesFolderName}`); 14 | const entryObj: webpack.EntryObject = {}; 15 | const templateRegx = /(.*)(\.)(ejs|html)/g; 16 | fs.readdirSync(folderPath).forEach((o: string) => { 17 | if (!o.match(templateRegx)) return; 18 | let entryName: string = o.replace(templateRegx, `$1`); 19 | const entryRegex = /(.*)(\.)(.*)/g; 20 | if (entryName.match(entryRegex)) { 21 | entryName = entryName.replace(entryRegex, `$3`); 22 | } 23 | 24 | const entryDependency = atRoot ? entryName : `${templatesFolderName}/${entryName}` 25 | 26 | let entryPath = resolve(__dirname, `src/ts/${entryDependency}.ts`); 27 | // entry stylesheet 28 | let entryStyleSheetPath = resolve(__dirname, `./src/scss/${entryDependency}.scss`); 29 | 30 | entryPath = fs.existsSync(entryPath) ? entryPath : undefined; 31 | entryStyleSheetPath = fs.existsSync(entryStyleSheetPath) ? entryStyleSheetPath : undefined; 32 | 33 | // import es6-promise and scss util automatically 34 | entryObj[entryName] = ['es6-promise/auto', entryPath, './src/scss/reset.scss', entryStyleSheetPath].filter(function (x: string | undefined) { 35 | return x !== undefined; 36 | }); 37 | 38 | }) 39 | return entryObj; 40 | } 41 | 42 | const getTemaplteInstancesByParsingTemplateNames = (templatesFolderName, atRoot = true) => { 43 | const forderPath = resolve(__dirname, `./src/${templatesFolderName}`); 44 | return fs.readdirSync(forderPath).map((fullFileName: string) => { 45 | const templateRegx = /(.*)(\.)(ejs|html)/g; 46 | const ejsRegex = /(.*)(\.ejs)/g; 47 | const entryRegex = /(.*)(\.)(.*)(\.)(ejs|html)/g; 48 | if (!fullFileName.match(templateRegx)) return; 49 | const isEjs = fullFileName.match(ejsRegex); 50 | let outputFileName = fullFileName.replace(templateRegx, `$1`); 51 | let entryName = outputFileName; 52 | if (fullFileName.match(entryRegex)) { 53 | outputFileName = fullFileName.replace(entryRegex, `$1`); 54 | entryName = fullFileName.replace(entryRegex, `$3`); 55 | } 56 | const ejsFilePath = resolve(forderPath, `${fullFileName}`); 57 | const data = fs.readFileSync(ejsFilePath, 'utf8') 58 | if (!data) { 59 | fs.writeFile(ejsFilePath, ' ', () => { }); 60 | console.warn(`WARNING : ${fullFileName} is an empty file`); 61 | } 62 | 63 | return new HtmlWebpackPlugin({ 64 | cache: false, 65 | chunks: [entryName], 66 | filename: `${atRoot ? '' : templatesFolderName + '/'}${outputFileName}.html`, 67 | template: isEjs ? ejsFilePath : ejsFilePath.replace(ejsRegex, `$1.html`), 68 | favicon: 'src/assets/images/logo.svg', 69 | minify: COMPRESS ? { 70 | collapseWhitespace: true, 71 | keepClosingSlash: true, 72 | removeComments: true, 73 | removeRedundantAttributes: false, 74 | removeScriptTypeAttributes: true, 75 | removeStyleLinkTypeAttributes: true, 76 | useShortDoctype: true 77 | } : false 78 | }) 79 | }).filter(function (x: HtmlWebpackPlugin | undefined) { 80 | return x !== undefined; 81 | }); 82 | } 83 | 84 | const pageEntries: webpack.EntryObject = getEntriesByParsingTemplateNames('pages'); 85 | //generate htmlWebpackPlugin instances 86 | const pageEntryTemplates: HtmlWebpackPlugin[] = getTemaplteInstancesByParsingTemplateNames('pages'); 87 | 88 | 89 | const config = (env: any, argv: any): webpack.Configuration => { 90 | const configObj: webpack.Configuration = { 91 | entry: pageEntries, 92 | output: { 93 | filename: 'js/[name].[chunkhash].js', 94 | chunkFilename: '[id].[chunkhash].js', 95 | path: resolve(__dirname, 'dist'), 96 | clean: true 97 | }, 98 | target: ['web', 'es6'], 99 | devtool: 'source-map', 100 | devServer: { 101 | // host: '192.168.1.101', 102 | historyApiFallback: true, 103 | open: true, 104 | compress: true, 105 | watchFiles: [ 106 | 'src/pages/*.html', 107 | 'src/template/*.html', 108 | 'src/template/**/*.html', 109 | 'src/pages/*.ejs', 110 | 'src/template/*.ejs', 111 | 'src/template/**/*.ejs', 112 | ],// this is important 113 | port: 8080 114 | }, 115 | mode: 'development', 116 | experiments: { 117 | topLevelAwait: true 118 | }, 119 | module: { 120 | rules: [ 121 | { 122 | test: /\.tsx?$/, 123 | use: [ 124 | { 125 | loader: 'babel-loader', 126 | options: { 127 | plugins: ['@babel/plugin-syntax-top-level-await'], 128 | }, 129 | }, 130 | 'ts-loader' 131 | ], 132 | exclude: /node_modules/, 133 | }, 134 | { 135 | test: /\.html$/, 136 | use: [ 137 | { 138 | loader: 'html-loader', 139 | options: { 140 | minimize: COMPRESS 141 | } 142 | } 143 | ], 144 | }, 145 | { 146 | test: /\.ejs$/, 147 | use: [ 148 | { 149 | loader: 'html-loader', 150 | options: { 151 | minimize: COMPRESS 152 | } 153 | }, 154 | { 155 | loader: 'template-ejs-loader', 156 | options: { 157 | data: { 158 | mode: argv.mode 159 | } 160 | } 161 | } 162 | ] 163 | }, 164 | { 165 | test: /\.(jpe?g|png|gif|svg)$/, 166 | type: 'asset/resource', 167 | generator: { 168 | filename: 'assets/images/[name][ext]' 169 | } 170 | }, 171 | { 172 | test: /\.(sass|scss|css)$/, 173 | use: [ 174 | { 175 | loader: MiniCssExtractPlugin.loader, 176 | options: { 177 | publicPath: '../' 178 | } 179 | }, 180 | 'css-loader', 181 | { 182 | loader: 'postcss-loader', 183 | options: { 184 | postcssOptions: { 185 | ident: 'postcss', 186 | plugins: [ 187 | require('postcss-preset-env')() 188 | ] 189 | } 190 | } 191 | }, 192 | (() => { 193 | return COMPRESS ? 'sass-loader' : { 194 | loader: 'sass-loader', 195 | options: { sourceMap: true, sassOptions: { minimize: false, outputStyle: 'expanded' } } 196 | } 197 | })() 198 | 199 | ] 200 | }, 201 | { 202 | test: /\.(woff(2)?|eot|ttf|otf)$/, 203 | type: 'asset/resource', 204 | generator: { 205 | filename: 'assets/fonts/[name][ext]' 206 | } 207 | } 208 | 209 | ] 210 | }, 211 | resolve: { 212 | fallback: { 213 | "url": require.resolve("url/"), 214 | "assert": require.resolve("assert/"), 215 | "stream": require.resolve("stream-browserify"), 216 | "https": require.resolve("https-browserify"), 217 | "http": require.resolve("stream-http"), 218 | "zlib": require.resolve("browserify-zlib"), 219 | "buffer": require.resolve("buffer/") 220 | }, 221 | extensions: ['.ts', '.tsx', '.js', '.jsx', 'json'], 222 | alias: { 223 | '@img': resolve(__dirname, './src/assets/images/'), 224 | '@font': resolve(__dirname, './src/assets/fonts/') 225 | } 226 | }, 227 | optimization: { 228 | minimize: COMPRESS, 229 | minimizer: [ 230 | new TerserPlugin({ 231 | terserOptions: { 232 | format: { 233 | comments: false, 234 | }, 235 | }, 236 | test: /\.js(\?.*)?$/i, 237 | extractComments: false 238 | }), 239 | new CssMinimizerPlugin() 240 | ], 241 | splitChunks: { name: 'vendor', chunks: 'all' } 242 | }, 243 | performance: { 244 | hints: false, 245 | maxEntrypointSize: 512000, 246 | maxAssetSize: 512000 247 | }, 248 | plugins: [ 249 | new webpack.DefinePlugin({ 250 | 'PROCESS.MODE': JSON.stringify(argv.mode), 251 | }), 252 | new webpack.ProvidePlugin({ 253 | process: 'process/browser', 254 | }), 255 | new MiniCssExtractPlugin({ 256 | filename: 'css/[name].css' 257 | }), 258 | new CopyPlugin( 259 | { 260 | patterns: [ 261 | { 262 | from: 'src/static', 263 | to: 'static', 264 | globOptions: { 265 | dot: true, 266 | ignore: ['**/.DS_Store', '**/.gitkeep'], 267 | }, 268 | noErrorOnMissing: true, 269 | }, 270 | { 271 | from: 'src/assets/images', 272 | to: 'assets/images', 273 | globOptions: { 274 | dot: true, 275 | ignore: ['**/.DS_Store', '**/.gitkeep'], 276 | }, 277 | noErrorOnMissing: true, 278 | } 279 | ], 280 | } 281 | ), 282 | ...pageEntryTemplates 283 | 284 | ].filter(function (x) { 285 | return x !== undefined; 286 | }) 287 | } 288 | return configObj; 289 | } 290 | 291 | 292 | export default config; -------------------------------------------------------------------------------- /src/scss/components/_chat-block.scss: -------------------------------------------------------------------------------- 1 | @mixin toggler() { 2 | position: relative; 3 | width: 100%; 4 | padding-bottom: 100%; 5 | border: none; 6 | cursor: pointer; 7 | background-color: transparent; 8 | background-color: rgb(34, 34, 34); 9 | border-radius: 50%; 10 | box-shadow: 11 | 2.8px 2.8px 2.2px rgba(0, 0, 0, 0.02), 12 | 6.7px 6.7px 5.3px rgba(0, 0, 0, 0.028), 13 | 12.5px 12.5px 10px rgba(0, 0, 0, 0.035), 14 | 22.3px 22.3px 17.9px rgba(0, 0, 0, 0.042), 15 | 41.8px 41.8px 33.4px rgba(0, 0, 0, 0.05), 16 | 100px 100px 80px rgba(0, 0, 0, 0.07); 17 | 18 | &:before, 19 | &:after { 20 | content: ''; 21 | display: block; 22 | position: absolute; 23 | background-image: url(~@img/chat.png); 24 | background-size: cover; 25 | top: 0; 26 | bottom: 0; 27 | left: 0; 28 | right: 0; 29 | margin: auto; 30 | width: 50%; 31 | height: 50%; 32 | transition: opacity 500ms; 33 | opacity: 0.5; 34 | z-index: 2; 35 | } 36 | 37 | } 38 | 39 | .chat-block { 40 | background-color: rgb(38, 38, 40); 41 | display: flex; 42 | flex-direction: column; 43 | position: relative; 44 | 45 | &__login { 46 | position: absolute; 47 | width: 100%; 48 | height: 100%; 49 | top: 0; 50 | left: 0; 51 | z-index: 2; 52 | box-shadow: 19px -1px 30px -17px rgba(0, 0, 0, 0.5) inset; 53 | } 54 | 55 | &__tools { 56 | position: fixed; 57 | padding-bottom: 20px; 58 | padding-right: 20px; 59 | padding-top: 20px; 60 | width: 70px; 61 | height: 100%; 62 | z-index: 5; 63 | right: 0; 64 | bottom: 0; 65 | display: flex; 66 | flex-direction: column; 67 | justify-content: flex-end; 68 | align-items: center; 69 | 70 | @include rwd($screen-pad-portrait) { 71 | padding-top: 10px; 72 | padding-right: 10px; 73 | padding-bottom: 10px; 74 | width: 50px; 75 | } 76 | } 77 | 78 | &__tools>*+* { 79 | margin-top: 20px; 80 | } 81 | 82 | &__toggler { 83 | @include toggler(); 84 | } 85 | 86 | @include hasHover() { 87 | &__toggler:hover::before { 88 | opacity: 0.75; 89 | } 90 | } 91 | 92 | &__rotation-lock { 93 | @include toggler(); 94 | transition: background-color 500ms, opacity 500ms; 95 | 96 | &:before { 97 | opacity: 0.5; 98 | background-image: url(~@img/rotation-lock.png); 99 | } 100 | 101 | &:after { 102 | opacity: 0.5; 103 | transition: opacity 500ms, transform 500ms; 104 | background-image: url(~@img/rotation-lock-n.svg); 105 | 106 | } 107 | 108 | } 109 | 110 | &__search-music-button { 111 | @include toggler(); 112 | 113 | 114 | &:before { 115 | background-image: url(~@img/headphone.svg); 116 | background-size: cover; 117 | width: 90%; 118 | height: 73%; 119 | transform: scale(0.6); 120 | top: -10%; 121 | margin: auto; 122 | 123 | @include rwd($screen-pad-portrait) { 124 | transform: scale(0.65); 125 | } 126 | } 127 | 128 | &:after { 129 | display: none; 130 | } 131 | 132 | >* { 133 | position: absolute; 134 | transition: opacity 500ms; 135 | opacity: 0.5; 136 | left: 50%; 137 | top: 55%; 138 | transform: translate(-50%, -50%) scale(0.5); 139 | } 140 | 141 | 142 | svg { 143 | path:nth-child(1) { 144 | animation: bar1 .6s infinite alternate; 145 | transform: translateY(20%); 146 | animation-delay: 0.4s; 147 | } 148 | 149 | path:nth-child(2) { 150 | animation: bar2 .3s infinite alternate; 151 | animation-delay: 0.3s; 152 | } 153 | 154 | path:nth-child(3) { 155 | animation: bar3 .4s infinite alternate; 156 | transform: translateY(50%); 157 | animation-delay: 0.5s; 158 | } 159 | } 160 | 161 | 162 | @keyframes bar1 { 163 | 0% { 164 | transform: translateY(40%); 165 | } 166 | 167 | 100% { 168 | transform: translateY(0%); 169 | } 170 | } 171 | 172 | @keyframes bar2 { 173 | 0% { 174 | transform: translateY(30%); 175 | } 176 | 177 | 100% { 178 | transform: translateY(0%); 179 | } 180 | } 181 | 182 | @keyframes bar3 { 183 | 0% { 184 | transform: translateY(60%); 185 | } 186 | 187 | 100% { 188 | transform: translateY(0%); 189 | } 190 | } 191 | } 192 | 193 | 194 | &__rotation-lock--active { 195 | background-color: gray; 196 | opacity: 0.5; 197 | 198 | &:before { 199 | opacity: 0; 200 | } 201 | 202 | &:after { 203 | transform: rotate(-180deg) 204 | } 205 | 206 | } 207 | 208 | @include hasHover() { 209 | &__rotation-lock:hover { 210 | background-color: gray; 211 | opacity: 0.5; 212 | 213 | &:before { 214 | opacity: 0; 215 | } 216 | 217 | &:after { 218 | transform: rotate(-180deg) 219 | } 220 | } 221 | } 222 | 223 | @include hasHover() { 224 | &__search-music-button:hover { 225 | 226 | >*, 227 | &:before { 228 | opacity: 0.75; 229 | } 230 | } 231 | } 232 | 233 | 234 | &__search-music-button--active { 235 | 236 | >*, 237 | &:before { 238 | opacity: 0.75; 239 | } 240 | 241 | } 242 | 243 | &__header { 244 | flex: none; 245 | padding: 20px; 246 | box-shadow: -1px 3px 27px 0px rgba(0, 0, 0, 0.75); 247 | } 248 | 249 | &__body { 250 | flex: 1; 251 | overflow: auto; 252 | 253 | @include custom-scroll(); 254 | } 255 | 256 | &__body-inner { 257 | padding: 20px 20px; 258 | 259 | } 260 | 261 | &__footer { 262 | flex: none; 263 | padding: 0 20px; 264 | box-shadow: 1px -7px 19px -4px rgba(0, 0, 0, 0.35); 265 | } 266 | 267 | &__input { 268 | @include rwd($screen-pad-portrait) { 269 | padding-left: 10px; 270 | padding-right: 10px; 271 | } 272 | } 273 | 274 | &__author { 275 | height: 50px; 276 | 277 | @include rwd($screen-pad-portrait) { 278 | padding-left: 10px; 279 | padding-right: 10px; 280 | } 281 | } 282 | 283 | 284 | &--active &__content { 285 | display: none; 286 | } 287 | 288 | } 289 | 290 | .user { 291 | display: flex; 292 | align-items: center; 293 | 294 | &__avatar { 295 | width: 30px; 296 | height: 30px; 297 | border-radius: 50%; 298 | transition: opacity 500ms; 299 | opacity: 0.5; 300 | 301 | img { 302 | width: 100%; 303 | } 304 | } 305 | 306 | @include hasHover() { 307 | &__avatar:hover { 308 | opacity: 0.75; 309 | } 310 | } 311 | 312 | 313 | &__avatar+&__name { 314 | margin-left: 20px; 315 | } 316 | 317 | &__name { 318 | font-size: 1rem; 319 | color: transparentize($color: $white, $amount: 0.5); 320 | user-select: none; 321 | pointer-events: none; 322 | } 323 | 324 | &__logout { 325 | background-color: transparent; 326 | color: $white; 327 | font-weight: 400; 328 | margin-left: auto; 329 | font-size: 14px; 330 | padding: 10px; 331 | display: flex; 332 | align-items: center; 333 | opacity: 0.5; 334 | transition: opacity 500ms; 335 | border: 1px solid $white; 336 | border-radius: 5px; 337 | cursor: pointer; 338 | 339 | &:after { 340 | content: ''; 341 | display: block; 342 | background-image: url(~@img/logout.svg); 343 | width: 20px; 344 | height: 20px; 345 | background-size: cover; 346 | margin-left: 15px; 347 | } 348 | 349 | @include hasHover() { 350 | &:hover { 351 | opacity: 0.75; 352 | } 353 | } 354 | 355 | } 356 | } 357 | 358 | .chat-main { 359 | position: relative; 360 | 361 | &--cube { 362 | @include gradient-border($border-width: 5px, $outer-shadow: 0px 0px 20px rgba(255, 255, 255, 0.5)); 363 | width: 420px; 364 | height: 420px; 365 | background: rgba(28, 28, 31, 0.25); 366 | 367 | &:before { 368 | animation: flickr 5000ms infinite forwards linear; 369 | } 370 | } 371 | 372 | &--cube &__inner { 373 | width: 100%; 374 | height: 100%; 375 | padding: 10px; 376 | overflow: auto; 377 | @include custom-scroll(); 378 | } 379 | 380 | &__inner--overlay { 381 | position: absolute; 382 | width: calc(100% - 30px); 383 | height: calc(100% - 30px); 384 | top: 0; 385 | left: 0; 386 | } 387 | 388 | &__inner--hide { 389 | display: none; 390 | } 391 | 392 | &__inner &__login-guide, 393 | &__inner &__guest-list { 394 | height: 100%; 395 | width: 100%; 396 | } 397 | 398 | &__chat+&__chat { 399 | margin-top: 20px; 400 | } 401 | 402 | &--cube &__chat+&__chat { 403 | margin-top: 20px; 404 | } 405 | 406 | &__chat--other+&__chat:not(#{&}__chat--other), 407 | &__chat:not(#{&}__chat--other)+&__chat--other { 408 | margin-top: 40px; 409 | } 410 | 411 | 412 | 413 | 414 | 415 | &__chat { 416 | text-align: right; 417 | 418 | &--other { 419 | text-align: left; 420 | } 421 | } 422 | 423 | &__bubble-name { 424 | color: transparentize($white, 0.5); 425 | font-size: 14px; 426 | margin: 10px; 427 | } 428 | 429 | 430 | &__bubble { 431 | background: #bcbfc5; 432 | color: rgb(54, 54, 54); 433 | font-size: 15px; 434 | border-radius: 15px 15px 0px 15px; 435 | font-weight: 400; 436 | padding: 15px; 437 | max-width: 200px; 438 | width: 100%; 439 | text-align: left; 440 | display: inline-block; 441 | animation: bubble 500ms; 442 | transform-origin: 100% 0%; 443 | opacity: 0.75; 444 | transition: opacity 500ms; 445 | word-break: break-all; 446 | 447 | @include hasHover() { 448 | &:hover { 449 | opacity: 1; 450 | } 451 | } 452 | 453 | } 454 | 455 | &--cube &__bubble { 456 | background: transparent; 457 | border: 1px solid $white; 458 | color: $white 459 | } 460 | 461 | &__chat--other &__bubble { 462 | background: rgb(84, 67, 102); 463 | color: #c4c4c4; 464 | border-radius: 15px 15px 15px 0px; 465 | font-size: 15px; 466 | margin-right: 0; 467 | transform-origin: 0% 0%; 468 | } 469 | 470 | 471 | &--cube &__chat--other &__bubble { 472 | background: rgba(84, 67, 102, 0.5); 473 | color: $white; 474 | border-radius: 15px 15px 15px 0px; 475 | font-size: 15px; 476 | margin-right: 0; 477 | transform-origin: 0% 0%; 478 | } 479 | } -------------------------------------------------------------------------------- /src/pages/index.main.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%- include('../template/head.ejs',{title:'3D Cube Chat'})%> 5 | 6 | 7 |
    8 |
    9 |
    10 |
    11 | 12 |
    13 |
    14 | 28 |
    29 | 30 | 39 | 40 |
    41 |
    42 |
    43 | Mizok 44 |
    45 | 46 |
    47 |
    48 |
    49 | 55 |
    56 |
    57 | 78 |
    79 |
    80 |
    81 |
    82 |
    83 |
    84 |
    85 |
    86 |
    87 |

    Guest List

    88 |
    89 |
    90 |
      91 | 94 |
    95 |
    96 |
    97 |
    98 |
    99 | 104 |
    105 |
    106 | 107 |
    108 |
    109 |
    110 |
    111 |
    112 |
    113 | 114 |
    115 |
    116 |
    117 |
    118 | 119 |
    120 |
    121 | 131 |
    132 |
    133 |
    134 |
    135 |
    136 |
    137 | 3D Cube Chat Music Player 138 |
    139 |
    140 | 141 | 142 | 143 |
    144 |
    145 |
    146 | 147 |
    148 |
    149 |
    150 |
    151 | May 152 |   153 | 01 154 | , 155 |   156 | 2022 157 |
    158 |
    159 | 00 160 | : 161 | 00 162 | : 163 | 00 164 |
    165 |
    166 |
    167 | 216 |
    217 | 219 |
    220 |
    221 |
    222 |
    223 | 224 |
    225 |
    226 | 3D-CUBE-CHAT 227 |
    228 |
    229 | 230 |
    231 |
    232 |
    233 | 234 | 235 | -------------------------------------------------------------------------------- /src/ts/main.ts: -------------------------------------------------------------------------------- 1 | import { Base } from './class/base'; 2 | import { io, Socket } from 'socket.io-client'; 3 | import { trim } from 'lodash'; 4 | import { ShowScreenTargets } from './interface'; 5 | import { createElementFromHTML } from './util/function'; 6 | import { doAlert, doConfirm, playViaIframe, pointerEvent, searchMusic } from './util/service'; 7 | 8 | class Main extends Base { 9 | private wrapper: Element = document.querySelector('#wrapper'); 10 | private chatBlock: Element = document.querySelector('#chat-block'); 11 | private rotationLock: Element = this.chatBlock.querySelector('#rotation-lock'); 12 | private curtainCall: Element = document.querySelector('#curtain-call'); 13 | private chatBlockActive = false; 14 | private socket: Socket; 15 | private myName: string; 16 | private path = 'https://3d-cube-chat.fly.dev'; 17 | // private path = 'http://192.168.1.101:5500'; 18 | constructor(canvas: HTMLCanvasElement, domCanvas: HTMLElement, domBundle: HTMLElement) { 19 | super(canvas, domCanvas, domBundle); 20 | this.init(); 21 | } 22 | 23 | private init() { 24 | this.initCurtainCall(); 25 | this.initChatUI(); 26 | this.initMusicUI(); 27 | } 28 | 29 | 30 | toggleRotationLockFromUI(): void { 31 | const status = this.getRotationLockStatus(); 32 | if (status) { 33 | this.rotationLock.classList.remove('chat-block__rotation-lock--active'); 34 | } 35 | else { 36 | this.rotationLock.classList.add('chat-block__rotation-lock--active'); 37 | } 38 | this.setRotationLock(!status) 39 | } 40 | 41 | setRotationLockFromUI(status: boolean): void { 42 | if (status) { 43 | this.rotationLock.classList.add('chat-block__rotation-lock--active'); 44 | } 45 | else { 46 | this.rotationLock.classList.remove('chat-block__rotation-lock--active'); 47 | } 48 | 49 | this.setRotationLock(status) 50 | } 51 | 52 | private initCurtainCall() { 53 | this.playground.on('env-ready', () => { 54 | this.curtainCall.classList.add('curtain-call--hide'); 55 | }) 56 | } 57 | 58 | private initChatUI() { 59 | const toggler = this.chatBlock.querySelector('#chat-block-toggler'); 60 | const loginBtn = this.chatBlock.querySelector('#login-button'); 61 | const sendBtn = this.chatBlock.querySelector('#send-message-button'); 62 | const logoutBtn = this.chatBlock.querySelector('#logout-button'); 63 | const panelToggle = () => { 64 | let showTarget: ShowScreenTargets 65 | if (this.chatBlockActive) { 66 | //準備關閉側選單 67 | this.wrapper.classList.remove('wrapper--active'); 68 | showTarget = this.getLoginStatus() ? 'chatMainInner' : 'loginGuide'; 69 | this.setRotationLockFromUI(false); 70 | } 71 | else { 72 | //準備開啟側選單 73 | this.wrapper.classList.add('wrapper--active'); 74 | showTarget = this.getLoginStatus() ? 'guestList' : 'loginGuide'; 75 | this.setRotationLockFromUI(true); 76 | this.playground.showChat(); 77 | } 78 | this.playground.domCube.chat.showScreen(showTarget) 79 | this.chatBlockActive = !this.chatBlockActive; 80 | } 81 | const panelSet = (status: boolean) => { 82 | let showTarget: ShowScreenTargets 83 | if (status == true) { 84 | //準備開啟側選單 85 | this.wrapper.classList.add('wrapper--active'); 86 | showTarget = this.getLoginStatus() ? 'guestList' : 'loginGuide'; 87 | if (this.getLoginStatus()) { 88 | this.setRotationLockFromUI(true); 89 | } 90 | this.playground.showChat(); 91 | } 92 | else { 93 | //準備關閉側選單 94 | this.wrapper.classList.remove('wrapper--active'); 95 | showTarget = this.getLoginStatus() ? 'chatMainInner' : 'loginGuide'; 96 | this.setRotationLockFromUI(false); 97 | } 98 | this.playground.domCube.chat.showScreen(showTarget) 99 | this.chatBlockActive = status; 100 | 101 | } 102 | 103 | toggler.addEventListener(pointerEvent, panelToggle) 104 | 105 | this.playground.on('ready', () => { 106 | this.playground.domCube.chat.showScreen('loginGuide'); 107 | this.playground.domCube.chat.on('cube-login-button-click', () => { 108 | this.setRotationLockFromUI(true); 109 | panelSet(true); 110 | }) 111 | }) 112 | 113 | 114 | this.rotationLock.addEventListener(pointerEvent, this.toggleRotationLockFromUI.bind(this)) 115 | 116 | loginBtn.addEventListener(pointerEvent, () => { 117 | this.myName = trim((this.chatBlock.querySelector('#login-name') as HTMLInputElement).value); 118 | if (this.myName) { 119 | /*發送事件*/ 120 | this.socket = io(this.path); 121 | this.initChatSocket(); 122 | this.socket.emit('login', { username: this.myName }) 123 | } else { 124 | doAlert('Please enter a name :)') 125 | } 126 | }) 127 | 128 | sendBtn.addEventListener(pointerEvent, () => { 129 | this.sendMessage(); 130 | }) 131 | 132 | logoutBtn.addEventListener(pointerEvent, async () => { 133 | let leave = await doConfirm('Are you sure you want to leave the chat?') 134 | if (leave) { 135 | /*觸發 logout 事件*/ 136 | this.socket.emit('logout', { username: this.myName }); 137 | } 138 | }) 139 | 140 | 141 | 142 | document.addEventListener('keydown', (evt: KeyboardEvent) => { 143 | if (evt.keyCode == 13) { 144 | this.sendMessage() 145 | } 146 | }) 147 | 148 | } 149 | 150 | private initMusicUI() { 151 | const musicBtn = this.chatBlock.querySelector('#search-music-button'); 152 | let stopRotationTimeOut: any; 153 | 154 | musicBtn.addEventListener(pointerEvent, async () => { 155 | 156 | this.setRotationLockFromUI(true); 157 | this.playground.showMusic(); 158 | 159 | await searchMusic().then((data) => { 160 | if (data) { 161 | playViaIframe(data).then(() => { 162 | clearTimeout(stopRotationTimeOut); 163 | stopRotationTimeOut = setTimeout(() => { 164 | this.setRotationLockFromUI(false); 165 | }, 1000) 166 | }); 167 | } else { 168 | this.setRotationLockFromUI(false); 169 | } 170 | }) 171 | 172 | 173 | }) 174 | } 175 | 176 | private initChatSocket() { 177 | /*登入成功*/ 178 | this.socket.on('loginSuccess', (data) => { 179 | if (data.username === this.myName) { 180 | this.checkIn(data) 181 | } else { 182 | doAlert('Wrong username:( Please try again!') 183 | } 184 | }) 185 | 186 | /*登入失敗*/ 187 | this.socket.on('loginFail', () => { 188 | doAlert('Duplicate name already exists :(') 189 | }) 190 | 191 | /*加入聊天室提示*/ 192 | this.socket.on('add', (data) => { 193 | var html = `

    ${data.username} 加入聊天室

    ` 194 | // $('.chat-con').append(html); 195 | this.playground.domCube.chat.setGuestNumber(data.userCount); 196 | this.playground.domCube.chat.refreshGuestList(data?.users); 197 | }) 198 | 199 | //離開成功 200 | this.socket.on('leaveSuccess', () => { 201 | this.checkOut() 202 | }) 203 | 204 | //退出提示 205 | this.socket.on('leave', (data) => { 206 | if (data.username != null) { 207 | this.playground.domCube.chat.refreshGuestList(data?.users) 208 | let html = `

    ${data.username} 退出聊天室

    `; 209 | // $('.chat-con').append(html); 210 | this.playground.domCube.chat.setGuestNumber(data.userCount) 211 | } 212 | }) 213 | 214 | //收到訊息 215 | this.socket.on('receiveMessage', (data) => { 216 | 217 | this.showMessage(data) 218 | }) 219 | } 220 | private checkIn(data: any) { 221 | const loginWrapper = this.chatBlock.querySelector('#login'); 222 | const userNameEle = this.chatBlock.querySelector('#my-name'); 223 | userNameEle.innerHTML = data.username; 224 | loginWrapper.classList.add('login--logined'); 225 | this.setRotationLockFromUI(true); 226 | this.setLoginStatus(true); 227 | this.playground.showChat(); 228 | } 229 | 230 | private checkOut() { 231 | const loginWrapper = this.chatBlock.querySelector('#login'); 232 | loginWrapper.classList.remove('login--logined'); 233 | this.playground.domCube.chat.showScreen('loginGuide'); 234 | this.chatBlock.querySelector('#chat-main').innerHTML = ''; 235 | this.setLoginStatus(false); 236 | } 237 | 238 | private sendMessage() { 239 | const inputEle = this.chatBlock.querySelector('#message-input'); 240 | const message = (inputEle as HTMLInputElement).value; 241 | (inputEle as HTMLInputElement).value = '' 242 | if (message) { 243 | /*觸發 sendMessage 事件*/ 244 | this.socket.emit('sendMessage', { username: this.myName, message: message }); 245 | } 246 | } 247 | 248 | private showMessage(data: any) { 249 | let html; 250 | if (data.username === this.myName) { 251 | html = `
    252 |
    You
    253 |
    ${data.message}
    254 |
    255 | `; 256 | } else { 257 | html = `
    258 |
    ${data.username}
    259 |
    ${data.message}
    260 |
    261 | `; 262 | } 263 | const ele = createElementFromHTML(html) 264 | const containerMain = this.chatBlock.querySelector('#chat-main'); 265 | const containerCube = this.wrapper.querySelector('#chat-main-cube .chat-main__inner#chat-main-inner'); 266 | containerMain.appendChild(ele); 267 | containerCube.appendChild(ele.cloneNode(true)); 268 | containerMain.parentElement.scrollTop = containerMain.scrollHeight; 269 | containerCube.scrollTop = containerCube.scrollHeight; 270 | } 271 | 272 | 273 | } 274 | 275 | 276 | (() => { 277 | const cvs = document.querySelector('#canvas'); 278 | const dcvs = document.querySelector('#dom-canvas'); 279 | const domBundle = document.querySelector('#dom-bundle'); 280 | const instance = new Main(cvs as HTMLCanvasElement, dcvs as HTMLElement, domBundle as HTMLElement); 281 | })() -------------------------------------------------------------------------------- /src/scss/components/_modal.scss: -------------------------------------------------------------------------------- 1 | .img-replace { 2 | /* replace text with an image */ 3 | display: inline-block; 4 | overflow: hidden; 5 | text-indent: 100%; 6 | color: transparent; 7 | white-space: nowrap; 8 | } 9 | 10 | 11 | /* -------------------------------- 12 | 13 | xpopup 14 | 15 | -------------------------------- */ 16 | .modal-prompt { 17 | @include modalBase(); 18 | 19 | &__inner { 20 | position: relative; 21 | width: 90%; 22 | max-width: 400px; 23 | margin: 4em auto; 24 | background: #FFF; 25 | border-radius: .25em .25em .4em .4em; 26 | text-align: center; 27 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); 28 | transform: translateY(-40px); 29 | backface-visibility: hidden; 30 | transition: transform 0.3s; 31 | 32 | p { 33 | padding: 3em 1em; 34 | } 35 | } 36 | 37 | &--active &__inner { 38 | transform: translateY(0); 39 | } 40 | 41 | 42 | 43 | &__buttons { 44 | user-select: none; 45 | display: flex; 46 | 47 | li { 48 | flex: 1; 49 | width: 50%; 50 | list-style: none; 51 | } 52 | 53 | a { 54 | display: block; 55 | height: 60px; 56 | line-height: 60px; 57 | text-transform: uppercase; 58 | color: #FFF; 59 | -webkit-transition: background-color 0.2s; 60 | -moz-transition: background-color 0.2s; 61 | transition: background-color 0.2s; 62 | text-decoration: none; 63 | } 64 | 65 | li:first-child a { 66 | background: #595959; 67 | border-radius: 0 0 0 .25em; 68 | } 69 | 70 | @include hasHover() { 71 | li:first-child a:hover { 72 | background-color: #656565; 73 | } 74 | } 75 | 76 | 77 | li:last-child a { 78 | background: #9b9b9b; 79 | border-radius: 0 0 .25em 0; 80 | } 81 | 82 | @include hasHover() { 83 | li:last-child a:hover { 84 | background-color: #8d8d8d; 85 | } 86 | } 87 | } 88 | } 89 | 90 | .modal-music-search { 91 | @include modalBase(); 92 | 93 | &__inner { 94 | position: absolute; 95 | width: calc(100% - 40px); 96 | height: calc(100% - 100px); 97 | max-width: 1024px; 98 | max-height: 1024px; 99 | margin: auto; 100 | top: 0; 101 | bottom: 0; 102 | left: 0; 103 | right: 0; 104 | background: rgba(31, 31, 31, 0.9); 105 | border-radius: 20px; 106 | text-align: center; 107 | box-shadow: 0 0 20px rgba(0, 0, 0, 0.2); 108 | transform: translateY(-40px); 109 | backface-visibility: hidden; 110 | transition: transform 0.3s; 111 | display: flex; 112 | flex-direction: column; 113 | 114 | } 115 | 116 | &--active &__inner { 117 | transform: translateY(0); 118 | } 119 | 120 | &__inner &__close { 121 | background-color: rgb(42, 42, 42); 122 | height: 40px; 123 | width: 40px; 124 | right: 0; 125 | top: 0; 126 | transform: translate(30%, -30%); 127 | border-radius: 50%; 128 | 129 | @include rwd($screen-pad-portrait) { 130 | width: 30px; 131 | height: 30px; 132 | } 133 | 134 | &:before, 135 | &:after { 136 | background-color: #fff; 137 | height: 50%; 138 | } 139 | } 140 | 141 | &__header { 142 | padding: 30px; 143 | background-color: rgba(128, 128, 128, 0.482); 144 | border-top-left-radius: 20px; 145 | border-top-right-radius: 20px; 146 | 147 | @include rwd($screen-pad-portrait) { 148 | padding: 20px; 149 | } 150 | } 151 | 152 | &__header .input-block { 153 | max-width: 600px; 154 | position: relative; 155 | margin: 0 auto; 156 | 157 | input { 158 | font-size: 1.5rem; 159 | 160 | @include rwd($screen-pad-portrait) { 161 | font-size: 1rem; 162 | } 163 | } 164 | 165 | } 166 | 167 | &__header .input-block__button { 168 | width: 30px; 169 | height: 30px; 170 | 171 | @include rwd($screen-pad-portrait) { 172 | width: 20px; 173 | height: 20px; 174 | } 175 | } 176 | 177 | &__body { 178 | border-top: 1px solid gray; 179 | display: flex; 180 | align-items: center; 181 | justify-content: center; 182 | overflow: hidden; 183 | 184 | } 185 | 186 | 187 | &__inner &__header { 188 | flex: none; 189 | } 190 | 191 | &__inner &__body { 192 | flex: 1; 193 | } 194 | 195 | 196 | 197 | &__no-result { 198 | display: none; 199 | } 200 | 201 | &__ul:not(:empty) { 202 | padding-left: 50px; 203 | padding-right: 50px; 204 | height: 100%; 205 | width: 100%; 206 | overflow: auto; 207 | @include custom-scroll(); 208 | 209 | @include rwd($screen-pad-portrait) { 210 | padding-left: 20px; 211 | padding-right: 20px; 212 | } 213 | 214 | 215 | &:before, 216 | &:after { 217 | content: ''; 218 | display: block; 219 | height: 50px; 220 | width: 100%; 221 | 222 | @include rwd($screen-pad-portrait) { 223 | height: 30px; 224 | } 225 | 226 | } 227 | } 228 | 229 | &__ul:empty { 230 | height: auto; 231 | } 232 | 233 | &__ul:empty+&__no-result { 234 | display: block; 235 | } 236 | 237 | &__li { 238 | opacity: 0; 239 | animation: fadeIndownSmall 500ms forwards; 240 | 241 | &--loading { 242 | position: relative; 243 | top: calc(50% - 50px); 244 | width: 200px; 245 | height: 200px; 246 | left: 50%; 247 | transform: translate(-50%, -50%); 248 | animation: none; 249 | opacity: 0.35; 250 | } 251 | } 252 | 253 | &__li+&__li { 254 | border-top: 1px solid rgba(255, 255, 255, 0.1); 255 | } 256 | 257 | 258 | } 259 | 260 | .music-search-item { 261 | user-select: none; 262 | 263 | &__inner { 264 | display: flex; 265 | padding: 20px; 266 | align-items: center; 267 | 268 | @include rwd($screen-pad-portrait) { 269 | padding-left: 0; 270 | padding-right: 0; 271 | } 272 | } 273 | 274 | &__img { 275 | width: 100px; 276 | border-radius: 50%; 277 | opacity: 0.5; 278 | transition: opacity 500ms; 279 | overflow: hidden; 280 | flex: none; 281 | position: relative; 282 | 283 | @include rwd($screen-pad-portrait) { 284 | width: 50px; 285 | } 286 | 287 | &:after { 288 | content: ''; 289 | display: block; 290 | padding-bottom: 100%; 291 | } 292 | 293 | img { 294 | position: absolute; 295 | left: 0; 296 | top: 0; 297 | width: 100%; 298 | } 299 | } 300 | 301 | &__img--not-found { 302 | background-image: url(~@img/not-found.jpg); 303 | background-size: cover; 304 | background-position: center; 305 | 306 | img { 307 | opacity: 0; 308 | } 309 | } 310 | 311 | @include hasHover() { 312 | &:hover &__img { 313 | opacity: 1; 314 | } 315 | } 316 | 317 | &__head { 318 | flex: none; 319 | } 320 | 321 | &__head+&__body { 322 | margin-left: 50px; 323 | 324 | @include rwd($screen-pad-portrait) { 325 | margin-left: 20px; 326 | } 327 | 328 | } 329 | 330 | &__body { 331 | text-align: left; 332 | flex: 1; 333 | overflow: hidden; 334 | } 335 | 336 | &__body+&__play { 337 | margin-left: 50px; 338 | 339 | @include rwd($screen-pad-portrait) { 340 | margin-left: 20px; 341 | } 342 | } 343 | 344 | &__play { 345 | width: 30px; 346 | height: 30px; 347 | flex: none; 348 | border-radius: 50%; 349 | background-image: url(~@img/play.svg); 350 | background-size: cover; 351 | opacity: 0.35; 352 | transition: opacity 500ms; 353 | background-color: transparent; 354 | border: none; 355 | cursor: pointer; 356 | } 357 | 358 | @include hasHover() { 359 | &:hover &__play { 360 | opacity: 0.5; 361 | } 362 | } 363 | 364 | 365 | &__title { 366 | position: relative; 367 | display: inline-flex; 368 | font-size: 1.25rem; 369 | color: rgba(255, 255, 255, 1); 370 | font-family: 'Roboto Condensed'; 371 | font-weight: 300; 372 | text-align: left; 373 | opacity: 0.5; 374 | transition: opacity 500ms; 375 | width: 100%; 376 | 377 | span { 378 | overflow: hidden; 379 | text-overflow: ellipsis; 380 | white-space: nowrap; 381 | } 382 | 383 | @include rwd($screen-pad-portrait) { 384 | font-size: 1rem; 385 | } 386 | } 387 | 388 | @include hasHover() { 389 | &:hover &__title { 390 | opacity: 0.75; 391 | } 392 | } 393 | 394 | @include hasHover() { 395 | &__title:hover { 396 | @include rwd($screen-pad-portrait) { 397 | span { 398 | text-overflow: clip; 399 | overflow: visible; 400 | display: inline-block; 401 | animation: marquee 10s linear infinite; 402 | } 403 | 404 | } 405 | } 406 | } 407 | 408 | &__title+&__descrp { 409 | margin-top: 20px; 410 | 411 | @include rwd($screen-pad-portrait) { 412 | margin-top: 10px; 413 | } 414 | } 415 | 416 | &__descrp { 417 | display: flex; 418 | align-items: center; 419 | color: $white; 420 | } 421 | 422 | &__artist, 423 | &__album-title { 424 | font-size: 14px; 425 | opacity: 0.5; 426 | transition: opacity 500ms; 427 | white-space: nowrap; 428 | text-overflow: ellipsis; 429 | overflow: hidden; 430 | max-width: 12em; 431 | 432 | @include rwd($screen-pad-portrait) { 433 | font-size: 12px; 434 | } 435 | } 436 | 437 | 438 | @include hasHover() { 439 | 440 | &:hover &__artist, 441 | &:hover &__album-title { 442 | opacity: 0.75; 443 | } 444 | } 445 | 446 | &__artist+&__album-title:not(:empty) { 447 | display: flex; 448 | align-items: center; 449 | 450 | &:before { 451 | content: '-'; 452 | display: block; 453 | font-size: 1rem; 454 | margin: 0 10px; 455 | 456 | @include rwd($screen-pad-portrait) { 457 | margin: 0 3px; 458 | } 459 | } 460 | } 461 | } -------------------------------------------------------------------------------- /src/scss/util/_mixins.scss: -------------------------------------------------------------------------------- 1 | // ----------------------------------------------------------------------------- 2 | // This file contains all application-wide Sass mixins. 3 | // ----------------------------------------------------------------------------- 4 | 5 | /// @example 若某元素有滑鼠互動特效時導入 6 | /// @param {Bool} $self [false] - 當值為 false時,不將本身納入樣式有效對象, 也就是說當值為true時, compile出來的css會是 .element,.element:hover,element:active,.element:focus{...} 7 | @mixin on-event($self: false) { 8 | @if $self { 9 | 10 | &, 11 | &:hover, 12 | &:active, 13 | &:focus { 14 | @content; 15 | } 16 | } 17 | 18 | @else { 19 | 20 | &:hover, 21 | &:active, 22 | &:focus { 23 | @content; 24 | } 25 | } 26 | } 27 | 28 | /// @example 單行字數若溢出容器最大寬度,將超出的字轉換為... 29 | /// @param {None} none 30 | @mixin text-ellipsis { 31 | text-overflow: ellipsis; 32 | white-space: nowrap; 33 | overflow: hidden; 34 | } 35 | 36 | /// @example 多行字數若溢出容器最大尺寸,將超出的字轉換為...(IE無效) 37 | /// @param {Num} howManyLine 參數請填段落最大行數(整數) 38 | @mixin multi-line-ellipsis($how-many-line: 3) { 39 | display: -webkit-box; 40 | -webkit-line-clamp: $how-many-line; 41 | -webkit-box-orient: vertical; 42 | overflow: hidden; 43 | } 44 | 45 | /// @example 將字體大小的px單位轉換為Rem單位 46 | /// @param {Num} size 參數請填瀏覽器字體需要成為的字體大小(數字) 47 | /// @param {Num} base 參數請填瀏覽器字體基準大小(數字) 48 | @mixin px-to-rem($size: 16, $base: 16) { 49 | font-size: $size; // fallback for old browsers 50 | font-size: $size / $base * 1rem; 51 | } 52 | 53 | /// @example 快速創建固定間距寬度(寬度可為各種長度單位)的flex grid system 54 | /// @param {Num} column-number 參數請填數字,grid欄數 55 | /// @param {Size} column-gutter 參數請填長度單位(px,rem,em,%,vw,...,etc), grid容器內部子容器的水平間距 56 | /// @param {Size} vertical-gutter 參數請填長度單位(px,rem,em,%,vw,...,etc), grid容器內部子容器的垂直間距, 預設等於column-gutter 57 | /// @param {String} grid-child-selector 參數請填gird容器內部子容器想要的class名稱, 預設為'*' 58 | /// @param {String} max-width 參數請填長度單位,gird容器的最大寬度 59 | /// @param {Size} box-sizing-fix 這個變數是為了要應對在Ie的flex bug, IE在flex-basis為純值的狀況下 60 | /// 會忽略掉boxing-sizing:border-box的設定,因此padding和border並不會被計算在flex-basis的範圍內,變成必須要手動扣除,這邊請填寫padding-left/padding-right & border-left/border-right的總和 61 | /// @param {String} min-screen-width 參數請填長度單位,呈現出這個 mixin 參數所導出的grid樣式時,其最小視窗寬度,預設為0 62 | @mixin fixed-gutter-flex-grid($column-number: 3, 63 | $column-gutter: 30px, 64 | $vertical-gutter: $column-gutter, 65 | $grid-child-selector: "*", 66 | $box-sizing-fix: 0px, 67 | $max-width: 100%, 68 | $min-screen-width: 0px, 69 | $max-screen-width: 999999px) { 70 | @media screen and (min-width: $min-screen-width) and (max-width: $max-screen-width) { 71 | width: 100%; 72 | display: inline-flex; 73 | max-width: $max-width; 74 | font-size: 0; 75 | flex-wrap: wrap; 76 | 77 | >#{$grid-child-selector} { 78 | display: inline-block; 79 | flex: none; 80 | flex-grow: 0; 81 | flex-shrink: 0; 82 | flex-basis: calc((100% - (#{$column-number} - 1) * #{$column-gutter}) /#{$column-number}); 83 | width: calc((100% - (#{$column-number} - 1) * #{$column-gutter}) /#{$column-number}); 84 | margin-right: #{$column-gutter}; 85 | margin-bottom: #{$vertical-gutter}; 86 | 87 | &:nth-child(#{$column-number}n) { 88 | margin-right: 0; 89 | } 90 | 91 | // @for $i from 1 through $column-number { 92 | // &:nth-last-child(#{$i}) { 93 | // margin-bottom: 0; 94 | // } 95 | // } 96 | 97 | } 98 | } 99 | 100 | @media all and (-ms-high-contrast: none) and (min-width: $min-screen-width) and (max-width: $max-screen-width) { 101 | >#{$grid-child-selector} { 102 | flex-basis: calc(((99.99% - (#{$column-number} - 1) * #{$column-gutter}) /#{$column-number}) - #{$box-sizing-fix}); 103 | width: calc(((99.99% - (#{$column-number} - 1) * #{$column-gutter}) /#{$column-number}) - #{$box-sizing-fix}); 104 | } 105 | 106 | } 107 | } 108 | 109 | /// @example 快速創建固定欄位寬度(寬度可為各種長度單位)的flex grid system 110 | /// @param {Num} column-number 參數請填數字,grid欄數 111 | /// @param {Size} vertical-gutter 參數請填長度單位(px,rem,em,%,vw,...,etc), gird容器內部子容器的垂直間距 112 | /// @param {Size} column-width 參數請填長度單位(px,rem,em,%,vw,...,etc), gird容器內部子容器的固定寬度 113 | /// @param {String} grid-child-selector 參數請填gird容器內部子容器想要的class名稱, 預設為'*' 114 | /// @param {Size} box-sizing-fix 這個變數是為了要應對在Ie的flex bug, IE在flex-basis為純值的狀況下 115 | /// 會忽略掉boxing-sizing:border-box的設定,因此padding和border並不會被計算在flex-basis的範圍內,變成必須要手動扣除,這邊請填寫padding-left/padding-right & border-left/border-right的總和 116 | /// @param {String} max-width 參數請填長度單位,gird容器的最大寬度 117 | /// @param {String} min-screen-width 參數請填長度單位。呈現出這個 mixin 參數所導出的grid樣式時,其最小視窗寬度,預設為0 118 | 119 | @mixin fixed-column-flex-grid($column-number: 3, 120 | $column-width: 50px, 121 | $vertical-gutter: 30px, 122 | $grid-child-selector: "*", 123 | $max-width: 100%, 124 | $box-sizing-fix: 0px, 125 | $min-screen-width: 0px, 126 | $max-screen-width: 9999999px) { 127 | $gutter-num: ($column-number - 1); 128 | 129 | @media screen and (min-width:#{$min-screen-width}) and (max-width:#{$max-screen-width}) { 130 | width: 100%; 131 | display: inline-flex; 132 | max-width: #{$max-width}; 133 | font-size: 0; 134 | flex-wrap: wrap; 135 | 136 | >#{$grid-child-selector} { 137 | display: inline-block; 138 | flex: none; 139 | flex-grow: 0; 140 | flex-shrink: 0; 141 | flex-basis: #{$column-width}; 142 | width: #{$column-width}; 143 | margin-right: calc((100% - #{$column-number} * #{$column-width}) / #{$gutter-num}); 144 | margin-bottom: #{$vertical-gutter}; 145 | 146 | &:nth-child(#{$column-number}n) { 147 | margin-right: 0; 148 | } 149 | 150 | // @for $i from 1 through $column-number { 151 | // &:nth-last-child(#{$i}) { 152 | // margin-bottom: 0; 153 | // } 154 | // } 155 | } 156 | } 157 | 158 | ///修正ie異常狀況 159 | @media screen and (-ms-high-contrast: none) and (min-width:#{$min-screen-width}) and (max-width:#{$max-screen-width}) { 160 | >#{$grid-child-selector} { 161 | margin-right: calc((99.99% - #{$column-number} * #{$column-width}) / #{$gutter-num}); 162 | flex-basis: calc(#{$column-width} - #{$box-sizing-fix}); 163 | width: calc(#{$column-width} - #{$box-sizing-fix}); 164 | 165 | } 166 | } 167 | } 168 | 169 | /// @example 快速創建CSS border 三角形 170 | /// @param {String} 171 | 172 | @mixin gen-triangle($orientation: "top", $arrow-width: 15px, $arrow-size: 20px, $arrow-color: #000) { 173 | width: 0; 174 | height: 0; 175 | border-style: solid; 176 | border-width: 0; 177 | border-color: transparent; 178 | display: inline-block; 179 | 180 | @if ($orientation=="left") { 181 | border-width: $arrow-width $arrow-size $arrow-width 0; 182 | border-right-color: $arrow-color; 183 | } 184 | 185 | @else if($orientation=="right") { 186 | border-width: $arrow-width 0 $arrow-size $arrow-width; 187 | border-left-color: $arrow-color; 188 | } 189 | 190 | @else if($orientation=="top") { 191 | border-width: 0 $arrow-width $arrow-size $arrow-width; 192 | border-bottom-color: $arrow-color; 193 | } 194 | 195 | @else if($orientation=="bottom") { 196 | border-width: $arrow-width $arrow-size 0 $arrow-width; 197 | border-top-color: $arrow-color; 198 | } 199 | } 200 | 201 | /// @example 快速創建CSS 半圓形 202 | /// @param {String} diameter 參數請填寬度(px) 203 | 204 | @mixin semi-circle-btm($diameter: 30px) { 205 | height: $diameter/2; 206 | width: $diameter; 207 | border-radius: 0 0 $diameter $diameter; 208 | padding: 0; 209 | } 210 | 211 | /// @example 快速創建禁用樣式 212 | /// @param {String} opacity 參數請填透明度小數 213 | 214 | @mixin status-disable($opacity: 0.3) { 215 | cursor: not-allowed; 216 | opacity: $opacity; 217 | 218 | * { 219 | pointer-events: none !important; 220 | } 221 | } 222 | 223 | /// @example 上下左右箭頭 224 | /// @param {String} $arrow-type 參數請填箭頭方向名稱,預設為'down' 225 | /// @param {Color} $arrow-border-color 參數請填箭頭顏色 226 | @mixin arrow($arrow-type: down, $arrow-border-color: 2px solid #f09813) { 227 | display: block; 228 | position: relative; 229 | cursor: pointer; 230 | 231 | &:after { 232 | position: absolute; 233 | content: ""; 234 | top: 50%; 235 | transform: translateY(-50%); 236 | right: 0; 237 | width: 12px; 238 | height: 12px; 239 | border-right: transparent; 240 | border-top: transparent; 241 | border-bottom: $arrow-border-color; 242 | border-left: $arrow-border-color; 243 | } 244 | 245 | @if $arrow-type==down { 246 | &:after { 247 | transform: translateY(-50%) rotate(-45deg); 248 | } 249 | } 250 | 251 | @else if $arrow-type==top { 252 | &:after { 253 | transform: translateY(-50%) rotate(135deg); 254 | } 255 | } 256 | 257 | @else if $arrow-type==right { 258 | &:after { 259 | transform: translateY(-50%) rotate(225deg); 260 | } 261 | } 262 | 263 | @else if $arrow-type==left { 264 | &:after { 265 | transform: translateY(-50%) rotate(45deg); 266 | } 267 | } 268 | 269 | @else { 270 | @error "請填寫$arrow-type正確參數:top/left/bottom/right"; 271 | } 272 | } 273 | 274 | /// @example 製作圓角方塊 275 | /// @param {Size} $width 參數請填寬度,預設為'27px' 276 | /// @param {Size} $height 參數請填高度,預設為'27px' 277 | /// @param {Size} $border-radius 參數請填圓角,預設為'0px 0 5px 0' 278 | /// @param {Color} $background 參數請填背景顏色 279 | 280 | @mixin squareArrow($width: 27px, $height: 27px, $border-radius: 0px 0 5px 0, $background: #f09813) { 281 | width: $width; 282 | height: $height; 283 | border-radius: $border-radius; 284 | background: $background; 285 | } 286 | 287 | /// @example 製作圓形 288 | /// @param {Size} $circle-size 參數請填圓形設定尺寸(寬/高/行高設定均相同),預設為'75px' 289 | /// @param {Size} $background 參數請填背景顏色 290 | 291 | @mixin circle($circle-size: 75px, $background: #f09813) { 292 | border-radius: 50%; 293 | width: $circle-size; 294 | height: $circle-size; 295 | line-height: $circle-size; 296 | background: $background; 297 | } 298 | 299 | 300 | /// @example 製作叉叉 301 | /// @param {Size} $block-width 參數請填產生叉叉的方塊的寬 302 | /// @param {Size} $block-height 參數請填產生叉叉的方塊的高 303 | /// @param {Size} $line-width:2px 參數請填叉叉的線寬 304 | /// @param {Size} $line-length:80% 參數請填叉叉的線長 305 | /// @param {Color} $line-color 參數請填叉叉的線色 306 | 307 | @mixin genCross($block-width: 50px, $block-height: 50px, $line-width: 2px, $line-length: 80%, $line-color: green) { 308 | position: relative; 309 | width: $block-width; 310 | height: $block-height; 311 | 312 | &:after, 313 | &:before { 314 | content: ''; 315 | display: block; 316 | height: $line-length; 317 | width: $line-width; 318 | position: absolute; 319 | left: 0; 320 | right: 0; 321 | margin: 0 auto; 322 | top: 50%; 323 | background-color: $line-color; 324 | transform-origin: 50% 50%; 325 | } 326 | 327 | &:after { 328 | transform: translateY(-50%) rotate(-45deg); 329 | } 330 | 331 | &:before { 332 | transform: translateY(-50%) rotate(45deg); 333 | } 334 | } 335 | 336 | 337 | 338 | @mixin flex-width($width: 0px, $box-sizing-fix: 0px) { 339 | flex-basis: $width; 340 | width: $width; 341 | flex-grow: 0; 342 | flex-shrink: 0; 343 | 344 | @media all and (-ms-high-contrast: none) { 345 | flex-basis: calc(#{$width} - #{$box-sizing-fix}); 346 | } 347 | } 348 | 349 | @mixin flex-height($height: 0px, $box-sizing-fix: 0px) { 350 | flex-basis: $height; 351 | height: $height; 352 | flex-grow: 0; 353 | flex-shrink: 0; 354 | 355 | @media all and (-ms-high-contrast: none) { 356 | flex-basis: calc(#{$height} - #{$box-sizing-fix}); 357 | } 358 | } 359 | 360 | /// @example 快速創建Device Width Media Query規則 361 | /// @param {String} $breakpoint 參數請填長度單位,裝置斷點寬度 362 | 363 | @mixin rwd($breakpoint) { 364 | @media (max-width: $breakpoint) { 365 | @content; 366 | } 367 | } 368 | 369 | 370 | @mixin min-rwd($breakpoint) { 371 | @media (min-width: $breakpoint) { 372 | @content; 373 | } 374 | } 375 | 376 | 377 | @mixin between-rwd($min-screen-width, $max-screen-width) { 378 | @media screen and (min-width:$min-screen-width) and (max-width: $max-screen-width) { 379 | @content; 380 | } 381 | } 382 | 383 | @mixin gradient-border( // 384 | $padding: 10px, 385 | // padding-box 留白量 386 | $border-width: 3px, 387 | //邊框寬度 388 | $border-radius: 10px, 389 | //圓角 390 | $backdrop-filter: none, 391 | //padding-box 的backdrop-filter屬性 392 | $outer-shadow: none, 393 | // 外光暈 394 | $inner-shadow: none, 395 | //內光暈 inner shadow 要記得補 inset 396 | $border-image: // border的漸層或是顏色 397 | radial-gradient(circle, rgba(63, 94, 251, 1) 0%, rgba(252, 70, 107, 1) 100%), 398 | $inner-image: // 內部 padding-box的漸層或是顏色 399 | 400 | rgb(28, 28, 31)) { 401 | position: relative; 402 | padding: $padding; 403 | background: $inner-image; 404 | border-radius: $border-radius; 405 | box-shadow: $outer-shadow; 406 | 407 | @if ($backdrop-filter) { 408 | backdrop-filter: $backdrop-filter; 409 | -webkit-backdrop-filter: $backdrop-filter; 410 | } 411 | 412 | &:before { 413 | pointer-events: none; 414 | content: ""; 415 | position: absolute; 416 | top: 0; 417 | left: 0; 418 | right: 0; 419 | bottom: 0; 420 | inset: 0; 421 | display: block; 422 | border-radius: $border-radius; 423 | padding: $border-width; 424 | background: $border-image border-box; 425 | -webkit-mask: linear-gradient(#fff 0 0) content-box, 426 | linear-gradient(#fff 0 0); 427 | -webkit-mask-composite: xor; 428 | mask-composite: exclude; 429 | pointer-events: none; 430 | } 431 | 432 | &:after { 433 | pointer-events: none; 434 | content: ""; 435 | display: block; 436 | position: absolute; 437 | top: 0; 438 | left: 0; 439 | right: 0; 440 | bottom: 0; 441 | border-radius: $border-radius; 442 | border: $border-width solid transparent; 443 | box-shadow: $inner-shadow; 444 | } 445 | } 446 | 447 | 448 | @mixin custom-scroll { 449 | scrollbar-width: auto; 450 | scrollbar-color: #7e7a7f #ffffff; 451 | 452 | /* Chrome, Edge, and Safari */ 453 | &::-webkit-scrollbar { 454 | width: 16px; 455 | 456 | @include rwd($screen-pad-portrait) { 457 | width: 5px; 458 | } 459 | } 460 | 461 | &::-webkit-scrollbar-track { 462 | background: transparent; 463 | } 464 | 465 | &::-webkit-scrollbar-thumb { 466 | background-color: #7e7a7f; 467 | border-radius: 10px; 468 | border: none; 469 | } 470 | } 471 | 472 | 473 | @mixin modalBase() { 474 | position: fixed; 475 | left: 0; 476 | top: 0; 477 | height: 100%; 478 | width: 100%; 479 | background-color: rgba(100, 100, 100, 0.482); 480 | opacity: 0; 481 | visibility: hidden; 482 | transition: opacity 0.3s 0s, visibility 0s 0.3s; 483 | z-index: 99; 484 | 485 | &--active { 486 | opacity: 1; 487 | visibility: visible; 488 | transition: opacity 0.3s 0s, visibility 0s 0s; 489 | } 490 | 491 | &__inner &__close { 492 | position: absolute; 493 | top: 10px; 494 | right: 10px; 495 | width: 15px; 496 | height: 15px; 497 | } 498 | 499 | &__close::before, 500 | &__close::after { 501 | content: ''; 502 | position: absolute; 503 | left: 0; 504 | right: 0; 505 | top: 0; 506 | bottom: 0; 507 | margin: auto; 508 | width: 3px; 509 | height: 100%; 510 | background-color: #8f9cb5; 511 | transform-origin: 50% 50%; 512 | } 513 | 514 | &__close::before { 515 | transform: rotate(45deg); 516 | } 517 | 518 | &__close::after { 519 | transform: rotate(-45deg); 520 | } 521 | } 522 | 523 | @mixin hasHover() { 524 | @media(hover:hover) { 525 | @content 526 | } 527 | } -------------------------------------------------------------------------------- /src/ts/util/waveform.ts: -------------------------------------------------------------------------------- 1 | /*This is a modified version of https://github.com/samcrosoft/waveformjs, the original one is written in coffee script, and I rewrite it into typescript*/ 2 | 3 | import { EventEmitter } from "./event-emitter"; 4 | 5 | interface WaveformOptions { 6 | container?: HTMLElement 7 | canvas?: HTMLCanvasElement 8 | trackLength?: number 9 | data?: number[] 10 | outerColor?: string 11 | reflection?: number 12 | interpolate?: boolean 13 | bindResize?: boolean 14 | fadeOpacity?: number 15 | width?: number 16 | height?: number 17 | gutterWidth?: number 18 | waveWidth?: number 19 | } 20 | 21 | export const DEFAULT_MAX_OPACITY = 1 22 | export const DEFAULT_MIN_OPACITY = 0.2 23 | export const WAVE_FOCUS = 'waveFocus' 24 | export const WAVE = 'wave' 25 | export const WAVE_ACTIVE = 'waveActive' 26 | export const WAVE_SELECTED = 'waveSelected' 27 | export const GUTTER = 'gutter' 28 | export const GUTTER_ACTIVE = 'gutterActive' 29 | export const GUTTER_SELECTED = 'gutterSelected' 30 | export const REFLECTION = 'reflection' 31 | export const REFLECTION_ACTIVE = 'reflectionActive' 32 | export const EVENT_READY = "ready" 33 | export const EVENT_CLICK = "click" 34 | export const EVENT_HOVER = "hover" 35 | export const EVENT_RESIZED = "hover" 36 | 37 | export class Waveform extends EventEmitter { 38 | readonly DEFAULT_MAX_OPACITY = DEFAULT_MAX_OPACITY 39 | readonly DEFAULT_MIN_OPACITY = DEFAULT_MIN_OPACITY 40 | readonly WAVE_FOCUS = WAVE_FOCUS 41 | readonly WAVE = WAVE 42 | readonly WAVE_ACTIVE = WAVE_ACTIVE 43 | readonly WAVE_SELECTED = WAVE_SELECTED 44 | readonly GUTTER = GUTTER 45 | readonly GUTTER_ACTIVE = GUTTER_ACTIVE 46 | readonly GUTTER_SELECTED = GUTTER_SELECTED 47 | readonly REFLECTION = REFLECTION 48 | readonly REFLECTION_ACTIVE = REFLECTION_ACTIVE 49 | readonly EVENT_READY = EVENT_READY 50 | readonly EVENT_CLICK = EVENT_CLICK 51 | readonly EVENT_HOVER = EVENT_HOVER 52 | readonly EVENT_RESIZED = EVENT_RESIZED 53 | private container: HTMLElement 54 | private canvas: HTMLCanvasElement 55 | private data: number[] = [] 56 | private outerColor = 'transparent' 57 | private reflection = 0 58 | private interpolate = true 59 | private bindResize = false 60 | private fadeOpacity = 0.5 61 | private wavesCollection: number[] = [] 62 | private context: CanvasRenderingContext2D 63 | private width = 0 64 | private height = 0 65 | private waveWidth = 2; 66 | private gutterWidth = 1; 67 | private waveOffset = 0; 68 | private reflectionHeight = 0; 69 | private waveHeight = 0; 70 | private colors: { [key: string]: (string | CanvasGradient) } = {}; 71 | private active = -1; 72 | private clickPercent = 0; 73 | private selected = -1; 74 | private isPlaying = false; 75 | private hasStartedPlaying = false; 76 | private trackLength = 0; 77 | constructor(options: WaveformOptions) { 78 | super() 79 | Object.assign(this, options); 80 | if (!this.canvas) { 81 | if (this.container) { 82 | this.spawnCanvas(options.width || this.container.clientWidth, options.height || this.container.clientHeight); 83 | this.width = parseInt(this.context.canvas.width.toString(), 10); 84 | this.height = parseInt(this.context.canvas.height.toString(), 10); 85 | } else { 86 | throw 'Either canvas or container option must be passed'; 87 | } 88 | } 89 | 90 | this.fadeOpacity = this.fadeOpacity || this.DEFAULT_MAX_OPACITY; 91 | if (isNaN(this.fadeOpacity)) { 92 | throw new Error('Fade Opacity Can Only Be A Number'); 93 | } else if (this.fadeOpacity < this.DEFAULT_MIN_OPACITY || this.fadeOpacity > this.DEFAULT_MAX_OPACITY) { 94 | throw new Error("Fade Opacity Can Only Be A Number Between " + this.DEFAULT_MIN_OPACITY + " and " + this.DEFAULT_MAX_OPACITY); 95 | } 96 | 97 | this.initialize(); 98 | } 99 | 100 | private spawnCanvas(width: number, height: number) { 101 | this.canvas = document.createElement("canvas"); 102 | this.container.appendChild(this.canvas); 103 | this.canvas.width = width; 104 | this.canvas.height = height; 105 | this.context = this.canvas.getContext('2d'); 106 | }; 107 | 108 | private initialize() { 109 | this.updateHeight(); 110 | this.setColors(); 111 | this.bindEventHandlers(); 112 | this.cache(); 113 | this.redraw(); 114 | if (this.bindResize === true) { 115 | this.bindContainerResize(); 116 | } 117 | this.fireEvent(this.EVENT_READY); 118 | }; 119 | 120 | private updateHeight() { 121 | this.waveOffset = Math.round(this.height - (this.height * this.reflection)); 122 | this.reflectionHeight = Math.round(this.height - this.waveOffset); 123 | this.waveHeight = this.height - this.reflectionHeight; 124 | }; 125 | 126 | private setColors() { 127 | this.setColor(this.WAVE_FOCUS, '#333333'); 128 | this.setGradient(this.WAVE, [{ color: '#666666', offset: 0 }, { color: '#868686', offset: 1 }]); 129 | this.setGradient(this.WAVE_ACTIVE, [{ color: '#FF3300', offset: 0 }, { color: '#FF5100', offset: 1 }]); 130 | this.setGradient(this.WAVE_SELECTED, [{ color: '#993016', offset: 0 }, { color: '#973C15', offset: 1 }]); 131 | this.setGradient(this.GUTTER, [{ color: '#6B6B6B', offset: 0 }, { color: '#c9c9c9', offset: 1 }]); 132 | this.setGradient(this.GUTTER_ACTIVE, [{ color: '#FF3704', offset: 0 }, { color: '#FF8F63', offset: 1 }]); 133 | this.setGradient(this.GUTTER_SELECTED, [{ color: '#9A371E', offset: 0 }, { color: '#CE9E8A', offset: 1 }]); 134 | this.setColor(this.REFLECTION, '#999999'); 135 | this.setColor(this.REFLECTION_ACTIVE, '#FFC0A0'); 136 | } 137 | 138 | private setColor(name: string, color: string) { 139 | this.colors[name] = color; 140 | } 141 | 142 | private setGradient(name: string, colorObjs: { color: string, offset: number }[]) { 143 | let gradient: CanvasGradient, i: number; 144 | gradient = this.context.createLinearGradient(0, this.waveOffset, 0, 0); 145 | i = 0; 146 | while (i < colorObjs.length) { 147 | gradient.addColorStop(colorObjs[i].offset, colorObjs[i].color); 148 | i += 1; 149 | } 150 | this.colors[name] = gradient; 151 | }; 152 | 153 | private getMouseClickPosition(evt: MouseEvent) { 154 | let canvas: HTMLCanvasElement, rect: DOMRect, x: number, y: number; 155 | canvas = this.canvas; 156 | rect = canvas.getBoundingClientRect(); 157 | x = Math.round((evt.clientX - rect.left) / (rect.right - rect.left) * canvas.width); 158 | y = Math.round((evt.clientY - rect.top) / (rect.bottom - rect.top) * canvas.height); 159 | return [x, y]; 160 | }; 161 | 162 | private fireEvent(name: string, ...data: any[]) { 163 | this.trigger(name, data); 164 | }; 165 | 166 | private bindEventHandlers() { 167 | this.canvas.addEventListener('mousedown', this.onMouseDown.bind(this)); 168 | this.canvas.addEventListener('mousemove', this.onMouseOver.bind(this)); 169 | this.canvas.addEventListener('mouseout', this.onMouseOut.bind(this)); 170 | }; 171 | 172 | private onMouseOut(e: MouseEvent) { 173 | this.selected = -1; 174 | this.redraw(); 175 | }; 176 | 177 | private onMouseOver(e: MouseEvent) { 178 | let 179 | aPos: number[],//coordinate in [x,y] format 180 | mousePosTrackTime: number, 181 | waveClicked: number, 182 | x: number; 183 | if (this.hasStartedPlaying === true && this.isPaused() === true) { 184 | return true; 185 | } 186 | aPos = this.getMouseClickPosition(e); 187 | x = aPos[0]; 188 | waveClicked = this.getWaveClicked(x); 189 | mousePosTrackTime = this.getMousePosTrackTime(x); 190 | this.fireEvent(this.EVENT_HOVER, mousePosTrackTime, waveClicked); 191 | this.selected = waveClicked; 192 | this.redraw(); 193 | }; 194 | 195 | private onMouseDown(e: MouseEvent) { 196 | let aPos: number[],//coordinate in [x,y] format 197 | x: number; 198 | aPos = this.getMouseClickPosition(e); 199 | x = aPos[0]; 200 | this.clickPercent = x / this.width; 201 | this.fireEvent(this.EVENT_CLICK, this.clickPercent * 100); 202 | this.active = this.calcPercent(); 203 | this.redraw(); 204 | }; 205 | 206 | private bindContainerResize() { 207 | window.addEventListener("resize", () => { 208 | return () => { 209 | let iContWidth: number; 210 | iContWidth = this.container.clientWidth; 211 | this.update({ 212 | width: iContWidth 213 | }); 214 | this.redraw(); 215 | return this.trigger(this.EVENT_RESIZED, [iContWidth]); 216 | }; 217 | }); 218 | }; 219 | 220 | 221 | private setPlaying(val = true) { 222 | this.isPlaying = val; 223 | }; 224 | 225 | private setPaused() { 226 | this.setPlaying(false); 227 | }; 228 | 229 | isPaused() { 230 | return this.active > 0 && this.isPlaying === false; 231 | }; 232 | 233 | play(perct: number) { 234 | this.playProgress(perct); 235 | }; 236 | 237 | pause() { 238 | this.setPaused(); 239 | console.log("is paused is ", this.isPaused()); 240 | }; 241 | 242 | playProgress(perct: number) { 243 | let iActive: number; 244 | if (this.hasStartedPlaying === null) { 245 | this.hasStartedPlaying = true; 246 | } 247 | if (this.isPlaying === false) { 248 | this.setPlaying(true); 249 | } 250 | iActive = Math.round((perct / 100) * this.wavesCollection.length); 251 | this.active = iActive; 252 | this.redraw(); 253 | }; 254 | 255 | private calcPercent() { 256 | return Math.round(this.clickPercent * this.width / (this.waveWidth + this.gutterWidth)); 257 | }; 258 | 259 | private getWaveClicked(x: number) { 260 | let fReturn: number, waveClicked: number; 261 | waveClicked = Math.round(x / (this.waveWidth + this.gutterWidth)); 262 | fReturn = 0; 263 | if (waveClicked > this.wavesCollection.length) { 264 | fReturn = this.wavesCollection.length; 265 | } else if (waveClicked < 0) { 266 | fReturn = 0; 267 | } else { 268 | fReturn = waveClicked; 269 | } 270 | return fReturn; 271 | }; 272 | 273 | private getMousePosTrackTime(x: number) { 274 | let fReturn: number, mousePosTrackTime: number; 275 | mousePosTrackTime = this.trackLength / this.wavesCollection.length * this.getWaveClicked(x); 276 | fReturn = 0; 277 | if (mousePosTrackTime > this.trackLength) { 278 | fReturn = this.trackLength; 279 | } else if (mousePosTrackTime < 0) { 280 | fReturn = 0; 281 | } else { 282 | fReturn = mousePosTrackTime; 283 | } 284 | return fReturn; 285 | }; 286 | 287 | private redraw() { 288 | requestAnimationFrame(this.render.bind(this)); 289 | }; 290 | 291 | private render() { 292 | let d: number, dNext: number, gutterX: number, i: number, j: number, len: number, ref: number[], reflectionHeight: number, results: number[], t: number, xPos: number, yPos: number; 293 | i = 0; 294 | ref = this.wavesCollection; 295 | t = this.width / this.data.length; 296 | xPos = 0; 297 | yPos = this.waveOffset; 298 | this.clear(); 299 | j = 0; 300 | len = ref.length; 301 | results = []; 302 | while (j < len) { 303 | d = ref[j]; 304 | dNext = ref[j + 1]; 305 | 306 | /* 307 | Draw the wave here 308 | */ 309 | if (this.selected > 0 && (this.selected <= j && j < this.active) || (this.selected > j && j >= this.active)) { 310 | this.context.fillStyle = this.colors[this.WAVE_SELECTED]; 311 | } else if (this.active > j) { 312 | this.context.fillStyle = this.colors[this.WAVE_ACTIVE]; 313 | } else { 314 | this.context.fillStyle = this.colors[this.WAVE_FOCUS]; 315 | } 316 | 317 | this.context.fillRect(xPos, yPos, this.waveWidth, d); 318 | 319 | /* 320 | draw the gutter 321 | */ 322 | if (this.selected > 0 && (this.selected <= j && j < this.active) || (this.selected > j && j >= this.active)) { 323 | this.context.fillStyle = this.colors[this.GUTTER_SELECTED]; 324 | } else if (this.active > j) { 325 | this.context.fillStyle = this.colors[this.GUTTER_ACTIVE]; 326 | } else { 327 | this.context.fillStyle = this.colors[this.GUTTER]; 328 | } 329 | gutterX = Math.max(d, dNext); 330 | this.context.fillRect(xPos + this.waveWidth, yPos, this.gutterWidth, gutterX); 331 | 332 | /* 333 | draw the reflection 334 | */ 335 | if (this.reflection > 0) { 336 | reflectionHeight = Math.abs(d) / (1 - this.reflection) * this.reflection; 337 | if (this.active > i) { 338 | this.context.fillStyle = this.colors[this.REFLECTION_ACTIVE]; 339 | } else { 340 | this.context.fillStyle = this.colors[this.REFLECTION]; 341 | } 342 | this.context.fillRect(xPos, yPos, this.waveWidth, reflectionHeight); 343 | } 344 | xPos += this.waveWidth + this.gutterWidth; 345 | results.push(j++); 346 | } 347 | return results; 348 | }; 349 | 350 | private clear() { 351 | this.context.fillStyle = this.outerColor; 352 | this.context.clearRect(0, 0, this.width, this.height); 353 | return this.context.fillRect(0, 0, this.width, this.height); 354 | }; 355 | 356 | 357 | /* 358 | Data related codes 359 | */ 360 | 361 | private setData(data: number[]) { 362 | return this.data = data; 363 | }; 364 | 365 | private getData() { 366 | return this.data; 367 | }; 368 | 369 | private setDataInterpolated(data: number[]) { 370 | return this.setData(this.interpolateArray(data, this.width)); 371 | }; 372 | 373 | private setDataCropped(data: number[]) { 374 | return this.setData(this.expandArray(data, this.width)); 375 | }; 376 | 377 | private linearInterpolate(before: number, after: number, atPoint: number) { 378 | return before + (after - before) * atPoint; 379 | }; 380 | 381 | private expandArray(data: number[], limit: number, defaultValue = 0) { 382 | let i: number, j: number, newData: number[] = [], ref: number; 383 | if (defaultValue === null) { 384 | defaultValue = 0; 385 | } 386 | if (data.length > limit) { 387 | newData = data.slice(data.length - limit, data.length); 388 | } else { 389 | i = j = 0; 390 | ref = limit - 1; 391 | while ((0 <= ref ? j <= ref : j >= ref)) { 392 | newData[i] = data[i] || defaultValue; 393 | i = (0 <= ref ? ++j : --j); 394 | } 395 | } 396 | return newData; 397 | }; 398 | 399 | private interpolateArray(data: number[], fitCount: number) { 400 | let after: number, atPoint: number, before: number, i: number, newData: number[] = [], springFactor: number, tmp: number; 401 | springFactor = (data.length - 1) / (fitCount - 1); 402 | newData[0] = data[0]; 403 | i = 1; 404 | while (i < fitCount - 1) { 405 | tmp = i * springFactor; 406 | before = Math.floor(tmp); 407 | after = Math.ceil(tmp); 408 | atPoint = tmp - before; 409 | newData[i] = this.linearInterpolate(data[before], data[after], atPoint); 410 | i++; 411 | } 412 | newData[fitCount - 1] = data[data.length - 1]; 413 | return newData; 414 | }; 415 | 416 | private putDataIntoWaveBlock() { 417 | let data: number[], fAbsValue: number, fAverage: number, fWavePoint: number, i: number, iWaveBlock: number, iWaveCount: number, j: number, key: number, newDataBlocks: number[], sum: number; 418 | iWaveBlock = this.waveWidth + this.gutterWidth; 419 | data = this.getData(); 420 | newDataBlocks = []; 421 | iWaveCount = Math.ceil(data.length / iWaveBlock); 422 | i = 0; 423 | while (i < iWaveCount) { 424 | sum = 0; 425 | j = 0; 426 | while (j < iWaveBlock) { 427 | key = (i * iWaveBlock) + j; 428 | sum += data[key]; 429 | j++; 430 | } 431 | fAverage = sum / iWaveBlock; 432 | fAbsValue = fAverage * this.waveHeight; 433 | fWavePoint = Math.floor(-Math.abs(fAbsValue)); 434 | newDataBlocks.push(fWavePoint); 435 | i++; 436 | } 437 | return newDataBlocks; 438 | }; 439 | 440 | private cache() { 441 | if (this.interpolate === false) { 442 | this.setDataCropped(this.data); 443 | } else { 444 | this.setDataInterpolated(this.data); 445 | } 446 | this.wavesCollection = this.putDataIntoWaveBlock(); 447 | }; 448 | 449 | 450 | /* 451 | Data update details here 452 | */ 453 | 454 | private update(options: WaveformOptions) { 455 | if (options) { 456 | if (options.gutterWidth) { 457 | this.gutterWidth = options.gutterWidth; 458 | } 459 | if (options.waveWidth) { 460 | this.waveWidth = options.waveWidth; 461 | } 462 | if (options.width) { 463 | this.width = options.width; 464 | this.canvas.width = this.width; 465 | } 466 | if (options.height) { 467 | this.height = options.height; 468 | this.canvas.height = this.height; 469 | } 470 | if (options.reflection === 0 || options.reflection) { 471 | this.reflection = options.reflection; 472 | } 473 | if (options.interpolate) { 474 | this.interpolate = options.interpolate; 475 | } 476 | 477 | /* 478 | Re-calculate the wave block formations once one of the following is altered 479 | */ 480 | if (options.gutterWidth || options.waveWidth || options.width || options.height || options.reflection || options.interpolate || options.reflection === 0) { 481 | this.cache(); 482 | } 483 | if (options.height || options.reflection || options.reflection === 0) { 484 | this.updateHeight(); 485 | } 486 | } 487 | this.redraw(); 488 | }; 489 | 490 | 491 | } 492 | --------------------------------------------------------------------------------