Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Text support #1

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
28 changes: 0 additions & 28 deletions README.markdown

This file was deleted.

49 changes: 49 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Lettercrap

_Lettercrap_ is a Javascript library that generates dynamic ascii art on the web, using an image mask. It looks like this:

<img src="https://raw.githubusercontent.com/nate-parrott/lettercrap/gh-pages/crap.gif" width="400px">

[Here's a live demo.](https://nate-parrott.github.io/lettercrap)

## Usage

To use _Lettercrap_, import `lettercrap.js` and `lettercrap.css`. Create a `div` with the `data-letter-crap`
attribute with the source of a black-and-white image, which will serve as the shape of the generated ascii art:

<div data-letter-crap='abcs.png' style='width: 500px; height: 200px'></div>

Make sure to set a height for your `div`. If you want to set a dynamic height based on the div's fluid width,
you can specify an aspect ratio using the `data-lettercrap-aspect-ratio` attribute, and _Lettercrap_ will set the height of the `div` for you:

<div data-letter-crap='abcs.png' style='width: 70%' data-lettercrap-aspect-ratio='0.5'></div>

By default, _Lettercrap_ uses `0`s, `1`s and the occasional `_` to fill in the shape of your image. You can
change the set of letters used with the `data-lettercrap-letters` attribute:

<div data-letter-crap='abcs.png' data-lettercrap-letters='ABC'></div>

_Lettercrap_ can also throw the occasional _full word_ into the mix—specify the words to choose from using
the `data-lettercrap-words` attribute:

<div data-letter-crap='words.png' data-lettercrap-words='apple banana peach'></div>

_Lettercrap_ allows you to generate a canvas from text if you don't want to generate an image beforehand
with the `data-lettercrap-text` attribute:

<div data-lettercrap-text='CHECK IT OUT' data-lettercrap-aspect-ratio='0.3'></div>

Check out the `examples/index.html` file to see exactly how this all fits together.

## Development
To get the example working locally, you need to host it on localhost somehow. This is due to the security
features involved with pulling images from local storage, unfortunately.

Here are a few of the commands that work, depending on what software you have in your environment. Just
be sure to run them from the lettercrap folder, not the example folder:
- PHP: `php -S localhost:8080`
- Python 2: `python -m SimpleHTTPServer 8080`
- Python 3: `python -m http.server 8080`
- Node.js: `npm install http-server -g` then `http-server`

Then visit `http://localhost:8080/example` to see it in action.
File renamed without changes
File renamed without changes
File renamed without changes
10 changes: 7 additions & 3 deletions example/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<html>
<head>
<title>lettercrap.js demo</title>
<meta charset="UTF-8">
<script src='../lettercrap.js'></script>
<link rel='stylesheet' href='../lettercrap.css'/>
<style>
Expand All @@ -17,16 +18,19 @@
margin: auto;
margin-top: 3em;
margin-bottom: 5em;
width: 70%;
}
</style>
</head>
<body>
<div data-letter-crap='lettercrap.png' style='width: 70%;' data-lettercrap-aspect-ratio='0.3'></div>
<div data-letter-crap='example/images/lettercrap.png' data-lettercrap-aspect-ratio='0.3'></div>
<br/>
<p>Customize the letters used with the <code>data-lettercrap-letters='ABC'</code>:</p>
<div data-letter-crap='abcs.png' data-lettercrap-letters='ABC' style='width: 70%' data-lettercrap-aspect-ratio='0.5'></div>
<div data-letter-crap='example/images/abcs.png' data-lettercrap-letters='ABC' data-lettercrap-aspect-ratio='0.5'></div>
<br/>
<p>Randomly throw specific words into the mix using <code>data-lettercrap-words='apple banana peach'</code>:</p>
<div data-letter-crap='words.png' data-lettercrap-words='apple banana peach' style='width: 70%' data-lettercrap-aspect-ratio='0.3'></div>
<div data-letter-crap='example/images/words.png' data-lettercrap-words='apple banana peach' data-lettercrap-aspect-ratio='0.3'></div>
<p>Generate art from text, instead of making an image each time:</p>
<div data-lettercrap-text='CHECK IT OUT' data-lettercrap-aspect-ratio='0.3'></div>
</body>
</html>
179 changes: 98 additions & 81 deletions lettercrap.js
Original file line number Diff line number Diff line change
@@ -1,88 +1,105 @@
(function() {
var charWidth = 6;
var charHeight = 10;
var likelihoodOfReplacingWord = 0.05;
var likelihoodOfChangingExistingText = 0.1;
const charWidth = 6;
const charHeight = 10;
const updateInterval = 150;
const likelihoodOfReplacingWord = 0.05;
const likelihoodOfChangingExistingText = 0.1;
const randomChoice = x => x[Math.floor(Math.random() * x.length)];

function randomChoice(x) {
return x[Math.floor(Math.random() * x.length)];
}

function initElement(element) {
var img = new Image();
img.onload = function() {
render(element, img, null);
}
img.src = element.getAttribute('data-letter-crap');
}

function getTextContentWithImageAtSize(image, width, height, existingText, words, letters) {
existingText = existingText ? existingText.replace(/\r?\n|\r/g, '') : null;
var shouldReplaceExisting = function() { return !existingText || Math.random() < likelihoodOfChangingExistingText };

var canvas = document.createElement('canvas');
canvas.width = parseInt(width / charWidth);
canvas.height = parseInt(height / charHeight);
canvas.getContext('2d').drawImage(image, 0, 0, canvas.width, canvas.height);
var data = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);
var chars = "";
var startOfFilledInSequence = 0;
var i = 0;
for (var y=0; y<data.height; y++) {
for (var x=0; x<data.width; x++) {
var black = data.data[i*4] < 120;
var transparent = data.data[i*4+3] < 50;
if (black && !transparent) {
if (startOfFilledInSequence === null) startOfFilledInSequence = i;

if (shouldReplaceExisting()) {
chars += randomChoice(letters);
} else {
chars += existingText[i];
}

if (words.length > 0 && Math.random() < likelihoodOfReplacingWord && shouldReplaceExisting()) {
var word = randomChoice(words);
if (i + 1 - startOfFilledInSequence >= word.length) {
chars = chars.substring(0, chars.length - word.length) + word;
}
}
} else {
chars += " ";
startOfFilledInSequence = null;
}
i++;
}
chars += "\n";
startOfFilledInSequence = null;
}
return chars
function createImageURL(text) {
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
let fontsize = measureTextBinaryMethod(text, 'monospace', 0, 10, canvas.width);
canvas.height = fontsize + 1;
canvas.width = context.measureText(text).width + 2;
context.fillText(text, 1, fontsize);
return canvas.toDataURL();

// https://jsfiddle.net/be6ppdre/29/
function measureTextBinaryMethod(text, fontface, min, max, desiredWidth) {
if (max - min < 1) return min;
let test = min + ((max - min) / 2); // Find half interval
context.font = `${test}px ${fontface}`;
let measureTest = context.measureText(text).width;

const condition = measureTest > desiredWidth;
return measureTextBinaryMethod(text, fontface, condition ? min : test,
condition ? test : max, desiredWidth);
}
}

function initElement(element) {
let img = new Image();
img.onload = () => render(element, img, null);
img.src = element.getAttribute('data-letter-crap');
img.crossOrigin = 'anonymous';
}

function getTextContentWithImageAtSize(image, width, height, existingText, words, letters) {
existingText = existingText ? existingText.replace(/\r?\n|\r/g, '') : null;
const shouldReplaceExisting = () => !existingText || Math.random() < likelihoodOfChangingExistingText;

let canvas = document.createElement('canvas');
canvas.width = parseInt(width / charWidth);
canvas.height = parseInt(height / charHeight);
canvas.getContext('2d').drawImage(image, 0, 0, canvas.width, canvas.height);
let data = canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height);
let chars = "";
let startOfFilledInSequence = 0;
let i = 0;

for (let y = 0; y < data.height; y++) {
for (let x = 0; x < data.width; x++) {
let black = data.data[i*4] < 120;
let transparent = data.data[i*4+3] < 50;
if (black && !transparent) {
if (startOfFilledInSequence === null) startOfFilledInSequence = i;
chars += shouldReplaceExisting() ? randomChoice(letters) : existingText[i];

if (words.length > 0 && Math.random() < likelihoodOfReplacingWord && shouldReplaceExisting()) {
let word = randomChoice(words);
if (i + 1 - startOfFilledInSequence >= word.length) {
chars = chars.substring(0, chars.length - word.length) + word;
}
}
} else {
chars += " ";
startOfFilledInSequence = null;
}
i++;
}
chars += "\n";
startOfFilledInSequence = null;
}
return chars;
}

function render(element, image, prev) {
if (element.hasAttribute('data-lettercrap-aspect-ratio')) {
var aspect = parseFloat(element.getAttribute('data-lettercrap-aspect-ratio'));
element.style.height = element.clientWidth * aspect + 'px';
}
var text;
var words = element.hasAttribute('data-lettercrap-words') ? element.getAttribute('data-lettercrap-words').split(' ') : [];
var letters = element.hasAttribute('data-lettercrap-letters') ? element.getAttribute('data-lettercrap-letters') : '0101010101_';
if (prev && prev.width == element.clientWidth && prev.height == element.clientHeight) {
text = getTextContentWithImageAtSize(image, element.clientWidth, element.clientHeight, prev.text, words, letters);
} else {
text = getTextContentWithImageAtSize(image, element.clientWidth, element.clientHeight, null, words, letters);
}
element.textContent = text;
var data = {width: element.clientWidth, height: element.clientHeight, text: text};
setTimeout(function() {
render(element, image, data);
}, 150);
if (element.hasAttribute('data-lettercrap-aspect-ratio')) {
let aspect = parseFloat(element.getAttribute('data-lettercrap-aspect-ratio'));
element.style.height = element.clientWidth * aspect + 'px';
}

let words = element.hasAttribute('data-lettercrap-words') ? element.getAttribute('data-lettercrap-words').split(' ') : [];
let letters = element.hasAttribute('data-lettercrap-letters') ? element.getAttribute('data-lettercrap-letters') : '0101010101_';
let textCondition = prev && prev.width === element.clientWidth && prev.height === element.clientHeight;
let text = getTextContentWithImageAtSize(image, element.clientWidth, element.clientHeight,
textCondition ? prev.text : null, words, letters);

element.textContent = text;
let data = {width: element.clientWidth, height: element.clientHeight, text: text};
setTimeout(() => render(element, image, data), updateInterval);
}

document.addEventListener('DOMContentLoaded', function() {
var elements = document.querySelectorAll('[data-letter-crap]');
for (var i=0; i<elements.length; i++) {
initElement(elements[i]);
}
})

document.addEventListener('DOMContentLoaded', function() {
let textElements = document.querySelectorAll('[data-lettercrap-text]');
for (let i = 0; i < textElements.length; i++) {
let text = textElements[i].getAttribute('data-lettercrap-text');
let imageURL = createImageURL(text);
textElements[i].setAttribute('data-letter-crap', imageURL);
}

let elements = document.querySelectorAll('[data-letter-crap]');
for (let i = 0; i < elements.length; i++) initElement(elements[i]);
})
})()