懒羊羊
2023-08-30 1ac2bc1590406d9babec036e154d8d08f34a6aa1
提交 | 用户 | 时间
1ac2bc 1     window.onload = function () {
2             var paper = Raphael("holder");
3
4             //var curve = paper.ellipse(100, 100, 1, 1).attr({"stroke-width": 0, fill: Color.red});
5             
6             var text = "Betty Botter bought some butter but, she said, the butter's bitter. If I put it in my batter, it will make my batter bitter. But a bit of better butter will make my batter better. So, she bought a bit of butter, better than her bitter butter, and she put it in her batter, and the batter was not bitter. It was better Betty Botter bought a bit better butter.";
7             var font = {font: "11px Arial", "font-style":"italic", opacity: 1, "fill": LABEL_COLOR, stroke: LABEL_COLOR, "stroke-width":.3};
8             var font = {font: "11px Arial", opacity: 1, "fill": LABEL_COLOR};
9             var boxWidth = 100
10             
11             var AttributedStringIterator = function(text){
12                 //this.text = this.rtrim(this.ltrim(text));
13                 text = text.replace(/(\s)+/, " ");
14                 this.text = this.rtrim(text);
15                 /*
16                 if (beginIndex < 0 || beginIndex > endIndex || endIndex > length()) {
17                     throw new IllegalArgumentException("Invalid substring range");
18                 }
19                 */
20                 this.beginIndex = 0;
21                 this.endIndex = this.text.length;
22                 this.currentIndex = this.beginIndex;
23                 
24                 //console.group("[AttributedStringIterator]");
25                 var i = 0;
26                 var string = this.text;
27                 var fullPos = 0;
28                 
29                 //console.log("string: \"" + string + "\", length: " + string.length);
30                 this.startWordOffsets = [];
31                 this.startWordOffsets.push(fullPos);
32                 
33                 // TODO: remove i 1000
34                 while (i<1000) {
35                     var pos = string.search(/[ \t\n\f-\.\,]/);
36                     if (pos == -1)
37                         break;
38                     
39                     // whitespace start
40                     fullPos += pos;
41                     string = string.substr(pos);
42                     ////console.log("fullPos: " + fullPos + ", pos: " + pos +  ", string: ", string);
43                     
44                     // remove whitespaces
45                     var pos = string.search(/[^ \t\n\f-\.\,]/);
46                     if (pos == -1)
47                         break;
48                         
49                     // whitespace end
50                     fullPos += pos;
51                     string = string.substr(pos);
52                     
53                     ////console.log("fullPos: " + fullPos);
54                     this.startWordOffsets.push(fullPos);
55                     
56                     i++;
57                 }
58                 //console.log("startWordOffsets: ", this.startWordOffsets);
59                 //console.groupEnd();
60             };
61             AttributedStringIterator.prototype = {
62                 getEndIndex: function(pos){
63                     if (typeof(pos) == "undefined")
64                         return this.endIndex;
65                         
66                     var string = this.text.substr(pos, this.endIndex - pos);
67                     
68                     var posEndOfLine = string.search(/[\n]/);
69                     if (posEndOfLine == -1)
70                         return this.endIndex;
71                     else
72                         return pos + posEndOfLine;
73                 },
74                 getBeginIndex: function(){
75                     return this.beginIndex;
76                 },
77                 isWhitespace: function(pos){
78                     var str = this.text[pos];
79                     var whitespaceChars = " \t\n\f";
80                     
81                     return (whitespaceChars.indexOf(str) != -1);
82                 },
83                 isNewLine: function(pos){
84                     var str = this.text[pos];
85                     var whitespaceChars = "\n";
86                     
87                     return (whitespaceChars.indexOf(str) != -1);
88                 },
89                 preceding: function(pos){
90                     //console.group("[AttributedStringIterator.preceding]");
91                     for(var i in this.startWordOffsets) {
92                         var startWordOffset = this.startWordOffsets[i];
93                         if (pos < startWordOffset && i>0) {
94                             //console.log("startWordOffset: " + this.startWordOffsets[i-1]);
95                             //console.groupEnd();
96                             return this.startWordOffsets[i-1];
97                         }
98                     }
99                     //console.log("pos: " + pos);
100                     //console.groupEnd();
101                     return this.startWordOffsets[i];
102                 },
103                 following: function(pos){
104                     //console.group("[AttributedStringIterator.following]");
105                     for(var i in this.startWordOffsets) {
106                         var startWordOffset = this.startWordOffsets[i];
107                         if (pos < startWordOffset && i>0) {
108                             //console.log("startWordOffset: " + this.startWordOffsets[i]);
109                             //console.groupEnd();
110                             return this.startWordOffsets[i];
111                         }
112                     }
113                     //console.log("pos: " + pos);
114                     //console.groupEnd();
115                     return this.startWordOffsets[i];
116                 },
117                 ltrim: function(str){
118                     var patt2=/^\s+/g;
119                     return str.replace(patt2, "");
120                 }, 
121                 rtrim: function(str){
122                     var patt2=/\s+$/g;
123                     return str.replace(patt2, "");
124                 },
125                 getLayout: function(start, limit){
126                     return this.text.substr(start, limit - start);
127                 },
128                 getCharAtPos: function(pos) {
129                     return this.text[pos];
130                 }
131             };
132             
133             /*
134             var TextMeasurer = function(paper, text, fontAttrs){
135                 this.text = text;
136                 this.paper = paper;
137                 this.fontAttrs = fontAttrs;
138                 
139                 this.fStart = this.text.getBeginIndex();
140
141             };
142             TextMeasurer.prototype = {
143                 getLineBreakIndex: function(start, maxAdvance){
144                     var localStart = start - this.fStart;
145                 },
146                 getLayout: function(){
147                 }
148             }
149             */
150             
151             
152             var LineBreakMeasurer = function(paper, text, fontAttrs){
153                 this.paper = paper;
154                 this.text = new AttributedStringIterator(text);
155                 this.fontAttrs = fontAttrs;
156                 
157                 if (this.text.getEndIndex() - this.text.getBeginIndex() < 1) {
158                     throw {message: "Text must contain at least one character.", code: "IllegalArgumentException"};
159                 }
160                 
161                 //this.measurer = new TextMeasurer(paper, this.text, this.fontAttrs);
162                 this.limit = this.text.getEndIndex();
163                 this.pos = this.start = this.text.getBeginIndex();
164                 
165                 this.rafaelTextObject = this.paper.text(100, 100, this.text.text).attr(fontAttrs).attr("text-anchor", "start");
166                 this.svgTextObject = this.rafaelTextObject[0];
167             };
168             LineBreakMeasurer.prototype = {
169                 nextOffset: function(wrappingWidth, offsetLimit, requireNextWord) {
170                     //console.group("[nextOffset]");
171                     var nextOffset = this.pos;
172                     if (this.pos < this.limit) {
173                         if (offsetLimit <= this.pos) {
174                             throw {message: "offsetLimit must be after current position", code: "IllegalArgumentException"};
175                         }
176                         
177                         var charAtMaxAdvance = this.getLineBreakIndex(this.pos, wrappingWidth);
178                         //charAtMaxAdvance --;
179                         //console.log("charAtMaxAdvance:", charAtMaxAdvance, ", [" + this.text.getCharAtPos(charAtMaxAdvance) + "]");
180                         
181                         if (charAtMaxAdvance == this.limit) {
182                             nextOffset = this.limit;
183                             //console.log("charAtMaxAdvance == this.limit");
184                         } else if (this.text.isNewLine(charAtMaxAdvance)) {
185                             console.log("isNewLine");
186                             nextOffset = charAtMaxAdvance+1;
187                         } else if (this.text.isWhitespace(charAtMaxAdvance)) {
188                             // TODO: find next noSpaceChar
189                             //return nextOffset;
190                             nextOffset = this.text.following(charAtMaxAdvance);
191                         } else {
192                             // Break is in a word;  back up to previous break.
193                             /*
194                             var testPos = charAtMaxAdvance + 1;
195                             if (testPos == this.limit) {
196                                 console.error("hbz...");
197                             } else {
198                                 nextOffset = this.text.preceding(charAtMaxAdvance);
199                             }
200                             */
201                             nextOffset = this.text.preceding(charAtMaxAdvance);
202                             
203                             if (nextOffset <= this.pos) {
204                                 nextOffset = Math.max(this.pos+1, charAtMaxAdvance);
205                             }
206                         }
207                     }
208                     if (nextOffset > offsetLimit) {
209                         nextOffset = offsetLimit;
210                     }
211                     //console.log("nextOffset: " + nextOffset);
212                     //console.groupEnd();
213                     return nextOffset;
214                 },
215                 nextLayout: function(wrappingWidth) {
216                     //console.groupCollapsed("[nextLayout]");
217                     if (this.pos < this.limit) {
218                         var requireNextWord = false;
219                         var layoutLimit = this.nextOffset(wrappingWidth, this.limit, requireNextWord);
220                         //console.log("layoutLimit:", layoutLimit);
221                         if (layoutLimit == this.pos) {
222                             //console.groupEnd();
223                             return null;
224                         }
225                         var result = this.text.getLayout(this.pos, layoutLimit);
226                         //console.log("layout: \"" + result + "\"");
227                         
228                         // remove end of line
229                         
230                         //var posEndOfLine = this.text.getEndIndex(this.pos);
231                         //if (posEndOfLine < result.length)
232                         //    result = result.substr(0, posEndOfLine);
233                         
234                         this.pos = layoutLimit;
235                         
236                         //console.groupEnd();
237                         return result;
238                     } else {
239                         //console.groupEnd();
240                         return null;
241                     }
242                 },
243                 getLineBreakIndex: function(pos, wrappingWidth) {
244                     //console.group("[getLineBreakIndex]");
245                     //console.log("pos:"+pos + ", text: \""+ this.text.text.replace(/\n/g, "_").substr(pos, 1) + "\"");
246                     
247                     var bb = this.rafaelTextObject.getBBox();
248                     
249                     var charNum = -1;
250                     try {
251                         var svgPoint = this.svgTextObject.getStartPositionOfChar(pos);
252                         //var dot = this.paper.ellipse(svgPoint.x, svgPoint.y, 1, 1).attr({"stroke-width": 0, fill: Color.blue});
253                         svgPoint.x = svgPoint.x + wrappingWidth;
254                         //svgPoint.y = bb.y;
255                         //console.log("svgPoint:", svgPoint);
256                     
257                         //var dot = this.paper.ellipse(svgPoint.x, svgPoint.y, 1, 1).attr({"stroke-width": 0, fill: Color.red});
258                     
259                         charNum = this.svgTextObject.getCharNumAtPosition(svgPoint);
260                     } catch (e){
261                         console.warn("getStartPositionOfChar error, pos:" + pos);
262                         /*
263                         var testPos = pos + 1;
264                         if (testPos < this.limit) {
265                             return testPos
266                         }
267                         */
268                     }
269                     //console.log("charNum:", charNum);
270                     if (charNum == -1) {
271                         //console.groupEnd();
272                         return this.text.getEndIndex(pos);
273                     } else {
274                         // When case there is new line between pos and charnum then use this new line
275                         var newLineIndex = this.text.getEndIndex(pos);
276                         if (newLineIndex < charNum ) {
277                             console.log("newLineIndex <= charNum, newLineIndex:"+newLineIndex+", charNum:"+charNum, "\"" + this.text.text.substr(newLineIndex+1).replace(/\n/g, "↵") + "\"");
278                             //console.groupEnd();
279                             
280                             return newLineIndex;
281                         }
282                             
283                         //var charAtMaxAdvance  = this.text.text.substring(charNum, charNum + 1);
284                         var charAtMaxAdvance  = this.text.getCharAtPos(charNum);
285                         //console.log("!!charAtMaxAdvance: " + charAtMaxAdvance);
286                         //console.groupEnd();
287                         return charNum;
288                     }
289                 }, 
290                 getPosition: function() {
291                     return this.pos;
292                 }
293             };
294             
295             
296             
297             // ******
298             function drawMultilineText(text, x, y, boxWidth, boxHeight, options) {
299                 var TEXT_PADDING = 3;
300                 var width = boxWidth - (2 * TEXT_PADDING);
301                 if (boxHeight)
302                     var height = boxHeight - (2 * TEXT_PADDING);
303             
304                 var layouts = [];
305                 
306                 var measurer = new LineBreakMeasurer(paper, text, font);
307                 var lineHeight = measurer.rafaelTextObject.getBBox().height;
308                 console.log("text: ", text.replace(/\n/g, "↵"));
309                 
310                 if (height) {
311                     var availableLinesCount = parseInt(height/lineHeight);
312                     console.log("availableLinesCount: " + availableLinesCount);
313                 }
314                 
315                 var i = 1;
316                 while (measurer.getPosition() < measurer.text.getEndIndex()) {
317                     var layout = measurer.nextLayout(width);
318                     //console.log("LAYOUT: " + layout + ", getPosition: " + measurer.getPosition());
319                     
320                     if (layout != null) {
321                         if (!availableLinesCount || i < availableLinesCount) {
322                             layouts.push(layout);
323                         } else {
324                             layouts.push(fitTextToWidth(layout + "...", boxWidth));
325                             break;
326                         }
327                     }
328                     i++;
329                 };
330                 console.log(layouts);
331                 
332                 measurer.rafaelTextObject.attr({"text": layouts.join("\n")});
333                 //measurer.rafaelTextObject.attr({"text-anchor": "end"});
334                 //measurer.rafaelTextObject.attr({"text-anchor": "middle"});
335                 if (options)
336                     measurer.rafaelTextObject.attr({"text-anchor": options["text-anchor"]});
337                     
338                 var bb = measurer.rafaelTextObject.getBBox();
339                 //measurer.rafaelTextObject.attr({"x": x + boxWidth/2});
340                 if (options["vertical-align"] == "top")
341                     measurer.rafaelTextObject.attr({"y": y + bb.height/2 + TEXT_PADDING});
342                 else
343                     measurer.rafaelTextObject.attr({"y": y + height/2});
344                 //var bb = measurer.rafaelTextObject.getBBox();
345                 
346                 if (measurer.rafaelTextObject.attr("text-anchor") == "middle" )
347                     measurer.rafaelTextObject.attr("x",  x + boxWidth/2 + TEXT_PADDING/2);
348                 else if (measurer.rafaelTextObject.attr("text-anchor") == "end" )
349                     measurer.rafaelTextObject.attr("x",  x + boxWidth + TEXT_PADDING/2);
350                 else 
351                     measurer.rafaelTextObject.attr("x", x + boxWidth/2 - bb.width/2 + TEXT_PADDING/2);
352                 
353                 var boxStyle = {stroke: Color.LightSteelBlue2, "stroke-width": 1.0, "stroke-dasharray": "- "};
354                 /*
355                 var box = paper.rect(x+.0 + boxWidth/2 - bb.width/2+ TEXT_PADDING/2, y + .5 + boxHeight/2 - bb.height/2, width, height).attr(boxStyle);
356                 box.attr("height", bb.height);
357                 */
358                 //var box = paper.rect(bb.x - .5 + bb.width/2 + TEXT_PADDING, bb.y + bb.height/2, bb.width, bb.height).attr(boxStyle);
359                 
360                 var textAreaCX = x + boxWidth/2;
361                 var textAreaCY = y + height/2;
362                 var dotLeftTop = paper.ellipse(x, y, 3, 3).attr({"stroke-width": 0, fill: Color.LightSteelBlue, stroke: "none"});
363                 var dotCenter = paper.ellipse(textAreaCX, textAreaCY, 3, 3).attr({fill: Color.LightSteelBlue2, stroke: "none"});
364
365                 /*
366                 // real bbox
367                 var bb = measurer.rafaelTextObject.getBBox();
368                 var rect = paper.rect(bb.x+.5, bb.y + .5, bb.width, bb.height).attr({"stroke-width": 1});
369                 */
370                 var boxStyle = {stroke: Color.LightSteelBlue2, "stroke-width": 1.0, "stroke-dasharray": "- "};
371                 var rect = paper.rect(x+.5, y + .5, boxWidth, boxHeight).attr(boxStyle);
372             }
373             
374             
375             
376             
377             /*
378             for (var i=0; i<1; i++) {
379                 var t = text;
380                 //var t = "Высококвалифицирова";
381                 
382                 var text = paper.text(300, 100, t).attr(font).attr("text-anchor", "start");
383                 var bbText = text.getBBox();
384                 paper.rect(300+.5, 100 + .5, bbText.width, bbText.height).attr({"stroke-width": 1});
385                 console.log("t: ", t.replace(/\n/g, "↵"));
386                 
387                 while (measurer.getPosition() < measurer.text.getEndIndex()) {
388                     var layout = measurer.nextLayout(width);
389                     //console.log("LAYOUT: " + layout + ", getPosition: " + measurer.getPosition());
390                     if (layout != null)
391                         layouts.push(layout);
392                 };
393                 
394                 measurer.rafaelTextObject.attr("text", layouts.join("\n"));
395                 var bb = measurer.rafaelTextObject.getBBox();
396                 var rect = paper.rect(bb.x+.5, bb.y + .5, bb.width, bb.height).attr({"stroke-width": 1});
397                 
398                 lay.push(layouts);
399                 console.log(layouts);
400             }
401             */
402             
403             
404             var fitTextToWidth = function(original, width) {
405                 var text = original;
406
407                 // TODO: move attr on parameters
408                 var attr = {font: "11px Arial", opacity: 0};
409                 
410                 // remove length for "..."
411                 var dots = paper.text(0, 0, "...").attr(attr).hide();
412                 var dotsBB = dots.getBBox();
413                 
414                 var maxWidth = width - dotsBB.width;
415                 
416                 var textElement = paper.text(0, 0, text).attr(attr).hide();
417                 var bb = textElement.getBBox();
418                 
419                 // it's a little bit incorrect with "..."
420                 while (bb.width > maxWidth && text.length > 0) {
421                     text = text.substring(0, text.length - 1);
422                     textElement.attr({"text": text});
423                     bb = textElement.getBBox();
424                 }
425
426                 // remove element from paper
427                 textElement.remove();
428                 
429                 if (text != original) {
430                     text = text + "...";
431                 }
432
433                 return text;
434             }
435             
436             
437             var x=100, y=90, height=20;
438             var options = {"text-anchor": "middle", "boxHeight": 150, "vertical-align": "top"};
439             var options = {"boxHeight": 150, "vertical-align": "top"};
440             drawMultilineText(text, x, y, 150, 100, options);
441     };