fix(canvas): render Figma text decoration styles
This commit is contained in:
parent
ec31ea1186
commit
80daa0d1e9
|
|
@ -195,6 +195,31 @@ function textDecorationValue(ck: CanvasKit, decoration: string): number {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function textDecorationStyleValue<T>(
|
||||||
|
ck: { DecorationStyle: { Solid: T; Dotted: T; Wavy: T } },
|
||||||
|
style: SceneNode['textDecorationStyle'] | undefined
|
||||||
|
): T {
|
||||||
|
switch (style) {
|
||||||
|
case 'DOTTED':
|
||||||
|
return ck.DecorationStyle.Dotted
|
||||||
|
case 'WAVY':
|
||||||
|
return ck.DecorationStyle.Wavy
|
||||||
|
default:
|
||||||
|
return ck.DecorationStyle.Solid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function textDecorationColor(
|
||||||
|
ck: CanvasKit,
|
||||||
|
fills: SceneNode['textDecorationFills'] | undefined,
|
||||||
|
fallback: Float32Array
|
||||||
|
): Float32Array {
|
||||||
|
const fill = fills?.find((item) => item.visible && item.type === 'SOLID')
|
||||||
|
if (!fill) return fallback
|
||||||
|
const color = resolveRGBAForPreview(fill.color).color
|
||||||
|
return ck.Color4f(color.r, color.g, color.b, color.a * fill.opacity)
|
||||||
|
}
|
||||||
|
|
||||||
function styleRunColor(
|
function styleRunColor(
|
||||||
ck: CanvasKit,
|
ck: CanvasKit,
|
||||||
style: SceneNode['styleRuns'][number]['style'],
|
style: SceneNode['styleRuns'][number]['style'],
|
||||||
|
|
@ -238,6 +263,17 @@ function pushStyleRun(
|
||||||
fontFeatures: textFontFeatures(style.fontFeatures ?? node.fontFeatures),
|
fontFeatures: textFontFeatures(style.fontFeatures ?? node.fontFeatures),
|
||||||
letterSpacing: style.letterSpacing ?? (node.letterSpacing || 0),
|
letterSpacing: style.letterSpacing ?? (node.letterSpacing || 0),
|
||||||
decoration: textDecorationValue(ck, style.textDecoration ?? node.textDecoration),
|
decoration: textDecorationValue(ck, style.textDecoration ?? node.textDecoration),
|
||||||
|
decorationStyle: textDecorationStyleValue(
|
||||||
|
ck,
|
||||||
|
style.textDecorationStyle ?? node.textDecorationStyle
|
||||||
|
),
|
||||||
|
decorationThickness:
|
||||||
|
style.textDecorationThickness ?? node.textDecorationThickness ?? undefined,
|
||||||
|
decorationColor: textDecorationColor(
|
||||||
|
ck,
|
||||||
|
style.textDecorationFills ?? node.textDecorationFills,
|
||||||
|
baseColor
|
||||||
|
),
|
||||||
heightMultiplier: runLineHeight ? runLineHeight / runFontSize : undefined,
|
heightMultiplier: runLineHeight ? runLineHeight / runFontSize : undefined,
|
||||||
halfLeading
|
halfLeading
|
||||||
})
|
})
|
||||||
|
|
@ -310,6 +346,9 @@ export function buildParagraph(
|
||||||
fontFeatures: textFontFeatures(node.fontFeatures),
|
fontFeatures: textFontFeatures(node.fontFeatures),
|
||||||
letterSpacing: node.letterSpacing || 0,
|
letterSpacing: node.letterSpacing || 0,
|
||||||
decoration: textDecorationValue(ck, node.textDecoration),
|
decoration: textDecorationValue(ck, node.textDecoration),
|
||||||
|
decorationStyle: textDecorationStyleValue(ck, node.textDecorationStyle),
|
||||||
|
decorationThickness: node.textDecorationThickness ?? undefined,
|
||||||
|
decorationColor: textDecorationColor(ck, node.textDecorationFills, baseColor),
|
||||||
heightMultiplier: node.lineHeight ? node.lineHeight / baseFontSize : undefined,
|
heightMultiplier: node.lineHeight ? node.lineHeight / baseFontSize : undefined,
|
||||||
halfLeading
|
halfLeading
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -275,10 +275,7 @@ function importedTextLineHeight(nc: NodeChange): number | null {
|
||||||
return convertLineHeight(nc.lineHeight, nc.fontSize)
|
return convertLineHeight(nc.lineHeight, nc.fontSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertTextProps(
|
type TextProps = Pick<
|
||||||
nc: NodeChange,
|
|
||||||
blobs: Uint8Array[]
|
|
||||||
): Pick<
|
|
||||||
SceneNode,
|
SceneNode,
|
||||||
| 'text'
|
| 'text'
|
||||||
| 'fontSize'
|
| 'fontSize'
|
||||||
|
|
@ -290,6 +287,9 @@ function convertTextProps(
|
||||||
| 'textAutoResize'
|
| 'textAutoResize'
|
||||||
| 'textCase'
|
| 'textCase'
|
||||||
| 'textDecoration'
|
| 'textDecoration'
|
||||||
|
| 'textDecorationStyle'
|
||||||
|
| 'textDecorationThickness'
|
||||||
|
| 'textDecorationFills'
|
||||||
| 'lineHeight'
|
| 'lineHeight'
|
||||||
| 'letterSpacing'
|
| 'letterSpacing'
|
||||||
| 'maxLines'
|
| 'maxLines'
|
||||||
|
|
@ -300,7 +300,23 @@ function convertTextProps(
|
||||||
| 'textDirection'
|
| 'textDirection'
|
||||||
| 'figmaDerivedLayout'
|
| 'figmaDerivedLayout'
|
||||||
| 'figmaDerivedTextGlyphs'
|
| 'figmaDerivedTextGlyphs'
|
||||||
|
>
|
||||||
|
|
||||||
|
function convertTextDecorationProps(
|
||||||
|
nc: NodeChange
|
||||||
|
): Pick<
|
||||||
|
SceneNode,
|
||||||
|
'textDecoration' | 'textDecorationStyle' | 'textDecorationThickness' | 'textDecorationFills'
|
||||||
> {
|
> {
|
||||||
|
return {
|
||||||
|
textDecoration: mapTextDecoration(nc.textDecoration as string),
|
||||||
|
textDecorationStyle: (nc.textDecorationStyle ?? 'SOLID') as SceneNode['textDecorationStyle'],
|
||||||
|
textDecorationThickness: nc.textDecorationThickness?.value ?? null,
|
||||||
|
textDecorationFills: convertFills(nc.textDecorationFillPaints)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertTextProps(nc: NodeChange, blobs: Uint8Array[]): TextProps {
|
||||||
return {
|
return {
|
||||||
text: nc.textData?.characters ?? '',
|
text: nc.textData?.characters ?? '',
|
||||||
fontSize: nc.fontSize ?? 14,
|
fontSize: nc.fontSize ?? 14,
|
||||||
|
|
@ -315,7 +331,7 @@ function convertTextProps(
|
||||||
textAlignVertical: (nc.textAlignVertical ?? 'TOP') as TextAlignVertical,
|
textAlignVertical: (nc.textAlignVertical ?? 'TOP') as TextAlignVertical,
|
||||||
textAutoResize: (nc.textAutoResize ?? 'NONE') as TextAutoResize,
|
textAutoResize: (nc.textAutoResize ?? 'NONE') as TextAutoResize,
|
||||||
textCase: (nc.textCase ?? 'ORIGINAL') as TextCase,
|
textCase: (nc.textCase ?? 'ORIGINAL') as TextCase,
|
||||||
textDecoration: mapTextDecoration(nc.textDecoration as string),
|
...convertTextDecorationProps(nc),
|
||||||
lineHeight: importedTextLineHeight(nc),
|
lineHeight: importedTextLineHeight(nc),
|
||||||
letterSpacing: convertLetterSpacing(nc.letterSpacing, nc.fontSize),
|
letterSpacing: convertLetterSpacing(nc.letterSpacing, nc.fontSize),
|
||||||
maxLines: (nc.maxLines ?? null) as number | null,
|
maxLines: (nc.maxLines ?? null) as number | null,
|
||||||
|
|
|
||||||
|
|
@ -317,6 +317,13 @@ function serializeTextProps(
|
||||||
if (node.textDecoration !== 'NONE') {
|
if (node.textDecoration !== 'NONE') {
|
||||||
nc.textDecoration = node.textDecoration === 'UNDERLINE' ? 'UNDERLINE' : 'STRIKETHROUGH'
|
nc.textDecoration = node.textDecoration === 'UNDERLINE' ? 'UNDERLINE' : 'STRIKETHROUGH'
|
||||||
}
|
}
|
||||||
|
if (node.textDecorationStyle !== 'SOLID') nc.textDecorationStyle = node.textDecorationStyle
|
||||||
|
if (node.textDecorationThickness != null) {
|
||||||
|
nc.textDecorationThickness = { value: node.textDecorationThickness, units: 'PIXELS' }
|
||||||
|
}
|
||||||
|
if (node.textDecorationFills.length > 0) {
|
||||||
|
nc.textDecorationFillPaints = node.textDecorationFills.map(fillToKiwiPaint)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeStackMode(value: string | undefined): KiwiNodeChange['stackMode'] {
|
function normalizeStackMode(value: string | undefined): KiwiNodeChange['stackMode'] {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,15 @@ function convertStyleOverride(
|
||||||
}
|
}
|
||||||
const deco = override.textDecoration
|
const deco = override.textDecoration
|
||||||
if (deco) style.textDecoration = mapTextDecoration(deco)
|
if (deco) style.textDecoration = mapTextDecoration(deco)
|
||||||
|
if (override.textDecorationStyle)
|
||||||
|
style.textDecorationStyle =
|
||||||
|
override.textDecorationStyle as CharacterStyleOverride['textDecorationStyle']
|
||||||
|
if (override.textDecorationThickness)
|
||||||
|
style.textDecorationThickness = override.textDecorationThickness.value ?? null
|
||||||
|
if (override.textDecorationFillPaints) {
|
||||||
|
const decorationFills = convertFills(override.textDecorationFillPaints)
|
||||||
|
if (decorationFills.length > 0) style.textDecorationFills = decorationFills
|
||||||
|
}
|
||||||
if (override.fillPaints) {
|
if (override.fillPaints) {
|
||||||
const fills = convertFills(override.fillPaints)
|
const fills = convertFills(override.fillPaints)
|
||||||
if (fills.length > 0) style.fills = fills
|
if (fills.length > 0) style.fills = fills
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,13 @@ function textStyleOverrideToKiwi(
|
||||||
override.lineHeight = { value: style.lineHeight, units: 'PIXELS' }
|
override.lineHeight = { value: style.lineHeight, units: 'PIXELS' }
|
||||||
}
|
}
|
||||||
if (style.textDecoration) override.textDecoration = style.textDecoration
|
if (style.textDecoration) override.textDecoration = style.textDecoration
|
||||||
|
if (style.textDecorationStyle) override.textDecorationStyle = style.textDecorationStyle
|
||||||
|
if (style.textDecorationThickness != null) {
|
||||||
|
override.textDecorationThickness = { value: style.textDecorationThickness, units: 'PIXELS' }
|
||||||
|
}
|
||||||
|
if (style.textDecorationFills && style.textDecorationFills.length > 0) {
|
||||||
|
override.textDecorationFillPaints = style.textDecorationFills.map(fillToKiwiPaint)
|
||||||
|
}
|
||||||
if (style.fills && style.fills.length > 0) {
|
if (style.fills && style.fills.length > 0) {
|
||||||
override.fillPaints = style.fills.map(fillToKiwiPaint)
|
override.fillPaints = style.fills.map(fillToKiwiPaint)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,9 @@ export function copyStyleRun(r: StyleRun): StyleRun {
|
||||||
style: {
|
style: {
|
||||||
...r.style,
|
...r.style,
|
||||||
fills: r.style.fills ? r.style.fills.map(copyFill) : undefined,
|
fills: r.style.fills ? r.style.fills.map(copyFill) : undefined,
|
||||||
|
textDecorationFills: r.style.textDecorationFills
|
||||||
|
? r.style.textDecorationFills.map(copyFill)
|
||||||
|
: undefined,
|
||||||
fontVariations: r.style.fontVariations
|
fontVariations: r.style.fontVariations
|
||||||
? r.style.fontVariations.map((v) => ({ ...v }))
|
? r.style.fontVariations.map((v) => ({ ...v }))
|
||||||
: undefined,
|
: undefined,
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,9 @@ export function createDefaultNode(
|
||||||
textAutoResize: 'NONE',
|
textAutoResize: 'NONE',
|
||||||
textCase: 'ORIGINAL',
|
textCase: 'ORIGINAL',
|
||||||
textDecoration: 'NONE',
|
textDecoration: 'NONE',
|
||||||
|
textDecorationStyle: 'SOLID',
|
||||||
|
textDecorationThickness: null,
|
||||||
|
textDecorationFills: [],
|
||||||
maxLines: null,
|
maxLines: null,
|
||||||
styleRuns: [],
|
styleRuns: [],
|
||||||
fontVariations: [],
|
fontVariations: [],
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,9 @@ const RAW_NODE_FIELD_KEYS = new Set([
|
||||||
'textAutoResize',
|
'textAutoResize',
|
||||||
'textCase',
|
'textCase',
|
||||||
'textDecoration',
|
'textDecoration',
|
||||||
|
'textDecorationStyle',
|
||||||
|
'textDecorationThickness',
|
||||||
|
'textDecorationFills',
|
||||||
'lineHeight',
|
'lineHeight',
|
||||||
'letterSpacing',
|
'letterSpacing',
|
||||||
'maxLines',
|
'maxLines',
|
||||||
|
|
|
||||||
|
|
@ -187,6 +187,7 @@ export type TextAutoResize = 'NONE' | 'HEIGHT' | 'WIDTH_AND_HEIGHT' | 'TRUNCATE'
|
||||||
export type TextAlignVertical = 'TOP' | 'CENTER' | 'BOTTOM'
|
export type TextAlignVertical = 'TOP' | 'CENTER' | 'BOTTOM'
|
||||||
export type TextCase = 'ORIGINAL' | 'UPPER' | 'LOWER' | 'TITLE'
|
export type TextCase = 'ORIGINAL' | 'UPPER' | 'LOWER' | 'TITLE'
|
||||||
export type TextDecoration = 'NONE' | 'UNDERLINE' | 'STRIKETHROUGH'
|
export type TextDecoration = 'NONE' | 'UNDERLINE' | 'STRIKETHROUGH'
|
||||||
|
export type TextDecorationStyle = 'SOLID' | 'DOTTED' | 'WAVY'
|
||||||
export type TextDirection = 'AUTO' | 'LTR' | 'RTL'
|
export type TextDirection = 'AUTO' | 'LTR' | 'RTL'
|
||||||
export type LayoutDirection = 'AUTO' | 'LTR' | 'RTL'
|
export type LayoutDirection = 'AUTO' | 'LTR' | 'RTL'
|
||||||
|
|
||||||
|
|
@ -204,6 +205,9 @@ export interface CharacterStyleOverride {
|
||||||
fontWeight?: number
|
fontWeight?: number
|
||||||
italic?: boolean
|
italic?: boolean
|
||||||
textDecoration?: TextDecoration
|
textDecoration?: TextDecoration
|
||||||
|
textDecorationStyle?: TextDecorationStyle
|
||||||
|
textDecorationThickness?: number | null
|
||||||
|
textDecorationFills?: Fill[]
|
||||||
fontSize?: number
|
fontSize?: number
|
||||||
fontFamily?: string
|
fontFamily?: string
|
||||||
letterSpacing?: number
|
letterSpacing?: number
|
||||||
|
|
@ -350,6 +354,9 @@ export interface SceneNode {
|
||||||
textAutoResize: TextAutoResize
|
textAutoResize: TextAutoResize
|
||||||
textCase: TextCase
|
textCase: TextCase
|
||||||
textDecoration: TextDecoration
|
textDecoration: TextDecoration
|
||||||
|
textDecorationStyle: TextDecorationStyle
|
||||||
|
textDecorationThickness: number | null
|
||||||
|
textDecorationFills: Fill[]
|
||||||
lineHeight: number | null
|
lineHeight: number | null
|
||||||
letterSpacing: number
|
letterSpacing: number
|
||||||
maxLines: number | null
|
maxLines: number | null
|
||||||
|
|
|
||||||
|
|
@ -150,7 +150,7 @@ Figma's design documentation groups features into these areas:
|
||||||
| Reverse z-index / align-content | ✅ | ◐ | — | ✅ | ✅ | Modeled and exported; UI is limited. |
|
| Reverse z-index / align-content | ✅ | ◐ | — | ✅ | ✅ | Modeled and exported; UI is limited. |
|
||||||
| Constraints | ✅ | ◐ | — | ✅ | ✅ | Tools/API expose constraints; main UI is limited. |
|
| Constraints | ✅ | ◐ | — | ✅ | ✅ | Tools/API expose constraints; main UI is limited. |
|
||||||
| Layout grids / guides | ↩ | — | — | ↩ | — | `styleIdForGrid` and `guides` are preserved only. |
|
| Layout grids / guides | ↩ | — | — | ↩ | — | `styleIdForGrid` and `guides` are preserved only. |
|
||||||
| Text styles | ↩ | ◐ | — | ↩ | — | Style IDs round-trip; no style management UI. Rich schema metadata such as derived text data, leading trim, decoration style, and semantic font style/weight is preserved for round-trip. |
|
| Text styles | ↩ | ◐ | — | ↩ | — | Style IDs round-trip; no style management UI. Rich schema metadata such as derived text data, leading trim, decoration style/thickness/fill, and semantic font style/weight is preserved for round-trip. |
|
||||||
| Rich style runs | ✅ | ✅ | ◐ | ✅ | ✅ | Import/render/export support; editing mixed runs is partial. |
|
| Rich style runs | ✅ | ✅ | ◐ | ✅ | ✅ | Import/render/export support; editing mixed runs is partial. |
|
||||||
| Text auto resize | ✅ | ✅ | ◐ | ✅ | ✅ | Used by renderer/layout; UI does not expose every mode. |
|
| Text auto resize | ✅ | ✅ | ◐ | ✅ | ✅ | Used by renderer/layout; UI does not expose every mode. |
|
||||||
| Text truncation / max lines | ✅ | ✅ | — | ✅ | ✅ | Renderer supports ending truncation; no inspector control. |
|
| Text truncation / max lines | ✅ | ✅ | — | ✅ | ✅ | Renderer supports ending truncation; no inspector control. |
|
||||||
|
|
@ -195,7 +195,7 @@ OpenPencil deliberately preserves many Figma/Kiwi fields even when they are not
|
||||||
| Version/sort/publish/library metadata | ↩ | — | ◐ | Assets UI shows a subset; publish/update workflow is missing. |
|
| Version/sort/publish/library metadata | ↩ | — | ◐ | Assets UI shows a subset; publish/update workflow is missing. |
|
||||||
| Variable and parameter consumption maps | ✅ | ◐ | ◐ | Filtered/preserved for safe round-trip; normalized bindings cover common cases. |
|
| Variable and parameter consumption maps | ✅ | ◐ | ◐ | Filtered/preserved for safe round-trip; normalized bindings cover common cases. |
|
||||||
| Page fields: background, page type, guides | ↩ | ◐ | — | Background/page type/guides mostly round-trip. Guides are not rendered/editable. |
|
| Page fields: background, page type, guides | ↩ | ◐ | — | Background/page type/guides mostly round-trip. Guides are not rendered/editable. |
|
||||||
| Text internals: `textData`, layout versions, font version, derived data | ✅ | ✅ | — | Important for text fidelity; most internals are not editable. Imported derived text data, leading trim, decoration style, underline decoration paint/offset/thickness, semantic font metadata, and raw OpenType feature toggles are preserved for round-trip when safe. |
|
| Text internals: `textData`, layout versions, font version, derived data | ✅ | ✅ | — | Important for text fidelity; most internals are not editable. Imported derived text data, leading trim, decoration style, underline decoration paint/offset/thickness, semantic font metadata, and raw OpenType feature toggles are preserved for round-trip when safe; decoration style/thickness/color now render through CanvasKit.
|
||||||
| `fontVariations` | ✅ | ✅ | — | Variable font axes are imported, rendered, and exported for text nodes and style runs. |
|
| `fontVariations` | ✅ | ✅ | — | Variable font axes are imported, rendered, and exported for text nodes and style runs. |
|
||||||
| Raw paint/effect/vector/geometry payloads | ✅ | ✅ | ◐ | Converted fields render; raw payloads preserve Figma import/export details, including mask, background paint, layout grid, export setting, and prototype interaction metadata where safe. |
|
| Raw paint/effect/vector/geometry payloads | ✅ | ✅ | ◐ | Converted fields render; raw payloads preserve Figma import/export details, including mask, background paint, layout grid, export setting, and prototype interaction metadata where safe. |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,18 @@ describe('Figma font variation export', () => {
|
||||||
const text = graph.createNode('TEXT', page.id, {
|
const text = graph.createNode('TEXT', page.id, {
|
||||||
text: 'Axis',
|
text: 'Axis',
|
||||||
fontVariations: [{ axis: 'wght', value: 650 }],
|
fontVariations: [{ axis: 'wght', value: 650 }],
|
||||||
|
textDecoration: 'UNDERLINE',
|
||||||
|
textDecorationStyle: 'WAVY',
|
||||||
|
textDecorationThickness: 1.5,
|
||||||
|
textDecorationFills: [
|
||||||
|
{
|
||||||
|
type: 'SOLID',
|
||||||
|
color: { r: 1, g: 0, b: 0, a: 1 },
|
||||||
|
opacity: 1,
|
||||||
|
visible: true,
|
||||||
|
blendMode: 'NORMAL'
|
||||||
|
}
|
||||||
|
],
|
||||||
fontFeatures: [
|
fontFeatures: [
|
||||||
{ tag: 'LIGA', enabled: false },
|
{ tag: 'LIGA', enabled: false },
|
||||||
{ tag: 'DLIG', enabled: true },
|
{ tag: 'DLIG', enabled: true },
|
||||||
|
|
@ -24,7 +36,10 @@ describe('Figma font variation export', () => {
|
||||||
fontFeatures: [
|
fontFeatures: [
|
||||||
{ tag: 'CALT', enabled: false },
|
{ tag: 'CALT', enabled: false },
|
||||||
{ tag: 'SS01', enabled: true }
|
{ tag: 'SS01', enabled: true }
|
||||||
]
|
],
|
||||||
|
textDecoration: 'UNDERLINE',
|
||||||
|
textDecorationStyle: 'DOTTED',
|
||||||
|
textDecorationThickness: 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -36,6 +51,9 @@ describe('Figma font variation export', () => {
|
||||||
expect(nodeChange.fontVariations).toEqual([
|
expect(nodeChange.fontVariations).toEqual([
|
||||||
{ axisTag: 0x77676874, axisName: 'wght', value: 650 }
|
{ axisTag: 0x77676874, axisName: 'wght', value: 650 }
|
||||||
])
|
])
|
||||||
|
expect(nodeChange.textDecorationStyle).toBe('WAVY')
|
||||||
|
expect(nodeChange.textDecorationThickness).toEqual({ value: 1.5, units: 'PIXELS' })
|
||||||
|
expect(nodeChange.textDecorationFillPaints?.[0]?.type).toBe('SOLID')
|
||||||
expect(nodeChange.fontVariantCommonLigatures).toBe(false)
|
expect(nodeChange.fontVariantCommonLigatures).toBe(false)
|
||||||
expect(nodeChange.fontVariantContextualLigatures).toBe(true)
|
expect(nodeChange.fontVariantContextualLigatures).toBe(true)
|
||||||
expect(nodeChange.toggledOnOTFeatures).toEqual(['DLIG'])
|
expect(nodeChange.toggledOnOTFeatures).toEqual(['DLIG'])
|
||||||
|
|
@ -45,5 +63,10 @@ describe('Figma font variation export', () => {
|
||||||
])
|
])
|
||||||
expect(nodeChange.textData?.styleOverrideTable?.[0]?.fontVariantContextualLigatures).toBe(false)
|
expect(nodeChange.textData?.styleOverrideTable?.[0]?.fontVariantContextualLigatures).toBe(false)
|
||||||
expect(nodeChange.textData?.styleOverrideTable?.[0]?.toggledOnOTFeatures).toEqual(['SS01'])
|
expect(nodeChange.textData?.styleOverrideTable?.[0]?.toggledOnOTFeatures).toEqual(['SS01'])
|
||||||
|
expect(nodeChange.textData?.styleOverrideTable?.[0]?.textDecorationStyle).toBe('DOTTED')
|
||||||
|
expect(nodeChange.textData?.styleOverrideTable?.[0]?.textDecorationThickness).toEqual({
|
||||||
|
value: 2,
|
||||||
|
units: 'PIXELS'
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,29 @@ describe('Figma font variation import', () => {
|
||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('imports text decoration style metadata', () => {
|
||||||
|
const props = nodeChangeToProps(
|
||||||
|
{
|
||||||
|
type: 'TEXT',
|
||||||
|
textData: { characters: 'Decorated' },
|
||||||
|
textDecoration: 'UNDERLINE',
|
||||||
|
textDecorationStyle: 'WAVY',
|
||||||
|
textDecorationThickness: { value: 1.5, units: 'PIXELS' },
|
||||||
|
textDecorationFillPaints: [
|
||||||
|
{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 0.75 }
|
||||||
|
]
|
||||||
|
} as NodeChange,
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(props).toMatchObject({
|
||||||
|
textDecoration: 'UNDERLINE',
|
||||||
|
textDecorationStyle: 'WAVY',
|
||||||
|
textDecorationThickness: 1.5,
|
||||||
|
textDecorationFills: [{ type: 'SOLID', color: { r: 1, g: 0, b: 0, a: 1 }, opacity: 0.75 }]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test('imports styled-run variable font axes and OpenType feature toggles', () => {
|
test('imports styled-run variable font axes and OpenType feature toggles', () => {
|
||||||
const runs = importStyleRuns({
|
const runs = importStyleRuns({
|
||||||
type: 'TEXT',
|
type: 'TEXT',
|
||||||
|
|
@ -56,7 +79,10 @@ describe('Figma font variation import', () => {
|
||||||
styleID: 1,
|
styleID: 1,
|
||||||
fontVariations: [{ axisName: 'GRAD', value: -50 }],
|
fontVariations: [{ axisName: 'GRAD', value: -50 }],
|
||||||
fontVariantCommonLigatures: false,
|
fontVariantCommonLigatures: false,
|
||||||
toggledOnOTFeatures: ['SS01']
|
toggledOnOTFeatures: ['SS01'],
|
||||||
|
textDecoration: 'UNDERLINE',
|
||||||
|
textDecorationStyle: 'DOTTED',
|
||||||
|
textDecorationThickness: { value: 2, units: 'PIXELS' }
|
||||||
} as NodeChange
|
} as NodeChange
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -71,7 +97,10 @@ describe('Figma font variation import', () => {
|
||||||
fontFeatures: [
|
fontFeatures: [
|
||||||
{ tag: 'LIGA', enabled: false },
|
{ tag: 'LIGA', enabled: false },
|
||||||
{ tag: 'SS01', enabled: true }
|
{ tag: 'SS01', enabled: true }
|
||||||
]
|
],
|
||||||
|
textDecoration: 'UNDERLINE',
|
||||||
|
textDecorationStyle: 'DOTTED',
|
||||||
|
textDecorationThickness: 2
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,23 @@
|
||||||
import { describe, expect, test } from 'bun:test'
|
import { describe, expect, test } from 'bun:test'
|
||||||
|
|
||||||
import { textFontFeatures, textFontVariations } from '#core/canvas/text'
|
import { textDecorationStyleValue, textFontFeatures, textFontVariations } from '#core/canvas/text'
|
||||||
|
|
||||||
describe('canvas text font variations', () => {
|
describe('canvas text font variations', () => {
|
||||||
|
test('maps imported text decoration styles to CanvasKit', () => {
|
||||||
|
const ck = {
|
||||||
|
DecorationStyle: {
|
||||||
|
Solid: 'solid',
|
||||||
|
Dotted: 'dotted',
|
||||||
|
Wavy: 'wavy'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(textDecorationStyleValue(ck, 'SOLID')).toBe('solid')
|
||||||
|
expect(textDecorationStyleValue(ck, 'DOTTED')).toBe('dotted')
|
||||||
|
expect(textDecorationStyleValue(ck, 'WAVY')).toBe('wavy')
|
||||||
|
expect(textDecorationStyleValue(ck, undefined)).toBe('solid')
|
||||||
|
})
|
||||||
|
|
||||||
test('passes imported variable font axes to CanvasKit text styles', () => {
|
test('passes imported variable font axes to CanvasKit text styles', () => {
|
||||||
expect(
|
expect(
|
||||||
textFontVariations([
|
textFontVariations([
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue