diff --git a/dist/nomnoml.js b/dist/nomnoml.js index 17b6c34..6be1761 100644 --- a/dist/nomnoml.js +++ b/dist/nomnoml.js @@ -224,61 +224,40 @@ var nomnoml; })(nomnoml || (nomnoml = {})); var nomnoml; (function (nomnoml) { + nomnoml.version = '0.10.0'; function fitCanvasSize(canvas, rect, zoom) { canvas.width = rect.width * zoom; canvas.height = rect.height * zoom; } - function setFont(config, isBold, isItalic, graphics) { - var style = (isBold === 'bold' ? 'bold' : ''); - if (isItalic) - style = 'italic ' + style; - var defaultFont = 'Helvetica, sans-serif'; - var font = nomnoml.skanaar.format('# #pt #, #', style, config.fontSize, config.font, defaultFont); - graphics.font(font); - } - function parseAndRender(code, graphics, canvas, scale) { - var parsedDiagram = nomnoml.parse(code); - var config = parsedDiagram.config; - var measurer = { + function Measurer(config, graphics) { + return { setFont: function (conf, bold, ital) { - setFont(conf, bold, ital, graphics); + graphics.setFont(conf.font, bold, ital, config.fontSize); }, textWidth: function (s) { return graphics.measureText(s).width; }, textHeight: function () { return config.leading * config.fontSize; } }; + } + ; + function parseAndRender(code, graphics, canvas, scale) { + var parsedDiagram = nomnoml.parse(code); + var config = parsedDiagram.config; + var measurer = Measurer(config, graphics); var layout = nomnoml.layout(measurer, config, parsedDiagram.root); - fitCanvasSize(canvas, layout, config.zoom * scale); + if (canvas) { + fitCanvasSize(canvas, layout, config.zoom * scale); + } config.zoom *= scale; nomnoml.render(graphics, config, layout, measurer.setFont); - return { config: config }; + return { config: config, layout: layout }; } - nomnoml.version = '0.10.0'; function draw(canvas, code, scale) { return parseAndRender(code, nomnoml.skanaar.Canvas(canvas), canvas, scale || 1); } nomnoml.draw = draw; - function renderSvg(code, docCanvas) { - var parsedDiagram = nomnoml.parse(code); - var config = parsedDiagram.config; - var skCanvas = nomnoml.skanaar.Svg('', docCanvas); - function setFont(config, isBold, isItalic) { - var style = (isBold === 'bold' ? 'bold' : ''); - if (isItalic) - style = 'italic ' + style; - var defFont = 'Helvetica, sans-serif'; - var template = 'font-weight:#; font-size:#pt; font-family:\'#\', #'; - var font = nomnoml.skanaar.format(template, style, config.fontSize, config.font, defFont); - skCanvas.font(font); - } - var measurer = { - setFont: function (conf, bold, ital) { - setFont(conf, bold, ital); - }, - textWidth: function (s) { return skCanvas.measureText(s).width; }, - textHeight: function () { return config.leading * config.fontSize; } - }; - var layout = nomnoml.layout(measurer, config, parsedDiagram.root); - nomnoml.render(skCanvas, config, layout, measurer.setFont); + function renderSvg(code, document) { + var skCanvas = nomnoml.skanaar.Svg('', document); + var _a = parseAndRender(code, skCanvas, null, 1), config = _a.config, layout = _a.layout; return skCanvas.serialize({ width: layout.width, height: layout.height @@ -366,7 +345,7 @@ var nomnoml; fill: (d.fill || '#eee8d5;#fdf6e3;#eee8d5;#fdf6e3').split(';'), background: d.background || 'transparent', fillArrows: d.fillArrows === 'true', - font: d.font || 'Calibri', + font: d.font || 'Helvetica', fontSize: (+d.fontSize) || 12, leading: (+d.leading) || 1.25, lineWidth: (+d.lineWidth) || 3, @@ -716,7 +695,9 @@ var nomnoml; ctx.closePath(); return chainable; }, - font: function (f) { ctx.font = f; }, + setFont: function (font, bold, ital, fontSize) { + ctx.font = bold + " " + (ital || '') + " " + fontSize + "pt " + font + ", Helvetica, sans-serif"; + }, fillStyle: function (s) { ctx.fillStyle = s; }, strokeStyle: function (s) { ctx.strokeStyle = s; }, textAlign: function (a) { ctx.textAlign = a; }, @@ -752,7 +733,8 @@ var nomnoml; .replace(/"/g, '"') .replace(/'/g, '''); } - function Svg(globalStyle, canvas) { + skanaar.charWidths = { "0": 9, "1": 9, "2": 9, "3": 9, "4": 9, "5": 9, "6": 9, "7": 9, "8": 9, "9": 9, " ": 4, "!": 4, "\"": 6, "#": 9, "$": 9, "%": 14, "&": 11, "'": 3, "(": 5, ")": 5, "*": 6, "+": 9, ",": 4, "-": 5, ".": 4, "/": 4, ":": 4, ";": 4, "<": 9, "=": 9, ">": 9, "?": 9, "@": 16, "A": 11, "B": 11, "C": 12, "D": 12, "E": 11, "F": 10, "G": 12, "H": 12, "I": 4, "J": 8, "K": 11, "L": 9, "M": 13, "N": 12, "O": 12, "P": 11, "Q": 12, "R": 12, "S": 11, "T": 10, "U": 12, "V": 11, "W": 15, "X": 11, "Y": 11, "Z": 10, "[": 4, "\\": 4, "]": 4, "^": 8, "_": 9, "`": 5, "a": 9, "b": 9, "c": 8, "d": 9, "e": 9, "f": 4, "g": 9, "h": 9, "i": 4, "j": 4, "k": 8, "l": 4, "m": 13, "n": 9, "o": 9, "p": 9, "q": 9, "r": 5, "s": 8, "t": 4, "u": 9, "v": 8, "w": 12, "x": 8, "y": 8, "z": 8, "{": 5, "|": 4, "}": 5, "~": 9 }; + function Svg(globalStyle, document) { var initialState = { x: 0, y: 0, @@ -761,14 +743,13 @@ var nomnoml; dashArray: 'none', fill: 'none', textAlign: 'left', - font: null + font: 'Helvetica, Arial, sans-serif', + fontSize: 12 }; var states = [initialState]; var elements = []; - var ctx = canvas ? canvas.getContext('2d') : null; - var canUseCanvas = false; - var waitingForFirstFont = true; - var docFont = ''; + var measurementCanvas = document ? document.createElement('canvas') : null; + var ctx = measurementCanvas ? measurementCanvas.getContext('2d') : null; function Element(name, attr, content) { return { name: name, @@ -800,7 +781,7 @@ var nomnoml; }; } function State(dx, dy) { - return { x: dx, y: dy, stroke: null, strokeWidth: null, fill: null, textAlign: null, dashArray: 'none', font: null }; + return { x: dx, y: dy, stroke: null, strokeWidth: null, fill: null, textAlign: null, dashArray: 'none', font: null, fontSize: null }; } function trans(coord, axis) { states.forEach(function (t) { coord += t[axis]; }); @@ -863,31 +844,10 @@ var nomnoml; element.attr.d += ' Z'; return element; }, - font: function (font) { + setFont: function (font, bold, ital, fontSize) { + var font = bold + " " + (ital || '') + " " + fontSize + "pt " + font + ", Helvetica, sans-serif"; last(states).font = font; - if (waitingForFirstFont) { - if (ctx) { - var primaryFont = font.replace(/^.*family:/, '').replace(/[, ].*$/, ''); - primaryFont = primaryFont.replace(/'/g, ''); - canUseCanvas = /^(Arial|Helvetica|Times|Times New Roman)$/.test(primaryFont); - if (canUseCanvas) { - var fontSize = font.replace(/^.*font-size:/, '').replace(/;.*$/, '') + ' '; - if (primaryFont === 'Arial') { - docFont = fontSize + 'Arial, Helvetica, sans-serif'; - } - else if (primaryFont === 'Helvetica') { - docFont = fontSize + 'Helvetica, Arial, sans-serif'; - } - else if (primaryFont === 'Times New Roman') { - docFont = fontSize + '"Times New Roman", Times, serif'; - } - else if (primaryFont === 'Times') { - docFont = fontSize + 'Times, "Times New Roman", serif'; - } - } - } - waitingForFirstFont = false; - } + last(states).fontSize = fontSize; }, strokeStyle: function (stroke) { last(states).stroke = stroke; @@ -904,14 +864,8 @@ var nomnoml; fillText: function (text, x, y) { var attr = { x: tX(x), y: tY(y), style: 'fill: ' + last(states).fill + ';' }; var font = lastDefined('font'); - if (font.indexOf('bold') === -1) { - attr.style += 'font-weight:normal;'; - } - else { - attr.style += 'font-weight:bold;'; - } - if (font.indexOf('italic') > -1) { - attr.style += 'font-style:italic;'; + if (font) { + attr.style += 'font:' + font + ';'; } if (lastDefined('textAlign') === 'center') { attr.style += 'text-anchor: middle;'; @@ -928,20 +882,18 @@ var nomnoml; last(states).strokeWidth = w; }, measureText: function (s) { - if (canUseCanvas) { - var fontStr = lastDefined('font'); - var italicSpec = (/\bitalic\b/.test(fontStr) ? 'italic' : 'normal') + ' normal '; - var boldSpec = /\bbold\b/.test(fontStr) ? 'bold ' : 'normal '; - ctx.font = italicSpec + boldSpec + docFont; + if (ctx) { + ctx.font = lastDefined('font') || 'normal 12pt Helvetica'; return ctx.measureText(s); } else { return { width: skanaar.sum(s, function (c) { - if (c === 'M' || c === 'W') { - return 14; + var scale = lastDefined('fontSize') / 12; + if (skanaar.charWidths[c]) { + return skanaar.charWidths[c] * scale; } - return c.charCodeAt(0) < 200 ? 9.5 : 16; + return 16 * scale; }) }; } @@ -994,7 +946,7 @@ var nomnoml; xmlns: 'http://www.w3.org/2000/svg', 'xmlns:xlink': 'http://www.w3.org/1999/xlink', 'xmlns:ev': 'http://www.w3.org/2001/xml-events', - style: lastDefined('font') + ';' + globalStyle + style: 'font:' + lastDefined('font') + ';' + globalStyle }; return ''; } @@ -1316,7 +1268,7 @@ var nomnoml; nomnoml.visualizers = { actor: function (node, x, y, config, g) { var a = config.padding / 2; - var yp = y + a * 3; + var yp = y + a * 4; var faceCenter = { x: node.x, y: yp - a }; g.circle(faceCenter, a).fillAndStroke(); g.path([{ x: node.x, y: yp }, { x: node.x, y: yp + 2 * a }]).stroke(); diff --git a/src/Graphics.ts b/src/Graphics.ts index 9d37a62..c3a0362 100644 --- a/src/Graphics.ts +++ b/src/Graphics.ts @@ -20,7 +20,7 @@ interface Graphics { rect(x: number, y: number, w: number, h: number): Chainable path(points: Vector[]): Chainable circuit(path: Vector[], offset?: Vec, s?: number): Chainable - font(font: string): void + setFont(fontFamily: string, bold: 'bold'|'normal', italic: 'italic'|null, fontSize: number): void strokeStyle(stroke: string): void fillStyle(fill: any): void arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void diff --git a/src/nomnoml.ts b/src/nomnoml.ts index 5ab12dd..216849b 100644 --- a/src/nomnoml.ts +++ b/src/nomnoml.ts @@ -13,6 +13,8 @@ interface Nomnoml { namespace nomnoml { + export var version = '0.10.0' + export interface SetFont { (config: Config, isBold: string, isItalic?: string): void } @@ -24,59 +26,34 @@ namespace nomnoml { canvas.height = rect.height * zoom; } - function setFont(config: Config, isBold: 'bold'|'normal', isItalic: 'italic'|undefined, graphics: Graphics) { - var style = (isBold === 'bold' ? 'bold' : '') - if (isItalic) style = 'italic ' + style - var defaultFont = 'Helvetica, sans-serif' - var font = skanaar.format('# #pt #, #', style, config.fontSize, config.font, defaultFont) - graphics.font(font) - } + function Measurer(config: Config, graphics: Graphics) { + return { + setFont(conf: Config, bold: 'bold'|'normal', ital:'italic'|undefined): void { + graphics.setFont(conf.font, bold, ital, config.fontSize) + }, + textWidth: function (s: string): number { return graphics.measureText(s).width }, + textHeight: function (): number { return config.leading * config.fontSize } + } + }; function parseAndRender(code: string, graphics: Graphics, canvas: HTMLCanvasElement, scale: number) { var parsedDiagram = parse(code) var config = parsedDiagram.config - var measurer = { - setFont(conf: Config, bold: 'bold'|'normal', ital:'italic'|undefined): void { - setFont(conf, bold, ital, graphics) - }, - textWidth(s: string): number { return graphics.measureText(s).width }, - textHeight(): number { return config.leading * config.fontSize } - }; + var measurer = Measurer(config, graphics) var layout = nomnoml.layout(measurer, config, parsedDiagram.root) - fitCanvasSize(canvas, layout, config.zoom * scale) + if (canvas) { fitCanvasSize(canvas, layout, config.zoom * scale) } config.zoom *= scale nomnoml.render(graphics, config, layout, measurer.setFont) - return { config: config } + return { config: config, layout: layout } } - export var version = '0.10.0' - export function draw(canvas: HTMLCanvasElement, code: string, scale: number): { config: Config } { return parseAndRender(code, skanaar.Canvas(canvas), canvas, scale || 1) } - export function renderSvg(code: string, docCanvas?: HTMLCanvasElement): string { - var parsedDiagram = parse(code) - var config = parsedDiagram.config - var skCanvas = skanaar.Svg('', docCanvas) - function setFont(config: Config, isBold: 'bold'|'normal', isItalic: 'italic'|undefined) { - var style = (isBold === 'bold' ? 'bold' : '') - if (isItalic) style = 'italic ' + style - var defFont = 'Helvetica, sans-serif' - var template = 'font-weight:#; font-size:#pt; font-family:\'#\', #' - var font = skanaar.format(template, style, config.fontSize, config.font, defFont) - skCanvas.font(font) - } - var measurer = { - setFont(conf: Config, bold: 'bold'|'normal', ital:'italic'|undefined): void { - setFont(conf, bold, ital) - }, - textWidth: function (s: string): number { return skCanvas.measureText(s).width }, - textHeight: function (): number { return config.leading * config.fontSize } - }; - var layout = nomnoml.layout(measurer, config, parsedDiagram.root) - - nomnoml.render(skCanvas, config, layout, measurer.setFont) + export function renderSvg(code: string, document?: HTMLDocument): string { + var skCanvas = skanaar.Svg('', document) + var { config, layout } = parseAndRender(code, skCanvas, null, 1) return skCanvas.serialize({ width: layout.width, height: layout.height diff --git a/src/parser.ts b/src/parser.ts index 67afed1..5c1b8cc 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -96,7 +96,7 @@ namespace nomnoml { fill: (d.fill || '#eee8d5;#fdf6e3;#eee8d5;#fdf6e3').split(';'), background: d.background || 'transparent', fillArrows: d.fillArrows === 'true', - font: d.font || 'Calibri', + font: d.font || 'Helvetica', fontSize: (+d.fontSize) || 12, leading: (+d.leading) || 1.25, lineWidth: (+d.lineWidth) || 3, diff --git a/src/skanaar.canvas.ts b/src/skanaar.canvas.ts index bdf5f7e..f8c3008 100644 --- a/src/skanaar.canvas.ts +++ b/src/skanaar.canvas.ts @@ -115,7 +115,9 @@ namespace nomnoml.skanaar { ctx.closePath() return chainable }, - font: function (f){ ctx.font = f }, + setFont: function (font: string, bold: 'bold'|'normal', ital:'italic'|null, fontSize: number): void { + ctx.font = `${bold} ${ital || ''} ${fontSize}pt ${font}, Helvetica, sans-serif` + }, fillStyle: function (s){ ctx.fillStyle = s }, strokeStyle: function (s){ ctx.strokeStyle = s }, textAlign: function (a){ ctx.textAlign = a as CanvasTextAlign }, diff --git a/src/skanaar.svg.ts b/src/skanaar.svg.ts index 42555d2..0031722 100644 --- a/src/skanaar.svg.ts +++ b/src/skanaar.svg.ts @@ -9,6 +9,7 @@ namespace nomnoml.skanaar { fill: string|null textAlign: string|null font: string|null + fontSize: number|null } interface SvgGraphics extends Graphics { @@ -24,7 +25,9 @@ namespace nomnoml.skanaar { .replace(/'/g, ''') } - export function Svg(globalStyle: string, canvas?: HTMLCanvasElement): SvgGraphics { + export var charWidths: any = {"0":9,"1":9,"2":9,"3":9,"4":9,"5":9,"6":9,"7":9,"8":9,"9":9," ":4,"!":4,"\"":6,"#":9,"$":9,"%":14,"&":11,"'":3,"(":5,")":5,"*":6,"+":9,",":4,"-":5,".":4,"/":4,":":4,";":4,"<":9,"=":9,">":9,"?":9,"@":16,"A":11,"B":11,"C":12,"D":12,"E":11,"F":10,"G":12,"H":12,"I":4,"J":8,"K":11,"L":9,"M":13,"N":12,"O":12,"P":11,"Q":12,"R":12,"S":11,"T":10,"U":12,"V":11,"W":15,"X":11,"Y":11,"Z":10,"[":4,"\\":4,"]":4,"^":8,"_":9,"`":5,"a":9,"b":9,"c":8,"d":9,"e":9,"f":4,"g":9,"h":9,"i":4,"j":4,"k":8,"l":4,"m":13,"n":9,"o":9,"p":9,"q":9,"r":5,"s":8,"t":4,"u":9,"v":8,"w":12,"x":8,"y":8,"z":8,"{":5,"|":4,"}":5,"~":9} + + export function Svg(globalStyle: string, document?: HTMLDocument): SvgGraphics { var initialState: SvgState = { x: 0, y: 0, @@ -33,16 +36,14 @@ namespace nomnoml.skanaar { dashArray: 'none', fill: 'none', textAlign: 'left', - font: null + font: 'Helvetica, Arial, sans-serif', + fontSize: 12 } var states = [initialState] var elements: any[] = [] - // canvas is an optional parameter - var ctx = canvas ? canvas.getContext('2d') : null - var canUseCanvas = false - var waitingForFirstFont = true - var docFont = '' + var measurementCanvas: HTMLCanvasElement = document ? document.createElement('canvas') : null; + var ctx = measurementCanvas ? measurementCanvas.getContext('2d') : null function Element(name: string, attr: any, content?: string): any { return { @@ -76,7 +77,7 @@ namespace nomnoml.skanaar { } function State(dx: number, dy: number): SvgState { - return { x: dx, y: dy, stroke: null, strokeWidth: null, fill: null, textAlign: null, dashArray:'none', font: null } + return { x: dx, y: dy, stroke: null, strokeWidth: null, fill: null, textAlign: null, dashArray:'none', font: null, fontSize: null } } function trans(coord: number, axis: 'x'|'y'){ @@ -150,30 +151,10 @@ namespace nomnoml.skanaar { element.attr.d += ' Z' return element }, - font: function (font){ + setFont: function (font: string, bold: 'bold'|'normal', ital:'italic'|null, fontSize: number): void { + var font = `${bold} ${ital || ''} ${fontSize}pt ${font}, Helvetica, sans-serif` last(states).font = font; - - if (waitingForFirstFont) { - // This is our first chance to test if we can use a canvas to measure text width. - if (ctx) { - var primaryFont = font.replace(/^.*family:/, '').replace(/[, ].*$/, '') - primaryFont = primaryFont.replace(/'/g, '') - canUseCanvas = /^(Arial|Helvetica|Times|Times New Roman)$/.test(primaryFont) - if (canUseCanvas) { - var fontSize = font.replace(/^.*font-size:/, '').replace(/;.*$/, '') + ' ' - if (primaryFont === 'Arial') { - docFont = fontSize + 'Arial, Helvetica, sans-serif' - } else if (primaryFont === 'Helvetica') { - docFont = fontSize + 'Helvetica, Arial, sans-serif' - } else if (primaryFont === 'Times New Roman') { - docFont = fontSize + '"Times New Roman", Times, serif' - } else if (primaryFont === 'Times') { - docFont = fontSize + 'Times, "Times New Roman", serif' - } - } - } - waitingForFirstFont = false - } + last(states).fontSize = fontSize; }, strokeStyle: function (stroke){ last(states).stroke = stroke @@ -190,13 +171,8 @@ namespace nomnoml.skanaar { fillText: function (text, x, y){ var attr = { x: tX(x), y: tY(y), style: 'fill: '+last(states).fill+';' } var font = lastDefined('font') - if (font.indexOf('bold') === -1) { - attr.style += 'font-weight:normal;' - } else { - attr.style += 'font-weight:bold;' - } - if (font.indexOf('italic') > -1) { - attr.style += 'font-style:italic;' + if (font) { + attr.style += 'font:'+font+';' } if (lastDefined('textAlign') === 'center') { attr.style += 'text-anchor: middle;' @@ -213,17 +189,17 @@ namespace nomnoml.skanaar { last(states).strokeWidth = w }, measureText: function (s: string){ - if (canUseCanvas) { - var fontStr = lastDefined('font') - var italicSpec = (/\bitalic\b/.test(fontStr) ? 'italic' : 'normal') + ' normal ' - var boldSpec = /\bbold\b/.test(fontStr) ? 'bold ' : 'normal ' - ctx.font = italicSpec + boldSpec + docFont + if (ctx) { + // use supplied canvas to measure text + ctx.font = lastDefined('font') || 'normal 12pt Helvetica' return ctx.measureText(s) } else { + // use heuristic to measure text return { width: skanaar.sum(s, function (c: string){ - if (c === 'M' || c === 'W') { return 14 } - return c.charCodeAt(0) < 200 ? 9.5 : 16 + var scale = lastDefined('fontSize')/12 + if (charWidths[c]) { return charWidths[c] * scale } + return 16 * scale // non-ascii characters all treated as wide }) } } @@ -280,7 +256,7 @@ namespace nomnoml.skanaar { xmlns: 'http://www.w3.org/2000/svg', 'xmlns:xlink': 'http://www.w3.org/1999/xlink', 'xmlns:ev': 'http://www.w3.org/2001/xml-events', - style: lastDefined('font') + ';' + globalStyle, + style: 'font:'+lastDefined('font') + ';' + globalStyle, } return '' } diff --git a/test/char-widths.html b/test/char-widths.html new file mode 100644 index 0000000..cfd5047 --- /dev/null +++ b/test/char-widths.html @@ -0,0 +1,33 @@ + + +
+ +