|
'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); |
|
} |
|
} |