openpencil/packages/dom-css/src/css-values.ts
2026-06-30 10:48:13 +03:00

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
}