2015年9月30日星期三

UNBLUR BACKGROUND (PART 2)

Start to draw

The basic idea is to draw lines with blurring effect from the last coordinate to the current coordinate whenever mouse moved and make it fade over time to zero. Here I want to draw line with brash effect and blur effect. And then to make it fade, I simply manipulate the opacity a bit. So let's see what comes out.

var ctx = $('canvas')[0].getContext('2d');
ctx.lineCap = "round";
ctx.shadowColor = "#000";
ctx.lineWidth = 30;
ctx.shadowBlur = 30;
for(var i=0; i<=10;i++){
 ctx.strokeStyle = "rgba(0,0,0,"+(1-i/10)+")";
 ctx.beginPath();
 ctx.moveTo(i*70+50, 20);
 ctx.lineTo(i*70+50, 100);
 ctx.stroke();
}

Well, to draw lines with a single color is easy. What about copy some photo parts and fill them to those lines? There are two options, compositing and clipping. Initially I thought clipping was more promising because it was simpler and more straightforward. Soon after that I began to have problems with implementing blurring and fading effects. And most importantly the browser started to lag when those was added. So I decided to try the other approach.

Global Composite Operation

By definition, the canvas 2D rendering context globalCompositeOperation property of the Canvas 2D API sets the type of compositing operation to apply when drawing new shapes. And the type "destination-in" keeps the existing canvas content where both the new shape and existing canvas content overlap. Everything else is made transparent. So I can draw a line and paste the image to achieve the goal. See the effect.

But wait, it can only draw something in the overlap zone. to make trace of mouse move I'm definitely going to draw lots of lines not just in a narrowing area. Ideally, it would be great I can draw whole trace in somewhere else and paste it into canvas. It took me a while to figure out that it could actually copy the drawing from one canvas and paste it to another one. So the idea came out. Draw the whole trace in a temporary canvas and then paste the drawing to the main canvas. And I found out that this temp canvas doesn't have to be inside the body. It just needs to be instantiate properly with exactly same width and height.

var painter = $('<canvas>').attr('width', $(window).width()).attr('height', $(window).height())[0], 
    pctx = painter.getContext('2d');

pctx.lineCap = "round";
pctx.shadowColor = "#000";
pctx.lineWidth = 30;
pctx.shadowBlur = 30;

for(var i = 0; i <= 10; i++){
 pctx.strokeStyle = "rgba(0,0,0," + (1 - i / 10) + ")";
 pctx.beginPath();
 pctx.moveTo(i * 70 + 50, 20);
 pctx.lineTo(i * 70 + 50, 100);
 pctx.stroke();
}

var ctx = $('canvas')[0].getContext('2d'), img= $('#imgtarget')[0];

ctx.drawImage(img, 0, 0, img.width, img.height);
ctx.globalCompositeOperation = "destination-in";
ctx.drawImage(painter, 0, 0);
ctx.globalCompositeOperation = "source-over";

So I've got the method to draw. The next step would be to use it to handle the mousemove event. But handling the event was far more complicated than I thought. I will break it down next time.



UNBLUR BACKGROUND (PART 1)

UNBLUR BACKGROUND (PART 3)

2015年9月27日星期日

UNBLUR BACKGROUND (PART 1)

Recently I've found a very interesting animation effect from this website, which can unblur the blurred background and leave a disappearing track on the screen. I want to make one myself but I couldn't find any library can do something like that. So I tried to build one from scratch. So the basic idea is to use a blurred image as background image and use HTML5 <canvas> tag to draw a small dot with the original image on the location whenever mouse move.

Prepare background

Frist thing I will need two same images, one blur and one not. And then set up the blurred image as background and make it not repeat, cover the whole screen and not moving with scroll. Here background-attachment:fixed makes it fixed with regard to the viewport, and background-size:cover makes it always cover the whole screen and keep the image aspect ratio.

body{
  background: url('images/background-blur.jpg') no-repeat 0 0 fixed;
  background-size: cover;
}

Prepare image

And then set up the image to draw. This image will basically need to be kept invisible. But I cannot use display:none here because I'm going to need to read its width and height in the future. Most importantly, the image needs to be adjust its width and height to be identical with the background.

<div style="position:absolute;width:0;height:0;overflow:hidden;">
  <img src="background.jpg" alt="" / >
</div>
<script>
  var img = $('img')[0];
  var windowHeight = $(window).height(), windowWidth = $(window).width(), windowRate = windowWidth/windowHeight;
  var imgHeight = img.naturalHeight, imgWidth = img.naturalWidth, imgRate = imgWidth/imgHeight;

  if(windowRate>=imgRate){
    $(img).attr('height', (imgHeight/imgWidth*windowWidth)).attr('width', windowWidth);
  }else{
    $(img).attr('width', (imgWidth/imgHeight*windowHeight)).attr('height', windowHeight);
  }
</script>

Prepare canvas

I also need create a <canvas> element, make it cover the whole screen and set the position to fixed. Here I got a problem. It is a bit difficult to set height. And I have to use a piece of javascript to make it work properly.

<script>
  $('body').append(
    $('<canvas style="position:fixed;top:0;left:0;z-index:-1;"></canvas>')
   .attr('width', $(window).width()).attr('height', $(window).height())
  );
</script>

I'm going to have to split this article to several parts. Next time I'm going to explain how it works between image and canvas.



UNBLUR BACKGROUND (PART 2)

UNBLUR BACKGROUND (PART 3)

2015年9月23日星期三

CSS: hiding text when it's overflow

Use the following CSS to hide the text. This piece of CSS will hide the exceeded part and replace with some "..." instead.

.nowrap{
  white-space: nowrap;
  overflow:hidden;
  text-overflow:ellipsis;
}

This will work with both fixed width and percentage width. See the sample below:

The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.
The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.

But sometime it doesn't work. I encountered several problems when using it. So I list the problems here just in case somebody got the problem.

  • "text-overflow" will only apply to its PLAIN text child elements.
    The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.
    The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.
  • "text-overflow" will not apply to "table" element.
    The text-overflow property determines how overflowed content that is not displayed is signaled to users. It can be clipped, display an ellipsis , or display a custom string.

2015年9月21日星期一

Nested Table using AngularJS and Bootstrap

Last time I created a bootstrap like nested table. Now I'm going to show you that how it's going to work with AngularJS. Let's say I got some JSON data from a RESTful service. BTW, AngularJS doesn't really need JQuery. But I simply attached the JQuery and use it whenever I need it.

$scope.data =
[{
  name: 'test name',
  createDate: '2013-05-10T15:04:44.593',
  description: "Don't use data attributes from multiple plugins on the same element.",
  amount: '$20',
  quantity: '2',
  children: [{
    name: 'test name',
    createDate: '2013-05-10T15:04:44.593',
    description: "Don't use data attributes from multiple plugins on the same element.",
    amount: '$20',
    quantity: '2'
  },
  {
    name: 'test name',
    createDate: '2013-05-10T15:04:44.593',
    description: "Don't use data attributes from multiple plugins on the same element.",
    amount: '$20',
    quantity: '2'
  }]
},
{
  name: 'test name',
  createDate: '2013-05-10T15:04:44.593',
  description: "Don't use data attributes from multiple plugins on the same element.",
  amount: '$20',
  quantity: '2',
  children: []
}];

And the angular way of doing this would be basically like this.

<div class="table">
  <div class="header">
    <div class="line">
      <div ng-repeat="(key, value) in data[0]" >{{key}}</div>
    </div>
  </div>
  <div class="body">
    <div class="line" ng-repeat="row in data" >
      <div class="main">
     <div ng-show="row.children && row.children.length" ><button.../div>
        <div ng-repeat="(key, value) in row" >{{value}}</div>
      </div>
      <div class="sub">
        <div class="table">
          ...
        </div>
      </div>
    </div>
  </div>
</div>

There are two major problems about this solution. It needs two definitions for each data in the array and columns to show. For each data, there needs an object to store some status, functions and maybe the data also. For columns, there could also store some definitions such as width, formatter. Fortunately, Angular also developed a lot more advanced solution called Angular UI-Grid that provides some pretty good concepts I can use for my own. So I took a bit of its concepts and implemented them to my template.

The column concept

So in the controller, I created an option object in which I put column definitions. But unlike UI-Grid, I put the data in the scope and passed them separately to the template instead of just one option object. Because the nested table will need to bind with child objects which is different from its parent.

var app = angular.module('demoApp', ['ngRoute']);
app.controller('demoController', ['$scope', '$timeout', function ($scope, $timeout) {
  $scope.options = {
    colDefs: [
      { name: "name", displayName: "Name", width: '15%' },
      { name: "createDate", displayName: "Create Date", width: '15%', cellFilter: "date:'yyyy-MM-dd'" },
      { name: "description", displayName: "Description", width: '35%' },
      { name: "quantity", displayName: "Quantity", width: '10%' },
      { name: "amount", displayName: "Total", width: '10%' }
    ]
  };
  
  $timeout(function(){
    $scope.options.data = [...];
  });
}]);

The table template directives

Then I will need to create four directives for table, row, header and column. Here I'm going to explain a bit about the row directive first. ng-repeat creates a child scope and store the iteration in it. that gives the perfect place to store the row definitions. So in table directive I looped the data array and created a separate object for each element. And then here came a problem. Since I created an array of row objects, ng-repeat will not be able to notice whether there is something change in the data array. So I will need to watch the data manually, and see if there is any data insertion and removal. The header and column directives are relative simple. They just needs to read the column definitions from the option, display the value or name in a proper format.

app.directive('uiTable', ['$rootScope', '$compile', '$parse', function ($rootScope, $compile, $parse) {
  return {
    restrict: 'A',
    scope: {
      options: "=uiTable",
      items: "=uiTableModel"
    },
    controller: ["$scope", "$element", "$attrs", function ($scope, $element, $attrs) {
      //template
      var template =  "<div class=\"table\">\n" +
              "  <div class=\"header\">\n" +
              "    <div class=\"line\">\n" +
              "      <div ng-repeat=\"colDef in options.colDefs\" grid-header-render ></div>\n" +
              "    </div>\n" +
              "  </div>\n" +
              "  <div class=\"body\" ng-show=\"rows.length\" >\n" +
              "    <div class=\"line\" ng-class=\"{ 'open': row.isOpened }\" ng-repeat=\"row in rows\" >\n" +
              "      <div class=\"main\" grid-row-render ></div>\n" +
              "      <div class=\"sub\" data-ui-table=\"options\" data-ui-table-model=\"row.entity[options.children]\" ></div>\n" +
              "    </div>\n" +
              "  </div>\n" +
              "   <div ng-hide=\"rows.length\" >No Data</div>\n" +
              "</div>";

      var row = function (entity) {
        this.entity = entity;
      };
      row.prototype.expand = function (e) {
        e.preventDefault();
        this.isOpened = !this.isOpened;
      };

      $scope.rows = [];
      $scope.options.children = $scope.options.children || 'children';
      //watch data
      var deregFunctions = [];
      var dataWatchFunction = function () {
        var newData = $scope.items, newRows = [];
        $.each(newData, function (i, entity) {
          var oldRows = $scope.rows.filter(function (o) { return o.entity == entity; });
          if (oldRows.length) {
            newRows.push(oldRows[0]);
          } else {
            newRows.push(new row(entity));
          }
        });
        $scope.rows = newRows;
      };
      if (!angular.isString($scope.options.data)) {
        deregFunctions.push($scope.$watch(function () { return $scope.items; }, dataWatchFunction));
        deregFunctions.push($scope.$watch(function () { return $scope.items.length; }, dataWatchFunction));
      }

      var $table = $compile(template)($scope);

      if ($scope.options.cssClass) {
        $table.addClass($scope.options.cssClass);
      }

      //Rendering template.
      $element.html('').append($table);
      //destory watch
      $scope.$on('$destroy', function () {
        deregFunctions.forEach(function (deregFn) { deregFn(); });
      });
    }]
  };
}])
.directive('gridRowRender', ['$rootScope', '$compile', '$parse', function ($rootScope, $compile, $parse) {
  return {
    restrict: 'A',
    link: function ($scope, $element, $attrs) {
      var template = $scope.options.rowTemplate || "<div ng-repeat=\"colDef in options.colDefs\" grid-col-render></div>";
      var $row = $compile(template)($scope);

      if ($scope.options.rowCssClass) {
        $row.addClass($scope.options.rowCssClass);
      }

      $element.append($row);
    }
  };
}])
.directive('gridColRender', ['$rootScope', '$compile', '$parse', function ($rootScope, $compile, $parse) {
  return {
    restrict: 'A',
    link: function ($scope, $element, $attrs) {
      var template = $scope.colDef.cellTemplate || "<div><span class=\"view\">{{row.entity[colDef.name] CELLFILTER }}</span></div>";
      template = template.replace("CELLFILTER", $scope.colDef.cellFilter ? "| " + $scope.colDef.cellFilter : "");
      var $cell = $compile(template)($scope);

      if ($scope.colDef.cellClass) {
        $cell.addClass($scope.colDef.cellClass);
      }

      if ($scope.colDef.width) {
        $cell.css('width', $scope.colDef.width);
      }

      $element.replaceWith($cell);
    }
  };
}])
.directive('gridHeaderRender', ['$rootScope', '$compile', '$parse', function ($rootScope, $compile, $parse) {
  return {
    restrict: 'A',
    link: function ($scope, $element, $attrs) {
      var template = $scope.colDef.cellHeaderTemplate || $scope.options.headerTemplate || "<div>{{ colDef.displayName || colDef.name }}</div>";

      var $row = $compile(template)($scope);

      if ($scope.colDef.headerClass) {
        $row.addClass($scope.colDef.cellClass);
      }

      if ($scope.colDef.width) {
        $row.css('width', $scope.colDef.width);
      }
        
      $element.replaceWith($row);
    }
  };
}]);

Sample

2015年9月20日星期日

Visual Studio theme for code prettifier

I put this code in here just in case somebody might want it.

.com       { color: #008000; }
.str, .tag { color: #A31515; }
.kwd, .atv { color: #0000FF; }
.typ       { color: #2B91AF; }
.lit, .atn { color: #FF0000; }
.pun, .pln { color: #000000; }
.dec       { color: #800080; }
.kwd{
  font-style:italic;
}

2015年9月16日星期三

Bootstrap Nested Table

Bootstrap Nested Table

In my recent project, I came across a problem to make bootstrap like table that can fit in for a nested table to display some sub items in each row.The problem for a normal <table> is that it doesn't provide to much flexibility for hierarchy structured data.It will need to create a separate row with a spanned column to store a nested table. So I came up with this <div> solution.

Name
Date
Description
Amount
Quantity
Test Name
2013-05-10T15:04:44.593
Don't use data attributes from multiple plugins on the same element.
$20
2
Name
Date
Description
Amount
Quantity
Test Name
2013-05-10T15:04:44.593
Don't use data attributes from multiple plugins on the same element.
$20
2
Test Name
2013-05-10T15:04:44.593
Don't use data attributes from multiple plugins on the same element.
$20
2
Test Name
2013-05-10T15:04:44.593
Don't use data attributes from multiple plugins on the same element.
$20
2

Table Template

The first thing is to create table like template which has similar table structure. And each line has two containers, "main" container for certain columns and "sub" for nested table.

<div class="table">
  <div class="header">
    <div class="line">
      headers...
    </div>
  </div>
  <div class="body">
    <div class="line">
      <div class="main">
        columns...
      </div>
      <div class="sub">
        <div class="table">
          ...
        </div>
      </div>
    </div>
  </div>
</div>

CSS

The major problem of the <div> table approach is, the column elements cannot auto align and adjust their widths. So a width value must be manually set to each element, which can be a fixed value or precentage value. For fixed value, "overflow:auto" must be set in case of the total width exceed its parent element. For precentage value, it's better to have to total value to be 100%. And also, an indent value needs to be set. It's better but not necessary to be the width of the first column.And then, to make to be consistent with Bootstrap table style and configurable for further table styles. The following styles are needed. I've also add some styles to make it to be fitting to the basic Bootstrap table style.

.header>.line>div:nth-child(1), .body>.line>.main>div:nth-child(1){
  width:5%;
}
.header>.line>div:nth-child(2), .body>.line>.main>div:nth-child(2){
  width:15%;
}
.header>.line>div:nth-child(3), .body>.line>.main>div:nth-child(3){
  width:15%;
}
.header>.line>div:nth-child(4), .body>.line>.main>div:nth-child(4){
  width:45%;
}
.header>.line>div:nth-child(5), .body>.line>.main>div:nth-child(5){
  width:10%;
}
.header>.line>div:nth-child(6), .body>.line>.main>div:nth-child(6){
  width:10%;
}
.sub{
  padding-left:5%;
}
.body>.line a{
  color:#222;
  font-size:12px;
}
.header{
  font-weight:bold;
}
.header>.line{
  border-bottom:solid 2px #AAA;
}
.header>.line>div, .body>.line>.main>div{
  float:left;
  padding:8px;
}
.header>.line:before, .body>.line>.main:before, .header>.line:after, .body>.line>.main:after{
  content:' ';
  display:table;
}
.header>.line:after, .body>.line>.main:after{
  clear:both;
}
.table>.body>.line:not(:first-child)>.main{
  border-top:solid 1px #AAA;
}
.table>.body>.line.open>.main{
  border-bottom:solid 1px #AAA;
}

Javascript

Here it needs a little javascript to expand and unexpand nested tables. In real practice, it will also need to define whether the button shows or hides.

function togglecollapse(e){
  e.preventDefault();
  $(e.currentTarget.parentNode.parentNode.parentNode).toggleClass('open');
}

Next time I will write an article to demonstrate how I implement this template with some MVC approach.