diff --git a/include/parasol/modules/xml.h b/include/parasol/modules/xml.h index 65847bb69..66729b14b 100644 --- a/include/parasol/modules/xml.h +++ b/include/parasol/modules/xml.h @@ -82,7 +82,7 @@ typedef struct XMLAttrib { std::string Value; // Value of the attribute inline bool isContent() const { return Name.empty(); } inline bool isTag() const { return !Name.empty(); } - XMLAttrib(std::string pName, std::string pValue) : Name(pName), Value(pValue) { }; + XMLAttrib(std::string pName, std::string pValue = "") : Name(pName), Value(pValue) { }; XMLAttrib() = default; } XMLATTRIB; diff --git a/src/svg/animation.cpp b/src/svg/animation.cpp index 383651572..93020fec9 100644 --- a/src/svg/animation.cpp +++ b/src/svg/animation.cpp @@ -3,7 +3,50 @@ // https://www.w3.org/TR/2001/REC-smil-animation-20010904 //******************************************************************************************************************** -// Set common animation properties + +void anim_base::activate(extSVG *SVG) +{ + // Reset all the variables that control time management and the animation will start from scratch. + begin_offset = (double(PreciseTime()) / 1000000.0) - SVG->AnimEpoch; + repeat_index = 0; + start_time = SVG->AnimEpoch + begin_offset; + end_time = 0; + + // Test: w3-animate-elem-21-t.svg + + for (auto &other : start_on_begin) { + other->activate(SVG); + other->start_time = start_time; // Ensure that times match exactly + } +} + +//******************************************************************************************************************** + +void anim_base::stop(extSVG *SVG, double Time) +{ + if (!begin_series.empty()) { + // Check if there's a serialised begin offset following the one that's completed. + LONG i; + for (i=0; i < std::ssize(begin_series)-1; i++) { + if (begin_offset IS begin_series[i]) { + begin_offset = begin_series[i+1]; + start_time = 0; + return; + } + } + } + + end_time = Time; + seek = 1.0; // Necessary in case the seek range calculation has overflowed + + // Start animations that are to be triggered from our ending. + for (auto &other : start_on_end) { + other->activate(SVG); + other->start_time = Time; + } +} + +//******************************************************************************************************************** static ERR parse_spline(APTR Path, LONG Index, LONG Command, double X, double Y, anim_base::SPLINE_POINTS &Meta) { @@ -17,6 +60,9 @@ static ERR parse_spline(APTR Path, LONG Index, LONG Command, double X, double Y, return ERR::Okay; } +//******************************************************************************************************************** +// Set common animation properties + static ERR set_anim_property(extSVG *Self, anim_base &Anim, XMLTag &Tag, ULONG Hash, const std::string_view Value) { switch (Hash) { @@ -735,147 +781,173 @@ void anim_transform::perform(extSVG &SVG) } //******************************************************************************************************************** -// -// -// void anim_value::perform(extSVG &SVG) { - if ((end_time) and (!freeze)) return; + pf::Log log; + if ((end_time) and (!freeze)) return; + pf::ScopedObjectLock vector(target_vector, 1000); if (vector.granted()) { - // Determine the type of the attribute that we're targeting, then interpolate the value and set it. + if (vector->Class->ClassID IS ID_VECTORGROUP) { + // Groups are a special case because they act as a placeholder and aren't guaranteed to propagate all + // attributes to their children. - switch(StrHash(target_attrib)) { - case SVF_FONT_SIZE: { - auto val = get_numeric_value(**vector, FID_FontSize); - vector->set(FID_FontSize, val); - break; - } + // Note that group attributes do not override values that are defined by the client. - case SVF_FILL: { - auto val = get_colour_value(**vector, FID_FillColour); - vector->setArray(FID_FillColour, (float *)&val, 4); - break; - } + for (auto &child : tag->Children) { + if (!child.isTag()) continue; + // Any tag producing a vector object can theoretically be subject to animation. + if (auto si = child.attrib("_id")) { + // We can't override attributes that were defined by the client. + if (child.attrib(target_attrib)) continue; - case SVF_FILL_OPACITY: { - auto val = get_numeric_value(**vector, FID_FillOpacity); - vector->set(FID_FillOpacity, val); - break; + pf::ScopedObjectLock cv(std::stoi(*si), 1000); + if (cv.granted()) set_value(**cv); + } } + } + else set_value(**vector); + } +} - case SVF_STROKE: { - auto val = get_colour_value(**vector, FID_StrokeColour); - vector->setArray(FID_StrokeColour, (float *)&val, 4); - break; - } +//******************************************************************************************************************** - case SVF_STROKE_WIDTH: - vector->set(FID_StrokeWidth, get_numeric_value(**vector, FID_StrokeWidth)); - break; +void anim_value::set_value(objVector &Vector) +{ + // Determine the type of the attribute that we're targeting, then interpolate the value and set it. - case SVF_OPACITY: - vector->set(FID_Opacity, get_numeric_value(**vector, FID_Opacity)); - break; + switch(StrHash(target_attrib)) { + case SVF_FONT_SIZE: { + auto val = get_numeric_value(Vector, FID_FontSize); + Vector.set(FID_FontSize, val); + break; + } - case SVF_DISPLAY: { - auto val = get_string(); - if (StrMatch("none", val) IS ERR::Okay) vector->set(FID_Visibility, LONG(VIS::HIDDEN)); - else if (StrMatch("inline", val) IS ERR::Okay) vector->set(FID_Visibility, LONG(VIS::VISIBLE)); - else if (StrMatch("inherit", val) IS ERR::Okay) vector->set(FID_Visibility, LONG(VIS::INHERIT)); - break; - } + case SVF_FILL: { + auto val = get_colour_value(Vector, FID_FillColour); + Vector.setArray(FID_FillColour, (float *)&val, 4); + break; + } - case SVF_R: - vector->set(FID_Radius, get_dimension(**vector, FID_Radius)); - break; + case SVF_FILL_OPACITY: { + auto val = get_numeric_value(Vector, FID_FillOpacity); + Vector.set(FID_FillOpacity, val); + break; + } - case SVF_RX: - vector->set(FID_RadiusX, get_dimension(**vector, FID_RadiusX)); - break; + case SVF_STROKE: { + auto val = get_colour_value(Vector, FID_StrokeColour); + Vector.setArray(FID_StrokeColour, (float *)&val, 4); + break; + } - case SVF_RY: - vector->set(FID_RadiusY, get_dimension(**vector, FID_RadiusY)); - break; + case SVF_STROKE_WIDTH: + Vector.set(FID_StrokeWidth, get_numeric_value(Vector, FID_StrokeWidth)); + break; - case SVF_CX: - vector->set(FID_CX, get_dimension(**vector, FID_CX)); - break; + case SVF_OPACITY: + Vector.set(FID_Opacity, get_numeric_value(Vector, FID_Opacity)); + break; - case SVF_CY: - vector->set(FID_CY, get_dimension(**vector, FID_CY)); - break; + case SVF_DISPLAY: { + auto val = get_string(); + if (StrMatch("none", val) IS ERR::Okay) Vector.set(FID_Visibility, LONG(VIS::HIDDEN)); + else if (StrMatch("inline", val) IS ERR::Okay) Vector.set(FID_Visibility, LONG(VIS::VISIBLE)); + else if (StrMatch("inherit", val) IS ERR::Okay) Vector.set(FID_Visibility, LONG(VIS::INHERIT)); + break; + } + + case SVF_VISIBILITY: { + auto val = get_string(); + Vector.set(FID_Visibility, val); + break; + } + + case SVF_R: + Vector.set(FID_Radius, get_dimension(Vector, FID_Radius)); + break; + + case SVF_RX: + Vector.set(FID_RadiusX, get_dimension(Vector, FID_RadiusX)); + break; + + case SVF_RY: + Vector.set(FID_RadiusY, get_dimension(Vector, FID_RadiusY)); + break; + + case SVF_CX: + Vector.set(FID_CX, get_dimension(Vector, FID_CX)); + break; + + case SVF_CY: + Vector.set(FID_CY, get_dimension(Vector, FID_CY)); + break; - case SVF_X1: - vector->set(FID_X1, get_dimension(**vector, FID_X1)); - break; + case SVF_X1: + Vector.set(FID_X1, get_dimension(Vector, FID_X1)); + break; - case SVF_Y1: - vector->set(FID_Y1, get_dimension(**vector, FID_Y1)); - break; + case SVF_Y1: + Vector.set(FID_Y1, get_dimension(Vector, FID_Y1)); + break; - case SVF_X2: - vector->set(FID_X2, get_dimension(**vector, FID_X2)); - break; + case SVF_X2: + Vector.set(FID_X2, get_dimension(Vector, FID_X2)); + break; - case SVF_Y2: - vector->set(FID_Y2, get_dimension(**vector, FID_Y2)); - break; + case SVF_Y2: + Vector.set(FID_Y2, get_dimension(Vector, FID_Y2)); + break; - case SVF_X: { - if (vector->Class->ClassID IS ID_VECTORGROUP) { - // Special case: SVG groups don't have an (x,y) position, but can declare one in the form of a - // transform. Refer to xtag_use() for a working example as to why. + case SVF_X: { + if (Vector.Class->ClassID IS ID_VECTORGROUP) { + // Special case: SVG groups don't have an (x,y) position, but can declare one in the form of a + // transform. Refer to xtag_use() for a working example as to why. - VectorMatrix *m; - for (m=vector->Matrices; (m) and (m->Tag != MTAG_SVG_TRANSFORM); m=m->Next); + VectorMatrix *m; + for (m=Vector.Matrices; (m) and (m->Tag != MTAG_SVG_TRANSFORM); m=m->Next); - if (!m) { - vecNewMatrix(*vector, &m, false); - m->Tag = MTAG_SVG_TRANSFORM; - } + if (!m) { + vecNewMatrix(&Vector, &m, false); + m->Tag = MTAG_SVG_TRANSFORM; + } - if (m) { - m->TranslateX = get_dimension(**vector, FID_X); - vecFlushMatrix(m); - } + if (m) { + m->TranslateX = get_dimension(Vector, FID_X); + vecFlushMatrix(m); } - else vector->set(FID_X, get_dimension(**vector, FID_X)); - break; } + else Vector.set(FID_X, get_dimension(Vector, FID_X)); + break; + } - case SVF_Y: { - if (vector->Class->ClassID IS ID_VECTORGROUP) { - VectorMatrix *m; - for (m=vector->Matrices; (m) and (m->Tag != MTAG_SVG_TRANSFORM); m=m->Next); + case SVF_Y: { + if (Vector.Class->ClassID IS ID_VECTORGROUP) { + VectorMatrix *m; + for (m=Vector.Matrices; (m) and (m->Tag != MTAG_SVG_TRANSFORM); m=m->Next); - if (!m) { - vecNewMatrix(*vector, &m, false); - m->Tag = MTAG_SVG_TRANSFORM; - } + if (!m) { + vecNewMatrix(&Vector, &m, false); + m->Tag = MTAG_SVG_TRANSFORM; + } - if (m) { - m->TranslateY = get_dimension(**vector, FID_Y); - vecFlushMatrix(m); - } + if (m) { + m->TranslateY = get_dimension(Vector, FID_Y); + vecFlushMatrix(m); } - else vector->set(FID_Y, get_dimension(**vector, FID_Y)); - break; } + else Vector.set(FID_Y, get_dimension(Vector, FID_Y)); + break; + } - case SVF_WIDTH: - vector->set(FID_Width, get_dimension(**vector, FID_Width)); - break; - - case SVF_HEIGHT: - vector->set(FID_Height, get_dimension(**vector, FID_Height)); - break; + case SVF_WIDTH: + Vector.set(FID_Width, get_dimension(Vector, FID_Width)); + break; - case SVF_VISIBILITY: - vector->set(FID_Visibility, get_string()); - break; - } + case SVF_HEIGHT: + Vector.set(FID_Height, get_dimension(Vector, FID_Height)); + break; } -} +} \ No newline at end of file diff --git a/src/svg/animation.h b/src/svg/animation.h index 43f5dec58..95d6d5704 100644 --- a/src/svg/animation.h +++ b/src/svg/animation.h @@ -1,5 +1,6 @@ #include +#include static const double DEG2RAD = 0.01745329251994329576923690768489; // Multiple any angle by this value to convert to radians static const double RAD2DEG = 57.295779513082320876798154814105; @@ -119,7 +120,6 @@ class anim_base { double min_duration = 0; // The minimum value of the active duration. If zero, the active duration is not constrained. double max_duration = 0; // The maximum value of the active duration. double duration = 0; // Measured in seconds, anything < 0 means infinite. - double first_time = 0; // Time-stamp of the first iteration double start_time = 0; // This is time-stamped once the animation has started (the first begin event is hit) double end_time = 0; // This is time-stamped once the animation has finished all of its cycles (including repetitions) double end = 0; @@ -144,40 +144,11 @@ class anim_base { double get_numeric_value(objVector &, FIELD); std::string get_string(); FRGB get_colour_value(objVector &, FIELD); - bool started(double); + bool started(extSVG *, double); bool next_frame(double); void set_orig_value(); - - void activate(void) { - // Reset all the variables that control time management and the animation will start from scratch. - begin_offset = 0; - repeat_index = 0; - start_time = 0; - end_time = 0; - } - - void stop(double Time) { - if (!begin_series.empty()) { - // Check if there's a serialised begin offset following the one that's completed. - LONG i; - for (i=0; i < std::ssize(begin_series)-1; i++) { - if (begin_offset IS begin_series[i]) { - begin_offset = begin_series[i+1]; - start_time = 0; - return; - } - } - } - - end_time = Time; - seek = 1.0; // Necessary in case the seek range calculation has overflowed - - // Start animations that are to be triggered from our ending. - for (auto &other : start_on_end) { - other->activate(); - other->start_time = Time; - } - } + void activate(extSVG *); + void stop(extSVG *, double); virtual void perform(class extSVG &) = 0; @@ -245,8 +216,12 @@ class anim_motion : public anim_base { class anim_value : public anim_base { public: - anim_value(OBJECTID pTarget) : anim_base(pTarget) { } + XMLTag *tag = NULL; + + anim_value(OBJECTID pTarget, XMLTag *pTag) : anim_base(pTarget), tag(pTag) { } void perform(extSVG &); + void set_value(objVector &Vector); + }; //******************************************************************************************************************** diff --git a/src/svg/animation_timing.cpp b/src/svg/animation_timing.cpp index 1cfc13f6b..a3e8f3b28 100644 --- a/src/svg/animation_timing.cpp +++ b/src/svg/animation_timing.cpp @@ -1,27 +1,26 @@ //******************************************************************************************************************** -// Return true if the animation has started +// Return true if the animation has started. For absolute consistency, animations start 'at the time they should have +// started', which we can strictly calculate from begin and duration timing values. -bool anim_base::started(double CurrentTime) +bool anim_base::started(extSVG *SVG, double CurrentTime) { - if (not first_time) first_time = CurrentTime; - + if (end_time) return false; if (start_time) return true; if (repeat_index > 0) return true; - if (begin_offset) { - // Check if one of the animation's begin triggers has been tripped. - const double elapsed = CurrentTime - start_time; - if (elapsed < begin_offset) return false; - } + // Check if the begin trigger has been tripped. If begin_offset is 0 then we start immediately. + + if (CurrentTime < SVG->AnimEpoch + begin_offset) return false; + start_time = SVG->AnimEpoch + begin_offset; + + // Start/Reset linked animations that are to start when we do - // Start/Reset linked animations for (auto &other : start_on_begin) { - other->activate(); - other->start_time = CurrentTime; + other->activate(SVG); + other->start_time = start_time; // Ensure that times match exactly } - start_time = CurrentTime; return true; } @@ -71,17 +70,19 @@ static ERR animation_timer(extSVG *SVG, LARGE TimeElapsed, LARGE CurrentTime) matrix.second.transforms.clear(); } - for (auto &record : SVG->Animations) { - std::visit([SVG](auto &&anim) { - double current_time = double(PreciseTime()) / 1000000.0; + if (!SVG->AnimEpoch) { + SVG->AnimEpoch = double(CurrentTime) / 1000000.0; + } - if (not anim.started(current_time)) return; + double current_time = double(CurrentTime) / 1000000.0; - if (anim.next_frame(current_time)) { - anim.perform(*SVG); - anim.stop(current_time); - } - else anim.perform(*SVG); + for (auto &record : SVG->Animations) { + std::visit([ SVG, current_time ](auto &&anim) { + if (not anim.started(SVG, current_time)) return; + + bool stop = anim.next_frame(current_time); + anim.perform(*SVG); + if (stop) anim.stop(SVG, current_time); }, record); } @@ -140,6 +141,7 @@ static ERR animation_timer(extSVG *SVG, LARGE TimeElapsed, LARGE CurrentTime) vt.matrix[0] *= t->matrix; //if (t->additive IS ADD::SUM) vt.matrix[0] *= t->matrix; //else vt.matrix[0] = t->matrix; + vecFlushMatrix(vt.matrix); }); } diff --git a/src/svg/class_svg.cpp b/src/svg/class_svg.cpp index a06a3d5aa..da3825af5 100644 --- a/src/svg/class_svg.cpp +++ b/src/svg/class_svg.cpp @@ -76,7 +76,7 @@ static ERR SVG_DataFeed(extSVG *Self, struct acDataFeed *Args) if (!Args) return ERR::NullArgs; if (Args->Datatype IS DATA::XML) { - return load_svg(Self, 0, (CSTRING)Args->Buffer); + return parse_svg(Self, 0, (CSTRING)Args->Buffer); } return ERR::Okay; @@ -107,6 +107,7 @@ static ERR SVG_Free(extSVG *Self, APTR Void) if (Self->Path) { FreeResource(Self->Path); Self->Path = NULL; } if (Self->Title) { FreeResource(Self->Title); Self->Title = NULL; } if (Self->Statement) { FreeResource(Self->Statement); Self->Statement = NULL; } + if (Self->XML) { FreeResource(Self->XML); Self->XML = NULL; } return ERR::Okay; } @@ -131,8 +132,8 @@ static ERR SVG_Init(extSVG *Self, APTR Void) else return ERR::NewObject; } - if (Self->Path) return load_svg(Self, Self->Path, NULL); - else if (Self->Statement) return load_svg(Self, NULL, Self->Statement); + if (Self->Path) return parse_svg(Self, Self->Path, NULL); + else if (Self->Statement) return parse_svg(Self, NULL, Self->Statement); return ERR::Okay; } diff --git a/src/svg/parser.cpp b/src/svg/parser.cpp index 0b482cd69..dbb702bfe 100644 --- a/src/svg/parser.cpp +++ b/src/svg/parser.cpp @@ -124,7 +124,7 @@ static void process_children(extSVG *Self, svgState &State, XMLTag &Tag, OBJECTP objVector *sibling = NULL; for (auto &child : Tag.Children) { if (child.isTag()) { - xtag_default(Self, State, child, Vector, sibling); + xtag_default(Self, State, child, Tag, Vector, sibling); } } } @@ -140,12 +140,12 @@ static void process_shape_children(extSVG *Self, svgState &State, XMLTag &Tag, O if (!child.isTag()) continue; switch(StrHash(child.name())) { - case SVF_ANIMATE: xtag_animate(Self, child, Vector); break; - case SVF_ANIMATECOLOR: xtag_animate_colour(Self, child, Vector); break; + case SVF_ANIMATE: xtag_animate(Self, child, Tag, Vector); break; + case SVF_ANIMATECOLOR: xtag_animate_colour(Self, child, Tag, Vector); break; case SVF_ANIMATETRANSFORM: xtag_animate_transform(Self, child, Vector); break; case SVF_ANIMATEMOTION: xtag_animate_motion(Self, child, Vector); break; + case SVF_SET: xtag_set(Self, child, Tag, Vector); break; case SVF_PARASOL_MORPH: xtag_morph(Self, child, Vector); break; - case SVF_SET: xtag_set(Self, child, Vector); break; case SVF_TEXTPATH: if (Vector->Class->ClassID IS ID_VECTORTEXT) { if (!child.Children.empty()) { @@ -1236,7 +1236,7 @@ static ERR parse_fe_source(extSVG *Self, svgState &State, objVectorFilter *Filte // live reference being found. if (auto tagref = find_href_tag(Self, ref)) { - xtag_default(Self, State, *tagref, Self->Scene, vector); + xtag_default(Self, State, *tagref, Tag, Self->Scene, vector); } else log.warning("Element id '%s' not found.", ref.c_str()); } @@ -1587,6 +1587,7 @@ static ERR process_shape(extSVG *Self, CLASSID VectorID, svgState &State, XMLTag if (vector->init() IS ERR::Okay) { process_shape_children(Self, State, Tag, vector); + Tag.Attribs.push_back(XMLAttrib { "_id", std::to_string(vector->UID) }); Result = vector; return error; } @@ -1601,7 +1602,7 @@ static ERR process_shape(extSVG *Self, CLASSID VectorID, svgState &State, XMLTag //******************************************************************************************************************** // See also process_children() -static ERR xtag_default(extSVG *Self, svgState &State, XMLTag &Tag, OBJECTPTR Parent, objVector * &Vector) +static ERR xtag_default(extSVG *Self, svgState &State, XMLTag &Tag, XMLTag &ParentTag, OBJECTPTR Parent, objVector * &Vector) { pf::Log log(__FUNCTION__); @@ -1627,11 +1628,11 @@ static ERR xtag_default(extSVG *Self, svgState &State, XMLTag &Tag, OBJECTPTR Pa case SVF_CONICGRADIENT: xtag_conicgradient(Self, Tag); break; case SVF_LINEARGRADIENT: xtag_lineargradient(Self, Tag); break; case SVF_SYMBOL: xtag_symbol(Self, Tag); break; - case SVF_ANIMATE: xtag_animate(Self, Tag, Parent); break; - case SVF_ANIMATECOLOR: xtag_animate_colour(Self, Tag, Parent); break; + case SVF_ANIMATE: xtag_animate(Self, Tag, ParentTag, Parent); break; + case SVF_ANIMATECOLOR: xtag_animate_colour(Self, Tag, ParentTag, Parent); break; case SVF_ANIMATETRANSFORM: xtag_animate_transform(Self, Tag, Parent); break; case SVF_ANIMATEMOTION: xtag_animate_motion(Self, Tag, Parent); break; - case SVF_SET: xtag_set(Self, Tag, Parent); break; + case SVF_SET: xtag_set(Self, Tag, ParentTag, Parent); break; case SVF_FILTER: xtag_filter(Self, State, Tag); break; case SVF_DEFS: xtag_defs(Self, State, Tag, Parent); break; case SVF_CLIPPATH: xtag_clippath(Self, Tag); break; @@ -1920,6 +1921,7 @@ static ERR xtag_image(extSVG *Self, svgState &State, XMLTag &Tag, OBJECTPTR Pare if (Vector->init() IS ERR::Okay) { process_shape_children(Self, State, Tag, Vector); + Tag.Attribs.push_back(XMLAttrib { "_id", std::to_string(Vector->UID) }); return ERR::Okay; } else { @@ -2161,8 +2163,8 @@ static void xtag_morph(extSVG *Self, XMLTag &Tag, OBJECTPTR Parent) static void xtag_use(extSVG *Self, svgState &State, XMLTag &Tag, OBJECTPTR Parent) { pf::Log log(__FUNCTION__); - std::string ref; + std::string ref; for (LONG a=1; (a < std::ssize(Tag.Attribs)) and ref.empty(); a++) { switch(StrHash(Tag.Attribs[a].Name)) { case SVF_HREF: // SVG2 @@ -2183,11 +2185,6 @@ static void xtag_use(extSVG *Self, svgState &State, XMLTag &Tag, OBJECTPTR Paren return; } - objVector *viewport = NULL; - - auto state = State; - state.applyTag(Tag); // Apply all attribute values to the current state. - // Increment the Cloning variable to indicate that we are in a region that is being cloned. // This is important for some elements like clip-path, whereby the path only needs to be created // once and can then be referenced multiple times. @@ -2200,6 +2197,14 @@ static void xtag_use(extSVG *Self, svgState &State, XMLTag &Tag, OBJECTPTR Paren if ((StrMatch("symbol", tagref->name()) IS ERR::Okay) or (StrMatch("svg", tagref->name()) IS ERR::Okay)) { // SVG spec requires that we create a VectorGroup and then create a Viewport underneath that. However if there // are no attributes to apply to the group then there is no sense in creating an empty one. + + // TODO: We should be using the same replace-and-expand tag method that is applied for group + // handling, as seen further below in this routine. + + objVector *viewport = NULL; + + auto state = State; + state.applyTag(Tag); // Apply all attribute values to the current state. objVector *group; bool need_group = false; @@ -2281,41 +2286,21 @@ static void xtag_use(extSVG *Self, svgState &State, XMLTag &Tag, OBJECTPTR Paren // additional transformation translate(x,y) is appended to the end (i.e., right-side) of the 'transform' // attribute on the generated 'g', where x and y represent the values of the 'x' and 'y' attributes on the // 'use' element. The referenced object and its contents are deep-cloned into the generated tree. - // - // NOTE: The SVG documentation appears to be silent on the matter of children in the element. So far we've - // encountered animate instructions in the area, and this expands into a complicated dual-group configuration - // (at least that appears to be the only way we can get our animations to match expected behaviour patterns in W3C - // tests). In any case, if a element contains children then the dual group method is employed. - - objVector *group, *subgroup; - if (NewObject(ID_VECTORGROUP, &group) IS ERR::Okay) { - SetOwner(group, Parent); - SetName(group, "UseElement"); - - if (Tag.hasChildTags()) { - if (NewObject(ID_VECTORGROUP, &subgroup) IS ERR::Okay) { - SetOwner(subgroup, group); - SetName(subgroup, "UseElementSG"); - } - else subgroup = group; - } - else subgroup = group; - state.applyTag(Tag); // Apply supported attribute values to the current state. + auto use_attribs = Tag.Attribs; + Tag.Attribs[0].Name = "g"; + if (Tag.Attribs.size() > 1) Tag.Attribs.erase(Tag.Attribs.begin()+1, Tag.Attribs.end()); - // Apply 'use' attributes to the group, making a special case for 'x' and 'y'. + if (Tag.Children.empty()) { + Tag.Children = { *tagref }; // Deep-clone - FUNIT tx, ty; - for (LONG t=1; t < std::ssize(Tag.Attribs); t++) { - if (Tag.Attribs[t].Value.empty()) continue; - - // Ignore unrecognised namespaces, e.g. 'inkscape:dx' - if (Tag.Attribs[t].Name.find(':') != std::string::npos) continue; + // Apply 'use' attributes, making a special case for 'x' and 'y'. - auto hash = StrHash(Tag.Attribs[t].Name); - switch (hash) { - case SVF_X: tx = FUNIT(FID_X, Tag.Attribs[t].Value); break; - case SVF_Y: ty = FUNIT(FID_Y, Tag.Attribs[t].Value); break; + FUNIT tx, ty; + for (LONG t=1; t < std::ssize(use_attribs); t++) { + switch (StrHash(use_attribs[t].Name)) { + case SVF_X: tx = FUNIT(FID_X, use_attribs[t].Value); break; + case SVF_Y: ty = FUNIT(FID_Y, use_attribs[t].Value); break; // SVG states that the following are not to be applied to the group... case SVF_WIDTH: case SVF_HEIGHT: @@ -2324,32 +2309,47 @@ static void xtag_use(extSVG *Self, svgState &State, XMLTag &Tag, OBJECTPTR Paren break; default: - if (auto error = set_property(Self, subgroup, hash, Tag, State, Tag.Attribs[t].Value); error != ERR::Okay) { - log.warning("Failed to apply %s=%s to group: %s", Tag.Attribs[t].Name.c_str(), Tag.Attribs[t].Value.c_str(), GetErrorMsg(error)); - } + Tag.Attribs.push_back(use_attribs[t]); } } if ((!tx.empty()) or (!ty.empty())) { - parse_transform(subgroup, "translate(" + std::to_string(tx) + " " + std::to_string(ty) + ")", MTAG_USE_TRANSFORM); + Tag.Attribs.push_back({ "transform", "translate(" + std::to_string(tx) + " " + std::to_string(ty) + ")" }); } + } + else { + // The SVG documentation appears to be silent on the matter of children in the element. So far we've + // encountered animate instructions in the area, and this expands into a complicated dual-group configuration + // (at least that appears to be the only way we can get our animations to match expected behaviour patterns in W3C + // tests). - if ((group IS subgroup) or (group->init() IS ERR::Okay)) { - if (subgroup->init() IS ERR::Okay) { - // Perform the deep-clone as stipulated by W3C. Generated objects will inherit attributes from the group. - log.branch("Duplicating tags at %s", ref.c_str()); - objVector *sibling = NULL; - xtag_default(Self, state, *tagref, subgroup, sibling); + auto &subgroup = Tag.Children.emplace_back(XMLTag(0)); + subgroup.Attribs.push_back(XMLAttrib("g")); + subgroup.Children.push_back(*tagref); - log.traceBranch("Processing all child elements within "); - process_children(Self, state, Tag, group); - return; + FUNIT tx, ty; + for (LONG t=1; t < std::ssize(use_attribs); t++) { + switch (StrHash(use_attribs[t].Name)) { + case SVF_X: tx = FUNIT(FID_X, use_attribs[t].Value); break; + case SVF_Y: ty = FUNIT(FID_Y, use_attribs[t].Value); break; + case SVF_WIDTH: + case SVF_HEIGHT: + case SVF_XLINK_HREF: + case SVF_HREF: + break; + + default: + subgroup.Attribs.push_back(use_attribs[t]); } } - FreeResource(group); - if (group != subgroup) FreeResource(subgroup); + if ((!tx.empty()) or (!ty.empty())) { + subgroup.Attribs.push_back({ "transform", "translate(" + std::to_string(tx) + " " + std::to_string(ty) + ")" }); + } } + + objVector *new_group = NULL; + xtag_group(Self, State, Tag, Parent, new_group); } } @@ -2371,8 +2371,8 @@ static ERR link_event(objVector *Vector, const InputEvent *Events, svgLink *Link // The link activates a document node, like an animation. if (find_href_tag(Self, Link->ref)) { for (auto &record : Self->Animations) { - std::visit([ Link ](auto &&anim) { - if (anim.id IS Link->ref.substr(1)) anim.activate(); + std::visit([ Link, Self ](auto &&anim) { + if (anim.id IS Link->ref.substr(1)) anim.activate(Self); }, record); } } @@ -2445,35 +2445,13 @@ static void xtag_group(extSVG *Self, svgState &State, XMLTag &Tag, OBJECTPTR Par SetOwner(group, Parent); if (!Tag.Children.empty()) state.applyTag(Tag); // Apply all group attribute values to the current state. process_attrib(Self, Tag, State, group); - - // Process child tags in advance. Unfortunately there is a requirement for to work only - // on objects that do not already have the target 'attributeName' already defined. We therefore need - // to do a bit of filtering and clone the tag for the affected elements. - - for (auto find_anim=Tag.Children.begin(); find_anim != Tag.Children.end(); ) { - if ((find_anim->isTag()) and (iequals("animate", find_anim->name()))) { - if ((find_anim->attrib("href")) or (find_anim->attrib("xlink:href"))) { - // Ignore animation tags that specify a link. - find_anim++; - continue; - } - else if (auto attrib = find_anim->attrib("attributeName")) { - for (auto &clone_to : Tag.Children) { - if (clone_to.isTag() and (not iequals("animate", clone_to.name()))) { - if (!clone_to.attrib(*attrib)) { - clone_to.Children.push_back(*find_anim); - } - } - } - } - find_anim = Tag.Children.erase(find_anim); - } - else find_anim++; - } process_children(Self, state, Tag, group); - if (group->init() IS ERR::Okay) Vector = group; + if (group->init() IS ERR::Okay) { + Vector = group; + Tag.Attribs.push_back(XMLAttrib { "_id", std::to_string(group->UID) }); + } else FreeResource(group); } @@ -2619,7 +2597,7 @@ static void xtag_svg(extSVG *Self, svgState &State, XMLTag &Tag, OBJECTPTR Paren switch(StrHash(child.name())) { case SVF_DEFS: xtag_defs(Self, state, child, viewport); break; - default: xtag_default(Self, state, child, viewport, sibling); break; + default: xtag_default(Self, state, child, Tag, viewport, sibling); break; } } } @@ -2668,12 +2646,12 @@ static ERR xtag_animate_transform(extSVG *Self, XMLTag &Tag, OBJECTPTR Parent) //******************************************************************************************************************** // The 'animate' element is used to animate a single attribute or property over time. For example, to make a // rectangle repeatedly fade away over 5 seconds: -// -// NOTE: Also see xtag_group() which duplicates elements for technical reasons. -static ERR xtag_animate(extSVG *Self, XMLTag &Tag, OBJECTPTR Parent) +static ERR xtag_animate(extSVG *Self, XMLTag &Tag, XMLTag &ParentTag, OBJECTPTR Parent) { - auto &new_anim = Self->Animations.emplace_back(anim_value { Parent->UID }); + pf::Log log(__FUNCTION__); + + auto &new_anim = Self->Animations.emplace_back(anim_value { Parent->UID, &ParentTag }); auto &anim = std::get(new_anim); for (LONG a=1; a < std::ssize(Tag.Attribs); a++) { @@ -2697,9 +2675,9 @@ static ERR xtag_animate(extSVG *Self, XMLTag &Tag, OBJECTPTR Parent) //******************************************************************************************************************** // is largely equivalent to but does not interpolate values. -static ERR xtag_set(extSVG *Self, XMLTag &Tag, OBJECTPTR Parent) +static ERR xtag_set(extSVG *Self, XMLTag &Tag, XMLTag &ParentTag, OBJECTPTR Parent) { - auto &new_anim = Self->Animations.emplace_back(anim_value { Parent->UID }); + auto &new_anim = Self->Animations.emplace_back(anim_value { Parent->UID, &ParentTag }); auto &anim = std::get(new_anim); for (LONG a=1; a < std::ssize(Tag.Attribs); a++) { @@ -2725,9 +2703,9 @@ static ERR xtag_set(extSVG *Self, XMLTag &Tag, OBJECTPTR Parent) // The tag is considered deprecated because its functionality can be represented entirely by the // existing tag. -static ERR xtag_animate_colour(extSVG *Self, XMLTag &Tag, OBJECTPTR Parent) +static ERR xtag_animate_colour(extSVG *Self, XMLTag &Tag, XMLTag &ParentTag, OBJECTPTR Parent) { - auto &new_anim = Self->Animations.emplace_back(anim_value { Parent->UID }); + auto &new_anim = Self->Animations.emplace_back(anim_value { Parent->UID, &ParentTag }); auto &anim = std::get(new_anim); for (LONG a=1; a < std::ssize(Tag.Attribs); a++) { @@ -2833,6 +2811,7 @@ static void process_attrib(extSVG *Self, XMLTag &Tag, svgState &State, objVector auto &value = Tag.Attribs[t].Value; if (name.find(':') != std::string::npos) continue; // Do not interpret non-SVG attributes, e.g. 'inkscape:dx' + if (name == "_id") continue; // Ignore temporary private attribs like '_id' log.trace("%s = %.40s", name.c_str(), value.c_str()); diff --git a/src/svg/svg.cpp b/src/svg/svg.cpp index d30e0c4d8..83b0a12c1 100644 --- a/src/svg/svg.cpp +++ b/src/svg/svg.cpp @@ -29,7 +29,6 @@ Relevant SVG reference manuals: #include #include #include "svg_def.c" -#include "animation.h" #include #include @@ -71,6 +70,10 @@ struct svgAnimState { #include +class extSVG; + +#include "animation.h" + //******************************************************************************************************************** class extSVG : public objSVG { @@ -79,6 +82,7 @@ class extSVG : public objSVG { std::unordered_map IDs; std::unordered_map Effects; // All effects, registered by their SVG identifier. DOUBLE SVGVersion; + DOUBLE AnimEpoch; // Epoch time for the animations. objXML *XML; objVectorScene *Scene; STRING Folder; @@ -120,28 +124,32 @@ struct svgState { static ERR animation_timer(extSVG *, LARGE, LARGE); static void convert_styles(objXML::TAGS &); +static ERR set_property(extSVG *, objVector *, ULONG, XMLTag &, svgState &, std::string); + static ERR init_svg(void); static ERR init_rsvg(void); + static void process_attrib(extSVG *, XMLTag &, svgState &, objVector *); static void process_children(extSVG *, svgState &, XMLTag &, OBJECTPTR); static void process_rule(extSVG *, objXML::TAGS &, KatanaRule *); static ERR process_shape(extSVG *, CLASSID, svgState &, XMLTag &, OBJECTPTR, objVector * &); + static ERR save_svg_scan(extSVG *, objXML *, objVector *, LONG); static ERR save_svg_defs(extSVG *, objXML *, objVectorScene *, LONG); static ERR save_svg_scan_std(extSVG *, objXML *, objVector *, LONG); static ERR save_svg_transform(VectorMatrix *, std::stringstream &); -static ERR set_property(extSVG *, objVector *, ULONG, XMLTag &, svgState &, std::string); -static ERR xtag_animate(extSVG *, XMLTag &, OBJECTPTR); -static ERR xtag_animate_colour(extSVG *, XMLTag &, OBJECTPTR); + +static ERR xtag_animate(extSVG *, XMLTag &, XMLTag &, OBJECTPTR); +static ERR xtag_animate_colour(extSVG *, XMLTag &, XMLTag &, OBJECTPTR); static ERR xtag_animate_motion(extSVG *, XMLTag &, OBJECTPTR); static ERR xtag_animate_transform(extSVG *, XMLTag &, OBJECTPTR); -static ERR xtag_default(extSVG *, svgState &, XMLTag &, OBJECTPTR, objVector * &); +static ERR xtag_default(extSVG *, svgState &, XMLTag &, XMLTag &, OBJECTPTR, objVector * &); static ERR xtag_defs(extSVG *, svgState &, XMLTag &, OBJECTPTR); static void xtag_group(extSVG *, svgState &, XMLTag &, OBJECTPTR, objVector * &); static ERR xtag_image(extSVG *, svgState &, XMLTag &, OBJECTPTR, objVector * &); static void xtag_link(extSVG *, svgState &, XMLTag &, OBJECTPTR, objVector * &); static void xtag_morph(extSVG *, XMLTag &, OBJECTPTR); -static ERR xtag_set(extSVG *, XMLTag &, OBJECTPTR); +static ERR xtag_set(extSVG *, XMLTag &, XMLTag &, OBJECTPTR); static void xtag_svg(extSVG *, svgState &, XMLTag &, OBJECTPTR, objVector * &); static void xtag_use(extSVG *, svgState &, XMLTag &, OBJECTPTR); static ERR xtag_style(extSVG *, XMLTag &); @@ -171,6 +179,8 @@ static ERR CMDInit(OBJECTPTR argModule, struct CoreBase *argCoreBase) return ERR::Okay; } +//******************************************************************************************************************** + static ERR CMDExpunge(void) { if (modDisplay) { FreeResource(modDisplay); modDisplay = NULL; } diff --git a/src/svg/tests/animation/w3-animate-elem-37-t.svg b/src/svg/tests/animation/w3-animate-elem-37-t.svg new file mode 100644 index 000000000..6cd796b2b --- /dev/null +++ b/src/svg/tests/animation/w3-animate-elem-37-t.svg @@ -0,0 +1,46 @@ + + + <animateTransform> shape elements + + + + + <path> + + + + + + <rect> + + + + + + <circle> + + + + + + <ellipse> + + + + + + <line> + + + + + + <polyline> + + + + + + <polygon> + + diff --git a/src/svg/utility.cpp b/src/svg/utility.cpp index 2b3b82a17..3238e8387 100644 --- a/src/svg/utility.cpp +++ b/src/svg/utility.cpp @@ -452,8 +452,9 @@ static void add_inherit(extSVG *Self, OBJECTPTR Object, const std::string ID) } //******************************************************************************************************************** +// Parse SVG from a file or string buffer. -static ERR load_svg(extSVG *Self, CSTRING Path, CSTRING Buffer) +static ERR parse_svg(extSVG *Self, CSTRING Path, CSTRING Buffer) { pf::Log log(__FUNCTION__); @@ -465,6 +466,8 @@ static ERR load_svg(extSVG *Self, CSTRING Path, CSTRING Buffer) AdjustLogLevel(1); #endif + if (Self->XML) { FreeResource(Self->XML); Self->XML = NULL; } + objXML *xml; ERR error = ERR::Okay; if (NewObject(ID_XML, NF::INTEGRAL, &xml) IS ERR::Okay) { @@ -540,8 +543,6 @@ static ERR load_svg(extSVG *Self, CSTRING Path, CSTRING Buffer) while ((view) and (view->Class->ClassID != ID_VECTORVIEWPORT)) view = (objVectorViewport *)view->Next; if (view) view->setFields(fl::Width(SCALE(1.0)), fl::Height(SCALE(1.0))); } - - Self->XML = NULL; } else error = ERR::Init; @@ -549,8 +550,6 @@ static ERR load_svg(extSVG *Self, CSTRING Path, CSTRING Buffer) task->setPath(working_path); FreeResource(working_path); } - - FreeResource(xml); } else error = ERR::NewObject; diff --git a/src/xml/xml.fdl b/src/xml/xml.fdl index ed61d1086..a809e0d06 100644 --- a/src/xml/xml.fdl +++ b/src/xml/xml.fdl @@ -48,7 +48,7 @@ module({ name="XML", copyright="Paul Manias © 2001-2024", version=1.0 }, functi [[ inline bool isContent() const { return Name.empty(); } inline bool isTag() const { return !Name.empty(); } - XMLAttrib(std::string pName, std::string pValue) : Name(pName), Value(pValue) { }; + XMLAttrib(std::string pName, std::string pValue = "") : Name(pName), Value(pValue) { }; XMLAttrib() = default; ]])