From da5ce36250a79850c882d81f6f012819e43c22b9 Mon Sep 17 00:00:00 2001 From: Alexander Klingenbeck Date: Wed, 31 May 2023 01:21:20 +0200 Subject: [PATCH] Improve framework --- examples/ts_game/src/animation.ts | 43 ----------- examples/ts_game/src/entity.ts | 117 +++++++++++------------------- examples/ts_game/src/game.ts | 77 +++++++++++++++----- examples/ts_game/src/index.ts | 34 ++++----- examples/ts_game/src/text.ts | 68 +++++++++-------- examples/ts_game/src/timing.ts | 58 +++++++++++++++ 6 files changed, 207 insertions(+), 190 deletions(-) delete mode 100644 examples/ts_game/src/animation.ts create mode 100644 examples/ts_game/src/timing.ts diff --git a/examples/ts_game/src/animation.ts b/examples/ts_game/src/animation.ts deleted file mode 100644 index 5f7d88a..0000000 --- a/examples/ts_game/src/animation.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Clickable, HasColor } from "./entity" -import { makeUpdateablePromise } from "./game" - -export type easeFunc = (t: number, a: number, b: number, d: number) => number - -export const interpolate = (a: number, b: number, d: number, setter: (v: number) => void, fn: easeFunc) => { - const start = getTime() - return makeUpdateablePromise(() => { - const cur = getTime()-start - if(cur < d){ - setter(fn(cur, a, b, d)) - return false - } else { - setter(b) - return true - } - }) -} - -export const waitCondition = (predicate: () => boolean) => { - const start = getTime() - return makeUpdateablePromise(() => { - if(predicate()){ - return false - } else { - return true - } - }) -} - -export const wait = (time: number) => { - const start = getTime() - return waitCondition(() => (getTime()-start) < time) -} - -export const waitFrame = (frames = 1) => waitCondition(() => !!(frames--) || frames <= 0) - -export const waitKeyPressed = (key: number) => waitCondition(() => !isKeyPressed(key)) -export const waitClick = (button: number = MOUSE_BUTTON_LEFT) => waitCondition(() => !isMouseButtonDown(button)) -export const waitEntityClicked = (entity: Clickable) => waitCondition(() => !entity.isClicked) - -export const fadeIn = (c: HasColor, time: number, easeFunc = easeLinearNone) => interpolate(0, 1, time, (v) => c.color = fade(c.color, v), easeFunc) -export const fadeOut = (c: HasColor, time: number, easeFunc = easeLinearNone) => interpolate(0, 1, time, (v) => c.color = fade(c.color, 1-v), easeFunc) \ No newline at end of file diff --git a/examples/ts_game/src/entity.ts b/examples/ts_game/src/entity.ts index e95f0f2..ad5d732 100644 --- a/examples/ts_game/src/entity.ts +++ b/examples/ts_game/src/entity.ts @@ -1,21 +1,36 @@ -import { makeText } from "./text" +import { makeInlineText } from "./text" + +export interface Behaviour { + load?: (entity: T) => void, + unload?: (entity: T) => void, + update?: (entity: T) => void, + draw?: (entity: T) => void +} + +export function addBehaviour(obj: T, behaviour: Behaviour){ + obj.behaviours.push(behaviour) +} +export function removeBehaviour(obj: T, behaviour: Behaviour){ + const idx = obj.behaviours.findIndex(x => x === behaviour) + if(idx !== -1) obj.behaviours.splice(idx, 1) +} export type Creator = (objIn: A) => B export type Builder = Creator, A> export type Extender = Creator, B> -export function makeCombined(fn1: Builder): Builder -export function makeCombined(fn1: Builder, fn2: Extender): Builder -export function makeCombined(fn1: Builder, fn2: Extender, fn3: Extender): Builder -export function makeCombined(fn1: Builder, fn2: Extender, fn3: Extender, fn4: Extender): Builder -export function makeCombined(fn1: Builder, fn2: Extender, fn3: Extender, fn4: Extender, fn5: Extender): Builder -export function makeCombined(fn1: Builder, fn2: Extender, fn3: Extender, fn4: Extender, fn5: Extender, fn6: Extender): Builder -export function makeCombined(fn1: Builder, fn2?: Extender, fn3?: Extender, fn4?: Extender, fn5?: Extender, fn6?: Extender): Builder | Builder | Builder | Builder | Builder | Builder +export function combine(fn1: Builder): Builder +export function combine(fn1: Builder, fn2: Extender): Builder +export function combine(fn1: Builder, fn2: Extender, fn3: Extender): Builder +export function combine(fn1: Builder, fn2: Extender, fn3: Extender, fn4: Extender): Builder +export function combine(fn1: Builder, fn2: Extender, fn3: Extender, fn4: Extender, fn5: Extender): Builder +export function combine(fn1: Builder, fn2: Extender, fn3: Extender, fn4: Extender, fn5: Extender, fn6: Extender): Builder +export function combine(fn1: Builder, fn2?: Extender, fn3?: Extender, fn4?: Extender, fn5?: Extender, fn6?: Extender): Builder | Builder | Builder | Builder | Builder | Builder { - if(fn2 && fn3 && fn4 && fn5 && fn6) return makeCombined(makeCombined(fn1, fn2, fn3, fn4, fn5), fn6) - if(fn2 && fn3 && fn4 && fn5) return makeCombined(makeCombined(fn1, fn2, fn3, fn4), fn5) - if(fn2 && fn3 && fn4) return makeCombined(makeCombined(fn1, fn2, fn3), fn4) - if(fn2 && fn3) return makeCombined(makeCombined(fn1, fn2), fn3) + if(fn2 && fn3 && fn4 && fn5 && fn6) return combine(combine(fn1, fn2, fn3, fn4, fn5), fn6) + if(fn2 && fn3 && fn4 && fn5) return combine(combine(fn1, fn2, fn3, fn4), fn5) + if(fn2 && fn3 && fn4) return combine(combine(fn1, fn2, fn3), fn4) + if(fn2 && fn3) return combine(combine(fn1, fn2), fn3) if(fn2) return (objIn: Partial) => fn2(>fn1(objIn)) return fn1 } @@ -36,21 +51,6 @@ export interface HasBoundingBox { boundingBox: Rectangle } -export interface Behaviour { - load?: (entity: T) => void, - unload?: (entity: T) => void, - update?: (entity: T) => void, - draw?: (entity: T) => void -} - -export function addBehaviour(obj: T, behaviour: Behaviour){ - obj.behaviours.push(behaviour) -} -export function removeBehaviour(obj: T, behaviour: Behaviour){ - const idx = obj.behaviours.findIndex(x => x === behaviour) - if(idx !== -1) obj.behaviours.splice(idx, 1) -} - export interface HasBehaviour { behaviours: Behaviour[] } @@ -58,55 +58,33 @@ export interface HasBehaviour { export type Entity = HasIdentity & HasBehaviour export type EntityOf = Entity & T -export const setPropFn = (obj: T, key: keyof T, valueFn: () => any) => { if(obj[key] === undefined) obj[key] = valueFn() } -export const setProp = (obj: T, key: keyof T, value: any) => { if(obj[key] === undefined) obj[key] = value } +export const hasDefaultFn = (obj: T, key: keyof T, valueFn: () => any) => { if(obj[key] === undefined) obj[key] = valueFn() } +export const hasDefault = (obj: T, key: keyof T, value: any) => { if(obj[key] === undefined) obj[key] = value } +export const withComponent = (fn?: (obj: Partial) => void) => (obj: Partial) => { if(fn){fn(obj)}; return obj } +export const which = (behaviour: Behaviour) => (obj: T) => { addBehaviour(obj,behaviour); return obj; } let ID = 0 +export const withIdentity = withComponent(x => hasDefaultFn(x,'id', () => ID++)) +export const withBehaviour = withComponent(x => hasDefaultFn(x, 'behaviours', () => [])) +export const withPosition = withComponent(x => hasDefaultFn(x, 'position', () => new Vector2(0,0))) +export const withColor = withComponent(x => hasDefaultFn(x, 'color', () => new Color(255,255,255,255))) +export const withBoundingBox = withComponent(x => hasDefaultFn(x, 'boundingBox', () => new Rectangle(0,0,0,0))) -export const makeIdentity = (obj: Partial) => { - setPropFn(obj, 'id', () => ID++) - return obj -} - -export const makeBehaviour = (obj: Partial) => { - setPropFn(obj, 'behaviours', () => []) - return obj -} - -export const makeEntity: Builder = makeCombined(makeIdentity, makeBehaviour) - -export const makePosition = (obj: Partial,x = 0, y = 0) => { - setPropFn(obj, 'position', () => new Vector2(x,y)) - return obj -} - -export const makeColorRgb = (obj: Partial, r = 255, g = 255, b = 255, a = 255) => { - setPropFn(obj, 'color', () => new Color(r, g, b, a)) - return obj -} - -export const makeColor = (obj: Partial, c = WHITE) => { - setPropFn(obj, 'color', () => new Color(c.r, c.g, c.b, c.a)) - return obj -} - -export const makeBoundingBox = (obj: Partial, x = 0, y = 0, width = 0, height = 0) => { - setPropFn(obj, 'boundingBox', () => new Rectangle(x,y,width,height)) - return obj -} +export const makeEntity: Builder = combine(withIdentity, withBehaviour) export const debugRectDrawFn = (obj: HasBoundingBox, color = GREEN) => drawRectangleLines(obj.boundingBox.x, obj.boundingBox.y, obj.boundingBox.width, obj.boundingBox.height, color) export const debugRectDrawBehaviour = { draw: debugRectDrawFn } -export interface Clickable extends HasBoundingBox, HasBehaviour { +export interface HasMouseInteraction { isClicked: boolean hasMouseOver: boolean, hasMouseEntered: boolean, hasMouseLeft: boolean debugClickable: boolean } -export const clickableBehaviour: Behaviour = { - update: (obj: Clickable) => { +export const withMouseInteraction = withComponent() +export const checksBoundingBoxClicks: Behaviour = { + update: obj => { const over = checkCollisionPointRec(getMousePosition(), obj.boundingBox) obj.hasMouseEntered = !obj.hasMouseOver && over obj.hasMouseLeft = obj.hasMouseOver && !over @@ -115,23 +93,14 @@ export const clickableBehaviour: Behaviour = { if(obj.hasMouseEntered) setMouseCursor(MOUSE_CURSOR_POINTING_HAND) if(obj.hasMouseLeft) setMouseCursor(MOUSE_CURSOR_DEFAULT) }, - draw: (obj: Clickable) => { + draw: obj => { if(obj.debugClickable){ debugRectDrawFn(obj, obj.hasMouseOver ? RED : GREEN) drawCircle(getMouseX(), getMouseY(), 10, YELLOW) } }, - unload: obj => setMouseCursor(MOUSE_CURSOR_DEFAULT) + unload: obj => obj.hasMouseOver && setMouseCursor(MOUSE_CURSOR_DEFAULT) } -export const makeClickable: Builder = makeCombined(makeBehaviour, makeBoundingBox, (obj: HasBehaviour & HasBoundingBox & Partial) => { - setProp(obj, 'hasMouseOver', false) - setProp(obj, 'isClicked', false) - setProp(obj, 'hasMouseEntered', false) - setProp(obj, 'hasMouseLeft', false) - setProp(obj, 'debugClickable', false) - addBehaviour(obj, clickableBehaviour) - return obj -}) diff --git a/examples/ts_game/src/game.ts b/examples/ts_game/src/game.ts index 858c996..b690c9f 100644 --- a/examples/ts_game/src/game.ts +++ b/examples/ts_game/src/game.ts @@ -2,38 +2,75 @@ import { Behaviour, Entity, EntityOf } from "./entity"; import { forEachReverse } from "./helpers"; import { resourceUnloadAll } from "./resource"; -const promiseUpdateList: (()=>boolean)[] = [] +const promiseUpdateList: PromiseContext[] = [] const entitiyList: Entity[] = [] const dispatchPromises = () => { for (var i = promiseUpdateList.length - 1; i >= 0; i--) { - const finished = promiseUpdateList[i]() - if (finished) { + const p = promiseUpdateList[i] + p.update() + if (p.isFinished) { promiseUpdateList.splice(i, 1); } } } -export const makeUpdateablePromise = (updateFn: () => boolean) => { - let resFn: () => void - let rejFn: (reason: any) => void - const promise = new Promise((resolve, reject) => { - resFn = resolve - rejFn = reject - }); - const update = () => { - try { - const res = updateFn() - if(res) resFn() - return res - } catch(e: any){ - traceLog(LOG_INFO, "ERROR!") - rejFn(e) - return true +class PromiseContext { + + private _result: T | null = null; + public get result(): T | null { + return this._result; + } + private _error: any | null = null; + public get error(): any | null { + return this._error; + } + private _isFinished = false; + public get isFinished() { + return this._isFinished; + } + private _isCancellationRequested = false; + public get isCancellationRequested() { + return this._isCancellationRequested; + } + + constructor(private readonly resolveFn: (val: T | PromiseLike) => void, + private readonly rejectFn: (err: any) => void, + private readonly updateFn: (p: PromiseContext) => void){} + + update(){ + if(!this.isFinished){ + this.updateFn(this) } } - promiseUpdateList.unshift(update) + + resolve(val: T){ + this._result = val + this._isFinished = true + this.resolveFn(val) + } + + reject(reason: any){ + this._error = reason + this._isFinished = true + this.rejectFn(reason) + } + + cancel(){ + this._isCancellationRequested = true + } +} +export interface ExtendedPromise extends Promise { + context: PromiseContext +} +export const makeUpdateablePromise = (update: (ctx: PromiseContext) => void) => { + let context: PromiseContext + const promise = >new Promise((resolve, reject) => { + context = new PromiseContext(resolve,reject,update) + }); + promise.context = context! + promiseUpdateList.unshift(context!) return promise } diff --git a/examples/ts_game/src/index.ts b/examples/ts_game/src/index.ts index b0879f5..3511449 100644 --- a/examples/ts_game/src/index.ts +++ b/examples/ts_game/src/index.ts @@ -1,13 +1,11 @@ import { Choice } from "inkjs/engine/Choice"; -import { fadeIn, fadeOut, wait, waitClick, waitEntityClicked, waitFrame, waitKeyPressed } from "./animation"; -import { Builder, Clickable, addBehaviour, debugRectDrawBehaviour, makeCombined, setProp } from "./entity"; +import { fadeIn, fadeOut, move, wait, waitAnyClicked, waitClick } from "./timing"; +import { Builder, combine, withComponent } from "./entity"; import { entityAdd, entityRemove, runGame } from "./game"; -import { ClickableText as ClickableText, Text, makeClickableText as makeClickableText, makeParagraph, makeText as makeText } from "./text"; +import { ClickableText, makeClickableText, makeParagraph } from "./text"; import { Compiler } from "inkjs"; - - -runGame(800,400, "Typescript Game", async (quit) => { +runGame(800,400, "The Intercept", async (quit) => { const source = loadFileText("resources/intercept.ink") const c = new Compiler(source) const story = c.Compile() @@ -19,12 +17,7 @@ runGame(800,400, "Typescript Game", async (quit) => { color: fade(WHITE, 0) } - interface HasChoice { choice: Choice } - type ChoiceEntity = ClickableText & HasChoice - const makeChoice: Builder = makeCombined(makeClickableText, (obj: ClickableText & Partial) => { - setProp(obj, "choice", null) - return obj - }) + const makeChoice = combine(makeClickableText, withComponent<{ choice: Choice }>()) const text = makeParagraph({ ...textTemplate, @@ -37,9 +30,12 @@ runGame(800,400, "Typescript Game", async (quit) => { while(story.canContinue){ const txt = story.Continue() if(txt?.trim() !== ''){ - await fadeOut(text, 1, easeCubicOut) + move(text, new Vector2(32,64), 1, easeCubicIn) + await fadeOut(text, 1) text.text = txt! - await fadeIn(text, 1, easeCubicInOut) + text.position = new Vector2(32,0) + move(text, new Vector2(32,32), 1, easeCubicOut) + await fadeIn(text, 1) await waitClick() } } @@ -48,20 +44,22 @@ runGame(800,400, "Typescript Game", async (quit) => { ...textTemplate, choice: v, color: RAYWHITE, - //debugClickable: true, position: new Vector2(getScreenWidth()/2,getScreenHeight()/2+(textTemplate.size + 10)*i), text: `(${i}) ${v.text}`, })) for (const choice of choices) { entityAdd(choice) + choice.position.x += 32 + move(choice, new Vector2(choice.position.x-32,choice.position.y), 1, easeSineOut) fadeIn(choice, 1) await wait(0.5) } - let choiceIdx = -1 - await Promise.race(choices.map(x => waitEntityClicked(x).then(() => choiceIdx = x.choice.index))) + const choiceIdx = await waitAnyClicked(choices) traceLog(LOG_INFO, "Clicked: " + choiceIdx) - choices.forEach(x => entityRemove(x)) story.ChooseChoiceIndex(choiceIdx) + choices.forEach(x => { move(x, vector2Add(x.position,new Vector2(-32,0)), 1, easeSineIn); fadeOut(x, 1) }) + await wait(1) + choices.forEach(x => entityRemove(x)) } }) diff --git a/examples/ts_game/src/text.ts b/examples/ts_game/src/text.ts index c3f40ec..30afcb7 100644 --- a/examples/ts_game/src/text.ts +++ b/examples/ts_game/src/text.ts @@ -1,42 +1,41 @@ -import { Behaviour, Clickable, Entity, EntityOf, HasBehaviour, HasBoundingBox, HasColor, HasPosition, addBehaviour, makeBehaviour, makeBoundingBox, makeClickable, makeColor, makeColorRgb, makeCombined, makeEntity, makePosition, removeBehaviour, setProp, setPropFn } from "./entity" -import { fontLoad, resourceUnload, textureLoad } from "./resource" +import { Behaviour, HasMouseInteraction, Entity, HasBehaviour, HasBoundingBox, HasColor, HasPosition, withBehaviour, withBoundingBox, withColor, combine, makeEntity, withPosition, hasDefault, which, withComponent, Builder, withMouseInteraction, checksBoundingBoxClicks } from "./entity" +import { fontLoad, resourceUnload } from "./resource" // FONT -export interface HasFont extends HasBehaviour, HasColor { +export interface HasFont { font?: string, fontSize: number, fontSpacing: number fontResource?: Font fontResourceId?: string } -export const fontLoadBehaviour: Behaviour = { +export const loadsFont: Behaviour = { load: t => { t.fontResourceId = t.font ? t.font + ":" + t.fontSize : undefined t.fontResource = (t.font ? fontLoad(t.fontResourceId!) : getFontDefault()) }, unload: t => t.font ? resourceUnload(t.fontResourceId!) : undefined } -export const makeFont = makeCombined(makeBehaviour, makeColor, (obj: HasBehaviour & Partial) => { - setProp(obj, 'font', undefined) - setProp(obj, 'fontSize', 20) - setProp(obj, 'fontSpacing', 1) - addBehaviour(obj, fontLoadBehaviour) - return obj -}) +export const withFont = combine(withBehaviour, withComponent(x => { + hasDefault(x, 'fontSize', 20) + hasDefault(x, 'fontSpacing', 1) +}), which(loadsFont)) // TEXT -export interface Text extends Entity, HasFont, HasPosition { - text: string, +export interface HasText { + text: string } -export const makeText = makeCombined(makeEntity, makeFont, makePosition, (obj: Partial) => { - setProp(obj, 'text', "") - addBehaviour(obj, textBehaviour) - return obj -}) export const textDrawFn = (t: Text, text?: string, position?: Vector2) => drawTextEx(t.fontResource!, text ?? t.text, position ?? t.position, t.fontSize, t.fontSpacing, t.color); -export const textBehaviour: Behaviour = { +export const withText = withComponent(x => hasDefault(x,'text', '')) +export type Text = Entity & HasFont & HasPosition & HasColor & HasText +export const makeText: Builder = combine(makeEntity, withFont, withPosition, withColor, withText) + +// INLINE TEXT +export type InlineText = Entity & Text +export const drawsInlineText: Behaviour = { draw: textDrawFn } +export const makeInlineText: Builder = combine(makeText, which(drawsInlineText)) // PARAGRAPH export interface Line { @@ -72,7 +71,7 @@ export const breakTextLinesFn = (p: Paragraph) => { lines.push({ text: currentLine, width: lastw }); return lines; } -export const paragraphBehaviour: Behaviour = { +export const drawsParagraph: Behaviour = { update: p => { if (p._textCached !== p.text) { p.lines = breakTextLinesFn(p) @@ -87,17 +86,14 @@ export const paragraphBehaviour: Behaviour = { } } } -export const makeParagraph = makeCombined(makeText, (obj: Partial) => { - setProp(obj, 'lines', []) - setProp(obj, 'maxWidth', 100) - setProp(obj, "_textCached", "") - removeBehaviour(obj, textBehaviour) - addBehaviour(obj, paragraphBehaviour) - return obj -}) +export const makeParagraph = combine(makeText, withComponent(obj => { + hasDefault(obj, 'lines', []) + hasDefault(obj, 'maxWidth', 100) + hasDefault(obj, "_textCached", "") +}), which(drawsParagraph)) // CLICKABLE TEXT -export interface ClickableText extends Text, Clickable { +export interface HasTextDecoration { underlineWidth: number } export const calcTextBoundsFn = (obj: Text & HasBoundingBox) => { @@ -109,12 +105,14 @@ export const calcTextBoundsFn = (obj: Text & HasBoundingBox) => { } obj.boundingBox = rec } -export const clickableTextBehaviour: Behaviour = { +export type ClickableText = InlineText & HasMouseInteraction & HasTextDecoration & HasBoundingBox +export const drawsClickableText: Behaviour = { load: calcTextBoundsFn, draw: t => t.hasMouseOver ? drawRectangle(t.boundingBox.x, t.boundingBox.y + t.boundingBox.height, t.boundingBox.width, t.underlineWidth, t.color) : undefined } -export const makeClickableText = makeCombined(makeText, makeClickable, (obj: Entity & Clickable & Partial) => { - setProp(obj, 'underlineWidth', 1) - addBehaviour(obj, clickableTextBehaviour) - return obj -}) +export const makeClickableText: Builder = combine(makeInlineText, + withBoundingBox, + withMouseInteraction, + withComponent(obj => hasDefault(obj,'underlineWidth',1)), + which(checksBoundingBoxClicks), + which(drawsClickableText)) diff --git a/examples/ts_game/src/timing.ts b/examples/ts_game/src/timing.ts new file mode 100644 index 0000000..ee917fd --- /dev/null +++ b/examples/ts_game/src/timing.ts @@ -0,0 +1,58 @@ +import { HasMouseInteraction, HasColor, HasPosition } from "./entity" +import { makeUpdateablePromise } from "./game" + +export type easeFunc = (t: number, a: number, b: number, d: number) => number + +export const interpolate = (a: number, b: number, d: number, setter: (v: number) => void, fn: easeFunc) => { + const start = getTime() + const delta = b - a + return makeUpdateablePromise(ctx => { + const cur = getTime()-start + if(cur < d && !ctx.isCancellationRequested){ + setter(fn(cur, a, delta, d)) + } else { + setter(b) + ctx.resolve() + } + }) +} +export const interpolateVec2 = (a: Vector2, b: Vector2, d: number, setter: (v: Vector2) => void, fn: easeFunc) => { + const start = getTime() + const delta = vector2Subtract(b, a) + return makeUpdateablePromise(ctx => { + const cur = getTime()-start + if(cur < d && !ctx.isCancellationRequested){ + const x = fn(cur, a.x, delta.x, d) + const y = fn(cur, a.y, delta.y, d) + setter(new Vector2(x,y)) + } else { + setter(b) + ctx.resolve() + } + }) +} + +export const waitCondition = (predicate: () => boolean) => makeUpdateablePromise(ctx => { + if(predicate() || ctx.isCancellationRequested){ + ctx.resolve() + } +}) +export const waitFirst = (list: T[], predicate: (item: T) => boolean) => makeUpdateablePromise(ctx => { + let idx = list.findIndex(x => predicate(x)) + if(idx !== -1 || ctx.isCancellationRequested) ctx.resolve(idx) +}) + + +export const wait = (time: number) => { + const start = getTime() + return waitCondition(() => (getTime()-start) >= time) +} + +export const waitKeyPressed = (key: number) => waitCondition(() => isKeyPressed(key)) +export const waitClick = (button: number = MOUSE_BUTTON_LEFT) => waitCondition(() => isMouseButtonDown(button)) +export const waitClicked = (entity: HasMouseInteraction) => waitCondition(() => entity.isClicked) +export const waitAnyClicked = (entites: HasMouseInteraction[]) => waitFirst(entites, x => x.isClicked) + +export const fadeIn = (c: HasColor, time: number, easeFunc = easeLinearNone) => interpolate(0, 1, time, (v) => c.color = fade(c.color, v), easeFunc) +export const fadeOut = (c: HasColor, time: number, easeFunc = easeLinearNone) => interpolate(1, 0, time, (v) => c.color = fade(c.color, v), easeFunc) +export const move = (p: HasPosition, to: Vector2, time: number, easeFunc = easeLinearNone) => interpolateVec2(p.position, to, time, v2 => p.position = v2, easeFunc) \ No newline at end of file