Skip to content

Commit 5c29a48

Browse files
committed
Add a trail
1 parent 00ca416 commit 5c29a48

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

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

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ class MapEngine implements MapHandle {
142142
y: 0,
143143
hasValue: false,
144144
}
145+
private readonly pointerTrail = new Float32Array(192)
146+
private trailCount = 0
145147
private readonly onActiveMarkerChange?: (id: string | null) => void
146148
private destroyed = false
147149

@@ -172,6 +174,8 @@ class MapEngine implements MapHandle {
172174

173175
this.fullscreenVAO = this.gl.createVertexArray() as WebGLVertexArrayObject
174176
this.uploadMarkerUniforms()
177+
this.pointerTrail.fill(-1)
178+
this.trailCount = 0
175179

176180
this.resizeObserver = new ResizeObserver(() => this.resizeCanvas())
177181
this.resizeObserver.observe(this.canvas)
@@ -366,6 +370,62 @@ class MapEngine implements MapHandle {
366370
this.onActiveMarkerChange(id)
367371
}
368372

373+
private updatePointerTrail(px: number, py: number) {
374+
const now = performance.now() * 0.001
375+
const maxGap = this.cellSize * this.pixelRatio * 1.5
376+
377+
if (this.trailCount === 0) {
378+
const idx = 0
379+
this.pointerTrail[idx] = px
380+
this.pointerTrail[idx + 1] = py
381+
this.pointerTrail[idx + 2] = now
382+
this.trailCount = 1
383+
return
384+
}
385+
386+
const lastIdx = 0
387+
const lastX = this.pointerTrail[lastIdx]
388+
const lastY = this.pointerTrail[lastIdx + 1]
389+
const lastTime = this.pointerTrail[lastIdx + 2]
390+
const dx = px - lastX
391+
const dy = py - lastY
392+
const dist = Math.sqrt(dx * dx + dy * dy)
393+
394+
if (dist <= maxGap) {
395+
this.addTrailPoint(px, py, now)
396+
} else {
397+
const steps = Math.ceil(dist / maxGap)
398+
const stepSize = 1.0 / steps
399+
const timeStep = (now - lastTime) / steps
400+
401+
for (let i = 0; i < steps; i++) {
402+
const t = (i + 1) * stepSize
403+
const interpX = lastX + dx * t
404+
const interpY = lastY + dy * t
405+
const interpTime = lastTime + timeStep * (i + 1)
406+
this.addTrailPoint(interpX, interpY, interpTime)
407+
}
408+
}
409+
}
410+
411+
private addTrailPoint(px: number, py: number, time: number) {
412+
if (this.trailCount >= 64) {
413+
for (let i = 189; i >= 3; i -= 3) {
414+
this.pointerTrail[i] = this.pointerTrail[i - 3]
415+
this.pointerTrail[i - 1] = this.pointerTrail[i - 4]
416+
this.pointerTrail[i - 2] = this.pointerTrail[i - 5]
417+
}
418+
} else {
419+
for (let i = this.trailCount * 3 - 1; i >= 0; i--) {
420+
this.pointerTrail[i + 3] = this.pointerTrail[i]
421+
}
422+
this.trailCount++
423+
}
424+
this.pointerTrail[0] = px
425+
this.pointerTrail[1] = py
426+
this.pointerTrail[2] = time
427+
}
428+
369429
private attachEvents() {
370430
this.canvas.style.cursor = "default"
371431
this.canvas.addEventListener("pointerdown", this.handlePointerDown)
@@ -419,6 +479,10 @@ class MapEngine implements MapHandle {
419479
this.hoverPointer.x = event.clientX
420480
this.hoverPointer.y = event.clientY
421481
this.hoverPointer.hasValue = true
482+
const devicePos = this.pointerToDevice(event.clientX, event.clientY)
483+
if (devicePos) {
484+
this.updatePointerTrail(devicePos[0], devicePos[1])
485+
}
422486
if (!this.pointer.active) {
423487
this.updateHoveredMarkerFromClient(event.clientX, event.clientY)
424488
}
@@ -457,6 +521,8 @@ class MapEngine implements MapHandle {
457521
if (event.type === "pointerleave" || event.type === "pointercancel") {
458522
this.hoverPointer.hasValue = false
459523
this.notifyHoverChange(-1)
524+
this.pointerTrail.fill(-1)
525+
this.trailCount = 0
460526
}
461527
if (!this.pointer.active || event.pointerId !== this.pointer.id) return
462528
this.pointer.active = false
@@ -596,11 +662,35 @@ class MapEngine implements MapHandle {
596662
this.fps = this.fps * 0.9 + instantaneous * 0.1
597663
this.applyInertia(dt)
598664
this.updateActiveMarkers(dt)
665+
this.removeCooledTrailPositions()
599666
this.render()
600667
this.loop()
601668
})
602669
}
603670

671+
private removeCooledTrailPositions() {
672+
if (this.trailCount === 0) return
673+
const now = performance.now() * 0.001
674+
const newestTime = this.pointerTrail[2]
675+
const maxAge = 2.0
676+
const threshold = newestTime - maxAge
677+
678+
let writeIdx = 0
679+
for (let i = 0; i < this.trailCount; i++) {
680+
const idx = i * 3
681+
const time = this.pointerTrail[idx + 2]
682+
if (time >= threshold) {
683+
if (writeIdx !== i) {
684+
this.pointerTrail[writeIdx * 3] = this.pointerTrail[idx]
685+
this.pointerTrail[writeIdx * 3 + 1] = this.pointerTrail[idx + 1]
686+
this.pointerTrail[writeIdx * 3 + 2] = this.pointerTrail[idx + 2]
687+
}
688+
writeIdx++
689+
}
690+
}
691+
this.trailCount = writeIdx
692+
}
693+
604694
private render() {
605695
const gl = this.gl
606696
const deviceRatio = getDevicePixelRatio()
@@ -663,6 +753,15 @@ class MapEngine implements MapHandle {
663753
)
664754
setUniform1f(gl, this.dotsProgram, "uHaloMinOpacity", this.haloMinOpacity)
665755
setUniform1i(gl, this.dotsProgram, "uMarkerCount", this.markerCount)
756+
setUniform1f(gl, this.dotsProgram, "uTime", performance.now() * 0.001)
757+
const trailLocation = gl.getUniformLocation(
758+
this.dotsProgram,
759+
"uPointerTrail",
760+
)
761+
if (trailLocation) {
762+
gl.uniform3fv(trailLocation, this.pointerTrail)
763+
}
764+
setUniform1i(gl, this.dotsProgram, "uTrailCount", this.trailCount)
666765
gl.activeTexture(gl.TEXTURE0)
667766
gl.bindTexture(gl.TEXTURE_2D, this.landTexture)
668767
setUniform1i(gl, this.dotsProgram, "uLand", 0)

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ uniform int uMarkerCount;
3333
uniform vec3 uMarkerColor;
3434
uniform vec3 uHaloColor;
3535
uniform float uHaloMinOpacity;
36+
uniform vec3 uPointerTrail[64];
37+
uniform int uTrailCount;
38+
uniform float uTime;
3639
3740
vec2 markerCellCenter(vec4 marker, vec2 referencePx) {
3841
float periodX = uWorldSize.x * uZoom;
@@ -121,6 +124,37 @@ void main() {
121124
color = uMarkerColor;
122125
}
123126
float squareHalf = baseHalfSquare;
127+
if (markerType <= 0.5) {
128+
float maxDecrease = 0.0;
129+
float oldestTime = 0.0;
130+
float newestTime = uTime;
131+
if (uTrailCount > 0) {
132+
oldestTime = uPointerTrail[uTrailCount - 1].z;
133+
newestTime = uPointerTrail[0].z;
134+
}
135+
float timeRange = max(newestTime - oldestTime, 0.001);
136+
for (int i = 0; i < 64; i++) {
137+
if (i >= uTrailCount) {
138+
break;
139+
}
140+
vec3 trailPoint = uPointerTrail[i];
141+
if (trailPoint.x <= 0.0 && trailPoint.y <= 0.0) {
142+
continue;
143+
}
144+
vec2 trailPos = trailPoint.xy;
145+
float dist = length(center - trailPos);
146+
float age = (uTime - trailPoint.z) / timeRange;
147+
age = clamp(age, 0.0, 1.0);
148+
float positionInTrail = float(i) / max(float(uTrailCount - 1), 1.0);
149+
float widthFactor = 1.0 - positionInTrail;
150+
float coolingFactor = pow(1.0 - age, 2.5);
151+
float combinedFactor = widthFactor * coolingFactor;
152+
float cellDist = dist / uCell;
153+
float decrease = 3.0 * combinedFactor * exp(-cellDist * 0.8);
154+
maxDecrease = max(maxDecrease, decrease);
155+
}
156+
squareHalf = max(0.0, squareHalf - maxDecrease);
157+
}
124158
vec2 delta = abs(fragPx - center);
125159
bool insideSquare = delta.x <= squareHalf && delta.y <= squareHalf;
126160
bool isSeaCell = markerType <= 0.5 && landCoverage < 0.5;
-10 Bytes
Loading

0 commit comments

Comments
 (0)