• About Us
  • Privacy Policy
  • Disclaimer
  • Contact Us
AimactGrow
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing
No Result
View All Result
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing
No Result
View All Result
AimactGrow
No Result
View All Result

Inside HAOQI.DESIGN: Letting DOM and WebGL Share a Retro-Futurist Stage

Admin by Admin
August 15, 2026
Home Coding
Share on FacebookShare on Twitter



As a baby, I first noticed the iMac G3 in an artwork e-book. Its translucent shell and distinctive interface made me discover the visible design of digital merchandise lengthy earlier than I knew what that area was known as. Apple’s hey (once more) marketing campaign stayed with me too, and have become one of many beginning factors for this web site.

As we speak I work throughout design techniques and design engineering. Frontend improvement started as a method to notice my designs, then step by step grew to become a part of the work itself. I replace my private web site from time to time to gather what I’ve been fascinated about and making. It’s an index of my work, but in addition a design and improvement challenge in its personal proper.

This model got here collectively over about three to 5 months, in bits of spare time. Throughout the earlier two years, I had been utilizing AI as a method to study extra about shaders and Three.js. This time, I needed to convey that data into one coherent expertise as a substitute of presenting a group of remoted results. The visible path grew round hey, glass, coloured mild, and retro-futurism. The central technical query was tips on how to give DOM and WebGL distinct jobs whereas holding them collectively by scroll and interplay.

Technical Overview

  • Subsequent.js + React
  • Lenis
  • Movement
  • Three.js / React Three Fiber / Drei
  • Customized shaders / post-processing
  • Spline for 3D fashions
  • Figma for drawing stickers

1. Preserving DOM and WebGL on the Identical Body

One scroll supply for DOM and WebGL

The positioning scrolls vertically within the traditional means. I needed DOM to deal with textual content and typography, whereas a set canvas carries the glass mannequin and picture results. Scrolling naturally strikes the DOM, so the primary process was to make each object within the canvas comply with the identical place.

Within the first model, Lenis up to date the DOM whereas R3F learn window.scrollY inside useFrame and up to date WebGL. It regarded superb at low speeds, however a quick scroll revealed a constant one-frame delay in WebGL. The rationale was that Lenis and R3F every owned a requestAnimationFrame loop. If R3F ran first, it learn the earlier scroll worth. Lenis would solely advance afterwards and transfer the DOM for the present body. A customized scroll container made the mismatch extra apparent as a result of window.scrollY was not essentially the worth Lenis was sustaining. No quantity of interpolation tuning may remedy an issue attributable to execution order and an unreliable information supply.

After Lenis’ handbook raf method and JOYCO’s WebGL Scroll Sync, I moved scrolling and rendering into one body loop. Lenis’ personal loop is disabled. R3F calls lenis.raf by addEffect, then a ScrollBus data Lenis’ scroll worth for that body. Each later useFrame client reads the identical snapshot. DOM and WebGL now agree on each the info and the second it turns into present.

// scroll_root.tsx
operate ScrollShell({ kids }: { kids: React.ReactNode }) {
  return (
    
      
      {kids}
    
  )
}

operate LenisScrollEnvBridge() {
  const lenis = useLenis()

  useEffect(() => {
    bindLenisScrollBus(lenis ?? null)
    return () => bindLenisScrollBus(null)
  }, [lenis])

  useEffect(() => {
    if (!lenis) return
    return addEffect((time: quantity) => {
      lenis.raf(time)
    })
  }, [lenis])

  return null
}

As soon as lenis.raf advances the scroll, Lenis emits its scroll occasion and updates the ScrollBus in the identical body. WebGL elements that run afterwards can learn the recent snapshot straight.

// lenis_scroll_bus.ts
import sort Lenis from "lenis"

// The manufacturing snapshot additionally contains restrict, progress, velocity,
// path, and viewportHeight.
sort ScrollSnapshot = { scrollTop: quantity }

let snapshot: ScrollSnapshot = { scrollTop: 0 }
const listeners = new Set<() => void>()
let unbind: (() => void) | null = null

export const bindLenisScrollBus = (lenis: Lenis | null) => {
  unbind?.()
  unbind = null
  if (!lenis) return

  const onScroll = ({ scroll }: { scroll: quantity }) => {
    snapshot = { scrollTop: scroll }
    for (const listener of listeners) listener()
  }

  lenis.on("scroll", onScroll)
  unbind = () => lenis.off("scroll", onScroll)
  snapshot = { scrollTop: lenis.scroll }
}

export const getLenisScrollSnapshot = () => snapshot
export const subscribeLenisScroll = (listener: () => void) => {
  listeners.add(listener)
  return () => listeners.delete(listener)
}

WebGL already runs body by body, so it could merely learn the newest worth. React elements that want scroll state for DOM output subscribe by useSyncExternalStore. The remainder of the element tree stays untouched.

// WebGL reads throughout useFrame with out triggering React renders.
const scrollY = getLenisScrollSnapshot().scrollTop

// React subscribes solely the place the worth impacts DOM output.
const SERVER_SCROLL_SNAPSHOT = { scrollTop: 0 }
const scroll = useSyncExternalStore(
  subscribeLenisScroll,
  getLenisScrollSnapshot,
  () => SERVER_SCROLL_SNAPSHOT,
)

Within the side-by-side demo, the left retains the 2 impartial loops whereas the correct makes use of the shared body loop and ScrollBus. The one-frame slip disappears on the correct, even throughout a quick scroll.

One pointer coordinate system for each impact

When interplay stays inside a canvas, R3F’s normalized state.pointer is normally sufficient. On this web site, the identical pointer additionally drives DOM coordinate readouts, digicam parallax, the glass rim mild, and a fluid impact. Earlier variations let every characteristic pay attention for pointer enter by itself. They labored, however each new impact needed to repeat the identical coordinate conversion, Y-axis inversion, and leave-state dealing with. The enter path grew extra fragmented with each addition.

I reused the ScrollBus thought and constructed a world PointerBus. It converts browser coordinates right into a 0-to-1 UV as soon as, and retains an inside flag to say whether or not the pointer remains to be within the window. When the pointer leaves, the web page loses focus, or the tab turns into hidden, the UV returns to the middle. Results can settle again to their preliminary state as a substitute of leaping from a stale coordinate when the pointer comes again.

A single write updates each a mutable Vector2 for WebGL and an immutable snapshot for React. Something new can devour the PointerBus with out including one other listener or inventing its personal coordinate guidelines.

sort PointerSnapshot = {
  x: quantity
  y: quantity
  inside: boolean
}

// One write retains DOM and WebGL on the identical x / y / inside state.
const updatePointer = (subsequent: PointerSnapshot) => {
  snapshotRef.present = subsequent      // React snapshot
  uv.set(subsequent.x, subsequent.y)          // WebGL
  insideRef.present = subsequent.inside // WebGL
  scheduleNotify()                // React, at most as soon as per body
}

With scroll and pointer enter unified, I first put the system to work on the challenge grid. It holds the location’s principal content material and has probably the most overlap between DOM format and WebGL results, which made it a helpful check for the entire hybrid method.

2. DOM for Format, WebGL for the Surprising

In contrast to the hero, the challenge record must be simple to learn and browse earlier than it does anything. I didn’t wish to sacrifice the work for an impact, however I additionally didn’t need one other acquainted picture grid. DOM and CSS Grid due to this fact personal the construction, responsive conduct, and accessibility. Clear picture placeholders are measured and mirrored into the canvas, the place WebGL steps in just for states that will be awkward to create with common DOM. The expertise additionally doesn’t ask guests to allow a browser flag for the experimental HTML-in-Canvas API.

Mirroring a DOM grid in WebGL

Every challenge picture retains a clear DOM placeholder with a ref. CSS Grid decides its place and dimensions. The browser’s Ingredient.getBoundingClientRect() provides me that rectangle, however the manufacturing web site doesn’t ask each card to learn format on each body. One sampler maintains a shared rectangle cache as a substitute.

Throughout a scroll, the sampler first corrects cached rectangles by the scroll delta. Playing cards close to the viewport are measured each body so they continue to be aligned after a format change. Distant playing cards refresh as soon as each 12 frames, staggered throughout the record, which avoids bunching all DOM reads into the identical body.

// Simplified DomTargetRectSampler.
useFrame(() => {
  const rects = targetRectMapRef.present
  const scrollTop = getScrollTop()
  const deltaY = scrollTop - lastScrollTop
  lastScrollTop = scrollTop

  // Scroll strikes cached viewport rects with out one other format learn.
  for (const rect of Object.values(rects)) {
    rect.high -= deltaY
    rect.backside -= deltaY
  }

  layers.forEach((layer, index) => )

  body += 1
}, -3)

The sampler runs earlier than the picture elements, so a close-by picture reads a freshly measured DOM rectangle in the identical body. The cache lives in a ref map and by no means causes a React render. A mesh hides and stops updating when its texture will not be prepared, its rectangle is invalid, or the picture is way exterior the viewport. Its reveal progress additionally resets offscreen, so it’s prepared when the cardboard returns.

I exploit one fullscreen mesh per picture to maintain the coordinate math easy. Fairly than transferring 3D geometry to match the DOM, I write the rectangle’s place and dimension into uRect. The shader turns display UV into card-local coordinates, holding the canvas picture aligned with its placeholder. Fullscreen meshes do add overdraw, so solely photographs close to the viewport are rendered.

// dom_sync.frag.glsl
uniform vec4 uRect; // xy origin, zw dimension
uniform sampler2D map;

vec4 sampleDomImage(vec2 screenUv) {
  vec2 localUv = (screenUv - uRect.xy) / uRect.zw;
  vec2 edge = min(localUv, 1.0 - localUv);
  float inside = step(0.0, edge.x) * step(0.0, edge.y);
  vec4 colour = texture2D(map, clamp(localUv, 0.0, 1.0));
  colour.a *= inside;
  return colour;
}

Writing uRect solely requires one coordinate-system correction. Display coordinates start on the high left, whereas shader UV begins on the backside left, so Y must be flipped. After that, CSS stays free to vary columns, gaps, and card ratios. WebGL solely follows the ensuing rectangles.

The surprising begins on hover

I needed every challenge card to carry extra visible data, so each card has two photographs. A plain crossfade nonetheless felt too acquainted. As a substitute, the shader divides the display into a set grid. The reveal spreads from the middle of the cardboard whereas a sq. grows inside every cell, step by step uncovering the second picture. This dot-matrix language later discovered its means into loading, web page transitions, and the cellular menu.

The shader does the work in three steps. First, uRect transforms fullscreen UV into native card UV so each photographs match the identical DOM placeholder. Subsequent, display area is split into fixed-size cells. Lastly, the cardboard’s side ratio is compensated for whereas hover progress expands a round area from the middle. Inside that area, every cell’s sq. grows. The ensuing masks blends the 2 textures.

// Inputs shared by each photographs.
uniform sampler2D map;
uniform sampler2D mapHover;
uniform vec4 uRect;
uniform float uHoverRevealProgress;
uniform float uDotPixelSize;
uniform vec2 uViewportPx;

vec4 revealHoverImage(vec2 screenUv) {
  // 1. Map the full-screen UV into the DOM card.
  vec2 localUv = (screenUv - uRect.xy) / uRect.zw;
  float rectWidthPx = max(uRect.z * uViewportPx.x, 1.0);
  float rectHeightPx = max(uRect.w * uViewportPx.y, 1.0);

  // 2. Divide display area into fixed-size cells.
  vec2 viewportPx = max(uViewportPx, vec2(1.0));
  vec2 cellSizeUv = vec2(max(2.0, uDotPixelSize)) / viewportPx;
  vec2 cellUv = fract(screenUv / cellSizeUv);
  float squareDist = max(abs(cellUv.x - 0.5), abs(cellUv.y - 0.5));

  // 3. Increase from the cardboard heart and develop a sq. in every cell.
  float rectAspect = rectWidthPx / rectHeightPx;
  vec2 centered = localUv * 2.0 - 1.0;
  centered.x *= rectAspect;
  float distToCenter = size(centered);
  float maxRadius = size(vec2(rectAspect, 1.0));
  float progress = clamp(uHoverRevealProgress, 0.0, 1.0);
  float radius = progress * (maxRadius + 0.12);
  float develop = 1.0 - smoothstep(radius - 0.12, radius + 0.12, distToCenter);
  develop *= step(0.0001, progress);

  float squareExtent = combine(0.0, 0.5, develop);
  float squareAa = max(fwidth(squareDist), 0.0001);
  float squareMask = 1.0 - smoothstep(
    squareExtent - squareAa,
    squareExtent + squareAa,
    squareDist
  );

  // Combine the aligned textures with the generated masks.
  vec4 baseColor = texture2D(map, clamp(localUv, 0.0, 1.0));
  vec4 hoverColor = texture2D(mapHover, clamp(localUv, 0.0, 1.0));
  vec4 colour = combine(baseColor, hoverColor, squareMask);
  vec2 edge = min(localUv, 1.0 - localUv);
  colour.a *= step(0.0, edge.x) * step(0.0, edge.y);

  return colour;
}

Growing the picture because it enters the body

This concept got here from a photographer’s web site I as soon as noticed. The shift from a adverse again to full colour felt proper as a picture entrance, so I introduced it into the challenge grid. As soon as a card enters the viewport, its picture develops over 0.8 seconds. The progress returns to zero when the cardboard leaves fully, able to play once more on its subsequent go to. With prefers-reduced-motion enabled, the transition is skipped and the unique colour seems instantly.

uniform float uPolarityPositive; // 0 = adverse, 1 = authentic

// Mix from the adverse picture again to its authentic colour.
vec3 applyPolarity(vec3 rgb) {
  float t = clamp(uPolarityPositive, 0.0, 1.0);
  return combine(1.0 - rgb, rgb, t);
}

Making scroll velocity seen with a shader

After hover and the doorway impact had been in place, I needed one final WebGL conduct for the grid. Quick scrolling ought to really feel sooner. The gap travelled between two frames, divided by time, provides me velocity. I normalize it into uCurlStrength, so the photographs flex barely alongside the horizontal axis in response to hurry quite than accumulating distortion with scroll distance.

Trackpads introduce a whole lot of small velocity fluctuations. To maintain them from turning into visible noise, the power makes use of two time constants with a quick assault and a slower launch. delta is clamped as nicely, stopping an excessive worth when a backgrounded web page wakes up.

// dom_sync_img.tsx
operate createCurlStrengthSampler() {
  let previousScrollY: quantity | null = null
  let exercise = 0

  return (scrollY: quantity, delta: quantity) => {
    const dt = THREE.MathUtils.clamp(delta, 1 / 240, 0.1)
    const velocity = previousScrollY == null
      ? 0
      : Math.abs(scrollY - previousScrollY) / dt
    previousScrollY = scrollY

    // Normalize scroll velocity into the goal curl exercise.
    const goal = THREE.MathUtils.clamp(velocity / 800, 0, 1)

    // Quick assault and gradual launch clean small trackpad fluctuations.
    const tau = goal > exercise ? 0.025 : 0.175
    const alpha = 1 - Math.exp(-dt / tau)
    exercise += (goal - exercise) * alpha

    // Map the smoothed exercise to the utmost curl power.
    return 0.06 * exercise
  }
}

The sampler retains the earlier scroll place and present power between frames. That reminiscence is what makes an ongoing velocity measurement and its smoothing doable. Within the shader, a semicircular profile controls the horizontal UV compression. The center of the picture strikes little or no, whereas the highest and backside bend additional as uCurlStrength rises.

// dom_sync.frag.glsl
uniform float uCurlStrength;

vec2 applyCurl(vec2 screenUv) {
  float centered = 2.0 * screenUv.y - 1.0;
  float profile = 1.0 - sqrt(max(0.0, 1.0 - centered * centered));

  // Larger velocity will increase uCurlStrength and compresses X close to the highest and backside.
  float uvScale = 1.0 - profile * uCurlStrength;
  float distortedX = (screenUv.x - 0.5) * uvScale + 0.5;
  return vec2(distortedX, screenUv.y);
}

3. Turning hey right into a Glass Centerpiece

As soon as the challenge grid was working, I moved on to the location’s visible centerpiece, the glass hey. The geometry is easy sufficient that I didn’t use Blender. I constructed the textual content in Spline, exported it as GLTF, and stored solely the geometry. Three.js takes care of lighting and materials.

The glass shader builds on Maxime Heckel‘s tutorial, Refraction, dispersion, and different shader mild results. I began with its refraction, chromatic dispersion, and Fresnel concepts, then added interplay, theme-aware tinting, and extra management over rendering price.

Refraction occurs in two passes. First, the glass is excluded from an FBO whereas the scene behind it’s rendered right into a texture. The principle scene then attracts the glass, whose shader samples that texture alongside barely totally different refraction instructions to create distortion and dispersion. The glass makes use of its personal Three.js layer so the FBO by no means captures the thing itself. The falling stickers additionally want to sit down behind the glass if they’re to seem within the refracted pattern.

Letting the spotlight comply with the pointer with out leaving the rim

My first rim mild adopted the pointer straight. When the pointer approached the middle of the display, the mapped mild moved onto the face of the glass. It created a broad, vivid patch and weakened the contour of the letters. Apple’s Liquid Glass presentation gave me a greater reference, with a spotlight that travels across the edge. I stored the path of the pointer however discarded its distance from the middle.

Pointer UV is raycast onto a aircraft in entrance of the mannequin to supply mappedX and mappedY. atan2 turns that place into an angle, and the sunshine is all the time positioned on a circle with a set radius.

Angles can’t be interpolated like abnormal numbers. Crossing from pi to adverse pi could ship an everyday linear interpolation across the longer arc. dampAngle first wraps the distinction into the vary from adverse pi to pi, then applies exponential smoothing.

operate createRingLightFollower() {
  const defaultLight = { x: 4, y: 9 }
  const radius = Math.hypot(defaultLight.x, defaultLight.y)
  const defaultAngle = Math.atan2(defaultLight.y, defaultLight.x)
  let targetAngle = defaultAngle
  let currentAngle = defaultAngle

  const dampAngle = (present: quantity, goal: quantity, lambda: quantity, dt: quantity) => {
    const shortest = Math.atan2(
      Math.sin(goal - present),
      Math.cos(goal - present),
    )
    return present + shortest * (1 - Math.exp(-lambda * dt))
  }

  // mappedX / mappedY come from raycasting pointer UV onto the mannequin aircraft.
  return (mappedX: quantity, mappedY: quantity, inside: boolean, delta: quantity) => {
    if (inside && mappedX * mappedX + mappedY * mappedY > 1e-6) {
      targetAngle = Math.atan2(mappedY, mappedX)
    } else if (!inside) {
      targetAngle = defaultAngle
    }

    currentAngle = dampAngle(currentAngle, targetAngle, 6, delta)
    return {
      x: radius * Math.cos(currentAngle),
      y: radius * Math.sin(currentAngle),
    }
  }
}

The spotlight nonetheless responds to the pointer’s path, however it could not drift onto the entrance face of the glass.

Coloured glass for each mild and darkish modes

I needed the coloured glass to maintain a way of thickness towards a light-weight background, whereas remaining vivid and saturated sufficient in darkish mode. For the sunshine theme, the tint takes its cue from the Beer-Lambert legislation, extra strictly written as T = I / I0 = 10^(-epsilon cl) (IUPAC’s Beer-Lambert legislation entry). The positioning doesn’t simulate an actual spectrum or an entire optical path. As a substitute, an RGB tint represents the colour that survives transmission, and pow(tint, thickness) acts as an approximation of transmittance.

That very same operation regarded too dim towards a darkish background, so darkish mode makes use of Exhausting Mild to carry the colour. uDark strikes between the Beer-Lambert-inspired transmission and the art-directed mix. This isn’t one unified bodily mannequin. It’s a visible adjustment for 2 very totally different backgrounds.

The decreased instance under retains solely the 2 mixing paths. The manufacturing materials additionally blends two tints alongside the mannequin’s native Y coordinate, and estimates variation in thickness from the angle between the view path and the conventional.

uniform vec3 uTintColor;
uniform float uTintAmount;
uniform float uThickness;
uniform float uDark;

vec3 hardLight(vec3 base, vec3 mix) {
  vec3 low = 2.0 * base * mix;
  vec3 excessive = 1.0 - 2.0 * (1.0 - base) * (1.0 - mix);
  return combine(low, excessive, step(vec3(0.5), mix));
}

vec3 applyGlassTint(vec3 colour) {
  vec3 tintColor = clamp(uTintColor, 0.001, 1.0);
  float quantity = clamp(uTintAmount, 0.0, 1.0);

  // Mild mode: Beer-Lambert-inspired absorption.
  vec3 transmittance = pow(tintColor, vec3(max(uThickness, 0.01)));
  vec3 beerColor = combine(colour, colour * transmittance, quantity);

  // Darkish mode: an art-directed Exhausting Mild tint.
  vec3 hardColor = combine(
    colour,
    hardLight(clamp(colour, 0.0, 1.0), tintColor),
    quantity
  );

  return combine(beerColor, hardColor, clamp(uDark, 0.0, 1.0));
}

The comparability demo reveals the tint in each themes, alongside the distinction between direct pointer monitoring and the ring-constrained mild.

Giving the refraction one thing to work with

As soon as the rim mild and tint had been in place, the glass nonetheless wanted colour and movement behind it earlier than its refraction and dispersion may turn out to be apparent. I drew a set of colourful stickers in Figma and positioned them behind the letters. They fall by a slender space that overlaps the phrase. A zOffset retains them contained in the scene sampled by the FBO, whereas the CPU updates particle movement and lifetimes.

Giving each sticker its personal mesh and texture would create a collection of draw calls and materials switches, and the FBO would repeat that price. As a substitute, all the PNGs are packed into one CanvasTexture and drawn with a single InstancedMesh. The atlas solely wants a uvRect and side ratio for every sticker. Whereas writing the UV rectangles, I flip the Y-axis between Canvas and WebGL and inset the bounds by half a pixel so linear filtering doesn’t choose up clear padding.

sort AtlasImage = CanvasImageSource & { width: quantity; peak: quantity }

operate drawAtlasEntry(
  ctx: CanvasRenderingContext2D,
  picture: AtlasImage,
  x: quantity,
  y: quantity,
  atlasWidth: quantity,
  atlasHeight: quantity,
) {
  ctx.drawImage(picture, x, y, picture.width, picture.peak)

  // Canvas is top-left. WebGL UV is bottom-left.
  // The half-pixel inset avoids sampling clear atlas padding.
  const uvRect = new THREE.Vector4(
    (x + 0.5) / atlasWidth,
    1 - (y + picture.peak - 0.5) / atlasHeight,
    (picture.width - 1) / atlasWidth,
    (picture.peak - 1) / atlasHeight,
  )

  return { uvRect, side: picture.width / picture.peak }
}

Every occasion represents one particle. Place, rotation, and scale go into instanceMatrix, whereas a customized occasion attribute shops the corresponding uvRect within the atlas. The vertex shader maps native UV to that sticker with uvRect.xy + uv * uvRect.zw.

// Write CPU particle state into GPU occasion attributes.
for (let i = 0; i < visibleCount; i++) {
  const particle = renderParticles[i]
  const side = atlas.elements[particle.textureIndex]
  const uvOffset = particle.textureIndex * 4
  const baseScale = config.scale * particle.scale

  matrixObject.place.copy(particle.place)
  matrixObject.rotation.set(0, 0, particle.rotation)
  matrixObject.scale.set(baseScale * side, baseScale, 1)
  matrixObject.updateMatrix()
  mesh.setMatrixAt(i, matrixObject.matrix)

  uvAttribute.setXYZW(
    i,
    atlas.uvRects[uvOffset],
    atlas.uvRects[uvOffset + 1],
    atlas.uvRects[uvOffset + 2],
    atlas.uvRects[uvOffset + 3],
  )
}

mesh.instanceMatrix.needsUpdate = true
uvAttribute.needsUpdate = true

Inside a set occasion finances, the sticker area now shares one texture, one materials, and one instanced draw. The transferring colour makes refraction and dispersion a lot simpler to learn. Right here, enhancing the scene behind the glass did greater than including one other spherical of shader complexity.

4. Ending with a Retro-Futurist Visible Language

With the glass centerpiece completed, I resisted including one other focus. As a substitute, I returned to the location’s smaller particulars and gave them a standard reference. The path was retro-futurism. A part of it comes from optical artifacts in stage footage, and half from the dot matrices and character suggestions of early digital interfaces. The previous makes the glass really feel extra filmed. The latter shapes picture adjustments, fullscreen transitions, and the best way textual content seems.

Making glass really feel filmed with a Star 6 filter

Refraction, dispersion, and the transferring stickers gave the glass colour and movement, however its highlights nonetheless regarded like a clear digital render. I stored fascinated about star filters in music movies and stage footage from the 80s and 90s. That led me so as to add the sample of a Star 6 filter to a customized lens flare move.

The move retains the intense core and coloured path of a lens flare. Three fastened axes add six rays across the highlights, making the glass really feel just a little extra like one thing filmed by a digicam.

First, a luminance threshold isolates vivid sources within the body. Most of them come from the glass specular. streak samples each instructions alongside one axis. A vertical axis and two axes at plus and minus 30 levels produce the six rays. This decreased model reveals the spotlight extraction and star sample. The manufacturing move provides additional management over scorching spots, path colour, and the circumstances that allow the impact.

float luma(vec3 colour) {
  return dot(colour, vec3(0.2126, 0.7152, 0.0722));
}

float brightMask(float luminance) {
  // Hold solely highlights above the configured threshold.
  float worth = max(luminance - uThreshold, 0.0);
  worth /= max(1.0 - uThreshold, 1e-5);
  return smoothstep(0.0, 1.0, clamp(worth, 0.0, 1.0));
}

vec3 sampleBright(vec2 uv) {
  vec3 colour = texture2D(tDiffuse, uv).rgb;
  return colour * brightMask(luma(colour));
}

vec3 streak(vec2 path) {
  vec3 outcome = vec3(0.0);

  // Pattern either side of 1 axis.
  for (int i = 1; i <= 8; i++) {
    float distancePx = float(i) * 1.5;
    float weight = 1.0 / (1.0 + distancePx * 0.22);
    weight *= weight;

    vec2 offset = path * distancePx;
    outcome += sampleBright(vUv + offset) * weight;
    outcome += sampleBright(vUv - offset) * weight;
  }

  return outcome;
}

vec3 base = texture2D(tDiffuse, vUv).rgb;
vec3 flare = base * brightMask(luma(base)) * 1.2;
vec2 px = (1.0 / uResolution) * uStreakScale;

// Three axes produce six rays.
flare += streak(vec2(0.0, px.y));
flare += streak(vec2(px.x * 0.8660254,  px.y * 0.5));
flare += streak(vec2(px.x * 0.8660254, -px.y * 0.5));

The star texture renders at half decision and refreshes each different body, then composites with the total scene every body. When the intense part containing the glass is exterior the viewport, your entire move stops. There isn’t a purpose to maintain paying for samples that can’t produce a visual outcome.

Dot matrices and character decoding as one system of suggestions

Star 6 establishes the optical character of the picture. The dot matrix carries the identical reference into interactions and transitions. A card hover divides the display into cells and adjustments photographs by rising a sq. in each. Loading, route adjustments, and the cellular menu use a radial masks, with the alpha of every cell controlling the radius of a circle. They don’t share one shader. What they share is a visible rule that turns steady progress into the dimensions of a form on a set grid. A state change feels associated whether or not it occurs inside one card or throughout the entire web page.

// Card hover: develop a sq. inside every screen-space cell.
vec2 cardCellUv = fract(screenUv / cellSize);
vec2 fromCenter = abs(cardCellUv - vec2(0.5));
float squareExtent = combine(0.0, 0.5, develop);
float squareDistance = max(fromCenter.x, fromCenter.y);
float squareAa = fwidth(squareDistance) * 1.5;
float squareMask = 1.0 - smoothstep(
  squareExtent - squareAa,
  squareExtent + squareAa,
  squareDistance
);

// Full-screen transition: use the radial masks to develop a circle per cell.
vec2 cellId = ground(uv / pixelSizeUv);
vec2 cellCenter = (cellId + vec2(0.5)) * pixelSizeUv;
float cellAlpha = radialMaskAlpha(cellCenter);
float radius = 0.8 * cellAlpha;
float circleDistance = distance(fract(uv / pixelSizeUv), vec2(0.5));
float circleAa = fwidth(circleDistance) * 1.5;
float circleMask = 1.0 - smoothstep(
  radius - circleAa,
  radius + circleAa,
  circleDistance
);

Textual content follows the identical path with out repeating the dot sample. ScrambleLines briefly cycles every character by capital letters, numbers, and symbols earlier than settling into the supposed copy, like a CLI decoding a message. All textual content cases share one 40ms ticker. They subscribe solely after getting into the viewport and after the fullscreen transition begins to open, then unsubscribe as quickly because the animation is finished.

Its geometry is totally different from the dot matrix, however each categorical steady change by discrete items. Star 6 provides the glass the optical hint of a digicam. Dots and decoded characters outline the interface response. The code is totally different, however the impression belongs to at least one web site. Materials, transition, and typography all level in the identical retro-futurist path.

Reflections

An important lesson from this challenge was to provide DOM and WebGL dependable scroll and pointer information earlier than deciding the place an impact ought to seem. CSS owns construction and accessibility. WebGL is available in when curl, refraction, or a transition can add one thing CSS would battle to precise. Within the completed web site, shared state and a constant visible rule mattered greater than the variety of results.

AI helped me check shader concepts, examine issues, and transfer by early implementations sooner. It didn’t make the ultimate choices. These nonetheless got here all the way down to design judgment. If I had been beginning once more, I might set the cellular efficiency finances and the shutoff circumstances for every impact a lot earlier.

After launch, the location grew to become my first challenge to obtain Awwwards Web site of the Day, FWA of the Day, and CSSDesignAwards Web site of the Day. Lenis additionally included it of their Showcase. I worth that recognition, however I’m proudest that the completed web site nonetheless carries the design intent and stage of craft I got down to obtain.

Credit

Tags: DOMHAOQI.DESIGNlettingRetroFuturistShareStageWebGL
Admin

Admin

Next Post
Retrospec Joe Rev 2 Overview (2026): Placing the ‘Pleasure’ in Joyride

Retrospec Joe Rev 2 Overview (2026): Placing the ‘Pleasure’ in Joyride

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Recommended.

Stolen iPhones disabled by Apple’s anti-theft tech after Los Angeles looting

Stolen iPhones disabled by Apple’s anti-theft tech after Los Angeles looting

June 16, 2025
RansomHub associates linked to rival RaaS suppliers

WinRAR zero-day exploited in espionage assaults in opposition to high-value targets

August 13, 2025

Trending.

The Full Information to EcoGPT

The Full Information to EcoGPT

June 6, 2026
Backrooms director Kane Parsons explains the birds, the portals, and his sensible results

Backrooms director Kane Parsons explains the birds, the portals, and his sensible results

May 31, 2026
Authorized DUI PPC Companies in Atlanta

Authorized DUI PPC Companies in Atlanta

June 14, 2026
Telegram ban in India sparks a rush to VPNs, rival apps

Telegram ban in India sparks a rush to VPNs, rival apps

June 19, 2026
Customers, Progress, and International Tendencies

Customers, Progress, and International Tendencies

March 18, 2026

AimactGrow

Welcome to AimactGrow, your ultimate source for all things technology! Our mission is to provide insightful, up-to-date content on the latest advancements in technology, coding, gaming, digital marketing, SEO, cybersecurity, and artificial intelligence (AI).

Categories

  • AI
  • Coding
  • Cybersecurity
  • Digital marketing
  • Gaming
  • SEO
  • Technology

Recent News

Rideshare Stimulator Outlines AI Utilization After Contentious Change With Ex-Author

Rideshare Stimulator Outlines AI Utilization After Contentious Change With Ex-Author

August 15, 2026
Retrospec Joe Rev 2 Overview (2026): Placing the ‘Pleasure’ in Joyride

Retrospec Joe Rev 2 Overview (2026): Placing the ‘Pleasure’ in Joyride

August 15, 2026
  • About Us
  • Privacy Policy
  • Disclaimer
  • Contact Us

© 2025 https://blog.aimactgrow.com/ - All Rights Reserved

No Result
View All Result
  • Home
  • Technology
  • AI
  • SEO
  • Coding
  • Gaming
  • Cybersecurity
  • Digital marketing

© 2025 https://blog.aimactgrow.com/ - All Rights Reserved