109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
import { colorToCSS, parseColor } from '@open-pencil/core/color'
|
|
import type { Effect, Fill, SceneNode, Stroke } from '@open-pencil/core/scene-graph'
|
|
import type { Color } from '@open-pencil/core/types'
|
|
|
|
import type { DesignStyleDeclaration } from './types'
|
|
|
|
const TRANSPARENT_KEYWORDS = new Set(['transparent', 'rgba(0, 0, 0, 0)', 'rgb(0 0 0 / 0)'])
|
|
|
|
export function parseCSSNumber(value: string | undefined): number | null {
|
|
if (!value) return null
|
|
const trimmed = value.trim()
|
|
if (trimmed.length === 0 || trimmed === 'auto') return null
|
|
const parsed = Number.parseFloat(trimmed)
|
|
if (!Number.isFinite(parsed)) return null
|
|
return trimmed.endsWith('rem') ? parsed * 16 : parsed
|
|
}
|
|
|
|
export function parseCSSColor(value: string | undefined): Color | null {
|
|
if (!value) return null
|
|
const trimmed = value.trim()
|
|
if (trimmed.length === 0 || TRANSPARENT_KEYWORDS.has(trimmed.toLowerCase())) return null
|
|
return parseColor(trimmed)
|
|
}
|
|
|
|
export function fillToCSS(fill: Fill | undefined): string | undefined {
|
|
if (fill?.type !== 'SOLID' || !fill.visible) return undefined
|
|
return colorToCSS({ ...fill.color, a: fill.opacity })
|
|
}
|
|
|
|
export function colorToFillFromCSS(value: string | undefined): Fill[] {
|
|
const color = parseCSSColor(value)
|
|
if (!color) return []
|
|
return [{ type: 'SOLID', color, opacity: color.a, visible: true }]
|
|
}
|
|
|
|
export function strokeColorToCSS(stroke: Stroke | undefined): string | undefined {
|
|
if (!stroke?.visible) return undefined
|
|
return colorToCSS({ ...stroke.color, a: stroke.opacity })
|
|
}
|
|
|
|
export function strokeToCSS(stroke: Stroke | undefined): string | undefined {
|
|
const color = strokeColorToCSS(stroke)
|
|
if (!color || !stroke) return undefined
|
|
return `${stroke.weight}px solid ${color}`
|
|
}
|
|
|
|
export function colorToStrokeFromCSS(
|
|
colorValue: string | undefined,
|
|
weightValue: string | undefined
|
|
): Stroke[] {
|
|
const color = parseCSSColor(colorValue)
|
|
const weight = parseCSSNumber(weightValue)
|
|
if (!color || weight === null || weight <= 0) return []
|
|
return [{ color, weight, opacity: color.a, visible: true, align: 'INSIDE' }]
|
|
}
|
|
|
|
export function dropShadowToCSS(effect: Effect | undefined): string | undefined {
|
|
if (effect?.type !== 'DROP_SHADOW' || !effect.visible) return undefined
|
|
return `${effect.offset.x}px ${effect.offset.y}px ${effect.radius}px ${effect.spread}px ${colorToCSS({ ...effect.color, a: effect.color.a })}`
|
|
}
|
|
|
|
export function dropShadowFromCSS(value: string | undefined): Effect[] {
|
|
if (!value || value.trim() === 'none') return []
|
|
|
|
const colorMatch = value.match(
|
|
/(?:rgba?|hsla?)\([^)]*\)|#[\da-fA-F]{3,8}|\b(?:black|white|red|green|blue)\b/
|
|
)
|
|
const color = parseCSSColor(colorMatch?.[0])
|
|
if (!color) return []
|
|
|
|
const numericPart = colorMatch ? value.replace(colorMatch[0], '') : value
|
|
const numbers = numericPart
|
|
.trim()
|
|
.split(/\s+/)
|
|
.map(parseCSSNumber)
|
|
.filter((number): number is number => number !== null)
|
|
const [offsetX = 0, offsetY = 0, radius = 0, spread = 0] = numbers
|
|
|
|
return [
|
|
{
|
|
type: 'DROP_SHADOW',
|
|
color,
|
|
offset: { x: offsetX, y: offsetY },
|
|
radius,
|
|
spread,
|
|
visible: true,
|
|
blendMode: 'NORMAL'
|
|
}
|
|
]
|
|
}
|
|
|
|
export function pickStyle(elementStyle: DesignStyleDeclaration | undefined, property: string) {
|
|
return elementStyle?.[property]
|
|
}
|
|
|
|
export function mergedStyle(node: {
|
|
inlineStyle?: DesignStyleDeclaration
|
|
computedStyle?: DesignStyleDeclaration
|
|
}) {
|
|
return { ...node.inlineStyle, ...node.computedStyle }
|
|
}
|
|
|
|
export function sceneNodeSizeStyle(node: SceneNode): DesignStyleDeclaration {
|
|
const style: DesignStyleDeclaration = {}
|
|
if (node.width > 0) style.width = `${node.width}px`
|
|
if (node.height > 0) style.height = `${node.height}px`
|
|
return style
|
|
}
|