Merge pull request #51 from bitfeed-project/smooth-transitions

Smoother, fancier animations
This commit is contained in:
Mononaut 2022-06-10 03:44:25 +01:00 committed by GitHub
commit 50fcd5cfa8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 279 additions and 124 deletions

View File

@ -1,7 +1,7 @@
<script> <script>
import analytics from '../utils/analytics.js' import analytics from '../utils/analytics.js'
import { fly } from 'svelte/transition' import { fly } from 'svelte/transition'
import { linear } from 'svelte/easing' import { smootherstep } from '../utils/easing.js'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import Icon from '../components/Icon.svelte' import Icon from '../components/Icon.svelte'
import closeIcon from '../assets/icon/cil-x-circle.svg' import closeIcon from '../assets/icon/cil-x-circle.svg'
@ -80,16 +80,16 @@
$: { $: {
if (!$blockTransitionDirection || !visible || !block || !$blocksEnabled) { if (!$blockTransitionDirection || !visible || !block || !$blocksEnabled) {
transitionDirection = 'up' transitionDirection = 'up'
flyIn = { y: (restoring ? -50 : 50), duration: (restoring ? 500 : 1000), easing: linear, delay: (restoring ? 0 : newBlockDelay) } flyIn = { y: (restoring ? -50 : 50), duration: (restoring ? 1500 : 1000), easing: smootherstep, delay: (restoring ? 0 : newBlockDelay) }
flyOut = { y: -50, duration: 2000, easing: linear } flyOut = { y: -50, duration: 1500, easing: smootherstep, delay: 0 }
} else if ($blockTransitionDirection && $blockTransitionDirection === 'right') { } else if ($blockTransitionDirection && $blockTransitionDirection === 'right') {
transitionDirection = 'right' transitionDirection = 'right'
flyIn = { x: 100, easing: linear, delay: 1000, duration: 1000 } flyIn = { x: 100, easing: smootherstep, delay: 1000, duration: 1000 }
flyOut = { x: -100, easing: linear, delay: 0, duration: 1000 } flyOut = { x: -100, easing: smootherstep, delay: 0, duration: 1000 }
} else if ($blockTransitionDirection && $blockTransitionDirection === 'left') { } else if ($blockTransitionDirection && $blockTransitionDirection === 'left') {
transitionDirection = 'left' transitionDirection = 'left'
flyIn = { x: -100, easing: linear, delay: 1000, duration: 1000 } flyIn = { x: -100, easing: smootherstep, delay: 1000, duration: 1000 }
flyOut = { x: 100, easing: linear, delay: 0, duration: 1000 } flyOut = { x: 100, easing: smootherstep, delay: 0, duration: 1000 }
} else { } else {
transitionDirection = 'down' transitionDirection = 'down'
} }

View File

@ -207,6 +207,7 @@ export default class TxController {
if (!this.explorerBlockScene) this.clearBlock() if (!this.explorerBlockScene) this.clearBlock()
this.poolScene.scrollLock = true
if (this.blocksEnabled) { if (this.blocksEnabled) {
this.blockScene = new TxBlockScene({ width: this.blockAreaSize, height: this.blockAreaSize, blockId: block.id, controller: this, colorMode: this.colorMode }) this.blockScene = new TxBlockScene({ width: this.blockAreaSize, height: this.blockAreaSize, blockId: block.id, controller: this, colorMode: this.colorMode })
let knownCount = 0 let knownCount = 0
@ -239,7 +240,6 @@ export default class TxController {
currentBlock.set(block) currentBlock.set(block)
} }
} else { } else {
this.poolScene.scrollLock = true
for (let i = 0; i < block.txns.length; i++) { for (let i = 0; i < block.txns.length; i++) {
if (this.txs[block.txns[i].id] && this.txs[block.txns[i].id].view) { if (this.txs[block.txns[i].id] && this.txs[block.txns[i].id].view) {
this.txs[block.txns[i].id].view.update({ this.txs[block.txns[i].id].view.update({
@ -304,7 +304,7 @@ export default class TxController {
enterFromRight = true enterFromRight = true
} }
else prevBlockScene.exitRight() else prevBlockScene.exitRight()
prevBlockScene.expire(2000) prevBlockScene.expire(3000)
} else if (this.blockScene) { } else if (this.blockScene) {
this.blockScene.exitRight() this.blockScene.exitRight()
} }
@ -343,7 +343,7 @@ export default class TxController {
const prevBlock = this.explorerBlock const prevBlock = this.explorerBlock
const prevBlockScene = this.explorerBlockScene const prevBlockScene = this.explorerBlockScene
prevBlockScene.exitLeft() prevBlockScene.exitLeft()
prevBlockScene.expire(2000) prevBlockScene.expire(3000)
this.explorerBlockScene = null this.explorerBlockScene = null
this.explorerBlock = null this.explorerBlock = null
urlPath.set("/") urlPath.set("/")
@ -384,6 +384,10 @@ export default class TxController {
scene.remove(id) scene.remove(id)
}) })
if (this.txs[id]) this.txs[id].destroy() if (this.txs[id]) this.txs[id].destroy()
this.deleteTx(id)
}
deleteTx (id) {
delete this.txs[id] delete this.txs[id]
} }

View File

@ -73,7 +73,7 @@ export default class BitcoinTx {
this.is_partial = !!partial this.is_partial = !!partial
this.is_preview = !!preview this.is_preview = !!preview
this.id = id this.id = id
this.pixelPosition = { x: 0, y: 0, r: 0} if (!this.pixelPosition) this.pixelPosition = { x: 0, y: 0, r: 0}
this.screenPosition = { x: 0, y: 0, r: 0} this.screenPosition = { x: 0, y: 0, r: 0}
this.gridPosition = { x: 0, y: 0, r: 0} this.gridPosition = { x: 0, y: 0, r: 0}
this.inputs = inputs this.inputs = inputs

View File

@ -1,6 +1,7 @@
import TxMondrianPoolScene from './TxMondrianPoolScene.js' import TxMondrianPoolScene from './TxMondrianPoolScene.js'
import { settings } from '../stores.js' import { settings } from '../stores.js'
import { logTxSize, byteTxSize } from '../utils/misc.js' import { logTxSize, byteTxSize } from '../utils/misc.js'
import { orange } from '../utils/color.js'
import config from '../config.js' import config from '../config.js'
let settingsValue let settingsValue
@ -62,6 +63,7 @@ export default class TxBlockScene extends TxMondrianPoolScene {
} }
setTxOnScreen (tx, pixelPosition) { setTxOnScreen (tx, pixelPosition) {
this.saveGridToPixelPosition(tx)
if (!tx.view.initialised) { if (!tx.view.initialised) {
tx.view.update({ tx.view.update({
display: { display: {
@ -95,14 +97,17 @@ export default class TxBlockScene extends TxMondrianPoolScene {
color: tx.getColor('block', this.colorMode).color color: tx.getColor('block', this.colorMode).color
}, },
duration: this.laidOut ? 1000 : 2000, duration: this.laidOut ? 1000 : 2000,
delay: 0, delay: 50,
jitter: this.laidOut ? 0 : 1500, jitter: this.laidOut ? 500 : 1500,
smooth: true,
state: 'block' state: 'block'
}) })
} }
} }
prepareTxOnScreen (tx) { prepareTxOnScreen (tx, now) {
const oldRadius = tx.pixelPosition.r
this.saveGridToPixelPosition(tx)
if (!tx.view.initialised) { if (!tx.view.initialised) {
tx.view.update({ tx.view.update({
display: { display: {
@ -119,21 +124,40 @@ export default class TxBlockScene extends TxMondrianPoolScene {
delay: 0, delay: 0,
state: 'ready' state: 'ready'
}) })
} else {
const jitter = (Math.random() * 1700)
tx.view.update({
display: {
position: {
r: oldRadius + Math.max(2, oldRadius * 0.2)
},
},
delay: 50 + jitter,
start: now,
duration: 750,
smooth: true,
boomerang: true
})
tx.view.update({
display: {
color: settingsValue.darkMode && false ? {
h: 0.8,
l: 1.0
} : orange,
},
start: now,
delay: 50 + jitter,
duration: 500,
})
} }
tx.view.update({
display: {
color: tx.getColor('block', this.colorMode).color
},
duration: 2000,
delay: 0
})
} }
prepareTx (tx, sequence) { prepareTx (tx, sequence) {
this.prepareTxOnScreen(tx, this.layoutTx(tx, sequence, 0, false)) this.place(tx)
this.prepareTxOnScreen(tx)
} }
enterTx (tx, right) { enterTx (tx, start, right) {
tx.view.update({ tx.view.update({
display: { display: {
position: { position: {
@ -157,8 +181,11 @@ export default class TxBlockScene extends TxMondrianPoolScene {
alpha: 1 alpha: 1
} }
}, },
start,
duration: 2000, duration: 2000,
delay: 0 delay: 100,
jitter: 500,
smooth: true,
}) })
} }
@ -166,8 +193,9 @@ export default class TxBlockScene extends TxMondrianPoolScene {
this.hidden = false this.hidden = false
this.exited = false this.exited = false
const ids = this.getActiveTxList() const ids = this.getActiveTxList()
const start = performance.now()
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
this.enterTx(this.txs[ids[i]], right) this.enterTx(this.txs[ids[i]], start, right)
} }
} }
@ -179,7 +207,7 @@ export default class TxBlockScene extends TxMondrianPoolScene {
this.enter(false) this.enter(false)
} }
exitTx (tx, right) { exitTx (tx, start, right) {
tx.view.update({ tx.view.update({
display: { display: {
position: { position: {
@ -192,8 +220,11 @@ export default class TxBlockScene extends TxMondrianPoolScene {
alpha: 0 alpha: 0
} }
}, },
delay: 0, delay: 100,
duration: 2000 start,
jitter: 500,
duration: 2000,
smooth: true,
}) })
} }
@ -201,8 +232,9 @@ export default class TxBlockScene extends TxMondrianPoolScene {
this.hidden = true this.hidden = true
this.exited = true this.exited = true
const ids = this.getActiveTxList() const ids = this.getActiveTxList()
const start = performance.now()
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
this.exitTx(this.txs[ids[i]], right) this.exitTx(this.txs[ids[i]], start, right)
} }
} }
@ -214,7 +246,7 @@ export default class TxBlockScene extends TxMondrianPoolScene {
this.exit(false) this.exit(false)
} }
hideTx (tx) { hideTx (tx, now) {
this.savePixelsToScreenPosition(tx) this.savePixelsToScreenPosition(tx)
tx.view.update({ tx.view.update({
display: { display: {
@ -225,13 +257,15 @@ export default class TxBlockScene extends TxMondrianPoolScene {
alpha: 0 alpha: 0
} }
}, },
duration: 2000, start: now,
delay: 0, duration: 1500,
state: 'fadeout' delay: 50,
state: 'fadeout',
smooth: true,
}) })
} }
showTx (tx) { showTx (tx, now) {
this.savePixelsToScreenPosition(tx) this.savePixelsToScreenPosition(tx)
tx.view.update({ tx.view.update({
display: { display: {
@ -242,13 +276,16 @@ export default class TxBlockScene extends TxMondrianPoolScene {
alpha: 1 alpha: 1
} }
}, },
duration: 500, start: now,
delay: 0, duration: 1500,
state: 'fadeout' delay: 50,
state: 'fadeout',
smooth: true,
}) })
} }
prepareAll () { prepareAll () {
const now = performance.now()
this.resize({}) this.resize({})
this.scene.count = 0 this.scene.count = 0
let ids = this.getHiddenTxList() let ids = this.getHiddenTxList()
@ -258,7 +295,7 @@ export default class TxBlockScene extends TxMondrianPoolScene {
} }
ids = this.getActiveTxList() ids = this.getActiveTxList()
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
this.prepareTx(this.txs[ids[i]], this.scene.count++) this.prepareTx(this.txs[ids[i]], now)
} }
} }
@ -277,30 +314,43 @@ export default class TxBlockScene extends TxMondrianPoolScene {
}, 3000) }, 3000)
} }
resetScroll () {
return
}
hide () { hide () {
this.hidden = true this.hidden = true
const now = performance.now()
const ids = this.getActiveTxList() const ids = this.getActiveTxList()
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
this.hideTx(this.txs[ids[i]]) this.hideTx(this.txs[ids[i]], now)
} }
} }
show () { show () {
if (this.hidden) { if (this.hidden) {
this.hidden = false this.hidden = false
const now = performance.now()
const ids = this.getActiveTxList() const ids = this.getActiveTxList()
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
this.showTx(this.txs[ids[i]]) this.showTx(this.txs[ids[i]], now)
} }
} }
} }
expire (delay=3000) { expire (delay=3000) {
this.expired = true this.expired = true
const txIds = this.getTxList()
for (let i = 0; i < txIds.length; i++) {
if (this.txs[txIds[i]]) {
this.controller.deleteTx(txIds[i])
}
}
setTimeout(() => { setTimeout(() => {
const txIds = this.getTxList()
for (let i = 0; i < txIds.length; i++) { for (let i = 0; i < txIds.length; i++) {
if (this.txs[txIds[i]]) this.controller.destroyTx(txIds[i]) if (this.txs[txIds[i]]) {
this.txs[txIds[i]].destroy()
}
} }
this.layout.destroy() this.layout.destroy()
}, delay) }, delay)

View File

@ -30,8 +30,8 @@ export default class TxMondrianPoolScene extends TxPoolScene {
else return logTxSize(tx.value, this.blockWidth) else return logTxSize(tx.value, this.blockWidth)
} }
place (tx, index, size) { place (tx) {
// console.log(`placing tx at ${index} (size ${size})`) const size = this.txSize(tx)
const position = this.layout.place(tx, size) const position = this.layout.place(tx, size)
tx.gridPosition.x = position.x tx.gridPosition.x = position.x
tx.gridPosition.y = position.y tx.gridPosition.y = position.y

View File

@ -22,6 +22,7 @@ export default class TxPoolScene {
y: 0 y: 0
} }
} }
this.lastScroll = performance.now()
this.resize({ width, height }) this.resize({ width, height })
this.txs = {} this.txs = {}
@ -79,15 +80,19 @@ export default class TxPoolScene {
insert (tx, insertDelay, autoLayout=true) { insert (tx, insertDelay, autoLayout=true) {
if (autoLayout) { if (autoLayout) {
this.layoutTx(tx, this.scene.count++, insertDelay)
this.txs[tx.id] = tx this.txs[tx.id] = tx
this.place(tx)
if (this.checkTxScroll(tx)) {
this.applyTxScroll(tx)
}
this.setTxOnScreen(tx, insertDelay)
} else { } else {
this.hiddenTxs[tx.id] = tx this.hiddenTxs[tx.id] = tx
} }
} }
clearOffscreenTx (tx) { clearOffscreenTx (tx) {
if (tx.pixelPosition && (tx.pixelPosition.y + tx.pixelPosition.r) < -(this.scene.offset.y + 20)) { if (tx.pixelPosition && (tx.pixelPosition.y + tx.pixelPosition.r) < -(this.scene.offset.y + 100)) {
this.controller.destroyTx(tx.id) this.controller.destroyTx(tx.id)
} }
} }
@ -97,6 +102,7 @@ export default class TxPoolScene {
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
this.clearOffscreenTx(this.txs[ids[i]]) this.clearOffscreenTx(this.txs[ids[i]])
} }
this.clearTimer = null
} }
redrawTx (tx, now) { redrawTx (tx, now) {
@ -108,37 +114,40 @@ export default class TxPoolScene {
position: tx.screenPosition position: tx.screenPosition
}, },
duration: 1000, duration: 1000,
minDuration: 500,
start: now, start: now,
minDuration: 250, delay: 50,
smooth: true,
adjust: true adjust: true
}) })
} }
} }
updateChunk (ids) { // updateChunk (ids, now = performance.now()) {
const now = performance.now() // for (let i = 0; i < ids.length; i++) {
for (let i = 0; i < ids.length; i++) { // this.redrawTx(this.txs[ids[i]], now)
this.redrawTx(this.txs[ids[i]], now) // }
} // }
}
async doScroll (offset) { async doScroll (offset) {
const ids = this.getTxList() const now = performance.now()
this.scene.scroll += offset if (now - this.lastScroll > 1000) {
const processingChunks = [] this.lastScroll = now
// Scrolling operation is potentially very costly, as we're calculating and updating positions of every active tx in the pool const ids = this.getTxList()
// So attempt to spread the cost over several frames, by separately processing chunks of 100 txs each this.scene.scroll += offset
// Schedule ~1 chunk per frame at the targeted framerate (60 fps, frame every 16.7ms) this.maxHeight += offset
for (let i = 0; i < ids.length; i+=100) { if (this.heightStore) this.heightStore.set(this.maxHeight)
processingChunks.push(new Promise((resolve, reject) => {
setTimeout(() => { for (let i = 0; i < ids.length; i++) {
this.updateChunk(ids.slice(i, i+100)) this.redrawTx(this.txs[ids[i]], now)
resolve() }
}, (i / 100) * 20)
})) if (!this.clearTimer) {
this.clearTimer = setTimeout(() => {
this.clearOffscreenTxs()
}, 1500)
}
} }
await Promise.all(processingChunks)
this.clearOffscreenTxs()
} }
scroll (offset, force) { scroll (offset, force) {
@ -159,9 +168,7 @@ export default class TxPoolScene {
return 1 return 1
} }
layoutTx (tx, sequence, insertDelay, setOnScreen = true) { checkTxScroll (tx, insertDelay, setOnScreen = true) {
const units = this.txSize(tx)
this.place(tx, sequence, units)
this.saveGridToPixelPosition(tx) this.saveGridToPixelPosition(tx)
const top = tx.pixelPosition.y + tx.pixelPosition.r const top = tx.pixelPosition.y + tx.pixelPosition.r
const bottom = tx.pixelPosition.y - tx.pixelPosition.r const bottom = tx.pixelPosition.y - tx.pixelPosition.r
@ -169,16 +176,17 @@ export default class TxPoolScene {
this.maxHeight = top this.maxHeight = top
if (this.heightStore) this.heightStore.set(this.maxHeight) if (this.heightStore) this.heightStore.set(this.maxHeight)
} }
if (this.heightLimit && bottom > this.heightLimit) { return (this.heightLimit && bottom > this.heightLimit)
this.scroll(this.heightLimit - bottom) }
this.maxHeight += (this.heightLimit - bottom)
if (this.heightStore) this.heightStore.set(this.maxHeight) applyTxScroll (tx) {
this.saveGridToPixelPosition(tx) const bottom = tx.pixelPosition.y - tx.pixelPosition.r
} this.scroll(this.heightLimit - bottom)
if (setOnScreen) this.setTxOnScreen(tx, insertDelay)
} }
setTxOnScreen (tx, insertDelay=0) { setTxOnScreen (tx, insertDelay=0) {
this.saveGridToPixelPosition(tx)
this.savePixelsToScreenPosition(tx)
if (!tx.view.initialised) { if (!tx.view.initialised) {
const txColor = tx.getColor(this.sceneType, this.colorMode) const txColor = tx.getColor(this.sceneType, this.colorMode)
tx.view.update({ tx.view.update({
@ -198,7 +206,7 @@ export default class TxPoolScene {
}) })
tx.view.update({ tx.view.update({
display: { display: {
position: this.pixelsToScreen(tx.pixelPosition), position: tx.screenPosition,
color: txColor.color color: txColor.color
}, },
duration: 2500, duration: 2500,
@ -218,11 +226,13 @@ export default class TxPoolScene {
} else { } else {
tx.view.update({ tx.view.update({
display: { display: {
position: this.pixelsToScreen(tx.pixelPosition) position: tx.screenPosition
}, },
duration: 1500, duration: 1000,
minDuration: 1000, minDuration: 500,
delay: 0, delay: 50,
jitter: 500,
smooth: true,
adjust: true adjust: true
}) })
} }
@ -239,33 +249,37 @@ export default class TxPoolScene {
} }
ids = this.getActiveTxList() ids = this.getActiveTxList()
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
this.layoutTx(this.txs[ids[i]], this.scene.count++) this.place(this.txs[ids[i]])
this.saveGridToPixelPosition(this.txs[ids[i]])
} }
this.resetScroll()
for (let i = 0; i < ids.length; i++) {
this.setTxOnScreen(this.txs[ids[i]])
}
}
resetScroll () {
const ids = this.getActiveTxList()
let poolTop = -Infinity let poolTop = -Infinity
let poolBottom = Infinity let poolBottom = Infinity
let poolScreenTop = -Infinity let poolScreenTop = -Infinity
ids = this.getActiveTxList()
let tx let tx
for (let i = 0; i < ids.length; i++) { for (let i = 0; i < ids.length; i++) {
tx = this.txs[ids[i]] tx = this.txs[ids[i]]
this.saveGridToPixelPosition(tx) // this.saveGridToPixelPosition(tx)
poolTop = Math.max(poolTop, tx.pixelPosition.y - tx.pixelPosition.r) poolTop = Math.max(poolTop, tx.pixelPosition.y - tx.pixelPosition.r)
poolScreenTop = Math.max(poolScreenTop, tx.pixelPosition.y + tx.pixelPosition.r) poolScreenTop = Math.max(poolScreenTop, tx.pixelPosition.y + tx.pixelPosition.r)
poolBottom = Math.min(poolBottom, tx.pixelPosition.y - tx.pixelPosition.r) poolBottom = Math.min(poolBottom, tx.pixelPosition.y - tx.pixelPosition.r)
} }
this.maxHeight = poolScreenTop this.maxHeight = poolScreenTop
let scrollAmount = Math.min(-this.scene.scroll, this.heightLimit - poolTop)
this.scene.scroll += scrollAmount
this.maxHeight += scrollAmount
if (this.heightLimit && poolTop > this.heightLimit) {
let scrollAmount = this.heightLimit - poolTop
this.scroll(scrollAmount, true)
this.maxHeight += scrollAmount
} else if (this.heightLimit && poolTop < this.heightLimit) {
let scrollAmount = Math.min(-this.scene.scroll, this.heightLimit - poolTop)
this.scroll(scrollAmount, true)
this.maxHeight += scrollAmount
}
if (this.heightStore) this.heightStore.set(this.maxHeight) if (this.heightStore) this.heightStore.set(this.maxHeight)
} }
@ -343,7 +357,8 @@ export default class TxPoolScene {
return grid return grid
} }
place (tx, position, size) { place (tx) {
const size = this.txSize(tx)
tx.gridPosition.x = 1 + Math.floor(position % this.blockWidth) tx.gridPosition.x = 1 + Math.floor(position % this.blockWidth)
tx.gridPosition.y = 1 + (Math.floor(position / this.blockWidth)) tx.gridPosition.y = 1 + (Math.floor(position / this.blockWidth))
tx.gridPosition.r = size tx.gridPosition.r = size

View File

@ -1,30 +1,42 @@
import { smootherstep } from '../utils/easing.js'
function interpolateAttributeStart(attribute, now, modular) { function interpolateAttributeStart(attribute, now, modular) {
if (attribute.v == 0 || (attribute.t + attribute.d) <= now) { if (attribute.v == 0 || (attribute.t + attribute.d) <= now) {
// transition finished, next transition starts from current end state // transition finished, next transition starts from current end state
// (clamp to 1) // (clamp to 1)
attribute.a = attribute.b if (attribute.boom) {
attribute.v = 0 attribute.a = attribute.a
attribute.d = 0 attribute.v = 0
attribute.d = 0
} else {
attribute.a = attribute.b
attribute.v = 0
attribute.d = 0
}
return false
} else if (attribute.t > now) { } else if (attribute.t > now) {
// transition not started // transition not started
// (clamp to 0) // (clamp to 0)
return true
} else { } else {
// transition in progress // transition in progress
// (interpolate) // (interpolate)
let progress = (now - attribute.t) const progress = (now - attribute.t)
const delta = attribute.e ? smootherstep(progress / attribute.d) : (progress / attribute.d)
if (modular && Math.abs(attribute.a - attribute.b) > 0.5) { if (modular && Math.abs(attribute.a - attribute.b) > 0.5) {
if (attribute.a > 0.5) { if (attribute.a > 0.5) {
attribute.a -= 1 attribute.a -= 1
attribute.a = attribute.a + ((progress / attribute.d) * (attribute.b - attribute.a)) attribute.a = attribute.a + (delta * (attribute.b - attribute.a))
} else { } else {
attribute.a = attribute.a + ((progress / attribute.d) * (attribute.b - 1 - attribute.a)) attribute.a = attribute.a + (delta * (attribute.b - 1 - attribute.a))
} }
if (attribute.a < 0) attribute.a += 1 if (attribute.a < 0) attribute.a += 1
} else { } else {
attribute.a = attribute.a + ((progress / attribute.d) * (attribute.b - attribute.a)) attribute.a = attribute.a + (delta * (attribute.b - attribute.a))
} }
attribute.d = attribute.d - progress attribute.d = attribute.d - progress
attribute.v = 1 / attribute.d attribute.v = 1 / attribute.d
return true
} }
} }
@ -39,12 +51,12 @@ export default class TxSprite {
} }
this.attributes = { this.attributes = {
x: { a: x, b: x, t: offsetTime, v: 0, d: 0 }, x: { a: x, b: x, t: 0, v: 0, d: 0 },
y: { a: y, b: y, t: offsetTime, v: 0, d: 0 }, y: { a: y, b: y, t: 0, v: 0, d: 0 },
r: { a: r, b: r, t: offsetTime, v: 0, d: 0 }, r: { a: r, b: r, t: 0, v: 0, d: 0 },
h: { a: h, b: h, t: offsetTime, v: 0, d: 0 }, h: { a: h, b: h, t: 0, v: 0, d: 0 },
l: { a: l, b: l, t: offsetTime, v: 0, d: 0 }, l: { a: l, b: l, t: 0, v: 0, d: 0 },
a: { a: alpha, b: alpha, t: offsetTime, v: 0, d: 0 }, a: { a: alpha, b: alpha, t: 0, v: 0, d: 0 },
} }
// Used to temporarily modify the sprite, so that the base view can be resumed later // Used to temporarily modify the sprite, so that the base view can be resumed later
@ -55,15 +67,16 @@ export default class TxSprite {
this.compile() this.compile()
} }
interpolateAttributes (updateMap, attributes, offsetTime, v, duration, minDuration, adjust) { interpolateAttributes (updateMap, attributes, offsetTime, delay, v, smooth, boomerang, duration, minDuration, adjust) {
for (const key of Object.keys(updateMap)) { for (const key of Object.keys(updateMap)) {
// for each non-null attribute: // for each non-null attribute:
if (updateMap[key] != null) { if (updateMap[key] != null) {
// calculate current interpolated value, and set as 'from' // calculate current interpolated value, and set as 'from'
interpolateAttributeStart(attributes[key], offsetTime, key === 'h') const inProgress = interpolateAttributeStart(attributes[key], offsetTime, key === 'h')
// interpolateAttributeStart(attributes[key], offsetTime, key) // interpolateAttributeStart(attributes[key], offsetTime, key)
// set 'start' to now // set 'start' to now
attributes[key].t = offsetTime attributes[key].t = offsetTime
if (!adjust || !inProgress) attributes[key].t += (delay || 0)
// if 'adjust' flag set // if 'adjust' flag set
// set 'duration' to Max(remaining time, 'duration') // set 'duration' to Max(remaining time, 'duration')
if (!adjust || (duration && attributes[key].d == 0)) { if (!adjust || (duration && attributes[key].d == 0)) {
@ -75,11 +88,18 @@ export default class TxSprite {
} }
// set 'to' to target value // set 'to' to target value
attributes[key].b = updateMap[key] attributes[key].b = updateMap[key]
if (!adjust || !inProgress) {
if (smooth) attributes[key].e = true
else if (!smooth && attributes[key].e) delete attributes[key].e
if (boomerang) attributes[key].boom = true
else if (!boomerang && attributes[key].boom) delete attributes[key].boom
}
} }
} }
} }
update ({ now = performance.now(), x, y, r, h, l, alpha, duration, minDuration, adjust, modify }) { update ({ now = performance.now(), delay, x, y, r, h, l, alpha, smooth, boomerang, duration, minDuration, adjust, modify }) {
const offsetTime = now const offsetTime = now
const v = duration > 0 ? (1 / duration) : 0 const v = duration > 0 ? (1 / duration) : 0
@ -92,7 +112,7 @@ export default class TxSprite {
const isModified = !!this.modAttributes const isModified = !!this.modAttributes
if (!modify) { if (!modify) {
this.interpolateAttributes(this.updateMap, this.attributes, offsetTime, v, duration, minDuration, adjust) this.interpolateAttributes(this.updateMap, this.attributes, offsetTime, delay, v, smooth, boomerang, duration, minDuration, adjust)
} else { } else {
if (!isModified) { // set up the modAttributes if (!isModified) { // set up the modAttributes
this.modAttributes = {} this.modAttributes = {}
@ -102,7 +122,7 @@ export default class TxSprite {
} }
} }
} }
this.interpolateAttributes(this.updateMap, this.modAttributes, offsetTime, v, duration, minDuration, adjust) this.interpolateAttributes(this.updateMap, this.modAttributes, offsetTime, delay, v, smooth, boomerang, duration, minDuration, adjust)
} }
this.compile() this.compile()
@ -152,7 +172,18 @@ export default class TxSprite {
for (let step = 0; step < VI.length; step++) { for (let step = 0; step < VI.length; step++) {
// components of each field in the vertex array are defined by an entry in VI: // components of each field in the vertex array are defined by an entry in VI:
// VI[i].a is the attribute, VI[i].f is the inner field, VI[i].offA and VI[i].offB are offset factors // VI[i].a is the attribute, VI[i].f is the inner field, VI[i].offA and VI[i].offB are offset factors
this.vertexData[(vertex * vertexStride) + step + 2] = attributes[VI[step].a][VI[step].f] if (VI[step].f === 'v' && attributes[VI[step].a].e) {
if (VI[step].f === 'v' && attributes[VI[step].a].boom) {
this.vertexData[(vertex * vertexStride) + step + 2] = -20 - attributes[VI[step].a][VI[step].f]
}
else {
this.vertexData[(vertex * vertexStride) + step + 2] = -attributes[VI[step].a][VI[step].f]
}
} else if (VI[step].f === 'v' && attributes[VI[step].a].boom) {
this.vertexData[(vertex * vertexStride) + step + 2] = -10 - attributes[VI[step].a][VI[step].f]
} else {
this.vertexData[(vertex * vertexStride) + step + 2] = attributes[VI[step].a][VI[step].f]
}
} }
} }

View File

@ -4,13 +4,17 @@ const highlightTransitionTime = 300
// converts from this class's update format to TxSprite's update format // converts from this class's update format to TxSprite's update format
// now, id, value, position, size, color, alpha, duration, adjust // now, id, value, position, size, color, alpha, duration, adjust
function toSpriteUpdate(display, duration, delay, start, adjust) { function toSpriteUpdate(display, duration, minDuration, delay, start, adjust, smooth, boomerang) {
return { return {
now: (start || performance.now()) + (delay || 0), now: (start || performance.now()),
delay: delay,
duration: duration, duration: duration,
minDuration: minDuration,
...(display.position ? display.position: {}), ...(display.position ? display.position: {}),
...(display.color ? display.color: {}), ...(display.color ? display.color: {}),
adjust adjust,
smooth,
boomerang
} }
} }
@ -46,14 +50,14 @@ export default class TxView {
delay: for queued transitions, how long to wait after current transition delay: for queued transitions, how long to wait after current transition
completes to start. completes to start.
*/ */
update ({ display, duration, delay, jitter, state, start, adjust }) { update ({ display, duration, minDuration, delay = 0, jitter, state, start, adjust, smooth, boomerang }) {
this.state = state this.state = state
if (jitter) delay += (Math.random() * jitter) if (jitter) delay += (Math.random() * jitter)
if (!this.initialised || !this.sprite) { if (!this.initialised || !this.sprite) {
this.initialised = true this.initialised = true
this.sprite = new TxSprite( this.sprite = new TxSprite(
toSpriteUpdate(display, duration, delay, start), toSpriteUpdate(display, duration, minDuration, delay, start, adjust, smooth, boomerang),
this.vertexArray this.vertexArray
) )
// apply any pending modifications // apply any pending modifications
@ -74,7 +78,7 @@ export default class TxView {
} }
} else { } else {
this.sprite.update( this.sprite.update(
toSpriteUpdate(display, duration, delay, start, adjust) toSpriteUpdate(display, duration, minDuration, delay, start, adjust, smooth, boomerang)
) )
} }
} }
@ -87,7 +91,7 @@ export default class TxView {
...this.hoverColor, ...this.hoverColor,
duration: highlightTransitionTime, duration: highlightTransitionTime,
adjust: false, adjust: false,
modify: true modify: true,
}) })
} else { } else {
this.hover = false this.hover = false

View File

@ -15,9 +15,40 @@ uniform vec2 screenSize;
uniform float now; uniform float now;
uniform sampler2D colorTexture; uniform sampler2D colorTexture;
float interpolate(float x, bool useSmooth, bool boomerang) {
x = clamp(x, 0.0, 1.0);
if (boomerang) {
x = 2.0 * x;
if (x > 1.0) {
x = 2.0 - x;
}
}
if (useSmooth) {
float ix = 1.0 - x;
x = x * x;
return x / (x + ix * ix);
} else {
return x;
}
}
// hue is modular, so interpolation should take the shortest path, wrapping around if necessary // hue is modular, so interpolation should take the shortest path, wrapping around if necessary
float interpolateHue(vec4 hue) { float interpolateHue(vec4 hue) {
float delta = clamp((now - hue.z) * hue.w, 0.0, 1.0); bool useSmooth = false;
bool boomerang = false;
if (hue.w <= -20.0) {
boomerang = true;
useSmooth = true;
hue.w = (0.0 - hue.w) - 20.0;
} else if (hue.w <= -10.0) {
boomerang = true;
hue.w = (0.0 - hue.w) - 10.0;
} else if (hue.w < 0.0) {
useSmooth = true;
hue.w = -hue.w;
}
float d = clamp((now - hue.z) * hue.w, 0.0, 1.0);
float delta = interpolate(d, useSmooth, boomerang);
if (abs(hue.x - hue.y) > 0.5) { if (abs(hue.x - hue.y) > 0.5) {
if (hue.x > 0.5) { if (hue.x > 0.5) {
return mod(mix(hue.x - 1.0, hue.y, delta), 1.0); return mod(mix(hue.x - 1.0, hue.y, delta), 1.0);
@ -30,7 +61,21 @@ float interpolateHue(vec4 hue) {
} }
float interpolateAttribute(vec4 attr) { float interpolateAttribute(vec4 attr) {
float delta = clamp((now - attr.z) * attr.w, 0.0, 1.0); bool useSmooth = false;
bool boomerang = false;
if (attr.w <= -20.0) {
boomerang = true;
useSmooth = true;
attr.w = (0.0 - attr.w) - 20.0;
} else if (attr.w <= -10.0) {
boomerang = true;
attr.w = (0.0 - attr.w) - 10.0;
} else if (attr.w < 0.0) {
useSmooth = true;
attr.w = -attr.w;
}
float d = clamp((now - attr.z) * attr.w, 0.0, 1.0);
float delta = interpolate(d, useSmooth, boomerang);
return mix(attr.x, attr.y, delta); return mix(attr.x, attr.y, delta);
} }

View File

@ -4,3 +4,9 @@ export function easeOutBack(x) {
return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2); return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2);
} }
export function smootherstep(x) {
const ix = 1 - x
x = x * x
return x / (x + ix * ix)
}