File size: 6,916 Bytes
a9f4e61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"""Local test server for Quill.



    python server.py            -> http://127.0.0.1:8792

"""
import json
import os
import sys
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse

from api import render_query
from quill import INKS, STYLES

PORT = int(os.environ.get("PORT") or (sys.argv[1] if len(sys.argv) > 1 else 8792))
HOST = "0.0.0.0" if "PORT" in os.environ else "127.0.0.1"   # Heroku sets PORT

PAGE = """<!doctype html>

<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">

<title>Quill</title>

<style>

  :root { --bg:#f6f5f1; --fg:#1d1d1f; --muted:#6b6b70; --card:#fff; --line:#dcdad4; --accent:#7a3b1e; }

  @media (prefers-color-scheme: dark) { :root { --bg:#16161a; --fg:#ececf0; --muted:#9a9aa3; --card:#202026; --line:#33333b; --accent:#d08a5c; } }

  body { margin:0; background:var(--bg); color:var(--fg); font:15px/1.5 system-ui, sans-serif; }

  main { max-width:1000px; margin:0 auto; padding:24px 16px 48px; }

  h1 { font-size:22px; margin:0 0 4px; } .sub { color:var(--muted); margin:0 0 16px; }

  textarea, select { padding:10px 12px; font:inherit; border:1px solid var(--line); border-radius:8px;

    background:var(--card); color:var(--fg); }

  textarea { width:100%; box-sizing:border-box; min-height:84px; resize:vertical; }

  .controls { display:flex; flex-wrap:wrap; gap:12px; align-items:center; margin:10px 0 0; }

  label { display:flex; gap:6px; align-items:center; color:var(--muted); }

  button { padding:10px 18px; font:inherit; border:0; border-radius:8px; background:var(--accent); color:#fff; cursor:pointer; }

  button.ghost { background:var(--card); color:var(--fg); border:1px solid var(--line); }

  button:disabled { opacity:.5; cursor:wait; }

  .chips { display:flex; flex-wrap:wrap; gap:6px; margin-top:12px; }

  .chip { padding:4px 10px; border:1px solid var(--line); border-radius:999px; background:var(--card); cursor:pointer; font-size:13px; }

  #out { margin-top:20px; display:grid; gap:16px; }

  figure { margin:0; } figure img { max-width:100%; display:block; border-radius:10px; border:1px solid var(--line); }

  figcaption { color:var(--muted); font-size:13px; margin-top:4px; display:flex; gap:12px; }

  figcaption a { color:var(--accent); }

  #status { color:var(--muted); margin-top:12px; min-height:1.5em; }

</style></head><body><main>

<h1>Quill</h1>

<p class="sub">Calligraphy with hand-set pens over public stroke data (Hershey letters, KanjiVG kanji and kana). No training. Kanji or kana switch to the shodo brush automatically.</p>

<textarea id="t">Ars longa, vita brevis</textarea>

<div class="controls">

  <label>Style <select id="style">__STYLES__</select></label>

  <label>Ink <select id="ink"><option value="">style default</option>__INKS__</select></label>

  <label>Size <input id="size" type="range" min="32" max="140" value="72"><span id="sv">72</span>px</label>

  <label><input id="fl" type="checkbox"> Flourish</label>

  <label><input id="ini" type="checkbox"> Red initial</label>

  <label>Seal <input id="seal" maxlength="4" size="4" placeholder="和 / VK"></label>

  <label><input id="anim" type="checkbox"> Animate</label>

  <button id="go">Write</button>

  <button id="all" class="ghost">All styles</button>

</div>

<div class="chips" id="chips"></div>

<div id="status"></div>

<div id="out"></div>

</main><script>

const $ = id => document.getElementById(id);

const demos = ["Ars longa, vita brevis", "Happy Birthday\\nwith love from all of us", "Here beginneth the tale",

               "With kind regards", "Quiet rain on the lake", "一期一会\\n花鳥風月", "永", "夢", "ありがとう"];

demos.forEach(d => { const c = document.createElement('span'); c.className = 'chip'; c.textContent = d.replace('\\n', ' / ');

  c.onclick = () => { $('t').value = d; write([$('style').value]); }; $('chips').appendChild(c); });

$('size').oninput = () => $('sv').textContent = $('size').value;

async function write(styles) {

  $('go').disabled = $('all').disabled = true; $('out').innerHTML = '';

  for (const s of styles) {

    $('status').textContent = 'Writing ' + s + '…';

    const q = new URLSearchParams({ text: $('t').value, style: s, ink: $('ink').value, size: $('size').value,

                                    flourish: $('fl').checked ? '1' : '', initial: $('ini').checked ? '1' : '',

                                    seal: $('seal').value, animate: $('anim').checked ? '1' : '' });

    const t0 = performance.now();

    const r = await fetch('/render?' + q);

    const j = await r.json();

    if (!r.ok) { $('status').textContent = j.error; break; }

    const f = document.createElement('figure');

    f.innerHTML = '<img alt=""><figcaption><span></span><a download>Download PNG</a></figcaption>';

    f.querySelector('img').src = f.querySelector('a').href = j.src;

    const ext = j.src.slice(11, j.src.indexOf(';'));

    f.querySelector('a').download = 'quill_' + s + '.' + ext;

    f.querySelector('a').textContent = ext === 'png' ? 'Download PNG' : 'Download animation';

    f.querySelector('span').textContent = (j.style || s) + ' · ' + ((performance.now() - t0) / 1000).toFixed(1) + ' s';

    $('out').appendChild(f);

  }

  $('status').textContent = ''; $('go').disabled = $('all').disabled = false;

}

$('go').onclick = () => write([$('style').value]);

$('all').onclick = () => write(/[\\u3040-\\u9fff]/.test($('t').value) ? ['shodo'] : __ALL__.filter(s => s !== 'shodo'));

write([$('style').value]);

</script></body></html>"""


def page():
    opts = lambda xs: "".join(f'<option value="{x}">{x}</option>' for x in xs)
    return (PAGE.replace("__STYLES__", opts(STYLES)).replace("__INKS__", opts(INKS))
            .replace("__ALL__", json.dumps(list(STYLES))))


class Handler(BaseHTTPRequestHandler):
    protocol_version = "HTTP/1.1"   # HTTP/1.0 closes right after writing; on this Windows box that RSTs larger bodies

    def send(self, body, ctype, code=200):
        self.send_response(code)
        self.send_header("Content-Type", ctype)
        self.send_header("Content-Length", str(len(body)))
        self.send_header("Cache-Control", "no-store")
        self.end_headers()
        self.wfile.write(body)

    def do_GET(self):
        url = urlparse(self.path)
        if url.path == "/":
            self.send(page().encode(), "text/html; charset=utf-8")
        elif url.path == "/render":
            code, body = render_query(url.query)
            self.send(json.dumps(body).encode(), "application/json", code)
        else:
            self.send(b"not found", "text/plain", 404)

    def log_message(self, *a):
        pass


if __name__ == "__main__":
    print(f"Quill on http://{HOST}:{PORT}", flush=True)
    ThreadingHTTPServer((HOST, PORT), Handler).serve_forever()