- name: 2d.text.font.parse.basic code: | ctx.font = '20px serif'; @assert ctx.font === '20px serif'; ctx.font = '20PX SERIF'; @assert ctx.font === '20px serif'; @moz-todo - name: 2d.text.font.parse.tiny code: | ctx.font = '1px sans-serif'; @assert ctx.font === '1px sans-serif'; - name: 2d.text.font.parse.complex code: | ctx.font = 'small-caps italic 400 12px/2 Unknown Font, sans-serif'; @assert ['italic small-caps 12px "Unknown Font", sans-serif', 'italic small-caps 12px Unknown Font, sans-serif'].includes(ctx.font); - name: 2d.text.font.parse.complex2 code: | ctx.font = 'small-caps italic 400 12px/2 "Unknown Font #2", sans-serif'; @assert ctx.font === 'italic small-caps 12px "Unknown Font #2", sans-serif'; - name: 2d.text.font.parse.family code: | ctx.font = '20px cursive,fantasy,monospace,sans-serif,serif,UnquotedFont,"QuotedFont\\\\\\","'; @assert ctx.font === '20px cursive, fantasy, monospace, sans-serif, serif, UnquotedFont, "QuotedFont\\\\\\","'; # TODO: # 2d.text.font.parse.size.absolute # xx-small x-small small medium large x-large xx-large # 2d.text.font.parse.size.relative # smaller larger # 2d.text.font.parse.size.length.relative # em ex px # 2d.text.font.parse.size.length.absolute # in cm mm pt pc - name: 2d.text.font.parse.size.percentage canvas: 'style="font-size: 144px"' canvas_types: ['HtmlCanvas'] code: | ctx.font = '50% serif'; @assert ctx.font === '72px serif'; @moz-todo canvas.setAttribute('style', 'font-size: 100px'); @assert ctx.font === '72px serif'; @moz-todo - name: 2d.text.font.parse.size.percentage.default canvas_types: ['HtmlCanvas'] code: | var canvas2 = document.createElement('canvas'); var ctx2 = canvas2.getContext('2d'); ctx2.font = '1000% serif'; @assert ctx2.font === '100px serif'; @moz-todo - name: 2d.text.font.parse.system desc: System fonts must be computed to explicit values code: | ctx.font = 'message-box'; @assert ctx.font !== 'message-box'; - name: 2d.text.font.parse.invalid code: | ctx.font = '20px serif'; @assert ctx.font === '20px serif'; ctx.font = '20px serif'; ctx.font = ''; @assert ctx.font === '20px serif'; ctx.font = '20px serif'; ctx.font = 'bogus'; @assert ctx.font === '20px serif'; ctx.font = '20px serif'; ctx.font = 'inherit'; @assert ctx.font === '20px serif'; ctx.font = '20px serif'; ctx.font = '10px {bogus}'; @assert ctx.font === '20px serif'; ctx.font = '20px serif'; ctx.font = '10px initial'; @assert ctx.font === '20px serif'; @moz-todo ctx.font = '20px serif'; ctx.font = '10px default'; @assert ctx.font === '20px serif'; @moz-todo ctx.font = '20px serif'; ctx.font = '10px inherit'; @assert ctx.font === '20px serif'; ctx.font = '20px serif'; ctx.font = '10px revert'; @assert ctx.font === '20px serif'; ctx.font = '20px serif'; ctx.font = 'var(--x)'; @assert ctx.font === '20px serif'; ctx.font = '20px serif'; ctx.font = 'var(--x, 10px serif)'; @assert ctx.font === '20px serif'; ctx.font = '20px serif'; ctx.font = '1em serif; background: green; margin: 10px'; @assert ctx.font === '20px serif'; - name: 2d.text.font.default code: | @assert ctx.font === '10px sans-serif'; - name: 2d.text.font.relative_size canvas_types: ['HtmlCanvas'] code: | var canvas2 = document.createElement('canvas'); var ctx2 = canvas2.getContext('2d'); ctx2.font = '1em sans-serif'; @assert ctx2.font === '10px sans-serif'; - name: 2d.text.font.relative_size canvas_types: ['OffscreenCanvas', 'Worker'] code: | ctx.font = '1em sans-serif'; @assert ctx.font === '10px sans-serif'; - name: 2d.text.font.weight code: | ctx.font = 'italic 400 12px serif'; @assert ctx.font === 'italic 12px serif'; ctx.font = 'italic 300 12px serif'; @assert ctx.font === 'italic 300 12px serif'; - name: 2d.text.lang.default code: | @assert ctx.lang === 'inherit'; - name: 2d.text.lang.valid code: | ctx.lang = ''; @assert ctx.lang === ''; ctx.lang = 'inherit'; @assert ctx.lang === 'inherit'; ctx.lang = 'en-US'; @assert ctx.lang === 'en-US'; ctx.lang = 'not-a-real-lang'; @assert ctx.lang === 'not-a-real-lang'; - name: 2d.text.align.valid code: | ctx.textAlign = 'start'; @assert ctx.textAlign === 'start'; ctx.textAlign = 'end'; @assert ctx.textAlign === 'end'; ctx.textAlign = 'left'; @assert ctx.textAlign === 'left'; ctx.textAlign = 'right'; @assert ctx.textAlign === 'right'; ctx.textAlign = 'center'; @assert ctx.textAlign === 'center'; - name: 2d.text.align.invalid code: | ctx.textAlign = 'start'; ctx.textAlign = 'bogus'; @assert ctx.textAlign === 'start'; ctx.textAlign = 'start'; ctx.textAlign = 'END'; @assert ctx.textAlign === 'start'; ctx.textAlign = 'start'; ctx.textAlign = 'end '; @assert ctx.textAlign === 'start'; ctx.textAlign = 'start'; ctx.textAlign = 'end\0'; @assert ctx.textAlign === 'start'; - name: 2d.text.align.default code: | @assert ctx.textAlign === 'start'; - name: 2d.text.baseline.valid code: | ctx.textBaseline = 'top'; @assert ctx.textBaseline === 'top'; ctx.textBaseline = 'hanging'; @assert ctx.textBaseline === 'hanging'; ctx.textBaseline = 'middle'; @assert ctx.textBaseline === 'middle'; ctx.textBaseline = 'alphabetic'; @assert ctx.textBaseline === 'alphabetic'; ctx.textBaseline = 'ideographic'; @assert ctx.textBaseline === 'ideographic'; ctx.textBaseline = 'bottom'; @assert ctx.textBaseline === 'bottom'; - name: 2d.text.baseline.invalid code: | ctx.textBaseline = 'top'; ctx.textBaseline = 'bogus'; @assert ctx.textBaseline === 'top'; ctx.textBaseline = 'top'; ctx.textBaseline = 'MIDDLE'; @assert ctx.textBaseline === 'top'; ctx.textBaseline = 'top'; ctx.textBaseline = 'middle '; @assert ctx.textBaseline === 'top'; ctx.textBaseline = 'top'; ctx.textBaseline = 'middle\0'; @assert ctx.textBaseline === 'top'; - name: 2d.text.baseline.default code: | @assert ctx.textBaseline === 'alphabetic'; - name: 2d.text.direction.default code: | @assert ctx.direction === 'inherit'; - name: 2d.text.direction.valid code: | ctx.direction = 'ltr'; @assert ctx.direction === 'ltr'; ctx.direction = 'rtl'; @assert ctx.direction === 'rtl'; ctx.direction = 'inherit'; @assert ctx.direction === 'inherit'; - name: 2d.text.direction.invalid code: | ctx.direction = 'ltr'; ctx.direction = 'rtl '; @assert ctx.direction === 'ltr'; ctx.direction = 'rtl'; ctx.direction = 'LTR'; @assert ctx.direction === 'rtl'; ctx.direction = 'ltr'; ctx.direction = 'rtl\0'; @assert ctx.direction === 'ltr'; ctx.direction = 'ltr'; ctx.direction = 'bogus'; @assert ctx.direction === 'ltr'; ctx.direction = 'ltr'; ctx.direction = 'inheri'; @assert ctx.direction === 'ltr'; - name: 2d.text.draw.baseline.top desc: textBaseline top is the top of the em square (not the bounding box) test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.textBaseline = 'top'; ctx.fillText('CC', 0, 0); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @assert pixel 95,45 ==~ 0,255,0,255; expected: green variants: - &load-font-variant-definition HtmlCanvas: append_variants_to_name: false canvas_types: ['HtmlCanvas'] load_font: |- await document.fonts.ready; OffscreenCanvas: append_variants_to_name: false canvas_types: ['OffscreenCanvas', 'Worker'] load_font: |- var f = new FontFace("{{ fonts[0] }}", "url('/fonts/{{ fonts[0] }}.ttf')"); f.load(); {% set root = 'self' if canvas_type == 'Worker' else 'document' %} {{ root }}.fonts.add(f); await {{ root }}.fonts.ready; - name: 2d.text.draw.baseline.bottom desc: textBaseline bottom is the bottom of the em square (not the bounding box) test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.textBaseline = 'bottom'; ctx.fillText('CC', 0, 50); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @assert pixel 95,45 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.baseline.middle desc: textBaseline middle is the middle of the em square (not the bounding box) test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.textBaseline = 'middle'; ctx.fillText('CC', 0, 25); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @assert pixel 95,45 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.baseline.alphabetic test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.textBaseline = 'alphabetic'; ctx.fillText('CC', 0, 37.5); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @assert pixel 95,45 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.baseline.ideographic test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.textBaseline = 'ideographic'; ctx.fillText('CC', 0, 31.25); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @moz-todo @assert pixel 95,45 ==~ 0,255,0,255; @moz-todo expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.baseline.hanging test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.textBaseline = 'hanging'; ctx.fillText('CC', 0, 12.5); @assert pixel 5,5 ==~ 0,255,0,255; @moz-todo @assert pixel 95,5 ==~ 0,255,0,255; @moz-todo @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @assert pixel 95,45 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.space.collapse.space desc: Space characters are converted to U+0020, and are NOT collapsed test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.fillText('E EE', 0, 37.5); @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 255,0,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.space.collapse.other desc: Space characters are converted to U+0020, and are NOT collapsed test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.fillText('E \x09\x0a\x0c\x0d \x09\x0a\x0c\x0dEE', 0, 37.5); @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 255,0,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.space.collapse.start desc: Space characters at the start of a line are NOT collapsed test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.fillText(' EE', 0, 37.5); @assert pixel 25,25 ==~ 255,0,0,255; @moz-todo @assert pixel 75,25 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.space.collapse.end desc: Space characters at the end of a line are NOT collapsed test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.textAlign = 'right'; ctx.fillText('EE ', 100, 37.5); @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 255,0,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.measure.width.space desc: Space characters are converted to U+0020 and NOT collapsed test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; @assert ctx.measureText('A B').width === 150; @assert ctx.measureText('A B').width === 200; @assert ctx.measureText('A \x09\x0a\x0c\x0d \x09\x0a\x0c\x0dB').width === 650; @assert ctx.measureText('A \x0b B').width >= 200; @assert ctx.measureText(' AB').width === 150; @assert ctx.measureText('AB ').width === 150; variants: - *load-font-variant-definition - name: 2d.text.measure.width.nullCharacter desc: Null character does not take up space test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; @assert ctx.measureText('\u0000').width === 0; variants: - *load-font-variant-definition - name: 2d.text.drawing.style.measure.rtl.text desc: Measurement should follow canvas direction instead text direction code: | metrics = ctx.measureText('اَلْعَرَبِيَّةُ'); @assert metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight; metrics = ctx.measureText('hello'); @assert metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight; - name: 2d.text.drawing.style.measure.textAlign desc: Measurement should be related to textAlignment code: | ctx.textAlign = "right"; metrics = ctx.measureText('hello'); @assert metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight; ctx.textAlign = "left" metrics = ctx.measureText('hello'); @assert metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight; - name: 2d.text.drawing.style.measure.direction desc: Measurement should follow text direction code: | ctx.direction = "ltr"; metrics = ctx.measureText('hello'); @assert metrics.actualBoundingBoxLeft < metrics.actualBoundingBoxRight; ctx.direction = "rtl"; metrics = ctx.measureText('hello'); @assert metrics.actualBoundingBoxLeft > metrics.actualBoundingBoxRight; - name: 2d.text.draw.fill.basic desc: fillText draws filled text manual: code: | ctx.fillStyle = '#000'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.strokeStyle = '#f00'; ctx.font = '35px Arial, sans-serif'; ctx.fillText('PASS', 5, 35); expected: &passfill | size 100 50 cr.set_source_rgb(0, 0, 0) cr.rectangle(0, 0, 100, 50) cr.fill() cr.set_source_rgb(0, 1, 0) cr.select_font_face("Arial") cr.set_font_size(35) cr.translate(5, 35) cr.text_path("PASS") cr.fill() - name: 2d.text.draw.fill.unaffected desc: fillText does not start a new path or subpath code: | ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.moveTo(0, 0); ctx.lineTo(100, 0); ctx.font = '35px Arial, sans-serif'; ctx.fillText('FAIL', 5, 35); ctx.lineTo(100, 50); ctx.lineTo(0, 50); ctx.fillStyle = '#0f0'; ctx.fill(); @assert pixel 50,25 == 0,255,0,255; @assert pixel 5,45 == 0,255,0,255; expected: green - name: 2d.text.draw.fill.rtl desc: fillText respects Right-To-Left Override characters manual: code: | ctx.fillStyle = '#000'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.strokeStyle = '#f00'; ctx.font = '35px Arial, sans-serif'; ctx.fillText('\u202eFAIL \xa0 \xa0 SSAP', 5, 35); expected: *passfill - name: 2d.text.draw.fill.maxWidth.large desc: fillText handles maxWidth correctly manual: code: | ctx.fillStyle = '#000'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.font = '35px Arial, sans-serif'; ctx.fillText('PASS', 5, 35, 200); expected: *passfill - name: 2d.text.draw.fill.maxWidth.small desc: fillText handles maxWidth correctly code: | ctx.fillStyle = '#0f0'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#f00'; ctx.font = '35px Arial, sans-serif'; ctx.fillText('fail fail fail fail fail', -100, 35, 90); _assertGreen(ctx, 100, 50); expected: green - name: 2d.text.draw.fill.maxWidth.zero desc: fillText handles maxWidth correctly code: | ctx.fillStyle = '#0f0'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#f00'; ctx.font = '35px Arial, sans-serif'; ctx.fillText('fail fail fail fail fail', 5, 35, 0); _assertGreen(ctx, 100, 50); expected: green - name: 2d.text.draw.fill.maxWidth.negative desc: fillText handles maxWidth correctly code: | ctx.fillStyle = '#0f0'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#f00'; ctx.font = '35px Arial, sans-serif'; ctx.fillText('fail fail fail fail fail', 5, 35, -1); _assertGreen(ctx, 100, 50); expected: green - name: 2d.text.draw.fill.maxWidth.NaN desc: fillText handles maxWidth correctly code: | ctx.fillStyle = '#0f0'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#f00'; ctx.font = '35px Arial, sans-serif'; ctx.fillText('fail fail fail fail fail', 5, 35, NaN); _assertGreen(ctx, 100, 50); expected: green - name: 2d.text.draw.stroke.basic desc: strokeText draws stroked text manual: code: | ctx.fillStyle = '#000'; ctx.fillRect(0, 0, 100, 50); ctx.strokeStyle = '#0f0'; ctx.fillStyle = '#f00'; ctx.lineWidth = 1; ctx.font = '35px Arial, sans-serif'; ctx.strokeText('PASS', 5, 35); expected: | size 100 50 cr.set_source_rgb(0, 0, 0) cr.rectangle(0, 0, 100, 50) cr.fill() cr.set_source_rgb(0, 1, 0) cr.select_font_face("Arial") cr.set_font_size(35) cr.set_line_width(1) cr.translate(5, 35) cr.text_path("PASS") cr.stroke() - name: 2d.text.draw.stroke.unaffected desc: strokeText does not start a new path or subpath code: | ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.moveTo(0, 0); ctx.lineTo(100, 0); ctx.font = '35px Arial, sans-serif'; ctx.strokeStyle = '#f00'; ctx.strokeText('FAIL', 5, 35); ctx.lineTo(100, 50); ctx.lineTo(0, 50); ctx.fillStyle = '#0f0'; ctx.fill(); @assert pixel 50,25 == 0,255,0,255; @assert pixel 5,45 == 0,255,0,255; expected: green - name: 2d.text.draw.kern.consistent desc: Stroked and filled text should have exactly the same kerning so it overlaps manual: code: | ctx.fillStyle = '#0f0'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#f00'; ctx.strokeStyle = '#0f0'; ctx.lineWidth = 3; ctx.font = '20px Arial, sans-serif'; ctx.fillText('VAVAVAVAVAVAVA', -50, 25); ctx.fillText('ToToToToToToTo', -50, 45); ctx.strokeText('VAVAVAVAVAVAVA', -50, 25); ctx.strokeText('ToToToToToToTo', -50, 45); expected: green - name: 2d.text.drawing.style.reset.fontKerning.none desc: >- crbug/338965374, fontKerning still works after setting font for a second time. code: | ctx.font = '100px serif'; ctx.fontKerning = "none"; const width1 = ctx.measureText("AW").width; ctx.font = '100px serif'; @assert ctx.fontKerning === "none"; const width2 = ctx.measureText("AW").width; @assert width1 === width2; - name: 2d.text.drawing.style.reset.fontKerning.none2 desc: FontKerning value still applies after font changes. code: | ctx.font = '10px serif'; ctx.fontKerning = "none"; ctx.font = '20px serif'; ctx.fillText("TATATA", 20, 30); reference: | ctx.font = '20px serif'; ctx.fontKerning = "none"; ctx.fillText("TATATA", 20, 30); # CanvasTest is: # A = (0, 0) to (1em, 0.75em) (above baseline) # B = (0, 0) to (1em, -0.25em) (below baseline) # C = (0, -0.25em) to (1em, 0.75em) (the em square) plus some Xs above and below # D = (0, -0.25em) to (1em, 0.75em) (the em square) plus some Xs left and right # E = (0, -0.25em) to (1em, 0.75em) (the em square) # space = empty, 1em wide # # At 50px, "E" will fill the canvas vertically # At 67px, "A" will fill the canvas vertically # # Ideographic baseline is 0.125em above alphabetic # Mathematical baseline is 0.375em above alphabetic # Hanging baseline is 0.500em above alphabetic - name: 2d.text.draw.fill.maxWidth.fontface desc: fillText works on @font-face fonts test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#0f0'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#f00'; ctx.fillText('EEEE', -50, 37.5, 40); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.fill.maxWidth.bound desc: fillText handles maxWidth based on line size, not bounding box size test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.fillText('DD', 0, 37.5, 100); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.fontface fonts: - CanvasTest test_type: promise code: | {{ load_font }} ctx.font = '67px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.fillText('AA', 0, 50); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.fontface.repeat desc: Draw with the font immediately, then wait a bit until and draw again. (This crashes some version of WebKit.) test_type: promise fonts: - CanvasTest font_unused_in_dom: true code: | {{ load_font }} ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.font = '67px CanvasTest'; ctx.fillStyle = '#0f0'; ctx.fillText('AA', 0, 50); await new Promise(resolve => t.step_timeout(resolve, 500)); ctx.fillText('AA', 0, 50); _assertPixelApprox(canvas, 5,5, 0,255,0,255, 2); _assertPixelApprox(canvas, 95,5, 0,255,0,255, 2); _assertPixelApprox(canvas, 25,25, 0,255,0,255, 2); _assertPixelApprox(canvas, 75,25, 0,255,0,255, 2); expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.fontface.notinpage desc: '@font-face fonts should work even if they are not used in the page' test_type: promise fonts: - CanvasTest font_unused_in_dom: true code: | {{ load_font }} ctx.font = '67px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.fillText('AA', 0, 50); @assert pixel 5,5 ==~ 0,255,0,255; @moz-todo @assert pixel 95,5 ==~ 0,255,0,255; @moz-todo @assert pixel 25,25 ==~ 0,255,0,255; @moz-todo @assert pixel 75,25 ==~ 0,255,0,255; @moz-todo expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.align.left desc: textAlign left is the left of the first em square (not the bounding box) test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.textAlign = 'left'; ctx.fillText('DD', 0, 37.5); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @assert pixel 95,45 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.align.right desc: textAlign right is the right of the last em square (not the bounding box) test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.textAlign = 'right'; ctx.fillText('DD', 100, 37.5); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @assert pixel 95,45 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.align.start.ltr desc: textAlign start with ltr is the left edge canvas: dir="ltr" test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; {% if canvas_type != 'HtmlCanvas' %} ctx.direction = 'ltr'; {% endif %} ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.textAlign = 'start'; ctx.fillText('DD', 0, 37.5); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @assert pixel 95,45 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.align.start.rtl desc: textAlign start with rtl is the right edge canvas: dir="rtl" test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; {% if canvas_type != 'HtmlCanvas' %} ctx.direction = 'rtl'; {% endif %} ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.textAlign = 'start'; ctx.fillText('DD', 100, 37.5); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @assert pixel 95,45 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.align.end.ltr desc: textAlign end with ltr is the right edge test_type: promise canvas: dir="ltr" fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; {% if canvas_type != 'HtmlCanvas' %} ctx.direction = 'ltr'; {% endif %} ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.textAlign = 'end'; ctx.fillText('DD', 100, 37.5); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @assert pixel 95,45 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.align.end.rtl desc: textAlign end with rtl is the left edge canvas: dir="rtl" test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; {% if canvas_type != 'HtmlCanvas' %} ctx.direction = 'rtl'; {% endif %} ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.textAlign = 'end'; ctx.fillText('DD', 0, 37.5); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @assert pixel 95,45 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.align.center desc: textAlign center is the center of the em squares (not the bounding box) test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.textAlign = 'center'; ctx.fillText('DD', 50, 37.5); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 95,5 ==~ 0,255,0,255; @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @assert pixel 95,45 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.space.basic desc: U+0020 is rendered the correct size (1em wide) test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.fillText('E EE', -100, 37.5); @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.draw.space.collapse.nonspace desc: Non-space characters are not converted to U+0020 and collapsed test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, 100, 50); ctx.fillStyle = '#0f0'; ctx.fillText('E\x0b EE', -150, 37.5); @assert pixel 25,25 ==~ 0,255,0,255; @assert pixel 75,25 ==~ 0,255,0,255; expected: green variants: - *load-font-variant-definition - name: 2d.text.measure.width.basic desc: The width of character is same as font used test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; @assert ctx.measureText('A').width === 50; @assert ctx.measureText('AA').width === 100; @assert ctx.measureText('ABCD').width === 200; ctx.font = '100px CanvasTest'; @assert ctx.measureText('A').width === 100; variants: - *load-font-variant-definition - name: 2d.text.measure.width.empty desc: The empty string has zero width test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; @assert ctx.measureText("").width === 0; variants: - *load-font-variant-definition - name: 2d.text.measure.actualBoundingBox desc: Testing actualBoundingBox test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.direction = 'ltr'; ctx.align = 'left' ctx.baseline = 'alphabetic' // Different platforms may render text slightly different. // Values that are nominally expected to be zero might actually vary by a // pixel or so if the UA accounts for antialiasing at glyph edges, so we // allow a slight deviation. @assert Math.abs(ctx.measureText('A').actualBoundingBoxLeft) <= 1; @assert ctx.measureText('A').actualBoundingBoxRight >= 50; @assert ctx.measureText('A').actualBoundingBoxAscent >= 35; @assert Math.abs(ctx.measureText('A').actualBoundingBoxDescent) <= 1; @assert ctx.measureText('D').actualBoundingBoxLeft >= 48; @assert ctx.measureText('D').actualBoundingBoxLeft <= 52; @assert ctx.measureText('D').actualBoundingBoxRight >= 75; @assert ctx.measureText('D').actualBoundingBoxRight <= 80; @assert ctx.measureText('D').actualBoundingBoxAscent >= 35; @assert ctx.measureText('D').actualBoundingBoxAscent <= 40; @assert ctx.measureText('D').actualBoundingBoxDescent >= 12; @assert ctx.measureText('D').actualBoundingBoxDescent <= 15; @assert Math.abs(ctx.measureText('ABCD').actualBoundingBoxLeft) <= 1; @assert ctx.measureText('ABCD').actualBoundingBoxRight >= 200; @assert ctx.measureText('ABCD').actualBoundingBoxAscent >= 85; @assert ctx.measureText('ABCD').actualBoundingBoxDescent >= 37; variants: - *load-font-variant-definition - name: 2d.text.measure.actualBoundingBox.whitespace desc: Testing actualBoundingBox with leading/trailing whitespace test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.direction = 'ltr'; ctx.align = 'left' ctx.baseline = 'alphabetic' // Different platforms may render text slightly different. // Values that are nominally expected to be zero might actually vary by a // pixel or so if the UA accounts for antialiasing at glyph edges, so we // allow a slight deviation. var whitespaces = [0x9, 0xa, 0xc, 0xd, 0x20, 0x3000]; for (var codepoint of whitespaces) { let whitespace = String.fromCharCode(codepoint); @assert Math.abs(ctx.measureText('A' + whitespace).actualBoundingBoxLeft) <= 1; @assert ctx.measureText('A' + whitespace).actualBoundingBoxRight >= 50; @assert Math.abs(ctx.measureText(whitespace + 'A').actualBoundingBoxLeft) >= 49; @assert ctx.measureText(whitespace + 'A').actualBoundingBoxRight <= 101; } variants: - *load-font-variant-definition - name: 2d.text.measure.actualBoundingBox.small-font desc: Testing that actualBoundingBox metrics are precise at small font sizes test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '1.5px CanvasTest'; ctx.direction = 'ltr'; ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic'; // At small font sizes, Skia may store glyph bounds as integers, causing // loss of precision (crbug.com/479240778). The CanvasTest 'E' glyph fills // the em square (0, -0.25em) to (1em, 0.75em), so at 1.5px: // actualBoundingBoxRight ≈ 1.5 (1em) // actualBoundingBoxAscent ≈ 1.125 (0.75em) // actualBoundingBoxDescent≈ 0.375 (0.25em) // With integer quantization these would snap to 2, 2, 1 respectively. // Use ±0.25 tolerances so quantized values are reliably caught. var m = ctx.measureText('E'); @assert m.actualBoundingBoxRight > 1.25; @assert m.actualBoundingBoxRight < 1.75; @assert m.actualBoundingBoxAscent > 0.875; @assert m.actualBoundingBoxAscent < 1.375; @assert m.actualBoundingBoxDescent > 0.125; @assert m.actualBoundingBoxDescent < 0.625; variants: - *load-font-variant-definition - name: 2d.text.measure.fontBoundingBox desc: Testing fontBoundingBox measurements test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '40px CanvasTest'; ctx.direction = 'ltr'; ctx.align = 'left' @assert ctx.measureText('A').fontBoundingBoxAscent === 30; @assert ctx.measureText('A').fontBoundingBoxDescent === 10; @assert ctx.measureText('ABCD').fontBoundingBoxAscent === 30; @assert ctx.measureText('ABCD').fontBoundingBoxDescent === 10; variants: - *load-font-variant-definition - name: 2d.text.measure.fontBoundingBox.ahem desc: Testing fontBoundingBox for font ahem test_type: promise fonts: - Ahem code: | {{ load_font }} ctx.font = '50px Ahem'; ctx.direction = 'ltr'; ctx.align = 'left' @assert ctx.measureText('A').fontBoundingBoxAscent === 40; @assert ctx.measureText('A').fontBoundingBoxDescent === 10; @assert ctx.measureText('ABCD').fontBoundingBoxAscent === 40; @assert ctx.measureText('ABCD').fontBoundingBoxDescent === 10; variants: - *load-font-variant-definition - name: 2d.text.measure.fontBoundingBox-reduced-ascent desc: Testing fontBoundingBox for OffscreenCanvas with reduced ascent metric test_type: promise fonts: - CanvasTest-ascent256 code: | {{ load_font }} ctx.font = '40px CanvasTest-ascent256'; ctx.direction = 'ltr'; ctx.align = 'left' @assert ctx.measureText('A').fontBoundingBoxAscent === 10; @assert ctx.measureText('A').fontBoundingBoxDescent === 10; @assert ctx.measureText('ABCD').fontBoundingBoxAscent === 10; @assert ctx.measureText('ABCD').fontBoundingBoxDescent === 10; variants: - *load-font-variant-definition - name: 2d.text.measure.fontBoundingBox-zero-descent desc: Testing fontBoundingBox for OffscreenCanvas with zero descent metric test_type: promise fonts: - CanvasTest-descent0 code: | {{ load_font }} ctx.font = '40px CanvasTest-descent0'; ctx.direction = 'ltr'; ctx.align = 'left' @assert ctx.measureText('A').fontBoundingBoxAscent === 30; @assert ctx.measureText('A').fontBoundingBoxDescent === 0; @assert ctx.measureText('ABCD').fontBoundingBoxAscent === 30; @assert ctx.measureText('ABCD').fontBoundingBoxDescent === 0; variants: - *load-font-variant-definition - name: 2d.text.measure.emHeights desc: Testing emHeights test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '40px CanvasTest'; ctx.direction = 'ltr'; ctx.align = 'left' @assert ctx.measureText('A').emHeightAscent === 30; @assert ctx.measureText('A').emHeightDescent === 10; @assert ctx.measureText('A').emHeightDescent + ctx.measureText('A').emHeightAscent === 40; @assert ctx.measureText('ABCD').emHeightAscent === 30; @assert ctx.measureText('ABCD').emHeightDescent === 10; @assert ctx.measureText('ABCD').emHeightDescent + ctx.measureText('ABCD').emHeightAscent === 40; variants: - *load-font-variant-definition - name: 2d.text.measure.emHeights-low-ascent desc: Testing emHeights with reduced ascent metric test_type: promise fonts: - CanvasTest-ascent256 code: | {{ load_font }} ctx.font = '40px CanvasTest-ascent256'; ctx.direction = 'ltr'; ctx.align = 'left' @assert ctx.measureText('A').emHeightAscent === 20; @assert ctx.measureText('A').emHeightDescent === 20; @assert ctx.measureText('A').emHeightDescent + ctx.measureText('A').emHeightAscent === 40; @assert ctx.measureText('ABCD').emHeightAscent === 20; @assert ctx.measureText('ABCD').emHeightDescent === 20; @assert ctx.measureText('ABCD').emHeightDescent + ctx.measureText('ABCD').emHeightAscent === 40; variants: - *load-font-variant-definition - name: 2d.text.measure.emHeights-zero-descent desc: Testing emHeights with zero descent metric test_type: promise fonts: - CanvasTest-descent0 code: | {{ load_font }} ctx.font = '40px CanvasTest-descent0'; ctx.direction = 'ltr'; ctx.align = 'left' @assert ctx.measureText('A').emHeightAscent === 40; @assert ctx.measureText('A').emHeightDescent === 0; @assert ctx.measureText('A').emHeightDescent + ctx.measureText('A').emHeightAscent === 40; @assert ctx.measureText('ABCD').emHeightAscent === 40; @assert ctx.measureText('ABCD').emHeightDescent === 0; @assert ctx.measureText('ABCD').emHeightDescent + ctx.measureText('ABCD').emHeightAscent === 40; variants: - *load-font-variant-definition - name: 2d.text.measure.baselines desc: Testing baselines test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.direction = 'ltr'; ctx.align = 'left' @assert Math.abs(ctx.measureText('A').alphabeticBaseline) === 0; @assert ctx.measureText('A').ideographicBaseline === 6.25; @assert ctx.measureText('A').hangingBaseline === 25; @assert Math.abs(ctx.measureText('ABCD').alphabeticBaseline) === 0; @assert ctx.measureText('ABCD').ideographicBaseline === 6.25; @assert ctx.measureText('ABCD').hangingBaseline === 25; variants: - *load-font-variant-definition - name: 2d.text.measure.selection-rects.tentative desc: >- Check that TextMetrics::getSelectionRects() matches its DOM equivalent, with direction {{ text_direction }}, text align {{ text_align }}, {{ letter_spacing }} letter spacing, and {{ variant_names[3] }}. canvas_types: ['HtmlCanvas', 'OffscreenCanvas'] code: | function placeAndSelectTextInDOM(text, from, to) { const el = document.createElement("div"); el.innerHTML = text; el.style.font = '50px sans-serif'; el.style.direction = '{{ text_direction }}'; el.style.textAlign = '{{ text_align }}'; el.style.letterSpacing = '{{ letter_spacing }}'; document.body.appendChild(el); let range = document.createRange(); range.setStart(el.childNodes[0], 0); range.setEnd(el.childNodes[0], text.length); const parent = range.getClientRects()[0]; let width = 0; for (const rect of range.getClientRects()) { width += rect.width; } range.setStart(el.childNodes[0], from); range.setEnd(el.childNodes[0], to); let sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); let sel_rects = sel.getRangeAt(0).getClientRects(); document.body.removeChild(el); // Offset to the alignment point determined by textAlign. let text_align_dx; switch (el.style.textAlign) { case 'right': text_align_dx = width; break; case 'center': text_align_dx = width / 2; break; default: text_align_dx = 0; } for(let i = 0 ; i < sel_rects.length ; ++i) { sel_rects[i].x -= parent.x + text_align_dx; sel_rects[i].y -= parent.y; } return sel_rects; } function checkRectListsMatch(list_a, list_b) { @assert list_a.length === list_b.length; for(let i = 0 ; i < list_a.length ; ++i) { assert_approx_equals(list_a[i].x, list_b[i].x, 1.0); assert_approx_equals(list_a[i].width, list_b[i].width, 1.0); assert_approx_equals(list_a[i].height, list_b[i].height, 1.0); // Y-position not tested here as getting the baseline for text in the // DOM is not straightforward. } } {% if use_directional_override %} function addDirectionalOverrideCharacters(text, direction_is_ltr) { // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en const LTR_OVERRIDE = '\u202D'; const RTL_OVERRIDE = '\u202E'; const OVERRIDE_END = '\u202C'; if (direction_is_ltr) { return LTR_OVERRIDE + text + OVERRIDE_END; } return RTL_OVERRIDE + text + OVERRIDE_END; } {% endif %} ctx.font = '50px sans-serif'; ctx.direction = '{{ text_direction }}'; ctx.textAlign = '{{ text_align }}'; ctx.letterSpacing = '{{ letter_spacing }}'; const kTexts = [ 'UNAVAILABLE', '🏁🎶🏁', ')(あ)(', '-abcd_', 'איפה הספרייה?', 'bidiמתמטיקה' ] for (text of kTexts) { {% if use_directional_override %} text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr'); {% endif %} const tm = ctx.measureText(text); // First character. checkRectListsMatch( tm.getSelectionRects(0, 1), placeAndSelectTextInDOM(text, 0, 1) ); // Last character. checkRectListsMatch( tm.getSelectionRects(text.length - 1, text.length), placeAndSelectTextInDOM(text, text.length - 1, text.length) ); // Whole string. checkRectListsMatch( tm.getSelectionRects(0, text.length), placeAndSelectTextInDOM(text, 0, text.length) ); // Intermediate string. checkRectListsMatch( tm.getSelectionRects(1, text.length - 1), placeAndSelectTextInDOM(text, 1, text.length - 1) ); // Invalid start > end string. Creates 0 width rectangle. checkRectListsMatch( tm.getSelectionRects(3, 2), placeAndSelectTextInDOM(text, 3, 2) ); checkRectListsMatch( tm.getSelectionRects(1, 0), placeAndSelectTextInDOM(text, 1, 0) ); } variants_layout: [single_file, single_file, single_file, single_file] variants: - direction-ltr: text_direction: ltr direction-rtl: text_direction: rtl - align-left: text_align: left align-center: text_align: center align-right: text_align: right - no-spacing: letter_spacing: 0px spacing: letter_spacing: 10px - no-directional-override: use_directional_override: false directional-override: use_directional_override: true - name: 2d.text.measure.selection-rects-baselines.tentative desc: >- Check that TextMetrics::getSelectionRects() works correctly with textBaseline. code: | ctx.font = '50px sans-serif'; const kBaselines = [ "top", "hanging", "middle", "alphabetic", "ideographic", "bottom", ]; const kTexts = [ 'UNAVAILABLE', '🏁🎶🏁', ')(あ)(', '-abcd_', 'איפה הספרייה?', 'bidiמתמטיקה' ] for (const text of kTexts) { for (const baseline of kBaselines) { const tm = ctx.measureText(text); // First character. for (const r of tm.getSelectionRects(0, 1)) { assert_approx_equals(r.top, -tm.fontBoundingBoxAscent, 1.0); assert_approx_equals(r.bottom, tm.fontBoundingBoxDescent, 1.0); } // Last character. for (const r of tm.getSelectionRects(text.length - 1, text.length)) { assert_approx_equals(r.top, -tm.fontBoundingBoxAscent, 1.0); assert_approx_equals(r.bottom, tm.fontBoundingBoxDescent, 1.0); } // Whole string. for (const r of tm.getSelectionRects(0, text.length)) { assert_approx_equals(r.top, -tm.fontBoundingBoxAscent, 1.0); assert_approx_equals(r.bottom, tm.fontBoundingBoxDescent, 1.0); } // Intermediate string. for (const r of tm.getSelectionRects(1, text.length - 1)) { assert_approx_equals(r.top, -tm.fontBoundingBoxAscent, 1.0); assert_approx_equals(r.bottom, tm.fontBoundingBoxDescent, 1.0); } // Invalid start > end string. Creates 0 width rectangle. for (const r of tm.getSelectionRects(3, 2)) { assert_approx_equals(r.top, -tm.fontBoundingBoxAscent, 1.0); assert_approx_equals(r.bottom, tm.fontBoundingBoxDescent, 1.0); } for (const r of tm.getSelectionRects(1, 0)) { assert_approx_equals(r.top, -tm.fontBoundingBoxAscent, 1.0); assert_approx_equals(r.bottom, tm.fontBoundingBoxDescent, 1.0); } } } - name: 2d.text.measure.selection-rects-exceptions.tentative desc: >- Check that TextMetrics::getSelectionRects() throws when using invalid indexes. code: | const kTexts = [ 'UNAVAILABLE', '🏁🎶🏁', ')(あ)(', '-abcd_', 'اين المكتبة؟', 'bidiالرياضيات' ] for (const text of kTexts) { const tm = ctx.measureText(text); // Handled by the IDL binding. assert_throws_js(TypeError, () => tm.getSelectionRects(-1, 0) ); assert_throws_js(TypeError, () => tm.getSelectionRects(0, -1) ); assert_throws_js(TypeError, () => tm.getSelectionRects(-1, -1) ); // Thrown in TextMetrics. assert_throws_dom("IndexSizeError", () => tm.getSelectionRects(text.length + 1, 0) ); assert_throws_dom("IndexSizeError", () => tm.getSelectionRects(0, text.length + 1) ); assert_throws_dom("IndexSizeError", () => tm.getSelectionRects(text.length + 1, text.length + 1) ); } - name: 2d.text.measure.getActualBoundingBox.tentative desc: >- Test TextMetrics::getActualBoundingBox(), with text align {{ text_align }} , and {{ letter_spacing }} letter spacing. test_type: promise fonts: - CanvasTest size: [800, 200] code: | // Use measureText to create a rect for the whole text function getFullTextBoundingBox(text) { const tm = ctx.measureText(text); return { x: -tm.actualBoundingBoxLeft, y: -tm.actualBoundingBoxAscent, width: tm.actualBoundingBoxLeft + tm.actualBoundingBoxRight, height: tm.actualBoundingBoxAscent + tm.actualBoundingBoxDescent }; } // Returns a string a replaces the characters in text that are not in the // range [start, end) with spaces. function buildTestString(text, start, end) { let ret = ''; for (let i = 0; i < text.length; ++i) { if (start <= i && i < end) ret += text[i]; else ret += ' '; } return ret; } function checkRectsMatch(rect_a, rect_b, text, start, end) { assert_approx_equals(rect_a.x, rect_b.x, 1.0, `${text} :: x :: ${start} to ${end}`); assert_approx_equals(rect_a.y, rect_b.y, 1.0, `${text} :: y :: ${start} to ${end}`); assert_approx_equals(rect_a.width, rect_b.width, 1.0, `${text} :: width :: ${start} to ${end}`); assert_approx_equals(rect_a.height, rect_b.height, 1.0, `${text} :: height :: ${start} to ${end}`); } function testForSubstring(text, start, end) { const textMetrics = ctx.measureText(text); const rect_from_api = textMetrics.getActualBoundingBox(start, end); const rect_from_full_bounds = getFullTextBoundingBox( buildTestString(text, start, end)); checkRectsMatch(rect_from_api, rect_from_full_bounds, buildTestString(text, start, end), start, end); } {{ load_font }} ctx.textAlign = '{{ text_align }}'; ctx.letterSpacing = '{{ letter_spacing }}'; const kAligns = [ 'left', 'center', 'right', ]; const kBaselines = [ 'top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom', ]; ctx.font = '50px CanvasTest'; const text = 'ABCDE'; for (const align of kAligns) { for (const baseline of kBaselines) { ctx.textAlign = align; ctx.textBaseline = baseline; // Full string. testForSubstring(text, 0, text.length); // Intermediate string. testForSubstring(text, 1, text.length - 1); // First character. testForSubstring(text, 0, 1); // Intermediate character. testForSubstring(text, 2, 3); } } variants_layout: [multi_files, single_file, single_file] variants: - *load-font-variant-definition - align-left: text_align: left align-center: text_align: center align-right: text_align: right - no-spacing: letter_spacing: 0px spacing: letter_spacing: 10px - name: 2d.text.measure.getActualBoundingBox-full-text.tentative desc: >- Test TextMetrics::getActualBoundingBox() for the full length of the string for some edge cases, with direction {{ text_direction }} and {{ variant_names[1] }} size: [800, 200] code: | // Use measureText to create a rect for the whole text function getFullTextBoundingBox(text) { const tm = ctx.measureText(text); return { x: -tm.actualBoundingBoxLeft, y: -tm.actualBoundingBoxAscent, width: tm.actualBoundingBoxLeft + tm.actualBoundingBoxRight, height: tm.actualBoundingBoxAscent + tm.actualBoundingBoxDescent }; } function checkRectsMatch(rect_a, rect_b) { assert_approx_equals(rect_a.x, rect_b.x, 1.0); assert_approx_equals(rect_a.y, rect_b.y, 1.0); assert_approx_equals(rect_a.width, rect_b.width, 1.0); assert_approx_equals(rect_a.height, rect_b.height, 1.0); } {% if use_directional_override %} function addDirectionalOverrideCharacters(text, direction_is_ltr) { // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en const LTR_OVERRIDE = '\u202D'; const RTL_OVERRIDE = '\u202E'; const OVERRIDE_END = '\u202C'; if (direction_is_ltr) { return LTR_OVERRIDE + text + OVERRIDE_END; } return RTL_OVERRIDE + text + OVERRIDE_END; } {% endif %} const kAligns = [ 'left', 'center', 'right', ]; const kBaselines = [ 'top', 'hanging', 'middle', 'alphabetic', 'ideographic', 'bottom', ]; const kTexts = [ 'UNAVAILABLE', '🏁🎶🏁', ')(あ)(', '-abcd ', 'اين المكتبة؟', 'bidiالرياضيات' ] ctx.font = '50px sans-serif'; ctx.direction = '{{ text_direction }}'; for (const align of kAligns) { for (const baseline of kBaselines) { ctx.textAlign = align; ctx.textBaseline = baseline; for (text of kTexts) { {% if use_directional_override %} text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr'); {% endif %} const tm = ctx.measureText(text); const rect_from_api = tm.getActualBoundingBox(0, text.length); const rect_from_full_bounds = getFullTextBoundingBox(text); checkRectsMatch(rect_from_api, rect_from_full_bounds) } } } variants_layout: [single_file, single_file] variants: - direction-ltr: text_direction: ltr direction-rtl: text_direction: rtl - no-directional-override: use_directional_override: false directional-override: use_directional_override: true - name: 2d.text.measure.getActualBoundingBox-exceptions.tentative desc: >- Check that TextMetrics::getActualBoundingBox() throws when using invalid indexes. code: | const kTexts = [ 'UNAVAILABLE', '🏁🎶🏁', ')(あ)(', '-abcd_', 'اين المكتبة؟', 'bidiالرياضيات' ] for (const text of kTexts) { const tm = ctx.measureText(text); // Handled by the IDL binding. assert_throws_js(TypeError, () => tm.getActualBoundingBox(-1, 0) ); assert_throws_js(TypeError, () => tm.getActualBoundingBox(0, -1) ); assert_throws_js(TypeError, () => tm.getActualBoundingBox(-1, -1) ); // Thrown in TextMetrics. assert_throws_dom("IndexSizeError", () => tm.getActualBoundingBox(text.length, 0) ); assert_throws_dom("IndexSizeError", () => tm.getActualBoundingBox(0, text.length + 1) ); assert_throws_dom("IndexSizeError", () => tm.getActualBoundingBox(text.length, text.length + 1) ); } - name: 2d.text.measure.index-from-offset.tentative desc: >- Check that TextMetrics::getIndexFromOffset() matches its DOM equivalent, where possible, with direction {{ text_direction }}, text align {{ text_align }}, {{ letter_spacing }} letter spacing and {{ variant_names[3] }}. canvas_types: ['HtmlCanvas', 'OffscreenCanvas'] code: | function alignOffset(offset, width) { if ('{{ text_align }}' == 'center') { offset -= width / 2; } else if ('{{ text_align }}' == 'right' || ('{{ text_direction }}' == 'ltr' && '{{ text_align }}' == 'end') || ('{{ text_direction }}' == 'rtl' && '{{ text_align }}' == 'start')) { offset -= width; } return offset; } {% if use_directional_override %} function addDirectionalOverrideCharacters(text, direction_is_ltr) { // source: www.w3.org/International/questions/qa-bidi-unicode-controls.en const LTR_OVERRIDE = '\u202D'; const RTL_OVERRIDE = '\u202E'; const OVERRIDE_END = '\u202C'; if (direction_is_ltr) { return LTR_OVERRIDE + text + OVERRIDE_END; } return RTL_OVERRIDE + text + OVERRIDE_END; } {% endif %} function placeAndMeasureTextInDOM(text, text_width, point) { const el = document.createElement("p"); el.innerHTML = text; el.style.font = '50px sans-serif'; el.style.direction = '{{ text_direction }}'; el.style.letterSpacing = '{{ letter_spacing }}'; // Put the text top left to make offsets simpler. el.style.padding = '0px'; el.style.margin = '0px'; el.style.position = 'absolute'; el.style.x = '0px'; el.style.y = '0px'; document.body.appendChild(el); text_bound = el.getBoundingClientRect(); text_x = text_bound.x; text_y = text_bound.y + text_bound.height / 2; // Offset to the requested point determined by textAlign and direction. let text_align_dx = 0; if ('{{ text_align }}' == 'center') { text_align_dx = text_width / 2; } else if ('{{ text_align }}' == 'right' || ('{{ text_direction }}' == 'ltr' && '{{ text_align }}' == 'end') || ('{{ text_direction }}' == 'rtl' && '{{ text_align }}' == 'start')) { text_align_dx = text_width; } position = document.caretPositionFromPoint(point + text_align_dx + text_x, text_y); @assert position.offsetNode.parentNode === el; document.body.removeChild(el); return position.offset; } ctx.font = '50px sans-serif'; ctx.direction = '{{ text_direction }}'; ctx.textAlign = '{{ text_align }}'; ctx.letterSpacing = '{{ letter_spacing }}'; const kTexts = [ 'UNAVAILABLE', '🏁🎶🏁', ')(あ)(', ')(ああ)(', 'ああ)(ああ', '--abcd__' ] for (text of kTexts) { {% if use_directional_override %} text = addDirectionalOverrideCharacters(text, ctx.direction == 'ltr'); {% endif %} const tm = ctx.measureText(text); text_width = tm.width; step = 30; if ('{{letter_spacing}}' == '10px') { step = 40; } offset = step; adjusted_offset = alignOffset(offset, text_width); tm_position = tm.getIndexFromOffset(adjusted_offset); doc_position = placeAndMeasureTextInDOM(text, text_width, adjusted_offset); assert_equals(tm_position, doc_position, "for " + text + " offset " + offset); offset = text_width / 2 - 10; adjusted_offset = alignOffset(offset, text_width); tm_position = tm.getIndexFromOffset(adjusted_offset); doc_position = placeAndMeasureTextInDOM(text, text_width, adjusted_offset); assert_equals(tm_position, doc_position, "for " + text + " offset " + offset); offset = text_width / 2 + 10; adjusted_offset = alignOffset(offset, text_width); tm_position = tm.getIndexFromOffset(adjusted_offset); doc_position = placeAndMeasureTextInDOM(text, text_width, adjusted_offset); assert_equals(tm_position, doc_position, "for " + text + " offset " + offset); offset = text_width - step; adjusted_offset = alignOffset(offset, text_width); tm_position = tm.getIndexFromOffset(adjusted_offset); doc_position = placeAndMeasureTextInDOM(text, text_width, adjusted_offset); assert_equals(tm_position, doc_position, "for " + text + " offset " + offset); } variants_layout: [single_file, single_file, single_file, single_file] variants: - direction-ltr: text_direction: ltr direction-rtl: text_direction: rtl - align-left: text_align: left align-center: text_align: center align-right: text_align: right align-start: text_align: start align-end: text_align: end - no-spacing: letter_spacing: 0px spacing: letter_spacing: 10px - no-directional-override: use_directional_override: false directional-override: use_directional_override: true - name: 2d.text.measure.index-from-offset-edges.tentative desc: >- Check that TextMetrics::getIndexFromOffset() gives correct edges when the requested point is outside the range, with direction {{ text_direction }} and text align {{ text_align }}. code: | function computeExpected(text, text_width, offset) { expected_position = 0; if ('{{ text_align }}' == 'center' && offset == 0) { return text.length / 2; } if ('{{ text_direction }}' == 'ltr') { if (offset >= text_width) { return text.length; } if (offset <= -text_width) { return 0; } // offset must be 0. if ('{{ text_align }}' == 'start' || '{{ text_align }}' == 'left') { return 0; } else { return text.length; } } else { if (offset >= text_width) { return 0; } if (offset <= -text_width) { return text.length; } // offset must be 0. if ('{{ text_align }}' == 'start' || '{{ text_align }}' == 'right') { return 0; } else { return text.length; } } return expected_position; } ctx.font = '50px sans-serif'; ctx.direction = '{{ text_direction }}'; ctx.textAlign = '{{ text_align }}'; ctx.letterSpacing = '{{ letter_spacing }}'; // The leading and trailing '-' cause the string to always follow // the specified direction, even though the interior will always be ltr. const text = '-0123456789-'; // Points are multiples of the string width as reported by // textMetrics.width. const kPoints = [ -2, -1, 0, 1, 2 ] const tm = ctx.measureText(text); text_width = tm.width; for (const multiple of kPoints) { offset = multiple * text_width; tm_position = tm.getIndexFromOffset(offset); expected_position = computeExpected(text, text_width, offset); assert_equals(tm_position, expected_position, "for " + text + " multiple " + multiple); } variants_layout: [single_file, single_file] variants: - direction-ltr: text_direction: ltr direction-rtl: text_direction: rtl - align-left: text_align: left align-center: text_align: center align-right: text_align: right align-start: text_align: start align-end: text_align: end - name: 2d.text.measure.index-from-offset-edge-cases.tentative desc: >- Test the edge cases for getIndexFromOffset, where the point is at the edge of glyph and at the midpoint. test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.direction = 'ltr'; ctx.align = 'left' ctx.baseline = 'alphabetic' tm = ctx.measureText('A'); const a_width = tm.width; tm = ctx.measureText('B'); const b_width = tm.width; tm = ctx.measureText('C'); const c_width = tm.width; const epsilon = 1.0e-4; tm = ctx.measureText('ABC'); @assert tm.getIndexFromOffset(0) == 0; @assert tm.getIndexFromOffset(a_width / 2) == 0; @assert tm.getIndexFromOffset(a_width / 2 + 1) == 1; @assert tm.getIndexFromOffset(a_width) == 1; @assert tm.getIndexFromOffset(a_width + b_width / 2) == 1; @assert tm.getIndexFromOffset(a_width + b_width / 2 + 1) == 2; @assert tm.getIndexFromOffset(a_width + b_width) == 2; @assert tm.getIndexFromOffset(a_width + b_width + c_width / 2) == 2; @assert tm.getIndexFromOffset(a_width + b_width + c_width / 2 + 1) == 3; @assert tm.getIndexFromOffset(a_width + b_width + c_width) == 3; variants: - *load-font-variant-definition - name: 2d.text.measure.text-clusters-split.tentative desc: >- Test that getTextClusters() splits the input correctly into the minimal clusters, keeping emojis together. size: [400, 200] code: | function getClusterIndexes(text) { const clusters = ctx.measureText(text).getTextClusters(); const result = []; for (let i = 0; i < clusters.length; i++) { const end = clusters[i].end; if (end === (i + 1 < clusters.length ? clusters[i + 1].start : text.length)) { result.push(clusters[i].start); } else { result.push([clusters[i].start, clusters[i].end]); } } return result; } function assertClusterIndexes(text, expected) { const actual = getClusterIndexes(text); assert_array_equals(actual, expected); } ctx.font = '50px serif'; const text = 'ABC ☺️❤️'; ctx.fillText(text, 20, 100); assertClusterIndexes(text, [0, 1, 2, 3, 4, 6]); // [UAX#29]: https://unicode.org/reports/tr29/ // [UAX#29] GB9: × (Extend | ZWJ) assertClusterIndexes('X\u200DY', [0, 2]); // [UAX#29] GB11: \p{Extended_Pictographic} Extend* ZWJ × \p{Extended_Pictographic} assertClusterIndexes('\u{1FFFD}\u200D\u{1FFFD}', [0]); - name: 2d.text.measure.text-clusters-exceptions.tentative desc: >- Check that TextMetrics::getTextClusters() throws when using invalid indexes. code: | const kTexts = [ 'UNAVAILABLE', '🏁🎶🏁', ')(あ)(', '-abcd_' ] for (const text of kTexts) { const tm = ctx.measureText(text); // Handled by the IDL binding. assert_throws_js(TypeError, () => tm.getTextClusters(-1, 0) ); assert_throws_js(TypeError, () => tm.getTextClusters(0, -1) ); assert_throws_js(TypeError, () => tm.getTextClusters(-1, -1) ); // Thrown in TextMetrics. assert_throws_dom("IndexSizeError", () => tm.getTextClusters(text.length, 0) ); assert_throws_dom("IndexSizeError", () => tm.getTextClusters(0, text.length + 1) ); assert_throws_dom("IndexSizeError", () => tm.getTextClusters(text.length, text.length + 1) ); } - name: 2d.text.measure.text-clusters-position.tentative desc: >- Test that TextMetrics::getTextClusters() returns clusters that are positioned according to the target align and baseline passed as options. size: [500, 500] test_type: promise fonts: - CanvasTest code: | {{ load_font }} ctx.font = '40px CanvasTest'; const text = 'E'; // Origin for all the measurements is placed at the top left corner. ctx.textAlign = 'left'; ctx.textBaseline = 'top'; let tm = ctx.measureText(text); // X position. @assert Math.abs(tm.getTextClusters({align: 'left'})[0].x) === 0; @assert tm.getTextClusters({align: 'center'})[0].x === 20; @assert tm.getTextClusters({align: 'right'})[0].x === 40; // Y position. @assert Math.abs(tm.getTextClusters({baseline: 'top'})[0].y) === 0; @assert tm.getTextClusters({baseline: 'middle'})[0].y === 20; @assert tm.getTextClusters({baseline: 'bottom'})[0].y === 40; @assert tm.getTextClusters({baseline: 'alphabetic'})[0].y === 30; variants: - *load-font-variant-definition - name: 2d.text.measure.fillTextCluster-align.tentative desc: >- Test that fillTextCluster() correctly positions the text, taking into account the textAlign from the context at the time the text was measured. size: [250, 43] code: | ctx.font = '20px serif'; const text = 'Test ☺️ א'; const x = canvas.width / 2; const y = canvas.height / 2; ctx.textAlign = '{{ ctx_align }}'; let tm = ctx.measureText(text); const clusters = tm.getTextClusters(); // Rendering all clusters with the same (x, y) parameters must be // equivalent to a fillText() call at (x, y). for (const cluster of clusters) { ctx.fillTextCluster(cluster, x, y); } reference: | ctx.font = '20px serif'; const text = 'Test ☺️ א'; const x = canvas.width / 2; const y = canvas.height / 2; ctx.textAlign = '{{ ctx_align }}'; // Rendering all clusters with the same (x, y) parameters must be // equivalent to a fillText() call at (x, y). ctx.fillText(text, x, y); variants_layout: [single_file] variants: - ctx_align_left: ctx_align: left ctx_align_center: ctx_align: center ctx_align_right: ctx_align: right - name: 2d.text.measure.strokeTextCluster-align.tentative desc: >- Test that strokeTextCluster() correctly positions the text, taking into account the textAlign from the context at the time the text was measured. size: [250, 43] code: | ctx.font = '20px serif'; const text = 'Test ☺️ א'; const x = canvas.width / 2; const y = canvas.height / 2; ctx.textAlign = '{{ ctx_align }}'; let tm = ctx.measureText(text); const clusters = tm.getTextClusters(); // Rendering all clusters with the same (x, y) parameters must be // equivalent to a strokeText() call at (x, y). for (const cluster of clusters) { ctx.strokeTextCluster(cluster, x, y); } reference: | ctx.font = '20px serif'; const text = 'Test ☺️ א'; const x = canvas.width / 2; const y = canvas.height / 2; ctx.textAlign = '{{ ctx_align }}'; // Rendering all clusters with the same (x, y) parameters must be // equivalent to a strokeText() call at (x, y). ctx.strokeText(text, x, y); variants_layout: [single_file] variants: - ctx_align_left: ctx_align: left ctx_align_center: ctx_align: center ctx_align_right: ctx_align: right - name: 2d.text.measure.fillTextCluster-baseline.tentative desc: >- Test that fillTextCluster() correctly positions the text, taking into account the textBaseline from the context at the time the text was measured. size: [180, 43] code: | ctx.font = '20px serif'; const text = 'Test ☺️ א'; const x = 20; const y = canvas.height / 2; ctx.textBaseline = '{{ ctx_baseline }}'; let tm = ctx.measureText(text); const clusters = tm.getTextClusters(); // Rendering all clusters with the same (x, y) parameters must be // equivalent to a fillText() call at (x, y). for (const cluster of clusters) { ctx.fillTextCluster(cluster, x, y); } reference: | ctx.font = '20px serif'; const text = 'Test ☺️ א'; const x = 20; const y = canvas.height / 2; ctx.textBaseline = '{{ ctx_baseline }}'; ctx.fillText(text, x, y); variants_layout: [single_file] variants: - ctx_baseline_top: ctx_baseline: top ctx_baseline_middle: ctx_baseline: middle ctx_baseline_bottom: ctx_baseline: bottom ctx_baseline_alphabetic: ctx_baseline: alphabetic - name: 2d.text.measure.strokeTextCluster-baseline.tentative desc: >- Test that strokeTextCluster() correctly positions the text, taking into account the textBaseline from the context at the time the text was measured. size: [180, 43] code: | ctx.font = '20px serif'; const text = 'Test ☺️ א'; const x = 20; const y = canvas.height / 2; ctx.textBaseline = '{{ ctx_baseline }}'; let tm = ctx.measureText(text); const clusters = tm.getTextClusters(); // Rendering all clusters with the same (x, y) parameters must be // equivalent to a strokeText() call at (x, y). for (const cluster of clusters) { ctx.strokeTextCluster(cluster, x, y); } reference: | ctx.font = '20px serif'; const text = 'Test ☺️ א'; const x = 20; const y = canvas.height / 2; ctx.textBaseline = '{{ ctx_baseline }}'; ctx.strokeText(text, x, y); variants_layout: [single_file] variants: - ctx_baseline_top: ctx_baseline: top ctx_baseline_middle: ctx_baseline: middle ctx_baseline_bottom: ctx_baseline: bottom ctx_baseline_alphabetic: ctx_baseline: alphabetic - name: 2d.text.measure.fillTextCluster-font-change.tentative desc: >- Test that fillTextCluster() renders in the font used originally when the text was measured, even if the font set on the context has changed since. size: [500, 200] code: | ctx.font = '50px sans-serif'; const text = 'Hello ♦️ World!'; let tm = ctx.measureText(text); const clusters = tm.getTextClusters(); ctx.font = '80px serif'; const x = 100; const y = 100; for (const cluster of clusters) { ctx.fillTextCluster(cluster, x, y); } reference: | ctx.font = '50px sans-serif'; const text = 'Hello ♦️ World!'; let tm = ctx.measureText(text); const clusters = tm.getTextClusters(); const x = 100; const y = 100; ctx.fillText(text, x, y); - name: 2d.text.measure.strokeTextCluster-font-change.tentative desc: >- Test that strokeTextCluster() renders in the font used originally when the text was measured, even if the font set on the context has changed since. size: [500, 200] code: | ctx.font = '50px sans-serif'; const text = 'Hello ♦️ World!'; let tm = ctx.measureText(text); const clusters = tm.getTextClusters(); ctx.font = '80px serif'; const x = 100; const y = 100; for (const cluster of clusters) { ctx.strokeTextCluster(cluster, x, y); } reference: | ctx.font = '50px sans-serif'; const text = 'Hello ♦️ World!'; let tm = ctx.measureText(text); const clusters = tm.getTextClusters(); const x = 100; const y = 100; ctx.strokeText(text, x, y); - name: 2d.text.measure.fillTextCluster-drawing-styles-change.tentative desc: >- Test that fillTextCluster() renders using the drawing styles as they were when `ctx.measureText()` was called, regardless of any changes in the context since. size: [250, 80] code: | ctx.font = '20px serif'; const text = 'Test ♦️ find'; {{ original_value }} let tm = ctx.measureText(text); const clusters = tm.getTextClusters(); {{ modified_value }} for (const cluster of clusters) { ctx.fillTextCluster(cluster, 10, 25); } ctx.fillText(text, 10, 50); reference: | ctx.font = '20px serif'; const text = 'Test ♦️ find'; {{ original_value }} ctx.fillText(text, 10, 25); {{ modified_value }} ctx.fillText(text, 10, 50); variants_layout: [single_file] grid_width: 2 variants: - letter_spacing: original_value: |- ctx.letterSpacing = '2px'; modified_value: |- ctx.letterSpacing = '6px'; word_spacing: original_value: |- ctx.wordSpacing = '2px'; modified_value: |- ctx.wordSpacing = '10px'; font_kerning: original_value: |- ctx.fontKerning = 'none'; modified_value: |- ctx.fontKerning = 'normal'; font_variant_caps: original_value: |- ctx.fontVariantCaps = 'small-caps'; modified_value: |- ctx.fontVariantCaps = 'all-small-caps'; - name: 2d.text.measure.strokeTextCluster-drawing-styles-change.tentative desc: >- Test that strokeTextCluster() renders using the drawing styles as they were when `ctx.measureText()` was called, regardless of any changes in the context since. size: [250, 80] code: | ctx.font = '20px serif'; const text = 'Test ♦️ find'; {{ original_value }} let tm = ctx.measureText(text); const clusters = tm.getTextClusters(); {{ modified_value }} for (const cluster of clusters) { ctx.strokeTextCluster(cluster, 10, 25); } ctx.strokeText(text, 10, 50); reference: | ctx.font = '20px serif'; const text = 'Test ♦️ find'; {{ original_value }} ctx.strokeText(text, 10, 25); {{ modified_value }} ctx.strokeText(text, 10, 50); variants_layout: [single_file] grid_width: 2 variants: - letter_spacing: original_value: |- ctx.letterSpacing = '2px'; modified_value: |- ctx.letterSpacing = '6px'; word_spacing: original_value: |- ctx.wordSpacing = '2px'; modified_value: |- ctx.wordSpacing = '10px'; font_kerning: original_value: |- ctx.fontKerning = 'none'; modified_value: |- ctx.fontKerning = 'normal'; font_variant_caps: original_value: |- ctx.fontVariantCaps = 'small-caps'; modified_value: |- ctx.fontVariantCaps = 'all-small-caps'; - name: 2d.text.measure.fillTextCluster-range.tentative desc: >- Test that getTextClusters() and fillTextCluster() correctly render different ranges of the input text. test_type: promise fonts: - CanvasTest size: [400, 300] code: | // Renders all the clusters in the list from position (x, y). function renderClusters(clusters, x, y) { for (const cluster of clusters) { ctx.fillTextCluster(cluster, x, y); } } {{ load_font }} ctx.font = '50px CanvasTest'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; const text = 'EEEEE'; let tm = ctx.measureText(text); // Background color. ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#0f0'; // Without the first character. renderClusters(tm.getTextClusters(1, 5), 0, 0); @assert pixel 5,5 ==~ 255,0,0,255; @assert pixel 55,5 ==~ 0,255,0,255; @assert pixel 105,5 ==~ 0,255,0,255; @assert pixel 155,5 ==~ 0,255,0,255; @assert pixel 205,5 ==~ 0,255,0,255; // Without the last character. renderClusters(tm.getTextClusters(0, 4), 0, 100); @assert pixel 5,105 ==~ 0,255,0,255; @assert pixel 55,105 ==~ 0,255,0,255; @assert pixel 105,105 ==~ 0,255,0,255; @assert pixel 155,105 ==~ 0,255,0,255; @assert pixel 205,105 ==~ 255,0,0,255; // Only the middle character. renderClusters(tm.getTextClusters(2, 3), 0, 150); @assert pixel 5,155 ==~ 255,0,0,255; @assert pixel 55,155 ==~ 255,0,0,255; @assert pixel 105,155 ==~ 0,255,0,255; @assert pixel 155,155 ==~ 255,0,0,255; @assert pixel 205,155 ==~ 255,0,0,255; variants: - *load-font-variant-definition - name: 2d.text.measure.strokeTextCluster-range.tentative desc: >- Test that getTextClusters() and strokeTextCluster() correctly render different ranges of the input text. test_type: promise fonts: - CanvasTest size: [400, 300] code: | // Renders all the clusters in the list from position (x, y). function renderClusters(clusters, x, y) { for (const cluster of clusters) { ctx.strokeTextCluster(cluster, x, y); } } {{ load_font }} ctx.font = '50px CanvasTest'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; const text = 'EEEEE'; let tm = ctx.measureText(text); // Background color. ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = '#0f0'; ctx.lineWidth = 12; // Without the first character. renderClusters(tm.getTextClusters(1, 5), 0, 0); @assert pixel 5,5 ==~ 255,0,0,255; @assert pixel 55,5 ==~ 0,255,0,255; @assert pixel 105,5 ==~ 0,255,0,255; @assert pixel 155,5 ==~ 0,255,0,255; @assert pixel 205,5 ==~ 0,255,0,255; // Without the last character. renderClusters(tm.getTextClusters(0, 4), 0, 100); @assert pixel 5,105 ==~ 0,255,0,255; @assert pixel 55,105 ==~ 0,255,0,255; @assert pixel 105,105 ==~ 0,255,0,255; @assert pixel 155,105 ==~ 0,255,0,255; @assert pixel 245,105 ==~ 255,0,0,255; // Only the middle character. renderClusters(tm.getTextClusters(2, 3), 0, 200); @assert pixel 5,205 ==~ 255,0,0,255; @assert pixel 55,205 ==~ 255,0,0,255; @assert pixel 105,205 ==~ 0,255,0,255; @assert pixel 195,205 ==~ 255,0,0,255; @assert pixel 245,205 ==~ 255,0,0,255; variants: - *load-font-variant-definition - name: 2d.text.measure.fillTextCluster-options.tentative desc: >- Test that fillTextCluster() correctly applies the options passed as a dictionary. test_type: promise fonts: - CanvasTest size: [100, 300] code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; const text = 'E'; const tm = ctx.measureText(text); const cluster = tm.getTextClusters()[0]; // Background color. ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = '#0f0'; // Override the align and baseline of the cluster. ctx.fillTextCluster(cluster, 50, 50, {align: 'right', baseline: 'bottom'}); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 45,5 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @assert pixel 45,45 ==~ 0,255,0,255; @assert pixel 55,5 ==~ 255,0,0,255; @assert pixel 5,55 ==~ 255,0,0,255; @assert pixel 55,55 ==~ 255,0,0,255; // Override the x and y values of the cluster. ctx.fillTextCluster(cluster, 0, 100, {x: 10, y: 10}); @assert pixel 15,115 ==~ 0,255,0,255; @assert pixel 55,115 ==~ 0,255,0,255; @assert pixel 15,155 ==~ 0,255,0,255; @assert pixel 55,155 ==~ 0,255,0,255; @assert pixel 65,115 ==~ 255,0,0,255; @assert pixel 15,165 ==~ 255,0,0,255; @assert pixel 65,165 ==~ 255,0,0,255; // Override the align, baseline, x, and y values of the cluster. ctx.fillTextCluster(cluster, 50, 250, {align: 'right', baseline: 'bottom', x: 10, y: 10}); @assert pixel 15,215 ==~ 0,255,0,255; @assert pixel 55,215 ==~ 0,255,0,255; @assert pixel 15,255 ==~ 0,255,0,255; @assert pixel 55,255 ==~ 0,255,0,255; @assert pixel 65,215 ==~ 255,0,0,255; @assert pixel 15,265 ==~ 255,0,0,255; @assert pixel 65,265 ==~ 255,0,0,255; variants: - *load-font-variant-definition - name: 2d.text.measure.strokeTextCluster-options.tentative desc: >- Test that strokeTextCluster() correctly applies the options passed as a dictionary. test_type: promise fonts: - CanvasTest size: [100, 300] code: | {{ load_font }} ctx.font = '50px CanvasTest'; ctx.textAlign = 'left'; ctx.textBaseline = 'top'; const text = 'E'; const tm = ctx.measureText(text); const cluster = tm.getTextClusters()[0]; // Background color. ctx.fillStyle = '#f00'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = '#0f0'; ctx.lineWidth = 12; // Override the align and baseline of the cluster. ctx.strokeTextCluster(cluster, 50, 50, {align: 'right', baseline: 'bottom'}); @assert pixel 5,5 ==~ 0,255,0,255; @assert pixel 45,5 ==~ 0,255,0,255; @assert pixel 5,45 ==~ 0,255,0,255; @assert pixel 45,45 ==~ 0,255,0,255; @assert pixel 5,95 ==~ 255,0,0,255; @assert pixel 50,95 ==~ 255,0,0,255; @assert pixel 95,50 ==~ 255,0,0,255; @assert pixel 95,5 ==~ 255,0,0,255; // Override the x and y values of the cluster. ctx.strokeTextCluster(cluster, 0, 100, {x: 10, y: 10}); @assert pixel 15,115 ==~ 0,255,0,255; @assert pixel 55,115 ==~ 0,255,0,255; @assert pixel 15,155 ==~ 0,255,0,255; @assert pixel 55,155 ==~ 0,255,0,255; @assert pixel 1,101 ==~ 255,0,0,255; @assert pixel 1,151 ==~ 255,0,0,255; @assert pixel 51,101 ==~ 255,0,0,255; // Override the align, baseline, x, and y values of the cluster. ctx.strokeTextCluster(cluster, 50, 250, {align: 'right', baseline: 'bottom', x: 10, y: 10}); @assert pixel 15,215 ==~ 0,255,0,255; @assert pixel 55,215 ==~ 0,255,0,255; @assert pixel 15,255 ==~ 0,255,0,255; @assert pixel 55,255 ==~ 0,255,0,255; @assert pixel 5,295 ==~ 255,0,0,255; @assert pixel 50,295 ==~ 255,0,0,255; @assert pixel 95,250 ==~ 255,0,0,255; @assert pixel 95,25 ==~ 255,0,0,255; variants: - *load-font-variant-definition - name: 2d.text.drawing.style.absolute.spacing desc: Testing letter spacing and word spacing with absolute length code: | @assert ctx.letterSpacing === '0px'; @assert ctx.wordSpacing === '0px'; ctx.letterSpacing = '3px'; @assert ctx.letterSpacing === '3px'; @assert ctx.wordSpacing === '0px'; ctx.wordSpacing = '5px'; @assert ctx.letterSpacing === '3px'; @assert ctx.wordSpacing === '5px'; ctx.letterSpacing = '-1px'; ctx.wordSpacing = '-1px'; @assert ctx.letterSpacing === '-1px'; @assert ctx.wordSpacing === '-1px'; ctx.letterSpacing = '1PX'; ctx.wordSpacing = '10PX'; @assert ctx.letterSpacing === '1px'; @assert ctx.wordSpacing === '10px'; - name: 2d.text.measure.lang desc: Testing the lang attribute test_type: promise fonts: - Lato-Medium code: | {{ load_font }} ctx.font = '50px Lato-Medium'; ctx.lang = 'tr'; const text = 'fi'; const tm_tr = ctx.measureText(text); const tr_width = tm_tr.width; ctx.lang = 'en'; const tm_en = ctx.measureText(text); const en_width = tm_en.width; @assert tr_width > en_width; variants: - *load-font-variant-definition - name: 2d.text.measure.lang.inherit desc: Testing the lang attribute test_type: promise canvas_types: ['HtmlCanvas', 'OffscreenCanvas'] fonts: - Lato-Medium canvas: 'lang="tr"' code: | {{ load_font }} ctx.font = '50px Lato-Medium'; ctx.lang = 'inherit'; const text = 'fi'; const tm_tr = ctx.measureText(text); const tr_width = tm_tr.width; ctx.lang = 'en'; const tm_en = ctx.measureText(text); const en_width = tm_en.width; @assert tr_width > en_width; variants: - &load-font-variant-definitio-and-set-lang HtmlCanvas: append_variants_to_name: false canvas_types: ['HtmlCanvas'] load_font: |- await document.fonts.ready; OffscreenCanvas: append_variants_to_name: false canvas_types: ['OffscreenCanvas'] load_font: |- document.documentElement.setAttribute('lang','tr'); var f = new FontFace("{{ fonts[0] }}", "url('/fonts/{{ fonts[0] }}.ttf')"); f.load(); {% set root = 'self' if canvas_type == 'Worker' else 'document' %} {{ root }}.fonts.add(f); await {{ root }}.fonts.ready; - name: 2d.text.drawing.style.font-relative.spacing desc: Testing letter spacing and word spacing with font-relative length code: | @assert ctx.letterSpacing === '0px'; @assert ctx.wordSpacing === '0px'; ctx.letterSpacing = '1EX'; ctx.wordSpacing = '1EM'; @assert ctx.letterSpacing === '1ex'; @assert ctx.wordSpacing === '1em'; ctx.letterSpacing = '1ch'; ctx.wordSpacing = '1ic'; @assert ctx.letterSpacing === '1ch'; @assert ctx.wordSpacing === '1ic'; - name: 2d.text.drawing.style.nonfinite.spacing desc: Testing letter spacing and word spacing with nonfinite inputs code: | @assert ctx.letterSpacing === '0px'; @assert ctx.wordSpacing === '0px'; function test_word_spacing(value) { ctx.wordSpacing = value; ctx.letterSpacing = value; @assert ctx.wordSpacing === '0px'; @assert ctx.letterSpacing === '0px'; } @nonfinite test_word_spacing(<0 NaN Infinity -Infinity>); - name: 2d.text.drawing.style.invalid.spacing desc: Testing letter spacing and word spacing with invalid units code: | @assert ctx.letterSpacing === '0px'; @assert ctx.wordSpacing === '0px'; function test_word_spacing(value) { ctx.wordSpacing = value; ctx.letterSpacing = value; @assert ctx.wordSpacing === '0px'; @assert ctx.letterSpacing === '0px'; } @nonfinite test_word_spacing(< '0s' '1min' '1deg' '1pp' 'initial' 'inherit' 'normal' 'none'>); - name: 2d.text.drawing.style.letterSpacing.measure desc: Testing letter spacing with different length units code: | @assert ctx.letterSpacing === '0px'; @assert ctx.wordSpacing === '0px'; var width_normal = ctx.measureText('Hello World').width; function test_letter_spacing(value, difference_spacing, epsilon) { ctx.letterSpacing = value; @assert ctx.letterSpacing === value; @assert ctx.wordSpacing === '0px'; width_with_letter_spacing = ctx.measureText('Hello World').width; assert_approx_equals(width_with_letter_spacing, width_normal + difference_spacing, epsilon, "letter spacing doesn't work."); } // The first value is the letter Spacing to be set, the second value the // change in length of string 'Hello World', note that there are 11 letters // in 'hello world', so the length difference is always letterSpacing * 11. // and the third value is the acceptable differencee for the length change, // note that unit such as 1cm/1mm doesn't map to an exact pixel value. test_cases = [['3px', 33, 0.1], ['5px', 55, 0.1], ['-2px', -22, 0.1], ['1em', 110, 0.1], ['-0.1em', -11, 0.1], ['1in', 1056, 0.1], ['-0.1cm', -41.65, 0.2], ['-0.6mm', -24.95, 0.2]] for (const test_case of test_cases) { test_letter_spacing(test_case[0], test_case[1], test_case[2]); } - name: 2d.text.drawing.style.wordSpacing.measure desc: Testing word spacing with different length units code: | @assert ctx.letterSpacing === '0px'; @assert ctx.wordSpacing === '0px'; var width_normal = ctx.measureText('Hello World, again').width; function test_word_spacing(value, difference_spacing, epsilon) { ctx.wordSpacing = value; @assert ctx.letterSpacing === '0px'; @assert ctx.wordSpacing === value; width_with_word_spacing = ctx.measureText('Hello World, again').width; assert_approx_equals(width_with_word_spacing, width_normal + difference_spacing, epsilon, "word spacing doesn't work."); } // The first value is the word Spacing to be set, the second value the // change in length of string 'Hello World', note that there are 2 words // in 'Hello World, again', so the length difference is always wordSpacing * 2. // and the third value is the acceptable differencee for the length change, // note that unit such as 1cm/1mm doesn't map to an exact pixel value. test_cases = [['3px', 6, 0.1], ['5px', 10, 0.1], ['-2px', -4, 0.1], ['1em', 20, 0.1], ['-0.5em', -10, 0.1], ['1in', 192, 0.1], ['-0.1cm', -7.57, 0.2], ['-0.6mm', -4.54, 0.2]] for (const test_case of test_cases) { test_word_spacing(test_case[0], test_case[1], test_case[2]); } - name: 2d.text.drawing.style.letterSpacing.change.font desc: Set letter spacing and word spacing to font dependent value and verify it works after font change. code: | @assert ctx.letterSpacing === '0px'; @assert ctx.wordSpacing === '0px'; // Get the width for 'Hello World' at default size, 10px. var width_normal = ctx.measureText('Hello World').width; ctx.letterSpacing = '1em'; @assert ctx.letterSpacing === '1em'; // 1em = 10px. Add 10px after each letter in "Hello World", // makes it 110px longer. var width_with_spacing = ctx.measureText('Hello World').width; assert_approx_equals(width_with_spacing, width_normal + 110, 0.1, "letter-spacing error"); // Changing font to 20px. Without resetting the spacing, 1em letterSpacing // is now 20px, so it's suppose to be 220px longer without any letterSpacing set. ctx.font = '20px serif'; width_with_spacing = ctx.measureText('Hello World').width; // Now calculate the reference spacing for "Hello World" with no spacing. ctx.letterSpacing = '0em'; width_normal = ctx.measureText('Hello World').width; assert_approx_equals(width_with_spacing, width_normal + 220, 0.1, "letter-spacing error after font change"); - name: 2d.text.drawing.style.wordSpacing.change.font desc: Set word spacing and word spacing to font dependent value and verify it works after font change. code: | @assert ctx.letterSpacing === '0px'; @assert ctx.wordSpacing === '0px'; // Get the width for 'Hello World, again' at default size, 10px. var width_normal = ctx.measureText('Hello World, again').width; ctx.wordSpacing = '1em'; @assert ctx.wordSpacing === '1em'; // 1em = 10px. Add 10px after each word in "Hello World, again", // makes it 20px longer. var width_with_spacing = ctx.measureText('Hello World, again').width; @assert width_with_spacing === width_normal + 20; // Changing font to 20px. Without resetting the spacing, 1em wordSpacing // is now 20px, so it's suppose to be 40px longer without any wordSpacing set. ctx.font = '20px serif'; width_with_spacing = ctx.measureText('Hello World, again').width; // Now calculate the reference spacing for "Hello World, again" with no spacing. ctx.wordSpacing = '0em'; width_normal = ctx.measureText('Hello World, again').width; @assert width_with_spacing === width_normal + 40; - name: 2d.text.drawing.style.fontKerning desc: Testing basic functionalities of fontKerning for canvas code: | @assert ctx.fontKerning === "auto"; ctx.fontKerning = "normal"; @assert ctx.fontKerning === "normal"; width_normal = ctx.measureText("TAWATAVA").width; ctx.fontKerning = "none"; @assert ctx.fontKerning === "none"; width_none = ctx.measureText("TAWATAVA").width; @assert width_normal < width_none; - name: 2d.text.drawing.style.fontKerning.with.uppercase desc: Testing basic functionalities of fontKerning for canvas code: | @assert ctx.fontKerning === "auto"; ctx.fontKerning = "Normal"; @assert ctx.fontKerning === "auto"; ctx.fontKerning = "normal"; @assert ctx.fontKerning === "normal"; ctx.fontKerning = "Auto"; @assert ctx.fontKerning === "normal"; ctx.fontKerning = "auto"; ctx.fontKerning = "noRmal"; @assert ctx.fontKerning === "auto"; ctx.fontKerning = "auto"; ctx.fontKerning = "NoRMal"; @assert ctx.fontKerning === "auto"; ctx.fontKerning = "auto"; ctx.fontKerning = "NORMAL"; @assert ctx.fontKerning === "auto"; ctx.fontKerning = "None"; @assert ctx.fontKerning === "auto"; ctx.fontKerning = "none"; @assert ctx.fontKerning === "none"; ctx.fontKerning = "Auto"; @assert ctx.fontKerning === "none"; ctx.fontKerning = "auto"; ctx.fontKerning = "nOne"; @assert ctx.fontKerning === "auto"; ctx.fontKerning = "auto"; ctx.fontKerning = "nonE"; @assert ctx.fontKerning === "auto"; ctx.fontKerning = "auto"; ctx.fontKerning = "NONE"; @assert ctx.fontKerning === "auto"; - name: 2d.text.drawing.style.fontVariant.settings desc: Testing basic functionalities of fontVariant for canvas code: | // Setting fontVariantCaps with lower cases @assert ctx.fontVariantCaps === "normal"; ctx.fontVariantCaps = "normal"; @assert ctx.fontVariantCaps === "normal"; ctx.fontVariantCaps = "small-caps"; @assert ctx.fontVariantCaps === "small-caps"; ctx.fontVariantCaps = "all-small-caps"; @assert ctx.fontVariantCaps === "all-small-caps"; ctx.fontVariantCaps = "petite-caps"; @assert ctx.fontVariantCaps === "petite-caps"; ctx.fontVariantCaps = "all-petite-caps"; @assert ctx.fontVariantCaps === "all-petite-caps"; ctx.fontVariantCaps = "unicase"; @assert ctx.fontVariantCaps === "unicase"; ctx.fontVariantCaps = "titling-caps"; @assert ctx.fontVariantCaps === "titling-caps"; // Setting fontVariantCaps with mixed-case values is not valid ctx.fontVariantCaps = "nORmal"; @assert ctx.fontVariantCaps === "titling-caps"; ctx.fontVariantCaps = "normal"; @assert ctx.fontVariantCaps === "normal"; ctx.fontVariantCaps = "smaLL-caps"; @assert ctx.fontVariantCaps === "normal"; ctx.fontVariantCaps = "all-small-CAPS"; @assert ctx.fontVariantCaps === "normal"; ctx.fontVariantCaps = "pEtitE-caps"; @assert ctx.fontVariantCaps === "normal"; ctx.fontVariantCaps = "All-Petite-Caps"; @assert ctx.fontVariantCaps === "normal"; ctx.fontVariantCaps = "uNIcase"; @assert ctx.fontVariantCaps === "normal"; ctx.fontVariantCaps = "titling-CAPS"; @assert ctx.fontVariantCaps === "normal"; // Setting fontVariantCaps with non-existing font variant. ctx.fontVariantCaps = "titling-caps"; ctx.fontVariantCaps = "abcd"; @assert ctx.fontVariantCaps === "titling-caps"; - name: 2d.text.drawing.style.textRendering.settings desc: Testing basic functionalities of textRendering in Canvas code: | // Setting textRendering with correct case. @assert ctx.textRendering === "auto"; ctx.textRendering = "optimizeSpeed"; @assert ctx.textRendering === "optimizeSpeed"; ctx.textRendering = "optimizeLegibility"; @assert ctx.textRendering === "optimizeLegibility"; ctx.textRendering = "geometricPrecision"; @assert ctx.textRendering === "geometricPrecision"; ctx.textRendering = "auto"; @assert ctx.textRendering === "auto"; // Setting textRendering with incorrect case is ignored. ctx.textRendering = "OPtimizeSpeed"; @assert ctx.textRendering === "auto"; ctx.textRendering = "OPtimizELEgibility"; @assert ctx.textRendering === "auto"; ctx.textRendering = "GeometricPrecision"; @assert ctx.textRendering === "auto"; ctx.textRendering = "optimizespeed"; @assert ctx.textRendering === "auto"; ctx.textRendering = "optimizelegibility"; @assert ctx.textRendering === "auto"; ctx.textRendering = "geometricprecision"; @assert ctx.textRendering === "auto"; ctx.textRendering = "optimizeLegibility"; @assert ctx.textRendering === "optimizeLegibility"; ctx.textRendering = "AUTO"; @assert ctx.textRendering === "optimizeLegibility"; ctx.textRendering = "Auto"; @assert ctx.textRendering === "optimizeLegibility"; // Setting textRendering with non-existing font variant. ctx.textRendering = "abcd"; @assert ctx.textRendering === "optimizeLegibility"; ctx.textRendering = "normal"; @assert ctx.textRendering === "optimizeLegibility"; ctx.textRendering = ""; @assert ctx.textRendering === "optimizeLegibility"; ctx.textRendering = "auto"; @assert ctx.textRendering === "auto"; - name: 2d.text.drawing.style.reset.TextRendering desc: TextRendering stays the same after font change. code: | ctx.font = '20px serif'; ctx.textRendering = "optimizeSpeed"; @assert ctx.textRendering === "optimizeSpeed"; ctx.font = '10px serif'; @assert ctx.textRendering === "optimizeSpeed"; - name: 2d.text.drawing.style.fontStretch.settings desc: Testing value setting of fontStretch in Canvas code: | // Setting fontStretch with lower cases ctx.fontStretch = "ultra-condensed"; @assert ctx.fontStretch === "ultra-condensed"; ctx.fontStretch = "extra-condensed"; @assert ctx.fontStretch === "extra-condensed"; ctx.fontStretch = "condensed"; @assert ctx.fontStretch === "condensed"; ctx.fontStretch = "semi-condensed"; @assert ctx.fontStretch === "semi-condensed"; ctx.fontStretch = "normal"; @assert ctx.fontStretch === "normal"; ctx.fontStretch = "semi-expanded"; @assert ctx.fontStretch === "semi-expanded"; ctx.fontStretch = "expanded"; @assert ctx.fontStretch === "expanded"; ctx.fontStretch = "extra-expanded"; @assert ctx.fontStretch === "extra-expanded"; ctx.fontStretch = "ultra-expanded"; @assert ctx.fontStretch === "ultra-expanded"; // Setting fontStretch with lower cases and upper cases word, // these values should be ignored. ctx.fontStretch = "ulTra-condensed"; @assert ctx.fontStretch === "ultra-expanded"; ctx.fontStretch = "Extra-condensed"; @assert ctx.fontStretch === "ultra-expanded"; ctx.fontStretch = "cOndensed"; @assert ctx.fontStretch === "ultra-expanded"; ctx.fontStretch = "Semi-Condensed"; @assert ctx.fontStretch === "ultra-expanded"; ctx.fontStretch = "normaL"; @assert ctx.fontStretch === "ultra-expanded"; ctx.fontStretch = "semi-Expanded"; @assert ctx.fontStretch === "ultra-expanded"; ctx.fontStretch = "Expanded"; @assert ctx.fontStretch === "ultra-expanded"; ctx.fontStretch = "eXtra-expanded"; @assert ctx.fontStretch === "ultra-expanded"; ctx.fontStretch = "abcd"; @assert ctx.fontStretch === "ultra-expanded"; - name: 2d.text.fontVariantCaps1 desc: Testing small caps setting in fontVariant code: | ctx.font = "32px serif"; ctx.fontVariantCaps = "small-caps"; // This should render the same as font = "small-caps 32px serif". ctx.fillText("Hello World", 20, 100); reference: | ctx.font = "small-caps 32px serif"; ctx.fillText("Hello World", 20, 100); - name: 2d.text.fontVariantCaps2 desc: Testing small caps setting in fontVariant code: | ctx.font = "small-caps 32px serif"; // "mismatch" test, to verify that small-caps does change the rendering. smallCaps_len = ctx.measureText("Hello World").width; ctx.font = "32px serif"; normalCaps_len = ctx.measureText("Hello World").width; @assert smallCaps_len != normalCaps_len; - name: 2d.text.fontVariantCaps3 desc: Testing small caps setting in fontVariant code: | ctx.font = "32px serif"; ctx.fontVariantCaps = "all-small-caps"; // This should render the same as using font = "small-caps 32px serif" // with all the underlying text in lowercase. ctx.fillText("Hello World", 20, 100); reference: | ctx.font = "small-caps 32px serif"; ctx.fillText("hello world", 20, 100); - name: 2d.text.fontVariantCaps4 desc: Testing small caps setting in fontVariant code: | ctx.font = "small-caps 32px serif"; // fontVariantCaps overrides the small-caps setting from the font attribute // (spec unclear, cf. https://github.com/whatwg/html/issues/8103) ctx.fontVariantCaps = "all-small-caps"; ctx.fillText("Hello World", 20, 100); reference: | ctx.font = "small-caps 32px serif"; ctx.fillText("hello world", 20, 100); - name: 2d.text.fontVariantCaps5 desc: Testing small caps setting in fontVariant code: | ctx.font = "small-caps 32px serif"; // fontVariantCaps 'normal' does not override the setting from the font attribute. // (spec unclear, cf. https://github.com/whatwg/html/issues/8103) ctx.fontVariantCaps = "normal"; ctx.fillText("Hello World", 20, 100); reference: | ctx.font = "small-caps 32px serif"; ctx.fillText("Hello World", 20, 100); - name: 2d.text.fontVariantCaps6 desc: Testing small caps setting in fontVariant code: | // fontVariantCaps is reset when the font attribute is set. // (spec unclear, cf. https://github.com/whatwg/html/issues/8103) ctx.fontVariantCaps = "all-small-caps"; ctx.font = "32px serif"; ctx.fillText("Hello World", 20, 100); reference: | ctx.font = "32px serif"; ctx.fillText("Hello World", 20, 100); - name: 2d.text.fontVariantCaps.after.reset.font desc: Testing if the fontVariantCaps is reset after font change size: [300, 300] code: | ctx.font = "32px serif"; ctx.fontVariantCaps = "small-caps"; ctx.font = "31px serif"; ctx.fillText("Hello World", 20, 40); ctx.fontVariantCaps = "small-caps"; ctx.fillText("Hello World", 20, 80); reference: | ctx.font = "31px serif"; ctx.fillText("Hello World", 20, 40); ctx.fontVariantCaps = "small-caps"; ctx.fillText("Hello World", 20, 80); - name: 2d.text.setFont.mathFont desc: crbug.com/1212190, make sure offscreencanvas doesn't crash with Math Font code: | ctx.font = "math serif"; - name: 2d.text.writingmode desc: writing-mode in css should not change how text is rendered canvas_types: ['HtmlCanvas'] size: [300, 300] code: | canvas.style.textOrientation = "upright"; canvas.style.writingMode = "vertical-rl"; canvas.style.fontFamily = "Arial"; ctx.font = "bold 64px Arial"; ctx.textBaseline = "top"; ctx.fillText("Happy", 100, 0); reference: | ctx.font = "bold 64px Arial"; ctx.textBaseline = "top"; ctx.fillText("Happy", 100, 0); # TODO: shadows, alpha, composite, clip