diff --git a/src/components/TxRender.svelte b/src/components/TxRender.svelte
index cc3c6d0..6afed29 100644
--- a/src/components/TxRender.svelte
+++ b/src/components/TxRender.svelte
@@ -21,15 +21,15 @@
export let running = false
// Shader attributes
+ // each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
+ // shader interpolates between start and end values at the given rate, from the given time
const attribs = {
- startTime: { type: 'FLOAT', count: 1, pointer: null },
- zIndex: { type: 'FLOAT', count: 1, pointer: null },
- speed: { type: 'FLOAT', count: 1, pointer: null },
- positions: { type: 'FLOAT', count: 4, pointer: null },
- sizes: { type: 'FLOAT', count: 2, pointer: null },
- palettes: { type: 'FLOAT', count: 2, pointer: null },
- colors: { type: 'FLOAT', count: 2, pointer: null },
- alphas: { type: 'FLOAT', count: 2, pointer: null }
+ posX: { type: 'FLOAT', count: 4, pointer: null },
+ posY: { type: 'FLOAT', count: 4, pointer: null },
+ sizes: { type: 'FLOAT', count: 4, pointer: null },
+ palettes: { type: 'FLOAT', count: 4, pointer: null },
+ colors: { type: 'FLOAT', count: 4, pointer: null },
+ alphas: { type: 'FLOAT', count: 4, pointer: null }
}
// Auto-calculate the number of bytes per vertex based on specified attributes
const stride = Object.values(attribs).reduce((total, attrib) => {
@@ -58,9 +58,10 @@
function getTxPointArray () {
if (controller) {
- return new Float32Array(
- controller.getScenes().flatMap(scene => scene.getVertexData())
- )
+ return controller.getVertexData()
+ // return new Float32Array(
+ // controller.getScenes().flatMap(scene => scene.getVertexData())
+ // )
} else return []
}
@@ -136,7 +137,9 @@
})
/* DRAW */
- gl.drawArrays(gl.POINTS, 0, pointArray.length / 3)
+ if (pointArray.length) {
+ gl.drawArrays(gl.POINTS, 0, pointArray.length / 3)
+ }
/* LOOP */
window.requestAnimationFrame(currentTime => {
@@ -214,6 +217,8 @@
colorTexture = loadColorTexture(gl, '#f7941d', 'rgb(0%,100%,80%)', 500);
running = true
+
+ console.log(this)
})
@@ -222,9 +227,9 @@
position: absolute;
left: 0;
right: 0;
- top: -5px;
+ top: 0;
bottom: 0;
- pointer-events: none;
+ /* pointer-events: none; */
overflow: hidden;
}
diff --git a/src/components/TxViz.svelte b/src/components/TxViz.svelte
index 03a60cf..d846ba4 100644
--- a/src/components/TxViz.svelte
+++ b/src/components/TxViz.svelte
@@ -3,13 +3,12 @@
import TxController from '../controllers/TxController.js'
import TxRender from './TxRender.svelte'
import getTxStream from '../controllers/TxStream.js'
- import { darkMode, serverConnected, serverDelay, txQueueLength } from '../stores.js'
+ import { darkMode, serverConnected, serverDelay, txQueueLength, txCount } from '../stores.js'
import BitcoinBlock from '../models/BitcoinBlock.js'
let width = window.innerWidth
let height = window.innerHeight
let txController
- let txCount = 0
let blockCount = 0
let running = false
let txStream = getTxStream()
@@ -53,6 +52,10 @@
// }))
}
+ function fakeTx () {
+ txController.simulateDumpTx(1)
+ }
+
function fakeTxs () {
txController.simulateDumpTx(200)
}
@@ -107,6 +110,10 @@
position: absolute;
top: 20px;
right: 20px;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ justify-content: flex-end;
.status-light {
display: block;
@@ -124,6 +131,20 @@
background: greenyellow;
}
}
+
+ .stat-counter {
+ margin-top: 5px;
+
+ &.red {
+ color: red;
+ }
+ &.amber {
+ color: yellow;
+ }
+ &.green {
+ color: greenyellow;
+ }
+ }
}
@@ -134,12 +155,14 @@
+
-
{ $txQueueLength }
+
{ $txQueueLength }
+
{ $txCount }
diff --git a/src/controllers/TxController.js b/src/controllers/TxController.js
index 544e8bf..d8fee46 100644
--- a/src/controllers/TxController.js
+++ b/src/controllers/TxController.js
@@ -2,13 +2,15 @@ import TxPoolScene from '../models/TxPoolScene.js'
import TxBlockScene from '../models/TxBlockScene.js'
import BitcoinTx from '../models/BitcoinTx.js'
import BitcoinBlock from '../models/BitcoinBlock.js'
+import { FastVertexArray } from '../utils/memory.js'
import { txQueueLength } from '../stores.js'
export default class TxController {
constructor ({ width, height }) {
+ this.vertexArray = new FastVertexArray(2048, 24)
this.txs = {}
this.expiredTxs = {}
- this.pool = new TxPoolScene({ width, height, layer: 0.0 })
+ this.pool = new TxPoolScene({ width, height, layer: 0.0, controller: this })
this.blocks = {}
this.clearBlockTimeout = null
@@ -19,6 +21,10 @@ export default class TxController {
this.scheduleQueue(1000)
}
+ getVertexData () {
+ return this.vertexArray.getVertexData()
+ }
+
getScenes () {
return [this.pool, ...Object.values(this.blocks)]
}
@@ -30,7 +36,8 @@ export default class TxController {
})
}
- addTx (tx) {
+ addTx (txData) {
+ const tx = new BitcoinTx(txData, this.vertexArray)
if (!this.txs[tx.id] && !this.expiredTxs[tx.id]) {
this.pendingTxs.push([tx, Date.now()])
txQueueLength.increment()
@@ -77,7 +84,8 @@ export default class TxController {
}, delay)
}
- addBlock (block) {
+ addBlock (blockData) {
+ const block = new BitcoinBlock(blockData)
if (this.clearBlockTimeout) clearTimeout(this.clearBlockTimeout)
this.expiredTxs = {}
@@ -86,7 +94,7 @@ export default class TxController {
if (!this.blocks[blockId].expired) this.clearBlock(blockId)
})
- this.blocks[block.id] = new TxBlockScene({ width: 500, height: 500, layer: 1.0 })
+ this.blocks[block.id] = new TxBlockScene({ width: 500, height: 500, layer: 1.0, blockId: block.id, controller: this })
let knownCount = 0
let unknownCount = 0
for (let i = 0; i < block.txns.length; i++) {
@@ -99,14 +107,14 @@ export default class TxController {
const tx = new BitcoinTx({
...block.txns[i],
block: block.id
- })
+ }, this.vertexArray)
this.txs[tx.id] = tx
this.blocks[block.id].insert(this.txs[tx.id], false)
}
this.expiredTxs[block.txns[i].id] = true
}
console.log(`New block with ${knownCount} known transactions and ${unknownCount} unknown transactions`)
- this.blocks[block.id].layoutAll()
+ this.blocks[block.id].initialLayout()
setTimeout(() => { this.pool.layoutAll() }, 2000)
this.clearBlockTimeout = setTimeout(() => { this.clearBlock(block.id) }, 10000)
@@ -129,7 +137,7 @@ export default class TxController {
// }
// })
const simulatedTxns = []
- Object.values(this.txs).forEach(tx => {
+ Object.values(this.pool.txs).forEach(tx => {
if (Math.random() < 0.5) {
simulatedTxns.push({
version: tx.version,
@@ -150,7 +158,7 @@ export default class TxController {
txn_count: 20,
txns: simulatedTxns
}))
- }, 2500)
+ }, 0)
}
simulateDumpTx (n) {
@@ -160,22 +168,25 @@ export default class TxController {
time: Date.now(),
id: `simulated_${i}_${Math.random()}`,
value: Math.floor(Math.random() * 100000)
- }))
+ }, this.vertexArray))
}
}
clearBlock (id) {
if (this.blocks[id]) {
this.blocks[id].expire()
- setTimeout(() => {
- const txs = this.blocks[id].getTxList()
- for (let i = 0; i < txs.length; i++) {
- if (this.blocks[id].remove(txs[i])) {
- delete this.txs[txs[i]]
- }
- }
- delete this.blocks[id]
- }, 3000)
}
}
+
+ destroyTx (id) {
+ this.getScenes().forEach(scene => {
+ scene.remove(id)
+ })
+ if (this.txs[id]) this.txs[id].destroy()
+ delete this.txs[id]
+ }
+
+ destroyBlock (id) {
+ if (this.blocks) delete this.blocks[id]
+ }
}
diff --git a/src/controllers/TxStream.js b/src/controllers/TxStream.js
index d4243fd..71ee3b6 100644
--- a/src/controllers/TxStream.js
+++ b/src/controllers/TxStream.js
@@ -1,5 +1,3 @@
-import BitcoinTx from '../models/BitcoinTx.js'
-import BitcoinBlock from '../models/BitcoinBlock.js'
import { serverConnected, serverDelay } from '../stores.js'
class TxStream {
@@ -88,12 +86,10 @@ class TxStream {
} else {
const msg = JSON.parse(event.data)
if (msg && msg.type === 'txn') {
- const tx = new BitcoinTx(msg.txn)
- window.dispatchEvent(new CustomEvent('bitcoin_tx', { detail: tx }))
+ window.dispatchEvent(new CustomEvent('bitcoin_tx', { detail: msg.txn }))
} else if (msg && msg.type === 'block') {
- const block = new BitcoinBlock(msg.block)
- console.log('Block recieved: ', block)
- window.dispatchEvent(new CustomEvent('bitcoin_block', { detail: block }))
+ console.log('Block recieved: ', msg.block)
+ window.dispatchEvent(new CustomEvent('bitcoin_block', { detail: msg.block }))
} else {
console.log('unknown message from websocket: ', msg)
}
diff --git a/src/models/BitcoinTx.js b/src/models/BitcoinTx.js
index 5694df2..6e4bd69 100644
--- a/src/models/BitcoinTx.js
+++ b/src/models/BitcoinTx.js
@@ -1,9 +1,10 @@
import TxView from './TxView.js'
export default class BitcoinTx {
- constructor ({ version, id, value, inputs, outputs, witnesses, time, block }) {
+ constructor ({ version, id, value, inputs, outputs, witnesses, time, block }, vertexArray) {
this.version = version
this.id = id
+ this.vertexArray = vertexArray
if (inputs && outputs) {
this.inputs = inputs
@@ -20,6 +21,10 @@ export default class BitcoinTx {
this.view = new TxView(this)
}
+ destroy () {
+ if (this.view) this.view.destroy()
+ }
+
calcValue () {
if (this.outputs && this.outputs.length) {
return this.outputs.reduce((acc, output) => {
@@ -37,7 +42,11 @@ export default class BitcoinTx {
if (this.view) this.view.update(update)
}
+ setPosition (position) {
+ this.position = position
+ }
+
getPosition () {
- if (this.view) return this.view.getPosition()
+ if (this.position) return this.position
}
}
diff --git a/src/models/TxBlockScene.js b/src/models/TxBlockScene.js
index 3890b4a..9504f3a 100644
--- a/src/models/TxBlockScene.js
+++ b/src/models/TxBlockScene.js
@@ -1,10 +1,12 @@
import TxPoolScene from './TxPoolScene.js'
export default class TxBlockScene extends TxPoolScene {
- constructor ({ width, height, unit = 4, padding = 1, layer }) {
- super({ width, height, unit, padding, layer })
+ constructor ({ width, height, unit = 4, padding = 1, layer, blockId, controller }) {
+ super({ width, height, unit, padding, layer, controller })
this.heightLimit = null
this.expired = false
+ this.layedOut = false
+ this.blockId = blockId
}
resize ({ width, height, unit, padding }) {
@@ -48,7 +50,8 @@ export default class TxBlockScene extends TxPoolScene {
state: 'ready'
})
}
- const flyToBlock = {
+
+ this.updateTx(tx, {
display: {
position,
size: 4,
@@ -60,33 +63,44 @@ export default class TxBlockScene extends TxPoolScene {
},
duration: 1500,
delay: 0,
- state: 'flytoblock',
- next: {
- display: {},
- state: 'block'
- }
- }
- if (tx.view.state === 'pool' || tx.view.state === 'colorfade') {
+ state: 'block'
+ })
+ }
+
+ prepareTx (tx, sequence) {
+ const position = this.place(tx.id, sequence)
+ if (!tx.view.initialised) {
this.updateTx(tx, {
display: {
layer: this.layer,
+ position: {
+ x: window.innerWidth * ((position.x - this.scene.offset.x) / this.width),
+ y: window.innerHeight * (1 + ((position.y - this.scene.offset.y) / this.height))
+ },
+ size: 8,
color: {
palette: 0,
index: 0,
alpha: 1
}
},
- duration: 1000,
+ duration: 1500,
delay: 0,
- state: 'colorfade',
- next: flyToBlock
- })
- } else {
- this.updateTx(tx, {
- ...flyToBlock,
- duration: 2500
+ state: 'ready'
})
}
+ this.updateTx(tx, {
+ display: {
+ layer: this.layer,
+ color: {
+ palette: 0,
+ index: 0,
+ alpha: 1
+ }
+ },
+ duration: 2000,
+ delay: 0
+ })
}
expireTx (tx) {
@@ -103,9 +117,30 @@ export default class TxBlockScene extends TxPoolScene {
})
}
+ prepareAll () {
+ this.resize({})
+ this.scene.count = 0
+ let ids = this.getHiddenTxList()
+ for (let i = 0; i < ids.length; i++) {
+ this.txs[ids[i]] = this.hiddenTxs[ids[i]]
+ delete this.hiddenTxs[ids[i]]
+ }
+ ids = this.getActiveTxList()
+ for (let i = 0; i < ids.length; i++) {
+ this.prepareTx(this.txs[ids[i]], this.scene.count++)
+ }
+ }
+
layoutAll () {
this.resize({})
- super.layoutAll(4)
+ super.layoutAll()
+ }
+
+ initialLayout () {
+ this.prepareAll()
+ setTimeout(() => {
+ this.layoutAll()
+ }, 2000)
}
expire () {
@@ -114,5 +149,12 @@ export default class TxBlockScene extends TxPoolScene {
for (let i = 0; i < ids.length; i++) {
this.expireTx(this.txs[ids[i]])
}
+ setTimeout(() => {
+ const txIds = this.getTxList()
+ for (let i = 0; i < txIds.length; i++) {
+ if (this.txs[txIds[i]]) this.controller.destroyTx(txIds[i])
+ }
+ this.controller.destroyBlock(this.blockId)
+ }, 3000)
}
}
diff --git a/src/models/TxPoolScene.js b/src/models/TxPoolScene.js
index a1ced3d..9b9611d 100644
--- a/src/models/TxPoolScene.js
+++ b/src/models/TxPoolScene.js
@@ -1,14 +1,20 @@
export default class TxPoolScene {
- constructor ({ width, height, unit, padding, layer }) {
- this.init({ width, height, unit, padding, layer })
+ constructor ({ width, height, unit, padding, layer, controller }) {
+ this.init({ width, height, unit, padding, layer, controller })
}
- init ({ width, height, unit = 8, padding = 1, layer }) {
+ init ({ width, height, unit = 8, padding = 1, layer, controller }) {
+ this.controller = controller
this.layer = layer
this.resize({ width, height, unit, padding })
this.txs = {}
this.hiddenTxs = {}
- this.heightLimit = this.height - 50
+
+ this.heightLimit = Math.max(150, height / 4)
+ this.heightBound = this.height - this.heightLimit
+ this.poolTop = this.height
+ this.poolBottom = this.height
+
this.scene = {
width: width,
height: height,
@@ -21,6 +27,8 @@ export default class TxPoolScene {
}
this.scrollRateLimitTimer = null
this.initialised = true
+
+ console.log('pool', this)
}
resize ({ width, height, unit, padding }) {
@@ -37,14 +45,8 @@ export default class TxPoolScene {
if (tx) tx.updateView(update)
}
- scroll (offset) {
- if (!this.scrollRateLimitTimer || Date.now() < (this.scrollRateLimitTimer + 10000)) {
- console.log(`scrolling pool by ${offset}`)
- this.scrollRateLimitTimer = Date.now()
- this.doScroll(offset)
- } else {
- console.log('scroll recharging')
- }
+ getPoolHeight () {
+ return this.poolBottom - this.poolTop
}
insert (tx, autoLayout=true) {
@@ -56,36 +58,81 @@ export default class TxPoolScene {
}
}
+ clearOffscreenTx (tx) {
+ const currentTargetPosition = tx.getPosition()
+ if (currentTargetPosition && (currentTargetPosition.y + this.scene.scroll) > this.height + 150) {
+ this.controller.destroyTx(tx.id)
+ }
+ }
+
+ clearOffscreenTxs () {
+ if (this.poolBottom + this.scene.scroll > (this.height + 150)) {
+ const ids = this.getTxList()
+ for (let i = 0; i < ids.length; i++) {
+ this.clearOffscreenTx(this.txs[ids[i]])
+ }
+ }
+ }
+
scrollTx (tx, scrollDistance) {
if (tx.view.initialised) {
- let currentPosition = tx.getPosition()
- this.updateTx(tx, {
- display: {
- position: {
- y: currentPosition.y + scrollDistance
- }
- }
- })
+ let currentTargetPosition = tx.getPosition()
+ if (currentTargetPosition) {
+ this.updateTx(tx, {
+ display: {
+ position: {
+ y: currentTargetPosition.y + scrollDistance
+ }
+ },
+ duration: 500,
+ minDuration: 250,
+ adjust: true
+ })
+ }
}
}
doScroll (offset) {
- const ids = this.getActiveTxList()
- for (let i = 0; i < ids.length; i++) {
- this.scrollTx(this.txs[ids[i]], offset)
- }
+ const ids = this.getTxList()
this.scene.scroll -= offset
+ for (let i = 0; i < ids.length; i++) {
+ this.scrollTx(this.txs[ids[i]], this.scene.scroll)
+ }
+ this.clearOffscreenTxs()
+ }
+
+ scroll (offset, force) {
+ if (!this.scrollRateLimitTimer || force || Date.now() > (this.scrollRateLimitTimer + 1000)) {
+ this.scrollRateLimitTimer = Date.now()
+ this.doScroll(offset)
+ }
+ }
+
+ txSize (value) {
+ // let scale = Math.log10(value)
+ // let size = (scale*scale) / 5
+ // let rounded = Math.pow(2, Math.ceil(Math.log2(size)))
+ // return Math.max(4, rounded)
+ return this.unitWidth
}
layoutTx (tx, sequence) {
- const position = this.place(tx.id, sequence)
- // if (this.heightLimit && position.y < this.heightLimit) this.scroll(position.y - this.heightLimit)
+ const rawPosition = this.place(tx.id, sequence)
+ const scrolledPosition = {
+ x: rawPosition.x,
+ y: rawPosition.y + this.scene.scroll
+ }
+ tx.setPosition(rawPosition)
+ if (this.heightLimit && scrolledPosition.y < this.heightBound) {
+ this.scroll(scrolledPosition.y - this.heightBound)
+ scrolledPosition.y = rawPosition.y + this.scene.scroll
+ }
if (!tx.view.initialised) {
this.updateTx(tx, {
display: {
layer: this.layer,
position: {
- x: position.x,
+ x: scrolledPosition.x,
y: 0
},
size: this.unitWidth,
@@ -102,8 +149,9 @@ export default class TxPoolScene {
this.updateTx(tx, {
display: {
layer: this.layer,
- position,
- size: this.unitWidth,
+ position: scrolledPosition,
+ // size: this.unitWidth,
+ size: this.txSize(tx.value),
color: {
palette: 0,
index: 0,
@@ -112,60 +160,35 @@ export default class TxPoolScene {
},
duration: 1500,
delay: 0,
- state: 'entering',
- next: {
- display: {
- color: {
- palette: 0,
- index: 1
- }
- },
- duration: 30000,
- delay: 100,
- state: 'colorfade',
- next: {
- display: {},
- state: 'pool'
+ state: 'pool'
+ })
+ this.updateTx(tx, {
+ display: {
+ color: {
+ palette: 0,
+ index: 1
}
- }
+ },
+ duration: 30000,
+ delay: 0
})
} else {
- const shuffleUpdate = {
+ this.updateTx(tx, {
display: {
- position
+ position: scrolledPosition
},
duration: 1000,
+ minDuration: 1000,
delay: 0,
- state: 'shuffling',
- next: {
- display: {
- color: {
- index: 1
- }
- },
- state: 'colorfade',
- duration: 15000,
- delay: 100
- }
- }
- // if (tx.view.state === 'pool' || tx.view.state === 'colorfade') {
- // this.updateTx(tx, {
- // display: {
- // layer: this.layer
- // },
- // duration: 2000,
- // delay: 0,
- // state: 'pause',
- // next: shuffleUpdate
- // })
- // } else {
- this.updateTx(tx, shuffleUpdate)
- // }
+ adjust: true
+ })
}
}
layoutAll () {
this.scene.count = 0
+ this.poolTop = Infinity
+ this.poolBottom = -Infinity
let ids = this.getHiddenTxList()
for (let i = 0; i < ids.length; i++) {
this.txs[ids[i]] = this.hiddenTxs[ids[i]]
@@ -175,6 +198,14 @@ export default class TxPoolScene {
for (let i = 0; i < ids.length; i++) {
this.layoutTx(this.txs[ids[i]], this.scene.count++)
}
+
+ if (this.heightLimit && ((this.poolTop + this.scene.scroll) < this.heightBound)) {
+ let scrollAmount = (this.poolTop + this.scene.scroll) - this.heightBound
+ this.scroll(scrollAmount, true)
+ } else if (this.heightLimit && ((this.poolTop + this.scene.scroll) > this.heightBound)) {
+ let scrollAmount = Math.min(this.scene.scroll, (this.poolTop + this.scene.scroll) - this.heightBound)
+ this.scroll(scrollAmount, true)
+ }
}
remove (id) {
@@ -201,10 +232,16 @@ export default class TxPoolScene {
}
place (id, position) {
- return {
+ const placement = {
x: this.scene.offset.x + (this.paddedUnitWidth * (1 + Math.floor(position % this.blockWidth))),
- y: this.scene.offset.y + this.height - (this.paddedUnitWidth * (0.5 + (Math.floor(position / this.blockWidth)))) + this.scene.scroll
+ y: this.scene.offset.y + this.height - (this.paddedUnitWidth * (1 + (Math.floor(position / this.blockWidth))))
}
+ if (placement.y < this.poolTop) {
+ this.poolTop = placement.y
+ } else if (placement.y > this.poolBottom) {
+ this.poolBottom = placement.y
+ }
+ return placement
}
getVertexData () {
diff --git a/src/models/TxSprite.js b/src/models/TxSprite.js
index 2aca5ac..244d557 100644
--- a/src/models/TxSprite.js
+++ b/src/models/TxSprite.js
@@ -1,105 +1,132 @@
-function interpolateAnimationState (from, to, progress) {
- const clampedProgress = Math.max(0,Math.min(1,progress))
- return Object.keys(from).reduce((result, key) => {
- if (to[key] != null) {
- result[key] = from[key] + ((to[key] - from[key]) * clampedProgress)
- }
- return result
- }, from)
+function interpolateAttributeStart(attribute, now, label) {
+ if (attribute.v == 0 || (attribute.t + attribute.d) <= now) {
+ // transition finished, next transition starts from current end state
+ // (clamp to 1)
+ attribute.a = attribute.b
+ attribute.v = 0
+ attribute.d = 0
+ } else if (attribute.t > now) {
+ // transition not started
+ // (clamp to 0)
+ } else {
+ // transition in progress
+ // (interpolate)
+ let progress = (now - attribute.t)
+ attribute.a = attribute.a + ((progress / attribute.d) * (attribute.b - attribute.a))
+ attribute.d = attribute.d - progress
+ attribute.v = 1 / attribute.d
+ }
}
export default class TxSprite {
- constructor({ now, id, value, layer, position, size, palette, color, alpha }) {
+ constructor({ now = Date.now(), id, value, layer, position, size, palette, color, alpha }, vertexArray) {
this.id = id
this.value = value
this.layer = layer
+ this.vertexArray = vertexArray
- this.duration = 0
- this.v = 0
- this.start = now
+ this.attributes = {
+ x: { a: position.x, b: position.x, t: now, v: 0, d: 0 },
+ y: { a: position.y, b: position.y, t: now, v: 0, d: 0 },
+ r: { a: size, b: size, t: now, v: 0, d: 0 },
+ p: { a: palette, b: palette, t: now, v: 0, d: 0 },
+ c: { a: color, b: color, t: now, v: 0, d: 0 },
+ a: { a: alpha, b: alpha, t: now, v: 0, d: 0 },
+ }
- this.from = {
- x: position.x,
- y: position.y,
+ this.vertexPointer = this.vertexArray.insert(this)
+
+ this.compile()
+ }
+
+ update({ now, layer, position, size, palette, color, alpha, duration, minDuration, adjust }) {
+ const v = duration > 0 ? (1 / duration) : 0
+
+ let update = {
+ x: position ? position.x : null,
+ y: position ? position.y: null,
r: size,
p: palette,
c: color,
a: alpha
}
- this.to = {
- ...this.from
- }
- this.compile()
- }
-
- update({ now, duration, layer, position, size, palette, color, alpha }) {
- // Save a copy of the current target display
- const currentTarget = this.to
-
- // Check if we're mid-transition
- const progress = (this.duration && this.start) ? ((now - this.start) / this.duration) : 0
- if (progress >= 1 || progress <= 0) {
- // Transition finished or hasn't started:
- // so next transition starts from the current display state
- this.from = {
- ...currentTarget
+ for (const key of Object.keys(update)) {
+ // for each non-null attribute:
+ if (update[key] != null) {
+ // calculate current interpolated value, and set as 'from'
+ interpolateAttributeStart(this.attributes[key], now, key)
+ // set 'start' to now
+ this.attributes[key].t = now
+ // if 'adjust' flag set
+ // set 'duration' to Max(remaining time, 'duration')
+ if (!adjust || (duration && this.attributes[key].d == 0)) {
+ this.attributes[key].v = v
+ this.attributes[key].d = duration
+ } else if (minDuration > this.attributes[key].d) {
+ this.attributes[key].v = 1 / minDuration
+ this.attributes[key].d = minDuration
+ }
+ // set 'to' to target value
+ this.attributes[key].b = update[key]
}
- } else {
- // Mid-transition:
- // we want to start the next transition from our current intermediate state
- // so find that by interpolating between the last transitions start and end states
- this.from = interpolateAnimationState(this.from, currentTarget, progress)
}
- // Build new target display, inheriting from the current target where necessary
- this.to = {
- x: (position && position.x != null) ? position.x : this.from.x,
- y: (position && position.y != null) ? position.y : this.from.y,
- r: (size != null) ? size : this.from.r,
- p: (palette != null) ? palette : this.from.p,
- c: (color != null) ? color : this.from.c,
- a: (alpha != null) ? alpha : this.from.a
- }
-
- if (layer != null) this.layer = layer
- if (duration != null) {
- this.duration = duration
- this.v = duration ? (1 / duration) : 0
- }
- this.start = now
-
this.compile()
}
compile () {
this.vertexData = [
- this.start,
- this.layer,
- this.v,
- this.from.x,
- this.from.y,
- this.to.x,
- this.to.y,
- this.from.r,
- this.to.r,
- this.from.p,
- this.to.p,
- this.from.c,
- this.to.c,
- this.from.a,
- this.to.a,
+ this.attributes.x.a,
+ this.attributes.x.b,
+ this.attributes.x.t,
+ this.attributes.x.v,
+
+ this.attributes.y.a,
+ this.attributes.y.b,
+ this.attributes.y.t,
+ this.attributes.y.v,
+
+ this.attributes.r.a,
+ this.attributes.r.b,
+ this.attributes.r.t,
+ this.attributes.r.v,
+
+ this.attributes.p.a,
+ this.attributes.p.b,
+ this.attributes.p.t,
+ this.attributes.p.v,
+
+ this.attributes.c.a,
+ this.attributes.c.b,
+ this.attributes.c.t,
+ this.attributes.c.v,
+
+ this.attributes.a.a,
+ this.attributes.a.b,
+ this.attributes.a.t,
+ this.attributes.a.v,
]
+ this.vertexArray.setData(this.vertexPointer, this.vertexData)
+ }
+
+ moveVertexPointer (index) {
+ this.vertexPointer = index
}
getPosition () {
return {
- x: this.to.x,
- y: this.to.y
+ x: this.attributes.x.b,
+ y: this.attributes.y.b
}
}
getVertexData () {
return this.vertexData
}
+
+ destroy () {
+ this.vertexArray.remove(this.vertexPointer)
+ this.vertexPointer = null
+ }
}
diff --git a/src/models/TxView.js b/src/models/TxView.js
index 7f0bd5e..a3ee6df 100644
--- a/src/models/TxView.js
+++ b/src/models/TxView.js
@@ -2,21 +2,28 @@ import TxSprite from './TxSprite.js'
import { timeOffset } from '../utils/time.js'
// converts from this class's update format to TxSprite's update format
-function toSpriteUpdate(display, duration, start) {
+// now, id, value, layer, position, size, palette, color, alpha, duration, adjust
+function toSpriteUpdate(display, duration, start, adjust) {
return {
now: start ? start - timeOffset : Date.now() - timeOffset,
- duration,
+ duration: duration,
...display,
- ...(display.color ? { palette: display.color.palette, color: display.color.index, alpha: display.color.alpha } : { color: null })
+ ...(display.color ? { palette: display.color.palette, color: display.color.index, alpha: display.color.alpha } : { color: null }),
+ adjust
}
}
export default class TxView {
- constructor ({ id, time, value }) {
+ constructor ({ id, time, value, vertexArray }) {
this.id = id
this.time = time
this.value = value
this.initialised = false
+ this.vertexArray = vertexArray
+ }
+
+ destroy () {
+ if (this.sprite) this.sprite.destroy()
}
/*
@@ -30,28 +37,18 @@ export default class TxView {
duration: of the tweening animation from the previous display state
delay: for queued transitions, how long to wait after current transition
completes to start.
- next: another transition to animate after this one completes
- (you can nest several transitions like this)
- Performing another direct update cancels any pending transitions
*/
- update ({ display, duration, delay, state, next, start }) {
- if (next) {
- if (this.awaiting) clearTimeout(this.awaiting)
- this.awaiting = setTimeout(() => {
- this.update(next)
- }, (duration || 0) + (next.delay || 0))
- }
-
+ update ({ display, duration, delay, state, start, adjust }) {
this.state = state
- if (!this.initialised) {
+ if (!this.initialised || !this.sprite) {
this.initialised = true
const update = toSpriteUpdate(display, duration, start)
update.id = this.id
update.value = this.value
- this.sprite = new TxSprite(update)
+ this.sprite = new TxSprite(update, this.vertexArray)
} else {
- const update = toSpriteUpdate(display, duration, start)
+ const update = toSpriteUpdate(display, duration, start, adjust)
this.sprite.update(update)
}
}
diff --git a/src/shaders/tx.vert b/src/shaders/tx.vert
index 5a27f27..72f21a0 100644
--- a/src/shaders/tx.vert
+++ b/src/shaders/tx.vert
@@ -1,17 +1,16 @@
varying lowp vec4 vColor;
-attribute float startTime;
-attribute float zIndex;
-attribute float speed;
-attribute vec4 positions;
-attribute vec2 sizes;
-attribute vec2 colors;
-attribute vec2 palettes;
-attribute vec2 alphas;
+// each attribute contains [x: startValue, y: endValue, z: startTime, w: rate]
+// shader interpolates between start and end values at the given rate, from the given time
+attribute vec4 posX;
+attribute vec4 posY;
+attribute vec4 sizes;
+attribute vec4 colors;
+attribute vec4 palettes;
+attribute vec4 alphas;
uniform vec2 screenSize;
uniform float now;
-// uniform float opacityTarget; // target opacity for fading points
uniform sampler2D colorTexture;
vec3 selectPalette(float index) {
@@ -22,17 +21,20 @@ vec3 selectPalette(float index) {
}
}
+float interpolateAttribute(vec4 attr) {
+ float delta = clamp((now - attr.z) * attr.w, 0.0, 1.0);
+ return mix(attr.x, attr.y, delta);
+}
+
void main() {
vec4 screenTransform = vec4(2.0 / screenSize.x, -2.0 / screenSize.y, -1.0, 1.0);
- float delta = clamp((now - startTime) * speed, 0.0, 1.0);
+ vec2 position = vec2(interpolateAttribute(posX), interpolateAttribute(posY));
+ gl_PointSize = interpolateAttribute(sizes);
+ gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, 1.0, 1.0);
- vec2 position = mix(positions.xy, positions.zw, delta);
- gl_PointSize = mix(sizes.x, sizes.y, delta);
- gl_Position = vec4(position * screenTransform.xy + screenTransform.zw, zIndex, 1.0);
-
- float colorIndex = mix(colors.x, colors.y, delta);
- float alpha = mix(alphas.x, alphas.y, delta);
+ float colorIndex = interpolateAttribute(colors);
+ float alpha = interpolateAttribute(alphas);
if (palettes.y < 1.0) { // texture color
vec4 texel = texture2D(colorTexture, vec2(colorIndex, 0.0));
vColor = vec4(texel.rgb, alpha);
diff --git a/src/stores.js b/src/stores.js
index dae3be9..3912858 100644
--- a/src/stores.js
+++ b/src/stores.js
@@ -296,3 +296,4 @@ function createCounter () {
}
export const txQueueLength = createCounter()
+export const txCount = createCounter()
diff --git a/src/utils/memory.js b/src/utils/memory.js
new file mode 100644
index 0000000..54a2b8b
--- /dev/null
+++ b/src/utils/memory.js
@@ -0,0 +1,112 @@
+import { txCount } from '../stores.js'
+
+/*
+ Utility class for access and management of low-level sprite data
+
+ Maintains a single Float32Array of sprite data, keeping track of empty slots
+ to allow constant-time insertion and deletion
+
+ Automatically resizes by copying to a new, larger Float32Array when necessary,
+ or compacting into a smaller Float32Array when there's space to do so.
+*/
+export class FastVertexArray {
+ constructor (length, stride) {
+ // console.log(`Creating Fast Vertex Array with length ${length} and stride ${stride} `)
+ this.length = length
+ this.count = 0
+ this.stride = stride
+ this.sprites = []
+ this.data = new Float32Array(this.length * this.stride)
+ this.freeSlots = []
+ this.lastSlot = 0
+ this.nullSprite = new Float32Array(this.stride)
+ // this.print()
+ }
+
+ print () {
+ // console.log(`Length: ${this.length}, Free slots: ${this.freeSlots.length}, last slot: ${this.lastSlot}`)
+ // console.log(this.freeSlots)
+ // console.log(this.data)
+ }
+
+ insert (sprite) {
+ // console.log('inserting into FVA')
+ this.count++
+ txCount.increment()
+
+ let position
+ if (this.freeSlots.length) {
+ position = this.freeSlots.shift()
+ } else {
+ position = this.lastSlot
+ this.lastSlot++
+ if (this.lastSlot > this.length) {
+ this.expand()
+ }
+ }
+ // this.print()
+ this.sprites[position] = sprite
+ return position
+ }
+
+ remove (index) {
+ this.count--
+ txCount.decrement()
+ this.setData(index, this.nullSprite)
+ this.freeSlots.push(index)
+ this.sprites[index] = null
+ if (this.length > 2048 && this.count < (this.length * 0.4)) this.compact()
+ // this.print()
+ }
+
+ setData (index, dataChunk) {
+ // console.log(`Updating chunk at ${index} (${index * this.stride})`)
+ this.data.set(dataChunk, (index * this.stride))
+ // this.print()
+ }
+
+ getData (index) {
+ return this.data.subarray(index, this.stride)
+ }
+
+ expand () {
+ // console.log('Expanding FVA')
+ this.length *= 2
+ const newData = new Float32Array(this.length * this.stride)
+ newData.set(this.data)
+ this.data = newData
+ this.print()
+ }
+
+ compact () {
+ // console.log('Compacting FVA')
+ // console.log(this.sprites)
+ // New array length is the smallest power of 2 larger than the sprite count
+ const newLength = Math.pow(2, Math.ceil(Math.log2(this.count)))
+ if (this.newLength != this.length) {
+ // console.log(`compacting from ${this.length} to ${newLength}`)
+ this.length = newLength
+ this.data = new Float32Array(this.length * this.stride)
+ let sprite
+ const newSprites = []
+ let i = 0
+ for (var index in this.sprites) {
+ sprite = this.sprites[index]
+ if (sprite) {
+ newSprites.push(sprite)
+ sprite.moveVertexPointer(i)
+ sprite.compile()
+ i++
+ }
+ }
+ this.sprites = newSprites
+ this.freeSlots = []
+ this.lastSlot = i
+ }
+ this.print()
+ }
+
+ getVertexData () {
+ return this.data
+ }
+}