@stevejtrettel/shader-sandbox 0.1.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/README.md +391 -0
- package/bin/cli.js +389 -0
- package/dist-lib/app/App.d.ts +134 -0
- package/dist-lib/app/App.d.ts.map +1 -0
- package/dist-lib/app/App.js +570 -0
- package/dist-lib/app/types.d.ts +32 -0
- package/dist-lib/app/types.d.ts.map +1 -0
- package/dist-lib/app/types.js +6 -0
- package/dist-lib/editor/EditorPanel.d.ts +39 -0
- package/dist-lib/editor/EditorPanel.d.ts.map +1 -0
- package/dist-lib/editor/EditorPanel.js +274 -0
- package/dist-lib/editor/prism-editor.css +99 -0
- package/dist-lib/editor/prism-editor.d.ts +19 -0
- package/dist-lib/editor/prism-editor.d.ts.map +1 -0
- package/dist-lib/editor/prism-editor.js +96 -0
- package/dist-lib/embed.d.ts +17 -0
- package/dist-lib/embed.d.ts.map +1 -0
- package/dist-lib/embed.js +35 -0
- package/dist-lib/engine/ShadertoyEngine.d.ts +160 -0
- package/dist-lib/engine/ShadertoyEngine.d.ts.map +1 -0
- package/dist-lib/engine/ShadertoyEngine.js +704 -0
- package/dist-lib/engine/glHelpers.d.ts +79 -0
- package/dist-lib/engine/glHelpers.d.ts.map +1 -0
- package/dist-lib/engine/glHelpers.js +298 -0
- package/dist-lib/engine/types.d.ts +77 -0
- package/dist-lib/engine/types.d.ts.map +1 -0
- package/dist-lib/engine/types.js +7 -0
- package/dist-lib/index.d.ts +12 -0
- package/dist-lib/index.d.ts.map +1 -0
- package/dist-lib/index.js +9 -0
- package/dist-lib/layouts/DefaultLayout.d.ts +17 -0
- package/dist-lib/layouts/DefaultLayout.d.ts.map +1 -0
- package/dist-lib/layouts/DefaultLayout.js +27 -0
- package/dist-lib/layouts/FullscreenLayout.d.ts +17 -0
- package/dist-lib/layouts/FullscreenLayout.d.ts.map +1 -0
- package/dist-lib/layouts/FullscreenLayout.js +27 -0
- package/dist-lib/layouts/SplitLayout.d.ts +26 -0
- package/dist-lib/layouts/SplitLayout.d.ts.map +1 -0
- package/dist-lib/layouts/SplitLayout.js +61 -0
- package/dist-lib/layouts/TabbedLayout.d.ts +38 -0
- package/dist-lib/layouts/TabbedLayout.d.ts.map +1 -0
- package/dist-lib/layouts/TabbedLayout.js +305 -0
- package/dist-lib/layouts/index.d.ts +24 -0
- package/dist-lib/layouts/index.d.ts.map +1 -0
- package/dist-lib/layouts/index.js +36 -0
- package/dist-lib/layouts/split.css +196 -0
- package/dist-lib/layouts/tabbed.css +345 -0
- package/dist-lib/layouts/types.d.ts +48 -0
- package/dist-lib/layouts/types.d.ts.map +1 -0
- package/dist-lib/layouts/types.js +4 -0
- package/dist-lib/main.d.ts +15 -0
- package/dist-lib/main.d.ts.map +1 -0
- package/dist-lib/main.js +102 -0
- package/dist-lib/project/generatedLoader.d.ts +3 -0
- package/dist-lib/project/generatedLoader.d.ts.map +1 -0
- package/dist-lib/project/generatedLoader.js +17 -0
- package/dist-lib/project/loadProject.d.ts +22 -0
- package/dist-lib/project/loadProject.d.ts.map +1 -0
- package/dist-lib/project/loadProject.js +350 -0
- package/dist-lib/project/loaderHelper.d.ts +7 -0
- package/dist-lib/project/loaderHelper.d.ts.map +1 -0
- package/dist-lib/project/loaderHelper.js +240 -0
- package/dist-lib/project/types.d.ts +192 -0
- package/dist-lib/project/types.d.ts.map +1 -0
- package/dist-lib/project/types.js +7 -0
- package/dist-lib/styles/base.css +29 -0
- package/package.json +48 -0
- package/src/app/App.ts +699 -0
- package/src/app/app.css +208 -0
- package/src/app/types.ts +36 -0
- package/src/editor/EditorPanel.ts +340 -0
- package/src/editor/editor-panel.css +175 -0
- package/src/editor/prism-editor.css +99 -0
- package/src/editor/prism-editor.ts +124 -0
- package/src/embed.ts +55 -0
- package/src/engine/ShadertoyEngine.ts +929 -0
- package/src/engine/glHelpers.ts +432 -0
- package/src/engine/types.ts +118 -0
- package/src/index.ts +13 -0
- package/src/layouts/DefaultLayout.ts +40 -0
- package/src/layouts/FullscreenLayout.ts +40 -0
- package/src/layouts/SplitLayout.ts +81 -0
- package/src/layouts/TabbedLayout.ts +371 -0
- package/src/layouts/default.css +22 -0
- package/src/layouts/fullscreen.css +15 -0
- package/src/layouts/index.ts +44 -0
- package/src/layouts/split.css +196 -0
- package/src/layouts/tabbed.css +345 -0
- package/src/layouts/types.ts +58 -0
- package/src/main.ts +114 -0
- package/src/project/generatedLoader.ts +23 -0
- package/src/project/loadProject.ts +421 -0
- package/src/project/loaderHelper.ts +300 -0
- package/src/project/types.ts +243 -0
- package/src/styles/base.css +29 -0
- package/src/styles/embed.css +14 -0
- package/src/vite-env.d.ts +1 -0
- package/templates/index.html +28 -0
- package/templates/main.ts +126 -0
- package/templates/package.json +12 -0
- package/templates/shaders/example-buffer/bufferA.glsl +14 -0
- package/templates/shaders/example-buffer/config.json +10 -0
- package/templates/shaders/example-buffer/image.glsl +5 -0
- package/templates/shaders/example-gradient/config.json +4 -0
- package/templates/shaders/example-gradient/image.glsl +7 -0
- package/templates/vite.config.js +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# Shader Sandbox
|
|
2
|
+
|
|
3
|
+
A lightweight, Shadertoy-compatible GLSL shader playground built for teaching and learning shader programming.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Shadertoy Compatibility** - Copy/paste shaders directly from Shadertoy
|
|
8
|
+
- **Full Shadertoy Uniforms** - `iTime`, `iResolution`, `iFrame`, `iMouse`, `iTimeDelta`, `iChannel0-3`
|
|
9
|
+
- **Multi-Buffer Rendering** - BufferA-D passes with correct ping-pong semantics
|
|
10
|
+
- **Texture Support** - Load external images with configurable filtering and wrapping
|
|
11
|
+
- **Keyboard Input** - Full keyboard state via Shadertoy-compatible texture
|
|
12
|
+
- **Playback Controls** - Play/pause, reset, and screenshot capture
|
|
13
|
+
- **Multiple Layout Modes** - Fullscreen, default, split-view, or tabbed code display
|
|
14
|
+
- **Zero Runtime Dependencies** - Pure WebGL2
|
|
15
|
+
- **Tiny Builds** - ~26KB JS (gzipped)
|
|
16
|
+
|
|
17
|
+
## Quick Start
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install
|
|
21
|
+
npm run new my-shader
|
|
22
|
+
npm run dev:demo my-shader
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Open `http://localhost:3000` to see your shader.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Use as NPM Package
|
|
30
|
+
|
|
31
|
+
Create your own shader collection:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Create a new project (does everything in one step)
|
|
35
|
+
npx shader-sandbox create my-shaders
|
|
36
|
+
|
|
37
|
+
# Run a shader
|
|
38
|
+
cd my-shaders
|
|
39
|
+
shader dev example-gradient
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
That's it! The `create` command sets up the directory, installs dependencies, and creates example shaders.
|
|
43
|
+
|
|
44
|
+
### CLI Commands
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
shader create <name> # Create a new shader project (recommended)
|
|
48
|
+
shader init # Initialize shaders in current directory
|
|
49
|
+
shader list # List all shaders
|
|
50
|
+
shader dev <name> # Run shader in development mode
|
|
51
|
+
shader build <name> # Build shader for production
|
|
52
|
+
shader new <name> # Create a new shader
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Project Structure
|
|
56
|
+
|
|
57
|
+
After `shader create`:
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
my-shaders/
|
|
61
|
+
├── shaders/
|
|
62
|
+
│ ├── example-gradient/
|
|
63
|
+
│ │ ├── image.glsl # Main shader
|
|
64
|
+
│ │ └── config.json # Optional config
|
|
65
|
+
│ └── example-buffer/
|
|
66
|
+
│ ├── image.glsl
|
|
67
|
+
│ ├── bufferA.glsl # Feedback buffer
|
|
68
|
+
│ └── config.json
|
|
69
|
+
├── main.ts
|
|
70
|
+
├── vite.config.js
|
|
71
|
+
└── package.json
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Adding a New Shader
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
shader new my-cool-shader
|
|
78
|
+
# Creates shaders/my-cool-shader/image.glsl
|
|
79
|
+
|
|
80
|
+
shader dev my-cool-shader
|
|
81
|
+
# Opens browser with live reload
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Common Setups
|
|
87
|
+
|
|
88
|
+
### 1. Simple Shader (just image.glsl)
|
|
89
|
+
|
|
90
|
+
The simplest setup - no config needed.
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm run new my-shader
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
**Files:**
|
|
97
|
+
```
|
|
98
|
+
demos/my-shader/
|
|
99
|
+
└── image.glsl
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**image.glsl:**
|
|
103
|
+
```glsl
|
|
104
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
105
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
106
|
+
vec3 col = 0.5 + 0.5 * cos(iTime + uv.xyx + vec3(0, 2, 4));
|
|
107
|
+
fragColor = vec4(col, 1.0);
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
### 2. One Buffer (feedback/trails)
|
|
114
|
+
|
|
115
|
+
For effects that accumulate over time (trails, paint, fluid).
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
npm run new my-shader 1
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Files:**
|
|
122
|
+
```
|
|
123
|
+
demos/my-shader/
|
|
124
|
+
├── bufferA.glsl
|
|
125
|
+
├── image.glsl
|
|
126
|
+
└── config.json
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**config.json:**
|
|
130
|
+
```json
|
|
131
|
+
{
|
|
132
|
+
"BufferA": {
|
|
133
|
+
"iChannel0": "BufferA"
|
|
134
|
+
},
|
|
135
|
+
"Image": {
|
|
136
|
+
"iChannel0": "BufferA"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**bufferA.glsl:**
|
|
142
|
+
```glsl
|
|
143
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
144
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
145
|
+
|
|
146
|
+
// Read previous frame with fade
|
|
147
|
+
vec4 prev = texture(iChannel0, uv) * 0.98;
|
|
148
|
+
|
|
149
|
+
// Draw at mouse
|
|
150
|
+
vec2 mouse = iMouse.xy / iResolution.xy;
|
|
151
|
+
float d = length(uv - mouse);
|
|
152
|
+
float spot = smoothstep(0.05, 0.0, d);
|
|
153
|
+
|
|
154
|
+
fragColor = prev + vec4(spot);
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
**image.glsl:**
|
|
159
|
+
```glsl
|
|
160
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
161
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
162
|
+
fragColor = texture(iChannel0, uv);
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
### 3. Multiple Buffers (interacting simulations)
|
|
169
|
+
|
|
170
|
+
For reaction-diffusion, fluid dynamics, etc. All buffers can read all other buffers.
|
|
171
|
+
|
|
172
|
+
```bash
|
|
173
|
+
npm run new my-shader 2
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Files:**
|
|
177
|
+
```
|
|
178
|
+
demos/my-shader/
|
|
179
|
+
├── bufferA.glsl
|
|
180
|
+
├── bufferB.glsl
|
|
181
|
+
├── image.glsl
|
|
182
|
+
└── config.json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**config.json:**
|
|
186
|
+
```json
|
|
187
|
+
{
|
|
188
|
+
"BufferA": {
|
|
189
|
+
"iChannel0": "BufferA",
|
|
190
|
+
"iChannel1": "BufferB"
|
|
191
|
+
},
|
|
192
|
+
"BufferB": {
|
|
193
|
+
"iChannel0": "BufferA",
|
|
194
|
+
"iChannel1": "BufferB"
|
|
195
|
+
},
|
|
196
|
+
"Image": {
|
|
197
|
+
"iChannel0": "BufferA",
|
|
198
|
+
"iChannel1": "BufferB"
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Channel mapping:** `iChannel0` = BufferA, `iChannel1` = BufferB, etc.
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
### 4. Texture + Image (image processing)
|
|
208
|
+
|
|
209
|
+
Load an image and process it.
|
|
210
|
+
|
|
211
|
+
**Files:**
|
|
212
|
+
```
|
|
213
|
+
demos/my-shader/
|
|
214
|
+
├── image.glsl
|
|
215
|
+
├── photo.jpg
|
|
216
|
+
└── config.json
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**config.json:**
|
|
220
|
+
```json
|
|
221
|
+
{
|
|
222
|
+
"Image": {
|
|
223
|
+
"iChannel0": "photo.jpg"
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**image.glsl:**
|
|
229
|
+
```glsl
|
|
230
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
231
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
232
|
+
vec4 img = texture(iChannel0, uv);
|
|
233
|
+
|
|
234
|
+
// Example: grayscale
|
|
235
|
+
float gray = dot(img.rgb, vec3(0.299, 0.587, 0.114));
|
|
236
|
+
|
|
237
|
+
fragColor = vec4(vec3(gray), 1.0);
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
**Texture options (all optional):**
|
|
242
|
+
```json
|
|
243
|
+
{ "texture": "photo.jpg", "filter": "linear", "wrap": "repeat", "type": "2d" }
|
|
244
|
+
```
|
|
245
|
+
- `filter`: `"linear"` (smooth, default) or `"nearest"` (pixelated)
|
|
246
|
+
- `wrap`: `"repeat"` (tile, default) or `"clamp"` (stretch edges)
|
|
247
|
+
- `type`: `"2d"` (standard, default) or `"cubemap"` (equirectangular environment map)
|
|
248
|
+
|
|
249
|
+
**Cubemap textures:** Use `"type": "cubemap"` for equirectangular environment maps (360° panoramas). The engine will automatically convert 3D direction lookups to 2D coordinates:
|
|
250
|
+
```json
|
|
251
|
+
{ "texture": "environment.jpg", "type": "cubemap" }
|
|
252
|
+
```
|
|
253
|
+
```glsl
|
|
254
|
+
// In your shader, sample with a 3D direction:
|
|
255
|
+
vec3 dir = normalize(rayDirection);
|
|
256
|
+
vec4 sky = texture(iChannel0, dir); // Automatically converted
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
### 5. Texture + Buffers (image + feedback)
|
|
262
|
+
|
|
263
|
+
Combine textures with buffer feedback for effects like painting on an image.
|
|
264
|
+
|
|
265
|
+
**Files:**
|
|
266
|
+
```
|
|
267
|
+
demos/my-shader/
|
|
268
|
+
├── bufferA.glsl
|
|
269
|
+
├── image.glsl
|
|
270
|
+
├── photo.jpg
|
|
271
|
+
└── config.json
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**config.json:**
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"BufferA": {
|
|
278
|
+
"iChannel0": "BufferA",
|
|
279
|
+
"iChannel1": "photo.jpg"
|
|
280
|
+
},
|
|
281
|
+
"Image": {
|
|
282
|
+
"iChannel0": "BufferA",
|
|
283
|
+
"iChannel1": "photo.jpg"
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
**bufferA.glsl:**
|
|
289
|
+
```glsl
|
|
290
|
+
void mainImage(out vec4 fragColor, in vec2 fragCoord) {
|
|
291
|
+
vec2 uv = fragCoord / iResolution.xy;
|
|
292
|
+
|
|
293
|
+
vec4 prev = texture(iChannel0, uv); // Previous frame
|
|
294
|
+
vec4 img = texture(iChannel1, uv); // Original image
|
|
295
|
+
|
|
296
|
+
// Paint with mouse
|
|
297
|
+
vec2 mouse = iMouse.xy / iResolution.xy;
|
|
298
|
+
float d = length(uv - mouse);
|
|
299
|
+
float brush = smoothstep(0.05, 0.0, d);
|
|
300
|
+
|
|
301
|
+
// Blend: painted areas persist, unpainted fade to original
|
|
302
|
+
fragColor = mix(mix(prev, img, 0.01), prev + brush, brush);
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## Buffer Execution & Frame Timing
|
|
309
|
+
|
|
310
|
+
**Execution order:** BufferA → BufferB → BufferC → BufferD → Image
|
|
311
|
+
|
|
312
|
+
All buffer reads default to the **previous frame**. This is safe for all cases:
|
|
313
|
+
- Self-reference (feedback effects)
|
|
314
|
+
- Reading buffers that haven't run yet this frame
|
|
315
|
+
- Reading buffers that have already run (you get their latest output)
|
|
316
|
+
|
|
317
|
+
Use `{ "buffer": "BufferA", "current": true }` only if you specifically need the in-progress current frame (rare).
|
|
318
|
+
|
|
319
|
+
---
|
|
320
|
+
|
|
321
|
+
## Layouts
|
|
322
|
+
|
|
323
|
+
Control how the shader is displayed with the `layout` option in `config.json`:
|
|
324
|
+
|
|
325
|
+
```json
|
|
326
|
+
{
|
|
327
|
+
"layout": "split",
|
|
328
|
+
"BufferA": { ... },
|
|
329
|
+
"Image": { ... }
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
| Layout | Description | Best for |
|
|
334
|
+
|--------|-------------|----------|
|
|
335
|
+
| `fullscreen` | Canvas fills entire viewport | Immersive art, games, installations |
|
|
336
|
+
| `default` | Canvas centered with max-width | General viewing (default without config) |
|
|
337
|
+
| `tabbed` | Tabs to switch between shader and code | Exploring/debugging |
|
|
338
|
+
| `split` | Side-by-side: shader left, code right | Teaching, presentations, tutorials |
|
|
339
|
+
|
|
340
|
+
**`fullscreen`** - No chrome, canvas fills the screen:
|
|
341
|
+
```json
|
|
342
|
+
{ "layout": "fullscreen" }
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**`default`** - Clean centered view with rounded corners:
|
|
346
|
+
```json
|
|
347
|
+
{ "layout": "default" }
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
**`tabbed`** - Click tabs to switch between live shader and source code:
|
|
351
|
+
```json
|
|
352
|
+
{ "layout": "tabbed" }
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
**`split`** - See shader and code simultaneously (code panel has tabs for multi-file projects):
|
|
356
|
+
```json
|
|
357
|
+
{ "layout": "split" }
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## Keyboard Shortcuts
|
|
363
|
+
|
|
364
|
+
| Key | Action |
|
|
365
|
+
|-----|--------|
|
|
366
|
+
| **S** | Save screenshot (PNG) |
|
|
367
|
+
| **Space** | Play/Pause (when controls enabled) |
|
|
368
|
+
| **R** | Reset to frame 0 (when controls enabled) |
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## NPM Scripts
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
npm run new <name> [buffers] # Create new shader project
|
|
376
|
+
npm run dev:demo <name> # Development server with hot reload
|
|
377
|
+
npm run build:demo <name> # Production build to dist/
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Documentation
|
|
383
|
+
|
|
384
|
+
- [Getting Started](docs/learn/getting-started.md) - Your first shader
|
|
385
|
+
- [Buffers and Channels](docs/learn/buffers-and-channels.md) - Multi-pass rendering
|
|
386
|
+
- [Configuration](docs/learn/configuration.md) - Full config reference
|
|
387
|
+
- [Architecture](docs/dev/architecture.md) - How the engine works
|
|
388
|
+
|
|
389
|
+
## License
|
|
390
|
+
|
|
391
|
+
MIT
|