2019年12月29日 星期日


在使用 Angularjs 的 directive 時,
有時我們會想要在外層 (即使用 directive 的 controller 或其他 directive) 去呼叫 directive 裡面的 function,
去改變一些 directive 的內部狀態,
這時使用 service 就是一個比較好的做好,
service 可以作為 directive 及使用 directive 的元件 (例如外部 controller 或其他 directive)
的溝通橋梁,
利用把 directive 的 scope 或 內部 controller 綁定設定到 service 中,
我們即可在外部將 service 注入,並使用 service 來控制 directive。

下面給出一個範例 : 可以看到,<my-form-directive> 是我們實作的一個 directive,
裡面有一個名為 "pristineForm" 的 function ,
其可以設定 $scope 裡面的 myForm (為在 directive template 中 name="myForm" 的 <form>)
成初始狀態 ( 使用 formController.$setPristine() )。

在 <my-form-directive> 外,有一個 <div> 被設定了  ng-click="ctrl.pristineForm() ,
會去呼叫外層 controller 的 pristineForm(),
而 pristineForm() 會利用引入的 directiveHandler 這個 service ,
去呼叫 myFormDirective 這個 directive 內部的 pristineForm()。

下面是詳細的程式碼:

Javascript :
angular.module("app", [])
  .controller("controller", ["directiveHandler", function(directiveHandler) {
    var self = this;
    self.pristineForm = function(directiveName) {
     var directiveName = directiveName ? directiveName : 'myFormDirective';
      directiveHandler.getDirective(directiveName).pristineForm();
    }

  }])
  .directive("myFormDirective", ["directiveHandler", function(directiveHandler) {
    return {
      restrict: "E",
      template: `<div>
                  <form name="myForm">
                    Username : <input type="text" name="username" ng-model="username" required/>
                    <div ng-show="myForm.username.$dirty && myForm.username.$invalid">
                      Please fill in username.
                    </div>
                    <div>Form $pristine : {{myForm.$pristine}}</div>
                  </form>
            </div>`,
      replace: true,
      scope: {
      },
      link: function($scope, $elm, $attrs, $ctrl) {
        var directiveNameAttr = $attrs.dirName ? $attrs.dirName : "myFormDirective";
        directiveHandler.registerDirective(directiveNameAttr, $scope);

        $scope.username = "";
        $scope.pristineForm = function() {
          $scope.myForm.$setPristine();
        }
      }
    };
  }]).factory('directiveHandler', function() {
    var instance_map = {};
    var service = {
      registerDirective: registerDirective,
      getDirective: getDirective,
      deregisterDirective: deregisterDirective
    };

    return service;

    function registerDirective(name, ctrl) {
      instance_map[name] = ctrl;
    }

    function getDirective(name) {
      return instance_map[name];
    }

    function deregisterDirective(name) {
      instance_map[name] = null;
    }
  });

HTML :
<div ng-app="app" ng-controller="controller as ctrl">
  <my-form-directive dir-name="myFormDir"></my-form-directive>
  <div ng-click="ctrl.pristineForm('myFormDir')"><button>Set Form Pristine</button></div>
  
  <my-form-directive dir-name="myFormDir2"></my-form-directive>
  <div ng-click="ctrl.pristineForm('myFormDir2')"><button>Set Form Pristine</button></div>
</div>



說明:
在 directiveHandler 中,設定了一個內部管理的 instance_map,
用 key-value 的次式將需要的 directive instance (可能是 isolate scope 的 scope,可能是 directive 設定的 controller 等,依情況自行決定) 存起來,
設定了三個 function,

主要的為  registerDirective(name, ctrl) 和 getDirective(name),
registerDirective(name, ctrl) 主要在 directive 初始化時使用一次,
用想要的 name 作為 key,把想要暴露給外部使用的 instance 作為 ctrl 設定進來。

getDirective(name) 在外部使用,例如此例的 controller,
用 name 值來取得 directive 的內部 instance,例如此例即為 <my-form-directive> 的 isolate scope,
取得 instance 後,即可執行 instance 上所設定的 function,當然取得 instance 上設定的值也是可以的。

在 html 中使用 <my-form-directive> 時,給了一個 dir-name 屬性,
dir-name 屬性在 directive 用來作為 name 參數呼叫 registerDirective(name, ctrl),
如果有多個 directive 的話,為了區分各個 directive,
給定不同的 name 作為 instance 在 directiveHandler 中的 key 值將會
使管理各個 directive 的 instance 更為方便。

為了演示,我們在 html 中使用了兩個 <my-form-directive>,並給它們設定了不同的 dir-anem 屬性值。

參考資料:

  1. How to call a method defined in an AngularJS directive? (最喜歡Mudassir Ali的回答)

2019年12月9日 星期一

RWD <table> - 純 html, css - 無 javascript

這篇紀錄了學到的 <table> RWD  的純 html, css 顯示技巧。
雖然在現今的 RWD 網站設計裡,因為 <table>的一些缺點,
通常都還是建議以不要用 <table> 的方式去做設計 (改用 <div>....等)。

但有時我們還是會有需要用到 <table> 的情況,
例如:

  1. 因為某些原因難以重改 html 結構 (含有 <table> ) 的網站 (老專案、時程、人力、需求等 ......)。
  2. <table> 在表示列表型資料時的方便性,居中對齊、控制寬度容 (雖然通常代表寬度 無法 RWD) 易等。

如果真得要用 <table> 時,有辦法加上一些 RWD 的效果嗎?
以下為 <table> RWD CSS 的範例,先看在 JSFiddle 的成果展示:



如果是寬螢幕的話,會是如下顯示:



如果是窄螢幕的話,就會是如下顯示:



再來看 html 結構及 CSS:

 html :
<table class="rwd-table">
  <tr>
    <th></th>
    <th>Very poor</th>
    <th>Poor</th>
    <th>Fair</th>
    <th>Good</th>
    <th>Very good</th>
  </tr>
  <tr>
    <td data-th="">Score 1 :</td>
    <td data-th="Very poor"><input type="radio" name="Score1"/></td>
    <td data-th="Poor"><input type="radio" name="Score1"/></td>
    <td data-th="Fair"><input type="radio" name="Score1"/></td>
    <td data-th="Good"><input type="radio" name="Score1"/></td>
    <td data-th="Very good"><input type="radio" name="Score1"/></td>
  </tr>
  <tr>
    <td data-th="">Score 1 :</td>
    <td data-th="Very poor"><input type="radio" name="Score2"/></td>
    <td data-th="Poor"><input type="radio" name="Score2"/></td>
    <td data-th="Fair"><input type="radio" name="Score2"/></td>
    <td data-th="Good"><input type="radio" name="Score2"/></td>
    <td data-th="Very good"><input type="radio" name="Score2"/></td>
  </tr>
  <tr>
    <td data-th="">Score 1 :</td>
    <td data-th="Very poor"><input type="radio" name="Score3"/></td>
    <td data-th="Poor"><input type="radio" name="Score3"/></td>
    <td data-th="Fair"><input type="radio" name="Score3"/></td>
    <td data-th="Good"><input type="radio" name="Score3"/></td>
    <td data-th="Very good"><input type="radio" name="Score3"/></td>
  </tr>
</table>

在 html 裡,只是放了一個普通的 <table> ,並在其加上了一個class, "rwd-table" 做為 rwd table 的 css selector,
另一個是我們用了 data-th (屬性名可自取) 這個屬性來記錄每個 <td> 對應的 <th> 標題值,
以利在窄螢幕時,可以用 css 的 :before 或 :after 偽元素中的 content 來取得標題值。

CSS:
/* normal table css */
/* normal table css */
table {
    border-collapse: collapse;
    border-spacing: 0;
}

table tr td, table tr th{
  border : 1px solid black;
}

table tr:nth-child(even){
  background-color : #d0aaaa;
}

/* rwd table css */
.rwd-table {
 overflow: hidden;
}

.rwd-table {
  min-width: 100%;
}

.rwd-table th {
  display: none;
}

.rwd-table td {
  display: block;
}

.rwd-table td:after {
  content: attr(data-th);
  display: inline-block;
}


.rwd-table tr {
    margin-bottom: 5px;
    display: inline-block;
    width: 100%;
}

@media (min-width: 583px) {
  .rwd-table td:after {
    display: none;
  }
 .rwd-table th, .rwd-table td {
    display: table-cell;
  }
  .rwd-table tr {
    margin-bottom: 0px;
    display: table-row;
  }
  
}

在 CSS 中,只要注意關於 .rwd-table 的部份 (即注解 /* rwd table css */ 以下的部份),
重點就是:

  1. 在寬螢幕時:
    1. th 正常顯示
    2. td:after 或 td:before 不顯示
    3. th, td 使用 display: table-cell
    4. tr 使用 display: table-row
  2. 在窄螢幕時:
    1. th 不顯示
    2. td:after 或 td:before 顯示,使用 content : attr(data-th) 全得 td 上的 data-th 屬性值
    3. td 使用 display: block
    4. tr 使用 display: inline-block


參考資料:
  1. Bootstrap教學-實現Table表格也支援RWD自適應效果
  2. 利用 Pure CSS 讓 HTML Table 也能有 RWD 效果 - Yowko's Notes
  3. 使用CSS製作響應式破壞式表格

2019年12月7日 星期六

React 練習 - ToDo List (待辦事項) 簡易實作

這篇要來記錄一下 React 的
ToDo List (待辦事項) 簡易實作(無用到 Redux 之類的)

首先先來看一下成果的樣子:

並將它分

接下來是需求說明:
  1. 有一個供使用者輸入的<input type="text"/> 框,使用者輸入待辦事項的文字後,
    按下Enter鍵(配合 <form>)或按下 "addToDo" 按鈕,可以加進下方的待辦事項顯示區域 (<ul><li>)。
    為了演示一下雙向資料流的實作,放了兩個待辦事項輸入框,
    在使用者在其中一個輸入框輸入資料時,另一個的輸入框會同步顯示輸入的待辦事項的內容。
  2. 使用者在輸入待辦事項的文字時,下方有一行文字可以顯示使用者正在輸入的文字。
  3. 下方的待辦事項清單中,各個待辦事項右方有一個 "Delete" 按鈕,按下可將待辦事項移除

再來把其需求分解成各個Component,如下圖所示:
  1. Main.jsx :
    統整所有 Component 的最上層 Component,也管理著 state。
  2. TextInput.jsx :
    供使用者輸入待辦事項的地方。
  3. TextDisplayer.jsx :
    顯示使用者正在輸入的待辦事項。
  4. ToDoList.jsx :
    顯示待辦事項例表的 Component,其內部用到了子 Component, "ToDoRow.jsx"。
  5. ToDoRow.jsx :
    顯示各個獨立待辦事項的 Component,包含著 Delete 等功能。

2019年9月23日 星期一

使用RxJs 實作 Hover 的下拉選單 - RxJs

RxJs 是一個能幫助處理Javascript 各種事件(event)的好工具,
尤其是要偵測的事件是一組事件,而不是單一的事件時會非常的好用,

今天要來利用 RxJs 來實作 Hover 的下來選單,
成品如下:

首先在螢幕上方有一個 "Menu" 區塊,
在 "Menu" 區塊中有一個 "Menu Child" 區塊。

當滑鼠移到 "Menu" 區塊時,下方會滑出一塊 "Dropdown" 區塊,
裡面有一個 "Dropdown Child" 區塊。

當滑鼠移出 "Menu" 區塊及 "Dropdown" ,稍微等一下 (例如 100 毫秒),
如果此時滑鼠不在 "Menu" 區塊或 "Dropdown" 區塊,就將 "Dropdown" 區塊上來關掉,
不然就不做任何事 (即 "Dropdown" 區塊繼續維持下拉開著)。

在這裡我們有一個一組的事件需要處理,就是 :

滑鼠移出  "Menu" 區塊及 "Dropdown" --> 等一下 -->  滑鼠是否在 "Menu" 區塊或 "Dropdown" 區塊上

如果使用純 Javascript,我們可能會需要使用綁定 mouseenter, mouseleave 並配合 setTimeout 來處理,
也就是在 mouseenter 上做紀錄是否滑鼠移動進 "Menu" 或 "Dropdown" 區塊,
並在 mouseleave 後,用setTimeout 等一下讓 mouseenter 觸發上述紀錄的程式 (因為 mouseleave 會比 mouseenter先提早觸發) ,
等 mouseenter 紀錄完後在判斷要不要關掉 "Dropdown" 區塊。

但使用 RxJS 的話,將會使程式碼簡化許多。

先來看完整的程式碼:
使用的工具:
RxJS : 5.0.1 (下載)
jQuery : 1.9.1

HTML:
<div class="wrapper">
  <div class="menu">
    Menu
    <div class="menu_child">
      Menu Child
    </div>
  </div>
  <div class="dropdown" style="display:none;">
    Dropdown
    <div class="dropdown_child">
      Dropdown Child
    </div>
  </div>
</div>

CSS :
.wrapper {
  width: 500px;
  margin: 0 auto;
}

.menu {
  background-color: yellow;
}

.menu_child {
  background-color: #00ffd0;
  width: 300px;
  margin: 0 auto;
}

.dropdown {
  background-color: green;
  height: 300px;
}

.dropdown_child {
  background-color: #bc0eda;
  width: 300px;
  margin: 0 auto;
}

Javascript :
var $menu = $(".menu");
var $menu = $(".menu");
var $dropdown = $(".dropdown");

$menu.on("mouseenter", function() {
  $dropdown.slideDown();
});

var $menuMouseLeaveEvent = Rx.Observable.fromEvent($menu[0], "mouseleave");
var $dropdownMouseLeaveEvent = Rx.Observable.fromEvent($dropdown[0], "mouseleave");
var $menuMouseEnterEvent = Rx.Observable.fromEvent($menu[0], "mouseenter")
var $dropdownMouseEnterEvent = Rx.Observable.fromEvent($dropdown[0], "mouseenter")

var mouseLeaveEvent = Rx.Observable.merge($menuMouseLeaveEvent, $dropdownMouseLeaveEvent);
var mouseEnterEvent = Rx.Observable.merge($menuMouseEnterEvent, $dropdownMouseEnterEvent);

mouseLeaveEvent.map(function(e){
 return mouseEnterEvent.buffer(Rx.Observable.timer(100));
})
.concatAll()
.subscribe({
  next: function(value) {
   //if value.length > 0, it means one or more than one mouseEnterEvent 
    // was observed during 100 ms.
    if (value.length > 0) {
      $dropdown.slideDown();
    } else {
      $dropdown.slideUp();
    }
  }
});

說明:

  1. mouseEnterEvent.buffer(Rx.Observable.timer(100));
    的效果是,在一定時間後 (這裡是 100 毫秒),
    將這段時間中觀察到的 event 丟出,在此例中,就是 mouseEnterEvent。
  2. 使用 mouseenter 和 mouseleave 而不使用 mouseover 和 mouseout 事件,
    這樣才會不在滑鼠在"Menu", "Dropdown" 的子層元素 (Child DOM ) 進出時觸發了mouseover和mouseout事件。

--------------------------------------------------------------------------------------------------------------------------

改良版:
參考 Build dropdown list with RxJS 了這篇文章以後,
非常喜歡他的思考方式,
跟我之前單純用事件去想的方式不同,
他採用了狀態的概念,下面說明:

使用狀態來表示滑鼠目前是在 Menu 還是 Dropdown 上面,
這邊先用 isOnMenu 來表示是否在 Menu 上的狀態,
用 isOnDropdown 來表示是否在 Dropdown 上的狀態,
isOnMenu叨和 isOnDropdown 的值都為布林值 (boolean),即 true 或 false。

如果我們能正確的得到 isOnMenu 和 isOnDropdown 的值,
那我們就可以歸納出如下的結果:
isOnMenu = true isOnMenu = false
isOnDropdown = true Dropdown 要打開 Dropdown 要打開
isOnDropdown = false Dropdown 要打開 Dropdown 不要打開

可以從上表看出,只有在 isOnMenu 和 isOnDropdown 都為 false 的時候,
Dropdown 才不需要被打開。

接著就是如何正確地得到 isOnMenu 和 isOnDropdown 的值。
以 isOnMenu 為例 (isOnDropdown 同理),
方式就是用 RxJS 偵測滑鼠對於 Menu 的
mouseenter 和 mouseleave 事件,將事件 map 成我們要的資訊。

例如 map 成一個物件 Object,
其中存了 isOnMenu 的布林值
和一些其他資訊 (例如在有多個 Menu 的情況,滑鼠是 hover 了哪個 Menu 的訊息),
並且把兩個事件流結合 (Merge) 起來,取最後得到的事件 (即 Menu 的最後狀態)。

這樣我們就可以根據最後得到的 isOnMenu 和 isOnDropdown 狀態 (用 CombineLatest 來 combine isOnMenu 和 isOnDropdown 事件流) 來決定是否要開關 Dropdown 了。

以下直接以實作例子來舉例,
先看在 jsFiddle 上的成品如下:


情境為畫面上有兩個 (或多個) Menu 及各所屬的 Dropdown,

Html:
<div class="wrapper">
  <div class="menu" targetDropdown="dropdown1">
    Menu1
    <div class="menu_child">
      Menu1 Child
    </div>
  </div>
  <div class="menu" targetDropdown="dropdown2">
    Menu2
    <div class="menu_child">
      Menu2 Child
    </div>
  </div>
  
  <div class="dropdown" id="dropdown1">
    Dropdown1
    <div class="dropdown_child">
      Dropdown1 Child
    </div>
  </div>
  <div class="dropdown" id="dropdown2">
    Dropdown2
    <div class="dropdown_child">
      Dropdown2 Child
    </div>
  </div>
</div>


CSS:
.wrapper {
  width: 500px;
  margin: 0 auto;
}

.menu {
  background-color: yellow;
  display: inline-block;
  width: 45%;
  margin: 0;
}

.menu_child {
  background-color: #00ffd0;
  width: 30%;
  margin: 0 auto;
}

.dropdown {
  background-color: green;
  width: 80%;
  height: 0px;
  overflow: hidden;
  position: absolute;
}

.dropdown_child {
  background-color: #bc0eda;
  width: 300px;
  margin: 0 auto;
}

Javascript 程式碼如下,已將說明寫在程式碼之中:
//取得 Menu 及 Dropdown 的 DOM
var $menu = $(".menu");
var $dropdown = $(".dropdown");

// 偵測 Menu 及 Dropdown 的 mouseenter 和 mouseleave 事件
var $menuMouseLeaveEvent = Rx.Observable.fromEvent($menu, "mouseleave");
var $dropdownMouseLeaveEvent = Rx.Observable.fromEvent($dropdown, "mouseleave");
var $menuMouseEnterEvent = Rx.Observable.fromEvent($menu, "mouseenter");
var $dropdownMouseEnterEvent = Rx.Observable.fromEvent($dropdown, "mouseenter");

//將 Menu 的 mouseenter 和 mouseleave 事件轉成對應的資訊物件 
//(這裡儲存了滑鼠有無在 Menu 上面、滑鼠滑進 (或滑出) 了哪個 Menu DOM 的資訊),
//並將 mouseenter 和 mouseleave 事件 merge 起來,只要看最後的狀態就好
var isMouseOnMenuEvent = Rx.Observable.merge(
  $menuMouseEnterEvent.map(function(e) {
    return {
    	isHovered : true,
      hoveredDom : e.currentTarget
    };
  }),
  $menuMouseLeaveEvent.map(function(e) {
    return {
    	isHovered : false,
      hoveredDom : e.currentTarget
    };
  }));

//將 Dropdown 的 mouseenter 和 mouseleave 事件轉成對應的資訊物件 
//(這裡儲存了滑鼠有無在 Dropdown 上面、滑鼠滑進 (或滑出) 了哪個 Dropdown DOM 的資訊)
//並將 mouseenter 和 mouseleave 事件 merge 起來,只要看最後的狀態就好
var isMouseOnDropDownEvent = Rx.Observable.merge(
  $dropdownMouseEnterEvent.map(function() {
    return {
    	isHovered : true
    };
  }),
  $dropdownMouseLeaveEvent.map(function() {
    return {
    	isHovered :false
    };
  })).startWith({ 
    // 因為 RxJS 的 combineLatest() 不能 combine NULL 的事件 (會出錯),
    // 而一開始 Dropdown 沒有被打開時,不會有 Dropdown 的 mouseenter 和 mouseleave 事件,
    // 所以在這邊用 startWith 來手動增加一個 event 在最前面
  	isHovered :false
	});

// 將 isMouseOnMenuEvent 和 isMouseOnDropDownEvent 兩個事件流 Combine 在一起,
// 只看各自的最後一個狀態。
// 因為可能存在在短時間中,事件還沒有被觸發的情況,在這之中的狀態會不準而不對,
// 例如: 
// 滑鼠滑出了 Menu,要準備滑進 Dropdown 但還沒滑進的這一段時間中,
// 此時 isOnMenu = false,但 isDropdown 也等於 false,這時用這組狀態判斷 Dropdown 的開關就會不正確,
// 所以在這使用了 debounceTime() 來延長事件偵測的時間,例如滑鼠移出 Menu 後,等 100 毫秒看有沒有其他事件再決定送出事件
var mouseHoverStatus = Rx.Observable.combineLatest(isMouseOnMenuEvent, isMouseOnDropDownEvent)
                        .debounceTime(100);

// 在註冊事件 (subscribe) 裡判斷狀態,決定是否要開關 Dropdown、和要開關哪個 Dropdown
mouseHoverStatus.subscribe(function(e){
	if (e[0].isHovered || e[1].isHovered){
    var targetDropdown = $("#" + $(e[0].hoveredDom).attr("targetDropdown"));
    
    $dropdown.not(targetDropdown).stop(true).animate({
    	height: 0
    });
    
  	targetDropdown.stop(true)
    .animate({
    	height: 300
    });
  }else {
  	$dropdown.stop(true).animate({
    	height: 0
    });
  }
});



參考資料:

  1. 30 天精通 RxJS (01):認識 RxJS
  2. Element: mouseenter event
  3. Build dropdown list with RxJS

2019年7月18日 星期四

Typescript + Webpack (使用 ts-loader ) 練習

在"Typescript + Webpack (or SystemJs) 練習"這篇文章中,使用了多次指令分別去:
1. 用 npm 去 install 要的工具
2. 用 tsc 去使用 typescript 編譯
3. 用 webpack 去使用 webpack 打包程式

這次要來紀錄只使用 npm 指令,
因為 npm 的 package.json 可以設定 script 來方便的執行 command line 的指令,
而 webpack 可以用各種 loader 來處理項任務,其中 ts-loader 可以處理 typescript 的任務,
所以我們可以讓npm 去呼叫 webpack ,webpack 去呼叫 typescript 去完成任務。

這次的檔案結構跟上次差不多,如下所示:

其中,js 資料夾裡的檔案是 webpack 搭配 ts-loader 編擇 src資料夾下檔案所產生出來的,
package-lock.json不用管它,因為是npm 自已產生的。

我們先看看 Greeter.ts 和 test.ts :
Greeter.ts :
export class Greeter{
    name : string;
    constructor(name :string){
        this.name = name;
    }
    greet(){
        console.log("Hello, HIHI, " + this.name);
        alert("Hello, HIHI, " + this.name);
    }
}

test.ts :
import {Greeter} from "./Greeter";
 
let name : string = "Hugo";
let greeter : Greeter = new Greeter(name);
greeter.greet();

可以注意的是,在 test.ts 中 import 的Greeter可以只有檔名而沒有副檔名,因為在webpack.config.js中,有設定resolve {extensions : ['.js', '.ts']} 的設定。
接著我們來看看 npm的package.json、tyscript的tsconfig.json和webpack的webpack.config.js
三個 config 檔的內容:


package.json :
{
  "name": "webpacktypescripttest",
  "scripts": {
    "devEnvBuild": "npm install --save-dev typescript ts-loader webpack webpack-cli",
    "deploy": "webpack"
  },
  "devDependencies": {
    "ts-loader": "^6.0.4",
    "typescript": "^3.5.3",
    "webpack": "^4.36.1",
    "webpack-cli": "^3.3.6"
  }
}

tsconfig.json :
{
  "compilerOptions": {
    "target": "es5",
    "module": "commonjs",
    "sourceMap": true,
    // "outDir": "./dist", //不需要
    "strict": true,
    "esModuleInterop": true    
  }
}

webpack.config.js :
var path = require('path');
module.exports = {
    entry: "./src/test.ts",
    output: {
        path: __dirname + "/js",
        filename: "test.js"
    },
    watch: false,
    devtool : 'source-map',
    mode : "production",
    resolve : {
     extensions : ['.js', '.ts']
    },
    module: {
     rules: [
       { 
        test: /\.ts$/, 
        use: 'ts-loader',
        include: path.resolve(__dirname, 'src')
       }
     ]
 }
}

可以注意到,在 tsconfig.json 中,我們不需要寫 outDir 來指定 typescript 的輸出位置,
其實就算指定了,經過 webpack 用 ts-loader 來執行 typescript 時,
也不會在 outDir 指的地方出現輸出的檔案。

先假設整個環境除了 npm 以外都沒建置完成,即 webpack, typescript, ts-loader.... 等
先用 command line 視窗到 webpackTypescriptTest 資料夾的路徑,
執行指令:
npm run devEnvBuild
就可以執行已經寫在 package.json 中,script 裡,devEnvBuild的指令,等同於執行
npm install --save-dev typescript ts-loader webpack webpack-cli
接著再執行
npm run deploy
就可以發現在 js 資料夾中,檔案被產生出來了,其中已經包括了 typescript的編擇和 webpack 的打包
可以注意到的是,在 webpack.config.js 中 ,
我們用了 module 設定了 ts-loader 來為 ts 檔在打包前進行了 typescript 的編譯。

最後我們在 index.html 中就可以使用 src/test.js 了 :

index.html :

 <head>
  <script src="js/test.js"></script>
 </head>
 <body> 
 </body>
</html>

原始碼下載:
webpackTypescriptTest.7z

參考資料:
  1.  TypeScript | webpack
  2. TYPESCRIPT合成WEBPACK中

2019年6月4日 星期二

取得Youtube及Vimeo影片的縮圖


Youtube:
各種尺寸解析度的縮圖API (XXX為影片ID):
https://img.youtube.com/vi/XXX/0.jpg
https://img.youtube.com/vi/XXX/1.jpg
https://img.youtube.com/vi/XXX/2.jpg
https://img.youtube.com/vi/XXX/3.jpg
https://img.youtube.com/vi/XXX/default.jpg
https://img.youtube.com/vi/XXX/hqdefault.jpg
https://img.youtube.com/vi/XXX/mqdefault.jpg
https://img.youtube.com/vi/XXX/sddefault.jpg
https://img.youtube.com/vi/XXX/maxresdefault.jpg

回應都為縮圖的網址
--------------------------------------------------------------------------------------------------

Vimeo:
API為 (XXX為影片ID)
http://vimeo.com/api/v2/video/XXX.json

例如:
http://vimeo.com/api/v2/video/336638047.json
回應為

{
    "id": 336638047,
    "title": "Machine Learning",
    "description": "Commissioned piece <br />\nDirector's cut<br />\n<br />\nStarring Thi-Mai Nguyen<br />\n<br />\n<br />\n<br />\nDir: Luke White<br />\nProd: Lauren Tyson<br />\nDOP: Giuseppe Favale<br />\nEditor: Brendan Jenkins<br />\nSound & Music: Mutant Jukebox<br />\nPost: FreeFolk<br />\nArt Dir: James Hamilton",
    "url": "https://vimeo.com/336638047",
    "upload_date": "2019-05-16 13:33:33",
    "thumbnail_small": "http://i.vimeocdn.com/video/783713149_100x75.jpg",
    "thumbnail_medium": "http://i.vimeocdn.com/video/783713149_200x150.jpg",
    "thumbnail_large": "http://i.vimeocdn.com/video/783713149_640.jpg",
    "user_id": 581986,
    "user_name": "Luke White",
    "user_url": "https://vimeo.com/lukewhite",
    "user_portrait_small": "http://i.vimeocdn.com/portrait/31879108_30x30",
    "user_portrait_medium": "http://i.vimeocdn.com/portrait/31879108_75x75",
    "user_portrait_large": "http://i.vimeocdn.com/portrait/31879108_100x100",
    "user_portrait_huge": "http://i.vimeocdn.com/portrait/31879108_300x300",
    "stats_number_of_likes": 403,
    "stats_number_of_plays": 8057,
    "stats_number_of_comments": 20,
    "duration": 110,
    "width": 1920,
    "height": 1080,
    "tags": "dance, ai, robot, artificial intelligence, choreography, experiment, machine learning, mountain, TSO, elbows, technology, computer, limits, dead, falling, woman, playing",
    "embed_privacy": "anywhere"
}

其中的
thumbnail_small
thumbnail_medium
thumbnail_large
就是不同尺寸大小的縮圖網址

參考資料:

  1. How do I get a YouTube video thumbnail from the YouTube API?
  2. Get img thumbnails from Vimeo?

2019年6月3日 星期一

Javascript Canvas - 物理球碰撞 (含重力加速度)

Javascript Canvas - 物理球碰撞 (含重力加速度) 的分享
其中用到了二維非彈性碰撞物理公式
假設每個球的質量都一樣,
設置了牆壁和球的恢復係數 (coefficient of restitution) ,
在空白處按滑鼠可以產生出一個新的球

https://jsfiddle.net/hugogo7646/ep4x608d/



html:
<canvas></canvas>
==================
CSS:
canvas {
  display: block;
  width: 100%;
  height: 100%;
  border: 1px solid blue;
}
==================
Javascript:
//Class definition - Start
function Ball(x, y) {
  this.x = x;
  this.y = y;
  this.r = 10;
  this.vx = 0; //Math.random(10);
  this.vy = 0; //Math.random(10);
  this.ax = 0;
  this.ay = g;
  this.color_R = Math.round(Math.random() * 255);
  this.color_G = Math.round(Math.random() * 255);
  this.color_B = Math.round(Math.random() * 255);
}
Ball.prototype = {
  draw: function() {
    this.vx += this.ax;
    this.vy += this.ay;     
    this.x += this.vx;
    this.y += this.vy;

//if ball hit with border
    if (isTopWallCollision(this)) {
    this.y = this.r;
    this.vy *= -1 * restitutionCoefficient_of_border
    }
    if (isBottomWallCollision(this)) {
    this.y = canvas.height - this.r;
    this.vy *= -1 * restitutionCoefficient_of_border
    }
    if (isLeftWallCollision(this)) {
    this.x = this.r;
    this.vx *= -1 * restitutionCoefficient_of_border
    }
    if (isRightWallCollision(this)) {
    this.x = canvas.width - this.r;
    this.vx *= -1 * restitutionCoefficient_of_border
    }
 
    context.beginPath();
    context.arc(this.x, this.y, this.r, 0, Math.PI * 2, false);
    context.fillStyle = 'rgba(' + this.color_R + ', ' + this.color_G + ', ' + this.color_B + ', 1)';
    context.fill()
  }
}
//Class definition - End

//Config - Start //
var ballCount = 30;
var ballList = [];
var g = 0.1;
var restitutionCoefficient_of_border = 0.9;
var restitutionCoefficient_of_ball = 0.9;
//Config - End //

var canvas = document.querySelectorAll("canvas")[0];
var context = canvas.getContext("2d");

//Create balls
for (var i = 0; i < ballCount; i++) {
  var isNewBallCollision = false;
  var newBall;
  do {
    newBall = new Ball(Math.random() * canvas.width, Math.random() * canvas.height);
    isNewBallCollision = isWallCollision(newBall);
    if (isNewBallCollision){
    continue;
    }
    for (var j = 0; j < ballList.length; j++) {
      isNewBallCollision = isBallCollision(ballList[j], newBall);
      if (isNewBallCollision) {
        break;
      }
    }
  } while (isNewBallCollision);

  ballList.push(newBall);
}

draw();

canvas.addEventListener('click', function(e) {
  var x = event.clientX;
  var y = event.clientY;
  var rect = this.getBoundingClientRect();
  //Recalculate mouse offsets to relative offsets
  x -= rect.left;
  y -= rect.top;
  //Also recalculate offsets of canvas is stretched
  //changes coordinates by ratio
  x *= this.width / this.clientWidth;
  y *= this.height / this.clientHeight;
  ballList.push(new Ball(x, y));
})

function draw() {
  context.clearRect(0, 0, canvas.width, canvas.height);

  for (var i = 0; i < ballList.length; i++) {
    ballList[i].draw();
    for (var j = i + 1; j < ballList.length; j++) {
      if (isBallCollision(ballList[i], ballList[j])) {
        ballCollision(ballList[i], ballList[j]);
      }
    }
  }
  requestAnimationFrame(draw);
}

function isBallCollision(ball1, ball2) {
  return Math.pow(ball1.x - ball2.x, 2) + Math.pow(ball1.y - ball2.y, 2) <= Math.pow(ball1.r + ball2.r, 2)
}
//Wall Collision functions - Start
function isWallCollision(ball) {
  return isTopWallCollision(ball) || isBottomWallCollision(ball) || isLeftWallCollision(ball) || isRightWallCollision(ball);
}

function isTopWallCollision(ball) {
  return (ball.y - ball.r) <= 0;
}

function isBottomWallCollision(ball) {
  return (ball.y + ball.r) >= canvas.height;
}

function isLeftWallCollision(ball) {
  return (ball.x - ball.r) <= 0;
}

function isRightWallCollision(ball) {
  return (ball.x + ball.r) >= canvas.width;
}
//Wall Collision functions - End

function ballCollision(ball1, ball2) {
  var clooisionVectorLength = Math.sqrt(Math.pow((ball1.x - ball2.x), 2) + Math.pow((ball1.y - ball2.y), 2));
  var collisionX = (ball1.x - ball2.x) / clooisionVectorLength;
  var collisionY = (ball1.y - ball2.y) / clooisionVectorLength;

  var collision_ball1_x = Math.pow(collisionX, 2) * ball1.vx + collisionX * collisionY * ball1.vy;
  var collision_ball1_y = collisionX * collisionY * ball1.vx + Math.pow(collisionY, 2) * ball1.vy;

  var collision_ball2_x = Math.pow(collisionX, 2) * ball2.vx + collisionX * collisionY * ball2.vy;
  var collision_ball2_y = collisionX * collisionY * ball2.vx + Math.pow(collisionY, 2) * ball2.vy;

  ball1.vx = collision_ball2_x + (ball1.vx - collision_ball1_x);
  ball1.vy = collision_ball2_y + (ball1.vy - collision_ball1_y);
  ball2.vx = collision_ball1_x + (ball2.vx - collision_ball2_x);
  ball2.vy = collision_ball1_y + (ball2.vy - collision_ball2_y);

  ball1.vx *= restitutionCoefficient_of_ball;
  ball1.vy *= restitutionCoefficient_of_ball;
  ball2.vx *= restitutionCoefficient_of_ball;
  ball2.vy *= restitutionCoefficient_of_ball;

  ball1.x += collisionX * (ball1.r + ball2.r - clooisionVectorLength);
  ball1.y += collisionY * (ball1.r + ball2.r - clooisionVectorLength);
  ball2.x += -1 * collisionX * (ball1.r + ball2.r - clooisionVectorLength);
  ball2.y += -1 * collisionY * (ball1.r + ball2.r - clooisionVectorLength);
}

正規表示法的 Positive/Negative Lookhead 和 Positive/Negative Lookbehind

Positive Lookhead :
a(?=b)
後面跟著 "b" 才match "a" ("b"不在match的結果裡)
例如:
可以match:ab, abc, bab
不會match:ac, ade, bad

Negative Lookhead :
a(?!b)
後面不跟著 "b" 才match "a" ("b"不在match的結果裡)
例如:
可以match:ac, ade, bad
不會match:ab, abc, bab


Positive Lookbehind:
(?<=a)b
前面跟著 "a" 才match "b" ("a"不在match的結果裡)
例如:
可以match:ab, abc, bab
不會match:xb, xbc, xbb


Negative Lookbehind:
(?<!a)b
前面不跟著 "a" 才match "b" ("a"不在match的結果裡)
例如:
可以match:xb, xbc, xbb
不會match:ab, abc, bab

應用範例:
找出單獨的 "a",不要match連續的 "a" (ex: aaa )
(?<!a)a(?!a)
解釋:
前面不跟著 "a", 才 match a(?!a),
後面不跟著 "a", 才 match a

Note:
以下網站不支援  Positive/Negative  Lookbehind
Regexper
DebuggexBeta

可以用 regular expressions 101 網站測試。
Javascript, Java 等程式語言也有支援  Positive/Negative  Lookbehind

參考資料:
  1. Lookahead and Lookbehind Zero-Length Assertions

2019年5月2日 星期四

Javascript - Canvas 文字粒子特效

今天要來演示如何利用Javascript的canvas來制作文字子特效,
成品效果就像下面這樣,只要在<input>裡面打字,下面的canvas就會出現相應的粒子特效:
其他別人做的更厲害的版本可以參考這裡:
Text particle

不過不管是簡單還是複雜酷炫的版本,其基本原理都相同,就是利用了
Javascript的canvas,繪制了文字之後,擷取繪制了文字的canvas各pixel顏色和透明度資訊 (RGBA),進行處理後再依我們想要的效果再次重繪。

下面就來進行說明:
  1. 建立畫布
    在頁面中建立一個<canvas>用來當做畫布
    <canvas width="500" hiehgt="500"></canvas>
  2. 取得canvas物件後先繪制文字(text)上去
    var canvas = document.querySelectorAll("canvas")[0];
    var context = canvas.getContext("2d");
    context.textAlign = "center";
    context.font = "100px arial";
    context.fillText(text, 200, 100);
  3. 取得canvas的RGBA資訊
     var imageData = context.getImageData(0, 0, canvas.width, canvas.height).data;
    其中data是一個類陣列物件,每四個一組分別代表canvas各pixel位置 Red, Green, Blue, Alpha 的值 (0~255),
    例如 {R1, G1, B1, A1, R2, G2, B2 ,A2, R3, G3, B3, A3, ..........}
    以此類推,pixel位置由canvas的由左至右,由上至下。
    有了RGBA資訊後,我們就能來進行自已想要的粒子化處理了
  4. 粒子化處理
    再處理前,先把canvas 清空
    context.clearRect(0, 0, canvas.width, canvas.height);
    接著對data取得每四組的RGBA值,其中我們不想拿取每個pixel的RGBA,
    只想取得間隔為5 pixel (sampleRate ) 的 RGBA資訊,
    並且檢查透明度 (Alpha),如果大於0的話,我們就用canvas繪制一個圓在相應的位置上。

    如此一來,就會呈現原來的文字被間隔取樣重繪成圓圈的子效果了。

    var sampleRate = 5;
      for (var j = 0; j < canvas.height; j += sampleRate) {
        for (var i = 0; i < canvas.width; i += sampleRate) {
          //Get RGBA data
          var red = imageData[(i + j * canvas.width) * 4];
          var green = imageData[(i + j * canvas.width) * 4 + 1];
          var blue = imageData[(i + j * canvas.width) * 4 + 2];
          var alpha = imageData[(i + j * canvas.width) * 4 + 3];
          if (alpha > 0) { //If alpha > 0, draw circle
            context.beginPath();
            //context.strokeStyle = "rgba(" + red + ", " + green + ", " + blue + ", " + alpha + ")";
            context.fillStyle = "rgba(" + red + ", " + green + ", " + blue + ", " + alpha + ")";
            context.arc(i, j, 2, 0, 2 * Math.PI);
            context.fill();
          }
        }
      }
下面是完整的程式碼 :
Html :
<div>Please insert text here to see partical special effect.</div>
<div><input type="text" /></div>
<div><canvas width="500" hiehgt="500"></canvas></div>

Javascript :
//Text to draw
var sampleText = "👚👕 H";
drawParticleText(sampleText);

document.querySelectorAll("input")[0].addEventListener("input", function(event) {
 drawParticleText( this.value ? this.value : sampleText);
});

function drawParticleText(text) {
  var x = 200;
  var y = 100;
  var canvas = document.querySelectorAll("canvas")[0];
  var context = canvas.getContext("2d");
  
  //clear canvas first
  context.clearRect(0, 0, canvas.width, canvas.height);
  
  //Draw text first
  context.textAlign = "center";
  context.font = "100px arial";
  context.fillText(text, 200, 100);
  //Get canvas data (RGB and alpha),
  //Data in imageData : [(r, g, b, a) of 1st place, (r, g, b, a) of 2nd place......]
  //from left to right, from top to bottom
  var imageData = context.getImageData(0, 0, canvas.width, canvas.height).data;
  //Clear drawing
  context.clearRect(0, 0, canvas.width, canvas.height);

  //Sampling rate
  var sampleRate = 5;
  for (var j = 0; j < canvas.height; j += sampleRate) {
    for (var i = 0; i < canvas.width; i += sampleRate) {
      //Get RGBA data
      var red = imageData[(i + j * canvas.width) * 4];
      var green = imageData[(i + j * canvas.width) * 4 + 1];
      var blue = imageData[(i + j * canvas.width) * 4 + 2];
      var alpha = imageData[(i + j * canvas.width) * 4 + 3];
      if (alpha > 0) { //If alpha > 0, draw circle
        context.beginPath();
        //context.strokeStyle = "rgba(" + red + ", " + green + ", " + blue + ", " + alpha + ")";
        context.fillStyle = "rgba(" + red + ", " + green + ", " + blue + ", " + alpha + ")";
        context.arc(i, j, 2, 0, 2 * Math.PI);
        context.fill();
      }
    }
  }
}

CSS (加框只是方便觀察):
canvas {
  border: 1px solid blue;
}

參考資料:

  1. 随便谈谈用canvas来实现文字图片粒子化
  2. 《每周一点canvas动画》—— 文字粒子
  3. Text particle