"Enluminure" /ɑ̃.ly.mi.nyʁ/ is a french word meaning the technique of illuminating manuscript with flourishes and drawings in order to beautify the text content.
Although the french term means creating a picture alongside a written text, this javascript library turns any picture into a a composition of colorful letters. It uses canvases internally to perform the conversion, so it only works inside of browsers.
Enluminure is a really simple algorithm that takes its inspiration from ASCII art. It works by first slicing a source picture into tiles. These tiles are then iterated over and their lightness is extracted. When the extracting process is done, the rendering starts by assigning a letter from a given pool (a word, a random chain of characters, ...) for each of the tiles. This letter will be as bright as the source tile, but with a different hue.
This is as simple as that, but sometimes the simpler solutions are usually the most effective.
Below you'll find a detail from a Boticelli painting and some enluminures rendering of it.
![]() |
![]() |
![]() |
![]() |
The library is available as a Typescript library in the src directory. To compile it to ES6 into a dist
directory, run the following commandes after cloning.
# Install the dependencies
npm install
# Creates a directory named dist with an "enluminure.js" file
npm run compile
You can also use Vite bundler provided in the dependencies to run the example file by running :
npm run dev
Creates an enluminure from an image and adding it to the DOM using an <img>
tag:
import Enluminure from './src/enluminure'
const enluminure = new Enluminure()
await enluminure.loadImage('./path/to/img.jpg')
const img = document.createElement('img')
img.src = enluminure.render('image/png')
document.body.appendChild(img)
Since render can take a while it could be useful to spawn a dedicated worker thread to process the rendering so the main execution thread is not blocked.
main.js
:
const enluminure = new Enluminure()
await enluminure.loadImage('./path/to/img.jpg')
const renderWorker = new Worker('render.js')
renderWorker.postMessage(enluminure)
renderWorker.onmessage = ({ data }) => {
const img = document.createElement('img')
img.src = data
document.body.appendChild(img)
}
render.js
:
onmessage = ({ data }) => {
const img = data.render()
postMessage(img)
}
import Enluminure from './src/lib/ts/enluminure'
const component = () => {
const [src, setSrc] = useState(null)
const handleChange = e => {
const f = e.target.files[0]
const enluminure = new Enluminure()
await enluminure.loadImage(f)
setSrc( enluminure.render('image/jpeg') )
}
return (
<div>
<input type="file" onChange={handleChange} />
{ src && <img {src} /> }
</div>
)
}
You can specify options from the start using the constructor or using the setOptions
method. There are several options available. Make sure to fine tune each of them. Possibilities are nearly endless, from suggestive numeric art to near-copy of the source, there is a lot of room to explore in between!
All parameters give deterministic output excepted:
- Those used with the RANDOM preset (hueRotation, characterDistribution)
- The jitter function
This means you can easily predict an output based on input parameters.
Name | Values | Default Value | Description |
---|---|---|---|
backgroundColor | A color string (Hex, rgb(), hsl()) | '#000000' | The background color of the generated enluminure |
characterDistribution | see CharacterDistibutions enum | ROW | Defines how each letter is pasted onto the enluminure. On ROW mode each character from the characterPool is laid from left to right, on COLUMN from top to bottom, on RANDOM in no specific order |
characterPool | A string | 'ËNłÛMÍИЦR€' | A set of letters to generate the enluminure |
focusGradientOpacity | A number from 0 (transparent) to 1 (opaque) | 0 | Creates a radial gradient on top of the enluminure to better highlight the center of the image |
fontFamily | A valid font string | 'monospace' | Defines the font used to generate the enluminure |
fontSize | A number | 6 | Defines the size of the font on the enluminure |
hueMax | A number between 0 and 360 | 60 | The enluminure will be drawn using a color palette, between [hueMin , hueMax ]. You can restrict the generated picture to a certain part of the spectrum using these two parameters |
hueMin | A number between 0 and 360 | 0 | (see hueMax ) |
hueRotation | see HueRotations enum | LINEAR_FORWARD | Defines how the colors are distributed on the enluminure. The hue is selected from the HSL circle, either on a forward motion, a back and forth motion, a random value, or with a trigonometric function crafting scattered hues on the generated enluminure. |
jitterProbability | A number between 0 and 1 | 0 | The probability that the jitter function (see maxJitterOffset ) gets triggered.
|
luminanceFactor | Number | 100 | The luminance factor defines the scaling of the Lightness parameter in the HSL circle. Greater value will give brighter images and greater contrasts, smaller value will give more colorful images |
maxJitterOffsetX | number | 0 | The jitter function will move a character from it's original position to a randomly assigned position along the X-axis. This parameter gives the maximum distance, in px, by which the character can be moved from it's original offset. For the jitter function to be triggered you should also check the jitterProbability option |
maxJitterOffsetY | number | 0 | The jitter function will move a character from it's original position to a randomly assigned position along the Y-axis. This parameter gives the maximum distance, in px, by which the character can be moved from it's original offset. For the jitter function to be triggered you should also check the jitterProbability option |
saturation | A number between 0 and 100 | 100 | This parameter controls the overall saturation of the enluminure. 100 will give you satured images across the whole color spectrum, 0 will render a greyscale enluminure. |
tileSize | Any number (greater than 0) | 6 | Enluminures are generated by iterating over the source picture at a given scale, this parameter specifies the size of each step. Smaller values will give you more detailed image (at the expense of more processing time). |
Initialize the enluminure with a set of (optional) parameters.
const defaultEnluminure = new Enluminure()
const customEnluminure = new Enluminre({
jitterChance: 0.2,
maxJitterOffsetX: 50,
maxJitterOffsetY: 10,
saturation: 20,
})
Loads a reference image for the enluminure. If the file is succesfully loaded returns the dimensions of the enluminure to render.
Returns the dimensions of the enluminure to be generated. The size of the enluminure could be slightly smaller than the source picture, since it is always a multiple of the tileSize
parameter.
Returns the enluminure current options.
const enluminure = new Enluminure()
const fileInput = document.createElement('input')
fileInput.type = 'file'
fileInput.addEventListener('change', async e => {
await enluminure.loadImage(e.target.files[0])
const result = enluminure.render()
const img = document.createElement('img')
img.src = result
document.body.appendChild(result)
})
document.body.appendChild(fileInput)
const enluminure = new Enluminure()
await enluminure.loadImage('picture.jpg')
const result = enluminure.render()
const img = document.createElement('img')
img.src = result
document.body.appendChild(result)
Set options for the generated enluminure after initialization (ex: taking parameters from user input)
const enluminure = new Enluminure()
await enluminure.loadImage('picture.jpg')
document.querySelector('input[type="range"]').addEventListener('change', e => {
enluminure.setOptions({ saturation: e.value })
})
Renders the enluminure and returns the result as a data URL. You can specify a custom mimetype for the rendered image. The default values depends of your browser.