Repository: namniak/canvas-text-wrapper Branch: master Commit: 268a8620ab05 Files: 10 Total size: 19.5 KB Directory structure: gitextract_xkcmgvhz/ ├── .gitignore ├── .npmignore ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── bower.json ├── canvas-text-wrapper.js ├── index.d.ts ├── package.json └── test/ └── test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ .idea/ node_modules/ ================================================ FILE: .npmignore ================================================ node_modules examples bower.json ================================================ FILE: Gruntfile.js ================================================ module.exports = function (grunt) { var saveLicense = require('uglify-save-license'); // project configuration grunt.initConfig({ pkg: grunt.file.readJSON('package.json'), uglify: { my_target: { options: { preserveComments: saveLicense }, src: ['canvas-text-wrapper.js'], dest: 'canvas-text-wrapper.min.js' } } }); // load plugins grunt.loadNpmTasks('uglify-save-license'); grunt.loadNpmTasks('grunt-contrib-uglify'); // default tasks. grunt.registerTask('default', [ 'uglify' ]); }; ================================================ FILE: LICENSE.md ================================================ The MIT License (MIT) Copyright (c) 2016 Vadim Namniak Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # canvas-text-wrapper ## Syntax ```javascript CanvasTextWrapper(HTMLCanvasElement, String [, options]); ``` ## Options | Option | Value | Description | | ------------- | ------------- | ------------- | | **font** | *String* | Text style that includes font size (in px), font weight, font family, etc. | | **lineHeight** | *String* or *Number* | Number - 'n' times font size where 1 is equivalent to '100%'. Also the property can be set in '%' or 'px'. | | **textAlign** | `"left"` `"center"` `"right"` | Horizontal alignment of each line. | | **verticalAlign** | `"top"` `"middle"` `"bottom"` | Vertical alignment of the whole text block. | | **paddingX** | *Number* | Horizontal padding (in px) that is equally set on left and right sides. | | **paddingY** | *Number* | Vertical padding (in px) that is equally set on top and bottoms. | | **fitParent** | *Boolean* | Fit canvas' container size instead of its own size. | | **lineBreak** | `"auto"` `"word"` | `"auto"` - text goes to the next line on a whole word when there's no room `"word"` - each next word is placed on a new line | | **sizeToFill** | *Boolean* | Ignore given font size and line height and resize text to fill its padded container. | | **maxFontSizeToFill** | *Number* | If above option is `true` text won't be bigger than set. | | **strokeText** | *Boolean* | Allow text outline based on canvas context configuration. | | **justifyLines** | *Boolean* | All lines will try to match the same width with flexed spaces between the words. | | **allowNewLine** | *Boolean* | Text breaks on a new line character "\n". Supports multiple consecutive new lines. | | **renderHDPI** | *Boolean* | Text is rendered based on device pixel ratio. | | **textDecoration** | `"none"` `"underline"` | Text is underlined according to `context.strokeStyle` and `context.lineWidth` | NOTE: if a single word is too long to fit the width with specified font size, it will break on any letter unless ```sizeToFill``` option is enabled. ## Default options ```javascript { font: '18px Arial, sans-serif', lineHeight: 1, textAlign: 'left', verticalAlign: 'top', paddingX: 0, paddingY: 0, fitParent: false, lineBreak: 'auto', strokeText: false sizeToFill: false, maxFontSizeToFill: false, allowNewLine: true, justifyLines: false, renderHDPI: true, textDecoration: 'none' } ``` ## Usage Configure context properties such as ```fillStyle```, ```lineWidth```, ```strokeStyle``` etc. before passing it to CanvasTextWrapper like so: ```javascript var CanvasTextWrapper = require('canvas-text-wrapper').CanvasTextWrapper; var canvas = document.getElementById('#canvasText'); canvas.width = 200; canvas.height = 200; context = canvas.getContext('2d'); context.lineWidth = 2; context.strokeStyle = '#ff0000'; CanvasTextWrapper(canvas, 'Hello', {strokeText: true}); ``` ## Test Run ```npm t``` NOTE: Test requires [beefy](http://didact.us/beefy/) to be installed globally ## Examples [see here](http://namniak.github.io/canvas-text-wrapper/) ## Install ```sh npm i canvas-text-wrapper --save bower install canvas-text-wrapper ``` ================================================ FILE: bower.json ================================================ { "name": "canvas-text-wrapper", "version": "0.9.2", "ignore": [ "**/.*", "**/*.log", "**/*.json", "Gruntfile.js", "examples", "node_modules", "README.md" ], "repository": { "type": "git", "url": "git://github.com/namniak/canvas-text-wrapper.git" }, "homepage": "http://namniak.github.io/canvas-text-wrapper/" } ================================================ FILE: canvas-text-wrapper.js ================================================ (function (root) { function CanvasTextWrapper(canvas, text, options) { 'use strict'; var defaults = { font: '18px Arial, sans-serif', sizeToFill: false, maxFontSizeToFill: false, lineHeight: 1, allowNewLine: true, lineBreak: 'auto', textAlign: 'left', verticalAlign: 'top', justifyLines: false, paddingX: 0, paddingY: 0, fitParent: false, strokeText: false, renderHDPI: true, textDecoration: 'none' }; var opts = {}; for (var key in defaults) { opts[key] = options.hasOwnProperty(key) ? options[key] : defaults[key]; } var context = canvas.getContext('2d'); context.font = opts.font; context.textBaseline = 'bottom'; var scale = 1; var devicePixelRatio = (typeof global !== 'undefined') ? global.devicePixelRatio : root.devicePixelRatio; if (opts.renderHDPI && devicePixelRatio > 1) { var tempCtx = {}; // store context settings in a temp object before scaling otherwise they will be lost for (var key in context) { tempCtx[key] = context[key]; } var canvasWidth = canvas.width; var canvasHeight = canvas.height; scale = devicePixelRatio; canvas.width = canvasWidth * scale; canvas.height = canvasHeight * scale; canvas.style.width = canvasWidth * scale * 0.5 + 'px'; canvas.style.height = canvasHeight * scale * 0.5 + 'px'; // restore context settings for (var key in tempCtx) { try { context[key] = tempCtx[key]; } catch (e) { } } context.scale(scale, scale); } var EL_WIDTH = (!opts.fitParent ? canvas.width : canvas.parentNode.clientWidth) / scale; var EL_HEIGHT = (!opts.fitParent ? canvas.height : canvas.parentNode.clientHeight) / scale; var MAX_TXT_WIDTH = EL_WIDTH - (opts.paddingX * 2); var MAX_TXT_HEIGHT = EL_HEIGHT - (opts.paddingY * 2); var fontSize = opts.font.match(/\d+(px|em|%)/g) ? +opts.font.match(/\d+(px|em|%)/g)[0].match(/\d+/g) : 18; var textBlockHeight = 0; var lines = []; var newLineIndexes = []; var textPos = {x: 0, y: 0}; var lineHeight = 0; var fontParts; var multiNewLineDelimiter = '\u200B'; text = handleMultipleNewline(text); setFont(fontSize); setLineHeight(); validate(); render(); function handleMultipleNewline (text) { do { text = text.replace(/\n\n/g, '\n' + multiNewLineDelimiter + '\n'); } while (text.indexOf('\n\n') > -1); return text; } function setFont(fontSize) { if (!fontParts) fontParts = (!opts.sizeToFill) ? opts.font.split(/\b\d+px\b/i) : context.font.split(/\b\d+px\b/i); context.font = fontParts[0] + fontSize + 'px' + fontParts[1]; } function setLineHeight() { if (!isNaN(opts.lineHeight)) { lineHeight = fontSize * opts.lineHeight; } else if (opts.lineHeight.toString().indexOf('px') !== -1) { lineHeight = parseInt(opts.lineHeight); } else if (opts.lineHeight.toString().indexOf('%') !== -1) { lineHeight = (parseInt(opts.lineHeight) / 100) * fontSize; } } function render() { if (opts.sizeToFill) { var wordsCount = text.trim().split(/\s+/).length; var newFontSize = 0; var fontSizeHasLimit = opts.maxFontSizeToFill !== false; do { if (fontSizeHasLimit) { if (++newFontSize <= opts.maxFontSizeToFill) { adjustFontSize(newFontSize); } else { break; } } else { adjustFontSize(++newFontSize); } } while (textBlockHeight < MAX_TXT_HEIGHT && (lines.join(' ').split(/\s+/).length == wordsCount)); adjustFontSize(--newFontSize); } else { wrap(); } if (opts.justifyLines && opts.lineBreak === 'auto') { justify(); } setVertAlign(); setHorizAlign(); drawText(); } function adjustFontSize(size) { setFont(size); lineHeight = size; wrap(); } function wrap() { if (opts.allowNewLine) { var newLines = text.trim().split('\n'); for (var i = 0, idx = 0; i < newLines.length - 1; i++) { idx += newLines[i].trim().split(/\s+/).length; newLineIndexes.push(idx) } } var words = text.trim().split(/\s+/); checkLength(words); breakText(words); textBlockHeight = lines.length * lineHeight; } function checkLength(words) { var testString, tokenLen, sliced, leftover; words.forEach(function (word, index) { testString = ''; tokenLen = context.measureText(word).width; if (tokenLen > MAX_TXT_WIDTH) { for (var k = 0; (context.measureText(testString + word[k]).width <= MAX_TXT_WIDTH) && (k < word.length); k++) { testString += word[k]; } sliced = word.slice(0, k); leftover = word.slice(k); words.splice(index, 1, sliced, leftover); } }); } function breakText(words) { lines = []; for (var i = 0, j = 0; i < words.length; j++) { lines[j] = ''; if (opts.lineBreak === 'auto') { if (context.measureText(lines[j] + words[i]).width > MAX_TXT_WIDTH) { break; } else { while ((context.measureText(lines[j] + words[i]).width <= MAX_TXT_WIDTH) && (i < words.length)) { lines[j] += words[i] + ' '; i++; if (opts.allowNewLine) { for (var k = 0; k < newLineIndexes.length; k++) { if (newLineIndexes[k] === i) { j++; lines[j] = ''; break; } } } } } lines[j] = lines[j].trim(); } else { lines[j] = words[i]; i++; } } } function justify() { var maxLen, longestLineIndex, tokenLen; for (var i = 0; i < lines.length; i++) { tokenLen = context.measureText(lines[i]).width; if (!maxLen || tokenLen > maxLen) { maxLen = tokenLen; longestLineIndex = i; } } // fill lines with extra spaces var numWords, spaceLength, numOfSpaces, num, filler; var delimiter = '\u200A'; for (i = 0; i < lines.length; i++) { if (i === longestLineIndex) continue; numWords = lines[i].trim().split(/\s+/).length; if (numWords <= 1) continue; lines[i] = lines[i].trim().split(/\s+/).join(delimiter); spaceLength = context.measureText(delimiter).width; numOfSpaces = (maxLen - context.measureText(lines[i]).width) / spaceLength; num = numOfSpaces / (numWords - 1); filler = ''; for (var j = 0; j < num; j++) { filler += delimiter; } lines[i] = lines[i].trim().split(delimiter).join(filler); } } function underline(text, x, y) { var width = context.measureText(text).width; switch (context.textAlign) { case 'center': x -= (width / 2); break; case 'right': x -= width; break; } context.beginPath(); context.moveTo(x, y); context.lineTo(x + width, y); context.stroke(); } function drawText() { var skipLineOnMatch = multiNewLineDelimiter + ' '; for (var i = 0; i < lines.length; i++) { textPos.y = parseInt(textPos.y) + lineHeight; if (lines[i] !== skipLineOnMatch) { context.fillText(lines[i], textPos.x, textPos.y); if (opts.strokeText) { context.strokeText(lines[i], textPos.x, textPos.y); } if (opts.textDecoration.toLocaleLowerCase() === 'underline') { underline(lines[i], textPos.x, textPos.y); } } } } function setHorizAlign() { context.textAlign = opts.textAlign; if (opts.textAlign == 'center') { textPos.x = EL_WIDTH / 2; } else if (opts.textAlign == 'right') { textPos.x = EL_WIDTH - opts.paddingX; } else { textPos.x = opts.paddingX; } } function setVertAlign() { if (opts.verticalAlign == 'middle') { textPos.y = (EL_HEIGHT - textBlockHeight) / 2; } else if (opts.verticalAlign == 'bottom') { textPos.y = EL_HEIGHT - textBlockHeight - opts.paddingY; } else { textPos.y = opts.paddingY; } } function validate() { if (typeof text !== 'string') throw new TypeError('The second parameter must be a String.'); if (isNaN(fontSize)) throw new TypeError('Cannot parse "font".'); if (isNaN(lineHeight)) throw new TypeError('Cannot parse "lineHeight".'); if (opts.textAlign.toLocaleLowerCase() !== 'left' && opts.textAlign.toLocaleLowerCase() !== 'center' && opts.textAlign.toLocaleLowerCase() !== 'right') throw new TypeError('Property "textAlign" must be set to either "left", "center", or "right".'); if (opts.verticalAlign.toLocaleLowerCase() !== 'top' && opts.verticalAlign.toLocaleLowerCase() !== 'middle' && opts.verticalAlign.toLocaleLowerCase() !== 'bottom') throw new TypeError('Property "verticalAlign" must be set to either "top", "middle", or "bottom".'); if (typeof opts.justifyLines !== 'boolean') throw new TypeError('Property "justifyLines" must be a Boolean.'); if (isNaN(opts.paddingX)) throw new TypeError('Property "paddingX" must be a Number.'); if (isNaN(opts.paddingY)) throw new TypeError('Property "paddingY" must be a Number.'); if (typeof opts.fitParent !== 'boolean') throw new TypeError('Property "fitParent" must be a Boolean.'); if (opts.lineBreak.toLocaleLowerCase() !== 'auto' && opts.lineBreak.toLocaleLowerCase() !== 'word') throw new TypeError('Property "lineBreak" must be set to either "auto" or "word".'); if (typeof opts.sizeToFill !== 'boolean') throw new TypeError('Property "sizeToFill" must be a Boolean.'); if (typeof opts.strokeText !== 'boolean') throw new TypeError('Property "strokeText" must be a Boolean.'); if (typeof opts.renderHDPI !== 'boolean') throw new TypeError('Property "renderHDPI" must be a Boolean.'); if (opts.textDecoration.toLocaleLowerCase() !== 'none' && opts.textDecoration.toLocaleLowerCase() !== 'underline') throw new TypeError('Property "textDecoration" must be set to either "none" or "underline".'); } return(lines); } if ('module' in root && 'exports' in module) { module.exports = CanvasTextWrapper; } else { root.CanvasTextWrapper = CanvasTextWrapper; } })(this); ================================================ FILE: index.d.ts ================================================ export function CanvasTextWrapper(element: HTMLCanvasElement, text: string, options?: CanvasTextWrapperOptions): void; export interface CanvasTextWrapperOptions { /** * Text style that includes font size (in px), font weight, font family, etc. */ font?: string, /** * Number - 'n' times font size where 1 is equivalent to '100%'. Also the property can be set in '%' or 'px'. */ lineHeight?: string | number, /** * Horizontal alignment of each line. */ textAlign?: "left" | "center" | "right", /** * Vertical alignment of the whole text block. */ verticalAlign?: "top" | "middle" | "bottom", /** * Horizontal padding (in px) that is equally set on left and right sides. */ paddingX?: number, /** * Vertical padding (in px) that is equally set on top and bottoms. */ paddingY?: number, /** * Fit canvas' container size instead of its own size. */ fitParent?: boolean, /** * "auto" - text goes to the next line on a whole word when there's no room * "word" - each next word is placed on a new line */ lineBreak?: "auto" | "word", /** * Ignore given font size and line height and resize text to fill its padded container. */ sizeToFill?: boolean, /** * If above option is true text won't be bigger than set. */ maxFontSizeToFill?: number, /** * Allow text outline based on canvas context configuration. */ strokeText?: boolean, /** * All lines will try to match the same width with flexed spaces between the words. */ justifyLines?: boolean, /** * Text breaks on a new line character "\n". Supports multiple consecutive new lines. */ allowNewLine?: boolean, /** * Text is rendered based on device pixel ratio. */ renderHDPI?: boolean, /** * Text is underlined according to context.strokeStyle and context.lineWidth */ textDecoration?: "none" | "underline" } ================================================ FILE: package.json ================================================ { "name": "canvas-text-wrapper", "description": "Split canvas text into lines on specified rule with optional alignment, padding, and more. Supports HDPI screens.", "version": "0.10.2", "license": "MIT", "main": "canvas-text-wrapper.js", "keywords": [ "canvas", "canvas-text", "text", "split", "wrap", "hdpi-canvas", "text-split" ], "homepage": "http://namniak.github.io/canvas-text-wrapper/", "repository": { "type": "git", "url": "https://github.com/namniak/canvas-text-wrapper" }, "author": "Vadim Namniak (https://github.com/namniak)", "bugs": { "url": "https://github.com/namniak/canvas-text-wrapper/issues" }, "scripts": { "test": "beefy test/test.js --live --open" }, "devDependencies": { "grunt": "^0.4.5", "grunt-contrib-uglify": "^0.9.1", "uglify-save-license": "^0.4.1" } } ================================================ FILE: test/test.js ================================================ var CanvasTextWrapper = require('../canvas-text-wrapper.js').CanvasTextWrapper; var body = document.getElementsByTagName('body')[0]; body.style.margin = 0; var canvas = document.createElement('canvas'); canvas.width = 600; canvas.height = 600; canvas.style.position = 'absolute'; canvas.style.left = '50%'; canvas.style.top = '50%'; canvas.style.border = '1px solid #212121'; canvas.style.transform = 'translateX(-50%) translateY(-50%)'; body.appendChild(canvas); var ctx = canvas.getContext('2d'); var gradient = ctx.createLinearGradient(0, 0, canvas.width, 0); gradient.addColorStop('0.2', 'magenta'); gradient.addColorStop('0.5', 'blue'); gradient.addColorStop('0.7', 'purple'); ctx.fillStyle = gradient; var opts = { sizeToFill: true, textAlign: 'center', verticalAlign: 'middle', paddingX: 20 }; CanvasTextWrapper(canvas, 'What an awesome library!', opts);