UNPKG

threepipe

Version:

A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.

245 lines (225 loc) 9.37 kB
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 = ` <svg style="background-color:${svgBackground}" width="${width}" height="${height}" viewBox="0 0 ${boxWidth} ${boxHeight}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <style> ${style} .text-g{ overflow:hidden; text-anchor: ${textAnchor}; font-size: ${fontSize}px; font-family: ${JSON.stringify(fontFamily || 'Arial')}; font-weight: ${fontWeight}; font-style: ${fontStyle}; line-height: ${lineHeight}; letter-spacing: ${letterSpacing}; white-space: ${whiteSpace}; direction: ${direction}; } </style> </defs> <g class="text-g"> <defs> ` + (maskText ? ` <mask id="textMask"> <text style="fill:white; font-size: ${fontSize}px; font-weight: ${fontWeight}; font-style: ${fontStyle}; line-height: ${lineHeight}; letter-spacing: ${letterSpacing}; white-space: ${whiteSpace}; direction: ${direction};" x="${xOffset + boxWidth / 2}" y="${boxHeight / 2 + yOffset + fontSize / 4}" > ${text} </text> </mask> ` : '') + ` ` + (innerShadow ? ` <filter id="innerShadow" x="-20%" y="-20%" width="140%" height="140%"> <feGaussianBlur in="SourceGraphic" stdDeviation="0.5" result="blur"/> <feOffset in="blur" dx="1.5" dy="1.5"/> </filter> ` : '') + ` </defs> ` + (maskText ? ` <g mask="url(#textMask)"> ` : '') + ` <rect x="0" y="0" width="${boxWidth}" height="${boxHeight}" style="fill:${bgFillColor}"/> <text style="${innerShadow ? 'filter: url(#innerShadow);' : ''} fill:${textColor}; font-weight: ${fontWeight}; font-style: ${fontStyle}; line-height: ${lineHeight}; letter-spacing: ${letterSpacing}; white-space: ${whiteSpace}; direction: ${direction};" x="${xOffset + boxWidth / 2}" y="${boxHeight / 2 + yOffset + fontSize / 4}"> ${text} </text> ` + (maskText ? ` </g> ` : '') + ` </g> </svg> ` return s } /** * List of font names and paths to font files. */ const fonts: Record<string, string> = { 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<ITexture>(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 '' } }