const InkscapeNS = "http://www.inkscape.org/namespaces/inkscape"; const N = "(-?[\\d.eE+]+)"; const S = "[,\\s]+"; const TranslateRE = new RegExp( `translate\\(\\s*${ N }${ S }${ N }\\s*\\)` ); const MatrixRE = new RegExp( `matrix\\(\\s*${ N }${ S }${ N }${ S }${ N }${ S }${ N }${ S }${ N }${ S }${ N }\\s*\\)` ); const RotateRE = new RegExp( `rotate\\(\\s*${ N }(?:${ S }${ N }${ S }${ N })?\\s*\\)` ); function applyTransformStr( s: string, x: number, y: number ): [ number, number ] { if ( null == s || "" === s ) { return [ x, y ]; } let m = s.match( TranslateRE ); if ( null != m ) { return [ x + parseFloat( m[ 1 ] ), y + parseFloat( m[ 2 ] ) ]; } m = s.match( MatrixRE ); if ( null != m ) { const a = parseFloat( m[ 1 ] ), b = parseFloat( m[ 2 ] ); const c = parseFloat( m[ 3 ] ), d = parseFloat( m[ 4 ] ); const e = parseFloat( m[ 5 ] ), f = parseFloat( m[ 6 ] ); return [ a * x + c * y + e, b * x + d * y + f ]; } m = s.match( RotateRE ); if ( null != m ) { const angle = parseFloat( m[ 1 ] ) * Math.PI / 180; const cos = Math.cos( angle ); const sin = Math.sin( angle ); const cx = null != m[ 2 ] ? parseFloat( m[ 2 ] ) : 0; const cy = null != m[ 3 ] ? parseFloat( m[ 3 ] ) : 0; const dx = x - cx; const dy = y - cy; return [ cx + dx * cos - dy * sin, cy + dx * sin + dy * cos ]; } return [ x, y ]; } export class BoneData { group: SVGGElement = null; name: string = ""; restTransform: string = ""; pivotLocalX: number = 0; pivotLocalY: number = 0; } export class RojoAnimator { svgRoot: SVGSVGElement = null; Bones: Map = new Map(); static create( svgRoot: SVGSVGElement ) { let ra = new RojoAnimator(); ra.svgRoot = svgRoot; ra._processBones(); ra._stripInkscape(); return ra; } _label( el: Element ) { return el.getAttributeNS( InkscapeNS, "label" ) ?? ""; } _processBones() { const groups = Array.from( this.svgRoot.querySelectorAll( "g" ) ); for ( const g of groups ) { const label = this._label( g ); if ( !label.endsWith( "-transform" ) ) { continue; } const name = label.slice( 0, label.length - "-transform".length ); const restTransform = g.getAttribute( "transform" ) ?? ""; const pivotLabel = name + "-pivot"; let pivotCircle: Element = null; for ( const child of Array.from( g.children ) ) { if ( "circle" === child.localName && pivotLabel === this._label( child ) ) { pivotCircle = child; break; } } if ( null == pivotCircle ) { continue; } const cx = parseFloat( pivotCircle.getAttribute( "cx" ) ?? "0" ); const cy = parseFloat( pivotCircle.getAttribute( "cy" ) ?? "0" ); const pivotTransform = pivotCircle.getAttribute( "transform" ) ?? ""; const [ lx, ly ] = applyTransformStr( pivotTransform, cx, cy ); pivotCircle.remove(); const bone = new BoneData(); bone.group = g as SVGGElement; bone.name = name; bone.restTransform = restTransform; bone.pivotLocalX = lx; bone.pivotLocalY = ly; this.Bones.set( name, bone ); } } _stripInkscape() { const namedview = Array.from( this.svgRoot.children ) .find( el => "namedview" === el.localName ); if ( null != namedview ) { namedview.remove(); } const all: Element[] = [ this.svgRoot, ...Array.from( this.svgRoot.querySelectorAll( "*" ) ) ]; for ( const el of all ) { const toRemove: string[] = []; for ( let i = 0; i < el.attributes.length; i++ ) { const { name } = el.attributes[ i ]; if ( name.startsWith( "inkscape:" ) || name.startsWith( "sodipodi:" ) ) { toRemove.push( name ); } } for ( const attr of toRemove ) { el.removeAttribute( attr ); } } } setBoneAngle( name: string, degrees: number ) { const bone = this.Bones.get( name ); if ( null == bone ) { return; } const rest = bone.restTransform; const lx = bone.pivotLocalX; const ly = bone.pivotLocalY; const rotation = `translate(${ lx },${ ly }) rotate(${ degrees }) translate(${ -lx },${ -ly })`; const transform = "" === rest ? rotation : `${ rest } ${ rotation }`; bone.group.setAttribute( "transform", transform ); } }