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.
Files changed (62) hide show
  1. package/dist/index.js +166 -14
  2. package/package.json +3 -2
  3. package/runtime/sdk/animate/animate.ts +238 -0
  4. package/runtime/sdk/animate/bezier.ts +138 -0
  5. package/runtime/sdk/animate/easings.ts +126 -0
  6. package/runtime/sdk/core/color.ts +66 -0
  7. package/runtime/sdk/core/events.ts +38 -0
  8. package/runtime/sdk/core/registry.ts +23 -0
  9. package/runtime/sdk/g2/Camera2D.ts +73 -0
  10. package/runtime/sdk/g2/Node2D.ts +104 -0
  11. package/runtime/sdk/g2/Scene2D.ts +73 -0
  12. package/runtime/sdk/g2/Sprite.ts +164 -0
  13. package/runtime/sdk/g2/Texture2D.ts +37 -0
  14. package/runtime/sdk/g2/Tilemap.ts +41 -0
  15. package/runtime/sdk/g2/loop.ts +22 -0
  16. package/runtime/sdk/gl/Body.ts +101 -0
  17. package/runtime/sdk/gl/Camera.ts +40 -0
  18. package/runtime/sdk/gl/Geometry.ts +218 -0
  19. package/runtime/sdk/gl/Light.ts +37 -0
  20. package/runtime/sdk/gl/Material.ts +124 -0
  21. package/runtime/sdk/gl/Mesh.ts +123 -0
  22. package/runtime/sdk/gl/Node.ts +307 -0
  23. package/runtime/sdk/gl/Noise.ts +30 -0
  24. package/runtime/sdk/gl/Particles.ts +185 -0
  25. package/runtime/sdk/gl/Plane.ts +53 -0
  26. package/runtime/sdk/gl/Ray.ts +16 -0
  27. package/runtime/sdk/gl/Scene.ts +179 -0
  28. package/runtime/sdk/gl/Texture.ts +35 -0
  29. package/runtime/sdk/gl/controls.ts +95 -0
  30. package/runtime/sdk/gl/state.ts +6 -0
  31. package/runtime/sdk/gl/touch.ts +56 -0
  32. package/runtime/sdk/inject.ts +72 -0
  33. package/runtime/sdk/math/Mathf.ts +118 -0
  34. package/runtime/sdk/math/mat4.ts +278 -0
  35. package/runtime/sdk/math/quat.ts +232 -0
  36. package/runtime/sdk/math/vec.ts +255 -0
  37. package/runtime/sdk/plugins/qr.ts +36 -0
  38. package/runtime/sdk/runtime/device.ts +33 -0
  39. package/runtime/sdk/runtime/fetch.ts +71 -0
  40. package/runtime/sdk/runtime/files.ts +23 -0
  41. package/runtime/sdk/runtime/input.ts +12 -0
  42. package/runtime/sdk/runtime/media.ts +111 -0
  43. package/runtime/sdk/runtime/misc.ts +12 -0
  44. package/runtime/sdk/runtime/net.ts +36 -0
  45. package/runtime/sdk/runtime/storage.ts +13 -0
  46. package/runtime/sdk/runtime/touch.ts +44 -0
  47. package/runtime/sdk/ui/UI.ts +19 -0
  48. package/runtime/sdk/ui/UIButton.ts +84 -0
  49. package/runtime/sdk/ui/UIContainer.ts +100 -0
  50. package/runtime/sdk/ui/UIImage.ts +68 -0
  51. package/runtime/sdk/ui/UIInput.ts +78 -0
  52. package/runtime/sdk/ui/UINode.ts +342 -0
  53. package/runtime/sdk/ui/UIScreen.ts +122 -0
  54. package/runtime/sdk/ui/UIScrollable.ts +70 -0
  55. package/runtime/sdk/ui/UISpacer.ts +14 -0
  56. package/runtime/sdk/ui/UIText.ts +40 -0
  57. package/runtime/sdk/ui/UIVideo.ts +38 -0
  58. package/runtime/sdk/ui/UIVirtualizedList.ts +222 -0
  59. package/runtime/sdk/ui/UIWidget.ts +97 -0
  60. package/runtime/sdk/ui/fonts.ts +10 -0
  61. package/runtime/sdk/ui/router.ts +73 -0
  62. 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 readFileSync8 } from "node:fs";
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
- *.png
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 { dirname as dirname4, join as join8 } from "node:path";
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 bundleProject = async (entrypoint, files, format = "esm") => {
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 bundleProject(entry, files, opts.format ?? "esm");
1110
- return opts.header === false ? code : buildHeader(opts.name, code, { icon: opts.icon }) + code;
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 vendoredInject = join9(dirname5(fileURLToPath2(import.meta.url)), "..", "runtime", "inject.js");
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 readFileSync7 } from "node:fs";
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 = readFileSync7(file.absPath, "utf8");
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), readFileSync7(file.absPath));
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 here = dirname7(fileURLToPath3(import.meta.url));
1531
- return JSON.parse(readFileSync8(join11(here, "..", "package.json"), "utf8")).version ?? "unknown";
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.6",
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
+ }