diff --git a/lib/nib.js b/lib/nib.js index 0d3c14c4..f61bb107 100644 --- a/lib/nib.js +++ b/lib/nib.js @@ -12,16 +12,13 @@ var stylus = require('stylus') , nodes = stylus.nodes , utils = stylus.utils - , gradient + , gradient = require('./nodes/gradient') , Canvas; exports = module.exports = plugin; -// conditionally expose gradient api - try { - require('canvas'); - gradient = require('./nodes/gradient'); + Canvas = require('canvas'); } catch (err) { // ignore } @@ -48,10 +45,13 @@ exports.path = __dirname; function plugin() { return function(style){ style.include(__dirname); - if (gradient) { - style.define('create-gradient-image', gradient.create) + style.define('create-gradient-image', gradient.create) + style.define('gradient-svg-data-uri', gradient.svgDataURL) + style.define('add-color-stop', gradient.addColorStop) + style.define('set-gradient-size', gradient.setSize) + + if (Canvas) { style.define('gradient-data-uri', gradient.dataURL) - style.define('add-color-stop', gradient.addColorStop) style.define('has-canvas', nodes.true); } else { style.define('has-canvas', nodes.false); diff --git a/lib/nib/config.styl b/lib/nib/config.styl index a254587c..cdd8c55e 100644 --- a/lib/nib/config.styl +++ b/lib/nib/config.styl @@ -10,3 +10,9 @@ support-for-ie = true */ vendor-prefixes ?= webkit moz official + +/* + * Default vendor prefixes. + */ + +gradient-formats = png svg css \ No newline at end of file diff --git a/lib/nib/gradients.styl b/lib/nib/gradients.styl index d0ab996c..0fa89001 100644 --- a/lib/nib/gradients.styl +++ b/lib/nib/gradients.styl @@ -74,6 +74,25 @@ join-stops(stops, translate) str += translate(color, pos) unquote(str) + +/* +* Build a linear gradient object with the given start position +* and variable number of color stops. +*/ + +build-gradient(start, stops) + stops = stops[0] if length(stops) == 1 + if start[0] is a 'unit' + size = start[0] + start = start[1] or 'top' + else + start = start[0] + grad = create-gradient-image(start) + set-gradient-size(grad, size) if size is a 'unit' + stops = normalize-stops(stops) + add-color-stop(grad, stop[0], stop[1]) for stop in stops + grad + /* * Legacy Webkit color stop. */ @@ -101,33 +120,38 @@ std-stop(color, pos) * */ -linear-gradient(start, stops...) +linear-gradient(start, stops..., formats=gradient-formats) error('color stops required') unless length(stops) prop = current-property[0] val = current-property[1] stops = normalize-stops(stops) - - // gradient image - if start[0] is a 'unit' - if has-canvas - img = linear-gradient-image(start, stops) - add-property(prop, replace(val, '__CALL__', img)) - start = start[1] - - // legacy webkit - end = grad-point(opposite-position(start)) - webkit-legacy = '-webkit-gradient(linear, %s, %s, %s)' % (grad-point(start) end join-stops(stops, webkit-stop)) - add-property(prop, replace(val, '__CALL__', webkit-legacy)) - - // vendor prefixed - stops = join-stops(stops, std-stop) - for prefix in vendor-prefixes - unless prefix == official - gradient = '-%s-linear-gradient(%s, %s)' % (prefix start stops) - add-property(prop, replace(val, '__CALL__', gradient)) - - // standard - 'linear-gradient(%s, %s)' % (start stops) + + // gradient png image + if png in formats and start[0] is a 'unit' + img = linear-gradient-image(start, stops) + add-property(prop, replace(val, '__CALL__', img)) + + // gradient svg image + if svg in formats + img = linear-gradient-svg(start, stops) + add-property(prop, replace(val, '__CALL__', img)) + + if css in formats + start = start[1] if start[0] is a 'unit' + // legacy webkit + end = grad-point(opposite-position(start)) + webkit-legacy = '-webkit-gradient(linear, %s, %s, %s)' % (grad-point(start) end join-stops(stops, webkit-stop)) + add-property(prop, replace(val, '__CALL__', webkit-legacy)) + + // vendor prefixed + stops = join-stops(stops, std-stop) + for prefix in vendor-prefixes + unless prefix == official + gradient = '-%s-linear-gradient(%s, %s)' % (prefix start stops) + add-property(prop, replace(val, '__CALL__', gradient)) + + // standard + 'linear-gradient(%s, %s)' % (start stops) /* * Create a linear gradient image with the given start position @@ -135,12 +159,10 @@ linear-gradient(start, stops...) */ linear-gradient-image(start, stops...) - error('node-canvas is required for linear-gradient-image()') unless has-canvas - stops = stops[0] if length(stops) == 1 error('gradient image size required') unless start[0] is a 'unit' - size = start[0] - start = start[1] or 'top' - grad = create-gradient-image(size, start) - stops = normalize-stops(stops) - add-color-stop(grad, stop[0], stop[1]) for stop in stops + grad = build-gradient(start, stops) 'url(%s)' % gradient-data-uri(grad) + +linear-gradient-svg(start, stops...) + grad = build-gradient(start, stops) + 'url(%s)' % gradient-svg-data-uri(grad) diff --git a/lib/nodes/gradient.js b/lib/nodes/gradient.js index d1aea9bf..810a97eb 100644 --- a/lib/nodes/gradient.js +++ b/lib/nodes/gradient.js @@ -4,10 +4,19 @@ */ var stylus = require('stylus') - , Canvas = require('canvas') + , Canvas + , jade = require('jade') + , readFileSync = require('fs').readFileSync , nodes = stylus.nodes - , utils = stylus.utils; - + , utils = stylus.utils + , renderSVGLinearGradient = jade.compile(readFileSync(__dirname + '/../templates/linearGradient.jade', 'utf8')); + +try { + Canvas = require('canvas'); +} catch (err) { + // ignore +} + /** * Expose `Gradient`. */ @@ -18,16 +27,28 @@ exports = module.exports = Gradient; * Create a new `Gradient` node with the given `size` * and `start` position. * - * @param {Number} size * @param {String|Ident|Literal} start * @return {Gradient} * @api public */ -exports.create = function(size, start){ - utils.assertType(size, 'unit', 'size'); +exports.create = function(start){ utils.assertString(start, 'start'); - return new Gradient(size.val, start.string); + return new Gradient(start.string); +}; + +/** + * Set gradient size + * + * @param {Gradient} grad + * @param {Number} size + * @return {Null} + * @api public + */ +exports.setSize = function(grad, size){ + utils.assertType(size, 'unit', 'size'); + grad.size = size.val; + return nodes.null; }; /** @@ -61,23 +82,22 @@ exports.dataURL = function(grad){ return new nodes.String(grad.toDataURL()); }; +exports.svgDataURL = function(grad){ + utils.assertType(grad, 'gradient'); + return new nodes.String(grad.toSVGDataURL()); +}; + /** * Initialize a new `Gradient` node with the given `size` * and `start` position. * - * @param {Number} size * @param {String} start * @api private */ -function Gradient(size, start) { - this.size = size; - this.canvas = new Canvas(1, 1); - this.setStartPosition(start); - this.ctx = this.canvas.getContext('2d'); - this.grad = this.ctx.createLinearGradient( - this.from[0], this.from[1] - , this.to[0], this.to[1]); +function Gradient(start) { + this.start = start; + this.colorStops = []; }; /** @@ -88,75 +108,128 @@ function Gradient(size, start) { */ Gradient.prototype.toString = function(){ - return 'Gradient(' + this.size + 'px ' + var size = this.size ? ' ' + this.size + 'px ' : ''; + return 'Gradient(' + size + this.stops.map(function(stop){ return stop[0] + ' ' + stop[1]; }).join(', ') + ')'; }; /** - * Set `start` position. + * Add color stop `pos` / `color`. * - * @param {String} start + * @param {Number} pos + * @param {String} color * @api private */ -Gradient.prototype.setStartPosition = function(start){ - var size = this.size - , canvas = this.canvas; +Gradient.prototype.addColorStop = function(pos, color){ + this.colorStops.push({ pos: pos, color: color }) +}; + +/** + * Return a PNG data URI string. + * + * @return {String} + * @api private + */ + +Gradient.prototype.toDataURL = function(){ + return this.toPNGDataURL(); +}; + +/** + * Return a PNG data URI string. + * + * @return {String} + * @api private + */ - switch (start) { +Gradient.prototype.toPNGDataURL = function() { + var canvas = new Canvas(1, 1) + , from + , to + , ctx + , grad; + + if (!this.size) throw new Error('Size required for PNG data URL'); + + switch (this.start) { case 'top': - canvas.height = size; - this.from = [canvas.width / 2, 0]; - this.to = [canvas.width / 2, canvas.height]; + canvas.height = this.size; + from = [canvas.width / 2, 0]; + to = [canvas.width / 2, canvas.height]; break; case 'bottom': - canvas.height = size; - this.from = [canvas.width / 2, canvas.height]; - this.to = [canvas.width / 2, 0]; + canvas.height = this.size; + from = [canvas.width / 2, canvas.height]; + to = [canvas.width / 2, 0]; break; case 'left': - canvas.width = size; - this.from = [0, 0]; - this.to = [canvas.width, canvas.height]; + canvas.width = this.size; + from = [0, 0]; + to = [canvas.width, canvas.height]; break; case 'right': - canvas.width = size; - this.from = [canvas.width, canvas.height]; - this.to = [0, 0]; + canvas.width = this.size; + from = [canvas.width, canvas.height]; + to = [0, 0]; break; default: throw new Error('invalid start position "' + start + '"'); } -}; - -/** - * Add color stop `pos` / `color`. - * - * @param {Number} pos - * @param {String} color - * @api private - */ - -Gradient.prototype.addColorStop = function(pos, color){ - this.grad.addColorStop(pos, color); -}; + + ctx = canvas.getContext('2d'); + grad = ctx.createLinearGradient( + from[0], from[1] + , to[0], to[1]); + + this.colorStops.forEach(function(colorStop) { + grad.addColorStop(colorStop.pos, colorStop.color); + }); + + ctx.fillStyle = grad; + ctx.fillRect(0, 0, canvas.width, canvas.height); + return canvas.toDataURL(); +} /** - * Return data URI string. + * Return a SVG data URI string. * * @return {String} * @api private */ -Gradient.prototype.toDataURL = function(){ - var canvas = this.canvas - , ctx = this.ctx; - ctx.fillStyle = this.grad; - ctx.fillRect(0, 0, canvas.width, canvas.height); - return canvas.toDataURL(); -}; +Gradient.prototype.toSVGDataURL = function() { + var x1 = y1 = x2 = y2 = "0%" + , svg + , height = '100%' + , width = '100%'; + + switch (this.start) { + case 'top': + y2 = '100%'; + height = this.size || height; + break; + case 'bottom': + y1 = '100%'; + height = this.size || height; + break; + case 'left': + x2 = '100%'; + width = this.size || width; + break; + case 'right': + x1 = '100%'; + width = this.size || width; + break; + default: + throw new Error('invalid start position "' + start + '"'); + } + + svg = renderSVGLinearGradient.call(this, { height: height, width: width, colorStops: this.colorStops, x1: x1, x2: x2, y1: y1, y2: y2 }) + return 'data:image/svg+xml;base64,' + new Buffer(svg).toString('base64'); +} /** * Inherit from `nodes.Node.prototype`. diff --git a/lib/templates/linearGradient.jade b/lib/templates/linearGradient.jade new file mode 100644 index 00000000..03090d98 --- /dev/null +++ b/lib/templates/linearGradient.jade @@ -0,0 +1,7 @@ +svg(xmlns="http://www.w3.org/2000/svg", height=height, width=width, viewBox="0 0 1 1", preserveAspectRatio="none", version="1.1") + defs + linearGradient(id="g", x1=x1, y1=y1, x2=x2, y2=y2) + - colorStops.forEach(function(colorStop) { + stop(offset=colorStop.pos, stop-color=colorStop.color) + - }) + rect(fill="url(#g)", width="1", height="1") \ No newline at end of file diff --git a/test/cases/linear-gradient.styl b/test/cases/linear-gradient.styl index 21d636a3..5f20dbf8 100644 --- a/test/cases/linear-gradient.styl +++ b/test/cases/linear-gradient.styl @@ -2,18 +2,18 @@ @import 'nib/gradients' body - background: linear-gradient(top, white, black) + background: linear-gradient(top, white, black, formats: css) body - background: linear-gradient(top left, white, red, blue, black) + background: linear-gradient(top left, white, red, blue, black, formats: css) body - background: linear-gradient(bottom right, white, black 80%) + background: linear-gradient(bottom right, white, black 80%, formats: css) body - background: linear-gradient(right bottom, white, 80% black) + background: linear-gradient(right bottom, white, 80% black, formats: css) vendor-prefixes = webkit moz ms o official body - background: linear-gradient(top, white, black) \ No newline at end of file + background: linear-gradient(top, white, black, formats: css) \ No newline at end of file diff --git a/test/gradients.styl b/test/gradients.styl index b35418d1..00dc47bb 100644 --- a/test/gradients.styl +++ b/test/gradients.styl @@ -1,7 +1,7 @@ @import 'nib/gradients' -#gradients tr +#gradients tbody tr height: 50px color: white td @@ -14,43 +14,90 @@ tr:nth-child(1) td:first-child background: linear-gradient(top, yellow, blue) + td:nth-child(2) + background: linear-gradient(50px, yellow, blue, formats: png) + td:nth-child(3) + background: linear-gradient(top, yellow, blue, formats: svg) td:last-child - background: linear-gradient-image(50px, yellow, blue) + background: linear-gradient(25px, yellow, blue, formats: svg) + background-repeat: no-repeat tr:nth-child(2) td:first-child background: linear-gradient(top, red, green, yellow, blue) + td:nth-child(2) + background: linear-gradient(50px, red, green, yellow, blue, formats: png) + td:nth-child(3) + background: linear-gradient(top, red, green, yellow, blue, formats: svg) td:last-child - background: linear-gradient-image(50px, red, green, yellow, blue) + background: linear-gradient(25px, red, green, yellow, blue, formats: svg) + background-repeat: no-repeat tr:nth-child(3) td:first-child background: linear-gradient(top, red, green 10%, 90% yellow, blue) + td:nth-child(2) + background: linear-gradient(50px, red, green 10%, 90% yellow, blue, formats: png) + td:nth-child(3) + background: linear-gradient(top, red, green 10%, 90% yellow, blue, formats: svg) td:last-child - background: linear-gradient-image(50px, red, green 10%, 90% yellow, blue) + background: linear-gradient(25px, red, green 10%, 90% yellow, blue, formats: svg) + background-repeat: no-repeat tr:nth-child(4) td:first-child background: linear-gradient(top, red 15, green 80%, white, 90% yellow, blue) + td:nth-child(2) + background: linear-gradient(50px, red 15, green 80%, white, 90% yellow, blue, formats: png) + td:nth-child(3) + background: linear-gradient(top, red 15, green 80%, white, 90% yellow, blue, formats: svg) td:last-child - background: linear-gradient-image(50px, red 15, green 80%, white, 90% yellow, blue) + background: linear-gradient(25px top, red 15, green 80%, white, 90% yellow, blue, formats: svg) + background-repeat: no-repeat tr:nth-child(5) td:first-child background: linear-gradient(bottom, #fff, #000) + td:nth-child(2) + background: linear-gradient(50px bottom, #fff, #000, formats: png) + td:nth-child(3) + background: linear-gradient(bottom, #fff, #000, formats: svg) td:last-child - background: linear-gradient-image(50px bottom, #fff, #000) + background: linear-gradient(25px bottom, #fff, #000, formats: svg) + background-repeat: no-repeat tr:nth-child(6) td:first-child background: linear-gradient(left, #fff, #000) + td:nth-child(2) + background: linear-gradient(150px left, #fff, #000, formats: png) + td:nth-child(3) + background: linear-gradient(left, #fff, #000, formats: svg) td:last-child - background: linear-gradient-image(150px left, #fff, #000) + background: linear-gradient(75px left, #fff, #000, formats: svg) + background-repeat: no-repeat tr:nth-child(7) td:first-child background: linear-gradient(right, #008AB4, #E9FAFF, 2% #90E4FD, #1FCBFF, 80% #008AB4) + td:nth-child(2) + background: linear-gradient(150px right, #008AB4, #E9FAFF, 2% #90E4FD, #1FCBFF, 80% #008AB4, formats: png) + td:nth-child(3) + background: linear-gradient(right, #008AB4, #E9FAFF, 2% #90E4FD, #1FCBFF, 80% #008AB4, formats: svg) td:last-child - background: linear-gradient-image(150px right, #008AB4, #E9FAFF, 2% #90E4FD, #1FCBFF, 80% #008AB4) + background: linear-gradient(75px right, #008AB4, #E9FAFF, 2% #90E4FD, #1FCBFF, 80% #008AB4, formats: svg) + background-repeat: no-repeat tr:nth-child(8) td:first-child background: linear-gradient(top, red, 50% green, blue) + td:nth-child(2) + background: linear-gradient(50px top, red, 50% green, blue, formats: png) + td:nth-child(3) + background: linear-gradient(top, red, 50% green, blue, formats: svg) td:last-child - background: linear-gradient-image(50px top, red, 50% green, blue) + background: linear-gradient(25px top, red, 50% green, blue, formats: svg) + background-repeat: no-repeat tr:nth-child(9) td:first-child - background: linear-gradient(50px top, red, green, yellow, blue) \ No newline at end of file + background: linear-gradient(top, red, green, yellow, blue) + td:nth-child(2) + background: linear-gradient(50px top, red, green, yellow, blue, formats: png) + td:nth-child(3) + background: linear-gradient(top, red, green, yellow, blue, formats: svg) + td:last-child + background: linear-gradient(25px top, red, green, yellow, blue, formats: svg) + background-repeat: no-repeat \ No newline at end of file diff --git a/test/index.jade b/test/index.jade index 773d061f..a1a7a208 100644 --- a/test/index.jade +++ b/test/index.jade @@ -15,12 +15,20 @@ html body h2 Gradients table#gradients + thead + tr + th CSS + th PNG Data URI + th SVG Data URI + th Sized SVG Data URI - var n = 9 tbody - while (n--) tr td td + td + td h2 Buttons table#buttons tbody