Projekt v1.0

This commit is contained in:
2026-04-16 23:00:25 +02:00
commit 4c3a1fcf4b
3 changed files with 2330 additions and 0 deletions
+186
View File
@@ -0,0 +1,186 @@
# trakodag
Edytor obrazu sterowany kodem. Każda warstwa to osobny program w JavaScripcie,
który operuje na pikselach wynikowego obrazu poprzedniej warstwy. Pełny dostęp
do każdego piksela, pętle, zmienne, funkcje matematyczne, dodatkowe obrazy
jako tablice pikseli.
Cała apka to jeden plik `index.html` — bez zależności, bez build-stepu.
## Uruchomienie
Otwórz `index.html` w przeglądarce:
```sh
xdg-open index.html # Linux
open index.html # macOS
firefox index.html # albo dowolna inna przeglądarka
```
## Model warstw
Warstwy tworzą **pipeline**:
```
original → layer 1 → layer 2 → … → final
```
Każda warstwa otrzymuje wynik poprzedniej jako `prev` (dla pierwszej `prev = original`)
i pisze do `output`. Wynik trafia jako `prev` do następnej warstwy. Suwak
**strength** każdej warstwy to mieszanie z wejściem: `lerp(prev, output, strength)`
— przy `0.00` warstwa jest niewidoczna, przy `1.00` w pełni nadpisuje.
Wyłączenie ikony oka pomija warstwę całkowicie.
## Interfejs
- **Load image** — załaduj obraz źródłowy (PNG, JPG, …)
- **+ Add image** — dorzuć dodatkowy obraz dostępny w kodzie jako `images[i]`
- **Run / auto-run** — odpalenie pipeline'u (auto-run debouncuje wpisywanie ~250 ms);
skrót `Ctrl/Cmd + Enter`
- **scale** — rozdzielczość robocza podglądu (`1x`, `0.5x`, `0.25x`, `0.125x`).
Per-piksel JS jest wolny, więc edytuj na np. `0.25x`, eksport zawsze leci w `1x`.
- **theme** — `blue / orange` lub `purple / sea`; wybór pamiętany w `localStorage`
- **Export PNG** — render w pełnej rozdzielczości i pobranie pliku
- **Save project / Load project** — `.json` z samymi warstwami i kodem (bez obrazu)
- **? / ⓘ Vars** — ściąga API i pełny zestaw przykładów
Najechanie kursorem na canvas pokazuje **oryginał**.
## Edytor
- Kolorowanie składni: komentarze, stringi, liczby, słowa kluczowe, nazwy API,
funkcje, operatory.
- `Tab` wstawia 2 spacje.
- Nazwy warstw: dwuklik → edycja inline.
## Code API
Zmienne dostępne w kodzie warstwy:
| Nazwa | Co to |
|---|---|
| `width`, `height` | wymiary obrazu w aktualnej rozdzielczości roboczej |
| `original` | obiekt źródłowego obrazu: `{ pixels, width, height, getPixel(x,y) }` |
| `prev` | wynik poprzedniej warstwy (dla pierwszej = `original`), ten sam kształt |
| `images[i]` | dodatkowe obrazy dorzucone przyciskiem **+ Add image** |
| `output` | `Uint8ClampedArray` RGBA długości `width*height*4` — to do tego piszesz |
Helpery:
| Nazwa | Działanie |
|---|---|
| `getPixel(x, y)` | skrót do `prev.getPixel(x, y)``{r, g, b, a}` |
| `setPixel(x, y, r, g, b, a)` | zapis piksela; `a` domyślnie `255` |
| `setPixel(x, y, p)` | zapis z obiektu `{r, g, b, a}` |
| `forEach((x, y, p) => { … })` | iteracja po wszystkich pikselach `prev`; mutuj `p` lub zwróć nowy |
| `clamp(v, lo=0, hi=255)` | obcięcie do zakresu |
| `lerp(a, b, t)` | interpolacja liniowa |
| `log(...)` | wypisanie do panelu konsoli pod edytorem |
Random:
| Nazwa | Wynik |
|---|---|
| `random1()` | float `[0, 1)` |
| `random256()` | int `[0, 255]` |
| `randomBoolean()` | `true` / `false` (50/50) |
| `randomRange(lo, hi)` | float `[lo, hi)` |
| `randomInt(lo, hi)` | int `[lo, hi]` (inclusive) |
| `random()` | alias `Math.random()` |
Matematyka — wszystko z `Math` jest dostępne pod skróconymi nazwami:
`sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `abs`, `sqrt`, `pow`,
`exp`, `floor`, `ceil`, `round`, `min`, `max`, `hypot`, `sign`, plus stałe
`PI`, `TAU`, `E`. Pełen obiekt też jest pod `Math`.
## Przykłady
Brightness +5:
```js
forEach((x, y, p) => {
p.r = clamp(p.r + 5);
p.g = clamp(p.g + 5);
p.b = clamp(p.b + 5);
});
```
Maksymalny niebieski w kolumnie x = 50:
```js
forEach((x, y, p) => {
if (x === 50) { p.r = 0; p.g = 0; p.b = 255; }
});
```
Co 500-ny piksel: pomnóż zielony kanał razy 2:
```js
forEach((x, y, p) => {
const i = y * width + x;
if (i % 500 === 0) p.g = clamp(p.g * 2);
});
```
Wklejenie obrazu `images[0]` w punkcie `(50, 100)`:
```js
const img = images[0];
const offsetX = 50, offsetY = 100;
for (let y = 0; y < img.height; y++) {
for (let x = 0; x < img.width; x++) {
const dx = x + offsetX, dy = y + offsetY;
if (dx < 0 || dx >= width || dy < 0 || dy >= height) continue;
const q = img.getPixel(x, y);
setPixel(dx, dy, q.r, q.g, q.b, q.a);
}
}
```
Sinusoidalne przesunięcie poziome:
```js
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const dx = floor(sin(y * 0.05) * 20);
const p = prev.getPixel((x + dx + width) % width, y);
setPixel(x, y, p.r, p.g, p.b, p.a);
}
}
```
Więcej przykładów w panelu `?` — basics / coordinates / random / geometry / second image.
## Format pliku projektu
`Save project` zapisuje JSON ze stanem warstw (sam kod, bez obrazu):
```json
{
"format": "trakodag.web",
"version": 1,
"savedAt": "2026-04-16T20:00:00.000Z",
"layers": [
{
"name": "base",
"code": "forEach((x,y,p) => { p.r = clamp(p.r + 5); ... });",
"visible": true,
"opacity": 1.0
}
]
}
```
Obraz wczytujesz osobno przyciskiem **Load image** — projekt jest przenośny
między różnymi obrazami.
## Wydajność
Per-piksel JS na obrazach kilkudziesięciu MP będzie zauważalnie wolny. Schemat
pracy:
1. Wczytaj obraz, zostaw `scale` na `0.25x` (lub `0.125x` dla bardzo dużych)
2. Pisz i debuguj kod na małym podglądzie
3. Przed eksportem opcjonalnie podbij `scale` żeby zobaczyć finalny render
4. **Export PNG** zawsze leci w `1x` — nie musisz nic zmieniać
+1932
View File
File diff suppressed because it is too large Load Diff
+212
View File
@@ -0,0 +1,212 @@
{
"format": "trakodag.web.presets",
"version": 1,
"name": "trakodag — artistic presets",
"description": "Pamiątkowy zestaw efektów. Załaduj plik przez 'Load project' i wybierz preset z listy.",
"presets": [
{
"name": "Sepia Vintage",
"description": "Ciepły sepia look + delikatna winieta — klasyczna pocztówka.",
"layers": [
{
"name": "sepia",
"visible": true,
"opacity": 1.0,
"code": "// klasyczna macierz sepia\nforEach((x, y, p) => {\n const r = p.r * 0.393 + p.g * 0.769 + p.b * 0.189;\n const g = p.r * 0.349 + p.g * 0.686 + p.b * 0.168;\n const b = p.r * 0.272 + p.g * 0.534 + p.b * 0.131;\n p.r = clamp(r);\n p.g = clamp(g);\n p.b = clamp(b);\n});\n"
},
{
"name": "vignette",
"visible": true,
"opacity": 1.0,
"code": "// ściemnienie krawędzi (radialne)\nconst cx = width / 2, cy = height / 2;\nconst maxD = hypot(cx, cy);\nforEach((x, y, p) => {\n const d = hypot(x - cx, y - cy) / maxD;\n const k = 1 - pow(d, 1.6) * 0.75;\n p.r = clamp(p.r * k);\n p.g = clamp(p.g * k);\n p.b = clamp(p.b * k);\n});\n"
},
{
"name": "grain",
"visible": true,
"opacity": 0.5,
"code": "// drobne ziarno filmu\nforEach((x, y, p) => {\n const n = randomInt(-15, 15);\n p.r = clamp(p.r + n);\n p.g = clamp(p.g + n);\n p.b = clamp(p.b + n);\n});\n"
}
]
},
{
"name": "CRT Scanlines",
"description": "Stary monitor / TV — boost kolorów + poziome linie skanowania + lekkie cieplejsze tony.",
"layers": [
{
"name": "boost",
"visible": true,
"opacity": 1.0,
"code": "// nasycenie i jasność jak na nasyconym CRT\nforEach((x, y, p) => {\n p.r = clamp(p.r * 1.12 + 4);\n p.g = clamp(p.g * 1.06 + 2);\n p.b = clamp(p.b * 1.10 + 4);\n});\n"
},
{
"name": "scanlines",
"visible": true,
"opacity": 1.0,
"code": "// co druga linia ciemniejsza\nforEach((x, y, p) => {\n if (y % 2 === 0) {\n p.r = (p.r * 0.55) | 0;\n p.g = (p.g * 0.55) | 0;\n p.b = (p.b * 0.55) | 0;\n }\n});\n"
},
{
"name": "rgb mask",
"visible": true,
"opacity": 0.6,
"code": "// maska RGB co 3 piksele w poziomie (jak fosfory)\nforEach((x, y, p) => {\n const m = x % 3;\n if (m === 0) { p.g = (p.g * 0.7) | 0; p.b = (p.b * 0.7) | 0; }\n else if (m === 1) { p.r = (p.r * 0.7) | 0; p.b = (p.b * 0.7) | 0; }\n else { p.r = (p.r * 0.7) | 0; p.g = (p.g * 0.7) | 0; }\n});\n"
}
]
},
{
"name": "Cyberpunk Glitch",
"description": "Losowe poziome przesunięcia + chromatyczna aberracja + neonowe ziarno.",
"layers": [
{
"name": "rgb split",
"visible": true,
"opacity": 1.0,
"code": "// chromatic aberration — R w lewo, B w prawo\nconst shift = 6;\nforEach((x, y, p) => {\n const r = prev.getPixel(x - shift, y).r;\n const b = prev.getPixel(x + shift, y).b;\n p.r = r;\n p.b = b;\n});\n"
},
{
"name": "glitch slices",
"visible": true,
"opacity": 1.0,
"code": "// losowe paski przesunięte w poziomie\nconst sliceCount = 30;\nfor (let s = 0; s < sliceCount; s++) {\n const yStart = randomInt(0, height - 1);\n const yEnd = min(height, yStart + randomInt(2, 14));\n const shift = randomInt(-40, 40);\n for (let y = yStart; y < yEnd; y++) {\n for (let x = 0; x < width; x++) {\n const sx = (x - shift + width) % width;\n const q = prev.getPixel(sx, y);\n setPixel(x, y, q.r, q.g, q.b, q.a);\n }\n }\n}\n"
},
{
"name": "neon tint",
"visible": true,
"opacity": 0.4,
"code": "// fioletowo-cyjanowe przesunięcie kolorów\nforEach((x, y, p) => {\n p.r = clamp(p.r + 25);\n p.b = clamp(p.b + 35);\n});\n"
}
]
},
{
"name": "Pixel Mosaic",
"description": "Pikselizacja blokowa — z każdego bloku bierzemy kolor środka.",
"layers": [
{
"name": "mosaic",
"visible": true,
"opacity": 1.0,
"code": "// rozmiar bloku w pikselach\nconst block = 14;\nfor (let y = 0; y < height; y += block) {\n for (let x = 0; x < width; x += block) {\n const c = prev.getPixel(x + (block >> 1), y + (block >> 1));\n for (let by = 0; by < block; by++) {\n for (let bx = 0; bx < block; bx++) {\n setPixel(x + bx, y + by, c.r, c.g, c.b, c.a);\n }\n }\n }\n}\n"
}
]
},
{
"name": "Neon Edges",
"description": "Wyciągnięcie krawędzi przez różnicę sąsiadów + mocne kolory na czarnym tle.",
"layers": [
{
"name": "edges",
"visible": true,
"opacity": 1.0,
"code": "// |sąsiad - piksel| dla każdego kanału\nforEach((x, y, p) => {\n const a = prev.getPixel(x, y);\n const b = prev.getPixel(x + 1, y);\n const c = prev.getPixel(x, y + 1);\n p.r = clamp((abs(a.r - b.r) + abs(a.r - c.r)) * 3);\n p.g = clamp((abs(a.g - b.g) + abs(a.g - c.g)) * 3);\n p.b = clamp((abs(a.b - b.b) + abs(a.b - c.b)) * 3);\n});\n"
},
{
"name": "neon boost",
"visible": true,
"opacity": 1.0,
"code": "// gdziekolwiek jest jakaś krawędź — pompuj nasycenie\nforEach((x, y, p) => {\n const m = max(p.r, max(p.g, p.b));\n if (m > 30) {\n p.r = clamp(p.r * 1.6);\n p.g = clamp(p.g * 1.6);\n p.b = clamp(p.b * 1.6);\n }\n});\n"
}
]
},
{
"name": "Dream Wave",
"description": "Sinusoidalne falowanie + pastelowe rozmycie — łagodny, oniryczny look.",
"layers": [
{
"name": "wave",
"visible": true,
"opacity": 1.0,
"code": "// poziome i pionowe falowanie\nfor (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const dx = floor(sin(y * 0.04) * 10);\n const dy = floor(cos(x * 0.04) * 6);\n const p = prev.getPixel((x + dx + width) % width, (y + dy + height) % height);\n setPixel(x, y, p.r, p.g, p.b, p.a);\n }\n}\n"
},
{
"name": "soft glow",
"visible": true,
"opacity": 0.55,
"code": "// fake-blur: średnia z 4 losowych sąsiadów (tanie i działa)\nforEach((x, y, p) => {\n let r = 0, g = 0, b = 0;\n for (let i = 0; i < 4; i++) {\n const q = prev.getPixel(x + randomInt(-3, 3), y + randomInt(-3, 3));\n r += q.r; g += q.g; b += q.b;\n }\n p.r = (r >> 2);\n p.g = (g >> 2);\n p.b = (b >> 2);\n});\n"
},
{
"name": "pastel lift",
"visible": true,
"opacity": 0.6,
"code": "// dodaj odrobinę bieli, żeby wybielić cienie\nforEach((x, y, p) => {\n p.r = clamp(p.r + 30);\n p.g = clamp(p.g + 35);\n p.b = clamp(p.b + 40);\n});\n"
}
]
},
{
"name": "Posterize Pop",
"description": "Redukcja palety (5 poziomów na kanał) + boost nasycenia — pop-art.",
"layers": [
{
"name": "posterize",
"visible": true,
"opacity": 1.0,
"code": "// kwantyzacja kolorów\nconst levels = 5;\nconst step = 255 / (levels - 1);\nforEach((x, y, p) => {\n p.r = round(p.r / step) * step;\n p.g = round(p.g / step) * step;\n p.b = round(p.b / step) * step;\n});\n"
},
{
"name": "saturate",
"visible": true,
"opacity": 1.0,
"code": "// proste wzmocnienie nasycenia względem szarości\nforEach((x, y, p) => {\n const gray = (p.r + p.g + p.b) / 3;\n p.r = clamp(gray + (p.r - gray) * 1.6);\n p.g = clamp(gray + (p.g - gray) * 1.6);\n p.b = clamp(gray + (p.b - gray) * 1.6);\n});\n"
}
]
},
{
"name": "Film Noir",
"description": "Twarda czerń i biel z mocnym kontrastem + ziarno.",
"layers": [
{
"name": "grayscale",
"visible": true,
"opacity": 1.0,
"code": "// luminancja BT.601\nforEach((x, y, p) => {\n const g = (p.r * 0.299 + p.g * 0.587 + p.b * 0.114) | 0;\n p.r = p.g = p.b = g;\n});\n"
},
{
"name": "contrast",
"visible": true,
"opacity": 1.0,
"code": "// rozciągnięcie kontrastu wokół 128\nforEach((x, y, p) => {\n const v = clamp((p.r - 128) * 1.7 + 128);\n p.r = p.g = p.b = v;\n});\n"
},
{
"name": "grain",
"visible": true,
"opacity": 0.7,
"code": "// mocniejsze ziarno niż w sepii\nforEach((x, y, p) => {\n const n = randomInt(-25, 25);\n p.r = clamp(p.r + n);\n p.g = clamp(p.g + n);\n p.b = clamp(p.b + n);\n});\n"
}
]
},
{
"name": "Static Storm",
"description": "Burza statyki — losowe punkty białego/czarnego szumu rozsypane po obrazie.",
"layers": [
{
"name": "noise sprinkle",
"visible": true,
"opacity": 1.0,
"code": "// 8% pikseli — biały szum, 4% czarny\nforEach((x, y, p) => {\n const r = random1();\n if (r < 0.08) {\n const v = random256();\n p.r = p.g = p.b = v;\n } else if (r < 0.12) {\n p.r = p.g = p.b = 0;\n }\n});\n"
},
{
"name": "horizontal lines",
"visible": true,
"opacity": 0.6,
"code": "// losowe całe linie do bieli\nfor (let i = 0; i < 6; i++) {\n const y = randomInt(0, height - 1);\n for (let x = 0; x < width; x++) {\n setPixel(x, y, 220, 220, 220, 255);\n }\n}\n"
}
]
},
{
"name": "Mirror Kaleidoscope",
"description": "Pionowe lustro + przesunięcie sinusoidalne — efekt kalejdoskopu.",
"layers": [
{
"name": "mirror",
"visible": true,
"opacity": 1.0,
"code": "// odbijamy lewą połówkę na prawą\nfor (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const sx = x < width / 2 ? x : (width - 1 - x);\n const q = prev.getPixel(sx, y);\n setPixel(x, y, q.r, q.g, q.b, q.a);\n }\n}\n"
},
{
"name": "sine flow",
"visible": true,
"opacity": 1.0,
"code": "// powolny przepływ\nfor (let y = 0; y < height; y++) {\n for (let x = 0; x < width; x++) {\n const dx = floor(sin(y * 0.08) * 18);\n const q = prev.getPixel((x + dx + width) % width, y);\n setPixel(x, y, q.r, q.g, q.b, q.a);\n }\n}\n"
}
]
}
]
}