Codrops 원본

Building a Mouse-Following Square Lens Effect with Three.js and GLSL

포인터 · MIT

포인터 더 보기
ORIGINAL PREVIEW
Building a Mouse-Following Square Lens Effect with Three.js and GLSL 정적 미리보기

갤러리 안에서는 화면을 벗어나거나 움직임을 멈추면 예제도 닫힙니다. 다시 재생할 때는 처음부터 시작해요.

큰 화면으로 보기 새 탭

SOURCE FILES

원본 코드 읽기

28개 파일

수집한 원본 소스와 실행 안내를 함께 제공합니다.

astro.config.mjs
파일 저장

// @ts-check
import { defineConfig } from "astro/config";
import path from "path";
import glsl from "vite-plugin-glsl";

// https://astro.build/config
export default defineConfig({
  // base: "/webgl/",
  build: {
    assets: "assets/js",
  },
  server: {
    host: true,
  },
  site: "https://www.yukiloz7.com/",
  vite: {
    build: {
      rollupOptions: {
        output: {
          entryFileNames: "assets/js/[name].[hash].js",
          chunkFileNames: "assets/js/[name].[hash].js",
          assetFileNames: (assetInfo) => {
            const name = assetInfo.name ?? "";

            if (/\.css$/i.test(name)) {
              return "assets/css/[name].[hash][extname]";
            }

            if (/\.(avif|webp|png|jpe?g|gif|svg)$/i.test(name)) {
              return "assets/img/[name].[hash][extname]";
            }

            return "assets/[name].[hash][extname]";
          },
        },
      },
    },
    plugins: [
      glsl({
        include: ["**/*.glsl", "**/*.vert", "**/*.frag"],
        warnDuplicatedImports: true,
        removeDuplicatedImports: true,
        minify: false,
        watch: true,
      }),
    ],
    resolve: {
      alias: {
        "@": path.join(process.cwd(), "src"),
        "@scripts": path.join(process.cwd(), "src/scripts"),
        "@styles": path.join(process.cwd(), "src/styles"),
        "@layouts": path.join(process.cwd(), "src/layouts"),
      },
    },
  },
});
eslint.config.mjs
파일 저장

import js from "@eslint/js";
import astro from "eslint-plugin-astro";
import tseslint from "typescript-eslint";

export default [
  {
    ignores: ["dist/**", "node_modules/**", ".astro/**"],
  },
  js.configs.recommended,
  ...tseslint.configs.recommended,
  ...astro.configs["flat/recommended"],
];
함께 쓰는 파일 26개 보기
src/env.d.ts
파일 저장

/// <reference types="astro/client" />

declare module "*.vert" {
  const shader: string
  export default shader
}

declare module "*.frag" {
  const shader: string
  export default shader
}

declare module "*.glsl" {
  const shader: string
  export default shader
}
src/layouts/BaseLayout.astro

큰 소스 파일과 이미지·모델 등의 자료는 파일을 저장해 확인할 수 있어요.

src/pages/index.astro

큰 소스 파일과 이미지·모델 등의 자료는 파일을 저장해 확인할 수 있어요.

src/scripts/index.ts
파일 저장

import Webgl from "./webgl/Webgl"

const revealPage = (): void => {
  document.body.classList.remove("loading")
}

const boot = async (): Promise<void> => {
  const root = document.querySelector("[data-page]")

  if (!root) {
    revealPage()
    return
  }

  const webgl = new Webgl()

  try {
    webgl.init()
    const isReady = await webgl.enter()

    if (isReady) {
      await new Promise<void>((resolve) => {
        window.requestAnimationFrame(() => resolve())
      })
      document.body.classList.add("webgl-ready")
    }
  } catch (error) {
    console.error("Failed to initialize the WebGL demo.", error)
  } finally {
    revealPage()
  }
}

if (document.readyState === "loading") {
  document.addEventListener("DOMContentLoaded", boot, { once: true })
} else {
  boot()
}
src/scripts/webgl/Webgl.ts
파일 저장

import GUI from "lil-gui"
import Stage from "./stage/Stage"
import Mesh, { type ShaderParams } from "./mesh/Mesh"

interface SelectorNames {
  root: string
  canvas: string
  mesh: string
}

export default class Webgl {
  private selectorNames: SelectorNames
  private shaderParams: ShaderParams
  private $root: HTMLElement | null
  private $canvas: HTMLCanvasElement | null
  private stage: Stage | null
  private mesh: Mesh | null
  private gui: GUI | null
  private rafId: number | null

  constructor() {
    this.selectorNames = {
      root: "[data-page]",
      canvas: "[data-canvas]",
      mesh: "[data-mesh]"
    }
    this.shaderParams = {
      squareSize: 0.4,
      lensDistortion: 1.5,
      rgbShiftR: 0.01,
      rgbShiftG: 0,
      rgbShiftB: -0.01,
      waveFrequency: 10,
      waveStrength: 0.01,
      waveSpeed: 1,
      randomFrequency: 1,
      randomStrength: 0.02,
      randomSpeed: 0.2,
      pointerEase: 0.1
    }

    this.$root = null
    this.$canvas = null
    this.stage = null
    this.mesh = null
    this.gui = null
    this.rafId = null
  }

  public init(): void {
    this.setSelector()
    this.setStage()
    this.addEventListeners()
    this.raf()
  }

  public destroy(): void {
    this.caf()
    this.removeEventListeners()
    this.leave()

    if (this.stage) {
      this.stage.destroy()
    }
  }

  public async enter(): Promise<boolean> {
    const $target = this.$root?.querySelector<HTMLElement>(
      this.selectorNames.mesh
    )
    const scene = this.stage?.getScene()

    if (!$target || !scene) return false

    this.mesh = new Mesh({
      scene,
      $target,
      shaderParams: this.shaderParams
    })
    await this.mesh.init()
    this.setGUI()

    return true
  }

  public leave(): void {
    this.destroyGUI()

    if (this.mesh) {
      this.mesh.destroy()
      this.mesh = null
    }
  }

  private setSelector(): void {
    this.$root = document.querySelector(this.selectorNames.root)
    this.$canvas = document.querySelector(this.selectorNames.canvas)
  }

  private setStage(): void {
    this.stage = new Stage({
      $canvas: this.$canvas
    })
    this.stage.init()
  }

  private setGUI(): void {
    this.destroyGUI()

    this.gui = new GUI({
      title: "Square Texture Effect"
    })

    this.gui.onChange(this.updateShaderParams)

    const squareFolder = this.gui.addFolder("Square")
    const rgbShiftFolder = this.gui.addFolder("RGB Shift")
    const waveFolder = this.gui.addFolder("Wave")
    const randomFolder = this.gui.addFolder("Random")
    const pointerFolder = this.gui.addFolder("Pointer")

    squareFolder
      .add(this.shaderParams, "squareSize", 0, 5, 0.01)
      .name("Square Size")

    squareFolder
      .add(this.shaderParams, "lensDistortion", -5, 5, 0.01)
      .name("Lens Distortion")

    rgbShiftFolder
      .add(this.shaderParams, "rgbShiftR", -0.05, 0.05, 0.001)
      .name("Red Shift")

    rgbShiftFolder
      .add(this.shaderParams, "rgbShiftG", -0.05, 0.05, 0.001)
      .name("Green Shift")

    rgbShiftFolder
      .add(this.shaderParams, "rgbShiftB", -0.05, 0.05, 0.001)
      .name("Blue Shift")

    waveFolder
      .add(this.shaderParams, "waveFrequency", 0, 200, 1)
      .name("Wave Frequency")

    waveFolder
      .add(this.shaderParams, "waveStrength", 0, 0.1, 0.001)
      .name("Wave Strength")

    waveFolder
      .add(this.shaderParams, "waveSpeed", 0, 5, 0.01)
      .name("Wave Speed")

    randomFolder
      .add(this.shaderParams, "randomFrequency", 0.1, 20, 0.1)
      .name("Random Frequency")

    randomFolder
      .add(this.shaderParams, "randomStrength", 0, 0.1, 0.001)
      .name("Random Strength")

    randomFolder
      .add(this.shaderParams, "randomSpeed", 0, 2, 0.01)
      .name("Random Speed")

    pointerFolder
      .add(this.shaderParams, "pointerEase", 0.01, 1, 0.01)
      .name("Pointer Ease")
  }

  private destroyGUI(): void {
    if (!this.gui) return

    this.gui.destroy()
    this.gui = null
  }

  private addEventListeners(): void {
    window.addEventListener("pointermove", this.onPointerMove)
    window.addEventListener("resize", this.onResize)
  }

  private removeEventListeners(): void {
    window.removeEventListener("pointermove", this.onPointerMove)
    window.removeEventListener("resize", this.onResize)
  }

  private onPointerMove = (event: PointerEvent): void => {
    if (this.mesh) {
      this.mesh.onPointerMove(event)
    }
  }

  private onResize = (): void => {
    if (this.stage) {
      this.stage.onResize()
    }

    if (this.mesh) {
      this.mesh.onResize()
    }
  }

  private updateShaderParams = (): void => {
    if (this.mesh) {
      this.mesh.setShaderParams(this.shaderParams)
    }
  }

  private raf(): void {
    this.rafId = requestAnimationFrame(this.onRaf)
  }

  private caf(): void {
    if (this.rafId !== null) {
      cancelAnimationFrame(this.rafId)
      this.rafId = null
    }
  }

  private onRaf = (): void => {
    this.render()
    this.rafId = requestAnimationFrame(this.onRaf)
  }

  private render(): void {
    if (this.mesh) {
      this.mesh.render()
    }

    if (this.stage) {
      this.stage.render()
    }
  }
}
src/scripts/webgl/glsl/chunks/ccLens.glsl
파일 저장

// ---------------------------------------------------------------------------
// Applies a CC Lens-style radial distortion to UV coordinates.
// ---------------------------------------------------------------------------
float getCCLensScale(float distortion, float radius2) {
  if (distortion >= 0.0) {
    return 1.0 + distortion * radius2;
  }

  return 1.0 / (1.0 - distortion * radius2);
}

vec2 getCCLensUv(vec2 uv, vec2 resolution, float distortion) {
  vec2 centeredUv = uv - 0.5;
  vec2 aspectScale = vec2(resolution.x / resolution.y, 1.0);
  vec2 centeredPosition = centeredUv * aspectScale;

  float radius2 = dot(centeredPosition, centeredPosition);
  float lensScale = getCCLensScale(distortion, radius2);

  vec2 distortedPosition = centeredPosition * lensScale;
  vec2 distortedCenteredUv = distortedPosition / aspectScale;
  vec2 distortedUv = distortedCenteredUv + 0.5;
  vec2 distortionOffset = distortedUv - uv;

  return uv - distortionOffset;
}
src/scripts/webgl/glsl/chunks/coverUv.glsl
파일 저장

// ---------------------------------------------------------------------------
// Make the texture UVs behave like CSS's `background-size: cover`.
// ---------------------------------------------------------------------------
vec2 getCoverUv(vec2 uv, vec2 meshSize, vec2 textureSize) {
  vec2 meshRatio = vec2(meshSize.x / meshSize.y, meshSize.y / meshSize.x);
  vec2 textureRatio = vec2(textureSize.x / textureSize.y, textureSize.y / textureSize.x);
  vec2 resolutionRatio = vec2(
    min(meshRatio.x / textureRatio.x, 1.0),
    min(meshRatio.y / textureRatio.y, 1.0)
  );

  return (uv - 0.5) * resolutionRatio + 0.5;
}
src/scripts/webgl/glsl/chunks/random3.glsl
파일 저장

// ---------------------------------------------------------------------------
// Generates a pseudo-random vec3 from a 3D input coordinate.
// ---------------------------------------------------------------------------
// <www.shadertoy.com/view/XsX3zB>
// by Nikita Miropolskiy
vec3 random3(vec3 c) {
  float j = 4096.0 * sin(dot(c, vec3(17.0, 59.4, 15.0)));
  vec3 r;

  r.z = fract(512.0 * j);
  j *= 0.125;
  r.x = fract(512.0 * j);
  j *= 0.125;
  r.y = fract(512.0 * j);

  return r - 0.5;
}
src/scripts/webgl/glsl/frag/frag.glsl
파일 저장

precision highp float;

uniform sampler2D u_texture1;
uniform sampler2D u_texture2;
uniform vec2 u_meshSize;
uniform vec2 u_textureSize1;
uniform vec2 u_textureSize2;
uniform vec2 u_mouse;
uniform float u_squareSize;
uniform float u_lensDistortion;
uniform float u_rgbShiftR;
uniform float u_rgbShiftG;
uniform float u_rgbShiftB;
uniform float u_waveFrequency;
uniform float u_waveStrength;
uniform float u_waveSpeed;
uniform float u_randomFrequency;
uniform float u_randomStrength;
uniform float u_randomSpeed;
uniform float u_time;

varying vec2 v_uv;

#include "../chunks/coverUv.glsl"
#include "../chunks/ccLens.glsl"
#include "../chunks/random3.glsl"

void main() {
  vec2 texture2Uv = getCoverUv(v_uv, u_meshSize, u_textureSize2);

  // Convert the square UV coordinates from the 0.0-to-1.0 range to the -1.0-to-1.0 range.
  vec2 uvSquare = v_uv * 2.0 - 1.0;

  // Offset the coordinates so the mouse position becomes the origin for the square test.
  uvSquare -= u_mouse;

  // Calculate an aspect-ratio correction factor for determining the square's bounds.
  vec2 squareAspectScale = vec2(min((u_meshSize.y / u_meshSize.x), 1.0), min((u_meshSize.x / u_meshSize.y), 1.0));

  // Correct the test coordinates so the mask remains square.
  uvSquare /= squareAspectScale;

  float wave = sin(
    texture2Uv.y * u_waveFrequency
    + u_time * u_waveSpeed
  ) * u_waveStrength;
  texture2Uv.y += wave;
  texture2Uv += random3(
    vec3(texture2Uv * u_randomFrequency, u_time * u_randomSpeed)
  ).x * u_randomStrength;

  // Store half the square's side length so u_squareSize can control its size.
  float squareHalfSize = u_squareSize;

  // Calculate the square's left edge.
  float left = -squareHalfSize;

  // Calculate the square's right edge.
  float right = squareHalfSize;

  // Calculate the square's bottom edge.
  float bottom = -squareHalfSize;

  // Calculate the square's top edge.
  float top = squareHalfSize;

  // Return 1.0 when uvSquare is inside the square and 0.0 when it is outside.
  float squareMask = step(left, uvSquare.x) * (1.0 - step(right, uvSquare.x)) * step(bottom, uvSquare.y) * (1.0 - step(top, uvSquare.y));

  // Convert uvSquare to the 0.0-to-1.0 range for the CC Lens distortion.
  vec2 squareUv = uvSquare / (squareHalfSize * 2.0) + 0.5;

  // Apply the CC Lens distortion inside the square.
  vec2 distortedSquareUv = getCCLensUv(squareUv, vec2(1.0), u_lensDistortion);

  // Calculate the square-local offset from the difference before and after distortion.
  vec2 squareLensOffset = distortedSquareUv - squareUv;

  // squareLensOffset is relative to the square. Adding it directly to v_uv would treat it as
  // relative to the entire mesh and make the distortion too large, so scale it by the square's
  // half-size and aspect-ratio correction factor to convert it to a mesh-relative offset.
  vec2 viewportLensOffset = squareLensOffset * squareHalfSize * squareAspectScale;

  // Convert the coordinates inside the square to UV coordinates for u_texture1.
  vec2 lensTexture1Uv = getCoverUv(v_uv + viewportLensOffset, u_meshSize, u_textureSize1);

  // Use the square's center as the origin so the RGB shift grows stronger toward the outside.
  vec2 rgbShiftDirection = (squareUv - 0.5) * 2.0;

  // Sample each RGB channel with a different UV offset to create color separation.
  float r = texture2D(u_texture1, lensTexture1Uv + rgbShiftDirection * u_rgbShiftR).r;
  float g = texture2D(u_texture1, lensTexture1Uv + rgbShiftDirection * u_rgbShiftG).g;
  float b = texture2D(u_texture1, lensTexture1Uv + rgbShiftDirection * u_rgbShiftB).b;

  // Recombine the shifted u_texture1 channels to create the color inside the square.
  vec4 insideColor = vec4(r, g, b, 1.0);

  // Sample u_texture2 for the color outside the square.
  vec4 outsideColor = texture2D(u_texture2, texture2Uv);

  // Blend the inside and outside colors according to the square mask.
  vec4 finalColor = mix(outsideColor, insideColor, squareMask);

  gl_FragColor = finalColor;
}
src/scripts/webgl/glsl/frag/steps/01-display-color-image.glsl
파일 저장

precision highp float;

uniform sampler2D u_texture1;
uniform sampler2D u_texture2;
uniform vec2 u_meshSize;
uniform vec2 u_textureSize1;
uniform vec2 u_textureSize2;

varying vec2 v_uv;

#include "../chunks/coverUv.glsl"

void main() {
  vec2 texture1Uv = getCoverUv(
    v_uv,
    u_meshSize,
    u_textureSize1
  );

  vec2 texture2Uv = getCoverUv(
    v_uv,
    u_meshSize,
    u_textureSize2
  );

  vec4 insideColor = texture2D(
    u_texture1,
    texture1Uv
  );

  vec4 outsideColor = texture2D(
    u_texture2,
    texture2Uv
  );

  gl_FragColor = insideColor;
}
src/scripts/webgl/glsl/frag/steps/02-display-grayscale-image.glsl
파일 저장

precision highp float;

uniform sampler2D u_texture1;
uniform sampler2D u_texture2;
uniform vec2 u_meshSize;
uniform vec2 u_textureSize1;
uniform vec2 u_textureSize2;

varying vec2 v_uv;

#include "../chunks/coverUv.glsl"

void main() {
  vec2 texture1Uv = getCoverUv(
    v_uv,
    u_meshSize,
    u_textureSize1
  );

  vec2 texture2Uv = getCoverUv(
    v_uv,
    u_meshSize,
    u_textureSize2
  );

  vec4 insideColor = texture2D(
    u_texture1,
    texture1Uv
  );

  vec4 outsideColor = texture2D(
    u_texture2,
    texture2Uv
  );

  gl_FragColor = outsideColor;
}
src/scripts/webgl/glsl/frag/steps/03-create-square-mask.glsl
파일 저장

precision highp float;

uniform sampler2D u_texture1;
uniform sampler2D u_texture2;
uniform vec2 u_meshSize;
uniform vec2 u_textureSize1;
uniform vec2 u_textureSize2;
uniform float u_squareSize;

varying vec2 v_uv;

#include "../chunks/coverUv.glsl"

void main() {
  vec2 texture1Uv = getCoverUv(
    v_uv,
    u_meshSize,
    u_textureSize1
  );

  vec2 texture2Uv = getCoverUv(
    v_uv,
    u_meshSize,
    u_textureSize2
  );

  vec4 insideColor = texture2D(
    u_texture1,
    texture1Uv
  );

  vec4 outsideColor = texture2D(
    u_texture2,
    texture2Uv
  );

  vec2 uvSquare = v_uv * 2.0 - 1.0;

  float squareHalfSize = u_squareSize;

  float left = -squareHalfSize;
  float right = squareHalfSize;
  float bottom = -squareHalfSize;
  float top = squareHalfSize;

  float squareMask =
    step(left, uvSquare.x)
    * (1.0 - step(right, uvSquare.x))
    * step(bottom, uvSquare.y)
    * (1.0 - step(top, uvSquare.y));

  gl_FragColor = vec4(vec3(squareMask), 1.0);
}
src/scripts/webgl/glsl/frag/steps/04-correct-mask-aspect-ratio.glsl
파일 저장

precision highp float;

uniform sampler2D u_texture1;
uniform sampler2D u_texture2;
uniform vec2 u_meshSize;
uniform vec2 u_textureSize1;
uniform vec2 u_textureSize2;
uniform float u_squareSize;

varying vec2 v_uv;

#include "../chunks/coverUv.glsl"

void main() {
  vec2 texture1Uv = getCoverUv(
    v_uv,
    u_meshSize,
    u_textureSize1
  );

  vec2 texture2Uv = getCoverUv(
    v_uv,
    u_meshSize,
    u_textureSize2
  );

  vec4 insideColor = texture2D(
    u_texture1,
    texture1Uv
  );

  vec4 outsideColor = texture2D(
    u_texture2,
    texture2Uv
  );

  vec2 uvSquare = v_uv * 2.0 - 1.0;

  vec2 squareAspectScale = vec2(
    min(u_meshSize.y / u_meshSize.x, 1.0),
    min(u_meshSize.x / u_meshSize.y, 1.0)
  );

  uvSquare /= squareAspectScale;

  float squareHalfSize = u_squareSize;

  float left = -squareHalfSize;
  float right = squareHalfSize;
  float bottom = -squareHalfSize;
  float top = squareHalfSize;

  float squareMask =
    step(left, uvSquare.x)
    * (1.0 - step(right, uvSquare.x))
    * step(bottom, uvSquare.y)
    * (1.0 - step(top, uvSquare.y));

  gl_FragColor = vec4(vec3(squareMask), 1.0);
}
src/scripts/webgl/glsl/frag/steps/05-composite-images.glsl
파일 저장

precision highp float;

uniform sampler2D u_texture1;
uniform sampler2D u_texture2;
uniform vec2 u_meshSize;
uniform vec2 u_textureSize1;
uniform vec2 u_textureSize2;
uniform float u_squareSize;

varying vec2 v_uv;

#include "../chunks/coverUv.glsl"

void main() {
  vec2 texture1Uv = getCoverUv(
    v_uv,
    u_meshSize,
    u_textureSize1
  );

  vec2 texture2Uv = getCoverUv(
    v_uv,
    u_meshSize,
    u_textureSize2
  );

  vec2 uvSquare = v_uv * 2.0 - 1.0;

  vec2 squareAspectScale = vec2(
    min(u_meshSize.y / u_meshSize.x, 1.0),
    min(u_meshSize.x / u_meshSize.y, 1.0)
  );

  uvSquare /= squareAspectScale;

  float squareHalfSize = u_squareSize;

  float left = -squareHalfSize;
  float right = squareHalfSize;
  float bottom = -squareHalfSize;
  float top = squareHalfSize;

  float squareMask =
    step(left, uvSquare.x)
    * (1.0 - step(right, uvSquare.x))
    * step(bottom, uvSquare.y)
    * (1.0 - step(top, uvSquare.y));

  vec4 insideColor = texture2D(
    u_texture1,
    texture1Uv
  );

  vec4 outsideColor = texture2D(
    u_texture2,
    texture2Uv
  );

  vec4 finalColor = mix(
    outsideColor,
    insideColor,
    squareMask
  );

  gl_FragColor = finalColor;
}
src/scripts/webgl/glsl/frag/steps/06-apply-cc-lens-distortion.glsl
파일 저장

precision highp float;

uniform sampler2D u_texture1;
uniform sampler2D u_texture2;
uniform vec2 u_meshSize;
uniform vec2 u_textureSize1;
uniform vec2 u_textureSize2;
uniform float u_squareSize;
uniform float u_lensDistortion;

varying vec2 v_uv;

#include "../chunks/coverUv.glsl"
#include "../chunks/ccLens.glsl"

void main() {
  vec2 texture2Uv = getCoverUv(
    v_uv,
    u_meshSize,
    u_textureSize2
  );

  vec2 uvSquare = v_uv * 2.0 - 1.0;

  vec2 squareAspectScale = vec2(
    min(u_meshSize.y / u_meshSize.x, 1.0),
    min(u_meshSize.x / u_meshSize.y, 1.0)
  );

  uvSquare /= squareAspectScale;

  float squareHalfSize = u_squareSize;

  float left = -squareHalfSize;
  float right = squareHalfSize;
  float bottom = -squareHalfSize;
  float top = squareHalfSize;

  float squareMask =
    step(left, uvSquare.x)
    * (1.0 - step(right, uvSquare.x))
    * step(bottom, uvSquare.y)
    * (1.0 - step(top, uvSquare.y));

  vec2 squareUv =
    uvSquare / (squareHalfSize * 2.0) + 0.5;

  vec2 distortedSquareUv = getCCLensUv(
    squareUv,
    vec2(1.0),
    u_lensDistortion
  );

  vec2 squareLensOffset =
    distortedSquareUv - squareUv;

  vec2 viewportLensOffset =
    squareLensOffset
    * squareHalfSize
    * squareAspectScale;

  vec2 lensTexture1Uv = getCoverUv(
    v_uv + viewportLensOffset,
    u_meshSize,
    u_textureSize1
  );

  vec4 insideColor = texture2D(
    u_texture1,
    lensTexture1Uv
  );

  vec4 outsideColor = texture2D(
    u_texture2,
    texture2Uv
  );

  vec4 finalColor = mix(
    outsideColor,
    insideColor,
    squareMask
  );

  gl_FragColor = finalColor;
}
src/scripts/webgl/glsl/frag/steps/07-apply-rgb-shift.glsl
파일 저장

precision highp float;

uniform sampler2D u_texture1;
uniform sampler2D u_texture2;
uniform vec2 u_meshSize;
uniform vec2 u_textureSize1;
uniform vec2 u_textureSize2;
uniform float u_squareSize;
uniform float u_lensDistortion;
uniform float u_rgbShiftR;
uniform float u_rgbShiftG;
uniform float u_rgbShiftB;

varying vec2 v_uv;

#include "../chunks/coverUv.glsl"
#include "../chunks/ccLens.glsl"

void main() {
  vec2 texture2Uv = getCoverUv(
    v_uv,
    u_meshSize,
    u_textureSize2
  );

  vec2 uvSquare = v_uv * 2.0 - 1.0;

  vec2 squareAspectScale = vec2(
    min(u_meshSize.y / u_meshSize.x, 1.0),
    min(u_meshSize.x / u_meshSize.y, 1.0)
  );

  uvSquare /= squareAspectScale;

  float squareHalfSize = u_squareSize;

  float left = -squareHalfSize;
  float right = squareHalfSize;
  float bottom = -squareHalfSize;
  float top = squareHalfSize;

  float squareMask =
    step(left, uvSquare.x)
    * (1.0 - step(right, uvSquare.x))
    * step(bottom, uvSquare.y)
    * (1.0 - step(top, uvSquare.y));

  vec2 squareUv =
    uvSquare / (squareHalfSize * 2.0) + 0.5;

  vec2 distortedSquareUv = getCCLensUv(
    squareUv,
    vec2(1.0),
    u_lensDistortion
  );

  vec2 squareLensOffset =
    distortedSquareUv - squareUv;

  vec2 viewportLensOffset =
    squareLensOffset
    * squareHalfSize
    * squareAspectScale;

  vec2 lensTexture1Uv = getCoverUv(
    v_uv + viewportLensOffset,
    u_meshSize,
    u_textureSize1
  );

  vec2 rgbShiftDirection =
    (squareUv - 0.5) * 2.0;

  float r = texture2D(
    u_texture1,
    lensTexture1Uv
      + rgbShiftDirection * u_rgbShiftR
  ).r;
  float g = texture2D(
    u_texture1,
    lensTexture1Uv
      + rgbShiftDirection * u_rgbShiftG
  ).g;
  float b = texture2D(
    u_texture1,
    lensTexture1Uv
      + rgbShiftDirection * u_rgbShiftB
  ).b;

  vec4 insideColor = vec4(r, g, b, 1.0);

  vec4 outsideColor = texture2D(
    u_texture2,
    texture2Uv
  );

  vec4 finalColor = mix(
    outsideColor,
    insideColor,
    squareMask
  );

  gl_FragColor = finalColor;
}
src/scripts/webgl/glsl/frag/steps/08-follow-mouse.glsl
파일 저장

precision highp float;

uniform sampler2D u_texture1;
uniform sampler2D u_texture2;
uniform vec2 u_meshSize;
uniform vec2 u_textureSize1;
uniform vec2 u_textureSize2;
uniform vec2 u_mouse;
uniform float u_squareSize;
uniform float u_lensDistortion;
uniform float u_rgbShiftR;
uniform float u_rgbShiftG;
uniform float u_rgbShiftB;

varying vec2 v_uv;

#include "../chunks/coverUv.glsl"
#include "../chunks/ccLens.glsl"

void main() {
  vec2 texture2Uv = getCoverUv(
    v_uv,
    u_meshSize,
    u_textureSize2
  );

  vec2 uvSquare = v_uv * 2.0 - 1.0;

  uvSquare -= u_mouse;

  vec2 squareAspectScale = vec2(
    min(u_meshSize.y / u_meshSize.x, 1.0),
    min(u_meshSize.x / u_meshSize.y, 1.0)
  );

  uvSquare /= squareAspectScale;

  float squareHalfSize = u_squareSize;

  float left = -squareHalfSize;
  float right = squareHalfSize;
  float bottom = -squareHalfSize;
  float top = squareHalfSize;

  float squareMask =
    step(left, uvSquare.x)
    * (1.0 - step(right, uvSquare.x))
    * step(bottom, uvSquare.y)
    * (1.0 - step(top, uvSquare.y));

  vec2 squareUv =
    uvSquare / (squareHalfSize * 2.0) + 0.5;

  vec2 distortedSquareUv = getCCLensUv(
    squareUv,
    vec2(1.0),
    u_lensDistortion
  );

  vec2 squareLensOffset =
    distortedSquareUv - squareUv;

  vec2 viewportLensOffset =
    squareLensOffset
    * squareHalfSize
    * squareAspectScale;

  vec2 lensTexture1Uv = getCoverUv(
    v_uv + viewportLensOffset,
    u_meshSize,
    u_textureSize1
  );

  vec2 rgbShiftDirection =
    (squareUv - 0.5) * 2.0;

  float r = texture2D(
    u_texture1,
    lensTexture1Uv
      + rgbShiftDirection * u_rgbShiftR
  ).r;
  float g = texture2D(
    u_texture1,
    lensTexture1Uv
      + rgbShiftDirection * u_rgbShiftG
  ).g;
  float b = texture2D(
    u_texture1,
    lensTexture1Uv
      + rgbShiftDirection * u_rgbShiftB
  ).b;

  vec4 insideColor = vec4(r, g, b, 1.0);

  vec4 outsideColor = texture2D(
    u_texture2,
    texture2Uv
  );

  vec4 finalColor = mix(
    outsideColor,
    insideColor,
    squareMask
  );

  gl_FragColor = finalColor;
}
src/scripts/webgl/glsl/vert/vert.glsl
파일 저장

precision highp float;

attribute vec3 position;
attribute vec2 uv;

uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

varying vec2 v_uv;

void main() {
	v_uv = uv;

	gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
src/scripts/webgl/mesh/Mesh.ts
파일 저장

import {
  TextureLoader,
  PlaneGeometry,
  RawShaderMaterial,
  Mesh as ThreeMesh,
  Texture,
  Vector2
} from "three"
import type { IUniform, Scene } from "three"
import VertexShader from "../glsl/vert/vert.glsl"
import FragmentShader from "../glsl/frag/frag.glsl"

interface MeshOptions {
  scene: Scene
  $target: HTMLElement
  shaderParams: ShaderParams
}

interface Uniforms {
  [uniform: string]: IUniform<unknown>
  u_texture1: IUniform<Texture | null>
  u_texture2: IUniform<Texture | null>
  u_meshSize: IUniform<Vector2>
  u_textureSize1: IUniform<Vector2>
  u_textureSize2: IUniform<Vector2>
  u_mouse: IUniform<Vector2>
  u_squareSize: IUniform<number>
  u_lensDistortion: IUniform<number>
  u_rgbShiftR: IUniform<number>
  u_rgbShiftG: IUniform<number>
  u_rgbShiftB: IUniform<number>
  u_waveFrequency: IUniform<number>
  u_waveStrength: IUniform<number>
  u_waveSpeed: IUniform<number>
  u_randomFrequency: IUniform<number>
  u_randomStrength: IUniform<number>
  u_randomSpeed: IUniform<number>
  u_time: IUniform<number>
}

export interface ShaderParams {
  squareSize: number
  lensDistortion: number
  rgbShiftR: number
  rgbShiftG: number
  rgbShiftB: number
  waveFrequency: number
  waveStrength: number
  waveSpeed: number
  randomFrequency: number
  randomStrength: number
  randomSpeed: number
  pointerEase: number
}

export default class Mesh {
  private scene: Scene
  private uniforms: Uniforms
  private $target: HTMLElement
  private geometry: PlaneGeometry | null
  private material: RawShaderMaterial | null
  private mesh: ThreeMesh | null
  private windowWidth: number
  private windowHeight: number
  private mouse: Vector2
  private mouseEase: Vector2
  private pointerEase: number

  constructor({ scene, $target, shaderParams }: MeshOptions) {
    this.uniforms = {
      u_texture1: { value: null },
      u_texture2: { value: null },
      u_meshSize: { value: new Vector2(1, 1) },
      u_textureSize1: { value: new Vector2(1, 1) },
      u_textureSize2: { value: new Vector2(1, 1) },
      u_mouse: { value: new Vector2() },
      u_squareSize: { value: shaderParams.squareSize },
      u_lensDistortion: { value: shaderParams.lensDistortion },
      u_rgbShiftR: { value: shaderParams.rgbShiftR },
      u_rgbShiftG: { value: shaderParams.rgbShiftG },
      u_rgbShiftB: { value: shaderParams.rgbShiftB },
      u_waveFrequency: { value: shaderParams.waveFrequency },
      u_waveStrength: { value: shaderParams.waveStrength },
      u_waveSpeed: { value: shaderParams.waveSpeed },
      u_randomFrequency: { value: shaderParams.randomFrequency },
      u_randomStrength: { value: shaderParams.randomStrength },
      u_randomSpeed: { value: shaderParams.randomSpeed },
      u_time: { value: 0 }
    }

    this.scene = scene
    this.$target = $target

    this.geometry = null
    this.material = null
    this.mesh = null

    this.windowWidth = 0
    this.windowHeight = 0
    this.pointerEase = shaderParams.pointerEase

    this.mouse = new Vector2()
    this.mouseEase = new Vector2()
  }

  public async init(): Promise<void> {
    this.setWindowSize()
    await this.setTexture()
    this.setMesh()
    this.setMeshScale()
  }

  public destroy(): void {
    if (this.mesh) {
      this.scene.remove(this.mesh)
    }

    if (this.material) {
      this.material.dispose()
    }

    if (this.uniforms.u_texture1.value) {
      this.uniforms.u_texture1.value.dispose()
    }

    if (this.uniforms.u_texture2.value) {
      this.uniforms.u_texture2.value.dispose()
    }

    if (this.geometry) {
      this.geometry.dispose()
    }

    this.mesh = null
  }

  private setWindowSize(): void {
    this.windowWidth = window.innerWidth
    this.windowHeight = window.innerHeight
  }

  private async setTexture(): Promise<void> {
    const loader = new TextureLoader()
    const texture1Path = this.$target.dataset.texture1Path
    const texture2Path = this.$target.dataset.texture2Path

    if (!texture1Path || !texture2Path) return

    const [texture1, texture2] = await Promise.all([
      loader.loadAsync(texture1Path),
      loader.loadAsync(texture2Path)
    ])

    const image1 = texture1.image as {
      width: number
      height: number
    }

    const image2 = texture2.image as {
      width: number
      height: number
    }

    this.uniforms.u_texture1.value = texture1
    this.uniforms.u_texture2.value = texture2
    this.uniforms.u_textureSize1.value.set(image1.width, image1.height)
    this.uniforms.u_textureSize2.value.set(image2.width, image2.height)
  }

  private setMesh(): void {
    this.geometry = new PlaneGeometry()
    this.material = new RawShaderMaterial({
      vertexShader: VertexShader,
      fragmentShader: FragmentShader,
      uniforms: this.uniforms
    })
    this.mesh = new ThreeMesh(this.geometry, this.material)
    this.scene.add(this.mesh)
  }

  private setMeshScale(): void {
    if (!this.mesh) return

    this.mesh.scale.set(this.windowWidth, this.windowHeight, 1)
    this.uniforms.u_meshSize.value.set(this.mesh.scale.x, this.mesh.scale.y)
  }

  public setShaderParams({
    squareSize,
    lensDistortion,
    rgbShiftR,
    rgbShiftG,
    rgbShiftB,
    waveFrequency,
    waveStrength,
    waveSpeed,
    randomFrequency,
    randomStrength,
    randomSpeed,
    pointerEase
  }: ShaderParams): void {
    this.uniforms.u_squareSize.value = squareSize
    this.uniforms.u_lensDistortion.value = lensDistortion
    this.uniforms.u_rgbShiftR.value = rgbShiftR
    this.uniforms.u_rgbShiftG.value = rgbShiftG
    this.uniforms.u_rgbShiftB.value = rgbShiftB
    this.uniforms.u_waveFrequency.value = waveFrequency
    this.uniforms.u_waveStrength.value = waveStrength
    this.uniforms.u_waveSpeed.value = waveSpeed
    this.uniforms.u_randomFrequency.value = randomFrequency
    this.uniforms.u_randomStrength.value = randomStrength
    this.uniforms.u_randomSpeed.value = randomSpeed
    this.pointerEase = pointerEase
  }

  public onPointerMove(event: PointerEvent): void {
    this.mouse.set(
      (event.clientX / this.windowWidth) * 2 - 1,
      -(event.clientY / this.windowHeight) * 2 + 1
    )
  }

  public onResize(): void {
    this.setWindowSize()
    this.setMeshScale()
  }

  public render(): void {
    if (!this.mesh) return

    this.mouseEase.lerp(this.mouse, this.pointerEase)
    this.uniforms.u_mouse.value.copy(this.mouseEase)
    this.uniforms.u_time.value = performance.now() * 0.001
  }
}
src/scripts/webgl/stage/Stage.ts
파일 저장

import { PerspectiveCamera, Scene, WebGLRenderer } from "three"

const calcViewportDistance = (height: number, fov: number): number => {
  return height / (2 * Math.tan((fov * Math.PI) / 360))
}

interface RendererParams {
  canvas: HTMLCanvasElement | undefined
  alpha: boolean
}

interface RendererColorParams {
  color: number
  alpha: number
}

interface StageSize {
  width: number
  height: number
  aspect: {
    xy: number
  }
}

interface CameraParams {
  fov: number
  aspect: number
  near: number
  far: number
}

export default class Stage {
  private rendererParams: RendererParams
  private rendererColorParams: RendererColorParams
  private stageSize: StageSize
  private cameraParams: CameraParams
  private $canvas: HTMLCanvasElement | null
  private renderer: WebGLRenderer | null
  private scene: Scene | null
  private camera: PerspectiveCamera | null

  constructor(options = {}) {
    this.rendererParams = {
      canvas: undefined,
      alpha: true
    }
    this.rendererColorParams = {
      color: 0xffffff,
      alpha: 0
    }
    this.stageSize = {
      width: 0,
      height: 0,
      aspect: {
        xy: 0
      }
    }
    this.cameraParams = {
      fov: 45,
      aspect: 0,
      near: 0.01,
      far: 10000
    }

    this.$canvas = null
    this.renderer = null
    this.scene = null
    this.camera = null

    Object.assign(this, options)
  }

  public init(): void {
    this.setCanvas()
    this.setScene()
    this.setStageSize()
    this.setCamera()
    this.setRenderer()
  }

  public destroy(): void {
    if (this.renderer) {
      this.renderer.dispose()
    }

    this.camera = null
    this.scene = null
    this.renderer = null
  }

  private setCanvas(): void {
    this.rendererParams.canvas = this.$canvas ?? undefined
  }

  private setScene(): void {
    this.scene = new Scene()
  }

  private setStageSize(): void {
    this.stageSize.width = window.innerWidth
    this.stageSize.height = window.innerHeight
    this.stageSize.aspect.xy = this.stageSize.width / this.stageSize.height
  }

  private setCamera(): void {
    if (!this.camera) {
      this.camera = new PerspectiveCamera(
        this.cameraParams.fov,
        this.cameraParams.aspect,
        this.cameraParams.near,
        this.cameraParams.far
      )
    }

    this.camera.aspect = this.stageSize.aspect.xy
    this.camera.updateProjectionMatrix()
    this.camera.position.z = calcViewportDistance(
      this.stageSize.height,
      this.camera.fov
    )
  }

  private setRenderer(): void {
    if (!this.renderer) {
      this.renderer = new WebGLRenderer(this.rendererParams)
    }

    this.renderer.setClearColor(
      this.rendererColorParams.color,
      this.rendererColorParams.alpha
    )
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
    this.renderer.setSize(this.stageSize.width, this.stageSize.height)
  }

  public render(): void {
    if (this.renderer && this.scene && this.camera) {
      this.renderer.render(this.scene, this.camera)
    }
  }

  public getScene(): Scene | null {
    return this.scene
  }

  public onResize(): void {
    this.setStageSize()
    this.setRenderer()
    this.setCamera()
  }
}
src/styles/_base.scss
파일 저장

*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

:root {
  --color-text: #f0ede8;
  --color-bg: #232121;
  --color-link: #f0ede8;
  --color-link-hover: #fff;
  --page-padding: 1.5rem;

  font-size: 12px;
}

html {
  text-size-adjust: 100%;
  text-rendering: optimizelegibility;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  touch-action: manipulation;
}

body {
  min-block-size: 100svb;
  margin: 0;
  overflow: hidden;
  color: var(--color-text);
  background-color: var(--color-bg);
  font-family:
    system-ui,
    -apple-system,
    BlinkMacSystemFont,
    "Segoe UI",
    Roboto,
    Oxygen,
    Ubuntu,
    Cantarell,
    "Open Sans",
    "Helvetica Neue",
    sans-serif;
}

html,
body {
  overscroll-behavior: none;
}

img {
  block-size: auto;
  inline-size: 100%;
  max-inline-size: 100%;
  vertical-align: middle;
}

a {
  color: var(--color-link);
  text-decoration: none;
  cursor: pointer;

  &:hover {
    color: var(--color-link-hover);
    text-decoration: underline;
  }
}

::selection {
  color: var(--color-bg);
  background-color: var(--color-text);
}

:focus:not(:focus-visible) {
  outline: none;
}

:focus-visible {
  outline: 2px solid #f00;
  outline-offset: 3px;
}

@media (scripting: enabled) {
  .loading {
    &::before,
    &::after {
      position: fixed;
      z-index: 10000;
      content: "";
    }

    &::before {
      inset: 0;
      background: var(--color-bg);
    }

    &::after {
      inset-block-start: 50%;
      inset-inline-start: 50%;
      inline-size: 100px;
      block-size: 1px;
      margin-inline-start: -50px;
      background: var(--color-link);
      animation: loader 1.5s ease-in-out infinite alternate forwards;
    }
  }
}

@keyframes loader {
  0% {
    transform: scaleX(0);
    transform-origin: 0% 50%;
  }

  50% {
    transform: scaleX(1);
    transform-origin: 0% 50%;
  }

  50.1% {
    transform: scaleX(1);
    transform-origin: 100% 50%;
  }

  100% {
    transform: scaleX(0);
    transform-origin: 100% 50%;
  }
}

.no-script-message {
  position: fixed;
  inset: 0;
  z-index: 20000;
  display: grid;
  place-content: center;
  gap: 0.75rem;
  padding: 2rem;
  color: var(--color-text);
  text-align: center;
  background: var(--color-bg);

  h1 {
    font-size: 1rem;
  }
}

.frame {
  position: relative;
  z-index: 1000;
  display: grid;
  grid-template:
    "title" auto
    "links" auto
    "tags" auto
    "sponsor" auto / 100%;
  gap: 1rem;
  justify-items: start;
  padding: var(--page-padding);
  pointer-events: none;

  #cdawrap {
    grid-area: sponsor;
    justify-self: start;
  }

  a,
  button {
    pointer-events: auto;
  }
}

.frame-title {
  grid-area: title;
  margin: 0;
  font-size: inherit;
  font-weight: 400;
}

.frame-links,
.frame-tags {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
}

.frame-links {
  grid-area: links;
}

.frame-tags {
  grid-area: tags;
}

@media screen and (width >= 53em) {
  .frame {
    position: fixed;
    inset: 0;
    grid-template:
      "title links ." auto
      "tags tags sponsor" auto / auto auto 1fr;
    align-content: space-between;
    inline-size: 100%;
    block-size: 100%;

    #cdawrap {
      justify-self: end;
      max-inline-size: 300px;
      text-align: end;
    }
  }
}

.webgl-ready [data-mesh] {
  opacity: 0;
}
src/styles/index.scss
파일 저장

@use "base";
stylelint.config.mjs
파일 저장

export default {
  extends: ["stylelint-config-standard-scss"],
  ignoreFiles: ["dist/**", "node_modules/**"],
  overrides: [
    {
      files: ["**/*.astro"],
      customSyntax: "postcss-html",
    },
  ],
};
Media credits and license evidence실행 안내·자료
파일 저장

README.md
## Credits

- Built with [Astro](https://astro.build/), [Three.js](https://threejs.org/), and [lil-gui](https://lil-gui.georgealways.com/)
- The `random3` GLSL function is based on [Nikita Miropolskiy's Shadertoy example](https://www.shadertoy.com/view/XsX3zB)
- Demo imagery uses [Red rose flower](https://unsplash.com/ja/%E5%86%99%E7%9C%9F/%E8%B5%A4%E3%81%84%E3%83%90%E3%83%A9%E3%81%AE%E8%8A%B1-Y7iHt3LRWGg) by [Kelly Sikkema](https://unsplash.com/@kellysikkema) on Unsplash


## License

[MIT](LICENSE)
LICENSE실행 안내·자료
파일 저장

MIT License

Copyright (c) 2026 Mouse Following Square Lens Effect contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Bundled dependency licenses실행 안내·자료
파일 저장

lil-gui@0.21.0 — LICENSE.md
MIT License

Copyright (c) 2019 George Michael Brower

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

three@0.184.0 — LICENSE
The MIT License

Copyright © 2010-2026 three.js authors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.