Skip to content

Commit

Permalink
Merge pull request #411 from Shopify/feature/updated-path-interpolation
Browse files Browse the repository at this point in the history
Updated Interpolation example and fix rare crash
  • Loading branch information
chrfalch authored Apr 20, 2022
2 parents 2d36a23 + d0ccedd commit e76041d
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 128 deletions.
121 changes: 90 additions & 31 deletions example/src/Examples/Graphs/Interpolation.tsx
Original file line number Diff line number Diff line change
@@ -1,66 +1,125 @@
import type {
AnimatedProps,
PathDef,
PathProps,
SkPath,
} from "@shopify/react-native-skia";
import {
processPath,
Easing,
Canvas,
Fill,
LinearGradient,
Path,
runSpring,
Spring,
runTiming,
useDerivedValue,
useValue,
vec,
} from "@shopify/react-native-skia";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { StyleSheet, Text, View } from "react-native";

import { createGraphPath } from "./createGraphPath";
import type { GraphProps } from "./types";

export const Interpolation: React.FC<GraphProps> = ({ height, width }) => {
const path = useMemo(
() => createGraphPath(width, height, 60),
[height, width]
const [currentPath, setCurrentPath] = useState<SkPath>(() =>
createGraphPath(width, height, 60)
);

const path2 = useMemo(
() => createGraphPath(width, height, 60),
[height, width]
);
const onPress = useCallback(() => {
setCurrentPath(createGraphPath(width, height, 60));
}, [height, width]);

const progress = useValue(0);
const [toggled, setToggled] = useState(false);
const onPress = useCallback(() => setToggled((p) => !p), []);
useEffect(() => {
runSpring(progress, toggled ? 1 : 0, Spring.Config.Gentle);
}, [progress, toggled]);

const interpolatedPath = useDerivedValue(
() => path.interpolate(path2, progress.current),
[progress]
);
const isRunning = { value: true };
const dispatchChange = () => {
setTimeout(() => {
setCurrentPath(createGraphPath(width, height, 60));
if (isRunning.value) {
dispatchChange();
}
}, Math.random() * 400 + 500);
};
dispatchChange();
return () => {
isRunning.value = false;
};
}, [height, width]);

return (
<View style={{ height, marginBottom: 10 }} onTouchEnd={onPress}>
<Canvas style={styles.graph}>
<Fill color="black" />
<Path
path={interpolatedPath}
<TransitioningPath
path={currentPath}
strokeWidth={4}
style="stroke"
strokeJoin="round"
strokeCap="round"
>
<LinearGradient
start={vec(0, height * 0.5)}
end={vec(width * 0.5, height * 0.5)}
colors={["black", "#cccc66"]}
/>
</Path>
color="#cccc66"
/>
</Canvas>
<Text>Touch graph to interpolate</Text>
</View>
);
};

const TransitioningPath = ({
path,
...props
}: AnimatedProps<PathProps> & { path: PathDef }) => {
// Save current and next paths (initially the same)
const currentPathRef = useRef(processPath(path));
const nextPathRef = useRef(processPath(path));

// Progress value drives the animation
const progress = useValue(0);

// The animated path is derived from the current and next paths based
// on the value of the progress.
const animatedPath = useDerivedValue(
() =>
nextPathRef.current.interpolate(currentPathRef.current, progress.current),
[progress, path]
);

useEffect(() => {
if (currentPathRef.current !== path) {
// Process path - can be an SVG string
const processedPath = processPath(path);

// Ensure paths have the same length
if (
currentPathRef.current.countPoints() !== processedPath.countPoints()
) {
console.warn(
"Paths must have the same length. Skipping interpolation."
);
return;
}
// Set current path to the current interpolated path to make
// sure we can interrupt animations
currentPathRef.current = animatedPath.current;
// Set the next path to be the value in the updated path property
nextPathRef.current = processedPath;
// reset progress - this will cause the derived value to be updated and
// the path to be repainted through its parent canvas.
progress.current = 0;
// Run animation
runTiming(progress, 1, {
duration: 750,
easing: Easing.inOut(Easing.cubic),
});
}
}, [animatedPath, path, progress]);

return <Path {...props} path={animatedPath} />;
};

TransitioningPath.defaultProps = {
start: 0,
end: 1,
};

const styles = StyleSheet.create({
graph: {
flex: 1,
Expand Down
68 changes: 0 additions & 68 deletions example/src/Examples/Graphs/Mount.tsx

This file was deleted.

2 changes: 0 additions & 2 deletions example/src/Examples/Graphs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from "react";
import { View, StyleSheet, useWindowDimensions } from "react-native";

import { Interpolation } from "./Interpolation";
import { MountAnimation } from "./Mount";
import { Slider } from "./Slider";

const Padding = 10;
Expand All @@ -11,7 +10,6 @@ export const GraphsScreen: React.FC = () => {
const { width, height } = useWindowDimensions();
return (
<View style={styles.container}>
<MountAnimation height={height * 0.25} width={width - Padding * 2} />
<Interpolation height={height * 0.25} width={width - Padding * 2} />
<Slider height={height * 0.25} width={width - Padding * 2} />
</View>
Expand Down
11 changes: 4 additions & 7 deletions package/cpp/rnskia/RNSkAnimation.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,24 @@ using namespace facebook;
/**
Implements an animation that can be used to drive other values
*/
class RNSkAnimation : public RNSkClockValue, public std::enable_shared_from_this<RNSkAnimation>
class RNSkAnimation : public RNSkClockValue
{

public:
RNSkAnimation(std::shared_ptr<RNSkPlatformContext> platformContext,
size_t identifier,
jsi::Runtime& runtime,
const jsi::Value *arguments,
size_t count)
: RNSkClockValue(platformContext, identifier, runtime, arguments, count) {
size_t count) :
RNSkClockValue(platformContext, identifier, runtime, arguments, count) {
// Save the update function
_updateFunction = std::make_shared<jsi::Function>(arguments[0].asObject(runtime).asFunction(runtime));

// Set state to undefined initially.
_args[1] = jsi::Value::undefined();
}

~RNSkAnimation() {
// We need to stop/unsubscribe
stopClock();
}
virtual ~RNSkAnimation() {}

JSI_HOST_FUNCTION(cancel) {
stopClock();
Expand Down
8 changes: 4 additions & 4 deletions package/cpp/rnskia/values/RNSkClockValue.h
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
#include <algorithm>
#include <functional>
#include <chrono>
#include <mutex>

namespace RNSkia
{
Expand Down Expand Up @@ -38,7 +37,7 @@ enum RNSkClockState {
update(_runtime, static_cast<double>(0));
}

~RNSkClockValue() {
virtual ~RNSkClockValue() {
stopClock();
}

Expand Down Expand Up @@ -100,10 +99,11 @@ enum RNSkClockState {
return;
}

// Avoid moving on if we are being called after the dtor was started
// Ensure we call any updates from the draw loop on the javascript thread
getContext()->runOnJavascriptThread(
[this]() {
// To ensure that this shared_ptr instance is not deallocated before we are done
// running the update lambda we pass a shared from this to the lambda scope.
[self = shared_from_this(), this]() {
if(_state == RNSkClockState::Running) {
auto now = std::chrono::high_resolution_clock::now();
auto deltaFromStart = std::chrono::duration_cast<std::chrono::milliseconds>(now - _start).count();
Expand Down
2 changes: 1 addition & 1 deletion package/cpp/rnskia/values/RNSkDerivedValue.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ class RNSkDerivedValue : public RNSkReadonlyValue
dependencyUpdated(runtime);
}

~RNSkDerivedValue() {
virtual ~RNSkDerivedValue() {
// Unregister listeners
for(const auto &unsubscribe: _unsubscribers) {
unsubscribe();
Expand Down
24 changes: 9 additions & 15 deletions package/cpp/rnskia/values/RNSkReadonlyValue.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
#include <algorithm>
#include <functional>
#include <chrono>
#include <mutex>
#include <unordered_map>
#include <utility>
#include <memory>

#include <jsi/jsi.h>
Expand All @@ -22,16 +20,15 @@ using namespace facebook;
Implements a readonly Value that is updated every time the screen is redrawn. Its value will be the
number of milliseconds since the animation value was started.
*/
class RNSkReadonlyValue : public JsiSkHostObject
class RNSkReadonlyValue : public JsiSkHostObject,
public std::enable_shared_from_this<RNSkReadonlyValue>
{
public:
RNSkReadonlyValue(std::shared_ptr<RNSkPlatformContext> platformContext)
: JsiSkHostObject(platformContext),
_propNameId(jsi::PropNameID::forUtf8(*platformContext->getJsRuntime(), "value")) {}

~RNSkReadonlyValue() {
_invalidated = true;
}
virtual ~RNSkReadonlyValue() { }

JSI_PROPERTY_GET(__typename__) {
return jsi::String::createFromUtf8(runtime, "RNSkValue");
Expand All @@ -43,16 +40,17 @@ class RNSkReadonlyValue : public JsiSkHostObject

JSI_EXPORT_PROPERTY_GETTERS(JSI_EXPORT_PROP_GET(RNSkReadonlyValue, __typename__),
JSI_EXPORT_PROP_GET(RNSkReadonlyValue, current))



JSI_HOST_FUNCTION(addListener) {
if(!arguments[0].isObject() || !arguments[0].asObject(runtime).isFunction(runtime)) {
jsi::detail::throwJSError(runtime, "Expected function as first parameter.");
return jsi::Value::undefined();
}
auto callback = std::make_shared<jsi::Function>(arguments[0].asObject(runtime).asFunction(runtime));

auto unsubscribe = addListener([this, callback = std::move(callback)](jsi::Runtime& runtime){
auto unsubscribe = addListener([self = shared_from_this(),
this,
callback = std::move(callback)](jsi::Runtime& runtime){
callback->call(runtime, get_current(runtime));
});

Expand All @@ -76,10 +74,8 @@ class RNSkReadonlyValue : public JsiSkHostObject
std::lock_guard<std::mutex> lock(_mutex);
auto listenerId = _listenerId++;
_listeners.emplace(listenerId, cb);
return [this, listenerId]() {
if(!_invalidated) {
removeListener(listenerId);
}
return [self = shared_from_this(), this, listenerId]() {
removeListener(listenerId);
};
}

Expand Down Expand Up @@ -128,8 +124,6 @@ class RNSkReadonlyValue : public JsiSkHostObject
_listeners.erase(listenerId);
}

std::atomic<bool> _invalidated = { false };

private:
jsi::PropNameID _propNameId;
std::shared_ptr<jsi::Object> _valueHolder;
Expand Down

0 comments on commit e76041d

Please sign in to comment.