The scene view and the animation preview share one camera, the three form dialogs one base, and keys are found through one set of helpers (#85)
game-maker / gate (push) Successful in 1m36s
game-maker / suite (push) Successful in 5m41s

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5 committed 2026-09-17 14:19:53 +02:00
1 parent bf02c9e8b3
commit 2a390e52ff
22 files changed
+418 -452

No files matched your search

+36 -4
View File
@@ -17,9 +17,9 @@ export type Playback = (typeof PLAYBACKS)[number];
export const ANIMATION_ROOT_TYPES: readonly AssetType[] = ['sprite', 'text', 'container', 'scene'];
/** Position lives on the object, not in its property bag, so the timeline offers it by name. */
export const POSITION_KEYS = ['x', 'y'] as const;
const POSITION_KEYS = ['x', 'y'] as const;
export function isPositionKey(key: string): key is (typeof POSITION_KEYS)[number] {
function isPositionKey(key: string): key is (typeof POSITION_KEYS)[number] {
return (POSITION_KEYS as readonly string[]).includes(key);
}
@@ -46,7 +46,7 @@ export function fpsOf(asset: AnimationAsset): number {
}
/** The frame of the last key over all rows; -1 for an animation without keys. */
export function lastKeyFrame(asset: AnimationAsset): number {
function lastKeyFrame(asset: AnimationAsset): number {
let last = -1;
for (const track of asset.tracks) {
const key = track.keys[track.keys.length - 1];
@@ -133,7 +133,7 @@ export function interpolationOf(key: AnimationKey): KeyInterpolation {
* How far along the way to the next key a value is, `t` the share of the time passed. The eases are
* quadratic: ease-in reaches 0.25 at half the time, ease-out 0.75, ease-in-out 0.5 but 0.125 at a quarter.
*/
export function easedShare(interpolation: KeyInterpolation, t: number): number {
function easedShare(interpolation: KeyInterpolation, t: number): number {
switch (interpolation) {
case 'linear':
return t;
@@ -210,6 +210,16 @@ export function animatedKind(type: AssetType, key: string): string | undefined {
return animatableDefs(type).find((d) => d.key === key)?.kind;
}
/** The kind `key` has on an asset holding `bag` – see `animatableDefsOf`. */
export function animatedKindOf(
project: Project,
asset: Asset,
bag: PropertyBag,
key: string,
): string | undefined {
return animatableDefsOf(project, asset, bag).find((d) => d.key === key)?.kind;
}
function hasPosition(type: AssetType): boolean {
return type !== 'scene' && type !== 'behavior';
}
@@ -501,6 +511,28 @@ export function pauseAnimation(holder: PlayerHolder, animationId: string): void
if (animationId === '' || player.animationId === animationId) player.playing = false;
}
/** A key on one row, as the timeline selects it. */
export interface KeyRef {
trackId: string;
frame: number;
}
export function sameKey(a: KeyRef, b: KeyRef): boolean {
return a.trackId === b.trackId && a.frame === b.frame;
}
export function hasKey(keys: readonly KeyRef[], key: KeyRef): boolean {
return keys.some((k) => sameKey(k, key));
}
export function findTrack(asset: AnimationAsset, trackId: string): AnimationTrack | undefined {
return asset.tracks.find((t) => t.id === trackId);
}
export function findKey(asset: AnimationAsset, ref: KeyRef): AnimationKey | undefined {
return findTrack(asset, ref.trackId)?.keys.find((k) => k.frame === ref.frame);
}
/** Sorted by frame, at most one per frame (the later wins), frames whole and not negative. */
export function normalizeKeys(keys: readonly AnimationKey[]): AnimationKey[] {
const byFrame = new Map<number, AnimationKey>();
+4 -7
View File
@@ -5,7 +5,7 @@
*/
import {
animatableDefs,
animatableDefsOf,
animatedKindOf,
frameCount,
isPositionRow,
PLACED_ROOT_ID,
@@ -18,9 +18,6 @@ import { instanceChildId, objectRef } from './instance';
import { findAsset, type ObjectHolder, type Project } from './Project';
import { resolveSceneObjectProps } from '../properties/resolve';
/** The one object the preview places the root on – what a row aimed at the root itself writes to. */
export const PREVIEW_ROOT_ID = PLACED_ROOT_ID;
/**
* The holder the preview draws at `frame`: a copy of the scene for a scene root, otherwise a holder
* of one object placing the root at the origin, its content resolved from the template like any
@@ -50,7 +47,7 @@ export function previewHolder(
/** The id the preview draws a row's target under – for a check that wants to look at it. */
export function previewObjectId(root: Asset | undefined, target: string): string {
if (root?.type === 'scene') return target;
return target === '' ? PREVIEW_ROOT_ID : instanceChildId(PREVIEW_ROOT_ID, target);
return target === '' ? PLACED_ROOT_ID : instanceChildId(PLACED_ROOT_ID, target);
}
/** Deep enough that nothing a row writes – a position, an override, an instance edit – is the document's. */
@@ -68,7 +65,7 @@ function sceneCopy(scene: ObjectHolder): ObjectHolder {
}
function placedHolder(root: Asset): ObjectHolder {
return { id: PREVIEW_ROOT_ID, type: 'container', name: root.name, props: {}, objects: [placedRoot(root)] };
return { id: PLACED_ROOT_ID, type: 'container', name: root.name, props: {}, objects: [placedRoot(root)] };
}
/** A row on the scene itself: its value goes into the copy's own properties. */
@@ -92,7 +89,7 @@ function writeObject(
if (!ref || !asset) return;
// Resolved, so a shader worn through an override or an instance edit brings its parameters (#104).
const props = resolveSceneObjectProps(project, asset, ref.object);
const kind = animatableDefsOf(project, asset, props).find((d) => d.key === track.property)?.kind;
const kind = animatedKindOf(project, asset, props, track.property);
if (!kind) return;
const key = track.property;
if (isPositionRow(asset.type, key)) {