DEV Community

Cover image for Split Fiction Effect in Browser
Louis Liu
Louis Liu

Posted on • Edited on

Split Fiction Effect in Browser

Inspired by the video game Split Fiction, I'm amazed by the creators' imagination in showcasing two distinct worlds side by side through a split screen. Motivated by their creativity, I am eager to recreate this split effect right within the browser.

TL;DR

Code | Demo

Basic HTML & CSS

The easiest part of this effect is to create a static split screen in the browser.

The skeleton:

<div class="container" id="container">
  <div class="side left-side" id="leftSide"></div>
  <div class="side right-side" id="rightSide"></div>
</div>
Enter fullscreen mode Exit fullscreen mode

Some basic CSS reset:

html,
body {
  margin: 0;
  padding: 0;
  height: 100%;
  overflow: hidden;
  user-select: none;
  cursor: crosshair;
  background: #222;
}
Enter fullscreen mode Exit fullscreen mode

Let the container cover the full screen:

.container {
  position: relative;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
}
Enter fullscreen mode Exit fullscreen mode

Both sides should cover the entire screen, utilizing clip-path to control which area is displayed on each side.

.side {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

.left-side {
  background-color: rgb(150, 72, 222);
  z-index: 1;
  clip-path: polygon(0 0, 50% 0, 50% 100%, 0 100%);
}

.right-side {
  background-color: rgb(55, 234, 136);
  z-index: 1;
  clip-path: polygon(50% 0, 100% 0, 100% 100%, 50% 100%);
}
Enter fullscreen mode Exit fullscreen mode

You got:

basic split screen

Add background image for both sides:

.left-side {
  background-image: url(https://static0.gamerantimages.com/wordpress/wp-content/uploads/2024/12/split-fiction-splitscreen-platforming.jpg?q=49&fit=crop&w=750&h=422&dpr=2);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 1;
  clip-path: polygon(0 0, 50% 0, 50% 100%, 0 100%);
}

.right-side {
  background-image: url(https://static0.gamerantimages.com/wordpress/wp-content/uploads/2024/12/split-fiction-fantasy-transformations.jpg?q=49&fit=crop&w=750&h=422&dpr=2);
  background-size: cover;
  background-position: center;
  background-repeat: no-repeat;
  z-index: 1;
  clip-path: polygon(50% 0, 100% 0, 100% 100%, 50% 100%);
}
Enter fullscreen mode Exit fullscreen mode

You got background image on both sides:

Image description

Rotate the Split Line with JavaScript

Our target is to update the clip-path along with mouse moving. Here's the brain-teaser time!🤯

For each side, we need four points to draw the polygon.

Image description

Image description

First thing we need to do is get intersections of screen edges and the split line.

Image description

Some preparation work, I need the coordinates of the center point, the angle between the center and the mouse, ...

const containerWidth = container.clientWidth;
const containerHeight = container.clientHeight;
const centerX = containerWidth / 2;
const centerY = containerHeight / 2;

let deltaX = mouseX - centerX;
let deltaY = mouseY - centerY;

if (deltaX === 0 && deltaY === 0) {
  deltaX = 0.0001; // Avoid zero vector
}

// Calculate the angle between the center and the mouse position
const angleRadians = Math.atan2(deltaY, deltaX);

// Direction vector of the split line (unit vector)
const lineDirX = Math.cos(angleRadians);
const lineDirY = Math.sin(angleRadians);

// Define the edges of the viewport
const viewportEdges = [
  [{ x: 0, y: 0 }, { x: containerWidth, y: 0 }],   // Top edge
  [{ x: containerWidth, y: 0 }, { x: containerWidth, y: containerHeight }],   // Right edge
  [{ x: containerWidth, y: containerHeight }, { x: 0, y: containerHeight }],   // Bottom edge
  [{ x: 0, y: containerHeight }, { x: 0, y: 0 }]    // Left edge
];

// Find intersections of the split line with the viewport edges
let intersectionPoints = [];
for (const edge of viewportEdges) {
  const intersection = lineSegmentIntersection(centerX, centerY, lineDirX, lineDirY, edge[0], edge[1]);
  if (intersection) intersectionPoints.push(intersection);
}

Enter fullscreen mode Exit fullscreen mode

And, this is the function to calculate whether the split line is intersect with the edge. If true, it will return the coordinates of the intersection point.

/**
 * Calculates the intersection point between an infinite line and a line segment.
 * @param {number} linePointX - X coordinate of a point on the infinite line.
 * @param {number} linePointY - Y coordinate of a point on the infinite line.
 * @param {number} lineDirX - X component of the infinite line's direction vector.
 * @param {number} lineDirY - Y component of the infinite line's direction vector.
 * @param {Object} segmentStart - Starting point of the line segment {x, y}.
 * @param {Object} segmentEnd - Ending point of the line segment {x, y}.
 * @returns {Object|null} - Intersection point {x, y} or null if no intersection exists.
 */
function lineSegmentIntersection(linePointX, linePointY, lineDirX, lineDirY, segmentStart, segmentEnd) {
  // Direction vector of the line segment
  const segmentDirX = segmentEnd.x - segmentStart.x;
  const segmentDirY = segmentEnd.y - segmentStart.y;

  // Calculate the determinant (cross product of direction vectors)
  const determinant = lineDirX * segmentDirY - lineDirY * segmentDirX;

  // If determinant is 0, the line and segment are parallel or collinear
  if (determinant === 0) return null;

  // Calculate parameters t and u
  const t = ((segmentStart.x - linePointX) * segmentDirY - (segmentStart.y - linePointY) * segmentDirX) / determinant;
  const u = ((segmentStart.x - linePointX) * lineDirY - (segmentStart.y - linePointY) * lineDirX) / determinant;

  // If u is not in the range [0, 1], the intersection point is outside the segment
  if (u < 0 || u > 1) return null;

  // Calculate the intersection point
  return {
    x: linePointX + t * lineDirX,
    y: linePointY + t * lineDirY
  };
}
Enter fullscreen mode Exit fullscreen mode

Now, we need to determine whether corners belong to the left side or the right side.

Image description

The code:

// Normal vector: perpendicular to the direction vector
const normalX = lineDirY;
const normalY = -lineDirX;

// Define the corners of the viewport
const viewportCorners = [
  { x: 0, y: 0 },
  { x: containerWidth, y: 0 },
  { x: containerWidth, y: containerHeight },
  { x: 0, y: containerHeight }
];

// Separate corners into left and right groups based on the signed distance
let leftSidePoints = [];
let rightSidePoints = [];

for (const corner of viewportCorners) {
  const distance = signedDistance(corner.x, corner.y, centerX, centerY, normalX, normalY);
  if (distance < 0) {
    leftSidePoints.push(corner);
  } else {
    rightSidePoints.push(corner);
  }
}

// Add intersection points to both sides
leftSidePoints.push(...intersectionPoints);
rightSidePoints.push(...intersectionPoints);
Enter fullscreen mode Exit fullscreen mode

Use signedDistance to determine the corner is on which side of the split line.

/**
 * Calculates the signed distance from a point to a line defined by a normal vector.
 * The sign of the distance indicates which side of the line the point lies on.
 * 
 * @param {number} px - X coordinate of the point.
 * @param {number} py - Y coordinate of the point.
 * @param {number} cx - X coordinate of a point on the line (e.g., the center point).
 * @param {number} cy - Y coordinate of a point on the line.
 * @param {number} nx - X component of the line's normal vector.
 * @param {number} ny - Y component of the line's normal vector.
 * @returns {number} - The signed distance from the point to the line.
 */
function signedDistance(px, py, cx, cy, nx, ny) {
  // Calculate the vector from the line's reference point (cx, cy) to the target point (px, py).
  const vectorX = px - cx;
  const vectorY = py - cy;

  // Compute the dot product of the vector and the normal vector of the line.
  // This projects the vector onto the normal vector, giving the signed distance.
  return vectorX * nx + vectorY * ny;
}
Enter fullscreen mode Exit fullscreen mode

The coordinates we require are stored in the leftSidePoints and rightSidePoints. Reorder those coordinates in clock wise before do the final step.

/**
 * Orders a set of points in clockwise order around their centroid.
 * This is useful for defining polygons where the order of points matters.
 * 
 * @param {Array<Object>} points - An array of points, each with {x, y} properties.
 */
function orderPointsClockwise(points) {
  // Step 1: Calculate the centroid (geometric center) of the points.
  // The centroid is the average of all x and y coordinates.
  const centroid = points.reduce((acc, p) => ({
    x: acc.x + p.x,
    y: acc.y + p.y
  }), { x: 0, y: 0 });
  centroid.x /= points.length;
  centroid.y /= points.length;

  // Step 2: Sort the points based on their angle relative to the centroid.
  // Use Math.atan2 to calculate the angle between each point and the centroid.
  points.sort((a, b) => {
    const angleA = Math.atan2(a.y - centroid.y, a.x - centroid.x); // Angle of point A
    const angleB = Math.atan2(b.y - centroid.y, b.x - centroid.x); // Angle of point B
    return angleA - angleB; // Sort in ascending order of angle
  });
}

// Order points clockwise for clip-path polygons
orderPointsClockwise(leftSidePoints);
orderPointsClockwise(rightSidePoints);
Enter fullscreen mode Exit fullscreen mode

Let's convert the coordinates to the clip-path properties.

// Convert points to clip-path format
const pointsToClipPath = (points) => points.map(point => `${point.x}px ${point.y}px`).join(', ');

// Update the clip-path for both sides
leftSide.style.clipPath = `polygon(${pointsToClipPath(leftSidePoints)})`;
rightSide.style.clipPath = `polygon(${pointsToClipPath(rightSidePoints)})`;
Enter fullscreen mode Exit fullscreen mode

Listen to the Mouse Move

The split line needs to move along with the mouse.

// Initialize with vertical split on page load
updateSplit(window.innerWidth / 2, window.innerHeight / 2);

// Update on mouse enter and move
container.addEventListener('mouseenter', e => {
  updateSplit(e.clientX, e.clientY);
});

container.addEventListener('mousemove', e => {
  updateSplit(e.clientX, e.clientY);
});

// Reset on mouse leave
container.addEventListener('mouseleave', () => {
  leftSide.style.clipPath = 'polygon(0 0, 50% 0, 50% 100%, 0 100%)';
  rightSide.style.clipPath = 'polygon(50% 0, 100% 0, 100% 100%, 50% 100%)';
});
Enter fullscreen mode Exit fullscreen mode

Demo


Or access the code on GitHub.

Top comments (6)

Collapse
 
nevodavid profile image
Nevo David

i think this is super slick - i always wanted to mess around with split screens like that but never got around to it

Collapse
 
louis7 profile image
Louis Liu

Now we have LLM tools, we can quickly get some ideas on how to achieve these effects 😎. It can't give you the code directly though 😑.

Collapse
 
targetx25 profile image
Abhay Pratap Singh

This is really cool... THANKS FOR SHARING

Collapse
 
louis7 profile image
Louis Liu • Edited

I'm glad you like it!!

Collapse
 
himanshu_code profile image
Himanshu Sorathiya

Picture perfect thing

Collapse
 
louis7 profile image
Louis Liu

thanks!