1var MAX_SWAP_IMG_SIZE = 400; 2var MAGNIFIER_WIDTH = 200; 3var MAGNIFIER_HEIGHT = 200; 4var MAGNIFIER_HALF_WIDTH = MAGNIFIER_WIDTH * 0.5; 5var MAGNIFIER_HALF_HEIGHT = MAGNIFIER_HEIGHT * 0.5; 6// TODO add support for a magnified scale factor 7var MAGNIFIER_SCALE_FACTOR = 2.0; 8 9angular.module('diff_viewer', []). 10directive('imgCompare', function() { 11 // Custom directive for comparing (3-way) images 12 return { 13 restrict: 'E', // The directive can be used as an element name 14 replace: true, // The directive replaces itself with the template 15 template: '<canvas/>', 16 scope: true, 17 link: function(scope, elm, attrs, ctrl) { 18 var image = new Image(); 19 var canvas = elm[0]; 20 var ctx = canvas.getContext('2d'); 21 22 var magnifyContent = false; 23 var maskCanvas = false; 24 25 // When the type attribute changes, load the image and then render 26 attrs.$observe('type', function(value) { 27 switch(value) { 28 case "alphaMask": 29 image.src = scope.record.differencePath; 30 maskCanvas = true; 31 break; 32 case "baseline": 33 image.src = scope.record.baselinePath; 34 magnifyContent = true; 35 break; 36 case "test": 37 image.src = scope.record.testPath; 38 magnifyContent = true; 39 break; 40 default: 41 console.log("Unknown type attribute on <img-compare>: " + value); 42 return; 43 } 44 45 image.onload = function() { 46 // compute the scaled image width/height for image and canvas 47 var divisor = 1; 48 // Make it so the maximum size of an image is MAX_SWAP_IMG_SIZE, 49 // and the images are scaled down in halves. 50 while ((image.width / divisor) > MAX_SWAP_IMG_SIZE) { 51 divisor *= 2; 52 } 53 54 scope.setImgScaleFactor(1 / divisor); 55 56 // Set canvas to correct size 57 canvas.width = image.width * scope.imgScaleFactor; 58 canvas.height = image.height * scope.imgScaleFactor; 59 60 // update the size for non-alphaMask canvas when loading baseline image 61 if (!scope.maskSizeUpdated) { 62 if (!maskCanvas) { 63 scope.updateMaskCanvasSize({width: canvas.width, height: canvas.height}); 64 } 65 scope.maskCanvasSizeUpdated(true); 66 } 67 68 // render the image onto the canvas 69 scope.renderImage(); 70 } 71 }); 72 73 // when updatedMaskSize changes, update mask canvas size. 74 scope.$watch('updatedMaskSize', function(updatedSize) { 75 if (!maskCanvas) { 76 return; 77 } 78 79 canvas.width = updatedSize.width; 80 canvas.height = updatedSize.height; 81 }); 82 83 // When the magnify attribute changes, render the magnified rect at 84 // the default zoom level. 85 scope.$watch('magnifyCenter', function(magCenter) { 86 if (!magnifyContent) { 87 return; 88 } 89 90 scope.renderImage(); 91 92 if (!magCenter) { 93 return; 94 } 95 96 var magX = magCenter.x - MAGNIFIER_HALF_WIDTH; 97 var magY = magCenter.y - MAGNIFIER_HALF_HEIGHT; 98 99 var magMaxX = canvas.width - MAGNIFIER_WIDTH; 100 var magMaxY = canvas.height - MAGNIFIER_HEIGHT; 101 102 var magRect = { x: Math.max(0, Math.min(magX, magMaxX)), 103 y: Math.max(0, Math.min(magY, magMaxY)), 104 width: MAGNIFIER_WIDTH, 105 height: MAGNIFIER_HEIGHT 106 }; 107 108 var imgRect = { x: (magCenter.x / scope.imgScaleFactor) - MAGNIFIER_HALF_WIDTH, 109 y: (magCenter.y / scope.imgScaleFactor) - MAGNIFIER_HALF_HEIGHT, 110 width: MAGNIFIER_WIDTH, 111 height: MAGNIFIER_HEIGHT 112 }; 113 114 // draw the magnified image 115 ctx.clearRect(magRect.x, magRect.y, magRect.width, magRect.height); 116 ctx.drawImage(image, imgRect.x, imgRect.y, imgRect.width, imgRect.height, 117 magRect.x, magRect.y, magRect.width, magRect.height); 118 119 // draw the outline rect 120 ctx.beginPath(); 121 ctx.rect(magRect.x, magRect.y, magRect.width, magRect.height); 122 ctx.lineWidth = 2; 123 ctx.strokeStyle = 'red'; 124 ctx.stroke(); 125 126 }); 127 128 // render the image to the canvas. This is often done every frame prior 129 // to any special effects (i.e. magnification). 130 scope.renderImage = function() { 131 ctx.clearRect(0, 0, canvas.width, canvas.height); 132 ctx.drawImage(image, 0, 0, canvas.width, canvas.height); 133 }; 134 135 // compute a rect (x,y,width,height) that represents the bounding box for 136 // the magnification effect 137 scope.computeMagnifierOutline = function(event) { 138 var scaledWidth = MAGNIFIER_WIDTH * scope.imgScaleFactor; 139 var scaledHeight = MAGNIFIER_HEIGHT * scope.imgScaleFactor; 140 return { 141 x: event.offsetX - (scaledWidth * 0.5), 142 y: event.offsetY - (scaledHeight * 0.5), 143 width: scaledWidth, 144 height: scaledHeight 145 }; 146 }; 147 148 // event handler for mouse events that triggers the magnification 149 // effect across the 3 images being compared. 150 scope.MagnifyDraw = function(event, startMagnify) { 151 if (startMagnify) { 152 scope.setMagnifierState(true); 153 } else if (!scope.magnifierOn) { 154 return; 155 } 156 157 scope.renderImage(); 158 159 // render the magnifier outline rect 160 var rect = scope.computeMagnifierOutline(event); 161 ctx.save(); 162 ctx.beginPath(); 163 ctx.rect(rect.x, rect.y, rect.width, rect.height); 164 ctx.lineWidth = 2; 165 ctx.strokeStyle = 'red'; 166 ctx.stroke(); 167 ctx.restore(); 168 169 // update scope on baseline / test that will cause them to render 170 scope.setMagnifyCenter({x: event.offsetX, y: event.offsetY}); 171 }; 172 173 // event handler that triggers the end of the magnification effect and 174 // resets all the canvases to their original state. 175 scope.MagnifyEnd = function(event) { 176 scope.renderImage(); 177 // update scope on baseline / test that will cause them to render 178 scope.setMagnifierState(false); 179 scope.setMagnifyCenter(undefined); 180 }; 181 } 182 }; 183}); 184 185function ImageController($scope, $http, $location, $timeout, $parse) { 186 $scope.imgScaleFactor = 1.0; 187 $scope.magnifierOn = false; 188 $scope.magnifyCenter = undefined; 189 $scope.updatedMaskSize = undefined; 190 $scope.maskSizeUpdated = false; 191 192 $scope.setImgScaleFactor = function(scaleFactor) { 193 $scope.imgScaleFactor = scaleFactor; 194 } 195 196 $scope.setMagnifierState = function(magnifierOn) { 197 $scope.magnifierOn = magnifierOn; 198 } 199 200 $scope.setMagnifyCenter = function(magnifyCenter) { 201 $scope.magnifyCenter = magnifyCenter; 202 } 203 204 $scope.updateMaskCanvasSize = function(updatedSize) { 205 $scope.updatedMaskSize = updatedSize; 206 } 207 208 $scope.maskCanvasSizeUpdated = function(flag) { 209 $scope.maskSizeUpdated = flag; 210 } 211} 212 213function DiffListController($scope, $http, $location, $timeout, $parse) { 214 // Detect if we are running the web server version of the viewer. If so, we set a flag and 215 // enable some extra functionality of the website for rebaselining. 216 $scope.isDynamic = ($location.protocol() == "http" || $location.protocol() == "https"); 217 218 // Label each kind of differ for the sort buttons. 219 $scope.differs = [ 220 { 221 "title": "Different Pixels" 222 }, 223 { 224 "title": "Perceptual Difference" 225 } 226 ]; 227 228 // Puts the records within AngularJS scope 229 $scope.records = SkPDiffRecords.records; 230 231 // Keep track of the index of the last record to change so that shift clicking knows what range 232 // of records to apply the action to. 233 $scope.lastSelectedIndex = undefined; 234 235 // Indicates which diff metric is used for sorting 236 $scope.sortIndex = 1; 237 238 // Called by the sort buttons to adjust the metric used for sorting 239 $scope.setSortIndex = function(idx) { 240 $scope.sortIndex = idx; 241 242 // Because the index of things has most likely changed, the ranges of shift clicking no 243 // longer make sense from the user's point of view. We reset it to avoid confusion. 244 $scope.lastSelectedIndex = undefined; 245 }; 246 247 // A predicate for pulling out the number used for sorting 248 $scope.sortingDiffer = function(record) { 249 return record.diffs[$scope.sortIndex].result; 250 }; 251 252 // Flash status indicator on the page, and then remove it so the style can potentially be 253 // reapplied later. 254 $scope.flashStatus = function(success) { 255 var flashStyle = success ? "success-flash" : "failure-flash"; 256 var flashDurationMillis = success ? 500 : 800; 257 258 // Store the style in the record. The row will pick up the style this way instead of through 259 // index because index can change with sort order. 260 $scope.statusClass = flashStyle; 261 262 // The animation cannot be repeated unless the class is removed the element. 263 $timeout(function() { 264 $scope.statusClass = ""; 265 }, flashDurationMillis); 266 }; 267 268 $scope.selectedRebaseline = function(index, event) { 269 // Retrieve the records in the same order they are displayed. 270 var recordsInOrder = $parse("records | orderBy:sortingDiffer")($scope); 271 272 // If the user is shift clicking, apply the last tick/untick to all elements in between this 273 // record, and the last one they ticked/unticked. 274 if (event.shiftKey && $scope.lastSelectedIndex !== undefined) { 275 var currentAction = recordsInOrder[index].isRebaselined; 276 var smallerIndex = Math.min($scope.lastSelectedIndex, index); 277 var largerIndex = Math.max($scope.lastSelectedIndex, index); 278 for (var recordIndex = smallerIndex; recordIndex <= largerIndex; recordIndex++) { 279 recordsInOrder[recordIndex].isRebaselined = currentAction; 280 } 281 $scope.lastSelectedIndex = index; 282 } 283 else 284 { 285 $scope.lastSelectedIndex = index; 286 } 287 288 }; 289 290 $scope.commitRebaselines = function() { 291 // Gather up all records that have the rebaseline set. 292 var rebaselines = []; 293 for (var recordIndex = 0; recordIndex < $scope.records.length; recordIndex++) { 294 if ($scope.records[recordIndex].isRebaselined) { 295 rebaselines.push($scope.records[recordIndex].testPath); 296 } 297 } 298 $http.post("/commit_rebaselines", { 299 "rebaselines": rebaselines 300 }).success(function(data) { 301 $scope.flashStatus(data.success); 302 }).error(function() { 303 $scope.flashStatus(false); 304 }); 305 }; 306} 307