From b319de256a3c0ac2fd3c048e3391c06cac2165a6 Mon Sep 17 00:00:00 2001 From: colorlink Date: Fri, 16 Oct 2026 01:27:36 +0000 Subject: [PATCH] =?UTF-8?q?feat(batch11):=20motion/GSAP=20layer=20?= =?UTF-8?q?=E2=80=94=20Elementor=20entrance=20animations=20+=20scroll=20mo?= =?UTF-8?q?tion-FX=20presets?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01Q8t9GEyyLZbUQu26na1JQg --- includes/Extensions/Batch11.php | 216 ++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 includes/Extensions/Batch11.php diff --git a/includes/Extensions/Batch11.php b/includes/Extensions/Batch11.php new file mode 100644 index 0000000..698ae20 --- /dev/null +++ b/includes/Extensions/Batch11.php @@ -0,0 +1,216 @@ + [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]; + } +}