• Экраны
  • Доки
1Online
ПодпискаКаталог
  • Экраны
  • Доки
1Online
ПодпискаКаталог
  • Экраны
  • Доки
1Online
ПодпискаКаталог
Приветствие V2 — экран из раздела «Приветствие»
Приветствие

Приветствие V2

Первый запуск и онбординг — стартовый кадр приложения.

Установка

src/shared/components/blocks/welcome-v2
npx astra-rn add welcome-v2
pnpm dlx astra-rn add welcome-v2
yarn dlx astra-rn add welcome-v2
bunx --bun astra-rn add welcome-v2

Зависимости

2 пакета
npm install @shopify/react-native-skia react-native-svg
pnpm add @shopify/react-native-skia react-native-svg
yarn add @shopify/react-native-skia react-native-svg
bun add @shopify/react-native-skia react-native-svg

Исходник

3 файла
import { Canvas, Fill, Shader, Skia } from "@shopify/react-native-skia";
import React, { memo, useCallback, useMemo, useState } from "react";
import {
  Pressable,
  StyleSheet,
  Text,
  View,
  useWindowDimensions,
} from "react-native";
import Svg, { Circle, Rect } from "react-native-svg";
import {
  ACTION_HEIGHT,
  ACTION_RADIUS,
  COLORS,
  CONTENT_HORIZONTAL_PADDING,
  DEFAULT_ACTION_LABEL,
  DEFAULT_FOOTER_ACTION_LABEL,
  DEFAULT_FOOTER_PROMPT,
  DEFAULT_LEGAL_PREFIX,
  DEFAULT_LEGAL_SEPARATOR,
  DEFAULT_LEGAL_SUFFIX,
  DEFAULT_PRIVACY_LABEL,
  DEFAULT_SUBTITLE,
  DEFAULT_TERMS_LABEL,
  DEFAULT_TITLE,
  LOGO_SIZE,
  ORB_BACKDROP,
  ORB_BLUE,
  ORB_BLUE_ANGLE,
  ORB_BLUE_GAIN,
  ORB_BLUE_RING,
  ORB_BLUE_SPREAD,
  ORB_BLUE_WIDTH,
  ORB_BLUE_OFFSET,
  ORB_CENTER,
  ORB_CLOUD,
  ORB_CLOUD_SCALE,
  ORB_DOME_FADE,
  ORB_EXPOSURE,
  ORB_GRAIN,
  ORB_GRAIN_SCALE,
  ORB_HAZE_GAIN,
  ORB_HAZE_RING,
  ORB_HAZE_WIDTH,
  ORB_SCALE,
  ORB_SHADER,
  ORB_WARM,
  ORB_WARM_ANGLE,
  ORB_WARM_GAIN,
  ORB_WARM_OFFSET,
  ORB_WARM_RING,
  ORB_WARM_SPREAD,
  ORB_WARM_WIDTH,
} from "./const";
import type { IWelcomeScreenV2 } from "./types";

const WelcomeScreenV2: React.FC<IWelcomeScreenV2> = ({
  title = DEFAULT_TITLE,
  subtitle = DEFAULT_SUBTITLE,
  actionLabel = DEFAULT_ACTION_LABEL,
  footerPrompt = DEFAULT_FOOTER_PROMPT,
  footerActionLabel = DEFAULT_FOOTER_ACTION_LABEL,
  legalPrefix = DEFAULT_LEGAL_PREFIX,
  termsLabel = DEFAULT_TERMS_LABEL,
  legalSeparator = DEFAULT_LEGAL_SEPARATOR,
  privacyLabel = DEFAULT_PRIVACY_LABEL,
  legalSuffix = DEFAULT_LEGAL_SUFFIX,
  logo,
  style,
  onActionPress,
  onFooterPress,
  onTermsPress,
  onPrivacyPress,
}: IWelcomeScreenV2) => {
  const { width, height } = useWindowDimensions();
  const [pressed, setPressed] = useState<boolean>(false);

  const effect = useMemo(() => Skia.RuntimeEffect.Make(ORB_SHADER), []);

  const uniforms = useMemo(
    () => ({
      resolution: [width, height],
      center: ORB_CENTER,
      orbScale: ORB_SCALE,
      backdrop: ORB_BACKDROP,
      warmColor: ORB_WARM,
      blueColor: ORB_BLUE,
      warmOffset: ORB_WARM_OFFSET,
      warmRing: ORB_WARM_RING,
      warmWidth: ORB_WARM_WIDTH,
      warmGain: ORB_WARM_GAIN,
      warmAngle: ORB_WARM_ANGLE,
      warmSpread: ORB_WARM_SPREAD,
      blueOffset: ORB_BLUE_OFFSET,
      blueRing: ORB_BLUE_RING,
      blueWidth: ORB_BLUE_WIDTH,
      blueGain: ORB_BLUE_GAIN,
      blueAngle: ORB_BLUE_ANGLE,
      blueSpread: ORB_BLUE_SPREAD,
      hazeRing: ORB_HAZE_RING,
      hazeWidth: ORB_HAZE_WIDTH,
      hazeGain: ORB_HAZE_GAIN,
      cloud: ORB_CLOUD,
      cloudScale: ORB_CLOUD_SCALE,
      domeFade: ORB_DOME_FADE,
      exposure: ORB_EXPOSURE,
      grain: ORB_GRAIN,
      grainScale: ORB_GRAIN_SCALE,
    }),
    [height, width],
  );

  const handlePressIn = useCallback(() => setPressed(true), []);
  const handlePressOut = useCallback(() => setPressed(false), []);

  return (
    <View style={[styles.container, style]}>
      {effect ? (
        <Canvas style={StyleSheet.absoluteFill}>
          <Fill>
            <Shader source={effect} uniforms={uniforms} />
          </Fill>
        </Canvas>
      ) : null}

      <View style={styles.content}>
        <Text style={styles.title}>{title}</Text>
        <Text style={styles.subtitle}>{subtitle}</Text>

        <View style={styles.actionGlow}>
          <Pressable
            accessibilityRole="button"
            accessibilityLabel={actionLabel}
            onPress={onActionPress}
            onPressIn={handlePressIn}
            onPressOut={handlePressOut}
            style={[styles.action, pressed ? styles.actionPressed : null]}
          >
            <View style={styles.actionInner}>
              <Text style={styles.actionLabel} numberOfLines={1}>
                {actionLabel}
              </Text>
            </View>
          </Pressable>
        </View>

        <Text style={styles.footer}>
          {footerPrompt}{" "}
          <Text
            accessibilityRole="link"
            onPress={onFooterPress}
            style={styles.footerAction}
          >
            {footerActionLabel}
          </Text>
        </Text>

        <Text style={styles.legal}>
          {legalPrefix}{" "}
          <Text
            accessibilityRole="link"
            onPress={onTermsPress}
            style={styles.legalLink}
          >
            {termsLabel}
          </Text>{" "}
          {legalSeparator}{" "}
          <Text
            accessibilityRole="link"
            onPress={onPrivacyPress}
            style={styles.legalLink}
          >
            {privacyLabel}
          </Text>
          {legalSuffix}
        </Text>
      </View>
    </View>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: COLORS.screen,
  },
  content: {
    flex: 1,
    alignItems: "center",
    justifyContent: "flex-end",
    paddingHorizontal: CONTENT_HORIZONTAL_PADDING,
    paddingBottom: 40,
  },
  logo: {
    marginBottom: 26,
  },
  title: {
    fontSize: 28,
    lineHeight: 34,
    fontWeight: "700",
    letterSpacing: -0.6,
    textAlign: "center",
    color: COLORS.title,
  },
  subtitle: {
    marginTop: 12,
    maxWidth: 320,
    fontSize: 17,
    lineHeight: 24,
    letterSpacing: -0.2,
    textAlign: "center",
    color: COLORS.subtitle,
  },
  actionGlow: {
    width: "100%",
    marginTop: 32,
    borderRadius: ACTION_RADIUS,
    shadowColor: "#ffffff",
    shadowOpacity: 0.3,
    shadowRadius: 26,
    shadowOffset: { width: 0, height: 0 },
    elevation: 12,
  },
  action: {
    width: "100%",
    height: ACTION_HEIGHT,
    borderRadius: ACTION_RADIUS,
    backgroundColor: COLORS.action,
    overflow: "hidden",
  },
  actionPressed: {
    backgroundColor: COLORS.actionPressed,
  },
  actionInner: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    paddingHorizontal: 18,
  },
  actionLabel: {
    fontSize: 17,
    fontWeight: "600",
    letterSpacing: -0.3,
    color: COLORS.actionLabel,
  },
  footer: {
    marginTop: 24,
    fontSize: 15.5,
    letterSpacing: -0.2,
    textAlign: "center",
    color: COLORS.footer,
  },
  footerAction: {
    fontWeight: "700",
    color: COLORS.footerAction,
  },
  legal: {
    marginTop: 26,
    maxWidth: 300,
    fontSize: 12.5,
    lineHeight: 18,
    textAlign: "center",
    color: COLORS.legal,
  },
  legalLink: {
    color: COLORS.legalLink,
  },
});

export { WelcomeScreenV2 };
export type { IGlyph, IWelcomeOrb, IWelcomeScreenV2 } from "./types";
export default memo(WelcomeScreenV2);
export const DEFAULT_TITLE = "Welcome to Beside";
export const DEFAULT_SUBTITLE =
  "Starting today, your AI phone assistant saves you 2 hours daily.";
export const DEFAULT_ACTION_LABEL = "Get Started";
export const DEFAULT_FOOTER_PROMPT = "Already have an account?";
export const DEFAULT_FOOTER_ACTION_LABEL = "Log in";
export const DEFAULT_LEGAL_PREFIX = "By tapping Get Started, I agree with the";
export const DEFAULT_TERMS_LABEL = "Terms of Service";
export const DEFAULT_LEGAL_SEPARATOR = "and";
export const DEFAULT_PRIVACY_LABEL = "Privacy Policy";
export const DEFAULT_LEGAL_SUFFIX = ".";

export const ORB_CENTER = [0.5, 0.42];
export const ORB_SCALE = [0.62, 0.5];

export const ORB_BACKDROP = [0.042, 0.042, 0.05];
export const ORB_WARM = [0.965, 0.925, 0.855];
export const ORB_BLUE = [0.09, 0.235, 0.98];

export const ORB_WARM_OFFSET = [0.07, 0.02];
export const ORB_WARM_RING = 0.9;
export const ORB_WARM_WIDTH = 0.12;
export const ORB_WARM_GAIN = 0.5;
export const ORB_WARM_ANGLE = 88;
export const ORB_WARM_SPREAD = 0.7;
export const ORB_BLUE_OFFSET = [-0.09, 0.06];
export const ORB_BLUE_RING = 0.72;
export const ORB_BLUE_WIDTH = 0.2;
export const ORB_BLUE_GAIN = 1.7;
export const ORB_BLUE_ANGLE = 0;
export const ORB_BLUE_SPREAD = 1.2;
export const ORB_HAZE_RING = 0.78;
export const ORB_HAZE_WIDTH = 0.5;
export const ORB_HAZE_GAIN = 0.09;
export const ORB_CLOUD = 0.055;
export const ORB_CLOUD_SCALE = 1.7;
export const ORB_DOME_FADE = 0.08;
export const ORB_EXPOSURE = 1.25;
export const ORB_GRAIN = 0.14;
export const ORB_GRAIN_SCALE = 2;
export const ORB_SHADER =  `
uniform float2 resolution;
uniform float2 center;
uniform float2 orbScale;
uniform float3 backdrop;
uniform float3 warmColor;
uniform float3 blueColor;
uniform float2 warmOffset;
uniform float warmRing;
uniform float warmWidth;
uniform float warmGain;
uniform float warmAngle;
uniform float warmSpread;
uniform float2 blueOffset;
uniform float blueRing;
uniform float blueWidth;
uniform float blueGain;
uniform float blueAngle;
uniform float blueSpread;
uniform float hazeRing;
uniform float hazeWidth;
uniform float hazeGain;
uniform float cloud;
uniform float cloudScale;
uniform float domeFade;
uniform float exposure;
uniform float grain;
uniform float grainScale;

float hash1(float2 p) {
  return fract(sin(dot(p, float2(12.9898, 78.233))) * 43758.5453);
}

float vnoise(float2 p) {
  float2 i = floor(p);
  float2 f = fract(p);
  float2 u = f * f * (3.0 - 2.0 * f);
  float a = hash1(i);
  float b = hash1(i + float2(1.0, 0.0));
  float c = hash1(i + float2(0.0, 1.0));
  float d = hash1(i + float2(1.0, 1.0));
  return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}

float fbm(float2 p) {
  float v = 0.0;
  float a = 0.5;
  for (int i = 0; i < 4; i++) {
    v += a * vnoise(p);
    p = p * 2.03;
    a = a * 0.2;
  }
  return v;
}

float band(float r, float ring, float width) {
  float t = (r - ring) / width;
  return exp(-t * t);
}

float lobe(float2 dir, float deg, float spread) {
  float a = radians(deg);
  float2 axis = float2(sin(a), -cos(a));
  return pow(clamp(0.5 + 0.5 * dot(dir, axis), 0.0, 1.0), spread);
}

half4 main(float2 fragCoord) {
  float2 q = (fragCoord - center * resolution) / (orbScale * resolution.x);
  float base = length(q);
  float2 dir = base > 0.0001 ? q / base : float2(0.0, -1.0);

  float nWarm = fbm(q * cloudScale + 4.1) - 0.47;
  float nBlue = fbm(q * cloudScale + 13.7) - 0.47;

  float rWarm = length(q - warmOffset) + nWarm * cloud;
  float rBlue = length(q - blueOffset) + nBlue * cloud;

  float dome = smoothstep(domeFade, -0.6, q.y);

  float warm = band(rWarm, warmRing, warmWidth) * lobe(dir, warmAngle, warmSpread) * warmGain;
  float blue = band(rBlue, blueRing, blueWidth) * lobe(dir, blueAngle, blueSpread) * blueGain;
  float haze = band(base, hazeRing, hazeWidth) * hazeGain;

  float3 glow = blueColor * blue + warmColor * (warm + haze);
  float3 color = backdrop + glow * dome;

  color = 1.0 - exp(-color * exposure);

  float coarse = hash1(floor(fragCoord / grainScale));
  float fine = hash1(fragCoord * 1.37 + 7.1);
  float n = mix(coarse, fine, 0.35) - 0.5;
  float luma = dot(color, float3(0.299, 0.587, 0.114));
  color += n * grain * (0.18 + 1.5 * luma);

  return half4(half3(clamp(color, 0.0, 1.0)), 1.0);
}
`;

export const LOGO_SIZE = 46;
export const CONTENT_HORIZONTAL_PADDING = 24;
export const ACTION_HEIGHT = 58;
export const ACTION_RADIUS = ACTION_HEIGHT / 2;

export const COLORS = {
  screen: "#0e0e11",
  title: "#ffffff",
  subtitle: "#9b9ba1",
  action: "#ffffff",
  actionPressed: "#e6e6ea",
  actionLabel: "#0b0b0d",
  footer: "#8e8e94",
  footerAction: "#ffffff",
  legal: "#5c5c62",
  legalLink: "#8e8e94",
  logo: "#ffffff",
} as const;
import type { StyleProp, ViewStyle } from "react-native";

export interface IGlyph {
  size?: number;
  color?: string;
}

export interface IWelcomeOrb {
  width: number;
  height: number;
}

export interface IWelcomeScreenV2 {
  title?: string;
  subtitle?: string;
  actionLabel?: string;
  footerPrompt?: string;
  footerActionLabel?: string;
  legalPrefix?: string;
  termsLabel?: string;
  legalSeparator?: string;
  privacyLabel?: string;
  legalSuffix?: string;
  logo?: React.ReactNode;
  style?: StyleProp<ViewStyle>;
  onActionPress?: () => void;
  onFooterPress?: () => void;
  onTermsPress?: () => void;
  onPrivacyPress?: () => void;
}

ПРИСОЕДИНЯЙТЕСЬ

ПолучитезначокAstra Proужесегодня

ASTRAPRO
Дизайн и разработка — ©Astra