From 4c3a1fcf4b1f56788b6661f17dc04411f39473c9 Mon Sep 17 00:00:00 2001 From: Tonik Date: Thu, 16 Apr 2026 23:00:25 +0200 Subject: [PATCH] Projekt v1.0 --- README.md | 186 +++++ index.html | 1932 ++++++++++++++++++++++++++++++++++++++++++++++++++ presets.json | 212 ++++++ 3 files changed, 2330 insertions(+) create mode 100644 README.md create mode 100644 index.html create mode 100644 presets.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..23324d5 --- /dev/null +++ b/README.md @@ -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ć diff --git a/index.html b/index.html new file mode 100644 index 0000000..76d87c8 --- /dev/null +++ b/index.html @@ -0,0 +1,1932 @@ + + + + +Trakodag Web — Code Image Editor + + + + +
+
trakodag.web
+ + + + + + + +
+ + + + +
+ + + + +
+ + + + + +
+ + + +
+ + + + + + + +
+
no image loaded
+
+ +
+
+
+ Load an image to start.
+ Write code on the right — each layer is its own program.
+ Hover the canvas to see the original. +
+ +
+ +
+
+ Layers +
+ +
+
+ +
+
+ Code +
+ + +
+
+ + +
+
+
+ +
+ Images +
+ access in code as images[i] +
+
+
no extra images — use "+ Add image"
+
+ +
+
+
+ + + +
+ × +

Code API

+

Each layer runs its own JS code as a step in a pipeline:
+ original → layer1 → layer2 → … → final
+ A layer's prev is the previous layer's output (or the original for the bottom layer). + Write to output — the result is fed as prev to the next layer. + The "strength" number on each layer is a soft mix: lerp(prev, output, strength).

+ +

Available variables

+
width, height          // image size (after scale)
+original.pixels        // Uint8ClampedArray RGBA of source
+original.width, .height
+original.getPixel(x,y) // -> {r,g,b,a}
+prev                   // same shape — previous layer's output
+                       // (= original for the bottom layer)
+images[i]              // additional images you added
+                       // .width .height .pixels .getPixel(x,y)
+output                 // Uint8ClampedArray you write to
+                       // length = width*height*4 (RGBA)
+ +

Helpers

+
getPixel(x, y)         // reads from prev
+setPixel(x, y, r, g, b, a=255)
+forEach((x, y, p) => { p.r += 5; return p; })
+                       // iterates every pixel of prev, writes back
+clamp(v, lo=0, hi=255)
+lerp(a, b, t)
+PI, TAU, E
+sin, cos, tan, asin, acos, atan, atan2,
+abs, sqrt, pow, exp, log, floor, ceil, round, min, max,
+random, hypot, sign
+ +

Examples — basics

+
// brightness +5
+forEach((x,y,p) => { p.r=clamp(p.r+5); p.g=clamp(p.g+5); p.b=clamp(p.b+5); });
+
+// invert
+forEach((x,y,p) => { p.r=255-p.r; p.g=255-p.g; p.b=255-p.b; });
+
+// grayscale
+forEach((x,y,p) => {
+  const g = (p.r * 0.299 + p.g * 0.587 + p.b * 0.114) | 0;
+  p.r = p.g = p.b = g;
+});
+ +

Examples — pixel coordinates / index

+
// max blue at column x === 50
+forEach((x, y, p) => {
+  if (x === 50) { p.r = 0; p.g = 0; p.b = 255; }
+});
+
+// horizontal red line at y === 100, 1px tall
+forEach((x, y, p) => {
+  if (y === 100) { p.r = 255; p.g = 0; p.b = 0; }
+});
+
+// every 500th pixel (by ordinal index): green channel * 2
+forEach((x, y, p) => {
+  const i = y * width + x;          // ordinal pixel index
+  if (i % 500 === 0) p.g = clamp(p.g * 2);
+});
+
+// vertical stripes every 20 px
+forEach((x, y, p) => {
+  if (x % 20 === 0) { p.r = 255; p.g = 255; p.b = 0; }
+});
+
+// checkerboard tint
+forEach((x, y, p) => {
+  if (((x >> 4) + (y >> 4)) & 1) p.b = clamp(p.b + 80);
+});
+ +

Examples — random

+
// pure noise
+forEach((x, y, p) => {
+  p.r = random256(); p.g = random256(); p.b = random256();
+});
+
+// salt & pepper: 1% pixels become white, 1% black
+forEach((x, y, p) => {
+  const r = random1();
+  if (r < 0.01)      { p.r = p.g = p.b = 255; }
+  else if (r < 0.02) { p.r = p.g = p.b = 0;   }
+});
+
+// random R/B channel swap on half the pixels
+forEach((x, y, p) => {
+  if (randomBoolean()) { const t = p.r; p.r = p.b; p.b = t; }
+});
+
+// jittered brightness ±30
+forEach((x, y, p) => {
+  const j = randomInt(-30, 30);
+  p.r = clamp(p.r + j); p.g = clamp(p.g + j); p.b = clamp(p.b + j);
+});
+
+// 30% chance: take pixel from random spot in original
+forEach((x, y, p) => {
+  if (random1() < 0.3) {
+    const q = original.getPixel(randomInt(0, width-1), randomInt(0, height-1));
+    p.r = q.r; p.g = q.g; p.b = q.b;
+  }
+});
+ +

Examples — geometry & math

+
// sine wave horizontal displacement
+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);
+  }
+}
+
+// radial gradient over original (darken edges)
+const cx = width / 2, cy = height / 2;
+const maxD = hypot(cx, cy);
+forEach((x, y, p) => {
+  const d = hypot(x - cx, y - cy) / maxD;   // 0..1
+  const k = 1 - d;
+  p.r = (p.r * k) | 0;
+  p.g = (p.g * k) | 0;
+  p.b = (p.b * k) | 0;
+});
+ +

Examples — second image

+
// average with images[0] (added via "+ Add image")
+const img = images[0];
+forEach((x, y, p) => {
+  const q = img.getPixel(x % img.width, y % img.height);
+  p.r = (p.r + q.r) >> 1;
+  p.g = (p.g + q.g) >> 1;
+  p.b = (p.b + q.b) >> 1;
+});
+
+// difference (abs) — highlights what's different
+const img = images[0];
+forEach((x, y, p) => {
+  const q = img.getPixel(x % img.width, y % img.height);
+  p.r = abs(p.r - q.r); p.g = abs(p.g - q.g); p.b = abs(p.b - q.b);
+});
+
+// paste images[0] at coordinates (offsetX, offsetY)
+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);
+  }
+}
+
+// paste with alpha blending (respects q.a as transparency)
+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);
+    const p = prev.getPixel(dx, dy);
+    const a = q.a / 255;
+    setPixel(dx, dy,
+      p.r * (1 - a) + q.r * a,
+      p.g * (1 - a) + q.g * a,
+      p.b * (1 - a) + q.b * a,
+      255);
+  }
+}
+
+// paste centered on the canvas
+const img = images[0];
+const offsetX = ((width  - img.width)  / 2) | 0;
+const offsetY = ((height - img.height) / 2) | 0;
+for (let y = 0; y < img.height; y++) {
+  for (let x = 0; x < img.width; x++) {
+    const q = img.getPixel(x, y);
+    setPixel(x + offsetX, y + offsetY, q.r, q.g, q.b, q.a);
+  }
+}
+
+// paste with math: add images[0] to original at (offsetX, offsetY)
+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);
+    const p = prev.getPixel(dx, dy);
+    setPixel(dx, dy, clamp(p.r + q.r), clamp(p.g + q.g), clamp(p.b + q.b));
+  }
+}
+ +

Performance tip

+

JS per-pixel loops are slow on huge images. Use the scale selector + (e.g. 0.25x) for live editing, then bump to 1x when exporting.

+
+ + + + diff --git a/presets.json b/presets.json new file mode 100644 index 0000000..67cf4bc --- /dev/null +++ b/presets.json @@ -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" + } + ] + } + ] +}