Gerador de conjunto PHP Mandelbrot

Sep 04 2020

Preciso me familiarizar com a programação da web, então decidi escrever uma página PHP bastante simples que aceita solicitações GET e exibe uma imagem do conjunto de Mandelbrot ... como uma tabela de células coloridas.

Exemplos:

e

Estou enviando a resposta como uma tabela porque ainda não queria usar bibliotecas de imagens, então queria tentar fazer isso inteiramente usando HTML e não consegui pensar em uma maneira mais simples.

Basicamente, como funciona, você indica onde no Conjunto deseja visualizar, especificando os campos "Limites", o tamanho da visualização e da tabela, a proporção da imagem (como um decimal) e, em seguida, nove campos coloridos que decidem como a imagem será colorido.

A lógica principal é essencialmente uma porta do meu código C do ano passado , e a ideia de colorir foi retirada de um projeto que fiz há vários anos em Clojure.

Estou procurando sugestões sobre qualquer coisa. Faz anos que não uso HTML, PHP ou CSS e, para começar, nunca fui muito bom com eles. Há muito aqui, então não espero revisões super completas, mas qualquer coisa seria apreciada.

Porém, especificamente:

  • A função new_color_f()é um desastre. Basicamente, quero ser capaz de produzir uma função que tenha alguns parâmetros já definidos e estou fazendo isso fechando os parâmetros da função envolvente. É longo e feio, porém, e é piorado pela usesintaxe do PHP . Qualquer sugestão seria apreciada.

  • A função também defaulting_get()parece um cheiro. Eu preciso levar em consideração os dados GET ausentes e potencialmente vazios. Isso levou ao uso de uma mistura bizarra de operadores ??e ?:.

Qualquer coisa, exceto:

  • Sim, isso é estúpido de fazer em PHP do jeito que eu sou, e também é estúpido fazer isso usando HTML table. Isso causa problemas com o formulário de entrada sendo apagado de todas as entradas e também causa um atraso incrível do navegador ao receber tabelas maiores. Eu queria ter alguma experiência com PHP antes de tentar algo mais complicado, e sempre adorei esse projeto. Realmente, eu deveria estar fazendo isso em JavaScript ou usando AJAX para solicitar imagens em vez de usar um formulário.

O servidor está executando o PHP 7.4 para referência.


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;
    }

Respostas

3 SᴀᴍOnᴇᴌᴀ Sep 05 2020 at 06:36

Interface de usuário

Percebi que se eu inserir valores no formulário e depois enviá-los, os valores não serão persistidos após o carregamento da próxima página. Seria bom ter os valores persistentes nas entradas do formulário. Isso é fácil de conseguir, pois o formulário é renderizado por uma página PHP.

Como o código PHP para a última metade de index.php vem depois que o HTML é fechado, o HTML gerado para a tabela está tecnicamente fora do documento HTML:

    </form>

  </body>
</html>

<table class='display_table'>

A maioria dos navegadores modernos exibe a tabela como se estivesse dentro da <body>tag, mas não é tecnicamente uma estrutura válida. Usandohttps://validator.w3.org/ com a fonte gerada mostra esta saída:

Uma convenção comum é ter a maior parte do código PHP no topo do arquivo - especialmente útil se houver funcionalidade que modifica os cabeçalhos HTTP, por exemplo, chamadas para, header()uma vez que os cabeçalhos não podem ser enviados depois que o HTML foi emitido.

Lembre-se de que header()deve ser chamado antes de qualquer saída real ser enviada, seja por tags HTML normais, linhas em branco em um arquivo ou de PHP. 1

O HTML gerado a partir das chamadas PHP (por exemplo, chamadas para as emit_mandelbrot_view()quais precisariam returndo HTML gerado em vez de chamar echodiretamente) pode ser armazenado em uma variável ou emitido embutido, opcionalmente usando a sintaxe de atalho para echo- isto é <?=:

<?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>

Sugestões de código

  • new_color_fé um desastre. Basicamente, quero ser capaz de produzir uma função que tenha alguns parâmetros já definidos e estou fazendo isso fechando os parâmetros da função envolvente. É longo e feio, porém, e é piorado pela sintaxe de uso do PHP. Qualquer sugestão seria apreciada.

Como você confirmou que o servidor em que o código é executado está usando PHP 7.4, o código pode ser atualizado para aproveitar as vantagens de recursos mais recentes, como funções de seta

As funções de seta suportam os mesmos recursos das funções anônimas, exceto que o uso de variáveis ​​do escopo pai é sempre automático.

Isso permitiria que a usedeclaração fosse eliminada em new_color_f().


A função range()pode ser usada foreachpara simplificar os loops aninhados em 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) {

Pode ser simplificado para:

foreach (range($lower_imag, $upper_imag, $imag_step) as $imag) { echo "<tr>"; foreach (range($lower_real, $upper_real, $real_step) as $real) {

embora dada a quantidade de vezes que a função é chamada, pode não ser aconselhável adicionar mais chamadas de função apenas para simplificar a sintaxe.


Para este bloco em 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),
    );

Obviamente, há muita duplicação aqui. Uma simplificação seria colocar as chaves da string de consulta em uma matriz e fazer um loop sobre elas, passando-as para a função e armazenando os resultados em uma matriz que pode ser distribuída em uma chamada para new_color_f().


No test_point()iterador é configurado fora do loop:

$i = 0; for (; $i < $max_iters && is_under_limit($current, $inf_limit); $i++) {
       $current = mandelbrot_iteration($initial_complex, $current);
}

Ao contrário do código c, $ipode ser declarado na primeira instrução do forloop e ainda ser visível fora do loop, pois as variáveis ​​não estão limitadas a um bloco, mas sim a uma função (se aplicável). Isso não é muito explicado no escopo da variável de documentação do PHP, embora haja um comentário sobre isso .

dodothedreamer no gmail ponto com ¶ 8 anos atrás


Observe que, ao contrário de Java e C ++, as variáveis ​​declaradas dentro de blocos, como loops ou if's, também serão reconhecidas e acessíveis fora do bloco, portanto:

<?php
for($j=0; $j<3; $j++) { if($j == 1)
        $a = 4; } echo $a;
?>

Imprime 4.


Esta definição constante se parece com uma otimização de estilo C

const COLOR_MAX = (2 << 7) - 1; 

Corrija-me se eu estiver errado, mas duvido que haja alguma otimização em declarar isso como 255:

const COLOR_MAX = 255;

PS Eu perguntei sobre essa declaração constante no Stack Overflow . Pelas respostas até agora, parece ser apenas uma preferência pessoal quanto à forma como o valor é declarado. Se você preferir usar a sintaxe bit-shifted, sinta-se à vontade para fazê-lo, mas esteja avisado de que, se você trabalhar em uma equipe com outras pessoas, isso pode surgir como uma questão em uma revisão de código.

1 bdsl Sep 13 2020 at 22:55

Tipos

Isso torna o código mais fácil de entender se você especificar tipos para todas as assinaturas de função. O PHP verificará os tipos especificados em tempo de execução sempre que entrar e sair de uma função e ao gravar em uma propriedade de classe, ajudando você a detectar e encontrar erros muito mais rapidamente, especialmente se estiver usando um IDE ou ferramenta de análise estática. Eles também ajudam a comunicar a intenção do seu código para tornar mais fácil adicionar mais a ele.

Alguns exemplos de mudanças para adicionar tipos:

Em index.php, mude parafunction defaulting_get($key, $default) {function defaulting_get(string $key, int $default): int {

Em display.php, mude function clamp_color($n) {parafunction clamp_color(int $n): int {

e

    function new_color_f($red_real, $red_imag, $red_iter, $gre_real, $gre_imag, $gre_iter,
                         $blu_real, $blu_imag, $blu_iter) {

para

    /** @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 {

Não tenho certeza se entendi o tipo de encerramento certo no docblock.

Eu faria isso para cada função.

Evitando use

Você pode evitar a necessidade de usetirar proveito da sintaxe de encerramento breve introduzida no 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
            );
        };
    }

O PHP inclui automaticamente todas as variáveis ​​mencionadas após o =>no encerramento curto.

Aulas e carregamento automático

O PHP idiomático moderno, particularmente para aplicativos maiores, tende a ter quase todo o código na forma de classes, mesmo se o aplicativo não for orientado a objetos. Uma vantagem de usar classes é que o sistema de carregamento automático do PHP pode carregá-las sob demanda. Isso não está disponível para funções gratuitas. Funções estáticas em classes podem ser convenientemente usadas no lugar de funções livres.

Substituir todas as funções livres por funções de classe estática também tornará possível marcar quaisquer funções usadas apenas dentro da classe que as declara como private, o que deve tornar muito mais fácil entender o fluxo do código.

A maneira típica de usar o carregamento automático é configurar o projeto com o Composer, organizar as classes uma por arquivo, seguindo as regras do PSR-4 para a localização e nome de cada arquivo, e fazer o Composer gerar um arquivo autoload.php . Isso pode ser chamado com require_oncede index.phpou qualquer outro ponto de entrada. Todas as classes necessárias depois disso serão carregadas automaticamente se puderem ser encontradas.

Eu também faria new_color_f retornar um objeto em vez de um encerramento e renomeá-lo-ia de acordo. Este objeto pode ter campos privados em vez de variáveis ​​incluídas e uma getRGBfunção pública.