Drag symbols onto the stage or the timeline, with previews
A symbol dragged out of the pool shows where it would land: on the stage, a dashed outline of its first frame centred on the pointer, and on the timeline a preview row of its own length. A stage drop places it at the playhead under the pointer; a timeline drop places it at the frame under the pointer, in its own coordinates. A drop that would make a cycle is refused while hovering. Pool thumbnails are capped at 40x30. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d5f044c6da
commit
f590ff19cf
7 changed files with 267 additions and 60 deletions
|
|
@ -70,16 +70,7 @@
|
|||
(assoc-in db [:ui :knobs [scope id knob]] value)))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; the pool, onto the stage
|
||||
|
||||
(rf/reg-event-db
|
||||
::place-symbol
|
||||
(fn [db [_ sid [x y]]]
|
||||
(let [uuid (random-uuid)
|
||||
host (get-in db [:ui :open])]
|
||||
(-> db
|
||||
(edit/edit #(clip/place-symbol % host sid (get-in db [:playback :frame]) uuid [x y]))
|
||||
(assoc-in [:ui :selection] [:node host uuid])))))
|
||||
;; a new symbol
|
||||
|
||||
(defn- where-new-goes
|
||||
"The row path, from the open symbol down, of the symbol a new thing goes into:
|
||||
|
|
@ -112,3 +103,32 @@
|
|||
;; Open every row down to it, or the new row is inside a closed one and
|
||||
;; the button looks like it did nothing.
|
||||
(update-in [:ui :expanded] into (rest (reductions conj [] down)))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; a drop in flight
|
||||
;;
|
||||
;; `[:ui :drop]` is where a drag out of the pool would land: `:where` is :stage or
|
||||
;; :timeline, `:frame` the frame of the open symbol it would start on, and
|
||||
;; `:point` the stage pixel under the pointer when that is the stage. `:label`
|
||||
;; and `:frames` ride along so the timeline can draw the preview row without
|
||||
;; reaching into the drag. Only written when it CHANGES — dragover fires every few
|
||||
;; milliseconds whether the pointer moved or not.
|
||||
|
||||
(rf/reg-event-db
|
||||
::drop-hover
|
||||
(fn [db [_ hover]]
|
||||
(if (= hover (get-in db [:ui :drop])) db (assoc-in db [:ui :drop] hover))))
|
||||
|
||||
(rf/reg-event-db
|
||||
::drop-clear
|
||||
(fn [db _] (update db :ui dissoc :drop)))
|
||||
|
||||
(rf/reg-event-db
|
||||
::drop-symbol
|
||||
(fn [db [_ sid frame pos]]
|
||||
(let [uuid (random-uuid)
|
||||
host (get-in db [:ui :open])]
|
||||
(-> db
|
||||
(update :ui dissoc :drop)
|
||||
(edit/edit #(clip/place-symbol % host sid frame uuid pos))
|
||||
(assoc-in [:ui :selection] [:node host uuid [uuid]])))))
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
(rf/reg-sub ::tone (fn [db _] (get-in db [:ui :tone])))
|
||||
(rf/reg-sub ::tool (fn [db _] (get-in db [:ui :tool])))
|
||||
(rf/reg-sub ::draft (fn [db _] (get-in db [:ui :draft])))
|
||||
(rf/reg-sub ::drop (fn [db _] (get-in db [:ui :drop])))
|
||||
(rf/reg-sub ::tabs (fn [db _] (get-in db [:ui :tabs])))
|
||||
(rf/reg-sub ::expanded (fn [db _] (get-in db [:ui :expanded])))
|
||||
(rf/reg-sub ::knobs (fn [db _] (get-in db [:ui :knobs])))
|
||||
|
|
|
|||
98
frontend/src/arthur/ui/drag.cljs
Normal file
98
frontend/src/arthur/ui/drag.cljs
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
(ns arthur.ui.drag
|
||||
"What a drag out of the pool is carrying while it is in flight, and what it
|
||||
would look like where it lands.
|
||||
|
||||
A PAGE-LOCAL ATOM, not `dataTransfer`. A browser lets a drop target read the
|
||||
payload on `drop` and hides it on every `dragover` before that, which is exactly
|
||||
when a preview needs it — so the pool writes the payload here on `dragstart`,
|
||||
and the `text/plain` it also sets is only what makes the drag a drag. A plain
|
||||
atom rather than app-db because it lives for one gesture and nothing renders
|
||||
from it; what the preview DOES render from — the frame and point under the
|
||||
pointer — is `[:ui :drop]`, set by `::ui/drop-hover`."
|
||||
(:require [arthur.domain.clip :as clip]
|
||||
[arthur.domain.palette :as pal]
|
||||
[arthur.events.ui :as ui]
|
||||
[arthur.footage.store :as store]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
(defonce carrying (atom nil))
|
||||
|
||||
(defn- outline
|
||||
"Frame 0 of symbol `sid` as plain shapes in its own space, and the centre of
|
||||
their bounds. Frame 0 because an instance dropped at the playhead starts there."
|
||||
[document st sid]
|
||||
(let [shapes (keep (fn [op]
|
||||
(case (:kind op)
|
||||
:poly {:kind :poly
|
||||
:pts (vec (take (* 2 (:n op)) (array-seq (:pts op))))}
|
||||
:disc (select-keys op [:kind :cx :cy :r])
|
||||
:rect (select-keys op [:kind :cx :cy :size])
|
||||
nil))
|
||||
((clip/resolver document st pal/index-of sid) 0))
|
||||
xys (mapcat (fn [{:keys [kind pts cx cy]}]
|
||||
(if (= :poly kind) (partition 2 pts) [[cx cy]]))
|
||||
shapes)]
|
||||
(if (empty? xys)
|
||||
{:shapes []}
|
||||
(let [xs (map first xys) ys (map second xys)
|
||||
[x0 x1 y0 y1] [(apply min xs) (apply max xs) (apply min ys) (apply max ys)]]
|
||||
{;; Under a few pixels there is nothing to see — a face symbol is drawn in
|
||||
;; units of one image height and scaled up by what places it — so the
|
||||
;; preview is the crosshair instead.
|
||||
:shapes (if (< (max (- x1 x0) (- y1 y0)) 3) [] (vec shapes))
|
||||
:center [(/ (+ x0 x1) 2) (/ (+ y0 y1) 2)]}))))
|
||||
|
||||
(defn symbol!
|
||||
"Start carrying symbol `sid` of the loaded document into the open symbol."
|
||||
[clip-id sid open]
|
||||
(let [{document :clip st :store} (store/entry clip-id)]
|
||||
(reset! carrying (merge {:kind :symbol :sid sid
|
||||
:label (clip/symbol-name document sid)
|
||||
:frames (clip/frames document sid)
|
||||
;; A symbol cannot go inside itself or inside
|
||||
;; anything it places. Refused by not ACCEPTING the
|
||||
;; drop, so the pointer says so while it hovers.
|
||||
:refused? (clip/contains-symbol? document sid open)}
|
||||
(outline document st sid)))))
|
||||
|
||||
(defn accepts?
|
||||
"Whether a drop target should accept what is being carried."
|
||||
[]
|
||||
(let [c @carrying] (and c (not (:refused? c)))))
|
||||
|
||||
(defn other!
|
||||
"Start carrying something that is not yet in the document: `:kind` says what,
|
||||
and the rest is what a preview can show of it before it is fetched."
|
||||
[payload]
|
||||
(reset! carrying payload))
|
||||
|
||||
(defn done!
|
||||
"The gesture is over, dropped or abandoned."
|
||||
[]
|
||||
(reset! carrying nil)
|
||||
(rf/dispatch [::ui/drop-clear]))
|
||||
|
||||
(defn hover!
|
||||
"Say where the drag would land, for the previews. `point` is nil over the
|
||||
timeline."
|
||||
[where frame point]
|
||||
(when-let [{:keys [label frames]} @carrying]
|
||||
(rf/dispatch [::ui/drop-hover {:where where :frame frame :point point
|
||||
:label label :frames frames}])))
|
||||
|
||||
(defn pos-for
|
||||
"Where an instance goes so the middle of its first frame is under `point`. A
|
||||
symbol with nothing drawn has no middle, and its origin goes there instead."
|
||||
[point]
|
||||
(let [[cx cy] (:center @carrying)]
|
||||
(if cx (mapv - point [cx cy]) point)))
|
||||
|
||||
(defn land!
|
||||
"Drop what is being carried at `frame` of the open symbol, with its instance
|
||||
at `pos`."
|
||||
[frame pos]
|
||||
(when-let [{:keys [kind sid]} (when (accepts?) @carrying)]
|
||||
(case kind
|
||||
:symbol (rf/dispatch [::ui/drop-symbol sid frame pos])
|
||||
nil))
|
||||
(done!))
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
[arthur.subs.playback :as playback]
|
||||
[arthur.subs.render :as render]
|
||||
[arthur.subs.ui :as sub]
|
||||
[arthur.ui.drag :as drag]
|
||||
[clojure.string :as str]
|
||||
[re-frame.core :as rf]
|
||||
[reagent.core :as r]))
|
||||
|
|
@ -43,11 +44,16 @@
|
|||
[:span.text label (when sub [:span.sub sub])]])
|
||||
|
||||
(defn- carrying
|
||||
"Drag-start handler for a row whose payload is `payload`."
|
||||
[payload]
|
||||
(fn [^js event]
|
||||
(.setData (.-dataTransfer event) "text/plain" payload)
|
||||
(set! (.. event -dataTransfer -effectAllowed) "copy")))
|
||||
"The drag handlers for a row. `text` is what makes it a drag at all and what
|
||||
another application would receive; `start!` puts the real payload where the
|
||||
stage and the timeline can read it while hovering — see `ui/drag`."
|
||||
[text start!]
|
||||
{:draggable true
|
||||
:on-drag-start (fn [^js event]
|
||||
(.setData (.-dataTransfer event) "text/plain" text)
|
||||
(set! (.. event -dataTransfer -effectAllowed) "copy")
|
||||
(start!))
|
||||
:on-drag-end (fn [_] (drag/done!))})
|
||||
|
||||
(defn- thumbnail
|
||||
"One frame of a video, from its proxy. `#t=` seeks a paused, muted element to a
|
||||
|
|
@ -55,18 +61,22 @@
|
|||
played and nothing is decoded past it."
|
||||
[{:keys [video]}]
|
||||
(if video
|
||||
;; Sized here as well as in the stylesheet: a video element with no size
|
||||
;; is as big as its footage, and a portrait phone clip is 1440×1920.
|
||||
[:video.thumb {:src (str video "#t=0.2") :muted true :preload "metadata"
|
||||
:plays-inline true :tab-index -1}]
|
||||
:plays-inline true :tab-index -1
|
||||
:style {:width 40 :height 30 :max-width 40 :max-height 30}}]
|
||||
[:span.thumb]))
|
||||
|
||||
(defn- footage-row [{:keys [id label frames fps] :as f} chosen]
|
||||
[item {:label label
|
||||
[item (merge {:label label
|
||||
:sub (str frames "f @ " fps)
|
||||
:thumb [thumbnail f]
|
||||
:on? (= id chosen)
|
||||
:draggable true
|
||||
:on-drag-start (carrying (str "footage:" id))
|
||||
:on-click #(rf/dispatch [::footage/choose id])}])
|
||||
:on-click #(rf/dispatch [::footage/choose id])}
|
||||
(carrying (str "footage:" id)
|
||||
#(drag/other! {:kind :footage :id id :label label
|
||||
:frames frames :fps fps})))])
|
||||
|
||||
(defn- folder [title & children]
|
||||
(into [:details.pool-folder {:open true} [:summary title]] children))
|
||||
|
|
@ -76,6 +86,7 @@
|
|||
|
||||
(defn- this-project []
|
||||
(let [document @(rf/subscribe [::render/clip])
|
||||
clip-id @(rf/subscribe [::render/clip-id])
|
||||
selection @(rf/subscribe [::sub/selection])
|
||||
open @(rf/subscribe [::render/open])
|
||||
media @(rf/subscribe [::sub/project-footage])
|
||||
|
|
@ -86,14 +97,14 @@
|
|||
(for [sid (sort-by str (keys (:symbols document)))
|
||||
:let [sym (clip/symbol document sid)]]
|
||||
^{:key (str sid)}
|
||||
[item {:label (clip/symbol-name document sid)
|
||||
[item (merge {:label (clip/symbol-name document sid)
|
||||
:sub (str (:frames sym) "f · " (count (:nodes sym)) " nodes"
|
||||
(when (= sid open) " · open"))
|
||||
:on? (= selection [:symbol sid])
|
||||
:draggable true
|
||||
:on-drag-start (carrying (str "symbol:" (subs (str sid) 1)))
|
||||
:on-click #(rf/dispatch [::ui/select [:symbol sid]])
|
||||
:on-double-click #(rf/dispatch [::pb/open-symbol sid])}]))
|
||||
:on-double-click #(rf/dispatch [::pb/open-symbol sid])}
|
||||
(carrying (str "symbol:" (subs (str sid) 1))
|
||||
#(drag/symbol! clip-id sid open)))]))
|
||||
[:div.dim "double-click to open · drag to place"])
|
||||
(group "media"
|
||||
(if (empty? media)
|
||||
|
|
@ -124,10 +135,12 @@
|
|||
(doall
|
||||
(for [{:keys [cid symbol name frames]} rows]
|
||||
^{:key (str cid symbol)}
|
||||
[item {:label name
|
||||
:sub (str frames "f")
|
||||
:draggable true
|
||||
:on-drag-start (carrying (str "import:" (str/join "|" [pid cid symbol])))}]))]))))))))
|
||||
[item (merge {:label name :sub (str frames "f")}
|
||||
(carrying (str "import:" (str/join "|" [pid cid symbol]))
|
||||
#(drag/other! {:kind :import :label name
|
||||
:frames frames
|
||||
:project pid :cid cid
|
||||
:symbol symbol})))]))]))))))))
|
||||
|
||||
(defn view []
|
||||
(r/with-let [;; Counted, not a boolean. `dragenter`/`dragleave` fire for every
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
[arthur.subs.playback :as playback]
|
||||
[arthur.subs.render :as render]
|
||||
[arthur.subs.ui :as sub]
|
||||
[arthur.ui.drag :as drag]
|
||||
[arthur.ui.player :as player]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
|
|
@ -65,6 +66,34 @@
|
|||
(or (not= :linear (channel/segment-interp geom active))
|
||||
(contains? (:keys geom) frame)))]))))
|
||||
|
||||
(defn- ghost
|
||||
"Where a drag out of the pool would land: the outline of its first frame,
|
||||
dashed, with its middle under the pointer. Drawn from the drag's own outline
|
||||
rather than by resolving anything, so hovering costs one re-render and no
|
||||
evaluation."
|
||||
[]
|
||||
(let [{:keys [where point]} @(rf/subscribe [::sub/drop])
|
||||
{:keys [shapes]} @drag/carrying]
|
||||
(when (and (= :stage where) point)
|
||||
(let [[x y] (drag/pos-for point)]
|
||||
[:g.ghost {:transform (str "translate(" x "," y ")")}
|
||||
(if (seq shapes)
|
||||
(doall
|
||||
(map-indexed
|
||||
(fn [i {:keys [kind pts cx cy r size]}]
|
||||
(case kind
|
||||
:poly ^{:key i} [:polygon {:points (points-text pts)}]
|
||||
:disc ^{:key i} [:circle {:cx cx :cy cy :r r}]
|
||||
:rect ^{:key i} [:rect {:x (- cx (/ size 2)) :y (- cy (/ size 2))
|
||||
:width size :height size}]
|
||||
nil))
|
||||
shapes))
|
||||
;; Nothing drawn, or nothing big enough to see: a mark where its
|
||||
;; middle will land.
|
||||
(let [[cx cy] (or (:center @drag/carrying) [0 0])]
|
||||
[:path {:d (str "M " (- cx 5) " " cy " H " (+ cx 5)
|
||||
" M " cx " " (- cy 5) " V " (+ cy 5))}]))]))))
|
||||
|
||||
(defn- overlay [w h]
|
||||
(let [clip @(rf/subscribe [::render/clip])
|
||||
frame @(rf/subscribe [::playback/frame])
|
||||
|
|
@ -87,6 +116,7 @@
|
|||
(stage-point event w h)])))
|
||||
:on-pointer-up (fn [_] (reset! dragging nil))
|
||||
:on-pointer-cancel (fn [_] (reset! dragging nil))}
|
||||
[ghost]
|
||||
(when (seq draft)
|
||||
[:polyline {:points (points-text draft) :fill "none"
|
||||
:stroke "#d0ba86" :stroke-width 1}])
|
||||
|
|
@ -108,32 +138,31 @@
|
|||
(.-pointerId event))
|
||||
(reset! dragging [sid id active i]))}])))])]))
|
||||
|
||||
(defn- dropped-symbol
|
||||
"The symbol a drag out of the media pool is carrying, or nil.
|
||||
|
||||
`text/plain` with a prefix rather than a custom MIME type: the payload is one
|
||||
short string, every browser agrees about this type, and a drag that arrives
|
||||
from somewhere else simply fails the prefix test."
|
||||
[^js event]
|
||||
(let [data (.getData (.-dataTransfer event) "text/plain")]
|
||||
(when (and data (.startsWith data "symbol:"))
|
||||
(keyword (subs data (count "symbol:"))))))
|
||||
|
||||
(defn view []
|
||||
;; Reactive on the clip's dimensions, so selecting a clip of another size
|
||||
;; resizes the canvas. `ui/canvas` guards the width assignment — which
|
||||
;; reallocates the backing store — so this re-rendering costs nothing per frame.
|
||||
(let [w @(rf/subscribe [::playback/width])
|
||||
h @(rf/subscribe [::playback/height])]
|
||||
h @(rf/subscribe [::playback/height])
|
||||
frame @(rf/subscribe [::playback/frame])]
|
||||
[:div.stage-area
|
||||
[:div.stage-wrap
|
||||
{:on-drag-over (fn [^js event]
|
||||
(when (.-types (.-dataTransfer event))
|
||||
(.preventDefault event)))
|
||||
;; A drop on the stage lands at the PLAYHEAD, where the pointer is in space;
|
||||
;; a drop on the timeline lands where the pointer is in time.
|
||||
;; `dragenter` is cancelled as well as `dragover`: a drop target has to
|
||||
;; accept on BOTH, and the element under the pointer changes whenever the
|
||||
;; preview re-renders beneath it, which fires a fresh `dragenter`.
|
||||
{:on-drag-enter (fn [^js event] (when (drag/accepts?) (.preventDefault event)))
|
||||
:on-drag-over (fn [^js event]
|
||||
(when (drag/accepts?)
|
||||
(.preventDefault event)
|
||||
(drag/hover! :stage frame (stage-point event w h))))
|
||||
:on-drag-leave (fn [^js event]
|
||||
(when-not (.contains (.-currentTarget event) (.-relatedTarget event))
|
||||
(rf/dispatch [::ui/drop-clear])))
|
||||
:on-drop (fn [^js event]
|
||||
(.preventDefault event)
|
||||
(when-let [sid (dropped-symbol event)]
|
||||
(rf/dispatch [::ui/place-symbol sid (stage-point event w h)])))}
|
||||
(drag/land! frame (drag/pos-for (stage-point event w h))))}
|
||||
[:canvas.stage {:ref #(player/set-canvas! %)
|
||||
:width w :height h
|
||||
:style {:width (str (* zoom w) "px")
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
[arthur.subs.playback :as playback]
|
||||
[arthur.subs.render :as render]
|
||||
[arthur.subs.ui :as sub]
|
||||
[arthur.ui.drag :as drag]
|
||||
[arthur.ui.player :as player]
|
||||
[re-frame.core :as rf]
|
||||
[reagent.core :as r]))
|
||||
|
|
@ -197,7 +198,8 @@
|
|||
|
||||
(defn- label-cell [{:keys [path depth label kind node-kind select expandable? expanded? of]}
|
||||
selection]
|
||||
[:div {:class (str "tl-label" (when (and select (= select selection)) " on"))
|
||||
[:div {:class (str "tl-label" (when (and select (= select selection)) " on")
|
||||
(when (= :ghost kind) " ghost"))
|
||||
:style {:padding-left (str (+ 4 (* 11 depth)) "px")}
|
||||
:title label
|
||||
:on-click #(when select (rf/dispatch [::ui/select select]))
|
||||
|
|
@ -212,13 +214,13 @@
|
|||
[:span.name label]
|
||||
(when (= :node kind) [:span.kind (str "·" (name node-kind))])])
|
||||
|
||||
(defn- track-cell [{:keys [span keys dense?]} frames]
|
||||
(defn- track-cell [{:keys [span keys dense? kind]} frames]
|
||||
[:div.tl-track
|
||||
;; Clipped to the ruler: an instance longer than the room left in its
|
||||
;; symbol still plays its own frames from 0, it is just cut off at the end.
|
||||
(when-let [[in out] (when span [(max 0 (first span)) (min frames (second span))])]
|
||||
(when (< in out)
|
||||
[:div {:class (str "tl-span" (when dense? " dense"))
|
||||
[:div {:class (str "tl-span" (when dense? " dense") (when (= :ghost kind) " ghost"))
|
||||
:style {:left (edge% in frames)
|
||||
:width (str (* 100 (/ (- out in) (max 1 frames))) "%")}}]))
|
||||
;; A dense channel has a value on every frame, so ticking each one is a solid
|
||||
|
|
@ -235,7 +237,15 @@
|
|||
frame @(rf/subscribe [::playback/frame])
|
||||
selection @(rf/subscribe [::sub/selection])
|
||||
expanded @(rf/subscribe [::sub/expanded])
|
||||
visible (rows clip @(rf/subscribe [::render/open]) expanded)
|
||||
drop @(rf/subscribe [::sub/drop])
|
||||
;; Where a drag out of the pool would land, as a row of its own at the
|
||||
;; top: its own length, starting on the frame it would start on. The
|
||||
;; stage's drop shows it too, at the playhead.
|
||||
visible (cond->> (rows clip @(rf/subscribe [::render/open]) expanded)
|
||||
drop (cons {:path [::drop] :depth 0 :kind :ghost
|
||||
:label (str "+ " (:label drop))
|
||||
:span [(:frame drop) (+ (:frame drop) (or (:frames drop) 1))]
|
||||
:keys []}))
|
||||
;; Roughly ten labels, on a round number of frames.
|
||||
step (* 10 (js/Math.ceil (/ frames 100)))]
|
||||
[:section.pane.time
|
||||
|
|
@ -248,10 +258,23 @@
|
|||
(doall (for [row visible]
|
||||
^{:key (str (:path row))} [label-cell row selection]))]
|
||||
[:div.tl-tracks
|
||||
{:on-drag-enter (fn [^js e] (when (drag/accepts?) (.preventDefault e)))
|
||||
:on-drag-over (fn [^js e]
|
||||
(when (drag/accepts?)
|
||||
(.preventDefault e)
|
||||
(drag/hover! :timeline (frame-at e frames) nil)))
|
||||
:on-drag-leave (fn [^js e]
|
||||
(when-not (.contains (.-currentTarget e) (.-relatedTarget e))
|
||||
(rf/dispatch [::ui/drop-clear])))
|
||||
;; In its own coordinates: dropped in time, nowhere in particular in
|
||||
;; space, so what was drawn at a place stays at that place.
|
||||
:on-drop (fn [^js e]
|
||||
(.preventDefault e)
|
||||
(drag/land! (frame-at e frames) [0 0]))
|
||||
;; Five frames as a percentage of the whole span, handed to the
|
||||
;; stylesheet so the frame grid can be a repeating background instead of
|
||||
;; a div per frame. A 900-frame take is 900 elements nobody needs.
|
||||
{:style {"--tick" (str (* 100 (/ 5 frames)) "%")}}
|
||||
;; stylesheet so the frame grid can be a repeating background instead
|
||||
;; of a div per frame. A 900-frame take is 900 elements nobody needs.
|
||||
:style {"--tick" (str (* 100 (/ 5 frames)) "%")}}
|
||||
[:div.tl-ruler
|
||||
{:on-pointer-down (fn [^js e]
|
||||
(rf/dispatch [::pb/seek (frame-at e frames)])
|
||||
|
|
|
|||
|
|
@ -308,6 +308,8 @@ input[type="range"] { width: 100%; accent-color: var(--sel); }
|
|||
flex: 0 0 40px;
|
||||
width: 40px;
|
||||
height: 30px;
|
||||
max-width: 40px;
|
||||
max-height: 30px;
|
||||
object-fit: cover;
|
||||
background: var(--stage);
|
||||
border-radius: 2px;
|
||||
|
|
@ -430,6 +432,19 @@ input[type="range"] { width: 100%; accent-color: var(--sel); }
|
|||
.paint-overlay.drawing { cursor: crosshair; }
|
||||
.paint-overlay circle { cursor: grab; }
|
||||
|
||||
/* Where a drag out of the pool would land. Dashed and unfilled, so it reads as
|
||||
"not there yet" over whatever is already drawn. */
|
||||
/* Never a hit target: it is drawn under the pointer, and anything under the
|
||||
pointer that changes mid-drag gets a dragenter of its own. */
|
||||
.paint-overlay .ghost { pointer-events: none; }
|
||||
.paint-overlay .ghost > * {
|
||||
fill: none;
|
||||
stroke: #fff1be;
|
||||
stroke-width: 1;
|
||||
stroke-dasharray: 3 2;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
|
||||
/* --------------------------------------------------------------------------
|
||||
params */
|
||||
|
||||
|
|
@ -582,6 +597,14 @@ input[type="range"] { width: 100%; accent-color: var(--sel); }
|
|||
-45deg, var(--span) 0 3px, #c3d4ea 3px 6px);
|
||||
}
|
||||
|
||||
/* The preview row of a drop in flight: its own length, where it would start. */
|
||||
.tl-span.ghost, .tl-label.ghost { pointer-events: none; }
|
||||
.tl-span.ghost {
|
||||
background: transparent;
|
||||
border: 1px dashed var(--sel);
|
||||
}
|
||||
.tl-label.ghost { color: var(--sel); font-style: italic; }
|
||||
|
||||
/* Flash's keyframe: a filled dot. */
|
||||
.tl-key {
|
||||
position: absolute;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue