mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge remote-tracking branch 'origin/master' into lint
This commit is contained in:
@@ -32,9 +32,10 @@ export function acquireTextureAtlas(
|
||||
deviceCharWidth: number,
|
||||
deviceCharHeight: number,
|
||||
devicePixelRatio: number,
|
||||
deviceMaxTextureSize: number
|
||||
deviceMaxTextureSize: number,
|
||||
customGlyphs: boolean = true
|
||||
): ITextureAtlas {
|
||||
const newConfig = generateConfig(deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, options, colors, devicePixelRatio, deviceMaxTextureSize);
|
||||
const newConfig = generateConfig(deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, options, colors, devicePixelRatio, deviceMaxTextureSize, customGlyphs);
|
||||
|
||||
// Check to see if the terminal already owns this config
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ITerminalOptions } from '@xterm/xterm';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { NULL_COLOR } from 'common/Color';
|
||||
|
||||
export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number, deviceMaxTextureSize: number): ICharAtlasConfig {
|
||||
export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number, deviceMaxTextureSize: number, customGlyphs: boolean = true): ICharAtlasConfig {
|
||||
// null out some fields that don't matter
|
||||
const clonedColors: IColorSet = {
|
||||
foreground: colors.foreground,
|
||||
@@ -32,7 +32,7 @@ export function generateConfig(deviceCellWidth: number, deviceCellHeight: number
|
||||
halfContrastCache: colors.halfContrastCache
|
||||
};
|
||||
return {
|
||||
customGlyphs: options.customGlyphs,
|
||||
customGlyphs,
|
||||
devicePixelRatio,
|
||||
deviceMaxTextureSize,
|
||||
letterSpacing: options.letterSpacing,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { ITerminalAddon, Terminal } from '@xterm/xterm';
|
||||
import type { WebglAddon as IWebglApi } from '@xterm/addon-webgl';
|
||||
import type { IWebglAddonOptions, WebglAddon as IWebglApi } from '@xterm/addon-webgl';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
@@ -28,9 +28,10 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
|
||||
private readonly _onContextLoss = this._register(new Emitter<void>());
|
||||
public readonly onContextLoss = this._onContextLoss.event;
|
||||
|
||||
constructor(
|
||||
private _preserveDrawingBuffer?: boolean
|
||||
) {
|
||||
private readonly _customGlyphs: boolean;
|
||||
private readonly _preserveDrawingBuffer?: boolean;
|
||||
|
||||
constructor(options?: IWebglAddonOptions) {
|
||||
if (isSafari && getSafariVersion() < 16) {
|
||||
// Perform an extra check to determine if Webgl2 is manually enabled in developer settings
|
||||
const contextAttributes = {
|
||||
@@ -44,6 +45,8 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
|
||||
}
|
||||
}
|
||||
super();
|
||||
this._customGlyphs = options?.customGlyphs ?? true;
|
||||
this._preserveDrawingBuffer = options?.preserveDrawingBuffer;
|
||||
}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
@@ -79,6 +82,7 @@ export class WebglAddon extends Disposable implements ITerminalAddon , IWebglApi
|
||||
decorationService,
|
||||
optionsService,
|
||||
themeService,
|
||||
this._customGlyphs,
|
||||
this._preserveDrawingBuffer
|
||||
));
|
||||
this._register(Event.forward(this._renderer.onContextLoss, this._onContextLoss));
|
||||
|
||||
@@ -72,6 +72,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private readonly _decorationService: IDecorationService,
|
||||
private readonly _optionsService: IOptionsService,
|
||||
private readonly _themeService: IThemeService,
|
||||
private readonly _customGlyphs: boolean = true,
|
||||
preserveDrawingBuffer?: boolean
|
||||
) {
|
||||
super();
|
||||
@@ -278,7 +279,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this.dimensions.device.char.width,
|
||||
this.dimensions.device.char.height,
|
||||
this._coreBrowserService.dpr,
|
||||
this._deviceMaxTextureSize
|
||||
this._deviceMaxTextureSize,
|
||||
this._customGlyphs
|
||||
);
|
||||
if (this._charAtlas !== atlas) {
|
||||
this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
||||
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { customGlyphDefinitions } from './CustomGlyphDefinitions';
|
||||
import { CustomGlyphDefinitionType, CustomGlyphVectorType, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type CustomGlyphRegionDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
|
||||
import { CustomGlyphDefinitionType, CustomGlyphVectorType, type CustomGlyphDefinitionPart, type CustomGlyphPathDrawFunctionDefinition, type CustomGlyphPatternDefinition, type ICustomGlyphSolidOctantBlockVector, type ICustomGlyphVectorShape } from './Types';
|
||||
|
||||
/**
|
||||
* Try drawing a custom block element or box drawing character, returning whether it was
|
||||
@@ -24,42 +24,63 @@ export function tryDrawCustomGlyph(
|
||||
): boolean {
|
||||
const unifiedCharDefinition = customGlyphDefinitions[c];
|
||||
if (unifiedCharDefinition) {
|
||||
switch (unifiedCharDefinition.type) {
|
||||
case CustomGlyphDefinitionType.SOLID_OCTANT_BLOCK_VECTOR:
|
||||
drawBlockVectorChar(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.BLOCK_PATTERN:
|
||||
drawPatternChar(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_REGION:
|
||||
drawBlockPatternWithRegion(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_REGION_AND_SOLID_OCTANT_BLOCK_VECTOR:
|
||||
drawBlockPatternWithRegion(ctx, unifiedCharDefinition.data.pattern, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
drawBlockVectorChar(ctx, unifiedCharDefinition.data.vectors, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_CLIP_PATH:
|
||||
drawBlockPatternWithClipPath(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.PATH_FUNCTION:
|
||||
case CustomGlyphDefinitionType.PATH:
|
||||
drawPathDefinitionCharacter(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.PATH_NEGATIVE:
|
||||
drawPathNegativeDefinitionCharacter(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, devicePixelRatio, backgroundColor);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.VECTOR_SHAPE:
|
||||
drawVectorShape(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, fontSize, devicePixelRatio);
|
||||
return true;
|
||||
case CustomGlyphDefinitionType.PATH_FUNCTION_WITH_WEIGHT:
|
||||
drawPathDefinitionCharacterWithWeight(ctx, unifiedCharDefinition.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, devicePixelRatio);
|
||||
return true;
|
||||
// Normalize to array for uniform handling
|
||||
const parts = Array.isArray(unifiedCharDefinition) ? unifiedCharDefinition : [unifiedCharDefinition];
|
||||
for (const part of parts) {
|
||||
drawDefinitionPart(ctx, part, xOffset, yOffset, deviceCellWidth, deviceCellHeight, fontSize, devicePixelRatio, backgroundColor);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function drawDefinitionPart(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
part: CustomGlyphDefinitionPart,
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number,
|
||||
fontSize: number,
|
||||
devicePixelRatio: number,
|
||||
backgroundColor?: string
|
||||
): void {
|
||||
// Handle clipPath generically for any definition type
|
||||
if (part.clipPath) {
|
||||
ctx.save();
|
||||
applyClipPath(ctx, part.clipPath, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
}
|
||||
|
||||
switch (part.type) {
|
||||
case CustomGlyphDefinitionType.SOLID_OCTANT_BLOCK_VECTOR:
|
||||
drawBlockVectorChar(ctx, part.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.BLOCK_PATTERN:
|
||||
drawPatternChar(ctx, part.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.PATH_FUNCTION:
|
||||
drawPathFunctionCharacter(ctx, part.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, devicePixelRatio, part.strokeWidth);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.PATH:
|
||||
drawPathDefinitionCharacter(ctx, part.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.PATH_NEGATIVE:
|
||||
drawPathNegativeDefinitionCharacter(ctx, part.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, devicePixelRatio, backgroundColor);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.VECTOR_SHAPE:
|
||||
drawVectorShape(ctx, part.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight, fontSize, devicePixelRatio);
|
||||
break;
|
||||
case CustomGlyphDefinitionType.BRAILLE:
|
||||
drawBrailleCharacter(ctx, part.data, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
break;
|
||||
}
|
||||
|
||||
if (part.clipPath) {
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
||||
function drawBlockVectorChar(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
charDefinition: ICustomGlyphSolidOctantBlockVector[],
|
||||
@@ -81,6 +102,52 @@ function drawBlockVectorChar(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Braille dot positions in octant coordinates (x, y for center of each dot area)
|
||||
* Columns: left=1-2, right=5-6 (leaving 0 and 7 as margins, 3-4 as gap)
|
||||
* Rows: 0-1, 2-3, 4-5, 6-7 for the 4 rows
|
||||
*/
|
||||
const brailleDotPositions = new Uint8Array([
|
||||
1, 0, // dot 1 - bit 0
|
||||
1, 2, // dot 2 - bit 1
|
||||
1, 4, // dot 3 - bit 2
|
||||
5, 0, // dot 4 - bit 3
|
||||
5, 2, // dot 5 - bit 4
|
||||
5, 4, // dot 6 - bit 5
|
||||
1, 6, // dot 7 - bit 6
|
||||
5, 6, // dot 8 - bit 7
|
||||
]);
|
||||
|
||||
/**
|
||||
* Draws a braille pattern
|
||||
*/
|
||||
function drawBrailleCharacter(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
pattern: number,
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number
|
||||
): void {
|
||||
const xEighth = deviceCellWidth / 8;
|
||||
const paddingY = deviceCellHeight * 0.1;
|
||||
const usableHeight = deviceCellHeight * 0.8;
|
||||
const yEighth = usableHeight / 8;
|
||||
const radius = Math.min(xEighth, yEighth);
|
||||
|
||||
for (let bit = 0; bit < 8; bit++) {
|
||||
if (pattern & (1 << bit)) {
|
||||
const x = brailleDotPositions[bit * 2];
|
||||
const y = brailleDotPositions[bit * 2 + 1];
|
||||
const cx = xOffset + (x + 1) * xEighth;
|
||||
const cy = yOffset + paddingY + (y + 1) * yEighth;
|
||||
ctx.beginPath();
|
||||
ctx.arc(cx, cy, radius, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function drawPathDefinitionCharacter(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
charDefinition: CustomGlyphPathDrawFunctionDefinition | string,
|
||||
@@ -398,139 +465,66 @@ function drawPatternChar(
|
||||
ctx.fillRect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws rectangular shade characters - medium shade pattern clipped to a region.
|
||||
* Uses a checkerboard pattern that shifts 1px each row (same as medium shade U+2592).
|
||||
*/
|
||||
function drawBlockPatternWithRegion(
|
||||
function drawPathFunctionCharacter(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
definition: [pattern: CustomGlyphPatternDefinition, region: CustomGlyphRegionDefinition],
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number
|
||||
): void {
|
||||
const [pattern, region] = definition;
|
||||
const [rx, ry, rw, rh] = region;
|
||||
const regionX = Math.round(xOffset + rx * deviceCellWidth);
|
||||
const regionY = Math.round(yOffset + ry * deviceCellHeight);
|
||||
const regionW = Math.round(rw * deviceCellWidth);
|
||||
const regionH = Math.round(rh * deviceCellHeight);
|
||||
|
||||
// Save context state
|
||||
ctx.save();
|
||||
|
||||
// Clip to the region
|
||||
ctx.beginPath();
|
||||
ctx.rect(regionX, regionY, regionW, regionH);
|
||||
ctx.clip();
|
||||
|
||||
// Draw the pattern
|
||||
drawPatternChar(ctx, pattern, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
|
||||
// Restore context state
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the following box drawing characters by mapping a subset of SVG d attribute instructions to
|
||||
* canvas draw calls.
|
||||
*
|
||||
* Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐
|
||||
* ┌─┬─┐ ┏━┳━┓ ╔═╦═╗ ┠╂┨┝┿┥╟╫╢╞╪╡ ┡╇┩├╊┫┢╈┪┣╉┤
|
||||
* │ │ │ ┃ ┃ ┃ ║ ║ ║ ┖┸┚┕┷┙╙╨╜╘╧╛ └┴┘└┺┛┗┻┛┗┹┘
|
||||
* ├─┼─┤ ┣━╋━┫ ╠═╬═╣ ┏┱┐┌┲┓┌┬┐┌┬┐ ┏┳┓┌┮┓┌┬┐┏┭┐
|
||||
* │ │ │ ┃ ┃ ┃ ║ ║ ║ ┡╃┤├╄┩├╆┪┢╅┤ ┞╀┦├┾┫┟╁┧┣┽┤
|
||||
* └─┴─┘ ┗━┻━┛ ╚═╩═╝ └┴┘└┴┘└┺┛┗┹┘ └┴┘└┶┛┗┻┛┗┵┘
|
||||
*
|
||||
* Other:
|
||||
* ╭─╮ ╲ ╱ ╷╻╎╏┆┇┊┋ ╺╾╴ ╌╌╌ ┄┄┄ ┈┈┈
|
||||
* │ │ ╳ ╽╿╎╏┆┇┊┋ ╶╼╸ ╍╍╍ ┅┅┅ ┉┉┉
|
||||
* ╰─╯ ╱ ╲ ╹╵╎╏┆┇┊┋
|
||||
*
|
||||
* All box drawing characters:
|
||||
* ─ ━ │ ┃ ┄ ┅ ┆ ┇ ┈ ┉ ┊ ┋ ┌ ┍ ┎ ┏
|
||||
* ┐ ┑ ┒ ┓ └ ┕ ┖ ┗ ┘ ┙ ┚ ┛ ├ ┝ ┞ ┟
|
||||
* ┠ ┡ ┢ ┣ ┤ ┥ ┦ ┧ ┨ ┩ ┪ ┫ ┬ ┭ ┮ ┯
|
||||
* ┰ ┱ ┲ ┳ ┴ ┵ ┶ ┷ ┸ ┹ ┺ ┻ ┼ ┽ ┾ ┿
|
||||
* ╀ ╁ ╂ ╃ ╄ ╅ ╆ ╇ ╈ ╉ ╊ ╋ ╌ ╍ ╎ ╏
|
||||
* ═ ║ ╒ ╓ ╔ ╕ ╖ ╗ ╘ ╙ ╚ ╛ ╜ ╝ ╞ ╟
|
||||
* ╠ ╡ ╢ ╣ ╤ ╥ ╦ ╧ ╨ ╩ ╪ ╫ ╬ ╭ ╮ ╯
|
||||
* ╰ ╱ ╲ ╳ ╴ ╵ ╶ ╷ ╸ ╹ ╺ ╻ ╼ ╽ ╾ ╿
|
||||
*
|
||||
* ---
|
||||
*
|
||||
* Box drawing alignment tests: █
|
||||
* ▉
|
||||
* ╔══╦══╗ ┌──┬──┐ ╭──┬──╮ ╭──┬──╮ ┏━━┳━━┓ ┎┒┏┑ ╷ ╻ ┏┯┓ ┌┰┐ ▊ ╱╲╱╲╳╳╳
|
||||
* ║┌─╨─┐║ │╔═╧═╗│ │╒═╪═╕│ │╓─╁─╖│ ┃┌─╂─┐┃ ┗╃╄┙ ╶┼╴╺╋╸┠┼┨ ┝╋┥ ▋ ╲╱╲╱╳╳╳
|
||||
* ║│╲ ╱│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╿ │┃ ┍╅╆┓ ╵ ╹ ┗┷┛ └┸┘ ▌ ╱╲╱╲╳╳╳
|
||||
* ╠╡ ╳ ╞╣ ├╢ ╟┤ ├┼─┼─┼┤ ├╫─╂─╫┤ ┣┿╾┼╼┿┫ ┕┛┖┚ ┌┄┄┐ ╎ ┏┅┅┓ ┋ ▍ ╲╱╲╱╳╳╳
|
||||
* ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎
|
||||
* ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏
|
||||
* ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█
|
||||
*
|
||||
* Source: https://www.w3.org/2001/06/utf-8-test/UTF-8-demo.html
|
||||
*/
|
||||
function drawPathDefinitionCharacterWithWeight(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
charDefinition: { [fontWeight: number]: string | ((xp: number, yp: number) => string) },
|
||||
charDefinition: string | ((xp: number, yp: number) => string),
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number,
|
||||
devicePixelRatio: number
|
||||
devicePixelRatio: number,
|
||||
strokeWidth?: number
|
||||
): void {
|
||||
ctx.strokeStyle = ctx.fillStyle;
|
||||
for (const [fontWeight, instructions] of Object.entries(charDefinition)) {
|
||||
ctx.beginPath();
|
||||
ctx.lineWidth = devicePixelRatio * Number.parseInt(fontWeight);
|
||||
let actualInstructions: string;
|
||||
if (typeof instructions === 'function') {
|
||||
const xp = .15;
|
||||
const yp = .15 / deviceCellHeight * deviceCellWidth;
|
||||
actualInstructions = instructions(xp, yp);
|
||||
} else {
|
||||
actualInstructions = instructions;
|
||||
}
|
||||
for (const instruction of actualInstructions.split(' ')) {
|
||||
const type = instruction[0];
|
||||
if (type === 'Z') {
|
||||
ctx.closePath();
|
||||
continue;
|
||||
}
|
||||
const f = svgToCanvasInstructionMap[type];
|
||||
if (!f) {
|
||||
console.error(`Could not find drawing instructions for "${type}"`);
|
||||
continue;
|
||||
}
|
||||
const args: string[] = instruction.substring(1).split(',');
|
||||
if (!args[0] || !args[1]) {
|
||||
continue;
|
||||
}
|
||||
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio));
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
ctx.beginPath();
|
||||
let actualInstructions: string;
|
||||
if (typeof charDefinition === 'function') {
|
||||
const xp = .15;
|
||||
const yp = .15 / deviceCellHeight * deviceCellWidth;
|
||||
actualInstructions = charDefinition(xp, yp);
|
||||
} else {
|
||||
actualInstructions = charDefinition;
|
||||
}
|
||||
const state: ISvgPathState = { currentX: 0, currentY: 0, lastControlX: 0, lastControlY: 0, lastCommand: '' };
|
||||
for (const instruction of actualInstructions.split(' ')) {
|
||||
const type = instruction[0];
|
||||
if (type === 'Z') {
|
||||
ctx.closePath();
|
||||
state.lastCommand = type;
|
||||
continue;
|
||||
}
|
||||
const f = svgToCanvasInstructionMap[type];
|
||||
if (!f) {
|
||||
console.error(`Could not find drawing instructions for "${type}"`);
|
||||
continue;
|
||||
}
|
||||
const args: string[] = instruction.substring(1).split(',');
|
||||
if (!args[0] || !args[1]) {
|
||||
continue;
|
||||
}
|
||||
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio), state);
|
||||
state.lastCommand = type;
|
||||
}
|
||||
if (strokeWidth !== undefined) {
|
||||
ctx.strokeStyle = ctx.fillStyle;
|
||||
ctx.lineWidth = devicePixelRatio * strokeWidth;
|
||||
ctx.stroke();
|
||||
} else {
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws a pattern clipped to an arbitrary path (for triangular shades, etc.)
|
||||
* Applies a clip path to the canvas context from SVG-like path instructions.
|
||||
*/
|
||||
function drawBlockPatternWithClipPath(
|
||||
function applyClipPath(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
definition: [pattern: CustomGlyphPatternDefinition, clipPath: string],
|
||||
clipPath: string,
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number
|
||||
): void {
|
||||
const [pattern, clipPath] = definition;
|
||||
|
||||
ctx.save();
|
||||
|
||||
// Build clip path from SVG-like instructions
|
||||
ctx.beginPath();
|
||||
for (const instruction of clipPath.split(' ')) {
|
||||
const type = instruction[0];
|
||||
@@ -551,11 +545,6 @@ function drawBlockPatternWithClipPath(
|
||||
}
|
||||
}
|
||||
ctx.clip();
|
||||
|
||||
// Draw the pattern
|
||||
drawPatternChar(ctx, pattern, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function drawVectorShape(
|
||||
@@ -577,10 +566,12 @@ function drawVectorShape(
|
||||
// Scale the stroke with DPR and font size
|
||||
const cssLineWidth = fontSize / 12;
|
||||
ctx.lineWidth = devicePixelRatio * cssLineWidth;
|
||||
const state: ISvgPathState = { currentX: 0, currentY: 0, lastControlX: 0, lastControlY: 0, lastCommand: '' };
|
||||
for (const instruction of charDefinition.d.split(' ')) {
|
||||
const type = instruction[0];
|
||||
if (type === 'Z') {
|
||||
ctx.closePath();
|
||||
state.lastCommand = type;
|
||||
continue;
|
||||
}
|
||||
const f = svgToCanvasInstructionMap[type];
|
||||
@@ -602,7 +593,8 @@ function drawVectorShape(
|
||||
devicePixelRatio,
|
||||
(charDefinition.leftPadding ?? 0) * (cssLineWidth / 2),
|
||||
(charDefinition.rightPadding ?? 0) * (cssLineWidth / 2)
|
||||
));
|
||||
), state);
|
||||
state.lastCommand = type;
|
||||
}
|
||||
if (charDefinition.type === CustomGlyphVectorType.STROKE) {
|
||||
ctx.strokeStyle = ctx.fillStyle;
|
||||
@@ -617,11 +609,55 @@ function clamp(value: number, max: number, min: number = 0): number {
|
||||
return Math.max(Math.min(value, max), min);
|
||||
}
|
||||
|
||||
const svgToCanvasInstructionMap: { [index: string]: any } = {
|
||||
'C': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.bezierCurveTo(args[0], args[1], args[2], args[3], args[4], args[5]),
|
||||
'L': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.lineTo(args[0], args[1]),
|
||||
'M': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.moveTo(args[0], args[1]),
|
||||
'Q': (ctx: CanvasRenderingContext2D, args: number[]) => ctx.quadraticCurveTo(args[0], args[1], args[2], args[3])
|
||||
interface ISvgPathState {
|
||||
currentX: number;
|
||||
currentY: number;
|
||||
lastControlX: number;
|
||||
lastControlY: number;
|
||||
lastCommand: string;
|
||||
}
|
||||
|
||||
const svgToCanvasInstructionMap: { [index: string]: (ctx: CanvasRenderingContext2D, args: number[], state: ISvgPathState) => void } = {
|
||||
'C': (ctx, args, state) => {
|
||||
ctx.bezierCurveTo(args[0], args[1], args[2], args[3], args[4], args[5]);
|
||||
state.lastControlX = args[2];
|
||||
state.lastControlY = args[3];
|
||||
state.currentX = args[4];
|
||||
state.currentY = args[5];
|
||||
},
|
||||
'L': (ctx, args, state) => {
|
||||
ctx.lineTo(args[0], args[1]);
|
||||
state.lastControlX = state.currentX = args[0];
|
||||
state.lastControlY = state.currentY = args[1];
|
||||
},
|
||||
'M': (ctx, args, state) => {
|
||||
ctx.moveTo(args[0], args[1]);
|
||||
state.lastControlX = state.currentX = args[0];
|
||||
state.lastControlY = state.currentY = args[1];
|
||||
},
|
||||
'Q': (ctx, args, state) => {
|
||||
ctx.quadraticCurveTo(args[0], args[1], args[2], args[3]);
|
||||
state.lastControlX = args[0];
|
||||
state.lastControlY = args[1];
|
||||
state.currentX = args[2];
|
||||
state.currentY = args[3];
|
||||
},
|
||||
'T': (ctx, args, state) => {
|
||||
let cpX: number;
|
||||
let cpY: number;
|
||||
if (state.lastCommand === 'Q' || state.lastCommand === 'T') {
|
||||
cpX = 2 * state.currentX - state.lastControlX;
|
||||
cpY = 2 * state.currentY - state.lastControlY;
|
||||
} else {
|
||||
cpX = state.currentX;
|
||||
cpY = state.currentY;
|
||||
}
|
||||
ctx.quadraticCurveTo(cpX, cpY, args[0], args[1]);
|
||||
state.lastControlX = cpX;
|
||||
state.lastControlY = cpY;
|
||||
state.currentX = args[0];
|
||||
state.currentY = args[1];
|
||||
}
|
||||
};
|
||||
|
||||
function translateArgs(args: string[], cellWidth: number, cellHeight: number, xOffset: number, yOffset: number, doClamp: boolean, devicePixelRatio: number, leftPadding: number = 0, rightPadding: number = 0): number[] {
|
||||
|
||||
@@ -33,29 +33,37 @@ export type CustomGlyphPatternDefinition = number[][];
|
||||
export const enum CustomGlyphDefinitionType {
|
||||
SOLID_OCTANT_BLOCK_VECTOR,
|
||||
BLOCK_PATTERN,
|
||||
BLOCK_PATTERN_WITH_REGION,
|
||||
BLOCK_PATTERN_WITH_REGION_AND_SOLID_OCTANT_BLOCK_VECTOR,
|
||||
BLOCK_PATTERN_WITH_CLIP_PATH,
|
||||
PATH_FUNCTION,
|
||||
PATH_FUNCTION_WITH_WEIGHT,
|
||||
PATH,
|
||||
PATH_NEGATIVE,
|
||||
VECTOR_SHAPE,
|
||||
BRAILLE,
|
||||
}
|
||||
|
||||
export type CustomGlyphRegionDefinition = [x: number, y: number, w: number, h: number];
|
||||
|
||||
export type CustomGlyphCharacterDefinition = (
|
||||
export type CustomGlyphDefinitionPartRaw = (
|
||||
{ type: CustomGlyphDefinitionType.SOLID_OCTANT_BLOCK_VECTOR, data: ICustomGlyphSolidOctantBlockVector[] } |
|
||||
{ type: CustomGlyphDefinitionType.BLOCK_PATTERN, data: CustomGlyphPatternDefinition } |
|
||||
{ type: CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_REGION, data: [pattern: CustomGlyphPatternDefinition, region: CustomGlyphRegionDefinition] } |
|
||||
// TODO: Consolidate these, draws should be possible via regions/clipping instead of special
|
||||
// casing
|
||||
{ type: CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_REGION_AND_SOLID_OCTANT_BLOCK_VECTOR, data: { pattern: [pattern: CustomGlyphPatternDefinition, region: CustomGlyphRegionDefinition], vectors: ICustomGlyphSolidOctantBlockVector[] } } |
|
||||
{ type: CustomGlyphDefinitionType.BLOCK_PATTERN_WITH_CLIP_PATH, data: [pattern: CustomGlyphPatternDefinition, clipPath: string] } |
|
||||
{ type: CustomGlyphDefinitionType.PATH_FUNCTION, data: CustomGlyphPathDrawFunctionDefinition } |
|
||||
{ type: CustomGlyphDefinitionType.PATH_FUNCTION_WITH_WEIGHT, data: { [fontWeight: number]: string | CustomGlyphPathDrawFunctionDefinition } } |
|
||||
{ type: CustomGlyphDefinitionType.PATH_FUNCTION, data: CustomGlyphPathDrawFunctionDefinition | string } |
|
||||
{ type: CustomGlyphDefinitionType.PATH, data: string } |
|
||||
{ type: CustomGlyphDefinitionType.PATH_NEGATIVE, data: ICustomGlyphVectorShape } |
|
||||
{ type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: ICustomGlyphVectorShape }
|
||||
{ type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: ICustomGlyphVectorShape} |
|
||||
{ type: CustomGlyphDefinitionType.BRAILLE, data: number }
|
||||
);
|
||||
|
||||
export interface ICustomGlyphDefinitionCommon {
|
||||
/**
|
||||
* A custom clip path for the draw definition, restricting the area it can draw to.
|
||||
*/
|
||||
clipPath?: string;
|
||||
/**
|
||||
* The stroke width to use when drawing the path. Defaults to 1.
|
||||
*/
|
||||
strokeWidth?: number;
|
||||
}
|
||||
|
||||
export type CustomGlyphDefinitionPart = CustomGlyphDefinitionPartRaw & ICustomGlyphDefinitionCommon;
|
||||
|
||||
/**
|
||||
* A character definition that can be a single part or an array of parts drawn in sequence.
|
||||
*/
|
||||
export type CustomGlyphCharacterDefinition = CustomGlyphDefinitionPart | CustomGlyphDefinitionPart[];
|
||||
|
||||
Reference in New Issue
Block a user