diff --git a/.travis.yml b/.travis.yml index 3218f2b..58649b7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,4 +8,7 @@ env: language: node_js node_js: - - 0.10 + - '0.10' + - '0.12' + - '4.2' + - '5.0' diff --git a/examples/smoketest/blur-convolution.js b/examples/smoketest/blur-convolution.js new file mode 100644 index 0000000..7d85089 --- /dev/null +++ b/examples/smoketest/blur-convolution.js @@ -0,0 +1,51 @@ +// the code is taken from https://github.com/mattlockyer/iat455/blob/6493c882f1956703133c1bffa1d7ee9a83741cbe/assignment1/assignment/effects/blur-effect-dyn.js +// (c) Matt Lockyer, https://github.com/mattlockyer + +function hypotenuse(x1, y1, x2, y2) { + var xSquare = Math.pow(x1 - x2, 2); + var ySquare = Math.pow(y1 - y2, 2); + return Math.sqrt(xSquare + ySquare); +} + +/* + * Generates a kernel used for the gaussian blur effect. + * + * @param dimension is an odd integer + * @param sigma is the standard deviation used for our gaussian function. + * + * @returns an array with dimension^2 number of numbers, all less than or equal + * to 1. Represents our gaussian blur kernel. + */ +function generateGaussianKernel(dimension, sigma) { + if (!(dimension % 2) || Math.floor(dimension) !== dimension || dimension<3) { + throw new Error( + 'The dimension must be an odd integer greater than or equal to 3' + ); + } + var kernel = []; + + var twoSigmaSquare = 2 * sigma * sigma; + var centre = (dimension - 1) / 2; + + for (var i = 0; i < dimension; i++) { + for (var j = 0; j < dimension; j++) { + var distance = hypotenuse(i, j, centre, centre); + + // The following is an algorithm that came from the gaussian blur + // wikipedia page [1]. + // + // http://en.wikipedia.org/w/index.php?title=Gaussian_blur&oldid=608793634#Mechanics + var gaussian = (1 / Math.sqrt( + Math.PI * twoSigmaSquare + )) * Math.exp((-1) * (Math.pow(distance, 2) / twoSigmaSquare)); + + kernel.push(gaussian); + } + } + + // Returns the unit vector of the kernel array. + var sum = kernel.reduce(function (c, p) { return c + p; }); + return kernel.map(function (e) { return e / sum; }); +} + +module.exports = generateGaussianKernel; diff --git a/examples/smoketest/trapezoids.js b/examples/smoketest/trapezoids.js new file mode 100644 index 0000000..7361629 --- /dev/null +++ b/examples/smoketest/trapezoids.js @@ -0,0 +1,125 @@ +var _ = require('underscore'); +var x11 = require('../../lib'); +var Exposure = x11.eventMask.Exposure; +var PointerMotion = x11.eventMask.PointerMotion; +var ButtonPress = x11.eventMask.ButtonPress; + +var draw; + +var useConvo3 = true; + +x11.createClient({ debug: true}, function(err, display) { + + var X = display.client; + var root = display.screen[0].root; + + var wid = X.AllocID(); + X.CreateWindow( + wid, root, + 0, 0, 800, 600, + 0, 0, 0, 0, + { + eventMask: Exposure|PointerMotion|ButtonPress + } + ); + X.MapWindow(wid); + + + X.require('render', function(err, Render) { + + Render.QueryFilters(console.log); + + var pixMask = X.AllocID(); + X.CreatePixmap(pixMask, wid, 8, 600, 600); + var pictTraps = X.AllocID(); + Render.CreatePicture(pictTraps, pixMask, Render.a8); + + var pictWin = X.AllocID(); + Render.CreatePicture(pictWin, wid, Render.rgb24); + + var pictSolid = X.AllocID(); + r = 0.2; g = 0.2; b = 0.2; a = 1; + Render.CreateSolidFill(pictSolid, r, g, b, a); + + var pixBuff = X.AllocID(); + X.CreatePixmap(pixBuff, wid, 24, 600, 600); + var pictBuff = X.AllocID(); + Render.CreatePicture(pictBuff, pixBuff, Render.rgb24); + + var convo5 = [ 5, 5, 0.0030, 0.0133, 0.0219, 0.0133, 0.0030, + 0.0133, 0.0596, 0.0983, 0.0596, 0.0133, + 0.0219, 0.0983, 0.1621, 0.0983, 0.0219, + 0.0133, 0.0596, 0.0983, 0.0596, 0.0133, + 0.0030, 0.0133, 0.0219, 0.0133, 0.0030]; + var convo3 = [3, 3, 0.01, 0.08, 0.01, 0.08, 0.64, 0.08, 0.01, 0.08, 0.01]; + + var convo5 = [21, 21].concat(require('./blur-convolution')(21, 11)); + + //Render.SetPictureFilter(pictBuff, 'convolution', convo3); + //Render.SetPictureFilter(pictTraps, 'convolution', convo3); + //Render.SetPictureFilter(pictBuff, 'bilinear', []); + Render.SetPictureFilter(pictBuff, 'best', []); + + draw1 = function(x, y) { + console.log('draw 1', x, y); + var r = 3 + 2*Math.floor(x / 100); + var convo = [r, r].concat(require('./blur-convolution')(r, r)); + Render.SetPictureFilter(pictBuff, 'convolution', convo); + + var a = (x-400)/500; + var m = [ + Math.cos(a), Math.sin(a), 0, + -Math.sin(a), Math.cos(a), 0, + 0, 0, 1 + ]; + // Render.SetPictureTransform(pictTraps, m) + + var r, g, b; + + // fill window + r = 1; g = 1; b = 1; a = 0.5; + Render.FillRectangles(1, pictBuff, [r, g, b, a], [0, 0, 1000, 1000]) + + // fill traps + r = 0; g = 0; b = 0; a = 0; + Render.FillRectangles(1, pictTraps, [r, g, b, a], [0, 0, 1000, 1000]) + + Render.AddTraps(pictTraps, 0, 0, [ + x, 200, y, + //150, 200, 50, + 5, 250, 300, + 110, 200, 310, + 50, 150, 500 + ]); + + + // (op, src, mask, dst, srcX, srcY, maskX, maskY, dstX, dstY, width, height) + //Render.Composite(Render.PictOp.Over, pictSolid, pictTraps, pictWin, 0, 0, 0, 0, 0, 0, 800, 600); + //Render.PictOp.Over + + Render.Composite(Render.PictOp.Over, pictSolid, pictTraps, pictBuff, 0, 0, 0, 0, 0, 0, 800, 600); + Render.Composite(Render.PictOp.Over, pictBuff, 0, pictWin, 0, 0, 0, 0, 0, 0, 800, 600); + + }; + + draw = function(x, y) { + var f = _.debounce(function() { + draw1(x, y); + }, 100); + f(); + }; + + }); + +}).on('error', function(err) { + console.log(err); +}).on('event', function(ev) { + //console.log(ev); + if (ev.name == 'MotionNotify') { + draw(ev.x, ev.y); + } else if (ev.name == 'ButtonPress') { + useConvo3 = !useConvo3; + draw(ev.x, ev.y); + //console.log(ev); + } +}); diff --git a/lib/ext/render.js b/lib/ext/render.js index cac22db..b609c29 100644 --- a/lib/ext/render.js +++ b/lib/ext/render.js @@ -32,8 +32,8 @@ exports.requireExt = function(display, callback) ext.QueryPictFormat = function(callback) { - X.seq_num++; X.pack_stream.pack('CCS', [ext.majorOpcode, 1, 1]); + X.seq_num++; X.replies[X.seq_num] = [ function (buf, opt) { var res = {}; @@ -42,7 +42,7 @@ exports.requireExt = function(display, callback) var num_screens = res1[1]; var num_depths = res1[2]; var num_visuals = res1[3]; - var num_subpixel = res1[4]; + var num_subpixel = res1[4]; // formats list: var offset = 24; res.formats = []; @@ -62,8 +62,8 @@ exports.requireExt = function(display, callback) ext.QueryFilters = function(callback) { - X.seq_num++; X.pack_stream.pack('CCSL', [ext.majorOpcode, 29, 2, display.screen[0].root]); + X.seq_num++; X.replies[X.seq_num] = [ function(buf, opt) { var h = buf.unpack('LL'); @@ -118,7 +118,6 @@ exports.requireExt = function(display, callback) ext.CreatePicture = function(pid, drawable, pictformat, values) { - X.seq_num++; var mask = 0; var reqLen = 5; // + (values + pad)/4 var format = 'CCSLLLL'; @@ -149,53 +148,93 @@ exports.requireExt = function(display, callback) } X.pack_stream.pack(format, params); X.pack_stream.flush(); + X.seq_num++; } ext.FreePicture = function(pid) { - X.seq_num++; - X.pack_stream.pack('CCSL', [ext.majorOpcode, 7, 2, pid]); - X.pack_stream.flush(); - } + X.pack_stream.pack('CCSL', [ext.majorOpcode, 7, 2, pid]); + X.pack_stream.flush(); + X.seq_num++; + }; function floatToFix(f) { return parseInt(f*65536); } + function colorToFix(f) + { + if (f < 0) f = 0; + if (f > 1) f = 1; + return parseInt(f*65535); + } + + ext.SetPictureTransform = function(pid, matrix) { + var format = 'CCSLLLLLLLLLLLLL'; + if (matrix.length !== 9) + throw 'Render.SetPictureTransform: incorrect transform matrix. Must be array of 9 numbers'; + var params = [ext.majorOpcode, 28, 11, pid]; + for (var i=0; i < 9; ++i) { + if (typeof matrix[i] !== 'number') + throw 'Render.SetPictureTransform: matrix element must be a number'; + params.push(floatToFix(matrix[i])); + } + X.pack_stream.pack(format, params); + X.pack_stream.flush(); + X.seq_num++; + }; + // see example of blur filter here: https://github.com/richoH/rxvt-unicode/blob/master/src/background.C - // TODO: not ready yet. ext.SetPictureFilter = function(pid, name, filterParams) { - X.seq_num++; - var reqLen = 2; //header + params + 1xStopfix+2xColors - var format = 'CCSLa'; - var params = [ext.majorOpcode, 30, reqLen, pid]; - /* - if (name == 'convolution') - { - reqLen += 2; - format += 'L'; - params.push(floatToFix(filterParams)); + if (filterParams === 0) + filterParams = [0]; + if (!filterParams) + filterParams = []; + if (!Array.isArray(filterParams)) + filterParams = [filterParams]; + + var reqLen = 2; + var format = 'CCSLSxxp'; + var params = [ext.majorOpcode, 30, reqLen, pid, name.length, name]; + reqLen += xutil.padded_length(name.length+3)/4 + filterParams.length; + + if (name == 'nearest' || name == 'bilinear' || name == 'fast' || name == 'good' || name == 'best') { + if (filterParams.length !== 0) { + throw 'Render.SetPictureFilter: "' + name + '" - unexpected parameters for filters'; + } + } else if (name == 'convolution') { + if (filterParams.length < 2 || ((filterParams[0]*filterParams[1] + 2) !== filterParams.length) ) { + throw 'Render.SetPictureFilter: "convolution" - incorrect matrix dimensions. Must be flat array [ w, h, elem1, elem2, ... ]'; + } + for (var i=0; i < filterParams.length; ++i) { + format += 'L'; + params.push(floatToFix(filterParams[i])); + } + } else if (name == 'binomial' || name == 'gaussian') { + if (filterParams.length !== 1) { + throw 'Render.SetPictureFilter: "' + name + '" - incorrect number of parameters, must be exactly 1 number, instead got: ' + filterParams; + } + format += 'L'; + params.push(floatToFix(filterParams[0])); } else { - throw 'Not implemented filter ' + name; + throw 'Render.SetPictureFilter: unknown filter "' + name + '"'; } - */ params[2] = reqLen; X.pack_stream.pack(format, params); X.pack_stream.flush(); + X.seq_num++; }; ext.CreateSolidFill = function(pid, r, g, b, a) { - X.seq_num++; - X.pack_stream.pack('CCSLSSSS', [ext.majorOpcode, 33, 4, pid, floatToFix(r), floatToFix(g), floatToFix(b), floatToFix(a)]); + X.pack_stream.pack('CCSLSSSS', [ext.majorOpcode, 33, 4, pid, colorToFix(r), colorToFix(g), colorToFix(b), colorToFix(a)]); X.pack_stream.flush(); + X.seq_num++; }; ext.RadialGradient = function(pid, p1, p2, r1, r2, stops) { - // TODO: merge with linear gradient - X.seq_num++; var reqLen = 9+stops.length*3; //header + params + 1xStopfix+2xColors var format = 'CCSLLLLLLLL'; var params = [ext.majorOpcode, 35, reqLen, pid]; @@ -220,15 +259,15 @@ exports.requireExt = function(display, callback) { format += 'SSSS'; for (var j=0; j < 4; ++j) - params.push(stops[i][1][j]); + params.push(colorToFix(stops[i][1][j])); } X.pack_stream.pack(format, params); X.pack_stream.flush(); + X.seq_num++; }; ext.LinearGradient = function(pid, p1, p2, stops) { - X.seq_num++; var reqLen = 7+stops.length*3; //header + params + 1xStopfix+2xColors var format = 'CCSLLLLLL'; var params = [ext.majorOpcode, 34, reqLen, pid]; @@ -252,15 +291,15 @@ exports.requireExt = function(display, callback) { format += 'SSSS'; for (var j=0; j < 4; ++j) - params.push(stops[i][1][j]); + params.push(colorToFix(stops[i][1][j])); } X.pack_stream.pack(format, params); X.pack_stream.flush(); + X.seq_num++; } ext.ConicalGradient = function(pid, center, angle, stops) { - X.seq_num++; var reqLen = 6+stops.length*3; //header + params + 1xStopfix+2xColors var format = 'CCSLLLLL'; var params = [ext.majorOpcode, 36, reqLen, pid]; @@ -283,20 +322,20 @@ exports.requireExt = function(display, callback) { format += 'SSSS'; for (var j=0; j < 4; ++j) - params.push(stops[i][1][j]); + params.push(colorToFix(stops[i][1][j])); } X.pack_stream.pack(format, params); X.pack_stream.flush(); + X.seq_num++; } ext.FillRectangles = function(op, pid, color, rects) { - X.seq_num++; var reqLen = 5+rects.length/2; var format = 'CCSCxxxLSSSS'; var params = [ext.majorOpcode, 26, reqLen, op, pid]; for (var j=0; j < 4; ++j) - params.push(color[j]); + params.push(colorToFix(color[j])); for (var i=0; i < rects.length; i+=4) { format += 'ssSS'; @@ -307,24 +346,25 @@ exports.requireExt = function(display, callback) } X.pack_stream.pack(format, params); X.pack_stream.flush(); + X.seq_num++; } ext.Composite = function(op, src, mask, dst, srcX, srcY, maskX, maskY, dstX, dstY, width, height) { - X.seq_num++; X.pack_stream.pack( 'CCSCxxxLLLssssssSS', [ext.majorOpcode, 8, 9, op, src, mask, dst, srcX, srcY, maskX, maskY, dstX, dstY, width, height] ) .flush(); + X.seq_num++; } + // note that Trapezoids is considered deprecated by Render extension ext.Trapezoids = function(op, src, srcX, srcY, dst, maskFormat, trapz) { - X.seq_num++; var format = 'CCSCxxxLLLss'; var params = [ext.majorOpcode, 10, 6+trapz.length, op, src, dst, maskFormat, srcX, srcY]; - for (var i=0; i < trapz.length; i+=10) + for (var i=0; i < trapz.length; i++) { format += 'llllllllll'; for (var j=0; j < 10; ++j) @@ -332,11 +372,24 @@ exports.requireExt = function(display, callback) } X.pack_stream.pack(format, params); X.pack_stream.flush(); - } + X.seq_num++; + }; + + ext.AddTraps = function(pic, offX, offY, trapList) { + var format = 'CCSLss'; + var params = [ext.majorOpcode, 32, 3+trapList.length, pic, offX, offY]; + for (var i=0; i < trapList.length; i++) + { + format += 'l'; + params.push(floatToFix(trapList[i])); + } + X.pack_stream.pack(format, params); + X.pack_stream.flush(); + X.seq_num++; + }; ext.Triangles = function(op, src, srcX, srcY, dst, maskFormat, tris) { - X.seq_num++; var format = 'CCSCxxxLLLss'; var params = [ext.majorOpcode, 11, 6+tris.length, op, src, dst, maskFormat, srcX, srcY]; for (var i=0; i < tris.length; i+=6) @@ -352,28 +405,28 @@ exports.requireExt = function(display, callback) } X.pack_stream.pack(format, params); X.pack_stream.flush(); + X.seq_num++; } ext.CreateGlyphSet = function(gsid, format) { - X.seq_num++; X.pack_stream.pack('CCSLL', [ext.majorOpcode, 17, 3, gsid, format]); X.pack_stream.flush(); + X.seq_num++; } ext.ReferenceGlyphSet = function(gsid, existing) { - X.seq_num++; X.pack_stream.pack('CCSLL', [ext.majorOpcode, 18, 3, gsid, existing]); X.pack_stream.flush(); + X.seq_num++; } ext.FreeGlyphSet = function(gsid) { - X.seq_num++; X.pack_stream.pack('CCSL', [ext.majorOpcode, 19, 2, gsid]); X.pack_stream.flush(); + X.seq_num++; } ext.AddGlyphs = function(gsid, glyphs) { - X.seq_num++; var numGlyphs = glyphs.length; var imageBytes = 0; var glyphPaddedLength; @@ -417,6 +470,7 @@ exports.requireExt = function(display, callback) X.pack_stream.write_queue.push(glyphs[i].image); } X.pack_stream.flush(); + X.seq_num++; } //AddGlyphsFromPicture, opcode=21 (not in spec) @@ -464,7 +518,6 @@ exports.requireExt = function(display, callback) var opcode = compositeGlyphsOpcodeFromBits[glyphBits]; var charFormat = formatFromBits[glyphBits]; var charLength = glyphBits / 8; - X.seq_num++; var length = 7; var glyphs_length_split = []; for (var i=0; i < glyphs.length; ++i) { @@ -500,6 +553,7 @@ exports.requireExt = function(display, callback) } } X.pack_stream.flush(); + X.seq_num++; }; ext.CompositeGlyphs8 = function(op, src, dst, maskFormat, gsid, srcX, srcY, glyphs) @@ -565,5 +619,112 @@ exports.requireExt = function(display, callback) }; }); + ext.PictOp = { + Minimum: 0, + Clear: 0, + Src: 1, + Dst: 2, + Over: 3, + OverReverse: 4, + In: 5, + InReverse: 6, + Out: 7, + OutReverse: 8, + Atop: 9, + AtopReverse: 10, + Xor: 11, + Add: 12, + Saturate: 13, + Maximum: 13, + + /*, + * Operators only available in version 0.2, + */ + DisjointMinimum: 0x10, + DisjointClear: 0x10, + DisjointSrc: 0x11, + DisjointDst: 0x12, + DisjointOver: 0x13, + DisjointOverReverse: 0x14, + DisjointIn: 0x15, + DisjointInReverse: 0x16, + DisjointOut: 0x17, + DisjointOutReverse: 0x18, + DisjointAtop: 0x19, + DisjointAtopReverse: 0x1a, + DisjointXor: 0x1b, + DisjointMaximum: 0x1b, + + ConjointMinimum: 0x20, + ConjointClear: 0x20, + ConjointSrc: 0x21, + ConjointDst: 0x22, + ConjointOver: 0x23, + ConjointOverReverse: 0x24, + ConjointIn: 0x25, + ConjointInReverse: 0x26, + ConjointOut: 0x27, + ConjointOutReverse: 0x28, + ConjointAtop: 0x29, + ConjointAtopReverse: 0x2a, + ConjointXor: 0x2b, + ConjointMaximum: 0x2b, + + /*, + * Operators only available in version 0.11, + */ + BlendMinimum : 0x30, + Multiply : 0x30, + Screen : 0x31, + Overlay : 0x32, + Darken : 0x33, + Lighten : 0x34, + ColorDodge : 0x35, + ColorBurn : 0x36, + HardLight : 0x37, + SoftLight : 0x38, + Difference : 0x39, + Exclusion : 0x3a, + HSLHue : 0x3b, + HSLSaturation: 0x3c, + HSLColor : 0x3d, + HSLLuminosity: 0x3e, + BlendMaximum : 0x3e + }; + + ext.PolyEdge = { + Sharp: 0, + Smooth: 1 + }; + + ext.PolyMode = { + Precise: 0, + Imprecise: 1 + }; + + ext.Repeat = { + None: 0, + Normal: 1, + Pad: 2, + Reflect: 3 + }; + + ext.Subpixel = { + Unknown: 0, + HorizontalRGB: 1, + HorizontalBGR: 2, + VerticalRGB : 3, + VerticalBGR : 4, + None : 5 + }; + + ext.Filters = { + Nearest: 'nearest', + Bilinear: 'bilinear', + Convolution: 'convolution', + Fast: 'fast', + Good: 'good', + Best: 'best' + }; }); } diff --git a/test/smoke-require-ext.js b/test/smoke-require-ext.js new file mode 100644 index 0000000..5ce9d58 --- /dev/null +++ b/test/smoke-require-ext.js @@ -0,0 +1,16 @@ +var fs = require('fs'); +var assert = require('assert'); + +describe('all extension modules', function() { + it('should not throw when require\'d', function(done) { + var extFolder = __dirname + '/../lib/ext'; + fs.readdir(extFolder, function(err, list) { + assert.ifError(err); + list.forEach(function(name) { + var m = require(extFolder + '/' + name); + }); + done(); + }); + }); +}) +