import {embedUrlRefs, parseFileExtension, svgUrl} from 'ts-browser-helpers'
import {uiDropdown, uiFolderContainer, uiInput, UiObjectConfig, uiSlider, uiToggle} from 'uiconfig.js'
import {IAssetImporter} from '../assetmanager'
import {LinearFilter} from 'three'
import {ITexture} from '../core'
import {DataUrlLoader} from '../assetmanager/import/DataUrlLoader'
import {isNonRelativeUrl} from './browser-helpers'
export interface ITextSVGOptions{
text: string;
fontFamily?: string;
fontPath?: string;
svgBackground?: string;
xOffset?: number; yOffset?: number;
width?: number; height?: number;
boxWidth?: number; boxHeight?: number;
fontSize?: number;
fontWeight?: string | number;
fontStyle?: 'normal' | 'italic' | 'oblique';
lineHeight?: string | number;
letterSpacing?: string | number;
whiteSpace?: 'normal' | 'pre' | 'nowrap' | 'pre-wrap' | 'pre-line';
direction?: 'auto' | 'ltr' | 'rtl';
maskText?: boolean; innerShadow?: boolean;
bgFillColor?: string;
textColor?: string;
textAnchor?: 'start' | 'middle' | 'end';
style?: string;
}
const onOpsChange = (ctx: TextSVGOptions)=>({
onChange: (ev: any)=>{
if (!ev.last) return
ctx.onChange()
},
})
@uiFolderContainer('Text SVG Options')
export class TextSVGOptions implements ITextSVGOptions {
@uiInput('Text', onOpsChange) text = 'Custom Text'
@uiSlider('Font Size', [2, 1024], 1, onOpsChange) fontSize = 100
@uiSlider('Width', [2, 4096], 1, onOpsChange) width = 1024
@uiSlider('Height', [2, 4096], 1, onOpsChange) height = 1024
@uiSlider('X Offset', [-1024, 1024], 1, onOpsChange) xOffset = 0
@uiSlider('Y Offset', [-1024, 1024], 1, onOpsChange) yOffset = 0
@uiSlider('V-Width', [2, 4096], 1, onOpsChange) boxWidth = 1024
@uiSlider('V-Height', [2, 4096], 1, onOpsChange) boxHeight = 1024
@uiDropdown('Text Anchor', ['start', 'middle', 'end'].map(label=>({label} as UiObjectConfig)), onOpsChange) textAnchor: 'start'|'middle'|'end' = 'middle'
@uiInput('Font', onOpsChange) fontFamily = ''
@uiInput('Font Url', onOpsChange) fontPath = ''
@uiInput('Font Weight', onOpsChange) fontWeight: string | number = 'normal'
@uiDropdown('Font Style', ['normal', 'italic', 'oblique'].map(label=>({label} as UiObjectConfig)), onOpsChange) fontStyle: 'normal'|'italic'|'oblique' = 'normal'
@uiInput('Line Height', onOpsChange) lineHeight: string | number = 'normal'
@uiInput('Letter Spacing', onOpsChange) letterSpacing: string | number = 'normal'
@uiDropdown('White Space', ['normal', 'pre', 'nowrap', 'pre-wrap', 'pre-line'].map(label=>({label} as UiObjectConfig)), onOpsChange) whiteSpace: 'normal'|'pre'|'nowrap'|'pre-wrap'|'pre-line' = 'normal'
@uiDropdown('Direction', ['auto', 'ltr', 'rtl'].map(label=>({label} as UiObjectConfig)), onOpsChange) direction: 'ltr'|'rtl' = 'ltr'
@uiToggle('Mask Text', onOpsChange) maskText = false
@uiToggle('Inner Shadow', onOpsChange) innerShadow = false
@uiInput('Text Color', onOpsChange) textColor = '#000000'
@uiInput('BG Fill', onOpsChange) bgFillColor = '#ffffff'
@uiInput('SVG BG', onOpsChange) svgBackground = '#ffffff'
onChange = ()=>{return}
set(ops: ITextSVGOptions) {
Object.assign(this, ops)
}
reset() {
const oc = this.onChange
Object.assign(this, new TextSVGOptions())
this.onChange = oc
}
toJSON() {
return {
text: this.text,
fontFamily: this.fontFamily,
fontPath: this.fontPath,
svgBackground: this.svgBackground,
width: this.width,
height: this.height,
xOffset: this.xOffset,
yOffset: this.yOffset,
boxWidth: this.boxWidth,
boxHeight: this.boxHeight,
fontSize: this.fontSize,
fontWeight: this.fontWeight,
fontStyle: this.fontStyle,
lineHeight: this.lineHeight,
letterSpacing: this.letterSpacing,
whiteSpace: this.whiteSpace,
direction: this.direction,
maskText: this.maskText,
innerShadow: this.innerShadow,
bgFillColor: this.bgFillColor,
textColor: this.textColor,
textAnchor: this.textAnchor,
}
}
declare uiConfig: UiObjectConfig
}
export const fontFormatExtensionMap: any = {
'woff': 'woff',
'woff2': 'woff2',
'ttf': 'truetype',
'otf': 'opentype',
'eot': 'embedded-opentype',
}
export function buildTextSvg({
text = 'Custom Text',
svgBackground = '#ffffff',
xOffset = 0, yOffset = 0,
width = 1024, height = 1024,
boxWidth = 1024, boxHeight = 1024,
fontFamily = '', fontSize = 32,
fontWeight = 'normal',
fontStyle = 'normal',
lineHeight = 'normal',
letterSpacing = 'normal',
whiteSpace = 'normal',
direction = 'auto',
maskText = false, innerShadow = false,
bgFillColor = '#000000', textColor = '#ffffff',
textAnchor = 'middle',
style = '',
}: ITextSVGOptions) {
// noinspection CssInvalidPropertyValue
const s = `
`
return s
}
/**
* List of font names and paths to font files.
*/
const fonts: Record = {
roboto: 'https://fonts.gstatic.com/s/roboto/v30/KFOmCnqEu92Fr1Mu4mxKKTU1Kg.woff2',
}
export async function makeTextSvgAdvanced(options: ITextSVGOptions, importer: IAssetImporter) {
const fontFamily = options.fontFamily || 'Arial'
let fontPath = options.fontPath || fonts[fontFamily] || ''
let style = options.style || ''
if (fontPath.length > 0) {
if (!isNonRelativeUrl(fontPath) && !fontPath.startsWith('blob:') && !fontPath.startsWith('ftp:') && globalThis.window) {
// assume relative path to current url window.location
const url = new URL(fontPath, window.location.href)
fontPath = url.href
}
const fontExt = parseFileExtension(fontPath) || 'woff'
style += '\n' +
(fontPath.length > 0 ? `
@font-face {
font-family: ${JSON.stringify(fontFamily)};
src: url(${fontPath}) format(${fontFormatExtensionMap[fontExt] || fontExt});
}` : '')
}
let svg = buildTextSvg({
...options,
fontFamily,
style,
})
svg = await embedUrlRefs(svg, async(p)=>getAssetData(p, importer))
svg = svgUrl(svg)
// const svgTex = await new SVGTextureLoader().loadAsync(svg)
const svgTex = await importer.importSingle(svg)
if (!svgTex) return null
svgTex.generateMipmaps = false
svgTex.minFilter = LinearFilter
// svgTex._isSVGTexture = true
svgTex.flipY = true
svgTex.needsUpdate = true
return svgTex
}
const assetLoadOptions = undefined
async function getAssetData(path: string, importer: IAssetImporter) {
if (path.startsWith('http://www.w3.org')) return path
if (!importer) throw new Error('no importer')
const assetLoadOptions1 = assetLoadOptions || {
fileHandler: new DataUrlLoader(importer.loadingManager),
processRaw: false,
}
try {
const assetData = (await importer.importSingle(path, assetLoadOptions1)) as any as string
// console.log(asset, assetData, JSON.stringify(this.assetLoadOptions1))
return assetData
} catch (e) {
console.error(e)
return ''
}
}