diff --git a/example/src/App.tsx b/example/src/App.tsx index 4fd9c447a9..32c7319c2c 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -14,6 +14,7 @@ import { Aurora, Breathe, Filters, + MagnifyingGlass, Gooey, GraphsScreen, Hue, @@ -44,6 +45,7 @@ const linking: LinkingOptions = { API: "api", Breathe: "breathe", Filters: "filters", + MagnifyingGlass: "magnifying-glass", Gooey: "gooey", Hue: "hue", Matrix: "matrix", @@ -127,6 +129,7 @@ const App = () => { + { render(); @@ -69,4 +70,8 @@ it("should render the Filter example correctly", () => { render(); }); +it("should render the MagnifyingGlass example correctly", () => { + render(); +}); + afterEach(cleanup); diff --git a/example/src/Examples/MagnifyingGlass/MagnifyingGlass.tsx b/example/src/Examples/MagnifyingGlass/MagnifyingGlass.tsx new file mode 100644 index 0000000000..f4d6b1e896 --- /dev/null +++ b/example/src/Examples/MagnifyingGlass/MagnifyingGlass.tsx @@ -0,0 +1,244 @@ +import React, { useState } from "react"; +import type { LayoutChangeEvent } from "react-native"; +import { StyleSheet, Switch, Text, View } from "react-native"; +import { + Canvas, + Fill, + ImageShader, + Skia, + Shader, + useImage, + vec, +} from "@shopify/react-native-skia"; +import { useDerivedValue, useSharedValue } from "react-native-reanimated"; +import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import Slider from "@react-native-community/slider"; + +const source = Skia.RuntimeEffect.Make(` +uniform shader image; +uniform vec2 screen; +uniform vec2 touchPos; +uniform float drawing; +uniform float zoomLevel; +uniform float magnifierDiameter; +uniform float isFixed; + +const vec2 magnifier_center = vec2(80); + +// unit in percentage relative to the screen width +const float magnifier_offset = 0.025; + +// in pixels +const float border_width = 4; + +half4 main(vec2 pos) { + if (drawing == 0) + return image.eval(pos); + + // Convert to UV coordinates, accounting for aspect ratio + vec2 uv = pos / screen.y; + + vec2 touch = touchPos.xy; + if (touch == vec2(0)) + touch = screen.xy / 2; + + // UV coordinates of touch + vec2 touch_uv = touch / screen.y; + + // Distance to touch + float touch_dist = distance(uv, touch_uv); + + // UV coordinates of magnifier center + vec2 magnifier_uv = vec2((screen.x / screen.y) * (magnifierDiameter / 2 + magnifier_offset)); + float magnifier_radius = (screen.x / screen.y) * magnifierDiameter / 2; + + // Distance from magnifier to touch + float magnifier_touch_dist = distance(magnifier_uv, touch_uv); + + if (magnifier_touch_dist < magnifier_radius) + magnifier_uv.x = (screen.x / screen.y) - magnifier_uv.x; + + // Distance to magnifier center + float magnifier_dist = distance(uv, magnifier_uv); + + // Draw the texture + half4 fragColor = image.eval(uv * screen.y); + + float border = ((screen.x / screen.y) / screen.y) * border_width; + + if (isFixed == 1) { + // Draw the outline of the glass + if (magnifier_dist < magnifier_radius + border) + fragColor = half4(1, 1, 1, 1); + + // Draw a zoomed-in version of the texture + if (magnifier_dist < magnifier_radius) + fragColor = image.eval((touch_uv - ((magnifier_uv - uv) * zoomLevel)) * screen.y); + } else { + // Draw the outline of the glass + if (touch_dist < magnifier_radius + border) + fragColor = half4(1, 1, 1, 1); + + // Draw a zoomed-in version of the texture + if (touch_dist < magnifier_radius) + fragColor = image.eval((uv + (touch_uv - uv) * (1 - zoomLevel)) * screen.y); + } + + return fragColor; +}`)!; + +export const MagnifyingGlass = () => { + const canvasWidth = useSharedValue(0); + const canvasHeight = useSharedValue(0); + + const drawing = useSharedValue(0); + const touchPosX = useSharedValue(0); + const touchPosY = useSharedValue(0); + + // 1 means no zoom and 0 max + const zoomLevel = useSharedValue(0.4); + // percentage relative to the screen width + const magnifierDiameter = useSharedValue(0.33); + + const [isFixed, setIsFixed] = useState(true); + const isFixedSharedValue = useSharedValue(1); + + const image = useImage(require("../../assets/oslo2.jpg")); + + const gesture = Gesture.Pan() + .minDistance(0) + .onBegin((e) => { + touchPosX.value = e.x; + touchPosY.value = e.y; + drawing.value = 1; + }) + .onChange((e) => { + touchPosX.value = e.x; + touchPosY.value = e.y; + }) + .onFinalize(() => { + drawing.value = 0; + }); + + const uniforms = useDerivedValue(() => { + return { + screen: vec(canvasWidth.value, canvasHeight.value), + touchPos: vec(touchPosX.value, touchPosY.value), + drawing: drawing.value, + zoomLevel: zoomLevel.value, + magnifierDiameter: magnifierDiameter.value, + isFixed: isFixedSharedValue.value, + }; + }, [ + drawing, + canvasWidth, + canvasHeight, + zoomLevel, + magnifierDiameter, + isFixedSharedValue, + ]); + + if (!image) { + return ( + + Loading image... + + ); + } + + const handleCanvasLayoutChange = (event: LayoutChangeEvent) => { + canvasWidth.value = event.nativeEvent.layout.width; + canvasHeight.value = event.nativeEvent.layout.height; + }; + + return ( + + + + + + + + + + + + + Zoom: + (zoomLevel.value = value)} + onSlidingStart={() => { + drawing.value = 1; + touchPosX.value = canvasWidth.value / 2; + touchPosY.value = canvasHeight.value / 2; + }} + onSlidingComplete={() => { + drawing.value = 0; + }} + /> + + + Size: + (magnifierDiameter.value = value)} + onSlidingStart={() => { + drawing.value = 1; + touchPosX.value = canvasWidth.value / 2; + touchPosY.value = canvasHeight.value / 2; + }} + onSlidingComplete={() => { + drawing.value = 0; + }} + /> + + + Fixed? + { + setIsFixed((prev) => { + isFixedSharedValue.value = !prev ? 1 : 0; + return !prev; + }); + }} + /> + + + + ); +}; + +const styles = StyleSheet.create({ + controls: { + height: 120, + paddingHorizontal: "15%", + justifyContent: "space-evenly", + alignItems: "flex-start", + backgroundColor: "rgba(0,0,0,0.5)", + gap: 12, + }, + control: { + flexDirection: "row", + alignItems: "center", + gap: 12, + }, +}); diff --git a/example/src/Examples/MagnifyingGlass/index.tsx b/example/src/Examples/MagnifyingGlass/index.tsx new file mode 100644 index 0000000000..7fa2064a2d --- /dev/null +++ b/example/src/Examples/MagnifyingGlass/index.tsx @@ -0,0 +1 @@ +export { MagnifyingGlass } from "./MagnifyingGlass"; diff --git a/example/src/Examples/SpeedTest/Slider.tsx b/example/src/Examples/SpeedTest/Slider.tsx index 9e1e1afc49..66b1fa1431 100644 --- a/example/src/Examples/SpeedTest/Slider.tsx +++ b/example/src/Examples/SpeedTest/Slider.tsx @@ -14,6 +14,7 @@ interface Props { onValueChange: (value: number) => void; minValue: number; maxValue: number; + initialValue?: number; } const size = 32; @@ -27,6 +28,7 @@ export const Slider: React.FC = ({ onValueChange, minValue, maxValue, + initialValue = minValue, }) => { const { width } = useWindowDimensions(); @@ -34,7 +36,13 @@ export const Slider: React.FC = ({ const pickerR = size / 2; const progressBarHeight = 3; - const translateX = useSharedValue(-pickerR); + const initialTranslateX = interpolate( + initialValue, + [minValue, maxValue], + [-pickerR, sliderWidth - pickerR] + ); + + const translateX = useSharedValue(initialTranslateX); const contextX = useSharedValue(0); const scale = useSharedValue(1); diff --git a/example/src/Examples/index.ts b/example/src/Examples/index.ts index 024a807cae..2285ac8dcf 100644 --- a/example/src/Examples/index.ts +++ b/example/src/Examples/index.ts @@ -2,6 +2,7 @@ export * from "./Reanimated"; export * from "./API"; export * from "./Breathe"; export * from "./Filters"; +export * from "./MagnifyingGlass"; export * from "./Gooey"; export * from "./Matrix"; export * from "./Graphs"; diff --git a/example/src/Home/HomeScreen.tsx b/example/src/Home/HomeScreen.tsx index a96f939d12..bc9dc5020b 100644 --- a/example/src/Home/HomeScreen.tsx +++ b/example/src/Home/HomeScreen.tsx @@ -27,6 +27,11 @@ export const HomeScreen = () => { description="Simple Image Filters" route="Filters" /> +