Add polygon painting with per-gap drawing keys
This commit is contained in:
parent
73ab153b02
commit
a45e89f4e4
15 changed files with 435 additions and 11 deletions
|
|
@ -30,6 +30,16 @@
|
|||
(is (= (mapv #(ch/value-at c %) frames)
|
||||
(mapv #(ch/sample! cursor %) frames)))))
|
||||
|
||||
(deftest one-channel-can-cut-then-tween
|
||||
(let [c (assoc (ch/keyed {0 [0 0], 4 [4 0], 8 [8 0]})
|
||||
:segments {4 :linear})
|
||||
cursor (ch/cursor c)]
|
||||
(is (= [0 0] (ch/value-at c 2)))
|
||||
(is (= [4 0] (ch/value-at c 4)))
|
||||
(is (= [6 0] (ch/value-at c 6)))
|
||||
(is (= (mapv #(ch/value-at c %) [0 2 4 6 8 3 7])
|
||||
(mapv #(ch/sample! cursor %) [0 2 4 6 8 3 7])))))
|
||||
|
||||
(deftest a-frame-before-the-first-key-reads-the-first-key
|
||||
;; The JS activeKey clamps low, and that is kept: a channel's first key is the
|
||||
;; pose the part starts in. Having NO value is a different question — it is a
|
||||
|
|
|
|||
32
frontend/test/arthur/domain/paint_test.cljs
Normal file
32
frontend/test/arthur/domain/paint_test.cljs
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
(ns arthur.domain.paint-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[arthur.demo :as demo]
|
||||
[arthur.domain.channel :as channel]
|
||||
[arthur.domain.leaf :as leaf]
|
||||
[arthur.domain.paint :as paint]
|
||||
[arthur.domain.timeline :as timeline]))
|
||||
|
||||
(defn- geometry [clip]
|
||||
(get-in clip [:timelines :main :nodes :paint-test :channels paint/geometry]))
|
||||
|
||||
(deftest drawing-keys-hold-and-tween-on-the-timeline-clock
|
||||
(let [a [10 10 30 10 20 30]
|
||||
c0 (paint/new-shape demo/clip :paint-test 3 a :brow)
|
||||
c1 (paint/add-key c0 :paint-test 9)
|
||||
c2 (paint/set-vertex c1 :paint-test 9 0 [22 10])
|
||||
c3 (paint/add-key c2 :paint-test 15)
|
||||
c4 (paint/set-vertex c3 :paint-test 15 0 [34 10])
|
||||
held (geometry c4)
|
||||
mixed-clip (paint/set-segment-interp c4 :paint-test 9 :linear)
|
||||
mixed (geometry mixed-clip)]
|
||||
(is (= [3 229] (get-in c2 [:timelines :main :nodes :paint-test :span])))
|
||||
(is (= a (channel/value-at held 8)))
|
||||
(is (= 10 (first (channel/value-at held 8))))
|
||||
(is (= 22 (first (channel/value-at held 9))))
|
||||
(is (= 10 (first (channel/value-at mixed 6))) "the first gap cuts")
|
||||
(is (= 28 (first (channel/value-at mixed 12))) "the second gap tweens")
|
||||
(is (empty? (channel/problems mixed)))
|
||||
;; The demo's root is exposed on 2s. Paint at frame 3 must still appear at 3.
|
||||
(is (some #(= :paint-test (:node %))
|
||||
(timeline/eval-frame (get-in c2 [:timelines :main]) 3)))
|
||||
(is (= mixed-clip (leaf/clip :c1 (leaf/leaves :c1 mixed-clip))))))
|
||||
|
|
@ -394,6 +394,95 @@ async function main() {
|
|||
check(!back[0].selectedClip.includes('take'), 'the picture is the reopened document',
|
||||
JSON.stringify(back[0].selectedClip));
|
||||
|
||||
// Paint through the visible controls. This exercises the SVG pointer path,
|
||||
// the authored node, the raster preview and the document round trip.
|
||||
await page.eval(SEEK(0));
|
||||
await sleep(150);
|
||||
const beforePaint = await page.eval(`(() => {
|
||||
const c = document.querySelector('canvas.stage');
|
||||
const d = c.getContext('2d').getImageData(25, 20, 1, 1).data;
|
||||
return [...d];
|
||||
})()`);
|
||||
const painted = await page.eval(`(() => {
|
||||
const button = [...document.querySelectorAll('.paint-tools button')]
|
||||
.find(b => b.textContent === 'new polygon');
|
||||
button.click();
|
||||
const svg = document.querySelector('.paint-overlay');
|
||||
const box = svg.getBoundingClientRect();
|
||||
for (const [x, y] of [[10, 10], [40, 10], [25, 40]]) {
|
||||
svg.dispatchEvent(new PointerEvent('pointerdown', {
|
||||
bubbles: true, clientX: box.left + x * box.width / 320,
|
||||
clientY: box.top + y * box.height / 200,
|
||||
}));
|
||||
}
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(100);
|
||||
const finished = await page.eval(`(() => {
|
||||
const button = [...document.querySelectorAll('.paint-tools button')]
|
||||
.find(b => b.textContent === 'finish shape');
|
||||
if (!button || button.disabled) return false;
|
||||
button.click();
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(250);
|
||||
const afterPaint = await page.eval(`(() => {
|
||||
const c = document.querySelector('canvas.stage');
|
||||
const d = c.getContext('2d').getImageData(25, 20, 1, 1).data;
|
||||
return [...d];
|
||||
})()`);
|
||||
check(painted && finished && beforePaint.join(',') !== afterPaint.join(','),
|
||||
'a polygon drawn with the paint controls appears on the canvas');
|
||||
for (const f of [8, 16]) {
|
||||
await page.eval(SEEK(f));
|
||||
await sleep(100);
|
||||
check(await page.eval(`(() => {
|
||||
const button = [...document.querySelectorAll('.paint-tools button')]
|
||||
.find(b => b.textContent === 'new drawing key');
|
||||
if (!button || button.disabled) return false;
|
||||
button.click();
|
||||
return true;
|
||||
})()`), `a drawing key can be added at frame ${f}`);
|
||||
await sleep(100);
|
||||
}
|
||||
await page.eval(SEEK(8));
|
||||
await sleep(100);
|
||||
const tweenSelected = await page.eval(`(() => {
|
||||
const label = [...document.querySelectorAll('.paint-tools label')]
|
||||
.find(el => el.textContent.startsWith('key 8 → 16'));
|
||||
const select = label?.querySelector('select');
|
||||
if (!select) return false;
|
||||
select.value = 'linear';
|
||||
select.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
return true;
|
||||
})()`);
|
||||
await sleep(100);
|
||||
check(tweenSelected && await page.eval(`(() => {
|
||||
const label = [...document.querySelectorAll('.paint-tools label')]
|
||||
.find(el => el.textContent.startsWith('key 8 → 16'));
|
||||
return label?.querySelector('select')?.value === 'linear';
|
||||
})()`), 'the second drawing gap can be set to tween');
|
||||
check(await page.eval(CLICK('save')), 'the painted document can be saved');
|
||||
check((await statusMatching(/saved r\d+ · \d+ leaves/)) !== null,
|
||||
'the painted shape is saved', await page.eval(STATUS));
|
||||
check(await page.eval(CLICK('open')), 'the painted document can be reopened');
|
||||
check((await statusMatching(/opened /)) !== null,
|
||||
'the painted shape is reopened', await page.eval(STATUS));
|
||||
await sleep(150);
|
||||
const reopenedPaint = await page.eval(`(() => {
|
||||
const c = document.querySelector('canvas.stage');
|
||||
return [...c.getContext('2d').getImageData(25, 20, 1, 1).data];
|
||||
})()`);
|
||||
check(afterPaint.join(',') === reopenedPaint.join(','),
|
||||
'the painted polygon survives the project round trip');
|
||||
await page.eval(SEEK(8));
|
||||
await sleep(100);
|
||||
check(await page.eval(`(() => {
|
||||
const label = [...document.querySelectorAll('.paint-tools label')]
|
||||
.find(el => el.textContent.startsWith('key 8 → 16'));
|
||||
return label?.querySelector('select')?.value === 'linear';
|
||||
})()`), 'the per-gap tween setting survives the project round trip');
|
||||
|
||||
// The local 8625 study is optional in a fresh test database. When present,
|
||||
// tune its shared symbol while its stage is playing.
|
||||
const stageSource = await page.eval(`fetch('/api/projects/4379f900-bdd2-409b-acf6-32081f8ce01f')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue