├── debug.yaml ├── deploy.yaml ├── templates ├── base.html ├── drawing.html ├── index_user.html └── index_public.html ├── .gitignore ├── index.yaml ├── static ├── script_user.js ├── page_style.css ├── drawing_style.css └── script.js ├── README.md ├── main.go └── LICENSE /debug.yaml: -------------------------------------------------------------------------------- 1 | 2 | runtime: go 3 | api_version: go1 4 | 5 | handlers: 6 | - url: /static 7 | static_dir: static 8 | 9 | - url: /.* 10 | script: _go_app 11 | -------------------------------------------------------------------------------- /deploy.yaml: -------------------------------------------------------------------------------- 1 | 2 | runtime: go111 3 | 4 | handlers: 5 | - url: /static 6 | static_dir: static 7 | expiration: "7d" 8 | 9 | - url: /.* 10 | script: auto 11 | 12 | -------------------------------------------------------------------------------- /templates/base.html: -------------------------------------------------------------------------------- 1 | {{define "base"}} 2 | 3 | 4 | 5 | {{block "title" .}}{{end}} 6 | 7 | {{block "extrahead" .}} 8 | 13 | {{end}} 14 | 15 | {{block "body" .}}{{end}} 16 | 17 | {{end}} 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/go,appengine 2 | 3 | ### AppEngine ### 4 | # Google App Engine generated folder 5 | appengine-generated/ 6 | 7 | ### Go ### 8 | # Binaries for programs and plugins 9 | *.exe 10 | *.exe~ 11 | *.dll 12 | *.so 13 | *.dylib 14 | 15 | # Test binary, build with `go test -c` 16 | *.test 17 | 18 | # Output of the go coverage tool, specifically when used with LiteIDE 19 | *.out 20 | 21 | ### Go Patch ### 22 | /vendor/ 23 | /Godeps/ 24 | 25 | 26 | # End of https://www.gitignore.io/api/go,appengine 27 | 28 | -------------------------------------------------------------------------------- /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: Curve 14 | ancestor: yes 15 | properties: 16 | - name: Index 17 | -------------------------------------------------------------------------------- /static/script_user.js: -------------------------------------------------------------------------------- 1 | 2 | document.addEventListener("DOMContentLoaded", function(evt) { 3 | var new_drawing = document.getElementById("new_drawing"); 4 | new_drawing.onclick = function() { 5 | fetch(window.origin + "/api/drawing/add", { 6 | method: "POST", 7 | }).then(res => res.json()) 8 | .then(resp => { 9 | document.getElementById("drawing_url_container").style.display = 'block'; 10 | document.getElementById("drawing_url").value = resp.url; 11 | document.getElementById("drawing_url2").href = resp.url; 12 | }) 13 | .catch(console.log); 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Notation 3 | 4 | [**Interested in these kinds of hacks? There's more at our *Notion Hacks* community!**](https://www.notion.so/notionhacks/Notion-Hacks-27b92f71afcd4ae2ac9a4d14fef0ce47) 5 | 6 | A simple web app to make drawings that are easy to embed, especially in [Notion.so](https://notion.so). [Click here for more information](https://www.notion.so/evertheylen/About-Notation-e7a4f861a5ed4d14a767326062f80e89), or check out the [running instance](https://notation-gcp.appspot.com/). 7 | 8 | All code copyrighted by Evert Heylen. Licensed under the AGPL, for more information see the `LICENSE` file. Although not every file references this license, the license still applies to each file individually. 9 | 10 | -------------------------------------------------------------------------------- /templates/drawing.html: -------------------------------------------------------------------------------- 1 | {{template "base" .}} 2 | 3 | {{define "title"}}Notation{{end}} 4 | 5 | {{define "extrahead"}} 6 | 7 | {{end}} 8 | 9 | {{define "body"}} 10 | 11 | 12 | 21 | 22 | 23 | 24 | 25 | Your browser does not support the canvas element. 26 | 27 | 28 | 29 | 30 | 31 | {{ end }} 32 | -------------------------------------------------------------------------------- /templates/index_user.html: -------------------------------------------------------------------------------- 1 | {{template "base" .}} 2 | 3 | {{define "title"}}Notation{{end}} 4 | 5 | {{define "extrahead"}} 6 | 7 | {{end}} 8 | 9 | {{define "body"}} 10 | 11 | 12 |
13 |
14 |

Notation

15 |

You are logged in as {{ .user.Email }}.

16 |

In the future, your drawings will be shown here.

17 |
New drawing
18 |
19 |

Here is a fresh new URL (click to copy):

20 | 21 |

Or click here to open in a new tab!

22 |
23 |
24 | Logout 25 |
26 |
27 | 28 | {{end}} 29 | -------------------------------------------------------------------------------- /templates/index_public.html: -------------------------------------------------------------------------------- 1 | 2 | {{template "base" .}} 3 | 4 | {{define "title"}}Notation{{end}} 5 | 6 | {{define "extrahead"}} 7 | 8 | {{end}} 9 | 10 | {{define "body"}} 11 | 12 |
13 |
14 |

Notation

15 |

A simple web app to make drawings that are easy to embed. 16 | Click here for more information and a demo. 17 | Made by Evert Heylen, source available on github.

18 |

This was made specifically to fill a shortcoming in Notion. 19 | Use the /embed command and paste the generated URL. 20 | All drawings are visible to anyone who has the URL, but only you can edit them.

21 |

Cookie and privacy policy: This service uses cookies and local storage to remember your preferences and authentication info. 22 | The personal information we store is limited to your email address, which we use to authenticate you. If you want all your data deleted or obtain a copy, 23 | please contact the author. 24 |

25 |
26 | Login or signup with Google 27 |
28 |
29 | 30 | {{end}} 31 | -------------------------------------------------------------------------------- /static/page_style.css: -------------------------------------------------------------------------------- 1 | 2 | body { 3 | background-color: white; 4 | font-family: sans-serif; 5 | } 6 | 7 | .page_container { 8 | width: 100%; 9 | height: 100%; 10 | display: flex; 11 | align-content: center; 12 | justify-content: center; 13 | } 14 | 15 | .content { 16 | max-width: 800px; 17 | width: 100%; 18 | background: white; 19 | padding: 20px; 20 | border-radius: 3px; 21 | box-shadow: rgba(15, 15, 15, 0.05) 0px 0px 0px 1px, 22 | rgba(15, 15, 15, 0.1) 0px 3px 6px, 23 | rgba(15, 15, 15, 0.2) 0px 9px 24px; 24 | } 25 | 26 | .button { 27 | padding: 15px; 28 | text-align: center; 29 | border: 1px solid black; 30 | font-weight: bold; 31 | border-radius: 3px; 32 | display: block; 33 | color: black; 34 | text-decoration: none; 35 | max-width: 400px; 36 | margin-left: auto; 37 | margin-right: auto; 38 | margin-bottom: 16px; 39 | } 40 | 41 | .button:hover { 42 | cursor: pointer; 43 | background-color: rgba(0, 0, 0, 0.1); 44 | } 45 | 46 | #drawing_url_container { 47 | display: none; 48 | padding: 20px; 49 | background-color: rgba(0, 0, 0, 0.1); 50 | border-radius: 3px; 51 | margin-bottom: 16px; 52 | } 53 | 54 | #drawing_url { 55 | font-family: monospace; 56 | font-size: 20px; 57 | width: 100%; 58 | box-sizing: border-box; 59 | background-color: rgba(0, 0, 0, 0.1); 60 | border: none; 61 | border-radius: 3px; 62 | padding: 10px; 63 | } 64 | 65 | -------------------------------------------------------------------------------- /static/drawing_style.css: -------------------------------------------------------------------------------- 1 | 2 | html, body { 3 | width: 100%; 4 | height: 100%; 5 | margin: 0; 6 | overflow: hidden; 7 | font-family: sans-serif; 8 | font-size: 14px; 9 | 10 | -webkit-touch-callout: none; 11 | -webkit-user-select: none; 12 | -khtml-user-select: none; 13 | -moz-user-select: none; 14 | -ms-user-select: none; 15 | user-select: none; 16 | } 17 | 18 | .light { 19 | background-color: rgb(250, 250, 248); 20 | } 21 | 22 | .dark { 23 | background-color: rgb(25, 28, 30); 24 | } 25 | 26 | .fullscreen_canvas { 27 | touch-action: none; 28 | position: absolute; 29 | } 30 | 31 | #firstresponse_canvas { 32 | z-index: 10; 33 | display: none; 34 | } 35 | 36 | #contextmenu { 37 | z-index: 20; 38 | position: absolute; 39 | display: none; 40 | /*padding-top: 10px;*/ 41 | padding-bottom: 10px; 42 | border-radius: 3px; 43 | cursor: default; 44 | } 45 | 46 | .menu-item { 47 | padding: 6px 10px 6px 10px; 48 | } 49 | 50 | .menu-row { 51 | display: flex; 52 | width: 100%; 53 | } 54 | 55 | .menu-button { 56 | padding-top: 8px; 57 | padding-bottom: 8px; 58 | flex: 1; 59 | text-align: center; 60 | } 61 | 62 | .menu-button:hover { 63 | cursor: pointer; 64 | } 65 | 66 | .link:hover { 67 | cursor: pointer; 68 | } 69 | 70 | .dark .menu-item:hover, .dark .menu-button:hover { 71 | background: rgba(255, 255, 255, 0.07); 72 | } 73 | 74 | .light .menu-item:hover, .light .menu-button:hover { 75 | background: rgba(50, 50, 50, 0.08); 76 | } 77 | 78 | .light #contextmenu { 79 | color: rgb(55, 53, 47); 80 | background: rgb(255, 255, 255); 81 | box-shadow: rgba(15, 15, 15, 0.05) 0px 0px 0px 1px, 82 | rgba(15, 15, 15, 0.1) 0px 3px 6px, 83 | rgba(15, 15, 15, 0.2) 0px 9px 24px; 84 | } 85 | 86 | .dark #contextmenu { 87 | color: rgb(202, 204, 206); 88 | background: rgb(63, 68, 71); 89 | box-shadow: rgba(15, 15, 15, 0.1) 0px 0px 0px 1px, 90 | rgba(15, 15, 15, 0.2) 0px 3px 6px, 91 | rgba(15, 15, 15, 0.4) 0px 9px 24px; 92 | } 93 | 94 | #contextmenu_link { 95 | display: none; 96 | font-size: 18px; 97 | position: absolute; 98 | z-index: 15; 99 | top: 0; 100 | left: 0; 101 | padding: 5px; 102 | cursor: pointer; 103 | } 104 | 105 | .light #contextmenu_link { 106 | color: rgba(55, 53, 47, 0.25); 107 | } 108 | 109 | .dark #contextmenu_link { 110 | color: rgba(202, 204, 206, 0.3); 111 | } 112 | 113 | .pick-color, .pick-size { 114 | height: 25px; 115 | width: 25px; 116 | border-radius: 50%; 117 | display: inline-flex; 118 | justify-content: center; 119 | align-items: center; 120 | position: relative; 121 | top: 1px; 122 | } 123 | 124 | .dot { 125 | display: inline-flex; 126 | border-radius: 50%; 127 | position: absolute; /* no idea why this is needed, but does weird things otherwise */ 128 | } 129 | 130 | .pick-color:hover, .pick-size:hover { 131 | cursor: pointer; 132 | } 133 | 134 | .light .dot { 135 | background-color: #37352f; 136 | } 137 | 138 | .light .pick-size { 139 | background-color: rgba(55, 53, 47, 0.2); 140 | } 141 | 142 | .dark .dot { 143 | background-color: #caccce; 144 | } 145 | 146 | .dark .pick-size { 147 | background-color: rgba(202, 204, 206, 0.2); 148 | } 149 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "html/template" 7 | "net/http" 8 | "strings" 9 | 10 | "google.golang.org/appengine" 11 | "google.golang.org/appengine/datastore" 12 | //"cloud.google.com/go/datastore" 13 | "google.golang.org/appengine/log" 14 | "google.golang.org/appengine/user" 15 | ) 16 | 17 | // Datastore Types 18 | // ---------------------------------------------- 19 | 20 | // User proxies the default appengine user 21 | type User user.User 22 | 23 | // Drawing is made out of Curves 24 | type Drawing struct { 25 | // parent should be a User 26 | ValidIndex int64 `json:"valid_index" datastore:",noindex"` 27 | } 28 | 29 | // Curve contains points 30 | type Curve struct { 31 | // parent should be a Drawing 32 | ID int64 `json:"ID" datastore:"-"` // to be manually set 33 | Index int64 `json:"index" datastore:""` 34 | Color string `json:"color" datastore datastore:",noindex"` 35 | WidthMultiplier float64 `json:"width_multiplier" datastore:",noindex"` 36 | Points []Point `json:"points" datastore:",noindex"` 37 | } 38 | 39 | // Point represents a single measurement of the pen 40 | type Point struct { 41 | X float64 `json:"x"` 42 | Y float64 `json:"y"` 43 | Pressure float64 `json:"p"` 44 | Width float64 `json:"w"` 45 | // milliseconds since epoch 46 | Time int64 `json:"t"` 47 | } 48 | 49 | // Handlers 50 | // ---------------------------------------------- 51 | 52 | /* 53 | .Funcs(template.FuncMap{ 54 | "toJSON": func(v interface{}) template.JS { 55 | a, err := json.Marshal(v) 56 | if err != nil { 57 | panic(err) 58 | } 59 | return template.JS(a) 60 | }, 61 | }) 62 | */ 63 | 64 | var templates map[string]*template.Template 65 | 66 | func main() { 67 | http.HandleFunc("/", root) 68 | http.HandleFunc("/d/", drawing) 69 | http.HandleFunc("/api/curve/add", curveAdd) 70 | http.HandleFunc("/api/drawing/add", drawingAdd) 71 | http.HandleFunc("/api/drawing/set", drawingSet) 72 | // /static defined inside app.yaml 73 | 74 | funcMap := template.FuncMap{ 75 | "toJSON": func(v interface{}) template.JS { 76 | a, err := json.Marshal(v) 77 | if err != nil { 78 | panic(err) 79 | } 80 | return template.JS(a) 81 | }, 82 | } 83 | 84 | templates = make(map[string]*template.Template) 85 | templates["index_user"], _ = template.New("index_user.html").Funcs(funcMap).ParseFiles("templates/base.html", "templates/index_user.html") 86 | templates["index_public"], _ = template.New("index_public.html").Funcs(funcMap).ParseFiles("templates/base.html", "templates/index_public.html") 87 | templates["drawing"], _ = template.New("drawing.html").Funcs(funcMap).ParseFiles("templates/base.html", "templates/drawing.html") 88 | 89 | appengine.Main() 90 | } 91 | 92 | func root(w http.ResponseWriter, r *http.Request) { 93 | if r.URL.Path != "/" { 94 | // meh 95 | http.NotFound(w, r) 96 | return 97 | } 98 | 99 | ctx := appengine.NewContext(r) 100 | u := user.Current(ctx) 101 | data := make(map[string]interface{}) 102 | 103 | if u == nil { 104 | // not logged in 105 | log.Infof(ctx, "Showing homepage") 106 | 107 | url, err := user.LoginURL(ctx, "/") 108 | if err != nil { 109 | http.Error(w, "Couldn't login: " + err.Error(), 500) 110 | return 111 | } 112 | data["login_url"] = url 113 | 114 | err = templates["index_public"].Execute(w, &data) 115 | if err != nil { 116 | http.Error(w, "Couldn't render index_public: " + err.Error(), 500) 117 | return 118 | } 119 | } else { 120 | // logged in 121 | log.Infof(ctx, "Showing user page") 122 | data["user"] = u 123 | 124 | // upsert user 125 | k := datastore.NewKey(ctx, "User", u.ID, 0, nil) 126 | k, err := datastore.Put(ctx, k, u) 127 | if err != nil { 128 | http.Error(w, "Couldn't upsert user: " + err.Error(), 500) 129 | return 130 | } 131 | 132 | url, err := user.LogoutURL(ctx, "/") 133 | if err != nil { 134 | http.Error(w, "Couldn't create logout URL: " + err.Error(), 500) 135 | return 136 | } 137 | data["logout_url"] = url 138 | 139 | err = templates["index_user"].Execute(w, &data) 140 | if err != nil { 141 | http.Error(w, "Couldn't render index_user: " + err.Error(), 500) 142 | return 143 | } 144 | } 145 | } 146 | 147 | func drawing(w http.ResponseWriter, r *http.Request) { 148 | ctx := appengine.NewContext(r) 149 | log.Infof(ctx, "Showing a drawing") 150 | uk := getUserKey(ctx) 151 | data := make(map[string]interface{}) 152 | drawing_key := strings.TrimPrefix(r.URL.Path, "/d/") 153 | k, err := datastore.DecodeKey(drawing_key) 154 | if err != nil { 155 | http.Error(w, "Couldn't decode drawing key: " + err.Error(), 500) 156 | return 157 | } 158 | 159 | var drawing Drawing 160 | err = datastore.Get(ctx, k, &drawing) 161 | if err != nil { 162 | http.Error(w, "Couldn't get drawing from datastore: " + err.Error(), 500) 163 | return 164 | } 165 | 166 | curves := make([]Curve, 0) 167 | keys, err := datastore.NewQuery("Curve").Order("Index").Ancestor(k). 168 | Filter("Index <=", drawing.ValidIndex).GetAll(ctx, &curves) 169 | if err != nil { 170 | http.Error(w, "Couldn't get curves: " + err.Error(), 500) 171 | return 172 | } 173 | for i, _ := range curves { 174 | curves[i].ID = keys[i].IntID() 175 | } 176 | 177 | data["editable"] = (uk != nil && *k.Parent() == *uk) 178 | data["drawing"] = drawing 179 | data["curves"] = curves 180 | 181 | err = templates["drawing"].Execute(w, &data) 182 | if err != nil { 183 | http.Error(w, "Couldn't render drawing template: " + err.Error(), 500) 184 | return 185 | } 186 | } 187 | 188 | // API handlers 189 | // ---------------------------------------------- 190 | 191 | func curveAdd(w http.ResponseWriter, r *http.Request) { 192 | ctx := appengine.NewContext(r) 193 | uk := getUserKey(ctx) 194 | if uk == nil { 195 | http.Error(w, "Not logged in", 401) 196 | return 197 | } 198 | 199 | var info struct { 200 | DrawingKey string `json:"drawing_key"` 201 | Curve Curve `json:"curve"` 202 | } 203 | 204 | decoder := json.NewDecoder(r.Body) 205 | err := decoder.Decode(&info) 206 | if err != nil { 207 | http.Error(w, "Couldn't decode JSON: " + err.Error(), 500) 208 | return 209 | } 210 | 211 | dk, err := datastore.DecodeKey(info.DrawingKey) 212 | if err != nil { 213 | http.Error(w, "Couldn't decode drawing key: " + err.Error(), 500) 214 | return 215 | } 216 | 217 | if *dk.Parent() != *uk { 218 | http.Error(w, "Not authorized", 401) 219 | return 220 | } 221 | 222 | drawing := Drawing{} 223 | err = datastore.Get(ctx, dk, &drawing) 224 | if err != nil { 225 | http.Error(w, "Couldn't get drawing from datastore: ", 500) 226 | return 227 | } 228 | 229 | new_valid_index := info.Curve.Index 230 | for true { 231 | to_delete, err := datastore.NewQuery("Curve").Ancestor(dk).Filter("Index >= ", new_valid_index). 232 | KeysOnly().Limit(500).GetAll(ctx, nil) 233 | if err != nil { 234 | http.Error(w, "Couldn't get keys of curves to delete: " + err.Error(), 500) 235 | return 236 | } 237 | 238 | if len(to_delete) > 0 { 239 | err = datastore.DeleteMulti(ctx, to_delete) 240 | if err != nil { 241 | http.Error(w, "Couldn't delete curves: " + err.Error(), 500) 242 | return 243 | } 244 | } 245 | if len(to_delete) < 500 { 246 | break 247 | } 248 | } 249 | 250 | /* 251 | last_curve_arr := make([]Curve, 1) 252 | _, err = datastore.NewQuery("Curve").Order("-index").Limit(1).GetAll(ctx, &last_curve_arr); 253 | if err != nil { 254 | http.Error(w, err.Error(), 500) 255 | return 256 | } 257 | last_curve := last_curve_arr[0]; 258 | */ 259 | 260 | ck := datastore.NewIncompleteKey(ctx, "Curve", dk) 261 | _, err = datastore.Put(ctx, ck, &info.Curve) 262 | if err != nil { 263 | http.Error(w, "Couldn't insert new curve: " + err.Error(), 500) 264 | return 265 | } 266 | 267 | drawing.ValidIndex = new_valid_index 268 | log.Infof(ctx, "drawing index %v", new_valid_index) 269 | // Upsert drawing 270 | _, err = datastore.Put(ctx, dk, &drawing) 271 | if err != nil { 272 | http.Error(w, "Couldn't update drawing: " + err.Error(), 500) 273 | return 274 | } 275 | } 276 | 277 | func drawingAdd(w http.ResponseWriter, r *http.Request) { 278 | ctx := appengine.NewContext(r) 279 | uk := getUserKey(ctx) 280 | if uk == nil { 281 | http.Error(w, "Not logged in", 401) 282 | return 283 | } 284 | 285 | k := datastore.NewIncompleteKey(ctx, "Drawing", uk) 286 | k, err := datastore.Put(ctx, k, new(Drawing)) 287 | if err != nil { 288 | http.Error(w, "Couldn't insert new drawing: " + err.Error(), 500) 289 | return 290 | } 291 | renderJSON(w, map[string]string{ 292 | "url": "https://" + r.Host + "/d/" + k.Encode(), 293 | }) 294 | } 295 | 296 | func drawingSet(w http.ResponseWriter, r *http.Request) { 297 | ctx := appengine.NewContext(r) 298 | uk := getUserKey(ctx) 299 | if uk == nil { 300 | http.Error(w, "Not logged in", 401) 301 | return 302 | } 303 | 304 | var info struct { 305 | DrawingKey string `json:"drawing_key"` 306 | Drawing Drawing `json:"drawing"` 307 | } 308 | 309 | decoder := json.NewDecoder(r.Body) 310 | err := decoder.Decode(&info) 311 | if err != nil { 312 | http.Error(w, "Couldn't decode JSON: " + err.Error(), 500) 313 | return 314 | } 315 | 316 | dk, err := datastore.DecodeKey(info.DrawingKey) 317 | if err != nil { 318 | http.Error(w, "Invalid drawing key: " + err.Error(), 500) 319 | return 320 | } 321 | 322 | if *dk.Parent() != *uk { 323 | http.Error(w, "Not authorized", 401) 324 | return 325 | } 326 | 327 | log.Infof(ctx, "new set drawing index %v", info.Drawing.ValidIndex) 328 | 329 | // Upsert drawing 330 | _, err = datastore.Put(ctx, dk, &info.Drawing) 331 | if err != nil { 332 | http.Error(w, "Couldn't upsert drawing: " + err.Error(), 500) 333 | return 334 | } 335 | } 336 | 337 | // Utilities 338 | // ---------------------------------------------- 339 | 340 | func parseTemplate(name string) *template.Template { 341 | t, err := template.New("templates/" + name).Funcs(template.FuncMap{ 342 | "toJSON": func(v interface{}) template.JS { 343 | a, err := json.Marshal(v) 344 | if err != nil { 345 | panic(err) 346 | } 347 | return template.JS(a) 348 | }, 349 | }).ParseFiles("templates/base.html") 350 | if err != nil { 351 | panic(err) 352 | } 353 | return t 354 | } 355 | 356 | func renderJSON(w http.ResponseWriter, data interface{}) (err error) { 357 | js, err := json.Marshal(data) 358 | if err != nil { 359 | http.Error(w, "Couldn't marshal JSON: " + err.Error(), 500) 360 | return 361 | } 362 | 363 | w.Header().Set("Content-Type", "application/json") 364 | _, err = w.Write(js) 365 | if err != nil { 366 | http.Error(w, "Error while writing JS: " + err.Error(), 500) 367 | return 368 | } 369 | return 370 | } 371 | 372 | func getUserKey(ctx context.Context) *datastore.Key { 373 | u := user.Current(ctx) 374 | if u == nil { 375 | return nil 376 | } 377 | return datastore.NewKey(ctx, "User", u.ID, 0, nil) 378 | } 379 | -------------------------------------------------------------------------------- /static/script.js: -------------------------------------------------------------------------------- 1 | 2 | // Initialize canvas, some stuff, and bind handlers 3 | // ------------------------------------------------ 4 | 5 | var CV = undefined; 6 | var CTX = undefined; 7 | 8 | log = console.log; 9 | drawing_key = undefined; 10 | 11 | function dom_content_loaded(editable) { 12 | CV = document.getElementById("main_canvas"); 13 | CTX = CV.getContext("2d"); 14 | 15 | FRCV = document.getElementById("firstresponse_canvas"); 16 | FRCTX = FRCV.getContext("2d"); 17 | 18 | stored_color_mode = window.localStorage.getItem('color_mode'); 19 | if (stored_color_mode == null) { 20 | stored_color_mode = 'light'; 21 | } 22 | 23 | if (editable) { 24 | // Don't worry, the server will block if you don't have the proper credentials :) 25 | log("Enabling editing"); 26 | ctx_menu = htmlToElement(ctx_menu_html); 27 | document.body.appendChild(ctx_menu); 28 | set_color_mode(stored_color_mode); 29 | FRCV.addEventListener("pointerdown", handle_start, false); 30 | FRCV.addEventListener("pointerup", handle_end, false); 31 | FRCV.addEventListener("pointercancel", handle_cancel, false); 32 | FRCV.addEventListener("pointerleave", handle_cancel, false); // cancel if cursor leaves window 33 | FRCV.addEventListener("pointermove", handle_move, false); 34 | 35 | document.addEventListener('contextmenu', on_context_menu); 36 | set_ctx_menu_handlers(ctx_menu); 37 | document.getElementById("contextmenu_link").style.display = 'block'; 38 | document.getElementById("firstresponse_canvas").style.display = 'block'; 39 | 40 | // CTRL+Z and CTRL+SHIFT+Z 41 | document.onkeydown = function(e) { 42 | if (e.keyCode == 90 && e.ctrlKey) { 43 | if (e.shiftKey) { 44 | redo(); 45 | } else { 46 | undo(); 47 | } 48 | } 49 | } 50 | } else { 51 | set_color_mode(stored_color_mode); 52 | } 53 | 54 | curves = preload_curves(); 55 | curves_valid_index = curves.length-1; 56 | // init_history 57 | drawing_history = [ 58 | {valid_index: curves_valid_index} 59 | ]; 60 | resizeCanvas(); 61 | window.addEventListener('resize', resizeCanvas, false); 62 | drawing_key = window.location.pathname.split('/')[2]; 63 | log("Drawing key = '" + drawing_key + "'"); 64 | } 65 | 66 | function resizeCanvas(evt) { 67 | CV.width = document.body.clientWidth; 68 | CV.height = document.body.clientHeight; 69 | redraw_all(); 70 | FRCV.width = document.body.clientWidth; 71 | FRCV.height = document.body.clientHeight; 72 | } 73 | 74 | 75 | // Colors and dark/light mode 76 | // -------------------------- 77 | 78 | var color_mode = undefined; 79 | 80 | function set_color_mode(mode) { 81 | console.log("Setting color mode " + mode); 82 | if (mode === color_mode) return; 83 | color_mode = mode; 84 | 85 | document.body.className = color_mode; 86 | var switchmode = document.getElementById('switchmode'); 87 | if (switchmode) { 88 | switchmode.textContent = {light: "Use Dark Mode", dark: "Use Light Mode"}[color_mode]; 89 | } 90 | 91 | redraw_all(); 92 | 93 | var swatches = document.getElementsByClassName('pick-color'); 94 | for (var i=0; i 152 | 157 | 170 | 177 | 178 | ` 179 | 180 | var ctx_menu = undefined; 181 | var ctx_menu_reason = undefined; 182 | 183 | function on_context_menu(evt) { 184 | ctx_menu_reason = 'rightclick'; 185 | if (ctx_menu.style.display == 'block') { 186 | ctx_menu.style.display = 'none'; 187 | } else { 188 | // Some non trivial logic to make sure menu is displayed correctly 189 | // First display but hide element so we know bounding rect 190 | ctx_menu.style.top = '5px'; 191 | ctx_menu.style.removeProperty('bottom'); 192 | ctx_menu.style.left = '5px'; 193 | ctx_menu.style.removeProperty('right'); 194 | 195 | ctx_menu.style.visibility = 'hidden'; 196 | ctx_menu.style.display = 'block'; 197 | 198 | var body_rect = document.body.getBoundingClientRect(); 199 | var menu_rect = ctx_menu.getBoundingClientRect(); 200 | 201 | if (evt.clientX + menu_rect.width + 8 > body_rect.width) { 202 | ctx_menu.style.removeProperty('left'); 203 | ctx_menu.style.right = (body_rect.width - evt.clientX + 4) + 'px'; 204 | } else { 205 | ctx_menu.style.left = (evt.clientX+4) + 'px'; 206 | ctx_menu.style.removeProperty('right'); 207 | } 208 | 209 | if (evt.clientY + menu_rect.height + 8 > body_rect.height) { 210 | ctx_menu.style.removeProperty('top'); 211 | ctx_menu.style.bottom = (body_rect.height - evt.clientY + 4) + 'px'; 212 | } else { 213 | ctx_menu.style.top = (evt.clientY+4) + 'px'; 214 | ctx_menu.style.removeProperty('bottom'); 215 | } 216 | 217 | ctx_menu.style.removeProperty('visibility'); 218 | } 219 | evt.preventDefault(); 220 | } 221 | 222 | function set_ctx_menu_handlers(ctx_menu) { 223 | document.getElementById("switchmode").onclick = function(evt) { 224 | if (color_mode === 'light') set_color_mode('dark'); 225 | else set_color_mode('light'); 226 | menu_action_done(); 227 | }; 228 | 229 | document.getElementById("clearcanvas").onclick = function(evt) { 230 | CTX.clearRect(0, 0, CV.width, CV.height); 231 | menu_action_done(); 232 | add_to_history({ 233 | valid_index: -1, 234 | clear_history: true 235 | }); 236 | clear_history = true; 237 | be_drawing_set(); 238 | } 239 | 240 | 241 | document.getElementById("undo").onclick = function(evt) { 242 | menu_action_done(); 243 | undo(); 244 | } 245 | 246 | document.getElementById("redo").onclick = function(evt) { 247 | menu_action_done(); 248 | redo(); 249 | } 250 | 251 | document.getElementById("contextmenu_link").onclick = function(evt) { 252 | if (ctx_menu.style.display == 'block') { 253 | ctx_menu.style.display = 'none'; 254 | } else { 255 | ctx_menu_reason = 'menu'; 256 | ctx_menu.style.top = '30px'; 257 | ctx_menu.style.removeProperty('bottom'); 258 | ctx_menu.style.left = '5px'; 259 | ctx_menu.style.removeProperty('right'); 260 | ctx_menu.style.display = 'block'; 261 | } 262 | } 263 | } 264 | 265 | function menu_action_done() { 266 | if (ctx_menu_reason == 'rightclick') { 267 | ctx_menu.style.display = 'none'; 268 | } 269 | } 270 | 271 | // Actual drawing state, history and functions 272 | // ------------------------------------------- 273 | 274 | var color = 'default'; 275 | var width_multiplier = 5; // if negative: eraser 276 | 277 | const smoothing = 0.1; 278 | // There is no way to draw a bezier curve on a canvas with 279 | // a different starting and ending thickness (without writing 280 | // your own render engine). So by limiting the maximum change 281 | // in thickness, it hopefully looks fluent enough that 282 | // no one notices it. 283 | const max_pressure_diff = 0.5; 284 | 285 | // Drawing state 286 | var ongoing_curves = {}; 287 | var curves = new Array(); 288 | var curves_valid_index = -1; 289 | var drawing_history = null; // to be set later 290 | // points to current actual point in history 291 | var current_history_index = 0; 292 | var clear_history = false; // clear history if curve is added 293 | 294 | function bounded(min, val, max) { 295 | return Math.max(Math.min(val, max), min); 296 | } 297 | 298 | function add_to_history(event) { 299 | // aka 'set_present' ? 300 | if (drawing_history[current_history_index].clear_history) { 301 | console.log("clearing history"); 302 | drawing_history = [{valid_index: -1}]; 303 | current_history_index = 0; 304 | } else { 305 | // remove future if necessary 306 | drawing_history = drawing_history.slice(0, current_history_index+1); 307 | } 308 | drawing_history.push(event); 309 | current_history_index = drawing_history.length - 1; 310 | curves_valid_index = event.valid_index; 311 | } 312 | 313 | function set_history(i) { 314 | if (i<0 || i>=drawing_history.length) { 315 | console.log("ignoring set_history", i); 316 | return; 317 | } 318 | 319 | curves_valid_index = drawing_history[i].valid_index; 320 | console.log("Setting history to", i, curves_valid_index); 321 | current_history_index = i; 322 | resizeCanvas(); 323 | be_drawing_set(); 324 | } 325 | 326 | function undo() { 327 | set_history(current_history_index-1); 328 | } 329 | 330 | function redo() { 331 | set_history(current_history_index+1); 332 | } 333 | 334 | function redraw_all() { 335 | for (var i=0; i curves_valid_index) break; 338 | redraw(curve); 339 | } 340 | } 341 | 342 | function redraw(curve) { 343 | var pts = curve.points; 344 | const len = pts.length; 345 | 346 | CTX.strokeStyle = colors[color_mode][curve.color]; 347 | CTX.fillStyle = colors[color_mode][curve.color]; 348 | 349 | CTX.beginPath(); 350 | CTX.arc(pts[0].x, pts[0].y, pts[0].w/2, 0, 2*Math.PI); 351 | CTX.fill(); 352 | if (len == 2) { 353 | straight_line(pts[0], pts[1], curve, CTX); 354 | } else if (len > 2) { 355 | bezier_line(pts[0], pts[0], pts[1], pts[2], curve); 356 | for (var i=1; i 3) { 404 | CTX.beginPath(); 405 | CTX.arc(c.x, c.y, c.w/2, 0, 2*Math.PI); 406 | CTX.fill(); 407 | } 408 | 409 | if (dist <= 3) { 410 | straight_line(b, c, curve, CTX); 411 | return; 412 | } 413 | 414 | const c1 = bezier_controlpoint(a, b, c, true); 415 | const c2 = bezier_controlpoint(b, c, d, false); 416 | 417 | CTX.beginPath(); 418 | CTX.moveTo(b.x, b.y); 419 | CTX.bezierCurveTo(c1.x, c1.y, c2.x, c2.y, c.x, c.y); 420 | CTX.lineWidth = c.w; 421 | CTX.stroke(); 422 | 423 | /* 424 | CTX.beginPath(); 425 | CTX.arc(b.x, b.y, 8, 0, 2*Math.PI); 426 | CTX.strokeStyle = 'red'; 427 | CTX.stroke(); 428 | CTX.strokeStyle = 'black'; 429 | 430 | [c1, c2].forEach(p => { 431 | CTX.beginPath(); 432 | CTX.arc(p.x, p.y, 3, 0, 2*Math.PI); 433 | CTX.fillStyle = 'green'; 434 | CTX.fill(); 435 | CTX.fillStyle = 'black'; 436 | }); 437 | */ 438 | } 439 | 440 | function add_and_draw_point(curve, point, is_end) { 441 | var pts = curve.points; 442 | pts.push(point); 443 | var len = pts.length; 444 | var prev_point = pts[len-2]; 445 | 446 | // First we add the most immediate point as a simple line 447 | straight_line(prev_point, point, curve); 448 | 449 | // Then we do bezier stuff if possible :) 450 | if (len == 3) { 451 | remove_straight_line(pts[0], pts[1], curve); 452 | bezier_line(pts[0], pts[0], pts[1], pts[2], curve); 453 | } else if (len >= 4) { 454 | remove_straight_line(pts[len-3], pts[len-2], curve); 455 | bezier_line(pts[len-4], pts[len-3], pts[len-2], pts[len-1], curve); 456 | } 457 | 458 | if (is_end) { 459 | remove_straight_line(pts[len-2], pts[len-1], curve); 460 | bezier_line(pts[len-3], pts[len-2], pts[len-1], pts[len-1], curve); 461 | } 462 | } 463 | 464 | function event_to_point(evt, width_mult, prev_width) { 465 | var width = (prev_width == undefined) ? 466 | (evt.pressure * width_mult) : 467 | bounded(prev_width - max_pressure_diff, evt.pressure * width_mult, prev_width + max_pressure_diff); 468 | 469 | return { 470 | x: evt.clientX, 471 | y: evt.clientY, 472 | p: evt.pressure, 473 | w: width, 474 | t: Date.now(), 475 | }; 476 | } 477 | 478 | function handle_start(evt) { 479 | // 1 -> actual drawing 480 | // 2 -> right mouse 481 | // 4 -> second button on my wacom 482 | if (evt.buttons === 1) { 483 | var pt = event_to_point(evt, width_multiplier); 484 | var curve = { 485 | index: curves_valid_index+1, 486 | color: color, 487 | width_multiplier: width_multiplier, 488 | points: [pt] 489 | }; 490 | CTX.strokeStyle = colors[color_mode][curve.color]; 491 | CTX.fillStyle = colors[color_mode][curve.color]; 492 | FRCTX.strokeStyle = colors[color_mode][curve.color]; 493 | ongoing_curves[evt.pointerId] = curve; 494 | 495 | CTX.beginPath(); 496 | CTX.arc(pt.x, pt.y, pt.w/2, 0, 2*Math.PI); 497 | CTX.fill(); 498 | } 499 | if (evt.buttons !== 2) { 500 | document.getElementById('contextmenu').style.display = 'none'; 501 | } 502 | } 503 | 504 | function handle_move(evt) { 505 | var curve = ongoing_curves[evt.pointerId]; 506 | if (curve != undefined) { 507 | var pt = event_to_point(evt, curve.width_multiplier, curve.points[curve.points.length-1].w); 508 | add_and_draw_point(curve, pt); 509 | } 510 | } 511 | 512 | function finalize_curve(curve, pointer_id) { 513 | delete ongoing_curves[pointer_id]; 514 | 515 | add_to_history({ 516 | valid_index: curves_valid_index+1, 517 | clear_history: false 518 | }); 519 | 520 | var last_valid_arr_index = -1; 521 | for (var i=curves.length-1; i>=0; i--) { 522 | var existing_curve = curves[i]; 523 | if (existing_curve.index <= curves_valid_index-1) { 524 | last_valid_arr_index = i; 525 | break; 526 | } 527 | } 528 | curves = curves.slice(0, last_valid_arr_index+1); 529 | 530 | curves.push(curve); 531 | var pts = curve.points; 532 | var total_length = 0; 533 | var prev = pts[0]; 534 | for (var i=1; i 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The GNU Affero General Public License is a free, copyleft license for 12 | software and other kinds of works, specifically designed to ensure 13 | cooperation with the community in the case of network server software. 14 | 15 | The licenses for most software and other practical works are designed 16 | to take away your freedom to share and change the works. By contrast, 17 | our General Public Licenses are intended to guarantee your freedom to 18 | share and change all versions of a program--to make sure it remains free 19 | software for all its users. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | them if you wish), that you receive source code or can get it if you 25 | want it, that you can change the software or use pieces of it in new 26 | free programs, and that you know you can do these things. 27 | 28 | Developers that use our General Public Licenses protect your rights 29 | with two steps: (1) assert copyright on the software, and (2) offer 30 | you this License which gives you legal permission to copy, distribute 31 | and/or modify the software. 32 | 33 | A secondary benefit of defending all users' freedom is that 34 | improvements made in alternate versions of the program, if they 35 | receive widespread use, become available for other developers to 36 | incorporate. Many developers of free software are heartened and 37 | encouraged by the resulting cooperation. However, in the case of 38 | software used on network servers, this result may fail to come about. 39 | The GNU General Public License permits making a modified version and 40 | letting the public access it on a server without ever releasing its 41 | source code to the public. 42 | 43 | The GNU Affero General Public License is designed specifically to 44 | ensure that, in such cases, the modified source code becomes available 45 | to the community. It requires the operator of a network server to 46 | provide the source code of the modified version running there to the 47 | users of that server. Therefore, public use of a modified version, on 48 | a publicly accessible server, gives the public access to the source 49 | code of the modified version. 50 | 51 | An older license, called the Affero General Public License and 52 | published by Affero, was designed to accomplish similar goals. This is 53 | a different license, not a version of the Affero GPL, but Affero has 54 | released a new version of the Affero GPL which permits relicensing under 55 | this license. 56 | 57 | The precise terms and conditions for copying, distribution and 58 | modification follow. 59 | 60 | TERMS AND CONDITIONS 61 | 62 | 0. Definitions. 63 | 64 | "This License" refers to version 3 of the GNU Affero General Public License. 65 | 66 | "Copyright" also means copyright-like laws that apply to other kinds of 67 | works, such as semiconductor masks. 68 | 69 | "The Program" refers to any copyrightable work licensed under this 70 | License. Each licensee is addressed as "you". "Licensees" and 71 | "recipients" may be individuals or organizations. 72 | 73 | To "modify" a work means to copy from or adapt all or part of the work 74 | in a fashion requiring copyright permission, other than the making of an 75 | exact copy. The resulting work is called a "modified version" of the 76 | earlier work or a work "based on" the earlier work. 77 | 78 | A "covered work" means either the unmodified Program or a work based 79 | on the Program. 80 | 81 | To "propagate" a work means to do anything with it that, without 82 | permission, would make you directly or secondarily liable for 83 | infringement under applicable copyright law, except executing it on a 84 | computer or modifying a private copy. Propagation includes copying, 85 | distribution (with or without modification), making available to the 86 | public, and in some countries other activities as well. 87 | 88 | To "convey" a work means any kind of propagation that enables other 89 | parties to make or receive copies. Mere interaction with a user through 90 | a computer network, with no transfer of a copy, is not conveying. 91 | 92 | An interactive user interface displays "Appropriate Legal Notices" 93 | to the extent that it includes a convenient and prominently visible 94 | feature that (1) displays an appropriate copyright notice, and (2) 95 | tells the user that there is no warranty for the work (except to the 96 | extent that warranties are provided), that licensees may convey the 97 | work under this License, and how to view a copy of this License. If 98 | the interface presents a list of user commands or options, such as a 99 | menu, a prominent item in the list meets this criterion. 100 | 101 | 1. Source Code. 102 | 103 | The "source code" for a work means the preferred form of the work 104 | for making modifications to it. "Object code" means any non-source 105 | form of a work. 106 | 107 | A "Standard Interface" means an interface that either is an official 108 | standard defined by a recognized standards body, or, in the case of 109 | interfaces specified for a particular programming language, one that 110 | is widely used among developers working in that language. 111 | 112 | The "System Libraries" of an executable work include anything, other 113 | than the work as a whole, that (a) is included in the normal form of 114 | packaging a Major Component, but which is not part of that Major 115 | Component, and (b) serves only to enable use of the work with that 116 | Major Component, or to implement a Standard Interface for which an 117 | implementation is available to the public in source code form. A 118 | "Major Component", in this context, means a major essential component 119 | (kernel, window system, and so on) of the specific operating system 120 | (if any) on which the executable work runs, or a compiler used to 121 | produce the work, or an object code interpreter used to run it. 122 | 123 | The "Corresponding Source" for a work in object code form means all 124 | the source code needed to generate, install, and (for an executable 125 | work) run the object code and to modify the work, including scripts to 126 | control those activities. However, it does not include the work's 127 | System Libraries, or general-purpose tools or generally available free 128 | programs which are used unmodified in performing those activities but 129 | which are not part of the work. For example, Corresponding Source 130 | includes interface definition files associated with source files for 131 | the work, and the source code for shared libraries and dynamically 132 | linked subprograms that the work is specifically designed to require, 133 | such as by intimate data communication or control flow between those 134 | subprograms and other parts of the work. 135 | 136 | The Corresponding Source need not include anything that users 137 | can regenerate automatically from other parts of the Corresponding 138 | Source. 139 | 140 | The Corresponding Source for a work in source code form is that 141 | same work. 142 | 143 | 2. Basic Permissions. 144 | 145 | All rights granted under this License are granted for the term of 146 | copyright on the Program, and are irrevocable provided the stated 147 | conditions are met. This License explicitly affirms your unlimited 148 | permission to run the unmodified Program. The output from running a 149 | covered work is covered by this License only if the output, given its 150 | content, constitutes a covered work. This License acknowledges your 151 | rights of fair use or other equivalent, as provided by copyright law. 152 | 153 | You may make, run and propagate covered works that you do not 154 | convey, without conditions so long as your license otherwise remains 155 | in force. You may convey covered works to others for the sole purpose 156 | of having them make modifications exclusively for you, or provide you 157 | with facilities for running those works, provided that you comply with 158 | the terms of this License in conveying all material for which you do 159 | not control copyright. Those thus making or running the covered works 160 | for you must do so exclusively on your behalf, under your direction 161 | and control, on terms that prohibit them from making any copies of 162 | your copyrighted material outside their relationship with you. 163 | 164 | Conveying under any other circumstances is permitted solely under 165 | the conditions stated below. Sublicensing is not allowed; section 10 166 | makes it unnecessary. 167 | 168 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 169 | 170 | No covered work shall be deemed part of an effective technological 171 | measure under any applicable law fulfilling obligations under article 172 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 173 | similar laws prohibiting or restricting circumvention of such 174 | measures. 175 | 176 | When you convey a covered work, you waive any legal power to forbid 177 | circumvention of technological measures to the extent such circumvention 178 | is effected by exercising rights under this License with respect to 179 | the covered work, and you disclaim any intention to limit operation or 180 | modification of the work as a means of enforcing, against the work's 181 | users, your or third parties' legal rights to forbid circumvention of 182 | technological measures. 183 | 184 | 4. Conveying Verbatim Copies. 185 | 186 | You may convey verbatim copies of the Program's source code as you 187 | receive it, in any medium, provided that you conspicuously and 188 | appropriately publish on each copy an appropriate copyright notice; 189 | keep intact all notices stating that this License and any 190 | non-permissive terms added in accord with section 7 apply to the code; 191 | keep intact all notices of the absence of any warranty; and give all 192 | recipients a copy of this License along with the Program. 193 | 194 | You may charge any price or no price for each copy that you convey, 195 | and you may offer support or warranty protection for a fee. 196 | 197 | 5. Conveying Modified Source Versions. 198 | 199 | You may convey a work based on the Program, or the modifications to 200 | produce it from the Program, in the form of source code under the 201 | terms of section 4, provided that you also meet all of these conditions: 202 | 203 | a) The work must carry prominent notices stating that you modified 204 | it, and giving a relevant date. 205 | 206 | b) The work must carry prominent notices stating that it is 207 | released under this License and any conditions added under section 208 | 7. This requirement modifies the requirement in section 4 to 209 | "keep intact all notices". 210 | 211 | c) You must license the entire work, as a whole, under this 212 | License to anyone who comes into possession of a copy. This 213 | License will therefore apply, along with any applicable section 7 214 | additional terms, to the whole of the work, and all its parts, 215 | regardless of how they are packaged. This License gives no 216 | permission to license the work in any other way, but it does not 217 | invalidate such permission if you have separately received it. 218 | 219 | d) If the work has interactive user interfaces, each must display 220 | Appropriate Legal Notices; however, if the Program has interactive 221 | interfaces that do not display Appropriate Legal Notices, your 222 | work need not make them do so. 223 | 224 | A compilation of a covered work with other separate and independent 225 | works, which are not by their nature extensions of the covered work, 226 | and which are not combined with it such as to form a larger program, 227 | in or on a volume of a storage or distribution medium, is called an 228 | "aggregate" if the compilation and its resulting copyright are not 229 | used to limit the access or legal rights of the compilation's users 230 | beyond what the individual works permit. Inclusion of a covered work 231 | in an aggregate does not cause this License to apply to the other 232 | parts of the aggregate. 233 | 234 | 6. Conveying Non-Source Forms. 235 | 236 | You may convey a covered work in object code form under the terms 237 | of sections 4 and 5, provided that you also convey the 238 | machine-readable Corresponding Source under the terms of this License, 239 | in one of these ways: 240 | 241 | a) Convey the object code in, or embodied in, a physical product 242 | (including a physical distribution medium), accompanied by the 243 | Corresponding Source fixed on a durable physical medium 244 | customarily used for software interchange. 245 | 246 | b) Convey the object code in, or embodied in, a physical product 247 | (including a physical distribution medium), accompanied by a 248 | written offer, valid for at least three years and valid for as 249 | long as you offer spare parts or customer support for that product 250 | model, to give anyone who possesses the object code either (1) a 251 | copy of the Corresponding Source for all the software in the 252 | product that is covered by this License, on a durable physical 253 | medium customarily used for software interchange, for a price no 254 | more than your reasonable cost of physically performing this 255 | conveying of source, or (2) access to copy the 256 | Corresponding Source from a network server at no charge. 257 | 258 | c) Convey individual copies of the object code with a copy of the 259 | written offer to provide the Corresponding Source. This 260 | alternative is allowed only occasionally and noncommercially, and 261 | only if you received the object code with such an offer, in accord 262 | with subsection 6b. 263 | 264 | d) Convey the object code by offering access from a designated 265 | place (gratis or for a charge), and offer equivalent access to the 266 | Corresponding Source in the same way through the same place at no 267 | further charge. You need not require recipients to copy the 268 | Corresponding Source along with the object code. If the place to 269 | copy the object code is a network server, the Corresponding Source 270 | may be on a different server (operated by you or a third party) 271 | that supports equivalent copying facilities, provided you maintain 272 | clear directions next to the object code saying where to find the 273 | Corresponding Source. Regardless of what server hosts the 274 | Corresponding Source, you remain obligated to ensure that it is 275 | available for as long as needed to satisfy these requirements. 276 | 277 | e) Convey the object code using peer-to-peer transmission, provided 278 | you inform other peers where the object code and Corresponding 279 | Source of the work are being offered to the general public at no 280 | charge under subsection 6d. 281 | 282 | A separable portion of the object code, whose source code is excluded 283 | from the Corresponding Source as a System Library, need not be 284 | included in conveying the object code work. 285 | 286 | A "User Product" is either (1) a "consumer product", which means any 287 | tangible personal property which is normally used for personal, family, 288 | or household purposes, or (2) anything designed or sold for incorporation 289 | into a dwelling. In determining whether a product is a consumer product, 290 | doubtful cases shall be resolved in favor of coverage. For a particular 291 | product received by a particular user, "normally used" refers to a 292 | typical or common use of that class of product, regardless of the status 293 | of the particular user or of the way in which the particular user 294 | actually uses, or expects or is expected to use, the product. A product 295 | is a consumer product regardless of whether the product has substantial 296 | commercial, industrial or non-consumer uses, unless such uses represent 297 | the only significant mode of use of the product. 298 | 299 | "Installation Information" for a User Product means any methods, 300 | procedures, authorization keys, or other information required to install 301 | and execute modified versions of a covered work in that User Product from 302 | a modified version of its Corresponding Source. The information must 303 | suffice to ensure that the continued functioning of the modified object 304 | code is in no case prevented or interfered with solely because 305 | modification has been made. 306 | 307 | If you convey an object code work under this section in, or with, or 308 | specifically for use in, a User Product, and the conveying occurs as 309 | part of a transaction in which the right of possession and use of the 310 | User Product is transferred to the recipient in perpetuity or for a 311 | fixed term (regardless of how the transaction is characterized), the 312 | Corresponding Source conveyed under this section must be accompanied 313 | by the Installation Information. But this requirement does not apply 314 | if neither you nor any third party retains the ability to install 315 | modified object code on the User Product (for example, the work has 316 | been installed in ROM). 317 | 318 | The requirement to provide Installation Information does not include a 319 | requirement to continue to provide support service, warranty, or updates 320 | for a work that has been modified or installed by the recipient, or for 321 | the User Product in which it has been modified or installed. Access to a 322 | network may be denied when the modification itself materially and 323 | adversely affects the operation of the network or violates the rules and 324 | protocols for communication across the network. 325 | 326 | Corresponding Source conveyed, and Installation Information provided, 327 | in accord with this section must be in a format that is publicly 328 | documented (and with an implementation available to the public in 329 | source code form), and must require no special password or key for 330 | unpacking, reading or copying. 331 | 332 | 7. Additional Terms. 333 | 334 | "Additional permissions" are terms that supplement the terms of this 335 | License by making exceptions from one or more of its conditions. 336 | Additional permissions that are applicable to the entire Program shall 337 | be treated as though they were included in this License, to the extent 338 | that they are valid under applicable law. If additional permissions 339 | apply only to part of the Program, that part may be used separately 340 | under those permissions, but the entire Program remains governed by 341 | this License without regard to the additional permissions. 342 | 343 | When you convey a copy of a covered work, you may at your option 344 | remove any additional permissions from that copy, or from any part of 345 | it. (Additional permissions may be written to require their own 346 | removal in certain cases when you modify the work.) You may place 347 | additional permissions on material, added by you to a covered work, 348 | for which you have or can give appropriate copyright permission. 349 | 350 | Notwithstanding any other provision of this License, for material you 351 | add to a covered work, you may (if authorized by the copyright holders of 352 | that material) supplement the terms of this License with terms: 353 | 354 | a) Disclaiming warranty or limiting liability differently from the 355 | terms of sections 15 and 16 of this License; or 356 | 357 | b) Requiring preservation of specified reasonable legal notices or 358 | author attributions in that material or in the Appropriate Legal 359 | Notices displayed by works containing it; or 360 | 361 | c) Prohibiting misrepresentation of the origin of that material, or 362 | requiring that modified versions of such material be marked in 363 | reasonable ways as different from the original version; or 364 | 365 | d) Limiting the use for publicity purposes of names of licensors or 366 | authors of the material; or 367 | 368 | e) Declining to grant rights under trademark law for use of some 369 | trade names, trademarks, or service marks; or 370 | 371 | f) Requiring indemnification of licensors and authors of that 372 | material by anyone who conveys the material (or modified versions of 373 | it) with contractual assumptions of liability to the recipient, for 374 | any liability that these contractual assumptions directly impose on 375 | those licensors and authors. 376 | 377 | All other non-permissive additional terms are considered "further 378 | restrictions" within the meaning of section 10. If the Program as you 379 | received it, or any part of it, contains a notice stating that it is 380 | governed by this License along with a term that is a further 381 | restriction, you may remove that term. If a license document contains 382 | a further restriction but permits relicensing or conveying under this 383 | License, you may add to a covered work material governed by the terms 384 | of that license document, provided that the further restriction does 385 | not survive such relicensing or conveying. 386 | 387 | If you add terms to a covered work in accord with this section, you 388 | must place, in the relevant source files, a statement of the 389 | additional terms that apply to those files, or a notice indicating 390 | where to find the applicable terms. 391 | 392 | Additional terms, permissive or non-permissive, may be stated in the 393 | form of a separately written license, or stated as exceptions; 394 | the above requirements apply either way. 395 | 396 | 8. Termination. 397 | 398 | You may not propagate or modify a covered work except as expressly 399 | provided under this License. Any attempt otherwise to propagate or 400 | modify it is void, and will automatically terminate your rights under 401 | this License (including any patent licenses granted under the third 402 | paragraph of section 11). 403 | 404 | However, if you cease all violation of this License, then your 405 | license from a particular copyright holder is reinstated (a) 406 | provisionally, unless and until the copyright holder explicitly and 407 | finally terminates your license, and (b) permanently, if the copyright 408 | holder fails to notify you of the violation by some reasonable means 409 | prior to 60 days after the cessation. 410 | 411 | Moreover, your license from a particular copyright holder is 412 | reinstated permanently if the copyright holder notifies you of the 413 | violation by some reasonable means, this is the first time you have 414 | received notice of violation of this License (for any work) from that 415 | copyright holder, and you cure the violation prior to 30 days after 416 | your receipt of the notice. 417 | 418 | Termination of your rights under this section does not terminate the 419 | licenses of parties who have received copies or rights from you under 420 | this License. If your rights have been terminated and not permanently 421 | reinstated, you do not qualify to receive new licenses for the same 422 | material under section 10. 423 | 424 | 9. Acceptance Not Required for Having Copies. 425 | 426 | You are not required to accept this License in order to receive or 427 | run a copy of the Program. Ancillary propagation of a covered work 428 | occurring solely as a consequence of using peer-to-peer transmission 429 | to receive a copy likewise does not require acceptance. However, 430 | nothing other than this License grants you permission to propagate or 431 | modify any covered work. These actions infringe copyright if you do 432 | not accept this License. Therefore, by modifying or propagating a 433 | covered work, you indicate your acceptance of this License to do so. 434 | 435 | 10. Automatic Licensing of Downstream Recipients. 436 | 437 | Each time you convey a covered work, the recipient automatically 438 | receives a license from the original licensors, to run, modify and 439 | propagate that work, subject to this License. You are not responsible 440 | for enforcing compliance by third parties with this License. 441 | 442 | An "entity transaction" is a transaction transferring control of an 443 | organization, or substantially all assets of one, or subdividing an 444 | organization, or merging organizations. If propagation of a covered 445 | work results from an entity transaction, each party to that 446 | transaction who receives a copy of the work also receives whatever 447 | licenses to the work the party's predecessor in interest had or could 448 | give under the previous paragraph, plus a right to possession of the 449 | Corresponding Source of the work from the predecessor in interest, if 450 | the predecessor has it or can get it with reasonable efforts. 451 | 452 | You may not impose any further restrictions on the exercise of the 453 | rights granted or affirmed under this License. For example, you may 454 | not impose a license fee, royalty, or other charge for exercise of 455 | rights granted under this License, and you may not initiate litigation 456 | (including a cross-claim or counterclaim in a lawsuit) alleging that 457 | any patent claim is infringed by making, using, selling, offering for 458 | sale, or importing the Program or any portion of it. 459 | 460 | 11. Patents. 461 | 462 | A "contributor" is a copyright holder who authorizes use under this 463 | License of the Program or a work on which the Program is based. The 464 | work thus licensed is called the contributor's "contributor version". 465 | 466 | A contributor's "essential patent claims" are all patent claims 467 | owned or controlled by the contributor, whether already acquired or 468 | hereafter acquired, that would be infringed by some manner, permitted 469 | by this License, of making, using, or selling its contributor version, 470 | but do not include claims that would be infringed only as a 471 | consequence of further modification of the contributor version. For 472 | purposes of this definition, "control" includes the right to grant 473 | patent sublicenses in a manner consistent with the requirements of 474 | this License. 475 | 476 | Each contributor grants you a non-exclusive, worldwide, royalty-free 477 | patent license under the contributor's essential patent claims, to 478 | make, use, sell, offer for sale, import and otherwise run, modify and 479 | propagate the contents of its contributor version. 480 | 481 | In the following three paragraphs, a "patent license" is any express 482 | agreement or commitment, however denominated, not to enforce a patent 483 | (such as an express permission to practice a patent or covenant not to 484 | sue for patent infringement). To "grant" such a patent license to a 485 | party means to make such an agreement or commitment not to enforce a 486 | patent against the party. 487 | 488 | If you convey a covered work, knowingly relying on a patent license, 489 | and the Corresponding Source of the work is not available for anyone 490 | to copy, free of charge and under the terms of this License, through a 491 | publicly available network server or other readily accessible means, 492 | then you must either (1) cause the Corresponding Source to be so 493 | available, or (2) arrange to deprive yourself of the benefit of the 494 | patent license for this particular work, or (3) arrange, in a manner 495 | consistent with the requirements of this License, to extend the patent 496 | license to downstream recipients. "Knowingly relying" means you have 497 | actual knowledge that, but for the patent license, your conveying the 498 | covered work in a country, or your recipient's use of the covered work 499 | in a country, would infringe one or more identifiable patents in that 500 | country that you have reason to believe are valid. 501 | 502 | If, pursuant to or in connection with a single transaction or 503 | arrangement, you convey, or propagate by procuring conveyance of, a 504 | covered work, and grant a patent license to some of the parties 505 | receiving the covered work authorizing them to use, propagate, modify 506 | or convey a specific copy of the covered work, then the patent license 507 | you grant is automatically extended to all recipients of the covered 508 | work and works based on it. 509 | 510 | A patent license is "discriminatory" if it does not include within 511 | the scope of its coverage, prohibits the exercise of, or is 512 | conditioned on the non-exercise of one or more of the rights that are 513 | specifically granted under this License. You may not convey a covered 514 | work if you are a party to an arrangement with a third party that is 515 | in the business of distributing software, under which you make payment 516 | to the third party based on the extent of your activity of conveying 517 | the work, and under which the third party grants, to any of the 518 | parties who would receive the covered work from you, a discriminatory 519 | patent license (a) in connection with copies of the covered work 520 | conveyed by you (or copies made from those copies), or (b) primarily 521 | for and in connection with specific products or compilations that 522 | contain the covered work, unless you entered into that arrangement, 523 | or that patent license was granted, prior to 28 March 2007. 524 | 525 | Nothing in this License shall be construed as excluding or limiting 526 | any implied license or other defenses to infringement that may 527 | otherwise be available to you under applicable patent law. 528 | 529 | 12. No Surrender of Others' Freedom. 530 | 531 | If conditions are imposed on you (whether by court order, agreement or 532 | otherwise) that contradict the conditions of this License, they do not 533 | excuse you from the conditions of this License. If you cannot convey a 534 | covered work so as to satisfy simultaneously your obligations under this 535 | License and any other pertinent obligations, then as a consequence you may 536 | not convey it at all. For example, if you agree to terms that obligate you 537 | to collect a royalty for further conveying from those to whom you convey 538 | the Program, the only way you could satisfy both those terms and this 539 | License would be to refrain entirely from conveying the Program. 540 | 541 | 13. Remote Network Interaction; Use with the GNU General Public License. 542 | 543 | Notwithstanding any other provision of this License, if you modify the 544 | Program, your modified version must prominently offer all users 545 | interacting with it remotely through a computer network (if your version 546 | supports such interaction) an opportunity to receive the Corresponding 547 | Source of your version by providing access to the Corresponding Source 548 | from a network server at no charge, through some standard or customary 549 | means of facilitating copying of software. This Corresponding Source 550 | shall include the Corresponding Source for any work covered by version 3 551 | of the GNU General Public License that is incorporated pursuant to the 552 | following paragraph. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the work with which it is combined will remain governed by version 560 | 3 of the GNU General Public License. 561 | 562 | 14. Revised Versions of this License. 563 | 564 | The Free Software Foundation may publish revised and/or new versions of 565 | the GNU Affero General Public License from time to time. Such new versions 566 | will be similar in spirit to the present version, but may differ in detail to 567 | address new problems or concerns. 568 | 569 | Each version is given a distinguishing version number. If the 570 | Program specifies that a certain numbered version of the GNU Affero General 571 | Public License "or any later version" applies to it, you have the 572 | option of following the terms and conditions either of that numbered 573 | version or of any later version published by the Free Software 574 | Foundation. If the Program does not specify a version number of the 575 | GNU Affero General Public License, you may choose any version ever published 576 | by the Free Software Foundation. 577 | 578 | If the Program specifies that a proxy can decide which future 579 | versions of the GNU Affero General Public License can be used, that proxy's 580 | public statement of acceptance of a version permanently authorizes you 581 | to choose that version for the Program. 582 | 583 | Later license versions may give you additional or different 584 | permissions. However, no additional obligations are imposed on any 585 | author or copyright holder as a result of your choosing to follow a 586 | later version. 587 | 588 | 15. Disclaimer of Warranty. 589 | 590 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 591 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 592 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 593 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 594 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 595 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 596 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 597 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 598 | 599 | 16. Limitation of Liability. 600 | 601 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 602 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 603 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 604 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 605 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 606 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 607 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 608 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 609 | SUCH DAMAGES. 610 | 611 | 17. Interpretation of Sections 15 and 16. 612 | 613 | If the disclaimer of warranty and limitation of liability provided 614 | above cannot be given local legal effect according to their terms, 615 | reviewing courts shall apply local law that most closely approximates 616 | an absolute waiver of all civil liability in connection with the 617 | Program, unless a warranty or assumption of liability accompanies a 618 | copy of the Program in return for a fee. 619 | 620 | END OF TERMS AND CONDITIONS 621 | --------------------------------------------------------------------------------