@lovelace_lol/polymer 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +226 -0
- package/dist/index.d.ts +2603 -0
- package/dist/index.js +1031 -0
- package/dist/test.cjs +1519 -0
- package/dist/worker/agency-worker.js +972 -0
- package/dist/worker/manifest.edn +1 -0
- package/package.json +57 -0
- package/shadow-cljs.edn +173 -0
- package/src/polymer/animation/agency.cljs +84 -0
- package/src/polymer/animation/domain.cljs +110 -0
- package/src/polymer/animation/planner.cljs +88 -0
- package/src/polymer/animation/runtime.cljs +137 -0
- package/src/polymer/animation/scheduler.cljs +178 -0
- package/src/polymer/animation/service.cljs +981 -0
- package/src/polymer/animation/state.cljs +112 -0
- package/src/polymer/blink/agency.cljs +131 -0
- package/src/polymer/blink/planner.cljs +115 -0
- package/src/polymer/blink/scheduler.cljs +90 -0
- package/src/polymer/blink/snippet.cljs +50 -0
- package/src/polymer/blink/state.cljs +102 -0
- package/src/polymer/camera_context/agency.cljs +132 -0
- package/src/polymer/camera_context/domain.cljs +186 -0
- package/src/polymer/camera_context/planner.cljs +50 -0
- package/src/polymer/camera_context/scheduler.cljs +113 -0
- package/src/polymer/camera_context/state.cljs +121 -0
- package/src/polymer/character.cljs +471 -0
- package/src/polymer/conversation/agency.cljs +83 -0
- package/src/polymer/conversation/domain.cljs +35 -0
- package/src/polymer/conversation/effector.cljs +130 -0
- package/src/polymer/conversation/planner.cljs +178 -0
- package/src/polymer/conversation/scheduler.cljs +202 -0
- package/src/polymer/conversation/service.cljs +329 -0
- package/src/polymer/conversation/state.cljs +186 -0
- package/src/polymer/core.cljs +172 -0
- package/src/polymer/eye_head/agency.cljs +75 -0
- package/src/polymer/eye_head/domain.cljs +133 -0
- package/src/polymer/eye_head/goap.cljs +106 -0
- package/src/polymer/eye_head/scheduler.cljs +176 -0
- package/src/polymer/eye_head/service.cljs +259 -0
- package/src/polymer/eye_head/snippet.cljs +92 -0
- package/src/polymer/eye_head/state.cljs +167 -0
- package/src/polymer/gaze/agency.cljs +85 -0
- package/src/polymer/gaze/domain.cljs +211 -0
- package/src/polymer/gaze/goap.cljs +208 -0
- package/src/polymer/gaze/scheduler.cljs +184 -0
- package/src/polymer/gaze/state.cljs +202 -0
- package/src/polymer/gesture/agency.cljs +185 -0
- package/src/polymer/gesture/goap.cljs +461 -0
- package/src/polymer/gesture/scheduler.cljs +122 -0
- package/src/polymer/gesture/snippet.cljs +198 -0
- package/src/polymer/gesture/state.cljs +273 -0
- package/src/polymer/lipsync/agency.cljs +593 -0
- package/src/polymer/lipsync/articulation/jaw.cljs +258 -0
- package/src/polymer/lipsync/articulation/snippet.cljs +605 -0
- package/src/polymer/lipsync/articulation/tongue.cljs +418 -0
- package/src/polymer/lipsync/articulation/visemes.cljs +429 -0
- package/src/polymer/lipsync/goap.cljs +132 -0
- package/src/polymer/lipsync/pronunciation/effector.cljs +51 -0
- package/src/polymer/lipsync/scheduler.cljs +302 -0
- package/src/polymer/lipsync/state.cljs +202 -0
- package/src/polymer/nlp/phonemes.cljs +164 -0
- package/src/polymer/prosodic/agency.cljs +196 -0
- package/src/polymer/prosodic/goap.cljs +122 -0
- package/src/polymer/prosodic/scheduler.cljs +99 -0
- package/src/polymer/prosodic/snippet.cljs +106 -0
- package/src/polymer/prosodic/state.cljs +139 -0
- package/src/polymer/runtime/embody.cljs +117 -0
- package/src/polymer/runtime/embody_wasm.cljs +177 -0
- package/src/polymer/runtime/rust_embody_host.cljs +645 -0
- package/src/polymer/society.cljs +239 -0
- package/src/polymer/stream.cljs +130 -0
- package/src/polymer/transcription/agency.cljs +69 -0
- package/src/polymer/transcription/domain.cljs +68 -0
- package/src/polymer/transcription/effector.cljs +611 -0
- package/src/polymer/transcription/planner.cljs +203 -0
- package/src/polymer/transcription/scheduler.cljs +225 -0
- package/src/polymer/transcription/service.cljs +209 -0
- package/src/polymer/transcription/state.cljs +239 -0
- package/src/polymer/tts/agency.cljs +503 -0
- package/src/polymer/tts/azure.cljs +290 -0
- package/src/polymer/tts/planner.cljs +178 -0
- package/src/polymer/tts/runtime.cljs +624 -0
- package/src/polymer/tts/scheduler.cljs +162 -0
- package/src/polymer/tts/state.cljs +191 -0
- package/src/polymer/worker.cljs +70 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
(ns polymer.animation.state)
|
|
2
|
+
|
|
3
|
+
;; Animation state is the agency-local schedule ledger.
|
|
4
|
+
;;
|
|
5
|
+
;; The Animation agency owns this schedule state and directly talks to the
|
|
6
|
+
;; injected animation runtime. Other agencies can inspect summary state and
|
|
7
|
+
;; subscribe to events, but they do not reach through to runtime handles.
|
|
8
|
+
|
|
9
|
+
(def default-state
|
|
10
|
+
{:agency "animation"
|
|
11
|
+
:scheduled {}
|
|
12
|
+
:scheduledCount 0
|
|
13
|
+
:startedCount 0
|
|
14
|
+
:seekCount 0
|
|
15
|
+
:updatedCount 0
|
|
16
|
+
:removedCount 0
|
|
17
|
+
:lastEvent nil})
|
|
18
|
+
|
|
19
|
+
(defn finite-number? [value]
|
|
20
|
+
(and (number? value) (js/isFinite value)))
|
|
21
|
+
|
|
22
|
+
(defn now-ms []
|
|
23
|
+
(.now js/Date))
|
|
24
|
+
|
|
25
|
+
(defn snippet-name [snippet fallback]
|
|
26
|
+
;; Snippets should normally come with stable names so the runtime can clean up
|
|
27
|
+
;; the same clip later. Generate a fallback only for defensive JS interop.
|
|
28
|
+
(let [name (:name snippet)]
|
|
29
|
+
(if (and (string? name) (pos? (count name)))
|
|
30
|
+
name
|
|
31
|
+
fallback)))
|
|
32
|
+
|
|
33
|
+
(defn snippet-duration-ms [snippet]
|
|
34
|
+
;; Animation snippets use seconds. Timers use milliseconds. Non-finite or
|
|
35
|
+
;; missing maxTime means there is no automatic cleanup timer.
|
|
36
|
+
(when-let [max-time (:maxTime snippet)]
|
|
37
|
+
(when (finite-number? max-time)
|
|
38
|
+
(* 1000 max-time))))
|
|
39
|
+
|
|
40
|
+
(defn snippet-summary [snippet options source-agency requested-at]
|
|
41
|
+
;; Store enough state for diagnostics without copying runtime handles into
|
|
42
|
+
;; snapshots. Handles stay private inside the Animation agency.
|
|
43
|
+
{:name (:name snippet)
|
|
44
|
+
:sourceAgency source-agency
|
|
45
|
+
:snippetCategory (:snippetCategory snippet)
|
|
46
|
+
:snippetPriority (:snippetPriority snippet)
|
|
47
|
+
:maxTime (:maxTime snippet)
|
|
48
|
+
:loop (boolean (:loop snippet))
|
|
49
|
+
:autoPlay (boolean (:autoPlay options))
|
|
50
|
+
:requestedAt requested-at
|
|
51
|
+
:startedAt nil
|
|
52
|
+
:lastUpdateAt nil
|
|
53
|
+
:lastUpdateParams nil
|
|
54
|
+
:removedAt nil
|
|
55
|
+
:removeReason nil})
|
|
56
|
+
|
|
57
|
+
(defn record-schedule [state snippet options source-agency requested-at]
|
|
58
|
+
(let [summary (snippet-summary snippet options source-agency requested-at)]
|
|
59
|
+
(-> state
|
|
60
|
+
(assoc-in [:scheduled (:name summary)] summary)
|
|
61
|
+
(update :scheduledCount inc)
|
|
62
|
+
(assoc :lastEvent {:type "animationSnippetScheduled"
|
|
63
|
+
:name (:name summary)
|
|
64
|
+
:sourceAgency source-agency
|
|
65
|
+
:at requested-at}))))
|
|
66
|
+
|
|
67
|
+
(defn record-start [state name source-agency started-at]
|
|
68
|
+
(-> state
|
|
69
|
+
(assoc-in [:scheduled name :startedAt] started-at)
|
|
70
|
+
(update :startedCount inc)
|
|
71
|
+
(assoc :lastEvent {:type "animationSnippetStarted"
|
|
72
|
+
:name name
|
|
73
|
+
:sourceAgency source-agency
|
|
74
|
+
:at started-at})))
|
|
75
|
+
|
|
76
|
+
(defn record-remove [state name source-agency removed-at reason]
|
|
77
|
+
(-> state
|
|
78
|
+
(update :scheduled dissoc name)
|
|
79
|
+
(update :removedCount inc)
|
|
80
|
+
(assoc :lastEvent {:type "animationSnippetRemoved"
|
|
81
|
+
:name name
|
|
82
|
+
:sourceAgency source-agency
|
|
83
|
+
:reason reason
|
|
84
|
+
:at removed-at})))
|
|
85
|
+
|
|
86
|
+
(defn record-seek [state name source-agency seeked-at offset-sec]
|
|
87
|
+
;; Seeking is still schedule state: the Animation agency owns the active
|
|
88
|
+
;; snippet handles, so drift correction from LipSync comes here instead
|
|
89
|
+
;; of reaching through LoomLarge or bypassing Polymer's agency boundary.
|
|
90
|
+
(-> state
|
|
91
|
+
(assoc-in [:scheduled name :lastSeekAt] seeked-at)
|
|
92
|
+
(assoc-in [:scheduled name :lastSeekOffsetSec] offset-sec)
|
|
93
|
+
(update :seekCount inc)
|
|
94
|
+
(assoc :lastEvent {:type "animationSnippetSeeked"
|
|
95
|
+
:name name
|
|
96
|
+
:sourceAgency source-agency
|
|
97
|
+
:offsetSec offset-sec
|
|
98
|
+
:at seeked-at})))
|
|
99
|
+
|
|
100
|
+
(defn record-update [state name source-agency updated-at params]
|
|
101
|
+
(-> state
|
|
102
|
+
(assoc-in [:scheduled name :lastUpdateAt] updated-at)
|
|
103
|
+
(assoc-in [:scheduled name :lastUpdateParams] params)
|
|
104
|
+
(update :updatedCount inc)
|
|
105
|
+
(assoc :lastEvent {:type "animationSnippetUpdated"
|
|
106
|
+
:name name
|
|
107
|
+
:sourceAgency source-agency
|
|
108
|
+
:params params
|
|
109
|
+
:at updated-at})))
|
|
110
|
+
|
|
111
|
+
(defn visible-state [state]
|
|
112
|
+
(clj->js state))
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
(ns polymer.blink.agency
|
|
2
|
+
(:require [polymer.blink.planner :as planner]
|
|
3
|
+
[polymer.blink.scheduler :as scheduler]
|
|
4
|
+
[polymer.blink.state :as state]
|
|
5
|
+
[polymer.stream :as stream]))
|
|
6
|
+
|
|
7
|
+
;; Blink is the smallest useful agency, so it is the proving ground for the
|
|
8
|
+
;; Polymer architecture:
|
|
9
|
+
;;
|
|
10
|
+
;; input - commands coming from callers, workers, or later from other Polymer
|
|
11
|
+
;; agencies.
|
|
12
|
+
;; events - facts Blink observed or decided, such as a planned blink or a
|
|
13
|
+
;; cross-agency signal.
|
|
14
|
+
;;
|
|
15
|
+
;; The agency owns only local Blink state and timers. It does not know about
|
|
16
|
+
;; UI components, legacy services, animation engines, DOM APIs, storage, audio,
|
|
17
|
+
;; video, or HTTP.
|
|
18
|
+
|
|
19
|
+
(defn js-command [type value]
|
|
20
|
+
#js {:type type :value value})
|
|
21
|
+
|
|
22
|
+
(defn create-blink-agency [config]
|
|
23
|
+
(let [input-stream (stream/create-stream)
|
|
24
|
+
event-stream (stream/create-stream)
|
|
25
|
+
state-atom (atom (state/config->state config))
|
|
26
|
+
disposed? (atom false)
|
|
27
|
+
emit-input (:emit input-stream)
|
|
28
|
+
emit-event (:emit event-stream)
|
|
29
|
+
scheduler-atom (atom nil)]
|
|
30
|
+
(letfn [(record-plan! [plan]
|
|
31
|
+
;; Planning mutates only Blink's local counters/timestamps, then
|
|
32
|
+
;; leaves the runtime fact on the event stream. This is
|
|
33
|
+
;; intentional: scheduled blink counters are diagnostics and
|
|
34
|
+
;; agency-to-agency feedback, not render state. Publishing them
|
|
35
|
+
;; through a render-facing stream would make UI controls update
|
|
36
|
+
;; on every automatic blink.
|
|
37
|
+
(let [now-ms (or (:created-at plan) (.now js/Date))]
|
|
38
|
+
(swap! state-atom state/record-plan plan now-ms)))
|
|
39
|
+
|
|
40
|
+
(refresh-auto! []
|
|
41
|
+
(when-let [agency-scheduler @scheduler-atom]
|
|
42
|
+
((:refresh-auto agency-scheduler))))
|
|
43
|
+
|
|
44
|
+
(stop-auto! []
|
|
45
|
+
(when-let [agency-scheduler @scheduler-atom]
|
|
46
|
+
((:stop agency-scheduler))))
|
|
47
|
+
|
|
48
|
+
(run-action! [action]
|
|
49
|
+
(case (:op action)
|
|
50
|
+
:trigger
|
|
51
|
+
(when-let [agency-scheduler @scheduler-atom]
|
|
52
|
+
((:trigger agency-scheduler)
|
|
53
|
+
(:reason action)
|
|
54
|
+
(:options action)
|
|
55
|
+
(:random-value action)
|
|
56
|
+
(:now-ms action)))
|
|
57
|
+
|
|
58
|
+
:apply-state
|
|
59
|
+
(do
|
|
60
|
+
(swap! state-atom state/apply-command (:payload action))
|
|
61
|
+
(emit-event {:type "blinkConfigChanged"
|
|
62
|
+
:agency "blink"
|
|
63
|
+
:state @state-atom}))
|
|
64
|
+
|
|
65
|
+
:refresh-auto
|
|
66
|
+
(refresh-auto!)
|
|
67
|
+
|
|
68
|
+
:stop-auto
|
|
69
|
+
(stop-auto!)
|
|
70
|
+
|
|
71
|
+
:error
|
|
72
|
+
(emit-event {:type "error"
|
|
73
|
+
:agency "blink"
|
|
74
|
+
:message (:message action)})
|
|
75
|
+
|
|
76
|
+
(emit-event {:type "error"
|
|
77
|
+
:agency "blink"
|
|
78
|
+
:message (str "Unknown Blink planner action: " (:op action))})))
|
|
79
|
+
|
|
80
|
+
(dispatch! [command]
|
|
81
|
+
(when-not @disposed?
|
|
82
|
+
(let [payload (js->clj command :keywordize-keys true)
|
|
83
|
+
now-ms (.now js/Date)
|
|
84
|
+
random-value (js/Math.random)]
|
|
85
|
+
;; Input is observable so tests/workers can see the same
|
|
86
|
+
;; commands that the agency processes. This stream is not used
|
|
87
|
+
;; to cause side effects.
|
|
88
|
+
(emit-input {:type "command"
|
|
89
|
+
:agency "blink"
|
|
90
|
+
:command payload})
|
|
91
|
+
(doseq [action (planner/plan-command @state-atom payload random-value now-ms)]
|
|
92
|
+
(run-action! action)))))]
|
|
93
|
+
(let [agency-scheduler (scheduler/create-scheduler
|
|
94
|
+
{:state-atom state-atom
|
|
95
|
+
:emit-event emit-event
|
|
96
|
+
:record-plan! record-plan!})]
|
|
97
|
+
(reset! scheduler-atom agency-scheduler)
|
|
98
|
+
#js {:dispatch dispatch!
|
|
99
|
+
:snapshot (fn [] (state/visible-state @state-atom))
|
|
100
|
+
:input (stream/writable-port input-stream dispatch!)
|
|
101
|
+
:events (stream/readable-port event-stream)
|
|
102
|
+
:subscribeInput (fn [listener] ((:subscribe input-stream) listener))
|
|
103
|
+
:subscribeEvents (fn [listener] ((:subscribe event-stream) listener))
|
|
104
|
+
;; Compatibility aliases expose the same domain event stream.
|
|
105
|
+
:subscribe (fn [listener] ((:subscribe event-stream) listener))
|
|
106
|
+
:subscribeStatus (fn [listener] ((:subscribe event-stream) listener))
|
|
107
|
+
:enable (fn [] (dispatch! (js-command "enable" true)))
|
|
108
|
+
:disable (fn [] (dispatch! (js-command "disable" false)))
|
|
109
|
+
:triggerBlink (fn
|
|
110
|
+
([] (dispatch! #js {:type "triggerBlink"}))
|
|
111
|
+
([options] (dispatch! #js {:type "triggerBlink" :options options})))
|
|
112
|
+
:setFrequency (fn [value] (dispatch! (js-command "setFrequency" value)))
|
|
113
|
+
:setDuration (fn [value] (dispatch! (js-command "setDuration" value)))
|
|
114
|
+
:setIntensity (fn [value] (dispatch! (js-command "setIntensity" value)))
|
|
115
|
+
:setRandomness (fn [value] (dispatch! (js-command "setRandomness" value)))
|
|
116
|
+
:setLeftEyeIntensity (fn [value] (dispatch! (js-command "setLeftEyeIntensity" value)))
|
|
117
|
+
:setRightEyeIntensity (fn [value] (dispatch! (js-command "setRightEyeIntensity" value)))
|
|
118
|
+
:setBurstEnabled (fn [value] (dispatch! (js-command "setBurstEnabled" value)))
|
|
119
|
+
:setBurstFrequency (fn [value] (dispatch! (js-command "setBurstFrequency" value)))
|
|
120
|
+
:setBurstCount (fn [value] (dispatch! (js-command "setBurstCount" value)))
|
|
121
|
+
:setBurstGap (fn [value] (dispatch! (js-command "setBurstGap" value)))
|
|
122
|
+
:reset (fn [] (dispatch! #js {:type "reset"}))
|
|
123
|
+
:schedulerQueue (fn []
|
|
124
|
+
(when-let [agency-scheduler @scheduler-atom]
|
|
125
|
+
(clj->js ((:queue agency-scheduler)))))
|
|
126
|
+
:dispose (fn []
|
|
127
|
+
(when-not @disposed?
|
|
128
|
+
(reset! disposed? true)
|
|
129
|
+
((:dispose agency-scheduler))
|
|
130
|
+
((:dispose input-stream))
|
|
131
|
+
((:dispose event-stream))))}))))
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
(ns polymer.blink.planner
|
|
2
|
+
(:require [polymer.blink.state :as state]))
|
|
3
|
+
|
|
4
|
+
;; The planner is pure decision logic. It turns incoming commands and blink
|
|
5
|
+
;; opportunities into action maps for the agency/scheduler to execute. It does
|
|
6
|
+
;; not mutate state, touch clocks, choose random values, or emit stream data.
|
|
7
|
+
|
|
8
|
+
(def fast-blink-frequency-threshold 36)
|
|
9
|
+
|
|
10
|
+
(defn random-factor [randomness random-value]
|
|
11
|
+
;; Randomness stretches or shrinks the next automatic blink interval around
|
|
12
|
+
;; the frequency-derived baseline.
|
|
13
|
+
(+ 1 (* (- random-value 0.5) randomness)))
|
|
14
|
+
|
|
15
|
+
(defn next-interval-ms [blink-state random-value]
|
|
16
|
+
;; Frequency is stored as blinks per minute, so the baseline interval is
|
|
17
|
+
;; 60/frequency seconds. A disabled scheduler handles whether this value is
|
|
18
|
+
;; used; the planner only answers the interval question.
|
|
19
|
+
(let [frequency (:frequency blink-state)]
|
|
20
|
+
(if (<= frequency 0)
|
|
21
|
+
nil
|
|
22
|
+
(* (/ 60 frequency) 1000 (random-factor (:randomness blink-state) random-value)))))
|
|
23
|
+
|
|
24
|
+
(defn trigger-options [options]
|
|
25
|
+
;; JS callers may omit options. Normalize once so the rest of the planner can
|
|
26
|
+
;; stay in ordinary Clojure data.
|
|
27
|
+
(if options (js->clj options :keywordize-keys true) {}))
|
|
28
|
+
|
|
29
|
+
(defn resolve-blink-count [blink-state reason options random-value]
|
|
30
|
+
;; Manual triggers may request an exact burst count. Automatic triggers choose
|
|
31
|
+
;; a burst only some of the time, controlled by burstFrequency.
|
|
32
|
+
(let [manual-count (:burstCount options)]
|
|
33
|
+
(cond
|
|
34
|
+
(state/finite-number? manual-count)
|
|
35
|
+
(int (state/clamp 1 8 manual-count))
|
|
36
|
+
|
|
37
|
+
(and (= reason "auto")
|
|
38
|
+
(:burstEnabled blink-state)
|
|
39
|
+
(< random-value (:burstFrequency blink-state)))
|
|
40
|
+
(:burstCount blink-state)
|
|
41
|
+
|
|
42
|
+
:else 1)))
|
|
43
|
+
|
|
44
|
+
(defn make-plan [blink-state reason options random-value now-ms]
|
|
45
|
+
;; A plan is the normalized, immutable description of one blink opportunity.
|
|
46
|
+
;; Time and randomness are supplied by the scheduler/agency boundary so this
|
|
47
|
+
;; function remains deterministic and easy to test.
|
|
48
|
+
(let [options (trigger-options options)
|
|
49
|
+
blink-count (resolve-blink-count blink-state reason options random-value)
|
|
50
|
+
duration (state/clamp 0.05 1 (state/number-or (:duration options) (:duration blink-state)))
|
|
51
|
+
gap (state/clamp 0.02 0.5 (state/number-or (:burstGap options) (:burstGap blink-state)))
|
|
52
|
+
intensity (state/clamp 0 1 (state/number-or (:intensity options) (:intensity blink-state)))]
|
|
53
|
+
{:agency "blink"
|
|
54
|
+
:name (str "polymer:blink:" now-ms)
|
|
55
|
+
:reason reason
|
|
56
|
+
:blink-count blink-count
|
|
57
|
+
:duration duration
|
|
58
|
+
:burst-gap gap
|
|
59
|
+
:intensity intensity
|
|
60
|
+
:created-at now-ms
|
|
61
|
+
:fast? (or (>= (:frequency blink-state) fast-blink-frequency-threshold)
|
|
62
|
+
(> blink-count 2))}))
|
|
63
|
+
|
|
64
|
+
(def config-command-types
|
|
65
|
+
#{"setFrequency"
|
|
66
|
+
"setDuration"
|
|
67
|
+
"setIntensity"
|
|
68
|
+
"setRandomness"
|
|
69
|
+
"setLeftEyeIntensity"
|
|
70
|
+
"setRightEyeIntensity"
|
|
71
|
+
"setBurstEnabled"
|
|
72
|
+
"setBurstFrequency"
|
|
73
|
+
"setBurstCount"
|
|
74
|
+
"setBurstGap"
|
|
75
|
+
"configure"})
|
|
76
|
+
|
|
77
|
+
(defn error-action [message]
|
|
78
|
+
{:op :error
|
|
79
|
+
:message message})
|
|
80
|
+
|
|
81
|
+
(defn trigger-action [reason payload random-value now-ms]
|
|
82
|
+
{:op :trigger
|
|
83
|
+
:reason reason
|
|
84
|
+
:options (:options payload)
|
|
85
|
+
:random-value random-value
|
|
86
|
+
:now-ms now-ms})
|
|
87
|
+
|
|
88
|
+
(defn apply-state-action [payload]
|
|
89
|
+
{:op :apply-state
|
|
90
|
+
:payload payload})
|
|
91
|
+
|
|
92
|
+
(defn plan-command [blink-state payload random-value now-ms]
|
|
93
|
+
(let [type (:type payload)]
|
|
94
|
+
(cond
|
|
95
|
+
(= "triggerBlink" type)
|
|
96
|
+
[(trigger-action "manual" payload random-value now-ms)]
|
|
97
|
+
|
|
98
|
+
(= "requestBlink" type)
|
|
99
|
+
[(trigger-action (or (:reason payload) "request") payload random-value now-ms)]
|
|
100
|
+
|
|
101
|
+
(= "enable" type)
|
|
102
|
+
[(apply-state-action payload)
|
|
103
|
+
{:op :refresh-auto}]
|
|
104
|
+
|
|
105
|
+
(or (= "disable" type)
|
|
106
|
+
(= "reset" type))
|
|
107
|
+
[(apply-state-action payload)
|
|
108
|
+
{:op :stop-auto}]
|
|
109
|
+
|
|
110
|
+
(contains? config-command-types type)
|
|
111
|
+
[(apply-state-action payload)
|
|
112
|
+
{:op :refresh-auto}]
|
|
113
|
+
|
|
114
|
+
:else
|
|
115
|
+
[(error-action (str "Unknown Blink command: " type))])))
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
(ns polymer.blink.scheduler
|
|
2
|
+
(:require [polymer.blink.planner :as planner]
|
|
3
|
+
[polymer.blink.snippet :as snippet]))
|
|
4
|
+
|
|
5
|
+
;; The scheduler is the only Blink namespace that owns timers.
|
|
6
|
+
;;
|
|
7
|
+
;; It still does not call animation engines directly. When a blink should play,
|
|
8
|
+
;; it emits animation intent. The Polymer character network routes that intent
|
|
9
|
+
;; to Polymer Animation, and only Polymer Animation talks to the animation
|
|
10
|
+
;; runtime.
|
|
11
|
+
|
|
12
|
+
(defn clear-timeout! [timer-atom]
|
|
13
|
+
(when-let [timer @timer-atom]
|
|
14
|
+
(js/clearTimeout timer)
|
|
15
|
+
(reset! timer-atom nil)))
|
|
16
|
+
|
|
17
|
+
(defn create-scheduler [{:keys [state-atom emit-event record-plan!]}]
|
|
18
|
+
(let [auto-timer (atom nil)
|
|
19
|
+
queue (atom [])
|
|
20
|
+
disposed? (atom false)]
|
|
21
|
+
(letfn [(enqueue! [entry]
|
|
22
|
+
(let [queued (assoc entry
|
|
23
|
+
:agency "blink"
|
|
24
|
+
:queueIndex (count @queue)
|
|
25
|
+
:queuedAt (.now js/Date))]
|
|
26
|
+
(swap! queue conj queued)
|
|
27
|
+
queued))
|
|
28
|
+
|
|
29
|
+
(emit-plan! [plan snippet-data next-delay-ms]
|
|
30
|
+
;; Domain events describe what Blink decided. They are not
|
|
31
|
+
;; imperative requests. Other agencies can listen to them without
|
|
32
|
+
;; causing side effects.
|
|
33
|
+
(emit-event {:type "blinkPlanned"
|
|
34
|
+
:agency "blink"
|
|
35
|
+
:plan plan
|
|
36
|
+
:snippetName (:name snippet-data)
|
|
37
|
+
:nextDelayMs next-delay-ms})
|
|
38
|
+
(when (:fast? plan)
|
|
39
|
+
;; Cross-agency signals also live on the event stream. The
|
|
40
|
+
;; Polymer character network routes this signal to the
|
|
41
|
+
;; Prosodic agency, which decides how to express fast blinking.
|
|
42
|
+
(emit-event {:type "signal"
|
|
43
|
+
:agency "blink"
|
|
44
|
+
:signal "blink-fast"
|
|
45
|
+
:plan plan})))
|
|
46
|
+
|
|
47
|
+
(trigger! [reason options random-value now-ms]
|
|
48
|
+
(when-not @disposed?
|
|
49
|
+
(let [plan (planner/make-plan @state-atom reason options random-value now-ms)
|
|
50
|
+
snippet-data (snippet/build-blink-snippet plan)]
|
|
51
|
+
(enqueue! {:type "blinkScheduled"
|
|
52
|
+
:reason reason
|
|
53
|
+
:snippetName (:name snippet-data)
|
|
54
|
+
:burstCount (:burstCount plan)})
|
|
55
|
+
(record-plan! plan)
|
|
56
|
+
;; Blink requests animation as data. The Polymer agency
|
|
57
|
+
;; network routes it to Animation instead of letting Blink
|
|
58
|
+
;; touch the animation runtime.
|
|
59
|
+
(emit-event {:type "animation.requestScheduleSnippet"
|
|
60
|
+
:agency "blink"
|
|
61
|
+
:requestId (:name snippet-data)
|
|
62
|
+
:snippet snippet-data
|
|
63
|
+
:options {:autoPlay true}})
|
|
64
|
+
(emit-plan! plan snippet-data nil)
|
|
65
|
+
snippet-data)))
|
|
66
|
+
|
|
67
|
+
(schedule-next-auto! [extra-delay-ms]
|
|
68
|
+
(clear-timeout! auto-timer)
|
|
69
|
+
(when-not @disposed?
|
|
70
|
+
(let [blink-state @state-atom
|
|
71
|
+
interval (planner/next-interval-ms blink-state (js/Math.random))]
|
|
72
|
+
(when (and (:enabled blink-state) interval)
|
|
73
|
+
(let [delay-ms (+ interval (or extra-delay-ms 0))]
|
|
74
|
+
(reset! auto-timer
|
|
75
|
+
(js/setTimeout
|
|
76
|
+
(fn []
|
|
77
|
+
(reset! auto-timer nil)
|
|
78
|
+
(when-let [snippet-data (trigger! "auto" nil (js/Math.random) (.now js/Date))]
|
|
79
|
+
(let [extra-ms (max 0 (- (snippet/snippet-duration-ms snippet-data)
|
|
80
|
+
(* 1000 (:duration @state-atom))))]
|
|
81
|
+
(schedule-next-auto! extra-ms))))
|
|
82
|
+
delay-ms)))))))]
|
|
83
|
+
{:trigger trigger!
|
|
84
|
+
:refresh-auto (fn [] (schedule-next-auto! 0))
|
|
85
|
+
:stop (fn [] (clear-timeout! auto-timer))
|
|
86
|
+
:queue (fn [] @queue)
|
|
87
|
+
:dispose (fn []
|
|
88
|
+
(reset! disposed? true)
|
|
89
|
+
(clear-timeout! auto-timer)
|
|
90
|
+
(reset! queue []))})))
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
(ns polymer.blink.snippet
|
|
2
|
+
(:require [polymer.blink.state :as state]))
|
|
3
|
+
|
|
4
|
+
;; Snippet construction translates a Blink plan into animation data, but it does
|
|
5
|
+
;; not schedule that animation. Polymer Animation consumes this plain map and
|
|
6
|
+
;; decides how to schedule it against the injected animation runtime.
|
|
7
|
+
|
|
8
|
+
(defn pulse-points [offset duration intensity]
|
|
9
|
+
;; A blink is modeled as a close, brief hold, and open curve on AU 43. Burst
|
|
10
|
+
;; blinks reuse the same shape at shifted offsets.
|
|
11
|
+
(let [close-time (* duration 0.35)
|
|
12
|
+
hold-time (* duration 0.1)
|
|
13
|
+
open-time (* duration 0.55)]
|
|
14
|
+
[{:time offset :intensity 0}
|
|
15
|
+
{:time (+ offset (* close-time 0.3)) :intensity (* intensity 0.4)}
|
|
16
|
+
{:time (+ offset close-time) :intensity intensity}
|
|
17
|
+
{:time (+ offset close-time hold-time) :intensity (* intensity 0.98)}
|
|
18
|
+
{:time (+ offset close-time hold-time (* open-time 0.5)) :intensity (* intensity 0.5)}
|
|
19
|
+
{:time (+ offset close-time hold-time (* open-time 0.85)) :intensity (* intensity 0.15)}
|
|
20
|
+
{:time (+ offset duration) :intensity 0}]))
|
|
21
|
+
|
|
22
|
+
(defn build-blink-snippet [plan]
|
|
23
|
+
;; Build one animation snippet even for n-blink bursts. This avoids overlapping
|
|
24
|
+
;; several tiny snippets and gives the scheduler one duration to account for
|
|
25
|
+
;; before the next automatic blink is planned.
|
|
26
|
+
(let [blink-count (:blink-count plan)
|
|
27
|
+
duration (:duration plan)
|
|
28
|
+
gap (:burst-gap plan)
|
|
29
|
+
intensity (:intensity plan)
|
|
30
|
+
max-time (+ (* blink-count duration) (* (max 0 (dec blink-count)) gap))
|
|
31
|
+
points (into []
|
|
32
|
+
(mapcat (fn [index]
|
|
33
|
+
(pulse-points (* index (+ duration gap)) duration intensity)))
|
|
34
|
+
(range blink-count))]
|
|
35
|
+
{:name (:name plan)
|
|
36
|
+
:curves {"43" points}
|
|
37
|
+
:channels [{:target {:type "au" :id 43}
|
|
38
|
+
:keyframes points}]
|
|
39
|
+
:maxTime max-time
|
|
40
|
+
:loop false
|
|
41
|
+
:snippetPriority 100
|
|
42
|
+
:snippetPlaybackRate 1
|
|
43
|
+
:snippetIntensityScale 1
|
|
44
|
+
:metadata {:agency "blink"
|
|
45
|
+
:blinkCount blink-count
|
|
46
|
+
:burst (> blink-count 1)}}))
|
|
47
|
+
|
|
48
|
+
(defn snippet-duration-ms [snippet]
|
|
49
|
+
;; Timer code works in milliseconds while snippets store maxTime in seconds.
|
|
50
|
+
(* 1000 (state/number-or (:maxTime snippet) 0)))
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
(ns polymer.blink.state)
|
|
2
|
+
|
|
3
|
+
;; Blink state is ordinary immutable data. The agency owns one atom containing
|
|
4
|
+
;; this state, but all transformations below are pure functions. Keeping clamps
|
|
5
|
+
;; and defaults here makes the JS API forgiving without spreading validation
|
|
6
|
+
;; across UI components.
|
|
7
|
+
|
|
8
|
+
(def default-state
|
|
9
|
+
{:agency "blink"
|
|
10
|
+
:enabled false
|
|
11
|
+
:frequency 17
|
|
12
|
+
:duration 0.15
|
|
13
|
+
:intensity 1
|
|
14
|
+
:randomness 0.3
|
|
15
|
+
:leftEyeIntensity nil
|
|
16
|
+
:rightEyeIntensity nil
|
|
17
|
+
:burstEnabled true
|
|
18
|
+
:burstFrequency 0.08
|
|
19
|
+
:burstCount 2
|
|
20
|
+
:burstGap 0.08
|
|
21
|
+
:lastBlinkTime nil
|
|
22
|
+
:scheduledBlinkCount 0
|
|
23
|
+
:scheduledBurstCount 0})
|
|
24
|
+
|
|
25
|
+
(defn clamp [lo hi value]
|
|
26
|
+
;; Clamp numeric settings to the agency-supported range.
|
|
27
|
+
(-> value (max lo) (min hi)))
|
|
28
|
+
|
|
29
|
+
(defn finite-number? [value]
|
|
30
|
+
;; JS interop can pass NaN/Infinity. Treat those as invalid config values.
|
|
31
|
+
(and (number? value) (js/isFinite value)))
|
|
32
|
+
|
|
33
|
+
(defn number-or [value fallback]
|
|
34
|
+
;; Use a fallback for omitted, null, or non-finite settings.
|
|
35
|
+
(if (finite-number? value) value fallback))
|
|
36
|
+
|
|
37
|
+
(defn maybe-intensity [value]
|
|
38
|
+
;; Per-eye overrides are optional. nil means use the shared intensity.
|
|
39
|
+
(when (finite-number? value)
|
|
40
|
+
(clamp 0 1 value)))
|
|
41
|
+
|
|
42
|
+
(defn sanitize-state [state]
|
|
43
|
+
;; Every public configuration path flows through this sanitizer. That gives
|
|
44
|
+
;; future callers one predictable state contract regardless of whether
|
|
45
|
+
;; commands came from workers, tests, or another agency.
|
|
46
|
+
(-> state
|
|
47
|
+
(update :enabled boolean)
|
|
48
|
+
(update :frequency #(clamp 0 60 (number-or % (:frequency default-state))))
|
|
49
|
+
(update :duration #(clamp 0.05 1 (number-or % (:duration default-state))))
|
|
50
|
+
(update :intensity #(clamp 0 1 (number-or % (:intensity default-state))))
|
|
51
|
+
(update :randomness #(clamp 0 1 (number-or % (:randomness default-state))))
|
|
52
|
+
(update :leftEyeIntensity maybe-intensity)
|
|
53
|
+
(update :rightEyeIntensity maybe-intensity)
|
|
54
|
+
(update :burstEnabled boolean)
|
|
55
|
+
(update :burstFrequency #(clamp 0 1 (number-or % (:burstFrequency default-state))))
|
|
56
|
+
(update :burstCount #(int (clamp 1 8 (number-or % (:burstCount default-state)))))
|
|
57
|
+
(update :burstGap #(clamp 0.02 0.5 (number-or % (:burstGap default-state))))))
|
|
58
|
+
|
|
59
|
+
(defn config->state [config]
|
|
60
|
+
;; Convert optional JS config into the canonical CLJS map used internally.
|
|
61
|
+
(let [input (if config (js->clj config :keywordize-keys true) {})]
|
|
62
|
+
(sanitize-state (merge default-state input))))
|
|
63
|
+
|
|
64
|
+
(defn visible-state [state]
|
|
65
|
+
;; Public snapshots cross the package boundary as plain JavaScript data.
|
|
66
|
+
(clj->js state))
|
|
67
|
+
|
|
68
|
+
(defn configure [state config]
|
|
69
|
+
;; Partial configuration updates merge into current state, then sanitize.
|
|
70
|
+
(let [input (if config (js->clj config :keywordize-keys true) {})]
|
|
71
|
+
(sanitize-state (merge state input))))
|
|
72
|
+
|
|
73
|
+
(defn apply-command [state payload]
|
|
74
|
+
;; Command handling returns a new state map. Delay cancellation and emitted
|
|
75
|
+
;; stream messages are handled by the agency/scheduler, not by these pure
|
|
76
|
+
;; state functions.
|
|
77
|
+
(let [type (:type payload)]
|
|
78
|
+
(case type
|
|
79
|
+
"enable" (assoc state :enabled true)
|
|
80
|
+
"disable" (assoc state :enabled false)
|
|
81
|
+
"setFrequency" (sanitize-state (assoc state :frequency (:value payload)))
|
|
82
|
+
"setDuration" (sanitize-state (assoc state :duration (:value payload)))
|
|
83
|
+
"setIntensity" (sanitize-state (assoc state :intensity (:value payload)))
|
|
84
|
+
"setRandomness" (sanitize-state (assoc state :randomness (:value payload)))
|
|
85
|
+
"setLeftEyeIntensity" (sanitize-state (assoc state :leftEyeIntensity (:value payload)))
|
|
86
|
+
"setRightEyeIntensity" (sanitize-state (assoc state :rightEyeIntensity (:value payload)))
|
|
87
|
+
"setBurstEnabled" (sanitize-state (assoc state :burstEnabled (:value payload)))
|
|
88
|
+
"setBurstFrequency" (sanitize-state (assoc state :burstFrequency (:value payload)))
|
|
89
|
+
"setBurstCount" (sanitize-state (assoc state :burstCount (:value payload)))
|
|
90
|
+
"setBurstGap" (sanitize-state (assoc state :burstGap (:value payload)))
|
|
91
|
+
"configure" (configure state (:config payload))
|
|
92
|
+
"reset" default-state
|
|
93
|
+
state)))
|
|
94
|
+
|
|
95
|
+
(defn record-plan [state plan now-ms]
|
|
96
|
+
;; Called after the planner/scheduler accepts a blink plan. Counters are useful
|
|
97
|
+
;; for UI, tests, and later agency-to-agency feedback.
|
|
98
|
+
(let [blink-count (:blink-count plan)]
|
|
99
|
+
(-> state
|
|
100
|
+
(assoc :lastBlinkTime now-ms)
|
|
101
|
+
(update :scheduledBlinkCount + blink-count)
|
|
102
|
+
(update :scheduledBurstCount + (if (> blink-count 1) 1 0)))))
|