@@ -571,6 +571,21 @@ static INLINE const char* _dup_cstr(const char* s)
571571 return d;
572572}
573573
574+ /* Compute the active duration for an anim item (used by restart logic). */
575+ static INLINE float _anim_item_active_dur (const psx_svg_anim_item* it)
576+ {
577+ if (!it || it->dur_sec <= 0 .0f ) {
578+ return 0 .0f ;
579+ }
580+ if (it->repeat_dur_sec > 0 .0f ) {
581+ return it->repeat_dur_sec ;
582+ }
583+ if (it->repeat_count == 0 ) {
584+ return 1e30f; /* indefinite */
585+ }
586+ return it->dur_sec * (float )it->repeat_count ;
587+ }
588+
574589static INLINE float _anim_item_begin_for_time (const psx_svg_anim_item* it, float doc_t )
575590{
576591 if (!it) {
@@ -584,9 +599,54 @@ static INLINE float _anim_item_begin_for_time(const psx_svg_anim_item* it, float
584599 if (psx_array_empty ((psx_array*)&it->begins_sec )) {
585600 return it->begin_sec ;
586601 }
587- // choose latest begin <= doc_t
588- float best = -1 .0f ;
602+
589603 const uint32_t n = psx_array_size ((psx_array*)&it->begins_sec );
604+
605+ // restart="never": only use the first (earliest) begin.
606+ if (it->restart_mode == SVG_ANIMATION_RESTART_NEVER ) {
607+ const float * fp = psx_array_get ((psx_array*)&it->begins_sec , 0 , float );
608+ return fp ? *fp : 0 .0f ;
609+ }
610+
611+ // restart="whenNotActive": iterate forwards, skip begins that fall
612+ // within a previous begin's active interval.
613+ if (it->restart_mode == SVG_ANIMATION_RESTART_WHEN_NOT_ACTIVE ) {
614+ float ad = _anim_item_active_dur (it);
615+ // Build valid begins by forward scan: a begin is valid only if it
616+ // does not fall within the active interval of the current valid begin.
617+ float cur_valid = -1 .0f ;
618+ float best = -1 .0f ;
619+ for (uint32_t i = 0 ; i < n; i++) {
620+ const float * bp = psx_array_get ((psx_array*)&it->begins_sec , i, float );
621+ float b = bp ? *bp : 0 .0f ;
622+ if (cur_valid < 0 .0f || b >= cur_valid + ad) {
623+ // This begin is valid (not inside a previous active interval).
624+ cur_valid = b;
625+ if (b <= doc_t && b > best) {
626+ best = b;
627+ }
628+ }
629+ // else: b falls within cur_valid's active interval => skip
630+ }
631+ if (best < 0 .0f ) {
632+ // all valid begins are in the future: return earliest valid
633+ cur_valid = -1 .0f ;
634+ for (uint32_t i = 0 ; i < n; i++) {
635+ const float * bp = psx_array_get ((psx_array*)&it->begins_sec , i, float );
636+ float b = bp ? *bp : 0 .0f ;
637+ if (cur_valid < 0 .0f || b >= cur_valid + ad) {
638+ cur_valid = b;
639+ return b;
640+ }
641+ }
642+ const float * fp = psx_array_get ((psx_array*)&it->begins_sec , 0 , float );
643+ return fp ? *fp : 0 .0f ;
644+ }
645+ return best;
646+ }
647+
648+ // restart="always" (default): choose latest begin <= doc_t
649+ float best = -1 .0f ;
590650 for (uint32_t i = 0 ; i < n; i++) {
591651 const float * bp = psx_array_get ((psx_array*)&it->begins_sec , i, float );
592652 float b = bp ? *bp : 0 .0f ;
@@ -677,6 +737,7 @@ static INLINE bool _anim_resolve_local_t(const psx_svg_anim_item* it, float doc_
677737
678738 float total = 0 .0f ;
679739 bool has_total = false ;
740+ float raw_total = 0 .0f ;
680741 if (it->repeat_dur_sec > 0 .0f ) {
681742 total = it->repeat_dur_sec ;
682743 has_total = true ;
@@ -686,6 +747,24 @@ static INLINE bool _anim_resolve_local_t(const psx_svg_anim_item* it, float doc_
686747 total = it->dur_sec * (float )it->repeat_count ;
687748 has_total = true ;
688749 }
750+ raw_total = total;
751+
752+ // Apply min/max active duration constraints (SVG Tiny 1.2 §16.3.3).
753+ // min > max => both ignored.
754+ if (has_total) {
755+ float min_s = it->min_sec ;
756+ float max_s = it->max_sec ;
757+ if (min_s > 0 .0f && max_s > 0 .0f && min_s > max_s) {
758+ // ignore both
759+ } else {
760+ if (max_s > 0 .0f && total > max_s) {
761+ total = max_s;
762+ }
763+ if (min_s > 0 .0f && total < min_s) {
764+ total = min_s;
765+ }
766+ }
767+ }
689768
690769 if (!has_total) {
691770 local = _anim_fmod (local, it->dur_sec );
@@ -696,6 +775,14 @@ static INLINE bool _anim_resolve_local_t(const psx_svg_anim_item* it, float doc_
696775 } else {
697776 return false ;
698777 }
778+ } else if (raw_total > 0 .0f && local >= raw_total) {
779+ // In min-extended region: past original duration but within min.
780+ // fill=freeze: hold final value; fill=remove: no override.
781+ if (it->fill_mode == SVG_ANIMATION_FREEZE ) {
782+ local = it->dur_sec ;
783+ } else {
784+ return false ;
785+ }
699786 } else {
700787 local = _anim_fmod (local, it->dur_sec );
701788 }
@@ -968,14 +1055,23 @@ static INLINE bool _anim_eval_simple(const psx_svg_anim_item* it, float doc_t, f
9681055
9691056 const psx_svg_attr* afrom = _find_attr (it->anim_node , SVG_ATTR_FROM );
9701057 const psx_svg_attr* ato = _find_attr (it->anim_node , SVG_ATTR_TO );
971- if (!afrom || !ato) {
972- return false ;
1058+ if (afrom && ato) {
1059+ float v0 = _attr_as_number (afrom);
1060+ float v1 = _attr_as_number (ato);
1061+ *out_v = _anim_lerp (v0, v1, t);
1062+ return true ;
9731063 }
9741064
975- float v0 = _attr_as_number (afrom);
976- float v1 = _attr_as_number (ato);
977- *out_v = _anim_lerp (v0, v1, t);
978- return true ;
1065+ // by attribute fallback: by alone => from=0 to=by; from+by => to=from+by
1066+ const psx_svg_attr* aby = _find_attr (it->anim_node , SVG_ATTR_BY );
1067+ if (aby && !ato) {
1068+ float by_val = _attr_as_number (aby);
1069+ float from_val = afrom ? _attr_as_number (afrom) : 0 .0f ;
1070+ *out_v = _anim_lerp (from_val, from_val + by_val, t);
1071+ return true ;
1072+ }
1073+
1074+ return false ;
9791075}
9801076
9811077static INLINE bool _anim_eval_transform_translate_discrete (const psx_svg_anim_item* it, float doc_t ,
@@ -3102,7 +3198,7 @@ static void _collect_anims(psx_svg_player* p, const psx_svg_node* node)
31023198 || node->type () == SVG_TAG_ANIMATE_TRANSFORM || node->type () == SVG_TAG_ANIMATE_MOTION )
31033199 && (abegin->attr_id == SVG_ATTR_BEGIN )) {
31043200 const psx_svg_timing_list* tl = (const psx_svg_timing_list*)abegin->value .val ;
3105- if (t == SVG_TAG_SET && tl->event_token ) {
3201+ if (tl->event_token ) {
31063202 item.begin_event = _dup_cstr (tl->event_token );
31073203 psx_array_clear (&item.begins_sec );
31083204 item.begin_sec = 1e30f; // don't auto-activate until triggered
@@ -3182,6 +3278,15 @@ static void _collect_anims(psx_svg_player* p, const psx_svg_node* node)
31823278 item.additive_mode = (uint32_t )additive_attr->value .ival ;
31833279 }
31843280
3281+ item.restart_mode = SVG_ANIMATION_RESTART_ALWAYS ;
3282+ const psx_svg_attr* arestart = _find_attr (node, SVG_ATTR_RESTART );
3283+ if (arestart && arestart->val_type == SVG_ATTR_VALUE_DATA ) {
3284+ item.restart_mode = (uint32_t )arestart->value .ival ;
3285+ }
3286+
3287+ item.min_sec = _attr_as_time_sec (_find_attr (node, SVG_ATTR_MIN ));
3288+ item.max_sec = _attr_as_time_sec (_find_attr (node, SVG_ATTR_MAX ));
3289+
31853290 psx_array_append (&p->anims , NULL );
31863291 psx_svg_anim_item* dst = psx_array_get_last (&p->anims , psx_svg_anim_item);
31873292 *dst = item;
@@ -3651,6 +3756,21 @@ void psx_svg_player_trigger(psx_svg_player* p, const char* target_id, const char
36513756 }
36523757
36533758 float sec = p->time_sec ;
3759+
3760+ // restart guard: check restart_mode before appending new begin.
3761+ if (it->restart_mode == SVG_ANIMATION_RESTART_NEVER ) {
3762+ if (!psx_array_empty (&it->begins_sec )) {
3763+ continue ; // already activated once; ignore
3764+ }
3765+ } else if (it->restart_mode == SVG_ANIMATION_RESTART_WHEN_NOT_ACTIVE ) {
3766+ // Check if currently active: find current begin and see if sec < begin + active_dur.
3767+ float ad = _anim_item_active_dur (it);
3768+ float cur_begin = _anim_item_begin_for_time (it, sec);
3769+ if (cur_begin < 1e29f && sec >= cur_begin && sec < cur_begin + ad) {
3770+ continue ; // currently active; ignore
3771+ }
3772+ }
3773+
36543774 psx_array_append (&it->begins_sec , &sec);
36553775 _anim_item_begin_list_normalize (it);
36563776 any = true ;
0 commit comments