[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]; } }