Smoother, fancier animations

This commit is contained in:
Mononaut 2022-06-09 22:08:56 +00:00
parent d3c84c7406
commit 29627a5ebd
10 changed files with 279 additions and 124 deletions

View File

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

View File

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

View File

@ -73,7 +73,7 @@ export default class BitcoinTx {
this.is_partial = !!partial
this.is_preview = !!preview
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.gridPosition = { x: 0, y: 0, r: 0}
this.inputs = inputs

View File

@ -1,6 +1,7 @@
import TxMondrianPoolScene from './TxMondrianPoolScene.js'
import { settings } from '../stores.js'
import { logTxSize, byteTxSize } from '../utils/misc.js'
import { orange } from '../utils/color.js'
import config from '../config.js'
let settingsValue
@ -62,6 +63,7 @@ export default class TxBlockScene extends TxMondrianPoolScene {
}
setTxOnScreen (tx, pixelPosition) {
this.saveGridToPixelPosition(tx)
if (!tx.view.initialised) {
tx.view.update({
display: {
@ -95,14 +97,17 @@ export default class TxBlockScene extends TxMondrianPoolScene {
color: tx.getColor('block', this.colorMode).color
},
duration: this.laidOut ? 1000 : 2000,
delay: 0,
jitter: this.laidOut ? 0 : 1500,
delay: 50,
jitter: this.laidOut ? 500 : 1500,
smooth: true,
state: 'block'
})
}
}
prepareTxOnScreen (tx) {
prepareTxOnScreen (tx, now) {
const oldRadius = tx.pixelPosition.r
this.saveGridToPixelPosition(tx)
if (!tx.view.initialised) {
tx.view.update({
display: {
@ -119,21 +124,40 @@ export default class TxBlockScene extends TxMondrianPoolScene {
delay: 0,
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) {
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({
display: {
position: {
@ -157,8 +181,11 @@ export default class TxBlockScene extends TxMondrianPoolScene {
alpha: 1
}
},
start,
duration: 2000,
delay: 0
delay: 100,
jitter: 500,
smooth: true,
})
}
@ -166,8 +193,9 @@ export default class TxBlockScene extends TxMondrianPoolScene {
this.hidden = false
this.exited = false
const ids = this.getActiveTxList()
const start = performance.now()
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)
}
exitTx (tx, right) {
exitTx (tx, start, right) {
tx.view.update({
display: {
position: {
@ -192,8 +220,11 @@ export default class TxBlockScene extends TxMondrianPoolScene {
alpha: 0
}
},
delay: 0,
duration: 2000
delay: 100,
start,
jitter: 500,
duration: 2000,
smooth: true,
})
}
@ -201,8 +232,9 @@ export default class TxBlockScene extends TxMondrianPoolScene {
this.hidden = true
this.exited = true
const ids = this.getActiveTxList()
const start = performance.now()
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)
}
hideTx (tx) {
hideTx (tx, now) {
this.savePixelsToScreenPosition(tx)
tx.view.update({
display: {
@ -225,13 +257,15 @@ export default class TxBlockScene extends TxMondrianPoolScene {
alpha: 0
}
},
duration: 2000,
delay: 0,
state: 'fadeout'
start: now,
duration: 1500,
delay: 50,
state: 'fadeout',
smooth: true,
})
}
showTx (tx) {
showTx (tx, now) {
this.savePixelsToScreenPosition(tx)
tx.view.update({
display: {
@ -242,13 +276,16 @@ export default class TxBlockScene extends TxMondrianPoolScene {
alpha: 1
}
},
duration: 500,
delay: 0,
state: 'fadeout'
start: now,
duration: 1500,
delay: 50,
state: 'fadeout',
smooth: true,
})
}
prepareAll () {
const now = performance.now()
this.resize({})
this.scene.count = 0
let ids = this.getHiddenTxList()
@ -258,7 +295,7 @@ export default class TxBlockScene extends TxMondrianPoolScene {
}
ids = this.getActiveTxList()
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)
}
resetScroll () {
return
}
hide () {
this.hidden = true
const now = performance.now()
const ids = this.getActiveTxList()
for (let i = 0; i < ids.length; i++) {
this.hideTx(this.txs[ids[i]])
this.hideTx(this.txs[ids[i]], now)
}
}
show () {
if (this.hidden) {
this.hidden = false
const now = performance.now()
const ids = this.getActiveTxList()
for (let i = 0; i < ids.length; i++) {
this.showTx(this.txs[ids[i]])
this.showTx(this.txs[ids[i]], now)
}
}
}
expire (delay=3000) {
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(() => {
const txIds = this.getTxList()
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()
}, delay)

View File

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

View File

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

View File

@ -1,30 +1,42 @@
import { smootherstep } from '../utils/easing.js'
function interpolateAttributeStart(attribute, now, modular) {
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
if (attribute.boom) {
attribute.a = attribute.a
attribute.v = 0
attribute.d = 0
} else {
attribute.a = attribute.b
attribute.v = 0
attribute.d = 0
}
return false
} else if (attribute.t > now) {
// transition not started
// (clamp to 0)
return true
} else {
// transition in progress
// (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 (attribute.a > 0.5) {
attribute.a -= 1
attribute.a = attribute.a + ((progress / attribute.d) * (attribute.b - attribute.a))
attribute.a = attribute.a + (delta * (attribute.b - attribute.a))
} 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
} 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.v = 1 / attribute.d
return true
}
}
@ -39,12 +51,12 @@ export default class TxSprite {
}
this.attributes = {
x: { a: x, b: x, t: offsetTime, v: 0, d: 0 },
y: { a: y, b: y, t: offsetTime, v: 0, d: 0 },
r: { a: r, b: r, t: offsetTime, v: 0, d: 0 },
h: { a: h, b: h, t: offsetTime, v: 0, d: 0 },
l: { a: l, b: l, t: offsetTime, v: 0, d: 0 },
a: { a: alpha, b: alpha, t: offsetTime, v: 0, d: 0 },
x: { a: x, b: x, t: 0, v: 0, d: 0 },
y: { a: y, b: y, t: 0, v: 0, d: 0 },
r: { a: r, b: r, t: 0, v: 0, d: 0 },
h: { a: h, b: h, t: 0, v: 0, d: 0 },
l: { a: l, b: l, t: 0, 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
@ -55,15 +67,16 @@ export default class TxSprite {
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 each non-null attribute:
if (updateMap[key] != null) {
// 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)
// set 'start' to now
attributes[key].t = offsetTime
if (!adjust || !inProgress) attributes[key].t += (delay || 0)
// if 'adjust' flag set
// set 'duration' to Max(remaining time, 'duration')
if (!adjust || (duration && attributes[key].d == 0)) {
@ -75,11 +88,18 @@ export default class TxSprite {
}
// set 'to' to target value
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 v = duration > 0 ? (1 / duration) : 0
@ -92,7 +112,7 @@ export default class TxSprite {
const isModified = !!this.modAttributes
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 {
if (!isModified) { // set up the 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()
@ -152,7 +172,18 @@ export default class TxSprite {
for (let step = 0; step < VI.length; step++) {
// 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
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
// 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 {
now: (start || performance.now()) + (delay || 0),
now: (start || performance.now()),
delay: delay,
duration: duration,
minDuration: minDuration,
...(display.position ? display.position: {}),
...(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
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
if (jitter) delay += (Math.random() * jitter)
if (!this.initialised || !this.sprite) {
this.initialised = true
this.sprite = new TxSprite(
toSpriteUpdate(display, duration, delay, start),
toSpriteUpdate(display, duration, minDuration, delay, start, adjust, smooth, boomerang),
this.vertexArray
)
// apply any pending modifications
@ -74,7 +78,7 @@ export default class TxView {
}
} else {
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,
duration: highlightTransitionTime,
adjust: false,
modify: true
modify: true,
})
} else {
this.hover = false

View File

@ -15,9 +15,40 @@ uniform vec2 screenSize;
uniform float now;
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
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 (hue.x > 0.5) {
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 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);
}

View File

@ -4,3 +4,9 @@ export function easeOutBack(x) {
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)
}