Files

217 lines
9.9 KiB
PHP

<?php
/**
* Batch 11 — Motion / GSAP-style animation (Pro-equivalent, net-new).
*
* Entrance animations and scroll motion-FX for Elementor elements, plus named
* preset bundles. Operates on the post's `_elementor_data`. Guarded by
* ELEMENTOR_VERSION — inert where Elementor is absent.
* GPL-2.0-or-later. See NOTICE.md.
*/
namespace AskTamMCP\Extensions;
defined('ABSPATH') || exit;
class Batch11 {
/** Entrance animations Elementor ships (subset, stable names). */
private const ENTRANCES = [
'fadeIn', 'fadeInDown', 'fadeInLeft', 'fadeInRight', 'fadeInUp',
'zoomIn', 'zoomInUp', 'bounceIn', 'slideInUp', 'slideInDown',
'slideInLeft', 'slideInRight', 'flipInX', 'flipInY', 'lightSpeedIn',
];
/** Preset bundles: name => [entrance, delay_step_ms, scroll_fx]. */
private const PRESETS = [
'reveal-up' => ['animation' => 'fadeInUp', 'step' => 150, 'scroll' => null],
'reveal-fade' => ['animation' => 'fadeIn', 'step' => 120, 'scroll' => null],
'zoom-in' => ['animation' => 'zoomIn', 'step' => 150, 'scroll' => null],
'stagger-left' => ['animation' => 'fadeInLeft', 'step' => 180, 'scroll' => null],
'parallax-soft' => ['animation' => null, 'step' => 0, 'scroll' => 'translateY'],
'fade-on-scroll' => ['animation' => null, 'step' => 0, 'scroll' => 'opacity'],
];
public static function register($registry): void {
if (!defined('ELEMENTOR_VERSION')) return;
$def = function (string $name, string $desc, array $props, array $required, string $scope, string $method) {
$schema = ['type' => 'object', 'properties' => $props];
if ($required) $schema['required'] = $required;
return ['name' => $name, 'description' => $desc, 'scope' => $scope,
'handler' => 'AskTamMCP\\Extensions\\Batch11::' . $method, 'inputSchema' => $schema];
};
$registry->register($def('motion_list_presets', 'List available entrance animations, scroll motion-FX effects, and named preset bundles.',
[], [], 'read', 'listPresets'));
$registry->register($def('motion_set_entrance', 'Set an Elementor entrance animation (and optional delay in ms) on one element by its id within a post.',
['post_id' => ['type' => 'integer'], 'element_id' => ['type' => 'string'],
'animation' => ['type' => 'string'], 'delay_ms' => ['type' => 'integer', 'default' => 0]],
['post_id', 'element_id', 'animation'], 'write', 'setEntrance'));
$registry->register($def('motion_set_scroll_fx', 'Apply a scroll motion-FX (translateY parallax or opacity fade) to one Elementor element by id.',
['post_id' => ['type' => 'integer'], 'element_id' => ['type' => 'string'],
'effect' => ['type' => 'string', 'enum' => ['translateY', 'opacity']],
'speed' => ['type' => 'integer', 'default' => 2]],
['post_id', 'element_id', 'effect'], 'write', 'setScrollFx'));
$registry->register($def('motion_apply_preset', 'Apply a named motion preset bundle to all top-level elements of an Elementor post (staggered).',
['post_id' => ['type' => 'integer'], 'preset' => ['type' => 'string']],
['post_id', 'preset'], 'write', 'applyPreset'));
$registry->register($def('motion_clear', 'Remove entrance animation and scroll motion-FX settings from one element (or all top-level elements if element_id omitted).',
['post_id' => ['type' => 'integer'], 'element_id' => ['type' => 'string']],
['post_id'], 'write', 'clearMotion'));
}
private static function loadData(int $postId): array {
$raw = get_post_meta($postId, '_elementor_data', true);
if (empty($raw)) throw new \RuntimeException('No Elementor data on post ' . $postId);
$data = is_string($raw) ? json_decode($raw, true) : $raw;
if (!is_array($data)) throw new \RuntimeException('Elementor data not decodable on post ' . $postId);
return $data;
}
private static function saveData(int $postId, array $data): void {
update_post_meta($postId, '_elementor_data', wp_slash(wp_json_encode($data)));
if (class_exists('\Elementor\Plugin')) {
$css = \Elementor\Core\Files\CSS\Post::create($postId);
$css->update();
}
}
/** Recursively find & mutate one element's settings via callback. Returns true if hit. */
private static function walk(array &$elements, string $id, callable $cb): bool {
foreach ($elements as &$el) {
if (isset($el['id']) && $el['id'] === $id) {
if (!isset($el['settings']) || !is_array($el['settings'])) $el['settings'] = [];
$cb($el['settings']);
return true;
}
if (!empty($el['elements']) && is_array($el['elements'])) {
if (self::walk($el['elements'], $id, $cb)) return true;
}
}
return false;
}
private static function entranceMutator(string $anim, int $delay): callable {
return function (array &$s) use ($anim, $delay) {
$s['_animation'] = $anim;
if ($delay > 0) $s['_animation_delay'] = $delay;
};
}
private static function scrollMutator(string $effect, int $speed): callable {
return function (array &$s) use ($effect, $speed) {
$s['motion_fx_motion_fx_scrolling'] = 'yes';
if ($effect === 'translateY') {
$s['motion_fx_translateY_effect'] = 'yes';
$s['motion_fx_translateY_speed'] = ['unit' => 'px', 'size' => max(1, $speed)];
$s['motion_fx_translateY_direction'] = 'negative';
} else {
$s['motion_fx_opacity_effect'] = 'yes';
$s['motion_fx_opacity_level'] = ['unit' => 'px', 'size' => 10];
$s['motion_fx_opacity_range'] = ['unit' => '%', 'sizes' => ['start' => 0, 'end' => 20]];
}
};
}
private static function clearMutator(): callable {
return function (array &$s) {
foreach (array_keys($s) as $k) {
if ($k === '_animation' || $k === '_animation_delay' || strpos($k, 'motion_fx_') === 0) {
unset($s[$k]);
}
}
};
}
public static function listPresets(array $args): array {
return [
'entrances' => self::ENTRANCES,
'scroll_fx' => ['translateY', 'opacity'],
'presets' => array_keys(self::PRESETS),
];
}
public static function setEntrance(array $args): array {
$postId = (int) ($args['post_id'] ?? 0);
$id = (string) ($args['element_id'] ?? '');
$anim = (string) ($args['animation'] ?? '');
$delay = (int) ($args['delay_ms'] ?? 0);
if (!in_array($anim, self::ENTRANCES, true)) {
throw new \RuntimeException('Unknown animation. Use motion_list_presets.');
}
$data = self::loadData($postId);
$hit = self::walk($data, $id, self::entranceMutator($anim, $delay));
if (!$hit) throw new \RuntimeException('Element id not found: ' . $id);
self::saveData($postId, $data);
return ['post_id' => $postId, 'element_id' => $id, 'animation' => $anim, 'delay_ms' => $delay, 'applied' => true];
}
public static function setScrollFx(array $args): array {
$postId = (int) ($args['post_id'] ?? 0);
$id = (string) ($args['element_id'] ?? '');
$effect = (string) ($args['effect'] ?? '');
$speed = (int) ($args['speed'] ?? 2);
if (!in_array($effect, ['translateY', 'opacity'], true)) {
throw new \RuntimeException('effect must be translateY or opacity.');
}
$data = self::loadData($postId);
$hit = self::walk($data, $id, self::scrollMutator($effect, $speed));
if (!$hit) throw new \RuntimeException('Element id not found: ' . $id);
self::saveData($postId, $data);
return ['post_id' => $postId, 'element_id' => $id, 'effect' => $effect, 'speed' => $speed, 'applied' => true];
}
public static function applyPreset(array $args): array {
$postId = (int) ($args['post_id'] ?? 0);
$preset = (string) ($args['preset'] ?? '');
if (!isset(self::PRESETS[$preset])) {
throw new \RuntimeException('Unknown preset. Use motion_list_presets.');
}
$p = self::PRESETS[$preset];
$data = self::loadData($postId);
$touched = 0;
$i = 0;
foreach ($data as &$el) {
if (!isset($el['settings']) || !is_array($el['settings'])) $el['settings'] = [];
if (!empty($p['animation'])) {
$delay = (int) $p['step'] * $i;
$m = self::entranceMutator($p['animation'], $delay);
$m($el['settings']);
}
if (!empty($p['scroll'])) {
$m = self::scrollMutator($p['scroll'], 2);
$m($el['settings']);
}
$touched++;
$i++;
}
unset($el);
self::saveData($postId, $data);
return ['post_id' => $postId, 'preset' => $preset, 'elements_touched' => $touched];
}
public static function clearMotion(array $args): array {
$postId = (int) ($args['post_id'] ?? 0);
$id = isset($args['element_id']) ? (string) $args['element_id'] : '';
$data = self::loadData($postId);
if ($id !== '') {
$hit = self::walk($data, $id, self::clearMutator());
if (!$hit) throw new \RuntimeException('Element id not found: ' . $id);
$count = 1;
} else {
$m = self::clearMutator();
$count = 0;
foreach ($data as &$el) {
if (isset($el['settings']) && is_array($el['settings'])) { $m($el['settings']); $count++; }
}
unset($el);
}
self::saveData($postId, $data);
return ['post_id' => $postId, 'cleared' => $count];
}
}