Skip to content

Commit

Permalink
Add support for rounded corners
Browse files Browse the repository at this point in the history
  • Loading branch information
SamVerschueren committed Sep 6, 2019
1 parent 4b6bcf1 commit d90cbe7
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 3 deletions.
22 changes: 21 additions & 1 deletion index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ const pwaIcons = require('pwa-icon-list');
const androidIcons = require('android-icon-list');
const bb10Icons = require('bb10-icon-list');
const iosIcons = require('ios-icon-list');
const execa = require('execa');
const mask = require('./lib/mask');

const mkdirp = pify(mkdir);

Expand All @@ -18,6 +20,12 @@ const platformIcons = {
blackberry10: bb10Icons()
};

// See https://material.io/design/platform-guidance/android-icons.html#keyline-shapes
const platformRadius = new Map([
['android', 0.0909],
['pwa', 0.0909]
]);

const calculateDimension = (imgSize, iconSize, opts, resizeFn) => {
let width;
let height;
Expand Down Expand Up @@ -48,6 +56,8 @@ module.exports = (file, opts) => {
platform: '',
dest: process.cwd(),
background: 'white',
roundedCorners: platformRadius.has(opts.platform),
borderRadius: platformRadius.get(opts.platform),
contentRatio: 1
}, opts);

Expand Down Expand Up @@ -77,7 +87,17 @@ module.exports = (file, opts) => {
.background(opts.background)
.extent(icon.dimension, icon.dimension);

return mkdirp(path.dirname(dest)).then(() => pify(image.write.bind(image))(dest));
return mkdirp(path.dirname(dest))
.then(() => pify(image.write.bind(image))(dest))
.then(() => {
if (opts.roundedCorners) {
return mask(icon.dimension, opts.borderRadius)
.then(maskLocation => {
// Apply the mask and overwrite the original image
return execa('gm', ['composite', '-compose', 'in', dest, maskLocation, dest]);
});
}
});
}));
});
};
50 changes: 50 additions & 0 deletions lib/mask.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict';
const fs = require('fs');
const tempy = require('tempy');
const pify = require('pify');
const gm = require('gm');

/**
* Generate an SVG mask image.
*
* @param borderRadius - Radius of the corners.
*/
const generateSVGMask = borderRadius => {
const tempfile = tempy.file({
extension: 'svg'
});

const size = 2048;
const radius = Math.round(2048 * borderRadius);

fs.writeFileSync(tempfile, `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="${size}px" height="${size}px" viewBox="0 0 ${size} ${size}" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<title>Mobicon</title>
<defs>Mask</defs>
<rect width="${size}" height="${size}" rx="${radius}" ry="${radius}" style="fill:rgb(0,0,0)" />
</svg>
`);

return tempfile;
};

/**
* Generate the mask image for rounded corners.
*
* @param dimension - Dimension of the icon.
* @param borderRadius - Radius of the corners.
*/
module.exports = (dimension, borderRadius) => {
const svgMask = generateSVGMask(borderRadius);

const pngMask = tempy.file({
extension: 'png'
});

const maskImage = gm(svgMask)
.resize(dimension, dimension)
.background('transparent');

return pify(maskImage.write.bind(maskImage))(pngMask)
.then(() => pngMask);
};
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"test": "clinton && xo && ava"
},
"files": [
"index.js"
"index.js",
"lib"
],
"keywords": [
"mobile",
Expand All @@ -33,16 +34,19 @@
"dependencies": {
"android-icon-list": "^1.0.1",
"bb10-icon-list": "^1.0.0",
"execa": "^1.0.0",
"gm": "^1.21.1",
"ios-icon-list": "^1.0.0",
"mkdirp": "^0.5.1",
"path-exists": "^3.0.0",
"pify": "^2.3.0",
"pwa-icon-list": "^1.0.0"
"pwa-icon-list": "^1.0.0",
"tempy": "^0.3.0"
},
"devDependencies": {
"ava": "*",
"clinton": "*",
"parse-png": "^1.1.2",
"tempfile": "^1.1.1",
"xo": "*"
}
Expand Down
14 changes: 14 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ Default: `1`

Logo-icon ratio. `1` means the logo will fill up the entire width (or height) of the icon, `0.5` means it will only fill up half of the icon.

##### roundedCorners

Type: `boolean`<br>
Default: `true` (only for `Android` and `PWA`)

Boolean indicating if the generated icons should have rounded corners. This is `true` by default for the `Android` and `PWA` platform, `false` otherwise.

##### borderRadius

Type: `number`<br>
Default: `0.0909`

The corner radius percentage of the generated icon. The default value is `9.09%`. See the [material design styleguide](https://material.io/design/platform-guidance/android-icons.html#keyline-shapes) for more information.

##### dest

Type: `string`<br>
Expand Down
14 changes: 14 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import path from 'path';
import fs from 'fs';
import test from 'ava';
import pathExists from 'path-exists';
import tempfile from 'tempfile';
import gm from 'gm';
import pify from 'pify';
import parsePNG from 'parse-png';
import fn from '.';

test.beforeEach(t => {
Expand Down Expand Up @@ -68,3 +70,15 @@ test('output size svg', async t => {
t.is(width, 40);
t.is(height, 40);
});

test('transparent corners', async t => {
await fn('fixtures/icon.svg', {platform: 'pwa', dest: t.context.tmp, roundedCorners: true});

const {data} = await parsePNG(fs.readFileSync(path.join(t.context.tmp, 'icon-72x72.png')));

// Check the first pixel
t.is(data[0], 0); // R
t.is(data[1], 0); // G
t.is(data[2], 0); // B
t.is(data[3], 0); // A
});

0 comments on commit d90cbe7

Please sign in to comment.