Skip to content

Commit 79bec73

Browse files
committed
draft halo effect
1 parent 7619e4f commit 79bec73

File tree

2 files changed

+153
-0
lines changed

2 files changed

+153
-0
lines changed

src/app/(main)/community/events/map/engine.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ const MIN_ZOOM = 1
4747
const MAX_ZOOM = 20
4848
const MARKER_TYPE_REGULAR = 1
4949
const MARKER_TYPE_HUB = 2
50+
const POINTER_TRAIL_CAPACITY = 8
51+
const POINTER_TRAIL_LIFETIME_MS = 480
52+
const POINTER_TRAIL_MIN_DISTANCE = 6
5053
/**
5154
* Per-frame damping factor (scaled by dt / (1/60s)).
5255
* Decrease value to increase damping.
@@ -87,6 +90,12 @@ type InternalOptions = BootOptions & {
8790
landTexture: WebGLTexture
8891
}
8992

93+
type PointerTrailEntry = {
94+
x: number
95+
y: number
96+
time: number
97+
}
98+
9099
class MapEngine implements MapHandle {
91100
private gl: WebGL2RenderingContext
92101
private canvas: HTMLCanvasElement
@@ -133,6 +142,8 @@ class MapEngine implements MapHandle {
133142
y: 0,
134143
hasValue: false,
135144
}
145+
private pointerTrail: PointerTrailEntry[] = []
146+
private pointerTrailBuffer = new Float32Array(POINTER_TRAIL_CAPACITY * 4)
136147
private destroyed = false
137148

138149
constructor(options: InternalOptions) {
@@ -242,6 +253,45 @@ class MapEngine implements MapHandle {
242253
return count
243254
}
244255

256+
private pointerToDevice(clientX: number, clientY: number) {
257+
const rect = this.canvas.getBoundingClientRect()
258+
if (
259+
clientX < rect.left ||
260+
clientX > rect.right ||
261+
clientY < rect.top ||
262+
clientY > rect.bottom
263+
) {
264+
return null
265+
}
266+
const relativeX = clientX - rect.left
267+
const relativeY = clientY - rect.top
268+
const px = relativeX * this.pixelRatio
269+
const py = this.canvas.height - relativeY * this.pixelRatio
270+
return [px, py] as const
271+
}
272+
273+
private pushPointerTrail(px: number, py: number, time: number) {
274+
const last = this.pointerTrail[this.pointerTrail.length - 1]
275+
if (last) {
276+
const dx = last.x - px
277+
const dy = last.y - py
278+
const distanceSq = dx * dx + dy * dy
279+
if (
280+
distanceSq <
281+
POINTER_TRAIL_MIN_DISTANCE * POINTER_TRAIL_MIN_DISTANCE
282+
) {
283+
last.x = px
284+
last.y = py
285+
last.time = time
286+
return
287+
}
288+
}
289+
if (this.pointerTrail.length >= POINTER_TRAIL_CAPACITY) {
290+
this.pointerTrail.shift()
291+
}
292+
this.pointerTrail.push({ x: px, y: py, time })
293+
}
294+
245295
private attachEvents() {
246296
this.canvas.style.cursor = "default"
247297
this.canvas.addEventListener("pointerdown", this.handlePointerDown)
@@ -294,6 +344,14 @@ class MapEngine implements MapHandle {
294344
this.hoverPointer.x = event.clientX
295345
this.hoverPointer.y = event.clientY
296346
this.hoverPointer.hasValue = true
347+
const pointerPosition = this.pointerToDevice(event.clientX, event.clientY)
348+
if (pointerPosition) {
349+
this.pushPointerTrail(
350+
pointerPosition[0],
351+
pointerPosition[1],
352+
performance.now(),
353+
)
354+
}
297355
if (!this.pointer.active || event.pointerId !== this.pointer.id) return
298356
const scale = this.pixelRatio
299357
const dx = (event.clientX - this.pointer.startX) * scale
@@ -326,6 +384,9 @@ class MapEngine implements MapHandle {
326384
}
327385

328386
private handlePointerUp = (event: PointerEvent) => {
387+
if (event.type === "pointerleave" || event.type === "pointercancel") {
388+
this.hoverPointer.hasValue = false
389+
}
329390
if (!this.pointer.active || event.pointerId !== this.pointer.id) return
330391
this.pointer.active = false
331392
this.canvas.releasePointerCapture(event.pointerId)
@@ -490,6 +551,49 @@ class MapEngine implements MapHandle {
490551
const panY = this.pan[1]
491552
const deviceCell = this.cellSize * this.pixelRatio
492553
const deviceSquare = this.squareSize * this.pixelRatio
554+
let pointerActive = 0
555+
let pointerCenterX = 0
556+
let pointerCenterY = 0
557+
const pointerTrailBuffer = this.pointerTrailBuffer
558+
pointerTrailBuffer.fill(0)
559+
let pointerTrailCount = 0
560+
const now = performance.now()
561+
if (this.hoverPointer.hasValue && deviceCell > 0) {
562+
const pointerDevice = this.pointerToDevice(
563+
this.hoverPointer.x,
564+
this.hoverPointer.y,
565+
)
566+
if (pointerDevice) {
567+
pointerActive = 1
568+
const pointerPx = pointerDevice[0]
569+
const pointerPy = pointerDevice[1]
570+
const cellX = Math.floor(pointerPx / deviceCell)
571+
const cellY = Math.floor(pointerPy / deviceCell)
572+
pointerCenterX = (cellX + 0.5) * deviceCell
573+
pointerCenterY = (cellY + 0.5) * deviceCell
574+
this.pushPointerTrail(pointerPx, pointerPy, now)
575+
}
576+
}
577+
const nextTrail: PointerTrailEntry[] = []
578+
for (let i = this.pointerTrail.length - 1; i >= 0; i--) {
579+
const entry = this.pointerTrail[i]
580+
const ageMs = now - entry.time
581+
if (ageMs > POINTER_TRAIL_LIFETIME_MS) {
582+
continue
583+
}
584+
if (pointerTrailCount >= POINTER_TRAIL_CAPACITY) {
585+
break
586+
}
587+
const age = ageMs / POINTER_TRAIL_LIFETIME_MS
588+
const base = pointerTrailCount * 4
589+
pointerTrailBuffer[base + 0] = entry.x
590+
pointerTrailBuffer[base + 1] = entry.y
591+
pointerTrailBuffer[base + 2] = age
592+
pointerTrailBuffer[base + 3] = 0
593+
pointerTrailCount++
594+
nextTrail.unshift(entry)
595+
}
596+
this.pointerTrail = nextTrail
493597

494598
gl.useProgram(this.dotsProgram)
495599
gl.bindVertexArray(this.fullscreenVAO)
@@ -524,6 +628,22 @@ class MapEngine implements MapHandle {
524628
this.hubMarkerColor[2],
525629
)
526630
setUniform1i(gl, this.dotsProgram, "uMarkerCount", this.markerCount)
631+
setUniform1i(gl, this.dotsProgram, "uPointerActive", pointerActive)
632+
setUniform2f(
633+
gl,
634+
this.dotsProgram,
635+
"uPointerCenter",
636+
pointerCenterX,
637+
pointerCenterY,
638+
)
639+
setUniform1i(gl, this.dotsProgram, "uPointerTrailCount", pointerTrailCount)
640+
const pointerTrailLocation = gl.getUniformLocation(
641+
this.dotsProgram,
642+
"uPointerTrail",
643+
)
644+
if (pointerTrailLocation) {
645+
gl.uniform4fv(pointerTrailLocation, pointerTrailBuffer)
646+
}
527647
gl.activeTexture(gl.TEXTURE0)
528648
gl.bindTexture(gl.TEXTURE_2D, this.landTexture)
529649
setUniform1i(gl, this.dotsProgram, "uLand", 0)

src/app/(main)/community/events/map/shaders.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ uniform vec4 uMarkers[${MARKER_CAPACITY}];
3131
uniform int uMarkerCount;
3232
uniform vec3 uMarkerColor;
3333
uniform vec3 uHubMarkerColor;
34+
uniform int uPointerActive;
35+
uniform vec2 uPointerCenter;
36+
uniform int uPointerTrailCount;
37+
uniform vec4 uPointerTrail[8];
3438
3539
float horizontalDelta(float markerX, float cellX) {
3640
float diff = abs(markerX - cellX);
@@ -98,6 +102,35 @@ void main() {
98102
} else if (markerType > 0.5) {
99103
color = uMarkerColor;
100104
}
105+
float pointerHalo = 0.0;
106+
if (uPointerActive > 0 && markerType > 0.5) {
107+
vec2 pointerDelta = abs(center - uPointerCenter);
108+
if (pointerDelta.x < 0.5 * uCell && pointerDelta.y < 0.5 * uCell) {
109+
float haloRadius = 0.5 * uSquare + 0.75 * uCell;
110+
float haloDist = length(fragPx - uPointerCenter);
111+
float haloFactor = clamp(1.0 - haloDist / haloRadius, 0.0, 1.0);
112+
pointerHalo = haloFactor * haloFactor;
113+
}
114+
}
115+
float pointerTrail = 0.0;
116+
for (int i = 0; i < 8; i++) {
117+
if (i >= uPointerTrailCount) {
118+
break;
119+
}
120+
vec4 entry = uPointerTrail[i];
121+
vec2 trailPos = entry.xy;
122+
float age = clamp(entry.z, 0.0, 1.0);
123+
float fade = 1.0 - age;
124+
float dist = length(center - trailPos);
125+
float influence = max(0.0, 1.0 - dist / (uCell * 3.0));
126+
pointerTrail = max(pointerTrail, fade * influence);
127+
}
128+
if (pointerTrail > 0.0) {
129+
color = mix(color, vec3(1.0), pointerTrail * 0.3);
130+
}
131+
if (pointerHalo > 0.0) {
132+
color = mix(color, vec3(1.0), pointerHalo * 0.7);
133+
}
101134
outColor = vec4(color, 1.0);
102135
}
103136
`

0 commit comments

Comments
 (0)