This commit is contained in:
Richard Davey 2017-12-13 22:08:18 +00:00
commit 4320524119
4 changed files with 346 additions and 12 deletions

View file

@ -24,7 +24,11 @@ var propertyMap = {
fixedWidth: [ 'fixedWidth', false ],
fixedHeight: [ 'fixedHeight', false ],
rtl: [ 'rtl', false ],
testString: [ 'testString', '|MÉqgy' ]
testString: [ 'testString', '|MÉqgy' ],
wordWrapWidth: [ 'wordWrap.width', null ],
wordWrapCallback: [ 'wordWrap.callback', null ],
wordWrapCallbackScope: [ 'wordWrap.callbackScope', null ],
wordWrapUseAdvanced: [ 'wordWrap.useAdvancedWrap', false ]
};
var TextStyle = new Class({
@ -90,7 +94,15 @@ var TextStyle = new Class({
for (var key in propertyMap)
{
this[key] = GetAdvancedValue(style, propertyMap[key][0], propertyMap[key][1]);
if (key === 'wordWrapCallback' || key === 'wordWrapCallbackScope')
{
// Callback & scope should be set without processing the values
this[key] = GetValue(style, propertyMap[key][0], propertyMap[key][1]);
}
else
{
this[key] = GetAdvancedValue(style, propertyMap[key][0], propertyMap[key][1]);
}
}
// Allow for 'font' override
@ -124,6 +136,10 @@ var TextStyle = new Class({
syncFont: function (canvas, context)
{
context.font = this._font;
},
syncStyle: function (canvas, context)
{
context.textBaseline = 'alphabetic';
context.fillStyle = this.color;
@ -335,6 +351,47 @@ var TextStyle = new Class({
return this.update(false);
},
/**
* Set the width (in pixels) to use for wrapping lines. Pass in null to remove wrapping by
* width.
*
* @param {number|null} width - The maximum width of a line in pixels. Set to null to remove
* wrapping.
* @param {boolean} [useAdvancedWrap=false] - Whether or not to use the advanced wrapping
* algorithm. If true, spaces are collapsed and whitespace is trimmed from lines. If false,
* spaces and whitespace are left as is.
* @return {this}
*/
setWordWrapWidth: function (width, useAdvancedWrap)
{
if (useAdvancedWrap === undefined) { useAdvancedWrap = false; }
this.wordWrapWidth = width;
this.wordWrapUseAdvanced = useAdvancedWrap;
return this.update(false);
},
/**
* Set a custom callback for wrapping lines. Pass in null to remove wrapping by callback.
*
* @param {function} callback - A custom function that will be responsible for wrapping the
* text. It will receive two arguments: text (the string to wrap), textObject (this Text
* instance). It should return the wrapped lines either as an array of lines or as a string with
* newline characters in place to indicate where breaks should happen.
* @param {object} [scope=null] - The scope that will be applied when the callback is invoked.
* @return {this}
*/
setWordWrapCallback: function (callback, scope)
{
if (scope === undefined) { scope = null; }
this.wordWrapCallback = callback;
this.wordWrapCallbackScope = scope;
return this.update(false);
},
setAlign: function (align)
{
if (align === undefined) { align = 'left'; }

View file

@ -106,7 +106,7 @@ var Text = new Class({
}
// Here is where the crazy starts.
//
//
// Due to browser implementation issues, you cannot fillText BiDi text to a canvas
// that is not part of the DOM. It just completely ignores the direction property.
@ -124,6 +124,228 @@ var Text = new Class({
this.originX = 1;
},
/**
* Greedy wrapping algorithm that will wrap words as the line grows longer than its horizontal
* bounds.
*
* @param {string} text - The text to perform word wrap detection against.
* @return {string} The text after wrapping has been applied.
*/
runWordWrap: function (text)
{
var style = this.style;
if (style.wordWrapCallback)
{
var wrappedLines = style.wordWrapCallback.call(style.wordWrapCallbackScope, text, this);
if (Array.isArray(wrappedLines))
{
wrappedLines = wrappedLines.join('\n');
}
return wrappedLines;
}
else if (style.wordWrapWidth)
{
if (style.wordWrapUseAdvanced)
{
return this.advancedWordWrap(text, this.context, this.style.wordWrapWidth);
}
else
{
return this.basicWordWrap(text, this.context, this.style.wordWrapWidth);
}
}
else
{
return text;
}
},
/**
* Advanced wrapping algorithm that will wrap words as the line grows longer than its horizontal
* bounds. Consecutive spaces will be collapsed and replaced with a single space. Lines will be
* trimmed of white space before processing. Throws an error if wordWrapWidth is less than a
* single character.
*
* @param {string} text - The text to perform word wrap detection against.
* @param {CanvasRenderingContext2D} context
* @param {number} wordWrapWidth
* @return {string} The wrapped text.
*/
advancedWordWrap: function (text, context, wordWrapWidth)
{
var output = '';
// Condense consecutive spaces and split into lines
var lines = text
.replace(/ +/gi, ' ')
.split(this.splitRegExp);
var linesCount = lines.length;
for (var i = 0; i < linesCount; i++)
{
var line = lines[i];
var out = '';
// Trim whitespace
line = line.replace(/^ *|\s*$/gi, '');
// If entire line is less than wordWrapWidth append the entire line and exit early
var lineWidth = context.measureText(line).width;
if (lineWidth < wordWrapWidth)
{
output += line + '\n';
continue;
}
// Otherwise, calculate new lines
var currentLineWidth = wordWrapWidth;
// Split into words
var words = line.split(' ');
for (var j = 0; j < words.length; j++)
{
var word = words[j];
var wordWithSpace = word + ' ';
var wordWidth = context.measureText(wordWithSpace).width;
if (wordWidth > currentLineWidth)
{
// Break word
if (j === 0)
{
// Shave off letters from word until it's small enough
var newWord = wordWithSpace;
while (newWord.length)
{
newWord = newWord.slice(0, -1);
wordWidth = context.measureText(newWord).width;
if (wordWidth <= currentLineWidth)
{
break;
}
}
// If wordWrapWidth is too small for even a single letter, shame user
// failure with a fatal error
if (!newWord.length)
{
throw new Error('This text\'s wordWrapWidth setting is less than a single character!');
}
// Replace current word in array with remainder
var secondPart = word.substr(newWord.length);
words[j] = secondPart;
// Append first piece to output
out += newWord;
}
// If existing word length is 0, don't include it
var offset = (words[j].length) ? j : j + 1;
// Collapse rest of sentence and remove any trailing white space
var remainder = words.slice(offset).join(' ')
.replace(/[ \n]*$/gi, '');
// Prepend remainder to next line
lines[i + 1] = remainder + ' ' + (lines[i + 1] || '');
linesCount = lines.length;
break; // Processing on this line
// Append word with space to output
}
else
{
out += wordWithSpace;
currentLineWidth -= wordWidth;
}
}
// Append processed line to output
output += out.replace(/[ \n]*$/gi, '') + '\n';
}
// Trim the end of the string
output = output.replace(/[\s|\n]*$/gi, '');
return output;
},
/**
* Greedy wrapping algorithm that will wrap words as the line grows longer than its horizontal
* bounds. Spaces are not collapsed and whitespace is not trimmed.
*
* @param {string} text - The text to perform word wrap detection against.
* @param {CanvasRenderingContext2D} context
* @param {number} wordWrapWidth
* @return {string} The wrapped text.
*/
basicWordWrap: function (text, context, wordWrapWidth)
{
var result = '';
var lines = text.split(this.splitRegExp);
for (var i = 0; i < lines.length; i++)
{
var spaceLeft = wordWrapWidth;
var words = lines[i].split(' ');
for (var j = 0; j < words.length; j++)
{
var wordWidth = context.measureText(words[j]).width;
var wordWidthWithSpace = wordWidth + context.measureText(' ').width;
if (wordWidthWithSpace > spaceLeft)
{
// Skip printing the newline if it's the first word of the line that is greater
// than the word wrap width.
if (j > 0)
{
result += '\n';
}
result += words[j] + ' ';
spaceLeft = wordWrapWidth - wordWidth;
}
else
{
spaceLeft -= wordWidthWithSpace;
result += words[j] + ' ';
}
}
if (i < lines.length - 1)
{
result += '\n';
}
}
return result;
},
/**
* Runs the given text through this Text object's word wrapping and returns the results as an
* array, where each element of the array corresponds to a wrapped line of text.
*
* @param {string} [text] - The text for which the wrapping will be calculated. If unspecified,
* the Text object's current text will be used.
* @return {array} An array of strings with the pieces of wrapped text.
*/
getWrappedText: function (text)
{
if (text === undefined) { text = this.text; }
var wrappedLines = this.runWordWrap(text);
return wrappedLines.split(this.splitRegExp);
},
setText: function (value)
{
if (Array.isArray(value))
@ -221,6 +443,37 @@ var Text = new Class({
return this.style.setShadowFill(enabled);
},
/**
* Set the width (in pixels) to use for wrapping lines. Pass in null to remove wrapping by
* width.
*
* @param {number|null} width - The maximum width of a line in pixels. Set to null to remove
* wrapping.
* @param {boolean} [useAdvancedWrap=false] - Whether or not to use the advanced wrapping
* algorithm. If true, spaces are collapsed and whitespace is trimmed from lines. If false,
* spaces and whitespace are left as is.
* @return {this}
*/
setWordWrapWidth: function (width, useAdvancedWrap)
{
return this.style.setWordWrapWidth(width, useAdvancedWrap);
},
/**
* Set a custom callback for wrapping lines. Pass in null to remove wrapping by callback.
*
* @param {function} callback - A custom function that will be responsible for wrapping the
* text. It will receive two arguments: text (the string to wrap), textObject (this Text
* instance). It should return the wrapped lines either as an array of lines or as a string with
* newline characters in place to indicate where breaks should happen.
* @param {object} [scope=null] - The scope that will be applied when the callback is invoked.
* @return {this}
*/
setWordWrapCallback: function (callback, scope)
{
return this.style.setWordWrapCallback(callback, scope);
},
setAlign: function (align)
{
return this.style.setAlign(align);
@ -289,12 +542,14 @@ var Text = new Class({
var style = this.style;
var size = style.metrics;
style.syncFont(canvas, context);
var outputText = this.text;
// if (style.wordWrap)
// {
// outputText = this.runWordWrap(this.text);
// }
if (style.wordWrapWidth || style.wordWrapCallback)
{
outputText = this.runWordWrap(this.text);
}
// Split text into lines
var lines = outputText.split(this.splitRegExp);
@ -325,6 +580,7 @@ var Text = new Class({
{
canvas.width = w;
canvas.height = h;
style.syncFont(canvas, context); // Resizing resets the context
}
else
{
@ -339,7 +595,7 @@ var Text = new Class({
context.fillRect(0, 0, w, h);
}
style.syncFont(canvas, context);
style.syncStyle(canvas, context);
context.textBaseline = 'alphabetic';

View file

@ -265,7 +265,7 @@ var BaseSound = new Class({
this.volume = this.currentConfig.volume;
this.rate = this.currentConfig.rate;
this.detune = this.currentConfig.detune;
// TODO assign other config values to buffer source
this.loop = this.currentConfig.loop;
},
/**
* @protected

View file

@ -109,6 +109,7 @@ var WebAudioSound = new Class({
*/
// TODO add delay param
createAndStartBufferSource: function () {
var _this = this;
var seek = this.currentConfig.seek;
var offset = (this.currentMarker ? this.currentMarker.start : 0) + seek;
var duration = this.duration - seek;
@ -117,12 +118,18 @@ var WebAudioSound = new Class({
this.source.buffer = this.audioBuffer;
this.source.connect(this.muteNode);
this.source.onended = function (ev) {
if (ev.target === this.source) {
if (ev.target === _this.source) {
// sound ended
this.hasEnded = true;
if (_this.currentConfig.loop) {
_this.resetConfig();
_this.createAndStartBufferSource();
}
else {
_this.hasEnded = true;
}
}
// else was stopped
}.bind(this);
};
this.applyConfig();
this.source.start(0, Math.max(0, offset), Math.max(0, duration));
this.resetConfig();
@ -284,4 +291,18 @@ Object.defineProperty(WebAudioSound.prototype, 'seek', {
}
}
});
/**
* Property indicating whether or not
* the sound or current sound marker will loop.
*
* @property {boolean} loop
*/
Object.defineProperty(WebAudioSound.prototype, 'loop', {
get: function () {
return this.currentConfig.loop;
},
set: function (value) {
this.currentConfig.loop = value;
}
});
module.exports = WebAudioSound;