Compare commits
3 commits
4ddaddda3d
...
d971512a31
| Author | SHA1 | Date | |
|---|---|---|---|
| d971512a31 | |||
| 856d380fb7 | |||
| fb0321bfd5 |
3 changed files with 485 additions and 18 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -1,2 +1,4 @@
|
|||
sessions/*
|
||||
ignore
|
||||
kanban/
|
||||
.pi-tasks
|
||||
|
|
|
|||
22
README.md
22
README.md
|
|
@ -2,9 +2,9 @@
|
|||
|
||||
Revival of the old ZapMachine. A collage machine that merges random images layer by layer remaining about 50% of the original images
|
||||
|
||||
########################################################
|
||||
#### ApFab - Zap Machine V0.9.0 ###
|
||||
########################################################
|
||||
|
||||
## ApFab - Zap Machine V0.9.0
|
||||
|
||||
🄯 2009 - 2026 Fabian de Boer, Vincent Bruijne
|
||||
www.apfab.com
|
||||
|
||||
|
|
@ -28,21 +28,7 @@ You need to acquire the following API key and put it into `conf/conf.php`:
|
|||
|
||||
See `conf/conf.php` for the exact constant names to use.
|
||||
|
||||
Adjust permission (chmod 777) for Sessions folder, run index.html, fill in the form and voila.
|
||||
|
||||
|
||||
|
||||
# how it works
|
||||
|
||||
- index.html contains user interface
|
||||
- user input put into Session variables in init.php
|
||||
- display.inc.php dynamically displays two img tags that are loaded in turn with image data from zap.php using display.js
|
||||
- zap.php downloads images one by one using imgSearch Class
|
||||
- zap.php processes collages one by one using imgProcess Class
|
||||
- All downloaded images, collage images and logfiles(feature is far from ready yet) are put in a folder named 'Session/zap'.date()
|
||||
- the machine is ready when an error is loaded into img document class. (a weak point in our construction)
|
||||
|
||||
|
||||
Adjust permission (chmod 777) for 'sessions' folder, run index.php in the www/ directory, fill in the form and voila.
|
||||
|
||||
# yet to come
|
||||
- Logfile also generated in imgProcess Class (0.9.2)
|
||||
|
|
|
|||
479
docs/framework_guide.md
Normal file
479
docs/framework_guide.md
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
# ZAP Framework Guide
|
||||
|
||||
This document explains the internal architecture of ZapMachine and provides step‑by‑step instructions for extending it with new handlers, templates, and interactive elements.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Front-Controller Routing (`?mode=X` → `ZAPController` → `ZAP{Mode}`)
|
||||
|
||||
All HTTP requests enter through `www/index.php`. The front controller reads the `mode` query parameter and dispatches to a class whose name is formed as `ZAP` + the ucfirst'd mode value.
|
||||
|
||||
```php
|
||||
// www/index.php (simplified)
|
||||
$mode = isset($_GET['mode']) ? $_GET['mode'] : 'home';
|
||||
require_once '..' . DIRECTORY_SEPARATOR . 'conf' . DIRECTORY_SEPARATOR . 'init.php';
|
||||
$config = Config::getInstance();
|
||||
$config->register('engines', $search_engines);
|
||||
$frontcontroller = new ZAPController($mode);
|
||||
$output = $frontcontroller->fetch();
|
||||
print($output);
|
||||
```
|
||||
|
||||
`ZAPController` (in `lib/ZAPController.class.php`) constructs the class name from a prefix (`ZAP`) and the mode, verifies it exists via the autoloader, instantiates it, and calls `getContent()`:
|
||||
|
||||
```php
|
||||
class ZAPController {
|
||||
private $prefix = 'ZAP';
|
||||
|
||||
public function __construct($mode = 'home') {
|
||||
$mode = trim(strip_tags($mode));
|
||||
$this->mode = ucfirst($mode);
|
||||
$clazz = $this->prefix . $this->mode;
|
||||
if (!class_exists($clazz)) {
|
||||
die('Wrong parameter');
|
||||
}
|
||||
$this->action = new $clazz();
|
||||
}
|
||||
|
||||
public function fetch() {
|
||||
return $this->action->getContent();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Registered modes and their handlers:**
|
||||
|
||||
| `?mode=` | Class | Template | Purpose |
|
||||
|----------|-------|----------|---------|
|
||||
| `home` (default) | `ZAPHome` | `tpl/index.tpl` | Word-input form |
|
||||
| `display` | `ZAPDisplay` | `tpl/display.tpl` | Collage-generation progress screen |
|
||||
| `zap` | `ZAPZap` | *(raw image output)* | Downloads images & processes collages, returns image blobs |
|
||||
|
||||
---
|
||||
|
||||
### Autoloader
|
||||
|
||||
`conf/init.php` registers an `spl_autoload_register` callback that searches every directory on the include path for both `{ClassName}.class.php` and `{ClassName}.interface.php`:
|
||||
|
||||
```php
|
||||
spl_autoload_register(function ($class_name) {
|
||||
$include_path_tokens = explode(PATH_SEPARATOR, get_include_path());
|
||||
foreach ($include_path_tokens as $prefix) {
|
||||
$path[0] = $prefix . DIRECTORY_SEPARATOR . $class_name . '.interface.php';
|
||||
$path[1] = $prefix . DIRECTORY_SEPARATOR . $class_name . '.class.php';
|
||||
foreach ($path as $thisPath) {
|
||||
if (file_exists($thisPath)) {
|
||||
require_once $thisPath;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The include path is built from these directories (relative to the project root):
|
||||
|
||||
| Constant | Directory |
|
||||
|----------|-----------|
|
||||
| `ZAP_RESOURCE_DIR` | `resource/` — search-engine implementations |
|
||||
| `ZAP_UTILS_DIR` | `utils/` — utility classes (Session, ZAPCurl, Log) |
|
||||
| `ZAP_SESSIONS_DIR` | `sessions/` |
|
||||
| `ZAP_LIB_DIR` | `lib/` — core handlers and image processing |
|
||||
| `ZAP_TPL_DIR` | `tpl/` — templates |
|
||||
| `ZAP_CONF_DIR` | `conf/` — configuration |
|
||||
|
||||
This means any `.class.php` or `.interface.php` file placed in one of those directories is automatically found — no manual `require_once` calls needed for library classes.
|
||||
|
||||
---
|
||||
|
||||
### Config Singleton
|
||||
|
||||
Configuration values that must be accessible from any class are managed through `Config` (`conf/Config.class.php`), a classic singleton:
|
||||
|
||||
```php
|
||||
$config = Config::getInstance();
|
||||
$config->register('engines', $search_engines); // store
|
||||
$engines = $config->retrieve('engines'); // retrieve — returns false if not found
|
||||
```
|
||||
|
||||
The singleton is wired in `www/index.php` immediately after `init.php` is loaded.
|
||||
|
||||
---
|
||||
|
||||
### Session Management
|
||||
|
||||
The `Session` class (`utils/Session.class.php`) wraps PHP's native sessions:
|
||||
|
||||
```php
|
||||
Session::start('ZAP'); // Begin session (sessions named 'ZAP')
|
||||
Session::reset(); // Clear $_SESSION array
|
||||
Session::deleteCookie(); // Remove client-side session cookie
|
||||
Session::destroy(); // Reset + delete cookie + session_destroy()
|
||||
```
|
||||
|
||||
Session data is stored verbatim in `$_SESSION` and uses these keys:
|
||||
|
||||
| Key | Type | Set by | Purpose |
|
||||
|-----|------|--------|---------|
|
||||
| `words` | `string[]` | `ZAPDisplay` | Keyword list for image search |
|
||||
| `folder` | `string` | `ZAPDisplay` | Absolute path to session folder |
|
||||
| `collages` | `int` | `ZAPDisplay` | Number of collages to generate |
|
||||
| `width` | `int` | `ZAPDisplay` | Collage width in pixels |
|
||||
| `height` | `int` | `ZAPDisplay` | Collage height in pixels |
|
||||
| `images` | `string[]` | `ZAPZap` | Queue of source image paths for current collage |
|
||||
| `imgOriginal` | `string[]` | `ZAPZap` | Immutable copy of the original image list |
|
||||
| `collageNr` | `int` | `ZAPZap` | Current collage sequence number |
|
||||
|
||||
**CLI note:** Session is only started automatically in web mode. See [CLI Bootstrap](#cli-bootstrap) below.
|
||||
|
||||
---
|
||||
|
||||
### Plugin-Based Search Engines
|
||||
|
||||
Search engines implement the `Search` interface (`resource/Search.interface.php`):
|
||||
|
||||
```php
|
||||
interface Search {
|
||||
public function setQuery($word);
|
||||
public function setParam($key, $value);
|
||||
public function getData();
|
||||
}
|
||||
```
|
||||
|
||||
Each engine class lives in `resource/` and returns data in a universal array format:
|
||||
|
||||
```php
|
||||
$data['result'] = true; // Boolean: success / failure
|
||||
$data['engine'] = __CLASS__; // Engine name, e.g. 'Brave'
|
||||
$data['results'][0]['clickurl'] = '...'; // Image URL to download
|
||||
$data['results'][0]['referer'] = '...'; // Source page URL
|
||||
$data['results'][0]['thumb'] = '...'; // Thumbnail URL
|
||||
```
|
||||
|
||||
Available engines are registered as an array of names in `conf/conf.php` and wired through the Config singleton at runtime. `ImgSearch` randomly selects an engine from the registered list each time it runs:
|
||||
|
||||
```php
|
||||
$config = Config::getInstance();
|
||||
$engines = $config->retrieve('engines');
|
||||
$this->engine = $engines[array_rand($engines)];
|
||||
```
|
||||
|
||||
**Current engine:** `Brave` (`resource/Brave.class.php`) — uses the Brave Search API.
|
||||
|
||||
To add a new engine:
|
||||
1. Create `resource/YourEngine.class.php` implementing `Search`.
|
||||
2. Add `'yourengine'` to the `$search_engines` array in `conf/conf.php`.
|
||||
|
||||
---
|
||||
|
||||
### Image Processing Pipeline
|
||||
|
||||
The image pipeline runs entirely server-side through three classes in `lib/`:
|
||||
|
||||
1. **`ImgSearch`** (`lib/ImgSearch.class.php`) — searches for an image URL via a registered engine, downloads the file, validates its MIME type (`image/jpeg`, `image/gif`, `image/png`), and saves it to the session folder.
|
||||
|
||||
2. **`ImgProcess`** (`lib/ImgProcess.class.php`) — constructs collages using ImageMagick. Each collage starts with `createBasicCanvas()` (zoom-crops the first source image) then layers additional images via `addToCanvas()` (random transparency + composite).
|
||||
|
||||
3. **`ImgIOTools`** (`lib/ImgIOTools.class.php`) — handles output: `onScreen()` streams an Imagick object as an HTTP image response; `urlOnScreen()` loads a file path first; `saveImg()` writes to disk.
|
||||
|
||||
4. **`ImgTools`** (`lib/ImgTools.class.php`) — utility functions: `zoomCrop()`, `transparantRandom()`, `randomRotate()`, `resize()`, `recolor()`, `convertGifsToPng()`.
|
||||
|
||||
---
|
||||
|
||||
### CLI Bootstrap
|
||||
|
||||
CLI scripts (in `cli_scripts/`) bootstrap the application differently from web requests. Because `init.php` skips `Session::start()` when `php_sapi_name() === 'cli'`, CLI scripts must handle sessions manually if needed.
|
||||
|
||||
**Example — `cli_scripts/test.php`:**
|
||||
|
||||
```php
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
// Make sure relative paths in init.php resolve against this script's directory
|
||||
chdir(__DIR__);
|
||||
|
||||
// Bootstrap the application (autoloader, constants, Config, but NOT session)
|
||||
require_once __DIR__ . '/../conf/init.php';
|
||||
|
||||
// If you need sessions in CLI, call Session::start() manually:
|
||||
// Session::start('ZAP');
|
||||
|
||||
// Now all classes, the autoloader, and Config singleton are available
|
||||
$config = Config::getInstance();
|
||||
$config->register('engines', $search_engines);
|
||||
```
|
||||
|
||||
Key differences from the web entry point:
|
||||
- `chdir(__DIR__)` is **required** before `require_once 'init.php'` because `init.php` uses `realpath('.')` to define `ZAP_APP_BASE_DIR`.
|
||||
- `Session::start()` is **not called** — the `init.php` condition `php_sapi_name() !== 'cli'` guards against it.
|
||||
- You must explicitly register engines with `Config::getInstance()->register('engines', …)` because `www/index.php` normally does this.
|
||||
|
||||
---
|
||||
|
||||
### Logging
|
||||
|
||||
The `Log` class (`utils/Log.class.php`) writes timestamped messages to a session folder. It is used by `ImgSearch` (controlled by the `ZAP_LOG` constant in `conf/conf.php`). Each log call appends to both `log.txt` (plain text) and `log.xml` (XML-friendly) in the session's output directory.
|
||||
|
||||
```php
|
||||
if (ZAP_LOG == true) {
|
||||
$log = new Log($this->folder);
|
||||
$log->addToLog('ok', 'Search: ', "downloaded to $imageName");
|
||||
$log->addToLog('error', 'Search: ', 'bad search data received');
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Template Rendering Pipeline
|
||||
|
||||
The template system is a minimal PHP-native pattern built around `ob_start()` / `include` / `extract()` / `ob_get_clean()`. There is no template engine — templates are plain PHP files that receive variables via the symbol table.
|
||||
|
||||
### How Templates Work
|
||||
|
||||
Every `ZAP{Mode}` handler follows the same rendering pattern:
|
||||
|
||||
```php
|
||||
class ZAPHome {
|
||||
private $template = 'index'; // corresponds to tpl/index.tpl
|
||||
private $tpl = '';
|
||||
|
||||
public function __construct() {
|
||||
$this->ver = ZAP_VERSION;
|
||||
extract(get_object_vars($this)); // ← makes all private properties available as $variables
|
||||
|
||||
ob_start(); // ← start output buffer
|
||||
include getZAPTemplate($this->template); // ← include the .tpl file
|
||||
$this->tpl .= ob_get_clean(); // ← capture buffer content, clean buffer
|
||||
}
|
||||
|
||||
public function getContent() {
|
||||
return $this->tpl;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step by step:**
|
||||
|
||||
1. Properties are set in the constructor (often from `$_POST`, `$_SESSION`, or constants like `ZAP_VERSION`).
|
||||
2. `extract(get_object_vars($this))` promotes every private property to a local variable in the current symbol table. A property `$this->template = 'index'` becomes `$template = 'index'`.
|
||||
3. `ob_start()` begins capturing all output.
|
||||
4. `include getZAPTemplate($this->template)` includes the file `tpl/{name}.tpl`. The included file inherits the current symbol table, so it can use `$ver`, `$folder`, `$words`, etc. directly.
|
||||
5. `ob_get_clean()` retrieves the captured HTML and closes the buffer.
|
||||
6. `getContent()` returns the accumulated output string, which `ZAPController::fetch()` returns to `index.php` for `print()`.
|
||||
|
||||
### The `getZAPTemplate()` Helper
|
||||
|
||||
```php
|
||||
function getZAPTemplate($tplname) {
|
||||
$tplfile = $tplname . '.tpl';
|
||||
$tplfilepath = ZAP_APP_BASE_DIR . DIRECTORY_SEPARATOR
|
||||
. ZAP_TPL_DIR . DIRECTORY_SEPARATOR . $tplfile;
|
||||
if (file_exists($tplfilepath)) {
|
||||
return $tplfilepath;
|
||||
} else {
|
||||
return ERROR_PREFIX . 'No template';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
All templates live in the `tpl/` directory and have the `.tpl` extension. The helper resolves the absolute path so the `include` call works from any working directory.
|
||||
|
||||
### Adding a New Template
|
||||
|
||||
To add a new page/view:
|
||||
|
||||
1. **Create the `ZAP{Mode}` handler** in `lib/`. The class must be named `ZAP{Mode}` (case-sensitive, PascalCase after the prefix) and must implement a `getContent()` method that returns a string:
|
||||
|
||||
```php
|
||||
<?php
|
||||
// lib/ZAPExample.class.php
|
||||
class ZAPExample {
|
||||
private $template = 'example';
|
||||
private $tpl = '';
|
||||
|
||||
public function __construct() {
|
||||
$this->pageTitle = 'My Example Page';
|
||||
extract(get_object_vars($this));
|
||||
|
||||
ob_start();
|
||||
include getZAPTemplate($this->template);
|
||||
$this->tpl .= ob_get_clean();
|
||||
}
|
||||
|
||||
public function getContent() {
|
||||
return $this->tpl;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
2. **Create the template** at `tpl/example.tpl`:
|
||||
|
||||
```php
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><title>Example — <?php echo $pageTitle; ?></title></head>
|
||||
<body>
|
||||
<h1><?php echo $pageTitle; ?></h1>
|
||||
<p>ZAP version <?php echo $ver; ?></p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
3. **Use the route:** Navigate to `index.php?mode=example`. The autoloader finds `ZAPExample` in `lib/`, the front controller instantiates it, and `getZAPTemplate()` loads `tpl/example.tpl`.
|
||||
|
||||
No registration step is required — the front controller's naming convention (`ZAP` + ucfirst(mode)) and the autoloader handle discovery automatically.
|
||||
|
||||
---
|
||||
|
||||
## Adding Interactive Elements
|
||||
|
||||
This section documents the two interactive-element patterns used in the application. Each pattern pairs a server-side template with a client-side JavaScript file.
|
||||
|
||||
### Pattern 1: Form with Client-Side Validation (`index.tpl` + `js/main.js`)
|
||||
|
||||
Used by `ZAPHome` to collect word inputs, validate them in the browser, and POST to the `display` mode.
|
||||
|
||||
**Server side (`ZAPHome` in `lib/ZAPHome.class.php`):**
|
||||
|
||||
The handler passes two template variables into `tpl/index.tpl`:
|
||||
- `$ver` — the ZAP version string.
|
||||
- `$status` — an optional status message from the query string (`?stat=...`).
|
||||
|
||||
The template renders a `<form id="form1">` that POSTs to `index.php?mode=display`. Each input is named `word01`, `word02`, … `word{MAX_WORDS}` (zeropadded to two digits). A `<select name="collages">` controls the number of collages, and width/height text inputs control dimensions.
|
||||
|
||||
**Client side (`js/main.js`):**
|
||||
|
||||
The `form1Submit()` function performs validation before the form is submitted:
|
||||
|
||||
```javascript
|
||||
function form1Submit() {
|
||||
// Minimum of two words required
|
||||
var word1 = document.getElementById('word01').value;
|
||||
var word2 = document.getElementById('word02').value;
|
||||
var width = document.getElementById('width').value;
|
||||
var height = document.getElementById('height').value;
|
||||
|
||||
if (word1.length <= 0) {
|
||||
document.getElementById('status').innerHTML = 'Minimum of two words required';
|
||||
return;
|
||||
}
|
||||
if (word2.length <= 0) {
|
||||
document.getElementById('status').innerHTML = 'Minimum of two words required';
|
||||
return;
|
||||
}
|
||||
if (width.length <= 0) {
|
||||
document.getElementById('status').innerHTML = 'Value <b>width</b> required';
|
||||
return;
|
||||
}
|
||||
if (height.length <= 0) {
|
||||
document.getElementById('status').innerHTML = 'Value <b>height</b> required';
|
||||
return;
|
||||
}
|
||||
if (!IsNumeric(width)) {
|
||||
document.getElementById('status').innerHTML = 'Value <b>width</b> needs numeric input';
|
||||
return;
|
||||
}
|
||||
if (!IsNumeric(height)) {
|
||||
document.getElementById('status').innerHTML = 'Value <b>height</b> needs numeric input';
|
||||
return;
|
||||
}
|
||||
|
||||
// Hide form, show loading indicator, then submit
|
||||
document.getElementById('status').innerHTML = ' ';
|
||||
document.getElementById('loading').style.visibility = 'visible';
|
||||
document.getElementById('form1').style.visibility = 'hidden';
|
||||
document.getElementById('form1').submit();
|
||||
}
|
||||
```
|
||||
|
||||
**To reproduce this pattern in a new handler:**
|
||||
|
||||
1. Create a form in your `.tpl` that POSTs to `index.php?mode=yourmode`.
|
||||
2. Create a `ZAPYourMode` handler in `lib/` that reads `$_POST` values in its constructor and stores them in `$_SESSION`.
|
||||
3. Write a `js/your.js` file with a validation function, and include `<script src="js/your.js"></script>` in the template's `<head>`.
|
||||
4. Use a `<div id="status">` element in the template to display validation errors.
|
||||
|
||||
---
|
||||
|
||||
### Pattern 2: Polling Image Display (`display.tpl` + `js/display.js`)
|
||||
|
||||
Used by the `display` and `zap` modes to show collage progress. The server streams individual image responses, and the client alternates between two `<img>` tags to give the appearance of continuous updating.
|
||||
|
||||
**Server side:**
|
||||
|
||||
`ZAPDisplay` (in `lib/ZAPDisplay.class.php`) sets session variables from `$_POST` and renders `tpl/display.tpl`. The template declares `onload="go(this,['img1','img2'],5000);"` on the `<body>` tag, which kicks off the polling loop.
|
||||
|
||||
`ZAPZap` (in `lib/ZAPZap.class.php`) handles each individual poll request. On each invocation it either:
|
||||
- Downloads the next source image (if `$_SESSION['words']` is not empty) and streams it via `ImgIOTools::onScreen()`.
|
||||
- Starts the next collage (if `$_SESSION['words']` is empty but `$_SESSION['collages'] > 0`).
|
||||
- Adds the next source image to the current collage via `ImgProcess::addToCanvas()`.
|
||||
- Signals completion by destroying the session and returning an error string (which triggers the `onerror` handler in JavaScript).
|
||||
|
||||
**Client side (`js/display.js`):**
|
||||
|
||||
```javascript
|
||||
var go = function(b, img_id, interval) {
|
||||
var i = 0;
|
||||
var img0 = document.getElementById(img_id[0]);
|
||||
var img1 = document.getElementById(img_id[1]);
|
||||
var img = [img0, img1];
|
||||
var activeImage = 1; // index of visible image
|
||||
var url = 'index.php?mode=zap';
|
||||
|
||||
if (isNaN(interval)) {
|
||||
interval = 5000;
|
||||
}
|
||||
|
||||
// Trigger the next image fetch after the current one has loaded.
|
||||
img[0].onload = img[1].onload = function() {
|
||||
var inactiveImage = activeImage === 0 ? 1 : 0;
|
||||
img[activeImage].style.display = 'none';
|
||||
img[inactiveImage].style.display = '';
|
||||
activeImage = inactiveImage;
|
||||
|
||||
setTimeout(function() {
|
||||
img[inactiveImage].src = url + '&i=' + (i++);
|
||||
}, interval);
|
||||
};
|
||||
|
||||
// When the backend signals completion it returns a small HTML page.
|
||||
// Show the ready link and stop fetching new images.
|
||||
img[0].onerror = img[1].onerror = function() {
|
||||
document.getElementById('ready').innerHTML = '<a href="/">Ready</a>';
|
||||
};
|
||||
|
||||
// Kick off the first request.
|
||||
img[activeImage].src = url + '&i=' + (i++);
|
||||
};
|
||||
```
|
||||
|
||||
**How the polling cadence works:**
|
||||
|
||||
1. `body onload="go(this,['img1','img2'],5000)"` starts the loop with a 5 s interval between successive fetches.
|
||||
2. Two `<img>` tags (`#img1` and `#img2`) are used — one is visible, the other hidden.
|
||||
3. When the visible image finishes loading (`img.onload`), the pair swaps: the loaded image is hidden, and the previously hidden one is shown. After a 5 s delay (`setTimeout`), the newly hidden image's `src` is set to `index.php?mode=zap&i={counter}`.
|
||||
4. Each request to `mode=zap` returns raw image data (a `Content-Type: image/jpeg` response). The browser renders it into the `<img>` tag, triggering `onload` again and continuing the cycle.
|
||||
5. When `ZAPZap` finishes all collages, it destroys the session and returns an HTML error string instead of image data. The `<img>` tag fails to load an image, triggering `onerror`, which replaces the image area with a "Ready" link.
|
||||
|
||||
**To reproduce this pattern in a new handler:**
|
||||
|
||||
1. Create a template with two `<img>` elements and `<body onload="go(this,['id1','id2'],intervalMs)">`.
|
||||
2. Include `js/display.js` (or copy the pattern into your own JS file).
|
||||
3. Create a `ZAP{Mode}` handler that returns raw image data (set `Content-Type` headers and use `echo` with `readfile()` or Imagick's `getImageBlob()`).
|
||||
4. Signal completion by returning a non-image response that will trigger `onerror` on the `<img>` tag.
|
||||
|
||||
---
|
||||
|
||||
## Summary: Creating a New Feature End-to-End
|
||||
|
||||
To add a completely new feature to ZapMachine, follow these steps:
|
||||
|
||||
1. **Add configuration** in `conf/conf.php` (constants, engine names) if needed.
|
||||
2. **Create the handler** `lib/ZAP{Mode}.class.php` with `getContent()` returning a string.
|
||||
3. **Create the template** `tpl/{mode}.tpl` using the `ob_start`/`include`/`extract`/`ob_get_clean` pattern.
|
||||
4. **Choose an interactive pattern:**
|
||||
- For form input with validation: add a form in the template, write a JS validation function, and include the script.
|
||||
- For polling/image updates: use the two-`<img>` swap with `onload`/`onerror`.
|
||||
5. **Create a JS file** in `www/js/` and reference it from the template's `<head>`.
|
||||
6. **Test the route** at `index.php?mode={ModeName}`.
|
||||
7. **Test from CLI** using the bootstrap pattern in `cli_scripts/test.php` (remember `chdir(__DIR__)` + manual `Session::start()` if needed).
|
||||
Loading…
Add table
Add a link
Reference in a new issue