Генератор множеств Мандельброта PHP
Мне нужно познакомиться с веб-программированием, поэтому я решил написать довольно простую страницу PHP, которая принимает запросы GET и возвращает изображение набора Мандельброта ... в виде таблицы цветных ячеек.
Примеры:
и
Я отправляю ответ в виде таблицы, потому что я еще не хотел использовать библиотеки изображений, поэтому я хотел попробовать сделать это полностью с помощью HTML, и я не мог придумать более простого способа.
В основном, как это работает, вы указываете, где в наборе вы хотите просмотреть, указав поля «Границы», размер представления и таблицы, соотношение сторон (в виде десятичного числа), а затем девять цветных полей, которые определяют, как изображение будет раскрашен.
Основная логика - это, по сути, перенос моего кода на C из прошлого года , а идея раскраски взята из проекта, который я делал несколько лет назад в Clojure.
Ищу предложения по чему угодно. Я не использовал HTML, PHP или CSS уже много лет, и с самого начала я никогда не был хорош с ними. Здесь много всего, поэтому я не жду супер тщательных обзоров, но все будет оценено.
В частности, хотя:
Функция
new_color_f()катастрофическая. По сути, я хочу иметь возможность создать функцию, у которой уже установлены некоторые параметры, и я делаю это, закрывая параметры включающей функции. Однако он длинный и уродливый, и его усугубляетuseсинтаксис PHP . Любые предложения будут оценены.Функция
defaulting_get()тоже похожа на запах. Мне нужно учитывать, что данные GET отсутствуют и потенциально пусты. Это привело к использованию причудливую смесь??и?:операторов , хотя.
Все, кроме:
- Да, это глупо делать в PHP так, как я, и также глупо делать это с помощью HTML
table. Это вызывает проблемы с очисткой формы ввода при каждой записи, а также вызывает невероятную задержку браузера при получении больших таблиц. Я хотел получить некоторый опыт работы с PHP, прежде чем пробовать что-то более сложное, и мне всегда нравился этот проект. На самом деле, я должен делать это в JavaScript или использовать AJAX для запроса изображений вместо использования формы.
Для справки на сервере работает PHP 7.4.
index.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Mandelbrot Generator</title>
<style>
.display_table {
font-size: 1px;
border-spacing: 0;
}
.input_form {
display: table;
}
.input_row {
display: table-row;
}
.input_row label, .input_row input {
display: table-cell;
}
.red_input input, .green_input input, .blue_input input {
border-width: 5px;
}
.red_input input {
border-color: red;
}
.green_input input {
border-color: lightgreen;
}
.blue_input input {
border-color: blue;
}
</style>
</head>
<body>
<form method="get" class="input_form">
<div class="input">
<div class="location_input">
<div class="input_row">
<label>Lower Real Bound</label>
<input name="lreal" type="number" min="-2" max="2" step="any">
</div>
<div class="input_row">
<label>Lower Imaginary Bound</label>
<input name="limag" type="number" min="-2" max="2" step="any">
</div>
<div class="input_row">
<label>View Width</label>
<input name="vwidth" type="number" min="0" max="4" step="any">
</div>
<div class="input_row">
<label>Pixels Wide</label>
<input name="pwidth" type="number" min="0" max="1000" step="any">
</div>
<div class="input_row">
<label>Aspect Ratio</label>
<input name="aratio" type="number" min="0" max="2" step="any">
</div>
</div>
<div class="color_input">
<div class="red_input">
<div class="input_row">
<label>Real</label>
<input name="rr" type="number" min="-1000" max="1000" step="any">
</div>
<div class="input_row">
<label>Imaginary</label>
<input name="rim" type="number" min="-1000" max="1000" step="any">
</div>
<div class="input_row">
<label>Iters</label>
<input name="rit" type="number" min="-1000" max="1000" step="any">
</div>
</div>
<div class="green_input">
<div class="input_row">
<label>Real</label>
<input name="gr" type="number" min="-1000" max="1000" step="any">
</div>
<div class="input_row">
<label>Imaginary</label>
<input name="gim" type="number" min="-1000" max="1000" step="any">
</div>
<div class="input_row">
<label>Iters</label>
<input name="git" type="number" min="-1000" max="1000" step="any">
</div>
</div>
<div class="blue_input">
<div class="input_row">
<label>Real</label>
<input name="br" type="number" min="-1000" max="1000" step="any">
</div>
<div class="input_row">
<label>Imaginary</label>
<input name="bim" type="number" min="-1000" max="1000" step="any">
</div>
<div class="input_row">
<label>Iters</label>
<input name="bit" type="number" min="-1000" max="1000" step="any">
</div>
</div>
</div>
</div>
<button type="submit">Submit</button>
</form>
</body>
</html>
<?php
include "display.php";
const DEF_LOW_REAL = -2;
const DEF_LOW_IMAG = -2;
const DEF_VIEW_WIDTH = 4;
const DEF_PIX_WIDTH = 100;
const DEF_ASP_RATIO = 1;
const DEF_COLOR_MULT = 2;
function defaulting_get($key, $default) {
return ($_GET[$key] ?? $default) ?: $default;
}
$low_real = defaulting_get("lreal", DEF_LOW_REAL); $low_imag = defaulting_get("limag", DEF_LOW_IMAG);
$view_width = defaulting_get("vwidth", DEF_VIEW_WIDTH); $pixels_wide = defaulting_get("pwidth", DEF_PIX_WIDTH);
$aspect_ratio = defaulting_get("aratio", DEF_ASP_RATIO); $view_height = $view_width / $aspect_ratio;
$high_real = $low_real + $view_height; $high_imag = $low_imag + $view_width;
$pixels_high = $pixels_wide / $aspect_ratio; $color_f = new_color_f(
defaulting_get("rr", DEF_COLOR_MULT),
defaulting_get("rim", DEF_COLOR_MULT),
defaulting_get("rit", DEF_COLOR_MULT),
defaulting_get("gr", DEF_COLOR_MULT),
defaulting_get("gim", DEF_COLOR_MULT),
defaulting_get("git", DEF_COLOR_MULT),
defaulting_get("br", DEF_COLOR_MULT),
defaulting_get("bim", DEF_COLOR_MULT),
defaulting_get("bit", DEF_COLOR_MULT),
);
emit_mandelbrot_view(
$color_f, $low_real,
$high_real, $low_imag,
$high_imag, $pixels_wide,
$pixels_high);
display.php
<?php
include "iteration.php";
const COLOR_MAX = (2 << 7) - 1;
function clamp_color($n) {
return max(0, min(COLOR_MAX, $n)); } function checked_produce_rgb($red, $green, $blue) {
$c_red = clamp_color($red);
$c_green = clamp_color($green);
$c_blue = clamp_color(($blue));
return "rgb($c_red, $c_green, $c_blue)"; } function new_color_f($red_real, $red_imag, $red_iter,
$gre_real, $gre_imag, $gre_iter, $blu_real, $blu_imag, $blu_iter) {
$color_func = function($real, $imag, $iters) use
($red_real, $red_imag, $red_iter, $gre_real, $gre_imag, $gre_iter,
$blu_real, $blu_imag, $blu_iter) { return checked_produce_rgb( $real * $red_real + $imag * $red_imag + $iters * $red_iter, $real * $gre_real + $imag * $gre_imag + $iters * $gre_iter, $real * $blu_real + $imag * $blu_imag + $iters * $blu_iter ); }; return $color_func;
}
function produce_pixel($color) { return "<td style='background: $color; color: $color'>_</td>"; } function emit_mandelbrot_view($color_f,
$lower_real, $upper_real,
$lower_imag, $upper_imag,
$pixels_wide, $pixels_high) {
$real_step = ($upper_real - $lower_real) / $pixels_wide;
$imag_step = ($upper_imag - $lower_imag) / $pixels_high;
echo "<table class='display_table'>";
for ($imag = $lower_imag; $imag <= $upper_imag; $imag += $imag_step) {
echo "<tr>";
for ($real = $lower_real; $real <= $upper_real; $real += $real_step) {
$iters = test_point([$real, $imag]); $color = $color_f($real, $imag, $iters);
echo produce_pixel($color);
}
echo "</tr>";
}
echo "</table>";
}
iteration.php
<?php
// Make mutative?
const STD_MAX_ITERS = 200;
const STD_INF_LIMIT = 2;
function square_complex($complex) {
[$real, $imag] = $complex; return [($real * $real) - ($imag * $imag), 2 * $real * $imag]; } function mandelbrot_iteration($init_complex, $curr_complex) { [$i_real, $i_imag] = $init_complex;
$sqrd = square_complex($curr_complex);
$sqrd[0] += $i_real;
$sqrd[1] += $i_imag;
return $sqrd; } function is_under_limit($complex, $inf_limit) { [$real, $imag] = $complex;
return ($real * $real) + ($imag * $imag) <= ($inf_limit * $inf_limit);
}
function test_point($initial_complex, $max_iters = STD_MAX_ITERS,
$inf_limit = STD_INF_LIMIT) { $current = $initial_complex; $i = 0;
for (; $i < $max_iters && is_under_limit($current, $inf_limit); $i++) { $current = mandelbrot_iteration($initial_complex, $current);
}
return $i;
}
Ответы
Пользовательский интерфейс
Я заметил, что если я ввел значения в форму, а затем отправил ее, значения не сохранялись после загрузки следующей страницы. Было бы неплохо, чтобы значения сохранялись во входных данных формы. Этого легко добиться, поскольку форма отображается на странице PHP.
Поскольку код PHP для второй половины index.php появляется после закрытия HTML, HTML, созданный для таблицы, технически находится за пределами документа HTML:
</form> </body> </html> <table class='display_table'>
Большинство современных браузеров отображают таблицу, как если бы она находилась внутри <body>тега, но технически это недопустимая структура. С помощьюhttps://validator.w3.org/ со сгенерированным источником показывает этот вывод:
Распространенным соглашением является размещение большей части кода PHP в верхней части файла - особенно полезно, если есть функции, которые изменяют заголовки HTTP, например, вызовы, header()поскольку заголовки не могут быть отправлены после отправки HTML.
Помните, что он
header()должен быть вызван перед отправкой любого фактического вывода, либо с помощью обычных тегов HTML, пустых строк в файле, либо из PHP. 1
HTML, сгенерированный из вызовов PHP (например, вызовы, для emit_mandelbrot_view()которых потребуется returnсгенерированный HTML вместо echoпрямого вызова ), может быть сохранен в переменной или выдан встроенным, необязательно с использованием синтаксиса ярлыка для echo- то есть <?=:
<?php
//setup PHP code here
?>
<!DOCTYPE html>
<html lang="en">
<head>
<!-- ... HTML headers here -->
</head>
<body>
<form>
<!-- ... HTML here -->
</form>
<?= emit_mandelbrot_view($color_f...)?>
</body>
</html>
Предложения кода
new_color_fэто катастрофа. По сути, я хочу иметь возможность создать функцию, у которой уже установлены некоторые параметры, и я делаю это, закрывая параметры включающей функции. Это длинно и уродливо, и усугубляется синтаксисом использования PHP. Любые предложения будут оценены.
Поскольку вы подтвердили, что сервер, на котором работает код, использует PHP 7.4, код можно обновить, чтобы использовать новые функции, такие как функции стрелок.
Стрелочные функции поддерживают те же функции, что и анонимные функции, за исключением того, что использование переменных из родительской области всегда происходит автоматически.
Это позволит useисключить оператор в new_color_f().
Функцию range()можно использовать foreachдля упрощения вложенных циклов внутри emit_mandelbrot_view():
for ($imag = $lower_imag; $imag <= $upper_imag; $imag += $imag_step) { echo "<tr>"; for ($real = $lower_real; $real <= $upper_real; $real += $real_step) {
Можно упростить до:
foreach (range($lower_imag, $upper_imag, $imag_step) as $imag) { echo "<tr>"; foreach (range($lower_real, $upper_real, $real_step) as $real) {
хотя, учитывая количество вызовов этой функции, может быть нецелесообразно добавлять дополнительные вызовы функций только для упрощения синтаксиса.
Для этого блока в index.php:
$color_f = new_color_f( defaulting_get("rr", DEF_COLOR_MULT), defaulting_get("rim", DEF_COLOR_MULT), defaulting_get("rit", DEF_COLOR_MULT), defaulting_get("gr", DEF_COLOR_MULT), defaulting_get("gim", DEF_COLOR_MULT), defaulting_get("git", DEF_COLOR_MULT), defaulting_get("br", DEF_COLOR_MULT), defaulting_get("bim", DEF_COLOR_MULT), defaulting_get("bit", DEF_COLOR_MULT), );
Очевидно, здесь много дублирования. Одним из упрощений было бы поместить ключи строки запроса в массив и перебрать их, передав их функции и сохраняя результаты в массиве, который можно распространить в вызове new_color_f().
В test_point()итераторе настраивается вне цикла:
$i = 0; for (; $i < $max_iters && is_under_limit($current, $inf_limit); $i++) { $current = mandelbrot_iteration($initial_complex, $current); }
В отличие от кода c код $iможет быть объявлен в первом операторе forцикла и по-прежнему быть видимым вне цикла, поскольку переменные не ограничиваются блоком, а скорее функцией (если применимо). Это не совсем хорошо объяснено в области видимости переменных документации PHP, хотя есть комментарий по этому поводу .
dodothedreamer at gmail dot com ¶ 8 лет назад
Обратите внимание, что в отличие от Java и C ++ переменные, объявленные внутри блоков, таких как циклы или if, также будут распознаваться и доступны за пределами блока, поэтому:
<?php for($j=0; $j<3; $j++) { if($j == 1) $a = 4; } echo $a; ?>Напечатал бы 4.
Это определение константы выглядит как оптимизация в стиле C.
const COLOR_MAX = (2 << 7) - 1;
Поправьте меня, если я ошибаюсь, но я сомневаюсь, что есть какая-то оптимизация по объявлению этого как 255:
const COLOR_MAX = 255;
PS Я спросил об этом постоянном объявлении в Stack Overflow . Судя по полученным ответам, то, как объявляется стоимость, кажется, что это личное предпочтение. Если вы предпочитаете использовать синтаксис с битовым смещением, то не стесняйтесь делать это, но имейте в виду, что если вы будете работать в команде с другими, это может возникнуть в качестве вопроса при проверке кода.
Типы
Это упрощает понимание кода, если вы укажете типы для всех сигнатур функций. PHP будет проверять типы, если они указаны во время выполнения, при входе и выходе из функции, а также при записи в свойство класса, помогая вам обнаруживать и находить ошибки гораздо быстрее, особенно при использовании IDE или инструмента статического анализа. Они также помогают донести цель вашего кода, чтобы упростить его добавление.
Некоторые примеры изменений для добавления типов:
В index.php измените наfunction defaulting_get($key, $default) {function defaulting_get(string $key, int $default): int {
В display.php измените function clamp_color($n) {наfunction clamp_color(int $n): int {
и
function new_color_f($red_real, $red_imag, $red_iter, $gre_real, $gre_imag, $gre_iter,
$blu_real, $blu_imag, $blu_iter) {
к
/** @return \Closure(int, int, int): string */
function new_color_f(int $red_real, int $red_imag, int $red_iter,
int $gre_real, int $gre_imag, int $gre_iter, int $blu_real, int $blu_imag, int $blu_iter): \Closure {
Я не уверен, что у меня есть тип этого закрытия прямо в докблоке.
Я бы сделал это для каждой функции.
Избегая use
Вы можете избежать необходимости use, воспользовавшись коротким синтаксисом закрытия, представленным в PHP 7.4:
/** @return \Closure(int, int, int): string */
function new_color_f(int $red_real, int $red_imag, int $red_iter, int $gre_real, int $gre_imag, int $gre_iter,
int $blu_real, int $blu_imag, int $blu_iter): \Closure { return fn(int $real, int $imag, int $iters): string =>
checked_produce_rgb(
$real * $red_real + $imag * $red_imag + $iters * $red_iter,
$real * $gre_real + $imag * $gre_imag + $iters * $gre_iter,
$real * $blu_real + $imag * $blu_imag + $iters * $blu_iter
);
};
}
PHP автоматически включает в себя любые переменные, упомянутые после =>короткого замыкания.
Классы и автозагрузка
Современный идиоматический PHP, особенно для больших приложений, имеет тенденцию иметь почти весь код в форме классов, даже если приложение не предназначено для объектно-ориентированного программирования. Одним из преимуществ использования классов является то, что система автозагрузки PHP может загружать их по запросу. Это недоступно для бесплатных функций. Статические функции в классах можно удобно использовать вместо бесплатных функций.
Замена всех бесплатных функций статическими функциями класса также позволит пометить любые функции, используемые только внутри класса, который объявляет их как private, что должно значительно упростить понимание потока кода.
Типичный способ использования автозагрузки - это настроить проект с помощью Composer, организовать классы по одному для каждого файла, следуя правилам PSR-4 для расположения и имени каждого файла, и заставить Composer сгенерировать файл autoload.php . Это можно вызвать с помощью require_oncefrom index.phpили любой другой точки входа. Любые классы, которые понадобятся после этого, будут автоматически загружены, если их можно будет найти.
Я бы также заставил new_color_f возвращать объект вместо закрытия и соответственно переименовать его. Этот объект может иметь закрытые поля вместо закрытых переменных и getRGBобщедоступную функцию.