diff --git a/CMakeLists.txt b/CMakeLists.txt index 0ff1c56..cbd927e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -222,7 +222,15 @@ add_library(rhubarb-animation src/animation/animationRules.h src/animation/mouthAnimation.cpp src/animation/mouthAnimation.h + src/animation/pauseAnimation.cpp + src/animation/pauseAnimation.h + src/animation/roughAnimation.cpp + src/animation/roughAnimation.h + src/animation/shapeRule.cpp + src/animation/shapeRule.h src/animation/shapeShorthands.h + src/animation/tweening.cpp + src/animation/tweening.h ) target_include_directories(rhubarb-animation PUBLIC "src/animation") target_link_libraries(rhubarb-animation diff --git a/src/animation/animationRules.cpp b/src/animation/animationRules.cpp index 385f3a9..3f517d2 100644 --- a/src/animation/animationRules.cpp +++ b/src/animation/animationRules.cpp @@ -2,6 +2,7 @@ #include #include "shapeShorthands.h" #include "array.h" +#include "ContinuousTimeline.h" using std::chrono::duration_cast; using boost::algorithm::clamp; diff --git a/src/animation/mouthAnimation.cpp b/src/animation/mouthAnimation.cpp index 6178ee3..e7ad31b 100644 --- a/src/animation/mouthAnimation.cpp +++ b/src/animation/mouthAnimation.cpp @@ -1,194 +1,9 @@ #include "mouthAnimation.h" -#include "logging.h" -#include -#include -#include -#include #include "timedLogging.h" -#include "shapeShorthands.h" -#include "animationRules.h" - -using std::map; -using std::unordered_set; -using std::unordered_map; -using std::vector; -using boost::optional; -using boost::make_optional; -using std::chrono::duration_cast; -using boost::algorithm::clamp; -using boost::adaptors::transformed; -using std::pair; -using std::tuple; - -JoiningContinuousTimeline insertTweens(JoiningContinuousTimeline shapes) { - centiseconds minTweenDuration = 4_cs; - centiseconds maxTweenDuration = 10_cs; - - JoiningContinuousTimeline result(shapes); - - for (auto first = shapes.begin(), second = std::next(shapes.begin()); - first != shapes.end() && second != shapes.end(); - ++first, ++second) - { - auto pair = getTween(first->getValue(), second->getValue()); - if (!pair) continue; - - Shape tweenShape; - TweenTiming tweenTiming; - std::tie(tweenShape, tweenTiming) = *pair; - TimeRange firstTimeRange = first->getTimeRange(); - TimeRange secondTimeRange = second->getTimeRange(); - - centiseconds tweenStart, tweenDuration; - switch (tweenTiming) { - case TweenTiming::Early: { - tweenDuration = std::min(firstTimeRange.getDuration() / 3, maxTweenDuration); - tweenStart = firstTimeRange.getEnd() - tweenDuration; - break; - } - case TweenTiming::Centered: { - tweenDuration = std::min({ firstTimeRange.getDuration() / 3, secondTimeRange.getDuration() / 3, maxTweenDuration }); - tweenStart = firstTimeRange.getEnd() - tweenDuration / 2; - break; - } - case TweenTiming::Late: { - tweenDuration = std::min(secondTimeRange.getDuration() / 3, maxTweenDuration); - tweenStart = secondTimeRange.getStart(); - break; - } - } - - if (tweenDuration < minTweenDuration) continue; - - result.set(tweenStart, tweenStart + tweenDuration, tweenShape); - } - - return result; -} - -JoiningContinuousTimeline animatePauses(const JoiningContinuousTimeline& shapes) { - JoiningContinuousTimeline result(shapes); - - // Don't close mouth for short pauses - for_each_adjacent(shapes.begin(), shapes.end(), [&](const Timed& lhs, const Timed& pause, const Timed& rhs) { - if (pause.getValue() != X) return; - - const centiseconds maxPausedOpenMouthDuration = 35_cs; - const TimeRange timeRange = pause.getTimeRange(); - if (timeRange.getDuration() <= maxPausedOpenMouthDuration) { - result.set(timeRange, getRelaxedBridge(lhs.getValue(), rhs.getValue())); - } - }); - - // Keep mouth open into pause if it just opened - for_each_adjacent(shapes.begin(), shapes.end(), [&](const Timed& secondLast, const Timed& last, const Timed& pause) { - if (pause.getValue() != X) return; - - centiseconds lastDuration = last.getDuration(); - const centiseconds minOpenDuration = 20_cs; - if (isClosed(secondLast.getValue()) && !isClosed(last.getValue()) && lastDuration < minOpenDuration) { - const centiseconds minSpillDuration = 20_cs; - centiseconds spillDuration = std::min(minSpillDuration, pause.getDuration()); - result.set(pause.getStart(), pause.getStart() + spillDuration, getRelaxedBridge(last.getValue(), X)); - } - }); - - return result; -} - -template -ContinuousTimeline, AutoJoin> boundedTimelinetoContinuousOptional(const BoundedTimeline& timeline) { - return { - timeline.getRange(), boost::none, - timeline | transformed([](const Timed& timedValue) { return Timed>(timedValue.getTimeRange(), timedValue.getValue()); }) - }; -} - -using ShapeRule = tuple>; - -ContinuousTimeline getShapeRules(const BoundedTimeline& phones) { - // Convert to continuous timeline so that silences aren't skipped when iterating - auto continuousPhones = boundedTimelinetoContinuousOptional(phones); - - // Create timeline of shape rules - ContinuousTimeline shapeRules(phones.getRange(), {{X}, boost::none}); - centiseconds previousDuration = 0_cs; - for (const auto& timedPhone : continuousPhones) { - optional phone = timedPhone.getValue(); - centiseconds duration = timedPhone.getDuration(); - - if (phone) { - // Animate one phone - Timeline phoneShapeSets = getShapeSets(*phone, duration, previousDuration); - - // Result timing is relative to phone. Make absolute. - phoneShapeSets.shift(timedPhone.getStart()); - - // Copy to timeline. - // Later shape sets may overwrite earlier ones if overlapping. - for (const auto& timedShapeSet : phoneShapeSets) { - shapeRules.set(timedShapeSet.getTimeRange(), {timedShapeSet.getValue(), phone}); - } - } - - previousDuration = duration; - } - - return shapeRules; -} - -// Create timeline of shapes using a bidirectional algorithm. -// Here's a rough sketch: -// -// * Most consonants result in shape sets with multiple options; most vowels have only one shape option. -// * When speaking, we tend to slur mouth shapes into each other. So we animate from start to end, -// always choosing a shape from the current set that resembles the last shape and is somewhat relaxed. -// * When speaking, we anticipate vowels, trying to form their shape before the actual vowel. -// So whenever we come across a one-shape vowel, we backtrack a little, spreating that shape to the left. -JoiningContinuousTimeline animateRough(const ContinuousTimeline& shapeRules) { - JoiningContinuousTimeline shapes(shapeRules.getRange(), X); - - Shape referenceShape = X; - // Animate forwards - centiseconds lastAnticipatedShapeStart = -1_cs; - for (auto it = shapeRules.begin(); it != shapeRules.end(); ++it) { - const ShapeRule shapeRule = it->getValue(); - const ShapeSet shapeSet = std::get(shapeRule); - const Shape shape = getClosestShape(referenceShape, shapeSet); - shapes.set(it->getTimeRange(), shape); - const auto phone = std::get>(shapeRule); - const bool anticipateShape = phone && isVowel(*phone) && shapeSet.size() == 1; - if (anticipateShape) { - // Animate backwards a little - const Shape anticipatedShape = shape; - const centiseconds anticipatedShapeStart = it->getStart(); - referenceShape = anticipatedShape; - for (auto reverseIt = it; reverseIt != shapeRules.begin(); ) { - --reverseIt; - - // Make sure we haven't animated too far back - centiseconds anticipatingShapeStart = reverseIt->getStart(); - if (anticipatingShapeStart == lastAnticipatedShapeStart) break; - const centiseconds maxAnticipationDuration = 20_cs; - const centiseconds anticipationDuration = anticipatedShapeStart - anticipatingShapeStart; - if (anticipationDuration > maxAnticipationDuration) break; - - // Make sure the new, backwards-animated shape still resembles the anticipated shape - const Shape anticipatingShape = getClosestShape(referenceShape, std::get(reverseIt->getValue())); - if (getBasicShape(anticipatingShape) != getBasicShape(anticipatedShape)) break; - - // Overwrite forward-animated shape with backwards-animated, anticipating shape - shapes.set(reverseIt->getTimeRange(), anticipatingShape); - - referenceShape = anticipatingShape; - } - lastAnticipatedShapeStart = anticipatedShapeStart; - } - referenceShape = anticipateShape ? shape : relax(shape); - } - - return shapes; -} +#include "shapeRule.h" +#include "roughAnimation.h" +#include "pauseAnimation.h" +#include "tweening.h" JoiningContinuousTimeline animate(const BoundedTimeline &phones) { // Create timeline of shape rules diff --git a/src/animation/pauseAnimation.cpp b/src/animation/pauseAnimation.cpp new file mode 100644 index 0000000..9f9746c --- /dev/null +++ b/src/animation/pauseAnimation.cpp @@ -0,0 +1,32 @@ +#include "pauseAnimation.h" +#include "animationRules.h" + +JoiningContinuousTimeline animatePauses(const JoiningContinuousTimeline& shapes) { + JoiningContinuousTimeline result(shapes); + + // Don't close mouth for short pauses + for_each_adjacent(shapes.begin(), shapes.end(), [&](const Timed& lhs, const Timed& pause, const Timed& rhs) { + if (pause.getValue() != Shape::X) return; + + const centiseconds maxPausedOpenMouthDuration = 35_cs; + const TimeRange timeRange = pause.getTimeRange(); + if (timeRange.getDuration() <= maxPausedOpenMouthDuration) { + result.set(timeRange, getRelaxedBridge(lhs.getValue(), rhs.getValue())); + } + }); + + // Keep mouth open into pause if it just opened + for_each_adjacent(shapes.begin(), shapes.end(), [&](const Timed& secondLast, const Timed& last, const Timed& pause) { + if (pause.getValue() != Shape::X) return; + + centiseconds lastDuration = last.getDuration(); + const centiseconds minOpenDuration = 20_cs; + if (isClosed(secondLast.getValue()) && !isClosed(last.getValue()) && lastDuration < minOpenDuration) { + const centiseconds minSpillDuration = 20_cs; + centiseconds spillDuration = std::min(minSpillDuration, pause.getDuration()); + result.set(pause.getStart(), pause.getStart() + spillDuration, getRelaxedBridge(last.getValue(), Shape::X)); + } + }); + + return result; +} diff --git a/src/animation/pauseAnimation.h b/src/animation/pauseAnimation.h new file mode 100644 index 0000000..8b38a36 --- /dev/null +++ b/src/animation/pauseAnimation.h @@ -0,0 +1,7 @@ +#pragma once + +#include "Shape.h" +#include "ContinuousTimeline.h" + +// Takes an existing animation and modifies the pauses (X shapes) to look better. +JoiningContinuousTimeline animatePauses(const JoiningContinuousTimeline& shapes); diff --git a/src/animation/roughAnimation.cpp b/src/animation/roughAnimation.cpp new file mode 100644 index 0000000..a08abb7 --- /dev/null +++ b/src/animation/roughAnimation.cpp @@ -0,0 +1,57 @@ +#include "roughAnimation.h" +#include + +using boost::optional; + +// Create timeline of shapes using a bidirectional algorithm. +// Here's a rough sketch: +// +// * Most consonants result in shape sets with multiple options; most vowels have only one shape option. +// * When speaking, we tend to slur mouth shapes into each other. So we animate from start to end, +// always choosing a shape from the current set that resembles the last shape and is somewhat relaxed. +// * When speaking, we anticipate vowels, trying to form their shape before the actual vowel. +// So whenever we come across a one-shape vowel, we backtrack a little, spreating that shape to the left. +JoiningContinuousTimeline animateRough(const ContinuousTimeline& shapeRules) { + JoiningContinuousTimeline shapes(shapeRules.getRange(), Shape::X); + + Shape referenceShape = Shape::X; + // Animate forwards + centiseconds lastAnticipatedShapeStart = -1_cs; + for (auto it = shapeRules.begin(); it != shapeRules.end(); ++it) { + const ShapeRule shapeRule = it->getValue(); + const ShapeSet shapeSet = std::get(shapeRule); + const Shape shape = getClosestShape(referenceShape, shapeSet); + shapes.set(it->getTimeRange(), shape); + const auto phone = std::get>(shapeRule); + const bool anticipateShape = phone && isVowel(*phone) && shapeSet.size() == 1; + if (anticipateShape) { + // Animate backwards a little + const Shape anticipatedShape = shape; + const centiseconds anticipatedShapeStart = it->getStart(); + referenceShape = anticipatedShape; + for (auto reverseIt = it; reverseIt != shapeRules.begin(); ) { + --reverseIt; + + // Make sure we haven't animated too far back + centiseconds anticipatingShapeStart = reverseIt->getStart(); + if (anticipatingShapeStart == lastAnticipatedShapeStart) break; + const centiseconds maxAnticipationDuration = 20_cs; + const centiseconds anticipationDuration = anticipatedShapeStart - anticipatingShapeStart; + if (anticipationDuration > maxAnticipationDuration) break; + + // Make sure the new, backwards-animated shape still resembles the anticipated shape + const Shape anticipatingShape = getClosestShape(referenceShape, std::get(reverseIt->getValue())); + if (getBasicShape(anticipatingShape) != getBasicShape(anticipatedShape)) break; + + // Overwrite forward-animated shape with backwards-animated, anticipating shape + shapes.set(reverseIt->getTimeRange(), anticipatingShape); + + referenceShape = anticipatingShape; + } + lastAnticipatedShapeStart = anticipatedShapeStart; + } + referenceShape = anticipateShape ? shape : relax(shape); + } + + return shapes; +} diff --git a/src/animation/roughAnimation.h b/src/animation/roughAnimation.h new file mode 100644 index 0000000..86781ee --- /dev/null +++ b/src/animation/roughAnimation.h @@ -0,0 +1,6 @@ +#pragma once + +#include "shapeRule.h" + +// Does a rough animation (no tweening, special pause animation, etc.) using a bidirectional algorithm. +JoiningContinuousTimeline animateRough(const ContinuousTimeline& shapeRules); diff --git a/src/animation/shapeRule.cpp b/src/animation/shapeRule.cpp new file mode 100644 index 0000000..9ea6424 --- /dev/null +++ b/src/animation/shapeRule.cpp @@ -0,0 +1,45 @@ +#include "shapeRule.h" +#include +#include "ContinuousTimeline.h" + +using boost::optional; +using boost::adaptors::transformed; + +template +ContinuousTimeline, AutoJoin> boundedTimelinetoContinuousOptional(const BoundedTimeline& timeline) { + return{ + timeline.getRange(), boost::none, + timeline | transformed([](const Timed& timedValue) { return Timed>(timedValue.getTimeRange(), timedValue.getValue()); }) + }; +} + +ContinuousTimeline getShapeRules(const BoundedTimeline& phones) { + // Convert to continuous timeline so that silences aren't skipped when iterating + auto continuousPhones = boundedTimelinetoContinuousOptional(phones); + + // Create timeline of shape rules + ContinuousTimeline shapeRules(phones.getRange(), {{Shape::X}, boost::none}); + centiseconds previousDuration = 0_cs; + for (const auto& timedPhone : continuousPhones) { + optional phone = timedPhone.getValue(); + centiseconds duration = timedPhone.getDuration(); + + if (phone) { + // Animate one phone + Timeline phoneShapeSets = getShapeSets(*phone, duration, previousDuration); + + // Result timing is relative to phone. Make absolute. + phoneShapeSets.shift(timedPhone.getStart()); + + // Copy to timeline. + // Later shape sets may overwrite earlier ones if overlapping. + for (const auto& timedShapeSet : phoneShapeSets) { + shapeRules.set(timedShapeSet.getTimeRange(), {timedShapeSet.getValue(), phone}); + } + } + + previousDuration = duration; + } + + return shapeRules; +} diff --git a/src/animation/shapeRule.h b/src/animation/shapeRule.h new file mode 100644 index 0000000..d16abda --- /dev/null +++ b/src/animation/shapeRule.h @@ -0,0 +1,12 @@ +#pragma once + +#include "Phone.h" +#include "animationRules.h" +#include "BoundedTimeline.h" +#include "ContinuousTimeline.h" + +// A shape set with its original phone +using ShapeRule = std::tuple>; + +// Returns shape rules for an entire timeline of phones. +ContinuousTimeline getShapeRules(const BoundedTimeline& phones); diff --git a/src/animation/tweening.cpp b/src/animation/tweening.cpp new file mode 100644 index 0000000..4f79767 --- /dev/null +++ b/src/animation/tweening.cpp @@ -0,0 +1,48 @@ +#include "tweening.h" +#include "animationRules.h" + +JoiningContinuousTimeline insertTweens(const JoiningContinuousTimeline& shapes) { + centiseconds minTweenDuration = 4_cs; + centiseconds maxTweenDuration = 10_cs; + + JoiningContinuousTimeline result(shapes); + + for (auto first = shapes.begin(), second = std::next(shapes.begin()); + first != shapes.end() && second != shapes.end(); + ++first, ++second) + { + auto pair = getTween(first->getValue(), second->getValue()); + if (!pair) continue; + + Shape tweenShape; + TweenTiming tweenTiming; + std::tie(tweenShape, tweenTiming) = *pair; + TimeRange firstTimeRange = first->getTimeRange(); + TimeRange secondTimeRange = second->getTimeRange(); + + centiseconds tweenStart, tweenDuration; + switch (tweenTiming) { + case TweenTiming::Early: { + tweenDuration = std::min(firstTimeRange.getDuration() / 3, maxTweenDuration); + tweenStart = firstTimeRange.getEnd() - tweenDuration; + break; + } + case TweenTiming::Centered: { + tweenDuration = std::min({firstTimeRange.getDuration() / 3, secondTimeRange.getDuration() / 3, maxTweenDuration}); + tweenStart = firstTimeRange.getEnd() - tweenDuration / 2; + break; + } + case TweenTiming::Late: { + tweenDuration = std::min(secondTimeRange.getDuration() / 3, maxTweenDuration); + tweenStart = secondTimeRange.getStart(); + break; + } + } + + if (tweenDuration < minTweenDuration) continue; + + result.set(tweenStart, tweenStart + tweenDuration, tweenShape); + } + + return result; +} diff --git a/src/animation/tweening.h b/src/animation/tweening.h new file mode 100644 index 0000000..4a2d8f7 --- /dev/null +++ b/src/animation/tweening.h @@ -0,0 +1,7 @@ +#pragma once + +#include "Shape.h" +#include "ContinuousTimeline.h" + +// Takes an existing animation and inserts inbetween shapes for smoother results. +JoiningContinuousTimeline insertTweens(const JoiningContinuousTimeline& shapes);