mirror of
https://github.com/niklasvh/html2canvas.git
synced 2023-08-10 21:13:10 +03:00
finish textarea rendering
This commit is contained in:
parent
3da0ef0ea7
commit
95ca5a627a
57
src/render/canvas/__tests__/textarea-layout.ts
Normal file
57
src/render/canvas/__tests__/textarea-layout.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import {strictEqual} from 'assert';
|
||||
import {layout} from '../textarea-layout';
|
||||
import {fromCodePoint, toCodePoints} from 'css-line-break';
|
||||
|
||||
describe('textarea-layout', () => {
|
||||
it('should wrap lines at word boundaries', () => {
|
||||
layoutEqual(' A long text with several lines.',
|
||||
[' A long text', 'with several', 'lines.']);
|
||||
});
|
||||
it('should omit spaces at the end of a line', () => {
|
||||
layoutEqual(' A long text with several lines. 2',
|
||||
[' A long text', 'with several', 'lines. 2']);
|
||||
});
|
||||
it('should omit spaces at the end of a line 2', () => {
|
||||
layoutEqual(' A long text with several lines. 2',
|
||||
[' A long', 'text with', 'several', 'lines. 2']);
|
||||
});
|
||||
it('should omit spaces at the end of a line 3', () => {
|
||||
layoutEqual('A long text with sev eral lines.',
|
||||
['A long text', 'with sev', 'eral lines.']);
|
||||
});
|
||||
it('should respect newlines', () => {
|
||||
layoutEqual(' A long text\n with\n\n several lines.',
|
||||
[' A long text', ' with', '', ' several', 'lines.']);
|
||||
});
|
||||
it('should wrap too long words', () => {
|
||||
layoutEqual('Donaudampfschifffahrtskapitänskoch 2',
|
||||
['Donaudampfsc', 'hifffahrtska', 'pitänskoch 2']);
|
||||
});
|
||||
it('should wrap too long words 2', () => {
|
||||
layoutEqual('Donaudampfschifffahrtskapitänskoch\n 2',
|
||||
['Donaudampfsc', 'hifffahrtska', 'pitänskoch', ' 2']);
|
||||
});
|
||||
it('should wrap too long words 3', () => {
|
||||
layoutEqual('Donaudampfsc x2',
|
||||
['Donaudampfsc', 'x2']);
|
||||
});
|
||||
});
|
||||
|
||||
function layoutEqual(s: string, lines: string[]) {
|
||||
const pos = layout(toCodePoints(s).map(i => fromCodePoint(i)), 120, _ => 10);
|
||||
let line = '';
|
||||
let y = 0;
|
||||
let j = 0;
|
||||
for (let i = 0; i < pos.length; i++) {
|
||||
if (y === pos[i][1]) {
|
||||
if (pos[i][0] >= 0) {
|
||||
line += s.charAt(i);
|
||||
}
|
||||
} else {
|
||||
strictEqual(line, lines[j]);
|
||||
j++;
|
||||
line = pos[i][0] >= 0 ? s.charAt(i) : '';
|
||||
y = pos[i][1];
|
||||
}
|
||||
}
|
||||
}
|
@ -38,6 +38,7 @@ import {TextareaElementContainer} from '../../dom/elements/textarea-element-cont
|
||||
import {SelectElementContainer} from '../../dom/elements/select-element-container';
|
||||
import {IFrameElementContainer} from '../../dom/replaced-elements/iframe-element-container';
|
||||
import {TextShadow} from '../../css/property-descriptors/text-shadow';
|
||||
import {layout} from './textarea-layout';
|
||||
|
||||
export type RenderConfigurations = RenderOptions & {
|
||||
backgroundColor: Color | null;
|
||||
@ -141,77 +142,18 @@ export class CanvasRenderer {
|
||||
}
|
||||
|
||||
renderTextWithLetterSpacing(text: TextBounds, letterSpacing: number, lineHeight?: number) {
|
||||
if (letterSpacing === 0) {
|
||||
if (lineHeight===undefined){
|
||||
this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + text.bounds.height);
|
||||
}else {
|
||||
let pos = 0
|
||||
let word = 0
|
||||
let line = 0
|
||||
let y = text.bounds.top + lineHeight / 2
|
||||
while (pos < text.text.length) {
|
||||
let c = text.text.charAt(pos)
|
||||
pos++
|
||||
switch (c) {
|
||||
case ' ':
|
||||
if (this.ctx.measureText(text.text.substring(line, pos - 1)).width > text.bounds.width) {
|
||||
if (line === word) {
|
||||
let p = pos
|
||||
while (this.ctx.measureText(text.text.substring(line, p)).width > text.bounds.width) {
|
||||
p--
|
||||
}
|
||||
this.ctx.fillText(text.text.substring(line, p), text.bounds.left, y)
|
||||
y += lineHeight
|
||||
line = word = p
|
||||
} else {
|
||||
this.ctx.fillText(text.text.substring(line, word), text.bounds.left, y)
|
||||
y += lineHeight
|
||||
let s = word === pos - 1
|
||||
while (pos < text.text.length && text.text.charAt(pos) === ' ') pos++
|
||||
line = s ? pos : word
|
||||
word = pos
|
||||
}
|
||||
} else {
|
||||
word = pos
|
||||
}
|
||||
break
|
||||
case '\n':
|
||||
if (this.ctx.measureText(text.text.substring(line, pos - 1)).width > text.bounds.width) {
|
||||
if (line === word) {
|
||||
let p = pos
|
||||
while (this.ctx.measureText(text.text.substring(line, p)).width > text.bounds.width) {
|
||||
p--
|
||||
}
|
||||
this.ctx.fillText(text.text.substring(line, p), text.bounds.left, y)
|
||||
y += lineHeight
|
||||
line = word = p
|
||||
} else {
|
||||
this.ctx.fillText(text.text.substring(line, word), text.bounds.left, y)
|
||||
y += lineHeight
|
||||
line = word
|
||||
word = pos
|
||||
}
|
||||
} else {
|
||||
this.ctx.fillText(text.text.substring(line, pos - 1), text.bounds.left, y)
|
||||
y += lineHeight
|
||||
line = pos
|
||||
word = pos
|
||||
}
|
||||
break
|
||||
default:
|
||||
}
|
||||
}
|
||||
if (pos > line) {
|
||||
this.ctx.fillText(text.text.substring(line, pos + 1), text.bounds.left, y)
|
||||
if (lineHeight === undefined) {
|
||||
this.ctx.fillText(text.text, text.bounds.left, text.bounds.top + text.bounds.height);
|
||||
} else {
|
||||
const chars = toCodePoints(text.text).map(i => fromCodePoint(i));
|
||||
const pos = layout(chars, text.bounds.width, s => this.ctx.measureText(s).width + letterSpacing);
|
||||
const dx = text.bounds.left;
|
||||
const dy = text.bounds.top + lineHeight / 2;
|
||||
for (let i = 0; i < pos.length; i++) {
|
||||
if (pos[i][0] >= 0) {
|
||||
this.ctx.fillText(chars[i], pos[i][0] + dx, pos[i][1] * lineHeight + dy);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const letters = toCodePoints(text.text).map(i => fromCodePoint(i));
|
||||
letters.reduce((left, letter) => {
|
||||
this.ctx.fillText(letter, left, text.bounds.top + text.bounds.height);
|
||||
|
||||
return left + this.ctx.measureText(letter).width;
|
||||
}, text.bounds.left);
|
||||
}
|
||||
}
|
||||
|
||||
@ -455,7 +397,8 @@ export class CanvasRenderer {
|
||||
]);
|
||||
|
||||
this.ctx.clip();
|
||||
this.renderTextWithLetterSpacing(new TextBounds(container.value, textBounds), styles.letterSpacing, computeLineHeight(styles.lineHeight, styles.fontSize.number));
|
||||
const lineHeight = (container instanceof TextareaElementContainer) ? computeLineHeight(styles.lineHeight, styles.fontSize.number) : undefined;
|
||||
this.renderTextWithLetterSpacing(new TextBounds(container.value, textBounds), styles.letterSpacing, lineHeight);
|
||||
this.ctx.restore();
|
||||
this.ctx.textBaseline = 'bottom';
|
||||
this.ctx.textAlign = 'left';
|
||||
|
45
src/render/canvas/textarea-layout.ts
Normal file
45
src/render/canvas/textarea-layout.ts
Normal file
@ -0,0 +1,45 @@
|
||||
export function layout(chars: string[], width: number, measure: (s: string) => number): number[][] {
|
||||
const pos: number[][] = [];
|
||||
let line = 0;
|
||||
let lineWidth = 0;
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
if (chars[i] === '\n') {
|
||||
pos.push([-1, line]);
|
||||
line++;
|
||||
lineWidth = 0;
|
||||
} else {
|
||||
pos.push([lineWidth, line]);
|
||||
lineWidth += measure(chars[i]);
|
||||
if (lineWidth > width) {
|
||||
if (chars[i] === ' ') {
|
||||
let p = i;
|
||||
while (p > 0 && pos[p][1] === line && chars[p] === ' ') p--;
|
||||
for (let j = i; j > p; j--) {
|
||||
pos[j][0] = -1;
|
||||
}
|
||||
line++;
|
||||
lineWidth = 0;
|
||||
while (i < chars.length + 1 && chars[i + 1] === ' ') {
|
||||
pos.push([-1, line]);
|
||||
i++;
|
||||
}
|
||||
} else {
|
||||
let p = i;
|
||||
while (p > 0 && pos[p][1] === line && chars[p] !== ' ') p--;
|
||||
line++;
|
||||
if (chars[p] === ' ') {
|
||||
lineWidth -= pos[p + 1][0];
|
||||
for (let j = i; j > p; j--) {
|
||||
pos[j] = [pos[j][0] - pos[p + 1][0], line];
|
||||
}
|
||||
pos[p][0] = -1;
|
||||
} else {
|
||||
lineWidth -= pos[i][0];
|
||||
pos[i] = [0, line];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return pos;
|
||||
}
|
@ -16,31 +16,33 @@
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<p>1. <code>word wrap</code></p>
|
||||
<p>1. word wrap</p>
|
||||
<textarea> A long text with many words that should be wrapped on various lines.
|
||||
</textarea>
|
||||
<p>1. <code>word wrap</code></p>
|
||||
|
||||
<p>2. padding</p>
|
||||
<textarea style="padding: 1em">A long text with many words that should be wrapped on various lines.
|
||||
</textarea>
|
||||
<p>1. <code>word wrap</code></p>
|
||||
|
||||
<p>3. line height</p>
|
||||
<textarea style="line-height: 25px">A long text with many words.
|
||||
</textarea>
|
||||
<p>1. <code>word wrap</code></p>
|
||||
|
||||
<p>4. letter spacing</p>
|
||||
<textarea style="letter-spacing: 5px"> A long text with many words that should be wrapped on various lines.</textarea>
|
||||
|
||||
<p>5. multiple spaces</p>
|
||||
<textarea>A long text with many words that should be wrapped on various lines.
|
||||
</textarea>
|
||||
<p>1. <code>word wrap</code></p>
|
||||
|
||||
<p>6. newlines</p>
|
||||
<textarea>A long text with many
|
||||
words that
|
||||
|
||||
should be wrapped on various lines.
|
||||
</textarea>
|
||||
<p>1. <code>word wrap</code></p>
|
||||
<textarea>Donaudampfschifffahrtsgesellschaft
|
||||
</textarea>
|
||||
<p>1. <code>word wrap</code></p>
|
||||
<textarea>Donaudampfschifffahrtsgesellschaft Kapitän Koch
|
||||
</textarea>
|
||||
<p>1. <code>word wrap</code></p>
|
||||
|
||||
<p>7. long word</p>
|
||||
<textarea>Donaudampfschifffahrtsgesellschaftskapitänskochmütze</textarea>
|
||||
</div>
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user