Today I wrote a JavaScript module (with module pattern) which generate gradient texts using canvas tags. This is my first JavaScript "program" (I don't know how to call it) created for a real need.
I would like to receive opinions about:
- The performance factor: Is creating canvas slow? Or is it acceptable?
- The maintainability factor: Will this code be handy for any changes?
- The application of module pattern
- The generale structure of code
Here is the module:
var CanvasGradientText = (function (doc, win) {
var cg = {};
var tags = [];
var gradients = [];
//Config
cg.config = {
dataPrefix: "cg"
};
//Wrapper which contains the tag and add some new features, a sort of decorator pattern
function GradientCanvasTagWrapper(element) {
this.element = element;
this.gradientIDDataAttribute = "data-" + this.dataPrefix + "-gradientID";
this.gradientTextDataAttribute = "data-" + this.dataPrefix + "-text";
if (this.element.style.fontSize) this.setFontSize(element.style.fontSize);
if (this.element.style.fontFamily) this.setFontFamily(element.style.fontFamily);
if (this.element.style.textShadow) this.setTextShadow(shadowStringToObject(element.style.textShadow));
}
GradientCanvasTagWrapper.prototype.dataPrefix = cg.config.dataPrefix;
GradientCanvasTagWrapper.prototype.fontSize = "16px";
GradientCanvasTagWrapper.prototype.fontFamily = "Times New Roman";
GradientCanvasTagWrapper.prototype.textShadow = {
offsetX: 0,
offsetY: 0,
blur: 0,
color: 0
};
GradientCanvasTagWrapper.prototype.setFontSize = function (fontSize) {
this.fontSize = fontSize;
};
GradientCanvasTagWrapper.prototype.setFontFamily = function (fontFamily) {
this.fontFamily = fontFamily;
};
GradientCanvasTagWrapper.prototype.setTextShadow = function (textShadow) {
this.textShadow = textShadow;
};
GradientCanvasTagWrapper.prototype.getDataAttribute = function (data) {
return this[data + "DataAttribute"] || false;
};
GradientCanvasTagWrapper.prototype.getFontSize = function () {
return this.fontSize || false;
};
GradientCanvasTagWrapper.prototype.getTextShadow = function () {
return this.textShadow || false;
};
GradientCanvasTagWrapper.prototype.getFontFamily = function () {
return this.fontFamily || false;
};
//A class for single gradient
function Gradient() {
this.id = "";
this.colorStops = [];
}
Gradient.prototype.direction = "vertical";
Gradient.prototype.addColorStop = function (offset, color) {
this.colorStops.push({
offset: offset,
color: color
});
};
function isIE() {
var ua = win.navigator.userAgent;
var msie = ua.indexOf("MSIE ");
return (msie > 0 || !!navigator.userAgent.match(/Trident.*rv\:11\./)) || false;
}
// "white 10px 10px 10px" => {color:"white",offsetX:"10",offsetY:"10",blur:"10"}
function shadowStringToObject(shadow) {
var m = shadow.match(/(\d+(?=[a-zA-Z]{2})|rgb\(.+\)|(?!\s)[^px][a-zA-Z]+)/g);
var res = {};
if (!isIE()) {
res.color = m[0];
res.offsetX = m[1];
res.offsetY = m[2];
res.blur = m[3];
} else {
res.color = m[3];
res.offsetX = m[0];
res.offsetY = m[1];
res.blur = m[2];
}
return res;
}
//Public method for register gradients
cg.addGradient = function (gradient) {
var id;
var colorStops;
if (gradient.id === undefined) {
return false;
} else {
id = gradient.id;
}
if (gradient.colorStops.length < 1) {
return false;
} else {
colorStops = gradient.colorStops;
}
var t = new Gradient;
t.id = id;
t.direction = gradient.direction || t.direction;
for (var k in colorStops) {
if (colorStops.hasOwnProperty(k)) {
t.addColorStop(colorStops[k].offset, colorStops[k].color);
}
}
gradients[id] = t;
return true;
};
//Private method for getting gradients given the id
function getGradient(id) {
return gradients[id] || false;
}
//Private method for checking if a tag(contained in the wrapper) is valid
function isLegalTag(tagWrapper) {
var element = tagWrapper.element;
if (element.offsetWidth < 1 || element.offsetHeight < 1) return false;
var id = element.getAttribute(tagWrapper.getDataAttribute('gradientID'));
var g = getGradient(id);
return (id && g) || false;
}
//Public method for generating canvas element based on the tag wrapped by GradientCanvasTagWrapper
//If the parameter is not wrapped, then wrap it, for allowing to use this method in the global scope
cg.transform = function (tag) {
var elementWrapper;
var element;
if (tag instanceof GradientCanvasTagWrapper) {
elementWrapper = tag;
element = elementWrapper.element;
} else {
element = tag;
elementWrapper = new GradientCanvasTagWrapper(element);
}
var canvasTag = doc.createElement("canvas");
var gradientID = element.getAttribute(elementWrapper.getDataAttribute('gradientID'));
var gradient = getGradient(gradientID);
var direction = gradient.direction;
var fontSize = elementWrapper.getFontSize();
var fontFamily = elementWrapper.getFontFamily();
var textShadow = elementWrapper.getTextShadow();
var centerX;
var centerY;
var ctx;
var ctxGradient;
canvasTag.id = "canvasTextGradient-" + element.id;
canvasTag.width = element.offsetWidth;
canvasTag.height = element.offsetHeight;
centerX = canvasTag.width / 2;
centerY = canvasTag.height / 2;
ctx = canvasTag.getContext("2d");
switch (direction) {
case "vertical":
ctxGradient = ctx.createLinearGradient(0, 0, 0, canvasTag.height);
break;
case "horizontal":
ctxGradient = ctx.createLinearGradient(0, 0, canvasTag.width, 0);
break;
default:
return false;
}
for (var k in gradient.colorStops) {
if (gradient.colorStops.hasOwnProperty(k)) {
ctxGradient.addColorStop(gradient.colorStops[k].offset, gradient.colorStops[k].color);
}
}
ctx.fillStyle = ctxGradient;
ctx.font = fontSize + " " + fontFamily;
ctx.textAlign = "center";
ctx.shadowColor = textShadow.color;
ctx.shadowOffsetX = textShadow.offsetX;
ctx.shadowOffsetY = textShadow.offsetY;
ctx.shadowBlur = textShadow.blur;
ctx.fillText(element.getAttribute(elementWrapper.getDataAttribute("gradientText")), centerX, centerY);
element.appendChild(canvasTag);
return true;
};
//Public method which should handle the configuration changes
cg.setConfig = function (config) {
cg.config.dataPrefix = config.dataPrefix || cg.config.dataPrefix;
};
//Public method for generate canvas in every tag which have determinate attributes
cg.transformAll = function () {
tags = doc.querySelectorAll("[data-" + cg.config.dataPrefix + "-active=true]");
for (var k in tags) {
if (tags.hasOwnProperty(k) && tags[k] instanceof HTMLElement) {
var t = new GradientCanvasTagWrapper(tags[k]);
if (!isLegalTag(t)) {
delete tags[k];
} else {
this.transform(t);
}
}
}
return true;
};
return cg;
}(document, window));
CanvasGradientText.addGradient({id: "testGradient", direction: "vertical", colorStops: {0: {offset: "0", color: "#eaffb1"}, 1: {offset: "0.6", color: "#565e41"}, 2: {offset: "1.0", color: "#565e41"}}});
CanvasGradientText.addGradient({id: "testGradient2", direction: "horizontal", colorStops: {0: {offset: "0", color: "black"}, 1: {offset: "0.6", color: "black"}, 2: {offset: "1.0", color: "white"}}});
CanvasGradientText.transformAll();
Here is the implementation:
<div data-cg-gradientID="testGradient" data-cg-text="a random text" data-cg-active="true" style="
width:300px;
height:50px;
font-family:Georgia;
font-size:30px;
text-shadow: #495a1b 1px 2px 5px;">
</div>
<div data-cg-gradientID="testGradient2" data-cg-text="Another random text" data-cg-active="true" style="
width:300px;
height:50px;
font-family:Arial;
font-size:25px;
text-shadow: #495a1b 1px 2px 5px;">
</div>
<script type="text/javascript" src="js/canvasGradientText.js"></script>
<script type="text/javascript">
CanvasGradientText.addGradient({id:"testGradient",direction:"vertical",colorStops:{0:{offset:"0",color:"#eaffb1"},1:{offset:"0.6",color:"#565e41"},2:{offset:"1.0",color:"#565e41"}}});
CanvasGradientText.addGradient({id:"testGradient2",direction:"horizontal",colorStops:{0:{offset:"0",color:"black"},1:{offset:"0.6",color:"black"},2:{offset:"1.0",color:"white"}}});
CanvasGradientText.transformAll();
</script>
These are my perplexities:
- The switch block inside the trasnform method. I feel that it is wrong. I thought to create a oop interface for the direction, maybe using a decorator pattern, but I don't know how to do.
- I also thought to make a sort of builder class which accept the context object as parameter in the constructor. At this point I would make methods like
setDirection(DirectionObject)
,setGradient(GradientObject)
,setShadow(ShadowObject)
etc. but I'm very confused about the real utility and implementation of this procedure.
Edit:
If I'll need to check if there is the data attribute, if so use it as text, otherwise use the text in the tag, there is a better way than putting this in the transform method:
var text = element.getAttribute(elementWrapper.getDataAttribute("gradientText");
if(text){
ctx.fillText(text,centerX,centerY);
} else {
ctx.fillText(element.innerHTML);
element.innerHTML = "";
}