首頁

【Git】 取消上一次commit或push

前端達(dá)人

(轉(zhuǎn)自csdn)

目錄

一、取消上一次commit

方法1:使用 Git reset

方法2:使用 Git revert

方法3:使用 Git checkout

二、取消上一次push

git push --force

git reflog

git reset

git push --force

三、擴(kuò)展

commit參數(shù)

--mixed

--soft

--hard

--amend


一、取消上一次commit

如果你需要取消上一次的 Git 提交,有幾個不同的方法可以實(shí)現(xiàn)。其中包括撤消提交、提交到新的分支、使用 Git 回滾等等。

下面介紹三種方法:

方法1:使用 Git reset

使用 Git reset 命令來取消上一次提交

git reset HEAD~1

這會把 HEAD 指針移回上一個提交(HEAD~1),并清除最后一次提交的內(nèi)容。

git reset HEAD^

撤回兩次或者n次

git reset HEAD~2

方法2:使用 Git revert

使用 Git revert 命令來撤消上一次提交并創(chuàng)建一個新的提交來撤消原來的提交:

git revert HEAD

這會創(chuàng)建一個新的提交來撤消提交之前的更改。在命令行中輸入該命令后,你需要編輯撤消的提交信息,以便 Git 創(chuàng)建一個新的提交。

方法3:使用 Git checkout

還可以使用 Git checkout 命令來將工作樹恢復(fù)到上一次提交的狀態(tài)。這將清除所有未提交的更改,所以請確保你有一個備份:

git checkout HEAD~1

這會將工作樹恢復(fù)到上一次提交的狀態(tài),也就是你上一次提交之前的狀態(tài)。請注意,這里的修改都將被丟棄。

無論你使用的是哪種方法,請確保在撤消提交之前首先備份你的工作。這可以幫助你避免在操作過程中意外刪除無法恢復(fù)的內(nèi)容。

二、取消上一次push

如果你已經(jīng)push了代碼,并且想要撤回這個commit,可以通過以下步驟實(shí)現(xiàn):

git push --force

首先,在使用git push命令時,需要加上--force參數(shù),強(qiáng)制覆蓋遠(yuǎn)程倉庫上已經(jīng)存在的commit。命令如下:

git push --force origin <branch_name>

其中,<branch_name>表示你要撤銷的分支名稱。

git reflog

如果在本地倉庫沒有回到該commit的上一個狀態(tài),需要使用git reflog命令找到該commit的SHA-1值。命令如下:

git reflog

該命令會列出整個Git倉庫的提交歷史記錄,包括HEAD指針?biāo)赶虻奶峤缓鸵呀?jīng)被廢棄的提交。

git reset

找到要回到的某個commit的SHA-1值,然后使用如下命令回到該commit的狀態(tài):

git reset --hard <commit_SHA-1>

其中,<commit_SHA-1>表示要回到的commit的SHA-1值。

git push --force

然后使用之前的推送命令進(jìn)行推送,添加--force參數(shù),覆蓋遠(yuǎn)程倉庫的歷史提交記錄。命令如下:

git push --force origin <branch_name>

提醒:使用git push --force命令可能會導(dǎo)致遠(yuǎn)程倉庫、其他成員的倉庫和歷史版本產(chǎn)生不可逆的影響,因此操作時需要謹(jǐn)慎。一般情況下,在工作流中使用git revert命令回滾某個commit,以保證版本控制的完整性和可維護(hù)性。

三、擴(kuò)展

git log  查看提交日志

commit參數(shù)

--mixed

 不刪除工作空間改動代碼,撤銷 commit,并撤銷 git add . 操作

 git reset --mixed HEAD^ 效果等同 git reset HEAD^

--soft

    不刪除工作空間改動代碼,撤銷commit,不撤銷git add .

--hard

    刪除工作空間改動代碼,撤銷commit,撤銷git add .

--amend

修改注釋, 進(jìn)入vim編輯器, 改完:wq即可

 

藍(lán)藍(lán)設(shè)計(jì)(sillybuy.com )是一家專注而深入的界面設(shè)計(jì)公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的大數(shù)據(jù)可視化界面設(shè)計(jì)、B端界面設(shè)計(jì)、桌面端界面設(shè)計(jì)、APP界面設(shè)計(jì)、圖標(biāo)定制、用戶體驗(yàn)設(shè)計(jì)、交互設(shè)計(jì)UI咨詢、高端網(wǎng)站設(shè)計(jì)平面設(shè)計(jì),以及相關(guān)的軟件開發(fā)服務(wù),咨詢電話:01063334945。

關(guān)鍵詞:UI咨詢、UI設(shè)計(jì)服務(wù)公司軟件界面設(shè)計(jì)公司、界面設(shè)計(jì)公司、UI設(shè)計(jì)公司、UI交互設(shè)計(jì)公司、數(shù)據(jù)可視化設(shè)計(jì)公司、用戶體驗(yàn)公司、高端網(wǎng)站設(shè)計(jì)公司

銀行金融軟件UI界面設(shè)計(jì)能源及監(jiān)控軟件UI界面設(shè)計(jì)、氣象行業(yè)UI界面設(shè)計(jì)軌道交通界面設(shè)計(jì)、地理信息系統(tǒng)GIS UI界面設(shè)計(jì)、航天軍工軟件UI界面設(shè)計(jì)、醫(yī)療行業(yè)軟件UI界面設(shè)計(jì)教育行業(yè)軟件UI界面設(shè)計(jì)、企業(yè)信息化UI界面設(shè)計(jì)、軟件qt開發(fā)、軟件wpf開發(fā)軟件vue開發(fā)

國內(nèi) Linux 集群服務(wù)器網(wǎng)絡(luò)差無法 Git Clone 大項(xiàng)目的解決辦法

前端達(dá)人

GitHub 是一個軟件源代碼托管服務(wù)平臺,Linux 上使用的大部分工具都可以在上面獲得。

但是由于 GitHub 的服務(wù)器在國外,國內(nèi)訪問經(jīng)常出現(xiàn)網(wǎng)絡(luò)問題導(dǎo)致訪問失敗,尤其是使用 git clone 對 repo 進(jìn)行克隆的時候,會經(jīng)常出現(xiàn)因網(wǎng)絡(luò)問題而克隆失敗。

Linux 集群服務(wù)器的用戶,在比較難實(shí)現(xiàn)科學(xué)上網(wǎng)的前提下,對大型 repo 克隆的任務(wù)顯得尤為困難,而且想使用 gradle build 的話,git clone 似乎是唯一選擇。

既然 git clone 一兩次不成功,那我就想辦法讓他 clone 到成功為止,于是我就寫了一個循環(huán)腳本,將它投遞到計(jì)算節(jié)點(diǎn)上 24h 運(yùn)行,直到它克隆成功才結(jié)束任務(wù)。

以克隆 GATK (repo 大小約 300mb)為例,腳本如下:

#!/bin/bash
#SBATCH --job-name=GitClone        #任務(wù)名稱
#SBATCH --nodelist=litchi-0-1    #計(jì)算節(jié)點(diǎn)名稱
#SBATCH --partition=WHEEL        #使用 WHEEL 用戶組
#SBATCH --nodes=1                #使用計(jì)算節(jié)點(diǎn)數(shù)量
#SBATCH --ntasks=1                #使用 1 個線程
#SBATCH --mail-type=end            #任務(wù)結(jié)束時發(fā)送郵件
#SBATCH --mail-user=***@qq.com  #郵箱地址
#SBATCH --output=/home/$USER/Slurm_JobLogs/JobLog_%j_%x.log        #標(biāo)準(zhǔn)輸出保存路徑
#SBATCH --error=/home/$USER/Slurm_JobLogs/JobLog_%j_%x.err        #標(biāo)準(zhǔn)錯誤保存路徑
#------------------------------------------------------------
# PRINT JOB'S INFORMATION
  source /home/$USER/.bashrc
  threads=$SLURM_NTASKS
  echo "## Job:[$SLURM_JOB_NAME]"
  echo "## Node:[$SLURM_NODELIST]"
  echo "## CPUs:[$SLURM_NTASKS]"
  echo "## Mem:[$SLURM_MEM_PER_NODE]"
  echo `date`
  echo -e $(printf -- "-%.0s" {1..100})"\n" #cut-off-line
#------------------------------------------------------------
# PATH
  repo_url="https://github.com/broadinstitute/gatk.git"
  repo_name="gatk"
#------------------------------------------------------------
# SHELL
  cd /home/$USER/Accessories/Softwares
  while true; do                #循環(huán)執(zhí)行任務(wù)
    git clone $repo_url            #執(zhí)行 git clone
    if [ $? -eq 0 ]; then        #如果以上命令的退出碼等于 0 (任務(wù)成功)
      echo "Git clone successful!"
      break                        #則退出循環(huán)
    else                        #如果命令的退出碼不等于 0 (任務(wù)失敗)
      echo "Git clone failed:( Retrying..."
      rm -rf ./${repo_name}        #則刪除這個repo
      sleep 1                    #睡眠 1 秒緩沖一下繼續(xù)循環(huán)
    fi
  done
#------------------------------------------------------------
# JOB ENDS
  echo -e $(printf -- "-%.0s" {1..100}) #cut-off-line
  echo "## Runtime:[`sacct -j $SLURM_JOB_ID --format=Elapsed --noheader | awk '{print $1}' | sed -n '2p'`]"
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

這個腳本的路徑為 ~/Scripts/GitClone_GATK.sh

將任務(wù)投遞到計(jì)算節(jié)點(diǎn)運(yùn)行:

sbatch ~/Scripts/GitClone_GATK.sh
  • 1

最終,這個任務(wù)運(yùn)行了 40 次,歷時 13h,終于是克隆成功了!

藍(lán)藍(lán)設(shè)計(jì)(sillybuy.com )是一家專注而深入的界面設(shè)計(jì)公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的大數(shù)據(jù)可視化界面設(shè)計(jì)、B端界面設(shè)計(jì)、桌面端界面設(shè)計(jì)、APP界面設(shè)計(jì)、圖標(biāo)定制、用戶體驗(yàn)設(shè)計(jì)、交互設(shè)計(jì)UI咨詢、高端網(wǎng)站設(shè)計(jì)、平面設(shè)計(jì),以及相關(guān)的軟件開發(fā)服務(wù),咨詢電話:01063334945。

關(guān)鍵詞:UI咨詢UI設(shè)計(jì)服務(wù)公司、軟件界面設(shè)計(jì)公司、界面設(shè)計(jì)公司、UI設(shè)計(jì)公司、UI交互設(shè)計(jì)公司、數(shù)據(jù)可視化設(shè)計(jì)公司、用戶體驗(yàn)公司高端網(wǎng)站設(shè)計(jì)公司

銀行金融軟件UI界面設(shè)計(jì)、能源及監(jiān)控軟件UI界面設(shè)計(jì)、氣象行業(yè)UI界面設(shè)計(jì)、軌道交通界面設(shè)計(jì)、地理信息系統(tǒng)GIS UI界面設(shè)計(jì)航天軍工軟件UI界面設(shè)計(jì)、醫(yī)療行業(yè)軟件UI界面設(shè)計(jì)、教育行業(yè)軟件UI界面設(shè)計(jì)企業(yè)信息化UI界面設(shè)計(jì)、軟件qt開發(fā)、軟件wpf開發(fā)、軟件vue開發(fā)

GIT生成SSH公鑰圖文教程 轉(zhuǎn)自csdn

前端達(dá)人

GIT介紹

GIT是一種分布式版本控制系統(tǒng),用于追蹤文件的變化和協(xié)作開發(fā)。本文將詳細(xì)介紹GIT的基本架構(gòu)、工作流程和常用命令,并對其優(yōu)勢和應(yīng)用場景進(jìn)行分析。

1. GIT的基本架構(gòu)

GIT的基本架構(gòu)由三個主要組件組成:工作區(qū)(Working Directory)、暫存區(qū)(Stage)和倉庫(Repository)。

  • 工作區(qū)即本地文件夾,用于存放項(xiàng)目文件。
  • 暫存區(qū)是位于工作區(qū)與倉庫之間的緩沖區(qū)域,用于臨時存儲修改。
  • 倉庫則是存放項(xiàng)目歷史記錄的地方,包含項(xiàng)目所有文件的完整歷史。

2. GIT的工作流程

GIT的工作流程通常包括以下幾個步驟:

  • 初始化倉庫:使用git init命令初始化一個空白的倉庫。
  • 添加文件:使用git add命令將文件添加到暫存區(qū)中。
  • 提交修改:使用git commit命令將暫存區(qū)的修改提交到倉庫中。
  • 分支管理:使用git branch命令創(chuàng)建、切換和刪除分支。
  • 遠(yuǎn)程倉庫:使用git remote命令管理遠(yuǎn)程倉庫,并使用git pushgit pull命令與遠(yuǎn)程倉庫進(jìn)行數(shù)據(jù)同步。

3. GIT常用命令

以下是GIT的一些常用命令及其功能:

  • git init:初始化一個新的倉庫。
  • git add <file>:將指定文件添加到暫存區(qū)中。
  • git commit -m "<message>":提交暫存區(qū)的修改并添加注釋。
  • git status:顯示工作區(qū)和暫存區(qū)的狀態(tài)。
  • git log:顯示倉庫的提交歷史。
  • git branch:管理分支,包括創(chuàng)建、切換和刪除分支。
  • git checkout [branch]:切換到指定分支。
  • git merge <branch>:將指定分支合并到當(dāng)前分支。
  • git remote add <name> <url>:添加遠(yuǎn)程倉庫。
  • git push <remote> <branch>:將本地分支推送到遠(yuǎn)程倉庫。
  • git pull <remote> <branch>:從遠(yuǎn)程倉庫拉取最新代碼。

4. GIT的優(yōu)勢

GIT具有以下幾個優(yōu)勢:

  • 分布式:每個開發(fā)者都擁有完整的倉庫副本,可以在離線環(huán)境下進(jìn)行工作,并能方便地處理分支操作和合并沖突。
  • 高效性:GIT采用了快照方式保存文件,不會重復(fù)存儲相同的內(nèi)容,大幅節(jié)省存儲空間。
  • 數(shù)據(jù)完整性:使用哈希算法確保每個文件和每次提交都有唯一的標(biāo)識,可以有效避免數(shù)據(jù)損壞和篡改。
  • 可擴(kuò)展性:GIT具有良好的插件和擴(kuò)展支持,可以根據(jù)需求靈活地進(jìn)行功能擴(kuò)展。

5. GIT的應(yīng)用場景

由于GIT的優(yōu)勢和高效性,它廣泛應(yīng)用于軟件開發(fā)中的版本控制和協(xié)作管理。以下是幾個常見的應(yīng)用場景:

  • 代碼版本控制:開發(fā)人員可以使用GIT來追蹤和管理代碼的版本變化,輕松地回退、回顧和比較代碼的不同版本。
  • 多人協(xié)作開發(fā):GIT支持跨團(tuán)隊(duì)和跨地域的多人協(xié)作開發(fā),能夠有效解決代碼沖突,并提供完整的歷史記錄和審計(jì)功能。
  • 敏捷開發(fā):GIT的分支管理和快速迭代特性非常適合敏捷開發(fā)方法,可以幫助團(tuán)隊(duì)快速迭代并保持項(xiàng)目的整潔和穩(wěn)定。

綜上所述,GIT作為一種強(qiáng)大的分布式版本控制系統(tǒng),在軟件開發(fā)中發(fā)揮著重要的作用。通過了解GIT的基本架構(gòu)、工作流程和常用命令,開發(fā)人員可以更好地利用和實(shí)踐GIT,提高代碼管理和協(xié)作效率。

教程

1.下載安裝git客戶端

此處省略1萬字,本人是通過idea開發(fā)工具下載的git.

2.鼠標(biāo)右鍵進(jìn)入,給 GIT base Here 

 

輸入 生成SSH公鑰命令

ssh-keygen -t rsa -b 4096 -C "your_email@example.com"

然后需要輸入的地方,回車,如圖所示,生成ssh公鑰成功

 

在找到公鑰生成的位置,打開id_rsa.pub文件,即可找到ssh公鑰

 

 在git服務(wù)平臺如碼云、conding、github等里添加自己公鑰

 

補(bǔ)充知識

SSH公鑰是一種加密技術(shù),用于實(shí)現(xiàn)安全的遠(yuǎn)程登錄和文件傳輸。它使用非對稱加密算法,將用戶的公鑰保存在被訪問的服務(wù)器上,而私鑰則由用戶自己保管。

當(dāng)用戶要進(jìn)行遠(yuǎn)程登錄或文件傳輸時,客戶端會生成一對公鑰和私鑰,并將公鑰發(fā)送給服務(wù)器。服務(wù)器將收到的公鑰存儲起來。當(dāng)用戶進(jìn)行身份驗(yàn)證時,服務(wù)器會向客戶端發(fā)送一個隨機(jī)的挑戰(zhàn),并使用存儲的公鑰對其進(jìn)行加密。用戶收到挑戰(zhàn)后,使用自己保管的私鑰進(jìn)行解密并返回結(jié)果給服務(wù)器。如果解密結(jié)果正確,身份驗(yàn)證就成功了。

SSH公鑰具有以下優(yōu)點(diǎn):

  1. 安全性:使用非對稱加密算法,確保通信和數(shù)據(jù)傳輸?shù)陌踩浴?/li>
  2. 方便性:無需記住密碼,只需要保管好私鑰即可。
  3. 靈活性:可以在多個終端之間共享公鑰,方便用戶同時訪問多臺服務(wù)器。
  4. 可審計(jì)性:公鑰在服務(wù)器上留下痕跡,方便審計(jì)和管理。

為了確保SSH公鑰的安全,用戶應(yīng)該妥善保管自己的私鑰,并定期更新公鑰,防止被惡意利用。此外,服務(wù)器管理員也應(yīng)確保正確配置和管理公鑰,避免安全漏洞的出現(xiàn)。

git入門:Linux操作系統(tǒng)下 git環(huán)境搭建 | 生成公私密鑰

前端達(dá)人

前言

該示例是在ubuntu下完成的

 

 

一、安裝 git 和 ssh

sudo apt-get install git
sudo apt-get install openssh-server

二、配置公鑰

因?yàn)間it和 GitHub之間是通過ssh加密傳輸?shù)?,因此需要配置公鑰,所以需要先生成公私密鑰。

命令:該郵箱是你注冊GitHub的郵箱地址

ssh-keygen -t rsa -C “xxxxxxxxxx@163.com”

如圖,表示生成成功

三、得到公鑰在GitHub配置

進(jìn)入 .ssh 文件里,查看id_rsa.pub,表示公鑰(public)

復(fù)制該密鑰到GitHub里配置

標(biāo)題title隨便起

四、查看能否通信成功

輸入該命令:如果出現(xiàn)下面這句話,就說明成功了。

ssh -T git@github.com

五、配置郵箱和用戶名

通過以下兩個命令配置用戶名和郵箱,以后提交代碼文件,就會附帶該用戶的信息。

git config --global user.name " xxx "
git config --global user.email " xxx@xx.com"

在 .gitconfig文件可查看

藍(lán)藍(lán)設(shè)計(jì)(sillybuy.com )是一家專注而深入的界面設(shè)計(jì)公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的大數(shù)據(jù)可視化界面設(shè)計(jì)B端界面設(shè)計(jì)、桌面端界面設(shè)計(jì)、APP界面設(shè)計(jì)圖標(biāo)定制、用戶體驗(yàn)設(shè)計(jì)交互設(shè)計(jì)、UI咨詢、高端網(wǎng)站設(shè)計(jì)、平面設(shè)計(jì),以及相關(guān)的軟件開發(fā)服務(wù),咨詢電話:01063334945。

關(guān)鍵詞:UI咨詢、UI設(shè)計(jì)服務(wù)公司軟件界面設(shè)計(jì)公司、界面設(shè)計(jì)公司、UI設(shè)計(jì)公司、UI交互設(shè)計(jì)公司、數(shù)據(jù)可視化設(shè)計(jì)公司、用戶體驗(yàn)公司高端網(wǎng)站設(shè)計(jì)公司

銀行金融軟件UI界面設(shè)計(jì)、能源及監(jiān)控軟件UI界面設(shè)計(jì)、氣象行業(yè)UI界面設(shè)計(jì)軌道交通界面設(shè)計(jì)、地理信息系統(tǒng)GIS UI界面設(shè)計(jì)、航天軍工軟件UI界面設(shè)計(jì)、醫(yī)療行業(yè)軟件UI界面設(shè)計(jì)、教育行業(yè)軟件UI界面設(shè)計(jì)、企業(yè)信息化UI界面設(shè)計(jì)、軟件qt開發(fā)、軟件wpf開發(fā)、軟件vue開發(fā)

4個步驟,總結(jié)B端設(shè)計(jì)師的競品分析方法|北京藍(lán)藍(lán)UI設(shè)計(jì)公司

周周

競品分析方法很多,但大多數(shù)是偏向 B 端產(chǎn)品經(jīng)理方面的或者是 C 端設(shè)計(jì)師的,真正在 B 端設(shè)計(jì)中的方法比較少,這次為大家介紹適合 B 端設(shè)計(jì)師使用的競品分析方法。

如何拆解UI界面設(shè)計(jì)?試試這個三維分析法?。本┧{(lán)藍(lán)UI設(shè)計(jì)公司

周周

界面設(shè)計(jì)就是將一堆分散的元素、內(nèi)容或者組件,通過正確使用可視層次結(jié)構(gòu),進(jìn)行合理的規(guī)劃和重組,有目的地呈現(xiàn)在用戶的面前,這樣有助于內(nèi)容傳達(dá)和提升用戶使用效率,幫助用戶快速解決問題。

10個產(chǎn)品細(xì)節(jié)剖析,看看高手是如何做設(shè)計(jì)的?。本┧{(lán)藍(lán)UI設(shè)計(jì)公司

周周

俗話說:“細(xì)節(jié)決定成敗”,對于設(shè)計(jì)而言亦是如此,把設(shè)計(jì)做到極致才能在競爭中脫穎而出。對于產(chǎn)品設(shè)計(jì)師來說不僅需要有發(fā)現(xiàn)的眼睛,也要有做到極致的習(xí)慣。通過優(yōu)秀的設(shè)計(jì)案例不斷培養(yǎng)自己的設(shè)計(jì)思維,才能在項(xiàng)目中不斷創(chuàng)新和做到極致。

最近黑馬哥發(fā)現(xiàn)了一些優(yōu)秀的設(shè)計(jì)細(xì)節(jié),相信這些小小的細(xì)節(jié)必然可以帶來更好的體驗(yàn),助力產(chǎn)品帶給用戶更好的使用體驗(yàn)。

 

4000字干貨!幫你快速了解產(chǎn)品設(shè)計(jì)中的心智模式|北京藍(lán)藍(lán)UI設(shè)計(jì)公司

周周

進(jìn)入主題之前,向大家強(qiáng)烈推薦李睿秋先生的《打開心智》一書,雖然書名有一點(diǎn)成功學(xué),但是內(nèi)容非常有參考性。本次分享內(nèi)容的核心也來源于《打開心智》一書,自己僅做了內(nèi)容的整理與 PPT 設(shè)計(jì)。

好看的圣誕樹(動態(tài)效果) 轉(zhuǎn)自 csdn

前端達(dá)人

、制作方法 

1.復(fù)制代碼到Dreamweaver或HBuilder或vscode中

2.點(diǎn)擊運(yùn)行---運(yùn)行到瀏覽器---選擇你要打開的瀏覽器

3.打開后會出現(xiàn)這個界面,前四個是固定音樂,最后一個是自主選擇的音樂,你可以選擇你電腦上的歌曲,什么歌曲都行(第一次打開可能會有點(diǎn)慢,稍等片刻即可,選擇音樂的時候點(diǎn)一下沒反應(yīng)的話多點(diǎn)幾下即可,第一次打開這屬于正?,F(xiàn)象)

 

4.特別提醒:打開的時候電腦一定要處于聯(lián)網(wǎng)狀態(tài)
 

三、源代碼

 
  1.  
    <!DOCTYPE html>
  2.  
    <html lang="en">
  3.  
     
  4.  
    <head>
  5.  
    <meta charset="UTF-8">
  6.  
     
  7.  
    <title>圣誕樹</title>
  8.  
     
  9.  
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/5.0.0/normalize.min.css">
  10.  
     
  11.  
    <style>
  12.  
    * {
  13.  
    box-sizing: border-box;
  14.  
    }
  15.  
     
  16.  
     
  17.  
    body {
  18.  
    margin: 0;
  19.  
    height: 100vh;
  20.  
    overflow: hidden;
  21.  
    display: flex;
  22.  
    align-items: center;
  23.  
    justify-content: center;
  24.  
    background: #161616;
  25.  
    color: #c5a880;
  26.  
    font-family: sans-serif;
  27.  
    }
  28.  
     
  29.  
     
  30.  
    label {
  31.  
    display: inline-block;
  32.  
    background-color: #161616;
  33.  
    padding: 16px;
  34.  
    border-radius: 0.3rem;
  35.  
    cursor: pointer;
  36.  
    margin-top: 1rem;
  37.  
    width: 300px;
  38.  
    border-radius: 10px;
  39.  
    border: 1px solid #c5a880;
  40.  
    text-align: center;
  41.  
    }
  42.  
     
  43.  
     
  44.  
    ul {
  45.  
    list-style-type: none;
  46.  
    padding: 0;
  47.  
    margin: 0;
  48.  
    }
  49.  
     
  50.  
     
  51.  
    .btn {
  52.  
    background-color: #161616;
  53.  
    border-radius: 10px;
  54.  
    color: #c5a880;
  55.  
    border: 1px solid #c5a880;
  56.  
    padding: 16px;
  57.  
    width: 300px;
  58.  
    margin-bottom: 16px;
  59.  
    line-height: 1.5;
  60.  
    cursor: pointer;
  61.  
    }
  62.  
     
  63.  
    .separator {
  64.  
    font-weight: bold;
  65.  
    text-align: center;
  66.  
    width: 300px;
  67.  
    margin: 16px 0px;
  68.  
    color: #a07676;
  69.  
    }
  70.  
     
  71.  
     
  72.  
    .title {
  73.  
    color: #a07676;
  74.  
    font-weight: bold;
  75.  
    font-size: 1.25rem;
  76.  
    margin-bottom: 16px;
  77.  
    }
  78.  
     
  79.  
     
  80.  
    .text-loading {
  81.  
    font-size: 2rem;
  82.  
    }
  83.  
    </style>
  84.  
     
  85.  
    <script>
  86.  
    window.console = window.console || function (t) { };
  87.  
    </script>
  88.  
     
  89.  
     
  90.  
     
  91.  
    <script>
  92.  
    if (document.location.search.match(/type=embed/gi)) {
  93.  
    window.parent.postMessage("resize", "*");
  94.  
    }
  95.  
    </script>
  96.  
     
  97.  
     
  98.  
    </head>
  99.  
     
  100.  
    <body translate="no">
  101.  
    <script src="https://cdn.jsdelivr.net/npm/three@0.115.0/build/three.min.js"></script>
  102.  
    <script src="https://cdn.jsdelivr.net/npm/three@0.115.0/examples/js/postprocessing/EffectComposer.js"></script>
  103.  
    <script src="https://cdn.jsdelivr.net/npm/three@0.115.0/examples/js/postprocessing/RenderPass.js"></script>
  104.  
    <script src="https://cdn.jsdelivr.net/npm/three@0.115.0/examples/js/postprocessing/ShaderPass.js"></script>
  105.  
    <script src="https://cdn.jsdelivr.net/npm/three@0.115.0/examples/js/shaders/CopyShader.js"></script>
  106.  
    <script src="https://cdn.jsdelivr.net/npm/three@0.115.0/examples/js/shaders/LuminosityHighPassShader.js"></script>
  107.  
    <script src="https://cdn.jsdelivr.net/npm/three@0.115.0/examples/js/postprocessing/UnrealBloomPass.js"></script>
  108.  
     
  109.  
    <div id="overlay">
  110.  
    <ul>
  111.  
    <li class="title">請選擇音樂</li>
  112.  
    <li>
  113.  
    <button class="btn" id="btnA" type="button">
  114.  
    Snowflakes Falling Down by Simon Panrucker
  115.  
    </button>
  116.  
    </li>
  117.  
    <li><button class="btn" id="btnB" type="button">This Christmas by Dott</button></li>
  118.  
    <li><button class="btn" id="btnC" type="button">No room at the inn by TRG Banks</button></li>
  119.  
    <li><button class="btn" id="btnD" type="button">Jingle Bell Swing by Mark Smeby</button></li>
  120.  
    <li class="separator">或者</li>
  121.  
    <li>
  122.  
    <input type="file" id="upload" hidden />
  123.  
    <label for="upload">Upload File</label>
  124.  
    </li>
  125.  
    </ul>
  126.  
    </div>
  127.  
     
  128.  
    <script id="rendered-js">
  129.  
    const { PI, sin, cos } = Math;
  130.  
    const TAU = 2 * PI;
  131.  
     
  132.  
    const map = (value, sMin, sMax, dMin, dMax) => {
  133.  
    return dMin + (value - sMin) / (sMax - sMin) * (dMax - dMin);
  134.  
    };
  135.  
     
  136.  
    const range = (n, m = 0) =>
  137.  
    Array(n).
  138.  
    fill(m).
  139.  
    map((i, j) => i + j);
  140.  
     
  141.  
    const rand = (max, min = 0) => min + Math.random() * (max - min);
  142.  
    const randInt = (max, min = 0) => Math.floor(min + Math.random() * (max - min));
  143.  
    const randChoise = arr => arr[randInt(arr.length)];
  144.  
    const polar = (ang, r = 1) => [r * cos(ang), r * sin(ang)];
  145.  
     
  146.  
    let scene, camera, renderer, analyser;
  147.  
    let step = 0;
  148.  
    const uniforms = {
  149.  
    time: { type: "f", value: 0.0 },
  150.  
    step: { type: "f", value: 0.0 }
  151.  
    };
  152.  
     
  153.  
    const params = {
  154.  
    exposure: 1,
  155.  
    bloomStrength: 0.9,
  156.  
    bloomThreshold: 0,
  157.  
    bloomRadius: 0.5
  158.  
    };
  159.  
     
  160.  
    let composer;
  161.  
     
  162.  
    const fftSize = 2048;
  163.  
    const totalPoints = 4000;
  164.  
     
  165.  
    const listener = new THREE.AudioListener();
  166.  
     
  167.  
    const audio = new THREE.Audio(listener);
  168.  
     
  169.  
    document.querySelector("input").addEventListener("change", uploadAudio, false);
  170.  
     
  171.  
    const buttons = document.querySelectorAll(".btn");
  172.  
    buttons.forEach((button, index) =>
  173.  
    button.addEventListener("click", () => loadAudio(index)));
  174.  
     
  175.  
     
  176.  
    function init() {
  177.  
    const overlay = document.getElementById("overlay");
  178.  
    overlay.remove();
  179.  
     
  180.  
    scene = new THREE.Scene();
  181.  
    renderer = new THREE.WebGLRenderer({ antialias: true });
  182.  
    renderer.setPixelRatio(window.devicePixelRatio);
  183.  
    renderer.setSize(window.innerWidth, window.innerHeight);
  184.  
    document.body.appendChild(renderer.domElement);
  185.  
     
  186.  
    camera = new THREE.PerspectiveCamera(
  187.  
    60,
  188.  
    window.innerWidth / window.innerHeight,
  189.  
    1,
  190.  
    1000);
  191.  
     
  192.  
    camera.position.set(-0.09397456774197047, -2.5597086635726947, 24.420789670889008);
  193.  
    camera.rotation.set(0.10443543723052419, -0.003827152981119352, 0.0004011488708739715);
  194.  
     
  195.  
    const format = renderer.capabilities.isWebGL2 ?
  196.  
    THREE.RedFormat :
  197.  
    THREE.LuminanceFormat;
  198.  
     
  199.  
    uniforms.tAudioData = {
  200.  
    value: new THREE.DataTexture(analyser.data, fftSize / 2, 1, format)
  201.  
    };
  202.  
     
  203.  
     
  204.  
    addPlane(scene, uniforms, 3000);
  205.  
    addSnow(scene, uniforms);
  206.  
     
  207.  
    range(10).map(i => {
  208.  
    addTree(scene, uniforms, totalPoints, [20, 0, -20 * i]);
  209.  
    addTree(scene, uniforms, totalPoints, [-20, 0, -20 * i]);
  210.  
    });
  211.  
     
  212.  
    const renderScene = new THREE.RenderPass(scene, camera);
  213.  
     
  214.  
    const bloomPass = new THREE.UnrealBloomPass(
  215.  
    new THREE.Vector2(window.innerWidth, window.innerHeight),
  216.  
    1.5,
  217.  
    0.4,
  218.  
    0.85);
  219.  
     
  220.  
    bloomPass.threshold = params.bloomThreshold;
  221.  
    bloomPass.strength = params.bloomStrength;
  222.  
    bloomPass.radius = params.bloomRadius;
  223.  
     
  224.  
    composer = new THREE.EffectComposer(renderer);
  225.  
    composer.addPass(renderScene);
  226.  
    composer.addPass(bloomPass);
  227.  
     
  228.  
    addListners(camera, renderer, composer);
  229.  
    animate();
  230.  
    }
  231.  
     
  232.  
    function animate(time) {
  233.  
    analyser.getFrequencyData();
  234.  
    uniforms.tAudioData.value.needsUpdate = true;
  235.  
    step = (step + 1) % 1000;
  236.  
    uniforms.time.value = time;
  237.  
    uniforms.step.value = step;
  238.  
    composer.render();
  239.  
    requestAnimationFrame(animate);
  240.  
    }
  241.  
     
  242.  
    function loadAudio(i) {
  243.  
    document.getElementById("overlay").innerHTML =
  244.  
    '<div class="text-loading">正在下載音樂,請稍等...</div>';
  245.  
    const files = [
  246.  
    "https://files.freemusicarchive.org/storage-freemusicarchive-org/music/no_curator/Simon_Panrucker/Happy_Christmas_You_Guys/Simon_Panrucker_-_01_-_Snowflakes_Falling_Down.mp3",
  247.  
    "https://files.freemusicarchive.org/storage-freemusicarchive-org/music/no_curator/Dott/This_Christmas/Dott_-_01_-_This_Christmas.mp3",
  248.  
    "https://files.freemusicarchive.org/storage-freemusicarchive-org/music/ccCommunity/TRG_Banks/TRG_Banks_Christmas_Album/TRG_Banks_-_12_-_No_room_at_the_inn.mp3",
  249.  
    "https://files.freemusicarchive.org/storage-freemusicarchive-org/music/ccCommunity/Mark_Smeby/En_attendant_Nol/Mark_Smeby_-_07_-_Jingle_Bell_Swing.mp3"];
  250.  
     
  251.  
    const file = files[i];
  252.  
     
  253.  
    const loader = new THREE.AudioLoader();
  254.  
    loader.load(file, function (buffer) {
  255.  
    audio.setBuffer(buffer);
  256.  
    audio.play();
  257.  
    analyser = new THREE.AudioAnalyser(audio, fftSize);
  258.  
    init();
  259.  
    });
  260.  
     
  261.  
     
  262.  
     
  263.  
     
  264.  
    }
  265.  
     
  266.  
     
  267.  
    function uploadAudio(event) {
  268.  
    document.getElementById("overlay").innerHTML =
  269.  
    '<div class="text-loading">請稍等...</div>';
  270.  
    const files = event.target.files;
  271.  
    const reader = new FileReader();
  272.  
     
  273.  
    reader.onload = function (file) {
  274.  
    var arrayBuffer = file.target.result;
  275.  
     
  276.  
    listener.context.decodeAudioData(arrayBuffer, function (audioBuffer) {
  277.  
    audio.setBuffer(audioBuffer);
  278.  
    audio.play();
  279.  
    analyser = new THREE.AudioAnalyser(audio, fftSize);
  280.  
    init();
  281.  
    });
  282.  
    };
  283.  
     
  284.  
    reader.readAsArrayBuffer(files[0]);
  285.  
    }
  286.  
     
  287.  
    function addTree(scene, uniforms, totalPoints, treePosition) {
  288.  
    const vertexShader = `
  289.  
    attribute float mIndex;
  290.  
    varying vec3 vColor;
  291.  
    varying float opacity;
  292.  
    uniform sampler2D tAudioData;
  293.  
    float norm(float value, float min, float max ){
  294.  
    return (value - min) / (max - min);
  295.  
    }
  296.  
    float lerp(float norm, float min, float max){
  297.  
    return (max - min) * norm + min;
  298.  
    }
  299.  
    float map(float value, float sourceMin, float sourceMax, float destMin, float destMax){
  300.  
    return lerp(norm(value, sourceMin, sourceMax), destMin, destMax);
  301.  
    }
  302.  
    void main() {
  303.  
    vColor = color;
  304.  
    vec3 p = position;
  305.  
    vec4 mvPosition = modelViewMatrix * vec4( p, 1.0 );
  306.  
    float amplitude = texture2D( tAudioData, vec2( mIndex, 0.1 ) ).r;
  307.  
    float amplitudeClamped = clamp(amplitude-0.4,0.0, 0.6 );
  308.  
    float sizeMapped = map(amplitudeClamped, 0.0, 0.6, 1.0, 20.0);
  309.  
    opacity = map(mvPosition.z , -200.0, 15.0, 0.0, 1.0);
  310.  
    gl_PointSize = sizeMapped * ( 100.0 / -mvPosition.z );
  311.  
    gl_Position = projectionMatrix * mvPosition;
  312.  
    }
  313.  
    `;
  314.  
    const fragmentShader = `
  315.  
    varying vec3 vColor;
  316.  
    varying float opacity;
  317.  
    uniform sampler2D pointTexture;
  318.  
    void main() {
  319.  
    gl_FragColor = vec4( vColor, opacity );
  320.  
    gl_FragColor = gl_FragColor * texture2D( pointTexture, gl_PointCoord );
  321.  
    }
  322.  
    `;
  323.  
    const shaderMaterial = new THREE.ShaderMaterial({
  324.  
    uniforms: {
  325.  
    ...uniforms,
  326.  
    pointTexture: {
  327.  
    value: new THREE.TextureLoader().load(`https://assets.codepen.io/3685267/spark1.png`)
  328.  
    }
  329.  
    },
  330.  
     
  331.  
     
  332.  
    vertexShader,
  333.  
    fragmentShader,
  334.  
    blending: THREE.AdditiveBlending,
  335.  
    depthTest: false,
  336.  
    transparent: true,
  337.  
    vertexColors: true
  338.  
    });
  339.  
     
  340.  
     
  341.  
    const geometry = new THREE.BufferGeometry();
  342.  
    const positions = [];
  343.  
    const colors = [];
  344.  
    const sizes = [];
  345.  
    const phases = [];
  346.  
    const mIndexs = [];
  347.  
     
  348.  
    const color = new THREE.Color();
  349.  
     
  350.  
    for (let i = 0; i < totalPoints; i++) {
  351.  
    const t = Math.random();
  352.  
    const y = map(t, 0, 1, -8, 10);
  353.  
    const ang = map(t, 0, 1, 0, 6 * TAU) + TAU / 2 * (i % 2);
  354.  
    const [z, x] = polar(ang, map(t, 0, 1, 5, 0));
  355.  
     
  356.  
    const modifier = map(t, 0, 1, 1, 0);
  357.  
    positions.push(x + rand(-0.3 * modifier, 0.3 * modifier));
  358.  
    positions.push(y + rand(-0.3 * modifier, 0.3 * modifier));
  359.  
    positions.push(z + rand(-0.3 * modifier, 0.3 * modifier));
  360.  
     
  361.  
    color.setHSL(map(i, 0, totalPoints, 1.0, 0.0), 1.0, 0.5);
  362.  
     
  363.  
    colors.push(color.r, color.g, color.b);
  364.  
    phases.push(rand(1000));
  365.  
    sizes.push(1);
  366.  
    const mIndex = map(i, 0, totalPoints, 1.0, 0.0);
  367.  
    mIndexs.push(mIndex);
  368.  
    }
  369.  
     
  370.  
    geometry.setAttribute(
  371.  
    "position",
  372.  
    new THREE.Float32BufferAttribute(positions, 3).setUsage(
  373.  
    THREE.DynamicDrawUsage));
  374.  
     
  375.  
     
  376.  
    geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
  377.  
    geometry.setAttribute("size", new THREE.Float32BufferAttribute(sizes, 1));
  378.  
    geometry.setAttribute("phase", new THREE.Float32BufferAttribute(phases, 1));
  379.  
    geometry.setAttribute("mIndex", new THREE.Float32BufferAttribute(mIndexs, 1));
  380.  
     
  381.  
    const tree = new THREE.Points(geometry, shaderMaterial);
  382.  
     
  383.  
    const [px, py, pz] = treePosition;
  384.  
     
  385.  
    tree.position.x = px;
  386.  
    tree.position.y = py;
  387.  
    tree.position.z = pz;
  388.  
     
  389.  
    scene.add(tree);
  390.  
    }
  391.  
     
  392.  
    function addSnow(scene, uniforms) {
  393.  
    const vertexShader = `
  394.  
    attribute float size;
  395.  
    attribute float phase;
  396.  
    attribute float phaseSecondary;
  397.  
    varying vec3 vColor;
  398.  
    varying float opacity;
  399.  
    uniform float time;
  400.  
    uniform float step;
  401.  
    float norm(float value, float min, float max ){
  402.  
    return (value - min) / (max - min);
  403.  
    }
  404.  
    float lerp(float norm, float min, float max){
  405.  
    return (max - min) * norm + min;
  406.  
    }
  407.  
    float map(float value, float sourceMin, float sourceMax, float destMin, float destMax){
  408.  
    return lerp(norm(value, sourceMin, sourceMax), destMin, destMax);
  409.  
    }
  410.  
    void main() {
  411.  
    float t = time* 0.0006;
  412.  
    vColor = color;
  413.  
    vec3 p = position;
  414.  
    p.y = map(mod(phase+step, 1000.0), 0.0, 1000.0, 25.0, -8.0);
  415.  
    p.x += sin(t+phase);
  416.  
    p.z += sin(t+phaseSecondary);
  417.  
    opacity = map(p.z, -150.0, 15.0, 0.0, 1.0);
  418.  
    vec4 mvPosition = modelViewMatrix * vec4( p, 1.0 );
  419.  
    gl_PointSize = size * ( 100.0 / -mvPosition.z );
  420.  
    gl_Position = projectionMatrix * mvPosition;
  421.  
    }
  422.  
    `;
  423.  
     
  424.  
    const fragmentShader = `
  425.  
    uniform sampler2D pointTexture;
  426.  
    varying vec3 vColor;
  427.  
    varying float opacity;
  428.  
    void main() {
  429.  
    gl_FragColor = vec4( vColor, opacity );
  430.  
    gl_FragColor = gl_FragColor * texture2D( pointTexture, gl_PointCoord );
  431.  
    }
  432.  
    `;
  433.  
    function createSnowSet(sprite) {
  434.  
    const totalPoints = 300;
  435.  
    const shaderMaterial = new THREE.ShaderMaterial({
  436.  
    uniforms: {
  437.  
    ...uniforms,
  438.  
    pointTexture: {
  439.  
    value: new THREE.TextureLoader().load(sprite)
  440.  
    }
  441.  
    },
  442.  
     
  443.  
     
  444.  
    vertexShader,
  445.  
    fragmentShader,
  446.  
    blending: THREE.AdditiveBlending,
  447.  
    depthTest: false,
  448.  
    transparent: true,
  449.  
    vertexColors: true
  450.  
    });
  451.  
     
  452.  
     
  453.  
    const geometry = new THREE.BufferGeometry();
  454.  
    const positions = [];
  455.  
    const colors = [];
  456.  
    const sizes = [];
  457.  
    const phases = [];
  458.  
    const phaseSecondaries = [];
  459.  
     
  460.  
    const color = new THREE.Color();
  461.  
     
  462.  
    for (let i = 0; i < totalPoints; i++) {
  463.  
    const [x, y, z] = [rand(25, -25), 0, rand(15, -150)];
  464.  
    positions.push(x);
  465.  
    positions.push(y);
  466.  
    positions.push(z);
  467.  
     
  468.  
    color.set(randChoise(["#f1d4d4", "#f1f6f9", "#eeeeee", "#f1f1e8"]));
  469.  
     
  470.  
    colors.push(color.r, color.g, color.b);
  471.  
    phases.push(rand(1000));
  472.  
    phaseSecondaries.push(rand(1000));
  473.  
    sizes.push(rand(4, 2));
  474.  
    }
  475.  
     
  476.  
    geometry.setAttribute(
  477.  
    "position",
  478.  
    new THREE.Float32BufferAttribute(positions, 3));
  479.  
     
  480.  
    geometry.setAttribute("color", new THREE.Float32BufferAttribute(colors, 3));
  481.  
    geometry.setAttribute("size", new THREE.Float32BufferAttribute(sizes, 1));
  482.  
    geometry.setAttribute("phase", new THREE.Float32BufferAttribute(phases, 1));
  483.  
    geometry.setAttribute(
  484.  
    "phaseSecondary",
  485.  
    new THREE.Float32BufferAttribute(phaseSecondaries, 1));
  486.  
     
  487.  
     
  488.  
    const mesh = new THREE.Points(geometry, shaderMaterial);
  489.  
     
  490.  
    scene.add(mesh);
  491.  
    }
  492.  
    const sprites = [
  493.  
    "https://assets.codepen.io/3685267/snowflake1.png",
  494.  
    "https://assets.codepen.io/3685267/snowflake2.png",
  495.  
    "https://assets.codepen.io/3685267/snowflake3.png",
  496.  
    "https://assets.codepen.io/3685267/snowflake4.png",
  497.  
    "https://assets.codepen.io/3685267/snowflake5.png"];
  498.  
     
  499.  
    sprites.forEach(sprite => {
  500.  
    createSnowSet(sprite);
  501.  
    });
  502.  
    }
  503.  
     
  504.  
    function addPlane(scene, uniforms, totalPoints) {
  505.  
    const vertexShader = `
  506.  
    attribute float size;
  507.  
    attribute vec3 customColor;
  508.  
    varying vec3 vColor;
  509.  
    void main() {
  510.  
    vColor = customColor;
  511.  
    vec4 mvPosition = modelViewMatrix * vec4( position, 1.0 );
  512.  
    gl_PointSize = size * ( 300.0 / -mvPosition.z );
  513.  
    gl_Position = projectionMatrix * mvPosition;
  514.  
    }
  515.  
    `;
  516.  
    const fragmentShader = `
  517.  
    uniform vec3 color;
  518.  
    uniform sampler2D pointTexture;
  519.  
    varying vec3 vColor;
  520.  
    void main() {
  521.  
    gl_FragColor = vec4( vColor, 1.0 );
  522.  
    gl_FragColor = gl_FragColor * texture2D( pointTexture, gl_PointCoord );
  523.  
    }
  524.  
    `;
  525.  
    const shaderMaterial = new THREE.ShaderMaterial({
  526.  
    uniforms: {
  527.  
    ...uniforms,
  528.  
    pointTexture: {
  529.  
    value: new THREE.TextureLoader().load(`https://assets.codepen.io/3685267/spark1.png`)
  530.  
    }
  531.  
    },
  532.  
     
  533.  
     
  534.  
    vertexShader,
  535.  
    fragmentShader,
  536.  
    blending: THREE.AdditiveBlending,
  537.  
    depthTest: false,
  538.  
    transparent: true,
  539.  
    vertexColors: true
  540.  
    });
  541.  
     
  542.  
     
  543.  
    const geometry = new THREE.BufferGeometry();
  544.  
    const positions = [];
  545.  
    const colors = [];
  546.  
    const sizes = [];
  547.  
     
  548.  
    const color = new THREE.Color();
  549.  
     
  550.  
    for (let i = 0; i < totalPoints; i++) {
  551.  
    const [x, y, z] = [rand(-25, 25), 0, rand(-150, 15)];
  552.  
    positions.push(x);
  553.  
    positions.push(y);
  554.  
    positions.push(z);
  555.  
     
  556.  
    color.set(randChoise(["#93abd3", "#f2f4c0", "#9ddfd3"]));
  557.  
     
  558.  
    colors.push(color.r, color.g, color.b);
  559.  
    sizes.push(1);
  560.  
    }
  561.  
     
  562.  
    geometry.setAttribute(
  563.  
    "position",
  564.  
    new THREE.Float32BufferAttribute(positions, 3).setUsage(
  565.  
    THREE.DynamicDrawUsage));
  566.  
     
  567.  
     
  568.  
    geometry.setAttribute(
  569.  
    "customColor",
  570.  
    new THREE.Float32BufferAttribute(colors, 3));
  571.  
     
  572.  
    geometry.setAttribute("size", new THREE.Float32BufferAttribute(sizes, 1));
  573.  
     
  574.  
    const plane = new THREE.Points(geometry, shaderMaterial);
  575.  
     
  576.  
    plane.position.y = -8;
  577.  
    scene.add(plane);
  578.  
    }
  579.  
     
  580.  
    function addListners(camera, renderer, composer) {
  581.  
    document.addEventListener("keydown", e => {
  582.  
    const { x, y, z } = camera.position;
  583.  
    console.log(`camera.position.set(${x},${y},${z})`);
  584.  
    const { x: a, y: b, z: c } = camera.rotation;
  585.  
    console.log(`camera.rotation.set(${a},$,${c})`);
  586.  
    });
  587.  
     
  588.  
    window.addEventListener(
  589.  
    "resize",
  590.  
    () => {
  591.  
    const width = window.innerWidth;
  592.  
    const height = window.innerHeight;
  593.  
     
  594.  
    camera.aspect = width / height;
  595.  
    camera.updateProjectionMatrix();
  596.  
     
  597.  
    renderer.setSize(width, height);
  598.  
    composer.setSize(width, height);
  599.  
    },
  600.  
    false);
  601.  
     
  602.  
    }
  603.  
    </script>
  604.  
     
  605.  
    </body>
  606.  
     
  607.  
    </html>
 

WebSocket:實(shí)現(xiàn)實(shí)時雙向數(shù)據(jù)傳輸?shù)腤eb通信協(xié)議

前端達(dá)人

 

前言

在當(dāng)今互聯(lián)網(wǎng)時代,實(shí)時通信已成為很多應(yīng)用的需求。為了滿足這種需求,WebSocket協(xié)議被設(shè)計(jì)出來。WebSocket是一種基于TCP議的全雙工通信協(xié)議,通過WebSocket,Web應(yīng)用程序可以與服務(wù)器建立持久的連接,實(shí)現(xiàn)實(shí)時雙向數(shù)據(jù)輸,提供極低的延遲和高效的數(shù)據(jù)傳輸。


WebSocket原理

  • HTTP請求-響應(yīng)協(xié)議

在理解WebSocket原理之前,我們需要了解HTTP請求-響應(yīng)協(xié)議。HTTP是一種無狀態(tài)的請求-響應(yīng)協(xié)議,客戶端通過發(fā)送HTTP請求到服務(wù)器,服務(wù)器接收并處理請求,并返回HTTP響應(yīng)給客戶端。但是,在傳統(tǒng)的HTTP協(xié)議中,客戶端只能發(fā)送請求,而服務(wù)器只能通過響應(yīng)來處理客戶端的請求。

  • WebSocket協(xié)議

WebSocket協(xié)議是在HTTP協(xié)議的基礎(chǔ)上進(jìn)行擴(kuò)展的。在建立WebSocket連接時,客戶端首先發(fā)送一個HTTP請求到服務(wù)器,并將Upgrade頭部字段設(shè)置為"websocket",表示希望升級到WebSocket協(xié)議。服務(wù)器接收到這個請求后,如果支持WebSocket協(xié)議,會返回一個狀態(tài)碼101 Switching Protocols的HTTP響應(yīng),并通過Upgrade頭部字段將連接升級為WebSocket連接。

升級完成后,客戶端和服務(wù)器之間的通信不再遵循HTTP請求-響應(yīng)模式,而是通過WebSocket協(xié)議進(jìn)行雙向的實(shí)時通信??蛻舳撕头?wù)器可以直接發(fā)送消息給對方,不需要等待對方的請求。


如何使用WebSocket

建立WebSocket連接:

要建立WebSocket連接,需要在客戶端和服務(wù)器之間進(jìn)行系列的握手操作。下面是詳細(xì)的代碼教程,示了如何在Web應(yīng)用程序中建立WebSocket連接。

在戶端(JavaScript):

// 創(chuàng)建WebSocket對象并指定服務(wù)器地址
var socket = new WebSocket("ws://example.com/socket");

// 監(jiān)聽連接建立事件
socket.onopen = function() {
console.log("WebSocket連接已建立");
// 在連接建立后,可以發(fā)送消息到服務(wù)器
socket.send("Hello Server!");
};


// 監(jiān)聽接收到服務(wù)器發(fā)送的消息
socket.onmessage = function(event) {
var message = event.data;
console.log("接收到服務(wù)器發(fā)送的消息:" + message);
};


// 監(jiān)聽連接關(guān)閉事件
socket.onclose = function(event) {
console.log("WebSocket連接已關(guān)閉");
};


// 監(jiān)聽連接錯誤事件
socket.onerror = function(event) {
console.error("WebSocket連接錯誤:" + event};

 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在服務(wù)器端(示例使用Node.js):

const WebSocket = require("ws");

// 創(chuàng)建WebSocket服務(wù)器
const wss = new WebSocket.Server({ port: 8080 });


// 監(jiān)聽連接建立事件
wss.on("connection", function(socket) {
console.log("WebSocket連接已建立");


// 監(jiān)聽接收到客戶端發(fā)送的消息
socket.on("message", function(message) {
console.log("接收到戶端發(fā)送的消息:" + message);


<span class="token comment">// 向客戶端發(fā)送消息</span>
socket<span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span><span class="token string">"Hello Client!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

});


// 監(jiān)聽連接關(guān)閉事件
socket.on("close", function() {
console.log("WebSocket連接已關(guān)閉");
});
});

 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

在以上代碼中,客戶端通過創(chuàng)建WebSocket對象,并指定服務(wù)器地址"ws://example.com/socket"來建立WebSocket連接。同時,客戶端通過監(jiān)聽onopen事件,可以在連接建立后發(fā)送消息到服務(wù)器。服務(wù)器端使用WebSocket.Server類創(chuàng)建WebSocket服務(wù)器,并監(jiān)聽"connection事件來處理連接建立后的操作。服務(wù)器端通過socket.on(“message”)來監(jiān)聽接收到客戶端發(fā)送的消息,并通過socket.send()向客戶端發(fā)送消息。

數(shù)據(jù)傳輸:

建立WebSocket連接后,客戶端和服務(wù)器可以通過WebSocket對象進(jìn)行雙向的實(shí)時數(shù)據(jù)傳輸。下面是一個示例代碼,演示了如何在客戶端和服務(wù)器之間進(jìn)行數(shù)據(jù)傳輸。

在客戶端(JavaScript):

// 發(fā)送消息到服務(wù)器
socket.send("Hello Server!");

// 監(jiān)聽接收到服務(wù)器發(fā)送的消息
socket.onmessage = function(event) {
var message = event.data;
console.log("接收到服務(wù)器發(fā)送的消息:" + message);
};

 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

在服務(wù)器端(示例使用Node.js):

// 向客戶端發(fā)送消息
socket.send("Hello Client!");

// 監(jiān)聽接收到客戶端發(fā)送的消息
socket.on("message", function(message) {
console.log("接收到客戶端發(fā)送的消息:" + message);
});

 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

在以上代碼中,客戶端通過調(diào)用socket.send()方法將消息發(fā)送到服務(wù)器,服務(wù)器通過socket.send()方法將消息發(fā)送到客戶端??蛻舳送ㄟ^監(jiān)聽socket.onmessage事件來接收服務(wù)器發(fā)送的消息,服務(wù)器通過監(jiān)聽socket.on("message")事件來接收客戶端發(fā)送的消息。

通過以上代碼示例,你可以詳細(xì)了解如何使用WebSocket建立連接并進(jìn)行數(shù)據(jù)傳輸。請注意,示例代碼中使用的服務(wù)器地址和端口號需要根據(jù)實(shí)際情況進(jìn)行修改。同時,你還可以在具體應(yīng)用中根據(jù)需要使用WebSocket的其他方法和事件來實(shí)現(xiàn)更復(fù)雜的功能。


WebSocket的真實(shí)使用場景

即時通訊:

WebSocket非常適合用于即時通訊應(yīng)用,因?yàn)樗軌驅(qū)崿F(xiàn)實(shí)時雙向通信。以下是一個簡單的即時聊天應(yīng)用的代碼教程。

在客戶端(JavaScript):

// 創(chuàng)建WebSocket對象并指定服務(wù)器地址
var socket = new WebSocket("ws://example.com/socket");

// 監(jiān)聽連接建立事件
socket.onopen = function() {
console.log("WebSocket連接已建立");


// 監(jiān)聽文本框輸入,按下Enter鍵時發(fā)送消息
var input = document.getElementById("input");
input.addEventListener("keyup", function(event) {
if (event.keyCode === 13) {
var message = input.value;
socket.send(message);
input.value = "";
}
});
};


// 監(jiān)聽接收到服務(wù)器發(fā)送的消息
socket.onmessage = function(event) {
var message = event.data;
console.log("接收到服務(wù)器發(fā)送的消息:" + message);


// 將接收到的消息顯示在聊天窗口中
var chatWindow = document.getElementById("chatWindow");
chatWindow.innerHTML += "<p>" + message + "</p>";
};


// 監(jiān)聽連接關(guān)閉事件
socket.onclose = function(event) {
console.log("WebSocket連接已關(guān)閉");
};


// 監(jiān)聽連接錯誤事件
socket.onerror = function(event) {
console.error("WebSocket連接錯誤:" + event};

 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

在服務(wù)器端(示例使用Node.js):

const WebSocket = require("ws");

// 創(chuàng)建WebSocket服務(wù)器
const wss = new WebSocket.Server({ port: 8080 });


// 監(jiān)聽連接建立事件
wss.on("connection", function(socket) {
console.log("WebSocket連接已建立");


// 監(jiān)聽接收到客戶端發(fā)送的消息
socket.on("message", function(message) {
console.log("接收到客戶端發(fā)送的消息:" + message);


<span class="token comment">// 向所有連接的客戶端發(fā)送消息</span>
wss<span class="token punctuation">.</span>clients<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">client</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  client<span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span>message<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

});


// 監(jiān)聽連接關(guān)閉事件
socket.on("close", function() {
console.log("WebSocket連接已關(guān)閉");
});
});

 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在上述代碼中,客戶端通過創(chuàng)建WebSocket對象連接到服務(wù)器。輸入框中的文本框用于錄入要發(fā)送的消息,按下Enter鍵時會將消息發(fā)送給服務(wù)器。服務(wù)器接收到消息后,通過遍歷所有連接的客戶端,向每個客戶端發(fā)送消息。

這樣,多個客戶端就可以實(shí)時地進(jìn)行聊天,并且所有的消息都會實(shí)時地在各個客戶端之間同步顯示。

多人協(xié)作:

WebSocket還可用于多人協(xié)作應(yīng)用,讓多個用戶可以實(shí)時地協(xié)同編輯文檔或畫布。以下是一個簡單的代碼教程。

在客戶端(JavaScript):

// 創(chuàng)建WebSocket對象并指定服務(wù)器地址
var socket = new WebSocket("ws://example.com/socket");

// 監(jiān)聽連接建立事件
socket.onopen = function() {
console.log("WebSocket連接已建立");


// 監(jiān)聽文本框輸入,按下Enter鍵時發(fā)送繪畫指令
var canvas = document.getElementById("canvas");
canvas.addEventListener("mousedown", function(event) {
// 繪畫指令的數(shù)據(jù)格式可以自定義,這里使用了簡單的示例
var instruction = {
type: "draw",
position: {
x: event.clientX,
y: event.clientY
}
};
socket.send(JSON.stringify(instruction));
});
};


// 監(jiān)聽接收到服務(wù)器發(fā)送的消息
socket.onmessage = function(event) {
var message = JSON.parse(event.data);
console.log("接收到服務(wù)器發(fā)送的消息:" + message);


// 根據(jù)消息執(zhí)行相應(yīng)的操作,示例中處理了繪畫指令
if (message.type === "draw") {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.arc(message.position.x, message.position.y, 5, 0, 2 * Math.PI);
ctx.fill();
}
};


// 監(jiān)聽連接關(guān)閉事件
socket.onclose = function(event) {
console.log("WebSocket連接已關(guān)閉");
};


// 監(jiān)聽連接錯誤事件
socket.onerror = function(event) {
console.error("WebSocket連接錯誤:" + event};

 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

在服務(wù)器端(示例使用Node.js):

const WebSocket = require("ws");

// 創(chuàng)建WebSocket服務(wù)器
const wss = new WebSocket.Server({ port: 8080 });


// 監(jiān)聽連接建立事件
wss.on("connection", function(socket) {
console.log("WebSocket連接已建立");


// 監(jiān)聽接收到客戶端發(fā)送的消息
socket.on("message", function(message) {
console.log("接收到客戶端發(fā)送的消息:" + message);


<span class="token comment">// 向所有連接的客戶端發(fā)送消息</span>
wss<span class="token punctuation">.</span>clients<span class="token punctuation">.</span><span class="token function">forEach</span><span class="token punctuation">(</span><span class="token keyword">function</span><span class="token punctuation">(</span><span class="token parameter">client</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
  client<span class="token punctuation">.</span><span class="token function">send</span><span class="token punctuation">(</span>message<span class="token punctuation">)</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

});


// 監(jiān)聽連接關(guān)閉事件
socket.on("close", function() {
console.log("WebSocket連接已關(guān)閉");
});
});

 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

在上述代碼中,客戶端通過創(chuàng)建WebSocket對象連接到服務(wù)器。當(dāng)鼠標(biāo)在畫布上按下時,將繪畫指令發(fā)送給服務(wù)器。服務(wù)器接收到繪畫指令后,將指令廣播給所有連接的客戶端,并在各個客戶端上進(jìn)行繪畫操作。

這樣,多個用戶就可以實(shí)時地協(xié)同編輯同一個畫布或文檔,所有的繪畫指令都會即時同步在各個客戶端之間。

實(shí)時數(shù)據(jù)更新:

WebSocket還可以用于實(shí)時數(shù)據(jù)更新應(yīng)用,例如股票交易應(yīng)用中的實(shí)時股票價格更新。以下是一個簡單的代碼教程。

在客戶端(JavaScript):

// 創(chuàng)建WebSocket對象并指定服務(wù)器地址
var socket = new WebSocket("鏈接");

// 監(jiān)聽連接建立事件
socket.onopen = function() {
console.log("WebSocket連接已建立");
};


// 監(jiān)聽接收到服務(wù)器發(fā)送的消息
socket.onmessage = function(event) {
var message = JSON.parse(event.data);
console.log("接收到服務(wù)器發(fā)送的消息:" + message);


// 對接收到的實(shí)時數(shù)據(jù)進(jìn)行處理
var stockPriceElement = document.getElementById("stockPrice");
stockPriceElement.innerText = message.price;
};


// 監(jiān)聽連接關(guān)閉事件
socket.onclose = function(event) {
console.log("WebSocket連接已關(guān)閉");
};


// 監(jiān)聽連接錯誤事件
socket.onerror = function(event) {
console.error("WebSocket連接錯誤:" + event};

 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

在服務(wù)器端(示例使用Node.js):

const WebSocket = require("ws");

// 創(chuàng)建WebSocket服務(wù)器
const wss = new WebSocket.Server({ port: 8080 });


// 模擬實(shí)時股票價格更新
setInterval(function() {
var stockPrice = Math.random() * 100;


// 向所有連接的客戶端發(fā)送實(shí)時數(shù)據(jù)
wss.clients.forEach(function(client) {
var data = {
price: stockPrice
};
client.send(JSON.stringify(data));
});
}, 2000);


// 監(jiān)聽連接建立事件
wss.on("connection", function(socket) {
console.log("WebSocket連接已建立");


// 初始化發(fā)送實(shí)時數(shù)據(jù)
var stockPrice = Math.random() * 100;
var data = {
price: stockPrice
};
socket.send(JSON.stringify(data));


// 監(jiān)聽連接關(guān)閉事件
socket.on("close", function() {
console.log("WebSocket連接已關(guān)閉");
});
});

 

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34

在上述代碼中,客戶端通過創(chuàng)建WebSocket對象連接服務(wù)器。服務(wù)器使用setInterval函數(shù)模擬實(shí)時股票價格的更新,并將更新的數(shù)據(jù)發(fā)送給所有連接的客戶端。客戶端監(jiān)聽接收到服務(wù)器發(fā)送的消息,并處理接收到的實(shí)時數(shù)據(jù)。

這樣,在股票交易應(yīng)用中,多個用戶可以實(shí)時地接收和顯示股票價格的更新信息。包括打游戲的時候,隊(duì)友之間互相溝通,打字交流,或者走位,放技能等等,都是即時的。


WebSocket的優(yōu)勢與局限性

  • WebSocket的優(yōu)勢:
  • 雙向?qū)崟r通信:WebSocket提供了雙向的實(shí)時信能力,客戶端和服務(wù)器可以通過該協(xié)議進(jìn)行雙向數(shù)據(jù)傳輸,實(shí)時反饋更新信息,實(shí)現(xiàn)即時通訊、實(shí)時數(shù)據(jù)推送等功能。

  • 較低的延遲:與傳統(tǒng)的HTTP請求相比,WebSocket降低了通信的開銷,減少了傳輸和處理數(shù)據(jù)的延遲,因此可以更快進(jìn)行實(shí)時數(shù)據(jù)傳輸。

  • 更高的性能:由于WebSocket使用較少的頭部信息和更有效的消息傳輸格式,因此在相同帶寬下可以傳輸更多的數(shù)據(jù),提高了性能和效率。

  • 廣泛的瀏覽器支持:WebSocket是HTML5的一部分,并且得到了大多數(shù)現(xiàn)代瀏覽器的支持,因此它可以在各種平臺和設(shè)備上使用。

  • 連接保持:與傳統(tǒng)的HTTP請求不同,WebSocket連接保持在建立之后,雙方可以隨時進(jìn)行數(shù)據(jù)傳輸,避免了不必要的連接和斷開操作。

  • WebSocket的局限性:
  • 兼容性問題:雖然現(xiàn)代瀏覽器廣泛支持WebSocket,但在某些舊版本或特定設(shè)備上可能存在兼容性問題。為了兼容性,可以使用輪訓(xùn)技術(shù)(如長輪詢)作為備選方案。

  • 部署和維護(hù)復(fù)雜性:WebSocket服務(wù)器的設(shè)置和配置可能比傳統(tǒng)的Web服務(wù)器復(fù)雜一些,需要專門的服務(wù)器環(huán)境和配置。

  • 安全性問題:由于WebSocket是在HTTP協(xié)議的基礎(chǔ)上建立的,它們共享相同的安全風(fēng)險(xiǎn),例如跨站點(diǎn)腳本(XSS)和跨站請求偽造(CSRF)。因此,在使用WebSocket時需要考慮到安全性,并采取適當(dāng)?shù)陌踩胧?/p>

  • 擴(kuò)展問題:WebSocket協(xié)議還不支持像HTTP/2那樣的一些高級特性,例如頭部壓縮和流量控制。在某些特殊情況下,可能需要通過其他方式實(shí)現(xiàn)這些功能。

盡管WebSocket具有上述局限性,但它仍然是實(shí)時通訊、實(shí)時數(shù)據(jù)傳輸和實(shí)時協(xié)作等場景下的首選協(xié)議,因?yàn)樗邆淞穗p向?qū)崟r通信和較低延遲等一系列的優(yōu)勢。在開發(fā)時,需要根據(jù)具體需求和限制,綜合考慮使用WebSocket的適用性。


結(jié)論

WebSocket是一種能夠提供雙向?qū)崟r通信的協(xié)議,適用于需要實(shí)時數(shù)據(jù)傳輸和雙向通信的場景。它具有較低的延遲、更高的性和廣泛的瀏覽器持等優(yōu)勢,能夠?qū)崿F(xiàn)即時通訊、多人協(xié)和實(shí)時數(shù)據(jù)更新等功能。

然而,WebSocket也存在兼容性、部署和維護(hù)復(fù)雜性、安全性問題以及缺乏一些高級特性等局限性。在開發(fā)時,需要仔細(xì)考慮具體需求和限制,并必要時采取適當(dāng)?shù)慕鉀Q方案。

總的來說,WebSocket在實(shí)時通信和實(shí)時數(shù)據(jù)傳輸方面具有明顯的優(yōu)勢,是構(gòu)建現(xiàn)代Web應(yīng)用的重要工具之一。

藍(lán)藍(lán)設(shè)計(jì)(sillybuy.com )是一家專注而深入的界面設(shè)計(jì)公司,為期望卓越的國內(nèi)外企業(yè)提供卓越的大數(shù)據(jù)可視化界面設(shè)計(jì)、B端界面設(shè)計(jì)、桌面端界面設(shè)計(jì)APP界面設(shè)計(jì)、圖標(biāo)定制、用戶體驗(yàn)設(shè)計(jì)、交互設(shè)計(jì)UI咨詢、高端網(wǎng)站設(shè)計(jì)、平面設(shè)計(jì),以及相關(guān)的軟件開發(fā)服務(wù),咨詢電話:01063334945。

關(guān)鍵詞:UI咨詢、UI設(shè)計(jì)服務(wù)公司軟件界面設(shè)計(jì)公司、界面設(shè)計(jì)公司、UI設(shè)計(jì)公司、UI交互設(shè)計(jì)公司、數(shù)據(jù)可視化設(shè)計(jì)公司、用戶體驗(yàn)公司、高端網(wǎng)站設(shè)計(jì)公司

銀行金融軟件UI界面設(shè)計(jì)、能源及監(jiān)控軟件UI界面設(shè)計(jì)、氣象行業(yè)UI界面設(shè)計(jì)、軌道交通界面設(shè)計(jì)、地理信息系統(tǒng)GIS UI界面設(shè)計(jì)、航天軍工軟件UI界面設(shè)計(jì)、醫(yī)療行業(yè)軟件UI界面設(shè)計(jì)教育行業(yè)軟件UI界面設(shè)計(jì)、企業(yè)信息化UI界面設(shè)計(jì)、軟件qt開發(fā)、軟件wpf開發(fā)軟件vue開發(fā)

日歷

鏈接

個人資料

存檔