@lark-apaas/coding-registry-buildless 0.1.20 → 0.1.22
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/components/animations.jsx +173 -6
- package/components/design-canvas.jsx +260 -14
- package/components/tweaks-panel.jsx +8 -5
- package/package.json +1 -1
- package/scripts/android-frame.js +253 -0
- package/scripts/browser-window.js +246 -0
- package/scripts/deck-stage.js +176 -6
- package/scripts/design-canvas.js +2189 -0
- package/scripts/ios-frame.js +338 -0
- package/scripts/macos-window.js +260 -0
|
@@ -20,9 +20,82 @@
|
|
|
20
20
|
// `progress` via the useSprite() hook. Use Easing + interpolate()/animate()
|
|
21
21
|
// for tweens; TextSprite / ImageSprite / RectSprite have built-in entry/exit.
|
|
22
22
|
// Build YOUR scenes by composing Sprites inside a Stage.
|
|
23
|
+
//
|
|
24
|
+
// window.__miaodaMirror (defined below) is Magic-Share sync plumbing between
|
|
25
|
+
// this component and the miaoda host — internal; do not call or remove it.
|
|
23
26
|
/* END USAGE */
|
|
24
27
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
25
28
|
|
|
29
|
+
// ── Mirror sync singleton ──────────────────────────────────────────────────
|
|
30
|
+
// Shared by every buildless script on the page (first one to load defines
|
|
31
|
+
// it). Inlined per script rather than shipped as its own registry file:
|
|
32
|
+
// call sites use the global bare, so inlining is what guarantees
|
|
33
|
+
// definition-before-use — a separate copy-in file would add a whole "SDK
|
|
34
|
+
// copied but <script> tag missing" failure mode. Keep this block identical
|
|
35
|
+
// across deck-stage.js / design-canvas.jsx / animations.jsx — same
|
|
36
|
+
// convention as the mobile-UA regex. Two halves of
|
|
37
|
+
// the 妙享 (Magic Share) mirror protocol:
|
|
38
|
+
// emit(msg, opts) — stamps msg.__mirror so the host relay treats the
|
|
39
|
+
// message as a broadcastable UI-state snapshot;
|
|
40
|
+
// unstamped messages are never mirrored. opts.key is
|
|
41
|
+
// the sync channel (maps to the share SDK's
|
|
42
|
+
// actionType): late joiners receive only the LATEST
|
|
43
|
+
// snapshot per key, so states that overwrite each
|
|
44
|
+
// other must share one key. opts.throttle is the
|
|
45
|
+
// suggested host-side coalescing interval in ms —
|
|
46
|
+
// snapshots make dropped intermediate frames
|
|
47
|
+
// lossless, so hint it on any continuous gesture.
|
|
48
|
+
// onApply(type, fn) — replay entry for a message type this script itself
|
|
49
|
+
// emits. The host sends {type:'miaoda:mirror:apply',
|
|
50
|
+
// uuid, message, latencyMs} and needs
|
|
51
|
+
// {type:'miaoda:mirror:apply-result', uuid, result}
|
|
52
|
+
// back to drive the SDK's retry: result.type
|
|
53
|
+
// 'failed' means retry (use it for transient
|
|
54
|
+
// not-ready states), 'ignored' is terminal (not
|
|
55
|
+
// applicable / bad payload), 'applied' is success.
|
|
56
|
+
// Unregistered types stay silent on purpose —
|
|
57
|
+
// another script on the page may own them, and the
|
|
58
|
+
// host's timeout is the backstop.
|
|
59
|
+
if (!window.__miaodaMirror) {
|
|
60
|
+
const appliers = new Map(); // message.type -> Set<fn>
|
|
61
|
+
window.__miaodaMirror = {
|
|
62
|
+
emit(msg, opts) {
|
|
63
|
+
try { window.parent.postMessage(Object.assign({}, msg, { __mirror: Object.assign({ v: 1 }, opts) }), '*'); } catch (e) {}
|
|
64
|
+
},
|
|
65
|
+
onApply(type, fn) {
|
|
66
|
+
if (!appliers.has(type)) appliers.set(type, new Set());
|
|
67
|
+
appliers.get(type).add(fn);
|
|
68
|
+
return () => { const s = appliers.get(type); if (s) s.delete(fn); };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
window.addEventListener('message', async (e) => {
|
|
72
|
+
// Replays are accepted from the host frame only — generated pages embed
|
|
73
|
+
// arbitrary third-party iframes, any of which could otherwise forge an
|
|
74
|
+
// apply and drive state (plus its persisted side effects).
|
|
75
|
+
if (e.source !== window.parent) return;
|
|
76
|
+
const d = e.data;
|
|
77
|
+
if (!d || d.type !== 'miaoda:mirror:apply' || !d.message || !d.message.type) return;
|
|
78
|
+
const fns = appliers.get(d.message.type);
|
|
79
|
+
if (!fns || !fns.size) return;
|
|
80
|
+
// Merge across handlers (multi-instance case): one success is enough.
|
|
81
|
+
// Instances share a channel, so a sibling that can't apply yet (a lazily
|
|
82
|
+
// slotted second deck returning 'failed') must not poison an applied
|
|
83
|
+
// result into an SDK retry storm of no-ops — 'failed' only stands when
|
|
84
|
+
// nobody applied. Every handler still runs (no short-circuit) so each
|
|
85
|
+
// sibling instance gets the state.
|
|
86
|
+
let result = { type: 'ignored', reason: 'no applier result' };
|
|
87
|
+
for (const fn of Array.from(fns)) {
|
|
88
|
+
let r;
|
|
89
|
+
try { r = await fn(d.message, { latencyMs: d.latencyMs, uuid: d.uuid }); }
|
|
90
|
+
catch (err) { r = { type: 'failed', reason: String((err && err.message) || err) }; }
|
|
91
|
+
if (!r) continue;
|
|
92
|
+
if (r.type === 'applied') result = r;
|
|
93
|
+
else if (result.type !== 'applied' && (r.type === 'failed' || result.type !== 'failed')) result = r;
|
|
94
|
+
}
|
|
95
|
+
try { window.parent.postMessage({ type: 'miaoda:mirror:apply-result', uuid: d.uuid, result }, '*'); } catch (e2) {}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
26
99
|
// ── Easing functions (hand-rolled, Popmotion-style) ─────────────────────────
|
|
27
100
|
// All easings take t ∈ [0,1] and return eased t ∈ [0,1] (may overshoot for back/elastic).
|
|
28
101
|
const Easing = {
|
|
@@ -346,6 +419,13 @@ function Stage({
|
|
|
346
419
|
// ~1s as a fallback — paused, with the playback bar hidden, so the product
|
|
347
420
|
// thumbnail is a deterministic frame of the first composed scene.
|
|
348
421
|
const captureMode = typeof location !== 'undefined' && /[?&]thumbnail=/.test(location.search || '');
|
|
422
|
+
// Mirror silence additionally covers _snthumb frames (deck's presenter
|
|
423
|
+
// popup pins LIVE product iframes per slide — a stage embedded in such a
|
|
424
|
+
// slide is on the presenter's side, so the role gate can't drop its
|
|
425
|
+
// stamped emits). Kept separate from captureMode on purpose: the poster
|
|
426
|
+
// freeze stays keyed to ?thumbnail= only, so _snthumb thumbnails still
|
|
427
|
+
// look like the live slide.
|
|
428
|
+
const captureFrame = captureMode || (typeof location !== 'undefined' && /[?&]_snthumb=/.test(location.search || ''));
|
|
349
429
|
|
|
350
430
|
const [time, setTime] = React.useState(() => {
|
|
351
431
|
if (captureMode) return clamp(poster == null ? 1 : poster, 0, duration);
|
|
@@ -362,7 +442,11 @@ function Stage({
|
|
|
362
442
|
const rafRef = React.useRef(null);
|
|
363
443
|
const lastTsRef = React.useRef(null);
|
|
364
444
|
|
|
365
|
-
// Persist playhead
|
|
445
|
+
// Persist playhead. A Magic-Share-replayed time persists too, on purpose:
|
|
446
|
+
// unlike the rail/viewport preferences this IS the stage's current
|
|
447
|
+
// position, and while playing the next local rAF tick would re-persist it
|
|
448
|
+
// anyway (a guard would only reach the paused case, whose landing spot is
|
|
449
|
+
// equally this stage's real position).
|
|
366
450
|
React.useEffect(() => {
|
|
367
451
|
try { localStorage.setItem(persistKey + ':t', String(time)); } catch {}
|
|
368
452
|
}, [time, persistKey]);
|
|
@@ -402,7 +486,51 @@ function Stage({
|
|
|
402
486
|
// The playback bar is a *sibling* of this node, so it never lands in a capture
|
|
403
487
|
// and needs no hiding.
|
|
404
488
|
const bridgeStateRef = React.useRef(null);
|
|
405
|
-
bridgeStateRef.current = { time, duration, fps, width, height, setTime, setPlaying };
|
|
489
|
+
bridgeStateRef.current = { time, duration, fps, width, height, playing, loop, setTime, setPlaying };
|
|
490
|
+
|
|
491
|
+
// Magic Share: playhead sync fires only from explicit interactions (playbar,
|
|
492
|
+
// keyboard) plus the coarse keepalive below — never from the rAF tick, which
|
|
493
|
+
// would broadcast at frame rate for nothing (each message is already a full
|
|
494
|
+
// snapshot). Scrub-drags still call this per mousemove, so the actual post
|
|
495
|
+
// is trailing-throttled locally (same rule as design-canvas's viewport:
|
|
496
|
+
// no per-event cross-frame posts). Capture iframes are frozen stills and
|
|
497
|
+
// the host export drives the bridge programmatically, so captureMode
|
|
498
|
+
// stays silent. ts rides along for the HOST, which derives from it the
|
|
499
|
+
// latencyMs it hands back to replays.
|
|
500
|
+
const emitTRef = React.useRef(0);
|
|
501
|
+
React.useEffect(() => () => clearTimeout(emitTRef.current), []);
|
|
502
|
+
const emitPlayhead = () => {
|
|
503
|
+
if (captureFrame || emitTRef.current) return;
|
|
504
|
+
emitTRef.current = setTimeout(() => {
|
|
505
|
+
emitTRef.current = 0;
|
|
506
|
+
// Read at fire time, not call time — by 80ms the interaction's state
|
|
507
|
+
// has committed and bridgeStateRef is fresh (same live-ref pattern as
|
|
508
|
+
// design-canvas's tf trailing timer).
|
|
509
|
+
const cur = bridgeStateRef.current;
|
|
510
|
+
window.__miaodaMirror.emit(
|
|
511
|
+
{ type: 'miaoda:anim:playhead-changed', t: cur.time, playing: cur.playing, ts: Date.now() },
|
|
512
|
+
{ key: 'anim:playhead', throttle: 60 },
|
|
513
|
+
);
|
|
514
|
+
}, 80);
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
// Coarse drift correction: followers free-run their own rAF between
|
|
518
|
+
// messages, so while playing re-anchor them every few seconds. 3s is
|
|
519
|
+
// coarse enough to be free and tight enough that free-run drift stays
|
|
520
|
+
// imperceptible. Quiesced while replays are flowing in: a replayed
|
|
521
|
+
// playing:true arms this effect on followers too, and their competing
|
|
522
|
+
// snapshots are otherwise only stopped by the host's presenter role gate —
|
|
523
|
+
// don't lean on a single layer across presenter-handoff windows.
|
|
524
|
+
const lastApplyTsRef = React.useRef(0);
|
|
525
|
+
React.useEffect(() => {
|
|
526
|
+
if (!playing || captureFrame) return;
|
|
527
|
+
const iv = setInterval(() => {
|
|
528
|
+
if (Date.now() - lastApplyTsRef.current < 3000) return;
|
|
529
|
+
emitPlayhead();
|
|
530
|
+
}, 3000);
|
|
531
|
+
return () => clearInterval(iv);
|
|
532
|
+
}, [playing]);
|
|
533
|
+
|
|
406
534
|
React.useEffect(() => {
|
|
407
535
|
const s = bridgeStateRef;
|
|
408
536
|
const bridge = {
|
|
@@ -420,7 +548,42 @@ function Stage({
|
|
|
420
548
|
// Announce to the host that this product IS an animation scene, so it can
|
|
421
549
|
// reveal the export entry (mirrors deck-stage's miaoda:deck:available).
|
|
422
550
|
try { window.parent.postMessage({ type: 'miaoda:anim:available' }, '*'); } catch (e) {}
|
|
423
|
-
|
|
551
|
+
// Magic Share replay — a thin adapter over the same setTime/setPlaying the
|
|
552
|
+
// export bridge exposes. The setters themselves never emit, but
|
|
553
|
+
// setPlaying(true) does arm the keepalive effect, whose re-emit — like
|
|
554
|
+
// every follower emit — is dropped host-side by the presenter role gate.
|
|
555
|
+
const offApply = window.__miaodaMirror.onApply('miaoda:anim:playhead-changed', (m, ctx) => {
|
|
556
|
+
// Capture iframes are frozen stills — a forwarded replay's playing:true
|
|
557
|
+
// would unfreeze the poster mid-screenshot. The emit side is already
|
|
558
|
+
// silent under captureMode; keep the apply side symmetric.
|
|
559
|
+
if (captureFrame) return { type: 'ignored', reason: 'capture iframe' };
|
|
560
|
+
// isFinite, not typeof: NaN would sail through clamp() (Math.min/max
|
|
561
|
+
// propagate it) into setTime and persisted state.
|
|
562
|
+
if (!Number.isFinite(m.t)) return { type: 'ignored', reason: 'no t' };
|
|
563
|
+
// Kill any pending local-emit timer — it reads state at fire time and
|
|
564
|
+
// would re-broadcast the just-replayed values as this end's own (same
|
|
565
|
+
// rule as design-canvas's viewport applier).
|
|
566
|
+
clearTimeout(emitTRef.current);
|
|
567
|
+
emitTRef.current = 0;
|
|
568
|
+
const st = s.current;
|
|
569
|
+
// While playing, advance the replayed time by the transport delay so
|
|
570
|
+
// both playheads aim at the same wall-clock frame. A looping stage
|
|
571
|
+
// wraps rather than clamps — a snapshot sent near the seam would
|
|
572
|
+
// otherwise pin the follower on the last frame until its own rAF
|
|
573
|
+
// wraps.
|
|
574
|
+
const lat = m.playing && ctx && Number.isFinite(ctx.latencyMs) ? ctx.latencyMs / 1000 : 0;
|
|
575
|
+
const nt = m.t + lat;
|
|
576
|
+
// Double-mod wrap: JS % keeps the sign, and a (forged/skewed) negative
|
|
577
|
+
// t would otherwise render a negative playhead.
|
|
578
|
+
st.setTime(m.playing && st.loop && st.duration > 0 ? ((nt % st.duration) + st.duration) % st.duration : clamp(nt, 0, st.duration));
|
|
579
|
+
st.setPlaying(!!m.playing);
|
|
580
|
+
lastApplyTsRef.current = Date.now();
|
|
581
|
+
return { type: 'applied' };
|
|
582
|
+
});
|
|
583
|
+
return () => {
|
|
584
|
+
offApply();
|
|
585
|
+
if (window.__animStage === bridge) delete window.__animStage;
|
|
586
|
+
};
|
|
424
587
|
}, []);
|
|
425
588
|
|
|
426
589
|
// Animation loop
|
|
@@ -457,12 +620,16 @@ function Stage({
|
|
|
457
620
|
if (e.code === 'Space') {
|
|
458
621
|
e.preventDefault();
|
|
459
622
|
setPlaying(p => !p);
|
|
623
|
+
emitPlayhead();
|
|
460
624
|
} else if (e.code === 'ArrowLeft') {
|
|
461
625
|
setTime(t => clamp(t - (e.shiftKey ? 1 : 0.1), 0, duration));
|
|
626
|
+
emitPlayhead();
|
|
462
627
|
} else if (e.code === 'ArrowRight') {
|
|
463
628
|
setTime(t => clamp(t + (e.shiftKey ? 1 : 0.1), 0, duration));
|
|
629
|
+
emitPlayhead();
|
|
464
630
|
} else if (e.key === '0' || e.code === 'Home') {
|
|
465
631
|
setTime(0);
|
|
632
|
+
emitPlayhead();
|
|
466
633
|
}
|
|
467
634
|
};
|
|
468
635
|
window.addEventListener('keydown', onKey);
|
|
@@ -519,9 +686,9 @@ function Stage({
|
|
|
519
686
|
time={time}
|
|
520
687
|
duration={duration}
|
|
521
688
|
playing={playing}
|
|
522
|
-
onPlayPause={() => setPlaying(p => !p)}
|
|
523
|
-
onReset={() => { setTime(0); }}
|
|
524
|
-
onSeek={(t) => setTime(t)}
|
|
689
|
+
onPlayPause={() => { setPlaying(p => !p); emitPlayhead(); }}
|
|
690
|
+
onReset={() => { setTime(0); emitPlayhead(); }}
|
|
691
|
+
onSeek={(t) => { setTime(t); emitPlayhead(); }}
|
|
525
692
|
/>
|
|
526
693
|
)}
|
|
527
694
|
</div>
|
|
@@ -32,6 +32,9 @@
|
|
|
32
32
|
// On mobile devices the canvas automatically becomes a browse-only surface
|
|
33
33
|
// (touch pan/zoom, editing disabled). This needs NO authoring changes:
|
|
34
34
|
// never add custom touch handling or mobile-specific layouts.
|
|
35
|
+
//
|
|
36
|
+
// window.__miaodaMirror (defined below) is Magic-Share sync plumbing between
|
|
37
|
+
// this component and the miaoda host — internal; do not call or remove it.
|
|
35
38
|
/* END USAGE */
|
|
36
39
|
|
|
37
40
|
const DC = {
|
|
@@ -54,6 +57,82 @@ const dcIsMobile = typeof navigator !== 'undefined' &&
|
|
|
54
57
|
(DC_MOBILE_UA_RE.test(navigator.userAgent) ||
|
|
55
58
|
(/iPad|Tab|Tablet/i.test(navigator.userAgent) && !!navigator.maxTouchPoints && navigator.maxTouchPoints > 1));
|
|
56
59
|
|
|
60
|
+
// Capture iframes (?thumbnail= product screenshots) sit out Magic Share in
|
|
61
|
+
// BOTH directions — same rule as deck-stage's _snthumb and animations'
|
|
62
|
+
// captureMode: their emits would carry a throwaway instance's state, and a
|
|
63
|
+
// (mis)forwarded replay could reframe the canvas mid-shot.
|
|
64
|
+
const dcCaptureMode = typeof location !== 'undefined' && /[?&](_snthumb|thumbnail)=/.test(location.search || '');
|
|
65
|
+
|
|
66
|
+
// ── Mirror sync singleton ──────────────────────────────────────────────────
|
|
67
|
+
// Shared by every buildless script on the page (first one to load defines
|
|
68
|
+
// it). Inlined per script rather than shipped as its own registry file:
|
|
69
|
+
// call sites use the global bare, so inlining is what guarantees
|
|
70
|
+
// definition-before-use — a separate copy-in file would add a whole "SDK
|
|
71
|
+
// copied but <script> tag missing" failure mode. Keep this block identical
|
|
72
|
+
// across deck-stage.js / design-canvas.jsx / animations.jsx — same
|
|
73
|
+
// convention as the mobile-UA regex. Two halves of
|
|
74
|
+
// the 妙享 (Magic Share) mirror protocol:
|
|
75
|
+
// emit(msg, opts) — stamps msg.__mirror so the host relay treats the
|
|
76
|
+
// message as a broadcastable UI-state snapshot;
|
|
77
|
+
// unstamped messages are never mirrored. opts.key is
|
|
78
|
+
// the sync channel (maps to the share SDK's
|
|
79
|
+
// actionType): late joiners receive only the LATEST
|
|
80
|
+
// snapshot per key, so states that overwrite each
|
|
81
|
+
// other must share one key. opts.throttle is the
|
|
82
|
+
// suggested host-side coalescing interval in ms —
|
|
83
|
+
// snapshots make dropped intermediate frames
|
|
84
|
+
// lossless, so hint it on any continuous gesture.
|
|
85
|
+
// onApply(type, fn) — replay entry for a message type this script itself
|
|
86
|
+
// emits. The host sends {type:'miaoda:mirror:apply',
|
|
87
|
+
// uuid, message, latencyMs} and needs
|
|
88
|
+
// {type:'miaoda:mirror:apply-result', uuid, result}
|
|
89
|
+
// back to drive the SDK's retry: result.type
|
|
90
|
+
// 'failed' means retry (use it for transient
|
|
91
|
+
// not-ready states), 'ignored' is terminal (not
|
|
92
|
+
// applicable / bad payload), 'applied' is success.
|
|
93
|
+
// Unregistered types stay silent on purpose —
|
|
94
|
+
// another script on the page may own them, and the
|
|
95
|
+
// host's timeout is the backstop.
|
|
96
|
+
if (!window.__miaodaMirror) {
|
|
97
|
+
const appliers = new Map(); // message.type -> Set<fn>
|
|
98
|
+
window.__miaodaMirror = {
|
|
99
|
+
emit(msg, opts) {
|
|
100
|
+
try { window.parent.postMessage(Object.assign({}, msg, { __mirror: Object.assign({ v: 1 }, opts) }), '*'); } catch (e) {}
|
|
101
|
+
},
|
|
102
|
+
onApply(type, fn) {
|
|
103
|
+
if (!appliers.has(type)) appliers.set(type, new Set());
|
|
104
|
+
appliers.get(type).add(fn);
|
|
105
|
+
return () => { const s = appliers.get(type); if (s) s.delete(fn); };
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
window.addEventListener('message', async (e) => {
|
|
109
|
+
// Replays are accepted from the host frame only — generated pages embed
|
|
110
|
+
// arbitrary third-party iframes, any of which could otherwise forge an
|
|
111
|
+
// apply and drive state (plus its persisted side effects).
|
|
112
|
+
if (e.source !== window.parent) return;
|
|
113
|
+
const d = e.data;
|
|
114
|
+
if (!d || d.type !== 'miaoda:mirror:apply' || !d.message || !d.message.type) return;
|
|
115
|
+
const fns = appliers.get(d.message.type);
|
|
116
|
+
if (!fns || !fns.size) return;
|
|
117
|
+
// Merge across handlers (multi-instance case): one success is enough.
|
|
118
|
+
// Instances share a channel, so a sibling that can't apply yet (a lazily
|
|
119
|
+
// slotted second deck returning 'failed') must not poison an applied
|
|
120
|
+
// result into an SDK retry storm of no-ops — 'failed' only stands when
|
|
121
|
+
// nobody applied. Every handler still runs (no short-circuit) so each
|
|
122
|
+
// sibling instance gets the state.
|
|
123
|
+
let result = { type: 'ignored', reason: 'no applier result' };
|
|
124
|
+
for (const fn of Array.from(fns)) {
|
|
125
|
+
let r;
|
|
126
|
+
try { r = await fn(d.message, { latencyMs: d.latencyMs, uuid: d.uuid }); }
|
|
127
|
+
catch (err) { r = { type: 'failed', reason: String((err && err.message) || err) }; }
|
|
128
|
+
if (!r) continue;
|
|
129
|
+
if (r.type === 'applied') result = r;
|
|
130
|
+
else if (result.type !== 'applied' && (r.type === 'failed' || result.type !== 'failed')) result = r;
|
|
131
|
+
}
|
|
132
|
+
try { window.parent.postMessage({ type: 'miaoda:mirror:apply-result', uuid: d.uuid, result }, '*'); } catch (e2) {}
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
57
136
|
// One-time CSS injection (classes are dc-prefixed so they don't collide with
|
|
58
137
|
// the hosted design's own styles).
|
|
59
138
|
if (typeof document !== 'undefined' && !document.getElementById('dc-styles')) {
|
|
@@ -228,10 +307,23 @@ function dcFlatten(children) {
|
|
|
228
307
|
// ─────────────────────────────────────────────────────────────
|
|
229
308
|
const DC_STATE_FILE = '.design-canvas.state.json';
|
|
230
309
|
|
|
231
|
-
// Persist a sidecar file back to the host.
|
|
232
|
-
|
|
310
|
+
// Persist a sidecar file back to the host. writeContext is an OWNERSHIP
|
|
311
|
+
// declaration for the host's release pipeline (not a sync receipt — the
|
|
312
|
+
// product can't know whether a Magic Share sync actually reached anyone):
|
|
313
|
+
// {kind:'mirror-managed', key} says this file's full content is the same
|
|
314
|
+
// state a mirror channel carries, so the host may skip reloading followers
|
|
315
|
+
// in FOLLOW mode for a release made purely of such writes — they already
|
|
316
|
+
// have the state via mirror, and channel guarantees (retry / per-key latest
|
|
317
|
+
// on rejoin / re-seed after reload / the file itself on next cold load)
|
|
318
|
+
// cover every miss. Persistence and deploy proceed unchanged — this is NOT
|
|
319
|
+
// a notify:false. Only declare it where emit and write are fed by the very
|
|
320
|
+
// same snapshot — an untagged write reloads as before, which is the
|
|
321
|
+
// fail-safe for states that are NOT mirrored.
|
|
322
|
+
function miaodaWriteFile(path, content, writeContext) {
|
|
233
323
|
try {
|
|
234
|
-
|
|
324
|
+
const msg = { type: 'miaoda:bridge:write-file', path: path, content: content };
|
|
325
|
+
if (writeContext) msg.writeContext = writeContext;
|
|
326
|
+
window.parent.postMessage(msg, '*');
|
|
235
327
|
} catch (e) {
|
|
236
328
|
/* no parent — ignore */
|
|
237
329
|
}
|
|
@@ -248,13 +340,30 @@ function DesignCanvas({ children, minScale, maxScale, style }) {
|
|
|
248
340
|
const [ready, setReady] = React.useState(false);
|
|
249
341
|
const didRead = React.useRef(false);
|
|
250
342
|
const skipNextWrite = React.useRef(false);
|
|
343
|
+
// Magic Share echo guards, one per mirrored state: a remote apply must land
|
|
344
|
+
// locally without re-broadcasting as if the user did it here.
|
|
345
|
+
const skipNextMirrorEmit = React.useRef(false);
|
|
346
|
+
const skipNextFocusEmit = React.useRef(false);
|
|
347
|
+
// Current focus, readable synchronously by the applier — an apply carrying
|
|
348
|
+
// the value we already hold must NOT set the skip flag: same-value setState
|
|
349
|
+
// re-renders but the [state.focus] effect bails out (primitive dep), so the
|
|
350
|
+
// flag would strand and swallow the next real focus broadcast.
|
|
351
|
+
const focusRef = React.useRef(state.focus);
|
|
352
|
+
focusRef.current = state.focus;
|
|
353
|
+
|
|
354
|
+
// Set once a Magic Share structure replay has landed. A late joiner's
|
|
355
|
+
// replay races the sidecar fetch below — the replay is the fresher truth
|
|
356
|
+
// (it IS the presenter's current arrangement, and it was already answered
|
|
357
|
+
// with 'applied' so the SDK won't resend), so a slower fetch must not
|
|
358
|
+
// clobber it with this end's stale sidecar.
|
|
359
|
+
const mirrorSeeded = React.useRef(false);
|
|
251
360
|
|
|
252
361
|
React.useEffect(() => {
|
|
253
362
|
let off = false;
|
|
254
363
|
fetch('./' + DC_STATE_FILE)
|
|
255
364
|
.then((r) => (r.ok ? r.json() : null))
|
|
256
365
|
.then((saved) => {
|
|
257
|
-
if (off || !saved || !saved.sections) return;
|
|
366
|
+
if (off || mirrorSeeded.current || !saved || !saved.sections) return;
|
|
258
367
|
skipNextWrite.current = true;
|
|
259
368
|
setState((s) => ({ ...s, sections: saved.sections }));
|
|
260
369
|
})
|
|
@@ -266,13 +375,78 @@ function DesignCanvas({ children, minScale, maxScale, style }) {
|
|
|
266
375
|
|
|
267
376
|
React.useEffect(() => {
|
|
268
377
|
if (!didRead.current) return;
|
|
378
|
+
// Magic Share: snapshot the arrangement whenever it settles — including
|
|
379
|
+
// the sidecar hydration (the skipNextWrite path), which is exactly the
|
|
380
|
+
// baseline a late-joining follower needs. Only a remote apply must not
|
|
381
|
+
// re-broadcast.
|
|
382
|
+
if (skipNextMirrorEmit.current) skipNextMirrorEmit.current = false;
|
|
383
|
+
else if (!dcCaptureMode) window.__miaodaMirror.emit({ type: 'miaoda:canvas:structure-changed', sections: state.sections }, { key: 'canvas:structure' });
|
|
269
384
|
if (skipNextWrite.current) { skipNextWrite.current = false; return; }
|
|
270
385
|
const t = setTimeout(() => {
|
|
271
|
-
|
|
386
|
+
// mirror-managed precondition: this write persists the same snapshot
|
|
387
|
+
// the emit above carried on canvas:structure — keep both fed by the
|
|
388
|
+
// same state if either moves.
|
|
389
|
+
miaodaWriteFile(DC_STATE_FILE, JSON.stringify({ sections: state.sections }),
|
|
390
|
+
{ kind: 'mirror-managed', key: 'canvas:structure' }).catch(() => {});
|
|
272
391
|
}, 250);
|
|
273
392
|
return () => clearTimeout(t);
|
|
274
393
|
}, [state.sections]);
|
|
275
394
|
|
|
395
|
+
// Magic Share: focus is ephemeral view state — mirror it as its own key so
|
|
396
|
+
// a follower enters/exits the focus overlay in step with the presenter.
|
|
397
|
+
React.useEffect(() => {
|
|
398
|
+
if (dcCaptureMode) return;
|
|
399
|
+
if (skipNextFocusEmit.current) { skipNextFocusEmit.current = false; return; }
|
|
400
|
+
window.__miaodaMirror.emit({ type: 'miaoda:canvas:focus-changed', focus: state.focus }, { key: 'canvas:focus' });
|
|
401
|
+
}, [state.focus]);
|
|
402
|
+
|
|
403
|
+
// Magic Share replay entries — consume the same message types this
|
|
404
|
+
// component emits, so the emit↔apply contract stays in this one file.
|
|
405
|
+
React.useEffect(() => {
|
|
406
|
+
if (dcCaptureMode) return;
|
|
407
|
+
const offs = [
|
|
408
|
+
window.__miaodaMirror.onApply('miaoda:canvas:structure-changed', (m) => {
|
|
409
|
+
// Per-field shape check, not just typeof: a malformed snapshot (e.g.
|
|
410
|
+
// order that isn't an array) would pass setState here, be answered
|
|
411
|
+
// 'applied', and only then blow up the whole React tree in render —
|
|
412
|
+
// an unrecoverable failure the retry protocol can't see.
|
|
413
|
+
if (!m.sections || typeof m.sections !== 'object') return { type: 'ignored', reason: 'no sections' };
|
|
414
|
+
for (const sec of Object.values(m.sections)) {
|
|
415
|
+
if (!sec || typeof sec !== 'object' ||
|
|
416
|
+
(sec.order !== undefined && !Array.isArray(sec.order)) ||
|
|
417
|
+
(sec.hidden !== undefined && !Array.isArray(sec.hidden)) ||
|
|
418
|
+
(sec.labels !== undefined && (typeof sec.labels !== 'object' || sec.labels === null))) {
|
|
419
|
+
return { type: 'ignored', reason: 'malformed sections' };
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
skipNextMirrorEmit.current = true;
|
|
423
|
+
// A replayed arrangement is the presenter's state, not a local edit —
|
|
424
|
+
// never persist it back through this end's write bridge.
|
|
425
|
+
skipNextWrite.current = true;
|
|
426
|
+
// Marking didRead lets the sections effect run NOW and consume both
|
|
427
|
+
// flags — before hydration settles they would otherwise strand on the
|
|
428
|
+
// effect's !didRead early-return and swallow a later real emit/write.
|
|
429
|
+
mirrorSeeded.current = true;
|
|
430
|
+
didRead.current = true;
|
|
431
|
+
setState((s) => ({ ...s, sections: m.sections }));
|
|
432
|
+
return { type: 'applied' };
|
|
433
|
+
}),
|
|
434
|
+
window.__miaodaMirror.onApply('miaoda:canvas:focus-changed', (m) => {
|
|
435
|
+
const next = typeof m.focus === 'string' ? m.focus : null;
|
|
436
|
+
// Same value → nothing will change and the [state.focus] effect won't
|
|
437
|
+
// re-run; setting the skip flag would strand it (see focusRef note).
|
|
438
|
+
if (next === focusRef.current) return { type: 'applied' };
|
|
439
|
+
// Sync the ref now, not at next render — a second apply landing in
|
|
440
|
+
// the same tick would otherwise compare against the stale value.
|
|
441
|
+
focusRef.current = next;
|
|
442
|
+
skipNextFocusEmit.current = true;
|
|
443
|
+
setState((s) => ({ ...s, focus: next }));
|
|
444
|
+
return { type: 'applied' };
|
|
445
|
+
}),
|
|
446
|
+
];
|
|
447
|
+
return () => offs.forEach((off) => off());
|
|
448
|
+
}, []);
|
|
449
|
+
|
|
276
450
|
// Build registries synchronously from children so FocusOverlay can read
|
|
277
451
|
// them in the same render. Fragments are flattened; wrapping in other
|
|
278
452
|
// elements still opts out of focus/reorder.
|
|
@@ -466,6 +640,16 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
466
640
|
const saveT = React.useRef(0);
|
|
467
641
|
|
|
468
642
|
const lastPostedScale = React.useRef();
|
|
643
|
+
// Magic Share: trailing-throttle timer for the viewport snapshot, plus a
|
|
644
|
+
// guard so a replayed viewport doesn't schedule its own re-broadcast.
|
|
645
|
+
const mirrorEmitT = React.useRef(0);
|
|
646
|
+
const applyingRemoteView = React.useRef(false);
|
|
647
|
+
// True while tf holds a REMOTE framing (last write came from a replay,
|
|
648
|
+
// no local gesture since). Gates every localStorage persist path — the
|
|
649
|
+
// 200ms saveT and the pagehide/unmount flush both read tf at fire time,
|
|
650
|
+
// so guarding only the scheduling call would still leak the presenter's
|
|
651
|
+
// framing into this viewer's saved viewport.
|
|
652
|
+
const viewIsRemote = React.useRef(false);
|
|
469
653
|
// Imperative handle for the on-canvas zoom control to zoom around the
|
|
470
654
|
// viewport centre (same path as the host's set-zoom message). Assigned in
|
|
471
655
|
// the pointer/wheel effect below where zoomAt is in scope.
|
|
@@ -500,12 +684,36 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
500
684
|
// ticks leave scale unchanged — skip the cross-frame post for those.
|
|
501
685
|
if (lastPostedScale.current !== scale) {
|
|
502
686
|
lastPostedScale.current = scale;
|
|
503
|
-
window.parent.postMessage({ type: 'miaoda:canvas:zoom', scale }, '*');
|
|
687
|
+
try { window.parent.postMessage({ type: 'miaoda:canvas:zoom', scale }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
688
|
+
}
|
|
689
|
+
// A replayed viewport is the presenter's framing, not this viewer's — it
|
|
690
|
+
// must not overwrite the viewport this end will restore when opening the
|
|
691
|
+
// canvas on its own later. Any local gesture re-owns the view (and its
|
|
692
|
+
// persistence); apply() is the single fan-out for both, so this branch
|
|
693
|
+
// is the one place that distinction needs to live.
|
|
694
|
+
if (applyingRemoteView.current) {
|
|
695
|
+
viewIsRemote.current = true;
|
|
696
|
+
} else {
|
|
697
|
+
viewIsRemote.current = false;
|
|
698
|
+
clearTimeout(saveT.current);
|
|
699
|
+
saveT.current = setTimeout(() => {
|
|
700
|
+
try { localStorage.setItem(tfKey, JSON.stringify(tf.current)); } catch {}
|
|
701
|
+
}, 200);
|
|
702
|
+
}
|
|
703
|
+
// Magic Share: mirror the full viewport (scale AND pan) as a snapshot.
|
|
704
|
+
// Trailing-throttled at 120ms rather than per-apply — apply() runs at
|
|
705
|
+
// pointer rate, and this file deliberately avoids per-frame cross-frame
|
|
706
|
+
// posts (see the zoom guard above); 120ms keeps a follower tracking a
|
|
707
|
+
// continuous pan without that cost.
|
|
708
|
+
if (!dcCaptureMode && !applyingRemoteView.current && !mirrorEmitT.current) {
|
|
709
|
+
mirrorEmitT.current = setTimeout(() => {
|
|
710
|
+
mirrorEmitT.current = 0;
|
|
711
|
+
window.__miaodaMirror.emit(
|
|
712
|
+
{ type: 'miaoda:canvas:viewport-changed', x: tf.current.x, y: tf.current.y, scale: tf.current.scale },
|
|
713
|
+
{ key: 'canvas:viewport', throttle: 60 },
|
|
714
|
+
);
|
|
715
|
+
}, 120);
|
|
504
716
|
}
|
|
505
|
-
clearTimeout(saveT.current);
|
|
506
|
-
saveT.current = setTimeout(() => {
|
|
507
|
-
try { localStorage.setItem(tfKey, JSON.stringify(tf.current)); } catch {}
|
|
508
|
-
}, 200);
|
|
509
717
|
}, [tfKey]);
|
|
510
718
|
|
|
511
719
|
// Whether the mount-time restore found a saved viewport — when it did, the
|
|
@@ -516,6 +724,9 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
516
724
|
React.useLayoutEffect(() => {
|
|
517
725
|
const flush = () => {
|
|
518
726
|
clearTimeout(saveT.current);
|
|
727
|
+
// Unload/unmount with a replayed framing on screen: persist nothing —
|
|
728
|
+
// the saved viewport must stay this viewer's own.
|
|
729
|
+
if (viewIsRemote.current) return;
|
|
519
730
|
try { localStorage.setItem(tfKey, JSON.stringify(tf.current)); } catch {}
|
|
520
731
|
};
|
|
521
732
|
try {
|
|
@@ -846,16 +1057,48 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
846
1057
|
// images/fonts is after our mount-time announce, so re-announce.
|
|
847
1058
|
// Clear the pan-tick guard so apply() re-posts the current scale
|
|
848
1059
|
// even if it's unchanged — the host just reset dcScale to 1.
|
|
849
|
-
window.parent.postMessage({ type: 'miaoda:canvas:present' }, '*');
|
|
1060
|
+
try { window.parent.postMessage({ type: 'miaoda:canvas:present' }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
850
1061
|
// Re-announce the upgrade capability too — the host's readyGen reset
|
|
851
1062
|
// (fires on iframe 'load', which for image/font-heavy canvases lands
|
|
852
1063
|
// after the mount-time announce) drops prior state, so re-declare here.
|
|
853
|
-
window.parent.postMessage({ type: 'miaoda:upgrade:available', kind: 'design-canvas' }, '*');
|
|
1064
|
+
try { window.parent.postMessage({ type: 'miaoda:upgrade:available', kind: 'design-canvas' }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
854
1065
|
lastPostedScale.current = undefined;
|
|
1066
|
+
// The re-announce apply() must not change who owns the view: a
|
|
1067
|
+
// viewport seed can land BEFORE the iframe's native 'load' fires this
|
|
1068
|
+
// probe, and a plain local apply() would flip viewIsRemote and
|
|
1069
|
+
// persist the presenter's framing — the exact leak viewIsRemote
|
|
1070
|
+
// exists to stop. The zoom % re-post is ungated either way.
|
|
1071
|
+
applyingRemoteView.current = viewIsRemote.current;
|
|
855
1072
|
apply();
|
|
1073
|
+
applyingRemoteView.current = false;
|
|
856
1074
|
}
|
|
857
1075
|
};
|
|
858
1076
|
window.addEventListener('message', onHostMsg);
|
|
1077
|
+
// Magic Share replay entry for the viewport — reuses the local apply()
|
|
1078
|
+
// pipeline (DOM transform, --dc-inv-zoom, host % readout); the
|
|
1079
|
+
// applyingRemoteView guard keeps the replay from re-broadcasting or
|
|
1080
|
+
// persisting the presenter's framing.
|
|
1081
|
+
const offViewportApply = dcCaptureMode ? () => {} : window.__miaodaMirror.onApply('miaoda:canvas:viewport-changed', (m) => {
|
|
1082
|
+
if (![m.x, m.y, m.scale].every(Number.isFinite)) return { type: 'ignored', reason: 'bad snapshot' };
|
|
1083
|
+
// Kill any pending local-emit timer: it was armed before this replay
|
|
1084
|
+
// and would otherwise fire 120ms later with tf already holding the
|
|
1085
|
+
// REMOTE state — echoing the presenter's viewport back as our own.
|
|
1086
|
+
clearTimeout(mirrorEmitT.current);
|
|
1087
|
+
mirrorEmitT.current = 0;
|
|
1088
|
+
// Same staleness for the persist timer: scheduled by a local gesture,
|
|
1089
|
+
// but it reads tf at fire time and would save the remote framing.
|
|
1090
|
+
clearTimeout(saveT.current);
|
|
1091
|
+
// A replayed framing IS this canvas's framing now — mark the first-visit
|
|
1092
|
+
// fit as satisfied, or its layout effect (armed whenever a fresh viewer
|
|
1093
|
+
// has no saved viewport) would re-measure and override the presenter's
|
|
1094
|
+
// viewport as soon as content mounts.
|
|
1095
|
+
didFit.current = true;
|
|
1096
|
+
applyingRemoteView.current = true;
|
|
1097
|
+
tf.current = { x: m.x, y: m.y, scale: Math.min(maxScale, Math.max(minScale, m.scale)) };
|
|
1098
|
+
apply();
|
|
1099
|
+
applyingRemoteView.current = false;
|
|
1100
|
+
return { type: 'applied' };
|
|
1101
|
+
});
|
|
859
1102
|
// Announce canvas mode so the host toolbar proxies its % control here
|
|
860
1103
|
// instead of scaling the iframe element (which would just shrink the
|
|
861
1104
|
// viewport window of an infinite canvas). The apply() that follows emits
|
|
@@ -863,13 +1106,13 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
863
1106
|
// lastPostedScale reset mirrors the miaoda:canvas:probe handler: the layout
|
|
864
1107
|
// effect's restore-path apply() may already have posted the restored
|
|
865
1108
|
// scale (before miaoda:canvas:present), so clear the guard to re-post it in order.
|
|
866
|
-
window.parent.postMessage({ type: 'miaoda:canvas:present' }, '*');
|
|
1109
|
+
try { window.parent.postMessage({ type: 'miaoda:canvas:present' }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
867
1110
|
// Generic "upgrade" capability signal — announce that this artifact offers
|
|
868
1111
|
// an upgrade entry so the host can show/hide its upgrade button. Decoupled
|
|
869
1112
|
// from the canvas zoom-proxy protocol above; any product may post the same
|
|
870
1113
|
// message. Listener (onHostMsg) is already registered above → announce-after-
|
|
871
1114
|
// register order holds. No client-side behavior here.
|
|
872
|
-
window.parent.postMessage({ type: 'miaoda:upgrade:available', kind: 'design-canvas' }, '*');
|
|
1115
|
+
try { window.parent.postMessage({ type: 'miaoda:upgrade:available', kind: 'design-canvas' }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
873
1116
|
lastPostedScale.current = undefined;
|
|
874
1117
|
apply();
|
|
875
1118
|
|
|
@@ -889,6 +1132,9 @@ function DCViewport({ children, minScale = 0.1, maxScale = 8, style = {} }) {
|
|
|
889
1132
|
vp.addEventListener('pointercancel', pu);
|
|
890
1133
|
return () => {
|
|
891
1134
|
window.removeEventListener('message', onHostMsg);
|
|
1135
|
+
offViewportApply();
|
|
1136
|
+
clearTimeout(mirrorEmitT.current);
|
|
1137
|
+
mirrorEmitT.current = 0;
|
|
892
1138
|
vp.removeEventListener('wheel', onWheel);
|
|
893
1139
|
vp.removeEventListener('gesturestart', onGestureStart);
|
|
894
1140
|
vp.removeEventListener('gesturechange', onGestureChange);
|
|
@@ -190,7 +190,7 @@ function useTweaks(defaults) {
|
|
|
190
190
|
const edits = typeof keyOrEdits === 'object' && keyOrEdits !== null
|
|
191
191
|
? keyOrEdits : { [keyOrEdits]: val };
|
|
192
192
|
setValues((prev) => ({ ...prev, ...edits }));
|
|
193
|
-
window.parent.postMessage({ type: 'miaoda:tweaks:set-keys', edits }, '*');
|
|
193
|
+
try { window.parent.postMessage({ type: 'miaoda:tweaks:set-keys', edits }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
194
194
|
// Same-window signal so in-page listeners (deck-stage rail thumbnails)
|
|
195
195
|
// can react — the parent message only reaches the host, not peers.
|
|
196
196
|
window.dispatchEvent(new CustomEvent('tweakchange', { detail: edits }));
|
|
@@ -248,13 +248,13 @@ function TweaksPanel({ title = 'Tweaks', children }) {
|
|
|
248
248
|
else if (t === 'miaoda:tweaks:deactivate') setOpen(false);
|
|
249
249
|
};
|
|
250
250
|
window.addEventListener('message', onMsg);
|
|
251
|
-
window.parent.postMessage({ type: 'miaoda:tweaks:available' }, '*');
|
|
251
|
+
try { window.parent.postMessage({ type: 'miaoda:tweaks:available' }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
252
252
|
return () => window.removeEventListener('message', onMsg);
|
|
253
253
|
}, []);
|
|
254
254
|
|
|
255
255
|
const dismiss = () => {
|
|
256
256
|
setOpen(false);
|
|
257
|
-
window.parent.postMessage({ type: 'miaoda:tweaks:dismissed' }, '*');
|
|
257
|
+
try { window.parent.postMessage({ type: 'miaoda:tweaks:dismissed' }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
258
258
|
};
|
|
259
259
|
|
|
260
260
|
const onDragStart = (e) => {
|
|
@@ -586,8 +586,11 @@ Object.assign(window, {
|
|
|
586
586
|
document.head.appendChild(s);
|
|
587
587
|
})();
|
|
588
588
|
|
|
589
|
-
const __twkSendChat = (text) =>
|
|
590
|
-
|
|
589
|
+
const __twkSendChat = (text) => {
|
|
590
|
+
// Posting to the host must never take the component down: the parent frame can be
|
|
591
|
+
// cross-origin or already gone, and postMessage throws in both cases.
|
|
592
|
+
try { window.parent.postMessage({ type: 'miaoda:tweaks:chat', text }, '*'); } catch (e) { /* no parent — ignore */ }
|
|
593
|
+
};
|
|
591
594
|
|
|
592
595
|
const __TWK_SPARK_PATH = 'M18.3658 62.2435L36.7823 51.9165L37.0858 51.012L36.7823 50.5083H35.8716L32.7853 50.3206L22.2616 50.0389L13.1546 49.6634L4.30054 49.194L2.07438 48.7246L0 45.9551L0.202378 44.5938L2.07438 43.3264L4.75589 43.5611L10.6755 43.9836L19.5801 44.5938L26.0056 44.9693L35.568 45.9551H37.0858L37.2882 45.3448L36.7823 44.9693L36.3775 44.5938L27.1693 38.3507L17.2022 31.7789L11.9909 27.9767L9.20822 26.0522L7.79157 24.2684L7.18443 20.3254L9.71416 17.5089L13.1546 17.7436L14.0147 17.9783L17.5057 20.654L24.9431 26.4277L34.6573 33.5627L36.0739 34.7362L36.6444 34.3512L36.7317 34.079L36.0739 32.9994L30.8121 23.4704L25.1961 13.7537L22.6664 9.71675L22.0086 7.32277C21.7539 6.31812 21.6039 5.48695 21.6039 4.45938L24.4878 0.516349L26.1068 0L30.0026 0.516349L31.6216 1.92457L34.0502 7.46359L37.9459 16.1476L44.0173 27.9767L45.7881 31.4973L46.7494 34.7362L47.1036 35.722H47.7107V35.1587L48.2166 28.4931L49.1274 20.3254L50.0381 9.81063L50.3416 6.85336L51.8089 3.28586L54.7434 1.36128L57.0201 2.44092L58.8921 5.11655L58.6391 6.85336L57.5261 14.0822L55.3505 25.395L53.9338 32.9994H54.7434L55.7047 32.0136L59.5498 26.944L65.9753 18.8702L68.8086 15.6782L72.1479 12.1577L74.2729 10.4678H78.3204L81.2549 14.8802L79.9395 19.4335L75.7907 24.6909L72.3503 29.1503L67.4173 35.7593L64.3563 41.0732L64.6308 41.5116L65.3682 41.4487L76.499 39.0548L82.5198 37.9751L89.7042 36.7547L92.9423 38.2568L93.2964 39.8058L92.0316 42.9509L84.3412 44.8285L75.3354 46.6592L61.9245 49.8162L61.776 49.9356L61.9513 50.1956L67.9991 50.743L70.5795 50.8839H76.9038L88.6923 51.7757L91.7786 53.7942L93.6 56.282L93.2964 58.2066L88.5405 60.6006L82.1656 59.0985L67.2402 55.531L62.1302 54.2636H61.4218V54.6861L65.6718 58.8638L73.514 65.9049L83.2787 75.0114L83.7846 77.2646L82.5198 79.0483L81.2043 78.8606L72.6032 72.3827L69.264 69.4724L61.776 63.1354H61.2701V63.7926L62.9903 66.3274L72.1479 80.081L72.6032 84.3057L71.9455 85.667L69.5676 86.5119L66.9872 86.0425L61.5736 78.4851L56.0588 70.0357L51.6065 62.4313L51.0687 62.7708L48.419 91.0652L47.2048 92.5204L44.3715 93.6L41.9935 91.8162L40.7286 88.9059L41.9935 83.1322L43.5114 75.6217L44.7256 69.6602L45.8387 62.2435L46.5185 59.7659L46.4584 59.6001L45.9153 59.6914L40.3239 67.3601L31.824 78.8606L25.0949 86.0425L23.4759 86.6997L20.6932 85.2445L20.9462 82.6628L22.5146 80.3627L31.824 68.5336L37.44 61.1639L41.0595 56.9335L41.0243 56.3216L40.8245 56.3046L16.0891 72.4297L11.6874 72.993L9.76476 71.2092L10.0177 68.2989L10.9284 67.3601L18.3658 62.2435Z';
|
|
593
596
|
|