165 lines
4.3 KiB
TypeScript
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 );
|
||
|
|
}
|
||
|
|
}
|