1
0
Fork 0

First Commit, Functional and Ready for NPM

This commit is contained in:
sharpshark28 2016-11-13 20:20:37 -06:00
commit 583dcba4d2
22 changed files with 447 additions and 0 deletions

50
.gitignore vendored Normal file
View file

@ -0,0 +1,50 @@
# Logs
logs
*.log
npm-debug.log*
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# node-waf configuration
.lock-wscript
# Compiled binary addons (http://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules
jspm_packages
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# OSX Junk
.DS_Store

67
README.md Normal file
View file

@ -0,0 +1,67 @@
# Blabber-Comic
## Sample
![Preview](/comic.png')
## How it works
Powered by Node and a node-canvas a comic can automatically be generated from a json array of users/text and some characters/backgrounds to be chosen at random.
## How to use it
### Generate base64Data
```javascript
const blabbercomic = require('blabber-comic');
let messages = [] // Array of messages...
blabbercomic(messages).then(response => {
console.log('Generated comic as base64 data', response);
}).catch(error => {
throw error;
});
```
### Save as file with `fs`
Example included in project. Clone then run `npm run test`.
```javascript
const blabbercomic = require('blabber-comic');
const fs = require('fs');
let messages = [] // Array of messages...
blabbercomic(messages).then(response => {
let base64Data = response.replace(/^data:image\/png;base64,/, '');
fs.writeFile('./storage/comics/comic.png', base64Data, 'base64', error => {
if (error) console.error('Uhoh...', error);
else console.log('Saved file as `comic.png`');
});
}).catch(error => {
throw error;
});
```
### Customizing characters and backgrounds
```javascript
const blabbercomic = require('blabber-comic');
let backgrounds = ['./assets/backgrounds/1.png', './assets/backgrounds/2.png'];
let characters = ['./assets/characters/1.png', './assets/characters/2.png', './assets/characters/3.png']; // Provide at least 3
let comicSize = 400; // in px square
let messages = [] // Array of messages...
let config = { backgrounds, characters, comicSize };
blabbercomic(messages, config);
```
---
## Special thanks to:
* [node-canvas](https://github.com/Automattic/node-canvas) by Cairo
* avatars by Iulia Ardeleanu from the Noun Project
* backgrounds by Olga Libby from Subtle Patterns

BIN
assets/backgrounds/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
assets/backgrounds/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
assets/backgrounds/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
assets/characters/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

BIN
assets/characters/10.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
assets/characters/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
assets/characters/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

BIN
assets/characters/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

BIN
assets/characters/5.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

BIN
assets/characters/6.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
assets/characters/7.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
assets/characters/8.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

BIN
assets/characters/9.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

BIN
comic.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

108
helpers/canvas.js Normal file
View file

@ -0,0 +1,108 @@
function fillTextWrapped(ctx, text, x, y, maxWidth, lineHeight, borderWidth) {
// Borrowed with love from http://stackoverflow.com/questions/2936112/text-wrap-in-a-canvas-element
let words = text.split(' ');
let line = '';
let bubblePadding = 10;
let bubbleBorder = 1;
maxWidth -= bubblePadding * 2;
// White BG
ctx.fillStyle = 'white';
ctx.fillRect(
x,
y,
maxWidth + (bubblePadding * 2),
lineHeight
);
// Top border
ctx.fillStyle = 'black';
ctx.fillRect(
x,
y,
maxWidth + (bubblePadding * 2),
bubbleBorder
);
for(var n = 0; n < words.length; n++) {
var testLine = line + words[n] + ' ';
var metrics = ctx.measureText(testLine);
var testWidth = metrics.width;
if (testWidth > maxWidth && n > 0) {
// White BG
ctx.fillStyle = 'white';
ctx.fillRect(
x,
y + bubblePadding,
maxWidth + (bubblePadding * 2),
lineHeight + bubblePadding
);
// Side borders
ctx.fillStyle = 'black';
ctx.fillRect( // Left
x,
y,
bubbleBorder,
lineHeight
);
ctx.fillRect( // Right
x + maxWidth + (bubblePadding * 2) - bubbleBorder,
y,
bubbleBorder,
lineHeight + (bubblePadding * 2)
);
// Text
ctx.fillText(
line,
x + bubblePadding,
y + bubblePadding
);
line = words[n] + ' ';
y += lineHeight;
}
else {
line = testLine;
}
}
// White BG
ctx.fillStyle = 'white';
ctx.fillRect(
x,
y + bubblePadding,
maxWidth + (bubblePadding * 2),
lineHeight + bubblePadding
);
// Side and Bottom Borders
ctx.fillStyle = 'black';
ctx.fillRect( // Bottom
x,
y + lineHeight *2,
maxWidth + (bubblePadding * 2),
bubbleBorder
);
ctx.fillRect( // Right
x,
y,
bubbleBorder,
lineHeight + (bubblePadding * 2)
);
ctx.fillRect( // Right
x + maxWidth + (bubblePadding * 2) - bubbleBorder,
y,
bubbleBorder,
lineHeight + (bubblePadding * 2)
);
// Text
ctx.fillText(
line,
x + bubblePadding,
y + bubblePadding
);
return y; // Useful for knowing how far down text rendered
}
module.exports = {
fillTextWrapped
};

26
helpers/files.js Normal file
View file

@ -0,0 +1,26 @@
const fs = require('fs');
const Canvas = require('canvas-prebuilt');
function isImage(fileString) {
let ext = fileString.slice(fileString.indexOf('.'));
return ['.png', '.jpg'].indexOf(ext) > -1;
}
function readImageFromPath(path) {
return new Promise(resolve => {
fs.readFile(path, (err, loadedImage) => {
if (err) throw err;
let img = new Canvas.Image;
img.src = loadedImage;
resolve(img);
});
}).catch(error => {
throw error;
});
}
module.exports = {
isImage,
readImageFromPath
};

39
helpers/parse.js Normal file
View file

@ -0,0 +1,39 @@
const _ = require('lodash');
/**
* Parsing Linear Messages to a comic friendly format
* Expects: Array of messages, containing the text and the user.
* Returns: Array of panels, containing messages, containing the text and the user.
*/
function parseMessages(messages) {
let structuredMessages = [[]];
let usersInCurrentPanel = [];
messages.forEach(message => {
let isUserInCurrentPanel = usersInCurrentPanel.indexOf(message.user) > -1;
if (isUserInCurrentPanel) {
structuredMessages.push([message]); // New panel
usersInCurrentPanel = [message.user]; // Reset
} else {
structuredMessages[structuredMessages.length - 1]
.push(message); // Add to current panel
usersInCurrentPanel.push(message.user); // Save user in current panel
}
});
return structuredMessages;
}
function parseCharacters(messages, config) {
let users = messages.map(message => message.user);
let uniqueUsers = _.uniq(users);
let characterImages = _.sampleSize(config.assetCharacters, uniqueUsers.length);
return uniqueUsers.map((user, index) => {
return {user, background: characterImages[index]};
});
}
module.exports = {
parseMessages,
parseCharacters
};

96
index.js Normal file
View file

@ -0,0 +1,96 @@
const fs = require('fs');
const _ = require('lodash');
const Canvas = require('canvas-prebuilt');
const { parseMessages, parseCharacters } = require('./helpers/parse.js');
const { fillTextWrapped } = require('./helpers/canvas.js');
const { isImage, readImageFromPath } = require('./helpers/files.js');
const maxCharacters = 3;
const assetBackgrounds = fs.readdirSync(__dirname + '/assets/backgrounds')
.filter(isImage)
.map(path => __dirname + '/assets/backgrounds/' + path);
const assetCharacters = fs.readdirSync(__dirname + '/assets/characters')
.filter(isImage)
.map(path => __dirname + '/assets/characters/' + path);
const defaultConfig = {
assetBackgrounds,
assetCharacters,
font: 'Impact',
textColor: '#000000',
borderColor: '#CCCCCC',
borderWidth: 20, // px
comicPaneSize: 500 // px square
};
const generate = function(messages, config = {}) {
if (!messages) {
console.error('Please supply messages...');
}
config = _.assign(defaultConfig, config);
let comicPaneSize = config.comicPaneSize;
let panels = parseMessages(messages);
let characters = parseCharacters(messages, config);
let imageBackground = readImageFromPath(_.sample(config.assetBackgrounds));
let imagesCharacters = characters.map(character => readImageFromPath(character.background));
let imagesToLoad = [imageBackground].concat(imagesCharacters);
let canvas = new Canvas(panels.length * comicPaneSize + (config.borderWidth * (panels.length + 1)), comicPaneSize + (config.borderWidth * 2));
let ctx = canvas.getContext('2d');
return new Promise((resolve, reject) => {
return Promise.all(imagesToLoad).then(loadedImages => {
let loadedBackgroundImage = loadedImages[0];
let loadedCharacterImages = loadedImages.slice(1);
let characterSize = 150; // px square
let fontsize = 20; // px
ctx.font = fontsize + 'px ' + config.font;
ctx.textBaseline = 'top';
let paneLeftOffset = config.borderWidth;
panels.forEach((panel, panelIndex) => {
ctx.drawImage(loadedBackgroundImage, paneLeftOffset, config.borderWidth);
ctx.fillStyle = config.borderColor;
ctx.fillRect(paneLeftOffset - config.borderWidth, 0, config.borderWidth, canvas.height);
characters.forEach((character, index) => {
let alignToBottom = comicPaneSize - characterSize + config.borderWidth;
let characterLeftOffset = paneLeftOffset + ((comicPaneSize / maxCharacters) * index);
ctx.drawImage(loadedCharacterImages[index], characterLeftOffset, alignToBottom);
ctx.fillStyle = config.textColor;
fillTextWrapped(ctx, character.user, characterLeftOffset, alignToBottom - (fontsize * 2), comicPaneSize / maxCharacters, fontsize);
});
let previousMessageHeight = 0;
panel.forEach((message, messageIndex) => {
let characterIndex = _.findIndex(characters, {user: message.user});
let top = (previousMessageHeight + (fontsize * 1.5) * messageIndex) + config.borderWidth;
let left = paneLeftOffset + ((comicPaneSize / maxCharacters) * characterIndex);
ctx.fillStyle = config.textColor;
previousMessageHeight = fillTextWrapped(ctx, message.text, left, top, comicPaneSize / maxCharacters, fontsize);
});
paneLeftOffset += comicPaneSize + config.borderWidth;
});
// Comic Borders
ctx.fillStyle = config.borderColor;
ctx.fillRect(0, 0, canvas.width, config.borderWidth);
ctx.fillRect(0, canvas.height - config.borderWidth, canvas.width, config.borderWidth);
ctx.fillRect(canvas.width - config.borderWidth, 0, config.borderWidth, canvas.height);
resolve(canvas.toDataURL());
});
}).catch(error => {
throw error;
});
}
module.exports = generate;

23
package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "blabber-comic",
"version": "1.0.0",
"description": "Converts chat logs to comics.",
"main": "index.js",
"scripts": {
"test": "node test.js"
},
"keywords": [
"comic",
"generate",
"node",
"canvas",
"chat"
],
"author": "Joe Wroten <sharpshark28@gmail.com> (http://joewroten.com/)",
"license": "ISC",
"dependencies": {
"canvas-prebuilt": "^1.6.0",
"fs": "0.0.1-security",
"lodash": "^4.16.6"
}
}

38
test.js Normal file
View file

@ -0,0 +1,38 @@
const fs = require('fs');
const generate = require('./index.js');
const exampleMessages = [
{
user: 'Jade',
text: 'So, two people walk into a bar'
}, {
user: 'Sam',
text: 'No, don\'t',
}, {
user: 'Jade',
text: 'The bartender says, why the long face?'
}, {
user: 'Sam',
text: '...',
}, {
user: 'Kit',
text: 'I can tell this is gonna get worse'
}, {
user: 'Jade',
text: 'To get to the other side!'
}, {
user: 'Kit',
text: '...WHY DO YOU DO THIS?',
},
];
generate(exampleMessages).then(response => {
let base64Data = response.replace(/^data:image\/png;base64,/, '');
fs.writeFile('./comic.png', base64Data, 'base64', error => {
if (error) console.error('Uhoh...', error);
else console.log('Saved file as `comic.png`');
});
}).catch(error => {
throw error;
});