diff --git a/README.md b/README.md index 644ab75..2b28992 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Do not commit extracted geometry, icons, or a third-party app bundle. ## What's on the site -The rail switches three views. **Customise** is 18 body shapes, 11 inks and the rest-face playlist. **Animations** is bloub’s timeline editor on our 39 states: drag to reorder, pull a card to set duration, save named cycles in `localStorage`. The right-hand tiles append a block. **Settings** is language (French, English, Chinese) and credits. GIF/MP4 export is not in this tree. +The rail switches three views. **Customise** is 8 body shapes, 11 inks and the rest-face playlist; stills export as PNG / SVG / GIF. **Animations** is bloub’s timeline editor on our 39 states: drag to reorder, pull a card to set duration, save named cycles in `localStorage`, export the sequence as MP4 or GIF. The right-hand tiles append a block. **Settings** is language (French, English, Chinese) and credits. The centre avatar is always `GrokCharacter` in `hold` mode. Tiles paint one frame and stop. Nothing from bloub’s `src/bot/` is in this tree. diff --git a/replica/index.html b/replica/index.html index f3ec1b2..247d90e 100644 --- a/replica/index.html +++ b/replica/index.html @@ -72,6 +72,17 @@ place-items: center; box-shadow: inset 0 -8px 18px rgba(40, 30, 10, 0.08), 0 12px 28px rgba(0,0,0,.3); } + .missing { + width: min(100%, 280px); + margin: 12px auto 0; + padding: 22px 18px; + border-radius: 16px; + background: #2a2823; + color: #d8d0c0; + font-size: 13px; + } + .missing strong { display: block; margin-bottom: 8px; color: var(--accent); font-weight: 560; } + .missing a { color: var(--accent); } .disk-login svg { width: 64px; height: 64px; overflow: visible; color-scheme: light; --bg: var(--disk); --fg: #111; } .disk-login span { display: block; @@ -181,6 +192,12 @@ 登录 sizePx: 64 +
@@ -296,6 +313,10 @@ document.getElementById("mode-hold").setAttribute("aria-pressed", mode === "hold"); } + if (!window.GROK_GEO?.shapes || typeof window.GROK?.create !== "function") { + document.getElementById("missing-geo").hidden = false; + } else { + GROK_META.groups.forEach((group) => { const h = document.createElement("h2"); h.textContent = group.label; @@ -387,6 +408,7 @@ document.getElementById("act-spin").onclick = () => bots.forEach((el) => el.spinOnce(1)); document.getElementById("act-bounce").onclick = () => bots.forEach((el) => el.bounceOnce()); document.getElementById("act-burst").onclick = () => bots.forEach((el) => el.burstOnce()); + } diff --git a/replica/src/api.js b/replica/src/api.js index 181690b..8496760 100644 --- a/replica/src/api.js +++ b/replica/src/api.js @@ -56,7 +56,8 @@ connectedCallback() { if (this._handle) return; - const root = this.attachShadow({ mode: "open" }); + const root = this.shadowRoot || this.attachShadow({ mode: "open" }); + root.replaceChildren(); const box = document.createElement("div"); box.setAttribute("part", "disk"); const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); diff --git a/replica/src/character.js b/replica/src/character.js index 393d874..27234b2 100644 --- a/replica/src/character.js +++ b/replica/src/character.js @@ -276,10 +276,20 @@ this.svg.style.setProperty("--bg", this.eyeColor || EYE_BG); } + _clock() { + return this.driven ? this.last : performance.now(); + } + setState(name, { resetEyes = false } = {}) { if (!this.playlists[name]) return; this.state = name; - this.stateAt = performance.now(); + this.stateAt = this._clock(); + if (this.driven) { + this.trick = null; + this.spinTurn = null; + this.hopAt = -1; + this.wildWide = false; + } const list = this.playlists[name]; this.eyeIdx = 0; if (resetEyes) { @@ -407,6 +417,11 @@ return this.snapshot(); } + setSize(px) { + this.sizePx = px > 0 ? px : null; + this._applyPoseScale(); + } + _applyPoseScale() { const sc = this.loginWrap ? poseScale(this.shapeName) : (this.pose.scale || 1); this.pose.scale = sc; @@ -529,7 +544,7 @@ } _cycleShapeTrick() { - if (this.reduceMotion || this.paused) return; + if (this.reduceMotion || this.paused || this.driven) return; this.trickCycle = (this.trickCycle + 1) % 5; this.wildWide = false; if (this.trickCycle === 0) this._pn(1); @@ -686,7 +701,7 @@ this.celebrateAt = now + 6200; } - if (now >= this.trickAt) { + if (!this.driven && now >= this.trickAt) { if ((V_T.has(this.state) || B_T.has(this.state)) && !this.spinTurn && this.hopAt < 0 && !this.trick) { const z = Math.random(); if (V_T.has(this.state)) { diff --git a/src/App.vue b/src/App.vue index 5c2899b..b774cfa 100644 --- a/src/App.vue +++ b/src/App.vue @@ -112,6 +112,7 @@ watch(activeId, (v) => { ecris('cycle', v) block.value = 0 elapsed.value = 0 + state.value = cycle.value.blocks[0]?.state ?? 'idle' }) watch(block, (i) => { @@ -144,9 +145,12 @@ function onSeek(t: number) { state.value = cycle.value.blocks[index]?.state ?? 'idle' } +const gardeAnim = ref(false) + watch(view, (now) => { intro.value = false if (now === 'animations') { + gardeAnim.value = true playing.value = true state.value = cycle.value.blocks[block.value]?.state ?? 'idle' return @@ -248,6 +252,11 @@ function annuleCycle() { watch(dialogueCycle, (ouverte) => { if (ouverte) erreurCycle.value = false + else annuleCycle() +}) + +watch([view, preview], () => { + if (view.value !== 'animations' || preview.value) annuleCycle() }) const CONFIRMATION_MS = 1800 @@ -330,6 +339,7 @@ onUnmounted(() => { clearTimeout(pending) clearTimeout(minuteurBarre) clearTimeout(confirmation) + annuleCycle() }) @@ -350,7 +360,8 @@ onUnmounted(() => {
{ :class="droite ? 'panneau--ouvert max-lg:order-2' : 'max-lg:hidden'" >
+ { @preview="preview = true" @exporter="dialogueCycle = true" /> + diff --git a/src/components/AnimPanel.vue b/src/components/AnimPanel.vue index 182154c..7093945 100644 --- a/src/components/AnimPanel.vue +++ b/src/components/AnimPanel.vue @@ -11,6 +11,7 @@ const props = defineProps<{ shape: string color: string ready?: boolean + active?: boolean }>() const groups = computed(() => { @@ -43,6 +44,7 @@ const groups = computed(() => { :state="id" :shape="shape" :color="color" + :active="active" @click="emit('pick', id)" />
diff --git a/src/components/BotTile.vue b/src/components/BotTile.vue index 89e9441..b773877 100644 --- a/src/components/BotTile.vue +++ b/src/components/BotTile.vue @@ -12,8 +12,9 @@ const props = withDefaults( size?: number interactive?: boolean showLabel?: boolean + active?: boolean }>(), - { interactive: true, showLabel: true } + { interactive: true, showLabel: true, active: true } ) const svg = ref(null) @@ -22,7 +23,6 @@ const painted = ref(false) async function paint() { const node = svg.value if (!node) return - node.innerHTML = '' const mount = await paintTile(node, { state: props.state ?? 'idle', shape: props.shape ?? 'blob', @@ -34,12 +34,27 @@ async function paint() { painted.value = !!mount } +const dirty = ref(false) + onMounted(() => { void paint() }) watch(() => [props.state, props.shape, props.color], () => { + if (!props.active) { + dirty.value = true + return + } void paint() }) +watch( + () => props.active, + (on) => { + if (on && dirty.value) { + dirty.value = false + void paint() + } + } +)