@tinyvolt/pie 0.0.0-stage → 0.2.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/.nvmrc +1 -0
- package/LICENSE +21 -0
- package/README.md +68 -2
- package/functions/midpoint.ts +29 -0
- package/functions/square.ts +20 -0
- package/init/index.js +87 -0
- package/init/site-agents.md +257 -0
- package/init/site-gitignore +28 -0
- package/init/site-tsconfig.json +20 -0
- package/init/site-vite.config.ts +6 -0
- package/layouts/header.tsx +16 -0
- package/layouts/index.tsx +10 -0
- package/nodes/square.tsx +36 -0
- package/package.json +63 -4
- package/pages/about.md +5 -0
- package/pages/example.md +7 -0
- package/pages/python-editor.md +29 -0
- package/public/favicon.ico +0 -0
- package/public/logo.svg +5 -0
- package/site.yml +1 -0
- package/src/app.css +387 -0
- package/src/app.tsx +30 -0
- package/src/authorStyles.ts +3 -0
- package/src/components/Header.tsx +16 -0
- package/src/components/PageMeta.tsx +40 -0
- package/src/components/PageTitle.tsx +10 -0
- package/src/components/SplitPane.tsx +170 -0
- package/src/components/icons.tsx +94 -0
- package/src/entry-client.tsx +4 -0
- package/src/entry-server.tsx +21 -0
- package/src/global.d.ts +7 -0
- package/src/lib/appDir.ts +4 -0
- package/src/lib/autodiff/index.ts +49 -0
- package/src/lib/layouts/layout.ts +20 -0
- package/src/lib/layouts/vitePlugin.ts +39 -0
- package/src/lib/markdown/components/CanvasPage.tsx +25 -0
- package/src/lib/markdown/components/MarkdownBody.tsx +42 -0
- package/src/lib/markdown/components/PythonEditor.tsx +254 -0
- package/src/lib/markdown/frontmatter.ts +83 -0
- package/src/lib/markdown/pages.ts +23 -0
- package/src/lib/markdown/render.ts +62 -0
- package/src/lib/markdown/vitePlugin.ts +94 -0
- package/src/lib/nodes/components/CenteredGrid.tsx +59 -0
- package/src/lib/nodes/components/NodeCanvas.tsx +58 -0
- package/src/lib/nodes/execute.ts +166 -0
- package/src/lib/nodes/functions/Node2NodeFunc.ts +307 -0
- package/src/lib/nodes/functions/array.ts +36 -0
- package/src/lib/nodes/functions/autodiff.ts +64 -0
- package/src/lib/nodes/functions/basic.ts +298 -0
- package/src/lib/nodes/functions/registry.ts +46 -0
- package/src/lib/nodes/functions/sideEffects.ts +193 -0
- package/src/lib/nodes/functions/vitePlugin.ts +40 -0
- package/src/lib/nodes/hooks/useCanvasResize.ts +38 -0
- package/src/lib/nodes/jax.ts +191 -0
- package/src/lib/nodes/render/renderLogic.tsx +306 -0
- package/src/lib/nodes/state/NameGenerator.ts +31 -0
- package/src/lib/nodes/state/Node.ts +211 -0
- package/src/lib/nodes/state/NodeStore.ts +110 -0
- package/src/lib/nodes/tensors/broadcast.ts +101 -0
- package/src/lib/nodes/tensors/tensor.ts +42 -0
- package/src/lib/python/lift.ts +105 -0
- package/src/lib/python/py/__pycache__/pie.cpython-314.pyc +0 -0
- package/src/lib/python/py/pie.py +231 -0
- package/src/lib/python/py/pie_mpl.py +15 -0
- package/src/lib/python/py/show_figures.py +5 -0
- package/src/lib/python/runner.ts +202 -0
- package/src/lib/python/worker.ts +109 -0
- package/src/lib/site/vitePlugin.ts +58 -0
- package/src/routes/[...slug].tsx +39 -0
- package/src/routes/index.tsx +24 -0
- package/src/routes/pages/index.tsx +20 -0
- package/styles/theme.css +27 -0
- package/vite.ts +96 -0
package/.nvmrc
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
24.21.0
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TinyVolt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,69 @@
|
|
|
1
|
-
|
|
1
|
+
<img src="public/logo.svg" alt="pie-ssg logo" width="96">
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
# pie-ssg
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@tinyvolt/pie)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
A Markdown-based SSG for interactive science explainers
|
|
9
|
+
|
|
10
|
+
See the [roadmap](ROADMAP.md) for planned features and [documentation](DOCS.md) for existing features.
|
|
11
|
+
|
|
12
|
+
## Create a site
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npx @tinyvolt/pie init my-site
|
|
16
|
+
cd my-site
|
|
17
|
+
npm install
|
|
18
|
+
npm run dev
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
With pnpm:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
pnpx @tinyvolt/pie init my-site
|
|
25
|
+
cd my-site
|
|
26
|
+
pnpm install
|
|
27
|
+
pnpm dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Leave out `my-site` and it asks for a folder name. The folder must not exist yet, or be empty.
|
|
31
|
+
|
|
32
|
+
The site installs the app as the `@tinyvolt/pie` package. To update it:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
npm install @tinyvolt/pie@latest
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Requires Node 24. Package: [@tinyvolt/pie](https://www.npmjs.com/package/@tinyvolt/pie).
|
|
39
|
+
|
|
40
|
+
## Developing pie-ssg
|
|
41
|
+
|
|
42
|
+
Requires Node 24 (see `.nvmrc`) and pnpm.
|
|
43
|
+
|
|
44
|
+
```sh
|
|
45
|
+
nvm use
|
|
46
|
+
corepack enable
|
|
47
|
+
pnpm install
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
pnpm dev # start the dev server
|
|
54
|
+
pnpm build # build to .output/
|
|
55
|
+
pnpm start # run the built server
|
|
56
|
+
pnpm preview # preview the build with vite
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Publishing
|
|
60
|
+
|
|
61
|
+
Bump `version` in `package.json`, then at the repo root:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
npm whoami
|
|
65
|
+
npm login # if npm whoami fails
|
|
66
|
+
npm publish --dry-run # list the files without publishing
|
|
67
|
+
npm publish
|
|
68
|
+
git tag pie@<version> && git push origin pie@<version>
|
|
69
|
+
```
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Node2NodeFunc } from '~/lib/nodes/functions/Node2NodeFunc';
|
|
2
|
+
import { AnyNode, PointNode } from '~/lib/nodes/state/Node';
|
|
3
|
+
import { add, div, ref, scalar } from '~/lib/nodes/jax';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The point halfway between two points. Called from Python as `pie.midpoint(p1, p2)`.
|
|
7
|
+
* @param point1 - First point
|
|
8
|
+
* @param point2 - Second point
|
|
9
|
+
* @returns Point node
|
|
10
|
+
* @auxiliaryNodes None
|
|
11
|
+
*/
|
|
12
|
+
export const Midpoint: Node2NodeFunc = {
|
|
13
|
+
keyword: 'midpoint', outputType: 'Point',
|
|
14
|
+
parameters: [{ name: 'point1', type: 'Point' }, { name: 'point2', type: 'Point' }],
|
|
15
|
+
compute(inputs: AnyNode[]) {
|
|
16
|
+
const p1 = inputs[0] as PointNode;
|
|
17
|
+
const p2 = inputs[1] as PointNode;
|
|
18
|
+
return {
|
|
19
|
+
main: {
|
|
20
|
+
type: 'Point',
|
|
21
|
+
x: div(add(ref(p1.x), ref(p2.x)), scalar(2)),
|
|
22
|
+
y: div(add(ref(p1.y), ref(p2.y)), scalar(2)),
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Docs category for the functions defined in this file. */
|
|
29
|
+
export const category = 'Custom';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Node2NodeFunc } from '~/lib/nodes/functions/Node2NodeFunc';
|
|
2
|
+
import { AnyNode } from '~/lib/nodes/state/Node';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* An axis-aligned square, drawn by the `Square` node type in `nodes/square.tsx`. Called from Python as `pie.square(center, side)`.
|
|
6
|
+
* @param center - Center point of the square
|
|
7
|
+
* @param side - Side length
|
|
8
|
+
* @returns Square node
|
|
9
|
+
* @auxiliaryNodes None
|
|
10
|
+
*/
|
|
11
|
+
export const Square: Node2NodeFunc = {
|
|
12
|
+
keyword: 'square', outputType: 'Square',
|
|
13
|
+
parameters: [{ name: 'center', type: 'Point' }, { name: 'side', type: 'Scalar' }],
|
|
14
|
+
compute(inputs: AnyNode[]) {
|
|
15
|
+
return { main: { type: 'Square', center: inputs[0], side: inputs[1] } };
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** Docs category for the functions defined in this file. */
|
|
20
|
+
export const category = 'Custom';
|
package/init/index.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `npx @tinyvolt/pie init [dir]`: creates a new pie-ssg site in `dir`, which installs this package.
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import readline from 'node:readline/promises';
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DIR = 'pie-site';
|
|
8
|
+
const USAGE = 'Usage: npx @tinyvolt/pie init [dir]';
|
|
9
|
+
// This package's root. Its example site is where a new site starts.
|
|
10
|
+
const root = path.resolve(import.meta.dirname, '..');
|
|
11
|
+
|
|
12
|
+
// Package path -> path in the new site.
|
|
13
|
+
const FILES = {
|
|
14
|
+
'pages': 'pages',
|
|
15
|
+
'functions': 'functions',
|
|
16
|
+
'nodes': 'nodes',
|
|
17
|
+
'styles': 'styles',
|
|
18
|
+
'layouts': 'layouts',
|
|
19
|
+
'public': 'public',
|
|
20
|
+
'site.yml': 'site.yml',
|
|
21
|
+
'.nvmrc': '.nvmrc',
|
|
22
|
+
// Named so tools working on this repo don't load them. npm drops `.gitignore` from packages.
|
|
23
|
+
'init/site-agents.md': 'AGENTS.md',
|
|
24
|
+
'init/site-vite.config.ts': 'vite.config.ts',
|
|
25
|
+
'init/site-tsconfig.json': 'tsconfig.json',
|
|
26
|
+
'init/site-gitignore': '.gitignore',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
async function askDir() {
|
|
30
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
31
|
+
const answer = await rl.question(`Project folder (${DEFAULT_DIR}): `);
|
|
32
|
+
rl.close();
|
|
33
|
+
return answer.trim() || DEFAULT_DIR;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A valid npm package name from the folder name: lowercase, other characters -> `-`. */
|
|
37
|
+
const toPackageName = (name) =>
|
|
38
|
+
name.toLowerCase().replace(/[^a-z0-9._~-]+/g, '-').replace(/^[._-]+/, '') || DEFAULT_DIR;
|
|
39
|
+
|
|
40
|
+
const [command, dirArg] = process.argv.slice(2);
|
|
41
|
+
if (command !== 'init') {
|
|
42
|
+
console.error(USAGE);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const dir = dirArg || (await askDir());
|
|
47
|
+
const target = path.resolve(dir);
|
|
48
|
+
|
|
49
|
+
if (fs.existsSync(target) && fs.readdirSync(target).length > 0) {
|
|
50
|
+
console.error(`${dir} already exists and is not empty. Pick another folder.`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const [from, to] of Object.entries(FILES)) {
|
|
55
|
+
fs.cpSync(path.join(root, from), path.join(target, to), { recursive: true });
|
|
56
|
+
}
|
|
57
|
+
// Claude Code reads `CLAUDE.md`; `@AGENTS.md` loads the guide from there.
|
|
58
|
+
fs.writeFileSync(path.join(target, 'CLAUDE.md'), '@AGENTS.md\n');
|
|
59
|
+
|
|
60
|
+
// The site depends on this exact version, plus its peers at the exact versions it was tested with.
|
|
61
|
+
// pnpm only lets a site see its own dependencies, so `vite` (the CLI, the config) and `solid-js` (JSX in `nodes/`) are listed here.
|
|
62
|
+
// `--configLoader runner` lets Vite load `@tinyvolt/pie/vite`: Node won't strip TypeScript types under `node_modules`.
|
|
63
|
+
const app = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
64
|
+
const vite = (command) => `vite ${command} --configLoader runner`;
|
|
65
|
+
const pkg = {
|
|
66
|
+
name: toPackageName(path.basename(target)),
|
|
67
|
+
type: 'module',
|
|
68
|
+
scripts: {
|
|
69
|
+
dev: vite('dev'),
|
|
70
|
+
build: vite('build'),
|
|
71
|
+
start: app.scripts.start,
|
|
72
|
+
preview: vite('preview'),
|
|
73
|
+
},
|
|
74
|
+
dependencies: { [app.name]: app.version, ...app.peerDependencies },
|
|
75
|
+
devDependencies: { '@types/node': app.devDependencies['@types/node'], typescript: app.devDependencies.typescript },
|
|
76
|
+
engines: app.engines,
|
|
77
|
+
};
|
|
78
|
+
fs.writeFileSync(path.join(target, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
|
|
79
|
+
|
|
80
|
+
const usesPnpm = (process.env.npm_config_user_agent ?? '').startsWith('pnpm');
|
|
81
|
+
console.log(`
|
|
82
|
+
Created ${dir}. Next:
|
|
83
|
+
|
|
84
|
+
cd ${/\s/.test(dir) ? `"${dir}"` : dir}
|
|
85
|
+
${usesPnpm ? 'pnpm install' : 'npm install'}
|
|
86
|
+
${usesPnpm ? 'pnpm dev' : 'npm run dev'}
|
|
87
|
+
`);
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
[fully written by an agent except this line]
|
|
2
|
+
# pie-ssg site
|
|
3
|
+
|
|
4
|
+
This folder is a site made with [pie-ssg](https://github.com/TinyVolt/pie-ssg): a Markdown-based static site generator for interactive science explainers. Pages are Markdown. Readers can run Python in the page and draw on a canvas.
|
|
5
|
+
|
|
6
|
+
Requires Node 24.
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm run dev # dev server
|
|
10
|
+
npm run build # build to .output/; also runs the checks listed under "Build checks"
|
|
11
|
+
npm run start # run the built server
|
|
12
|
+
npm run preview # preview the build with vite
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Where things go
|
|
16
|
+
|
|
17
|
+
| Folder / file | What it holds |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| `pages/` | Markdown pages. `pages/a/b.md` is served at `/a/b`. |
|
|
20
|
+
| `functions/` | Your own TypeScript functions, called from Python as `pie.<keyword>(...)`. |
|
|
21
|
+
| `nodes/` | Your own node types: what a node holds and how it is drawn. |
|
|
22
|
+
| `layouts/` | Optional overrides: `header.tsx` (top of every page) and `index.tsx` (homepage, `/`). |
|
|
23
|
+
| `styles/` | CSS. Every `.css` file here loads after the built-in styles, so it wins. |
|
|
24
|
+
| `public/` | Static files, served as-is. |
|
|
25
|
+
| `site.yml` | Site settings. |
|
|
26
|
+
| `node_modules/@tinyvolt/pie/src/` | The pie-ssg app itself, installed as the `@tinyvolt/pie` package. `~/` in imports means this folder. |
|
|
27
|
+
|
|
28
|
+
Do the work in the folders above. Don't edit `node_modules/@tinyvolt/pie/`: installing or updating the package replaces it.
|
|
29
|
+
|
|
30
|
+
## site.yml
|
|
31
|
+
|
|
32
|
+
Flat `key: value` lines. Values may be quoted. A key may appear only once.
|
|
33
|
+
|
|
34
|
+
- `title` (required): shown in the header and in every browser tab title.
|
|
35
|
+
- `url`: where the site is deployed, e.g. `https://example.com`. Needed for `sitemap.xml`, `robots.txt`, canonical links and `og:url`; without it the build skips them and warns.
|
|
36
|
+
- `description`: shown in search results and link previews for a page with no `description` of its own.
|
|
37
|
+
|
|
38
|
+
## Pages
|
|
39
|
+
|
|
40
|
+
A page is a `.md` file under `pages/`. `/pages` lists every page.
|
|
41
|
+
|
|
42
|
+
Optional frontmatter at the very top, as flat `key: value` lines:
|
|
43
|
+
|
|
44
|
+
```md
|
|
45
|
+
---
|
|
46
|
+
title: Circles
|
|
47
|
+
canvas: left
|
|
48
|
+
---
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
- `title`: the tab title. Empty means the tab shows the site title alone.
|
|
52
|
+
- `description`: shown in search results and link previews. Empty means `site.yml`'s `description`.
|
|
53
|
+
- `canvas`: `left`, `right`, `top`, `bottom` or `none` (default). Adds a canvas on that side of the page.
|
|
54
|
+
|
|
55
|
+
Other keys are ignored. A bad value fails the build, and shows an error screen in dev.
|
|
56
|
+
|
|
57
|
+
Math uses KaTeX: `$…$` inline, and `$$…$$` on its own lines for a display block.
|
|
58
|
+
|
|
59
|
+
A page's URL must not match a route in `node_modules/@tinyvolt/pie/src/routes/` (`/`, `/pages`). For example, `pages/pages.md` fails the build.
|
|
60
|
+
|
|
61
|
+
## Python editors
|
|
62
|
+
|
|
63
|
+
A `python-editor` code fence becomes a runnable editor with Run and Stop buttons. A plain `python` fence is only highlighted code.
|
|
64
|
+
|
|
65
|
+
````md
|
|
66
|
+
```python-editor
|
|
67
|
+
a = pie.point(1, 2, out='a')
|
|
68
|
+
r = pie.scalar(1, out='r')
|
|
69
|
+
pie.circle(a, r)
|
|
70
|
+
```
|
|
71
|
+
````
|
|
72
|
+
|
|
73
|
+
- Python runs in the browser with Pyodide 0.28.2. `pie` is already imported.
|
|
74
|
+
- All editors on one page share the same Python variables and the same canvas.
|
|
75
|
+
- `import numpy`, `scipy`, `matplotlib` and other [Pyodide packages](https://pyodide.org/en/0.28.2/usage/packages-in-pyodide.html) load on first use.
|
|
76
|
+
- `plt.show()` shows matplotlib figures under the editor.
|
|
77
|
+
- `` ```python-editor collapsed `` starts the editor collapsed: only its toolbar shows until the reader expands it. Other words after `python-editor` are ignored.
|
|
78
|
+
|
|
79
|
+
## Drawing on the canvas: `pie.*`
|
|
80
|
+
|
|
81
|
+
Each `pie.<keyword>(...)` call makes a node: one thing the page tracks, such as a point or a circle. Nodes are drawn on the canvas. When a node changes, every node built from it updates.
|
|
82
|
+
|
|
83
|
+
`out='<id>'` names a node. Calling again with the same `out` replaces that node, and everything built from it updates:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
pie.scalar(3, out='r') # the circle above now has radius 3
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Common mistake: `center = pie.point(1, 2)` does not give the node the id `center`. It gets an automatic id. Use `pie.point(1, 2, out='center')`.
|
|
90
|
+
|
|
91
|
+
`fill='<color>'` and `stroke='<color>'` color a node, e.g. `pie.circle(a, r, fill='orange', stroke='#333')`:
|
|
92
|
+
|
|
93
|
+
- A color is any CSS color (`'red'`, `'#ff0000'`, `'rgb(255, 0, 0)'`) or `'none'`. A bad color raises an error.
|
|
94
|
+
- Every node the call makes gets the colors, including extra nodes such as the center point of `ellipse_from_foci`.
|
|
95
|
+
- A node shows only the colors its render function draws with. A point uses only `fill`, a line only `stroke`. For `partial`'s arrow, `stroke` colors the line and `fill` the head.
|
|
96
|
+
|
|
97
|
+
`unit` is a system node: a scalar holding the pixels per grid unit (default 50). Changing it changes the grid unit, e.g. `pie.scalar(80, out='unit')`.
|
|
98
|
+
|
|
99
|
+
Built-in functions:
|
|
100
|
+
|
|
101
|
+
| Call | Makes |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| `pie.scalar(value)` | a number |
|
|
104
|
+
| `pie.text(value, x, y, font_size, w, h)` | a string; drawn centered at (x, y) only if both are given. All optional: `value` `''`, `font_size` 12 (pixels), `w` and `h` 0. `w`/`h`: 0 and 0 = one line, no box; above 0 = fixed size; -1 = fits the text, at most 5 |
|
|
105
|
+
| `pie.point(x, y)` | a point |
|
|
106
|
+
| `pie.line(p1, p2)` | a line |
|
|
107
|
+
| `pie.distance(p1, p2)` | a number |
|
|
108
|
+
| `pie.triangle(v1, v2, v3)` | a triangle |
|
|
109
|
+
| `pie.circle(center, radius)` | a circle |
|
|
110
|
+
| `pie.ellipse(center, radius_x, radius_y, rotation)` | an ellipse; rotation in degrees |
|
|
111
|
+
| `pie.ellipse_from_foci(f1, f2, string_length)` | an ellipse |
|
|
112
|
+
| `pie.arc(center, radius, start_angle, end_angle)` | an arc; angles in degrees |
|
|
113
|
+
| `pie.bezier_quadratic(p1, control, p2)` | a curve |
|
|
114
|
+
| `pie.bezier_cubic(p1, control1, control2, p2)` | a curve |
|
|
115
|
+
| `pie.partial(target, var)` | the derivative of scalar `target` with respect to `var`. `var` is a scalar node (gives a number) or a point node (gives an arrow at the point). |
|
|
116
|
+
|
|
117
|
+
Arguments can be nodes or plain numbers. Passing an array where one item is expected runs the function once per item (except `partial`).
|
|
118
|
+
|
|
119
|
+
### Side effects
|
|
120
|
+
|
|
121
|
+
These change existing nodes instead of making a new one. They run once, when called.
|
|
122
|
+
|
|
123
|
+
| Call | Does |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `pie.clear()` | removes every node and resets `unit` (pixels per grid unit) to 50 |
|
|
126
|
+
| `pie.translate_point_to(p, x, y)` | moves point `p` to (x, y) |
|
|
127
|
+
| `pie.translate_point_by(p, dx, dy)` | moves point `p` by (dx, dy) |
|
|
128
|
+
| `pie.translate_scalar_to(s, value)` | sets scalar `s` to `value` |
|
|
129
|
+
| `pie.translate_scalar_by(s, delta)` | adds `delta` to scalar `s` |
|
|
130
|
+
| `pie.set_fill(node, color)` | sets `node`'s fill |
|
|
131
|
+
| `pie.set_stroke(node, color)` | sets `node`'s stroke |
|
|
132
|
+
| `pie.hide(node)` | stops drawing `node` |
|
|
133
|
+
| `pie.show(node)` | draws a hidden `node` again |
|
|
134
|
+
| `pie.remove(node)` | removes `node`; nodes built from it are removed too |
|
|
135
|
+
|
|
136
|
+
- `p`, `s` or `node` can be an array: every item changes.
|
|
137
|
+
- A moved node stops following the nodes it was built from. Nodes built from it follow the move. A moved point keeps its colors.
|
|
138
|
+
- The four `translate_*` calls take `animate=True`: the value moves to its new value over half a second instead of jumping. The call returns when the move ends, so a loop of animated calls plays one after another. An array's items move together. Other calls raise an error if passed `animate`.
|
|
139
|
+
|
|
140
|
+
### Lifting a Python function: `pie.lift`
|
|
141
|
+
|
|
142
|
+
`pie.lift(f, out_type)` turns a plain Python function into one that takes nodes and returns a node. It reruns when its input nodes change. `out_type` is one of `pie.Scalar`, `pie.Point`, `pie.Text` (or `str`), `pie.Array[pie.Scalar]`, `pie.Array[pie.Point]`.
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import numpy as np
|
|
146
|
+
linspace = pie.lift(np.linspace, out_type=pie.Array[pie.Scalar])
|
|
147
|
+
n = pie.scalar(5, out='n')
|
|
148
|
+
xs = linspace(0, n, 5)
|
|
149
|
+
pie.point(xs, 0) # 5 points from 0 to 5; set `n` again and they move
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
A lifted function also takes `fill` and `stroke`, e.g. `linspace(0, n, 5, fill='red')`.
|
|
153
|
+
|
|
154
|
+
Limits:
|
|
155
|
+
|
|
156
|
+
- A lifted function can't call `pie.*` or index arrays.
|
|
157
|
+
- `pie.partial` can't differentiate through a lifted function.
|
|
158
|
+
|
|
159
|
+
## Your own functions: `functions/`
|
|
160
|
+
|
|
161
|
+
Each `functions/**/*.ts` file exports one or more `Node2NodeFunc` objects. Each becomes `pie.<keyword>` in Python. Copy `functions/midpoint.ts` as a starting point:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { Node2NodeFunc } from '~/lib/nodes/functions/Node2NodeFunc';
|
|
165
|
+
import { AnyNode, PointNode } from '~/lib/nodes/state/Node';
|
|
166
|
+
import { add, div, ref, scalar } from '~/lib/nodes/jax';
|
|
167
|
+
|
|
168
|
+
export const Midpoint: Node2NodeFunc = {
|
|
169
|
+
keyword: 'midpoint', outputType: 'Point',
|
|
170
|
+
parameters: [{ name: 'point1', type: 'Point' }, { name: 'point2', type: 'Point' }],
|
|
171
|
+
compute(inputs: AnyNode[]) {
|
|
172
|
+
const p1 = inputs[0] as PointNode;
|
|
173
|
+
const p2 = inputs[1] as PointNode;
|
|
174
|
+
return {
|
|
175
|
+
main: { type: 'Point', x: div(add(ref(p1.x), ref(p2.x)), scalar(2)), y: div(add(ref(p1.y), ref(p2.y)), scalar(2)) },
|
|
176
|
+
};
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
- `compute` returns an object whose `main` entry is the output node. Other entries are extra nodes drawn alongside it.
|
|
182
|
+
- Do the math with the helpers in `~/lib/nodes/jax` (`add`, `sub`, `mul`, `div`, `sqrt`, `sin`, `cos`, `atan2`, …), not plain JS operators. Wrap a node's number field in `ref(...)` before using it. These keep `pie.partial` working on your function.
|
|
183
|
+
- Node types and their fields are in `node_modules/@tinyvolt/pie/src/lib/nodes/state/Node.ts`.
|
|
184
|
+
- A file can also export a `SideEffectFunc` (same module as `Node2NodeFunc`): `keyword`, `parameters`, and `run(inputs, store)`, which changes the store instead of returning nodes. See `node_modules/@tinyvolt/pie/src/lib/nodes/functions/sideEffects.ts`.
|
|
185
|
+
- A keyword used twice, by two of your files or by yours and a built-in, fails the build.
|
|
186
|
+
|
|
187
|
+
## Your own node types: `nodes/`
|
|
188
|
+
|
|
189
|
+
Each `nodes/**/*.{ts,tsx}` file exports one or more `NodeDefinition` objects. Each adds a node type that functions can output and the canvas draws. Copy `nodes/square.tsx` as a starting point:
|
|
190
|
+
|
|
191
|
+
```tsx
|
|
192
|
+
import type { Accessor } from 'solid-js';
|
|
193
|
+
import type { Node, NodeDefinition, PointNode, ScalarNode } from '~/lib/nodes/state/Node';
|
|
194
|
+
import { num } from '~/lib/nodes/jax';
|
|
195
|
+
|
|
196
|
+
export interface SquareNode extends Node {
|
|
197
|
+
type: 'Square';
|
|
198
|
+
center: PointNode;
|
|
199
|
+
side: ScalarNode;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Adds 'Square' to the node types, so functions can take and return it.
|
|
203
|
+
declare module '~/lib/nodes/state/Node' {
|
|
204
|
+
interface NodeTypeMap {
|
|
205
|
+
Square: SquareNode;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const renderSquare = (square: Accessor<SquareNode>) => {
|
|
210
|
+
const side = () => num(square().side.value);
|
|
211
|
+
return (
|
|
212
|
+
<rect
|
|
213
|
+
x={num(square().center.x) - side() / 2}
|
|
214
|
+
y={num(square().center.y) - side() / 2}
|
|
215
|
+
width={side()}
|
|
216
|
+
height={side()}
|
|
217
|
+
fill={square().fill ?? 'none'} stroke={square().stroke ?? 'currentColor'}
|
|
218
|
+
stroke-width={2} vector-effect="non-scaling-stroke"
|
|
219
|
+
/>
|
|
220
|
+
);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export const Square: NodeDefinition<SquareNode> = { type: 'Square', prefix: 'sq', render: renderSquare };
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
- `type`: the type name. It must match the interface's `type`.
|
|
227
|
+
- `prefix`: the start of automatic ids: `sq` gives `sq0`, `sq1`, …. Default: the type name in lower case.
|
|
228
|
+
- `render(node, unit)`: returns Solid SVG elements, in grid units. `node()` returns the node's current data. Call it inside the JSX attributes, so the drawing updates when the node changes. `unit` is pixels per grid unit: divide by it for a size that stays fixed on screen. Leave `render` out for a node that isn't drawn.
|
|
229
|
+
- `num(...)` turns a node's number field into a plain JS number.
|
|
230
|
+
- `node().fill` and `node().stroke` hold the colors set from Python, or `undefined`. Use them before your default, e.g. `fill={square().fill ?? 'none'}`. A render function that doesn't read them ignores them.
|
|
231
|
+
- The `declare module` block is needed so that a `Node2NodeFunc` accepts the type in `outputType` and `parameters`.
|
|
232
|
+
- Python makes a node of your type through a function in `functions/`. `functions/square.ts` adds `pie.square(center, side)`.
|
|
233
|
+
- The built-in node types are in `node_modules/@tinyvolt/pie/src/lib/nodes/state/Node.ts`, and their render functions are in `node_modules/@tinyvolt/pie/src/lib/nodes/render/renderLogic.tsx`.
|
|
234
|
+
- A type name used twice, by two of your files or by yours and a built-in, fails the build.
|
|
235
|
+
|
|
236
|
+
## Layouts: `layouts/`
|
|
237
|
+
|
|
238
|
+
- `layouts/header.tsx`: replaces the header on every page.
|
|
239
|
+
- `layouts/index.tsx`: replaces the homepage.
|
|
240
|
+
|
|
241
|
+
Each must `export default` a Solid component (Solid JSX: `class`, not `className`). Delete the file to get the built-in one back. `header.tsx` shows how to read `site.yml`: `import site from "../site.yml"`.
|
|
242
|
+
|
|
243
|
+
## Styles: `styles/`
|
|
244
|
+
|
|
245
|
+
Change the CSS variables in `styles/theme.css` (colors, fonts, text width), or add rules, e.g. `main h1 { color: crimson; }`.
|
|
246
|
+
|
|
247
|
+
## Build checks
|
|
248
|
+
|
|
249
|
+
`npm run build` fails, with a message naming the file, when:
|
|
250
|
+
|
|
251
|
+
- a page's frontmatter or `site.yml` has a bad line or value, or `site.yml` has no `title`
|
|
252
|
+
- a page's URL matches a route in `node_modules/@tinyvolt/pie/src/routes/`
|
|
253
|
+
- a `functions/` keyword is used twice
|
|
254
|
+
- a `nodes/` type name is used twice
|
|
255
|
+
- a `layouts/` file has no default export
|
|
256
|
+
|
|
257
|
+
Run `npm run build` after a change to confirm it.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
dist
|
|
2
|
+
.wrangler
|
|
3
|
+
.output
|
|
4
|
+
.vercel
|
|
5
|
+
.netlify
|
|
6
|
+
.vinxi
|
|
7
|
+
app.config.timestamp_*.js
|
|
8
|
+
|
|
9
|
+
# Environment
|
|
10
|
+
.env
|
|
11
|
+
.env*.local
|
|
12
|
+
|
|
13
|
+
# dependencies
|
|
14
|
+
/node_modules
|
|
15
|
+
|
|
16
|
+
# IDEs and editors
|
|
17
|
+
/.idea
|
|
18
|
+
.project
|
|
19
|
+
.classpath
|
|
20
|
+
*.launch
|
|
21
|
+
.settings/
|
|
22
|
+
|
|
23
|
+
# System Files
|
|
24
|
+
.DS_Store
|
|
25
|
+
Thumbs.db
|
|
26
|
+
|
|
27
|
+
# Python
|
|
28
|
+
__pycache__/
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ESNext",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"allowSyntheticDefaultImports": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"jsx": "preserve",
|
|
9
|
+
"jsxImportSource": "solid-js",
|
|
10
|
+
"allowJs": true,
|
|
11
|
+
"strict": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"types": ["vite/client", "node"],
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"paths": {
|
|
16
|
+
"~/*": ["./node_modules/@tinyvolt/pie/src/*"]
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"include": ["**/*", "node_modules/@tinyvolt/pie/src/global.d.ts"]
|
|
20
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import site from "../site.yml";
|
|
2
|
+
|
|
3
|
+
/** The site name and nav links, shown at the top of every page. */
|
|
4
|
+
export default function Header() {
|
|
5
|
+
return (
|
|
6
|
+
<header class="site-header">
|
|
7
|
+
<a class="site-header-title" href="/">
|
|
8
|
+
{site.title}
|
|
9
|
+
</a>
|
|
10
|
+
<nav class="site-header-nav">
|
|
11
|
+
<a href="/about">About</a>
|
|
12
|
+
<a href="/pages">Pages</a>
|
|
13
|
+
</nav>
|
|
14
|
+
</header>
|
|
15
|
+
);
|
|
16
|
+
}
|
package/nodes/square.tsx
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Accessor } from 'solid-js';
|
|
2
|
+
import type { Node, NodeDefinition, PointNode, ScalarNode } from '~/lib/nodes/state/Node';
|
|
3
|
+
import { num } from '~/lib/nodes/jax';
|
|
4
|
+
|
|
5
|
+
/** An axis-aligned square. Made from Python with `pie.square(center, side)`, see `functions/square.ts`. */
|
|
6
|
+
export interface SquareNode extends Node {
|
|
7
|
+
type: 'Square';
|
|
8
|
+
center: PointNode;
|
|
9
|
+
side: ScalarNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Adds 'Square' to the node types, so functions can take and return it.
|
|
13
|
+
declare module '~/lib/nodes/state/Node' {
|
|
14
|
+
interface NodeTypeMap {
|
|
15
|
+
Square: SquareNode;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const renderSquare = (square: Accessor<SquareNode>) => {
|
|
20
|
+
const side = () => num(square().side.value);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<rect
|
|
24
|
+
x={num(square().center.x) - side() / 2}
|
|
25
|
+
y={num(square().center.y) - side() / 2}
|
|
26
|
+
width={side()}
|
|
27
|
+
height={side()}
|
|
28
|
+
fill={square().fill ?? 'none'}
|
|
29
|
+
stroke={square().stroke ?? 'currentColor'}
|
|
30
|
+
stroke-width={2}
|
|
31
|
+
vector-effect="non-scaling-stroke"
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export const Square: NodeDefinition<SquareNode> = { type: 'Square', prefix: 'sq', render: renderSquare };
|