b319de256a
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q8t9GEyyLZbUQu26na1JQg
217 lines
9.9 KiB
PHP
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];
|
|
}
|
|
}
|