1 /* 2 * Copyright (C) 2008, 2009 Mihai Şucan 3 * 4 * This file is part of PaintWeb. 5 * 6 * PaintWeb is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License as published by 8 * the Free Software Foundation, either version 3 of the License, or 9 * (at your option) any later version. 10 * 11 * PaintWeb is distributed in the hope that it will be useful, 12 * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 * GNU General Public License for more details. 15 * 16 * You should have received a copy of the GNU General Public License 17 * along with PaintWeb. If not, see <http://www.gnu.org/licenses/>. 18 * 19 * $URL: http://code.google.com/p/paintweb $ 20 * $Date: 2009-08-27 20:30:01 +0300 $ 21 */ 22 23 /** 24 * @author <a lang="ro" href="http://www.robodesign.ro/mihai">Mihai Şucan</a> 25 * @fileOverview Holds the text tool implementation. 26 */ 27 28 // TODO: make this tool nicer to use. 29 30 /** 31 * @class The text tool. 32 * 33 * @param {PaintWeb} app Reference to the main paint application object. 34 */ 35 pwlib.tools.text = function (app) { 36 var _self = this, 37 clearInterval = app.win.clearInterval, 38 config = app.config.text, 39 context = app.buffer.context, 40 doc = app.doc, 41 gui = app.gui, 42 image = app.image, 43 lang = app.lang, 44 MathRound = Math.round, 45 mouse = app.mouse, 46 setInterval = app.win.setInterval; 47 48 /** 49 * The interval ID used for invoking the drawing operation every few 50 * milliseconds. 51 * 52 * @private 53 * @see PaintWeb.config.toolDrawDelay 54 */ 55 var timer = null; 56 57 /** 58 * Holds the previous tool ID. 59 * 60 * @private 61 * @type String 62 */ 63 var prevTool = app.tool ? app.tool._id : null; 64 65 /** 66 * Tells if the drawing canvas needs to be updated or not. 67 * 68 * @private 69 * @type Boolean 70 * @default false 71 */ 72 var needsRedraw = false; 73 74 var inputString = null, 75 input_fontFamily = null, 76 ev_configChangeId = null, 77 ns_svg = "http://www.w3.org/2000/svg", 78 svgDoc = null, 79 svgText = null, 80 textWidth = 0, 81 textHeight = 0; 82 83 /** 84 * Tool preactivation code. This method check if the browser has support for 85 * rendering text in Canvas. 86 * 87 * @returns {Boolean} True if the tool can be activated successfully, or false 88 * if not. 89 */ 90 this.preActivate = function () { 91 if (!gui.inputs.textString || !gui.inputs.text_fontFamily || 92 !gui.elems.viewport) { 93 return false; 94 95 } 96 97 // Canvas 2D Text API 98 if (context.fillText && context.strokeText) { 99 return true; 100 } 101 102 // Opera can only render text via SVG Text. 103 // Note: support for Opera has been disabled. 104 // There are severe SVG redraw issues when updating the SVG text element. 105 // Besides, there are important memory leaks. 106 // Ultimately, there's a deal breaker: security violation. The SVG document 107 // which is rendered inside Canvas is considered "external" 108 // - get/putImageData() and toDataURL() stop working after drawImage(svg) is 109 // invoked. Eh. 110 /*if (pwlib.browser.opera) { 111 return true; 112 }*/ 113 114 // Gecko 1.9.0 had its own proprietary Canvas 2D Text API. 115 if (context.mozPathText) { 116 return true; 117 } 118 119 alert(lang.errorTextUnsupported); 120 return false; 121 }; 122 123 /** 124 * The tool activation code. This sets up a few variables, starts the drawing 125 * timer and adds event listeners as needed. 126 */ 127 this.activate = function () { 128 // Reset the mouse coordinates to the scroll top/left corner such that the 129 // text is rendered there. 130 mouse.x = Math.round(gui.elems.viewport.scrollLeft / image.canvasScale), 131 mouse.y = Math.round(gui.elems.viewport.scrollTop / image.canvasScale), 132 133 input_fontFamily = gui.inputs.text_fontFamily; 134 inputString = gui.inputs.textString; 135 136 if (!context.fillText && pwlib.browser.opera) { 137 ev_configChangeId = app.events.add('configChange', ev_configChange_opera); 138 inputString.addEventListener('input', ev_configChange_opera, false); 139 inputString.addEventListener('change', ev_configChange_opera, false); 140 } else { 141 ev_configChangeId = app.events.add('configChange', ev_configChange); 142 inputString.addEventListener('input', ev_configChange, false); 143 inputString.addEventListener('change', ev_configChange, false); 144 } 145 146 // Render text using the Canvas 2D context text API defined by HTML 5. 147 if (context.fillText && context.strokeText) { 148 _self.draw = _self.draw_spec; 149 150 } else if (pwlib.browser.opera) { 151 // Render text using a SVG Text element which is copied into Canvas using 152 // drawImage(). 153 _self.draw = _self.draw_opera; 154 initOpera(); 155 156 } else if (context.mozPathText) { 157 // Render text using proprietary API available in Gecko 1.9.0. 158 _self.draw = _self.draw_moz; 159 textWidth = context.mozMeasureText(inputString.value); 160 } 161 162 if (!timer) { 163 timer = setInterval(_self.draw, app.config.toolDrawDelay); 164 } 165 needsRedraw = true; 166 }; 167 168 /** 169 * The tool deactivation simply consists of removing the event listeners added 170 * when the tool was constructed, and clearing the buffer canvas. 171 */ 172 this.deactivate = function () { 173 if (timer) { 174 clearInterval(timer); 175 timer = null; 176 } 177 needsRedraw = false; 178 179 if (ev_configChangeId) { 180 app.events.remove('configChange', ev_configChangeId); 181 } 182 183 if (!context.fillText && pwlib.browser.opera) { 184 inputString.removeEventListener('input', ev_configChange_opera, false); 185 inputString.removeEventListener('change', ev_configChange_opera, false); 186 } else { 187 inputString.removeEventListener('input', ev_configChange, false); 188 inputString.removeEventListener('change', ev_configChange, false); 189 } 190 191 svgText = null; 192 svgDoc = null; 193 194 context.clearRect(0, 0, image.width, image.height); 195 196 return true; 197 }; 198 199 /** 200 * Initialize the SVG document for Opera. This is used for rendering the text. 201 * @private 202 */ 203 function initOpera () { 204 svgDoc = doc.createElementNS(ns_svg, 'svg'); 205 svgDoc.setAttributeNS(ns_svg, 'version', '1.1'); 206 207 svgText = doc.createElementNS(ns_svg, 'text'); 208 svgText.appendChild(doc.createTextNode(inputString.value)); 209 svgDoc.appendChild(svgText); 210 211 svgText.style.font = context.font; 212 213 if (app.config.shapeType !== 'stroke') { 214 svgText.style.fill = context.fillStyle; 215 } else { 216 svgText.style.fill = 'none'; 217 } 218 219 if (app.config.shapeType !== 'fill') { 220 svgText.style.stroke = context.strokeStyle; 221 svgText.style.strokeWidth = context.lineWidth; 222 } else { 223 svgText.style.stroke = 'none'; 224 svgText.style.strokeWidth = context.lineWidth; 225 } 226 227 textWidth = svgText.getComputedTextLength(); 228 textHeight = svgText.getBBox().height; 229 230 svgDoc.setAttributeNS(ns_svg, 'width', textWidth); 231 svgDoc.setAttributeNS(ns_svg, 'height', textHeight + 10); 232 svgText.setAttributeNS(ns_svg, 'x', 0); 233 svgText.setAttributeNS(ns_svg, 'y', textHeight); 234 }; 235 236 /** 237 * The <code>configChange</code> application event handler. This is also the 238 * <code>input</code> and <code>change</code> event handler for the text 239 * string input element. This method updates the Canvas text-related 240 * properties as needed, and re-renders the text. 241 * 242 * <p>This function is not used on Opera. 243 * 244 * @param {Event|pwlib.appEvent.configChange} ev The application/DOM event 245 * object. 246 */ 247 function ev_configChange (ev) { 248 if (ev.type === 'input' || ev.type === 'change' || 249 (!ev.group && ev.config === 'shapeType') || 250 (ev.group === 'line' && ev.config === 'lineWidth')) { 251 needsRedraw = true; 252 253 // Update the text width. 254 if (!context.fillText && context.mozMeasureText) { 255 textWidth = context.mozMeasureText(inputString.value); 256 } 257 return; 258 } 259 260 if (ev.type !== 'configChange' && ev.group !== 'text') { 261 return; 262 } 263 264 var font = ''; 265 266 switch (ev.config) { 267 case 'fontFamily': 268 if (ev.value === '+') { 269 fontFamilyAdd(ev); 270 } 271 case 'bold': 272 case 'italic': 273 case 'fontSize': 274 if (config.bold) { 275 font += 'bold '; 276 } 277 if (config.italic) { 278 font += 'italic '; 279 } 280 font += config.fontSize + 'px ' + config.fontFamily; 281 context.font = font; 282 283 if ('mozTextStyle' in context) { 284 context.mozTextStyle = font; 285 } 286 287 case 'textAlign': 288 case 'textBaseline': 289 needsRedraw = true; 290 } 291 292 // Update the text width. 293 if (ev.config !== 'textAlign' && ev.config !== 'textBaseline' && 294 !context.fillText && context.mozMeasureText) { 295 textWidth = context.mozMeasureText(inputString.value); 296 } 297 }; 298 299 /** 300 * The <code>configChange</code> application event handler. This is also the 301 * <code>input</code> and <code>change</code> event handler for the text 302 * string input element. This method updates the Canvas text-related 303 * properties as needed, and re-renders the text. 304 * 305 * <p>This is function is specific to Opera. 306 * 307 * @param {Event|pwlib.appEvent.configChange} ev The application/DOM event 308 * object. 309 */ 310 function ev_configChange_opera (ev) { 311 if (ev.type === 'input' || ev.type === 'change') { 312 svgText.replaceChild(doc.createTextNode(this.value), svgText.firstChild); 313 needsRedraw = true; 314 } 315 316 if (!ev.group && ev.config === 'shapeType') { 317 if (ev.value !== 'stroke') { 318 svgText.style.fill = context.fillStyle; 319 } else { 320 svgText.style.fill = 'none'; 321 } 322 323 if (ev.value !== 'fill') { 324 svgText.style.stroke = context.strokeStyle; 325 svgText.style.strokeWidth = context.lineWidth; 326 } else { 327 svgText.style.stroke = 'none'; 328 svgText.style.strokeWidth = context.lineWidth; 329 } 330 needsRedraw = true; 331 } 332 333 if (!ev.group && ev.config === 'fillStyle') { 334 if (app.config.shapeType !== 'stroke') { 335 svgText.style.fill = ev.value; 336 needsRedraw = true; 337 } 338 } 339 340 if ((!ev.group && ev.config === 'strokeStyle') || 341 (ev.group === 'line' && ev.config === 'lineWidth')) { 342 if (app.config.shapeType !== 'fill') { 343 svgText.style.stroke = context.strokeStyle; 344 svgText.style.strokeWidth = context.lineWidth; 345 needsRedraw = true; 346 } 347 } 348 349 if (ev.type === 'configChange' && ev.group === 'text') { 350 var font = ''; 351 switch (ev.config) { 352 case 'fontFamily': 353 if (ev.value === '+') { 354 fontFamilyAdd(ev); 355 } 356 case 'bold': 357 case 'italic': 358 case 'fontSize': 359 if (config.bold) { 360 font += 'bold '; 361 } 362 if (config.italic) { 363 font += 'italic '; 364 } 365 font += config.fontSize + 'px ' + config.fontFamily; 366 context.font = font; 367 svgText.style.font = font; 368 369 case 'textAlign': 370 case 'textBaseline': 371 needsRedraw = true; 372 } 373 } 374 375 textWidth = svgText.getComputedTextLength(); 376 textHeight = svgText.getBBox().height; 377 378 svgDoc.setAttributeNS(ns_svg, 'width', textWidth); 379 svgDoc.setAttributeNS(ns_svg, 'height', textHeight + 10); 380 svgText.setAttributeNS(ns_svg, 'x', 0); 381 svgText.setAttributeNS(ns_svg, 'y', textHeight); 382 }; 383 384 /** 385 * Add a new font family into the font family drop down. This function is 386 * invoked by the <code>ev_configChange()</code> function when the user 387 * attempts to add a new font family. 388 * 389 * @private 390 * 391 * @param {pwlib.appEvent.configChange} ev The application event object. 392 */ 393 function fontFamilyAdd (ev) { 394 var new_font = prompt(lang.promptTextFont) || ''; 395 new_font = new_font.replace(/^\s+/, '').replace(/\s+$/, '') || 396 ev.previousValue; 397 398 // Check if the font name is already in the list. 399 var opt, new_font2 = new_font.toLowerCase(), 400 n = input_fontFamily.options.length; 401 402 for (var i = 0; i < n; i++) { 403 opt = input_fontFamily.options[i]; 404 if (opt.value.toLowerCase() == new_font2) { 405 config.fontFamily = opt.value; 406 input_fontFamily.selectedIndex = i; 407 input_fontFamily.value = config.fontFamily; 408 ev.value = config.fontFamily; 409 410 return; 411 } 412 } 413 414 // Add the new font. 415 opt = doc.createElement('option'); 416 opt.value = new_font; 417 opt.appendChild(doc.createTextNode(new_font)); 418 input_fontFamily.insertBefore(opt, input_fontFamily.options[n-1]); 419 input_fontFamily.selectedIndex = n-1; 420 input_fontFamily.value = new_font; 421 ev.value = new_font; 422 config.fontFamily = new_font; 423 }; 424 425 /** 426 * The <code>mousemove</code> event handler. 427 */ 428 this.mousemove = function () { 429 needsRedraw = true; 430 }; 431 432 /** 433 * Perform the drawing operation using standard 2D context methods. 434 * 435 * @see PaintWeb.config.toolDrawDelay 436 */ 437 this.draw_spec = function () { 438 if (!needsRedraw) { 439 return; 440 } 441 442 context.clearRect(0, 0, image.width, image.height); 443 444 if (app.config.shapeType != 'stroke') { 445 context.fillText(inputString.value, mouse.x, mouse.y); 446 } 447 448 if (app.config.shapeType != 'fill') { 449 context.beginPath(); 450 context.strokeText(inputString.value, mouse.x, mouse.y); 451 context.closePath(); 452 } 453 454 needsRedraw = false; 455 }; 456 457 /** 458 * Perform the drawing operation in Gecko 1.9.0. 459 */ 460 this.draw_moz = function () { 461 if (!needsRedraw) { 462 return; 463 } 464 465 context.clearRect(0, 0, image.width, image.height); 466 467 var x = mouse.x, 468 y = mouse.y; 469 470 if (config.textAlign === 'center') { 471 x -= MathRound(textWidth / 2); 472 } else if (config.textAlign === 'right') { 473 x -= textWidth; 474 } 475 476 if (config.textBaseline === 'top') { 477 y += config.fontSize; 478 } else if (config.textBaseline === 'middle') { 479 y += MathRound(config.fontSize / 2); 480 } 481 482 context.setTransform(1, 0, 0, 1, x, y); 483 context.beginPath(); 484 context.mozPathText(inputString.value); 485 486 if (app.config.shapeType != 'stroke') { 487 context.fill(); 488 } 489 490 if (app.config.shapeType != 'fill') { 491 context.stroke(); 492 } 493 context.closePath(); 494 context.setTransform(1, 0, 0, 1, 0, 0); 495 496 needsRedraw = false; 497 }; 498 499 /** 500 * Perform the drawing operation in Opera using SVG. 501 */ 502 this.draw_opera = function () { 503 if (!needsRedraw) { 504 return; 505 } 506 507 context.clearRect(0, 0, image.width, image.height); 508 509 var x = mouse.x, 510 y = mouse.y; 511 512 if (config.textAlign === 'center') { 513 x -= MathRound(textWidth / 2); 514 } else if (config.textAlign === 'right') { 515 x -= textWidth; 516 } 517 518 if (config.textBaseline === 'bottom') { 519 y -= textHeight; 520 } else if (config.textBaseline === 'middle') { 521 y -= MathRound(textHeight / 2); 522 } 523 524 context.drawImage(svgDoc, x, y); 525 526 needsRedraw = false; 527 }; 528 529 /** 530 * The <code>click</code> event handler. This method completes the drawing 531 * operation by inserting the text into the layer canvas. 532 */ 533 this.click = function () { 534 _self.draw(); 535 app.layerUpdate(); 536 }; 537 538 /** 539 * The <code>keydown</code> event handler allows users to press the 540 * <kbd>Escape</kbd> key to cancel the drawing operation and return to the 541 * previous tool. 542 * 543 * @param {Event} ev The DOM Event object. 544 * @returns {Boolean} True if the key was recognized, or false if not. 545 */ 546 this.keydown = function (ev) { 547 if (!prevTool || ev.kid_ != 'Escape') { 548 return false; 549 } 550 551 mouse.buttonDown = false; 552 app.toolActivate(prevTool, ev); 553 554 return true; 555 }; 556 }; 557 558 // vim:set spell spl=en fo=wan1croqlt tw=80 ts=2 sw=2 sts=2 sta et ai cin fenc=utf-8 ff=unix: 559 560