rojects/source/rojos/rojo-animator.ts

165 lines
4.3 KiB
TypeScript

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<string, BoneData> = 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 );
}
}