Skip to content

Commit c312bc2

Browse files
committed
🛠️ Fixed step/pre-post keyframes
- Fixed step/pre-post keyframes using wrong interpolation methods - Added the ability to edit texture mcmeta files.
1 parent f9958ee commit c312bc2

File tree

7 files changed

+4105
-347
lines changed

7 files changed

+4105
-347
lines changed

src/blueprintFormat.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,7 @@ export const BLUEPRINT_FORMAT = new Blockbench.ModelFormat({
620620
animated_textures: true,
621621
animation_controllers: true,
622622
animation_files: true,
623+
texture_mcmeta: true,
623624
animation_mode: true,
624625
bone_binding_expression: true,
625626
bone_rig: true,

src/mods/customKeyframeEasingsMod.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { ContextProperty, createBlockbenchMod } from '../util/moddingTools'
2+
import { isCurrentFormat } from '../blueprintFormat'
3+
import { PACKAGE } from '../constants'
4+
5+
import {
6+
EASING_DEFAULT,
7+
EasingKey,
8+
easingFunctions,
9+
getEasingArgDefault,
10+
hasArgs,
11+
} from '../util/easing'
12+
13+
interface IEasingProperties {
14+
easing?: EasingKey
15+
easingArgs?: any[]
16+
}
17+
18+
function lerp(start: number, stop: number, amt: number): number {
19+
return amt * (stop - start) + start
20+
}
21+
22+
createBlockbenchMod(
23+
`${PACKAGE.name}:keyframeEasingMod`,
24+
{
25+
originalGetLerp: Blockbench.Keyframe.prototype.getLerp,
26+
easingProperty: undefined as ContextProperty<'string'>,
27+
easingArgsProperty: undefined as ContextProperty<'array'>,
28+
},
29+
context => {
30+
context.easingProperty = new Property(Blockbench.Keyframe, 'string', 'easing', {
31+
default: EASING_DEFAULT,
32+
condition: isCurrentFormat(),
33+
})
34+
context.easingArgsProperty = new Property(Blockbench.Keyframe, 'array', 'easingArgs', {
35+
condition: isCurrentFormat(),
36+
})
37+
38+
Blockbench.Keyframe.prototype.getLerp = function (
39+
this: _Keyframe,
40+
other,
41+
axis,
42+
amount,
43+
allowExpression
44+
): number {
45+
const easing = other.easing || 'linear'
46+
47+
if (!isCurrentFormat() || easing === 'linear')
48+
return context.originalGetLerp.call(this, other, axis, amount, allowExpression)
49+
50+
let easingFunc = easingFunctions[easing]
51+
if (hasArgs(easing)) {
52+
const arg1 =
53+
Array.isArray(other.easingArgs) && other.easingArgs.length > 0
54+
? other.easingArgs[0]
55+
: getEasingArgDefault(other)
56+
57+
easingFunc = easingFunc.bind(null, arg1 || 0)
58+
}
59+
const easedAmount = easingFunc(amount)
60+
const start = this.calc(axis)
61+
const stop = other.calc(axis)
62+
const result = lerp(start, stop, easedAmount)
63+
64+
if (Number.isNaN(result)) {
65+
throw new Error('Invalid easing function or arguments.')
66+
}
67+
return result
68+
}
69+
70+
return context
71+
},
72+
context => {
73+
context.easingProperty?.delete()
74+
context.easingArgsProperty?.delete()
75+
Blockbench.Keyframe.prototype.getLerp = context.originalGetLerp
76+
}
77+
)
78+
79+
export function reverseEasing(easing?: EasingKey): EasingKey | undefined {
80+
if (!easing) return easing
81+
if (easing.startsWith('easeInOut')) return easing
82+
if (easing.startsWith('easeIn')) return easing.replace('easeIn', 'easeOut')
83+
if (easing.startsWith('easeOut')) return easing.replace('easeOut', 'easeIn')
84+
return easing
85+
}
86+
87+
createBlockbenchMod(
88+
`${PACKAGE.name}:reverseKeyframesMod`,
89+
{
90+
action: BarItems.reverse_keyframes as Action,
91+
originalClick: (BarItems.reverse_keyframes as Action).click,
92+
},
93+
context => {
94+
context.action.click = function (event?: Event) {
95+
context.originalClick.call(this, event)
96+
// There's not really an easy way to merge our undo operation with the original one so we'll make a new one instead
97+
Undo.initEdit({ keyframes: Timeline.selected || undefined })
98+
99+
const kfByAnimator: Record<string, _Keyframe[]> = {}
100+
for (const kf of Timeline.selected || []) {
101+
kfByAnimator[kf.animator.uuid] ??= []
102+
kfByAnimator[kf.animator.uuid].push(kf)
103+
}
104+
105+
const kfByAnimatorAndChannel: Record<string, Record<string, _Keyframe[]>> = {}
106+
for (const [animatorUuid, keyframes] of Object.entries(kfByAnimator)) {
107+
const channel: Record<string, _Keyframe[]> = {}
108+
kfByAnimatorAndChannel[animatorUuid] = channel
109+
for (const kf of keyframes) {
110+
channel[kf.channel] ??= []
111+
channel[kf.channel].push(kf)
112+
}
113+
}
114+
115+
for (const channelGroups of Object.values(kfByAnimatorAndChannel)) {
116+
for (const keyframes of Object.values(channelGroups)) {
117+
// Ensure keyframes are in temporal order. Not sure if this is already the case, but it couldn't hurt
118+
keyframes.sort((a, b) => a.time - b.time)
119+
// Reverse easing direction
120+
const easingData: IEasingProperties[] = keyframes.map((kf: _Keyframe) => ({
121+
easing: reverseEasing(kf.easing),
122+
easingArgs: kf.easingArgs,
123+
}))
124+
// Shift easing data to the right by one keyframe
125+
keyframes.forEach((kf: _Keyframe, i: number) => {
126+
if (i == 0) {
127+
kf.easing = undefined
128+
kf.easingArgs = undefined
129+
return
130+
}
131+
const newEasingData = easingData[i - 1]
132+
kf.easing = newEasingData.easing
133+
kf.easingArgs = newEasingData.easingArgs
134+
})
135+
}
136+
}
137+
138+
Undo.finishEdit('Reverse keyframe easing')
139+
updateKeyframeSelection()
140+
Animator.preview()
141+
}
142+
return context
143+
},
144+
context => {
145+
context.action.click = context.originalClick
146+
}
147+
)

src/mods/index.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1+
import './addLocatorActionMod'
12
import './animationControllerMod'
23
import './animationPropertiesAction'
34
import './animationPropertiesMod'
5+
import './blockbenchReadMod'
6+
import './boneInterpolationMod'
47
import './bonePropertiesMod'
58
import './cubeOutlineMod'
9+
import './customKeyframeEasingsMod'
610
import './customKeyframesMod'
711
import './exportOverActionMod'
12+
import './formatIconMod'
813
import './groupContextMenuMod'
914
import './groupNameMod'
1015
import './keyframeMod'
@@ -19,9 +24,5 @@ import './projectSettingsActionOverride'
1924
import './saveAllAnimationsActionMod'
2025
import './saveProjectActionMod'
2126
import './saveProjectAsActionMod'
22-
import './variantPreviewCubeFaceMod'
2327
import './showDefaultPoseMod'
24-
import './addLocatorActionMod'
25-
import './blockbenchReadMod'
26-
import './formatIconMod'
27-
import './boneInterpolationMod'
28+
import './variantPreviewCubeFaceMod'

src/mods/keyframeMod.ts

Lines changed: 1 addition & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,7 @@
11
import { isCurrentFormat } from '../blueprintFormat'
22
import { PACKAGE } from '../constants'
33
import { events } from '../util/events'
4-
import { ContextProperty, createBlockbenchMod } from '../util/moddingTools'
5-
import {
6-
EASING_DEFAULT,
7-
EasingKey,
8-
easingFunctions,
9-
getEasingArgDefault,
10-
hasArgs,
11-
} from '../util/easing'
12-
13-
interface IEasingProperties {
14-
easing?: EasingKey
15-
easingArgs?: any[]
16-
}
4+
import { createBlockbenchMod } from '../util/moddingTools'
175

186
createBlockbenchMod(
197
`${PACKAGE.name}:keyframeSelectEventMod`,
@@ -84,133 +72,3 @@ createBlockbenchMod(
8472
context.barItem.change = context.originalChange
8573
}
8674
)
87-
88-
export function reverseEasing(easing?: EasingKey): EasingKey | undefined {
89-
if (!easing) return easing
90-
if (easing.startsWith('easeInOut')) return easing
91-
if (easing.startsWith('easeIn')) return easing.replace('easeIn', 'easeOut')
92-
if (easing.startsWith('easeOut')) return easing.replace('easeOut', 'easeIn')
93-
return easing
94-
}
95-
96-
createBlockbenchMod(
97-
`${PACKAGE.name}:reverseKeyframesMod`,
98-
{
99-
action: BarItems.reverse_keyframes as Action,
100-
originalClick: (BarItems.reverse_keyframes as Action).click,
101-
},
102-
context => {
103-
context.action.click = function (event?: Event) {
104-
context.originalClick.call(this, event)
105-
// There's not really an easy way to merge our undo operation with the original one so we'll make a new one instead
106-
Undo.initEdit({ keyframes: Timeline.selected || undefined })
107-
108-
const kfByAnimator: Record<string, _Keyframe[]> = {}
109-
for (const kf of Timeline.selected || []) {
110-
kfByAnimator[kf.animator.uuid] ??= []
111-
kfByAnimator[kf.animator.uuid].push(kf)
112-
}
113-
114-
const kfByAnimatorAndChannel: Record<string, Record<string, _Keyframe[]>> = {}
115-
for (const [animatorUuid, keyframes] of Object.entries(kfByAnimator)) {
116-
const channel: Record<string, _Keyframe[]> = {}
117-
kfByAnimatorAndChannel[animatorUuid] = channel
118-
for (const kf of keyframes) {
119-
channel[kf.channel] ??= []
120-
channel[kf.channel].push(kf)
121-
}
122-
}
123-
124-
for (const channelGroups of Object.values(kfByAnimatorAndChannel)) {
125-
for (const keyframes of Object.values(channelGroups)) {
126-
// Ensure keyframes are in temporal order. Not sure if this is already the case, but it couldn't hurt
127-
keyframes.sort((a, b) => a.time - b.time)
128-
// Reverse easing direction
129-
const easingData: IEasingProperties[] = keyframes.map((kf: _Keyframe) => ({
130-
easing: reverseEasing(kf.easing),
131-
easingArgs: kf.easingArgs,
132-
}))
133-
// Shift easing data to the right by one keyframe
134-
keyframes.forEach((kf: _Keyframe, i: number) => {
135-
if (i == 0) {
136-
kf.easing = undefined
137-
kf.easingArgs = undefined
138-
return
139-
}
140-
const newEasingData = easingData[i - 1]
141-
kf.easing = newEasingData.easing
142-
kf.easingArgs = newEasingData.easingArgs
143-
})
144-
}
145-
}
146-
147-
Undo.finishEdit('Reverse keyframe easing')
148-
updateKeyframeSelection()
149-
Animator.preview()
150-
}
151-
return context
152-
},
153-
context => {
154-
context.action.click = context.originalClick
155-
}
156-
)
157-
158-
function lerp(start: number, stop: number, amt: number): number {
159-
return amt * (stop - start) + start
160-
}
161-
162-
createBlockbenchMod(
163-
`${PACKAGE.name}:keyframeEasingMod`,
164-
{
165-
originalGetLerp: Blockbench.Keyframe.prototype.getLerp,
166-
easingProperty: undefined as ContextProperty<'string'>,
167-
easingArgsProperty: undefined as ContextProperty<'array'>,
168-
},
169-
context => {
170-
context.easingProperty = new Property(Blockbench.Keyframe, 'string', 'easing', {
171-
default: EASING_DEFAULT,
172-
condition: isCurrentFormat(),
173-
})
174-
context.easingArgsProperty = new Property(Blockbench.Keyframe, 'array', 'easingArgs', {
175-
condition: isCurrentFormat(),
176-
})
177-
178-
Blockbench.Keyframe.prototype.getLerp = function (
179-
this: _Keyframe,
180-
other,
181-
axis,
182-
amount,
183-
allowExpression
184-
): number {
185-
if (!isCurrentFormat())
186-
return context.originalGetLerp.call(this, other, axis, amount, allowExpression)
187-
188-
const easing = other.easing || 'linear'
189-
let easingFunc = easingFunctions[easing]
190-
if (hasArgs(easing)) {
191-
const arg1 =
192-
Array.isArray(other.easingArgs) && other.easingArgs.length > 0
193-
? other.easingArgs[0]
194-
: getEasingArgDefault(other)
195-
196-
easingFunc = easingFunc.bind(null, arg1 || 0)
197-
}
198-
const easedAmount = easingFunc(amount)
199-
const start = this.calc(axis)
200-
const stop = other.calc(axis)
201-
const result = lerp(start, stop, easedAmount)
202-
203-
if (Number.isNaN(result)) {
204-
throw new Error('Invalid easing function or arguments.')
205-
}
206-
return result
207-
}
208-
209-
return context
210-
},
211-
context => {
212-
context.easingProperty?.delete()
213-
context.easingArgsProperty?.delete()
214-
Blockbench.Keyframe.prototype.getLerp = context.originalGetLerp
215-
}
216-
)

0 commit comments

Comments
 (0)