@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.
Files changed (85) hide show
  1. package/README.md +226 -0
  2. package/dist/index.d.ts +2603 -0
  3. package/dist/index.js +1031 -0
  4. package/dist/test.cjs +1519 -0
  5. package/dist/worker/agency-worker.js +972 -0
  6. package/dist/worker/manifest.edn +1 -0
  7. package/package.json +57 -0
  8. package/shadow-cljs.edn +173 -0
  9. package/src/polymer/animation/agency.cljs +84 -0
  10. package/src/polymer/animation/domain.cljs +110 -0
  11. package/src/polymer/animation/planner.cljs +88 -0
  12. package/src/polymer/animation/runtime.cljs +137 -0
  13. package/src/polymer/animation/scheduler.cljs +178 -0
  14. package/src/polymer/animation/service.cljs +981 -0
  15. package/src/polymer/animation/state.cljs +112 -0
  16. package/src/polymer/blink/agency.cljs +131 -0
  17. package/src/polymer/blink/planner.cljs +115 -0
  18. package/src/polymer/blink/scheduler.cljs +90 -0
  19. package/src/polymer/blink/snippet.cljs +50 -0
  20. package/src/polymer/blink/state.cljs +102 -0
  21. package/src/polymer/camera_context/agency.cljs +132 -0
  22. package/src/polymer/camera_context/domain.cljs +186 -0
  23. package/src/polymer/camera_context/planner.cljs +50 -0
  24. package/src/polymer/camera_context/scheduler.cljs +113 -0
  25. package/src/polymer/camera_context/state.cljs +121 -0
  26. package/src/polymer/character.cljs +471 -0
  27. package/src/polymer/conversation/agency.cljs +83 -0
  28. package/src/polymer/conversation/domain.cljs +35 -0
  29. package/src/polymer/conversation/effector.cljs +130 -0
  30. package/src/polymer/conversation/planner.cljs +178 -0
  31. package/src/polymer/conversation/scheduler.cljs +202 -0
  32. package/src/polymer/conversation/service.cljs +329 -0
  33. package/src/polymer/conversation/state.cljs +186 -0
  34. package/src/polymer/core.cljs +172 -0
  35. package/src/polymer/eye_head/agency.cljs +75 -0
  36. package/src/polymer/eye_head/domain.cljs +133 -0
  37. package/src/polymer/eye_head/goap.cljs +106 -0
  38. package/src/polymer/eye_head/scheduler.cljs +176 -0
  39. package/src/polymer/eye_head/service.cljs +259 -0
  40. package/src/polymer/eye_head/snippet.cljs +92 -0
  41. package/src/polymer/eye_head/state.cljs +167 -0
  42. package/src/polymer/gaze/agency.cljs +85 -0
  43. package/src/polymer/gaze/domain.cljs +211 -0
  44. package/src/polymer/gaze/goap.cljs +208 -0
  45. package/src/polymer/gaze/scheduler.cljs +184 -0
  46. package/src/polymer/gaze/state.cljs +202 -0
  47. package/src/polymer/gesture/agency.cljs +185 -0
  48. package/src/polymer/gesture/goap.cljs +461 -0
  49. package/src/polymer/gesture/scheduler.cljs +122 -0
  50. package/src/polymer/gesture/snippet.cljs +198 -0
  51. package/src/polymer/gesture/state.cljs +273 -0
  52. package/src/polymer/lipsync/agency.cljs +593 -0
  53. package/src/polymer/lipsync/articulation/jaw.cljs +258 -0
  54. package/src/polymer/lipsync/articulation/snippet.cljs +605 -0
  55. package/src/polymer/lipsync/articulation/tongue.cljs +418 -0
  56. package/src/polymer/lipsync/articulation/visemes.cljs +429 -0
  57. package/src/polymer/lipsync/goap.cljs +132 -0
  58. package/src/polymer/lipsync/pronunciation/effector.cljs +51 -0
  59. package/src/polymer/lipsync/scheduler.cljs +302 -0
  60. package/src/polymer/lipsync/state.cljs +202 -0
  61. package/src/polymer/nlp/phonemes.cljs +164 -0
  62. package/src/polymer/prosodic/agency.cljs +196 -0
  63. package/src/polymer/prosodic/goap.cljs +122 -0
  64. package/src/polymer/prosodic/scheduler.cljs +99 -0
  65. package/src/polymer/prosodic/snippet.cljs +106 -0
  66. package/src/polymer/prosodic/state.cljs +139 -0
  67. package/src/polymer/runtime/embody.cljs +117 -0
  68. package/src/polymer/runtime/embody_wasm.cljs +177 -0
  69. package/src/polymer/runtime/rust_embody_host.cljs +645 -0
  70. package/src/polymer/society.cljs +239 -0
  71. package/src/polymer/stream.cljs +130 -0
  72. package/src/polymer/transcription/agency.cljs +69 -0
  73. package/src/polymer/transcription/domain.cljs +68 -0
  74. package/src/polymer/transcription/effector.cljs +611 -0
  75. package/src/polymer/transcription/planner.cljs +203 -0
  76. package/src/polymer/transcription/scheduler.cljs +225 -0
  77. package/src/polymer/transcription/service.cljs +209 -0
  78. package/src/polymer/transcription/state.cljs +239 -0
  79. package/src/polymer/tts/agency.cljs +503 -0
  80. package/src/polymer/tts/azure.cljs +290 -0
  81. package/src/polymer/tts/planner.cljs +178 -0
  82. package/src/polymer/tts/runtime.cljs +624 -0
  83. package/src/polymer/tts/scheduler.cljs +162 -0
  84. package/src/polymer/tts/state.cljs +191 -0
  85. 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)))))