ncel 0.2.1__tar.gz

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.
ncel-0.2.1/PKG-INFO ADDED
@@ -0,0 +1,122 @@
1
+ Metadata-Version: 2.4
2
+ Name: ncel
3
+ Version: 0.2.1
4
+ Summary: NorthCel (.ncel) one-command CLI: runs the native MoonBit + Bend/HVM2 + GLSL runtime when the platform supports it, and says so plainly when it does not.
5
+ Author: North Noir
6
+ License: Proprietary
7
+ Project-URL: Homepage, https://northnoir.com/northcel
8
+ Project-URL: Documentation, https://northnoir.com/northcel
9
+ Project-URL: Repository, https://github.com/syver777/NorthNoir/tree/main/packaging
10
+ Keywords: ncel,northcel,ai-drawn-video,animation,hand-drawn,moonbit,bend,glsl
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Operating System :: POSIX :: Linux
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Multimedia :: Graphics
20
+ Classifier: Topic :: Multimedia :: Video
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: northcel==0.2.1
24
+ Requires-Dist: ncel-native==0.2.1; sys_platform == "linux" and platform_machine == "x86_64"
25
+
26
+ # ncel — NorthCel, drawn frame by frame by a native runtime
27
+
28
+ `ncel` is the one-command NorthCel (`.ncel`) CLI. It runs the **native runtime**:
29
+
30
+ * **MoonBit** parses the typed scene/film AST, decodes every contour chain, expands
31
+ `def`/`use`, applies frame deltas and the deterministic boil hash;
32
+ * **Bend 0.2.38 / HVM 2.0.22** evaluates every `bend_swarm` agent;
33
+ * **GLSL 4.30**, compiled by the **Mesa/EGL** driver (llvmpipe, no GPU), rasterises
34
+ the normative `aa×aa` even-odd integration.
35
+
36
+ The Python reference implementation (`northcel`) ships alongside as the
37
+ conformance oracle and as a **clearly labelled** fallback where the native runtime
38
+ cannot run. Nothing is ever silently substituted.
39
+
40
+ ## Install
41
+
42
+ ```
43
+ pip install ncel # Linux x86_64
44
+ ncel doctor # stages, exact versions, platform verdict
45
+ ncel verify # real renders + determinism, from bundled fixtures
46
+ ```
47
+
48
+ Supported today: **Linux x86_64**, glibc >= 2.39, Mesa `libegl1` + `libgl1` +
49
+ `mesa-libgallium` (Ubuntu 24.04+, Debian 13, Fedora 40+, RHEL 10).
50
+
51
+ Not supported yet, and it says so: macOS (Apple's OpenGL caps at 4.1; the 4.30
52
+ shader cannot compile — macOS needs Vulkan/MoltenVK or a tested GL 4.1 path),
53
+ Windows, and non-x86_64 Linux. On those platforms `pip install ncel` still
54
+ installs the CLI, `ncel doctor` prints the exact reason, and `render`/`film`/`check`
55
+ use the Python reference renderer, marked `python-reference` — never "native".
56
+
57
+ `pip install ncel` brings `ncel-native` (the ~3 MB runtime bundle, no download at
58
+ first run) and `northcel` (the reference parser/validator/formatter).
59
+
60
+ ## Use
61
+
62
+ ```
63
+ ncel render scene.ncel -o frame.png --frame 0 --json
64
+ ncel film film.ncel -o out/ --frames 0..5 --mp4 out.mp4 --json
65
+ ncel check scene.ncel --json
66
+ ncel info scene.ncel --json
67
+ ncel fmt scene.ncel --check
68
+ ncel guide # offline authoring guide
69
+ ncel doctor --json # machine-readable stage report
70
+ ncel toolchain status|install # MoonBit compiler, only needed to rebuild
71
+ ```
72
+
73
+ `--json` receipts come from the native runtime and include `language`,
74
+ `northcel_version`, `spec_version`, size/fps/frames, source bytes, per-frame
75
+ `sha256`, stage timings, geometry, swarm counts and `"stage": "MoonBit+Bend+GLSL"`.
76
+
77
+ ## What `doctor` proves
78
+
79
+ ```
80
+ ncel 0.2.1 (NorthCel spec 0.2.0-draft)
81
+ platform linux-x86_64 glibc 2.39
82
+ supported yes
83
+ native 0.2.0 (bundle checksums 13 verified, stage MoonBit+Bend+GLSL)
84
+ MoonBit stage present: yes binary ncel-ast compiled with MoonBit 0.1.20261102 (ea07287 2026-11-02)
85
+ Bend bend-lang 0.2.38 …/bin/bend
86
+ HVM2 hvm 2.0.22 …/bin/hvm
87
+ GL runtime 4.5 (Core Profile) Mesa 25.2.8 llvmpipe (LLVM 20.1.2, 256 bits) GLSL 4.50
88
+ GLSL draw430 14829a3cbb6c… compiles draw410 fe4d0c4fe4e0… compiles
89
+ ffmpeg present (optional, --mp4)
90
+ reference northcel 0.2.1 — fallback only — non-native reference renderer
91
+ ```
92
+
93
+ ## What `verify` proves
94
+
95
+ * a starter scene renders to a 320×180 PNG;
96
+ * the shower `f0001` frame reproduces the pinned conformance hash
97
+ (`be0eb036…`), which the Python reference reproduces pixel-for-pixel;
98
+ * a 6-frame film renders 6 distinct frames;
99
+ * both renders repeat with identical sha256 (determinism).
100
+
101
+ ## Toolchain notes
102
+
103
+ A runtime needs no downloads: the MoonBit-compiled stage, Bend and HVM2 are inside
104
+ `ncel-native`, with every file sha256-pinned in `bundle-manifest.json` and checked
105
+ against `native/ncel/toolchain.lock`. The **MoonBit compiler** (101 MB) is only
106
+ needed to rebuild the native tree from source — `ncel toolchain install` fetches
107
+ and verifies it into `~/.local/share/ncel/toolchain`.
108
+
109
+ GLSL is not installed; it is compiled by the GL driver. That is why the installer
110
+ provisions the **Mesa/EGL runtime** and why `doctor` compiles and links both
111
+ shaders as proof.
112
+
113
+ ## Uninstall
114
+
115
+ ```
116
+ pip uninstall ncel ncel-native northcel
117
+ rm -rf ~/.local/share/ncel # only if you ran `ncel toolchain install`
118
+ ```
119
+
120
+ ## Licence
121
+
122
+ Proprietary — North Noir. See `LICENSE` in the NorthCel distribution.
ncel-0.2.1/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # ncel — NorthCel, drawn frame by frame by a native runtime
2
+
3
+ `ncel` is the one-command NorthCel (`.ncel`) CLI. It runs the **native runtime**:
4
+
5
+ * **MoonBit** parses the typed scene/film AST, decodes every contour chain, expands
6
+ `def`/`use`, applies frame deltas and the deterministic boil hash;
7
+ * **Bend 0.2.38 / HVM 2.0.22** evaluates every `bend_swarm` agent;
8
+ * **GLSL 4.30**, compiled by the **Mesa/EGL** driver (llvmpipe, no GPU), rasterises
9
+ the normative `aa×aa` even-odd integration.
10
+
11
+ The Python reference implementation (`northcel`) ships alongside as the
12
+ conformance oracle and as a **clearly labelled** fallback where the native runtime
13
+ cannot run. Nothing is ever silently substituted.
14
+
15
+ ## Install
16
+
17
+ ```
18
+ pip install ncel # Linux x86_64
19
+ ncel doctor # stages, exact versions, platform verdict
20
+ ncel verify # real renders + determinism, from bundled fixtures
21
+ ```
22
+
23
+ Supported today: **Linux x86_64**, glibc >= 2.39, Mesa `libegl1` + `libgl1` +
24
+ `mesa-libgallium` (Ubuntu 24.04+, Debian 13, Fedora 40+, RHEL 10).
25
+
26
+ Not supported yet, and it says so: macOS (Apple's OpenGL caps at 4.1; the 4.30
27
+ shader cannot compile — macOS needs Vulkan/MoltenVK or a tested GL 4.1 path),
28
+ Windows, and non-x86_64 Linux. On those platforms `pip install ncel` still
29
+ installs the CLI, `ncel doctor` prints the exact reason, and `render`/`film`/`check`
30
+ use the Python reference renderer, marked `python-reference` — never "native".
31
+
32
+ `pip install ncel` brings `ncel-native` (the ~3 MB runtime bundle, no download at
33
+ first run) and `northcel` (the reference parser/validator/formatter).
34
+
35
+ ## Use
36
+
37
+ ```
38
+ ncel render scene.ncel -o frame.png --frame 0 --json
39
+ ncel film film.ncel -o out/ --frames 0..5 --mp4 out.mp4 --json
40
+ ncel check scene.ncel --json
41
+ ncel info scene.ncel --json
42
+ ncel fmt scene.ncel --check
43
+ ncel guide # offline authoring guide
44
+ ncel doctor --json # machine-readable stage report
45
+ ncel toolchain status|install # MoonBit compiler, only needed to rebuild
46
+ ```
47
+
48
+ `--json` receipts come from the native runtime and include `language`,
49
+ `northcel_version`, `spec_version`, size/fps/frames, source bytes, per-frame
50
+ `sha256`, stage timings, geometry, swarm counts and `"stage": "MoonBit+Bend+GLSL"`.
51
+
52
+ ## What `doctor` proves
53
+
54
+ ```
55
+ ncel 0.2.1 (NorthCel spec 0.2.0-draft)
56
+ platform linux-x86_64 glibc 2.39
57
+ supported yes
58
+ native 0.2.0 (bundle checksums 13 verified, stage MoonBit+Bend+GLSL)
59
+ MoonBit stage present: yes binary ncel-ast compiled with MoonBit 0.1.20261102 (ea07287 2026-11-02)
60
+ Bend bend-lang 0.2.38 …/bin/bend
61
+ HVM2 hvm 2.0.22 …/bin/hvm
62
+ GL runtime 4.5 (Core Profile) Mesa 25.2.8 llvmpipe (LLVM 20.1.2, 256 bits) GLSL 4.50
63
+ GLSL draw430 14829a3cbb6c… compiles draw410 fe4d0c4fe4e0… compiles
64
+ ffmpeg present (optional, --mp4)
65
+ reference northcel 0.2.1 — fallback only — non-native reference renderer
66
+ ```
67
+
68
+ ## What `verify` proves
69
+
70
+ * a starter scene renders to a 320×180 PNG;
71
+ * the shower `f0001` frame reproduces the pinned conformance hash
72
+ (`be0eb036…`), which the Python reference reproduces pixel-for-pixel;
73
+ * a 6-frame film renders 6 distinct frames;
74
+ * both renders repeat with identical sha256 (determinism).
75
+
76
+ ## Toolchain notes
77
+
78
+ A runtime needs no downloads: the MoonBit-compiled stage, Bend and HVM2 are inside
79
+ `ncel-native`, with every file sha256-pinned in `bundle-manifest.json` and checked
80
+ against `native/ncel/toolchain.lock`. The **MoonBit compiler** (101 MB) is only
81
+ needed to rebuild the native tree from source — `ncel toolchain install` fetches
82
+ and verifies it into `~/.local/share/ncel/toolchain`.
83
+
84
+ GLSL is not installed; it is compiled by the GL driver. That is why the installer
85
+ provisions the **Mesa/EGL runtime** and why `doctor` compiles and links both
86
+ shaders as proof.
87
+
88
+ ## Uninstall
89
+
90
+ ```
91
+ pip uninstall ncel ncel-native northcel
92
+ rm -rf ~/.local/share/ncel # only if you ran `ncel toolchain install`
93
+ ```
94
+
95
+ ## Licence
96
+
97
+ Proprietary — North Noir. See `LICENSE` in the NorthCel distribution.
@@ -0,0 +1,18 @@
1
+ """`ncel` — the one-command NorthCel CLI.
2
+
3
+ The native runtime (MoonBit typed AST + Bend/HVM2 swarms + GLSL via Mesa/EGL) is
4
+ the implementation this CLI calls whenever it is available. The Python reference
5
+ renderer (`northcel`) is used only when the native stages genuinely cannot run on
6
+ the current platform, and then the tool says so and labels the result.
7
+ """
8
+
9
+ __version__ = "0.2.1"
10
+
11
+ #: Version of the bundled native runtime build (native/ncel, commit 3ad5f7e).
12
+ NATIVE_RUNTIME_VERSION = "0.2.0"
13
+
14
+ #: NorthCel language specification implemented by both implementations.
15
+ SPEC_VERSION = "0.2.0-draft"
16
+
17
+ #: Exit code used when the native runtime is unavailable on this platform.
18
+ EXIT_NO_NATIVE = 3
@@ -0,0 +1,10 @@
1
+ """`python -m ncel` — same entry point as the `ncel` script."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+
7
+ from .cli import main
8
+
9
+ if __name__ == "__main__":
10
+ sys.exit(main())
@@ -0,0 +1,84 @@
1
+ """Locate and verify the bundled native runtime (`ncel-native` wheel)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+
9
+
10
+ def bundle_dir() -> Path | None:
11
+ """Directory of the installed native bundle, or None when it is not installed."""
12
+ try:
13
+ import ncel_native_bundle # noqa: PLC0415 - optional platform dependency
14
+ except Exception:
15
+ return None
16
+ path = Path(ncel_native_bundle.__file__).resolve().parent
17
+ return path if (path / "bundle-manifest.json").is_file() else path
18
+
19
+
20
+ def manifest_path() -> Path | None:
21
+ root = bundle_dir()
22
+ if not root:
23
+ return None
24
+ manifest = root / "bundle-manifest.json"
25
+ return manifest if manifest.is_file() else None
26
+
27
+
28
+ def load_manifest() -> dict | None:
29
+ path = manifest_path()
30
+ if not path:
31
+ return None
32
+ try:
33
+ return json.loads(path.read_text(encoding="utf-8"))
34
+ except Exception:
35
+ return None
36
+
37
+
38
+ def binary() -> Path | None:
39
+ root = bundle_dir()
40
+ if not root:
41
+ return None
42
+ exe = root / "bin" / "ncel-native"
43
+ return exe if exe.is_file() else None
44
+
45
+
46
+ def sha256_file(path: Path) -> str:
47
+ digest = hashlib.sha256()
48
+ with path.open("rb") as handle:
49
+ for chunk in iter(lambda: handle.read(1 << 20), b""):
50
+ digest.update(chunk)
51
+ return digest.hexdigest()
52
+
53
+
54
+ def verify_checksums(manifest: dict | None = None) -> dict:
55
+ """Hash every file named in the bundle manifest.
56
+
57
+ Returns {"checked": n, "failed": [{"path", "expected", "actual"}], "missing": [...]}.
58
+ """
59
+ root = bundle_dir()
60
+ manifest = manifest if manifest is not None else load_manifest()
61
+ if not root or not manifest:
62
+ return {"checked": 0, "failed": [], "missing": [], "available": False}
63
+
64
+ checked = 0
65
+ failed: list[dict] = []
66
+ missing: list[str] = []
67
+ for entry in manifest.get("files", []):
68
+ rel = entry.get("path", "")
69
+ target = root / rel
70
+ if not target.is_file():
71
+ missing.append(rel)
72
+ continue
73
+ actual = sha256_file(target)
74
+ checked += 1
75
+ if actual != entry.get("sha256"):
76
+ failed.append(
77
+ {"path": rel, "expected": entry.get("sha256"), "actual": actual}
78
+ )
79
+ return {
80
+ "checked": checked,
81
+ "failed": failed,
82
+ "missing": missing,
83
+ "available": True,
84
+ }
ncel-0.2.1/ncel/cli.py ADDED
@@ -0,0 +1,189 @@
1
+ """`ncel` command-line entry point.
2
+
3
+ Dispatch rule (this is the whole product promise): if the native runtime can run
4
+ here, run it. If it cannot, say why, use the Python reference and label the result.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ from . import EXIT_NO_NATIVE, NATIVE_RUNTIME_VERSION, SPEC_VERSION, __version__, fallback, native
13
+ from . import doctor as doctor_mod
14
+ from . import toolchain as toolchain_mod
15
+ from . import verify as verify_mod
16
+
17
+ USAGE = """ncel — NorthCel (.ncel) command line
18
+
19
+ usage: ncel <command> [options]
20
+
21
+ doctor [--json] stage availability, exact versions, platform verdict
22
+ verify [--json] [--keep DIR] render the bundled fixtures with the native runtime
23
+ check FILE [--json] parse + validate (native when available)
24
+ info FILE [--json] scene/film metadata, layers, rings, vertices
25
+ render FILE [-o OUT.png] [--frame N] [--seed N] [--json]
26
+ film FILE -o OUTDIR [--frames 0..5] [--mp4 OUT.mp4] [--json]
27
+ fmt FILE [-w|--write] [--check] canonical formatting (Python reference)
28
+ guide offline authoring guide
29
+ toolchain <status|install> [--json] [--dry-run]
30
+ version, --version versions (launcher, native runtime, spec)
31
+
32
+ Global: --no-native force the labelled Python reference path (never called "native")
33
+ Platform: Linux x86_64, glibc >= 2.39, Mesa EGL/llvmpipe. macOS/Windows are not
34
+ supported in this release; `ncel doctor` explains exactly what is missing.
35
+ """
36
+
37
+
38
+ def _version_line() -> str:
39
+ native_version = NATIVE_RUNTIME_VERSION
40
+ usable, _ = native.available()
41
+ if usable:
42
+ doc = native.doctor_json() or {}
43
+ if doc.get("version"):
44
+ native_version = doc["version"]
45
+ state = "native" if usable else "python-reference only"
46
+ return f"ncel {__version__} (native runtime {native_version}, spec {SPEC_VERSION}, {state})"
47
+
48
+
49
+ def _pop_flag(args: list[str], *names: str) -> bool:
50
+ hit = False
51
+ for name in names:
52
+ while name in args:
53
+ args.remove(name)
54
+ hit = True
55
+ return hit
56
+
57
+
58
+ def _pop_option(args: list[str], *names: str) -> str | None:
59
+ for name in names:
60
+ for index, token in enumerate(args):
61
+ if token == name and index + 1 < len(args):
62
+ value = args[index + 1]
63
+ del args[index : index + 2]
64
+ return value
65
+ if token.startswith(name + "="):
66
+ value = token.split("=", 1)[1]
67
+ del args[index]
68
+ return value
69
+ return None
70
+
71
+
72
+ def _native_or_reason(args: list[str], command: str) -> tuple[bool, str | None]:
73
+ if _pop_flag(args, "--no-native", "--python"):
74
+ return False, "explicitly requested with --no-native"
75
+ return native.available() if command != "" else (False, None)
76
+
77
+
78
+ def _passthrough(command: str, args: list[str], strip_native_only: bool = True) -> int:
79
+ usable, reason = _native_or_reason(args, command)
80
+ if strip_native_only:
81
+ _pop_flag(args, "--require-full")
82
+ if not usable:
83
+ fallback.notify(reason)
84
+ return fallback.delegate([command, *args])
85
+ return native.run([command, *args]).returncode
86
+
87
+
88
+ def _cmd_render(args: list[str]) -> int:
89
+ usable, reason = _native_or_reason(args, "render")
90
+ if usable:
91
+ return native.run(["render", *args]).returncode
92
+ fallback.notify(reason)
93
+ as_json = _pop_flag(args, "--json")
94
+ out = _pop_option(args, "-o", "--out")
95
+ frame_raw = _pop_option(args, "-f", "--frame") or "0"
96
+ seed_raw = _pop_option(args, "--seed")
97
+ files = [a for a in args if not a.startswith("-")]
98
+ if not files:
99
+ print("usage: ncel render FILE [-o OUT.png] [--frame N] [--seed N]", file=sys.stderr)
100
+ return 2
101
+ try:
102
+ frame = int(frame_raw)
103
+ seed = int(seed_raw) if seed_raw is not None else None
104
+ except ValueError:
105
+ print("ncel: --frame/--seed must be integers", file=sys.stderr)
106
+ return 2
107
+ return fallback.render(files[0], out, frame, seed, as_json)
108
+
109
+
110
+ def _cmd_film(args: list[str]) -> int:
111
+ usable, reason = _native_or_reason(args, "film")
112
+ if usable:
113
+ return native.run(["film", *args]).returncode
114
+ fallback.notify(reason)
115
+ as_json = _pop_flag(args, "--json")
116
+ out_dir = _pop_option(args, "-o", "--out-dir")
117
+ frames = _pop_option(args, "--frames")
118
+ mp4 = _pop_option(args, "--mp4")
119
+ files = [a for a in args if not a.startswith("-")]
120
+ if not files or not out_dir:
121
+ print("usage: ncel film FILE -o OUTDIR [--frames 0..5] [--mp4 OUT.mp4]", file=sys.stderr)
122
+ return 2
123
+ return fallback.film(files[0], out_dir, frames, mp4, as_json)
124
+
125
+
126
+ def _cmd_guide(args: list[str]) -> int:
127
+ path = Path(__file__).with_name("data") / "AGENT-GUIDE.md"
128
+ if path.is_file():
129
+ sys.stdout.write(path.read_text(encoding="utf-8"))
130
+ return 0
131
+ return fallback.delegate(["guide", *args])
132
+
133
+
134
+ def _cmd_toolchain(args: list[str]) -> int:
135
+ as_json = _pop_flag(args, "--json")
136
+ dry_run = _pop_flag(args, "--dry-run")
137
+ action = next((a for a in args if not a.startswith("-")), "status")
138
+ return toolchain_mod.cmd_toolchain(action, as_json, dry_run)
139
+
140
+
141
+ def main(argv: list[str] | None = None) -> int:
142
+ args = list(sys.argv[1:] if argv is None else argv)
143
+ if not args or args[0] in {"-h", "--help", "help"}:
144
+ print(USAGE, end="")
145
+ return 0
146
+
147
+ command, rest = args[0], args[1:]
148
+
149
+ if command in {"--version", "version", "-V"}:
150
+ print(_version_line())
151
+ return 0
152
+
153
+ if command == "doctor":
154
+ return doctor_mod.cmd_doctor(_pop_flag(rest, "--json"))
155
+
156
+ if command == "verify":
157
+ as_json = _pop_flag(rest, "--json")
158
+ keep = _pop_option(rest, "--keep")
159
+ return verify_mod.cmd_verify(as_json, keep)
160
+
161
+ if command == "check":
162
+ return _passthrough("check", rest)
163
+ if command == "info":
164
+ return _passthrough("info", rest)
165
+
166
+ if command == "fmt":
167
+ # The native runtime has no formatter; formatting is the Python reference.
168
+ return fallback.delegate(["fmt", *rest])
169
+
170
+ if command == "guide":
171
+ return _cmd_guide(rest)
172
+
173
+ if command == "render":
174
+ return _cmd_render(rest)
175
+ if command == "film":
176
+ return _cmd_film(rest)
177
+
178
+ if command == "toolchain":
179
+ return _cmd_toolchain(rest)
180
+
181
+ if command in {"--json-doctor"}:
182
+ return doctor_mod.cmd_doctor(True)
183
+
184
+ print(f"ncel: unknown command {command!r}\n", file=sys.stderr)
185
+ print(USAGE, end="", file=sys.stderr)
186
+ return 2
187
+
188
+
189
+ __all__ = ["main", "EXIT_NO_NATIVE"]
@@ -0,0 +1,92 @@
1
+ # NorthCel quick agent guide — tools 0.2.1, spec 0.2.0-draft
2
+
3
+ Discover this guide offline with `npx northcel guide`. The npm package is a
4
+ zero-dependency **partial JS pre-check and Python CLI bridge**, not a renderer.
5
+
6
+ ## Start here (a complete 3-second film)
7
+
8
+ ```ncel
9
+ northcel "0.2.0-draft"
10
+ film Kite(width: 320, height: 180, fps: 12, frames: 36, precision: 0.25px, seed: 42) {
11
+ palette { ink: #202830, red: #D65C52 }
12
+ paper { color: #FFF8EC, antialias_grid: 4 }
13
+ cel kite(color: $red, outline: ($ink, 2px), boil: 0.3px,
14
+ translate: (lerp(0, 100, t), sin(t * 6.28318) * 10)) {
15
+ "240,120 100,80 -100,140 -100,-140"
16
+ }
17
+ stroke string(color: $ink, taper: (1.5, 0.5)) {
18
+ (60 + 100 * t, 85 + sin(t * 6.28318) * 10), (110, 160)
19
+ }
20
+ }
21
+ ```
22
+
23
+ Coordinates are pixels, origin top left. Quoted rings are **relative integer
24
+ quarter-pixel pairs**, NOT absolute pixel vertices: multiply vertices by 4,
25
+ write the first vertex then deltas; at least three vertices per ring. `|`
26
+ separates rings (even-odd holes). `precision: 1px` and `0.25px-b63` instead use
27
+ `x0,y0:ABCD...` Base-63 pairs; prefer plain quarter-pixel deltas for hand authoring.
28
+ Never put expressions inside a quoted ring: move it with `translate: (x, y)`.
29
+
30
+ ## Five language features to combine
31
+
32
+ 1. **Reusable shapes:** `def Mark(dx: 0, dy: 0) { stroke line(width: 2) { (0, 0), (8, 4) } }`
33
+ then `use Mark(dx: 20, dy: 30)`. Only primitives inside `def`, not nested `use`.
34
+ `dx/dy` auto-translate expanded geometry; don't also add them to coordinates.
35
+ Same-named cels in repeated uses replace one another; use distinct named defs
36
+ for independent cel instances (stroke/ribbon uses append).
37
+ 2. **Film/time:** `frames` belongs in the film **header**, never `film X { frames: 36 }`.
38
+ Duration = frames / fps. `t = f / max(1, frames - 1)`; `f` is zero-based.
39
+ Expressions: numbers, parameters, `t`, `f`, `seed`, `+ - * /`, `sin(x)`,
40
+ `cos(x)`, `lerp(a, b, t)`, `step(edge, x)`. Use spaces around minus.
41
+ `frame 12 { ... }` affects exactly frame 12, not all subsequent frames.
42
+ 3. **Deterministic line boil:** `boil: 0.3px` or `boil: (0.3, 1)`; keep it subtle.
43
+ No randomness, wall clock or cross-frame state.
44
+ 4. **Colour/cels:** `palette { ink: #202830 }` before `$ink` references; hex, `rgb(r,g,b)`
45
+ or `tone: 0..255`. `cel name(color: $ink, outline: (#000, 2px)) { "..." }`
46
+ couples fill and outline. `layer`/`wash` fills; `bend_swarm`/`swarm` adds detail rings.
47
+ 5. **Tapered ink:** `stroke name(color: $ink, taper: (3, 1)) { (10, 10), (40, 20) }`;
48
+ `ribbon name(color: $ink) { (10, 10, 3), (40, 20, 1) }` uses per-point widths.
49
+ At least two points. Give every primitive a distinct name in each block.
50
+
51
+ ## Honest validation and look-and-fix loop
52
+
53
+ 1. Run `npx northcel check film.ncel --json`. Without Python it reports
54
+ `partial: true`, `validator: "js"`, a reason and next command. `ok: true` and
55
+ exit 0 mean **only that the JS pre-check found no error**, not full validity.
56
+ Text says **PARTIAL**, never OK. Syntax/header/ring/palette checks help, but
57
+ numeric ranges, expression evaluation, def resolution and render quality remain unchecked.
58
+ 2. Run `npx northcel check film.ncel --require-full --json` before claiming full
59
+ validation. Exit 2 = no full validator (clean partial); exit 1 = detected error;
60
+ exit 0 = Python full check succeeded. Install `pipx install northcel` (or
61
+ `pip install northcel` in a venv), expose its Python CLI on PATH, then repeat.
62
+ The npm bridge finds importable Python modules or pipx-style Python entrypoints.
63
+ Never suppress exit 2 or call a partial check fully validated.
64
+ 3. Python: `northcel render film.ncel -o frame0.png --frame 0`, inspect the PNG;
65
+ `northcel film film.ncel -o frames/ --mp4 preview.mp4`, inspect first/middle/last
66
+ and transition frames. ffmpeg is required for MP4; verify the MP4 exists.
67
+ Render samples in reverse order and compare native RGB hashes for determinism.
68
+ 4. Fix the named block that is wrong, preserve good parts, render again; up to
69
+ three look-and-fix rounds. `northcel fmt -w film.ncel` canonicalizes it.
70
+ Full validation is not a visual-quality or runtime-expression guarantee.
71
+
72
+ Alternatively install `pip install "northnoir[ncel]"` and use
73
+ `northnoir init ink --language ncel --duration 3 --fps 24`,
74
+ `northnoir check ink/scene.ncel`, `northnoir render ink/scene.ncel -o ink/video.mp4`.
75
+ This is an explicit optional local path; JavaScript `scene.html` stays the default.
76
+ NorthCel films are silent; there is no native audio engine or hosted NorthCel API.
77
+
78
+ ## Common mistakes
79
+
80
+ - No root, unclosed quote/brace, unknown statement: fix the reported line:column.
81
+ - `frames:` in the body: move it to `film Name(..., frames: N) { ... }`.
82
+ - Missing primitive attributes: `cel`, `layer`, `swarm`, `stroke` need `(...)`,
83
+ even `()`. `ribbon`, `def` and `use` may omit it. Palette takes `{ key: value }`
84
+ or `(key: value)`, not a name. `use Name(...)` has **no body**.
85
+ - Invalid/odd Base-63 tail: use only `0-9A-Za-z_`, two characters per delta.
86
+ Quarter-pixel payloads need whole integer pairs, not decimals or suffixes.
87
+ - Undefined `$colour`: declare it earlier, including before `def` bodies.
88
+ - Duplicate names: intentional layer/cel replacement is legal (JS warns, not errors).
89
+ Frame overrides replace named layers/cels; same-named strokes/ribbons append,
90
+ leaving stale ink. For moving ink use cels or time expressions instead.
91
+ - Undefined def, bad sizes/fps, expression variable errors: JS may miss these;
92
+ use Python full check **and real renders**. No JS success authorizes shipping.
@@ -0,0 +1,14 @@
1
+ northcel "0.1.0-draft"
2
+
3
+ film LanternFilm(width: 512, height: 512, fps: 12, frames: 6, precision: 0.25px, seed: 108) {
4
+ paper { color: #101824, antialias_grid: 4, grain: 0.012 }
5
+ def SeaWave(dx: 0, dy: 0) {
6
+ stroke crest(color: #6CCFF6, taper: (4.5, 1.5), boil: 0.75px) { (40 + dx, 410 + dy), (180 + dx, 398 + dy), (330 + dx, 412 + dy), (470 + dx, 402 + dy) }
7
+ }
8
+ layer sea_base(color: #1B3B5A, rings: 1) { "0,1520 2048,0 0,528 -2048,0" }
9
+ layer lantern_halo(color: #F4A261, rings: 1, boil: 0.6px, translate: (sin(t * 6.28318) * 14, 0)) { "920,640 208,0 40,120 -40,120 -208,0 -40,-120" }
10
+ ribbon post_ink(color: #F8F9FA, boil: 0.4px) { (256, 480, 12), (256 + sin(t * 6.28318) * 6, 320, 9), (256 + sin(t * 6.28318) * 14, 190, 6) }
11
+ bend_swarm rain_streaks(agents: 4, color: #E0FBFC, boil: 0.35px, translate: (lerp(-18, 18, t), lerp(0, 48, t))) { "320,240 24,72 -8,4 -24,-72|840,300 24,72 -8,4 -24,-72|1420,220 24,72 -8,4 -24,-72|1760,340 24,72 -8,4 -24,-72" }
12
+ use SeaWave(dx: sin(t * 6.28318) * 10, dy: 0)
13
+ use SeaWave(dx: cos(t * 6.28318) * -12, dy: 42)
14
+ }