Dev #1

Merged
w-vandal merged 62 commits from dev into main 2026-08-26 04:56:44 +00:00
5 changed files with 351 additions and 450 deletions
Showing only changes of commit ea9f91fc19 - Show all commits
+19 -31
View File
@@ -114,7 +114,7 @@ body.objectEditBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden
.content-objectEdit .colLeft{ overflow-y:auto; height:100%; }
.content-objectEdit .colRight{ display:flex; flex-direction:column; min-height:0; height:100%; }
.content-objectEdit .colRight h2{ flex:0 0 auto; }
.content-objectEdit .colRight > .hint{ flex:0 0 auto; }
.content-objectEdit .colRight > .hint, .content-objectEdit .colRight > button{ flex:0 0 auto; }
.fieldsTableWrap{ flex:1 1 auto; min-height:0; overflow:auto; border:1px solid var(--border); border-radius:8px; }
.fieldsTable{ width:100%; border-collapse:collapse; font-size:12.5px; }
@@ -149,7 +149,6 @@ body.builderBody > main.content{ flex:1 1 auto; min-height:0; overflow:hidden; d
width:100%;
max-width:1600px;
}
.content-builder > .fieldRow:first-child{ flex:0 0 auto; }
/* 3 colonnes : ajout d'éléments (gauche) / écran en grand (centre) / propriétés (droite) —
toutes les trois occupent exactement la même hauteur (celle qui reste
@@ -375,37 +374,26 @@ code{ background:var(--panel2); border:1px solid var(--border); border-radius:4p
.floatPanelReopenBtn{ display:none !important; }
}
/* ---------- Logique de la scène : éditeur de flow à nœuds ---------- */
/* Panneau du bas, dans le même flux flexbox que .builder3 (pas une
surcouche/overlay) : replié, il ne prend que la hauteur de son en-tête et
.builder3 récupère tout le reste de l'espace ; ouvert, sa hauteur est
fixée en pixels (réglable à la souris via la poignée juste au-dessus) et
.builder3 rétrécit d'autant automatiquement — jamais l'un par-dessus
l'autre. */
.flowPanel{
flex:0 0 auto; border:1px solid var(--border); border-radius:10px; background:var(--panel);
margin-top:8px; display:flex; flex-direction:column; overflow:hidden;
/* ---------- Onglets du centre : Écran / Logique de la scène / Timeline
d'animation ---------- */
/* Un seul onglet visible à la fois (voir switchBuilderTab() dans
screen_edit.html), chacun occupant tout l'espace disponible dans
.builderCanvasArea — remplace l'ancien panneau du bas rétractable/
redimensionnable à la souris : un onglet "prend toute la place" par
nature, pas besoin d'une poignée de redimensionnement séparée, et en
changer d'onglet équivaut à "fermer" celui qu'on quitte. */
.builderTabs{ display:flex; gap:2px; flex:0 0 auto; border-bottom:1px solid var(--border); margin-bottom:10px; }
.builderTabBtn{
background:transparent; border:none; border-bottom:2px solid transparent; border-radius:0;
padding:8px 12px; color:var(--text-dim); font-size:12.5px; cursor:pointer;
}
.flowPanelHeader{
flex:0 0 auto; padding:10px 16px; font-weight:700; cursor:pointer; user-select:none;
display:flex; align-items:center; gap:6px;
}
.flowPanelHeader:hover{ background:var(--panel2); }
.flowToggleIcon{ color:var(--text-dim); font-size:11px; transition:transform .15s; display:inline-block; }
.flowPanel.open .flowToggleIcon{ transform:rotate(90deg); }
.flowPanelBody{ flex:1 1 auto; min-height:0; padding:0 16px 16px; display:none; flex-direction:column; }
.flowPanel.open .flowPanelBody{ display:flex; }
.flowToolbar{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:10px; flex:0 0 auto; }
.builderTabBtn:hover{ color:var(--text); }
.builderTabBtn.active{ color:var(--accent); border-bottom-color:var(--accent); font-weight:600; }
.flowResizeHandle{
flex:0 0 auto; height:10px; margin-top:6px; cursor:row-resize; position:relative; display:none;
}
.flowResizeHandle.visible{ display:block; }
.flowResizeHandle::before{
content:""; position:absolute; left:50%; top:3px; width:60px; height:4px;
background:var(--border); border-radius:2px; transform:translateX(-50%);
}
.flowResizeHandle:hover::before{ background:var(--accent); }
.builderTabPanel{ flex:1 1 auto; min-height:0; display:none; flex-direction:column; }
.builderTabPanel.active{ display:flex; }
.flowToolbar{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:10px; flex:0 0 auto; }
.nodeForm{ border:1px solid var(--border); border-radius:8px; padding:12px; margin-bottom:12px; background:var(--panel2); max-width:420px; flex:0 0 auto; }
.nodeForm label{ margin-top:8px; }
+1 -1
View File
@@ -24,10 +24,10 @@
<div class="colRight">
<h2>Champs</h2>
<button type="button" class="button is-outlined is-fullwidth mb-2" id="btnAddField">+ Ajouter un champ</button>
<div class="fieldsTableWrap" style="padding:10px;">
<div id="fieldsList"></div>
</div>
<button type="button" class="button is-outlined is-fullwidth mt-2" id="btnAddField">+ Ajouter un champ</button>
</div>
</div>
</form>
+323 -413
View File
@@ -12,16 +12,6 @@
<li class="is-active"><a>{{ screen.name }}</a></li>
{% endblock %}
{% block content %}
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<form method="post" action="{{ url_for('screen_rename', slug=game.slug, screen_id=screen.id) }}" class="field has-addons" style="align-items:center; margin:0;">
<div class="control"><input class="input" type="text" name="name" value="{{ screen.name }}" required style="font-size:16px; font-weight:700; width:auto;"></div>
<div class="control"><button type="submit" class="button is-small">Renommer</button></div>
</form>
{% if not screen.is_template %}
<a class="button is-small is-primary" href="{{ url_for('game_play', slug=game.slug) }}" target="_blank">▶️ Jouer depuis le début</a>
{% endif %}
</div>
{% if screen.is_template %}
<p class="hint" style="background:#1c2130; border:1px solid #2b3140; border-radius:8px; padding:10px 14px; margin:10px 0;">
🧩 Contenu d'un élément de jeu réutilisable.
@@ -67,6 +57,25 @@
<button type="button" class="button is-small" onclick="closeLeftPanel()" title="Fermer">✕</button>
</div>
<div class="floatPanelBody">
<!-- Nom de l'écran, aperçu jouable, et format de l'aperçu (Portrait/
Paysage/Carré) : regroupés ici plutôt qu'au-dessus du canevas —
ce sont des réglages qu'on touche rarement une fois l'écran en
cours de construction, pas besoin qu'ils prennent en permanence de
la place dans la zone centrale. -->
<form method="post" action="{{ url_for('screen_rename', slug=game.slug, screen_id=screen.id) }}" class="field has-addons" style="align-items:center; margin-bottom:8px;">
<div class="control is-expanded"><input class="input is-small" type="text" name="name" value="{{ screen.name }}" required></div>
<div class="control"><button type="submit" class="button is-small">Renommer</button></div>
</form>
{% if not screen.is_template %}
<a class="button is-small is-primary is-fullwidth mb-2" href="{{ url_for('game_play', slug=game.slug) }}" target="_blank">▶️ Jouer depuis le début</a>
{% endif %}
{% set current_aspect = screen.preview_aspect or '9/16' %}
<div class="buttons has-addons are-small mb-2" style="flex-wrap:nowrap;">
<button type="button" class="button smallBtn aspectBtn {{ 'active' if current_aspect == '9/16' else '' }}" data-aspect="9/16">📱 Portrait</button>
<button type="button" class="button smallBtn aspectBtn {{ 'active' if current_aspect == '16/9' else '' }}" data-aspect="16/9">🖥️ Paysage</button>
<button type="button" class="button smallBtn aspectBtn {{ 'active' if current_aspect == '1/1' else '' }}" data-aspect="1/1">◻️ Carré</button>
</div>
<hr style="border-color:var(--border); margin:10px 0;">
<h2 class="subtitle is-6">Éléments de cet écran</h2>
{% if top_level_elements %}
<div class="elementTree">
@@ -149,27 +158,299 @@
<button type="button" onclick="contextMenuDuplicate()">📄 Dupliquer</button>
</div>
<!-- Centre : l'écran, en très grand -->
<!-- Centre : l'écran (onglet "Écran"), ou la logique de la scène / la
timeline d'animation quand leur onglet est actif — un seul onglet
visible à la fois, chacun occupant tout l'espace disponible (voir
switchBuilderTab() plus bas). Absents sur un écran-modèle (élément
de jeu) : ses déclencheurs référenceraient des éléments dont l'id
change à chaque copie (voir instantiate_template_tree), donc une
logique/animation posée ici ne fonctionnerait plus une fois
l'élément réutilisé — seul l'onglet "Écran" a alors un sens. -->
<div class="builderCanvasArea">
{% set current_aspect = screen.preview_aspect or '9/16' %}
<div class="canvasToolbar">
<span class="hint">Aperçu :</span>
<button type="button" class="smallBtn aspectBtn {{ 'active' if current_aspect == '9/16' else '' }}" data-aspect="9/16">📱 Portrait</button>
<button type="button" class="smallBtn aspectBtn {{ 'active' if current_aspect == '16/9' else '' }}" data-aspect="16/9">🖥️ Paysage</button>
<button type="button" class="smallBtn aspectBtn {{ 'active' if current_aspect == '1/1' else '' }}" data-aspect="1/1">◻️ Carré</button>
{% if not screen.is_template %}
<div class="builderTabs" id="builderTabs">
<button type="button" class="builderTabBtn active" data-tab="screen" onclick="switchBuilderTab('screen')">🖥️ Écran</button>
<button type="button" class="builderTabBtn" data-tab="flow" onclick="switchBuilderTab('flow')">🔀 Logique de la scène</button>
<button type="button" class="builderTabBtn" data-tab="anim" onclick="switchBuilderTab('anim')">🎬 Timeline d'animation</button>
</div>
<div class="canvasFrame">
<div class="canvas" id="canvas" style="aspect-ratio:{{ current_aspect }};">
{% for el in top_level_elements %}
<div class="canvasElement {{ 'selected' if el.id == selected_id else '' }}" id="el-{{ el.id }}" style="{{ el|elstyle }}" data-anim-target="{{ el.id }}"
onmousedown="onElementMouseDown(event, {{ el.id }})">
<div class="canvasElementInner" style="{{ el|eltransform }}">{{ el.rendered_html|safe }}</div>
<div class="resizeHandle" onmousedown="onResizeMouseDown(event, {{ el.id }})"></div>
{% endif %}
<div class="builderTabPanel active" data-tab-panel="screen">
<div class="canvasFrame">
<div class="canvas" id="canvas" style="aspect-ratio:{{ current_aspect }};">
{% for el in top_level_elements %}
<div class="canvasElement {{ 'selected' if el.id == selected_id else '' }}" id="el-{{ el.id }}" style="{{ el|elstyle }}" data-anim-target="{{ el.id }}"
onmousedown="onElementMouseDown(event, {{ el.id }})">
<div class="canvasElementInner" style="{{ el|eltransform }}">{{ el.rendered_html|safe }}</div>
<div class="resizeHandle" onmousedown="onResizeMouseDown(event, {{ el.id }})"></div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
<p class="hint" style="text-align:center;">Glisse pour déplacer, tire le coin pour redimensionner, clique pour sélectionner.</p>
</div>
{% if not screen.is_template %}
<div class="builderTabPanel" id="flowTabPanel" data-tab-panel="flow">
<div class="flowToolbar">
<button type="button" class="smallBtn button is-small" onclick="openNodeForm('trigger')">➕ Déclencheur</button>
<button type="button" class="smallBtn button is-small" onclick="openNodeForm('condition')">➕ Condition</button>
<button type="button" class="smallBtn button is-small" onclick="openNodeForm('action')">➕ Action</button>
<span class="hint">Clique un point de sortie d'un nœud, puis le point d'entrée d'un autre nœud, pour les relier. Reclique une connexion pour la supprimer.</span>
</div>
<div id="nodeForm" class="nodeForm" style="display:none;">
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<strong id="nodeFormTitle">Nouveau nœud</strong>
<button type="button" class="smallBtn button is-small" onclick="closeNodeForm()">✕</button>
</div>
<div id="nodeFormTrigger" style="display:none;">
<label>Événement<select id="nf-trigger-event" onchange="toggleFlowTriggerFields()">
{% for key, label in trigger_events %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select></label>
<label id="nf-trigger-element-row">Élément<select id="nf-trigger-element">
{% for e2 in elements %}
<option value="{{ e2.id }}">{{ e2|elabel }} (elt-{{ e2.id }})</option>
{% endfor %}
</select></label>
<p class="hint" id="nf-trigger-repeater-hint" style="display:none;"></p>
<p class="hint" id="nf-trigger-affichage-hint" style="display:none;"></p>
</div>
<div id="nodeFormCondition" style="display:none;">
{% if not definitions %}
<p class="hint">Aucun objet défini dans ce jeu pour l'instant — <a href="{{ url_for('object_new', slug=game.slug) }}">crées-en un</a>.</p>
{% else %}
<label>Objet<select id="nf-cond-definition" onchange="onFlowDefinitionChange('cond')">
{% for d in definitions %}
<option value="{{ d.id }}">{{ d.name }}</option>
{% endfor %}
</select></label>
<label>Ligne concernée<select id="nf-cond-row"></select></label>
<label>Champ<select id="nf-cond-field" onchange="onFlowFieldChange()"></select></label>
<label>Condition<select id="nf-cond-operator">
{% for key, label in condition_operators %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select></label>
<label>Valeur de comparaison<input type="text" id="nf-cond-value" placeholder="ex. 1, vrai, 10..."></label>
<div id="nf-cond-extra-wrap">
<label id="nf-cond-combinator-row" style="display:none;">Combiner les conditions avec
<select id="nf-cond-combinator">
<option value="et">ET (toutes les conditions doivent être vraies)</option>
<option value="ou">OU (au moins une condition doit être vraie)</option>
</select>
</label>
<div id="nf-cond-extra-clauses"></div>
<button type="button" class="smallBtn button is-small" onclick="addFlowConditionClause()">+ Ajouter une condition</button>
</div>
{% endif %}
</div>
<div id="nodeFormAction" style="display:none;">
<label>Type d'action<select id="nf-action-type" onchange="toggleFlowActionFields()">
{% for key, label in action_type_labels.items() %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select></label>
<select id="nf-action-target-screen" style="display:none;">
{% for s in all_screens %}
<option value="{{ s.id }}">{{ s.name }}</option>
{% endfor %}
</select>
<div id="nf-action-element-fields" style="display:none;">
<select id="nf-action-target-element">
{% for e2 in elements %}
<option value="{{ e2.id }}">{{ e2|elabel }} (elt-{{ e2.id }})</option>
{% endfor %}
</select>
<p class="hint" id="nf-action-onglet-hint" style="display:none;"></p>
<div id="nf-action-element-property-wrap">
<select id="nf-action-element-property" onchange="toggleFlowElementPropertyValue()" style="margin-top:6px;">
{% for key, label in element_action_properties %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<select id="nf-action-element-value-visibility" style="margin-top:6px;">
{% for key, label in element_visibility_values %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<select id="nf-action-element-value-surbrillance" style="display:none; margin-top:6px;">
{% for key, label in element_surbrillance_values %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<select id="nf-action-element-value-desactive" style="display:none; margin-top:6px;">
{% for key, label in element_desactive_values %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<input type="color" id="nf-action-element-value-color" style="display:none; margin-top:6px;" value="#5b8cff">
<input type="number" id="nf-action-element-value-number" style="display:none; margin-top:6px;" value="50" placeholder="Nouvelle valeur en %">
<input type="text" id="nf-action-element-value-text" style="display:none; margin-top:6px;" placeholder="Nouveau texte affiché">
<label id="nf-action-element-toggle-row" style="display:flex; align-items:center; gap:6px; margin-top:10px; font-size:12px; color:var(--text-dim);">
<input type="checkbox" id="nf-action-element-toggle" onchange="toggleFlowElementToggleMode()" style="width:auto; margin:0;">
Va-et-vient : au 1er clic, passer à cette valeur ; au 2e clic, revenir à la précédente ; etc.
</label>
<div id="nf-action-element-value2-wrap" style="display:none; margin-top:6px;">
<input type="color" id="nf-action-element-value2-color" style="display:none;" value="#ff0000">
<input type="number" id="nf-action-element-value2-number" style="display:none;" placeholder="Valeur au 2e état en %">
</div>
</div>
</div>
<div id="nf-action-wait-fields" style="display:none;">
<label>Délai en secondes avant de continuer
<input type="number" id="nf-action-wait-seconds" min="0" step="0.5" value="3" placeholder="ex. 3">
</label>
</div>
<div id="nf-action-data-fields" style="display:none;">
{% if not definitions %}
<p class="hint">Aucun objet défini dans ce jeu pour l'instant — <a href="{{ url_for('object_new', slug=game.slug) }}">crées-en un</a>.</p>
{% else %}
<select id="nf-action-data-definition" onchange="onFlowDefinitionChange('action')">
{% for d in definitions %}
<option value="{{ d.id }}">{{ d.name }}</option>
{% endfor %}
</select>
<select id="nf-action-data-row" style="margin-top:6px;"></select>
<select id="nf-action-data-field" style="margin-top:6px;"></select>
<select id="nf-action-data-operation" style="margin-top:6px;">
{% for key, label in data_operations %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<input type="text" id="nf-action-data-value" style="margin-top:6px;" placeholder="Valeur / montant">
{% endif %}
</div>
<div id="nf-action-variable-fields" style="display:none;">
{% if not global_variables %}
<p class="hint">Aucune variable globale pour l'instant — crées-en une depuis "Condition de visibilité" dans les propriétés d'un élément.</p>
{% else %}
<select id="nf-action-variable-name">
{% for v in global_variables %}
<option value="{{ v.name }}">{{ v.name }}</option>
{% endfor %}
</select>
<select id="nf-action-variable-operation" style="margin-top:6px;">
{% for key, label in data_operations %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<input type="text" id="nf-action-variable-value" style="margin-top:6px;" placeholder="Valeur / montant">
{% endif %}
</div>
</div>
<div class="actionsRow"><button type="button" class="smallBtn button is-small is-primary" onclick="submitNodeForm()">+ Ajouter ce nœud</button></div>
</div>
<div class="flowCanvasWrap">
<svg id="flowEdges" class="flowEdgesSvg"></svg>
<div id="flowCanvas" class="flowCanvas"></div>
</div>
</div>
<p class="hint" style="text-align:center;">Glisse pour déplacer, tire le coin pour redimensionner, clique pour sélectionner.</p>
<div class="builderTabPanel" id="animTabPanel" data-tab-panel="anim">
<div class="flowToolbar">
<label style="display:flex; align-items:center; gap:6px;">
Piste
<select id="anim-new-track-element">
{% for e2 in elements %}
<option value="{{ e2.id }}">{{ e2|elabel }}</option>
{% endfor %}
</select>
</label>
<button type="button" class="smallBtn button is-small" onclick="addAnimTrack()">➕ Ajouter un clip</button>
</div>
<div id="animClipForm" class="nodeForm" style="display:none;">
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<strong id="animClipFormTitle">Clip d'animation</strong>
<button type="button" class="smallBtn button is-small" onclick="closeAnimClipForm()">✕</button>
</div>
<label>Type
<select id="anim-clip-kind" onchange="toggleAnimClipKind()">
<option value="animate_css">Animation Animate.css</option>
<option value="custom">Animation personnalisée — images-clés</option>
</select>
</label>
<div id="anim-clip-animate-css-fields">
<label>Animation
<select id="anim-clip-animate-name">
{% for category, items in animate_css_catalog %}
<optgroup label="{{ category }}">
{% for key, label in items %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</optgroup>
{% endfor %}
</select>
</label>
</div>
<div id="anim-clip-custom-fields" style="display:none;">
<div id="anim-clip-keyframes"></div>
<button type="button" class="smallBtn button is-small" onclick="addAnimKeyframeRow()">+ Ajouter une image-clé</button>
</div>
<div class="fieldRow">
<label style="flex:1;">Départ en secondes
<input type="number" id="anim-clip-start" min="0" step="0.1" value="0">
</label>
<label style="flex:1;">Durée en secondes
<input type="number" id="anim-clip-duration" min="0.1" step="0.1" value="1">
</label>
</div>
<div class="fieldRow">
<label style="flex:1;">Délai en secondes
<input type="number" id="anim-clip-delay" min="0" step="0.1" value="0">
</label>
<label style="flex:1;">Vitesse
<select id="anim-clip-easing">
<option value="ease">Progressive</option>
<option value="linear">Constante</option>
<option value="ease-in">Accélère</option>
<option value="ease-out">Ralentit</option>
<option value="ease-in-out">Accélère puis ralentit</option>
</select>
</label>
</div>
<label>Répétitions
<select id="anim-clip-iteration">
<option value="1">Une fois</option>
<option value="2">2 fois</option>
<option value="3">3 fois</option>
<option value="infinite">En boucle</option>
</select>
</label>
<div class="actionsRow">
<button type="button" class="smallBtn button is-small is-primary" onclick="saveAnimClipForm()">Enregistrer</button>
<button type="button" class="smallBtn button is-small" onclick="previewAnimClip()">▶ Aperçu</button>
<button type="button" class="smallBtn button is-small is-danger" onclick="deleteAnimClipForm()">🗑️ Supprimer</button>
</div>
</div>
<div class="animTimelineWrap">
<div id="animRuler" class="animRuler"></div>
<div id="animTracks" class="animTracks"></div>
</div>
</div>
{% endif %}
</div>
<!-- Panneau droit : propriétés de l'élément sélectionné — flottant,
@@ -468,298 +749,6 @@
</div>
</div>
{% if not screen.is_template %}
<!-- Poignée de redimensionnement : visible seulement quand le panneau du bas
est ouvert, on la tire à la souris pour régler sa hauteur. -->
<div class="flowResizeHandle" id="flowResizeHandle"></div>
<!-- Panneau bas, rétractable : logique de la scène (éditeur de flow à nœuds).
Volontairement PAS un <details> natif : sa hauteur ouverte doit pouvoir
être réglée à la souris (voir flowResizeHandle), ce qu'un <details> ne
permet pas nativement. Absent pour un écran-modèle (élément de jeu) :
ses déclencheurs référenceraient des éléments dont l'id change à chaque
copie (voir instantiate_template_tree), donc une logique posée ici ne
fonctionnerait plus une fois l'élément réutilisé. -->
<div class="flowPanel" id="flowPanel">
<div class="flowPanelHeader" onclick="toggleFlowPanel()">
<span class="flowToggleIcon">▸</span> 🔀 Logique de la scène — nœuds et connexions
</div>
<div class="flowPanelBody" id="flowPanelBody">
<div class="flowToolbar">
<button type="button" class="smallBtn button is-small" onclick="openNodeForm('trigger')">➕ Déclencheur</button>
<button type="button" class="smallBtn button is-small" onclick="openNodeForm('condition')">➕ Condition</button>
<button type="button" class="smallBtn button is-small" onclick="openNodeForm('action')">➕ Action</button>
<span class="hint">Clique un point de sortie d'un nœud, puis le point d'entrée d'un autre nœud, pour les relier. Reclique une connexion pour la supprimer.</span>
</div>
<div id="nodeForm" class="nodeForm" style="display:none;">
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<strong id="nodeFormTitle">Nouveau nœud</strong>
<button type="button" class="smallBtn button is-small" onclick="closeNodeForm()">✕</button>
</div>
<div id="nodeFormTrigger" style="display:none;">
<label>Événement<select id="nf-trigger-event" onchange="toggleFlowTriggerFields()">
{% for key, label in trigger_events %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select></label>
<label id="nf-trigger-element-row">Élément<select id="nf-trigger-element">
{% for e2 in elements %}
<option value="{{ e2.id }}">{{ e2|elabel }} (elt-{{ e2.id }})</option>
{% endfor %}
</select></label>
<p class="hint" id="nf-trigger-repeater-hint" style="display:none;"></p>
<p class="hint" id="nf-trigger-affichage-hint" style="display:none;"></p>
</div>
<div id="nodeFormCondition" style="display:none;">
{% if not definitions %}
<p class="hint">Aucun objet défini dans ce jeu pour l'instant — <a href="{{ url_for('object_new', slug=game.slug) }}">crées-en un</a>.</p>
{% else %}
<label>Objet<select id="nf-cond-definition" onchange="onFlowDefinitionChange('cond')">
{% for d in definitions %}
<option value="{{ d.id }}">{{ d.name }}</option>
{% endfor %}
</select></label>
<label>Ligne concernée<select id="nf-cond-row"></select></label>
<label>Champ<select id="nf-cond-field" onchange="onFlowFieldChange()"></select></label>
<label>Condition<select id="nf-cond-operator">
{% for key, label in condition_operators %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select></label>
<label>Valeur de comparaison<input type="text" id="nf-cond-value" placeholder="ex. 1, vrai, 10..."></label>
<div id="nf-cond-extra-wrap">
<label id="nf-cond-combinator-row" style="display:none;">Combiner les conditions avec
<select id="nf-cond-combinator">
<option value="et">ET (toutes les conditions doivent être vraies)</option>
<option value="ou">OU (au moins une condition doit être vraie)</option>
</select>
</label>
<div id="nf-cond-extra-clauses"></div>
<button type="button" class="smallBtn button is-small" onclick="addFlowConditionClause()">+ Ajouter une condition</button>
</div>
{% endif %}
</div>
<div id="nodeFormAction" style="display:none;">
<label>Type d'action<select id="nf-action-type" onchange="toggleFlowActionFields()">
{% for key, label in action_type_labels.items() %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select></label>
<select id="nf-action-target-screen" style="display:none;">
{% for s in all_screens %}
<option value="{{ s.id }}">{{ s.name }}</option>
{% endfor %}
</select>
<div id="nf-action-element-fields" style="display:none;">
<select id="nf-action-target-element">
{% for e2 in elements %}
<option value="{{ e2.id }}">{{ e2|elabel }} (elt-{{ e2.id }})</option>
{% endfor %}
</select>
<p class="hint" id="nf-action-onglet-hint" style="display:none;"></p>
<div id="nf-action-element-property-wrap">
<select id="nf-action-element-property" onchange="toggleFlowElementPropertyValue()" style="margin-top:6px;">
{% for key, label in element_action_properties %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<select id="nf-action-element-value-visibility" style="margin-top:6px;">
{% for key, label in element_visibility_values %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<select id="nf-action-element-value-surbrillance" style="display:none; margin-top:6px;">
{% for key, label in element_surbrillance_values %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<select id="nf-action-element-value-desactive" style="display:none; margin-top:6px;">
{% for key, label in element_desactive_values %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<input type="color" id="nf-action-element-value-color" style="display:none; margin-top:6px;" value="#5b8cff">
<input type="number" id="nf-action-element-value-number" style="display:none; margin-top:6px;" value="50" placeholder="Nouvelle valeur en %">
<input type="text" id="nf-action-element-value-text" style="display:none; margin-top:6px;" placeholder="Nouveau texte affiché">
<label id="nf-action-element-toggle-row" style="display:flex; align-items:center; gap:6px; margin-top:10px; font-size:12px; color:var(--text-dim);">
<input type="checkbox" id="nf-action-element-toggle" onchange="toggleFlowElementToggleMode()" style="width:auto; margin:0;">
Va-et-vient : au 1er clic, passer à cette valeur ; au 2e clic, revenir à la précédente ; etc.
</label>
<div id="nf-action-element-value2-wrap" style="display:none; margin-top:6px;">
<input type="color" id="nf-action-element-value2-color" style="display:none;" value="#ff0000">
<input type="number" id="nf-action-element-value2-number" style="display:none;" placeholder="Valeur au 2e état en %">
</div>
</div>
</div>
<div id="nf-action-wait-fields" style="display:none;">
<label>Délai en secondes avant de continuer
<input type="number" id="nf-action-wait-seconds" min="0" step="0.5" value="3" placeholder="ex. 3">
</label>
</div>
<div id="nf-action-data-fields" style="display:none;">
{% if not definitions %}
<p class="hint">Aucun objet défini dans ce jeu pour l'instant — <a href="{{ url_for('object_new', slug=game.slug) }}">crées-en un</a>.</p>
{% else %}
<select id="nf-action-data-definition" onchange="onFlowDefinitionChange('action')">
{% for d in definitions %}
<option value="{{ d.id }}">{{ d.name }}</option>
{% endfor %}
</select>
<select id="nf-action-data-row" style="margin-top:6px;"></select>
<select id="nf-action-data-field" style="margin-top:6px;"></select>
<select id="nf-action-data-operation" style="margin-top:6px;">
{% for key, label in data_operations %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<input type="text" id="nf-action-data-value" style="margin-top:6px;" placeholder="Valeur / montant">
{% endif %}
</div>
<div id="nf-action-variable-fields" style="display:none;">
{% if not global_variables %}
<p class="hint">Aucune variable globale pour l'instant — crées-en une depuis "Condition de visibilité" dans les propriétés d'un élément.</p>
{% else %}
<select id="nf-action-variable-name">
{% for v in global_variables %}
<option value="{{ v.name }}">{{ v.name }}</option>
{% endfor %}
</select>
<select id="nf-action-variable-operation" style="margin-top:6px;">
{% for key, label in data_operations %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</select>
<input type="text" id="nf-action-variable-value" style="margin-top:6px;" placeholder="Valeur / montant">
{% endif %}
</div>
</div>
<div class="actionsRow"><button type="button" class="smallBtn button is-small is-primary" onclick="submitNodeForm()">+ Ajouter ce nœud</button></div>
</div>
<div class="flowCanvasWrap">
<svg id="flowEdges" class="flowEdgesSvg"></svg>
<div id="flowCanvas" class="flowCanvas"></div>
</div>
</div>
</div>
<!-- Poignée de redimensionnement du panneau timeline, même principe que
flowResizeHandle ci-dessus. -->
<div class="flowResizeHandle" id="animResizeHandle"></div>
<!-- Panneau bas, rétractable, positionné sous "Logique de la scène" :
timeline d'animation. Une piste par élément animé, des clips posés sur
un axe du temps (secondes depuis l'affichage de l'écran) — glisse un
clip pour changer son départ, tire son bord droit pour changer sa
durée. Chaque clip est soit une animation de la bibliothèque
Animate.css, soit une animation personnalisée (images-clés propres à
ce jeu). -->
<div class="flowPanel" id="animPanel">
<div class="flowPanelHeader" onclick="toggleAnimPanel()">
<span class="flowToggleIcon">▸</span> 🎬 Timeline d'animation
</div>
<div class="flowPanelBody" id="animPanelBody">
<div class="flowToolbar">
<label style="display:flex; align-items:center; gap:6px;">
Piste
<select id="anim-new-track-element">
{% for e2 in elements %}
<option value="{{ e2.id }}">{{ e2|elabel }}</option>
{% endfor %}
</select>
</label>
<button type="button" class="smallBtn button is-small" onclick="addAnimTrack()">➕ Ajouter un clip</button>
</div>
<div id="animClipForm" class="nodeForm" style="display:none;">
<div class="fieldRow" style="align-items:center; justify-content:space-between;">
<strong id="animClipFormTitle">Clip d'animation</strong>
<button type="button" class="smallBtn button is-small" onclick="closeAnimClipForm()">✕</button>
</div>
<label>Type
<select id="anim-clip-kind" onchange="toggleAnimClipKind()">
<option value="animate_css">Animation Animate.css</option>
<option value="custom">Animation personnalisée — images-clés</option>
</select>
</label>
<div id="anim-clip-animate-css-fields">
<label>Animation
<select id="anim-clip-animate-name">
{% for category, items in animate_css_catalog %}
<optgroup label="{{ category }}">
{% for key, label in items %}
<option value="{{ key }}">{{ label }}</option>
{% endfor %}
</optgroup>
{% endfor %}
</select>
</label>
</div>
<div id="anim-clip-custom-fields" style="display:none;">
<div id="anim-clip-keyframes"></div>
<button type="button" class="smallBtn button is-small" onclick="addAnimKeyframeRow()">+ Ajouter une image-clé</button>
</div>
<div class="fieldRow">
<label style="flex:1;">Départ en secondes
<input type="number" id="anim-clip-start" min="0" step="0.1" value="0">
</label>
<label style="flex:1;">Durée en secondes
<input type="number" id="anim-clip-duration" min="0.1" step="0.1" value="1">
</label>
</div>
<div class="fieldRow">
<label style="flex:1;">Délai en secondes
<input type="number" id="anim-clip-delay" min="0" step="0.1" value="0">
</label>
<label style="flex:1;">Vitesse
<select id="anim-clip-easing">
<option value="ease">Progressive</option>
<option value="linear">Constante</option>
<option value="ease-in">Accélère</option>
<option value="ease-out">Ralentit</option>
<option value="ease-in-out">Accélère puis ralentit</option>
</select>
</label>
</div>
<label>Répétitions
<select id="anim-clip-iteration">
<option value="1">Une fois</option>
<option value="2">2 fois</option>
<option value="3">3 fois</option>
<option value="infinite">En boucle</option>
</select>
</label>
<div class="actionsRow">
<button type="button" class="smallBtn button is-small is-primary" onclick="saveAnimClipForm()">Enregistrer</button>
<button type="button" class="smallBtn button is-small" onclick="previewAnimClip()">▶ Aperçu</button>
<button type="button" class="smallBtn button is-small is-danger" onclick="deleteAnimClipForm()">🗑️ Supprimer</button>
</div>
</div>
<div class="animTimelineWrap">
<div id="animRuler" class="animRuler"></div>
<div id="animTracks" class="animTracks"></div>
</div>
</div>
</div>
{% endif %}
<script>
function saveGeometry(id, x, y, width, height) {
@@ -2251,58 +2240,24 @@ function submitNodeForm() {
}
// Panneau de logique de scène absent sur un écran-modèle (élément de jeu) —
// voir le commentaire Jinja au-dessus de la div flowPanel.
var HAS_FLOW_PANEL = !!document.getElementById('flowPanel');
// voir le commentaire Jinja au-dessus de .builderTabs.
var HAS_FLOW_PANEL = !!document.getElementById('flowTabPanel');
if (HAS_FLOW_PANEL) { renderFlow(); }
// ---------- Ouverture / fermeture et redimensionnement du panneau du bas ----------
var FLOW_PANEL_DEFAULT_HEIGHT = 340;
var flowPanelLastHeight = FLOW_PANEL_DEFAULT_HEIGHT;
function toggleFlowPanel() {
var panel = document.getElementById('flowPanel');
var handle = document.getElementById('flowResizeHandle');
if (!panel || !handle) return;
var isOpen = panel.classList.contains('open');
if (isOpen) {
flowPanelLastHeight = panel.getBoundingClientRect().height;
panel.classList.remove('open');
panel.style.height = '';
handle.classList.remove('visible');
} else {
panel.classList.add('open');
panel.style.height = flowPanelLastHeight + 'px';
handle.classList.add('visible');
requestAnimationFrame(drawFlowEdges);
}
}
if (HAS_FLOW_PANEL) (function(){
var handle = document.getElementById('flowResizeHandle');
var panel = document.getElementById('flowPanel');
handle.addEventListener('mousedown', function(e){
e.preventDefault();
if (!panel.classList.contains('open')) return;
var startY = e.clientY;
var startHeight = panel.getBoundingClientRect().height;
var wrap = document.querySelector('.content-builder');
var maxHeight = Math.max(160, wrap.getBoundingClientRect().height - 160);
function onMove(ev) {
var dy = ev.clientY - startY;
var newHeight = Math.min(maxHeight, Math.max(140, startHeight - dy));
panel.style.height = newHeight + 'px';
flowPanelLastHeight = newHeight;
drawFlowEdges();
}
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
// ---------- Onglets du centre : Écran / Logique de la scène / Timeline
// d'animation — un seul visible à la fois, chacun occupant tout l'espace
// disponible (remplace l'ancien panneau du bas rétractable/redimensionnable
// à la souris : un onglet "prend toute la place" par nature, et en changer
// équivaut à "fermer" celui qu'on quitte). ----------
function switchBuilderTab(tab) {
document.querySelectorAll('.builderTabBtn').forEach(function (btn) {
btn.classList.toggle('active', btn.dataset.tab === tab);
});
})();
document.querySelectorAll('.builderTabPanel').forEach(function (panel) {
panel.classList.toggle('active', panel.dataset.tabPanel === tab);
});
if (tab === 'flow') requestAnimationFrame(drawFlowEdges);
}
// ---------- Timeline d'animation : piste par élément, clips Animate.css ou
// personnalisés (images-clés), glissés/redimensionnés à la souris sur un
@@ -2617,53 +2572,8 @@ function previewAnimClip() {
}
// Panneau timeline absent sur un écran-modèle (élément de jeu), comme le
// panneau de logique — voir le commentaire Jinja au-dessus de la div animPanel.
var HAS_ANIM_PANEL = !!document.getElementById('animPanel');
// panneau de logique — voir le commentaire Jinja au-dessus de .builderTabs.
var HAS_ANIM_PANEL = !!document.getElementById('animTabPanel');
if (HAS_ANIM_PANEL) { renderAnimTimeline(); }
var ANIM_PANEL_DEFAULT_HEIGHT = 260;
var animPanelLastHeight = ANIM_PANEL_DEFAULT_HEIGHT;
function toggleAnimPanel() {
var panel = document.getElementById('animPanel');
var handle = document.getElementById('animResizeHandle');
if (!panel || !handle) return;
var isOpen = panel.classList.contains('open');
if (isOpen) {
animPanelLastHeight = panel.getBoundingClientRect().height;
panel.classList.remove('open');
panel.style.height = '';
handle.classList.remove('visible');
} else {
panel.classList.add('open');
panel.style.height = animPanelLastHeight + 'px';
handle.classList.add('visible');
}
}
if (HAS_ANIM_PANEL) (function() {
var handle = document.getElementById('animResizeHandle');
var panel = document.getElementById('animPanel');
handle.addEventListener('mousedown', function(e) {
e.preventDefault();
if (!panel.classList.contains('open')) return;
var startY = e.clientY;
var startHeight = panel.getBoundingClientRect().height;
var wrap = document.querySelector('.content-builder');
var maxHeight = Math.max(160, wrap.getBoundingClientRect().height - 160);
function onMove(ev) {
var dy = ev.clientY - startY;
var newHeight = Math.min(maxHeight, Math.max(140, startHeight - dy));
panel.style.height = newHeight + 'px';
animPanelLastHeight = newHeight;
}
function onUp() {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
}
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
});
})();
</script>
{% endblock %}
+1 -1
View File
@@ -89,7 +89,7 @@ def test_clip_delete_removes_it(client, game):
def test_screen_edit_exposes_timeline_panel_and_animate_css_cdn(client, game):
screen_id = _create_screen(client, game)
html = client.get(f"/game/{game}/screens/{screen_id}/edit").data.decode()
assert "animPanel" in html
assert "animTabPanel" in html
assert "Timeline d'animation" in html
assert "animate.css" in html
+7 -4
View File
@@ -20,10 +20,13 @@ def _create_onglets(client, slug, screen_id):
def _tab_rename_ids(html):
"""Ids des formulaires de renommage DANS le bloc "Onglets" du panneau de
propriétés — pas le premier /elements/<id>/rename de la page, qui est le
champ "Nommer" générique de l'élément actuellement sélectionné (pas
forcément un onglet, ex. l'élément Onglets lui-même)."""
block = html.split("Onglets (", 1)[-1]
propriétés — pas le premier /elements/<id>/rename de la page (le champ
"Nommer" générique de l'élément actuellement sélectionné), ni une
occurrence de "Onglets (" dans une liste déroulante d'éléments de la
Logique de la scène (ex. "<option>Onglets (elt-1)</option>"), qui existe
aussi sur la page — seul l'en-tête du groupe de réglages ("<summary>🗂️
Onglets (") repère sans ambiguïté le bloc de propriétés lui-même."""
block = html.split(">🗂️ Onglets (", 1)[-1]
return [int(x) for x in re.findall(r"/elements/(\d+)/rename", block)]