lecodes-cli 0.1.6 → 0.1.7
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/dist/index.js +166 -14
- package/package.json +3 -2
- package/runtime/sdk/animate/animate.ts +238 -0
- package/runtime/sdk/animate/bezier.ts +138 -0
- package/runtime/sdk/animate/easings.ts +126 -0
- package/runtime/sdk/core/color.ts +66 -0
- package/runtime/sdk/core/events.ts +38 -0
- package/runtime/sdk/core/registry.ts +23 -0
- package/runtime/sdk/g2/Camera2D.ts +73 -0
- package/runtime/sdk/g2/Node2D.ts +104 -0
- package/runtime/sdk/g2/Scene2D.ts +73 -0
- package/runtime/sdk/g2/Sprite.ts +164 -0
- package/runtime/sdk/g2/Texture2D.ts +37 -0
- package/runtime/sdk/g2/Tilemap.ts +41 -0
- package/runtime/sdk/g2/loop.ts +22 -0
- package/runtime/sdk/gl/Body.ts +101 -0
- package/runtime/sdk/gl/Camera.ts +40 -0
- package/runtime/sdk/gl/Geometry.ts +218 -0
- package/runtime/sdk/gl/Light.ts +37 -0
- package/runtime/sdk/gl/Material.ts +124 -0
- package/runtime/sdk/gl/Mesh.ts +123 -0
- package/runtime/sdk/gl/Node.ts +307 -0
- package/runtime/sdk/gl/Noise.ts +30 -0
- package/runtime/sdk/gl/Particles.ts +185 -0
- package/runtime/sdk/gl/Plane.ts +53 -0
- package/runtime/sdk/gl/Ray.ts +16 -0
- package/runtime/sdk/gl/Scene.ts +179 -0
- package/runtime/sdk/gl/Texture.ts +35 -0
- package/runtime/sdk/gl/controls.ts +95 -0
- package/runtime/sdk/gl/state.ts +6 -0
- package/runtime/sdk/gl/touch.ts +56 -0
- package/runtime/sdk/inject.ts +72 -0
- package/runtime/sdk/math/Mathf.ts +118 -0
- package/runtime/sdk/math/mat4.ts +278 -0
- package/runtime/sdk/math/quat.ts +232 -0
- package/runtime/sdk/math/vec.ts +255 -0
- package/runtime/sdk/plugins/qr.ts +36 -0
- package/runtime/sdk/runtime/device.ts +33 -0
- package/runtime/sdk/runtime/fetch.ts +71 -0
- package/runtime/sdk/runtime/files.ts +23 -0
- package/runtime/sdk/runtime/input.ts +12 -0
- package/runtime/sdk/runtime/media.ts +111 -0
- package/runtime/sdk/runtime/misc.ts +12 -0
- package/runtime/sdk/runtime/net.ts +36 -0
- package/runtime/sdk/runtime/storage.ts +13 -0
- package/runtime/sdk/runtime/touch.ts +44 -0
- package/runtime/sdk/ui/UI.ts +19 -0
- package/runtime/sdk/ui/UIButton.ts +84 -0
- package/runtime/sdk/ui/UIContainer.ts +100 -0
- package/runtime/sdk/ui/UIImage.ts +68 -0
- package/runtime/sdk/ui/UIInput.ts +78 -0
- package/runtime/sdk/ui/UINode.ts +342 -0
- package/runtime/sdk/ui/UIScreen.ts +122 -0
- package/runtime/sdk/ui/UIScrollable.ts +70 -0
- package/runtime/sdk/ui/UISpacer.ts +14 -0
- package/runtime/sdk/ui/UIText.ts +40 -0
- package/runtime/sdk/ui/UIVideo.ts +38 -0
- package/runtime/sdk/ui/UIVirtualizedList.ts +222 -0
- package/runtime/sdk/ui/UIWidget.ts +97 -0
- package/runtime/sdk/ui/fonts.ts +10 -0
- package/runtime/sdk/ui/router.ts +73 -0
- package/runtime/inject.js +0 -4629
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
4
4
|
|
|
5
5
|
// src/index.ts
|
|
6
|
-
import { readFileSync as
|
|
6
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
7
7
|
import { dirname as dirname7, join as join11 } from "node:path";
|
|
8
8
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
9
9
|
|
|
@@ -322,10 +322,13 @@ var DEFAULT_IGNORE_FILE = `# Files matched here are NOT pushed to LeCodes.
|
|
|
322
322
|
# Only untracked files are affected — assets already in the project stay tracked
|
|
323
323
|
# even if they match a pattern. Built-in defaults (node_modules, _*, *.log) always apply.
|
|
324
324
|
|
|
325
|
-
# Render / preview outputs
|
|
325
|
+
# Render / preview outputs (the default \`lecodes render --png\` target)
|
|
326
326
|
screenshot.png
|
|
327
327
|
shots/
|
|
328
|
-
|
|
328
|
+
|
|
329
|
+
# Image & other binary assets ARE pushed by default — they're real project resources.
|
|
330
|
+
# Uncomment a pattern below only if you want to keep some untracked ones out of pushes.
|
|
331
|
+
# *.png
|
|
329
332
|
|
|
330
333
|
# Local notes & dumps (uncomment to ignore)
|
|
331
334
|
# *.json
|
|
@@ -904,12 +907,14 @@ var collectEntries = (root, manifest, project) => {
|
|
|
904
907
|
|
|
905
908
|
// src/compile/projectCompile.ts
|
|
906
909
|
import { existsSync as existsSync6 } from "node:fs";
|
|
910
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
907
911
|
import { dirname as dirname5, join as join9 } from "node:path";
|
|
908
912
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
909
913
|
// ../sdk/src/compile/bundler.ts
|
|
910
914
|
import * as esbuild from "esbuild";
|
|
911
|
-
import { existsSync as existsSync5 } from "node:fs";
|
|
912
|
-
import {
|
|
915
|
+
import { existsSync as existsSync5, readdirSync as readdirSync4, readFileSync as readFileSync7, statSync } from "node:fs";
|
|
916
|
+
import { spawnSync } from "node:child_process";
|
|
917
|
+
import { dirname as dirname4, join as join8, relative as relative2 } from "node:path";
|
|
913
918
|
import { fileURLToPath } from "node:url";
|
|
914
919
|
|
|
915
920
|
// ../sdk/src/compile/assetMacro.ts
|
|
@@ -989,7 +994,7 @@ var resolveWithExtensions = (files, importer, specifier) => {
|
|
|
989
994
|
throw new Error(`Module not found: ${specifier} from ${importer}`);
|
|
990
995
|
};
|
|
991
996
|
var getLoader = (path) => path.endsWith(".ts") ? "ts" : "js";
|
|
992
|
-
var
|
|
997
|
+
var bundleProjectEsbuild = async (entrypoint, files, format = "esm") => {
|
|
993
998
|
const fileMap = new Map(Object.entries(files).map(([k, v]) => [normalize(k), v]));
|
|
994
999
|
const entry = normalize(entrypoint);
|
|
995
1000
|
if (!fileMap.has(entry))
|
|
@@ -1030,6 +1035,95 @@ var bundleProject = async (entrypoint, files, format = "esm") => {
|
|
|
1030
1035
|
throw new Error("Compilation produced no output");
|
|
1031
1036
|
return output.text;
|
|
1032
1037
|
};
|
|
1038
|
+
var CHISEL_DEFINE = { RAD2DEG: "57.29577951308232", DEG2RAD: "0.017453292519943295" };
|
|
1039
|
+
var CHISEL_KEEP = ["_*"];
|
|
1040
|
+
var resolveSdkSrc = () => {
|
|
1041
|
+
const inject = resolveInject();
|
|
1042
|
+
if (!inject.endsWith("inject.ts")) {
|
|
1043
|
+
throw new Error(`chisel needs the SDK source tree (src/inject.ts); got a prebuilt inject at ${inject}. ` + `Set LECODES_SDK_INJECT to the source, or use esbuild (unset LECODES_BUNDLER).`);
|
|
1044
|
+
}
|
|
1045
|
+
return dirname4(inject);
|
|
1046
|
+
};
|
|
1047
|
+
var isSdkSource = (path) => /\.[tj]s$/.test(path) && !path.endsWith(".d.ts") && !path.includes("/compile/") && !path.includes("/tests/") && !path.includes("/__tests__/");
|
|
1048
|
+
var sdkFilesCache = new Map;
|
|
1049
|
+
var collectSdkFiles = (sdkSrc) => {
|
|
1050
|
+
const cached = sdkFilesCache.get(sdkSrc);
|
|
1051
|
+
if (cached)
|
|
1052
|
+
return cached;
|
|
1053
|
+
const out = {};
|
|
1054
|
+
const walk = (dir) => {
|
|
1055
|
+
for (const name of readdirSync4(dir)) {
|
|
1056
|
+
const p = join8(dir, name);
|
|
1057
|
+
if (statSync(p).isDirectory())
|
|
1058
|
+
walk(p);
|
|
1059
|
+
else if (isSdkSource(p))
|
|
1060
|
+
out["/__sdk/" + relative2(sdkSrc, p)] = readFileSync7(p, "utf8");
|
|
1061
|
+
}
|
|
1062
|
+
};
|
|
1063
|
+
walk(sdkSrc);
|
|
1064
|
+
sdkFilesCache.set(sdkSrc, out);
|
|
1065
|
+
return out;
|
|
1066
|
+
};
|
|
1067
|
+
var resolveChiselBin = () => {
|
|
1068
|
+
const override = process.env.LECODES_CHISEL_BIN;
|
|
1069
|
+
if (override) {
|
|
1070
|
+
if (existsSync5(override))
|
|
1071
|
+
return override;
|
|
1072
|
+
throw new Error(`LECODES_CHISEL_BIN points at a missing file: ${override}`);
|
|
1073
|
+
}
|
|
1074
|
+
const here = dirname4(fileURLToPath(import.meta.url));
|
|
1075
|
+
for (const rel of ["release/chisel", "debug/chisel"]) {
|
|
1076
|
+
const p = join8(here, "..", "..", "..", "..", "..", "chisel", "target", rel);
|
|
1077
|
+
if (existsSync5(p))
|
|
1078
|
+
return p;
|
|
1079
|
+
}
|
|
1080
|
+
throw new Error("chisel binary not found. Build it (`cargo build --release` in the chisel repo) or set LECODES_CHISEL_BIN.");
|
|
1081
|
+
};
|
|
1082
|
+
var bundleProjectChisel = async (entrypoint, files, format, sourcemap) => {
|
|
1083
|
+
const entry = normalize(entrypoint);
|
|
1084
|
+
const projFiles = {};
|
|
1085
|
+
for (const [k, v] of Object.entries(files)) {
|
|
1086
|
+
const path = normalize(k);
|
|
1087
|
+
projFiles[path] = /\.[tj]sx?$/.test(path) && hasAssetMacro(v) ? desugarAssetMacros(v, path) : v;
|
|
1088
|
+
}
|
|
1089
|
+
if (!projFiles[entry])
|
|
1090
|
+
throw new Error(`Entrypoint not found: ${entry}`);
|
|
1091
|
+
const sdkFiles = collectSdkFiles(resolveSdkSrc());
|
|
1092
|
+
const minify = process.env.LECODES_CHISEL_MINIFY !== "0";
|
|
1093
|
+
const input = JSON.stringify({
|
|
1094
|
+
files: { ...sdkFiles, ...projFiles },
|
|
1095
|
+
entry,
|
|
1096
|
+
inject: ["/__sdk/inject.ts"],
|
|
1097
|
+
format,
|
|
1098
|
+
minify,
|
|
1099
|
+
fuse: process.env.LECODES_CHISEL_FUSE === "1",
|
|
1100
|
+
define: CHISEL_DEFINE,
|
|
1101
|
+
keep: CHISEL_KEEP,
|
|
1102
|
+
sourcemap
|
|
1103
|
+
});
|
|
1104
|
+
const bin = resolveChiselBin();
|
|
1105
|
+
const r = spawnSync(bin, [], { input, encoding: "utf8", maxBuffer: 256 * 1024 * 1024 });
|
|
1106
|
+
if (r.status === 2 || r.error) {
|
|
1107
|
+
throw new Error(`chisel failed: ${r.error?.message ?? r.stderr ?? "unknown error"}`);
|
|
1108
|
+
}
|
|
1109
|
+
let parsed;
|
|
1110
|
+
try {
|
|
1111
|
+
parsed = JSON.parse(r.stdout || "{}");
|
|
1112
|
+
} catch {
|
|
1113
|
+
throw new Error(`chisel produced invalid output: ${(r.stdout || r.stderr || "").slice(0, 200)}`);
|
|
1114
|
+
}
|
|
1115
|
+
if (parsed.error)
|
|
1116
|
+
throw new Error(`chisel: ${parsed.error}`);
|
|
1117
|
+
if (parsed.code == null)
|
|
1118
|
+
throw new Error("chisel produced no code");
|
|
1119
|
+
return { code: parsed.code, map: parsed.map ?? null };
|
|
1120
|
+
};
|
|
1121
|
+
var bundleProjectWithMap = async (entrypoint, files, format = "esm") => {
|
|
1122
|
+
if (process.env.LECODES_BUNDLER === "chisel") {
|
|
1123
|
+
return bundleProjectChisel(entrypoint, files, format, true);
|
|
1124
|
+
}
|
|
1125
|
+
return { code: await bundleProjectEsbuild(entrypoint, files, format), map: null };
|
|
1126
|
+
};
|
|
1033
1127
|
|
|
1034
1128
|
// ../sdk/src/compile/header.ts
|
|
1035
1129
|
var buildHeader = (name, code, opts = {}) => {
|
|
@@ -1067,6 +1161,19 @@ var buildHeader = (name, code, opts = {}) => {
|
|
|
1067
1161
|
return header;
|
|
1068
1162
|
};
|
|
1069
1163
|
|
|
1164
|
+
// ../sdk/src/compile/sourcemap.ts
|
|
1165
|
+
var offsetSourceMap = (mapJson, lines) => {
|
|
1166
|
+
if (lines <= 0)
|
|
1167
|
+
return mapJson;
|
|
1168
|
+
const map = JSON.parse(mapJson);
|
|
1169
|
+
map.mappings = ";".repeat(lines) + map.mappings;
|
|
1170
|
+
return JSON.stringify(map);
|
|
1171
|
+
};
|
|
1172
|
+
var inlineSourceMapComment = (mapJson) => {
|
|
1173
|
+
const b64 = Buffer.from(mapJson, "utf8").toString("base64");
|
|
1174
|
+
return `//# lecodesMap=data:application/json;charset=utf-8;base64,${b64}`;
|
|
1175
|
+
};
|
|
1176
|
+
|
|
1070
1177
|
// ../sdk/src/compile/compileProject.ts
|
|
1071
1178
|
var detectEntry = (entries) => {
|
|
1072
1179
|
let entry = "";
|
|
@@ -1106,13 +1213,39 @@ var compileProject = async (opts) => {
|
|
|
1106
1213
|
files[e.path] = e.text ?? "";
|
|
1107
1214
|
}
|
|
1108
1215
|
}
|
|
1109
|
-
const code = await
|
|
1110
|
-
|
|
1216
|
+
const { code, map } = await bundleProjectWithMap(entry, files, opts.format ?? "esm");
|
|
1217
|
+
if (opts.header === false) {
|
|
1218
|
+
return map ? `${code}
|
|
1219
|
+
${inlineSourceMapComment(map)}` : code;
|
|
1220
|
+
}
|
|
1221
|
+
const header = buildHeader(opts.name, code, { icon: opts.icon });
|
|
1222
|
+
if (!map)
|
|
1223
|
+
return header + code;
|
|
1224
|
+
const headerLines = (header.match(/\n/g) ?? []).length;
|
|
1225
|
+
return `${header}${code}
|
|
1226
|
+
${inlineSourceMapComment(offsetSourceMap(map, headerLines))}`;
|
|
1111
1227
|
};
|
|
1112
1228
|
// src/compile/projectCompile.ts
|
|
1113
|
-
var
|
|
1229
|
+
var here = dirname5(fileURLToPath2(import.meta.url));
|
|
1230
|
+
var vendoredInject = join9(here, "..", "runtime", "sdk", "inject.ts");
|
|
1114
1231
|
if (existsSync6(vendoredInject))
|
|
1115
1232
|
process.env.LECODES_SDK_INJECT = vendoredInject;
|
|
1233
|
+
var resolveChiselBin2 = () => {
|
|
1234
|
+
if (process.env.LECODES_CHISEL_BIN)
|
|
1235
|
+
return process.env.LECODES_CHISEL_BIN;
|
|
1236
|
+
try {
|
|
1237
|
+
return createRequire2(import.meta.url)("@letary/chisel").binaryPath() ?? null;
|
|
1238
|
+
} catch {
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
};
|
|
1242
|
+
if (!process.env.LECODES_BUNDLER) {
|
|
1243
|
+
const chiselBin = resolveChiselBin2();
|
|
1244
|
+
if (chiselBin) {
|
|
1245
|
+
process.env.LECODES_CHISEL_BIN = chiselBin;
|
|
1246
|
+
process.env.LECODES_BUNDLER = "chisel";
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1116
1249
|
var compileProject2 = (opts) => compileProject({
|
|
1117
1250
|
entries: opts.entries.map((e) => ({ path: e.path, type: e.type, text: e.text, fileSrc: e.fileSrc, shaders: e.shaders })),
|
|
1118
1251
|
name: opts.name,
|
|
@@ -1356,7 +1489,7 @@ var pullPaths = async (root, manifest, project, paths, apiUrl, token) => {
|
|
|
1356
1489
|
|
|
1357
1490
|
// src/commands/push.ts
|
|
1358
1491
|
import { basename as basename3 } from "node:path";
|
|
1359
|
-
import { readFileSync as
|
|
1492
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
1360
1493
|
var push = async (args) => {
|
|
1361
1494
|
const root = findProjectRoot(process.cwd());
|
|
1362
1495
|
const manifest = readManifest(root);
|
|
@@ -1381,6 +1514,12 @@ var push = async (args) => {
|
|
|
1381
1514
|
if (commits.headId !== manifest.baseHeadId) {
|
|
1382
1515
|
throw new CliError("The project changed on the server since your last sync. Run `lecodes diff` to compare, `lecodes pull` to take the server's version, or `lecodes push --force` to overwrite.");
|
|
1383
1516
|
}
|
|
1517
|
+
if (commits.dirty) {
|
|
1518
|
+
const drift = serverPathDrift(await getProject(apiUrl, token, manifest.uuid), manifest);
|
|
1519
|
+
if (drift) {
|
|
1520
|
+
throw new CliError(`The project has uncommitted changes in the web editor your checkout doesn't have (${drift}). Run \`lecodes pull\` to take them, or \`lecodes push --force\` to overwrite the server.`);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1384
1523
|
}
|
|
1385
1524
|
if (force) {
|
|
1386
1525
|
const project = await getProject(apiUrl, token, manifest.uuid);
|
|
@@ -1409,9 +1548,9 @@ var push = async (args) => {
|
|
|
1409
1548
|
if (source)
|
|
1410
1549
|
entry.assetId = source.id;
|
|
1411
1550
|
if (isTextKind(kind)) {
|
|
1412
|
-
entry.text =
|
|
1551
|
+
entry.text = readFileSync8(file.absPath, "utf8");
|
|
1413
1552
|
} else if (!source || source.sha !== file.sha) {
|
|
1414
|
-
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename3(file.path),
|
|
1553
|
+
const blob = await uploadBlob(apiUrl, token, manifest.uuid, basename3(file.path), readFileSync8(file.absPath));
|
|
1415
1554
|
entry.blobFileId = blob.id;
|
|
1416
1555
|
uploaded++;
|
|
1417
1556
|
}
|
|
@@ -1443,6 +1582,19 @@ var push = async (args) => {
|
|
|
1443
1582
|
if (publish)
|
|
1444
1583
|
reportPublish(res);
|
|
1445
1584
|
};
|
|
1585
|
+
var serverPathDrift = (project, manifest) => {
|
|
1586
|
+
const pathById = buildPaths(project.assets);
|
|
1587
|
+
const serverPaths = new Set(project.assets.filter((a) => a.type !== "folder").map((a) => pathById.get(a.id)));
|
|
1588
|
+
const manifestPaths = new Set(Object.keys(manifest.files));
|
|
1589
|
+
const added = [...serverPaths].filter((p) => !manifestPaths.has(p));
|
|
1590
|
+
const removed = [...manifestPaths].filter((p) => !serverPaths.has(p));
|
|
1591
|
+
const describe = (label, paths) => `${paths.length} ${label} on the server: ${paths.slice(0, 3).join(", ")}${paths.length > 3 ? ", …" : ""}`;
|
|
1592
|
+
const parts = [
|
|
1593
|
+
added.length ? describe("added", added) : "",
|
|
1594
|
+
removed.length ? describe("removed", removed) : ""
|
|
1595
|
+
].filter(Boolean);
|
|
1596
|
+
return parts.length ? parts.join("; ") : null;
|
|
1597
|
+
};
|
|
1446
1598
|
var reportPublish = (res) => {
|
|
1447
1599
|
if (res.published)
|
|
1448
1600
|
success("Published — the project is now live.");
|
|
@@ -1527,8 +1679,8 @@ Config lives in ~/.lecodes/config.json (override with LECODES_API / LECODES_TOKE
|
|
|
1527
1679
|
var commands = { login, create, clone, status, diff, pull, push, preview, render };
|
|
1528
1680
|
var readVersion = () => {
|
|
1529
1681
|
try {
|
|
1530
|
-
const
|
|
1531
|
-
return JSON.parse(
|
|
1682
|
+
const here2 = dirname7(fileURLToPath3(import.meta.url));
|
|
1683
|
+
return JSON.parse(readFileSync9(join11(here2, "..", "package.json"), "utf8")).version ?? "unknown";
|
|
1532
1684
|
} catch {
|
|
1533
1685
|
return "unknown";
|
|
1534
1686
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lecodes-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Clone, edit and push LeCodes projects from your machine.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,12 +21,13 @@
|
|
|
21
21
|
},
|
|
22
22
|
"scripts": {
|
|
23
23
|
"vendor-runtime": "bun scripts/vendor-runtime.ts",
|
|
24
|
-
"build": "bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external esbuild --external lecodes-renderer --banner=\"#!/usr/bin/env node\"",
|
|
24
|
+
"build": "bun scripts/vendor-runtime.ts && bun build src/index.ts --target=node --outfile dist/index.js --external esbuild --external @letary/chisel --external lecodes-renderer --banner=\"#!/usr/bin/env node\"",
|
|
25
25
|
"typecheck": "tsc --noEmit",
|
|
26
26
|
"dev": "bun run src/index.ts",
|
|
27
27
|
"prepublishOnly": "bun run build"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
+
"@letary/chisel": "^0.1.2",
|
|
30
31
|
"esbuild": "^0.27.3"
|
|
31
32
|
},
|
|
32
33
|
"peerDependencies": {
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// animate self-implements its matrix decompose/recompose; these are just the numeric-array shapes.
|
|
2
|
+
type mat4 = Float32Array | number[]
|
|
3
|
+
type quat = Float32Array | number[]
|
|
4
|
+
type vec2 = Float32Array | number[]
|
|
5
|
+
type vec3 = Float32Array | number[]
|
|
6
|
+
import type { Easing } from "./easings"
|
|
7
|
+
|
|
8
|
+
type AnimateProps = number | string | vec2 | vec3 | quat
|
|
9
|
+
|
|
10
|
+
export type AnimateOptions<T> = {
|
|
11
|
+
from: T,
|
|
12
|
+
to: T,
|
|
13
|
+
duration: number,
|
|
14
|
+
onUpdate(val: T): void,
|
|
15
|
+
onComplete?(): void,
|
|
16
|
+
easing?: Easing,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const animate = <T extends AnimateProps>(options: AnimateOptions<T>): number => {
|
|
20
|
+
|
|
21
|
+
let onRawUpdate
|
|
22
|
+
if (typeof options.to === "number") {
|
|
23
|
+
const { onUpdate, from, to } = options as AnimateOptions<number>
|
|
24
|
+
onRawUpdate = (t: number) => onUpdate(from + (to - from) * t)
|
|
25
|
+
} else if (options.to === "string") {
|
|
26
|
+
// TODO: Make color animate here
|
|
27
|
+
const { onUpdate, from, to } = options as AnimateOptions<string>
|
|
28
|
+
onRawUpdate = (t: number) => onUpdate(from)
|
|
29
|
+
|
|
30
|
+
// onRawUpdate = (val: number) => onUpdate(from + (to - from) * val)
|
|
31
|
+
} else if (options.to.length === 2) {
|
|
32
|
+
const { onUpdate, from, to } = options as AnimateOptions<vec2>
|
|
33
|
+
const tempVec = new Float32Array(3)
|
|
34
|
+
onRawUpdate = (t: number) => onUpdate(lerp2(tempVec, from, to, t))
|
|
35
|
+
|
|
36
|
+
} else if (options.to.length === 3) {
|
|
37
|
+
const { onUpdate, from, to } = options as AnimateOptions<vec3>
|
|
38
|
+
const tempVec = new Float32Array(3)
|
|
39
|
+
|
|
40
|
+
onRawUpdate = (t: number) => onUpdate(lerp(tempVec, from, to, t))
|
|
41
|
+
} else if (options.to.length === 4) {
|
|
42
|
+
const { onUpdate, from, to } = options as AnimateOptions<quat>
|
|
43
|
+
|
|
44
|
+
const tempQuat = new Float32Array(4)
|
|
45
|
+
onRawUpdate = (t: number) => onUpdate(slerp(tempQuat, from, to, t))
|
|
46
|
+
} else {
|
|
47
|
+
onRawUpdate = () => {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const animationId = _creatorUtils.animate(val => {
|
|
51
|
+
const _val = options.easing? options.easing(val): val
|
|
52
|
+
onRawUpdate(_val)
|
|
53
|
+
if (val === 1 && options.onComplete) {
|
|
54
|
+
options.onComplete()
|
|
55
|
+
}
|
|
56
|
+
}, options.duration)
|
|
57
|
+
|
|
58
|
+
return animationId
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const stopAnimation = (id: number): void => _creatorUtils.stopAnimation(id)
|
|
62
|
+
export const pauseAnimation = (id: number): void => _creatorUtils.pauseAnimation(id)
|
|
63
|
+
export const resumeAnimation = (id: number): void => _creatorUtils.resumeAnimation(id)
|
|
64
|
+
|
|
65
|
+
export const animateMat4 = (options: AnimateOptions<mat4>): number => {
|
|
66
|
+
const fromPosition = new Float32Array(3)
|
|
67
|
+
const fromQuaternion = new Float32Array(4)
|
|
68
|
+
const fromScale = new Float32Array(3)
|
|
69
|
+
const toPosition = new Float32Array(3)
|
|
70
|
+
const toQuaternion = new Float32Array(4)
|
|
71
|
+
const toScale = new Float32Array(3)
|
|
72
|
+
decompose(fromQuaternion, fromPosition, fromScale, (options as AnimateOptions<mat4>).from)
|
|
73
|
+
decompose(toQuaternion, toPosition, toScale, (options as AnimateOptions<mat4>).to)
|
|
74
|
+
|
|
75
|
+
const tempPosition = new Float32Array(3)
|
|
76
|
+
const tempQuat = new Float32Array(4)
|
|
77
|
+
const tempScale = new Float32Array(3)
|
|
78
|
+
const tempMat = new Float32Array(16)
|
|
79
|
+
|
|
80
|
+
const onUpdate = (options as AnimateOptions<mat4>).onUpdate
|
|
81
|
+
const onRawUpdate = (t: number) => {
|
|
82
|
+
lerp(tempPosition, fromPosition, toPosition, t)
|
|
83
|
+
slerp(tempQuat, fromQuaternion, toQuaternion, t)
|
|
84
|
+
lerp(tempScale, fromScale, toScale, t)
|
|
85
|
+
|
|
86
|
+
onUpdate(fromRotationTranslationScale(tempMat, tempQuat, tempPosition, tempScale))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const animationId = _creatorUtils.animate(val => {
|
|
90
|
+
const _val = options.easing? options.easing(val): val
|
|
91
|
+
onRawUpdate(_val)
|
|
92
|
+
if (val === 1 && options.onComplete) {
|
|
93
|
+
options.onComplete()
|
|
94
|
+
}
|
|
95
|
+
}, options.duration)
|
|
96
|
+
|
|
97
|
+
return animationId
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const EPSILON = 1e-6;
|
|
101
|
+
function slerp(out: quat, a: quat, b: quat, t: number) {
|
|
102
|
+
const ax = a[0], ay = a[1], az = a[2], aw = a[3];
|
|
103
|
+
let bx = b[0], by = b[1], bz = b[2], bw = b[3];
|
|
104
|
+
let omega, cosom, sinom, scale0, scale1;
|
|
105
|
+
cosom = ax * bx + ay * by + az * bz + aw * bw;
|
|
106
|
+
if (cosom < 0) {
|
|
107
|
+
cosom = -cosom;
|
|
108
|
+
bx = -bx;
|
|
109
|
+
by = -by;
|
|
110
|
+
bz = -bz;
|
|
111
|
+
bw = -bw;
|
|
112
|
+
}
|
|
113
|
+
if (1 - cosom > EPSILON) {
|
|
114
|
+
omega = Math.acos(cosom);
|
|
115
|
+
sinom = Math.sin(omega);
|
|
116
|
+
scale0 = Math.sin((1 - t) * omega) / sinom;
|
|
117
|
+
scale1 = Math.sin(t * omega) / sinom;
|
|
118
|
+
} else {
|
|
119
|
+
scale0 = 1 - t;
|
|
120
|
+
scale1 = t;
|
|
121
|
+
}
|
|
122
|
+
out[0] = scale0 * ax + scale1 * bx;
|
|
123
|
+
out[1] = scale0 * ay + scale1 * by;
|
|
124
|
+
out[2] = scale0 * az + scale1 * bz;
|
|
125
|
+
out[3] = scale0 * aw + scale1 * bw;
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
function lerp(out: vec3, a: vec3, b: vec3, t: number) {
|
|
129
|
+
const ax = a[0];
|
|
130
|
+
const ay = a[1];
|
|
131
|
+
const az = a[2];
|
|
132
|
+
out[0] = ax + t * (b[0] - ax);
|
|
133
|
+
out[1] = ay + t * (b[1] - ay);
|
|
134
|
+
out[2] = az + t * (b[2] - az);
|
|
135
|
+
return out;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function lerp2(out: vec2, a: vec2, b: vec2, t: number) {
|
|
139
|
+
const ax = a[0];
|
|
140
|
+
const ay = a[1];
|
|
141
|
+
const az = a[2];
|
|
142
|
+
out[0] = ax + t * (b[0] - ax);
|
|
143
|
+
out[1] = ay + t * (b[1] - ay);
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
function fromRotationTranslationScale(out: mat4, q: quat, v: vec3, s: vec3) {
|
|
147
|
+
const x = q[0], y = q[1], z = q[2], w = q[3];
|
|
148
|
+
const x2 = x + x;
|
|
149
|
+
const y2 = y + y;
|
|
150
|
+
const z2 = z + z;
|
|
151
|
+
const xx = x * x2;
|
|
152
|
+
const xy = x * y2;
|
|
153
|
+
const xz = x * z2;
|
|
154
|
+
const yy = y * y2;
|
|
155
|
+
const yz = y * z2;
|
|
156
|
+
const zz = z * z2;
|
|
157
|
+
const wx = w * x2;
|
|
158
|
+
const wy = w * y2;
|
|
159
|
+
const wz = w * z2;
|
|
160
|
+
const sx = s[0];
|
|
161
|
+
const sy = s[1];
|
|
162
|
+
const sz = s[2];
|
|
163
|
+
out[0] = (1 - (yy + zz)) * sx;
|
|
164
|
+
out[1] = (xy + wz) * sx;
|
|
165
|
+
out[2] = (xz - wy) * sx;
|
|
166
|
+
out[3] = 0;
|
|
167
|
+
out[4] = (xy - wz) * sy;
|
|
168
|
+
out[5] = (1 - (xx + zz)) * sy;
|
|
169
|
+
out[6] = (yz + wx) * sy;
|
|
170
|
+
out[7] = 0;
|
|
171
|
+
out[8] = (xz + wy) * sz;
|
|
172
|
+
out[9] = (yz - wx) * sz;
|
|
173
|
+
out[10] = (1 - (xx + yy)) * sz;
|
|
174
|
+
out[11] = 0;
|
|
175
|
+
out[12] = v[0];
|
|
176
|
+
out[13] = v[1];
|
|
177
|
+
out[14] = v[2];
|
|
178
|
+
out[15] = 1;
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function decompose(out_r: mat4, out_t: vec3, out_s: vec3, mat: mat4) {
|
|
183
|
+
out_t[0] = mat[12];
|
|
184
|
+
out_t[1] = mat[13];
|
|
185
|
+
out_t[2] = mat[14];
|
|
186
|
+
const m11 = mat[0];
|
|
187
|
+
const m12 = mat[1];
|
|
188
|
+
const m13 = mat[2];
|
|
189
|
+
const m21 = mat[4];
|
|
190
|
+
const m22 = mat[5];
|
|
191
|
+
const m23 = mat[6];
|
|
192
|
+
const m31 = mat[8];
|
|
193
|
+
const m32 = mat[9];
|
|
194
|
+
const m33 = mat[10];
|
|
195
|
+
out_s[0] = Math.hypot(m11, m12, m13);
|
|
196
|
+
out_s[1] = Math.hypot(m21, m22, m23);
|
|
197
|
+
out_s[2] = Math.hypot(m31, m32, m33);
|
|
198
|
+
const is1 = 1 / out_s[0];
|
|
199
|
+
const is2 = 1 / out_s[1];
|
|
200
|
+
const is3 = 1 / out_s[2];
|
|
201
|
+
const sm11 = m11 * is1;
|
|
202
|
+
const sm12 = m12 * is2;
|
|
203
|
+
const sm13 = m13 * is3;
|
|
204
|
+
const sm21 = m21 * is1;
|
|
205
|
+
const sm22 = m22 * is2;
|
|
206
|
+
const sm23 = m23 * is3;
|
|
207
|
+
const sm31 = m31 * is1;
|
|
208
|
+
const sm32 = m32 * is2;
|
|
209
|
+
const sm33 = m33 * is3;
|
|
210
|
+
const trace = sm11 + sm22 + sm33;
|
|
211
|
+
let S = 0;
|
|
212
|
+
if (trace > 0) {
|
|
213
|
+
S = Math.sqrt(trace + 1) * 2;
|
|
214
|
+
out_r[3] = 0.25 * S;
|
|
215
|
+
out_r[0] = (sm23 - sm32) / S;
|
|
216
|
+
out_r[1] = (sm31 - sm13) / S;
|
|
217
|
+
out_r[2] = (sm12 - sm21) / S;
|
|
218
|
+
} else if (sm11 > sm22 && sm11 > sm33) {
|
|
219
|
+
S = Math.sqrt(1 + sm11 - sm22 - sm33) * 2;
|
|
220
|
+
out_r[3] = (sm23 - sm32) / S;
|
|
221
|
+
out_r[0] = 0.25 * S;
|
|
222
|
+
out_r[1] = (sm12 + sm21) / S;
|
|
223
|
+
out_r[2] = (sm31 + sm13) / S;
|
|
224
|
+
} else if (sm22 > sm33) {
|
|
225
|
+
S = Math.sqrt(1 + sm22 - sm11 - sm33) * 2;
|
|
226
|
+
out_r[3] = (sm31 - sm13) / S;
|
|
227
|
+
out_r[0] = (sm12 + sm21) / S;
|
|
228
|
+
out_r[1] = 0.25 * S;
|
|
229
|
+
out_r[2] = (sm23 + sm32) / S;
|
|
230
|
+
} else {
|
|
231
|
+
S = Math.sqrt(1 + sm33 - sm11 - sm22) * 2;
|
|
232
|
+
out_r[3] = (sm12 - sm21) / S;
|
|
233
|
+
out_r[0] = (sm31 + sm13) / S;
|
|
234
|
+
out_r[1] = (sm23 + sm32) / S;
|
|
235
|
+
out_r[2] = 0.25 * S;
|
|
236
|
+
}
|
|
237
|
+
return out_r;
|
|
238
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Bezier function generator
|
|
3
|
+
|
|
4
|
+
Gaëtan Renaudeau's BezierEasing
|
|
5
|
+
https://github.com/gre/bezier-easing/blob/master/src/index.js
|
|
6
|
+
https://github.com/gre/bezier-easing/blob/master/LICENSE
|
|
7
|
+
You're a hero
|
|
8
|
+
|
|
9
|
+
Use
|
|
10
|
+
|
|
11
|
+
const easeOut = cubicBezier(.17,.67,.83,.67);
|
|
12
|
+
const x = easeOut(0.5); // returns 0.627...
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { Easing } from "./easings"
|
|
16
|
+
|
|
17
|
+
const a = (a1: number, a2: number) => 1.0 - 3.0 * a2 + 3.0 * a1
|
|
18
|
+
const b = (a1: number, a2: number) => 3.0 * a2 - 6.0 * a1
|
|
19
|
+
const c = (a1: number) => 3.0 * a1
|
|
20
|
+
|
|
21
|
+
// Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
|
|
22
|
+
const calcBezier = (t: number, a1: number, a2: number) =>
|
|
23
|
+
((a(a1, a2) * t + b(a1, a2)) * t + c(a1)) * t
|
|
24
|
+
|
|
25
|
+
// Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
|
|
26
|
+
const getSlope = (t: number, a1: number, a2: number) =>
|
|
27
|
+
3.0 * a(a1, a2) * t * t + 2.0 * b(a1, a2) * t + c(a1)
|
|
28
|
+
|
|
29
|
+
const subdivisionPrecision = 0.0000001
|
|
30
|
+
const subdivisionMaxIterations = 10
|
|
31
|
+
|
|
32
|
+
function binarySubdivide(
|
|
33
|
+
aX: number,
|
|
34
|
+
aA: number,
|
|
35
|
+
aB: number,
|
|
36
|
+
mX1: number,
|
|
37
|
+
mX2: number
|
|
38
|
+
) {
|
|
39
|
+
let currentX: number
|
|
40
|
+
let currentT: number
|
|
41
|
+
let i: number = 0
|
|
42
|
+
|
|
43
|
+
do {
|
|
44
|
+
currentT = aA + (aB - aA) / 2.0
|
|
45
|
+
currentX = calcBezier(currentT, mX1, mX2) - aX
|
|
46
|
+
if (currentX > 0.0) {
|
|
47
|
+
aB = currentT
|
|
48
|
+
} else {
|
|
49
|
+
aA = currentT
|
|
50
|
+
}
|
|
51
|
+
} while (
|
|
52
|
+
Math.abs(currentX) > subdivisionPrecision &&
|
|
53
|
+
++i < subdivisionMaxIterations
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return currentT
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const newtonIterations = 8
|
|
60
|
+
const newtonMinSlope = 0.001
|
|
61
|
+
function newtonRaphsonIterate(
|
|
62
|
+
aX: number,
|
|
63
|
+
aGuessT: number,
|
|
64
|
+
mX1: number,
|
|
65
|
+
mX2: number
|
|
66
|
+
) {
|
|
67
|
+
for (let i = 0; i < newtonIterations; ++i) {
|
|
68
|
+
const currentSlope = getSlope(aGuessT, mX1, mX2)
|
|
69
|
+
if (currentSlope === 0.0) {
|
|
70
|
+
return aGuessT
|
|
71
|
+
}
|
|
72
|
+
const currentX = calcBezier(aGuessT, mX1, mX2) - aX
|
|
73
|
+
aGuessT -= currentX / currentSlope
|
|
74
|
+
}
|
|
75
|
+
return aGuessT
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const kSplineTableSize = 11
|
|
79
|
+
const kSampleStepSize = 1.0 / (kSplineTableSize - 1.0)
|
|
80
|
+
|
|
81
|
+
const linear: Easing = p => p
|
|
82
|
+
|
|
83
|
+
export function cubicBezier(
|
|
84
|
+
mX1: number,
|
|
85
|
+
mY1: number,
|
|
86
|
+
mX2: number,
|
|
87
|
+
mY2: number
|
|
88
|
+
): Easing {
|
|
89
|
+
// If this is a linear gradient, return linear easing
|
|
90
|
+
if (mX1 === mY1 && mX2 === mY2) return linear
|
|
91
|
+
|
|
92
|
+
// Precompute samples table
|
|
93
|
+
const sampleValues = new Float32Array(kSplineTableSize)
|
|
94
|
+
|
|
95
|
+
for (let i = 0; i < kSplineTableSize; ++i) {
|
|
96
|
+
sampleValues[i] = calcBezier(i * kSampleStepSize, mX1, mX2)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function getTForX(aX: number) {
|
|
100
|
+
let intervalStart = 0.0
|
|
101
|
+
let currentSample = 1
|
|
102
|
+
const lastSample = kSplineTableSize - 1
|
|
103
|
+
|
|
104
|
+
for (
|
|
105
|
+
;
|
|
106
|
+
currentSample !== lastSample && sampleValues[currentSample] <= aX;
|
|
107
|
+
++currentSample
|
|
108
|
+
) {
|
|
109
|
+
intervalStart += kSampleStepSize
|
|
110
|
+
}
|
|
111
|
+
--currentSample
|
|
112
|
+
|
|
113
|
+
// Interpolate to provide an initial guess for t
|
|
114
|
+
const dist =
|
|
115
|
+
(aX - sampleValues[currentSample]) /
|
|
116
|
+
(sampleValues[currentSample + 1] - sampleValues[currentSample])
|
|
117
|
+
const guessForT = intervalStart + dist * kSampleStepSize
|
|
118
|
+
|
|
119
|
+
const initialSlope = getSlope(guessForT, mX1, mX2)
|
|
120
|
+
if (initialSlope >= newtonMinSlope) {
|
|
121
|
+
return newtonRaphsonIterate(aX, guessForT, mX1, mX2)
|
|
122
|
+
} else if (initialSlope === 0.0) {
|
|
123
|
+
return guessForT
|
|
124
|
+
} else {
|
|
125
|
+
return binarySubdivide(
|
|
126
|
+
aX,
|
|
127
|
+
intervalStart,
|
|
128
|
+
intervalStart + kSampleStepSize,
|
|
129
|
+
mX1,
|
|
130
|
+
mX2
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// If animation is at start/end, return t without easing
|
|
136
|
+
return (t: number) =>
|
|
137
|
+
t === 0 || t === 1 ? t : calcBezier(getTForX(t), mY1, mY2)
|
|
138
|
+
}
|