canvas實(shí)現(xiàn)水波紋效果

本文將會(huì)從水波的基本原理開始,詳細(xì)講解在canvas中模擬水波擴(kuò)散,分析并計(jì)算水波的能量分布,并通過振幅模擬水波對(duì)圖像的折射效果烙常,最后實(shí)現(xiàn)水波特效。

水波基本原理

首先復(fù)習(xí)一波高中物理知識(shí)被冒。

波是指振動(dòng)的傳播军掂。波的傳播方向與質(zhì)點(diǎn)振動(dòng)方向垂直的為橫波轮蜕,相同則為縱波,水波是橫波和縱波的疊加蝗锥。

對(duì)于水波這種波跃洛,我們?cè)趯?shí)現(xiàn)這個(gè)特效的時(shí)候,需要考慮到下面的特性:

  • 圓形波:當(dāng)你投一塊石頭到水池中時(shí)终议,你會(huì)看到一個(gè)以石頭入水點(diǎn)為圓心所形成的一圈圈的水波
  • 反射:水波碰到墻壁后會(huì)反射
  • 衰減:因?yàn)樗怯凶枘岬幕憬撸阅銜?huì)看到水波越往外擴(kuò)散,越弱穴张,最后消失细燎,水面回復(fù)平靜
  • 水波使得圖像發(fā)生折射,由于水波皂甘,使得水面凹凸不平玻驻,會(huì)折射和反射水池中的圖像
  • 衍射,波在傳播中遇到有很大障礙物或遇到大障礙物中的孔隙時(shí)偿枕,會(huì)繞過障礙物的邊緣或孔隙的邊緣璧瞬,呈現(xiàn)路徑彎曲,在障礙物或孔隙邊緣的背后展衍渐夸。

水波紋效果反映到圖像上嗤锉,其本質(zhì)就是像素的偏移,相當(dāng)于很多縮放的結(jié)合墓塌。因此對(duì)圖像的處理就轉(zhuǎn)化為如何移動(dòng)圖像上的像素點(diǎn)瘟忱,從而模擬和表現(xiàn)出水波紋的效果勾习。下面是本文將會(huì)實(shí)現(xiàn)的水波紋特效:更好的效果頁(yè)面

http://asset.uusama.com/example/water_ripple.html

波幅計(jì)算

波幅表示方法

波的本質(zhì)是振動(dòng)充岛,然后傳遞能量,波的表現(xiàn)形式就是能量的分布情況原献,我們使用波幅(振動(dòng)幅度)來描述每一點(diǎn)攜帶的能量态坦。

假設(shè)一開始水面是平靜的盐数,整個(gè)水面的能量均勻分布。我們知道在canvas中伞梯,我們可以使用ctx.getImageData(0, 0, width, height)方法將一幅寬為width,高為height的圖像像素信息存入一個(gè)數(shù)組中帚屉,這個(gè)數(shù)組大小為 width × height × 4 bytes(RGBA信息)谜诫。

我們可以建立兩個(gè)和圖像一樣大小 width × height的數(shù)組,用來保存水面上每一個(gè)點(diǎn)的前一時(shí)刻和后一時(shí)刻波幅數(shù)據(jù)攻旦∮骺酰或者直接使用一個(gè) 2 × width × height的數(shù)組,分為前半部分和后半部分來保存前后時(shí)刻的波幅數(shù)據(jù)牢屋。

水面在初始狀態(tài)時(shí)是平靜的平面且预,各點(diǎn)的波幅都為0槽袄,所以,數(shù)組的所有初始值都等于0锋谐。

var width = settings.width,  // canvas寬度
      height = settings.height, // canvas高度
      amplitude_size = width * (height + 2) * 2, // 振幅數(shù)組大小
      ripple_map = [],  // 產(chǎn)生水波下一時(shí)刻振幅
      last_map = [];  // 初始時(shí)刻振幅
// 波幅數(shù)組初始化為0
for (var i = 0; i < amplitude_size; i++) {
    ripple_map[i] = last_map[i] = 0;
}

忽略阻尼計(jì)算振幅

由上面一小節(jié)遍尺,我們可以用X_i來表示圖像中的任意一個(gè)像素點(diǎn),其中i的值在0到 width × height之間涮拗,我們把寬度width簡(jiǎn)記為W乾戏,將高度height簡(jiǎn)記為H,則可以用下面的集合表示圖像上的像素點(diǎn)集合

如果你發(fā)現(xiàn)下面的公式顯示不正常三热,那么是解析器插件罷工了鼓择,請(qǐng)移步到這兒

\\{ X_i|0\le i \le WH \\}

  • 其中坐標(biāo)為(x,y)的點(diǎn)為X_{yW+x}

由于波的傳播特性,某一點(diǎn)下一時(shí)刻的振動(dòng)情況就漾,受到周圍質(zhì)點(diǎn)的振動(dòng)以及自身振動(dòng)情況的聯(lián)合影響呐能。為了使問題簡(jiǎn)化,我們假設(shè)X_i點(diǎn)的振幅A_i除了受到自身的影響外抑堡,還受到來自它周圍前催跪、后、左夷野、右四個(gè)點(diǎn)(X_{i-W},X_{i+W},X_{i-1},X_{i+1})的影響懊蒸,并且假設(shè)這四個(gè)點(diǎn)對(duì)X_i點(diǎn)的影響力機(jī)會(huì)均等并且線性疊加的。那么可以得到X_i點(diǎn)的振幅公式:

A_i^{\prime} = a(A_{i-W}+A_{i+W}+A_{i-1}+A_{i+1})+bA_i

  • A_i分別為點(diǎn)X_i當(dāng)前時(shí)刻的振幅
  • a悯搔、b為待定系數(shù)骑丸,A_0^{\prime}X_0點(diǎn)下一時(shí)刻的振幅
  • 對(duì)于圖像邊界上的點(diǎn),需要進(jìn)行特殊處理妒貌,可以適當(dāng)增大振幅數(shù)組:(W+2)x(H+2)

假設(shè)水的阻尼為0通危。在這種理想條件下,水的總勢(shì)能將保持不變灌曙。也就是說在任何時(shí)刻菊碟,所有點(diǎn)的振幅的和保持不變。那么可以得到下面能量守恒公式:

\sum_{i=0}^n{A_i^{\prime}}=\sum_{i=0}^n{A_i}

將上面的X_i點(diǎn)的振幅公式帶入可得:

\sum_{i=0}^n{[a(A_{i-W}+A_{i+W}+A_{i-1}+A_{i+1})+bA_i]}=\sum_{i=0}^n{A_i}

拆開可得:

a \sum_{i=0}^n{A_{i-W}}+a \sum_{i=0}^n{A_{i+W}}+a \sum_{i=0}^n{A_{i-1}}+a \sum_{i=0}^n{A_{i+1}}+b \sum_{i=0}^n{A_i}=\sum_{i=0}^n{A_i}

其中可以近似的認(rèn)為:

\sum_{i=0}^n{A_{i-W}}= \sum_{i=0}^n{A_{i+W}}= \sum_{i=0}^n{A_{i-1}}= \sum_{i=0}^n{A_{i+1}}= \sum_{i=0}^n{A_i}

等式兩邊消去可得:

4a+b=1

找出一個(gè)最簡(jiǎn)解:a = \frac{1}{2}, b = -1

因?yàn)?img class="math-inline" src="https://math.jianshu.com/math?formula=%5Cfrac%7B1%7D%7B2%7D" alt="\frac{1}{2}" mathimg="1">可以用移位運(yùn)算符“>>”來進(jìn)行在刺,不用進(jìn)行乘除法逆害,所以,這組解是最適用的而且是最快的蚣驼。那么最后得到的下一時(shí)刻的振幅公式就是:

A_i^{\prime} =\frac{1}{2}(A_{i-W}+A_{i+W}+A_{i-1}+A_{i+1})-A_i

得到上面這個(gè)近似公式后魄幕,如果已知某一時(shí)刻水面上任意一點(diǎn)的波幅,就可以求出下一時(shí)刻水面上任意一點(diǎn)的波幅颖杏。

考慮阻尼

然而纯陨,在實(shí)際中是存在阻尼的,否則,用上面這個(gè)公式翼抠,一旦你在水中增加一個(gè)波源咙轩,水面將永不停止的震蕩下去。

所以阴颖,還需要對(duì)波幅數(shù)據(jù)進(jìn)行衰減處理活喊,讓每一個(gè)點(diǎn)在經(jīng)過一次計(jì)算后,波幅都比理想值按一定的比例降低膘盖。這個(gè)衰減率經(jīng)過測(cè)試胧弛,用\frac{1}{32}比較合適,也就是\frac{1}{2^5}侠畔,可以通過移位運(yùn)算很快的獲得结缚。

最后的振幅計(jì)算算法如下:

// 計(jì)算下一時(shí)刻波幅,index為像素點(diǎn)位置软棺,old_amplitude為上一時(shí)刻該點(diǎn)波幅
function calculAmplitude(index, old_amplitude) {
    var x_boundary = 0, judge = map_index % width;
    // 由于波幅數(shù)據(jù)順序存儲(chǔ)红竭,加上左右邊界檢查,避免左邊水波傳遞到右邊
    if (judge == 0) {
        x_boundary = 1; // 左邊邊界
    }else if (judge == width - 1) {
        x_boundary = 2; // 右邊邊界
    }
    var top = ripple_map[index - width],// 上邊的相鄰點(diǎn)
        bottom = ripple_map[index + width],// 下邊的相鄰點(diǎn)
        left = x_boundary != 1 ? ripple_map[index - 1] : 0,// 左邊的相鄰點(diǎn)
        right = x_boundary != 2 ? ripple_map[index + 1] : 0;// 右邊的相鄰點(diǎn)
    // 計(jì)算當(dāng)前像素點(diǎn)下一時(shí)刻的振幅
    var amplitude = top + bottom + left + right;
    amplitude >>= 1;
    amplitude -= old_amplitude;
    amplitude -= amplitude >> 5;  // 計(jì)算衰減
    return amplitude;
}

頁(yè)面渲染

因?yàn)樗恼凵浯洌?dāng)水面不與我們的視線相垂直的時(shí)候茵宪,我們所看到的水下的景物并不是在觀察點(diǎn)的正下方,而存在一定的偏移瘦棋。

偏移的程度與水波的斜率稀火,水的折射率和水的深度都有關(guān)系,如果要進(jìn)行精確的計(jì)算的話赌朋,顯然是很不現(xiàn)實(shí)的凰狞。同樣,我們只需要做線形的近似處理就行了沛慢。

因?yàn)樗嬖絻A斜赡若,所看到的水下景物偏移量就越大,最簡(jiǎn)單的做法可以近似的用水面上某點(diǎn)的前后团甲、左右兩點(diǎn)的波幅之差來代表所看到水底景物的偏移量逾冬。

這里我們選用畫面的中點(diǎn)作為參考點(diǎn)來計(jì)算視覺的偏移。

我們將原始圖像的像素信息保存在兩個(gè)數(shù)組中躺苦,一個(gè)用于保存原始圖像數(shù)據(jù)身腻,一個(gè)用于實(shí)時(shí)保存實(shí)時(shí)渲染數(shù)據(jù)。這里需要注意更新圖像的時(shí)候圾另,圖像的恢復(fù)問題霸株,這里我們用一個(gè)反相器來進(jìn)行恢復(fù),一個(gè)點(diǎn)偏移了集乔,我們給它一個(gè)反方向的偏移來抵消就可以恢復(fù)。

根據(jù)偏移量將原始圖象上的每一個(gè)象素復(fù)制到渲染頁(yè)面上,將渲染數(shù)據(jù)繪制到canvas中即可扰路。

// 渲染下一幀
function renderRipple() {
    var i = old_index,
        deviation_x,  // x水平方向偏移
        deviation_y,  // y豎直方向偏移
        pixel_deviation, // 偏移后的ImageData對(duì)象像素索引
        pixel_source;  // 原始ImageData對(duì)象像素索引

    // 交互索引 old_index, new_index
    old_index = new_index;
    new_index = i;

    // 設(shè)置像素索引和振幅索引
    i = 0;
    map_index = old_index;

    // 渲染所有像素點(diǎn)
    for (var y = 0; y < height; y++) {
        for (var x = 0; x < width; x++) {
            // 計(jì)算當(dāng)前像素點(diǎn)下一時(shí)刻的振幅
            var amplitude = calculAmplitude(map_index, ripple_map[new_index + i]);

            // 更新振幅數(shù)組
            ripple_map[new_index + i] = amplitude;

            amplitude = 1024 - amplitude;
            var old_amplitude = last_map[i];
            last_map[i] = amplitude;

            if (old_amplitude != amplitude) {
                 // 計(jì)算偏移
                deviation_x = (((x - half_width) * amplitude / 1024) << 0) + half_width;
                deviation_y = (((y - half_height) * amplitude / 1024) << 0) + half_height;

                // 檢查邊界
                if (deviation_x > width) {
                    deviation_x = width - 1;
                }
                if (deviation_x < 0) {
                    deviation_x = 0;
                }
                if (deviation_y > height) {
                    deviation_y = height - 1;
                }
                if (deviation_y < 0) {
                    deviation_y = 0;
                }
                
                // 計(jì)算imageData中對(duì)應(yīng)的像素RGBA偏移位置
                pixel_source = i * 4;
                pixel_deviation = (deviation_x + (deviation_y * width)) * 4;

                // 移動(dòng)像素的RGBA信息尤溜,ripple和texture為背景圖的ImageData對(duì)象
                ripple.data[pixel_source] = texture.data[pixel_deviation];
                ripple.data[pixel_source + 1] = texture.data[pixel_deviation + 1];
                ripple.data[pixel_source + 2] = texture.data[pixel_deviation + 2];
            }
            ++i;
            ++map_index;
        }
    }
    // 渲染處理之后的圖像
    ctx.putImageData(ripple, 0, 0);
}

波源

為了形成波,我們必須在平靜的水面上加入波源汗唱,就像向水池中投入一個(gè)石頭一樣宫莱,形成的波源的大小和能量與石頭的半徑和你扔石頭的力量都有關(guān)系。

為了模擬波源哩罪,我們只需要修改一開始初始化的波幅分布數(shù)組即可授霸。需要注意投入石頭的地方的波幅不易過小和過大。

另外际插,這個(gè)波源的半徑也很好控制碘耳,只要以波源為圓心,畫一個(gè)圓框弛,讓這個(gè)圓內(nèi)的所有點(diǎn)都來一個(gè)脈沖即可辛辨。

波源生成方法如下:

// 在指定地點(diǎn)產(chǎn)生波源
function disturb(circleX, circleY) {
    // 下面的移位運(yùn)算可以將值向下取整
    circleX <<= 0;
    circleY <<= 0;
    var maxDistanceX = circleX + dropRadius,
        maxDistanceY = circleY + dropRadius;
    for (var y = circleY - dropRadius; y < maxDistanceY; y++) {
        for (var x = circleX - dropRadius; x < maxDistanceX; x++) {
            ripple_map[old_index + y * width + x] += 512;
        }
    }
}

待處理事宜

還有很多要完善的地方,以后會(huì)更新到github瑟枫,本文所有的效果代碼也可以在Git上面找到斗搞,歡迎大家star。

最后慷妙,簡(jiǎn)單列一下接下來需要優(yōu)化的點(diǎn):

  • 添加衍射
  • 兼容跨域圖片
  • 圖片自動(dòng)縮放處理
  • JQuery插件化封裝
  • 適配優(yōu)化僻焚,速度優(yōu)化,效果優(yōu)化
  • 普通HTML元素支持膝擂,局部特效

衍射

在水波擴(kuò)散的過程中虑啤,如果遇到障礙物,水波會(huì)繞過障礙物的邊緣或孔隙的邊緣猿挚,呈現(xiàn)路徑彎曲咐旧,在障礙物或孔隙邊緣的背后展衍。

其實(shí)實(shí)現(xiàn)起來很簡(jiǎn)單绩蜻,我們只要始終保持障礙物的振幅一直為0即可铣墨。

原文鏈接:http://uusama.com/643.html
canvas系列教程:http://uusama.com/tag/canvas

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市办绝,隨后出現(xiàn)的幾起案子伊约,更是在濱河造成了極大的恐慌,老刑警劉巖孕蝉,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件屡律,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡降淮,警方通過查閱死者的電腦和手機(jī)超埋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人霍殴,你說我怎么就攤上這事媒惕。” “怎么了来庭?”我有些...
    開封第一講書人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵妒蔚,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我月弛,道長(zhǎng)肴盏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任帽衙,我火速辦了婚禮菜皂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘佛寿。我一直安慰自己幌墓,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開白布冀泻。 她就那樣靜靜地躺著常侣,像睡著了一般。 火紅的嫁衣襯著肌膚如雪弹渔。 梳的紋絲不亂的頭發(fā)上胳施,一...
    開封第一講書人閱讀 49,816評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音肢专,去河邊找鬼舞肆。 笑死,一個(gè)胖子當(dāng)著我的面吹牛博杖,可吹牛的內(nèi)容都是我干的椿胯。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼剃根,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼哩盲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起狈醉,我...
    開封第一講書人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤廉油,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后苗傅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抒线,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年渣慕,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了嘶炭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抱慌。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖旱物,靈堂內(nèi)的尸體忽然破棺而出遥缕,到底是詐尸還是另有隱情卫袒,我是刑警寧澤宵呛,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站夕凝,受9級(jí)特大地震影響宝穗,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜码秉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一逮矛、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧转砖,春花似錦须鼎、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至姓赤,卻和暖如春赡译,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背不铆。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來泰國(guó)打工蝌焚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人誓斥。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓只洒,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親劳坑。 傳聞我的和親對(duì)象是個(gè)殘疾皇子毕谴,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容