├── .gitignore ├── README.md ├── ads.txt ├── app.yaml ├── config └── __init__.py ├── fix_path.py ├── handlers ├── __init__.py ├── base.py ├── error.py ├── gallery.py ├── image.py ├── login.py ├── oauth.py ├── piskel.py ├── redirect.py ├── simple.py ├── user.py ├── user_settings.py └── user_stats.py ├── index.yaml ├── lib ├── httplib2 │ └── httplib2 │ │ ├── __init__.py │ │ ├── cacerts.txt │ │ ├── iri2uri.py │ │ └── socks.py ├── oauth2 │ └── oauth2 │ │ ├── __init__.py │ │ └── _version.py └── simpleauth │ └── simpleauth │ ├── __init__.py │ └── handler.py ├── main.py ├── models ├── __init__.py ├── framesheet.py ├── image.py ├── piskel.py └── piskel_user.py ├── robots.txt ├── static ├── css │ ├── piskel-app-download.css │ ├── piskel-app-editor.css │ ├── piskel-app-faq.css │ ├── piskel-app-home.css │ ├── piskel-app-legal.css │ ├── piskel-app-piskel-history.css │ ├── piskel-app-piskel.css │ ├── piskel-app-previewcard.css │ ├── piskel-app-user-settings.css │ ├── piskel-app-user.css │ └── piskel-app.css ├── editor │ ├── .gitignore │ └── VERSION ├── google392c8e9117089539.html ├── js │ ├── piskel-animated-preview.js │ ├── piskel-details-page.js │ ├── piskel-image-scale.js │ ├── piskel-nav.js │ ├── piskel-user-settings.js │ ├── piskel-user.js │ └── piskel-utils.js └── resources │ ├── animated-preview-checker-background.png │ ├── bio-icon.svg │ ├── calendar-icon.svg │ ├── default-avatar.gif │ ├── download │ ├── download-linux-logo.png │ ├── download-linux-logo.svg │ ├── download-offline.png │ ├── download-osx-logo.png │ ├── download-osx-logo.svg │ ├── download-windows-logo.png │ ├── download-windows-logo.svg │ └── offline.svg │ ├── error │ └── 403 │ │ └── 403-guy.png │ ├── faq │ ├── faq-support-icon-200.png │ └── faq-support-icon.png │ ├── favicon-v2.png │ ├── favicon.ico │ ├── favicon.png │ ├── footer │ ├── footer-item-github-logo.png │ ├── footer-item-support.png │ └── footer-item-twitter-logo.png │ ├── gallery-icon.png │ ├── gallery-icon.svg │ ├── gallery-icon@2x.png │ ├── github_octocat.gif │ ├── home │ ├── featured │ │ ├── checkered-background.png │ │ ├── joakim.jpg │ │ ├── julian.jpg │ │ ├── llama.png │ │ ├── llama_preview.png │ │ ├── megaman.png │ │ ├── megaman_preview.png │ │ ├── panda.png │ │ ├── panda_preview.png │ │ ├── patrick.jpg │ │ ├── pattern.png │ │ ├── pattern_preview.png │ │ ├── snakes.png │ │ ├── snakes_preview.png │ │ ├── stormtrooper.png │ │ └── vincent.jpg │ ├── features │ │ ├── export.svg │ │ ├── feature-export-gold@2x.png │ │ ├── feature-export@2x.png │ │ ├── feature-google-sign-in.png │ │ ├── feature-google-sign-in@2x.png │ │ ├── feature-live-preview.gif │ │ ├── feature-offline-application.png │ │ ├── feature-open-source@2x.gif │ │ ├── feature-private-gallery-gold@2x.png │ │ └── private-gallery.svg │ ├── megamanx_animated.gif │ ├── megamanx_animated@2x.gif │ ├── megamanx_preview.jpg │ ├── megamanx_preview.png │ ├── megamanx_spritesheet.png │ ├── preview_gif_megaman.png │ ├── screenshot_with_browser_ui_generic_1200.jpg │ ├── screenshot_with_browser_ui_generic_1200.png │ ├── screenshot_with_browser_ui_generic_800.png │ └── screenshot_with_browser_ui_generic_full.png │ ├── iconmonstr-globe-4-icon.svg │ ├── iconmonstr-lock-3-icon.svg │ ├── logo_transparent_big_compact.png │ ├── logo_transparent_small_compact.png │ ├── logout-icon.png │ ├── logout-icon.svg │ ├── logout-icon@2x.png │ ├── logout.svg │ ├── pacman-loader.gif │ ├── settings-icon.png │ ├── settings-icon@2x.png │ ├── unavailable.png │ └── user-icon.svg └── templates ├── ads ├── piskel_home_footer.html └── piskel_skyscraper.html ├── adsense.html ├── analytics.html ├── base.html ├── default.html ├── download └── download.html ├── editor.html ├── editor ├── boot-partial.html └── header-partial.html ├── error ├── http-404.html ├── http-500.html ├── piskel-private.html └── piskel-unauthorized-action.html ├── faq └── faq.html ├── footer.html ├── gallery ├── list.html ├── navigation.html ├── pagination.html └── piskelcard.html ├── home ├── features.html ├── home.html └── picked.html ├── login.html ├── nav.html ├── partial ├── macros.html └── nav-actions-partial.html ├── piskel ├── piskel-details-edit.html ├── piskel-details.html └── piskel-history.html ├── privacy └── privacy.html ├── social ├── piskel-opengraph-card-private.html └── piskel-opengraph-card.html ├── terms └── terms.html └── user ├── user-piskelcard.html ├── user-settings.html └── user.html /.gitignore: -------------------------------------------------------------------------------- 1 | # mac artefacts 2 | *.DS_Store 3 | .vscode 4 | 5 | # nodejs local installs 6 | node_modules 7 | npm-debug.log 8 | 9 | # sublime text stuff (the -project should actually be shared, but then we'd have to share the same disk location) 10 | *.sublime-project 11 | *.sublime-workspace 12 | 13 | # git stackdumps 14 | *.stackdump 15 | 16 | # compiled files 17 | *.pyc 18 | 19 | # secret file ! 20 | config/secrets.py 21 | 22 | # grunt generated files 23 | templates/editor/main-partial.html 24 | 25 | # vscode 26 | settings.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | piskel-website 2 | ============== 3 | 4 | UI Tests 5 | -------- 6 | 7 | __Profile page__ 8 | ![Profile 5](http://screenletstore.appspot.com/img/67840d2b-01c7-11e3-b6c6-2100f77fad17.png "Profile 5") 9 | ![Profile 4](http://screenletstore.appspot.com/img/0286cb57-f24a-11e2-8938-a9c727762163.png "Profile 4") 10 | ![Profile 3](http://screenletstore.appspot.com/img/7e19c1dc-f158-11e2-81eb-0d40971d4c97.png "Profile 3") 11 | ![Proposal BenC_black](http://screenletstore.appspot.com/img/8d09154f-edeb-11e2-8bdd-27c145d9d344.png "Proposal BenC_black") 12 | ![Profile 1](http://screenletstore.appspot.com/img/28f2954c-eda9-11e2-b907-19bebcbd9685.png "Profile 1") 13 | ![Profile 2](http://screenletstore.appspot.com/img/641cf6ba-eda9-11e2-a677-19bebcbd9685.png "Profile 2") 14 | 15 | 16 | __Piskel page__ 17 | ___21 07 2013___ 18 | ![Piskel 1](http://screenletstore.appspot.com/img/707e732b-f24a-11e2-9a61-a9c727762163.png "Piskel 1") 19 | ![Piskel 1 edit](http://screenletstore.appspot.com/img/4428ad61-f24a-11e2-a013-a9c727762163.png "Piskel 1 edit") 20 | 21 | Front website for the piskel application ! Should allow to easily find animations/sprites made using piskel. 22 | 23 | ![Screenshot 1](https://dl.dropbox.com/u/17803671/screen_piskel_website_1.png "Screenshot 1") 24 | -------------------------------------------------------------------------------- /ads.txt: -------------------------------------------------------------------------------- 1 | google.com, pub-9577407496912049, DIRECT, f08c47fec0942fa0 -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | application: piskel-app 2 | version: beta 3 | runtime: python27 4 | api_version: 1 5 | threadsafe: yes 6 | automatic_scaling: 7 | max_idle_instances: 2 8 | 9 | skip_files: 10 | - ^(.*/)?(\.git/.*)$ 11 | 12 | handlers: 13 | - url: /ads\.txt 14 | static_files: ads.txt 15 | upload: ads\.txt 16 | 17 | - url: /favicon\.ico 18 | static_files: static/resources/favicon.ico 19 | upload: static/resources/favicon\.ico 20 | 21 | - url: /robots\.txt 22 | static_files: robots.txt 23 | upload: robots\.txt 24 | 25 | - url: /google392c8e9117089539\.html 26 | static_files: static/google392c8e9117089539.html 27 | upload: static/google392c8e9117089539\.html 28 | 29 | - url: /static/resources/(.*\.svg) 30 | static_files: static/resources/\1 31 | upload: static/resources/(.*\.svg) 32 | mime_type: image/svg+xml 33 | http_headers: 34 | Cache-Control: public, max-age=31536000 35 | 36 | - url: /static/resources/(.*\.gif) 37 | static_files: static/resources/\1 38 | upload: static/resources/(.*\.gif) 39 | mime_type: image/gif 40 | http_headers: 41 | Cache-Control: public, max-age=31536000 42 | 43 | - url: /static/resources/(.*\.png) 44 | static_files: static/resources/\1 45 | upload: static/resources/(.*\.png) 46 | mime_type: image/png 47 | http_headers: 48 | Cache-Control: public, max-age=31536000 49 | 50 | - url: /static/js/(.*\.js) 51 | static_files: static/js/\1 52 | upload: static/js/(.*\.js) 53 | mime_type: application/javascript 54 | http_headers: 55 | Cache-Control: public, max-age=31536000 56 | 57 | - url: /static/css/(.*\.css) 58 | static_files: static/css/\1 59 | upload: static/css/(.*\.css) 60 | mime_type: text/css 61 | http_headers: 62 | Cache-Control: public, max-age=31536000 63 | 64 | - url: /static 65 | static_dir: static 66 | 67 | - url: /p/(.*\.svg) 68 | static_files: static/editor/\1 69 | upload: static/editor/(.*\.svg) 70 | mime_type: image/svg+xml 71 | http_headers: 72 | Cache-Control: public, max-age=31536000 73 | 74 | - url: /p/(.*\.eot) 75 | static_files: static/editor/\1 76 | upload: static/editor/(.*\.eot) 77 | mime_type: application/vnd.ms-fontobject 78 | http_headers: 79 | Cache-Control: public, max-age=31536000 80 | 81 | - url: /p/(.*\.ttf) 82 | static_files: static/editor/\1 83 | upload: static/editor/(.*\.ttf) 84 | mime_type: application/octet-stream 85 | http_headers: 86 | Cache-Control: public, max-age=31536000 87 | 88 | - url: /p/(.*\.woff) 89 | static_files: static/editor/\1 90 | upload: static/editor/(.*\.woff) 91 | mime_type: application/x-woff 92 | http_headers: 93 | Cache-Control: public, max-age=31536000 94 | 95 | - url: /p/(.*\.png) 96 | static_files: static/editor/\1 97 | upload: static/editor/(.*\.png) 98 | mime_type: image/png 99 | http_headers: 100 | Cache-Control: public, max-age=31536000 101 | 102 | - url: /p/(.*\.(?:css|js)) 103 | static_files: static/editor/\1 104 | upload: static/editor/(.*\.(css|js)) 105 | http_headers: 106 | Cache-Control: public, max-age=31536000 107 | 108 | - url: /.* 109 | script: main.app 110 | secure: always 111 | redirect_http_response_code: 301 112 | 113 | libraries: 114 | - name: webapp2 115 | version: "2.5.1" 116 | 117 | - name: jinja2 118 | version: "2.6" 119 | 120 | - name: lxml 121 | version: "latest" -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/config/__init__.py -------------------------------------------------------------------------------- /fix_path.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | DIR_PATH = os.path.abspath(os.path.dirname(os.path.realpath(__file__))) 5 | 6 | EXTRA_PATHS = [ 7 | DIR_PATH, 8 | os.path.join(DIR_PATH, 'lib', 'httplib2'), 9 | os.path.join(DIR_PATH, 'lib', 'oauth2'), 10 | os.path.join(DIR_PATH, 'lib', 'simpleauth') 11 | ] 12 | 13 | 14 | def fix_sys_path(extra_extra_paths=()): 15 | """Fix the sys.path to include our extra paths.""" 16 | extra_paths = EXTRA_PATHS[:] 17 | extra_paths.extend(extra_extra_paths) 18 | sys.path = extra_paths + sys.path 19 | -------------------------------------------------------------------------------- /handlers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/handlers/__init__.py -------------------------------------------------------------------------------- /handlers/base.py: -------------------------------------------------------------------------------- 1 | import webapp2, logging 2 | from webapp2_extras import jinja2, auth, sessions 3 | from config import secrets 4 | 5 | class BaseHandler(webapp2.RequestHandler): 6 | def dispatch(self): 7 | # Get a session store for this request. 8 | self.session_store = sessions.get_store(request=self.request) 9 | 10 | try: 11 | # Dispatch the request. 12 | webapp2.RequestHandler.dispatch(self) 13 | finally: 14 | # Save all sessions. 15 | self.session_store.save_sessions(self.response) 16 | 17 | @webapp2.cached_property 18 | def auth(self): 19 | return auth.get_auth() 20 | 21 | @webapp2.cached_property 22 | def jinja2(self): 23 | # Returns a Jinja2 renderer cached in the app registry. 24 | return jinja2.get_jinja2(app=self.app) 25 | 26 | @webapp2.cached_property 27 | def is_logged_in(self): 28 | """Returns true if a user is currently logged in, false otherwise""" 29 | return self.auth.get_user_by_session() is not None 30 | 31 | @webapp2.cached_property 32 | def username(self): 33 | return self.current_user.name 34 | 35 | @webapp2.cached_property 36 | def session_user(self): 37 | return self.auth.get_user_by_session() 38 | 39 | @webapp2.cached_property 40 | def session_user_id(self): 41 | return self.session_user['user_id'] 42 | 43 | @webapp2.cached_property 44 | def current_user(self): 45 | return self.auth.store.user_model.get_by_id(self.session_user['user_id']) 46 | 47 | def get_user(self, user_id): 48 | return self.auth.store.user_model.get_by_id(user_id) 49 | 50 | def render(self, template, values): 51 | # Add user (auth) specific variables 52 | values.update( 53 | { 54 | 'user': self.current_user if self.is_logged_in else False, 55 | 'session': self.session_user if self.is_logged_in else False, 56 | 'is_logged_in': self.is_logged_in 57 | } 58 | ) 59 | 60 | try: 61 | self.response.write(self.jinja2.render_template(template, **values)) 62 | except Exception, e: 63 | self.abort(404, detail=e) -------------------------------------------------------------------------------- /handlers/error.py: -------------------------------------------------------------------------------- 1 | import os.path, traceback, logging 2 | from base import BaseHandler 3 | 4 | DEFAULT_ERROR = 500 5 | 6 | class ErrorHandler(BaseHandler): 7 | def handle_error(self, exception): 8 | # Determine status code and set it 9 | status_int = hasattr(exception, 'status_int') and exception.status_int or DEFAULT_ERROR 10 | self.response.set_status(status_int) 11 | 12 | # Determine template file 13 | template_name = 'error/http-'+ str(DEFAULT_ERROR) +'.html' 14 | 15 | # Default to error 500 if we don't have template file for the specific error 16 | try: 17 | template_name = self.jinja2.environment.get_template('error/http-'+ str(status_int) +'.html').name 18 | except: 19 | pass 20 | 21 | values = { 22 | 'exception': exception, 23 | 'traceback': traceback.format_exc() 24 | } 25 | 26 | logging.error('Handled error: '+ str(status_int) +', exception message: '+ repr(exception) +', traceback: '+ values['traceback']) 27 | 28 | self.render(template_name, values) -------------------------------------------------------------------------------- /handlers/gallery.py: -------------------------------------------------------------------------------- 1 | from base import BaseHandler 2 | from models.piskel import Piskel 3 | import models 4 | 5 | class BrowseHandler(BaseHandler): 6 | def is_admin(self): 7 | if not self.is_logged_in: 8 | return False 9 | 10 | user_id = self.session_user['user_id'] 11 | user = self.auth.store.user_model.get_by_id(long(user_id)) 12 | return user.is_admin 13 | 14 | def get(self): 15 | self.getPage(1) 16 | 17 | def get_page_piskels(self, index=1): 18 | piskels = models.get_recent_piskels(index) 19 | return Piskel.prepare_piskels_for_view(piskels) 20 | 21 | def getPage(self, index): 22 | if not self.is_admin(): 23 | return self.render('error/piskel-unauthorized-action.html', {}) 24 | 25 | index = int(index) 26 | values = { 27 | 'is_home': False, 28 | 'has_footer' : False, 29 | 'title': 'Browse all', 30 | 'path': '/admin/browse', 31 | 'has_previous_page' : index > 1, 32 | 'has_next_page' : True, 33 | 'index' : index, 34 | 'page_piskels' : self.get_page_piskels(index) 35 | } 36 | 37 | self.render('gallery/list.html', values) 38 | 39 | 40 | class FeaturedHandler(BaseHandler): 41 | def get(self): 42 | self.getPage(1) 43 | 44 | def get_page_piskels(self, index): 45 | piskels = models.get_featured_piskels(index) 46 | return Piskel.prepare_piskels_for_view(piskels) 47 | 48 | def getPage(self, index): 49 | index = int(index) 50 | values = { 51 | 'is_home': False, 52 | 'has_footer' : False, 53 | 'title': 'Featured pixel art', 54 | 'path': '/featured', 55 | 'has_previous_page' : index > 1, 56 | 'has_next_page' : True, 57 | 'index' : index, 58 | 'page_piskels' : self.get_page_piskels(index) 59 | } 60 | 61 | self.render('gallery/list.html', values) 62 | -------------------------------------------------------------------------------- /handlers/image.py: -------------------------------------------------------------------------------- 1 | import re, logging 2 | from google.appengine.api import memcache 3 | from google.appengine.ext import db 4 | from google.appengine.ext import webapp 5 | 6 | from models import image as image_model 7 | 8 | def respond_image(db_image, response): 9 | file_name = db_image.key().name() 10 | 11 | response.headers['Expires'] = 'Thu, 01 Dec 2504 16:00:00 GMT' 12 | response.headers['Content-Type'] = str('image/'+db_image.extension) 13 | 14 | # Cache-Control 15 | # public means the cached version can be saved by proxies and other intermediate servers, where everyone can see it 16 | response.headers['Cache-Control'] = 'public, max-age= 31536000' # 1 year in second (about infinity on Internet Time) 17 | response.headers['X-Image-Name'] = str(file_name) 18 | response.out.write(db_image.content) 19 | 20 | def match_extension(data): 21 | return re.search('data:image/(\w+);base64', data) 22 | 23 | def validate_data(data): 24 | return match_extension(data) is not None 25 | 26 | def get_extension(data): 27 | return match_extension(data).group(1) 28 | 29 | def create_link(data, name=''): 30 | image = store_image_from_data(data, name) 31 | return image.key().name() 32 | 33 | def store_image_from_data(data, name=''): 34 | # prepare extension and image data 35 | extension = get_extension(data) 36 | image_data = str.replace(str(data), 'data:image/'+extension+';base64', '') 37 | return image_model.store_image(image_data, extension, name) 38 | 39 | class UploadSingleImage(webapp.RequestHandler): 40 | def post(self): 41 | data = self.request.get('data') 42 | if validate_data(data): 43 | image = store_image_from_data(data) 44 | if image: 45 | self.response.out.write(image.key().name()) 46 | else: 47 | self.response.out.write('Sorry, could not add image. Image is probably too big') 48 | else: 49 | self.response.out.write("Unexpected data argument. data should start with 'data:image/[ext];base64'") 50 | 51 | 52 | class GetImageHandler(webapp.RequestHandler): 53 | def get(self, image_name): 54 | image = image_model.get_image(image_name) 55 | if image: 56 | respond_image(image, self.response) 57 | else: 58 | self.response.out.write('Sorry, could not find image.') 59 | 60 | def get_framesheet_preview(self, framesheet_id): 61 | mem_key = 'image_preview_' + str(framesheet_id) 62 | link = memcache.get(mem_key) 63 | if link: 64 | self.get(link) 65 | else: 66 | framesheet = db.get(framesheet_id) 67 | if framesheet: 68 | memcache.set(mem_key, framesheet.preview_link) 69 | self.get(framesheet.preview_link) 70 | else: 71 | self.response.out.write('Sorry, could not find image.') 72 | 73 | def get_framesheet(self, framesheet_id): 74 | mem_key = 'image_framesheet_' + str(framesheet_id) 75 | link = memcache.get(mem_key) 76 | if link: 77 | self.get(link) 78 | else: 79 | framesheet = db.get(framesheet_id) 80 | if framesheet: 81 | framesheet_link = framesheet.framesheet_link 82 | if framesheet_link: 83 | memcache.set(mem_key, framesheet_link) 84 | self.get(framesheet_link) 85 | else: 86 | # fallback on preview_link if framesheet_link not generated yet 87 | # only needed for backward compatibility of entities, all framesheets now should have a preview link 88 | self.get(framesheet.preview_link) 89 | else: 90 | self.response.out.write('Sorry, could not find image.') 91 | 92 | def get_piskel_sprite(self, piskel_id): 93 | piskel = db.get(piskel_id) 94 | mem_key = 'image_piskel_' + str(piskel_id) 95 | if memcache.get(mem_key): 96 | self.get(memcache.get(mem_key)) 97 | else: 98 | if piskel: 99 | framesheet = piskel.get_current_framesheet() 100 | if framesheet and framesheet.framesheet_link: 101 | memcache.set(mem_key, framesheet.framesheet_link) 102 | self.get(framesheet.framesheet_link) 103 | return 104 | self.response.out.write('Sorry, could not find image.') 105 | -------------------------------------------------------------------------------- /handlers/login.py: -------------------------------------------------------------------------------- 1 | import webapp2 2 | from webapp2_extras import jinja2 3 | from base import BaseHandler 4 | 5 | class LoginHandler(BaseHandler): 6 | def get(self): 7 | if self.is_logged_in: 8 | self.redirect_to('home') 9 | 10 | values = { 11 | 'username': self.username if self.is_logged_in else False, 12 | 'is_logged_in': self.is_logged_in 13 | } 14 | 15 | self.render('login.html', values) -------------------------------------------------------------------------------- /handlers/oauth.py: -------------------------------------------------------------------------------- 1 | from simpleauth import SimpleAuthHandler 2 | from base import BaseHandler 3 | from config import secrets 4 | import logging 5 | 6 | class AuthHandler(BaseHandler, SimpleAuthHandler): 7 | """Authentication handler for OAuth 2.0, 1.0(a) and OpenID.""" 8 | 9 | # Enable optional OAuth 2.0 CSRF guard 10 | OAUTH2_CSRF_STATE = True 11 | 12 | USER_ATTRS = { 13 | 'facebook' : { 14 | 'id' : lambda id: ('avatar_url', 15 | 'http://graph.facebook.com/{0}/picture?type=large'.format(id)), 16 | 'name' : 'name', 17 | 'link' : 'link' 18 | }, 19 | 'google' : { 20 | 'picture': 'avatar_url', 21 | 'name' : 'name', 22 | 'profile': 'link' 23 | }, 24 | 'windows_live': { 25 | 'avatar_url': 'avatar_url', 26 | 'name' : 'name', 27 | 'link' : 'link' 28 | }, 29 | 'twitter' : { 30 | 'profile_image_url': 'avatar_url', 31 | 'screen_name' : 'name', 32 | 'link' : 'link' 33 | }, 34 | 'foursquare' : { 35 | 'photo' : lambda photo: ('avatar_url', photo.get('prefix') + '100x100' + photo.get('suffix')), 36 | 'firstName': 'firstName', 37 | 'lastName' : 'lastName', 38 | 'contact' : lambda contact: ('email',contact.get('email')), 39 | 'id' : lambda id: ('link', 'http://foursquare.com/user/{0}'.format(id)) 40 | }, 41 | 'openid' : { 42 | 'id' : lambda id: ('avatar_url', '/img/missing-avatar.png'), 43 | 'nickname': 'name', 44 | 'email' : 'link' 45 | } 46 | } 47 | 48 | def _on_signin(self, data, auth_info, provider): 49 | """Callback whenever a new or existing user is logging in. 50 | data is a user info dictionary. 51 | auth_info contains access token or oauth token and secret. 52 | """ 53 | auth_id = '%s:%s' % (provider, data['id']) 54 | logging.info('Looking for a user with id %s', auth_id) 55 | 56 | user = self.auth.store.user_model.get_by_auth_id(auth_id) 57 | _attrs = self._to_user_model_attrs(data, self.USER_ATTRS[provider]) 58 | 59 | session_user = None 60 | if user: 61 | logging.info('Found existing user to log in') 62 | self.auth.set_session(self.auth.store.user_to_dict(user)) 63 | 64 | else: 65 | # check whether there's a user currently logged in 66 | # then, create a new user if nobody's signed in, 67 | # otherwise add this auth_id to currently logged in user. 68 | 69 | if self.is_logged_in: 70 | # We only support one login provider: google, and we are not supporting any flow 71 | # to "merge" two google accounts. If a user is already logged in, this is most 72 | # likely a shared computer and we should logout() to be safe. 73 | self.logout() 74 | else: 75 | logging.info('Creating a brand new user') 76 | ok, user = self.auth.store.user_model.create_user(auth_id, **_attrs) 77 | logging.info('User created') 78 | if ok: 79 | logging.info('Setting session') 80 | self.auth.set_session(self.auth.store.user_to_dict(user)) 81 | logging.info('Session set') 82 | 83 | 84 | # Go to the profile page 85 | self.redirect_to('user-page', user_id=self.session_user['user_id']) 86 | 87 | def logout(self): 88 | self.auth.unset_session() 89 | self.redirect_to('home') 90 | 91 | def handle_exception(self, exception, debug): 92 | logging.error(exception) 93 | self.render('error.html', {'exception': exception}) 94 | 95 | def _callback_uri_for(self, provider): 96 | return self.uri_for('auth_callback', provider=provider, _full=True) 97 | 98 | def _get_consumer_info_for(self, provider): 99 | """Returns a tuple (key, secret) for auth init requests.""" 100 | return secrets.AUTH_CONFIG[provider] 101 | 102 | def _to_user_model_attrs(self, data, attrs_map): 103 | """Get the needed information from the provider dataset.""" 104 | user_attrs = {} 105 | for k, v in attrs_map.iteritems(): 106 | attr = (v, data.get(k)) if isinstance(v, str) else v(data.get(k)) 107 | user_attrs.setdefault(*attr) 108 | 109 | return user_attrs -------------------------------------------------------------------------------- /handlers/redirect.py: -------------------------------------------------------------------------------- 1 | from base import BaseHandler 2 | 3 | class RedirectHandler(BaseHandler): 4 | def get(self, path): 5 | self.redirect('http://www.piskelapp.com' + path, permanent=True) -------------------------------------------------------------------------------- /handlers/simple.py: -------------------------------------------------------------------------------- 1 | from base import BaseHandler 2 | 3 | 4 | def get_template(handler, template, is_home=False): 5 | values = { 6 | 'is_home': is_home, 7 | 'has_footer' : True, 8 | 'has_previous_page' : True, 9 | 'has_next_page' : True 10 | } 11 | 12 | handler.render(template, values) 13 | 14 | 15 | class HomeHandler(BaseHandler): 16 | def get(self): 17 | get_template(self, 'home/home.html', True) 18 | 19 | 20 | class PrivacyHandler(BaseHandler): 21 | def get(self): 22 | get_template(self, 'privacy/privacy.html', False) 23 | 24 | 25 | class TermsHandler(BaseHandler): 26 | def get(self): 27 | get_template(self, 'terms/terms.html', False) 28 | 29 | 30 | class DownloadHandler(BaseHandler): 31 | def get(self): 32 | get_template(self, 'download/download.html', False) 33 | 34 | 35 | class FaqHandler(BaseHandler): 36 | def get(self): 37 | get_template(self, 'faq/faq.html', False) 38 | -------------------------------------------------------------------------------- /handlers/user.py: -------------------------------------------------------------------------------- 1 | from base import BaseHandler 2 | from models import get_piskels 3 | from models.piskel import Piskel 4 | 5 | import json 6 | 7 | PUBLIC_CATEGORIES = ['public'] 8 | PRIVATE_CATEGORIES = ['all', 'public', 'private', 'deleted'] 9 | 10 | 11 | class UserHandler(BaseHandler): 12 | def get_default(self, user_id): 13 | user = self.auth.store.user_model.get_by_id(long(user_id)) 14 | if user: 15 | is_own_profile = self.is_logged_in and long(user_id) == self.session_user['user_id'] 16 | if is_own_profile: 17 | self.get(user_id, 'all') 18 | else: 19 | self.get(user_id, 'public') 20 | 21 | else: 22 | self.abort(404) 23 | 24 | def get(self, user_id, cat): 25 | user = self.auth.store.user_model.get_by_id(long(user_id)) 26 | if user: 27 | is_own_profile = self.is_logged_in and long(user_id) == self.session_user['user_id'] 28 | 29 | if self._is_valid_category(user_id, cat): 30 | categories = PRIVATE_CATEGORIES if is_own_profile else PUBLIC_CATEGORIES 31 | values = { 32 | 'user_id': user_id, 33 | 'profile_user': user, 34 | 'category': cat, 35 | 'categories': categories, 36 | 'is_own_profile': is_own_profile 37 | } 38 | self.render('user/user.html', values) 39 | else: 40 | self.redirect('/user/' + user_id + '/public') 41 | else: 42 | self.abort(404) 43 | 44 | def get_piskels(self, user_id, cat, offset, limit): 45 | user = self.auth.store.user_model.get_by_id(long(user_id)) 46 | if user: 47 | if self._is_valid_category(user_id, cat): 48 | piskels = get_piskels(user_id, cat, long(offset), long(limit)) 49 | if piskels: 50 | view_piskels = Piskel.prepare_piskels_for_view(piskels) 51 | obj = { 52 | 'piskelsCount': len(piskels), 53 | 'piskels': view_piskels 54 | } 55 | else: 56 | obj = { 57 | 'piskelsCount': 0 58 | } 59 | self.response.headers['Content-Type'] = 'application/json' 60 | self.response.out.write(json.dumps(obj)) 61 | else: 62 | self.abort(401) # Unauthorized 63 | else: 64 | self.abort(404) # User not found 65 | 66 | def _is_valid_category(self, user_id, cat): 67 | is_own_profile = self.is_logged_in and long(user_id) == self.session_user['user_id'] 68 | if is_own_profile and cat in ['all', 'public', 'private', 'deleted']: 69 | return True 70 | else: 71 | return cat == 'public' 72 | -------------------------------------------------------------------------------- /handlers/user_settings.py: -------------------------------------------------------------------------------- 1 | from base import BaseHandler 2 | from google.appengine.ext import db 3 | from webapp2_extras.appengine.auth.models import Unique 4 | from handlers import image as image_handler 5 | import logging 6 | import models 7 | import json 8 | import uuid 9 | 10 | 11 | PUBLIC_CATEGORIES = ['public'] 12 | PRIVATE_CATEGORIES = ['all', 'public', 'private', 'deleted'] 13 | DEFAULT_AVATAR_URL = '/static/resources/default-avatar.gif' 14 | 15 | 16 | class UserSettingsHandler(BaseHandler): 17 | 18 | def get(self, user_id): 19 | user = self.auth.store.user_model.get_by_id(long(user_id)) 20 | if user: 21 | is_own_profile = self.is_logged_in and long(user_id) == self.session_user['user_id'] 22 | if is_own_profile: 23 | if not user.apikey: 24 | user.apikey = str(uuid.uuid1()) 25 | user.put() 26 | values = { 27 | 'user_id': user_id, 28 | 'profile_user': user, 29 | 'has_footer' : True, 30 | 'DEFAULT_AVATAR_URL' : DEFAULT_AVATAR_URL, 31 | } 32 | self.render('user/user-settings.html', values) 33 | else: 34 | self.abort(401) # Unauthorized 35 | else: 36 | self.abort(404) 37 | 38 | 39 | def update(self, user_id): 40 | user = self.auth.store.user_model.get_by_id(long(user_id)) 41 | post_data = self.request.POST 42 | user.name = str(post_data.get('name')) 43 | user.location = str(post_data.get('location')) 44 | user.bio = str(post_data.get('bio')) 45 | 46 | avatar_url = str(post_data.get('avatar')) 47 | if avatar_url.startswith('data:image/'): 48 | path = image_handler.create_link(avatar_url, user_id) 49 | suffix = str(uuid.uuid1()) 50 | user.avatar_url = '/img/' + path + '?' + suffix 51 | elif avatar_url == 'DEFAULT': 52 | user.avatar_url = DEFAULT_AVATAR_URL 53 | 54 | user.put() 55 | self.redirect('/user/' + user_id + '/settings') 56 | 57 | 58 | def get_target_transfer_user(self): 59 | if not self.is_logged_in: 60 | raise Exception('Not logged in') 61 | 62 | post_data = self.request.POST 63 | 64 | target_userid = str(post_data.get('target_userid')) 65 | 66 | if not target_userid: 67 | raise Exception('Missing target user id') 68 | 69 | if str(self.session_user['user_id']) == target_userid: 70 | raise Exception('Please use a different user id') 71 | 72 | try: 73 | target_user = self.auth.store.user_model.get_by_id(long(target_userid)) 74 | except Exception as e: 75 | # unable to find user. 76 | target_user = False 77 | 78 | if not target_user: 79 | raise Exception('Unable to find user') 80 | 81 | target_apikey = str(post_data.get('target_apikey')) 82 | 83 | if not target_apikey: 84 | raise Exception('Missing target api key') 85 | 86 | if target_apikey != target_user.apikey: 87 | raise Exception('Wrong key') 88 | 89 | return target_user 90 | 91 | 92 | def prepare_transfer(self): 93 | try: 94 | target_user = self.get_target_transfer_user() 95 | current_user_id = self.session_user['user_id'] 96 | stats = models.get_stats_for_user(current_user_id) 97 | count = int(stats['piskels_count']) 98 | 99 | return self.send_json_response({ 100 | 'status': 'ok', 101 | 'count': count, 102 | 'target_name': target_user.name 103 | }) 104 | except Exception as error: 105 | return self.send_json_error(str(error)) 106 | 107 | 108 | def do_transfer(self): 109 | try: 110 | target_user = self.get_target_transfer_user() 111 | current_user_id = self.session_user['user_id'] 112 | piskels = models.get_piskels_for_user(current_user_id) 113 | for piskel in piskels: 114 | piskel.owner = target_user.key.id() 115 | piskel.put() 116 | # force consistency 117 | db.get(piskel.key()) 118 | 119 | models.invalidate_user_cache(target_user.key.id()) 120 | models.invalidate_user_cache(current_user_id) 121 | 122 | return self.send_json_response({ 123 | 'status': 'ok', 124 | 'count': len(piskels), 125 | 'target_name': target_user.name 126 | }) 127 | except Exception as error: 128 | return self.send_json_error(str(error)) 129 | 130 | def do_delete(self): 131 | if not self.is_logged_in: 132 | self.send_json_error('Not logged in') 133 | 134 | try: 135 | userid = self.session_user['user_id'] 136 | user = self.auth.store.user_model.get_by_id(userid) 137 | 138 | # delete all piskels for the current user 139 | piskels = models.get_piskels_for_user(userid) 140 | for piskel in piskels: 141 | piskel.delete() 142 | # force consistency 143 | db.get(piskel.key()) 144 | 145 | # logout current user 146 | self.auth.unset_session() 147 | 148 | # from webapp2_extras.appengine.auth.models.User 149 | # http://webapp-improved.appspot.com/_modules/webapp2_extras/appengine/auth/models.html#User 150 | # 151 | # def add_auth_id(self, auth_id): 152 | # ... 153 | # unique = '%s.auth_id:%s' % (self.__class__.__name__, auth_id) 154 | # ... 155 | Unique.delete_multi( map(lambda s: 'User.auth_id:' + s, user.auth_ids) ) 156 | 157 | # delete user entry 158 | user.key.delete() 159 | 160 | return self.send_json_response({ 161 | 'status': 'ok' 162 | }) 163 | 164 | except Exception as error: 165 | return self.send_json_error(repr(error)) 166 | 167 | 168 | def send_json_error(self, error): 169 | obj = { 170 | 'status': 'error', 171 | 'error': error 172 | } 173 | self.send_json_response(obj) 174 | 175 | 176 | def send_json_response(self, obj): 177 | self.response.headers['Content-Type'] = 'application/json' 178 | self.response.out.write(json.dumps(obj)) 179 | -------------------------------------------------------------------------------- /handlers/user_stats.py: -------------------------------------------------------------------------------- 1 | from base import BaseHandler 2 | import models 3 | import json 4 | 5 | 6 | # Handler dedicated to return stat information for a user as json 7 | class UserStatsHandler(BaseHandler): 8 | def get(self, user_id): 9 | user = self.auth.store.user_model.get_by_id(long(user_id)) 10 | if user: 11 | stats = models.get_stats_for_user(user_id) 12 | self.response.headers['Content-Type'] = 'application/json' 13 | # Set cache control to 10 minutes 14 | self.response.headers['Cache-Control'] = 'public, max-age=600' 15 | obj = { 16 | 'piskelsCount': int(stats['piskels_count']), 17 | 'framesCount': int(stats['frames_count']), 18 | 'animationDuration': float(stats['anim_length']), 19 | } 20 | self.response.out.write(json.dumps(obj)) 21 | -------------------------------------------------------------------------------- /index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | # AUTOGENERATED 4 | 5 | # This index.yaml is automatically updated whenever the dev_appserver 6 | # detects that a new type of query is run. If you want to manage the 7 | # index.yaml file manually, remove the above marker line (the line 8 | # saying "# AUTOGENERATED"). If you want to manage some indexes 9 | # manually, move them above the marker line. The index.yaml file is 10 | # automatically uploaded to the admin console when you next deploy 11 | # your application using appcfg.py. 12 | 13 | - kind: Framesheet 14 | properties: 15 | - name: owner 16 | - name: date 17 | 18 | - kind: Framesheet 19 | properties: 20 | - name: owner 21 | - name: date 22 | direction: desc 23 | 24 | - kind: Framesheet 25 | properties: 26 | - name: piskel_id 27 | - name: date 28 | direction: desc 29 | 30 | - kind: Framesheet 31 | properties: 32 | - name: piskel_id 33 | - name: frames 34 | 35 | - kind: Piskel 36 | properties: 37 | - name: deleted 38 | - name: featured 39 | - name: garbage 40 | - name: private 41 | - name: creation_date 42 | direction: desc 43 | 44 | - kind: Piskel 45 | properties: 46 | - name: deleted 47 | - name: garbage 48 | - name: owner 49 | - name: creation_date 50 | direction: desc 51 | 52 | - kind: Piskel 53 | properties: 54 | - name: deleted 55 | - name: garbage 56 | - name: owner 57 | - name: private 58 | - name: creation_date 59 | direction: desc 60 | 61 | - kind: Piskel 62 | properties: 63 | - name: deleted 64 | - name: garbage 65 | - name: private 66 | - name: creation_date 67 | direction: desc 68 | 69 | - kind: Piskel 70 | properties: 71 | - name: deleted 72 | - name: owner 73 | - name: creation_date 74 | direction: desc 75 | 76 | - kind: Piskel 77 | properties: 78 | - name: garbage 79 | - name: owner 80 | - name: creation_date 81 | direction: desc 82 | 83 | - kind: Piskel 84 | properties: 85 | - name: garbage 86 | - name: private 87 | - name: creation_date 88 | direction: desc 89 | 90 | - kind: Piskel 91 | properties: 92 | - name: owner 93 | - name: creation_date 94 | direction: desc 95 | 96 | - kind: Piskel 97 | properties: 98 | - name: owner 99 | - name: date 100 | direction: desc 101 | 102 | - kind: Piskel 103 | properties: 104 | - name: owner 105 | - name: private 106 | - name: creation_date 107 | direction: desc 108 | -------------------------------------------------------------------------------- /lib/httplib2/httplib2/iri2uri.py: -------------------------------------------------------------------------------- 1 | """ 2 | iri2uri 3 | 4 | Converts an IRI to a URI. 5 | 6 | """ 7 | __author__ = "Joe Gregorio (joe@bitworking.org)" 8 | __copyright__ = "Copyright 2006, Joe Gregorio" 9 | __contributors__ = [] 10 | __version__ = "1.0.0" 11 | __license__ = "MIT" 12 | __history__ = """ 13 | """ 14 | 15 | import urlparse 16 | 17 | 18 | # Convert an IRI to a URI following the rules in RFC 3987 19 | # 20 | # The characters we need to enocde and escape are defined in the spec: 21 | # 22 | # iprivate = %xE000-F8FF / %xF0000-FFFFD / %x100000-10FFFD 23 | # ucschar = %xA0-D7FF / %xF900-FDCF / %xFDF0-FFEF 24 | # / %x10000-1FFFD / %x20000-2FFFD / %x30000-3FFFD 25 | # / %x40000-4FFFD / %x50000-5FFFD / %x60000-6FFFD 26 | # / %x70000-7FFFD / %x80000-8FFFD / %x90000-9FFFD 27 | # / %xA0000-AFFFD / %xB0000-BFFFD / %xC0000-CFFFD 28 | # / %xD0000-DFFFD / %xE1000-EFFFD 29 | 30 | escape_range = [ 31 | (0xA0, 0xD7FF ), 32 | (0xE000, 0xF8FF ), 33 | (0xF900, 0xFDCF ), 34 | (0xFDF0, 0xFFEF), 35 | (0x10000, 0x1FFFD ), 36 | (0x20000, 0x2FFFD ), 37 | (0x30000, 0x3FFFD), 38 | (0x40000, 0x4FFFD ), 39 | (0x50000, 0x5FFFD ), 40 | (0x60000, 0x6FFFD), 41 | (0x70000, 0x7FFFD ), 42 | (0x80000, 0x8FFFD ), 43 | (0x90000, 0x9FFFD), 44 | (0xA0000, 0xAFFFD ), 45 | (0xB0000, 0xBFFFD ), 46 | (0xC0000, 0xCFFFD), 47 | (0xD0000, 0xDFFFD ), 48 | (0xE1000, 0xEFFFD), 49 | (0xF0000, 0xFFFFD ), 50 | (0x100000, 0x10FFFD) 51 | ] 52 | 53 | def encode(c): 54 | retval = c 55 | i = ord(c) 56 | for low, high in escape_range: 57 | if i < low: 58 | break 59 | if i >= low and i <= high: 60 | retval = "".join(["%%%2X" % ord(o) for o in c.encode('utf-8')]) 61 | break 62 | return retval 63 | 64 | 65 | def iri2uri(uri): 66 | """Convert an IRI to a URI. Note that IRIs must be 67 | passed in a unicode strings. That is, do not utf-8 encode 68 | the IRI before passing it into the function.""" 69 | if isinstance(uri ,unicode): 70 | (scheme, authority, path, query, fragment) = urlparse.urlsplit(uri) 71 | authority = authority.encode('idna') 72 | # For each character in 'ucschar' or 'iprivate' 73 | # 1. encode as utf-8 74 | # 2. then %-encode each octet of that utf-8 75 | uri = urlparse.urlunsplit((scheme, authority, path, query, fragment)) 76 | uri = "".join([encode(c) for c in uri]) 77 | return uri 78 | 79 | if __name__ == "__main__": 80 | import unittest 81 | 82 | class Test(unittest.TestCase): 83 | 84 | def test_uris(self): 85 | """Test that URIs are invariant under the transformation.""" 86 | invariant = [ 87 | u"ftp://ftp.is.co.za/rfc/rfc1808.txt", 88 | u"http://www.ietf.org/rfc/rfc2396.txt", 89 | u"ldap://[2001:db8::7]/c=GB?objectClass?one", 90 | u"mailto:John.Doe@example.com", 91 | u"news:comp.infosystems.www.servers.unix", 92 | u"tel:+1-816-555-1212", 93 | u"telnet://192.0.2.16:80/", 94 | u"urn:oasis:names:specification:docbook:dtd:xml:4.1.2" ] 95 | for uri in invariant: 96 | self.assertEqual(uri, iri2uri(uri)) 97 | 98 | def test_iri(self): 99 | """ Test that the right type of escaping is done for each part of the URI.""" 100 | self.assertEqual("http://xn--o3h.com/%E2%98%84", iri2uri(u"http://\N{COMET}.com/\N{COMET}")) 101 | self.assertEqual("http://bitworking.org/?fred=%E2%98%84", iri2uri(u"http://bitworking.org/?fred=\N{COMET}")) 102 | self.assertEqual("http://bitworking.org/#%E2%98%84", iri2uri(u"http://bitworking.org/#\N{COMET}")) 103 | self.assertEqual("#%E2%98%84", iri2uri(u"#\N{COMET}")) 104 | self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}")) 105 | self.assertEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}"))) 106 | self.assertNotEqual("/fred?bar=%E2%98%9A#%E2%98%84", iri2uri(u"/fred?bar=\N{BLACK LEFT POINTING INDEX}#\N{COMET}".encode('utf-8'))) 107 | 108 | unittest.main() 109 | 110 | 111 | -------------------------------------------------------------------------------- /lib/oauth2/oauth2/_version.py: -------------------------------------------------------------------------------- 1 | # This is the version of this source code. 2 | 3 | manual_verstr = "1.5" 4 | 5 | 6 | 7 | auto_build_num = "211" 8 | 9 | 10 | 11 | verstr = manual_verstr + "." + auto_build_num 12 | try: 13 | from pyutil.version_class import Version as pyutil_Version 14 | __version__ = pyutil_Version(verstr) 15 | except (ImportError, ValueError): 16 | # Maybe there is no pyutil installed. 17 | from distutils.version import LooseVersion as distutils_Version 18 | __version__ = distutils_Version(verstr) 19 | -------------------------------------------------------------------------------- /lib/simpleauth/simpleauth/__init__.py: -------------------------------------------------------------------------------- 1 | """A simple auth handler for Google App Engine supporting OAuth 1.0a, 2.0 and OpenID.""" 2 | 3 | __version__ = '0.1.2' 4 | __license__ = "MIT" 5 | __author__ = "Alex Vagin (http://alex.cloudware.it)" 6 | __copyright__ = "Copyright 2012, Alex Vagin" 7 | 8 | __all__ = [] 9 | 10 | from handler import * 11 | __all__ += handler.__all__ 12 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Copyright 2007 Google Inc. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"); 6 | # you may not use this file except in compliance with the License. 7 | # You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | # -*- coding: utf-8 -*- 18 | from fix_path import fix_sys_path 19 | fix_sys_path() 20 | 21 | from config import secrets 22 | 23 | from webapp2 import WSGIApplication, Route 24 | from webapp2_extras import routes 25 | from handlers import user, user_stats, redirect, error 26 | from models import piskel_user 27 | 28 | # webapp2 config 29 | config = { 30 | 'webapp2_extras.sessions': { 31 | 'cookie_name': '_piskel_session', 32 | 'secret_key': secrets.SESSION_KEY 33 | }, 34 | 'webapp2_extras.auth': { 35 | # Use the custom Piskel User extending webapp2_extras.appengine.auth.models.User 36 | 'user_model': piskel_user.User, 37 | # List of User model properties available in the session object 38 | 'user_attributes': ['is_admin', 'is_searchable'] 39 | } 40 | } 41 | 42 | routes = [ 43 | routes.DomainRoute('alpha.piskel-app.appspot.com', [Route('<:.*>', handler=redirect.RedirectHandler, name='redirect')]), 44 | Route('/', handler='handlers.simple.HomeHandler', name='home'), 45 | # Route('/privacy', handler='handlers.simple.PrivacyHandler', name='privacy'), 46 | # Route('/terms', handler='handlers.simple.TermsHandler', name='terms'), 47 | Route('/download', handler='handlers.simple.DownloadHandler', name='download'), 48 | Route('/faq', handler='handlers.simple.FaqHandler', name='faq'), 49 | # ################ # 50 | # GALLERY ROUTES # 51 | # ################ # 52 | Route('/admin/browse', handler='handlers.gallery.BrowseHandler', name='admin-browse'), 53 | Route('/admin/browse/', handler='handlers.gallery.BrowseHandler:getPage', name='admin-browse-page'), 54 | # Route('/featured', handler='handlers.gallery.FeaturedHandler', name='featured'), 55 | # Route('/featured/', handler='handlers.gallery.FeaturedHandler:getPage', name='featured-page'), 56 | # ############# # 57 | # USER ROUTES # 58 | # ############# # 59 | Route('/user/', handler='handlers.user.UserHandler:get_default', name='user-page'), 60 | Route('/user//settings', handler='handlers.user_settings.UserSettingsHandler', name='user-settings'), 61 | Route('/user/transfer/prepare', handler='handlers.user_settings.UserSettingsHandler:prepare_transfer', name='user-transfer-prepare', methods=['POST']), 62 | Route('/user/transfer/confirm', handler='handlers.user_settings.UserSettingsHandler:do_transfer', name='user-transfer-confirm', methods=['POST']), 63 | Route('/user/settings/delete', handler='handlers.user_settings.UserSettingsHandler:do_delete', name='user-delete', methods=['POST']), 64 | Route('/user//update', handler='handlers.user_settings.UserSettingsHandler:update', name='user-update'), 65 | Route('/user//stats', handler=user_stats.UserStatsHandler, name='user-stats'), 66 | Route('/user///piskels//', handler='handlers.user.UserHandler:get_piskels', name='user-piskels'), 67 | Route('/user//', handler=user.UserHandler, name='user-page-cat'), 68 | # ############# # 69 | # AUTH ROUTES # 70 | # ############# # 71 | Route('/auth/', handler='handlers.oauth.AuthHandler:_simple_auth', name='auth_login'), 72 | Route('/auth//callback', handler='handlers.oauth.AuthHandler:_auth_callback', name='auth_callback'), 73 | Route('/logout', handler='handlers.oauth.AuthHandler:logout', name='logout'), 74 | Route('/login', handler='handlers.login.LoginHandler', name='login'), 75 | # ############# # 76 | # EDITOR ROUTES # 77 | # ############# # 78 | Route('/p/create', handler='handlers.piskel.PiskelHandler:create', name='piskel-create'), 79 | # ############# # 80 | # PISKEL ROUTES # 81 | # ############# # 82 | Route('/p//history', handler='handlers.piskel.PiskelHandler:get_history', name='piskel-view-history'), 83 | Route('/p//view', handler='handlers.piskel.PiskelHandler:view', name='piskel-view'), 84 | Route('/p//delete', handler='handlers.piskel.PiskelHandler:delete', name='piskel-delete'), 85 | Route('/p//perm_delete', handler='handlers.piskel.PiskelHandler:permanently_delete', name='piskel-permanent-delete'), 86 | Route('/p//clone', handler='handlers.piskel.PiskelHandler:clone', name='piskel-clone'), 87 | Route('/p//clone/', handler='handlers.piskel.PiskelHandler:clone', name='piskel-clone-action'), 88 | Route('/p//rollback/', handler='handlers.piskel.PiskelHandler:rollback_piskel_to_framesheet', name='piskel-rollback'), 89 | Route('/p//restore', handler='handlers.piskel.PiskelHandler:restore', name='piskel-restore'), 90 | Route('/p//edit', handler='handlers.piskel.PiskelHandler:edit', name='piskel-edit'), 91 | Route('/p//save', handler='handlers.piskel.PiskelHandler:save', name='piskel-save', methods=['POST']), 92 | Route('/p//updateinfo', handler='handlers.piskel.PiskelHandler:updateinfo', name='piskel-update', methods=['POST']), 93 | Route('/p//sprite', handler='handlers.image.GetImageHandler:get_piskel_sprite', name='piskel-get-sprite'), 94 | # ############# # 95 | # IMAGE ROUTES # 96 | # ############# # 97 | Route('/img/', handler='handlers.image.GetImageHandler', name='image-get'), 98 | Route('/img//preview', handler='handlers.image.GetImageHandler:get_framesheet_preview', name='image-get-preview'), 99 | Route('/img//framesheet', handler='handlers.image.GetImageHandler:get_framesheet', name='image-get-framesheet') 100 | ] 101 | 102 | app = WSGIApplication(routes, config=config, debug=True) 103 | 104 | def error_handler(request, response, exception): 105 | h = error.ErrorHandler(request, response) 106 | h.handle_error(exception) 107 | 108 | error_codes = [403, 404, 500] 109 | for error_code in error_codes: 110 | app.error_handlers[error_code] = error_handler 111 | -------------------------------------------------------------------------------- /models/__init__.py: -------------------------------------------------------------------------------- 1 | from google.appengine.ext import db 2 | from google.appengine.api import memcache 3 | from time import gmtime, strftime 4 | 5 | 6 | BUCKET_SIZE = 100 7 | MAX_BUCKETS = 100 8 | CATEGORIES = ['all', 'public', 'private', 'deleted'] 9 | 10 | 11 | def _clear_get_piskels_cache(user_id): 12 | keys = [] 13 | # Piskels are retrieved by buckets and each bucket is memcached independently to avoid 14 | # exceeding the memcache size limit. 15 | # We can not really store the used memcache keys here, so instead we aggressively clear caches 16 | # for the first 100 buckets for each category. 17 | for cat in CATEGORIES: 18 | for offset in range(0, MAX_BUCKETS): 19 | keys.append(cat + '_' + str(offset * BUCKET_SIZE)) 20 | 21 | memcache.delete_multi(keys, key_prefix='user_piskels_' + str(user_id) + '_' + str(BUCKET_SIZE) + '_') 22 | 23 | 24 | def get_query(cat): 25 | if cat == 'all': 26 | return 'SELECT * FROM Piskel WHERE owner = :1 and garbage=False and deleted=False ORDER BY creation_date DESC' 27 | if cat == 'public': 28 | return 'SELECT * FROM Piskel WHERE owner = :1 and garbage=False and private=False and deleted=False ORDER BY creation_date DESC' 29 | if cat == 'private': 30 | return 'SELECT * FROM Piskel WHERE owner = :1 and garbage=False and private=True and deleted=False ORDER BY creation_date DESC' 31 | if cat == 'deleted': 32 | return 'SELECT * FROM Piskel WHERE owner = :1 and garbage=False and deleted=True ORDER BY creation_date DESC' 33 | 34 | 35 | def get_piskels(user_id, cat, offset, limit): 36 | piskels = None 37 | mem_key = None 38 | 39 | # only memcache if the query respects the current default bucket size 40 | if limit == BUCKET_SIZE and (offset % BUCKET_SIZE) == 0: 41 | mem_key = 'user_piskels_' + str(user_id) + '_' + str(BUCKET_SIZE) + '_' + cat + '_' + str(offset) 42 | piskels = memcache.get(mem_key) 43 | 44 | if not piskels: 45 | q = db.GqlQuery(get_query(cat), long(user_id)) 46 | piskels = q.fetch(offset=offset, limit=limit) 47 | if mem_key: 48 | memcache.set(mem_key, piskels) 49 | 50 | return piskels 51 | 52 | 53 | def get_recent_piskels(index): 54 | mem_key = 'recent_piskels_' + str(index) + '_' + strftime('%Y%m%d%H', gmtime()) 55 | piskels = memcache.get(mem_key) 56 | if not piskels: 57 | q = db.GqlQuery('SELECT * FROM Piskel WHERE garbage=False and private=False and deleted=False ORDER BY creation_date DESC') 58 | piskels = q.fetch(offset=(index-1)*20, limit=20) 59 | memcache.set(mem_key, piskels) 60 | return piskels 61 | 62 | 63 | def get_piskels_for_user(user_id): 64 | offset = 0 65 | 66 | piskels = [] 67 | while True: 68 | bucket = get_piskels(user_id, 'all', offset, BUCKET_SIZE) 69 | if len(bucket) == 0: 70 | break 71 | piskels += bucket 72 | offset += BUCKET_SIZE 73 | 74 | return piskels 75 | 76 | 77 | def invalidate_user_cache(user_id): 78 | memcache.delete('user_piskels_' + str(user_id)) 79 | memcache.delete('user_stats_' + str(user_id)) 80 | _clear_get_piskels_cache(user_id) 81 | 82 | 83 | def get_framesheet_fps(framesheet): 84 | try: 85 | return float(framesheet.fps) 86 | except: 87 | # FPS might be undefined. In this case return 0 and fixup the model 88 | framesheet.fps = "0" 89 | framesheet.put() 90 | return 0 91 | 92 | 93 | def get_stats_for_piskel(piskel): 94 | mem_key = 'piskel_stats_' + str(piskel.key()) 95 | stat = memcache.get(mem_key) 96 | if stat: 97 | return stat 98 | 99 | framesheet = piskel.get_current_framesheet() 100 | if framesheet: 101 | frames_count = long(framesheet.frames) 102 | fps = get_framesheet_fps(framesheet) 103 | 104 | if fps > 0: 105 | anim_length = float(frames_count) * (1/float(fps)) 106 | else: 107 | anim_length = 0 108 | 109 | stat = { 110 | 'frames_count': frames_count, 111 | 'anim_length': anim_length 112 | } 113 | memcache.set(mem_key, stat) 114 | return stat 115 | else: 116 | return None 117 | 118 | 119 | def get_stats_for_user(user_id): 120 | mem_key = 'user_stats_' + str(user_id) 121 | stat = memcache.get(mem_key) 122 | if stat: 123 | return stat 124 | 125 | frames_count = 0 126 | anim_length = 0 127 | piskels = get_piskels_for_user(user_id) 128 | for piskel in piskels: 129 | piskel_stats = get_stats_for_piskel(piskel) 130 | if piskel_stats: 131 | frames_count = frames_count + long(piskel_stats['frames_count']) 132 | anim_length = anim_length + float(piskel_stats['anim_length']) 133 | 134 | stat = { 135 | 'piskels_count': len(piskels), 136 | 'frames_count': frames_count, 137 | 'anim_length': '{:10.2f}'.format(anim_length) 138 | } 139 | memcache.set(mem_key, stat) 140 | return stat 141 | -------------------------------------------------------------------------------- /models/framesheet.py: -------------------------------------------------------------------------------- 1 | from google.appengine.ext import db 2 | 3 | 4 | class Framesheet(db.Model): 5 | """Models a framesheet entry containing only content.""" 6 | content = db.TextProperty() 7 | fps = db.StringProperty() 8 | date = db.DateTimeProperty(auto_now_add=True) 9 | # piskel = db.ReferenceProperty(Piskel, collection_name='versions') 10 | piskel_id = db.StringProperty() 11 | active = db.BooleanProperty() 12 | frames = db.IntegerProperty() 13 | preview_link = db.StringProperty() 14 | framesheet_link = db.StringProperty() 15 | 16 | def clone(self, piskel_id=None): 17 | return Framesheet( 18 | piskel_id=piskel_id if piskel_id else self.piskel_id, 19 | fps=self.fps, 20 | content=self.content, 21 | frames=self.frames, 22 | preview_link=self.preview_link, 23 | framesheet_link=self.framesheet_link 24 | ) 25 | -------------------------------------------------------------------------------- /models/image.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import uuid 3 | import base64 4 | import pickle 5 | 6 | from google.appengine.api import memcache 7 | from google.appengine.ext import db 8 | 9 | 10 | class Image(db.Model): 11 | content = db.BlobProperty(default=None) 12 | extension = db.StringProperty(default='png') 13 | 14 | 15 | class ImageInfo(db.Model): 16 | pass 17 | 18 | 19 | def store_image(image_data, extension, name=''): 20 | if name: 21 | file_name = name + '.' + extension 22 | memcache.delete(file_name) 23 | else: 24 | file_name = str(uuid.uuid1()) + '.' + extension 25 | 26 | image = Image(key_name=file_name) 27 | image.content = db.Blob(base64.decodestring(image_data)) 28 | image.extension = extension 29 | image.put() 30 | 31 | image_info = ImageInfo(key_name=file_name) 32 | image_info.put() 33 | 34 | return image 35 | 36 | 37 | def get_image(file_name): 38 | image = memcache.get(file_name) 39 | if not image: 40 | image = Image.get_by_key_name(file_name) 41 | memcache.set(file_name, image) 42 | 43 | return image -------------------------------------------------------------------------------- /models/piskel.py: -------------------------------------------------------------------------------- 1 | from google.appengine.ext import db 2 | from google.appengine.api import memcache 3 | 4 | 5 | import urllib 6 | import models 7 | 8 | 9 | class Piskel(db.Model): 10 | owner = db.IntegerProperty() 11 | creation_date = db.DateTimeProperty(auto_now_add=True) 12 | private = db.BooleanProperty(default=False) 13 | deleted = db.BooleanProperty(default=False) 14 | garbage = db.BooleanProperty(default=False) 15 | name = db.StringProperty(required=True, default='New piskel') 16 | description = db.TextProperty() 17 | 18 | #override 19 | def put(self): 20 | models.invalidate_user_cache(self.owner) 21 | if self.is_saved(): 22 | memcache.delete('piskel_json_' + str(self.key())) 23 | return db.Model.put(self) 24 | 25 | #override 26 | def delete(self): 27 | models.invalidate_user_cache(self.owner) 28 | return db.Model.delete(self) 29 | 30 | def set_current_framesheet(self, framesheet, force_consistency=False): 31 | memcache.delete('image_piskel_' + str(self.key())) 32 | memcache.delete('piskel_json_' + str(self.key())) 33 | memcache.delete('user_stats_' + str(self.owner)) 34 | 35 | current = self.get_current_framesheet() 36 | 37 | framesheet.active = True 38 | framesheet.put() 39 | 40 | # Only flip current.active if framesheet put completed successfully. 41 | if current: 42 | current.active = False 43 | current.put() 44 | 45 | if force_consistency: 46 | db.get(current.key()) 47 | db.get(framesheet.key()) 48 | db.get(self.key()) 49 | 50 | def get_current_framesheet(self): 51 | q = db.GqlQuery('SELECT * FROM Framesheet WHERE piskel_id = :1 AND active=true', str(self.key())) 52 | return q.get() 53 | 54 | def get_framesheets(self): 55 | q = db.GqlQuery('SELECT * FROM Framesheet WHERE piskel_id = :1 ORDER BY date DESC', str(self.key())) 56 | return q.fetch(None) 57 | 58 | # @return an object {key, fps, frames, preview_link, name, date} or None if the piskel has no framesheet 59 | def prepare_for_view(self): 60 | mem_key = 'piskel_json_' + str(self.key()) 61 | if memcache.get(mem_key): 62 | return memcache.get(mem_key) 63 | 64 | framesheet = self.get_current_framesheet() 65 | if framesheet: 66 | url = 'http://www.piskelapp.com/img/' + framesheet.preview_link 67 | resize_service_url = 'http://piskel-resizer.appspot.com/resize?size=200&url=' 68 | view = { 69 | 'key': str(self.key()), 70 | 'fps': framesheet.fps, 71 | 'framesheet_key': str(framesheet.key()), 72 | 'preview_url': resize_service_url + urllib.quote(url, '&'), 73 | 'frames': framesheet.frames, 74 | 'name': self.name, 75 | 'description': self.description if self.description else None, 76 | 'private': self.private, 77 | 'deleted': self.deleted, 78 | 'date': self.creation_date.strftime('%A, %d. %B %Y %I:%M%p') 79 | } 80 | memcache.set(mem_key, view) 81 | return view 82 | else: 83 | return None 84 | 85 | @classmethod 86 | def prepare_piskels_for_view(cls, piskels): 87 | view_piskels = [] 88 | for piskel in piskels: 89 | piskel_v = piskel.prepare_for_view() 90 | if piskel_v is not None: 91 | view_piskels.append(piskel_v) 92 | return view_piskels 93 | -------------------------------------------------------------------------------- /models/piskel_user.py: -------------------------------------------------------------------------------- 1 | from webapp2_extras.appengine.auth.models import User as Webapp2User 2 | from google.appengine.ext import ndb 3 | 4 | class User(Webapp2User): 5 | is_admin = ndb.BooleanProperty(default=False) 6 | is_searchable = ndb.BooleanProperty(default=False) 7 | location = ndb.StringProperty(default='') 8 | bio = ndb.StringProperty(default='') 9 | apikey = ndb.StringProperty(default='') 10 | -------------------------------------------------------------------------------- /robots.txt: -------------------------------------------------------------------------------- 1 | # robots.txt 2 | 3 | User-agent: * 4 | Disallow: /p/templates/ -------------------------------------------------------------------------------- /static/css/piskel-app-download.css: -------------------------------------------------------------------------------- 1 | .download-section { 2 | display: flex; 3 | justify-content: center; 4 | padding: 30px 0; 5 | } 6 | 7 | .download-section-inner { 8 | width: 800px; 9 | font-size: 14px; 10 | line-height: 20px; 11 | padding: 0 30px; 12 | } 13 | 14 | .download-section-title { 15 | color: white; 16 | margin: 0; 17 | margin-bottom: 30px; 18 | font-size: 24px; 19 | } 20 | 21 | .download-intro-content { 22 | display: flex; 23 | align-items: center; 24 | } 25 | 26 | .download-intro-image { 27 | width: 50px; 28 | height: 50px; 29 | border: 3px solid white; 30 | border-radius: 3px; 31 | background-color: #333; 32 | flex-shrink: 0; 33 | } 34 | 35 | .download-intro-desc { 36 | margin-left: 20px; 37 | font-size: 16px; 38 | color: white; 39 | } 40 | 41 | .download-main-actions { 42 | display: flex; 43 | justify-content: space-around; 44 | } 45 | 46 | .download-button { 47 | height: 60px; 48 | width: 120px; 49 | display: inline-flex; 50 | align-items: center; 51 | justify-content: space-around; 52 | } 53 | 54 | .download-button-image { 55 | height: 30px; 56 | width: 30px; 57 | } 58 | 59 | .download-button-text { 60 | font-size: 16px; 61 | } 62 | 63 | @media (max-width: 500px) { 64 | .download-button-text { 65 | font-size: 12px; 66 | } 67 | 68 | .download-button { 69 | height: 40px; 70 | width: 80px; 71 | } 72 | } 73 | 74 | .download-sub-section:not(:last-child) { 75 | margin-bottom: 30px; 76 | } 77 | 78 | .download-about-items { 79 | list-style-type: initial; 80 | } 81 | 82 | .download-about-item { 83 | margin-left: 20px; 84 | } -------------------------------------------------------------------------------- /static/css/piskel-app-editor.css: -------------------------------------------------------------------------------- 1 | #loading-mask { 2 | position:fixed; 3 | top:0;right:0;bottom:0;left:0; 4 | background:black; 5 | opacity:1; 6 | z-index : 20000; 7 | transition : opacity 0.5s; 8 | color:white; 9 | font-size:24px; 10 | } 11 | 12 | #loading-mask div { 13 | top:50%;left:50%; 14 | height:50px; 15 | width:300px; 16 | vertical-align:middle; 17 | text-align:center; 18 | line-height:50px; 19 | margin-top:-25px; 20 | margin-left:-150px; 21 | position:absolute; 22 | } 23 | 24 | .editor-header-wrap { 25 | z-index: 10000; 26 | position: relative; 27 | font-size: 12pt; 28 | } 29 | 30 | .editor-header-wrap .piskel-button { 31 | color: black; 32 | } 33 | 34 | .editor-header-wrap .user-menu-popup-link { 35 | color: white; 36 | } 37 | .editor-header-wrap .user-menu-popup-link:hover { 38 | color: gold; 39 | } 40 | 41 | .editor-header-wrap .navigation { 42 | height : 36px; 43 | line-height : 36px; 44 | } 45 | 46 | .editor-header-wrap .link { 47 | position: relative; 48 | z-index : 2; 49 | } 50 | 51 | .editor-header-wrap .small-user-avatar { 52 | width: 36px; 53 | height: 36px; 54 | border: none; 55 | } 56 | 57 | .editor-header-wrap .flat-button { 58 | margin: 0 1px 0 0; 59 | padding: 0 10px 0 30px; 60 | border-top-width: 3px; 61 | border-bottom-width: 0; 62 | border-radius: 0; 63 | height: 36px; 64 | 65 | transition : border 0.2s, color 0.2s; 66 | } 67 | 68 | .editor-header-wrap .flat-button:hover { 69 | border-top-color: gold; 70 | color: gold; 71 | } 72 | 73 | .piskel-name { 74 | position : absolute; 75 | left: 0; 76 | width : 100%; 77 | text-align: center; 78 | font-size: 16pt; 79 | color: #888; 80 | text-shadow: 0 -1px 0 #000; 81 | } 82 | 83 | .piskel-name-saving:after { 84 | content: ""; 85 | position: absolute; 86 | top: 5px; 87 | width: 25px; 88 | background-image: url('/static/resources/pacman-loader.gif'); 89 | height: 25px; 90 | margin-left: 5px; 91 | } 92 | 93 | .main-wrapper { 94 | margin-top: 40px; 95 | transition: margin 200ms ease-out; 96 | } 97 | 98 | .with-ads .main-wrapper, 99 | .with-ads .editor-header-wrap .link, 100 | .with-ads .cheatsheet-link { 101 | margin-left: 170px; 102 | } 103 | 104 | .with-ads .navigation { 105 | border-bottom-width: 0; 106 | } 107 | 108 | .editor-ad-container { 109 | width: 160px; 110 | position: fixed; 111 | top: 0px; 112 | background: #2D2D2D; 113 | height: 100%; 114 | display: flex; 115 | flex-direction: column; 116 | justify-content: center; 117 | } 118 | 119 | .editor-ad-message { 120 | padding: 10px; 121 | line-height: 1.6; 122 | font-size: 12px; 123 | color: #8c8c8c; 124 | } 125 | 126 | .sticky-section { 127 | position: absolute !important; 128 | } 129 | 130 | .navigation .logout-link { 131 | font-size: 1em; 132 | padding-left: 0px; 133 | padding-right: 30px; 134 | background-position: 50px 10px; 135 | line-height: 36px; 136 | } 137 | 138 | @media (max-width: 870px) { 139 | .editor-nav-longdesc { 140 | display: none; 141 | } 142 | 143 | .nav-user-drop-arrow { 144 | margin-left: 3px; 145 | } 146 | } -------------------------------------------------------------------------------- /static/css/piskel-app-faq.css: -------------------------------------------------------------------------------- 1 | .faq-section { 2 | display: flex; 3 | justify-content: center; 4 | padding: 30px 0; 5 | } 6 | 7 | .faq-section-inner { 8 | width: 800px; 9 | font-size: 14px; 10 | line-height: 20px; 11 | padding: 0 30px; 12 | } 13 | 14 | .faq-section-title { 15 | color: white; 16 | margin: 0; 17 | margin-bottom: 30px; 18 | font-size: 24px; 19 | } 20 | 21 | .faq-intro-content { 22 | display: flex; 23 | align-items: center; 24 | } 25 | 26 | .faq-intro-image { 27 | width: 50px; 28 | height: 50px; 29 | border: 3px solid white; 30 | border-radius: 3px; 31 | background-color: #333; 32 | flex-shrink: 0; 33 | } 34 | 35 | .faq-intro-desc { 36 | margin-left: 20px; 37 | font-size: 16px; 38 | color: white; 39 | } 40 | 41 | .faq-item { 42 | font-size: 16px; 43 | margin-bottom: 30px; 44 | } 45 | 46 | .faq-question { 47 | color: white; 48 | font-weight: bold; 49 | margin-bottom: 10px; 50 | } 51 | 52 | .faq-answer-image { 53 | display: block; 54 | margin: 0 0 15px 10px; 55 | border: 3px solid #ccc; 56 | border-radius: 3px; 57 | } 58 | 59 | .faq-answer ul { 60 | list-style-type: initial; 61 | margin: 1em; 62 | } -------------------------------------------------------------------------------- /static/css/piskel-app-legal.css: -------------------------------------------------------------------------------- 1 | .legal-container { 2 | max-width: 800px; 3 | padding: 10px 50px; 4 | font-size: 0.8em; 5 | 6 | } 7 | 8 | .legal-container h1 { 9 | color: gold; 10 | } 11 | 12 | .legal-container h2 { 13 | color: white; 14 | } 15 | 16 | .legal-container li { 17 | margin-bottom: 10px; 18 | } 19 | -------------------------------------------------------------------------------- /static/css/piskel-app-piskel-history.css: -------------------------------------------------------------------------------- 1 | .history-counter { 2 | line-height: 1.4rem; 3 | font-size: 1.2rem; 4 | margin-right:3px; 5 | } 6 | 7 | .history-framesheets-list { 8 | overflow:auto; 9 | } 10 | 11 | .history-framesheet { 12 | margin:5px 0; 13 | overflow:hidden; 14 | } 15 | 16 | .history-details-list { 17 | float:left; 18 | padding:10px; 19 | } 20 | 21 | .history-detail:not(:first-child) { 22 | margin:5px 0; 23 | } 24 | 25 | .history-detail-small { 26 | font-size: 0.8rem; 27 | } 28 | 29 | .history-framesheet-thumbnail { 30 | float:left; 31 | width:128px; 32 | position:relative; 33 | } 34 | 35 | .piskel-history-container { 36 | overflow:hidden; 37 | } -------------------------------------------------------------------------------- /static/css/piskel-app-piskel.css: -------------------------------------------------------------------------------- 1 | .piskel-container { 2 | padding: 40px 0 0 40px; 3 | } 4 | 5 | .piskel-meta { 6 | margin-bottom : 20px; 7 | } 8 | 9 | .piskel-name { 10 | display:inline-block; 11 | margin-right:10px; 12 | color:#FAFAFA; 13 | } 14 | 15 | .piskel-description { 16 | color : #FAFAFA; 17 | } 18 | 19 | .piskel-meta-title { 20 | font-size: 1.2em; 21 | margin: 5px 0 10px 0; 22 | font-weight: normal; 23 | color: #FAFAFA; 24 | } 25 | 26 | .piskel-meta-title::after{ 27 | display: block; 28 | border-bottom: 1px solid #4a4a4a; 29 | border-top: 1px solid #666; 30 | content: ""; 31 | margin-top: 5px; 32 | } 33 | 34 | .piskel-privacy, 35 | .piskel-history { 36 | color : #FAFAFA; 37 | font-weight: bold; 38 | } 39 | 40 | .piskel-info-container { 41 | overflow: hidden; 42 | box-sizing : border-box; 43 | } 44 | 45 | .piskel-preview-container { 46 | width:512px; 47 | float:left; 48 | margin-right : 40px; 49 | position:relative; 50 | overflow: inherit; 51 | } 52 | 53 | .piskel-preview-container::before { 54 | border-radius: 0; 55 | box-shadow:inset 0 0 6px 0 rgba(0,0,0,0.7); 56 | } 57 | 58 | .piskel-details-actions { 59 | font-size: 0; 60 | } 61 | 62 | .piskel-details-actions .piskel-button { 63 | margin-right: 5px; 64 | } 65 | 66 | /********************************/ 67 | /*****@MEDIA-QUERY : PORTRAIT****/ 68 | /********************************/ 69 | @media (orientation:portrait) { 70 | .piskel-container { 71 | position: relative; 72 | left: 50%; 73 | width: 512px; 74 | margin-left: -256px; 75 | padding-left:0; 76 | } 77 | 78 | .piskel-info-container { 79 | position: relative; 80 | width: 512px; 81 | } 82 | } 83 | 84 | .piskel-owner { 85 | margin: -5px 5px 0 5px; 86 | vertical-align: middle; 87 | border: 2px solid gold; 88 | transition : border-color 0.3s; 89 | } 90 | 91 | .piskel-owner:hover { 92 | border-color : #FAFAFA; 93 | } 94 | 95 | .piskel-preview-container canvas, 96 | .piskel-preview-container img, 97 | .piskel-error-image-container img { 98 | z-index : 50; 99 | position: relative; 100 | display: block; 101 | } 102 | 103 | .piskel-error-container { 104 | width: 512px; 105 | left: 50%; 106 | position: relative; 107 | margin-left: -256px; 108 | } 109 | 110 | .piskel-error-title { 111 | font-size: 7em; 112 | margin-top: 30px; 113 | margin-bottom: 10px; 114 | } 115 | 116 | .piskel-error-details { 117 | color: white; 118 | font-size: 3em; 119 | margin-top: 10px; 120 | margin-bottom: 20px; 121 | } 122 | 123 | .piskel-error-image-container { 124 | position:absolute; 125 | left: 50%; 126 | } 127 | 128 | @media (max-height: 900px) { 129 | .piskel-error-container { 130 | width: 512px; 131 | padding: 20px; 132 | box-sizing: border-box; 133 | right: 50%; 134 | left :auto; 135 | position: absolute; 136 | margin-left: 0; 137 | } 138 | 139 | .piskel-error-image-container { 140 | position:absolute; 141 | left: 50%; 142 | margin-left: 0 !important; 143 | } 144 | } 145 | .piskel-edit { 146 | box-sizing : border-box; 147 | 148 | position: absolute; 149 | overflow: hidden; 150 | 151 | left: 0; 152 | top: 0; 153 | width : 512px; 154 | height: 512px; 155 | padding : 20px; 156 | 157 | background: #2d2d2d; 158 | 159 | transition: left 0.3s; 160 | } 161 | 162 | .piskel-edit.show { 163 | left: 512px; 164 | } 165 | 166 | .piskel-edit>div { 167 | margin-bottom: 10px; 168 | } 169 | 170 | .piskel-edit label{ 171 | vertical-align: top; 172 | width : 120px; 173 | display:inline-block; 174 | } 175 | 176 | .piskel-edit input[type="text"], textarea { 177 | box-sizing: border-box; 178 | background: #F2F2F2; 179 | color: #2d2d2d; 180 | border: 1px solid #111; 181 | border-radius: 3px; 182 | font-family: inherit; 183 | } 184 | 185 | .piskel-edit input[type="text"]{ 186 | width: 347px; 187 | height: 30px; 188 | padding: 0 5px; 189 | font-size: 1rem; 190 | font-weight: bold; 191 | } 192 | 193 | .piskel-edit textarea{ 194 | width: 347px; 195 | padding: 5px; 196 | font-size: 0.8rem; 197 | font-weight: bold; 198 | } 199 | 200 | .piskel-edit .piskel-button { 201 | width: 100px; 202 | margin-left: 10px; 203 | } 204 | 205 | .piskel-edit-actions { 206 | text-align: right; 207 | } -------------------------------------------------------------------------------- /static/css/piskel-app-previewcard.css: -------------------------------------------------------------------------------- 1 | .card-container{ 2 | padding : 0; 3 | background : white; 4 | float : left; 5 | position:relative; 6 | 7 | margin : 0 10px 10px 0; 8 | height : 260px; 9 | width : 192px; 10 | 11 | border-radius : 2px; 12 | border-bottom: 1px solid #3a3a3a; 13 | overflow:hidden; 14 | } 15 | 16 | .card-preview-container { 17 | position:relative; 18 | } 19 | 20 | .card-preview-container .card-preview-link { 21 | overflow:hidden; 22 | display:block; 23 | } 24 | 25 | .card-preview-container .card-preview-link::before { 26 | display: block; 27 | content: ''; 28 | 29 | position: absolute; 30 | width: 100%; 31 | 32 | height: 100%; 33 | -moz-box-shadow: inset 0 0 3px 0 rgba(0,0,0,0.7); 34 | box-shadow: inset 0 0 3px 0 rgba(0,0,0,0.7); 35 | } 36 | .card-preview-actions { 37 | position:absolute; 38 | bottom:0; 39 | overflow:hidden; 40 | background-color:rgba(0,0,0,0.8); 41 | width:100%; 42 | 43 | height : 0; 44 | transition : height 0.3s; 45 | } 46 | 47 | .card-container:hover .card-preview-actions { 48 | height:24px; 49 | } 50 | 51 | .card-preview-action { 52 | color:white; 53 | text-decoration:none; 54 | font-size:0.7rem; 55 | margin-left:8px; 56 | line-height:24px; 57 | } 58 | 59 | .card-preview-action:hover { 60 | text-decoration:underline; 61 | } 62 | 63 | .card-preview-action.right { 64 | float:right; 65 | margin-right:8px 66 | } 67 | 68 | .card-info-container { 69 | padding: 0 0.2em; 70 | height: 70px; 71 | cursor: default; 72 | color: #888; 73 | transition: color 0.3s; 74 | 75 | background-size: 1.5em; 76 | background-repeat: no-repeat; 77 | background-position: 100% 40px; 78 | } 79 | 80 | .card-container-private .card-info-container { 81 | background-image: url('/static/resources/iconmonstr-lock-3-icon.svg'); 82 | } 83 | 84 | .card-container:hover .card-info-container { 85 | color:black; 86 | } 87 | 88 | .card-info-name { 89 | height: 1.6em; 90 | line-height: 1.6em; 91 | overflow: hidden; 92 | margin: 0 3px; 93 | } 94 | 95 | .card-info-name a { 96 | text-decoration:none; 97 | color: inherit; 98 | } 99 | 100 | .card-info-name a:hover { 101 | text-decoration:underline; 102 | } 103 | 104 | .card-info-meta { 105 | font-size : 0.8em; 106 | opacity:0.8; 107 | margin : 10px 3px; 108 | } 109 | 110 | .card-info-meta .counter{ 111 | font-weight : bold; 112 | font-size : 0.8rem; 113 | } -------------------------------------------------------------------------------- /static/css/piskel-app-user-settings.css: -------------------------------------------------------------------------------- 1 | .user-settings-container { 2 | max-width: 800px; 3 | width: 100%; 4 | margin: 0 auto 30px auto; 5 | padding: 0 30px; 6 | box-sizing: border-box; 7 | } 8 | 9 | .user-settings-main { 10 | overflow: hidden; 11 | display: flex; 12 | } 13 | 14 | .user-settings-title { 15 | color: white; 16 | font-size: 20px; 17 | font-weight: bold; 18 | } 19 | 20 | .user-settings-sections { 21 | display: flex; 22 | flex-direction: column; 23 | width: 80%; 24 | } 25 | 26 | .user-settings-header { 27 | padding: 30px; 28 | background: #333; 29 | margin: 30px 0; 30 | display: flex; 31 | overflow: hidden; 32 | justify-content: space-between; 33 | align-items: center; 34 | } 35 | 36 | .user-settings-container .piskel-button { 37 | font-size: 16px; 38 | height: 50px; 39 | width: 150px; 40 | padding: 0 15px; 41 | } 42 | 43 | .user-settings-header .piskel-button { 44 | float: right; 45 | } 46 | 47 | .user-settings-avatar { 48 | padding: 30px; 49 | background: #333; 50 | width: 20%; 51 | } 52 | 53 | .user-settings-avatar-preview { 54 | width: 100%; 55 | background-color: white; 56 | background-size: cover; 57 | background-position: center; 58 | } 59 | 60 | .user-settings-avatar-preview:after { 61 | content: ""; 62 | padding-top: 100%; 63 | display: block; 64 | } 65 | 66 | .user-settings-avatar-actions { 67 | text-align: center; 68 | margin-top: 30px; 69 | line-height: 30px; 70 | } 71 | 72 | .user-settings-section { 73 | padding: 30px; 74 | background: #333; 75 | margin-bottom:30px; 76 | margin-left: 30px; 77 | } 78 | 79 | .user-settings-section:last-child { 80 | margin-bottom: 0; 81 | } 82 | 83 | .user-settings-section-title { 84 | color: white; 85 | font-size: 20px; 86 | font-weight: bold; 87 | margin-bottom: 30px; 88 | } 89 | 90 | .user-settings-section-description { 91 | margin: 30px 0; 92 | font-style: italic; 93 | } 94 | 95 | .user-settings-input { 96 | justify-content: flex-end; 97 | display: flex; 98 | } 99 | 100 | .user-settings-label { 101 | margin: 10px 10px 0 0; 102 | font-weight: bold; 103 | } 104 | 105 | .user-settings-input:not(:last-child) { 106 | margin-bottom: 10px; 107 | } 108 | 109 | .user-settings-input input, 110 | .user-settings-input textarea { 111 | border: 3px solid #AAA; 112 | border-radius: 3px; 113 | padding: 10px; 114 | width: 66%; 115 | font-size: 16px; 116 | resize: none; 117 | } 118 | 119 | .user-settings-input input { 120 | font-weight: bold; 121 | } 122 | 123 | /* ************* */ 124 | /* MODAL DIALOGS */ 125 | /* ************* */ 126 | 127 | body.has-modal { 128 | overflow: hidden; 129 | } 130 | 131 | .user-settings-modal.hidden { 132 | display: none; 133 | } 134 | 135 | .modal-header { 136 | position: absolute; 137 | top: 0; 138 | width: 100%; 139 | height: 30px; 140 | line-height: 30px; 141 | background-color: gold; 142 | color: black; 143 | font-weight: bold; 144 | padding-left: 5px; 145 | box-sizing: border-box; 146 | } 147 | 148 | .modal-header-close { 149 | position: absolute; 150 | right: 10px; 151 | cursor: pointer; 152 | } 153 | 154 | .user-settings-modal-mask { 155 | position: fixed; 156 | top: 0; 157 | right: 0; 158 | bottom: 0; 159 | left: 0; 160 | z-index: 10; 161 | background: rgba(0, 0, 0, 0.8); 162 | } 163 | 164 | .user-settings-modal-content { 165 | padding: 40px 10px 10px 10px; 166 | } 167 | 168 | .user-settings-modal-description { 169 | font-weight: bold; 170 | margin: 0 0 20px 0; 171 | color: white; 172 | } 173 | 174 | .user-settings-modal-container { 175 | position: fixed; 176 | z-index: 11; 177 | width: 600px; 178 | top: 50%; 179 | left: 50%; 180 | margin-left: -300px; 181 | background: #444444; 182 | border: 3px solid gold; 183 | border-radius: 2px; 184 | } 185 | 186 | #transfer-modal .user-settings-modal-container { 187 | height: 400px; 188 | margin-top: -200px; 189 | } 190 | 191 | #delete-modal .user-settings-modal-container, 192 | #delete-completed-modal .user-settings-modal-container, 193 | #transfer-confirm-modal .user-settings-modal-container , 194 | #transfer-completed-modal .user-settings-modal-container { 195 | height: 200px; 196 | margin-top: -100px; 197 | } 198 | 199 | .transfer-modal-target-info { 200 | position: absolute; 201 | right: 10px; 202 | bottom: 10px; 203 | left: 10px; 204 | color: white; 205 | border: 1px solid gold; 206 | padding: 10px; 207 | font-size: 14px; 208 | } 209 | 210 | .transfer-modal-target-info ul { 211 | list-style-type: disc; 212 | padding-left: 20px; 213 | margin: 10px 0; 214 | } 215 | 216 | .transfer-modal-target-info li:not(:first-child) { 217 | margin-top: 10px; 218 | } 219 | 220 | .modal-description { 221 | margin-bottom: 10px; 222 | } 223 | 224 | .modal-actions button:not(:first-child) { 225 | margin-left: 10px; 226 | } 227 | -------------------------------------------------------------------------------- /static/css/piskel-app-user.css: -------------------------------------------------------------------------------- 1 | .main-container { 2 | margin : 40px; 3 | } 4 | 5 | .main-container > div { 6 | margin-bottom : 10px; 7 | } 8 | 9 | .user-avatar-big { 10 | width: 15em; 11 | height :15em; 12 | } 13 | 14 | .user-piskels-menu-wrapper, 15 | .user-piskels-menu { 16 | -moz-box-sizing: border-box; 17 | box-sizing : border-box; 18 | } 19 | 20 | .user-piskels-menu-item { 21 | font-size: 1.2em; 22 | cursor : pointer; 23 | } 24 | 25 | .user-piskels-menu-item.selected a { 26 | color: gold; 27 | } 28 | 29 | .user-piskels-menu-item:hover a{ 30 | color : white; 31 | text-decoration: none; 32 | } 33 | 34 | /********************************/ 35 | /****@MEDIA-QUERY : LANDSCAPE****/ 36 | /********************************/ 37 | 38 | @media (orientation:landscape) { 39 | .user-card { 40 | width: 300px; 41 | float : left; 42 | } 43 | 44 | .user-piskels-menu-wrapper { 45 | height: 100%; 46 | position: absolute; 47 | padding-bottom: 5px; 48 | width: 100px; 49 | } 50 | 51 | .user-piskels-menu { 52 | height: 100%; 53 | text-align: right; 54 | margin-right: 0.6em; 55 | padding-right: 0.6em; 56 | padding-top: 0.6em; 57 | border-right : 1px solid #888; 58 | } 59 | 60 | .user-piskels-grid { 61 | overflow:hidden; 62 | margin-left: 100px; 63 | } 64 | 65 | .user-piskels-menu-item.selected a, 66 | .user-piskels-menu-item:hover a { 67 | border-right-style: solid; 68 | border-right-width: 3px; 69 | padding-right: 9px; 70 | margin-right: -12px; 71 | } 72 | 73 | .user-piskels-menu-item.selected a { 74 | border-right-color : gold; 75 | } 76 | 77 | .user-piskels-menu-item:hover a{ 78 | border-right-color : white; 79 | } 80 | 81 | .user-piskels-menu-item { 82 | margin-bottom: 20px; 83 | } 84 | 85 | } 86 | 87 | 88 | /********************************/ 89 | /*****@MEDIA-QUERY : PORTRAIT****/ 90 | /********************************/ 91 | @media (orientation:portrait) { 92 | .user-card { 93 | overflow: hidden; 94 | } 95 | 96 | .user-avatar-container, .user-info-container { 97 | float : left; 98 | } 99 | 100 | .user-info-container { 101 | margin-left : 1em; 102 | } 103 | 104 | .user-piskels-menu-wrapper, 105 | .user-piskels-menu { 106 | height: 2.5em; 107 | width: 100%; 108 | } 109 | 110 | .user-piskels-menu-wrapper { 111 | padding-bottom: 5px; 112 | } 113 | 114 | .user-piskels-menu { 115 | text-align: left; 116 | margin-bottom: 0.6em; 117 | padding-bottom: 0.6em; 118 | padding-top: 0.6em; 119 | border-bottom : 0.1em solid #888; 120 | } 121 | 122 | .user-piskels-menu-item { 123 | float : left; 124 | margin-right : 1.2em; 125 | } 126 | 127 | .user-piskels-grid { 128 | overflow:hidden; 129 | margin-top: 1em; 130 | } 131 | .user-piskels-menu-item.selected a, 132 | .user-piskels-menu-item:hover a { 133 | border-bottom-style: solid; 134 | border-bottom-width: 0.15em; 135 | padding-bottom: 0.4em; 136 | margin-bottom: -0.8em; 137 | } 138 | 139 | .user-piskels-menu-item.selected a { 140 | border-bottom-color : gold; 141 | } 142 | 143 | .user-piskels-menu-item:hover a{ 144 | border-bottom-color : white; 145 | } 146 | } 147 | 148 | .user-avatar-container { 149 | display : inline-block; 150 | position : relative; 151 | background-color: white; 152 | } 153 | 154 | .user-avatar { 155 | vertical-align: middle; 156 | } 157 | 158 | .user-info-container { 159 | margin-top : 1em; 160 | } 161 | 162 | .user-info-header { 163 | font-size : 1.5em; 164 | font-weight : bold; 165 | margin: 0; 166 | } 167 | 168 | .user-info-details { 169 | margin-top : 10px; 170 | font-size : 0.8em; 171 | } 172 | 173 | .user-info-details > li{ 174 | margin: 10px 40px 0 5px; 175 | } 176 | 177 | .user-info-icon { 178 | width: 16px; 179 | height: 16px; 180 | display: inline-block; 181 | margin-right: 5px; 182 | } 183 | 184 | .user-info-icon::before { 185 | content: ""; 186 | width: 20px; 187 | background-size: 20px; 188 | height: 20px; 189 | position: absolute; 190 | } 191 | 192 | .user-info-location-icon::before { 193 | background-image: url(/static/resources/iconmonstr-globe-4-icon.svg); 194 | } 195 | 196 | .user-info-joined-icon::before { 197 | background-image: url(/static/resources/calendar-icon.svg); 198 | } 199 | 200 | .user-info-bio-icon::before { 201 | background-image: url(/static/resources/bio-icon.svg); 202 | } 203 | 204 | .user-info-description { 205 | line-height: 20px; 206 | } 207 | 208 | .user-piskels { 209 | overflow:hidden; 210 | position : relative; 211 | min-height: 256px; 212 | } 213 | 214 | .user-piskels-empty-message { 215 | text-align: center; 216 | font-size: 2.5em; 217 | color: #777; 218 | line-height: 2em; 219 | } 220 | 221 | .user-piskels-menu-item a { 222 | color : #AAA; 223 | transition : color 0.3s; 224 | } 225 | 226 | .user-info-counter { 227 | margin-right: 3px; 228 | 229 | line-height: 1.4em; 230 | font-size: 1.2em; 231 | } 232 | -------------------------------------------------------------------------------- /static/editor/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | 3 | !.gitignore 4 | !VERSION -------------------------------------------------------------------------------- /static/editor/VERSION: -------------------------------------------------------------------------------- 1 | 0.14.0 -------------------------------------------------------------------------------- /static/google392c8e9117089539.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google392c8e9117089539.html -------------------------------------------------------------------------------- /static/js/piskel-animated-preview.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // @require piskel-utils 3 | // @require piskel-image-scale 4 | 5 | window.pskl = window.pskl || {}; 6 | window.pskl.website = window.pskl.website || {}; 7 | 8 | var images = {}; 9 | 10 | var onCanvasOver = function(id, targetSize) { 11 | var imageInfo = images[id]; 12 | imageInfo.hover = true; 13 | if (!imageInfo.framesheet_canvas) { 14 | var img = new Image(); 15 | img.onload = (function(id) { 16 | return function() { 17 | var imageInfo = images[id]; 18 | var width = imageInfo.width; 19 | var height = imageInfo.height; 20 | var zoom = imageInfo.zoom; 21 | 22 | var framesheet = []; 23 | var frames = Math.floor(img.width / width); 24 | for (var i = 0; i < frames; i++) { 25 | var canvas = pskl.website.createCanvas(width, height); 26 | canvas.getContext('2d').drawImage(img, i * width, 0, width, height, 0, 0, width, height); 27 | framesheet.push(pskl.website.scale(canvas, zoom)); 28 | } 29 | imageInfo.framesheet = framesheet; 30 | if (imageInfo.hover) { 31 | startAnimationTimer(id); 32 | } 33 | }; 34 | })(id); 35 | img.src = imageInfo.url; 36 | } else { 37 | startAnimationTimer(id); 38 | } 39 | }; 40 | 41 | var startAnimationTimer = function(id) { 42 | var imageInfo = images[id]; 43 | 44 | if (imageInfo.timer) { 45 | window.clearTimeout(imageInfo.timer); 46 | } 47 | 48 | // Default to 1 fps 49 | if (imageInfo.fps === 0) { 50 | imageInfo.fps = 1; 51 | } 52 | 53 | imageInfo.timer = window.setTimeout(function() { 54 | var w = imageInfo.width * imageInfo.zoom, 55 | h = imageInfo.height * imageInfo.zoom; 56 | var context = imageInfo.canvas.getContext('2d'); 57 | context.clearRect(0, 0, imageInfo.canvas.width, imageInfo.canvas.height); 58 | context.drawImage(imageInfo.framesheet[imageInfo.animationIndex], imageInfo.xOffset, imageInfo.yOffset, w, h); 59 | startAnimationTimer(id); 60 | imageInfo.animationIndex = (imageInfo.animationIndex + 1) % imageInfo.framesheet.length; 61 | }, 1000 / imageInfo.fps); 62 | }; 63 | 64 | var onCanvasOut = function(id) { 65 | var imageInfo = images[id]; 66 | imageInfo.hover = false; 67 | window.clearTimeout(imageInfo.timer); 68 | imageInfo.animationIndex = 0; 69 | var context = imageInfo.canvas.getContext('2d'); 70 | context.clearRect(0, 0, imageInfo.canvas.width, imageInfo.canvas.height); 71 | context.drawImage(imageInfo.preview_canvas, imageInfo.xOffset, imageInfo.yOffset); 72 | }; 73 | 74 | var __id = -1; 75 | window.pskl.website.createAnimatedPreview = function(piskelId, fps, event, animate) { 76 | var spritesheet_url = "/img/" + piskelId + "/framesheet"; 77 | window.pskl.website.createSpritesheetPreview(piskelId, spritesheet_url, fps, event, animate); 78 | }; 79 | 80 | window.pskl.website.createSpritesheetPreview = function(id, spritesheet_url, fps, event, animate) { 81 | var image = event.target || document.getElementById("image" + id); 82 | 83 | var targetSize = image.width; 84 | 85 | var zoom = targetSize / Math.max(image.naturalWidth, image.naturalHeight); 86 | var preview_canvas = pskl.website.scale(image, zoom); 87 | 88 | var canvas = pskl.website.createCanvas(targetSize, targetSize); 89 | let xOffset = Math.floor((targetSize - preview_canvas.width) / 2); 90 | let yOffset = Math.floor((targetSize - preview_canvas.height) / 2); 91 | canvas.getContext('2d').drawImage(preview_canvas, xOffset, yOffset); 92 | canvas.className = "animated-preview-widget"; 93 | 94 | __id++; 95 | images["key" + __id] = { 96 | url: spritesheet_url, 97 | fps: fps, 98 | width: image.naturalWidth, 99 | height: image.naturalHeight, 100 | preview_canvas: preview_canvas, 101 | canvas: canvas, 102 | zoom: zoom, 103 | animationIndex: 0, 104 | hover: false, 105 | xOffset: xOffset, 106 | yOffset: yOffset 107 | }; 108 | 109 | image.parentNode.replaceChild(canvas, image); 110 | 111 | if (animate) { 112 | onCanvasOver("key" + __id, targetSize); 113 | } else { 114 | // Targetting parentNode because the canvas is below an inset border 115 | canvas.parentNode.addEventListener('mouseover', (function(id, targetSize) { 116 | return function(event) { 117 | onCanvasOver(id, targetSize); 118 | }; 119 | })("key" + __id, targetSize)); 120 | 121 | canvas.parentNode.addEventListener('mouseout', (function(id, targetSize) { 122 | return function(event) { 123 | onCanvasOut(id, targetSize); 124 | }; 125 | })("key" + __id, targetSize)); 126 | } 127 | }; 128 | })(); 129 | -------------------------------------------------------------------------------- /static/js/piskel-details-page.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | window.pskl = window.pskl || {}; 3 | window.pskl.website = window.pskl.website || {}; 4 | 5 | var form = null; 6 | var __memForm = function () { 7 | if (!form) form = document.getElementById("piskel-edit"); 8 | return form; 9 | }; 10 | 11 | window.addEventListener("keyup", function (evt) { 12 | if (evt.keyCode == 27 /* ESCAPE */) { 13 | pskl.website.hideEditForm(); 14 | } 15 | }); 16 | 17 | pskl.website.showEditForm = function () { 18 | if (__memForm()) form.classList.add("show"); 19 | }; 20 | pskl.website.hideEditForm = function () { 21 | if (__memForm()) form.classList.remove("show"); 22 | }; 23 | pskl.website.confirmDestroy = function (piskelKey, userId) { 24 | if (window.confirm('This will permanently delete this piskel. Continue ?')) { 25 | window.location = "/p/"+piskelKey+"/perm_delete?callback_url=/user/" + userId; 26 | } 27 | }; 28 | 29 | })(); -------------------------------------------------------------------------------- /static/js/piskel-image-scale.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | // @ require piskel-utils 3 | 4 | window.pskl = window.pskl || {}; 5 | window.pskl.website = window.pskl.website || {}; 6 | 7 | var __getImageData = function(image) { 8 | var w = getDim(image, "width"); 9 | var h = getDim(image, "height"); 10 | var canvas = pskl.website.createCanvas(w, h); 11 | 12 | var sourceContext = canvas.getContext('2d'); 13 | sourceContext.drawImage(image, 0, 0); 14 | return sourceContext.getImageData(0, 0, w, h).data; 15 | }; 16 | 17 | var _scaleNearestNeighbour = function(image, scaleInfo) { 18 | var canvas = pskl.website.createCanvas(scaleInfo.width, scaleInfo.height); 19 | 20 | var context = canvas.getContext('2d'); 21 | 22 | var imgData = __getImageData(image); 23 | 24 | var yRanges = {}, xOffset = 0, 25 | yOffset = 0; 26 | // Draw the zoomed-up pixels to a different canvas context 27 | for (var x = 0; x < scaleInfo.srcWidth; x++) { 28 | // Calculate X Range 29 | xRange = (((x + 1) * scaleInfo.zoom) | 0) - xOffset; 30 | 31 | for (var y = 0; y < scaleInfo.srcHeight; y++) { 32 | // Calculate Y Range 33 | if (!yRanges[y + ""]) { 34 | // Cache Y Range 35 | yRanges[y + ""] = (((y + 1) * scaleInfo.zoom) | 0) - yOffset; 36 | } 37 | yRange = yRanges[y + ""]; 38 | 39 | var i = (y * scaleInfo.srcWidth + x) * 4; 40 | var r = imgData[i]; 41 | var g = imgData[i + 1]; 42 | var b = imgData[i + 2]; 43 | var a = imgData[i + 3]; 44 | 45 | context.fillStyle = "rgba(" + r + "," + g + "," + b + "," + (a / 255) + ")"; 46 | context.fillRect(xOffset, yOffset, xRange, yRange); 47 | yOffset += yRange; 48 | } 49 | yOffset = 0; 50 | xOffset += xRange; 51 | } 52 | return canvas; 53 | }; 54 | 55 | var _scaleAntiAlias = function(image, scaleInfo) { 56 | var canvas = pskl.website.createCanvas(scaleInfo.width, scaleInfo.height); 57 | var context = canvas.getContext('2d'); 58 | 59 | context.save(); 60 | context.translate(canvas.width / 2, canvas.height / 2); 61 | context.scale(scaleInfo.zoom, scaleInfo.zoom); 62 | context.drawImage(image, -scaleInfo.srcWidth / 2, -scaleInfo.srcHeight / 2); 63 | context.restore(); 64 | 65 | return canvas; 66 | }; 67 | 68 | var _getScaleInfo = function(image, zoom) { 69 | var w = getDim(image, "width"), h = getDim(image, "height"); 70 | return { 71 | zoom: zoom, 72 | srcWidth: w, 73 | srcHeight: h, 74 | width: Math.round(zoom * w), 75 | height: Math.round(zoom * h) 76 | }; 77 | }; 78 | 79 | /** 80 | * pskl.website.scale can accept images or canvas elements 81 | * Resolving dimensions differs depending on which tag is used. 82 | */ 83 | var getDim = function (el, dim) { 84 | if (el.tagName == "CANVAS") { 85 | return el[dim]; 86 | } else if (el.tagName == "IMG") { 87 | return el["natural" + dim.substring(0,1).toUpperCase() + dim.substr(1)]; 88 | } 89 | }; 90 | 91 | pskl.website.scale = function(image, zoom) { 92 | var canvas = null; 93 | if (zoom > 1) { 94 | canvas = _scaleNearestNeighbour(image, _getScaleInfo(image, zoom)); 95 | } else { 96 | canvas = _scaleAntiAlias(image, _getScaleInfo(image, zoom)); 97 | } 98 | canvas.setAttribute("data-src", image.src); 99 | return canvas; 100 | }; 101 | })(); 102 | -------------------------------------------------------------------------------- /static/js/piskel-nav.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | var addPopupEventListeners = function (menuSelector, userLinkSelector) { 4 | window.addEventListener("click", function (e) { 5 | var target = e.target; 6 | var menu = document.querySelector(menuSelector); 7 | var userlink = document.querySelector(userLinkSelector); 8 | if (!menu) { 9 | return; 10 | } 11 | 12 | var isVisible = menu.classList.contains("visible"); 13 | if (isVisible && !menu.contains(target)) { 14 | menu.classList.remove("visible"); 15 | e.preventDefault(); 16 | } else if (!isVisible && userlink.contains(target)) { 17 | menu.classList.add("visible"); 18 | e.preventDefault(); 19 | } 20 | }); 21 | }; 22 | 23 | addPopupEventListeners("#user-menu-popup", ".user-link"); 24 | addPopupEventListeners("#nav-about-popup", ".nav-about-container"); 25 | addPopupEventListeners("#tiny-menu-popup", ".tiny-button"); 26 | })(); -------------------------------------------------------------------------------- /static/js/piskel-user.js: -------------------------------------------------------------------------------- 1 | function getUserId() { 2 | return window.__pageInfo.userid; 3 | } 4 | 5 | function getCategory() { 6 | return window.__pageInfo.category; 7 | } 8 | 9 | function get(url, onload, onerror) { 10 | var xhr = new XMLHttpRequest(); 11 | xhr.open("GET", url, true); 12 | xhr.onload = onload; 13 | xhr.onerror = onerror; 14 | xhr.send(); 15 | } 16 | 17 | var dummyEl; 18 | var getDummyEl = function () { 19 | if (!dummyEl) { 20 | dummyEl = document.createElement("div"); 21 | } 22 | return dummyEl; 23 | }; 24 | 25 | /** 26 | * Random templating util extracted from the piskel codebase. 27 | */ 28 | var replace = function (template, dict) { 29 | for (var key in dict) { 30 | if (dict.hasOwnProperty(key)) { 31 | var value = sanitize(dict[key]); 32 | template = template.replace(new RegExp('\\{\\{' + key + '\\}\\}', 'g'), value); 33 | } 34 | } 35 | return template; 36 | }; 37 | 38 | /** 39 | * String sanitizer for the template values. 40 | */ 41 | var sanitize = function (string) { 42 | var dummyEl = getDummyEl(); 43 | 44 | // Apply the unsafe string as text content and 45 | dummyEl.textContent = string; 46 | var sanitizedString = dummyEl.innerHTML; 47 | dummyEl.innerHTML = ''; 48 | 49 | return sanitizedString; 50 | }; 51 | 52 | /** 53 | * Sub template for generating the piskel "actions" part of the piskelcard. 54 | * @param {Object} piskel 55 | * Piskel returned by the server 56 | * @return {String} the markup for the piskelcard actions section 57 | */ 58 | var getPiskelActions = function (piskel) { 59 | var info = window.__pageInfo; 60 | var template = ''; 61 | 62 | if (info.isOwnProfile) { 63 | if (piskel.deleted) { 64 | template = 65 | 'Destroy\ 66 | Restore'; 67 | } else { 68 | template = 69 | 'Delete\ 70 | Clone\ 71 | View\ 72 | Edit'; 73 | } 74 | } else { 75 | if (info.isLoggedIn) { 76 | template = 'Clone'; 77 | } 78 | template += 'View'; 79 | } 80 | 81 | return template; 82 | }; 83 | 84 | /** 85 | * Sub template for generating the piskel "frames" part of the piskelcard. 86 | * @param {Object} piskel 87 | * Piskel returned by the server 88 | * @return {String} the markup for the piskelcard frames section 89 | */ 90 | var getPiskelFrames = function (piskel) { 91 | if (piskel.frames > 1) { 92 | return '{{frames}} frames - {{fps}} fps'; 93 | } else { 94 | return 'Single frame'; 95 | } 96 | }; 97 | 98 | /** 99 | * Main template for generating a piskel card element 100 | * @param {Object} piskel 101 | * Piskel returned by the server 102 | * @return {String} the markup for the piskelcard 103 | */ 104 | var getPiskelCard = function (piskel) { 105 | var template = '
\ 106 |
\ 107 | \ 108 | \ 114 | \ 115 |
' + getPiskelActions(piskel) + '
\ 116 |
\ 117 |
\ 118 |

{{name}}

\ 119 |

' + getPiskelFrames(piskel) + '

\ 120 |
\ 121 |
'; 122 | 123 | return replace(template, { 124 | className: piskel.private ? 'card-container-private': '', 125 | key: piskel.key, 126 | framesheet_key: piskel.framesheet_key, 127 | fps: piskel.fps, 128 | name: piskel.name, 129 | frames: piskel.frames, 130 | user_id: getUserId(), 131 | category: getCategory() 132 | }); 133 | }; 134 | 135 | /** 136 | * Create DOM elements for each provided piskel and insert it in the user piskels container 137 | * @param {Array} piskels array of piskel objects 138 | */ 139 | function insertPiskelCards(piskels) { 140 | if (!piskels || piskels.length === 0) { 141 | return; 142 | } 143 | 144 | var container = document.querySelector('.user-piskels-grid'); 145 | var dummyEl = getDummyEl(); 146 | dummyEl.innerHTML = piskels.reduce(function (markup, piskel) { 147 | return markup + getPiskelCard(piskel); 148 | }, ''); 149 | while (dummyEl.firstChild) { 150 | container.appendChild(dummyEl.firstChild); 151 | } 152 | } 153 | 154 | /** 155 | * Load a bucket of user piskels. After retrieving them from the server 156 | * they will be inserted in the DOM of the page. 157 | * 158 | * @param {Number} offset 159 | * The bucket offset 160 | * @param {Number} limit 161 | * The bucket size 162 | */ 163 | function loadUserPiskels(offset, limit) { 164 | get( 165 | "/user/" + getUserId() + "/" + getCategory() + "/piskels/" + offset + "/" + limit, 166 | function (e) { 167 | if (this.status != 200) { 168 | console.error("Failed to retrieve user piskels"); 169 | return; 170 | } 171 | 172 | var response = JSON.parse(this.responseText); 173 | var container = document.querySelector('.user-piskels-grid'); 174 | if (response.piskelsCount > 0) { 175 | if (offset === 0) { 176 | // If this is the first bucket received, clear out the loading message. 177 | container.innerHTML = ''; 178 | } 179 | // If the response contained some piskels, add them in the page and call the next bucket. 180 | insertPiskelCards(response.piskels); 181 | loadUserPiskels(offset + limit, limit); 182 | } else if (offset === 0) { 183 | container.innerHTML = 184 | '
' + 185 | 'No piskel available in \'' + getCategory() +'\' category.' + 186 | '
'; 187 | } 188 | }, 189 | function (e) { 190 | console.error("Failed to retrieve user piskels"); 191 | } 192 | ); 193 | } 194 | 195 | /** 196 | * Load the stats for the current user and display them in the page. 197 | */ 198 | function loadUserStats() { 199 | get( 200 | "/user/" + getUserId() + "/stats", 201 | function () { 202 | if (this.status != 200) { 203 | console.error("Failed to retrieve user/user_id/stats"); 204 | return; 205 | } 206 | 207 | try { 208 | var stats = JSON.parse(this.responseText); 209 | document.getElementById("animation-duration").innerText = stats.animationDuration; 210 | document.getElementById("piskels-count").innerText = stats.piskelsCount; 211 | document.getElementById("frames-count").innerText = stats.framesCount; 212 | } catch (e) { 213 | // nothing to do ... 214 | console.error("Could not parse JSON response from user/user_id/stats"); 215 | } 216 | }, 217 | function () { 218 | console.error("Failed to retrieve user/user_id/stats"); 219 | } 220 | ); 221 | } 222 | 223 | 224 | window.addEventListener("load", function () { 225 | // If the BUCKET_SIZE needs to be updated, it should also be changed on the server side in 226 | // models/__init__.py, otherwise the calls to get the piskels will no longer be memcached. 227 | var BUCKET_SIZE = 100; 228 | // Get user piskels by buckets of 100. 229 | loadUserPiskels(0, BUCKET_SIZE); 230 | // Get user stats. 231 | loadUserStats(); 232 | }); 233 | -------------------------------------------------------------------------------- /static/js/piskel-utils.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | window.pskl = window.pskl || {}; 3 | window.pskl.website = window.pskl.website || {}; 4 | 5 | window.pskl.website.createCanvas = function(width, height) { 6 | var canvas = document.createElement('canvas'); 7 | canvas.setAttribute("width", width); 8 | canvas.setAttribute("height", height); 9 | return canvas; 10 | }; 11 | })(); -------------------------------------------------------------------------------- /static/resources/animated-preview-checker-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/animated-preview-checker-background.png -------------------------------------------------------------------------------- /static/resources/bio-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 29 | 35 | 36 | 37 | 57 | 59 | 60 | 62 | image/svg+xml 63 | 65 | 66 | 67 | 68 | 69 | 74 | 81 | 87 | 93 | 99 | 105 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /static/resources/default-avatar.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/default-avatar.gif -------------------------------------------------------------------------------- /static/resources/download/download-linux-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/download/download-linux-logo.png -------------------------------------------------------------------------------- /static/resources/download/download-linux-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 60 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /static/resources/download/download-offline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/download/download-offline.png -------------------------------------------------------------------------------- /static/resources/download/download-osx-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/download/download-osx-logo.png -------------------------------------------------------------------------------- /static/resources/download/download-osx-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 20 | 22 | 23 | 25 | image/svg+xml 26 | 28 | 29 | 30 | 31 | 32 | 34 | 54 | 59 | 60 | -------------------------------------------------------------------------------- /static/resources/download/download-windows-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/download/download-windows-logo.png -------------------------------------------------------------------------------- /static/resources/download/download-windows-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 31 | 33 | 53 | 60 | 61 | -------------------------------------------------------------------------------- /static/resources/download/offline.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /static/resources/error/403/403-guy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/error/403/403-guy.png -------------------------------------------------------------------------------- /static/resources/faq/faq-support-icon-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/faq/faq-support-icon-200.png -------------------------------------------------------------------------------- /static/resources/faq/faq-support-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/faq/faq-support-icon.png -------------------------------------------------------------------------------- /static/resources/favicon-v2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/favicon-v2.png -------------------------------------------------------------------------------- /static/resources/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/favicon.ico -------------------------------------------------------------------------------- /static/resources/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/favicon.png -------------------------------------------------------------------------------- /static/resources/footer/footer-item-github-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/footer/footer-item-github-logo.png -------------------------------------------------------------------------------- /static/resources/footer/footer-item-support.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/footer/footer-item-support.png -------------------------------------------------------------------------------- /static/resources/footer/footer-item-twitter-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/footer/footer-item-twitter-logo.png -------------------------------------------------------------------------------- /static/resources/gallery-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/gallery-icon.png -------------------------------------------------------------------------------- /static/resources/gallery-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 40 | 42 | 43 | 45 | image/svg+xml 46 | 48 | 49 | 50 | 51 | 52 | 57 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /static/resources/gallery-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/gallery-icon@2x.png -------------------------------------------------------------------------------- /static/resources/github_octocat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/github_octocat.gif -------------------------------------------------------------------------------- /static/resources/home/featured/checkered-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/checkered-background.png -------------------------------------------------------------------------------- /static/resources/home/featured/joakim.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/joakim.jpg -------------------------------------------------------------------------------- /static/resources/home/featured/julian.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/julian.jpg -------------------------------------------------------------------------------- /static/resources/home/featured/llama.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/llama.png -------------------------------------------------------------------------------- /static/resources/home/featured/llama_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/llama_preview.png -------------------------------------------------------------------------------- /static/resources/home/featured/megaman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/megaman.png -------------------------------------------------------------------------------- /static/resources/home/featured/megaman_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/megaman_preview.png -------------------------------------------------------------------------------- /static/resources/home/featured/panda.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/panda.png -------------------------------------------------------------------------------- /static/resources/home/featured/panda_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/panda_preview.png -------------------------------------------------------------------------------- /static/resources/home/featured/patrick.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/patrick.jpg -------------------------------------------------------------------------------- /static/resources/home/featured/pattern.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/pattern.png -------------------------------------------------------------------------------- /static/resources/home/featured/pattern_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/pattern_preview.png -------------------------------------------------------------------------------- /static/resources/home/featured/snakes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/snakes.png -------------------------------------------------------------------------------- /static/resources/home/featured/snakes_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/snakes_preview.png -------------------------------------------------------------------------------- /static/resources/home/featured/stormtrooper.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/stormtrooper.png -------------------------------------------------------------------------------- /static/resources/home/featured/vincent.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/featured/vincent.jpg -------------------------------------------------------------------------------- /static/resources/home/features/feature-export-gold@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/features/feature-export-gold@2x.png -------------------------------------------------------------------------------- /static/resources/home/features/feature-export@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/features/feature-export@2x.png -------------------------------------------------------------------------------- /static/resources/home/features/feature-google-sign-in.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/features/feature-google-sign-in.png -------------------------------------------------------------------------------- /static/resources/home/features/feature-google-sign-in@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/features/feature-google-sign-in@2x.png -------------------------------------------------------------------------------- /static/resources/home/features/feature-live-preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/features/feature-live-preview.gif -------------------------------------------------------------------------------- /static/resources/home/features/feature-offline-application.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/features/feature-offline-application.png -------------------------------------------------------------------------------- /static/resources/home/features/feature-open-source@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/features/feature-open-source@2x.gif -------------------------------------------------------------------------------- /static/resources/home/features/feature-private-gallery-gold@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/features/feature-private-gallery-gold@2x.png -------------------------------------------------------------------------------- /static/resources/home/features/private-gallery.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 41 | 43 | 44 | 46 | image/svg+xml 47 | 49 | 50 | 51 | 52 | 53 | 58 | 68 | 78 | 85 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /static/resources/home/megamanx_animated.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/megamanx_animated.gif -------------------------------------------------------------------------------- /static/resources/home/megamanx_animated@2x.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/megamanx_animated@2x.gif -------------------------------------------------------------------------------- /static/resources/home/megamanx_preview.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/megamanx_preview.jpg -------------------------------------------------------------------------------- /static/resources/home/megamanx_preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/megamanx_preview.png -------------------------------------------------------------------------------- /static/resources/home/megamanx_spritesheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/megamanx_spritesheet.png -------------------------------------------------------------------------------- /static/resources/home/preview_gif_megaman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/preview_gif_megaman.png -------------------------------------------------------------------------------- /static/resources/home/screenshot_with_browser_ui_generic_1200.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/screenshot_with_browser_ui_generic_1200.jpg -------------------------------------------------------------------------------- /static/resources/home/screenshot_with_browser_ui_generic_1200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/screenshot_with_browser_ui_generic_1200.png -------------------------------------------------------------------------------- /static/resources/home/screenshot_with_browser_ui_generic_800.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/screenshot_with_browser_ui_generic_800.png -------------------------------------------------------------------------------- /static/resources/home/screenshot_with_browser_ui_generic_full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/home/screenshot_with_browser_ui_generic_full.png -------------------------------------------------------------------------------- /static/resources/iconmonstr-lock-3-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 13 | 14 | -------------------------------------------------------------------------------- /static/resources/logo_transparent_big_compact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/logo_transparent_big_compact.png -------------------------------------------------------------------------------- /static/resources/logo_transparent_small_compact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/logo_transparent_small_compact.png -------------------------------------------------------------------------------- /static/resources/logout-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/logout-icon.png -------------------------------------------------------------------------------- /static/resources/logout-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 44 | 46 | 47 | 49 | image/svg+xml 50 | 52 | 53 | 54 | 55 | 56 | 61 | 69 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /static/resources/logout-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/logout-icon@2x.png -------------------------------------------------------------------------------- /static/resources/logout.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /static/resources/pacman-loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/pacman-loader.gif -------------------------------------------------------------------------------- /static/resources/settings-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/settings-icon.png -------------------------------------------------------------------------------- /static/resources/settings-icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/settings-icon@2x.png -------------------------------------------------------------------------------- /static/resources/unavailable.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/piskelapp/piskel-website/60b2b1c7802c8edc5f3386a2d234767496361886/static/resources/unavailable.png -------------------------------------------------------------------------------- /static/resources/user-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 7 | 9 | 14 | 15 | -------------------------------------------------------------------------------- /templates/ads/piskel_home_footer.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 9 | 12 |
13 | -------------------------------------------------------------------------------- /templates/ads/piskel_skyscraper.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 8 | 11 |
-------------------------------------------------------------------------------- /templates/adsense.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/analytics.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {% include "adsense.html" %} 5 | 6 | 7 | 8 | 9 | 10 | 11 | {% block title %} title {% endblock %} 12 | 13 | 14 | {% include "analytics.html" %} 15 | {% block css %}{% endblock %} 16 | {% block head_end %}{% endblock %} 17 | 18 | 19 | 20 | 21 | 22 | 23 | {% block js %}{% endblock %} 24 | {% include "nav.html" %} 25 |
26 |
27 | {% block main %} main {% endblock %} 28 |
29 | {% include "ads/piskel_skyscraper.html" %} 30 |
31 | {% if has_footer %}{% include "footer.html" %}{% endif %} 32 | 33 | -------------------------------------------------------------------------------- /templates/default.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}{% endblock %} 4 | 5 | {% block css %} 6 | 7 | {% endblock %} 8 | 9 | {% block main %} 10 | 11 | {% endblock %} -------------------------------------------------------------------------------- /templates/download/download.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block bodyclass %}{% endblock %} 4 | {% block title %}Piskel - Download{% endblock %} 5 | 6 | {% block css %} 7 | 8 | {% endblock %} 9 | 10 | {% block main %} 11 |
12 |
13 |

Offline Applications

14 |
15 | 16 | 17 | If you want to use Piskel without having an internet connection, you can download an offline version of Piskel below. 18 | We provide applications for Windows, Mac OS X and Linux. 19 | 20 |
21 |
22 |
23 | 24 |
25 |
26 |

Download

27 |
28 | The current version is v0.14.0 29 |
30 | 43 |
44 | Looking for older versions? Check the 45 | download page 46 | on GitHub. 47 |
48 |
49 |
50 | 51 |
52 | {% include "ads/piskel_home_footer.html" %} 53 |
54 | 55 |
56 |
57 |

About the offline applications

58 |
59 | Piskel is built as a browser-based editor first. The offline applications are based on the Piskel editor, 60 | with a few differences worth highlighting. 61 |
62 | 63 |
64 |
Pros
65 |
    66 |
  • no internet connection needed
  • 67 |
  • save to a file without downloading a copy
  • 68 |
69 |
70 | 71 |
72 |
Cons
73 |
    74 |
  • not connected to your piskelapp.com account
  • 75 |
  • performance is usually better with the online version
  • 76 |
  • limited testing and quality assurance
  • 77 |
78 |
79 |
80 |
81 | 82 | {% endblock %} -------------------------------------------------------------------------------- /templates/editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Piskel - {{ piskel.name }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {% include "analytics.html" %} 16 | 17 | 18 | 19 |
20 |
Loading your piskel ...
21 |
22 | {% include "editor/header-partial.html" %} 23 | {% include "editor/boot-partial.html" %} 24 | 25 | 26 | {% include "editor/main-partial.html" %} 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /templates/editor/boot-partial.html: -------------------------------------------------------------------------------- 1 | {% autoescape off %} 2 | 34 | {% endautoescape %} -------------------------------------------------------------------------------- /templates/editor/header-partial.html: -------------------------------------------------------------------------------- 1 |
2 | 11 |
-------------------------------------------------------------------------------- /templates/error/http-404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Page not found!{% endblock %} 4 | 5 | {% block css %} 6 | 7 | {% endblock %} 8 | 9 | {% block main %} 10 |
11 |

Sorry!

12 |

Page not found

13 | We couldn't locate the page you were looking for. 14 |
15 | {% endblock %} -------------------------------------------------------------------------------- /templates/error/http-500.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Unexpected error!{% endblock %} 4 | 5 | {% block css %} 6 | 7 | {% endblock %} 8 | 9 | {% block main %} 10 |
11 |

Sorry!

12 |

Unexpected error

13 | Oops. Something bad happened on our side.
14 | If you can reproduce this error, please file an issue on GitHub. 15 |
16 | {% endblock %} -------------------------------------------------------------------------------- /templates/error/piskel-private.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Private Piskel!{% endblock %} 4 | 5 | {% block css %} 6 | 7 | {% endblock %} 8 | 9 | {% block head_end %} 10 | {% include "social/piskel-opengraph-card-private.html" %} 11 | {% endblock %} 12 | 13 | 14 | {% block main %} 15 |
16 |

Sorry!

17 |

Access forbidden

18 | This piskel is private. If you want to see this piskel, please contact the author and ask to save it as public. 19 |
20 |
21 |
22 | 23 |
24 |
25 | {% endblock %} -------------------------------------------------------------------------------- /templates/error/piskel-unauthorized-action.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Insufficient permissions!{% endblock %} 4 | 5 | {% block css %} 6 | 7 | {% endblock %} 8 | 9 | {% block main %} 10 |
11 |

Sorry!

12 |

Insufficient permissions

13 | {% if not is_logged_in %} 14 | Did you forget to login?
15 | {% else %} 16 | Make sure the piskel on which you are trying to perform this action is owned by you. 17 | If you think this is an error then please open an issue on github. 18 | {% endif %} 19 |
20 |
21 |
22 | 23 |
24 |
25 | {% endblock %} -------------------------------------------------------------------------------- /templates/faq/faq.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block bodyclass %}{% endblock %} 4 | {% block title %}Piskel - FAQ{% endblock %} 5 | 6 | {% block css %} 7 | 8 | {% endblock %} 9 | 10 | {% block main %} 11 |
12 |
13 |

Frequently Asked Questions

14 |
15 | 16 | 17 | This FAQ aims to regroup most of the questions we often get on Twitter & GitHub, as well as a few tips for using the editor. 18 | If you cannot find the answer you are looking for here, contact us! 19 | 20 |
21 |
22 |
23 | 24 |
25 |
26 |

General Questions

27 |
28 |
29 |
Is there a mobile application?
30 |
31 | For smartphones, not at the moment, nor in the foreseeable future. 32 | Reworking the user interface of Piskel to be usable on a small screen feels 33 | like a new dedicated project on its own. Since Piskel is open source, anybody 34 | is welcome to fork the project and try to port it to mobile however! 35 |
36 |
37 | Piskel can be used on tablets however, so a mobile application dedicated to tablets 38 | could be an option. Some user interface adjustments are still needed to improve the 39 | experience, but that seems feasible. 40 |
41 |
42 |
43 |
Can I use a sprite made with Piskel in a commercial project?
44 |
45 | Yes! Whatever you create with Piskel is yours. We do not hold any rights 46 | on user creations made on piskelapp.com or with the offline Piskel editors. 47 |
48 |
49 |
50 |
Why do you have ads?
51 |
52 | We are not big fans of ads either, but running piskelapp.com has a cost and if we want to keep 53 | the service alive, it needs to be sustainable. The ads are here to cover the server costs, 54 | performing costly backups and maybe buy some testing hardware. In the long run, we hope 55 | ads will allow us to spend more time and resources on Piskel than we do at the moment. 56 |
57 |
58 | With that said, we don't plan to add banners on the editor's interface. We believe the editor 59 | should remain a safe zone where you can focus on your creation. 60 |
61 |
62 |
63 |
Do you have a privacy policy?
64 |
65 | We are working on both a Privacy Policy and a Terms and Conditions page. 66 |
67 |
68 | The short version is: 69 |
70 | We are not keeping any user information other than the one displayed on the user pages, 71 | we don't share this information with 3rd parties, we don't hold any copyright on the user creations. 72 | We offer private and public settings for all user creations, to be used at the user's discretion. 73 |
74 |
75 | We use cookies, we collect anonymous usage data for analytics. We are not responsible for the content 76 | created by users but encourage our users to keep their public content safe for all ages. We are not 77 | responsible for the content of any of the external websites linked on piskelapp.com. 78 |
79 |
80 | If you need more details before our T&C and Privacy statements are finalized, please contact us. 81 |
82 |
83 |
84 |
How can I customize or delete my account?
85 |
86 | We are working on adding user settings to customize the name & avatar of a profile. 87 | Deleting an account is not possible at the moment, but if you really need your account 88 | to be disabled, then contact us! 89 |
90 |
91 |
92 |
How can I contact you?
93 |
94 | If it is a general question, a question about your account, or a support request you can: 95 | 99 | If you have a bug to report, a feature to propose, the best is to open an issue on GitHub: 100 | 104 |
105 |
106 |
107 |
Do you accept donations?
108 |
109 | Thanks for asking! Not at the moment, We are experimenting with using 110 | ads to cover the costs of running Piskel. 111 |
112 |
113 |
114 |
115 |
116 | 117 |
118 | {% include "ads/piskel_home_footer.html" %} 119 |
120 | 121 |
122 |
123 |

Tips & troubleshooting

124 |
125 |
126 |
How do you copy-paste in Piskel?
127 |
128 | Copy/paste is not really intuitive. It should be improved(!), but in the meantime here are the steps: 129 |
    130 |
  1. choose a selection tool
  2. 131 |
  3. select an area on the screen
  4. 132 |
  5. copy (ctrl+c) or cut (ctrl+x) (or cmd+c and cmd+x if on Mac)
  6. 133 |
  7. move your selection (don't switch tools, just click and drag the selected area, you should see the cursor change)
  8. 134 |
  9. paste (ctrl+v or cmd+v)
  10. 135 |
136 |
137 |
138 |
139 |
The editor crashed, how can I recover my content?
140 |
141 | The first thing to try if Piskel crashed is to restore the last session. 142 | Piskel periodically saves your work and allows you to restore previous sessions. 143 |
    144 |
  1. open the Piskel editor
  2. 145 |
  3. open the Import panel
  4. 146 |
  5. click on the Browse backups button of the Recover Recent Sessions section.
  6. 147 |
148 | 149 | If this didn't work, you need to go back to your previously saved version. 150 | Piskel was made to create small sprites. If you see the performance 151 | warning while working on your sprite, remember to save your work often. 152 |
153 |
154 | Whenever you experience a crash, if you can file an issue on the 155 | GitHub repository 156 | , it would be great! 157 |
158 |
159 |
160 |
161 |
162 | 163 | {% endblock %} -------------------------------------------------------------------------------- /templates/footer.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/gallery/list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% from "partial/macros.html" import render_animated_preview %} 3 | 4 | {% block title %}Piskel - {{ title }}{% endblock %} 5 | 6 | {% block css %} 7 | 8 | 9 | 10 | {% endblock %} 11 | 12 | {% block main %} 13 | 14 | {% include "gallery/navigation.html" %} 15 | 16 |
17 | {% for piskel in page_piskels %} 18 | {% include "gallery/piskelcard.html" %} 19 | {% endfor %} 20 |
21 | 22 | {% include "gallery/pagination.html" %} 23 | 24 | {% endblock %} -------------------------------------------------------------------------------- /templates/gallery/navigation.html: -------------------------------------------------------------------------------- 1 |
8 | 14 | {{ title }} 19 | 20 |
21 | -------------------------------------------------------------------------------- /templates/gallery/pagination.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {% if has_previous_page %} 4 | ⇚ Previous 5 | {% endif %} 6 | {% if has_next_page %} 7 | Next ⇛ 8 | {% endif %} 9 |
10 |
-------------------------------------------------------------------------------- /templates/gallery/piskelcard.html: -------------------------------------------------------------------------------- 1 | {% from "partial/macros.html" import render_animated_preview %} 2 |
3 | 8 |
9 |

{{ piskel.name }}

10 |

11 | {% if piskel.frames > 1 %} 12 | {{ piskel.frames }} frames - {{ piskel.fps }} fps 13 | {% else %} 14 | Single frame 15 | {% endif %} 16 |

17 |
18 |
-------------------------------------------------------------------------------- /templates/home/features.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |

Live preview

7 |

8 | Check a preview of your animation in real time as you draw. Adjust the frame delay on the fly. 9 |

10 |
11 |
12 |
13 |
14 |
15 | 16 |
17 |

Google Sign in

18 |

19 | No need to remember yet another password, just use your Google account to sign in. 20 |

21 |
22 |
23 |
24 |
25 |
26 | 27 |
28 |

Export to GIF, PNG...

29 |

30 | Several export modes supported. Animated GIFs for sharing, spritesheet PNG/ZIP for bigger projects etc… 31 |

32 |
33 |
34 |
35 |
36 |
37 | 38 |
39 |

Open Source

40 |

All the code is open-source and available on GitHub.
41 | http://github.com/piskelapp/piskel 42 |

43 |
44 |
45 |
46 |
47 |
48 | 49 |
50 |

Private gallery

51 |

52 | You can chose to make any of your sprites public or private. 53 | Private sprites are only visible to you. 54 |

55 |
56 |
57 |
58 |
59 |
60 | 62 |
63 |

Offline versions

64 |

Free desktop & offline applications for Windows, OSX and Linux. Checkout the download page.

65 |
66 |
67 |
68 |
-------------------------------------------------------------------------------- /templates/home/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% from "partial/macros.html" import render_spritesheet %} 3 | 4 | {% block title %}Piskel - Free online sprite editor{% endblock %} 5 | 6 | {% block css %} 7 | 8 | {% endblock %} 9 | 10 | {% block bodyclass %}bg3{% endblock %} 11 | {% block main %} 12 |
13 |
14 |
15 | Piskel is a free online editor for animated sprites & pixel art 16 |
17 |
18 | {% if is_logged_in %} 19 | Welcome {{ user.name }}!
20 | Go to your gallery or simply create a new sprite! 21 | {% else %} 22 | Create animations in your browser.
23 | Try an example, use Google sign in to access your gallery or simply create a new sprite. 24 | {% endif %} 25 |
26 |
27 | {% if not is_logged_in %} 28 | 31 | {% else %} 32 | 33 | My Gallery 34 | 35 | {% endif %} 36 | 37 | Create Sprite 38 | 39 |
40 |
41 |
42 |
43 |
44 |
45 | {{ render_spritesheet("megamanx", "/static/resources/home/megamanx_preview.jpg", "/static/resources/home/megamanx_spritesheet.png", 264, 16) }} 46 |
47 |
48 |
49 |
50 | 51 |
52 | {% include "home/picked.html" %} 53 |
54 | 55 | 56 | {% include "home/features.html" %} 57 | 58 | {% endblock %} -------------------------------------------------------------------------------- /templates/home/picked.html: -------------------------------------------------------------------------------- 1 | {% from "partial/macros.html" import render_spritesheet %} 2 | 3 | -------------------------------------------------------------------------------- /templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Piskel - Login 6 | 7 | 8 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /templates/nav.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /templates/partial/macros.html: -------------------------------------------------------------------------------- 1 | {% macro render_animated_preview(framesheet_key, size, fps, animate='false') -%} 2 | 8 | {% endmacro %} 9 | 10 | {% macro render_spritesheet(id, preview_url, spritesheet_url, size, fps) -%} 11 | 17 | {% endmacro %} -------------------------------------------------------------------------------- /templates/partial/nav-actions-partial.html: -------------------------------------------------------------------------------- 1 | 18 | 24 | 25 | 38 | 39 | 40 | {% if is_logged_in %} 41 | 48 | {% endif %} -------------------------------------------------------------------------------- /templates/piskel/piskel-details-edit.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 |
6 |
7 | 8 | 9 |
10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 |
-------------------------------------------------------------------------------- /templates/piskel/piskel-details.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% from "partial/macros.html" import render_animated_preview %} 3 | 4 | {% block title %}Piskel - {{ piskel.name }}{% endblock %} 5 | 6 | {% block css %} 7 | 8 | {% endblock %} 9 | 10 | {% block head_end %} 11 | {% include "social/piskel-opengraph-card.html" %} 12 | {% endblock %} 13 | 14 | {% block js %} 15 | {% if is_author %} 16 | 17 | {% endif %} 18 | {% endblock %} 19 | 20 | {% block main %} 21 | 22 |
23 |
24 | {% if is_author %}{% include "piskel/piskel-details-edit.html" %}{% endif %} 25 | {{ render_animated_preview(piskel.framesheet_key, 512, piskel.fps, 'true') }} 26 | 27 |
28 |
29 |
30 |

{{ piskel.name }}

31 | by 32 | 36 | {{ owner.name }} 37 |
38 | {% if piskel.description %} 39 |

Description

40 | {{piskel.description}} 41 | {% else %} 42 | No description 43 | {% endif %} 44 |
45 |
46 |

Visibility

47 | This sprite is {% if not piskel.private %}public{% else %}private{% endif %}. 48 |
49 | 50 | {% if is_author %} 51 |
52 |

History

53 | {% if framesheets|length > 1 %} 54 | {{framesheets|length - 1}} previous version{% if framesheets|length > 2 %}s{% endif %} available for this sprite  55 | View previous versions 56 | {% else %} 57 | No history available for this sprite. 58 | {% endif %} 59 |
60 | {% endif %} 61 | 62 | {% if is_logged_in %} 63 |
64 |

Actions

65 |
66 | {% if is_author %} 67 | {% if piskel.deleted %} 68 | Restore 69 | 70 | {% else %} 71 | 72 | Edit Sprite 73 | Clone 74 | Delete 75 | {% endif %} 76 | {% else %} 77 | Clone 78 | Clone and Edit 79 | {% endif %} 80 |
81 |
82 | {% endif %} 83 |
84 |
85 |
86 | {% endblock %} -------------------------------------------------------------------------------- /templates/piskel/piskel-history.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% from "partial/macros.html" import render_animated_preview %} 3 | 4 | {% block title %}Piskel - {{ piskel.name }} - History{% endblock %} 5 | 6 | {% block css %} 7 | 8 | 9 | {% endblock %} 10 | 11 | {# DATA: 12 | user 13 | session 14 | is_logged_in 15 | is_author 16 | owner 17 | piskel_id 18 | piskel 19 | framesheets 20 | #} 21 | 22 | {% block main %} 23 | 24 |
25 | 26 | {{ render_animated_preview(piskel.framesheet_key, 512, piskel.fps) }} 27 | 28 |
29 |
30 |

{{ piskel.name }}

- Version history

31 | by 32 | 36 |
37 |
    38 | {% for f in framesheets %} 39 |
  • 40 |
    41 | {{ render_animated_preview(f.key(), 128, f.fps) }} 42 |
    43 |
      44 |
    • Version {{framesheets|length - loop.index0}} - created on {{ f.date.strftime("%B %d %Y at %H:%m") }}
    • 45 |
    • {{f.frames}} frames
    • 46 |
    • {{f.fps}} frames per second
    • 47 | {% if not loop.first %} 48 |
    • 49 | Clone as current version 50 |
    • 51 | {% endif %} 52 |
    53 |
  • 54 | {% endfor %} 55 |
56 |
57 |
58 |
59 | {% endblock %} -------------------------------------------------------------------------------- /templates/privacy/privacy.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Piskel - Privacy Policy{% endblock %} 4 | 5 | {% block css %} 6 | 7 | {% endblock %} 8 | 9 | {% block main %} 10 | 50 | {% endblock %} -------------------------------------------------------------------------------- /templates/social/piskel-opengraph-card-private.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% autoescape off %} 4 | 5 | {% endautoescape %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /templates/social/piskel-opengraph-card.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% autoescape off %} 4 | 5 | {% endautoescape %} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /templates/terms/terms.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Piskel - Terms and Conditions{% endblock %} 4 | 5 | {% block css %} 6 | 7 | {% endblock %} 8 | 9 | {% block main %} 10 | 114 | {% endblock %} -------------------------------------------------------------------------------- /templates/user/user-piskelcard.html: -------------------------------------------------------------------------------- 1 | {% from "partial/macros.html" import render_animated_preview %} 2 |
3 |
4 | 5 | {{ render_animated_preview(piskel.framesheet_key, 192, piskel.fps) }} 6 | 7 |
8 | {% if is_own_profile %} 9 | {% if piskel.deleted %} 10 | Destroy 11 | Restore 12 | {% else %} 13 | Delete 14 | Clone 15 | View 16 | Edit 17 | {% endif %} 18 | {% else %} 19 | {% if is_logged_in %} 20 | Clone 21 | {% endif %} 22 | View 23 | {% endif %} 24 |
25 |
26 |
27 |

{{ piskel.name }}

28 |

29 | {% if piskel.frames > 1 %} 30 | {{ piskel.frames }} frames - {{ piskel.fps }} fps 31 | {% else %} 32 | Single frame 33 | {% endif %} 34 |

35 |
36 |
-------------------------------------------------------------------------------- /templates/user/user.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block title %}Piskel - {{ profile_user.name }}{% endblock %} 4 | 5 | {% block js %} 6 | 7 | {% endblock %} 8 | {% block css %} 9 | 10 | 11 | {% endblock %} 12 | 13 | {% block main %} 14 |
15 |
16 |
17 | 21 |
22 | 52 |
53 | 54 |
55 |
56 |
    57 | {% for c in categories %} 58 |
  • {{c}}
  • 59 | {% endfor %} 60 |
61 |
62 |
63 |
64 | Loading user sprites... 65 |
66 |
67 |
68 | 69 | 76 |
77 | {% endblock %} --------------------------------------------------------------------------------