@classytic/stage 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +7 -3
- package/dist/assets/index.mjs +0 -1
- package/dist/assets/kit/glyphs.d.mts +5 -5
- package/dist/assets/kit/glyphs.mjs +4 -4
- package/dist/builder/Palette.mjs +50 -89
- package/dist/builder/SceneBuilder.mjs +18 -76
- package/dist/builder/editor.d.mts +2 -1
- package/dist/builder/editor.mjs +14 -4
- package/dist/builder/tools.d.mts +1 -1
- package/dist/builder/tools.mjs +1 -1
- package/dist/chem/index.d.mts +5 -5
- package/dist/chem/index.mjs +4 -4
- package/dist/circuit/index.d.mts +92 -0
- package/dist/circuit/index.mjs +333 -0
- package/dist/core/clock.d.mts +1 -1
- package/dist/core/clock.mjs +3 -3
- package/dist/core/control.d.mts +1 -1
- package/dist/core/control.mjs +2 -2
- package/dist/core/coords.d.mts +4 -4
- package/dist/core/coords.mjs +1 -1
- package/dist/core/index.d.mts +2 -1
- package/dist/core/index.mjs +2 -1
- package/dist/core/learner.d.mts +1 -1
- package/dist/core/learner.mjs +1 -1
- package/dist/core/math.d.mts +26 -0
- package/dist/core/math.mjs +37 -0
- package/dist/core/motion.d.mts +6 -6
- package/dist/core/motion.mjs +6 -6
- package/dist/core/richText.d.mts +2 -2
- package/dist/core/vec.d.mts +1 -1
- package/dist/field/index.d.mts +11 -3
- package/dist/field/index.mjs +20 -3
- package/dist/finance/bizsim.d.mts +93 -0
- package/dist/finance/bizsim.mjs +117 -0
- package/dist/finance/index.d.mts +118 -0
- package/dist/finance/index.mjs +203 -0
- package/dist/index.d.mts +6 -3
- package/dist/index.mjs +6 -4
- package/dist/interaction/MovableDot.d.mts +17 -1
- package/dist/interaction/MovableDot.mjs +160 -43
- package/dist/interaction/index.d.mts +2 -1
- package/dist/interaction/index.mjs +2 -1
- package/dist/interaction/useDraggable.d.mts +1 -1
- package/dist/interaction/useDraggable.mjs +55 -5
- package/dist/interaction/usePressSpring.d.mts +24 -0
- package/dist/interaction/usePressSpring.mjs +67 -0
- package/dist/logic/ast.d.mts +1 -1
- package/dist/logic/index.mjs +8 -3
- package/dist/logic/minimize.d.mts +2 -2
- package/dist/logic/minimize.mjs +56 -18
- package/dist/logic/table.mjs +1 -1
- package/dist/math/ast.d.mts +1 -1
- package/dist/math/ast.mjs +1 -1
- package/dist/math/calculus.d.mts +22 -1
- package/dist/math/calculus.mjs +224 -11
- package/dist/math/compile.d.mts +8 -0
- package/dist/math/compile.mjs +40 -0
- package/dist/math/defs.mjs +1 -1
- package/dist/math/index.d.mts +5 -4
- package/dist/math/index.mjs +5 -4
- package/dist/math/latex.mjs +8 -0
- package/dist/math/parse.mjs +2 -2
- package/dist/math/tokenize.mjs +1 -1
- package/dist/primitives/CanvasLayer.mjs +3 -3
- package/dist/primitives/Dot.d.mts +2 -15
- package/dist/primitives/Dot.mjs +6 -4
- package/dist/primitives/Grid.d.mts +41 -14
- package/dist/primitives/Grid.mjs +100 -22
- package/dist/primitives/Label.d.mts +2 -15
- package/dist/primitives/Label.mjs +4 -3
- package/dist/primitives/Lines.d.mts +4 -32
- package/dist/primitives/Lines.mjs +10 -8
- package/dist/primitives/Shapes.d.mts +5 -43
- package/dist/primitives/Shapes.mjs +12 -10
- package/dist/primitives/Tex.mjs +2 -2
- package/dist/primitives/index.d.mts +2 -2
- package/dist/primitives/index.mjs +2 -2
- package/dist/primitives/props.mjs +31 -0
- package/dist/scene/Scene.d.mts +6 -1
- package/dist/scene/Scene.mjs +16 -41
- package/dist/scene/assets.d.mts +2 -2
- package/dist/scene/assets.mjs +2 -2
- package/dist/scene/commands.d.mts +10 -1
- package/dist/scene/commands.mjs +42 -2
- package/dist/scene/evaluators.mjs +2 -2
- package/dist/scene/index.d.mts +2 -2
- package/dist/scene/index.mjs +2 -2
- package/dist/scene/migrate.mjs +1 -1
- package/dist/scene/sims.mjs +1 -1
- package/dist/scene/types.d.mts +4 -4
- package/dist/sim/particles.d.mts +2 -2
- package/dist/sim/rate.d.mts +4 -4
- package/dist/sim/rate.mjs +1 -1
- package/dist/sim/registry.d.mts +1 -1
- package/dist/sim/sampler.d.mts +5 -5
- package/dist/sim/thermal.d.mts +3 -3
- package/dist/sim/thermal.mjs +2 -2
- package/dist/sim/types.d.mts +2 -2
- package/dist/sim/wave.d.mts +2 -2
- package/dist/sim/wave.mjs +2 -2
- package/dist/steps/index.d.mts +1 -1
- package/dist/steps/index.mjs +3 -3
- package/dist/thermo/index.d.mts +1 -1
- package/dist/thermo/index.mjs +1 -1
- package/dist/view/Stage.mjs +6 -13
- package/dist/view/useInView.mjs +1 -1
- package/package.json +37 -25
- package/styles.css +141 -1
- package/dist/assets/kit/index.mjs +0 -4
package/dist/core/motion.mjs
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
//#region src/core/motion.ts
|
|
2
2
|
/**
|
|
3
|
-
* Motion core
|
|
3
|
+
* Motion core, the "physics of feel" layer. Pure, dependency-free functions that
|
|
4
4
|
* turn time into eased / springy / oscillating values. They consume the `dtMs`
|
|
5
5
|
* (or `timeMs`) you already receive from `useFrameLoop`; they do NOT own a loop,
|
|
6
|
-
* schedule frames, or touch React
|
|
6
|
+
* schedule frames, or touch React, so they stay deterministic (same time → same
|
|
7
7
|
* value) and work identically under a Remotion fixed-frame driver.
|
|
8
8
|
*
|
|
9
9
|
* Discipline (mirrors clock.ts): for video-determinism prefer the time-driven
|
|
10
10
|
* helpers (`oscillate`, `timeline`) that are a pure function of `timeMs`. `spring`
|
|
11
|
-
* is stateful (it integrates a velocity)
|
|
11
|
+
* is stateful (it integrates a velocity), fine for interactive feel, but it will
|
|
12
12
|
* not reproduce frame-for-frame across drivers, so don't use it for rendered video.
|
|
13
13
|
*/
|
|
14
14
|
/** Snap-to-end gate: honor the user's reduced-motion preference (SSR-safe). */
|
|
@@ -30,14 +30,14 @@ const ease = {
|
|
|
30
30
|
},
|
|
31
31
|
quintOut: (t) => 1 - (1 - clamp01(t)) ** 5,
|
|
32
32
|
sineInOut: (t) => -(Math.cos(Math.PI * clamp01(t)) - 1) / 2,
|
|
33
|
-
/** Overshoots then settles
|
|
33
|
+
/** Overshoots then settles, good for "pop" on appear. */
|
|
34
34
|
backOut: (t) => {
|
|
35
35
|
const c1 = 1.70158;
|
|
36
36
|
const c3 = 2.70158;
|
|
37
37
|
t = clamp01(t);
|
|
38
38
|
return 1 + c3 * (t - 1) ** 3 + c1 * (t - 1) ** 2;
|
|
39
39
|
},
|
|
40
|
-
/** Springy overshoot with decaying wobble
|
|
40
|
+
/** Springy overshoot with decaying wobble, for playful reveals. */
|
|
41
41
|
elasticOut: (t) => {
|
|
42
42
|
t = clamp01(t);
|
|
43
43
|
if (t === 0 || t === 1) return t;
|
|
@@ -63,7 +63,7 @@ function spring(current, target, vel, dtSec, opts = {}) {
|
|
|
63
63
|
vel.current += accel * dt;
|
|
64
64
|
return current + vel.current * dt;
|
|
65
65
|
}
|
|
66
|
-
/** A pure periodic signal of `timeSec
|
|
66
|
+
/** A pure periodic signal of `timeSec`, the engine under AC sources, pendulums, waves. */
|
|
67
67
|
function oscillate(timeSec, opts = {}) {
|
|
68
68
|
const { amp = 1, freq = 1, phase = 0, shape = "sine" } = opts;
|
|
69
69
|
const turns = freq * timeSec + phase;
|
package/dist/core/richText.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
//#region src/core/richText.d.ts
|
|
2
2
|
/**
|
|
3
|
-
* richText
|
|
3
|
+
* richText, the ONE grammar for inline subscript/superscript markup, shared by
|
|
4
4
|
* every renderer so the parsing lives in a single place and can't drift:
|
|
5
5
|
* • stage's SVG `<Label>` turns spans into <tspan> (baseline-shifted), and
|
|
6
6
|
* • labs' HTML `<RichText>` turns them into <sub>/<sup>.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `_x` / `^x` → a run of the SAME character class as the char right
|
|
10
10
|
* after the marker (all-letters OR all-digits). So
|
|
11
11
|
* `V_RC` → subscript "RC", `H_2O` → subscript "2" then
|
|
12
|
-
* base "O", `v_max` → subscript "max"
|
|
12
|
+
* base "O", `v_max` → subscript "max", no braces needed.
|
|
13
13
|
* `_{abc}` / `^{ab}` → an explicit group, taken verbatim (use for mixed runs
|
|
14
14
|
* or symbols, e.g. `10^{-3}`, `x^{n+1}`)
|
|
15
15
|
* a lone `_` or `^` → stays literal
|
package/dist/core/vec.d.mts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* 2D vector + affine-matrix algebra. Points are {x,y} objects (serialize cleanly
|
|
4
4
|
* in the scene JSON). Matrices are SVG-order tuples [a,b,c,d,e,f] mapping
|
|
5
5
|
* (x,y) → (a·x + c·y + e, b·x + d·y + f), so `mat.toSvg` is a direct
|
|
6
|
-
* `matrix(a b c d e f)` attribute string
|
|
6
|
+
* `matrix(a b c d e f)` attribute string, never a CSS variable (a CSS transform
|
|
7
7
|
* is not reliably composed into getScreenCTM()).
|
|
8
8
|
*/
|
|
9
9
|
interface Vec2 {
|
package/dist/field/index.d.mts
CHANGED
|
@@ -23,6 +23,14 @@ interface Bounds {
|
|
|
23
23
|
/** Net field vector at point `p` from all sources (superposition). */
|
|
24
24
|
declare function fieldAt(sources: readonly FieldSource[], p: Vec2): Vec2;
|
|
25
25
|
declare function fieldMag(sources: readonly FieldSource[], p: Vec2): number;
|
|
26
|
+
/**
|
|
27
|
+
* Scalar electric potential at `p`: V = Σ k·q / r (superposition). This is the
|
|
28
|
+
* companion to `fieldAt` for potential / equipotential / work labs, where the work
|
|
29
|
+
* to move a charge Q from A to B is W = Q·(V_A − V_B), path-independent. `wire`
|
|
30
|
+
* sources have no scalar potential and are skipped. Same softening as `fieldAt`,
|
|
31
|
+
* so V stays finite at a source.
|
|
32
|
+
*/
|
|
33
|
+
declare function potentialAt(sources: readonly FieldSource[], p: Vec2, k?: number): number;
|
|
26
34
|
interface TraceOpts {
|
|
27
35
|
/** Follow the field (+1) or run against it (−1). Default +1. */
|
|
28
36
|
dir?: number;
|
|
@@ -41,13 +49,13 @@ interface FieldLinesOpts extends TraceOpts {
|
|
|
41
49
|
/** Seed radius around a source. Default 0.2. */
|
|
42
50
|
seed?: number;
|
|
43
51
|
}
|
|
44
|
-
/** Convenience: a full set of field lines
|
|
52
|
+
/** Convenience: a full set of field lines, radial from each charge/pole, circular
|
|
45
53
|
* loops around each wire. Each line carries the sign of its originating source. */
|
|
46
54
|
declare function fieldLines(sources: readonly FieldSource[], opts?: FieldLinesOpts): {
|
|
47
55
|
points: Vec2[];
|
|
48
56
|
sign: number;
|
|
49
57
|
}[];
|
|
50
|
-
/** Sample the field on a regular grid
|
|
58
|
+
/** Sample the field on a regular grid, for a quiver / arrow plot. */
|
|
51
59
|
declare function fieldGrid(sources: readonly FieldSource[], bounds: Bounds, nx: number, ny: number): {
|
|
52
60
|
at: Vec2;
|
|
53
61
|
v: Vec2;
|
|
@@ -55,4 +63,4 @@ declare function fieldGrid(sources: readonly FieldSource[], bounds: Bounds, nx:
|
|
|
55
63
|
/** Build a bar magnet as an N(+)/S(−) pole pair, centred at `center` along `dir`. */
|
|
56
64
|
declare function barMagnet(center: Vec2, dir: Vec2, strength?: number, length?: number): [PointCharge, PointCharge];
|
|
57
65
|
//#endregion
|
|
58
|
-
export { Bounds, FieldLinesOpts, FieldSource, LineCurrent, PointCharge, TraceOpts, barMagnet, fieldAt, fieldGrid, fieldLines, fieldMag, traceLine };
|
|
66
|
+
export { Bounds, FieldLinesOpts, FieldSource, LineCurrent, PointCharge, TraceOpts, barMagnet, fieldAt, fieldGrid, fieldLines, fieldMag, potentialAt, traceLine };
|
package/dist/field/index.mjs
CHANGED
|
@@ -26,6 +26,23 @@ function fieldMag(sources, p) {
|
|
|
26
26
|
const v = fieldAt(sources, p);
|
|
27
27
|
return Math.hypot(v.x, v.y);
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Scalar electric potential at `p`: V = Σ k·q / r (superposition). This is the
|
|
31
|
+
* companion to `fieldAt` for potential / equipotential / work labs, where the work
|
|
32
|
+
* to move a charge Q from A to B is W = Q·(V_A − V_B), path-independent. `wire`
|
|
33
|
+
* sources have no scalar potential and are skipped. Same softening as `fieldAt`,
|
|
34
|
+
* so V stays finite at a source.
|
|
35
|
+
*/
|
|
36
|
+
function potentialAt(sources, p, k = 1) {
|
|
37
|
+
let v = 0;
|
|
38
|
+
for (const s of sources) {
|
|
39
|
+
if (s.kind !== "point") continue;
|
|
40
|
+
const dx = p.x - s.at.x, dy = p.y - s.at.y;
|
|
41
|
+
const r = Math.sqrt(dx * dx + dy * dy + SOFT);
|
|
42
|
+
v += k * s.q / r;
|
|
43
|
+
}
|
|
44
|
+
return v;
|
|
45
|
+
}
|
|
29
46
|
function unit(v, dir) {
|
|
30
47
|
const m = Math.hypot(v.x, v.y);
|
|
31
48
|
if (m < 1e-9) return null;
|
|
@@ -64,7 +81,7 @@ function traceLine(sources, start, opts = {}) {
|
|
|
64
81
|
}
|
|
65
82
|
return pts;
|
|
66
83
|
}
|
|
67
|
-
/** Convenience: a full set of field lines
|
|
84
|
+
/** Convenience: a full set of field lines, radial from each charge/pole, circular
|
|
68
85
|
* loops around each wire. Each line carries the sign of its originating source. */
|
|
69
86
|
function fieldLines(sources, opts = {}) {
|
|
70
87
|
const { perSource = 12, seed = .2, ...trace } = opts;
|
|
@@ -114,7 +131,7 @@ function fieldLines(sources, opts = {}) {
|
|
|
114
131
|
}
|
|
115
132
|
return out;
|
|
116
133
|
}
|
|
117
|
-
/** Sample the field on a regular grid
|
|
134
|
+
/** Sample the field on a regular grid, for a quiver / arrow plot. */
|
|
118
135
|
function fieldGrid(sources, bounds, nx, ny) {
|
|
119
136
|
const out = [];
|
|
120
137
|
const dx = (bounds.xMax - bounds.xMin) / (nx + 1);
|
|
@@ -153,4 +170,4 @@ function barMagnet(center, dir, strength = 1, length = 1.6) {
|
|
|
153
170
|
}
|
|
154
171
|
|
|
155
172
|
//#endregion
|
|
156
|
-
export { barMagnet, fieldAt, fieldGrid, fieldLines, fieldMag, traceLine };
|
|
173
|
+
export { barMagnet, fieldAt, fieldGrid, fieldLines, fieldMag, potentialAt, traceLine };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
//#region src/finance/bizsim.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* bizsim — a tiny, always-balanced business-simulation engine (part of `finance`).
|
|
4
|
+
*
|
|
5
|
+
* This is the PRIMITIVE a "run a business" story lesson is built on: a teacher/creator
|
|
6
|
+
* authors a scenario as a sequence of business ACTIONS (invest capital, take a loan,
|
|
7
|
+
* buy equipment, buy stock, sell, pay an expense, depreciate, close the period). Each
|
|
8
|
+
* action is a pure, DOUBLE-ENTRY-BALANCED transform of the business state, so the
|
|
9
|
+
* derived income statement and balance sheet are always correct and the balance sheet
|
|
10
|
+
* always balances — no accounting knowledge required to author, and it scales all the
|
|
11
|
+
* way to financial analysis (ratios) at the end.
|
|
12
|
+
*
|
|
13
|
+
* The engine is content-neutral: it knows nothing about samosas, apps or coffee — the
|
|
14
|
+
* creator supplies the story and the numbers.
|
|
15
|
+
*/
|
|
16
|
+
interface BizState {
|
|
17
|
+
cash: number;
|
|
18
|
+
/** Stock on hand: quantity and its value at cost. */
|
|
19
|
+
inventoryQty: number;
|
|
20
|
+
inventoryValue: number;
|
|
21
|
+
/** Fixed assets at book value. */
|
|
22
|
+
equipment: number;
|
|
23
|
+
/** Liabilities (loans). */
|
|
24
|
+
loan: number;
|
|
25
|
+
/** Owner capital contributed. */
|
|
26
|
+
capital: number;
|
|
27
|
+
/** Accumulated profit from closed periods. */
|
|
28
|
+
retained: number;
|
|
29
|
+
/** Current open-period tallies (become net profit at closePeriod). */
|
|
30
|
+
revenue: number;
|
|
31
|
+
cogs: number;
|
|
32
|
+
expenses: number;
|
|
33
|
+
}
|
|
34
|
+
declare const initialBiz: (over?: Partial<BizState>) => BizState;
|
|
35
|
+
type BizAction = {
|
|
36
|
+
type: 'invest';
|
|
37
|
+
amount: number;
|
|
38
|
+
} | {
|
|
39
|
+
type: 'loan';
|
|
40
|
+
amount: number;
|
|
41
|
+
} | {
|
|
42
|
+
type: 'repay';
|
|
43
|
+
amount: number;
|
|
44
|
+
} | {
|
|
45
|
+
type: 'buyEquipment';
|
|
46
|
+
amount: number;
|
|
47
|
+
} | {
|
|
48
|
+
type: 'depreciate';
|
|
49
|
+
amount: number;
|
|
50
|
+
} | {
|
|
51
|
+
type: 'buyStock';
|
|
52
|
+
qty: number;
|
|
53
|
+
unitCost: number;
|
|
54
|
+
} | {
|
|
55
|
+
type: 'sell';
|
|
56
|
+
qty: number;
|
|
57
|
+
price: number;
|
|
58
|
+
} | {
|
|
59
|
+
type: 'expense';
|
|
60
|
+
amount: number;
|
|
61
|
+
} | {
|
|
62
|
+
type: 'closePeriod';
|
|
63
|
+
};
|
|
64
|
+
/** Apply one action; pure and balance-preserving (assets = liabilities + equity). */
|
|
65
|
+
declare function applyBiz(s: BizState, a: BizAction): BizState;
|
|
66
|
+
/** Run a whole authored scenario from an (optional) starting state. */
|
|
67
|
+
declare function runBiz(actions: BizAction[], init?: Partial<BizState>): BizState;
|
|
68
|
+
interface IncomeStatement {
|
|
69
|
+
revenue: number;
|
|
70
|
+
cogs: number;
|
|
71
|
+
grossProfit: number;
|
|
72
|
+
expenses: number;
|
|
73
|
+
netProfit: number;
|
|
74
|
+
}
|
|
75
|
+
/** Income statement for the current open period. */
|
|
76
|
+
declare function incomeStatement(s: BizState): IncomeStatement;
|
|
77
|
+
interface BalanceSheet {
|
|
78
|
+
cash: number;
|
|
79
|
+
inventory: number;
|
|
80
|
+
equipment: number;
|
|
81
|
+
totalAssets: number;
|
|
82
|
+
loan: number;
|
|
83
|
+
capital: number;
|
|
84
|
+
retained: number;
|
|
85
|
+
openProfit: number;
|
|
86
|
+
totalEquity: number;
|
|
87
|
+
totalLiabilitiesEquity: number;
|
|
88
|
+
balances: boolean;
|
|
89
|
+
}
|
|
90
|
+
/** Balance sheet; `balances` is always true by construction (a self-check). */
|
|
91
|
+
declare function balanceSheet(s: BizState): BalanceSheet;
|
|
92
|
+
//#endregion
|
|
93
|
+
export { BalanceSheet, BizAction, BizState, IncomeStatement, applyBiz, balanceSheet, incomeStatement, initialBiz, runBiz };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
//#region src/finance/bizsim.ts
|
|
2
|
+
const initialBiz = (over = {}) => ({
|
|
3
|
+
cash: 0,
|
|
4
|
+
inventoryQty: 0,
|
|
5
|
+
inventoryValue: 0,
|
|
6
|
+
equipment: 0,
|
|
7
|
+
loan: 0,
|
|
8
|
+
capital: 0,
|
|
9
|
+
retained: 0,
|
|
10
|
+
revenue: 0,
|
|
11
|
+
cogs: 0,
|
|
12
|
+
expenses: 0,
|
|
13
|
+
...over
|
|
14
|
+
});
|
|
15
|
+
/** Apply one action; pure and balance-preserving (assets = liabilities + equity). */
|
|
16
|
+
function applyBiz(s, a) {
|
|
17
|
+
switch (a.type) {
|
|
18
|
+
case "invest": return {
|
|
19
|
+
...s,
|
|
20
|
+
cash: s.cash + a.amount,
|
|
21
|
+
capital: s.capital + a.amount
|
|
22
|
+
};
|
|
23
|
+
case "loan": return {
|
|
24
|
+
...s,
|
|
25
|
+
cash: s.cash + a.amount,
|
|
26
|
+
loan: s.loan + a.amount
|
|
27
|
+
};
|
|
28
|
+
case "repay": return {
|
|
29
|
+
...s,
|
|
30
|
+
cash: s.cash - a.amount,
|
|
31
|
+
loan: s.loan - a.amount
|
|
32
|
+
};
|
|
33
|
+
case "buyEquipment": return {
|
|
34
|
+
...s,
|
|
35
|
+
cash: s.cash - a.amount,
|
|
36
|
+
equipment: s.equipment + a.amount
|
|
37
|
+
};
|
|
38
|
+
case "depreciate": return {
|
|
39
|
+
...s,
|
|
40
|
+
equipment: s.equipment - a.amount,
|
|
41
|
+
expenses: s.expenses + a.amount
|
|
42
|
+
};
|
|
43
|
+
case "buyStock": return {
|
|
44
|
+
...s,
|
|
45
|
+
cash: s.cash - a.qty * a.unitCost,
|
|
46
|
+
inventoryQty: s.inventoryQty + a.qty,
|
|
47
|
+
inventoryValue: s.inventoryValue + a.qty * a.unitCost
|
|
48
|
+
};
|
|
49
|
+
case "sell": {
|
|
50
|
+
const avg = s.inventoryQty > 0 ? s.inventoryValue / s.inventoryQty : 0;
|
|
51
|
+
const qty = Math.min(a.qty, s.inventoryQty);
|
|
52
|
+
const cost = qty * avg, sales = qty * a.price;
|
|
53
|
+
return {
|
|
54
|
+
...s,
|
|
55
|
+
cash: s.cash + sales,
|
|
56
|
+
revenue: s.revenue + sales,
|
|
57
|
+
cogs: s.cogs + cost,
|
|
58
|
+
inventoryQty: s.inventoryQty - qty,
|
|
59
|
+
inventoryValue: s.inventoryValue - cost
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
case "expense": return {
|
|
63
|
+
...s,
|
|
64
|
+
cash: s.cash - a.amount,
|
|
65
|
+
expenses: s.expenses + a.amount
|
|
66
|
+
};
|
|
67
|
+
case "closePeriod": {
|
|
68
|
+
const net = s.revenue - s.cogs - s.expenses;
|
|
69
|
+
return {
|
|
70
|
+
...s,
|
|
71
|
+
retained: s.retained + net,
|
|
72
|
+
revenue: 0,
|
|
73
|
+
cogs: 0,
|
|
74
|
+
expenses: 0
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
default: return s;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** Run a whole authored scenario from an (optional) starting state. */
|
|
81
|
+
function runBiz(actions, init = {}) {
|
|
82
|
+
return actions.reduce(applyBiz, initialBiz(init));
|
|
83
|
+
}
|
|
84
|
+
/** Income statement for the current open period. */
|
|
85
|
+
function incomeStatement(s) {
|
|
86
|
+
const grossProfit = s.revenue - s.cogs;
|
|
87
|
+
return {
|
|
88
|
+
revenue: s.revenue,
|
|
89
|
+
cogs: s.cogs,
|
|
90
|
+
grossProfit,
|
|
91
|
+
expenses: s.expenses,
|
|
92
|
+
netProfit: grossProfit - s.expenses
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/** Balance sheet; `balances` is always true by construction (a self-check). */
|
|
96
|
+
function balanceSheet(s) {
|
|
97
|
+
const totalAssets = s.cash + s.inventoryValue + s.equipment;
|
|
98
|
+
const openProfit = s.revenue - s.cogs - s.expenses;
|
|
99
|
+
const totalEquity = s.capital + s.retained + openProfit;
|
|
100
|
+
const totalLiabilitiesEquity = s.loan + totalEquity;
|
|
101
|
+
return {
|
|
102
|
+
cash: s.cash,
|
|
103
|
+
inventory: s.inventoryValue,
|
|
104
|
+
equipment: s.equipment,
|
|
105
|
+
totalAssets,
|
|
106
|
+
loan: s.loan,
|
|
107
|
+
capital: s.capital,
|
|
108
|
+
retained: s.retained,
|
|
109
|
+
openProfit,
|
|
110
|
+
totalEquity,
|
|
111
|
+
totalLiabilitiesEquity,
|
|
112
|
+
balances: Math.abs(totalAssets - totalLiabilitiesEquity) < 1e-6
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
export { applyBiz, balanceSheet, incomeStatement, initialBiz, runBiz };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { BalanceSheet, BizAction, BizState, IncomeStatement, applyBiz, balanceSheet, incomeStatement, initialBiz, runBiz } from "./bizsim.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/finance/index.d.ts
|
|
4
|
+
/** Simple interest I = P·r·t (interest only, not the total). */
|
|
5
|
+
declare function simpleInterest(principal: number, rate: number, years: number): number;
|
|
6
|
+
/** Amount after compound interest, A = P(1 + r/m)^(m·t). */
|
|
7
|
+
declare function compoundAmount(principal: number, rate: number, years: number, m?: number): number;
|
|
8
|
+
/** Compound interest earned (total − principal). */
|
|
9
|
+
declare function compoundInterest(principal: number, rate: number, years: number, m?: number): number;
|
|
10
|
+
/** Future value of a lump sum, FV = P(1+r)^n. */
|
|
11
|
+
declare function futureValue(principal: number, rate: number, n: number): number;
|
|
12
|
+
/** Present value of a future lump sum, PV = FV/(1+r)^n. */
|
|
13
|
+
declare function presentValue(fv: number, rate: number, n: number): number;
|
|
14
|
+
/** Effective annual rate from a nominal rate compounded m times/year. */
|
|
15
|
+
declare function effectiveRate(nominal: number, m: number): number;
|
|
16
|
+
/** Rule of 72: approximate years for money to double at a given annual rate. */
|
|
17
|
+
declare function rule72(rate: number): number;
|
|
18
|
+
/** Yearly simple-vs-compound growth series, for the "money snowball" curve. */
|
|
19
|
+
declare function growthSeries(principal: number, rate: number, years: number): {
|
|
20
|
+
year: number;
|
|
21
|
+
simple: number;
|
|
22
|
+
compound: number;
|
|
23
|
+
}[];
|
|
24
|
+
interface AmortRow {
|
|
25
|
+
period: number;
|
|
26
|
+
interest: number;
|
|
27
|
+
principal: number;
|
|
28
|
+
balance: number;
|
|
29
|
+
}
|
|
30
|
+
interface AmortResult {
|
|
31
|
+
payment: number;
|
|
32
|
+
totalInterest: number;
|
|
33
|
+
schedule: AmortRow[];
|
|
34
|
+
}
|
|
35
|
+
/** Amortize a loan into equal payments; returns the payment, total interest, and per-period schedule. */
|
|
36
|
+
declare function amortize(principal: number, annualRate: number, years: number, m?: number): AmortResult;
|
|
37
|
+
interface DepRow {
|
|
38
|
+
year: number;
|
|
39
|
+
depreciation: number;
|
|
40
|
+
bookValue: number;
|
|
41
|
+
}
|
|
42
|
+
/** Straight-line depreciation: equal charge (cost − residual)/life each year. */
|
|
43
|
+
declare function straightLine(cost: number, residual: number, life: number): {
|
|
44
|
+
perYear: number;
|
|
45
|
+
schedule: DepRow[];
|
|
46
|
+
};
|
|
47
|
+
/** Reducing-balance depreciation: a fixed % of the falling book value each year. */
|
|
48
|
+
declare function reducingBalance(cost: number, rate: number, life: number): DepRow[];
|
|
49
|
+
interface CostModel {
|
|
50
|
+
fixedCost: number;
|
|
51
|
+
price: number;
|
|
52
|
+
variableCost: number;
|
|
53
|
+
}
|
|
54
|
+
interface BreakEven {
|
|
55
|
+
contributionPerUnit: number;
|
|
56
|
+
units: number;
|
|
57
|
+
revenue: number;
|
|
58
|
+
contributionMargin: number;
|
|
59
|
+
}
|
|
60
|
+
/** Break-even: units where contribution covers fixed cost; plus revenue and margin. */
|
|
61
|
+
declare function breakEven({
|
|
62
|
+
fixedCost,
|
|
63
|
+
price,
|
|
64
|
+
variableCost
|
|
65
|
+
}: CostModel): BreakEven;
|
|
66
|
+
/** Profit at a given output: units·(price − variable) − fixed. */
|
|
67
|
+
declare function profitAt(units: number, m: CostModel): number;
|
|
68
|
+
/** Margin of safety = actual output − break-even output (units). */
|
|
69
|
+
declare function marginOfSafety(currentUnits: number, breakEvenUnits: number): number;
|
|
70
|
+
type InvMethod = 'fifo' | 'lifo' | 'avco';
|
|
71
|
+
interface InvMove {
|
|
72
|
+
type: 'buy' | 'sell';
|
|
73
|
+
qty: number;
|
|
74
|
+
unitCost?: number;
|
|
75
|
+
}
|
|
76
|
+
interface InvResult {
|
|
77
|
+
cogs: number;
|
|
78
|
+
closingValue: number;
|
|
79
|
+
closingQty: number;
|
|
80
|
+
}
|
|
81
|
+
/** Value stock and cost of goods sold under FIFO, LIFO or weighted-average (AVCO). */
|
|
82
|
+
declare function inventoryValue(method: InvMethod, moves: InvMove[]): InvResult;
|
|
83
|
+
/** Economic order quantity: √(2·D·S / H) — demand D, order cost S, holding cost/unit H. */
|
|
84
|
+
declare function eoq(annualDemand: number, orderCost: number, holdingCostPerUnit: number): number;
|
|
85
|
+
/** Reorder level = usage during the lead time, plus any buffer (safety) stock. */
|
|
86
|
+
declare function reorderLevel(usagePerDay: number, leadTimeDays: number, bufferStock?: number): number;
|
|
87
|
+
/** Split a total cost across cost-centres in proportion to a basis (floor area sq ft, headcount, machine hours…). */
|
|
88
|
+
declare function apportion(total: number, weights: number[]): number[];
|
|
89
|
+
interface FinInputs {
|
|
90
|
+
currentAssets: number;
|
|
91
|
+
inventory: number;
|
|
92
|
+
currentLiabilities: number;
|
|
93
|
+
nonCurrentLiabilities: number;
|
|
94
|
+
equity: number;
|
|
95
|
+
revenue: number;
|
|
96
|
+
costOfSales: number;
|
|
97
|
+
expenses?: number;
|
|
98
|
+
grossProfit?: number;
|
|
99
|
+
netProfit?: number;
|
|
100
|
+
capitalEmployed?: number;
|
|
101
|
+
}
|
|
102
|
+
interface Ratios {
|
|
103
|
+
current: number;
|
|
104
|
+
quick: number;
|
|
105
|
+
gearing: number;
|
|
106
|
+
grossMargin: number;
|
|
107
|
+
netMargin: number;
|
|
108
|
+
roce: number;
|
|
109
|
+
inventoryTurnover: number;
|
|
110
|
+
}
|
|
111
|
+
/** Standard analysis ratios from summary figures (liquidity, gearing, profitability, efficiency). */
|
|
112
|
+
declare function ratios(f: FinInputs): Ratios;
|
|
113
|
+
/** Net present value; `cashflows[0]` is the initial outlay (usually negative). */
|
|
114
|
+
declare function npv(rate: number, cashflows: number[]): number;
|
|
115
|
+
/** Payback period in years (fractional), from year-0 outlay + yearly inflows. */
|
|
116
|
+
declare function payback(cashflows: number[]): number;
|
|
117
|
+
//#endregion
|
|
118
|
+
export { AmortResult, AmortRow, BalanceSheet, BizAction, BizState, BreakEven, CostModel, DepRow, FinInputs, IncomeStatement, InvMethod, InvMove, InvResult, Ratios, amortize, applyBiz, apportion, balanceSheet, breakEven, compoundAmount, compoundInterest, effectiveRate, eoq, futureValue, growthSeries, incomeStatement, initialBiz, inventoryValue, marginOfSafety, npv, payback, presentValue, profitAt, ratios, reducingBalance, reorderLevel, rule72, runBiz, simpleInterest, straightLine };
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { applyBiz, balanceSheet, incomeStatement, initialBiz, runBiz } from "./bizsim.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/finance/index.ts
|
|
4
|
+
/** Simple interest I = P·r·t (interest only, not the total). */
|
|
5
|
+
function simpleInterest(principal, rate, years) {
|
|
6
|
+
return principal * rate * years;
|
|
7
|
+
}
|
|
8
|
+
/** Amount after compound interest, A = P(1 + r/m)^(m·t). */
|
|
9
|
+
function compoundAmount(principal, rate, years, m = 1) {
|
|
10
|
+
return principal * Math.pow(1 + rate / m, m * years);
|
|
11
|
+
}
|
|
12
|
+
/** Compound interest earned (total − principal). */
|
|
13
|
+
function compoundInterest(principal, rate, years, m = 1) {
|
|
14
|
+
return compoundAmount(principal, rate, years, m) - principal;
|
|
15
|
+
}
|
|
16
|
+
/** Future value of a lump sum, FV = P(1+r)^n. */
|
|
17
|
+
function futureValue(principal, rate, n) {
|
|
18
|
+
return principal * Math.pow(1 + rate, n);
|
|
19
|
+
}
|
|
20
|
+
/** Present value of a future lump sum, PV = FV/(1+r)^n. */
|
|
21
|
+
function presentValue(fv, rate, n) {
|
|
22
|
+
return fv / Math.pow(1 + rate, n);
|
|
23
|
+
}
|
|
24
|
+
/** Effective annual rate from a nominal rate compounded m times/year. */
|
|
25
|
+
function effectiveRate(nominal, m) {
|
|
26
|
+
return Math.pow(1 + nominal / m, m) - 1;
|
|
27
|
+
}
|
|
28
|
+
/** Rule of 72: approximate years for money to double at a given annual rate. */
|
|
29
|
+
function rule72(rate) {
|
|
30
|
+
return 72 / (rate * 100);
|
|
31
|
+
}
|
|
32
|
+
/** Yearly simple-vs-compound growth series, for the "money snowball" curve. */
|
|
33
|
+
function growthSeries(principal, rate, years) {
|
|
34
|
+
const out = [];
|
|
35
|
+
for (let y = 0; y <= years; y++) out.push({
|
|
36
|
+
year: y,
|
|
37
|
+
simple: principal * (1 + rate * y),
|
|
38
|
+
compound: principal * Math.pow(1 + rate, y)
|
|
39
|
+
});
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
/** Amortize a loan into equal payments; returns the payment, total interest, and per-period schedule. */
|
|
43
|
+
function amortize(principal, annualRate, years, m = 12) {
|
|
44
|
+
const n = Math.round(years * m), i = annualRate / m;
|
|
45
|
+
const payment = i === 0 ? principal / n : principal * i / (1 - Math.pow(1 + i, -n));
|
|
46
|
+
const schedule = [];
|
|
47
|
+
let bal = principal;
|
|
48
|
+
for (let k = 1; k <= n; k++) {
|
|
49
|
+
const interest = bal * i;
|
|
50
|
+
const princ = payment - interest;
|
|
51
|
+
bal = Math.max(0, bal - princ);
|
|
52
|
+
schedule.push({
|
|
53
|
+
period: k,
|
|
54
|
+
interest,
|
|
55
|
+
principal: princ,
|
|
56
|
+
balance: bal
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
payment,
|
|
61
|
+
totalInterest: payment * n - principal,
|
|
62
|
+
schedule
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** Straight-line depreciation: equal charge (cost − residual)/life each year. */
|
|
66
|
+
function straightLine(cost, residual, life) {
|
|
67
|
+
const perYear = (cost - residual) / life;
|
|
68
|
+
const schedule = [];
|
|
69
|
+
for (let y = 1; y <= life; y++) schedule.push({
|
|
70
|
+
year: y,
|
|
71
|
+
depreciation: perYear,
|
|
72
|
+
bookValue: cost - perYear * y
|
|
73
|
+
});
|
|
74
|
+
return {
|
|
75
|
+
perYear,
|
|
76
|
+
schedule
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Reducing-balance depreciation: a fixed % of the falling book value each year. */
|
|
80
|
+
function reducingBalance(cost, rate, life) {
|
|
81
|
+
const schedule = [];
|
|
82
|
+
let bv = cost;
|
|
83
|
+
for (let y = 1; y <= life; y++) {
|
|
84
|
+
const dep = bv * rate;
|
|
85
|
+
bv -= dep;
|
|
86
|
+
schedule.push({
|
|
87
|
+
year: y,
|
|
88
|
+
depreciation: dep,
|
|
89
|
+
bookValue: bv
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return schedule;
|
|
93
|
+
}
|
|
94
|
+
/** Break-even: units where contribution covers fixed cost; plus revenue and margin. */
|
|
95
|
+
function breakEven({ fixedCost, price, variableCost }) {
|
|
96
|
+
const contributionPerUnit = price - variableCost;
|
|
97
|
+
const units = contributionPerUnit > 0 ? fixedCost / contributionPerUnit : Infinity;
|
|
98
|
+
return {
|
|
99
|
+
contributionPerUnit,
|
|
100
|
+
units,
|
|
101
|
+
revenue: units * price,
|
|
102
|
+
contributionMargin: price > 0 ? contributionPerUnit / price : 0
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
/** Profit at a given output: units·(price − variable) − fixed. */
|
|
106
|
+
function profitAt(units, m) {
|
|
107
|
+
return units * (m.price - m.variableCost) - m.fixedCost;
|
|
108
|
+
}
|
|
109
|
+
/** Margin of safety = actual output − break-even output (units). */
|
|
110
|
+
function marginOfSafety(currentUnits, breakEvenUnits) {
|
|
111
|
+
return currentUnits - breakEvenUnits;
|
|
112
|
+
}
|
|
113
|
+
/** Value stock and cost of goods sold under FIFO, LIFO or weighted-average (AVCO). */
|
|
114
|
+
function inventoryValue(method, moves) {
|
|
115
|
+
let cogs = 0;
|
|
116
|
+
if (method === "avco") {
|
|
117
|
+
let qty = 0, val = 0;
|
|
118
|
+
for (const m of moves) if (m.type === "buy") {
|
|
119
|
+
qty += m.qty;
|
|
120
|
+
val += m.qty * (m.unitCost ?? 0);
|
|
121
|
+
} else {
|
|
122
|
+
const avg = qty > 0 ? val / qty : 0;
|
|
123
|
+
cogs += m.qty * avg;
|
|
124
|
+
val -= m.qty * avg;
|
|
125
|
+
qty -= m.qty;
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
cogs,
|
|
129
|
+
closingValue: val,
|
|
130
|
+
closingQty: qty
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
const lots = [];
|
|
134
|
+
for (const m of moves) {
|
|
135
|
+
if (m.type === "buy") {
|
|
136
|
+
lots.push({
|
|
137
|
+
qty: m.qty,
|
|
138
|
+
cost: m.unitCost ?? 0
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
let need = m.qty;
|
|
143
|
+
while (need > 0 && lots.length) {
|
|
144
|
+
const idx = method === "fifo" ? 0 : lots.length - 1;
|
|
145
|
+
const lot = lots[idx];
|
|
146
|
+
const take = Math.min(need, lot.qty);
|
|
147
|
+
cogs += take * lot.cost;
|
|
148
|
+
lot.qty -= take;
|
|
149
|
+
need -= take;
|
|
150
|
+
if (lot.qty === 0) lots.splice(idx, 1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
cogs,
|
|
155
|
+
closingValue: lots.reduce((a, l) => a + l.qty * l.cost, 0),
|
|
156
|
+
closingQty: lots.reduce((a, l) => a + l.qty, 0)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/** Economic order quantity: √(2·D·S / H) — demand D, order cost S, holding cost/unit H. */
|
|
160
|
+
function eoq(annualDemand, orderCost, holdingCostPerUnit) {
|
|
161
|
+
return holdingCostPerUnit > 0 ? Math.sqrt(2 * annualDemand * orderCost / holdingCostPerUnit) : Infinity;
|
|
162
|
+
}
|
|
163
|
+
/** Reorder level = usage during the lead time, plus any buffer (safety) stock. */
|
|
164
|
+
function reorderLevel(usagePerDay, leadTimeDays, bufferStock = 0) {
|
|
165
|
+
return usagePerDay * leadTimeDays + bufferStock;
|
|
166
|
+
}
|
|
167
|
+
/** Split a total cost across cost-centres in proportion to a basis (floor area sq ft, headcount, machine hours…). */
|
|
168
|
+
function apportion(total, weights) {
|
|
169
|
+
const sum = weights.reduce((a, b) => a + b, 0) || 1;
|
|
170
|
+
return weights.map((w) => total * w / sum);
|
|
171
|
+
}
|
|
172
|
+
/** Standard analysis ratios from summary figures (liquidity, gearing, profitability, efficiency). */
|
|
173
|
+
function ratios(f) {
|
|
174
|
+
const grossProfit = f.grossProfit ?? f.revenue - f.costOfSales;
|
|
175
|
+
const netProfit = f.netProfit ?? grossProfit - (f.expenses ?? 0);
|
|
176
|
+
const capEmp = f.capitalEmployed ?? f.equity + f.nonCurrentLiabilities;
|
|
177
|
+
return {
|
|
178
|
+
current: f.currentAssets / f.currentLiabilities,
|
|
179
|
+
quick: (f.currentAssets - f.inventory) / f.currentLiabilities,
|
|
180
|
+
gearing: f.nonCurrentLiabilities / (f.equity + f.nonCurrentLiabilities),
|
|
181
|
+
grossMargin: grossProfit / f.revenue,
|
|
182
|
+
netMargin: netProfit / f.revenue,
|
|
183
|
+
roce: netProfit / capEmp,
|
|
184
|
+
inventoryTurnover: f.costOfSales / f.inventory
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/** Net present value; `cashflows[0]` is the initial outlay (usually negative). */
|
|
188
|
+
function npv(rate, cashflows) {
|
|
189
|
+
return cashflows.reduce((a, cf, t) => a + cf / Math.pow(1 + rate, t), 0);
|
|
190
|
+
}
|
|
191
|
+
/** Payback period in years (fractional), from year-0 outlay + yearly inflows. */
|
|
192
|
+
function payback(cashflows) {
|
|
193
|
+
let cum = 0;
|
|
194
|
+
for (let t = 0; t < cashflows.length; t++) {
|
|
195
|
+
const prev = cum;
|
|
196
|
+
cum += cashflows[t];
|
|
197
|
+
if (cum >= 0 && t > 0) return t - 1 + -prev / cashflows[t];
|
|
198
|
+
}
|
|
199
|
+
return Infinity;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
//#endregion
|
|
203
|
+
export { amortize, applyBiz, apportion, balanceSheet, breakEven, compoundAmount, compoundInterest, effectiveRate, eoq, futureValue, growthSeries, incomeStatement, initialBiz, inventoryValue, marginOfSafety, npv, payback, presentValue, profitAt, ratios, reducingBalance, reorderLevel, rule72, runBiz, simpleInterest, straightLine };
|