SceneItem.ts

  1. import Animator, { isDirectionReverse } from "./Animator";
  2. import Frame from "./Frame";
  3. import {
  4. toFixed,
  5. isFixed,
  6. playCSS,
  7. toId,
  8. exportCSS,
  9. getRealId,
  10. makeId,
  11. isPausedCSS,
  12. isRole,
  13. isInProperties,
  14. getValueByNames,
  15. isEndedCSS,
  16. setPlayCSS,
  17. getNames,
  18. updateFrame,
  19. } from "./utils";
  20. import { dotValue } from "./utils/dot";
  21. import {
  22. START_ANIMATION,
  23. PREFIX, THRESHOLD,
  24. TIMING_FUNCTION, ALTERNATE, ALTERNATE_REVERSE, INFINITE,
  25. REVERSE, EASING, FILL_MODE, DIRECTION, ITERATION_COUNT,
  26. EASING_NAME, DELAY, PLAY_SPEED, DURATION, PAUSE_ANIMATION, DATA_SCENE_ID, SELECTOR, ROLES, CURRENT_TIME
  27. } from "./consts";
  28. import {
  29. isObject, isArray, isUndefined, decamelize,
  30. ANIMATION, fromCSS, addClass, removeClass, hasClass,
  31. KEYFRAMES, requestAnimationFrame, isFunction,
  32. IObject, $, splitComma, toArray, isString, IArrayFormat,
  33. dot as dotNumber,
  34. find,
  35. findIndex,
  36. } from "@daybrush/utils";
  37. import {
  38. NameType, RoleObject, AnimateElement, AnimatorState,
  39. SceneItemState, SceneItemOptions, EasingType, PlayCondition, DirectionType
  40. } from "./types";
  41. function getNearTimeIndex(times: number[], time: number) {
  42. const length = times.length;
  43. for (let i = 0; i < length; ++i) {
  44. if (times[i] === time) {
  45. return [i, i];
  46. } else if (times[i] > time) {
  47. return [i > 0 ? i - 1 : 0, i];
  48. }
  49. }
  50. return [length - 1, length - 1];
  51. }
  52. function makeAnimationProperties(properties: object) {
  53. const cssArray = [];
  54. for (const name in properties) {
  55. cssArray.push(`${ANIMATION}-${decamelize(name)}:${properties[name]};`);
  56. }
  57. return cssArray.join("");
  58. }
  59. function addTime(times: number[], time: number) {
  60. const length = times.length;
  61. for (let i = 0; i < length; ++i) {
  62. if (time < times[i]) {
  63. times.splice(i, 0, time);
  64. return;
  65. }
  66. }
  67. times[length] = time;
  68. }
  69. function addEntry(entries: number[][], time: number, keytime: number) {
  70. const prevEntry = entries[entries.length - 1];
  71. (!prevEntry || prevEntry[0] !== time || prevEntry[1] !== keytime) &&
  72. entries.push([toFixed(time), toFixed(keytime)]);
  73. }
  74. export function getEntries(times: number[], states: AnimatorState[]) {
  75. let entries = times.map(time => ([time, time]));
  76. let nextEntries = [];
  77. states.forEach(state => {
  78. const iterationCount = state[ITERATION_COUNT] as number;
  79. const delay = state[DELAY];
  80. const playSpeed = state[PLAY_SPEED];
  81. const direction = state[DIRECTION];
  82. const intCount = Math.ceil(iterationCount);
  83. const currentDuration = entries[entries.length - 1][0];
  84. const length = entries.length;
  85. const lastTime = currentDuration * iterationCount;
  86. for (let i = 0; i < intCount; ++i) {
  87. const isReverse =
  88. direction === REVERSE ||
  89. direction === ALTERNATE && i % 2 ||
  90. direction === ALTERNATE_REVERSE && !(i % 2);
  91. for (let j = 0; j < length; ++j) {
  92. const entry = entries[isReverse ? length - j - 1 : j];
  93. const time = entry[1];
  94. const currentTime = currentDuration * i + (isReverse ? currentDuration - entry[0] : entry[0]);
  95. const prevEntry = entries[isReverse ? length - j : j - 1];
  96. if (currentTime > lastTime) {
  97. if (j !== 0) {
  98. const prevTime = currentDuration * i +
  99. (isReverse ? currentDuration - prevEntry[0] : prevEntry[0]);
  100. const divideTime = dotNumber(prevEntry[1], time, lastTime - prevTime, currentTime - lastTime);
  101. addEntry(nextEntries, (delay + currentDuration * iterationCount) / playSpeed, divideTime);
  102. }
  103. break;
  104. } else if (
  105. currentTime === lastTime
  106. && nextEntries.length
  107. && nextEntries[nextEntries.length - 1][0] === lastTime + delay
  108. ) {
  109. break;
  110. }
  111. addEntry(nextEntries, (delay + currentTime) / playSpeed, time);
  112. }
  113. }
  114. // delay time
  115. delay && nextEntries.unshift([0, nextEntries[0][1]]);
  116. entries = nextEntries;
  117. nextEntries = [];
  118. });
  119. return entries;
  120. }
  121. /**
  122. * manage Frame Keyframes and play keyframes.
  123. * @extends Animator
  124. * @example
  125. const item = new SceneItem({
  126. 0: {
  127. display: "none",
  128. },
  129. 1: {
  130. display: "block",
  131. opacity: 0,
  132. },
  133. 2: {
  134. opacity: 1,
  135. }
  136. });
  137. */
  138. class SceneItem extends Animator<SceneItemOptions, SceneItemState> {
  139. public times: number[] = [];
  140. public items: IObject<Frame> = {};
  141. public names: RoleObject = {};
  142. public elements: AnimateElement[] = [];
  143. public temp: Frame;
  144. private needUpdate: boolean = true;
  145. private target: any;
  146. private targetFunc: (frame: Frame) => void;
  147. /**
  148. * @param - properties
  149. * @param - options
  150. * @example
  151. const item = new SceneItem({
  152. 0: {
  153. display: "none",
  154. },
  155. 1: {
  156. display: "block",
  157. opacity: 0,
  158. },
  159. 2: {
  160. opacity: 1,
  161. }
  162. });
  163. */
  164. constructor(properties?: any, options?: Partial<SceneItemOptions>) {
  165. super();
  166. this.load(properties, options);
  167. }
  168. public getDuration() {
  169. const times = this.times;
  170. const length = times.length;
  171. return (length === 0 ? 0 : times[length - 1]) || this.state[DURATION];
  172. }
  173. /**
  174. * get size of list
  175. * @return {Number} length of list
  176. */
  177. public size() {
  178. return this.times.length;
  179. }
  180. public setDuration(duration: number) {
  181. if (!duration) {
  182. return this;
  183. }
  184. const originalDuration = this.getDuration();
  185. if (originalDuration > 0) {
  186. const ratio = duration / originalDuration;
  187. const { times, items } = this;
  188. const obj: IObject<Frame> = {};
  189. this.times = times.map(time => {
  190. const time2 = toFixed(time * ratio);
  191. obj[time2] = items[time];
  192. return time2;
  193. });
  194. this.items = obj;
  195. } else {
  196. this.newFrame(duration);
  197. }
  198. return this;
  199. }
  200. public setId(id?: number | string) {
  201. const state = this.state;
  202. state.id = id || makeId(!!length);
  203. const elements = this.elements;
  204. if (elements.length && !state[SELECTOR]) {
  205. const sceneId = toId(this.getId());
  206. state[SELECTOR] = `[${DATA_SCENE_ID}="${sceneId}"]`;
  207. elements.forEach(element => {
  208. element.setAttribute(DATA_SCENE_ID, sceneId);
  209. });
  210. }
  211. return this;
  212. }
  213. /**
  214. * Set properties to the sceneItem at that time
  215. * @param {Number} time - time
  216. * @param {...String|Object} [properties] - property names or values
  217. * @return {SceneItem} An instance itself
  218. * @example
  219. item.set(0, "a", "b") // item.getFrame(0).set("a", "b")
  220. console.log(item.get(0, "a")); // "b"
  221. */
  222. public set(time: any, ...args: any[]) {
  223. if (time instanceof SceneItem) {
  224. return this.set(0, time);
  225. } else if (isArray(time)) {
  226. const length = time.length;
  227. for (let i = 0; i < length; ++i) {
  228. const t = length === 1 ? 0 : this.getUnitTime(`${i / (length - 1) * 100}%`);
  229. this.set(t, time[i]);
  230. }
  231. } else if (isObject(time)) {
  232. for (const t in time) {
  233. const value = time[t];
  234. splitComma(t).forEach(eachTime => {
  235. const realTime = this.getUnitTime(eachTime);
  236. if (isNaN(realTime)) {
  237. getNames(value, [eachTime]).forEach(names => {
  238. const innerValue = getValueByNames(names.slice(1), value);
  239. const arr = isArray(innerValue) ?
  240. innerValue : [getValueByNames(names, this.target), innerValue];
  241. const length = arr.length;
  242. for (let i = 0; i < length; ++i) {
  243. this.newFrame(`${i / (length - 1) * 100}%`).set(...names, arr[i]);
  244. }
  245. });
  246. } else {
  247. this.set(realTime, value);
  248. }
  249. });
  250. }
  251. } else if (!isUndefined(time)) {
  252. const value = args[0];
  253. splitComma(time + "").forEach(eachTime => {
  254. const realTime = this.getUnitTime(eachTime);
  255. if (value instanceof SceneItem) {
  256. const delay = value.getDelay();
  257. const frames = value.toObject(!this.hasFrame(realTime + delay));
  258. const duration = value.getDuration();
  259. const direction = value.getDirection();
  260. const isReverse = direction.indexOf("reverse") > -1;
  261. for (const frameTime in frames) {
  262. const nextTime = isReverse ? duration - parseFloat(frameTime) : parseFloat(frameTime);
  263. this.set(realTime + nextTime, frames[frameTime]);
  264. }
  265. } else if (args.length === 1 && isArray(value)) {
  266. value.forEach((item: any) => {
  267. this.set(realTime, item);
  268. });
  269. } else {
  270. const frame = this.newFrame(realTime);
  271. frame.set(...args);
  272. }
  273. });
  274. }
  275. this.needUpdate = true;
  276. return this;
  277. }
  278. /**
  279. * Get properties of the sceneItem at that time
  280. * @param {Number} time - time
  281. * @param {...String|Object} args property's name or properties
  282. * @return {Number|String|PropertyObejct} property value
  283. * @example
  284. item.get(0, "a"); // item.getFrame(0).get("a");
  285. item.get(0, "transform", "translate"); // item.getFrame(0).get("transform", "translate");
  286. */
  287. public get(time: string | number, ...args: NameType[]) {
  288. const frame = this.getFrame(time);
  289. return frame && frame.get(...args);
  290. }
  291. public remove(time: string | number, ...args: any[]): this;
  292. /**
  293. * remove properties to the sceneItem at that time
  294. * @param {Number} time - time
  295. * @param {...String|Object} [properties] - property names or values
  296. * @return {SceneItem} An instance itself
  297. * @example
  298. item.remove(0, "a");
  299. */
  300. public remove(time: string | number, ...args: NameType[]) {
  301. if (args.length) {
  302. const frame = this.getFrame(time);
  303. frame && frame.remove(...args);
  304. } else {
  305. this.removeFrame(time);
  306. }
  307. this.needUpdate = true;
  308. return this;
  309. }
  310. /**
  311. * Append the item or object at the last time.
  312. * @param - the scene item or item object
  313. * @return An instance itself
  314. * @example
  315. item.append(new SceneItem({
  316. 0: {
  317. opacity: 0,
  318. },
  319. 1: {
  320. opacity: 1,
  321. }
  322. }));
  323. item.append({
  324. 0: {
  325. opacity: 0,
  326. },
  327. 1: {
  328. opacity: 1,
  329. }
  330. });
  331. item.set(item.getDuration(), {
  332. 0: {
  333. opacity: 0,
  334. },
  335. 1: {
  336. opacity: 1,
  337. }
  338. });
  339. */
  340. public append(item: SceneItem | IObject<any>) {
  341. if (item instanceof SceneItem) {
  342. this.set(this.getDuration(), item);
  343. } else {
  344. this.append(new SceneItem(item));
  345. }
  346. return this;
  347. }
  348. /**
  349. * Push the front frames for the time and prepend the scene item or item object.
  350. * @param - the scene item or item object
  351. * @return An instance itself
  352. */
  353. public prepend(item: SceneItem | IObject<any>) {
  354. if (item instanceof SceneItem) {
  355. const unshiftTime = item.getDuration() + item.getDelay();
  356. const firstFrame = this.getFrame(0);
  357. // remove first frame
  358. this.removeFrame(0);
  359. this.unshift(unshiftTime);
  360. this.set(0, item);
  361. this.set(unshiftTime + THRESHOLD, firstFrame);
  362. } else {
  363. this.prepend(new SceneItem(item));
  364. }
  365. return this;
  366. }
  367. /**
  368. * Push out the amount of time.
  369. * @param - time to push
  370. * @example
  371. item.get(0); // frame 0
  372. item.unshift(3);
  373. item.get(3) // frame 0
  374. */
  375. public unshift(time: number) {
  376. const { times, items } = this;
  377. const obj: IObject<Frame> = {};
  378. this.times = times.map(t => {
  379. const time2 = toFixed(time + t);
  380. obj[time2] = items[t];
  381. return time2;
  382. });
  383. this.items = obj;
  384. return this;
  385. }
  386. /**
  387. * Get the frames in the item in object form.
  388. * @return {}
  389. * @example
  390. item.toObject();
  391. // {0: {display: "none"}, 1: {display: "block"}}
  392. */
  393. public toObject(isStartZero = true): IObject<Frame> {
  394. const obj: IObject<Frame> = {};
  395. const delay = this.getDelay();
  396. this.forEach((frame: Frame, time: number) => {
  397. obj[(!time && !isStartZero ? THRESHOLD : 0) + delay + time] = frame.clone();
  398. });
  399. return obj;
  400. }
  401. /**
  402. * Specifies an element to synchronize items' keyframes.
  403. * @param {string} selectors - Selectors to find elements in items.
  404. * @return {SceneItem} An instance itself
  405. * @example
  406. item.setSelector("#id.class");
  407. */
  408. public setSelector(target: string | boolean | ((id: number | string) => string)) {
  409. if (isFunction(target)) {
  410. this.setElement(target(this.getId()));
  411. } else {
  412. this.setElement(target);
  413. }
  414. return this;
  415. }
  416. /**
  417. * Get the elements connected to SceneItem.
  418. */
  419. public getElements(): AnimateElement[] {
  420. return this.elements;
  421. }
  422. /**
  423. * Specifies an element to synchronize item's keyframes.
  424. * @param - elements to synchronize item's keyframes.
  425. * @param - Make sure that you have peusdo.
  426. * @return {SceneItem} An instance itself
  427. * @example
  428. item.setElement(document.querySelector("#id.class"));
  429. item.setElement(document.querySelectorAll(".class"));
  430. */
  431. public setElements(target: boolean | string | AnimateElement | IArrayFormat<AnimateElement>): this {
  432. return this.setElement(target);
  433. }
  434. /**
  435. * Specifies an element to synchronize item's keyframes.
  436. * @param - elements to synchronize item's keyframes.
  437. * @param - Make sure that you have peusdo.
  438. * @return {SceneItem} An instance itself
  439. * @example
  440. item.setElement(document.querySelector("#id.class"));
  441. item.setElement(document.querySelectorAll(".class"));
  442. */
  443. public setElement(target: boolean | string | AnimateElement | IArrayFormat<AnimateElement>) {
  444. const state = this.state;
  445. let elements: AnimateElement[] = [];
  446. if (!target) {
  447. return this;
  448. } else if (target === true || isString(target)) {
  449. const selector = target === true ? `${state.id}` : target;
  450. const matches = /([\s\S]+)(:+[a-zA-Z]+)$/g.exec(selector);
  451. elements = toArray($(matches ? matches[1] : selector, true));
  452. state[SELECTOR] = selector;
  453. } else {
  454. elements = (target instanceof Element) ? [target] : toArray(target);
  455. }
  456. if (!elements.length) {
  457. return this;
  458. }
  459. this.elements = elements;
  460. this.setId(this.getId());
  461. this.target = elements[0].style;
  462. this.targetFunc = (frame: Frame) => {
  463. const attributes = frame.get("attribute");
  464. if (attributes) {
  465. for (const name in attributes) {
  466. elements.forEach(el => {
  467. el.setAttribute(name, attributes[name]);
  468. });
  469. }
  470. }
  471. if (frame.has("html")) {
  472. const html = frame.get("html");
  473. elements.forEach(el => {
  474. el.innerHTML = html;
  475. });
  476. }
  477. const cssText = frame.toCSS();
  478. if (state.cssText !== cssText) {
  479. state.cssText = cssText;
  480. elements.forEach(el => {
  481. el.style.cssText += cssText;
  482. });
  483. return frame;
  484. }
  485. };
  486. return this;
  487. }
  488. public setTarget(target: any): this {
  489. this.target = target;
  490. this.targetFunc = (frame: Frame) => {
  491. const obj = frame.get();
  492. for (const name in obj) {
  493. target[name] = obj[name];
  494. }
  495. };
  496. return this;
  497. }
  498. /**
  499. * add css styles of items's element to the frame at that time.
  500. * @param {Array} properties - elements to synchronize item's keyframes.
  501. * @return {SceneItem} An instance itself
  502. * @example
  503. item.setElement(document.querySelector("#id.class"));
  504. item.setCSS(0, ["opacity"]);
  505. item.setCSS(0, ["opacity", "width", "height"]);
  506. */
  507. public setCSS(time: number, properties: string[]) {
  508. this.set(time, fromCSS(this.elements, properties));
  509. return this;
  510. }
  511. public setTime(time: number | string, isTick?: boolean, isParent?: boolean, parentEasing?: EasingType) {
  512. super.setTime(time, isTick, isParent);
  513. const iterationTime = this.getIterationTime();
  514. const easing = this.getEasing() || parentEasing;
  515. const frame = this.getNowFrame(iterationTime, easing);
  516. const currentTime = this.getTime();
  517. this.temp = frame;
  518. /**
  519. * This event is fired when timeupdate and animate.
  520. * @event SceneItem#animate
  521. * @param {Number} param.currentTime The total time that the animator is running.
  522. * @param {Number} param.time The iteration time during duration that the animator is running.
  523. * @param {Frame} param.frame frame of that time.
  524. */
  525. this.trigger("animate", {
  526. frame,
  527. currentTime,
  528. time: iterationTime,
  529. });
  530. this.targetFunc && this.targetFunc(frame);
  531. return this;
  532. }
  533. /**
  534. * update property names used in frames.
  535. * @return {SceneItem} An instance itself
  536. * @example
  537. item.update();
  538. */
  539. public update() {
  540. const names = {};
  541. this.forEach(frame => {
  542. updateFrame(names, frame.properties);
  543. });
  544. this.names = names;
  545. this.needUpdate = false;
  546. return this;
  547. }
  548. /**
  549. * Create and add a frame to the sceneItem at that time
  550. * @param {Number} time - frame's time
  551. * @return {Frame} Created frame.
  552. * @example
  553. item.newFrame(time);
  554. */
  555. public newFrame(time: string | number) {
  556. let frame = this.getFrame(time);
  557. if (frame) {
  558. return frame;
  559. }
  560. frame = new Frame();
  561. this.setFrame(time, frame);
  562. return frame;
  563. }
  564. /**
  565. * Add a frame to the sceneItem at that time
  566. * @param {Number} time - frame's time
  567. * @return {SceneItem} An instance itself
  568. * @example
  569. item.setFrame(time, frame);
  570. */
  571. public setFrame(time: string | number, frame: Frame) {
  572. const realTime = this.getUnitTime(time);
  573. this.items[realTime] = frame;
  574. addTime(this.times, realTime);
  575. this.needUpdate = true;
  576. return this;
  577. }
  578. public getFrame(time: number | string, ...names: any[]): Frame;
  579. /**
  580. * get sceneItem's frame at that time
  581. * @param {Number} time - frame's time
  582. * @return {Frame} sceneItem's frame at that time
  583. * @example
  584. const frame = item.getFrame(time);
  585. */
  586. public getFrame(time: number | string) {
  587. return this.items[this.getUnitTime(time)];
  588. }
  589. public removeFrame(time: number | string, ...names: any[]): this;
  590. /**
  591. * remove sceneItem's frame at that time
  592. * @param - frame's time
  593. * @return {SceneItem} An instance itself
  594. * @example
  595. item.removeFrame(time);
  596. */
  597. public removeFrame(time: number | string) {
  598. const realTime = this.getUnitTime(time);
  599. const items = this.items;
  600. const index = this.times.indexOf(realTime);
  601. delete items[realTime];
  602. // remove time
  603. if (index > -1) {
  604. this.times.splice(index, 1);
  605. }
  606. this.needUpdate = true;
  607. return this;
  608. }
  609. /**
  610. * check if the item has a frame at that time
  611. * @param {Number} time - frame's time
  612. * @return {Boolean} true: the item has a frame // false: not
  613. * @example
  614. if (item.hasFrame(10)) {
  615. // has
  616. } else {
  617. // not
  618. }
  619. */
  620. public hasFrame(time: number | string) {
  621. return this.getUnitTime(time) in this.items;
  622. }
  623. /**
  624. * Check if keyframes has propery's name
  625. * @param - property's time
  626. * @return {boolean} true: if has property, false: not
  627. * @example
  628. item.hasName(["transform", "translate"]); // true or not
  629. */
  630. public hasName(args: string[]) {
  631. this.needUpdate && this.update();
  632. return isInProperties(this.names, args, true);
  633. }
  634. /**
  635. * merge frame of the previous time at the next time.
  636. * @param - The time of the frame to merge
  637. * @param - The target frame
  638. * @return {SceneItem} An instance itself
  639. * @example
  640. // getFrame(1) contains getFrame(0)
  641. item.merge(0, 1);
  642. */
  643. public mergeFrame(time: number | string, frame: Frame) {
  644. if (frame) {
  645. const toFrame = this.newFrame(time);
  646. toFrame.merge(frame);
  647. }
  648. return this;
  649. }
  650. /**
  651. * Get frame of the current time
  652. * @param {Number} time - the current time
  653. * @param {function} easing - the speed curve of an animation
  654. * @return {Frame} frame of the current time
  655. * @example
  656. let item = new SceneItem({
  657. 0: {
  658. display: "none",
  659. },
  660. 1: {
  661. display: "block",
  662. opacity: 0,
  663. },
  664. 2: {
  665. opacity: 1,
  666. }
  667. });
  668. // opacity: 0.7; display:"block";
  669. const frame = item.getNowFrame(1.7);
  670. */
  671. public getNowFrame(time: number, easing?: EasingType, isAccurate?: boolean) {
  672. this.needUpdate && this.update();
  673. const frame = new Frame();
  674. const [left, right] = getNearTimeIndex(this.times, time);
  675. let realEasing = this.getEasing() || easing;
  676. let nameObject = this.names;
  677. if (this.hasName([TIMING_FUNCTION])) {
  678. const nowEasing = this.getNowValue(time, [TIMING_FUNCTION], left, right, false, 0, true);
  679. isFunction(nowEasing) && (realEasing = nowEasing);
  680. }
  681. if (isAccurate) {
  682. const prevFrame = this.getFrame(time);
  683. const prevNames = updateFrame({}, prevFrame.properties);
  684. for (const name in ROLES) {
  685. if (name in prevNames) {
  686. prevNames[name] = nameObject[name];
  687. }
  688. }
  689. nameObject = prevNames;
  690. }
  691. const names = getNames(nameObject, []);
  692. names.forEach(properties => {
  693. const value = this.getNowValue(time, properties, left, right, isAccurate, realEasing, isFixed(properties));
  694. if (isUndefined(value)) {
  695. return;
  696. }
  697. frame.set(properties, value);
  698. });
  699. return frame;
  700. }
  701. public load(properties: any = {}, options = properties.options) {
  702. options && this.setOptions(options);
  703. if (isArray(properties)) {
  704. this.set(properties);
  705. } else if (properties.keyframes) {
  706. this.set(properties.keyframes);
  707. } else {
  708. for (const time in properties) {
  709. if (time !== "options") {
  710. this.set({
  711. [time]: properties[time],
  712. });
  713. }
  714. }
  715. }
  716. if (options && options[DURATION]) {
  717. this.setDuration(options[DURATION]);
  718. }
  719. return this;
  720. }
  721. /**
  722. * clone SceneItem.
  723. * @return {SceneItem} An instance of clone
  724. * @example
  725. * item.clone();
  726. */
  727. public clone() {
  728. const item = new SceneItem();
  729. item.setOptions(this.state);
  730. this.forEach((frame: Frame, time: number) => {
  731. item.setFrame(time, frame.clone());
  732. });
  733. return item;
  734. }
  735. /**
  736. * executes a provided function once for each scene item.
  737. * @param - Function to execute for each element, taking three arguments
  738. * @return {Keyframes} An instance itself
  739. */
  740. public forEach(callback: (item: Frame, time: number, items: IObject<Frame>) => void) {
  741. const times = this.times;
  742. const items = this.items;
  743. times.forEach(time => {
  744. callback(items[time], time, items);
  745. });
  746. return this;
  747. }
  748. public setOptions(options: Partial<SceneItemOptions> = {}) {
  749. super.setOptions(options);
  750. const { id, selector, elements, element, target } = options;
  751. id && this.setId(id);
  752. if (target) {
  753. this.setTarget(target);
  754. } else if (selector) {
  755. this.setSelector(selector);
  756. } else if (elements || element) {
  757. this.setElement(elements || element);
  758. }
  759. return this;
  760. }
  761. public toCSS(
  762. playCondition: PlayCondition = { className: START_ANIMATION },
  763. parentDuration = this.getDuration(), states: AnimatorState[] = []) {
  764. const itemState = this.state;
  765. const selector = itemState[SELECTOR];
  766. if (!selector) {
  767. return "";
  768. }
  769. const originalDuration = this.getDuration();
  770. itemState[DURATION] = originalDuration;
  771. states.push(itemState);
  772. const reversedStates = toArray(states).reverse();
  773. const id = toId(getRealId(this));
  774. const superParent = states[0];
  775. const infiniteIndex = findIndex(reversedStates, state => {
  776. return state[ITERATION_COUNT] === INFINITE || !isFinite(state[DURATION]);
  777. }, states.length - 1);
  778. const finiteStates = reversedStates.slice(0, infiniteIndex);
  779. const duration = parentDuration || finiteStates.reduce((prev, cur) => {
  780. return (cur[DELAY] + prev * (cur[ITERATION_COUNT] as number)) / cur[PLAY_SPEED];
  781. }, originalDuration);
  782. const delay = reversedStates.slice(infiniteIndex).reduce((prev, cur) => {
  783. return (prev + cur[DELAY]) / cur[PLAY_SPEED];
  784. }, 0);
  785. const easingName = find(reversedStates, state => (state[EASING] && state[EASING_NAME]), itemState)[EASING_NAME];
  786. const iterationCount = reversedStates[infiniteIndex][ITERATION_COUNT];
  787. const fillMode = superParent[FILL_MODE];
  788. const direction = reversedStates[infiniteIndex][DIRECTION];
  789. const cssText = makeAnimationProperties({
  790. fillMode,
  791. direction,
  792. iterationCount,
  793. delay: `${delay}s`,
  794. name: `${PREFIX}KEYFRAMES_${id}`,
  795. duration: `${duration / superParent[PLAY_SPEED]}s`,
  796. timingFunction: easingName,
  797. });
  798. const selectors = splitComma(selector).map(sel => {
  799. const matches = /([\s\S]+)(:+[a-zA-Z]+)$/g.exec(sel);
  800. if (matches) {
  801. return [matches[1], matches[2]];
  802. } else {
  803. return [sel, ""];
  804. }
  805. });
  806. const className = playCondition.className;
  807. const selectorCallback = playCondition.selector;
  808. const preselector = isFunction(selectorCallback) ? selectorCallback(this, selector) : selectorCallback;
  809. return `
  810. ${preselector || selectors.map(([sel, peusdo]) => `${sel}.${className}${peusdo}`)} {${cssText}}
  811. ${selectors.map(([sel, peusdo]) => `${sel}.${PAUSE_ANIMATION}${peusdo}`)} {${ANIMATION}-play-state: paused;}
  812. @${KEYFRAMES} ${PREFIX}KEYFRAMES_${id}{${this._toKeyframes(duration, finiteStates, direction)}}`;
  813. }
  814. /**
  815. * Export the CSS of the items to the style.
  816. * @param - Add a selector or className to play.
  817. * @return {SceneItem} An instance itself
  818. */
  819. public exportCSS(
  820. playCondition?: PlayCondition,
  821. duration?: number, options?: AnimatorState[]) {
  822. if (!this.elements.length) {
  823. return "";
  824. }
  825. const css = this.toCSS(playCondition, duration, options);
  826. const isParent = options && !isUndefined(options[ITERATION_COUNT]);
  827. !isParent && exportCSS(getRealId(this), css);
  828. return this;
  829. }
  830. public pause() {
  831. super.pause();
  832. isPausedCSS(this) && this.pauseCSS();
  833. return this;
  834. }
  835. public pauseCSS() {
  836. this.elements.forEach(element => {
  837. addClass(element, PAUSE_ANIMATION);
  838. });
  839. return this;
  840. }
  841. public endCSS() {
  842. this.elements.forEach(element => {
  843. removeClass(element, PAUSE_ANIMATION);
  844. removeClass(element, START_ANIMATION);
  845. });
  846. setPlayCSS(this, false);
  847. return this;
  848. }
  849. public end() {
  850. isEndedCSS(this) && this.endCSS();
  851. super.end();
  852. return this;
  853. }
  854. /**
  855. * Play using the css animation and keyframes.
  856. * @param - Check if you want to export css.
  857. * @param [playClassName="startAnimation"] - Add a class name to play.
  858. * @param - The shorthand properties for six of the animation properties.
  859. * @see {@link https://www.w3schools.com/cssref/css3_pr_animation.asp}
  860. * @example
  861. item.playCSS();
  862. item.playCSS(false, "startAnimation", {
  863. direction: "reverse",
  864. fillMode: "forwards",
  865. });
  866. */
  867. public playCSS(isExportCSS = true, playClassName?: string, properties: object = {}) {
  868. playCSS(this, isExportCSS, playClassName, properties);
  869. return this;
  870. }
  871. public addPlayClass(isPaused: boolean, playClassName?: string, properties: object = {}) {
  872. const elements = this.elements;
  873. const length = elements.length;
  874. const cssText = makeAnimationProperties(properties);
  875. if (!length) {
  876. return;
  877. }
  878. if (isPaused) {
  879. elements.forEach(element => {
  880. removeClass(element, PAUSE_ANIMATION);
  881. });
  882. } else {
  883. elements.forEach(element => {
  884. element.style.cssText += cssText;
  885. if (hasClass(element, START_ANIMATION)) {
  886. removeClass(element, START_ANIMATION);
  887. requestAnimationFrame(() => {
  888. requestAnimationFrame(() => {
  889. addClass(element, START_ANIMATION);
  890. });
  891. });
  892. } else {
  893. addClass(element, START_ANIMATION);
  894. }
  895. });
  896. }
  897. return elements[0];
  898. }
  899. public getNowValue(
  900. time: number,
  901. properties: string[],
  902. left?: number,
  903. right?: number,
  904. isAccurate?: boolean,
  905. easing?: EasingType,
  906. usePrevValue?: boolean,
  907. ) {
  908. const times = this.times;
  909. const length = times.length;
  910. let prevTime: number;
  911. let nextTime: number;
  912. let prevFrame: Frame;
  913. let nextFrame: Frame;
  914. const isUndefinedLeft = isUndefined(left);
  915. const isUndefinedRight = isUndefined(right);
  916. if (isUndefinedLeft || isUndefinedRight) {
  917. const indicies = getNearTimeIndex(times, time);
  918. isUndefinedLeft && (left = indicies[0]);
  919. isUndefinedRight && (right = indicies[1]);
  920. }
  921. for (let i = left; i >= 0; --i) {
  922. const frame = this.getFrame(times[i]);
  923. if (frame.has(...properties)) {
  924. prevTime = times[i];
  925. prevFrame = frame;
  926. break;
  927. }
  928. }
  929. const prevValue = prevFrame && prevFrame.raw(...properties);
  930. if (isAccurate && !isRole([properties[0]])) {
  931. return prevTime === time ? prevValue : undefined;
  932. }
  933. if (usePrevValue) {
  934. return prevValue;
  935. }
  936. for (let i = right; i < length; ++i) {
  937. const frame = this.getFrame(times[i]);
  938. if (frame.has(...properties)) {
  939. nextTime = times[i];
  940. nextFrame = frame;
  941. break;
  942. }
  943. }
  944. const nextValue = nextFrame && nextFrame.raw(...properties);
  945. if (!prevFrame || isUndefined(prevValue)) {
  946. return nextValue;
  947. }
  948. if (!nextFrame || isUndefined(nextValue) || prevValue === nextValue) {
  949. return prevValue;
  950. }
  951. return dotValue(time, Math.max(prevTime, 0), nextTime, prevValue, nextValue, easing);
  952. }
  953. private _toKeyframes(duration: number, states: AnimatorState[], direction: DirectionType) {
  954. const frames: IObject<string> = {};
  955. const times = this.times.slice();
  956. if (!times.length) {
  957. return "";
  958. }
  959. const originalDuration = this.getDuration();
  960. (!this.getFrame(0)) && times.unshift(0);
  961. (!this.getFrame(originalDuration)) && times.push(originalDuration);
  962. const entries = getEntries(times, states);
  963. const lastEntry = entries[entries.length - 1];
  964. // end delay time
  965. lastEntry[0] < duration && addEntry(entries, duration, lastEntry[1]);
  966. let prevTime = -1;
  967. return entries.map(([time, keytime]) => {
  968. if (!frames[keytime]) {
  969. frames[keytime] =
  970. (!this.hasFrame(keytime) || keytime === 0 || keytime === originalDuration ?
  971. this.getNowFrame(keytime) : this.getNowFrame(keytime, 0, true)).toCSS();
  972. }
  973. let frameTime = time / duration * 100;
  974. if (frameTime - prevTime < THRESHOLD) {
  975. frameTime += THRESHOLD;
  976. }
  977. prevTime = frameTime;
  978. return `${Math.min(frameTime, 100)}%{
  979. ${time === 0 && !isDirectionReverse(0, 1, direction) ? "" : frames[keytime]}
  980. }`;
  981. }).join("");
  982. }
  983. }
  984. export default SceneItem;