Skip to content

Latest commit

 

History

History
144 lines (113 loc) · 5.49 KB

File metadata and controls

144 lines (113 loc) · 5.49 KB

Tear Effect: step-by-step explanation

1. What TearEffect.metal does

The tearDistortion shader receives, per pixel, the current position plus parameters coming from SwiftUI:

  • tearStart: where the tear starts (on the real edge of the view).
  • tearEnd: current finger point (projected onto the fixed tear line).
  • tearProgress: overall gesture progress (0...1).
  • maxGap: maximum opening between the two halves.
  • edgeRoughness: irregularity of the torn edge.
  • viewSize: view size for safe clipping.

Internal shader flow:

  1. If tearProgress is near 0, it leaves the pixel unchanged (layer.sample(position)).
  2. It builds the tear line using tearDir = tearEnd - tearStart.
  3. It derives two axes:
  • tearNorm: axis along the cut.
  • tearPerp: axis perpendicular to the cut.
  1. For each pixel, it computes:
  • alongTear: how far the pixel is along the tear line.
  • acrossTear: perpendicular distance to the tear line.
  1. It defines a wedge-shaped opening:
  • widest at tearStart.
  • zero at tearEnd.
  1. It applies sinusoidal noise to simulate torn fibers (edgeRoughness).
  2. If the pixel falls inside the gap (absDist < halfGap), it becomes transparent.
  3. If it is outside the gap, sampling is shifted to separate both halves visually.
  4. If sampling goes out of viewSize, it returns transparent to avoid artifacts.

Result: a clean tear with separated halves and rough edges, without curl/warp deformation.

2. How it is connected in the package structure

The integration is split across:

  • TearState.swift: state + geometric computations.
  • TearModifier.swift: gesture orchestration + shader uniforms.
  • Extensions/View+TearKitExtensions.swift: public tearable(...) API.

State (TearState)

Key variables:

  • edgePoint: real tear anchor point on the edge.
  • fingerPoint: current tear end point (projected finger).
  • fixedDirection: unit direction locked at gesture start.
  • directionLocked: whether direction is already locked.
  • gapWidth: opening sent as maxGap.
  • tearProgress: progress sent to the shader.
  • lockProjection: lock-time baseline projection to avoid jumps.
  • maxTrajectoryDistance: maximum tear-line distance available inside the view.
  • hasFullyTorn: prevents re-triggering once fully torn.

Rendering

In body(content:):

  1. geometryReader stores viewSize.
  2. layerEffect creates a Shader(function:arguments:) for tearDistortion and passes state values.
  3. isEnabled activates the shader only when:
  • gesture is active,
  • direction is locked,
  • and there is a real opening (gapWidth > 0.1).
  1. The modifier does not apply drop/opacity effects; those reactions are delegated to the host view via callbacks.

Gesture (DragGesture)

  1. On first onChanged, if startLocation is near an edge:
  • it activates state,
  • initializes edgePoint and fingerPoint with that start location.
  1. While direction is not locked (directionLocked == false):
  • it waits until drag exceeds ~20pt,
  • then calls lockDirection(from:to:cutDirection:).
  1. lockDirection:
  • computes unit direction,
  • raycasts backward to get the real edge intersection (edgePoint),
  • stores lockProjection as a baseline.
  1. During this initial phase:
  • tearProgress = 0,
  • gapWidth = 0,
  • so no visible effect appears.
  1. Once direction is locked:
  • finger is projected with projectedEnd(fingerLocation:),
  • projection is clamped to the valid in-view segment,
  • distance, progress, and gap are updated.
  1. On onEnded:
  • if tearProgress >= breakThreshold (default 0.9): marks full break and notifies via callback.
  • otherwise: animated recovery.

3. Business rule: 90% breaks / below 90% recovers

Decision is made when the gesture ends:

  1. Compare tearProgress against breakThreshold.
    • tearProgress is computed as:
    • traveled distance along the fixed line
    • divided by the maximum distance of that same line inside the view.
  2. If it exceeds threshold:
  • force tearProgress = 1 and max gap,
  • trigger onCompletion(true).
    • The final action/animation (fall, fade, navigation, etc.) is defined by the view using the modifier.
  1. If it does not exceed threshold:
  • first animate closing the cut frame-by-frame (explicit interpolation of finger/progress/gap to 0),
  • then perform internal reset(),
  • trigger onCompletion(false).

4. Public API for integration

The tearable(...) extension now exposes:

  • edgeRoughness: torn-edge irregularity strength.
  • maxGapWidth: maximum opening near tear start.
  • breakThreshold: full-break threshold (0...1).
  • cutDirection: tear direction mode (.vertical, .horizontal, .anyDirection).
  • isBroken: optional Binding<Bool> to observe/control the broken state.
  • onProgressChanged: continuous progress callback (0...1).
  • onCompletion: final result callback.

Quick cutDirection examples:

  • .vertical: constrained to vertical cuts only.
  • .horizontal: constrained to horizontal cuts only.
  • .anyDirection: follows the initial drag direction.

onCompletion semantics:

  • true: view fully broke and falls.
  • false: threshold not reached and view recovers.

isBroken semantics:

  • set false to trigger recovery/reset.
  • set true to force broken state externally.

5. Quick mental model (Gesture -> State -> Shader -> Result)

  1. User drags from an edge.
  2. SwiftUI updates TearState.
  3. TearModifier sends uniforms to the shader.
  4. tearDistortion computes gap + per-pixel separation.
  5. On release, threshold is evaluated (>=90% or not).
  6. Final result is reported via onCompletion(Bool).