update Github information (devs and project stars)
This commit is contained in:
@ -13,9 +13,17 @@
|
||||
<body>
|
||||
{#- navigation bar -#}
|
||||
<nav class="navbar container is-light" aria-label="main navigation">
|
||||
<div class="navbar-menu">
|
||||
<div class="navbar-brand">
|
||||
<a class="navbar-item{% if 'index' in base['active_nav'] %} is-active{% endif %}" href="{{ base['url_to'](['index.html']) }}">{{ macros.render_icon({'class':'home'}) }}<span>OSGL</span></a>
|
||||
<a class="navbar-item" href="https://github.com/Trilarion/opensourcegames">{{ macros.render_icon({'class':'github'}) }}<span>On GitHub</span></a>
|
||||
<a role="button" class="navbar-burger" aria-label="menu" aria-expanded="false" data-target="navbarMenu">
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
<span aria-hidden="true"></span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="navbar-menu is-active">
|
||||
<div class="navbar-start">
|
||||
<a class="navbar-item{% if 'index' in base['active_nav'] %} is-active{% endif %}" href="{{ base['url_to'](['index.html']) }}">{{ macros.render_icon({'class':'home'}) }}<span>Home</span></a>
|
||||
<a class="navbar-item{% if 'games' in base['active_nav'] %} is-active{% endif %}" href="{{ base['url_to'](['games', 'index.html']) }}">{{ macros.render_icon({'class':'dice'}) }}<span>Games</span></a>
|
||||
<div class="navbar-item has-dropdown is-hoverable">
|
||||
<a class="navbar-link{% if 'filter' in base['active_nav'] %} is-active{% endif %}">{{ macros.render_icon({'class':'filter'}) }}<span>Filter</span></a>
|
||||
@ -31,7 +39,6 @@
|
||||
<a class="navbar-item{% if 'inspirations' in base['active_nav'] %} is-active{% endif %}" href="{{ base['url_to'](['inspirations', 'index.html']) }}">{{ macros.render_icon({'class':'bulb'}) }}<span>Inspirations</span></a>
|
||||
<a class="navbar-item{% if 'statistics' in base['active_nav'] %} is-active{% endif %}" href="{{ base['url_to'](['statistics.html']) }}">{{ macros.render_icon({'class':'stats-dots'}) }}<span>Statistics</span></a>
|
||||
<a class="navbar-item{% if 'contribute' in base['active_nav'] %} is-active{% endif %}" href="{{ base['url_to'](['contribute.html']) }}">{{ macros.render_icon({'class':'pencil'}) }}<span>Contribute</span></a>
|
||||
<a class="navbar-item" href="https://github.com/Trilarion/opensourcegames">{{ macros.render_icon({'class':'github'}) }}<span>On GitHub</span></a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
977
code/html/generate_static_website.py
Normal file
977
code/html/generate_static_website.py
Normal file
@ -0,0 +1,977 @@
|
||||
"""
|
||||
Generates the static website
|
||||
|
||||
Uses Jinja2 (see https://jinja.palletsprojects.com/en/2.11.x/)
|
||||
|
||||
Listing:
|
||||
|
||||
- title: top level title
|
||||
- items: list of items
|
||||
- anchor-id, name: title of each item
|
||||
- fields: list of fields in item
|
||||
- type: one of 'linebreak', 'text', 'enumeration'
|
||||
if type == 'text': // macro render_text
|
||||
content: the text to display
|
||||
class: the class of the text modifications (optional)
|
||||
if type == 'enumeration': // macro render_enumeration
|
||||
|
||||
"""
|
||||
|
||||
# TODO game engines should be sorted with frameworks/tools, not with games (they aren't games or are they?)
|
||||
|
||||
# TODO if the only change is a change in last updated, do not change it (we can probably check with git diff for it) or checksums
|
||||
|
||||
# TODO minimize tag usage: jinja template optimization for line breaks and indention and minimal amount of spaces (and size of files) and minimal amount of repetition of tags
|
||||
|
||||
# TODO contribute.html add content
|
||||
|
||||
# TODO more icons - missing categories
|
||||
# TODO replace or remove @notices in entries (maybe different entries format)
|
||||
|
||||
# TODO everywhere: singular, plural (game, entries, items)
|
||||
|
||||
# TODO statistics: better and more statistics with links where possible
|
||||
# TODO statistics: with nice graphics (pie charts in SVG) with matplotlib, seaborn, plotly?
|
||||
# TODO statistics: get it from common statistics generator
|
||||
|
||||
# TODO frameworks: icons for frameworks/libraries/tools
|
||||
|
||||
# TODO filter by category: icons too large (overlapping)
|
||||
|
||||
# TODO games: @see-home/@see-download/@add (ignore or replace?)
|
||||
# TODO games: top 50 list from Github via their stars with download links (add to entries) and with screenshot
|
||||
# TODO games: add screenshot ability (add screenshot to database, at least for top 50)
|
||||
|
||||
# TODO update Bulma
|
||||
|
||||
# TODO everywhere: meta/title tag
|
||||
# TODO everywhere: style URLs (Github, Wikipedia, Internet archive, SourceForge, ...)
|
||||
|
||||
# TODO inspirations: icon full lamp (not contained in icomoon.io)
|
||||
|
||||
# TODO games: developers if more than a single line (collapse, expand?) without JS? (https://stackoverflow.com/questions/41220717/collapse-without-javascript, https://codeconvey.com/html-expand-collapse-text-without-javascript/)
|
||||
# TODO games: technical info (collapse on click)
|
||||
# TODO games: link to dependencies (either if existing or if url)
|
||||
# TODO games: javascript table
|
||||
# TODO games: repositories comments have too much space after( and before )
|
||||
|
||||
# TODO add sitemap
|
||||
|
||||
# TODO mobile view: optimize layout for mobile view
|
||||
|
||||
# TODO languages: C# redirects to C
|
||||
|
||||
# TODO keywords: explain most common ones
|
||||
|
||||
# TODO categories: use icon-text as in https://bulma.io/documentation/elements/icon/ instead of is-large
|
||||
|
||||
# TODO info: show only first line of info (which should be a short description if present)
|
||||
|
||||
# TODO developers: anchors to non-latin written developers do not work (chinese names have simply xxxxx)
|
||||
|
||||
# TODO SEO optimizations, google search ...
|
||||
# TODO sitemap
|
||||
# TODO Google search console
|
||||
# TODO <a> rel attribute https://www.w3schools.com/TAGS/att_a_rel.asp
|
||||
|
||||
# TODO inspirations: if included in the database, link instead to game
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import math
|
||||
import datetime
|
||||
import time
|
||||
from functools import partial
|
||||
from utils import osg, constants as c, utils
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
import html5lib
|
||||
|
||||
alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
extra = '0'
|
||||
extended_alphabet = alphabet + extra
|
||||
extended_alphabet_names = {k: k for k in extended_alphabet}
|
||||
alphabet_replacements = {'Á': 'A', 'Å': 'A', 'É': 'E', 'Ł': 'L', 'Ľ': 'L', 'А': 'A', 'Б': 'B', 'Д': 'D', 'И': 'I', 'К': 'K', 'П': 'P'}
|
||||
|
||||
|
||||
games_path = ['games']
|
||||
frameworks_path = ['frameworks']
|
||||
inspirations_path = ['inspirations']
|
||||
developers_path = ['developers']
|
||||
|
||||
games_index_path = games_path + ['index.html']
|
||||
frameworks_index_path = frameworks_path + ['index.html']
|
||||
inspirations_index_path = inspirations_path + ['index.html']
|
||||
developers_index_path = developers_path + ['index.html']
|
||||
|
||||
games_by_language_path = games_path + ['languages.html']
|
||||
games_by_genres_path = games_path + ['genres.html']
|
||||
games_by_platform_path = games_path + ['platforms.html']
|
||||
games_top50_path = games_path + ['top50.html']
|
||||
|
||||
platform_color = {
|
||||
'Windows': 'is-danger',
|
||||
'Linux': 'is-link',
|
||||
'macOS': 'is-success',
|
||||
'Android': 'is-black',
|
||||
'iOS': 'is-primary',
|
||||
'Web': 'is-warning',
|
||||
}
|
||||
|
||||
platform_icon_map = {
|
||||
'Windows': 'windows',
|
||||
'Linux': 'tux',
|
||||
'macOS': 'appleinc',
|
||||
'Android': 'android',
|
||||
'iOS': 'ios',
|
||||
'Web': 'earth',
|
||||
'Unspecified': 'device_unknown'
|
||||
}
|
||||
|
||||
genre_icon_map = {
|
||||
'Action': 'target',
|
||||
'Adventure': 'dice',
|
||||
'Arcade': 'pacman',
|
||||
'Educational': 'graduation-cap',
|
||||
'Game engine': 'car',
|
||||
'Puzzle': 'puzzle-piece',
|
||||
'Remake': 'undo',
|
||||
'Role playing': 'user-secret',
|
||||
'Simulation': 'rocket1',
|
||||
'Strategy': 'fort-awesome',
|
||||
'Cards': 'spades',
|
||||
'Music': 'music',
|
||||
'Visual novel': 'book',
|
||||
'Framework': 'stack',
|
||||
'Library': 'library'
|
||||
}
|
||||
|
||||
plurals = {k: k+'s' for k in ('Assets license', 'Contact', 'Code language', 'Code license', 'Developer', 'Download', 'Inspiration', 'Game', 'Keyword', 'Home', 'Homepage', 'Organization', 'Platform', 'Tag')}
|
||||
for k in ('Media', 'Play', 'Play online', 'State'):
|
||||
plurals[k] = k
|
||||
for k in ('Code repository', 'Code dependency'):
|
||||
plurals[k] = k[:-1] + 'ies'
|
||||
|
||||
code_language_references = {l: games_by_language_path[:-1] + ['{}#{}'.format(games_by_language_path[-1], osg.canonical_name(l))] for l in c.known_languages}
|
||||
|
||||
|
||||
def get_plural_or_singular(name, amount):
|
||||
if not name in plurals.keys():
|
||||
raise RuntimeError('"{}" not a known singular!'.format(name))
|
||||
if amount == 1:
|
||||
return name
|
||||
return plurals[name]
|
||||
|
||||
|
||||
framework_names = {
|
||||
'tool': 'Tools',
|
||||
'framework': 'Frameworks',
|
||||
'library': 'Libraries'
|
||||
}
|
||||
|
||||
|
||||
html5parser = html5lib.HTMLParser(strict=True)
|
||||
|
||||
|
||||
def raise_helper(msg):
|
||||
raise Exception(msg)
|
||||
|
||||
|
||||
def is_list(obj):
|
||||
return isinstance(obj, list)
|
||||
|
||||
|
||||
def write(text, file):
|
||||
"""
|
||||
|
||||
:param text:
|
||||
:param file:
|
||||
"""
|
||||
# validate text
|
||||
if isinstance(file, str):
|
||||
file = [file]
|
||||
try:
|
||||
html5parser.parse(text)
|
||||
except Exception as e:
|
||||
utils.write_text(os.path.join(c.web_path, 'invalid.html'), text) # for further checking with https://validator.w3.org/
|
||||
print('probem with file {}, see invalid.html'.format(file))
|
||||
raise RuntimeError(e)
|
||||
# output file
|
||||
file = os.path.join(c.web_path, *file)
|
||||
# create output directory if necessary
|
||||
containing_dir = os.path.dirname(file)
|
||||
if not os.path.isdir(containing_dir):
|
||||
os.mkdir(containing_dir)
|
||||
# write text
|
||||
utils.write_text(file, text)
|
||||
|
||||
|
||||
def sort_into_categories(list, categories, fit, unknown_category_name=None):
|
||||
"""
|
||||
|
||||
:param list:
|
||||
:param categories:
|
||||
:param fit:
|
||||
:param unknown_category_name:
|
||||
:return:
|
||||
"""
|
||||
categorized_sublists = {}
|
||||
for category in categories:
|
||||
sublist = [item for item in list if fit(item, category)]
|
||||
categorized_sublists[category] = sublist
|
||||
if unknown_category_name:
|
||||
# now those that do not fit
|
||||
sublist = [item for item in list if not any(fit(item, category) for category in categories)]
|
||||
categorized_sublists[unknown_category_name] = sublist
|
||||
return categorized_sublists
|
||||
|
||||
|
||||
def divide_in_columns(categorized_lists, transform):
|
||||
"""
|
||||
|
||||
:param categorized_lists:
|
||||
:param key:
|
||||
:return:
|
||||
"""
|
||||
number_entries = {category: len(categorized_lists[category]) for category in categorized_lists.keys()}
|
||||
entries = {}
|
||||
for category in categorized_lists.keys():
|
||||
e = categorized_lists[category]
|
||||
# transform entry
|
||||
e = [transform(e) for e in e]
|
||||
# divide in three equal lists
|
||||
n = len(e)
|
||||
n1 = math.ceil(n/3)
|
||||
n2 = math.ceil(2*n/3)
|
||||
e = [e[:n1], e[n1:n2], e[n2:]]
|
||||
entries[category] = e
|
||||
return {'number_entries': number_entries, 'entries': entries}
|
||||
|
||||
|
||||
def url_to(current, target, info=None):
|
||||
"""
|
||||
|
||||
:param current: Current path
|
||||
:param target:
|
||||
:return:
|
||||
"""
|
||||
# if it's an absolute url, just return
|
||||
if isinstance(target, str) and any(target.startswith(x) for x in ('http://', 'https://')):
|
||||
return target
|
||||
if isinstance(target, str):
|
||||
target = [target]
|
||||
# split by slash
|
||||
#if current:
|
||||
# current = current.split('/')
|
||||
#target = target.split('/')
|
||||
# reduce by common elements
|
||||
while len(current) > 0 and len(target) > 1 and current[0] == target[0]:
|
||||
current = current[1:]
|
||||
target = target[1:]
|
||||
# add .. as often as length of current still left
|
||||
target = ['..'] * len(current) + target
|
||||
# join by slash again
|
||||
url = '/'.join(target)
|
||||
return url
|
||||
|
||||
|
||||
def preprocess(list, key, url):
|
||||
"""
|
||||
|
||||
:param list:
|
||||
:param key:
|
||||
:return:
|
||||
"""
|
||||
_ = set()
|
||||
for item in list:
|
||||
# add unique anchor ref
|
||||
anchor = osg.canonical_name(item[key])
|
||||
while anchor in _:
|
||||
anchor += 'x'
|
||||
_.add(anchor)
|
||||
item['anchor-id'] = anchor
|
||||
|
||||
# for alphabetic sorting
|
||||
start = item[key][0].upper()
|
||||
# special treatment of some variables
|
||||
start = alphabet_replacements.get(start, start)
|
||||
if not start in alphabet:
|
||||
start = extra
|
||||
item['letter'] = start
|
||||
item['href'] = url + ['{}.html#{}'.format(start, anchor)]
|
||||
|
||||
|
||||
def game_index(entry):
|
||||
e = {
|
||||
'url': make_url(entry['href'], entry['Title']),
|
||||
'anchor-id': entry['anchor-id']
|
||||
}
|
||||
tags = []
|
||||
if 'beta' in entry['State']:
|
||||
tags.append('beta')
|
||||
if osg.is_inactive(entry):
|
||||
tags.append('inactive since {}'.format(osg.extract_inactive_year(entry)))
|
||||
if tags:
|
||||
e['tags'] = make_text('({})'.format(', '.join(tags)), 'is-light is-size-7')
|
||||
return e
|
||||
|
||||
|
||||
def inspiration_index(inspiration):
|
||||
e = {
|
||||
'url': make_url(inspiration['href'], inspiration['Name']),
|
||||
'anchor-id': inspiration['anchor-id'],
|
||||
}
|
||||
n = len(inspiration['Inspired entries'])
|
||||
if n > 1:
|
||||
e['tags'] = make_text('({})'.format(n), 'is-light is-size-7')
|
||||
return e
|
||||
|
||||
|
||||
def developer_index(developer):
|
||||
e = {
|
||||
'url': make_url(developer['href'], developer['Name']),
|
||||
'anchor-id': developer['anchor-id']
|
||||
}
|
||||
n = len(developer['Games'])
|
||||
if n > 1:
|
||||
e['tags'] = make_text('({})'.format(n), 'is-light is-size-7')
|
||||
return e
|
||||
|
||||
|
||||
def shortcut_url(url, name):
|
||||
|
||||
# remove slash at the end
|
||||
if url.endswith('/'):
|
||||
url = url[:-1]
|
||||
|
||||
# gitlab
|
||||
gl_prefix = 'https://gitlab.com/'
|
||||
if url.startswith(gl_prefix):
|
||||
return [make_text(url[len(gl_prefix):]), make_icon('gitlab')]
|
||||
|
||||
# github
|
||||
gh_prefix = 'https://github.com/'
|
||||
if url.startswith(gh_prefix):
|
||||
return [make_text(url[len(gh_prefix):]), make_icon('github')]
|
||||
|
||||
# sourceforge
|
||||
sf_prefix = 'https://sourceforge.net/projects/'
|
||||
if url.startswith(sf_prefix):
|
||||
return [make_text(url[len(sf_prefix):]), make_icon('sourceforge')]
|
||||
|
||||
# archive link
|
||||
ia_prefix = 'https://web.archive.org/web/'
|
||||
if url.startswith(ia_prefix):
|
||||
return 'Archive: ' + url[len(ia_prefix):]
|
||||
|
||||
# Wikipedia link
|
||||
wp_prefix = 'https://en.wikipedia.org/wiki/'
|
||||
if url.startswith(wp_prefix):
|
||||
# return 'WP: ' + url[len(wp_prefix):]
|
||||
return [make_text(name), make_icon('wikipedia')]
|
||||
|
||||
# cutoff common prefixes
|
||||
for prefix in ('http://', 'https://'):
|
||||
if url.startswith(prefix):
|
||||
return url[len(prefix):]
|
||||
# as is
|
||||
return url
|
||||
|
||||
|
||||
def make_url(href, content, title=None, css_class=None):
|
||||
if isinstance(content, str):
|
||||
content = make_text(content)
|
||||
url = {
|
||||
'type': 'url',
|
||||
'href': href,
|
||||
'content': content
|
||||
}
|
||||
if title:
|
||||
url['title'] = title
|
||||
if css_class:
|
||||
url['class'] = css_class
|
||||
return url
|
||||
|
||||
|
||||
def make_repo_url(x, name):
|
||||
# parse comments
|
||||
comments = []
|
||||
if x.has_comment():
|
||||
for c in x.comment.split(','):
|
||||
c = c.strip()
|
||||
if not c.startswith('@'):
|
||||
continue
|
||||
c = c.split(' ')
|
||||
key = c[0][1:] # without the @
|
||||
if len(c) > 1:
|
||||
value = c[1]
|
||||
if key == 'archived':
|
||||
comments.append(make_text('archived', css_class='is-size-7'))
|
||||
if key == 'created':
|
||||
comments.append(make_text('since {}'.format(value), css_class='is-size-7'))
|
||||
if key == 'stars':
|
||||
value = int(value)
|
||||
if value > 200:
|
||||
comments.append(make_icon('star', 'top rated'))
|
||||
elif value > 30:
|
||||
comments.append(make_icon('star-half-full', 'medium rated'))
|
||||
else:
|
||||
comments.append(make_icon('star-o', 'low rated'))
|
||||
# this is the default element
|
||||
url = make_url(x.value, shortcut_url(x.value, name), css_class='is-size-7')
|
||||
if comments:
|
||||
return make_enumeration([url, make_enclose(make_enumeration(comments), '(', ')')], '')
|
||||
else:
|
||||
return url
|
||||
|
||||
|
||||
def make_icon(css_class, title=None):
|
||||
icon = {
|
||||
'type': 'icon',
|
||||
'class': css_class,
|
||||
}
|
||||
if title:
|
||||
icon['title'] = title
|
||||
return icon
|
||||
|
||||
|
||||
def make_text(content, css_class=None):
|
||||
text = {
|
||||
'type': 'text',
|
||||
'text': content
|
||||
}
|
||||
if css_class:
|
||||
text['class'] = css_class
|
||||
return text
|
||||
|
||||
|
||||
def make_nothing():
|
||||
return {
|
||||
'type': 'nothing'
|
||||
}
|
||||
|
||||
|
||||
def make_enclose(entry, left, right):
|
||||
enclose = {
|
||||
'type': 'enclose',
|
||||
'entry': entry,
|
||||
'left': left,
|
||||
'right': right
|
||||
}
|
||||
return enclose
|
||||
|
||||
|
||||
def make_enumeration(entries, divider=', '):
|
||||
enumeration = {
|
||||
'type': 'enumeration',
|
||||
'entries': entries,
|
||||
'divider': divider
|
||||
}
|
||||
return enumeration
|
||||
|
||||
|
||||
def make_tags(entries):
|
||||
return {
|
||||
'type': 'tags',
|
||||
'enumeration': make_enumeration(entries, divider='')
|
||||
}
|
||||
|
||||
|
||||
def developer_profile_link(link):
|
||||
if link.endswith('@SF'):
|
||||
return make_url('https://sourceforge.net/u/{}/profile/'.format(link[:-3]), make_icon('sourceforge'), 'Profile on Sourceforge')
|
||||
if link.endswith('@GH'):
|
||||
return make_url('https://github.com/{}'.format(link[:-3]), make_icon('github'), 'Profile on Github')
|
||||
if link.endswith('@GL'):
|
||||
return make_url('https://gitlab.com/{}'.format(link[:-3]), make_icon('gitlab'), 'Profile on Gitlab')
|
||||
if link.endswith('@BB'):
|
||||
return make_url('https://bitbucket.org/{}/'.format(link[:-3]), make_icon('bitbucket'), 'Profile on BitBucket')
|
||||
raise RuntimeError('Unknown profile link {}'.format(link))
|
||||
|
||||
|
||||
def convert_inspirations(inspirations, entries):
|
||||
entries_references = {entry['Title']:entry['href'] for entry in entries}
|
||||
for inspiration in inspirations:
|
||||
name = inspiration['Name']
|
||||
inspiration['name'] = name
|
||||
|
||||
# media
|
||||
if 'Media' in inspiration:
|
||||
entries = inspiration['Media']
|
||||
entries = [make_url(url, shortcut_url(url, name)) for url in entries]
|
||||
inspiration['media'] = [make_text('Media: '), make_enumeration(entries)]
|
||||
|
||||
# inspired entries (with links to them)
|
||||
inspired_entries = inspiration['Inspired entries']
|
||||
entries = [make_url(entries_references[entry], make_text(entry, 'has-text-weight-semibold')) for entry in inspired_entries]
|
||||
name = make_text('Inspired {}: '.format(get_plural_or_singular('Game', len(entries)).lower()), 'has-text-weight-semibold')
|
||||
inspiration['inspired'] = [name, make_enumeration(entries)]
|
||||
|
||||
|
||||
def convert_developers(developers, entries):
|
||||
entries_references = {entry['Title']:entry['href'] for entry in entries}
|
||||
for developer in developers:
|
||||
name = developer['Name']
|
||||
developer['name'] = name
|
||||
|
||||
# games
|
||||
developed_entries = developer['Games']
|
||||
entries = [make_url(entries_references[entry], make_text(entry, 'has-text-weight-semibold')) for entry in developed_entries]
|
||||
name = make_text('Developed {}:'.format(get_plural_or_singular('Game', len(entries)).lower()), 'has-text-weight-semibold')
|
||||
developer['games'] = [name, make_enumeration(entries)]
|
||||
|
||||
# contacts
|
||||
contacts = developer.get('Contact', [])
|
||||
entries = [developer_profile_link(entry) for entry in contacts]
|
||||
developer['contact'] = entries
|
||||
|
||||
# other fields
|
||||
for field in ('Organization',):
|
||||
if field in developer:
|
||||
entries = developer[field]
|
||||
if field in c.url_developer_fields:
|
||||
entries = [make_url(entry, shortcut_url(entry, name)) for entry in entries]
|
||||
else:
|
||||
entries = [make_text(entry) for entry in entries]
|
||||
developer[field.lower()] = [make_text(get_plural_or_singular(field, len(entries))+': '), make_enumeration(entries)]
|
||||
|
||||
|
||||
def create_keyword_tag(keyword):
|
||||
if keyword in c.recommended_keywords:
|
||||
if keyword in c.framework_keywords:
|
||||
url = frameworks_index_path.copy()
|
||||
else:
|
||||
url = games_by_genres_path.copy()
|
||||
url[-1] += '#{}'.format(keyword)
|
||||
if keyword.capitalize() in genre_icon_map:
|
||||
return make_url(url, [make_icon(genre_icon_map[keyword.capitalize()]), make_text(keyword)], '{} games'.format(keyword), 'tag is-info')
|
||||
else:
|
||||
return make_url(url, make_text(keyword), '{} games'.format(keyword), 'tag is-info')
|
||||
else:
|
||||
return make_text(keyword, 'tag is-light')
|
||||
|
||||
|
||||
def create_state_texts(states):
|
||||
texts = []
|
||||
if 'mature' in states:
|
||||
texts.append(make_text('mature', 'is-size-7 has-text-weight-bold has-text-info'))
|
||||
else:
|
||||
texts.append(make_text('beta', 'is-size-7 has-text-gray-lighter'))
|
||||
inactive = [x for x in states if x.startswith('inactive since')]
|
||||
if inactive:
|
||||
texts.append([make_text(inactive[0], 'is-size-7 has-text-gray-lighter'), make_icon('brightness_3')])
|
||||
else:
|
||||
texts.append([make_text('active', 'is-size-7 has-text-weight-bold has-text-info'), make_icon('sun')])
|
||||
return texts
|
||||
|
||||
|
||||
def convert_entries(entries, inspirations, developers):
|
||||
inspirations_references = {inspiration['Name']: inspiration['href'] for inspiration in inspirations}
|
||||
developer_references = {developer['Name']: developer['href'] for developer in developers}
|
||||
for entry in entries:
|
||||
# name
|
||||
name = entry['Title']
|
||||
entry['name'] = name
|
||||
|
||||
# state
|
||||
entry['state'] = create_state_texts(entry['State'])
|
||||
|
||||
# note
|
||||
if 'Note' in entry:
|
||||
entry['note'] = make_text(entry['Note'], 'is-italic')
|
||||
|
||||
# keywords as tags
|
||||
e = [create_keyword_tag(x.value) for x in entry['Keyword']]
|
||||
entry['keyword'] = make_tags(e)
|
||||
|
||||
# other normal fields (not technical info)
|
||||
for field in ('Home', 'Inspiration', 'Media', 'Download', 'Play', 'Developer'):
|
||||
if field in entry:
|
||||
e = entry[field]
|
||||
divider = ', '
|
||||
if isinstance(e[0], osg.osg_parse.ValueWithComment):
|
||||
e = [x.value for x in e]
|
||||
if field == 'Inspiration':
|
||||
e = [make_url(inspirations_references[x], make_text(x, 'has-text-weight-semibold')) for x in e]
|
||||
elif field == 'Developer':
|
||||
if len(e) > 10: # many devs, print smaller
|
||||
e = [make_url(developer_references[x], make_text(x, 'has-text-weight-semibold is-size-7')) for x in e]
|
||||
else:
|
||||
e = [make_url(developer_references[x], make_text(x, 'has-text-weight-semibold')) for x in e]
|
||||
elif field in c.url_fields:
|
||||
e = [make_url(x, shortcut_url(x, name)) for x in e]
|
||||
else:
|
||||
e = [make_text(x) for x in e]
|
||||
if field == 'Home': # Home -> Homepage
|
||||
field = 'Homepage'
|
||||
elif field == 'Play': # Play -> Play online
|
||||
field = 'Play online'
|
||||
namex = make_text('{}: '.format(get_plural_or_singular(field, len(e))), 'has-text-weight-semibold')
|
||||
entry[field.lower()] = [namex, make_enumeration(e, divider)]
|
||||
|
||||
# platforms
|
||||
if 'Platform' in entry:
|
||||
e = entry['Platform']
|
||||
if isinstance(e[0], osg.osg_parse.ValueWithComment):
|
||||
e = [x.value for x in e]
|
||||
else:
|
||||
e = ['Unspecified']
|
||||
e = [make_url(games_by_platform_path[:-1] + ['{}#{}'.format(games_by_platform_path[-1], x.lower())], make_icon(platform_icon_map[x]), x) if x in platform_icon_map else make_text(x, 'is-size-7') for x in e]
|
||||
namex = make_text('{}:'.format(get_plural_or_singular('Platform', len(e))), 'has-text-weight-semibold')
|
||||
entry['platform'] = [namex] + e
|
||||
|
||||
# technical info fields
|
||||
for field in ('Code language', 'Code license', 'Code repository', 'Code dependency', 'Assets license'):
|
||||
if field in entry:
|
||||
e = entry[field]
|
||||
divider = ', '
|
||||
if not e:
|
||||
continue
|
||||
if isinstance(e[0], osg.osg_parse.ValueWithComment) and field != 'Code repository':
|
||||
e = [x.value for x in e]
|
||||
if field == 'Code repository':
|
||||
e = [make_repo_url(x, name) for x in e]
|
||||
elif field == 'Code language':
|
||||
e = [make_url(code_language_references[x], make_text(x, 'is-size-7')) for x in e]
|
||||
elif field == 'Code license' or field == 'Assets license':
|
||||
e = [make_url(c.license_urls[x], x, css_class='is-size-7') if x in c.license_urls else make_text(x, 'is-size-7') for x in e]
|
||||
elif field in c.url_fields:
|
||||
e = [make_url(x, shortcut_url(x, name), css_class='is-size-7') for x in e]
|
||||
else:
|
||||
e = [make_text(x, 'is-size-7') for x in e]
|
||||
namex = make_text('{}: '.format(get_plural_or_singular(field, len(entries))), 'is-size-7')
|
||||
entry[field.lower()] = [namex, make_enumeration(e, divider)]
|
||||
|
||||
# build system
|
||||
field = 'Build system'
|
||||
if field in entry['Building']:
|
||||
e = entry['Building'][field]
|
||||
divider = ', '
|
||||
if isinstance(e[0], osg.osg_parse.ValueWithComment):
|
||||
e = [x.value for x in e]
|
||||
e = [make_url(c.build_system_urls[x], x, css_class='is-size-7') if x in c.build_system_urls else make_text(x, 'is-size-7') for x in e]
|
||||
namex = make_text('{}: '.format(field), 'is-size-7')
|
||||
entry[field.lower()] = [namex, make_enumeration(e, divider)]
|
||||
|
||||
entry['raw-path'] = 'https://raw.githubusercontent.com/Trilarion/opensourcegames/master/entries/' + entry['File']
|
||||
|
||||
|
||||
def add_license_links_to_entries(entries):
|
||||
for entry in entries:
|
||||
licenses = entry['Code license']
|
||||
licenses = [(c.license_urls.get(license.value, ''), license.value) for license in licenses]
|
||||
entry['Code license'] = licenses
|
||||
|
||||
|
||||
def get_top50_games(games):
|
||||
top50_games = []
|
||||
for game in games:
|
||||
# get stars of repositories
|
||||
stars = 0
|
||||
for repo in game.get('Code repository', []):
|
||||
if repo.has_comment():
|
||||
for c in repo.comment.split(','):
|
||||
c = c.strip()
|
||||
if not c.startswith('@'):
|
||||
continue
|
||||
c = c.split(' ')
|
||||
key = c[0][1:] # without the @
|
||||
if len(c) > 1:
|
||||
value = c[1]
|
||||
if key == 'stars':
|
||||
value = int(value)
|
||||
if value > stars:
|
||||
stars = value
|
||||
top50_games.append((game, stars))
|
||||
top50_games.sort(key=lambda x:x[1], reverse=True)
|
||||
top50_games = top50_games[:50]
|
||||
top50_games =[game for game, stars in top50_games]
|
||||
return top50_games
|
||||
|
||||
|
||||
def generate(entries, inspirations, developers):
|
||||
"""
|
||||
|
||||
:param entries:
|
||||
:param inspirations:
|
||||
:param developers:
|
||||
"""
|
||||
|
||||
# split entries in games and frameworks
|
||||
games, frameworks = [], []
|
||||
for entry in entries:
|
||||
(games, frameworks)[any([keyword in entry['Keyword'] for keyword in c.framework_keywords])].append(entry)
|
||||
|
||||
# preprocess
|
||||
preprocess(games, 'Title', games_path)
|
||||
preprocess(frameworks, 'Title', frameworks_path)
|
||||
# TODO preprocess doesn't set the urls for frameworks correctly fix here, do better later
|
||||
for framework in frameworks:
|
||||
keyword = [keyword for keyword in c.framework_keywords if keyword in framework['Keyword']][0]
|
||||
framework['href'] = frameworks_path + ['{}.html#{}'.format(keyword, framework['anchor-id'])]
|
||||
entries = games + frameworks
|
||||
preprocess(inspirations, 'Name', inspirations_path)
|
||||
preprocess(developers, 'Name', developers_path)
|
||||
|
||||
# set internal links up
|
||||
convert_inspirations(inspirations, entries)
|
||||
convert_developers(developers, entries)
|
||||
convert_entries(games, inspirations, developers)
|
||||
convert_entries(frameworks, inspirations, developers)
|
||||
|
||||
# set external links up
|
||||
add_license_links_to_entries(games)
|
||||
|
||||
# sort into categories
|
||||
sorter = lambda item, category: category == item['letter']
|
||||
games_by_alphabet = sort_into_categories(games, extended_alphabet, sorter)
|
||||
inspirations_by_alphabet = sort_into_categories(inspirations, extended_alphabet, sorter)
|
||||
developers_by_alphabet = sort_into_categories(developers, extended_alphabet, sorter)
|
||||
|
||||
genres = [keyword.capitalize() for keyword in c.recommended_keywords if keyword not in c.framework_keywords]
|
||||
genres.sort()
|
||||
games_by_genre = sort_into_categories(games, genres, lambda item, category: category.lower() in item['Keyword'])
|
||||
games_by_platform = sort_into_categories(entries, c.valid_platforms, lambda item, category: category in item.get('Platform', []), 'Unspecified')
|
||||
games_by_language = sort_into_categories(entries, c.known_languages, lambda item, category: category in item['Code language'])
|
||||
frameworks_by_type = sort_into_categories(frameworks, c.framework_keywords, lambda item, category: category in item['Keyword'])
|
||||
|
||||
# extract top 50 Github stars games
|
||||
top50_games = get_top50_games(games)
|
||||
|
||||
|
||||
# base dictionary
|
||||
base = {
|
||||
'title': 'OSGL',
|
||||
'creation-date': datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M')
|
||||
}
|
||||
|
||||
# copy css
|
||||
utils.copy_tree(os.path.join(c.web_template_path, 'css'), c.web_css_path)
|
||||
|
||||
# collage_image and google search console token
|
||||
shutil.copyfile(os.path.join(c.web_template_path, 'collage_games.jpg'), os.path.join(c.web_path, 'collage_games.jpg'))
|
||||
shutil.copyfile(os.path.join(c.web_template_path, 'google1f8a3863114cbcb3.html'), os.path.join(c.web_path, 'google1f8a3863114cbcb3.html'))
|
||||
|
||||
|
||||
# create Jinja Environment
|
||||
environment = Environment(loader=FileSystemLoader(c.web_template_path), autoescape=True)
|
||||
environment.globals['base'] = base
|
||||
environment.globals['raise'] = raise_helper
|
||||
environment.globals['is_list'] = is_list
|
||||
|
||||
# multiple times used templates
|
||||
template_categorical_index = environment.get_template('categorical_index.jinja')
|
||||
template_listing_entries = environment.get_template('listing_entries.jinja')
|
||||
|
||||
# top level folder
|
||||
base['url_to'] = partial(url_to, [])
|
||||
|
||||
# index.html
|
||||
base['active_nav'] = 'index'
|
||||
index = {'subtitle': make_text('Contains information about {} open source games and {} frameworks/tools.'.format(len(games), len(frameworks))) }
|
||||
template = environment.get_template('index.jinja')
|
||||
write(template.render(index=index), ['index.html'])
|
||||
|
||||
# contribute.html
|
||||
base['active_nav'] = 'contribute'
|
||||
template = environment.get_template('contribute.jinja')
|
||||
write(template.render(), ['contribute.html'])
|
||||
|
||||
# statistics
|
||||
base['active_nav'] = 'statistics'
|
||||
|
||||
# preparation
|
||||
template = environment.get_template('statistics.jinja')
|
||||
data = {
|
||||
'title': 'Statistics',
|
||||
'sections': []
|
||||
}
|
||||
|
||||
# build-systems
|
||||
build_systems = []
|
||||
field = 'Build system'
|
||||
for entry in entries:
|
||||
if field in entry['Building']:
|
||||
build_systems.extend(entry['Building'][field])
|
||||
build_systems = [x.value for x in build_systems]
|
||||
|
||||
unique_build_systems = set(build_systems)
|
||||
unique_build_systems = [(l, build_systems.count(l)) for l in unique_build_systems]
|
||||
unique_build_systems.sort(key=lambda x: str.casefold(x[0])) # first sort by name
|
||||
unique_build_systems.sort(key=lambda x: -x[1]) # then sort by occurrence (highest occurrence first)
|
||||
section = {
|
||||
'title': 'Build system',
|
||||
'items': ['{} ({})'.format(*item) for item in unique_build_systems]
|
||||
}
|
||||
data['sections'].append(section)
|
||||
write(template.render(data=data), ['statistics.html'])
|
||||
|
||||
# frameworks folder
|
||||
base['url_to'] = partial(url_to, frameworks_path)
|
||||
base['active_nav'] = 'frameworks'
|
||||
|
||||
# frameworks by type
|
||||
index = divide_in_columns(frameworks_by_type, game_index)
|
||||
index['title'] = make_text('Open source game frameworks/tools')
|
||||
index['subtitle'] = make_text('Index of {} game frameworks/tools'.format(len(frameworks)))
|
||||
index['categories'] = c.framework_keywords
|
||||
index['category-names'] = framework_names
|
||||
index['number_entries_per_category_threshold'] = 0
|
||||
index['entry_bold'] = lambda x: 'tags' not in x
|
||||
index['category-infos'] = {}
|
||||
write(template_categorical_index.render(index=index), frameworks_index_path)
|
||||
|
||||
# generate frameworks pages
|
||||
for keyword in c.framework_keywords:
|
||||
listing = {
|
||||
'title': framework_names[keyword],
|
||||
'subtitle': make_url(frameworks_index_path, 'Index'),
|
||||
'items': frameworks_by_type[keyword]
|
||||
}
|
||||
write(template_listing_entries.render(listing=listing), frameworks_path +['{}.html'.format(keyword)])
|
||||
|
||||
# games folder
|
||||
base['url_to'] = partial(url_to, games_path)
|
||||
base['active_nav'] = 'games'
|
||||
|
||||
# generate games pages
|
||||
for letter in extended_alphabet:
|
||||
listing = {
|
||||
'title': 'Games starting with {}'.format(letter.capitalize()),
|
||||
'items': games_by_alphabet[letter]
|
||||
}
|
||||
write(template_listing_entries.render(listing=listing), games_path + ['{}.html'.format(letter.capitalize())])
|
||||
|
||||
# generate games index
|
||||
index = divide_in_columns(games_by_alphabet, game_index)
|
||||
index['title'] = make_text('Open source games')
|
||||
index['subtitle'] = make_text('Alphabetical index of {} games'.format(len(games)))
|
||||
index['categories'] = extended_alphabet
|
||||
index['category-names'] = extended_alphabet_names
|
||||
index['number_entries_per_category_threshold'] = 20
|
||||
index['entry_bold'] = lambda x: 'tags' not in x
|
||||
index['category-infos'] = {}
|
||||
write(template_categorical_index.render(index=index), games_index_path)
|
||||
|
||||
# genres
|
||||
base['active_nav'] = ['filter', 'genres']
|
||||
index = divide_in_columns(games_by_genre, game_index)
|
||||
index['title'] = make_text('Open source games')
|
||||
index['subtitle'] = make_text('Index by game genre')
|
||||
index['categories'] = genres
|
||||
index['category-names'] = {k:[make_icon(genre_icon_map[k]), make_text(k)] if k in genre_icon_map else make_text(k) for k in index['categories']}
|
||||
index['number_entries_per_category_threshold'] = 25
|
||||
index['entry_bold'] = lambda x: 'tags' not in x
|
||||
index['category-infos'] = {}
|
||||
write(template_categorical_index.render(index=index), games_by_genres_path)
|
||||
|
||||
# games by language
|
||||
base['active_nav'] = ['filter', 'code language']
|
||||
index = divide_in_columns(games_by_language, game_index)
|
||||
index['title'] = 'Open source games and frameworks'
|
||||
index['subtitle'] = make_text('Index by programming language')
|
||||
index['categories'] = c.known_languages
|
||||
index['category-names'] = {k:k for k in index['categories']}
|
||||
index['number_entries_per_category_threshold'] = 15
|
||||
index['entry_bold'] = lambda x: 'tags' not in x
|
||||
index['category-infos'] = {category: make_url(c.language_urls[category], 'Language information', css_class='is-size-7') for category in c.known_languages if category in c.language_urls}
|
||||
write(template_categorical_index.render(index=index), games_by_language_path)
|
||||
|
||||
# games by platform
|
||||
base['active_nav'] = ['filter', 'platforms']
|
||||
index = divide_in_columns(games_by_platform, game_index)
|
||||
index['title'] = 'Open source games and frameworks'
|
||||
index['subtitle'] = make_text('Index by supported platform')
|
||||
index['categories'] = c.valid_platforms + ('Unspecified',)
|
||||
index['category-names'] = {k:[make_icon(platform_icon_map[k]), make_text(k)] for k in index['categories']}
|
||||
index['number_entries_per_category_threshold'] = 15
|
||||
index['entry_bold'] = lambda x: 'tags' not in x
|
||||
index['category-infos'] = {}
|
||||
write(template_categorical_index.render(index=index), games_by_platform_path)
|
||||
|
||||
# top 50 games
|
||||
base['active_nav'] = ['filter', 'top50']
|
||||
listing = {
|
||||
'title': 'GitHub Stars Top 50',
|
||||
'subtitle': '50 highest rated (by stars on Github) open source games in the database',
|
||||
'items': top50_games
|
||||
}
|
||||
write(template_listing_entries.render(listing=listing), games_top50_path)
|
||||
|
||||
# inspirations folder
|
||||
base['url_to'] = partial(url_to, inspirations_path)
|
||||
base['active_nav'] = 'inspirations'
|
||||
|
||||
# inspirations
|
||||
|
||||
# inspirations index
|
||||
index = divide_in_columns(inspirations_by_alphabet, inspiration_index)
|
||||
index['title'] = 'Inspirations'
|
||||
index['subtitle'] = make_text('Alphabetical index of {} games used as inspirations'.format(len(inspirations)))
|
||||
index['categories'] = extended_alphabet
|
||||
index['category-names'] = extended_alphabet_names
|
||||
index['number_entries_per_category_threshold'] = 10
|
||||
index['entry_bold'] = lambda x: 'tags' in x
|
||||
index['category-infos'] = {}
|
||||
write(template_categorical_index.render(index=index), inspirations_index_path)
|
||||
|
||||
# inspirations single pages
|
||||
template_listing_inspirations = environment.get_template('listing_inspirations.jinja')
|
||||
for letter in extended_alphabet:
|
||||
listing = {
|
||||
'title': 'Inspirations ({})'.format(letter.capitalize()),
|
||||
'items': inspirations_by_alphabet[letter]
|
||||
}
|
||||
write(template_listing_inspirations.render(listing=listing), inspirations_path + ['{}.html'.format(letter.capitalize())])
|
||||
|
||||
# developers folder
|
||||
base['url_to'] = partial(url_to, developers_path)
|
||||
base['active_nav'] = 'developers'
|
||||
|
||||
# developers single pages
|
||||
template_listing_developers = environment.get_template('listing_developers.jinja')
|
||||
for letter in extended_alphabet:
|
||||
listing = {
|
||||
'title': 'Open source game developers ({})'.format(letter.capitalize()),
|
||||
'items': developers_by_alphabet[letter]
|
||||
}
|
||||
write(template_listing_developers.render(listing=listing), developers_path + ['{}.html'.format(letter.capitalize())])
|
||||
|
||||
# developers index
|
||||
index = divide_in_columns(developers_by_alphabet, developer_index)
|
||||
index['title'] = 'Open source game developers'
|
||||
index['subtitle'] = make_text('Alphabetical index of {} developers'.format(len(developers)))
|
||||
index['categories'] = extended_alphabet
|
||||
index['category-names'] = extended_alphabet_names
|
||||
index['number_entries_per_category_threshold'] = 10
|
||||
index['entry_bold'] = lambda x: 'tags' in x
|
||||
index['category-infos'] = {}
|
||||
write(template_categorical_index.render(index=index), developers_index_path)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
start_time = time.process_time()
|
||||
|
||||
# clean the output directory
|
||||
print('clean current static website')
|
||||
utils.recreate_directory(c.web_path)
|
||||
|
||||
# load entries, inspirations and developers and sort them
|
||||
print('load entries, inspirations and developers')
|
||||
entries = osg.read_entries()
|
||||
entries.sort(key=lambda x: str.casefold(x['Title']))
|
||||
|
||||
inspirations = osg.read_inspirations()
|
||||
inspirations = list(inspirations.values())
|
||||
inspirations.sort(key=lambda x: str.casefold(x['Name']))
|
||||
|
||||
developers = osg.read_developers()
|
||||
developers = list(developers.values())
|
||||
developers.sort(key=lambda x: str.casefold(x['Name']))
|
||||
|
||||
# re-generate static website
|
||||
print('re-generate static website')
|
||||
generate(entries, inspirations, developers)
|
||||
|
||||
# timing
|
||||
print('took {:.3f}s'.format(time.process_time()-start_time))
|
1
code/html/google1f8a3863114cbcb3.html
Normal file
1
code/html/google1f8a3863114cbcb3.html
Normal file
@ -0,0 +1 @@
|
||||
google-site-verification: google1f8a3863114cbcb3.html
|
202
code/html/osgameclones_download_images_create_collage.py
Normal file
202
code/html/osgameclones_download_images_create_collage.py
Normal file
@ -0,0 +1,202 @@
|
||||
"""
|
||||
Downloads images from games, stored in the osgameclones-database, then creates a collage of them.
|
||||
"""
|
||||
|
||||
import ruamel.yaml as yaml
|
||||
import os
|
||||
import requests
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
import numpy as np
|
||||
|
||||
from progress.bar import IncrementalBar
|
||||
|
||||
def download_images():
|
||||
# import the osgameclones data
|
||||
path = os.path.realpath(os.path.join(root_path, os.path.pardir, 'osgameclones.git', 'games'))
|
||||
files = os.listdir(path)
|
||||
|
||||
# iterate over all yaml files in osgameclones/data folder and load contents
|
||||
entries = []
|
||||
for file in files:
|
||||
# read yaml
|
||||
with open(os.path.join(path, file), 'r', encoding='utf-8') as stream:
|
||||
try:
|
||||
_ = yaml.safe_load(stream)
|
||||
except Exception as exc:
|
||||
print(file)
|
||||
raise exc
|
||||
|
||||
# add to entries
|
||||
entries.extend(_)
|
||||
|
||||
print('imported {} entries'.format(len(entries)))
|
||||
|
||||
# collect all image informations
|
||||
images = []
|
||||
for entry in entries:
|
||||
if 'images' in entry:
|
||||
images.extend(entry['images'])
|
||||
|
||||
print('contain {} image links'.format(len(images)))
|
||||
|
||||
# download them all
|
||||
for url in images:
|
||||
name = "".join(x for x in url[5:] if (x.isalnum() or x in '._-'))
|
||||
outfile = os.path.join(download_path, name)
|
||||
if not os.path.isfile(outfile):
|
||||
try:
|
||||
r = requests.get(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64)'},
|
||||
timeout=20, allow_redirects=True)
|
||||
if r.status_code == requests.codes.ok:
|
||||
im = Image.open(BytesIO(r.content))
|
||||
im.save(outfile)
|
||||
print('saved {}'.format(url))
|
||||
except:
|
||||
pass
|
||||
|
||||
def downsize_images():
|
||||
scale_factor = 10
|
||||
for file in os.listdir(download_path):
|
||||
file_path = os.path.join(download_path, file)
|
||||
if not os.path.isfile(file_path):
|
||||
continue
|
||||
outfile = os.path.join(downsized_path, file[:-4]+'.png') # losless storage of downsize image
|
||||
if os.path.isfile(outfile):
|
||||
continue
|
||||
im = Image.open(file_path)
|
||||
if im.mode != 'RGB':
|
||||
print('{} - {}'.format(file, im.mode))
|
||||
continue
|
||||
width = im.width
|
||||
height = im.height
|
||||
if width < target_width * scale_factor or height < target_height * scale_factor:
|
||||
continue
|
||||
box = [(width-target_width*scale_factor)/2, (height-target_height*scale_factor)/2, target_width * scale_factor, target_height * scale_factor]
|
||||
box[2] += box[0]
|
||||
box[3] += box[1]
|
||||
im_resized = im.resize((target_width, target_height), resample=Image.LANCZOS, box=box)
|
||||
im_resized.save(outfile)
|
||||
print('saved {}'.format(file))
|
||||
|
||||
|
||||
def assemble_collage():
|
||||
print('start assembling collage')
|
||||
|
||||
# load all from downsized path
|
||||
files = os.listdir(downsized_path)
|
||||
files = [file for file in files if os.path.isfile(os.path.join(downsized_path, file))]
|
||||
images = []
|
||||
bar = IncrementalBar('Loading', max=len(files))
|
||||
for file in files:
|
||||
im = Image.open(os.path.join(downsized_path, file))
|
||||
im = np.asarray(im)
|
||||
images.append(im)
|
||||
bar.next()
|
||||
bar.finish()
|
||||
|
||||
# compute total amount of light in each image and only keep the N brightest
|
||||
images = [(np.sum(image), image) for image in images]
|
||||
images.sort(key=lambda x: x[0], reverse=True)
|
||||
images = images[:N]
|
||||
images = [x[1] for x in images]
|
||||
|
||||
# compute the average color in each quadrant
|
||||
Cx = int(target_height / 2)
|
||||
Cy = int(target_width / 2)
|
||||
U = [np.mean(image[:Cx, :, :], axis=(1, 2)) for image in images]
|
||||
D = [np.mean(image[Cx:, :, :], axis=(1, 2)) for image in images]
|
||||
R = [np.mean(image[:, :Cy, :], axis=(1, 2)) for image in images]
|
||||
L = [np.mean(image[:, Cy:, :], axis=(1, 2)) for image in images]
|
||||
|
||||
# initially just sort them in randomly
|
||||
map = np.random.permutation(N).reshape((Nx, Ny))
|
||||
|
||||
# optimize neighbors with a stochastic metropolis algorithm
|
||||
Ni = 500000
|
||||
T = np.linspace(150, 2, Ni)
|
||||
A = np.zeros((Ni, 1))
|
||||
u = lambda x: (x + 1) % Nx
|
||||
d = lambda x: (x - 1) % Nx
|
||||
r = lambda x: (x + 1) % Ny
|
||||
l = lambda x: (x - 1) % Ny
|
||||
score = lambda i1, j1, i2, j2: np.linalg.norm(U[map[i1, j1]] - D[map[u(i2), j2]]) + np.linalg.norm(D[map[i1, j1]] - U[map[d(i2), j2]]) + np.linalg.norm(L[map[i1, j1]] - R[map[i2, l(j2)]]) + np.linalg.norm(R[map[i1, j1]] - L[map[i2, r(j2)]])
|
||||
bar = IncrementalBar('Optimization', max=Ni)
|
||||
for ai in range(Ni):
|
||||
# get two non-equal random locations
|
||||
i1 = np.random.randint(Nx)
|
||||
j1 = np.random.randint(Ny)
|
||||
while True:
|
||||
i2 = np.random.randint(Nx)
|
||||
j2 = np.random.randint(Ny)
|
||||
if i1 != i2 or j1 != j2:
|
||||
break
|
||||
# compute score
|
||||
x = score(i1, j1, i1, j1) - score(i1, j1, i2, j2) + score(i2, j2, i2, j2) - score(i2, j2, i1, j1)
|
||||
|
||||
# exchange
|
||||
# if x < 0:
|
||||
# if x > 0:
|
||||
if x > 0 or np.exp(x / T[ai]) > np.random.uniform():
|
||||
map[i1, j1], map[i2, j2] = map[i2, j2], map[i1, j1]
|
||||
A[ai] = 1
|
||||
|
||||
bar.next()
|
||||
bar.finish()
|
||||
# time evolution of acceptance rate
|
||||
Nc = int(np.floor(Ni / 20))
|
||||
for ai in range(20):
|
||||
print('{}: {}'.format(ai, np.mean(A[ai*Nc:(ai+1)*Nc])))
|
||||
|
||||
# shift brightest to center
|
||||
B = np.zeros((Nx, Ny))
|
||||
for i in range(Nx):
|
||||
for j in range(Ny):
|
||||
B[i, j] = np.sum(images[map[i, j]])
|
||||
sk = np.array([0.25, 0.5, 1, 0.5, 0.25])
|
||||
# convolve in 1D along all rows and all columns
|
||||
for i in range(Nx):
|
||||
B[i, :] = np.convolve(B[i, :], sk, mode='same')
|
||||
for j in range(Ny):
|
||||
B[:, j] = np.convolve(B[:, j], sk, mode='same')
|
||||
cx, cy = np.unravel_index(np.argmax(B), B.shape)
|
||||
map = np.roll(map, (int(Nx/2-cx), int(Ny/2-cy)), axis=(0, 1))
|
||||
|
||||
# assemble image
|
||||
final = np.zeros((Nx * target_height, Ny * target_width, 3), dtype=np.uint8)
|
||||
for i in range(Nx):
|
||||
for j in range(Ny):
|
||||
final[i*target_height:(i+1)*target_height, j*target_width:(j+1)*target_width] = images[map[i, j]]
|
||||
|
||||
# convert back to pillow image and save
|
||||
im = Image.fromarray(final)
|
||||
im.save(output_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
target_height = 60
|
||||
target_width = 80
|
||||
|
||||
Nx = 12 # vertical
|
||||
Ny = 18 # horizontal
|
||||
N = Nx * Ny
|
||||
|
||||
# paths
|
||||
root_path = os.path.realpath(os.path.join(os.path.dirname(__file__), os.path.pardir))
|
||||
download_path = os.path.join(root_path, 'code', '', 'images-download')
|
||||
downsized_path = os.path.join(download_path, 'downsized')
|
||||
output_file = os.path.join(root_path, 'code', '', 'collage_games.jpg')
|
||||
if not os.path.exists(download_path):
|
||||
os.mkdir(download_path)
|
||||
if not os.path.exists(downsized_path):
|
||||
os.mkdir(downsized_path)
|
||||
|
||||
# download files
|
||||
# download_images()
|
||||
|
||||
# downsize downloaded images
|
||||
# downsize_images()
|
||||
|
||||
# assemble collage
|
||||
assemble_collage()
|
80
code/html/static_website_design.md
Normal file
80
code/html/static_website_design.md
Normal file
@ -0,0 +1,80 @@
|
||||
# Design of the static website
|
||||
|
||||
The website is built with the parsed entries, developers and inspirations read in Python, then a script uses Jinja templates
|
||||
to generate html pages which use a CSS framework and Javascript data tables. The finished site is pushed to a suitable
|
||||
location, only changed content would need to be copied though.
|
||||
|
||||
## Pages
|
||||
|
||||
index.html - overview of all pages
|
||||
contribute.html - information how to edit and contribute
|
||||
|
||||
games/index.html - overview of all games (with recommended keywords) sorted alphabetically
|
||||
games/table.html - overview of all games as table
|
||||
games/[A-Z].html - entries sorted by title and categorized alphabetically
|
||||
games/genres.html - all games in a certain genre
|
||||
games/languages.html - all games with a certain language
|
||||
games/platform.html - all games with a certain platform
|
||||
games/dependencies.html - all games with a certain dependency
|
||||
|
||||
inspirations/index.html - overview of all inspirations (with number of games inspired) sorted alphabetically
|
||||
inspirations/table.html - overview of all inspirations as table
|
||||
inspirations/[A-Z].html - inspirations sorted by title and categorized alphabetically
|
||||
|
||||
developers/index.html - overview of all developers (with number of games created) sorted alphabetically
|
||||
developers/table.html - overview of all developers as table
|
||||
developers/[A-Z].html - developers sorted by name and categorized alphabetically
|
||||
|
||||
statistics/index.html - overview of statistics
|
||||
statistics/keywords.html - statistics of keywords (links to genres/xx)
|
||||
statistics/state.html - statistics of inactive games
|
||||
statistics/languages.html - statistics of languages (links to languages/xx)
|
||||
statistics/licenses.html - statistics of licenses
|
||||
statistics/dependencies.html - statistics of code dependencies (links to dependencies/xx)
|
||||
statistics/build-systems.html - statistics of build systems
|
||||
|
||||
## Header/Footer
|
||||
|
||||
Header: link to overview, link to contribute, link to Github
|
||||
Footer: link to Blog, link to overviews, link to Github
|
||||
|
||||
## Pages structure
|
||||
|
||||
### Game entry
|
||||
|
||||
- Title (anchor) -- [edit] (aligned right, forwards to contribute)
|
||||
- Genre, Platform (say "unknown" if unknown), State
|
||||
- Home (main website)
|
||||
- Secondary homes: (includes code repository)
|
||||
- Inspirations: (optional)
|
||||
- Media: (optional)
|
||||
- Download: (optional)
|
||||
- Play: (optional)
|
||||
- Other keywords: (optional)
|
||||
- Developer: (optional)
|
||||
- Note: (optional)
|
||||
|
||||
Technical info (hidden initially, can be toggled on/off)
|
||||
|
||||
- Code language
|
||||
- Code repository
|
||||
- Code license
|
||||
- Code dependencies (optional)
|
||||
- Build system/information (optional)
|
||||
- Assets (optional)
|
||||
|
||||
### Inspiration entry
|
||||
|
||||
- Title (anchor) -- [edit]
|
||||
- Media: (optional)
|
||||
- Inspired entries: (with links)
|
||||
|
||||
### Developer entry
|
||||
|
||||
- Name (anchor) -- [edit]
|
||||
- Games: (with links)
|
||||
- Contact: links to profiles on SourceForge, GitHub, .. converted to links
|
||||
|
||||
## Overviews
|
||||
|
||||
Simple paragraphs with headers and columns (for example game names in three columns)
|
Reference in New Issue
Block a user