Update /photography.

This commit is contained in:
voussoir 2023-09-12 19:43:44 -07:00
parent 42d498f3f7
commit 1c40e761f7
2 changed files with 541 additions and 458 deletions

View file

@ -1,330 +0,0 @@
:root
{
--color_bodybg: #272822;
--color_codebg: rgba(255, 255, 255, 0.05);
--color_codeborder: rgba(255, 255, 255, 0.2);
--color_h1bg: #284142;
--color_htmlbg: #1b1c18;
--color_blockquotebg: rgba(0, 0, 0, 0.2);
--color_blockquoteedge: rgba(255, 255, 255, 0.2);
--color_inlinecodebg: rgba(255, 255, 255, 0.1);
--color_link: #ae81ff;
--color_maintext: #ddd;
}
*, *:before, *:after
{
box-sizing: inherit;
}
html
{
height: 100vh;
box-sizing: border-box;
background-color: var(--color_htmlbg);
color: var(--color_maintext);
font-family: Verdana, sans-serif;
font-size: 10pt;
margin: 0;
}
body
{
min-height: 100%;
width: fit-content;
margin-left: auto;
margin-right: auto;
margin-top: 0;
margin-bottom: 0;
padding: 8px;
}
body.noscrollbar::-webkit-scrollbar
{
display: none;
}
body.noscrollbar
{
scrollbar-width: none;
}
header
{
width: 100%;
max-width: 120em;
margin-left: auto;
margin-right: auto;
text-align: end;
}
header > *
{
display: inline-block;
padding: 16px;
background-color: var(--color_bodybg);
}
.album,
.photograph
{
position: relative;
margin-left: auto;
margin-right: auto;
margin-top: 8vh;
margin-bottom: 8vh;
}
.photograph
{
padding: 2vh;
background-color: var(--color_bodybg);
border-radius: 16px;
}
article .photograph:first-of-type
{
margin-top: 0;
}
article .photograph:last-of-type
{
margin-bottom: 0;
}
.photograph img
{
max-height: 92vh;
border-radius: 8px;
}
article .morelink
{
font-size: 2em;
text-align: center;
margin-top: 0;
}
@media not print
{
.photograph
{
box-shadow: #000 0px 0px 40px -10px;
}
}
@media screen and (min-width: 600px)
{
article
{
width: fit-content;
}
}
@media screen and (max-width: 600px)
{
.photograph
{
box-shadow: none;
}
}
@media not all and (pointer: fine)
{
#keyboardhint,
#scrollbartoggle
{
display: none;
}
}
h1, h2, h3, h4, h5
{
margin-bottom: 0;
padding: 8px;
}
h1:first-child, h2:first-child, h3:first-child, h4:first-child, h5:first-child
{
margin-top: 0;
}
h2, h3, h4, h5
{
border-bottom: 1px solid var(--color_maintext);
/*background-color: var(--color_h1bg);*/
}
h1 + p
{
margin-top: 0;
}
p:last-child
{
margin-bottom: 0;
}
h1 {font-size: 2.00em;} h1 * {font-size: inherit;}
h2 {font-size: 1.75em;} h2 * {font-size: inherit;}
h3 {font-size: 1.50em;} h3 * {font-size: inherit;}
h4 {font-size: 1.25em;} h4 * {font-size: inherit;}
h5 {font-size: 1.00em;} h5 * {font-size: inherit;}
.header_anchor_link {display: none; font-size: 1.0em; text-decoration: none}
h1:hover > .header_anchor_link {display: initial;}
h2:hover > .header_anchor_link {display: initial;}
h3:hover > .header_anchor_link {display: initial;}
h4:hover > .header_anchor_link {display: initial;}
h5:hover > .header_anchor_link {display: initial;}
a
{
color: var(--color_link);
cursor: pointer;
}
article *
{
max-width: 100%;
word-wrap: break-word;
}
#table_of_contents
{
border: 1px solid var(--color_blockquoteedge);
padding-top: 8px;
padding-bottom: 8px;
border-radius: 8px;
}
blockquote
{
background-color: var(--color_blockquotebg);
margin-inline-start: 0;
margin-inline-end: 0;
border-left: 4px solid var(--color_blockquoteedge);
padding: 8px;
padding-inline-start: 20px;
padding-inline-end: 20px;
}
table
{
border-collapse: collapse;
font-size: 1em;
}
table, table th, table td
{
border: 1px solid var(--color_maintext);
}
table th, table td
{
padding: 4px;
}
ol ol, ul ul, ol ul, ul ol
{
padding-inline-start: 20px;
}
*:not(pre) > code
{
background-color: var(--color_inlinecodebg);
border-radius: 4px;
line-height: 1.5;
padding-left: 4px;
padding-right: 4px;
}
pre
{
padding: 8px;
border: 1px solid var(--color_codeborder);
background-color: var(--color_codebg);
overflow-x: auto;
}
code,
pre,
.highlight *
{
font-family: monospace;
}
/*
Thank you richleland for pre-building this Monokai style.
https://github.com/richleland/pygments-css
*/
:root
{
--color_monokai_bg: #272822;
--color_monokai_purple: #ae81ff;
--color_monokai_green: #a6e22e;
--color_monokai_pink: #f92672;
--color_monokai_white: #f8f8f2;
--color_monokai_orange: #fd971f;
--color_monokai_yellow: #e6db74;
--color_monokai_blue: #66d9ef;
}
.highlight .hll { background-color: #49483e }
.highlight { background-color: var(--color_monokai_bg); color: var(--color_monokai_white) }
.highlight .c { color: #75715e } /* Comment */
.highlight .err { color: #960050; background-color: #1e0010 } /* Error */
.highlight .k { color: var(--color_monokai_pink) } /* Keyword */
.highlight .l { color: var(--color_monokai_purple) } /* Literal */
.highlight .n { color: var(--color_monokai_white) } /* Name */
.highlight .o { color: var(--color_monokai_pink) } /* Operator */
.highlight .p { color: var(--color_monokai_white) } /* Punctuation */
.highlight .ch { color: #75715e } /* Comment.Hashbang */
.highlight .cm { color: #75715e } /* Comment.Multiline */
.highlight .cp { color: #75715e } /* Comment.Preproc */
.highlight .cpf { color: #75715e } /* Comment.PreprocFile */
.highlight .c1 { color: #75715e } /* Comment.Single */
.highlight .cs { color: #75715e } /* Comment.Special */
.highlight .gd { color: var(--color_monokai_pink) } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gi { color: var(--color_monokai_green) } /* Generic.Inserted */
.highlight .gs { font-weight: bold } /* Generic.Strong */
.highlight .gu { color: #75715e } /* Generic.Subheading */
.highlight .kc { color: var(--color_monokai_purple) } /* Keyword.Constant */
.highlight .kd { color: var(--color_monokai_blue) } /* Keyword.Declaration */
.highlight .kn { color: var(--color_monokai_pink) } /* Keyword.Namespace */
.highlight .kp { color: var(--color_monokai_blue) } /* Keyword.Pseudo */
.highlight .kr { color: var(--color_monokai_blue) } /* Keyword.Reserved */
.highlight .kt { color: var(--color_monokai_blue) } /* Keyword.Type */
.highlight .ld { color: var(--color_monokai_yellow) } /* Literal.Date */
.highlight .m { color: var(--color_monokai_purple) } /* Literal.Number */
.highlight .s { color: var(--color_monokai_yellow) } /* Literal.String */
.highlight .na { color: var(--color_monokai_white) } /* Name.Attribute */
.highlight .narg {color: var(--color_monokai_orange) } /* Custom Name.Argument */
.highlight .nb { color: var(--color_monokai_blue) } /* Name.Builtin */
.highlight .nc { color: var(--color_monokai_white) } /* Name.Class */
.highlight .no { color: var(--color_monokai_blue) } /* Name.Constant */
.highlight .nd { color: var(--color_monokai_green) } /* Name.Decorator */
.highlight .ni { color: var(--color_monokai_white) } /* Name.Entity */
.highlight .ne { color: var(--color_monokai_blue) } /* Name.Exception */
.highlight .nf { color: var(--color_monokai_green) } /* Name.Function */
.highlight .nl { color: var(--color_monokai_white) } /* Name.Label */
.highlight .nn { color: var(--color_monokai_white) } /* Name.Namespace */
.highlight .nx { color: var(--color_monokai_green) } /* Name.Other */
.highlight .py { color: var(--color_monokai_white) } /* Name.Property */
.highlight .nt { color: var(--color_monokai_pink) } /* Name.Tag */
.highlight .nv { color: var(--color_monokai_white) } /* Name.Variable */
.highlight .ow { color: var(--color_monokai_pink) } /* Operator.Word */
.highlight .w { color: var(--color_monokai_white) } /* Text.Whitespace */
.highlight .mb { color: var(--color_monokai_purple) } /* Literal.Number.Bin */
.highlight .mf { color: var(--color_monokai_purple) } /* Literal.Number.Float */
.highlight .mh { color: var(--color_monokai_purple) } /* Literal.Number.Hex */
.highlight .mi { color: var(--color_monokai_purple) } /* Literal.Number.Integer */
.highlight .mo { color: var(--color_monokai_purple) } /* Literal.Number.Oct */
.highlight .sa { color: var(--color_monokai_white) } /* Literal.String.Affix */
.highlight .sb { color: var(--color_monokai_yellow) } /* Literal.String.Backtick */
.highlight .sc { color: var(--color_monokai_yellow) } /* Literal.String.Char */
.highlight .dl { color: var(--color_monokai_yellow) } /* Literal.String.Delimiter */
.highlight .sd { color: var(--color_monokai_yellow) } /* Literal.String.Doc */
.highlight .s2 { color: var(--color_monokai_yellow) } /* Literal.String.Double */
.highlight .se { color: var(--color_monokai_purple) } /* Literal.String.Escape */
.highlight .sh { color: var(--color_monokai_yellow) } /* Literal.String.Heredoc */
.highlight .si { color: var(--color_monokai_yellow) } /* Literal.String.Interpol */
.highlight .sx { color: var(--color_monokai_yellow) } /* Literal.String.Other */
.highlight .sr { color: var(--color_monokai_yellow) } /* Literal.String.Regex */
.highlight .s1 { color: var(--color_monokai_yellow) } /* Literal.String.Single */
.highlight .ss { color: var(--color_monokai_yellow) } /* Literal.String.Symbol */
.highlight .bp { color: var(--color_monokai_white) } /* Name.Builtin.Pseudo */
.highlight .fm { color: var(--color_monokai_blue) } /* Name.Function.Magic */
.highlight .vc { color: var(--color_monokai_white) } /* Name.Variable.Class */
.highlight .vg { color: var(--color_monokai_white) } /* Name.Variable.Global */
.highlight .vi { color: var(--color_monokai_white) } /* Name.Variable.Instance */
.highlight .vm { color: var(--color_monokai_white) } /* Name.Variable.Magic */
.highlight .il { color: var(--color_monokai_purple) } /* Literal.Number.Integer.Long */

View file

@ -1,18 +1,43 @@
import boto3
import io
import jinja2 import jinja2
import PIL.Image import PIL.Image
import sys import sys
import urllib.parse
import etiquette
import r2_credentials
from voussoirkit import dotdict from voussoirkit import dotdict
from voussoirkit import imagetools from voussoirkit import imagetools
from voussoirkit import pathclass from voussoirkit import pathclass
from voussoirkit import spinal from voussoirkit import spinal
from voussoirkit import vlogging
log = vlogging.get_logger(__name__, 'photography_generate')
pdb = etiquette.photodb.PhotoDB('D:\\Documents\\Photos\\_etiquette')
s3 = boto3.resource('s3',
endpoint_url = f'https://{r2_credentials.get_account_id()}.r2.cloudflarestorage.com',
aws_access_key_id = r2_credentials.get_access_key(),
aws_secret_access_key = r2_credentials.get_access_secret(),
)
bucket = s3.Bucket('voussoir')
S3_EXISTING_FILES = set(item.key for item in bucket.objects.filter(Prefix="photography/"))
PUBLISH_TAGNAME = 'voussoir_net_publish'
HEADLINER_TAGNAME = 'voussoir_net_headliner'
PHOTOGRAPHY_ROOTDIR = pathclass.Path(__file__).parent PHOTOGRAPHY_ROOTDIR = pathclass.Path(__file__).parent
ATOM_FILE = PHOTOGRAPHY_ROOTDIR.with_child('photography.atom') ATOM_FILE = PHOTOGRAPHY_ROOTDIR.with_child('photography.atom')
DOMAIN_ROOTDIR = PHOTOGRAPHY_ROOTDIR.parent DOMAIN_ROOTDIR = PHOTOGRAPHY_ROOTDIR.parent
CSS_CONTENT = PHOTOGRAPHY_ROOTDIR.with_child('dark.css').read('r', encoding='utf-8') DOMAIN_WEBROOT = 'https://voussoir.net'
DOMAIN_WEBROOT = ('file:///' + DOMAIN_ROOTDIR.absolute_path) if '--test' in sys.argv else 'https://voussoir.net' PHOTOGRAPHY_WEBROOT = 'https://voussoir.net/photography'
DOMAIN_WEBROOT = DOMAIN_WEBROOT.replace('\\', '/') S3_WEBROOT = 'https://files.voussoir.net'
SIZE_SMALL = 1440
SIZE_TINY = 360
def webpath(path, anchor=None): def webpath(path, anchor=None):
path = path.relative_to(DOMAIN_ROOTDIR, simple=True).replace('\\', '/').lstrip('/') path = path.relative_to(DOMAIN_ROOTDIR, simple=True).replace('\\', '/').lstrip('/')
@ -22,92 +47,129 @@ def webpath(path, anchor=None):
return path return path
class Photo: class Photo:
def __init__(self, filepath): def __init__(self, etq_photo, etq_album=None):
self.filepath = filepath self.etq_photo = etq_photo
self.thumbnail = make_thumbnail(filepath) self.article_id = self.etq_photo.real_path.replace_extension('').basename
self.article_id = filepath.replace_extension('').basename
self.anchor = f'#{self.article_id}' if etq_album is None:
self.published = imagetools.get_exif_datetime(filepath) parent_key = 'photography'
else:
parent_key = f'photography/{etq_album.title}'
self.s3_key = f'{parent_key}/{self.etq_photo.real_path.basename}'
self.small_key = f'{parent_key}/small_{self.etq_photo.real_path.basename}'
self.tiny_key = f'{parent_key}/tiny_{self.etq_photo.real_path.basename}'
self.color_class = 'monochrome' if self.etq_photo.has_tag('monochrome') else ''
self.s3_exists = self.s3_key in S3_EXISTING_FILES;
self.img_url = f'{S3_WEBROOT}/{self.s3_key}'
self.small_url = f'{S3_WEBROOT}/{self.small_key}'
self.tiny_url = f'{S3_WEBROOT}/{self.tiny_key}'
self.anchor_url = f'{DOMAIN_WEBROOT}/{parent_key}#{self.article_id}'
self.published = imagetools.get_exif_datetime(self.etq_photo.real_path)
def prepare(self):
if not self.s3_exists:
self.s3_upload()
def make_thumbnail(self, size):
image = PIL.Image.open(self.etq_photo.real_path.absolute_path)
icc = image.info.get('icc_profile')
(image_width, image_height) = image.size
exif = image.getexif()
(width, height) = imagetools.fit_into_bounds(image_width, image_height, size, size)
image = image.resize((width, height), PIL.Image.LANCZOS)
bio = io.BytesIO()
image.save(bio, format='jpeg', quality=75, exif=exif, icc_profile=icc)
bio.seek(0)
return bio
def s3_upload(self):
log.info('Uploading %s as %s', self.etq_photo.real_path.absolute_path, self.s3_key)
bucket.upload_fileobj(self.make_thumbnail(SIZE_SMALL), self.small_key)
bucket.upload_fileobj(self.make_thumbnail(SIZE_TINY), self.tiny_key)
bucket.upload_fileobj(self.etq_photo.real_path.open('rb'), self.s3_key)
self.s3_exists = True
def render_web(self, index=None, totalcount=None):
if totalcount is not None:
number_tag = f'<span class="number_tag">#{index}/{totalcount}</a>'
else:
number_tag = ''
def render_web(self, relative_directory=None):
return f''' return f'''
<article id="{self.article_id}" class="photograph"> <article id="{self.article_id}" class="photograph {self.color_class}">
<a href="{webpath(self.filepath)}" target="_blank"><img src="{webpath(self.thumbnail)}" loading="lazy"/></a> <a href="{self.img_url}" target="_blank"><img src="{self.small_url}" loading="lazy"/></a>
{number_tag}
</article> </article>
''' '''
def render_atom(self): def render_atom(self):
href = webpath(PHOTOGRAPHY_ROOTDIR, anchor=self.anchor)
imgsrc = webpath(self.thumbnail)
return f''' return f'''
<id>{self.article_id}</id> <id>{self.article_id}</id>
<title>{self.article_id}</title> <title>{self.article_id}</title>
<link rel="alternate" type="text/html" href="{href}"/> <link rel="alternate" type="text/html" href="{self.anchor_url}"/>
<updated>{self.published.isoformat()}</updated> <updated>{self.published.isoformat()}</updated>
<content type="html"> <content type="html">
<![CDATA[ <![CDATA[
<a href="{href}"><img src="{imgsrc}"/></a> <a href="{self.img_url}"><img src="{self.small_url}"/></a>
]]> ]]>
</content> </content>
''' '''
class Album: class Album:
def __init__(self, path): def __init__(self, etq_album):
self.path = path self.etq_album = etq_album
self.article_id = path.basename self.article_id = self.etq_album.title
self.link = webpath(path) self.photos = list(self.etq_album.get_photos())
self.published = imagetools.get_exif_datetime(sorted(path.glob_files('*.jpg'))[0]) self.photos = [p for p in self.photos if p.has_tag(PUBLISH_TAGNAME)]
self.photos = list(spinal.walk( self.photos = [Photo(etq_photo=photo, etq_album=self.etq_album) for photo in self.photos]
self.path, self.photos.sort(key=lambda p: p.published)
glob_filenames={'*.jpg'}, # self.link = webpath(path)
exclude_filenames={'*_small*'}, self.web_url = f'{PHOTOGRAPHY_WEBROOT}/{self.article_id}'
recurse=False, self.published = self.photos[0].published
yield_directories=False,
))
self.photos.sort(key=lambda file: file.basename)
self.photos = [Photo(file) for file in self.photos]
def render_web(self): def prepare(self):
firsts = self.photos[:5] for photo in self.photos:
remaining = self.photos[5:] photo.prepare()
if remaining:
next_after_more = remaining[0] def render_web(self, index=None, totalcount=None):
else: headliners = [p for p in self.photos if p.etq_photo.has_tag(HEADLINER_TAGNAME)]
next_after_more = None
return jinja2.Template(''' return jinja2.Template('''
<article id="{{article_id}}" class="album"> <article id="{{article_id}}" class="album">
<h1><a href="{{album_path}}">{{directory.basename}}</a></h1> <h1><a href="{{web_url}}">{{article_id}}</a></h1>
{% for photo in firsts %} <div class="albumphotos">
{% for photo in headliners %}
{{photo.render_web()}} {{photo.render_web()}}
{% endfor %} {% endfor %}
{% if remaining > 0 %} <div class="album_tinies">
<p class="morelink"><a href="{{album_path}}{{next_after_more.anchor}}">{{remaining}} more</a></p> {% for photo in photos %}
{% endif %} <a class="tiny_thumbnail {{photo.color_class}}" href="{{photo.anchor_url}}"><img src="{{photo.tiny_url}}" loading="lazy"/></a>
{% endfor %}
</div>
</div>
</article> </article>
''').render( ''').render(
article_id=self.article_id, article_id=self.article_id,
directory=self.path, web_url=self.web_url,
album_path=webpath(self.path), photos=self.photos,
next_after_more=next_after_more, headliners=headliners,
firsts=firsts,
remaining=len(remaining),
) )
def render_atom(self): def render_atom(self):
photos = [] photos = []
for photo in self.photos: for photo in self.photos:
href = webpath(photo.filepath) line = f'<article><a href="{photo.anchor_url}"><img src="{photo.small_url}" loading="lazy"/></a>'.replace('\\', '/')
imgsrc = webpath(photo.thumbnail)
line = f'<article><a href="{href}"><img src="{imgsrc}"/></a>'.replace('\\', '/')
photos.append(line) photos.append(line)
photos = '\n'.join(photos) photos = '\n'.join(photos)
return f''' return f'''
<id>{self.article_id}</id> <id>{self.article_id}</id>
<title>{self.article_id}</title> <title>{self.article_id}</title>
<link rel="alternate" type="text/html" href="{self.link}"/> <link rel="alternate" type="text/html" href="{self.web_url}"/>
<updated>{self.published.isoformat()}</updated> <updated>{self.published.isoformat()}</updated>
<content type="html"> <content type="html">
<![CDATA[ <![CDATA[
@ -126,30 +188,12 @@ def write(path, content):
print(path.absolute_path) print(path.absolute_path)
path.write('w', content, encoding='utf-8') path.write('w', content, encoding='utf-8')
def write_directory_index(directory): def make_webpage(items, is_root, doctitle):
rss_link = webpath(ATOM_FILE) if directory == PHOTOGRAPHY_ROOTDIR else None rss_link = f'{PHOTOGRAPHY_WEBROOT}/{ATOM_FILE.basename}' if is_root else None
back_link = webpath(PHOTOGRAPHY_ROOTDIR) if directory != PHOTOGRAPHY_ROOTDIR else None back_link = None if is_root else PHOTOGRAPHY_WEBROOT
sort_reverse = directory == PHOTOGRAPHY_ROOTDIR sort_reverse = is_root
items = list(spinal.walk( html = jinja2.Template('''
directory,
glob_filenames={'*.jpg'},
exclude_filenames={'*_small*'},
recurse=False,
yield_directories=False,
)) + list(directory.listdir_directories())
items2 = []
for item in items:
if item.is_file:
items2.append(Photo(item))
else:
items2.append(Album(item))
items = items2
items.sort(key=lambda item: item.published, reverse=sort_reverse)
page = jinja2.Template('''
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -158,10 +202,229 @@ def write_directory_index(directory):
{% if rss_link %} {% if rss_link %}
<link rel="alternate" type="application/atom+xml" href="{{rss_link}}"/> <link rel="alternate" type="application/atom+xml" href="{{rss_link}}"/>
{% endif %} {% endif %}
<title>{{directory.basename}}</title> <title>{{doctitle}}</title>
<style> <style>
{{css_content}} :root
{
--color_bodybg: #272822;
--color_htmlbg: #1b1c18;
--color_link: #ae81ff;
--color_maintext: #ddd;
--img_borderradius: 16px;
--img_borderradius_tiny: 4px;
--img_sepia: 0%;
--img_huerotate: 0deg;
--img_saturate: 100%;
--img_blur: 0px;
--img_mixblendmode: normal;
}
*, *:before, *:after
{
box-sizing: inherit;
margin: 0;
}
.hidden
{
display: none !important;
}
html
{
height: 100vh;
box-sizing: border-box;
background-color: var(--color_htmlbg);
color: var(--color_maintext);
font-family: Verdana, sans-serif;
font-size: 10pt;
}
body
{
width: 100em;
max-width: 100%;
margin-left: auto;
margin-right: auto;
margin-top: 0;
margin-bottom: 0;
padding: 8px;
padding-bottom:8vh;
display: grid;
grid-auto-flow: row;
grid-row-gap: 12vh;
}
body.noscrollbar::-webkit-scrollbar
{
display: none;
}
body.noscrollbar
{
scrollbar-width: none;
}
a
{
color: var(--color_link);
cursor: pointer;
}
h1
{
text-align: center;
}
article *
{
max-width: 100%;
word-wrap: break-word;
}
header
{
width: 100%;
max-width: 120em;
margin-left: auto;
margin-right: auto;
text-align: end;
}
header > *
{
display: inline-block;
padding: 16px;
background-color: var(--color_bodybg);
}
.album,
.photograph,
.album_tinies
{
position: relative;
margin-left: auto;
margin-right: auto;
}
.album
{
width: 100%;
}
.album .albumphotos
{
width: 100%;
display: grid;
grid-auto-flow: row;
grid-row-gap: 12vh;
}
.photograph
{
width: fit-content;
}
.photograph img
{
display: block;
max-height: 92vh;
border-radius: var(--img_borderradius);
border: 1.25vh solid var(--color_bodybg);
filter: hue-rotate(var(--img_huerotate)) saturate(var(--img_saturate)) blur(var(--img_blur));
mix-blend-mode: var(--img_mixblendmode);
}
.photograph.monochrome img,
.tiny_thumbnail.monochrome img
{
filter: sepia(var(--img_sepia)) hue-rotate(var(--img_huerotate)) saturate(var(--img_saturate)) blur(var(--img_blur));
}
.photograph .number_tag
{
position: absolute;
bottom: 8px;
right: 8px;
text-align: center;
background-color: white;
padding: 1px;
color: black;
text-decoration: none;
font-family: sans-serif;
border-radius: 4px;
font-weight: bold;
opacity: 50%;
}
.album .album_tinies
{
max-width: 80em;
text-align: justify;
}
.tiny_thumbnail
{
vertical-align: middle;
display:inline-block;
margin: 8px;
}
.tiny_thumbnail img
{
aspect-ratio: auto;
border-radius: var(--img_borderradius_tiny);
outline: 4px solid var(--color_bodybg);
filter: hue-rotate(var(--img_huerotate)) saturate(var(--img_saturate)) blur(var(--img_blur));
mix-blend-mode: var(--img_mixblendmode);
}
@media not print
{
.photograph img
{
box-shadow: #000 0px 0px 40px -10px;
}
}
@media screen and (min-width: 600px)
{
.tiny_thumbnail img
{
height: 128px;
}
}
@media screen and (max-width: 600px)
{
.tiny_thumbnail img
{
height: 64px;
}
.photograph
{
box-shadow: none;
}
}
@media not all and (pointer: fine)
{
#keyboardhint,
#scrollbartoggle
{
display: none;
}
}
h1 {font-size: 2.00em;} h1 * {font-size: inherit;}
h2 {font-size: 1.75em;} h2 * {font-size: inherit;}
h3 {font-size: 1.50em;} h3 * {font-size: inherit;}
h4 {font-size: 1.25em;} h4 * {font-size: inherit;}
h5 {font-size: 1.00em;} h5 * {font-size: inherit;}
#a_new_perspective
{
background-color: var(--color_bodybg);
position: fixed;
left: 8px;
padding: 8px;
border-radius: 8px;
}
</style> </style>
</head> </head>
@ -178,9 +441,47 @@ def write_directory_index(directory):
{%- endif -%} {%- endif -%}
</header> </header>
{% if not is_root %}
<h1>{{doctitle}}</h1>
{% endif %}
{% for item in items %} {% for item in items %}
{{item.render_web()}} {{item.render_web(index=loop.index, totalcount=none if is_root else (items|length))}}
{% endfor %} {% endfor %}
<footer>
<p>Ethan Dalool</p>
<p>Contact me: photography@voussoir.net</p>
</footer>
<p><button id="new_perspective_button" onclick="return new_perspective_button_onclick(event);">👁 Try a different perspective</button></p>
<form id="a_new_perspective" class="hidden">
<div><label>Background color: <input type="color" value="#1b1c18" oninput="return backgroundcolor_onchange(event);"/></label></div>
<div><label>Border radius: <input type="range" min="0" value="16" max="500" oninput="return border_radius_onchange(event);"/></label> <span id="border_radius_value">16px</span></div>
<div><label>Saturation: <input type="range" min="0" value="100" max="500" oninput="return saturate_onchange(event);"/></label> <span id="saturate_value">100%</span></div>
<div><label>Hue rotate: <input type="range" min="0" value="0" max="360" oninput="return hue_rotate_onchange(event);"/></label> <span id="hue_rotate_value">0deg</span></div>
<div><label>Blur: <input type="range" min="0" value="0" max="50" oninput="return blur_onchange(event);"/></label> <span id="blur_value">0%</span></div>
<div><label>Mix blend mode: <select onchange="return mixblendmode_onchange(event);">
<option selected>normal</option>
<option>multiply</option>
<option>screen</option>
<option>overlay</option>
<option>darken</option>
<option>lighten</option>
<option>color-dodge</option>
<option>color-burn</option>
<option>hard-light</option>
<option>soft-light</option>
<option>difference</option>
<option>exclusion</option>
<option>hue</option>
<option>saturation</option>
<option>color</option>
<option>luminosity</option>
</select></label></div>
</form>
</body> </body>
<script type="text/javascript"> <script type="text/javascript">
@ -212,70 +513,93 @@ def write_directory_index(directory):
} }
} }
function get_center_img() const SCROLL_STOPS = Array.from(document.querySelectorAll("article.photograph img, .album_tinies"));
function get_center_stop()
{ {
let center_x = window.innerWidth / 2; let center_x = window.innerWidth / 2;
let center_y = window.innerHeight / 2; let center_y = window.pageYOffset + (window.innerHeight / 2);
while (true) let final;
for (const stop of SCROLL_STOPS)
{ {
const element = document.elementFromPoint(center_x, center_y); const stop_top = stop.getBoundingClientRect().top + window.pageYOffset;
if (element.tagName === "IMG") const stop_bottom = stop_top + stop.offsetHeight;
console.log("-----");
console.log(stop);
console.log(`${center_y} versus ${stop_top}--${stop_bottom}`);
console.log("-----");
if (center_y > stop_top)
{ {
return element; final = stop;
} }
center_y -= 20; else
if (center_y <= 0)
{ {
return null; break;
} }
} }
if (final)
{
return final;
}
} }
function next_img(img) function next_img(img)
{ {
const images = Array.from(document.images); const this_index = SCROLL_STOPS.indexOf(img);
const this_index = images.indexOf(img); if (this_index === SCROLL_STOPS.length-1)
if (this_index === images.length-1)
{ {
return img; return img;
} }
return images[this_index + 1]; return SCROLL_STOPS[this_index + 1];
} }
function previous_img(img) function previous_img(img)
{ {
const images = Array.from(document.images); const this_index = SCROLL_STOPS.indexOf(img);
const this_index = images.indexOf(img);
if (this_index === 0) if (this_index === 0)
{ {
return img; return img;
} }
return images[this_index - 1]; return SCROLL_STOPS[this_index - 1];
} }
function scroll_step() function scroll_step()
{ {
const distance = desired_scroll_position - document.body.scrollTop; const distance = desired_scroll_position - document.documentElement.scrollTop;
const jump = (distance * 0.25) + (document.body.scrollTop < desired_scroll_position ? 1 : -1); const jump = (distance * 0.25) + (document.documentElement.scrollTop < desired_scroll_position ? 1 : -1);
document.body.scrollTop = document.body.scrollTop + jump; document.documentElement.scrollTop = document.documentElement.scrollTop + jump;
console.log(`${document.body.scrollTop} ${desired_scroll_position}`); //console.log(`${document.documentElement.scrollTop} ${desired_scroll_position}`);
const new_distance = desired_scroll_position - document.body.scrollTop; const new_distance = desired_scroll_position - document.documentElement.scrollTop;
if (Math.abs(new_distance / distance) < 0.97) if (Math.abs(new_distance / distance) < 0.97)
{ {
window.requestAnimationFrame(scroll_step); window.requestAnimationFrame(scroll_step);
} }
} }
function scroll_to_img(img) function scroll_to_stop(stop)
{ {
const img_centerline = img.getBoundingClientRect().top + img.ownerDocument.defaultView.pageYOffset + (img.offsetHeight / 2); if (stop.offsetHeight > window.innerHeight)
// document.body.scrollTop = img_centerline - (window.innerHeight / 2); {
desired_scroll_position = stop.getBoundingClientRect().top + window.pageYOffset;
}
else
{
const img_centerline = stop.getBoundingClientRect().top + window.pageYOffset + (stop.offsetHeight / 2);
// document.documentElement.scrollTop = img_centerline - (window.innerHeight / 2);
desired_scroll_position = Math.round(img_centerline - (window.innerHeight / 2)); desired_scroll_position = Math.round(img_centerline - (window.innerHeight / 2));
}
scroll_step(); scroll_step();
} }
function scroll_to_next_img() function scroll_to_next_img()
{ {
scroll_to_img(next_img(get_center_img())); const current_stop = get_center_stop();
if (current_stop)
{
scroll_to_stop(next_img(current_stop));
}
} }
function scroll_to_previous_img() function scroll_to_previous_img()
{ {
scroll_to_img(previous_img(get_center_img())); const current_stop = get_center_stop();
if (current_stop)
{
scroll_to_stop(previous_img(current_stop));
}
} }
function arrowkey_listener(event) function arrowkey_listener(event)
{ {
@ -304,6 +628,78 @@ def write_directory_index(directory):
clearTimeout(hide_cursor_timeout); clearTimeout(hide_cursor_timeout);
hide_cursor_timeout = setTimeout(hide_cursor, 3000); hide_cursor_timeout = setTimeout(hide_cursor, 3000);
} }
function new_perspective_button_onclick(event)
{
const p = event.target.parentElement;
document.getElementById("a_new_perspective").classList.remove("hidden");
p.parentElement.removeChild(p);
}
function backgroundcolor_onchange(event)
{
document.documentElement.style.setProperty("--color_htmlbg", event.target.value);
}
function border_radius_onchange(event)
{
if (event.target.value == "500")
{
const value = "100%";
document.documentElement.style.setProperty("--img_borderradius", value);
document.documentElement.style.setProperty("--img_borderradius_tiny", value);
document.getElementById("border_radius_value").textContent = value;
}
else
{
const value = event.target.value + "px";
document.documentElement.style.setProperty("--img_borderradius", value);
document.documentElement.style.setProperty("--img_borderradius_tiny", (event.target.value / 4) + "px");
document.getElementById("border_radius_value").textContent = value;
}
}
function saturate_onchange(event)
{
const value = event.target.value + "%";
document.documentElement.style.setProperty("--img_saturate", value);
if (event.target.value == "0")
{
document.getElementById("saturate_value").textContent = "Artistic";
}
else
{
document.getElementById("saturate_value").textContent = value;
}
}
function hue_rotate_onchange(event)
{
const value = event.target.value + "deg";
if (event.target.value === "0")
{
document.documentElement.style.setProperty("--img_sepia", "0");
}
else
{
document.documentElement.style.setProperty("--img_sepia", "100%");
}
document.documentElement.style.setProperty("--img_huerotate", value);
document.getElementById("hue_rotate_value").textContent = value;
}
function blur_onchange(event)
{
const value = event.target.value + "px";
document.documentElement.style.setProperty("--img_blur", value);
document.getElementById("blur_value").textContent = value;
}
function mixblendmode_onchange(event)
{
document.documentElement.style.setProperty("--img_mixblendmode", event.target.value);
}
function on_pageload() function on_pageload()
{ {
document.documentElement.addEventListener("keydown", arrowkey_listener); document.documentElement.addEventListener("keydown", arrowkey_listener);
@ -315,23 +711,20 @@ def write_directory_index(directory):
</script> </script>
</html> </html>
''').render( ''').render(
css_content=CSS_CONTENT, is_root=is_root,
directory=directory, doctitle=doctitle,
rss_link=rss_link, rss_link=rss_link,
back_link=back_link, back_link=back_link,
items=items, items=items,
) )
write(directory.with_child('index.html'), page) return html
if rss_link:
write_atom(items)
def write_atom(items): def write_atom(items):
atom = jinja2.Template(''' atom = jinja2.Template('''
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"> <feed xmlns="http://www.w3.org/2005/Atom">
<title>voussoir.net/photography</title> <title>voussoir.net/photography</title>
<link href="webpath(PHOTOGRAPHY_ROOTDIR)}"/> <link href="https://voussoir.net/photography"/>
<id>voussoir.net/photography</id> <id>voussoir.net/photography</id>
{% for item in items %} {% for item in items %}
@ -343,21 +736,41 @@ def write_atom(items):
'''.strip()).render(items=items) '''.strip()).render(items=items)
write(ATOM_FILE, atom) write(ATOM_FILE, atom)
def make_thumbnail(photo): # write_directory_index(PHOTOGRAPHY_ROOTDIR)
small_name = photo.replace_extension('').basename + '_small' # for directory in PHOTOGRAPHY_ROOTDIR.walk_directories():
small_name = photo.parent.with_child(small_name).add_extension(photo.extension) # write_directory_index(directory)
if small_name.is_file:
return small_name
image = PIL.Image.open(photo.absolute_path)
icc = image.info.get('icc_profile')
(image_width, image_height) = image.size
exif = image.getexif()
(width, height) = imagetools.fit_into_bounds(image_width, image_height, 1440, 1440)
image = image.resize((width, height), PIL.Image.LANCZOS)
image.save(small_name.absolute_path, quality=75, exif=exif, icc_profile=icc)
print(small_name)
return small_name
write_directory_index(PHOTOGRAPHY_ROOTDIR) @vlogging.main_decorator
for directory in PHOTOGRAPHY_ROOTDIR.walk_directories(): def main(argv):
write_directory_index(directory) singlephotos = list(pdb.search(tag_mays=[PUBLISH_TAGNAME], has_albums=False, yield_albums=False, yield_photos=True).results)
singlephotos += list(pdb.search(tag_mays=['voussoir_net_publish_single'], yield_albums=False, yield_photos=True).results)
singlephotos = [Photo(p) for p in singlephotos]
singlephotos.sort(key=lambda i: i.published, reverse=True)
albums = list(pdb.search(tag_musts=[PUBLISH_TAGNAME], tag_forbids=['voussoir_net_publish_single'], has_albums=True, yield_albums=True, yield_photos=False).results)
albums = [Album(a) for a in albums]
albums.sort(key=lambda i: i.published, reverse=True)
items = singlephotos + albums
items.sort(key=lambda i: i.published, reverse=True)
for item in items:
item.prepare()
log.info('Writing homepage')
homepage_html = make_webpage(items, is_root=True, doctitle='photography')
homepage_file = PHOTOGRAPHY_ROOTDIR.with_child('photography.html')
homepage_file.write('w', homepage_html)
for album in albums:
album_html = make_webpage(album.photos, is_root=False, doctitle=album.article_id)
album_file = PHOTOGRAPHY_ROOTDIR.with_child(album.article_id).replace_extension('html')
log.info('Writing %s', album_file.absolute_path)
album_file.write('w', album_html)
write_atom(items)
return 0
if __name__ == '__main__':
raise SystemExit(main(sys.argv[1:]))