Understanding CSS triangles and rhombus interactions in React native with absolute positions

Viewed 33

I have a complex structure diagram with Rhombuses and triangles. I was able to create the structure using CSS tricks and absolute position property.

I want each rhombus and triangle to be clickable but the behavior is not as expected given how CSS triangles work with the borders.

An output image with some markers In this image we have a rhombus and two triangles,

What I want:

  • On clicking at A,B,E - rhombus is clicked
  • On clicking C,D - first triangle (numbered 2 in picture) is clicked.

What happens instead:

  • On clicking A, nothing happens (I suspect the absolute and transform property here)
  • On clicking B, first triangle (numbered 2 in picture) is clicked. It is because of the transparent right border of first triangle.
  • On clicking C, second triangle (numbered 3 in picture) is clicked. It is because of the transparent top border of second triangle.
  • On clicking D, first triangle is clicked which is the expected behaviour.
  • On clicking E, rhombus is clicked which is the expected behaviour.

How can I make such complex interactive patterns with series of such shapes? Shall I consider using images/SVGs instead of CSS shapes? I also need to add multiple dynamic count of numbers/text/emojis in each shape.

Here's a simplified snippet of code:

import React from 'react';
import {
  View,
  TouchableOpacity,
  StyleSheet,
  SafeAreaView,
} from 'react-native';

const MyComplexDiagram = ({navigation, route}) => {

  const clickShape = text => {
    console.log('clicked: ', text);
  };
  return (
    <SafeAreaView style={{flex: 1}}>
      <View>
        <TouchableOpacity onPress={() => clickShape("rhombus)}>
          <View style={[styles.rhombus]} />
        </TouchableOpacity>

        <TouchableOpacity onPress={() => clickShape("first triangle")}>
          <View style={[styles.firstTriangle]} />
        </TouchableOpacity>
        
        <TouchableOpacity onPress={() => clickShape("second triangle")}>
          <View style={[styles.secondTriangle]} />
        </TouchableOpacity>
    </SafeAreaView>
  );
};

export default MyComplexShape;

const styles = StyleSheet.create({
  rhombus: {
    position: 'absolute',
    top: 50,
    left: 100,
    width: 100,
    height: 100,
    backgroundColor: 'green',
    transform: [{rotate: '45deg'}],
  },
  firstTriangle: {
    position: 'absolute',
    top: 0,
    left: 0,
    backgroundColor: 'transparent',
    borderStyle: 'solid',
    borderLeftWidth: 50,
    borderRightWidth: 50,
    borderTopWidth: 50,
    borderLeftColor: 'transparent',
    borderRightColor: 'transparent',
    borderTopColor: 'red',
  }),
  secondTriangle: {
    position: 'absolute',
    top: 0,
    left: 0,
    backgroundColor: 'transparent',
    borderStyle: 'solid',
    borderLeftWidth: 50 * Math.sqrt(2),
    borderTopWidth: 50 * Math.sqrt(2),
    borderBottomWidth: 50 * Math.sqrt(2),
    borderLeftColor: 'black',
    borderBottomColor: 'transparent',
    borderTopColor: 'transparent',
  }
});

1 Answers

Im not sure if the same thing happens in a browser context, but the issue with the css trick is that although the view appears to be a triangle, it is actually still a rectangle. Giving the triangles a non-transparent background color will demonstrate this. The middle triangle is intercepting the first triangle presses because its hidden rectangle entirely overlaps the first triangle. (Here's a demo showing that). You can overcome this by the other TouchableOpacities a higher zIndex than the middle one:

import React, { useMemo, useEffect, useRef, useState } from 'react';
import {
  View,
  TouchableOpacity,
  StyleSheet,
  SafeAreaView,
  useWindowDimensions,
  Button,
  Text
} from 'react-native';

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  withSequence,
  withRepeat,
  cancelAnimation,
  withDelay,
} from 'react-native-reanimated';
const getHypotenuse = (side1, side2) => {
  if (!side2) side2 = side1;
  return Math.sqrt(side1 ** 2 + side2 ** 2);
};

const startRotation = 270;
const MyComplexDiagram = ({ navigation, route }) => {
  const [animationIsRunning, setAnimationIsRunning] = useState(false);
  const rotation = useSharedValue(startRotation);
  const backgroundColor = useSharedValue('rgba(0,0,0,0)');
  const { width, height } = useWindowDimensions();
  const rotationStyle = useAnimatedStyle(() => {
    return {
      transform: [{ rotate: `${rotation.value}deg` }],
    };
  });
  const colorStyle = useAnimatedStyle(() => {
    return {
      backgroundColor: backgroundColor.value,
    };
  });
  const { rhombusStyle, middleTriangleStyle, leftTriangleStyle } =
    useMemo(() => {
      const remainingWidth =
        width - styles.container.padding - styles.shapeContainer.padding;
      const rhombusSize = remainingWidth * 0.3;
      const rhombusHypotenuse = getHypotenuse(rhombusSize);
      const rhombusLeft = remainingWidth - rhombusHypotenuse;
      const rhombusTop = 25;
      const middleLeft = rhombusLeft - rhombusSize;
      const leftLeft = middleLeft;
      return {
        rhombusStyle: {
          width: rhombusSize,
          height: rhombusSize,
          left: rhombusLeft,
          top: rhombusTop,
        },
        middleTriangleStyle: {
          borderWidth: rhombusSize * 0.8,
          borderLeftWidth: rhombusSize * 0.8,
          left: rhombusLeft - rhombusSize * 1.1,
          top: 10,
        },
        leftTriangleStyle: {
          borderWidth: rhombusSize,
          borderBottom: rhombusSize,
          left: rhombusLeft - rhombusHypotenuse * 1.7,
          top: 0,
        },
      };
    }, [width]);
  const clickShape = (text) => {
    console.log('clicked: ', text);
  };
  const toggleAnimation = () => {
    if (animationIsRunning) {
      backgroundColor.value = withTiming('rgba(0,0,0,0)');
    } else {
      backgroundColor.value = withRepeat(
        withSequence(
          withTiming('rgba(255,0,0,1)', { duration: 2000 }),
          withTiming('rgba(0,0,0,0'),
          withDelay(4000,withTiming('rgba(0,0,0,0'))
        ),
        -1
      );
    }
    setAnimationIsRunning((prev) => !prev);
  };

  return (
    <SafeAreaView style={styles.container}>
      <Button
        title={`${animationIsRunning ? 'Hide' : 'Show'} Triangle Background`}
        onPress={toggleAnimation}
      />
      <Text style={{zIndex:5}}>{animationIsRunning ? 'Using zIndex to force black triangle to be on top of orange':'Default behavior without zIndex changes'}</Text>
      <View style={styles.shapeContainer}>
        <TouchableOpacity
          style={animationIsRunning && { zIndex: 5 }}
          onPress={() => clickShape('leftTriangle')}>
          <Animated.View
            style={[styles.leftTriangle, leftTriangleStyle, colorStyle]}
          />
        </TouchableOpacity>
        <TouchableOpacity
          style={animationIsRunning && { zIndex: 4 }}
          onPress={() => clickShape('middleTriangle')}>
          <Animated.View
            style={[
              styles.middleTriangle,
              middleTriangleStyle,
              rotationStyle,
              colorStyle,
            ]}
          />
        </TouchableOpacity>
        <TouchableOpacity
          style={animationIsRunning && { zIndex: 5 }}
          onPress={() => clickShape('rhombus')}>
          <Animated.View style={[styles.rhombus, rhombusStyle]} />
        </TouchableOpacity>
      </View>
    </SafeAreaView>
  );
};

export default MyComplexDiagram;

const styles = StyleSheet.create({
  container: {
    flex: 1,
    // alignItems:'center',
    justifyContent: 'center',
    padding: 8,
  },
  shapeContainer: {
    padding: 10,
  },
  rhombus: {
    transform: [{ rotate: '45deg' }],
    position: 'absolute',
    top: 0,
    bottom: 0,
    right: 0,
    left: 0,
    backgroundColor: 'green',
  },
  middleTriangle: {
    position: 'absolute',
    top: 0,
    bottom: 0,
    right: 0,
    left: 0,
    width: 0,
    height: 0,
    borderColor: 'transparent',
    borderRightColor: 'orange',
  },
  leftTriangle: {
    position: 'absolute',
    transform: [{ rotate: '225deg' }],

    top: 0,
    bottom: 0,
    right: 0,
    left: 0,
    width: 0,
    height: 0,
    borderColor: 'transparent',
    borderLeftColor: 'black',
  },
});

Related