From 7b0c01560c082f7c6286913c1d1ef5289fe5f78e Mon Sep 17 00:00:00 2001 From: Rokojori Date: Mon, 6 Jul 2026 22:05:27 +0200 Subject: [PATCH] history: rojo chibi bone animator prototype Co-Authored-By: Claude Sonnet 4.6 --- src/rojos/rojo-animator.ts | 164 ++++++++++ src/rojos/rojo-base.svg | 298 ++++++++++++++++++ .../history/2026/07-July/06-Monday/index.html | 50 ++- workspace/history/index.html | 2 +- workspace/index.html | 28 +- 5 files changed, 535 insertions(+), 7 deletions(-) create mode 100644 src/rojos/rojo-animator.ts create mode 100644 src/rojos/rojo-base.svg diff --git a/src/rojos/rojo-animator.ts b/src/rojos/rojo-animator.ts new file mode 100644 index 0000000..c20f52a --- /dev/null +++ b/src/rojos/rojo-animator.ts @@ -0,0 +1,164 @@ +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 ); + } +} diff --git a/src/rojos/rojo-base.svg b/src/rojos/rojo-base.svg new file mode 100644 index 0000000..2fd4ca2 --- /dev/null +++ b/src/rojos/rojo-base.svg @@ -0,0 +1,298 @@ + + + + diff --git a/workspace/history/2026/07-July/06-Monday/index.html b/workspace/history/2026/07-July/06-Monday/index.html index 813ca5e..6d03a78 100644 --- a/workspace/history/2026/07-July/06-Monday/index.html +++ b/workspace/history/2026/07-July/06-Monday/index.html @@ -16,7 +16,8 @@

File tree rename/delete, locale generator rewrite, locale documentation, doc/ renamed to workspace/, fixed navigation header system, - Guides/Actions workspace restructure, and Update History action rewrite. + Guides/Actions workspace restructure, Update History action rewrite, + and Rojo chibi bone animator prototype.

@@ -181,8 +182,6 @@ - -

Update History Action: Commit and Push

@@ -205,6 +204,44 @@

+
+

Rojo Chibi Bone Animator Prototype

+

+ A 2D skeletal animation prototype was started for an in-app chibi character + intended to accompany AI agent interactions. The source artwork is an Inkscape + SVG (src/rojos/rojo-base.svg) where each body part is a + <g> labelled *-transform and contains a + <circle> labelled *-pivot that marks the + rotation point. The pivot circle carries an inverse transform so its + cx/cy sits at the exact world-space pivot regardless of the + bone's own transform. +

+

+ src/rojos/rojo-animator.ts provides RojoAnimator, + which walks all *-transform groups on load, applies the pivot + circle's transform to its cx/cy to recover the local-space pivot, + removes the circles, and strips Inkscape namespaced attributes. At runtime, + setBoneAngle(name, degrees) writes + restTransform translate(lx,ly) rotate(θ) translate(-lx,-ly) + directly onto the group element — no graphics library required. The parser + handles all three transform types present in the SVG: + translate, matrix, and rotate(angle,cx,cy). +

+

+ public/rojos/rojo-demo.html fetches the SVG, injects it into + the page, and runs a requestAnimationFrame loop with three + switchable animations: Idle (breathing sway), Walk + (legs and arms in opposite phase), and Wave (right arm wave). +

+
+ src/rojos/rojo-animator.ts + public/rojos/rojo-demo.html + public/rojos/rojo-base.svg + SVG bone animation + no graphics library +
+
+
@@ -292,7 +329,12 @@ server/localeGenerator.ts — complete rewrite (per-directory output)
src/locales/generated/ — new: generated locale classes
src/locales/LocaleManager.tsstatic $ changed to static get $
- CLAUDE.md — updated to point to workspace/outline/index.html + CLAUDE.md — updated to point to workspace/outline/index.html
+ workspace/index.html — updated: added Guides and Actions cards
+ src/rojos/rojo-base.svg — new: Inkscape chibi character with bone/pivot structure
+ src/rojos/rojo-animator.ts — new: SVG bone animator (pivot parser + setBoneAngle)
+ public/rojos/rojo-base.svg — new: copy of SVG for static serving
+ public/rojos/rojo-demo.html — new: interactive animation demo (idle/walk/wave)

diff --git a/workspace/history/index.html b/workspace/history/index.html index 07b6fd1..7358173 100644 --- a/workspace/history/index.html +++ b/workspace/history/index.html @@ -21,7 +21,7 @@

Monday, 6 July 2026

-

File tree rename/delete (right-click context menu), locale generator rewrite (per-directory output, new naming conventions), locale documentation, doc/ renamed to workspace/, fixed navigation header system, Guides/Actions workspace restructure, Update History action rewrite (commit/push with confirmation).

+

File tree rename/delete (right-click context menu), locale generator rewrite (per-directory output, new naming conventions), locale documentation, doc/ renamed to workspace/, fixed navigation header system, Guides/Actions workspace restructure, Update History action rewrite, Rojo chibi bone animator prototype (SVG pivot system, no graphics library).

diff --git a/workspace/index.html b/workspace/index.html index ead380d..f1a49b4 100644 --- a/workspace/index.html +++ b/workspace/index.html @@ -22,14 +22,33 @@

Outline

- Explains Roject's goals, features and future plans and actions that are used for - the project. + Explains Roject's goals, features and future plans for the project.
Read more about the outline

+
+

Guides

+

+ Context, rules, workflows and examples for how to do things. +
+ + Read more about the guides +

+
+ +
+

Actions

+

+ Named tasks and steps for recurring topics. +
+ + Read more about the actions +

+
+

History

@@ -40,6 +59,11 @@

+ +
+ To get the full picture, follow each page (as human or agent). +
+