Monday, 24 March 2025

Web Tutorial: The Movie Poster Generator

Hi, readers. Do I have a fun one today!

We will be leveraging on the code I wrote for the Nike Meme Generator, to generate something else that may require an image upload - a Movie Poster Generator! This one is going to require some ChatGPT finangling as well.

Let's begin by setting up the uploads directory, and adding a default image - an AI-generated portrait of Angelina Jolie.
uploads/angelinajolie.jpg

Then, in the parent folder, we copy over the code from the Nike Meme Generator in index.php. We'll be removing quite a lot of lines. You'll see what we've retained is the file upload functions and the form. We've also made a few text changes as well. The div memeContainer has been replaced by posterContainer, along with styling.
<?php
$filecode = "angelinajolie";
$filetype = "jpg";
//$line1 = "Believe in something.";
//$line2 = "Even if it means sacrificing everything.";
//$slogan = "Just Do It.";

$strmessage="";

if (isset($_POST["btSubmit"]))
{
    //$line1 = $_POST["txtLine1"];
    //$line2 = $_POST["txtLine2"];
    //$slogan = $_POST["txtSlogan"];

    if (basename($_FILES["flUpload"]["name"]) != "")
    {
        $uploadsize = intval($_POST["hidUploadSize"]);
         $filetype = pathinfo($_FILES["flUpload"]["name"],PATHINFO_EXTENSION);
         $filetype = strtolower($filetype);

         if ($_FILES["flUpload"]["size"] > $uploadsize)
         {
             $strmessage = "Error was encountered while uploading file. File cannot exceed " . ($uploadsize/1000) . "kb";
        }
        else
        {
          if (!is_array(getimagesize($_FILES["flUpload"]["tmp_name"])))
          {
           $strmessage = "File type invalid";
          }
          else
          {
              $filecode=strtotime("now").rand();
        
              if (move_uploaded_file($_FILES["flUpload"]["tmp_name"], "uploads/" . $filecode . "." . $filetype))
              {
               $strmessage = "File uploaded.";
              }
              else
              {
                  $strmessage = "Error was encountered while uploading file.";
              }  
          }
         }
    }
    else
    {
         $strmessage="No file selected.";
    }
}
?>

<!DOCTYPE html>
<html>
    <head>
        <title>Movie Poster Generator</title>

        <style>
        #pnlMessage
        {
            width: 100%;
            height: 50px;
            color: #FF0000;
            outline: 0px solid #DDDDDD;
        }

        #formContainer
        {
            width: 400px;
            padding: 5px;
            margin: 5px;
            float: left;
            outline: 0px solid #DDDDDD;
        }

        /*
        #memeContainer
        {
            width: 500px;
            height: 500px;
            padding: 5px;
            margin: 5px;
            float: left;
            outline: 1px solid #DDDDDD;
            background: url(<?php echo "uploads/" . $filecode . "." . $filetype; ?>) center center no-repeat;
            background-size: cover;
            font-family: georgia;
            color: #FFFFFF;
            font-size: 25px;
            -webkit-filter: grayscale(100%);
            filter: grayscale(100%);
            text-align: center;
        }
         */

        #posterContainer
        {
            width: 800px;

            height: 500px;
            margin: 5px;
            float: left;
            text-align: center;
        }

        @media print
        {
               #formContainer, #pnlMessage
               {
                   display: none;
               }
    
               #posterContainer
               {
                    margin: 10% auto 0 auto;
                    float: none;
               }
        }
        </style>
    </head>

    <body>
        <div id="pnlMessage"><?php echo $strmessage; ?></div>

        <div id="formContainer">
            <form id="frmUpload" name="frmUpload" action="" method="POST" enctype="multipart/form-data">
                  <label for="flUpload">File</label>
                  <input type="file" name="flUpload" id="flUpload">
                  <input type="hidden" name="hidUploadSize" id="hidUploadSize" value="50000000">
                  <br /><br />
                  <!---
                 <label for="txtLine1">Line 1</label>
                  <input name="txtLine1" id="txtLine1" maxlength="50" value="<?php echo $line1; ?>" />
                  <br /><br />
                 <label for="txtLine2">Line 2</label>
                  <input name="txtLine2" id="txtLine2" maxlength="50" value="<?php echo $line2; ?>" />
                  <br /><br />
                  <label for="txtSlogan">Slogan</label>
                  <input name="txtSlogan" id="txtSlogan" maxlength="20" value="<?php echo $slogan; ?>" />
                  --->
                  <br /><br />
                  <input type="submit" name="btSubmit" id="btSubmit" value="Create your Movie Poster!">
            </form>
        </div>

        <div id="posterContainer">
             <!---
             <p style="margin-top:50%"><?php echo $line1;?><br /><?php echo $line2;?></p>
             <p style="margin-top:30%"><img src="nikelogo.png"> <?php echo $slogan;?></p>
             --->
        </div>
    </body>
</html>


Because I plan on using jQuery in here, let's include the library as well. And a script tag for custom JavaScript.
<head>
  <title>Movie Poster Generator</title>

  <style>
    #pnlMessage
    {
      width: 100%;
      height: 50px;
      color: #FF0000;
      outline: 0px solid #DDDDDD;
    }

    #formContainer
    {
      width: 400px;
      padding: 5px;
      margin: 5px;
      float: left;
      outline: 0px solid #DDDDDD;
    }

    #posterContainer
    {
      width: 800px;
      height: 500px;
      margin: 5px;
      float: left;
      text-align: center;
    }

    @media print
    {
        #formContainer, #pnlMessage
        {
          display: none;
        }

        #posterContainer
        {
          margin: 10% auto 0 auto;
          float: none;
        }
    }
  </style>

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
  <script>

  </script>

</head>


We are going to begin the PHP with some variables other than strmessage, filecode and filetype. You'll see that I've specified some default values too.

space_from_top: controls the vertical position that the block of text occupies.
movie_title: self-explanatory.
movie_title_color: what color it appears in.
movie_title_size: what font size to use for the title.
movie_tagline: the tagline that appears beneath the title.
movie_tagline_color: what color it appears in.
movie_tagline_size: what font size to use for the tagline.
movie_starring: the movie star name(s). Since I'm using a picture of Angeline Jolie, that's her name I'm using.
movie_starring_color: what color it appears in.
movie_starring_size: what font size to use.
reviews: this is an array which will contain the stuff we get from ChatGPT.
reviews_color: what color they appear in.
reviews_bgcolor: what background color the reviews will use.

<?php
$filecode = "angelinajolie";
$filetype = "jpg";

$space_from_top = "20";

$movie_title = "Modern-day Maleficent";
$movie_title_color = "#FFFFFF";
$movie_title_size = "30";

$movie_tagline = "A re-imagining of the original tale of darkness";
$movie_tagline_color = "#FFFFFF";
$movie_tagline_size = "15";

$movie_starring = "Angelina Jolie";
$movie_starring_color = "#FFFFFF";
$movie_starring_size = "10";

$reviews = [];
$reviews_color = "#FFFFFF";
$reviews_bgcolor = "#000000";


$strmessage="";


Inside the posterContainer div, add three divs, left, middle and right. left and right are styled using the CSS class review.
<div id="posterContainer">
    <div id="left" class="review">

    </div>
    <div id="middle">

    </div>
    <div id="right" class="review">

    </div>

</div>


In the styles, both left and right have different text alignments. As they are both styled by the CSS class review, they have a certain width and height, they're floated left with a little padding, and color and background-color properties are determined by reviews_color and reviews_bgcolor respectively. The font type is less important and it's a personal choice.
#posterContainer
{
    width: 800px;
    height: 500px;
    margin: 5px;
    float: left;
     text-align: center;
}

#left
{
    text-align: right;
}

#right
{
    text-align: left;
}

.review
{
    width: 180px;
    height: 480px;
    float: left;
    padding: 10px;
    color: <?php echo $reviews_color;?>;
    background-color: <?php echo $reviews_bgcolor;?>;
    font-family: Georgia;
}

@media print
{
    #formContainer, #pnlMessage
    {
        display: none;
    }

    #posterContainer
    {
        margin: 10% auto 0 auto;
        float: none;
    }
}


Now we have middle. Like the reviews CSS class, it has a certain width and height, and is floated left. The background is determined by filecode, which currently points to angelinajolie.jpg in the uploads folder.
#posterContainer
{
    width: 800px;
    height: 500px;
    margin: 5px;
    float: left;
     text-align: center;
}

#middle
{
    width: 380px;
    height: 500px;
    background: url(<?php echo "uploads/" . $filecode . "." . $filetype; ?>) center center no-repeat;
    background-size: cover;
    float: left;
}

#left
{
    text-align: right;
}

#right
{
    text-align: left;
}


Here's a preview!

In the middle div, add a paragraph tag with the id title_and_tagline. In there, we have span tags with the ids movie_title and movie_tagline respectively.
<div id="middle">
    <p id="title_and_tagline">
    
    <span id="movie_title"></span>
        <br />
        <span id="movie_tagline"></span>
    </p>
</div>


Populate these span tags with the strings for movie_title and movie_tagline.
<div id="middle">
    <p id="title_and_tagline">
        <span id="movie_title"><?php echo $movie_title;?></span>
        <br />
        <span id="movie_tagline"><?php echo $movie_tagline;?></span>
    </p>
</div>


Now add another paragraph with id movie_starring.
<div id="middle">
    <p id="title_and_tagline">
        <span id="movie_title"><?php echo $movie_title;?></span>
        <br />
        <span id="movie_tagline"><?php echo $movie_tagline;?></span>
    </p>
    <p id="movie_starring">

    </p>
</div>


In there, have some PHP. It's possible that movie_starring is an empty string, in which case we want no text in that paragraph. But if it's not an empty string, we want it to say "starring" followed by movie_starring.
<div id="middle">
    <p id="title_and_tagline">
        <span id="movie_title"><?php echo $movie_title;?></span>
        <br />
        <span id="movie_tagline"><?php echo $movie_tagline;?></span>
    </p>
    <p id="movie_starring">
        <?php echo ($movie_starring == "" ? "" : "starring ");?>
        <?php echo $movie_starring;?>
    </p>
</div>


Let's do some more styling. We have styles for title_and_tagline, movie_title, movie_tagline and movie_starring. We're doing largely what we did for the review CSS class, with font-size and color property values determined by the PHP variables. For title_and_tagline, the margin-top property is determined by space_from_top.
.review
{
    width: 180px;
    height: 480px;
    float: left;
    padding: 10px;
    color: <?php echo $reviews_color;?>;
    background-color: <?php echo $reviews_bgcolor;?>;
    font-family: Georgia;
}

#title_and_tagline
{
    margin-top: <?php echo $space_from_top;?>px;
}

#movie_title
{
    color: <?php echo $movie_title_color;?>;
    font-size: <?php echo $movie_title_size;?>px;
    font-weight: bold;
    font-family: Impact, Verdana;
}

#movie_tagline
{
    color: <?php echo $movie_tagline_color;?>;
    font-size: <?php echo $movie_tagline_size;?>px;
    font-family: Arial, Helvetica, Verdana;
}

#movie_starring
{
    color: <?php echo $movie_starring_color;?>;
    font-size: <?php echo $movie_starring_size;?>px;
    font-family: Arial, Helvetica, Verdana;
}

@media print
{
    #formContainer, #pnlMessage
    {
        display: none;
    }

    #posterContainer
    {
        margin: 10% auto 0 auto;
        float: none;
    }
}


You see the text appears!

Now we are going to make randomly-generated reviews appear. Remember the empty array reviews? Basically, we're about to populate it. The code will run only if the form has been submitted, so put it inside the If block. You'll need your own OpenAI Developer Account, so replace "xxx" with your credentials. Then define headers with the appropriate properties for authentication.
if (isset($_POST["btSubmit"]))
{
    if (basename($_FILES["flUpload"]["name"]) != "")
    {
         $uploadsize = intval($_POST["hidUploadSize"]);
         $filetype = pathinfo($_FILES["flUpload"]["name"],PATHINFO_EXTENSION);
         $filetype = strtolower($filetype);

         if ($_FILES["flUpload"]["size"] > $uploadsize)
         {
             $strmessage = "Error was encountered while uploading file. File cannot exceed " . ($uploadsize/1000) . "kb";
         }
         else
         {
             if (!is_array(getimagesize($_FILES["flUpload"]["tmp_name"])))
             {
                  $strmessage = "File type invalid";
             }
             else
             {
                  $filecode=strtotime("now").rand();
        
                  if (move_uploaded_file($_FILES["flUpload"]["tmp_name"], "uploads/" . $filecode . "." . $filetype))
                  {
                       $strmessage = "File uploaded.";
                  }
                  else
                  {
                       $strmessage = "Error was encountered while uploading file.";
                  }  
              }
         }
    }
    else
    {
         $strmessage="No file selected.";
    }

    $key = "xxx";
    $org = "org-xxx";
    $url = 'https://api.openai.com/v1/chat/completions';  

    $headers = [
        "Authorization: Bearer " . $key,
        "OpenAI-Organization: " . $org,
        "Content-Type: application/json"
    ];
}


Here, we're defining the prompt. We use movie_title (and movie_starring, if it's not an empty string), and generate an array, FakeReviews, of 10 objects. Each object will have a one-sentence comment and a string to determine the "source" of the comment.
$key = "xxx";
$org = "org-xxx";
$url = 'https://api.openai.com/v1/chat/completions';  

$headers = [
    "Authorization: Bearer " . $key,
    "OpenAI-Organization: " . $org,
    "Content-Type: application/json"
];

$messages = [];
$obj = [];
$obj["role"] = "user";
$obj["content"] = "Give me a JSON object with one property. The property should be named 'FakeReviews', and should be an array of ten objects. Each object should have the property 'review', which is a random fictional complimentary about the movie '" . $movie_title . "'" . ($movie_starring == "" ? "" : " or celebrity '" . $movie_starring . "'") .  " (range between three to ten words) sentence in a string, and the property 'critic' which contains the fictional publication for that quote.";
$messages[] = $obj;


The rest of the code we've gone through before in The Random Christmas Card and The Self-affirmations Wordpress Plugin
$messages = [];
$obj = [];
$obj["role"] = "user";
$obj["content"] = "Give me a JSON object with one property. The property should be named 'FakeReviews', and should be an array of ten objects. Each object should have the property 'review', which is a random fictional complimentary about the movie '" . $movie_title . "'" . ($movie_starring == "" ? "" : " or celebrity '" . $movie_starring . "'") .  " (range between three to ten words) sentence in a string, and the property 'critic' which contains the fictional publication for that quote.";
$messages[] = $obj;

$data = [];
$data["model"] = "gpt-3.5-turbo";
$data["messages"] = $messages;
$data["max_tokens"] = 1000;


We use cURL to send the data to the API endpoint. The returned response is in result, and we handle any errors before closing out with curl_close().
$data = [];
$data["model"] = "gpt-3.5-turbo";
$data["messages"] = $messages;
$data["max_tokens"] = 1000;

$curl = curl_init($url);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

$result = curl_exec($curl);
if (curl_errno($curl)) 

{
    echo 'Error:' . curl_error($curl);
}

curl_close($curl);


Then we'll use json_decode() on result, and then extract FakeReviews from it.
$result = curl_exec($curl);
if (curl_errno($curl)) 
{
    echo 'Error:' . curl_error($curl);
}

curl_close($curl);

$result = json_decode($result);
$content = $result->choices[0]->message->content;
$content = json_decode($content);

$reviews = $content->FakeReviews;


In the left div, use a PHP If block to check if reviews has 10 elements, just to filter out any nasty surprises. Then use a For loop to go through the first 5 elements.
<div id="left" class="review">
<?php
if (count($reviews) == 10) 

{
    for ($i = 0; $i < 5; $i++)
    {

    }
}
?>

</div>


For each element, you want a span element, and a nicely formatted review property. Be sure to use htmlspecialchars() on it.
<div id="left" class="review">
<?php
if (count($reviews) == 10) 
{
    for ($i = 0; $i < 5; $i++)
    {
?>
    <span>
    <b>&ldquo;<?php echo htmlspecialchars($reviews[$i]->review); ?>&rdquo;</b>
    </span>
<?php

    }
}
?>
</div>


We want the font size to be inversely proportional to the length of the string. Thus, if the review was "Go watch it!", it would be in a significantly larger font than "This movie will bring you through a roller-coaster ride of emotions!".
<div id="left" class="review">
<?php
if (count($reviews) == 10) 
{
    for ($i = 0; $i < 5; $i++)
    {
?>
    <span style="font-size:<?php echo (1.5 - (strlen($reviews[$i]->review) / 10)); ?>em">
    <b>&ldquo;<?php echo htmlspecialchars($reviews[$i]->review); ?>&rdquo;</b>
    </span>
<?php
    }
}
?>
</div>


After that, we have a small tag and the critic property in italics.
<div id="left" class="review">
<?php
if (count($reviews) == 10) 
{
    for ($i = 0; $i < 5; $i++)
    {
?>
    <span style="font-size:<?php echo (1.5 - (strlen($reviews[$i]->review) / 10)); ?>em">
    <b>&ldquo;<?php echo htmlspecialchars($reviews[$i]->review); ?>&rdquo;</b>
    </span>
    <br />
    <small>
    <i><?php echo $reviews[$i]->critic; ?></i>
    </small>
    <br />
    <br />

<?php
    }
}
?>
</div>


We then want there to be between 3 to 5 stars. So we use the HTML symbol 3 times...
<small>
<i><?php echo $reviews[$i]->critic; ?></i>
&nbsp;&#9733;&#9733;&#9733;
</small>


Then use a For loop and the rand() function to potentially put down a maximum of 2 more stars.
<small>
<i><?php echo $reviews[$i]->critic; ?></i>
&nbsp;&#9733;&#9733;&#9733;
<?php 
for ($j = 0; $j <= 1; $j++)
{
    if (rand(1, 2) == 1) echo "&#9733;";
}
?>

</small>


You have to click the "Create your Movie Poster" button to test this.

Repeat for the other side. This time, set the For loop to iterate through elements 5 to 10 of reviews.
<div id="right" class="review">
<?php
if (count($reviews) == 10) 

{
    for ($i = 5; $i < 10; $i++)
    {
?>
    <span style="font-size:<?php echo (2 - (strlen($reviews[$i]->review) / 10)); ?>em"><b>&ldquo;<?php echo htmlspecialchars($reviews[$i]->review); ?>&rdquo;</b>
    </span>
    <br />
    <small>
    <i><?php echo $reviews[$i]->critic; ?></i>
    &nbsp;&#9733;&#9733;&#9733;
    <?php 
    for ($j = 0; $j <= 1; $j++)
    {
        if (rand(1, 2) == 1) echo "&#9733;";
    }
    ?>
    </small>
    <br />
    <br />
<?php
    }
}
?>

</div>


Looks like the other side is done!


We're not quite done yet...

We want to make a dashboard to manipulate customizable variables. The good news is, we've done that already before in The Easter Egg Generator, all the way back in 2016. Can't really reuse any code, but the principles are the same.

Let's define some fieldsets, with legends.
<form id="frmUpload" name="frmUpload" action="" method="POST" enctype="multipart/form-data">
    <label for="flUpload">File</label>
    <input type="file" name="flUpload" id="flUpload">
    <input type="hidden" name="hidUploadSize" id="hidUploadSize" value="50000000">
    <br /><br />
    <fieldset>
        <legend>Movie Title</legend>            

    </fieldset>

    <fieldset>
        <legend>Movie Tagline</legend>

    </fieldset>

    <fieldset>
        <legend>Starring</legend>

    </fieldset>           

    <fieldset>
        <legend>Reviews</legend>

    </fieldset>
    <br /><br />

    <input type="submit" name="btSubmit" id="btSubmit" value="Create your Movie Poster!">
</form>


Taking shape!


We'll want controls that the user can use to change the PHP variables. For numeric values, we'll use a range input. For colors, we'll use color inputs. And for text values, we will just have vanilla text inputs. For the latter, I've included maxlength attributes out of sheer habit. Take a note of name and id attributes - those will be relevant real soon.
<input type="hidden" name="hidUploadSize" id="hidUploadSize" value="50000000">
<br /><br />
<label for="txtSpace_from_top">Space From Top</label>
<input type="range" min="10" max="400" name="txtSpace_from_top" id="txtSpace_from_top" value="" />

<br /><br />
<fieldset>
        <legend>Movie Title</legend>
        <label for="txtMovie_title">Text</label>
        <input name="txtMovie_title" id="txtMovie_title" maxlength="20" value="" />
        <br />
        <label for="txtMovie_title_color">Color</label>
        <input type="color" name="txtMovie_title_color" id="txtMovie_title_color" value="" />
        <br />
        <label for="txtMovie_title_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_title_size" id="txtMovie_title_size" value="" />
            
</fieldset>

<fieldset>
        <legend>Movie Tagline</legend>
        <label for="txtMovie_tagline">Text</label>
        <input name="txtMovie_tagline" id="txtMovie_tagline" maxlength="50" value="" />
        <br />
        <label for="txtMovie_tagline_color">Color</label>
        <input type="color" name="txtMovie_tagline_color" id="txtMovie_tagline_color" value="" />
        <br />
        <label for="txtMovie_tagline_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_tagline_size" id="txtMovie_tagline_size" value="" />

</fieldset>

<fieldset>
        <legend>Starring</legend>
        <label for="txtMovie_starring">Text</label>
        <input name="txtMovie_starring" id="txtMovie_starring" maxlength="100" value="" />
        <br />
        <label for="txtMovie_starring_color">Color</label>
        <input type="color" name="txtMovie_starring_color" id="txtMovie_starring_color" value="" />
        <br />
        <label for="txtMovie_starring_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_starring_size" id="txtMovie_starring_size" value="" />

</fieldset>           

<fieldset>
        <legend>Reviews</legend>
        <label for="txtReviews_color">Color</label>
        <input type="color" name="txtReviews_color" id="txtReviews_color" value="" />
        <br />
        <label for="txtReviews_bgcolor">Background Color</label>
        <input type="color" name="txtReviews_bgcolor" id="txtReviews_bgcolor" value="" />

</fieldset>


And here are the inputs. Just a bit messy, so let's clean stuff up.


In the styles, style labels to have a fixed width. I've also styled font. While we're at it, let's have the submit button also given a fixed width, a bit of spacing at the top, and float it right.
#formContainer
{
        width: 400px;
        padding: 5px;
        margin: 5px;
        float: left;
        outline: 0px solid #DDDDDD;
}

label
{
        display: inline-block;
        font-family: arial;
        font-size: 12px;
        width: 10em;
}


#btSubmit
{
        width: 15em;
        margin-top: 1em;
        float: right;
}

#posterContainer
{
        width: 800px;
        height: 500px;
        margin: 5px;
        float: left;
        text-align: center;
}


And then let's populate the values of all these controls with the PHP variables.
<label for="txtSpace_from_top">Space From Top</label>
<input type="range" min="10" max="400" name="txtSpace_from_top" id="txtSpace_from_top" value="<?php echo $space_from_top; ?>" />
<br /><br />
<fieldset>
        <legend>Movie Title</legend>
        <label for="txtMovie_title">Text</label>
        <input name="txtMovie_title" id="txtMovie_title" maxlength="20" value="<?php echo $movie_title; ?>" />
        <br />
        <label for="txtMovie_title_color">Color</label>
        <input type="color" name="txtMovie_title_color" id="txtMovie_title_color" value="<?php echo $movie_title_color; ?>" />
        <br />
        <label for="txtMovie_title_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_title_size" id="txtMovie_title_size" value="<?php echo $movie_title_size; ?>" />             
</fieldset>

<fieldset>
        <legend>Movie Tagline</legend>
        <label for="txtMovie_tagline">Text</label>
        <input name="txtMovie_tagline" id="txtMovie_tagline" maxlength="50" value="<?php echo $movie_tagline; ?>" />
        <br />
        <label for="txtMovie_tagline_color">Color</label>
        <input type="color" name="txtMovie_tagline_color" id="txtMovie_tagline_color" value="<?php echo $movie_tagline_color; ?>" />
        <br />
        <label for="txtMovie_tagline_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_tagline_size" id="txtMovie_tagline_size" value="<?php echo $movie_tagline_size; ?>" />
</fieldset>

<fieldset>
        <legend>Starring</legend>
        <label for="txtMovie_starring">Text</label>
        <input name="txtMovie_starring" id="txtMovie_starring" maxlength="100" value="<?php echo $movie_starring; ?>" />
        <br />
        <label for="txtMovie_starring_color">Color</label>
        <input type="color" name="txtMovie_starring_color" id="txtMovie_starring_color" value="<?php echo $movie_tagline_color; ?>" />
        <br />
        <label for="txtMovie_starring_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_starring_size" id="txtMovie_starring_size" value="<?php echo $movie_starring_size; ?>" />
</fieldset>           

<fieldset>
        <legend>Reviews</legend>
        <label for="txtReviews_color">Color</label>
        <input type="color" name="txtReviews_color" id="txtReviews_color" value="<?php echo $reviews_color; ?>" />
        <br />
        <label for="txtReviews_bgcolor">Background Color</label>
<input type="color" name="txtReviews_bgcolor" id="txtReviews_bgcolor" value="<?php echo $reviews_bgcolor; ?>" />
</fieldset>


There you go.


Now, here's some more PHP code. This ensures that if you change any of the variables in the form and then submit the form, the changes take effect.
if (isset($_POST["btSubmit"]))
{
    if (basename($_FILES["flUpload"]["name"]) != "")
    {
         $uploadsize = intval($_POST["hidUploadSize"]);
         $filetype = pathinfo($_FILES["flUpload"]["name"],PATHINFO_EXTENSION);
         $filetype = strtolower($filetype);

         if ($_FILES["flUpload"]["size"] > $uploadsize)
         {
             $strmessage = "Error was encountered while uploading file. File cannot exceed " . ($uploadsize/1000) . "kb";
         }
         else
         {
             if (!is_array(getimagesize($_FILES["flUpload"]["tmp_name"])))
             {
                  $strmessage = "File type invalid";
             }
             else
             {
                  $filecode=strtotime("now").rand();
        
                  if (move_uploaded_file($_FILES["flUpload"]["tmp_name"], "uploads/" . $filecode . "." . $filetype))
                  {
                      $strmessage = "File uploaded.";
                  }
                  else
                  {
                      $strmessage = "Error was encountered while uploading file.";
                  }  
             }
         }
    }
    else
    {
         $strmessage="No file selected.";
    }

    $space_from_top = $_POST["txtSpace_from_top"];

    $movie_title = $_POST["txtMovie_title"];
    $movie_title_color = $_POST["txtMovie_title_color"];
    $movie_title_size = $_POST["txtMovie_title_size"];

    $movie_tagline = $_POST["txtMovie_tagline"];
    $movie_tagline_color = $_POST["txtMovie_tagline_color"];
    $movie_tagline_size = $_POST["txtMovie_tagline_size"];

    $movie_starring = $_POST["txtMovie_starring"];
    $movie_starring_color = $_POST["txtMovie_starring_color"];
    $movie_starring_size = $_POST["txtMovie_starring_size"];

    $reviews = [];
    $reviews_color = $_POST["txtReviews_color"];
    $reviews_bgcolor = $_POST["txtReviews_bgcolor"];


    $key = "xxx";
    $org = "org-xxx";
    $url = 'https://api.openai.com/v1/chat/completions';  

    $headers = [
        "Authorization: Bearer " . $key,
        "OpenAI-Organization: " . $org,
        "Content-Type: application/json"
    ];

    $messages = [];
    $obj = [];
    $obj["role"] = "user";
    $obj["content"] = "Give me a JSON object with one property. The property should be named 'FakeReviews', and should be an array of ten objects. Each object should have the property 'review', which is a random fictional complimentary about the movie '" . $movie_title . "'" . ($movie_starring == "" ? "" : " or celebrity '" . $movie_starring . "'") .  " (range between three to ten words) sentence in a string, and the property 'critic' which contains the fictional publication for that quote.";
    $messages[] = $obj;

    $data = [];
    $data["model"] = "gpt-3.5-turbo";
    $data["messages"] = $messages;
    $data["max_tokens"] = 1000;

    $curl = curl_init($url);
    curl_setopt($curl, CURLOPT_POST, 1);
    curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($data));
    curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);

    $result = curl_exec($curl);
    if (curl_errno($curl)) 
    {
         echo 'Error:' . curl_error($curl);
    }

    curl_close($curl);

    $result = json_decode($result);
    $content = $result->choices[0]->message->content;
    $content = json_decode($content);

    $reviews = $content->FakeReviews;
}


See what I mean?


Now, it would be nice to have whatever changes you make, be reflected in "real-time" instead of having to submit the form. Well, image changes have to involve submitting the form, but not the rest! So, in the fields, add the oninput attribute and set it to call the function useVariables().
<label for="txtSpace_from_top">Space From Top</label>
<input type="range" min="10" max="400" name="txtSpace_from_top" id="txtSpace_from_top" value="<?php echo $space_from_top; ?>" oninput="useVariables()" />
<br /><br />
<fieldset>
        <legend>Movie Title</legend>
        <label for="txtMovie_title">Text</label>
        <input name="txtMovie_title" id="txtMovie_title" maxlength="20" value="<?php echo $movie_title; ?>" oninput="useVariables()" />
        <br />
        <label for="txtMovie_title_color">Color</label>
        <input type="color" name="txtMovie_title_color" id="txtMovie_title_color" value="<?php echo $movie_title_color; ?>" oninput="useVariables()" />
        <br />
        <label for="txtMovie_title_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_title_size" id="txtMovie_title_size" value="<?php echo $movie_title_size; ?>" oninput="useVariables()" />             
</fieldset>

<fieldset>
        <legend>Movie Tagline</legend>
        <label for="txtMovie_tagline">Text</label>
        <input name="txtMovie_tagline" id="txtMovie_tagline" maxlength="50" value="<?php echo $movie_tagline; ?>" oninput="useVariables()" />
        <br />
        <label for="txtMovie_tagline_color">Color</label>
        <input type="color" name="txtMovie_tagline_color" id="txtMovie_tagline_color" value="<?php echo $movie_tagline_color; ?>" oninput="useVariables()" />
        <br />
        <label for="txtMovie_tagline_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_tagline_size" id="txtMovie_tagline_size" value="<?php echo $movie_tagline_size; ?>" oninput="useVariables()" />
</fieldset>

<fieldset>
        <legend>Starring</legend>
        <label for="txtMovie_starring">Text</label>
        <input name="txtMovie_starring" id="txtMovie_starring" maxlength="100" value="<?php echo $movie_starring; ?>" oninput="useVariables()" />
        <br />
        <label for="txtMovie_starring_color">Color</label>
        <input type="color" name="txtMovie_starring_color" id="txtMovie_starring_color" value="<?php echo $movie_tagline_color; ?>" oninput="useVariables()" />
        <br />
        <label for="txtMovie_starring_size">Size</label>
        <input type="range" min="10" max="50" name="txtMovie_starring_size" id="txtMovie_starring_size" value="<?php echo $movie_starring_size; ?>" oninput="useVariables()" />
</fieldset>           

<fieldset>
        <legend>Reviews</legend>
        <label for="txtReviews_color">Color</label>
        <input type="color" name="txtReviews_color" id="txtReviews_color" value="<?php echo $reviews_color; ?>" oninput="useVariables()" />
        <br />
        <label for="txtReviews_bgcolor">Background Color</label>
<input type="color" name="txtReviews_bgcolor" id="txtReviews_bgcolor" value="<?php echo $reviews_bgcolor; ?>" oninput="useVariables()" />
</fieldset>


And then we create the useVariables() function in the JavaScript.
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script>
function useVariables()
{

}

</script>


We will be making changes to these elements...
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script>
function useVariables()
{
    $("#title_and_tagline")

    $(".review")

    $("#movie_title")

    $("#movie_tagline")

    $("#movie_starring")

}
</script>


All of these elements will have changes made to the style attribute. As you can see, the changes are mostly about font size and color. In the case of title_and_tagline, the margin-top property is changed. In the case of divs styled using the review CSS class, it's color and background color that's changed.
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script>
function useVariables()
{
    $("#title_and_tagline")
    .attr("style", "margin-top:" + $("#txtSpace_from_top").val() + "px");

    $(".review")
    .attr("style", "color:" + $("#txtReviews_color").val() + ";background-color: " +       $("#txtReviews_bgcolor").val());

    $("#movie_title")
    .attr("style", "color:" + $("#txtMovie_title_color").val() + ";font-size: " + $("#txtMovie_title_size").val() + "px");

    $("#movie_tagline")
    .attr("style", "color:" + $("#txtMovie_tagline_color").val() + ";font-size: " + $("#txtMovie_tagline_size").val() + "px");

    $("#movie_starring")
    .attr("style", "color:" + $("#txtMovie_starring_color").val() + ";font-size: " + $("#txtMovie_starring_size").val() + "px");
}
</script>


For movie_title, movie_tagline and movie_starring, we use the html() method to change the text. Note that for movie_starring, the same rules apply as they did with the PHP script - if the contents of the txtMovie_starring text box is an empty string, this placeholder will be empty as well; otherwise, prepend with "starring".
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script>
function useVariables()
{
    $("#title_and_tagline")
    .attr("style", "margin-top:" + $("#txtSpace_from_top").val() + "px");

    $(".review")
    .attr("style", "color:" + $("#txtReviews_color").val() + ";background-color: " + $("#txtReviews_bgcolor").val());

    $("#movie_title")
    .attr("style", "color:" + $("#txtMovie_title_color").val() + ";font-size: " + $("#txtMovie_title_size").val() + "px")
    .html($("#txtMovie_title").val());

    $("#movie_tagline")
    .attr("style", "color:" + $("#txtMovie_tagline_color").val() + ";font-size: " + $("#txtMovie_tagline_size").val() + "px")
    .html($("#txtMovie_tagline").val());

    $("#movie_starring")
    .attr("style", "color:" + $("#txtMovie_starring_color").val() + ";font-size: " + $("#txtMovie_starring_size").val() + "px")
    .html(($("#txtMovie_starring").val() == "" ? "" : "starring " + $("#txtMovie_starring").val()));
}
</script>


You can't really tell from here, but the screen is reflecting the changes I'm making right now.


Before I forget...

You can even print out your poster. The media queries I specified in the CSS ensure that the dashboard is not visible in print view.

Now, wasn't that fun?!

Generative Artificial Intelligence really injects a little bit of random craziness into tiny projects like these. Love it!

Getting the picture? Hur hur,
T___T

Wednesday, 19 March 2025

Film Review: Black Mirror Series Five (Part 3/3)

The final episode, titled Rachel, Jack and Ashley Too, stars Miley Cyrus!


Yes, you got that right. Black Mirror is full of surprises, eh? What's perhaps even more surprising is that this episode isn't dark and gloomy like most of the other Black Mirror episodes are.

The Premise

Ashley O is a huge star who gets put in a coma. Her consciousness is placed inside robotic dolls electronically. One of these dolls belongs to a girl named Rachel. An adventure ensues as Rachel and her older sister Jack, follow a trail that leads to Ashley's rescue.

The Characters

Miley Cyrus has the main role of Ashley Ortiz. Cyrus is actually in her element here. The song and dance sequences, while sometimes goofy, are on point. Cyrus isn't afraid to get ugly too, in the sequences that see her wake up from her coma.

Angourie Rice
is Rachel Goggins, the lonely kid who's still distanced from her older sister and harbors dreams of being a star like Ashley O. I didn't really like watching her, not sure why. Maybe it was the cringey dancing the script made her do.

Madison Davenport as Jack Goggins, Rachel's older sister. She's mean, snarky and ill-tempered, with a soft center. Her facial expressions are to die for - those looks of WTF did I just hear she throws at Rachel and Ashley Too? Such delicious disdain. Davenport outdid herself here.

Susan Pourfar as Ashley's aunt Catherine Ortiz. At the beginning, she comes across as cheerful and friendly, if a little stressed out, which makes it even more chilling when her true colors as a scheming bitch emerge.

Marc Menchaca is Rachel and Jack's dad, Kevin. He's a basement geek who tries to be a good dad after the girls' mother passed away two years ago. Well-intentioned, if a little oblivious.

Nicholas Pauling as Dr Munk, the goto guy for drugs. He's ostensibly a doctor, but this guy just feels comes across more like some kind of sleazy thug, especially later on as he's choking Jack.

James III as Jackson Hanabera, technical director, who comes up with the scheme to harvest Ashley's dreams while she's in a coma. Weird, the guy just didn't give off that kind of psycho vibe.

Daniel Stewart Sherman as Bear, security guy. Huge and menacing. I loved watching his quizzical expressions when Jack feeds him bullshit.

Jerah Milligan puts in a brief appearance as BusyG, a TV host.

The Mood

It's upbeat and bright, kind of like a Disney movie. Even as the story progresses, it doesn't ever get -that- dark. This episode has more of an adventure movie vibe going for it, and actually ends on a positive note (bad guy gets nabbed, heroine gets rescued, etc).

What I liked

The commercial Ashley's music and lyrics are corny and shallow and major cringe. It's a good contrast to the work that she actually wants to produce.

When the limiter around Ashley Too's "brain" is removed, the ensuing expletive-filled rant it goes on, is marvellous. Later on, the sarcasm-filled exchanges between Ashley Too and Jack, as well.


The entire subplot revolving around Kevin's mousetrapping technology, and how it ultimately helps in freeing Ashley Too.


And of course, that damn car!


I think the scaled-up holographic concert was pretty neat, as well.


This shot of Ashley Too at the end, turned into a punk rocker, was amusing.

What I didn't

The scenes of Rachel dancing with the encouragement of Ashley Too, are a little draggy and pretty cringey. But I suppose that was the whole point.

Watching the 15-year old character Rachel dancing to lyrics like "Oh honey kiss me up against the wall" and "I can't take it so don't you fake it" gave me the ick.

This is a minor one, but the episode title's a little lazy. It's literally just the names of the two sisters, and the doll. Somewhere along the way, someone stopped giving a fuck about catchy episode titles!

Conclusion

The series ends on a high with this one. Although, it has to be said, expectations were low after the last one. But even judged on its own merits, this episode stands strong. Even though the villains were almost cartoony, I couldn't help but enjoy myself. The plot wasn't all that original - pretty sure I've seen some variation of that story somewhere - but boy, was it an engaging hour or so.

My Rating

8 / 10

Final Thoughts on Black Mirror Series Five

The weak link in Black Mirror Series Five was undoubtedly Smithereens. And while Striking Vipers was more thematically Black Mirror, its glaring flaws hinder it significantly. Still, it did serve as a decent opening act.

Rachel, Jack and Ashley Too, conversely, has less of the feel of Black Mirror, but takes the prize due to an almost flawless execution. Or it could merely seem flawless coming right after the severely flawed Smithereens.

All in all, Black Mirror Series Five is a worthy addition to the series, though far from the best offering.

That's all for now... "go to sleep"!
T___T

Monday, 17 March 2025

Film Review: Black Mirror Series Five (Part 2/3)

This next story, Smithereens, is set in the UK - London, to be exact. Smithereen is the name of a Social Media company owned by Billy Bauer.


As the exclamation mark icon would suggest, Smithereen produces some kind of Twitter-like Social Media product.

The Premise

Rideshare driver Chris Gilhaney takes a man hostage in an effort to speak with the founder of Smithereen, Billy Bauer. That's pretty much it; this is, in broad strokes, what takes place. The rest of it is in-character drama and backstory.

The tragedy itself is a commentary on the addictve nature of Social Media.

The Characters

Andrew Scott as Christopher Gilhaney. At first we think he's an unkempt cab driver with an unusually intense stare. We later find out that he's extremely stressed and has a hair-trigger temper, but ultimately a decent chap at heart. He's also got a fair amount of tragedy in his life which leads him to do this.

Damson Idris plays Smithereen intern Jaden Tommins as a scared kid taken hostage. He does this wonderfully too, and really elicits sympathy here. At the end, the character exhibits loads of empathy with his kidnapper Chris and even tries to stop him from killing himself.

Topher Grace as Smithereen CEO Billy Bauer. Grace appears like I've never seen him before - as a new-agey man-bunned hipster out on a "silent retreat". Pretensions aside, he's entirely sympathetic as a tech geek who begins building his product with benign intentions but ends up creating a monster.

Calum Callaghan as male cop, Damien Cullen. Slightly goofy, but the portrayal of a stodgy cop seemed entirely authentic.

Ambreen Razia as female cop, Najma Haque. Portrayed as the more on-the-ball and alert partner.

Amanda Drew is Hayley Blackwood. She is a grieving mother whose daughter took her own life, and has a one-night stand with Chris. I liked her in this role, but I'm not sure if her inclusion was necessary at all.

Caitlin Innes Edwards as Hannah Kent from Smithereen HR. Nothing much to see here, really. Honestly didn't do much. I get that the plot warranted her inclusion; she just wasn't that interesting to watch.

Maggie Bain as Maryam, who appears to be in charge of Smithereen in London. Another superfluous role.

Ruibo Qian as Penelope Wu, COO. She acts as the counterfoil to Billy Bauer's empathetic nature, mostly cold and calculating and concerned with the less human side of things.

Monica Dolan as Chief Superintendent Linda Grace, who tries to take charge but does a really bad job of it.

Daniel Ings as David Gilkes, negotiator. Another role that felt superfluous.

Quincy Dunn-Baker as Don from legal and Mirirai Sithole as Shonelle from Analytics. Fucking superfluous. FFS, they should have just combined the two roles with no loss to the story. Were they trying to set some kind of record here?

Jorge Cordova as Ernesto Cruz from FBI. Wow, this is Superfluous with a capital S. What did the character contribute here, really? Neat pornstache, though.

Adam McNamara as Harris the sniper. Alice Bailey-Johnson as the other sniper. Supremely incompetent at their jobs, considering how much they missed and how little distance it was.

Crystal Clarke does a decent job playing Tipi. Upbeat and earnest. Clarke made the most out of the limited scope of the role as the staff member sent to alert Billy Bauer.

The Mood

It looks like a typical dreary day in London but soon (after an unneccessary sex scene) the action picks up and becomes some kind of hostage thriller. Doubles as tragedy when one realizes what led Chris to take such extreme measures.

What I liked

I really enjoyed the fact that this episode was all about existing tech. Smithereen was like a real-world Twitter (now X), Persona the equivalent of Facebook or Instagram, and Hitcher like Uber. It made it all the more relatable, especially since the bleakness was a dark (or black, hur hur) mirror to the all-too-real ills of Social Media today.


The transcriber being a prude as it recorded Chris delivering a cluster F-bomb, substituting all instances of "fuck" with "duck". I probably shouldn't find it that amusing, but I did.

What I didn't

The cast just felt unnecessarily huge. There was a number of speaking roles in the episode that felt like they were there just to make up the numbers. The characterization for these roles were just flat.

The plot was bloated beyond belief. A lot of it felt like meaningless waiting. Did it ratchet up the tension? Maybe a little. But not enough to warrant sitting through a whole lot of stuff that could have been cut out.

For example, Hayley's subplot. Near the end, we see that Billy Bauer has called in to Persona to request that they give Hayley her daughter's password. But anyone who works in tech (or even has the faintest idea how this shit works) knows that passwords have not been stored in cleartext since the last decade. Basic web security. There is no way for anyone to go into the database and search for a user and simply retrieve a non-hashed password. The best they could have done was reset the password or send Hayley a link to do so.



Now, normally I could have overlooked this... if it weren't for the fact that this password was a plot point (the password apparently matched the number of the boat in a photo that Hayley and her daughter took together), which resulted in the password thing being a gigantic fucking plot hole.

Cut out this entirely, and we would have eliminated Hayley and her one night stand with Chris -and- the plot hole. Though it would have been a pity because I actually liked Amanda Drew as Hayley.

All in all, this amateurish error ruined the episode for me. You want to make a show about tech, get a writer who actually understands tech. Jesus!

Conclusion

A poor, poor episode, especially since it could have been halfway decent once a lot of fat was trimmed. I liked the premise and the Aesop of the episode, but the huge plot hole and execution flaws made this one just about unsalvageable.

My Rating

4.5 / 10

Next

Rachel, Jack and Ashley Too

Saturday, 15 March 2025

Film Review: Black Mirror Series Five (Part 1/3)

I'm back with more Black Mirror! For those who are new to this, Black Mirror is an anthological series with standalone episodes around the theme of technology gone wrong. Sometimes it can be funny, but often it's bleak AF.


We're now in Series Five of Black Mirror, and there are three episodes to go through. Prepare to be shocked, disgusted and entertained in varying degrees.

Warning - spoilers ahead, important plot points discussed!

I try not to hold back where spoilers are concerned, and at this point, Black Mirror isn't exactly new. There's really no point in being coy about spoilers.

Also... if you don't approve of video games, stop watching. If you don't like bad language, stop watching. And if seeing two grown men kiss makes your brain implode, definitely fuck off and stop watching.

The Premise

Striking Vipers is the first episode, and it is centered around two old friends playing a fighting video game, but with VR Black Mirror style.


As both of them become obsessed with their time within the game, however, this results in their real lives suffering for it.

Generally, this episode warns against the dangers of getting too absorbed in a game. The tech used appears to be the same one used in USS Callister, and bears the same Aesop.

The Characters

Anthony Mackie as Danny Parker. He's the serious responsible type who's now married with a kid, but sex has become a bit of a chore. Mackie gives us a tremendous portrayal of a man who's not a cheater by nature, but gets caught up in something that doesn't feel like cheating until it's too late. I've been watching Mackie on-screen as Sam Wilson in the Marvel Cinematic Universe, and this isn't too far from what he portrays there.

Yahya Abdul-Mateen II as incorrigible manchild Karl Houghton with Peter Pan syndrome. He's finding it increasingly harder to relate to the girls he dates, because they're so much younger, even though the sex is presumably great. And the actor delivers with such earnestness lines like the one below.
"I fucked a polar bear and I still couldn't get you out of my mind."

Only in a series like Black Mirror would that quote make sense.

Anyway, I've seen the actor in superhero flicks like Aquaman and Aquaman and the Lost Kingdom. He gets a lot more to do here.

Nicole Beharie as Theo Parker. She has a role as Danny's wife and Tyler's mother, and while her portrayal is a little generic, Beharie does bring in some sassy charm into it.

Pom Klementieff is a breath of fresh air as Roxette. I loved seeing her in Mission Impossible: Dead Recknoning Part One and the Marvel Cinematic Universe, and she brings the same zany offbeat quality to the role, almost perfectly mirroring Karl offline.

Ludi Lin as Lance. Last saw him in a similar role as Liu Kang in Mortal Kombat. This time it felt a little flat, if I'm being honest. Nice abs, but I can't seem to buy him as Danny Parker's online alter-ego.

Fola Evans-Akingbola portrays Karl's girlfriend Mariella as this slightly bimbotic girl who's obsessed with her phone. Serves to show us that Karl gets great sex physically but he's missing that extra emotional connection. Someone who gets him.

August Muschett is Danny and Theo's son Tyler Parker. He's a cute little tyke, perfectly normal. And only serves to underscore how much Danny has going for him.

The Mood

Scenes of wedded bliss and suburban serenity are interspersed by nighttime landscapes within the game.


Unlike the usual Black Mirror fare, none of this is particularly unsettling, but there is a fair bit of drama and tension as we get into it.

What I liked

The banter between Danny and Karl, at Danny's birthday barbecue, is delightful. It really serves to illustrate what a vibe these two buddies had before things got weird.

I normally prefer Black Mirror episodes to be dark and creepy (maybe even violent) but this episode focused on the friendship between two men and it was glorious even though relationship drama isn't really my thing.

The name "Striking Vipers" is really such a sly nod to gay sex. Not that what's between Danny and Karl is 100% gay sex. When it happens, they're choosing to use avatars Roxette (a female) and Lance (a male).


The locations within the game are gorgeous. We can really see the effort they put into it. Also, the fighting moves. So outlandish. Love it! Especially that pancake slam by Roxette.


Could be just me, but I thoroughly enjoyed this over-the-top visual representation of Tetris.

What I didn't

I find it extremely hard to believe that the anatomical accuracy in the game would be so high as to facilitate sexual intercourse. And since this is a major plot point without which nothing else makes sense, it's pretty difficult to ignore.

The ending's also a little off, I think. So once a year Theo and Danny both get a license for infidelity? Looking at how obsessed Karl was, is once a year even enough? What does he do in between, fuck more polar bears?

Conclusion

This episode had so much promise despite the nagging little plot holes. The premise was decent, that's for sure. Thematically, it fits right in with the mythos. It was a fun watch.

My Rating

7 / 10

Next

Smithereens