Skip to content

Instantly share code, notes, and snippets.

@jtmcdole
Created May 1, 2016 02:14
Show Gist options
  • Save jtmcdole/8e303a6cd9aa668f3b858422684bef2c to your computer and use it in GitHub Desktop.
Save jtmcdole/8e303a6cd9aa668f3b858422684bef2c to your computer and use it in GitHub Desktop.
simple canvas font texture atlas
import 'dart:async';
import 'dart:html';
import 'dart:convert';
// add <link href='https://fonts.googleapis.com/css?family=Roboto+Condensed' rel='stylesheet' type='text/css'> to html
// Draw red top, green ascent, and blue height
bool drawBounds = true || window.location.href.contains('drawBounds');
bool drawBox = true || window.location.href.contains('drawBox');
main() async {
await genFontMap(height: 18);
await genFontMap(height: 24);
await genFontMap(height: 32);
await genFontMap(height: 18, font: 'Roboto');
await genFontMap(height: 18, font: 'Roboto Condensed');
await genFontMap(height: 24, font: 'Roboto');
await genFontMap(height: 24, font: 'Roboto Condensed');
await genFontMap(height: 36, font: 'Roboto');
await genFontMap(height: 36, font: 'Roboto Condensed');
}
var str = new String.fromCharCodes(new List.generate(0xFF - 0x20, (i) => i));
Future<Map> getHeight(font) async {
var ascent;
var descent;
var height;
var block = new DivElement();
block.style
..display = 'inline-block'
..width = '1px'
..height = '0px';
var text = new SpanElement()
..style.font = font
..text = str;
var div = new DivElement();
div.style.whiteSpace = 'nowrap';
div..append(text)..append(block);
document.body.append(div);
block.style.verticalAlign = 'baseline';
await new Future.value();
ascent = block.offsetTop - text.offsetTop;
block.style.verticalAlign = 'bottom';
await new Future.value();
height = block.offsetTop - text.offsetTop;
descent = height - ascent;
print('$font: height:$height descent:$descent ascent:$ascent');
div.remove();
return {'descent': descent, 'ascent': ascent, 'height': height};
}
marker(can, x, y, style) {
can
..strokeStyle = style
..beginPath()
..lineWidth = 1
..moveTo(0, y)
..lineTo(x, y)
..closePath()
..stroke();
}
genFontMap(
{int height: 32, String font: 'monospace', String style: '#fff'}) async {
const int canWidth = 512;
var json = await getHeight('${height}px $font');
var ascent = json['ascent'];
var rHeight = json['height'];
var descent = json['descent'];
document.body.append(new Element.html("<span>$font $json</span>")
..style.font = '${height}px $font');
document.body.append(new Element.br());
await new Future.delayed(const Duration(milliseconds: 200));
var ele = new CanvasElement(width: canWidth, height: 512);
var can = ele.context2D;
can.textBaseline = 'top';
can.fillStyle = style;
can.font = '${height}px $font';
Map characters = {};
json['characters'] = characters;
List<num> widths = new List.filled(0xFF, 0);
try {
can.clearRect(0, 0, 512, 512);
var x = 2.5;
var y = 2.5;
for (int i = 0x20; i < 0xFF; i++) {
if (i == 0x7F) {
i += 32;
continue;
}
var char = new String.fromCharCode(i);
var metrics = can.measureText(char);
widths[i] = metrics.width;
if ((x + metrics.width) > canWidth) {
y += rHeight + 2;
x = 2.5;
}
if (x == 2.5) {
if (drawBounds) {
marker(can, canWidth, y, 'rgba(255, 0, 0, 1.0)');
marker(can, canWidth, y + ascent, 'rgba(0, 255, 0, 1.0)');
marker(can, canWidth, y + rHeight, 'rgba(0, 0, 255, 1.0)');
}
}
if (drawBox) {
can.strokeStyle = 'rgba(255, 255, 0, 1.0)';
can.rect(x.toInt() + .5, y.toInt() + .5, metrics.width.ceil(), rHeight);
can.stroke();
}
characters[char] = [x.toInt(), y.toInt(), metrics.width];
can.fillText(new String.fromCharCode(i), x, y);
x += metrics.width.ceil() + 2.5;
}
y += rHeight;
var kerning = {};
json['kerning'] = kerning;
// collect kerning now we have all single letter widths
for (int left = 0x20; left < 0xFF; left++) {
if (left == 0x7F) {
left += 32;
continue;
}
for (int right = 0x20; right < 0xFF; right++) {
if (right == 0x7F) {
right += 32;
continue;
}
var sum = widths[left] + widths[right];
var pair = new String.fromCharCodes([left, right]);
var cWidth = can.measureText(pair).width;
cWidth = cWidth - sum;
if (cWidth != 0.0) {
kerning[pair] = cWidth;
}
}
}
// force into power of 2 textur
if (y < 32)
y = 32;
else if (y < 64)
y = 64;
else if (y < 128)
y = 128;
else if (y < 256)
y = 256;
else if (y < 512) y = 512;
var ele2 = new CanvasElement(width: canWidth, height: y);
var can2 = ele2.context2D;
can2.drawImage(ele, 0, 0);
json['font_height_px'] = height;
json['font'] = font;
json['style'] = style;
json['img_width'] = canWidth;
json['img_height'] = y;
print('$font: $canWidth x $y');
document.body.append(ele2);
document.body.append(new Element.br());
var filename = '$font.$height';
var img = new AnchorElement()
..text = '$filename.data'
..download = '$filename.data';
img.href = 'data:application/octet-stream;base64,'
'${BASE64.encode(can2.getImageData(0, 0, canWidth, y).data)}';
document.body.append(img);
document.body.append(new Element.br());
document.body.append(new AnchorElement()
..download = '$filename.json'
..text = '$filename.json'
..href = 'data:text/html;charset=utf-8,${JSON.encode(json)}');
document.body.append(new Element.br());
} catch (e, s) {
print('error: $e, stack: $s');
}
return json;
}
<html>
<head>
<link href='https://fonts.googleapis.com/css?family=Roboto:400,300,300italic,400italic,700,700italic' rel='stylesheet' type='text/css'>
<link href='https://fonts.googleapis.com/css?family=Roboto+Condensed:400,300,300italic,400italic,700,700italic' rel='stylesheet' type='text/css'>
<script defer type="application/dart" src="canvas_font_texture.dart"></script>
<script defer src="packages/browser/dart.js"></script>
<style>
a {
color: #49F;
}
body {
background: #666;
}
</style>
</head>
<body>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment