component

Rift

A pair of linked rifts torn in the air, ragged and edged in iridescent, glitter-shedding light. Each shows the other end live and in parallax, and walking into either carries you to the other: walkable shortcuts within one world (use Portal for other worlds or pages).

⌖ Explore the library in 3D → · View as Markdown

Tune every prop in the workshop →

Unreleased: this lands in the next release. Until then, runek add can't install it.

Add it

npx @runek/cli add rift

Pulls sign, @react-three/fiber@^9.6.1, @react-three/rapier@^2.2.0, @runek/core@^0.15.0, three@^0.184.0.

Use it

import { Rift } from './runek/Rift'

<Rift position={[0, 0, 0]} seed={1} />

Or as a node in a world file (worlds as data):

{ "type": "Rift", "props": { "position": [0, 0, 0], "seed": 1 } }

Props

Prop Type Default Description
destination [x, y, z] Where the other end stands (the foot of its rift), in world coordinates. Each end shows the other live through its opening, and walking into either carries you to the other. Default: 8 units behind position.
destinationRotation [x, y, z] Euler rotation of the far end, in radians: which way its caption faces. Default rotation.
label string Floating caption above the rift at position (e.g. the name of where it leads).
returnLabel string Floating caption above the rift at destination.
radius number 1.5 Radius of each opening, in units. The ragged edge breathes around it.
color color Tint for the rims and glitter. Unset, the rims are an iridescent pink, violet and cyan film.
active boolean true Open both rifts and let them carry travelers. When false they shrink to small shimmering bubbles and stay inert; flipping it to true tears them open. Default true.
onTravel function (code only) Called after a traveler is carried through, with the foot of the end they came out of.
position [x, y, z] [0, 0, 0] Position [x, y, z] in units (1 unit = 1 m, Y-up).
rotation [x, y, z] [0, 0, 0] Euler rotation [x, y, z] in radians.
seed number 1 Seed for the deterministic variation: same seed, same result.

Registry manifest

registry manifest /r/components/rift.json Self-contained JSON: inlined source plus resolved dependencies, exactly what runek add rift fetches.

Browse the whole catalog in the gallery →.

Source (Rift.tsx)
import { useFrame, useThree } from '@react-three/fiber'
import { BallCollider, type RapierRigidBody, RigidBody } from '@react-three/rapier'
import { rng, sub, useWorld, type Vec3, type WorldComponentProps } from '@runek/core'
import { type RefObject, useEffect, useMemo, useRef } from 'react'
import {
  AdditiveBlending,
  BufferAttribute,
  BufferGeometry,
  Color,
  type Group,
  HalfFloatType,
  type Mesh,
  type Object3D,
  PerspectiveCamera,
  Plane,
  ShaderMaterial,
  Vector3,
  Vector4,
  WebGLRenderTarget,
} from 'three'
import { Sign } from './Sign'

export interface RiftProps extends WorldComponentProps {
  /**
   * Where the other end stands (the foot of its rift), in world coordinates. Each end shows
   * the other live through its opening, and walking into either carries you to the other.
   * Default: 8 units behind `position`.
   */
  destination?: Vec3
  /** Euler rotation of the far end, in radians: which way its caption faces. Default `rotation`. */
  destinationRotation?: Vec3
  /** Floating caption above the rift at `position` (e.g. the name of where it leads). */
  label?: string
  /** Floating caption above the rift at `destination`. */
  returnLabel?: string
  /** Radius of each opening, in units. The ragged edge breathes around it. */
  radius?: number
  /** Tint for the rims and glitter. Unset, the rims are an iridescent pink, violet and cyan film. */
  color?: string
  /**
   * Open both rifts and let them carry travelers. When false they shrink to small shimmering
   * bubbles and stay inert; flipping it to true tears them open. Default true.
   */
  active?: boolean
  /** Called after a traveler is carried through, with the foot of the end they came out of. */
  onTravel?: (arrival: Vec3) => void
}

// The quad reaches past the edge so the halo and the shed glitter have room; in multiples of `radius`.
const EXTENT = 1.7
// How far open the rifts sit while inactive: small bubbles, in multiples of `radius`.
const CLOSED = 0.18
const MOTES = 700

// The edge of a rift, shared by the window and the glitter so sparks are born right on it.
const EDGE = /* glsl */ `
uniform float uTime;
uniform float uSeed;
uniform float uOpen;
uniform float uTurb;

float hash(vec2 p) { return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453); }
float noise(vec2 p) {
  vec2 i = floor(p), f = fract(p);
  vec2 u = f * f * (3.0 - 2.0 * f);
  return mix(mix(hash(i), hash(i + vec2(1.0, 0.0)), u.x),
             mix(hash(i + vec2(0.0, 1.0)), hash(i + vec2(1.0, 1.0)), u.x), u.y);
}
float fbm(vec2 p) {
  float v = 0.0, a = 0.5;
  for (int i = 0; i < 3; i++) { v += a * noise(p); p = p * 2.07 + 13.3; a *= 0.5; }
  return v;
}
// Sampling noise on the unit circle keeps the outline seamless all the way round.
float edge(vec2 dir) {
  float ragged = smoothstep(${CLOSED.toFixed(2)}, 1.0, uOpen) * uTurb;
  float slow = fbm(dir * 1.4 + uSeed + uTime * 0.11) - 0.5;
  float fine = noise(dir * 6.0 - uSeed + uTime * 0.7) - 0.5;
  return uOpen * (0.82 + slow * 0.55 * ragged + fine * 0.1 * ragged);
}
// Thin-film iridescence, held to the pink / violet / cyan end of the spectrum.
vec3 film(float x) {
  x = fract(x);
  vec3 c = mix(vec3(1.0, 0.08, 0.55), vec3(0.35, 0.12, 1.0), smoothstep(0.0, 0.33, x));
  c = mix(c, vec3(0.1, 0.75, 1.0), smoothstep(0.33, 0.66, x));
  return mix(c, vec3(1.0, 0.08, 0.55), smoothstep(0.66, 1.0, x));
}`

// Camera-facing around the rift's center, so it reads as a hole in space from any side.
const WINDOW_VERTEX = /* glsl */ `
uniform float uR;
varying vec2 vP;
void main() {
  vP = position.xy / uR;
  vec4 mv = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
  mv.xy += position.xy * length(modelMatrix[0].xyz);
  gl_Position = projectionMatrix * mv;
}`

const WINDOW_FRAGMENT = /* glsl */ `
${EDGE}
uniform sampler2D uView;
uniform vec4 uViewport;
uniform vec3 uTint;
uniform float uTintMix;
varying vec2 vP;

void main() {
  float r = length(vP);
  vec2 dir = vP / max(r, 1e-4);
  float d = r - edge(dir);
  float s = mix(0.45, 1.0, uOpen);
  float inside = 1.0 - smoothstep(-0.006 * s, 0.006 * s, d);

  // The far end is sampled in screen space, so it moves with the viewer like a real opening;
  // near the rim it bends outward a touch, like light through a soap film.
  float lens = exp(min(d, 0.0) / (0.05 * s));
  vec2 uv = (gl_FragCoord.xy - uViewport.xy) / uViewport.zw - dir * lens * 0.012;
  vec3 far = texture2D(uView, uv).rgb;
#ifdef TONE_MAPPING
  far = toneMapping(far);
#endif

  float a = atan(vP.y, vP.x);
  vec3 tint = mix(film(a * 0.32 + d * 1.2 + uTime * 0.08), uTint, uTintMix);
  far = mix(far, tint * 0.5, lens * 0.25);

  // The rim flickers along its length like plasma rather than glowing as one even line.
  float plasma = 0.55 + 0.9 * noise(dir * 14.0 + vec2(uTime * 1.7, -uTime * 1.3) + uSeed);
  float core = exp(-pow(d / (0.02 * s), 2.0)) * plasma;
  float glow = exp(-abs(d) / (0.04 * s)) * plasma;
  float halo = step(0.0, d) * exp(-pow(d / (0.2 * s), 2.0)) * 0.16;

  // Glints that twinkle in a band hugging the rim, mostly on the outside.
  vec2 g = vP * 40.0;
  vec2 id = floor(g);
  float h = hash(id + uSeed);
  vec2 off = (vec2(hash(id + 3.1), hash(id + 7.7)) - 0.5) * 0.6;
  float twinkle = pow(0.5 + 0.5 * sin(uTime * (2.0 + h * 5.0) + h * 40.0), 6.0);
  float band = exp(-pow((d - 0.05 * s) / (0.1 * s), 2.0));
  float glint = step(0.6, h) * twinkle * smoothstep(0.14, 0.0, length(fract(g) - 0.5 - off)) * band;

  // Fade everything out before the edge of the quad so it never shows as a square.
  float fade = 1.0 - smoothstep(${(EXTENT * 0.75).toFixed(2)}, ${EXTENT.toFixed(2)}, r);
  vec3 light = (tint * (glow * 1.1 + halo) + vec3(1.0) * core * 1.8
             + mix(vec3(1.0), tint, 0.4) * glint * 2.5) * fade;

  // Premultiplied: the window is opaque, the rim adds over whatever is behind it.
  float alpha = max(inside, clamp((core * 0.6 + glow * 0.6 + halo * 1.2 + glint * 0.5) * fade, 0.0, 1.0));
  gl_FragColor = vec4(far * inside + light, alpha);
  #include <colorspace_fragment>
}`

const MOTE_VERTEX = /* glsl */ `
${EDGE}
uniform float uR;
uniform float uSize;
uniform float uHalfHeight;
uniform vec3 uTint;
uniform float uTintMix;
attribute vec3 aMote;
attribute vec3 aKind;
varying vec3 vColor;
void main() {
  float life = fract(aMote.y + uTime * aMote.z);
  vec2 dir = vec2(cos(aMote.x), sin(aMote.x));
  // Born on the rim, then shed outward (or, for a few, inward) and settling like glitter.
  float r = edge(dir) + aKind.z * life * mix(0.4, 1.0, uOpen);
  float a = aMote.x + life * 0.35;
  vec2 p = vec2(cos(a), sin(a)) * r - vec2(0.0, 0.3 * life * life * uOpen);
  vec4 mv = modelViewMatrix * vec4(0.0, 0.0, 0.0, 1.0);
  float scale = length(modelMatrix[0].xyz);
  mv.xy += p * uR * scale;
  gl_Position = projectionMatrix * mv;

  float fade = smoothstep(0.0, 0.08, life) * (1.0 - smoothstep(0.55, 1.0, life));
  float twinkle = 0.35 + 0.65 * pow(0.5 + 0.5 * sin(uTime * (5.0 + aKind.y * 9.0) + aMote.y * 60.0), 3.0);
  vColor = mix(vec3(1.0), mix(film(aKind.y), uTint, uTintMix), 0.55) * fade * twinkle;
  gl_PointSize = uSize * scale * (0.4 + aKind.x) * projectionMatrix[1][1] * uHalfHeight / -mv.z;
}`

const MOTE_FRAGMENT = /* glsl */ `
varying vec3 vColor;
void main() {
  float d = length(gl_PointCoord - 0.5);
  float spark = pow(smoothstep(0.5, 0.0, d), 2.0);
  gl_FragColor = vec4(vColor * spark * 1.6, 1.0);
}`

const POOL_VERTEX = /* glsl */ `
varying vec2 vUv;
void main() {
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`

const POOL_FRAGMENT = /* glsl */ `
uniform vec3 uColor;
uniform float uOpen;
varying vec2 vUv;
void main() {
  float r = length(vUv - 0.5) * 2.0;
  float a = pow(max(1.0 - r, 0.0), 2.2) * 0.4 * uOpen;
  gl_FragColor = vec4(uColor * a, 1.0);
}`

type Uniform<T> = { value: T }
interface Shared {
  uTime: Uniform<number>
  uOpen: Uniform<number>
  uTurb: Uniform<number>
}

// Only one rift draws its far view at a time: a rift seen inside another's view keeps the
// frame it last drew rather than recursing.
let drawingView = false

// ecctrl's camera rig: a pivot at the scene root holding a single follow point.
const isCameraRig = (o: Object3D) =>
  o.type === 'Object3D' && o.children.length === 1 && o.children[0].type === 'Object3D'

/**
 * A pair of linked rifts torn in the air: ragged, breathing openings edged in iridescent light
 * and shedding glitter. Each shows the other end live and in parallax, exactly as you'd see it
 * standing there, and walking into either carries you through to the other, momentum and all.
 * Both ends are places in the same world, so use it for shortcuts you can walk (across a map,
 * up a tower, between rooms); for travel to another world or page, use `Portal`. The look is
 * procedural and asset-free (the seed shapes the edges and the glitter). Each open rift on
 * screen costs a second render of the scene, clipped to the rift's footprint.
 */
export function Rift({
  position = [0, 0, 0],
  rotation = [0, 0, 0],
  seed = 1,
  destination,
  destinationRotation,
  label,
  returnLabel,
  radius = 1.5,
  color,
  active = true,
  onTravel,
}: RiftProps) {
  const { unit, player } = useWorld()
  const scene = useThree((s) => s.scene)
  const get = useThree((s) => s.get)
  const R = radius * unit
  const there: Vec3 = destination ?? [position[0], position[1], position[2] - 8 * unit]

  const shared = useMemo<Shared>(
    () => ({ uTime: { value: 0 }, uOpen: { value: CLOSED }, uTurb: { value: 1 } }),
    [],
  )
  const near = useRef<Group>(null)
  const far = useRef<Group>(null)
  const nearCore = useRef<Group>(null)
  const farCore = useRef<Group>(null)
  const ends = useMemo(() => [near, far], [])
  const arrivals = useMemo(() => [new Set<number>(), new Set<number>()], [])

  useFrame(({ clock }, dt) => {
    const goal = active ? 1 : CLOSED
    const open = shared.uOpen.value
    shared.uOpen.value = open + (goal - open) * (1 - Math.exp(-dt * 1.8))
    shared.uTurb.value = 1 + 2.5 * Math.abs(goal - open)
    shared.uTime.value = clock.elapsedTime
  })

  // Carry a body from one end to the matching spot at the other, keeping its velocity. When it
  // is the avatar, its camera rig hops with it so the view doesn't sweep across the world.
  const travel = (
    body: RapierRigidBody,
    from: RefObject<Group | null>,
    to: RefObject<Group | null>,
    arrival: Vec3,
    landing: Set<number>,
  ) => {
    if (!from.current || !to.current) return
    const offset = to.current
      .getWorldPosition(new Vector3())
      .sub(from.current.getWorldPosition(new Vector3()))
    const t = body.translation()
    const was = new Vector3(t.x, t.y, t.z)
    landing.add(body.handle)
    body.setTranslation(was.clone().add(offset), true)

    const eye = player?.current?.getWorldPosition(new Vector3())
    if (eye && eye.distanceToSquared(was) < 4 * unit * unit) {
      get().camera.position.add(offset)
      for (const o of scene.children) {
        if (isCameraRig(o) && o.position.distanceToSquared(was) < 9 * unit * unit) {
          o.position.add(offset)
        }
      }
    }
    onTravel?.(arrival)
  }

  const common = { R, unit, shared, color, active, ends }
  return (
    <>
      <RiftEnd
        {...common}
        end={near}
        core={nearCore}
        otherCore={farCore}
        at={position}
        rotation={rotation}
        seed={sub(seed, 0)}
        label={label}
        arrivals={arrivals[0]}
        onEnter={(body) => travel(body, nearCore, farCore, there, arrivals[1])}
      />
      <RiftEnd
        {...common}
        end={far}
        core={farCore}
        otherCore={nearCore}
        at={there}
        rotation={destinationRotation ?? rotation}
        seed={sub(seed, 1)}
        label={returnLabel}
        arrivals={arrivals[1]}
        onEnter={(body) => travel(body, farCore, nearCore, position, arrivals[0])}
      />
    </>
  )
}

interface RiftEndProps {
  end: RefObject<Group | null>
  /** The center of this rift and of the other one; the view and travel follow their world positions. */
  core: RefObject<Group | null>
  otherCore: RefObject<Group | null>
  ends: RefObject<Group | null>[]
  at: Vec3
  rotation: Vec3
  R: number
  unit: number
  seed: number
  label?: string
  color?: string
  active: boolean
  shared: Shared
  arrivals: Set<number>
  onEnter: (body: RapierRigidBody) => void
}

function RiftEnd({
  end,
  core,
  otherCore,
  ends,
  at,
  rotation,
  R,
  unit,
  seed,
  label,
  color,
  active,
  shared,
  arrivals,
  onEnter,
}: RiftEndProps) {
  const pane = useRef<Mesh>(null)

  const view = useMemo(() => {
    const camera = new PerspectiveCamera()
    camera.matrixAutoUpdate = false
    camera.matrixWorldAutoUpdate = false
    return {
      camera,
      target: new WebGLRenderTarget(1, 1, { type: HalfFloatType, samples: 4 }),
      viewport: new Vector4(),
      clip: new Plane(),
      forward: new Vector3(),
      point: new Vector3(),
      here: new Vector3(),
      there: new Vector3(),
    }
  }, [])
  useEffect(() => () => view.target.dispose(), [view])

  const tint = useMemo(() => new Color(color ?? '#ffffff'), [color])
  const tintMix = color ? 0.65 : 0
  const own = useMemo(() => ({ uSeed: { value: rng(seed)() * 100 } }), [seed])

  const material = useMemo(
    () =>
      new ShaderMaterial({
        vertexShader: WINDOW_VERTEX,
        fragmentShader: WINDOW_FRAGMENT,
        uniforms: {
          ...shared,
          ...own,
          uR: { value: R },
          uView: { value: view.target.texture },
          uViewport: { value: view.viewport },
          uTint: { value: tint },
          uTintMix: { value: tintMix },
        },
        transparent: true,
        premultipliedAlpha: true,
        depthWrite: false,
      }),
    [shared, own, R, view, tint, tintMix],
  )

  const motes = useMemo(() => {
    const r = rng(sub(seed, 1))
    const mote = new Float32Array(MOTES * 3)
    const kind = new Float32Array(MOTES * 3)
    for (let i = 0; i < MOTES; i++) {
      mote[i * 3] = r() * Math.PI * 2
      mote[i * 3 + 1] = r()
      mote[i * 3 + 2] = 0.12 + r() * 0.3
      kind[i * 3] = r() * r()
      kind[i * 3 + 1] = r()
      kind[i * 3 + 2] = r() < 0.2 ? -0.08 - r() * 0.12 : 0.1 + r() * 0.55
    }
    const geometry = new BufferGeometry()
    geometry.setAttribute('position', new BufferAttribute(new Float32Array(MOTES * 3), 3))
    geometry.setAttribute('aMote', new BufferAttribute(mote, 3))
    geometry.setAttribute('aKind', new BufferAttribute(kind, 3))
    const material = new ShaderMaterial({
      vertexShader: MOTE_VERTEX,
      fragmentShader: MOTE_FRAGMENT,
      uniforms: {
        ...shared,
        ...own,
        uR: { value: R },
        uSize: { value: 0.05 * R },
        uHalfHeight: { value: 400 },
        uTint: { value: tint },
        uTintMix: { value: tintMix },
      },
      transparent: true,
      blending: AdditiveBlending,
      depthWrite: false,
      toneMapped: false,
    })
    return { geometry, material }
  }, [shared, own, R, seed, tint, tintMix])

  const pool = useMemo(
    () =>
      new ShaderMaterial({
        vertexShader: POOL_VERTEX,
        fragmentShader: POOL_FRAGMENT,
        uniforms: { uColor: { value: new Color(color ?? '#c86bff') }, uOpen: shared.uOpen },
        transparent: true,
        blending: AdditiveBlending,
        depthWrite: false,
        toneMapped: false,
      }),
    [color, shared],
  )

  useFrame(({ size, viewport }) => {
    motes.material.uniforms.uHalfHeight.value = (size.height * viewport.dpr) / 2
  })

  // Draw the other end just before this window draws, with whatever camera is drawing it, so
  // the view never lags the viewer by a frame and is skipped whenever the rift is culled.
  useEffect(() => {
    const mesh = pane.current
    if (!mesh) return
    mesh.onBeforeRender = (renderer, scene, camera) => {
      if (drawingView || !core.current || !otherCore.current) return
      drawingView = true
      core.current.getWorldPosition(view.here)
      otherCore.current.getWorldPosition(view.there)
      const scale = core.current.getWorldScale(view.point).x
      const target = renderer.getRenderTarget()
      const xr = renderer.xr.enabled
      const shadows = renderer.shadowMap.autoUpdate
      const planes = renderer.clippingPlanes
      renderer.getCurrentViewport(view.viewport)
      const w = view.viewport.z
      const h = view.viewport.w
      view.target.setSize(w, h)

      // The viewer's camera, carried to the other end.
      const eye = view.camera
      eye.projectionMatrix.copy(camera.projectionMatrix)
      eye.projectionMatrixInverse.copy(camera.projectionMatrixInverse)
      eye.matrixWorld.copy(camera.matrixWorld)
      const m = eye.matrixWorld.elements
      m[12] += view.there.x - view.here.x
      m[13] += view.there.y - view.here.y
      m[14] += view.there.z - view.here.z
      eye.matrixWorldInverse.copy(eye.matrixWorld).invert()

      // Cut away whatever stands between the carried camera and the far rift (a wall the viewer
      // isn't actually behind), keeping a rift's radius of what's just in front of it.
      view.forward.set(-m[8], -m[9], -m[10]).normalize()
      view.point.copy(view.there).addScaledVector(view.forward, -R * scale)
      view.clip.setFromNormalAndCoplanarPoint(view.forward, view.point)

      // Only shade the pixels this rift covers on screen.
      const c = view.point.copy(view.here).applyMatrix4(camera.matrixWorldInverse)
      const extent = R * EXTENT * scale
      if (-c.z > extent) {
        const p = camera.projectionMatrix.elements
        const rx = ((extent * p[0]) / -c.z) * (w / 2)
        const ry = ((extent * p[5]) / -c.z) * (h / 2)
        c.applyMatrix4(camera.projectionMatrix)
        const x = Math.max(0, Math.floor((c.x * 0.5 + 0.5) * w - rx))
        const y = Math.max(0, Math.floor((c.y * 0.5 + 0.5) * h - ry))
        view.target.scissor.set(x, y, Math.ceil(2 * rx), Math.ceil(2 * ry))
        view.target.scissorTest = true
      } else {
        view.target.scissorTest = false
      }

      for (const e of ends) if (e.current) e.current.visible = false
      renderer.xr.enabled = false
      renderer.shadowMap.autoUpdate = false
      renderer.clippingPlanes = [view.clip]
      renderer.setRenderTarget(view.target)
      renderer.state.buffers.depth.setMask(true)
      if (!renderer.autoClear) renderer.clear()
      renderer.render(scene, eye)
      renderer.clippingPlanes = planes
      renderer.shadowMap.autoUpdate = shadows
      renderer.xr.enabled = xr
      renderer.setRenderTarget(target)
      renderer.state.viewport(view.viewport)
      for (const e of ends) if (e.current) e.current.visible = true
      drawingView = false
    }
    return () => {
      mesh.onBeforeRender = () => {}
    }
  }, [view, ends, core, otherCore, R])

  const extent = R * EXTENT

  return (
    <RigidBody type="fixed" colliders={false} position={at} rotation={rotation}>
      {/* A sensor filling the opening, the same from every side. Only dynamic bodies (the
          avatar, loose props) travel; one that just arrived isn't sent back until it steps out. */}
      <BallCollider
        sensor
        args={[Math.max(R * 0.8, 0.6 * unit)]}
        position={[0, R, 0]}
        onIntersectionEnter={({ other: o }) => {
          const body = o.rigidBody
          if (!active || !body?.isDynamic() || arrivals.has(body.handle)) return
          onEnter(body)
        }}
        onIntersectionExit={({ other: o }) => {
          if (o.rigidBody) arrivals.delete(o.rigidBody.handle)
        }}
      />

      <group ref={end}>
        <group ref={core} position={[0, R, 0]}>
          <mesh ref={pane} material={material} renderOrder={1}>
            <planeGeometry args={[extent * 2, extent * 2]} />
          </mesh>
          <points
            geometry={motes.geometry}
            material={motes.material}
            frustumCulled={false}
            renderOrder={2}
          />
        </group>

        <mesh position={[0, 0.02 * unit, 0]} rotation={[-Math.PI / 2, 0, 0]} material={pool}>
          <planeGeometry args={[R * 2.6, R * 2.6]} />
        </mesh>

        {label && (
          <Sign position={[0, R * 2 + 0.7 * unit, 0]} size={0.6} color={color ?? '#e9b8ff'} glow>
            {label}
          </Sign>
        )}
      </group>
    </RigidBody>
  )
}