api.ps
488 lines
| 14.0 KiB
| application/postscript
|
PostScriptLexer
/ src / api.ps
r1 | ||||
r25 | (in-package sugar-qsp.api) | |||
r1 | ||||
;;; API deals with DOM manipulation and some bookkeeping for the | ||||
;;; intrinsics, namely variables | ||||
;;; API is an implementation detail and has no QSP documentation. It | ||||
;;; doesn't call intrinsics | ||||
r6 | ;;; Utils | |||
r25 | (defun make-act-html (title img) | |||
r32 | (+ "<a class='qsp-act' href='" (href-call call-act title) "' onmouseover='" (inline-call select-act title) "'>" | |||
r30 | (if img (+ "<img src='" img "'>") "") | |||
r6 | title | |||
"</a>")) | ||||
r25 | (defun make-menu-item-html (num title img loc) | |||
r32 | (+ "<a href='" (href-call finish-menu loc) "'>" | |||
r30 | (if img (+ "<img src='" img "'>") "") | |||
r11 | title | |||
"</a>")) | ||||
r32 | (defun make-obj (title img selected) | |||
r33 | (+ "<li onclick='" (inline-call select-obj title img) "'>" | |||
"<a class='qsp-obj" (if selected " qsp-obj-selected" "") "'>" | ||||
r32 | (if img (+ "<img src='" img "'>") "") | |||
r33 | title | |||
r32 | "</a>")) | |||
r30 | (defun make-menu-delimiter () | |||
"<hr>") | ||||
r41 | (defun copy-obj (obj) | |||
(chain *j-s-o-n (parse (chain *j-s-o-n (stringify obj))))) | ||||
r25 | (defun report-error (text) | |||
r20 | (alert text)) | |||
r29 | (defun start-sleeping () | |||
r30 | (chain (by-id "qsp") class-list (add "disable"))) | |||
r29 | ||||
(defun finish-sleeping () | ||||
r30 | (chain (by-id "qsp") class-list (remove "disable"))) | |||
r29 | ||||
r25 | (defun sleep (msec) | |||
r30 | (with-sleep (resume) | |||
(set-timeout resume msec))) | ||||
r24 | ||||
r25 | (defun init-dom () | |||
r18 | ;; Save/load buttons | |||
r25 | (let ((btn (by-id "qsp-btn-save"))) | |||
(setf (@ btn onclick) savegame) | ||||
(setf (@ btn href) "#")) | ||||
(let ((btn (by-id "qsp-btn-open"))) | ||||
(setf (@ btn onclick) opengame) | ||||
(setf (@ btn href) "#")) | ||||
r18 | ;; Close image on click | |||
r25 | (setf (@ (by-id "qsp-image-container") onclick) | |||
r32 | show-image) | |||
r33 | ;; Enter in input field | |||
r32 | (setf (@ (get-frame :input) onkeyup) | |||
on-input-key) | ||||
r18 | ;; Close the dropdown on any click | |||
r25 | (setf (@ window onclick) | |||
r18 | (lambda (event) | |||
r30 | (setf (@ window mouse) | |||
(list (@ event page-x) | ||||
(@ event page-y))) | ||||
(finish-menu nil)))) | ||||
r18 | ||||
r25 | (defun call-serv-loc (var-name &rest args) | |||
r37 | (let ((loc-name (get-global var-name 0))) | |||
r20 | (when loc-name | |||
r39 | (let ((loc (getprop *locs loc-name))) | |||
r20 | (when loc | |||
r32 | (call-loc loc-name args)))))) | |||
r11 | ||||
r31 | (defun filename-game (filename) | |||
(let ((game-name (chain filename (match "(.*/)?([^.]+)(\\.[a-zA-Z]+)?") 2)))) | ||||
r39 | (getprop *games game-name)) | |||
r31 | ||||
(defun run-game (name) | ||||
(let ((game (filename-game name))) | ||||
r39 | (setf *main-game name) | |||
r31 | ;; Replace locations with the new game's | |||
r39 | (setf *locs game) | |||
r31 | (funcall (getprop game | |||
(chain *object (keys game) 0)) | ||||
(list)))) | ||||
r9 | ;;; Misc | |||
r25 | (defun newline (key) | |||
(append-id (key-to-id key) "<br>" t)) | ||||
r22 | ||||
r25 | (defun clear-id (id) | |||
(setf (inner-html (by-id id)) "")) | ||||
r22 | ||||
r36 | (defun escape-html (text) | |||
(chain text | ||||
(replace (regex "/&/g") "&") | ||||
(replace (regex "/</g") "<") | ||||
(replace (regex "/>/g") ">") | ||||
(replace (regex "/\"/g") """) | ||||
(replace (regex "/'/g") "'"))) | ||||
r22 | ||||
r25 | (defun prepare-contents (s &optional force-html) | |||
r37 | (setf s (chain s (to-string))) | |||
(if (or force-html (get-global "USEHTML" 0)) | ||||
r22 | s | |||
r36 | (escape-html s))) | |||
r9 | ||||
r25 | (defun get-id (id &optional force-html) | |||
(inner-html (by-id id))) | ||||
r9 | ||||
r25 | (defun set-id (id contents &optional force-html) | |||
(setf (inner-html (by-id id)) (prepare-contents contents force-html))) | ||||
r9 | ||||
r25 | (defun append-id (id contents &optional force-html) | |||
r22 | (when contents | |||
r25 | (incf (inner-html (by-id id)) (prepare-contents contents force-html)))) | |||
r9 | ||||
r32 | (defun on-input-key (ev) | |||
(when (= 13 (@ ev key-code)) | ||||
(chain ev (prevent-default)) | ||||
r37 | (call-serv-loc "$USERCOM"))) | |||
r32 | ||||
r1 | ;;; Function calls | |||
r25 | (defun init-args (args) | |||
r1 | (dotimes (i (length args)) | |||
r16 | (let ((arg (elt args i))) | |||
(if (numberp arg) | ||||
r25 | (set-var args i :num arg) | |||
(set-var args i :str arg))))) | ||||
r1 | ||||
r25 | (defun get-result () | |||
r37 | (or (get-global "$RESULT" 0) | |||
(get-global "RESULT" 0))) | ||||
r1 | ||||
r25 | (defun call-loc (name args) | |||
r30 | (setf name (chain name (to-upper-case))) | |||
r23 | (with-frame | |||
r25 | (with-call-args args | |||
r41 | (funcall (getprop *locs name)))) | |||
(void)) | ||||
r23 | ||||
r25 | (defun call-act (title) | |||
r41 | (setf *current-action title) | |||
(with-frame | ||||
(funcall (getprop *acts title :act))) | ||||
(setf *current-action nil) | ||||
(void)) | ||||
r21 | ||||
r1 | ;;; Text windows | |||
r25 | (defun key-to-id (key) | |||
r6 | (case key | |||
r32 | (:all "qsp") | |||
r6 | (:main "qsp-main") | |||
(:stat "qsp-stat") | ||||
r11 | (:objs "qsp-objs") | |||
(:acts "qsp-acts") | ||||
(:input "qsp-input") | ||||
r30 | (:image "qsp-image") | |||
r11 | (:dropdown "qsp-dropdown") | |||
r25 | (t (report-error "Internal error!")))) | |||
r6 | ||||
r25 | (defun get-frame (key) | |||
(by-id (key-to-id key))) | ||||
r11 | ||||
r25 | (defun add-text (key text) | |||
(append-id (key-to-id key) text)) | ||||
r6 | ||||
r25 | (defun get-text (key) | |||
(get-id (key-to-id key))) | ||||
r6 | ||||
r25 | (defun clear-text (key) | |||
(clear-id (key-to-id key))) | ||||
r6 | ||||
r25 | (defun enable-frame (key enable) | |||
(let ((obj (get-frame key))) | ||||
(setf (@ obj style display) (if enable "block" "none")) | ||||
r27 | (void))) | |||
r11 | ||||
r1 | ;;; Actions | |||
r25 | (defun add-act (title img act) | |||
r39 | (setf (getprop *acts title) | |||
r32 | (create :title title :img img :act act :selected nil)) | |||
r25 | (update-acts)) | |||
r6 | ||||
r41 | (defun del-act (&optional title) | |||
r40 | (delete (getprop *acts (or title *current-action))) | |||
r25 | (update-acts)) | |||
r6 | ||||
r25 | (defun clear-act () | |||
r39 | (setf *acts (create)) | |||
r32 | (update-acts)) | |||
r6 | ||||
r25 | (defun update-acts () | |||
(clear-id "qsp-acts") | ||||
(let ((elt (by-id "qsp-acts"))) | ||||
r39 | (for-in (title *acts) | |||
(let ((obj (getprop *acts title))) | ||||
r25 | (incf (inner-html elt) (make-act-html title (getprop obj :img))))))) | |||
r32 | (defun select-act (title) | |||
r39 | (loop :for (k v) :of *acts | |||
r33 | :do (setf (getprop v :selected) nil)) | |||
r39 | (setf (getprop *acts title :selected) t) | |||
r37 | (call-serv-loc "$ONACTSEL")) | |||
r1 | ||||
r19 | ;;; "Syntax" | |||
r25 | (defun qspfor (name index from to step body) | |||
(for ((i from)) | ||||
((< i to)) | ||||
((incf i step)) | ||||
(set-var name index :num i) | ||||
r36 | (unless (await (funcall body)) | |||
r25 | (return-from qspfor)))) | |||
r19 | ||||
r16 | ;;; Variables | |||
r6 | ||||
r38 | (defun new-var (slot &rest indexes) | |||
r37 | (let ((v (list))) | |||
r38 | (dolist (index indexes) | |||
r37 | (setf (elt v index) (if (eq #\$ (elt slot 0)) "" 0))) | |||
r38 | (setf (@ v :indexes) (create)) | |||
r37 | v)) | |||
r14 | ||||
r37 | (defun set-str-element (slot index value) | |||
r41 | (if (has index (getprop slot :indexes)) | |||
r37 | (setf (elt (getprop slot) | |||
(getprop slot :indexes index)) | ||||
value) | ||||
(progn | ||||
(chain slot (push value)) | ||||
(setf (elt slot index) | ||||
(length slot))))) | ||||
r6 | ||||
r37 | (defun set-any-element (slot index value) | |||
(if (numberp index) | ||||
(setf (elt slot index) value) | ||||
(set-str-element slot index value))) | ||||
r11 | ||||
r37 | (defun get-element (slot index) | |||
(if (numberp index) | ||||
(elt slot index) | ||||
(elt slot (getprop slot :indexes index)))) | ||||
r14 | ||||
r37 | (defun get-global (name index) | |||
r39 | (elt (getprop *globals name) index)) | |||
r37 | ||||
(defun kill-var (store name &optional index) | ||||
r30 | (setf name (chain name (to-upper-case))) | |||
r16 | (if (and index (not (= 0 index))) | |||
r40 | (chain (getprop *globals name) (kill index)) | |||
(delete (getprop *globals name))) | ||||
r27 | (void)) | |||
r6 | ||||
r25 | (defun array-size (name) | |||
r32 | (@ (var-ref name) :values length)) | |||
r16 | ||||
;;; Locals | ||||
r25 | (defun push-local-frame () | |||
r39 | (chain *locals (push (create))) | |||
r27 | (void)) | |||
r16 | ||||
r25 | (defun pop-local-frame () | |||
r39 | (chain *locals (pop)) | |||
r27 | (void)) | |||
r16 | ||||
r25 | (defun current-local-frame () | |||
r39 | (elt *locals (1- (length *locals)))) | |||
r16 | ||||
r6 | ;;; Objects | |||
r32 | (defun select-obj (title img) | |||
r39 | (loop :for (k v) :of *objs | |||
r33 | :do (setf (getprop v :selected) nil)) | |||
r39 | (setf (getprop *objs title :selected) t) | |||
r37 | (call-serv-loc "$ONOBJSEL" title img)) | |||
r32 | ||||
r25 | (defun update-objs () | |||
(let ((elt (by-id "qsp-objs"))) | ||||
(setf (inner-html elt) "<ul>") | ||||
r39 | (loop :for (name obj) :of *objs | |||
r32 | :do (incf (inner-html elt) | |||
r33 | (make-obj name (@ obj :img) (@ obj :selected)))) | |||
r25 | (incf (inner-html elt) "</ul>"))) | |||
r11 | ||||
;;; Menu | ||||
r30 | (defun open-menu (menu-data) | |||
(let ((elt (get-frame :dropdown)) | ||||
r11 | (i 0)) | |||
(loop :for item :in menu-data | ||||
:do (incf i) | ||||
r30 | :do (incf (inner-html elt) | |||
(if (eq item :delimiter) | ||||
(make-menu-delimiter i) | ||||
(make-menu-item-html i | ||||
(@ item :text) | ||||
(@ item :icon) | ||||
(@ item :loc))))) | ||||
(let ((mouse (@ window mouse))) | ||||
(setf (@ elt style left) (+ (elt mouse 0) "px")) | ||||
(setf (@ elt style top) (+ (elt mouse 1) "px")) | ||||
;; Make sure it's inside the viewport | ||||
(when (> (@ document body inner-width) | ||||
(+ (elt mouse 0) (@ elt inner-width))) | ||||
(incf (@ elt style left) (@ elt inner-width))) | ||||
(when (> (@ document body inner-height) | ||||
(+ (elt mouse 0) (@ elt inner-height))) | ||||
(incf (@ elt style top) (@ elt inner-height)))) | ||||
r25 | (setf (@ elt style display) "block"))) | |||
r12 | ||||
r30 | (defun finish-menu (loc) | |||
r39 | (when *menu-resume | |||
r30 | (let ((elt (get-frame :dropdown))) | |||
(setf (inner-html elt) "") | ||||
(setf (@ elt style display) "none") | ||||
r39 | (funcall *menu-resume) | |||
(setf *menu-resume nil)) | ||||
r30 | (when loc | |||
(call-loc loc))) | ||||
(void)) | ||||
(defun menu (menu-data) | ||||
(with-sleep (resume) | ||||
(open-menu menu-data) | ||||
r39 | (setf *menu-resume resume)) | |||
r30 | (void)) | |||
r12 | ;;; Content | |||
r25 | (defun clean-audio () | |||
r39 | (loop :for k :in (chain *object (keys *playing)) | |||
:for v := (getprop *playing k) | ||||
r25 | :do (when (@ v ended) | |||
r39 | (delete (@ *playing k))))) | |||
r14 | ||||
r25 | (defun show-image (path) | |||
r30 | (let ((img (get-frame :image))) | |||
r18 | (cond (path | |||
r25 | (setf (@ img src) path) | |||
(setf (@ img style display) "flex")) | ||||
r18 | (t | |||
r25 | (setf (@ img src) "") | |||
(setf (@ img style display) "hidden"))))) | ||||
r18 | ||||
r34 | (defun show-inline-images (frame-name images) | |||
(let ((frame (get-frame frame-name)) | ||||
(text "")) | ||||
(incf text "<div style='position:relative; display: inline-block'>") | ||||
(incf text (+ "<img src='" (@ images 0) "'>")) | ||||
(loop :for image :in (chain images (slice 1)) | ||||
:do (incf text | ||||
(+ "<img style='position:absolute' src='" image "'>"))) | ||||
(incf text "</div>") | ||||
(incf (inner-html frame) text))) | ||||
r32 | (defun rgb-string (rgb) | |||
r33 | (let ((red (ps::>> rgb 16)) | |||
(green (logand (ps::>> rgb 8) 255)) | ||||
(blue (logand rgb 255))) | ||||
r32 | (flet ((rgb-to-hex (comp) | |||
(let ((hex (chain (*number comp) (to-string 16)))) | ||||
(if (< (length hex) 2) | ||||
(+ "0" hex) | ||||
hex)))) | ||||
(+ "#" (rgb-to-hex red) (rgb-to-hex green) (rgb-to-hex red))))) | ||||
r18 | ;;; Saves | |||
r25 | (defun opengame () | |||
(let ((element (chain document (create-element :input)))) | ||||
(chain element (set-attribute :type :file)) | ||||
(chain element (set-attribute :id :qsp-opengame)) | ||||
(chain element (set-attribute :tabindex -1)) | ||||
(chain element (set-attribute "aria-hidden" t)) | ||||
(setf (@ element style display) :block) | ||||
(setf (@ element style visibility) :hidden) | ||||
(setf (@ element style position) :fixed) | ||||
(setf (@ element onchange) | ||||
r18 | (lambda (event) | |||
r25 | (let* ((file (@ event target files 0)) | |||
(reader (new (*file-reader)))) | ||||
(setf (@ reader onload) | ||||
r18 | (lambda (ev) | |||
(block nil | ||||
r25 | (let ((target (@ ev current-target))) | |||
(unless (@ target result) | ||||
(return)) | ||||
(base64-to-state (@ target result)) | ||||
(unstash-state))))) | ||||
(chain reader (read-as-text file))))) | ||||
(chain document body (append-child element)) | ||||
(chain element (click)) | ||||
(chain document body (remove-child element)))) | ||||
r18 | ||||
r25 | (defun savegame () | |||
(let ((element (chain document (create-element :a)))) | ||||
(chain element (set-attribute :href (+ "data:text/plain;charset=utf-8," (state-to-base64)))) | ||||
(chain element (set-attribute :download "savegame.sav")) | ||||
(setf (@ element style display) :none) | ||||
(chain document body (append-child element)) | ||||
(chain element (click)) | ||||
(chain document body (remove-child element)))) | ||||
r20 | ||||
r25 | (defun stash-state (args) | |||
r37 | (call-serv-loc "$ONGSAVE") | |||
r39 | (setf *state-stash | |||
r25 | (chain *j-s-o-n (stringify | |||
r40 | (create :vars *globals | |||
r39 | :objs *objs | |||
r27 | :loc-args args | |||
r39 | :msecs (- (chain *date (now)) *started-at) | |||
:timer-interval *timer-interval | ||||
r27 | :main-html (inner-html | |||
r30 | (get-frame :main)) | |||
r27 | :stat-html (inner-html | |||
r30 | (get-frame :stat)) | |||
r39 | :next-location *current-location)))) | |||
r27 | (void)) | |||
r20 | ||||
r25 | (defun unstash-state () | |||
r39 | (let ((data (chain *j-s-o-n (parse *state-stash)))) | |||
r25 | (clear-act) | |||
r40 | (setf *globals (@ data :vars)) | |||
(loop :for k :in (chain *object (keys *globals)) | ||||
:do (chain *object (set-prototype-of (getprop *globals k) | ||||
r25 | (@ *var prototype)))) | |||
r39 | (setf *started-at (- (chain *date (now)) (@ data :msecs))) | |||
(setf *objs (@ data :objs)) | ||||
(setf *current-location (@ data :next-location)) | ||||
r30 | (setf (inner-html (get-frame :main)) | |||
r27 | (@ data :main-html)) | |||
r30 | (setf (inner-html (get-frame :stat)) | |||
r27 | (@ data :stat-html)) | |||
r25 | (update-objs) | |||
r27 | (set-timer (@ data :timer-interval)) | |||
r37 | (call-serv-loc "$ONGLOAD") | |||
r39 | (call-loc *current-location (@ data :loc-args)) | |||
r27 | (void))) | |||
r20 | ||||
r25 | (defun state-to-base64 () | |||
r39 | (btoa (encode-u-r-i-component *state-stash))) | |||
r20 | ||||
r25 | (defun base64-to-state (data) | |||
r39 | (setf *state-stash (decode-u-r-i-component (atob data)))) | |||
r20 | ||||
;;; Timers | ||||
r25 | (defun set-timer (interval) | |||
r39 | (setf *timer-interval interval) | |||
(clear-interval *timer-obj) | ||||
(setf *timer-obj | ||||
r20 | (set-interval | |||
(lambda () | ||||
r37 | (call-serv-loc "$COUNTER")) | |||
r20 | interval))) | |||
r32 | ||||
;;; Special variables | ||||
r33 | (defvar serv-vars (create)) | |||
r32 | (define-serv-var backimage (:str path) | |||
(setf (@ (get-frame :main) style background-image) path)) | ||||
(define-serv-var bcolor (:num color) | ||||
(setf (@ (get-frame :all) style background-color) (rgb-string color))) | ||||
(define-serv-var fcolor (:num color) | ||||
(setf (@ (get-frame :all) style color) (rgb-string color))) | ||||
(define-serv-var lcolor (:num color) | ||||
(setf (@ (get-frame :style) inner-text) | ||||
(+ "a { color: " (rgb-string color) ";}"))) | ||||
r33 | ||||
(define-serv-var fsize (:num size) | ||||
(setf (@ (get-frame :all) style font-size) size)) | ||||
(define-serv-var fname (:str font-name) | ||||
(setf (@ (get-frame :all) style font-family) (+ font-name ",serif"))) | ||||