<|start_filename|>apps/comics/templates/comics/500.html<|end_filename|>
{% extends 'comics/base.html' %}
{% load static %}

{% block page_title %} | Server Error {% endblock %}

{% block content %}
<img class="comic-image" src="{% if comic and comic.error_500_image %}{{ comic.error_500_image.url }}{% else %}{% static 'comics/default-500.png' %}{% endif %}"/>
{% endblock %}

<|start_filename|>apps/comics/templates/comics/archive/base.html<|end_filename|>
{% extends 'comics/base.html' %}
{% load static %}

{% block content %}
<div class="content-frame">
    <p>
        Back to: <a class="tag" {% if comic.favicon_image %}style="background-image: url({{ comic.favicon_image.url }})"{% endif %} href="{% url 'index' %}">Comic</a>
        {% for crumb in breadcrumbs %}
            &gt;
            <a class="tag" href="{{ crumb.url }}" {% if crumb.icon %}style="background-image: url({{ crumb.icon }})"{% endif %}>
                {{ crumb.title }}
            </a>
        {% endfor %}
    </p>
    <h1 class="center-text">{% block title %}{% endblock %}</h1>
    {% block archive_content %}{% endblock %}
</div>
{% endblock %}

<|start_filename|>apps/comics/templates/comics/404.html<|end_filename|>
{% extends 'comics/base.html' %}
{% load static %}

{% block page_title %} | Not Found{% endblock %}

{% block content %}
<img class="comic-image" src="{% if comic and comic.error_404_image %}{{ comic.error_404_image.url }}{% else %}{% static 'comics/default-404.png' %}{% endif %}"/>
{% endblock %}

<|start_filename|>apps/comics/templates/comics/index.html<|end_filename|>
{% extends 'comics/base.html' %}
{% block page_title %}Comics Index{% endblock %}
{% block content %}
    <div>
        <br/>
        <h1>Comics Platform</h1>
        <br/>
    </div>
    {% for comic in comics %}
    <div class="index-section">
        <h2>{{ comic.title }}</h2>
        <p>by {{ comic.author }}</p>
        <p>{{ comic.genre }}</p>
        <p>{{ comic.pages.active.count }} pages</p>

        <a href="{{request.scheme}}://{{ comic.domain }}">
            <img style="width: 100%" src="{% if comic.header_image %}{{ comic.header_image.url }}{% endif %}" alt="{{ comic.title }}">
        </a>
        <p>{{ comic.description }}</p>

    </div>
    {% endfor %}

{% endblock %}

<|start_filename|>apps/comics/templates/comics/archive/index.html<|end_filename|>
{% extends 'comics/archive/base.html' %}

{% block title %}Archive{% endblock %}
{% block archive_content %}
    <div class="archive-tile-container">
        {% for type in comic.tag_types.all %}
        {% with count=type.tags.count %}
        <a class="archive-tile" href="{{ type.get_absolute_url }}" style="background-image: url({{ type.best_icon }});">
            <strong>{{ type.title }}</strong>
            <small>{{ count }} Tag{{ count|pluralize:",s" }}</small>
        </a>
        {% endwith %}
        {% endfor %}
        <a class="archive-tile" href="{% url 'archive-pages' %}" {% if comic.archive_icon %}style="background-image: url({{ comic.archive_icon.url }});" {% endif %}>
            <strong>Pages</strong>
            <small>{{ comic.pages.count }} Pages</small>
        </a>
    </div>
{% endblock %}

<|start_filename|>apps/comics/templates/comics/reader.html<|end_filename|>
{% extends 'comics/base.html' %}
{% load snippet_extras %}

{% block page_title %} | {{ page.title }}{% endblock %}
{% block page_description %}{{ page.transcript_txt|truncatechars:160 }}{% endblock %}

{% block social_media_tags %}
	<meta property="og:title" content="{{ page.title }}"/>
	<meta property="og:type" content="article"/>
	<meta property="og:url" content="{{ request.scheme }}://{{ request.get_host }}{{ page.get_absolute_url }}"/>
	<meta property="og:image" content="{{ request.scheme }}://{{ request.get_host }}{{ page.image.url }}"/>
	<meta property="og:description" content="{{ page.transcript_txt }}"/>
	<meta property="og:site_name" content="{{ comic.title }} | A webcomic by {{ comic.author }}"/>
{% endblock %}

{% block content %}
	<script>
		{% include 'comics/js/reader.js' %}
	</script>

	<!-- JSON-ld schema -->
	{% if comic.header_image %}
		<script type="application/ld+json">
		{
			"@context": "https://schema.org",
			"@type": "CreativeWork",
			"name": "{{ comic.title }}",
			"image": "{{ request.scheme }}://{{ request.get_host }}{{ comic.header_image.url }}",
			"url": "{{ request.scheme }}://{{ request.get_host }}",
			"genre": "{{ comic.genre }}",
			"accessMode": ["textual", "visual"],
			"sameAs": "{{ request.scheme }}://{{ request.get_host }}",
			"hasPart": {
				"@type": "ComicStory",
				"name": "{{ page.title }}",
				"position": "{{ page.ordering }}",
				"datePublished": "{{ page.posted_at|date:'Y-m-d' }}",
				"image": "{{ request.scheme }}://{{ request.get_host }}{{ page.image.url }}",
				"text": "{{ page.transcript_txt }}"
			},
			"author": {
				"@type": "Person",
				"name": "{{ comic.author }}"
			}
		}
		</script>
	{% endif %}

	<div id="reader-panel" class="panel">
		<div class="navigation-wrapper">
			<a href="{% url 'reader' nav.first.slug %}" class="navigation-button navigation-first"
				 onclick="return COMICS.firstButtonPressed();"></a>
			<a href="{% if nav.previous %}{% url 'reader' nav.previous.slug %}{% endif %}"
				 class="navigation-button navigation-previous" onclick="return COMICS.previousButtonPressed();"></a>
			<a href="{% url 'random-reader-redirect' %}" class="navigation-button navigation-random"
				 onclick="COMICS.randomButtonPressed(); return false;">RANDOM</a>
			<a href="{% if nav.next %}{% url 'reader' nav.next.slug %}{% endif %}" class="navigation-button navigation-next"
				 onclick="return COMICS.nextButtonPressed();"></a>
			<a href="{% url 'reader' nav.last.slug %}" class="navigation-button navigation-last"
				 onclick="return COMICS.lastButtonPressed();"></a>
		</div>

		<div class="page-image-wrapper">
			<img id="comic-image" class="comic-image" src="{{ page.image.url }}" title="{{ page.alt_text }}"
					 onload="COMICS.imageLoaded();"/>
			<div id="comic-image-spinner"></div>
		</div>

		<div class="navigation-wrapper">
			<a href="{% url 'reader' nav.first.slug %}" class="navigation-button navigation-first"
				 onclick="return COMICS.firstButtonPressed();"></a>
			<a href="{% if nav.previous %}{% url 'reader' nav.previous.slug %}{% endif %}"
				 class="navigation-button navigation-previous" onclick="return COMICS.previousButtonPressed();"></a>
			<a href="{% url 'random-reader-redirect' %}" class="navigation-button navigation-random"
				 onclick="COMICS.randomButtonPressed(); return false;">RANDOM</a>
			<a href="{% if nav.next %}{% url 'reader' nav.next.slug %}{% endif %}" class="navigation-button navigation-next"
				 onclick="return COMICS.nextButtonPressed();"></a>
			<a href="{% url 'reader' nav.last.slug %}" class="navigation-button navigation-last"
				 onclick="return COMICS.lastButtonPressed();"></a>
		</div>

		<div class="ad-banner-content">
			{% snippets_ad_content %}
		</div>
	</div>

	<div class="panel">
		<div class="tab-group">
			<div class="tab active" data-target="info-frame">Info</div>
			<div class="tab" data-target="comments-frame">Comments</div>
			<div class="tab" data-target="quests-frame">Quests</div>
		</div>
		<div class="content-frame">
			<div class="tab-content-area" id="info-frame">
				<h1 id="comic-title">{{ page.title }}</h1>
				<div id="comic-post-date">{{ page.posted_at|date }}</div>

				<p id="staff-text">Since you're staff, you can <a id="staff-link" href="">EDIT</a> this comic!</p>

				<div id="comic-tags">
					<!-- If you edit this, you also need to update the templates in the reader.js -->
					{% regroup page.tags.all by type as tag_groups %}
					{% for group in tag_groups %}
						<p>{{ group.grouper.title }}:
							{% for tag in group.list %}
								<a class="tag" style="background-image: url({{ tag.icon_url }});"
									 href="{% url 'archive-tag' tag.type.title tag.title %}">{{ tag.title }}</a>
							{% endfor %}
						</p>
					{% endfor %}
				</div>

				<div id="comic-post">{{ page.post_html | safe }}</div>
				<div class="comic-transcript-content">
					<h2>Transcript</h2>
					<div id="comic-transcript">{{ page.transcript_html | safe }}</div>
					<h2>Secret Text</h2>
					<div id="comic-alt-text">{{ page.alt_text }}</div>
				</div>
			</div>
			<div class="tab-content-area" id="comments-frame">
				<h2>Comments</h2>
				<div id="discourse-comments"></div>
			</div>
			<div class="tab-content-area" id="quests-frame">
				<h2>Quests</h2>
				<p>Help {{ comic.title }} grow by completing these quests!</p>

				<h2>Share This Page</h2>
				<p>The easiest, completely free way to support the comic.</p>
				<div id="social-share-container" class="archive-tile-container"></div>

				<h2>Join us on Social Media</h2>
				<p>Never miss another page. Meet the fans. Discuss theories.</p>
				<div id="social-follow-container" class="archive-tile-container"></div>

				<h2>Support the Author</h2>
				<p>Got a little extra cash? Make a big impact using these platforms.</p>
				<div id="social-money-container" class="archive-tile-container"></div>
			</div>

			<div class="ad-banner-content">
				{% snippets_ad_info %}
			</div>
		</div>

		{% if ad %}
			<div class="invitation">
				<a target="_blank" rel="noopener" href="{{ ad.url }}">
					<img alt="Invitation Banner" src="{{ ad.image.url }}"/>
				</a>
			</div>
		{% endif %}
	</div>

{% endblock %}


<|start_filename|>apps/comics/templates/comics/archive/tag.html<|end_filename|>
{% extends 'comics/archive/base.html' %}

{% block social_media_tags %}
    <meta property="og:title" content="{{ tag.title }}" />
    <meta property="og:type" content="article" />
    <meta property="og:url" content="{{ request.scheme }}://{{request.get_host}}{{ tag.get_absolute_url }}" />
    {% if tag.icon_url %}<meta property="og:image" content="{{ request.scheme }}://{{request.get_host}}{{ tag.icon_url }}" />{% endif %}
    <meta property="og:description" content="{{ tag.post }}" />
    <meta property="og:site_name" content="{{ comic.title }} | A webcomic by {{ comic.author }}" />
{% endblock %}

{% block archive_content %}
    <div class="tag-detail-header">
        <img {% if tag.icon_url %}src="{{ tag.icon_url }}"{% endif %}/>
        <div>
            <h1>{{ tag.title }}</h1>
        </div>
    </div>

    <div id="tag-post">{{ tag.post_html | safe }}</div>

    {% with count=tag.pages.active.count %}
    <h2>{{ tag.title }} appears on {{ count }} Page{{ count|pluralize:",s" }}:</h2>
    {% endwith %}
    <div class="archive-tile-container">
        {% for page in tag.pages.active %}
            <a data-title="{{ page.title }}" class="archive-tile archive-tile-short" href="{{ page.get_absolute_url }}" style="background-image: url({{page.thumb_url}});">
                <strong>{{ page.title }}</strong>
                <small>{{ page.posted_at | date:"d M Y" }}</small>
            </a>
        {% endfor %}
    </div>
{% endblock %}

<|start_filename|>apps/comics/templates/comics/base.html<|end_filename|>
<!DOCTYPE html>
{% load static %}
{% load snippet_extras %}
<html lang="en">
<head>
	{% snippets_start_of_head %}

	<!-- Favicon -->
	<link rel="shortcut icon" type="image/png" href="{{ request.scheme }}://{{request.get_host}}{% if comic.favicon_image %}{{ comic.favicon_image.url }}{% else %}{% static 'comics/default-favicon/192.png' %}{% endif %}"/>

	<meta charset="utf-8">
	<meta name="viewport" content="width=device-width, initial-scale=1.0" />
	<meta name="keywords" content="webcomic,comic{% if comic.genre %},{{ comic.genre }}{% endif %}">
	<title>{% if comic %}{{ comic.title }}{% else %}Comics Platform{% endif %}{% block page_title %} | A webcomic by {{ comic.author }}{% endblock %}</title>
	<meta name="description" content="{% block page_description %}{{ comic.description }}{% endblock %}" />

	<style>
		{% include 'comics/reader.css' %}
	</style>
	<script>
		{% include 'comics/quests.js' %}
	</script>

{% if comic %}
	<style>
	{% if comic.font %}
	@font-face {
	  font-family: 'ComicFont';
	  src: url({{ comic.font.url }});
	}
	{% endif %}

	:root {
		{% for style in comic.style_configurations.all %}
			--{{ style.property }}: {{ style.value }};
		{% endfor %}
	}
	</style>
{% endif %}

{% block social_media_tags %}{% endblock %}
</head>
<body data-discourse-url="{{ comic.discourse_url }}">

<div class="content-background">
	{% if comic.header_image %}
	<header>
		<div>
			<a href="{% url 'reader-redirect' %}"><img alt="Logo" class="header-image" src="{{ comic.header_image.url }}"/></a>
		</div>
		<div class="header-link-container">
			{% for link in comic.header_links.active %}
				<a class="header-link" href="{{ link.url }}">{{ link.text }}</a>
			{% endfor %}
		</div>
		<div class="ad-mobile-leaderboard">
			{% snippets_ad_header %}
		</div>
	</header>
	{% endif %}
	<div class="content">
		{% block content %}{% endblock %}
	</div>

	<footer>
		<div class="footer-subsection">
			{% if comic %}
			<p>{{ comic.title }} © 2018-{% now 'Y' %} {{ comic.author }}</p>
			{% endif %}
			<p>Comics Platform © 2018-{% now 'Y' %} <NAME>. <a href="https://github.com/pennomi/comics">Fork us on GitHub!</a></p>
		</div>
	</footer>
</div>


{% snippets_end_of_body %}
</body>
</html>


<|start_filename|>apps/comics/templates/comics/quests.js<|end_filename|>
"use strict";


const QUESTS = function () {
    function createShareLink(template) {
        let uri = encodeURIComponent(window.location.href);
        let message = encodeURIComponent("Visit Swords!");
        return template.replace("{url}", uri).replace("{message}", message);
    }
    function makeTile(url="", image="", title="", cta="", share=false) {
        return `
            <a class="archive-tile" ${ share ? `onclick="QUESTS.share('${ url }')"` : `href="${ url }"` }
               rel="noopener"
               target="blank" style="background-image: url('${ image }')">
              <strong>${ title }</strong>
              <small>${ cta }</small>
            </a>
        `;
    }

    const initializePage = async function () {
        // Fetch the data
        const response = await fetch("/comic/data/");
        const data = await response.json()
        let html;

        // Set up the social share links
        html = "";
        for (let link of data.socialLinks) {
            if (!link.shareUrlTemplate) { continue; }
            html += makeTile(createShareLink(link.shareUrlTemplate), link.image, link.title, link.shareCta, true);
        }
        // TODO: If the device supports navigator.share, make a special tile
        const shareContainer = document.getElementById("social-share-container");
        shareContainer.innerHTML = html;

        // Set up the social follow links
        html = "";
        for (let link of data.socialLinks) {
            if (link.followUrl === undefined && link.visitUrl === undefined) { continue; }
            if (link.requiresMoney) { continue; }
            html += makeTile(link.followUrl || link.visitUrl, link.image, link.title, link.followCta);
        }
        const followContainer = document.getElementById("social-follow-container");
        followContainer.innerHTML = html;

        // Set up the contribution follow links
        html = "";
        for (let link of data.socialLinks) {
            if (link.followUrl === undefined && link.visitUrl === undefined) { continue; }
            if (!link.requiresMoney) { continue; }
            html += makeTile(link.followUrl || link.visitUrl, link.image, link.title, link.followCta);
        }
        const moneyContainer = document.getElementById("social-money-container");
        moneyContainer.innerHTML = html;

    };

    const share = function (url) {
        // TODO: Log this in GA
        const options = 'toolbar=0,status=0,resizable=1,width=626,height=436';
        window.open(url, 'sharer', options);
        return true;
    };

    const navigatorShare = function (url, title, message) {
        if (!navigator.share) {
            return false;
        }
        navigator.share({
            url: url,
            text: text,
            title: title,
        });
    };

    // Run the initialization and then publish any variables that need to be public.
    return {
        initializePage: initializePage,
        share: share,
        navigatorShare: navigatorShare,
    };
}();

document.addEventListener("DOMContentLoaded", function(event) {
    QUESTS.initializePage();
});


<|start_filename|>apps/comics/templates/comics/reader.css<|end_filename|>
{% load static %}

:root {
    /* Configurable settings */

    /* Base Colors */
    --overflow-background: #ECEFF1;
    --content-background: #FFFFFF;
    --primary-text-color: #212121;
    --footer-background-color: #B0BEC5;

    /* Tag Colors */
    --tag-background-color: #B0BEC5;
    --tag-text-color: #212121;

    /* Tab Colors */
    --tab-inactive-color: #B0BEC5;
    --tab-active-color: #EFEFEF;
    --tab-text-color: #212121;

    /* Debug Areas */
    --ad-debug-color: #ffffff00;

    /* Images */
    --spinner-image: url({% static "comics/default-spinner.png" %});
    --navigation-spritesheet: url({% static "comics/default-sprites.png" %});
    --post-border-image: url({% static "comics/default-container.png" %});

    /* Unconfigurable settings */
    --tag-error-color: #B00020;
    --default-tag-icon: url({% static "comics/default-tag-icon.png" %});

    /* Padding standards */
    --paragraph-padding: 8px;
    --tile-size: 105px;  /* At current settings, 105px is good for both iPhone size and desktop size */

    /* Page sizing */
    --comic-max-width: 728px;  /* 728px because that fits the widest ad size */
    --comic-min-width: 320px;  /* 728px because that fits the widest ad size */
    --overflow-extra-width: 82px;  /* Just a comfortable padding for wider screens. */
    --spinner-size: 120px;
    --navigation-height: 64px;
    /* Most common ad sizes globally:
    300x250 (Medium Rectangle) - 40%
    728x90 (Leaderboard) - 25%
    320x50 (Mobile Leaderboard) - 12%
    160x600 (Wide Skyscraper) - 12%
    300x600 (Half Page) - 5%
    Everything else together represents only 6% of ads, and we ignore them.
    */

    /* Markdown Styling */
    --blockquote-border-color: #00000044;
    --blockquote-background-color: #FFFFFF44;
}


/* RESET CSS AND GLOBAL STYLING */
html, body, div, span, applet, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, big, cite, code,
del, dfn, em, img, ins, kbd, q, s, samp,
small, strike, strong, sub, sup, tt, var,
b, u, i, center,
dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td,
article, aside, canvas, details, embed,
figure, figcaption, footer, header, hgroup,
menu, nav, output, ruby, section, summary,
time, mark, audio, video {
    margin: 0;
    padding: 0;
    border: 0;
    font-size: 100%;
    font: inherit;
    vertical-align: baseline;
}
article, aside, details, figcaption, figure,
footer, header, hgroup, menu, nav, section {
    display: block;
}
body {
    line-height: 1;
}
ol, ul {
    list-style: none;
}
blockquote, q {
    quotes: none;
}
blockquote:before, blockquote:after,
q:before, q:after {
    content: '';
    content: none;
}
table {
    border-collapse: collapse;
    border-spacing: 0;
}


/* GLOBAL STYLING */
h1 { font-size: 28px; font-weight: bold; margin-bottom: var(--paragraph-padding); }
h2 { font-size: 20px; margin-top: 20px; font-weight: bold; margin-bottom: var(--paragraph-padding); }
p { font-size: 14px; margin-bottom: var(--paragraph-padding); }
small { font-size: 12px; }
em { font-style: italic; }
strong { font-weight: bold; }
img { vertical-align: bottom; }
blockquote {
    border-left: 4px solid var(--blockquote-border-color);
    background-color: var(--blockquote-background-color);
    padding: calc(var(--paragraph-padding) + 1px) 8px 1px 8px;
    margin-bottom: var(--paragraph-padding);
    width: auto;
    box-sizing: border-box;
}
a:link {
    color: var(--primary-text-color);
}
a:visited {
    color: var(--primary-text-color);
}


/* GLOBAL STYLING FOR FORM ELEMENTS */
input, select {
    font-family: 'ComicFont', 'Arial', sans-serif;
    box-sizing: border-box;
    vertical-align:top;
    height: 32px;
    width: 200px;
    padding: 6px;
    margin: var(--paragraph-padding);
    color: var(--primary-text-color);
    background-color: var(--blockquote-background-color);
    border: 2px solid var(--blockquote-border-color);
}
input:focus, select:focus {
    outline: none;
}
input::-webkit-search-cancel-button {
    -webkit-appearance: none;
    background-image: url({% static "comics/icons/delete.svg" %});
    background-size: contain;
    height: 14px;
    width: 14px;
    margin: 0;
}
select option {
    background-color: var(--blockquote-background-color);
    border: 2px solid var(--blockquote-border-color);
}


/* CORE PAGE LAYOUT */
html {
    height: 100%;
    overflow-x: hidden;
    -webkit-tap-highlight-color: transparent;
}
body {
    overflow-x: hidden;
    font-family: 'ComicFont', 'Arial', sans-serif;
    padding: 0;
    margin: 0;
    min-width: var(--comic-min-width);
    background: var(--overflow-background);
    background-size: cover;
    display: flex;
    justify-content: center;
    color: var(--primary-text-color);
    min-height: 100%;
}
.content-background {
    background: var(--content-background);
    display: flex;
    flex-direction: column;
    align-items: center;
    max-width: calc(var(--comic-max-width) + var(--overflow-extra-width) * 2);
    min-width: var(--comic-min-width);
    width: 100%;
    box-shadow: 0 0 10px #00000088;
}
@media (min-width: 728px) and (orientation:landscape) {
    .content-background {
        max-width: calc(var(--comic-max-width) * 2 + var(--overflow-extra-width) * 2);
    }
}
.content {
    width: 100%;
    max-width: var(--comic-max-width);
    min-width: var(--comic-min-width);
    overflow: hidden;
    display: flex;
    flex-wrap: wrap;
}
@media (min-width: 728px) and (orientation:landscape) {
    .content {
        max-width: calc(var(--comic-max-width) * 2 + var(--paragraph-padding) * 2);
    }
}
.panel {
    position: relative;
    max-width: var(--comic-max-width);
    min-width: var(--comic-min-width);
    width: 100%;
}
@media (min-width: 728px) and (orientation:landscape) {
    .panel {
        width: calc(50% - var(--paragraph-padding));
        margin-left: calc(var(--paragraph-padding) / 2);
        margin-right: calc(var(--paragraph-padding) / 2);
    }
}

.hidden {
    display: none;
}

.center-text {
    text-align: center;
}

.content-frame {
    box-sizing: border-box;
    border: 20px solid transparent;
    border-image-source: var(--post-border-image);
    border-image-slice: 20 fill;
    border-image-repeat: repeat;
    margin-bottom: calc(var(--paragraph-padding)*2);
    width: 100%;
}

header {
    width: 100%;
    max-width: var(--comic-max-width);
    min-width: var(--comic-min-width);
    overflow: hidden;
    margin-bottom: calc(var(--paragraph-padding) * 2);
    user-select: none;
}
.header-image {
    width: 33%;
    margin: var(--paragraph-padding);
}
.header-link-container {
    display: flex;
    flex-wrap: wrap;
}
.header-link {
    display: block;
    color: white !important;
    text-decoration: none;
    margin: var(--paragraph-padding);
}


/* FOOTER AND SOCIAL ICONS */
.social-container {
    display: flex;
    align-items: center;
    flex-direction: row;
    justify-content: center;
    align-items: center;
    padding: 8px;
    max-width: 100%;
}
.social-icon {
    width: 24px;
    height: 24px;
    font-size: 24px;
    padding: 8px;
    text-align: center;
    margin: 8px;
}
footer {
    background-color: var(--footer-background-color);
    min-height: 100px;
    width: 100%;
    overflow: hidden;
    display: flex;
    justify-content: center;
    flex-direction: column;
    align-items: center;
    margin-top: auto;
    text-align: center;
    padding-bottom: 0;
}
.footer-content {
    padding: 8px;
    padding-top: calc(var(--paragraph-padding) * 2);
    box-sizing: border-box;
    display: flex;
    flex-direction: column;
    align-items: center;
}
.footer-subsection {
    padding: 8px;
    width: 100%;
    box-sizing: border-box;
    text-align: center;
}
footer a:hover {
    color: #978b65;
}
footer a:active {
    color: #978b65;
}


/* AD SIZES */
.ad-banner-content {
    min-width: 100%;
    min-height: 0px;
    max-width: var(--comic-max-width);
    overflow: hidden;
    background-color: var(--ad-debug-color);
    margin-bottom: calc(var(--paragraph-padding)*2);
}
.ad-mobile-leaderboard {
    min-width: 100%;
    min-height: 0px;
    background-color: var(--ad-debug-color);
    display: inline-block;
    margin-bottom: calc(var(--paragraph-padding));
}


/* READER PAGE */
#staff-text {
    display: none;
}
#reader-panel {
    user-select: none;
}
#comic-image-spinner {
    position: absolute;
    top: calc(50% - var(--spinner-size) / 2);
    left: calc(50% - var(--spinner-size) / 2);
    width: var(--spinner-size);
    height: var(--spinner-size);
    background-image: var(--spinner-image);
    background-size: contain;
    transition: opacity 0.1s ease-out;
    animation: spin 2s linear infinite;
    opacity: 0;
}
.page-image-wrapper {
    position: relative;
}
@keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
}
.comic-image {
    width: 100%;
    height: auto;
    transition: opacity 0.5s ease-out
}
#comic-post-date {
    margin-bottom: calc(var(--paragraph-padding) * 2);
    font-size: 12px;
}
.tab-group {
    display: flex;
    margin-left: 6px;
    height: var(--navigation-height);
    align-items: flex-end;
}
.tab {
    background-color: var(--tab-inactive-color);
    height: 16px;
    padding: 6px 12px;
    margin-left: 3px;
    margin-right: 3px;
    margin-bottom: -7px;
    z-index: 1;
    border: 3px solid black;
    border-bottom: 4px solid var(--tab-inactive-color);
    user-select: none;
    cursor: pointer;
}
.tab.active {
    background-color: var(--tab-active-color);
    border-bottom: 4px solid var(--tab-active-color);
}


/* NAV BUTTONS */
.navigation-wrapper {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 100%;
    height: var(--navigation-height);
    transition: opacity 0.5s ease-out;
}
.navigation-button {
    height: var(--navigation-height);
    width: calc(var(--navigation-height) * 2);
    max-width: calc(var(--navigation-height) * 2);
    background: var(--navigation-spritesheet);
    background-repeat: no-repeat;
    background-size : 400% 200%;
}

/* Shrink the button size on very small screens. */
/* This needs to be anything smaller than calc(--navigation-height * 8), but can't do that in media queries */
@media (max-width: 512px) {
    .navigation-wrapper {
        height: calc(var(--navigation-height) / 1.5);
    }
    .navigation-button {
        height: calc(var(--navigation-height) / 1.5);
        width: calc(var(--navigation-height) * 2 / 1.5);
        max-width: calc(var(--navigation-height) * 2 / 1.5);
    }
}

.navigation-button:active {
   background-position-y: 100%;
}
@media (pointer:fine) {  /* If we don't do this in a media query, it gets stuck hovered on touch devices */
    .navigation-button:hover {
       background-position-y: 100%;
    }
}
.navigation-first {
    background-position-x: 0;
    left: 10%;
}
.navigation-previous {
    background-position-x: 33.3%;
    left: 30%;
}
.navigation-next {
    background-position-x: 66.6%;
    left: 50%;
}
.navigation-last {
    background-position-x: 100%;
    left: 70%;
}
.navigation-random {
    background: transparent;
    color: white !important;
    text-decoration: none;
    text-align: center;
    line-height: var(--navigation-height);
}


/* COMIC INVITATION SECTION */
.invitation {
    width: 100%;
    margin-bottom: calc(var(--paragraph-padding)*2);
}
.invitation img {
    width: 100%;
}


/* TAGS */
#comic-tags {
    display: flex;
    flex-direction: column;
    width: 100%;
    font-size: 12px;
}
.tag {
    background-color: var(--tag-background-color);
    margin: 1px 2px;
    padding: 4px;
    padding-left: 24px;
    height: 16px;
    line-height: 16px;
    font-size: 12px;
    text-align: center;
    text-decoration: none;
    display: inline-block;
    background-image: var(--default-tag-icon);
    background-size: 16px 16px;
    background-position: 4px 4px;
    background-repeat: no-repeat;
}
.tag:hover {
    filter: brightness(125%);
}
.tag.error {
    background-color: var(--tag-error-color);
}


/* ARCHIVE TILES */
.archive-tile-container {
    width: 100%;
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
}
.archive-tile {
    width: var(--tile-size);
    background-color: var(--tag-background-color);
    padding: var(--paragraph-padding);
    padding-top: calc(var(--tile-size) + var(--paragraph-padding) * 2);
    margin: var(--paragraph-padding);
    background-image: var(--default-tag-icon);
    background-size: var(--tile-size) var(--tile-size);
    background-position: var(--paragraph-padding) var(--paragraph-padding);
    background-repeat: no-repeat;
    text-align: center;
    text-decoration: none;
    word-wrap: break-word;
    font-size: 14px;
}
.archive-tile:hover {
    filter: brightness(125%);
}
.archive-tile > strong, .archive-tile > small {
    display: block;
}
.archive-tile-short {
    padding-top: var(--paragraph-padding);
}


/* ARCHIVE PAGE TABLE */
.archive-page-td-date {
    min-width: 128px;
    padding-top: 4px;
    padding-bottom: 4px;
}
.archive-page-td-title {
    border-left: 4px solid var(--blockquote-border-color);
    padding-top: 4px;
    padding-bottom: 4px;
    padding-left: 24px;
}


/* TAG DETAIL PAGE */
.tag-detail-header {
    display: flex;
    align-items: center;
}

.tag-detail-header img {
    height: 128px;
    width: 128px;
    border-right: 4px solid var(--blockquote-border-color);
    margin-right: 24px;
    padding-right: 24px;
}

#tag-post {
    margin-top: 24px;
    margin-bottom: 24px;
}


/* GLOBAL INDEX PAGE */
.index-section {
    background-color: #CFD8DC;
    padding: 24px;
    margin: 0;
    text-decoration: wavy;
}
.index-section:nth-child(even) {
    background-color: #B0BEC5;
}


<|start_filename|>apps/comics/templates/comics/archive/pages.html<|end_filename|>
{% extends 'comics/archive/base.html' %}

{% block title %}Pages{% endblock %}
{% block archive_content %}
<script>{% include 'comics/archive/filters.js' %}</script>
<div class="archive-tile-container">
    <input type="search" spellcheck="false" oninput="filterResults(this)" placeholder="Search Page Titles...">
</div>
<div class="archive-tile-container">
    {% for page in pages_and_chapters %}
        {% if page.posted_at %}
            {# This is a Page #}
            <a data-title="{{ page.title }}" class="archive-tile archive-tile-short" href="{{ page.get_absolute_url }}" style="background-image: url({{page.thumb_url}});">
                <strong>{{ page.title }}</strong>
                <small>{{ page.posted_at | date:"d M Y" }}</small>
            </a>
        {% else %}
            {# This is a Chapter Heading instead #}
            <h2 class="archive-chapter-break" style="width:100%">{{ page.title }}</h2>
        {% endif %}
    {% endfor %}
</div>
{% endblock %}


<|start_filename|>apps/comics/templates/comics/community.html<|end_filename|>
{% extends 'comics/base.html' %}

{% block page_title %} | Community{% endblock %}
{% block page_description %}Join us on your favorite social media platform!{% endblock %}

{% block content %}
<div class="content-frame">
    <p>
        Back to: <a class="tag" {% if comic.favicon_image %}style="background-image: url({{ comic.favicon_image.url }})"{% endif %} href="{% url 'index' %}">Comic</a>
    </p>
    <h1 class="center-text">Can't Get Enough Swords?</h1>
    <p class="center-text">Join us on your favorite social platform!</p>

    <div class="archive-tile-container">
        {% for link in comic.social_links.all %}
            {% if link.visit_url %}
            <a class="archive-tile" href="{{ link.visit_url }}"
               style="background-image: url('{% if link.platform.image %}{{ link.platform.image.url }}{% else %}?{% endif %}')">
              <strong>{% if link.title %}{{ link.title }}{% else %}{{ link.platform.name }}{% endif %}</strong>
              <small>{% if link.visit_cta %}{{ link.visit_cta }}{% else %}{{ link.platform.visit_cta }}{% endif %}</small>
            </a>
            {% endif %}
        {% endfor %}
        <!-- TODO: Forums -->

    </div>
</div>
{% endblock %}

<|start_filename|>apps/comics/templates/comics/js/reader.js<|end_filename|>
"use strict";

const COMICS = function () {

    // Set up some global variables
    const CACHE = {};
    const COMIC = {
        "title": "{{ comic.title }}",
        "pages": [],
    };
    let NUM_ACTIVE_REQUESTS = 0;

    const bodyTag = document.getElementsByTagName("body")[0];
    const DISCOURSE_URL = bodyTag.dataset.discourseUrl;

    // Convenience function to set opacity on a query
    function setOpacity(querySelector, newOpacity) {
        document.querySelectorAll(querySelector).forEach(function (e) {
            e.style.opacity = newOpacity;
        });
    }

    // Make the current comic's data appear in the page
    function loadDataIntoDOM(pageData) {
        // Browser State
        document.title = pageData.title + " | " + COMIC.title;

        // Compute Tag HTML
        let tagHTML = "";
        pageData.tag_types.forEach(function (tagType) {
            let tagStrings = "";
            tagType.tags.forEach(function (tag) {
                if (tag.icon !== "") {
                    tagStrings += `<a class="tag" style="background-image: url(${tag.icon});" href="${tag.url}">${tag.title}</a>`;
                } else {
                    tagStrings += `<a class="tag" href="${tag.url}">${tag.title}</a>`;
                }
            });

            tagHTML += `<p>${tagType.title}: ${tagStrings}</p>`;
        });

        // Page Content
        document.getElementById("comic-title").innerHTML = pageData.title;
        document.getElementById("comic-tags").innerHTML = tagHTML;
        document.getElementById("comic-post-date").innerHTML = pageData.posted_at;
        document.getElementById("comic-post").innerHTML = pageData.post;
        document.getElementById("comic-transcript").innerHTML = pageData.transcript;
        document.getElementById("comic-alt-text").innerHTML = pageData.alt_text;
        document.getElementById("comic-image").src = pageData.image;
        document.getElementById("comic-image").title = pageData.alt_text;
        setOpacity("#comic-image", 0.5);
        setOpacity("#comic-image-spinner", 1);

        if (localStorage.getItem("admin") === "true") {
            // TODO: instead of showing/hiding this, inject it
            document.getElementById("staff-text").style.display = "block";
            document.getElementById("staff-link").href = pageData.admin;
        }

        // Navigation Buttons
        recalculateNavigationVisibility();
    }

    // Make the navigation buttons appear or disappear
    function recalculateNavigationVisibility() {
        // TODO: This might be re-rendering the page too often because it's messing with the style directly.
        // TODO: Do this with a CSS class
        if (NUM_ACTIVE_REQUESTS > 0) {
            setOpacity(".navigation-wrapper", 0.5);
        } else {
            setOpacity(".navigation-wrapper", 1);
        }

        const page = getActivePageData();

        if (page === undefined) {
            // We're still loading
            return;
        }

        // TODO: Change these into CSS class for "hidden"
        if (page.slug === page.last) {
            setOpacity(".navigation-next, .navigation-last", 0);
        } else {
            setOpacity(".navigation-next, .navigation-last", 1);
        }
        if (page.slug === page.first) {
            setOpacity(".navigation-previous, .navigation-first", 0);
        } else {
            setOpacity(".navigation-previous, .navigation-first", 1);
        }

        document.querySelectorAll(".navigation-first, .navigation-previous, .navigation-next, .navigation-last").forEach(function (e) {
            e.blur();
        });
    }

    function imageLoaded() {
        setOpacity("#comic-image", 1);
        setOpacity("#comic-image-spinner", 0);
    }

    function getActivePageData() {
        return CACHE[getComicAndPageFromActiveUrl().pageSlug];
    }

    // Kickstart the page load
    async function initializePage() {
        // Bind the tab functionality
        initializeTabs();

        // Load the page data
        const response = await requestPageData(getComicAndPageFromActiveUrl().pageSlug);
        await navigateToPage(response.slug, false);

        await preloadRandomComics();
    }

    async function preloadRandomComics() {
        const response = await fetch("/comic/data/");
        const data = await response.json();
        COMIC.pages = data.pages;
    }

    async function navigateToPage(pageSlug, pushState = true) {
        const pageData = CACHE[pageSlug];

        // This should never happen, but it's protection
        if (pageData === undefined) {
            console.log("Can't navigate, unknown destination. " + pageSlug);
            return;
        }

        // We don't push the state on the initial load or if using the back/forward buttons
        // We also don't care if there are outstanding requests in those cases
        if (pushState) {
            if (NUM_ACTIVE_REQUESTS > 0) {
                console.log("Can't navigate, " + NUM_ACTIVE_REQUESTS + " active requests.");
                return;
            }
            window.history.pushState(pageData, pageData.title, '/comic/' + pageData.slug + "/");
        }

        // Render the new page
        loadDataIntoDOM(pageData);

        // If we're currently not able to see the top of the next page, scroll up to it
        const readerElement = document.getElementById("reader-panel");
        readerElement.scrollIntoView({behavior: "smooth"});

        // Tell Google Analytics that we successfully loaded the page TODO: Make this into a global event that can be used in the CodeSnippets functionality
        if ("ga" in window && ga.getAll !== undefined) {
            var tracker = ga.getAll()[0];
            if (tracker) {
                tracker.set('page', window.location.pathname);
                tracker.send('pageview');
            }
        }

        // Try to refresh the comments
        refreshDiscourseComments();

        // Cache all the pages we can navigate to from this page
        await Promise.all([
            requestPageData(pageData.first),
            requestPageData(pageData.previous),
            requestPageData(pageData.next),
            requestPageData(pageData.last),
        ])
    }

    // The next 4 functions perform the navigation.
    function firstButtonPressed() {
        navigateToPage(getActivePageData().first);
        return false;
    }

    function previousButtonPressed() {
        navigateToPage(getActivePageData().previous);
        return false;
    }

    async function randomButtonPressed() {
        if (COMIC.pages.length <= 0) {
            return false;
        }

        let randomSlug = COMIC.pages[Math.floor(Math.random() * COMIC.pages.length)];
        await requestPageData(randomSlug);
        await navigateToPage(randomSlug);

        return false;
    }

    function nextButtonPressed() {
        navigateToPage(getActivePageData().next);
        return false;
    }

    function lastButtonPressed() {
        navigateToPage(getActivePageData().last);
        return false;
    }

    // Make an AJAX request to get data
    async function requestPageData(pageSlug) {
        // Don't try to get missing pages
        if (pageSlug === null) {
            return;
        }

        if (CACHE[pageSlug] !== undefined) {
            recalculateNavigationVisibility();
            return CACHE[pageSlug];
        }

        // Make the nav buttons go transparent
        NUM_ACTIVE_REQUESTS += 1;
        recalculateNavigationVisibility();

        // Execute the request
        const response = await fetch("/comic/data/" + pageSlug + "/");
        const data = await response.json()

        // Run the callback and update the navigation state
        CACHE[pageSlug] = data;

        // Make the nav buttons go opaque
        NUM_ACTIVE_REQUESTS -= 1;
        recalculateNavigationVisibility();

        // Pre-warm the image cache
        preloadImage(data.image);

        return data;
    }

    function preloadImage(url) {
        let img = new Image();
        img.onload = function () {
            img.src = "";
            img = null;
        }
        img.src = url;
    }

    // Implement keyboard navigation using the arrow keys
    function keyboardNav(event) {
        // handling Internet Explorer stupidity with window.event
        const keycode = (event.which) ? event.which : event.keyCode;
        const LEFT = 37;
        const RIGHT = 39;
        if (event.altKey) {
            return true;
        }
        if (keycode === LEFT) {
            previousButtonPressed();
            return false;
        } else if (keycode === RIGHT) {
            nextButtonPressed();
            return false;
        }
        return true;
    }

    document.onkeydown = keyboardNav;

    function refreshDiscourseComments() {
        if (!DISCOURSE_URL) {
            return;
        }
        try {
            const url = new URL(document.location);
            window.DiscourseEmbed = {
                discourseUrl: DISCOURSE_URL,
                discourseEmbedUrl: url.origin + url.pathname
            };

            // If the comments chain already exists, remove it
            const discourseScript = document.getElementById('discourse-script');
            if (discourseScript) {
                discourseScript.remove();
            }
            const discourseComments = document.getElementById('discourse-comments');
            if (discourseComments) {
                discourseComments.innerHTML = '';
            }

            // Create a script tag that loads the comments into the #discourse-comments tag
            const d = document.createElement('script');
            d.id = 'discourse-script';
            d.type = 'text/javascript';
            d.async = true;
            d.src = window.DiscourseEmbed.discourseUrl + 'javascripts/embed.js';
            document.getElementsByTagName('head')[0].appendChild(d);
        } catch (error) {
            console.log("Comments not loaded, will try again next time the page navigates.");
        }
    }

    function getComicAndPageFromActiveUrl() {
        const url = new URL(document.location).pathname;
        const split = url.split('/');
        return {"pageSlug": split[2]};
    }

    function initializeTabs() {
        document.querySelectorAll('.tab').forEach(function (element) {
            element.addEventListener('click', function (event) {
                const target = event.target.dataset.target;
                activateTab(target);
            });
        });

        let activeTab = localStorage.getItem("comics.activeTab");
        if (activeTab === null || !["info-frame", "comments-frame", "quests-frame"].includes(activeTab)) {
            activeTab = "info-frame";
        }
        activateTab(activeTab);
    }

    function activateTab(target) {
        localStorage.setItem("comics.activeTab", target);

        // Set tab styling
        document.querySelectorAll('.tab').forEach(function (element) {
            if (element.dataset.target === target) {
                element.classList.add("active");
            } else {
                element.classList.remove("active");
            }
        });

        // Set content styling
        document.querySelectorAll('.tab-content-area').forEach(function (element) {
            if (element.id === target) {
                element.style.display = "block";
            } else {
                element.style.display = "none";
            }
        });
    }

    window.onpopstate = function (event) {
        navigateToPage(getComicAndPageFromActiveUrl().pageSlug, false);
    };

    // Run the initialization and then publish any variables that need to be public.
    return {
        initializePage: initializePage,
        firstButtonPressed: firstButtonPressed,
        previousButtonPressed: previousButtonPressed,
        randomButtonPressed: randomButtonPressed,
        nextButtonPressed: nextButtonPressed,
        lastButtonPressed: lastButtonPressed,
        imageLoaded: imageLoaded,
    };
}();

document.addEventListener("DOMContentLoaded", function(event) {
    COMICS.initializePage();
});


<|start_filename|>apps/comics/templates/comics/archive/filters.js<|end_filename|>
function filterResults(inputElement) {
    // Declare variables
    var query = inputElement.value.toLowerCase();

    var tiles = document.querySelectorAll(".archive-tile");
    tiles.forEach(function (tile) {
        tile.hidden = tile.dataset.title.toLowerCase().indexOf(query) == -1;
    });
}

function sortResults(selectElement) {
    var selection = selectElement.value;

    // Define common sorting functions
    var alphabetical = function(a, b) {
        return a.dataset.title.toLowerCase().localeCompare(b.dataset.title.toLowerCase());
    }
    var mostAppearances = function(a, b) {
        return parseInt(b.dataset.count) - parseInt(a.dataset.count);
    }
    var fewestAppearances = function(a, b) {
        return parseInt(a.dataset.count) - parseInt(b.dataset.count);
    }

    // Sort the array
    var tiles = Array.from(document.querySelectorAll(".archive-tile"));
    tiles.sort({
        alphabetical: alphabetical,
        mostAppearances: mostAppearances,
        fewestAppearances: fewestAppearances,
    }[selection]);

    // Reorder the elements
    tiles.forEach(function (tile) {
        tile.parentNode.appendChild(tile);
    });
}

<|start_filename|>saliency_test/index.js<|end_filename|>
/**
 * This function captures an image from the webcam, resizes it to the preferred
 * dimensions of the selected model, and represents it in a format suitable for
 * input to the network.
 */

function predictSaliency(image, model, networkInputSize) {
  return tf.tidy(() => {
    // Fetch input image
    loadedImage = tf.browser.fromPixels(image);
    const batchedImage = loadedImage.toFloat().expandDims();
    const resizedImage = tf.image.resizeBilinear(batchedImage, networkInputSize, true);
    const clippedImage = tf.clipByValue(resizedImage, 0.0, 255.0);  // TODO: Does this actually do anything?
    const reversedImage = tf.reverse(clippedImage, 2);

    // Make a prediction
    const modelOutput = model.predict(reversedImage);
    const resizedOutput = tf.image.resizeBilinear(modelOutput, [image.height, image.width], true);
    const clippedOutput = tf.clipByValue(resizedOutput, 0.0, 255.0);  // TODO: Does this actually do anything?
    return clippedOutput.squeeze();
  });
}

function injectCanvas(query, width, height) {
    // Generate a new canvas element with the target size and put it into the dom under the queried element
    var cropContainer = document.querySelector(query);
    const newCanvas = document.createElement("canvas");
    newCanvas.width = width;
    newCanvas.height = height;
    cropContainer.appendChild(newCanvas);
    return newCanvas;
}

function brightestRect(tensor, rectSize, ctx) {
    console.log("Calculating brightest rect...");
    // How roughly are we guessing. If this is bigger, it will run faster
    const stepSize = rectSize / 5;

    var maxWidth = tensor.shape[0];
    var maxHeight = tensor.shape[1];

    var bestValue = 0;
    var bestPosition = undefined;
    // Iterate over chunks by stepSize, limited by bounds
    for (var col = 0; col < maxWidth - rectSize + 1; col += stepSize) {
        for (var row = 0; row < maxHeight - rectSize + 1; row += stepSize) {

            // Slice the tensor to get the submatrix of [rectSize, rectSize] starting at this chunk.
            var submatrix = tf.slice(tensor, [col, row], [rectSize, rectSize]);
            // Calculate the sum of the submatrix
            var value = submatrix.mean().dataSync()[0];
            if (value > bestValue) {
                bestValue = value;
                bestPosition = [row, col];
            }

            // Draw selected bounding box
            ctx.globalCompositeOperation = "source-over";
            ctx.beginPath();
            ctx.rect(row, col, rectSize, rectSize);
            ctx.strokeStyle = "#ffff0011";
            ctx.stroke();

            ctx.font = "6px Arial";
            ctx.fillStyle = "#ffff00";
            ctx.fillText("" + Math.round(value * 100), row, col + 12);
        }
    }
    return bestPosition;
}


function cropRotateAndScale(img, targetSize) {
    var newCanvas = injectCanvas("#cropped", targetSize, targetSize);

    // Determine the portion of the panel we want to target
    var sourceRect = [0, 0, img.naturalWidth, img.naturalHeight];

    // If we're very wide, take the leftmost quarter (it's probably a single row strip)
    if (img.naturalWidth > 2*img.naturalHeight) {
        sourceRect = [0, 0, Math.floor(img.naturalWidth/4), img.naturalHeight];
    // If we're very tall, take the topmost quarter (it's probably a very long page)
    } else if (img.naturalHeight > 2*img.naturalWidth) {
        sourceRect = [0, 0, img.naturalWidth, Math.floor(img.naturalHeight/4)];
    } else {
        sourceRect = [0, 0, Math.floor(img.naturalWidth/2), Math.floor(img.naturalHeight/2)];
    }

    // Make sure the aspect ratio stays correct for the targetSize
    var targetRect = [0, 0, targetSize, targetSize];
    if (sourceRect[2] > sourceRect[3]) {
        let size = targetSize / sourceRect[2] * sourceRect[3];
        targetRect = [0, (targetSize - size) / 2, targetSize, size];
    } else if (sourceRect[3] > sourceRect[2]) {
        let size = targetSize / sourceRect[3] * sourceRect[2];
        targetRect = [(targetSize - size) / 2, 0, size, targetSize];
    }

    // TODO: Try shaving a tiny bit off the top and left, since those are probably panel borders

    // Draw the target part of the image onto the canvas, and fill the rest of the area with black
    const ctx = newCanvas.getContext("2d");
    ctx.fillStyle = 'black';
    ctx.fillRect(0, 0, targetSize, targetSize);
    ctx.drawImage(img, ...sourceRect, ...targetRect);

    // TODO: try applying a vignette to block out some comic border spam?
//    var grd = ctx.createRadialGradient(targetSize / 2, targetSize / 2, targetSize / 3, targetSize / 2, targetSize / 2, targetSize / 2);
//    grd.addColorStop(0, "#00000000");
//    grd.addColorStop(1, "#000000ff");
//    ctx.fillStyle = grd;
//    ctx.fillRect(0, 0, targetSize, targetSize);

    return newCanvas;
}


async function predictSaliencyAndRender(image, model, networkSize) {
    console.log("Predicting saliency...");
    // Calculate the saliency
    var saliencyMap = predictSaliency(image, model, [networkSize, networkSize]);
//    var dropLow = 0.25;
//    saliencyMap = saliencyMap.clipByValue(dropLow, 1.0);
//    saliencyMap = saliencyMap.sub(tf.scalar(dropLow)).mul(tf.scalar(1 / (1-dropLow)));

    // Initialize random trash
    const canvas = injectCanvas("#saliency", networkSize, networkSize);
    const ctx = canvas.getContext("2d");

    // Draw saliency map
    await tf.browser.toPixels(saliencyMap, canvas);

    // Multiply in the original image
    ctx.globalCompositeOperation = "multiply";
    ctx.drawImage(image, 0, 0);

    return saliencyMap;
}


function getThumbnailOptions(croppedImage, saliencyMap) {
    thumbSize = Math.floor(croppedImage.width / 1.75);
    const canvas = injectCanvas("#thumbnails", thumbSize, thumbSize);
    const ctx = canvas.getContext("2d");

    // Calculate the row/col of the center of the rectangle. Most effective solution, currently.
    var corner = brightestRect(saliencyMap, thumbSize, ctx);

    // Calculate the most salient point as the center instead. Not as effective.
//    var i = tf.argMax(saliencyMap.flatten()).dataSync()[0]
//    var row = Math.floor(i / saliencyMap.shape[1]);
//    var col = i % saliencyMap.shape[1];
//    corner = [col - (thumbSize / 2), row - (thumbSize / 2)];

    // TODO: Calculate the size of the blob originating around the most salient point, and capture that in a square?

    // Draw selected bounding box
    ctx.drawImage(croppedImage, corner[0], corner[1], thumbSize, thumbSize, 0, 0, thumbSize, thumbSize);
}



async function runModel() {
    // Load model
    console.log("Loading model...");
    var networkSize = 128;
    const modelURL = "https://storage.googleapis.com/msi-net/model/high/model.json";
    const model = await tf.loadGraphModel(modelURL);
    console.log("Model loaded!");

    document.querySelectorAll("#inputs img").forEach(async function (img) {
        // Capture the portion of the image we're interested in
        var croppedImage = cropRotateAndScale(img, networkSize);
        // Calculate the saliency map and render it
        var saliencyMap = await predictSaliencyAndRender(croppedImage, model, networkSize);
        // Render thumbnails using different possible algorithms
        getThumbnailOptions(croppedImage, saliencyMap);
        // TODO: Make this return from the original source, if possible.

        // Cleanup
        saliencyMap.dispose();
    });

    model.dispose();
}
