├── .gitignore ├── .gitmodules ├── LICENSE ├── Procfile ├── README.md ├── TODO.md ├── css ├── gradient-example.png └── gradient-scanner.css ├── index.html ├── js ├── color-stops.js ├── edit-gradient.js ├── example-image.js ├── flow-section.js ├── gradient-scanner.js ├── image-data-utils.js ├── image-loader.js ├── line-selector.js ├── line-utils.js └── sensitivity.js ├── lib └── colorpicker │ ├── css │ └── colorpicker.css │ ├── images │ ├── colorpicker_background.png │ ├── colorpicker_hex.png │ ├── colorpicker_hsb_b.png │ ├── colorpicker_hsb_h.png │ ├── colorpicker_hsb_s.png │ ├── colorpicker_indic.gif │ ├── colorpicker_overlay.png │ ├── colorpicker_rgb_b.png │ ├── colorpicker_rgb_g.png │ ├── colorpicker_rgb_r.png │ └── colorpicker_select.gif │ └── js │ └── colorpicker.js ├── package.json ├── server.js └── test ├── color-stops.js ├── css-gradient-dropdown-menu.gif ├── index.html ├── line-utils.js └── testData.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.tmproj 2 | npm-debug.log 3 | node_modules 4 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/jquery-tmpl"] 2 | path = lib/jquery-tmpl 3 | url = https://github.com/jquery/jquery-tmpl.git 4 | [submodule "lib/pixastic"] 5 | path = lib/pixastic 6 | url = https://github.com/kpdecker/pixastic.git 7 | [submodule "test/qunit"] 8 | path = test/qunit 9 | url = https://github.com/jquery/qunit.git 10 | [submodule "lib/user-image-cache"] 11 | path = lib/user-image-cache 12 | url = https://github.com/kpdecker/user-image-cache.git 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Kevin Decker, http://www.incaseofstairs.com/ 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | - Neither the name of the Kevin Decker nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 9 | 10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 11 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node server.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gradient-scanner 2 | Utility to generate CSS3 gradients from source raster files like mock images. 3 | 4 | ## Features 5 | - Loading for mockup files via URL or local file access (File API) 6 | - Scanning of a user selected section of text for possible gradients 7 | - User adjustment of the gradient estimations with live preview 8 | - Cross-browser gradient CSS generation 9 | 10 | ## Setup 11 | ### Checkout 12 | $ git clone git@github.com:kpdecker/gradient-scanner.git 13 | $ cd gradient-scanner 14 | $ git submodule update --init 15 | 16 | ### Development Requirements 17 | Development of the app must be done over a HTTP connection (or connection other than file://) due to the canvas [security model](http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#security-with-canvas-elements). 18 | 19 | ### Node Server Setup 20 | In order to access files that are on URLs outside of the current server, we must proxy images due to the security model. 21 | 22 | $ npm install express 23 | 24 | ### Node Server Execution 25 | 26 | $ cd ./server 27 | $ node main.js 28 | 29 | ## Development Plan 30 | Development currently mid-phase 1. 31 | 32 | ### Phase 1: Lowest Common Denominator 33 | Implement support for only the features that are supported by both the latest WebKit and Mozilla implementations. 34 | 35 | This amounts to: 36 | 37 | - Linear gradients 38 | - Radial gradients with a single center point 39 | - Arbitrary number of color stops 40 | 41 | ### Phase 2: Single Feature Mode 42 | Support the features that are specific to a single browser, attempting to best match the display for the other browser. 43 | 44 | This will add support for radial gradients with multiple center points and repeating gradients. 45 | 46 | ### Phase 3: Automated Feature Detection 47 | Implement automatic feature detection in addition to the user-input based system. 48 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | - CSS generation 3 | - Cross browser 4 | - Implement the help UI 5 | - Investigate the effects that blurring the image can have on the final result 6 | - Preview UI 7 | - Radial support 8 | - Repeating support 9 | -------------------------------------------------------------------------------- /css/gradient-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpdecker/gradient-scanner/33136b64375667523d7b6f2338870617e1222d69/css/gradient-example.png -------------------------------------------------------------------------------- /css/gradient-scanner.css: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Kevin Decker (http://www.incaseofstairs.com/) 3 | * See LICENSE for license information 4 | */ 5 | /* General Page UI */ 6 | body { 7 | color: white; 8 | background-color: #83ADC8; 9 | font-size: 9pt; 10 | font-family: trebuchet ms,arial,helvetica,sans-serif; 11 | margin: 0; 12 | } 13 | .wrapper { 14 | margin: 0px auto; 15 | padding: 0; 16 | background-color: #131F27; 17 | 18 | width: 1000px; 19 | } 20 | .header { 21 | background-color: black; 22 | margin: 20px 0 0; 23 | } 24 | 25 | .wrapper, 26 | .wrapper > .header { 27 | -moz-border-radius: 10px 10px 0px 0px; 28 | -webkit-border-radius: 10px 10px 0px 0px; 29 | border-radius: 10px 10px 0px 0px; 30 | } 31 | 32 | .header > h1 { 33 | display: inline-block; 34 | 35 | margin: 0; 36 | padding: 5px 15px; 37 | 38 | font-size: 15pt; 39 | font-weight: normal; 40 | letter-spacing: -2px; 41 | } 42 | 43 | .content { 44 | margin: 5px 15px; 45 | overflow: hidden; 46 | } 47 | 48 | #errorMsg { 49 | color: red; 50 | } 51 | 52 | .footer { 53 | background-color: black; 54 | padding: 5px 10px; 55 | clear: both; 56 | overflow: auto; 57 | } 58 | .footer-link { 59 | text-align: center; 60 | } 61 | .header > a, 62 | .footer > div > a { 63 | color: #6598B8; 64 | } 65 | 66 | .help-text { 67 | padding: 0 0 15px 15px; 68 | } 69 | 70 | /* Flow */ 71 | .flow-section { 72 | display: none; 73 | } 74 | .flow-section.active { 75 | display: block; 76 | } 77 | .flow-prev { 78 | visibility: hidden; 79 | float: left; 80 | } 81 | .flow-next { 82 | visibility: hidden; 83 | float: right; 84 | } 85 | .flow-prev.active, 86 | .flow-next.active { 87 | visibility: visible; 88 | } 89 | 90 | /* File Input Click Handler: See http://www.quirksmode.org/dom/inputfile.html */ 91 | .no-local .localDependent { 92 | display: none; 93 | } 94 | 95 | label { 96 | display: block; 97 | } 98 | .info-marker { 99 | position: relative; 100 | display: inline-block; 101 | color: yellow; 102 | font-weight: bold; 103 | } 104 | .info-marker > .info { 105 | position: absolute; 106 | top: 100%; 107 | left: 0; 108 | min-width: 300px; 109 | z-index: 1000; 110 | display: none; 111 | 112 | background-color: #83ADC8; 113 | color: black; 114 | font-weight: normal; 115 | 116 | padding: 8px; 117 | 118 | -webkit-border-radius: 15px; 119 | -webkit-border-top-left-radius: 0; 120 | -moz-border-radius: 15px; 121 | -moz-border-radius-topleft: 0; 122 | border-radius: 15px; 123 | border-top-left-radius: 0; 124 | } 125 | .info-marker:hover > .info { 126 | display: block; 127 | } 128 | 129 | #pathToImage { 130 | width: 250px; 131 | margin-left: 0.6em; 132 | } 133 | .fileInput { 134 | position: relative; 135 | } 136 | 137 | .fileInput > input[type="button"] { 138 | position: absolute; 139 | top: 0px; 140 | left: 0px; 141 | z-index: 1; 142 | } 143 | 144 | .fileInput > input[type='file'] { 145 | position: relative; 146 | text-align: right; 147 | opacity: 0; 148 | z-index: 2; 149 | } 150 | 151 | /* Line Overlay Components */ 152 | .image-display { 153 | position: relative; 154 | display: inline-block; 155 | 156 | margin: 10px 0; 157 | cursor: crosshair; 158 | } 159 | #lineOverlay { 160 | position: absolute; 161 | background: rgba(0, 0, 0, 0.1); 162 | height: 5px; 163 | border: dashed yellow 1px; 164 | 165 | /* linked to both the height and the border. Needs to be adjusted if those are changed */ 166 | -moz-transform-origin: 2px 2px; 167 | -o-transform-origin: 2px 2px; 168 | -webkit-transform-origin: 2px 2px; 169 | 170 | /* Default to hidden */ 171 | top: -1000px; 172 | left: -1000px; 173 | width: 0; 174 | } 175 | 176 | /* Focus Line Preview Components */ 177 | .gradient-preview { 178 | display: inline-block; 179 | width: 400px; 180 | } 181 | 182 | .preview-component { 183 | display: block; 184 | width: 100%; 185 | height: 50px; 186 | } 187 | 188 | .display-gradient { 189 | float: left; 190 | margin-bottom: 15px; 191 | } 192 | .stop-editor { 193 | display: none; 194 | float: right; 195 | margin-bottom: 15px; 196 | } 197 | .stop-editor.active { 198 | display: block; 199 | } 200 | 201 | .delta-e-slider, 202 | .stop-position-slider { 203 | margin: 5px 0; 204 | width: 400px; 205 | } 206 | 207 | /* Stop List */ 208 | .stops-list { 209 | width: 400px; 210 | height: 200px; 211 | overflow: auto; 212 | 213 | border: 1px solid #333; 214 | } 215 | .color-stop { 216 | padding: 5px 5px 0; 217 | line-height: 30px; 218 | } 219 | .color-stop.disabled { 220 | opacity: 0.5; 221 | text-decoration: line-through; 222 | } 223 | .color-stop.editing, 224 | .color-stop:hover { 225 | background-color: #0AE; 226 | cursor: pointer; 227 | } 228 | .color-preview { 229 | float: left; 230 | width: 25px; 231 | height: 25px; 232 | margin-right: 5px; 233 | } 234 | .stop-editor-field { 235 | display: block; 236 | } 237 | 238 | .generated-css { 239 | padding: 15px; 240 | overflow: auto; 241 | clear: both; 242 | 243 | border: 1px solid #333; 244 | } 245 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | gradient-scanner 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |
18 |

gradient-scanner

19 |
20 |
21 |
22 |
23 |

Select Image

24 |
25 | Open an image that contains a gradient and then draw a line over the major components of the gradient. 26 |
27 | 44 | 45 |
46 | 47 |
48 |
49 | 50 |
51 |
52 |

Adjust Color Sensitivity

53 |
54 | Use the slider to adjust the sensitivity of the color matcher. 55 |
56 |
57 |
58 | 59 |
60 |
61 |
62 |
63 |
64 |

 65 |             
66 | 67 |
68 |

Edit Gradient

69 |
70 | Manually adjust the color stops as necessary. 71 |
72 |
73 |
74 | 75 |
76 |
77 |
78 |
79 |
80 |
81 | Color: 82 |
83 | 87 | 91 |
92 |

 93 |             
94 |
95 | 96 | 111 |
112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /js/color-stops.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Kevin Decker (http://www.incaseofstairs.com/) 3 | * See LICENSE for license information 4 | */ 5 | var ColorStops = {}; 6 | 7 | (function() { 8 | ColorStops.JND = 2.39; 9 | 10 | function slopeChanged(oldDeriv, newDeriv) { 11 | if (!oldDeriv[0] && !oldDeriv[1] && !oldDeriv[2] && !oldDeriv[3]) { 12 | return false; 13 | } 14 | 15 | if (!newDeriv[0] && !newDeriv[1] && !newDeriv[2] && !newDeriv[3]) { 16 | return true; 17 | } 18 | 19 | return (newDeriv[0] && (oldDeriv[0]<0 !== newDeriv[0]<0)) 20 | || (newDeriv[1] && (oldDeriv[1]<0 !== newDeriv[1]<0)) 21 | || (newDeriv[2] && (oldDeriv[2]<0 !== newDeriv[2]<0)) 22 | || (newDeriv[3] && (oldDeriv[3]<0 !== newDeriv[3]<0)); 23 | } 24 | function updateDeriv(oldDeriv, newDeriv) { 25 | return [ 26 | newDeriv[0] || oldDeriv[0], 27 | newDeriv[1] || oldDeriv[1], 28 | newDeriv[2] || oldDeriv[2], 29 | newDeriv[3] || oldDeriv[3] 30 | ]; 31 | } 32 | 33 | function RGBtoXYZ(color) { 34 | // Algorithm from http://www.brucelindbloom.com/Eqn_RGB_to_XYZ.html 35 | var R = color[0] / 255, 36 | G = color[1] / 255, 37 | B = color[2] / 255; 38 | 39 | // Inverse Gamma Compounding, using Adobe RGB (1998) color 40 | // http://www.brucelindbloom.com/WorkingSpaceInfo.html#Specifications 41 | var r = Math.pow(R, 2.2), 42 | g = Math.pow(G, 2.2), 43 | b = Math.pow(B, 2.2); 44 | 45 | // Constants here are constructed from the M value for Adobe RGB (1998) defined 46 | // here: http://www.brucelindbloom.com/Eqn_RGB_XYZ_Matrix.html 47 | return [ 48 | (0.5767309*r + 0.1855540*b + 0.1881852*g), 49 | (0.2973769*r + 0.6273491*b + 0.0752741*g), 50 | (0.0270343*r + 0.0706872*b + 0.9911085*g) 51 | ]; 52 | } 53 | function XYZtoLAB(color) { 54 | // Algorithm from: http://www.brucelindbloom.com/Eqn_XYZ_to_Lab.html 55 | const X_r=0.95047, Y_r=1.00, Z_r=1.08883, 56 | EPSILON = 0.008856, 57 | KAPPA = 903.3; 58 | 59 | var x = color[0]/X_r, 60 | y = color[1]/X_r, 61 | z = color[2]/X_r; 62 | 63 | function f_(a) { 64 | if (a > EPSILON) { 65 | return Math.pow(a, 1/3); 66 | } else { 67 | return (KAPPA*a+16)/116; 68 | } 69 | } 70 | var f_x = f_(x), 71 | f_y = f_(y), 72 | f_z = f_(z); 73 | 74 | return [ 75 | 116*f_y-16, 76 | 500*(f_x-f_z), 77 | 200*(f_y-f_z) 78 | ]; 79 | } 80 | function deltaE(color1, color2) { 81 | color1 = XYZtoLAB(RGBtoXYZ(color1)); 82 | color2 = XYZtoLAB(RGBtoXYZ(color2)); 83 | 84 | return Math.sqrt(Math.pow(color2[0]-color1[0], 2)+Math.pow(color2[1]-color1[1], 2)+Math.pow(color2[2]-color1[2], 2)); 85 | } 86 | function colorsApproxEqual(color1, color2, dELimit) { 87 | const ALPHA_EPSILON = 256*0.1; 88 | 89 | var dE = deltaE(color1, color2); 90 | return dE <= dELimit 91 | && Math.abs(color1[3]-color2[3]) <= ALPHA_EPSILON; 92 | } 93 | function stopsLinear(prev, cur, next, dELimit) { 94 | var deltaX = next.position-prev.position, 95 | slope = [ 96 | (next.color[0]-prev.color[0])/deltaX, 97 | (next.color[1]-prev.color[1])/deltaX, 98 | (next.color[2]-prev.color[2])/deltaX, 99 | (next.color[3]-prev.color[3])/deltaX 100 | ], 101 | curDeltaX = cur.position-prev.position, 102 | curEstimate = [ 103 | prev.color[0]+slope[0]*curDeltaX | 0, 104 | prev.color[1]+slope[1]*curDeltaX | 0, 105 | prev.color[2]+slope[2]*curDeltaX | 0, 106 | prev.color[3]+slope[3]*curDeltaX | 0 107 | ]; 108 | 109 | return colorsApproxEqual(cur.color, curEstimate, dELimit); 110 | } 111 | function cullDuplicates(stops, dELimit) { 112 | const EDGE_DISTANCE = 0.01; 113 | 114 | dELimit = dELimit || ColorStops.JND; 115 | if (stops.length < 2) { 116 | return; 117 | } 118 | 119 | // Special case for the first to remove any repeating sections 120 | if (stops[1].position-stops[0].position > EDGE_DISTANCE 121 | && colorsApproxEqual(stops[0].color, stops[1].color, dELimit)) { 122 | stops.splice(0, 1); 123 | } 124 | 125 | var len = stops.length; 126 | for (var i = 1; i < len-1; i++) { 127 | var prev = stops[i-1], 128 | cur = stops[i], 129 | next = stops[i+1]; 130 | // Check to see if the current element is on the same line as the previous and next. If so remove. 131 | if (stopsLinear(prev, cur, next, dELimit)) { 132 | stops.splice(i, 1); 133 | i--; len--; 134 | } 135 | } 136 | 137 | // Another special case for the last to remove any repeating sections 138 | if (len >= 2 139 | && stops[len-1].position-stops[len-2].position > EDGE_DISTANCE 140 | && colorsApproxEqual(stops[len-2].color, stops[len-1].color, dELimit)) { 141 | stops.splice(len-1, 1); 142 | } 143 | } 144 | 145 | ColorStops.extractColorStops = function(lineData, dELimit) { 146 | var len = lineData.length, 147 | last = [lineData[0], lineData[1], lineData[2], lineData[3]], 148 | deriv = [0, 0, 0, 0], 149 | secDeriv = [0, 0, 0, 0], 150 | ret = []; 151 | 152 | // We always have a color stop on the first pixel 153 | ret.push({ 154 | position: 0.0, 155 | color: last 156 | }); 157 | 158 | // Scan through the remaining pixels looking for any of our possible triggers, err on the side of false 159 | // positives (which may be culled in the next step) 160 | var totalDelta = 0, 161 | runCount = 0; 162 | for (var i = 4; i < len; i += 4) { 163 | var cur = [lineData[i], lineData[i+1], lineData[i+2], lineData[i+3]], 164 | newDeriv = [cur[0]-last[0], cur[1]-last[1], cur[2]-last[2], cur[3]-last[3]], 165 | newSecDeriv = [newDeriv[0]-deriv[0], newDeriv[1]-deriv[1], newDeriv[2]-deriv[2], newDeriv[3]-deriv[3]], 166 | delta = Math.abs(newDeriv[0]) + Math.abs(newDeriv[1]) + Math.abs(newDeriv[2]) + Math.abs(newDeriv[3]); 167 | 168 | // Check to see if this is a drastic change in derivative from the previous trend 169 | var prevAverage = totalDelta/runCount, 170 | deltaVariance = Math.abs(prevAverage-delta); 171 | if (runCount && prevAverage < 0.75*deltaVariance) { 172 | ret.push({ 173 | position: i / len, 174 | color: last 175 | }); 176 | ret.push({ 177 | position: i / len, 178 | color: cur 179 | }); 180 | totalDelta = 0; 181 | runCount = 0; 182 | } else { 183 | totalDelta += delta; 184 | runCount++; 185 | } 186 | 187 | // Check for a zero crossing in the first or second dervative 188 | if (slopeChanged(deriv, newDeriv) || slopeChanged(secDeriv, newSecDeriv)) { 189 | ret.push({ 190 | position: i / len, 191 | color: cur 192 | }); 193 | deriv = newDeriv; 194 | secDeriv = newSecDeriv; 195 | } 196 | 197 | last = cur; 198 | deriv = updateDeriv(deriv, newDeriv); 199 | secDeriv = updateDeriv(secDeriv, newSecDeriv); 200 | } 201 | 202 | ret.push({ 203 | position: 1.0, 204 | color: last 205 | }); 206 | 207 | // Clear out any stops that do not provide additional data (per configurable dELimit) 208 | cullDuplicates(ret, dELimit); 209 | 210 | // Make the position values nice and clean 211 | ret.forEach(function(stop) { 212 | stop.position = Math.floor(stop.position*1000)/1000; 213 | }); 214 | 215 | return ret; 216 | }; 217 | 218 | ColorStops.getColorValue = function(color) { 219 | color = color.map(Math.floor); 220 | if (color[3] === 255) { 221 | color.pop(); 222 | 223 | var hex = color.map(function(color) { 224 | var ret = color.toString(16); 225 | if (ret.length < 2) { 226 | ret = "0" + ret; 227 | } 228 | return ret; 229 | }), 230 | reduce = hex.every(function(color) { 231 | return color[0] === color[1]; 232 | }); 233 | 234 | if (reduce) { 235 | hex = hex.map(function(color) { 236 | return color[0]; 237 | }); 238 | } 239 | 240 | return "#" + hex.join(""); 241 | } else { 242 | return "rgba(" + color.join(", ") + ")"; 243 | } 244 | }; 245 | 246 | function generateContainer(dragStart, dragEnd) { 247 | if (LineUtils.getUnit(dragStart.x) === "%" || LineUtils.getUnit(dragStart.y) === "%" 248 | || LineUtils.getUnit(dragEnd.x) === "%" || LineUtils.getUnit(dragEnd.y) === "%") { 249 | return {x:0,y:0, width:"100%",height:"100%"}; 250 | } else { 251 | return { 252 | x:0, 253 | y:0, 254 | width:Math.max(parseFloat(dragStart.x), parseFloat(dragEnd.x)) + (LineUtils.getUnit(dragStart.x) || LineUtils.getUnit(dragEnd.x) || 0), 255 | height:Math.max(parseFloat(dragStart.y), parseFloat(dragEnd.y)) + (LineUtils.getUnit(dragStart.y) || LineUtils.getUnit(dragEnd.y) || 0) 256 | }; 257 | } 258 | } 259 | 260 | function newGenerator(prefix, type, dragStart, dragEnd, colorStops, container) { 261 | function formatUnit(value) { 262 | return parseFloat(value) + (LineUtils.getUnit(value) || ""); 263 | } 264 | 265 | container = container || generateContainer(dragStart, dragEnd); 266 | 267 | var gradientPoints = LineUtils.gradientPoints(dragStart, dragEnd, container), 268 | stopCSS = colorStops.map(function(stop, index) { 269 | var prev = colorStops[index-1], 270 | next = colorStops[index+1], 271 | position = " " + Math.floor((gradientPoints.startOff + stop.position*gradientPoints.scale) * 1000)/10 + "%"; 272 | if (prev && next) { 273 | // Check to see if these are equally spaced and should have their values removed 274 | var avgPos = prev.position + (next.position-prev.position)/2; 275 | if (avgPos === stop.position) { 276 | // We are on the same line, no need to display the position value 277 | position = ""; 278 | } 279 | } else if ((!index && position === " 0%") || (!next && position === " 100%")) { 280 | position = ""; 281 | } 282 | return ColorStops.getColorValue(stop.color) + position; 283 | }).join(", "), 284 | 285 | angle = 360-LineUtils.radsToDegrees(LineUtils.slopeInRads(dragStart, dragEnd)), 286 | point = !LineUtils.isOnEdge(dragStart, container) ? formatUnit(dragStart.x) + " " + formatUnit(dragStart.y) : ""; 287 | 288 | // Generate the position component if necessary 289 | var unprefixedAngle = Math.abs(angle - 450) % 360, 290 | position = angle !== 270 ? angle + 'deg, ' : '', 291 | unprefixedPosition = unprefixedAngle !== 180 ? unprefixedAngle + 'deg, ' : ''; 292 | 293 | if (type === "linear") { 294 | return (prefix ? '-' + prefix + '-' : '') 295 | + 'linear-gradient(' 296 | + (prefix ? position : unprefixedPosition) 297 | + stopCSS 298 | + ')'; 299 | } else if (type === "radial") { 300 | } 301 | } 302 | var cssGenerators = { 303 | webkitOld: function(type, dragStart, dragEnd, colorStops) { 304 | function formatUnit(value) { 305 | if (LineUtils.getUnit(value) === "%") { 306 | return parseFloat(value) + "%"; 307 | } else { 308 | return parseFloat(value); 309 | } 310 | } 311 | 312 | var stopCSS = colorStops.map(function(stop) { 313 | if (!stop.position) { 314 | return "from(" + ColorStops.getColorValue(stop.color) + ")"; 315 | } else if (stop.position === 1.0) { 316 | return "to(" + ColorStops.getColorValue(stop.color) + ")"; 317 | } else { 318 | return "color-stop(" + stop.position + ", " + ColorStops.getColorValue(stop.color) + ")"; 319 | } 320 | }).join(", "); 321 | 322 | var points = formatUnit(dragStart.x) + " " + formatUnit(dragStart.y) + ", " + formatUnit(dragEnd.x) + " " + formatUnit(dragEnd.y); 323 | 324 | return "-webkit-gradient(" + type + ", " + points + ", " + stopCSS + ")"; 325 | }, 326 | webkitNew: function(type, dragStart, dragEnd, colorStops, container) { 327 | return newGenerator("webkit", type, dragStart, dragEnd, colorStops, container); 328 | }, 329 | mozilla: function(type, dragStart, dragEnd, colorStops, container) { 330 | return newGenerator("moz", type, dragStart, dragEnd, colorStops, container); 331 | }, 332 | opera: function(type, dragStart, dragEnd, colorStops, container) { 333 | return newGenerator("o", type, dragStart, dragEnd, colorStops, container); 334 | }, 335 | standard: function(type, dragStart, dragEnd, colorStops, container) { 336 | return newGenerator('', type, dragStart, dragEnd, colorStops, container); 337 | } 338 | }; 339 | 340 | ColorStops.generateCSS = function(type, dragStart, dragEnd, colorStops, container) { 341 | colorStops = colorStops.filter(function(stop) { 342 | return !stop.disabled; 343 | }); 344 | 345 | var ret = []; 346 | for (var name in cssGenerators) { 347 | if (cssGenerators.hasOwnProperty(name)) { 348 | ret.push(cssGenerators[name](type, dragStart, dragEnd, colorStops, container)); 349 | } 350 | } 351 | 352 | return ret; 353 | }; 354 | 355 | ColorStops.applyBackground = function(el, type, dragStart, dragEnd, colorStops) { 356 | var css = ColorStops.generateCSS(type, dragStart, dragEnd, colorStops), 357 | len = css.length; 358 | for (var i = 0; i < len; i++) { 359 | el.css("background-image", css[i]); 360 | } 361 | }; 362 | })(); 363 | -------------------------------------------------------------------------------- /js/edit-gradient.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Kevin Decker (http://www.incaseofstairs.com/) 3 | * See LICENSE for license information 4 | */ 5 | /*global $, jQuery, ColorStops, GradientScanner */ 6 | 7 | $(document).ready(function() { 8 | $.template( 9 | "colorStopTemplate", 10 | "
" 11 | + "
" 12 | + "${colorCss} ${position}%" 13 | + "
"); 14 | 15 | var colorStopsEl = $("#colorStops"); 16 | 17 | var colorStops, editStop, deltaE = ColorStops.JND; 18 | 19 | function renderStop(stop, index) { 20 | var stopEl = $.tmpl("colorStopTemplate", { 21 | position: Math.floor(stop.position*1000)/10, 22 | colorCss: ColorStops.getColorValue(stop.color), 23 | disabled: stop.disabled ? "disabled" : "" 24 | }); 25 | stopEl.data("stopIndex", index); 26 | 27 | return stopEl; 28 | } 29 | function outputGradient() { 30 | $(document).trigger(new jQuery.Event("gradientUpdated")); 31 | } 32 | function updateGradient() { 33 | colorStops = GradientScanner.colorStops; 34 | 35 | colorStopsEl.html(""); 36 | $(".stop-editor").removeClass("active"); 37 | 38 | colorStops.forEach(function(stop, index) { 39 | colorStopsEl.append(renderStop(stop, index)); 40 | }); 41 | 42 | outputGradient(); 43 | } 44 | 45 | $(".stop-position-slider").slider({ 46 | step: 0.001, 47 | min: 0, 48 | max: 1, 49 | slide: function(event, ui) { 50 | var growing = editStop.position < ui.value, 51 | 52 | editingEl = $(".color-stop.editing"), 53 | curIndex = editingEl.data("stopIndex"), 54 | 55 | toUpdate = colorStopsEl.children().filter(function() { 56 | var el = $(this), 57 | index = el.data("stopIndex"), 58 | stop = colorStops[index]; 59 | if (growing) { 60 | return index > curIndex && stop.position < ui.value; 61 | } else { 62 | return index < curIndex && stop.position > ui.value; 63 | } 64 | }); 65 | 66 | // Update the stop index for all of the elements that are between this location and 67 | // the destination 68 | toUpdate.each(function() { 69 | var el = $(this); 70 | el.data("stopIndex", el.data("stopIndex") + (growing?-1:1)); 71 | }); 72 | 73 | // Update the data model 74 | curIndex += (growing?1:-1)*toUpdate.length; 75 | editStop.position = ui.value; 76 | colorStops.sort(function(a, b) { return a.position-b.position; }); 77 | 78 | // Rerender the element for the updated state 79 | var stopEl = renderStop(editStop, curIndex); 80 | stopEl.addClass("editing"); 81 | if (!toUpdate.length) { 82 | editingEl.after(stopEl); 83 | } else if (growing) { 84 | toUpdate.last().after(stopEl); 85 | } else { 86 | toUpdate.first().before(stopEl); 87 | } 88 | editingEl.remove(); 89 | 90 | // Make sure that the element is visible 91 | // TODO : Cache this height value 92 | colorStopsEl.scrollTop(stopEl.height()*(curIndex-1)); 93 | 94 | // Update the rest of the app 95 | outputGradient(); 96 | } 97 | }); 98 | 99 | $("#disableCheck").click(function() { 100 | var el = $(this); 101 | editStop.disabled = this.checked; 102 | $(".color-stop.editing").toggleClass("disabled", editStop.disabled); 103 | 104 | // Update the rest of the app 105 | outputGradient(); 106 | }); 107 | 108 | $(".color-sel").ColorPicker({ 109 | flat: true, 110 | onChange: function(hsb, hex, rgb) { 111 | editStop.color[0] = rgb.r; 112 | editStop.color[1] = rgb.g; 113 | editStop.color[2] = rgb.b; 114 | 115 | var editingEl = $(".color-stop.editing"); 116 | 117 | // Rerender the element for the updated state 118 | var stopEl = renderStop(editStop, editingEl.data("stopIndex")); 119 | stopEl.addClass("editing"); 120 | editingEl.replaceWith(stopEl); 121 | 122 | // Update the rest of the app 123 | outputGradient(); 124 | } 125 | }); 126 | 127 | colorStopsEl.delegate(".color-stop", "click", function(event) { 128 | var el = $(this); 129 | 130 | editStop = colorStops[el.data("stopIndex")]; 131 | 132 | if (!el.hasClass("editing")) { 133 | $(".stop-position-slider").slider("option", "value", editStop.position); 134 | $("#disableCheck").attr("checked", editStop.disabled ? "checked" : ""); 135 | $(".color-sel").ColorPickerSetColor({r:editStop.color[0], g:editStop.color[1], b:editStop.color[2]}); 136 | 137 | $(".color-stop.editing").removeClass("editing"); 138 | $(".stop-editor").addClass("active"); 139 | } else { 140 | $(".stop-editor").removeClass("active"); 141 | } 142 | el.toggleClass("editing"); 143 | 144 | outputGradient(); 145 | }); 146 | 147 | $(document).bind("deltaEUpdated", updateGradient); 148 | }); 149 | -------------------------------------------------------------------------------- /js/example-image.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() { 2 | 3 | var img = document.createElement("img"); 4 | $(img).load(function() { 5 | GradientScanner.loadImage(img, "example image"); 6 | }); 7 | img.src = "css/gradient-example.png"; 8 | 9 | $(document).bind("imageLoaded", function lineSeed() { 10 | $(this).unbind("imageLoaded", lineSeed); 11 | 12 | var offset = $("#imageDisplay").offset(); 13 | 14 | var mousedown = jQuery.Event("mousedown"); 15 | mousedown.which = 1; 16 | 17 | mousedown.pageX = 300; 18 | mousedown.pageY = 1; 19 | 20 | mousedown.pageX += offset.left; 21 | mousedown.pageY += offset.top; 22 | 23 | var mousemove = jQuery.Event("mousemove"); 24 | mousemove.which = 1; 25 | 26 | mousemove.pageX = 300; 27 | mousemove.pageY = 89; 28 | 29 | mousemove.pageX += offset.left; 30 | mousemove.pageY += offset.top; 31 | 32 | $("#imageDisplay") 33 | .trigger(mousedown) 34 | .trigger(mousemove) 35 | .trigger(jQuery.Event("mouseup")); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /js/flow-section.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Kevin Decker (http://www.incaseofstairs.com/) 3 | * See LICENSE for license information 4 | */ 5 | (function ($) { 6 | var FlowSection = { 7 | init: function(options) { 8 | var flowSection = this; 9 | 10 | this.first().addClass("active"); 11 | $(".flow-prev").click(function() { 12 | var prev = FlowSection.getPrev.call(flowSection); 13 | if (prev) { 14 | flowSection.filter(".active").removeClass("active"); 15 | 16 | prev.el.addClass("active"); 17 | 18 | // Update the button states 19 | $(".flow-next").addClass("active"); 20 | } 21 | 22 | // Update the prev button state 23 | if (!prev || prev.index <= 0) { 24 | $(".flow-prev").removeClass("active"); 25 | } else { 26 | $(".flow-prev").addClass("active"); 27 | } 28 | }); 29 | $(".flow-next").addClass("active").click(function() { 30 | var next = FlowSection.getNext.call(flowSection); 31 | 32 | if (next) { 33 | flowSection.filter(".active").removeClass("active"); 34 | next.el.addClass("active"); 35 | 36 | // Update the button states 37 | $(".flow-prev").addClass("active"); 38 | } 39 | 40 | // Update the next button state 41 | if (!next || next.index === flowSection.length-1) { 42 | $(".flow-next").removeClass("active"); 43 | } else { 44 | $(".flow-next").addClass("active"); 45 | } 46 | }); 47 | 48 | return this; 49 | }, 50 | 51 | getPrev: function() { 52 | var last, 53 | ret; 54 | this.each(function(index) { 55 | var el = $(this); 56 | if (el.hasClass("active")) { 57 | ret = { 58 | el: last, 59 | index: index-1 60 | }; 61 | 62 | return false; 63 | } 64 | last = el; 65 | }); 66 | 67 | return ret; 68 | }, 69 | getNext: function() { 70 | var foundActive, 71 | ret; 72 | this.each(function(index) { 73 | var el = $(this); 74 | if (el.hasClass("active")) { 75 | foundActive = true; 76 | } else if (foundActive) { 77 | ret = { 78 | el: el, 79 | index: index 80 | }; 81 | return false; 82 | } 83 | }); 84 | return ret; 85 | }, 86 | 87 | enablePrev: function() { 88 | // Enable the prev button if possible 89 | var prev = FlowSection.getPrev.call(this); 90 | if (prev && prev.index > 0) { 91 | $(".flow-prev").addClass("active"); 92 | } 93 | }, 94 | enableNext: function() { 95 | // Enable the next button if possible 96 | var prev = FlowSection.getNext.call(this); 97 | if (prev && prev.index < this.length-1) { 98 | $(".flow-next").addClass("active"); 99 | } 100 | }, 101 | disable: function(next) { 102 | // Disable the next/prev button 103 | $(next ? ".flow-next" : ".flow-prev").removeClass("active"); 104 | } 105 | } 106 | 107 | $.fn.flowSection = function(method, options) { 108 | if (method) { 109 | return FlowSection[method].call(this, options); 110 | } else { 111 | return FlowSection.init.call(this, options); 112 | } 113 | }; 114 | })(jQuery); -------------------------------------------------------------------------------- /js/gradient-scanner.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Kevin Decker (http://www.incaseofstairs.com/) 3 | * See LICENSE for license information 4 | */ 5 | /*global $, ColorStops, LineUtils */ 6 | 7 | // Global namespace 8 | var GradientScanner = {}; 9 | 10 | $(document).ready(function() { 11 | var linePreview = $(".line-preview"); 12 | 13 | GradientScanner.resetLinePreview = function() { 14 | // Provide a 1px transparent image for the preview images 15 | linePreview.attr("src", "data:image/gif;base64,R0lGODlhAQABAIAAAP///////yH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="); 16 | }; 17 | GradientScanner.resetLinePreview(); 18 | 19 | var line, relLine, gradientType = "linear"; 20 | 21 | $(document).bind("gradientUpdated", function(event) { 22 | line = GradientScanner.line; 23 | 24 | // Clip the coords to their containing boxes. 25 | var relContaining = LineUtils.containingRect(line.start, line.end); 26 | relLine = { 27 | start: LineUtils.relativeCoords(line.start, relContaining), 28 | end: LineUtils.relativeCoords(line.end, relContaining) 29 | }; 30 | 31 | var css = ColorStops.generateCSS(gradientType, relLine.start, relLine.end, GradientScanner.colorStops); 32 | $(".generated-css").text("background-image: " + css.join(";\nbackground-image: ")); 33 | }); 34 | 35 | $(document).bind("imageLoaded", function(event) { 36 | $(".generated-css").text(""); 37 | 38 | $(".flow-section").flowSection("disable", true); 39 | }); 40 | 41 | $(".flow-section").flowSection(); 42 | }); 43 | -------------------------------------------------------------------------------- /js/image-data-utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Kevin Decker (http://www.incaseofstairs.com/) 3 | * See LICENSE for license information 4 | */ 5 | var ImageDataUtils; 6 | 7 | (function() { 8 | const DEFAULT_SNAP_SCAN_RANGE = 10; 9 | 10 | function sign(a, b) { 11 | return (a > b) ? 1 : (a < b) ? -1 : 0; 12 | } 13 | 14 | function getPixelDelta(coord, endOffset, imageData) { 15 | var testOffset = ImageDataUtils.getOffset(coord, imageData), 16 | data = imageData.data; 17 | 18 | return Math.abs(data[endOffset]-data[testOffset]) 19 | + Math.abs(data[endOffset+1]-data[testOffset+1]) 20 | + Math.abs(data[endOffset+2]-data[testOffset+2]) 21 | + Math.abs(data[endOffset+3]-data[testOffset+3]); 22 | } 23 | 24 | ImageDataUtils = { 25 | getCoords: function(offset, imageData) { 26 | var binWidth = imageData.width * 4; 27 | return { 28 | x: offset % binWidth, 29 | y: offset / binWidth | 0 30 | }; 31 | }, 32 | getOffset: function(coords, imageData) { 33 | return (coords.x + coords.y*imageData.width)*4; 34 | }, 35 | getPixel: function(context, coord) { 36 | return context.getImageData(coord.x, coord.y, 1, 1).data; 37 | }, 38 | getWindow: function getSnapToWindow(context, coord, windowSize) { 39 | var firstPixel = {x: Math.max(coord.x-windowSize, 0), y: Math.max(coord.y-windowSize, 0)}; 40 | return { 41 | firstPixel: firstPixel, 42 | focusPixel: {x: coord.x-firstPixel.x, y: coord.y-firstPixel.y}, 43 | imageData: context.getImageData( 44 | firstPixel.x, 45 | firstPixel.y, 46 | Math.min(2*windowSize, context.canvas.width-firstPixel.x), 47 | Math.min(2*windowSize, context.canvas.height-firstPixel.y)) 48 | }; 49 | }, 50 | getLinePixels: function(context, coordStart, coordEnd) { 51 | // Extract the rectangle that contains our data 52 | var containingRect = LineUtils.containingRect(coordStart, coordEnd), 53 | image = context.getImageData( 54 | containingRect.x, 55 | containingRect.y, 56 | Math.min(containingRect.width+1, context.canvas.width-containingRect.x), // Need to limit to image coords for Mozilla 57 | Math.min(containingRect.height+1, context.canvas.height-containingRect.y)), 58 | imageData = image.data; 59 | 60 | // Determine the properties of our line 61 | var len = LineUtils.distance(coordStart, coordEnd)|0; 62 | if (!len) { 63 | return; 64 | } 65 | 66 | // Create our destination image 67 | var line = context.createImageData(len, 1), 68 | lineData = line.data; 69 | 70 | LineUtils.walkLine(coordStart, coordEnd, function(t, coords) { 71 | var firstCoord = {x: Math.floor(coords.x), y: Math.floor(coords.y)}, 72 | nextCoord = {x: Math.floor(coords.x), y: Math.ceil(coords.y)}; 73 | 74 | var firstOffset = ImageDataUtils.getOffset(firstCoord, image), 75 | nextOffset = ImageDataUtils.getOffset(nextCoord, image); 76 | 77 | var xPercentage = 1-(coords.x-firstCoord.x), 78 | yPercentage = 1-(coords.y-firstCoord.y); 79 | function calcComponent(offset1, offset2) { 80 | return imageData[offset1 ]*xPercentage*yPercentage 81 | + (imageData[offset1+4]||0)*(1-xPercentage)*yPercentage 82 | + (imageData[offset2 ]||0)*xPercentage*(1-yPercentage) 83 | + (imageData[offset2+4]||0)*(1-xPercentage)*(1-yPercentage); 84 | } 85 | 86 | var lineOffset = t*4; 87 | lineData[lineOffset] = calcComponent(firstOffset, nextOffset); 88 | lineData[lineOffset+1] = calcComponent(firstOffset+1, nextOffset+1); 89 | lineData[lineOffset+2] = calcComponent(firstOffset+2, nextOffset+2); 90 | lineData[lineOffset+3] = calcComponent(firstOffset+3, nextOffset+3); 91 | }); 92 | 93 | return line; 94 | }, 95 | getInitialSnapToTarget: function(edgeContext, coordStart, scanRange) { 96 | // Load the window we are concerned with right now 97 | var snapWindow = ImageDataUtils.getWindow(edgeContext, coordStart, scanRange || DEFAULT_SNAP_SCAN_RANGE), 98 | focusPixel = snapWindow.focusPixel, 99 | imageData = snapWindow.imageData, 100 | data = snapWindow.imageData.data, 101 | endOffset = ImageDataUtils.getOffset(focusPixel, snapWindow.imageData); 102 | 103 | // Setup our data tracker 104 | var maximum = focusPixel; 105 | maximum.distance = Infinity; 106 | maximum.delta = 0; 107 | 108 | function checkPixel(coord) { 109 | var delta = getPixelDelta(coord, endOffset, imageData); 110 | 111 | if (delta > 128) { 112 | coord.distance = Math.max(Math.abs(coord.x-focusPixel.x), Math.abs(coord.y-focusPixel.y)); 113 | coord.delta = delta; 114 | if (coord.distance && (maximum.distance-coord.distance || coord.delta-maximum.delta) > 0) { 115 | maximum = coord; 116 | } 117 | } 118 | } 119 | 120 | // Scan the window for any pixels that are dratically different 121 | var y = imageData.height; 122 | while (y--) { 123 | checkPixel({x: focusPixel.x, y: y}); 124 | } 125 | 126 | var x = imageData.width; 127 | while (x--) { 128 | checkPixel({x: x, y: focusPixel.y}); 129 | } 130 | 131 | // Move the return one pixel further along the path 132 | maximum.x = maximum.x+-1*sign(maximum.x, focusPixel.x)+snapWindow.firstPixel.x; 133 | maximum.y = maximum.y+-1*sign(maximum.y, focusPixel.y)+snapWindow.firstPixel.y; 134 | 135 | return maximum; 136 | }, 137 | getSnapToTarget: function(edgeContext, coordStart, coordEnd, scanRange) { 138 | // Load the window we are concerned with right now 139 | var snapWindow = ImageDataUtils.getWindow(edgeContext, coordEnd, scanRange || DEFAULT_SNAP_SCAN_RANGE), 140 | imageData = snapWindow.imageData, 141 | data = imageData.data, 142 | endOffset = ImageDataUtils.getOffset(snapWindow.focusPixel, imageData), 143 | 144 | angle = LineUtils.slopeInRads(coordStart, coordEnd), 145 | shiftedStart = {x: coordStart.x-snapWindow.firstPixel.x, y: coordStart.y-snapWindow.firstPixel.y}; 146 | 147 | var maximum = snapWindow.focusPixel; 148 | maximum.slopeDelta = 0; 149 | maximum.distance = LineUtils.distance(shiftedStart, maximum); 150 | maximum.delta = 0; 151 | 152 | // Scan the window for any pixels that are dratically different 153 | var y = imageData.height; 154 | while (y--) { 155 | var x = imageData.width; 156 | while (x--) { 157 | var coord = {x: x, y: y}, 158 | delta = getPixelDelta(coord, endOffset, imageData); 159 | 160 | if (delta > 128) { 161 | coord.distance = LineUtils.distance(shiftedStart, coord); 162 | coord.delta = delta; 163 | coord.slopeDelta = Math.abs(LineUtils.slopeInRads(shiftedStart, coord)-angle); 164 | if ((maximum.slopeDelta-coord.slopeDelta || coord.delta-maximum.delta || coord.distance-maximum.distance) > 0) { 165 | maximum = coord; 166 | } 167 | } 168 | } 169 | } 170 | 171 | maximum.x += snapWindow.firstPixel.x; 172 | maximum.y += snapWindow.firstPixel.y; 173 | 174 | return maximum; 175 | }, 176 | getEdgeContext: function(el) { 177 | var loadOptions = {}; 178 | Pixastic.process(ImageDataUtils.cloneCanvas(el), "edges", loadOptions); 179 | return loadOptions.resultCanvas.getContext("2d"); 180 | }, 181 | cloneCanvas: function(el) { 182 | var clone = document.createElement("canvas"); 183 | clone.width = el.naturalWidth || el.width; 184 | clone.height = el.naturalHeight || el.height; 185 | clone.getContext("2d").drawImage(el, 0, 0); 186 | return clone; 187 | }, 188 | createCanvasFromImageData: function(imageData) { 189 | var canvas = document.createElement("canvas"); 190 | canvas.setAttribute("width", imageData.width); 191 | canvas.setAttribute("height", imageData.height); 192 | 193 | var context = canvas.getContext("2d"); 194 | context.putImageData(imageData, 0, 0); 195 | 196 | return canvas; 197 | }, 198 | }; 199 | })(); 200 | -------------------------------------------------------------------------------- /js/image-loader.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Kevin Decker (http://www.incaseofstairs.com/) 3 | * See LICENSE for license information 4 | */ 5 | /*global $,jQuery,LineUtils,ImageDataUtils,UserImageCache,GradientScanner */ 6 | $(document).ready(function() { 7 | var canvas = $("#imageDisplay"), 8 | context = canvas[0].getContext("2d"), 9 | pathToImage = $("#pathToImage"); 10 | 11 | function errorHandler(code) { 12 | $("#errorMsg").html("*** " + "Failed to load image. Error code: " + JSON.stringify(code)); 13 | } 14 | 15 | // Generic loader used by the UI and the seeder 16 | GradientScanner.loadImage = function(image, name) { 17 | canvas.attr({ 18 | width: image.width, 19 | height: image.height 20 | }); 21 | 22 | context.drawImage(image, 0, 0); 23 | 24 | pathToImage.val(name); 25 | 26 | GradientScanner.edgeContext = ImageDataUtils.getEdgeContext(context.canvas); 27 | $(document).trigger(new jQuery.Event("imageLoaded")); 28 | }; 29 | 30 | // User Image Cache loader 31 | var loader = new Image(); 32 | $(loader).load(function() { 33 | GradientScanner.loadImage(this, UserImageCache.getDisplayName()); 34 | }) 35 | .error(function() { errorHandler(); }); 36 | UserImageCache.setImageEl(loader); 37 | UserImageCache.setRemoteProxy("http://" + location.host + "/proxy?href="); 38 | 39 | // User Input Setup 40 | pathToImage.change(function(event) { 41 | $("#errorMsg").html(""); 42 | UserImageCache.load(pathToImage.val(), errorHandler); 43 | }); 44 | 45 | // File API 46 | if (UserImageCache.isLocalSupported()) { 47 | $("#localImage").bind("change", function(event) { 48 | var file = this.files[0]; 49 | 50 | $("#errorMsg").html(""); 51 | UserImageCache.load(file, errorHandler); 52 | }); 53 | 54 | // Unhide the browse button if the current browser supports local files 55 | $(".no-local").removeClass("no-local"); 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /js/line-selector.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Kevin Decker (http://www.incaseofstairs.com/) 3 | * See LICENSE for license information 4 | */ 5 | /*global $,jQuery,LineUtils,ImageDataUtils,GradientScanner */ 6 | $(document).ready(function() { 7 | const SNAP_TO_PX = 10; 8 | 9 | var canvas = $("#imageDisplay"), 10 | context = canvas[0].getContext("2d"), 11 | 12 | linePreview = $(".line-preview"); 13 | 14 | var dragging, dragStart, dragEnd, imageData; 15 | 16 | function resetLineOverlay() { 17 | $("#lineOverlay").css({ 18 | width: "", 19 | top: "", 20 | left: "" 21 | }); 22 | } 23 | 24 | canvas.parent().mousedown(function(event) { 25 | // Only activate this if the event is due to a left click 26 | if (event.which !== 1) { 27 | return; 28 | } 29 | 30 | dragging = true; 31 | var canvasOffset = canvas.offset(); 32 | dragStart = {x: event.pageX-canvasOffset.left, y: event.pageY-canvasOffset.top}; 33 | 34 | var edgeSnaps = ImageDataUtils.getInitialSnapToTarget(GradientScanner.edgeContext, dragStart); 35 | dragStart = edgeSnaps || dragStart; 36 | // Init the line overlay 37 | $("#lineOverlay").css("width", "0px") 38 | .css("left", dragStart.x+"px") 39 | .css("top", dragStart.y+"px"); 40 | 41 | // Reset any existing data 42 | dragEnd = undefined; 43 | imageData = undefined; 44 | 45 | GradientScanner.line = {}; 46 | GradientScanner.resetLinePreview(); 47 | $(".flow-section").flowSection("disable", true); 48 | 49 | event.preventDefault(); 50 | }).mousemove(function(event) { 51 | if (dragging) { 52 | var canvasOffset = canvas.offset(); 53 | dragEnd = {x: event.pageX-canvasOffset.left, y: event.pageY-canvasOffset.top}; 54 | 55 | // Check for snapto 56 | if (Math.abs(dragEnd.y-dragStart.y) < SNAP_TO_PX) { 57 | dragEnd.y = dragStart.y; 58 | } else if (Math.abs(dragEnd.x-dragStart.x) < SNAP_TO_PX) { 59 | dragEnd.x = dragStart.x; 60 | } 61 | 62 | // Check for edge snapto 63 | var edgeSnap = ImageDataUtils.getSnapToTarget(GradientScanner.edgeContext, dragStart, dragEnd); 64 | dragEnd = edgeSnap || dragEnd; 65 | 66 | // Collect the line data while the user is dragging 67 | imageData = ImageDataUtils.getLinePixels(context, dragStart, dragEnd); 68 | if (!imageData) { 69 | GradientScanner.resetLinePreview(); 70 | return; 71 | } 72 | 73 | // Display the line preview 74 | var stretcher = ImageDataUtils.createCanvasFromImageData(imageData); 75 | linePreview.attr("src", stretcher.toDataURL()); 76 | 77 | // Move the line indicator 78 | var distance = LineUtils.distance(dragStart, dragEnd), 79 | rotate = "rotate(" + LineUtils.slopeInRads(dragStart, dragEnd) + "rad)"; 80 | $("#lineOverlay").css("width", distance) 81 | .css("-moz-transform", rotate) 82 | .css("-o-transform", rotate) 83 | .css("-webkit-transform", rotate); 84 | } 85 | }).mouseup(function(event) { 86 | dragging = false; 87 | 88 | // If they didn't move the mouse then there isn't much we can do 89 | if (!dragEnd || !LineUtils.distance(dragStart, dragEnd)) { 90 | resetLineOverlay(); 91 | return; 92 | } 93 | 94 | GradientScanner.line = { 95 | start: dragStart, 96 | end: dragEnd, 97 | imageData: imageData 98 | }; 99 | 100 | $(document).trigger(new jQuery.Event("lineUpdated")); 101 | $(".flow-section").flowSection("enableNext"); 102 | }); 103 | 104 | $(document).bind("imageLoaded", function(event) { 105 | GradientScanner.line = {}; 106 | GradientScanner.resetLinePreview(); 107 | 108 | resetLineOverlay(); 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /js/line-utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Kevin Decker (http://www.incaseofstairs.com/) 3 | * See LICENSE for license information 4 | */ 5 | var LineUtils; 6 | 7 | (function() { 8 | 9 | function getUnit(value) { 10 | if (typeof value === "number") { 11 | return value ? "px" : 0; 12 | } 13 | 14 | if (parseFloat(value) === 0) { 15 | return 0; 16 | } 17 | return (/\d+(?:\.\d+)?(.*)/).exec(value)[1] || "px"; 18 | } 19 | function checkUnit(unit, value) { 20 | var newUnit = getUnit(value); 21 | if (unit === 0 || unit === newUnit) { 22 | return newUnit; 23 | } else if (newUnit === 0) { 24 | return unit; 25 | } else { 26 | return false; 27 | } 28 | } 29 | function combineUnit(magnitude, unit) { 30 | if (!magnitude || unit === "px") { 31 | return magnitude; 32 | } else { 33 | return magnitude + unit; 34 | } 35 | } 36 | 37 | LineUtils = { 38 | /** 39 | * Retrieves the unit from a given value (defaulting to pixels if undefined) 40 | */ 41 | getUnit: getUnit, 42 | 43 | /** 44 | * Determines the minimum rectangle that will cover the line segment with the given endpoints and optionally width 45 | */ 46 | containingRect: function(start, end, width) { 47 | var unit = 0; 48 | unit = checkUnit(unit, start.x); 49 | unit = checkUnit(unit, start.y); 50 | unit = checkUnit(unit, end.x); 51 | unit = checkUnit(unit, end.y); 52 | if (width) { 53 | unit = checkUnit(unit, width); 54 | } 55 | if (unit === false) { 56 | return NaN; 57 | } 58 | 59 | // Remove any units, we'll restore them later 60 | start = {x: parseFloat(start.x), y: parseFloat(start.y)}; 61 | end = {x: parseFloat(end.x), y: parseFloat(end.y)}; 62 | width = parseFloat(width); 63 | 64 | var topLeft = {x: Math.min(start.x, end.x), y: Math.min(start.y, end.y)}, 65 | bottomRight = {x: Math.max(start.x, end.x), y: Math.max(start.y, end.y)}; 66 | 67 | if (width) { 68 | var angle = LineUtils.slopeInRads(start, end), 69 | horz = Math.floor(width*Math.abs(Math.sin(angle))/2), 70 | vert = Math.floor(width*Math.abs(Math.cos(angle))/2); 71 | topLeft.x -= horz; 72 | topLeft.y -= vert; 73 | bottomRight.x += horz; 74 | bottomRight.y += vert; 75 | } 76 | 77 | return { 78 | x: combineUnit(topLeft.x, unit), 79 | y: combineUnit(topLeft.y, unit), 80 | width: combineUnit(Math.max(bottomRight.x-topLeft.x, 1), unit), 81 | height: combineUnit(Math.max(bottomRight.y-topLeft.y, 1), unit) 82 | }; 83 | }, 84 | 85 | /** 86 | * Returns the relative position for a given coord, relative to a specific origin. 87 | */ 88 | relativeCoords: function(coord, origin) { 89 | var unit = 0; 90 | unit = checkUnit(unit, coord.x); 91 | unit = checkUnit(unit, coord.y); 92 | unit = checkUnit(unit, origin.x); 93 | unit = checkUnit(unit, origin.y); 94 | if (unit === false) { 95 | return NaN; 96 | } 97 | 98 | return { 99 | x: combineUnit(parseFloat(coord.x)-parseFloat(origin.x), unit), 100 | y: combineUnit(parseFloat(coord.y)-parseFloat(origin.y), unit) 101 | }; 102 | }, 103 | 104 | /** 105 | * Determines the distance between two points. The units of this value are the same as those input. For the percentage case, 106 | * distance is distance covered within the plane of a square with demensions of 100% x 100%. This can not be mapped back 107 | * to a fixed unit space directly. 108 | */ 109 | distance: function(start, end) { 110 | // Check that the units match 111 | var unit = 0; 112 | unit = checkUnit(unit, start.x); 113 | unit = checkUnit(unit, start.y); 114 | unit = checkUnit(unit, end.x); 115 | unit = checkUnit(unit, end.y); 116 | if (unit === false) { 117 | return NaN; 118 | } 119 | 120 | return Math.sqrt(Math.pow(parseFloat(end.y)-parseFloat(start.y),2) + Math.pow(parseFloat(end.x)-parseFloat(start.x), 2)); 121 | }, 122 | 123 | /** 124 | * Calculates the percentage of the ray length that a line segment covers, when the ray is constrained by the given container. 125 | * @return percentage value [0, 1] 126 | */ 127 | percentageOfRay: function(start, end, container) { 128 | // Check that the units match 129 | var unit = 0; 130 | unit = checkUnit(unit, start.x); 131 | unit = checkUnit(unit, start.y); 132 | unit = checkUnit(unit, end.x); 133 | unit = checkUnit(unit, end.y); 134 | unit = checkUnit(unit, container.x); 135 | unit = checkUnit(unit, container.y); 136 | unit = checkUnit(unit, container.width); 137 | unit = checkUnit(unit, container.height); 138 | if (unit === false) { 139 | return NaN; 140 | } 141 | 142 | var intercepts = LineUtils.lineIntercepts(start, end, container, true); 143 | if (!intercepts.length) { 144 | return 0; 145 | } 146 | 147 | var rayDistance = LineUtils.distance(start, intercepts[intercepts.length-1]), 148 | segmentDistance = LineUtils.distance(start, end); 149 | return segmentDistance/rayDistance; 150 | }, 151 | 152 | isOnEdge: function(point, container) { 153 | // Check that the units match 154 | var unit = 0; 155 | unit = checkUnit(unit, point.x); 156 | unit = checkUnit(unit, point.y); 157 | unit = checkUnit(unit, container.x); 158 | unit = checkUnit(unit, container.y); 159 | unit = checkUnit(unit, container.width); 160 | unit = checkUnit(unit, container.height); 161 | if (unit === false) { 162 | return NaN; 163 | } 164 | 165 | point = {x: parseFloat(point.x), y: parseFloat(point.y)}; 166 | container = { 167 | x: parseFloat(container.x), y: parseFloat(container.y), 168 | width: parseFloat(container.width), height: parseFloat(container.height) 169 | }; 170 | 171 | if (point.x === container.x || point.x === container.x+container.width) { 172 | return container.y <= point.y && point.y <= container.y + container.height; 173 | } else if (point.y === container.y || point.y === container.y+container.height) { 174 | return container.x <= point.x && point.x <= container.x + container.width; 175 | } else { 176 | return false; 177 | } 178 | }, 179 | 180 | /** 181 | * Calculates the gradient start and end points as defined by http://dev.w3.org/csswg/css3-images/#linear-gradients 182 | * as well as the color stop adjustment values to fix the color stops to an centroid angle gradient. 183 | */ 184 | gradientPoints: function(start, end, container) { 185 | // Check that the units match 186 | var unit = 0; 187 | unit = checkUnit(unit, start.x); 188 | unit = checkUnit(unit, start.y); 189 | unit = checkUnit(unit, end.x); 190 | unit = checkUnit(unit, end.y); 191 | unit = checkUnit(unit, container.x); 192 | unit = checkUnit(unit, container.y); 193 | unit = checkUnit(unit, container.width); 194 | unit = checkUnit(unit, container.height); 195 | if (unit === false) { 196 | return NaN; 197 | } 198 | 199 | start = {x: parseFloat(start.x), y: parseFloat(start.y)}; 200 | end = {x: parseFloat(end.x), y: parseFloat(end.y)}; 201 | container = { 202 | x: parseFloat(container.x), y: parseFloat(container.y), 203 | width: parseFloat(container.width), height: parseFloat(container.height) 204 | }; 205 | 206 | var rise = end.y-start.y, 207 | run = end.x-start.x, 208 | slope = rise/run, 209 | perpendicularSlope = -1/slope, 210 | 211 | top = container.y, 212 | bottom = container.y + container.height, 213 | left = container.x, 214 | right = container.x + container.width, 215 | center = {x: container.x + container.width/2, y: container.y + container.height/2}, 216 | 217 | startCorner, endCorner, passedCornerIntercept, totalDist; 218 | 219 | function findLineIntersect(point1, slope1, point2, slope2) { 220 | var retX = (point1.y - point2.y + slope2*point2.x - slope1*point1.x)/(slope2 - slope1); 221 | return { 222 | x: retX, 223 | y: slope1*(retX - point1.x) + point1.y 224 | }; 225 | } 226 | 227 | // Special case the vertical and horizontal as they or their perpendicular have the lil divide by zero concern 228 | if (!run) { 229 | startCorner = {x: center.x, y: end.ystart.y?bottom:top}; 231 | passedCornerIntercept = {x: start.x, y: startCorner.y}; 232 | } else if (!rise) { 233 | // Corners are the vertical intercepts 234 | startCorner = {x: end.xstart.x?right:left, y: center.y}; 236 | passedCornerIntercept = {x: startCorner.x, y: start.y}; 237 | } else if (slope < 0) { 238 | // The corners are (0,h) and (w,0) 239 | var topRight = findLineIntersect({x:right, y:top}, perpendicularSlope, center, slope), 240 | bottomLeft = findLineIntersect({x:left, y:bottom}, perpendicularSlope, center, slope); 241 | 242 | startCorner = run < 0 ? topRight : bottomLeft; 243 | endCorner = run < 0 ? bottomLeft : topRight; 244 | passedCornerIntercept = findLineIntersect(start, slope, startCorner, perpendicularSlope); 245 | } else { 246 | // The corners are (0,0) and bottom (w,h) 247 | var topLeft = findLineIntersect({x:left, y:top}, perpendicularSlope, center, slope), 248 | bottomRight = findLineIntersect({x:right, y:bottom}, perpendicularSlope, center, slope); 249 | 250 | startCorner = run < 0 ? bottomRight : topLeft; 251 | endCorner = run < 0 ? topLeft : bottomRight; 252 | passedCornerIntercept = findLineIntersect(start, slope, startCorner, perpendicularSlope); 253 | } 254 | 255 | totalDist = LineUtils.distance(startCorner, endCorner); 256 | return { 257 | start: {x: combineUnit(startCorner.x, unit), y: combineUnit(startCorner.y, unit)}, 258 | startOff: LineUtils.distance(passedCornerIntercept, start)/totalDist, 259 | end: {x: combineUnit(endCorner.x, unit), y: combineUnit(endCorner.y, unit)}, 260 | scale: LineUtils.distance(start, end)/totalDist 261 | }; 262 | }, 263 | 264 | /** 265 | * Determines the two points at which the line connecting start and end intersects with the container 266 | * boundaries. 267 | */ 268 | lineIntercepts: function(start, end, container, ray) { 269 | // Check that the units match 270 | var unit = 0; 271 | unit = checkUnit(unit, start.x); 272 | unit = checkUnit(unit, start.y); 273 | unit = checkUnit(unit, end.x); 274 | unit = checkUnit(unit, end.y); 275 | unit = checkUnit(unit, container.x); 276 | unit = checkUnit(unit, container.y); 277 | unit = checkUnit(unit, container.width); 278 | unit = checkUnit(unit, container.height); 279 | if (unit === false) { 280 | return NaN; 281 | } 282 | 283 | start = {x: parseFloat(start.x), y: parseFloat(start.y)}; 284 | end = {x: parseFloat(end.x), y: parseFloat(end.y)}; 285 | container = { 286 | x: parseFloat(container.x), y: parseFloat(container.y), 287 | width: parseFloat(container.width), height: parseFloat(container.height) 288 | }; 289 | 290 | var rise = end.y-start.y, 291 | run = end.x-start.x, 292 | slope = rise/run, 293 | b = (start.y*end.x - end.y*start.x)/run, 294 | 295 | intercepts = [], 296 | 297 | x, y, 298 | len, check; 299 | 300 | // Special case the vertical 301 | if (!run) { 302 | if (container.x <= end.x && end.x <= container.x+container.width) { 303 | intercepts = [{x: end.x, y: container.y+(end.ystart.y?container.height:0)}]; 304 | 305 | len = intercepts.length; 306 | while (len--) { 307 | check = intercepts[len]; 308 | if (ray && (rise>0 ? check.ystart.y)) { 309 | intercepts.splice(len, 1); 310 | } 311 | 312 | check.x = combineUnit(check.x, unit); 313 | check.y = combineUnit(check.y, unit); 314 | } 315 | 316 | return intercepts; 317 | } else { 318 | return []; 319 | } 320 | } 321 | 322 | // 4 possible intercepts (of which two will be selected) 323 | // 1) y = container.y 324 | x = (container.y - b)/slope; 325 | if (container.x <= x && x <= container.x+container.width) { 326 | intercepts.push({x: x, y: container.y}); 327 | } 328 | 329 | // 2) x === container.x 330 | y = slope*container.x + b; 331 | if (container.y <= y && y <= container.y+container.height) { 332 | intercepts.push({x: container.x, y: y}); 333 | } 334 | 335 | // 3) y === container.y+container.height 336 | x = (container.y+container.height - b)/slope; 337 | if (container.x <= x && x <= container.x+container.width) { 338 | intercepts.push({x: x, y: container.y+container.height}); 339 | } 340 | 341 | // 4) x === container.x+container.width 342 | y = slope*(container.x+container.width) + b; 343 | if (container.y <= y && y <= container.y+container.height) { 344 | intercepts.push({x: container.x+container.width, y: y}); 345 | } 346 | 347 | // Order the elements so the first is the entering intercept and the later is the leaving 348 | intercepts.sort(function(a, b) { return run>0? a.x-b.x : b.x-a.x; }); 349 | 350 | // Remove any duplicates or entries before the ray start and restore the units 351 | len = intercepts.length; 352 | x = undefined; 353 | while (len--) { 354 | check = intercepts[len]; 355 | if (check.x === x || (ray && (run>0 ? check.xstart.x))) { 356 | intercepts.splice(len, 1); 357 | } 358 | x = check.x; 359 | 360 | check.x = combineUnit(check.x, unit); 361 | check.y = combineUnit(check.y, unit); 362 | } 363 | 364 | return intercepts; 365 | }, 366 | 367 | /** 368 | * Determines the angle, in radians of the line connected by these two points. 369 | * Range: [0, 2PI] 370 | */ 371 | slopeInRads: function(start, end) { 372 | // Check that the units match 373 | var unit = 0; 374 | unit = checkUnit(unit, start.x); 375 | unit = checkUnit(unit, start.y); 376 | unit = checkUnit(unit, end.x); 377 | unit = checkUnit(unit, end.y); 378 | if (unit === false) { 379 | return NaN; 380 | } 381 | 382 | var rise = parseFloat(end.y)-parseFloat(start.y), 383 | run = parseFloat(end.x)-parseFloat(start.x); 384 | return (run<0 ? Math.PI : (rise<0 ? 2*Math.PI : 0)) + (run ? Math.atan(rise/run) : (rise<0?-1:1)*Math.PI/2); 385 | }, 386 | radsToDegrees: function(rads) { 387 | return 180*rads/Math.PI; 388 | }, 389 | walkLine: function(start, end, callback) { 390 | // Determine the properties of our line 391 | var rise = end.y-start.y, 392 | run = end.x-start.x, 393 | xIntercept = start.x-Math.min(start.x, end.x), 394 | yIntercept = start.y-Math.min(start.y, end.y), 395 | distance = LineUtils.distance(start, end), 396 | 397 | len = distance|0; 398 | if (!len) { 399 | return len; 400 | } 401 | 402 | // Scale the run and rise for each integer step 403 | run /= len; 404 | rise /= len; 405 | 406 | // Walk the parameterized line 407 | for (var t = 0; t < len; t++) { 408 | callback(t, { 409 | x: (run*t + xIntercept), 410 | y: (rise*t + yIntercept) 411 | }); 412 | } 413 | return len; 414 | } 415 | }; 416 | 417 | })(); 418 | -------------------------------------------------------------------------------- /js/sensitivity.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Kevin Decker (http://www.incaseofstairs.com/) 3 | * See LICENSE for license information 4 | */ 5 | /*global $, jQuery, ColorStops, GradientScanner */ 6 | 7 | $(document).ready(function() { 8 | var colorStops, deltaE = ColorStops.JND; 9 | 10 | function outputGradient() { 11 | ColorStops.applyBackground($(".gradient-preview"), "linear", {x: 0, y: 0}, {x: "100%", y: 0}, colorStops); 12 | $(".stop-count").text("Count: " + colorStops.filter(function(stop) { return !stop.disabled; }).length + " deltaE: " + deltaE); 13 | } 14 | function updateGradient() { 15 | GradientScanner.colorStops = colorStops = ColorStops.extractColorStops(GradientScanner.line.imageData.data, deltaE); 16 | 17 | $(document).trigger(new jQuery.Event("deltaEUpdated")); 18 | } 19 | 20 | $(".delta-e-slider").slider({ 21 | value: deltaE, 22 | step: 0.5, 23 | min: 1, 24 | max: 15, 25 | slide: function(event, ui) { 26 | deltaE = ui.value; 27 | 28 | updateGradient(); 29 | } 30 | }); 31 | 32 | $(document).bind("imageLoaded", function(event) { 33 | $(".gradient-preview").css("background", "none"); 34 | $(".stop-count").text(''); 35 | }); 36 | $(document).bind("lineUpdated", updateGradient); 37 | $(document).bind("deltaEUpdated", outputGradient); 38 | $(document).bind("gradientUpdated", outputGradient); 39 | }); 40 | -------------------------------------------------------------------------------- /lib/colorpicker/css/colorpicker.css: -------------------------------------------------------------------------------- 1 | .colorpicker { 2 | width: 356px; 3 | height: 176px; 4 | overflow: hidden; 5 | position: absolute; 6 | background: url(../images/colorpicker_background.png); 7 | font-family: Arial, Helvetica, sans-serif; 8 | display: none; 9 | } 10 | .colorpicker_color { 11 | width: 150px; 12 | height: 150px; 13 | left: 14px; 14 | top: 13px; 15 | position: absolute; 16 | background: #f00; 17 | overflow: hidden; 18 | cursor: crosshair; 19 | } 20 | .colorpicker_color div { 21 | position: absolute; 22 | top: 0; 23 | left: 0; 24 | width: 150px; 25 | height: 150px; 26 | background: url(../images/colorpicker_overlay.png); 27 | } 28 | .colorpicker_color div div { 29 | position: absolute; 30 | top: 0; 31 | left: 0; 32 | width: 11px; 33 | height: 11px; 34 | overflow: hidden; 35 | background: url(../images/colorpicker_select.gif); 36 | margin: -5px 0 0 -5px; 37 | } 38 | .colorpicker_hue { 39 | position: absolute; 40 | top: 13px; 41 | left: 171px; 42 | width: 35px; 43 | height: 150px; 44 | cursor: n-resize; 45 | } 46 | .colorpicker_hue div { 47 | position: absolute; 48 | width: 35px; 49 | height: 9px; 50 | overflow: hidden; 51 | background: url(../images/colorpicker_indic.gif) left top; 52 | margin: -4px 0 0 0; 53 | left: 0px; 54 | } 55 | .colorpicker_new_color { 56 | position: absolute; 57 | width: 60px; 58 | height: 30px; 59 | left: 213px; 60 | top: 13px; 61 | background: #f00; 62 | } 63 | .colorpicker_current_color { 64 | position: absolute; 65 | width: 60px; 66 | height: 30px; 67 | left: 283px; 68 | top: 13px; 69 | background: #f00; 70 | } 71 | .colorpicker input { 72 | background-color: transparent; 73 | border: 1px solid transparent; 74 | position: absolute; 75 | font-size: 10px; 76 | font-family: Arial, Helvetica, sans-serif; 77 | color: #898989; 78 | top: 4px; 79 | right: 11px; 80 | text-align: right; 81 | margin: 0; 82 | padding: 0; 83 | height: 11px; 84 | } 85 | .colorpicker_hex { 86 | position: absolute; 87 | width: 72px; 88 | height: 22px; 89 | background: url(../images/colorpicker_hex.png) top; 90 | left: 212px; 91 | top: 142px; 92 | } 93 | .colorpicker_hex input { 94 | right: 6px; 95 | } 96 | .colorpicker_field { 97 | height: 22px; 98 | width: 62px; 99 | background-position: top; 100 | position: absolute; 101 | } 102 | .colorpicker_field span { 103 | position: absolute; 104 | width: 12px; 105 | height: 22px; 106 | overflow: hidden; 107 | top: 0; 108 | right: 0; 109 | cursor: n-resize; 110 | } 111 | .colorpicker_rgb_r { 112 | background-image: url(../images/colorpicker_rgb_r.png); 113 | top: 52px; 114 | left: 212px; 115 | } 116 | .colorpicker_rgb_g { 117 | background-image: url(../images/colorpicker_rgb_g.png); 118 | top: 82px; 119 | left: 212px; 120 | } 121 | .colorpicker_rgb_b { 122 | background-image: url(../images/colorpicker_rgb_b.png); 123 | top: 112px; 124 | left: 212px; 125 | } 126 | .colorpicker_hsb_h { 127 | background-image: url(../images/colorpicker_hsb_h.png); 128 | top: 52px; 129 | left: 282px; 130 | } 131 | .colorpicker_hsb_s { 132 | background-image: url(../images/colorpicker_hsb_s.png); 133 | top: 82px; 134 | left: 282px; 135 | } 136 | .colorpicker_hsb_b { 137 | background-image: url(../images/colorpicker_hsb_b.png); 138 | top: 112px; 139 | left: 282px; 140 | } 141 | .colorpicker_focus { 142 | background-position: center; 143 | } 144 | .colorpicker_hex.colorpicker_focus { 145 | background-position: bottom; 146 | } 147 | .colorpicker_slider { 148 | background-position: bottom; 149 | } 150 | -------------------------------------------------------------------------------- /lib/colorpicker/images/colorpicker_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpdecker/gradient-scanner/33136b64375667523d7b6f2338870617e1222d69/lib/colorpicker/images/colorpicker_background.png -------------------------------------------------------------------------------- /lib/colorpicker/images/colorpicker_hex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpdecker/gradient-scanner/33136b64375667523d7b6f2338870617e1222d69/lib/colorpicker/images/colorpicker_hex.png -------------------------------------------------------------------------------- /lib/colorpicker/images/colorpicker_hsb_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpdecker/gradient-scanner/33136b64375667523d7b6f2338870617e1222d69/lib/colorpicker/images/colorpicker_hsb_b.png -------------------------------------------------------------------------------- /lib/colorpicker/images/colorpicker_hsb_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpdecker/gradient-scanner/33136b64375667523d7b6f2338870617e1222d69/lib/colorpicker/images/colorpicker_hsb_h.png -------------------------------------------------------------------------------- /lib/colorpicker/images/colorpicker_hsb_s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpdecker/gradient-scanner/33136b64375667523d7b6f2338870617e1222d69/lib/colorpicker/images/colorpicker_hsb_s.png -------------------------------------------------------------------------------- /lib/colorpicker/images/colorpicker_indic.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpdecker/gradient-scanner/33136b64375667523d7b6f2338870617e1222d69/lib/colorpicker/images/colorpicker_indic.gif -------------------------------------------------------------------------------- /lib/colorpicker/images/colorpicker_overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpdecker/gradient-scanner/33136b64375667523d7b6f2338870617e1222d69/lib/colorpicker/images/colorpicker_overlay.png -------------------------------------------------------------------------------- /lib/colorpicker/images/colorpicker_rgb_b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpdecker/gradient-scanner/33136b64375667523d7b6f2338870617e1222d69/lib/colorpicker/images/colorpicker_rgb_b.png -------------------------------------------------------------------------------- /lib/colorpicker/images/colorpicker_rgb_g.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpdecker/gradient-scanner/33136b64375667523d7b6f2338870617e1222d69/lib/colorpicker/images/colorpicker_rgb_g.png -------------------------------------------------------------------------------- /lib/colorpicker/images/colorpicker_rgb_r.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpdecker/gradient-scanner/33136b64375667523d7b6f2338870617e1222d69/lib/colorpicker/images/colorpicker_rgb_r.png -------------------------------------------------------------------------------- /lib/colorpicker/images/colorpicker_select.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpdecker/gradient-scanner/33136b64375667523d7b6f2338870617e1222d69/lib/colorpicker/images/colorpicker_select.gif -------------------------------------------------------------------------------- /lib/colorpicker/js/colorpicker.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * Color picker 4 | * Author: Stefan Petre www.eyecon.ro 5 | * 6 | * Dual licensed under the MIT and GPL licenses 7 | * 8 | */ 9 | (function ($) { 10 | var ColorPicker = function () { 11 | var 12 | ids = {}, 13 | inAction, 14 | charMin = 65, 15 | visible, 16 | tpl = '
', 17 | defaults = { 18 | eventName: 'click', 19 | onShow: function () {}, 20 | onBeforeShow: function(){}, 21 | onHide: function () {}, 22 | onChange: function () {}, 23 | onSubmit: function () {}, 24 | color: 'ff0000', 25 | livePreview: true, 26 | flat: false 27 | }, 28 | fillRGBFields = function (hsb, cal) { 29 | var rgb = HSBToRGB(hsb); 30 | $(cal).data('colorpicker').fields 31 | .eq(1).val(rgb.r).end() 32 | .eq(2).val(rgb.g).end() 33 | .eq(3).val(rgb.b).end(); 34 | }, 35 | fillHSBFields = function (hsb, cal) { 36 | $(cal).data('colorpicker').fields 37 | .eq(4).val(hsb.h).end() 38 | .eq(5).val(hsb.s).end() 39 | .eq(6).val(hsb.b).end(); 40 | }, 41 | fillHexFields = function (hsb, cal) { 42 | $(cal).data('colorpicker').fields 43 | .eq(0).val(HSBToHex(hsb)).end(); 44 | }, 45 | setSelector = function (hsb, cal) { 46 | $(cal).data('colorpicker').selector.css('backgroundColor', '#' + HSBToHex({h: hsb.h, s: 100, b: 100})); 47 | $(cal).data('colorpicker').selectorIndic.css({ 48 | left: parseInt(150 * hsb.s/100, 10), 49 | top: parseInt(150 * (100-hsb.b)/100, 10) 50 | }); 51 | }, 52 | setHue = function (hsb, cal) { 53 | $(cal).data('colorpicker').hue.css('top', parseInt(150 - 150 * hsb.h/360, 10)); 54 | }, 55 | setCurrentColor = function (hsb, cal) { 56 | $(cal).data('colorpicker').currentColor.css('backgroundColor', '#' + HSBToHex(hsb)); 57 | }, 58 | setNewColor = function (hsb, cal) { 59 | $(cal).data('colorpicker').newColor.css('backgroundColor', '#' + HSBToHex(hsb)); 60 | }, 61 | keyDown = function (ev) { 62 | var pressedKey = ev.charCode || ev.keyCode || -1; 63 | if ((pressedKey > charMin && pressedKey <= 90) || pressedKey == 32) { 64 | return false; 65 | } 66 | var cal = $(this).parent().parent(); 67 | if (cal.data('colorpicker').livePreview === true) { 68 | change.apply(this); 69 | } 70 | }, 71 | change = function (ev) { 72 | var cal = $(this).parent().parent(), col; 73 | if (this.parentNode.className.indexOf('_hex') > 0) { 74 | cal.data('colorpicker').color = col = HexToHSB(fixHex(this.value)); 75 | } else if (this.parentNode.className.indexOf('_hsb') > 0) { 76 | cal.data('colorpicker').color = col = fixHSB({ 77 | h: parseInt(cal.data('colorpicker').fields.eq(4).val(), 10), 78 | s: parseInt(cal.data('colorpicker').fields.eq(5).val(), 10), 79 | b: parseInt(cal.data('colorpicker').fields.eq(6).val(), 10) 80 | }); 81 | } else { 82 | cal.data('colorpicker').color = col = RGBToHSB(fixRGB({ 83 | r: parseInt(cal.data('colorpicker').fields.eq(1).val(), 10), 84 | g: parseInt(cal.data('colorpicker').fields.eq(2).val(), 10), 85 | b: parseInt(cal.data('colorpicker').fields.eq(3).val(), 10) 86 | })); 87 | } 88 | if (ev) { 89 | fillRGBFields(col, cal.get(0)); 90 | fillHexFields(col, cal.get(0)); 91 | fillHSBFields(col, cal.get(0)); 92 | } 93 | setSelector(col, cal.get(0)); 94 | setHue(col, cal.get(0)); 95 | setNewColor(col, cal.get(0)); 96 | cal.data('colorpicker').onChange.apply(cal, [col, HSBToHex(col), HSBToRGB(col)]); 97 | }, 98 | blur = function (ev) { 99 | var cal = $(this).parent().parent(); 100 | cal.data('colorpicker').fields.parent().removeClass('colorpicker_focus'); 101 | }, 102 | focus = function () { 103 | charMin = this.parentNode.className.indexOf('_hex') > 0 ? 70 : 65; 104 | $(this).parent().parent().data('colorpicker').fields.parent().removeClass('colorpicker_focus'); 105 | $(this).parent().addClass('colorpicker_focus'); 106 | }, 107 | downIncrement = function (ev) { 108 | var field = $(this).parent().find('input').focus(); 109 | var current = { 110 | el: $(this).parent().addClass('colorpicker_slider'), 111 | max: this.parentNode.className.indexOf('_hsb_h') > 0 ? 360 : (this.parentNode.className.indexOf('_hsb') > 0 ? 100 : 255), 112 | y: ev.pageY, 113 | field: field, 114 | val: parseInt(field.val(), 10), 115 | preview: $(this).parent().parent().data('colorpicker').livePreview 116 | }; 117 | $(document).bind('mouseup', current, upIncrement); 118 | $(document).bind('mousemove', current, moveIncrement); 119 | }, 120 | moveIncrement = function (ev) { 121 | ev.data.field.val(Math.max(0, Math.min(ev.data.max, parseInt(ev.data.val + ev.pageY - ev.data.y, 10)))); 122 | if (ev.data.preview) { 123 | change.apply(ev.data.field.get(0), [true]); 124 | } 125 | return false; 126 | }, 127 | upIncrement = function (ev) { 128 | change.apply(ev.data.field.get(0), [true]); 129 | ev.data.el.removeClass('colorpicker_slider').find('input').focus(); 130 | $(document).unbind('mouseup', upIncrement); 131 | $(document).unbind('mousemove', moveIncrement); 132 | return false; 133 | }, 134 | downHue = function (ev) { 135 | var current = { 136 | cal: $(this).parent(), 137 | y: $(this).offset().top 138 | }; 139 | current.preview = current.cal.data('colorpicker').livePreview; 140 | $(document).bind('mouseup', current, upHue); 141 | $(document).bind('mousemove', current, moveHue); 142 | }, 143 | moveHue = function (ev) { 144 | change.apply( 145 | ev.data.cal.data('colorpicker') 146 | .fields 147 | .eq(4) 148 | .val(parseInt(360*(150 - Math.max(0,Math.min(150,(ev.pageY - ev.data.y))))/150, 10)) 149 | .get(0), 150 | [ev.data.preview] 151 | ); 152 | return false; 153 | }, 154 | upHue = function (ev) { 155 | fillRGBFields(ev.data.cal.data('colorpicker').color, ev.data.cal.get(0)); 156 | fillHexFields(ev.data.cal.data('colorpicker').color, ev.data.cal.get(0)); 157 | $(document).unbind('mouseup', upHue); 158 | $(document).unbind('mousemove', moveHue); 159 | return false; 160 | }, 161 | downSelector = function (ev) { 162 | var current = { 163 | cal: $(this).parent(), 164 | pos: $(this).offset() 165 | }; 166 | current.preview = current.cal.data('colorpicker').livePreview; 167 | $(document).bind('mouseup', current, upSelector); 168 | $(document).bind('mousemove', current, moveSelector); 169 | }, 170 | moveSelector = function (ev) { 171 | change.apply( 172 | ev.data.cal.data('colorpicker') 173 | .fields 174 | .eq(6) 175 | .val(parseInt(100*(150 - Math.max(0,Math.min(150,(ev.pageY - ev.data.pos.top))))/150, 10)) 176 | .end() 177 | .eq(5) 178 | .val(parseInt(100*(Math.max(0,Math.min(150,(ev.pageX - ev.data.pos.left))))/150, 10)) 179 | .get(0), 180 | [ev.data.preview] 181 | ); 182 | return false; 183 | }, 184 | upSelector = function (ev) { 185 | fillRGBFields(ev.data.cal.data('colorpicker').color, ev.data.cal.get(0)); 186 | fillHexFields(ev.data.cal.data('colorpicker').color, ev.data.cal.get(0)); 187 | $(document).unbind('mouseup', upSelector); 188 | $(document).unbind('mousemove', moveSelector); 189 | return false; 190 | }, 191 | enterSubmit = function (ev) { 192 | $(this).addClass('colorpicker_focus'); 193 | }, 194 | leaveSubmit = function (ev) { 195 | $(this).removeClass('colorpicker_focus'); 196 | }, 197 | clickSubmit = function (ev) { 198 | var cal = $(this).parent(); 199 | var col = cal.data('colorpicker').color; 200 | cal.data('colorpicker').origColor = col; 201 | setCurrentColor(col, cal.get(0)); 202 | cal.data('colorpicker').onSubmit(col, HSBToHex(col), HSBToRGB(col), cal.data('colorpicker').el); 203 | }, 204 | show = function (ev) { 205 | var cal = $('#' + $(this).data('colorpickerId')); 206 | cal.data('colorpicker').onBeforeShow.apply(this, [cal.get(0)]); 207 | var pos = $(this).offset(); 208 | var viewPort = getViewport(); 209 | var top = pos.top + this.offsetHeight; 210 | var left = pos.left; 211 | if (top + 176 > viewPort.t + viewPort.h) { 212 | top -= this.offsetHeight + 176; 213 | } 214 | if (left + 356 > viewPort.l + viewPort.w) { 215 | left -= 356; 216 | } 217 | cal.css({left: left + 'px', top: top + 'px'}); 218 | if (cal.data('colorpicker').onShow.apply(this, [cal.get(0)]) != false) { 219 | cal.show(); 220 | } 221 | $(document).bind('mousedown', {cal: cal}, hide); 222 | return false; 223 | }, 224 | hide = function (ev) { 225 | if (!isChildOf(ev.data.cal.get(0), ev.target, ev.data.cal.get(0))) { 226 | if (ev.data.cal.data('colorpicker').onHide.apply(this, [ev.data.cal.get(0)]) != false) { 227 | ev.data.cal.hide(); 228 | } 229 | $(document).unbind('mousedown', hide); 230 | } 231 | }, 232 | isChildOf = function(parentEl, el, container) { 233 | if (parentEl == el) { 234 | return true; 235 | } 236 | if (parentEl.contains) { 237 | return parentEl.contains(el); 238 | } 239 | if ( parentEl.compareDocumentPosition ) { 240 | return !!(parentEl.compareDocumentPosition(el) & 16); 241 | } 242 | var prEl = el.parentNode; 243 | while(prEl && prEl != container) { 244 | if (prEl == parentEl) 245 | return true; 246 | prEl = prEl.parentNode; 247 | } 248 | return false; 249 | }, 250 | getViewport = function () { 251 | var m = document.compatMode == 'CSS1Compat'; 252 | return { 253 | l : window.pageXOffset || (m ? document.documentElement.scrollLeft : document.body.scrollLeft), 254 | t : window.pageYOffset || (m ? document.documentElement.scrollTop : document.body.scrollTop), 255 | w : window.innerWidth || (m ? document.documentElement.clientWidth : document.body.clientWidth), 256 | h : window.innerHeight || (m ? document.documentElement.clientHeight : document.body.clientHeight) 257 | }; 258 | }, 259 | fixHSB = function (hsb) { 260 | return { 261 | h: Math.min(360, Math.max(0, hsb.h)), 262 | s: Math.min(100, Math.max(0, hsb.s)), 263 | b: Math.min(100, Math.max(0, hsb.b)) 264 | }; 265 | }, 266 | fixRGB = function (rgb) { 267 | return { 268 | r: Math.min(255, Math.max(0, rgb.r)), 269 | g: Math.min(255, Math.max(0, rgb.g)), 270 | b: Math.min(255, Math.max(0, rgb.b)) 271 | }; 272 | }, 273 | fixHex = function (hex) { 274 | var len = 6 - hex.length; 275 | if (len > 0) { 276 | var o = []; 277 | for (var i=0; i -1) ? hex.substring(1) : hex), 16); 287 | return {r: hex >> 16, g: (hex & 0x00FF00) >> 8, b: (hex & 0x0000FF)}; 288 | }, 289 | HexToHSB = function (hex) { 290 | return RGBToHSB(HexToRGB(hex)); 291 | }, 292 | RGBToHSB = function (rgb) { 293 | var hsb = { 294 | h: 0, 295 | s: 0, 296 | b: 0 297 | }; 298 | var min = Math.min(rgb.r, rgb.g, rgb.b); 299 | var max = Math.max(rgb.r, rgb.g, rgb.b); 300 | var delta = max - min; 301 | hsb.b = max; 302 | if (max != 0) { 303 | 304 | } 305 | hsb.s = max != 0 ? 255 * delta / max : 0; 306 | if (hsb.s != 0) { 307 | if (rgb.r == max) { 308 | hsb.h = (rgb.g - rgb.b) / delta; 309 | } else if (rgb.g == max) { 310 | hsb.h = 2 + (rgb.b - rgb.r) / delta; 311 | } else { 312 | hsb.h = 4 + (rgb.r - rgb.g) / delta; 313 | } 314 | } else { 315 | hsb.h = -1; 316 | } 317 | hsb.h *= 60; 318 | if (hsb.h < 0) { 319 | hsb.h += 360; 320 | } 321 | hsb.s *= 100/255; 322 | hsb.b *= 100/255; 323 | return hsb; 324 | }, 325 | HSBToRGB = function (hsb) { 326 | var rgb = {}; 327 | var h = Math.round(hsb.h); 328 | var s = Math.round(hsb.s*255/100); 329 | var v = Math.round(hsb.b*255/100); 330 | if(s == 0) { 331 | rgb.r = rgb.g = rgb.b = v; 332 | } else { 333 | var t1 = v; 334 | var t2 = (255-s)*v/255; 335 | var t3 = (t1-t2)*(h%60)/60; 336 | if(h==360) h = 0; 337 | if(h<60) {rgb.r=t1; rgb.b=t2; rgb.g=t2+t3} 338 | else if(h<120) {rgb.g=t1; rgb.b=t2; rgb.r=t1-t3} 339 | else if(h<180) {rgb.g=t1; rgb.r=t2; rgb.b=t2+t3} 340 | else if(h<240) {rgb.b=t1; rgb.r=t2; rgb.g=t1-t3} 341 | else if(h<300) {rgb.b=t1; rgb.g=t2; rgb.r=t2+t3} 342 | else if(h<360) {rgb.r=t1; rgb.g=t2; rgb.b=t1-t3} 343 | else {rgb.r=0; rgb.g=0; rgb.b=0} 344 | } 345 | return {r:Math.round(rgb.r), g:Math.round(rgb.g), b:Math.round(rgb.b)}; 346 | }, 347 | RGBToHex = function (rgb) { 348 | var hex = [ 349 | rgb.r.toString(16), 350 | rgb.g.toString(16), 351 | rgb.b.toString(16) 352 | ]; 353 | $.each(hex, function (nr, val) { 354 | if (val.length == 1) { 355 | hex[nr] = '0' + val; 356 | } 357 | }); 358 | return hex.join(''); 359 | }, 360 | HSBToHex = function (hsb) { 361 | return RGBToHex(HSBToRGB(hsb)); 362 | }, 363 | restoreOriginal = function () { 364 | var cal = $(this).parent(); 365 | var col = cal.data('colorpicker').origColor; 366 | cal.data('colorpicker').color = col; 367 | fillRGBFields(col, cal.get(0)); 368 | fillHexFields(col, cal.get(0)); 369 | fillHSBFields(col, cal.get(0)); 370 | setSelector(col, cal.get(0)); 371 | setHue(col, cal.get(0)); 372 | setNewColor(col, cal.get(0)); 373 | }; 374 | return { 375 | init: function (opt) { 376 | opt = $.extend({}, defaults, opt||{}); 377 | if (typeof opt.color == 'string') { 378 | opt.color = HexToHSB(opt.color); 379 | } else if (opt.color.r != undefined && opt.color.g != undefined && opt.color.b != undefined) { 380 | opt.color = RGBToHSB(opt.color); 381 | } else if (opt.color.h != undefined && opt.color.s != undefined && opt.color.b != undefined) { 382 | opt.color = fixHSB(opt.color); 383 | } else { 384 | return this; 385 | } 386 | return this.each(function () { 387 | if (!$(this).data('colorpickerId')) { 388 | var options = $.extend({}, opt); 389 | options.origColor = opt.color; 390 | var id = 'collorpicker_' + parseInt(Math.random() * 1000); 391 | $(this).data('colorpickerId', id); 392 | var cal = $(tpl).attr('id', id); 393 | if (options.flat) { 394 | cal.appendTo(this).show(); 395 | } else { 396 | cal.appendTo(document.body); 397 | } 398 | options.fields = cal 399 | .find('input') 400 | .bind('keyup', keyDown) 401 | .bind('change', change) 402 | .bind('blur', blur) 403 | .bind('focus', focus); 404 | cal 405 | .find('span').bind('mousedown', downIncrement).end() 406 | .find('>div.colorpicker_current_color').bind('click', restoreOriginal); 407 | options.selector = cal.find('div.colorpicker_color').bind('mousedown', downSelector); 408 | options.selectorIndic = options.selector.find('div div'); 409 | options.el = this; 410 | options.hue = cal.find('div.colorpicker_hue div'); 411 | cal.find('div.colorpicker_hue').bind('mousedown', downHue); 412 | options.newColor = cal.find('div.colorpicker_new_color'); 413 | options.currentColor = cal.find('div.colorpicker_current_color'); 414 | cal.data('colorpicker', options); 415 | cal.find('div.colorpicker_submit') 416 | .bind('mouseenter', enterSubmit) 417 | .bind('mouseleave', leaveSubmit) 418 | .bind('click', clickSubmit); 419 | fillRGBFields(options.color, cal.get(0)); 420 | fillHSBFields(options.color, cal.get(0)); 421 | fillHexFields(options.color, cal.get(0)); 422 | setHue(options.color, cal.get(0)); 423 | setSelector(options.color, cal.get(0)); 424 | setCurrentColor(options.color, cal.get(0)); 425 | setNewColor(options.color, cal.get(0)); 426 | if (options.flat) { 427 | cal.css({ 428 | position: 'relative', 429 | display: 'block' 430 | }); 431 | } else { 432 | $(this).bind(options.eventName, show); 433 | } 434 | } 435 | }); 436 | }, 437 | showPicker: function() { 438 | return this.each( function () { 439 | if ($(this).data('colorpickerId')) { 440 | show.apply(this); 441 | } 442 | }); 443 | }, 444 | hidePicker: function() { 445 | return this.each( function () { 446 | if ($(this).data('colorpickerId')) { 447 | $('#' + $(this).data('colorpickerId')).hide(); 448 | } 449 | }); 450 | }, 451 | setColor: function(col) { 452 | if (typeof col == 'string') { 453 | col = HexToHSB(col); 454 | } else if (col.r != undefined && col.g != undefined && col.b != undefined) { 455 | col = RGBToHSB(col); 456 | } else if (col.h != undefined && col.s != undefined && col.b != undefined) { 457 | col = fixHSB(col); 458 | } else { 459 | return this; 460 | } 461 | return this.each(function(){ 462 | if ($(this).data('colorpickerId')) { 463 | var cal = $('#' + $(this).data('colorpickerId')); 464 | cal.data('colorpicker').color = col; 465 | cal.data('colorpicker').origColor = col; 466 | fillRGBFields(col, cal.get(0)); 467 | fillHSBFields(col, cal.get(0)); 468 | fillHexFields(col, cal.get(0)); 469 | setHue(col, cal.get(0)); 470 | setSelector(col, cal.get(0)); 471 | setCurrentColor(col, cal.get(0)); 472 | setNewColor(col, cal.get(0)); 473 | } 474 | }); 475 | } 476 | }; 477 | }(); 478 | $.fn.extend({ 479 | ColorPicker: ColorPicker.init, 480 | ColorPickerHide: ColorPicker.hidePicker, 481 | ColorPickerShow: ColorPicker.showPicker, 482 | ColorPickerSetColor: ColorPicker.setColor 483 | }); 484 | })(jQuery) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Kevin Decker (http://incaseofstairs.com)", 3 | "name": "gradient-scanner", 4 | "version": "0.2.0", 5 | "homepage": "http://gradient-scanner.com", 6 | "repository": { 7 | "type": "git", 8 | "url": "git://github.com/kpdecker/gradient-scanner.git" 9 | }, 10 | "engines": { 11 | "node": "~0.6" 12 | }, 13 | "dependencies": { 14 | "express": "*" 15 | }, 16 | "devDependencies": {} 17 | } 18 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | var express = require("express"), 2 | http = require("http"), 3 | url = require("url"); 4 | 5 | var app = express.createServer(), 6 | port = process.env.PORT || 3000; 7 | 8 | app.configure(function(){ 9 | app.use(app.router); 10 | app.use(function(req, res, next) { 11 | // Filter out non-content. Ideally this would be in a subdir 12 | // but didn't want to move as the focus is the static client content. 13 | if ('/' === req.url 14 | || '/index.html' === req.url 15 | || '/index.htm' === req.url 16 | || /^\/css\//.test(req.url) 17 | || /^\/lib\//.test(req.url) 18 | || /^\/js\//.test(req.url)) { 19 | next(); 20 | } else { 21 | res.send(404); 22 | } 23 | }); 24 | app.use(express.static(__dirname)); 25 | }); 26 | 27 | app.get('/proxy', function(req, res){ 28 | var hrefParam = req.param("href"), 29 | href = url.parse(hrefParam), 30 | options = { 31 | host: href.hostname, 32 | port: href.port || 80, 33 | path: (href.pathname || "") + (href.search || "") 34 | }, 35 | len = 0; 36 | 37 | http.get(options, function(sourceRes) { 38 | sourceRes.on("data", function(chunk) { 39 | res.write(chunk); 40 | len += chunk.length; 41 | }); 42 | sourceRes.on("end", function() { 43 | console.info("proxied url", hrefParam, "length", len, "status", sourceRes.statusCode); 44 | res.end(); 45 | }); 46 | 47 | res.writeHead(sourceRes.statusCode, sourceRes.headers); 48 | }).on("error", function(e) { 49 | console.info("error", e); 50 | res.send(502); 51 | }); 52 | }); 53 | 54 | console.log("Starting gradient scanner server on port", port); 55 | app.listen(port); 56 | -------------------------------------------------------------------------------- /test/color-stops.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Kevin Decker (http://www.incaseofstairs.com/) 3 | * See LICENSE for license information 4 | */ 5 | $(document).ready(function(){ 6 | module("ColorStops"); 7 | 8 | test("getColorValue", function() { 9 | expect(12); 10 | equal(ColorStops.getColorValue([255, 0, 0, 255]), "#f00", "getColorValue(255, 0, 0, 255)"); 11 | equal(ColorStops.getColorValue([0, 255, 0, 255]), "#0f0", "getColorValue(0, 255, 0, 255)"); 12 | equal(ColorStops.getColorValue([0, 0, 255, 255]), "#00f", "getColorValue(0, 0, 255, 255)"); 13 | equal(ColorStops.getColorValue([254, 0, 0, 255]), "#fe0000", "getColorValue(255, 0, 0, 255)"); 14 | equal(ColorStops.getColorValue([0, 254, 0, 255]), "#00fe00", "getColorValue(0, 255, 0, 255)"); 15 | equal(ColorStops.getColorValue([0, 0, 254, 255]), "#0000fe", "getColorValue(0, 0, 255, 255)"); 16 | equal(ColorStops.getColorValue([0, 0, 0, 255]), "#000", "getColorValue(0, 0, 0, 255)"); 17 | 18 | equal(ColorStops.getColorValue([255, 0, 0, 0]), "rgba(255, 0, 0, 0)", "getColorValue(255, 0, 0, 0)"); 19 | equal(ColorStops.getColorValue([0, 255, 0, 0]), "rgba(0, 255, 0, 0)", "getColorValue(0, 255, 0, 0)"); 20 | equal(ColorStops.getColorValue([0, 0, 255, 0]), "rgba(0, 0, 255, 0)", "getColorValue(0, 0, 255, 0)"); 21 | equal(ColorStops.getColorValue([0, 0, 0, 0]), "rgba(0, 0, 0, 0)", "getColorValue(0, 0, 0, 0)"); 22 | 23 | equal(ColorStops.getColorValue([128.5, 128.5, 128.5, 128.5]), "rgba(128, 128, 128, 128)", "getColorValue(128.5, 128.5, 128.5, 128.5)"); 24 | }); 25 | 26 | test("generateCSS", function() { 27 | expect(24); 28 | var redToBlue = [ 29 | {position: 0, color: [255, 0, 0, 255]}, 30 | {position: 0.5, color: [255, 0, 255, 255], disabled: true}, 31 | {position: 1, color: [0, 0, 255, 255]} 32 | ], 33 | steppedRedToBlue = [ 34 | {position: 0.25, color: [255, 0, 0, 255]}, 35 | {position: 0.5, color: [255, 0, 255, 255], disabled: true}, 36 | {position: 0.75, color: [0, 0, 255, 255]} 37 | ]; 38 | 39 | // Colorstop Generation Tests 40 | deepEqual(ColorStops.generateCSS("linear", {x:0, y:0}, {x:0, y:"100%"}, redToBlue), [ 41 | "-webkit-gradient(linear, 0 0, 0 100%, from(#f00), to(#00f))", 42 | "-webkit-linear-gradient(#f00, #00f)", 43 | "-moz-linear-gradient(#f00, #00f)", 44 | '-o-linear-gradient(#f00, #00f)', 45 | 'linear-gradient(#f00, #00f)' 46 | ], "generateCSS(linear, {0,0}, {0,100%}, redToBlue)"); 47 | 48 | deepEqual(ColorStops.generateCSS("linear", {x:0, y:0}, {x:0, y:"100%"}, steppedRedToBlue), [ 49 | "-webkit-gradient(linear, 0 0, 0 100%, color-stop(0.25, #f00), color-stop(0.75, #00f))", 50 | "-webkit-linear-gradient(#f00 25%, #00f 75%)", 51 | "-moz-linear-gradient(#f00 25%, #00f 75%)", 52 | '-o-linear-gradient(#f00 25%, #00f 75%)', 53 | 'linear-gradient(#f00 25%, #00f 75%)' 54 | ], "generateCSS(linear, {0,0}, {0,100%}, steppedRedToBlue)"); 55 | 56 | // Position and angle Tests 57 | deepEqual(ColorStops.generateCSS("linear", {x:0, y:"100%"}, {x:0, y:0}, redToBlue), [ 58 | "-webkit-gradient(linear, 0 100%, 0 0, from(#f00), to(#00f))", 59 | "-webkit-linear-gradient(90deg, #f00, #00f)", 60 | "-moz-linear-gradient(90deg, #f00, #00f)", 61 | '-o-linear-gradient(90deg, #f00, #00f)', 62 | 'linear-gradient(90deg, #f00, #00f)' 63 | ], "generateCSS(linear, {0,100%}, {0,0}, redToBlue)"); 64 | 65 | deepEqual(ColorStops.generateCSS("linear", {x:0, y:0}, {x:"100%", y:0}, redToBlue), [ 66 | "-webkit-gradient(linear, 0 0, 100% 0, from(#f00), to(#00f))", 67 | "-webkit-linear-gradient(360deg, #f00, #00f)", 68 | "-moz-linear-gradient(360deg, #f00, #00f)", 69 | '-o-linear-gradient(360deg, #f00, #00f)', 70 | 'linear-gradient(360deg, #f00, #00f)' 71 | ], "generateCSS(linear, {0,0}, {100%, 0}, redToBlue)"); 72 | deepEqual(ColorStops.generateCSS("linear", {x:"100%", y:0}, {x:0, y:0}, redToBlue), [ 73 | "-webkit-gradient(linear, 100% 0, 0 0, from(#f00), to(#00f))", 74 | "-webkit-linear-gradient(180deg, #f00, #00f)", 75 | "-moz-linear-gradient(180deg, #f00, #00f)", 76 | '-o-linear-gradient(180deg, #f00, #00f)', 77 | 'linear-gradient(180deg, #f00, #00f)' 78 | ], "generateCSS(linear, {100%,0}, {0, 0}, redToBlue)"); 79 | 80 | deepEqual(ColorStops.generateCSS("linear", {x:0, y:0}, {x:"100%", y:"100%"}, redToBlue), [ 81 | "-webkit-gradient(linear, 0 0, 100% 100%, from(#f00), to(#00f))", 82 | "-webkit-linear-gradient(315deg, #f00, #00f)", 83 | "-moz-linear-gradient(315deg, #f00, #00f)", 84 | '-o-linear-gradient(315deg, #f00, #00f)', 85 | 'linear-gradient(315deg, #f00, #00f)' 86 | ], "generateCSS(linear, {0,0}, {100%, 100%}, redToBlue)"); 87 | deepEqual(ColorStops.generateCSS("linear", {x:"100%", y:"100%"}, {x:0, y:0}, redToBlue), [ 88 | "-webkit-gradient(linear, 100% 100%, 0 0, from(#f00), to(#00f))", 89 | "-webkit-linear-gradient(135deg, #f00, #00f)", 90 | "-moz-linear-gradient(135deg, #f00, #00f)", 91 | '-o-linear-gradient(135deg, #f00, #00f)', 92 | 'linear-gradient(135deg, #f00, #00f)' 93 | ], "generateCSS(linear, {100%, 100%}, {0,0}, redToBlue)"); 94 | 95 | // Positions that do not fill the entire region 96 | deepEqual(ColorStops.generateCSS("linear", {x:0, y:"25%"}, {x:0, y:"50%"}, redToBlue), [ 97 | "-webkit-gradient(linear, 0 25%, 0 50%, from(#f00), to(#00f))", 98 | "-webkit-linear-gradient(#f00 25%, #00f 50%)", 99 | "-moz-linear-gradient(#f00 25%, #00f 50%)", 100 | '-o-linear-gradient(#f00 25%, #00f 50%)', 101 | 'linear-gradient(#f00 25%, #00f 50%)' 102 | ], "generateCSS(linear, {0,25%}, {0,50%}, redToBlue)"); 103 | deepEqual(ColorStops.generateCSS("linear", {x:0, y:"50%"}, {x:0, y:"25%"}, redToBlue), [ 104 | "-webkit-gradient(linear, 0 50%, 0 25%, from(#f00), to(#00f))", 105 | "-webkit-linear-gradient(90deg, #f00 50%, #00f 75%)", 106 | "-moz-linear-gradient(90deg, #f00 50%, #00f 75%)", 107 | '-o-linear-gradient(90deg, #f00 50%, #00f 75%)', 108 | 'linear-gradient(90deg, #f00 50%, #00f 75%)' 109 | ], "generateCSS(linear, {0,50%}, {0,25%}, redToBlue)"); 110 | 111 | deepEqual(ColorStops.generateCSS("linear", {x:"25%", y:0}, {x:"50%", y:0}, redToBlue), [ 112 | "-webkit-gradient(linear, 25% 0, 50% 0, from(#f00), to(#00f))", 113 | "-webkit-linear-gradient(360deg, #f00 25%, #00f 50%)", 114 | "-moz-linear-gradient(360deg, #f00 25%, #00f 50%)", 115 | '-o-linear-gradient(360deg, #f00 25%, #00f 50%)', 116 | 'linear-gradient(360deg, #f00 25%, #00f 50%)' 117 | ], "generateCSS(linear, {25%,0}, {50%,0}, redToBlue)"); 118 | deepEqual(ColorStops.generateCSS("linear", {x:"50%", y:0}, {x:"25%", y:0}, redToBlue), [ 119 | "-webkit-gradient(linear, 50% 0, 25% 0, from(#f00), to(#00f))", 120 | "-webkit-linear-gradient(180deg, #f00 50%, #00f 75%)", 121 | "-moz-linear-gradient(180deg, #f00 50%, #00f 75%)", 122 | '-o-linear-gradient(180deg, #f00 50%, #00f 75%)', 123 | 'linear-gradient(180deg, #f00 50%, #00f 75%)' 124 | ], "generateCSS(linear, {50%,0}, {25%,0}, redToBlue)"); 125 | 126 | deepEqual(ColorStops.generateCSS("linear", {x:"25%", y:"25%"}, {x:"50%", y:"50%"}, redToBlue), [ 127 | "-webkit-gradient(linear, 25% 25%, 50% 50%, from(#f00), to(#00f))", 128 | "-webkit-linear-gradient(315deg, #f00 25%, #00f 50%)", 129 | "-moz-linear-gradient(315deg, #f00 25%, #00f 50%)", 130 | '-o-linear-gradient(315deg, #f00 25%, #00f 50%)', 131 | 'linear-gradient(315deg, #f00 25%, #00f 50%)' 132 | ], "generateCSS(linear, {25%,25%}, {50%,50%}, redToBlue)"); 133 | deepEqual(ColorStops.generateCSS("linear", {x:"50%", y:"50%"}, {x:"25%", y:"25%"}, redToBlue), [ 134 | "-webkit-gradient(linear, 50% 50%, 25% 25%, from(#f00), to(#00f))", 135 | "-webkit-linear-gradient(135deg, #f00 50%, #00f 75%)", 136 | "-moz-linear-gradient(135deg, #f00 50%, #00f 75%)", 137 | '-o-linear-gradient(135deg, #f00 50%, #00f 75%)', 138 | 'linear-gradient(135deg, #f00 50%, #00f 75%)' 139 | ], "generateCSS(linear, {50%,50%}, {25%,25%}, redToBlue)"); 140 | 141 | // Shifted offset 142 | deepEqual(ColorStops.generateCSS("linear", {x:"25%", y:"50%"}, {x:"50%", y:"75%"}, redToBlue), [ 143 | "-webkit-gradient(linear, 25% 50%, 50% 75%, from(#f00), to(#00f))", 144 | "-webkit-linear-gradient(315deg, #f00 37.5%, #00f 62.5%)", 145 | "-moz-linear-gradient(315deg, #f00 37.5%, #00f 62.5%)", 146 | '-o-linear-gradient(315deg, #f00 37.5%, #00f 62.5%)', 147 | 'linear-gradient(315deg, #f00 37.5%, #00f 62.5%)' 148 | ], "generateCSS(linear, {25%,50%}, {50%,75%}, redToBlue)"); 149 | 150 | // Stepped partial 151 | deepEqual(ColorStops.generateCSS("linear", {x:0, y:"25%"}, {x:0, y:"50%"}, steppedRedToBlue), [ 152 | "-webkit-gradient(linear, 0 25%, 0 50%, color-stop(0.25, #f00), color-stop(0.75, #00f))", 153 | "-webkit-linear-gradient(#f00 31.2%, #00f 43.7%)", 154 | "-moz-linear-gradient(#f00 31.2%, #00f 43.7%)", 155 | '-o-linear-gradient(#f00 31.2%, #00f 43.7%)', 156 | 'linear-gradient(#f00 31.2%, #00f 43.7%)' 157 | ], "generateCSS(linear, {0,25%}, {0,50%}, steppedRedToBlue)"); 158 | 159 | // Non-percentage positions 160 | deepEqual(ColorStops.generateCSS("linear", {x:25, y:0}, {x:50, y:0}, redToBlue), [ 161 | "-webkit-gradient(linear, 25 0, 50 0, from(#f00), to(#00f))", 162 | "-webkit-linear-gradient(360deg, #f00 50%, #00f)", 163 | "-moz-linear-gradient(360deg, #f00 50%, #00f)", 164 | '-o-linear-gradient(360deg, #f00 50%, #00f)', 165 | 'linear-gradient(360deg, #f00 50%, #00f)' 166 | ], "generateCSS(linear, {25,0}, {50,0}, redToBlue)"); 167 | deepEqual(ColorStops.generateCSS("linear", {x:0, y:25}, {x:0, y:50}, redToBlue), [ 168 | "-webkit-gradient(linear, 0 25, 0 50, from(#f00), to(#00f))", 169 | "-webkit-linear-gradient(#f00 50%, #00f)", 170 | "-moz-linear-gradient(#f00 50%, #00f)", 171 | '-o-linear-gradient(#f00 50%, #00f)', 172 | 'linear-gradient(#f00 50%, #00f)' 173 | ], "generateCSS(linear, {0,25}, {0,50}, redToBlue)"); 174 | deepEqual(ColorStops.generateCSS("linear", {x:25, y:25}, {x:50, y:50}, redToBlue), [ 175 | "-webkit-gradient(linear, 25 25, 50 50, from(#f00), to(#00f))", 176 | "-webkit-linear-gradient(315deg, #f00 50%, #00f)", 177 | "-moz-linear-gradient(315deg, #f00 50%, #00f)", 178 | '-o-linear-gradient(315deg, #f00 50%, #00f)', 179 | 'linear-gradient(315deg, #f00 50%, #00f)' 180 | ], "generateCSS(linear, {25,25}, {50,50}, redToBlue)"); 181 | deepEqual(ColorStops.generateCSS("linear", {x:50, y:50}, {x:25, y:25}, redToBlue), [ 182 | "-webkit-gradient(linear, 50 50, 25 25, from(#f00), to(#00f))", 183 | "-webkit-linear-gradient(135deg, #f00, #00f 50%)", 184 | "-moz-linear-gradient(135deg, #f00, #00f 50%)", 185 | '-o-linear-gradient(135deg, #f00, #00f 50%)', 186 | 'linear-gradient(135deg, #f00, #00f 50%)' 187 | ], "generateCSS(linear, {50,50}, {25,25}, redToBlue)"); 188 | deepEqual(ColorStops.generateCSS("linear", {x:25, y:25}, {x:50, y:50}, redToBlue, {x:0,y:0, width:100,height:100}), [ 189 | "-webkit-gradient(linear, 25 25, 50 50, from(#f00), to(#00f))", 190 | "-webkit-linear-gradient(315deg, #f00 25%, #00f 50%)", 191 | "-moz-linear-gradient(315deg, #f00 25%, #00f 50%)", 192 | '-o-linear-gradient(315deg, #f00 25%, #00f 50%)', 193 | 'linear-gradient(315deg, #f00 25%, #00f 50%)' 194 | ], "generateCSS(linear, {25,25}, {50,50}, redToBlue, {0,0,100,100})"); 195 | deepEqual(ColorStops.generateCSS("linear", {x:50, y:50}, {x:25, y:25}, redToBlue, {x:0,y:0, width:100,height:100}), [ 196 | "-webkit-gradient(linear, 50 50, 25 25, from(#f00), to(#00f))", 197 | "-webkit-linear-gradient(135deg, #f00 50%, #00f 75%)", 198 | "-moz-linear-gradient(135deg, #f00 50%, #00f 75%)", 199 | '-o-linear-gradient(135deg, #f00 50%, #00f 75%)', 200 | 'linear-gradient(135deg, #f00 50%, #00f 75%)' 201 | ], "generateCSS(linear, {50,50}, {25,25}, redToBlue, {0,0,100,100})"); 202 | 203 | deepEqual(ColorStops.generateCSS("linear", {x:25.25, y:0}, {x:50.5, y:0}, redToBlue), [ 204 | "-webkit-gradient(linear, 25.25 0, 50.5 0, from(#f00), to(#00f))", 205 | "-webkit-linear-gradient(360deg, #f00 50%, #00f)", 206 | "-moz-linear-gradient(360deg, #f00 50%, #00f)", 207 | '-o-linear-gradient(360deg, #f00 50%, #00f)', 208 | 'linear-gradient(360deg, #f00 50%, #00f)' 209 | ], "generateCSS(linear, {25.25,0}, {50.5,0}, redToBlue)"); 210 | 211 | // Position spacing 212 | var equallySpaced = [ 213 | {position: 0, color: [255, 0, 0, 255]}, 214 | {position: 0.5, color: [255, 0, 255, 255]}, 215 | {position: 1, color: [0, 0, 255, 255]} 216 | ], 217 | notEquallySpaced = [ 218 | {position: 0, color: [255, 0, 0, 255]}, 219 | {position: 0.5, color: [255, 0, 255, 255]}, 220 | {position: 0.75, color: [0, 0, 255, 255]} 221 | ]; 222 | deepEqual(ColorStops.generateCSS("linear", {x:0, y:0}, {x:0, y:"100%"}, equallySpaced), [ 223 | "-webkit-gradient(linear, 0 0, 0 100%, from(#f00), color-stop(0.5, #f0f), to(#00f))", 224 | "-webkit-linear-gradient(#f00, #f0f, #00f)", 225 | "-moz-linear-gradient(#f00, #f0f, #00f)", 226 | '-o-linear-gradient(#f00, #f0f, #00f)', 227 | 'linear-gradient(#f00, #f0f, #00f)' 228 | ], "generateCSS(linear, {0,0}, {0,100%}, equallySpaced)"); 229 | deepEqual(ColorStops.generateCSS("linear", {x:0, y:0}, {x:0, y:"100%"}, notEquallySpaced), [ 230 | "-webkit-gradient(linear, 0 0, 0 100%, from(#f00), color-stop(0.5, #f0f), color-stop(0.75, #00f))", 231 | "-webkit-linear-gradient(#f00, #f0f 50%, #00f 75%)", 232 | "-moz-linear-gradient(#f00, #f0f 50%, #00f 75%)", 233 | '-o-linear-gradient(#f00, #f0f 50%, #00f 75%)', 234 | 'linear-gradient(#f00, #f0f 50%, #00f 75%)' 235 | ], "generateCSS(linear, {0,0}, {0,100%}, notEquallySpaced)"); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /test/css-gradient-dropdown-menu.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kpdecker/gradient-scanner/33136b64375667523d7b6f2338870617e1222d69/test/css-gradient-dropdown-menu.gif -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | gradient-scanner tests 10 | 11 | 12 | 13 |

gradient-scanner tests

14 |

15 |
16 |

17 |
    18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /test/line-utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Kevin Decker (http://www.incaseofstairs.com/) 3 | * See LICENSE for license information 4 | */ 5 | $(document).ready(function(){ 6 | module("LineUtils"); 7 | 8 | test("getUnit", function() { 9 | expect(16); 10 | equals(LineUtils.getUnit(0), 0, "getUnit(0)"); 11 | equals(LineUtils.getUnit("0"), 0, "getUnit('0')"); 12 | equals(LineUtils.getUnit("0px"), 0, "getUnit(0px)"); 13 | equals(LineUtils.getUnit("0%"), 0, "getUnit(0%)"); 14 | 15 | equals(LineUtils.getUnit(1), "px", "getUnit(1)"); 16 | equals(LineUtils.getUnit("1"), "px", "getUnit('1')"); 17 | equals(LineUtils.getUnit("1px"), "px", "getUnit(1px)"); 18 | equals(LineUtils.getUnit("1%"), "%", "getUnit(1%)"); 19 | 20 | equals(LineUtils.getUnit(0.5), "px", "getUnit(0.5)"); 21 | equals(LineUtils.getUnit("0.5"), "px", "getUnit('0.5')"); 22 | equals(LineUtils.getUnit("0.5px"), "px", "getUnit(0.5px)"); 23 | equals(LineUtils.getUnit("0.5%"), "%", "getUnit(0.5%)"); 24 | 25 | equals(LineUtils.getUnit(1.5), "px", "getUnit(1.5)"); 26 | equals(LineUtils.getUnit("1.5"), "px", "getUnit('1.5')"); 27 | equals(LineUtils.getUnit("1.5px"), "px", "getUnit(1.5px)"); 28 | equals(LineUtils.getUnit("1.5%"), "%", "getUnit(1.5%)"); 29 | }); 30 | 31 | test("containingRect", function() { 32 | expect(27); 33 | deepEqual(LineUtils.containingRect({x:0, y:0}, {x:0, y:1}), {x:0,y:0, width:1,height:1}, "containingRect({0,0}, {0,1})"); 34 | deepEqual(LineUtils.containingRect({x:0, y:0}, {x:1, y:0}), {x:0,y:0, width:1,height:1}, "containingRect({0,0}, {1,0})"); 35 | deepEqual(LineUtils.containingRect({x:0, y:0}, {x:1, y:1}), {x:0,y:0, width:1,height:1}, "containingRect({0,0}, {1,1})"); 36 | deepEqual(LineUtils.containingRect({x:1, y:1}, {x:0, y:0}), {x:0,y:0, width:1,height:1}, "containingRect({1,1}, {0,0})"); 37 | deepEqual(LineUtils.containingRect({x:1, y:0}, {x:0, y:1}), {x:0,y:0, width:1,height:1}, "containingRect({1,0}, {0,1})"); 38 | deepEqual(LineUtils.containingRect({x:0, y:1}, {x:1, y:0}), {x:0,y:0, width:1,height:1}, "containingRect({0,1}, {1,0})"); 39 | 40 | deepEqual(LineUtils.containingRect({x:0, y:0}, {x:0, y:10}, 10), {x:-5,y:0, width:10,height:10}, "containingRect({0,0}, {0,10}, 10)"); 41 | deepEqual(LineUtils.containingRect({x:0, y:0}, {x:10, y:0}, 10), {x:0,y:-5, width:10,height:10}, "containingRect({0,0}, {10,0}, 10)"); 42 | deepEqual(LineUtils.containingRect({x:0, y:0}, {x:10, y:10}, 10), {x:-3,y:-3, width:16,height:16}, "containingRect({0,0}, {10,10}, 10)"); 43 | deepEqual(LineUtils.containingRect({x:10, y:10}, {x:0, y:0}, 10), {x:-3,y:-3, width:16,height:16}, "containingRect({10,10}, {0,0}, 10)"); 44 | deepEqual(LineUtils.containingRect({x:10, y:0}, {x:0, y:10}, 10), {x:-3,y:-3, width:16,height:16}, "containingRect({10,0}, {0,10}, 10)"); 45 | deepEqual(LineUtils.containingRect({x:0, y:10}, {x:10, y:0}, 10), {x:-3,y:-3, width:16,height:16}, "containingRect({0,10}, {10,0}, 10)"); 46 | 47 | deepEqual(LineUtils.containingRect({x:0, y:0}, {x:0, y:"1%"}), {x:0,y:0, width:"1%",height:"1%"}, "containingRect({0,0}, {0,1%})"); 48 | deepEqual(LineUtils.containingRect({x:0, y:0}, {x:"1%", y:0}), {x:0,y:0, width:"1%",height:"1%"}, "containingRect({0,0}, {1%,0})"); 49 | deepEqual(LineUtils.containingRect({x:0, y:0}, {x:"1%", y:"1%"}), {x:0,y:0, width:"1%",height:"1%"}, "containingRect({0,0}, {1%,1%})"); 50 | deepEqual(LineUtils.containingRect({x:"1%", y:"1%"}, {x:0, y:0}), {x:0,y:0, width:"1%",height:"1%"}, "containingRect({1%,1%}, {0,0})"); 51 | deepEqual(LineUtils.containingRect({x:"1%", y:0}, {x:0, y:"1%"}), {x:0,y:0, width:"1%",height:"1%"}, "containingRect({1%,0}, {0,1%})"); 52 | deepEqual(LineUtils.containingRect({x:0, y:"1%"}, {x:"1%", y:0}), {x:0,y:0, width:"1%",height:"1%"}, "containingRect({0,1%}, {1%,0})"); 53 | 54 | deepEqual(LineUtils.containingRect({x:0, y:0}, {x:0, y:"10%"}, "10%"), {x:"-5%",y:0, width:"10%",height:"10%"}, "containingRect({0,0}, {0,10%}, 10%)"); 55 | deepEqual(LineUtils.containingRect({x:0, y:0}, {x:"10%", y:0}, "10%"), {x:0,y:"-5%", width:"10%",height:"10%"}, "containingRect({0,0}, {10%,0}, 10%)"); 56 | deepEqual(LineUtils.containingRect({x:0, y:0}, {x:"10%", y:"10%"}, "10%"), {x:"-3%",y:"-3%", width:"16%",height:"16%"}, "containingRect({0,0}, {10%,10%}, 10%)"); 57 | deepEqual(LineUtils.containingRect({x:"10%", y:"10%"}, {x:0, y:0}, "10%"), {x:"-3%",y:"-3%", width:"16%",height:"16%"}, "containingRect({10%,10%}, {0,0}, 10%)"); 58 | deepEqual(LineUtils.containingRect({x:"10%", y:0}, {x:0, y:"10%"}, "10%"), {x:"-3%",y:"-3%", width:"16%",height:"16%"}, "containingRect({10%,0}, {0,10%}, 10%)"); 59 | deepEqual(LineUtils.containingRect({x:0, y:"10%"}, {x:"10%", y:0}, "10%"), {x:"-3%",y:"-3%", width:"16%",height:"16%"}, "containingRect({0,10%}, {10%,0}, 10%)"); 60 | 61 | deepEqual(LineUtils.containingRect({x:0, y:0}, {x:0, y:1.5}), {x:0,y:0, width:1,height:1.5}, "containingRect({0,0}, {0,1.5})"); 62 | 63 | deepEqual(LineUtils.containingRect({x:"1%", y:0}, {x:0, y:1}), NaN, "containingRect({1%,0}, {0,1})"); 64 | deepEqual(LineUtils.containingRect({x:"1%", y:0}, {x:0, y:"1%"}, 1), NaN, "containingRect({1%,0}, {0,1%}, 1)"); 65 | }); 66 | 67 | test("relativeCoords", function() { 68 | expect(7); 69 | deepEqual(LineUtils.relativeCoords({x:0, y:0}, {x:0, y:0}), {x:0,y:0}, "relativeCoords({0,0}, {0,0})"); 70 | deepEqual(LineUtils.relativeCoords({x:0, y:0}, {x:1, y:1}), {x:-1,y:-1}, "relativeCoords({0,0}, {1,1})"); 71 | deepEqual(LineUtils.relativeCoords({x:1, y:1}, {x:0, y:0}), {x:1,y:1}, "relativeCoords({1,1}, {0,0})"); 72 | 73 | deepEqual(LineUtils.relativeCoords({x:1, y:1}, {x:0.5, y:0.5}), {x:0.5,y:0.5}, "relativeCoords({1,1}, {0.5,0.5})"); 74 | 75 | deepEqual(LineUtils.relativeCoords({x:0, y:0}, {x:"1%", y:"1%"}), {x:"-1%",y:"-1%"}, "relativeCoords({0,0}, {1%,1%})"); 76 | deepEqual(LineUtils.relativeCoords({x:"1%", y:"1%"}, {x:0, y:0}), {x:"1%",y:"1%"}, "relativeCoords({1%,1%}, {0,0})"); 77 | 78 | deepEqual(LineUtils.relativeCoords({x:"1%", y:0}, {x:0, y:1}), NaN, "relativeCoords({1%,0}, {0,1})"); 79 | }); 80 | 81 | test("distance", function() { 82 | // Testing conditional logic only, assuming that the math is correct 83 | expect(14); 84 | equals(LineUtils.distance({x:0, y:0}, {x:2, y:0}), 2, "distance({0,0}, {2,0})"); 85 | equals(LineUtils.distance({x:2, y:0}, {x:0, y:0}), 2, "distance({2,0}, {0,0})"); 86 | 87 | equals(LineUtils.distance({x:0, y:0}, {x:0, y:2}), 2, "distance({0,0}, {0,2})"); 88 | equals(LineUtils.distance({x:0, y:2}, {x:0, y:0}), 2, "distance({0,2}, {0,0})"); 89 | 90 | equals(LineUtils.distance({x:0, y:0}, {x:3, y:4}), 5, "distance({0,0}, {3,4})"); 91 | equals(LineUtils.distance({x:3, y:4}, {x:0, y:0}), 5, "distance({3,4}, {0,0})"); 92 | 93 | equals(LineUtils.distance({x:0, y:0}, {x:4, y:3}), 5, "distance({0,0}, {4,3})"); 94 | equals(LineUtils.distance({x:4, y:3}, {x:0, y:0}), 5, "distance({4,3}, {0,0})"); 95 | 96 | equals(LineUtils.distance({x:0, y:0}, {x:"1%", y:0}), 1, "distance({0,0}, {1%,0})"); 97 | equals(LineUtils.distance({x:"1%", y:0}, {x:0, y:0}), 1, "distance({1%,0}, {0,0})"); 98 | 99 | equals(LineUtils.distance({x:0, y:0}, {x:0, y:"1%"}), 1, "distance({0,0}, {0,1%})"); 100 | equals(LineUtils.distance({x:0, y:"1%"}, {x:0, y:0}), 1, "distance({0,1%}, {0,0})"); 101 | 102 | equals(LineUtils.distance({x:0, y:2.5}, {x:0, y:0}), 2.5, "distance({0,2.5}, {0,0})"); 103 | 104 | deepEqual(LineUtils.distance({x:"1%", y:"1%"}, {x:1, y:0}), NaN, "distance({1%,1%}, {1,0})"); 105 | }); 106 | 107 | test("percentageOfRay", function() { 108 | expect(17); 109 | 110 | equals(LineUtils.percentageOfRay({x:0, y:0}, {x:0, y:1}, {x:0,y:0, width:1,height:1}), 1, "percentageOfRay({0,0}, {0,1}, {0,0,1,1})"); 111 | equals(LineUtils.percentageOfRay({x:0, y:0}, {x:1, y:0}, {x:0,y:0, width:1,height:1}), 1, "percentageOfRay({0,0}, {1,0}, {0,0,1,1})"); 112 | equals(LineUtils.percentageOfRay({x:0, y:0}, {x:1, y:1}, {x:0,y:0, width:1,height:1}), 1, "percentageOfRay({0,0}, {1,1}, {0,0,1,1})"); 113 | equals(LineUtils.percentageOfRay({x:1, y:1}, {x:0, y:0}, {x:0,y:0, width:1,height:1}), 1, "percentageOfRay({1,1}, {0,0}, {0,0,1,1})"); 114 | equals(LineUtils.percentageOfRay({x:1, y:0}, {x:0, y:1}, {x:0,y:0, width:1,height:1}), 1, "percentageOfRay({1,0}, {0,1}, {0,0,1,1})"); 115 | equals(LineUtils.percentageOfRay({x:0, y:1}, {x:1, y:0}, {x:0,y:0, width:1,height:1}), 1, "percentageOfRay({0,1}, {1,0}, {0,0,1,1})"); 116 | 117 | // No intercepts 118 | equals(LineUtils.percentageOfRay({x:0, y:10}, {x:10, y:10}, {x:0,y:0, width:1,height:1}), 0, "percentageOfRay({0,10}, {10,10}, {0,0,1,1})"); 119 | equals(LineUtils.percentageOfRay({x:10, y:0}, {x:10, y:10}, {x:0,y:0, width:1,height:1}), 0, "percentageOfRay({10,0}, {10,10}, {0,0,1,1})"); 120 | 121 | // Not connected to the edge cases 122 | equals(LineUtils.percentageOfRay({x:1, y:2}, {x:2, y:3}, {x:0,y:0, width:10,height:10}), 0.125, "percentageOfRay({1,2}, {2,3}, {0,0,10,10})"); 123 | equals(LineUtils.percentageOfRay({x:2, y:3}, {x:1, y:2}, {x:0,y:0, width:10,height:10}), 0.5, "percentageOfRay({2,3}, {1,2}, {0,0,10,10})"); 124 | 125 | equals(LineUtils.percentageOfRay({x:8, y:2}, {x:6, y:4}, {x:0,y:0, width:10,height:10}), 0.25, "percentageOfRay({8,2}, {6,4}, {0,0,10,10})"); 126 | equals(LineUtils.percentageOfRay({x:8, y:2}, {x:9, y:1}, {x:0,y:0, width:10,height:10}), 0.5, "percentageOfRay({2,8}, {9,1}, {0,0,10,10})"); 127 | 128 | equals(LineUtils.percentageOfRay({x:8, y:2}, {x:6, y:4}, {x:-1,y:-1, width:11,height:11}), 0.25, "percentageOfRay({8,2}, {6,4}, {-1,-1,11,11})"); 129 | equals(LineUtils.percentageOfRay({x:8, y:2}, {x:9, y:1}, {x:-1,y:-1, width:11,height:11}), 0.5, "percentageOfRay({2,8}, {9,1}, {-1,-1,11,11})"); 130 | 131 | // Units 132 | equals(LineUtils.percentageOfRay({x:0, y:0}, {x:0, y:"1%"}, {x:0,y:0, width:"1%",height:"1%"}), 1, "percentageOfRay({0,0}, {0,1%}, {0,0,1%,1%})"); 133 | equals(LineUtils.percentageOfRay({x:0, y:0}, {x:"1%", y:"1%"}, {x:0,y:0, width:"1%",height:"1%"}), 1, "percentageOfRay({0,0}, {1%,1%}, {0,0,1%,1%})"); 134 | deepEqual(LineUtils.percentageOfRay({x:0, y:0}, {x:0, y:"1%"}, {x:0,y:0, width:1,height:1}), NaN, "percentageOfRay({0,0}, {0,1%}, {0,0,1,1})"); 135 | }); 136 | 137 | 138 | test("isOnEdge", function() { 139 | expect(29); 140 | 141 | equals(LineUtils.isOnEdge({x:0, y:0}, {x:0,y:0, width:10,height:10}), true, "isOnEdge({0,0}, {10,10})"); 142 | equals(LineUtils.isOnEdge({x:1, y:1}, {x:0,y:0, width:10,height:10}), false, "isOnEdge({1,1}, {10,10})"); 143 | equals(LineUtils.isOnEdge({x:1, y:0}, {x:0,y:0, width:10,height:10}), true, "isOnEdge({1,0}, {10,10})"); 144 | equals(LineUtils.isOnEdge({x:0, y:1}, {x:0,y:0, width:10,height:10}), true, "isOnEdge({0,1}, {10,10})"); 145 | equals(LineUtils.isOnEdge({x:10, y:10}, {x:0,y:0, width:10,height:10}), true, "isOnEdge({10,10}, {10,10})"); 146 | equals(LineUtils.isOnEdge({x:10, y:0}, {x:0,y:0, width:10,height:10}), true, "isOnEdge({10,0}, {10,10})"); 147 | equals(LineUtils.isOnEdge({x:0, y:10}, {x:0,y:0, width:10,height:10}), true, "isOnEdge({0,10}, {10,10})"); 148 | equals(LineUtils.isOnEdge({x:-10, y:-10}, {x:0,y:0, width:10,height:10}), false, "isOnEdge({-10,-10}, {10,10})"); 149 | equals(LineUtils.isOnEdge({x:-10, y:0}, {x:0,y:0, width:10,height:10}), false, "isOnEdge({-10,0}, {10,10})"); 150 | equals(LineUtils.isOnEdge({x:0, y:-10}, {x:0,y:0, width:10,height:10}), false, "isOnEdge({0,-10}, {10,10})"); 151 | equals(LineUtils.isOnEdge({x:20, y:20}, {x:0,y:0, width:10,height:10}), false, "isOnEdge({20,20}, {10,10})"); 152 | equals(LineUtils.isOnEdge({x:20, y:0}, {x:0,y:0, width:10,height:10}), false, "isOnEdge({20,0}, {10,10})"); 153 | equals(LineUtils.isOnEdge({x:0, y:20}, {x:0,y:0, width:10,height:10}), false, "isOnEdge({0,20}, {10,10})"); 154 | 155 | equals(LineUtils.isOnEdge({x:0, y:0}, {x:0,y:0, width:"10%",height:"10%"}), true, "isOnEdge({0,0}, {10%,10%})"); 156 | equals(LineUtils.isOnEdge({x:"1%", y:"1%"}, {x:0,y:0, width:"10%",height:"10%"}), false, "isOnEdge({1%,1%}, {10%,10%})"); 157 | equals(LineUtils.isOnEdge({x:"1%", y:0}, {x:0,y:0, width:"10%",height:"10%"}), true, "isOnEdge({1%,0}, {10%,10%})"); 158 | equals(LineUtils.isOnEdge({x:0, y:"1%"}, {x:0,y:0, width:"10%",height:"10%"}), true, "isOnEdge({0,1%}, {10%,10%})"); 159 | equals(LineUtils.isOnEdge({x:"10%", y:"10%"}, {x:0,y:0, width:"10%",height:"10%"}), true, "isOnEdge({10%,1%0%}, {10%,10%})"); 160 | equals(LineUtils.isOnEdge({x:"10%", y:0}, {x:0,y:0, width:"10%",height:"10%"}), true, "isOnEdge({10%,0}, {10%,10%})"); 161 | equals(LineUtils.isOnEdge({x:0, y:"10%"}, {x:0,y:0, width:"10%",height:"10%"}), true, "isOnEdge({0,10%}, {10%,10%})"); 162 | equals(LineUtils.isOnEdge({x:"-10%", y:"-10%"}, {x:0,y:0, width:"10%",height:"10%"}), false, "isOnEdge({-10%,-10%}, {10%,10%})"); 163 | equals(LineUtils.isOnEdge({x:"-10%", y:0}, {x:0,y:0, width:"10%",height:"10%"}), false, "isOnEdge({-10%,0}, {10%,10%})"); 164 | equals(LineUtils.isOnEdge({x:0, y:"-10%"}, {x:0,y:0, width:"10%",height:"10%"}), false, "isOnEdge({0,-10%}, {10%,10%})"); 165 | equals(LineUtils.isOnEdge({x:"20%", y:"20%"}, {x:0,y:0, width:"10%",height:"10%"}), false, "isOnEdge({20%,20%}, {10%,10%})"); 166 | equals(LineUtils.isOnEdge({x:"20%", y:0}, {x:0,y:0, width:"10%",height:"10%"}), false, "isOnEdge({20%,0}, {10%,10%})"); 167 | equals(LineUtils.isOnEdge({x:0, y:"20%"}, {x:0,y:0, width:"10%",height:"10%"}), false, "isOnEdge({0,20%}, {10%,10%})"); 168 | 169 | equals(LineUtils.isOnEdge({x:10.5, y:10.5}, {x:0,y:0, width:10.5,height:10.5}), true, "isOnEdge({10.5,10.5}, {10.5,10.5})"); 170 | equals(LineUtils.isOnEdge({x:1.5, y:1.5}, {x:0,y:0, width:10.5,height:10.5}), false, "isOnEdge({1.5,1.5}, {10.5,10.5})"); 171 | 172 | deepEqual(LineUtils.isOnEdge({x:"1%", y:0}, {x:0, y:1, width:10,height:10}), NaN, "isOnEdge({1%,0}, {0,1})"); 173 | }); 174 | 175 | test("gradientPoints", function() { 176 | expect(17); 177 | 178 | deepEqual( 179 | LineUtils.gradientPoints({x:0, y:0}, {x:0, y:1}, {x:0,y:0, width:1,height:1}), 180 | {start: {x:0.5, y:0}, startOff: 0, end: {x:0.5, y:1}, scale: 1}, 181 | "gradientPoints({0,0}, {0,1}, {0,0,1,1})"); 182 | deepEqual( 183 | LineUtils.gradientPoints({x:0, y:0}, {x:1, y:0}, {x:0,y:0, width:1,height:1}), 184 | {start: {x:0, y:0.5}, startOff: 0, end: {x:1, y:0.5}, scale: 1}, 185 | "gradientPoints({0,0}, {1,0}, {0,0,1,1})"); 186 | deepEqual( 187 | LineUtils.gradientPoints({x:0, y:0}, {x:1, y:1}, {x:0,y:0, width:1,height:1}), 188 | {start: {x:0, y:0}, startOff: 0, end: {x:1, y:1}, scale: 1}, 189 | "gradientPoints({0,0}, {1,1}, {0,0,1,1})"); 190 | deepEqual( 191 | LineUtils.gradientPoints({x:1, y:1}, {x:0, y:0}, {x:0,y:0, width:1,height:1}), 192 | {start: {x:1, y:1}, startOff: 0, end: {x:0, y:0}, scale: 1}, 193 | "gradientPoints({1,1}, {0,0}, {0,0,1,1})"); 194 | deepEqual( 195 | LineUtils.gradientPoints({x:1, y:0}, {x:0, y:1}, {x:0,y:0, width:1,height:1}), 196 | {start: {x:1, y:0}, startOff: 0, end: {x:0, y:1}, scale: 1}, 197 | "gradientPoints({1,0}, {0,1}, {0,0,1,1})"); 198 | deepEqual( 199 | LineUtils.gradientPoints({x:0, y:1}, {x:1, y:0}, {x:0,y:0, width:1,height:1}), 200 | {start: {x:0, y:1}, startOff: 0, end: {x:1, y:0}, scale: 1}, 201 | "gradientPoints({0,1}, {1,0}, {0,0,1,1})"); 202 | 203 | // No intercepts 204 | deepEqual( 205 | LineUtils.gradientPoints({x:0, y:10}, {x:10, y:10}, {x:0,y:0, width:1,height:1}), 206 | {start: {x:0,y:0.5}, startOff: 0, end: {x:1,y:0.5}, scale: 10}, 207 | "gradientPoints({0,10}, {10,10}, {0,0,1,1})"); 208 | deepEqual( 209 | LineUtils.gradientPoints({x:10, y:0}, {x:10, y:10}, {x:0,y:0, width:1,height:1}), 210 | {start: {x:0.5,y:0}, startOff: 0, end: {x:0.5,y:1}, scale: 10}, 211 | "gradientPoints({10,0}, {10,10}, {0,0,1,1})"); 212 | 213 | // Not connected to the edge cases 214 | deepEqual( 215 | LineUtils.gradientPoints({x:1, y:2}, {x:2, y:3}, {x:0,y:0, width:10,height:10}), 216 | {start: {x:0, y:0}, startOff: 0.15, end: {x:10, y:10}, scale: 0.1}, 217 | "gradientPoints({1,2}, {2,3}, {0,0,10,10})"); 218 | deepEqual( 219 | LineUtils.gradientPoints({x:2, y:3}, {x:1, y:2}, {x:0,y:0, width:10,height:10}), 220 | {start: {x:10, y:10}, startOff: 0.75, end: {x:0, y:0}, scale: 0.1}, 221 | "gradientPoints({2,3}, {1,2}, {0,0,10,10})"); 222 | 223 | deepEqual( 224 | LineUtils.gradientPoints({x:9, y:1}, {x:8, y:2}, {x:0,y:0, width:10,height:10}), 225 | {start: {x:10, y:0}, startOff: 0.1, end: {x:0, y:10}, scale: 0.1}, 226 | "gradientPoints({9,1}, {8,2}, {0,0,10,10})"); 227 | deepEqual( 228 | LineUtils.gradientPoints({x:8, y:2}, {x:9, y:1}, {x:0,y:0, width:10,height:10}), 229 | {start: {x:0, y:10}, startOff: 0.8, end: {x:10, y:0}, scale: 0.1}, 230 | "gradientPoints({2,8}, {9,1}, {0,0,10,10})"); 231 | 232 | deepEqual( 233 | LineUtils.gradientPoints({x:9, y:1}, {x:8, y:2}, {x:-1,y:-1, width:11,height:11}), 234 | {start: {x:10, y:-1}, startOff: 0.13636363636363635, end: {x:-1, y:10}, scale: 0.09090909090909091}, 235 | "gradientPoints({9,1}, {8,2}, {-1,-1,11,11})"); 236 | deepEqual( 237 | LineUtils.gradientPoints({x:8, y:2}, {x:9, y:1}, {x:-1,y:-1, width:11,height:11}), 238 | {start: {x:-1, y:10}, startOff: 0.7727272727272727, end: {x:10, y:-1}, scale: 0.09090909090909091}, 239 | "gradientPoints({2,8}, {9,1}, {-1,-1,11,11})"); 240 | 241 | // Units 242 | deepEqual( 243 | LineUtils.gradientPoints({x:0, y:0}, {x:0, y:"1%"}, {x:0,y:0, width:"1%",height:"1%"}), 244 | {start: {x:"0.5%", y:0}, startOff: 0, end: {x:"0.5%", y:"1%"}, scale: 1}, 245 | "gradientPoints({0,0}, {0,1%}, {0,0,1%,1%})"); 246 | deepEqual( 247 | LineUtils.gradientPoints({x:0, y:0}, {x:"1%", y:"1%"}, {x:0,y:0, width:"1%",height:"1%"}), 248 | {start: {x:0, y:0}, startOff: 0, end: {x:"1%", y:"1%"}, scale: 1}, 249 | "gradientPoints({0,0}, {1%,1%}, {0,0,1%,1%})"); 250 | deepEqual( 251 | LineUtils.gradientPoints({x:0, y:0}, {x:0, y:"1%"}, {x:0,y:0, width:1,height:1}), 252 | NaN, 253 | "gradientPoints({0,0}, {0,1%}, {0,0,1,1})"); 254 | 255 | // TODO : Angles that resolve to something other than a direct corner 256 | }); 257 | 258 | test("lineIntercepts", function() { 259 | expect(29); 260 | 261 | deepEqual(LineUtils.lineIntercepts({x:0, y:0}, {x:0, y:1}, {x:0,y:0, width:1,height:1}), [{x:0, y:0}, {x:0, y:1}], "lineIntercepts({0,0}, {0,1}, {0,0,1,1})"); 262 | deepEqual(LineUtils.lineIntercepts({x:0, y:0}, {x:1, y:0}, {x:0,y:0, width:1,height:1}), [{x:0, y:0}, {x:1, y:0}], "lineIntercepts({0,0}, {1,0}, {0,0,1,1})"); 263 | deepEqual(LineUtils.lineIntercepts({x:0, y:0}, {x:1, y:1}, {x:0,y:0, width:1,height:1}), [{x:0, y:0}, {x:1, y:1}], "lineIntercepts({0,0}, {1,1}, {0,0,1,1})"); 264 | deepEqual(LineUtils.lineIntercepts({x:1, y:1}, {x:0, y:0}, {x:0,y:0, width:1,height:1}), [{x:1, y:1}, {x:0, y:0}], "lineIntercepts({1,1}, {0,0}, {0,0,1,1})"); 265 | deepEqual(LineUtils.lineIntercepts({x:1, y:0}, {x:0, y:1}, {x:0,y:0, width:1,height:1}), [{x:1, y:0}, {x:0, y:1}], "lineIntercepts({1,0}, {0,1}, {0,0,1,1})"); 266 | deepEqual(LineUtils.lineIntercepts({x:0, y:1}, {x:1, y:0}, {x:0,y:0, width:1,height:1}), [{x:0, y:1}, {x:1, y:0}], "lineIntercepts({0,1}, {1,0}, {0,0,1,1})"); 267 | 268 | // No intercepts 269 | deepEqual(LineUtils.lineIntercepts({x:0, y:10}, {x:10, y:10}, {x:0,y:0, width:1,height:1}), [], "lineIntercepts({0,10}, {10,10}, {0,0,1,1})"); 270 | deepEqual(LineUtils.lineIntercepts({x:10, y:0}, {x:10, y:10}, {x:0,y:0, width:1,height:1}), [], "lineIntercepts({10,0}, {10,10}, {0,0,1,1})"); 271 | 272 | // Not connected to the edge cases 273 | deepEqual(LineUtils.lineIntercepts({x:1, y:2}, {x:2, y:3}, {x:0,y:0, width:10,height:10}), [{x:0, y:1}, {x:9, y:10}], "lineIntercepts({1,2}, {2,3}, {0,0,10,10})"); 274 | deepEqual(LineUtils.lineIntercepts({x:2, y:3}, {x:1, y:2}, {x:0,y:0, width:10,height:10}), [{x:9, y:10}, {x:0, y:1}], "lineIntercepts({2,3}, {1,2}, {0,0,10,10})"); 275 | 276 | deepEqual(LineUtils.lineIntercepts({x:9, y:1}, {x:8, y:2}, {x:0,y:0, width:10,height:10}), [{x:10, y:0}, {x:0, y:10}], "lineIntercepts({9,1}, {8,2}, {0,0,10,10})"); 277 | deepEqual(LineUtils.lineIntercepts({x:8, y:2}, {x:9, y:1}, {x:0,y:0, width:10,height:10}), [{x:0, y:10}, {x:10, y:0}], "lineIntercepts({2,8}, {9,1}, {0,0,10,10})"); 278 | 279 | deepEqual(LineUtils.lineIntercepts({x:9, y:1}, {x:8, y:2}, {x:-1,y:-1, width:11,height:11}), [{x:10, y:0}, {x:0, y:10}], "lineIntercepts({9,1}, {8,2}, {-1,-1,11,11})"); 280 | deepEqual(LineUtils.lineIntercepts({x:8, y:2}, {x:9, y:1}, {x:-1,y:-1, width:11,height:11}), [{x:0, y:10}, {x:10, y:0}], "lineIntercepts({2,8}, {9,1}, {-1,-1,11,11})"); 281 | 282 | // Ray mode 283 | deepEqual(LineUtils.lineIntercepts({x:0, y:0}, {x:0, y:1}, {x:0,y:0, width:1,height:1}, true), [{x:0, y:0}, {x:0, y:1}], "lineIntercepts({0,0}, {0,1}, {0,0,1,1}, true)"); 284 | deepEqual(LineUtils.lineIntercepts({x:0, y:0}, {x:1, y:0}, {x:0,y:0, width:1,height:1}, true), [{x:0, y:0}, {x:1, y:0}], "lineIntercepts({0,0}, {1,0}, {0,0,1,1}, true)"); 285 | deepEqual(LineUtils.lineIntercepts({x:0, y:0}, {x:1, y:1}, {x:0,y:0, width:1,height:1}, true), [{x:0, y:0}, {x:1, y:1}], "lineIntercepts({0,0}, {1,1}, {0,0,1,1}, true)"); 286 | deepEqual(LineUtils.lineIntercepts({x:1, y:1}, {x:0, y:0}, {x:0,y:0, width:1,height:1}, true), [{x:1, y:1}, {x:0, y:0}], "lineIntercepts({1,1}, {0,0}, {0,0,1,1}, true)"); 287 | deepEqual(LineUtils.lineIntercepts({x:1, y:0}, {x:0, y:1}, {x:0,y:0, width:1,height:1}, true), [{x:1, y:0}, {x:0, y:1}], "lineIntercepts({1,0}, {0,1}, {0,0,1,1}, true)"); 288 | deepEqual(LineUtils.lineIntercepts({x:0, y:1}, {x:1, y:0}, {x:0,y:0, width:1,height:1}, true), [{x:0, y:1}, {x:1, y:0}], "lineIntercepts({0,1}, {1,0}, {0,0,1,1}, true)"); 289 | 290 | deepEqual(LineUtils.lineIntercepts({x:1, y:2}, {x:2, y:3}, {x:0,y:0, width:10,height:10}, true), [{x:9, y:10}], "lineIntercepts({1,2}, {2,3}, {0,0,10,10}, true)"); 291 | deepEqual(LineUtils.lineIntercepts({x:2, y:3}, {x:1, y:2}, {x:0,y:0, width:10,height:10}, true), [{x:0, y:1}], "lineIntercepts({2,3}, {1,2}, {0,0,10,10}, true)"); 292 | 293 | deepEqual(LineUtils.lineIntercepts({x:9, y:1}, {x:8, y:2}, {x:0,y:0, width:10,height:10}, true), [{x:0, y:10}], "lineIntercepts({9,1}, {8,2}, {0,0,10,10}, true)"); 294 | deepEqual(LineUtils.lineIntercepts({x:8, y:2}, {x:9, y:1}, {x:0,y:0, width:10,height:10}, true), [{x:10, y:0}], "lineIntercepts({2,8}, {9,1}, {0,0,10,10}, true)"); 295 | 296 | // Percents 297 | deepEqual(LineUtils.lineIntercepts({x:0, y:0}, {x:0, y:"1.5%"}, {x:0,y:0, width:"1.5%",height:"1.5%"}), [{x:0, y:0}, {x:0, y:"1.5%"}], "lineIntercepts({0,0}, {0,1.5%}, {0,0,1.5%,1.5%})"); 298 | deepEqual(LineUtils.lineIntercepts({x:0, y:0}, {x:"1.5%", y:"1.5%"}, {x:0,y:0, width:"1.5%",height:"1.5%"}), [{x:0, y:0}, {x:"1.5%", y:"1.5%"}], "lineIntercepts({0,0}, {1.5%,1.5%}, {0,0,1.5%,1.5%})"); 299 | 300 | // Units 301 | deepEqual(LineUtils.lineIntercepts({x:0, y:0}, {x:0, y:"1%"}, {x:0,y:0, width:"1%",height:"1%"}), [{x:0, y:0}, {x:0, y:"1%"}], "lineIntercepts({0,0}, {0,1%}, {0,0,1%,1%})"); 302 | deepEqual(LineUtils.lineIntercepts({x:0, y:0}, {x:"1%", y:"1%"}, {x:0,y:0, width:"1%",height:"1%"}), [{x:0, y:0}, {x:"1%", y:"1%"}], "lineIntercepts({0,0}, {1%,1%}, {0,0,1%,1%})"); 303 | deepEqual(LineUtils.lineIntercepts({x:0, y:0}, {x:0, y:"1%"}, {x:0,y:0, width:1,height:1}), NaN, "lineIntercepts({0,0}, {0,1%}, {0,0,1,1})"); 304 | }); 305 | 306 | test("slopeInRads", function() { 307 | // Testing conditional logic only, assuming that the math is correct 308 | expect(19); 309 | equals(LineUtils.slopeInRads({x:0, y:0}, {x:1, y:0}), 0, "slopeInRads({0,0}, {1,0})"); 310 | equals(LineUtils.slopeInRads({x:1, y:0}, {x:0, y:0}), Math.PI, "slopeInRads({1,0}, {0,0})"); 311 | 312 | equals(LineUtils.slopeInRads({x:0, y:0}, {x:0, y:1}), Math.PI/2, "slopeInRads({0,0}, {0,1})"); 313 | equals(LineUtils.slopeInRads({x:0, y:1}, {x:0, y:0}), 3*Math.PI/2, "slopeInRads({0,1}, {0,0})"); 314 | 315 | equals(LineUtils.slopeInRads({x:0, y:0}, {x:1, y:1}), Math.PI/4, "slopeInRads({0,0}, {1,1})"); 316 | equals(LineUtils.slopeInRads({x:1, y:1}, {x:0, y:0}), 5*Math.PI/4, "slopeInRads({1,1}, {0,0})"); 317 | 318 | equals(LineUtils.slopeInRads({x:0, y:0}, {x:"1%", y:0}), 0, "slopeInRads({0,0}, {1%,0})"); 319 | equals(LineUtils.slopeInRads({x:"1%", y:0}, {x:0, y:0}), Math.PI, "slopeInRads({1%,0}, {0,0})"); 320 | 321 | equals(LineUtils.slopeInRads({x:0, y:0}, {x:0, y:"1%"}), Math.PI/2, "slopeInRads({0,0}, {0,1%})"); 322 | equals(LineUtils.slopeInRads({x:0, y:"1%"}, {x:0, y:0}), 3*Math.PI/2, "slopeInRads({0,1%}, {0,0})"); 323 | 324 | equals(LineUtils.slopeInRads({x:0, y:0}, {x:"1%", y:"1%"}), Math.PI/4, "slopeInRads({0,0}, {1%,1%})"); 325 | equals(LineUtils.slopeInRads({x:"1%", y:"1%"}, {x:0, y:0}), 5*Math.PI/4, "slopeInRads({1%,1%}, {0,0})"); 326 | 327 | equals(LineUtils.slopeInRads({x:0, y:0}, {x:1.5, y:0}), 0, "slopeInRads({0,0}, {1.5,0})"); 328 | equals(LineUtils.slopeInRads({x:1.5, y:0}, {x:0, y:0}), Math.PI, "slopeInRads({1.5,0}, {0,0})"); 329 | 330 | equals(LineUtils.slopeInRads({x:0, y:0}, {x:0, y:1.5}), Math.PI/2, "slopeInRads({0,0}, {0,1.5})"); 331 | equals(LineUtils.slopeInRads({x:0, y:1.5}, {x:0, y:0}), 3*Math.PI/2, "slopeInRads({0,1.5}, {0,0})"); 332 | 333 | equals(LineUtils.slopeInRads({x:0, y:0}, {x:1.5, y:1.5}), Math.PI/4, "slopeInRads({0,0}, {1.5,1.5})"); 334 | equals(LineUtils.slopeInRads({x:1.5, y:1.5}, {x:0, y:0}), 5*Math.PI/4, "slopeInRads({1.5,1.5}, {0,0})"); 335 | 336 | deepEqual(LineUtils.slopeInRads({x:"1%", y:"1%"}, {x:1, y:0}), NaN, "slopeInRads({1%,1%}, {1,0})"); 337 | }); 338 | }); 339 | -------------------------------------------------------------------------------- /test/testData.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2011 Kevin Decker (http://www.incaseofstairs.com/) 3 | * See LICENSE for license information 4 | */ 5 | $(document).ready(function() { 6 | function drawGradient(x, y, r1, r2) { 7 | var radialGradient = context.createRadialGradient(x, y, r1, x, y, r2); 8 | radialGradient.addColorStop(0, "red"); 9 | radialGradient.addColorStop(1, "green"); 10 | context.fillStyle = radialGradient; 11 | 12 | context.beginPath(); 13 | context.arc(x, y, r2, 0, 360, false); 14 | context.fill(); 15 | context.closePath(); 16 | } 17 | var canvas = document.createElement("canvas"), 18 | context = canvas.getContext("2d"); 19 | canvas.width = canvas.height = 600; 20 | 21 | context.fillRect(0,0,50,50); 22 | 23 | var linearGradient = context.createLinearGradient(100, 100, 100, 600); 24 | linearGradient.addColorStop(0.0, "black"); 25 | linearGradient.addColorStop(0.5, "red"); 26 | linearGradient.addColorStop(1.0, "blue"); 27 | context.fillStyle = linearGradient; 28 | context.fillRect(100,100,500,600); 29 | 30 | drawGradient(75, 75, 25, 55); 31 | drawGradient(225, 75, 5, 55); 32 | 33 | var img = document.createElement("img"); 34 | $(img).load(function() { 35 | context.drawImage(img, 100, 100); 36 | 37 | GradientScanner.loadImage(canvas, "example image"); 38 | }); 39 | 40 | img.src = "test/css-gradient-dropdown-menu.gif"; 41 | 42 | $(document).bind("imageLoaded", function lineSeed() { 43 | $(this).unbind("imageLoaded", lineSeed); 44 | 45 | var offset = $("#imageDisplay").offset(); 46 | 47 | var mousedown = jQuery.Event("mousedown"); 48 | mousedown.which = 1; 49 | 50 | mousedown.pageX = 189; 51 | mousedown.pageY = 503; 52 | 53 | mousedown.pageX = 232; 54 | mousedown.pageY = 81; 55 | 56 | mousedown.pageX = 366; 57 | mousedown.pageY = 100; 58 | /* 59 | mousedown.pageX = 225; 60 | mousedown.pageY = 73; 61 | /* 62 | mousedown.pageX = 586; 63 | mousedown.pageY = 100; 64 | 65 | mousedown.pageX = 386; 66 | mousedown.pageY = 406; 67 | */ 68 | mousedown.pageX += offset.left; 69 | mousedown.pageY += offset.top; 70 | 71 | var mousemove = jQuery.Event("mousemove"); 72 | mousemove.which = 1; 73 | 74 | mousemove.pageX = 189; 75 | mousemove.pageY = 142; 76 | 77 | mousemove.pageX = 79; 78 | mousemove.pageY = 81; 79 | 80 | mousemove.pageX = 366; 81 | mousemove.pageY = 600; 82 | /* 83 | mousemove.pageX = 67; 84 | mousemove.pageY = 73; 85 | /* 86 | mousemove.pageX = 586; 87 | mousemove.pageY = 600; 88 | 89 | mousemove.pageX = 224; 90 | mousemove.pageY = 257; 91 | */ 92 | mousemove.pageX += offset.left; 93 | mousemove.pageY += offset.top; 94 | 95 | $("#imageDisplay") 96 | .trigger(mousedown) 97 | .trigger(mousemove) 98 | .trigger(jQuery.Event("mouseup")); 99 | }); 100 | }); 101 | --------------------------------------------------------------------------------