Skip to content

Instantly share code, notes, and snippets.

@burdiuz
Last active December 5, 2016 19:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save burdiuz/668290ad10fe4fd5ad26af8577a11b95 to your computer and use it in GitHub Desktop.
Save burdiuz/668290ad10fe4fd5ad26af8577a11b95 to your computer and use it in GitHub Desktop.
Create convex text from canvas

ConvexText

Result of ConvexText use

API

Install

via npm

npm install gist:668290ad10fe4fd5ad26af8577a11b95 --save

via git, dependencies excluded

git clone https://gist.github.com/burdiuz/668290ad10fe4fd5ad26af8577a11b95 ConvexText

Dependencies

'use strict';
import Pixels from 'Pixels';
import {HSBA, RGBA} from 'Color';
const pixels = new Pixels();
const hsba = new HSBA();
const PI_2 = Math.PI * 0.5;
const lookup = (pixels, x, y, cx, cy, max) => {
let s = 0;
do {
s++;
x += cx;
y += cy;
} while (pixels.getPixelAlpha(x, y) && (!max || s < max));
return s;
}
const makeGradientFont = (pixels, width, thickness, flatness) => {
let pos = 0;
const maxDistance = thickness * 0.6;
const edgeMult = 1 - flatness;
for (let color of pixels) {
if (RGBA.getAlpha(color) > 0) {
const x = pos % width;
const y = pos / width >> 0;
const t = lookup(pixels, x, y, -1, 0, maxDistance); // go top
const b = lookup(pixels, x, y, 1, 0, maxDistance); // go bottom
const l = lookup(pixels, x, y, 0, -1, maxDistance); // go left
const r = lookup(pixels, x, y, 0, 1, maxDistance); // go right
const v = t + b;
const h = l + r;
const edge = Math.min(t, l, r, b);
hsba.value = color;
let c;
if (v < h) { // vertical
c = Math.sin(PI_2 * (edge / v));
;
} else { // horizontal
c = Math.sin(PI_2 * (edge / h));
}
hsba.b = flatness + edgeMult * c;
pixels.setPixelByPosition(pos, hsba.value);
}
pos++;
}
};
/**
Used to partially store context state before changing canvas size.
*/
const copyContextTextState = (source) => {
let { strokeStyle, fillStyle, globalAlpha, lineWidth, lineCap, lineJoin, miterLimit, lineDashOffset, font, textAlign, textBaseline, direction, imageSmoothingEnabled } = source;
return {
strokeStyle,
fillStyle,
globalAlpha,
lineWidth,
lineCap,
lineJoin,
miterLimit,
lineDashOffset,
font,
textAlign,
textBaseline,
direction,
imageSmoothingEnabled
};
};
const CanvasRenderingContext2DFontAPIMixin = (Parent) => class extends Parent {
get font() {
return this._context.font;
}
set font(value) {
this._context.font = value;
}
get fillStyle() {
return this._context.fillStyle;
}
set fillStyle(value) {
this._context.fillStyle = value;
}
get strokeStyle() {
return this._context.strokeStyle;
}
set strokeStyle(value) {
this._context.strokeStyle = value;
}
get textAlign() {
return this._context.textAlign;
}
set textAlign(value) {
this._context.textAlign = value;
}
get textBaseline() {
return this._context.textBaseline;
}
set textBaseline(value) {
this._context.textBaseline = value;
}
measureText(text) {
return this._context.measureText(text);
}
fillText(text, x, y, maxWidth) {
return this._context.fillText(text, x, y, maxWidth);
}
strokeText(text, x, y, maxWidth) {
return this._context.strokeText(text, x, y, maxWidth);
}
get context() {
this._context;
}
}
export class ConvexText {
constructor(canvas) {
if (!canvas) {
canvas = ConvexText.createCanvas();
}
this.canvas = canvas;
this._fontThickness = 0;
this._fontFlatness = 0.3;
}
get canvas() {
return this._canvas;
}
set canvas(canvas) {
if (canvas) {
this._canvas = typeof(canvas) === 'string' ? document.getElementById(canvas) : canvas;
this._context = this._canvas.getContext('2d');
}
}
get fontThickness() {
return this._fontThickness;
}
set fontThickness(value) {
this._fontThickness = parseInt(value) || 0;
}
get fontFlatness() {
return this._fontFlatness;
}
set fontFlatness(value) {
this._fontFlatness = Math.max(0, Math.min(1, parseFloat(value) || 0));
}
getText(text) {
const data = ConvexText.getText(this._context, text, this._fontThickness, this._fontFlatness);
ConvexText.clear(this._context);
return data;
}
getTextImage(text, mimeType, qualityArgument) {
const image = ConvexText.getTextImage(this._context, text, this._fontThickness, this._fontFlatness, mimeType, qualityArgument);
ConvexText.clear(this._context);
return image;
}
static createCanvas() {
return document.createElement('canvas');
}
static clear(context) {
const canvas = context.canvas;
context.clearRect(0, 0, canvas.width, canvas.height);
}
static getTextArea(context, text) {
const measurement = context.measureText(text);
return {
width: Math.ceil(measurement.width),
height: parseInt(context.font.match(/(?:^|\s)(\d+)\w{2,}(?=$|\s)/)[1])
};
}
static validateCanvasSize(context, width, height, force = false) {
const canvas = context.canvas;
let newWidth = width;
let newHeight = height;
if (!force) {
newWidth = Math.max(canvas.width, width);
newHeight = Math.max(canvas.height, height);
}
if (newWidth !== canvas.width || newHeight !== canvas.height) {
const state = copyContextTextState(context, {});
canvas.width = newWidth;
canvas.height = newHeight;
Object.assign(context, state);
}
}
static _getImageData(context, thickness, flatness, x, y, width, height) {
const image = context.getImageData(x, y, width, height);
pixels.image = image;
makeGradientFont(pixels, width, thickness, flatness);
return image;
}
static getArea(context, thickness = 0, flatness = 0, x = 0, y = 0, width = 0, height = 0) {
width = width || context.canvas.width;
height = height || context.canvas.height;
const image = ConvexText._getImageData(context, thickness, flatness, x, y, width, height);
pixels.image = null;
return image;
}
static applyArea(context, thickness = 0, flatness = 0, x = 0, y = 0, width = 0, height = 0) {
const image = ConvexText.getArea(context, thickness, flatness, x, y, width, height);
context.putImageData(image, x, y);
}
static getText(context, text, thickness = 0, flatness = 0, x = 0, y = 0) {
const area = ConvexText.getTextArea(context, text);
ConvexText.validateCanvasSize(context, area.width, area.height);
context.fillText(text, x, y);
return ConvexText.getArea(context, thickness, flatness, x, y, area.width, area.height);
}
static getTextImage(context, text, thickness = 0, flatness = 0, mimeType = 'image/png', qualityArgument = 1) {
const canvas = context.canvas;
const area = ConvexText.getTextArea(context, text);
ConvexText.validateCanvasSize(context, area.width, area.height, true);
context.fillText(text, 0, area.height);
ConvexText.applyArea(context, thickness, flatness, 0, 0, area.width, area.height);
const img = new Image();
img.src = canvas.toDataURL(mimeType, qualityArgument);
img.width = area.width;
img.height = area.height;
return img;
}
}
export default class CanvasConvexText extends CanvasRenderingContext2DFontAPIMixin(ConvexText) {
constructor(canvas) {
super(canvas);
}
}
<!DOCTYPE html>
<html lang="en" >
<head >
<meta charset="UTF-8" >
<meta name="viewport" content="width=device-width, initial-scale=1" >
<title >Convex Text</title >
</head >
<body >
</body >
<script type="text/javascript" src="../node_modules/Color/Color.js" ></script >
<script type="text/javascript" src="../node_modules/Pixels/Pixels.js" ></script >
<script type="text/javascript" src="../index.js" ></script >
<script type="text/javascript" >
const text = new CanvasConvexText();
text.fillStyle = '#FF0000';
text.font = '140px Verdana';
text.fontThickness = 14;
document.body.appendChild(text.getTextImage('ConvexText'));
</script >
</html >
{
"name": "ConvexText",
"description": "Create convex text using canvas",
"version": "0.0.1",
"main": "ConvexText.js",
"dependencies": {
"Pixels": "https://gist.github.com/burdiuz/1788a9282f111476e966aea118100eac",
"Color": "https://gist.github.com/burdiuz/bca61f39fc4819768eb55a7c812f7e52"
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment