Skip to content

Commit

Permalink
Merge pull request #24 from thometnanni/keyframes
Browse files Browse the repository at this point in the history
keyframe functioning and video playback based on global time + interpolation + basic readme + removed old casestudy
  • Loading branch information
sinanatra authored Feb 25, 2025
2 parents f1b3b10 + 933655f commit e00a779
Show file tree
Hide file tree
Showing 15 changed files with 776 additions and 222,484 deletions.
387 changes: 128 additions & 259 deletions index.html

Large diffs are not rendered by default.

222,145 changes: 0 additions & 222,145 deletions public/airwars/buildings.geojson

This file was deleted.

19 changes: 0 additions & 19 deletions public/airwars/config.json

This file was deleted.

Binary file removed public/airwars/map.png
Binary file not shown.
1 change: 0 additions & 1 deletion public/airwars/scene.gltf

This file was deleted.

Binary file removed public/airwars/test_1.png
Binary file not shown.
Binary file removed public/airwars/test_2.png
Binary file not shown.
Binary file removed public/airwars/test_3.png
Binary file not shown.
Binary file removed public/airwars/test_4.png
Binary file not shown.
Binary file removed public/airwars/test_5.png
Binary file not shown.
48 changes: 48 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Vantage-renderer

Vantage-renderer is an open-source 3D projection renderer built with Three.js. It provides a modular framework for projecting media (images and videos) onto 3D environments using custom web components. Designed for fact-checkers, investigative journalists, and OSINT practitioners.


## Features

- `<vantage-renderer>`: Main container that sets up the Three.js scene, renderer, and camera controls.
- `<vantage-projection>`: Manages projections, texture/video loading, and keyframe updates.
- `<vantage-keyframe>`: Defines camera states (position, rotation, fov, etc.) at specific times.

## Installation

Install via npm:

`npm install vantage-renderer`

Or clone the repository:

```
git clone https://github.com/thometnanni/vantage-renderer.git
cd vantage-renderer
npm install
npm run dev
```

## Usage

Include the custom elements in your HTML:

```
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>vantage-renderer Example</title>
<script type="module" src="src/main.js"></script>
</head>
<body>
<vantage-renderer scene="path/to/scene.gltf" time="0" first-person="false" controls="edit">
<vantage-projection src="path/to/texture.mp4" focus="true">
<vantage-keyframe time="0" position="0 1.8 0" rotation="0 0 0" fov="60" far="150"></vantage-keyframe>
<vantage-keyframe time="10" position="10 1.8 0" rotation="0 0 0" fov="60" far="150"></vantage-keyframe>
</vantage-projection>
</vantage-renderer>
</body>
</html>
```
168 changes: 165 additions & 3 deletions src/Projection.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export default class Projection {
this.attributes = attributes

// this.updateLayerMeshes()
this.offset = parseFloat(element.getAttribute('time')) || 0

this.index = index
this.projectionType = projectionType
Expand Down Expand Up @@ -112,6 +113,7 @@ export default class Projection {

this.helper = new CameraHelper(this.camera)
this.#setHelperColor(0x00ff00)
this.helper.layers.set(2)
this.focus = focus
this.ready = true
}
Expand Down Expand Up @@ -297,15 +299,175 @@ export default class Projection {
this.plane.translateZ(-this.camera.far + this.camera.far * 0.001)
}

selectActiveKeyframe(globalTime) {
const offset = parseFloat(this.element.getAttribute('time')) || 0
const effectiveTime = globalTime - offset
const keyframeEls = Array.from(this.element.querySelectorAll('vantage-keyframe'))

const validKeyframes = keyframeEls.filter(
(kf) => parseFloat(kf.getAttribute('time')) <= effectiveTime
)
if (validKeyframes.length === 0) return keyframeEls[0]
return validKeyframes.reduce((prev, curr) =>
parseFloat(curr.getAttribute('time')) > parseFloat(prev.getAttribute('time')) ? curr : prev
)
}

hideProjection() {
if (this.plane) this.plane.visible = false
for (const layer in this.#layers) {
if (layer === 'vantage:screen') continue
if (this.material[layer]) this.material[layer].visible = false
}
}

updateMaterials() {
for (const layer in this.#layers) {
if (this.material[layer]) {
this.material[layer].visible = true
this.material[layer].project(this.#layers[layer])
}
}
}

getInterpolatedKeyframe = (globalTime) => {
const offset = parseFloat(this.element.getAttribute('time')) || 0
let effectiveTime = globalTime - offset
const keyframeEls = Array.from(this.element.querySelectorAll('vantage-keyframe'))
const sorted = keyframeEls.sort(
(a, b) => parseFloat(a.getAttribute('time')) - parseFloat(b.getAttribute('time'))
)

const firstKeyTime = parseFloat(sorted[0].getAttribute('time')) || 0
if (effectiveTime < firstKeyTime) effectiveTime = firstKeyTime

let active = null,
next = null
for (let i = 0; i < sorted.length; i++) {
const t = parseFloat(sorted[i].getAttribute('time')) || 0
if (t <= effectiveTime) {
active = sorted[i]
} else {
next = sorted[i]
break
}
}
if (!active) return null
if (!next) {
return {
position: active.getAttribute('position') || '0 0 0',
rotation: active.getAttribute('rotation') || '0 0 0',
fov: active.getAttribute('fov'),
far: active.getAttribute('far'),
opacity: active.getAttribute('opacity')
}
}
const activeTime = parseFloat(active.getAttribute('time')) || 0
const nextTime = parseFloat(next.getAttribute('time')) || 0
const ratio = (effectiveTime - activeTime) / (nextTime - activeTime)
const lerp = (a, b, t) => a + (b - a) * t
const lerpArray = (strA, strB) => {
const aArr = (strA || '0 0 0').split(' ').map(Number)
const bArr = (strB || '0 0 0').split(' ').map(Number)
return aArr.map((v, i) => lerp(v, bArr[i], ratio)).join(' ')
}
return {
position: lerpArray(active.getAttribute('position'), next.getAttribute('position')),
rotation: lerpArray(active.getAttribute('rotation'), next.getAttribute('rotation')),
fov: lerp(
parseFloat(active.getAttribute('fov')) || 0,
parseFloat(next.getAttribute('fov')) || 0,
ratio
),
far: lerp(
parseFloat(active.getAttribute('far')) || 0,
parseFloat(next.getAttribute('far')) || 0,
ratio
),
opacity: lerp(
parseFloat(active.getAttribute('opacity')) || 1,
parseFloat(next.getAttribute('opacity')) || 1,
ratio
)
}
}

updateCameraFromKeyframe = (data) => {
if (this.projectionType === 'map') {
let pos, rot
if (!data.position || data.position.trim() === '' || data.position.trim() === '0 0 0') {
pos = [0, 500, 0]
} else {
pos = data.position.split(' ').map(Number)
}

if (!data.rotation || data.rotation.trim() === '' || data.rotation.trim() === '0 0 0') {
rot = [-Math.PI / 2, -Math.PI / 2, 0]
} else {
rot = data.rotation.split(' ').map(Number)
}

this.camera.position.set(...pos)
this.camera.rotation.set(...rot)
this.camera.updateProjectionMatrix()
return
}

const pos = data.position.split(' ').map(Number)
const rot = data.rotation.split(' ').map(Number)
const fov = parseFloat(data.fov)
const far = parseFloat(data.far)
this.camera.position.set(...pos)
this.camera.rotation.set(...rot)
if (!isNaN(fov)) {
this.camera.fov = fov
this.camera.updateProjectionMatrix()
}
if (!isNaN(far)) {
this.camera.far = far
this.camera.updateProjectionMatrix()
}
}

update = () => {
if (!this.ready || this.scene.getObjectByName('vantage:base') == null) return
const rendererEl = this.element.closest('vantage-renderer')
const globalTime = rendererEl ? parseFloat(rendererEl.getAttribute('time')) : 0
const offset = parseFloat(this.element.getAttribute('time')) || 0
if (globalTime < offset) {
this.hideProjection()
return
}
const keyframeData = this.getInterpolatedKeyframe(globalTime)
if (!keyframeData) {
this.hideProjection()
return
}
this.updateCameraFromKeyframe(keyframeData)
this.updatePlane()
this.createDepthMap()
this.helper.update()
this.updateMaterials()
this.updateVideo(globalTime, { getAttribute: (attr) => keyframeData[attr] })
}

for (const layer in this.#layers) {
this.material[layer].project(this.#layers[layer])
}
// this needs to be fixed
updateVideo = (globalTime, keyframeObj) => {
if (
!this.texture ||
!this.texture.source ||
!(this.texture.source.data instanceof HTMLVideoElement)
)
return

const videoEl = this.texture.source.data
if (!videoEl.duration || videoEl.readyState < 2) return
videoEl.pause()

const offset = parseFloat(this.element.getAttribute('time')) || 0
let videoTime = globalTime - offset
if (videoTime < 0) videoTime = 0
videoEl.currentTime = videoTime
}

createLayers() {
Expand Down
42 changes: 34 additions & 8 deletions src/cameraOperator.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { MapControls } from 'three/addons/controls/MapControls'
import { PointerLockControls } from './CustomPointerLockControls'
import { DragControls } from 'three/addons/controls/DragControls.js'
import { getSelectedKeyframe } from './utils'

export default class CameraOperator extends EventDispatcher {
mapCamera = new PerspectiveCamera(60, innerWidth / innerHeight, 1, 10000)
Expand Down Expand Up @@ -155,6 +156,7 @@ export default class CameraOperator extends EventDispatcher {
keydown = ({ code }) => {
if (!this.controls) return
// if (this.mapControls.enabled) return;
if (this.firstPerson && !this.fpControls.enabled) return

switch (code) {
case 'KeyF':
Expand Down Expand Up @@ -233,17 +235,28 @@ export default class CameraOperator extends EventDispatcher {

this.fpControls.detachCamera()

const rendererEl = document.querySelector('vantage-renderer')
if (!rendererEl) return
const focusedProjection = Array.from(rendererEl.querySelectorAll('vantage-projection')).find(
(p) => p.hasAttribute('focus') && p.getAttribute('focus') !== 'false'
)
if (!focusedProjection) return

const keyframe = getSelectedKeyframe(focusedProjection)
if (!keyframe) return
keyframe.setAttribute('fov', this.#focusCamera.fov)
this.dispatchEvent({
type: 'vantage:update-fov',
value: this.#focusCamera.fov
})
}

createFocusMarker() {
const geom = new SphereGeometry(3, 16, 16)
const geom = new SphereGeometry(1, 16, 16)
const mat = new MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 1.0 })
this.focusMarker = new Mesh(geom, mat)
this.focusMarker.name = 'FocusMarker'
this.focusMarker.layers.set(2)
this.scene.add(this.focusMarker)
this.focusMarker.visible = false
}
Expand Down Expand Up @@ -294,6 +307,8 @@ export default class CameraOperator extends EventDispatcher {
this.dragControls = null
}
this.dragControls = new DragControls([this.focusMarker], this.mapCamera, this.domElement)
this.dragControls.raycaster.layers.enable(2)

this.dragControls.addEventListener('dragstart', () => {
if (this.mapControls) {
this.mapControls.enabled = false
Expand All @@ -312,13 +327,24 @@ export default class CameraOperator extends EventDispatcher {
const plane = new Plane(new Vector3(0, 1, 0), -focusProjection.camera.position.y)
const intersection = new Vector3()
raycaster.ray.intersectPlane(plane, intersection)
focusProjection.element.setAttribute('position', [...intersection].join(' '))
focusProjection.element.dispatchEvent(
new CustomEvent('vantage:set-position', {
bubbles: true,
detail: { position: [...intersection] }
})
)

const rendererEl = focusProjection.element.closest('vantage-renderer')
const globalTime = rendererEl ? parseFloat(rendererEl.getAttribute('time')) : 0
const activeKeyframe = focusProjection.element.selectActiveKeyframe(globalTime)
if (activeKeyframe) {
activeKeyframe.setAttribute(
'position',
`${intersection.x} ${intersection.y} ${intersection.z}`
)

activeKeyframe.dispatchEvent(
new CustomEvent('vantage:set-position', {
bubbles: true,
detail: { position: [...intersection] }
})
)
}

})
}
}
Loading

0 comments on commit e00a779

Please sign in to comment.