From 54501a136596f342e361bb4d2d814166e2311efc Mon Sep 17 00:00:00 2001 From: Jason Reed Date: Mon, 20 Nov 2023 17:35:49 -0500 Subject: [PATCH] Refactor animations Also make point decay animation a little snappier. --- src/core/animations.ts | 26 +++++++++++++++++++++ src/core/state-helpers.ts | 18 ++++---------- src/core/state.ts | 6 +---- src/ui/drawAnimation.ts | 35 ++++++++++++++++++++++++++++ src/ui/drawBonus.ts | 19 +++++++++++++++ src/ui/render.ts | 49 ++++----------------------------------- 6 files changed, 89 insertions(+), 64 deletions(-) create mode 100644 src/core/animations.ts create mode 100644 src/ui/drawAnimation.ts create mode 100644 src/ui/drawBonus.ts diff --git a/src/core/animations.ts b/src/core/animations.ts new file mode 100644 index 0000000..4a27020 --- /dev/null +++ b/src/core/animations.ts @@ -0,0 +1,26 @@ +import { Point } from "../util/types"; +import { vadd } from "../util/vutil"; + +export type Animation = + | { t: 'explosion', start_ms: number, duration_ms: number, center_in_world: Point, radius: number } + | { t: 'point-decay', start_ms: number, duration_ms: number, p_in_world_int: Point } + ; + +export function mkPointDecayAnimation(p: Point): Animation { + return { + t: 'point-decay', + duration_ms: 500, + p_in_world_int: p, + start_ms: Date.now(), + }; +} + +export function mkExplosionAnimation(p: Point, radius: number): Animation { + return { + t: 'explosion', + center_in_world: vadd(p, { x: 0.5, y: 0.5 }), + duration_ms: (radius + 1) * 250, + start_ms: Date.now(), + radius, + } +} diff --git a/src/core/state-helpers.ts b/src/core/state-helpers.ts index 0a873d5..c52c77a 100644 --- a/src/core/state-helpers.ts +++ b/src/core/state-helpers.ts @@ -1,3 +1,4 @@ +import { mkPointDecayAnimation, Animation, mkExplosionAnimation } from './animations'; import { DragWidgetPoint, WidgetPoint } from "../ui/widget-helpers"; import { logger } from "../util/debug"; import { produce } from "../util/produce"; @@ -9,7 +10,7 @@ import { PanicData, PauseData } from "./clock"; import { getLetterSample } from "./distribution"; import { checkConnected, checkGridWords, mkGridOfMainTiles } from "./grid"; import { Layer, Overlay, getOverlay, getOverlayLayer, mkOverlayFrom, overlayAny, overlayForEach, overlayPoints, setOverlay } from "./layer"; -import { Animation, GameState, Location, MainTile, SelectionState, Tile, TileEntity, getBonusLayer } from "./state"; +import { GameState, Location, MainTile, SelectionState, Tile, TileEntity, getBonusLayer } from "./state"; import { addHandTile, addWorldTile, ensureTileId, get_hand_tiles, get_main_tiles, get_tiles, removeTile } from "./tile-helpers"; export function addWorldTiles(state: GameState, tiles: Tile[]): GameState { @@ -81,13 +82,7 @@ function killTileOfState(state: GameState, wp: DragWidgetPoint, radius: number, switch (wp.t) { case 'world': { const p_in_world_int = vint(wp.p_in_local); - const anim: Animation = { - t: 'explosion', - center_in_world: vadd(p_in_world_int, { x: 0.5, y: 0.5 }), - duration_ms: (radius + 1) * 250, - start_ms: Date.now(), - radius, - } + const anim: Animation = mkExplosionAnimation(p_in_world_int, radius); function tileAt(p: Point): MainTile | undefined { return get_main_tiles(state).find(tile => vequal(tile.loc.p_in_world_int, p)); @@ -162,12 +157,7 @@ function resolveValid(state: GameState): GameState { scorings.forEach(p => { setOverlay(s.coreState.bonusOverlay, p, 'empty'); s.coreState.score++; - s.coreState.animations.push({ - t: 'point-decay', - duration_ms: 1000, - p_in_world_int: p, - start_ms: Date.now(), - }); + s.coreState.animations.push(mkPointDecayAnimation(p)); }); }); } diff --git a/src/core/state.ts b/src/core/state.ts index e4df871..c2e3173 100644 --- a/src/core/state.ts +++ b/src/core/state.ts @@ -1,3 +1,4 @@ +import { Animation } from './animations'; import { SE2 } from '../util/se2'; import { Point } from '../util/types'; import { Bonus, bonusGenerator, mkBonusLayer } from './bonus'; @@ -71,11 +72,6 @@ export type SelectionState = { selectedIds: string[], }; -export type Animation = - | { t: 'explosion', start_ms: number, duration_ms: number, center_in_world: Point, radius: number } - | { t: 'point-decay', start_ms: number, duration_ms: number, p_in_world_int: Point } - ; - export type CoreState = { animations: Animation[], toolIndex: number, diff --git a/src/ui/drawAnimation.ts b/src/ui/drawAnimation.ts new file mode 100644 index 0000000..803cf4d --- /dev/null +++ b/src/ui/drawAnimation.ts @@ -0,0 +1,35 @@ +import { Animation } from '../core/animations'; +import { SE2 } from '../util/se2'; +import { apply_to_rect } from "../util/se2-extra"; +import { Point } from "../util/types"; +import { unreachable } from "../util/util"; +import { vscale, vsub } from "../util/vutil"; +import { drawBonus } from "./drawBonus"; + +export function drawAnimation(d: CanvasRenderingContext2D, pan_canvas_from_world: SE2, time_ms: number, anim: Animation): void { + switch (anim.t) { + case 'explosion': { + const radius_in_world = (2 * anim.radius + 1) * 0.5 * (time_ms - anim.start_ms) / anim.duration_ms; + const radvec: Point = { x: radius_in_world, y: radius_in_world }; + const rect_in_canvas = apply_to_rect(pan_canvas_from_world, { + p: vsub(anim.center_in_world, radvec), sz: vscale(radvec, 2) + }); + d.strokeStyle = '#ff0000'; + d.lineWidth = 3; + d.beginPath(); + d.arc(rect_in_canvas.p.x + rect_in_canvas.sz.x / 2, + rect_in_canvas.p.y + rect_in_canvas.sz.y / 2, + rect_in_canvas.sz.y / 2, + 0, 360, + ); + d.stroke(); + return; + } break; + case 'point-decay': { + const fraction = Math.min(1, Math.max(0, 1 - (time_ms - anim.start_ms) / anim.duration_ms)); + drawBonus(d, pan_canvas_from_world, anim.p_in_world_int, fraction); + return; + } break; + } + unreachable(anim); +} diff --git a/src/ui/drawBonus.ts b/src/ui/drawBonus.ts new file mode 100644 index 0000000..525c4a3 --- /dev/null +++ b/src/ui/drawBonus.ts @@ -0,0 +1,19 @@ +import { SE2 } from '../util/se2'; +import { apply_to_rect } from "../util/se2-extra"; +import { Point } from "../util/types"; +import { midpointOfRect } from "../util/util"; + +export function drawBonus(d: CanvasRenderingContext2D, pan_canvas_from_world: SE2, p: Point, fraction: number = 1) { + const rect_in_canvas = apply_to_rect(pan_canvas_from_world, { p, sz: { x: 1, y: 1 } }); + d.fillStyle = 'rgba(0,0,255,0.5)'; + d.beginPath(); + const m = midpointOfRect(rect_in_canvas); + d.moveTo(m.x, m.y); + d.arc(rect_in_canvas.p.x + rect_in_canvas.sz.x / 2, + rect_in_canvas.p.y + rect_in_canvas.sz.y / 2, + rect_in_canvas.sz.y * 0.4, + 0, 2 * Math.PI * fraction, + ); + d.fill(); + +} diff --git a/src/ui/render.ts b/src/ui/render.ts index 4603e93..cfd8c69 100644 --- a/src/ui/render.ts +++ b/src/ui/render.ts @@ -2,14 +2,16 @@ import { getAssets } from "../core/assets"; import { getPanicFraction } from "../core/clock"; import { LocatedWord, getGrid } from "../core/grid"; import { getOverlay, getOverlayLayer } from "../core/layer"; -import { Animation, GameState, TileEntity, getBonusLayer } from "../core/state"; +import { GameState, TileEntity, getBonusLayer } from "../core/state"; import { getTileId, get_hand_tiles, get_main_tiles, isSelectedForDrag } from "../core/tile-helpers"; import { fillRect, fillText, strokeRect } from "../util/dutil"; import { SE2, apply, compose, inverse, translate } from '../util/se2'; import { apply_to_rect } from "../util/se2-extra"; import { Point, Rect } from "../util/types"; -import { boundRect, midpointOfRect, unreachable } from "../util/util"; +import { boundRect, midpointOfRect } from "../util/util"; import { vadd, vm, vscale, vsub, vtrans } from "../util/vutil"; +import { drawAnimation } from "./drawAnimation"; +import { drawBonus } from "./drawBonus"; import { CanvasInfo } from "./use-canvas"; import { canvas_from_drag_tile, pan_canvas_from_world_of_state } from "./view-helpers"; import { canvas_bds_in_canvas, canvas_from_hand, canvas_from_toolbar, hand_bds_in_canvas, pause_button_bds_in_canvas, toolbar_bds_in_canvas, world_bds_in_canvas } from "./widget-helpers"; @@ -24,34 +26,6 @@ export function paintWithScale(ci: CanvasInfo, state: GameState) { const backgroundGray = '#eeeeee'; -function drawAnimation(d: CanvasRenderingContext2D, pan_canvas_from_world: SE2, time_ms: number, anim: Animation): void { - switch (anim.t) { - case 'explosion': { - const radius_in_world = (2 * anim.radius + 1) * 0.5 * (time_ms - anim.start_ms) / anim.duration_ms; - const radvec: Point = { x: radius_in_world, y: radius_in_world }; - const rect_in_canvas = apply_to_rect(pan_canvas_from_world, { - p: vsub(anim.center_in_world, radvec), sz: vscale(radvec, 2) - }); - d.strokeStyle = '#ff0000'; - d.lineWidth = 3; - d.beginPath(); - d.arc(rect_in_canvas.p.x + rect_in_canvas.sz.x / 2, - rect_in_canvas.p.y + rect_in_canvas.sz.y / 2, - rect_in_canvas.sz.y / 2, - 0, 360, - ); - d.stroke(); - return; - } break; - case 'point-decay': { - const fraction = Math.min(1, Math.max(0, 1 - (time_ms - anim.start_ms) / anim.duration_ms)); - drawBonus(d, pan_canvas_from_world, anim.p_in_world_int, fraction); - return; - } break; - } - unreachable(anim); -} - export function drawPausedScreen(ci: CanvasInfo, state: GameState) { const { d } = ci; @@ -62,21 +36,6 @@ export function drawPausedScreen(ci: CanvasInfo, state: GameState) { fillText(d, "paused", midpointOfRect(canvas_bds_in_canvas), 'black', '48px sans-serif'); } -function drawBonus(d: CanvasRenderingContext2D, pan_canvas_from_world: SE2, p: Point, fraction: number = 1) { - const rect_in_canvas = apply_to_rect(pan_canvas_from_world, { p, sz: { x: 1, y: 1 } }); - d.fillStyle = 'rgba(0,0,255,0.5)'; - d.beginPath(); - const m = midpointOfRect(rect_in_canvas); - d.moveTo(m.x, m.y); - d.arc(rect_in_canvas.p.x + rect_in_canvas.sz.x / 2, - rect_in_canvas.p.y + rect_in_canvas.sz.y / 2, - rect_in_canvas.sz.y * 0.4, - 0, 2 * Math.PI * fraction, - ); - d.fill(); - -} - export function rawPaint(ci: CanvasInfo, state: GameState) { const cs = state.coreState;