diff --git a/.gitignore b/.gitignore index 081abfac..108061b6 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ __pycache__ /local-config.ini /code/archive/** /code/lgw-import +/code/html/images-download diff --git a/code/backlog.txt b/code/backlog.txt index 0d3e0394..7353e745 100644 --- a/code/backlog.txt +++ b/code/backlog.txt @@ -10,6 +10,8 @@ https://github.com/eduard-permyakov/permafrost-engine https://github.com/codenamecpp/carnage3d https://github.com/WohlSoft/PGE-Project https://github.com/wesnoth/haldric +https://github.com/project-imprimis/imprimis +https://github.com/WohlSoft/PGE-Project https://github.com/mgerhardy/engine https://github.com/GDQuest/godot-visual-effects https://sourceforge.net/projects/freetrain/ diff --git a/code/generate_static_website.py b/code/generate_static_website.py index 367645a0..f452dce8 100644 --- a/code/generate_static_website.py +++ b/code/generate_static_website.py @@ -17,12 +17,11 @@ Listing: """ -# TODO index.html tiles, content -# TODO index.html image (maybe downloaded and assembled from osgameclones) -# TODO index.html only count games -# TODO Font awesome 4 or others (icons for OS, for Github, Gitlab and maybe others like external link) -# TODO contribute.html tiles? content? -# TODO games: links to licenses (wikipedia) +# TODO index.html content +# TODO index.html only count games and frameworks separately +# TODO more icons? +# TODO contribute.html content +# TODO games: links to licenses (wikipedia) complete # TODO indexes: make categories bold that have a certain amount of entries! # TODO everywhere: style URLs (Github, Wikipedia, Internet archive, SourceForge, ...) # TODO developers: links to games and more information, styles @@ -48,6 +47,11 @@ Listing: # TODO icons: for the main categories (devs, games, statistics, home, ...) # TODO SEO optimizations, google search ... # TODO rel attribute https://www.w3schools.com/TAGS/att_a_rel.asp +# TODO recommended tags, links not going to genre +# TODO @see-home/@see-download (ignore or replace?) +# TODO tooltip of supported systems +# TODO improve or send feedback? +# TODO link dependencies import os import shutil @@ -91,6 +95,15 @@ platform_icon_map = { 'Unspecified': 'device_unknown' } +genre_icon_map = { + 'Action': 'target', + 'Arcade': 'pacman', + 'Visual novel': 'book', + 'Puzzle': 'puzzle-piece', + 'Cards': 'spades', + 'Music': 'music' +} + plurals = {k: k+'s' for k in ('Assets license', 'Contact', 'Code language', 'Code license', 'Developer', 'Download', 'Inspiration', 'Game', 'Keyword', 'Home', 'Organization', 'Platform')} for k in ('Media', 'Play', 'State'): plurals[k] = k @@ -426,7 +439,10 @@ def convert_developers(developers, entries): def create_keyword_tag(keyword): if keyword in c.recommended_keywords: - return make_url(games_by_genres_path, make_text(keyword), '{} games'.format(keyword), 'tag is-info') + if keyword.capitalize() in genre_icon_map: + return make_url(games_by_genres_path, [make_icon(genre_icon_map[keyword.capitalize()]), make_text(keyword)], '{} games'.format(keyword), 'tag is-info') + else: + return make_url(games_by_genres_path, make_text(keyword), '{} games'.format(keyword), 'tag is-info') else: return make_text(keyword, 'tag is-light') @@ -439,9 +455,9 @@ def create_state_texts(states): texts.append(make_text('beta', 'is-size-7 has-text-gray-light')) 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-light'), make_icon('bedtime')]) + texts.append([make_text(inactive[0], 'is-size-7 has-text-gray-light'), make_icon('brightness_3')]) else: - texts.append(make_text('active', 'is-size-7 has-text-weight-bold has-text-info')) + texts.append([make_text('active', 'is-size-7 has-text-weight-bold has-text-info'), make_icon('sun')]) return texts @@ -461,7 +477,7 @@ def convert_entries(entries, inspirations, developers): entry['note'] = make_text(entry['Note'], 'is-italic') # keywords as tags - e = [create_keyword_tag(x) for x in entry['Keyword']] + e = [create_keyword_tag(x.value) for x in entry['Keyword']] entry['keyword'] = make_tags(e) # other normal fields (not technical info) @@ -583,10 +599,11 @@ def generate(entries, inspirations, developers): 'creation-date': datetime.datetime.utcnow() } - # copy bulma css + # copy css utils.copy_tree(os.path.join(c.web_template_path, 'css'), c.web_css_path) - #os.mkdir(c.web_css_path) - #shutil.copy2(os.path.join(c.web_template_path, 'bulma.min.css'), c.web_css_path) + + # collage_image + shutil.copyfile(os.path.join(c.web_template_path, 'collage_games.jpg'), os.path.join(c.web_path, 'collage_games.jpg')) # create Jinja Environment environment = Environment(loader=FileSystemLoader(c.web_template_path), autoescape=True) @@ -657,7 +674,8 @@ def generate(entries, inspirations, developers): # generate frameworks pages for keyword in c.framework_keywords: listing = { - 'title': keyword.capitalize(), + 'title': framework_names[keyword], + 'subtitle': make_url(frameworks_path + ['index.html'], 'Index'), 'items': frameworks_by_type[keyword] } write(template_listing_entries.render(listing=listing), frameworks_path +['{}.html'.format(keyword)]) @@ -689,7 +707,7 @@ def generate(entries, inspirations, developers): index['title'] = make_text('Open source games') index['subtitle'] = make_text('Index by game genre') index['categories'] = genres - index['category-names'] = {k:k for k in index['categories']} + 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'] = 15 write(template_categorical_index.render(index=index), games_by_genres_path) diff --git a/code/html/base.jinja b/code/html/base.jinja index 42e98608..68f95866 100644 --- a/code/html/base.jinja +++ b/code/html/base.jinja @@ -15,9 +15,6 @@ {% for category in index['categories'] %}
-

{{ macros.render_element(index['category-names'][category]) }}

+

{{ macros.render_element(index['category-names'][category]) }}

{%- for entries_column in index['entries'][category] -%}
    {%- for entry in entries_column -%} -
  • {{ macros.render_element(entry['url']) }}{%- if 'tags' in entry -%}{{ macros.render_element(entry['tags']) }}{%- endif -%}
  • +
  • {%- if 'tags' in entry -%}{{ macros.render_element(entry['url']) }}{{ macros.render_element(entry['tags']) }} + {%- else -%} + {{ macros.render_element(entry['url']) }} + {%- endif -%}
  • {%- endfor -%}
diff --git a/code/html/collage_games.jpg b/code/html/collage_games.jpg new file mode 100644 index 00000000..d3053d1a Binary files /dev/null and b/code/html/collage_games.jpg differ diff --git a/code/html/css/fonts/osgl.svg b/code/html/css/fonts/osgl.svg index d9e36803..838b0e20 100644 --- a/code/html/css/fonts/osgl.svg +++ b/code/html/css/fonts/osgl.svg @@ -27,8 +27,14 @@ + + + + + + @@ -39,6 +45,7 @@ + diff --git a/code/html/css/fonts/osgl.ttf b/code/html/css/fonts/osgl.ttf index 79720db6..d67a4acb 100644 Binary files a/code/html/css/fonts/osgl.ttf and b/code/html/css/fonts/osgl.ttf differ diff --git a/code/html/css/fonts/osgl.woff b/code/html/css/fonts/osgl.woff index 426d1a47..b0284cf2 100644 Binary files a/code/html/css/fonts/osgl.woff and b/code/html/css/fonts/osgl.woff differ diff --git a/code/html/css/osgl.min.css b/code/html/css/osgl.min.css index 1bfcd00a..da8f8963 100644 --- a/code/html/css/osgl.min.css +++ b/code/html/css/osgl.min.css @@ -1,9 +1,9 @@ @font-face { font-family: 'osgl'; src: - url('fonts/osgl.ttf?twf6ln') format('truetype'), - url('fonts/osgl.woff?twf6ln') format('woff'), - url('fonts/osgl.svg?twf6ln#osgl') format('svg'); + url('fonts/osgl.ttf?mzgdt7') format('truetype'), + url('fonts/osgl.woff?mzgdt7') format('woff'), + url('fonts/osgl.svg?mzgdt7#osgl') format('svg'); font-weight: normal; font-style: normal; font-display: block; @@ -27,6 +27,12 @@ i { .icon-help:before { content: "\e908"; } +.icon-puzzle-piece:before { + content: "\e918"; +} +.icon-book:before { + content: "\e919"; +} .icon-question:before { content: "\e90f"; } @@ -45,6 +51,12 @@ i { .icon-idea:before { content: "\e90e"; } +.icon-brightness_3:before { + content: "\e914"; +} +.icon-wb_sunny:before { + content: "\e917"; +} .icon-device_unknown:before { content: "\e913"; } @@ -90,12 +102,18 @@ i { .icon-pencil:before { content: "\e907"; } +.icon-music:before { + content: "\e91a"; +} .icon-dice:before { content: "\e915"; } .icon-pacman:before { content: "\e916"; } +.icon-spades:before { + content: "\e91b"; +} .icon-library:before { content: "\e921"; } @@ -126,6 +144,9 @@ i { .icon-earth:before { content: "\e9ca"; } +.icon-sun:before { + content: "\e9d4"; +} .icon-filter:before { content: "\ea5b"; } diff --git a/code/html/index.jinja b/code/html/index.jinja index 966588a7..a70d5314 100644 --- a/code/html/index.jinja +++ b/code/html/index.jinja @@ -6,6 +6,7 @@

Open source games list (OSGL)

{{ macros.render_text(index['subtitle']) }}

+
diff --git a/code/html/listing_entries.jinja b/code/html/listing_entries.jinja index a33b29f9..c9b7b4ab 100644 --- a/code/html/listing_entries.jinja +++ b/code/html/listing_entries.jinja @@ -3,6 +3,9 @@

{{ listing['title'] }}

+ {%- if 'subtitle' in listing -%} +

{{ macros.render_element(listing['subtitle']) }}

+ {%- endif -%} {# iterate over items -#} {% for item in listing['items'] %}
diff --git a/code/osgameclones_download_images_create_collage.py b/code/osgameclones_download_images_create_collage.py index 80efe3ef..7df5a1fd 100644 --- a/code/osgameclones_download_images_create_collage.py +++ b/code/osgameclones_download_images_create_collage.py @@ -7,14 +7,10 @@ import os import requests from PIL import Image from io import BytesIO +import numpy as np -if __name__ == "__main__": - - # paths - root_path = os.path.realpath(os.path.join(os.path.dirname(__file__), os.path.pardir)) - download_path = os.path.join(root_path, 'code', 'html', 'images-download') - output_file = os.path.join(root_path, 'code', 'html', 'collage_games.jpg') +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) @@ -45,8 +41,152 @@ if __name__ == "__main__": # download them all for url in images: - r = requests.get(url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64)'}, - timeout=20, allow_redirects=True) + 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 - if r.status_code == requests.codes.ok: - im = Image.open(BytesIO(r.content)) \ No newline at end of file +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(): + # 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 = [] + for file in files: + im = Image.open(os.path.join(downsized_path, file)) + im = np.asarray(im) + images.append(im) + + # 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)]]) + 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 + # 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 = 20 # 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', 'html', 'images-download') + downsized_path = os.path.join(download_path, 'downsized') + output_file = os.path.join(root_path, 'code', 'html', '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() \ No newline at end of file diff --git a/code/requirements.txt b/code/requirements.txt index 2bfd0809..8221b8d5 100644 --- a/code/requirements.txt +++ b/code/requirements.txt @@ -6,4 +6,5 @@ wikipedia Jinja2 html5lib ruamel.yaml -requests \ No newline at end of file +requests +numpy \ No newline at end of file